From 5f5b7dac9e5a7291e74dd2195f9a22f79d79d4d2 Mon Sep 17 00:00:00 2001 From: jrperritt Date: Mon, 7 Sep 2015 22:13:25 -0600 Subject: [PATCH 1/6] shared ips extension --- rackspace/networking/v2/sharedips/fixtures.go | 217 ++++++++++++++++ rackspace/networking/v2/sharedips/requests.go | 230 +++++++++++++++++ .../networking/v2/sharedips/requests_test.go | 243 ++++++++++++++++++ rackspace/networking/v2/sharedips/results.go | 152 +++++++++++ rackspace/networking/v2/sharedips/urls.go | 47 ++++ 5 files changed, 889 insertions(+) create mode 100644 rackspace/networking/v2/sharedips/fixtures.go create mode 100644 rackspace/networking/v2/sharedips/requests.go create mode 100644 rackspace/networking/v2/sharedips/requests_test.go create mode 100644 rackspace/networking/v2/sharedips/results.go create mode 100644 rackspace/networking/v2/sharedips/urls.go diff --git a/rackspace/networking/v2/sharedips/fixtures.go b/rackspace/networking/v2/sharedips/fixtures.go new file mode 100644 index 00000000..d9f8db3f --- /dev/null +++ b/rackspace/networking/v2/sharedips/fixtures.go @@ -0,0 +1,217 @@ +package sharedips + +import ( + "fmt" + "net/http" + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +func MockListResponse(t *testing.T) { + th.Mux.HandleFunc("/ip_addresses", 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, ` + { + "ip_addresses": [ + { + "id": "4cacd68e-d7aa-4ff2-96f4-5c6f57dba737", + "network_id": "6870304a-7212-443f-bd0c-089c886b44df", + "address": "192.168.10.1", + "port_ids": [ + "2f693cca-7383-45da-8bae-d26b6c2d6718" + ], + "subnet_id": "f11687e8-ef0d-4207-8e22-c60e737e473b", + "tenant_id": "2345678", + "version": "4", + "type": "fixed" + } + ] + } + `) + }) +} + +func MockGetResponse(t *testing.T) { + th.Mux.HandleFunc("/ip_addresses/4cacd68e-d7aa-4ff2-96f4-5c6f57dba737", 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, ` + { + "ip_address": + { + "id": "4cacd68e-d7aa-4ff2-96f4-5c6f57dba737", + "network_id": "fda61e0b-a410-49e8-ad3a-64c595618c7e", + "address": "192.168.10.1", + "port_ids": ["6200d533-a42b-4c04-82a1-cc14dbdbf2de", + "9d0db2d7-62df-4c99-80cb-6f140a5260e8"], + "subnet_id": "f11687e8-ef0d-4207-8e22-c60e737e473b", + "tenant_id": "2345678", + "version": "4", + "type": "shared" + } + } + `) + }) +} + +func MockCreateResponse(t *testing.T) { + th.Mux.HandleFunc("/ip_addresses", 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, ` + { + "ip_address": { + "network_id": "00000000-0000-0000-0000-000000000000", + "version": 4, + "port_ids": [ + "6200d533-a42b-4c04-82a1-cc14dbdbf2de", + "9d0db2d7-62df-4c99-80cb-6f140a5260e8" + ], + "tenant_id": "2345678" + } + } + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, ` + { + "ip_address": + { + "id": "4cacd68e-d7aa-4ff2-96f4-5c6f57dba737", + "network_id": "fda61e0b-a410-49e8-ad3a-64c595618c7e", + "address": "192.168.10.1", + "port_ids": ["6200d533-a42b-4c04-82a1-cc14dbdbf2de", + "9d0db2d7-62df-4c99-80cb-6f140a5260e8"], + "subnet_id": "f11687e8-ef0d-4207-8e22-c60e737e473b", + "tenant_id": "2345678", + "version": "4", + "type": "shared" + } + } + `) + }) +} + +func MockDeleteResponse(t *testing.T) { + th.Mux.HandleFunc("/ip_addresses/4cacd68e-d7aa-4ff2-96f4-5c6f57dba737", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusNoContent) + }) +} + +func MockUpdateResponse(t *testing.T) { + th.Mux.HandleFunc("/ip_addresses/4cacd68e-d7aa-4ff2-96f4-5c6f57dba737", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestJSONRequest(t, r, ` + { + "ip_address": { + "port_ids": ["275b0516-206f-4421-8e42-1d3d1e4e9fb2", "66811c0a-fdbd-49d4-b1dd-f0f15a329744"] + } +} + `) + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, ` + { + "ip_address": + { + "id": "4cacd68e-d7aa-4ff2-96f4-5c6f57dba737", + "network_id": "6870304a-7212-443f-bd0c-089c886b44df", + "address": "192.168.10.1", + "port_ids": ["275b0516-206f-4421-8e42-1d3d1e4e9fb2", + "66811c0a-fdbd-49d4-b1dd-f0f15a329744"], + "subnet_id": "f11687e8-ef0d-4207-8e22-c60e737e473b", + "tenant_id": "2345678", + "version": "4", + "type": "shared" + } + } + `) + }) +} + +func MockListByServerResponse(t *testing.T) { + th.Mux.HandleFunc("/servers/123456/ip_associations", 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, ` + { + "ip_associations": + [ + { + "id": "1", + "address": "10.1.1.1" + }, + { + "id": "2", + "address": "10.1.1.2" + } + ] + } + `) + }) +} + +func MockGetByServerResponse(t *testing.T) { + th.Mux.HandleFunc("/servers/123456/ip_associations/1", 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, ` + { + "ip_association": + { + "id": "1", + "address": "10.1.1.1" + } +} + `) + }) +} + +func MockAssociateResponse(t *testing.T) { + th.Mux.HandleFunc("/servers/123456/ip_associations/2", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, ` + { + "ip_association": + { + "id": "2", + "address": "10.1.1.2" + } + } + `) + }) +} + +func MockDisassociateResponse(t *testing.T) { + th.Mux.HandleFunc("/servers/123456/ip_associations/2", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusNoContent) + }) +} diff --git a/rackspace/networking/v2/sharedips/requests.go b/rackspace/networking/v2/sharedips/requests.go new file mode 100644 index 00000000..d4fd9004 --- /dev/null +++ b/rackspace/networking/v2/sharedips/requests.go @@ -0,0 +1,230 @@ +package sharedips + +import ( + "fmt" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToSharedIPListQuery() (string, error) +} + +// ListOpts are options for listing shared IPs. Though currently empty, having +// it lets us add fields later should they get added to the API. +type ListOpts struct{} + +// ToSharedIPListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToSharedIPListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + if err != nil { + return "", err + } + return q.String(), nil +} + +// List returns a Pager which allows you to iterate over a collection of +// shared IPs. +func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(c) + if opts != nil { + query, err := opts.ToSharedIPListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return IPAddressPage{pagination.SinglePageBase(r)} + }) +} + +// CreateOptsBuilder allows extensions to add additional parameters to the +// Create request. +type CreateOptsBuilder interface { + ToSharedIPCreateMap() (map[string]interface{}, error) +} + +// CreateOpts contains options for provisioning a shared IP address. This object is passed to +// the sharedips.Create function. +type CreateOpts struct { + // REQUIRED + NetworkID string + // REQUIRED + PortIDs []string + // REQUIRED + Version int + // REQUIRED + TenantID string +} + +// ToSharedIPCreateMap assembles a request body based on the contents of a +// CreateOpts. +func (opts CreateOpts) ToSharedIPCreateMap() (map[string]interface{}, error) { + v := make(map[string]interface{}) + + if opts.NetworkID == "" { + return nil, fmt.Errorf("Required CreateOpts field 'NetworkID' not set.") + } + v["network_id"] = opts.NetworkID + + if len(opts.PortIDs) < 2 { + return nil, fmt.Errorf("Required CreateOpts field 'PortIDs' must contain at least 2 port IDs.") + } + v["port_ids"] = opts.PortIDs + + if opts.Version != 4 && opts.Version != 6 { + return nil, fmt.Errorf("Required CreateOpts field 'Version' not set.") + } + v["version"] = opts.Version + + if opts.TenantID == "" { + return nil, fmt.Errorf("Required CreateOpts field 'TenantID' not set.") + } + v["tenant_id"] = opts.TenantID + + return map[string]interface{}{"ip_address": v}, nil +} + +// Create will provision a new shapred IP address based on the values in CreateOpts. To extract +// the IPAddress object from the response, call the Extract method on the +// CreateResult. +func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult { + var res CreateResult + + reqBody, err := opts.ToSharedIPCreateMap() + if err != nil { + res.Err = err + return res + } + + _, res.Err = client.Post(createURL(client), reqBody, &res.Body, &gophercloud.RequestOpts{ + OkCodes: []int{201}, + }) + return res +} + +// Delete will deallocate the existing shared IP with the provided ID from the tenant. +// Before using this operation, all IP associations must be removed from the IP address +// by using the Disassociate function. +func Delete(client *gophercloud.ServiceClient, id string) DeleteResult { + var res DeleteResult + _, res.Err = client.Delete(deleteURL(client, id), nil) + return res +} + +// Get retrieves the shared IP with the provided ID. To extract the IPAddress object +// from the response, call the Extract method on the GetResult. +func Get(client *gophercloud.ServiceClient, id string) GetResult { + var res GetResult + _, res.Err = client.Get(getURL(client, id), &res.Body, nil) + return res +} + +// UpdateOptsBuilder allows extensions to add additional parameters to the +// Update request. +type UpdateOptsBuilder interface { + ToSharedIPUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts contain options for updating an existing shared IP. This object is passed +// to the sharedips.Update function. +type UpdateOpts struct { + // REQUIRED + PortIDs []string +} + +// ToSharedIPUpdateMap assembles a request body based on the contents of an +// UpdateOpts. +func (opts UpdateOpts) ToSharedIPUpdateMap() (map[string]interface{}, error) { + v := make(map[string]interface{}) + + if len(opts.PortIDs) < 2 { + return nil, fmt.Errorf("Required CreateOpts field 'PortIDs' must contain at least 2 port IDs.") + } + v["port_ids"] = opts.PortIDs + + return map[string]interface{}{"ip_address": v}, nil +} + +// Update will update the shared IP with provided information. To extract the updated +// IPAddress from the response, call the Extract method on the UpdateResult. +func Update(client *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) UpdateResult { + var res UpdateResult + + reqBody, err := opts.ToSharedIPUpdateMap() + if err != nil { + res.Err = err + return res + } + + _, res.Err = client.Put(updateURL(client, id), reqBody, &res.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + return res +} + +// ListByServerOptsBuilder allows extensions to add additional parameters to the +// ListByServer request. +type ListByServerOptsBuilder interface { + ToSharedIPListByServerQuery() (string, error) +} + +// ListByServerOpts are options for listing the shared IPs for a server. Though currently empty, having +// it lets us add fields later should they get added to the API. +type ListByServerOpts struct{} + +// ToSharedIPListByServerQuery formats a ListByServerOpts into a query string. +func (opts ListOpts) ToSharedIPListByServerQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + if err != nil { + return "", err + } + return q.String(), nil +} + +// ListByServer returns a Pager which allows you to iterate over a collection of +// IPAssociations. +func ListByServer(c *gophercloud.ServiceClient, serverID string, opts ListByServerOptsBuilder) pagination.Pager { + url := listByServerURL(c, serverID) + if opts != nil { + query, err := opts.ToSharedIPListByServerQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return IPAssociationPage{pagination.SinglePageBase(r)} + }) +} + +// Associate will associate the server with a shared IP address. +func Associate(client *gophercloud.ServiceClient, serverID, sharedIPID string) AssociateResult { + var res AssociateResult + + _, res.Err = client.Post(associateURL(client, serverID, sharedIPID), nil, &res.Body, &gophercloud.RequestOpts{ + OkCodes: []int{201}, + }) + return res +} + +// Disassociate will delete the association between the server and the IP address. +func Disassociate(client *gophercloud.ServiceClient, serverID, sharedIPID string) DisassociateResult { + var res DisassociateResult + _, res.Err = client.Delete(disassociateURL(client, serverID, sharedIPID), nil) + return res +} + +// GetByServer retrieves the shared IP with the provided ID and server ID. To extract +// the IPAssociation object from the response, call the Extract method on the GetByServerResult. +func GetByServer(client *gophercloud.ServiceClient, serverID, sharedIPID string) GetByServerResult { + var res GetByServerResult + _, res.Err = client.Get(getByServerURL(client, serverID, sharedIPID), &res.Body, nil) + return res +} diff --git a/rackspace/networking/v2/sharedips/requests_test.go b/rackspace/networking/v2/sharedips/requests_test.go new file mode 100644 index 00000000..037b19fb --- /dev/null +++ b/rackspace/networking/v2/sharedips/requests_test.go @@ -0,0 +1,243 @@ +package sharedips + +import ( + "testing" + + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockListResponse(t) + + count := 0 + err := List(client.ServiceClient(), &ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractIPAddresses(page) + if err != nil { + t.Errorf("Failed to extract IP addresses: %v", err) + return false, err + } + + expected := []IPAddress{ + IPAddress{ + ID: "4cacd68e-d7aa-4ff2-96f4-5c6f57dba737", + NetworkID: "6870304a-7212-443f-bd0c-089c886b44df", + Address: "192.168.10.1", + PortIDs: []string{"2f693cca-7383-45da-8bae-d26b6c2d6718"}, + SubnetID: "f11687e8-ef0d-4207-8e22-c60e737e473b", + TenantID: "2345678", + Version: 4, + Type: "fixed", + }, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, 1, count) +} + +func TestListAll(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockListResponse(t) + + allPages, err := List(client.ServiceClient(), &ListOpts{}).AllPages() + th.AssertNoErr(t, err) + actual, err := ExtractIPAddresses(allPages) + th.AssertNoErr(t, err) + + expected := []IPAddress{ + IPAddress{ + ID: "4cacd68e-d7aa-4ff2-96f4-5c6f57dba737", + NetworkID: "6870304a-7212-443f-bd0c-089c886b44df", + Address: "192.168.10.1", + PortIDs: []string{"2f693cca-7383-45da-8bae-d26b6c2d6718"}, + SubnetID: "f11687e8-ef0d-4207-8e22-c60e737e473b", + TenantID: "2345678", + Version: 4, + Type: "fixed", + }, + } + + th.CheckDeepEquals(t, expected, actual) +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockGetResponse(t) + + actual, err := Get(client.ServiceClient(), "4cacd68e-d7aa-4ff2-96f4-5c6f57dba737").Extract() + th.AssertNoErr(t, err) + + expected := &IPAddress{ + ID: "4cacd68e-d7aa-4ff2-96f4-5c6f57dba737", + NetworkID: "fda61e0b-a410-49e8-ad3a-64c595618c7e", + Address: "192.168.10.1", + PortIDs: []string{"6200d533-a42b-4c04-82a1-cc14dbdbf2de", + "9d0db2d7-62df-4c99-80cb-6f140a5260e8"}, + SubnetID: "f11687e8-ef0d-4207-8e22-c60e737e473b", + TenantID: "2345678", + Version: 4, + Type: "shared", + } + + th.CheckDeepEquals(t, expected, actual) +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockCreateResponse(t) + + options := &CreateOpts{ + NetworkID: "00000000-0000-0000-0000-000000000000", + Version: 4, + PortIDs: []string{ + "6200d533-a42b-4c04-82a1-cc14dbdbf2de", + "9d0db2d7-62df-4c99-80cb-6f140a5260e8", + }, + TenantID: "2345678", + } + actual, err := Create(client.ServiceClient(), options).Extract() + th.AssertNoErr(t, err) + + expected := &IPAddress{ + ID: "4cacd68e-d7aa-4ff2-96f4-5c6f57dba737", + NetworkID: "fda61e0b-a410-49e8-ad3a-64c595618c7e", + Address: "192.168.10.1", + PortIDs: []string{"6200d533-a42b-4c04-82a1-cc14dbdbf2de", + "9d0db2d7-62df-4c99-80cb-6f140a5260e8"}, + SubnetID: "f11687e8-ef0d-4207-8e22-c60e737e473b", + TenantID: "2345678", + Version: 4, + Type: "shared", + } + + th.CheckDeepEquals(t, expected, actual) +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockDeleteResponse(t) + + res := Delete(client.ServiceClient(), "4cacd68e-d7aa-4ff2-96f4-5c6f57dba737") + th.AssertNoErr(t, res.Err) +} + +func TestUpdate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockUpdateResponse(t) + + options := &UpdateOpts{ + PortIDs: []string{"275b0516-206f-4421-8e42-1d3d1e4e9fb2", "66811c0a-fdbd-49d4-b1dd-f0f15a329744"}, + } + actual, err := Update(client.ServiceClient(), "4cacd68e-d7aa-4ff2-96f4-5c6f57dba737", options).Extract() + th.AssertNoErr(t, err) + + expected := &IPAddress{ + ID: "4cacd68e-d7aa-4ff2-96f4-5c6f57dba737", + NetworkID: "6870304a-7212-443f-bd0c-089c886b44df", + Address: "192.168.10.1", + PortIDs: []string{"275b0516-206f-4421-8e42-1d3d1e4e9fb2", + "66811c0a-fdbd-49d4-b1dd-f0f15a329744"}, + SubnetID: "f11687e8-ef0d-4207-8e22-c60e737e473b", + TenantID: "2345678", + Version: 4, + Type: "shared", + } + th.CheckDeepEquals(t, expected, actual) +} + +func TestListByServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockListByServerResponse(t) + + count := 0 + err := ListByServer(client.ServiceClient(), "123456", &ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractIPAssociations(page) + if err != nil { + t.Errorf("Failed to extract IP associations: %v", err) + return false, err + } + + expected := []IPAssociation{ + IPAssociation{ + ID: "1", + Address: "10.1.1.1", + }, + IPAssociation{ + ID: "2", + Address: "10.1.1.2", + }, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, 1, count) +} + +func TestGetByServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockGetByServerResponse(t) + + actual, err := GetByServer(client.ServiceClient(), "123456", "1").Extract() + th.AssertNoErr(t, err) + + expected := &IPAssociation{ + ID: "1", + Address: "10.1.1.1", + } + + th.CheckDeepEquals(t, expected, actual) +} + +func TestAssociate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockAssociateResponse(t) + + actual, err := Associate(client.ServiceClient(), "123456", "2").Extract() + th.AssertNoErr(t, err) + + expected := &IPAssociation{ + ID: "2", + Address: "10.1.1.2", + } + + th.CheckDeepEquals(t, expected, actual) +} + +func TestDisassociate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockDisassociateResponse(t) + + res := Disassociate(client.ServiceClient(), "123456", "2") + th.AssertNoErr(t, res.Err) +} diff --git a/rackspace/networking/v2/sharedips/results.go b/rackspace/networking/v2/sharedips/results.go new file mode 100644 index 00000000..f5e4a33d --- /dev/null +++ b/rackspace/networking/v2/sharedips/results.go @@ -0,0 +1,152 @@ +package sharedips + +import ( + "github.com/mitchellh/mapstructure" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// IPAddress represents a shared IP address. +type IPAddress struct { + ID string `mapstructure:"id"` + NetworkID string `mapstructure:"network_id"` + Address string `mapstructure:"address"` + PortIDs []string `mapstructure:"port_ids"` + SubnetID string `mapstructure:"subnet_id"` + TenantID string `mapstructure:"tenant_id"` + Version int `mapstructure:"version"` + Type string `mapstructure:"type"` +} + +// IPAddressPage is the page returned by a pager when traversing over a +// collection of shared IP addresses. +type IPAddressPage struct { + pagination.SinglePageBase +} + +// IsEmpty checks whether an IPAddressPage struct is empty. +func (p IPAddressPage) IsEmpty() (bool, error) { + is, err := ExtractIPAddresses(p) + if err != nil { + return true, nil + } + return len(is) == 0, nil +} + +// ExtractIPAddresses accepts a Page struct, specifically an IPAddressPage struct, +// and extracts the elements into a slice of IPAddress structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractIPAddresses(page pagination.Page) ([]IPAddress, error) { + var resp struct { + IPAddresses []IPAddress `mapstructure:"ip_addresses"` + } + err := mapstructure.WeakDecode(page.(IPAddressPage).Body, &resp) + + return resp.IPAddresses, err +} + +type commonIPAddressResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts a shared IP resource. +func (r commonIPAddressResult) Extract() (*IPAddress, error) { + if r.Err != nil { + return nil, r.Err + } + + var res struct { + IPAddress *IPAddress `mapstructure:"ip_address"` + } + + err := mapstructure.WeakDecode(r.Body, &res) + + return res.IPAddress, err +} + +// CreateResult represents the result of a create operation. +type CreateResult struct { + commonIPAddressResult +} + +// GetResult represents the result of a get operation. +type GetResult struct { + commonIPAddressResult +} + +// UpdateResult represents the result of an update operation. +type UpdateResult struct { + commonIPAddressResult +} + +// DeleteResult represents the result of a delete operation. +type DeleteResult struct { + gophercloud.ErrResult +} + +// IPAssociation represents a shared IP address for a server. +type IPAssociation struct { + ID string `mapstructure:"id"` + Address string `mapstructure:"address"` +} + +// IPAssociationPage is the page returned by a pager when traversing over the +// collection of shared IP addresses for a server. +type IPAssociationPage struct { + pagination.SinglePageBase +} + +// IsEmpty checks whether an IPAssociationPage struct is empty. +func (p IPAssociationPage) IsEmpty() (bool, error) { + is, err := ExtractIPAssociations(p) + if err != nil { + return true, nil + } + return len(is) == 0, nil +} + +// ExtractIPAssociations accepts a Page struct, specifically an IPAssociationPage struct, +// and extracts the elements into a slice of IPAssociation structs. +func ExtractIPAssociations(page pagination.Page) ([]IPAssociation, error) { + var resp struct { + IPAssociations []IPAssociation `mapstructure:"ip_associations"` + } + + err := mapstructure.Decode(page.(IPAssociationPage).Body, &resp) + + return resp.IPAssociations, err +} + +type commonIPAssociationResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts a shared IP association. +func (r commonIPAssociationResult) Extract() (*IPAssociation, error) { + if r.Err != nil { + return nil, r.Err + } + + var res struct { + IPAssociation *IPAssociation `mapstructure:"ip_association"` + } + + err := mapstructure.Decode(r.Body, &res) + + return res.IPAssociation, err +} + +// AssociateResult represents the result of an Associate operation. +type AssociateResult struct { + commonIPAssociationResult +} + +// GetByServerResult represents the result of a GetByServer operation. +type GetByServerResult struct { + commonIPAssociationResult +} + +// DisassociateResult represents the result of a Disassociate operation. +type DisassociateResult struct { + gophercloud.ErrResult +} diff --git a/rackspace/networking/v2/sharedips/urls.go b/rackspace/networking/v2/sharedips/urls.go new file mode 100644 index 00000000..f250f69f --- /dev/null +++ b/rackspace/networking/v2/sharedips/urls.go @@ -0,0 +1,47 @@ +package sharedips + +import "github.com/rackspace/gophercloud" + +func resourceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL("ip_addresses", id) +} + +func rootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("ip_addresses") +} + +func listURL(c *gophercloud.ServiceClient) string { + return rootURL(c) +} + +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) +} + +func deleteURL(c *gophercloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func listByServerURL(c *gophercloud.ServiceClient, serverID string) string { + return c.ServiceURL("servers", serverID, "ip_associations") +} + +func getByServerURL(c *gophercloud.ServiceClient, serverID, sharedIPID string) string { + return c.ServiceURL("servers", serverID, "ip_associations", sharedIPID) +} + +func associateURL(c *gophercloud.ServiceClient, serverID, sharedIPID string) string { + return getByServerURL(c, serverID, sharedIPID) +} + +func disassociateURL(c *gophercloud.ServiceClient, serverID, sharedIPID string) string { + return getByServerURL(c, serverID, sharedIPID) +} From cb48c32b0b66ef961f58ae21d967b23b4d2952f6 Mon Sep 17 00:00:00 2001 From: jrperritt Date: Wed, 16 Sep 2015 11:15:44 -0600 Subject: [PATCH 2/6] add IPVersion type; rename pkg to ipaddresses --- params.go | 7 +++++++ .../v2/{sharedips => ipaddresses}/fixtures.go | 0 .../v2/{sharedips => ipaddresses}/requests.go | 2 +- .../{sharedips => ipaddresses}/requests_test.go | 0 .../v2/{sharedips => ipaddresses}/results.go | 16 ++++++++-------- .../v2/{sharedips => ipaddresses}/urls.go | 0 6 files changed, 16 insertions(+), 9 deletions(-) rename rackspace/networking/v2/{sharedips => ipaddresses}/fixtures.go (100%) rename rackspace/networking/v2/{sharedips => ipaddresses}/requests.go (99%) rename rackspace/networking/v2/{sharedips => ipaddresses}/requests_test.go (100%) rename rackspace/networking/v2/{sharedips => ipaddresses}/results.go (88%) rename rackspace/networking/v2/{sharedips => ipaddresses}/urls.go (100%) diff --git a/params.go b/params.go index 4d0f1e6e..67e0400a 100644 --- a/params.go +++ b/params.go @@ -9,6 +9,13 @@ import ( "time" ) +type IPVersion int + +const ( + IPv4 IPVersion = 4 + IPv6 IPVersion = 6 +) + // EnabledState is a convenience type, mostly used in Create and Update // operations. Because the zero value of a bool is FALSE, we need to use a // pointer instead to indicate zero-ness. diff --git a/rackspace/networking/v2/sharedips/fixtures.go b/rackspace/networking/v2/ipaddresses/fixtures.go similarity index 100% rename from rackspace/networking/v2/sharedips/fixtures.go rename to rackspace/networking/v2/ipaddresses/fixtures.go diff --git a/rackspace/networking/v2/sharedips/requests.go b/rackspace/networking/v2/ipaddresses/requests.go similarity index 99% rename from rackspace/networking/v2/sharedips/requests.go rename to rackspace/networking/v2/ipaddresses/requests.go index d4fd9004..2e69009a 100644 --- a/rackspace/networking/v2/sharedips/requests.go +++ b/rackspace/networking/v2/ipaddresses/requests.go @@ -57,7 +57,7 @@ type CreateOpts struct { // REQUIRED PortIDs []string // REQUIRED - Version int + Version gophercloud.IPVersion // REQUIRED TenantID string } diff --git a/rackspace/networking/v2/sharedips/requests_test.go b/rackspace/networking/v2/ipaddresses/requests_test.go similarity index 100% rename from rackspace/networking/v2/sharedips/requests_test.go rename to rackspace/networking/v2/ipaddresses/requests_test.go diff --git a/rackspace/networking/v2/sharedips/results.go b/rackspace/networking/v2/ipaddresses/results.go similarity index 88% rename from rackspace/networking/v2/sharedips/results.go rename to rackspace/networking/v2/ipaddresses/results.go index f5e4a33d..da708ae8 100644 --- a/rackspace/networking/v2/sharedips/results.go +++ b/rackspace/networking/v2/ipaddresses/results.go @@ -8,14 +8,14 @@ import ( // IPAddress represents a shared IP address. type IPAddress struct { - ID string `mapstructure:"id"` - NetworkID string `mapstructure:"network_id"` - Address string `mapstructure:"address"` - PortIDs []string `mapstructure:"port_ids"` - SubnetID string `mapstructure:"subnet_id"` - TenantID string `mapstructure:"tenant_id"` - Version int `mapstructure:"version"` - Type string `mapstructure:"type"` + ID string `mapstructure:"id"` + NetworkID string `mapstructure:"network_id"` + Address string `mapstructure:"address"` + PortIDs []string `mapstructure:"port_ids"` + SubnetID string `mapstructure:"subnet_id"` + TenantID string `mapstructure:"tenant_id"` + Version gophercloud.IPVersion `mapstructure:"version"` + Type string `mapstructure:"type"` } // IPAddressPage is the page returned by a pager when traversing over a diff --git a/rackspace/networking/v2/sharedips/urls.go b/rackspace/networking/v2/ipaddresses/urls.go similarity index 100% rename from rackspace/networking/v2/sharedips/urls.go rename to rackspace/networking/v2/ipaddresses/urls.go From 18607b7c1610a9e8da3547c07600c5b84ff292dc Mon Sep 17 00:00:00 2001 From: jrperritt Date: Wed, 16 Sep 2015 20:00:55 -0600 Subject: [PATCH 3/6] add publicIPZoneID field to rs Server; reorg some servers ops to make it work --- rackspace/compute/v2/servers/delegate.go | 33 +---- rackspace/compute/v2/servers/fixtures.go | 16 +- rackspace/compute/v2/servers/requests.go | 111 ++++++++++++++ rackspace/compute/v2/servers/results.go | 179 +++++++++++++++++++++++ rackspace/compute/v2/servers/urls.go | 31 ++++ 5 files changed, 328 insertions(+), 42 deletions(-) create mode 100644 rackspace/compute/v2/servers/results.go create mode 100644 rackspace/compute/v2/servers/urls.go diff --git a/rackspace/compute/v2/servers/delegate.go b/rackspace/compute/v2/servers/delegate.go index 7810d156..0a42592d 100644 --- a/rackspace/compute/v2/servers/delegate.go +++ b/rackspace/compute/v2/servers/delegate.go @@ -6,31 +6,11 @@ import ( "github.com/rackspace/gophercloud/pagination" ) -// List makes a request against the API to list servers accessible to you. -func List(client *gophercloud.ServiceClient, opts os.ListOptsBuilder) pagination.Pager { - return os.List(client, opts) -} - -// Create requests a server to be provisioned to the user in the current tenant. -func Create(client *gophercloud.ServiceClient, opts os.CreateOptsBuilder) os.CreateResult { - return os.Create(client, opts) -} - -// Update requests an existing server to be updated with the supplied options. -func Update(client *gophercloud.ServiceClient, id string, opts os.UpdateOptsBuilder) os.UpdateResult { - return os.Update(client, id, opts) -} - -// Delete requests that a server previously provisioned be removed from your account. +// Delete deletes an existing server instance. func Delete(client *gophercloud.ServiceClient, id string) os.DeleteResult { return os.Delete(client, id) } -// Get requests details on a single server, by ID. -func Get(client *gophercloud.ServiceClient, id string) os.GetResult { - return os.Get(client, id) -} - // ChangeAdminPassword alters the administrator or root password for a specified server. func ChangeAdminPassword(client *gophercloud.ServiceClient, id, newPassword string) os.ActionResult { return os.ChangeAdminPassword(client, id, newPassword) @@ -48,12 +28,6 @@ func Reboot(client *gophercloud.ServiceClient, id string, how os.RebootMethod) o return os.Reboot(client, id, how) } -// Rebuild will reprovision the server according to the configuration options provided in the -// RebuildOpts struct. -func Rebuild(client *gophercloud.ServiceClient, id string, opts os.RebuildOptsBuilder) os.RebuildResult { - return os.Rebuild(client, id, opts) -} - // Resize instructs the provider to change the flavor of the server. // Note that this implies rebuilding it. // Unfortunately, one cannot pass rebuild parameters to the resize function. @@ -77,11 +51,6 @@ func WaitForStatus(c *gophercloud.ServiceClient, id, status string, secs int) er return os.WaitForStatus(c, id, status, secs) } -// ExtractServers interprets the results of a single page from a List() call, producing a slice of Server entities. -func ExtractServers(page pagination.Page) ([]os.Server, error) { - return os.ExtractServers(page) -} - // ListAddresses makes a request against the API to list the servers IP addresses. func ListAddresses(client *gophercloud.ServiceClient, id string) pagination.Pager { return os.ListAddresses(client, id) diff --git a/rackspace/compute/v2/servers/fixtures.go b/rackspace/compute/v2/servers/fixtures.go index 75cccd04..15255827 100644 --- a/rackspace/compute/v2/servers/fixtures.go +++ b/rackspace/compute/v2/servers/fixtures.go @@ -2,10 +2,6 @@ package servers -import ( - os "github.com/rackspace/gophercloud/openstack/compute/v2/servers" -) - // ListOutput is the recorded output of a Rackspace servers.List request. const ListOutput = ` { @@ -308,7 +304,7 @@ const CreateOutput = ` ` // DevstackServer is the expected first result from parsing ListOutput. -var DevstackServer = os.Server{ +var DevstackServer = Server{ ID: "59818cee-bc8c-44eb-8073-673ee65105f7", Name: "devstack", TenantID: "111111", @@ -372,7 +368,7 @@ var DevstackServer = os.Server{ } // PerilServer is the expected second result from parsing ListOutput. -var PerilServer = os.Server{ +var PerilServer = Server{ ID: "25f1c7f5-e00a-4715-b354-16e24b2f4630", Name: "peril-dfw", TenantID: "111111", @@ -436,7 +432,7 @@ var PerilServer = os.Server{ } // GophercloudServer is the expected result from parsing GetOutput. -var GophercloudServer = os.Server{ +var GophercloudServer = Server{ ID: "8c65cb68-0681-4c30-bc88-6b83a8a26aee", Name: "Gophercloud-pxpGGuey", TenantID: "111111", @@ -500,7 +496,7 @@ var GophercloudServer = os.Server{ } // GophercloudUpdatedServer is the expected result from parsing UpdateOutput. -var GophercloudUpdatedServer = os.Server{ +var GophercloudUpdatedServer = Server{ ID: "8c65cb68-0681-4c30-bc88-6b83a8a26aee", Name: "test-server-updated", TenantID: "111111", @@ -564,11 +560,11 @@ var GophercloudUpdatedServer = os.Server{ } // CreatedServer is the partial Server struct that can be parsed from CreateOutput. -var CreatedServer = os.Server{ +var CreatedServer = Server{ ID: "bb63327b-6a2f-34bc-b0ef-4b6d97ea637e", AdminPass: "v7tADqbE5pr9", Links: []interface{}{}, } // ExpectedServerSlice is the collection of servers, in order, that should be parsed from ListOutput. -var ExpectedServerSlice = []os.Server{DevstackServer, PerilServer} +var ExpectedServerSlice = []Server{DevstackServer, PerilServer} diff --git a/rackspace/compute/v2/servers/requests.go b/rackspace/compute/v2/servers/requests.go index d4472a08..e46e2d9f 100644 --- a/rackspace/compute/v2/servers/requests.go +++ b/rackspace/compute/v2/servers/requests.go @@ -1,9 +1,16 @@ package servers import ( + "errors" + "fmt" + + "github.com/rackspace/gophercloud" "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/bootfromvolume" "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/diskconfig" + "github.com/rackspace/gophercloud/openstack/compute/v2/flavors" + "github.com/rackspace/gophercloud/openstack/compute/v2/images" os "github.com/rackspace/gophercloud/openstack/compute/v2/servers" + "github.com/rackspace/gophercloud/pagination" ) // CreateOpts specifies all of the options that Rackspace accepts in its Create request, including @@ -124,6 +131,90 @@ func (opts CreateOpts) ToServerCreateMap() (map[string]interface{}, error) { return res, nil } +// Create requests a server to be provisioned to the user in the current tenant. +func Create(client *gophercloud.ServiceClient, opts os.CreateOptsBuilder) CreateResult { + var res CreateResult + + reqBody, err := opts.ToServerCreateMap() + if err != nil { + res.Err = err + return res + } + + // If ImageRef isn't provided, use ImageName to ascertain the image ID. + if reqBody["server"].(map[string]interface{})["imageRef"].(string) == "" { + imageName := reqBody["server"].(map[string]interface{})["imageName"].(string) + if imageName == "" { + res.Err = errors.New("One and only one of ImageRef and ImageName must be provided.") + return res + } + imageID, err := images.IDFromName(client, imageName) + if err != nil { + res.Err = err + return res + } + reqBody["server"].(map[string]interface{})["imageRef"] = imageID + } + delete(reqBody["server"].(map[string]interface{}), "imageName") + + // If FlavorRef isn't provided, use FlavorName to ascertain the flavor ID. + if reqBody["server"].(map[string]interface{})["flavorRef"].(string) == "" { + flavorName := reqBody["server"].(map[string]interface{})["flavorName"].(string) + if flavorName == "" { + res.Err = errors.New("One and only one of FlavorRef and FlavorName must be provided.") + return res + } + flavorID, err := flavors.IDFromName(client, flavorName) + if err != nil { + res.Err = err + return res + } + reqBody["server"].(map[string]interface{})["flavorRef"] = flavorID + } + delete(reqBody["server"].(map[string]interface{}), "flavorName") + + _, res.Err = client.Post(createURL(client), reqBody, &res.Body, nil) + return res +} + +// List makes a request against the API to list servers accessible to you. +func List(client *gophercloud.ServiceClient, opts os.ListOptsBuilder) pagination.Pager { + url := listDetailURL(client) + + if opts != nil { + query, err := opts.ToServerListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + + createPageFn := func(r pagination.PageResult) pagination.Page { + return ServerPage{pagination.LinkedPageBase{PageResult: r}} + } + + return pagination.NewPager(client, url, createPageFn) +} + +// Update requests an existing server to be updated with the supplied options. +func Update(client *gophercloud.ServiceClient, id string, opts os.UpdateOptsBuilder) UpdateResult { + var result UpdateResult + reqBody := opts.ToServerUpdateMap() + _, result.Err = client.Put(updateURL(client, id), reqBody, &result.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + return result +} + +// Get requests details on a single server, by ID. +func Get(client *gophercloud.ServiceClient, id string) GetResult { + var result GetResult + _, result.Err = client.Get(getURL(client, id), &result.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200, 203}, + }) + return result +} + // RebuildOpts represents all of the configuration options used in a server rebuild operation that // are supported by Rackspace. type RebuildOpts struct { @@ -176,3 +267,23 @@ func (opts RebuildOpts) ToServerRebuildMap() (map[string]interface{}, error) { return drive.ToServerRebuildMap() } + +// Rebuild will reprovision the server according to the configuration options provided in the +// RebuildOpts struct. +func Rebuild(client *gophercloud.ServiceClient, id string, opts os.RebuildOptsBuilder) RebuildResult { + var result RebuildResult + + if id == "" { + result.Err = fmt.Errorf("ID is required") + return result + } + + reqBody, err := opts.ToServerRebuildMap() + if err != nil { + result.Err = err + return result + } + + _, result.Err = client.Post(actionURL(client, id), reqBody, &result.Body, nil) + return result +} diff --git a/rackspace/compute/v2/servers/results.go b/rackspace/compute/v2/servers/results.go new file mode 100644 index 00000000..28fd78d8 --- /dev/null +++ b/rackspace/compute/v2/servers/results.go @@ -0,0 +1,179 @@ +package servers + +import ( + "reflect" + + "github.com/mitchellh/mapstructure" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// Server represents a Rackspace server on the user's account. +type Server struct { + // ID uniquely identifies this server amongst all other servers, including those not accessible to the current tenant. + ID string + + // TenantID identifies the tenant owning this server resource. + TenantID string `mapstructure:"tenant_id"` + + // UserID uniquely identifies the user account owning the tenant. + UserID string `mapstructure:"user_id"` + + // Name contains the human-readable name for the server. + Name string + + // Updated and Created contain ISO-8601 timestamps of when the state of the server last changed, and when it was created. + Updated string + Created string + + HostID string + + // Status contains the current operational status of the server, such as IN_PROGRESS or ACTIVE. + Status string + + // Progress ranges from 0..100. + // A request made against the server completes only once Progress reaches 100. + Progress int + + // AccessIPv4 and AccessIPv6 contain the IP addresses of the server, suitable for remote access for administration. + AccessIPv4, AccessIPv6 string + + // Image refers to a JSON object, which itself indicates the OS image used to deploy the server. + Image map[string]interface{} + + // Flavor refers to a JSON object, which itself indicates the hardware configuration of the deployed server. + Flavor map[string]interface{} + + // Addresses includes a list of all IP addresses assigned to the server, keyed by pool. + Addresses map[string]interface{} + + // Metadata includes a list of all user-specified key-value pairs attached to the server. + Metadata map[string]interface{} + + // Links includes HTTP references to the itself, useful for passing along to other APIs that might want a server reference. + Links []interface{} + + // KeyName indicates which public key was injected into the server on launch. + KeyName string `json:"key_name" mapstructure:"key_name"` + + // AdminPass will generally be empty (""). However, it will contain the administrative password chosen when provisioning a new server without a set AdminPass setting in the first place. + // Note that this is the ONLY time this field will be valid. + AdminPass string `json:"adminPass" mapstructure:"adminPass"` + + // SecurityGroups includes the security groups that this instance has applied to it + SecurityGroups []map[string]interface{} `json:"security_groups" mapstructure:"security_groups"` + + // The public IP zone ID. Servers need to be in the same public IP zone ID to share a public IP. + PublicIPZoneID string `mapstructure:"RAX-PUBLIC-IP-ZONE-ID:publicIPZoneId"` +} + +type serverResult struct { + gophercloud.Result +} + +// CreateResult temporarily contains the response from a Create call. +type CreateResult struct { + serverResult +} + +// GetResult temporarily contains the response from a Get call. +type GetResult struct { + serverResult +} + +// UpdateResult temporarily contains the response from an Update call. +type UpdateResult struct { + serverResult +} + +// RebuildResult temporarily contains the response from a Rebuild call. +type RebuildResult struct { + serverResult +} + +// Extract interprets any serverResult as a Server, if possible. +func (r serverResult) Extract() (*Server, error) { + if r.Err != nil { + return nil, r.Err + } + + var response struct { + Server Server `mapstructure:"server"` + } + + config := &mapstructure.DecoderConfig{ + DecodeHook: toMapFromString, + Result: &response, + } + decoder, err := mapstructure.NewDecoder(config) + if err != nil { + return nil, err + } + + err = decoder.Decode(r.Body) + if err != nil { + return nil, err + } + + return &response.Server, nil +} + +func toMapFromString(from reflect.Kind, to reflect.Kind, data interface{}) (interface{}, error) { + if (from == reflect.String) && (to == reflect.Map) { + return map[string]interface{}{}, nil + } + return data, nil +} + +// ServerPage abstracts the raw results of making a List() request against the API. +// As OpenStack extensions may freely alter the response bodies of structures returned to the client, you may only safely access the +// data provided through the ExtractServers call. +type ServerPage struct { + pagination.LinkedPageBase +} + +// IsEmpty returns true if a page contains no Server results. +func (page ServerPage) IsEmpty() (bool, error) { + servers, err := ExtractServers(page) + if err != nil { + return true, err + } + return len(servers) == 0, nil +} + +// NextPageURL uses the response's embedded link reference to navigate to the next page of results. +func (page ServerPage) NextPageURL() (string, error) { + type resp struct { + Links []gophercloud.Link `mapstructure:"servers_links"` + } + + var r resp + err := mapstructure.Decode(page.Body, &r) + if err != nil { + return "", err + } + + return gophercloud.ExtractNextURL(r.Links) +} + +// ExtractServers interprets the results of a single page from a List() call, producing a slice of Server entities. +func ExtractServers(page pagination.Page) ([]Server, error) { + casted := page.(ServerPage).Body + + var response struct { + Servers []Server `mapstructure:"servers"` + } + + config := &mapstructure.DecoderConfig{ + DecodeHook: toMapFromString, + Result: &response, + } + decoder, err := mapstructure.NewDecoder(config) + if err != nil { + return nil, err + } + + err = decoder.Decode(casted) + + return response.Servers, err +} diff --git a/rackspace/compute/v2/servers/urls.go b/rackspace/compute/v2/servers/urls.go new file mode 100644 index 00000000..57587abc --- /dev/null +++ b/rackspace/compute/v2/servers/urls.go @@ -0,0 +1,31 @@ +package servers + +import "github.com/rackspace/gophercloud" + +func createURL(client *gophercloud.ServiceClient) string { + return client.ServiceURL("servers") +} + +func listURL(client *gophercloud.ServiceClient) string { + return createURL(client) +} + +func listDetailURL(client *gophercloud.ServiceClient) string { + return client.ServiceURL("servers", "detail") +} + +func deleteURL(client *gophercloud.ServiceClient, id string) string { + return client.ServiceURL("servers", id) +} + +func getURL(client *gophercloud.ServiceClient, id string) string { + return deleteURL(client, id) +} + +func updateURL(client *gophercloud.ServiceClient, id string) string { + return deleteURL(client, id) +} + +func actionURL(client *gophercloud.ServiceClient, id string) string { + return client.ServiceURL("servers", id, "action") +} From 4ad6ac325db3488896f044e986611c05982497a3 Mon Sep 17 00:00:00 2001 From: jrperritt Date: Thu, 17 Sep 2015 09:37:36 -0600 Subject: [PATCH 4/6] fix typo: shapred->shared --- rackspace/networking/v2/ipaddresses/requests.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rackspace/networking/v2/ipaddresses/requests.go b/rackspace/networking/v2/ipaddresses/requests.go index 2e69009a..7fa71d37 100644 --- a/rackspace/networking/v2/ipaddresses/requests.go +++ b/rackspace/networking/v2/ipaddresses/requests.go @@ -90,7 +90,7 @@ func (opts CreateOpts) ToSharedIPCreateMap() (map[string]interface{}, error) { return map[string]interface{}{"ip_address": v}, nil } -// Create will provision a new shapred IP address based on the values in CreateOpts. To extract +// Create will provision a new shared IP address based on the values in CreateOpts. To extract // the IPAddress object from the response, call the Extract method on the // CreateResult. func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult { From b359b60a637d47c7bc69e5412053b4e4304c0701 Mon Sep 17 00:00:00 2001 From: jrperritt Date: Thu, 17 Sep 2015 09:53:27 -0600 Subject: [PATCH 5/6] change pkg name: sharedips->ipaddresses --- .../networking/v2/ipaddresses/fixtures.go | 2 +- .../networking/v2/ipaddresses/requests.go | 26 +++++++++++++++---- .../v2/ipaddresses/requests_test.go | 2 +- .../networking/v2/ipaddresses/results.go | 2 +- rackspace/networking/v2/ipaddresses/urls.go | 2 +- 5 files changed, 25 insertions(+), 9 deletions(-) diff --git a/rackspace/networking/v2/ipaddresses/fixtures.go b/rackspace/networking/v2/ipaddresses/fixtures.go index d9f8db3f..e0150432 100644 --- a/rackspace/networking/v2/ipaddresses/fixtures.go +++ b/rackspace/networking/v2/ipaddresses/fixtures.go @@ -1,4 +1,4 @@ -package sharedips +package ipaddresses import ( "fmt" diff --git a/rackspace/networking/v2/ipaddresses/requests.go b/rackspace/networking/v2/ipaddresses/requests.go index 7fa71d37..e42e6bda 100644 --- a/rackspace/networking/v2/ipaddresses/requests.go +++ b/rackspace/networking/v2/ipaddresses/requests.go @@ -1,4 +1,4 @@ -package sharedips +package ipaddresses import ( "fmt" @@ -50,7 +50,7 @@ type CreateOptsBuilder interface { } // CreateOpts contains options for provisioning a shared IP address. This object is passed to -// the sharedips.Create function. +// the ipaddresses.Create function. type CreateOpts struct { // REQUIRED NetworkID string @@ -108,10 +108,26 @@ func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateRes return res } +// DeleteOptsBuilder allows extensions to add additional parameters to the +// Delete request. +type DeleteOptsBuilder interface { + ToSharedIPDeleteMap() (map[string]interface{}, error) +} + +// DeleteOpts contains options for provisioning a shared IP address. This object is passed to +// the ipaddresses.Delete function. +type DeleteOpts struct{} + +// ToSharedIPDeleteMap assembles a request body based on the contents of a +// DeleteOpts. +func (opts DeleteOpts) ToSharedIPDeleteMap() (map[string]interface{}, error) { + return map[string]interface{}{}, nil +} + // Delete will deallocate the existing shared IP with the provided ID from the tenant. // Before using this operation, all IP associations must be removed from the IP address // by using the Disassociate function. -func Delete(client *gophercloud.ServiceClient, id string) DeleteResult { +func Delete(client *gophercloud.ServiceClient, id string, opts DeleteOptsBuilder) DeleteResult { var res DeleteResult _, res.Err = client.Delete(deleteURL(client, id), nil) return res @@ -132,7 +148,7 @@ type UpdateOptsBuilder interface { } // UpdateOpts contain options for updating an existing shared IP. This object is passed -// to the sharedips.Update function. +// to the ipaddresses.Update function. type UpdateOpts struct { // REQUIRED PortIDs []string @@ -188,7 +204,7 @@ func (opts ListOpts) ToSharedIPListByServerQuery() (string, error) { } // ListByServer returns a Pager which allows you to iterate over a collection of -// IPAssociations. +// shared IP addresses associated with the given server. func ListByServer(c *gophercloud.ServiceClient, serverID string, opts ListByServerOptsBuilder) pagination.Pager { url := listByServerURL(c, serverID) if opts != nil { diff --git a/rackspace/networking/v2/ipaddresses/requests_test.go b/rackspace/networking/v2/ipaddresses/requests_test.go index 037b19fb..ee4e4187 100644 --- a/rackspace/networking/v2/ipaddresses/requests_test.go +++ b/rackspace/networking/v2/ipaddresses/requests_test.go @@ -1,4 +1,4 @@ -package sharedips +package ipaddresses import ( "testing" diff --git a/rackspace/networking/v2/ipaddresses/results.go b/rackspace/networking/v2/ipaddresses/results.go index da708ae8..a97a8aca 100644 --- a/rackspace/networking/v2/ipaddresses/results.go +++ b/rackspace/networking/v2/ipaddresses/results.go @@ -1,4 +1,4 @@ -package sharedips +package ipaddresses import ( "github.com/mitchellh/mapstructure" diff --git a/rackspace/networking/v2/ipaddresses/urls.go b/rackspace/networking/v2/ipaddresses/urls.go index f250f69f..f0b6ebea 100644 --- a/rackspace/networking/v2/ipaddresses/urls.go +++ b/rackspace/networking/v2/ipaddresses/urls.go @@ -1,4 +1,4 @@ -package sharedips +package ipaddresses import "github.com/rackspace/gophercloud" From 105abde920383bc2a6eaa5ee9f5cb26c521109de Mon Sep 17 00:00:00 2001 From: jrperritt Date: Thu, 17 Sep 2015 09:57:18 -0600 Subject: [PATCH 6/6] add DeleteOpts param to Delete op --- rackspace/networking/v2/ipaddresses/requests_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rackspace/networking/v2/ipaddresses/requests_test.go b/rackspace/networking/v2/ipaddresses/requests_test.go index ee4e4187..c0887d9a 100644 --- a/rackspace/networking/v2/ipaddresses/requests_test.go +++ b/rackspace/networking/v2/ipaddresses/requests_test.go @@ -134,7 +134,7 @@ func TestDelete(t *testing.T) { MockDeleteResponse(t) - res := Delete(client.ServiceClient(), "4cacd68e-d7aa-4ff2-96f4-5c6f57dba737") + res := Delete(client.ServiceClient(), "4cacd68e-d7aa-4ff2-96f4-5c6f57dba737", DeleteOpts{}) th.AssertNoErr(t, res.Err) }