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

     1  package cloudstack
     2  
     3  import (
     4  	"fmt"
     5  	"log"
     6  	"strconv"
     7  	"strings"
     8  	"sync"
     9  	"time"
    10  
    11  	multierror "github.com/hashicorp/go-multierror"
    12  	"github.com/hashicorp/terraform/helper/schema"
    13  	"github.com/xanzy/go-cloudstack/cloudstack"
    14  )
    15  
    16  type authorizeSecurityGroupParams interface {
    17  	SetCidrlist([]string)
    18  	SetIcmptype(int)
    19  	SetIcmpcode(int)
    20  	SetStartport(int)
    21  	SetEndport(int)
    22  	SetProtocol(string)
    23  	SetSecuritygroupid(string)
    24  	SetUsersecuritygrouplist(map[string]string)
    25  }
    26  
    27  func resourceCloudStackSecurityGroupRule() *schema.Resource {
    28  	return &schema.Resource{
    29  		Create: resourceCloudStackSecurityGroupRuleCreate,
    30  		Read:   resourceCloudStackSecurityGroupRuleRead,
    31  		Update: resourceCloudStackSecurityGroupRuleUpdate,
    32  		Delete: resourceCloudStackSecurityGroupRuleDelete,
    33  
    34  		Schema: map[string]*schema.Schema{
    35  			"security_group_id": &schema.Schema{
    36  				Type:     schema.TypeString,
    37  				Required: true,
    38  				ForceNew: true,
    39  			},
    40  
    41  			"rule": &schema.Schema{
    42  				Type:     schema.TypeSet,
    43  				Required: true,
    44  				Elem: &schema.Resource{
    45  					Schema: map[string]*schema.Schema{
    46  						"cidr_list": &schema.Schema{
    47  							Type:     schema.TypeSet,
    48  							Optional: true,
    49  							Elem:     &schema.Schema{Type: schema.TypeString},
    50  							Set:      schema.HashString,
    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  						"traffic_type": &schema.Schema{
    78  							Type:     schema.TypeString,
    79  							Optional: true,
    80  							Default:  "ingress",
    81  						},
    82  
    83  						"user_security_group_list": &schema.Schema{
    84  							Type:     schema.TypeSet,
    85  							Optional: true,
    86  							Elem:     &schema.Schema{Type: schema.TypeString},
    87  							Set:      schema.HashString,
    88  						},
    89  
    90  						"uuids": &schema.Schema{
    91  							Type:     schema.TypeMap,
    92  							Computed: true,
    93  						},
    94  					},
    95  				},
    96  			},
    97  
    98  			"project": &schema.Schema{
    99  				Type:     schema.TypeString,
   100  				Optional: true,
   101  				ForceNew: true,
   102  			},
   103  
   104  			"parallelism": &schema.Schema{
   105  				Type:     schema.TypeInt,
   106  				Optional: true,
   107  				Default:  2,
   108  			},
   109  		},
   110  	}
   111  }
   112  
   113  func resourceCloudStackSecurityGroupRuleCreate(d *schema.ResourceData, meta interface{}) error {
   114  	// We need to set this upfront in order to be able to save a partial state
   115  	d.SetId(d.Get("security_group_id").(string))
   116  
   117  	// Create all rules that are configured
   118  	if nrs := d.Get("rule").(*schema.Set); nrs.Len() > 0 {
   119  		// Create an empty rule set to hold all newly created rules
   120  		rules := resourceCloudStackSecurityGroupRule().Schema["rule"].ZeroValue().(*schema.Set)
   121  
   122  		err := createSecurityGroupRules(d, meta, rules, nrs)
   123  
   124  		// We need to update this first to preserve the correct state
   125  		d.Set("rule", rules)
   126  
   127  		if err != nil {
   128  			return err
   129  		}
   130  	}
   131  
   132  	return resourceCloudStackSecurityGroupRuleRead(d, meta)
   133  }
   134  
   135  func createSecurityGroupRules(d *schema.ResourceData, meta interface{}, rules *schema.Set, nrs *schema.Set) error {
   136  	cs := meta.(*cloudstack.CloudStackClient)
   137  	var errs *multierror.Error
   138  
   139  	var wg sync.WaitGroup
   140  	wg.Add(nrs.Len())
   141  
   142  	sem := make(chan struct{}, d.Get("parallelism").(int))
   143  	for _, rule := range nrs.List() {
   144  		// Put in a tiny sleep here to avoid DoS'ing the API
   145  		time.Sleep(500 * time.Millisecond)
   146  
   147  		go func(rule map[string]interface{}) {
   148  			defer wg.Done()
   149  			sem <- struct{}{}
   150  
   151  			// Make sure all required parameters are there
   152  			if err := verifySecurityGroupRuleParams(d, rule); err != nil {
   153  				errs = multierror.Append(errs, err)
   154  				return
   155  			}
   156  
   157  			var p authorizeSecurityGroupParams
   158  
   159  			if cidrList, ok := rule["cidr_list"].(*schema.Set); ok && cidrList.Len() > 0 {
   160  				for _, cidr := range cidrList.List() {
   161  					// Create a new parameter struct
   162  					switch rule["traffic_type"].(string) {
   163  					case "ingress":
   164  						p = cs.SecurityGroup.NewAuthorizeSecurityGroupIngressParams()
   165  					case "egress":
   166  						p = cs.SecurityGroup.NewAuthorizeSecurityGroupEgressParams()
   167  					}
   168  
   169  					p.SetSecuritygroupid(d.Id())
   170  					p.SetCidrlist([]string{cidr.(string)})
   171  
   172  					// Create a single rule
   173  					err := createSecurityGroupRule(d, meta, rule, p, cidr.(string))
   174  					if err != nil {
   175  						errs = multierror.Append(errs, err)
   176  					}
   177  				}
   178  			}
   179  
   180  			if usgList, ok := rule["user_security_group_list"].(*schema.Set); ok && usgList.Len() > 0 {
   181  				for _, usg := range usgList.List() {
   182  					sg, _, err := cs.SecurityGroup.GetSecurityGroupByName(
   183  						usg.(string),
   184  						cloudstack.WithProject(d.Get("project").(string)),
   185  					)
   186  					if err != nil {
   187  						errs = multierror.Append(errs, err)
   188  						continue
   189  					}
   190  
   191  					// Create a new parameter struct
   192  					switch rule["traffic_type"].(string) {
   193  					case "ingress":
   194  						p = cs.SecurityGroup.NewAuthorizeSecurityGroupIngressParams()
   195  					case "egress":
   196  						p = cs.SecurityGroup.NewAuthorizeSecurityGroupEgressParams()
   197  					}
   198  
   199  					p.SetSecuritygroupid(d.Id())
   200  					p.SetUsersecuritygrouplist(map[string]string{sg.Account: usg.(string)})
   201  
   202  					// Create a single rule
   203  					err = createSecurityGroupRule(d, meta, rule, p, usg.(string))
   204  					if err != nil {
   205  						errs = multierror.Append(errs, err)
   206  					}
   207  				}
   208  			}
   209  
   210  			// If we have at least one UUID, we need to save the rule
   211  			if len(rule["uuids"].(map[string]interface{})) > 0 {
   212  				rules.Add(rule)
   213  			}
   214  
   215  			<-sem
   216  		}(rule.(map[string]interface{}))
   217  	}
   218  
   219  	wg.Wait()
   220  
   221  	return errs.ErrorOrNil()
   222  }
   223  
   224  func createSecurityGroupRule(d *schema.ResourceData, meta interface{}, rule map[string]interface{}, p authorizeSecurityGroupParams, uuid string) error {
   225  	cs := meta.(*cloudstack.CloudStackClient)
   226  	uuids := rule["uuids"].(map[string]interface{})
   227  
   228  	// Set the protocol
   229  	p.SetProtocol(rule["protocol"].(string))
   230  
   231  	// If the protocol is ICMP set the needed ICMP parameters
   232  	if rule["protocol"].(string) == "icmp" {
   233  		p.SetIcmptype(rule["icmp_type"].(int))
   234  		p.SetIcmpcode(rule["icmp_code"].(int))
   235  
   236  		ruleID, err := createIngressOrEgressRule(cs, p)
   237  		if err != nil {
   238  			return err
   239  		}
   240  
   241  		uuids[uuid+"icmp"] = ruleID
   242  		rule["uuids"] = uuids
   243  	}
   244  
   245  	// If protocol is TCP or UDP, loop through all ports
   246  	if rule["protocol"].(string) == "tcp" || rule["protocol"].(string) == "udp" {
   247  		if ps := rule["ports"].(*schema.Set); ps.Len() > 0 {
   248  
   249  			// Create an empty schema.Set to hold all processed ports
   250  			ports := &schema.Set{F: schema.HashString}
   251  
   252  			for _, port := range ps.List() {
   253  				if _, ok := uuids[uuid+port.(string)]; ok {
   254  					ports.Add(port)
   255  					rule["ports"] = ports
   256  					continue
   257  				}
   258  
   259  				m := splitPorts.FindStringSubmatch(port.(string))
   260  
   261  				startPort, err := strconv.Atoi(m[1])
   262  				if err != nil {
   263  					return err
   264  				}
   265  
   266  				endPort := startPort
   267  				if m[2] != "" {
   268  					endPort, err = strconv.Atoi(m[2])
   269  					if err != nil {
   270  						return err
   271  					}
   272  				}
   273  
   274  				p.SetStartport(startPort)
   275  				p.SetEndport(endPort)
   276  
   277  				ruleID, err := createIngressOrEgressRule(cs, p)
   278  				if err != nil {
   279  					return err
   280  				}
   281  
   282  				ports.Add(port)
   283  				rule["ports"] = ports
   284  
   285  				uuids[uuid+port.(string)] = ruleID
   286  				rule["uuids"] = uuids
   287  			}
   288  		}
   289  	}
   290  
   291  	return nil
   292  }
   293  
   294  func createIngressOrEgressRule(cs *cloudstack.CloudStackClient, p authorizeSecurityGroupParams) (string, error) {
   295  	switch p := p.(type) {
   296  	case *cloudstack.AuthorizeSecurityGroupIngressParams:
   297  		r, err := cs.SecurityGroup.AuthorizeSecurityGroupIngress(p)
   298  		if err != nil {
   299  			return "", err
   300  		}
   301  		return r.Ruleid, nil
   302  	case *cloudstack.AuthorizeSecurityGroupEgressParams:
   303  		r, err := cs.SecurityGroup.AuthorizeSecurityGroupEgress(p)
   304  		if err != nil {
   305  			return "", err
   306  		}
   307  		return r.Ruleid, nil
   308  	default:
   309  		return "", fmt.Errorf("Unknown authorize security group rule type: %v", p)
   310  	}
   311  }
   312  
   313  func resourceCloudStackSecurityGroupRuleRead(d *schema.ResourceData, meta interface{}) error {
   314  	cs := meta.(*cloudstack.CloudStackClient)
   315  
   316  	// Get the security group details
   317  	sg, count, err := cs.SecurityGroup.GetSecurityGroupByID(
   318  		d.Id(),
   319  		cloudstack.WithProject(d.Get("project").(string)),
   320  	)
   321  	if err != nil {
   322  		if count == 0 {
   323  			log.Printf("[DEBUG] Security group %s does not longer exist", d.Get("name").(string))
   324  			d.SetId("")
   325  			return nil
   326  		}
   327  
   328  		return err
   329  	}
   330  
   331  	// Make a map of all the rule indexes so we can easily find a rule
   332  	sgRules := append(sg.Ingressrule, sg.Egressrule...)
   333  	ruleIndex := make(map[string]int, len(sgRules))
   334  	for idx, r := range sgRules {
   335  		ruleIndex[r.Ruleid] = idx
   336  	}
   337  
   338  	// Create an empty schema.Set to hold all rules
   339  	rules := resourceCloudStackSecurityGroupRule().Schema["rule"].ZeroValue().(*schema.Set)
   340  
   341  	// Read all rules that are configured
   342  	if rs := d.Get("rule").(*schema.Set); rs.Len() > 0 {
   343  		for _, rule := range rs.List() {
   344  			rule := rule.(map[string]interface{})
   345  
   346  			// First get any existing values
   347  			cidrList, cidrListOK := rule["cidr_list"].(*schema.Set)
   348  			usgList, usgListOk := rule["user_security_group_list"].(*schema.Set)
   349  
   350  			// Then reset the values to a new empty set
   351  			rule["cidr_list"] = &schema.Set{F: schema.HashString}
   352  			rule["user_security_group_list"] = &schema.Set{F: schema.HashString}
   353  
   354  			if cidrListOK && cidrList.Len() > 0 {
   355  				for _, cidr := range cidrList.List() {
   356  					readSecurityGroupRule(sg, ruleIndex, rule, cidr.(string))
   357  				}
   358  			}
   359  
   360  			if usgListOk && usgList.Len() > 0 {
   361  				for _, usg := range usgList.List() {
   362  					readSecurityGroupRule(sg, ruleIndex, rule, usg.(string))
   363  				}
   364  			}
   365  
   366  			rules.Add(rule)
   367  		}
   368  	}
   369  
   370  	return nil
   371  }
   372  
   373  func readSecurityGroupRule(sg *cloudstack.SecurityGroup, ruleIndex map[string]int, rule map[string]interface{}, uuid string) {
   374  	uuids := rule["uuids"].(map[string]interface{})
   375  	sgRules := append(sg.Ingressrule, sg.Egressrule...)
   376  
   377  	if rule["protocol"].(string) == "icmp" {
   378  		id, ok := uuids[uuid+"icmp"]
   379  		if !ok {
   380  			return
   381  		}
   382  
   383  		// Get the rule
   384  		idx, ok := ruleIndex[id.(string)]
   385  		if !ok {
   386  			delete(uuids, uuid+"icmp")
   387  			return
   388  		}
   389  
   390  		r := sgRules[idx]
   391  
   392  		// Update the values
   393  		if r.Cidr != "" {
   394  			rule["cidr_list"].(*schema.Set).Add(r.Cidr)
   395  		}
   396  
   397  		if r.Securitygroupname != "" {
   398  			rule["user_security_group_list"].(*schema.Set).Add(r.Securitygroupname)
   399  		}
   400  
   401  		rule["protocol"] = r.Protocol
   402  		rule["icmp_type"] = r.Icmptype
   403  		rule["icmp_code"] = r.Icmpcode
   404  	}
   405  
   406  	// If protocol is tcp or udp, loop through all ports
   407  	if rule["protocol"].(string) == "tcp" || rule["protocol"].(string) == "udp" {
   408  		if ps := rule["ports"].(*schema.Set); ps.Len() > 0 {
   409  
   410  			// Create an empty schema.Set to hold all ports
   411  			ports := &schema.Set{F: schema.HashString}
   412  
   413  			// Loop through all ports and retrieve their info
   414  			for _, port := range ps.List() {
   415  				id, ok := uuids[uuid+port.(string)]
   416  				if !ok {
   417  					continue
   418  				}
   419  
   420  				// Get the rule
   421  				idx, ok := ruleIndex[id.(string)]
   422  				if !ok {
   423  					delete(uuids, uuid+port.(string))
   424  					continue
   425  				}
   426  
   427  				r := sgRules[idx]
   428  
   429  				// Create a set with all CIDR's
   430  				cidrs := &schema.Set{F: schema.HashString}
   431  				for _, cidr := range strings.Split(r.Cidr, ",") {
   432  					cidrs.Add(cidr)
   433  				}
   434  
   435  				// Update the values
   436  				rule["protocol"] = r.Protocol
   437  				ports.Add(port)
   438  			}
   439  
   440  			// If there is at least one port found, add this rule to the rules set
   441  			if ports.Len() > 0 {
   442  				rule["ports"] = ports
   443  			}
   444  		}
   445  	}
   446  }
   447  
   448  func resourceCloudStackSecurityGroupRuleUpdate(d *schema.ResourceData, meta interface{}) error {
   449  	// Check if the rule set as a whole has changed
   450  	if d.HasChange("rule") {
   451  		o, n := d.GetChange("rule")
   452  		ors := o.(*schema.Set).Difference(n.(*schema.Set))
   453  		nrs := n.(*schema.Set).Difference(o.(*schema.Set))
   454  
   455  		// We need to start with a rule set containing all the rules we
   456  		// already have and want to keep. Any rules that are not deleted
   457  		// correctly and any newly created rules, will be added to this
   458  		// set to make sure we end up in a consistent state
   459  		rules := o.(*schema.Set).Intersection(n.(*schema.Set))
   460  
   461  		// First loop through all the old rules destroy them
   462  		if ors.Len() > 0 {
   463  			err := deleteSecurityGroupRules(d, meta, rules, ors)
   464  
   465  			// We need to update this first to preserve the correct state
   466  			d.Set("rule", rules)
   467  
   468  			if err != nil {
   469  				return err
   470  			}
   471  		}
   472  
   473  		// Then loop through all the new rules and delete them
   474  		if nrs.Len() > 0 {
   475  			err := createSecurityGroupRules(d, meta, rules, nrs)
   476  
   477  			// We need to update this first to preserve the correct state
   478  			d.Set("rule", rules)
   479  
   480  			if err != nil {
   481  				return err
   482  			}
   483  		}
   484  	}
   485  
   486  	return resourceCloudStackSecurityGroupRuleRead(d, meta)
   487  }
   488  
   489  func resourceCloudStackSecurityGroupRuleDelete(d *schema.ResourceData, meta interface{}) error {
   490  	// Create an empty rule set to hold all rules that where
   491  	// not deleted correctly
   492  	rules := resourceCloudStackSecurityGroupRule().Schema["rule"].ZeroValue().(*schema.Set)
   493  
   494  	// Delete all rules
   495  	if ors := d.Get("rule").(*schema.Set); ors.Len() > 0 {
   496  		err := deleteSecurityGroupRules(d, meta, rules, ors)
   497  
   498  		// We need to update this first to preserve the correct state
   499  		d.Set("rule", rules)
   500  
   501  		if err != nil {
   502  			return err
   503  		}
   504  	}
   505  
   506  	return nil
   507  }
   508  
   509  func deleteSecurityGroupRules(d *schema.ResourceData, meta interface{}, rules *schema.Set, ors *schema.Set) error {
   510  	var errs *multierror.Error
   511  
   512  	var wg sync.WaitGroup
   513  	wg.Add(ors.Len())
   514  
   515  	sem := make(chan struct{}, d.Get("parallelism").(int))
   516  	for _, rule := range ors.List() {
   517  		// Put a sleep here to avoid DoS'ing the API
   518  		time.Sleep(500 * time.Millisecond)
   519  
   520  		go func(rule map[string]interface{}) {
   521  			defer wg.Done()
   522  			sem <- struct{}{}
   523  
   524  			// Create a single rule
   525  			err := deleteSecurityGroupRule(d, meta, rule)
   526  			if err != nil {
   527  				errs = multierror.Append(errs, err)
   528  			}
   529  
   530  			// If we have at least one UUID, we need to save the rule
   531  			if len(rule["uuids"].(map[string]interface{})) > 0 {
   532  				rules.Add(rule)
   533  			}
   534  
   535  			<-sem
   536  		}(rule.(map[string]interface{}))
   537  	}
   538  
   539  	wg.Wait()
   540  
   541  	return errs.ErrorOrNil()
   542  }
   543  
   544  func deleteSecurityGroupRule(d *schema.ResourceData, meta interface{}, rule map[string]interface{}) error {
   545  	cs := meta.(*cloudstack.CloudStackClient)
   546  	uuids := rule["uuids"].(map[string]interface{})
   547  
   548  	for k, id := range uuids {
   549  		// We don't care about the count here, so just continue
   550  		if k == "%" {
   551  			continue
   552  		}
   553  
   554  		var err error
   555  		switch rule["traffic_type"].(string) {
   556  		case "ingress":
   557  			p := cs.SecurityGroup.NewRevokeSecurityGroupIngressParams(id.(string))
   558  			_, err = cs.SecurityGroup.RevokeSecurityGroupIngress(p)
   559  		case "egress":
   560  			p := cs.SecurityGroup.NewRevokeSecurityGroupEgressParams(id.(string))
   561  			_, err = cs.SecurityGroup.RevokeSecurityGroupEgress(p)
   562  		}
   563  
   564  		if err != nil {
   565  			// This is a very poor way to be told the ID does no longer exist :(
   566  			if strings.Contains(err.Error(), fmt.Sprintf(
   567  				"Invalid parameter id value=%s due to incorrect long value format, "+
   568  					"or entity does not exist", id.(string))) {
   569  				delete(uuids, k)
   570  				continue
   571  			}
   572  
   573  			return err
   574  		}
   575  
   576  		// Delete the UUID of this rule
   577  		delete(uuids, k)
   578  	}
   579  
   580  	return nil
   581  }
   582  
   583  func verifySecurityGroupRuleParams(d *schema.ResourceData, rule map[string]interface{}) error {
   584  	cidrList, cidrListOK := rule["cidr_list"].(*schema.Set)
   585  	usgList, usgListOK := rule["user_security_group_list"].(*schema.Set)
   586  
   587  	if (!cidrListOK || cidrList.Len() == 0) && (!usgListOK || usgList.Len() == 0) {
   588  		return fmt.Errorf(
   589  			"You must supply at least one 'cidr_list' or `user_security_group_ids` entry")
   590  	}
   591  
   592  	protocol := rule["protocol"].(string)
   593  	switch protocol {
   594  	case "icmp":
   595  		if _, ok := rule["icmp_type"]; !ok {
   596  			return fmt.Errorf(
   597  				"Parameter icmp_type is a required parameter when using protocol 'icmp'")
   598  		}
   599  		if _, ok := rule["icmp_code"]; !ok {
   600  			return fmt.Errorf(
   601  				"Parameter icmp_code is a required parameter when using protocol 'icmp'")
   602  		}
   603  	case "tcp", "udp":
   604  		if ports, ok := rule["ports"].(*schema.Set); ok {
   605  			for _, port := range ports.List() {
   606  				m := splitPorts.FindStringSubmatch(port.(string))
   607  				if m == nil {
   608  					return fmt.Errorf(
   609  						"%q is not a valid port value. Valid options are '80' or '80-90'", port.(string))
   610  				}
   611  			}
   612  		} else {
   613  			return fmt.Errorf(
   614  				"Parameter ports is a required parameter when *not* using protocol 'icmp'")
   615  		}
   616  	default:
   617  		_, err := strconv.ParseInt(protocol, 0, 0)
   618  		if err != nil {
   619  			return fmt.Errorf(
   620  				"%q is not a valid protocol. Valid options are 'tcp', 'udp' and 'icmp'", protocol)
   621  		}
   622  	}
   623  
   624  	traffic := rule["traffic_type"].(string)
   625  	if traffic != "ingress" && traffic != "egress" {
   626  		return fmt.Errorf(
   627  			"Parameter traffic_type only accepts 'ingress' or 'egress' as values")
   628  	}
   629  
   630  	return nil
   631  }