Merge pull request #346 from haklop/fix-pool-members
Add a missing omitempty on the CreateOpts struct of LBaaS members
diff --git a/.travis.yml b/.travis.yml
index cf4f8ca..946f98c 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -4,6 +4,8 @@
go:
- 1.1
- 1.2
+ - 1.3
+ - 1.4
- tip
script: script/cibuild
after_success:
@@ -12,3 +14,4 @@
- go get github.com/mattn/goveralls
- export PATH=$PATH:$HOME/gopath/bin/
- goveralls 2k7PTU3xa474Hymwgdj6XjqenNfGTNkO8
+sudo: false
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 4f596a1..93b798e 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -11,7 +11,7 @@
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](/#how-to-install).
+[README](/README.md#how-to-install).
2. Move into the directory that houses your local repository:
diff --git a/UPGRADING.md b/UPGRADING.md
index da3758b..a702cfc 100644
--- a/UPGRADING.md
+++ b/UPGRADING.md
@@ -238,7 +238,7 @@
imageList, err := images.ExtractImages(page)
for _, i := range imageList {
- // "i" will be a images.Image
+ // "i" will be an images.Image
}
})
```
@@ -284,7 +284,7 @@
imageList, err := images.ExtractImages(page)
for _, i := range imageList {
- // "i" will be a images.Image
+ // "i" will be an images.Image
}
})
```
diff --git a/acceptance/openstack/blockstorage/v1/snapshots_test.go b/acceptance/openstack/blockstorage/v1/snapshots_test.go
index 9132ee5..7741aa9 100644
--- a/acceptance/openstack/blockstorage/v1/snapshots_test.go
+++ b/acceptance/openstack/blockstorage/v1/snapshots_test.go
@@ -13,9 +13,9 @@
func TestSnapshots(t *testing.T) {
- client, err := newClient()
+ client, err := newClient(t)
th.AssertNoErr(t, err)
-
+
v, err := volumes.Create(client, &volumes.CreateOpts{
Name: "gophercloud-test-volume",
Size: 1,
diff --git a/acceptance/openstack/blockstorage/v1/volumes_test.go b/acceptance/openstack/blockstorage/v1/volumes_test.go
index 99da39a..7760427 100644
--- a/acceptance/openstack/blockstorage/v1/volumes_test.go
+++ b/acceptance/openstack/blockstorage/v1/volumes_test.go
@@ -13,7 +13,7 @@
th "github.com/rackspace/gophercloud/testhelper"
)
-func newClient() (*gophercloud.ServiceClient, error) {
+func newClient(t *testing.T) (*gophercloud.ServiceClient, error) {
ao, err := openstack.AuthOptionsFromEnv()
th.AssertNoErr(t, err)
@@ -26,7 +26,7 @@
}
func TestVolumes(t *testing.T) {
- client, err := newClient()
+ client, err := newClient(t)
th.AssertNoErr(t, err)
cv, err := volumes.Create(client, &volumes.CreateOpts{
diff --git a/acceptance/openstack/blockstorage/v1/volumetypes_test.go b/acceptance/openstack/blockstorage/v1/volumetypes_test.go
index 5adcd81..000bc01 100644
--- a/acceptance/openstack/blockstorage/v1/volumetypes_test.go
+++ b/acceptance/openstack/blockstorage/v1/volumetypes_test.go
@@ -12,7 +12,7 @@
)
func TestVolumeTypes(t *testing.T) {
- client, err := newClient()
+ client, err := newClient(t)
th.AssertNoErr(t, err)
vt, err := volumetypes.Create(client, &volumetypes.CreateOpts{
diff --git a/acceptance/openstack/compute/v2/bootfromvolume_test.go b/acceptance/openstack/compute/v2/bootfromvolume_test.go
index d08abe6..add0e5f 100644
--- a/acceptance/openstack/compute/v2/bootfromvolume_test.go
+++ b/acceptance/openstack/compute/v2/bootfromvolume_test.go
@@ -5,10 +5,10 @@
import (
"testing"
+ "github.com/rackspace/gophercloud/acceptance/tools"
"github.com/rackspace/gophercloud/openstack/compute/v2/extensions/bootfromvolume"
"github.com/rackspace/gophercloud/openstack/compute/v2/servers"
th "github.com/rackspace/gophercloud/testhelper"
- "github.com/smashwilson/gophercloud/acceptance/tools"
)
func TestBootFromVolume(t *testing.T) {
@@ -37,14 +37,19 @@
serverCreateOpts := servers.CreateOpts{
Name: name,
- FlavorRef: "3",
+ 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 deleteServer(t, client, server)
+ defer servers.Delete(client, server.ID)
t.Logf("Deleting server [%s]...", name)
}
diff --git a/acceptance/openstack/compute/v2/compute_test.go b/acceptance/openstack/compute/v2/compute_test.go
index 46eb9ff..33e49fe 100644
--- a/acceptance/openstack/compute/v2/compute_test.go
+++ b/acceptance/openstack/compute/v2/compute_test.go
@@ -1,4 +1,4 @@
-// +build acceptance
+// +build acceptance common
package v2
diff --git a/acceptance/openstack/compute/v2/keypairs_test.go b/acceptance/openstack/compute/v2/keypairs_test.go
new file mode 100644
index 0000000..3e12d6b
--- /dev/null
+++ b/acceptance/openstack/compute/v2/keypairs_test.go
@@ -0,0 +1,74 @@
+// +build acceptance
+
+package v2
+
+import (
+ "crypto/rand"
+ "crypto/rsa"
+ "testing"
+
+ "github.com/rackspace/gophercloud/acceptance/tools"
+ "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs"
+ "github.com/rackspace/gophercloud/openstack/compute/v2/servers"
+ th "github.com/rackspace/gophercloud/testhelper"
+
+ "code.google.com/p/go.crypto/ssh"
+)
+
+const keyName = "gophercloud_test_key_pair"
+
+func TestCreateServerWithKeyPair(t *testing.T) {
+ client, err := newClient()
+ th.AssertNoErr(t, err)
+
+ if testing.Short() {
+ t.Skip("Skipping test that requires server creation in short mode.")
+ }
+
+ privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
+ publicKey := privateKey.PublicKey
+ pub, err := ssh.NewPublicKey(&publicKey)
+ th.AssertNoErr(t, err)
+ pubBytes := ssh.MarshalAuthorizedKey(pub)
+ pk := string(pubBytes)
+
+ kp, err := keypairs.Create(client, keypairs.CreateOpts{
+ Name: keyName,
+ PublicKey: pk,
+ }).Extract()
+ th.AssertNoErr(t, err)
+ t.Logf("Created key pair: %s\n", kp)
+
+ choices, err := ComputeChoicesFromEnv()
+ th.AssertNoErr(t, err)
+
+ name := tools.RandomString("Gophercloud-", 8)
+ t.Logf("Creating server [%s] with key pair.", name)
+
+ serverCreateOpts := servers.CreateOpts{
+ Name: name,
+ FlavorRef: choices.FlavorID,
+ ImageRef: choices.ImageID,
+ }
+
+ server, err := servers.Create(client, keypairs.CreateOptsExt{
+ serverCreateOpts,
+ keyName,
+ }).Extract()
+ th.AssertNoErr(t, err)
+ defer servers.Delete(client, server.ID)
+ if err = waitForStatus(client, server, "ACTIVE"); err != nil {
+ t.Fatalf("Unable to wait for server: %v", err)
+ }
+
+ server, err = servers.Get(client, server.ID).Extract()
+ t.Logf("Created server: %+v\n", server)
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, server.KeyName, keyName)
+
+ t.Logf("Deleting key pair [%s]...", kp.Name)
+ err = keypairs.Delete(client, keyName).ExtractErr()
+ th.AssertNoErr(t, err)
+
+ t.Logf("Deleting server [%s]...", name)
+}
diff --git a/acceptance/openstack/compute/v2/secdefrules_test.go b/acceptance/openstack/compute/v2/secdefrules_test.go
new file mode 100644
index 0000000..78b0798
--- /dev/null
+++ b/acceptance/openstack/compute/v2/secdefrules_test.go
@@ -0,0 +1,72 @@
+// +build acceptance compute defsecrules
+
+package v2
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/acceptance/tools"
+ dsr "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/defsecrules"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestSecDefRules(t *testing.T) {
+ client, err := newClient()
+ th.AssertNoErr(t, err)
+
+ id := createDefRule(t, client)
+
+ listDefRules(t, client)
+
+ getDefRule(t, client, id)
+
+ deleteDefRule(t, client, id)
+}
+
+func createDefRule(t *testing.T, client *gophercloud.ServiceClient) string {
+ opts := dsr.CreateOpts{
+ FromPort: tools.RandomInt(80, 89),
+ ToPort: tools.RandomInt(90, 99),
+ IPProtocol: "TCP",
+ CIDR: "0.0.0.0/0",
+ }
+
+ rule, err := dsr.Create(client, opts).Extract()
+ th.AssertNoErr(t, err)
+
+ t.Logf("Created default rule %s", rule.ID)
+
+ return rule.ID
+}
+
+func listDefRules(t *testing.T, client *gophercloud.ServiceClient) {
+ err := dsr.List(client).EachPage(func(page pagination.Page) (bool, error) {
+ drList, err := dsr.ExtractDefaultRules(page)
+ th.AssertNoErr(t, err)
+
+ for _, dr := range drList {
+ t.Logf("Listing default rule %s: Name [%s] From Port [%s] To Port [%s] Protocol [%s]",
+ dr.ID, dr.FromPort, dr.ToPort, dr.IPProtocol)
+ }
+
+ return true, nil
+ })
+
+ th.AssertNoErr(t, err)
+}
+
+func getDefRule(t *testing.T, client *gophercloud.ServiceClient, id string) {
+ rule, err := dsr.Get(client, id).Extract()
+ th.AssertNoErr(t, err)
+
+ t.Logf("Getting rule %s: %#v", id, rule)
+}
+
+func deleteDefRule(t *testing.T, client *gophercloud.ServiceClient, id string) {
+ err := dsr.Delete(client, id).ExtractErr()
+ th.AssertNoErr(t, err)
+
+ t.Logf("Deleted rule %s", id)
+}
diff --git a/acceptance/openstack/compute/v2/secgroup_test.go b/acceptance/openstack/compute/v2/secgroup_test.go
new file mode 100644
index 0000000..4f50739
--- /dev/null
+++ b/acceptance/openstack/compute/v2/secgroup_test.go
@@ -0,0 +1,177 @@
+// +build acceptance compute secgroups
+
+package v2
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/acceptance/tools"
+ "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/secgroups"
+ "github.com/rackspace/gophercloud/openstack/compute/v2/servers"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestSecGroups(t *testing.T) {
+ client, err := newClient()
+ th.AssertNoErr(t, err)
+
+ serverID, needsDeletion := findServer(t, client)
+
+ groupID := createSecGroup(t, client)
+
+ listSecGroups(t, client)
+
+ newName := tools.RandomString("secgroup_", 5)
+ updateSecGroup(t, client, groupID, newName)
+
+ getSecGroup(t, client, groupID)
+
+ addRemoveRules(t, client, groupID)
+
+ addServerToSecGroup(t, client, serverID, newName)
+
+ removeServerFromSecGroup(t, client, serverID, newName)
+
+ if needsDeletion {
+ servers.Delete(client, serverID)
+ }
+
+ deleteSecGroup(t, client, groupID)
+}
+
+func createSecGroup(t *testing.T, client *gophercloud.ServiceClient) string {
+ opts := secgroups.CreateOpts{
+ Name: tools.RandomString("secgroup_", 5),
+ Description: "something",
+ }
+
+ group, err := secgroups.Create(client, opts).Extract()
+ th.AssertNoErr(t, err)
+
+ t.Logf("Created secgroup %s %s", group.ID, group.Name)
+
+ return group.ID
+}
+
+func listSecGroups(t *testing.T, client *gophercloud.ServiceClient) {
+ err := secgroups.List(client).EachPage(func(page pagination.Page) (bool, error) {
+ secGrpList, err := secgroups.ExtractSecurityGroups(page)
+ th.AssertNoErr(t, err)
+
+ for _, sg := range secGrpList {
+ t.Logf("Listing secgroup %s: Name [%s] Desc [%s] TenantID [%s]", sg.ID,
+ sg.Name, sg.Description, sg.TenantID)
+ }
+
+ return true, nil
+ })
+
+ th.AssertNoErr(t, err)
+}
+
+func updateSecGroup(t *testing.T, client *gophercloud.ServiceClient, id, newName string) {
+ opts := secgroups.UpdateOpts{
+ Name: newName,
+ Description: tools.RandomString("dec_", 10),
+ }
+ group, err := secgroups.Update(client, id, opts).Extract()
+ th.AssertNoErr(t, err)
+
+ t.Logf("Updated %s's name to %s", group.ID, group.Name)
+}
+
+func getSecGroup(t *testing.T, client *gophercloud.ServiceClient, id string) {
+ group, err := secgroups.Get(client, id).Extract()
+ th.AssertNoErr(t, err)
+
+ t.Logf("Getting %s: %#v", id, group)
+}
+
+func addRemoveRules(t *testing.T, client *gophercloud.ServiceClient, id string) {
+ opts := secgroups.CreateRuleOpts{
+ ParentGroupID: id,
+ FromPort: 22,
+ ToPort: 22,
+ IPProtocol: "TCP",
+ CIDR: "0.0.0.0/0",
+ }
+
+ rule, err := secgroups.CreateRule(client, opts).Extract()
+ th.AssertNoErr(t, err)
+
+ t.Logf("Adding rule %s to group %s", rule.ID, id)
+
+ err = secgroups.DeleteRule(client, rule.ID).ExtractErr()
+ th.AssertNoErr(t, err)
+
+ t.Logf("Deleted rule %s from group %s", rule.ID, id)
+}
+
+func findServer(t *testing.T, client *gophercloud.ServiceClient) (string, bool) {
+ var serverID string
+ var needsDeletion bool
+
+ err := servers.List(client, nil).EachPage(func(page pagination.Page) (bool, error) {
+ sList, err := servers.ExtractServers(page)
+ th.AssertNoErr(t, err)
+
+ for _, s := range sList {
+ serverID = s.ID
+ needsDeletion = false
+
+ t.Logf("Found an existing server: ID [%s]", serverID)
+ break
+ }
+
+ return true, nil
+ })
+ th.AssertNoErr(t, err)
+
+ if serverID == "" {
+ t.Log("No server found, creating one")
+
+ choices, err := ComputeChoicesFromEnv()
+ th.AssertNoErr(t, err)
+
+ opts := &servers.CreateOpts{
+ Name: tools.RandomString("secgroup_test_", 5),
+ ImageRef: choices.ImageID,
+ FlavorRef: choices.FlavorID,
+ }
+
+ s, err := servers.Create(client, opts).Extract()
+ th.AssertNoErr(t, err)
+ serverID = s.ID
+
+ t.Logf("Created server %s, waiting for it to build", s.ID)
+ err = servers.WaitForStatus(client, serverID, "ACTIVE", 300)
+ th.AssertNoErr(t, err)
+
+ needsDeletion = true
+ }
+
+ return serverID, needsDeletion
+}
+
+func addServerToSecGroup(t *testing.T, client *gophercloud.ServiceClient, serverID, groupName string) {
+ err := secgroups.AddServerToGroup(client, serverID, groupName).ExtractErr()
+ th.AssertNoErr(t, err)
+
+ t.Logf("Adding group %s to server %s", groupName, serverID)
+}
+
+func removeServerFromSecGroup(t *testing.T, client *gophercloud.ServiceClient, serverID, groupName string) {
+ err := secgroups.RemoveServerFromGroup(client, serverID, groupName).ExtractErr()
+ th.AssertNoErr(t, err)
+
+ t.Logf("Removing group %s from server %s", groupName, serverID)
+}
+
+func deleteSecGroup(t *testing.T, client *gophercloud.ServiceClient, id string) {
+ err := secgroups.Delete(client, id).ExtractErr()
+ th.AssertNoErr(t, err)
+
+ t.Logf("Deleted group %s", id)
+}
diff --git a/acceptance/openstack/compute/v2/servers_test.go b/acceptance/openstack/compute/v2/servers_test.go
index e223c18..d52a9d3 100644
--- a/acceptance/openstack/compute/v2/servers_test.go
+++ b/acceptance/openstack/compute/v2/servers_test.go
@@ -12,6 +12,7 @@
"github.com/rackspace/gophercloud/openstack/compute/v2/servers"
"github.com/rackspace/gophercloud/openstack/networking/v2/networks"
"github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
)
func TestListServers(t *testing.T) {
@@ -94,6 +95,8 @@
name := tools.RandomString("ACPTTEST", 16)
t.Logf("Attempting to create server: %s\n", name)
+ pwd := tools.MakeNewPassword("")
+
server, err := servers.Create(client, servers.CreateOpts{
Name: name,
FlavorRef: choices.FlavorID,
@@ -101,11 +104,14 @@
Networks: []servers.Network{
servers.Network{UUID: network.ID},
},
+ AdminPass: pwd,
}).Extract()
if err != nil {
t.Fatalf("Unable to create server: %v", err)
}
+ th.AssertEquals(t, pwd, server.AdminPass)
+
return server, err
}
@@ -391,3 +397,54 @@
t.Fatal(err)
}
}
+
+func TestServerMetadata(t *testing.T) {
+ t.Parallel()
+
+ choices, err := ComputeChoicesFromEnv()
+ th.AssertNoErr(t, err)
+
+ client, err := newClient()
+ if err != nil {
+ t.Fatalf("Unable to create a compute client: %v", err)
+ }
+
+ server, err := createServer(t, client, choices)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer servers.Delete(client, server.ID)
+ if err = waitForStatus(client, server, "ACTIVE"); err != nil {
+ t.Fatal(err)
+ }
+
+ metadata, err := servers.UpdateMetadata(client, server.ID, servers.MetadataOpts{
+ "foo": "bar",
+ "this": "that",
+ }).Extract()
+ th.AssertNoErr(t, err)
+ t.Logf("UpdateMetadata result: %+v\n", metadata)
+
+ err = servers.DeleteMetadatum(client, server.ID, "foo").ExtractErr()
+ th.AssertNoErr(t, err)
+
+ metadata, err = servers.CreateMetadatum(client, server.ID, servers.MetadatumOpts{
+ "foo": "baz",
+ }).Extract()
+ th.AssertNoErr(t, err)
+ t.Logf("CreateMetadatum result: %+v\n", metadata)
+
+ metadata, err = servers.Metadatum(client, server.ID, "foo").Extract()
+ th.AssertNoErr(t, err)
+ t.Logf("Metadatum result: %+v\n", metadata)
+ th.AssertEquals(t, "baz", metadata["foo"])
+
+ metadata, err = servers.Metadata(client, server.ID).Extract()
+ th.AssertNoErr(t, err)
+ t.Logf("Metadata result: %+v\n", metadata)
+
+ metadata, err = servers.ResetMetadata(client, server.ID, servers.MetadataOpts{}).Extract()
+ th.AssertNoErr(t, err)
+ t.Logf("ResetMetadata result: %+v\n", metadata)
+ th.AssertDeepEquals(t, map[string]string{}, metadata)
+}
diff --git a/acceptance/openstack/identity/v2/extension_test.go b/acceptance/openstack/identity/v2/extension_test.go
index 2b4e062..d1fa1e3 100644
--- a/acceptance/openstack/identity/v2/extension_test.go
+++ b/acceptance/openstack/identity/v2/extension_test.go
@@ -1,4 +1,4 @@
-// +build acceptance
+// +build acceptance identity
package v2
diff --git a/acceptance/openstack/identity/v2/identity_test.go b/acceptance/openstack/identity/v2/identity_test.go
index feae233..96bf1fd 100644
--- a/acceptance/openstack/identity/v2/identity_test.go
+++ b/acceptance/openstack/identity/v2/identity_test.go
@@ -1,4 +1,4 @@
-// +build acceptance
+// +build acceptance identity
package v2
diff --git a/acceptance/openstack/identity/v2/role_test.go b/acceptance/openstack/identity/v2/role_test.go
new file mode 100644
index 0000000..ba243fe
--- /dev/null
+++ b/acceptance/openstack/identity/v2/role_test.go
@@ -0,0 +1,58 @@
+// +build acceptance identity roles
+
+package v2
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/openstack/identity/v2/extensions/admin/roles"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestRoles(t *testing.T) {
+ client := authenticatedClient(t)
+
+ tenantID := findTenant(t, client)
+ userID := createUser(t, client, tenantID)
+ roleID := listRoles(t, client)
+
+ addUserRole(t, client, tenantID, userID, roleID)
+
+ deleteUserRole(t, client, tenantID, userID, roleID)
+
+ deleteUser(t, client, userID)
+}
+
+func listRoles(t *testing.T, client *gophercloud.ServiceClient) string {
+ var roleID string
+
+ err := roles.List(client).EachPage(func(page pagination.Page) (bool, error) {
+ roleList, err := roles.ExtractRoles(page)
+ th.AssertNoErr(t, err)
+
+ for _, role := range roleList {
+ t.Logf("Listing role: ID [%s] Name [%s]", role.ID, role.Name)
+ roleID = role.ID
+ }
+
+ return true, nil
+ })
+
+ th.AssertNoErr(t, err)
+
+ return roleID
+}
+
+func addUserRole(t *testing.T, client *gophercloud.ServiceClient, tenantID, userID, roleID string) {
+ err := roles.AddUserRole(client, tenantID, userID, roleID).ExtractErr()
+ th.AssertNoErr(t, err)
+ t.Logf("Added role %s to user %s", roleID, userID)
+}
+
+func deleteUserRole(t *testing.T, client *gophercloud.ServiceClient, tenantID, userID, roleID string) {
+ err := roles.DeleteUserRole(client, tenantID, userID, roleID).ExtractErr()
+ th.AssertNoErr(t, err)
+ t.Logf("Removed role %s from user %s", roleID, userID)
+}
diff --git a/acceptance/openstack/identity/v2/tenant_test.go b/acceptance/openstack/identity/v2/tenant_test.go
index 2054598..578fc48 100644
--- a/acceptance/openstack/identity/v2/tenant_test.go
+++ b/acceptance/openstack/identity/v2/tenant_test.go
@@ -1,4 +1,4 @@
-// +build acceptance
+// +build acceptance identity
package v2
diff --git a/acceptance/openstack/identity/v2/token_test.go b/acceptance/openstack/identity/v2/token_test.go
index 0632a48..d903140 100644
--- a/acceptance/openstack/identity/v2/token_test.go
+++ b/acceptance/openstack/identity/v2/token_test.go
@@ -1,4 +1,4 @@
-// +build acceptance
+// +build acceptance identity
package v2
diff --git a/acceptance/openstack/identity/v2/user_test.go b/acceptance/openstack/identity/v2/user_test.go
new file mode 100644
index 0000000..fe73d19
--- /dev/null
+++ b/acceptance/openstack/identity/v2/user_test.go
@@ -0,0 +1,127 @@
+// +build acceptance identity
+
+package v2
+
+import (
+ "strconv"
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/acceptance/tools"
+ "github.com/rackspace/gophercloud/openstack/identity/v2/tenants"
+ "github.com/rackspace/gophercloud/openstack/identity/v2/users"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestUsers(t *testing.T) {
+ client := authenticatedClient(t)
+
+ tenantID := findTenant(t, client)
+
+ userID := createUser(t, client, tenantID)
+
+ listUsers(t, client)
+
+ getUser(t, client, userID)
+
+ updateUser(t, client, userID)
+
+ listUserRoles(t, client, tenantID, userID)
+
+ deleteUser(t, client, userID)
+}
+
+func findTenant(t *testing.T, client *gophercloud.ServiceClient) string {
+ var tenantID string
+ err := tenants.List(client, nil).EachPage(func(page pagination.Page) (bool, error) {
+ tenantList, err := tenants.ExtractTenants(page)
+ th.AssertNoErr(t, err)
+
+ for _, t := range tenantList {
+ tenantID = t.ID
+ break
+ }
+
+ return true, nil
+ })
+ th.AssertNoErr(t, err)
+
+ return tenantID
+}
+
+func createUser(t *testing.T, client *gophercloud.ServiceClient, tenantID string) string {
+ t.Log("Creating user")
+
+ opts := users.CreateOpts{
+ Name: tools.RandomString("user_", 5),
+ Enabled: users.Disabled,
+ TenantID: tenantID,
+ Email: "new_user@foo.com",
+ }
+
+ user, err := users.Create(client, opts).Extract()
+ th.AssertNoErr(t, err)
+ t.Logf("Created user %s on tenant %s", user.ID, tenantID)
+
+ return user.ID
+}
+
+func listUsers(t *testing.T, client *gophercloud.ServiceClient) {
+ err := users.List(client).EachPage(func(page pagination.Page) (bool, error) {
+ userList, err := users.ExtractUsers(page)
+ th.AssertNoErr(t, err)
+
+ for _, user := range userList {
+ t.Logf("Listing user: ID [%s] Name [%s] Email [%s] Enabled? [%s]",
+ user.ID, user.Name, user.Email, strconv.FormatBool(user.Enabled))
+ }
+
+ return true, nil
+ })
+
+ th.AssertNoErr(t, err)
+}
+
+func getUser(t *testing.T, client *gophercloud.ServiceClient, userID string) {
+ _, err := users.Get(client, userID).Extract()
+ th.AssertNoErr(t, err)
+ t.Logf("Getting user %s", userID)
+}
+
+func updateUser(t *testing.T, client *gophercloud.ServiceClient, userID string) {
+ opts := users.UpdateOpts{Name: tools.RandomString("new_name", 5), Email: "new@foo.com"}
+ user, err := users.Update(client, userID, opts).Extract()
+ th.AssertNoErr(t, err)
+ t.Logf("Updated user %s: Name [%s] Email [%s]", userID, user.Name, user.Email)
+}
+
+func listUserRoles(t *testing.T, client *gophercloud.ServiceClient, tenantID, userID string) {
+ count := 0
+ err := users.ListRoles(client, tenantID, userID).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+
+ roleList, err := users.ExtractRoles(page)
+ th.AssertNoErr(t, err)
+
+ t.Logf("Listing roles for user %s", userID)
+
+ for _, r := range roleList {
+ t.Logf("- %s (%s)", r.Name, r.ID)
+ }
+
+ return true, nil
+ })
+
+ if count == 0 {
+ t.Logf("No roles for user %s", userID)
+ }
+
+ th.AssertNoErr(t, err)
+}
+
+func deleteUser(t *testing.T, client *gophercloud.ServiceClient, userID string) {
+ res := users.Delete(client, userID)
+ th.AssertNoErr(t, res.Err)
+ t.Logf("Deleted user %s", userID)
+}
diff --git a/acceptance/openstack/networking/v2/port_test.go b/acceptance/openstack/networking/v2/port_test.go
index 7f22dbd..03e8e27 100644
--- a/acceptance/openstack/networking/v2/port_test.go
+++ b/acceptance/openstack/networking/v2/port_test.go
@@ -82,7 +82,7 @@
th.AssertNoErr(t, err)
for _, p := range portList {
- t.Logf("Port: ID [%s] Name [%s] Status [%d] MAC addr [%s] Fixed IPs [%#v] Security groups [%#v]",
+ 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)
}
diff --git a/acceptance/rackspace/cdn/v1/base_test.go b/acceptance/rackspace/cdn/v1/base_test.go
new file mode 100644
index 0000000..135f5b3
--- /dev/null
+++ b/acceptance/rackspace/cdn/v1/base_test.go
@@ -0,0 +1,32 @@
+// +build acceptance
+
+package v1
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/rackspace/cdn/v1/base"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestBaseOps(t *testing.T) {
+ client := newClient(t)
+ t.Log("Retrieving Home Document")
+ testHomeDocumentGet(t, client)
+
+ t.Log("Pinging root URL")
+ testPing(t, client)
+}
+
+func testHomeDocumentGet(t *testing.T, client *gophercloud.ServiceClient) {
+ hd, err := base.Get(client).Extract()
+ th.AssertNoErr(t, err)
+ t.Logf("Retrieved home document: %+v", *hd)
+}
+
+func testPing(t *testing.T, client *gophercloud.ServiceClient) {
+ err := base.Ping(client).ExtractErr()
+ th.AssertNoErr(t, err)
+ t.Logf("Successfully pinged root URL")
+}
diff --git a/acceptance/rackspace/cdn/v1/common.go b/acceptance/rackspace/cdn/v1/common.go
new file mode 100644
index 0000000..2333ca7
--- /dev/null
+++ b/acceptance/rackspace/cdn/v1/common.go
@@ -0,0 +1,23 @@
+// +build acceptance
+
+package v1
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/rackspace"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func newClient(t *testing.T) *gophercloud.ServiceClient {
+ ao, err := rackspace.AuthOptionsFromEnv()
+ th.AssertNoErr(t, err)
+
+ client, err := rackspace.AuthenticatedClient(ao)
+ th.AssertNoErr(t, err)
+
+ c, err := rackspace.NewCDNV1(client, gophercloud.EndpointOpts{})
+ th.AssertNoErr(t, err)
+ return c
+}
diff --git a/acceptance/rackspace/cdn/v1/flavor_test.go b/acceptance/rackspace/cdn/v1/flavor_test.go
new file mode 100644
index 0000000..f26cff0
--- /dev/null
+++ b/acceptance/rackspace/cdn/v1/flavor_test.go
@@ -0,0 +1,47 @@
+// +build acceptance
+
+package v1
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ os "github.com/rackspace/gophercloud/openstack/cdn/v1/flavors"
+ "github.com/rackspace/gophercloud/pagination"
+ "github.com/rackspace/gophercloud/rackspace/cdn/v1/flavors"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestFlavor(t *testing.T) {
+ client := newClient(t)
+
+ t.Log("Listing Flavors")
+ id := testFlavorsList(t, client)
+
+ t.Log("Retrieving Flavor")
+ testFlavorGet(t, client, id)
+}
+
+func testFlavorsList(t *testing.T, client *gophercloud.ServiceClient) string {
+ var id string
+ err := flavors.List(client).EachPage(func(page pagination.Page) (bool, error) {
+ flavorList, err := os.ExtractFlavors(page)
+ th.AssertNoErr(t, err)
+
+ for _, flavor := range flavorList {
+ t.Logf("Listing flavor: ID [%s] Providers [%+v]", flavor.ID, flavor.Providers)
+ id = flavor.ID
+ }
+
+ return true, nil
+ })
+
+ th.AssertNoErr(t, err)
+ return id
+}
+
+func testFlavorGet(t *testing.T, client *gophercloud.ServiceClient, id string) {
+ flavor, err := flavors.Get(client, id).Extract()
+ th.AssertNoErr(t, err)
+ t.Logf("Retrieved Flavor: %+v", *flavor)
+}
diff --git a/acceptance/rackspace/cdn/v1/service_test.go b/acceptance/rackspace/cdn/v1/service_test.go
new file mode 100644
index 0000000..c19c241
--- /dev/null
+++ b/acceptance/rackspace/cdn/v1/service_test.go
@@ -0,0 +1,93 @@
+// +build acceptance
+
+package v1
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ os "github.com/rackspace/gophercloud/openstack/cdn/v1/services"
+ "github.com/rackspace/gophercloud/pagination"
+ "github.com/rackspace/gophercloud/rackspace/cdn/v1/services"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestService(t *testing.T) {
+ client := newClient(t)
+
+ t.Log("Creating Service")
+ loc := testServiceCreate(t, client, "test-site-1")
+ t.Logf("Created service at location: %s", loc)
+
+ defer testServiceDelete(t, client, loc)
+
+ t.Log("Updating Service")
+ testServiceUpdate(t, client, loc)
+
+ t.Log("Retrieving Service")
+ testServiceGet(t, client, loc)
+
+ t.Log("Listing Services")
+ testServiceList(t, client)
+}
+
+func testServiceCreate(t *testing.T, client *gophercloud.ServiceClient, name string) string {
+ createOpts := os.CreateOpts{
+ Name: name,
+ Domains: []os.Domain{
+ os.Domain{
+ Domain: "www." + name + ".com",
+ },
+ },
+ Origins: []os.Origin{
+ os.Origin{
+ Origin: name + ".com",
+ Port: 80,
+ SSL: false,
+ },
+ },
+ FlavorID: "cdn",
+ }
+ l, err := services.Create(client, createOpts).Extract()
+ th.AssertNoErr(t, err)
+ return l
+}
+
+func testServiceGet(t *testing.T, client *gophercloud.ServiceClient, id string) {
+ s, err := services.Get(client, id).Extract()
+ th.AssertNoErr(t, err)
+ t.Logf("Retrieved service: %+v", *s)
+}
+
+func testServiceUpdate(t *testing.T, client *gophercloud.ServiceClient, id string) {
+ opts := os.UpdateOpts{
+ os.Append{
+ Value: os.Domain{Domain: "newDomain.com", Protocol: "http"},
+ },
+ }
+
+ loc, err := services.Update(client, id, opts).Extract()
+ th.AssertNoErr(t, err)
+ t.Logf("Successfully updated service at location: %s", loc)
+}
+
+func testServiceList(t *testing.T, client *gophercloud.ServiceClient) {
+ err := services.List(client, nil).EachPage(func(page pagination.Page) (bool, error) {
+ serviceList, err := os.ExtractServices(page)
+ th.AssertNoErr(t, err)
+
+ for _, service := range serviceList {
+ t.Logf("Listing service: %+v", service)
+ }
+
+ return true, nil
+ })
+
+ th.AssertNoErr(t, err)
+}
+
+func testServiceDelete(t *testing.T, client *gophercloud.ServiceClient, id string) {
+ err := services.Delete(client, id).ExtractErr()
+ th.AssertNoErr(t, err)
+ t.Logf("Successfully deleted service (%s)", id)
+}
diff --git a/acceptance/rackspace/cdn/v1/serviceasset_test.go b/acceptance/rackspace/cdn/v1/serviceasset_test.go
new file mode 100644
index 0000000..c32bf25
--- /dev/null
+++ b/acceptance/rackspace/cdn/v1/serviceasset_test.go
@@ -0,0 +1,32 @@
+// +build acceptance
+
+package v1
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ osServiceAssets "github.com/rackspace/gophercloud/openstack/cdn/v1/serviceassets"
+ "github.com/rackspace/gophercloud/rackspace/cdn/v1/serviceassets"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestServiceAsset(t *testing.T) {
+ client := newClient(t)
+
+ t.Log("Creating Service")
+ loc := testServiceCreate(t, client, "test-site-2")
+ t.Logf("Created service at location: %s", loc)
+
+ t.Log("Deleting Service Assets")
+ testServiceAssetDelete(t, client, loc)
+}
+
+func testServiceAssetDelete(t *testing.T, client *gophercloud.ServiceClient, url string) {
+ deleteOpts := osServiceAssets.DeleteOpts{
+ All: true,
+ }
+ err := serviceassets.Delete(client, url, deleteOpts).ExtractErr()
+ th.AssertNoErr(t, err)
+ t.Log("Successfully deleted all Service Assets")
+}
diff --git a/acceptance/rackspace/compute/v2/bootfromvolume_test.go b/acceptance/rackspace/compute/v2/bootfromvolume_test.go
index 010bf42..d7e6aa7 100644
--- a/acceptance/rackspace/compute/v2/bootfromvolume_test.go
+++ b/acceptance/rackspace/compute/v2/bootfromvolume_test.go
@@ -5,11 +5,11 @@
import (
"testing"
+ "github.com/rackspace/gophercloud/acceptance/tools"
osBFV "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/bootfromvolume"
"github.com/rackspace/gophercloud/rackspace/compute/v2/bootfromvolume"
"github.com/rackspace/gophercloud/rackspace/compute/v2/servers"
th "github.com/rackspace/gophercloud/testhelper"
- "github.com/smashwilson/gophercloud/acceptance/tools"
)
func TestBootFromVolume(t *testing.T) {
@@ -41,6 +41,9 @@
}).Extract()
th.AssertNoErr(t, err)
t.Logf("Created server: %+v\n", server)
- //defer deleteServer(t, client, server)
- t.Logf("Deleting server [%s]...", name)
+ defer deleteServer(t, client, server)
+
+ getServer(t, client, server)
+
+ listServers(t, client)
}
diff --git a/acceptance/rackspace/compute/v2/servers_test.go b/acceptance/rackspace/compute/v2/servers_test.go
index 511f0a9..81c8599 100644
--- a/acceptance/rackspace/compute/v2/servers_test.go
+++ b/acceptance/rackspace/compute/v2/servers_test.go
@@ -39,11 +39,14 @@
name := tools.RandomString("Gophercloud-", 8)
+ pwd := tools.MakeNewPassword("")
+
opts := &servers.CreateOpts{
Name: name,
ImageRef: options.imageID,
FlavorRef: options.flavorID,
DiskConfig: diskconfig.Manual,
+ AdminPass: pwd,
}
if keyName != "" {
@@ -59,6 +62,8 @@
th.AssertNoErr(t, err)
t.Logf("Server created successfully.")
+ th.CheckEquals(t, pwd, s.AdminPass)
+
return s
}
@@ -120,7 +125,7 @@
original := server.AdminPass
t.Logf("Changing server password.")
- err := servers.ChangeAdminPassword(client, server.ID, tools.MakeNewPassword(original)).Extract()
+ err := servers.ChangeAdminPassword(client, server.ID, tools.MakeNewPassword(original)).ExtractErr()
th.AssertNoErr(t, err)
err = servers.WaitForStatus(client, server.ID, "ACTIVE", 300)
@@ -131,7 +136,7 @@
func rebootServer(t *testing.T, client *gophercloud.ServiceClient, server *os.Server) {
t.Logf("> servers.Reboot")
- err := servers.Reboot(client, server.ID, os.HardReboot).Extract()
+ err := servers.Reboot(client, server.ID, os.HardReboot).ExtractErr()
th.AssertNoErr(t, err)
err = servers.WaitForStatus(client, server.ID, "ACTIVE", 300)
diff --git a/acceptance/rackspace/identity/v2/pkg.go b/acceptance/rackspace/identity/v2/pkg.go
new file mode 100644
index 0000000..5ec3cc8
--- /dev/null
+++ b/acceptance/rackspace/identity/v2/pkg.go
@@ -0,0 +1 @@
+package v2
diff --git a/acceptance/rackspace/identity/v2/role_test.go b/acceptance/rackspace/identity/v2/role_test.go
new file mode 100644
index 0000000..efaeb75
--- /dev/null
+++ b/acceptance/rackspace/identity/v2/role_test.go
@@ -0,0 +1,59 @@
+// +build acceptance identity roles
+
+package v2
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ os "github.com/rackspace/gophercloud/openstack/identity/v2/extensions/admin/roles"
+
+ "github.com/rackspace/gophercloud/pagination"
+ "github.com/rackspace/gophercloud/rackspace/identity/v2/roles"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestRoles(t *testing.T) {
+ client := authenticatedClient(t)
+
+ userID := createUser(t, client)
+ roleID := listRoles(t, client)
+
+ addUserRole(t, client, userID, roleID)
+
+ deleteUserRole(t, client, userID, roleID)
+
+ deleteUser(t, client, userID)
+}
+
+func listRoles(t *testing.T, client *gophercloud.ServiceClient) string {
+ var roleID string
+
+ err := roles.List(client).EachPage(func(page pagination.Page) (bool, error) {
+ roleList, err := os.ExtractRoles(page)
+ th.AssertNoErr(t, err)
+
+ for _, role := range roleList {
+ t.Logf("Listing role: ID [%s] Name [%s]", role.ID, role.Name)
+ roleID = role.ID
+ }
+
+ return true, nil
+ })
+
+ th.AssertNoErr(t, err)
+
+ return roleID
+}
+
+func addUserRole(t *testing.T, client *gophercloud.ServiceClient, userID, roleID string) {
+ err := roles.AddUserRole(client, userID, roleID).ExtractErr()
+ th.AssertNoErr(t, err)
+ t.Logf("Added role %s to user %s", roleID, userID)
+}
+
+func deleteUserRole(t *testing.T, client *gophercloud.ServiceClient, userID, roleID string) {
+ err := roles.DeleteUserRole(client, userID, roleID).ExtractErr()
+ th.AssertNoErr(t, err)
+ t.Logf("Removed role %s from user %s", roleID, userID)
+}
diff --git a/acceptance/rackspace/identity/v2/user_test.go b/acceptance/rackspace/identity/v2/user_test.go
new file mode 100644
index 0000000..28c0c83
--- /dev/null
+++ b/acceptance/rackspace/identity/v2/user_test.go
@@ -0,0 +1,93 @@
+// +build acceptance identity
+
+package v2
+
+import (
+ "strconv"
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/acceptance/tools"
+ os "github.com/rackspace/gophercloud/openstack/identity/v2/users"
+ "github.com/rackspace/gophercloud/pagination"
+ "github.com/rackspace/gophercloud/rackspace/identity/v2/users"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestUsers(t *testing.T) {
+ client := authenticatedClient(t)
+
+ userID := createUser(t, client)
+
+ listUsers(t, client)
+
+ getUser(t, client, userID)
+
+ updateUser(t, client, userID)
+
+ resetApiKey(t, client, userID)
+
+ deleteUser(t, client, userID)
+}
+
+func createUser(t *testing.T, client *gophercloud.ServiceClient) string {
+ t.Log("Creating user")
+
+ opts := users.CreateOpts{
+ Username: tools.RandomString("user_", 5),
+ Enabled: os.Disabled,
+ Email: "new_user@foo.com",
+ }
+
+ user, err := users.Create(client, opts).Extract()
+ th.AssertNoErr(t, err)
+ t.Logf("Created user %s", user.ID)
+
+ return user.ID
+}
+
+func listUsers(t *testing.T, client *gophercloud.ServiceClient) {
+ err := users.List(client).EachPage(func(page pagination.Page) (bool, error) {
+ userList, err := os.ExtractUsers(page)
+ th.AssertNoErr(t, err)
+
+ for _, user := range userList {
+ t.Logf("Listing user: ID [%s] Username [%s] Email [%s] Enabled? [%s]",
+ user.ID, user.Username, user.Email, strconv.FormatBool(user.Enabled))
+ }
+
+ return true, nil
+ })
+
+ th.AssertNoErr(t, err)
+}
+
+func getUser(t *testing.T, client *gophercloud.ServiceClient, userID string) {
+ _, err := users.Get(client, userID).Extract()
+ th.AssertNoErr(t, err)
+ t.Logf("Getting user %s", userID)
+}
+
+func updateUser(t *testing.T, client *gophercloud.ServiceClient, userID string) {
+ opts := users.UpdateOpts{Username: tools.RandomString("new_name", 5), Email: "new@foo.com"}
+ user, err := users.Update(client, userID, opts).Extract()
+ th.AssertNoErr(t, err)
+ t.Logf("Updated user %s: Username [%s] Email [%s]", userID, user.Username, user.Email)
+}
+
+func deleteUser(t *testing.T, client *gophercloud.ServiceClient, userID string) {
+ res := users.Delete(client, userID)
+ th.AssertNoErr(t, res.Err)
+ t.Logf("Deleted user %s", userID)
+}
+
+func resetApiKey(t *testing.T, client *gophercloud.ServiceClient, userID string) {
+ key, err := users.ResetAPIKey(client, userID).Extract()
+ th.AssertNoErr(t, err)
+
+ if key.APIKey == "" {
+ t.Fatal("failed to reset API key for user")
+ }
+
+ t.Logf("Reset API key for user %s to %s", key.Username, key.APIKey)
+}
diff --git a/acceptance/rackspace/lb/v1/acl_test.go b/acceptance/rackspace/lb/v1/acl_test.go
new file mode 100644
index 0000000..7a38027
--- /dev/null
+++ b/acceptance/rackspace/lb/v1/acl_test.go
@@ -0,0 +1,94 @@
+// +build acceptance lbs
+
+package v1
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+ "github.com/rackspace/gophercloud/rackspace/lb/v1/acl"
+ "github.com/rackspace/gophercloud/rackspace/lb/v1/lbs"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestACL(t *testing.T) {
+ client := setup(t)
+
+ ids := createLB(t, client, 1)
+ lbID := ids[0]
+
+ createACL(t, client, lbID)
+
+ waitForLB(client, lbID, lbs.ACTIVE)
+
+ networkIDs := showACL(t, client, lbID)
+
+ deleteNetworkItem(t, client, lbID, networkIDs[0])
+ waitForLB(client, lbID, lbs.ACTIVE)
+
+ bulkDeleteACL(t, client, lbID, networkIDs[1:2])
+ waitForLB(client, lbID, lbs.ACTIVE)
+
+ deleteACL(t, client, lbID)
+ waitForLB(client, lbID, lbs.ACTIVE)
+
+ deleteLB(t, client, lbID)
+}
+
+func createACL(t *testing.T, client *gophercloud.ServiceClient, lbID int) {
+ opts := acl.CreateOpts{
+ acl.CreateOpt{Address: "206.160.163.21", Type: acl.DENY},
+ acl.CreateOpt{Address: "206.160.165.11", Type: acl.DENY},
+ acl.CreateOpt{Address: "206.160.165.12", Type: acl.DENY},
+ acl.CreateOpt{Address: "206.160.165.13", Type: acl.ALLOW},
+ }
+
+ err := acl.Create(client, lbID, opts).ExtractErr()
+ th.AssertNoErr(t, err)
+
+ t.Logf("Created ACL items for LB %d", lbID)
+}
+
+func showACL(t *testing.T, client *gophercloud.ServiceClient, lbID int) []int {
+ ids := []int{}
+
+ err := acl.List(client, lbID).EachPage(func(page pagination.Page) (bool, error) {
+ accessList, err := acl.ExtractAccessList(page)
+ th.AssertNoErr(t, err)
+
+ for _, i := range accessList {
+ t.Logf("Listing network item: ID [%s] Address [%s] Type [%s]", i.ID, i.Address, i.Type)
+ ids = append(ids, i.ID)
+ }
+
+ return true, nil
+ })
+ th.AssertNoErr(t, err)
+
+ return ids
+}
+
+func deleteNetworkItem(t *testing.T, client *gophercloud.ServiceClient, lbID, itemID int) {
+ err := acl.Delete(client, lbID, itemID).ExtractErr()
+
+ th.AssertNoErr(t, err)
+
+ t.Logf("Deleted network item %d", itemID)
+}
+
+func bulkDeleteACL(t *testing.T, client *gophercloud.ServiceClient, lbID int, items []int) {
+ err := acl.BulkDelete(client, lbID, items).ExtractErr()
+
+ th.AssertNoErr(t, err)
+
+ t.Logf("Deleted network items %s", intsToStr(items))
+}
+
+func deleteACL(t *testing.T, client *gophercloud.ServiceClient, lbID int) {
+ err := acl.DeleteAll(client, lbID).ExtractErr()
+
+ th.AssertNoErr(t, err)
+
+ t.Logf("Deleted ACL from LB %d", lbID)
+}
diff --git a/acceptance/rackspace/lb/v1/common.go b/acceptance/rackspace/lb/v1/common.go
new file mode 100644
index 0000000..4ce05e6
--- /dev/null
+++ b/acceptance/rackspace/lb/v1/common.go
@@ -0,0 +1,62 @@
+// +build acceptance lbs
+
+package v1
+
+import (
+ "os"
+ "strconv"
+ "strings"
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/acceptance/tools"
+ "github.com/rackspace/gophercloud/rackspace"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func newProvider() (*gophercloud.ProviderClient, error) {
+ opts, err := rackspace.AuthOptionsFromEnv()
+ if err != nil {
+ return nil, err
+ }
+ opts = tools.OnlyRS(opts)
+
+ return rackspace.AuthenticatedClient(opts)
+}
+
+func newClient() (*gophercloud.ServiceClient, error) {
+ provider, err := newProvider()
+ if err != nil {
+ return nil, err
+ }
+
+ return rackspace.NewLBV1(provider, gophercloud.EndpointOpts{
+ Region: os.Getenv("RS_REGION"),
+ })
+}
+
+func newComputeClient() (*gophercloud.ServiceClient, error) {
+ provider, err := newProvider()
+ if err != nil {
+ return nil, err
+ }
+
+ return rackspace.NewComputeV2(provider, gophercloud.EndpointOpts{
+ Region: os.Getenv("RS_REGION"),
+ })
+}
+
+func setup(t *testing.T) *gophercloud.ServiceClient {
+ client, err := newClient()
+ th.AssertNoErr(t, err)
+
+ return client
+}
+
+func intsToStr(ids []int) string {
+ strIDs := []string{}
+ for _, id := range ids {
+ strIDs = append(strIDs, strconv.Itoa(id))
+ }
+ return strings.Join(strIDs, ", ")
+}
diff --git a/acceptance/rackspace/lb/v1/lb_test.go b/acceptance/rackspace/lb/v1/lb_test.go
new file mode 100644
index 0000000..c67ddec
--- /dev/null
+++ b/acceptance/rackspace/lb/v1/lb_test.go
@@ -0,0 +1,214 @@
+// +build acceptance lbs
+
+package v1
+
+import (
+ "strconv"
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/acceptance/tools"
+ "github.com/rackspace/gophercloud/pagination"
+ "github.com/rackspace/gophercloud/rackspace/lb/v1/lbs"
+ "github.com/rackspace/gophercloud/rackspace/lb/v1/vips"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestLBs(t *testing.T) {
+ client := setup(t)
+
+ ids := createLB(t, client, 3)
+ id := ids[0]
+
+ listLBProtocols(t, client)
+
+ listLBAlgorithms(t, client)
+
+ listLBs(t, client)
+
+ getLB(t, client, id)
+
+ checkLBLogging(t, client, id)
+
+ checkErrorPage(t, client, id)
+
+ getStats(t, client, id)
+
+ updateLB(t, client, id)
+
+ deleteLB(t, client, id)
+
+ batchDeleteLBs(t, client, ids[1:])
+}
+
+func createLB(t *testing.T, client *gophercloud.ServiceClient, count int) []int {
+ ids := []int{}
+
+ for i := 0; i < count; i++ {
+ opts := lbs.CreateOpts{
+ Name: tools.RandomString("test_", 5),
+ Port: 80,
+ Protocol: "HTTP",
+ VIPs: []vips.VIP{
+ vips.VIP{Type: vips.PUBLIC},
+ },
+ }
+
+ lb, err := lbs.Create(client, opts).Extract()
+ th.AssertNoErr(t, err)
+
+ t.Logf("Created LB %d - waiting for it to build...", lb.ID)
+ waitForLB(client, lb.ID, lbs.ACTIVE)
+ t.Logf("LB %d has reached ACTIVE state", lb.ID)
+
+ ids = append(ids, lb.ID)
+ }
+
+ return ids
+}
+
+func waitForLB(client *gophercloud.ServiceClient, id int, state lbs.Status) {
+ gophercloud.WaitFor(60, func() (bool, error) {
+ lb, err := lbs.Get(client, id).Extract()
+ if err != nil {
+ return false, err
+ }
+ if lb.Status != state {
+ return false, nil
+ }
+ return true, nil
+ })
+}
+
+func listLBProtocols(t *testing.T, client *gophercloud.ServiceClient) {
+ err := lbs.ListProtocols(client).EachPage(func(page pagination.Page) (bool, error) {
+ pList, err := lbs.ExtractProtocols(page)
+ th.AssertNoErr(t, err)
+
+ for _, p := range pList {
+ t.Logf("Listing protocol: Name [%s]", p.Name)
+ }
+
+ return true, nil
+ })
+ th.AssertNoErr(t, err)
+}
+
+func listLBAlgorithms(t *testing.T, client *gophercloud.ServiceClient) {
+ err := lbs.ListAlgorithms(client).EachPage(func(page pagination.Page) (bool, error) {
+ aList, err := lbs.ExtractAlgorithms(page)
+ th.AssertNoErr(t, err)
+
+ for _, a := range aList {
+ t.Logf("Listing algorithm: Name [%s]", a.Name)
+ }
+
+ return true, nil
+ })
+ th.AssertNoErr(t, err)
+}
+
+func listLBs(t *testing.T, client *gophercloud.ServiceClient) {
+ err := lbs.List(client, lbs.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+ lbList, err := lbs.ExtractLBs(page)
+ th.AssertNoErr(t, err)
+
+ for _, lb := range lbList {
+ t.Logf("Listing LB: ID [%d] Name [%s] Protocol [%s] Status [%s] Node count [%d] Port [%d]",
+ lb.ID, lb.Name, lb.Protocol, lb.Status, lb.NodeCount, lb.Port)
+ }
+
+ return true, nil
+ })
+
+ th.AssertNoErr(t, err)
+}
+
+func getLB(t *testing.T, client *gophercloud.ServiceClient, id int) {
+ lb, err := lbs.Get(client, id).Extract()
+ th.AssertNoErr(t, err)
+ t.Logf("Getting LB %d: Created [%s] VIPs [%#v] Logging [%#v] Persistence [%#v] SourceAddrs [%#v]",
+ lb.ID, lb.Created, lb.VIPs, lb.ConnectionLogging, lb.SessionPersistence, lb.SourceAddrs)
+}
+
+func updateLB(t *testing.T, client *gophercloud.ServiceClient, id int) {
+ opts := lbs.UpdateOpts{
+ Name: tools.RandomString("new_", 5),
+ Protocol: "TCP",
+ HalfClosed: gophercloud.Enabled,
+ Algorithm: "RANDOM",
+ Port: 8080,
+ Timeout: 100,
+ HTTPSRedirect: gophercloud.Disabled,
+ }
+
+ err := lbs.Update(client, id, opts).ExtractErr()
+ th.AssertNoErr(t, err)
+
+ t.Logf("Updating LB %d - waiting for it to finish", id)
+ waitForLB(client, id, lbs.ACTIVE)
+ t.Logf("LB %d has reached ACTIVE state", id)
+}
+
+func deleteLB(t *testing.T, client *gophercloud.ServiceClient, id int) {
+ err := lbs.Delete(client, id).ExtractErr()
+ th.AssertNoErr(t, err)
+ t.Logf("Deleted LB %d", id)
+}
+
+func batchDeleteLBs(t *testing.T, client *gophercloud.ServiceClient, ids []int) {
+ err := lbs.BulkDelete(client, ids).ExtractErr()
+ th.AssertNoErr(t, err)
+ t.Logf("Deleted LB %s", intsToStr(ids))
+}
+
+func checkLBLogging(t *testing.T, client *gophercloud.ServiceClient, id int) {
+ err := lbs.EnableLogging(client, id).ExtractErr()
+ th.AssertNoErr(t, err)
+ t.Logf("Enabled logging for LB %d", id)
+
+ res, err := lbs.IsLoggingEnabled(client, id)
+ th.AssertNoErr(t, err)
+ t.Logf("LB %d log enabled? %s", id, strconv.FormatBool(res))
+
+ waitForLB(client, id, lbs.ACTIVE)
+
+ err = lbs.DisableLogging(client, id).ExtractErr()
+ th.AssertNoErr(t, err)
+ t.Logf("Disabled logging for LB %d", id)
+}
+
+func checkErrorPage(t *testing.T, client *gophercloud.ServiceClient, id int) {
+ content, err := lbs.SetErrorPage(client, id, "<html>New content!</html>").Extract()
+ t.Logf("Set error page for LB %d", id)
+
+ content, err = lbs.GetErrorPage(client, id).Extract()
+ th.AssertNoErr(t, err)
+ t.Logf("Error page for LB %d: %s", id, content)
+
+ err = lbs.DeleteErrorPage(client, id).ExtractErr()
+ t.Logf("Deleted error page for LB %d", id)
+}
+
+func getStats(t *testing.T, client *gophercloud.ServiceClient, id int) {
+ waitForLB(client, id, lbs.ACTIVE)
+
+ stats, err := lbs.GetStats(client, id).Extract()
+ th.AssertNoErr(t, err)
+
+ t.Logf("Stats for LB %d: %#v", id, stats)
+}
+
+func checkCaching(t *testing.T, client *gophercloud.ServiceClient, id int) {
+ err := lbs.EnableCaching(client, id).ExtractErr()
+ th.AssertNoErr(t, err)
+ t.Logf("Enabled caching for LB %d", id)
+
+ res, err := lbs.IsContentCached(client, id)
+ th.AssertNoErr(t, err)
+ t.Logf("Is caching enabled for LB? %s", strconv.FormatBool(res))
+
+ err = lbs.DisableCaching(client, id).ExtractErr()
+ th.AssertNoErr(t, err)
+ t.Logf("Disabled caching for LB %d", id)
+}
diff --git a/acceptance/rackspace/lb/v1/monitor_test.go b/acceptance/rackspace/lb/v1/monitor_test.go
new file mode 100644
index 0000000..c1a8e24
--- /dev/null
+++ b/acceptance/rackspace/lb/v1/monitor_test.go
@@ -0,0 +1,60 @@
+// +build acceptance lbs
+
+package v1
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/rackspace/lb/v1/lbs"
+ "github.com/rackspace/gophercloud/rackspace/lb/v1/monitors"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestMonitors(t *testing.T) {
+ client := setup(t)
+
+ ids := createLB(t, client, 1)
+ lbID := ids[0]
+
+ getMonitor(t, client, lbID)
+
+ updateMonitor(t, client, lbID)
+
+ deleteMonitor(t, client, lbID)
+
+ deleteLB(t, client, lbID)
+}
+
+func getMonitor(t *testing.T, client *gophercloud.ServiceClient, lbID int) {
+ hm, err := monitors.Get(client, lbID).Extract()
+ th.AssertNoErr(t, err)
+ t.Logf("Health monitor for LB %d: Type [%s] Delay [%d] Timeout [%d] AttemptLimit [%d]",
+ lbID, hm.Type, hm.Delay, hm.Timeout, hm.AttemptLimit)
+}
+
+func updateMonitor(t *testing.T, client *gophercloud.ServiceClient, lbID int) {
+ opts := monitors.UpdateHTTPMonitorOpts{
+ AttemptLimit: 3,
+ Delay: 10,
+ Timeout: 10,
+ BodyRegex: "hello is it me you're looking for",
+ Path: "/foo",
+ StatusRegex: "200",
+ Type: monitors.HTTP,
+ }
+
+ err := monitors.Update(client, lbID, opts).ExtractErr()
+ th.AssertNoErr(t, err)
+
+ waitForLB(client, lbID, lbs.ACTIVE)
+ t.Logf("Updated monitor for LB %d", lbID)
+}
+
+func deleteMonitor(t *testing.T, client *gophercloud.ServiceClient, lbID int) {
+ err := monitors.Delete(client, lbID).ExtractErr()
+ th.AssertNoErr(t, err)
+
+ waitForLB(client, lbID, lbs.ACTIVE)
+ t.Logf("Deleted monitor for LB %d", lbID)
+}
diff --git a/acceptance/rackspace/lb/v1/node_test.go b/acceptance/rackspace/lb/v1/node_test.go
new file mode 100644
index 0000000..18b9fe7
--- /dev/null
+++ b/acceptance/rackspace/lb/v1/node_test.go
@@ -0,0 +1,175 @@
+// +build acceptance lbs
+
+package v1
+
+import (
+ "os"
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/acceptance/tools"
+ "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/diskconfig"
+ "github.com/rackspace/gophercloud/pagination"
+ "github.com/rackspace/gophercloud/rackspace/compute/v2/servers"
+ "github.com/rackspace/gophercloud/rackspace/lb/v1/lbs"
+ "github.com/rackspace/gophercloud/rackspace/lb/v1/nodes"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestNodes(t *testing.T) {
+ client := setup(t)
+
+ serverIP := findServer(t)
+ ids := createLB(t, client, 1)
+ lbID := ids[0]
+
+ nodeID := addNodes(t, client, lbID, serverIP)
+
+ listNodes(t, client, lbID)
+
+ getNode(t, client, lbID, nodeID)
+
+ updateNode(t, client, lbID, nodeID)
+
+ listEvents(t, client, lbID)
+
+ deleteNode(t, client, lbID, nodeID)
+
+ waitForLB(client, lbID, lbs.ACTIVE)
+ deleteLB(t, client, lbID)
+}
+
+func findServer(t *testing.T) string {
+ var serverIP string
+
+ client, err := newComputeClient()
+ th.AssertNoErr(t, err)
+
+ err = servers.List(client, nil).EachPage(func(page pagination.Page) (bool, error) {
+ sList, err := servers.ExtractServers(page)
+ th.AssertNoErr(t, err)
+
+ for _, s := range sList {
+ serverIP = s.AccessIPv4
+ t.Logf("Found an existing server: ID [%s] Public IP [%s]", s.ID, serverIP)
+ break
+ }
+
+ return true, nil
+ })
+ th.AssertNoErr(t, err)
+
+ if serverIP == "" {
+ t.Log("No server found, creating one")
+
+ imageRef := os.Getenv("RS_IMAGE_ID")
+ if imageRef == "" {
+ t.Fatalf("OS var RS_IMAGE_ID undefined")
+ }
+ flavorRef := os.Getenv("RS_FLAVOR_ID")
+ if flavorRef == "" {
+ t.Fatalf("OS var RS_FLAVOR_ID undefined")
+ }
+
+ opts := &servers.CreateOpts{
+ Name: tools.RandomString("lb_test_", 5),
+ ImageRef: imageRef,
+ FlavorRef: flavorRef,
+ DiskConfig: diskconfig.Manual,
+ }
+
+ s, err := servers.Create(client, opts).Extract()
+ th.AssertNoErr(t, err)
+ serverIP = s.AccessIPv4
+
+ t.Logf("Created server %s, waiting for it to build", s.ID)
+ err = servers.WaitForStatus(client, s.ID, "ACTIVE", 300)
+ th.AssertNoErr(t, err)
+ t.Logf("Server created successfully.")
+ }
+
+ return serverIP
+}
+
+func addNodes(t *testing.T, client *gophercloud.ServiceClient, lbID int, serverIP string) int {
+ opts := nodes.CreateOpts{
+ nodes.CreateOpt{
+ Address: serverIP,
+ Port: 80,
+ Condition: nodes.ENABLED,
+ Type: nodes.PRIMARY,
+ },
+ }
+
+ page := nodes.Create(client, lbID, opts)
+
+ nodeList, err := page.ExtractNodes()
+ th.AssertNoErr(t, err)
+
+ var nodeID int
+ for _, n := range nodeList {
+ nodeID = n.ID
+ }
+ if nodeID == 0 {
+ t.Fatalf("nodeID could not be extracted from create response")
+ }
+
+ t.Logf("Added node %d to LB %d", nodeID, lbID)
+ waitForLB(client, lbID, lbs.ACTIVE)
+
+ return nodeID
+}
+
+func listNodes(t *testing.T, client *gophercloud.ServiceClient, lbID int) {
+ err := nodes.List(client, lbID, nil).EachPage(func(page pagination.Page) (bool, error) {
+ nodeList, err := nodes.ExtractNodes(page)
+ th.AssertNoErr(t, err)
+
+ for _, n := range nodeList {
+ t.Logf("Listing node: ID [%d] Address [%s:%d] Status [%s]", n.ID, n.Address, n.Port, n.Status)
+ }
+
+ return true, nil
+ })
+ th.AssertNoErr(t, err)
+}
+
+func getNode(t *testing.T, client *gophercloud.ServiceClient, lbID int, nodeID int) {
+ node, err := nodes.Get(client, lbID, nodeID).Extract()
+ th.AssertNoErr(t, err)
+ t.Logf("Getting node %d: Type [%s] Weight [%d]", nodeID, node.Type, node.Weight)
+}
+
+func updateNode(t *testing.T, client *gophercloud.ServiceClient, lbID int, nodeID int) {
+ opts := nodes.UpdateOpts{
+ Weight: gophercloud.IntToPointer(10),
+ Condition: nodes.DRAINING,
+ Type: nodes.SECONDARY,
+ }
+ err := nodes.Update(client, lbID, nodeID, opts).ExtractErr()
+ th.AssertNoErr(t, err)
+ t.Logf("Updated node %d", nodeID)
+ waitForLB(client, lbID, lbs.ACTIVE)
+}
+
+func listEvents(t *testing.T, client *gophercloud.ServiceClient, lbID int) {
+ pager := nodes.ListEvents(client, lbID, nodes.ListEventsOpts{})
+ err := pager.EachPage(func(page pagination.Page) (bool, error) {
+ eventList, err := nodes.ExtractNodeEvents(page)
+ th.AssertNoErr(t, err)
+
+ for _, e := range eventList {
+ t.Logf("Listing events for node %d: Type [%s] Msg [%s] Severity [%s] Date [%s]",
+ e.NodeID, e.Type, e.DetailedMessage, e.Severity, e.Created)
+ }
+
+ return true, nil
+ })
+ th.AssertNoErr(t, err)
+}
+
+func deleteNode(t *testing.T, client *gophercloud.ServiceClient, lbID int, nodeID int) {
+ err := nodes.Delete(client, lbID, nodeID).ExtractErr()
+ th.AssertNoErr(t, err)
+ t.Logf("Deleted node %d", nodeID)
+}
diff --git a/acceptance/rackspace/lb/v1/session_test.go b/acceptance/rackspace/lb/v1/session_test.go
new file mode 100644
index 0000000..8d85655
--- /dev/null
+++ b/acceptance/rackspace/lb/v1/session_test.go
@@ -0,0 +1,47 @@
+// +build acceptance lbs
+
+package v1
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/rackspace/lb/v1/sessions"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestSession(t *testing.T) {
+ client := setup(t)
+
+ ids := createLB(t, client, 1)
+ lbID := ids[0]
+
+ getSession(t, client, lbID)
+
+ enableSession(t, client, lbID)
+ waitForLB(client, lbID, "ACTIVE")
+
+ disableSession(t, client, lbID)
+ waitForLB(client, lbID, "ACTIVE")
+
+ deleteLB(t, client, lbID)
+}
+
+func getSession(t *testing.T, client *gophercloud.ServiceClient, lbID int) {
+ sp, err := sessions.Get(client, lbID).Extract()
+ th.AssertNoErr(t, err)
+ t.Logf("Session config: Type [%s]", sp.Type)
+}
+
+func enableSession(t *testing.T, client *gophercloud.ServiceClient, lbID int) {
+ opts := sessions.CreateOpts{Type: sessions.HTTPCOOKIE}
+ err := sessions.Enable(client, lbID, opts).ExtractErr()
+ th.AssertNoErr(t, err)
+ t.Logf("Enable %s sessions for %d", opts.Type, lbID)
+}
+
+func disableSession(t *testing.T, client *gophercloud.ServiceClient, lbID int) {
+ err := sessions.Disable(client, lbID).ExtractErr()
+ th.AssertNoErr(t, err)
+ t.Logf("Disable sessions for %d", lbID)
+}
diff --git a/acceptance/rackspace/lb/v1/throttle_test.go b/acceptance/rackspace/lb/v1/throttle_test.go
new file mode 100644
index 0000000..1cc1235
--- /dev/null
+++ b/acceptance/rackspace/lb/v1/throttle_test.go
@@ -0,0 +1,53 @@
+// +build acceptance lbs
+
+package v1
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/rackspace/lb/v1/throttle"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestThrottle(t *testing.T) {
+ client := setup(t)
+
+ ids := createLB(t, client, 1)
+ lbID := ids[0]
+
+ getThrottleConfig(t, client, lbID)
+
+ createThrottleConfig(t, client, lbID)
+ waitForLB(client, lbID, "ACTIVE")
+
+ deleteThrottleConfig(t, client, lbID)
+ waitForLB(client, lbID, "ACTIVE")
+
+ deleteLB(t, client, lbID)
+}
+
+func getThrottleConfig(t *testing.T, client *gophercloud.ServiceClient, lbID int) {
+ sp, err := throttle.Get(client, lbID).Extract()
+ th.AssertNoErr(t, err)
+ t.Logf("Throttle config: MaxConns [%s]", sp.MaxConnections)
+}
+
+func createThrottleConfig(t *testing.T, client *gophercloud.ServiceClient, lbID int) {
+ opts := throttle.CreateOpts{
+ MaxConnections: 200,
+ MaxConnectionRate: 100,
+ MinConnections: 0,
+ RateInterval: 10,
+ }
+
+ err := throttle.Create(client, lbID, opts).ExtractErr()
+ th.AssertNoErr(t, err)
+ t.Logf("Enable throttling for %d", lbID)
+}
+
+func deleteThrottleConfig(t *testing.T, client *gophercloud.ServiceClient, lbID int) {
+ err := throttle.Delete(client, lbID).ExtractErr()
+ th.AssertNoErr(t, err)
+ t.Logf("Disable throttling for %d", lbID)
+}
diff --git a/acceptance/rackspace/lb/v1/vip_test.go b/acceptance/rackspace/lb/v1/vip_test.go
new file mode 100644
index 0000000..bc0c2a8
--- /dev/null
+++ b/acceptance/rackspace/lb/v1/vip_test.go
@@ -0,0 +1,83 @@
+// +build acceptance lbs
+
+package v1
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+ "github.com/rackspace/gophercloud/rackspace/lb/v1/lbs"
+ "github.com/rackspace/gophercloud/rackspace/lb/v1/vips"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestVIPs(t *testing.T) {
+ client := setup(t)
+
+ ids := createLB(t, client, 1)
+ lbID := ids[0]
+
+ listVIPs(t, client, lbID)
+
+ vipIDs := addVIPs(t, client, lbID, 3)
+
+ deleteVIP(t, client, lbID, vipIDs[0])
+
+ bulkDeleteVIPs(t, client, lbID, vipIDs[1:])
+
+ waitForLB(client, lbID, lbs.ACTIVE)
+ deleteLB(t, client, lbID)
+}
+
+func listVIPs(t *testing.T, client *gophercloud.ServiceClient, lbID int) {
+ err := vips.List(client, lbID).EachPage(func(page pagination.Page) (bool, error) {
+ vipList, err := vips.ExtractVIPs(page)
+ th.AssertNoErr(t, err)
+
+ for _, vip := range vipList {
+ t.Logf("Listing VIP: ID [%s] Address [%s] Type [%s] Version [%s]",
+ vip.ID, vip.Address, vip.Type, vip.Version)
+ }
+
+ return true, nil
+ })
+ th.AssertNoErr(t, err)
+}
+
+func addVIPs(t *testing.T, client *gophercloud.ServiceClient, lbID, count int) []int {
+ ids := []int{}
+
+ for i := 0; i < count; i++ {
+ opts := vips.CreateOpts{
+ Type: vips.PUBLIC,
+ Version: vips.IPV6,
+ }
+
+ vip, err := vips.Create(client, lbID, opts).Extract()
+ th.AssertNoErr(t, err)
+
+ t.Logf("Created VIP %d", vip.ID)
+
+ waitForLB(client, lbID, lbs.ACTIVE)
+
+ ids = append(ids, vip.ID)
+ }
+
+ return ids
+}
+
+func deleteVIP(t *testing.T, client *gophercloud.ServiceClient, lbID, vipID int) {
+ err := vips.Delete(client, lbID, vipID).ExtractErr()
+ th.AssertNoErr(t, err)
+
+ t.Logf("Deleted VIP %d", vipID)
+
+ waitForLB(client, lbID, lbs.ACTIVE)
+}
+
+func bulkDeleteVIPs(t *testing.T, client *gophercloud.ServiceClient, lbID int, ids []int) {
+ err := vips.BulkDelete(client, lbID, ids).ExtractErr()
+ th.AssertNoErr(t, err)
+ t.Logf("Deleted VIPs %s", intsToStr(ids))
+}
diff --git a/acceptance/rackspace/networking/v2/common.go b/acceptance/rackspace/networking/v2/common.go
new file mode 100644
index 0000000..8170418
--- /dev/null
+++ b/acceptance/rackspace/networking/v2/common.go
@@ -0,0 +1,39 @@
+package v2
+
+import (
+ "os"
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/rackspace"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+var Client *gophercloud.ServiceClient
+
+func NewClient() (*gophercloud.ServiceClient, error) {
+ opts, err := rackspace.AuthOptionsFromEnv()
+ if err != nil {
+ return nil, err
+ }
+
+ provider, err := rackspace.AuthenticatedClient(opts)
+ if err != nil {
+ return nil, err
+ }
+
+ return rackspace.NewNetworkV2(provider, gophercloud.EndpointOpts{
+ Name: "cloudNetworks",
+ Region: os.Getenv("RS_REGION"),
+ })
+}
+
+func Setup(t *testing.T) {
+ client, err := NewClient()
+ th.AssertNoErr(t, err)
+ Client = client
+}
+
+func Teardown() {
+ Client = nil
+}
diff --git a/acceptance/rackspace/networking/v2/network_test.go b/acceptance/rackspace/networking/v2/network_test.go
new file mode 100644
index 0000000..3862123
--- /dev/null
+++ b/acceptance/rackspace/networking/v2/network_test.go
@@ -0,0 +1,65 @@
+// +build acceptance networking
+
+package v2
+
+import (
+ "strconv"
+ "testing"
+
+ os "github.com/rackspace/gophercloud/openstack/networking/v2/networks"
+ "github.com/rackspace/gophercloud/pagination"
+ "github.com/rackspace/gophercloud/rackspace/networking/v2/networks"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestNetworkCRUDOperations(t *testing.T) {
+ Setup(t)
+ defer Teardown()
+
+ // Create a network
+ n, err := networks.Create(Client, os.CreateOpts{Name: "sample_network", AdminStateUp: os.Up}).Extract()
+ th.AssertNoErr(t, err)
+ defer networks.Delete(Client, n.ID)
+ th.AssertEquals(t, "sample_network", n.Name)
+ th.AssertEquals(t, true, n.AdminStateUp)
+ networkID := n.ID
+
+ // List networks
+ pager := networks.List(Client, os.ListOpts{Limit: 2})
+ err = pager.EachPage(func(page pagination.Page) (bool, error) {
+ t.Logf("--- Page ---")
+
+ networkList, err := os.ExtractNetworks(page)
+ th.AssertNoErr(t, err)
+
+ for _, n := range networkList {
+ t.Logf("Network: ID [%s] Name [%s] Status [%s] Is shared? [%s]",
+ n.ID, n.Name, n.Status, strconv.FormatBool(n.Shared))
+ }
+
+ return true, nil
+ })
+ th.CheckNoErr(t, err)
+
+ // Get a network
+ if networkID == "" {
+ t.Fatalf("In order to retrieve a network, the NetworkID must be set")
+ }
+ n, err = networks.Get(Client, networkID).Extract()
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, "ACTIVE", n.Status)
+ th.AssertDeepEquals(t, []string{}, n.Subnets)
+ th.AssertEquals(t, "sample_network", n.Name)
+ th.AssertEquals(t, true, n.AdminStateUp)
+ th.AssertEquals(t, false, n.Shared)
+ th.AssertEquals(t, networkID, n.ID)
+
+ // Update network
+ n, err = networks.Update(Client, networkID, os.UpdateOpts{Name: "new_network_name"}).Extract()
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, "new_network_name", n.Name)
+
+ // Delete network
+ res := networks.Delete(Client, networkID)
+ th.AssertNoErr(t, res.Err)
+}
diff --git a/acceptance/rackspace/networking/v2/port_test.go b/acceptance/rackspace/networking/v2/port_test.go
new file mode 100644
index 0000000..3c42bb2
--- /dev/null
+++ b/acceptance/rackspace/networking/v2/port_test.go
@@ -0,0 +1,116 @@
+// +build acceptance networking
+
+package v2
+
+import (
+ "testing"
+
+ osNetworks "github.com/rackspace/gophercloud/openstack/networking/v2/networks"
+ osPorts "github.com/rackspace/gophercloud/openstack/networking/v2/ports"
+ osSubnets "github.com/rackspace/gophercloud/openstack/networking/v2/subnets"
+ "github.com/rackspace/gophercloud/pagination"
+ "github.com/rackspace/gophercloud/rackspace/networking/v2/networks"
+ "github.com/rackspace/gophercloud/rackspace/networking/v2/ports"
+ "github.com/rackspace/gophercloud/rackspace/networking/v2/subnets"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestPortCRUD(t *testing.T) {
+ Setup(t)
+ defer Teardown()
+
+ // Setup network
+ t.Log("Setting up network")
+ networkID, err := createNetwork()
+ th.AssertNoErr(t, err)
+ defer networks.Delete(Client, networkID)
+
+ // Setup subnet
+ t.Logf("Setting up subnet on network %s", networkID)
+ subnetID, err := createSubnet(networkID)
+ th.AssertNoErr(t, err)
+ defer subnets.Delete(Client, subnetID)
+
+ // Create port
+ t.Logf("Create port based on subnet %s", subnetID)
+ portID := createPort(t, networkID, subnetID)
+
+ // List ports
+ t.Logf("Listing all ports")
+ listPorts(t)
+
+ // Get port
+ if portID == "" {
+ t.Fatalf("In order to retrieve a port, the portID must be set")
+ }
+ p, err := ports.Get(Client, portID).Extract()
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, portID, p.ID)
+
+ // Update port
+ p, err = ports.Update(Client, portID, osPorts.UpdateOpts{Name: "new_port_name"}).Extract()
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, "new_port_name", p.Name)
+
+ // Delete port
+ res := ports.Delete(Client, portID)
+ th.AssertNoErr(t, res.Err)
+}
+
+func createPort(t *testing.T, networkID, subnetID string) string {
+ enable := true
+ opts := osPorts.CreateOpts{
+ NetworkID: networkID,
+ Name: "my_port",
+ AdminStateUp: &enable,
+ FixedIPs: []osPorts.IP{osPorts.IP{SubnetID: subnetID}},
+ }
+ p, err := ports.Create(Client, opts).Extract()
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, networkID, p.NetworkID)
+ th.AssertEquals(t, "my_port", p.Name)
+ th.AssertEquals(t, true, p.AdminStateUp)
+
+ return p.ID
+}
+
+func listPorts(t *testing.T) {
+ count := 0
+ pager := ports.List(Client, osPorts.ListOpts{})
+ err := pager.EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ t.Logf("--- Page ---")
+
+ portList, err := osPorts.ExtractPorts(page)
+ 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)
+ }
+
+ return true, nil
+ })
+
+ th.CheckNoErr(t, err)
+
+ if count == 0 {
+ t.Logf("No pages were iterated over when listing ports")
+ }
+}
+
+func createNetwork() (string, error) {
+ res, err := networks.Create(Client, osNetworks.CreateOpts{Name: "tmp_network", AdminStateUp: osNetworks.Up}).Extract()
+ return res.ID, err
+}
+
+func createSubnet(networkID string) (string, error) {
+ s, err := subnets.Create(Client, osSubnets.CreateOpts{
+ NetworkID: networkID,
+ CIDR: "192.168.199.0/24",
+ IPVersion: osSubnets.IPv4,
+ Name: "my_subnet",
+ EnableDHCP: osSubnets.Down,
+ }).Extract()
+ return s.ID, err
+}
diff --git a/acceptance/rackspace/networking/v2/subnet_test.go b/acceptance/rackspace/networking/v2/subnet_test.go
new file mode 100644
index 0000000..c401432
--- /dev/null
+++ b/acceptance/rackspace/networking/v2/subnet_test.go
@@ -0,0 +1,84 @@
+// +build acceptance networking
+
+package v2
+
+import (
+ "testing"
+
+ osNetworks "github.com/rackspace/gophercloud/openstack/networking/v2/networks"
+ osSubnets "github.com/rackspace/gophercloud/openstack/networking/v2/subnets"
+ "github.com/rackspace/gophercloud/pagination"
+ "github.com/rackspace/gophercloud/rackspace/networking/v2/networks"
+ "github.com/rackspace/gophercloud/rackspace/networking/v2/subnets"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestListSubnets(t *testing.T) {
+ Setup(t)
+ defer Teardown()
+
+ pager := subnets.List(Client, osSubnets.ListOpts{Limit: 2})
+ err := pager.EachPage(func(page pagination.Page) (bool, error) {
+ t.Logf("--- Page ---")
+
+ subnetList, err := osSubnets.ExtractSubnets(page)
+ th.AssertNoErr(t, err)
+
+ for _, s := range subnetList {
+ t.Logf("Subnet: ID [%s] Name [%s] IP Version [%d] CIDR [%s] GatewayIP [%s]",
+ s.ID, s.Name, s.IPVersion, s.CIDR, s.GatewayIP)
+ }
+
+ return true, nil
+ })
+ th.CheckNoErr(t, err)
+}
+
+func TestSubnetCRUD(t *testing.T) {
+ Setup(t)
+ defer Teardown()
+
+ // Setup network
+ t.Log("Setting up network")
+ n, err := networks.Create(Client, osNetworks.CreateOpts{Name: "tmp_network", AdminStateUp: osNetworks.Up}).Extract()
+ th.AssertNoErr(t, err)
+ networkID := n.ID
+ defer networks.Delete(Client, networkID)
+
+ // Create subnet
+ t.Log("Create subnet")
+ enable := false
+ opts := osSubnets.CreateOpts{
+ NetworkID: networkID,
+ CIDR: "192.168.199.0/24",
+ IPVersion: osSubnets.IPv4,
+ Name: "my_subnet",
+ EnableDHCP: &enable,
+ }
+ s, err := subnets.Create(Client, opts).Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, networkID, s.NetworkID)
+ th.AssertEquals(t, "192.168.199.0/24", s.CIDR)
+ th.AssertEquals(t, 4, s.IPVersion)
+ th.AssertEquals(t, "my_subnet", s.Name)
+ th.AssertEquals(t, false, s.EnableDHCP)
+ subnetID := s.ID
+
+ // Get subnet
+ t.Log("Getting subnet")
+ s, err = subnets.Get(Client, subnetID).Extract()
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, subnetID, s.ID)
+
+ // Update subnet
+ t.Log("Update subnet")
+ s, err = subnets.Update(Client, subnetID, osSubnets.UpdateOpts{Name: "new_subnet_name"}).Extract()
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, "new_subnet_name", s.Name)
+
+ // Delete subnet
+ t.Log("Delete subnet")
+ res := subnets.Delete(Client, subnetID)
+ th.AssertNoErr(t, res.Err)
+}
diff --git a/acceptance/tools/tools.go b/acceptance/tools/tools.go
index 61b1d7a..35679b7 100644
--- a/acceptance/tools/tools.go
+++ b/acceptance/tools/tools.go
@@ -1,10 +1,11 @@
-// +build acceptance
+// +build acceptance common
package tools
import (
"crypto/rand"
"errors"
+ mrand "math/rand"
"os"
"time"
@@ -72,6 +73,12 @@
return prefix + string(bytes)
}
+// RandomInt will return a random integer between a specified range.
+func RandomInt(min, max int) int {
+ mrand.Seed(time.Now().Unix())
+ return mrand.Intn(max-min) + min
+}
+
// Elide returns the first bit of its input string with a suffix of "..." if it's longer than
// a comfortable 40 characters.
func Elide(value string) string {
diff --git a/auth_options.go b/auth_options.go
index bc0ef65..19ce5d4 100644
--- a/auth_options.go
+++ b/auth_options.go
@@ -1,20 +1,28 @@
package gophercloud
-// AuthOptions allows anyone calling Authenticate to supply the required access
-// credentials. Its fields are the union of those recognized by each identity
-// implementation and provider.
+/*
+AuthOptions stores information needed to authenticate to an OpenStack cluster.
+You can populate one manually, or use a provider's AuthOptionsFromEnv() function
+to read relevant information from the standard environment variables. Pass one
+to a provider's AuthenticatedClient function to authenticate and obtain a
+ProviderClient representing an active session on that provider.
+
+Its fields are the union of those recognized by each identity implementation and
+provider.
+*/
type AuthOptions struct {
// IdentityEndpoint specifies the HTTP endpoint that is required to work with
- // the Identity API of the appropriate version. Required by the identity
- // services, but often populated by a provider Client.
+ // the Identity API of the appropriate version. While it's ultimately needed by
+ // all of the identity services, it will often be populated by a provider-level
+ // function.
IdentityEndpoint string
// Username is required if using Identity V2 API. Consult with your provider's
// control panel to discover your account's username. In Identity V3, either
- // UserID or a combination of Username and DomainID or DomainName.
+ // UserID or a combination of Username and DomainID or DomainName are needed.
Username, UserID string
- // Exactly one of Password or ApiKey is required for the Identity V2 and V3
+ // Exactly one of Password or APIKey is required for the Identity V2 and V3
// APIs. Consult with your provider's control panel to discover your account's
// preferred method of authentication.
Password, APIKey string
@@ -25,7 +33,7 @@
// The TenantID and TenantName fields are optional for the Identity V2 API.
// Some providers allow you to specify a TenantName instead of the TenantId.
- // Some require both. Your provider's authentication policies will determine
+ // Some require both. Your provider's authentication policies will determine
// how these fields influence authentication.
TenantID, TenantName string
@@ -34,5 +42,7 @@
// re-authenticate automatically if/when your token expires. If you set it to
// false, it will not cache these settings, but re-authentication will not be
// possible. This setting defaults to false.
+ //
+ // This setting is speculative and is currently not respected!
AllowReauth bool
}
diff --git a/auth_results.go b/auth_results.go
index 1a1faa5..856a233 100644
--- a/auth_results.go
+++ b/auth_results.go
@@ -2,10 +2,9 @@
import "time"
-// AuthResults encapsulates the raw results from an authentication request. As OpenStack allows
-// extensions to influence the structure returned in ways that Gophercloud cannot predict at
-// compile-time, you should use type-safe accessors to work with the data represented by this type,
-// such as ServiceCatalog and TokenID.
+// AuthResults [deprecated] is a leftover type from the v0.x days. It was
+// intended to describe common functionality among identity service results, but
+// is not actually used anywhere.
type AuthResults interface {
// TokenID returns the token's ID value from the authentication response.
TokenID() (string, error)
diff --git a/doc.go b/doc.go
new file mode 100644
index 0000000..fb81a9d
--- /dev/null
+++ b/doc.go
@@ -0,0 +1,67 @@
+/*
+Package gophercloud provides a multi-vendor interface to OpenStack-compatible
+clouds. The library has a three-level hierarchy: providers, services, and
+resources.
+
+Provider structs represent the service providers that offer and manage a
+collection of services. Examples of providers include: OpenStack, Rackspace,
+HP. These are defined like so:
+
+ opts := gophercloud.AuthOptions{
+ IdentityEndpoint: "https://my-openstack.com:5000/v2.0",
+ Username: "{username}",
+ Password: "{password}",
+ TenantID: "{tenant_id}",
+ }
+
+ provider, err := openstack.AuthenticatedClient(opts)
+
+Service structs are specific to a provider and handle all of the logic and
+operations for a particular OpenStack service. Examples of services include:
+Compute, Object Storage, Block Storage. In order to define one, you need to
+pass in the parent provider, like so:
+
+ opts := gophercloud.EndpointOpts{Region: "RegionOne"}
+
+ client := openstack.NewComputeV2(provider, opts)
+
+Resource structs are the domain models that services make use of in order
+to work with and represent the state of API resources:
+
+ server, err := servers.Get(client, "{serverId}").Extract()
+
+Intermediate Result structs are returned for API operations, which allow
+generic access to the HTTP headers, response body, and any errors associated
+with the network transaction. To turn a result into a usable resource struct,
+you must call the Extract method which is chained to the response, or an
+Extract function from an applicable extension:
+
+ result := servers.Get(client, "{serverId}")
+
+ // Attempt to extract the disk configuration from the OS-DCF disk config
+ // extension:
+ config, err := diskconfig.ExtractGet(result)
+
+All requests that enumerate a collection return a Pager struct that is used to
+iterate through the results one page at a time. Use the EachPage method on that
+Pager to handle each successive Page in a closure, then use the appropriate
+extraction method from that request's package to interpret that Page as a slice
+of results:
+
+ err := servers.List(client, nil).EachPage(func (page pagination.Page) (bool, error) {
+ s, err := servers.ExtractServers(page)
+ if err != nil {
+ return false, err
+ }
+
+ // Handle the []servers.Server slice.
+
+ // Return "false" or an error to prematurely stop fetching new pages.
+ return true, nil
+ })
+
+This top-level package contains utility functions and data types that are used
+throughout the provider and service packages. Of particular note for end users
+are the AuthOptions and EndpointOpts structs.
+*/
+package gophercloud
diff --git a/endpoint_search.go b/endpoint_search.go
index b6f6b48..5189431 100644
--- a/endpoint_search.go
+++ b/endpoint_search.go
@@ -3,58 +3,85 @@
import "errors"
var (
- // ErrServiceNotFound is returned when no service matches the EndpointOpts.
+ // ErrServiceNotFound is returned when no service in a service catalog matches
+ // the provided EndpointOpts. This is generally returned by provider service
+ // factory methods like "NewComputeV2()" and can mean that a service is not
+ // enabled for your account.
ErrServiceNotFound = errors.New("No suitable service could be found in the service catalog.")
- // ErrEndpointNotFound is returned when no available endpoints match the provided EndpointOpts.
+ // ErrEndpointNotFound is returned when no available endpoints match the
+ // provided EndpointOpts. This is also generally returned by provider service
+ // factory methods, and usually indicates that a region was specified
+ // incorrectly.
ErrEndpointNotFound = errors.New("No suitable endpoint could be found in the service catalog.")
)
-// Availability indicates whether a specific service endpoint is accessible.
-// Identity v2 lists these as different kinds of URLs ("adminURL",
-// "internalURL", and "publicURL"), while v3 lists them as "Interfaces".
+// Availability indicates to whom a specific service endpoint is accessible:
+// the internet at large, internal networks only, or only to administrators.
+// Different identity services use different terminology for these. Identity v2
+// lists them as different kinds of URLs within the service catalog ("adminURL",
+// "internalURL", and "publicURL"), while v3 lists them as "Interfaces" in an
+// endpoint's response.
type Availability string
const (
- // AvailabilityAdmin makes an endpoint only available to administrators.
+ // AvailabilityAdmin indicates that an endpoint is only available to
+ // administrators.
AvailabilityAdmin Availability = "admin"
- // AvailabilityPublic makes an endpoint available to everyone.
+ // AvailabilityPublic indicates that an endpoint is available to everyone on
+ // the internet.
AvailabilityPublic Availability = "public"
- // AvailabilityInternal makes an endpoint only available within the cluster.
+ // AvailabilityInternal indicates that an endpoint is only available within
+ // the cluster's internal network.
AvailabilityInternal Availability = "internal"
)
-// EndpointOpts contains options for finding an endpoint for an Openstack client.
+// EndpointOpts specifies search criteria used by queries against an
+// OpenStack service catalog. The options must contain enough information to
+// unambiguously identify one, and only one, endpoint within the catalog.
+//
+// Usually, these are passed to service client factory functions in a provider
+// package, like "rackspace.NewComputeV2()".
type EndpointOpts struct {
- // Type is the service type for the client (e.g., "compute", "object-store").
- // Required.
+ // Type [required] is the service type for the client (e.g., "compute",
+ // "object-store"). Generally, this will be supplied by the service client
+ // function, but a user-given value will be honored if provided.
Type string
- // Name is the service name for the client (e.g., "nova") as it appears in
- // the service catalog. Services can have the same Type but a different Name,
- // which is why both Type and Name are sometimes needed. Optional.
+ // Name [optional] is the service name for the client (e.g., "nova") as it
+ // appears in the service catalog. Services can have the same Type but a
+ // different Name, which is why both Type and Name are sometimes needed.
Name string
- // Region is the geographic region in which the service resides. Required only
- // for services that span multiple regions.
+ // Region [required] is the geographic region in which the endpoint resides,
+ // generally specifying which datacenter should house your resources.
+ // Required only for services that span multiple regions.
Region string
- // Availability is the visibility of the endpoint to be returned. Valid types
- // are: AvailabilityPublic, AvailabilityInternal, or AvailabilityAdmin.
- // Availability is not required, and defaults to AvailabilityPublic.
- // Not all providers or services offer all Availability options.
+ // Availability [optional] is the visibility of the endpoint to be returned.
+ // Valid types include the constants AvailabilityPublic, AvailabilityInternal,
+ // or AvailabilityAdmin from this package.
+ //
+ // Availability is not required, and defaults to AvailabilityPublic. Not all
+ // providers or services offer all Availability options.
Availability Availability
}
-// EndpointLocator is a function that describes how to locate a single endpoint
-// from a service catalog for a specific ProviderClient. It should be set
-// during ProviderClient authentication and used to discover related ServiceClients.
+/*
+EndpointLocator is an internal function to be used by provider implementations.
+
+It provides an implementation that locates a single endpoint from a service
+catalog for a specific ProviderClient based on user-provided EndpointOpts. The
+provider then uses it to discover related ServiceClients.
+*/
type EndpointLocator func(EndpointOpts) (string, error)
-// ApplyDefaults sets EndpointOpts fields if not already set. Currently,
-// EndpointOpts.Availability defaults to the public endpoint.
+// ApplyDefaults is an internal method to be used by provider implementations.
+//
+// It sets EndpointOpts fields if not already set, including a default type.
+// Currently, EndpointOpts.Availability defaults to the public endpoint.
func (eo *EndpointOpts) ApplyDefaults(t string) {
if eo.Type == "" {
eo.Type = t
diff --git a/openstack/blockstorage/v1/snapshots/util_test.go b/openstack/blockstorage/v1/snapshots/util_test.go
deleted file mode 100644
index a4c4c82..0000000
--- a/openstack/blockstorage/v1/snapshots/util_test.go
+++ /dev/null
@@ -1,38 +0,0 @@
-package snapshots
-
-import (
- "fmt"
- "net/http"
- "testing"
- "time"
-
- th "github.com/rackspace/gophercloud/testhelper"
- "github.com/rackspace/gophercloud/testhelper/client"
-)
-
-func TestWaitForStatus(t *testing.T) {
- th.SetupHTTP()
- defer th.TeardownHTTP()
-
- th.Mux.HandleFunc("/snapshots/1234", func(w http.ResponseWriter, r *http.Request) {
- time.Sleep(2 * time.Second)
- w.Header().Add("Content-Type", "application/json")
- w.WriteHeader(http.StatusOK)
- fmt.Fprintf(w, `
- {
- "snapshot": {
- "display_name": "snapshot-001",
- "id": "1234",
- "status":"available"
- }
- }`)
- })
-
- err := WaitForStatus(client.ServiceClient(), "1234", "available", 0)
- if err == nil {
- t.Errorf("Expected error: 'Time Out in WaitFor'")
- }
-
- err = WaitForStatus(client.ServiceClient(), "1234", "available", 3)
- th.CheckNoErr(t, err)
-}
diff --git a/openstack/blockstorage/v1/volumes/util_test.go b/openstack/blockstorage/v1/volumes/util_test.go
deleted file mode 100644
index 24ef3b6..0000000
--- a/openstack/blockstorage/v1/volumes/util_test.go
+++ /dev/null
@@ -1,38 +0,0 @@
-package volumes
-
-import (
- "fmt"
- "net/http"
- "testing"
- "time"
-
- th "github.com/rackspace/gophercloud/testhelper"
- "github.com/rackspace/gophercloud/testhelper/client"
-)
-
-func TestWaitForStatus(t *testing.T) {
- th.SetupHTTP()
- defer th.TeardownHTTP()
-
- th.Mux.HandleFunc("/volumes/1234", func(w http.ResponseWriter, r *http.Request) {
- time.Sleep(3 * time.Second)
- w.Header().Add("Content-Type", "application/json")
- w.WriteHeader(http.StatusOK)
- fmt.Fprintf(w, `
- {
- "volume": {
- "display_name": "vol-001",
- "id": "1234",
- "status":"available"
- }
- }`)
- })
-
- err := WaitForStatus(client.ServiceClient(), "1234", "available", 0)
- if err == nil {
- t.Errorf("Expected error: 'Time Out in WaitFor'")
- }
-
- err = WaitForStatus(client.ServiceClient(), "1234", "available", 6)
- th.CheckNoErr(t, err)
-}
diff --git a/openstack/cdn/v1/base/doc.go b/openstack/cdn/v1/base/doc.go
new file mode 100644
index 0000000..f78d4f7
--- /dev/null
+++ b/openstack/cdn/v1/base/doc.go
@@ -0,0 +1,4 @@
+// Package base provides information and interaction with the base API
+// resource in the OpenStack CDN service. This API resource allows for
+// retrieving the Home Document and pinging the root URL.
+package base
diff --git a/openstack/cdn/v1/base/fixtures.go b/openstack/cdn/v1/base/fixtures.go
new file mode 100644
index 0000000..19b5ece
--- /dev/null
+++ b/openstack/cdn/v1/base/fixtures.go
@@ -0,0 +1,53 @@
+package base
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+// HandleGetSuccessfully creates an HTTP handler at `/` on the test handler mux
+// that responds with a `Get` response.
+func HandleGetSuccessfully(t *testing.T) {
+ th.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ th.TestHeader(t, r, "Accept", "application/json")
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprintf(w, `
+ {
+ "resources": {
+ "rel/cdn": {
+ "href-template": "services{?marker,limit}",
+ "href-vars": {
+ "marker": "param/marker",
+ "limit": "param/limit"
+ },
+ "hints": {
+ "allow": [
+ "GET"
+ ],
+ "formats": {
+ "application/json": {}
+ }
+ }
+ }
+ }
+ }
+ `)
+
+ })
+}
+
+// HandlePingSuccessfully creates an HTTP handler at `/ping` on the test handler
+// mux that responds with a `Ping` response.
+func HandlePingSuccessfully(t *testing.T) {
+ th.Mux.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ w.WriteHeader(http.StatusNoContent)
+ })
+}
diff --git a/openstack/cdn/v1/base/requests.go b/openstack/cdn/v1/base/requests.go
new file mode 100644
index 0000000..9d8632c
--- /dev/null
+++ b/openstack/cdn/v1/base/requests.go
@@ -0,0 +1,30 @@
+package base
+
+import (
+ "github.com/rackspace/gophercloud"
+
+ "github.com/racker/perigee"
+)
+
+// Get retrieves the home document, allowing the user to discover the
+// entire API.
+func Get(c *gophercloud.ServiceClient) GetResult {
+ var res GetResult
+ _, res.Err = perigee.Request("GET", getURL(c), perigee.Options{
+ MoreHeaders: c.AuthenticatedHeaders(),
+ Results: &res.Body,
+ OkCodes: []int{200},
+ })
+ return res
+}
+
+// Ping retrieves a ping to the server.
+func Ping(c *gophercloud.ServiceClient) PingResult {
+ var res PingResult
+ _, res.Err = perigee.Request("GET", pingURL(c), perigee.Options{
+ MoreHeaders: c.AuthenticatedHeaders(),
+ OkCodes: []int{204},
+ OmitAccept: true,
+ })
+ return res
+}
diff --git a/openstack/cdn/v1/base/requests_test.go b/openstack/cdn/v1/base/requests_test.go
new file mode 100644
index 0000000..a8d95f9
--- /dev/null
+++ b/openstack/cdn/v1/base/requests_test.go
@@ -0,0 +1,43 @@
+package base
+
+import (
+ "testing"
+
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestGetHomeDocument(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleGetSuccessfully(t)
+
+ actual, err := Get(fake.ServiceClient()).Extract()
+ th.CheckNoErr(t, err)
+
+ expected := HomeDocument{
+ "rel/cdn": map[string]interface{}{
+ "href-template": "services{?marker,limit}",
+ "href-vars": map[string]interface{}{
+ "marker": "param/marker",
+ "limit": "param/limit",
+ },
+ "hints": map[string]interface{}{
+ "allow": []string{"GET"},
+ "formats": map[string]interface{}{
+ "application/json": map[string]interface{}{},
+ },
+ },
+ },
+ }
+ th.CheckDeepEquals(t, expected, *actual)
+}
+
+func TestPing(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandlePingSuccessfully(t)
+
+ err := Ping(fake.ServiceClient()).ExtractErr()
+ th.CheckNoErr(t, err)
+}
diff --git a/openstack/cdn/v1/base/results.go b/openstack/cdn/v1/base/results.go
new file mode 100644
index 0000000..bef1da8
--- /dev/null
+++ b/openstack/cdn/v1/base/results.go
@@ -0,0 +1,35 @@
+package base
+
+import (
+ "errors"
+
+ "github.com/rackspace/gophercloud"
+)
+
+// HomeDocument is a resource that contains all the resources for the CDN API.
+type HomeDocument map[string]interface{}
+
+// GetResult represents the result of a Get operation.
+type GetResult struct {
+ gophercloud.Result
+}
+
+// Extract is a function that accepts a result and extracts a home document resource.
+func (r GetResult) Extract() (*HomeDocument, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ submap, ok := r.Body.(map[string]interface{})["resources"]
+ if !ok {
+ return nil, errors.New("Unexpected HomeDocument structure")
+ }
+ casted := HomeDocument(submap.(map[string]interface{}))
+
+ return &casted, nil
+}
+
+// PingResult represents the result of a Ping operation.
+type PingResult struct {
+ gophercloud.ErrResult
+}
diff --git a/openstack/cdn/v1/base/urls.go b/openstack/cdn/v1/base/urls.go
new file mode 100644
index 0000000..a95e18b
--- /dev/null
+++ b/openstack/cdn/v1/base/urls.go
@@ -0,0 +1,11 @@
+package base
+
+import "github.com/rackspace/gophercloud"
+
+func getURL(c *gophercloud.ServiceClient) string {
+ return c.ServiceURL()
+}
+
+func pingURL(c *gophercloud.ServiceClient) string {
+ return c.ServiceURL("ping")
+}
diff --git a/openstack/cdn/v1/flavors/doc.go b/openstack/cdn/v1/flavors/doc.go
new file mode 100644
index 0000000..d406698
--- /dev/null
+++ b/openstack/cdn/v1/flavors/doc.go
@@ -0,0 +1,6 @@
+// Package flavors provides information and interaction with the flavors API
+// resource in the OpenStack CDN service. This API resource allows for
+// listing flavors and retrieving a specific flavor.
+//
+// A flavor is a mapping configuration to a CDN provider.
+package flavors
diff --git a/openstack/cdn/v1/flavors/fixtures.go b/openstack/cdn/v1/flavors/fixtures.go
new file mode 100644
index 0000000..f413b6b
--- /dev/null
+++ b/openstack/cdn/v1/flavors/fixtures.go
@@ -0,0 +1,82 @@
+package flavors
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+// HandleListCDNFlavorsSuccessfully creates an HTTP handler at `/flavors` on the test handler mux
+// that responds with a `List` response.
+func HandleListCDNFlavorsSuccessfully(t *testing.T) {
+ th.Mux.HandleFunc("/flavors", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprintf(w, `
+ {
+ "flavors": [
+ {
+ "id": "europe",
+ "providers": [
+ {
+ "provider": "Fastly",
+ "links": [
+ {
+ "href": "http://www.fastly.com",
+ "rel": "provider_url"
+ }
+ ]
+ }
+ ],
+ "links": [
+ {
+ "href": "https://www.poppycdn.io/v1.0/flavors/europe",
+ "rel": "self"
+ }
+ ]
+ }
+ ]
+ }
+ `)
+ })
+}
+
+// HandleGetCDNFlavorSuccessfully creates an HTTP handler at `/flavors/{id}` on the test handler mux
+// that responds with a `Get` response.
+func HandleGetCDNFlavorSuccessfully(t *testing.T) {
+ th.Mux.HandleFunc("/flavors/asia", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprintf(w, `
+ {
+ "id" : "asia",
+ "providers" : [
+ {
+ "provider" : "ChinaCache",
+ "links": [
+ {
+ "href": "http://www.chinacache.com",
+ "rel": "provider_url"
+ }
+ ]
+ }
+ ],
+ "links": [
+ {
+ "href": "https://www.poppycdn.io/v1.0/flavors/asia",
+ "rel": "self"
+ }
+ ]
+ }
+ `)
+ })
+}
diff --git a/openstack/cdn/v1/flavors/requests.go b/openstack/cdn/v1/flavors/requests.go
new file mode 100644
index 0000000..88ac891
--- /dev/null
+++ b/openstack/cdn/v1/flavors/requests.go
@@ -0,0 +1,27 @@
+package flavors
+
+import (
+ "github.com/racker/perigee"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// List returns a single page of CDN flavors.
+func List(c *gophercloud.ServiceClient) pagination.Pager {
+ url := listURL(c)
+ createPage := func(r pagination.PageResult) pagination.Page {
+ return FlavorPage{pagination.SinglePageBase(r)}
+ }
+ return pagination.NewPager(c, url, createPage)
+}
+
+// Get retrieves a specific flavor based on its unique ID.
+func Get(c *gophercloud.ServiceClient, id string) GetResult {
+ var res GetResult
+ _, res.Err = perigee.Request("GET", getURL(c, id), perigee.Options{
+ MoreHeaders: c.AuthenticatedHeaders(),
+ Results: &res.Body,
+ OkCodes: []int{200},
+ })
+ return res
+}
diff --git a/openstack/cdn/v1/flavors/requests_test.go b/openstack/cdn/v1/flavors/requests_test.go
new file mode 100644
index 0000000..7ddf1b1
--- /dev/null
+++ b/openstack/cdn/v1/flavors/requests_test.go
@@ -0,0 +1,90 @@
+package flavors
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestList(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ HandleListCDNFlavorsSuccessfully(t)
+
+ count := 0
+
+ err := List(fake.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractFlavors(page)
+ if err != nil {
+ t.Errorf("Failed to extract flavors: %v", err)
+ return false, err
+ }
+
+ expected := []Flavor{
+ Flavor{
+ ID: "europe",
+ Providers: []Provider{
+ Provider{
+ Provider: "Fastly",
+ Links: []gophercloud.Link{
+ gophercloud.Link{
+ Href: "http://www.fastly.com",
+ Rel: "provider_url",
+ },
+ },
+ },
+ },
+ Links: []gophercloud.Link{
+ gophercloud.Link{
+ Href: "https://www.poppycdn.io/v1.0/flavors/europe",
+ Rel: "self",
+ },
+ },
+ },
+ }
+
+ th.CheckDeepEquals(t, expected, actual)
+
+ return true, nil
+ })
+ th.AssertNoErr(t, err)
+ th.CheckEquals(t, 1, count)
+}
+
+func TestGet(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ HandleGetCDNFlavorSuccessfully(t)
+
+ expected := &Flavor{
+ ID: "asia",
+ Providers: []Provider{
+ Provider{
+ Provider: "ChinaCache",
+ Links: []gophercloud.Link{
+ gophercloud.Link{
+ Href: "http://www.chinacache.com",
+ Rel: "provider_url",
+ },
+ },
+ },
+ },
+ Links: []gophercloud.Link{
+ gophercloud.Link{
+ Href: "https://www.poppycdn.io/v1.0/flavors/asia",
+ Rel: "self",
+ },
+ },
+ }
+
+
+ actual, err := Get(fake.ServiceClient(), "asia").Extract()
+ th.AssertNoErr(t, err)
+ th.AssertDeepEquals(t, expected, actual)
+}
diff --git a/openstack/cdn/v1/flavors/results.go b/openstack/cdn/v1/flavors/results.go
new file mode 100644
index 0000000..8cab48b
--- /dev/null
+++ b/openstack/cdn/v1/flavors/results.go
@@ -0,0 +1,71 @@
+package flavors
+
+import (
+ "github.com/mitchellh/mapstructure"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// Provider represents a provider for a particular flavor.
+type Provider struct {
+ // Specifies the name of the provider. The name must not exceed 64 bytes in
+ // length and is limited to unicode, digits, underscores, and hyphens.
+ Provider string `mapstructure:"provider"`
+ // Specifies a list with an href where rel is provider_url.
+ Links []gophercloud.Link `mapstructure:"links"`
+}
+
+// Flavor represents a mapping configuration to a CDN provider.
+type Flavor struct {
+ // Specifies the name of the flavor. The name must not exceed 64 bytes in
+ // length and is limited to unicode, digits, underscores, and hyphens.
+ ID string `mapstructure:"id"`
+ // Specifies the list of providers mapped to this flavor.
+ Providers []Provider `mapstructure:"providers"`
+ // Specifies the self-navigating JSON document paths.
+ Links []gophercloud.Link `mapstructure:"links"`
+}
+
+// FlavorPage is the page returned by a pager when traversing over a
+// collection of CDN flavors.
+type FlavorPage struct {
+ pagination.SinglePageBase
+}
+
+// IsEmpty returns true if a FlavorPage contains no Flavors.
+func (r FlavorPage) IsEmpty() (bool, error) {
+ flavors, err := ExtractFlavors(r)
+ if err != nil {
+ return true, err
+ }
+ return len(flavors) == 0, nil
+}
+
+// ExtractFlavors extracts and returns Flavors. It is used while iterating over
+// a flavors.List call.
+func ExtractFlavors(page pagination.Page) ([]Flavor, error) {
+ var response struct {
+ Flavors []Flavor `json:"flavors"`
+ }
+
+ err := mapstructure.Decode(page.(FlavorPage).Body, &response)
+ return response.Flavors, err
+}
+
+// GetResult represents the result of a get operation.
+type GetResult struct {
+ gophercloud.Result
+}
+
+// Extract is a function that extracts a flavor from a GetResult.
+func (r GetResult) Extract() (*Flavor, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var res Flavor
+
+ err := mapstructure.Decode(r.Body, &res)
+
+ return &res, err
+}
diff --git a/openstack/cdn/v1/flavors/urls.go b/openstack/cdn/v1/flavors/urls.go
new file mode 100644
index 0000000..6eb38d2
--- /dev/null
+++ b/openstack/cdn/v1/flavors/urls.go
@@ -0,0 +1,11 @@
+package flavors
+
+import "github.com/rackspace/gophercloud"
+
+func listURL(c *gophercloud.ServiceClient) string {
+ return c.ServiceURL("flavors")
+}
+
+func getURL(c *gophercloud.ServiceClient, id string) string {
+ return c.ServiceURL("flavors", id)
+}
diff --git a/openstack/cdn/v1/serviceassets/doc.go b/openstack/cdn/v1/serviceassets/doc.go
new file mode 100644
index 0000000..ceecaa5
--- /dev/null
+++ b/openstack/cdn/v1/serviceassets/doc.go
@@ -0,0 +1,7 @@
+// Package serviceassets provides information and interaction with the
+// serviceassets API resource in the OpenStack CDN service. This API resource
+// allows for deleting cached assets.
+//
+// A service distributes assets across the network. Service assets let you
+// interrogate properties about these assets and perform certain actions on them.
+package serviceassets
diff --git a/openstack/cdn/v1/serviceassets/fixtures.go b/openstack/cdn/v1/serviceassets/fixtures.go
new file mode 100644
index 0000000..38e7fc5
--- /dev/null
+++ b/openstack/cdn/v1/serviceassets/fixtures.go
@@ -0,0 +1,19 @@
+package serviceassets
+
+import (
+ "net/http"
+ "testing"
+
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+// HandleDeleteCDNAssetSuccessfully creates an HTTP handler at `/services/{id}/assets` on the test handler mux
+// that responds with a `Delete` response.
+func HandleDeleteCDNAssetSuccessfully(t *testing.T) {
+ th.Mux.HandleFunc("/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0/assets", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "DELETE")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ w.WriteHeader(http.StatusAccepted)
+ })
+}
diff --git a/openstack/cdn/v1/serviceassets/requests.go b/openstack/cdn/v1/serviceassets/requests.go
new file mode 100644
index 0000000..5248ef2
--- /dev/null
+++ b/openstack/cdn/v1/serviceassets/requests.go
@@ -0,0 +1,52 @@
+package serviceassets
+
+import (
+ "strings"
+
+ "github.com/racker/perigee"
+ "github.com/rackspace/gophercloud"
+)
+
+// DeleteOptsBuilder allows extensions to add additional parameters to the Delete
+// request.
+type DeleteOptsBuilder interface {
+ ToCDNAssetDeleteParams() (string, error)
+}
+
+// DeleteOpts is a structure that holds options for deleting CDN service assets.
+type DeleteOpts struct {
+ // If all is set to true, specifies that the delete occurs against all of the
+ // assets for the service.
+ All bool `q:"all"`
+ // Specifies the relative URL of the asset to be deleted.
+ URL string `q:"url"`
+}
+
+// ToCDNAssetDeleteParams formats a DeleteOpts into a query string.
+func (opts DeleteOpts) ToCDNAssetDeleteParams() (string, error) {
+ q, err := gophercloud.BuildQueryString(opts)
+ if err != nil {
+ return "", err
+ }
+ return q.String(), nil
+}
+
+// Delete accepts a unique service ID or URL and deletes the CDN service asset associated with
+// it. For example, both "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0" and
+// "https://global.cdn.api.rackspacecloud.com/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0"
+// are valid options for idOrURL.
+func Delete(c *gophercloud.ServiceClient, idOrURL string, opts DeleteOptsBuilder) DeleteResult {
+ var url string
+ if strings.Contains(idOrURL, "/") {
+ url = idOrURL
+ } else {
+ url = deleteURL(c, idOrURL)
+ }
+
+ var res DeleteResult
+ _, res.Err = perigee.Request("DELETE", url, perigee.Options{
+ MoreHeaders: c.AuthenticatedHeaders(),
+ OkCodes: []int{202},
+ })
+ return res
+}
diff --git a/openstack/cdn/v1/serviceassets/requests_test.go b/openstack/cdn/v1/serviceassets/requests_test.go
new file mode 100644
index 0000000..32896ee
--- /dev/null
+++ b/openstack/cdn/v1/serviceassets/requests_test.go
@@ -0,0 +1,18 @@
+package serviceassets
+
+import (
+ "testing"
+
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestDelete(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ HandleDeleteCDNAssetSuccessfully(t)
+
+ err := Delete(fake.ServiceClient(), "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0", nil).ExtractErr()
+ th.AssertNoErr(t, err)
+}
diff --git a/openstack/cdn/v1/serviceassets/results.go b/openstack/cdn/v1/serviceassets/results.go
new file mode 100644
index 0000000..1d8734b
--- /dev/null
+++ b/openstack/cdn/v1/serviceassets/results.go
@@ -0,0 +1,8 @@
+package serviceassets
+
+import "github.com/rackspace/gophercloud"
+
+// DeleteResult represents the result of a Delete operation.
+type DeleteResult struct {
+ gophercloud.ErrResult
+}
diff --git a/openstack/cdn/v1/serviceassets/urls.go b/openstack/cdn/v1/serviceassets/urls.go
new file mode 100644
index 0000000..cb0aea8
--- /dev/null
+++ b/openstack/cdn/v1/serviceassets/urls.go
@@ -0,0 +1,7 @@
+package serviceassets
+
+import "github.com/rackspace/gophercloud"
+
+func deleteURL(c *gophercloud.ServiceClient, id string) string {
+ return c.ServiceURL("services", id, "assets")
+}
diff --git a/openstack/cdn/v1/services/doc.go b/openstack/cdn/v1/services/doc.go
new file mode 100644
index 0000000..41f7c60
--- /dev/null
+++ b/openstack/cdn/v1/services/doc.go
@@ -0,0 +1,7 @@
+// Package services provides information and interaction with the services API
+// resource in the OpenStack CDN service. This API resource allows for
+// listing, creating, updating, retrieving, and deleting services.
+//
+// A service represents an application that has its content cached to the edge
+// nodes.
+package services
diff --git a/openstack/cdn/v1/services/errors.go b/openstack/cdn/v1/services/errors.go
new file mode 100644
index 0000000..359584c
--- /dev/null
+++ b/openstack/cdn/v1/services/errors.go
@@ -0,0 +1,7 @@
+package services
+
+import "fmt"
+
+func no(str string) error {
+ return fmt.Errorf("Required parameter %s not provided", str)
+}
diff --git a/openstack/cdn/v1/services/fixtures.go b/openstack/cdn/v1/services/fixtures.go
new file mode 100644
index 0000000..d9bc9f2
--- /dev/null
+++ b/openstack/cdn/v1/services/fixtures.go
@@ -0,0 +1,372 @@
+package services
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+// HandleListCDNServiceSuccessfully creates an HTTP handler at `/services` on the test handler mux
+// that responds with a `List` response.
+func HandleListCDNServiceSuccessfully(t *testing.T) {
+ th.Mux.HandleFunc("/services", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ r.ParseForm()
+ marker := r.Form.Get("marker")
+ switch marker {
+ case "":
+ fmt.Fprintf(w, `
+ {
+ "links": [
+ {
+ "rel": "next",
+ "href": "https://www.poppycdn.io/v1.0/services?marker=96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0&limit=20"
+ }
+ ],
+ "services": [
+ {
+ "id": "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0",
+ "name": "mywebsite.com",
+ "domains": [
+ {
+ "domain": "www.mywebsite.com"
+ }
+ ],
+ "origins": [
+ {
+ "origin": "mywebsite.com",
+ "port": 80,
+ "ssl": false
+ }
+ ],
+ "caching": [
+ {
+ "name": "default",
+ "ttl": 3600
+ },
+ {
+ "name": "home",
+ "ttl": 17200,
+ "rules": [
+ {
+ "name": "index",
+ "request_url": "/index.htm"
+ }
+ ]
+ },
+ {
+ "name": "images",
+ "ttl": 12800,
+ "rules": [
+ {
+ "name": "images",
+ "request_url": "*.png"
+ }
+ ]
+ }
+ ],
+ "restrictions": [
+ {
+ "name": "website only",
+ "rules": [
+ {
+ "name": "mywebsite.com",
+ "referrer": "www.mywebsite.com"
+ }
+ ]
+ }
+ ],
+ "flavor_id": "asia",
+ "status": "deployed",
+ "errors" : [],
+ "links": [
+ {
+ "href": "https://www.poppycdn.io/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0",
+ "rel": "self"
+ },
+ {
+ "href": "mywebsite.com.cdn123.poppycdn.net",
+ "rel": "access_url"
+ },
+ {
+ "href": "https://www.poppycdn.io/v1.0/flavors/asia",
+ "rel": "flavor"
+ }
+ ]
+ },
+ {
+ "id": "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f1",
+ "name": "myothersite.com",
+ "domains": [
+ {
+ "domain": "www.myothersite.com"
+ }
+ ],
+ "origins": [
+ {
+ "origin": "44.33.22.11",
+ "port": 80,
+ "ssl": false
+ },
+ {
+ "origin": "77.66.55.44",
+ "port": 80,
+ "ssl": false,
+ "rules": [
+ {
+ "name": "videos",
+ "request_url": "^/videos/*.m3u"
+ }
+ ]
+ }
+ ],
+ "caching": [
+ {
+ "name": "default",
+ "ttl": 3600
+ }
+ ],
+ "restrictions": [
+ {}
+ ],
+ "flavor_id": "europe",
+ "status": "deployed",
+ "links": [
+ {
+ "href": "https://www.poppycdn.io/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f1",
+ "rel": "self"
+ },
+ {
+ "href": "myothersite.com.poppycdn.net",
+ "rel": "access_url"
+ },
+ {
+ "href": "https://www.poppycdn.io/v1.0/flavors/europe",
+ "rel": "flavor"
+ }
+ ]
+ }
+ ]
+ }
+ `)
+ case "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f1":
+ fmt.Fprintf(w, `{
+ "services": []
+ }`)
+ default:
+ t.Fatalf("Unexpected marker: [%s]", marker)
+ }
+ })
+}
+
+// HandleCreateCDNServiceSuccessfully creates an HTTP handler at `/services` on the test handler mux
+// that responds with a `Create` response.
+func HandleCreateCDNServiceSuccessfully(t *testing.T) {
+ th.Mux.HandleFunc("/services", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "POST")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ th.TestJSONRequest(t, r, `
+ {
+ "name": "mywebsite.com",
+ "domains": [
+ {
+ "domain": "www.mywebsite.com"
+ },
+ {
+ "domain": "blog.mywebsite.com"
+ }
+ ],
+ "origins": [
+ {
+ "origin": "mywebsite.com",
+ "port": 80,
+ "ssl": false
+ }
+ ],
+ "restrictions": [
+ {
+ "name": "website only",
+ "rules": [
+ {
+ "name": "mywebsite.com",
+ "referrer": "www.mywebsite.com"
+ }
+ ]
+ }
+ ],
+ "caching": [
+ {
+ "name": "default",
+ "ttl": 3600
+ }
+ ],
+
+ "flavor_id": "cdn"
+ }
+ `)
+ w.Header().Add("Location", "https://global.cdn.api.rackspacecloud.com/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0")
+ w.WriteHeader(http.StatusAccepted)
+ })
+}
+
+// HandleGetCDNServiceSuccessfully creates an HTTP handler at `/services/{id}` on the test handler mux
+// that responds with a `Get` response.
+func HandleGetCDNServiceSuccessfully(t *testing.T) {
+ th.Mux.HandleFunc("/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprintf(w, `
+ {
+ "id": "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0",
+ "name": "mywebsite.com",
+ "domains": [
+ {
+ "domain": "www.mywebsite.com",
+ "protocol": "http"
+ }
+ ],
+ "origins": [
+ {
+ "origin": "mywebsite.com",
+ "port": 80,
+ "ssl": false
+ }
+ ],
+ "caching": [
+ {
+ "name": "default",
+ "ttl": 3600
+ },
+ {
+ "name": "home",
+ "ttl": 17200,
+ "rules": [
+ {
+ "name": "index",
+ "request_url": "/index.htm"
+ }
+ ]
+ },
+ {
+ "name": "images",
+ "ttl": 12800,
+ "rules": [
+ {
+ "name": "images",
+ "request_url": "*.png"
+ }
+ ]
+ }
+ ],
+ "restrictions": [
+ {
+ "name": "website only",
+ "rules": [
+ {
+ "name": "mywebsite.com",
+ "referrer": "www.mywebsite.com"
+ }
+ ]
+ }
+ ],
+ "flavor_id": "cdn",
+ "status": "deployed",
+ "errors" : [],
+ "links": [
+ {
+ "href": "https://global.cdn.api.rackspacecloud.com/v1.0/110011/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0",
+ "rel": "self"
+ },
+ {
+ "href": "blog.mywebsite.com.cdn1.raxcdn.com",
+ "rel": "access_url"
+ },
+ {
+ "href": "https://global.cdn.api.rackspacecloud.com/v1.0/110011/flavors/cdn",
+ "rel": "flavor"
+ }
+ ]
+ }
+ `)
+ })
+}
+
+// HandleUpdateCDNServiceSuccessfully creates an HTTP handler at `/services/{id}` on the test handler mux
+// that responds with a `Update` response.
+func HandleUpdateCDNServiceSuccessfully(t *testing.T) {
+ th.Mux.HandleFunc("/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "PATCH")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ th.TestJSONRequest(t, r, `
+ [
+ {
+ "op": "add",
+ "path": "/domains/-",
+ "value": {"domain": "appended.mocksite4.com"}
+ },
+ {
+ "op": "add",
+ "path": "/domains/4",
+ "value": {"domain": "inserted.mocksite4.com"}
+ },
+ {
+ "op": "add",
+ "path": "/domains",
+ "value": [
+ {"domain": "bulkadded1.mocksite4.com"},
+ {"domain": "bulkadded2.mocksite4.com"}
+ ]
+ },
+ {
+ "op": "replace",
+ "path": "/origins/2",
+ "value": {"origin": "44.33.22.11", "port": 80, "ssl": false}
+ },
+ {
+ "op": "replace",
+ "path": "/origins",
+ "value": [
+ {"origin": "44.33.22.11", "port": 80, "ssl": false},
+ {"origin": "55.44.33.22", "port": 443, "ssl": true}
+ ]
+ },
+ {
+ "op": "remove",
+ "path": "/caching/8"
+ },
+ {
+ "op": "remove",
+ "path": "/caching"
+ },
+ {
+ "op": "replace",
+ "path": "/name",
+ "value": "differentServiceName"
+ }
+ ]
+ `)
+ w.Header().Add("Location", "https://www.poppycdn.io/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0")
+ w.WriteHeader(http.StatusAccepted)
+ })
+}
+
+// HandleDeleteCDNServiceSuccessfully creates an HTTP handler at `/services/{id}` on the test handler mux
+// that responds with a `Delete` response.
+func HandleDeleteCDNServiceSuccessfully(t *testing.T) {
+ th.Mux.HandleFunc("/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "DELETE")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ w.WriteHeader(http.StatusAccepted)
+ })
+}
diff --git a/openstack/cdn/v1/services/requests.go b/openstack/cdn/v1/services/requests.go
new file mode 100644
index 0000000..ad1e1da
--- /dev/null
+++ b/openstack/cdn/v1/services/requests.go
@@ -0,0 +1,391 @@
+package services
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/racker/perigee"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// ListOptsBuilder allows extensions to add additional parameters to the
+// List request.
+type ListOptsBuilder interface {
+ ToCDNServiceListQuery() (string, error)
+}
+
+// ListOpts allows the filtering and sorting of paginated collections through
+// the API. Marker and Limit are used for pagination.
+type ListOpts struct {
+ Marker string `q:"marker"`
+ Limit int `q:"limit"`
+}
+
+// ToCDNServiceListQuery formats a ListOpts into a query string.
+func (opts ListOpts) ToCDNServiceListQuery() (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
+// CDN services. It accepts a ListOpts struct, which allows for pagination via
+// marker and limit.
+func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
+ url := listURL(c)
+ if opts != nil {
+ query, err := opts.ToCDNServiceListQuery()
+ if err != nil {
+ return pagination.Pager{Err: err}
+ }
+ url += query
+ }
+
+ createPage := func(r pagination.PageResult) pagination.Page {
+ p := ServicePage{pagination.MarkerPageBase{PageResult: r}}
+ p.MarkerPageBase.Owner = p
+ return p
+ }
+
+ pager := pagination.NewPager(c, url, createPage)
+ return pager
+}
+
+// CreateOptsBuilder is the interface options structs have to satisfy in order
+// to be used in the main Create operation in this package. Since many
+// extensions decorate or modify the common logic, it is useful for them to
+// satisfy a basic interface in order for them to be used.
+type CreateOptsBuilder interface {
+ ToCDNServiceCreateMap() (map[string]interface{}, error)
+}
+
+// CreateOpts is the common options struct used in this package's Create
+// operation.
+type CreateOpts struct {
+ // REQUIRED. Specifies the name of the service. The minimum length for name is
+ // 3. The maximum length is 256.
+ Name string
+ // REQUIRED. Specifies a list of domains used by users to access their website.
+ Domains []Domain
+ // REQUIRED. Specifies a list of origin domains or IP addresses where the
+ // original assets are stored.
+ Origins []Origin
+ // REQUIRED. Specifies the CDN provider flavor ID to use. For a list of
+ // flavors, see the operation to list the available flavors. The minimum
+ // length for flavor_id is 1. The maximum length is 256.
+ FlavorID string
+ // OPTIONAL. Specifies the TTL rules for the assets under this service. Supports wildcards for fine-grained control.
+ Caching []CacheRule
+ // OPTIONAL. Specifies the restrictions that define who can access assets (content from the CDN cache).
+ Restrictions []Restriction
+}
+
+// ToCDNServiceCreateMap casts a CreateOpts struct to a map.
+func (opts CreateOpts) ToCDNServiceCreateMap() (map[string]interface{}, error) {
+ s := make(map[string]interface{})
+
+ if opts.Name == "" {
+ return nil, no("Name")
+ }
+ s["name"] = opts.Name
+
+ if opts.Domains == nil {
+ return nil, no("Domains")
+ }
+ for _, domain := range opts.Domains {
+ if domain.Domain == "" {
+ return nil, no("Domains[].Domain")
+ }
+ }
+ s["domains"] = opts.Domains
+
+ if opts.Origins == nil {
+ return nil, no("Origins")
+ }
+ for _, origin := range opts.Origins {
+ if origin.Origin == "" {
+ return nil, no("Origins[].Origin")
+ }
+ if origin.Rules == nil && len(opts.Origins) > 1 {
+ return nil, no("Origins[].Rules")
+ }
+ for _, rule := range origin.Rules {
+ if rule.Name == "" {
+ return nil, no("Origins[].Rules[].Name")
+ }
+ if rule.RequestURL == "" {
+ return nil, no("Origins[].Rules[].RequestURL")
+ }
+ }
+ }
+ s["origins"] = opts.Origins
+
+ if opts.FlavorID == "" {
+ return nil, no("FlavorID")
+ }
+ s["flavor_id"] = opts.FlavorID
+
+ if opts.Caching != nil {
+ for _, cache := range opts.Caching {
+ if cache.Name == "" {
+ return nil, no("Caching[].Name")
+ }
+ if cache.Rules != nil {
+ for _, rule := range cache.Rules {
+ if rule.Name == "" {
+ return nil, no("Caching[].Rules[].Name")
+ }
+ if rule.RequestURL == "" {
+ return nil, no("Caching[].Rules[].RequestURL")
+ }
+ }
+ }
+ }
+ s["caching"] = opts.Caching
+ }
+
+ if opts.Restrictions != nil {
+ for _, restriction := range opts.Restrictions {
+ if restriction.Name == "" {
+ return nil, no("Restrictions[].Name")
+ }
+ if restriction.Rules != nil {
+ for _, rule := range restriction.Rules {
+ if rule.Name == "" {
+ return nil, no("Restrictions[].Rules[].Name")
+ }
+ }
+ }
+ }
+ s["restrictions"] = opts.Restrictions
+ }
+
+ return s, nil
+}
+
+// Create accepts a CreateOpts struct and creates a new CDN service using the
+// values provided.
+func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult {
+ var res CreateResult
+
+ reqBody, err := opts.ToCDNServiceCreateMap()
+ if err != nil {
+ res.Err = err
+ return res
+ }
+
+ // Send request to API
+ resp, err := perigee.Request("POST", createURL(c), perigee.Options{
+ MoreHeaders: c.AuthenticatedHeaders(),
+ ReqBody: &reqBody,
+ OkCodes: []int{202},
+ })
+ res.Header = resp.HttpResponse.Header
+ res.Err = err
+ return res
+}
+
+// Get retrieves a specific service based on its URL or its unique ID. For
+// example, both "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0" and
+// "https://global.cdn.api.rackspacecloud.com/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0"
+// are valid options for idOrURL.
+func Get(c *gophercloud.ServiceClient, idOrURL string) GetResult {
+ var url string
+ if strings.Contains(idOrURL, "/") {
+ url = idOrURL
+ } else {
+ url = getURL(c, idOrURL)
+ }
+
+ var res GetResult
+ _, res.Err = perigee.Request("GET", url, perigee.Options{
+ MoreHeaders: c.AuthenticatedHeaders(),
+ Results: &res.Body,
+ OkCodes: []int{200},
+ })
+ return res
+}
+
+// Path is a JSON pointer location that indicates which service parameter is being added, replaced,
+// or removed.
+type Path struct {
+ baseElement string
+}
+
+func (p Path) renderRoot() string {
+ return "/" + p.baseElement
+}
+
+func (p Path) renderDash() string {
+ return fmt.Sprintf("/%s/-", p.baseElement)
+}
+
+func (p Path) renderIndex(index int64) string {
+ return fmt.Sprintf("/%s/%d", p.baseElement, index)
+}
+
+var (
+ // PathDomains indicates that an update operation is to be performed on a Domain.
+ PathDomains = Path{baseElement: "domains"}
+
+ // PathOrigins indicates that an update operation is to be performed on an Origin.
+ PathOrigins = Path{baseElement: "origins"}
+
+ // PathCaching indicates that an update operation is to be performed on a CacheRule.
+ PathCaching = Path{baseElement: "caching"}
+)
+
+type value interface {
+ toPatchValue() interface{}
+ appropriatePath() Path
+ renderRootOr(func(p Path) string) string
+}
+
+// Patch represents a single update to an existing Service. Multiple updates to a service can be
+// submitted at the same time.
+type Patch interface {
+ ToCDNServiceUpdateMap() map[string]interface{}
+}
+
+// Insertion is a Patch that requests the addition of a value (Domain, Origin, or CacheRule) to
+// a Service at a fixed index. Use an Append instead to append the new value to the end of its
+// collection. Pass it to the Update function as part of the Patch slice.
+type Insertion struct {
+ Index int64
+ Value value
+}
+
+// ToCDNServiceUpdateMap converts an Insertion into a request body fragment suitable for the
+// Update call.
+func (i Insertion) ToCDNServiceUpdateMap() map[string]interface{} {
+ return map[string]interface{}{
+ "op": "add",
+ "path": i.Value.renderRootOr(func(p Path) string { return p.renderIndex(i.Index) }),
+ "value": i.Value.toPatchValue(),
+ }
+}
+
+// Append is a Patch that requests the addition of a value (Domain, Origin, or CacheRule) to a
+// Service at the end of its respective collection. Use an Insertion instead to insert the value
+// at a fixed index within the collection. Pass this to the Update function as part of its
+// Patch slice.
+type Append struct {
+ Value value
+}
+
+// ToCDNServiceUpdateMap converts an Append into a request body fragment suitable for the
+// Update call.
+func (a Append) ToCDNServiceUpdateMap() map[string]interface{} {
+ return map[string]interface{}{
+ "op": "add",
+ "path": a.Value.renderRootOr(func(p Path) string { return p.renderDash() }),
+ "value": a.Value.toPatchValue(),
+ }
+}
+
+// Replacement is a Patch that alters a specific service parameter (Domain, Origin, or CacheRule)
+// in-place by index. Pass it to the Update function as part of the Patch slice.
+type Replacement struct {
+ Value value
+ Index int64
+}
+
+// ToCDNServiceUpdateMap converts a Replacement into a request body fragment suitable for the
+// Update call.
+func (r Replacement) ToCDNServiceUpdateMap() map[string]interface{} {
+ return map[string]interface{}{
+ "op": "replace",
+ "path": r.Value.renderRootOr(func(p Path) string { return p.renderIndex(r.Index) }),
+ "value": r.Value.toPatchValue(),
+ }
+}
+
+// NameReplacement specifically updates the Service name. Pass it to the Update function as part
+// of the Patch slice.
+type NameReplacement struct {
+ NewName string
+}
+
+// ToCDNServiceUpdateMap converts a NameReplacement into a request body fragment suitable for the
+// Update call.
+func (r NameReplacement) ToCDNServiceUpdateMap() map[string]interface{} {
+ return map[string]interface{}{
+ "op": "replace",
+ "path": "/name",
+ "value": r.NewName,
+ }
+}
+
+// Removal is a Patch that requests the removal of a service parameter (Domain, Origin, or
+// CacheRule) by index. Pass it to the Update function as part of the Patch slice.
+type Removal struct {
+ Path Path
+ Index int64
+ All bool
+}
+
+// ToCDNServiceUpdateMap converts a Removal into a request body fragment suitable for the
+// Update call.
+func (r Removal) ToCDNServiceUpdateMap() map[string]interface{} {
+ result := map[string]interface{}{"op": "remove"}
+ if r.All {
+ result["path"] = r.Path.renderRoot()
+ } else {
+ result["path"] = r.Path.renderIndex(r.Index)
+ }
+ return result
+}
+
+type UpdateOpts []Patch
+
+// Update accepts a slice of Patch operations (Insertion, Append, Replacement or Removal) and
+// updates an existing CDN service using the values provided. idOrURL can be either the service's
+// URL or its ID. For example, both "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0" and
+// "https://global.cdn.api.rackspacecloud.com/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0"
+// are valid options for idOrURL.
+func Update(c *gophercloud.ServiceClient, idOrURL string, opts UpdateOpts) UpdateResult {
+ var url string
+ if strings.Contains(idOrURL, "/") {
+ url = idOrURL
+ } else {
+ url = updateURL(c, idOrURL)
+ }
+
+ reqBody := make([]map[string]interface{}, len(opts))
+ for i, patch := range opts {
+ reqBody[i] = patch.ToCDNServiceUpdateMap()
+ }
+
+ resp, err := perigee.Request("PATCH", url, perigee.Options{
+ MoreHeaders: c.AuthenticatedHeaders(),
+ ReqBody: &reqBody,
+ OkCodes: []int{202},
+ })
+ var result UpdateResult
+ result.Header = resp.HttpResponse.Header
+ result.Err = err
+ return result
+}
+
+// Delete accepts a service's ID or its URL and deletes the CDN service
+// associated with it. For example, both "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0" and
+// "https://global.cdn.api.rackspacecloud.com/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0"
+// are valid options for idOrURL.
+func Delete(c *gophercloud.ServiceClient, idOrURL string) DeleteResult {
+ var url string
+ if strings.Contains(idOrURL, "/") {
+ url = idOrURL
+ } else {
+ url = deleteURL(c, idOrURL)
+ }
+
+ var res DeleteResult
+ _, res.Err = perigee.Request("DELETE", url, perigee.Options{
+ MoreHeaders: c.AuthenticatedHeaders(),
+ OkCodes: []int{202},
+ })
+ return res
+}
diff --git a/openstack/cdn/v1/services/requests_test.go b/openstack/cdn/v1/services/requests_test.go
new file mode 100644
index 0000000..59e826f
--- /dev/null
+++ b/openstack/cdn/v1/services/requests_test.go
@@ -0,0 +1,358 @@
+package services
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestList(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ HandleListCDNServiceSuccessfully(t)
+
+ count := 0
+
+ err := List(fake.ServiceClient(), &ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractServices(page)
+ if err != nil {
+ t.Errorf("Failed to extract services: %v", err)
+ return false, err
+ }
+
+ expected := []Service{
+ Service{
+ ID: "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0",
+ Name: "mywebsite.com",
+ Domains: []Domain{
+ Domain{
+ Domain: "www.mywebsite.com",
+ },
+ },
+ Origins: []Origin{
+ Origin{
+ Origin: "mywebsite.com",
+ Port: 80,
+ SSL: false,
+ },
+ },
+ Caching: []CacheRule{
+ CacheRule{
+ Name: "default",
+ TTL: 3600,
+ },
+ CacheRule{
+ Name: "home",
+ TTL: 17200,
+ Rules: []TTLRule{
+ TTLRule{
+ Name: "index",
+ RequestURL: "/index.htm",
+ },
+ },
+ },
+ CacheRule{
+ Name: "images",
+ TTL: 12800,
+ Rules: []TTLRule{
+ TTLRule{
+ Name: "images",
+ RequestURL: "*.png",
+ },
+ },
+ },
+ },
+ Restrictions: []Restriction{
+ Restriction{
+ Name: "website only",
+ Rules: []RestrictionRule{
+ RestrictionRule{
+ Name: "mywebsite.com",
+ Referrer: "www.mywebsite.com",
+ },
+ },
+ },
+ },
+ FlavorID: "asia",
+ Status: "deployed",
+ Errors: []Error{},
+ Links: []gophercloud.Link{
+ gophercloud.Link{
+ Href: "https://www.poppycdn.io/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0",
+ Rel: "self",
+ },
+ gophercloud.Link{
+ Href: "mywebsite.com.cdn123.poppycdn.net",
+ Rel: "access_url",
+ },
+ gophercloud.Link{
+ Href: "https://www.poppycdn.io/v1.0/flavors/asia",
+ Rel: "flavor",
+ },
+ },
+ },
+ Service{
+ ID: "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f1",
+ Name: "myothersite.com",
+ Domains: []Domain{
+ Domain{
+ Domain: "www.myothersite.com",
+ },
+ },
+ Origins: []Origin{
+ Origin{
+ Origin: "44.33.22.11",
+ Port: 80,
+ SSL: false,
+ },
+ Origin{
+ Origin: "77.66.55.44",
+ Port: 80,
+ SSL: false,
+ Rules: []OriginRule{
+ OriginRule{
+ Name: "videos",
+ RequestURL: "^/videos/*.m3u",
+ },
+ },
+ },
+ },
+ Caching: []CacheRule{
+ CacheRule{
+ Name: "default",
+ TTL: 3600,
+ },
+ },
+ Restrictions: []Restriction{},
+ FlavorID: "europe",
+ Status: "deployed",
+ Links: []gophercloud.Link{
+ gophercloud.Link{
+ Href: "https://www.poppycdn.io/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f1",
+ Rel: "self",
+ },
+ gophercloud.Link{
+ Href: "myothersite.com.poppycdn.net",
+ Rel: "access_url",
+ },
+ gophercloud.Link{
+ Href: "https://www.poppycdn.io/v1.0/flavors/europe",
+ Rel: "flavor",
+ },
+ },
+ },
+ }
+
+ th.CheckDeepEquals(t, expected, actual)
+
+ return true, nil
+ })
+ th.AssertNoErr(t, err)
+
+ if count != 1 {
+ t.Errorf("Expected 1 page, got %d", count)
+ }
+}
+
+func TestCreate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ HandleCreateCDNServiceSuccessfully(t)
+
+ createOpts := CreateOpts{
+ Name: "mywebsite.com",
+ Domains: []Domain{
+ Domain{
+ Domain: "www.mywebsite.com",
+ },
+ Domain{
+ Domain: "blog.mywebsite.com",
+ },
+ },
+ Origins: []Origin{
+ Origin{
+ Origin: "mywebsite.com",
+ Port: 80,
+ SSL: false,
+ },
+ },
+ Restrictions: []Restriction{
+ Restriction{
+ Name: "website only",
+ Rules: []RestrictionRule{
+ RestrictionRule{
+ Name: "mywebsite.com",
+ Referrer: "www.mywebsite.com",
+ },
+ },
+ },
+ },
+ Caching: []CacheRule{
+ CacheRule{
+ Name: "default",
+ TTL: 3600,
+ },
+ },
+ FlavorID: "cdn",
+ }
+
+ expected := "https://global.cdn.api.rackspacecloud.com/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0"
+ actual, err := Create(fake.ServiceClient(), createOpts).Extract()
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, expected, actual)
+}
+
+func TestGet(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ HandleGetCDNServiceSuccessfully(t)
+
+ expected := &Service{
+ ID: "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0",
+ Name: "mywebsite.com",
+ Domains: []Domain{
+ Domain{
+ Domain: "www.mywebsite.com",
+ Protocol: "http",
+ },
+ },
+ Origins: []Origin{
+ Origin{
+ Origin: "mywebsite.com",
+ Port: 80,
+ SSL: false,
+ },
+ },
+ Caching: []CacheRule{
+ CacheRule{
+ Name: "default",
+ TTL: 3600,
+ },
+ CacheRule{
+ Name: "home",
+ TTL: 17200,
+ Rules: []TTLRule{
+ TTLRule{
+ Name: "index",
+ RequestURL: "/index.htm",
+ },
+ },
+ },
+ CacheRule{
+ Name: "images",
+ TTL: 12800,
+ Rules: []TTLRule{
+ TTLRule{
+ Name: "images",
+ RequestURL: "*.png",
+ },
+ },
+ },
+ },
+ Restrictions: []Restriction{
+ Restriction{
+ Name: "website only",
+ Rules: []RestrictionRule{
+ RestrictionRule{
+ Name: "mywebsite.com",
+ Referrer: "www.mywebsite.com",
+ },
+ },
+ },
+ },
+ FlavorID: "cdn",
+ Status: "deployed",
+ Errors: []Error{},
+ Links: []gophercloud.Link{
+ gophercloud.Link{
+ Href: "https://global.cdn.api.rackspacecloud.com/v1.0/110011/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0",
+ Rel: "self",
+ },
+ gophercloud.Link{
+ Href: "blog.mywebsite.com.cdn1.raxcdn.com",
+ Rel: "access_url",
+ },
+ gophercloud.Link{
+ Href: "https://global.cdn.api.rackspacecloud.com/v1.0/110011/flavors/cdn",
+ Rel: "flavor",
+ },
+ },
+ }
+
+ actual, err := Get(fake.ServiceClient(), "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0").Extract()
+ th.AssertNoErr(t, err)
+ th.AssertDeepEquals(t, expected, actual)
+}
+
+func TestSuccessfulUpdate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ HandleUpdateCDNServiceSuccessfully(t)
+
+ expected := "https://www.poppycdn.io/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0"
+ ops := UpdateOpts{
+ // Append a single Domain
+ Append{Value: Domain{Domain: "appended.mocksite4.com"}},
+ // Insert a single Domain
+ Insertion{
+ Index: 4,
+ Value: Domain{Domain: "inserted.mocksite4.com"},
+ },
+ // Bulk addition
+ Append{
+ Value: DomainList{
+ Domain{Domain: "bulkadded1.mocksite4.com"},
+ Domain{Domain: "bulkadded2.mocksite4.com"},
+ },
+ },
+ // Replace a single Origin
+ Replacement{
+ Index: 2,
+ Value: Origin{Origin: "44.33.22.11", Port: 80, SSL: false},
+ },
+ // Bulk replace Origins
+ Replacement{
+ Index: 0, // Ignored
+ Value: OriginList{
+ Origin{Origin: "44.33.22.11", Port: 80, SSL: false},
+ Origin{Origin: "55.44.33.22", Port: 443, SSL: true},
+ },
+ },
+ // Remove a single CacheRule
+ Removal{
+ Index: 8,
+ Path: PathCaching,
+ },
+ // Bulk removal
+ Removal{
+ All: true,
+ Path: PathCaching,
+ },
+ // Service name replacement
+ NameReplacement{
+ NewName: "differentServiceName",
+ },
+ }
+
+ actual, err := Update(fake.ServiceClient(), "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0", ops).Extract()
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, expected, actual)
+}
+
+func TestDelete(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ HandleDeleteCDNServiceSuccessfully(t)
+
+ err := Delete(fake.ServiceClient(), "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0").ExtractErr()
+ th.AssertNoErr(t, err)
+}
diff --git a/openstack/cdn/v1/services/results.go b/openstack/cdn/v1/services/results.go
new file mode 100644
index 0000000..33406c4
--- /dev/null
+++ b/openstack/cdn/v1/services/results.go
@@ -0,0 +1,316 @@
+package services
+
+import (
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+
+ "github.com/mitchellh/mapstructure"
+)
+
+// Domain represents a domain used by users to access their website.
+type Domain struct {
+ // Specifies the domain used to access the assets on their website, for which
+ // a CNAME is given to the CDN provider.
+ Domain string `mapstructure:"domain" json:"domain"`
+ // Specifies the protocol used to access the assets on this domain. Only "http"
+ // or "https" are currently allowed. The default is "http".
+ Protocol string `mapstructure:"protocol" json:"protocol,omitempty"`
+}
+
+func (d Domain) toPatchValue() interface{} {
+ r := make(map[string]interface{})
+ r["domain"] = d.Domain
+ if d.Protocol != "" {
+ r["protocol"] = d.Protocol
+ }
+ return r
+}
+
+func (d Domain) appropriatePath() Path {
+ return PathDomains
+}
+
+func (d Domain) renderRootOr(render func(p Path) string) string {
+ return render(d.appropriatePath())
+}
+
+// DomainList provides a useful way to perform bulk operations in a single Patch.
+type DomainList []Domain
+
+func (list DomainList) toPatchValue() interface{} {
+ r := make([]interface{}, len(list))
+ for i, domain := range list {
+ r[i] = domain.toPatchValue()
+ }
+ return r
+}
+
+func (list DomainList) appropriatePath() Path {
+ return PathDomains
+}
+
+func (list DomainList) renderRootOr(_ func(p Path) string) string {
+ return list.appropriatePath().renderRoot()
+}
+
+// OriginRule represents a rule that defines when an origin should be accessed.
+type OriginRule struct {
+ // Specifies the name of this rule.
+ Name string `mapstructure:"name" json:"name"`
+ // Specifies the request URL this rule should match for this origin to be used. Regex is supported.
+ RequestURL string `mapstructure:"request_url" json:"request_url"`
+}
+
+// Origin specifies a list of origin domains or IP addresses where the original assets are stored.
+type Origin struct {
+ // Specifies the URL or IP address to pull origin content from.
+ Origin string `mapstructure:"origin" json:"origin"`
+ // Specifies the port used to access the origin. The default is port 80.
+ Port int `mapstructure:"port" json:"port,omitempty"`
+ // Specifies whether or not to use HTTPS to access the origin. The default
+ // is false.
+ SSL bool `mapstructure:"ssl" json:"ssl"`
+ // Specifies a collection of rules that define the conditions when this origin
+ // should be accessed. If there is more than one origin, the rules parameter is required.
+ Rules []OriginRule `mapstructure:"rules" json:"rules,omitempty"`
+}
+
+func (o Origin) toPatchValue() interface{} {
+ r := make(map[string]interface{})
+ r["origin"] = o.Origin
+ r["port"] = o.Port
+ r["ssl"] = o.SSL
+ if len(o.Rules) > 0 {
+ r["rules"] = make([]map[string]interface{}, len(o.Rules))
+ for index, rule := range o.Rules {
+ submap := r["rules"].([]map[string]interface{})[index]
+ submap["name"] = rule.Name
+ submap["request_url"] = rule.RequestURL
+ }
+ }
+ return r
+}
+
+func (o Origin) appropriatePath() Path {
+ return PathOrigins
+}
+
+func (o Origin) renderRootOr(render func(p Path) string) string {
+ return render(o.appropriatePath())
+}
+
+// OriginList provides a useful way to perform bulk operations in a single Patch.
+type OriginList []Origin
+
+func (list OriginList) toPatchValue() interface{} {
+ r := make([]interface{}, len(list))
+ for i, origin := range list {
+ r[i] = origin.toPatchValue()
+ }
+ return r
+}
+
+func (list OriginList) appropriatePath() Path {
+ return PathOrigins
+}
+
+func (list OriginList) renderRootOr(_ func(p Path) string) string {
+ return list.appropriatePath().renderRoot()
+}
+
+// TTLRule specifies a rule that determines if a TTL should be applied to an asset.
+type TTLRule struct {
+ // Specifies the name of this rule.
+ Name string `mapstructure:"name" json:"name"`
+ // Specifies the request URL this rule should match for this TTL to be used. Regex is supported.
+ RequestURL string `mapstructure:"request_url" json:"request_url"`
+}
+
+// CacheRule specifies the TTL rules for the assets under this service.
+type CacheRule struct {
+ // Specifies the name of this caching rule. Note: 'default' is a reserved name used for the default TTL setting.
+ Name string `mapstructure:"name" json:"name"`
+ // Specifies the TTL to apply.
+ TTL int `mapstructure:"ttl" json:"ttl"`
+ // Specifies a collection of rules that determine if this TTL should be applied to an asset.
+ Rules []TTLRule `mapstructure:"rules" json:"rules,omitempty"`
+}
+
+func (c CacheRule) toPatchValue() interface{} {
+ r := make(map[string]interface{})
+ r["name"] = c.Name
+ r["ttl"] = c.TTL
+ r["rules"] = make([]map[string]interface{}, len(c.Rules))
+ for index, rule := range c.Rules {
+ submap := r["rules"].([]map[string]interface{})[index]
+ submap["name"] = rule.Name
+ submap["request_url"] = rule.RequestURL
+ }
+ return r
+}
+
+func (c CacheRule) appropriatePath() Path {
+ return PathCaching
+}
+
+func (c CacheRule) renderRootOr(render func(p Path) string) string {
+ return render(c.appropriatePath())
+}
+
+// CacheRuleList provides a useful way to perform bulk operations in a single Patch.
+type CacheRuleList []CacheRule
+
+func (list CacheRuleList) toPatchValue() interface{} {
+ r := make([]interface{}, len(list))
+ for i, rule := range list {
+ r[i] = rule.toPatchValue()
+ }
+ return r
+}
+
+func (list CacheRuleList) appropriatePath() Path {
+ return PathCaching
+}
+
+func (list CacheRuleList) renderRootOr(_ func(p Path) string) string {
+ return list.appropriatePath().renderRoot()
+}
+
+// RestrictionRule specifies a rule that determines if this restriction should be applied to an asset.
+type RestrictionRule struct {
+ // Specifies the name of this rule.
+ Name string `mapstructure:"name" json:"name"`
+ // Specifies the http host that requests must come from.
+ Referrer string `mapstructure:"referrer" json:"referrer,omitempty"`
+}
+
+// Restriction specifies a restriction that defines who can access assets (content from the CDN cache).
+type Restriction struct {
+ // Specifies the name of this restriction.
+ Name string `mapstructure:"name" json:"name"`
+ // Specifies a collection of rules that determine if this TTL should be applied to an asset.
+ Rules []RestrictionRule `mapstructure:"rules" json:"rules"`
+}
+
+// Error specifies an error that occurred during the previous service action.
+type Error struct {
+ // Specifies an error message detailing why there is an error.
+ Message string `mapstructure:"message"`
+}
+
+// Service represents a CDN service resource.
+type Service struct {
+ // Specifies the service ID that represents distributed content. The value is
+ // a UUID, such as 96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0, that is generated by the server.
+ ID string `mapstructure:"id"`
+ // Specifies the name of the service.
+ Name string `mapstructure:"name"`
+ // Specifies a list of domains used by users to access their website.
+ Domains []Domain `mapstructure:"domains"`
+ // Specifies a list of origin domains or IP addresses where the original assets are stored.
+ Origins []Origin `mapstructure:"origins"`
+ // Specifies the TTL rules for the assets under this service. Supports wildcards for fine grained control.
+ Caching []CacheRule `mapstructure:"caching"`
+ // Specifies the restrictions that define who can access assets (content from the CDN cache).
+ Restrictions []Restriction `mapstructure:"restrictions" json:"restrictions,omitempty"`
+ // Specifies the CDN provider flavor ID to use. For a list of flavors, see the operation to list the available flavors.
+ FlavorID string `mapstructure:"flavor_id"`
+ // Specifies the current status of the service.
+ Status string `mapstructure:"status"`
+ // Specifies the list of errors that occurred during the previous service action.
+ Errors []Error `mapstructure:"errors"`
+ // Specifies the self-navigating JSON document paths.
+ Links []gophercloud.Link `mapstructure:"links"`
+}
+
+// ServicePage is the page returned by a pager when traversing over a
+// collection of CDN services.
+type ServicePage struct {
+ pagination.MarkerPageBase
+}
+
+// IsEmpty returns true if a ListResult contains no services.
+func (r ServicePage) IsEmpty() (bool, error) {
+ services, err := ExtractServices(r)
+ if err != nil {
+ return true, err
+ }
+ return len(services) == 0, nil
+}
+
+// LastMarker returns the last service in a ListResult.
+func (r ServicePage) LastMarker() (string, error) {
+ services, err := ExtractServices(r)
+ if err != nil {
+ return "", err
+ }
+ if len(services) == 0 {
+ return "", nil
+ }
+ return (services[len(services)-1]).ID, nil
+}
+
+// ExtractServices is a function that takes a ListResult and returns the services' information.
+func ExtractServices(page pagination.Page) ([]Service, error) {
+ var response struct {
+ Services []Service `mapstructure:"services"`
+ }
+
+ err := mapstructure.Decode(page.(ServicePage).Body, &response)
+ return response.Services, err
+}
+
+// CreateResult represents the result of a Create operation.
+type CreateResult struct {
+ gophercloud.Result
+}
+
+// Extract is a method that extracts the location of a newly created service.
+func (r CreateResult) Extract() (string, error) {
+ if r.Err != nil {
+ return "", r.Err
+ }
+ if l, ok := r.Header["Location"]; ok && len(l) > 0 {
+ return l[0], nil
+ }
+ return "", nil
+}
+
+// GetResult represents the result of a get operation.
+type GetResult struct {
+ gophercloud.Result
+}
+
+// Extract is a function that extracts a service from a GetResult.
+func (r GetResult) Extract() (*Service, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var res Service
+
+ err := mapstructure.Decode(r.Body, &res)
+
+ return &res, err
+}
+
+// UpdateResult represents the result of a Update operation.
+type UpdateResult struct {
+ gophercloud.Result
+}
+
+// Extract is a method that extracts the location of an updated service.
+func (r UpdateResult) Extract() (string, error) {
+ if r.Err != nil {
+ return "", r.Err
+ }
+ if l, ok := r.Header["Location"]; ok && len(l) > 0 {
+ return l[0], nil
+ }
+ return "", nil
+}
+
+// DeleteResult represents the result of a Delete operation.
+type DeleteResult struct {
+ gophercloud.ErrResult
+}
diff --git a/openstack/cdn/v1/services/urls.go b/openstack/cdn/v1/services/urls.go
new file mode 100644
index 0000000..d953d4c
--- /dev/null
+++ b/openstack/cdn/v1/services/urls.go
@@ -0,0 +1,23 @@
+package services
+
+import "github.com/rackspace/gophercloud"
+
+func listURL(c *gophercloud.ServiceClient) string {
+ return c.ServiceURL("services")
+}
+
+func createURL(c *gophercloud.ServiceClient) string {
+ return listURL(c)
+}
+
+func getURL(c *gophercloud.ServiceClient, id string) string {
+ return c.ServiceURL("services", id)
+}
+
+func updateURL(c *gophercloud.ServiceClient, id string) string {
+ return getURL(c, id)
+}
+
+func deleteURL(c *gophercloud.ServiceClient, id string) string {
+ return getURL(c, id)
+}
diff --git a/openstack/client.go b/openstack/client.go
index 99b3d46..9c12dca 100644
--- a/openstack/client.go
+++ b/openstack/client.go
@@ -203,3 +203,14 @@
}
return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil
}
+
+// NewCDNV1 creates a ServiceClient that may be used to access the OpenStack v1
+// CDN service.
+func NewCDNV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
+ eo.ApplyDefaults("cdn")
+ url, err := client.EndpointLocator(eo)
+ if err != nil {
+ return nil, err
+ }
+ return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil
+}
diff --git a/openstack/compute/v2/extensions/defsecrules/doc.go b/openstack/compute/v2/extensions/defsecrules/doc.go
new file mode 100644
index 0000000..2571a1a
--- /dev/null
+++ b/openstack/compute/v2/extensions/defsecrules/doc.go
@@ -0,0 +1 @@
+package defsecrules
diff --git a/openstack/compute/v2/extensions/defsecrules/fixtures.go b/openstack/compute/v2/extensions/defsecrules/fixtures.go
new file mode 100644
index 0000000..c28e492
--- /dev/null
+++ b/openstack/compute/v2/extensions/defsecrules/fixtures.go
@@ -0,0 +1,108 @@
+package defsecrules
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+const rootPath = "/os-security-group-default-rules"
+
+func mockListRulesResponse(t *testing.T) {
+ th.Mux.HandleFunc(rootPath, 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, `
+{
+ "security_group_default_rules": [
+ {
+ "from_port": 80,
+ "id": "{ruleID}",
+ "ip_protocol": "TCP",
+ "ip_range": {
+ "cidr": "10.10.10.0/24"
+ },
+ "to_port": 80
+ }
+ ]
+}
+ `)
+ })
+}
+
+func mockCreateRuleResponse(t *testing.T) {
+ th.Mux.HandleFunc(rootPath, func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "POST")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+ th.TestJSONRequest(t, r, `
+{
+ "security_group_default_rule": {
+ "ip_protocol": "TCP",
+ "from_port": 80,
+ "to_port": 80,
+ "cidr": "10.10.12.0/24"
+ }
+}
+ `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, `
+{
+ "security_group_default_rule": {
+ "from_port": 80,
+ "id": "{ruleID}",
+ "ip_protocol": "TCP",
+ "ip_range": {
+ "cidr": "10.10.12.0/24"
+ },
+ "to_port": 80
+ }
+}
+`)
+ })
+}
+
+func mockGetRuleResponse(t *testing.T, ruleID string) {
+ url := rootPath + "/" + ruleID
+ th.Mux.HandleFunc(url, 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, `
+{
+ "security_group_default_rule": {
+ "id": "{ruleID}",
+ "from_port": 80,
+ "to_port": 80,
+ "ip_protocol": "TCP",
+ "ip_range": {
+ "cidr": "10.10.12.0/24"
+ }
+ }
+}
+ `)
+ })
+}
+
+func mockDeleteRuleResponse(t *testing.T, ruleID string) {
+ url := rootPath + "/" + ruleID
+ th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "DELETE")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusNoContent)
+ })
+}
diff --git a/openstack/compute/v2/extensions/defsecrules/requests.go b/openstack/compute/v2/extensions/defsecrules/requests.go
new file mode 100644
index 0000000..7d19741
--- /dev/null
+++ b/openstack/compute/v2/extensions/defsecrules/requests.go
@@ -0,0 +1,111 @@
+package defsecrules
+
+import (
+ "errors"
+
+ "github.com/racker/perigee"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// List will return a collection of default rules.
+func List(client *gophercloud.ServiceClient) pagination.Pager {
+ createPage := func(r pagination.PageResult) pagination.Page {
+ return DefaultRulePage{pagination.SinglePageBase(r)}
+ }
+
+ return pagination.NewPager(client, rootURL(client), createPage)
+}
+
+// CreateOpts represents the configuration for adding a new default rule.
+type CreateOpts struct {
+ // Required - the lower bound of the port range that will be opened.
+ FromPort int `json:"from_port"`
+
+ // Required - the upper bound of the port range that will be opened.
+ ToPort int `json:"to_port"`
+
+ // Required - the protocol type that will be allowed, e.g. TCP.
+ IPProtocol string `json:"ip_protocol"`
+
+ // ONLY required if FromGroupID is blank. This represents the IP range that
+ // will be the source of network traffic to your security group. Use
+ // 0.0.0.0/0 to allow all IP addresses.
+ CIDR string `json:"cidr,omitempty"`
+}
+
+// CreateOptsBuilder builds the create rule options into a serializable format.
+type CreateOptsBuilder interface {
+ ToRuleCreateMap() (map[string]interface{}, error)
+}
+
+// ToRuleCreateMap builds the create rule options into a serializable format.
+func (opts CreateOpts) ToRuleCreateMap() (map[string]interface{}, error) {
+ rule := make(map[string]interface{})
+
+ if opts.FromPort == 0 {
+ return rule, errors.New("A FromPort must be set")
+ }
+ if opts.ToPort == 0 {
+ return rule, errors.New("A ToPort must be set")
+ }
+ if opts.IPProtocol == "" {
+ return rule, errors.New("A IPProtocol must be set")
+ }
+ if opts.CIDR == "" {
+ return rule, errors.New("A CIDR must be set")
+ }
+
+ rule["from_port"] = opts.FromPort
+ rule["to_port"] = opts.ToPort
+ rule["ip_protocol"] = opts.IPProtocol
+ rule["cidr"] = opts.CIDR
+
+ return map[string]interface{}{"security_group_default_rule": rule}, nil
+}
+
+// Create is the operation responsible for creating a new default rule.
+func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult {
+ var result CreateResult
+
+ reqBody, err := opts.ToRuleCreateMap()
+ if err != nil {
+ result.Err = err
+ return result
+ }
+
+ _, result.Err = perigee.Request("POST", rootURL(client), perigee.Options{
+ Results: &result.Body,
+ ReqBody: &reqBody,
+ MoreHeaders: client.AuthenticatedHeaders(),
+ OkCodes: []int{200},
+ })
+
+ return result
+}
+
+// Get will return details for a particular default rule.
+func Get(client *gophercloud.ServiceClient, id string) GetResult {
+ var result GetResult
+
+ _, result.Err = perigee.Request("GET", resourceURL(client, id), perigee.Options{
+ Results: &result.Body,
+ MoreHeaders: client.AuthenticatedHeaders(),
+ OkCodes: []int{200},
+ })
+
+ return result
+}
+
+// Delete will permanently delete a default rule from the project.
+func Delete(client *gophercloud.ServiceClient, id string) gophercloud.ErrResult {
+ var result gophercloud.ErrResult
+
+ _, result.Err = perigee.Request("DELETE", resourceURL(client, id), perigee.Options{
+ MoreHeaders: client.AuthenticatedHeaders(),
+ OkCodes: []int{204},
+ })
+
+ return result
+}
diff --git a/openstack/compute/v2/extensions/defsecrules/requests_test.go b/openstack/compute/v2/extensions/defsecrules/requests_test.go
new file mode 100644
index 0000000..d4ebe87
--- /dev/null
+++ b/openstack/compute/v2/extensions/defsecrules/requests_test.go
@@ -0,0 +1,100 @@
+package defsecrules
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/secgroups"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+ "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+const ruleID = "{ruleID}"
+
+func TestList(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockListRulesResponse(t)
+
+ count := 0
+
+ err := List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractDefaultRules(page)
+ th.AssertNoErr(t, err)
+
+ expected := []DefaultRule{
+ DefaultRule{
+ FromPort: 80,
+ ID: ruleID,
+ IPProtocol: "TCP",
+ IPRange: secgroups.IPRange{CIDR: "10.10.10.0/24"},
+ ToPort: 80,
+ },
+ }
+
+ th.CheckDeepEquals(t, expected, actual)
+
+ return true, nil
+ })
+
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, 1, count)
+}
+
+func TestCreate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockCreateRuleResponse(t)
+
+ opts := CreateOpts{
+ IPProtocol: "TCP",
+ FromPort: 80,
+ ToPort: 80,
+ CIDR: "10.10.12.0/24",
+ }
+
+ group, err := Create(client.ServiceClient(), opts).Extract()
+ th.AssertNoErr(t, err)
+
+ expected := &DefaultRule{
+ ID: ruleID,
+ FromPort: 80,
+ ToPort: 80,
+ IPProtocol: "TCP",
+ IPRange: secgroups.IPRange{CIDR: "10.10.12.0/24"},
+ }
+ th.AssertDeepEquals(t, expected, group)
+}
+
+func TestGet(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockGetRuleResponse(t, ruleID)
+
+ group, err := Get(client.ServiceClient(), ruleID).Extract()
+ th.AssertNoErr(t, err)
+
+ expected := &DefaultRule{
+ ID: ruleID,
+ FromPort: 80,
+ ToPort: 80,
+ IPProtocol: "TCP",
+ IPRange: secgroups.IPRange{CIDR: "10.10.12.0/24"},
+ }
+
+ th.AssertDeepEquals(t, expected, group)
+}
+
+func TestDelete(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockDeleteRuleResponse(t, ruleID)
+
+ err := Delete(client.ServiceClient(), ruleID).ExtractErr()
+ th.AssertNoErr(t, err)
+}
diff --git a/openstack/compute/v2/extensions/defsecrules/results.go b/openstack/compute/v2/extensions/defsecrules/results.go
new file mode 100644
index 0000000..e588d3e
--- /dev/null
+++ b/openstack/compute/v2/extensions/defsecrules/results.go
@@ -0,0 +1,69 @@
+package defsecrules
+
+import (
+ "github.com/mitchellh/mapstructure"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/secgroups"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// DefaultRule represents a default rule - which is identical to a
+// normal security rule.
+type DefaultRule secgroups.Rule
+
+// DefaultRulePage is a single page of a DefaultRule collection.
+type DefaultRulePage struct {
+ pagination.SinglePageBase
+}
+
+// IsEmpty determines whether or not a page of default rules contains any results.
+func (page DefaultRulePage) IsEmpty() (bool, error) {
+ users, err := ExtractDefaultRules(page)
+ if err != nil {
+ return false, err
+ }
+ return len(users) == 0, nil
+}
+
+// ExtractDefaultRules returns a slice of DefaultRules contained in a single
+// page of results.
+func ExtractDefaultRules(page pagination.Page) ([]DefaultRule, error) {
+ casted := page.(DefaultRulePage).Body
+ var response struct {
+ Rules []DefaultRule `mapstructure:"security_group_default_rules"`
+ }
+
+ err := mapstructure.WeakDecode(casted, &response)
+
+ return response.Rules, err
+}
+
+type commonResult struct {
+ gophercloud.Result
+}
+
+// CreateResult represents the result of a create operation.
+type CreateResult struct {
+ commonResult
+}
+
+// GetResult represents the result of a get operation.
+type GetResult struct {
+ commonResult
+}
+
+// Extract will extract a DefaultRule struct from most responses.
+func (r commonResult) Extract() (*DefaultRule, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var response struct {
+ Rule DefaultRule `mapstructure:"security_group_default_rule"`
+ }
+
+ err := mapstructure.WeakDecode(r.Body, &response)
+
+ return &response.Rule, err
+}
diff --git a/openstack/compute/v2/extensions/defsecrules/urls.go b/openstack/compute/v2/extensions/defsecrules/urls.go
new file mode 100644
index 0000000..cc928ab
--- /dev/null
+++ b/openstack/compute/v2/extensions/defsecrules/urls.go
@@ -0,0 +1,13 @@
+package defsecrules
+
+import "github.com/rackspace/gophercloud"
+
+const rulepath = "os-security-group-default-rules"
+
+func resourceURL(c *gophercloud.ServiceClient, id string) string {
+ return c.ServiceURL(rulepath, id)
+}
+
+func rootURL(c *gophercloud.ServiceClient) string {
+ return c.ServiceURL(rulepath)
+}
diff --git a/openstack/compute/v2/extensions/keypairs/requests.go b/openstack/compute/v2/extensions/keypairs/requests.go
index 7d1a2ac..7b372a3 100644
--- a/openstack/compute/v2/extensions/keypairs/requests.go
+++ b/openstack/compute/v2/extensions/keypairs/requests.go
@@ -5,9 +5,34 @@
"github.com/racker/perigee"
"github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/openstack/compute/v2/servers"
"github.com/rackspace/gophercloud/pagination"
)
+// CreateOptsExt adds a KeyPair option to the base CreateOpts.
+type CreateOptsExt struct {
+ servers.CreateOptsBuilder
+ KeyName string `json:"key_name,omitempty"`
+}
+
+// ToServerCreateMap adds the key_name and, optionally, key_data options to
+// the base server creation options.
+func (opts CreateOptsExt) ToServerCreateMap() (map[string]interface{}, error) {
+ base, err := opts.CreateOptsBuilder.ToServerCreateMap()
+ if err != nil {
+ return nil, err
+ }
+
+ if opts.KeyName == "" {
+ return base, nil
+ }
+
+ serverMap := base["server"].(map[string]interface{})
+ serverMap["key_name"] = opts.KeyName
+
+ return base, nil
+}
+
// List returns a Pager that allows you to iterate over a collection of KeyPairs.
func List(client *gophercloud.ServiceClient) pagination.Pager {
return pagination.NewPager(client, listURL(client), func(r pagination.PageResult) pagination.Page {
@@ -21,7 +46,7 @@
ToKeyPairCreateMap() (map[string]interface{}, error)
}
-// CreateOpts species keypair creation or import parameters.
+// CreateOpts specifies keypair creation or import parameters.
type CreateOpts struct {
// Name [required] is a friendly name to refer to this KeyPair in other services.
Name string
diff --git a/openstack/compute/v2/extensions/secgroups/doc.go b/openstack/compute/v2/extensions/secgroups/doc.go
new file mode 100644
index 0000000..702f32c
--- /dev/null
+++ b/openstack/compute/v2/extensions/secgroups/doc.go
@@ -0,0 +1 @@
+package secgroups
diff --git a/openstack/compute/v2/extensions/secgroups/fixtures.go b/openstack/compute/v2/extensions/secgroups/fixtures.go
new file mode 100644
index 0000000..ca76f68
--- /dev/null
+++ b/openstack/compute/v2/extensions/secgroups/fixtures.go
@@ -0,0 +1,265 @@
+package secgroups
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+const rootPath = "/os-security-groups"
+
+const listGroupsJSON = `
+{
+ "security_groups": [
+ {
+ "description": "default",
+ "id": "{groupID}",
+ "name": "default",
+ "rules": [],
+ "tenant_id": "openstack"
+ }
+ ]
+}
+`
+
+func mockListGroupsResponse(t *testing.T) {
+ th.Mux.HandleFunc(rootPath, 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, listGroupsJSON)
+ })
+}
+
+func mockListGroupsByServerResponse(t *testing.T, serverID string) {
+ url := fmt.Sprintf("%s/servers/%s%s", rootPath, serverID, rootPath)
+ th.Mux.HandleFunc(url, 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, listGroupsJSON)
+ })
+}
+
+func mockCreateGroupResponse(t *testing.T) {
+ th.Mux.HandleFunc(rootPath, func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "POST")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+ th.TestJSONRequest(t, r, `
+{
+ "security_group": {
+ "name": "test",
+ "description": "something"
+ }
+}
+ `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, `
+{
+ "security_group": {
+ "description": "something",
+ "id": "{groupID}",
+ "name": "test",
+ "rules": [],
+ "tenant_id": "openstack"
+ }
+}
+`)
+ })
+}
+
+func mockUpdateGroupResponse(t *testing.T, groupID string) {
+ url := fmt.Sprintf("%s/%s", rootPath, groupID)
+ th.Mux.HandleFunc(url, 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, `
+{
+ "security_group": {
+ "name": "new_name",
+ "description": "new_desc"
+ }
+}
+ `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, `
+{
+ "security_group": {
+ "description": "something",
+ "id": "{groupID}",
+ "name": "new_name",
+ "rules": [],
+ "tenant_id": "openstack"
+ }
+}
+`)
+ })
+}
+
+func mockGetGroupsResponse(t *testing.T, groupID string) {
+ url := fmt.Sprintf("%s/%s", rootPath, groupID)
+ th.Mux.HandleFunc(url, 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, `
+{
+ "security_group": {
+ "description": "default",
+ "id": "{groupID}",
+ "name": "default",
+ "rules": [
+ {
+ "from_port": 80,
+ "group": {
+ "tenant_id": "openstack",
+ "name": "default"
+ },
+ "ip_protocol": "TCP",
+ "to_port": 85,
+ "parent_group_id": "{groupID}",
+ "ip_range": {
+ "cidr": "0.0.0.0"
+ },
+ "id": "{ruleID}"
+ }
+ ],
+ "tenant_id": "openstack"
+ }
+}
+ `)
+ })
+}
+
+func mockGetNumericIDGroupResponse(t *testing.T, groupID int) {
+ url := fmt.Sprintf("%s/%d", rootPath, groupID)
+ th.Mux.HandleFunc(url, 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, `
+{
+ "security_group": {
+ "id": 12345
+ }
+}
+ `)
+ })
+}
+
+func mockDeleteGroupResponse(t *testing.T, groupID string) {
+ url := fmt.Sprintf("%s/%s", rootPath, groupID)
+ th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "DELETE")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusAccepted)
+ })
+}
+
+func mockAddRuleResponse(t *testing.T) {
+ th.Mux.HandleFunc("/os-security-group-rules", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "POST")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+ th.TestJSONRequest(t, r, `
+{
+ "security_group_rule": {
+ "from_port": 22,
+ "ip_protocol": "TCP",
+ "to_port": 22,
+ "parent_group_id": "{groupID}",
+ "cidr": "0.0.0.0/0"
+ }
+} `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, `
+{
+ "security_group_rule": {
+ "from_port": 22,
+ "group": {},
+ "ip_protocol": "TCP",
+ "to_port": 22,
+ "parent_group_id": "{groupID}",
+ "ip_range": {
+ "cidr": "0.0.0.0/0"
+ },
+ "id": "{ruleID}"
+ }
+}`)
+ })
+}
+
+func mockDeleteRuleResponse(t *testing.T, ruleID string) {
+ url := fmt.Sprintf("/os-security-group-rules/%s", ruleID)
+ th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "DELETE")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusAccepted)
+ })
+}
+
+func mockAddServerToGroupResponse(t *testing.T, serverID string) {
+ url := fmt.Sprintf("/servers/%s/action", serverID)
+ th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "POST")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+ th.TestJSONRequest(t, r, `
+{
+ "addSecurityGroup": {
+ "name": "test"
+ }
+}
+ `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusAccepted)
+ })
+}
+
+func mockRemoveServerFromGroupResponse(t *testing.T, serverID string) {
+ url := fmt.Sprintf("/servers/%s/action", serverID)
+ th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "POST")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+ th.TestJSONRequest(t, r, `
+{
+ "removeSecurityGroup": {
+ "name": "test"
+ }
+}
+ `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusAccepted)
+ })
+}
diff --git a/openstack/compute/v2/extensions/secgroups/requests.go b/openstack/compute/v2/extensions/secgroups/requests.go
new file mode 100644
index 0000000..09503d7
--- /dev/null
+++ b/openstack/compute/v2/extensions/secgroups/requests.go
@@ -0,0 +1,298 @@
+package secgroups
+
+import (
+ "errors"
+
+ "github.com/racker/perigee"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+func commonList(client *gophercloud.ServiceClient, url string) pagination.Pager {
+ createPage := func(r pagination.PageResult) pagination.Page {
+ return SecurityGroupPage{pagination.SinglePageBase(r)}
+ }
+
+ return pagination.NewPager(client, url, createPage)
+}
+
+// List will return a collection of all the security groups for a particular
+// tenant.
+func List(client *gophercloud.ServiceClient) pagination.Pager {
+ return commonList(client, rootURL(client))
+}
+
+// ListByServer will return a collection of all the security groups which are
+// associated with a particular server.
+func ListByServer(client *gophercloud.ServiceClient, serverID string) pagination.Pager {
+ return commonList(client, listByServerURL(client, serverID))
+}
+
+// GroupOpts is the underlying struct responsible for creating or updating
+// security groups. It therefore represents the mutable attributes of a
+// security group.
+type GroupOpts struct {
+ // Required - the name of your security group.
+ Name string `json:"name"`
+
+ // Required - the description of your security group.
+ Description string `json:"description"`
+}
+
+// CreateOpts is the struct responsible for creating a security group.
+type CreateOpts GroupOpts
+
+// CreateOptsBuilder builds the create options into a serializable format.
+type CreateOptsBuilder interface {
+ ToSecGroupCreateMap() (map[string]interface{}, error)
+}
+
+var (
+ errName = errors.New("Name is a required field")
+ errDesc = errors.New("Description is a required field")
+)
+
+// ToSecGroupCreateMap builds the create options into a serializable format.
+func (opts CreateOpts) ToSecGroupCreateMap() (map[string]interface{}, error) {
+ sg := make(map[string]interface{})
+
+ if opts.Name == "" {
+ return sg, errName
+ }
+ if opts.Description == "" {
+ return sg, errDesc
+ }
+
+ sg["name"] = opts.Name
+ sg["description"] = opts.Description
+
+ return map[string]interface{}{"security_group": sg}, nil
+}
+
+// Create will create a new security group.
+func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult {
+ var result CreateResult
+
+ reqBody, err := opts.ToSecGroupCreateMap()
+ if err != nil {
+ result.Err = err
+ return result
+ }
+
+ _, result.Err = perigee.Request("POST", rootURL(client), perigee.Options{
+ Results: &result.Body,
+ ReqBody: &reqBody,
+ MoreHeaders: client.AuthenticatedHeaders(),
+ OkCodes: []int{200},
+ })
+
+ return result
+}
+
+// UpdateOpts is the struct responsible for updating an existing security group.
+type UpdateOpts GroupOpts
+
+// UpdateOptsBuilder builds the update options into a serializable format.
+type UpdateOptsBuilder interface {
+ ToSecGroupUpdateMap() (map[string]interface{}, error)
+}
+
+// ToSecGroupUpdateMap builds the update options into a serializable format.
+func (opts UpdateOpts) ToSecGroupUpdateMap() (map[string]interface{}, error) {
+ sg := make(map[string]interface{})
+
+ if opts.Name == "" {
+ return sg, errName
+ }
+ if opts.Description == "" {
+ return sg, errDesc
+ }
+
+ sg["name"] = opts.Name
+ sg["description"] = opts.Description
+
+ return map[string]interface{}{"security_group": sg}, nil
+}
+
+// Update will modify the mutable properties of a security group, notably its
+// name and description.
+func Update(client *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) UpdateResult {
+ var result UpdateResult
+
+ reqBody, err := opts.ToSecGroupUpdateMap()
+ if err != nil {
+ result.Err = err
+ return result
+ }
+
+ _, result.Err = perigee.Request("PUT", resourceURL(client, id), perigee.Options{
+ Results: &result.Body,
+ ReqBody: &reqBody,
+ MoreHeaders: client.AuthenticatedHeaders(),
+ OkCodes: []int{200},
+ })
+
+ return result
+}
+
+// Get will return details for a particular security group.
+func Get(client *gophercloud.ServiceClient, id string) GetResult {
+ var result GetResult
+
+ _, result.Err = perigee.Request("GET", resourceURL(client, id), perigee.Options{
+ Results: &result.Body,
+ MoreHeaders: client.AuthenticatedHeaders(),
+ OkCodes: []int{200},
+ })
+
+ return result
+}
+
+// Delete will permanently delete a security group from the project.
+func Delete(client *gophercloud.ServiceClient, id string) gophercloud.ErrResult {
+ var result gophercloud.ErrResult
+
+ _, result.Err = perigee.Request("DELETE", resourceURL(client, id), perigee.Options{
+ MoreHeaders: client.AuthenticatedHeaders(),
+ OkCodes: []int{202},
+ })
+
+ return result
+}
+
+// CreateRuleOpts represents the configuration for adding a new rule to an
+// existing security group.
+type CreateRuleOpts struct {
+ // Required - the ID of the group that this rule will be added to.
+ ParentGroupID string `json:"parent_group_id"`
+
+ // Required - the lower bound of the port range that will be opened.
+ FromPort int `json:"from_port"`
+
+ // Required - the upper bound of the port range that will be opened.
+ ToPort int `json:"to_port"`
+
+ // Required - the protocol type that will be allowed, e.g. TCP.
+ IPProtocol string `json:"ip_protocol"`
+
+ // ONLY required if FromGroupID is blank. This represents the IP range that
+ // will be the source of network traffic to your security group. Use
+ // 0.0.0.0/0 to allow all IP addresses.
+ CIDR string `json:"cidr,omitempty"`
+
+ // ONLY required if CIDR is blank. This value represents the ID of a group
+ // that forwards traffic to the parent group. So, instead of accepting
+ // network traffic from an entire IP range, you can instead refine the
+ // inbound source by an existing security group.
+ FromGroupID string `json:"group_id,omitempty"`
+}
+
+// CreateRuleOptsBuilder builds the create rule options into a serializable format.
+type CreateRuleOptsBuilder interface {
+ ToRuleCreateMap() (map[string]interface{}, error)
+}
+
+// ToRuleCreateMap builds the create rule options into a serializable format.
+func (opts CreateRuleOpts) ToRuleCreateMap() (map[string]interface{}, error) {
+ rule := make(map[string]interface{})
+
+ if opts.ParentGroupID == "" {
+ return rule, errors.New("A ParentGroupID must be set")
+ }
+ if opts.FromPort == 0 {
+ return rule, errors.New("A FromPort must be set")
+ }
+ if opts.ToPort == 0 {
+ return rule, errors.New("A ToPort must be set")
+ }
+ if opts.IPProtocol == "" {
+ return rule, errors.New("A IPProtocol must be set")
+ }
+ if opts.CIDR == "" && opts.FromGroupID == "" {
+ return rule, errors.New("A CIDR or FromGroupID must be set")
+ }
+
+ rule["parent_group_id"] = opts.ParentGroupID
+ rule["from_port"] = opts.FromPort
+ rule["to_port"] = opts.ToPort
+ rule["ip_protocol"] = opts.IPProtocol
+
+ if opts.CIDR != "" {
+ rule["cidr"] = opts.CIDR
+ }
+ if opts.FromGroupID != "" {
+ rule["from_group_id"] = opts.FromGroupID
+ }
+
+ return map[string]interface{}{"security_group_rule": rule}, nil
+}
+
+// CreateRule will add a new rule to an existing security group (whose ID is
+// specified in CreateRuleOpts). You have the option of controlling inbound
+// traffic from either an IP range (CIDR) or from another security group.
+func CreateRule(client *gophercloud.ServiceClient, opts CreateRuleOptsBuilder) CreateRuleResult {
+ var result CreateRuleResult
+
+ reqBody, err := opts.ToRuleCreateMap()
+ if err != nil {
+ result.Err = err
+ return result
+ }
+
+ _, result.Err = perigee.Request("POST", rootRuleURL(client), perigee.Options{
+ Results: &result.Body,
+ ReqBody: &reqBody,
+ MoreHeaders: client.AuthenticatedHeaders(),
+ OkCodes: []int{200},
+ })
+
+ return result
+}
+
+// DeleteRule will permanently delete a rule from a security group.
+func DeleteRule(client *gophercloud.ServiceClient, id string) gophercloud.ErrResult {
+ var result gophercloud.ErrResult
+
+ _, result.Err = perigee.Request("DELETE", resourceRuleURL(client, id), perigee.Options{
+ MoreHeaders: client.AuthenticatedHeaders(),
+ OkCodes: []int{202},
+ })
+
+ return result
+}
+
+func actionMap(prefix, groupName string) map[string]map[string]string {
+ return map[string]map[string]string{
+ prefix + "SecurityGroup": map[string]string{"name": groupName},
+ }
+}
+
+// AddServerToGroup will associate a server and a security group, enforcing the
+// rules of the group on the server.
+func AddServerToGroup(client *gophercloud.ServiceClient, serverID, groupName string) gophercloud.ErrResult {
+ var result gophercloud.ErrResult
+
+ _, result.Err = perigee.Request("POST", serverActionURL(client, serverID), perigee.Options{
+ Results: &result.Body,
+ ReqBody: actionMap("add", groupName),
+ MoreHeaders: client.AuthenticatedHeaders(),
+ OkCodes: []int{202},
+ })
+
+ return result
+}
+
+// RemoveServerFromGroup will disassociate a server from a security group.
+func RemoveServerFromGroup(client *gophercloud.ServiceClient, serverID, groupName string) gophercloud.ErrResult {
+ var result gophercloud.ErrResult
+
+ _, result.Err = perigee.Request("POST", serverActionURL(client, serverID), perigee.Options{
+ Results: &result.Body,
+ ReqBody: actionMap("remove", groupName),
+ MoreHeaders: client.AuthenticatedHeaders(),
+ OkCodes: []int{202},
+ })
+
+ return result
+}
diff --git a/openstack/compute/v2/extensions/secgroups/requests_test.go b/openstack/compute/v2/extensions/secgroups/requests_test.go
new file mode 100644
index 0000000..4e21d5d
--- /dev/null
+++ b/openstack/compute/v2/extensions/secgroups/requests_test.go
@@ -0,0 +1,248 @@
+package secgroups
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+ "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+const (
+ serverID = "{serverID}"
+ groupID = "{groupID}"
+ ruleID = "{ruleID}"
+)
+
+func TestList(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockListGroupsResponse(t)
+
+ count := 0
+
+ err := List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractSecurityGroups(page)
+ if err != nil {
+ t.Errorf("Failed to extract users: %v", err)
+ return false, err
+ }
+
+ expected := []SecurityGroup{
+ SecurityGroup{
+ ID: groupID,
+ Description: "default",
+ Name: "default",
+ Rules: []Rule{},
+ TenantID: "openstack",
+ },
+ }
+
+ th.CheckDeepEquals(t, expected, actual)
+
+ return true, nil
+ })
+
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, 1, count)
+}
+
+func TestListByServer(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockListGroupsByServerResponse(t, serverID)
+
+ count := 0
+
+ err := ListByServer(client.ServiceClient(), serverID).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractSecurityGroups(page)
+ if err != nil {
+ t.Errorf("Failed to extract users: %v", err)
+ return false, err
+ }
+
+ expected := []SecurityGroup{
+ SecurityGroup{
+ ID: groupID,
+ Description: "default",
+ Name: "default",
+ Rules: []Rule{},
+ TenantID: "openstack",
+ },
+ }
+
+ th.CheckDeepEquals(t, expected, actual)
+
+ return true, nil
+ })
+
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, 1, count)
+}
+
+func TestCreate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockCreateGroupResponse(t)
+
+ opts := CreateOpts{
+ Name: "test",
+ Description: "something",
+ }
+
+ group, err := Create(client.ServiceClient(), opts).Extract()
+ th.AssertNoErr(t, err)
+
+ expected := &SecurityGroup{
+ ID: groupID,
+ Name: "test",
+ Description: "something",
+ TenantID: "openstack",
+ Rules: []Rule{},
+ }
+ th.AssertDeepEquals(t, expected, group)
+}
+
+func TestUpdate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockUpdateGroupResponse(t, groupID)
+
+ opts := UpdateOpts{
+ Name: "new_name",
+ Description: "new_desc",
+ }
+
+ group, err := Update(client.ServiceClient(), groupID, opts).Extract()
+ th.AssertNoErr(t, err)
+
+ expected := &SecurityGroup{
+ ID: groupID,
+ Name: "new_name",
+ Description: "something",
+ TenantID: "openstack",
+ Rules: []Rule{},
+ }
+ th.AssertDeepEquals(t, expected, group)
+}
+
+func TestGet(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockGetGroupsResponse(t, groupID)
+
+ group, err := Get(client.ServiceClient(), groupID).Extract()
+ th.AssertNoErr(t, err)
+
+ expected := &SecurityGroup{
+ ID: groupID,
+ Description: "default",
+ Name: "default",
+ TenantID: "openstack",
+ Rules: []Rule{
+ Rule{
+ FromPort: 80,
+ ToPort: 85,
+ IPProtocol: "TCP",
+ IPRange: IPRange{CIDR: "0.0.0.0"},
+ Group: Group{TenantID: "openstack", Name: "default"},
+ ParentGroupID: groupID,
+ ID: ruleID,
+ },
+ },
+ }
+
+ th.AssertDeepEquals(t, expected, group)
+}
+
+func TestGetNumericID(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ numericGroupID := 12345
+
+ mockGetNumericIDGroupResponse(t, numericGroupID)
+
+ group, err := Get(client.ServiceClient(), "12345").Extract()
+ th.AssertNoErr(t, err)
+
+ expected := &SecurityGroup{ID: "12345"}
+ th.AssertDeepEquals(t, expected, group)
+}
+
+func TestDelete(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockDeleteGroupResponse(t, groupID)
+
+ err := Delete(client.ServiceClient(), groupID).ExtractErr()
+ th.AssertNoErr(t, err)
+}
+
+func TestAddRule(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockAddRuleResponse(t)
+
+ opts := CreateRuleOpts{
+ ParentGroupID: groupID,
+ FromPort: 22,
+ ToPort: 22,
+ IPProtocol: "TCP",
+ CIDR: "0.0.0.0/0",
+ }
+
+ rule, err := CreateRule(client.ServiceClient(), opts).Extract()
+ th.AssertNoErr(t, err)
+
+ expected := &Rule{
+ FromPort: 22,
+ ToPort: 22,
+ Group: Group{},
+ IPProtocol: "TCP",
+ ParentGroupID: groupID,
+ IPRange: IPRange{CIDR: "0.0.0.0/0"},
+ ID: ruleID,
+ }
+
+ th.AssertDeepEquals(t, expected, rule)
+}
+
+func TestDeleteRule(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockDeleteRuleResponse(t, ruleID)
+
+ err := DeleteRule(client.ServiceClient(), ruleID).ExtractErr()
+ th.AssertNoErr(t, err)
+}
+
+func TestAddServer(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockAddServerToGroupResponse(t, serverID)
+
+ err := AddServerToGroup(client.ServiceClient(), serverID, "test").ExtractErr()
+ th.AssertNoErr(t, err)
+}
+
+func TestRemoveServer(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockRemoveServerFromGroupResponse(t, serverID)
+
+ err := RemoveServerFromGroup(client.ServiceClient(), serverID, "test").ExtractErr()
+ th.AssertNoErr(t, err)
+}
diff --git a/openstack/compute/v2/extensions/secgroups/results.go b/openstack/compute/v2/extensions/secgroups/results.go
new file mode 100644
index 0000000..478c5dc
--- /dev/null
+++ b/openstack/compute/v2/extensions/secgroups/results.go
@@ -0,0 +1,147 @@
+package secgroups
+
+import (
+ "github.com/mitchellh/mapstructure"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// SecurityGroup represents a security group.
+type SecurityGroup struct {
+ // The unique ID of the group. If Neutron is installed, this ID will be
+ // represented as a string UUID; if Neutron is not installed, it will be a
+ // numeric ID. For the sake of consistency, we always cast it to a string.
+ ID string
+
+ // The human-readable name of the group, which needs to be unique.
+ Name string
+
+ // The human-readable description of the group.
+ Description string
+
+ // The rules which determine how this security group operates.
+ Rules []Rule
+
+ // The ID of the tenant to which this security group belongs.
+ TenantID string `mapstructure:"tenant_id"`
+}
+
+// Rule represents a security group rule, a policy which determines how a
+// security group operates and what inbound traffic it allows in.
+type Rule struct {
+ // The unique ID. If Neutron is installed, this ID will be
+ // represented as a string UUID; if Neutron is not installed, it will be a
+ // numeric ID. For the sake of consistency, we always cast it to a string.
+ ID string
+
+ // The lower bound of the port range which this security group should open up
+ FromPort int `mapstructure:"from_port"`
+
+ // The upper bound of the port range which this security group should open up
+ ToPort int `mapstructure:"to_port"`
+
+ // The IP protocol (e.g. TCP) which the security group accepts
+ IPProtocol string `mapstructure:"ip_protocol"`
+
+ // The CIDR IP range whose traffic can be received
+ IPRange IPRange `mapstructure:"ip_range"`
+
+ // The security group ID to which this rule belongs
+ ParentGroupID string `mapstructure:"parent_group_id"`
+
+ // Not documented.
+ Group Group
+}
+
+// IPRange represents the IP range whose traffic will be accepted by the
+// security group.
+type IPRange struct {
+ CIDR string
+}
+
+// Group represents a group.
+type Group struct {
+ TenantID string `mapstructure:"tenant_id"`
+ Name string
+}
+
+// SecurityGroupPage is a single page of a SecurityGroup collection.
+type SecurityGroupPage struct {
+ pagination.SinglePageBase
+}
+
+// IsEmpty determines whether or not a page of Security Groups contains any results.
+func (page SecurityGroupPage) IsEmpty() (bool, error) {
+ users, err := ExtractSecurityGroups(page)
+ if err != nil {
+ return false, err
+ }
+ return len(users) == 0, nil
+}
+
+// ExtractSecurityGroups returns a slice of SecurityGroups contained in a single page of results.
+func ExtractSecurityGroups(page pagination.Page) ([]SecurityGroup, error) {
+ casted := page.(SecurityGroupPage).Body
+ var response struct {
+ SecurityGroups []SecurityGroup `mapstructure:"security_groups"`
+ }
+
+ err := mapstructure.WeakDecode(casted, &response)
+
+ return response.SecurityGroups, err
+}
+
+type commonResult struct {
+ gophercloud.Result
+}
+
+// CreateResult represents the result of a create operation.
+type CreateResult struct {
+ commonResult
+}
+
+// GetResult represents the result of a get operation.
+type GetResult struct {
+ commonResult
+}
+
+// UpdateResult represents the result of an update operation.
+type UpdateResult struct {
+ commonResult
+}
+
+// Extract will extract a SecurityGroup struct from most responses.
+func (r commonResult) Extract() (*SecurityGroup, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var response struct {
+ SecurityGroup SecurityGroup `mapstructure:"security_group"`
+ }
+
+ err := mapstructure.WeakDecode(r.Body, &response)
+
+ return &response.SecurityGroup, err
+}
+
+// CreateRuleResult represents the result when adding rules to a security group.
+type CreateRuleResult struct {
+ gophercloud.Result
+}
+
+// Extract will extract a Rule struct from a CreateRuleResult.
+func (r CreateRuleResult) Extract() (*Rule, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var response struct {
+ Rule Rule `mapstructure:"security_group_rule"`
+ }
+
+ err := mapstructure.WeakDecode(r.Body, &response)
+
+ return &response.Rule, err
+}
diff --git a/openstack/compute/v2/extensions/secgroups/urls.go b/openstack/compute/v2/extensions/secgroups/urls.go
new file mode 100644
index 0000000..f4760b6
--- /dev/null
+++ b/openstack/compute/v2/extensions/secgroups/urls.go
@@ -0,0 +1,32 @@
+package secgroups
+
+import "github.com/rackspace/gophercloud"
+
+const (
+ secgrouppath = "os-security-groups"
+ rulepath = "os-security-group-rules"
+)
+
+func resourceURL(c *gophercloud.ServiceClient, id string) string {
+ return c.ServiceURL(secgrouppath, id)
+}
+
+func rootURL(c *gophercloud.ServiceClient) string {
+ return c.ServiceURL(secgrouppath)
+}
+
+func listByServerURL(c *gophercloud.ServiceClient, serverID string) string {
+ return c.ServiceURL(secgrouppath, "servers", serverID, secgrouppath)
+}
+
+func rootRuleURL(c *gophercloud.ServiceClient) string {
+ return c.ServiceURL(rulepath)
+}
+
+func resourceRuleURL(c *gophercloud.ServiceClient, id string) string {
+ return c.ServiceURL(rulepath, id)
+}
+
+func serverActionURL(c *gophercloud.ServiceClient, id string) string {
+ return c.ServiceURL("servers", id, "action")
+}
diff --git a/openstack/compute/v2/extensions/startstop/doc.go b/openstack/compute/v2/extensions/startstop/doc.go
new file mode 100644
index 0000000..d2729f8
--- /dev/null
+++ b/openstack/compute/v2/extensions/startstop/doc.go
@@ -0,0 +1,5 @@
+/*
+Package startstop provides functionality to start and stop servers that have
+been provisioned by the OpenStack Compute service.
+*/
+package startstop
diff --git a/openstack/compute/v2/extensions/startstop/fixtures.go b/openstack/compute/v2/extensions/startstop/fixtures.go
new file mode 100644
index 0000000..670828a
--- /dev/null
+++ b/openstack/compute/v2/extensions/startstop/fixtures.go
@@ -0,0 +1,27 @@
+package startstop
+
+import (
+ "net/http"
+ "testing"
+
+ th "github.com/rackspace/gophercloud/testhelper"
+ "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func mockStartServerResponse(t *testing.T, id string) {
+ th.Mux.HandleFunc("/servers/"+id+"/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, `{"os-start": null}`)
+ w.WriteHeader(http.StatusAccepted)
+ })
+}
+
+func mockStopServerResponse(t *testing.T, id string) {
+ th.Mux.HandleFunc("/servers/"+id+"/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, `{"os-stop": null}`)
+ w.WriteHeader(http.StatusAccepted)
+ })
+}
diff --git a/openstack/compute/v2/extensions/startstop/requests.go b/openstack/compute/v2/extensions/startstop/requests.go
new file mode 100644
index 0000000..99c91b0
--- /dev/null
+++ b/openstack/compute/v2/extensions/startstop/requests.go
@@ -0,0 +1,40 @@
+package startstop
+
+import (
+ "github.com/racker/perigee"
+ "github.com/rackspace/gophercloud"
+)
+
+func actionURL(client *gophercloud.ServiceClient, id string) string {
+ return client.ServiceURL("servers", id, "action")
+}
+
+// Start is the operation responsible for starting a Compute server.
+func Start(client *gophercloud.ServiceClient, id string) gophercloud.ErrResult {
+ var res gophercloud.ErrResult
+
+ reqBody := map[string]interface{}{"os-start": nil}
+
+ _, res.Err = perigee.Request("POST", actionURL(client, id), perigee.Options{
+ MoreHeaders: client.AuthenticatedHeaders(),
+ ReqBody: reqBody,
+ OkCodes: []int{202},
+ })
+
+ return res
+}
+
+// Stop is the operation responsible for stopping a Compute server.
+func Stop(client *gophercloud.ServiceClient, id string) gophercloud.ErrResult {
+ var res gophercloud.ErrResult
+
+ reqBody := map[string]interface{}{"os-stop": nil}
+
+ _, res.Err = perigee.Request("POST", actionURL(client, id), perigee.Options{
+ MoreHeaders: client.AuthenticatedHeaders(),
+ ReqBody: reqBody,
+ OkCodes: []int{202},
+ })
+
+ return res
+}
diff --git a/openstack/compute/v2/extensions/startstop/requests_test.go b/openstack/compute/v2/extensions/startstop/requests_test.go
new file mode 100644
index 0000000..97a121b
--- /dev/null
+++ b/openstack/compute/v2/extensions/startstop/requests_test.go
@@ -0,0 +1,30 @@
+package startstop
+
+import (
+ "testing"
+
+ th "github.com/rackspace/gophercloud/testhelper"
+ "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+const serverID = "{serverId}"
+
+func TestStart(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockStartServerResponse(t, serverID)
+
+ err := Start(client.ServiceClient(), serverID).ExtractErr()
+ th.AssertNoErr(t, err)
+}
+
+func TestStop(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockStopServerResponse(t, serverID)
+
+ err := Stop(client.ServiceClient(), serverID).ExtractErr()
+ th.AssertNoErr(t, err)
+}
diff --git a/openstack/compute/v2/servers/fixtures.go b/openstack/compute/v2/servers/fixtures.go
index e872b07..0164605 100644
--- a/openstack/compute/v2/servers/fixtures.go
+++ b/openstack/compute/v2/servers/fixtures.go
@@ -457,3 +457,103 @@
fmt.Fprintf(w, response)
})
}
+
+// HandleServerRescueSuccessfully sets up the test server to respond to a server Rescue request.
+func HandleServerRescueSuccessfully(t *testing.T) {
+ th.Mux.HandleFunc("/servers/1234asdf/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, `{ "rescue": { "adminPass": "1234567890" } }`)
+
+ w.WriteHeader(http.StatusOK)
+ w.Write([]byte(`{ "adminPass": "1234567890" }`))
+ })
+}
+
+// HandleMetadatumGetSuccessfully sets up the test server to respond to a metadatum Get request.
+func HandleMetadatumGetSuccessfully(t *testing.T) {
+ th.Mux.HandleFunc("/servers/1234asdf/metadata/foo", 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")
+
+ w.WriteHeader(http.StatusOK)
+ w.Header().Add("Content-Type", "application/json")
+ w.Write([]byte(`{ "meta": {"foo":"bar"}}`))
+ })
+}
+
+// HandleMetadatumCreateSuccessfully sets up the test server to respond to a metadatum Create request.
+func HandleMetadatumCreateSuccessfully(t *testing.T) {
+ th.Mux.HandleFunc("/servers/1234asdf/metadata/foo", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "PUT")
+ th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+ th.TestJSONRequest(t, r, `{
+ "meta": {
+ "foo": "bar"
+ }
+ }`)
+
+ w.WriteHeader(http.StatusOK)
+ w.Header().Add("Content-Type", "application/json")
+ w.Write([]byte(`{ "meta": {"foo":"bar"}}`))
+ })
+}
+
+// HandleMetadatumDeleteSuccessfully sets up the test server to respond to a metadatum Delete request.
+func HandleMetadatumDeleteSuccessfully(t *testing.T) {
+ th.Mux.HandleFunc("/servers/1234asdf/metadata/foo", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "DELETE")
+ th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+
+ w.WriteHeader(http.StatusNoContent)
+ })
+}
+
+// HandleMetadataGetSuccessfully sets up the test server to respond to a metadata Get request.
+func HandleMetadataGetSuccessfully(t *testing.T) {
+ th.Mux.HandleFunc("/servers/1234asdf/metadata", 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")
+
+ w.WriteHeader(http.StatusOK)
+ w.Write([]byte(`{ "metadata": {"foo":"bar", "this":"that"}}`))
+ })
+}
+
+// HandleMetadataResetSuccessfully sets up the test server to respond to a metadata Create request.
+func HandleMetadataResetSuccessfully(t *testing.T) {
+ th.Mux.HandleFunc("/servers/1234asdf/metadata", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "PUT")
+ th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+ th.TestJSONRequest(t, r, `{
+ "metadata": {
+ "foo": "bar",
+ "this": "that"
+ }
+ }`)
+
+ w.WriteHeader(http.StatusOK)
+ w.Header().Add("Content-Type", "application/json")
+ w.Write([]byte(`{ "metadata": {"foo":"bar", "this":"that"}}`))
+ })
+}
+
+// HandleMetadataUpdateSuccessfully sets up the test server to respond to a metadata Update request.
+func HandleMetadataUpdateSuccessfully(t *testing.T) {
+ th.Mux.HandleFunc("/servers/1234asdf/metadata", 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, `{
+ "metadata": {
+ "foo": "baz",
+ "this": "those"
+ }
+ }`)
+
+ w.WriteHeader(http.StatusOK)
+ w.Header().Add("Content-Type", "application/json")
+ w.Write([]byte(`{ "metadata": {"foo":"baz", "this":"those"}}`))
+ })
+}
diff --git a/openstack/compute/v2/servers/requests.go b/openstack/compute/v2/servers/requests.go
index 95a4188..81a4fca 100644
--- a/openstack/compute/v2/servers/requests.go
+++ b/openstack/compute/v2/servers/requests.go
@@ -2,6 +2,7 @@
import (
"encoding/base64"
+ "errors"
"fmt"
"github.com/racker/perigee"
@@ -131,6 +132,10 @@
// ConfigDrive [optional] enables metadata injection through a configuration drive.
ConfigDrive bool
+
+ // AdminPass [optional] sets the root user password. If not set, a randomly-generated
+ // password will be created and returned in the response.
+ AdminPass string
}
// ToServerCreateMap assembles a request body based on the contents of a CreateOpts.
@@ -158,12 +163,16 @@
if opts.Metadata != nil {
server["metadata"] = opts.Metadata
}
+ if opts.AdminPass != "" {
+ server["adminPass"] = opts.AdminPass
+ }
if len(opts.SecurityGroups) > 0 {
securityGroups := make([]map[string]interface{}, len(opts.SecurityGroups))
for i, groupName := range opts.SecurityGroups {
securityGroups[i] = map[string]interface{}{"name": groupName}
}
+ server["security_groups"] = securityGroups
}
if len(opts.Networks) > 0 {
@@ -221,6 +230,7 @@
_, result.Err = perigee.Request("GET", getURL(client, id), perigee.Options{
Results: &result.Body,
MoreHeaders: client.AuthenticatedHeaders(),
+ OkCodes: []int{200, 203},
})
return result
}
@@ -475,7 +485,7 @@
FlavorRef string
}
-// ToServerResizeMap formats a ResizeOpts as a map that can be used as a JSON request body to the
+// ToServerResizeMap formats a ResizeOpts as a map that can be used as a JSON request body for the
// Resize request.
func (opts ResizeOpts) ToServerResizeMap() (map[string]interface{}, error) {
resize := map[string]interface{}{
@@ -536,3 +546,182 @@
return res
}
+
+// RescueOptsBuilder is an interface that allows extensions to override the
+// default structure of a Rescue request.
+type RescueOptsBuilder interface {
+ ToServerRescueMap() (map[string]interface{}, error)
+}
+
+// RescueOpts represents the configuration options used to control a Rescue
+// option.
+type RescueOpts struct {
+ // AdminPass is the desired administrative password for the instance in
+ // RESCUE mode. If it's left blank, the server will generate a password.
+ AdminPass string
+}
+
+// ToServerRescueMap formats a RescueOpts as a map that can be used as a JSON
+// request body for the Rescue request.
+func (opts RescueOpts) ToServerRescueMap() (map[string]interface{}, error) {
+ server := make(map[string]interface{})
+ if opts.AdminPass != "" {
+ server["adminPass"] = opts.AdminPass
+ }
+ return map[string]interface{}{"rescue": server}, nil
+}
+
+// Rescue instructs the provider to place the server into RESCUE mode.
+func Rescue(client *gophercloud.ServiceClient, id string, opts RescueOptsBuilder) RescueResult {
+ var result RescueResult
+
+ if id == "" {
+ result.Err = fmt.Errorf("ID is required")
+ return result
+ }
+ reqBody, err := opts.ToServerRescueMap()
+ if err != nil {
+ result.Err = err
+ return result
+ }
+
+ _, result.Err = perigee.Request("POST", actionURL(client, id), perigee.Options{
+ Results: &result.Body,
+ ReqBody: &reqBody,
+ MoreHeaders: client.AuthenticatedHeaders(),
+ OkCodes: []int{200},
+ })
+
+ return result
+}
+
+// ResetMetadataOptsBuilder allows extensions to add additional parameters to the
+// Reset request.
+type ResetMetadataOptsBuilder interface {
+ ToMetadataResetMap() (map[string]interface{}, error)
+}
+
+// MetadataOpts is a map that contains key-value pairs.
+type MetadataOpts map[string]string
+
+// ToMetadataResetMap assembles a body for a Reset request based on the contents of a MetadataOpts.
+func (opts MetadataOpts) ToMetadataResetMap() (map[string]interface{}, error) {
+ return map[string]interface{}{"metadata": opts}, nil
+}
+
+// ToMetadataUpdateMap assembles a body for an Update request based on the contents of a MetadataOpts.
+func (opts MetadataOpts) ToMetadataUpdateMap() (map[string]interface{}, error) {
+ return map[string]interface{}{"metadata": opts}, nil
+}
+
+// ResetMetadata will create multiple new key-value pairs for the given server ID.
+// Note: Using this operation will erase any already-existing metadata and create
+// the new metadata provided. To keep any already-existing metadata, use the
+// UpdateMetadatas or UpdateMetadata function.
+func ResetMetadata(client *gophercloud.ServiceClient, id string, opts ResetMetadataOptsBuilder) ResetMetadataResult {
+ var res ResetMetadataResult
+ metadata, err := opts.ToMetadataResetMap()
+ if err != nil {
+ res.Err = err
+ return res
+ }
+ _, res.Err = perigee.Request("PUT", metadataURL(client, id), perigee.Options{
+ ReqBody: metadata,
+ Results: &res.Body,
+ MoreHeaders: client.AuthenticatedHeaders(),
+ })
+ return res
+}
+
+// Metadata requests all the metadata for the given server ID.
+func Metadata(client *gophercloud.ServiceClient, id string) GetMetadataResult {
+ var res GetMetadataResult
+ _, res.Err = perigee.Request("GET", metadataURL(client, id), perigee.Options{
+ Results: &res.Body,
+ MoreHeaders: client.AuthenticatedHeaders(),
+ })
+ return res
+}
+
+// UpdateMetadataOptsBuilder allows extensions to add additional parameters to the
+// Create request.
+type UpdateMetadataOptsBuilder interface {
+ ToMetadataUpdateMap() (map[string]interface{}, error)
+}
+
+// UpdateMetadata updates (or creates) all the metadata specified by opts for the given server ID.
+// This operation does not affect already-existing metadata that is not specified
+// by opts.
+func UpdateMetadata(client *gophercloud.ServiceClient, id string, opts UpdateMetadataOptsBuilder) UpdateMetadataResult {
+ var res UpdateMetadataResult
+ metadata, err := opts.ToMetadataUpdateMap()
+ if err != nil {
+ res.Err = err
+ return res
+ }
+ _, res.Err = perigee.Request("POST", metadataURL(client, id), perigee.Options{
+ ReqBody: metadata,
+ Results: &res.Body,
+ MoreHeaders: client.AuthenticatedHeaders(),
+ })
+ return res
+}
+
+// MetadatumOptsBuilder allows extensions to add additional parameters to the
+// Create request.
+type MetadatumOptsBuilder interface {
+ ToMetadatumCreateMap() (map[string]interface{}, string, error)
+}
+
+// MetadatumOpts is a map of length one that contains a key-value pair.
+type MetadatumOpts map[string]string
+
+// ToMetadatumCreateMap assembles a body for a Create request based on the contents of a MetadataumOpts.
+func (opts MetadatumOpts) ToMetadatumCreateMap() (map[string]interface{}, string, error) {
+ if len(opts) != 1 {
+ return nil, "", errors.New("CreateMetadatum operation must have 1 and only 1 key-value pair.")
+ }
+ metadatum := map[string]interface{}{"meta": opts}
+ var key string
+ for k := range metadatum["meta"].(MetadatumOpts) {
+ key = k
+ }
+ return metadatum, key, nil
+}
+
+// CreateMetadatum will create or update the key-value pair with the given key for the given server ID.
+func CreateMetadatum(client *gophercloud.ServiceClient, id string, opts MetadatumOptsBuilder) CreateMetadatumResult {
+ var res CreateMetadatumResult
+ metadatum, key, err := opts.ToMetadatumCreateMap()
+ if err != nil {
+ res.Err = err
+ return res
+ }
+
+ _, res.Err = perigee.Request("PUT", metadatumURL(client, id, key), perigee.Options{
+ ReqBody: metadatum,
+ Results: &res.Body,
+ MoreHeaders: client.AuthenticatedHeaders(),
+ })
+ return res
+}
+
+// Metadatum requests the key-value pair with the given key for the given server ID.
+func Metadatum(client *gophercloud.ServiceClient, id, key string) GetMetadatumResult {
+ var res GetMetadatumResult
+ _, res.Err = perigee.Request("GET", metadatumURL(client, id, key), perigee.Options{
+ Results: &res.Body,
+ MoreHeaders: client.AuthenticatedHeaders(),
+ })
+ return res
+}
+
+// DeleteMetadatum will delete the key-value pair with the given key for the given server ID.
+func DeleteMetadatum(client *gophercloud.ServiceClient, id, key string) DeleteMetadatumResult {
+ var res DeleteMetadatumResult
+ _, res.Err = perigee.Request("DELETE", metadatumURL(client, id, key), perigee.Options{
+ Results: &res.Body,
+ MoreHeaders: client.AuthenticatedHeaders(),
+ })
+ return res
+}
diff --git a/openstack/compute/v2/servers/requests_test.go b/openstack/compute/v2/servers/requests_test.go
index 392e2d8..017e793 100644
--- a/openstack/compute/v2/servers/requests_test.go
+++ b/openstack/compute/v2/servers/requests_test.go
@@ -174,3 +174,93 @@
res := RevertResize(client.ServiceClient(), "1234asdf")
th.AssertNoErr(t, res.Err)
}
+
+func TestRescue(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ HandleServerRescueSuccessfully(t)
+
+ res := Rescue(client.ServiceClient(), "1234asdf", RescueOpts{
+ AdminPass: "1234567890",
+ })
+ th.AssertNoErr(t, res.Err)
+ adminPass, _ := res.Extract()
+ th.AssertEquals(t, "1234567890", adminPass)
+}
+
+func TestGetMetadatum(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ HandleMetadatumGetSuccessfully(t)
+
+ expected := map[string]string{"foo": "bar"}
+ actual, err := Metadatum(client.ServiceClient(), "1234asdf", "foo").Extract()
+ th.AssertNoErr(t, err)
+ th.AssertDeepEquals(t, expected, actual)
+}
+
+func TestCreateMetadatum(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ HandleMetadatumCreateSuccessfully(t)
+
+ expected := map[string]string{"foo": "bar"}
+ actual, err := CreateMetadatum(client.ServiceClient(), "1234asdf", MetadatumOpts{"foo": "bar"}).Extract()
+ th.AssertNoErr(t, err)
+ th.AssertDeepEquals(t, expected, actual)
+}
+
+func TestDeleteMetadatum(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ HandleMetadatumDeleteSuccessfully(t)
+
+ err := DeleteMetadatum(client.ServiceClient(), "1234asdf", "foo").ExtractErr()
+ th.AssertNoErr(t, err)
+}
+
+func TestGetMetadata(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ HandleMetadataGetSuccessfully(t)
+
+ expected := map[string]string{"foo": "bar", "this": "that"}
+ actual, err := Metadata(client.ServiceClient(), "1234asdf").Extract()
+ th.AssertNoErr(t, err)
+ th.AssertDeepEquals(t, expected, actual)
+}
+
+func TestResetMetadata(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ HandleMetadataResetSuccessfully(t)
+
+ expected := map[string]string{"foo": "bar", "this": "that"}
+ actual, err := ResetMetadata(client.ServiceClient(), "1234asdf", MetadataOpts{
+ "foo": "bar",
+ "this": "that",
+ }).Extract()
+ th.AssertNoErr(t, err)
+ th.AssertDeepEquals(t, expected, actual)
+}
+
+func TestUpdateMetadata(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ HandleMetadataUpdateSuccessfully(t)
+
+ expected := map[string]string{"foo": "baz", "this": "those"}
+ actual, err := UpdateMetadata(client.ServiceClient(), "1234asdf", MetadataOpts{
+ "foo": "baz",
+ "this": "those",
+ }).Extract()
+ th.AssertNoErr(t, err)
+ th.AssertDeepEquals(t, expected, actual)
+}
diff --git a/openstack/compute/v2/servers/results.go b/openstack/compute/v2/servers/results.go
index 53946ba..d63d7c8 100644
--- a/openstack/compute/v2/servers/results.go
+++ b/openstack/compute/v2/servers/results.go
@@ -1,6 +1,8 @@
package servers
import (
+ "reflect"
+
"github.com/mitchellh/mapstructure"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
@@ -20,8 +22,21 @@
Server Server `mapstructure:"server"`
}
- err := mapstructure.Decode(r.Body, &response)
- return &response.Server, err
+ 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
}
// CreateResult temporarily contains the response from a Create call.
@@ -39,7 +54,7 @@
serverResult
}
-// DeleteResult temporarily contains the response from an Delete call.
+// DeleteResult temporarily contains the response from a Delete call.
type DeleteResult struct {
gophercloud.ErrResult
}
@@ -54,6 +69,25 @@
gophercloud.ErrResult
}
+// RescueResult represents the result of a server rescue operation
+type RescueResult struct {
+ ActionResult
+}
+
+// Extract interprets any RescueResult as an AdminPass, if possible.
+func (r RescueResult) Extract() (string, error) {
+ if r.Err != nil {
+ return "", r.Err
+ }
+
+ var response struct {
+ AdminPass string `mapstructure:"adminPass"`
+ }
+
+ err := mapstructure.Decode(r.Body, &response)
+ return response.AdminPass, err
+}
+
// Server exposes only the standard OpenStack fields corresponding to a given 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.
@@ -145,6 +179,93 @@
var response struct {
Servers []Server `mapstructure:"servers"`
}
- err := mapstructure.Decode(casted, &response)
+
+ config := &mapstructure.DecoderConfig{
+ DecodeHook: toMapFromString,
+ Result: &response,
+ }
+ decoder, err := mapstructure.NewDecoder(config)
+ if err != nil {
+ return nil, err
+ }
+
+ err = decoder.Decode(casted)
+
+ //err := mapstructure.Decode(casted, &response)
return response.Servers, err
}
+
+// MetadataResult contains the result of a call for (potentially) multiple key-value pairs.
+type MetadataResult struct {
+ gophercloud.Result
+}
+
+// GetMetadataResult temporarily contains the response from a metadata Get call.
+type GetMetadataResult struct {
+ MetadataResult
+}
+
+// ResetMetadataResult temporarily contains the response from a metadata Reset call.
+type ResetMetadataResult struct {
+ MetadataResult
+}
+
+// UpdateMetadataResult temporarily contains the response from a metadata Update call.
+type UpdateMetadataResult struct {
+ MetadataResult
+}
+
+// MetadatumResult contains the result of a call for individual a single key-value pair.
+type MetadatumResult struct {
+ gophercloud.Result
+}
+
+// GetMetadatumResult temporarily contains the response from a metadatum Get call.
+type GetMetadatumResult struct {
+ MetadatumResult
+}
+
+// CreateMetadatumResult temporarily contains the response from a metadatum Create call.
+type CreateMetadatumResult struct {
+ MetadatumResult
+}
+
+// DeleteMetadatumResult temporarily contains the response from a metadatum Delete call.
+type DeleteMetadatumResult struct {
+ gophercloud.ErrResult
+}
+
+// Extract interprets any MetadataResult as a Metadata, if possible.
+func (r MetadataResult) Extract() (map[string]string, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var response struct {
+ Metadata map[string]string `mapstructure:"metadata"`
+ }
+
+ err := mapstructure.Decode(r.Body, &response)
+ return response.Metadata, err
+}
+
+// Extract interprets any MetadatumResult as a Metadatum, if possible.
+func (r MetadatumResult) Extract() (map[string]string, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var response struct {
+ Metadatum map[string]string `mapstructure:"meta"`
+ }
+
+ err := mapstructure.Decode(r.Body, &response)
+ return response.Metadatum, err
+}
+
+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
+}
diff --git a/openstack/compute/v2/servers/urls.go b/openstack/compute/v2/servers/urls.go
index 57587ab..4bc6586 100644
--- a/openstack/compute/v2/servers/urls.go
+++ b/openstack/compute/v2/servers/urls.go
@@ -29,3 +29,11 @@
func actionURL(client *gophercloud.ServiceClient, id string) string {
return client.ServiceURL("servers", id, "action")
}
+
+func metadatumURL(client *gophercloud.ServiceClient, id, key string) string {
+ return client.ServiceURL("servers", id, "metadata", key)
+}
+
+func metadataURL(client *gophercloud.ServiceClient, id string) string {
+ return client.ServiceURL("servers", id, "metadata")
+}
diff --git a/openstack/compute/v2/servers/urls_test.go b/openstack/compute/v2/servers/urls_test.go
index cc895c9..17a1d28 100644
--- a/openstack/compute/v2/servers/urls_test.go
+++ b/openstack/compute/v2/servers/urls_test.go
@@ -54,3 +54,15 @@
expected := endpoint + "servers/foo/action"
th.CheckEquals(t, expected, actual)
}
+
+func TestMetadatumURL(t *testing.T) {
+ actual := metadatumURL(endpointClient(), "foo", "bar")
+ expected := endpoint + "servers/foo/metadata/bar"
+ th.CheckEquals(t, expected, actual)
+}
+
+func TestMetadataURL(t *testing.T) {
+ actual := metadataURL(endpointClient(), "foo")
+ expected := endpoint + "servers/foo/metadata"
+ th.CheckEquals(t, expected, actual)
+}
diff --git a/openstack/compute/v2/servers/util_test.go b/openstack/compute/v2/servers/util_test.go
deleted file mode 100644
index e192ae3..0000000
--- a/openstack/compute/v2/servers/util_test.go
+++ /dev/null
@@ -1,38 +0,0 @@
-package servers
-
-import (
- "fmt"
- "net/http"
- "testing"
- "time"
-
- th "github.com/rackspace/gophercloud/testhelper"
- "github.com/rackspace/gophercloud/testhelper/client"
-)
-
-func TestWaitForStatus(t *testing.T) {
- th.SetupHTTP()
- defer th.TeardownHTTP()
-
- th.Mux.HandleFunc("/servers/4321", func(w http.ResponseWriter, r *http.Request) {
- time.Sleep(2 * time.Second)
- w.Header().Add("Content-Type", "application/json")
- w.WriteHeader(http.StatusOK)
- fmt.Fprintf(w, `
- {
- "server": {
- "name": "the-server",
- "id": "4321",
- "status": "ACTIVE"
- }
- }`)
- })
-
- err := WaitForStatus(client.ServiceClient(), "4321", "ACTIVE", 0)
- if err == nil {
- t.Errorf("Expected error: 'Time Out in WaitFor'")
- }
-
- err = WaitForStatus(client.ServiceClient(), "4321", "ACTIVE", 3)
- th.CheckNoErr(t, err)
-}
diff --git a/openstack/identity/v2/extensions/admin/roles/docs.go b/openstack/identity/v2/extensions/admin/roles/docs.go
new file mode 100644
index 0000000..8954178
--- /dev/null
+++ b/openstack/identity/v2/extensions/admin/roles/docs.go
@@ -0,0 +1,16 @@
+// Package roles provides functionality to interact with and control roles on
+// the API.
+//
+// A role represents a personality that a user can assume when performing a
+// specific set of operations. If a role includes a set of rights and
+// privileges, a user assuming that role inherits those rights and privileges.
+//
+// When a token is generated, the list of roles that user can assume is returned
+// back to them. Services that are being called by that user determine how they
+// interpret the set of roles a user has and to which operations or resources
+// each role grants access.
+//
+// It is up to individual services such as Compute or Image to assign meaning
+// to these roles. As far as the Identity service is concerned, a role is an
+// arbitrary name assigned by the user.
+package roles
diff --git a/openstack/identity/v2/extensions/admin/roles/fixtures.go b/openstack/identity/v2/extensions/admin/roles/fixtures.go
new file mode 100644
index 0000000..8256f0f
--- /dev/null
+++ b/openstack/identity/v2/extensions/admin/roles/fixtures.go
@@ -0,0 +1,48 @@
+package roles
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func MockListRoleResponse(t *testing.T) {
+ th.Mux.HandleFunc("/OS-KSADM/roles", 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, `
+{
+ "roles": [
+ {
+ "id": "123",
+ "name": "compute:admin",
+ "description": "Nova Administrator"
+ }
+ ]
+}
+ `)
+ })
+}
+
+func MockAddUserRoleResponse(t *testing.T) {
+ th.Mux.HandleFunc("/tenants/{tenant_id}/users/{user_id}/roles/OS-KSADM/{role_id}", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "PUT")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ w.WriteHeader(http.StatusCreated)
+ })
+}
+
+func MockDeleteUserRoleResponse(t *testing.T) {
+ th.Mux.HandleFunc("/tenants/{tenant_id}/users/{user_id}/roles/OS-KSADM/{role_id}", 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/openstack/identity/v2/extensions/admin/roles/requests.go b/openstack/identity/v2/extensions/admin/roles/requests.go
new file mode 100644
index 0000000..152031a
--- /dev/null
+++ b/openstack/identity/v2/extensions/admin/roles/requests.go
@@ -0,0 +1,44 @@
+package roles
+
+import (
+ "github.com/racker/perigee"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// List is the operation responsible for listing all available global roles
+// that a user can adopt.
+func List(client *gophercloud.ServiceClient) pagination.Pager {
+ createPage := func(r pagination.PageResult) pagination.Page {
+ return RolePage{pagination.SinglePageBase(r)}
+ }
+ return pagination.NewPager(client, rootURL(client), createPage)
+}
+
+// AddUserRole is the operation responsible for assigning a particular role to
+// a user. This is confined to the scope of the user's tenant - so the tenant
+// ID is a required argument.
+func AddUserRole(client *gophercloud.ServiceClient, tenantID, userID, roleID string) UserRoleResult {
+ var result UserRoleResult
+
+ _, result.Err = perigee.Request("PUT", userRoleURL(client, tenantID, userID, roleID), perigee.Options{
+ MoreHeaders: client.AuthenticatedHeaders(),
+ OkCodes: []int{200, 201},
+ })
+
+ return result
+}
+
+// DeleteUserRole is the operation responsible for deleting a particular role
+// from a user. This is confined to the scope of the user's tenant - so the
+// tenant ID is a required argument.
+func DeleteUserRole(client *gophercloud.ServiceClient, tenantID, userID, roleID string) UserRoleResult {
+ var result UserRoleResult
+
+ _, result.Err = perigee.Request("DELETE", userRoleURL(client, tenantID, userID, roleID), perigee.Options{
+ MoreHeaders: client.AuthenticatedHeaders(),
+ OkCodes: []int{204},
+ })
+
+ return result
+}
diff --git a/openstack/identity/v2/extensions/admin/roles/requests_test.go b/openstack/identity/v2/extensions/admin/roles/requests_test.go
new file mode 100644
index 0000000..7bfeea4
--- /dev/null
+++ b/openstack/identity/v2/extensions/admin/roles/requests_test.go
@@ -0,0 +1,64 @@
+package roles
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+ "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestRole(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ MockListRoleResponse(t)
+
+ count := 0
+
+ err := List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractRoles(page)
+ if err != nil {
+ t.Errorf("Failed to extract users: %v", err)
+ return false, err
+ }
+
+ expected := []Role{
+ Role{
+ ID: "123",
+ Name: "compute:admin",
+ Description: "Nova Administrator",
+ },
+ }
+
+ th.CheckDeepEquals(t, expected, actual)
+
+ return true, nil
+ })
+
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, 1, count)
+}
+
+func TestAddUserRole(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ MockAddUserRoleResponse(t)
+
+ err := AddUserRole(client.ServiceClient(), "{tenant_id}", "{user_id}", "{role_id}").ExtractErr()
+
+ th.AssertNoErr(t, err)
+}
+
+func TestDeleteUserRole(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ MockDeleteUserRoleResponse(t)
+
+ err := DeleteUserRole(client.ServiceClient(), "{tenant_id}", "{user_id}", "{role_id}").ExtractErr()
+
+ th.AssertNoErr(t, err)
+}
diff --git a/openstack/identity/v2/extensions/admin/roles/results.go b/openstack/identity/v2/extensions/admin/roles/results.go
new file mode 100644
index 0000000..ebb3aa5
--- /dev/null
+++ b/openstack/identity/v2/extensions/admin/roles/results.go
@@ -0,0 +1,53 @@
+package roles
+
+import (
+ "github.com/mitchellh/mapstructure"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// Role represents an API role resource.
+type Role struct {
+ // The unique ID for the role.
+ ID string
+
+ // The human-readable name of the role.
+ Name string
+
+ // The description of the role.
+ Description string
+
+ // The associated service for this role.
+ ServiceID string
+}
+
+// RolePage is a single page of a user Role collection.
+type RolePage struct {
+ pagination.SinglePageBase
+}
+
+// IsEmpty determines whether or not a page of Tenants contains any results.
+func (page RolePage) IsEmpty() (bool, error) {
+ users, err := ExtractRoles(page)
+ if err != nil {
+ return false, err
+ }
+ return len(users) == 0, nil
+}
+
+// ExtractRoles returns a slice of roles contained in a single page of results.
+func ExtractRoles(page pagination.Page) ([]Role, error) {
+ casted := page.(RolePage).Body
+ var response struct {
+ Roles []Role `mapstructure:"roles"`
+ }
+
+ err := mapstructure.Decode(casted, &response)
+ return response.Roles, err
+}
+
+// UserRoleResult represents the result of either an AddUserRole or
+// a DeleteUserRole operation.
+type UserRoleResult struct {
+ gophercloud.ErrResult
+}
diff --git a/openstack/identity/v2/extensions/admin/roles/urls.go b/openstack/identity/v2/extensions/admin/roles/urls.go
new file mode 100644
index 0000000..61b3155
--- /dev/null
+++ b/openstack/identity/v2/extensions/admin/roles/urls.go
@@ -0,0 +1,21 @@
+package roles
+
+import "github.com/rackspace/gophercloud"
+
+const (
+ ExtPath = "OS-KSADM"
+ RolePath = "roles"
+ UserPath = "users"
+)
+
+func resourceURL(c *gophercloud.ServiceClient, id string) string {
+ return c.ServiceURL(ExtPath, RolePath, id)
+}
+
+func rootURL(c *gophercloud.ServiceClient) string {
+ return c.ServiceURL(ExtPath, RolePath)
+}
+
+func userRoleURL(c *gophercloud.ServiceClient, tenantID, userID, roleID string) string {
+ return c.ServiceURL("tenants", tenantID, UserPath, userID, RolePath, ExtPath, roleID)
+}
diff --git a/openstack/identity/v2/users/doc.go b/openstack/identity/v2/users/doc.go
new file mode 100644
index 0000000..82abcb9
--- /dev/null
+++ b/openstack/identity/v2/users/doc.go
@@ -0,0 +1 @@
+package users
diff --git a/openstack/identity/v2/users/fixtures.go b/openstack/identity/v2/users/fixtures.go
new file mode 100644
index 0000000..8941868
--- /dev/null
+++ b/openstack/identity/v2/users/fixtures.go
@@ -0,0 +1,163 @@
+package users
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func MockListUserResponse(t *testing.T) {
+ th.Mux.HandleFunc("/users", 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, `
+{
+ "users":[
+ {
+ "id": "u1000",
+ "name": "John Smith",
+ "username": "jqsmith",
+ "email": "john.smith@example.org",
+ "enabled": true,
+ "tenant_id": "12345"
+ },
+ {
+ "id": "u1001",
+ "name": "Jane Smith",
+ "username": "jqsmith",
+ "email": "jane.smith@example.org",
+ "enabled": true,
+ "tenant_id": "12345"
+ }
+ ]
+}
+ `)
+ })
+}
+
+func mockCreateUserResponse(t *testing.T) {
+ th.Mux.HandleFunc("/users", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "POST")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+ th.TestJSONRequest(t, r, `
+{
+ "user": {
+ "name": "new_user",
+ "tenant_id": "12345",
+ "enabled": false,
+ "email": "new_user@foo.com"
+ }
+}
+ `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, `
+{
+ "user": {
+ "name": "new_user",
+ "tenant_id": "12345",
+ "enabled": false,
+ "email": "new_user@foo.com",
+ "id": "c39e3de9be2d4c779f1dfd6abacc176d"
+ }
+}
+`)
+ })
+}
+
+func mockGetUserResponse(t *testing.T) {
+ th.Mux.HandleFunc("/users/new_user", 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, `
+{
+ "user": {
+ "name": "new_user",
+ "tenant_id": "12345",
+ "enabled": false,
+ "email": "new_user@foo.com",
+ "id": "c39e3de9be2d4c779f1dfd6abacc176d"
+ }
+}
+`)
+ })
+}
+
+func mockUpdateUserResponse(t *testing.T) {
+ th.Mux.HandleFunc("/users/c39e3de9be2d4c779f1dfd6abacc176d", 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, `
+{
+ "user": {
+ "name": "new_name",
+ "enabled": true,
+ "email": "new_email@foo.com"
+ }
+}
+`)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, `
+{
+ "user": {
+ "name": "new_name",
+ "tenant_id": "12345",
+ "enabled": true,
+ "email": "new_email@foo.com",
+ "id": "c39e3de9be2d4c779f1dfd6abacc176d"
+ }
+}
+`)
+ })
+}
+
+func mockDeleteUserResponse(t *testing.T) {
+ th.Mux.HandleFunc("/users/c39e3de9be2d4c779f1dfd6abacc176d", 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 mockListRolesResponse(t *testing.T) {
+ th.Mux.HandleFunc("/tenants/1d8b6120dcc640fda4fc9194ffc80273/users/c39e3de9be2d4c779f1dfd6abacc176d/roles", 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, `
+{
+ "roles": [
+ {
+ "id": "9fe2ff9ee4384b1894a90878d3e92bab",
+ "name": "foo_role"
+ },
+ {
+ "id": "1ea3d56793574b668e85960fbf651e13",
+ "name": "admin"
+ }
+ ]
+}
+ `)
+ })
+}
diff --git a/openstack/identity/v2/users/requests.go b/openstack/identity/v2/users/requests.go
new file mode 100644
index 0000000..4ce395f
--- /dev/null
+++ b/openstack/identity/v2/users/requests.go
@@ -0,0 +1,180 @@
+package users
+
+import (
+ "errors"
+
+ "github.com/racker/perigee"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+func List(client *gophercloud.ServiceClient) pagination.Pager {
+ createPage := func(r pagination.PageResult) pagination.Page {
+ return UserPage{pagination.SinglePageBase(r)}
+ }
+
+ return pagination.NewPager(client, rootURL(client), createPage)
+}
+
+// EnabledState represents whether the user is enabled or not.
+type EnabledState *bool
+
+// Useful variables to use when creating or updating users.
+var (
+ iTrue = true
+ iFalse = false
+
+ Enabled EnabledState = &iTrue
+ Disabled EnabledState = &iFalse
+)
+
+// CommonOpts are the parameters that are shared between CreateOpts and
+// UpdateOpts
+type CommonOpts struct {
+ // Either a name or username is required. When provided, the value must be
+ // unique or a 409 conflict error will be returned. If you provide a name but
+ // omit a username, the latter will be set to the former; and vice versa.
+ Name, Username string
+
+ // The ID of the tenant to which you want to assign this user.
+ TenantID string
+
+ // Indicates whether this user is enabled or not.
+ Enabled EnabledState
+
+ // The email address of this user.
+ Email string
+}
+
+// CreateOpts represents the options needed when creating new users.
+type CreateOpts CommonOpts
+
+// CreateOptsBuilder describes struct types that can be accepted by the Create call.
+type CreateOptsBuilder interface {
+ ToUserCreateMap() (map[string]interface{}, error)
+}
+
+// ToUserCreateMap assembles a request body based on the contents of a CreateOpts.
+func (opts CreateOpts) ToUserCreateMap() (map[string]interface{}, error) {
+ m := make(map[string]interface{})
+
+ if opts.Name == "" && opts.Username == "" {
+ return m, errors.New("Either a Name or Username must be provided")
+ }
+
+ if opts.Name != "" {
+ m["name"] = opts.Name
+ }
+ if opts.Username != "" {
+ m["username"] = opts.Username
+ }
+ if opts.Enabled != nil {
+ m["enabled"] = &opts.Enabled
+ }
+ if opts.Email != "" {
+ m["email"] = opts.Email
+ }
+ if opts.TenantID != "" {
+ m["tenant_id"] = opts.TenantID
+ }
+
+ return map[string]interface{}{"user": m}, nil
+}
+
+// Create is the operation responsible for creating new users.
+func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult {
+ var res CreateResult
+
+ reqBody, err := opts.ToUserCreateMap()
+ if err != nil {
+ res.Err = err
+ return res
+ }
+
+ _, res.Err = perigee.Request("POST", rootURL(client), perigee.Options{
+ Results: &res.Body,
+ ReqBody: reqBody,
+ MoreHeaders: client.AuthenticatedHeaders(),
+ OkCodes: []int{200, 201},
+ })
+
+ return res
+}
+
+// Get requests details on a single user, either by ID.
+func Get(client *gophercloud.ServiceClient, id string) GetResult {
+ var result GetResult
+
+ _, result.Err = perigee.Request("GET", ResourceURL(client, id), perigee.Options{
+ Results: &result.Body,
+ MoreHeaders: client.AuthenticatedHeaders(),
+ OkCodes: []int{200},
+ })
+
+ return result
+}
+
+// UpdateOptsBuilder allows extensions to add additional attributes to the Update request.
+type UpdateOptsBuilder interface {
+ ToUserUpdateMap() map[string]interface{}
+}
+
+// UpdateOpts specifies the base attributes that may be updated on an existing server.
+type UpdateOpts CommonOpts
+
+// ToUserUpdateMap formats an UpdateOpts structure into a request body.
+func (opts UpdateOpts) ToUserUpdateMap() map[string]interface{} {
+ m := make(map[string]interface{})
+
+ if opts.Name != "" {
+ m["name"] = opts.Name
+ }
+ if opts.Username != "" {
+ m["username"] = opts.Username
+ }
+ if opts.Enabled != nil {
+ m["enabled"] = &opts.Enabled
+ }
+ if opts.Email != "" {
+ m["email"] = opts.Email
+ }
+ if opts.TenantID != "" {
+ m["tenant_id"] = opts.TenantID
+ }
+
+ return map[string]interface{}{"user": m}
+}
+
+// Update is the operation responsible for updating exist users by their UUID.
+func Update(client *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) UpdateResult {
+ var result UpdateResult
+
+ _, result.Err = perigee.Request("PUT", ResourceURL(client, id), perigee.Options{
+ Results: &result.Body,
+ ReqBody: opts.ToUserUpdateMap(),
+ MoreHeaders: client.AuthenticatedHeaders(),
+ OkCodes: []int{200},
+ })
+
+ return result
+}
+
+// Delete is the operation responsible for permanently deleting an API user.
+func Delete(client *gophercloud.ServiceClient, id string) DeleteResult {
+ var result DeleteResult
+
+ _, result.Err = perigee.Request("DELETE", ResourceURL(client, id), perigee.Options{
+ MoreHeaders: client.AuthenticatedHeaders(),
+ OkCodes: []int{204},
+ })
+
+ return result
+}
+
+func ListRoles(client *gophercloud.ServiceClient, tenantID, userID string) pagination.Pager {
+ createPage := func(r pagination.PageResult) pagination.Page {
+ return RolePage{pagination.SinglePageBase(r)}
+ }
+
+ return pagination.NewPager(client, listRolesURL(client, tenantID, userID), createPage)
+}
diff --git a/openstack/identity/v2/users/requests_test.go b/openstack/identity/v2/users/requests_test.go
new file mode 100644
index 0000000..04f8371
--- /dev/null
+++ b/openstack/identity/v2/users/requests_test.go
@@ -0,0 +1,165 @@
+package users
+
+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()
+
+ MockListUserResponse(t)
+
+ count := 0
+
+ err := List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractUsers(page)
+ if err != nil {
+ t.Errorf("Failed to extract users: %v", err)
+ return false, err
+ }
+
+ expected := []User{
+ User{
+ ID: "u1000",
+ Name: "John Smith",
+ Username: "jqsmith",
+ Email: "john.smith@example.org",
+ Enabled: true,
+ TenantID: "12345",
+ },
+ User{
+ ID: "u1001",
+ Name: "Jane Smith",
+ Username: "jqsmith",
+ Email: "jane.smith@example.org",
+ Enabled: true,
+ TenantID: "12345",
+ },
+ }
+
+ th.CheckDeepEquals(t, expected, actual)
+
+ return true, nil
+ })
+
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, 1, count)
+}
+
+func TestCreateUser(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockCreateUserResponse(t)
+
+ opts := CreateOpts{
+ Name: "new_user",
+ TenantID: "12345",
+ Enabled: Disabled,
+ Email: "new_user@foo.com",
+ }
+
+ user, err := Create(client.ServiceClient(), opts).Extract()
+
+ th.AssertNoErr(t, err)
+
+ expected := &User{
+ Name: "new_user",
+ ID: "c39e3de9be2d4c779f1dfd6abacc176d",
+ Email: "new_user@foo.com",
+ Enabled: false,
+ TenantID: "12345",
+ }
+
+ th.AssertDeepEquals(t, expected, user)
+}
+
+func TestGetUser(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockGetUserResponse(t)
+
+ user, err := Get(client.ServiceClient(), "new_user").Extract()
+ th.AssertNoErr(t, err)
+
+ expected := &User{
+ Name: "new_user",
+ ID: "c39e3de9be2d4c779f1dfd6abacc176d",
+ Email: "new_user@foo.com",
+ Enabled: false,
+ TenantID: "12345",
+ }
+
+ th.AssertDeepEquals(t, expected, user)
+}
+
+func TestUpdateUser(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockUpdateUserResponse(t)
+
+ id := "c39e3de9be2d4c779f1dfd6abacc176d"
+ opts := UpdateOpts{
+ Name: "new_name",
+ Enabled: Enabled,
+ Email: "new_email@foo.com",
+ }
+
+ user, err := Update(client.ServiceClient(), id, opts).Extract()
+
+ th.AssertNoErr(t, err)
+
+ expected := &User{
+ Name: "new_name",
+ ID: id,
+ Email: "new_email@foo.com",
+ Enabled: true,
+ TenantID: "12345",
+ }
+
+ th.AssertDeepEquals(t, expected, user)
+}
+
+func TestDeleteUser(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockDeleteUserResponse(t)
+
+ res := Delete(client.ServiceClient(), "c39e3de9be2d4c779f1dfd6abacc176d")
+ th.AssertNoErr(t, res.Err)
+}
+
+func TestListingUserRoles(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockListRolesResponse(t)
+
+ tenantID := "1d8b6120dcc640fda4fc9194ffc80273"
+ userID := "c39e3de9be2d4c779f1dfd6abacc176d"
+
+ err := ListRoles(client.ServiceClient(), tenantID, userID).EachPage(func(page pagination.Page) (bool, error) {
+ actual, err := ExtractRoles(page)
+ th.AssertNoErr(t, err)
+
+ expected := []Role{
+ Role{ID: "9fe2ff9ee4384b1894a90878d3e92bab", Name: "foo_role"},
+ Role{ID: "1ea3d56793574b668e85960fbf651e13", Name: "admin"},
+ }
+
+ th.CheckDeepEquals(t, expected, actual)
+
+ return true, nil
+ })
+
+ th.AssertNoErr(t, err)
+}
diff --git a/openstack/identity/v2/users/results.go b/openstack/identity/v2/users/results.go
new file mode 100644
index 0000000..f531d5d
--- /dev/null
+++ b/openstack/identity/v2/users/results.go
@@ -0,0 +1,128 @@
+package users
+
+import (
+ "github.com/mitchellh/mapstructure"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// User represents a user resource that exists on the API.
+type User struct {
+ // The UUID for this user.
+ ID string
+
+ // The human name for this user.
+ Name string
+
+ // The username for this user.
+ Username string
+
+ // Indicates whether the user is enabled (true) or disabled (false).
+ Enabled bool
+
+ // The email address for this user.
+ Email string
+
+ // The ID of the tenant to which this user belongs.
+ TenantID string `mapstructure:"tenant_id"`
+}
+
+// Role assigns specific responsibilities to users, allowing them to accomplish
+// certain API operations whilst scoped to a service.
+type Role struct {
+ // UUID of the role
+ ID string
+
+ // Name of the role
+ Name string
+}
+
+// UserPage is a single page of a User collection.
+type UserPage struct {
+ pagination.SinglePageBase
+}
+
+// RolePage is a single page of a user Role collection.
+type RolePage struct {
+ pagination.SinglePageBase
+}
+
+// IsEmpty determines whether or not a page of Tenants contains any results.
+func (page UserPage) IsEmpty() (bool, error) {
+ users, err := ExtractUsers(page)
+ if err != nil {
+ return false, err
+ }
+ return len(users) == 0, nil
+}
+
+// ExtractUsers returns a slice of Tenants contained in a single page of results.
+func ExtractUsers(page pagination.Page) ([]User, error) {
+ casted := page.(UserPage).Body
+ var response struct {
+ Users []User `mapstructure:"users"`
+ }
+
+ err := mapstructure.Decode(casted, &response)
+ return response.Users, err
+}
+
+// IsEmpty determines whether or not a page of Tenants contains any results.
+func (page RolePage) IsEmpty() (bool, error) {
+ users, err := ExtractRoles(page)
+ if err != nil {
+ return false, err
+ }
+ return len(users) == 0, nil
+}
+
+// ExtractRoles returns a slice of Roles contained in a single page of results.
+func ExtractRoles(page pagination.Page) ([]Role, error) {
+ casted := page.(RolePage).Body
+ var response struct {
+ Roles []Role `mapstructure:"roles"`
+ }
+
+ err := mapstructure.Decode(casted, &response)
+ return response.Roles, err
+}
+
+type commonResult struct {
+ gophercloud.Result
+}
+
+// Extract interprets any commonResult as a User, if possible.
+func (r commonResult) Extract() (*User, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var response struct {
+ User User `mapstructure:"user"`
+ }
+
+ err := mapstructure.Decode(r.Body, &response)
+
+ return &response.User, err
+}
+
+// CreateResult represents the result of a Create operation
+type CreateResult struct {
+ commonResult
+}
+
+// GetResult represents the result of a Get operation
+type GetResult struct {
+ commonResult
+}
+
+// UpdateResult represents the result of an Update operation
+type UpdateResult struct {
+ commonResult
+}
+
+// DeleteResult represents the result of a Delete operation
+type DeleteResult struct {
+ commonResult
+}
diff --git a/openstack/identity/v2/users/urls.go b/openstack/identity/v2/users/urls.go
new file mode 100644
index 0000000..7ec4385
--- /dev/null
+++ b/openstack/identity/v2/users/urls.go
@@ -0,0 +1,21 @@
+package users
+
+import "github.com/rackspace/gophercloud"
+
+const (
+ tenantPath = "tenants"
+ userPath = "users"
+ rolePath = "roles"
+)
+
+func ResourceURL(c *gophercloud.ServiceClient, id string) string {
+ return c.ServiceURL(userPath, id)
+}
+
+func rootURL(c *gophercloud.ServiceClient) string {
+ return c.ServiceURL(userPath)
+}
+
+func listRolesURL(c *gophercloud.ServiceClient, tenantID, userID string) string {
+ return c.ServiceURL(tenantPath, tenantID, userPath, userID, rolePath)
+}
diff --git a/openstack/identity/v3/services/requests_test.go b/openstack/identity/v3/services/requests_test.go
index 32e6d1b..42f05d3 100644
--- a/openstack/identity/v3/services/requests_test.go
+++ b/openstack/identity/v3/services/requests_test.go
@@ -38,7 +38,7 @@
}
if result.Description == nil || *result.Description != "Here's your service" {
- t.Errorf("Service description was unexpected [%s]", result.Description)
+ t.Errorf("Service description was unexpected [%s]", *result.Description)
}
if result.ID != "1234" {
t.Errorf("Service ID was unexpected [%s]", result.ID)
diff --git a/openstack/networking/v2/networks/requests.go b/openstack/networking/v2/networks/requests.go
index eaa7136..dedbb25 100644
--- a/openstack/networking/v2/networks/requests.go
+++ b/openstack/networking/v2/networks/requests.go
@@ -188,7 +188,7 @@
}
// Send request to API
- _, res.Err = perigee.Request("PUT", getURL(c, networkID), perigee.Options{
+ _, res.Err = perigee.Request("PUT", updateURL(c, networkID), perigee.Options{
MoreHeaders: c.AuthenticatedHeaders(),
ReqBody: &reqBody,
Results: &res.Body,
diff --git a/openstack/networking/v2/networks/urls.go b/openstack/networking/v2/networks/urls.go
index 33c2387..a9eecc5 100644
--- a/openstack/networking/v2/networks/urls.go
+++ b/openstack/networking/v2/networks/urls.go
@@ -22,6 +22,10 @@
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)
}
diff --git a/openstack/networking/v2/ports/requests.go b/openstack/networking/v2/ports/requests.go
index 3399907..06d273e 100644
--- a/openstack/networking/v2/ports/requests.go
+++ b/openstack/networking/v2/ports/requests.go
@@ -164,7 +164,6 @@
ReqBody: &reqBody,
Results: &res.Body,
OkCodes: []int{201},
- DumpReqJson: true,
})
return res
diff --git a/openstack/objectstorage/v1/objects/fixtures.go b/openstack/objectstorage/v1/objects/fixtures.go
index d951160..ec61637 100644
--- a/openstack/objectstorage/v1/objects/fixtures.go
+++ b/openstack/objectstorage/v1/objects/fixtures.go
@@ -105,13 +105,31 @@
})
}
-// HandleCreateObjectSuccessfully creates an HTTP handler at `/testContainer/testObject` on the test handler mux that
-// responds with a `Create` response.
-func HandleCreateObjectSuccessfully(t *testing.T) {
+// HandleCreateTextObjectSuccessfully creates an HTTP handler at `/testContainer/testObject` on the test handler mux
+// that responds with a `Create` response. A Content-Type of "text/plain" is expected.
+func HandleCreateTextObjectSuccessfully(t *testing.T) {
+ th.Mux.HandleFunc("/testContainer/testObject", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "PUT")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ th.TestHeader(t, r, "Content-Type", "text/plain")
+ th.TestHeader(t, r, "Accept", "application/json")
+ w.WriteHeader(http.StatusCreated)
+ })
+}
+
+// HandleCreateTypelessObjectSuccessfully creates an HTTP handler at `/testContainer/testObject` on the test handler
+// mux that responds with a `Create` response. No Content-Type header may be present in the request, so that server-
+// side content-type detection will be triggered properly.
+func HandleCreateTypelessObjectSuccessfully(t *testing.T) {
th.Mux.HandleFunc("/testContainer/testObject", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "PUT")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
th.TestHeader(t, r, "Accept", "application/json")
+
+ if contentType, present := r.Header["Content-Type"]; present {
+ t.Errorf("Expected Content-Type header to be omitted, but was %#v", contentType)
+ }
+
w.WriteHeader(http.StatusCreated)
})
}
diff --git a/openstack/objectstorage/v1/objects/requests.go b/openstack/objectstorage/v1/objects/requests.go
index 9778de3..0fdb041 100644
--- a/openstack/objectstorage/v1/objects/requests.go
+++ b/openstack/objectstorage/v1/objects/requests.go
@@ -18,6 +18,10 @@
// ListOpts is a structure that holds parameters for listing objects.
type ListOpts struct {
+ // Full is a true/false value that represents the amount of object information
+ // returned. If Full is set to true, then the content-type, number of bytes, hash
+ // date last modified, and name are returned. If set to false or not set, then
+ // only the object names are returned.
Full bool
Limit int `q:"limit"`
Marker string `q:"marker"`
@@ -88,7 +92,7 @@
// ToObjectDownloadParams formats a DownloadOpts into a query string and map of
// headers.
-func (opts ListOpts) ToObjectDownloadParams() (map[string]string, string, error) {
+func (opts DownloadOpts) ToObjectDownloadParams() (map[string]string, string, error) {
q, err := gophercloud.BuildQueryString(opts)
if err != nil {
return nil, "", err
@@ -125,7 +129,7 @@
resp, err := perigee.Request("GET", url, perigee.Options{
MoreHeaders: h,
- OkCodes: []int{200},
+ OkCodes: []int{200, 304},
})
res.Body = resp.HttpResponse.Body
@@ -146,7 +150,7 @@
Metadata map[string]string
ContentDisposition string `h:"Content-Disposition"`
ContentEncoding string `h:"Content-Encoding"`
- ContentLength int `h:"Content-Length"`
+ ContentLength int64 `h:"Content-Length"`
ContentType string `h:"Content-Type"`
CopyFrom string `h:"X-Copy-From"`
DeleteAfter int `h:"X-Delete-After"`
@@ -201,14 +205,20 @@
url += query
}
- contentType := h["Content-Type"]
-
- resp, err := perigee.Request("PUT", url, perigee.Options{
- ContentType: contentType,
+ popts := perigee.Options{
ReqBody: content,
MoreHeaders: h,
OkCodes: []int{201, 202},
- })
+ }
+
+ if contentType, explicit := h["Content-Type"]; explicit {
+ popts.ContentType = contentType
+ delete(h, "Content-Type")
+ } else {
+ popts.OmitContentType = true
+ }
+
+ resp, err := perigee.Request("PUT", url, popts)
res.Header = resp.HttpResponse.Header
res.Err = err
return res
diff --git a/openstack/objectstorage/v1/objects/requests_test.go b/openstack/objectstorage/v1/objects/requests_test.go
index c3c28a7..6be3534 100644
--- a/openstack/objectstorage/v1/objects/requests_test.go
+++ b/openstack/objectstorage/v1/objects/requests_test.go
@@ -83,14 +83,24 @@
func TestCreateObject(t *testing.T) {
th.SetupHTTP()
defer th.TeardownHTTP()
- HandleCreateObjectSuccessfully(t)
+ HandleCreateTextObjectSuccessfully(t)
content := bytes.NewBufferString("Did gyre and gimble in the wabe")
- options := &CreateOpts{ContentType: "application/json"}
+ options := &CreateOpts{ContentType: "text/plain"}
res := Create(fake.ServiceClient(), "testContainer", "testObject", content, options)
th.AssertNoErr(t, res.Err)
}
+func TestCreateObjectWithoutContentType(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleCreateTypelessObjectSuccessfully(t)
+
+ content := bytes.NewBufferString("The sky was the color of television, tuned to a dead channel.")
+ res := Create(fake.ServiceClient(), "testContainer", "testObject", content, &CreateOpts{})
+ th.AssertNoErr(t, res.Err)
+}
+
func TestCopyObject(t *testing.T) {
th.SetupHTTP()
defer th.TeardownHTTP()
diff --git a/openstack/objectstorage/v1/objects/results.go b/openstack/objectstorage/v1/objects/results.go
index 102d94c..b51b840 100644
--- a/openstack/objectstorage/v1/objects/results.go
+++ b/openstack/objectstorage/v1/objects/results.go
@@ -14,11 +14,22 @@
// Object is a structure that holds information related to a storage object.
type Object struct {
- Bytes int `json:"bytes" mapstructure:"bytes"`
- ContentType string `json:"content_type" mapstructure:"content_type"`
- Hash string `json:"hash" mapstructure:"hash"`
+ // Bytes is the total number of bytes that comprise the object.
+ Bytes int64 `json:"bytes" mapstructure:"bytes"`
+
+ // ContentType is the content type of the object.
+ ContentType string `json:"content_type" mapstructure:"content_type"`
+
+ // Hash represents the MD5 checksum value of the object's content.
+ Hash string `json:"hash" mapstructure:"hash"`
+
+ // LastModified is the RFC3339Milli time the object was last modified, represented
+ // as a string. For any given object (obj), this value may be parsed to a time.Time:
+ // lastModified, err := time.Parse(gophercloud.RFC3339Milli, obj.LastModified)
LastModified string `json:"last_modified" mapstructure:"last_modified"`
- Name string `json:"name" mapstructure:"name"`
+
+ // Name is the unique name for the object.
+ Name string `json:"name" mapstructure:"name"`
}
// ObjectPage is a single page of objects that is returned from a call to the
diff --git a/package.go b/package.go
deleted file mode 100644
index e8c2e82..0000000
--- a/package.go
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
-Package gophercloud provides a multi-vendor interface to OpenStack-compatible
-clouds. The library has a three-level hierarchy: providers, services, and
-resources.
-
-Provider structs represent the service providers that offer and manage a
-collection of services. Examples of providers include: OpenStack, Rackspace,
-HP. These are defined like so:
-
- opts := gophercloud.AuthOptions{
- IdentityEndpoint: "https://my-openstack.com:5000/v2.0",
- Username: "{username}",
- Password: "{password}",
- TenantID: "{tenant_id}",
- }
-
- provider, err := openstack.AuthenticatedClient(opts)
-
-Service structs are specific to a provider and handle all of the logic and
-operations for a particular OpenStack service. Examples of services include:
-Compute, Object Storage, Block Storage. In order to define one, you need to
-pass in the parent provider, like so:
-
- opts := gophercloud.EndpointOpts{Region: "RegionOne"}
-
- client := openstack.NewComputeV2(provider, opts)
-
-Resource structs are the domain models that services make use of in order
-to work with and represent the state of API resources:
-
- server, err := servers.Get(client, "{serverId}").Extract()
-
-Another convention is to return Result structs for API operations, which allow
-you to access the HTTP headers, response body, and associated errors with the
-network transaction. To get a resource struct, you then call the Extract
-method which is chained to the response.
-*/
-package gophercloud
diff --git a/params.go b/params.go
index 5fe3c2c..948783b 100644
--- a/params.go
+++ b/params.go
@@ -9,11 +9,35 @@
"time"
)
-// MaybeString takes a string that might be a zero-value, and either returns a
-// pointer to its address or a nil value (i.e. empty pointer). This is useful
-// for converting zero values in options structs when the end-user hasn't
-// defined values. Those zero values need to be nil in order for the JSON
-// serialization to ignore them.
+// 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.
+type EnabledState *bool
+
+// Convenience vars for EnabledState values.
+var (
+ iTrue = true
+ iFalse = false
+
+ Enabled EnabledState = &iTrue
+ Disabled EnabledState = &iFalse
+)
+
+// IntToPointer is a function for converting integers into integer pointers.
+// This is useful when passing in options to operations.
+func IntToPointer(i int) *int {
+ return &i
+}
+
+/*
+MaybeString is an internal function to be used by request methods in individual
+resource packages.
+
+It takes a string that might be a zero value and returns either a pointer to its
+address or nil. This is useful for allowing users to conveniently omit values
+from an options struct by leaving them zeroed, but still pass nil to the JSON
+serializer so they'll be omitted from the request body.
+*/
func MaybeString(original string) *string {
if original != "" {
return &original
@@ -21,8 +45,14 @@
return nil
}
-// MaybeInt takes an int that might be a zero-value, and either returns a
-// pointer to its address or a nil value (i.e. empty pointer).
+/*
+MaybeInt is an internal function to be used by request methods in individual
+resource packages.
+
+Like MaybeString, it accepts an int that may or may not be a zero value, and
+returns either a pointer to its address or nil. It's intended to hint that the
+JSON serializer should omit its field.
+*/
func MaybeInt(original int) *int {
if original != 0 {
return &original
@@ -61,19 +91,26 @@
}
/*
-BuildQueryString accepts a generic structure and parses it URL struct. It
-converts field names into query names based on "q" tags. So for example, this
-type:
+BuildQueryString is an internal function to be used by request methods in
+individual resource packages.
- struct {
+It accepts a tagged structure and expands it into a URL struct. Field names are
+converted into query parameters based on a "q" tag. For example:
+
+ type struct Something {
Bar string `q:"x_bar"`
Baz int `q:"lorem_ipsum"`
- }{
- Bar: "XXX",
- Baz: "YYY",
}
-will be converted into ?x_bar=XXX&lorem_ipsum=YYYY
+ instance := Something{
+ Bar: "AAA",
+ Baz: "BBB",
+ }
+
+will be converted into "?x_bar=AAA&lorem_ipsum=BBB".
+
+The struct's fields may be strings, integers, or boolean values. Fields left at
+their type's zero value will be omitted from the query.
*/
func BuildQueryString(opts interface{}) (*url.URL, error) {
optsValue := reflect.ValueOf(opts)
@@ -86,7 +123,8 @@
optsType = optsType.Elem()
}
- var optsSlice []string
+ params := url.Values{}
+
if optsValue.Kind() == reflect.Struct {
for i := 0; i < optsValue.NumField(); i++ {
v := optsValue.Field(i)
@@ -101,11 +139,11 @@
if !isZero(v) {
switch v.Kind() {
case reflect.String:
- optsSlice = append(optsSlice, tags[0]+"="+v.String())
+ params.Add(tags[0], v.String())
case reflect.Int:
- optsSlice = append(optsSlice, tags[0]+"="+strconv.FormatInt(v.Int(), 10))
+ params.Add(tags[0], strconv.FormatInt(v.Int(), 10))
case reflect.Bool:
- optsSlice = append(optsSlice, tags[0]+"="+strconv.FormatBool(v.Bool()))
+ params.Add(tags[0], strconv.FormatBool(v.Bool()))
}
} else {
// Otherwise, the field is not set.
@@ -115,26 +153,42 @@
}
}
}
+ }
- }
- // URL encode the string for safety.
- s := strings.Join(optsSlice, "&")
- if s != "" {
- s = "?" + s
- }
- u, err := url.Parse(s)
- if err != nil {
- return nil, err
- }
- return u, nil
+ return &url.URL{RawQuery: params.Encode()}, nil
}
// Return an error if the underlying type of 'opts' isn't a struct.
return nil, fmt.Errorf("Options type is not a struct.")
}
-// BuildHeaders accepts a generic structure and parses it into a string map. It
-// converts field names into header names based on "h" tags, and field values
-// into header values by a simple one-to-one mapping.
+/*
+BuildHeaders is an internal function to be used by request methods in
+individual resource packages.
+
+It accepts an arbitrary tagged structure and produces a string map that's
+suitable for use as the HTTP headers of an outgoing request. Field names are
+mapped to header names based in "h" tags.
+
+ type struct Something {
+ Bar string `h:"x_bar"`
+ Baz int `h:"lorem_ipsum"`
+ }
+
+ instance := Something{
+ Bar: "AAA",
+ Baz: "BBB",
+ }
+
+will be converted into:
+
+ map[string]string{
+ "x_bar": "AAA",
+ "lorem_ipsum": "BBB",
+ }
+
+Untagged fields and fields left at their zero values are skipped. Integers,
+booleans and string values are supported.
+*/
func BuildHeaders(opts interface{}) (map[string]string, error) {
optsValue := reflect.ValueOf(opts)
if optsValue.Kind() == reflect.Ptr {
@@ -182,3 +236,25 @@
// Return an error if the underlying type of 'opts' isn't a struct.
return optsMap, fmt.Errorf("Options type is not a struct.")
}
+
+// IDSliceToQueryString takes a slice of elements and converts them into a query
+// string. For example, if name=foo and slice=[]int{20, 40, 60}, then the
+// result would be `?name=20&name=40&name=60'
+func IDSliceToQueryString(name string, ids []int) string {
+ str := ""
+ for k, v := range ids {
+ if k == 0 {
+ str += "?"
+ } else {
+ str += "&"
+ }
+ str += fmt.Sprintf("%s=%s", name, strconv.Itoa(v))
+ }
+ return str
+}
+
+// IntWithinRange returns TRUE if an integer falls within a defined range, and
+// FALSE if not.
+func IntWithinRange(val, min, max int) bool {
+ return val > min && val < max
+}
diff --git a/params_test.go b/params_test.go
index 9f1d3bd..4a2c9fe 100644
--- a/params_test.go
+++ b/params_test.go
@@ -43,7 +43,7 @@
R: "red",
C: true,
}
- expected := &url.URL{RawQuery: "j=2&r=red&c=true"}
+ expected := &url.URL{RawQuery: "c=true&j=2&r=red"}
actual, err := BuildQueryString(&opts)
if err != nil {
t.Errorf("Error building query string: %v", err)
@@ -138,5 +138,18 @@
expected = false
actual = isZero(testStructValue)
th.CheckEquals(t, expected, actual)
+}
+func TestQueriesAreEscaped(t *testing.T) {
+ type foo struct {
+ Name string `q:"something"`
+ Shape string `q:"else"`
+ }
+
+ expected := &url.URL{RawQuery: "else=Triangl+e&something=blah%2B%3F%21%21foo"}
+
+ actual, err := BuildQueryString(foo{Name: "blah+?!!foo", Shape: "Triangl e"})
+ th.AssertNoErr(t, err)
+
+ th.AssertDeepEquals(t, expected, actual)
}
diff --git a/rackspace/cdn/v1/base/delegate.go b/rackspace/cdn/v1/base/delegate.go
new file mode 100644
index 0000000..5af7e07
--- /dev/null
+++ b/rackspace/cdn/v1/base/delegate.go
@@ -0,0 +1,18 @@
+package base
+
+import (
+ "github.com/rackspace/gophercloud"
+
+ os "github.com/rackspace/gophercloud/openstack/cdn/v1/base"
+)
+
+// Get retrieves the home document, allowing the user to discover the
+// entire API.
+func Get(c *gophercloud.ServiceClient) os.GetResult {
+ return os.Get(c)
+}
+
+// Ping retrieves a ping to the server.
+func Ping(c *gophercloud.ServiceClient) os.PingResult {
+ return os.Ping(c)
+}
diff --git a/rackspace/cdn/v1/base/delegate_test.go b/rackspace/cdn/v1/base/delegate_test.go
new file mode 100644
index 0000000..3c05801
--- /dev/null
+++ b/rackspace/cdn/v1/base/delegate_test.go
@@ -0,0 +1,44 @@
+package base
+
+import (
+ "testing"
+
+ os "github.com/rackspace/gophercloud/openstack/cdn/v1/base"
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestGetHomeDocument(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ os.HandleGetSuccessfully(t)
+
+ actual, err := Get(fake.ServiceClient()).Extract()
+ th.CheckNoErr(t, err)
+
+ expected := os.HomeDocument{
+ "rel/cdn": map[string]interface{}{
+ "href-template": "services{?marker,limit}",
+ "href-vars": map[string]interface{}{
+ "marker": "param/marker",
+ "limit": "param/limit",
+ },
+ "hints": map[string]interface{}{
+ "allow": []string{"GET"},
+ "formats": map[string]interface{}{
+ "application/json": map[string]interface{}{},
+ },
+ },
+ },
+ }
+ th.CheckDeepEquals(t, expected, *actual)
+}
+
+func TestPing(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ os.HandlePingSuccessfully(t)
+
+ err := Ping(fake.ServiceClient()).ExtractErr()
+ th.CheckNoErr(t, err)
+}
diff --git a/rackspace/cdn/v1/base/doc.go b/rackspace/cdn/v1/base/doc.go
new file mode 100644
index 0000000..5582306
--- /dev/null
+++ b/rackspace/cdn/v1/base/doc.go
@@ -0,0 +1,4 @@
+// Package base provides information and interaction with the base API
+// resource in the Rackspace CDN service. This API resource allows for
+// retrieving the Home Document and pinging the root URL.
+package base
diff --git a/rackspace/cdn/v1/flavors/delegate.go b/rackspace/cdn/v1/flavors/delegate.go
new file mode 100644
index 0000000..7152fa2
--- /dev/null
+++ b/rackspace/cdn/v1/flavors/delegate.go
@@ -0,0 +1,18 @@
+package flavors
+
+import (
+ "github.com/rackspace/gophercloud"
+
+ os "github.com/rackspace/gophercloud/openstack/cdn/v1/flavors"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// List returns a single page of CDN flavors.
+func List(c *gophercloud.ServiceClient) pagination.Pager {
+ return os.List(c)
+}
+
+// Get retrieves a specific flavor based on its unique ID.
+func Get(c *gophercloud.ServiceClient, id string) os.GetResult {
+ return os.Get(c, id)
+}
diff --git a/rackspace/cdn/v1/flavors/delegate_test.go b/rackspace/cdn/v1/flavors/delegate_test.go
new file mode 100644
index 0000000..d6d299d
--- /dev/null
+++ b/rackspace/cdn/v1/flavors/delegate_test.go
@@ -0,0 +1,90 @@
+package flavors
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ os "github.com/rackspace/gophercloud/openstack/cdn/v1/flavors"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestList(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ os.HandleListCDNFlavorsSuccessfully(t)
+
+ count := 0
+
+ err := List(fake.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := os.ExtractFlavors(page)
+ if err != nil {
+ t.Errorf("Failed to extract flavors: %v", err)
+ return false, err
+ }
+
+ expected := []os.Flavor{
+ os.Flavor{
+ ID: "europe",
+ Providers: []os.Provider{
+ os.Provider{
+ Provider: "Fastly",
+ Links: []gophercloud.Link{
+ gophercloud.Link{
+ Href: "http://www.fastly.com",
+ Rel: "provider_url",
+ },
+ },
+ },
+ },
+ Links: []gophercloud.Link{
+ gophercloud.Link{
+ Href: "https://www.poppycdn.io/v1.0/flavors/europe",
+ Rel: "self",
+ },
+ },
+ },
+ }
+
+ th.CheckDeepEquals(t, expected, actual)
+
+ return true, nil
+ })
+ th.AssertNoErr(t, err)
+ th.CheckEquals(t, 1, count)
+}
+
+func TestGet(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ os.HandleGetCDNFlavorSuccessfully(t)
+
+ expected := &os.Flavor{
+ ID: "asia",
+ Providers: []os.Provider{
+ os.Provider{
+ Provider: "ChinaCache",
+ Links: []gophercloud.Link{
+ gophercloud.Link{
+ Href: "http://www.chinacache.com",
+ Rel: "provider_url",
+ },
+ },
+ },
+ },
+ Links: []gophercloud.Link{
+ gophercloud.Link{
+ Href: "https://www.poppycdn.io/v1.0/flavors/asia",
+ Rel: "self",
+ },
+ },
+ }
+
+ actual, err := Get(fake.ServiceClient(), "asia").Extract()
+ th.AssertNoErr(t, err)
+ th.AssertDeepEquals(t, expected, actual)
+}
diff --git a/rackspace/cdn/v1/flavors/doc.go b/rackspace/cdn/v1/flavors/doc.go
new file mode 100644
index 0000000..4ad966e
--- /dev/null
+++ b/rackspace/cdn/v1/flavors/doc.go
@@ -0,0 +1,6 @@
+// Package flavors provides information and interaction with the flavors API
+// resource in the Rackspace CDN service. This API resource allows for
+// listing flavors and retrieving a specific flavor.
+//
+// A flavor is a mapping configuration to a CDN provider.
+package flavors
diff --git a/rackspace/cdn/v1/serviceassets/delegate.go b/rackspace/cdn/v1/serviceassets/delegate.go
new file mode 100644
index 0000000..07c93a8
--- /dev/null
+++ b/rackspace/cdn/v1/serviceassets/delegate.go
@@ -0,0 +1,13 @@
+package serviceassets
+
+import (
+ "github.com/rackspace/gophercloud"
+
+ os "github.com/rackspace/gophercloud/openstack/cdn/v1/serviceassets"
+)
+
+// Delete accepts a unique ID and deletes the CDN service asset associated with
+// it.
+func Delete(c *gophercloud.ServiceClient, id string, opts os.DeleteOptsBuilder) os.DeleteResult {
+ return os.Delete(c, id, opts)
+}
diff --git a/rackspace/cdn/v1/serviceassets/delegate_test.go b/rackspace/cdn/v1/serviceassets/delegate_test.go
new file mode 100644
index 0000000..328e168
--- /dev/null
+++ b/rackspace/cdn/v1/serviceassets/delegate_test.go
@@ -0,0 +1,19 @@
+package serviceassets
+
+import (
+ "testing"
+
+ os "github.com/rackspace/gophercloud/openstack/cdn/v1/serviceassets"
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestDelete(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ os.HandleDeleteCDNAssetSuccessfully(t)
+
+ err := Delete(fake.ServiceClient(), "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0", nil).ExtractErr()
+ th.AssertNoErr(t, err)
+}
diff --git a/rackspace/cdn/v1/serviceassets/doc.go b/rackspace/cdn/v1/serviceassets/doc.go
new file mode 100644
index 0000000..46b3d50
--- /dev/null
+++ b/rackspace/cdn/v1/serviceassets/doc.go
@@ -0,0 +1,7 @@
+// Package serviceassets provides information and interaction with the
+// serviceassets API resource in the Rackspace CDN service. This API resource
+// allows for deleting cached assets.
+//
+// A service distributes assets across the network. Service assets let you
+// interrogate properties about these assets and perform certain actions on them.
+package serviceassets
diff --git a/rackspace/cdn/v1/services/delegate.go b/rackspace/cdn/v1/services/delegate.go
new file mode 100644
index 0000000..e3f1459
--- /dev/null
+++ b/rackspace/cdn/v1/services/delegate.go
@@ -0,0 +1,37 @@
+package services
+
+import (
+ "github.com/rackspace/gophercloud"
+
+ os "github.com/rackspace/gophercloud/openstack/cdn/v1/services"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// List returns a Pager which allows you to iterate over a collection of
+// CDN services. It accepts a ListOpts struct, which allows for pagination via
+// marker and limit.
+func List(c *gophercloud.ServiceClient, opts os.ListOptsBuilder) pagination.Pager {
+ return os.List(c, opts)
+}
+
+// Create accepts a CreateOpts struct and creates a new CDN service using the
+// values provided.
+func Create(c *gophercloud.ServiceClient, opts os.CreateOptsBuilder) os.CreateResult {
+ return os.Create(c, opts)
+}
+
+// Get retrieves a specific service based on its unique ID.
+func Get(c *gophercloud.ServiceClient, id string) os.GetResult {
+ return os.Get(c, id)
+}
+
+// Update accepts a UpdateOpts struct and updates an existing CDN service using
+// the values provided.
+func Update(c *gophercloud.ServiceClient, id string, patches []os.Patch) os.UpdateResult {
+ return os.Update(c, id, patches)
+}
+
+// Delete accepts a unique ID and deletes the CDN service associated with it.
+func Delete(c *gophercloud.ServiceClient, id string) os.DeleteResult {
+ return os.Delete(c, id)
+}
diff --git a/rackspace/cdn/v1/services/delegate_test.go b/rackspace/cdn/v1/services/delegate_test.go
new file mode 100644
index 0000000..6c48365
--- /dev/null
+++ b/rackspace/cdn/v1/services/delegate_test.go
@@ -0,0 +1,359 @@
+package services
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ os "github.com/rackspace/gophercloud/openstack/cdn/v1/services"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestList(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ os.HandleListCDNServiceSuccessfully(t)
+
+ count := 0
+
+ err := List(fake.ServiceClient(), &os.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := os.ExtractServices(page)
+ if err != nil {
+ t.Errorf("Failed to extract services: %v", err)
+ return false, err
+ }
+
+ expected := []os.Service{
+ os.Service{
+ ID: "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0",
+ Name: "mywebsite.com",
+ Domains: []os.Domain{
+ os.Domain{
+ Domain: "www.mywebsite.com",
+ },
+ },
+ Origins: []os.Origin{
+ os.Origin{
+ Origin: "mywebsite.com",
+ Port: 80,
+ SSL: false,
+ },
+ },
+ Caching: []os.CacheRule{
+ os.CacheRule{
+ Name: "default",
+ TTL: 3600,
+ },
+ os.CacheRule{
+ Name: "home",
+ TTL: 17200,
+ Rules: []os.TTLRule{
+ os.TTLRule{
+ Name: "index",
+ RequestURL: "/index.htm",
+ },
+ },
+ },
+ os.CacheRule{
+ Name: "images",
+ TTL: 12800,
+ Rules: []os.TTLRule{
+ os.TTLRule{
+ Name: "images",
+ RequestURL: "*.png",
+ },
+ },
+ },
+ },
+ Restrictions: []os.Restriction{
+ os.Restriction{
+ Name: "website only",
+ Rules: []os.RestrictionRule{
+ os.RestrictionRule{
+ Name: "mywebsite.com",
+ Referrer: "www.mywebsite.com",
+ },
+ },
+ },
+ },
+ FlavorID: "asia",
+ Status: "deployed",
+ Errors: []os.Error{},
+ Links: []gophercloud.Link{
+ gophercloud.Link{
+ Href: "https://www.poppycdn.io/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0",
+ Rel: "self",
+ },
+ gophercloud.Link{
+ Href: "mywebsite.com.cdn123.poppycdn.net",
+ Rel: "access_url",
+ },
+ gophercloud.Link{
+ Href: "https://www.poppycdn.io/v1.0/flavors/asia",
+ Rel: "flavor",
+ },
+ },
+ },
+ os.Service{
+ ID: "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f1",
+ Name: "myothersite.com",
+ Domains: []os.Domain{
+ os.Domain{
+ Domain: "www.myothersite.com",
+ },
+ },
+ Origins: []os.Origin{
+ os.Origin{
+ Origin: "44.33.22.11",
+ Port: 80,
+ SSL: false,
+ },
+ os.Origin{
+ Origin: "77.66.55.44",
+ Port: 80,
+ SSL: false,
+ Rules: []os.OriginRule{
+ os.OriginRule{
+ Name: "videos",
+ RequestURL: "^/videos/*.m3u",
+ },
+ },
+ },
+ },
+ Caching: []os.CacheRule{
+ os.CacheRule{
+ Name: "default",
+ TTL: 3600,
+ },
+ },
+ Restrictions: []os.Restriction{},
+ FlavorID: "europe",
+ Status: "deployed",
+ Links: []gophercloud.Link{
+ gophercloud.Link{
+ Href: "https://www.poppycdn.io/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f1",
+ Rel: "self",
+ },
+ gophercloud.Link{
+ Href: "myothersite.com.poppycdn.net",
+ Rel: "access_url",
+ },
+ gophercloud.Link{
+ Href: "https://www.poppycdn.io/v1.0/flavors/europe",
+ Rel: "flavor",
+ },
+ },
+ },
+ }
+
+ th.CheckDeepEquals(t, expected, actual)
+
+ return true, nil
+ })
+ th.AssertNoErr(t, err)
+
+ if count != 1 {
+ t.Errorf("Expected 1 page, got %d", count)
+ }
+}
+
+func TestCreate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ os.HandleCreateCDNServiceSuccessfully(t)
+
+ createOpts := os.CreateOpts{
+ Name: "mywebsite.com",
+ Domains: []os.Domain{
+ os.Domain{
+ Domain: "www.mywebsite.com",
+ },
+ os.Domain{
+ Domain: "blog.mywebsite.com",
+ },
+ },
+ Origins: []os.Origin{
+ os.Origin{
+ Origin: "mywebsite.com",
+ Port: 80,
+ SSL: false,
+ },
+ },
+ Restrictions: []os.Restriction{
+ os.Restriction{
+ Name: "website only",
+ Rules: []os.RestrictionRule{
+ os.RestrictionRule{
+ Name: "mywebsite.com",
+ Referrer: "www.mywebsite.com",
+ },
+ },
+ },
+ },
+ Caching: []os.CacheRule{
+ os.CacheRule{
+ Name: "default",
+ TTL: 3600,
+ },
+ },
+ FlavorID: "cdn",
+ }
+
+ expected := "https://global.cdn.api.rackspacecloud.com/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0"
+ actual, err := Create(fake.ServiceClient(), createOpts).Extract()
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, expected, actual)
+}
+
+func TestGet(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ os.HandleGetCDNServiceSuccessfully(t)
+
+ expected := &os.Service{
+ ID: "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0",
+ Name: "mywebsite.com",
+ Domains: []os.Domain{
+ os.Domain{
+ Domain: "www.mywebsite.com",
+ Protocol: "http",
+ },
+ },
+ Origins: []os.Origin{
+ os.Origin{
+ Origin: "mywebsite.com",
+ Port: 80,
+ SSL: false,
+ },
+ },
+ Caching: []os.CacheRule{
+ os.CacheRule{
+ Name: "default",
+ TTL: 3600,
+ },
+ os.CacheRule{
+ Name: "home",
+ TTL: 17200,
+ Rules: []os.TTLRule{
+ os.TTLRule{
+ Name: "index",
+ RequestURL: "/index.htm",
+ },
+ },
+ },
+ os.CacheRule{
+ Name: "images",
+ TTL: 12800,
+ Rules: []os.TTLRule{
+ os.TTLRule{
+ Name: "images",
+ RequestURL: "*.png",
+ },
+ },
+ },
+ },
+ Restrictions: []os.Restriction{
+ os.Restriction{
+ Name: "website only",
+ Rules: []os.RestrictionRule{
+ os.RestrictionRule{
+ Name: "mywebsite.com",
+ Referrer: "www.mywebsite.com",
+ },
+ },
+ },
+ },
+ FlavorID: "cdn",
+ Status: "deployed",
+ Errors: []os.Error{},
+ Links: []gophercloud.Link{
+ gophercloud.Link{
+ Href: "https://global.cdn.api.rackspacecloud.com/v1.0/110011/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0",
+ Rel: "self",
+ },
+ gophercloud.Link{
+ Href: "blog.mywebsite.com.cdn1.raxcdn.com",
+ Rel: "access_url",
+ },
+ gophercloud.Link{
+ Href: "https://global.cdn.api.rackspacecloud.com/v1.0/110011/flavors/cdn",
+ Rel: "flavor",
+ },
+ },
+ }
+
+ actual, err := Get(fake.ServiceClient(), "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0").Extract()
+ th.AssertNoErr(t, err)
+ th.AssertDeepEquals(t, expected, actual)
+}
+
+func TestSuccessfulUpdate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ os.HandleUpdateCDNServiceSuccessfully(t)
+
+ expected := "https://www.poppycdn.io/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0"
+ ops := []os.Patch{
+ // Append a single Domain
+ os.Append{Value: os.Domain{Domain: "appended.mocksite4.com"}},
+ // Insert a single Domain
+ os.Insertion{
+ Index: 4,
+ Value: os.Domain{Domain: "inserted.mocksite4.com"},
+ },
+ // Bulk addition
+ os.Append{
+ Value: os.DomainList{
+ os.Domain{Domain: "bulkadded1.mocksite4.com"},
+ os.Domain{Domain: "bulkadded2.mocksite4.com"},
+ },
+ },
+ // Replace a single Origin
+ os.Replacement{
+ Index: 2,
+ Value: os.Origin{Origin: "44.33.22.11", Port: 80, SSL: false},
+ },
+ // Bulk replace Origins
+ os.Replacement{
+ Index: 0, // Ignored
+ Value: os.OriginList{
+ os.Origin{Origin: "44.33.22.11", Port: 80, SSL: false},
+ os.Origin{Origin: "55.44.33.22", Port: 443, SSL: true},
+ },
+ },
+ // Remove a single CacheRule
+ os.Removal{
+ Index: 8,
+ Path: os.PathCaching,
+ },
+ // Bulk removal
+ os.Removal{
+ All: true,
+ Path: os.PathCaching,
+ },
+ // Service name replacement
+ os.NameReplacement{
+ NewName: "differentServiceName",
+ },
+ }
+
+ actual, err := Update(fake.ServiceClient(), "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0", ops).Extract()
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, expected, actual)
+}
+
+func TestDelete(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ os.HandleDeleteCDNServiceSuccessfully(t)
+
+ err := Delete(fake.ServiceClient(), "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0").ExtractErr()
+ th.AssertNoErr(t, err)
+}
diff --git a/rackspace/cdn/v1/services/doc.go b/rackspace/cdn/v1/services/doc.go
new file mode 100644
index 0000000..ee6e2a5
--- /dev/null
+++ b/rackspace/cdn/v1/services/doc.go
@@ -0,0 +1,7 @@
+// Package services provides information and interaction with the services API
+// resource in the Rackspace CDN service. This API resource allows for
+// listing, creating, updating, retrieving, and deleting services.
+//
+// A service represents an application that has its content cached to the edge
+// nodes.
+package services
diff --git a/rackspace/client.go b/rackspace/client.go
index 5f739a8..45199a4 100644
--- a/rackspace/client.go
+++ b/rackspace/client.go
@@ -154,3 +154,36 @@
return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil
}
+
+// NewLBV1 creates a ServiceClient that can be used to access the Rackspace
+// Cloud Load Balancer v1 API.
+func NewLBV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
+ eo.ApplyDefaults("rax:load-balancer")
+ url, err := client.EndpointLocator(eo)
+ if err != nil {
+ return nil, err
+ }
+ return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil
+}
+
+// NewNetworkV2 creates a ServiceClient that can be used to access the Rackspace
+// Networking v2 API.
+func NewNetworkV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
+ eo.ApplyDefaults("network")
+ url, err := client.EndpointLocator(eo)
+ if err != nil {
+ return nil, err
+ }
+ return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil
+}
+
+// NewCDNV1 creates a ServiceClient that may be used to access the Rackspace v1
+// CDN service.
+func NewCDNV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
+ eo.ApplyDefaults("rax:cdn")
+ url, err := client.EndpointLocator(eo)
+ if err != nil {
+ return nil, err
+ }
+ return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil
+}
diff --git a/rackspace/compute/v2/flavors/fixtures.go b/rackspace/compute/v2/flavors/fixtures.go
index b6dca93..894f916 100644
--- a/rackspace/compute/v2/flavors/fixtures.go
+++ b/rackspace/compute/v2/flavors/fixtures.go
@@ -1,4 +1,5 @@
// +build fixtures
+
package flavors
import (
diff --git a/rackspace/compute/v2/images/fixtures.go b/rackspace/compute/v2/images/fixtures.go
index c46d196..ccfbdc6 100644
--- a/rackspace/compute/v2/images/fixtures.go
+++ b/rackspace/compute/v2/images/fixtures.go
@@ -1,4 +1,5 @@
// +build fixtures
+
package images
import (
diff --git a/rackspace/compute/v2/servers/requests.go b/rackspace/compute/v2/servers/requests.go
index 884b9cb..809183e 100644
--- a/rackspace/compute/v2/servers/requests.go
+++ b/rackspace/compute/v2/servers/requests.go
@@ -43,6 +43,10 @@
// ConfigDrive [optional] enables metadata injection through a configuration drive.
ConfigDrive bool
+ // AdminPass [optional] sets the root user password. If not set, a randomly-generated
+ // password will be created and returned in the response.
+ AdminPass string
+
// Rackspace-specific extensions begin here.
// KeyPair [optional] specifies the name of the SSH KeyPair to be injected into the newly launched
@@ -72,6 +76,7 @@
Metadata: opts.Metadata,
Personality: opts.Personality,
ConfigDrive: opts.ConfigDrive,
+ AdminPass: opts.AdminPass,
}
drive := diskconfig.CreateOptsExt{
diff --git a/rackspace/identity/v2/roles/delegate.go b/rackspace/identity/v2/roles/delegate.go
new file mode 100644
index 0000000..a6c01e4
--- /dev/null
+++ b/rackspace/identity/v2/roles/delegate.go
@@ -0,0 +1,53 @@
+package roles
+
+import (
+ "github.com/racker/perigee"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+
+ os "github.com/rackspace/gophercloud/openstack/identity/v2/extensions/admin/roles"
+)
+
+// List is the operation responsible for listing all available global roles
+// that a user can adopt.
+func List(client *gophercloud.ServiceClient) pagination.Pager {
+ return os.List(client)
+}
+
+// AddUserRole is the operation responsible for assigning a particular role to
+// a user. This is confined to the scope of the user's tenant - so the tenant
+// ID is a required argument.
+func AddUserRole(client *gophercloud.ServiceClient, userID, roleID string) UserRoleResult {
+ var result UserRoleResult
+
+ _, result.Err = perigee.Request("PUT", userRoleURL(client, userID, roleID), perigee.Options{
+ MoreHeaders: client.AuthenticatedHeaders(),
+ OkCodes: []int{200, 201},
+ })
+
+ return result
+}
+
+// DeleteUserRole is the operation responsible for deleting a particular role
+// from a user. This is confined to the scope of the user's tenant - so the
+// tenant ID is a required argument.
+func DeleteUserRole(client *gophercloud.ServiceClient, userID, roleID string) UserRoleResult {
+ var result UserRoleResult
+
+ _, result.Err = perigee.Request("DELETE", userRoleURL(client, userID, roleID), perigee.Options{
+ MoreHeaders: client.AuthenticatedHeaders(),
+ OkCodes: []int{204},
+ })
+
+ return result
+}
+
+// UserRoleResult represents the result of either an AddUserRole or
+// a DeleteUserRole operation.
+type UserRoleResult struct {
+ gophercloud.ErrResult
+}
+
+func userRoleURL(c *gophercloud.ServiceClient, userID, roleID string) string {
+ return c.ServiceURL(os.UserPath, userID, os.RolePath, os.ExtPath, roleID)
+}
diff --git a/rackspace/identity/v2/roles/delegate_test.go b/rackspace/identity/v2/roles/delegate_test.go
new file mode 100644
index 0000000..fcee97d
--- /dev/null
+++ b/rackspace/identity/v2/roles/delegate_test.go
@@ -0,0 +1,66 @@
+package roles
+
+import (
+ "testing"
+
+ os "github.com/rackspace/gophercloud/openstack/identity/v2/extensions/admin/roles"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+ "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestRole(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ MockListRoleResponse(t)
+
+ count := 0
+
+ err := List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := os.ExtractRoles(page)
+ if err != nil {
+ t.Errorf("Failed to extract users: %v", err)
+ return false, err
+ }
+
+ expected := []os.Role{
+ os.Role{
+ ID: "123",
+ Name: "compute:admin",
+ Description: "Nova Administrator",
+ ServiceID: "cke5372ebabeeabb70a0e702a4626977x4406e5",
+ },
+ }
+
+ th.CheckDeepEquals(t, expected, actual)
+
+ return true, nil
+ })
+
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, 1, count)
+}
+
+func TestAddUserRole(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ MockAddUserRoleResponse(t)
+
+ err := AddUserRole(client.ServiceClient(), "{user_id}", "{role_id}").ExtractErr()
+
+ th.AssertNoErr(t, err)
+}
+
+func TestDeleteUserRole(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ MockDeleteUserRoleResponse(t)
+
+ err := DeleteUserRole(client.ServiceClient(), "{user_id}", "{role_id}").ExtractErr()
+
+ th.AssertNoErr(t, err)
+}
diff --git a/rackspace/identity/v2/roles/fixtures.go b/rackspace/identity/v2/roles/fixtures.go
new file mode 100644
index 0000000..5f22d0f
--- /dev/null
+++ b/rackspace/identity/v2/roles/fixtures.go
@@ -0,0 +1,49 @@
+package roles
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func MockListRoleResponse(t *testing.T) {
+ th.Mux.HandleFunc("/OS-KSADM/roles", 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, `
+{
+ "roles": [
+ {
+ "id": "123",
+ "name": "compute:admin",
+ "description": "Nova Administrator",
+ "serviceId": "cke5372ebabeeabb70a0e702a4626977x4406e5"
+ }
+ ]
+}
+ `)
+ })
+}
+
+func MockAddUserRoleResponse(t *testing.T) {
+ th.Mux.HandleFunc("/users/{user_id}/roles/OS-KSADM/{role_id}", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "PUT")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ w.WriteHeader(http.StatusCreated)
+ })
+}
+
+func MockDeleteUserRoleResponse(t *testing.T) {
+ th.Mux.HandleFunc("/users/{user_id}/roles/OS-KSADM/{role_id}", 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/identity/v2/users/delegate.go b/rackspace/identity/v2/users/delegate.go
new file mode 100644
index 0000000..ae2acde
--- /dev/null
+++ b/rackspace/identity/v2/users/delegate.go
@@ -0,0 +1,145 @@
+package users
+
+import (
+ "errors"
+
+ "github.com/racker/perigee"
+ "github.com/rackspace/gophercloud"
+ os "github.com/rackspace/gophercloud/openstack/identity/v2/users"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// List returns a pager that allows traversal over a collection of users.
+func List(client *gophercloud.ServiceClient) pagination.Pager {
+ return os.List(client)
+}
+
+// CommonOpts are the options which are shared between CreateOpts and
+// UpdateOpts
+type CommonOpts struct {
+ // Required. The username to assign to the user. When provided, the username
+ // must:
+ // - start with an alphabetical (A-Za-z) character
+ // - have a minimum length of 1 character
+ //
+ // The username may contain upper and lowercase characters, as well as any of
+ // the following special character: . - @ _
+ Username string
+
+ // Required. Email address for the user account.
+ Email string
+
+ // Required. Indicates whether the user can authenticate after the user
+ // account is created. If no value is specified, the default value is true.
+ Enabled os.EnabledState
+
+ // Optional. The password to assign to the user. If provided, the password
+ // must:
+ // - start with an alphabetical (A-Za-z) character
+ // - have a minimum length of 8 characters
+ // - contain at least one uppercase character, one lowercase character, and
+ // one numeric character.
+ //
+ // The password may contain any of the following special characters: . - @ _
+ Password string
+}
+
+// CreateOpts represents the options needed when creating new users.
+type CreateOpts CommonOpts
+
+// ToUserCreateMap assembles a request body based on the contents of a CreateOpts.
+func (opts CreateOpts) ToUserCreateMap() (map[string]interface{}, error) {
+ m := make(map[string]interface{})
+
+ if opts.Username == "" {
+ return m, errors.New("Username is a required field")
+ }
+ if opts.Enabled == nil {
+ return m, errors.New("Enabled is a required field")
+ }
+ if opts.Email == "" {
+ return m, errors.New("Email is a required field")
+ }
+
+ if opts.Username != "" {
+ m["username"] = opts.Username
+ }
+ if opts.Email != "" {
+ m["email"] = opts.Email
+ }
+ if opts.Enabled != nil {
+ m["enabled"] = opts.Enabled
+ }
+ if opts.Password != "" {
+ m["OS-KSADM:password"] = opts.Password
+ }
+
+ return map[string]interface{}{"user": m}, nil
+}
+
+// Create is the operation responsible for creating new users.
+func Create(client *gophercloud.ServiceClient, opts os.CreateOptsBuilder) CreateResult {
+ return CreateResult{os.Create(client, opts)}
+}
+
+// Get requests details on a single user, either by ID.
+func Get(client *gophercloud.ServiceClient, id string) GetResult {
+ return GetResult{os.Get(client, id)}
+}
+
+// UpdateOptsBuilder allows extensions to add additional attributes to the Update request.
+type UpdateOptsBuilder interface {
+ ToUserUpdateMap() map[string]interface{}
+}
+
+// UpdateOpts specifies the base attributes that may be updated on an existing server.
+type UpdateOpts CommonOpts
+
+// ToUserUpdateMap formats an UpdateOpts structure into a request body.
+func (opts UpdateOpts) ToUserUpdateMap() map[string]interface{} {
+ m := make(map[string]interface{})
+
+ if opts.Username != "" {
+ m["username"] = opts.Username
+ }
+ if opts.Enabled != nil {
+ m["enabled"] = &opts.Enabled
+ }
+ if opts.Email != "" {
+ m["email"] = opts.Email
+ }
+
+ return map[string]interface{}{"user": m}
+}
+
+// Update is the operation responsible for updating exist users by their UUID.
+func Update(client *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) UpdateResult {
+ var result UpdateResult
+
+ _, result.Err = perigee.Request("POST", os.ResourceURL(client, id), perigee.Options{
+ Results: &result.Body,
+ ReqBody: opts.ToUserUpdateMap(),
+ MoreHeaders: client.AuthenticatedHeaders(),
+ OkCodes: []int{200},
+ })
+
+ return result
+}
+
+// Delete is the operation responsible for permanently deleting an API user.
+func Delete(client *gophercloud.ServiceClient, id string) os.DeleteResult {
+ return os.Delete(client, id)
+}
+
+// ResetAPIKey resets the User's API key.
+func ResetAPIKey(client *gophercloud.ServiceClient, id string) ResetAPIKeyResult {
+ var result ResetAPIKeyResult
+
+ _, result.Err = perigee.Request("POST", resetAPIKeyURL(client, id), perigee.Options{
+ Results: &result.Body,
+ MoreHeaders: client.AuthenticatedHeaders(),
+ OkCodes: []int{200},
+ })
+
+ return result
+}
diff --git a/rackspace/identity/v2/users/delegate_test.go b/rackspace/identity/v2/users/delegate_test.go
new file mode 100644
index 0000000..62faf0c
--- /dev/null
+++ b/rackspace/identity/v2/users/delegate_test.go
@@ -0,0 +1,111 @@
+package users
+
+import (
+ "testing"
+
+ os "github.com/rackspace/gophercloud/openstack/identity/v2/users"
+ "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()).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ users, err := os.ExtractUsers(page)
+
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, "u1000", users[0].ID)
+ th.AssertEquals(t, "u1001", users[1].ID)
+
+ return true, nil
+ })
+
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, 1, count)
+}
+
+func TestCreateUser(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockCreateUser(t)
+
+ opts := CreateOpts{
+ Username: "new_user",
+ Enabled: os.Disabled,
+ Email: "new_user@foo.com",
+ Password: "foo",
+ }
+
+ user, err := Create(client.ServiceClient(), opts).Extract()
+
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, "123456", user.ID)
+ th.AssertEquals(t, "5830280", user.DomainID)
+ th.AssertEquals(t, "DFW", user.DefaultRegion)
+}
+
+func TestGetUser(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockGetUser(t)
+
+ user, err := Get(client.ServiceClient(), "new_user").Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, true, user.Enabled)
+ th.AssertEquals(t, true, user.MultiFactorEnabled)
+}
+
+func TestUpdateUser(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockUpdateUser(t)
+
+ id := "c39e3de9be2d4c779f1dfd6abacc176d"
+
+ opts := UpdateOpts{
+ Enabled: os.Enabled,
+ Email: "new_email@foo.com",
+ }
+
+ user, err := Update(client.ServiceClient(), id, opts).Extract()
+
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, true, user.Enabled)
+ th.AssertEquals(t, "new_email@foo.com", user.Email)
+}
+
+func TestDeleteServer(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockDeleteUser(t)
+
+ res := Delete(client.ServiceClient(), "c39e3de9be2d4c779f1dfd6abacc176d")
+ th.AssertNoErr(t, res.Err)
+}
+
+func TestResetAPIKey(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockResetAPIKey(t)
+
+ apiKey, err := ResetAPIKey(client.ServiceClient(), "99").Extract()
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, "joesmith", apiKey.Username)
+ th.AssertEquals(t, "mooH1eiLahd5ahYood7r", apiKey.APIKey)
+}
diff --git a/rackspace/identity/v2/users/fixtures.go b/rackspace/identity/v2/users/fixtures.go
new file mode 100644
index 0000000..973f39e
--- /dev/null
+++ b/rackspace/identity/v2/users/fixtures.go
@@ -0,0 +1,154 @@
+package users
+
+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("/users", 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, `
+{
+ "users":[
+ {
+ "id": "u1000",
+ "username": "jqsmith",
+ "email": "john.smith@example.org",
+ "enabled": true
+ },
+ {
+ "id": "u1001",
+ "username": "jqsmith",
+ "email": "jane.smith@example.org",
+ "enabled": true
+ }
+ ]
+}
+ `)
+ })
+}
+
+func mockCreateUser(t *testing.T) {
+ th.Mux.HandleFunc("/users", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "POST")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+ th.TestJSONRequest(t, r, `
+{
+ "user": {
+ "username": "new_user",
+ "enabled": false,
+ "email": "new_user@foo.com",
+ "OS-KSADM:password": "foo"
+ }
+}
+ `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, `
+{
+ "user": {
+ "RAX-AUTH:defaultRegion": "DFW",
+ "RAX-AUTH:domainId": "5830280",
+ "id": "123456",
+ "username": "new_user",
+ "email": "new_user@foo.com",
+ "enabled": false
+ }
+}
+`)
+ })
+}
+
+func mockGetUser(t *testing.T) {
+ th.Mux.HandleFunc("/users/new_user", 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, `
+{
+ "user": {
+ "RAX-AUTH:defaultRegion": "DFW",
+ "RAX-AUTH:domainId": "5830280",
+ "RAX-AUTH:multiFactorEnabled": "true",
+ "id": "c39e3de9be2d4c779f1dfd6abacc176d",
+ "username": "jqsmith",
+ "email": "john.smith@example.org",
+ "enabled": true
+ }
+}
+`)
+ })
+}
+
+func mockUpdateUser(t *testing.T) {
+ th.Mux.HandleFunc("/users/c39e3de9be2d4c779f1dfd6abacc176d", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "POST")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+ th.TestJSONRequest(t, r, `
+{
+ "user": {
+ "email": "new_email@foo.com",
+ "enabled": true
+ }
+}
+`)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, `
+{
+ "user": {
+ "RAX-AUTH:defaultRegion": "DFW",
+ "RAX-AUTH:domainId": "5830280",
+ "RAX-AUTH:multiFactorEnabled": "true",
+ "id": "123456",
+ "username": "jqsmith",
+ "email": "new_email@foo.com",
+ "enabled": true
+ }
+}
+`)
+ })
+}
+
+func mockDeleteUser(t *testing.T) {
+ th.Mux.HandleFunc("/users/c39e3de9be2d4c779f1dfd6abacc176d", 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 mockResetAPIKey(t *testing.T) {
+ th.Mux.HandleFunc("/users/99/OS-KSADM/credentials/RAX-KSKEY:apiKeyCredentials/RAX-AUTH/reset", 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.StatusOK)
+ fmt.Fprintf(w, `
+{
+ "RAX-KSKEY:apiKeyCredentials": {
+ "username": "joesmith",
+ "apiKey": "mooH1eiLahd5ahYood7r"
+ }
+}`)
+ })
+}
diff --git a/rackspace/identity/v2/users/results.go b/rackspace/identity/v2/users/results.go
new file mode 100644
index 0000000..6936ecb
--- /dev/null
+++ b/rackspace/identity/v2/users/results.go
@@ -0,0 +1,129 @@
+package users
+
+import (
+ "strconv"
+
+ "github.com/rackspace/gophercloud"
+ os "github.com/rackspace/gophercloud/openstack/identity/v2/users"
+
+ "github.com/mitchellh/mapstructure"
+)
+
+// User represents a user resource that exists on the API.
+type User struct {
+ // The UUID for this user.
+ ID string
+
+ // The human name for this user.
+ Name string
+
+ // The username for this user.
+ Username string
+
+ // Indicates whether the user is enabled (true) or disabled (false).
+ Enabled bool
+
+ // The email address for this user.
+ Email string
+
+ // The ID of the tenant to which this user belongs.
+ TenantID string `mapstructure:"tenant_id"`
+
+ // Specifies the default region for the user account. This value is inherited
+ // from the user administrator when the account is created.
+ DefaultRegion string `mapstructure:"RAX-AUTH:defaultRegion"`
+
+ // Identifies the domain that contains the user account. This value is
+ // inherited from the user administrator when the account is created.
+ DomainID string `mapstructure:"RAX-AUTH:domainId"`
+
+ // The password value that the user needs for authentication. If the Add user
+ // request included a password value, this attribute is not included in the
+ // response.
+ Password string `mapstructure:"OS-KSADM:password"`
+
+ // Indicates whether the user has enabled multi-factor authentication.
+ MultiFactorEnabled bool `mapstructure:"RAX-AUTH:multiFactorEnabled"`
+}
+
+// CreateResult represents the result of a Create operation
+type CreateResult struct {
+ os.CreateResult
+}
+
+// GetResult represents the result of a Get operation
+type GetResult struct {
+ os.GetResult
+}
+
+// UpdateResult represents the result of an Update operation
+type UpdateResult struct {
+ os.UpdateResult
+}
+
+func commonExtract(resp interface{}, err error) (*User, error) {
+ if err != nil {
+ return nil, err
+ }
+
+ var respStruct struct {
+ User *User `json:"user"`
+ }
+
+ // Since the API returns a string instead of a bool, we need to hack the JSON
+ json := resp.(map[string]interface{})
+ user := json["user"].(map[string]interface{})
+ if s, ok := user["RAX-AUTH:multiFactorEnabled"].(string); ok && s != "" {
+ if b, err := strconv.ParseBool(s); err == nil {
+ user["RAX-AUTH:multiFactorEnabled"] = b
+ }
+ }
+
+ err = mapstructure.Decode(json, &respStruct)
+
+ return respStruct.User, err
+}
+
+// Extract will get the Snapshot object out of the GetResult object.
+func (r GetResult) Extract() (*User, error) {
+ return commonExtract(r.Body, r.Err)
+}
+
+// Extract will get the Snapshot object out of the CreateResult object.
+func (r CreateResult) Extract() (*User, error) {
+ return commonExtract(r.Body, r.Err)
+}
+
+// Extract will get the Snapshot object out of the UpdateResult object.
+func (r UpdateResult) Extract() (*User, error) {
+ return commonExtract(r.Body, r.Err)
+}
+
+// ResetAPIKeyResult represents the server response to the ResetAPIKey method.
+type ResetAPIKeyResult struct {
+ gophercloud.Result
+}
+
+// ResetAPIKeyValue represents an API Key that has been reset.
+type ResetAPIKeyValue struct {
+ // The Username for this API Key reset.
+ Username string `mapstructure:"username"`
+
+ // The new API Key for this user.
+ APIKey string `mapstructure:"apiKey"`
+}
+
+// Extract will get the Error or ResetAPIKeyValue object out of the ResetAPIKeyResult object.
+func (r ResetAPIKeyResult) Extract() (*ResetAPIKeyValue, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var response struct {
+ ResetAPIKeyValue ResetAPIKeyValue `mapstructure:"RAX-KSKEY:apiKeyCredentials"`
+ }
+
+ err := mapstructure.Decode(r.Body, &response)
+
+ return &response.ResetAPIKeyValue, err
+}
diff --git a/rackspace/identity/v2/users/urls.go b/rackspace/identity/v2/users/urls.go
new file mode 100644
index 0000000..bc1aaef
--- /dev/null
+++ b/rackspace/identity/v2/users/urls.go
@@ -0,0 +1,7 @@
+package users
+
+import "github.com/rackspace/gophercloud"
+
+func resetAPIKeyURL(client *gophercloud.ServiceClient, id string) string {
+ return client.ServiceURL("users", id, "OS-KSADM", "credentials", "RAX-KSKEY:apiKeyCredentials", "RAX-AUTH", "reset")
+}
diff --git a/rackspace/lb/v1/acl/doc.go b/rackspace/lb/v1/acl/doc.go
new file mode 100644
index 0000000..42325fe
--- /dev/null
+++ b/rackspace/lb/v1/acl/doc.go
@@ -0,0 +1,12 @@
+/*
+Package acl provides information and interaction with the access lists feature
+of the Rackspace Cloud Load Balancer service.
+
+The access list management feature allows fine-grained network access controls
+to be applied to the load balancer's virtual IP address. A single IP address,
+multiple IP addresses, or entire network subnets can be added. Items that are
+configured with the ALLOW type always takes precedence over items with the DENY
+type. To reject traffic from all items except for those with the ALLOW type,
+add a networkItem with an address of "0.0.0.0/0" and a DENY type.
+*/
+package acl
diff --git a/rackspace/lb/v1/acl/fixtures.go b/rackspace/lb/v1/acl/fixtures.go
new file mode 100644
index 0000000..e3c941c
--- /dev/null
+++ b/rackspace/lb/v1/acl/fixtures.go
@@ -0,0 +1,109 @@
+package acl
+
+import (
+ "fmt"
+ "net/http"
+ "strconv"
+ "testing"
+
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func _rootURL(lbID int) string {
+ return "/loadbalancers/" + strconv.Itoa(lbID) + "/accesslist"
+}
+
+func mockListResponse(t *testing.T, id int) {
+ th.Mux.HandleFunc(_rootURL(id), 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, `
+{
+ "accessList": [
+ {
+ "address": "206.160.163.21",
+ "id": 21,
+ "type": "DENY"
+ },
+ {
+ "address": "206.160.163.22",
+ "id": 22,
+ "type": "DENY"
+ },
+ {
+ "address": "206.160.163.23",
+ "id": 23,
+ "type": "DENY"
+ },
+ {
+ "address": "206.160.163.24",
+ "id": 24,
+ "type": "DENY"
+ }
+ ]
+}
+ `)
+ })
+}
+
+func mockCreateResponse(t *testing.T, lbID int) {
+ th.Mux.HandleFunc(_rootURL(lbID), func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "POST")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+ th.TestJSONRequest(t, r, `
+{
+ "accessList": [
+ {
+ "address": "206.160.163.21",
+ "type": "DENY"
+ },
+ {
+ "address": "206.160.165.11",
+ "type": "DENY"
+ }
+ ]
+}
+ `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusAccepted)
+ })
+}
+
+func mockDeleteAllResponse(t *testing.T, lbID int) {
+ th.Mux.HandleFunc(_rootURL(lbID), func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "DELETE")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ w.WriteHeader(http.StatusAccepted)
+ })
+}
+
+func mockBatchDeleteResponse(t *testing.T, lbID int, ids []int) {
+ th.Mux.HandleFunc(_rootURL(lbID), func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "DELETE")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+ r.ParseForm()
+
+ for k, v := range ids {
+ fids := r.Form["id"]
+ th.AssertEquals(t, strconv.Itoa(v), fids[k])
+ }
+
+ w.WriteHeader(http.StatusAccepted)
+ })
+}
+
+func mockDeleteResponse(t *testing.T, lbID, networkID int) {
+ th.Mux.HandleFunc(_rootURL(lbID)+"/"+strconv.Itoa(networkID), func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "DELETE")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ w.WriteHeader(http.StatusAccepted)
+ })
+}
diff --git a/rackspace/lb/v1/acl/requests.go b/rackspace/lb/v1/acl/requests.go
new file mode 100644
index 0000000..e1e92ac
--- /dev/null
+++ b/rackspace/lb/v1/acl/requests.go
@@ -0,0 +1,127 @@
+package acl
+
+import (
+ "errors"
+ "fmt"
+
+ "github.com/racker/perigee"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// List is the operation responsible for returning a paginated collection of
+// network items that define a load balancer's access list.
+func List(client *gophercloud.ServiceClient, lbID int) pagination.Pager {
+ url := rootURL(client, lbID)
+
+ return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page {
+ return AccessListPage{pagination.SinglePageBase(r)}
+ })
+}
+
+// CreateOptsBuilder is the interface responsible for generating the JSON
+// for a Create operation.
+type CreateOptsBuilder interface {
+ ToAccessListCreateMap() (map[string]interface{}, error)
+}
+
+// CreateOpts is a slice of CreateOpt structs, that allow the user to create
+// multiple nodes in a single operation (one node per CreateOpt).
+type CreateOpts []CreateOpt
+
+// CreateOpt represents the options to create a single node.
+type CreateOpt struct {
+ // Required - the IP address or CIDR for item to add to access list.
+ Address string
+
+ // Required - the type of the node. Either ALLOW or DENY.
+ Type Type
+}
+
+// ToAccessListCreateMap converts a slice of options into a map that can be
+// used for the JSON.
+func (opts CreateOpts) ToAccessListCreateMap() (map[string]interface{}, error) {
+ type itemMap map[string]interface{}
+ items := []itemMap{}
+
+ for k, v := range opts {
+ if v.Address == "" {
+ return itemMap{}, fmt.Errorf("Address is a required attribute, none provided for %d CreateOpt element", k)
+ }
+ if v.Type != ALLOW && v.Type != DENY {
+ return itemMap{}, fmt.Errorf("Type must be ALLOW or DENY")
+ }
+
+ item := make(itemMap)
+ item["address"] = v.Address
+ item["type"] = v.Type
+
+ items = append(items, item)
+ }
+
+ return itemMap{"accessList": items}, nil
+}
+
+// Create is the operation responsible for adding network items to the access
+// rules for a particular load balancer. If network items already exist, the
+// new item will be appended. A single IP address or subnet range is considered
+// unique and cannot be duplicated.
+func Create(client *gophercloud.ServiceClient, loadBalancerID int, opts CreateOptsBuilder) CreateResult {
+ var res CreateResult
+
+ reqBody, err := opts.ToAccessListCreateMap()
+ if err != nil {
+ res.Err = err
+ return res
+ }
+
+ _, res.Err = perigee.Request("POST", rootURL(client, loadBalancerID), perigee.Options{
+ MoreHeaders: client.AuthenticatedHeaders(),
+ ReqBody: &reqBody,
+ OkCodes: []int{202},
+ })
+
+ return res
+}
+
+// BulkDelete will delete multiple network items from a load balancer's access
+// list in a single operation.
+func BulkDelete(c *gophercloud.ServiceClient, loadBalancerID int, itemIDs []int) DeleteResult {
+ var res DeleteResult
+
+ if len(itemIDs) > 10 || len(itemIDs) == 0 {
+ res.Err = errors.New("You must provide a minimum of 1 and a maximum of 10 item IDs")
+ return res
+ }
+
+ url := rootURL(c, loadBalancerID)
+ url += gophercloud.IDSliceToQueryString("id", itemIDs)
+
+ _, res.Err = perigee.Request("DELETE", url, perigee.Options{
+ MoreHeaders: c.AuthenticatedHeaders(),
+ OkCodes: []int{202},
+ })
+
+ return res
+}
+
+// Delete will remove a single network item from a load balancer's access list.
+func Delete(c *gophercloud.ServiceClient, lbID, itemID int) DeleteResult {
+ var res DeleteResult
+ _, res.Err = perigee.Request("DELETE", resourceURL(c, lbID, itemID), perigee.Options{
+ MoreHeaders: c.AuthenticatedHeaders(),
+ OkCodes: []int{202},
+ })
+ return res
+}
+
+// DeleteAll will delete the entire contents of a load balancer's access list,
+// effectively resetting it and allowing all traffic.
+func DeleteAll(c *gophercloud.ServiceClient, lbID int) DeleteResult {
+ var res DeleteResult
+ _, res.Err = perigee.Request("DELETE", rootURL(c, lbID), perigee.Options{
+ MoreHeaders: c.AuthenticatedHeaders(),
+ OkCodes: []int{202},
+ })
+ return res
+}
diff --git a/rackspace/lb/v1/acl/requests_test.go b/rackspace/lb/v1/acl/requests_test.go
new file mode 100644
index 0000000..c4961a3
--- /dev/null
+++ b/rackspace/lb/v1/acl/requests_test.go
@@ -0,0 +1,91 @@
+package acl
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+ "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+const (
+ lbID = 12345
+ itemID1 = 67890
+ itemID2 = 67891
+)
+
+func TestList(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockListResponse(t, lbID)
+
+ count := 0
+
+ err := List(client.ServiceClient(), lbID).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractAccessList(page)
+ th.AssertNoErr(t, err)
+
+ expected := AccessList{
+ NetworkItem{Address: "206.160.163.21", ID: 21, Type: DENY},
+ NetworkItem{Address: "206.160.163.22", ID: 22, Type: DENY},
+ NetworkItem{Address: "206.160.163.23", ID: 23, Type: DENY},
+ NetworkItem{Address: "206.160.163.24", ID: 24, Type: DENY},
+ }
+
+ th.CheckDeepEquals(t, expected, actual)
+
+ return true, nil
+ })
+
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, 1, count)
+}
+
+func TestCreate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockCreateResponse(t, lbID)
+
+ opts := CreateOpts{
+ CreateOpt{Address: "206.160.163.21", Type: DENY},
+ CreateOpt{Address: "206.160.165.11", Type: DENY},
+ }
+
+ err := Create(client.ServiceClient(), lbID, opts).ExtractErr()
+ th.AssertNoErr(t, err)
+}
+
+func TestBulkDelete(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ ids := []int{itemID1, itemID2}
+
+ mockBatchDeleteResponse(t, lbID, ids)
+
+ err := BulkDelete(client.ServiceClient(), lbID, ids).ExtractErr()
+ th.AssertNoErr(t, err)
+}
+
+func TestDelete(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockDeleteResponse(t, lbID, itemID1)
+
+ err := Delete(client.ServiceClient(), lbID, itemID1).ExtractErr()
+ th.AssertNoErr(t, err)
+}
+
+func TestDeleteAll(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockDeleteAllResponse(t, lbID)
+
+ err := DeleteAll(client.ServiceClient(), lbID).ExtractErr()
+ th.AssertNoErr(t, err)
+}
diff --git a/rackspace/lb/v1/acl/results.go b/rackspace/lb/v1/acl/results.go
new file mode 100644
index 0000000..9ea5ea2
--- /dev/null
+++ b/rackspace/lb/v1/acl/results.go
@@ -0,0 +1,72 @@
+package acl
+
+import (
+ "github.com/mitchellh/mapstructure"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// AccessList represents the rules of network access to a particular load
+// balancer.
+type AccessList []NetworkItem
+
+// NetworkItem describes how an IP address or entire subnet may interact with a
+// load balancer.
+type NetworkItem struct {
+ // The IP address or subnet (CIDR) that defines the network item.
+ Address string
+
+ // The numeric unique ID for this item.
+ ID int
+
+ // Either ALLOW or DENY.
+ Type Type
+}
+
+// Type defines how an item may connect to the load balancer.
+type Type string
+
+// Convenience consts.
+const (
+ ALLOW Type = "ALLOW"
+ DENY Type = "DENY"
+)
+
+// AccessListPage is the page returned by a pager for traversing over a
+// collection of network items in an access list.
+type AccessListPage struct {
+ pagination.SinglePageBase
+}
+
+// IsEmpty checks whether an AccessListPage struct is empty.
+func (p AccessListPage) IsEmpty() (bool, error) {
+ is, err := ExtractAccessList(p)
+ if err != nil {
+ return true, nil
+ }
+ return len(is) == 0, nil
+}
+
+// ExtractAccessList accepts a Page struct, specifically an AccessListPage
+// struct, and extracts the elements into a slice of NetworkItem structs. In
+// other words, a generic collection is mapped into a relevant slice.
+func ExtractAccessList(page pagination.Page) (AccessList, error) {
+ var resp struct {
+ List AccessList `mapstructure:"accessList" json:"accessList"`
+ }
+
+ err := mapstructure.Decode(page.(AccessListPage).Body, &resp)
+
+ return resp.List, err
+}
+
+// CreateResult represents the result of a create operation.
+type CreateResult struct {
+ gophercloud.ErrResult
+}
+
+// DeleteResult represents the result of a delete operation.
+type DeleteResult struct {
+ gophercloud.ErrResult
+}
diff --git a/rackspace/lb/v1/acl/urls.go b/rackspace/lb/v1/acl/urls.go
new file mode 100644
index 0000000..e373fa1
--- /dev/null
+++ b/rackspace/lb/v1/acl/urls.go
@@ -0,0 +1,20 @@
+package acl
+
+import (
+ "strconv"
+
+ "github.com/rackspace/gophercloud"
+)
+
+const (
+ path = "loadbalancers"
+ aclPath = "accesslist"
+)
+
+func resourceURL(c *gophercloud.ServiceClient, lbID, networkID int) string {
+ return c.ServiceURL(path, strconv.Itoa(lbID), aclPath, strconv.Itoa(networkID))
+}
+
+func rootURL(c *gophercloud.ServiceClient, lbID int) string {
+ return c.ServiceURL(path, strconv.Itoa(lbID), aclPath)
+}
diff --git a/rackspace/lb/v1/lbs/doc.go b/rackspace/lb/v1/lbs/doc.go
new file mode 100644
index 0000000..05f0032
--- /dev/null
+++ b/rackspace/lb/v1/lbs/doc.go
@@ -0,0 +1,44 @@
+/*
+Package lbs provides information and interaction with the Load Balancer API
+resource for the Rackspace Cloud Load Balancer service.
+
+A load balancer is a logical device which belongs to a cloud account. It is
+used to distribute workloads between multiple back-end systems or services,
+based on the criteria defined as part of its configuration. This configuration
+is defined using the Create operation, and can be updated with Update.
+
+To conserve IPv4 address space, it is highly recommended that you share Virtual
+IPs between load balancers. If you have at least one load balancer, you may
+create subsequent ones that share a single virtual IPv4 and/or a single IPv6 by
+passing in a virtual IP ID to the Update operation (instead of a type). This
+feature is also highly desirable if you wish to load balance both an insecure
+and secure protocol using one IP or DNS name. In order to share a virtual IP,
+each Load Balancer must utilize a unique port.
+
+All load balancers have a Status attribute that shows the current configuration
+status of the device. This status is immutable by the caller and is updated
+automatically based on state changes within the service. When a load balancer
+is first created, it is placed into a BUILD state while the configuration is
+being generated and applied based on the request. Once the configuration is
+applied and finalized, it is in an ACTIVE status. In the event of a
+configuration change or update, the status of the load balancer changes to
+PENDING_UPDATE to signify configuration changes are in progress but have not yet
+been finalized. Load balancers in a SUSPENDED status are configured to reject
+traffic and do not forward requests to back-end nodes.
+
+An HTTP load balancer has the X-Forwarded-For (XFF) HTTP header set by default.
+This header contains the originating IP address of a client connecting to a web
+server through an HTTP proxy or load balancer, which many web applications are
+already designed to use when determining the source address for a request.
+
+It also includes the X-Forwarded-Proto (XFP) HTTP header, which has been added
+for identifying the originating protocol of an HTTP request as "http" or
+"https" depending on which protocol the client requested. This is useful when
+using SSL termination.
+
+Finally, it also includes the X-Forwarded-Port HTTP header, which has been
+added for being able to generate secure URLs containing the specified port.
+This header, along with the X-Forwarded-For header, provides the needed
+information to the underlying application servers.
+*/
+package lbs
diff --git a/rackspace/lb/v1/lbs/fixtures.go b/rackspace/lb/v1/lbs/fixtures.go
new file mode 100644
index 0000000..6325310
--- /dev/null
+++ b/rackspace/lb/v1/lbs/fixtures.go
@@ -0,0 +1,584 @@
+package lbs
+
+import (
+ "fmt"
+ "net/http"
+ "strconv"
+ "testing"
+
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func mockListLBResponse(t *testing.T) {
+ th.Mux.HandleFunc("/loadbalancers", 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, `
+{
+ "loadBalancers":[
+ {
+ "name":"lb-site1",
+ "id":71,
+ "protocol":"HTTP",
+ "port":80,
+ "algorithm":"RANDOM",
+ "status":"ACTIVE",
+ "nodeCount":3,
+ "virtualIps":[
+ {
+ "id":403,
+ "address":"206.55.130.1",
+ "type":"PUBLIC",
+ "ipVersion":"IPV4"
+ }
+ ],
+ "created":{
+ "time":"2010-11-30T03:23:42Z"
+ },
+ "updated":{
+ "time":"2010-11-30T03:23:44Z"
+ }
+ },
+ {
+ "name":"lb-site2",
+ "id":72,
+ "created":{
+ "time":"2011-11-30T03:23:42Z"
+ },
+ "updated":{
+ "time":"2011-11-30T03:23:44Z"
+ }
+ },
+ {
+ "name":"lb-site3",
+ "id":73,
+ "created":{
+ "time":"2012-11-30T03:23:42Z"
+ },
+ "updated":{
+ "time":"2012-11-30T03:23:44Z"
+ }
+ }
+ ]
+}
+ `)
+ })
+}
+
+func mockCreateLBResponse(t *testing.T) {
+ th.Mux.HandleFunc("/loadbalancers", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "POST")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+ th.TestJSONRequest(t, r, `
+{
+ "loadBalancer": {
+ "name": "a-new-loadbalancer",
+ "port": 80,
+ "protocol": "HTTP",
+ "virtualIps": [
+ {
+ "id": 2341
+ },
+ {
+ "id": 900001
+ }
+ ],
+ "nodes": [
+ {
+ "address": "10.1.1.1",
+ "port": 80,
+ "condition": "ENABLED"
+ }
+ ]
+ }
+}
+ `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusAccepted)
+
+ fmt.Fprintf(w, `
+{
+ "loadBalancer": {
+ "name": "a-new-loadbalancer",
+ "id": 144,
+ "protocol": "HTTP",
+ "halfClosed": false,
+ "port": 83,
+ "algorithm": "RANDOM",
+ "status": "BUILD",
+ "timeout": 30,
+ "cluster": {
+ "name": "ztm-n01.staging1.lbaas.rackspace.net"
+ },
+ "nodes": [
+ {
+ "address": "10.1.1.1",
+ "id": 653,
+ "port": 80,
+ "status": "ONLINE",
+ "condition": "ENABLED",
+ "weight": 1
+ }
+ ],
+ "virtualIps": [
+ {
+ "address": "206.10.10.210",
+ "id": 39,
+ "type": "PUBLIC",
+ "ipVersion": "IPV4"
+ },
+ {
+ "address": "2001:4801:79f1:0002:711b:be4c:0000:0021",
+ "id": 900001,
+ "type": "PUBLIC",
+ "ipVersion": "IPV6"
+ }
+ ],
+ "created": {
+ "time": "2010-11-30T03:23:42Z"
+ },
+ "updated": {
+ "time": "2010-11-30T03:23:44Z"
+ },
+ "connectionLogging": {
+ "enabled": false
+ }
+ }
+}
+ `)
+ })
+}
+
+func mockBatchDeleteLBResponse(t *testing.T, ids []int) {
+ th.Mux.HandleFunc("/loadbalancers", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "DELETE")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+ r.ParseForm()
+
+ for k, v := range ids {
+ fids := r.Form["id"]
+ th.AssertEquals(t, strconv.Itoa(v), fids[k])
+ }
+
+ w.WriteHeader(http.StatusAccepted)
+ })
+}
+
+func mockDeleteLBResponse(t *testing.T, id int) {
+ th.Mux.HandleFunc("/loadbalancers/"+strconv.Itoa(id), func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "DELETE")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ w.WriteHeader(http.StatusAccepted)
+ })
+}
+
+func mockGetLBResponse(t *testing.T, id int) {
+ th.Mux.HandleFunc("/loadbalancers/"+strconv.Itoa(id), 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, `
+{
+ "loadBalancer": {
+ "id": 2000,
+ "name": "sample-loadbalancer",
+ "protocol": "HTTP",
+ "port": 80,
+ "algorithm": "RANDOM",
+ "status": "ACTIVE",
+ "timeout": 30,
+ "connectionLogging": {
+ "enabled": true
+ },
+ "virtualIps": [
+ {
+ "id": 1000,
+ "address": "206.10.10.210",
+ "type": "PUBLIC",
+ "ipVersion": "IPV4"
+ }
+ ],
+ "nodes": [
+ {
+ "id": 1041,
+ "address": "10.1.1.1",
+ "port": 80,
+ "condition": "ENABLED",
+ "status": "ONLINE"
+ },
+ {
+ "id": 1411,
+ "address": "10.1.1.2",
+ "port": 80,
+ "condition": "ENABLED",
+ "status": "ONLINE"
+ }
+ ],
+ "sessionPersistence": {
+ "persistenceType": "HTTP_COOKIE"
+ },
+ "connectionThrottle": {
+ "maxConnections": 100
+ },
+ "cluster": {
+ "name": "c1.dfw1"
+ },
+ "created": {
+ "time": "2010-11-30T03:23:42Z"
+ },
+ "updated": {
+ "time": "2010-11-30T03:23:44Z"
+ },
+ "sourceAddresses": {
+ "ipv6Public": "2001:4801:79f1:1::1/64",
+ "ipv4Servicenet": "10.0.0.0",
+ "ipv4Public": "10.12.99.28"
+ }
+ }
+}
+ `)
+ })
+}
+
+func mockUpdateLBResponse(t *testing.T, id int) {
+ th.Mux.HandleFunc("/loadbalancers/"+strconv.Itoa(id), 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, `
+{
+ "loadBalancer": {
+ "name": "a-new-loadbalancer",
+ "protocol": "TCP",
+ "halfClosed": true,
+ "algorithm": "RANDOM",
+ "port": 8080,
+ "timeout": 100,
+ "httpsRedirect": false
+ }
+}
+ `)
+
+ w.WriteHeader(http.StatusAccepted)
+ })
+}
+
+func mockListProtocolsResponse(t *testing.T) {
+ th.Mux.HandleFunc("/loadbalancers/protocols", 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, `
+{
+ "protocols": [
+ {
+ "name": "DNS_TCP",
+ "port": 53
+ },
+ {
+ "name": "DNS_UDP",
+ "port": 53
+ },
+ {
+ "name": "FTP",
+ "port": 21
+ },
+ {
+ "name": "HTTP",
+ "port": 80
+ },
+ {
+ "name": "HTTPS",
+ "port": 443
+ },
+ {
+ "name": "IMAPS",
+ "port": 993
+ },
+ {
+ "name": "IMAPv4",
+ "port": 143
+ },
+ {
+ "name": "LDAP",
+ "port": 389
+ },
+ {
+ "name": "LDAPS",
+ "port": 636
+ },
+ {
+ "name": "MYSQL",
+ "port": 3306
+ },
+ {
+ "name": "POP3",
+ "port": 110
+ },
+ {
+ "name": "POP3S",
+ "port": 995
+ },
+ {
+ "name": "SMTP",
+ "port": 25
+ },
+ {
+ "name": "TCP",
+ "port": 0
+ },
+ {
+ "name": "TCP_CLIENT_FIRST",
+ "port": 0
+ },
+ {
+ "name": "UDP",
+ "port": 0
+ },
+ {
+ "name": "UDP_STREAM",
+ "port": 0
+ },
+ {
+ "name": "SFTP",
+ "port": 22
+ },
+ {
+ "name": "TCP_STREAM",
+ "port": 0
+ }
+ ]
+}
+ `)
+ })
+}
+
+func mockListAlgorithmsResponse(t *testing.T) {
+ th.Mux.HandleFunc("/loadbalancers/algorithms", 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, `
+{
+ "algorithms": [
+ {
+ "name": "LEAST_CONNECTIONS"
+ },
+ {
+ "name": "RANDOM"
+ },
+ {
+ "name": "ROUND_ROBIN"
+ },
+ {
+ "name": "WEIGHTED_LEAST_CONNECTIONS"
+ },
+ {
+ "name": "WEIGHTED_ROUND_ROBIN"
+ }
+ ]
+}
+ `)
+ })
+}
+
+func mockGetLoggingResponse(t *testing.T, id int) {
+ th.Mux.HandleFunc("/loadbalancers/"+strconv.Itoa(id)+"/connectionlogging", 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, `
+{
+ "connectionLogging": {
+ "enabled": true
+ }
+}
+ `)
+ })
+}
+
+func mockEnableLoggingResponse(t *testing.T, id int) {
+ th.Mux.HandleFunc("/loadbalancers/"+strconv.Itoa(id)+"/connectionlogging", 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, `
+{
+ "connectionLogging":{
+ "enabled":true
+ }
+}
+ `)
+
+ w.WriteHeader(http.StatusAccepted)
+ })
+}
+
+func mockDisableLoggingResponse(t *testing.T, id int) {
+ th.Mux.HandleFunc("/loadbalancers/"+strconv.Itoa(id)+"/connectionlogging", 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, `
+{
+ "connectionLogging":{
+ "enabled":false
+ }
+}
+ `)
+
+ w.WriteHeader(http.StatusAccepted)
+ })
+}
+
+func mockGetErrorPageResponse(t *testing.T, id int) {
+ th.Mux.HandleFunc("/loadbalancers/"+strconv.Itoa(id)+"/errorpage", 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, `
+{
+ "errorpage": {
+ "content": "<html>DEFAULT ERROR PAGE</html>"
+ }
+}
+ `)
+ })
+}
+
+func mockSetErrorPageResponse(t *testing.T, id int) {
+ th.Mux.HandleFunc("/loadbalancers/"+strconv.Itoa(id)+"/errorpage", 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, `
+{
+ "errorpage": {
+ "content": "<html>New error page</html>"
+ }
+}
+ `)
+
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, `
+{
+ "errorpage": {
+ "content": "<html>New error page</html>"
+ }
+}
+ `)
+ })
+}
+
+func mockDeleteErrorPageResponse(t *testing.T, id int) {
+ th.Mux.HandleFunc("/loadbalancers/"+strconv.Itoa(id)+"/errorpage", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "DELETE")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ w.WriteHeader(http.StatusOK)
+ })
+}
+
+func mockGetStatsResponse(t *testing.T, id int) {
+ th.Mux.HandleFunc("/loadbalancers/"+strconv.Itoa(id)+"/stats", 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, `
+{
+ "connectTimeOut": 10,
+ "connectError": 20,
+ "connectFailure": 30,
+ "dataTimedOut": 40,
+ "keepAliveTimedOut": 50,
+ "maxConn": 60,
+ "currentConn": 40,
+ "connectTimeOutSsl": 10,
+ "connectErrorSsl": 20,
+ "connectFailureSsl": 30,
+ "dataTimedOutSsl": 40,
+ "keepAliveTimedOutSsl": 50,
+ "maxConnSsl": 60,
+ "currentConnSsl": 40
+}
+ `)
+ })
+}
+
+func mockGetCachingResponse(t *testing.T, id int) {
+ th.Mux.HandleFunc("/loadbalancers/"+strconv.Itoa(id)+"/contentcaching", 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, `
+{
+ "contentCaching": {
+ "enabled": true
+ }
+}
+ `)
+ })
+}
+
+func mockEnableCachingResponse(t *testing.T, id int) {
+ th.Mux.HandleFunc("/loadbalancers/"+strconv.Itoa(id)+"/contentcaching", 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, `
+{
+ "contentCaching":{
+ "enabled":true
+ }
+}
+ `)
+
+ w.WriteHeader(http.StatusAccepted)
+ })
+}
+
+func mockDisableCachingResponse(t *testing.T, id int) {
+ th.Mux.HandleFunc("/loadbalancers/"+strconv.Itoa(id)+"/contentcaching", 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, `
+{
+ "contentCaching":{
+ "enabled":false
+ }
+}
+ `)
+
+ w.WriteHeader(http.StatusAccepted)
+ })
+}
diff --git a/rackspace/lb/v1/lbs/requests.go b/rackspace/lb/v1/lbs/requests.go
new file mode 100644
index 0000000..342f107
--- /dev/null
+++ b/rackspace/lb/v1/lbs/requests.go
@@ -0,0 +1,574 @@
+package lbs
+
+import (
+ "errors"
+
+ "github.com/mitchellh/mapstructure"
+ "github.com/racker/perigee"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+ "github.com/rackspace/gophercloud/rackspace/lb/v1/acl"
+ "github.com/rackspace/gophercloud/rackspace/lb/v1/monitors"
+ "github.com/rackspace/gophercloud/rackspace/lb/v1/nodes"
+ "github.com/rackspace/gophercloud/rackspace/lb/v1/sessions"
+ "github.com/rackspace/gophercloud/rackspace/lb/v1/throttle"
+ "github.com/rackspace/gophercloud/rackspace/lb/v1/vips"
+)
+
+var (
+ errNameRequired = errors.New("Name is a required attribute")
+ errTimeoutExceeded = errors.New("Timeout must be less than 120")
+)
+
+// ListOptsBuilder allows extensions to add additional parameters to the
+// List request.
+type ListOptsBuilder interface {
+ ToLBListQuery() (string, error)
+}
+
+// ListOpts allows the filtering and sorting of paginated collections through
+// the API.
+type ListOpts struct {
+ ChangesSince string `q:"changes-since"`
+ Status Status `q:"status"`
+ NodeAddr string `q:"nodeaddress"`
+ Marker string `q:"marker"`
+ Limit int `q:"limit"`
+}
+
+// ToLBListQuery formats a ListOpts into a query string.
+func (opts ListOpts) ToLBListQuery() (string, error) {
+ q, err := gophercloud.BuildQueryString(opts)
+ if err != nil {
+ return "", err
+ }
+ return q.String(), nil
+}
+
+// List is the operation responsible for returning a paginated collection of
+// load balancers. You may pass in a ListOpts struct to filter results.
+func List(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
+ url := rootURL(client)
+ if opts != nil {
+ query, err := opts.ToLBListQuery()
+ if err != nil {
+ return pagination.Pager{Err: err}
+ }
+ url += query
+ }
+
+ return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page {
+ return LBPage{pagination.LinkedPageBase{PageResult: r}}
+ })
+}
+
+// CreateOptsBuilder is the interface options structs have to satisfy in order
+// to be used in the main Create operation in this package. Since many
+// extensions decorate or modify the common logic, it is useful for them to
+// satisfy a basic interface in order for them to be used.
+type CreateOptsBuilder interface {
+ ToLBCreateMap() (map[string]interface{}, error)
+}
+
+// CreateOpts is the common options struct used in this package's Create
+// operation.
+type CreateOpts struct {
+ // Required - name of the load balancer to create. The name must be 128
+ // characters or fewer in length, and all UTF-8 characters are valid.
+ Name string
+
+ // Optional - nodes to be added.
+ Nodes []nodes.Node
+
+ // Required - protocol of the service that is being load balanced.
+ // See http://docs.rackspace.com/loadbalancers/api/v1.0/clb-devguide/content/protocols.html
+ // for a full list of supported protocols.
+ Protocol string
+
+ // Optional - enables or disables Half-Closed support for the load balancer.
+ // Half-Closed support provides the ability for one end of the connection to
+ // terminate its output, while still receiving data from the other end. Only
+ // available for TCP/TCP_CLIENT_FIRST protocols.
+ HalfClosed gophercloud.EnabledState
+
+ // Optional - the type of virtual IPs you want associated with the load
+ // balancer.
+ VIPs []vips.VIP
+
+ // Optional - the access list management feature allows fine-grained network
+ // access controls to be applied to the load balancer virtual IP address.
+ AccessList *acl.AccessList
+
+ // Optional - algorithm that defines how traffic should be directed between
+ // back-end nodes.
+ Algorithm string
+
+ // Optional - current connection logging configuration.
+ ConnectionLogging *ConnectionLogging
+
+ // Optional - specifies a limit on the number of connections per IP address
+ // to help mitigate malicious or abusive traffic to your applications.
+ ConnThrottle *throttle.ConnectionThrottle
+
+ // Optional - the type of health monitor check to perform to ensure that the
+ // service is performing properly.
+ HealthMonitor *monitors.Monitor
+
+ // Optional - arbitrary information that can be associated with each LB.
+ Metadata map[string]interface{}
+
+ // Optional - port number for the service you are load balancing.
+ Port int
+
+ // Optional - the timeout value for the load balancer and communications with
+ // its nodes. Defaults to 30 seconds with a maximum of 120 seconds.
+ Timeout int
+
+ // Optional - specifies whether multiple requests from clients are directed
+ // to the same node.
+ SessionPersistence *sessions.SessionPersistence
+
+ // Optional - enables or disables HTTP to HTTPS redirection for the load
+ // balancer. When enabled, any HTTP request returns status code 301 (Moved
+ // Permanently), and the requester is redirected to the requested URL via the
+ // HTTPS protocol on port 443. For example, http://example.com/page.html
+ // would be redirected to https://example.com/page.html. Only available for
+ // HTTPS protocol (port=443), or HTTP protocol with a properly configured SSL
+ // termination (secureTrafficOnly=true, securePort=443).
+ HTTPSRedirect gophercloud.EnabledState
+}
+
+// ToLBCreateMap casts a CreateOpts struct to a map.
+func (opts CreateOpts) ToLBCreateMap() (map[string]interface{}, error) {
+ lb := make(map[string]interface{})
+
+ if opts.Name == "" {
+ return lb, errNameRequired
+ }
+ if opts.Timeout > 120 {
+ return lb, errTimeoutExceeded
+ }
+
+ lb["name"] = opts.Name
+
+ if len(opts.Nodes) > 0 {
+ nodes := []map[string]interface{}{}
+ for _, n := range opts.Nodes {
+ nodes = append(nodes, map[string]interface{}{
+ "address": n.Address,
+ "port": n.Port,
+ "condition": n.Condition,
+ })
+ }
+ lb["nodes"] = nodes
+ }
+
+ if opts.Protocol != "" {
+ lb["protocol"] = opts.Protocol
+ }
+ if opts.HalfClosed != nil {
+ lb["halfClosed"] = opts.HalfClosed
+ }
+ if len(opts.VIPs) > 0 {
+ lb["virtualIps"] = opts.VIPs
+ }
+ if opts.AccessList != nil {
+ lb["accessList"] = &opts.AccessList
+ }
+ if opts.Algorithm != "" {
+ lb["algorithm"] = opts.Algorithm
+ }
+ if opts.ConnectionLogging != nil {
+ lb["connectionLogging"] = &opts.ConnectionLogging
+ }
+ if opts.ConnThrottle != nil {
+ lb["connectionThrottle"] = &opts.ConnThrottle
+ }
+ if opts.HealthMonitor != nil {
+ lb["healthMonitor"] = &opts.HealthMonitor
+ }
+ if len(opts.Metadata) != 0 {
+ lb["metadata"] = opts.Metadata
+ }
+ if opts.Port > 0 {
+ lb["port"] = opts.Port
+ }
+ if opts.Timeout > 0 {
+ lb["timeout"] = opts.Timeout
+ }
+ if opts.SessionPersistence != nil {
+ lb["sessionPersistence"] = &opts.SessionPersistence
+ }
+ if opts.HTTPSRedirect != nil {
+ lb["httpsRedirect"] = &opts.HTTPSRedirect
+ }
+
+ return map[string]interface{}{"loadBalancer": lb}, nil
+}
+
+// Create is the operation responsible for asynchronously provisioning a new
+// load balancer based on the configuration defined in CreateOpts. Once the
+// request is validated and progress has started on the provisioning process, a
+// response struct is returned. When extracted (with Extract()), you have
+// to the load balancer's unique ID and status.
+//
+// Once an ID is attained, you can check on the progress of the operation by
+// calling Get and passing in the ID. If the corresponding request cannot be
+// fulfilled due to insufficient or invalid data, an HTTP 400 (Bad Request)
+// error response is returned with information regarding the nature of the
+// failure in the body of the response. Failures in the validation process are
+// non-recoverable and require the caller to correct the cause of the failure.
+func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult {
+ var res CreateResult
+
+ reqBody, err := opts.ToLBCreateMap()
+ if err != nil {
+ res.Err = err
+ return res
+ }
+
+ _, res.Err = perigee.Request("POST", rootURL(c), perigee.Options{
+ MoreHeaders: c.AuthenticatedHeaders(),
+ ReqBody: &reqBody,
+ Results: &res.Body,
+ OkCodes: []int{202},
+ })
+
+ return res
+}
+
+// Get is the operation responsible for providing detailed information
+// regarding a specific load balancer which is configured and associated with
+// your account. This operation is not capable of returning details for a load
+// balancer which has been deleted.
+func Get(c *gophercloud.ServiceClient, id int) GetResult {
+ var res GetResult
+
+ _, res.Err = perigee.Request("GET", resourceURL(c, id), perigee.Options{
+ MoreHeaders: c.AuthenticatedHeaders(),
+ Results: &res.Body,
+ OkCodes: []int{200},
+ })
+
+ return res
+}
+
+// BulkDelete removes all the load balancers referenced in the slice of IDs.
+// Any and all configuration data associated with these load balancers is
+// immediately purged and is not recoverable.
+//
+// If one of the items in the list cannot be removed due to its current status,
+// a 400 Bad Request error is returned along with the IDs of the ones the
+// system identified as potential failures for this request.
+func BulkDelete(c *gophercloud.ServiceClient, ids []int) DeleteResult {
+ var res DeleteResult
+
+ if len(ids) > 10 || len(ids) == 0 {
+ res.Err = errors.New("You must provide a minimum of 1 and a maximum of 10 LB IDs")
+ return res
+ }
+
+ url := rootURL(c)
+ url += gophercloud.IDSliceToQueryString("id", ids)
+
+ _, res.Err = perigee.Request("DELETE", url, perigee.Options{
+ MoreHeaders: c.AuthenticatedHeaders(),
+ OkCodes: []int{202},
+ })
+
+ return res
+}
+
+// Delete removes a single load balancer.
+func Delete(c *gophercloud.ServiceClient, id int) DeleteResult {
+ var res DeleteResult
+
+ _, res.Err = perigee.Request("DELETE", resourceURL(c, id), perigee.Options{
+ MoreHeaders: c.AuthenticatedHeaders(),
+ OkCodes: []int{202},
+ })
+
+ return res
+}
+
+// UpdateOptsBuilder represents a type that can be converted into a JSON-like
+// map structure.
+type UpdateOptsBuilder interface {
+ ToLBUpdateMap() (map[string]interface{}, error)
+}
+
+// UpdateOpts represents the options for updating an existing load balancer.
+type UpdateOpts struct {
+ // Optional - new name of the load balancer.
+ Name string
+
+ // Optional - the new protocol you want your load balancer to have.
+ // See http://docs.rackspace.com/loadbalancers/api/v1.0/clb-devguide/content/protocols.html
+ // for a full list of supported protocols.
+ Protocol string
+
+ // Optional - see the HalfClosed field in CreateOpts for more information.
+ HalfClosed gophercloud.EnabledState
+
+ // Optional - see the Algorithm field in CreateOpts for more information.
+ Algorithm string
+
+ // Optional - see the Port field in CreateOpts for more information.
+ Port int
+
+ // Optional - see the Timeout field in CreateOpts for more information.
+ Timeout int
+
+ // Optional - see the HTTPSRedirect field in CreateOpts for more information.
+ HTTPSRedirect gophercloud.EnabledState
+}
+
+// ToLBUpdateMap casts an UpdateOpts struct to a map.
+func (opts UpdateOpts) ToLBUpdateMap() (map[string]interface{}, error) {
+ lb := make(map[string]interface{})
+
+ if opts.Name != "" {
+ lb["name"] = opts.Name
+ }
+ if opts.Protocol != "" {
+ lb["protocol"] = opts.Protocol
+ }
+ if opts.HalfClosed != nil {
+ lb["halfClosed"] = opts.HalfClosed
+ }
+ if opts.Algorithm != "" {
+ lb["algorithm"] = opts.Algorithm
+ }
+ if opts.Port > 0 {
+ lb["port"] = opts.Port
+ }
+ if opts.Timeout > 0 {
+ lb["timeout"] = opts.Timeout
+ }
+ if opts.HTTPSRedirect != nil {
+ lb["httpsRedirect"] = &opts.HTTPSRedirect
+ }
+
+ return map[string]interface{}{"loadBalancer": lb}, nil
+}
+
+// Update is the operation responsible for asynchronously updating the
+// attributes of a specific load balancer. Upon successful validation of the
+// request, the service returns a 202 Accepted response, and the load balancer
+// enters a PENDING_UPDATE state. A user can poll the load balancer with Get to
+// wait for the changes to be applied. When this happens, the load balancer will
+// return to an ACTIVE state.
+func Update(c *gophercloud.ServiceClient, id int, opts UpdateOptsBuilder) UpdateResult {
+ var res UpdateResult
+
+ reqBody, err := opts.ToLBUpdateMap()
+ if err != nil {
+ res.Err = err
+ return res
+ }
+
+ _, res.Err = perigee.Request("PUT", resourceURL(c, id), perigee.Options{
+ MoreHeaders: c.AuthenticatedHeaders(),
+ ReqBody: &reqBody,
+ OkCodes: []int{202},
+ })
+
+ return res
+}
+
+// ListProtocols is the operation responsible for returning a paginated
+// collection of load balancer protocols.
+func ListProtocols(client *gophercloud.ServiceClient) pagination.Pager {
+ url := protocolsURL(client)
+ return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page {
+ return ProtocolPage{pagination.SinglePageBase(r)}
+ })
+}
+
+// ListAlgorithms is the operation responsible for returning a paginated
+// collection of load balancer algorithms.
+func ListAlgorithms(client *gophercloud.ServiceClient) pagination.Pager {
+ url := algorithmsURL(client)
+ return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page {
+ return AlgorithmPage{pagination.SinglePageBase(r)}
+ })
+}
+
+// IsLoggingEnabled returns true if the load balancer has connection logging
+// enabled and false if not.
+func IsLoggingEnabled(client *gophercloud.ServiceClient, id int) (bool, error) {
+ var body interface{}
+
+ _, err := perigee.Request("GET", loggingURL(client, id), perigee.Options{
+ MoreHeaders: client.AuthenticatedHeaders(),
+ Results: &body,
+ OkCodes: []int{200},
+ })
+ if err != nil {
+ return false, err
+ }
+
+ var resp struct {
+ CL struct {
+ Enabled bool `mapstructure:"enabled"`
+ } `mapstructure:"connectionLogging"`
+ }
+
+ err = mapstructure.Decode(body, &resp)
+ return resp.CL.Enabled, err
+}
+
+func toConnLoggingMap(state bool) map[string]map[string]bool {
+ return map[string]map[string]bool{
+ "connectionLogging": map[string]bool{"enabled": state},
+ }
+}
+
+// EnableLogging will enable connection logging for a specified load balancer.
+func EnableLogging(client *gophercloud.ServiceClient, id int) gophercloud.ErrResult {
+ reqBody := toConnLoggingMap(true)
+ var res gophercloud.ErrResult
+
+ _, res.Err = perigee.Request("PUT", loggingURL(client, id), perigee.Options{
+ MoreHeaders: client.AuthenticatedHeaders(),
+ ReqBody: &reqBody,
+ OkCodes: []int{202},
+ })
+
+ return res
+}
+
+// DisableLogging will disable connection logging for a specified load balancer.
+func DisableLogging(client *gophercloud.ServiceClient, id int) gophercloud.ErrResult {
+ reqBody := toConnLoggingMap(false)
+ var res gophercloud.ErrResult
+
+ _, res.Err = perigee.Request("PUT", loggingURL(client, id), perigee.Options{
+ MoreHeaders: client.AuthenticatedHeaders(),
+ ReqBody: &reqBody,
+ OkCodes: []int{202},
+ })
+
+ return res
+}
+
+// GetErrorPage will retrieve the current error page for the load balancer.
+func GetErrorPage(client *gophercloud.ServiceClient, id int) ErrorPageResult {
+ var res ErrorPageResult
+
+ _, res.Err = perigee.Request("GET", errorPageURL(client, id), perigee.Options{
+ MoreHeaders: client.AuthenticatedHeaders(),
+ Results: &res.Body,
+ OkCodes: []int{200},
+ })
+
+ return res
+}
+
+// SetErrorPage will set the HTML of the load balancer's error page to a
+// specific value.
+func SetErrorPage(client *gophercloud.ServiceClient, id int, html string) ErrorPageResult {
+ var res ErrorPageResult
+
+ type stringMap map[string]string
+ reqBody := map[string]stringMap{"errorpage": stringMap{"content": html}}
+
+ _, res.Err = perigee.Request("PUT", errorPageURL(client, id), perigee.Options{
+ MoreHeaders: client.AuthenticatedHeaders(),
+ Results: &res.Body,
+ ReqBody: &reqBody,
+ OkCodes: []int{200},
+ })
+
+ return res
+}
+
+// DeleteErrorPage will delete the current error page for the load balancer.
+func DeleteErrorPage(client *gophercloud.ServiceClient, id int) gophercloud.ErrResult {
+ var res gophercloud.ErrResult
+
+ _, res.Err = perigee.Request("DELETE", errorPageURL(client, id), perigee.Options{
+ MoreHeaders: client.AuthenticatedHeaders(),
+ OkCodes: []int{200},
+ })
+
+ return res
+}
+
+// GetStats will retrieve detailed stats related to the load balancer's usage.
+func GetStats(client *gophercloud.ServiceClient, id int) StatsResult {
+ var res StatsResult
+
+ _, res.Err = perigee.Request("GET", statsURL(client, id), perigee.Options{
+ MoreHeaders: client.AuthenticatedHeaders(),
+ Results: &res.Body,
+ OkCodes: []int{200},
+ })
+
+ return res
+}
+
+// IsContentCached will check to see whether the specified load balancer caches
+// content. When content caching is enabled, recently-accessed files are stored
+// on the load balancer for easy retrieval by web clients. Content caching
+// improves the performance of high traffic web sites by temporarily storing
+// data that was recently accessed. While it's cached, requests for that data
+// are served by the load balancer, which in turn reduces load off the back-end
+// nodes. The result is improved response times for those requests and less
+// load on the web server.
+func IsContentCached(client *gophercloud.ServiceClient, id int) (bool, error) {
+ var body interface{}
+
+ _, err := perigee.Request("GET", cacheURL(client, id), perigee.Options{
+ MoreHeaders: client.AuthenticatedHeaders(),
+ Results: &body,
+ OkCodes: []int{200},
+ })
+ if err != nil {
+ return false, err
+ }
+
+ var resp struct {
+ CC struct {
+ Enabled bool `mapstructure:"enabled"`
+ } `mapstructure:"contentCaching"`
+ }
+
+ err = mapstructure.Decode(body, &resp)
+ return resp.CC.Enabled, err
+}
+
+func toCachingMap(state bool) map[string]map[string]bool {
+ return map[string]map[string]bool{
+ "contentCaching": map[string]bool{"enabled": state},
+ }
+}
+
+// EnableCaching will enable content-caching for the specified load balancer.
+func EnableCaching(client *gophercloud.ServiceClient, id int) gophercloud.ErrResult {
+ reqBody := toCachingMap(true)
+ var res gophercloud.ErrResult
+
+ _, res.Err = perigee.Request("PUT", cacheURL(client, id), perigee.Options{
+ MoreHeaders: client.AuthenticatedHeaders(),
+ ReqBody: &reqBody,
+ OkCodes: []int{202},
+ })
+
+ return res
+}
+
+// DisableCaching will disable content-caching for the specified load balancer.
+func DisableCaching(client *gophercloud.ServiceClient, id int) gophercloud.ErrResult {
+ reqBody := toCachingMap(false)
+ var res gophercloud.ErrResult
+
+ _, res.Err = perigee.Request("PUT", cacheURL(client, id), perigee.Options{
+ MoreHeaders: client.AuthenticatedHeaders(),
+ ReqBody: &reqBody,
+ OkCodes: []int{202},
+ })
+
+ return res
+}
diff --git a/rackspace/lb/v1/lbs/requests_test.go b/rackspace/lb/v1/lbs/requests_test.go
new file mode 100644
index 0000000..a8ec19e
--- /dev/null
+++ b/rackspace/lb/v1/lbs/requests_test.go
@@ -0,0 +1,438 @@
+package lbs
+
+import (
+ "testing"
+ "time"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+ "github.com/rackspace/gophercloud/rackspace/lb/v1/nodes"
+ "github.com/rackspace/gophercloud/rackspace/lb/v1/sessions"
+ "github.com/rackspace/gophercloud/rackspace/lb/v1/throttle"
+ "github.com/rackspace/gophercloud/rackspace/lb/v1/vips"
+ th "github.com/rackspace/gophercloud/testhelper"
+ "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+const (
+ id1 = 12345
+ id2 = 67890
+ ts1 = "2010-11-30T03:23:42Z"
+ ts2 = "2010-11-30T03:23:44Z"
+)
+
+func toTime(t *testing.T, str string) time.Time {
+ ts, err := time.Parse(time.RFC3339, str)
+ if err != nil {
+ t.Fatalf("Could not parse time: %s", err.Error())
+ }
+ return ts
+}
+
+func TestList(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockListLBResponse(t)
+
+ count := 0
+
+ err := List(client.ServiceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractLBs(page)
+ th.AssertNoErr(t, err)
+
+ expected := []LoadBalancer{
+ LoadBalancer{
+ Name: "lb-site1",
+ ID: 71,
+ Protocol: "HTTP",
+ Port: 80,
+ Algorithm: "RANDOM",
+ Status: ACTIVE,
+ NodeCount: 3,
+ VIPs: []vips.VIP{
+ vips.VIP{
+ ID: 403,
+ Address: "206.55.130.1",
+ Type: "PUBLIC",
+ Version: "IPV4",
+ },
+ },
+ Created: Datetime{Time: toTime(t, ts1)},
+ Updated: Datetime{Time: toTime(t, ts2)},
+ },
+ LoadBalancer{
+ ID: 72,
+ Name: "lb-site2",
+ Created: Datetime{Time: toTime(t, "2011-11-30T03:23:42Z")},
+ Updated: Datetime{Time: toTime(t, "2011-11-30T03:23:44Z")},
+ },
+ LoadBalancer{
+ ID: 73,
+ Name: "lb-site3",
+ Created: Datetime{Time: toTime(t, "2012-11-30T03:23:42Z")},
+ Updated: Datetime{Time: toTime(t, "2012-11-30T03:23:44Z")},
+ },
+ }
+
+ th.CheckDeepEquals(t, expected, actual)
+
+ return true, nil
+ })
+
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, 1, count)
+}
+
+func TestCreate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockCreateLBResponse(t)
+
+ opts := CreateOpts{
+ Name: "a-new-loadbalancer",
+ Port: 80,
+ Protocol: "HTTP",
+ VIPs: []vips.VIP{
+ vips.VIP{ID: 2341},
+ vips.VIP{ID: 900001},
+ },
+ Nodes: []nodes.Node{
+ nodes.Node{Address: "10.1.1.1", Port: 80, Condition: "ENABLED"},
+ },
+ }
+
+ lb, err := Create(client.ServiceClient(), opts).Extract()
+ th.AssertNoErr(t, err)
+
+ expected := &LoadBalancer{
+ Name: "a-new-loadbalancer",
+ ID: 144,
+ Protocol: "HTTP",
+ HalfClosed: false,
+ Port: 83,
+ Algorithm: "RANDOM",
+ Status: BUILD,
+ Timeout: 30,
+ Cluster: Cluster{Name: "ztm-n01.staging1.lbaas.rackspace.net"},
+ Nodes: []nodes.Node{
+ nodes.Node{
+ Address: "10.1.1.1",
+ ID: 653,
+ Port: 80,
+ Status: "ONLINE",
+ Condition: "ENABLED",
+ Weight: 1,
+ },
+ },
+ VIPs: []vips.VIP{
+ vips.VIP{
+ ID: 39,
+ Address: "206.10.10.210",
+ Type: vips.PUBLIC,
+ Version: vips.IPV4,
+ },
+ vips.VIP{
+ ID: 900001,
+ Address: "2001:4801:79f1:0002:711b:be4c:0000:0021",
+ Type: vips.PUBLIC,
+ Version: vips.IPV6,
+ },
+ },
+ Created: Datetime{Time: toTime(t, ts1)},
+ Updated: Datetime{Time: toTime(t, ts2)},
+ ConnectionLogging: ConnectionLogging{Enabled: false},
+ }
+
+ th.AssertDeepEquals(t, expected, lb)
+}
+
+func TestBulkDelete(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ ids := []int{id1, id2}
+
+ mockBatchDeleteLBResponse(t, ids)
+
+ err := BulkDelete(client.ServiceClient(), ids).ExtractErr()
+ th.AssertNoErr(t, err)
+}
+
+func TestDelete(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockDeleteLBResponse(t, id1)
+
+ err := Delete(client.ServiceClient(), id1).ExtractErr()
+ th.AssertNoErr(t, err)
+}
+
+func TestGet(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockGetLBResponse(t, id1)
+
+ lb, err := Get(client.ServiceClient(), id1).Extract()
+
+ expected := &LoadBalancer{
+ Name: "sample-loadbalancer",
+ ID: 2000,
+ Protocol: "HTTP",
+ Port: 80,
+ Algorithm: "RANDOM",
+ Status: ACTIVE,
+ Timeout: 30,
+ ConnectionLogging: ConnectionLogging{Enabled: true},
+ VIPs: []vips.VIP{
+ vips.VIP{
+ ID: 1000,
+ Address: "206.10.10.210",
+ Type: "PUBLIC",
+ Version: "IPV4",
+ },
+ },
+ Nodes: []nodes.Node{
+ nodes.Node{
+ Address: "10.1.1.1",
+ ID: 1041,
+ Port: 80,
+ Status: "ONLINE",
+ Condition: "ENABLED",
+ },
+ nodes.Node{
+ Address: "10.1.1.2",
+ ID: 1411,
+ Port: 80,
+ Status: "ONLINE",
+ Condition: "ENABLED",
+ },
+ },
+ SessionPersistence: sessions.SessionPersistence{Type: "HTTP_COOKIE"},
+ ConnectionThrottle: throttle.ConnectionThrottle{MaxConnections: 100},
+ Cluster: Cluster{Name: "c1.dfw1"},
+ Created: Datetime{Time: toTime(t, ts1)},
+ Updated: Datetime{Time: toTime(t, ts2)},
+ SourceAddrs: SourceAddrs{
+ IPv4Public: "10.12.99.28",
+ IPv4Private: "10.0.0.0",
+ IPv6Public: "2001:4801:79f1:1::1/64",
+ },
+ }
+
+ th.AssertDeepEquals(t, expected, lb)
+ th.AssertNoErr(t, err)
+}
+
+func TestUpdate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockUpdateLBResponse(t, id1)
+
+ opts := UpdateOpts{
+ Name: "a-new-loadbalancer",
+ Protocol: "TCP",
+ HalfClosed: gophercloud.Enabled,
+ Algorithm: "RANDOM",
+ Port: 8080,
+ Timeout: 100,
+ HTTPSRedirect: gophercloud.Disabled,
+ }
+
+ err := Update(client.ServiceClient(), id1, opts).ExtractErr()
+ th.AssertNoErr(t, err)
+}
+
+func TestListProtocols(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockListProtocolsResponse(t)
+
+ count := 0
+
+ err := ListProtocols(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractProtocols(page)
+ th.AssertNoErr(t, err)
+
+ expected := []Protocol{
+ Protocol{Name: "DNS_TCP", Port: 53},
+ Protocol{Name: "DNS_UDP", Port: 53},
+ Protocol{Name: "FTP", Port: 21},
+ Protocol{Name: "HTTP", Port: 80},
+ Protocol{Name: "HTTPS", Port: 443},
+ Protocol{Name: "IMAPS", Port: 993},
+ Protocol{Name: "IMAPv4", Port: 143},
+ }
+
+ th.CheckDeepEquals(t, expected[0:7], actual)
+
+ return true, nil
+ })
+
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, 1, count)
+}
+
+func TestListAlgorithms(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockListAlgorithmsResponse(t)
+
+ count := 0
+
+ err := ListAlgorithms(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractAlgorithms(page)
+ th.AssertNoErr(t, err)
+
+ expected := []Algorithm{
+ Algorithm{Name: "LEAST_CONNECTIONS"},
+ Algorithm{Name: "RANDOM"},
+ Algorithm{Name: "ROUND_ROBIN"},
+ Algorithm{Name: "WEIGHTED_LEAST_CONNECTIONS"},
+ Algorithm{Name: "WEIGHTED_ROUND_ROBIN"},
+ }
+
+ th.CheckDeepEquals(t, expected, actual)
+
+ return true, nil
+ })
+
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, 1, count)
+}
+
+func TestIsLoggingEnabled(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockGetLoggingResponse(t, id1)
+
+ res, err := IsLoggingEnabled(client.ServiceClient(), id1)
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, true, res)
+}
+
+func TestEnablingLogging(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockEnableLoggingResponse(t, id1)
+
+ err := EnableLogging(client.ServiceClient(), id1).ExtractErr()
+ th.AssertNoErr(t, err)
+}
+
+func TestDisablingLogging(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockDisableLoggingResponse(t, id1)
+
+ err := DisableLogging(client.ServiceClient(), id1).ExtractErr()
+ th.AssertNoErr(t, err)
+}
+
+func TestGetErrorPage(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockGetErrorPageResponse(t, id1)
+
+ content, err := GetErrorPage(client.ServiceClient(), id1).Extract()
+ th.AssertNoErr(t, err)
+
+ expected := &ErrorPage{Content: "<html>DEFAULT ERROR PAGE</html>"}
+ th.AssertDeepEquals(t, expected, content)
+}
+
+func TestSetErrorPage(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockSetErrorPageResponse(t, id1)
+
+ html := "<html>New error page</html>"
+ content, err := SetErrorPage(client.ServiceClient(), id1, html).Extract()
+ th.AssertNoErr(t, err)
+
+ expected := &ErrorPage{Content: html}
+ th.AssertDeepEquals(t, expected, content)
+}
+
+func TestDeleteErrorPage(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockDeleteErrorPageResponse(t, id1)
+
+ err := DeleteErrorPage(client.ServiceClient(), id1).ExtractErr()
+ th.AssertNoErr(t, err)
+}
+
+func TestGetStats(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockGetStatsResponse(t, id1)
+
+ content, err := GetStats(client.ServiceClient(), id1).Extract()
+ th.AssertNoErr(t, err)
+
+ expected := &Stats{
+ ConnectTimeout: 10,
+ ConnectError: 20,
+ ConnectFailure: 30,
+ DataTimedOut: 40,
+ KeepAliveTimedOut: 50,
+ MaxConnections: 60,
+ CurrentConnections: 40,
+ SSLConnectTimeout: 10,
+ SSLConnectError: 20,
+ SSLConnectFailure: 30,
+ SSLDataTimedOut: 40,
+ SSLKeepAliveTimedOut: 50,
+ SSLMaxConnections: 60,
+ SSLCurrentConnections: 40,
+ }
+ th.AssertDeepEquals(t, expected, content)
+}
+
+func TestIsCached(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockGetCachingResponse(t, id1)
+
+ res, err := IsContentCached(client.ServiceClient(), id1)
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, true, res)
+}
+
+func TestEnablingCaching(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockEnableCachingResponse(t, id1)
+
+ err := EnableCaching(client.ServiceClient(), id1).ExtractErr()
+ th.AssertNoErr(t, err)
+}
+
+func TestDisablingCaching(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockDisableCachingResponse(t, id1)
+
+ err := DisableCaching(client.ServiceClient(), id1).ExtractErr()
+ th.AssertNoErr(t, err)
+}
diff --git a/rackspace/lb/v1/lbs/results.go b/rackspace/lb/v1/lbs/results.go
new file mode 100644
index 0000000..98f3962
--- /dev/null
+++ b/rackspace/lb/v1/lbs/results.go
@@ -0,0 +1,420 @@
+package lbs
+
+import (
+ "reflect"
+ "time"
+
+ "github.com/mitchellh/mapstructure"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+ "github.com/rackspace/gophercloud/rackspace/lb/v1/acl"
+ "github.com/rackspace/gophercloud/rackspace/lb/v1/nodes"
+ "github.com/rackspace/gophercloud/rackspace/lb/v1/sessions"
+ "github.com/rackspace/gophercloud/rackspace/lb/v1/throttle"
+ "github.com/rackspace/gophercloud/rackspace/lb/v1/vips"
+)
+
+// Protocol represents the network protocol which the load balancer accepts.
+type Protocol struct {
+ // The name of the protocol, e.g. HTTP, LDAP, FTP, etc.
+ Name string
+
+ // The port number for the protocol.
+ Port int
+}
+
+// Algorithm defines how traffic should be directed between back-end nodes.
+type Algorithm struct {
+ // The name of the algorithm, e.g RANDOM, ROUND_ROBIN, etc.
+ Name string
+}
+
+// Status represents the potential state of a load balancer resource.
+type Status string
+
+const (
+ // ACTIVE indicates that the LB is configured properly and ready to serve
+ // traffic to incoming requests via the configured virtual IPs.
+ ACTIVE Status = "ACTIVE"
+
+ // BUILD indicates that the LB is being provisioned for the first time and
+ // configuration is being applied to bring the service online. The service
+ // cannot yet serve incoming requests.
+ BUILD Status = "BUILD"
+
+ // PENDINGUPDATE indicates that the LB is online but configuration changes
+ // are being applied to update the service based on a previous request.
+ PENDINGUPDATE Status = "PENDING_UPDATE"
+
+ // PENDINGDELETE indicates that the LB is online but configuration changes
+ // are being applied to begin deletion of the service based on a previous
+ // request.
+ PENDINGDELETE Status = "PENDING_DELETE"
+
+ // SUSPENDED indicates that the LB has been taken offline and disabled.
+ SUSPENDED Status = "SUSPENDED"
+
+ // ERROR indicates that the system encountered an error when attempting to
+ // configure the load balancer.
+ ERROR Status = "ERROR"
+
+ // DELETED indicates that the LB has been deleted.
+ DELETED Status = "DELETED"
+)
+
+// Datetime represents the structure of a Created or Updated field.
+type Datetime struct {
+ Time time.Time `mapstructure:"-"`
+}
+
+// LoadBalancer represents a load balancer API resource.
+type LoadBalancer struct {
+ // Human-readable name for the load balancer.
+ Name string
+
+ // The unique ID for the load balancer.
+ ID int
+
+ // Represents the service protocol being load balanced. See Protocol type for
+ // a list of accepted values.
+ // See http://docs.rackspace.com/loadbalancers/api/v1.0/clb-devguide/content/protocols.html
+ // for a full list of supported protocols.
+ Protocol string
+
+ // Defines how traffic should be directed between back-end nodes. The default
+ // algorithm is RANDOM. See Algorithm type for a list of accepted values.
+ Algorithm string
+
+ // The current status of the load balancer.
+ Status Status
+
+ // The number of load balancer nodes.
+ NodeCount int `mapstructure:"nodeCount"`
+
+ // Slice of virtual IPs associated with this load balancer.
+ VIPs []vips.VIP `mapstructure:"virtualIps"`
+
+ // Datetime when the LB was created.
+ Created Datetime
+
+ // Datetime when the LB was created.
+ Updated Datetime
+
+ // Port number for the service you are load balancing.
+ Port int
+
+ // HalfClosed provides the ability for one end of the connection to
+ // terminate its output while still receiving data from the other end. This
+ // is only available on TCP/TCP_CLIENT_FIRST protocols.
+ HalfClosed bool
+
+ // Timeout represents the timeout value between a load balancer and its
+ // nodes. Defaults to 30 seconds with a maximum of 120 seconds.
+ Timeout int
+
+ // The cluster name.
+ Cluster Cluster
+
+ // Nodes shows all the back-end nodes which are associated with the load
+ // balancer. These are the devices which are delivered traffic.
+ Nodes []nodes.Node
+
+ // Current connection logging configuration.
+ ConnectionLogging ConnectionLogging
+
+ // SessionPersistence specifies whether multiple requests from clients are
+ // directed to the same node.
+ SessionPersistence sessions.SessionPersistence
+
+ // ConnectionThrottle specifies a limit on the number of connections per IP
+ // address to help mitigate malicious or abusive traffic to your applications.
+ ConnectionThrottle throttle.ConnectionThrottle
+
+ // The source public and private IP addresses.
+ SourceAddrs SourceAddrs `mapstructure:"sourceAddresses"`
+
+ // Represents the access rules for this particular load balancer. IP addresses
+ // or subnet ranges, depending on their type (ALLOW or DENY), can be permitted
+ // or blocked.
+ AccessList acl.AccessList
+}
+
+// SourceAddrs represents the source public and private IP addresses.
+type SourceAddrs struct {
+ IPv4Public string `json:"ipv4Public" mapstructure:"ipv4Public"`
+ IPv4Private string `json:"ipv4Servicenet" mapstructure:"ipv4Servicenet"`
+ IPv6Public string `json:"ipv6Public" mapstructure:"ipv6Public"`
+ IPv6Private string `json:"ipv6Servicenet" mapstructure:"ipv6Servicenet"`
+}
+
+// ConnectionLogging - temp
+type ConnectionLogging struct {
+ Enabled bool
+}
+
+// Cluster - temp
+type Cluster struct {
+ Name string
+}
+
+// LBPage is the page returned by a pager when traversing over a collection of
+// LBs.
+type LBPage struct {
+ pagination.LinkedPageBase
+}
+
+// IsEmpty checks whether a NetworkPage struct is empty.
+func (p LBPage) IsEmpty() (bool, error) {
+ is, err := ExtractLBs(p)
+ if err != nil {
+ return true, nil
+ }
+ return len(is) == 0, nil
+}
+
+// ExtractLBs accepts a Page struct, specifically a LBPage struct, and extracts
+// the elements into a slice of LoadBalancer structs. In other words, a generic
+// collection is mapped into a relevant slice.
+func ExtractLBs(page pagination.Page) ([]LoadBalancer, error) {
+ var resp struct {
+ LBs []LoadBalancer `mapstructure:"loadBalancers" json:"loadBalancers"`
+ }
+
+ coll := page.(LBPage).Body
+ err := mapstructure.Decode(coll, &resp)
+
+ s := reflect.ValueOf(coll.(map[string]interface{})["loadBalancers"])
+
+ for i := 0; i < s.Len(); i++ {
+ val := (s.Index(i).Interface()).(map[string]interface{})
+
+ ts, err := extractTS(val, "created")
+ if err != nil {
+ return resp.LBs, err
+ }
+ resp.LBs[i].Created.Time = ts
+
+ ts, err = extractTS(val, "updated")
+ if err != nil {
+ return resp.LBs, err
+ }
+ resp.LBs[i].Updated.Time = ts
+ }
+
+ return resp.LBs, err
+}
+
+func extractTS(body map[string]interface{}, key string) (time.Time, error) {
+ val := body[key].(map[string]interface{})
+ return time.Parse(time.RFC3339, val["time"].(string))
+}
+
+type commonResult struct {
+ gophercloud.Result
+}
+
+// Extract interprets any commonResult as a LB, if possible.
+func (r commonResult) Extract() (*LoadBalancer, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var response struct {
+ LB LoadBalancer `mapstructure:"loadBalancer"`
+ }
+
+ err := mapstructure.Decode(r.Body, &response)
+
+ json := r.Body.(map[string]interface{})
+ lb := json["loadBalancer"].(map[string]interface{})
+
+ ts, err := extractTS(lb, "created")
+ if err != nil {
+ return nil, err
+ }
+ response.LB.Created.Time = ts
+
+ ts, err = extractTS(lb, "updated")
+ if err != nil {
+ return nil, err
+ }
+ response.LB.Updated.Time = ts
+
+ return &response.LB, err
+}
+
+// CreateResult represents the result of a create operation.
+type CreateResult struct {
+ commonResult
+}
+
+// DeleteResult represents the result of a delete operation.
+type DeleteResult struct {
+ gophercloud.ErrResult
+}
+
+// UpdateResult represents the result of an update operation.
+type UpdateResult struct {
+ gophercloud.ErrResult
+}
+
+// GetResult represents the result of a get operation.
+type GetResult struct {
+ commonResult
+}
+
+// ProtocolPage is the page returned by a pager when traversing over a
+// collection of LB protocols.
+type ProtocolPage struct {
+ pagination.SinglePageBase
+}
+
+// IsEmpty checks whether a ProtocolPage struct is empty.
+func (p ProtocolPage) IsEmpty() (bool, error) {
+ is, err := ExtractProtocols(p)
+ if err != nil {
+ return true, nil
+ }
+ return len(is) == 0, nil
+}
+
+// ExtractProtocols accepts a Page struct, specifically a ProtocolPage struct,
+// and extracts the elements into a slice of Protocol structs. In other
+// words, a generic collection is mapped into a relevant slice.
+func ExtractProtocols(page pagination.Page) ([]Protocol, error) {
+ var resp struct {
+ Protocols []Protocol `mapstructure:"protocols" json:"protocols"`
+ }
+ err := mapstructure.Decode(page.(ProtocolPage).Body, &resp)
+ return resp.Protocols, err
+}
+
+// AlgorithmPage is the page returned by a pager when traversing over a
+// collection of LB algorithms.
+type AlgorithmPage struct {
+ pagination.SinglePageBase
+}
+
+// IsEmpty checks whether an AlgorithmPage struct is empty.
+func (p AlgorithmPage) IsEmpty() (bool, error) {
+ is, err := ExtractAlgorithms(p)
+ if err != nil {
+ return true, nil
+ }
+ return len(is) == 0, nil
+}
+
+// ExtractAlgorithms accepts a Page struct, specifically an AlgorithmPage struct,
+// and extracts the elements into a slice of Algorithm structs. In other
+// words, a generic collection is mapped into a relevant slice.
+func ExtractAlgorithms(page pagination.Page) ([]Algorithm, error) {
+ var resp struct {
+ Algorithms []Algorithm `mapstructure:"algorithms" json:"algorithms"`
+ }
+ err := mapstructure.Decode(page.(AlgorithmPage).Body, &resp)
+ return resp.Algorithms, err
+}
+
+// ErrorPage represents the HTML file that is shown to an end user who is
+// attempting to access a load balancer node that is offline/unavailable.
+//
+// During provisioning, every load balancer is configured with a default error
+// page that gets displayed when traffic is requested for an offline node.
+//
+// You can add a single custom error page with an HTTP-based protocol to a load
+// balancer. Page updates override existing content. If a custom error page is
+// deleted, or the load balancer is changed to a non-HTTP protocol, the default
+// error page is restored.
+type ErrorPage struct {
+ Content string
+}
+
+// ErrorPageResult represents the result of an error page operation -
+// specifically getting or creating one.
+type ErrorPageResult struct {
+ gophercloud.Result
+}
+
+// Extract interprets any commonResult as an ErrorPage, if possible.
+func (r ErrorPageResult) Extract() (*ErrorPage, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var response struct {
+ ErrorPage ErrorPage `mapstructure:"errorpage"`
+ }
+
+ err := mapstructure.Decode(r.Body, &response)
+
+ return &response.ErrorPage, err
+}
+
+// Stats represents all the key information about a load balancer's usage.
+type Stats struct {
+ // The number of connections closed by this load balancer because its
+ // ConnectTimeout interval was exceeded.
+ ConnectTimeout int `mapstructure:"connectTimeOut"`
+
+ // The number of transaction or protocol errors for this load balancer.
+ ConnectError int
+
+ // Number of connection failures for this load balancer.
+ ConnectFailure int
+
+ // Number of connections closed by this load balancer because its Timeout
+ // interval was exceeded.
+ DataTimedOut int
+
+ // Number of connections closed by this load balancer because the
+ // 'keepalive_timeout' interval was exceeded.
+ KeepAliveTimedOut int
+
+ // The maximum number of simultaneous TCP connections this load balancer has
+ // processed at any one time.
+ MaxConnections int `mapstructure:"maxConn"`
+
+ // Number of simultaneous connections active at the time of the request.
+ CurrentConnections int `mapstructure:"currentConn"`
+
+ // Number of SSL connections closed by this load balancer because the
+ // ConnectTimeout interval was exceeded.
+ SSLConnectTimeout int `mapstructure:"connectTimeOutSsl"`
+
+ // Number of SSL transaction or protocol erros in this load balancer.
+ SSLConnectError int `mapstructure:"connectErrorSsl"`
+
+ // Number of SSL connection failures in this load balancer.
+ SSLConnectFailure int `mapstructure:"connectFailureSsl"`
+
+ // Number of SSL connections closed by this load balancer because the
+ // Timeout interval was exceeded.
+ SSLDataTimedOut int `mapstructure:"dataTimedOutSsl"`
+
+ // Number of SSL connections closed by this load balancer because the
+ // 'keepalive_timeout' interval was exceeded.
+ SSLKeepAliveTimedOut int `mapstructure:"keepAliveTimedOutSsl"`
+
+ // Maximum number of simultaneous SSL connections this load balancer has
+ // processed at any one time.
+ SSLMaxConnections int `mapstructure:"maxConnSsl"`
+
+ // Number of simultaneous SSL connections active at the time of the request.
+ SSLCurrentConnections int `mapstructure:"currentConnSsl"`
+}
+
+// StatsResult represents the result of a Stats operation.
+type StatsResult struct {
+ gophercloud.Result
+}
+
+// Extract interprets any commonResult as a Stats struct, if possible.
+func (r StatsResult) Extract() (*Stats, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+ res := &Stats{}
+ err := mapstructure.Decode(r.Body, res)
+ return res, err
+}
diff --git a/rackspace/lb/v1/lbs/urls.go b/rackspace/lb/v1/lbs/urls.go
new file mode 100644
index 0000000..471a86b
--- /dev/null
+++ b/rackspace/lb/v1/lbs/urls.go
@@ -0,0 +1,49 @@
+package lbs
+
+import (
+ "strconv"
+
+ "github.com/rackspace/gophercloud"
+)
+
+const (
+ path = "loadbalancers"
+ protocolsPath = "protocols"
+ algorithmsPath = "algorithms"
+ logPath = "connectionlogging"
+ epPath = "errorpage"
+ stPath = "stats"
+ cachePath = "contentcaching"
+)
+
+func resourceURL(c *gophercloud.ServiceClient, id int) string {
+ return c.ServiceURL(path, strconv.Itoa(id))
+}
+
+func rootURL(c *gophercloud.ServiceClient) string {
+ return c.ServiceURL(path)
+}
+
+func protocolsURL(c *gophercloud.ServiceClient) string {
+ return c.ServiceURL(path, protocolsPath)
+}
+
+func algorithmsURL(c *gophercloud.ServiceClient) string {
+ return c.ServiceURL(path, algorithmsPath)
+}
+
+func loggingURL(c *gophercloud.ServiceClient, id int) string {
+ return c.ServiceURL(path, strconv.Itoa(id), logPath)
+}
+
+func errorPageURL(c *gophercloud.ServiceClient, id int) string {
+ return c.ServiceURL(path, strconv.Itoa(id), epPath)
+}
+
+func statsURL(c *gophercloud.ServiceClient, id int) string {
+ return c.ServiceURL(path, strconv.Itoa(id), stPath)
+}
+
+func cacheURL(c *gophercloud.ServiceClient, id int) string {
+ return c.ServiceURL(path, strconv.Itoa(id), cachePath)
+}
diff --git a/rackspace/lb/v1/monitors/doc.go b/rackspace/lb/v1/monitors/doc.go
new file mode 100644
index 0000000..2c5be75
--- /dev/null
+++ b/rackspace/lb/v1/monitors/doc.go
@@ -0,0 +1,21 @@
+/*
+Package monitors provides information and interaction with the Health Monitor
+API resource for the Rackspace Cloud Load Balancer service.
+
+The load balancing service includes a health monitoring resource that
+periodically checks your back-end nodes to ensure they are responding correctly.
+If a node does not respond, it is removed from rotation until the health monitor
+determines that the node is functional. In addition to being performed
+periodically, a health check also executes against every new node that is
+added, to ensure that the node is operating properly before allowing it to
+service traffic. Only one health monitor is allowed to be enabled on a load
+balancer at a time.
+
+As part of a good strategy for monitoring connections, secondary nodes should
+also be created which provide failover for effectively routing traffic in case
+the primary node fails. This is an additional feature that ensures that you
+remain up in case your primary node fails.
+
+There are three types of health monitor: CONNECT, HTTP and HTTPS.
+*/
+package monitors
diff --git a/rackspace/lb/v1/monitors/fixtures.go b/rackspace/lb/v1/monitors/fixtures.go
new file mode 100644
index 0000000..a565abc
--- /dev/null
+++ b/rackspace/lb/v1/monitors/fixtures.go
@@ -0,0 +1,87 @@
+package monitors
+
+import (
+ "fmt"
+ "net/http"
+ "strconv"
+ "testing"
+
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func _rootURL(lbID int) string {
+ return "/loadbalancers/" + strconv.Itoa(lbID) + "/healthmonitor"
+}
+
+func mockGetResponse(t *testing.T, lbID int) {
+ th.Mux.HandleFunc(_rootURL(lbID), 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, `
+{
+ "healthMonitor": {
+ "type": "CONNECT",
+ "delay": 10,
+ "timeout": 10,
+ "attemptsBeforeDeactivation": 3
+ }
+}
+ `)
+ })
+}
+
+func mockUpdateConnectResponse(t *testing.T, lbID int) {
+ th.Mux.HandleFunc(_rootURL(lbID), 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, `
+{
+ "healthMonitor": {
+ "type": "CONNECT",
+ "delay": 10,
+ "timeout": 10,
+ "attemptsBeforeDeactivation": 3
+ }
+}
+ `)
+
+ w.WriteHeader(http.StatusAccepted)
+ })
+}
+
+func mockUpdateHTTPResponse(t *testing.T, lbID int) {
+ th.Mux.HandleFunc(_rootURL(lbID), 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, `
+{
+ "healthMonitor": {
+ "attemptsBeforeDeactivation": 3,
+ "bodyRegex": "{regex}",
+ "delay": 10,
+ "path": "/foo",
+ "statusRegex": "200",
+ "timeout": 10,
+ "type": "HTTPS"
+ }
+}
+ `)
+
+ w.WriteHeader(http.StatusAccepted)
+ })
+}
+
+func mockDeleteResponse(t *testing.T, lbID int) {
+ th.Mux.HandleFunc(_rootURL(lbID), func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "DELETE")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ w.WriteHeader(http.StatusAccepted)
+ })
+}
diff --git a/rackspace/lb/v1/monitors/requests.go b/rackspace/lb/v1/monitors/requests.go
new file mode 100644
index 0000000..cfc35d2
--- /dev/null
+++ b/rackspace/lb/v1/monitors/requests.go
@@ -0,0 +1,178 @@
+package monitors
+
+import (
+ "errors"
+
+ "github.com/racker/perigee"
+
+ "github.com/rackspace/gophercloud"
+)
+
+var (
+ errAttemptLimit = errors.New("AttemptLimit field must be an int greater than 1 and less than 10")
+ errDelay = errors.New("Delay field must be an int greater than 1 and less than 10")
+ errTimeout = errors.New("Timeout field must be an int greater than 1 and less than 10")
+)
+
+// UpdateOptsBuilder is the interface options structs have to satisfy in order
+// to be used in the main Update operation in this package.
+type UpdateOptsBuilder interface {
+ ToMonitorUpdateMap() (map[string]interface{}, error)
+}
+
+// UpdateConnectMonitorOpts represents the options needed to update a CONNECT
+// monitor.
+type UpdateConnectMonitorOpts struct {
+ // Required - number of permissible monitor failures before removing a node
+ // from rotation. Must be a number between 1 and 10.
+ AttemptLimit int
+
+ // Required - the minimum number of seconds to wait before executing the
+ // health monitor. Must be a number between 1 and 3600.
+ Delay int
+
+ // Required - maximum number of seconds to wait for a connection to be
+ // established before timing out. Must be a number between 1 and 300.
+ Timeout int
+}
+
+// ToMonitorUpdateMap produces a map for updating CONNECT monitors.
+func (opts UpdateConnectMonitorOpts) ToMonitorUpdateMap() (map[string]interface{}, error) {
+ type m map[string]interface{}
+
+ if !gophercloud.IntWithinRange(opts.AttemptLimit, 1, 10) {
+ return m{}, errAttemptLimit
+ }
+ if !gophercloud.IntWithinRange(opts.Delay, 1, 3600) {
+ return m{}, errDelay
+ }
+ if !gophercloud.IntWithinRange(opts.Timeout, 1, 300) {
+ return m{}, errTimeout
+ }
+
+ return m{"healthMonitor": m{
+ "attemptsBeforeDeactivation": opts.AttemptLimit,
+ "delay": opts.Delay,
+ "timeout": opts.Timeout,
+ "type": CONNECT,
+ }}, nil
+}
+
+// UpdateHTTPMonitorOpts represents the options needed to update a HTTP monitor.
+type UpdateHTTPMonitorOpts struct {
+ // Required - number of permissible monitor failures before removing a node
+ // from rotation. Must be a number between 1 and 10.
+ AttemptLimit int `mapstructure:"attemptsBeforeDeactivation"`
+
+ // Required - the minimum number of seconds to wait before executing the
+ // health monitor. Must be a number between 1 and 3600.
+ Delay int
+
+ // Required - maximum number of seconds to wait for a connection to be
+ // established before timing out. Must be a number between 1 and 300.
+ Timeout int
+
+ // Required - a regular expression that will be used to evaluate the contents
+ // of the body of the response.
+ BodyRegex string
+
+ // Required - the HTTP path that will be used in the sample request.
+ Path string
+
+ // Required - a regular expression that will be used to evaluate the HTTP
+ // status code returned in the response.
+ StatusRegex string
+
+ // Optional - the name of a host for which the health monitors will check.
+ HostHeader string
+
+ // Required - either HTTP or HTTPS
+ Type Type
+}
+
+// ToMonitorUpdateMap produces a map for updating HTTP(S) monitors.
+func (opts UpdateHTTPMonitorOpts) ToMonitorUpdateMap() (map[string]interface{}, error) {
+ type m map[string]interface{}
+
+ if !gophercloud.IntWithinRange(opts.AttemptLimit, 1, 10) {
+ return m{}, errAttemptLimit
+ }
+ if !gophercloud.IntWithinRange(opts.Delay, 1, 3600) {
+ return m{}, errDelay
+ }
+ if !gophercloud.IntWithinRange(opts.Timeout, 1, 300) {
+ return m{}, errTimeout
+ }
+ if opts.Type != HTTP && opts.Type != HTTPS {
+ return m{}, errors.New("Type must either by HTTP or HTTPS")
+ }
+ if opts.BodyRegex == "" {
+ return m{}, errors.New("BodyRegex is a required field")
+ }
+ if opts.Path == "" {
+ return m{}, errors.New("Path is a required field")
+ }
+ if opts.StatusRegex == "" {
+ return m{}, errors.New("StatusRegex is a required field")
+ }
+
+ json := m{
+ "attemptsBeforeDeactivation": opts.AttemptLimit,
+ "delay": opts.Delay,
+ "timeout": opts.Timeout,
+ "type": opts.Type,
+ "bodyRegex": opts.BodyRegex,
+ "path": opts.Path,
+ "statusRegex": opts.StatusRegex,
+ }
+
+ if opts.HostHeader != "" {
+ json["hostHeader"] = opts.HostHeader
+ }
+
+ return m{"healthMonitor": json}, nil
+}
+
+// Update is the operation responsible for updating a health monitor.
+func Update(c *gophercloud.ServiceClient, id int, opts UpdateOptsBuilder) UpdateResult {
+ var res UpdateResult
+
+ reqBody, err := opts.ToMonitorUpdateMap()
+ if err != nil {
+ res.Err = err
+ return res
+ }
+
+ _, res.Err = perigee.Request("PUT", rootURL(c, id), perigee.Options{
+ MoreHeaders: c.AuthenticatedHeaders(),
+ ReqBody: &reqBody,
+ OkCodes: []int{202},
+ })
+
+ return res
+}
+
+// Get is the operation responsible for showing details of a health monitor.
+func Get(c *gophercloud.ServiceClient, id int) GetResult {
+ var res GetResult
+
+ _, res.Err = perigee.Request("GET", rootURL(c, id), perigee.Options{
+ MoreHeaders: c.AuthenticatedHeaders(),
+ Results: &res.Body,
+ OkCodes: []int{200},
+ })
+
+ return res
+}
+
+// Delete is the operation responsible for deleting a health monitor.
+func Delete(c *gophercloud.ServiceClient, id int) DeleteResult {
+ var res DeleteResult
+
+ _, res.Err = perigee.Request("DELETE", rootURL(c, id), perigee.Options{
+ MoreHeaders: c.AuthenticatedHeaders(),
+ OkCodes: []int{202},
+ })
+
+ return res
+}
diff --git a/rackspace/lb/v1/monitors/requests_test.go b/rackspace/lb/v1/monitors/requests_test.go
new file mode 100644
index 0000000..76a60db
--- /dev/null
+++ b/rackspace/lb/v1/monitors/requests_test.go
@@ -0,0 +1,75 @@
+package monitors
+
+import (
+ "testing"
+
+ th "github.com/rackspace/gophercloud/testhelper"
+ "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+const lbID = 12345
+
+func TestUpdateCONNECT(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockUpdateConnectResponse(t, lbID)
+
+ opts := UpdateConnectMonitorOpts{
+ AttemptLimit: 3,
+ Delay: 10,
+ Timeout: 10,
+ }
+
+ err := Update(client.ServiceClient(), lbID, opts).ExtractErr()
+ th.AssertNoErr(t, err)
+}
+
+func TestUpdateHTTP(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockUpdateHTTPResponse(t, lbID)
+
+ opts := UpdateHTTPMonitorOpts{
+ AttemptLimit: 3,
+ Delay: 10,
+ Timeout: 10,
+ BodyRegex: "{regex}",
+ Path: "/foo",
+ StatusRegex: "200",
+ Type: HTTPS,
+ }
+
+ err := Update(client.ServiceClient(), lbID, opts).ExtractErr()
+ th.AssertNoErr(t, err)
+}
+
+func TestGet(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockGetResponse(t, lbID)
+
+ m, err := Get(client.ServiceClient(), lbID).Extract()
+ th.AssertNoErr(t, err)
+
+ expected := &Monitor{
+ Type: CONNECT,
+ Delay: 10,
+ Timeout: 10,
+ AttemptLimit: 3,
+ }
+
+ th.AssertDeepEquals(t, expected, m)
+}
+
+func TestDelete(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockDeleteResponse(t, lbID)
+
+ err := Delete(client.ServiceClient(), lbID).ExtractErr()
+ th.AssertNoErr(t, err)
+}
diff --git a/rackspace/lb/v1/monitors/results.go b/rackspace/lb/v1/monitors/results.go
new file mode 100644
index 0000000..eec556f
--- /dev/null
+++ b/rackspace/lb/v1/monitors/results.go
@@ -0,0 +1,90 @@
+package monitors
+
+import (
+ "github.com/mitchellh/mapstructure"
+
+ "github.com/rackspace/gophercloud"
+)
+
+// Type represents the type of Monitor.
+type Type string
+
+// Useful constants.
+const (
+ CONNECT Type = "CONNECT"
+ HTTP Type = "HTTP"
+ HTTPS Type = "HTTPS"
+)
+
+// Monitor represents a health monitor API resource. A monitor comes in three
+// forms: CONNECT, HTTP or HTTPS.
+//
+// A CONNECT monitor establishes a basic connection to each node on its defined
+// port to ensure that the service is listening properly. The connect monitor
+// is the most basic type of health check and does no post-processing or
+// protocol-specific health checks.
+//
+// HTTP and HTTPS health monitors are generally considered more intelligent and
+// powerful than CONNECT. It is capable of processing an HTTP or HTTPS response
+// to determine the condition of a node. It supports the same basic properties
+// as CONNECT and includes additional attributes that are used to evaluate the
+// HTTP response.
+type Monitor struct {
+ // Number of permissible monitor failures before removing a node from
+ // rotation.
+ AttemptLimit int `mapstructure:"attemptsBeforeDeactivation"`
+
+ // The minimum number of seconds to wait before executing the health monitor.
+ Delay int
+
+ // Maximum number of seconds to wait for a connection to be established
+ // before timing out.
+ Timeout int
+
+ // Type of the health monitor.
+ Type Type
+
+ // A regular expression that will be used to evaluate the contents of the
+ // body of the response.
+ BodyRegex string
+
+ // The name of a host for which the health monitors will check.
+ HostHeader string
+
+ // The HTTP path that will be used in the sample request.
+ Path string
+
+ // A regular expression that will be used to evaluate the HTTP status code
+ // returned in the response.
+ StatusRegex string
+}
+
+// UpdateResult represents the result of an Update operation.
+type UpdateResult struct {
+ gophercloud.ErrResult
+}
+
+// GetResult represents the result of a Get operation.
+type GetResult struct {
+ gophercloud.Result
+}
+
+// DeleteResult represents the result of an Delete operation.
+type DeleteResult struct {
+ gophercloud.ErrResult
+}
+
+// Extract interprets any GetResult as a Monitor.
+func (r GetResult) Extract() (*Monitor, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var response struct {
+ M Monitor `mapstructure:"healthMonitor"`
+ }
+
+ err := mapstructure.Decode(r.Body, &response)
+
+ return &response.M, err
+}
diff --git a/rackspace/lb/v1/monitors/urls.go b/rackspace/lb/v1/monitors/urls.go
new file mode 100644
index 0000000..0a1e6df
--- /dev/null
+++ b/rackspace/lb/v1/monitors/urls.go
@@ -0,0 +1,16 @@
+package monitors
+
+import (
+ "strconv"
+
+ "github.com/rackspace/gophercloud"
+)
+
+const (
+ path = "loadbalancers"
+ monitorPath = "healthmonitor"
+)
+
+func rootURL(c *gophercloud.ServiceClient, lbID int) string {
+ return c.ServiceURL(path, strconv.Itoa(lbID), monitorPath)
+}
diff --git a/rackspace/lb/v1/nodes/doc.go b/rackspace/lb/v1/nodes/doc.go
new file mode 100644
index 0000000..49c4318
--- /dev/null
+++ b/rackspace/lb/v1/nodes/doc.go
@@ -0,0 +1,35 @@
+/*
+Package nodes provides information and interaction with the Node API resource
+for the Rackspace Cloud Load Balancer service.
+
+Nodes are responsible for servicing the requests received through the load
+balancer's virtual IP. A node is usually a virtual machine. By default, the
+load balancer employs a basic health check that ensures the node is listening
+on its defined port. The node is checked at the time of addition and at regular
+intervals as defined by the load balancer's health check configuration. If a
+back-end node is not listening on its port, or does not meet the conditions of
+the defined check, then connections will not be forwarded to the node, and its
+status is changed to OFFLINE. Only nodes that are in an ONLINE status receive
+and can service traffic from the load balancer.
+
+All nodes have an associated status that indicates whether the node is
+ONLINE, OFFLINE, or DRAINING. Only nodes that are in ONLINE status can receive
+and service traffic from the load balancer. The OFFLINE status represents a
+node that cannot accept or service traffic. A node in DRAINING status
+represents a node that stops the traffic manager from sending any additional
+new connections to the node, but honors established sessions. If the traffic
+manager receives a request and session persistence requires that the node is
+used, the traffic manager uses it. The status is determined by the passive or
+active health monitors.
+
+If the WEIGHTED_ROUND_ROBIN load balancer algorithm mode is selected, then the
+caller should assign the relevant weights to the node as part of the weight
+attribute of the node element. When the algorithm of the load balancer is
+changed to WEIGHTED_ROUND_ROBIN and the nodes do not already have an assigned
+weight, the service automatically sets the weight to 1 for all nodes.
+
+One or more secondary nodes can be added to a specified load balancer so that
+if all the primary nodes fail, traffic can be redirected to secondary nodes.
+The type attribute allows configuring the node as either PRIMARY or SECONDARY.
+*/
+package nodes
diff --git a/rackspace/lb/v1/nodes/fixtures.go b/rackspace/lb/v1/nodes/fixtures.go
new file mode 100644
index 0000000..7c85945
--- /dev/null
+++ b/rackspace/lb/v1/nodes/fixtures.go
@@ -0,0 +1,207 @@
+package nodes
+
+import (
+ "fmt"
+ "net/http"
+ "strconv"
+ "testing"
+
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func _rootURL(lbID int) string {
+ return "/loadbalancers/" + strconv.Itoa(lbID) + "/nodes"
+}
+
+func _nodeURL(lbID, nodeID int) string {
+ return _rootURL(lbID) + "/" + strconv.Itoa(nodeID)
+}
+
+func mockListResponse(t *testing.T, lbID int) {
+ th.Mux.HandleFunc(_rootURL(lbID), 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, `
+{
+ "nodes": [
+ {
+ "id": 410,
+ "address": "10.1.1.1",
+ "port": 80,
+ "condition": "ENABLED",
+ "status": "ONLINE",
+ "weight": 3,
+ "type": "PRIMARY"
+ },
+ {
+ "id": 411,
+ "address": "10.1.1.2",
+ "port": 80,
+ "condition": "ENABLED",
+ "status": "ONLINE",
+ "weight": 8,
+ "type": "SECONDARY"
+ }
+ ]
+}
+ `)
+ })
+}
+
+func mockCreateResponse(t *testing.T, lbID int) {
+ th.Mux.HandleFunc(_rootURL(lbID), func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "POST")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+ th.TestJSONRequest(t, r, `
+{
+ "nodes": [
+ {
+ "address": "10.2.2.3",
+ "port": 80,
+ "condition": "ENABLED",
+ "type": "PRIMARY"
+ },
+ {
+ "address": "10.2.2.4",
+ "port": 81,
+ "condition": "ENABLED",
+ "type": "SECONDARY"
+ }
+ ]
+}
+ `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusAccepted)
+
+ fmt.Fprintf(w, `
+{
+ "nodes": [
+ {
+ "address": "10.2.2.3",
+ "id": 185,
+ "port": 80,
+ "status": "ONLINE",
+ "condition": "ENABLED",
+ "weight": 1,
+ "type": "PRIMARY"
+ },
+ {
+ "address": "10.2.2.4",
+ "id": 186,
+ "port": 81,
+ "status": "ONLINE",
+ "condition": "ENABLED",
+ "weight": 1,
+ "type": "SECONDARY"
+ }
+ ]
+}
+ `)
+ })
+}
+
+func mockBatchDeleteResponse(t *testing.T, lbID int, ids []int) {
+ th.Mux.HandleFunc(_rootURL(lbID), func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "DELETE")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+ r.ParseForm()
+
+ for k, v := range ids {
+ fids := r.Form["id"]
+ th.AssertEquals(t, strconv.Itoa(v), fids[k])
+ }
+
+ w.WriteHeader(http.StatusAccepted)
+ })
+}
+
+func mockDeleteResponse(t *testing.T, lbID, nodeID int) {
+ th.Mux.HandleFunc(_nodeURL(lbID, nodeID), func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "DELETE")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ w.WriteHeader(http.StatusAccepted)
+ })
+}
+
+func mockGetResponse(t *testing.T, lbID, nodeID int) {
+ th.Mux.HandleFunc(_nodeURL(lbID, nodeID), 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, `
+{
+ "node": {
+ "id": 410,
+ "address": "10.1.1.1",
+ "port": 80,
+ "condition": "ENABLED",
+ "status": "ONLINE",
+ "weight": 12,
+ "type": "PRIMARY"
+ }
+}
+ `)
+ })
+}
+
+func mockUpdateResponse(t *testing.T, lbID, nodeID int) {
+ th.Mux.HandleFunc(_nodeURL(lbID, nodeID), 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, `
+{
+ "node": {
+ "condition": "DRAINING",
+ "weight": 10,
+ "type": "SECONDARY"
+ }
+}
+ `)
+
+ w.WriteHeader(http.StatusAccepted)
+ })
+}
+
+func mockListEventsResponse(t *testing.T, lbID int) {
+ th.Mux.HandleFunc(_rootURL(lbID)+"/events", 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, `
+{
+ "nodeServiceEvents": [
+ {
+ "detailedMessage": "Node is ok",
+ "nodeId": 373,
+ "id": 7,
+ "type": "UPDATE_NODE",
+ "description": "Node '373' status changed to 'ONLINE' for load balancer '323'",
+ "category": "UPDATE",
+ "severity": "INFO",
+ "relativeUri": "/406271/loadbalancers/323/nodes/373/events",
+ "accountId": 406271,
+ "loadbalancerId": 323,
+ "title": "Node Status Updated",
+ "author": "Rackspace Cloud",
+ "created": "10-30-2012 10:18:23"
+ }
+ ]
+}
+`)
+ })
+}
diff --git a/rackspace/lb/v1/nodes/requests.go b/rackspace/lb/v1/nodes/requests.go
new file mode 100644
index 0000000..77894aa
--- /dev/null
+++ b/rackspace/lb/v1/nodes/requests.go
@@ -0,0 +1,279 @@
+package nodes
+
+import (
+ "errors"
+ "fmt"
+
+ "github.com/racker/perigee"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// List is the operation responsible for returning a paginated collection of
+// load balancer nodes. It requires the node ID, its parent load balancer ID,
+// and optional limit integer (passed in either as a pointer or a nil poitner).
+func List(client *gophercloud.ServiceClient, loadBalancerID int, limit *int) pagination.Pager {
+ url := rootURL(client, loadBalancerID)
+ if limit != nil {
+ url += fmt.Sprintf("?limit=%d", limit)
+ }
+
+ return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page {
+ return NodePage{pagination.SinglePageBase(r)}
+ })
+}
+
+// CreateOptsBuilder is the interface responsible for generating the JSON
+// for a Create operation.
+type CreateOptsBuilder interface {
+ ToNodeCreateMap() (map[string]interface{}, error)
+}
+
+// CreateOpts is a slice of CreateOpt structs, that allow the user to create
+// multiple nodes in a single operation (one node per CreateOpt).
+type CreateOpts []CreateOpt
+
+// CreateOpt represents the options to create a single node.
+type CreateOpt struct {
+ // Required - the IP address or CIDR for this back-end node. It can either be
+ // a private IP (ServiceNet) or a public IP.
+ Address string
+
+ // Optional - the port on which traffic is sent and received.
+ Port int
+
+ // Optional - the condition of the node. See the consts in Results.go.
+ Condition Condition
+
+ // Optional - the type of the node. See the consts in Results.go.
+ Type Type
+
+ // Optional - a pointer to an integer between 0 and 100.
+ Weight *int
+}
+
+func validateWeight(weight *int) error {
+ if weight != nil && (*weight > 100 || *weight < 0) {
+ return errors.New("Weight must be a valid int between 0 and 100")
+ }
+ return nil
+}
+
+// ToNodeCreateMap converts a slice of options into a map that can be used for
+// the JSON.
+func (opts CreateOpts) ToNodeCreateMap() (map[string]interface{}, error) {
+ type nodeMap map[string]interface{}
+ nodes := []nodeMap{}
+
+ for k, v := range opts {
+ if v.Address == "" {
+ return nodeMap{}, fmt.Errorf("ID is a required attribute, none provided for %d CreateOpt element", k)
+ }
+ if weightErr := validateWeight(v.Weight); weightErr != nil {
+ return nodeMap{}, weightErr
+ }
+
+ node := make(map[string]interface{})
+ node["address"] = v.Address
+
+ if v.Port > 0 {
+ node["port"] = v.Port
+ }
+ if v.Condition != "" {
+ node["condition"] = v.Condition
+ }
+ if v.Type != "" {
+ node["type"] = v.Type
+ }
+ if v.Weight != nil {
+ node["weight"] = &v.Weight
+ }
+
+ nodes = append(nodes, node)
+ }
+
+ return nodeMap{"nodes": nodes}, nil
+}
+
+// Create is the operation responsible for creating a new node on a load
+// balancer. Since every load balancer exists in both ServiceNet and the public
+// Internet, both private and public IP addresses can be used for nodes.
+//
+// If nodes need time to boot up services before they become operational, you
+// can temporarily prevent traffic from being sent to that node by setting the
+// Condition field to DRAINING. Health checks will still be performed; but once
+// your node is ready, you can update its condition to ENABLED and have it
+// handle traffic.
+func Create(client *gophercloud.ServiceClient, loadBalancerID int, opts CreateOptsBuilder) CreateResult {
+ var res CreateResult
+
+ reqBody, err := opts.ToNodeCreateMap()
+ if err != nil {
+ res.Err = err
+ return res
+ }
+
+ resp, err := perigee.Request("POST", rootURL(client, loadBalancerID), perigee.Options{
+ MoreHeaders: client.AuthenticatedHeaders(),
+ ReqBody: &reqBody,
+ Results: &res.Body,
+ OkCodes: []int{202},
+ })
+ if err != nil {
+ res.Err = err
+ return res
+ }
+
+ pr, err := pagination.PageResultFrom(resp.HttpResponse)
+ if err != nil {
+ res.Err = err
+ return res
+ }
+
+ return CreateResult{pagination.SinglePageBase(pr)}
+}
+
+// BulkDelete is the operation responsible for batch deleting multiple nodes in
+// a single operation. It accepts a slice of integer IDs and will remove them
+// from the load balancer. The maximum limit is 10 node removals at once.
+func BulkDelete(c *gophercloud.ServiceClient, loadBalancerID int, nodeIDs []int) DeleteResult {
+ var res DeleteResult
+
+ if len(nodeIDs) > 10 || len(nodeIDs) == 0 {
+ res.Err = errors.New("You must provide a minimum of 1 and a maximum of 10 node IDs")
+ return res
+ }
+
+ url := rootURL(c, loadBalancerID)
+ url += gophercloud.IDSliceToQueryString("id", nodeIDs)
+
+ _, res.Err = perigee.Request("DELETE", url, perigee.Options{
+ MoreHeaders: c.AuthenticatedHeaders(),
+ OkCodes: []int{202},
+ })
+
+ return res
+}
+
+// Get is the operation responsible for showing details for a single node.
+func Get(c *gophercloud.ServiceClient, lbID, nodeID int) GetResult {
+ var res GetResult
+
+ _, res.Err = perigee.Request("GET", resourceURL(c, lbID, nodeID), perigee.Options{
+ MoreHeaders: c.AuthenticatedHeaders(),
+ Results: &res.Body,
+ OkCodes: []int{200},
+ })
+
+ return res
+}
+
+// UpdateOptsBuilder represents a type that can be converted into a JSON-like
+// map structure.
+type UpdateOptsBuilder interface {
+ ToNodeUpdateMap() (map[string]interface{}, error)
+}
+
+// UpdateOpts represent the options for updating an existing node.
+type UpdateOpts struct {
+ // Optional - the condition of the node. See the consts in Results.go.
+ Condition Condition
+
+ // Optional - the type of the node. See the consts in Results.go.
+ Type Type
+
+ // Optional - a pointer to an integer between 0 and 100.
+ Weight *int
+}
+
+// ToNodeUpdateMap converts an options struct into a JSON-like map.
+func (opts UpdateOpts) ToNodeUpdateMap() (map[string]interface{}, error) {
+ node := make(map[string]interface{})
+
+ if opts.Condition != "" {
+ node["condition"] = opts.Condition
+ }
+ if opts.Weight != nil {
+ if weightErr := validateWeight(opts.Weight); weightErr != nil {
+ return node, weightErr
+ }
+ node["weight"] = &opts.Weight
+ }
+ if opts.Type != "" {
+ node["type"] = opts.Type
+ }
+
+ return map[string]interface{}{"node": node}, nil
+}
+
+// Update is the operation responsible for updating an existing node. A node's
+// IP, port, and status are immutable attributes and cannot be modified.
+func Update(c *gophercloud.ServiceClient, lbID, nodeID int, opts UpdateOptsBuilder) UpdateResult {
+ var res UpdateResult
+
+ reqBody, err := opts.ToNodeUpdateMap()
+ if err != nil {
+ res.Err = err
+ return res
+ }
+
+ _, res.Err = perigee.Request("PUT", resourceURL(c, lbID, nodeID), perigee.Options{
+ MoreHeaders: c.AuthenticatedHeaders(),
+ ReqBody: &reqBody,
+ OkCodes: []int{202},
+ })
+
+ return res
+}
+
+// Delete is the operation responsible for permanently deleting a node.
+func Delete(c *gophercloud.ServiceClient, lbID, nodeID int) DeleteResult {
+ var res DeleteResult
+ _, res.Err = perigee.Request("DELETE", resourceURL(c, lbID, nodeID), perigee.Options{
+ MoreHeaders: c.AuthenticatedHeaders(),
+ OkCodes: []int{202},
+ })
+ return res
+}
+
+// ListEventsOptsBuilder allows extensions to add additional parameters to the
+// List request.
+type ListEventsOptsBuilder interface {
+ ToEventsListQuery() (string, error)
+}
+
+// ListEventsOpts allows the filtering and sorting of paginated collections through
+// the API.
+type ListEventsOpts struct {
+ Marker string `q:"marker"`
+ Limit int `q:"limit"`
+}
+
+// ToEventsListQuery formats a ListOpts into a query string.
+func (opts ListEventsOpts) ToEventsListQuery() (string, error) {
+ q, err := gophercloud.BuildQueryString(opts)
+ if err != nil {
+ return "", err
+ }
+ return q.String(), nil
+}
+
+// ListEvents is the operation responsible for listing all the events
+// associated with the activity between the node and the load balancer. The
+// events report errors found with the node. The detailedMessage provides the
+// detailed reason for the error.
+func ListEvents(client *gophercloud.ServiceClient, loadBalancerID int, opts ListEventsOptsBuilder) pagination.Pager {
+ url := eventsURL(client, loadBalancerID)
+
+ if opts != nil {
+ query, err := opts.ToEventsListQuery()
+ if err != nil {
+ return pagination.Pager{Err: err}
+ }
+ url += query
+ }
+
+ return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page {
+ return NodeEventPage{pagination.SinglePageBase(r)}
+ })
+}
diff --git a/rackspace/lb/v1/nodes/requests_test.go b/rackspace/lb/v1/nodes/requests_test.go
new file mode 100644
index 0000000..003d347
--- /dev/null
+++ b/rackspace/lb/v1/nodes/requests_test.go
@@ -0,0 +1,211 @@
+package nodes
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+ "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+const (
+ lbID = 12345
+ nodeID = 67890
+ nodeID2 = 67891
+)
+
+func TestList(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockListResponse(t, lbID)
+
+ count := 0
+
+ err := List(client.ServiceClient(), lbID, nil).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractNodes(page)
+ th.AssertNoErr(t, err)
+
+ expected := []Node{
+ Node{
+ ID: 410,
+ Address: "10.1.1.1",
+ Port: 80,
+ Condition: ENABLED,
+ Status: ONLINE,
+ Weight: 3,
+ Type: PRIMARY,
+ },
+ Node{
+ ID: 411,
+ Address: "10.1.1.2",
+ Port: 80,
+ Condition: ENABLED,
+ Status: ONLINE,
+ Weight: 8,
+ Type: SECONDARY,
+ },
+ }
+
+ th.CheckDeepEquals(t, expected, actual)
+
+ return true, nil
+ })
+
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, 1, count)
+}
+
+func TestCreate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockCreateResponse(t, lbID)
+
+ opts := CreateOpts{
+ CreateOpt{
+ Address: "10.2.2.3",
+ Port: 80,
+ Condition: ENABLED,
+ Type: PRIMARY,
+ },
+ CreateOpt{
+ Address: "10.2.2.4",
+ Port: 81,
+ Condition: ENABLED,
+ Type: SECONDARY,
+ },
+ }
+
+ page := Create(client.ServiceClient(), lbID, opts)
+
+ actual, err := page.ExtractNodes()
+ th.AssertNoErr(t, err)
+
+ expected := []Node{
+ Node{
+ ID: 185,
+ Address: "10.2.2.3",
+ Port: 80,
+ Condition: ENABLED,
+ Status: ONLINE,
+ Weight: 1,
+ Type: PRIMARY,
+ },
+ Node{
+ ID: 186,
+ Address: "10.2.2.4",
+ Port: 81,
+ Condition: ENABLED,
+ Status: ONLINE,
+ Weight: 1,
+ Type: SECONDARY,
+ },
+ }
+
+ th.CheckDeepEquals(t, expected, actual)
+}
+
+func TestBulkDelete(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ ids := []int{nodeID, nodeID2}
+
+ mockBatchDeleteResponse(t, lbID, ids)
+
+ err := BulkDelete(client.ServiceClient(), lbID, ids).ExtractErr()
+ th.AssertNoErr(t, err)
+}
+
+func TestGet(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockGetResponse(t, lbID, nodeID)
+
+ node, err := Get(client.ServiceClient(), lbID, nodeID).Extract()
+ th.AssertNoErr(t, err)
+
+ expected := &Node{
+ ID: 410,
+ Address: "10.1.1.1",
+ Port: 80,
+ Condition: ENABLED,
+ Status: ONLINE,
+ Weight: 12,
+ Type: PRIMARY,
+ }
+
+ th.AssertDeepEquals(t, expected, node)
+}
+
+func TestUpdate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockUpdateResponse(t, lbID, nodeID)
+
+ opts := UpdateOpts{
+ Weight: gophercloud.IntToPointer(10),
+ Condition: DRAINING,
+ Type: SECONDARY,
+ }
+
+ err := Update(client.ServiceClient(), lbID, nodeID, opts).ExtractErr()
+ th.AssertNoErr(t, err)
+}
+
+func TestDelete(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockDeleteResponse(t, lbID, nodeID)
+
+ err := Delete(client.ServiceClient(), lbID, nodeID).ExtractErr()
+ th.AssertNoErr(t, err)
+}
+
+func TestListEvents(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockListEventsResponse(t, lbID)
+
+ count := 0
+
+ pager := ListEvents(client.ServiceClient(), lbID, ListEventsOpts{})
+
+ err := pager.EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractNodeEvents(page)
+ th.AssertNoErr(t, err)
+
+ expected := []NodeEvent{
+ NodeEvent{
+ DetailedMessage: "Node is ok",
+ NodeID: 373,
+ ID: 7,
+ Type: "UPDATE_NODE",
+ Description: "Node '373' status changed to 'ONLINE' for load balancer '323'",
+ Category: "UPDATE",
+ Severity: "INFO",
+ RelativeURI: "/406271/loadbalancers/323/nodes/373/events",
+ AccountID: 406271,
+ LoadBalancerID: 323,
+ Title: "Node Status Updated",
+ Author: "Rackspace Cloud",
+ Created: "10-30-2012 10:18:23",
+ },
+ }
+
+ th.CheckDeepEquals(t, expected, actual)
+
+ return true, nil
+ })
+
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, 1, count)
+}
diff --git a/rackspace/lb/v1/nodes/results.go b/rackspace/lb/v1/nodes/results.go
new file mode 100644
index 0000000..916485f
--- /dev/null
+++ b/rackspace/lb/v1/nodes/results.go
@@ -0,0 +1,210 @@
+package nodes
+
+import (
+ "github.com/mitchellh/mapstructure"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// Node represents a back-end device, usually a virtual machine, that can
+// handle traffic. It is assigned traffic based on its parent load balancer.
+type Node struct {
+ // The IP address or CIDR for this back-end node.
+ Address string
+
+ // The unique ID for this node.
+ ID int
+
+ // The port on which traffic is sent and received.
+ Port int
+
+ // The node's status.
+ Status Status
+
+ // The node's condition.
+ Condition Condition
+
+ // The priority at which this node will receive traffic if a weighted
+ // algorithm is used by its parent load balancer. Ranges from 1 to 100.
+ Weight int
+
+ // Type of node.
+ Type Type
+}
+
+// Type indicates whether the node is of a PRIMARY or SECONDARY nature.
+type Type string
+
+const (
+ // PRIMARY nodes are in the normal rotation to receive traffic from the load
+ // balancer.
+ PRIMARY Type = "PRIMARY"
+
+ // SECONDARY nodes are only in the rotation to receive traffic from the load
+ // balancer when all the primary nodes fail. This provides a failover feature
+ // that automatically routes traffic to the secondary node in the event that
+ // the primary node is disabled or in a failing state. Note that active
+ // health monitoring must be enabled on the load balancer to enable the
+ // failover feature to the secondary node.
+ SECONDARY Type = "SECONDARY"
+)
+
+// Condition represents the condition of a node.
+type Condition string
+
+const (
+ // ENABLED indicates that the node is permitted to accept new connections.
+ ENABLED Condition = "ENABLED"
+
+ // DISABLED indicates that the node is not permitted to accept any new
+ // connections regardless of session persistence configuration. Existing
+ // connections are forcibly terminated.
+ DISABLED Condition = "DISABLED"
+
+ // DRAINING indicates that the node is allowed to service existing
+ // established connections and connections that are being directed to it as a
+ // result of the session persistence configuration.
+ DRAINING Condition = "DRAINING"
+)
+
+// Status indicates whether the node can accept service traffic. If a node is
+// not listening on its port or does not meet the conditions of the defined
+// active health check for the load balancer, then the load balancer does not
+// forward connections, and its status is listed as OFFLINE.
+type Status string
+
+const (
+ // ONLINE indicates that the node is healthy and capable of receiving traffic
+ // from the load balancer.
+ ONLINE Status = "ONLINE"
+
+ // OFFLINE indicates that the node is not in a position to receive service
+ // traffic. It is usually switched into this state when a health check is not
+ // satisfied with the node's response time.
+ OFFLINE Status = "OFFLINE"
+)
+
+// NodePage is the page returned by a pager when traversing over a collection
+// of nodes.
+type NodePage struct {
+ pagination.SinglePageBase
+}
+
+// IsEmpty checks whether a NodePage struct is empty.
+func (p NodePage) IsEmpty() (bool, error) {
+ is, err := ExtractNodes(p)
+ if err != nil {
+ return true, nil
+ }
+ return len(is) == 0, nil
+}
+
+func commonExtractNodes(body interface{}) ([]Node, error) {
+ var resp struct {
+ Nodes []Node `mapstructure:"nodes" json:"nodes"`
+ }
+
+ err := mapstructure.Decode(body, &resp)
+
+ return resp.Nodes, err
+}
+
+// ExtractNodes accepts a Page struct, specifically a NodePage struct, and
+// extracts the elements into a slice of Node structs. In other words, a
+// generic collection is mapped into a relevant slice.
+func ExtractNodes(page pagination.Page) ([]Node, error) {
+ return commonExtractNodes(page.(NodePage).Body)
+}
+
+// CreateResult represents the result of a create operation. Since multiple
+// nodes can be added in one operation, this result represents multiple nodes
+// and should be treated as a typical pagination Page. Use its ExtractNodes
+// method to get out a slice of Node structs.
+type CreateResult struct {
+ pagination.SinglePageBase
+}
+
+// ExtractNodes extracts a slice of Node structs from a CreateResult.
+func (res CreateResult) ExtractNodes() ([]Node, error) {
+ return commonExtractNodes(res.Body)
+}
+
+// DeleteResult represents the result of a delete operation.
+type DeleteResult struct {
+ gophercloud.ErrResult
+}
+
+type commonResult struct {
+ gophercloud.Result
+}
+
+// GetResult represents the result of a get operation.
+type GetResult struct {
+ commonResult
+}
+
+// UpdateResult represents the result of an update operation.
+type UpdateResult struct {
+ gophercloud.ErrResult
+}
+
+func (r commonResult) Extract() (*Node, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var response struct {
+ Node Node `mapstructure:"node"`
+ }
+
+ err := mapstructure.Decode(r.Body, &response)
+
+ return &response.Node, err
+}
+
+// NodeEvent represents a service event that occurred between a node and a
+// load balancer.
+type NodeEvent struct {
+ ID int
+ DetailedMessage string
+ NodeID int
+ Type string
+ Description string
+ Category string
+ Severity string
+ RelativeURI string
+ AccountID int
+ LoadBalancerID int
+ Title string
+ Author string
+ Created string
+}
+
+// NodeEventPage is a concrete type which embeds the common SinglePageBase
+// struct, and is used when traversing node event collections.
+type NodeEventPage struct {
+ pagination.SinglePageBase
+}
+
+// IsEmpty is a concrete function which indicates whether an NodeEventPage is
+// empty or not.
+func (r NodeEventPage) IsEmpty() (bool, error) {
+ is, err := ExtractNodeEvents(r)
+ if err != nil {
+ return true, err
+ }
+ return len(is) == 0, nil
+}
+
+// ExtractNodeEvents accepts a Page struct, specifically a NodeEventPage
+// struct, and extracts the elements into a slice of NodeEvent structs. In
+// other words, the collection is mapped into a relevant slice.
+func ExtractNodeEvents(page pagination.Page) ([]NodeEvent, error) {
+ var resp struct {
+ Events []NodeEvent `mapstructure:"nodeServiceEvents" json:"nodeServiceEvents"`
+ }
+
+ err := mapstructure.Decode(page.(NodeEventPage).Body, &resp)
+
+ return resp.Events, err
+}
diff --git a/rackspace/lb/v1/nodes/urls.go b/rackspace/lb/v1/nodes/urls.go
new file mode 100644
index 0000000..2cefee2
--- /dev/null
+++ b/rackspace/lb/v1/nodes/urls.go
@@ -0,0 +1,25 @@
+package nodes
+
+import (
+ "strconv"
+
+ "github.com/rackspace/gophercloud"
+)
+
+const (
+ lbPath = "loadbalancers"
+ nodePath = "nodes"
+ eventPath = "events"
+)
+
+func resourceURL(c *gophercloud.ServiceClient, lbID, nodeID int) string {
+ return c.ServiceURL(lbPath, strconv.Itoa(lbID), nodePath, strconv.Itoa(nodeID))
+}
+
+func rootURL(c *gophercloud.ServiceClient, lbID int) string {
+ return c.ServiceURL(lbPath, strconv.Itoa(lbID), nodePath)
+}
+
+func eventsURL(c *gophercloud.ServiceClient, lbID int) string {
+ return c.ServiceURL(lbPath, strconv.Itoa(lbID), nodePath, eventPath)
+}
diff --git a/rackspace/lb/v1/sessions/doc.go b/rackspace/lb/v1/sessions/doc.go
new file mode 100644
index 0000000..dcec0a8
--- /dev/null
+++ b/rackspace/lb/v1/sessions/doc.go
@@ -0,0 +1,30 @@
+/*
+Package sessions provides information and interaction with the Session
+Persistence feature of the Rackspace Cloud Load Balancer service.
+
+Session persistence is a feature of the load balancing service that forces
+multiple requests from clients (of the same protocol) to be directed to the
+same node. This is common with many web applications that do not inherently
+share application state between back-end servers.
+
+There are two modes to choose from: HTTP_COOKIE and SOURCE_IP. You can only set
+one of the session persistence modes on a load balancer, and it can only
+support one protocol. If you set HTTP_COOKIE mode for an HTTP load balancer, it
+supports session persistence for HTTP requests only. Likewise, if you set
+SOURCE_IP mode for an HTTPS load balancer, it supports session persistence for
+only HTTPS requests.
+
+To support session persistence for both HTTP and HTTPS requests concurrently,
+choose one of the following options:
+
+- Use two load balancers, one configured for session persistence for HTTP
+requests and the other configured for session persistence for HTTPS requests.
+That way, the load balancers support session persistence for both HTTP and
+HTTPS requests concurrently, with each load balancer supporting one of the
+protocols.
+
+- Use one load balancer, configure it for session persistence for HTTP requests,
+and then enable SSL termination for that load balancer. The load balancer
+supports session persistence for both HTTP and HTTPS requests concurrently.
+*/
+package sessions
diff --git a/rackspace/lb/v1/sessions/fixtures.go b/rackspace/lb/v1/sessions/fixtures.go
new file mode 100644
index 0000000..9596819
--- /dev/null
+++ b/rackspace/lb/v1/sessions/fixtures.go
@@ -0,0 +1,58 @@
+package sessions
+
+import (
+ "fmt"
+ "net/http"
+ "strconv"
+ "testing"
+
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func _rootURL(id int) string {
+ return "/loadbalancers/" + strconv.Itoa(id) + "/sessionpersistence"
+}
+
+func mockGetResponse(t *testing.T, lbID int) {
+ th.Mux.HandleFunc(_rootURL(lbID), 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, `
+{
+ "sessionPersistence": {
+ "persistenceType": "HTTP_COOKIE"
+ }
+}
+`)
+ })
+}
+
+func mockEnableResponse(t *testing.T, lbID int) {
+ th.Mux.HandleFunc(_rootURL(lbID), 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, `
+{
+ "sessionPersistence": {
+ "persistenceType": "HTTP_COOKIE"
+ }
+}
+ `)
+
+ w.WriteHeader(http.StatusAccepted)
+ })
+}
+
+func mockDisableResponse(t *testing.T, lbID int) {
+ th.Mux.HandleFunc(_rootURL(lbID), func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "DELETE")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ w.WriteHeader(http.StatusAccepted)
+ })
+}
diff --git a/rackspace/lb/v1/sessions/requests.go b/rackspace/lb/v1/sessions/requests.go
new file mode 100644
index 0000000..9853ad1
--- /dev/null
+++ b/rackspace/lb/v1/sessions/requests.go
@@ -0,0 +1,82 @@
+package sessions
+
+import (
+ "errors"
+
+ "github.com/racker/perigee"
+
+ "github.com/rackspace/gophercloud"
+)
+
+// CreateOptsBuilder is the interface options structs have to satisfy in order
+// to be used in the main Create operation in this package.
+type CreateOptsBuilder interface {
+ ToSPCreateMap() (map[string]interface{}, error)
+}
+
+// CreateOpts is the common options struct used in this package's Create
+// operation.
+type CreateOpts struct {
+ // Required - can either be HTTPCOOKIE or SOURCEIP
+ Type Type
+}
+
+// ToSPCreateMap casts a CreateOpts struct to a map.
+func (opts CreateOpts) ToSPCreateMap() (map[string]interface{}, error) {
+ sp := make(map[string]interface{})
+
+ if opts.Type == "" {
+ return sp, errors.New("Type is a required field")
+ }
+
+ sp["persistenceType"] = opts.Type
+ return map[string]interface{}{"sessionPersistence": sp}, nil
+}
+
+// Enable is the operation responsible for enabling session persistence for a
+// particular load balancer.
+func Enable(c *gophercloud.ServiceClient, lbID int, opts CreateOptsBuilder) EnableResult {
+ var res EnableResult
+
+ reqBody, err := opts.ToSPCreateMap()
+ if err != nil {
+ res.Err = err
+ return res
+ }
+
+ _, res.Err = perigee.Request("PUT", rootURL(c, lbID), perigee.Options{
+ MoreHeaders: c.AuthenticatedHeaders(),
+ ReqBody: &reqBody,
+ Results: &res.Body,
+ OkCodes: []int{202},
+ })
+
+ return res
+}
+
+// Get is the operation responsible for showing details of the session
+// persistence configuration for a particular load balancer.
+func Get(c *gophercloud.ServiceClient, lbID int) GetResult {
+ var res GetResult
+
+ _, res.Err = perigee.Request("GET", rootURL(c, lbID), perigee.Options{
+ MoreHeaders: c.AuthenticatedHeaders(),
+ Results: &res.Body,
+ OkCodes: []int{200},
+ })
+
+ return res
+}
+
+// Disable is the operation responsible for disabling session persistence for a
+// particular load balancer.
+func Disable(c *gophercloud.ServiceClient, lbID int) DisableResult {
+ var res DisableResult
+
+ _, res.Err = perigee.Request("DELETE", rootURL(c, lbID), perigee.Options{
+ MoreHeaders: c.AuthenticatedHeaders(),
+ OkCodes: []int{202},
+ })
+
+ return res
+}
diff --git a/rackspace/lb/v1/sessions/requests_test.go b/rackspace/lb/v1/sessions/requests_test.go
new file mode 100644
index 0000000..f319e54
--- /dev/null
+++ b/rackspace/lb/v1/sessions/requests_test.go
@@ -0,0 +1,44 @@
+package sessions
+
+import (
+ "testing"
+
+ th "github.com/rackspace/gophercloud/testhelper"
+ "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+const lbID = 12345
+
+func TestEnable(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockEnableResponse(t, lbID)
+
+ opts := CreateOpts{Type: HTTPCOOKIE}
+ err := Enable(client.ServiceClient(), lbID, opts).ExtractErr()
+ th.AssertNoErr(t, err)
+}
+
+func TestGet(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockGetResponse(t, lbID)
+
+ sp, err := Get(client.ServiceClient(), lbID).Extract()
+ th.AssertNoErr(t, err)
+
+ expected := &SessionPersistence{Type: HTTPCOOKIE}
+ th.AssertDeepEquals(t, expected, sp)
+}
+
+func TestDisable(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockDisableResponse(t, lbID)
+
+ err := Disable(client.ServiceClient(), lbID).ExtractErr()
+ th.AssertNoErr(t, err)
+}
diff --git a/rackspace/lb/v1/sessions/results.go b/rackspace/lb/v1/sessions/results.go
new file mode 100644
index 0000000..fe90e72
--- /dev/null
+++ b/rackspace/lb/v1/sessions/results.go
@@ -0,0 +1,58 @@
+package sessions
+
+import (
+ "github.com/mitchellh/mapstructure"
+
+ "github.com/rackspace/gophercloud"
+)
+
+// Type represents the type of session persistence being used.
+type Type string
+
+const (
+ // HTTPCOOKIE is a session persistence mechanism that inserts an HTTP cookie
+ // and is used to determine the destination back-end node. This is supported
+ // for HTTP load balancing only.
+ HTTPCOOKIE Type = "HTTP_COOKIE"
+
+ // SOURCEIP is a session persistence mechanism that keeps track of the source
+ // IP address that is mapped and is able to determine the destination
+ // back-end node. This is supported for HTTPS pass-through and non-HTTP load
+ // balancing only.
+ SOURCEIP Type = "SOURCE_IP"
+)
+
+// SessionPersistence indicates how a load balancer is using session persistence
+type SessionPersistence struct {
+ Type Type `mapstructure:"persistenceType"`
+}
+
+// EnableResult represents the result of an enable operation.
+type EnableResult struct {
+ gophercloud.ErrResult
+}
+
+// DisableResult represents the result of a disable operation.
+type DisableResult struct {
+ gophercloud.ErrResult
+}
+
+// GetResult represents the result of a get operation.
+type GetResult struct {
+ gophercloud.Result
+}
+
+// Extract interprets a GetResult as an SP, if possible.
+func (r GetResult) Extract() (*SessionPersistence, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var response struct {
+ SP SessionPersistence `mapstructure:"sessionPersistence"`
+ }
+
+ err := mapstructure.Decode(r.Body, &response)
+
+ return &response.SP, err
+}
diff --git a/rackspace/lb/v1/sessions/urls.go b/rackspace/lb/v1/sessions/urls.go
new file mode 100644
index 0000000..c4a896d
--- /dev/null
+++ b/rackspace/lb/v1/sessions/urls.go
@@ -0,0 +1,16 @@
+package sessions
+
+import (
+ "strconv"
+
+ "github.com/rackspace/gophercloud"
+)
+
+const (
+ path = "loadbalancers"
+ spPath = "sessionpersistence"
+)
+
+func rootURL(c *gophercloud.ServiceClient, id int) string {
+ return c.ServiceURL(path, strconv.Itoa(id), spPath)
+}
diff --git a/rackspace/lb/v1/ssl/doc.go b/rackspace/lb/v1/ssl/doc.go
new file mode 100644
index 0000000..6a2c174
--- /dev/null
+++ b/rackspace/lb/v1/ssl/doc.go
@@ -0,0 +1,22 @@
+/*
+Package ssl provides information and interaction with the SSL Termination
+feature of the Rackspace Cloud Load Balancer service.
+
+You may only enable and configure SSL termination on load balancers with
+non-secure protocols, such as HTTP, but not HTTPS.
+
+SSL-terminated load balancers decrypt the traffic at the traffic manager and
+pass unencrypted traffic to the back-end node. Because of this, the customer's
+back-end nodes don't know what protocol the client requested. For this reason,
+the X-Forwarded-Proto (XFP) header has been added for identifying the
+originating protocol of an HTTP request as "http" or "https" depending on what
+protocol the client requested.
+
+Not every service returns certificates in the proper order. Please verify that
+your chain of certificates matches that of walking up the chain from the domain
+to the CA root.
+
+If used for HTTP to HTTPS redirection, the LoadBalancer's securePort attribute
+must be set to 443, and its secureTrafficOnly attribute must be true.
+*/
+package ssl
diff --git a/rackspace/lb/v1/ssl/fixtures.go b/rackspace/lb/v1/ssl/fixtures.go
new file mode 100644
index 0000000..1d40100
--- /dev/null
+++ b/rackspace/lb/v1/ssl/fixtures.go
@@ -0,0 +1,195 @@
+package ssl
+
+import (
+ "fmt"
+ "net/http"
+ "strconv"
+ "testing"
+
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func _rootURL(id int) string {
+ return "/loadbalancers/" + strconv.Itoa(id) + "/ssltermination"
+}
+
+func _certURL(id, certID int) string {
+ url := _rootURL(id) + "/certificatemappings"
+ if certID > 0 {
+ url += "/" + strconv.Itoa(certID)
+ }
+ return url
+}
+
+func mockGetResponse(t *testing.T, lbID int) {
+ th.Mux.HandleFunc(_rootURL(lbID), 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, `
+{
+ "sslTermination": {
+ "certificate": "-----BEGIN CERTIFICATE-----\nMIIEXTCCA0WgAwIBAgIGATTEAjK3MA0GCSqGSIb3DQEBBQUAMIGDMRkwFwYDVQQD\nExBUZXN0IENBIFNUdWIgS2V5MRcwFQYDVQQLEw5QbGF0Zm9ybSBMYmFhczEaMBgG\nA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFDASBgNVBAcTC1NhbiBBbnRvbmlvMQ4w\nDAYDVQQIEwVUZXhhczELMAkGA1UEBhMCVVMwHhcNMTIwMTA5MTk0NjQ1WhcNMTQw\nMTA4MTk0NjQ1WjCBgjELMAkGA1UEBhMCVVMxDjAMBgNVBAgTBVRleGFzMRQwEgYD\nVQQHEwtTYW4gQW50b25pbzEaMBgGA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFzAV\nBgNVBAsTDlBsYXRmb3JtIExiYWFzMRgwFgYDVQQDEw9UZXN0IENsaWVudCBLZXkw\nggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDAi51IylFnHtNLT8C0NVfc\nOBfAsP2D5es1qhrOWHCGlgAuDMksBsCc7FPo5PSBOmQ+6z8HtCFbrLoC5/Zx0F5b\nfVegjA+xKjI2HGASsYHHM0BFEH2UjUcJrWiMWtxQuW6Phbqulo7JwjmygMEmIkeK\nf+FtkE9mrq+E8K40/thrjxl3/ZcJD1+3dcp+ZuzVJ2t1E4iGKCx79IZFsysKiuf+\n+E0i6iGvvI6UcbcZxVxQj2TplJkFuoX5kDgClIX9Dr9y6aJ4SCh+GRhvHl+DTaz0\nnCvghachHZtIeztRDqxWApjOOzs93dSelrviMXDr8fqyEAGg7YIhgui0aZBsWCen\nAgMBAAGjgdUwgdIwgbAGA1UdIwSBqDCBpYAUNpx1Pc6cGA7KqEwHMmHBTZMA7lSh\ngYmkgYYwgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVU4IBATAd\nBgNVHQ4EFgQULueOfsjZZOHwJHZwBy6u0swnpccwDQYJKoZIhvcNAQEFBQADggEB\nAFNuqSVUaotUJoWDv4z7Kbi6JFpTjDht5ORw4BdVYlRD4h9DACAFzPrPV2ym/Osp\nhNMdZq6msZku7MdOSQVhdeGWrSNk3M8O9Hg7cVzPNXOF3iNoo3irQ5tURut44xs4\nWw5YWQqS9WyUY5snD8tm7Y1rQTPfhg+678xIq/zWCv/u+FSnfVv1nlhLVQkEeG/Y\ngh1uMaTIpUKTGEjIAGtpGP7wwIcXptR/HyfzhTUSTaWc1Ef7zoKT9LL5z3IV1hC2\njVWz+RwYs98LjMuksJFoHqRfWyYhCIym0jb6GTwaEmpxAjc+d7OLNQdnoEGoUYGP\nYjtfkRYg265ESMA+Kww4Xy8=\n-----END CERTIFICATE-----\n",
+ "enabled": true,
+ "secureTrafficOnly": false,
+ "intermediateCertificate": "-----BEGIN CERTIFICATE-----\nMIIDtTCCAp2gAwIBAgIBATANBgkqhkiG9w0BAQUFADCBgzEZMBcGA1UEAxMQVGVz\ndCBDQSBTVHViIEtleTEXMBUGA1UECxMOUGxhdGZvcm0gTGJhYXMxGjAYBgNVBAoT\nEVJhY2tzcGFjZSBIb3N0aW5nMRQwEgYDVQQHEwtTYW4gQW50b25pbzEOMAwGA1UE\nCBMFVGV4YXMxCzAJBgNVBAYTAlVTMB4XDTEyMDEwOTE5NDU0OVoXDTE0MDEwODE5\nNDU0OVowgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVUzCCASIw\nDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANNh55lwTVwQvNoEZjq1zGdYz9jA\nXXdjizn8AJhjHLOAallfPtvCfTEgKanhdoyz5FnhQE8HbDAop/KNS1lN2UMvdl5f\nZNLTSjJrNtedqxQwxN/i3bpyBxNVejUH2NjV1mmyj+5CJYwCzWalvI/gLPq/A3as\nO2EQqtf3U8unRgn0zXLRdYxV9MrUzNAmdipPNvNrsVdrCgA42rgF/8KsyRVQfJCX\nfN7PGCfrsC3YaUvhymraWxNnXIzMYTNa9wEeBZLUw8SlEtpa1Zsvui+TPXu3USNZ\nVnWH8Lb6ENlnoX0VBwo62fjOG3JzhNKoJawi3bRqyDdINOvafr7iPrrs/T8CAwEA\nAaMyMDAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUNpx1Pc6cGA7KqEwHMmHB\nTZMA7lQwDQYJKoZIhvcNAQEFBQADggEBAMoRgH3iTG3t317viLKoY+lNMHUgHuR7\nb3mn9MidJKyYVewe6hCDIN6WY4fUojmMW9wFJWJIo0hRMNHL3n3tq8HP2j20Mxy8\nacPdfGZJa+jiBw72CrIGdobKaFduIlIEDBA1pNdZIJ+EulrtqrMesnIt92WaypIS\n8JycbIgDMCiyC0ENHEk8UWlC6429c7OZAsplMTbHME/1R4btxjkdfrYZJjdJ2yL2\n8cjZDUDMCPTdW/ycP07Gkq30RB5tACB5aZdaCn2YaKC8FsEdhff4X7xEOfOEHWEq\nSRxADDj8Lx1MT6QpR07hCiDyHfTCtbqzI0iGjX63Oh7xXSa0f+JVTa8=\n-----END CERTIFICATE-----\n",
+ "securePort": 443
+ }
+}
+`)
+ })
+}
+
+func mockUpdateResponse(t *testing.T, lbID int) {
+ th.Mux.HandleFunc(_rootURL(lbID), 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, `
+{
+ "sslTermination": {
+ "enabled": true,
+ "securePort": 443,
+ "secureTrafficOnly": false,
+ "privateKey": "foo",
+ "certificate": "-----BEGIN CERTIFICATE-----\nMIIEXTCCA0WgAwIBAgIGATTEAjK3MA0GCSqGSIb3DQEBBQUAMIGDMRkwFwYDVQQD\nExBUZXN0IENBIFNUdWIgS2V5MRcwFQYDVQQLEw5QbGF0Zm9ybSBMYmFhczEaMBgG\nA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFDASBgNVBAcTC1NhbiBBbnRvbmlvMQ4w\nDAYDVQQIEwVUZXhhczELMAkGA1UEBhMCVVMwHhcNMTIwMTA5MTk0NjQ1WhcNMTQw\nMTA4MTk0NjQ1WjCBgjELMAkGA1UEBhMCVVMxDjAMBgNVBAgTBVRleGFzMRQwEgYD\nVQQHEwtTYW4gQW50b25pbzEaMBgGA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFzAV\nBgNVBAsTDlBsYXRmb3JtIExiYWFzMRgwFgYDVQQDEw9UZXN0IENsaWVudCBLZXkw\nggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDAi51IylFnHtNLT8C0NVfc\nOBfAsP2D5es1qhrOWHCGlgAuDMksBsCc7FPo5PSBOmQ+6z8HtCFbrLoC5/Zx0F5b\nfVegjA+xKjI2HGASsYHHM0BFEH2UjUcJrWiMWtxQuW6Phbqulo7JwjmygMEmIkeK\nf+FtkE9mrq+E8K40/thrjxl3/ZcJD1+3dcp+ZuzVJ2t1E4iGKCx79IZFsysKiuf+\n+E0i6iGvvI6UcbcZxVxQj2TplJkFuoX5kDgClIX9Dr9y6aJ4SCh+GRhvHl+DTaz0\nnCvghachHZtIeztRDqxWApjOOzs93dSelrviMXDr8fqyEAGg7YIhgui0aZBsWCen\nAgMBAAGjgdUwgdIwgbAGA1UdIwSBqDCBpYAUNpx1Pc6cGA7KqEwHMmHBTZMA7lSh\ngYmkgYYwgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVU4IBATAd\nBgNVHQ4EFgQULueOfsjZZOHwJHZwBy6u0swnpccwDQYJKoZIhvcNAQEFBQADggEB\nAFNuqSVUaotUJoWDv4z7Kbi6JFpTjDht5ORw4BdVYlRD4h9DACAFzPrPV2ym/Osp\nhNMdZq6msZku7MdOSQVhdeGWrSNk3M8O9Hg7cVzPNXOF3iNoo3irQ5tURut44xs4\nWw5YWQqS9WyUY5snD8tm7Y1rQTPfhg+678xIq/zWCv/u+FSnfVv1nlhLVQkEeG/Y\ngh1uMaTIpUKTGEjIAGtpGP7wwIcXptR/HyfzhTUSTaWc1Ef7zoKT9LL5z3IV1hC2\njVWz+RwYs98LjMuksJFoHqRfWyYhCIym0jb6GTwaEmpxAjc+d7OLNQdnoEGoUYGP\nYjtfkRYg265ESMA+Kww4Xy8=\n-----END CERTIFICATE-----\n",
+ "intermediateCertificate": "-----BEGIN CERTIFICATE-----\nMIIDtTCCAp2gAwIBAgIBATANBgkqhkiG9w0BAQUFADCBgzEZMBcGA1UEAxMQVGVz\ndCBDQSBTVHViIEtleTEXMBUGA1UECxMOUGxhdGZvcm0gTGJhYXMxGjAYBgNVBAoT\nEVJhY2tzcGFjZSBIb3N0aW5nMRQwEgYDVQQHEwtTYW4gQW50b25pbzEOMAwGA1UE\nCBMFVGV4YXMxCzAJBgNVBAYTAlVTMB4XDTEyMDEwOTE5NDU0OVoXDTE0MDEwODE5\nNDU0OVowgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVUzCCASIw\nDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANNh55lwTVwQvNoEZjq1zGdYz9jA\nXXdjizn8AJhjHLOAallfPtvCfTEgKanhdoyz5FnhQE8HbDAop/KNS1lN2UMvdl5f\nZNLTSjJrNtedqxQwxN/i3bpyBxNVejUH2NjV1mmyj+5CJYwCzWalvI/gLPq/A3as\nO2EQqtf3U8unRgn0zXLRdYxV9MrUzNAmdipPNvNrsVdrCgA42rgF/8KsyRVQfJCX\nfN7PGCfrsC3YaUvhymraWxNnXIzMYTNa9wEeBZLUw8SlEtpa1Zsvui+TPXu3USNZ\nVnWH8Lb6ENlnoX0VBwo62fjOG3JzhNKoJawi3bRqyDdINOvafr7iPrrs/T8CAwEA\nAaMyMDAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUNpx1Pc6cGA7KqEwHMmHB\nTZMA7lQwDQYJKoZIhvcNAQEFBQADggEBAMoRgH3iTG3t317viLKoY+lNMHUgHuR7\nb3mn9MidJKyYVewe6hCDIN6WY4fUojmMW9wFJWJIo0hRMNHL3n3tq8HP2j20Mxy8\nacPdfGZJa+jiBw72CrIGdobKaFduIlIEDBA1pNdZIJ+EulrtqrMesnIt92WaypIS\n8JycbIgDMCiyC0ENHEk8UWlC6429c7OZAsplMTbHME/1R4btxjkdfrYZJjdJ2yL2\n8cjZDUDMCPTdW/ycP07Gkq30RB5tACB5aZdaCn2YaKC8FsEdhff4X7xEOfOEHWEq\nSRxADDj8Lx1MT6QpR07hCiDyHfTCtbqzI0iGjX63Oh7xXSa0f+JVTa8=\n-----END CERTIFICATE-----\n"
+ }
+}
+ `)
+
+ w.WriteHeader(http.StatusOK)
+ })
+}
+
+func mockDeleteResponse(t *testing.T, lbID int) {
+ th.Mux.HandleFunc(_rootURL(lbID), func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "DELETE")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ w.WriteHeader(http.StatusOK)
+ })
+}
+
+func mockListCertsResponse(t *testing.T, lbID int) {
+ th.Mux.HandleFunc(_certURL(lbID, 0), 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, `
+{
+ "certificateMappings": [
+ {
+ "certificateMapping": {
+ "id": 123,
+ "hostName": "rackspace.com"
+ }
+ },
+ {
+ "certificateMapping": {
+ "id": 124,
+ "hostName": "*.rackspace.com"
+ }
+ }
+ ]
+}
+`)
+ })
+}
+
+func mockAddCertResponse(t *testing.T, lbID int) {
+ th.Mux.HandleFunc(_certURL(lbID, 0), func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "POST")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+ th.TestJSONRequest(t, r, `
+{
+ "certificateMapping": {
+ "hostName": "rackspace.com",
+ "privateKey":"-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEAwIudSMpRZx7TS0/AtDVX3DgXwLD9g+XrNaoazlhwhpYALgzJ\nLAbAnOxT6OT0gTpkPus/B7QhW6y6Auf2cdBeW31XoIwPsSoyNhxgErGBxzNARRB9\nlI1HCa1ojFrcULluj4W6rpaOycI5soDBJiJHin/hbZBPZq6vhPCuNP7Ya48Zd/2X\nCQ9ft3XKfmbs1SdrdROIhigse/SGRbMrCorn/vhNIuohr7yOlHG3GcVcUI9k6ZSZ\nBbqF+ZA4ApSF/Q6/cumieEgofhkYbx5fg02s9Jwr4IWnIR2bSHs7UQ6sVgKYzjs7\nPd3Unpa74jFw6/H6shABoO2CIYLotGmQbFgnpwIDAQABAoIBAQCBCQ+PCIclJHNV\ntUzfeCA5ZR4F9JbxHdRTUnxEbOB8UWotckQfTScoAvj4yvdQ42DrCZxj/UOdvFOs\nPufZvlp91bIz1alugWjE+p8n5+2hIaegoTyHoWZKBfxak0myj5KYfHZvKlbmv1ML\nXV4TwEVRfAIG+v87QTY/UUxuF5vR+BpKIbgUJLfPUFFvJUdl84qsJ44pToxaYUd/\nh5YAGC00U4ay1KVSAUnTkkPNZ0lPG/rWU6w6WcTvNRLMd8DzFLTKLOgQfHhbExAF\n+sXPWjWSzbBRP1O7fHqq96QQh4VFiY/7w9W+sDKQyV6Ul17OSXs6aZ4f+lq4rJTI\n1FG96YiBAoGBAO1tiH0h1oWDBYfJB3KJJ6CQQsDGwtHo/DEgznFVP4XwEVbZ98Ha\nBfBCn3sAybbaikyCV1Hwj7kfHMZPDHbrcUSFX7quu/2zPK+wO3lZKXSyu4YsguSa\nRedInN33PpdnlPhLyQdWSuD5sVHJDF6xn22vlyxeILH3ooLg2WOFMPmVAoGBAM+b\nUG/a7iyfpAQKYyuFAsXz6SeFaDY+ZYeX45L112H8Pu+Ie/qzon+bzLB9FIH8GP6+\nQpQgmm/p37U2gD1zChUv7iW6OfQBKk9rWvMpfRF6d7YHquElejhizfTZ+ntBV/VY\ndOYEczxhrdW7keLpatYaaWUy/VboRZmlz/9JGqVLAoGAHfqNmFc0cgk4IowEj7a3\ntTNh6ltub/i+FynwRykfazcDyXaeLPDtfQe8gVh5H8h6W+y9P9BjJVnDVVrX1RAn\nbiJ1EupLPF5sVDapW8ohTOXgfbGTGXBNUUW+4Nv+IDno+mz/RhjkPYHpnM0I7c/5\ntGzOZsC/2hjNgT8I0+MWav0CgYEAuULdJeQVlKalI6HtW2Gn1uRRVJ49H+LQkY6e\nW3+cw2jo9LI0CMWSphNvNrN3wIMp/vHj0fHCP0pSApDvIWbuQXfzKaGko7UCf7rK\nf6GvZRCHkV4IREBAb97j8bMvThxClMNqFfU0rFZyXP+0MOyhFQyertswrgQ6T+Fi\n2mnvKD8CgYAmJHP3NTDRMoMRyAzonJ6nEaGUbAgNmivTaUWMe0+leCvAdwD89gzC\nTKbm3eDUg/6Va3X6ANh3wsfIOe4RXXxcbcFDk9R4zO2M5gfLSjYm5Q87EBZ2hrdj\nM2gLI7dt6thx0J8lR8xRHBEMrVBdgwp0g1gQzo5dAV88/kpkZVps8Q==\n-----END RSA PRIVATE KEY-----\n",
+ "certificate":"-----BEGIN CERTIFICATE-----\nMIIEXTCCA0WgAwIBAgIGATTEAjK3MA0GCSqGSIb3DQEBBQUAMIGDMRkwFwYDVQQD\nExBUZXN0IENBIFNUdWIgS2V5MRcwFQYDVQQLEw5QbGF0Zm9ybSBMYmFhczEaMBgG\nA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFDASBgNVBAcTC1NhbiBBbnRvbmlvMQ4w\nDAYDVQQIEwVUZXhhczELMAkGA1UEBhMCVVMwHhcNMTIwMTA5MTk0NjQ1WhcNMTQw\nMTA4MTk0NjQ1WjCBgjELMAkGA1UEBhMCVVMxDjAMBgNVBAgTBVRleGFzMRQwEgYD\nVQQHEwtTYW4gQW50b25pbzEaMBgGA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFzAV\nBgNVBAsTDlBsYXRmb3JtIExiYWFzMRgwFgYDVQQDEw9UZXN0IENsaWVudCBLZXkw\nggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDAi51IylFnHtNLT8C0NVfc\nOBfAsP2D5es1qhrOWHCGlgAuDMksBsCc7FPo5PSBOmQ+6z8HtCFbrLoC5/Zx0F5b\nfVegjA+xKjI2HGASsYHHM0BFEH2UjUcJrWiMWtxQuW6Phbqulo7JwjmygMEmIkeK\nf+FtkE9mrq+E8K40/thrjxl3/ZcJD1+3dcp+ZuzVJ2t1E4iGKCx79IZFsysKiuf+\n+E0i6iGvvI6UcbcZxVxQj2TplJkFuoX5kDgClIX9Dr9y6aJ4SCh+GRhvHl+DTaz0\nnCvghachHZtIeztRDqxWApjOOzs93dSelrviMXDr8fqyEAGg7YIhgui0aZBsWCen\nAgMBAAGjgdUwgdIwgbAGA1UdIwSBqDCBpYAUNpx1Pc6cGA7KqEwHMmHBTZMA7lSh\ngYmkgYYwgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVU4IBATAd\nBgNVHQ4EFgQULueOfsjZZOHwJHZwBy6u0swnpccwDQYJKoZIhvcNAQEFBQADggEB\nAFNuqSVUaotUJoWDv4z7Kbi6JFpTjDht5ORw4BdVYlRD4h9DACAFzPrPV2ym/Osp\nhNMdZq6msZku7MdOSQVhdeGWrSNk3M8O9Hg7cVzPNXOF3iNoo3irQ5tURut44xs4\nWw5YWQqS9WyUY5snD8tm7Y1rQTPfhg+678xIq/zWCv/u+FSnfVv1nlhLVQkEeG/Y\ngh1uMaTIpUKTGEjIAGtpGP7wwIcXptR/HyfzhTUSTaWc1Ef7zoKT9LL5z3IV1hC2\njVWz+RwYs98LjMuksJFoHqRfWyYhCIym0jb6GTwaEmpxAjc+d7OLNQdnoEGoUYGP\nYjtfkRYg265ESMA+Kww4Xy8=\n-----END CERTIFICATE-----\n",
+ "intermediateCertificate":"-----BEGIN CERTIFICATE-----\nMIIDtTCCAp2gAwIBAgIBATANBgkqhkiG9w0BAQUFADCBgzEZMBcGA1UEAxMQVGVz\ndCBDQSBTVHViIEtleTEXMBUGA1UECxMOUGxhdGZvcm0gTGJhYXMxGjAYBgNVBAoT\nEVJhY2tzcGFjZSBIb3N0aW5nMRQwEgYDVQQHEwtTYW4gQW50b25pbzEOMAwGA1UE\nCBMFVGV4YXMxCzAJBgNVBAYTAlVTMB4XDTEyMDEwOTE5NDU0OVoXDTE0MDEwODE5\nNDU0OVowgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVUzCCASIw\nDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANNh55lwTVwQvNoEZjq1zGdYz9jA\nXXdjizn8AJhjHLOAallfPtvCfTEgKanhdoyz5FnhQE8HbDAop/KNS1lN2UMvdl5f\nZNLTSjJrNtedqxQwxN/i3bpyBxNVejUH2NjV1mmyj+5CJYwCzWalvI/gLPq/A3as\nO2EQqtf3U8unRgn0zXLRdYxV9MrUzNAmdipPNvNrsVdrCgA42rgF/8KsyRVQfJCX\nfN7PGCfrsC3YaUvhymraWxNnXIzMYTNa9wEeBZLUw8SlEtpa1Zsvui+TPXu3USNZ\nVnWH8Lb6ENlnoX0VBwo62fjOG3JzhNKoJawi3bRqyDdINOvafr7iPrrs/T8CAwEA\nAaMyMDAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUNpx1Pc6cGA7KqEwHMmHB\nTZMA7lQwDQYJKoZIhvcNAQEFBQADggEBAMoRgH3iTG3t317viLKoY+lNMHUgHuR7\nb3mn9MidJKyYVewe6hCDIN6WY4fUojmMW9wFJWJIo0hRMNHL3n3tq8HP2j20Mxy8\nacPdfGZJa+jiBw72CrIGdobKaFduIlIEDBA1pNdZIJ+EulrtqrMesnIt92WaypIS\n8JycbIgDMCiyC0ENHEk8UWlC6429c7OZAsplMTbHME/1R4btxjkdfrYZJjdJ2yL2\n8cjZDUDMCPTdW/ycP07Gkq30RB5tACB5aZdaCn2YaKC8FsEdhff4X7xEOfOEHWEq\nSRxADDj8Lx1MT6QpR07hCiDyHfTCtbqzI0iGjX63Oh7xXSa0f+JVTa8=\n-----END CERTIFICATE-----\n"
+ }
+}
+ `)
+
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, `
+{
+ "certificateMapping": {
+ "id": 123,
+ "hostName": "rackspace.com",
+ "certificate":"-----BEGIN CERTIFICATE-----\nMIIEXTCCA0WgAwIBAgIGATTEAjK3MA0GCSqGSIb3DQEBBQUAMIGDMRkwFwYDVQQD\nExBUZXN0IENBIFNUdWIgS2V5MRcwFQYDVQQLEw5QbGF0Zm9ybSBMYmFhczEaMBgG\nA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFDASBgNVBAcTC1NhbiBBbnRvbmlvMQ4w\nDAYDVQQIEwVUZXhhczELMAkGA1UEBhMCVVMwHhcNMTIwMTA5MTk0NjQ1WhcNMTQw\nMTA4MTk0NjQ1WjCBgjELMAkGA1UEBhMCVVMxDjAMBgNVBAgTBVRleGFzMRQwEgYD\nVQQHEwtTYW4gQW50b25pbzEaMBgGA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFzAV\nBgNVBAsTDlBsYXRmb3JtIExiYWFzMRgwFgYDVQQDEw9UZXN0IENsaWVudCBLZXkw\nggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDAi51IylFnHtNLT8C0NVfc\nOBfAsP2D5es1qhrOWHCGlgAuDMksBsCc7FPo5PSBOmQ+6z8HtCFbrLoC5/Zx0F5b\nfVegjA+xKjI2HGASsYHHM0BFEH2UjUcJrWiMWtxQuW6Phbqulo7JwjmygMEmIkeK\nf+FtkE9mrq+E8K40/thrjxl3/ZcJD1+3dcp+ZuzVJ2t1E4iGKCx79IZFsysKiuf+\n+E0i6iGvvI6UcbcZxVxQj2TplJkFuoX5kDgClIX9Dr9y6aJ4SCh+GRhvHl+DTaz0\nnCvghachHZtIeztRDqxWApjOOzs93dSelrviMXDr8fqyEAGg7YIhgui0aZBsWCen\nAgMBAAGjgdUwgdIwgbAGA1UdIwSBqDCBpYAUNpx1Pc6cGA7KqEwHMmHBTZMA7lSh\ngYmkgYYwgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVU4IBATAd\nBgNVHQ4EFgQULueOfsjZZOHwJHZwBy6u0swnpccwDQYJKoZIhvcNAQEFBQADggEB\nAFNuqSVUaotUJoWDv4z7Kbi6JFpTjDht5ORw4BdVYlRD4h9DACAFzPrPV2ym/Osp\nhNMdZq6msZku7MdOSQVhdeGWrSNk3M8O9Hg7cVzPNXOF3iNoo3irQ5tURut44xs4\nWw5YWQqS9WyUY5snD8tm7Y1rQTPfhg+678xIq/zWCv/u+FSnfVv1nlhLVQkEeG/Y\ngh1uMaTIpUKTGEjIAGtpGP7wwIcXptR/HyfzhTUSTaWc1Ef7zoKT9LL5z3IV1hC2\njVWz+RwYs98LjMuksJFoHqRfWyYhCIym0jb6GTwaEmpxAjc+d7OLNQdnoEGoUYGP\nYjtfkRYg265ESMA+Kww4Xy8=\n-----END CERTIFICATE-----\n",
+ "intermediateCertificate":"-----BEGIN CERTIFICATE-----\nMIIDtTCCAp2gAwIBAgIBATANBgkqhkiG9w0BAQUFADCBgzEZMBcGA1UEAxMQVGVz\ndCBDQSBTVHViIEtleTEXMBUGA1UECxMOUGxhdGZvcm0gTGJhYXMxGjAYBgNVBAoT\nEVJhY2tzcGFjZSBIb3N0aW5nMRQwEgYDVQQHEwtTYW4gQW50b25pbzEOMAwGA1UE\nCBMFVGV4YXMxCzAJBgNVBAYTAlVTMB4XDTEyMDEwOTE5NDU0OVoXDTE0MDEwODE5\nNDU0OVowgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVUzCCASIw\nDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANNh55lwTVwQvNoEZjq1zGdYz9jA\nXXdjizn8AJhjHLOAallfPtvCfTEgKanhdoyz5FnhQE8HbDAop/KNS1lN2UMvdl5f\nZNLTSjJrNtedqxQwxN/i3bpyBxNVejUH2NjV1mmyj+5CJYwCzWalvI/gLPq/A3as\nO2EQqtf3U8unRgn0zXLRdYxV9MrUzNAmdipPNvNrsVdrCgA42rgF/8KsyRVQfJCX\nfN7PGCfrsC3YaUvhymraWxNnXIzMYTNa9wEeBZLUw8SlEtpa1Zsvui+TPXu3USNZ\nVnWH8Lb6ENlnoX0VBwo62fjOG3JzhNKoJawi3bRqyDdINOvafr7iPrrs/T8CAwEA\nAaMyMDAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUNpx1Pc6cGA7KqEwHMmHB\nTZMA7lQwDQYJKoZIhvcNAQEFBQADggEBAMoRgH3iTG3t317viLKoY+lNMHUgHuR7\nb3mn9MidJKyYVewe6hCDIN6WY4fUojmMW9wFJWJIo0hRMNHL3n3tq8HP2j20Mxy8\nacPdfGZJa+jiBw72CrIGdobKaFduIlIEDBA1pNdZIJ+EulrtqrMesnIt92WaypIS\n8JycbIgDMCiyC0ENHEk8UWlC6429c7OZAsplMTbHME/1R4btxjkdfrYZJjdJ2yL2\n8cjZDUDMCPTdW/ycP07Gkq30RB5tACB5aZdaCn2YaKC8FsEdhff4X7xEOfOEHWEq\nSRxADDj8Lx1MT6QpR07hCiDyHfTCtbqzI0iGjX63Oh7xXSa0f+JVTa8=\n-----END CERTIFICATE-----\n"
+ }
+}
+ `)
+ })
+}
+
+func mockGetCertResponse(t *testing.T, lbID, certID int) {
+ th.Mux.HandleFunc(_certURL(lbID, certID), 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, `
+{
+ "certificateMapping": {
+ "id": 123,
+ "hostName": "rackspace.com",
+ "certificate":"-----BEGIN CERTIFICATE-----\nMIIEXTCCA0WgAwIBAgIGATTEAjK3MA0GCSqGSIb3DQEBBQUAMIGDMRkwFwYDVQQD\nExBUZXN0IENBIFNUdWIgS2V5MRcwFQYDVQQLEw5QbGF0Zm9ybSBMYmFhczEaMBgG\nA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFDASBgNVBAcTC1NhbiBBbnRvbmlvMQ4w\nDAYDVQQIEwVUZXhhczELMAkGA1UEBhMCVVMwHhcNMTIwMTA5MTk0NjQ1WhcNMTQw\nMTA4MTk0NjQ1WjCBgjELMAkGA1UEBhMCVVMxDjAMBgNVBAgTBVRleGFzMRQwEgYD\nVQQHEwtTYW4gQW50b25pbzEaMBgGA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFzAV\nBgNVBAsTDlBsYXRmb3JtIExiYWFzMRgwFgYDVQQDEw9UZXN0IENsaWVudCBLZXkw\nggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDAi51IylFnHtNLT8C0NVfc\nOBfAsP2D5es1qhrOWHCGlgAuDMksBsCc7FPo5PSBOmQ+6z8HtCFbrLoC5/Zx0F5b\nfVegjA+xKjI2HGASsYHHM0BFEH2UjUcJrWiMWtxQuW6Phbqulo7JwjmygMEmIkeK\nf+FtkE9mrq+E8K40/thrjxl3/ZcJD1+3dcp+ZuzVJ2t1E4iGKCx79IZFsysKiuf+\n+E0i6iGvvI6UcbcZxVxQj2TplJkFuoX5kDgClIX9Dr9y6aJ4SCh+GRhvHl+DTaz0\nnCvghachHZtIeztRDqxWApjOOzs93dSelrviMXDr8fqyEAGg7YIhgui0aZBsWCen\nAgMBAAGjgdUwgdIwgbAGA1UdIwSBqDCBpYAUNpx1Pc6cGA7KqEwHMmHBTZMA7lSh\ngYmkgYYwgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVU4IBATAd\nBgNVHQ4EFgQULueOfsjZZOHwJHZwBy6u0swnpccwDQYJKoZIhvcNAQEFBQADggEB\nAFNuqSVUaotUJoWDv4z7Kbi6JFpTjDht5ORw4BdVYlRD4h9DACAFzPrPV2ym/Osp\nhNMdZq6msZku7MdOSQVhdeGWrSNk3M8O9Hg7cVzPNXOF3iNoo3irQ5tURut44xs4\nWw5YWQqS9WyUY5snD8tm7Y1rQTPfhg+678xIq/zWCv/u+FSnfVv1nlhLVQkEeG/Y\ngh1uMaTIpUKTGEjIAGtpGP7wwIcXptR/HyfzhTUSTaWc1Ef7zoKT9LL5z3IV1hC2\njVWz+RwYs98LjMuksJFoHqRfWyYhCIym0jb6GTwaEmpxAjc+d7OLNQdnoEGoUYGP\nYjtfkRYg265ESMA+Kww4Xy8=\n-----END CERTIFICATE-----\n",
+ "intermediateCertificate":"-----BEGIN CERTIFICATE-----\nMIIDtTCCAp2gAwIBAgIBATANBgkqhkiG9w0BAQUFADCBgzEZMBcGA1UEAxMQVGVz\ndCBDQSBTVHViIEtleTEXMBUGA1UECxMOUGxhdGZvcm0gTGJhYXMxGjAYBgNVBAoT\nEVJhY2tzcGFjZSBIb3N0aW5nMRQwEgYDVQQHEwtTYW4gQW50b25pbzEOMAwGA1UE\nCBMFVGV4YXMxCzAJBgNVBAYTAlVTMB4XDTEyMDEwOTE5NDU0OVoXDTE0MDEwODE5\nNDU0OVowgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVUzCCASIw\nDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANNh55lwTVwQvNoEZjq1zGdYz9jA\nXXdjizn8AJhjHLOAallfPtvCfTEgKanhdoyz5FnhQE8HbDAop/KNS1lN2UMvdl5f\nZNLTSjJrNtedqxQwxN/i3bpyBxNVejUH2NjV1mmyj+5CJYwCzWalvI/gLPq/A3as\nO2EQqtf3U8unRgn0zXLRdYxV9MrUzNAmdipPNvNrsVdrCgA42rgF/8KsyRVQfJCX\nfN7PGCfrsC3YaUvhymraWxNnXIzMYTNa9wEeBZLUw8SlEtpa1Zsvui+TPXu3USNZ\nVnWH8Lb6ENlnoX0VBwo62fjOG3JzhNKoJawi3bRqyDdINOvafr7iPrrs/T8CAwEA\nAaMyMDAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUNpx1Pc6cGA7KqEwHMmHB\nTZMA7lQwDQYJKoZIhvcNAQEFBQADggEBAMoRgH3iTG3t317viLKoY+lNMHUgHuR7\nb3mn9MidJKyYVewe6hCDIN6WY4fUojmMW9wFJWJIo0hRMNHL3n3tq8HP2j20Mxy8\nacPdfGZJa+jiBw72CrIGdobKaFduIlIEDBA1pNdZIJ+EulrtqrMesnIt92WaypIS\n8JycbIgDMCiyC0ENHEk8UWlC6429c7OZAsplMTbHME/1R4btxjkdfrYZJjdJ2yL2\n8cjZDUDMCPTdW/ycP07Gkq30RB5tACB5aZdaCn2YaKC8FsEdhff4X7xEOfOEHWEq\nSRxADDj8Lx1MT6QpR07hCiDyHfTCtbqzI0iGjX63Oh7xXSa0f+JVTa8=\n-----END CERTIFICATE-----\n"
+ }
+}
+`)
+ })
+}
+
+func mockUpdateCertResponse(t *testing.T, lbID, certID int) {
+ th.Mux.HandleFunc(_certURL(lbID, certID), 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, `
+{
+ "certificateMapping": {
+ "hostName": "rackspace.com",
+ "privateKey":"-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEAwIudSMpRZx7TS0/AtDVX3DgXwLD9g+XrNaoazlhwhpYALgzJ\nLAbAnOxT6OT0gTpkPus/B7QhW6y6Auf2cdBeW31XoIwPsSoyNhxgErGBxzNARRB9\nlI1HCa1ojFrcULluj4W6rpaOycI5soDBJiJHin/hbZBPZq6vhPCuNP7Ya48Zd/2X\nCQ9ft3XKfmbs1SdrdROIhigse/SGRbMrCorn/vhNIuohr7yOlHG3GcVcUI9k6ZSZ\nBbqF+ZA4ApSF/Q6/cumieEgofhkYbx5fg02s9Jwr4IWnIR2bSHs7UQ6sVgKYzjs7\nPd3Unpa74jFw6/H6shABoO2CIYLotGmQbFgnpwIDAQABAoIBAQCBCQ+PCIclJHNV\ntUzfeCA5ZR4F9JbxHdRTUnxEbOB8UWotckQfTScoAvj4yvdQ42DrCZxj/UOdvFOs\nPufZvlp91bIz1alugWjE+p8n5+2hIaegoTyHoWZKBfxak0myj5KYfHZvKlbmv1ML\nXV4TwEVRfAIG+v87QTY/UUxuF5vR+BpKIbgUJLfPUFFvJUdl84qsJ44pToxaYUd/\nh5YAGC00U4ay1KVSAUnTkkPNZ0lPG/rWU6w6WcTvNRLMd8DzFLTKLOgQfHhbExAF\n+sXPWjWSzbBRP1O7fHqq96QQh4VFiY/7w9W+sDKQyV6Ul17OSXs6aZ4f+lq4rJTI\n1FG96YiBAoGBAO1tiH0h1oWDBYfJB3KJJ6CQQsDGwtHo/DEgznFVP4XwEVbZ98Ha\nBfBCn3sAybbaikyCV1Hwj7kfHMZPDHbrcUSFX7quu/2zPK+wO3lZKXSyu4YsguSa\nRedInN33PpdnlPhLyQdWSuD5sVHJDF6xn22vlyxeILH3ooLg2WOFMPmVAoGBAM+b\nUG/a7iyfpAQKYyuFAsXz6SeFaDY+ZYeX45L112H8Pu+Ie/qzon+bzLB9FIH8GP6+\nQpQgmm/p37U2gD1zChUv7iW6OfQBKk9rWvMpfRF6d7YHquElejhizfTZ+ntBV/VY\ndOYEczxhrdW7keLpatYaaWUy/VboRZmlz/9JGqVLAoGAHfqNmFc0cgk4IowEj7a3\ntTNh6ltub/i+FynwRykfazcDyXaeLPDtfQe8gVh5H8h6W+y9P9BjJVnDVVrX1RAn\nbiJ1EupLPF5sVDapW8ohTOXgfbGTGXBNUUW+4Nv+IDno+mz/RhjkPYHpnM0I7c/5\ntGzOZsC/2hjNgT8I0+MWav0CgYEAuULdJeQVlKalI6HtW2Gn1uRRVJ49H+LQkY6e\nW3+cw2jo9LI0CMWSphNvNrN3wIMp/vHj0fHCP0pSApDvIWbuQXfzKaGko7UCf7rK\nf6GvZRCHkV4IREBAb97j8bMvThxClMNqFfU0rFZyXP+0MOyhFQyertswrgQ6T+Fi\n2mnvKD8CgYAmJHP3NTDRMoMRyAzonJ6nEaGUbAgNmivTaUWMe0+leCvAdwD89gzC\nTKbm3eDUg/6Va3X6ANh3wsfIOe4RXXxcbcFDk9R4zO2M5gfLSjYm5Q87EBZ2hrdj\nM2gLI7dt6thx0J8lR8xRHBEMrVBdgwp0g1gQzo5dAV88/kpkZVps8Q==\n-----END RSA PRIVATE KEY-----\n",
+ "certificate":"-----BEGIN CERTIFICATE-----\nMIIEXTCCA0WgAwIBAgIGATTEAjK3MA0GCSqGSIb3DQEBBQUAMIGDMRkwFwYDVQQD\nExBUZXN0IENBIFNUdWIgS2V5MRcwFQYDVQQLEw5QbGF0Zm9ybSBMYmFhczEaMBgG\nA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFDASBgNVBAcTC1NhbiBBbnRvbmlvMQ4w\nDAYDVQQIEwVUZXhhczELMAkGA1UEBhMCVVMwHhcNMTIwMTA5MTk0NjQ1WhcNMTQw\nMTA4MTk0NjQ1WjCBgjELMAkGA1UEBhMCVVMxDjAMBgNVBAgTBVRleGFzMRQwEgYD\nVQQHEwtTYW4gQW50b25pbzEaMBgGA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFzAV\nBgNVBAsTDlBsYXRmb3JtIExiYWFzMRgwFgYDVQQDEw9UZXN0IENsaWVudCBLZXkw\nggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDAi51IylFnHtNLT8C0NVfc\nOBfAsP2D5es1qhrOWHCGlgAuDMksBsCc7FPo5PSBOmQ+6z8HtCFbrLoC5/Zx0F5b\nfVegjA+xKjI2HGASsYHHM0BFEH2UjUcJrWiMWtxQuW6Phbqulo7JwjmygMEmIkeK\nf+FtkE9mrq+E8K40/thrjxl3/ZcJD1+3dcp+ZuzVJ2t1E4iGKCx79IZFsysKiuf+\n+E0i6iGvvI6UcbcZxVxQj2TplJkFuoX5kDgClIX9Dr9y6aJ4SCh+GRhvHl+DTaz0\nnCvghachHZtIeztRDqxWApjOOzs93dSelrviMXDr8fqyEAGg7YIhgui0aZBsWCen\nAgMBAAGjgdUwgdIwgbAGA1UdIwSBqDCBpYAUNpx1Pc6cGA7KqEwHMmHBTZMA7lSh\ngYmkgYYwgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVU4IBATAd\nBgNVHQ4EFgQULueOfsjZZOHwJHZwBy6u0swnpccwDQYJKoZIhvcNAQEFBQADggEB\nAFNuqSVUaotUJoWDv4z7Kbi6JFpTjDht5ORw4BdVYlRD4h9DACAFzPrPV2ym/Osp\nhNMdZq6msZku7MdOSQVhdeGWrSNk3M8O9Hg7cVzPNXOF3iNoo3irQ5tURut44xs4\nWw5YWQqS9WyUY5snD8tm7Y1rQTPfhg+678xIq/zWCv/u+FSnfVv1nlhLVQkEeG/Y\ngh1uMaTIpUKTGEjIAGtpGP7wwIcXptR/HyfzhTUSTaWc1Ef7zoKT9LL5z3IV1hC2\njVWz+RwYs98LjMuksJFoHqRfWyYhCIym0jb6GTwaEmpxAjc+d7OLNQdnoEGoUYGP\nYjtfkRYg265ESMA+Kww4Xy8=\n-----END CERTIFICATE-----\n",
+ "intermediateCertificate":"-----BEGIN CERTIFICATE-----\nMIIDtTCCAp2gAwIBAgIBATANBgkqhkiG9w0BAQUFADCBgzEZMBcGA1UEAxMQVGVz\ndCBDQSBTVHViIEtleTEXMBUGA1UECxMOUGxhdGZvcm0gTGJhYXMxGjAYBgNVBAoT\nEVJhY2tzcGFjZSBIb3N0aW5nMRQwEgYDVQQHEwtTYW4gQW50b25pbzEOMAwGA1UE\nCBMFVGV4YXMxCzAJBgNVBAYTAlVTMB4XDTEyMDEwOTE5NDU0OVoXDTE0MDEwODE5\nNDU0OVowgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVUzCCASIw\nDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANNh55lwTVwQvNoEZjq1zGdYz9jA\nXXdjizn8AJhjHLOAallfPtvCfTEgKanhdoyz5FnhQE8HbDAop/KNS1lN2UMvdl5f\nZNLTSjJrNtedqxQwxN/i3bpyBxNVejUH2NjV1mmyj+5CJYwCzWalvI/gLPq/A3as\nO2EQqtf3U8unRgn0zXLRdYxV9MrUzNAmdipPNvNrsVdrCgA42rgF/8KsyRVQfJCX\nfN7PGCfrsC3YaUvhymraWxNnXIzMYTNa9wEeBZLUw8SlEtpa1Zsvui+TPXu3USNZ\nVnWH8Lb6ENlnoX0VBwo62fjOG3JzhNKoJawi3bRqyDdINOvafr7iPrrs/T8CAwEA\nAaMyMDAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUNpx1Pc6cGA7KqEwHMmHB\nTZMA7lQwDQYJKoZIhvcNAQEFBQADggEBAMoRgH3iTG3t317viLKoY+lNMHUgHuR7\nb3mn9MidJKyYVewe6hCDIN6WY4fUojmMW9wFJWJIo0hRMNHL3n3tq8HP2j20Mxy8\nacPdfGZJa+jiBw72CrIGdobKaFduIlIEDBA1pNdZIJ+EulrtqrMesnIt92WaypIS\n8JycbIgDMCiyC0ENHEk8UWlC6429c7OZAsplMTbHME/1R4btxjkdfrYZJjdJ2yL2\n8cjZDUDMCPTdW/ycP07Gkq30RB5tACB5aZdaCn2YaKC8FsEdhff4X7xEOfOEHWEq\nSRxADDj8Lx1MT6QpR07hCiDyHfTCtbqzI0iGjX63Oh7xXSa0f+JVTa8=\n-----END CERTIFICATE-----\n"
+ }
+}
+ `)
+
+ w.WriteHeader(http.StatusAccepted)
+
+ fmt.Fprintf(w, `
+{
+ "certificateMapping": {
+ "id": 123,
+ "hostName": "rackspace.com",
+ "certificate":"-----BEGIN CERTIFICATE-----\nMIIEXTCCA0WgAwIBAgIGATTEAjK3MA0GCSqGSIb3DQEBBQUAMIGDMRkwFwYDVQQD\nExBUZXN0IENBIFNUdWIgS2V5MRcwFQYDVQQLEw5QbGF0Zm9ybSBMYmFhczEaMBgG\nA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFDASBgNVBAcTC1NhbiBBbnRvbmlvMQ4w\nDAYDVQQIEwVUZXhhczELMAkGA1UEBhMCVVMwHhcNMTIwMTA5MTk0NjQ1WhcNMTQw\nMTA4MTk0NjQ1WjCBgjELMAkGA1UEBhMCVVMxDjAMBgNVBAgTBVRleGFzMRQwEgYD\nVQQHEwtTYW4gQW50b25pbzEaMBgGA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFzAV\nBgNVBAsTDlBsYXRmb3JtIExiYWFzMRgwFgYDVQQDEw9UZXN0IENsaWVudCBLZXkw\nggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDAi51IylFnHtNLT8C0NVfc\nOBfAsP2D5es1qhrOWHCGlgAuDMksBsCc7FPo5PSBOmQ+6z8HtCFbrLoC5/Zx0F5b\nfVegjA+xKjI2HGASsYHHM0BFEH2UjUcJrWiMWtxQuW6Phbqulo7JwjmygMEmIkeK\nf+FtkE9mrq+E8K40/thrjxl3/ZcJD1+3dcp+ZuzVJ2t1E4iGKCx79IZFsysKiuf+\n+E0i6iGvvI6UcbcZxVxQj2TplJkFuoX5kDgClIX9Dr9y6aJ4SCh+GRhvHl+DTaz0\nnCvghachHZtIeztRDqxWApjOOzs93dSelrviMXDr8fqyEAGg7YIhgui0aZBsWCen\nAgMBAAGjgdUwgdIwgbAGA1UdIwSBqDCBpYAUNpx1Pc6cGA7KqEwHMmHBTZMA7lSh\ngYmkgYYwgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVU4IBATAd\nBgNVHQ4EFgQULueOfsjZZOHwJHZwBy6u0swnpccwDQYJKoZIhvcNAQEFBQADggEB\nAFNuqSVUaotUJoWDv4z7Kbi6JFpTjDht5ORw4BdVYlRD4h9DACAFzPrPV2ym/Osp\nhNMdZq6msZku7MdOSQVhdeGWrSNk3M8O9Hg7cVzPNXOF3iNoo3irQ5tURut44xs4\nWw5YWQqS9WyUY5snD8tm7Y1rQTPfhg+678xIq/zWCv/u+FSnfVv1nlhLVQkEeG/Y\ngh1uMaTIpUKTGEjIAGtpGP7wwIcXptR/HyfzhTUSTaWc1Ef7zoKT9LL5z3IV1hC2\njVWz+RwYs98LjMuksJFoHqRfWyYhCIym0jb6GTwaEmpxAjc+d7OLNQdnoEGoUYGP\nYjtfkRYg265ESMA+Kww4Xy8=\n-----END CERTIFICATE-----\n",
+ "intermediateCertificate":"-----BEGIN CERTIFICATE-----\nMIIDtTCCAp2gAwIBAgIBATANBgkqhkiG9w0BAQUFADCBgzEZMBcGA1UEAxMQVGVz\ndCBDQSBTVHViIEtleTEXMBUGA1UECxMOUGxhdGZvcm0gTGJhYXMxGjAYBgNVBAoT\nEVJhY2tzcGFjZSBIb3N0aW5nMRQwEgYDVQQHEwtTYW4gQW50b25pbzEOMAwGA1UE\nCBMFVGV4YXMxCzAJBgNVBAYTAlVTMB4XDTEyMDEwOTE5NDU0OVoXDTE0MDEwODE5\nNDU0OVowgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVUzCCASIw\nDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANNh55lwTVwQvNoEZjq1zGdYz9jA\nXXdjizn8AJhjHLOAallfPtvCfTEgKanhdoyz5FnhQE8HbDAop/KNS1lN2UMvdl5f\nZNLTSjJrNtedqxQwxN/i3bpyBxNVejUH2NjV1mmyj+5CJYwCzWalvI/gLPq/A3as\nO2EQqtf3U8unRgn0zXLRdYxV9MrUzNAmdipPNvNrsVdrCgA42rgF/8KsyRVQfJCX\nfN7PGCfrsC3YaUvhymraWxNnXIzMYTNa9wEeBZLUw8SlEtpa1Zsvui+TPXu3USNZ\nVnWH8Lb6ENlnoX0VBwo62fjOG3JzhNKoJawi3bRqyDdINOvafr7iPrrs/T8CAwEA\nAaMyMDAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUNpx1Pc6cGA7KqEwHMmHB\nTZMA7lQwDQYJKoZIhvcNAQEFBQADggEBAMoRgH3iTG3t317viLKoY+lNMHUgHuR7\nb3mn9MidJKyYVewe6hCDIN6WY4fUojmMW9wFJWJIo0hRMNHL3n3tq8HP2j20Mxy8\nacPdfGZJa+jiBw72CrIGdobKaFduIlIEDBA1pNdZIJ+EulrtqrMesnIt92WaypIS\n8JycbIgDMCiyC0ENHEk8UWlC6429c7OZAsplMTbHME/1R4btxjkdfrYZJjdJ2yL2\n8cjZDUDMCPTdW/ycP07Gkq30RB5tACB5aZdaCn2YaKC8FsEdhff4X7xEOfOEHWEq\nSRxADDj8Lx1MT6QpR07hCiDyHfTCtbqzI0iGjX63Oh7xXSa0f+JVTa8=\n-----END CERTIFICATE-----\n"
+ }
+}
+ `)
+ })
+}
+
+func mockDeleteCertResponse(t *testing.T, lbID, certID int) {
+ th.Mux.HandleFunc(_certURL(lbID, certID), func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "DELETE")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ w.WriteHeader(http.StatusOK)
+ })
+}
diff --git a/rackspace/lb/v1/ssl/requests.go b/rackspace/lb/v1/ssl/requests.go
new file mode 100644
index 0000000..84b2712
--- /dev/null
+++ b/rackspace/lb/v1/ssl/requests.go
@@ -0,0 +1,278 @@
+package ssl
+
+import (
+ "errors"
+
+ "github.com/racker/perigee"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+var (
+ errPrivateKey = errors.New("PrivateKey is a required field")
+ errCertificate = errors.New("Certificate is a required field")
+ errIntCertificate = errors.New("IntCertificate is a required field")
+)
+
+// UpdateOptsBuilder is the interface options structs have to satisfy in order
+// to be used in the main Update operation in this package.
+type UpdateOptsBuilder interface {
+ ToSSLUpdateMap() (map[string]interface{}, error)
+}
+
+// UpdateOpts is the common options struct used in this package's Update
+// operation.
+type UpdateOpts struct {
+ // Required - consult the SSLTermConfig struct for more info.
+ SecurePort int
+
+ // Required - consult the SSLTermConfig struct for more info.
+ PrivateKey string
+
+ // Required - consult the SSLTermConfig struct for more info.
+ Certificate string
+
+ // Required - consult the SSLTermConfig struct for more info.
+ IntCertificate string
+
+ // Optional - consult the SSLTermConfig struct for more info.
+ Enabled *bool
+
+ // Optional - consult the SSLTermConfig struct for more info.
+ SecureTrafficOnly *bool
+}
+
+// ToSSLUpdateMap casts a CreateOpts struct to a map.
+func (opts UpdateOpts) ToSSLUpdateMap() (map[string]interface{}, error) {
+ ssl := make(map[string]interface{})
+
+ if opts.SecurePort == 0 {
+ return ssl, errors.New("SecurePort needs to be an integer greater than 0")
+ }
+ if opts.PrivateKey == "" {
+ return ssl, errPrivateKey
+ }
+ if opts.Certificate == "" {
+ return ssl, errCertificate
+ }
+ if opts.IntCertificate == "" {
+ return ssl, errIntCertificate
+ }
+
+ ssl["securePort"] = opts.SecurePort
+ ssl["privateKey"] = opts.PrivateKey
+ ssl["certificate"] = opts.Certificate
+ ssl["intermediateCertificate"] = opts.IntCertificate
+
+ if opts.Enabled != nil {
+ ssl["enabled"] = &opts.Enabled
+ }
+
+ if opts.SecureTrafficOnly != nil {
+ ssl["secureTrafficOnly"] = &opts.SecureTrafficOnly
+ }
+
+ return map[string]interface{}{"sslTermination": ssl}, nil
+}
+
+// Update is the operation responsible for updating the SSL Termination
+// configuration for a load balancer.
+func Update(c *gophercloud.ServiceClient, lbID int, opts UpdateOptsBuilder) UpdateResult {
+ var res UpdateResult
+
+ reqBody, err := opts.ToSSLUpdateMap()
+ if err != nil {
+ res.Err = err
+ return res
+ }
+
+ _, res.Err = perigee.Request("PUT", rootURL(c, lbID), perigee.Options{
+ MoreHeaders: c.AuthenticatedHeaders(),
+ ReqBody: &reqBody,
+ Results: &res.Body,
+ OkCodes: []int{200},
+ })
+
+ return res
+}
+
+// Get is the operation responsible for showing the details of the SSL
+// Termination configuration for a load balancer.
+func Get(c *gophercloud.ServiceClient, lbID int) GetResult {
+ var res GetResult
+
+ _, res.Err = perigee.Request("GET", rootURL(c, lbID), perigee.Options{
+ MoreHeaders: c.AuthenticatedHeaders(),
+ Results: &res.Body,
+ OkCodes: []int{200},
+ })
+
+ return res
+}
+
+// Delete is the operation responsible for deleting the SSL Termination
+// configuration for a load balancer.
+func Delete(c *gophercloud.ServiceClient, lbID int) DeleteResult {
+ var res DeleteResult
+
+ _, res.Err = perigee.Request("DELETE", rootURL(c, lbID), perigee.Options{
+ MoreHeaders: c.AuthenticatedHeaders(),
+ OkCodes: []int{200},
+ })
+
+ return res
+}
+
+// ListCerts will list all of the certificate mappings associated with a
+// SSL-terminated HTTP load balancer.
+func ListCerts(c *gophercloud.ServiceClient, lbID int) pagination.Pager {
+ url := certURL(c, lbID)
+ return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page {
+ return CertPage{pagination.LinkedPageBase{PageResult: r}}
+ })
+}
+
+// CreateCertOptsBuilder is the interface options structs have to satisfy in
+// order to be used in the AddCert operation in this package.
+type CreateCertOptsBuilder interface {
+ ToCertCreateMap() (map[string]interface{}, error)
+}
+
+// CreateCertOpts represents the options used when adding a new certificate mapping.
+type CreateCertOpts struct {
+ HostName string
+ PrivateKey string
+ Certificate string
+ IntCertificate string
+}
+
+// ToCertCreateMap will cast an CreateCertOpts struct to a map for JSON serialization.
+func (opts CreateCertOpts) ToCertCreateMap() (map[string]interface{}, error) {
+ cm := make(map[string]interface{})
+
+ if opts.HostName == "" {
+ return cm, errors.New("HostName is a required option")
+ }
+ if opts.PrivateKey == "" {
+ return cm, errPrivateKey
+ }
+ if opts.Certificate == "" {
+ return cm, errCertificate
+ }
+
+ cm["hostName"] = opts.HostName
+ cm["privateKey"] = opts.PrivateKey
+ cm["certificate"] = opts.Certificate
+
+ if opts.IntCertificate != "" {
+ cm["intermediateCertificate"] = opts.IntCertificate
+ }
+
+ return map[string]interface{}{"certificateMapping": cm}, nil
+}
+
+// CreateCert will add a new SSL certificate and allow an SSL-terminated HTTP
+// load balancer to use it. This feature is useful because it allows multiple
+// certificates to be used. The maximum number of certificates that can be
+// stored per LB is 20.
+func CreateCert(c *gophercloud.ServiceClient, lbID int, opts CreateCertOptsBuilder) CreateCertResult {
+ var res CreateCertResult
+
+ reqBody, err := opts.ToCertCreateMap()
+ if err != nil {
+ res.Err = err
+ return res
+ }
+
+ _, res.Err = perigee.Request("POST", certURL(c, lbID), perigee.Options{
+ MoreHeaders: c.AuthenticatedHeaders(),
+ ReqBody: &reqBody,
+ Results: &res.Body,
+ OkCodes: []int{200},
+ })
+
+ return res
+}
+
+// GetCert will show the details of an existing SSL certificate.
+func GetCert(c *gophercloud.ServiceClient, lbID, certID int) GetCertResult {
+ var res GetCertResult
+
+ _, res.Err = perigee.Request("GET", certResourceURL(c, lbID, certID), perigee.Options{
+ MoreHeaders: c.AuthenticatedHeaders(),
+ Results: &res.Body,
+ OkCodes: []int{200},
+ })
+
+ return res
+}
+
+// UpdateCertOptsBuilder is the interface options structs have to satisfy in
+// order to be used in the UpdateCert operation in this package.
+type UpdateCertOptsBuilder interface {
+ ToCertUpdateMap() (map[string]interface{}, error)
+}
+
+// UpdateCertOpts represents the options needed to update a SSL certificate.
+type UpdateCertOpts struct {
+ HostName string
+ PrivateKey string
+ Certificate string
+ IntCertificate string
+}
+
+// ToCertUpdateMap will cast an UpdateCertOpts struct into a map for JSON
+// seralization.
+func (opts UpdateCertOpts) ToCertUpdateMap() (map[string]interface{}, error) {
+ cm := make(map[string]interface{})
+
+ if opts.HostName != "" {
+ cm["hostName"] = opts.HostName
+ }
+ if opts.PrivateKey != "" {
+ cm["privateKey"] = opts.PrivateKey
+ }
+ if opts.Certificate != "" {
+ cm["certificate"] = opts.Certificate
+ }
+ if opts.IntCertificate != "" {
+ cm["intermediateCertificate"] = opts.IntCertificate
+ }
+
+ return map[string]interface{}{"certificateMapping": cm}, nil
+}
+
+// UpdateCert is the operation responsible for updating the details of an
+// existing SSL certificate.
+func UpdateCert(c *gophercloud.ServiceClient, lbID, certID int, opts UpdateCertOptsBuilder) UpdateCertResult {
+ var res UpdateCertResult
+
+ reqBody, err := opts.ToCertUpdateMap()
+ if err != nil {
+ res.Err = err
+ return res
+ }
+
+ _, res.Err = perigee.Request("PUT", certResourceURL(c, lbID, certID), perigee.Options{
+ MoreHeaders: c.AuthenticatedHeaders(),
+ ReqBody: &reqBody,
+ Results: &res.Body,
+ OkCodes: []int{202},
+ })
+
+ return res
+}
+
+// DeleteCert is the operation responsible for permanently removing a SSL
+// certificate.
+func DeleteCert(c *gophercloud.ServiceClient, lbID, certID int) DeleteResult {
+ var res DeleteResult
+
+ _, res.Err = perigee.Request("DELETE", certResourceURL(c, lbID, certID), perigee.Options{
+ MoreHeaders: c.AuthenticatedHeaders(),
+ OkCodes: []int{200},
+ })
+
+ return res
+}
diff --git a/rackspace/lb/v1/ssl/requests_test.go b/rackspace/lb/v1/ssl/requests_test.go
new file mode 100644
index 0000000..fb14c4a
--- /dev/null
+++ b/rackspace/lb/v1/ssl/requests_test.go
@@ -0,0 +1,167 @@
+package ssl
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+ "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+const (
+ lbID = 12345
+ certID = 67890
+)
+
+func TestGet(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockGetResponse(t, lbID)
+
+ sp, err := Get(client.ServiceClient(), lbID).Extract()
+ th.AssertNoErr(t, err)
+
+ expected := &SSLTermConfig{
+ Certificate: "-----BEGIN CERTIFICATE-----\nMIIEXTCCA0WgAwIBAgIGATTEAjK3MA0GCSqGSIb3DQEBBQUAMIGDMRkwFwYDVQQD\nExBUZXN0IENBIFNUdWIgS2V5MRcwFQYDVQQLEw5QbGF0Zm9ybSBMYmFhczEaMBgG\nA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFDASBgNVBAcTC1NhbiBBbnRvbmlvMQ4w\nDAYDVQQIEwVUZXhhczELMAkGA1UEBhMCVVMwHhcNMTIwMTA5MTk0NjQ1WhcNMTQw\nMTA4MTk0NjQ1WjCBgjELMAkGA1UEBhMCVVMxDjAMBgNVBAgTBVRleGFzMRQwEgYD\nVQQHEwtTYW4gQW50b25pbzEaMBgGA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFzAV\nBgNVBAsTDlBsYXRmb3JtIExiYWFzMRgwFgYDVQQDEw9UZXN0IENsaWVudCBLZXkw\nggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDAi51IylFnHtNLT8C0NVfc\nOBfAsP2D5es1qhrOWHCGlgAuDMksBsCc7FPo5PSBOmQ+6z8HtCFbrLoC5/Zx0F5b\nfVegjA+xKjI2HGASsYHHM0BFEH2UjUcJrWiMWtxQuW6Phbqulo7JwjmygMEmIkeK\nf+FtkE9mrq+E8K40/thrjxl3/ZcJD1+3dcp+ZuzVJ2t1E4iGKCx79IZFsysKiuf+\n+E0i6iGvvI6UcbcZxVxQj2TplJkFuoX5kDgClIX9Dr9y6aJ4SCh+GRhvHl+DTaz0\nnCvghachHZtIeztRDqxWApjOOzs93dSelrviMXDr8fqyEAGg7YIhgui0aZBsWCen\nAgMBAAGjgdUwgdIwgbAGA1UdIwSBqDCBpYAUNpx1Pc6cGA7KqEwHMmHBTZMA7lSh\ngYmkgYYwgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVU4IBATAd\nBgNVHQ4EFgQULueOfsjZZOHwJHZwBy6u0swnpccwDQYJKoZIhvcNAQEFBQADggEB\nAFNuqSVUaotUJoWDv4z7Kbi6JFpTjDht5ORw4BdVYlRD4h9DACAFzPrPV2ym/Osp\nhNMdZq6msZku7MdOSQVhdeGWrSNk3M8O9Hg7cVzPNXOF3iNoo3irQ5tURut44xs4\nWw5YWQqS9WyUY5snD8tm7Y1rQTPfhg+678xIq/zWCv/u+FSnfVv1nlhLVQkEeG/Y\ngh1uMaTIpUKTGEjIAGtpGP7wwIcXptR/HyfzhTUSTaWc1Ef7zoKT9LL5z3IV1hC2\njVWz+RwYs98LjMuksJFoHqRfWyYhCIym0jb6GTwaEmpxAjc+d7OLNQdnoEGoUYGP\nYjtfkRYg265ESMA+Kww4Xy8=\n-----END CERTIFICATE-----\n",
+ Enabled: true,
+ SecureTrafficOnly: false,
+ IntCertificate: "-----BEGIN CERTIFICATE-----\nMIIDtTCCAp2gAwIBAgIBATANBgkqhkiG9w0BAQUFADCBgzEZMBcGA1UEAxMQVGVz\ndCBDQSBTVHViIEtleTEXMBUGA1UECxMOUGxhdGZvcm0gTGJhYXMxGjAYBgNVBAoT\nEVJhY2tzcGFjZSBIb3N0aW5nMRQwEgYDVQQHEwtTYW4gQW50b25pbzEOMAwGA1UE\nCBMFVGV4YXMxCzAJBgNVBAYTAlVTMB4XDTEyMDEwOTE5NDU0OVoXDTE0MDEwODE5\nNDU0OVowgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVUzCCASIw\nDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANNh55lwTVwQvNoEZjq1zGdYz9jA\nXXdjizn8AJhjHLOAallfPtvCfTEgKanhdoyz5FnhQE8HbDAop/KNS1lN2UMvdl5f\nZNLTSjJrNtedqxQwxN/i3bpyBxNVejUH2NjV1mmyj+5CJYwCzWalvI/gLPq/A3as\nO2EQqtf3U8unRgn0zXLRdYxV9MrUzNAmdipPNvNrsVdrCgA42rgF/8KsyRVQfJCX\nfN7PGCfrsC3YaUvhymraWxNnXIzMYTNa9wEeBZLUw8SlEtpa1Zsvui+TPXu3USNZ\nVnWH8Lb6ENlnoX0VBwo62fjOG3JzhNKoJawi3bRqyDdINOvafr7iPrrs/T8CAwEA\nAaMyMDAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUNpx1Pc6cGA7KqEwHMmHB\nTZMA7lQwDQYJKoZIhvcNAQEFBQADggEBAMoRgH3iTG3t317viLKoY+lNMHUgHuR7\nb3mn9MidJKyYVewe6hCDIN6WY4fUojmMW9wFJWJIo0hRMNHL3n3tq8HP2j20Mxy8\nacPdfGZJa+jiBw72CrIGdobKaFduIlIEDBA1pNdZIJ+EulrtqrMesnIt92WaypIS\n8JycbIgDMCiyC0ENHEk8UWlC6429c7OZAsplMTbHME/1R4btxjkdfrYZJjdJ2yL2\n8cjZDUDMCPTdW/ycP07Gkq30RB5tACB5aZdaCn2YaKC8FsEdhff4X7xEOfOEHWEq\nSRxADDj8Lx1MT6QpR07hCiDyHfTCtbqzI0iGjX63Oh7xXSa0f+JVTa8=\n-----END CERTIFICATE-----\n",
+ SecurePort: 443,
+ }
+ th.AssertDeepEquals(t, expected, sp)
+}
+
+func TestUpdate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockUpdateResponse(t, lbID)
+
+ opts := UpdateOpts{
+ Enabled: gophercloud.Enabled,
+ SecurePort: 443,
+ SecureTrafficOnly: gophercloud.Disabled,
+ PrivateKey: "foo",
+ Certificate: "-----BEGIN CERTIFICATE-----\nMIIEXTCCA0WgAwIBAgIGATTEAjK3MA0GCSqGSIb3DQEBBQUAMIGDMRkwFwYDVQQD\nExBUZXN0IENBIFNUdWIgS2V5MRcwFQYDVQQLEw5QbGF0Zm9ybSBMYmFhczEaMBgG\nA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFDASBgNVBAcTC1NhbiBBbnRvbmlvMQ4w\nDAYDVQQIEwVUZXhhczELMAkGA1UEBhMCVVMwHhcNMTIwMTA5MTk0NjQ1WhcNMTQw\nMTA4MTk0NjQ1WjCBgjELMAkGA1UEBhMCVVMxDjAMBgNVBAgTBVRleGFzMRQwEgYD\nVQQHEwtTYW4gQW50b25pbzEaMBgGA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFzAV\nBgNVBAsTDlBsYXRmb3JtIExiYWFzMRgwFgYDVQQDEw9UZXN0IENsaWVudCBLZXkw\nggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDAi51IylFnHtNLT8C0NVfc\nOBfAsP2D5es1qhrOWHCGlgAuDMksBsCc7FPo5PSBOmQ+6z8HtCFbrLoC5/Zx0F5b\nfVegjA+xKjI2HGASsYHHM0BFEH2UjUcJrWiMWtxQuW6Phbqulo7JwjmygMEmIkeK\nf+FtkE9mrq+E8K40/thrjxl3/ZcJD1+3dcp+ZuzVJ2t1E4iGKCx79IZFsysKiuf+\n+E0i6iGvvI6UcbcZxVxQj2TplJkFuoX5kDgClIX9Dr9y6aJ4SCh+GRhvHl+DTaz0\nnCvghachHZtIeztRDqxWApjOOzs93dSelrviMXDr8fqyEAGg7YIhgui0aZBsWCen\nAgMBAAGjgdUwgdIwgbAGA1UdIwSBqDCBpYAUNpx1Pc6cGA7KqEwHMmHBTZMA7lSh\ngYmkgYYwgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVU4IBATAd\nBgNVHQ4EFgQULueOfsjZZOHwJHZwBy6u0swnpccwDQYJKoZIhvcNAQEFBQADggEB\nAFNuqSVUaotUJoWDv4z7Kbi6JFpTjDht5ORw4BdVYlRD4h9DACAFzPrPV2ym/Osp\nhNMdZq6msZku7MdOSQVhdeGWrSNk3M8O9Hg7cVzPNXOF3iNoo3irQ5tURut44xs4\nWw5YWQqS9WyUY5snD8tm7Y1rQTPfhg+678xIq/zWCv/u+FSnfVv1nlhLVQkEeG/Y\ngh1uMaTIpUKTGEjIAGtpGP7wwIcXptR/HyfzhTUSTaWc1Ef7zoKT9LL5z3IV1hC2\njVWz+RwYs98LjMuksJFoHqRfWyYhCIym0jb6GTwaEmpxAjc+d7OLNQdnoEGoUYGP\nYjtfkRYg265ESMA+Kww4Xy8=\n-----END CERTIFICATE-----\n",
+ IntCertificate: "-----BEGIN CERTIFICATE-----\nMIIDtTCCAp2gAwIBAgIBATANBgkqhkiG9w0BAQUFADCBgzEZMBcGA1UEAxMQVGVz\ndCBDQSBTVHViIEtleTEXMBUGA1UECxMOUGxhdGZvcm0gTGJhYXMxGjAYBgNVBAoT\nEVJhY2tzcGFjZSBIb3N0aW5nMRQwEgYDVQQHEwtTYW4gQW50b25pbzEOMAwGA1UE\nCBMFVGV4YXMxCzAJBgNVBAYTAlVTMB4XDTEyMDEwOTE5NDU0OVoXDTE0MDEwODE5\nNDU0OVowgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVUzCCASIw\nDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANNh55lwTVwQvNoEZjq1zGdYz9jA\nXXdjizn8AJhjHLOAallfPtvCfTEgKanhdoyz5FnhQE8HbDAop/KNS1lN2UMvdl5f\nZNLTSjJrNtedqxQwxN/i3bpyBxNVejUH2NjV1mmyj+5CJYwCzWalvI/gLPq/A3as\nO2EQqtf3U8unRgn0zXLRdYxV9MrUzNAmdipPNvNrsVdrCgA42rgF/8KsyRVQfJCX\nfN7PGCfrsC3YaUvhymraWxNnXIzMYTNa9wEeBZLUw8SlEtpa1Zsvui+TPXu3USNZ\nVnWH8Lb6ENlnoX0VBwo62fjOG3JzhNKoJawi3bRqyDdINOvafr7iPrrs/T8CAwEA\nAaMyMDAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUNpx1Pc6cGA7KqEwHMmHB\nTZMA7lQwDQYJKoZIhvcNAQEFBQADggEBAMoRgH3iTG3t317viLKoY+lNMHUgHuR7\nb3mn9MidJKyYVewe6hCDIN6WY4fUojmMW9wFJWJIo0hRMNHL3n3tq8HP2j20Mxy8\nacPdfGZJa+jiBw72CrIGdobKaFduIlIEDBA1pNdZIJ+EulrtqrMesnIt92WaypIS\n8JycbIgDMCiyC0ENHEk8UWlC6429c7OZAsplMTbHME/1R4btxjkdfrYZJjdJ2yL2\n8cjZDUDMCPTdW/ycP07Gkq30RB5tACB5aZdaCn2YaKC8FsEdhff4X7xEOfOEHWEq\nSRxADDj8Lx1MT6QpR07hCiDyHfTCtbqzI0iGjX63Oh7xXSa0f+JVTa8=\n-----END CERTIFICATE-----\n",
+ }
+ err := Update(client.ServiceClient(), lbID, opts).ExtractErr()
+ th.AssertNoErr(t, err)
+}
+
+func TestDelete(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockDeleteResponse(t, lbID)
+
+ err := Delete(client.ServiceClient(), lbID).ExtractErr()
+ th.AssertNoErr(t, err)
+}
+
+func TestListCerts(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockListCertsResponse(t, lbID)
+
+ count := 0
+
+ err := ListCerts(client.ServiceClient(), lbID).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractCerts(page)
+ th.AssertNoErr(t, err)
+
+ expected := []Certificate{
+ Certificate{ID: 123, HostName: "rackspace.com"},
+ Certificate{ID: 124, HostName: "*.rackspace.com"},
+ }
+
+ th.CheckDeepEquals(t, expected, actual)
+
+ return true, nil
+ })
+
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, 1, count)
+}
+
+func TestCreateCert(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockAddCertResponse(t, lbID)
+
+ opts := CreateCertOpts{
+ HostName: "rackspace.com",
+ PrivateKey: "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEAwIudSMpRZx7TS0/AtDVX3DgXwLD9g+XrNaoazlhwhpYALgzJ\nLAbAnOxT6OT0gTpkPus/B7QhW6y6Auf2cdBeW31XoIwPsSoyNhxgErGBxzNARRB9\nlI1HCa1ojFrcULluj4W6rpaOycI5soDBJiJHin/hbZBPZq6vhPCuNP7Ya48Zd/2X\nCQ9ft3XKfmbs1SdrdROIhigse/SGRbMrCorn/vhNIuohr7yOlHG3GcVcUI9k6ZSZ\nBbqF+ZA4ApSF/Q6/cumieEgofhkYbx5fg02s9Jwr4IWnIR2bSHs7UQ6sVgKYzjs7\nPd3Unpa74jFw6/H6shABoO2CIYLotGmQbFgnpwIDAQABAoIBAQCBCQ+PCIclJHNV\ntUzfeCA5ZR4F9JbxHdRTUnxEbOB8UWotckQfTScoAvj4yvdQ42DrCZxj/UOdvFOs\nPufZvlp91bIz1alugWjE+p8n5+2hIaegoTyHoWZKBfxak0myj5KYfHZvKlbmv1ML\nXV4TwEVRfAIG+v87QTY/UUxuF5vR+BpKIbgUJLfPUFFvJUdl84qsJ44pToxaYUd/\nh5YAGC00U4ay1KVSAUnTkkPNZ0lPG/rWU6w6WcTvNRLMd8DzFLTKLOgQfHhbExAF\n+sXPWjWSzbBRP1O7fHqq96QQh4VFiY/7w9W+sDKQyV6Ul17OSXs6aZ4f+lq4rJTI\n1FG96YiBAoGBAO1tiH0h1oWDBYfJB3KJJ6CQQsDGwtHo/DEgznFVP4XwEVbZ98Ha\nBfBCn3sAybbaikyCV1Hwj7kfHMZPDHbrcUSFX7quu/2zPK+wO3lZKXSyu4YsguSa\nRedInN33PpdnlPhLyQdWSuD5sVHJDF6xn22vlyxeILH3ooLg2WOFMPmVAoGBAM+b\nUG/a7iyfpAQKYyuFAsXz6SeFaDY+ZYeX45L112H8Pu+Ie/qzon+bzLB9FIH8GP6+\nQpQgmm/p37U2gD1zChUv7iW6OfQBKk9rWvMpfRF6d7YHquElejhizfTZ+ntBV/VY\ndOYEczxhrdW7keLpatYaaWUy/VboRZmlz/9JGqVLAoGAHfqNmFc0cgk4IowEj7a3\ntTNh6ltub/i+FynwRykfazcDyXaeLPDtfQe8gVh5H8h6W+y9P9BjJVnDVVrX1RAn\nbiJ1EupLPF5sVDapW8ohTOXgfbGTGXBNUUW+4Nv+IDno+mz/RhjkPYHpnM0I7c/5\ntGzOZsC/2hjNgT8I0+MWav0CgYEAuULdJeQVlKalI6HtW2Gn1uRRVJ49H+LQkY6e\nW3+cw2jo9LI0CMWSphNvNrN3wIMp/vHj0fHCP0pSApDvIWbuQXfzKaGko7UCf7rK\nf6GvZRCHkV4IREBAb97j8bMvThxClMNqFfU0rFZyXP+0MOyhFQyertswrgQ6T+Fi\n2mnvKD8CgYAmJHP3NTDRMoMRyAzonJ6nEaGUbAgNmivTaUWMe0+leCvAdwD89gzC\nTKbm3eDUg/6Va3X6ANh3wsfIOe4RXXxcbcFDk9R4zO2M5gfLSjYm5Q87EBZ2hrdj\nM2gLI7dt6thx0J8lR8xRHBEMrVBdgwp0g1gQzo5dAV88/kpkZVps8Q==\n-----END RSA PRIVATE KEY-----\n",
+ Certificate: "-----BEGIN CERTIFICATE-----\nMIIEXTCCA0WgAwIBAgIGATTEAjK3MA0GCSqGSIb3DQEBBQUAMIGDMRkwFwYDVQQD\nExBUZXN0IENBIFNUdWIgS2V5MRcwFQYDVQQLEw5QbGF0Zm9ybSBMYmFhczEaMBgG\nA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFDASBgNVBAcTC1NhbiBBbnRvbmlvMQ4w\nDAYDVQQIEwVUZXhhczELMAkGA1UEBhMCVVMwHhcNMTIwMTA5MTk0NjQ1WhcNMTQw\nMTA4MTk0NjQ1WjCBgjELMAkGA1UEBhMCVVMxDjAMBgNVBAgTBVRleGFzMRQwEgYD\nVQQHEwtTYW4gQW50b25pbzEaMBgGA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFzAV\nBgNVBAsTDlBsYXRmb3JtIExiYWFzMRgwFgYDVQQDEw9UZXN0IENsaWVudCBLZXkw\nggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDAi51IylFnHtNLT8C0NVfc\nOBfAsP2D5es1qhrOWHCGlgAuDMksBsCc7FPo5PSBOmQ+6z8HtCFbrLoC5/Zx0F5b\nfVegjA+xKjI2HGASsYHHM0BFEH2UjUcJrWiMWtxQuW6Phbqulo7JwjmygMEmIkeK\nf+FtkE9mrq+E8K40/thrjxl3/ZcJD1+3dcp+ZuzVJ2t1E4iGKCx79IZFsysKiuf+\n+E0i6iGvvI6UcbcZxVxQj2TplJkFuoX5kDgClIX9Dr9y6aJ4SCh+GRhvHl+DTaz0\nnCvghachHZtIeztRDqxWApjOOzs93dSelrviMXDr8fqyEAGg7YIhgui0aZBsWCen\nAgMBAAGjgdUwgdIwgbAGA1UdIwSBqDCBpYAUNpx1Pc6cGA7KqEwHMmHBTZMA7lSh\ngYmkgYYwgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVU4IBATAd\nBgNVHQ4EFgQULueOfsjZZOHwJHZwBy6u0swnpccwDQYJKoZIhvcNAQEFBQADggEB\nAFNuqSVUaotUJoWDv4z7Kbi6JFpTjDht5ORw4BdVYlRD4h9DACAFzPrPV2ym/Osp\nhNMdZq6msZku7MdOSQVhdeGWrSNk3M8O9Hg7cVzPNXOF3iNoo3irQ5tURut44xs4\nWw5YWQqS9WyUY5snD8tm7Y1rQTPfhg+678xIq/zWCv/u+FSnfVv1nlhLVQkEeG/Y\ngh1uMaTIpUKTGEjIAGtpGP7wwIcXptR/HyfzhTUSTaWc1Ef7zoKT9LL5z3IV1hC2\njVWz+RwYs98LjMuksJFoHqRfWyYhCIym0jb6GTwaEmpxAjc+d7OLNQdnoEGoUYGP\nYjtfkRYg265ESMA+Kww4Xy8=\n-----END CERTIFICATE-----\n",
+ IntCertificate: "-----BEGIN CERTIFICATE-----\nMIIDtTCCAp2gAwIBAgIBATANBgkqhkiG9w0BAQUFADCBgzEZMBcGA1UEAxMQVGVz\ndCBDQSBTVHViIEtleTEXMBUGA1UECxMOUGxhdGZvcm0gTGJhYXMxGjAYBgNVBAoT\nEVJhY2tzcGFjZSBIb3N0aW5nMRQwEgYDVQQHEwtTYW4gQW50b25pbzEOMAwGA1UE\nCBMFVGV4YXMxCzAJBgNVBAYTAlVTMB4XDTEyMDEwOTE5NDU0OVoXDTE0MDEwODE5\nNDU0OVowgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVUzCCASIw\nDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANNh55lwTVwQvNoEZjq1zGdYz9jA\nXXdjizn8AJhjHLOAallfPtvCfTEgKanhdoyz5FnhQE8HbDAop/KNS1lN2UMvdl5f\nZNLTSjJrNtedqxQwxN/i3bpyBxNVejUH2NjV1mmyj+5CJYwCzWalvI/gLPq/A3as\nO2EQqtf3U8unRgn0zXLRdYxV9MrUzNAmdipPNvNrsVdrCgA42rgF/8KsyRVQfJCX\nfN7PGCfrsC3YaUvhymraWxNnXIzMYTNa9wEeBZLUw8SlEtpa1Zsvui+TPXu3USNZ\nVnWH8Lb6ENlnoX0VBwo62fjOG3JzhNKoJawi3bRqyDdINOvafr7iPrrs/T8CAwEA\nAaMyMDAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUNpx1Pc6cGA7KqEwHMmHB\nTZMA7lQwDQYJKoZIhvcNAQEFBQADggEBAMoRgH3iTG3t317viLKoY+lNMHUgHuR7\nb3mn9MidJKyYVewe6hCDIN6WY4fUojmMW9wFJWJIo0hRMNHL3n3tq8HP2j20Mxy8\nacPdfGZJa+jiBw72CrIGdobKaFduIlIEDBA1pNdZIJ+EulrtqrMesnIt92WaypIS\n8JycbIgDMCiyC0ENHEk8UWlC6429c7OZAsplMTbHME/1R4btxjkdfrYZJjdJ2yL2\n8cjZDUDMCPTdW/ycP07Gkq30RB5tACB5aZdaCn2YaKC8FsEdhff4X7xEOfOEHWEq\nSRxADDj8Lx1MT6QpR07hCiDyHfTCtbqzI0iGjX63Oh7xXSa0f+JVTa8=\n-----END CERTIFICATE-----\n",
+ }
+
+ cm, err := CreateCert(client.ServiceClient(), lbID, opts).Extract()
+ th.AssertNoErr(t, err)
+
+ expected := &Certificate{
+ ID: 123,
+ HostName: "rackspace.com",
+ Certificate: "-----BEGIN CERTIFICATE-----\nMIIEXTCCA0WgAwIBAgIGATTEAjK3MA0GCSqGSIb3DQEBBQUAMIGDMRkwFwYDVQQD\nExBUZXN0IENBIFNUdWIgS2V5MRcwFQYDVQQLEw5QbGF0Zm9ybSBMYmFhczEaMBgG\nA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFDASBgNVBAcTC1NhbiBBbnRvbmlvMQ4w\nDAYDVQQIEwVUZXhhczELMAkGA1UEBhMCVVMwHhcNMTIwMTA5MTk0NjQ1WhcNMTQw\nMTA4MTk0NjQ1WjCBgjELMAkGA1UEBhMCVVMxDjAMBgNVBAgTBVRleGFzMRQwEgYD\nVQQHEwtTYW4gQW50b25pbzEaMBgGA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFzAV\nBgNVBAsTDlBsYXRmb3JtIExiYWFzMRgwFgYDVQQDEw9UZXN0IENsaWVudCBLZXkw\nggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDAi51IylFnHtNLT8C0NVfc\nOBfAsP2D5es1qhrOWHCGlgAuDMksBsCc7FPo5PSBOmQ+6z8HtCFbrLoC5/Zx0F5b\nfVegjA+xKjI2HGASsYHHM0BFEH2UjUcJrWiMWtxQuW6Phbqulo7JwjmygMEmIkeK\nf+FtkE9mrq+E8K40/thrjxl3/ZcJD1+3dcp+ZuzVJ2t1E4iGKCx79IZFsysKiuf+\n+E0i6iGvvI6UcbcZxVxQj2TplJkFuoX5kDgClIX9Dr9y6aJ4SCh+GRhvHl+DTaz0\nnCvghachHZtIeztRDqxWApjOOzs93dSelrviMXDr8fqyEAGg7YIhgui0aZBsWCen\nAgMBAAGjgdUwgdIwgbAGA1UdIwSBqDCBpYAUNpx1Pc6cGA7KqEwHMmHBTZMA7lSh\ngYmkgYYwgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVU4IBATAd\nBgNVHQ4EFgQULueOfsjZZOHwJHZwBy6u0swnpccwDQYJKoZIhvcNAQEFBQADggEB\nAFNuqSVUaotUJoWDv4z7Kbi6JFpTjDht5ORw4BdVYlRD4h9DACAFzPrPV2ym/Osp\nhNMdZq6msZku7MdOSQVhdeGWrSNk3M8O9Hg7cVzPNXOF3iNoo3irQ5tURut44xs4\nWw5YWQqS9WyUY5snD8tm7Y1rQTPfhg+678xIq/zWCv/u+FSnfVv1nlhLVQkEeG/Y\ngh1uMaTIpUKTGEjIAGtpGP7wwIcXptR/HyfzhTUSTaWc1Ef7zoKT9LL5z3IV1hC2\njVWz+RwYs98LjMuksJFoHqRfWyYhCIym0jb6GTwaEmpxAjc+d7OLNQdnoEGoUYGP\nYjtfkRYg265ESMA+Kww4Xy8=\n-----END CERTIFICATE-----\n",
+ IntCertificate: "-----BEGIN CERTIFICATE-----\nMIIDtTCCAp2gAwIBAgIBATANBgkqhkiG9w0BAQUFADCBgzEZMBcGA1UEAxMQVGVz\ndCBDQSBTVHViIEtleTEXMBUGA1UECxMOUGxhdGZvcm0gTGJhYXMxGjAYBgNVBAoT\nEVJhY2tzcGFjZSBIb3N0aW5nMRQwEgYDVQQHEwtTYW4gQW50b25pbzEOMAwGA1UE\nCBMFVGV4YXMxCzAJBgNVBAYTAlVTMB4XDTEyMDEwOTE5NDU0OVoXDTE0MDEwODE5\nNDU0OVowgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVUzCCASIw\nDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANNh55lwTVwQvNoEZjq1zGdYz9jA\nXXdjizn8AJhjHLOAallfPtvCfTEgKanhdoyz5FnhQE8HbDAop/KNS1lN2UMvdl5f\nZNLTSjJrNtedqxQwxN/i3bpyBxNVejUH2NjV1mmyj+5CJYwCzWalvI/gLPq/A3as\nO2EQqtf3U8unRgn0zXLRdYxV9MrUzNAmdipPNvNrsVdrCgA42rgF/8KsyRVQfJCX\nfN7PGCfrsC3YaUvhymraWxNnXIzMYTNa9wEeBZLUw8SlEtpa1Zsvui+TPXu3USNZ\nVnWH8Lb6ENlnoX0VBwo62fjOG3JzhNKoJawi3bRqyDdINOvafr7iPrrs/T8CAwEA\nAaMyMDAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUNpx1Pc6cGA7KqEwHMmHB\nTZMA7lQwDQYJKoZIhvcNAQEFBQADggEBAMoRgH3iTG3t317viLKoY+lNMHUgHuR7\nb3mn9MidJKyYVewe6hCDIN6WY4fUojmMW9wFJWJIo0hRMNHL3n3tq8HP2j20Mxy8\nacPdfGZJa+jiBw72CrIGdobKaFduIlIEDBA1pNdZIJ+EulrtqrMesnIt92WaypIS\n8JycbIgDMCiyC0ENHEk8UWlC6429c7OZAsplMTbHME/1R4btxjkdfrYZJjdJ2yL2\n8cjZDUDMCPTdW/ycP07Gkq30RB5tACB5aZdaCn2YaKC8FsEdhff4X7xEOfOEHWEq\nSRxADDj8Lx1MT6QpR07hCiDyHfTCtbqzI0iGjX63Oh7xXSa0f+JVTa8=\n-----END CERTIFICATE-----\n",
+ }
+ th.AssertDeepEquals(t, expected, cm)
+}
+
+func TestGetCertMapping(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockGetCertResponse(t, lbID, certID)
+
+ sp, err := GetCert(client.ServiceClient(), lbID, certID).Extract()
+ th.AssertNoErr(t, err)
+
+ expected := &Certificate{
+ ID: 123,
+ HostName: "rackspace.com",
+ Certificate: "-----BEGIN CERTIFICATE-----\nMIIEXTCCA0WgAwIBAgIGATTEAjK3MA0GCSqGSIb3DQEBBQUAMIGDMRkwFwYDVQQD\nExBUZXN0IENBIFNUdWIgS2V5MRcwFQYDVQQLEw5QbGF0Zm9ybSBMYmFhczEaMBgG\nA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFDASBgNVBAcTC1NhbiBBbnRvbmlvMQ4w\nDAYDVQQIEwVUZXhhczELMAkGA1UEBhMCVVMwHhcNMTIwMTA5MTk0NjQ1WhcNMTQw\nMTA4MTk0NjQ1WjCBgjELMAkGA1UEBhMCVVMxDjAMBgNVBAgTBVRleGFzMRQwEgYD\nVQQHEwtTYW4gQW50b25pbzEaMBgGA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFzAV\nBgNVBAsTDlBsYXRmb3JtIExiYWFzMRgwFgYDVQQDEw9UZXN0IENsaWVudCBLZXkw\nggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDAi51IylFnHtNLT8C0NVfc\nOBfAsP2D5es1qhrOWHCGlgAuDMksBsCc7FPo5PSBOmQ+6z8HtCFbrLoC5/Zx0F5b\nfVegjA+xKjI2HGASsYHHM0BFEH2UjUcJrWiMWtxQuW6Phbqulo7JwjmygMEmIkeK\nf+FtkE9mrq+E8K40/thrjxl3/ZcJD1+3dcp+ZuzVJ2t1E4iGKCx79IZFsysKiuf+\n+E0i6iGvvI6UcbcZxVxQj2TplJkFuoX5kDgClIX9Dr9y6aJ4SCh+GRhvHl+DTaz0\nnCvghachHZtIeztRDqxWApjOOzs93dSelrviMXDr8fqyEAGg7YIhgui0aZBsWCen\nAgMBAAGjgdUwgdIwgbAGA1UdIwSBqDCBpYAUNpx1Pc6cGA7KqEwHMmHBTZMA7lSh\ngYmkgYYwgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVU4IBATAd\nBgNVHQ4EFgQULueOfsjZZOHwJHZwBy6u0swnpccwDQYJKoZIhvcNAQEFBQADggEB\nAFNuqSVUaotUJoWDv4z7Kbi6JFpTjDht5ORw4BdVYlRD4h9DACAFzPrPV2ym/Osp\nhNMdZq6msZku7MdOSQVhdeGWrSNk3M8O9Hg7cVzPNXOF3iNoo3irQ5tURut44xs4\nWw5YWQqS9WyUY5snD8tm7Y1rQTPfhg+678xIq/zWCv/u+FSnfVv1nlhLVQkEeG/Y\ngh1uMaTIpUKTGEjIAGtpGP7wwIcXptR/HyfzhTUSTaWc1Ef7zoKT9LL5z3IV1hC2\njVWz+RwYs98LjMuksJFoHqRfWyYhCIym0jb6GTwaEmpxAjc+d7OLNQdnoEGoUYGP\nYjtfkRYg265ESMA+Kww4Xy8=\n-----END CERTIFICATE-----\n",
+ IntCertificate: "-----BEGIN CERTIFICATE-----\nMIIDtTCCAp2gAwIBAgIBATANBgkqhkiG9w0BAQUFADCBgzEZMBcGA1UEAxMQVGVz\ndCBDQSBTVHViIEtleTEXMBUGA1UECxMOUGxhdGZvcm0gTGJhYXMxGjAYBgNVBAoT\nEVJhY2tzcGFjZSBIb3N0aW5nMRQwEgYDVQQHEwtTYW4gQW50b25pbzEOMAwGA1UE\nCBMFVGV4YXMxCzAJBgNVBAYTAlVTMB4XDTEyMDEwOTE5NDU0OVoXDTE0MDEwODE5\nNDU0OVowgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVUzCCASIw\nDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANNh55lwTVwQvNoEZjq1zGdYz9jA\nXXdjizn8AJhjHLOAallfPtvCfTEgKanhdoyz5FnhQE8HbDAop/KNS1lN2UMvdl5f\nZNLTSjJrNtedqxQwxN/i3bpyBxNVejUH2NjV1mmyj+5CJYwCzWalvI/gLPq/A3as\nO2EQqtf3U8unRgn0zXLRdYxV9MrUzNAmdipPNvNrsVdrCgA42rgF/8KsyRVQfJCX\nfN7PGCfrsC3YaUvhymraWxNnXIzMYTNa9wEeBZLUw8SlEtpa1Zsvui+TPXu3USNZ\nVnWH8Lb6ENlnoX0VBwo62fjOG3JzhNKoJawi3bRqyDdINOvafr7iPrrs/T8CAwEA\nAaMyMDAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUNpx1Pc6cGA7KqEwHMmHB\nTZMA7lQwDQYJKoZIhvcNAQEFBQADggEBAMoRgH3iTG3t317viLKoY+lNMHUgHuR7\nb3mn9MidJKyYVewe6hCDIN6WY4fUojmMW9wFJWJIo0hRMNHL3n3tq8HP2j20Mxy8\nacPdfGZJa+jiBw72CrIGdobKaFduIlIEDBA1pNdZIJ+EulrtqrMesnIt92WaypIS\n8JycbIgDMCiyC0ENHEk8UWlC6429c7OZAsplMTbHME/1R4btxjkdfrYZJjdJ2yL2\n8cjZDUDMCPTdW/ycP07Gkq30RB5tACB5aZdaCn2YaKC8FsEdhff4X7xEOfOEHWEq\nSRxADDj8Lx1MT6QpR07hCiDyHfTCtbqzI0iGjX63Oh7xXSa0f+JVTa8=\n-----END CERTIFICATE-----\n",
+ }
+ th.AssertDeepEquals(t, expected, sp)
+}
+
+func TestUpdateCert(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockUpdateCertResponse(t, lbID, certID)
+
+ opts := UpdateCertOpts{
+ HostName: "rackspace.com",
+ PrivateKey: "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEAwIudSMpRZx7TS0/AtDVX3DgXwLD9g+XrNaoazlhwhpYALgzJ\nLAbAnOxT6OT0gTpkPus/B7QhW6y6Auf2cdBeW31XoIwPsSoyNhxgErGBxzNARRB9\nlI1HCa1ojFrcULluj4W6rpaOycI5soDBJiJHin/hbZBPZq6vhPCuNP7Ya48Zd/2X\nCQ9ft3XKfmbs1SdrdROIhigse/SGRbMrCorn/vhNIuohr7yOlHG3GcVcUI9k6ZSZ\nBbqF+ZA4ApSF/Q6/cumieEgofhkYbx5fg02s9Jwr4IWnIR2bSHs7UQ6sVgKYzjs7\nPd3Unpa74jFw6/H6shABoO2CIYLotGmQbFgnpwIDAQABAoIBAQCBCQ+PCIclJHNV\ntUzfeCA5ZR4F9JbxHdRTUnxEbOB8UWotckQfTScoAvj4yvdQ42DrCZxj/UOdvFOs\nPufZvlp91bIz1alugWjE+p8n5+2hIaegoTyHoWZKBfxak0myj5KYfHZvKlbmv1ML\nXV4TwEVRfAIG+v87QTY/UUxuF5vR+BpKIbgUJLfPUFFvJUdl84qsJ44pToxaYUd/\nh5YAGC00U4ay1KVSAUnTkkPNZ0lPG/rWU6w6WcTvNRLMd8DzFLTKLOgQfHhbExAF\n+sXPWjWSzbBRP1O7fHqq96QQh4VFiY/7w9W+sDKQyV6Ul17OSXs6aZ4f+lq4rJTI\n1FG96YiBAoGBAO1tiH0h1oWDBYfJB3KJJ6CQQsDGwtHo/DEgznFVP4XwEVbZ98Ha\nBfBCn3sAybbaikyCV1Hwj7kfHMZPDHbrcUSFX7quu/2zPK+wO3lZKXSyu4YsguSa\nRedInN33PpdnlPhLyQdWSuD5sVHJDF6xn22vlyxeILH3ooLg2WOFMPmVAoGBAM+b\nUG/a7iyfpAQKYyuFAsXz6SeFaDY+ZYeX45L112H8Pu+Ie/qzon+bzLB9FIH8GP6+\nQpQgmm/p37U2gD1zChUv7iW6OfQBKk9rWvMpfRF6d7YHquElejhizfTZ+ntBV/VY\ndOYEczxhrdW7keLpatYaaWUy/VboRZmlz/9JGqVLAoGAHfqNmFc0cgk4IowEj7a3\ntTNh6ltub/i+FynwRykfazcDyXaeLPDtfQe8gVh5H8h6W+y9P9BjJVnDVVrX1RAn\nbiJ1EupLPF5sVDapW8ohTOXgfbGTGXBNUUW+4Nv+IDno+mz/RhjkPYHpnM0I7c/5\ntGzOZsC/2hjNgT8I0+MWav0CgYEAuULdJeQVlKalI6HtW2Gn1uRRVJ49H+LQkY6e\nW3+cw2jo9LI0CMWSphNvNrN3wIMp/vHj0fHCP0pSApDvIWbuQXfzKaGko7UCf7rK\nf6GvZRCHkV4IREBAb97j8bMvThxClMNqFfU0rFZyXP+0MOyhFQyertswrgQ6T+Fi\n2mnvKD8CgYAmJHP3NTDRMoMRyAzonJ6nEaGUbAgNmivTaUWMe0+leCvAdwD89gzC\nTKbm3eDUg/6Va3X6ANh3wsfIOe4RXXxcbcFDk9R4zO2M5gfLSjYm5Q87EBZ2hrdj\nM2gLI7dt6thx0J8lR8xRHBEMrVBdgwp0g1gQzo5dAV88/kpkZVps8Q==\n-----END RSA PRIVATE KEY-----\n",
+ Certificate: "-----BEGIN CERTIFICATE-----\nMIIEXTCCA0WgAwIBAgIGATTEAjK3MA0GCSqGSIb3DQEBBQUAMIGDMRkwFwYDVQQD\nExBUZXN0IENBIFNUdWIgS2V5MRcwFQYDVQQLEw5QbGF0Zm9ybSBMYmFhczEaMBgG\nA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFDASBgNVBAcTC1NhbiBBbnRvbmlvMQ4w\nDAYDVQQIEwVUZXhhczELMAkGA1UEBhMCVVMwHhcNMTIwMTA5MTk0NjQ1WhcNMTQw\nMTA4MTk0NjQ1WjCBgjELMAkGA1UEBhMCVVMxDjAMBgNVBAgTBVRleGFzMRQwEgYD\nVQQHEwtTYW4gQW50b25pbzEaMBgGA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFzAV\nBgNVBAsTDlBsYXRmb3JtIExiYWFzMRgwFgYDVQQDEw9UZXN0IENsaWVudCBLZXkw\nggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDAi51IylFnHtNLT8C0NVfc\nOBfAsP2D5es1qhrOWHCGlgAuDMksBsCc7FPo5PSBOmQ+6z8HtCFbrLoC5/Zx0F5b\nfVegjA+xKjI2HGASsYHHM0BFEH2UjUcJrWiMWtxQuW6Phbqulo7JwjmygMEmIkeK\nf+FtkE9mrq+E8K40/thrjxl3/ZcJD1+3dcp+ZuzVJ2t1E4iGKCx79IZFsysKiuf+\n+E0i6iGvvI6UcbcZxVxQj2TplJkFuoX5kDgClIX9Dr9y6aJ4SCh+GRhvHl+DTaz0\nnCvghachHZtIeztRDqxWApjOOzs93dSelrviMXDr8fqyEAGg7YIhgui0aZBsWCen\nAgMBAAGjgdUwgdIwgbAGA1UdIwSBqDCBpYAUNpx1Pc6cGA7KqEwHMmHBTZMA7lSh\ngYmkgYYwgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVU4IBATAd\nBgNVHQ4EFgQULueOfsjZZOHwJHZwBy6u0swnpccwDQYJKoZIhvcNAQEFBQADggEB\nAFNuqSVUaotUJoWDv4z7Kbi6JFpTjDht5ORw4BdVYlRD4h9DACAFzPrPV2ym/Osp\nhNMdZq6msZku7MdOSQVhdeGWrSNk3M8O9Hg7cVzPNXOF3iNoo3irQ5tURut44xs4\nWw5YWQqS9WyUY5snD8tm7Y1rQTPfhg+678xIq/zWCv/u+FSnfVv1nlhLVQkEeG/Y\ngh1uMaTIpUKTGEjIAGtpGP7wwIcXptR/HyfzhTUSTaWc1Ef7zoKT9LL5z3IV1hC2\njVWz+RwYs98LjMuksJFoHqRfWyYhCIym0jb6GTwaEmpxAjc+d7OLNQdnoEGoUYGP\nYjtfkRYg265ESMA+Kww4Xy8=\n-----END CERTIFICATE-----\n",
+ IntCertificate: "-----BEGIN CERTIFICATE-----\nMIIDtTCCAp2gAwIBAgIBATANBgkqhkiG9w0BAQUFADCBgzEZMBcGA1UEAxMQVGVz\ndCBDQSBTVHViIEtleTEXMBUGA1UECxMOUGxhdGZvcm0gTGJhYXMxGjAYBgNVBAoT\nEVJhY2tzcGFjZSBIb3N0aW5nMRQwEgYDVQQHEwtTYW4gQW50b25pbzEOMAwGA1UE\nCBMFVGV4YXMxCzAJBgNVBAYTAlVTMB4XDTEyMDEwOTE5NDU0OVoXDTE0MDEwODE5\nNDU0OVowgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVUzCCASIw\nDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANNh55lwTVwQvNoEZjq1zGdYz9jA\nXXdjizn8AJhjHLOAallfPtvCfTEgKanhdoyz5FnhQE8HbDAop/KNS1lN2UMvdl5f\nZNLTSjJrNtedqxQwxN/i3bpyBxNVejUH2NjV1mmyj+5CJYwCzWalvI/gLPq/A3as\nO2EQqtf3U8unRgn0zXLRdYxV9MrUzNAmdipPNvNrsVdrCgA42rgF/8KsyRVQfJCX\nfN7PGCfrsC3YaUvhymraWxNnXIzMYTNa9wEeBZLUw8SlEtpa1Zsvui+TPXu3USNZ\nVnWH8Lb6ENlnoX0VBwo62fjOG3JzhNKoJawi3bRqyDdINOvafr7iPrrs/T8CAwEA\nAaMyMDAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUNpx1Pc6cGA7KqEwHMmHB\nTZMA7lQwDQYJKoZIhvcNAQEFBQADggEBAMoRgH3iTG3t317viLKoY+lNMHUgHuR7\nb3mn9MidJKyYVewe6hCDIN6WY4fUojmMW9wFJWJIo0hRMNHL3n3tq8HP2j20Mxy8\nacPdfGZJa+jiBw72CrIGdobKaFduIlIEDBA1pNdZIJ+EulrtqrMesnIt92WaypIS\n8JycbIgDMCiyC0ENHEk8UWlC6429c7OZAsplMTbHME/1R4btxjkdfrYZJjdJ2yL2\n8cjZDUDMCPTdW/ycP07Gkq30RB5tACB5aZdaCn2YaKC8FsEdhff4X7xEOfOEHWEq\nSRxADDj8Lx1MT6QpR07hCiDyHfTCtbqzI0iGjX63Oh7xXSa0f+JVTa8=\n-----END CERTIFICATE-----\n",
+ }
+
+ cm, err := UpdateCert(client.ServiceClient(), lbID, certID, opts).Extract()
+ th.AssertNoErr(t, err)
+
+ expected := &Certificate{
+ ID: 123,
+ HostName: "rackspace.com",
+ Certificate: "-----BEGIN CERTIFICATE-----\nMIIEXTCCA0WgAwIBAgIGATTEAjK3MA0GCSqGSIb3DQEBBQUAMIGDMRkwFwYDVQQD\nExBUZXN0IENBIFNUdWIgS2V5MRcwFQYDVQQLEw5QbGF0Zm9ybSBMYmFhczEaMBgG\nA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFDASBgNVBAcTC1NhbiBBbnRvbmlvMQ4w\nDAYDVQQIEwVUZXhhczELMAkGA1UEBhMCVVMwHhcNMTIwMTA5MTk0NjQ1WhcNMTQw\nMTA4MTk0NjQ1WjCBgjELMAkGA1UEBhMCVVMxDjAMBgNVBAgTBVRleGFzMRQwEgYD\nVQQHEwtTYW4gQW50b25pbzEaMBgGA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFzAV\nBgNVBAsTDlBsYXRmb3JtIExiYWFzMRgwFgYDVQQDEw9UZXN0IENsaWVudCBLZXkw\nggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDAi51IylFnHtNLT8C0NVfc\nOBfAsP2D5es1qhrOWHCGlgAuDMksBsCc7FPo5PSBOmQ+6z8HtCFbrLoC5/Zx0F5b\nfVegjA+xKjI2HGASsYHHM0BFEH2UjUcJrWiMWtxQuW6Phbqulo7JwjmygMEmIkeK\nf+FtkE9mrq+E8K40/thrjxl3/ZcJD1+3dcp+ZuzVJ2t1E4iGKCx79IZFsysKiuf+\n+E0i6iGvvI6UcbcZxVxQj2TplJkFuoX5kDgClIX9Dr9y6aJ4SCh+GRhvHl+DTaz0\nnCvghachHZtIeztRDqxWApjOOzs93dSelrviMXDr8fqyEAGg7YIhgui0aZBsWCen\nAgMBAAGjgdUwgdIwgbAGA1UdIwSBqDCBpYAUNpx1Pc6cGA7KqEwHMmHBTZMA7lSh\ngYmkgYYwgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVU4IBATAd\nBgNVHQ4EFgQULueOfsjZZOHwJHZwBy6u0swnpccwDQYJKoZIhvcNAQEFBQADggEB\nAFNuqSVUaotUJoWDv4z7Kbi6JFpTjDht5ORw4BdVYlRD4h9DACAFzPrPV2ym/Osp\nhNMdZq6msZku7MdOSQVhdeGWrSNk3M8O9Hg7cVzPNXOF3iNoo3irQ5tURut44xs4\nWw5YWQqS9WyUY5snD8tm7Y1rQTPfhg+678xIq/zWCv/u+FSnfVv1nlhLVQkEeG/Y\ngh1uMaTIpUKTGEjIAGtpGP7wwIcXptR/HyfzhTUSTaWc1Ef7zoKT9LL5z3IV1hC2\njVWz+RwYs98LjMuksJFoHqRfWyYhCIym0jb6GTwaEmpxAjc+d7OLNQdnoEGoUYGP\nYjtfkRYg265ESMA+Kww4Xy8=\n-----END CERTIFICATE-----\n",
+ IntCertificate: "-----BEGIN CERTIFICATE-----\nMIIDtTCCAp2gAwIBAgIBATANBgkqhkiG9w0BAQUFADCBgzEZMBcGA1UEAxMQVGVz\ndCBDQSBTVHViIEtleTEXMBUGA1UECxMOUGxhdGZvcm0gTGJhYXMxGjAYBgNVBAoT\nEVJhY2tzcGFjZSBIb3N0aW5nMRQwEgYDVQQHEwtTYW4gQW50b25pbzEOMAwGA1UE\nCBMFVGV4YXMxCzAJBgNVBAYTAlVTMB4XDTEyMDEwOTE5NDU0OVoXDTE0MDEwODE5\nNDU0OVowgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVUzCCASIw\nDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANNh55lwTVwQvNoEZjq1zGdYz9jA\nXXdjizn8AJhjHLOAallfPtvCfTEgKanhdoyz5FnhQE8HbDAop/KNS1lN2UMvdl5f\nZNLTSjJrNtedqxQwxN/i3bpyBxNVejUH2NjV1mmyj+5CJYwCzWalvI/gLPq/A3as\nO2EQqtf3U8unRgn0zXLRdYxV9MrUzNAmdipPNvNrsVdrCgA42rgF/8KsyRVQfJCX\nfN7PGCfrsC3YaUvhymraWxNnXIzMYTNa9wEeBZLUw8SlEtpa1Zsvui+TPXu3USNZ\nVnWH8Lb6ENlnoX0VBwo62fjOG3JzhNKoJawi3bRqyDdINOvafr7iPrrs/T8CAwEA\nAaMyMDAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUNpx1Pc6cGA7KqEwHMmHB\nTZMA7lQwDQYJKoZIhvcNAQEFBQADggEBAMoRgH3iTG3t317viLKoY+lNMHUgHuR7\nb3mn9MidJKyYVewe6hCDIN6WY4fUojmMW9wFJWJIo0hRMNHL3n3tq8HP2j20Mxy8\nacPdfGZJa+jiBw72CrIGdobKaFduIlIEDBA1pNdZIJ+EulrtqrMesnIt92WaypIS\n8JycbIgDMCiyC0ENHEk8UWlC6429c7OZAsplMTbHME/1R4btxjkdfrYZJjdJ2yL2\n8cjZDUDMCPTdW/ycP07Gkq30RB5tACB5aZdaCn2YaKC8FsEdhff4X7xEOfOEHWEq\nSRxADDj8Lx1MT6QpR07hCiDyHfTCtbqzI0iGjX63Oh7xXSa0f+JVTa8=\n-----END CERTIFICATE-----\n",
+ }
+ th.AssertDeepEquals(t, expected, cm)
+}
+
+func TestDeleteCert(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockDeleteCertResponse(t, lbID, certID)
+
+ err := DeleteCert(client.ServiceClient(), lbID, certID).ExtractErr()
+ th.AssertNoErr(t, err)
+}
diff --git a/rackspace/lb/v1/ssl/results.go b/rackspace/lb/v1/ssl/results.go
new file mode 100644
index 0000000..ead9fcd
--- /dev/null
+++ b/rackspace/lb/v1/ssl/results.go
@@ -0,0 +1,148 @@
+package ssl
+
+import (
+ "github.com/mitchellh/mapstructure"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// SSLTermConfig represents the SSL configuration for a particular load balancer.
+type SSLTermConfig struct {
+ // The port on which the SSL termination load balancer listens for secure
+ // traffic. The value must be unique to the existing LB protocol/port
+ // combination
+ SecurePort int `mapstructure:"securePort"`
+
+ // The private key for the SSL certificate which is validated and verified
+ // against the provided certificates.
+ PrivateKey string `mapstructure:"privatekey"`
+
+ // The certificate used for SSL termination, which is validated and verified
+ // against the key and intermediate certificate if provided.
+ Certificate string
+
+ // The intermediate certificate (for the user). The intermediate certificate
+ // is validated and verified against the key and certificate credentials
+ // provided. A user may only provide this value when accompanied by a
+ // Certificate, PrivateKey, and SecurePort. It may not be added or updated as
+ // a single attribute in a future operation.
+ IntCertificate string `mapstructure:"intermediatecertificate"`
+
+ // Determines if the load balancer is enabled to terminate SSL traffic or not.
+ // If this is set to false, the load balancer retains its specified SSL
+ // attributes but does not terminate SSL traffic.
+ Enabled bool
+
+ // Determines if the load balancer can only accept secure traffic. If set to
+ // true, the load balancer will not accept non-secure traffic.
+ SecureTrafficOnly bool
+}
+
+// DeleteResult represents the result of a delete operation.
+type DeleteResult struct {
+ gophercloud.ErrResult
+}
+
+// UpdateResult represents the result of an update operation.
+type UpdateResult struct {
+ gophercloud.ErrResult
+}
+
+// GetResult represents the result of a get operation.
+type GetResult struct {
+ gophercloud.Result
+}
+
+// Extract interprets a GetResult as a SSLTermConfig struct, if possible.
+func (r GetResult) Extract() (*SSLTermConfig, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var response struct {
+ SSL SSLTermConfig `mapstructure:"sslTermination"`
+ }
+
+ err := mapstructure.Decode(r.Body, &response)
+
+ return &response.SSL, err
+}
+
+// Certificate represents an SSL certificate associated with an SSL-terminated
+// HTTP load balancer.
+type Certificate struct {
+ ID int
+ HostName string
+ Certificate string
+ IntCertificate string `mapstructure:"intermediateCertificate"`
+}
+
+// CertPage represents a page of certificates.
+type CertPage struct {
+ pagination.LinkedPageBase
+}
+
+// IsEmpty checks whether a CertMappingPage struct is empty.
+func (p CertPage) IsEmpty() (bool, error) {
+ is, err := ExtractCerts(p)
+ if err != nil {
+ return true, nil
+ }
+ return len(is) == 0, nil
+}
+
+// ExtractCerts accepts a Page struct, specifically a CertPage struct, and
+// extracts the elements into a slice of Cert structs. In other words, a generic
+// collection is mapped into a relevant slice.
+func ExtractCerts(page pagination.Page) ([]Certificate, error) {
+ type NestedMap struct {
+ Cert Certificate `mapstructure:"certificateMapping" json:"certificateMapping"`
+ }
+ var resp struct {
+ Certs []NestedMap `mapstructure:"certificateMappings" json:"certificateMappings"`
+ }
+
+ err := mapstructure.Decode(page.(CertPage).Body, &resp)
+
+ slice := []Certificate{}
+ for _, cert := range resp.Certs {
+ slice = append(slice, cert.Cert)
+ }
+
+ return slice, err
+}
+
+type certResult struct {
+ gophercloud.Result
+}
+
+// Extract interprets a result as a CertMapping struct, if possible.
+func (r certResult) Extract() (*Certificate, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var response struct {
+ Cert Certificate `mapstructure:"certificateMapping"`
+ }
+
+ err := mapstructure.Decode(r.Body, &response)
+
+ return &response.Cert, err
+}
+
+// CreateCertResult represents the result of an CreateCert operation.
+type CreateCertResult struct {
+ certResult
+}
+
+// GetCertResult represents the result of a GetCert operation.
+type GetCertResult struct {
+ certResult
+}
+
+// UpdateCertResult represents the result of an UpdateCert operation.
+type UpdateCertResult struct {
+ certResult
+}
diff --git a/rackspace/lb/v1/ssl/urls.go b/rackspace/lb/v1/ssl/urls.go
new file mode 100644
index 0000000..aa814b3
--- /dev/null
+++ b/rackspace/lb/v1/ssl/urls.go
@@ -0,0 +1,25 @@
+package ssl
+
+import (
+ "strconv"
+
+ "github.com/rackspace/gophercloud"
+)
+
+const (
+ path = "loadbalancers"
+ sslPath = "ssltermination"
+ certPath = "certificatemappings"
+)
+
+func rootURL(c *gophercloud.ServiceClient, id int) string {
+ return c.ServiceURL(path, strconv.Itoa(id), sslPath)
+}
+
+func certURL(c *gophercloud.ServiceClient, id int) string {
+ return c.ServiceURL(path, strconv.Itoa(id), sslPath, certPath)
+}
+
+func certResourceURL(c *gophercloud.ServiceClient, id, certID int) string {
+ return c.ServiceURL(path, strconv.Itoa(id), sslPath, certPath, strconv.Itoa(certID))
+}
diff --git a/rackspace/lb/v1/throttle/doc.go b/rackspace/lb/v1/throttle/doc.go
new file mode 100644
index 0000000..1ed605d
--- /dev/null
+++ b/rackspace/lb/v1/throttle/doc.go
@@ -0,0 +1,5 @@
+/*
+Package throttle provides information and interaction with the Connection
+Throttling feature of the Rackspace Cloud Load Balancer service.
+*/
+package throttle
diff --git a/rackspace/lb/v1/throttle/fixtures.go b/rackspace/lb/v1/throttle/fixtures.go
new file mode 100644
index 0000000..40223f6
--- /dev/null
+++ b/rackspace/lb/v1/throttle/fixtures.go
@@ -0,0 +1,61 @@
+package throttle
+
+import (
+ "fmt"
+ "net/http"
+ "strconv"
+ "testing"
+
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func _rootURL(id int) string {
+ return "/loadbalancers/" + strconv.Itoa(id) + "/connectionthrottle"
+}
+
+func mockGetResponse(t *testing.T, lbID int) {
+ th.Mux.HandleFunc(_rootURL(lbID), 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, `
+{
+ "connectionThrottle": {
+ "maxConnections": 100
+ }
+}
+`)
+ })
+}
+
+func mockCreateResponse(t *testing.T, lbID int) {
+ th.Mux.HandleFunc(_rootURL(lbID), 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, `
+{
+ "connectionThrottle": {
+ "maxConnectionRate": 0,
+ "maxConnections": 200,
+ "minConnections": 0,
+ "rateInterval": 0
+ }
+}
+ `)
+
+ w.WriteHeader(http.StatusAccepted)
+ })
+}
+
+func mockDeleteResponse(t *testing.T, lbID int) {
+ th.Mux.HandleFunc(_rootURL(lbID), func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "DELETE")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ w.WriteHeader(http.StatusAccepted)
+ })
+}
diff --git a/rackspace/lb/v1/throttle/requests.go b/rackspace/lb/v1/throttle/requests.go
new file mode 100644
index 0000000..8c2e4be
--- /dev/null
+++ b/rackspace/lb/v1/throttle/requests.go
@@ -0,0 +1,95 @@
+package throttle
+
+import (
+ "errors"
+
+ "github.com/racker/perigee"
+
+ "github.com/rackspace/gophercloud"
+)
+
+// CreateOptsBuilder is the interface options structs have to satisfy in order
+// to be used in the main Create operation in this package.
+type CreateOptsBuilder interface {
+ ToCTCreateMap() (map[string]interface{}, error)
+}
+
+// CreateOpts is the common options struct used in this package's Create
+// operation.
+type CreateOpts struct {
+ // Required - the maximum amount of connections per IP address to allow per LB.
+ MaxConnections int
+
+ // Deprecated as of v1.22.
+ MaxConnectionRate int
+
+ // Deprecated as of v1.22.
+ MinConnections int
+
+ // Deprecated as of v1.22.
+ RateInterval int
+}
+
+// ToCTCreateMap casts a CreateOpts struct to a map.
+func (opts CreateOpts) ToCTCreateMap() (map[string]interface{}, error) {
+ ct := make(map[string]interface{})
+
+ if opts.MaxConnections < 0 || opts.MaxConnections > 100000 {
+ return ct, errors.New("MaxConnections must be an int between 0 and 100000")
+ }
+
+ ct["maxConnections"] = opts.MaxConnections
+ ct["maxConnectionRate"] = opts.MaxConnectionRate
+ ct["minConnections"] = opts.MinConnections
+ ct["rateInterval"] = opts.RateInterval
+
+ return map[string]interface{}{"connectionThrottle": ct}, nil
+}
+
+// Create is the operation responsible for creating or updating the connection
+// throttling configuration for a load balancer.
+func Create(c *gophercloud.ServiceClient, lbID int, opts CreateOptsBuilder) CreateResult {
+ var res CreateResult
+
+ reqBody, err := opts.ToCTCreateMap()
+ if err != nil {
+ res.Err = err
+ return res
+ }
+
+ _, res.Err = perigee.Request("PUT", rootURL(c, lbID), perigee.Options{
+ MoreHeaders: c.AuthenticatedHeaders(),
+ ReqBody: &reqBody,
+ Results: &res.Body,
+ OkCodes: []int{202},
+ })
+
+ return res
+}
+
+// Get is the operation responsible for showing the details of the connection
+// throttling configuration for a load balancer.
+func Get(c *gophercloud.ServiceClient, lbID int) GetResult {
+ var res GetResult
+
+ _, res.Err = perigee.Request("GET", rootURL(c, lbID), perigee.Options{
+ MoreHeaders: c.AuthenticatedHeaders(),
+ Results: &res.Body,
+ OkCodes: []int{200},
+ })
+
+ return res
+}
+
+// Delete is the operation responsible for deleting the connection throttling
+// configuration for a load balancer.
+func Delete(c *gophercloud.ServiceClient, lbID int) DeleteResult {
+ var res DeleteResult
+
+ _, res.Err = perigee.Request("DELETE", rootURL(c, lbID), perigee.Options{
+ MoreHeaders: c.AuthenticatedHeaders(),
+ OkCodes: []int{202},
+ })
+
+ return res
+}
diff --git a/rackspace/lb/v1/throttle/requests_test.go b/rackspace/lb/v1/throttle/requests_test.go
new file mode 100644
index 0000000..6e9703f
--- /dev/null
+++ b/rackspace/lb/v1/throttle/requests_test.go
@@ -0,0 +1,44 @@
+package throttle
+
+import (
+ "testing"
+
+ th "github.com/rackspace/gophercloud/testhelper"
+ "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+const lbID = 12345
+
+func TestCreate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockCreateResponse(t, lbID)
+
+ opts := CreateOpts{MaxConnections: 200}
+ err := Create(client.ServiceClient(), lbID, opts).ExtractErr()
+ th.AssertNoErr(t, err)
+}
+
+func TestGet(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockGetResponse(t, lbID)
+
+ sp, err := Get(client.ServiceClient(), lbID).Extract()
+ th.AssertNoErr(t, err)
+
+ expected := &ConnectionThrottle{MaxConnections: 100}
+ th.AssertDeepEquals(t, expected, sp)
+}
+
+func TestDelete(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockDeleteResponse(t, lbID)
+
+ err := Delete(client.ServiceClient(), lbID).ExtractErr()
+ th.AssertNoErr(t, err)
+}
diff --git a/rackspace/lb/v1/throttle/results.go b/rackspace/lb/v1/throttle/results.go
new file mode 100644
index 0000000..db93c6f
--- /dev/null
+++ b/rackspace/lb/v1/throttle/results.go
@@ -0,0 +1,43 @@
+package throttle
+
+import (
+ "github.com/mitchellh/mapstructure"
+
+ "github.com/rackspace/gophercloud"
+)
+
+// ConnectionThrottle represents the connection throttle configuration for a
+// particular load balancer.
+type ConnectionThrottle struct {
+ MaxConnections int
+}
+
+// CreateResult represents the result of a create operation.
+type CreateResult struct {
+ gophercloud.ErrResult
+}
+
+// DeleteResult represents the result of a delete operation.
+type DeleteResult struct {
+ gophercloud.ErrResult
+}
+
+// GetResult represents the result of a get operation.
+type GetResult struct {
+ gophercloud.Result
+}
+
+// Extract interprets a GetResult as a SP, if possible.
+func (r GetResult) Extract() (*ConnectionThrottle, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var response struct {
+ CT ConnectionThrottle `mapstructure:"connectionThrottle"`
+ }
+
+ err := mapstructure.Decode(r.Body, &response)
+
+ return &response.CT, err
+}
diff --git a/rackspace/lb/v1/throttle/urls.go b/rackspace/lb/v1/throttle/urls.go
new file mode 100644
index 0000000..b77f0ac
--- /dev/null
+++ b/rackspace/lb/v1/throttle/urls.go
@@ -0,0 +1,16 @@
+package throttle
+
+import (
+ "strconv"
+
+ "github.com/rackspace/gophercloud"
+)
+
+const (
+ path = "loadbalancers"
+ ctPath = "connectionthrottle"
+)
+
+func rootURL(c *gophercloud.ServiceClient, id int) string {
+ return c.ServiceURL(path, strconv.Itoa(id), ctPath)
+}
diff --git a/rackspace/lb/v1/vips/doc.go b/rackspace/lb/v1/vips/doc.go
new file mode 100644
index 0000000..5c3846d
--- /dev/null
+++ b/rackspace/lb/v1/vips/doc.go
@@ -0,0 +1,13 @@
+/*
+Package vips provides information and interaction with the Virtual IP API
+resource for the Rackspace Cloud Load Balancer service.
+
+A virtual IP (VIP) makes a load balancer accessible by clients. The load
+balancing service supports either a public VIP, routable on the public Internet,
+or a ServiceNet address, routable only within the region in which the load
+balancer resides.
+
+All load balancers must have at least one virtual IP associated with them at
+all times.
+*/
+package vips
diff --git a/rackspace/lb/v1/vips/fixtures.go b/rackspace/lb/v1/vips/fixtures.go
new file mode 100644
index 0000000..158759f
--- /dev/null
+++ b/rackspace/lb/v1/vips/fixtures.go
@@ -0,0 +1,88 @@
+package vips
+
+import (
+ "fmt"
+ "net/http"
+ "strconv"
+ "testing"
+
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func _rootURL(lbID int) string {
+ return "/loadbalancers/" + strconv.Itoa(lbID) + "/virtualips"
+}
+
+func mockListResponse(t *testing.T, lbID int) {
+ th.Mux.HandleFunc(_rootURL(lbID), 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, `
+{
+ "virtualIps": [
+ {
+ "id": 1000,
+ "address": "206.10.10.210",
+ "type": "PUBLIC"
+ }
+ ]
+}
+ `)
+ })
+}
+
+func mockCreateResponse(t *testing.T, lbID int) {
+ th.Mux.HandleFunc(_rootURL(lbID), func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "POST")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+ th.TestJSONRequest(t, r, `
+{
+ "type":"PUBLIC",
+ "ipVersion":"IPV6"
+}
+ `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusAccepted)
+
+ fmt.Fprintf(w, `
+{
+ "address":"fd24:f480:ce44:91bc:1af2:15ff:0000:0002",
+ "id":9000134,
+ "type":"PUBLIC",
+ "ipVersion":"IPV6"
+}
+ `)
+ })
+}
+
+func mockBatchDeleteResponse(t *testing.T, lbID int, ids []int) {
+ th.Mux.HandleFunc(_rootURL(lbID), func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "DELETE")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+ r.ParseForm()
+
+ for k, v := range ids {
+ fids := r.Form["id"]
+ th.AssertEquals(t, strconv.Itoa(v), fids[k])
+ }
+
+ w.WriteHeader(http.StatusAccepted)
+ })
+}
+
+func mockDeleteResponse(t *testing.T, lbID, vipID int) {
+ url := _rootURL(lbID) + "/" + strconv.Itoa(vipID)
+ th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "DELETE")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ w.WriteHeader(http.StatusAccepted)
+ })
+}
diff --git a/rackspace/lb/v1/vips/requests.go b/rackspace/lb/v1/vips/requests.go
new file mode 100644
index 0000000..42f0c1d
--- /dev/null
+++ b/rackspace/lb/v1/vips/requests.go
@@ -0,0 +1,112 @@
+package vips
+
+import (
+ "errors"
+
+ "github.com/racker/perigee"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// List is the operation responsible for returning a paginated collection of
+// load balancer virtual IP addresses.
+func List(client *gophercloud.ServiceClient, loadBalancerID int) pagination.Pager {
+ url := rootURL(client, loadBalancerID)
+ return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page {
+ return VIPPage{pagination.SinglePageBase(r)}
+ })
+}
+
+// CreateOptsBuilder is the interface options structs have to satisfy in order
+// to be used in the main Create operation in this package. Since many
+// extensions decorate or modify the common logic, it is useful for them to
+// satisfy a basic interface in order for them to be used.
+type CreateOptsBuilder interface {
+ ToVIPCreateMap() (map[string]interface{}, error)
+}
+
+// CreateOpts is the common options struct used in this package's Create
+// operation.
+type CreateOpts struct {
+ // Optional - the ID of an existing virtual IP. By doing this, you are
+ // allowing load balancers to share IPV6 addresses.
+ ID string
+
+ // Optional - the type of address.
+ Type Type
+
+ // Optional - the version of address.
+ Version Version
+}
+
+// ToVIPCreateMap casts a CreateOpts struct to a map.
+func (opts CreateOpts) ToVIPCreateMap() (map[string]interface{}, error) {
+ lb := make(map[string]interface{})
+
+ if opts.ID != "" {
+ lb["id"] = opts.ID
+ }
+ if opts.Type != "" {
+ lb["type"] = opts.Type
+ }
+ if opts.Version != "" {
+ lb["ipVersion"] = opts.Version
+ }
+
+ return lb, nil
+}
+
+// Create is the operation responsible for assigning a new Virtual IP to an
+// existing load balancer resource. Currently, only version 6 IP addresses may
+// be added.
+func Create(c *gophercloud.ServiceClient, lbID int, opts CreateOptsBuilder) CreateResult {
+ var res CreateResult
+
+ reqBody, err := opts.ToVIPCreateMap()
+ if err != nil {
+ res.Err = err
+ return res
+ }
+
+ _, res.Err = perigee.Request("POST", rootURL(c, lbID), perigee.Options{
+ MoreHeaders: c.AuthenticatedHeaders(),
+ ReqBody: &reqBody,
+ Results: &res.Body,
+ OkCodes: []int{202},
+ })
+
+ return res
+}
+
+// BulkDelete is the operation responsible for batch deleting multiple VIPs in
+// a single operation. It accepts a slice of integer IDs and will remove them
+// from the load balancer. The maximum limit is 10 VIP removals at once.
+func BulkDelete(c *gophercloud.ServiceClient, loadBalancerID int, vipIDs []int) DeleteResult {
+ var res DeleteResult
+
+ if len(vipIDs) > 10 || len(vipIDs) == 0 {
+ res.Err = errors.New("You must provide a minimum of 1 and a maximum of 10 VIP IDs")
+ return res
+ }
+
+ url := rootURL(c, loadBalancerID)
+ url += gophercloud.IDSliceToQueryString("id", vipIDs)
+
+ _, res.Err = perigee.Request("DELETE", url, perigee.Options{
+ MoreHeaders: c.AuthenticatedHeaders(),
+ OkCodes: []int{202},
+ })
+
+ return res
+}
+
+// Delete is the operation responsible for permanently deleting a VIP.
+func Delete(c *gophercloud.ServiceClient, lbID, vipID int) DeleteResult {
+ var res DeleteResult
+ _, res.Err = perigee.Request("DELETE", resourceURL(c, lbID, vipID), perigee.Options{
+ MoreHeaders: c.AuthenticatedHeaders(),
+ OkCodes: []int{202},
+ })
+ return res
+}
diff --git a/rackspace/lb/v1/vips/requests_test.go b/rackspace/lb/v1/vips/requests_test.go
new file mode 100644
index 0000000..74ac461
--- /dev/null
+++ b/rackspace/lb/v1/vips/requests_test.go
@@ -0,0 +1,87 @@
+package vips
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+ "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+const (
+ lbID = 12345
+ vipID = 67890
+ vipID2 = 67891
+)
+
+func TestList(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockListResponse(t, lbID)
+
+ count := 0
+
+ err := List(client.ServiceClient(), lbID).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractVIPs(page)
+ th.AssertNoErr(t, err)
+
+ expected := []VIP{
+ VIP{ID: 1000, Address: "206.10.10.210", Type: "PUBLIC"},
+ }
+
+ th.CheckDeepEquals(t, expected, actual)
+
+ return true, nil
+ })
+
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, 1, count)
+}
+
+func TestCreate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockCreateResponse(t, lbID)
+
+ opts := CreateOpts{
+ Type: "PUBLIC",
+ Version: "IPV6",
+ }
+
+ vip, err := Create(client.ServiceClient(), lbID, opts).Extract()
+ th.AssertNoErr(t, err)
+
+ expected := &VIP{
+ Address: "fd24:f480:ce44:91bc:1af2:15ff:0000:0002",
+ ID: 9000134,
+ Type: "PUBLIC",
+ Version: "IPV6",
+ }
+
+ th.CheckDeepEquals(t, expected, vip)
+}
+
+func TestBulkDelete(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ ids := []int{vipID, vipID2}
+
+ mockBatchDeleteResponse(t, lbID, ids)
+
+ err := BulkDelete(client.ServiceClient(), lbID, ids).ExtractErr()
+ th.AssertNoErr(t, err)
+}
+
+func TestDelete(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockDeleteResponse(t, lbID, vipID)
+
+ err := Delete(client.ServiceClient(), lbID, vipID).ExtractErr()
+ th.AssertNoErr(t, err)
+}
diff --git a/rackspace/lb/v1/vips/results.go b/rackspace/lb/v1/vips/results.go
new file mode 100644
index 0000000..678b2af
--- /dev/null
+++ b/rackspace/lb/v1/vips/results.go
@@ -0,0 +1,89 @@
+package vips
+
+import (
+ "github.com/mitchellh/mapstructure"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// VIP represents a Virtual IP API resource.
+type VIP struct {
+ Address string `json:"address,omitempty"`
+ ID int `json:"id,omitempty"`
+ Type Type `json:"type,omitempty"`
+ Version Version `json:"ipVersion,omitempty" mapstructure:"ipVersion"`
+}
+
+// Version represents the version of a VIP.
+type Version string
+
+// Convenient constants to use for type
+const (
+ IPV4 Version = "IPV4"
+ IPV6 Version = "IPV6"
+)
+
+// Type represents the type of a VIP.
+type Type string
+
+const (
+ // PUBLIC indicates a VIP type that is routable on the public Internet.
+ PUBLIC Type = "PUBLIC"
+
+ // PRIVATE indicates a VIP type that is routable only on ServiceNet.
+ PRIVATE Type = "SERVICENET"
+)
+
+// VIPPage is the page returned by a pager when traversing over a collection
+// of VIPs.
+type VIPPage struct {
+ pagination.SinglePageBase
+}
+
+// IsEmpty checks whether a VIPPage struct is empty.
+func (p VIPPage) IsEmpty() (bool, error) {
+ is, err := ExtractVIPs(p)
+ if err != nil {
+ return true, nil
+ }
+ return len(is) == 0, nil
+}
+
+// ExtractVIPs accepts a Page struct, specifically a VIPPage struct, and
+// extracts the elements into a slice of VIP structs. In other words, a
+// generic collection is mapped into a relevant slice.
+func ExtractVIPs(page pagination.Page) ([]VIP, error) {
+ var resp struct {
+ VIPs []VIP `mapstructure:"virtualIps" json:"virtualIps"`
+ }
+
+ err := mapstructure.Decode(page.(VIPPage).Body, &resp)
+
+ return resp.VIPs, err
+}
+
+type commonResult struct {
+ gophercloud.Result
+}
+
+func (r commonResult) Extract() (*VIP, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ resp := &VIP{}
+ err := mapstructure.Decode(r.Body, resp)
+
+ return resp, err
+}
+
+// CreateResult represents the result of a create operation.
+type CreateResult struct {
+ commonResult
+}
+
+// DeleteResult represents the result of a delete operation.
+type DeleteResult struct {
+ gophercloud.ErrResult
+}
diff --git a/rackspace/lb/v1/vips/urls.go b/rackspace/lb/v1/vips/urls.go
new file mode 100644
index 0000000..28f063a
--- /dev/null
+++ b/rackspace/lb/v1/vips/urls.go
@@ -0,0 +1,20 @@
+package vips
+
+import (
+ "strconv"
+
+ "github.com/rackspace/gophercloud"
+)
+
+const (
+ lbPath = "loadbalancers"
+ vipPath = "virtualips"
+)
+
+func resourceURL(c *gophercloud.ServiceClient, lbID, nodeID int) string {
+ return c.ServiceURL(lbPath, strconv.Itoa(lbID), vipPath, strconv.Itoa(nodeID))
+}
+
+func rootURL(c *gophercloud.ServiceClient, lbID int) string {
+ return c.ServiceURL(lbPath, strconv.Itoa(lbID), vipPath)
+}
diff --git a/rackspace/networking/v2/common/common_tests.go b/rackspace/networking/v2/common/common_tests.go
new file mode 100644
index 0000000..129cd63
--- /dev/null
+++ b/rackspace/networking/v2/common/common_tests.go
@@ -0,0 +1,12 @@
+package common
+
+import (
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+const TokenID = client.TokenID
+
+func ServiceClient() *gophercloud.ServiceClient {
+ return client.ServiceClient()
+}
diff --git a/rackspace/networking/v2/networks/delegate.go b/rackspace/networking/v2/networks/delegate.go
new file mode 100644
index 0000000..dcb0855
--- /dev/null
+++ b/rackspace/networking/v2/networks/delegate.go
@@ -0,0 +1,41 @@
+package networks
+
+import (
+ "github.com/rackspace/gophercloud"
+ os "github.com/rackspace/gophercloud/openstack/networking/v2/networks"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// List returns a Pager which allows you to iterate over a collection of
+// networks. It accepts a ListOpts struct, which allows you to filter and sort
+// the returned collection for greater efficiency.
+func List(c *gophercloud.ServiceClient, opts os.ListOptsBuilder) pagination.Pager {
+ return os.List(c, opts)
+}
+
+// Get retrieves a specific network based on its unique ID.
+func Get(c *gophercloud.ServiceClient, networkID string) os.GetResult {
+ return os.Get(c, networkID)
+}
+
+// Create accepts a CreateOpts struct and creates a new network using the values
+// provided. This operation does not actually require a request body, i.e. the
+// CreateOpts struct argument can be empty.
+//
+// The tenant ID that is contained in the URI is the tenant that creates the
+// network. An admin user, however, has the option of specifying another tenant
+// ID in the CreateOpts struct.
+func Create(c *gophercloud.ServiceClient, opts os.CreateOptsBuilder) os.CreateResult {
+ return os.Create(c, opts)
+}
+
+// Update accepts a UpdateOpts struct and updates an existing network using the
+// values provided. For more information, see the Create function.
+func Update(c *gophercloud.ServiceClient, networkID string, opts os.UpdateOptsBuilder) os.UpdateResult {
+ return os.Update(c, networkID, opts)
+}
+
+// Delete accepts a unique ID and deletes the network associated with it.
+func Delete(c *gophercloud.ServiceClient, networkID string) os.DeleteResult {
+ return os.Delete(c, networkID)
+}
diff --git a/rackspace/networking/v2/networks/delegate_test.go b/rackspace/networking/v2/networks/delegate_test.go
new file mode 100644
index 0000000..f51c732
--- /dev/null
+++ b/rackspace/networking/v2/networks/delegate_test.go
@@ -0,0 +1,276 @@
+package networks
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ os "github.com/rackspace/gophercloud/openstack/networking/v2/networks"
+ "github.com/rackspace/gophercloud/pagination"
+ fake "github.com/rackspace/gophercloud/rackspace/networking/v2/common"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestList(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/networks", 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, `
+{
+ "networks": [
+ {
+ "status": "ACTIVE",
+ "subnets": [
+ "54d6f61d-db07-451c-9ab3-b9609b6b6f0b"
+ ],
+ "name": "private-network",
+ "admin_state_up": true,
+ "tenant_id": "4fd44f30292945e481c7b8a0c8908869",
+ "shared": true,
+ "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22"
+ },
+ {
+ "status": "ACTIVE",
+ "subnets": [
+ "08eae331-0402-425a-923c-34f7cfe39c1b"
+ ],
+ "name": "private",
+ "admin_state_up": true,
+ "tenant_id": "26a7980765d0414dbc1fc1f88cdb7e6e",
+ "shared": true,
+ "id": "db193ab3-96e3-4cb3-8fc5-05f4296d0324"
+ }
+ ]
+}
+ `)
+ })
+
+ client := fake.ServiceClient()
+ count := 0
+
+ List(client, os.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := os.ExtractNetworks(page)
+ if err != nil {
+ t.Errorf("Failed to extract networks: %v", err)
+ return false, err
+ }
+
+ expected := []os.Network{
+ os.Network{
+ Status: "ACTIVE",
+ Subnets: []string{"54d6f61d-db07-451c-9ab3-b9609b6b6f0b"},
+ Name: "private-network",
+ AdminStateUp: true,
+ TenantID: "4fd44f30292945e481c7b8a0c8908869",
+ Shared: true,
+ ID: "d32019d3-bc6e-4319-9c1d-6722fc136a22",
+ },
+ os.Network{
+ Status: "ACTIVE",
+ Subnets: []string{"08eae331-0402-425a-923c-34f7cfe39c1b"},
+ Name: "private",
+ AdminStateUp: true,
+ TenantID: "26a7980765d0414dbc1fc1f88cdb7e6e",
+ Shared: true,
+ ID: "db193ab3-96e3-4cb3-8fc5-05f4296d0324",
+ },
+ }
+
+ th.CheckDeepEquals(t, expected, actual)
+
+ return true, nil
+ })
+
+ if count != 1 {
+ t.Errorf("Expected 1 page, got %d", count)
+ }
+}
+
+func TestGet(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/networks/d32019d3-bc6e-4319-9c1d-6722fc136a22", 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, `
+{
+ "network": {
+ "status": "ACTIVE",
+ "subnets": [
+ "54d6f61d-db07-451c-9ab3-b9609b6b6f0b"
+ ],
+ "name": "private-network",
+ "admin_state_up": true,
+ "tenant_id": "4fd44f30292945e481c7b8a0c8908869",
+ "shared": true,
+ "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22"
+ }
+}
+ `)
+ })
+
+ n, err := Get(fake.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22").Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, n.Status, "ACTIVE")
+ th.AssertDeepEquals(t, n.Subnets, []string{"54d6f61d-db07-451c-9ab3-b9609b6b6f0b"})
+ th.AssertEquals(t, n.Name, "private-network")
+ th.AssertEquals(t, n.AdminStateUp, true)
+ th.AssertEquals(t, n.TenantID, "4fd44f30292945e481c7b8a0c8908869")
+ th.AssertEquals(t, n.Shared, true)
+ th.AssertEquals(t, n.ID, "d32019d3-bc6e-4319-9c1d-6722fc136a22")
+}
+
+func TestCreate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/networks", 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, `
+{
+ "network": {
+ "name": "sample_network",
+ "admin_state_up": true
+ }
+}
+ `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusCreated)
+
+ fmt.Fprintf(w, `
+{
+ "network": {
+ "status": "ACTIVE",
+ "subnets": [],
+ "name": "net1",
+ "admin_state_up": true,
+ "tenant_id": "9bacb3c5d39d41a79512987f338cf177",
+ "shared": false,
+ "id": "4e8e5957-649f-477b-9e5b-f1f75b21c03c"
+ }
+}
+ `)
+ })
+
+ iTrue := true
+ options := os.CreateOpts{Name: "sample_network", AdminStateUp: &iTrue}
+ n, err := Create(fake.ServiceClient(), options).Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, n.Status, "ACTIVE")
+ th.AssertDeepEquals(t, n.Subnets, []string{})
+ th.AssertEquals(t, n.Name, "net1")
+ th.AssertEquals(t, n.AdminStateUp, true)
+ th.AssertEquals(t, n.TenantID, "9bacb3c5d39d41a79512987f338cf177")
+ th.AssertEquals(t, n.Shared, false)
+ th.AssertEquals(t, n.ID, "4e8e5957-649f-477b-9e5b-f1f75b21c03c")
+}
+
+func TestCreateWithOptionalFields(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/networks", 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, `
+{
+ "network": {
+ "name": "sample_network",
+ "admin_state_up": true,
+ "shared": true,
+ "tenant_id": "12345"
+ }
+}
+ `)
+
+ w.WriteHeader(http.StatusCreated)
+ })
+
+ iTrue := true
+ options := os.CreateOpts{Name: "sample_network", AdminStateUp: &iTrue, Shared: &iTrue, TenantID: "12345"}
+ _, err := Create(fake.ServiceClient(), options).Extract()
+ th.AssertNoErr(t, err)
+}
+
+func TestUpdate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/networks/4e8e5957-649f-477b-9e5b-f1f75b21c03c", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "PUT")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ th.TestHeader(t, r, "Content-Type", "application/json")
+ th.TestHeader(t, r, "Accept", "application/json")
+ th.TestJSONRequest(t, r, `
+{
+ "network": {
+ "name": "new_network_name",
+ "admin_state_up": false,
+ "shared": true
+ }
+}
+ `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, `
+{
+ "network": {
+ "status": "ACTIVE",
+ "subnets": [],
+ "name": "new_network_name",
+ "admin_state_up": false,
+ "tenant_id": "4fd44f30292945e481c7b8a0c8908869",
+ "shared": true,
+ "id": "4e8e5957-649f-477b-9e5b-f1f75b21c03c"
+ }
+}
+ `)
+ })
+
+ iTrue := true
+ options := os.UpdateOpts{Name: "new_network_name", AdminStateUp: os.Down, Shared: &iTrue}
+ n, err := Update(fake.ServiceClient(), "4e8e5957-649f-477b-9e5b-f1f75b21c03c", options).Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, n.Name, "new_network_name")
+ th.AssertEquals(t, n.AdminStateUp, false)
+ th.AssertEquals(t, n.Shared, true)
+ th.AssertEquals(t, n.ID, "4e8e5957-649f-477b-9e5b-f1f75b21c03c")
+}
+
+func TestDelete(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/networks/4e8e5957-649f-477b-9e5b-f1f75b21c03c", 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)
+ })
+
+ res := Delete(fake.ServiceClient(), "4e8e5957-649f-477b-9e5b-f1f75b21c03c")
+ th.AssertNoErr(t, res.Err)
+}
diff --git a/rackspace/networking/v2/ports/delegate.go b/rackspace/networking/v2/ports/delegate.go
new file mode 100644
index 0000000..091b99e
--- /dev/null
+++ b/rackspace/networking/v2/ports/delegate.go
@@ -0,0 +1,40 @@
+package ports
+
+import (
+ "github.com/rackspace/gophercloud"
+ os "github.com/rackspace/gophercloud/openstack/networking/v2/ports"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// List returns a Pager which allows you to iterate over a collection of
+// ports. It accepts a ListOpts struct, which allows you to filter and sort
+// the returned collection for greater efficiency.
+//
+// Default policy settings return only those ports that are owned by the tenant
+// who submits the request, unless the request is submitted by a user with
+// administrative rights.
+func List(c *gophercloud.ServiceClient, opts os.ListOptsBuilder) pagination.Pager {
+ return os.List(c, opts)
+}
+
+// Get retrieves a specific port based on its unique ID.
+func Get(c *gophercloud.ServiceClient, networkID string) os.GetResult {
+ return os.Get(c, networkID)
+}
+
+// Create accepts a CreateOpts struct and creates a new network using the values
+// provided. You must remember to provide a NetworkID value.
+func Create(c *gophercloud.ServiceClient, opts os.CreateOptsBuilder) os.CreateResult {
+ return os.Create(c, opts)
+}
+
+// Update accepts a UpdateOpts struct and updates an existing port using the
+// values provided.
+func Update(c *gophercloud.ServiceClient, networkID string, opts os.UpdateOptsBuilder) os.UpdateResult {
+ return os.Update(c, networkID, opts)
+}
+
+// Delete accepts a unique ID and deletes the port associated with it.
+func Delete(c *gophercloud.ServiceClient, networkID string) os.DeleteResult {
+ return os.Delete(c, networkID)
+}
diff --git a/rackspace/networking/v2/ports/delegate_test.go b/rackspace/networking/v2/ports/delegate_test.go
new file mode 100644
index 0000000..f53ff59
--- /dev/null
+++ b/rackspace/networking/v2/ports/delegate_test.go
@@ -0,0 +1,322 @@
+package ports
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ os "github.com/rackspace/gophercloud/openstack/networking/v2/ports"
+ "github.com/rackspace/gophercloud/pagination"
+ fake "github.com/rackspace/gophercloud/rackspace/networking/v2/common"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestList(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/ports", 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, `
+{
+ "ports": [
+ {
+ "status": "ACTIVE",
+ "binding:host_id": "devstack",
+ "name": "",
+ "admin_state_up": true,
+ "network_id": "70c1db1f-b701-45bd-96e0-a313ee3430b3",
+ "tenant_id": "",
+ "device_owner": "network:router_gateway",
+ "mac_address": "fa:16:3e:58:42:ed",
+ "fixed_ips": [
+ {
+ "subnet_id": "008ba151-0b8c-4a67-98b5-0d2b87666062",
+ "ip_address": "172.24.4.2"
+ }
+ ],
+ "id": "d80b1a3b-4fc1-49f3-952e-1e2ab7081d8b",
+ "security_groups": [],
+ "device_id": "9ae135f4-b6e0-4dad-9e91-3c223e385824"
+ }
+ ]
+}
+ `)
+ })
+
+ count := 0
+
+ List(fake.ServiceClient(), os.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := os.ExtractPorts(page)
+ if err != nil {
+ t.Errorf("Failed to extract subnets: %v", err)
+ return false, nil
+ }
+
+ expected := []os.Port{
+ os.Port{
+ Status: "ACTIVE",
+ Name: "",
+ AdminStateUp: true,
+ NetworkID: "70c1db1f-b701-45bd-96e0-a313ee3430b3",
+ TenantID: "",
+ DeviceOwner: "network:router_gateway",
+ MACAddress: "fa:16:3e:58:42:ed",
+ FixedIPs: []os.IP{
+ os.IP{
+ SubnetID: "008ba151-0b8c-4a67-98b5-0d2b87666062",
+ IPAddress: "172.24.4.2",
+ },
+ },
+ ID: "d80b1a3b-4fc1-49f3-952e-1e2ab7081d8b",
+ SecurityGroups: []string{},
+ DeviceID: "9ae135f4-b6e0-4dad-9e91-3c223e385824",
+ },
+ }
+
+ th.CheckDeepEquals(t, expected, actual)
+
+ return true, nil
+ })
+
+ if count != 1 {
+ t.Errorf("Expected 1 page, got %d", count)
+ }
+}
+
+func TestGet(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/ports/46d4bfb9-b26e-41f3-bd2e-e6dcc1ccedb2", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, `
+{
+ "port": {
+ "status": "ACTIVE",
+ "name": "",
+ "admin_state_up": true,
+ "network_id": "a87cc70a-3e15-4acf-8205-9b711a3531b7",
+ "tenant_id": "7e02058126cc4950b75f9970368ba177",
+ "device_owner": "network:router_interface",
+ "mac_address": "fa:16:3e:23:fd:d7",
+ "fixed_ips": [
+ {
+ "subnet_id": "a0304c3a-4f08-4c43-88af-d796509c97d2",
+ "ip_address": "10.0.0.1"
+ }
+ ],
+ "id": "46d4bfb9-b26e-41f3-bd2e-e6dcc1ccedb2",
+ "security_groups": [],
+ "device_id": "5e3898d7-11be-483e-9732-b2f5eccd2b2e"
+ }
+}
+ `)
+ })
+
+ n, err := Get(fake.ServiceClient(), "46d4bfb9-b26e-41f3-bd2e-e6dcc1ccedb2").Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, n.Status, "ACTIVE")
+ th.AssertEquals(t, n.Name, "")
+ th.AssertEquals(t, n.AdminStateUp, true)
+ th.AssertEquals(t, n.NetworkID, "a87cc70a-3e15-4acf-8205-9b711a3531b7")
+ th.AssertEquals(t, n.TenantID, "7e02058126cc4950b75f9970368ba177")
+ th.AssertEquals(t, n.DeviceOwner, "network:router_interface")
+ th.AssertEquals(t, n.MACAddress, "fa:16:3e:23:fd:d7")
+ th.AssertDeepEquals(t, n.FixedIPs, []os.IP{
+ os.IP{SubnetID: "a0304c3a-4f08-4c43-88af-d796509c97d2", IPAddress: "10.0.0.1"},
+ })
+ th.AssertEquals(t, n.ID, "46d4bfb9-b26e-41f3-bd2e-e6dcc1ccedb2")
+ th.AssertDeepEquals(t, n.SecurityGroups, []string{})
+ th.AssertEquals(t, n.Status, "ACTIVE")
+ th.AssertEquals(t, n.DeviceID, "5e3898d7-11be-483e-9732-b2f5eccd2b2e")
+}
+
+func TestCreate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/ports", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "POST")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ th.TestHeader(t, r, "Content-Type", "application/json")
+ th.TestHeader(t, r, "Accept", "application/json")
+ th.TestJSONRequest(t, r, `
+{
+ "port": {
+ "network_id": "a87cc70a-3e15-4acf-8205-9b711a3531b7",
+ "name": "private-port",
+ "admin_state_up": true,
+ "fixed_ips": [
+ {
+ "subnet_id": "a0304c3a-4f08-4c43-88af-d796509c97d2",
+ "ip_address": "10.0.0.2"
+ }
+ ],
+ "security_groups": ["foo"]
+ }
+}
+ `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusCreated)
+
+ fmt.Fprintf(w, `
+{
+ "port": {
+ "status": "DOWN",
+ "name": "private-port",
+ "allowed_address_pairs": [],
+ "admin_state_up": true,
+ "network_id": "a87cc70a-3e15-4acf-8205-9b711a3531b7",
+ "tenant_id": "d6700c0c9ffa4f1cb322cd4a1f3906fa",
+ "device_owner": "",
+ "mac_address": "fa:16:3e:c9:cb:f0",
+ "fixed_ips": [
+ {
+ "subnet_id": "a0304c3a-4f08-4c43-88af-d796509c97d2",
+ "ip_address": "10.0.0.2"
+ }
+ ],
+ "id": "65c0ee9f-d634-4522-8954-51021b570b0d",
+ "security_groups": [
+ "f0ac4394-7e4a-4409-9701-ba8be283dbc3"
+ ],
+ "device_id": ""
+ }
+}
+ `)
+ })
+
+ asu := true
+ options := os.CreateOpts{
+ Name: "private-port",
+ AdminStateUp: &asu,
+ NetworkID: "a87cc70a-3e15-4acf-8205-9b711a3531b7",
+ FixedIPs: []os.IP{
+ os.IP{SubnetID: "a0304c3a-4f08-4c43-88af-d796509c97d2", IPAddress: "10.0.0.2"},
+ },
+ SecurityGroups: []string{"foo"},
+ }
+ n, err := Create(fake.ServiceClient(), options).Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, n.Status, "DOWN")
+ th.AssertEquals(t, n.Name, "private-port")
+ th.AssertEquals(t, n.AdminStateUp, true)
+ th.AssertEquals(t, n.NetworkID, "a87cc70a-3e15-4acf-8205-9b711a3531b7")
+ th.AssertEquals(t, n.TenantID, "d6700c0c9ffa4f1cb322cd4a1f3906fa")
+ th.AssertEquals(t, n.DeviceOwner, "")
+ th.AssertEquals(t, n.MACAddress, "fa:16:3e:c9:cb:f0")
+ th.AssertDeepEquals(t, n.FixedIPs, []os.IP{
+ os.IP{SubnetID: "a0304c3a-4f08-4c43-88af-d796509c97d2", IPAddress: "10.0.0.2"},
+ })
+ th.AssertEquals(t, n.ID, "65c0ee9f-d634-4522-8954-51021b570b0d")
+ th.AssertDeepEquals(t, n.SecurityGroups, []string{"f0ac4394-7e4a-4409-9701-ba8be283dbc3"})
+}
+
+func TestRequiredCreateOpts(t *testing.T) {
+ res := Create(fake.ServiceClient(), os.CreateOpts{})
+ if res.Err == nil {
+ t.Fatalf("Expected error, got none")
+ }
+}
+
+func TestUpdate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/ports/65c0ee9f-d634-4522-8954-51021b570b0d", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "PUT")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ th.TestHeader(t, r, "Content-Type", "application/json")
+ th.TestHeader(t, r, "Accept", "application/json")
+ th.TestJSONRequest(t, r, `
+{
+ "port": {
+ "name": "new_port_name",
+ "fixed_ips": [
+ {
+ "subnet_id": "a0304c3a-4f08-4c43-88af-d796509c97d2",
+ "ip_address": "10.0.0.3"
+ }
+ ],
+ "security_groups": [
+ "f0ac4394-7e4a-4409-9701-ba8be283dbc3"
+ ]
+ }
+}
+ `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, `
+{
+ "port": {
+ "status": "DOWN",
+ "name": "new_port_name",
+ "admin_state_up": true,
+ "network_id": "a87cc70a-3e15-4acf-8205-9b711a3531b7",
+ "tenant_id": "d6700c0c9ffa4f1cb322cd4a1f3906fa",
+ "device_owner": "",
+ "mac_address": "fa:16:3e:c9:cb:f0",
+ "fixed_ips": [
+ {
+ "subnet_id": "a0304c3a-4f08-4c43-88af-d796509c97d2",
+ "ip_address": "10.0.0.3"
+ }
+ ],
+ "id": "65c0ee9f-d634-4522-8954-51021b570b0d",
+ "security_groups": [
+ "f0ac4394-7e4a-4409-9701-ba8be283dbc3"
+ ],
+ "device_id": ""
+ }
+}
+ `)
+ })
+
+ options := os.UpdateOpts{
+ Name: "new_port_name",
+ FixedIPs: []os.IP{
+ os.IP{SubnetID: "a0304c3a-4f08-4c43-88af-d796509c97d2", IPAddress: "10.0.0.3"},
+ },
+ SecurityGroups: []string{"f0ac4394-7e4a-4409-9701-ba8be283dbc3"},
+ }
+
+ s, err := Update(fake.ServiceClient(), "65c0ee9f-d634-4522-8954-51021b570b0d", options).Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, s.Name, "new_port_name")
+ th.AssertDeepEquals(t, s.FixedIPs, []os.IP{
+ os.IP{SubnetID: "a0304c3a-4f08-4c43-88af-d796509c97d2", IPAddress: "10.0.0.3"},
+ })
+ th.AssertDeepEquals(t, s.SecurityGroups, []string{"f0ac4394-7e4a-4409-9701-ba8be283dbc3"})
+}
+
+func TestDelete(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/ports/65c0ee9f-d634-4522-8954-51021b570b0d", 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)
+ })
+
+ res := Delete(fake.ServiceClient(), "65c0ee9f-d634-4522-8954-51021b570b0d")
+ th.AssertNoErr(t, res.Err)
+}
diff --git a/rackspace/networking/v2/subnets/delegate.go b/rackspace/networking/v2/subnets/delegate.go
new file mode 100644
index 0000000..a7fb7bb
--- /dev/null
+++ b/rackspace/networking/v2/subnets/delegate.go
@@ -0,0 +1,40 @@
+package subnets
+
+import (
+ "github.com/rackspace/gophercloud"
+ os "github.com/rackspace/gophercloud/openstack/networking/v2/subnets"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// List returns a Pager which allows you to iterate over a collection of
+// subnets. It accepts a ListOpts struct, which allows you to filter and sort
+// the returned collection for greater efficiency.
+//
+// Default policy settings return only those subnets that are owned by the tenant
+// who submits the request, unless the request is submitted by a user with
+// administrative rights.
+func List(c *gophercloud.ServiceClient, opts os.ListOptsBuilder) pagination.Pager {
+ return os.List(c, opts)
+}
+
+// Get retrieves a specific subnet based on its unique ID.
+func Get(c *gophercloud.ServiceClient, networkID string) os.GetResult {
+ return os.Get(c, networkID)
+}
+
+// Create accepts a CreateOpts struct and creates a new subnet using the values
+// provided. You must remember to provide a valid NetworkID, CIDR and IP version.
+func Create(c *gophercloud.ServiceClient, opts os.CreateOptsBuilder) os.CreateResult {
+ return os.Create(c, opts)
+}
+
+// Update accepts a UpdateOpts struct and updates an existing subnet using the
+// values provided.
+func Update(c *gophercloud.ServiceClient, networkID string, opts os.UpdateOptsBuilder) os.UpdateResult {
+ return os.Update(c, networkID, opts)
+}
+
+// Delete accepts a unique ID and deletes the subnet associated with it.
+func Delete(c *gophercloud.ServiceClient, networkID string) os.DeleteResult {
+ return os.Delete(c, networkID)
+}
diff --git a/rackspace/networking/v2/subnets/delegate_test.go b/rackspace/networking/v2/subnets/delegate_test.go
new file mode 100644
index 0000000..fafc6fb
--- /dev/null
+++ b/rackspace/networking/v2/subnets/delegate_test.go
@@ -0,0 +1,363 @@
+package subnets
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ os "github.com/rackspace/gophercloud/openstack/networking/v2/subnets"
+ "github.com/rackspace/gophercloud/pagination"
+ fake "github.com/rackspace/gophercloud/rackspace/networking/v2/common"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestList(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/subnets", 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, `
+{
+ "subnets": [
+ {
+ "name": "private-subnet",
+ "enable_dhcp": true,
+ "network_id": "db193ab3-96e3-4cb3-8fc5-05f4296d0324",
+ "tenant_id": "26a7980765d0414dbc1fc1f88cdb7e6e",
+ "dns_nameservers": [],
+ "allocation_pools": [
+ {
+ "start": "10.0.0.2",
+ "end": "10.0.0.254"
+ }
+ ],
+ "host_routes": [],
+ "ip_version": 4,
+ "gateway_ip": "10.0.0.1",
+ "cidr": "10.0.0.0/24",
+ "id": "08eae331-0402-425a-923c-34f7cfe39c1b"
+ },
+ {
+ "name": "my_subnet",
+ "enable_dhcp": true,
+ "network_id": "d32019d3-bc6e-4319-9c1d-6722fc136a22",
+ "tenant_id": "4fd44f30292945e481c7b8a0c8908869",
+ "dns_nameservers": [],
+ "allocation_pools": [
+ {
+ "start": "192.0.0.2",
+ "end": "192.255.255.254"
+ }
+ ],
+ "host_routes": [],
+ "ip_version": 4,
+ "gateway_ip": "192.0.0.1",
+ "cidr": "192.0.0.0/8",
+ "id": "54d6f61d-db07-451c-9ab3-b9609b6b6f0b"
+ }
+ ]
+}
+ `)
+ })
+
+ count := 0
+
+ List(fake.ServiceClient(), os.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := os.ExtractSubnets(page)
+ if err != nil {
+ t.Errorf("Failed to extract subnets: %v", err)
+ return false, nil
+ }
+
+ expected := []os.Subnet{
+ os.Subnet{
+ Name: "private-subnet",
+ EnableDHCP: true,
+ NetworkID: "db193ab3-96e3-4cb3-8fc5-05f4296d0324",
+ TenantID: "26a7980765d0414dbc1fc1f88cdb7e6e",
+ DNSNameservers: []string{},
+ AllocationPools: []os.AllocationPool{
+ os.AllocationPool{
+ Start: "10.0.0.2",
+ End: "10.0.0.254",
+ },
+ },
+ HostRoutes: []os.HostRoute{},
+ IPVersion: 4,
+ GatewayIP: "10.0.0.1",
+ CIDR: "10.0.0.0/24",
+ ID: "08eae331-0402-425a-923c-34f7cfe39c1b",
+ },
+ os.Subnet{
+ Name: "my_subnet",
+ EnableDHCP: true,
+ NetworkID: "d32019d3-bc6e-4319-9c1d-6722fc136a22",
+ TenantID: "4fd44f30292945e481c7b8a0c8908869",
+ DNSNameservers: []string{},
+ AllocationPools: []os.AllocationPool{
+ os.AllocationPool{
+ Start: "192.0.0.2",
+ End: "192.255.255.254",
+ },
+ },
+ HostRoutes: []os.HostRoute{},
+ IPVersion: 4,
+ GatewayIP: "192.0.0.1",
+ CIDR: "192.0.0.0/8",
+ ID: "54d6f61d-db07-451c-9ab3-b9609b6b6f0b",
+ },
+ }
+
+ th.CheckDeepEquals(t, expected, actual)
+
+ return true, nil
+ })
+
+ if count != 1 {
+ t.Errorf("Expected 1 page, got %d", count)
+ }
+}
+
+func TestGet(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/subnets/54d6f61d-db07-451c-9ab3-b9609b6b6f0b", 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, `
+{
+ "subnet": {
+ "name": "my_subnet",
+ "enable_dhcp": true,
+ "network_id": "d32019d3-bc6e-4319-9c1d-6722fc136a22",
+ "tenant_id": "4fd44f30292945e481c7b8a0c8908869",
+ "dns_nameservers": [],
+ "allocation_pools": [
+ {
+ "start": "192.0.0.2",
+ "end": "192.255.255.254"
+ }
+ ],
+ "host_routes": [],
+ "ip_version": 4,
+ "gateway_ip": "192.0.0.1",
+ "cidr": "192.0.0.0/8",
+ "id": "54d6f61d-db07-451c-9ab3-b9609b6b6f0b"
+ }
+}
+ `)
+ })
+
+ s, err := Get(fake.ServiceClient(), "54d6f61d-db07-451c-9ab3-b9609b6b6f0b").Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, s.Name, "my_subnet")
+ th.AssertEquals(t, s.EnableDHCP, true)
+ th.AssertEquals(t, s.NetworkID, "d32019d3-bc6e-4319-9c1d-6722fc136a22")
+ th.AssertEquals(t, s.TenantID, "4fd44f30292945e481c7b8a0c8908869")
+ th.AssertDeepEquals(t, s.DNSNameservers, []string{})
+ th.AssertDeepEquals(t, s.AllocationPools, []os.AllocationPool{
+ os.AllocationPool{
+ Start: "192.0.0.2",
+ End: "192.255.255.254",
+ },
+ })
+ th.AssertDeepEquals(t, s.HostRoutes, []os.HostRoute{})
+ th.AssertEquals(t, s.IPVersion, 4)
+ th.AssertEquals(t, s.GatewayIP, "192.0.0.1")
+ th.AssertEquals(t, s.CIDR, "192.0.0.0/8")
+ th.AssertEquals(t, s.ID, "54d6f61d-db07-451c-9ab3-b9609b6b6f0b")
+}
+
+func TestCreate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/subnets", 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, `
+{
+ "subnet": {
+ "network_id": "d32019d3-bc6e-4319-9c1d-6722fc136a22",
+ "ip_version": 4,
+ "cidr": "192.168.199.0/24",
+ "dns_nameservers": ["foo"],
+ "allocation_pools": [
+ {
+ "start": "192.168.199.2",
+ "end": "192.168.199.254"
+ }
+ ],
+ "host_routes": [{"destination":"","nexthop": "bar"}]
+ }
+}
+ `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusCreated)
+
+ fmt.Fprintf(w, `
+{
+ "subnet": {
+ "name": "",
+ "enable_dhcp": true,
+ "network_id": "d32019d3-bc6e-4319-9c1d-6722fc136a22",
+ "tenant_id": "4fd44f30292945e481c7b8a0c8908869",
+ "dns_nameservers": [],
+ "allocation_pools": [
+ {
+ "start": "192.168.199.2",
+ "end": "192.168.199.254"
+ }
+ ],
+ "host_routes": [],
+ "ip_version": 4,
+ "gateway_ip": "192.168.199.1",
+ "cidr": "192.168.199.0/24",
+ "id": "3b80198d-4f7b-4f77-9ef5-774d54e17126"
+ }
+}
+ `)
+ })
+
+ opts := os.CreateOpts{
+ NetworkID: "d32019d3-bc6e-4319-9c1d-6722fc136a22",
+ IPVersion: 4,
+ CIDR: "192.168.199.0/24",
+ AllocationPools: []os.AllocationPool{
+ os.AllocationPool{
+ Start: "192.168.199.2",
+ End: "192.168.199.254",
+ },
+ },
+ DNSNameservers: []string{"foo"},
+ HostRoutes: []os.HostRoute{
+ os.HostRoute{NextHop: "bar"},
+ },
+ }
+ s, err := Create(fake.ServiceClient(), opts).Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, s.Name, "")
+ th.AssertEquals(t, s.EnableDHCP, true)
+ th.AssertEquals(t, s.NetworkID, "d32019d3-bc6e-4319-9c1d-6722fc136a22")
+ th.AssertEquals(t, s.TenantID, "4fd44f30292945e481c7b8a0c8908869")
+ th.AssertDeepEquals(t, s.DNSNameservers, []string{})
+ th.AssertDeepEquals(t, s.AllocationPools, []os.AllocationPool{
+ os.AllocationPool{
+ Start: "192.168.199.2",
+ End: "192.168.199.254",
+ },
+ })
+ th.AssertDeepEquals(t, s.HostRoutes, []os.HostRoute{})
+ th.AssertEquals(t, s.IPVersion, 4)
+ th.AssertEquals(t, s.GatewayIP, "192.168.199.1")
+ th.AssertEquals(t, s.CIDR, "192.168.199.0/24")
+ th.AssertEquals(t, s.ID, "3b80198d-4f7b-4f77-9ef5-774d54e17126")
+}
+
+func TestRequiredCreateOpts(t *testing.T) {
+ res := Create(fake.ServiceClient(), os.CreateOpts{})
+ if res.Err == nil {
+ t.Fatalf("Expected error, got none")
+ }
+
+ res = Create(fake.ServiceClient(), os.CreateOpts{NetworkID: "foo"})
+ if res.Err == nil {
+ t.Fatalf("Expected error, got none")
+ }
+
+ res = Create(fake.ServiceClient(), os.CreateOpts{NetworkID: "foo", CIDR: "bar", IPVersion: 40})
+ if res.Err == nil {
+ t.Fatalf("Expected error, got none")
+ }
+}
+
+func TestUpdate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/subnets/08eae331-0402-425a-923c-34f7cfe39c1b", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "PUT")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ th.TestHeader(t, r, "Content-Type", "application/json")
+ th.TestHeader(t, r, "Accept", "application/json")
+ th.TestJSONRequest(t, r, `
+{
+ "subnet": {
+ "name": "my_new_subnet",
+ "dns_nameservers": ["foo"],
+ "host_routes": [{"destination":"","nexthop": "bar"}]
+ }
+}
+ `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusCreated)
+
+ fmt.Fprintf(w, `
+{
+ "subnet": {
+ "name": "my_new_subnet",
+ "enable_dhcp": true,
+ "network_id": "db193ab3-96e3-4cb3-8fc5-05f4296d0324",
+ "tenant_id": "26a7980765d0414dbc1fc1f88cdb7e6e",
+ "dns_nameservers": [],
+ "allocation_pools": [
+ {
+ "start": "10.0.0.2",
+ "end": "10.0.0.254"
+ }
+ ],
+ "host_routes": [],
+ "ip_version": 4,
+ "gateway_ip": "10.0.0.1",
+ "cidr": "10.0.0.0/24",
+ "id": "08eae331-0402-425a-923c-34f7cfe39c1b"
+ }
+}
+ `)
+ })
+
+ opts := os.UpdateOpts{
+ Name: "my_new_subnet",
+ DNSNameservers: []string{"foo"},
+ HostRoutes: []os.HostRoute{
+ os.HostRoute{NextHop: "bar"},
+ },
+ }
+ s, err := Update(fake.ServiceClient(), "08eae331-0402-425a-923c-34f7cfe39c1b", opts).Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, s.Name, "my_new_subnet")
+ th.AssertEquals(t, s.ID, "08eae331-0402-425a-923c-34f7cfe39c1b")
+}
+
+func TestDelete(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/subnets/08eae331-0402-425a-923c-34f7cfe39c1b", 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)
+ })
+
+ res := Delete(fake.ServiceClient(), "08eae331-0402-425a-923c-34f7cfe39c1b")
+ th.AssertNoErr(t, res.Err)
+}
diff --git a/rackspace/objectstorage/v1/objects/delegate_test.go b/rackspace/objectstorage/v1/objects/delegate_test.go
index 08831ec..8ab8029 100644
--- a/rackspace/objectstorage/v1/objects/delegate_test.go
+++ b/rackspace/objectstorage/v1/objects/delegate_test.go
@@ -66,14 +66,24 @@
func TestCreateObject(t *testing.T) {
th.SetupHTTP()
defer th.TeardownHTTP()
- os.HandleCreateObjectSuccessfully(t)
+ os.HandleCreateTextObjectSuccessfully(t)
content := bytes.NewBufferString("Did gyre and gimble in the wabe")
- options := &os.CreateOpts{ContentType: "application/json"}
+ options := &os.CreateOpts{ContentType: "text/plain"}
res := Create(fake.ServiceClient(), "testContainer", "testObject", content, options)
th.AssertNoErr(t, res.Err)
}
+func TestCreateObjectWithoutContentType(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ os.HandleCreateTypelessObjectSuccessfully(t)
+
+ content := bytes.NewBufferString("The sky was the color of television, tuned to a dead channel.")
+ res := Create(fake.ServiceClient(), "testContainer", "testObject", content, &os.CreateOpts{})
+ th.AssertNoErr(t, res.Err)
+}
+
func TestCopyObject(t *testing.T) {
th.SetupHTTP()
defer th.TeardownHTTP()
diff --git a/results.go b/results.go
index f480bc7..3fd5029 100644
--- a/results.go
+++ b/results.go
@@ -5,21 +5,37 @@
"net/http"
)
-// Result acts as a base struct that other results can embed.
+/*
+Result is an internal type to be used by individual resource packages, but its
+methods will be available on a wide variety of user-facing embedding types.
+
+It acts as a base struct that other Result types, returned from request
+functions, can embed for convenience. All Results capture basic information
+from the HTTP transaction that was performed, including the response body,
+HTTP headers, and any errors that happened.
+
+Generally, each Result type will have an Extract method that can be used to
+further interpret the result's payload in a specific context. Extensions or
+providers can then provide additional extraction functions to pull out
+provider- or extension-specific information as well.
+*/
type Result struct {
- // Body is the payload of the HTTP response from the server. In most cases, this will be the
- // deserialized JSON structure.
+ // Body is the payload of the HTTP response from the server. In most cases,
+ // this will be the deserialized JSON structure.
Body interface{}
// Header contains the HTTP header structure from the original response.
Header http.Header
- // Err is an error that occurred during the operation. It's deferred until extraction to make
- // it easier to chain operations.
+ // Err is an error that occurred during the operation. It's deferred until
+ // extraction to make it easier to chain the Extract call.
Err error
}
-// PrettyPrintJSON creates a string containing the full response body as pretty-printed JSON.
+// PrettyPrintJSON creates a string containing the full response body as
+// pretty-printed JSON. It's useful for capturing test fixtures and for
+// debugging extraction bugs. If you include its output in an issue related to
+// a buggy extraction function, we will all love you forever.
func (r Result) PrettyPrintJSON() string {
pretty, err := json.MarshalIndent(r.Body, "", " ")
if err != nil {
@@ -28,44 +44,67 @@
return string(pretty)
}
-// ErrResult represents results that only contain a potential error and
-// nothing else. Usually if the operation executed successfully, the Err field
-// will be nil; otherwise it will be stocked with a relevant error.
+// ErrResult is an internal type to be used by individual resource packages, but
+// its methods will be available on a wide variety of user-facing embedding
+// types.
+//
+// It represents results that only contain a potential error and
+// nothing else. Usually, if the operation executed successfully, the Err field
+// will be nil; otherwise it will be stocked with a relevant error. Use the
+// ExtractErr method
+// to cleanly pull it out.
type ErrResult struct {
Result
}
-// ExtractErr is a function that extracts error information from a result.
+// ExtractErr is a function that extracts error information, or nil, from a result.
func (r ErrResult) ExtractErr() error {
return r.Err
}
-// HeaderResult represents a result that only contains an `error` (possibly nil)
-// and an http.Header. This is used, for example, by the `objectstorage` packages
-// in `openstack`, because most of the operations don't return response bodies.
+/*
+HeaderResult is an internal type to be used by individual resource packages, but
+its methods will be available on a wide variety of user-facing embedding types.
+
+It represents a result that only contains an error (possibly nil) and an
+http.Header. This is used, for example, by the objectstorage packages in
+openstack, because most of the operations don't return response bodies, but do
+have relevant information in headers.
+*/
type HeaderResult struct {
Result
}
// ExtractHeader will return the http.Header and error from the HeaderResult.
-// Usage: header, err := objects.Create(client, "my_container", objects.CreateOpts{}).ExtractHeader()
+//
+// header, err := objects.Create(client, "my_container", objects.CreateOpts{}).ExtractHeader()
func (hr HeaderResult) ExtractHeader() (http.Header, error) {
return hr.Header, hr.Err
}
-// RFC3339Milli describes a time format used by API responses.
+// RFC3339Milli describes a common time format used by some API responses.
const RFC3339Milli = "2006-01-02T15:04:05.999999Z"
-// Link represents a structure that enables paginated collections how to
-// traverse backward or forward. The "Rel" field is usually either "next".
+/*
+Link is an internal type to be used in packages of collection resources that are
+paginated in a certain way.
+
+It's a response substructure common to many paginated collection results that is
+used to point to related pages. Usually, the one we care about is the one with
+Rel field set to "next".
+*/
type Link struct {
Href string `mapstructure:"href"`
Rel string `mapstructure:"rel"`
}
-// ExtractNextURL attempts to extract the next URL from a JSON structure. It
-// follows the common convention of nesting back and next URLs in a "links"
-// JSON array.
+/*
+ExtractNextURL is an internal function useful for packages of collection
+resources that are paginated in a certain way.
+
+It attempts attempts to extract the "next" URL from slice of Link structs, or
+"" if no such URL is present.
+*/
func ExtractNextURL(links []Link) (string, error) {
var url string
diff --git a/util.go b/util.go
index 101fd39..fbd9fe9 100644
--- a/util.go
+++ b/util.go
@@ -7,7 +7,9 @@
)
// WaitFor polls a predicate function, once per second, up to a timeout limit.
-// It usually does this to wait for the resource to transition to a certain state.
+// It usually does this to wait for a resource to transition to a certain state.
+// Resource packages will wrap this in a more convenient function that's
+// specific to a certain resource, but it can also be useful on its own.
func WaitFor(timeout int, predicate func() (bool, error)) error {
start := time.Now().Second()
for {
@@ -30,7 +32,10 @@
}
}
-// NormalizeURL ensures that each endpoint URL has a closing `/`, as expected by ServiceClient.
+// NormalizeURL is an internal function to be used by provider clients.
+//
+// It ensures that each endpoint URL has a closing `/`, as expected by
+// ServiceClient's methods.
func NormalizeURL(url string) string {
if !strings.HasSuffix(url, "/") {
return url + "/"