| // |
| // Licensed to the Apache Software Foundation (ASF) under one |
| // or more contributor license agreements. See the NOTICE file |
| // distributed with this work for additional information |
| // regarding copyright ownership. The ASF licenses this file |
| // to you 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. |
| // |
| |
| package cloudstack |
| |
| import ( |
| "fmt" |
| "log" |
| "strconv" |
| "strings" |
| "sync" |
| "time" |
| |
| "github.com/apache/cloudstack-go/v2/cloudstack" |
| "github.com/hashicorp/go-multierror" |
| "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" |
| ) |
| |
| func resourceCloudStackNetworkACLRule() *schema.Resource { |
| return &schema.Resource{ |
| Create: resourceCloudStackNetworkACLRuleCreate, |
| Read: resourceCloudStackNetworkACLRuleRead, |
| Update: resourceCloudStackNetworkACLRuleUpdate, |
| Delete: resourceCloudStackNetworkACLRuleDelete, |
| Importer: &schema.ResourceImporter{ |
| State: resourceCloudStackNetworkACLRuleImport, |
| }, |
| |
| Schema: map[string]*schema.Schema{ |
| "acl_id": { |
| Type: schema.TypeString, |
| Required: true, |
| ForceNew: true, |
| }, |
| |
| "managed": { |
| Type: schema.TypeBool, |
| Optional: true, |
| Default: false, |
| }, |
| |
| "rule": { |
| Type: schema.TypeList, |
| Optional: true, |
| Elem: &schema.Resource{ |
| Schema: map[string]*schema.Schema{ |
| "rule_number": { |
| Type: schema.TypeInt, |
| Optional: true, |
| Computed: true, |
| }, |
| |
| "action": { |
| Type: schema.TypeString, |
| Optional: true, |
| Default: "allow", |
| }, |
| |
| "cidr_list": { |
| Type: schema.TypeSet, |
| Required: true, |
| Elem: &schema.Schema{Type: schema.TypeString}, |
| Set: schema.HashString, |
| }, |
| |
| "protocol": { |
| Type: schema.TypeString, |
| Required: true, |
| }, |
| |
| "icmp_type": { |
| Type: schema.TypeInt, |
| Optional: true, |
| Computed: true, |
| }, |
| |
| "icmp_code": { |
| Type: schema.TypeInt, |
| Optional: true, |
| Computed: true, |
| }, |
| |
| "port": { |
| Type: schema.TypeString, |
| Optional: true, |
| }, |
| |
| "ports": { |
| Type: schema.TypeSet, |
| Optional: true, |
| Elem: &schema.Schema{Type: schema.TypeString}, |
| Set: schema.HashString, |
| Deprecated: "Use 'port' instead. 'ports' will be removed in a future version.", |
| }, |
| |
| "traffic_type": { |
| Type: schema.TypeString, |
| Optional: true, |
| Default: "ingress", |
| }, |
| |
| "description": { |
| Type: schema.TypeString, |
| Optional: true, |
| }, |
| |
| "uuids": { |
| Type: schema.TypeMap, |
| Computed: true, |
| }, |
| }, |
| }, |
| }, |
| |
| "project": { |
| Type: schema.TypeString, |
| Optional: true, |
| ForceNew: true, |
| }, |
| |
| "parallelism": { |
| Type: schema.TypeInt, |
| Optional: true, |
| Default: 2, |
| }, |
| }, |
| } |
| } |
| |
| func resourceCloudStackNetworkACLRuleCreate(d *schema.ResourceData, meta interface{}) error { |
| // Make sure all required parameters are there |
| if err := verifyNetworkACLParams(d); err != nil { |
| return err |
| } |
| |
| // We need to set this upfront in order to be able to save a partial state |
| d.SetId(d.Get("acl_id").(string)) |
| |
| // Create all rules that are configured |
| if nrs := d.Get("rule").([]interface{}); len(nrs) > 0 { |
| rules := make([]interface{}, 0, len(nrs)) |
| |
| err := createNetworkACLRules(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 resourceCloudStackNetworkACLRuleRead(d, meta) |
| } |
| |
| func createNetworkACLRules(d *schema.ResourceData, meta interface{}, rules *[]interface{}, nrs []interface{}) error { |
| var errs *multierror.Error |
| |
| var wg sync.WaitGroup |
| wg.Add(len(nrs)) |
| |
| sem := make(chan struct{}, d.Get("parallelism").(int)) |
| for _, rule := range nrs { |
| // 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{}{} |
| |
| // Create a single rule |
| err := createNetworkACLRule(d, meta, rule) |
| |
| // If we have at least one UUID, we need to save the rule |
| if len(rule["uuids"].(map[string]interface{})) > 0 { |
| *rules = append(*rules, rule) |
| } |
| |
| if err != nil { |
| errs = multierror.Append(errs, err) |
| } |
| |
| <-sem |
| }(rule.(map[string]interface{})) |
| } |
| |
| wg.Wait() |
| |
| return errs.ErrorOrNil() |
| } |
| |
| func createNetworkACLRule(d *schema.ResourceData, meta interface{}, rule map[string]interface{}) error { |
| cs := meta.(*cloudstack.CloudStackClient) |
| uuids := rule["uuids"].(map[string]interface{}) |
| |
| // Make sure all required parameters are there |
| if err := verifyNetworkACLRuleParams(d, rule); err != nil { |
| return err |
| } |
| |
| // Create a new parameter struct |
| p := cs.NetworkACL.NewCreateNetworkACLParams(rule["protocol"].(string)) |
| |
| // If a rule ID is specified, set it |
| if ruleNum, ok := rule["rule_number"].(int); ok && ruleNum > 0 { |
| p.SetNumber(ruleNum) |
| } |
| |
| // Set the acl ID |
| p.SetAclid(d.Id()) |
| |
| // Set the action |
| p.SetAction(rule["action"].(string)) |
| |
| // Set the CIDR list |
| var cidrList []string |
| for _, cidr := range rule["cidr_list"].(*schema.Set).List() { |
| cidrList = append(cidrList, cidr.(string)) |
| } |
| p.SetCidrlist(cidrList) |
| |
| // Set the traffic type |
| p.SetTraffictype(rule["traffic_type"].(string)) |
| |
| // Set the description |
| if desc, ok := rule["description"].(string); ok && desc != "" { |
| p.SetReason(desc) |
| } |
| |
| // 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)) |
| |
| r, err := Retry(4, retryableACLCreationFunc(cs, p)) |
| if err != nil { |
| return err |
| } |
| |
| uuids["icmp"] = r.(*cloudstack.CreateNetworkACLResponse).Id |
| rule["uuids"] = uuids |
| } |
| |
| // If the protocol is ALL set the needed parameters |
| if rule["protocol"].(string) == "all" { |
| r, err := Retry(4, retryableACLCreationFunc(cs, p)) |
| if err != nil { |
| return err |
| } |
| |
| uuids["all"] = r.(*cloudstack.CreateNetworkACLResponse).Id |
| rule["uuids"] = uuids |
| } |
| |
| var portStr string |
| if port, ok := rule["port"].(string); ok && port != "" { |
| portStr = port |
| if ports, ok := rule["ports"].(*schema.Set); ok && ports.Len() > 0 { |
| log.Printf("[WARN] Deprecated 'ports' is ignored. Only 'port' is used. Remove 'ports' from your config.") |
| } |
| } else if ports, ok := rule["ports"].(*schema.Set); ok && ports.Len() > 0 { |
| // Deprecated: use first port or join as range if two values |
| if ruleNum, ok := rule["rule_number"].(int); ok && ruleNum > 0 { |
| return fmt.Errorf("'ports' cannot be used with 'rule_number'. Please migrate to 'port' (string) for numbered rules.") |
| } |
| list := ports.List() |
| if len(list) == 1 { |
| portStr = list[0].(string) |
| } else if len(list) == 2 { |
| start := list[0].(string) |
| end := list[1].(string) |
| if strings.Contains(start, "-") || strings.Contains(end, "-") { |
| return fmt.Errorf("If specifying a port range, use a single string like '1000-2000' in 'port'. Do not mix ranges and single ports.") |
| } |
| portStr = fmt.Sprintf("%s-%s", start, end) |
| } else { |
| return fmt.Errorf("'ports' must have one or two values only. Got: %v", list) |
| } |
| log.Printf("[WARN] 'ports' is deprecated. Use 'port' instead.") |
| } else { |
| if rule["protocol"].(string) == "tcp" || rule["protocol"].(string) == "udp" { |
| return fmt.Errorf("Parameter port is required for protocol 'tcp' or 'udp'. Use 'port' (string) for new configs.") |
| } |
| } |
| if portStr != "" { |
| m := splitPorts.FindStringSubmatch(portStr) |
| if m == nil { |
| return fmt.Errorf("%q is not a valid port value. Valid options are '80' or '80-90'", portStr) |
| } |
| startPort, _ := strconv.Atoi(m[1]) |
| endPort := startPort |
| if m[2] != "" { |
| endPort, _ = strconv.Atoi(m[2]) |
| } |
| p.SetStartport(startPort) |
| p.SetEndport(endPort) |
| |
| r, err := Retry(4, retryableACLCreationFunc(cs, p)) |
| if err != nil { |
| return err |
| } |
| |
| uuids[portStr] = r.(*cloudstack.CreateNetworkACLResponse).Id |
| rule["uuids"] = uuids |
| } |
| |
| return nil |
| } |
| |
| func resourceCloudStackNetworkACLRuleRead(d *schema.ResourceData, meta interface{}) error { |
| cs := meta.(*cloudstack.CloudStackClient) |
| |
| // First check if the ACL itself still exists |
| _, count, err := cs.NetworkACL.GetNetworkACLListByID( |
| d.Id(), |
| cloudstack.WithProject(d.Get("project").(string)), |
| ) |
| if err != nil { |
| if count == 0 { |
| d.SetId("") |
| return nil |
| } |
| return err |
| } |
| |
| p := cs.NetworkACL.NewListNetworkACLsParams() |
| p.SetAclid(d.Id()) |
| p.SetListall(true) |
| |
| l, err := cs.NetworkACL.ListNetworkACLs(p) |
| if err != nil { |
| return err |
| } |
| |
| ruleMap := make(map[string]*cloudstack.NetworkACL, l.Count) |
| for _, r := range l.NetworkACLs { |
| ruleMap[r.Id] = r |
| } |
| |
| rules := make([]interface{}, 0, len(ruleMap)) |
| |
| if rs, ok := d.Get("rule").([]interface{}); ok && len(rs) > 0 { |
| for _, rule := range rs { |
| rule := rule.(map[string]interface{}) |
| matched := false |
| // 1. Try to match by UUID |
| if uuids, ok := rule["uuids"].(map[string]interface{}); ok { |
| for uuid := range uuids { |
| if r, ok := ruleMap[uuid]; ok { |
| cidrs := &schema.Set{F: schema.HashString} |
| for _, cidr := range strings.Split(r.Cidrlist, ",") { |
| cidrs.Add(cidr) |
| } |
| rule["action"] = strings.ToLower(r.Action) |
| rule["protocol"] = r.Protocol |
| rule["traffic_type"] = strings.ToLower(r.Traffictype) |
| rule["cidr_list"] = cidrs |
| rule["rule_number"] = int(r.Number) |
| if desc, ok := rule["description"].(string); ok && desc != "" { |
| if desc == r.Reason { |
| rule["description"] = r.Reason |
| } |
| } else if r.Reason != "" { |
| rule["description"] = r.Reason |
| } else { |
| rule["description"] = "" |
| } |
| if r.Protocol == "tcp" || r.Protocol == "udp" { |
| if r.Startport == r.Endport { |
| rule["port"] = r.Startport |
| } else { |
| rule["port"] = r.Startport + "-" + r.Endport |
| } |
| } else { |
| rule["port"] = "" |
| } |
| rule["uuids"] = map[string]interface{}{r.Id: r.Id} |
| matched = true |
| delete(ruleMap, r.Id) |
| break |
| } |
| } |
| } |
| // 2. If not found by UUID, match by all identity fields (rule_number, protocol, port, cidr, traffic_type) |
| if !matched { |
| for _, r := range ruleMap { |
| if int(r.Number) == rule["rule_number"].(int) && |
| strings.ToLower(r.Protocol) == strings.ToLower(rule["protocol"].(string)) && |
| strings.ToLower(r.Traffictype) == strings.ToLower(rule["traffic_type"].(string)) && |
| matchACLRuleByNumberAndFields(r, rule) { |
| cidrs := &schema.Set{F: schema.HashString} |
| for _, cidr := range strings.Split(r.Cidrlist, ",") { |
| cidrs.Add(cidr) |
| } |
| rule["action"] = strings.ToLower(r.Action) |
| rule["protocol"] = r.Protocol |
| rule["traffic_type"] = strings.ToLower(r.Traffictype) |
| rule["cidr_list"] = cidrs |
| rule["rule_number"] = int(r.Number) |
| if desc, ok := rule["description"].(string); ok && desc != "" { |
| if desc == r.Reason { |
| rule["description"] = r.Reason |
| } |
| } else if r.Reason != "" { |
| rule["description"] = r.Reason |
| } else { |
| rule["description"] = "" |
| } |
| if r.Protocol == "tcp" || r.Protocol == "udp" { |
| if r.Startport == r.Endport { |
| rule["port"] = r.Startport |
| } else { |
| rule["port"] = r.Startport + "-" + r.Endport |
| } |
| } else { |
| rule["port"] = "" |
| } |
| rule["uuids"] = map[string]interface{}{r.Id: r.Id} |
| matched = true |
| delete(ruleMap, r.Id) |
| break |
| } |
| } |
| } |
| // 3. If not found, do NOT update rule_number or other identity fields; just keep config values |
| rules = append(rules, rule) |
| } |
| } |
| |
| managed := d.Get("managed").(bool) |
| if managed && len(ruleMap) > 0 { |
| for uuid := range ruleMap { |
| cidrs := &schema.Set{F: schema.HashString} |
| cidrs.Add(uuid) |
| rule := map[string]interface{}{ |
| "cidr_list": cidrs, |
| "protocol": uuid, |
| "uuids": map[string]interface{}{uuid: uuid}, |
| } |
| rules = append(rules, rule) |
| } |
| } |
| |
| if len(rules) > 0 { |
| d.Set("rule", rules) |
| } else if !managed { |
| d.SetId("") |
| } |
| |
| return nil |
| } |
| |
| // Matches a CloudStack rule to a Terraform rule by rule_number, protocol, cidr, traffic_type, and port |
| func matchACLRuleByNumberAndFields(r *cloudstack.NetworkACL, rule map[string]interface{}) bool { |
| if ruleNum, ok := rule["rule_number"].(int); ok && ruleNum > 0 { |
| if int(r.Number) != ruleNum { |
| return false |
| } |
| } |
| if strings.ToLower(r.Protocol) != strings.ToLower(rule["protocol"].(string)) { |
| return false |
| } |
| if strings.ToLower(r.Traffictype) != strings.ToLower(rule["traffic_type"].(string)) { |
| return false |
| } |
| cidrSet := map[string]struct{}{} |
| for _, c := range strings.Split(r.Cidrlist, ",") { |
| cidrSet[strings.TrimSpace(c)] = struct{}{} |
| } |
| for _, c := range rule["cidr_list"].(*schema.Set).List() { |
| if _, ok := cidrSet[c.(string)]; !ok { |
| return false |
| } |
| } |
| portStr := "" |
| if p, ok := rule["port"].(string); ok { |
| portStr = p |
| } |
| startPort, _ := strconv.Atoi(r.Startport) |
| endPort, _ := strconv.Atoi(r.Endport) |
| if portStr != "" { |
| if strings.Contains(portStr, "-") { |
| parts := strings.SplitN(portStr, "-", 2) |
| sp, _ := strconv.Atoi(parts[0]) |
| ep, _ := strconv.Atoi(parts[1]) |
| if sp != startPort || ep != endPort { |
| return false |
| } |
| } else { |
| sp, _ := strconv.Atoi(portStr) |
| if sp != startPort || sp != endPort { |
| return false |
| } |
| } |
| } |
| return true |
| } |
| |
| func resourceCloudStackNetworkACLRuleUpdate(d *schema.ResourceData, meta interface{}) error { |
| // Make sure all required parameters are there |
| if err := verifyNetworkACLParams(d); err != nil { |
| return err |
| } |
| |
| // Check if the rule set as a whole has changed |
| if d.HasChange("rule") { |
| o, n := d.GetChange("rule") |
| oldRules := o.([]interface{}) // remote (from CloudStack) |
| newRules := n.([]interface{}) // config (planned) |
| |
| // Build UUID -> rule maps for old (remote) and new (config) rules using CloudStack rule IDs as keys |
| oldUUIDMap := map[string]map[string]interface{}{} |
| newUUIDMap := map[string]map[string]interface{}{} |
| for _, rule := range oldRules { |
| r := rule.(map[string]interface{}) |
| for _, id := range r["uuids"].(map[string]interface{}) { |
| oldUUIDMap[id.(string)] = r |
| } |
| } |
| for _, rule := range newRules { |
| r := rule.(map[string]interface{}) |
| for _, id := range r["uuids"].(map[string]interface{}) { |
| newUUIDMap[id.(string)] = r |
| } |
| } |
| |
| // For each CloudStack rule ID present in both old and new, update if any relevant field changed (compare config to remote) |
| updateFields := []string{"action", "cidr_list", "icmp_code", "icmp_type", "protocol", "description", "port", "traffic_type", "rule_number"} // add rule_number |
| updated := map[string]bool{} |
| for uuid, oldRule := range oldUUIDMap { // oldRule = remote, newRule = config |
| if newRule, ok := newUUIDMap[uuid]; ok && !updated[uuid] { |
| for _, field := range updateFields { |
| oldVal := normalizeField(oldRule[field]) |
| newVal := normalizeField(newRule[field]) |
| if oldVal != newVal { |
| log.Printf("[DEBUG] Updating ACL rule for UUID %s: field %s changed (remote=%v, config=%v)", uuid, field, oldRule[field], newRule[field]) |
| if err := updateNetworkACLRule(d, meta, newRule); err != nil { |
| return err |
| } |
| break |
| } |
| } |
| updated[uuid] = true |
| } |
| } |
| // Remove delete+create for rule_number change |
| // Create only truly new rules (no UUID) |
| for _, rule := range newRules { |
| r := rule.(map[string]interface{}) |
| if len(r["uuids"].(map[string]interface{})) == 0 { |
| if err := createNetworkACLRule(d, meta, r); err != nil { |
| return err |
| } |
| } |
| } |
| // Delete only truly removed rules |
| // Find rules in oldRules not present in newRules by UUID |
| toDelete := make([]map[string]interface{}, 0) |
| for uuid, oldRule := range oldUUIDMap { |
| if _, ok := newUUIDMap[uuid]; !ok { |
| toDelete = append(toDelete, oldRule) |
| } |
| } |
| if len(toDelete) > 0 { |
| for _, rule := range toDelete { |
| if err := deleteNetworkACLRule(d, meta, rule); err != nil { |
| return err |
| } |
| } |
| } |
| // Update state |
| d.Set("rule", newRules) |
| } |
| |
| return resourceCloudStackNetworkACLRuleRead(d, meta) |
| } |
| |
| // updateNetworkACLRule updates a single ACL rule in CloudStack |
| func updateNetworkACLRule(d *schema.ResourceData, meta interface{}, rule map[string]interface{}) error { |
| cs := meta.(*cloudstack.CloudStackClient) |
| uuids := rule["uuids"].(map[string]interface{}) |
| |
| for _, id := range uuids { |
| p := cs.NetworkACL.NewUpdateNetworkACLItemParams(id.(string)) |
| p.SetAction(rule["action"].(string)) |
| p.SetCidrlist(expandStringSet(rule["cidr_list"].(*schema.Set))) |
| p.SetProtocol(rule["protocol"].(string)) |
| p.SetTraffictype(rule["traffic_type"].(string)) |
| if desc, ok := rule["description"].(string); ok && desc != "" { |
| p.SetReason(desc) |
| } |
| if rule["protocol"].(string) == "icmp" { |
| p.SetIcmptype(rule["icmp_type"].(int)) |
| p.SetIcmpcode(rule["icmp_code"].(int)) |
| } |
| if rule["protocol"].(string) == "tcp" || rule["protocol"].(string) == "udp" { |
| if port, ok := rule["port"].(string); ok && port != "" { |
| m := splitPorts.FindStringSubmatch(port) |
| startPort, _ := strconv.Atoi(m[1]) |
| endPort := startPort |
| if m[2] != "" { |
| endPort, _ = strconv.Atoi(m[2]) |
| } |
| p.SetStartport(startPort) |
| p.SetEndport(endPort) |
| } |
| } |
| if ruleNum, ok := rule["rule_number"].(int); ok && ruleNum > 0 { |
| p.SetNumber(ruleNum) |
| } |
| _, err := cs.NetworkACL.UpdateNetworkACLItem(p) |
| if err != nil { |
| return err |
| } |
| } |
| return nil |
| } |
| |
| func resourceCloudStackNetworkACLRuleDelete(d *schema.ResourceData, meta interface{}) error { |
| // Create an empty rule slice to hold all rules that were not deleted correctly |
| rules := make([]interface{}, 0) |
| |
| // Delete all rules |
| if ors, ok := d.Get("rule").([]interface{}); ok && len(ors) > 0 { |
| for _, rule := range ors { |
| if err := deleteNetworkACLRule(d, meta, rule.(map[string]interface{})); err != nil { |
| // If we have at least one UUID, we need to save the rule |
| if len(rule.(map[string]interface{})["uuids"].(map[string]interface{})) > 0 { |
| rules = append(rules, rule) |
| } |
| return err |
| } |
| } |
| // We need to update this first to preserve the correct state |
| d.Set("rule", rules) |
| } |
| |
| return nil |
| } |
| |
| func deleteNetworkACLRules(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{}{} |
| |
| // Delete a single rule |
| err := deleteNetworkACLRule(d, meta, rule) |
| |
| // If we have at least one UUID, we need to save the rule |
| if len(rule["uuids"].(map[string]interface{})) > 0 { |
| rules.Add(rule) |
| } |
| |
| if err != nil { |
| err = multierror.Append(errs, err) |
| } |
| |
| <-sem |
| }(rule.(map[string]interface{})) |
| } |
| |
| wg.Wait() |
| |
| return errs.ErrorOrNil() |
| } |
| |
| func deleteNetworkACLRule(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 |
| } |
| |
| // Create the parameter struct |
| p := cs.NetworkACL.NewDeleteNetworkACLParams(id.(string)) |
| |
| // Delete the rule |
| if _, err := cs.NetworkACL.DeleteNetworkACL(p); 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) |
| rule["uuids"] = uuids |
| continue |
| } |
| |
| return err |
| } |
| |
| // Delete the UUID of this rule |
| delete(uuids, k) |
| rule["uuids"] = uuids |
| } |
| |
| return nil |
| } |
| |
| func verifyNetworkACLParams(d *schema.ResourceData) error { |
| managed := d.Get("managed").(bool) |
| _, rules := d.GetOk("rule") |
| |
| if !rules && !managed { |
| return fmt.Errorf( |
| "You must supply at least one 'rule' when not using the 'managed' firewall feature") |
| } |
| |
| return nil |
| } |
| |
| func verifyNetworkACLRuleParams(d *schema.ResourceData, rule map[string]interface{}) error { |
| // Disallow 'ports' for anything except deletes (backward compatibility) |
| if ports, ok := rule["ports"].(*schema.Set); ok && ports.Len() > 0 { |
| // Only allow deletes (rule is being removed) |
| if d != nil && d.HasChange("rule") { |
| o, n := d.GetChange("rule") |
| ors := o.(*schema.Set).Difference(n.(*schema.Set)) |
| isDelete := false |
| for _, r := range ors.List() { |
| rm := r.(map[string]interface{}) |
| if rm["ports"].(*schema.Set).Len() > 0 { |
| isDelete = true |
| break |
| } |
| } |
| if !isDelete { |
| return fmt.Errorf("The 'ports' attribute is deprecated and not allowed for new or updated rules. Please migrate to 'port' (string). Only deletion of existing rules with 'ports' is allowed.") |
| } |
| } else { |
| return fmt.Errorf("The 'ports' attribute is deprecated and not allowed for new or updated rules. Please migrate to 'port' (string). Only deletion of existing rules with 'ports' is allowed.") |
| } |
| } |
| // Disallow 'ports' with 'rule_number' |
| if ports, ok := rule["ports"].(*schema.Set); ok && ports.Len() > 0 { |
| if ruleNum, ok := rule["rule_number"].(int); ok && ruleNum > 0 { |
| return fmt.Errorf("'ports' cannot be used with 'rule_number'. Please migrate to 'port' (schema.TypeString) for numbered rules.") |
| } |
| } |
| |
| action := rule["action"].(string) |
| if action != "allow" && action != "deny" { |
| return fmt.Errorf("Parameter action only accepts 'allow' or 'deny' as values") |
| } |
| |
| 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 "all": |
| // No additional test are needed, so just leave this empty... |
| case "tcp", "udp": |
| // Error if both ports and rule_number are set (must be first check) |
| if ports, ok := rule["ports"].(*schema.Set); ok && ports.Len() > 0 { |
| if ruleNum, ok := rule["rule_number"].(int); ok && ruleNum > 0 { |
| return fmt.Errorf("'ports' cannot be used with 'rule_number'. Please migrate to 'port' (schema.TypeString) for numbered rules.") |
| } |
| } |
| var portStr string |
| if port, ok := rule["port"].(string); ok && port != "" { |
| portStr = port |
| } else if ports, ok := rule["ports"].(*schema.Set); ok && ports.Len() > 0 { |
| list := ports.List() |
| if len(list) == 1 { |
| portStr = list[0].(string) |
| } else if len(list) == 2 { |
| start := list[0].(string) |
| end := list[1].(string) |
| if strings.Contains(start, "-") || strings.Contains(end, "-") { |
| return fmt.Errorf("If specifying a port range, use a single string like '1000-2000' in 'port'. Do not mix ranges and single ports.") |
| } |
| portStr = fmt.Sprintf("%s-%s", start, end) |
| } else { |
| return fmt.Errorf("'ports' must have one or two values only. Got: %v", list) |
| } |
| log.Printf("[WARN] 'ports' is deprecated. Use 'port' instead.") |
| } |
| if portStr != "" { |
| m := splitPorts.FindStringSubmatch(portStr) |
| if m == nil { |
| return fmt.Errorf("%q is not a valid port value. Valid options are '80' or '80-90'", portStr) |
| } |
| } else { |
| return fmt.Errorf("Parameter port 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', "+ |
| "'icmp', 'all' or a valid protocol number", 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 |
| } |
| |
| func retryableACLCreationFunc( |
| cs *cloudstack.CloudStackClient, |
| p *cloudstack.CreateNetworkACLParams) func() (interface{}, error) { |
| return func() (interface{}, error) { |
| r, err := cs.NetworkACL.CreateNetworkACL(p) |
| if err != nil { |
| return nil, err |
| } |
| return r, nil |
| } |
| } |
| |
| func resourceCloudStackNetworkACLRuleImport(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { |
| cs := meta.(*cloudstack.CloudStackClient) |
| |
| aclID := d.Id() |
| |
| log.Printf("[DEBUG] Attempting to import ACL list with ID: %s", aclID) |
| if aclExists, err := checkACLListExists(cs, aclID); err != nil { |
| return nil, fmt.Errorf("error checking ACL list existence: %v", err) |
| } else if !aclExists { |
| return nil, fmt.Errorf("ACL list with ID %s does not exist", aclID) |
| } |
| |
| log.Printf("[DEBUG] Found ACL list with ID: %s", aclID) |
| d.Set("acl_id", aclID) |
| |
| log.Printf("[DEBUG] Setting managed=true for ACL list import") |
| d.Set("managed", true) |
| |
| return []*schema.ResourceData{d}, nil |
| } |
| |
| func checkACLListExists(cs *cloudstack.CloudStackClient, aclID string) (bool, error) { |
| log.Printf("[DEBUG] Checking if ACL list exists: %s", aclID) |
| _, count, err := cs.NetworkACL.GetNetworkACLListByID(aclID) |
| if err != nil { |
| log.Printf("[DEBUG] Error getting ACL list by ID: %v", err) |
| return false, err |
| } |
| |
| log.Printf("[DEBUG] ACL list check result: count=%d", count) |
| return count > 0, nil |
| } |
| |
| // expandStringSet converts a *schema.Set to a []string |
| func expandStringSet(set *schema.Set) []string { |
| var out []string |
| for _, v := range set.List() { |
| out = append(out, v.(string)) |
| } |
| return out |
| } |
| |
| // normalizeField returns a comparable value for a field (handles nil, empty, etc) |
| func normalizeField(v interface{}) interface{} { |
| switch val := v.(type) { |
| case nil: |
| return "" |
| case *schema.Set: |
| list := val.List() |
| strs := make([]string, len(list)) |
| for i, s := range list { |
| strs[i] = fmt.Sprintf("%v", s) |
| } |
| return strings.Join(strs, ",") |
| default: |
| return val |
| } |
| } |