Update Subnet Gateway Behavior (#102)
This commit enables all three behaviors of a gateway during subnet creation and
updating.
If a GatewayIP is omitted, Neutron will provision a default gateway.
If a GatewayIP is set to an empty string, no gateway will be provisioned.
If a GatewayIP is specified, it will be used as the gateway IP.
diff --git a/acceptance/openstack/networking/v2/networking.go b/acceptance/openstack/networking/v2/networking.go
index 9432dda..cc5befb 100644
--- a/acceptance/openstack/networking/v2/networking.go
+++ b/acceptance/openstack/networking/v2/networking.go
@@ -72,13 +72,12 @@
subnetOctet := tools.RandomInt(1, 250)
subnetCIDR := fmt.Sprintf("192.168.%d.0/24", subnetOctet)
subnetGateway := fmt.Sprintf("192.168.%d.1", subnetOctet)
- iFalse := false
createOpts := subnets.CreateOpts{
NetworkID: networkID,
CIDR: subnetCIDR,
IPVersion: 4,
Name: subnetName,
- EnableDHCP: &iFalse,
+ EnableDHCP: gophercloud.Disabled,
GatewayIP: &subnetGateway,
}
@@ -93,6 +92,68 @@
return subnet, nil
}
+// CreateSubnetWithDefaultGateway will create a subnet on the specified Network
+// ID and have Neutron set the gateway by default An error will be returned if
+// the subnet could not be created.
+func CreateSubnetWithDefaultGateway(t *testing.T, client *gophercloud.ServiceClient, networkID string) (*subnets.Subnet, error) {
+ subnetName := tools.RandomString("TESTACC-", 8)
+ subnetOctet := tools.RandomInt(1, 250)
+ subnetCIDR := fmt.Sprintf("192.168.%d.0/24", subnetOctet)
+ createOpts := subnets.CreateOpts{
+ NetworkID: networkID,
+ CIDR: subnetCIDR,
+ IPVersion: 4,
+ Name: subnetName,
+ EnableDHCP: gophercloud.Disabled,
+ }
+
+ t.Logf("Attempting to create subnet: %s", subnetName)
+
+ subnet, err := subnets.Create(client, createOpts).Extract()
+ if err != nil {
+ return subnet, err
+ }
+
+ t.Logf("Successfully created subnet.")
+ return subnet, nil
+}
+
+// CreateSubnetWithNoGateway will create a subnet with no gateway on the
+// specified Network ID. An error will be returned if the subnet could not be
+// created.
+func CreateSubnetWithNoGateway(t *testing.T, client *gophercloud.ServiceClient, networkID string) (*subnets.Subnet, error) {
+ var noGateway = ""
+ subnetName := tools.RandomString("TESTACC-", 8)
+ subnetOctet := tools.RandomInt(1, 250)
+ subnetCIDR := fmt.Sprintf("192.168.%d.0/24", subnetOctet)
+ dhcpStart := fmt.Sprintf("192.168.%d.10", subnetOctet)
+ dhcpEnd := fmt.Sprintf("192.168.%d.200", subnetOctet)
+ createOpts := subnets.CreateOpts{
+ NetworkID: networkID,
+ CIDR: subnetCIDR,
+ IPVersion: 4,
+ Name: subnetName,
+ EnableDHCP: gophercloud.Disabled,
+ GatewayIP: &noGateway,
+ AllocationPools: []subnets.AllocationPool{
+ {
+ Start: dhcpStart,
+ End: dhcpEnd,
+ },
+ },
+ }
+
+ t.Logf("Attempting to create subnet: %s", subnetName)
+
+ subnet, err := subnets.Create(client, createOpts).Extract()
+ if err != nil {
+ return subnet, err
+ }
+
+ t.Logf("Successfully created subnet.")
+ return subnet, nil
+}
+
// DeleteNetwork will delete a network with a specified ID. A fatal error will
// occur if the delete was not successful. This works best when used as a
// deferred function.
diff --git a/acceptance/openstack/networking/v2/subnets_test.go b/acceptance/openstack/networking/v2/subnets_test.go
index 49c970a..1d7696c 100644
--- a/acceptance/openstack/networking/v2/subnets_test.go
+++ b/acceptance/openstack/networking/v2/subnets_test.go
@@ -3,6 +3,8 @@
package v2
import (
+ "fmt"
+ "strings"
"testing"
"github.com/gophercloud/gophercloud/acceptance/clients"
@@ -71,3 +73,86 @@
PrintSubnet(t, newSubnet)
}
+
+func TestSubnetsDefaultGateway(t *testing.T) {
+ client, err := clients.NewNetworkV2Client()
+ if err != nil {
+ t.Fatalf("Unable to create a network client: %v", err)
+ }
+
+ // Create Network
+ network, err := CreateNetwork(t, client)
+ if err != nil {
+ t.Fatalf("Unable to create network: %v", err)
+ }
+ defer DeleteNetwork(t, client, network.ID)
+
+ // Create Subnet
+ subnet, err := CreateSubnetWithDefaultGateway(t, client, network.ID)
+ if err != nil {
+ t.Fatalf("Unable to create subnet: %v", err)
+ }
+ defer DeleteSubnet(t, client, subnet.ID)
+
+ PrintSubnet(t, subnet)
+
+ if subnet.GatewayIP == "" {
+ t.Fatalf("A default gateway was not created.")
+ }
+
+ var noGateway = ""
+ updateOpts := subnets.UpdateOpts{
+ GatewayIP: &noGateway,
+ }
+
+ newSubnet, err := subnets.Update(client, subnet.ID, updateOpts).Extract()
+ if err != nil {
+ t.Fatalf("Unable to update subnet")
+ }
+
+ if newSubnet.GatewayIP != "" {
+ t.Fatalf("Gateway was not updated correctly")
+ }
+}
+
+func TestSubnetsNoGateway(t *testing.T) {
+ client, err := clients.NewNetworkV2Client()
+ if err != nil {
+ t.Fatalf("Unable to create a network client: %v", err)
+ }
+
+ // Create Network
+ network, err := CreateNetwork(t, client)
+ if err != nil {
+ t.Fatalf("Unable to create network: %v", err)
+ }
+ defer DeleteNetwork(t, client, network.ID)
+
+ // Create Subnet
+ subnet, err := CreateSubnetWithNoGateway(t, client, network.ID)
+ if err != nil {
+ t.Fatalf("Unable to create subnet: %v", err)
+ }
+ defer DeleteSubnet(t, client, subnet.ID)
+
+ PrintSubnet(t, subnet)
+
+ if subnet.GatewayIP != "" {
+ t.Fatalf("A gateway exists when it shouldn't.")
+ }
+
+ subnetParts := strings.Split(subnet.CIDR, ".")
+ newGateway := fmt.Sprintf("%s.%s.%s.1", subnetParts[0], subnetParts[1], subnetParts[2])
+ updateOpts := subnets.UpdateOpts{
+ GatewayIP: &newGateway,
+ }
+
+ newSubnet, err := subnets.Update(client, subnet.ID, updateOpts).Extract()
+ if err != nil {
+ t.Fatalf("Unable to update subnet")
+ }
+
+ if newSubnet.GatewayIP == "" {
+ t.Fatalf("Gateway was not updated correctly")
+ }
+}
diff --git a/openstack/networking/v2/subnets/requests.go b/openstack/networking/v2/subnets/requests.go
index 769474d..896f13e 100644
--- a/openstack/networking/v2/subnets/requests.go
+++ b/openstack/networking/v2/subnets/requests.go
@@ -79,7 +79,7 @@
Name string `json:"name,omitempty"`
TenantID string `json:"tenant_id,omitempty"`
AllocationPools []AllocationPool `json:"allocation_pools,omitempty"`
- GatewayIP *string `json:"gateway_ip"`
+ GatewayIP *string `json:"gateway_ip,omitempty"`
IPVersion gophercloud.IPVersion `json:"ip_version,omitempty"`
EnableDHCP *bool `json:"enable_dhcp,omitempty"`
DNSNameservers []string `json:"dns_nameservers,omitempty"`
@@ -88,7 +88,16 @@
// ToSubnetCreateMap casts a CreateOpts struct to a map.
func (opts CreateOpts) ToSubnetCreateMap() (map[string]interface{}, error) {
- return gophercloud.BuildRequestBody(opts, "subnet")
+ b, err := gophercloud.BuildRequestBody(opts, "subnet")
+ if err != nil {
+ return nil, err
+ }
+
+ if m := b["subnet"].(map[string]interface{}); m["gateway_ip"] == "" {
+ m["gateway_ip"] = nil
+ }
+
+ return b, nil
}
// Create accepts a CreateOpts struct and creates a new subnet using the values
@@ -112,7 +121,7 @@
// UpdateOpts represents the attributes used when updating an existing subnet.
type UpdateOpts struct {
Name string `json:"name,omitempty"`
- GatewayIP string `json:"gateway_ip,omitempty"`
+ GatewayIP *string `json:"gateway_ip,omitempty"`
DNSNameservers []string `json:"dns_nameservers,omitempty"`
HostRoutes []HostRoute `json:"host_routes,omitempty"`
EnableDHCP *bool `json:"enable_dhcp,omitempty"`
@@ -120,7 +129,16 @@
// ToSubnetUpdateMap casts an UpdateOpts struct to a map.
func (opts UpdateOpts) ToSubnetUpdateMap() (map[string]interface{}, error) {
- return gophercloud.BuildRequestBody(opts, "subnet")
+ b, err := gophercloud.BuildRequestBody(opts, "subnet")
+ if err != nil {
+ return nil, err
+ }
+
+ if m := b["subnet"].(map[string]interface{}); m["gateway_ip"] == "" {
+ m["gateway_ip"] = nil
+ }
+
+ return b, nil
}
// Update accepts a UpdateOpts struct and updates an existing subnet using the
diff --git a/openstack/networking/v2/subnets/testing/requests_test.go b/openstack/networking/v2/subnets/testing/requests_test.go
index 13fa9df..9ff9181 100644
--- a/openstack/networking/v2/subnets/testing/requests_test.go
+++ b/openstack/networking/v2/subnets/testing/requests_test.go
@@ -231,16 +231,16 @@
"subnet": {
"network_id": "d32019d3-bc6e-4319-9c1d-6722fc136a22",
"ip_version": 4,
- "gateway_ip": null,
+ "gateway_ip": "192.168.199.1",
"cidr": "192.168.199.0/24",
- "dns_nameservers": ["foo"],
- "allocation_pools": [
- {
- "start": "192.168.199.2",
- "end": "192.168.199.254"
- }
- ],
- "host_routes": [{"destination":"","nexthop": "bar"}]
+ "dns_nameservers": ["foo"],
+ "allocation_pools": [
+ {
+ "start": "192.168.199.2",
+ "end": "192.168.199.254"
+ }
+ ],
+ "host_routes": [{"destination":"","nexthop": "bar"}]
}
}
`)
@@ -272,10 +272,12 @@
`)
})
+ var gatewayIP = "192.168.199.1"
opts := subnets.CreateOpts{
NetworkID: "d32019d3-bc6e-4319-9c1d-6722fc136a22",
IPVersion: 4,
CIDR: "192.168.199.0/24",
+ GatewayIP: &gatewayIP,
AllocationPools: []subnets.AllocationPool{
{
Start: "192.168.199.2",
@@ -323,13 +325,13 @@
"network_id": "d32019d3-bc6e-4319-9c1d-6722fc136a23",
"ip_version": 4,
"cidr": "192.168.1.0/24",
- "gateway_ip": null,
- "allocation_pools": [
- {
- "start": "192.168.1.2",
- "end": "192.168.1.254"
- }
- ]
+ "gateway_ip": null,
+ "allocation_pools": [
+ {
+ "start": "192.168.1.2",
+ "end": "192.168.1.254"
+ }
+ ]
}
}
`)
@@ -360,6 +362,91 @@
`)
})
+ var noGateway = ""
+ opts := subnets.CreateOpts{
+ NetworkID: "d32019d3-bc6e-4319-9c1d-6722fc136a23",
+ IPVersion: 4,
+ CIDR: "192.168.1.0/24",
+ GatewayIP: &noGateway,
+ AllocationPools: []subnets.AllocationPool{
+ {
+ Start: "192.168.1.2",
+ End: "192.168.1.254",
+ },
+ },
+ DNSNameservers: []string{},
+ }
+ s, err := subnets.Create(fake.ServiceClient(), opts).Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, s.Name, "")
+ th.AssertEquals(t, s.EnableDHCP, true)
+ th.AssertEquals(t, s.NetworkID, "d32019d3-bc6e-4319-9c1d-6722fc136a23")
+ th.AssertEquals(t, s.TenantID, "4fd44f30292945e481c7b8a0c8908869")
+ th.AssertDeepEquals(t, s.AllocationPools, []subnets.AllocationPool{
+ {
+ Start: "192.168.1.2",
+ End: "192.168.1.254",
+ },
+ })
+ th.AssertDeepEquals(t, s.HostRoutes, []subnets.HostRoute{})
+ th.AssertEquals(t, s.IPVersion, 4)
+ th.AssertEquals(t, s.GatewayIP, "")
+ th.AssertEquals(t, s.CIDR, "192.168.1.0/24")
+ th.AssertEquals(t, s.ID, "54d6f61d-db07-451c-9ab3-b9609b6b6f0c")
+}
+
+func TestCreateDefaultGateway(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/subnets", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "POST")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ th.TestHeader(t, r, "Content-Type", "application/json")
+ th.TestHeader(t, r, "Accept", "application/json")
+ th.TestJSONRequest(t, r, `
+{
+ "subnet": {
+ "network_id": "d32019d3-bc6e-4319-9c1d-6722fc136a23",
+ "ip_version": 4,
+ "cidr": "192.168.1.0/24",
+ "allocation_pools": [
+ {
+ "start": "192.168.1.2",
+ "end": "192.168.1.254"
+ }
+ ]
+ }
+}
+ `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusCreated)
+
+ fmt.Fprintf(w, `
+{
+ "subnet": {
+ "name": "",
+ "enable_dhcp": true,
+ "network_id": "d32019d3-bc6e-4319-9c1d-6722fc136a23",
+ "tenant_id": "4fd44f30292945e481c7b8a0c8908869",
+ "allocation_pools": [
+ {
+ "start": "192.168.1.2",
+ "end": "192.168.1.254"
+ }
+ ],
+ "host_routes": [],
+ "ip_version": 4,
+ "gateway_ip": "192.168.1.1",
+ "cidr": "192.168.1.0/24",
+ "id": "54d6f61d-db07-451c-9ab3-b9609b6b6f0c"
+ }
+}
+ `)
+ })
+
opts := subnets.CreateOpts{
NetworkID: "d32019d3-bc6e-4319-9c1d-6722fc136a23",
IPVersion: 4,
@@ -387,7 +474,7 @@
})
th.AssertDeepEquals(t, s.HostRoutes, []subnets.HostRoute{})
th.AssertEquals(t, s.IPVersion, 4)
- th.AssertEquals(t, s.GatewayIP, "")
+ th.AssertEquals(t, s.GatewayIP, "192.168.1.1")
th.AssertEquals(t, s.CIDR, "192.168.1.0/24")
th.AssertEquals(t, s.ID, "54d6f61d-db07-451c-9ab3-b9609b6b6f0c")
}
@@ -422,8 +509,8 @@
{
"subnet": {
"name": "my_new_subnet",
- "dns_nameservers": ["foo"],
- "host_routes": [{"destination":"","nexthop": "bar"}]
+ "dns_nameservers": ["foo"],
+ "host_routes": [{"destination":"","nexthop": "bar"}]
}
}
`)
@@ -469,6 +556,122 @@
th.AssertEquals(t, s.ID, "08eae331-0402-425a-923c-34f7cfe39c1b")
}
+func TestUpdateGateway(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/subnets/08eae331-0402-425a-923c-34f7cfe39c1b", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "PUT")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ th.TestHeader(t, r, "Content-Type", "application/json")
+ th.TestHeader(t, r, "Accept", "application/json")
+ th.TestJSONRequest(t, r, `
+{
+ "subnet": {
+ "name": "my_new_subnet",
+ "gateway_ip": "10.0.0.1"
+ }
+}
+ `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusCreated)
+
+ fmt.Fprintf(w, `
+{
+ "subnet": {
+ "name": "my_new_subnet",
+ "enable_dhcp": true,
+ "network_id": "db193ab3-96e3-4cb3-8fc5-05f4296d0324",
+ "tenant_id": "26a7980765d0414dbc1fc1f88cdb7e6e",
+ "dns_nameservers": [],
+ "allocation_pools": [
+ {
+ "start": "10.0.0.2",
+ "end": "10.0.0.254"
+ }
+ ],
+ "host_routes": [],
+ "ip_version": 4,
+ "gateway_ip": "10.0.0.1",
+ "cidr": "10.0.0.0/24",
+ "id": "08eae331-0402-425a-923c-34f7cfe39c1b"
+ }
+}
+ `)
+ })
+
+ var gatewayIP = "10.0.0.1"
+ opts := subnets.UpdateOpts{
+ Name: "my_new_subnet",
+ GatewayIP: &gatewayIP,
+ }
+ s, err := subnets.Update(fake.ServiceClient(), "08eae331-0402-425a-923c-34f7cfe39c1b", opts).Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, s.Name, "my_new_subnet")
+ th.AssertEquals(t, s.ID, "08eae331-0402-425a-923c-34f7cfe39c1b")
+ th.AssertEquals(t, s.GatewayIP, "10.0.0.1")
+}
+
+func TestUpdateRemoveGateway(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/subnets/08eae331-0402-425a-923c-34f7cfe39c1b", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "PUT")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ th.TestHeader(t, r, "Content-Type", "application/json")
+ th.TestHeader(t, r, "Accept", "application/json")
+ th.TestJSONRequest(t, r, `
+{
+ "subnet": {
+ "name": "my_new_subnet",
+ "gateway_ip": null
+ }
+}
+ `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusCreated)
+
+ fmt.Fprintf(w, `
+{
+ "subnet": {
+ "name": "my_new_subnet",
+ "enable_dhcp": true,
+ "network_id": "db193ab3-96e3-4cb3-8fc5-05f4296d0324",
+ "tenant_id": "26a7980765d0414dbc1fc1f88cdb7e6e",
+ "dns_nameservers": [],
+ "allocation_pools": [
+ {
+ "start": "10.0.0.2",
+ "end": "10.0.0.254"
+ }
+ ],
+ "host_routes": [],
+ "ip_version": 4,
+ "gateway_ip": null,
+ "cidr": "10.0.0.0/24",
+ "id": "08eae331-0402-425a-923c-34f7cfe39c1b"
+ }
+}
+ `)
+ })
+
+ var noGateway = ""
+ opts := subnets.UpdateOpts{
+ Name: "my_new_subnet",
+ GatewayIP: &noGateway,
+ }
+ s, err := subnets.Update(fake.ServiceClient(), "08eae331-0402-425a-923c-34f7cfe39c1b", opts).Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, s.Name, "my_new_subnet")
+ th.AssertEquals(t, s.ID, "08eae331-0402-425a-923c-34f7cfe39c1b")
+ th.AssertEquals(t, s.GatewayIP, "")
+}
+
func TestDelete(t *testing.T) {
th.SetupHTTP()
defer th.TeardownHTTP()