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/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()