github.com/jrasell/terraform@v0.6.17-0.20160523115548-2652f5232949/builtin/providers/cloudstack/resource_cloudstack_firewall.go (about)

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