Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 123 additions & 0 deletions cloudstack/resource_cloudstack_network.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,13 +78,26 @@ func resourceCloudStackNetwork() *schema.Resource {
ForceNew: true,
},

"ip6cidr": {
Type: schema.TypeString,
Optional: true,
ForceNew: true,
},

"gateway": {
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
},

"ip6gateway": {
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
},

"startip": {
Type: schema.TypeString,
Optional: true,
Expand All @@ -99,6 +112,20 @@ func resourceCloudStackNetwork() *schema.Resource {
ForceNew: true,
},

"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,
Expand Down Expand Up @@ -209,6 +236,31 @@ func resourceCloudStackNetworkCreate(d *schema.ResourceData, meta interface{}) e
p.SetEndip(endip)
}

// IPv6 support
if ip6cidr, ok := d.GetOk("ip6cidr"); ok && ip6cidr.(string) != none {
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))
Expand Down Expand Up @@ -306,6 +358,19 @@ func resourceCloudStackNetworkRead(d *schema.ResourceData, meta interface{}) err
d.Set("network_domain", n.Networkdomain)
d.Set("vpc_id", n.Vpcid)

// Only set ip6cidr if it has a value
if n.Ip6cidr != "" {
d.Set("ip6cidr", n.Ip6cidr)
}

// Only set ip6gateway if it has a value
if n.Ip6gateway != "" {
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
}
Expand Down Expand Up @@ -471,3 +536,61 @@ func parseCIDR(d *schema.ResourceData, specifyiprange bool) (map[string]string,

return m, nil
}

func parseCIDRv6(d *schema.ResourceData, specifyiprange bool) (map[string]string, error) {
m := make(map[string]string, 4)

cidr := d.Get("ip6cidr").(string)
_, ipnet, err := net.ParseCIDR(cidr)
if err != nil {
return nil, fmt.Errorf("Unable to parse cidr %s: %s", cidr, err)
}

if gateway, ok := d.GetOk("ip6gateway"); ok {
m["ip6gateway"] = gateway.(string)
} else {
// Default gateway to network address + 1 (e.g., 2001:db8::1)
ip16 := ipnet.IP.To16()
if ip16 == nil {
return nil, fmt.Errorf("cidr not valid for ipv6")
}
gwip := make(net.IP, len(ip16))
copy(gwip, ip16)
gwip[len(ip16)-1] = 1
m["ip6gateway"] = gwip.String()
}

if startipv6, ok := d.GetOk("startipv6"); ok {
m["startipv6"] = startipv6.(string)
} else if specifyiprange {
ip16 := ipnet.IP.To16()
if ip16 == nil {
return nil, fmt.Errorf("cidr not valid for ipv6")
}

myip := make(net.IP, len(ip16))
copy(myip, ip16)
myip[len(ip16)-1] = 2
m["startipv6"] = myip.String()
}

if endip, ok := d.GetOk("endipv6"); ok {
m["endipv6"] = endip.(string)
} 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
}
143 changes: 143 additions & 0 deletions cloudstack/resource_cloudstack_network_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@
// under the License.
//

// NOTE: IPv6 acceptance tests (TestAccCloudStackNetwork_ipv6*) are currently
// skipped when running against the CloudStack simulator because the simulator
// only supports IPv6 with advanced shared network offerings. These tests will
// work correctly against a real CloudStack environment with proper IPv6 support.
// Unit tests for the IPv6 CIDR parsing logic are available in
// resource_cloudstack_network_unit_test.go and do not require a CloudStack instance.

package cloudstack

import (
Expand Down Expand Up @@ -165,6 +172,75 @@ func TestAccCloudStackNetwork_importProject(t *testing.T) {
})
}

func TestAccCloudStackNetwork_ipv6(t *testing.T) {
t.Skip("Skipping IPv6 test: CloudStack simulator only supports IPv6 with advanced shared networks")
var network cloudstack.Network

resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckCloudStackNetworkDestroy,
Steps: []resource.TestStep{
{
Config: testAccCloudStackNetwork_ipv6,
Check: resource.ComposeTestCheckFunc(
testAccCheckCloudStackNetworkExists(
"cloudstack_network.foo", &network),
testAccCheckCloudStackNetworkIPv6Attributes(&network),
resource.TestCheckResourceAttr(
"cloudstack_network.foo", "ip6cidr", "2001:db8::/64"),
),
},
},
})
}

func TestAccCloudStackNetwork_ipv6_vpc(t *testing.T) {
t.Skip("Skipping IPv6 test: CloudStack simulator only supports IPv6 with advanced shared networks")
var network cloudstack.Network

resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckCloudStackNetworkDestroy,
Steps: []resource.TestStep{
{
Config: testAccCloudStackNetwork_ipv6_vpc,
Check: resource.ComposeTestCheckFunc(
testAccCheckCloudStackNetworkExists(
"cloudstack_network.foo", &network),
resource.TestCheckResourceAttr(
"cloudstack_network.foo", "ip6cidr", "2001:db8:1::/64"),
),
},
},
})
}

func TestAccCloudStackNetwork_ipv6_custom_gateway(t *testing.T) {
t.Skip("Skipping IPv6 test: CloudStack simulator only supports IPv6 with advanced shared networks")
var network cloudstack.Network

resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckCloudStackNetworkDestroy,
Steps: []resource.TestStep{
{
Config: testAccCloudStackNetwork_ipv6_custom_gateway,
Check: resource.ComposeTestCheckFunc(
testAccCheckCloudStackNetworkExists(
"cloudstack_network.foo", &network),
resource.TestCheckResourceAttr(
"cloudstack_network.foo", "ip6cidr", "2001:db8:2::/64"),
resource.TestCheckResourceAttr(
"cloudstack_network.foo", "ip6gateway", "2001:db8:2::1"),
),
},
},
})
}

func testAccCheckCloudStackNetworkExists(
n string, network *cloudstack.Network) resource.TestCheckFunc {
return func(s *terraform.State) error {
Expand Down Expand Up @@ -244,6 +320,34 @@ func testAccCheckCloudStackNetworkVPCAttributes(
}
}

func testAccCheckCloudStackNetworkIPv6Attributes(
network *cloudstack.Network) resource.TestCheckFunc {
return func(s *terraform.State) error {

if network.Name != "terraform-network-ipv6" {
return fmt.Errorf("Bad name: %s", network.Name)
}

if network.Displaytext != "terraform-network-ipv6" {
return fmt.Errorf("Bad display name: %s", network.Displaytext)
}

if network.Cidr != "10.1.2.0/24" {
return fmt.Errorf("Bad CIDR: %s", network.Cidr)
}

if network.Ip6cidr != "2001:db8::/64" {
return fmt.Errorf("Bad IPv6 CIDR: %s", network.Ip6cidr)
}

if network.Networkofferingname != "DefaultIsolatedNetworkOfferingWithSourceNatService" {
return fmt.Errorf("Bad network offering: %s", network.Networkofferingname)
}

return nil
}
}

func testAccCheckCloudStackNetworkDestroy(s *terraform.State) error {
cs := testAccProvider.Meta().(*cloudstack.CloudStackClient)

Expand Down Expand Up @@ -377,3 +481,42 @@ resource "cloudstack_network" "foo" {
acl_id = cloudstack_network_acl.bar.id
zone = cloudstack_vpc.foo.zone
}`

const testAccCloudStackNetwork_ipv6 = `
resource "cloudstack_network" "foo" {
name = "terraform-network-ipv6"
display_text = "terraform-network-ipv6"
cidr = "10.1.2.0/24"
ip6cidr = "2001:db8::/64"
network_offering = "DefaultIsolatedNetworkOfferingWithSourceNatService"
zone = "Sandbox-simulator"
}`

const testAccCloudStackNetwork_ipv6_vpc = `
resource "cloudstack_vpc" "foo" {
name = "terraform-vpc-ipv6"
cidr = "10.0.0.0/8"
vpc_offering = "Default VPC offering"
zone = "Sandbox-simulator"
}

resource "cloudstack_network" "foo" {
name = "terraform-network-ipv6"
display_text = "terraform-network-ipv6"
cidr = "10.1.1.0/24"
ip6cidr = "2001:db8:1::/64"
network_offering = "DefaultIsolatedNetworkOfferingForVpcNetworks"
vpc_id = cloudstack_vpc.foo.id
zone = cloudstack_vpc.foo.zone
}`

const testAccCloudStackNetwork_ipv6_custom_gateway = `
resource "cloudstack_network" "foo" {
name = "terraform-network-ipv6-custom"
display_text = "terraform-network-ipv6-custom"
cidr = "10.1.3.0/24"
ip6cidr = "2001:db8:2::/64"
ip6gateway = "2001:db8:2::1"
network_offering = "DefaultIsolatedNetworkOfferingWithSourceNatService"
zone = "Sandbox-simulator"
}`
Loading