Basic Rackspace cloud support

This enables all but Load Balancer support for the Rackspace public
cloud platform.
This commit is contained in:
Thom May
2015-02-17 10:48:48 +00:00
parent 6f84bdaba8
commit 8357e1521a
234 changed files with 17464 additions and 435 deletions

4
Godeps/Godeps.json generated
View File

@@ -203,8 +203,8 @@
}, },
{ {
"ImportPath": "github.com/rackspace/gophercloud", "ImportPath": "github.com/rackspace/gophercloud",
"Comment": "v1.0.0", "Comment": "v1.0.0-336-g2a6e319",
"Rev": "da56de6a59e53fdd61be1b5d9b87df34c47ac420" "Rev": "2a6e3190447abe5d000f951595ead1cf98df72d8"
}, },
{ {
"ImportPath": "github.com/skynetservices/skydns/msg", "ImportPath": "github.com/skynetservices/skydns/msg",

View File

@@ -4,6 +4,8 @@ install:
go: go:
- 1.1 - 1.1
- 1.2 - 1.2
- 1.3
- 1.4
- tip - tip
script: script/cibuild script: script/cibuild
after_success: after_success:
@@ -12,3 +14,4 @@ after_success:
- go get github.com/mattn/goveralls - go get github.com/mattn/goveralls
- export PATH=$PATH:$HOME/gopath/bin/ - export PATH=$PATH:$HOME/gopath/bin/
- goveralls 2k7PTU3xa474Hymwgdj6XjqenNfGTNkO8 - goveralls 2k7PTU3xa474Hymwgdj6XjqenNfGTNkO8
sudo: false

View File

@@ -11,7 +11,7 @@ As a contributor you will need to setup your workspace in a slightly different
way than just downloading it. Here are the basic installation instructions: way than just downloading it. Here are the basic installation instructions:
1. Configure your `$GOPATH` and run `go get` as described in the main 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: 2. Move into the directory that houses your local repository:

View File

@@ -238,7 +238,7 @@ err := pager.EachPage(func(page pagination.Page) (bool, error) {
imageList, err := images.ExtractImages(page) imageList, err := images.ExtractImages(page)
for _, i := range imageList { for _, i := range imageList {
// "i" will be a images.Image // "i" will be an images.Image
} }
}) })
``` ```
@@ -284,7 +284,7 @@ err := pager.EachPage(func(page pagination.Page) (bool, error) {
imageList, err := images.ExtractImages(page) imageList, err := images.ExtractImages(page)
for _, i := range imageList { for _, i := range imageList {
// "i" will be a images.Image // "i" will be an images.Image
} }
}) })
``` ```

View File

@@ -13,9 +13,9 @@ import (
func TestSnapshots(t *testing.T) { func TestSnapshots(t *testing.T) {
client, err := newClient() client, err := newClient(t)
th.AssertNoErr(t, err) th.AssertNoErr(t, err)
v, err := volumes.Create(client, &volumes.CreateOpts{ v, err := volumes.Create(client, &volumes.CreateOpts{
Name: "gophercloud-test-volume", Name: "gophercloud-test-volume",
Size: 1, Size: 1,

View File

@@ -13,7 +13,7 @@ import (
th "github.com/rackspace/gophercloud/testhelper" th "github.com/rackspace/gophercloud/testhelper"
) )
func newClient() (*gophercloud.ServiceClient, error) { func newClient(t *testing.T) (*gophercloud.ServiceClient, error) {
ao, err := openstack.AuthOptionsFromEnv() ao, err := openstack.AuthOptionsFromEnv()
th.AssertNoErr(t, err) th.AssertNoErr(t, err)
@@ -26,7 +26,7 @@ func newClient() (*gophercloud.ServiceClient, error) {
} }
func TestVolumes(t *testing.T) { func TestVolumes(t *testing.T) {
client, err := newClient() client, err := newClient(t)
th.AssertNoErr(t, err) th.AssertNoErr(t, err)
cv, err := volumes.Create(client, &volumes.CreateOpts{ cv, err := volumes.Create(client, &volumes.CreateOpts{

View File

@@ -12,7 +12,7 @@ import (
) )
func TestVolumeTypes(t *testing.T) { func TestVolumeTypes(t *testing.T) {
client, err := newClient() client, err := newClient(t)
th.AssertNoErr(t, err) th.AssertNoErr(t, err)
vt, err := volumetypes.Create(client, &volumetypes.CreateOpts{ vt, err := volumetypes.Create(client, &volumetypes.CreateOpts{

View File

@@ -5,10 +5,10 @@ package v2
import ( import (
"testing" "testing"
"github.com/rackspace/gophercloud/acceptance/tools"
"github.com/rackspace/gophercloud/openstack/compute/v2/extensions/bootfromvolume" "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/bootfromvolume"
"github.com/rackspace/gophercloud/openstack/compute/v2/servers" "github.com/rackspace/gophercloud/openstack/compute/v2/servers"
th "github.com/rackspace/gophercloud/testhelper" th "github.com/rackspace/gophercloud/testhelper"
"github.com/smashwilson/gophercloud/acceptance/tools"
) )
func TestBootFromVolume(t *testing.T) { func TestBootFromVolume(t *testing.T) {
@@ -37,14 +37,19 @@ func TestBootFromVolume(t *testing.T) {
serverCreateOpts := servers.CreateOpts{ serverCreateOpts := servers.CreateOpts{
Name: name, Name: name,
FlavorRef: "3", FlavorRef: choices.FlavorID,
ImageRef: choices.ImageID,
} }
server, err := bootfromvolume.Create(client, bootfromvolume.CreateOptsExt{ server, err := bootfromvolume.Create(client, bootfromvolume.CreateOptsExt{
serverCreateOpts, serverCreateOpts,
bd, bd,
}).Extract() }).Extract()
th.AssertNoErr(t, err) th.AssertNoErr(t, err)
if err = waitForStatus(client, server, "ACTIVE"); err != nil {
t.Fatal(err)
}
t.Logf("Created server: %+v\n", server) t.Logf("Created server: %+v\n", server)
//defer deleteServer(t, client, server) defer servers.Delete(client, server.ID)
t.Logf("Deleting server [%s]...", name) t.Logf("Deleting server [%s]...", name)
} }

View File

@@ -1,4 +1,4 @@
// +build acceptance // +build acceptance common
package v2 package v2

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -12,6 +12,7 @@ import (
"github.com/rackspace/gophercloud/openstack/compute/v2/servers" "github.com/rackspace/gophercloud/openstack/compute/v2/servers"
"github.com/rackspace/gophercloud/openstack/networking/v2/networks" "github.com/rackspace/gophercloud/openstack/networking/v2/networks"
"github.com/rackspace/gophercloud/pagination" "github.com/rackspace/gophercloud/pagination"
th "github.com/rackspace/gophercloud/testhelper"
) )
func TestListServers(t *testing.T) { func TestListServers(t *testing.T) {
@@ -94,6 +95,8 @@ func createServer(t *testing.T, client *gophercloud.ServiceClient, choices *Comp
name := tools.RandomString("ACPTTEST", 16) name := tools.RandomString("ACPTTEST", 16)
t.Logf("Attempting to create server: %s\n", name) t.Logf("Attempting to create server: %s\n", name)
pwd := tools.MakeNewPassword("")
server, err := servers.Create(client, servers.CreateOpts{ server, err := servers.Create(client, servers.CreateOpts{
Name: name, Name: name,
FlavorRef: choices.FlavorID, FlavorRef: choices.FlavorID,
@@ -101,11 +104,14 @@ func createServer(t *testing.T, client *gophercloud.ServiceClient, choices *Comp
Networks: []servers.Network{ Networks: []servers.Network{
servers.Network{UUID: network.ID}, servers.Network{UUID: network.ID},
}, },
AdminPass: pwd,
}).Extract() }).Extract()
if err != nil { if err != nil {
t.Fatalf("Unable to create server: %v", err) t.Fatalf("Unable to create server: %v", err)
} }
th.AssertEquals(t, pwd, server.AdminPass)
return server, err return server, err
} }
@@ -391,3 +397,54 @@ func TestActionResizeRevert(t *testing.T) {
t.Fatal(err) 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)
}

View File

@@ -1,4 +1,4 @@
// +build acceptance // +build acceptance identity
package v2 package v2

View File

@@ -1,4 +1,4 @@
// +build acceptance // +build acceptance identity
package v2 package v2

View File

@@ -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)
}

View File

@@ -1,4 +1,4 @@
// +build acceptance // +build acceptance identity
package v2 package v2

View File

@@ -1,4 +1,4 @@
// +build acceptance // +build acceptance identity
package v2 package v2

View File

@@ -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)
}

View File

@@ -82,7 +82,7 @@ func listPorts(t *testing.T) {
th.AssertNoErr(t, err) th.AssertNoErr(t, err)
for _, p := range portList { 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) p.ID, p.Name, p.Status, p.MACAddress, p.FixedIPs, p.SecurityGroups)
} }

View File

@@ -17,7 +17,10 @@ func TestAccounts(t *testing.T) {
// Update an account's metadata. // Update an account's metadata.
updateres := accounts.Update(client, accounts.UpdateOpts{Metadata: metadata}) updateres := accounts.Update(client, accounts.UpdateOpts{Metadata: metadata})
th.AssertNoErr(t, updateres.Err) t.Logf("Update Account Response: %+v\n", updateres)
updateHeaders, err := updateres.Extract()
th.AssertNoErr(t, err)
t.Logf("Update Account Response Headers: %+v\n", updateHeaders)
// Defer the deletion of the metadata set above. // Defer the deletion of the metadata set above.
defer func() { defer func() {
@@ -29,11 +32,14 @@ func TestAccounts(t *testing.T) {
th.AssertNoErr(t, updateres.Err) th.AssertNoErr(t, updateres.Err)
}() }()
// Retrieve account metadata.
getres := accounts.Get(client, nil)
th.AssertNoErr(t, getres.Err)
// Extract the custom metadata from the 'Get' response. // Extract the custom metadata from the 'Get' response.
am, err := getres.ExtractMetadata() res := accounts.Get(client, nil)
h, err := res.Extract()
th.AssertNoErr(t, err)
t.Logf("Get Account Response Headers: %+v\n", h)
am, err := res.ExtractMetadata()
th.AssertNoErr(t, err) th.AssertNoErr(t, err)
for k := range metadata { for k := range metadata {
if am[k] != metadata[strings.Title(k)] { if am[k] != metadata[strings.Title(k)] {

View File

@@ -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")
}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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")
}

View File

@@ -5,11 +5,11 @@ package v2
import ( import (
"testing" "testing"
"github.com/rackspace/gophercloud/acceptance/tools"
osBFV "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/bootfromvolume" 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/bootfromvolume"
"github.com/rackspace/gophercloud/rackspace/compute/v2/servers" "github.com/rackspace/gophercloud/rackspace/compute/v2/servers"
th "github.com/rackspace/gophercloud/testhelper" th "github.com/rackspace/gophercloud/testhelper"
"github.com/smashwilson/gophercloud/acceptance/tools"
) )
func TestBootFromVolume(t *testing.T) { func TestBootFromVolume(t *testing.T) {
@@ -41,6 +41,9 @@ func TestBootFromVolume(t *testing.T) {
}).Extract() }).Extract()
th.AssertNoErr(t, err) th.AssertNoErr(t, err)
t.Logf("Created server: %+v\n", server) t.Logf("Created server: %+v\n", server)
//defer deleteServer(t, client, server) defer deleteServer(t, client, server)
t.Logf("Deleting server [%s]...", name)
getServer(t, client, server)
listServers(t, client)
} }

View File

@@ -39,11 +39,14 @@ func createServer(t *testing.T, client *gophercloud.ServiceClient, keyName strin
name := tools.RandomString("Gophercloud-", 8) name := tools.RandomString("Gophercloud-", 8)
pwd := tools.MakeNewPassword("")
opts := &servers.CreateOpts{ opts := &servers.CreateOpts{
Name: name, Name: name,
ImageRef: options.imageID, ImageRef: options.imageID,
FlavorRef: options.flavorID, FlavorRef: options.flavorID,
DiskConfig: diskconfig.Manual, DiskConfig: diskconfig.Manual,
AdminPass: pwd,
} }
if keyName != "" { if keyName != "" {
@@ -59,6 +62,8 @@ func createServer(t *testing.T, client *gophercloud.ServiceClient, keyName strin
th.AssertNoErr(t, err) th.AssertNoErr(t, err)
t.Logf("Server created successfully.") t.Logf("Server created successfully.")
th.CheckEquals(t, pwd, s.AdminPass)
return s return s
} }
@@ -95,6 +100,18 @@ func getServer(t *testing.T, client *gophercloud.ServiceClient, server *os.Serve
logServer(t, details, -1) logServer(t, details, -1)
} }
func updateServer(t *testing.T, client *gophercloud.ServiceClient, server *os.Server) {
t.Logf("> servers.Get")
opts := os.UpdateOpts{
Name: "updated-server",
}
updatedServer, err := servers.Update(client, server.ID, opts).Extract()
th.AssertNoErr(t, err)
th.AssertEquals(t, "updated-server", updatedServer.Name)
logServer(t, updatedServer, -1)
}
func listServers(t *testing.T, client *gophercloud.ServiceClient) { func listServers(t *testing.T, client *gophercloud.ServiceClient) {
t.Logf("> servers.List") t.Logf("> servers.List")
@@ -120,7 +137,7 @@ func changeAdminPassword(t *testing.T, client *gophercloud.ServiceClient, server
original := server.AdminPass original := server.AdminPass
t.Logf("Changing server password.") 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) th.AssertNoErr(t, err)
err = servers.WaitForStatus(client, server.ID, "ACTIVE", 300) err = servers.WaitForStatus(client, server.ID, "ACTIVE", 300)
@@ -131,7 +148,7 @@ func changeAdminPassword(t *testing.T, client *gophercloud.ServiceClient, server
func rebootServer(t *testing.T, client *gophercloud.ServiceClient, server *os.Server) { func rebootServer(t *testing.T, client *gophercloud.ServiceClient, server *os.Server) {
t.Logf("> servers.Reboot") 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) th.AssertNoErr(t, err)
err = servers.WaitForStatus(client, server.ID, "ACTIVE", 300) err = servers.WaitForStatus(client, server.ID, "ACTIVE", 300)
@@ -192,6 +209,7 @@ func TestServerOperations(t *testing.T) {
defer deleteServer(t, client, server) defer deleteServer(t, client, server)
getServer(t, client, server) getServer(t, client, server)
updateServer(t, client, server)
listServers(t, client) listServers(t, client)
changeAdminPassword(t, client, server) changeAdminPassword(t, client, server)
rebootServer(t, client, server) rebootServer(t, client, server)

View File

@@ -0,0 +1 @@
package v2

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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, ", ")
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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))
}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -13,11 +13,11 @@ func TestAccounts(t *testing.T) {
c, err := createClient(t, false) c, err := createClient(t, false)
th.AssertNoErr(t, err) th.AssertNoErr(t, err)
updateres := raxAccounts.Update(c, raxAccounts.UpdateOpts{Metadata: map[string]string{"white": "mountains"}}) updateHeaders, err := raxAccounts.Update(c, raxAccounts.UpdateOpts{Metadata: map[string]string{"white": "mountains"}}).Extract()
th.AssertNoErr(t, updateres.Err) th.AssertNoErr(t, err)
t.Logf("Headers from Update Account request: %+v\n", updateres.Header) t.Logf("Update Account Response Headers: %+v\n", updateHeaders)
defer func() { defer func() {
updateres = raxAccounts.Update(c, raxAccounts.UpdateOpts{Metadata: map[string]string{"white": ""}}) updateres := raxAccounts.Update(c, raxAccounts.UpdateOpts{Metadata: map[string]string{"white": ""}})
th.AssertNoErr(t, updateres.Err) th.AssertNoErr(t, updateres.Err)
metadata, err := raxAccounts.Get(c).ExtractMetadata() metadata, err := raxAccounts.Get(c).ExtractMetadata()
th.AssertNoErr(t, err) th.AssertNoErr(t, err)
@@ -25,8 +25,13 @@ func TestAccounts(t *testing.T) {
th.CheckEquals(t, metadata["White"], "") th.CheckEquals(t, metadata["White"], "")
}() }()
metadata, err := raxAccounts.Get(c).ExtractMetadata() getResp := raxAccounts.Get(c)
th.AssertNoErr(t, err) th.AssertNoErr(t, getResp.Err)
getHeaders, _ := getResp.Extract()
t.Logf("Get Account Response Headers: %+v\n", getHeaders)
metadata, _ := getResp.ExtractMetadata()
t.Logf("Metadata from Get Account request (after update): %+v\n", metadata) t.Logf("Metadata from Get Account request (after update): %+v\n", metadata)
th.CheckEquals(t, metadata["White"], "mountains") th.CheckEquals(t, metadata["White"], "mountains")

View File

@@ -26,10 +26,11 @@ func TestCDNContainers(t *testing.T) {
raxCDNClient, err := createClient(t, true) raxCDNClient, err := createClient(t, true)
th.AssertNoErr(t, err) th.AssertNoErr(t, err)
enableRes := raxCDNContainers.Enable(raxCDNClient, "gophercloud-test", raxCDNContainers.EnableOpts{CDNEnabled: true, TTL: 900})
r := raxCDNContainers.Enable(raxCDNClient, "gophercloud-test", raxCDNContainers.EnableOpts{CDNEnabled: true, TTL: 900}) t.Logf("Header map from Enable CDN Container request: %+v\n", enableRes.Header)
th.AssertNoErr(t, r.Err) enableHeader, err := enableRes.Extract()
t.Logf("Headers from Enable CDN Container request: %+v\n", r.Header) th.AssertNoErr(t, err)
t.Logf("Headers from Enable CDN Container request: %+v\n", enableHeader)
t.Logf("Container Names available to the currently issued token:") t.Logf("Container Names available to the currently issued token:")
count := 0 count := 0
@@ -51,11 +52,15 @@ func TestCDNContainers(t *testing.T) {
t.Errorf("No CDN containers listed for your current token.") t.Errorf("No CDN containers listed for your current token.")
} }
updateres := raxCDNContainers.Update(raxCDNClient, "gophercloud-test", raxCDNContainers.UpdateOpts{CDNEnabled: false}) updateOpts := raxCDNContainers.UpdateOpts{XCDNEnabled: raxCDNContainers.Disabled, XLogRetention: raxCDNContainers.Enabled}
th.AssertNoErr(t, updateres.Err) updateHeader, err := raxCDNContainers.Update(raxCDNClient, "gophercloud-test", updateOpts).Extract()
t.Logf("Headers from Update CDN Container request: %+v\n", updateres.Header)
metadata, err := raxCDNContainers.Get(raxCDNClient, "gophercloud-test").ExtractMetadata()
th.AssertNoErr(t, err) th.AssertNoErr(t, err)
t.Logf("Headers from Get CDN Container request (after update): %+v\n", metadata) t.Logf("Headers from Update CDN Container request: %+v\n", updateHeader)
getRes := raxCDNContainers.Get(raxCDNClient, "gophercloud-test")
getHeader, err := getRes.Extract()
th.AssertNoErr(t, err)
t.Logf("Headers from Get CDN Container request (after update): %+v\n", getHeader)
metadata, err := getRes.ExtractMetadata()
t.Logf("Metadata from Get CDN Container request (after update): %+v\n", metadata)
} }

View File

@@ -57,29 +57,34 @@ func TestContainers(t *testing.T) {
t.Errorf("No containers listed for your current token.") t.Errorf("No containers listed for your current token.")
} }
createres := raxContainers.Create(c, "gophercloud-test", nil) createHeader, err := raxContainers.Create(c, "gophercloud-test", nil).Extract()
th.AssertNoErr(t, createres.Err) th.AssertNoErr(t, err)
t.Logf("Headers from Create Container request: %+v\n", createHeader)
defer func() { defer func() {
res := raxContainers.Delete(c, "gophercloud-test") deleteres := raxContainers.Delete(c, "gophercloud-test")
th.AssertNoErr(t, res.Err) deleteHeader, err := deleteres.Extract()
th.AssertNoErr(t, err)
t.Logf("Headers from Delete Container request: %+v\n", deleteres.Header)
t.Logf("Headers from Delete Container request: %+v\n", deleteHeader)
}() }()
updateres := raxContainers.Update(c, "gophercloud-test", raxContainers.UpdateOpts{Metadata: map[string]string{"white": "mountains"}}) updateHeader, err := raxContainers.Update(c, "gophercloud-test", raxContainers.UpdateOpts{Metadata: map[string]string{"white": "mountains"}}).Extract()
th.AssertNoErr(t, updateres.Err) th.AssertNoErr(t, err)
t.Logf("Headers from Update Account request: %+v\n", updateres.Header) t.Logf("Headers from Update Container request: %+v\n", updateHeader)
defer func() { defer func() {
res := raxContainers.Update(c, "gophercloud-test", raxContainers.UpdateOpts{Metadata: map[string]string{"white": ""}}) res := raxContainers.Update(c, "gophercloud-test", raxContainers.UpdateOpts{Metadata: map[string]string{"white": ""}})
th.AssertNoErr(t, res.Err) th.AssertNoErr(t, res.Err)
metadata, err := raxContainers.Get(c, "gophercloud-test").ExtractMetadata() metadata, err := raxContainers.Get(c, "gophercloud-test").ExtractMetadata()
th.AssertNoErr(t, err) th.AssertNoErr(t, err)
t.Logf("Metadata from Get Account request (after update reverted): %+v\n", metadata) t.Logf("Metadata from Get Container request (after update reverted): %+v\n", metadata)
th.CheckEquals(t, metadata["White"], "") th.CheckEquals(t, metadata["White"], "")
}() }()
getres := raxContainers.Get(c, "gophercloud-test") getres := raxContainers.Get(c, "gophercloud-test")
t.Logf("Headers from Get Account request (after update): %+v\n", getres.Header) getHeader, err := getres.Extract()
metadata, err := getres.ExtractMetadata()
th.AssertNoErr(t, err) th.AssertNoErr(t, err)
t.Logf("Metadata from Get Account request (after update): %+v\n", metadata) t.Logf("Headers from Get Container request (after update): %+v\n", getHeader)
metadata, err := getres.ExtractMetadata()
t.Logf("Metadata from Get Container request (after update): %+v\n", metadata)
th.CheckEquals(t, metadata["White"], "mountains") th.CheckEquals(t, metadata["White"], "mountains")
} }

View File

@@ -21,6 +21,7 @@ func TestObjects(t *testing.T) {
th.AssertNoErr(t, res.Err) th.AssertNoErr(t, res.Err)
defer func() { defer func() {
t.Logf("Deleting container...")
res := raxContainers.Delete(c, "gophercloud-test") res := raxContainers.Delete(c, "gophercloud-test")
th.AssertNoErr(t, res.Err) th.AssertNoErr(t, res.Err)
}() }()
@@ -29,7 +30,9 @@ func TestObjects(t *testing.T) {
options := &osObjects.CreateOpts{ContentType: "text/plain"} options := &osObjects.CreateOpts{ContentType: "text/plain"}
createres := raxObjects.Create(c, "gophercloud-test", "o1", content, options) createres := raxObjects.Create(c, "gophercloud-test", "o1", content, options)
th.AssertNoErr(t, createres.Err) th.AssertNoErr(t, createres.Err)
defer func() { defer func() {
t.Logf("Deleting object o1...")
res := raxObjects.Delete(c, "gophercloud-test", "o1", nil) res := raxObjects.Delete(c, "gophercloud-test", "o1", nil)
th.AssertNoErr(t, res.Err) th.AssertNoErr(t, res.Err)
}() }()
@@ -80,6 +83,7 @@ func TestObjects(t *testing.T) {
copyres := raxObjects.Copy(c, "gophercloud-test", "o1", &raxObjects.CopyOpts{Destination: "gophercloud-test/o2"}) copyres := raxObjects.Copy(c, "gophercloud-test", "o1", &raxObjects.CopyOpts{Destination: "gophercloud-test/o2"})
th.AssertNoErr(t, copyres.Err) th.AssertNoErr(t, copyres.Err)
defer func() { defer func() {
t.Logf("Deleting object o2...")
res := raxObjects.Delete(c, "gophercloud-test", "o2", nil) res := raxObjects.Delete(c, "gophercloud-test", "o2", nil)
th.AssertNoErr(t, res.Err) th.AssertNoErr(t, res.Err)
}() }()
@@ -99,7 +103,7 @@ func TestObjects(t *testing.T) {
metadata, err := raxObjects.Get(c, "gophercloud-test", "o2", nil).ExtractMetadata() metadata, err := raxObjects.Get(c, "gophercloud-test", "o2", nil).ExtractMetadata()
th.AssertNoErr(t, err) th.AssertNoErr(t, err)
t.Logf("Metadata from Get Account request (after update reverted): %+v\n", metadata) t.Logf("Metadata from Get Account request (after update reverted): %+v\n", metadata)
th.CheckEquals(t, metadata["White"], "") th.CheckEquals(t, "", metadata["White"])
}() }()
getres := raxObjects.Get(c, "gophercloud-test", "o2", nil) getres := raxObjects.Get(c, "gophercloud-test", "o2", nil)
@@ -108,5 +112,13 @@ func TestObjects(t *testing.T) {
metadata, err := getres.ExtractMetadata() metadata, err := getres.ExtractMetadata()
th.AssertNoErr(t, err) th.AssertNoErr(t, err)
t.Logf("Metadata from Get Account request (after update): %+v\n", metadata) t.Logf("Metadata from Get Account request (after update): %+v\n", metadata)
th.CheckEquals(t, metadata["White"], "mountains") th.CheckEquals(t, "mountains", metadata["White"])
createTempURLOpts := osObjects.CreateTempURLOpts{
Method: osObjects.GET,
TTL: 600,
}
tempURL, err := raxObjects.CreateTempURL(c, "gophercloud-test", "o1", createTempURLOpts)
th.AssertNoErr(t, err)
t.Logf("TempURL for object (%s): %s", "o1", tempURL)
} }

View File

@@ -1,10 +1,11 @@
// +build acceptance // +build acceptance common
package tools package tools
import ( import (
"crypto/rand" "crypto/rand"
"errors" "errors"
mrand "math/rand"
"os" "os"
"time" "time"
@@ -72,6 +73,12 @@ func RandomString(prefix string, n int) string {
return prefix + string(bytes) 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 // Elide returns the first bit of its input string with a suffix of "..." if it's longer than
// a comfortable 40 characters. // a comfortable 40 characters.
func Elide(value string) string { func Elide(value string) string {

View File

@@ -1,20 +1,28 @@
package gophercloud package gophercloud
// AuthOptions allows anyone calling Authenticate to supply the required access /*
// credentials. Its fields are the union of those recognized by each identity AuthOptions stores information needed to authenticate to an OpenStack cluster.
// implementation and provider. 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 { type AuthOptions struct {
// IdentityEndpoint specifies the HTTP endpoint that is required to work with // IdentityEndpoint specifies the HTTP endpoint that is required to work with
// the Identity API of the appropriate version. Required by the identity // the Identity API of the appropriate version. While it's ultimately needed by
// services, but often populated by a provider Client. // all of the identity services, it will often be populated by a provider-level
// function.
IdentityEndpoint string IdentityEndpoint string
// Username is required if using Identity V2 API. Consult with your provider's // 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 // 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 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 // APIs. Consult with your provider's control panel to discover your account's
// preferred method of authentication. // preferred method of authentication.
Password, APIKey string Password, APIKey string
@@ -25,7 +33,7 @@ type AuthOptions struct {
// The TenantID and TenantName fields are optional for the Identity V2 API. // 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 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. // how these fields influence authentication.
TenantID, TenantName string TenantID, TenantName string
@@ -34,5 +42,7 @@ type AuthOptions struct {
// re-authenticate automatically if/when your token expires. If you set it to // 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 // false, it will not cache these settings, but re-authentication will not be
// possible. This setting defaults to false. // possible. This setting defaults to false.
//
// This setting is speculative and is currently not respected!
AllowReauth bool AllowReauth bool
} }

View File

@@ -2,10 +2,9 @@ package gophercloud
import "time" import "time"
// AuthResults encapsulates the raw results from an authentication request. As OpenStack allows // AuthResults [deprecated] is a leftover type from the v0.x days. It was
// extensions to influence the structure returned in ways that Gophercloud cannot predict at // intended to describe common functionality among identity service results, but
// compile-time, you should use type-safe accessors to work with the data represented by this type, // is not actually used anywhere.
// such as ServiceCatalog and TokenID.
type AuthResults interface { type AuthResults interface {
// TokenID returns the token's ID value from the authentication response. // TokenID returns the token's ID value from the authentication response.
TokenID() (string, error) TokenID() (string, error)

View File

@@ -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

View File

@@ -3,58 +3,85 @@ package gophercloud
import "errors" import "errors"
var ( 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.") 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.") ErrEndpointNotFound = errors.New("No suitable endpoint could be found in the service catalog.")
) )
// Availability indicates whether a specific service endpoint is accessible. // Availability indicates to whom a specific service endpoint is accessible:
// Identity v2 lists these as different kinds of URLs ("adminURL", // the internet at large, internal networks only, or only to administrators.
// "internalURL", and "publicURL"), while v3 lists them as "Interfaces". // 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 type Availability string
const ( const (
// AvailabilityAdmin makes an endpoint only available to administrators. // AvailabilityAdmin indicates that an endpoint is only available to
// administrators.
AvailabilityAdmin Availability = "admin" 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" 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" 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 EndpointOpts struct {
// Type is the service type for the client (e.g., "compute", "object-store"). // Type [required] is the service type for the client (e.g., "compute",
// Required. // "object-store"). Generally, this will be supplied by the service client
// function, but a user-given value will be honored if provided.
Type string Type string
// Name is the service name for the client (e.g., "nova") as it appears in // Name [optional] is the service name for the client (e.g., "nova") as it
// the service catalog. Services can have the same Type but a different Name, // appears in the service catalog. Services can have the same Type but a
// which is why both Type and Name are sometimes needed. Optional. // different Name, which is why both Type and Name are sometimes needed.
Name string Name string
// Region is the geographic region in which the service resides. Required only // Region [required] is the geographic region in which the endpoint resides,
// for services that span multiple regions. // generally specifying which datacenter should house your resources.
// Required only for services that span multiple regions.
Region string Region string
// Availability is the visibility of the endpoint to be returned. Valid types // Availability [optional] is the visibility of the endpoint to be returned.
// are: AvailabilityPublic, AvailabilityInternal, or AvailabilityAdmin. // Valid types include the constants AvailabilityPublic, AvailabilityInternal,
// Availability is not required, and defaults to AvailabilityPublic. // or AvailabilityAdmin from this package.
// Not all providers or services offer all Availability options. //
// Availability is not required, and defaults to AvailabilityPublic. Not all
// providers or services offer all Availability options.
Availability Availability 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 EndpointLocator is an internal function to be used by provider implementations.
// during ProviderClient authentication and used to discover related ServiceClients.
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) type EndpointLocator func(EndpointOpts) (string, error)
// ApplyDefaults sets EndpointOpts fields if not already set. Currently, // ApplyDefaults is an internal method to be used by provider implementations.
// EndpointOpts.Availability defaults to the public endpoint. //
// 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) { func (eo *EndpointOpts) ApplyDefaults(t string) {
if eo.Type == "" { if eo.Type == "" {
eo.Type = t eo.Type = t

View File

@@ -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)
}

View File

@@ -28,7 +28,7 @@ type Volume struct {
CreatedAt string `mapstructure:"created_at"` CreatedAt string `mapstructure:"created_at"`
// Human-readable description for the volume. // Human-readable description for the volume.
Description string `mapstructure:"display_discription"` Description string `mapstructure:"display_description"`
// The type of volume to create, either SATA or SSD. // The type of volume to create, either SATA or SSD.
VolumeType string `mapstructure:"volume_type"` VolumeType string `mapstructure:"volume_type"`

View File

@@ -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)
}

View File

@@ -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

View File

@@ -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)
})
}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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")
}

View File

@@ -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

View File

@@ -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"
}
]
}
`)
})
}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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

View File

@@ -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)
})
}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -0,0 +1,8 @@
package serviceassets
import "github.com/rackspace/gophercloud"
// DeleteResult represents the result of a Delete operation.
type DeleteResult struct {
gophercloud.ErrResult
}

View File

@@ -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")
}

View File

@@ -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

View File

@@ -0,0 +1,7 @@
package services
import "fmt"
func no(str string) error {
return fmt.Errorf("Required parameter %s not provided", str)
}

View File

@@ -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)
})
}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -203,3 +203,14 @@ func NewBlockStorageV1(client *gophercloud.ProviderClient, eo gophercloud.Endpoi
} }
return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil 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
}

View File

@@ -0,0 +1 @@
package defsecrules

View File

@@ -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)
})
}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -5,9 +5,34 @@ import (
"github.com/racker/perigee" "github.com/racker/perigee"
"github.com/rackspace/gophercloud" "github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/openstack/compute/v2/servers"
"github.com/rackspace/gophercloud/pagination" "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. // List returns a Pager that allows you to iterate over a collection of KeyPairs.
func List(client *gophercloud.ServiceClient) pagination.Pager { func List(client *gophercloud.ServiceClient) pagination.Pager {
return pagination.NewPager(client, listURL(client), func(r pagination.PageResult) pagination.Page { return pagination.NewPager(client, listURL(client), func(r pagination.PageResult) pagination.Page {
@@ -21,7 +46,7 @@ type CreateOptsBuilder interface {
ToKeyPairCreateMap() (map[string]interface{}, error) ToKeyPairCreateMap() (map[string]interface{}, error)
} }
// CreateOpts species keypair creation or import parameters. // CreateOpts specifies keypair creation or import parameters.
type CreateOpts struct { type CreateOpts struct {
// Name [required] is a friendly name to refer to this KeyPair in other services. // Name [required] is a friendly name to refer to this KeyPair in other services.
Name string Name string

View File

@@ -0,0 +1 @@
package secgroups

View File

@@ -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)
})
}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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")
}

View File

@@ -0,0 +1,5 @@
/*
Package startstop provides functionality to start and stop servers that have
been provisioned by the OpenStack Compute service.
*/
package startstop

View File

@@ -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)
})
}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -457,3 +457,103 @@ func HandleRebuildSuccessfully(t *testing.T, response string) {
fmt.Fprintf(w, response) 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"}}`))
})
}

View File

@@ -2,6 +2,7 @@ package servers
import ( import (
"encoding/base64" "encoding/base64"
"errors"
"fmt" "fmt"
"github.com/racker/perigee" "github.com/racker/perigee"
@@ -131,6 +132,16 @@ type CreateOpts struct {
// ConfigDrive [optional] enables metadata injection through a configuration drive. // ConfigDrive [optional] enables metadata injection through a configuration drive.
ConfigDrive bool 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
// AccessIPv4 [optional] specifies an IPv4 address for the instance.
AccessIPv4 string
// AccessIPv6 [optional] specifies an IPv6 address for the instance.
AccessIPv6 string
} }
// ToServerCreateMap assembles a request body based on the contents of a CreateOpts. // ToServerCreateMap assembles a request body based on the contents of a CreateOpts.
@@ -158,12 +169,22 @@ func (opts CreateOpts) ToServerCreateMap() (map[string]interface{}, error) {
if opts.Metadata != nil { if opts.Metadata != nil {
server["metadata"] = opts.Metadata server["metadata"] = opts.Metadata
} }
if opts.AdminPass != "" {
server["adminPass"] = opts.AdminPass
}
if opts.AccessIPv4 != "" {
server["accessIPv4"] = opts.AccessIPv4
}
if opts.AccessIPv6 != "" {
server["accessIPv6"] = opts.AccessIPv6
}
if len(opts.SecurityGroups) > 0 { if len(opts.SecurityGroups) > 0 {
securityGroups := make([]map[string]interface{}, len(opts.SecurityGroups)) securityGroups := make([]map[string]interface{}, len(opts.SecurityGroups))
for i, groupName := range opts.SecurityGroups { for i, groupName := range opts.SecurityGroups {
securityGroups[i] = map[string]interface{}{"name": groupName} securityGroups[i] = map[string]interface{}{"name": groupName}
} }
server["security_groups"] = securityGroups
} }
if len(opts.Networks) > 0 { if len(opts.Networks) > 0 {
@@ -221,6 +242,7 @@ func Get(client *gophercloud.ServiceClient, id string) GetResult {
_, result.Err = perigee.Request("GET", getURL(client, id), perigee.Options{ _, result.Err = perigee.Request("GET", getURL(client, id), perigee.Options{
Results: &result.Body, Results: &result.Body,
MoreHeaders: client.AuthenticatedHeaders(), MoreHeaders: client.AuthenticatedHeaders(),
OkCodes: []int{200, 203},
}) })
return result return result
} }
@@ -475,7 +497,7 @@ type ResizeOpts struct {
FlavorRef string 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. // Resize request.
func (opts ResizeOpts) ToServerResizeMap() (map[string]interface{}, error) { func (opts ResizeOpts) ToServerResizeMap() (map[string]interface{}, error) {
resize := map[string]interface{}{ resize := map[string]interface{}{
@@ -536,3 +558,182 @@ func RevertResize(client *gophercloud.ServiceClient, id string) ActionResult {
return res 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
}

Some files were not shown because too many files have changed in this diff Show More