Merge pull request #522 from deniszh/master
[rfr] From Port and To Port should accept values of 0
diff --git a/.travis.yml b/.travis.yml
index 325b90a..42c095d 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,16 +1,20 @@
language: go
+sudo: false
install:
+ - go get golang.org/x/crypto/ssh
- go get -v -tags 'fixtures acceptance' ./...
go:
- - 1.2
- - 1.3
- 1.4
- 1.5
-script: script/cibuild
-after_success:
- - go get golang.org/x/tools/cmd/cover
+ - tip
+env:
+ - COVERALLS_TOKEN=2k7PTU3xa474Hymwgdj6XjqenNfGTNkO8
+before_install:
- go get github.com/axw/gocov/gocov
- go get github.com/mattn/goveralls
- - export PATH=$PATH:$HOME/gopath/bin/
- - goveralls 2k7PTU3xa474Hymwgdj6XjqenNfGTNkO8
-sudo: false
+ - go get github.com/pierrre/gotestcover
+ - if ! go get github.com/golang/tools/cmd/cover; then go get golang.org/x/tools/cmd/cover; fi
+script:
+ - $HOME/gopath/bin/gotestcover -v -tags=fixtures -coverprofile=cover.out ./...
+after_success:
+ - $HOME/gopath/bin/goveralls -service=travis-ci -coverprofile=cover.out
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 6ba5beb..73c95bd 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -11,7 +11,8 @@
way than just downloading it. Here are the basic installation instructions:
1. Configure your `$GOPATH` and run `go get` as described in the main
-[README](/README.md#how-to-install).
+[README](/README.md#how-to-install) but add `-tags "fixtures acceptance"` to
+get dependencies for unit and acceptance tests.
2. Move into the directory that houses your local repository:
@@ -158,25 +159,25 @@
To run all tests:
```bash
-go test ./...
+go test -tags fixtures ./...
```
To run all tests with verbose output:
```bash
-go test -v ./...
+go test -v -tags fixtures ./...
```
To run tests that match certain [build tags]():
```bash
-go test -tags "foo bar" ./...
+go test -tags "fixtures foo bar" ./...
```
To run tests for a particular sub-package:
```bash
-cd ./path/to/package && go test .
+cd ./path/to/package && go test -tags fixtures .
```
## Basic style guide
diff --git a/README.md b/README.md
index 0a0da59..4b0042b 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,5 @@
# Gophercloud: an OpenStack SDK for Go
-[![Build Status](https://travis-ci.org/rackspace/gophercloud.svg?branch=master)](https://travis-ci.org/rackspace/gophercloud)
+[![Build Status](https://travis-ci.org/rackspace/gophercloud.svg?branch=master)](https://travis-ci.org/rackspace/gophercloud) [![Coverage Status](https://coveralls.io/repos/rackspace/gophercloud/badge.png)](https://coveralls.io/r/rackspace/gophercloud)
Gophercloud is a flexible SDK that allows you to consume and work with OpenStack
clouds in a simple and idiomatic way using golang. Many services are supported,
diff --git a/acceptance/openstack/blockstorage/v1/pkg.go b/acceptance/openstack/blockstorage/v1/pkg.go
new file mode 100644
index 0000000..16d2567
--- /dev/null
+++ b/acceptance/openstack/blockstorage/v1/pkg.go
@@ -0,0 +1,3 @@
+// The v1 package contains acceptance tests for the Openstack Cinder V1 service.
+
+package v1
diff --git a/acceptance/openstack/compute/v2/bootfromvolume_test.go b/acceptance/openstack/compute/v2/bootfromvolume_test.go
index add0e5f..bbecfad 100644
--- a/acceptance/openstack/compute/v2/bootfromvolume_test.go
+++ b/acceptance/openstack/compute/v2/bootfromvolume_test.go
@@ -53,3 +53,64 @@
defer servers.Delete(client, server.ID)
t.Logf("Deleting server [%s]...", name)
}
+
+func TestMultiEphemeral(t *testing.T) {
+ client, err := newClient()
+ th.AssertNoErr(t, err)
+
+ if testing.Short() {
+ t.Skip("Skipping test that requires server creation in short mode.")
+ }
+
+ choices, err := ComputeChoicesFromEnv()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ name := tools.RandomString("Gophercloud-", 8)
+ t.Logf("Creating server [%s].", name)
+
+ bd := []bootfromvolume.BlockDevice{
+ bootfromvolume.BlockDevice{
+ BootIndex: 0,
+ UUID: choices.ImageID,
+ SourceType: bootfromvolume.Image,
+ DestinationType: "local",
+ DeleteOnTermination: true,
+ },
+ bootfromvolume.BlockDevice{
+ BootIndex: -1,
+ SourceType: bootfromvolume.Blank,
+ DestinationType: "local",
+ DeleteOnTermination: true,
+ GuestFormat: "ext4",
+ VolumeSize: 1,
+ },
+ bootfromvolume.BlockDevice{
+ BootIndex: -1,
+ SourceType: bootfromvolume.Blank,
+ DestinationType: "local",
+ DeleteOnTermination: true,
+ GuestFormat: "ext4",
+ VolumeSize: 1,
+ },
+ }
+
+ serverCreateOpts := servers.CreateOpts{
+ Name: name,
+ FlavorRef: choices.FlavorID,
+ ImageRef: choices.ImageID,
+ }
+ server, err := bootfromvolume.Create(client, bootfromvolume.CreateOptsExt{
+ serverCreateOpts,
+ bd,
+ }).Extract()
+ th.AssertNoErr(t, err)
+ if err = waitForStatus(client, server, "ACTIVE"); err != nil {
+ t.Fatal(err)
+ }
+
+ t.Logf("Created server: %+v\n", server)
+ defer servers.Delete(client, server.ID)
+ t.Logf("Deleting server [%s]...", name)
+}
diff --git a/acceptance/openstack/networking/v2/port_test.go b/acceptance/openstack/networking/v2/port_test.go
index 03e8e27..91bf5bd 100644
--- a/acceptance/openstack/networking/v2/port_test.go
+++ b/acceptance/openstack/networking/v2/port_test.go
@@ -45,10 +45,20 @@
th.AssertEquals(t, p.ID, portID)
// Update port
- p, err = ports.Update(Client, portID, ports.UpdateOpts{Name: "new_port_name"}).Extract()
+ updateOpts := ports.UpdateOpts{
+ Name: "new_port_name",
+ AllowedAddressPairs: []ports.AddressPair{
+ ports.AddressPair{IPAddress: "192.168.199.201"},
+ },
+ }
+ p, err = ports.Update(Client, portID, updateOpts).Extract()
+
th.AssertNoErr(t, err)
th.AssertEquals(t, p.Name, "new_port_name")
+ updatedPort, err := ports.Get(Client, portID).Extract()
+ th.AssertEquals(t, updatedPort.AllowedAddressPairs[0].IPAddress, "192.168.199.201")
+
// Delete port
res := ports.Delete(Client, portID)
th.AssertNoErr(t, res.Err)
@@ -82,8 +92,8 @@
th.AssertNoErr(t, err)
for _, p := range portList {
- t.Logf("Port: ID [%s] Name [%s] Status [%s] MAC addr [%s] Fixed IPs [%#v] Security groups [%#v]",
- p.ID, p.Name, p.Status, p.MACAddress, p.FixedIPs, p.SecurityGroups)
+ t.Logf("Port: ID [%s] Name [%s] Status [%s] MAC addr [%s] Fixed IPs [%#v] Security groups [%#v] Allowed Address Pairs [%#v]",
+ p.ID, p.Name, p.Status, p.MACAddress, p.FixedIPs, p.SecurityGroups, p.AllowedAddressPairs)
}
return true, nil
@@ -108,6 +118,9 @@
IPVersion: subnets.IPv4,
Name: "my_subnet",
EnableDHCP: subnets.Down,
+ AllocationPools: []subnets.AllocationPool{
+ subnets.AllocationPool{Start: "192.168.199.2", End: "192.168.199.200"},
+ },
}).Extract()
return s.ID, err
}
diff --git a/acceptance/rackspace/identity/v2/tokens_test.go b/acceptance/rackspace/identity/v2/tokens_test.go
index 95ee7e6..b241cd9 100644
--- a/acceptance/rackspace/identity/v2/tokens_test.go
+++ b/acceptance/rackspace/identity/v2/tokens_test.go
@@ -6,43 +6,10 @@
"os"
"testing"
- "github.com/rackspace/gophercloud"
- "github.com/rackspace/gophercloud/acceptance/tools"
- "github.com/rackspace/gophercloud/rackspace"
"github.com/rackspace/gophercloud/rackspace/identity/v2/tokens"
th "github.com/rackspace/gophercloud/testhelper"
)
-func rackspaceAuthOptions(t *testing.T) gophercloud.AuthOptions {
- // Obtain credentials from the environment.
- options, err := rackspace.AuthOptionsFromEnv()
- th.AssertNoErr(t, err)
- options = tools.OnlyRS(options)
-
- if options.Username == "" {
- t.Fatal("Please provide a Rackspace username as RS_USERNAME.")
- }
- if options.APIKey == "" {
- t.Fatal("Please provide a Rackspace API key as RS_API_KEY.")
- }
-
- return options
-}
-
-func createClient(t *testing.T, auth bool) *gophercloud.ServiceClient {
- ao := rackspaceAuthOptions(t)
-
- provider, err := rackspace.NewClient(ao.IdentityEndpoint)
- th.AssertNoErr(t, err)
-
- if auth {
- err = rackspace.Authenticate(provider, ao)
- th.AssertNoErr(t, err)
- }
-
- return rackspace.NewIdentityV2(provider)
-}
-
func TestTokenAuth(t *testing.T) {
authedClient := createClient(t, true)
token := authedClient.TokenID
@@ -54,7 +21,7 @@
authOpts := tokens.AuthOptions{}
authOpts.TenantID = tenantID
- authOpts.Token = token
+ authOpts.TokenID = token
_, err := tokens.Create(authedClient, authOpts).ExtractToken()
th.AssertNoErr(t, err)
diff --git a/openstack/client.go b/openstack/client.go
index 33602a6..baa4cb5 100644
--- a/openstack/client.go
+++ b/openstack/client.go
@@ -3,6 +3,7 @@
import (
"fmt"
"net/url"
+ "strings"
"github.com/rackspace/gophercloud"
tokens2 "github.com/rackspace/gophercloud/openstack/identity/v2/tokens"
@@ -64,8 +65,8 @@
// 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 @@
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 @@
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 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/openstack/compute/v2/extensions/bootfromvolume/requests.go b/openstack/compute/v2/extensions/bootfromvolume/requests.go
index c8edee0..dceff3d 100644
--- a/openstack/compute/v2/extensions/bootfromvolume/requests.go
+++ b/openstack/compute/v2/extensions/bootfromvolume/requests.go
@@ -15,6 +15,7 @@
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 @@
// 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 @@
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/openstack/compute/v2/extensions/bootfromvolume/requests_test.go b/openstack/compute/v2/extensions/bootfromvolume/requests_test.go
index 8a7fa74..2c8bf49 100644
--- a/openstack/compute/v2/extensions/bootfromvolume/requests_test.go
+++ b/openstack/compute/v2/extensions/bootfromvolume/requests_test.go
@@ -32,8 +32,8 @@
"name": "createdserver",
"imageRef": "asdfasdfasdf",
"flavorRef": "performance1-1",
- "flavorName": "",
- "imageName": "",
+ "flavorName": "",
+ "imageName": "",
"block_device_mapping_v2":[
{
"uuid":"123456",
@@ -51,3 +51,81 @@
th.AssertNoErr(t, err)
th.CheckJSONEquals(t, expected, actual)
}
+
+func TestCreateMultiEphemeralOpts(t *testing.T) {
+ base := servers.CreateOpts{
+ Name: "createdserver",
+ ImageRef: "asdfasdfasdf",
+ FlavorRef: "performance1-1",
+ }
+
+ ext := CreateOptsExt{
+ CreateOptsBuilder: base,
+ BlockDevice: []BlockDevice{
+ BlockDevice{
+ BootIndex: 0,
+ DeleteOnTermination: true,
+ DestinationType: "local",
+ SourceType: Image,
+ UUID: "123456",
+ },
+ BlockDevice{
+ BootIndex: -1,
+ DeleteOnTermination: true,
+ DestinationType: "local",
+ GuestFormat: "ext4",
+ SourceType: Blank,
+ VolumeSize: 1,
+ },
+ BlockDevice{
+ BootIndex: -1,
+ DeleteOnTermination: true,
+ DestinationType: "local",
+ GuestFormat: "ext4",
+ SourceType: Blank,
+ VolumeSize: 1,
+ },
+ },
+ }
+
+ expected := `
+ {
+ "server": {
+ "name": "createdserver",
+ "imageRef": "asdfasdfasdf",
+ "flavorRef": "performance1-1",
+ "flavorName": "",
+ "imageName": "",
+ "block_device_mapping_v2":[
+ {
+ "boot_index": "0",
+ "delete_on_termination": "true",
+ "destination_type":"local",
+ "source_type":"image",
+ "uuid":"123456",
+ "volume_size": "0"
+ },
+ {
+ "boot_index": "-1",
+ "delete_on_termination": "true",
+ "destination_type":"local",
+ "guest_format":"ext4",
+ "source_type":"blank",
+ "volume_size": "1"
+ },
+ {
+ "boot_index": "-1",
+ "delete_on_termination": "true",
+ "destination_type":"local",
+ "guest_format":"ext4",
+ "source_type":"blank",
+ "volume_size": "1"
+ }
+ ]
+ }
+ }
+ `
+ actual, err := ext.ToServerCreateMap()
+ th.AssertNoErr(t, err)
+ th.CheckJSONEquals(t, expected, actual)
+}
diff --git a/openstack/compute/v2/servers/fixtures.go b/openstack/compute/v2/servers/fixtures.go
index 4339a16..85cea70 100644
--- a/openstack/compute/v2/servers/fixtures.go
+++ b/openstack/compute/v2/servers/fixtures.go
@@ -235,6 +235,12 @@
}
`
+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 @@
})
}
+// 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 @@
})
}
+// 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/openstack/compute/v2/servers/requests.go b/openstack/compute/v2/servers/requests.go
index f9839d9..e649053 100644
--- a/openstack/compute/v2/servers/requests.go
+++ b/openstack/compute/v2/servers/requests.go
@@ -303,6 +303,17 @@
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 @@
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/openstack/compute/v2/servers/requests_test.go b/openstack/compute/v2/servers/requests_test.go
index 88cb54d..6e23c52 100644
--- a/openstack/compute/v2/servers/requests_test.go
+++ b/openstack/compute/v2/servers/requests_test.go
@@ -78,6 +78,15 @@
th.AssertNoErr(t, res.Err)
}
+func TestForceDeleteServer(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleServerForceDeletionSuccessfully(t)
+
+ res := ForceDelete(client.ServiceClient(), "asdfasdfasdf")
+ th.AssertNoErr(t, res.Err)
+}
+
func TestGetServer(t *testing.T) {
th.SetupHTTP()
defer th.TeardownHTTP()
@@ -115,6 +124,15 @@
th.AssertNoErr(t, res.Err)
}
+func TestGetPassword(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandlePasswordGetSuccessfully(t)
+
+ res := GetPassword(client.ServiceClient(), "1234asdf")
+ th.AssertNoErr(t, res.Err)
+}
+
func TestRebootServer(t *testing.T) {
th.SetupHTTP()
defer th.TeardownHTTP()
diff --git a/openstack/compute/v2/servers/results.go b/openstack/compute/v2/servers/results.go
index f278709..406f689 100644
--- a/openstack/compute/v2/servers/results.go
+++ b/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 @@
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 '<pwd>' | base64 -D | openssl rsautl -decrypt -inkey <private_key>
+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/openstack/compute/v2/servers/results_test.go b/openstack/compute/v2/servers/results_test.go
new file mode 100644
index 0000000..2dba484
--- /dev/null
+++ b/openstack/compute/v2/servers/results_test.go
@@ -0,0 +1,99 @@
+// +build fixtures
+
+package servers
+
+import (
+ "crypto/rsa"
+ "encoding/json"
+ "fmt"
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ th "github.com/rackspace/gophercloud/testhelper"
+ "golang.org/x/crypto/ssh"
+)
+
+// Fail - No password in JSON.
+func TestExtractPassword_no_pwd_data(t *testing.T) {
+
+ var dejson interface{}
+ err := json.Unmarshal([]byte(`{ "Crappy data": ".-.-." }`), &dejson)
+ if err != nil {
+ t.Fatalf("%s", err)
+ }
+ resp := GetPasswordResult{gophercloud.Result{Body: dejson}}
+
+ pwd, err := resp.ExtractPassword(nil)
+ th.AssertEquals(t, pwd, "")
+}
+
+// Ok - return encrypted password when no private key is given.
+func TestExtractPassword_encrypted_pwd(t *testing.T) {
+
+ var dejson interface{}
+ sejson := []byte(`{"password":"PP8EnwPO9DhEc8+O/6CKAkPF379mKsUsfFY6yyw0734XXvKsSdV9KbiHQ2hrBvzeZxtGMrlFaikVunCRizyLLWLMuOi4hoH+qy9F9sQid61gQIGkxwDAt85d/7Eau2/KzorFnZhgxArl7IiqJ67X6xjKkR3zur+Yp3V/mtVIehpPYIaAvPbcp2t4mQXl1I9J8yrQfEZOctLL1L4heDEVXnxvNihVLK6pivlVggp6SZCtjj9cduZGrYGsxsOCso1dqJQr7GCojfwvuLOoG0OYwEGuWVTZppxWxi/q1QgeHFhGKA5QUXlz7pS71oqpjYsTeViuHnfvlqb5TVYZpQ1haw=="}`)
+
+ err := json.Unmarshal(sejson, &dejson)
+ fmt.Printf("%v\n", dejson)
+ if err != nil {
+ t.Fatalf("%s", err)
+ }
+ resp := GetPasswordResult{gophercloud.Result{Body: dejson}}
+
+ pwd, err := resp.ExtractPassword(nil)
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, "PP8EnwPO9DhEc8+O/6CKAkPF379mKsUsfFY6yyw0734XXvKsSdV9KbiHQ2hrBvzeZxtGMrlFaikVunCRizyLLWLMuOi4hoH+qy9F9sQid61gQIGkxwDAt85d/7Eau2/KzorFnZhgxArl7IiqJ67X6xjKkR3zur+Yp3V/mtVIehpPYIaAvPbcp2t4mQXl1I9J8yrQfEZOctLL1L4heDEVXnxvNihVLK6pivlVggp6SZCtjj9cduZGrYGsxsOCso1dqJQr7GCojfwvuLOoG0OYwEGuWVTZppxWxi/q1QgeHFhGKA5QUXlz7pS71oqpjYsTeViuHnfvlqb5TVYZpQ1haw==", pwd)
+}
+
+// Ok - return decrypted password when private key is given.
+// Decrytion can be verified by:
+// echo "<enc_pwd>" | base64 -D | openssl rsautl -decrypt -inkey <privateKey.pem>
+func TestExtractPassword_decrypted_pwd(t *testing.T) {
+
+ privateKey, err := ssh.ParseRawPrivateKey([]byte(`
+-----BEGIN RSA PRIVATE KEY-----
+MIIEpQIBAAKCAQEAo1ODZgwMVdTJYim9UYuYhowoPMhGEuV5IRZjcJ315r7RBSC+
+yEiBb1V+jhf+P8fzAyU35lkBzZGDr7E3jxSesbOuYT8cItQS4ErUnI1LGuqvMxwv
+X3GMyE/HmOcaiODF1XZN3Ur5pMJdVknnmczgUsW0hT98Udrh3MQn9WSuh/6LRy6+
+x1QsKHOCLFPnkhWa3LKyxmpQq/Gvhz+6NLe+gt8MFullA5mKQxBJ/K6laVHeaMlw
+JG3GCX0EZhRlvzoV8koIBKZtbKFolFr8ZtxBm3R5LvnyrtOvp22sa+xeItUT5kG1
+ZnbGNdK87oYW+VigEUfzT/+8R1i6E2QIXoeZiQIDAQABAoIBAQCVZ70IqbbTAW8j
+RAlyQh/J3Qal65LmkFJJKUDX8TfT1/Q/G6BKeMEmxm+Zrmsfj1pHI1HKftt+YEG1
+g4jOc09kQXkgbmnfll6aHPn3J+1vdwXD3GGdjrL5PrnYrngAhJWU2r8J0x8hT8ew
+OrUJZXhDX6XuSpAAFRmOKUZgXbSmo4X+LZX76ACnarselJt5FL724ECvpWJ7xxC4
+FMzvp4RqMmNFvv/Uq9lE/EmoSk4dviYyIZZ16DbDNyc9k/sGqCAMktCEwZ3EQm//
+S5bkNhgP6oUXjluWy53aPRgykEylgDWo5SSdSEyKnw/fciU0xdprA9JrBGIcTyHS
+/k2kgD4xAoGBANTkJ88Q0YrxX3fZNZVqcn00XKTxPGmxN5LRs7eV743q30AxK5Db
+QU8iwaAA1IKUWV5DLhgUTNsDCOPUPue4aOSBD3/sj+WEmvIhj7afDL5didkYHsqf
+fDnhFHq7y/3i57d428C7BwwR79pGWVyi7vH3pfu9A1iwl1aNOae+zvbVAoGBAMRm
+AmwQ9fJ3Qc44jysFK/yliLRGdShjkMMah5G3JlrelwfPtwPwEL2EHHhJB/C1acMs
+n6Q6RaoF6WNSZUY65ksQg7aPOYf2X0FTFwQJvwDJ4qlWjmq7w+tQ0AoGJG+dVUmQ
+zHZ/Y+HokSXzz9c4oevk4v/rMgAQ00WHrTdtIhnlAoGBALIJJ72D7CkNGHCq5qPQ
+xHQukPejgolFGhufYXM7YX3GmPMe67cVlTVv9Isxhoa5N0+cUPT0LR3PGOUm/4Bb
+eOT3hZXOqLwhvE6XgI8Rzd95bClwgXekDoh80dqeKMdmta961BQGlKskaPiacmsF
+G1yhZV70P9Mwwy8vpbLB4GUNAoGAbTwbjsWkNfa0qCF3J8NZoszjCvnBQfSW2J1R
+1+8ZKyNwt0yFi3Ajr3TibNiZzPzp1T9lj29FvfpJxA9Y+sXZvthxmcFxizix5GB1
+ha5yCNtA8VSOI7lJkAFDpL+j1lyYyjD6N9JE2KqEyKoh6J+8F7sXsqW7CqRRDfQX
+mKNfey0CgYEAxcEoNoADN2hRl7qY9rbQfVvQb3RkoQkdHhl9gpLFCcV32IP8R4xg
+09NbQK5OmgcIuZhLVNzTmUHJbabEGeXqIFIV0DsqECAt3WzbDyKQO23VJysFD46c
+KSde3I0ybDz7iS2EtceKB7m4C0slYd+oBkm4efuF00rCOKDwpFq45m0=
+-----END RSA PRIVATE KEY-----
+`))
+ if err != nil {
+ t.Fatalf("Error parsing private key: %s\n", err)
+ }
+
+ var dejson interface{}
+ sejson := []byte(`{"password":"PP8EnwPO9DhEc8+O/6CKAkPF379mKsUsfFY6yyw0734XXvKsSdV9KbiHQ2hrBvzeZxtGMrlFaikVunCRizyLLWLMuOi4hoH+qy9F9sQid61gQIGkxwDAt85d/7Eau2/KzorFnZhgxArl7IiqJ67X6xjKkR3zur+Yp3V/mtVIehpPYIaAvPbcp2t4mQXl1I9J8yrQfEZOctLL1L4heDEVXnxvNihVLK6pivlVggp6SZCtjj9cduZGrYGsxsOCso1dqJQr7GCojfwvuLOoG0OYwEGuWVTZppxWxi/q1QgeHFhGKA5QUXlz7pS71oqpjYsTeViuHnfvlqb5TVYZpQ1haw=="}`)
+
+ err = json.Unmarshal(sejson, &dejson)
+ fmt.Printf("%v\n", dejson)
+ if err != nil {
+ t.Fatalf("%s", err)
+ }
+ resp := GetPasswordResult{gophercloud.Result{Body: dejson}}
+
+ pwd, err := resp.ExtractPassword(privateKey.(*rsa.PrivateKey))
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, "ruZKK0tqxRfYm5t7lSJq", pwd)
+}
diff --git a/openstack/compute/v2/servers/urls.go b/openstack/compute/v2/servers/urls.go
index 8998354..d51fcbe 100644
--- a/openstack/compute/v2/servers/urls.go
+++ b/openstack/compute/v2/servers/urls.go
@@ -45,3 +45,7 @@
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/openstack/compute/v2/servers/urls_test.go b/openstack/compute/v2/servers/urls_test.go
index 17a1d28..9015d06 100644
--- a/openstack/compute/v2/servers/urls_test.go
+++ b/openstack/compute/v2/servers/urls_test.go
@@ -66,3 +66,9 @@
expected := endpoint + "servers/foo/metadata"
th.CheckEquals(t, expected, actual)
}
+
+func TestPasswordURL(t *testing.T) {
+ actual := passwordURL(endpointClient(), "foo")
+ expected := endpoint + "servers/foo/os-server-password"
+ th.CheckEquals(t, expected, actual)
+}
diff --git a/openstack/identity/v3/tokens/requests.go b/openstack/identity/v3/tokens/requests.go
index d449ca3..d63b1bb 100644
--- a/openstack/identity/v3/tokens/requests.go
+++ b/openstack/identity/v3/tokens/requests.go
@@ -15,9 +15,9 @@
}
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/openstack/networking/v2/extensions/layer3/routers/requests.go b/openstack/networking/v2/extensions/layer3/routers/requests.go
index 8b6e73d..1ffc136 100644
--- a/openstack/networking/v2/extensions/layer3/routers/requests.go
+++ b/openstack/networking/v2/extensions/layer3/routers/requests.go
@@ -16,6 +16,7 @@
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 @@
type CreateOpts struct {
Name string
AdminStateUp *bool
+ Distributed *bool
TenantID string
GatewayInfo *GatewayInfo
}
@@ -62,6 +64,7 @@
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 @@
reqBody := request{Router: router{
Name: gophercloud.MaybeString(opts.Name),
AdminStateUp: opts.AdminStateUp,
+ Distributed: opts.Distributed,
TenantID: gophercloud.MaybeString(opts.TenantID),
}}
@@ -96,6 +100,7 @@
type UpdateOpts struct {
Name string
AdminStateUp *bool
+ Distributed *bool
GatewayInfo *GatewayInfo
Routes []Route
}
@@ -109,6 +114,7 @@
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 @@
reqBody := request{Router: router{
Name: gophercloud.MaybeString(opts.Name),
AdminStateUp: opts.AdminStateUp,
+ Distributed: opts.Distributed,
}}
if opts.GatewayInfo != nil {
diff --git a/openstack/networking/v2/extensions/layer3/routers/requests_test.go b/openstack/networking/v2/extensions/layer3/routers/requests_test.go
index 1981733..dbdc6fa 100644
--- a/openstack/networking/v2/extensions/layer3/routers/requests_test.go
+++ b/openstack/networking/v2/extensions/layer3/routers/requests_test.go
@@ -37,6 +37,7 @@
"name": "second_routers",
"admin_state_up": true,
"tenant_id": "6b96ff0cb17a4b859e1e575d221683d3",
+ "distributed": false,
"id": "7177abc4-5ae9-4bb7-b0d4-89e94a4abf3b"
},
{
@@ -47,6 +48,7 @@
"name": "router1",
"admin_state_up": true,
"tenant_id": "33a40233088643acb66ff6eb0ebea679",
+ "distributed": false,
"id": "a9254bdb-2613-4a13-ac4c-adc581fba50d"
}
]
@@ -69,6 +71,7 @@
Status: "ACTIVE",
GatewayInfo: GatewayInfo{NetworkID: ""},
AdminStateUp: true,
+ Distributed: false,
Name: "second_routers",
ID: "7177abc4-5ae9-4bb7-b0d4-89e94a4abf3b",
TenantID: "6b96ff0cb17a4b859e1e575d221683d3",
@@ -77,6 +80,7 @@
Status: "ACTIVE",
GatewayInfo: GatewayInfo{NetworkID: "3c5bcddd-6af9-4e6b-9c3e-c153e521cab8"},
AdminStateUp: true,
+ Distributed: false,
Name: "router1",
ID: "a9254bdb-2613-4a13-ac4c-adc581fba50d",
TenantID: "33a40233088643acb66ff6eb0ebea679",
@@ -127,6 +131,7 @@
"name": "foo_router",
"admin_state_up": false,
"tenant_id": "6b96ff0cb17a4b859e1e575d221683d3",
+ "distributed": false,
"id": "8604a0de-7f6b-409a-a47c-a1cc7bc77b2e"
}
}
@@ -176,6 +181,7 @@
"name": "router1",
"admin_state_up": true,
"tenant_id": "d6554fe62e2f41efbb6e026fad5c1542",
+ "distributed": false,
"id": "a07eea83-7710-4860-931b-5fe220fae533"
}
}
@@ -233,6 +239,7 @@
"name": "new_name",
"admin_state_up": true,
"tenant_id": "6b96ff0cb17a4b859e1e575d221683d3",
+ "distributed": false,
"id": "8604a0de-7f6b-409a-a47c-a1cc7bc77b2e",
"routes": [
{
@@ -287,6 +294,7 @@
"name": "name",
"admin_state_up": true,
"tenant_id": "6b96ff0cb17a4b859e1e575d221683d3",
+ "distributed": false,
"id": "8604a0de-7f6b-409a-a47c-a1cc7bc77b2e",
"routes": []
}
diff --git a/openstack/networking/v2/extensions/layer3/routers/results.go b/openstack/networking/v2/extensions/layer3/routers/results.go
index 5e297ab..4534123 100644
--- a/openstack/networking/v2/extensions/layer3/routers/results.go
+++ b/openstack/networking/v2/extensions/layer3/routers/results.go
@@ -35,6 +35,9 @@
// 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/openstack/networking/v2/ports/requests.go b/openstack/networking/v2/ports/requests.go
index 2caf1ca..e73e10a 100644
--- a/openstack/networking/v2/ports/requests.go
+++ b/openstack/networking/v2/ports/requests.go
@@ -95,15 +95,16 @@
// 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 @@
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 @@
// 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 @@
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/openstack/networking/v2/ports/requests_test.go b/openstack/networking/v2/ports/requests_test.go
index 9e323ef..b442996 100644
--- a/openstack/networking/v2/ports/requests_test.go
+++ b/openstack/networking/v2/ports/requests_test.go
@@ -164,7 +164,13 @@
"ip_address": "10.0.0.2"
}
],
- "security_groups": ["foo"]
+ "security_groups": ["foo"],
+ "allowed_address_pairs": [
+ {
+ "ip_address": "10.0.0.4",
+ "mac_address": "fa:16:3e:c9:cb:f0"
+ }
+ ]
}
}
`)
@@ -177,7 +183,6 @@
"port": {
"status": "DOWN",
"name": "private-port",
- "allowed_address_pairs": [],
"admin_state_up": true,
"network_id": "a87cc70a-3e15-4acf-8205-9b711a3531b7",
"tenant_id": "d6700c0c9ffa4f1cb322cd4a1f3906fa",
@@ -193,6 +198,12 @@
"security_groups": [
"f0ac4394-7e4a-4409-9701-ba8be283dbc3"
],
+ "allowed_address_pairs": [
+ {
+ "ip_address": "10.0.0.4",
+ "mac_address": "fa:16:3e:c9:cb:f0"
+ }
+ ],
"device_id": ""
}
}
@@ -208,6 +219,9 @@
IP{SubnetID: "a0304c3a-4f08-4c43-88af-d796509c97d2", IPAddress: "10.0.0.2"},
},
SecurityGroups: []string{"foo"},
+ AllowedAddressPairs: []AddressPair{
+ AddressPair{IPAddress: "10.0.0.4", MACAddress: "fa:16:3e:c9:cb:f0"},
+ },
}
n, err := Create(fake.ServiceClient(), options).Extract()
th.AssertNoErr(t, err)
@@ -224,6 +238,9 @@
})
th.AssertEquals(t, n.ID, "65c0ee9f-d634-4522-8954-51021b570b0d")
th.AssertDeepEquals(t, n.SecurityGroups, []string{"f0ac4394-7e4a-4409-9701-ba8be283dbc3"})
+ th.AssertDeepEquals(t, n.AllowedAddressPairs, []AddressPair{
+ AddressPair{IPAddress: "10.0.0.4", MACAddress: "fa:16:3e:c9:cb:f0"},
+ })
}
func TestRequiredCreateOpts(t *testing.T) {
@@ -252,6 +269,12 @@
"ip_address": "10.0.0.3"
}
],
+ "allowed_address_pairs": [
+ {
+ "ip_address": "10.0.0.4",
+ "mac_address": "fa:16:3e:c9:cb:f0"
+ }
+ ],
"security_groups": [
"f0ac4394-7e4a-4409-9701-ba8be283dbc3"
]
@@ -278,6 +301,12 @@
"ip_address": "10.0.0.3"
}
],
+ "allowed_address_pairs": [
+ {
+ "ip_address": "10.0.0.4",
+ "mac_address": "fa:16:3e:c9:cb:f0"
+ }
+ ],
"id": "65c0ee9f-d634-4522-8954-51021b570b0d",
"security_groups": [
"f0ac4394-7e4a-4409-9701-ba8be283dbc3"
@@ -294,6 +323,9 @@
IP{SubnetID: "a0304c3a-4f08-4c43-88af-d796509c97d2", IPAddress: "10.0.0.3"},
},
SecurityGroups: []string{"f0ac4394-7e4a-4409-9701-ba8be283dbc3"},
+ AllowedAddressPairs: []AddressPair{
+ AddressPair{IPAddress: "10.0.0.4", MACAddress: "fa:16:3e:c9:cb:f0"},
+ },
}
s, err := Update(fake.ServiceClient(), "65c0ee9f-d634-4522-8954-51021b570b0d", options).Extract()
@@ -303,6 +335,9 @@
th.AssertDeepEquals(t, s.FixedIPs, []IP{
IP{SubnetID: "a0304c3a-4f08-4c43-88af-d796509c97d2", IPAddress: "10.0.0.3"},
})
+ th.AssertDeepEquals(t, s.AllowedAddressPairs, []AddressPair{
+ AddressPair{IPAddress: "10.0.0.4", MACAddress: "fa:16:3e:c9:cb:f0"},
+ })
th.AssertDeepEquals(t, s.SecurityGroups, []string{"f0ac4394-7e4a-4409-9701-ba8be283dbc3"})
}
diff --git a/openstack/networking/v2/ports/results.go b/openstack/networking/v2/ports/results.go
index 2511ff5..1f7eea1 100644
--- a/openstack/networking/v2/ports/results.go
+++ b/openstack/networking/v2/ports/results.go
@@ -19,7 +19,6 @@
var res struct {
Port *Port `json:"port"`
}
-
err := mapstructure.Decode(r.Body, &res)
return res.Port, err
@@ -51,6 +50,11 @@
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 @@
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/provider_client.go b/provider_client.go
index 9264355..53fce73 100644
--- a/provider_client.go
+++ b/provider_client.go
@@ -177,6 +177,9 @@
}
}
+ // 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 @@
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 @@
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/rackspace/lb/v1/nodes/requests.go b/rackspace/lb/v1/nodes/requests.go
index dc2d46c..9da376f 100644
--- a/rackspace/lb/v1/nodes/requests.go
+++ b/rackspace/lb/v1/nodes/requests.go
@@ -249,3 +249,38 @@
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
+}