From 0cffcb0c39f756c2ab582b4fdbe44c4bb804ba07 Mon Sep 17 00:00:00 2001 From: Rickard von Essen Date: Mon, 22 Feb 2016 09:45:21 +0100 Subject: [PATCH] Updated vendor after update of gophercloud in 88a84f5 --- .../rackspace/gophercloud/.travis.yml | 4 +- .../rackspace/gophercloud/openstack/client.go | 43 +++++++++++++++-- .../v2/extensions/bootfromvolume/requests.go | 7 +++ .../openstack/compute/v2/servers/fixtures.go | 28 +++++++++++ .../openstack/compute/v2/servers/requests.go | 20 ++++++++ .../openstack/compute/v2/servers/results.go | 47 ++++++++++++++++++- .../openstack/compute/v2/servers/urls.go | 4 ++ .../openstack/identity/v3/tokens/requests.go | 6 +-- .../v2/extensions/layer3/routers/requests.go | 7 +++ .../v2/extensions/layer3/routers/results.go | 3 ++ .../openstack/networking/v2/ports/requests.go | 38 +++++++++------ .../openstack/networking/v2/ports/results.go | 8 +++- .../rackspace/gophercloud/provider_client.go | 23 +++++++++ .../rackspace/lb/v1/nodes/requests.go | 35 ++++++++++++++ 14 files changed, 246 insertions(+), 27 deletions(-) diff --git a/vendor/github.com/rackspace/gophercloud/.travis.yml b/vendor/github.com/rackspace/gophercloud/.travis.yml index 325b90ac8..250006e49 100644 --- a/vendor/github.com/rackspace/gophercloud/.travis.yml +++ b/vendor/github.com/rackspace/gophercloud/.travis.yml @@ -1,11 +1,11 @@ language: go install: + - go get golang.org/x/crypto/ssh - go get -v -tags 'fixtures acceptance' ./... go: - - 1.2 - - 1.3 - 1.4 - 1.5 + - tip script: script/cibuild after_success: - go get golang.org/x/tools/cmd/cover diff --git a/vendor/github.com/rackspace/gophercloud/openstack/client.go b/vendor/github.com/rackspace/gophercloud/openstack/client.go index 33602a696..baa4cb51c 100644 --- a/vendor/github.com/rackspace/gophercloud/openstack/client.go +++ b/vendor/github.com/rackspace/gophercloud/openstack/client.go @@ -3,6 +3,7 @@ package openstack import ( "fmt" "net/url" + "strings" "github.com/rackspace/gophercloud" tokens2 "github.com/rackspace/gophercloud/openstack/identity/v2/tokens" @@ -64,8 +65,8 @@ func AuthenticatedClient(options gophercloud.AuthOptions) (*gophercloud.Provider // Authenticate or re-authenticate against the most recent identity service supported at the provided endpoint. func Authenticate(client *gophercloud.ProviderClient, options gophercloud.AuthOptions) error { versions := []*utils.Version{ - &utils.Version{ID: v20, Priority: 20, Suffix: "/v2.0/"}, - &utils.Version{ID: v30, Priority: 30, Suffix: "/v3/"}, + {ID: v20, Priority: 20, Suffix: "/v2.0/"}, + {ID: v30, Priority: 30, Suffix: "/v3/"}, } chosen, endpoint, err := utils.ChooseVersion(client, versions) @@ -110,7 +111,7 @@ func v2auth(client *gophercloud.ProviderClient, endpoint string, options gopherc if options.AllowReauth { client.ReauthFunc = func() error { client.TokenID = "" - return AuthenticateV2(client, options) + return v2auth(client, endpoint, options) } } client.TokenID = token.ID @@ -168,7 +169,7 @@ func v3auth(client *gophercloud.ProviderClient, endpoint string, options gopherc if options.AllowReauth { client.ReauthFunc = func() error { client.TokenID = "" - return AuthenticateV3(client, options) + return v3auth(client, endpoint, options) } } client.EndpointLocator = func(opts gophercloud.EndpointOpts) (string, error) { @@ -198,6 +199,40 @@ func NewIdentityV3(client *gophercloud.ProviderClient) *gophercloud.ServiceClien } } +func NewIdentityAdminV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + eo.ApplyDefaults("identity") + eo.Availability = gophercloud.AvailabilityAdmin + + url, err := client.EndpointLocator(eo) + if err != nil { + return nil, err + } + + // Force using v2 API + if strings.Contains(url, "/v3") { + url = strings.Replace(url, "/v3", "/v2.0", -1) + } + + return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil +} + +func NewIdentityAdminV3(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + eo.ApplyDefaults("identity") + eo.Availability = gophercloud.AvailabilityAdmin + + url, err := client.EndpointLocator(eo) + if err != nil { + return nil, err + } + + // Force using v3 API + if strings.Contains(url, "/v2.0") { + url = strings.Replace(url, "/v2.0", "/v3", -1) + } + + return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil +} + // NewObjectStorageV1 creates a ServiceClient that may be used with the v1 object storage package. func NewObjectStorageV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { eo.ApplyDefaults("object-store") diff --git a/vendor/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/bootfromvolume/requests.go b/vendor/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/bootfromvolume/requests.go index c8edee0e8..dceff3d87 100644 --- a/vendor/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/bootfromvolume/requests.go +++ b/vendor/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/bootfromvolume/requests.go @@ -15,6 +15,7 @@ const ( Volume SourceType = "volume" Snapshot SourceType = "snapshot" Image SourceType = "image" + Blank SourceType = "blank" ) // BlockDevice is a structure with options for booting a server instance @@ -32,6 +33,9 @@ type BlockDevice struct { // and "local". DestinationType string `json:"destination_type"` + // GuestFormat [optional] specifies the format of the block device. + GuestFormat string `json:"guest_format"` + // SourceType [required] must be one of: "volume", "snapshot", "image". SourceType SourceType `json:"source_type"` @@ -82,6 +86,9 @@ func (opts CreateOptsExt) ToServerCreateMap() (map[string]interface{}, error) { if bd.DestinationType != "" { blockDevice[i]["destination_type"] = bd.DestinationType } + if bd.GuestFormat != "" { + blockDevice[i]["guest_format"] = bd.GuestFormat + } } serverMap["block_device_mapping_v2"] = blockDevice diff --git a/vendor/github.com/rackspace/gophercloud/openstack/compute/v2/servers/fixtures.go b/vendor/github.com/rackspace/gophercloud/openstack/compute/v2/servers/fixtures.go index 4339a16d4..85cea70a8 100644 --- a/vendor/github.com/rackspace/gophercloud/openstack/compute/v2/servers/fixtures.go +++ b/vendor/github.com/rackspace/gophercloud/openstack/compute/v2/servers/fixtures.go @@ -235,6 +235,12 @@ const SingleServerBody = ` } ` +const ServerPasswordBody = ` +{ + "password": "xlozO3wLCBRWAa2yDjCCVx8vwNPypxnypmRYDa/zErlQ+EzPe1S/Gz6nfmC52mOlOSCRuUOmG7kqqgejPof6M7bOezS387zjq4LSvvwp28zUknzy4YzfFGhnHAdai3TxUJ26pfQCYrq8UTzmKF2Bq8ioSEtVVzM0A96pDh8W2i7BOz6MdoiVyiev/I1K2LsuipfxSJR7Wdke4zNXJjHHP2RfYsVbZ/k9ANu+Nz4iIH8/7Cacud/pphH7EjrY6a4RZNrjQskrhKYed0YERpotyjYk1eDtRe72GrSiXteqCM4biaQ5w3ruS+AcX//PXk3uJ5kC7d67fPXaVz4WaQRYMg==" +} +` + var ( // ServerHerp is a Server struct that should correspond to the first result in ServerListBody. ServerHerp = Server{ @@ -399,6 +405,18 @@ func HandleServerDeletionSuccessfully(t *testing.T) { }) } +// HandleServerForceDeletionSuccessfully sets up the test server to respond to a server force deletion +// request. +func HandleServerForceDeletionSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/asdfasdfasdf/action", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, `{ "forceDelete": "" }`) + + w.WriteHeader(http.StatusAccepted) + }) +} + // HandleServerGetSuccessfully sets up the test server to respond to a server Get request. func HandleServerGetSuccessfully(t *testing.T) { th.Mux.HandleFunc("/servers/1234asdf", func(w http.ResponseWriter, r *http.Request) { @@ -662,3 +680,13 @@ func HandleCreateServerImageSuccessfully(t *testing.T) { }) } +// HandlePasswordGetSuccessfully sets up the test server to respond to a password Get request. +func HandlePasswordGetSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/1234asdf/os-server-password", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + + fmt.Fprintf(w, ServerPasswordBody) + }) +} diff --git a/vendor/github.com/rackspace/gophercloud/openstack/compute/v2/servers/requests.go b/vendor/github.com/rackspace/gophercloud/openstack/compute/v2/servers/requests.go index f9839d9f2..e6490539c 100644 --- a/vendor/github.com/rackspace/gophercloud/openstack/compute/v2/servers/requests.go +++ b/vendor/github.com/rackspace/gophercloud/openstack/compute/v2/servers/requests.go @@ -303,6 +303,17 @@ func Delete(client *gophercloud.ServiceClient, id string) DeleteResult { return res } +func ForceDelete(client *gophercloud.ServiceClient, id string) ActionResult { + var req struct { + ForceDelete string `json:"forceDelete"` + } + + var res ActionResult + _, res.Err = client.Post(actionURL(client, id), req, nil, nil) + return res + +} + // Get requests details on a single server, by ID. func Get(client *gophercloud.ServiceClient, id string) GetResult { var result GetResult @@ -850,3 +861,12 @@ func IDFromName(client *gophercloud.ServiceClient, name string) (string, error) return "", fmt.Errorf("Found %d servers matching %s", serverCount, name) } } + +// GetPassword makes a request against the nova API to get the encrypted administrative password. +func GetPassword(client *gophercloud.ServiceClient, serverId string) GetPasswordResult { + var res GetPasswordResult + _, res.Err = client.Request("GET", passwordURL(client, serverId), gophercloud.RequestOpts{ + JSONResponse: &res.Body, + }) + return res +} diff --git a/vendor/github.com/rackspace/gophercloud/openstack/compute/v2/servers/results.go b/vendor/github.com/rackspace/gophercloud/openstack/compute/v2/servers/results.go index f27870984..406f68935 100644 --- a/vendor/github.com/rackspace/gophercloud/openstack/compute/v2/servers/results.go +++ b/vendor/github.com/rackspace/gophercloud/openstack/compute/v2/servers/results.go @@ -1,10 +1,12 @@ package servers import ( - "reflect" + "crypto/rsa" + "encoding/base64" "fmt" - "path" "net/url" + "path" + "reflect" "github.com/mitchellh/mapstructure" "github.com/rackspace/gophercloud" @@ -82,6 +84,47 @@ type CreateImageResult struct { gophercloud.Result } +// GetPasswordResult represent the result of a get os-server-password operation. +type GetPasswordResult struct { + gophercloud.Result +} + +// ExtractPassword gets the encrypted password. +// If privateKey != nil the password is decrypted with the private key. +// If privateKey == nil the encrypted password is returned and can be decrypted with: +// echo '' | base64 -D | openssl rsautl -decrypt -inkey +func (r GetPasswordResult) ExtractPassword(privateKey *rsa.PrivateKey) (string, error) { + + if r.Err != nil { + return "", r.Err + } + + var response struct { + Password string `mapstructure:"password"` + } + + err := mapstructure.Decode(r.Body, &response) + if err == nil && privateKey != nil && response.Password != "" { + return decryptPassword(response.Password, privateKey) + } + return response.Password, err +} + +func decryptPassword(encryptedPassword string, privateKey *rsa.PrivateKey) (string, error) { + b64EncryptedPassword := make([]byte, base64.StdEncoding.DecodedLen(len(encryptedPassword))) + + n, err := base64.StdEncoding.Decode(b64EncryptedPassword, []byte(encryptedPassword)) + if err != nil { + return "", fmt.Errorf("Failed to base64 decode encrypted password: %s", err) + } + password, err := rsa.DecryptPKCS1v15(nil, privateKey, b64EncryptedPassword[0:n]) + if err != nil { + return "", fmt.Errorf("Failed to decrypt password: %s", err) + } + + return string(password), nil +} + // ExtractImageID gets the ID of the newly created server image from the header func (res CreateImageResult) ExtractImageID() (string, error) { if res.Err != nil { diff --git a/vendor/github.com/rackspace/gophercloud/openstack/compute/v2/servers/urls.go b/vendor/github.com/rackspace/gophercloud/openstack/compute/v2/servers/urls.go index 899835493..d51fcbe6c 100644 --- a/vendor/github.com/rackspace/gophercloud/openstack/compute/v2/servers/urls.go +++ b/vendor/github.com/rackspace/gophercloud/openstack/compute/v2/servers/urls.go @@ -45,3 +45,7 @@ func listAddressesURL(client *gophercloud.ServiceClient, id string) string { func listAddressesByNetworkURL(client *gophercloud.ServiceClient, id, network string) string { return client.ServiceURL("servers", id, "ips", network) } + +func passwordURL(client *gophercloud.ServiceClient, id string) string { + return client.ServiceURL("servers", id, "os-server-password") +} diff --git a/vendor/github.com/rackspace/gophercloud/openstack/identity/v3/tokens/requests.go b/vendor/github.com/rackspace/gophercloud/openstack/identity/v3/tokens/requests.go index d449ca36e..d63b1bb51 100644 --- a/vendor/github.com/rackspace/gophercloud/openstack/identity/v3/tokens/requests.go +++ b/vendor/github.com/rackspace/gophercloud/openstack/identity/v3/tokens/requests.go @@ -15,9 +15,9 @@ type Scope struct { } func subjectTokenHeaders(c *gophercloud.ServiceClient, subjectToken string) map[string]string { - h := c.AuthenticatedHeaders() - h["X-Subject-Token"] = subjectToken - return h + return map[string]string{ + "X-Subject-Token": subjectToken, + } } // Create authenticates and either generates a new token, or changes the Scope of an existing token. diff --git a/vendor/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/routers/requests.go b/vendor/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/routers/requests.go index 8b6e73de9..1ffc1369b 100644 --- a/vendor/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/routers/requests.go +++ b/vendor/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/routers/requests.go @@ -16,6 +16,7 @@ type ListOpts struct { ID string `q:"id"` Name string `q:"name"` AdminStateUp *bool `q:"admin_state_up"` + Distributed *bool `q:"distributed"` Status string `q:"status"` TenantID string `q:"tenant_id"` Limit int `q:"limit"` @@ -46,6 +47,7 @@ func List(c *gophercloud.ServiceClient, opts ListOpts) pagination.Pager { type CreateOpts struct { Name string AdminStateUp *bool + Distributed *bool TenantID string GatewayInfo *GatewayInfo } @@ -62,6 +64,7 @@ func Create(c *gophercloud.ServiceClient, opts CreateOpts) CreateResult { type router struct { Name *string `json:"name,omitempty"` AdminStateUp *bool `json:"admin_state_up,omitempty"` + Distributed *bool `json:"distributed,omitempty"` TenantID *string `json:"tenant_id,omitempty"` GatewayInfo *GatewayInfo `json:"external_gateway_info,omitempty"` } @@ -73,6 +76,7 @@ func Create(c *gophercloud.ServiceClient, opts CreateOpts) CreateResult { reqBody := request{Router: router{ Name: gophercloud.MaybeString(opts.Name), AdminStateUp: opts.AdminStateUp, + Distributed: opts.Distributed, TenantID: gophercloud.MaybeString(opts.TenantID), }} @@ -96,6 +100,7 @@ func Get(c *gophercloud.ServiceClient, id string) GetResult { type UpdateOpts struct { Name string AdminStateUp *bool + Distributed *bool GatewayInfo *GatewayInfo Routes []Route } @@ -109,6 +114,7 @@ func Update(c *gophercloud.ServiceClient, id string, opts UpdateOpts) UpdateResu type router struct { Name *string `json:"name,omitempty"` AdminStateUp *bool `json:"admin_state_up,omitempty"` + Distributed *bool `json:"distributed,omitempty"` GatewayInfo *GatewayInfo `json:"external_gateway_info,omitempty"` Routes []Route `json:"routes"` } @@ -120,6 +126,7 @@ func Update(c *gophercloud.ServiceClient, id string, opts UpdateOpts) UpdateResu reqBody := request{Router: router{ Name: gophercloud.MaybeString(opts.Name), AdminStateUp: opts.AdminStateUp, + Distributed: opts.Distributed, }} if opts.GatewayInfo != nil { diff --git a/vendor/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/routers/results.go b/vendor/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/routers/results.go index 5e297ab26..453412398 100644 --- a/vendor/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/routers/results.go +++ b/vendor/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/routers/results.go @@ -35,6 +35,9 @@ type Router struct { // Administrative state of the router. AdminStateUp bool `json:"admin_state_up" mapstructure:"admin_state_up"` + // Whether router is disitrubted or not.. + Distributed bool `json:"distributed" mapstructure:"distributed"` + // Human readable name for the router. Does not have to be unique. Name string `json:"name" mapstructure:"name"` diff --git a/vendor/github.com/rackspace/gophercloud/openstack/networking/v2/ports/requests.go b/vendor/github.com/rackspace/gophercloud/openstack/networking/v2/ports/requests.go index 2caf1cace..e73e10ac6 100644 --- a/vendor/github.com/rackspace/gophercloud/openstack/networking/v2/ports/requests.go +++ b/vendor/github.com/rackspace/gophercloud/openstack/networking/v2/ports/requests.go @@ -95,15 +95,16 @@ type CreateOptsBuilder interface { // CreateOpts represents the attributes used when creating a new port. type CreateOpts struct { - NetworkID string - Name string - AdminStateUp *bool - MACAddress string - FixedIPs interface{} - DeviceID string - DeviceOwner string - TenantID string - SecurityGroups []string + NetworkID string + Name string + AdminStateUp *bool + MACAddress string + FixedIPs interface{} + DeviceID string + DeviceOwner string + TenantID string + SecurityGroups []string + AllowedAddressPairs []AddressPair } // ToPortCreateMap casts a CreateOpts struct to a map. @@ -139,6 +140,9 @@ func (opts CreateOpts) ToPortCreateMap() (map[string]interface{}, error) { if opts.MACAddress != "" { p["mac_address"] = opts.MACAddress } + if opts.AllowedAddressPairs != nil { + p["allowed_address_pairs"] = opts.AllowedAddressPairs + } return map[string]interface{}{"port": p}, nil } @@ -168,12 +172,13 @@ type UpdateOptsBuilder interface { // UpdateOpts represents the attributes used when updating an existing port. type UpdateOpts struct { - Name string - AdminStateUp *bool - FixedIPs interface{} - DeviceID string - DeviceOwner string - SecurityGroups []string + Name string + AdminStateUp *bool + FixedIPs interface{} + DeviceID string + DeviceOwner string + SecurityGroups []string + AllowedAddressPairs []AddressPair } // ToPortUpdateMap casts an UpdateOpts struct to a map. @@ -198,6 +203,9 @@ func (opts UpdateOpts) ToPortUpdateMap() (map[string]interface{}, error) { if opts.Name != "" { p["name"] = opts.Name } + if opts.AllowedAddressPairs != nil { + p["allowed_address_pairs"] = opts.AllowedAddressPairs + } return map[string]interface{}{"port": p}, nil } diff --git a/vendor/github.com/rackspace/gophercloud/openstack/networking/v2/ports/results.go b/vendor/github.com/rackspace/gophercloud/openstack/networking/v2/ports/results.go index 2511ff53b..1f7eea169 100644 --- a/vendor/github.com/rackspace/gophercloud/openstack/networking/v2/ports/results.go +++ b/vendor/github.com/rackspace/gophercloud/openstack/networking/v2/ports/results.go @@ -19,7 +19,6 @@ func (r commonResult) Extract() (*Port, error) { var res struct { Port *Port `json:"port"` } - err := mapstructure.Decode(r.Body, &res) return res.Port, err @@ -51,6 +50,11 @@ type IP struct { IPAddress string `mapstructure:"ip_address" json:"ip_address,omitempty"` } +type AddressPair struct { + IPAddress string `mapstructure:"ip_address" json:"ip_address,omitempty"` + MACAddress string `mapstructure:"mac_address" json:"mac_address,omitempty"` +} + // Port represents a Neutron port. See package documentation for a top-level // description of what this is. type Port struct { @@ -78,6 +82,8 @@ type Port struct { 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"` + // Identifies the list of IP addresses the port will recognize/accept + AllowedAddressPairs []AddressPair `mapstructure:"allowed_address_pairs" json:"allowed_address_pairs"` } // PortPage is the page returned by a pager when traversing over a collection diff --git a/vendor/github.com/rackspace/gophercloud/provider_client.go b/vendor/github.com/rackspace/gophercloud/provider_client.go index 92643556f..53fce7370 100644 --- a/vendor/github.com/rackspace/gophercloud/provider_client.go +++ b/vendor/github.com/rackspace/gophercloud/provider_client.go @@ -177,6 +177,9 @@ func (client *ProviderClient) Request(method, url string, options RequestOpts) ( } } + // Set connection parameter to close the connection immediately when we've got the response + req.Close = true + // Issue the request. resp, err := client.HTTPClient.Do(req) if err != nil { @@ -246,6 +249,8 @@ func defaultOkCodes(method string) []int { return []int{201, 202} case method == "PUT": return []int{201, 202} + case method == "PATCH": + return []int{200, 204} case method == "DELETE": return []int{202, 204} } @@ -299,6 +304,24 @@ func (client *ProviderClient) Put(url string, JSONBody interface{}, JSONResponse return client.Request("PUT", url, *opts) } +func (client *ProviderClient) Patch(url string, JSONBody interface{}, JSONResponse *interface{}, opts *RequestOpts) (*http.Response, error) { + if opts == nil { + opts = &RequestOpts{} + } + + if v, ok := (JSONBody).(io.ReadSeeker); ok { + opts.RawBody = v + } else if JSONBody != nil { + opts.JSONBody = JSONBody + } + + if JSONResponse != nil { + opts.JSONResponse = JSONResponse + } + + return client.Request("PATCH", url, *opts) +} + func (client *ProviderClient) Delete(url string, opts *RequestOpts) (*http.Response, error) { if opts == nil { opts = &RequestOpts{} diff --git a/vendor/github.com/rackspace/gophercloud/rackspace/lb/v1/nodes/requests.go b/vendor/github.com/rackspace/gophercloud/rackspace/lb/v1/nodes/requests.go index dc2d46c89..9da376f83 100644 --- a/vendor/github.com/rackspace/gophercloud/rackspace/lb/v1/nodes/requests.go +++ b/vendor/github.com/rackspace/gophercloud/rackspace/lb/v1/nodes/requests.go @@ -249,3 +249,38 @@ func ListEvents(client *gophercloud.ServiceClient, loadBalancerID int, opts List return NodeEventPage{pagination.SinglePageBase(r)} }) } + +// GetByIPPort locates a load balancer node by IP and port. +func GetByIPPort( + client *gophercloud.ServiceClient, + loadBalancerID int, + address string, + port int, +) (*Node, error) { + + // nil until found + var found *Node + + List(client, loadBalancerID, nil).EachPage(func(page pagination.Page) (bool, error) { + lbNodes, err := ExtractNodes(page) + if err != nil { + return false, err + } + + for _, trialNode := range lbNodes { + if trialNode.Address == address && trialNode.Port == port { + found = &trialNode + return false, nil + } + + } + + return true, nil + }) + + if found == nil { + return nil, errors.New("Unable to get node by IP and Port") + } + + return found, nil +}