diff --git a/builtin/providers/cloudstack/provider.go b/builtin/providers/cloudstack/provider.go index 34addd605..c5c506fec 100644 --- a/builtin/providers/cloudstack/provider.go +++ b/builtin/providers/cloudstack/provider.go @@ -55,6 +55,7 @@ func Provider() terraform.ResourceProvider { "cloudstack_port_forward": resourceCloudStackPortForward(), "cloudstack_secondary_ipaddress": resourceCloudStackSecondaryIPAddress(), "cloudstack_security_group": resourceCloudStackSecurityGroup(), + "cloudstack_security_group_rule": resourceCloudStackSecurityGroupRule(), "cloudstack_ssh_keypair": resourceCloudStackSSHKeyPair(), "cloudstack_static_nat": resourceCloudStackStaticNAT(), "cloudstack_template": resourceCloudStackTemplate(), diff --git a/builtin/providers/cloudstack/resource_cloudstack_instance.go b/builtin/providers/cloudstack/resource_cloudstack_instance.go index 59f16492d..f0fc90647 100644 --- a/builtin/providers/cloudstack/resource_cloudstack_instance.go +++ b/builtin/providers/cloudstack/resource_cloudstack_instance.go @@ -391,7 +391,8 @@ func resourceCloudStackInstanceUpdate(d *schema.ResourceData, meta interface{}) } // Attributes that require reboot to update - if d.HasChange("name") || d.HasChange("service_offering") || d.HasChange("affinity_group_ids") || d.HasChange("affinity_group_names") || d.HasChange("keypair") || d.HasChange("user_data") { + if d.HasChange("name") || d.HasChange("service_offering") || d.HasChange("affinity_group_ids") || + d.HasChange("affinity_group_names") || d.HasChange("keypair") || d.HasChange("user_data") { // Before we can actually make these changes, the virtual machine must be stopped _, err := cs.VirtualMachine.StopVirtualMachine( cs.VirtualMachine.NewStopVirtualMachineParams(d.Id())) @@ -453,7 +454,16 @@ func resourceCloudStackInstanceUpdate(d *schema.ResourceData, meta interface{}) } } + // Set the new groups p.SetAffinitygroupids(groups) + + // Update the affinity groups + _, err = cs.AffinityGroup.UpdateVMAffinityGroup(p) + if err != nil { + return fmt.Errorf( + "Error updating the affinity groups for instance %s: %s", name, err) + } + d.SetPartial("affinity_group_ids") } // Check if the affinity group names have changed and if so, update the names @@ -467,7 +477,16 @@ func resourceCloudStackInstanceUpdate(d *schema.ResourceData, meta interface{}) } } - p.SetAffinitygroupids(groups) + // Set the new groups + p.SetAffinitygroupnames(groups) + + // Update the affinity groups + _, err = cs.AffinityGroup.UpdateVMAffinityGroup(p) + if err != nil { + return fmt.Errorf( + "Error updating the affinity groups for instance %s: %s", name, err) + } + d.SetPartial("affinity_group_names") } // Check if the keypair has changed and if so, update the keypair @@ -514,6 +533,7 @@ func resourceCloudStackInstanceUpdate(d *schema.ResourceData, meta interface{}) } d.Partial(false) + return resourceCloudStackInstanceRead(d, meta) } diff --git a/builtin/providers/cloudstack/resource_cloudstack_network_acl_rule.go b/builtin/providers/cloudstack/resource_cloudstack_network_acl_rule.go index 0bc86d392..98a3ba5e7 100644 --- a/builtin/providers/cloudstack/resource_cloudstack_network_acl_rule.go +++ b/builtin/providers/cloudstack/resource_cloudstack_network_acl_rule.go @@ -519,11 +519,7 @@ func resourceCloudStackNetworkACLRuleDelete(d *schema.ResourceData, meta interfa return nil } -func deleteNetworkACLRules( - d *schema.ResourceData, - meta interface{}, - rules *schema.Set, - ors *schema.Set) error { +func deleteNetworkACLRules(d *schema.ResourceData, meta interface{}, rules *schema.Set, ors *schema.Set) error { var errs *multierror.Error var wg sync.WaitGroup @@ -559,10 +555,7 @@ func deleteNetworkACLRules( return errs.ErrorOrNil() } -func deleteNetworkACLRule( - d *schema.ResourceData, - meta interface{}, - rule map[string]interface{}) error { +func deleteNetworkACLRule(d *schema.ResourceData, meta interface{}, rule map[string]interface{}) error { cs := meta.(*cloudstack.CloudStackClient) uuids := rule["uuids"].(map[string]interface{}) diff --git a/builtin/providers/cloudstack/resource_cloudstack_security_group.go b/builtin/providers/cloudstack/resource_cloudstack_security_group.go index e1619ab98..2b198bd88 100644 --- a/builtin/providers/cloudstack/resource_cloudstack_security_group.go +++ b/builtin/providers/cloudstack/resource_cloudstack_security_group.go @@ -3,7 +3,6 @@ package cloudstack import ( "fmt" "log" - "strconv" "strings" "github.com/hashicorp/terraform/helper/schema" @@ -33,47 +32,9 @@ func resourceCloudStackSecurityGroup() *schema.Resource { "project": &schema.Schema{ Type: schema.TypeString, Optional: true, + Computed: true, ForceNew: true, }, - - "rules": &schema.Schema{ - Type: schema.TypeList, - ForceNew: true, - Optional: true, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "cidr_list": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - ForceNew: true, - }, - - "security_group": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - ForceNew: true, - }, - - "protocol": &schema.Schema{ - Type: schema.TypeString, - Required: true, - ForceNew: true, - }, - - "ports": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - ForceNew: true, - }, - - "traffic_type": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - Default: "ingress", - }, - }, - }, - }, }, } } @@ -98,132 +59,21 @@ func resourceCloudStackSecurityGroupCreate(d *schema.ResourceData, meta interfac return err } - log.Printf("[DEBUG] Creating security group %s", name) r, err := cs.SecurityGroup.CreateSecurityGroup(p) if err != nil { - return err + return fmt.Errorf("Error creating security group %s: %s", name, err) } - log.Printf("[DEBUG] Security group %s successfully created with ID: %s", name, r.Id) d.SetId(r.Id) - // Create Rules - rules := d.Get("rules").([]interface{}) - for _, r := range rules { - - rule := r.(map[string]interface{}) - - m := splitPorts.FindStringSubmatch(rule["ports"].(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 - } - } - - traffic := rule["traffic_type"].(string) - if traffic == "ingress" { - param := cs.SecurityGroup.NewAuthorizeSecurityGroupIngressParams() - - param.SetSecuritygroupid(d.Id()) - if cidrlist := rule["cidr_list"]; cidrlist != "" { - param.SetCidrlist([]string{cidrlist.(string)}) - log.Printf("[DEBUG] cidr = %v", cidrlist) - } - if securitygroup := rule["security_group"]; securitygroup != "" { - // Get the security group details - ag, count, err := cs.SecurityGroup.GetSecurityGroupByName( - securitygroup.(string), - 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 - } - - log.Printf("[DEBUG] Found %v groups matching", count) - - return err - } - log.Printf("[DEBUG] ag = %v", ag) - m := make(map[string]string) - m[ag.Account] = ag.Name - log.Printf("[DEBUG] m = %v", m) - param.SetUsersecuritygrouplist(m) - } - param.SetStartport(startPort) - param.SetEndport(endPort) - param.SetProtocol(rule["protocol"].(string)) - - log.Printf("[DEBUG] Authorizing Ingress Rule %#v", param) - _, err := cs.SecurityGroup.AuthorizeSecurityGroupIngress(param) - if err != nil { - return err - } - } else if traffic == "egress" { - param := cs.SecurityGroup.NewAuthorizeSecurityGroupEgressParams() - - param.SetSecuritygroupid(d.Id()) - if cidrlist := rule["cidr_list"]; cidrlist != "" { - param.SetCidrlist([]string{cidrlist.(string)}) - log.Printf("[DEBUG] cidr = %v", cidrlist) - } - if securitygroup := rule["security_group"]; securitygroup != "" { - // Get the security group details - ag, count, err := cs.SecurityGroup.GetSecurityGroupByName( - securitygroup.(string), - 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 - } - - log.Printf("[DEBUG] Found %v groups matching", count) - - return err - } - log.Printf("[DEBUG] ag = %v", ag) - m := make(map[string]string) - m[ag.Account] = ag.Name - log.Printf("[DEBUG] m = %v", m) - param.SetUsersecuritygrouplist(m) - } - param.SetStartport(startPort) - param.SetEndport(endPort) - param.SetProtocol(rule["protocol"].(string)) - - log.Printf("[DEBUG] Authorizing Egress Rule %#v", param) - _, err := cs.SecurityGroup.AuthorizeSecurityGroupEgress(param) - if err != nil { - return err - } - } else { - return fmt.Errorf( - "Parameter traffic_type only accepts 'ingress' or 'egress' as values") - } - } - return resourceCloudStackSecurityGroupRead(d, meta) } func resourceCloudStackSecurityGroupRead(d *schema.ResourceData, meta interface{}) error { cs := meta.(*cloudstack.CloudStackClient) - log.Printf("[DEBUG] Retrieving security group %s (ID=%s)", d.Get("name").(string), d.Id()) - // Get the security group details - ag, count, err := cs.SecurityGroup.GetSecurityGroupByID( + sg, count, err := cs.SecurityGroup.GetSecurityGroupByID( d.Id(), cloudstack.WithProject(d.Get("project").(string)), ) @@ -234,49 +84,14 @@ func resourceCloudStackSecurityGroupRead(d *schema.ResourceData, meta interface{ return nil } - log.Printf("[DEBUG] Found %v groups matching", count) - return err } // Update the config - d.Set("name", ag.Name) - d.Set("description", ag.Description) + d.Set("name", sg.Name) + d.Set("description", sg.Description) - var rules []interface{} - for _, r := range ag.Ingressrule { - var ports string - if r.Startport == r.Endport { - ports = strconv.Itoa(r.Startport) - } else { - ports = fmt.Sprintf("%v-%v", r.Startport, r.Endport) - } - rule := map[string]interface{}{ - "cidr_list": r.Cidr, - "ports": ports, - "protocol": r.Protocol, - "traffic_type": "ingress", - "security_group": r.Securitygroupname, - } - rules = append(rules, rule) - } - for _, r := range ag.Egressrule { - var ports string - if r.Startport == r.Endport { - ports = strconv.Itoa(r.Startport) - } else { - ports = fmt.Sprintf("%s-%s", r.Startport, r.Endport) - } - rule := map[string]interface{}{ - "cidr_list": r.Cidr, - "ports": ports, - "protocol": r.Protocol, - "traffic_type": "egress", - "security_group": r.Securitygroupname, - } - rules = append(rules, rule) - } - d.Set("rules", rules) + setValueOrID(d, "project", sg.Project, sg.Projectid) return nil } diff --git a/builtin/providers/cloudstack/resource_cloudstack_security_group_rule.go b/builtin/providers/cloudstack/resource_cloudstack_security_group_rule.go new file mode 100644 index 000000000..4a538201e --- /dev/null +++ b/builtin/providers/cloudstack/resource_cloudstack_security_group_rule.go @@ -0,0 +1,631 @@ +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 +} diff --git a/builtin/providers/cloudstack/resource_cloudstack_security_group_rule_test.go b/builtin/providers/cloudstack/resource_cloudstack_security_group_rule_test.go new file mode 100644 index 000000000..cef5a39a2 --- /dev/null +++ b/builtin/providers/cloudstack/resource_cloudstack_security_group_rule_test.go @@ -0,0 +1,274 @@ +package cloudstack + +import ( + "fmt" + "strings" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "github.com/xanzy/go-cloudstack/cloudstack" +) + +func TestAccCloudStackSecurityGroupRule_basic(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackSecurityGroupRuleDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccCloudStackSecurityGroupRule_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackSecurityGroupRulesExist("cloudstack_security_group.foo"), + resource.TestCheckResourceAttr( + "cloudstack_security_group_rule.foo", "rule.#", "2"), + resource.TestCheckResourceAttr( + "cloudstack_security_group_rule.foo", "rule.1322309156.cidr_list.3056857544", "172.18.100.0/24"), + resource.TestCheckResourceAttr( + "cloudstack_security_group_rule.foo", "rule.1322309156.protocol", "tcp"), + resource.TestCheckResourceAttr( + "cloudstack_security_group_rule.foo", "rule.1322309156.ports.#", "1"), + resource.TestCheckResourceAttr( + "cloudstack_security_group_rule.foo", "rule.1322309156.ports.1889509032", "80"), + resource.TestCheckResourceAttr( + "cloudstack_security_group_rule.foo", "rule.1322309156.traffic_type", "ingress"), + resource.TestCheckResourceAttr( + "cloudstack_security_group_rule.foo", "rule.3666289950.protocol", "tcp"), + resource.TestCheckResourceAttr( + "cloudstack_security_group_rule.foo", "rule.3666289950.ports.1889509032", "80"), + resource.TestCheckResourceAttr( + "cloudstack_security_group_rule.foo", "rule.3666289950.ports.3638101695", "443"), + resource.TestCheckResourceAttr( + "cloudstack_security_group_rule.foo", "rule.3666289950.traffic_type", "egress"), + resource.TestCheckResourceAttr( + "cloudstack_security_group_rule.foo", "rule.3666289950.user_security_group_list.1089118859", "terraform-security-group-bar"), + ), + }, + }, + }) +} + +func TestAccCloudStackSecurityGroupRule_update(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackSecurityGroupRuleDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccCloudStackSecurityGroupRule_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackSecurityGroupRulesExist("cloudstack_security_group.foo"), + resource.TestCheckResourceAttr( + "cloudstack_security_group_rule.foo", "rule.#", "2"), + resource.TestCheckResourceAttr( + "cloudstack_security_group_rule.foo", "rule.1322309156.cidr_list.3056857544", "172.18.100.0/24"), + resource.TestCheckResourceAttr( + "cloudstack_security_group_rule.foo", "rule.1322309156.protocol", "tcp"), + resource.TestCheckResourceAttr( + "cloudstack_security_group_rule.foo", "rule.1322309156.ports.#", "1"), + resource.TestCheckResourceAttr( + "cloudstack_security_group_rule.foo", "rule.1322309156.ports.1889509032", "80"), + resource.TestCheckResourceAttr( + "cloudstack_security_group_rule.foo", "rule.1322309156.traffic_type", "ingress"), + resource.TestCheckResourceAttr( + "cloudstack_security_group_rule.foo", "rule.3666289950.protocol", "tcp"), + resource.TestCheckResourceAttr( + "cloudstack_security_group_rule.foo", "rule.3666289950.ports.1889509032", "80"), + resource.TestCheckResourceAttr( + "cloudstack_security_group_rule.foo", "rule.3666289950.ports.3638101695", "443"), + resource.TestCheckResourceAttr( + "cloudstack_security_group_rule.foo", "rule.3666289950.traffic_type", "egress"), + resource.TestCheckResourceAttr( + "cloudstack_security_group_rule.foo", "rule.3666289950.user_security_group_list.1089118859", "terraform-security-group-bar"), + ), + }, + + resource.TestStep{ + Config: testAccCloudStackSecurityGroupRule_update, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackSecurityGroupRulesExist("cloudstack_security_group.foo"), + resource.TestCheckResourceAttr( + "cloudstack_security_group_rule.foo", "rule.#", "3"), + resource.TestCheckResourceAttr( + "cloudstack_security_group_rule.foo", "rule.3156342770.cidr_list.3056857544", "172.18.100.0/24"), + resource.TestCheckResourceAttr( + "cloudstack_security_group_rule.foo", "rule.3156342770.cidr_list.951907883", "172.18.200.0/24"), + resource.TestCheckResourceAttr( + "cloudstack_security_group_rule.foo", "rule.3156342770.protocol", "tcp"), + resource.TestCheckResourceAttr( + "cloudstack_security_group_rule.foo", "rule.3156342770.ports.1889509032", "80"), + resource.TestCheckResourceAttr( + "cloudstack_security_group_rule.foo", "rule.3156342770.ports.3638101695", "443"), + resource.TestCheckResourceAttr( + "cloudstack_security_group_rule.foo", "rule.3839437815.cidr_list.#", "1"), + resource.TestCheckResourceAttr( + "cloudstack_security_group_rule.foo", "rule.3839437815.cidr_list.3056857544", "172.18.100.0/24"), + resource.TestCheckResourceAttr( + "cloudstack_security_group_rule.foo", "rule.3839437815.icmp_code", "-1"), + resource.TestCheckResourceAttr( + "cloudstack_security_group_rule.foo", "rule.3839437815.icmp_type", "-1"), + resource.TestCheckResourceAttr( + "cloudstack_security_group_rule.foo", "rule.1804489748.protocol", "tcp"), + resource.TestCheckResourceAttr( + "cloudstack_security_group_rule.foo", "rule.1804489748.ports.#", "1"), + resource.TestCheckResourceAttr( + "cloudstack_security_group_rule.foo", "rule.1804489748.ports.1889509032", "80"), + resource.TestCheckResourceAttr( + "cloudstack_security_group_rule.foo", "rule.1804489748.traffic_type", "egress"), + resource.TestCheckResourceAttr( + "cloudstack_security_group_rule.foo", "rule.1804489748.user_security_group_list.1089118859", "terraform-security-group-bar"), + ), + }, + }, + }) +} + +func testAccCheckCloudStackSecurityGroupRulesExist(n string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No security group rule ID is set") + } + + cs := testAccProvider.Meta().(*cloudstack.CloudStackClient) + sg, count, err := cs.SecurityGroup.GetSecurityGroupByID(rs.Primary.ID) + if err != nil { + if count == 0 { + return fmt.Errorf("Security group %s not found", rs.Primary.ID) + } + 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 + } + + for k, id := range rs.Primary.Attributes { + if !strings.Contains(k, ".uuids.") || strings.HasSuffix(k, ".uuids.%") { + continue + } + + if _, ok := ruleIndex[id]; !ok { + return fmt.Errorf("Security group rule %s not found", id) + } + } + + return nil + } +} + +func testAccCheckCloudStackSecurityGroupRuleDestroy(s *terraform.State) error { + cs := testAccProvider.Meta().(*cloudstack.CloudStackClient) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "cloudstack_security_group_rule" { + continue + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No security group rule ID is set") + } + + sg, count, err := cs.SecurityGroup.GetSecurityGroupByID(rs.Primary.ID) + if err != nil { + if count == 0 { + continue + } + 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 + } + + for k, id := range rs.Primary.Attributes { + if !strings.Contains(k, ".uuids.") || strings.HasSuffix(k, ".uuids.%") { + continue + } + + if _, ok := ruleIndex[id]; ok { + return fmt.Errorf("Security group rule %s still exists", rs.Primary.ID) + } + } + } + + return nil +} + +var testAccCloudStackSecurityGroupRule_basic = fmt.Sprintf(` +resource "cloudstack_security_group" "foo" { + name = "terraform-security-group-foo" + description = "terraform-security-group-text" +} + +resource "cloudstack_security_group" "bar" { + name = "terraform-security-group-bar" + description = "terraform-security-group-text" +} + +resource "cloudstack_security_group_rule" "foo" { + security_group_id = "${cloudstack_security_group.foo.id}" + + rule { + cidr_list = ["172.18.100.0/24"] + protocol = "tcp" + ports = ["80"] + } + + rule { + protocol = "tcp" + ports = ["80", "443"] + traffic_type = "egress" + user_security_group_list = ["terraform-security-group-bar"] + } + + depends_on = ["cloudstack_security_group.bar"] +}`) + +var testAccCloudStackSecurityGroupRule_update = fmt.Sprintf(` +resource "cloudstack_security_group" "foo" { + name = "terraform-security-group-foo" + description = "terraform-security-group-text" +} + +resource "cloudstack_security_group" "bar" { + name = "terraform-security-group-bar" + description = "terraform-security-group-text" +} + +resource "cloudstack_security_group_rule" "foo" { + security_group_id = "${cloudstack_security_group.foo.id}" + + rule { + cidr_list = ["172.18.100.0/24", "172.18.200.0/24"] + protocol = "tcp" + ports = ["80", "443"] + } + + rule { + cidr_list = ["172.18.100.0/24"] + protocol = "icmp" + icmp_type = "-1" + icmp_code = "-1" + traffic_type = "ingress" + } + + rule { + protocol = "tcp" + ports = ["80"] + traffic_type = "egress" + user_security_group_list = ["terraform-security-group-bar"] + } + + depends_on = ["cloudstack_security_group.bar"] +}`) diff --git a/builtin/providers/cloudstack/resource_cloudstack_security_group_test.go b/builtin/providers/cloudstack/resource_cloudstack_security_group_test.go new file mode 100644 index 000000000..50a1ebcb4 --- /dev/null +++ b/builtin/providers/cloudstack/resource_cloudstack_security_group_test.go @@ -0,0 +1,100 @@ +package cloudstack + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "github.com/xanzy/go-cloudstack/cloudstack" +) + +func TestAccCloudStackSecurityGroup_basic(t *testing.T) { + var sg cloudstack.SecurityGroup + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackSecurityGroupDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccCloudStackSecurityGroup_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackSecurityGroupExists( + "cloudstack_security_group.foo", &sg), + testAccCheckCloudStackSecurityGroupBasicAttributes(&sg), + ), + }, + }, + }) +} + +func testAccCheckCloudStackSecurityGroupExists( + n string, sg *cloudstack.SecurityGroup) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No security group ID is set") + } + + cs := testAccProvider.Meta().(*cloudstack.CloudStackClient) + resp, _, err := cs.SecurityGroup.GetSecurityGroupByID(rs.Primary.ID) + if err != nil { + return err + } + + if resp.Id != rs.Primary.ID { + return fmt.Errorf("Network ACL not found") + } + + *sg = *resp + + return nil + } +} + +func testAccCheckCloudStackSecurityGroupBasicAttributes( + sg *cloudstack.SecurityGroup) resource.TestCheckFunc { + return func(s *terraform.State) error { + + if sg.Name != "terraform-security-group" { + return fmt.Errorf("Bad name: %s", sg.Name) + } + + if sg.Description != "terraform-security-group-text" { + return fmt.Errorf("Bad description: %s", sg.Description) + } + + return nil + } +} + +func testAccCheckCloudStackSecurityGroupDestroy(s *terraform.State) error { + cs := testAccProvider.Meta().(*cloudstack.CloudStackClient) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "cloudstack_security_group" { + continue + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No security group ID is set") + } + + _, _, err := cs.SecurityGroup.GetSecurityGroupByID(rs.Primary.ID) + if err == nil { + return fmt.Errorf("Security group list %s still exists", rs.Primary.ID) + } + } + + return nil +} + +var testAccCloudStackSecurityGroup_basic = fmt.Sprintf(` +resource "cloudstack_security_group" "foo" { + name = "terraform-security-group" + description = "terraform-security-group-text" +}`) diff --git a/vendor/github.com/xanzy/go-cloudstack/cloudstack/FirewallService.go b/vendor/github.com/xanzy/go-cloudstack/cloudstack/FirewallService.go index 2a7ddbe18..566643ab8 100644 --- a/vendor/github.com/xanzy/go-cloudstack/cloudstack/FirewallService.go +++ b/vendor/github.com/xanzy/go-cloudstack/cloudstack/FirewallService.go @@ -287,11 +287,6 @@ func (s *FirewallService) ListPortForwardingRules(p *ListPortForwardingRulesPara return nil, err } - resp, err = convertFirewallServiceResponse(resp) - if err != nil { - return nil, err - } - var r ListPortForwardingRulesResponse if err := json.Unmarshal(resp, &r); err != nil { return nil, err @@ -506,11 +501,6 @@ func (s *FirewallService) CreatePortForwardingRule(p *CreatePortForwardingRulePa return nil, err } - resp, err = convertFirewallServiceResponse(resp) - if err != nil { - return nil, err - } - var r CreatePortForwardingRuleResponse if err := json.Unmarshal(resp, &r); err != nil { return nil, err @@ -614,11 +604,6 @@ func (s *FirewallService) DeletePortForwardingRule(p *DeletePortForwardingRulePa return nil, err } - resp, err = convertFirewallServiceResponse(resp) - if err != nil { - return nil, err - } - var r DeletePortForwardingRuleResponse if err := json.Unmarshal(resp, &r); err != nil { return nil, err @@ -748,11 +733,6 @@ func (s *FirewallService) UpdatePortForwardingRule(p *UpdatePortForwardingRulePa return nil, err } - resp, err = convertFirewallServiceResponse(resp) - if err != nil { - return nil, err - } - var r UpdatePortForwardingRuleResponse if err := json.Unmarshal(resp, &r); err != nil { return nil, err @@ -951,11 +931,6 @@ func (s *FirewallService) CreateFirewallRule(p *CreateFirewallRuleParams) (*Crea return nil, err } - resp, err = convertFirewallServiceResponse(resp) - if err != nil { - return nil, err - } - var r CreateFirewallRuleResponse if err := json.Unmarshal(resp, &r); err != nil { return nil, err @@ -1055,11 +1030,6 @@ func (s *FirewallService) DeleteFirewallRule(p *DeleteFirewallRuleParams) (*Dele return nil, err } - resp, err = convertFirewallServiceResponse(resp) - if err != nil { - return nil, err - } - var r DeleteFirewallRuleResponse if err := json.Unmarshal(resp, &r); err != nil { return nil, err @@ -1306,11 +1276,6 @@ func (s *FirewallService) ListFirewallRules(p *ListFirewallRulesParams) (*ListFi return nil, err } - resp, err = convertFirewallServiceResponse(resp) - if err != nil { - return nil, err - } - var r ListFirewallRulesResponse if err := json.Unmarshal(resp, &r); err != nil { return nil, err @@ -1412,11 +1377,6 @@ func (s *FirewallService) UpdateFirewallRule(p *UpdateFirewallRuleParams) (*Upda return nil, err } - resp, err = convertFirewallServiceResponse(resp) - if err != nil { - return nil, err - } - var r UpdateFirewallRuleResponse if err := json.Unmarshal(resp, &r); err != nil { return nil, err @@ -1611,11 +1571,6 @@ func (s *FirewallService) CreateEgressFirewallRule(p *CreateEgressFirewallRulePa return nil, err } - resp, err = convertFirewallServiceResponse(resp) - if err != nil { - return nil, err - } - var r CreateEgressFirewallRuleResponse if err := json.Unmarshal(resp, &r); err != nil { return nil, err @@ -1715,11 +1670,6 @@ func (s *FirewallService) DeleteEgressFirewallRule(p *DeleteEgressFirewallRulePa return nil, err } - resp, err = convertFirewallServiceResponse(resp) - if err != nil { - return nil, err - } - var r DeleteEgressFirewallRuleResponse if err := json.Unmarshal(resp, &r); err != nil { return nil, err @@ -1966,11 +1916,6 @@ func (s *FirewallService) ListEgressFirewallRules(p *ListEgressFirewallRulesPara return nil, err } - resp, err = convertFirewallServiceResponse(resp) - if err != nil { - return nil, err - } - var r ListEgressFirewallRulesResponse if err := json.Unmarshal(resp, &r); err != nil { return nil, err @@ -2072,11 +2017,6 @@ func (s *FirewallService) UpdateEgressFirewallRule(p *UpdateEgressFirewallRulePa return nil, err } - resp, err = convertFirewallServiceResponse(resp) - if err != nil { - return nil, err - } - var r UpdateEgressFirewallRuleResponse if err := json.Unmarshal(resp, &r); err != nil { return nil, err @@ -2224,11 +2164,6 @@ func (s *FirewallService) AddPaloAltoFirewall(p *AddPaloAltoFirewallParams) (*Ad return nil, err } - resp, err = convertFirewallServiceResponse(resp) - if err != nil { - return nil, err - } - var r AddPaloAltoFirewallResponse if err := json.Unmarshal(resp, &r); err != nil { return nil, err @@ -2320,11 +2255,6 @@ func (s *FirewallService) DeletePaloAltoFirewall(p *DeletePaloAltoFirewallParams return nil, err } - resp, err = convertFirewallServiceResponse(resp) - if err != nil { - return nil, err - } - var r DeletePaloAltoFirewallResponse if err := json.Unmarshal(resp, &r); err != nil { return nil, err @@ -2409,11 +2339,6 @@ func (s *FirewallService) ConfigurePaloAltoFirewall(p *ConfigurePaloAltoFirewall return nil, err } - resp, err = convertFirewallServiceResponse(resp) - if err != nil { - return nil, err - } - var r ConfigurePaloAltoFirewallResponse if err := json.Unmarshal(resp, &r); err != nil { return nil, err @@ -2550,11 +2475,6 @@ func (s *FirewallService) ListPaloAltoFirewalls(p *ListPaloAltoFirewallsParams) return nil, err } - resp, err = convertFirewallServiceResponse(resp) - if err != nil { - return nil, err - } - var r ListPaloAltoFirewallsResponse if err := json.Unmarshal(resp, &r); err != nil { return nil, err diff --git a/vendor/github.com/xanzy/go-cloudstack/cloudstack/LICENSE b/vendor/github.com/xanzy/go-cloudstack/cloudstack/LICENSE new file mode 100644 index 000000000..5c304d1a4 --- /dev/null +++ b/vendor/github.com/xanzy/go-cloudstack/cloudstack/LICENSE @@ -0,0 +1,201 @@ +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/vendor/github.com/xanzy/go-cloudstack/cloudstack/SecurityGroupService.go b/vendor/github.com/xanzy/go-cloudstack/cloudstack/SecurityGroupService.go index 803442498..7a56c1cbb 100644 --- a/vendor/github.com/xanzy/go-cloudstack/cloudstack/SecurityGroupService.go +++ b/vendor/github.com/xanzy/go-cloudstack/cloudstack/SecurityGroupService.go @@ -24,6 +24,38 @@ import ( "strings" ) +// Helper function for maintaining backwards compatibility +func convertAuthorizeSecurityGroupIngressResponse(b []byte) ([]byte, error) { + var raw struct { + Ingressrule []interface{} `json:"ingressrule"` + } + if err := json.Unmarshal(b, &raw); err != nil { + return nil, err + } + + if len(raw.Ingressrule) != 1 { + return b, nil + } + + return json.Marshal(raw.Ingressrule[0]) +} + +// Helper function for maintaining backwards compatibility +func convertAuthorizeSecurityGroupEgressResponse(b []byte) ([]byte, error) { + var raw struct { + Egressrule []interface{} `json:"egressrule"` + } + if err := json.Unmarshal(b, &raw); err != nil { + return nil, err + } + + if len(raw.Egressrule) != 1 { + return b, nil + } + + return json.Marshal(raw.Egressrule[0]) +} + type CreateSecurityGroupParams struct { p map[string]interface{} } @@ -472,6 +504,11 @@ func (s *SecurityGroupService) AuthorizeSecurityGroupIngress(p *AuthorizeSecurit return nil, err } + b, err = convertAuthorizeSecurityGroupIngressResponse(b) + if err != nil { + return nil, err + } + if err := json.Unmarshal(b, &r); err != nil { return nil, err } @@ -760,6 +797,11 @@ func (s *SecurityGroupService) AuthorizeSecurityGroupEgress(p *AuthorizeSecurity return nil, err } + b, err = convertAuthorizeSecurityGroupEgressResponse(b) + if err != nil { + return nil, err + } + if err := json.Unmarshal(b, &r); err != nil { return nil, err } diff --git a/vendor/vendor.json b/vendor/vendor.json index 22fa8100e..a8563ce42 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -2137,11 +2137,11 @@ "revision": "9051bd6b44125d9472e0c148b5965692ab283d4a" }, { - "checksumSHA1": "s6bz9U3gi638R0Ln7v14h5+e2Wg=", + "checksumSHA1": "tWnkfXhmDWitjF0TMHY7gy3Kt0k=", "comment": "v2.1.3", "path": "github.com/xanzy/go-cloudstack/cloudstack", - "revision": "ff444b1afdd1961f8118bad30db9fa27cb989ced", - "revisionTime": "2016-10-24T10:36:39Z" + "revision": "deca6e17c056012b540aa2d2eae3ea1e63203b85", + "revisionTime": "2016-10-26T18:16:49Z" }, { "path": "github.com/xanzy/ssh-agent", diff --git a/website/source/docs/providers/cloudstack/r/instance.html.markdown b/website/source/docs/providers/cloudstack/r/instance.html.markdown index 15470817c..7d82c597a 100644 --- a/website/source/docs/providers/cloudstack/r/instance.html.markdown +++ b/website/source/docs/providers/cloudstack/r/instance.html.markdown @@ -15,11 +15,11 @@ disk offering, and template. ``` resource "cloudstack_instance" "web" { - name = "server-1" - service_offering= "small" - network_id = "6eb22f91-7454-4107-89f4-36afcdf33021" - template = "CentOS 6.5" - zone = "zone-1" + name = "server-1" + service_offering= "small" + network_id = "6eb22f91-7454-4107-89f4-36afcdf33021" + template = "CentOS 6.5" + zone = "zone-1" } ``` diff --git a/website/source/docs/providers/cloudstack/r/security_group.html.markdown b/website/source/docs/providers/cloudstack/r/security_group.html.markdown index 418906f0b..2e1f50bb0 100644 --- a/website/source/docs/providers/cloudstack/r/security_group.html.markdown +++ b/website/source/docs/providers/cloudstack/r/security_group.html.markdown @@ -1,14 +1,14 @@ --- layout: "cloudstack" page_title: "CloudStack: cloudstack_security_group" -sidebar_current: "docs-cloudstack-resource-security_group" +sidebar_current: "docs-cloudstack-resource-security-group" description: |- - Creates security group. + Creates a security group. --- # cloudstack\_security\_group -Creates security group. +Creates a security group. ## Example Usage @@ -16,21 +16,6 @@ Creates security group. resource "cloudstack_security_group" "default" { name = "allow_web" description = "Allow access to HTTP and HTTPS" - - rules = [ - { - cidr_list = "0.0.0.0/0" - protocol = "tcp" - ports = "80" - traffic_type = "ingress" - }, - { - cidr_list = "0.0.0.0/0" - protocol = "tcp" - ports = "443" - traffic_type = "ingress" - }, - ] } ``` @@ -38,31 +23,18 @@ resource "cloudstack_security_group" "default" { The following arguments are supported: -* `name` - (Required) The Name of the Security Group to create. Changing this - forces a new resource to be created. +* `name` - (Required) The name of the security group. Changing this forces a + new resource to be created. -* `description` - (Optional) The description of the Security Group to create. - Changing this forces a new resource to be created. +* `description` - (Optional) The description of the security group. Changing + this forces a new resource to be created. -* `rules` - (Optional) List of rule blocks, supported fields documented below. - -The `rule` block supports: - -* `cidr_list` - (Optional) A CIDR list to allow access to the given ports. - -* `security_group` - (Optional) A Security Group to apply the rules to. - -* `protocol` - (Required) The name of the protocol to allow. Valid options are: - `tcp`, `udp` and `icmp`. - -* `ports` - (Optional) List of ports and/or port ranges to allow. This can only - be specified if the protocol is TCP or UDP. - -* `traffic_type` - (Optional) Weither Ingress or Egress. (Default: Ingress). +* `project` - (Optional) The name or ID of the project to create this security + group in. Changing this forces a new resource to be created. ## Attributes Reference The following attributes are exported: -* `name` - The name of the Security Group. +* `id` - The ID of the security group. diff --git a/website/source/docs/providers/cloudstack/r/security_group_rule.html.markdown b/website/source/docs/providers/cloudstack/r/security_group_rule.html.markdown new file mode 100644 index 000000000..f0aef6237 --- /dev/null +++ b/website/source/docs/providers/cloudstack/r/security_group_rule.html.markdown @@ -0,0 +1,72 @@ +--- +layout: "cloudstack" +page_title: "CloudStack: cloudstack_security_group_rule" +sidebar_current: "docs-cloudstack-resource-security-group-rule" +description: |- + Authorizes and revokes both ingress and egress rulea for a given security group. +--- + +# cloudstack\_security\_group\_rule + +Authorizes and revokes both ingress and egress rulea for a given security group. + +## Example Usage + +``` +resource "cloudstack_security_group_rule" "web" { + security_group_id = "e340b62b-fbc2-4081-8f67-e40455c44bce" + + rule { + cidr_list = ["0.0.0.0/0"] + protocol = "tcp" + ports = ["80", "443"] + } + + rule { + cidr_list = ["192.168.0.0/24", "192.168.1.0/25"] + protocol = "tcp" + ports = ["80-90", "443"] + traffic_type = "egress" + user_security_group_list = ["group01", "group02"] + } +} +``` + +## Argument Reference + +The following arguments are supported: + +* `security_group_id` - (Required) The security group ID for which to create + the rules. Changing this forces a new resource to be created. + +* `rule` - (Required) Can be specified multiple times. Each rule block supports + fields documented below. + +The `rule` block supports: + +* `cidr_list` - (Optional) A CIDR list to allow access to the given ports. + +* `protocol` - (Required) The name of the protocol to allow. Valid options are: + `tcp`, `udp`, `icmp`, `all` or a valid protocol number. + +* `icmp_type` - (Optional) The ICMP type to allow, or `-1` to allow `any`. This + can only be specified if the protocol is ICMP. (defaults 0) + +* `icmp_code` - (Optional) The ICMP code to allow, or `-1` to allow `any`. This + can only be specified if the protocol is ICMP. (defaults 0) + +* `ports` - (Optional) List of ports and/or port ranges to allow. This can only + be specified if the protocol is TCP, UDP, ALL or a valid protocol number. + +* `traffic_type` - (Optional) The traffic type for the rule. Valid options are: + `ingress` or `egress` (defaults ingress). + +* `user_security_group_list` - (Optional) A list of security groups to apply + the rules to. + +## Attributes Reference + +The following attributes are exported: + +* `id` - The security group ID for which the rules are created. + diff --git a/website/source/layouts/cloudstack.erb b/website/source/layouts/cloudstack.erb index c460d236f..ab99da78b 100644 --- a/website/source/layouts/cloudstack.erb +++ b/website/source/layouts/cloudstack.erb @@ -65,6 +65,14 @@ cloudstack_secondary_ipaddress + > + cloudstack_security_group + + + > + cloudstack_security_group_rule + + > cloudstack_ssh_keypair