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