blob: 1aebe76ecc328200e77f005dbff278223905980c [file]
//
// 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"
"net"
"strconv"
"strings"
"github.com/apache/cloudstack-go/v2/cloudstack"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)
const none = "none"
func resourceCloudStackNetwork() *schema.Resource {
aclidSchema := &schema.Schema{
Type: schema.TypeString,
Optional: true,
Default: none,
}
aclidSchema.StateFunc = func(v interface{}) string {
value := v.(string)
if value == none {
aclidSchema.ForceNew = true
} else {
aclidSchema.ForceNew = false
}
return value
}
return &schema.Resource{
Create: resourceCloudStackNetworkCreate,
Read: resourceCloudStackNetworkRead,
Update: resourceCloudStackNetworkUpdate,
Delete: resourceCloudStackNetworkDelete,
Importer: &schema.ResourceImporter{
State: importStatePassthrough,
},
Schema: map[string]*schema.Schema{
"name": {
Type: schema.TypeString,
Required: true,
},
"display_text": {
Type: schema.TypeString,
Optional: true,
Computed: true,
},
"cidr": {
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
},
"ip6cidr": {
Type: schema.TypeString,
Optional: true,
ForceNew: true,
StateFunc: func(v interface{}) string {
s, ok := v.(string)
if !ok {
return ""
}
// Leave empty value unchanged.
if s == "" {
return s
}
// Parse and canonicalize the IPv6 CIDR. If parsing fails,
// return the original string so invalid input is not altered.
_, ipnet, err := net.ParseCIDR(s)
if err != nil {
return s
}
return ipnet.String()
},
},
"gateway": {
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
RequiredWith: []string{"cidr"},
},
"ip6gateway": {
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
},
"startip": {
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
RequiredWith: []string{"cidr"},
},
"endip": {
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
RequiredWith: []string{"cidr"},
},
"startipv6": {
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
},
"endipv6": {
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
},
"network_domain": {
Type: schema.TypeString,
Optional: true,
Computed: true,
},
"network_offering": {
Type: schema.TypeString,
Required: true,
},
"vlan": {
Type: schema.TypeInt,
Optional: true,
ForceNew: true,
},
"vpc_id": {
Type: schema.TypeString,
Optional: true,
ForceNew: true,
},
"acl_id": aclidSchema,
"project": {
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
},
"source_nat_ip": {
Type: schema.TypeBool,
Optional: true,
ForceNew: true,
},
"source_nat_ip_address": {
Type: schema.TypeString,
Computed: true,
},
"source_nat_ip_id": {
Type: schema.TypeString,
Computed: true,
},
"zone": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"bypass_vlan_overlap_check": {
Type: schema.TypeBool,
Optional: true,
Default: false,
},
"tags": tagsSchema(),
},
}
}
func resourceCloudStackNetworkCreate(d *schema.ResourceData, meta interface{}) error {
cs := meta.(*cloudstack.CloudStackClient)
name := d.Get("name").(string)
// Retrieve the network_offering ID
networkofferingid, e := retrieveID(cs, "network_offering", d.Get("network_offering").(string))
if e != nil {
return e.Error()
}
// Retrieve the zone ID
zoneid, e := retrieveID(cs, "zone", d.Get("zone").(string))
if e != nil {
return e.Error()
}
// Create a new parameter struct
p := cs.Network.NewCreateNetworkParams(name, networkofferingid, zoneid)
if displaytext, ok := d.GetOk("display_text"); ok {
p.SetDisplaytext(displaytext.(string))
} else {
p.SetDisplaytext(name)
}
// Get the network offering to check if it supports specifying IP ranges
no, _, err := cs.NetworkOffering.GetNetworkOfferingByID(networkofferingid)
if err != nil {
return err
}
if _, ok := d.GetOk("cidr"); ok {
m, err := parseCIDR(d, no.Specifyipranges)
if err != nil {
return err
}
// Set the needed IP config
p.SetGateway(m["gateway"])
p.SetNetmask(m["netmask"])
// Only set the start IP if we have one
if startip, ok := m["startip"]; ok {
p.SetStartip(startip)
}
// Only set the end IP if we have one
if endip, ok := m["endip"]; ok {
p.SetEndip(endip)
}
}
// IPv6 support
if ip6cidr, ok := d.GetOk("ip6cidr"); ok {
m6, err := parseCIDRv6(d, no.Specifyipranges)
if err != nil {
return err
}
p.SetIp6cidr(ip6cidr.(string))
// Only set the start IPv6 if we have one
if startipv6, ok := m6["startipv6"]; ok {
p.SetStartipv6(startipv6)
}
// Only set the ipv6 gateway if we have one
if ip6gateway, ok := m6["ip6gateway"]; ok {
p.SetIp6gateway(ip6gateway)
}
// Only set the end IPv6 if we have one
if endipv6, ok := m6["endipv6"]; ok {
p.SetEndipv6(endipv6)
}
}
// Set the network domain if we have one
if networkDomain, ok := d.GetOk("network_domain"); ok {
p.SetNetworkdomain(networkDomain.(string))
}
if vlan, ok := d.GetOk("vlan"); ok {
p.SetVlan(strconv.Itoa(vlan.(int)))
}
// Bypass VLAN overlap check if necessary
if bypassVlanOverlapCheck, ok := d.GetOk("bypass_vlan_overlap_check"); ok {
p.SetBypassvlanoverlapcheck(bypassVlanOverlapCheck.(bool))
}
// Check is this network needs to be created in a VPC
if vpcid, ok := d.GetOk("vpc_id"); ok {
// Set the vpc id
p.SetVpcid(vpcid.(string))
// Since we're in a VPC, check if we want to associate an ACL list
if aclid, ok := d.GetOk("acl_id"); ok && aclid.(string) != none {
// Set the acl ID
p.SetAclid(aclid.(string))
}
// If no project is explicitly set, try to inherit it from the VPC
if _, ok := d.GetOk("project"); !ok {
// Get the VPC to retrieve its project
// Use listall to search across all projects
vpcParams := cs.VPC.NewListVPCsParams()
vpcParams.SetId(vpcid.(string))
vpcParams.SetListall(true)
vpcList, err := cs.VPC.ListVPCs(vpcParams)
if err == nil && vpcList.Count > 0 && vpcList.VPCs[0].Projectid != "" {
log.Printf("[DEBUG] Inheriting project %s from VPC %s", vpcList.VPCs[0].Projectid, vpcid.(string))
p.SetProjectid(vpcList.VPCs[0].Projectid)
}
}
}
// If there is a project supplied, we retrieve and set the project id
// This will override the inherited project from VPC if explicitly set
if err := setProjectid(p, cs, d); err != nil {
return err
}
// Create the new network
r, err := cs.Network.CreateNetwork(p)
if err != nil {
return fmt.Errorf("Error creating network %s: %s", name, err)
}
d.SetId(r.Id)
// Set tags if necessary
if err = setTags(cs, d, "network"); err != nil {
return fmt.Errorf("Error setting tags: %v", err)
}
if d.Get("source_nat_ip").(bool) {
// Create a new parameter struct
p := cs.Address.NewAssociateIpAddressParams()
// Set required options
p.SetNetworkid(r.Id)
p.SetZoneid(zoneid)
if vpcid, ok := d.GetOk("vpc_id"); ok {
// Set the vpcid
p.SetVpcid(vpcid.(string))
}
// If there is a project supplied, we retrieve and set the project id
if err := setProjectid(p, cs, d); err != nil {
return err
}
// Associate a new IP address
ip, err := cs.Address.AssociateIpAddress(p)
if err != nil {
return fmt.Errorf("Error associating a new IP address: %s", err)
}
d.Set("source_nat_ip_address", ip.Ipaddress)
d.Set("source_nat_ip_id", ip.Id)
// Set the additional partial
}
return resourceCloudStackNetworkRead(d, meta)
}
func resourceCloudStackNetworkRead(d *schema.ResourceData, meta interface{}) error {
cs := meta.(*cloudstack.CloudStackClient)
// Get the network details
// First try with the project from state (if any)
project := d.Get("project").(string)
n, count, err := cs.Network.GetNetworkByID(
d.Id(),
cloudstack.WithProject(project),
)
// If not found and no explicit project was set, try with projectid=-1
// This handles the case where the project was inherited from VPC
if count == 0 && project == "" {
n, count, err = cs.Network.GetNetworkByID(
d.Id(),
cloudstack.WithProject("-1"),
)
}
if err != nil {
if count == 0 {
log.Printf(
"[DEBUG] Network %s does no longer exist", d.Get("name").(string))
d.SetId("")
return nil
}
return err
}
d.Set("name", n.Name)
d.Set("display_text", n.Displaytext)
d.Set("cidr", n.Cidr)
d.Set("gateway", n.Gateway)
d.Set("network_domain", n.Networkdomain)
d.Set("vpc_id", n.Vpcid)
// Always set IPv6 fields to detect drift when IPv6 is removed server-side
d.Set("ip6cidr", n.Ip6cidr)
d.Set("ip6gateway", n.Ip6gateway)
// Note: CloudStack API may not return startipv6 and endipv6 fields
// These are typically only set during network creation
if n.Aclid == "" {
n.Aclid = none
}
d.Set("acl_id", n.Aclid)
tags := make(map[string]interface{})
for _, tag := range n.Tags {
tags[tag.Key] = tag.Value
}
d.Set("tags", tags)
setValueOrID(d, "network_offering", n.Networkofferingname, n.Networkofferingid)
setValueOrID(d, "project", n.Project, n.Projectid)
setValueOrID(d, "zone", n.Zonename, n.Zoneid)
if d.Get("source_nat_ip").(bool) {
ip, count, err := cs.Address.GetPublicIpAddressByID(
d.Get("source_nat_ip_id").(string),
cloudstack.WithProject(d.Get("project").(string)),
)
if err != nil {
if count == 0 {
log.Printf(
"[DEBUG] Source NAT IP with ID %s is no longer associated", d.Id())
d.Set("source_nat_ip", false)
d.Set("source_nat_ip_id", "")
return nil
}
return err
}
if n.Id != ip.Associatednetworkid {
d.Set("source_nat_ip", false)
d.Set("source_nat_ip_id", "")
}
}
return nil
}
func resourceCloudStackNetworkUpdate(d *schema.ResourceData, meta interface{}) error {
cs := meta.(*cloudstack.CloudStackClient)
name := d.Get("name").(string)
// Create a new parameter struct
p := cs.Network.NewUpdateNetworkParams(d.Id())
// Check if the name or display text is changed
if d.HasChange("name") || d.HasChange("display_text") {
p.SetName(name)
// Compute/set the display text
displaytext := d.Get("display_text").(string)
if displaytext == "" {
displaytext = name
}
p.SetDisplaytext(displaytext)
}
// Check if the cidr is changed
if d.HasChange("cidr") {
p.SetGuestvmcidr(d.Get("cidr").(string))
}
// Check if the network domain is changed
if d.HasChange("network_domain") {
p.SetNetworkdomain(d.Get("network_domain").(string))
}
// Check if the network offering is changed
if d.HasChange("network_offering") {
// Retrieve the network_offering ID
networkofferingid, e := retrieveID(cs, "network_offering", d.Get("network_offering").(string))
if e != nil {
return e.Error()
}
// Set the new network offering
p.SetNetworkofferingid(networkofferingid)
}
// Update the network
_, err := cs.Network.UpdateNetwork(p)
if err != nil {
return fmt.Errorf(
"Error updating network %s: %s", name, err)
}
// Replace the ACL if the ID has changed
if d.HasChange("acl_id") {
p := cs.NetworkACL.NewReplaceNetworkACLListParams(d.Get("acl_id").(string))
p.SetNetworkid(d.Id())
_, err := cs.NetworkACL.ReplaceNetworkACLList(p)
if err != nil {
return fmt.Errorf("Error replacing ACL: %s", err)
}
}
// Update tags if they have changed
if d.HasChange("tags") {
if err := updateTags(cs, d, "Network"); err != nil {
return fmt.Errorf("Error updating tags on ACL %s: %s", name, err)
}
}
return resourceCloudStackNetworkRead(d, meta)
}
func resourceCloudStackNetworkDelete(d *schema.ResourceData, meta interface{}) error {
cs := meta.(*cloudstack.CloudStackClient)
// Create a new parameter struct
p := cs.Network.NewDeleteNetworkParams(d.Id())
// Delete the network
_, err := cs.Network.DeleteNetwork(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", d.Id())) {
return nil
}
return fmt.Errorf("Error deleting network %s: %s", d.Get("name").(string), err)
}
return nil
}
func parseCIDR(d *schema.ResourceData, specifyiprange bool) (map[string]string, error) {
m := make(map[string]string, 4)
cidr := d.Get("cidr").(string)
ip, ipnet, err := net.ParseCIDR(cidr)
if err != nil {
return nil, fmt.Errorf("Unable to parse cidr %s: %s", cidr, err)
}
msk := ipnet.Mask
sub := ip.Mask(msk)
m["netmask"] = fmt.Sprintf("%d.%d.%d.%d", msk[0], msk[1], msk[2], msk[3])
if gateway, ok := d.GetOk("gateway"); ok {
m["gateway"] = gateway.(string)
} else {
m["gateway"] = fmt.Sprintf("%d.%d.%d.%d", sub[0], sub[1], sub[2], sub[3]+1)
}
if startip, ok := d.GetOk("startip"); ok {
m["startip"] = startip.(string)
} else if specifyiprange {
m["startip"] = fmt.Sprintf("%d.%d.%d.%d", sub[0], sub[1], sub[2], sub[3]+2)
}
if endip, ok := d.GetOk("endip"); ok {
m["endip"] = endip.(string)
} else if specifyiprange {
m["endip"] = fmt.Sprintf("%d.%d.%d.%d",
sub[0]+(0xff-msk[0]), sub[1]+(0xff-msk[1]), sub[2]+(0xff-msk[2]), sub[3]+(0xff-msk[3]-1))
}
return m, nil
}
// addToIPv6 adds an integer offset to an IPv6 address with proper carry across all bytes.
// Returns a new net.IP with the result.
func addToIPv6(ip net.IP, offset uint64) net.IP {
result := make(net.IP, len(ip))
copy(result, ip)
carry := offset
// Start from the least significant byte (rightmost) and work backwards
for i := len(result) - 1; i >= 0 && carry > 0; i-- {
sum := uint64(result[i]) + carry
result[i] = byte(sum & 0xff)
carry = sum >> 8
}
return result
}
// validateIPv6InCIDR verifies that a user-supplied IPv6 address parses and falls
// within the given network. It returns a clear local error instead of letting an
// out-of-subnet value surface as an opaque CloudStack API failure at apply time.
func validateIPv6InCIDR(field, value string, ipnet *net.IPNet) error {
ip := net.ParseIP(value)
if ip == nil {
return fmt.Errorf("%s %q is not a valid IP address", field, value)
}
if !ipnet.Contains(ip) {
return fmt.Errorf("%s %q is not within ip6cidr %s", field, value, ipnet.String())
}
return nil
}
func parseCIDRv6(d *schema.ResourceData, specifyiprange bool) (map[string]string, error) {
m := make(map[string]string, 4)
cidr := d.Get("ip6cidr").(string)
ip, ipnet, err := net.ParseCIDR(cidr)
if err != nil {
return nil, fmt.Errorf("Unable to parse cidr %s: %s", cidr, err)
}
// Validate that this is actually an IPv6 CIDR
if ip.To4() != nil {
return nil, fmt.Errorf("ip6cidr must be an IPv6 CIDR, got IPv4: %s", cidr)
}
if len(ipnet.Mask) != net.IPv6len {
return nil, fmt.Errorf("ip6cidr must be an IPv6 CIDR with 16-byte mask, got %d bytes: %s", len(ipnet.Mask), cidr)
}
// Validate prefix length to ensure we have enough addresses for gateway/start/end
ones, _ := ipnet.Mask.Size()
if specifyiprange {
// When specifyiprange is true, we need at least 3 addresses:
// - gateway (network + 1)
// - start IP (network + 2)
// - end IP (network + 3 or more)
// This requires a /126 or larger prefix (4 addresses minimum)
if ones > 126 {
return nil, fmt.Errorf("ip6cidr prefix /%d is too small for automatic IP range generation; minimum is /126 (4 addresses)", ones)
}
} else {
// When specifyiprange is false, we only need the gateway (network + 1)
// This requires a /127 or larger prefix (2 addresses minimum)
if ones > 127 {
return nil, fmt.Errorf("ip6cidr prefix /%d is too small for automatic gateway generation; minimum is /127 (2 addresses)", ones)
}
}
if gateway, ok := d.GetOk("ip6gateway"); ok {
gw := gateway.(string)
if err := validateIPv6InCIDR("ip6gateway", gw, ipnet); err != nil {
return nil, err
}
m["ip6gateway"] = gw
} else {
// Default gateway to network address + 1 (e.g., 2001:db8::1)
gwip := addToIPv6(ipnet.IP, 1)
m["ip6gateway"] = gwip.String()
}
if startipv6, ok := d.GetOk("startipv6"); ok {
start := startipv6.(string)
if err := validateIPv6InCIDR("startipv6", start, ipnet); err != nil {
return nil, err
}
m["startipv6"] = start
} else if specifyiprange {
// Default start IP to network address + 2
startip := addToIPv6(ipnet.IP, 2)
m["startipv6"] = startip.String()
}
if endip, ok := d.GetOk("endipv6"); ok {
end := endip.(string)
if err := validateIPv6InCIDR("endipv6", end, ipnet); err != nil {
return nil, err
}
m["endipv6"] = end
} else if specifyiprange {
ip16 := ipnet.IP.To16()
if ip16 == nil {
return nil, fmt.Errorf("cidr not valid for ipv6")
}
last := make(net.IP, len(ip16))
copy(last, ip16)
for i := range ip16 {
// Perform bitwise OR with the inverse of the mask
last[i] |= ^ipnet.Mask[i]
}
m["endipv6"] = last.String()
}
return m, nil
}