github.com/vtorhonen/terraform@v0.9.0-beta2.0.20170307220345-5d894e4ffda7/builtin/providers/cloudstack/resource_cloudstack_firewall.go (about)

     1  package cloudstack
     2  
     3  import (
     4  	"fmt"
     5  	"strconv"
     6  	"strings"
     7  	"sync"
     8  	"time"
     9  
    10  	"github.com/hashicorp/go-multierror"
    11  	"github.com/hashicorp/terraform/helper/schema"
    12  	"github.com/xanzy/go-cloudstack/cloudstack"
    13  )
    14  
    15  func resourceCloudStackFirewall() *schema.Resource {
    16  	return &schema.Resource{
    17  		Create: resourceCloudStackFirewallCreate,
    18  		Read:   resourceCloudStackFirewallRead,
    19  		Update: resourceCloudStackFirewallUpdate,
    20  		Delete: resourceCloudStackFirewallDelete,
    21  
    22  		Schema: map[string]*schema.Schema{
    23  			"ip_address_id": &schema.Schema{
    24  				Type:     schema.TypeString,
    25  				Required: true,
    26  				ForceNew: true,
    27  			},
    28  
    29  			"managed": &schema.Schema{
    30  				Type:     schema.TypeBool,
    31  				Optional: true,
    32  				Default:  false,
    33  			},
    34  
    35  			"rule": &schema.Schema{
    36  				Type:     schema.TypeSet,
    37  				Optional: true,
    38  				Elem: &schema.Resource{
    39  					Schema: map[string]*schema.Schema{
    40  						"cidr_list": &schema.Schema{
    41  							Type:     schema.TypeSet,
    42  							Required: true,
    43  							Elem:     &schema.Schema{Type: schema.TypeString},
    44  							Set:      schema.HashString,
    45  						},
    46  
    47  						"protocol": &schema.Schema{
    48  							Type:     schema.TypeString,
    49  							Required: true,
    50  						},
    51  
    52  						"icmp_type": &schema.Schema{
    53  							Type:     schema.TypeInt,
    54  							Optional: true,
    55  							Computed: true,
    56  						},
    57  
    58  						"icmp_code": &schema.Schema{
    59  							Type:     schema.TypeInt,
    60  							Optional: true,
    61  							Computed: true,
    62  						},
    63  
    64  						"ports": &schema.Schema{
    65  							Type:     schema.TypeSet,
    66  							Optional: true,
    67  							Elem:     &schema.Schema{Type: schema.TypeString},
    68  							Set:      schema.HashString,
    69  						},
    70  
    71  						"uuids": &schema.Schema{
    72  							Type:     schema.TypeMap,
    73  							Computed: true,
    74  						},
    75  					},
    76  				},
    77  			},
    78  
    79  			"parallelism": &schema.Schema{
    80  				Type:     schema.TypeInt,
    81  				Optional: true,
    82  				Default:  2,
    83  			},
    84  		},
    85  	}
    86  }
    87  
    88  func resourceCloudStackFirewallCreate(d *schema.ResourceData, meta interface{}) error {
    89  	// Make sure all required parameters are there
    90  	if err := verifyFirewallParams(d); err != nil {
    91  		return err
    92  	}
    93  
    94  	// We need to set this upfront in order to be able to save a partial state
    95  	d.SetId(d.Get("ip_address_id").(string))
    96  
    97  	// Create all rules that are configured
    98  	if nrs := d.Get("rule").(*schema.Set); nrs.Len() > 0 {
    99  		// Create an empty schema.Set to hold all rules
   100  		rules := resourceCloudStackFirewall().Schema["rule"].ZeroValue().(*schema.Set)
   101  
   102  		err := createFirewallRules(d, meta, rules, nrs)
   103  
   104  		// We need to update this first to preserve the correct state
   105  		d.Set("rule", rules)
   106  
   107  		if err != nil {
   108  			return err
   109  		}
   110  	}
   111  
   112  	return resourceCloudStackFirewallRead(d, meta)
   113  }
   114  func createFirewallRules(d *schema.ResourceData, meta interface{}, rules *schema.Set, nrs *schema.Set) error {
   115  	var errs *multierror.Error
   116  
   117  	var wg sync.WaitGroup
   118  	wg.Add(nrs.Len())
   119  
   120  	sem := make(chan struct{}, d.Get("parallelism").(int))
   121  	for _, rule := range nrs.List() {
   122  		// Put in a tiny sleep here to avoid DoS'ing the API
   123  		time.Sleep(500 * time.Millisecond)
   124  
   125  		go func(rule map[string]interface{}) {
   126  			defer wg.Done()
   127  			sem <- struct{}{}
   128  
   129  			// Create a single rule
   130  			err := createFirewallRule(d, meta, rule)
   131  
   132  			// If we have at least one UUID, we need to save the rule
   133  			if len(rule["uuids"].(map[string]interface{})) > 0 {
   134  				rules.Add(rule)
   135  			}
   136  
   137  			if err != nil {
   138  				errs = multierror.Append(errs, err)
   139  			}
   140  
   141  			<-sem
   142  		}(rule.(map[string]interface{}))
   143  	}
   144  
   145  	wg.Wait()
   146  
   147  	return errs.ErrorOrNil()
   148  }
   149  
   150  func createFirewallRule(d *schema.ResourceData, meta interface{}, rule map[string]interface{}) error {
   151  	cs := meta.(*cloudstack.CloudStackClient)
   152  	uuids := rule["uuids"].(map[string]interface{})
   153  
   154  	// Make sure all required rule parameters are there
   155  	if err := verifyFirewallRuleParams(d, rule); err != nil {
   156  		return err
   157  	}
   158  
   159  	// Create a new parameter struct
   160  	p := cs.Firewall.NewCreateFirewallRuleParams(d.Id(), rule["protocol"].(string))
   161  
   162  	// Set the CIDR list
   163  	var cidrList []string
   164  	for _, cidr := range rule["cidr_list"].(*schema.Set).List() {
   165  		cidrList = append(cidrList, cidr.(string))
   166  	}
   167  	p.SetCidrlist(cidrList)
   168  
   169  	// If the protocol is ICMP set the needed ICMP parameters
   170  	if rule["protocol"].(string) == "icmp" {
   171  		p.SetIcmptype(rule["icmp_type"].(int))
   172  		p.SetIcmpcode(rule["icmp_code"].(int))
   173  
   174  		r, err := cs.Firewall.CreateFirewallRule(p)
   175  		if err != nil {
   176  			return err
   177  		}
   178  
   179  		uuids["icmp"] = r.Id
   180  		rule["uuids"] = uuids
   181  	}
   182  
   183  	// If protocol is not ICMP, loop through all ports
   184  	if rule["protocol"].(string) != "icmp" {
   185  		if ps := rule["ports"].(*schema.Set); ps.Len() > 0 {
   186  
   187  			// Create an empty schema.Set to hold all processed ports
   188  			ports := &schema.Set{F: schema.HashString}
   189  
   190  			for _, port := range ps.List() {
   191  				if _, ok := uuids[port.(string)]; ok {
   192  					ports.Add(port)
   193  					rule["ports"] = ports
   194  					continue
   195  				}
   196  
   197  				m := splitPorts.FindStringSubmatch(port.(string))
   198  
   199  				startPort, err := strconv.Atoi(m[1])
   200  				if err != nil {
   201  					return err
   202  				}
   203  
   204  				endPort := startPort
   205  				if m[2] != "" {
   206  					endPort, err = strconv.Atoi(m[2])
   207  					if err != nil {
   208  						return err
   209  					}
   210  				}
   211  
   212  				p.SetStartport(startPort)
   213  				p.SetEndport(endPort)
   214  
   215  				r, err := cs.Firewall.CreateFirewallRule(p)
   216  				if err != nil {
   217  					return err
   218  				}
   219  
   220  				ports.Add(port)
   221  				rule["ports"] = ports
   222  
   223  				uuids[port.(string)] = r.Id
   224  				rule["uuids"] = uuids
   225  			}
   226  		}
   227  	}
   228  
   229  	return nil
   230  }
   231  
   232  func resourceCloudStackFirewallRead(d *schema.ResourceData, meta interface{}) error {
   233  	cs := meta.(*cloudstack.CloudStackClient)
   234  
   235  	// Get all the rules from the running environment
   236  	p := cs.Firewall.NewListFirewallRulesParams()
   237  	p.SetIpaddressid(d.Id())
   238  	p.SetListall(true)
   239  
   240  	l, err := cs.Firewall.ListFirewallRules(p)
   241  	if err != nil {
   242  		return err
   243  	}
   244  
   245  	// Make a map of all the rules so we can easily find a rule
   246  	ruleMap := make(map[string]*cloudstack.FirewallRule, l.Count)
   247  	for _, r := range l.FirewallRules {
   248  		ruleMap[r.Id] = r
   249  	}
   250  
   251  	// Create an empty schema.Set to hold all rules
   252  	rules := resourceCloudStackFirewall().Schema["rule"].ZeroValue().(*schema.Set)
   253  
   254  	// Read all rules that are configured
   255  	if rs := d.Get("rule").(*schema.Set); rs.Len() > 0 {
   256  		for _, rule := range rs.List() {
   257  			rule := rule.(map[string]interface{})
   258  			uuids := rule["uuids"].(map[string]interface{})
   259  
   260  			if rule["protocol"].(string) == "icmp" {
   261  				id, ok := uuids["icmp"]
   262  				if !ok {
   263  					continue
   264  				}
   265  
   266  				// Get the rule
   267  				r, ok := ruleMap[id.(string)]
   268  				if !ok {
   269  					delete(uuids, "icmp")
   270  					continue
   271  				}
   272  
   273  				// Delete the known rule so only unknown rules remain in the ruleMap
   274  				delete(ruleMap, id.(string))
   275  
   276  				// Create a set with all CIDR's
   277  				cidrs := &schema.Set{F: schema.HashString}
   278  				for _, cidr := range strings.Split(r.Cidrlist, ",") {
   279  					cidrs.Add(cidr)
   280  				}
   281  
   282  				// Update the values
   283  				rule["protocol"] = r.Protocol
   284  				rule["icmp_type"] = r.Icmptype
   285  				rule["icmp_code"] = r.Icmpcode
   286  				rule["cidr_list"] = cidrs
   287  				rules.Add(rule)
   288  			}
   289  
   290  			// If protocol is not ICMP, loop through all ports
   291  			if rule["protocol"].(string) != "icmp" {
   292  				if ps := rule["ports"].(*schema.Set); ps.Len() > 0 {
   293  
   294  					// Create an empty schema.Set to hold all ports
   295  					ports := &schema.Set{F: schema.HashString}
   296  
   297  					// Loop through all ports and retrieve their info
   298  					for _, port := range ps.List() {
   299  						id, ok := uuids[port.(string)]
   300  						if !ok {
   301  							continue
   302  						}
   303  
   304  						// Get the rule
   305  						r, ok := ruleMap[id.(string)]
   306  						if !ok {
   307  							delete(uuids, port.(string))
   308  							continue
   309  						}
   310  
   311  						// Delete the known rule so only unknown rules remain in the ruleMap
   312  						delete(ruleMap, id.(string))
   313  
   314  						// Create a set with all CIDR's
   315  						cidrs := &schema.Set{F: schema.HashString}
   316  						for _, cidr := range strings.Split(r.Cidrlist, ",") {
   317  							cidrs.Add(cidr)
   318  						}
   319  
   320  						// Update the values
   321  						rule["protocol"] = r.Protocol
   322  						rule["cidr_list"] = cidrs
   323  						ports.Add(port)
   324  					}
   325  
   326  					// If there is at least one port found, add this rule to the rules set
   327  					if ports.Len() > 0 {
   328  						rule["ports"] = ports
   329  						rules.Add(rule)
   330  					}
   331  				}
   332  			}
   333  		}
   334  	}
   335  
   336  	// If this is a managed firewall, add all unknown rules into a single dummy rule
   337  	managed := d.Get("managed").(bool)
   338  	if managed && len(ruleMap) > 0 {
   339  		for uuid := range ruleMap {
   340  			// We need to create and add a dummy value to a schema.Set as the
   341  			// cidr_list is a required field and thus needs a value
   342  			cidrs := &schema.Set{F: schema.HashString}
   343  			cidrs.Add(uuid)
   344  
   345  			// Make a dummy rule to hold the unknown UUID
   346  			rule := map[string]interface{}{
   347  				"cidr_list": cidrs,
   348  				"protocol":  uuid,
   349  				"uuids":     map[string]interface{}{uuid: uuid},
   350  			}
   351  
   352  			// Add the dummy rule to the rules set
   353  			rules.Add(rule)
   354  		}
   355  	}
   356  
   357  	if rules.Len() > 0 {
   358  		d.Set("rule", rules)
   359  	} else if !managed {
   360  		d.SetId("")
   361  	}
   362  
   363  	return nil
   364  }
   365  
   366  func resourceCloudStackFirewallUpdate(d *schema.ResourceData, meta interface{}) error {
   367  	// Make sure all required parameters are there
   368  	if err := verifyFirewallParams(d); err != nil {
   369  		return err
   370  	}
   371  
   372  	// Check if the rule set as a whole has changed
   373  	if d.HasChange("rule") {
   374  		o, n := d.GetChange("rule")
   375  		ors := o.(*schema.Set).Difference(n.(*schema.Set))
   376  		nrs := n.(*schema.Set).Difference(o.(*schema.Set))
   377  
   378  		// We need to start with a rule set containing all the rules we
   379  		// already have and want to keep. Any rules that are not deleted
   380  		// correctly and any newly created rules, will be added to this
   381  		// set to make sure we end up in a consistent state
   382  		rules := o.(*schema.Set).Intersection(n.(*schema.Set))
   383  
   384  		// First loop through all the old rules and delete them
   385  		if ors.Len() > 0 {
   386  			err := deleteFirewallRules(d, meta, rules, ors)
   387  
   388  			// We need to update this first to preserve the correct state
   389  			d.Set("rule", rules)
   390  
   391  			if err != nil {
   392  				return err
   393  			}
   394  		}
   395  
   396  		// Then loop through all the new rules and create them
   397  		if nrs.Len() > 0 {
   398  			err := createFirewallRules(d, meta, rules, nrs)
   399  
   400  			// We need to update this first to preserve the correct state
   401  			d.Set("rule", rules)
   402  
   403  			if err != nil {
   404  				return err
   405  			}
   406  		}
   407  	}
   408  
   409  	return resourceCloudStackFirewallRead(d, meta)
   410  }
   411  
   412  func resourceCloudStackFirewallDelete(d *schema.ResourceData, meta interface{}) error {
   413  	// Create an empty rule set to hold all rules that where
   414  	// not deleted correctly
   415  	rules := resourceCloudStackFirewall().Schema["rule"].ZeroValue().(*schema.Set)
   416  
   417  	// Delete all rules
   418  	if ors := d.Get("rule").(*schema.Set); ors.Len() > 0 {
   419  		err := deleteFirewallRules(d, meta, rules, ors)
   420  
   421  		// We need to update this first to preserve the correct state
   422  		d.Set("rule", rules)
   423  
   424  		if err != nil {
   425  			return err
   426  		}
   427  	}
   428  
   429  	return nil
   430  }
   431  
   432  func deleteFirewallRules(d *schema.ResourceData, meta interface{}, rules *schema.Set, ors *schema.Set) error {
   433  	var errs *multierror.Error
   434  
   435  	var wg sync.WaitGroup
   436  	wg.Add(ors.Len())
   437  
   438  	sem := make(chan struct{}, d.Get("parallelism").(int))
   439  	for _, rule := range ors.List() {
   440  		// Put a sleep here to avoid DoS'ing the API
   441  		time.Sleep(500 * time.Millisecond)
   442  
   443  		go func(rule map[string]interface{}) {
   444  			defer wg.Done()
   445  			sem <- struct{}{}
   446  
   447  			// Delete a single rule
   448  			err := deleteFirewallRule(d, meta, rule)
   449  
   450  			// If we have at least one UUID, we need to save the rule
   451  			if len(rule["uuids"].(map[string]interface{})) > 0 {
   452  				rules.Add(rule)
   453  			}
   454  
   455  			if err != nil {
   456  				errs = multierror.Append(errs, err)
   457  			}
   458  
   459  			<-sem
   460  		}(rule.(map[string]interface{}))
   461  	}
   462  
   463  	wg.Wait()
   464  
   465  	return errs.ErrorOrNil()
   466  }
   467  
   468  func deleteFirewallRule(d *schema.ResourceData, meta interface{}, rule map[string]interface{}) error {
   469  	cs := meta.(*cloudstack.CloudStackClient)
   470  	uuids := rule["uuids"].(map[string]interface{})
   471  
   472  	for k, id := range uuids {
   473  		// We don't care about the count here, so just continue
   474  		if k == "%" {
   475  			continue
   476  		}
   477  
   478  		// Create the parameter struct
   479  		p := cs.Firewall.NewDeleteFirewallRuleParams(id.(string))
   480  
   481  		// Delete the rule
   482  		if _, err := cs.Firewall.DeleteFirewallRule(p); err != nil {
   483  
   484  			// This is a very poor way to be told the ID does no longer exist :(
   485  			if strings.Contains(err.Error(), fmt.Sprintf(
   486  				"Invalid parameter id value=%s due to incorrect long value format, "+
   487  					"or entity does not exist", id.(string))) {
   488  				delete(uuids, k)
   489  				continue
   490  			}
   491  
   492  			return err
   493  		}
   494  
   495  		// Delete the UUID of this rule
   496  		delete(uuids, k)
   497  		rule["uuids"] = uuids
   498  	}
   499  
   500  	return nil
   501  }
   502  
   503  func verifyFirewallParams(d *schema.ResourceData) error {
   504  	managed := d.Get("managed").(bool)
   505  	_, rules := d.GetOk("rule")
   506  
   507  	if !rules && !managed {
   508  		return fmt.Errorf(
   509  			"You must supply at least one 'rule' when not using the 'managed' firewall feature")
   510  	}
   511  
   512  	return nil
   513  }
   514  
   515  func verifyFirewallRuleParams(d *schema.ResourceData, rule map[string]interface{}) error {
   516  	protocol := rule["protocol"].(string)
   517  	if protocol != "tcp" && protocol != "udp" && protocol != "icmp" {
   518  		return fmt.Errorf(
   519  			"%q is not a valid protocol. Valid options are 'tcp', 'udp' and 'icmp'", protocol)
   520  	}
   521  
   522  	if protocol == "icmp" {
   523  		if _, ok := rule["icmp_type"]; !ok {
   524  			return fmt.Errorf(
   525  				"Parameter icmp_type is a required parameter when using protocol 'icmp'")
   526  		}
   527  		if _, ok := rule["icmp_code"]; !ok {
   528  			return fmt.Errorf(
   529  				"Parameter icmp_code is a required parameter when using protocol 'icmp'")
   530  		}
   531  	} else {
   532  		if ports, ok := rule["ports"].(*schema.Set); ok {
   533  			for _, port := range ports.List() {
   534  				m := splitPorts.FindStringSubmatch(port.(string))
   535  				if m == nil {
   536  					return fmt.Errorf(
   537  						"%q is not a valid port value. Valid options are '80' or '80-90'", port.(string))
   538  				}
   539  			}
   540  		} else {
   541  			return fmt.Errorf(
   542  				"Parameter ports is a required parameter when *not* using protocol 'icmp'")
   543  		}
   544  	}
   545  
   546  	return nil
   547  }