package cloudstack import ( "fmt" "log" "strconv" "strings" "sync" "time" multierror "github.com/hashicorp/go-multierror" "github.com/hashicorp/terraform/helper/schema" "github.com/xanzy/go-cloudstack/cloudstack" ) type authorizeSecurityGroupParams interface { SetCidrlist([]string) SetIcmptype(int) SetIcmpcode(int) SetStartport(int) SetEndport(int) SetProtocol(string) SetSecuritygroupid(string) SetUsersecuritygrouplist(map[string]string) } func resourceCloudStackSecurityGroupRule() *schema.Resource { return &schema.Resource{ Create: resourceCloudStackSecurityGroupRuleCreate, Read: resourceCloudStackSecurityGroupRuleRead, Update: resourceCloudStackSecurityGroupRuleUpdate, Delete: resourceCloudStackSecurityGroupRuleDelete, Schema: map[string]*schema.Schema{ "security_group_id": &schema.Schema{ Type: schema.TypeString, Required: true, ForceNew: true, }, "rule": &schema.Schema{ Type: schema.TypeSet, Required: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "cidr_list": &schema.Schema{ Type: schema.TypeSet, Optional: true, Elem: &schema.Schema{Type: schema.TypeString}, Set: schema.HashString, }, "protocol": &schema.Schema{ Type: schema.TypeString, Required: true, }, "icmp_type": &schema.Schema{ Type: schema.TypeInt, Optional: true, Computed: true, }, "icmp_code": &schema.Schema{ Type: schema.TypeInt, Optional: true, Computed: true, }, "ports": &schema.Schema{ Type: schema.TypeSet, Optional: true, Elem: &schema.Schema{Type: schema.TypeString}, Set: schema.HashString, }, "traffic_type": &schema.Schema{ Type: schema.TypeString, Optional: true, Default: "ingress", }, "user_security_group_list": &schema.Schema{ Type: schema.TypeSet, Optional: true, Elem: &schema.Schema{Type: schema.TypeString}, Set: schema.HashString, }, "uuids": &schema.Schema{ Type: schema.TypeMap, Computed: true, }, }, }, }, "project": &schema.Schema{ Type: schema.TypeString, Optional: true, ForceNew: true, }, "parallelism": &schema.Schema{ Type: schema.TypeInt, Optional: true, Default: 2, }, }, } } func resourceCloudStackSecurityGroupRuleCreate(d *schema.ResourceData, meta interface{}) error { // We need to set this upfront in order to be able to save a partial state d.SetId(d.Get("security_group_id").(string)) // Create all rules that are configured if nrs := d.Get("rule").(*schema.Set); nrs.Len() > 0 { // Create an empty rule set to hold all newly created rules rules := resourceCloudStackSecurityGroupRule().Schema["rule"].ZeroValue().(*schema.Set) err := createSecurityGroupRules(d, meta, rules, nrs) // We need to update this first to preserve the correct state d.Set("rule", rules) if err != nil { return err } } return resourceCloudStackSecurityGroupRuleRead(d, meta) } func createSecurityGroupRules(d *schema.ResourceData, meta interface{}, rules *schema.Set, nrs *schema.Set) error { cs := meta.(*cloudstack.CloudStackClient) var errs *multierror.Error var wg sync.WaitGroup wg.Add(nrs.Len()) sem := make(chan struct{}, d.Get("parallelism").(int)) for _, rule := range nrs.List() { // Put in a tiny sleep here to avoid DoS'ing the API time.Sleep(500 * time.Millisecond) go func(rule map[string]interface{}) { defer wg.Done() sem <- struct{}{} // Make sure all required parameters are there if err := verifySecurityGroupRuleParams(d, rule); err != nil { errs = multierror.Append(errs, err) return } var p authorizeSecurityGroupParams if cidrList, ok := rule["cidr_list"].(*schema.Set); ok && cidrList.Len() > 0 { for _, cidr := range cidrList.List() { // Create a new parameter struct switch rule["traffic_type"].(string) { case "ingress": p = cs.SecurityGroup.NewAuthorizeSecurityGroupIngressParams() case "egress": p = cs.SecurityGroup.NewAuthorizeSecurityGroupEgressParams() } p.SetSecuritygroupid(d.Id()) p.SetCidrlist([]string{cidr.(string)}) // Create a single rule err := createSecurityGroupRule(d, meta, rule, p, cidr.(string)) if err != nil { errs = multierror.Append(errs, err) } } } if usgList, ok := rule["user_security_group_list"].(*schema.Set); ok && usgList.Len() > 0 { for _, usg := range usgList.List() { sg, _, err := cs.SecurityGroup.GetSecurityGroupByName( usg.(string), cloudstack.WithProject(d.Get("project").(string)), ) if err != nil { errs = multierror.Append(errs, err) continue } // Create a new parameter struct switch rule["traffic_type"].(string) { case "ingress": p = cs.SecurityGroup.NewAuthorizeSecurityGroupIngressParams() case "egress": p = cs.SecurityGroup.NewAuthorizeSecurityGroupEgressParams() } p.SetSecuritygroupid(d.Id()) p.SetUsersecuritygrouplist(map[string]string{sg.Account: usg.(string)}) // Create a single rule err = createSecurityGroupRule(d, meta, rule, p, usg.(string)) if err != nil { errs = multierror.Append(errs, err) } } } // If we have at least one UUID, we need to save the rule if len(rule["uuids"].(map[string]interface{})) > 0 { rules.Add(rule) } <-sem }(rule.(map[string]interface{})) } wg.Wait() return errs.ErrorOrNil() } func createSecurityGroupRule(d *schema.ResourceData, meta interface{}, rule map[string]interface{}, p authorizeSecurityGroupParams, uuid string) error { cs := meta.(*cloudstack.CloudStackClient) uuids := rule["uuids"].(map[string]interface{}) // Set the protocol p.SetProtocol(rule["protocol"].(string)) // If the protocol is ICMP set the needed ICMP parameters if rule["protocol"].(string) == "icmp" { p.SetIcmptype(rule["icmp_type"].(int)) p.SetIcmpcode(rule["icmp_code"].(int)) ruleID, err := createIngressOrEgressRule(cs, p) if err != nil { return err } uuids[uuid+"icmp"] = ruleID rule["uuids"] = uuids } // If protocol is TCP or UDP, loop through all ports if rule["protocol"].(string) == "tcp" || rule["protocol"].(string) == "udp" { if ps := rule["ports"].(*schema.Set); ps.Len() > 0 { // Create an empty schema.Set to hold all processed ports ports := &schema.Set{F: schema.HashString} for _, port := range ps.List() { if _, ok := uuids[uuid+port.(string)]; ok { ports.Add(port) rule["ports"] = ports continue } m := splitPorts.FindStringSubmatch(port.(string)) startPort, err := strconv.Atoi(m[1]) if err != nil { return err } endPort := startPort if m[2] != "" { endPort, err = strconv.Atoi(m[2]) if err != nil { return err } } p.SetStartport(startPort) p.SetEndport(endPort) ruleID, err := createIngressOrEgressRule(cs, p) if err != nil { return err } ports.Add(port) rule["ports"] = ports uuids[uuid+port.(string)] = ruleID rule["uuids"] = uuids } } } return nil } func createIngressOrEgressRule(cs *cloudstack.CloudStackClient, p authorizeSecurityGroupParams) (string, error) { switch p := p.(type) { case *cloudstack.AuthorizeSecurityGroupIngressParams: r, err := cs.SecurityGroup.AuthorizeSecurityGroupIngress(p) if err != nil { return "", err } return r.Ruleid, nil case *cloudstack.AuthorizeSecurityGroupEgressParams: r, err := cs.SecurityGroup.AuthorizeSecurityGroupEgress(p) if err != nil { return "", err } return r.Ruleid, nil default: return "", fmt.Errorf("Unknown authorize security group rule type: %v", p) } } func resourceCloudStackSecurityGroupRuleRead(d *schema.ResourceData, meta interface{}) error { cs := meta.(*cloudstack.CloudStackClient) // Get the security group details sg, count, err := cs.SecurityGroup.GetSecurityGroupByID( d.Id(), cloudstack.WithProject(d.Get("project").(string)), ) if err != nil { if count == 0 { log.Printf("[DEBUG] Security group %s does not longer exist", d.Get("name").(string)) d.SetId("") return nil } return err } // Make a map of all the rule indexes so we can easily find a rule sgRules := append(sg.Ingressrule, sg.Egressrule...) ruleIndex := make(map[string]int, len(sgRules)) for idx, r := range sgRules { ruleIndex[r.Ruleid] = idx } // Create an empty schema.Set to hold all rules rules := resourceCloudStackSecurityGroupRule().Schema["rule"].ZeroValue().(*schema.Set) // Read all rules that are configured if rs := d.Get("rule").(*schema.Set); rs.Len() > 0 { for _, rule := range rs.List() { rule := rule.(map[string]interface{}) // First get any existing values cidrList, cidrListOK := rule["cidr_list"].(*schema.Set) usgList, usgListOk := rule["user_security_group_list"].(*schema.Set) // Then reset the values to a new empty set rule["cidr_list"] = &schema.Set{F: schema.HashString} rule["user_security_group_list"] = &schema.Set{F: schema.HashString} if cidrListOK && cidrList.Len() > 0 { for _, cidr := range cidrList.List() { readSecurityGroupRule(sg, ruleIndex, rule, cidr.(string)) } } if usgListOk && usgList.Len() > 0 { for _, usg := range usgList.List() { readSecurityGroupRule(sg, ruleIndex, rule, usg.(string)) } } rules.Add(rule) } } return nil } func readSecurityGroupRule(sg *cloudstack.SecurityGroup, ruleIndex map[string]int, rule map[string]interface{}, uuid string) { uuids := rule["uuids"].(map[string]interface{}) sgRules := append(sg.Ingressrule, sg.Egressrule...) if rule["protocol"].(string) == "icmp" { id, ok := uuids[uuid+"icmp"] if !ok { return } // Get the rule idx, ok := ruleIndex[id.(string)] if !ok { delete(uuids, uuid+"icmp") return } r := sgRules[idx] // Update the values if r.Cidr != "" { rule["cidr_list"].(*schema.Set).Add(r.Cidr) } if r.Securitygroupname != "" { rule["user_security_group_list"].(*schema.Set).Add(r.Securitygroupname) } rule["protocol"] = r.Protocol rule["icmp_type"] = r.Icmptype rule["icmp_code"] = r.Icmpcode } // If protocol is tcp or udp, loop through all ports if rule["protocol"].(string) == "tcp" || rule["protocol"].(string) == "udp" { if ps := rule["ports"].(*schema.Set); ps.Len() > 0 { // Create an empty schema.Set to hold all ports ports := &schema.Set{F: schema.HashString} // Loop through all ports and retrieve their info for _, port := range ps.List() { id, ok := uuids[uuid+port.(string)] if !ok { continue } // Get the rule idx, ok := ruleIndex[id.(string)] if !ok { delete(uuids, uuid+port.(string)) continue } r := sgRules[idx] // Create a set with all CIDR's cidrs := &schema.Set{F: schema.HashString} for _, cidr := range strings.Split(r.Cidr, ",") { cidrs.Add(cidr) } // Update the values rule["protocol"] = r.Protocol ports.Add(port) } // If there is at least one port found, add this rule to the rules set if ports.Len() > 0 { rule["ports"] = ports } } } } func resourceCloudStackSecurityGroupRuleUpdate(d *schema.ResourceData, meta interface{}) error { // Check if the rule set as a whole has changed if d.HasChange("rule") { o, n := d.GetChange("rule") ors := o.(*schema.Set).Difference(n.(*schema.Set)) nrs := n.(*schema.Set).Difference(o.(*schema.Set)) // We need to start with a rule set containing all the rules we // already have and want to keep. Any rules that are not deleted // correctly and any newly created rules, will be added to this // set to make sure we end up in a consistent state rules := o.(*schema.Set).Intersection(n.(*schema.Set)) // First loop through all the old rules destroy them if ors.Len() > 0 { err := deleteSecurityGroupRules(d, meta, rules, ors) // We need to update this first to preserve the correct state d.Set("rule", rules) if err != nil { return err } } // Then loop through all the new rules and delete them if nrs.Len() > 0 { err := createSecurityGroupRules(d, meta, rules, nrs) // We need to update this first to preserve the correct state d.Set("rule", rules) if err != nil { return err } } } return resourceCloudStackSecurityGroupRuleRead(d, meta) } func resourceCloudStackSecurityGroupRuleDelete(d *schema.ResourceData, meta interface{}) error { // Create an empty rule set to hold all rules that where // not deleted correctly rules := resourceCloudStackSecurityGroupRule().Schema["rule"].ZeroValue().(*schema.Set) // Delete all rules if ors := d.Get("rule").(*schema.Set); ors.Len() > 0 { err := deleteSecurityGroupRules(d, meta, rules, ors) // We need to update this first to preserve the correct state d.Set("rule", rules) if err != nil { return err } } return nil } func deleteSecurityGroupRules(d *schema.ResourceData, meta interface{}, rules *schema.Set, ors *schema.Set) error { var errs *multierror.Error var wg sync.WaitGroup wg.Add(ors.Len()) sem := make(chan struct{}, d.Get("parallelism").(int)) for _, rule := range ors.List() { // Put a sleep here to avoid DoS'ing the API time.Sleep(500 * time.Millisecond) go func(rule map[string]interface{}) { defer wg.Done() sem <- struct{}{} // Create a single rule err := deleteSecurityGroupRule(d, meta, rule) if err != nil { errs = multierror.Append(errs, err) } // If we have at least one UUID, we need to save the rule if len(rule["uuids"].(map[string]interface{})) > 0 { rules.Add(rule) } <-sem }(rule.(map[string]interface{})) } wg.Wait() return errs.ErrorOrNil() } func deleteSecurityGroupRule(d *schema.ResourceData, meta interface{}, rule map[string]interface{}) error { cs := meta.(*cloudstack.CloudStackClient) uuids := rule["uuids"].(map[string]interface{}) for k, id := range uuids { // We don't care about the count here, so just continue if k == "%" { continue } var err error switch rule["traffic_type"].(string) { case "ingress": p := cs.SecurityGroup.NewRevokeSecurityGroupIngressParams(id.(string)) _, err = cs.SecurityGroup.RevokeSecurityGroupIngress(p) case "egress": p := cs.SecurityGroup.NewRevokeSecurityGroupEgressParams(id.(string)) _, err = cs.SecurityGroup.RevokeSecurityGroupEgress(p) } if err != nil { // This is a very poor way to be told the ID does no longer exist :( if strings.Contains(err.Error(), fmt.Sprintf( "Invalid parameter id value=%s due to incorrect long value format, "+ "or entity does not exist", id.(string))) { delete(uuids, k) continue } return err } // Delete the UUID of this rule delete(uuids, k) } return nil } func verifySecurityGroupRuleParams(d *schema.ResourceData, rule map[string]interface{}) error { cidrList, cidrListOK := rule["cidr_list"].(*schema.Set) usgList, usgListOK := rule["user_security_group_list"].(*schema.Set) if (!cidrListOK || cidrList.Len() == 0) && (!usgListOK || usgList.Len() == 0) { return fmt.Errorf( "You must supply at least one 'cidr_list' or `user_security_group_ids` entry") } protocol := rule["protocol"].(string) switch protocol { case "icmp": if _, ok := rule["icmp_type"]; !ok { return fmt.Errorf( "Parameter icmp_type is a required parameter when using protocol 'icmp'") } if _, ok := rule["icmp_code"]; !ok { return fmt.Errorf( "Parameter icmp_code is a required parameter when using protocol 'icmp'") } case "tcp", "udp": if ports, ok := rule["ports"].(*schema.Set); ok { for _, port := range ports.List() { m := splitPorts.FindStringSubmatch(port.(string)) if m == nil { return fmt.Errorf( "%q is not a valid port value. Valid options are '80' or '80-90'", port.(string)) } } } else { return fmt.Errorf( "Parameter ports is a required parameter when *not* using protocol 'icmp'") } default: _, err := strconv.ParseInt(protocol, 0, 0) if err != nil { return fmt.Errorf( "%q is not a valid protocol. Valid options are 'tcp', 'udp' and 'icmp'", protocol) } } traffic := rule["traffic_type"].(string) if traffic != "ingress" && traffic != "egress" { return fmt.Errorf( "Parameter traffic_type only accepts 'ingress' or 'egress' as values") } return nil }