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