Ports: Remove Security Groups and Address Pairs (#236)

This commit enables security groups and address pairs to be removed
from a port. This is done by allowing an empty value to be passed
for either attribute in the port update request.
diff --git a/acceptance/openstack/networking/v2/ports_test.go b/acceptance/openstack/networking/v2/ports_test.go
index fa1b4a4..a11ff55 100644
--- a/acceptance/openstack/networking/v2/ports_test.go
+++ b/acceptance/openstack/networking/v2/ports_test.go
@@ -6,6 +6,7 @@
 	"testing"
 
 	"github.com/gophercloud/gophercloud/acceptance/clients"
+	extensions "github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/extensions"
 	"github.com/gophercloud/gophercloud/acceptance/tools"
 	"github.com/gophercloud/gophercloud/openstack/networking/v2/ports"
 )
@@ -72,3 +73,120 @@
 
 	tools.PrintResource(t, newPort)
 }
+
+func TestPortsRemoveSecurityGroups(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 := CreateSubnet(t, client, network.ID)
+	if err != nil {
+		t.Fatalf("Unable to create subnet: %v", err)
+	}
+	defer DeleteSubnet(t, client, subnet.ID)
+
+	// Create port
+	port, err := CreatePort(t, client, network.ID, subnet.ID)
+	if err != nil {
+		t.Fatalf("Unable to create port: %v", err)
+	}
+	defer DeletePort(t, client, port.ID)
+
+	PrintPort(t, port)
+
+	// Create a Security Group
+	group, err := extensions.CreateSecurityGroup(t, client)
+	if err != nil {
+		t.Fatalf("Unable to create security group: %v", err)
+	}
+	defer extensions.DeleteSecurityGroup(t, client, group.ID)
+
+	// Add the group to the port
+	updateOpts := ports.UpdateOpts{
+		SecurityGroups: []string{group.ID},
+	}
+	newPort, err := ports.Update(client, port.ID, updateOpts).Extract()
+	if err != nil {
+		t.Fatalf("Could not update port: %v", err)
+	}
+
+	// Remove the group
+	updateOpts = ports.UpdateOpts{
+		SecurityGroups: []string{},
+	}
+	newPort, err = ports.Update(client, port.ID, updateOpts).Extract()
+	if err != nil {
+		t.Fatalf("Could not update port: %v", err)
+	}
+
+	PrintPort(t, newPort)
+
+	if len(newPort.SecurityGroups) > 0 {
+		t.Fatalf("Unable to remove security group from port")
+	}
+}
+
+func TestPortsRemoveAddressPair(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 := CreateSubnet(t, client, network.ID)
+	if err != nil {
+		t.Fatalf("Unable to create subnet: %v", err)
+	}
+	defer DeleteSubnet(t, client, subnet.ID)
+
+	// Create port
+	port, err := CreatePort(t, client, network.ID, subnet.ID)
+	if err != nil {
+		t.Fatalf("Unable to create port: %v", err)
+	}
+	defer DeletePort(t, client, port.ID)
+
+	PrintPort(t, port)
+
+	// Add an address pair to the port
+	updateOpts := ports.UpdateOpts{
+		AllowedAddressPairs: []ports.AddressPair{
+			ports.AddressPair{IPAddress: "192.168.255.10", MACAddress: "aa:bb:cc:dd:ee:ff"},
+		},
+	}
+	newPort, err := ports.Update(client, port.ID, updateOpts).Extract()
+	if err != nil {
+		t.Fatalf("Could not update port: %v", err)
+	}
+
+	// Remove the address pair
+	updateOpts = ports.UpdateOpts{
+		AllowedAddressPairs: []ports.AddressPair{},
+	}
+	newPort, err = ports.Update(client, port.ID, updateOpts).Extract()
+	if err != nil {
+		t.Fatalf("Could not update port: %v", err)
+	}
+
+	PrintPort(t, newPort)
+
+	if len(newPort.AllowedAddressPairs) > 0 {
+		t.Fatalf("Unable to remove the address pair")
+	}
+}
diff --git a/openstack/networking/v2/ports/requests.go b/openstack/networking/v2/ports/requests.go
index 2a53202..d353b7e 100644
--- a/openstack/networking/v2/ports/requests.go
+++ b/openstack/networking/v2/ports/requests.go
@@ -119,8 +119,8 @@
 	FixedIPs            interface{}   `json:"fixed_ips,omitempty"`
 	DeviceID            string        `json:"device_id,omitempty"`
 	DeviceOwner         string        `json:"device_owner,omitempty"`
-	SecurityGroups      []string      `json:"security_groups,omitempty"`
-	AllowedAddressPairs []AddressPair `json:"allowed_address_pairs,omitempty"`
+	SecurityGroups      []string      `json:"security_groups"`
+	AllowedAddressPairs []AddressPair `json:"allowed_address_pairs"`
 }
 
 // ToPortUpdateMap casts an UpdateOpts struct to a map.
diff --git a/openstack/networking/v2/ports/testing/requests_test.go b/openstack/networking/v2/ports/testing/requests_test.go
index 007dc5a..1da6ad3 100644
--- a/openstack/networking/v2/ports/testing/requests_test.go
+++ b/openstack/networking/v2/ports/testing/requests_test.go
@@ -342,6 +342,168 @@
 	th.AssertDeepEquals(t, s.SecurityGroups, []string{"f0ac4394-7e4a-4409-9701-ba8be283dbc3"})
 }
 
+func TestRemoveSecurityGroups(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/ports/65c0ee9f-d634-4522-8954-51021b570b0d", 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, `
+{
+    "port": {
+      "name": "new_port_name",
+      "fixed_ips": [
+        {
+          "subnet_id": "a0304c3a-4f08-4c43-88af-d796509c97d2",
+          "ip_address": "10.0.0.3"
+        }
+      ],
+      "allowed_address_pairs": [
+        {
+          "ip_address": "10.0.0.4",
+          "mac_address": "fa:16:3e:c9:cb:f0"
+        }
+      ],
+      "security_groups": []
+    }
+}
+			`)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+    "port": {
+        "status": "DOWN",
+        "name": "new_port_name",
+        "admin_state_up": true,
+        "network_id": "a87cc70a-3e15-4acf-8205-9b711a3531b7",
+        "tenant_id": "d6700c0c9ffa4f1cb322cd4a1f3906fa",
+        "device_owner": "",
+        "mac_address": "fa:16:3e:c9:cb:f0",
+        "fixed_ips": [
+            {
+                "subnet_id": "a0304c3a-4f08-4c43-88af-d796509c97d2",
+                "ip_address": "10.0.0.3"
+            }
+        ],
+        "allowed_address_pairs": [
+          {
+            "ip_address": "10.0.0.4",
+            "mac_address": "fa:16:3e:c9:cb:f0"
+          }
+        ],
+        "id": "65c0ee9f-d634-4522-8954-51021b570b0d",
+        "device_id": ""
+    }
+}
+		`)
+	})
+
+	options := ports.UpdateOpts{
+		Name: "new_port_name",
+		FixedIPs: []ports.IP{
+			{SubnetID: "a0304c3a-4f08-4c43-88af-d796509c97d2", IPAddress: "10.0.0.3"},
+		},
+		SecurityGroups: []string{},
+		AllowedAddressPairs: []ports.AddressPair{
+			{IPAddress: "10.0.0.4", MACAddress: "fa:16:3e:c9:cb:f0"},
+		},
+	}
+
+	s, err := ports.Update(fake.ServiceClient(), "65c0ee9f-d634-4522-8954-51021b570b0d", options).Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, s.Name, "new_port_name")
+	th.AssertDeepEquals(t, s.FixedIPs, []ports.IP{
+		{SubnetID: "a0304c3a-4f08-4c43-88af-d796509c97d2", IPAddress: "10.0.0.3"},
+	})
+	th.AssertDeepEquals(t, s.AllowedAddressPairs, []ports.AddressPair{
+		{IPAddress: "10.0.0.4", MACAddress: "fa:16:3e:c9:cb:f0"},
+	})
+	th.AssertDeepEquals(t, s.SecurityGroups, []string(nil))
+}
+
+func TestRemoveAllowedAddressPairs(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/ports/65c0ee9f-d634-4522-8954-51021b570b0d", 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, `
+{
+    "port": {
+      "name": "new_port_name",
+      "fixed_ips": [
+        {
+          "subnet_id": "a0304c3a-4f08-4c43-88af-d796509c97d2",
+          "ip_address": "10.0.0.3"
+        }
+      ],
+      "allowed_address_pairs": [],
+      "security_groups": [
+        "f0ac4394-7e4a-4409-9701-ba8be283dbc3"
+      ]
+    }
+}
+			`)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+    "port": {
+        "status": "DOWN",
+        "name": "new_port_name",
+        "admin_state_up": true,
+        "network_id": "a87cc70a-3e15-4acf-8205-9b711a3531b7",
+        "tenant_id": "d6700c0c9ffa4f1cb322cd4a1f3906fa",
+        "device_owner": "",
+        "mac_address": "fa:16:3e:c9:cb:f0",
+        "fixed_ips": [
+            {
+                "subnet_id": "a0304c3a-4f08-4c43-88af-d796509c97d2",
+                "ip_address": "10.0.0.3"
+            }
+        ],
+        "id": "65c0ee9f-d634-4522-8954-51021b570b0d",
+        "security_groups": [
+            "f0ac4394-7e4a-4409-9701-ba8be283dbc3"
+        ],
+        "device_id": ""
+    }
+}
+		`)
+	})
+
+	options := ports.UpdateOpts{
+		Name: "new_port_name",
+		FixedIPs: []ports.IP{
+			{SubnetID: "a0304c3a-4f08-4c43-88af-d796509c97d2", IPAddress: "10.0.0.3"},
+		},
+		SecurityGroups:      []string{"f0ac4394-7e4a-4409-9701-ba8be283dbc3"},
+		AllowedAddressPairs: []ports.AddressPair{},
+	}
+
+	s, err := ports.Update(fake.ServiceClient(), "65c0ee9f-d634-4522-8954-51021b570b0d", options).Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, s.Name, "new_port_name")
+	th.AssertDeepEquals(t, s.FixedIPs, []ports.IP{
+		{SubnetID: "a0304c3a-4f08-4c43-88af-d796509c97d2", IPAddress: "10.0.0.3"},
+	})
+	th.AssertDeepEquals(t, s.AllowedAddressPairs, []ports.AddressPair(nil))
+	th.AssertDeepEquals(t, s.SecurityGroups, []string{"f0ac4394-7e4a-4409-9701-ba8be283dbc3"})
+}
+
 func TestDelete(t *testing.T) {
 	th.SetupHTTP()
 	defer th.TeardownHTTP()