blob: 839f3e8ade32bac651f314031180c65fffeb7a80 [file] [log] [blame]
//
// 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 (
"context"
"fmt"
"log"
"sort"
"strconv"
"strings"
"sync"
"time"
"github.com/apache/cloudstack-go/v2/cloudstack"
"github.com/hashicorp/go-multierror"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry"
"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,
},
CustomizeDiff: func(ctx context.Context, diff *schema.ResourceDiff, meta interface{}) error {
// Force replacement for migration from deprecated 'ports' to 'port' field
if diff.HasChange("rule") {
oldRules, newRules := diff.GetChange("rule")
oldRulesList := oldRules.([]interface{})
newRulesList := newRules.([]interface{})
log.Printf("[DEBUG] CustomizeDiff: checking %d old rules -> %d new rules for migration", len(oldRulesList), len(newRulesList))
// Check if ANY old rule uses deprecated 'ports' field
hasDeprecatedPorts := false
for i, oldRule := range oldRulesList {
oldRuleMap := oldRule.(map[string]interface{})
protocol := oldRuleMap["protocol"].(string)
if protocol == "tcp" || protocol == "udp" {
if portsSet, hasPortsSet := oldRuleMap["ports"].(*schema.Set); hasPortsSet && portsSet.Len() > 0 {
log.Printf("[DEBUG] CustomizeDiff: OLD rule %d has deprecated ports field with %d ports: %v", i, portsSet.Len(), portsSet.List())
hasDeprecatedPorts = true
break
}
}
}
// Check if ANY new rule uses new 'port' field
hasNewPortFormat := false
for i, newRule := range newRulesList {
newRuleMap := newRule.(map[string]interface{})
protocol := newRuleMap["protocol"].(string)
if protocol == "tcp" || protocol == "udp" {
if portStr, hasPort := newRuleMap["port"].(string); hasPort && portStr != "" {
log.Printf("[DEBUG] CustomizeDiff: NEW rule %d has port field: %s", i, portStr)
hasNewPortFormat = true
break
}
}
}
// Force replacement if migrating from deprecated ports to new port format
if hasDeprecatedPorts && hasNewPortFormat {
log.Printf("[DEBUG] CustomizeDiff: MIGRATION DETECTED - old rules use deprecated 'ports', new rules use 'port' - FORCING REPLACEMENT")
diff.ForceNew("rule")
return nil
}
// Also force replacement if old rules have deprecated ports but new rules don't use ports at all
if hasDeprecatedPorts && !hasNewPortFormat {
log.Printf("[DEBUG] CustomizeDiff: POTENTIAL MIGRATION - old rules use deprecated 'ports' but new rules don't - FORCING REPLACEMENT")
diff.ForceNew("rule")
return nil
}
log.Printf("[DEBUG] CustomizeDiff: No migration detected - hasDeprecatedPorts=%t, hasNewPortFormat=%t", hasDeprecatedPorts, hasNewPortFormat)
}
return nil
},
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.TypeList,
Required: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
"protocol": {
Type: schema.TypeString,
Required: true,
},
"icmp_type": {
Type: schema.TypeInt,
Optional: true,
Computed: true,
},
"icmp_code": {
Type: schema.TypeInt,
Optional: true,
Computed: true,
},
"ports": {
Type: schema.TypeSet,
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
Set: schema.HashString,
Deprecated: "Use 'port' instead. The 'ports' field is deprecated and will be removed in a future version.",
},
"port": {
Type: schema.TypeString,
Optional: true,
},
"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 {
log.Printf("[DEBUG] Entering resourceCloudStackNetworkACLRuleCreate with acl_id=%s", d.Get("acl_id").(string))
// Make sure all required parameters are there
if err := verifyNetworkACLParams(d); err != nil {
log.Printf("[ERROR] Failed parameter verification: %v", err)
return err
}
// Create all rules that are configured
if nrs := d.Get("rule").([]interface{}); len(nrs) > 0 {
// Create an empty rule list to hold all newly created rules
rules := make([]interface{}, 0)
log.Printf("[DEBUG] Processing %d rules", len(nrs))
err := createNetworkACLRules(d, meta, &rules, nrs)
if err != nil {
log.Printf("[ERROR] Failed to create network ACL rules: %v", err)
return err
}
// Set the resource ID only after successful creation
log.Printf("[DEBUG] Setting resource ID to acl_id=%s", d.Get("acl_id").(string))
d.SetId(d.Get("acl_id").(string))
// Update state with created rules
if err := d.Set("rule", rules); err != nil {
log.Printf("[ERROR] Failed to set rule attribute: %v", err)
return err
}
} else {
log.Printf("[DEBUG] No rules provided, setting ID to acl_id=%s", d.Get("acl_id").(string))
d.SetId(d.Get("acl_id").(string))
}
log.Printf("[DEBUG] Calling resourceCloudStackNetworkACLRuleRead")
return resourceCloudStackNetworkACLRuleRead(d, meta)
}
func createNetworkACLRules(d *schema.ResourceData, meta interface{}, rules *[]interface{}, nrs []interface{}) error {
log.Printf("[DEBUG] Creating %d network ACL rules", len(nrs))
var errs *multierror.Error
results := make([]map[string]interface{}, len(nrs))
var mu sync.Mutex
var wg sync.WaitGroup
wg.Add(len(nrs))
sem := make(chan struct{}, d.Get("parallelism").(int))
for i, 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{}, index int) {
defer wg.Done()
sem <- struct{}{}
log.Printf("[DEBUG] Creating rule #%d: %+v", index+1, rule)
// Create a single rule
err := createNetworkACLRule(d, meta, rule)
if err != nil {
log.Printf("[ERROR] Failed to create rule #%d: %v", index+1, err)
mu.Lock()
errs = multierror.Append(errs, fmt.Errorf("rule #%d: %v", index+1, err))
mu.Unlock()
} else if len(rule["uuids"].(map[string]interface{})) > 0 {
log.Printf("[DEBUG] Successfully created rule #%d, storing at index %d", index+1, index)
results[index] = rule
} else {
log.Printf("[WARN] Rule #%d created but has no UUIDs", index+1)
}
<-sem
}(rule.(map[string]interface{}), i)
}
wg.Wait()
if err := errs.ErrorOrNil(); err != nil {
log.Printf("[ERROR] Errors occurred while creating rules: %v", err)
return err
}
for i, result := range results {
if result != nil {
*rules = append(*rules, result)
log.Printf("[DEBUG] Added rule #%d to final rules list", i+1)
}
}
log.Printf("[DEBUG] Successfully created all rules")
return nil
}
func createNetworkACLRule(d *schema.ResourceData, meta interface{}, rule map[string]interface{}) error {
cs := meta.(*cloudstack.CloudStackClient)
uuids := rule["uuids"].(map[string]interface{})
log.Printf("[DEBUG] Creating network ACL rule with protocol=%s", rule["protocol"].(string))
// Make sure all required parameters are there
if err := verifyNetworkACLRuleParams(d, rule); err != nil {
log.Printf("[ERROR] Failed to verify rule parameters: %v", err)
return err
}
// Create a new parameter struct
p := cs.NetworkACL.NewCreateNetworkACLParams(rule["protocol"].(string))
log.Printf("[DEBUG] Initialized CreateNetworkACLParams")
// If a rule ID is specified, set it
if ruleNum, ok := rule["rule_number"].(int); ok && ruleNum > 0 {
p.SetNumber(ruleNum)
log.Printf("[DEBUG] Set rule_number=%d", ruleNum)
}
// Set the acl ID from the configuration
aclID := d.Get("acl_id").(string)
p.SetAclid(aclID)
log.Printf("[DEBUG] Set aclid=%s", aclID)
// Set the action
p.SetAction(rule["action"].(string))
log.Printf("[DEBUG] Set action=%s", rule["action"].(string))
// Set the CIDR list
var cidrList []string
for _, cidr := range rule["cidr_list"].([]interface{}) {
cidrList = append(cidrList, cidr.(string))
}
p.SetCidrlist(cidrList)
log.Printf("[DEBUG] Set cidr_list=%v", cidrList)
// Set the traffic type
p.SetTraffictype(rule["traffic_type"].(string))
log.Printf("[DEBUG] Set traffic_type=%s", rule["traffic_type"].(string))
// Set the description
if desc, ok := rule["description"].(string); ok && desc != "" {
p.SetReason(desc)
log.Printf("[DEBUG] Set description=%s", 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))
log.Printf("[DEBUG] Set icmp_type=%d, icmp_code=%d", rule["icmp_type"].(int), rule["icmp_code"].(int))
r, err := Retry(4, retryableACLCreationFunc(cs, p))
if err != nil {
log.Printf("[ERROR] Failed to create ICMP rule: %v", err)
return err
}
uuids["icmp"] = r.(*cloudstack.CreateNetworkACLResponse).Id
rule["uuids"] = uuids
log.Printf("[DEBUG] Created ICMP rule with ID=%s", r.(*cloudstack.CreateNetworkACLResponse).Id)
}
// If the protocol is ALL set the needed parameters
if rule["protocol"].(string) == "all" {
r, err := Retry(4, retryableACLCreationFunc(cs, p))
if err != nil {
log.Printf("[ERROR] Failed to create ALL rule: %v", err)
return err
}
uuids["all"] = r.(*cloudstack.CreateNetworkACLResponse).Id
rule["uuids"] = uuids
log.Printf("[DEBUG] Created ALL rule with ID=%s", r.(*cloudstack.CreateNetworkACLResponse).Id)
}
// If protocol is TCP or UDP, create the rule (with or without port)
if rule["protocol"].(string) == "tcp" || rule["protocol"].(string) == "udp" {
// Check if deprecated ports field is used and reject it
if portsSet, hasPortsSet := rule["ports"].(*schema.Set); hasPortsSet && portsSet.Len() > 0 {
log.Printf("[ERROR] Attempt to create rule with deprecated ports field")
return fmt.Errorf("The 'ports' field is no longer supported for creating new rules. Please use the 'port' field with separate rules for each port/range.")
}
portStr, hasPort := rule["port"].(string)
if hasPort && portStr != "" {
// Handle single port
log.Printf("[DEBUG] Processing single port for TCP/UDP rule: %s", portStr)
if _, ok := uuids[portStr]; !ok {
m := splitPorts.FindStringSubmatch(portStr)
if m == nil {
log.Printf("[ERROR] Invalid port format: %s", portStr)
return fmt.Errorf("%q is not a valid port value. Valid options are '80' or '80-90'", portStr)
}
startPort, err := strconv.Atoi(m[1])
if err != nil {
log.Printf("[ERROR] Failed to parse start port %s: %v", m[1], err)
return err
}
endPort := startPort
if m[2] != "" {
endPort, err = strconv.Atoi(m[2])
if err != nil {
log.Printf("[ERROR] Failed to parse end port %s: %v", m[2], err)
return err
}
}
p.SetStartport(startPort)
p.SetEndport(endPort)
log.Printf("[DEBUG] Set port start=%d, end=%d", startPort, endPort)
r, err := Retry(4, retryableACLCreationFunc(cs, p))
if err != nil {
log.Printf("[ERROR] Failed to create TCP/UDP rule for port %s: %v", portStr, err)
return err
}
uuids[portStr] = r.(*cloudstack.CreateNetworkACLResponse).Id
rule["uuids"] = uuids
log.Printf("[DEBUG] Created TCP/UDP rule for port %s with ID=%s", portStr, r.(*cloudstack.CreateNetworkACLResponse).Id)
} else {
log.Printf("[DEBUG] Port %s already has UUID, skipping", portStr)
}
} else {
// No port specified - create rule for all ports
log.Printf("[DEBUG] No port specified for TCP/UDP rule, creating rule for all ports")
r, err := Retry(4, retryableACLCreationFunc(cs, p))
if err != nil {
log.Printf("[ERROR] Failed to create TCP/UDP rule for all ports: %v", err)
return err
}
uuids["all_ports"] = r.(*cloudstack.CreateNetworkACLResponse).Id
rule["uuids"] = uuids
log.Printf("[DEBUG] Created TCP/UDP rule for all ports with ID=%s", r.(*cloudstack.CreateNetworkACLResponse).Id)
}
}
log.Printf("[DEBUG] Successfully created rule with uuids=%+v", uuids)
return nil
}
func processTCPUDPRule(rule map[string]interface{}, ruleMap map[string]*cloudstack.NetworkACL, uuids map[string]interface{}, rules *[]interface{}) {
// Check for deprecated ports field first (for reading existing state during migration)
ps, hasPortsSet := rule["ports"].(*schema.Set)
portStr, hasPort := rule["port"].(string)
if hasPortsSet && ps.Len() > 0 {
log.Printf("[DEBUG] Processing deprecated ports field with %d ports during state read", ps.Len())
// Process each port in the deprecated ports set during state read
for _, port := range ps.List() {
portStr := port.(string)
if processPortForRule(portStr, rule, ruleMap, uuids) {
log.Printf("[DEBUG] Processed deprecated port %s during state read", portStr)
}
}
// Only add the rule once with all processed ports
if len(uuids) > 0 {
*rules = append(*rules, rule)
log.Printf("[DEBUG] Added TCP/UDP rule with deprecated ports to state during read: %+v", rule)
}
} else if hasPort && portStr != "" {
log.Printf("[DEBUG] Processing single port for TCP/UDP rule: %s", portStr)
if processPortForRule(portStr, rule, ruleMap, uuids) {
rule["port"] = portStr
*rules = append(*rules, rule)
log.Printf("[DEBUG] Added TCP/UDP rule with single port to state: %+v", rule)
}
} else {
log.Printf("[DEBUG] Processing TCP/UDP rule with no port specified")
id, ok := uuids["all_ports"]
if !ok {
log.Printf("[DEBUG] No UUID for all_ports, skipping rule")
return
}
r, ok := ruleMap[id.(string)]
if !ok {
log.Printf("[DEBUG] TCP/UDP rule for all_ports with ID %s not found, removing UUID", id.(string))
delete(uuids, "all_ports")
return
}
delete(ruleMap, id.(string))
var cidrs []interface{}
for _, cidr := range strings.Split(r.Cidrlist, ",") {
cidrs = append(cidrs, 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"] = r.Number
*rules = append(*rules, rule)
log.Printf("[DEBUG] Added TCP/UDP rule with no port to state: %+v", rule)
}
}
func processPortForRule(portStr string, rule map[string]interface{}, ruleMap map[string]*cloudstack.NetworkACL, uuids map[string]interface{}) bool {
id, ok := uuids[portStr]
if !ok {
log.Printf("[DEBUG] No UUID for port %s, skipping", portStr)
return false
}
r, ok := ruleMap[id.(string)]
if !ok {
log.Printf("[DEBUG] TCP/UDP rule for port %s with ID %s not found, removing UUID", portStr, id.(string))
delete(uuids, portStr)
return false
}
// Delete the known rule so only unknown rules remain in the ruleMap
delete(ruleMap, id.(string))
var cidrs []interface{}
for _, cidr := range strings.Split(r.Cidrlist, ",") {
cidrs = append(cidrs, 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"] = r.Number
return true
}
func resourceCloudStackNetworkACLRuleRead(d *schema.ResourceData, meta interface{}) error {
cs := meta.(*cloudstack.CloudStackClient)
log.Printf("[DEBUG] Entering resourceCloudStackNetworkACLRuleRead with acl_id=%s", d.Id())
// 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 {
log.Printf("[DEBUG] Network ACL list %s does not exist", d.Id())
d.SetId("")
return nil
}
log.Printf("[ERROR] Failed to get ACL list by ID: %v", err)
return err
}
// Get all the rules from the running environment with retries
p := cs.NetworkACL.NewListNetworkACLsParams()
p.SetAclid(d.Id())
p.SetListall(true)
var l *cloudstack.ListNetworkACLsResponse
retryErr := retry.RetryContext(context.Background(), 30*time.Second, func() *retry.RetryError {
var err error
l, err = cs.NetworkACL.ListNetworkACLs(p)
if err != nil {
log.Printf("[DEBUG] Failed to list network ACL rules, retrying: %v", err)
return retry.RetryableError(err)
}
if l.Count == 0 {
log.Printf("[DEBUG] No network ACL rules found for ACL %s, retrying", d.Id())
return retry.RetryableError(fmt.Errorf("no network ACL rules found for ACL %s", d.Id()))
}
log.Printf("[DEBUG] Found %d network ACL rules for ACL %s", l.Count, d.Id())
return nil
})
if retryErr != nil {
log.Printf("[WARN] Network ACL rules for %s not found after retries", d.Id())
d.SetId("")
return nil
}
// Make a map of all the rules so we can easily find a rule
ruleMap := make(map[string]*cloudstack.NetworkACL, l.Count)
for _, r := range l.NetworkACLs {
ruleMap[r.Id] = r
}
log.Printf("[DEBUG] Loaded %d rules into ruleMap", len(ruleMap))
// Create an empty rule list to hold all rules
var rules []interface{}
// Read all rules that are configured
if rs := d.Get("rule").([]interface{}); len(rs) > 0 {
for _, rule := range rs {
rule := rule.(map[string]interface{})
uuids := rule["uuids"].(map[string]interface{})
log.Printf("[DEBUG] Processing rule with protocol=%s, uuids=%+v", rule["protocol"].(string), uuids)
if rule["protocol"].(string) == "icmp" {
id, ok := uuids["icmp"]
if !ok {
log.Printf("[DEBUG] No ICMP UUID found, skipping rule")
continue
}
// Get the rule
r, ok := ruleMap[id.(string)]
if !ok {
log.Printf("[DEBUG] ICMP rule with ID %s not found, removing UUID", id.(string))
delete(uuids, "icmp")
continue
}
// Delete the known rule so only unknown rules remain in the ruleMap
delete(ruleMap, id.(string))
// Create a list with all CIDR's
var cidrs []interface{}
for _, cidr := range strings.Split(r.Cidrlist, ",") {
cidrs = append(cidrs, cidr)
}
// Update the values
rule["action"] = strings.ToLower(r.Action)
rule["protocol"] = r.Protocol
rule["icmp_type"] = r.Icmptype
rule["icmp_code"] = r.Icmpcode
rule["traffic_type"] = strings.ToLower(r.Traffictype)
rule["cidr_list"] = cidrs
rule["rule_number"] = r.Number
rules = append(rules, rule)
log.Printf("[DEBUG] Added ICMP rule to state: %+v", rule)
}
if rule["protocol"].(string) == "all" {
id, ok := uuids["all"]
if !ok {
log.Printf("[DEBUG] No ALL UUID found, skipping rule")
continue
}
// Get the rule
r, ok := ruleMap[id.(string)]
if !ok {
log.Printf("[DEBUG] ALL rule with ID %s not found, removing UUID", id.(string))
delete(uuids, "all")
continue
}
// Delete the known rule so only unknown rules remain in the ruleMap
delete(ruleMap, id.(string))
// Create a list with all CIDR's
var cidrs []interface{}
for _, cidr := range strings.Split(r.Cidrlist, ",") {
cidrs = append(cidrs, cidr)
}
// Update the values
rule["action"] = strings.ToLower(r.Action)
rule["protocol"] = r.Protocol
rule["traffic_type"] = strings.ToLower(r.Traffictype)
rule["cidr_list"] = cidrs
rule["rule_number"] = r.Number
rules = append(rules, rule)
log.Printf("[DEBUG] Added ALL rule to state: %+v", rule)
}
if rule["protocol"].(string) == "tcp" || rule["protocol"].(string) == "udp" {
uuids := rule["uuids"].(map[string]interface{})
processTCPUDPRule(rule, ruleMap, uuids, &rules)
}
}
}
// If this is a managed firewall, add all unknown rules into dummy rules
managed := d.Get("managed").(bool)
if managed && len(ruleMap) > 0 {
for uuid := range ruleMap {
// We need to create and add a dummy value to a list as the
// cidr_list is a required field and thus needs a value
cidrs := []interface{}{uuid}
// Make a dummy rule to hold the unknown UUID
rule := map[string]interface{}{
"cidr_list": cidrs,
"protocol": uuid,
"uuids": map[string]interface{}{uuid: uuid},
}
// Add the dummy rule to the rules list
rules = append(rules, rule)
log.Printf("[DEBUG] Added managed dummy rule for UUID %s", uuid)
}
}
if len(rules) > 0 {
log.Printf("[DEBUG] Setting %d rules in state", len(rules))
if err := d.Set("rule", rules); err != nil {
log.Printf("[ERROR] Failed to set rule attribute: %v", err)
return err
}
} else if !managed {
log.Printf("[DEBUG] No rules found and not managed, clearing ID")
d.SetId("")
}
log.Printf("[DEBUG] Completed resourceCloudStackNetworkACLRuleRead")
return nil
}
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 list has changed
if d.HasChange("rule") {
o, n := d.GetChange("rule")
oldRules := o.([]interface{})
newRules := n.([]interface{})
log.Printf("[DEBUG] Rule list changed: %d old rules -> %d new rules", len(oldRules), len(newRules))
// Check for migration from deprecated 'ports' to 'port' field
migrationDetected := isPortsMigration(oldRules, newRules)
if migrationDetected {
log.Printf("[DEBUG] Migration detected - performing complete rule replacement")
return performPortsMigration(d, meta, oldRules, newRules)
}
log.Printf("[DEBUG] Rule list changed, performing efficient updates")
err := updateNetworkACLRules(d, meta, oldRules, newRules)
if err != nil {
return err
}
}
return resourceCloudStackNetworkACLRuleRead(d, meta)
}
func resourceCloudStackNetworkACLRuleDelete(d *schema.ResourceData, meta interface{}) error {
// Delete all rules
if ors := d.Get("rule").([]interface{}); len(ors) > 0 {
for _, rule := range ors {
ruleMap := rule.(map[string]interface{})
err := deleteNetworkACLRule(d, meta, ruleMap)
if err != nil {
log.Printf("[ERROR] Failed to delete rule: %v", err)
return err
}
}
}
return nil
}
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 {
log.Printf("[DEBUG] Verifying parameters for rule: %+v", rule)
if ruleNum, ok := rule["rule_number"]; ok && ruleNum != nil {
if number, ok := ruleNum.(int); ok && number != 0 {
// Validate only if rule_number is explicitly set (non-zero)
if number < 1 || number > 65535 {
log.Printf("[ERROR] Invalid rule_number: %d", number)
return fmt.Errorf(
"%q must be between %d and %d inclusive, got: %d", "rule_number", 1, 65535, number)
}
}
}
action := rule["action"].(string)
if action != "allow" && action != "deny" {
log.Printf("[ERROR] Invalid action: %s", action)
return fmt.Errorf("Parameter action only accepts 'allow' or 'deny' as values")
}
protocol := rule["protocol"].(string)
log.Printf("[DEBUG] Validating protocol: %s", protocol)
switch protocol {
case "icmp":
if _, ok := rule["icmp_type"]; !ok {
log.Printf("[ERROR] Missing icmp_type for ICMP protocol")
return fmt.Errorf(
"Parameter icmp_type is a required parameter when using protocol 'icmp'")
}
if _, ok := rule["icmp_code"]; !ok {
log.Printf("[ERROR] Missing icmp_code for ICMP protocol")
return fmt.Errorf(
"Parameter icmp_code is a required parameter when using protocol 'icmp'")
}
case "all":
// No additional test are needed
log.Printf("[DEBUG] Protocol 'all' validated")
case "tcp", "udp":
// The deprecated 'ports' field is no longer supported in any scenario
portsSet, hasPortsSet := rule["ports"].(*schema.Set)
portStr, hasPort := rule["port"].(string)
// Block deprecated ports field completely
if hasPortsSet && portsSet.Len() > 0 {
log.Printf("[ERROR] Attempt to use deprecated ports field")
return fmt.Errorf("The 'ports' field is no longer supported. Please use the 'port' field instead.")
}
// Validate the new port field if used
if hasPort && portStr != "" {
log.Printf("[DEBUG] Found port for TCP/UDP: %s", portStr)
m := splitPorts.FindStringSubmatch(portStr)
if m == nil {
log.Printf("[ERROR] Invalid port format: %s", portStr)
return fmt.Errorf(
"%q is not a valid port value. Valid options are '80' or '80-90'", portStr)
}
} else {
log.Printf("[DEBUG] No port specified for TCP/UDP, allowing empty port")
}
default:
_, err := strconv.ParseInt(protocol, 0, 0)
if err != nil {
log.Printf("[ERROR] Invalid protocol: %s", protocol)
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" {
log.Printf("[ERROR] Invalid traffic_type: %s", traffic)
return fmt.Errorf(
"Parameter traffic_type only accepts 'ingress' or 'egress' as values")
}
log.Printf("[DEBUG] Rule parameters verified successfully")
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
}
func updateNetworkACLRules(d *schema.ResourceData, meta interface{}, oldRules, newRules []interface{}) error {
cs := meta.(*cloudstack.CloudStackClient)
log.Printf("[DEBUG] Updating ACL rules: %d old rules, %d new rules", len(oldRules), len(newRules))
log.Printf("[DEBUG] Performing normal rule updates")
return performNormalRuleUpdates(d, meta, cs, oldRules, newRules)
}
func performNormalRuleUpdates(d *schema.ResourceData, meta interface{}, cs *cloudstack.CloudStackClient, oldRules, newRules []interface{}) error {
rulesToUpdate := make(map[string]map[string]interface{}) // UUID -> new rule mapping
rulesToDelete := make([]map[string]interface{}, 0)
rulesToCreate := make([]map[string]interface{}, 0)
// Track which new rules match existing old rules
usedNewRules := make(map[int]bool)
// For each old rule, try to find a matching new rule
for _, oldRule := range oldRules {
oldRuleMap := oldRule.(map[string]interface{})
foundMatch := false
for newIdx, newRule := range newRules {
if usedNewRules[newIdx] {
continue
}
newRuleMap := newRule.(map[string]interface{})
log.Printf("[DEBUG] Comparing old rule %+v with new rule %+v", oldRuleMap, newRuleMap)
if rulesMatch(oldRuleMap, newRuleMap) {
log.Printf("[DEBUG] Found matching new rule for old rule")
if oldUUIDs, ok := oldRuleMap["uuids"].(map[string]interface{}); ok {
newRuleMap["uuids"] = oldUUIDs
}
if ruleNeedsUpdate(oldRuleMap, newRuleMap) {
log.Printf("[DEBUG] Rule needs updating")
if uuids, ok := oldRuleMap["uuids"].(map[string]interface{}); ok {
for _, uuid := range uuids {
if uuid != nil {
rulesToUpdate[uuid.(string)] = newRuleMap
break
}
}
}
}
usedNewRules[newIdx] = true
foundMatch = true
break
}
}
if !foundMatch {
log.Printf("[DEBUG] Old rule has no match, will be deleted")
rulesToDelete = append(rulesToDelete, oldRuleMap)
}
}
for newIdx, newRule := range newRules {
if !usedNewRules[newIdx] {
newRuleMap := newRule.(map[string]interface{})
log.Printf("[DEBUG] New rule has no match, will be created")
rulesToCreate = append(rulesToCreate, newRuleMap)
}
}
for _, ruleToDelete := range rulesToDelete {
log.Printf("[DEBUG] Deleting unmatched old rule")
err := deleteNetworkACLRule(d, meta, ruleToDelete)
if err != nil {
return fmt.Errorf("failed to delete old rule: %v", err)
}
}
for uuid, newRule := range rulesToUpdate {
log.Printf("[DEBUG] Updating rule with UUID %s", uuid)
tempOldRule := make(map[string]interface{})
tempOldRule["uuids"] = map[string]interface{}{"update": uuid}
err := updateNetworkACLRule(cs, tempOldRule, newRule)
if err != nil {
return fmt.Errorf("failed to update rule UUID %s: %v", uuid, err)
}
}
if len(rulesToCreate) > 0 {
log.Printf("[DEBUG] Creating %d new rules", len(rulesToCreate))
var createdRules []interface{}
var rulesToCreateInterface []interface{}
for _, rule := range rulesToCreate {
rulesToCreateInterface = append(rulesToCreateInterface, rule)
}
err := createNetworkACLRules(d, meta, &createdRules, rulesToCreateInterface)
if err != nil {
return fmt.Errorf("failed to create new rules: %v", err)
}
}
return nil
}
func rulesMatch(oldRule, newRule map[string]interface{}) bool {
if oldRule["protocol"].(string) != newRule["protocol"].(string) ||
oldRule["traffic_type"].(string) != newRule["traffic_type"].(string) ||
oldRule["action"].(string) != newRule["action"].(string) {
return false
}
protocol := newRule["protocol"].(string)
if protocol == "tcp" || protocol == "udp" {
oldPort, oldHasPort := oldRule["port"].(string)
newPort, newHasPort := newRule["port"].(string)
if oldHasPort && newHasPort {
return oldPort == newPort
}
if oldHasPort != newHasPort {
return false
}
return true
}
switch protocol {
case "icmp":
return oldRule["icmp_type"].(int) == newRule["icmp_type"].(int) &&
oldRule["icmp_code"].(int) == newRule["icmp_code"].(int)
case "all":
return true
default:
return true
}
}
func ruleNeedsUpdate(oldRule, newRule map[string]interface{}) bool {
if oldRule["action"].(string) != newRule["action"].(string) {
log.Printf("[DEBUG] Action changed: %s -> %s", oldRule["action"].(string), newRule["action"].(string))
return true
}
if oldRule["protocol"].(string) != newRule["protocol"].(string) {
log.Printf("[DEBUG] Protocol changed: %s -> %s", oldRule["protocol"].(string), newRule["protocol"].(string))
return true
}
if oldRule["traffic_type"].(string) != newRule["traffic_type"].(string) {
log.Printf("[DEBUG] Traffic type changed: %s -> %s", oldRule["traffic_type"].(string), newRule["traffic_type"].(string))
return true
}
// Check rule_number
oldRuleNum, oldHasRuleNum := oldRule["rule_number"].(int)
newRuleNum, newHasRuleNum := newRule["rule_number"].(int)
if oldHasRuleNum != newHasRuleNum || (oldHasRuleNum && newHasRuleNum && oldRuleNum != newRuleNum) {
log.Printf("[DEBUG] Rule number changed: %d -> %d", oldRuleNum, newRuleNum)
return true
}
oldDesc, oldHasDesc := oldRule["description"].(string)
newDesc, newHasDesc := newRule["description"].(string)
if oldHasDesc != newHasDesc || (oldHasDesc && newHasDesc && oldDesc != newDesc) {
log.Printf("[DEBUG] Description changed: %s -> %s", oldDesc, newDesc)
return true
}
protocol := newRule["protocol"].(string)
switch protocol {
case "icmp":
if oldRule["icmp_type"].(int) != newRule["icmp_type"].(int) {
log.Printf("[DEBUG] ICMP type changed: %d -> %d", oldRule["icmp_type"].(int), newRule["icmp_type"].(int))
return true
}
if oldRule["icmp_code"].(int) != newRule["icmp_code"].(int) {
log.Printf("[DEBUG] ICMP code changed: %d -> %d", oldRule["icmp_code"].(int), newRule["icmp_code"].(int))
return true
}
case "tcp", "udp":
oldPort, oldHasPort := oldRule["port"].(string)
newPort, newHasPort := newRule["port"].(string)
if oldHasPort != newHasPort || (oldHasPort && newHasPort && oldPort != newPort) {
log.Printf("[DEBUG] Port changed: %s -> %s", oldPort, newPort)
return true
}
}
oldCidrs := oldRule["cidr_list"].([]interface{})
newCidrs := newRule["cidr_list"].([]interface{})
if len(oldCidrs) != len(newCidrs) {
log.Printf("[DEBUG] CIDR list length changed: %d -> %d", len(oldCidrs), len(newCidrs))
return true
}
oldCidrStrs := make([]string, len(oldCidrs))
newCidrStrs := make([]string, len(newCidrs))
for i, cidr := range oldCidrs {
oldCidrStrs[i] = cidr.(string)
}
for i, cidr := range newCidrs {
newCidrStrs[i] = cidr.(string)
}
sort.Strings(oldCidrStrs)
sort.Strings(newCidrStrs)
for i, oldCidr := range oldCidrStrs {
if oldCidr != newCidrStrs[i] {
log.Printf("[DEBUG] CIDR changed at index %d: %s -> %s", i, oldCidr, newCidrStrs[i])
return true
}
}
return false
}
func updateNetworkACLRule(cs *cloudstack.CloudStackClient, oldRule, newRule map[string]interface{}) error {
uuids := oldRule["uuids"].(map[string]interface{})
for key, uuid := range uuids {
if key == "%" {
continue
}
log.Printf("[DEBUG] Updating ACL rule with UUID: %s", uuid.(string))
p := cs.NetworkACL.NewUpdateNetworkACLItemParams(uuid.(string))
p.SetAction(newRule["action"].(string))
var cidrList []string
for _, cidr := range newRule["cidr_list"].([]interface{}) {
cidrList = append(cidrList, cidr.(string))
}
p.SetCidrlist(cidrList)
if desc, ok := newRule["description"].(string); ok && desc != "" {
p.SetReason(desc)
}
p.SetProtocol(newRule["protocol"].(string))
p.SetTraffictype(newRule["traffic_type"].(string))
// Set rule number if provided and non-zero
if ruleNum, ok := newRule["rule_number"].(int); ok && ruleNum > 0 {
p.SetNumber(ruleNum)
log.Printf("[DEBUG] Set rule_number=%d", ruleNum)
}
protocol := newRule["protocol"].(string)
switch protocol {
case "icmp":
if icmpType, ok := newRule["icmp_type"].(int); ok {
p.SetIcmptype(icmpType)
log.Printf("[DEBUG] Set icmp_type=%d", icmpType)
}
if icmpCode, ok := newRule["icmp_code"].(int); ok {
p.SetIcmpcode(icmpCode)
log.Printf("[DEBUG] Set icmp_code=%d", icmpCode)
}
case "tcp", "udp":
if portStr, hasPort := newRule["port"].(string); hasPort && portStr != "" {
m := splitPorts.FindStringSubmatch(portStr)
if m != nil {
startPort, err := strconv.Atoi(m[1])
if err == nil {
endPort := startPort
if m[2] != "" {
if ep, err := strconv.Atoi(m[2]); err == nil {
endPort = ep
}
}
p.SetStartport(startPort)
p.SetEndport(endPort)
log.Printf("[DEBUG] Set port start=%d, end=%d", startPort, endPort)
}
}
}
}
_, err := cs.NetworkACL.UpdateNetworkACLItem(p)
if err != nil {
log.Printf("[ERROR] Failed to update ACL rule %s: %v", uuid.(string), err)
return err
}
log.Printf("[DEBUG] Successfully updated ACL rule %s", uuid.(string))
}
return nil
}
func hasDeprecatedPortsInOldRules(oldRules []interface{}) bool {
for _, oldRule := range oldRules {
oldRuleMap := oldRule.(map[string]interface{})
protocol := oldRuleMap["protocol"].(string)
if protocol == "tcp" || protocol == "udp" {
if portsSet, hasPortsSet := oldRuleMap["ports"].(*schema.Set); hasPortsSet && portsSet.Len() > 0 {
return true
}
}
}
return false
}
func containsMixedPortFields(oldRules, newRules []interface{}) bool {
hasDeprecatedInOld := hasDeprecatedPortsInOldRules(oldRules)
hasNewInNew := hasPortFieldInNewRules(newRules)
hasDeprecatedInNew := hasDeprecatedPortsInOldRules(newRules)
// Migration detected if:
// 1. Old rules have deprecated ports OR
// 2. We have a mix of deprecated and new port fields anywhere
return hasDeprecatedInOld || (hasDeprecatedInNew && hasNewInNew)
}
// Checks if any new rule uses the new 'port' field
func hasPortFieldInNewRules(newRules []interface{}) bool {
for _, newRule := range newRules {
newRuleMap := newRule.(map[string]interface{})
protocol := newRuleMap["protocol"].(string)
if protocol == "tcp" || protocol == "udp" {
if portStr, hasPort := newRuleMap["port"].(string); hasPort && portStr != "" {
return true
}
}
}
return false
}
// Detects if we're migrating from deprecated 'ports' to 'port' field
func isPortsMigration(oldRules, newRules []interface{}) bool {
log.Printf("[DEBUG] Migration detection: checking %d old rules and %d new rules", len(oldRules), len(newRules))
hasDeprecatedPorts := false
hasNewPortFormat := false
for i, oldRule := range oldRules {
oldRuleMap := oldRule.(map[string]interface{})
protocol := oldRuleMap["protocol"].(string)
log.Printf("[DEBUG] Migration detection: old rule %d has protocol %s", i, protocol)
if protocol == "tcp" || protocol == "udp" {
if portsSet, hasPortsSet := oldRuleMap["ports"].(*schema.Set); hasPortsSet && portsSet.Len() > 0 {
log.Printf("[DEBUG] Migration detection: old rule %d has deprecated ports field with %d ports", i, portsSet.Len())
hasDeprecatedPorts = true
}
oldPort, oldHasPort := oldRuleMap["port"].(string)
if !oldHasPort || oldPort == "" {
log.Printf("[DEBUG] Migration detection: old rule %d has no port field, checking if new rules use port field", i)
for j, newRule := range newRules {
newRuleMap := newRule.(map[string]interface{})
newProtocol := newRuleMap["protocol"].(string)
if newProtocol == protocol {
if newPortStr, newHasPort := newRuleMap["port"].(string); newHasPort && newPortStr != "" {
log.Printf("[DEBUG] Migration detection: new rule %d has port field '%s' while old rule had none - potential migration", j, newPortStr)
hasDeprecatedPorts = true
break
}
}
}
}
}
}
for i, newRule := range newRules {
newRuleMap := newRule.(map[string]interface{})
protocol := newRuleMap["protocol"].(string)
log.Printf("[DEBUG] Migration detection: new rule %d has protocol %s", i, protocol)
if protocol == "tcp" || protocol == "udp" {
if portStr, hasPort := newRuleMap["port"].(string); hasPort && portStr != "" {
log.Printf("[DEBUG] Migration detection: new rule %d has port field with value: %s", i, portStr)
hasNewPortFormat = true
}
if portsSet, hasPortsSet := newRuleMap["ports"].(*schema.Set); hasPortsSet && portsSet.Len() > 0 {
log.Printf("[DEBUG] Migration detection: new rule %d still has deprecated ports, not a migration", i)
return false
}
}
}
migrationDetected := hasDeprecatedPorts && hasNewPortFormat
log.Printf("[DEBUG] Migration detection result: hasDeprecatedPorts=%t, hasNewPortFormat=%t, migrationDetected=%t", hasDeprecatedPorts, hasNewPortFormat, migrationDetected)
// Migration is detected if:
// 1. We have old rules with deprecated ports OR no port field AND
// 2. We have new rules with port format (no deprecated ports)
return migrationDetected
}
func performPortsMigration(d *schema.ResourceData, meta interface{}, oldRules, newRules []interface{}) error {
log.Printf("[DEBUG] Starting ports->port migration")
cs := meta.(*cloudstack.CloudStackClient)
// Build a map of all UUIDs that need to be deleted
uuidsToDelete := make([]string, 0)
for _, oldRule := range oldRules {
oldRuleMap := oldRule.(map[string]interface{})
uuids, ok := oldRuleMap["uuids"].(map[string]interface{})
if !ok {
continue
}
for key, uuid := range uuids {
if key != "%" && uuid != nil {
uuidStr := uuid.(string)
if uuidStr != "" {
uuidsToDelete = append(uuidsToDelete, uuidStr)
}
}
}
}
log.Printf("[DEBUG] Total UUIDs to delete: %d", len(uuidsToDelete))
// Delete all old rules by UUID and wait for completion
for _, uuidToDelete := range uuidsToDelete {
p := cs.NetworkACL.NewDeleteNetworkACLParams(uuidToDelete)
_, err := cs.NetworkACL.DeleteNetworkACL(p)
if err != nil {
if strings.Contains(err.Error(), fmt.Sprintf(
"Invalid parameter id value=%s due to incorrect long value format, "+
"or entity does not exist", uuidToDelete)) {
continue
}
return fmt.Errorf("failed to delete old rule UUID %s during migration: %v", uuidToDelete, err)
}
}
// Wait a moment for CloudStack to process the deletions
if len(uuidsToDelete) > 0 {
log.Printf("[DEBUG] Waiting for CloudStack to process %d rule deletions", len(uuidsToDelete))
time.Sleep(3 * time.Second)
for _, uuidToCheck := range uuidsToDelete {
listParams := cs.NetworkACL.NewListNetworkACLsParams()
listParams.SetId(uuidToCheck)
listResp, err := cs.NetworkACL.ListNetworkACLs(listParams)
if err == nil && listResp.Count > 0 {
time.Sleep(2 * time.Second)
break
}
}
}
// Create all new rules with fresh UUIDs
if len(newRules) > 0 {
log.Printf("[DEBUG] Creating %d new rules with port field", len(newRules))
var rulesToCreate []interface{}
for _, newRule := range newRules {
newRuleMap := newRule.(map[string]interface{})
cleanRule := make(map[string]interface{})
for k, v := range newRuleMap {
cleanRule[k] = v
}
cleanRule["uuids"] = make(map[string]interface{})
rulesToCreate = append(rulesToCreate, cleanRule)
}
var createdRules []interface{}
err := createNetworkACLRules(d, meta, &createdRules, rulesToCreate)
if err != nil {
return fmt.Errorf("failed to create new rules during migration: %v", err)
}
log.Printf("[DEBUG] Successfully created %d new rules during migration", len(createdRules))
if err := d.Set("rule", createdRules); err != nil {
return fmt.Errorf("failed to update state with migrated rules: %v", err)
}
log.Printf("[DEBUG] Updated Terraform state with %d migrated rules", len(createdRules))
}
log.Printf("[DEBUG] Ports->port migration completed successfully")
return nil
}