Allow to specify hostid when creating/updating a port
diff --git a/openstack/networking/v2/extensions/portsbinding/doc.go b/openstack/networking/v2/extensions/portsbinding/doc.go
new file mode 100644
index 0000000..0d2ed58
--- /dev/null
+++ b/openstack/networking/v2/extensions/portsbinding/doc.go
@@ -0,0 +1,3 @@
+// Package portsbinding provides information and interaction with the port
+// binding extension for the OpenStack Networking service.
+package portsbinding
diff --git a/openstack/networking/v2/extensions/portsbinding/requests.go b/openstack/networking/v2/extensions/portsbinding/requests.go
new file mode 100644
index 0000000..952e082
--- /dev/null
+++ b/openstack/networking/v2/extensions/portsbinding/requests.go
@@ -0,0 +1,114 @@
+package portsbinding
+
+import (
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/openstack/networking/v2/ports"
+)
+
+// CreateOpts represents the attributes used when creating a new
+// port with extended attributes.
+type CreateOpts struct {
+ // CreateOptsBuilder is the interface options structs have to satisfy in order
+ // to be used in the main Create operation in this package.
+ ports.CreateOptsBuilder
+ // The ID of the host where the port is allocated
+ HostID string
+ // The virtual network interface card (vNIC) type that is bound to the
+ // neutron port
+ VNICType string
+ // A dictionary that enables the application running on the specified
+ // host to pass and receive virtual network interface (VIF) port-specific
+ // information to the plug-in
+ Profile map[string]string
+}
+
+// Get retrieves a specific port based on its unique ID.
+func Get(c *gophercloud.ServiceClient, id string) GetResult {
+ var res GetResult
+ _, res.Err = c.Get(getURL(c, id), &res.Body, nil)
+ return res
+}
+
+// ToPortCreateMap casts a CreateOpts struct to a map.
+func (opts CreateOpts) ToPortCreateMap() (map[string]interface{}, error) {
+ p, err := opts.CreateOptsBuilder.ToPortCreateMap()
+ if err != nil {
+ return nil, err
+ }
+
+ port := p["port"].(map[string]interface{})
+
+ if opts.HostID != "" {
+ port["binding:host_id"] = opts.HostID
+ }
+ if opts.VNICType != "" {
+ port["binding:vnic_type"] = opts.VNICType
+ }
+ if opts.Profile != nil {
+ port["binding:profile"] = opts.Profile
+ }
+
+ return map[string]interface{}{"port": port}, nil
+}
+
+// Create accepts a CreateOpts struct and creates a new port with extended attributes.
+// You must remember to provide a NetworkID value.
+func Create(c *gophercloud.ServiceClient, opts ports.CreateOptsBuilder) CreateResult {
+ var res CreateResult
+
+ reqBody, err := opts.ToPortCreateMap()
+ if err != nil {
+ res.Err = err
+ return res
+ }
+
+ _, res.Err = c.Post(createURL(c), reqBody, &res.Body, nil)
+ return res
+}
+
+// UpdateOpts represents the attributes used when updating an existing port.
+type UpdateOpts struct {
+ ports.UpdateOptsBuilder
+ HostID string
+ VNICType string
+ Profile map[string]string
+}
+
+// ToPortUpdateMap casts an UpdateOpts struct to a map.
+func (opts UpdateOpts) ToPortUpdateMap() (map[string]interface{}, error) {
+ p, err := opts.UpdateOptsBuilder.ToPortUpdateMap()
+ if err != nil {
+ return nil, err
+ }
+
+ port := p["port"].(map[string]interface{})
+
+ if opts.HostID != "" {
+ port["binding:host_id"] = opts.HostID
+ }
+ if opts.VNICType != "" {
+ port["binding:vnic_type"] = opts.VNICType
+ }
+ if opts.Profile != nil {
+ port["binding:profile"] = opts.Profile
+ }
+
+ return map[string]interface{}{"port": port}, nil
+}
+
+// Update accepts a UpdateOpts struct and updates an existing port using the
+// values provided.
+func Update(c *gophercloud.ServiceClient, id string, opts ports.UpdateOptsBuilder) UpdateResult {
+ var res UpdateResult
+
+ reqBody, err := opts.ToPortUpdateMap()
+ if err != nil {
+ res.Err = err
+ return res
+ }
+
+ _, res.Err = c.Put(updateURL(c, id), reqBody, &res.Body, &gophercloud.RequestOpts{
+ OkCodes: []int{200, 201},
+ })
+ return res
+}
diff --git a/openstack/networking/v2/extensions/portsbinding/requests_test.go b/openstack/networking/v2/extensions/portsbinding/requests_test.go
new file mode 100644
index 0000000..87592a6
--- /dev/null
+++ b/openstack/networking/v2/extensions/portsbinding/requests_test.go
@@ -0,0 +1,251 @@
+package portsbinding
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ fake "github.com/rackspace/gophercloud/openstack/networking/v2/common"
+ "github.com/rackspace/gophercloud/openstack/networking/v2/ports"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestGet(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/ports/46d4bfb9-b26e-41f3-bd2e-e6dcc1ccedb2", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, `
+{
+ "port": {
+ "status": "ACTIVE",
+ "name": "",
+ "admin_state_up": true,
+ "network_id": "a87cc70a-3e15-4acf-8205-9b711a3531b7",
+ "tenant_id": "7e02058126cc4950b75f9970368ba177",
+ "device_owner": "network:router_interface",
+ "mac_address": "fa:16:3e:23:fd:d7",
+ "fixed_ips": [
+ {
+ "subnet_id": "a0304c3a-4f08-4c43-88af-d796509c97d2",
+ "ip_address": "10.0.0.1"
+ }
+ ],
+ "id": "46d4bfb9-b26e-41f3-bd2e-e6dcc1ccedb2",
+ "security_groups": [],
+ "device_id": "5e3898d7-11be-483e-9732-b2f5eccd2b2e",
+ "binding:host_id": "HOST1",
+ "binding:vnic_type": "normal"
+ }
+}
+ `)
+ })
+
+ n, err := Get(fake.ServiceClient(), "46d4bfb9-b26e-41f3-bd2e-e6dcc1ccedb2").Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, n.Status, "ACTIVE")
+ th.AssertEquals(t, n.Name, "")
+ th.AssertEquals(t, n.AdminStateUp, true)
+ th.AssertEquals(t, n.NetworkID, "a87cc70a-3e15-4acf-8205-9b711a3531b7")
+ th.AssertEquals(t, n.TenantID, "7e02058126cc4950b75f9970368ba177")
+ th.AssertEquals(t, n.DeviceOwner, "network:router_interface")
+ th.AssertEquals(t, n.MACAddress, "fa:16:3e:23:fd:d7")
+ th.AssertDeepEquals(t, n.FixedIPs, []IP{
+ {SubnetID: "a0304c3a-4f08-4c43-88af-d796509c97d2", IPAddress: "10.0.0.1"},
+ })
+ th.AssertEquals(t, n.ID, "46d4bfb9-b26e-41f3-bd2e-e6dcc1ccedb2")
+ th.AssertDeepEquals(t, n.SecurityGroups, []string{})
+ th.AssertEquals(t, n.DeviceID, "5e3898d7-11be-483e-9732-b2f5eccd2b2e")
+ th.AssertEquals(t, n.HostID, "HOST1")
+ th.AssertEquals(t, n.VNICType, "normal")
+}
+
+func TestCreate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/ports", 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, `
+{
+ "port": {
+ "network_id": "a87cc70a-3e15-4acf-8205-9b711a3531b7",
+ "name": "private-port",
+ "admin_state_up": true,
+ "fixed_ips": [
+ {
+ "subnet_id": "a0304c3a-4f08-4c43-88af-d796509c97d2",
+ "ip_address": "10.0.0.2"
+ }
+ ],
+ "security_groups": ["foo"],
+ "binding:host_id": "HOST1",
+ "binding:vnic_type": "normal"
+ }
+}
+ `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusCreated)
+
+ fmt.Fprintf(w, `
+{
+ "port": {
+ "status": "DOWN",
+ "name": "private-port",
+ "allowed_address_pairs": [],
+ "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.2"
+ }
+ ],
+ "binding:host_id": "HOST1",
+ "binding:vnic_type": "normal",
+ "id": "65c0ee9f-d634-4522-8954-51021b570b0d",
+ "security_groups": [
+ "f0ac4394-7e4a-4409-9701-ba8be283dbc3"
+ ],
+ "device_id": ""
+ }
+}
+ `)
+ })
+
+ asu := true
+ options := CreateOpts{
+ CreateOptsBuilder: ports.CreateOpts{
+ Name: "private-port",
+ AdminStateUp: &asu,
+ NetworkID: "a87cc70a-3e15-4acf-8205-9b711a3531b7",
+ FixedIPs: []IP{
+ {SubnetID: "a0304c3a-4f08-4c43-88af-d796509c97d2", IPAddress: "10.0.0.2"},
+ },
+ SecurityGroups: []string{"foo"},
+ },
+ HostID: "HOST1",
+ VNICType: "normal",
+ }
+ n, err := Create(fake.ServiceClient(), options).Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, n.Status, "DOWN")
+ th.AssertEquals(t, n.Name, "private-port")
+ th.AssertEquals(t, n.AdminStateUp, true)
+ th.AssertEquals(t, n.NetworkID, "a87cc70a-3e15-4acf-8205-9b711a3531b7")
+ th.AssertEquals(t, n.TenantID, "d6700c0c9ffa4f1cb322cd4a1f3906fa")
+ th.AssertEquals(t, n.DeviceOwner, "")
+ th.AssertEquals(t, n.MACAddress, "fa:16:3e:c9:cb:f0")
+ th.AssertDeepEquals(t, n.FixedIPs, []IP{
+ {SubnetID: "a0304c3a-4f08-4c43-88af-d796509c97d2", IPAddress: "10.0.0.2"},
+ })
+ th.AssertEquals(t, n.ID, "65c0ee9f-d634-4522-8954-51021b570b0d")
+ th.AssertDeepEquals(t, n.SecurityGroups, []string{"f0ac4394-7e4a-4409-9701-ba8be283dbc3"})
+ th.AssertEquals(t, n.HostID, "HOST1")
+ th.AssertEquals(t, n.VNICType, "normal")
+}
+
+func TestRequiredCreateOpts(t *testing.T) {
+ res := Create(fake.ServiceClient(), CreateOpts{CreateOptsBuilder: ports.CreateOpts{}})
+ if res.Err == nil {
+ t.Fatalf("Expected error, got none")
+ }
+}
+
+func TestUpdate(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"
+ }
+ ],
+ "security_groups": [
+ "f0ac4394-7e4a-4409-9701-ba8be283dbc3"
+ ],
+ "binding:host_id": "HOST1",
+ "binding:vnic_type": "normal"
+ }
+}
+ `)
+
+ 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": "",
+ "binding:host_id": "HOST1",
+ "binding:vnic_type": "normal"
+ }
+}
+ `)
+ })
+
+ options := UpdateOpts{
+ UpdateOptsBuilder: ports.UpdateOpts{
+ Name: "new_port_name",
+ FixedIPs: []IP{
+ {SubnetID: "a0304c3a-4f08-4c43-88af-d796509c97d2", IPAddress: "10.0.0.3"},
+ },
+ SecurityGroups: []string{"f0ac4394-7e4a-4409-9701-ba8be283dbc3"},
+ },
+ HostID: "HOST1",
+ VNICType: "normal",
+ }
+
+ s, err := 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, []IP{
+ {SubnetID: "a0304c3a-4f08-4c43-88af-d796509c97d2", IPAddress: "10.0.0.3"},
+ })
+ th.AssertDeepEquals(t, s.SecurityGroups, []string{"f0ac4394-7e4a-4409-9701-ba8be283dbc3"})
+ th.AssertEquals(t, s.HostID, "HOST1")
+ th.AssertEquals(t, s.VNICType, "normal")
+}
diff --git a/openstack/networking/v2/extensions/portsbinding/results.go b/openstack/networking/v2/extensions/portsbinding/results.go
new file mode 100644
index 0000000..57379eb
--- /dev/null
+++ b/openstack/networking/v2/extensions/portsbinding/results.go
@@ -0,0 +1,84 @@
+package portsbinding
+
+import (
+ "github.com/mitchellh/mapstructure"
+ "github.com/rackspace/gophercloud"
+)
+
+type commonResult struct {
+ gophercloud.Result
+}
+
+// Extract is a function that accepts a result and extracts a port resource.
+func (r commonResult) Extract() (*Port, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var res struct {
+ Port *Port `json:"port"`
+ }
+
+ err := mapstructure.Decode(r.Body, &res)
+
+ return res.Port, err
+}
+
+// CreateResult represents the result of a create operation.
+type CreateResult struct {
+ commonResult
+}
+
+// GetResult represents the result of a get operation.
+type GetResult struct {
+ commonResult
+}
+
+// UpdateResult represents the result of an update operation.
+type UpdateResult struct {
+ commonResult
+}
+
+// IP is a sub-struct that represents an individual IP.
+type IP struct {
+ SubnetID string `mapstructure:"subnet_id" json:"subnet_id"`
+ IPAddress string `mapstructure:"ip_address" json:"ip_address,omitempty"`
+}
+
+// Port represents a Neutron port. See package documentation for a top-level
+// description of what this is.
+type Port struct {
+ // UUID for the port.
+ ID string `mapstructure:"id" json:"id"`
+ // Network that this port is associated with.
+ NetworkID string `mapstructure:"network_id" json:"network_id"`
+ // Human-readable name for the port. Might not be unique.
+ Name string `mapstructure:"name" json:"name"`
+ // Administrative state of port. If false (down), port does not forward packets.
+ AdminStateUp bool `mapstructure:"admin_state_up" json:"admin_state_up"`
+ // Indicates whether network is currently operational. Possible values include
+ // `ACTIVE', `DOWN', `BUILD', or `ERROR'. Plug-ins might define additional values.
+ Status string `mapstructure:"status" json:"status"`
+ // Mac address to use on this port.
+ MACAddress string `mapstructure:"mac_address" json:"mac_address"`
+ // Specifies IP addresses for the port thus associating the port itself with
+ // the subnets where the IP addresses are picked from
+ FixedIPs []IP `mapstructure:"fixed_ips" json:"fixed_ips"`
+ // Owner of network. Only admin users can specify a tenant_id other than its own.
+ TenantID string `mapstructure:"tenant_id" json:"tenant_id"`
+ // Identifies the entity (e.g.: dhcp agent) using this port.
+ DeviceOwner string `mapstructure:"device_owner" json:"device_owner"`
+ // Specifies the IDs of any security groups associated with a port.
+ SecurityGroups []string `mapstructure:"security_groups" json:"security_groups"`
+ // Identifies the device (e.g., virtual server) using this port.
+ DeviceID string `mapstructure:"device_id" json:"device_id"`
+ // The ID of the host where the port is allocated
+ HostID string `mapstructure:"binding:host_id" json:"binding:host_id"`
+ // The virtual network interface card (vNIC) type that is bound to the
+ // neutron port
+ VNICType string `mapstructure:"binding:vnic_type" json:"binding:vnic_type"`
+ // A dictionary that enables the application running on the specified
+ // host to pass and receive virtual network interface (VIF) port-specific
+ // information to the plug-in
+ Profile map[string]string `mapstructure:"binding:profile" json:"binding:profile"`
+}
diff --git a/openstack/networking/v2/extensions/portsbinding/urls.go b/openstack/networking/v2/extensions/portsbinding/urls.go
new file mode 100644
index 0000000..55307f4
--- /dev/null
+++ b/openstack/networking/v2/extensions/portsbinding/urls.go
@@ -0,0 +1,23 @@
+package portsbinding
+
+import "github.com/rackspace/gophercloud"
+
+func resourceURL(c *gophercloud.ServiceClient, id string) string {
+ return c.ServiceURL("ports", id)
+}
+
+func rootURL(c *gophercloud.ServiceClient) string {
+ return c.ServiceURL("ports")
+}
+
+func getURL(c *gophercloud.ServiceClient, id string) string {
+ return resourceURL(c, id)
+}
+
+func createURL(c *gophercloud.ServiceClient) string {
+ return rootURL(c)
+}
+
+func updateURL(c *gophercloud.ServiceClient, id string) string {
+ return resourceURL(c, id)
+}
diff --git a/openstack/networking/v2/extensions/portsbinding/urls_test.go b/openstack/networking/v2/extensions/portsbinding/urls_test.go
new file mode 100644
index 0000000..f9359ce
--- /dev/null
+++ b/openstack/networking/v2/extensions/portsbinding/urls_test.go
@@ -0,0 +1,32 @@
+package portsbinding
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+const endpoint = "http://localhost:57909/"
+
+func endpointClient() *gophercloud.ServiceClient {
+ return &gophercloud.ServiceClient{Endpoint: endpoint, ResourceBase: endpoint + "v2.0/"}
+}
+
+func TestGetURL(t *testing.T) {
+ actual := getURL(endpointClient(), "foo")
+ expected := endpoint + "v2.0/ports/foo"
+ th.AssertEquals(t, expected, actual)
+}
+
+func TestCreateURL(t *testing.T) {
+ actual := createURL(endpointClient())
+ expected := endpoint + "v2.0/ports"
+ th.AssertEquals(t, expected, actual)
+}
+
+func TestUpdateURL(t *testing.T) {
+ actual := updateURL(endpointClient(), "foo")
+ expected := endpoint + "v2.0/ports/foo"
+ th.AssertEquals(t, expected, actual)
+}