From 63ba88a2740db59f8354ae241902b43c92f8c76b Mon Sep 17 00:00:00 2001 From: Sreekanth Pothanis Date: Wed, 16 Sep 2015 12:00:25 -0700 Subject: [PATCH] updated gophercloud --- Godeps/Godeps.json | 4 +- .../rackspace/gophercloud/CONTRIBUTING.md | 5 +- .../openstack/compute/v2/network_test.go | 78 ++++++ .../openstack/compute/v2/servergroup_test.go | 87 ++++++- .../openstack/compute/v2/servers_test.go | 6 + .../rackspace/identity/v2/tokens_test.go | 61 +++++ .../rackspace/gophercloud/auth_options.go | 4 + .../blockstorage/v1/snapshots/requests.go | 33 +++ .../blockstorage/v1/volumes/requests.go | 33 +++ .../blockstorage/v1/volumes/requests_test.go | 13 +- .../blockstorage/v1/volumes/testing/doc.go | 7 + .../v1/volumes/{ => testing}/fixtures.go | 2 +- .../bootfromvolume/requests_test.go | 2 + .../v2/extensions/diskconfig/requests_test.go | 2 + .../compute/v2/extensions/networks/doc.go | 2 + .../v2/extensions/networks/fixtures.go | 209 +++++++++++++++++ .../v2/extensions/networks/requests.go | 22 ++ .../v2/extensions/networks/requests_test.go | 37 +++ .../compute/v2/extensions/networks/results.go | 222 ++++++++++++++++++ .../compute/v2/extensions/networks/urls.go | 17 ++ .../v2/extensions/networks/urls_test.go | 25 ++ .../v2/extensions/schedulerhints/doc.go | 3 + .../v2/extensions/schedulerhints/requests.go | 134 +++++++++++ .../schedulerhints/requests_test.go | 130 ++++++++++ .../v2/extensions/secgroups/fixtures.go | 2 + .../extensions/volumeattach/requests_test.go | 37 ++- .../v2/extensions/volumeattach/testing/doc.go | 7 + .../volumeattach/{ => testing}/fixtures.go | 30 +-- .../openstack/compute/v2/flavors/requests.go | 35 +++ .../openstack/compute/v2/images/requests.go | 37 +++ .../openstack/compute/v2/servers/requests.go | 147 ++++++++++-- .../compute/v2/servers/requests_test.go | 37 +++ .../openstack/identity/v2/tokens/errors.go | 2 +- .../openstack/identity/v2/tokens/requests.go | 32 ++- .../identity/v2/tokens/requests_test.go | 5 +- .../openstack/identity/v3/roles/doc.go | 3 + .../openstack/identity/v3/roles/requests.go | 50 ++++ .../identity/v3/roles/requests_test.go | 104 ++++++++ .../openstack/identity/v3/roles/results.go | 81 +++++++ .../openstack/identity/v3/roles/urls.go | 7 + .../openstack/identity/v3/roles/urls_test.go | 15 ++ .../fwaas/firewalls/requests_test.go | 4 +- .../extensions/lbaas/pools/requests_test.go | 1 + .../v2/extensions/security/groups/requests.go | 38 +++ .../v2/extensions/security/rules/requests.go | 5 + .../networking/v2/networks/requests.go | 35 +++ .../networking/v2/networks/requests_test.go | 1 + .../openstack/networking/v2/ports/requests.go | 35 +++ .../networking/v2/subnets/requests.go | 39 ++- .../networking/v2/subnets/results.go | 4 +- .../networking/v2/subnets/results_test.go | 54 +++++ .../objectstorage/v1/accounts/requests.go | 8 +- .../objectstorage/v1/containers/requests.go | 12 +- .../objectstorage/v1/objects/fixtures.go | 17 +- .../objectstorage/v1/objects/requests.go | 59 ++++- .../objectstorage/v1/objects/requests_test.go | 35 ++- .../orchestration/v1/stackevents/fixtures.go | 14 +- .../orchestration/v1/stackevents/results.go | 6 +- .../v1/stackresources/fixtures.go | 6 +- .../v1/stackresources/requests.go | 10 +- .../v1/stackresources/results.go | 8 +- .../orchestration/v1/stacks/fixtures.go | 8 +- .../orchestration/v1/stacks/results.go | 12 +- .../rackspace/gophercloud/pagination/http.go | 10 +- .../rackspace/gophercloud/pagination/pager.go | 6 +- .../rackspace/gophercloud/provider_client.go | 19 +- .../blockstorage/v1/volumes/delegate_test.go | 9 +- .../v2/bootfromvolume/delegate_test.go | 2 + .../rackspace/compute/v2/flavors/delegate.go | 11 +- .../rackspace/compute/v2/flavors/fixtures.go | 22 +- .../rackspace/compute/v2/flavors/results.go | 104 ++++++++ .../rackspace/compute/v2/flavors/urls.go | 9 + .../rackspace/compute/v2/servers/requests.go | 37 ++- .../compute/v2/servers/requests_test.go | 2 + .../compute/v2/volumeattach/delegate_test.go | 49 +++- .../rackspace/lb/v1/nodes/fixtures.go | 36 +++ .../rackspace/lb/v1/nodes/requests.go | 7 +- .../rackspace/lb/v1/nodes/requests_test.go | 32 +++ .../rackspace/lb/v1/nodes/results.go | 3 + .../rackspace/lb/v1/sessions/fixtures.go | 1 + .../rackspace/lb/v1/ssl/fixtures.go | 1 + .../rackspace/lb/v1/throttle/fixtures.go | 1 + .../networking/v2/networks/delegate_test.go | 11 +- .../objectstorage/v1/bulk/requests.go | 4 +- .../v1/cdncontainers/requests.go | 12 +- .../objectstorage/v1/objects/delegate.go | 2 +- .../objectstorage/v1/objects/delegate_test.go | 16 +- .../rackspace/gophercloud/results.go | 3 + 88 files changed, 2372 insertions(+), 217 deletions(-) create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/network_test.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/identity/v2/tokens_test.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/testing/doc.go rename Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/{ => testing}/fixtures.go (99%) create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/networks/doc.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/networks/fixtures.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/networks/requests.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/networks/requests_test.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/networks/results.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/networks/urls.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/networks/urls_test.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/schedulerhints/doc.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/schedulerhints/requests.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/schedulerhints/requests_test.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/volumeattach/testing/doc.go rename Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/volumeattach/{ => testing}/fixtures.go (74%) create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/roles/doc.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/roles/requests.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/roles/requests_test.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/roles/results.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/roles/urls.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/roles/urls_test.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/subnets/results_test.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/flavors/results.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/flavors/urls.go diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index 8e4a67f9562..6ea80bc47f4 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -489,8 +489,8 @@ }, { "ImportPath": "github.com/rackspace/gophercloud", - "Comment": "v1.0.0-569-gf3ced00", - "Rev": "f3ced00552c1c7d4a6184500af9062cfb4ff4463" + "Comment": "v1.0.0-665-gf928634", + "Rev": "f92863476c034f851073599c09d90cd61ee95b3d" }, { "ImportPath": "github.com/russross/blackfriday", diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/CONTRIBUTING.md b/Godeps/_workspace/src/github.com/rackspace/gophercloud/CONTRIBUTING.md index 9748c1ad2b1..6ba5beb8543 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/CONTRIBUTING.md +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/CONTRIBUTING.md @@ -28,11 +28,10 @@ fork as `origin` instead: git remote add origin git@github.com//gophercloud ``` -4. Checkout the latest development branch ([click here](/branches) to see all -the branches): +4. Checkout the latest development branch: ```bash - git checkout release/v1.0.1 + git checkout master ``` 5. If you're working on something (discussed more in detail below), you will diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/network_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/network_test.go new file mode 100644 index 00000000000..7ebe7ec7bba --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/network_test.go @@ -0,0 +1,78 @@ +// +build acceptance compute servers + +package v2 + +import ( + "os" + "testing" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/networks" + "github.com/rackspace/gophercloud/openstack/compute/v2/servers" + th "github.com/rackspace/gophercloud/testhelper" +) + +func getNetworkIDFromNetworkExtension(t *testing.T, client *gophercloud.ServiceClient, networkName string) (string, error) { + allPages, err := networks.List(client).AllPages() + if err != nil { + t.Fatalf("Unable to list networks: %v", err) + } + + networkList, err := networks.ExtractNetworks(allPages) + if err != nil { + t.Fatalf("Unable to list networks: %v", err) + } + + networkID := "" + for _, network := range networkList { + t.Logf("Network: %v", network) + if network.Label == networkName { + networkID = network.ID + } + } + + t.Logf("Found network ID for %s: %s\n", networkName, networkID) + + return networkID, nil +} + +func TestNetworks(t *testing.T) { + networkName := os.Getenv("OS_NETWORK_NAME") + if networkName == "" { + t.Fatalf("OS_NETWORK_NAME must be set") + } + + choices, err := ComputeChoicesFromEnv() + if err != nil { + t.Fatal(err) + } + + client, err := newClient() + if err != nil { + t.Fatalf("Unable to create a compute client: %v", err) + } + + networkID, err := getNetworkIDFromNetworkExtension(t, client, networkName) + if err != nil { + t.Fatalf("Unable to get network ID: %v", err) + } + + // createNetworkServer is defined in tenantnetworks_test.go + server, err := createNetworkServer(t, client, choices, networkID) + if err != nil { + t.Fatalf("Unable to create server: %v", err) + } + defer func() { + servers.Delete(client, server.ID) + t.Logf("Server deleted.") + }() + + if err = waitForStatus(client, server, "ACTIVE"); err != nil { + t.Fatalf("Unable to wait for server: %v", err) + } + + allPages, err := networks.List(client).AllPages() + allNetworks, err := networks.ExtractNetworks(allPages) + th.AssertNoErr(t, err) + t.Logf("Retrieved all %d networks: %+v", len(allNetworks), allNetworks) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/servergroup_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/servergroup_test.go index 80015e143f5..945854e3b27 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/servergroup_test.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/servergroup_test.go @@ -3,10 +3,15 @@ package v2 import ( + "fmt" "testing" "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/acceptance/tools" + "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/schedulerhints" "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/servergroups" + "github.com/rackspace/gophercloud/openstack/compute/v2/servers" + th "github.com/rackspace/gophercloud/testhelper" ) func createServerGroup(t *testing.T, computeClient *gophercloud.ServiceClient) (*servergroups.ServerGroup, error) { @@ -36,7 +41,53 @@ func getServerGroup(t *testing.T, computeClient *gophercloud.ServiceClient, sgID return nil } +func createServerInGroup(t *testing.T, computeClient *gophercloud.ServiceClient, choices *ComputeChoices, serverGroup *servergroups.ServerGroup) (*servers.Server, error) { + if testing.Short() { + t.Skip("Skipping test that requires server creation in short mode.") + } + + name := tools.RandomString("ACPTTEST", 16) + t.Logf("Attempting to create server: %s\n", name) + + pwd := tools.MakeNewPassword("") + + serverCreateOpts := servers.CreateOpts{ + Name: name, + FlavorRef: choices.FlavorID, + ImageRef: choices.ImageID, + AdminPass: pwd, + } + server, err := servers.Create(computeClient, schedulerhints.CreateOptsExt{ + serverCreateOpts, + schedulerhints.SchedulerHints{ + Group: serverGroup.ID, + }, + }).Extract() + if err != nil { + t.Fatalf("Unable to create server: %v", err) + } + + th.AssertEquals(t, pwd, server.AdminPass) + + return server, err +} + +func verifySchedulerWorked(t *testing.T, firstServer, secondServer *servers.Server) error { + t.Logf("First server hostID: %v", firstServer.HostID) + t.Logf("Second server hostID: %v", secondServer.HostID) + if firstServer.HostID == secondServer.HostID { + return nil + } + + return fmt.Errorf("%s and %s were not scheduled on the same host.", firstServer.ID, secondServer.ID) +} + func TestServerGroups(t *testing.T) { + choices, err := ComputeChoicesFromEnv() + if err != nil { + t.Fatal(err) + } + computeClient, err := newClient() if err != nil { t.Fatalf("Unable to create a compute client: %v", err) @@ -48,11 +99,45 @@ func TestServerGroups(t *testing.T) { } defer func() { servergroups.Delete(computeClient, sg.ID) - t.Logf("ServerGroup deleted.") + t.Logf("Server Group deleted.") }() err = getServerGroup(t, computeClient, sg.ID) if err != nil { t.Fatalf("Unable to get server group: %v", err) } + + firstServer, err := createServerInGroup(t, computeClient, choices, sg) + if err != nil { + t.Fatalf("Unable to create server: %v", err) + } + defer func() { + servers.Delete(computeClient, firstServer.ID) + t.Logf("Server deleted.") + }() + + if err = waitForStatus(computeClient, firstServer, "ACTIVE"); err != nil { + t.Fatalf("Unable to wait for server: %v", err) + } + + firstServer, err = servers.Get(computeClient, firstServer.ID).Extract() + + secondServer, err := createServerInGroup(t, computeClient, choices, sg) + if err != nil { + t.Fatalf("Unable to create server: %v", err) + } + defer func() { + servers.Delete(computeClient, secondServer.ID) + t.Logf("Server deleted.") + }() + + if err = waitForStatus(computeClient, secondServer, "ACTIVE"); err != nil { + t.Fatalf("Unable to wait for server: %v", err) + } + + secondServer, err = servers.Get(computeClient, secondServer.ID).Extract() + + if err = verifySchedulerWorked(t, firstServer, secondServer); err != nil { + t.Fatalf("Scheduling did not work: %v", err) + } } diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/servers_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/servers_test.go index 7b928e9ef50..f6c7c052458 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/servers_test.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/servers_test.go @@ -107,6 +107,12 @@ func createServer(t *testing.T, client *gophercloud.ServiceClient, choices *Comp servers.Network{UUID: network.ID}, }, AdminPass: pwd, + Personality: servers.Personality{ + &servers.File{ + Path: "/etc/test", + Contents: []byte("hello world"), + }, + }, }).Extract() if err != nil { t.Fatalf("Unable to create server: %v", err) diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/identity/v2/tokens_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/identity/v2/tokens_test.go new file mode 100644 index 00000000000..95ee7e660cd --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/identity/v2/tokens_test.go @@ -0,0 +1,61 @@ +// +build acceptance + +package v2 + +import ( + "os" + "testing" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/acceptance/tools" + "github.com/rackspace/gophercloud/rackspace" + "github.com/rackspace/gophercloud/rackspace/identity/v2/tokens" + th "github.com/rackspace/gophercloud/testhelper" +) + +func rackspaceAuthOptions(t *testing.T) gophercloud.AuthOptions { + // Obtain credentials from the environment. + options, err := rackspace.AuthOptionsFromEnv() + th.AssertNoErr(t, err) + options = tools.OnlyRS(options) + + if options.Username == "" { + t.Fatal("Please provide a Rackspace username as RS_USERNAME.") + } + if options.APIKey == "" { + t.Fatal("Please provide a Rackspace API key as RS_API_KEY.") + } + + return options +} + +func createClient(t *testing.T, auth bool) *gophercloud.ServiceClient { + ao := rackspaceAuthOptions(t) + + provider, err := rackspace.NewClient(ao.IdentityEndpoint) + th.AssertNoErr(t, err) + + if auth { + err = rackspace.Authenticate(provider, ao) + th.AssertNoErr(t, err) + } + + return rackspace.NewIdentityV2(provider) +} + +func TestTokenAuth(t *testing.T) { + authedClient := createClient(t, true) + token := authedClient.TokenID + + tenantID := os.Getenv("RS_TENANT_ID") + if tenantID == "" { + t.Skip("You must set RS_TENANT_ID environment variable to run this test") + } + + authOpts := tokens.AuthOptions{} + authOpts.TenantID = tenantID + authOpts.Token = token + + _, err := tokens.Create(authedClient, authOpts).ExtractToken() + th.AssertNoErr(t, err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/auth_options.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/auth_options.go index 9819e45f2d7..d26e16ac1c4 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/auth_options.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/auth_options.go @@ -43,4 +43,8 @@ type AuthOptions struct { // false, it will not cache these settings, but re-authentication will not be // possible. This setting defaults to false. AllowReauth bool + + // TokenID allows users to authenticate (possibly as another user) with an + // authentication token ID. + TokenID string } diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots/requests.go index d2f10aa6b59..71936e51b64 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots/requests.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots/requests.go @@ -171,3 +171,36 @@ func UpdateMetadata(client *gophercloud.ServiceClient, id string, opts UpdateMet }) return res } + +// IDFromName is a convienience function that returns a snapshot's ID given its name. +func IDFromName(client *gophercloud.ServiceClient, name string) (string, error) { + snapshotCount := 0 + snapshotID := "" + if name == "" { + return "", fmt.Errorf("A snapshot name must be provided.") + } + pager := List(client, nil) + pager.EachPage(func(page pagination.Page) (bool, error) { + snapshotList, err := ExtractSnapshots(page) + if err != nil { + return false, err + } + + for _, s := range snapshotList { + if s.Name == name { + snapshotCount++ + snapshotID = s.ID + } + } + return true, nil + }) + + switch snapshotCount { + case 0: + return "", fmt.Errorf("Unable to find snapshot: %s", name) + case 1: + return snapshotID, nil + default: + return "", fmt.Errorf("Found %d snapshots matching %s", snapshotCount, name) + } +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/requests.go index 253aaf7c545..3e9243ac495 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/requests.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/requests.go @@ -201,3 +201,36 @@ func Update(client *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder }) return res } + +// IDFromName is a convienience function that returns a server's ID given its name. +func IDFromName(client *gophercloud.ServiceClient, name string) (string, error) { + volumeCount := 0 + volumeID := "" + if name == "" { + return "", fmt.Errorf("A volume name must be provided.") + } + pager := List(client, nil) + pager.EachPage(func(page pagination.Page) (bool, error) { + volumeList, err := ExtractVolumes(page) + if err != nil { + return false, err + } + + for _, s := range volumeList { + if s.Name == name { + volumeCount++ + volumeID = s.ID + } + } + return true, nil + }) + + switch volumeCount { + case 0: + return "", fmt.Errorf("Unable to find volume: %s", name) + case 1: + return volumeID, nil + default: + return "", fmt.Errorf("Found %d volumes matching %s", volumeCount, name) + } +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/requests_test.go index c484cf08df3..75c2bbc5965 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/requests_test.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/requests_test.go @@ -3,6 +3,7 @@ package volumes import ( "testing" + fixtures "github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/testing" "github.com/rackspace/gophercloud/pagination" th "github.com/rackspace/gophercloud/testhelper" "github.com/rackspace/gophercloud/testhelper/client" @@ -12,7 +13,7 @@ func TestList(t *testing.T) { th.SetupHTTP() defer th.TeardownHTTP() - MockListResponse(t) + fixtures.MockListResponse(t) count := 0 @@ -49,7 +50,7 @@ func TestListAll(t *testing.T) { th.SetupHTTP() defer th.TeardownHTTP() - MockListResponse(t) + fixtures.MockListResponse(t) allPages, err := List(client.ServiceClient(), &ListOpts{}).AllPages() th.AssertNoErr(t, err) @@ -75,7 +76,7 @@ func TestGet(t *testing.T) { th.SetupHTTP() defer th.TeardownHTTP() - MockGetResponse(t) + fixtures.MockGetResponse(t) v, err := Get(client.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22").Extract() th.AssertNoErr(t, err) @@ -89,7 +90,7 @@ func TestCreate(t *testing.T) { th.SetupHTTP() defer th.TeardownHTTP() - MockCreateResponse(t) + fixtures.MockCreateResponse(t) options := &CreateOpts{Size: 75} n, err := Create(client.ServiceClient(), options).Extract() @@ -103,7 +104,7 @@ func TestDelete(t *testing.T) { th.SetupHTTP() defer th.TeardownHTTP() - MockDeleteResponse(t) + fixtures.MockDeleteResponse(t) res := Delete(client.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22") th.AssertNoErr(t, res.Err) @@ -113,7 +114,7 @@ func TestUpdate(t *testing.T) { th.SetupHTTP() defer th.TeardownHTTP() - MockUpdateResponse(t) + fixtures.MockUpdateResponse(t) options := UpdateOpts{Name: "vol-002"} v, err := Update(client.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22", options).Extract() diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/testing/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/testing/doc.go new file mode 100644 index 00000000000..2f66ba5e45a --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/testing/doc.go @@ -0,0 +1,7 @@ +/* +This is package created is to hold fixtures (which imports testing), +so that importing volumes package does not inadvertently import testing into production code +More information here: +https://github.com/rackspace/gophercloud/issues/473 +*/ +package testing diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/testing/fixtures.go similarity index 99% rename from Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/fixtures.go rename to Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/testing/fixtures.go index a1b8697c2a7..3df7653f763 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/fixtures.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/testing/fixtures.go @@ -1,4 +1,4 @@ -package volumes +package testing import ( "fmt" diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/bootfromvolume/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/bootfromvolume/requests_test.go index 5bf9137906c..8a7fa746753 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/bootfromvolume/requests_test.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/bootfromvolume/requests_test.go @@ -32,6 +32,8 @@ func TestCreateOpts(t *testing.T) { "name": "createdserver", "imageRef": "asdfasdfasdf", "flavorRef": "performance1-1", + "flavorName": "", + "imageName": "", "block_device_mapping_v2":[ { "uuid":"123456", diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/diskconfig/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/diskconfig/requests_test.go index e3c26d49a55..17418a3ce3a 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/diskconfig/requests_test.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/diskconfig/requests_test.go @@ -25,6 +25,8 @@ func TestCreateOpts(t *testing.T) { "name": "createdserver", "imageRef": "asdfasdfasdf", "flavorRef": "performance1-1", + "flavorName": "", + "imageName": "", "OS-DCF:diskConfig": "MANUAL" } } diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/networks/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/networks/doc.go new file mode 100644 index 00000000000..fafe4a04d74 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/networks/doc.go @@ -0,0 +1,2 @@ +// Package network provides the ability to manage nova-networks +package networks diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/networks/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/networks/fixtures.go new file mode 100644 index 00000000000..12b94859b16 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/networks/fixtures.go @@ -0,0 +1,209 @@ +// +build fixtures + +package networks + +import ( + "fmt" + "net/http" + "testing" + "time" + + th "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +// ListOutput is a sample response to a List call. +const ListOutput = ` +{ + "networks": [ + { + "bridge": "br100", + "bridge_interface": "eth0", + "broadcast": "10.0.0.7", + "cidr": "10.0.0.0/29", + "cidr_v6": null, + "created_at": "2011-08-15 06:19:19.387525", + "deleted": false, + "deleted_at": null, + "dhcp_start": "10.0.0.3", + "dns1": null, + "dns2": null, + "gateway": "10.0.0.1", + "gateway_v6": null, + "host": "nsokolov-desktop", + "id": "20c8acc0-f747-4d71-a389-46d078ebf047", + "injected": false, + "label": "mynet_0", + "multi_host": false, + "netmask": "255.255.255.248", + "netmask_v6": null, + "priority": null, + "project_id": "1234", + "rxtx_base": null, + "updated_at": "2011-08-16 09:26:13.048257", + "vlan": 100, + "vpn_private_address": "10.0.0.2", + "vpn_public_address": "127.0.0.1", + "vpn_public_port": 1000 + }, + { + "bridge": "br101", + "bridge_interface": "eth0", + "broadcast": "10.0.0.15", + "cidr": "10.0.0.10/29", + "cidr_v6": null, + "created_at": "2011-08-15 06:19:19.885495", + "deleted": false, + "deleted_at": null, + "dhcp_start": "10.0.0.11", + "dns1": null, + "dns2": null, + "gateway": "10.0.0.9", + "gateway_v6": null, + "host": null, + "id": "20c8acc0-f747-4d71-a389-46d078ebf000", + "injected": false, + "label": "mynet_1", + "multi_host": false, + "netmask": "255.255.255.248", + "netmask_v6": null, + "priority": null, + "project_id": null, + "rxtx_base": null, + "updated_at": null, + "vlan": 101, + "vpn_private_address": "10.0.0.10", + "vpn_public_address": null, + "vpn_public_port": 1001 + } + ] +} +` + +// GetOutput is a sample response to a Get call. +const GetOutput = ` +{ + "network": { + "bridge": "br101", + "bridge_interface": "eth0", + "broadcast": "10.0.0.15", + "cidr": "10.0.0.10/29", + "cidr_v6": null, + "created_at": "2011-08-15 06:19:19.885495", + "deleted": false, + "deleted_at": null, + "dhcp_start": "10.0.0.11", + "dns1": null, + "dns2": null, + "gateway": "10.0.0.9", + "gateway_v6": null, + "host": null, + "id": "20c8acc0-f747-4d71-a389-46d078ebf000", + "injected": false, + "label": "mynet_1", + "multi_host": false, + "netmask": "255.255.255.248", + "netmask_v6": null, + "priority": null, + "project_id": null, + "rxtx_base": null, + "updated_at": null, + "vlan": 101, + "vpn_private_address": "10.0.0.10", + "vpn_public_address": null, + "vpn_public_port": 1001 + } +} +` + +// FirstNetwork is the first result in ListOutput. +var nilTime time.Time +var FirstNetwork = Network{ + Bridge: "br100", + BridgeInterface: "eth0", + Broadcast: "10.0.0.7", + CIDR: "10.0.0.0/29", + CIDRv6: "", + CreatedAt: time.Date(2011, 8, 15, 6, 19, 19, 387525000, time.UTC), + Deleted: false, + DeletedAt: nilTime, + DHCPStart: "10.0.0.3", + DNS1: "", + DNS2: "", + Gateway: "10.0.0.1", + Gatewayv6: "", + Host: "nsokolov-desktop", + ID: "20c8acc0-f747-4d71-a389-46d078ebf047", + Injected: false, + Label: "mynet_0", + MultiHost: false, + Netmask: "255.255.255.248", + Netmaskv6: "", + Priority: 0, + ProjectID: "1234", + RXTXBase: 0, + UpdatedAt: time.Date(2011, 8, 16, 9, 26, 13, 48257000, time.UTC), + VLAN: 100, + VPNPrivateAddress: "10.0.0.2", + VPNPublicAddress: "127.0.0.1", + VPNPublicPort: 1000, +} + +// SecondNetwork is the second result in ListOutput. +var SecondNetwork = Network{ + Bridge: "br101", + BridgeInterface: "eth0", + Broadcast: "10.0.0.15", + CIDR: "10.0.0.10/29", + CIDRv6: "", + CreatedAt: time.Date(2011, 8, 15, 6, 19, 19, 885495000, time.UTC), + Deleted: false, + DeletedAt: nilTime, + DHCPStart: "10.0.0.11", + DNS1: "", + DNS2: "", + Gateway: "10.0.0.9", + Gatewayv6: "", + Host: "", + ID: "20c8acc0-f747-4d71-a389-46d078ebf000", + Injected: false, + Label: "mynet_1", + MultiHost: false, + Netmask: "255.255.255.248", + Netmaskv6: "", + Priority: 0, + ProjectID: "", + RXTXBase: 0, + UpdatedAt: nilTime, + VLAN: 101, + VPNPrivateAddress: "10.0.0.10", + VPNPublicAddress: "", + VPNPublicPort: 1001, +} + +// ExpectedNetworkSlice is the slice of results that should be parsed +// from ListOutput, in the expected order. +var ExpectedNetworkSlice = []Network{FirstNetwork, SecondNetwork} + +// HandleListSuccessfully configures the test server to respond to a List request. +func HandleListSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/os-networks", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, ListOutput) + }) +} + +// HandleGetSuccessfully configures the test server to respond to a Get request +// for an existing network. +func HandleGetSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/os-networks/20c8acc0-f747-4d71-a389-46d078ebf000", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, GetOutput) + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/networks/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/networks/requests.go new file mode 100644 index 00000000000..eb203878ba0 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/networks/requests.go @@ -0,0 +1,22 @@ +package networks + +import ( + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// List returns a Pager that allows you to iterate over a collection of Network. +func List(client *gophercloud.ServiceClient) pagination.Pager { + url := listURL(client) + createPage := func(r pagination.PageResult) pagination.Page { + return NetworkPage{pagination.SinglePageBase(r)} + } + return pagination.NewPager(client, url, createPage) +} + +// Get returns data about a previously created Network. +func Get(client *gophercloud.ServiceClient, id string) GetResult { + var res GetResult + _, res.Err = client.Get(getURL(client, id), &res.Body, nil) + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/networks/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/networks/requests_test.go new file mode 100644 index 00000000000..722b3f0bd07 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/networks/requests_test.go @@ -0,0 +1,37 @@ +package networks + +import ( + "testing" + + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListSuccessfully(t) + + count := 0 + err := List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractNetworks(page) + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, ExpectedNetworkSlice, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, 1, count) +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleGetSuccessfully(t) + + actual, err := Get(client.ServiceClient(), "20c8acc0-f747-4d71-a389-46d078ebf000").Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &SecondNetwork, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/networks/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/networks/results.go new file mode 100644 index 00000000000..55b361d7fc9 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/networks/results.go @@ -0,0 +1,222 @@ +package networks + +import ( + "fmt" + "time" + + "github.com/mitchellh/mapstructure" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// A Network represents a nova-network that an instance communicates on +type Network struct { + // The Bridge that VIFs on this network are connected to + Bridge string `mapstructure:"bridge"` + + // BridgeInterface is what interface is connected to the Bridge + BridgeInterface string `mapstructure:"bridge_interface"` + + // The Broadcast address of the network. + Broadcast string `mapstructure:"broadcast"` + + // CIDR is the IPv4 subnet. + CIDR string `mapstructure:"cidr"` + + // CIDRv6 is the IPv6 subnet. + CIDRv6 string `mapstructure:"cidr_v6"` + + // CreatedAt is when the network was created.. + CreatedAt time.Time `mapstructure:"-"` + + // Deleted shows if the network has been deleted. + Deleted bool `mapstructure:"deleted"` + + // DeletedAt is the time when the network was deleted. + DeletedAt time.Time `mapstructure:"-"` + + // DHCPStart is the start of the DHCP address range. + DHCPStart string `mapstructure:"dhcp_start"` + + // DNS1 is the first DNS server to use through DHCP. + DNS1 string `mapstructure:"dns_1"` + + // DNS2 is the first DNS server to use through DHCP. + DNS2 string `mapstructure:"dns_2"` + + // Gateway is the network gateway. + Gateway string `mapstructure:"gateway"` + + // Gatewayv6 is the IPv6 network gateway. + Gatewayv6 string `mapstructure:"gateway_v6"` + + // Host is the host that the network service is running on. + Host string `mapstructure:"host"` + + // ID is the UUID of the network. + ID string `mapstructure:"id"` + + // Injected determines if network information is injected into the host. + Injected bool `mapstructure:"injected"` + + // Label is the common name that the network has.. + Label string `mapstructure:"label"` + + // MultiHost is if multi-host networking is enablec.. + MultiHost bool `mapstructure:"multi_host"` + + // Netmask is the network netmask. + Netmask string `mapstructure:"netmask"` + + // Netmaskv6 is the IPv6 netmask. + Netmaskv6 string `mapstructure:"netmask_v6"` + + // Priority is the network interface priority. + Priority int `mapstructure:"priority"` + + // ProjectID is the project associated with this network. + ProjectID string `mapstructure:"project_id"` + + // RXTXBase configures bandwidth entitlement. + RXTXBase int `mapstructure:"rxtx_base"` + + // UpdatedAt is the time when the network was last updated. + UpdatedAt time.Time `mapstructure:"-"` + + // VLAN is the vlan this network runs on. + VLAN int `mapstructure:"vlan"` + + // VPNPrivateAddress is the private address of the CloudPipe VPN. + VPNPrivateAddress string `mapstructure:"vpn_private_address"` + + // VPNPublicAddress is the public address of the CloudPipe VPN. + VPNPublicAddress string `mapstructure:"vpn_public_address"` + + // VPNPublicPort is the port of the CloudPipe VPN. + VPNPublicPort int `mapstructure:"vpn_public_port"` +} + +// NetworkPage stores a single, only page of Networks +// results from a List call. +type NetworkPage struct { + pagination.SinglePageBase +} + +// IsEmpty determines whether or not a NetworkPage is empty. +func (page NetworkPage) IsEmpty() (bool, error) { + va, err := ExtractNetworks(page) + return len(va) == 0, err +} + +// ExtractNetworks interprets a page of results as a slice of Networks +func ExtractNetworks(page pagination.Page) ([]Network, error) { + var res struct { + Networks []Network `mapstructure:"networks"` + } + + err := mapstructure.Decode(page.(NetworkPage).Body, &res) + + var rawNetworks []interface{} + body := page.(NetworkPage).Body + switch body.(type) { + case map[string]interface{}: + rawNetworks = body.(map[string]interface{})["networks"].([]interface{}) + case map[string][]interface{}: + rawNetworks = body.(map[string][]interface{})["networks"] + default: + return res.Networks, fmt.Errorf("Unknown type") + } + + for i := range rawNetworks { + thisNetwork := rawNetworks[i].(map[string]interface{}) + if t, ok := thisNetwork["created_at"].(string); ok && t != "" { + createdAt, err := time.Parse("2006-01-02 15:04:05.000000", t) + if err != nil { + return res.Networks, err + } + res.Networks[i].CreatedAt = createdAt + } + + if t, ok := thisNetwork["updated_at"].(string); ok && t != "" { + updatedAt, err := time.Parse("2006-01-02 15:04:05.000000", t) + if err != nil { + return res.Networks, err + } + res.Networks[i].UpdatedAt = updatedAt + } + + if t, ok := thisNetwork["deleted_at"].(string); ok && t != "" { + deletedAt, err := time.Parse("2006-01-02 15:04:05.000000", t) + if err != nil { + return res.Networks, err + } + res.Networks[i].DeletedAt = deletedAt + } + } + + return res.Networks, err +} + +type NetworkResult struct { + gophercloud.Result +} + +// Extract is a method that attempts to interpret any Network resource +// response as a Network struct. +func (r NetworkResult) Extract() (*Network, error) { + if r.Err != nil { + return nil, r.Err + } + + var res struct { + Network *Network `json:"network" mapstructure:"network"` + } + + config := &mapstructure.DecoderConfig{ + Result: &res, + WeaklyTypedInput: true, + } + decoder, err := mapstructure.NewDecoder(config) + if err != nil { + return nil, err + } + + if err := decoder.Decode(r.Body); err != nil { + return nil, err + } + + b := r.Body.(map[string]interface{})["network"].(map[string]interface{}) + + if t, ok := b["created_at"].(string); ok && t != "" { + createdAt, err := time.Parse("2006-01-02 15:04:05.000000", t) + if err != nil { + return res.Network, err + } + res.Network.CreatedAt = createdAt + } + + if t, ok := b["updated_at"].(string); ok && t != "" { + updatedAt, err := time.Parse("2006-01-02 15:04:05.000000", t) + if err != nil { + return res.Network, err + } + res.Network.UpdatedAt = updatedAt + } + + if t, ok := b["deleted_at"].(string); ok && t != "" { + deletedAt, err := time.Parse("2006-01-02 15:04:05.000000", t) + if err != nil { + return res.Network, err + } + res.Network.DeletedAt = deletedAt + } + + return res.Network, err + +} + +// GetResult is the response from a Get operation. Call its Extract method to interpret it +// as a Network. +type GetResult struct { + NetworkResult +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/networks/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/networks/urls.go new file mode 100644 index 00000000000..69664620aff --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/networks/urls.go @@ -0,0 +1,17 @@ +package networks + +import "github.com/rackspace/gophercloud" + +const resourcePath = "os-networks" + +func resourceURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(resourcePath) +} + +func listURL(c *gophercloud.ServiceClient) string { + return resourceURL(c) +} + +func getURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(resourcePath, id) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/networks/urls_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/networks/urls_test.go new file mode 100644 index 00000000000..be54c90a566 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/networks/urls_test.go @@ -0,0 +1,25 @@ +package networks + +import ( + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestListURL(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + c := client.ServiceClient() + + th.CheckEquals(t, c.Endpoint+"os-networks", listURL(c)) +} + +func TestGetURL(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + c := client.ServiceClient() + id := "1" + + th.CheckEquals(t, c.Endpoint+"os-networks/"+id, getURL(c, id)) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/schedulerhints/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/schedulerhints/doc.go new file mode 100644 index 00000000000..0bd45661b56 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/schedulerhints/doc.go @@ -0,0 +1,3 @@ +// Package schedulerhints enables instances to provide the OpenStack scheduler +// hints about where they should be placed in the cloud. +package schedulerhints diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/schedulerhints/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/schedulerhints/requests.go new file mode 100644 index 00000000000..567eef41a11 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/schedulerhints/requests.go @@ -0,0 +1,134 @@ +package schedulerhints + +import ( + "fmt" + "net" + "regexp" + "strings" + + "github.com/rackspace/gophercloud/openstack/compute/v2/servers" +) + +// SchedulerHints represents a set of scheduling hints that are passed to the +// OpenStack scheduler +type SchedulerHints struct { + // Group specifies a Server Group to place the instance in. + Group string + + // DifferentHost will place the instance on a compute node that does not + // host the given instances. + DifferentHost []string + + // SameHost will place the instance on a compute node that hosts the given + // instances. + SameHost []string + + // Query is a conditional statement that results in compute nodes able to + // host the instance. + Query []interface{} + + // TargetCell specifies a cell name where the instance will be placed. + TargetCell string + + // BuildNearHostIP specifies a subnet of compute nodes to host the instance. + BuildNearHostIP string +} + +// SchedulerHintsBuilder builds the scheduler hints into a serializable format. +type SchedulerHintsBuilder interface { + ToServerSchedulerHintsMap() (map[string]interface{}, error) +} + +// ToServerSchedulerHintsMap builds the scheduler hints into a serializable format. +func (opts SchedulerHints) ToServerSchedulerHintsMap() (map[string]interface{}, error) { + sh := make(map[string]interface{}) + + uuidRegex, _ := regexp.Compile("^[a-z0-9]{8}-[a-z0-9]{4}-[1-5][a-z0-9]{3}-[a-z0-9]{4}-[a-z0-9]{12}$") + + if opts.Group != "" { + if !uuidRegex.MatchString(opts.Group) { + return nil, fmt.Errorf("Group must be a UUID") + } + sh["group"] = opts.Group + } + + if len(opts.DifferentHost) > 0 { + for _, diffHost := range opts.DifferentHost { + if !uuidRegex.MatchString(diffHost) { + return nil, fmt.Errorf("The hosts in DifferentHost must be in UUID format.") + } + } + sh["different_host"] = opts.DifferentHost + } + + if len(opts.SameHost) > 0 { + for _, sameHost := range opts.SameHost { + if !uuidRegex.MatchString(sameHost) { + return nil, fmt.Errorf("The hosts in SameHost must be in UUID format.") + } + } + sh["same_host"] = opts.SameHost + } + + /* Query can be something simple like: + [">=", "$free_ram_mb", 1024] + + Or more complex like: + ['and', + ['>=', '$free_ram_mb', 1024], + ['>=', '$free_disk_mb', 200 * 1024] + ] + + Because of the possible complexity, just make sure the length is a minimum of 3. + */ + if len(opts.Query) > 0 { + if len(opts.Query) < 3 { + return nil, fmt.Errorf("Query must be a conditional statement in the format of [op,variable,value]") + } + sh["query"] = opts.Query + } + + if opts.TargetCell != "" { + sh["target_cell"] = opts.TargetCell + } + + if opts.BuildNearHostIP != "" { + if _, _, err := net.ParseCIDR(opts.BuildNearHostIP); err != nil { + return nil, fmt.Errorf("BuildNearHostIP must be a valid subnet in the form 192.168.1.1/24") + } + ipParts := strings.Split(opts.BuildNearHostIP, "/") + sh["build_near_host_ip"] = ipParts[0] + sh["cidr"] = "/" + ipParts[1] + } + + return sh, nil +} + +// CreateOptsExt adds a SchedulerHints option to the base CreateOpts. +type CreateOptsExt struct { + servers.CreateOptsBuilder + + // SchedulerHints provides a set of hints to the scheduler. + SchedulerHints SchedulerHintsBuilder +} + +// ToServerCreateMap adds the SchedulerHints option 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 + } + + schedulerHints, err := opts.SchedulerHints.ToServerSchedulerHintsMap() + if err != nil { + return nil, err + } + + if len(schedulerHints) == 0 { + return base, nil + } + + base["os:scheduler_hints"] = schedulerHints + + return base, nil +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/schedulerhints/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/schedulerhints/requests_test.go new file mode 100644 index 00000000000..9b38b35d9c4 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/schedulerhints/requests_test.go @@ -0,0 +1,130 @@ +package schedulerhints + +import ( + "testing" + + "github.com/rackspace/gophercloud/openstack/compute/v2/servers" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestCreateOpts(t *testing.T) { + base := servers.CreateOpts{ + Name: "createdserver", + ImageRef: "asdfasdfasdf", + FlavorRef: "performance1-1", + } + + schedulerHints := SchedulerHints{ + Group: "101aed42-22d9-4a3e-9ba1-21103b0d1aba", + DifferentHost: []string{ + "a0cf03a5-d921-4877-bb5c-86d26cf818e1", + "8c19174f-4220-44f0-824a-cd1eeef10287", + }, + SameHost: []string{ + "a0cf03a5-d921-4877-bb5c-86d26cf818e1", + "8c19174f-4220-44f0-824a-cd1eeef10287", + }, + Query: []interface{}{">=", "$free_ram_mb", "1024"}, + TargetCell: "foobar", + BuildNearHostIP: "192.168.1.1/24", + } + + ext := CreateOptsExt{ + CreateOptsBuilder: base, + SchedulerHints: schedulerHints, + } + + expected := ` + { + "server": { + "name": "createdserver", + "imageRef": "asdfasdfasdf", + "flavorRef": "performance1-1", + "flavorName": "", + "imageName": "" + }, + "os:scheduler_hints": { + "group": "101aed42-22d9-4a3e-9ba1-21103b0d1aba", + "different_host": [ + "a0cf03a5-d921-4877-bb5c-86d26cf818e1", + "8c19174f-4220-44f0-824a-cd1eeef10287" + ], + "same_host": [ + "a0cf03a5-d921-4877-bb5c-86d26cf818e1", + "8c19174f-4220-44f0-824a-cd1eeef10287" + ], + "query": [ + ">=", "$free_ram_mb", "1024" + ], + "target_cell": "foobar", + "build_near_host_ip": "192.168.1.1", + "cidr": "/24" + } + } + ` + actual, err := ext.ToServerCreateMap() + th.AssertNoErr(t, err) + th.CheckJSONEquals(t, expected, actual) +} + +func TestCreateOptsWithComplexQuery(t *testing.T) { + base := servers.CreateOpts{ + Name: "createdserver", + ImageRef: "asdfasdfasdf", + FlavorRef: "performance1-1", + } + + schedulerHints := SchedulerHints{ + Group: "101aed42-22d9-4a3e-9ba1-21103b0d1aba", + DifferentHost: []string{ + "a0cf03a5-d921-4877-bb5c-86d26cf818e1", + "8c19174f-4220-44f0-824a-cd1eeef10287", + }, + SameHost: []string{ + "a0cf03a5-d921-4877-bb5c-86d26cf818e1", + "8c19174f-4220-44f0-824a-cd1eeef10287", + }, + Query: []interface{}{"and", []string{">=", "$free_ram_mb", "1024"}, []string{">=", "$free_disk_mb", "204800"}}, + TargetCell: "foobar", + BuildNearHostIP: "192.168.1.1/24", + } + + ext := CreateOptsExt{ + CreateOptsBuilder: base, + SchedulerHints: schedulerHints, + } + + expected := ` + { + "server": { + "name": "createdserver", + "imageRef": "asdfasdfasdf", + "flavorRef": "performance1-1", + "flavorName": "", + "imageName": "" + }, + "os:scheduler_hints": { + "group": "101aed42-22d9-4a3e-9ba1-21103b0d1aba", + "different_host": [ + "a0cf03a5-d921-4877-bb5c-86d26cf818e1", + "8c19174f-4220-44f0-824a-cd1eeef10287" + ], + "same_host": [ + "a0cf03a5-d921-4877-bb5c-86d26cf818e1", + "8c19174f-4220-44f0-824a-cd1eeef10287" + ], + "query": [ + "and", + [">=", "$free_ram_mb", "1024"], + [">=", "$free_disk_mb", "204800"] + ], + "target_cell": "foobar", + "build_near_host_ip": "192.168.1.1", + "cidr": "/24" + } + } + ` + actual, err := ext.ToServerCreateMap() + th.AssertNoErr(t, err) + th.CheckJSONEquals(t, expected, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/secgroups/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/secgroups/fixtures.go index 1c6ba394920..8c42e48e4ed 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/secgroups/fixtures.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/secgroups/fixtures.go @@ -242,6 +242,7 @@ func mockAddServerToGroupResponse(t *testing.T, serverID string) { w.Header().Add("Content-Type", "application/json") w.WriteHeader(http.StatusAccepted) + fmt.Fprintf(w, `{}`) }) } @@ -261,5 +262,6 @@ func mockRemoveServerFromGroupResponse(t *testing.T, serverID string) { w.Header().Add("Content-Type", "application/json") w.WriteHeader(http.StatusAccepted) + fmt.Fprintf(w, `{}`) }) } diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/volumeattach/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/volumeattach/requests_test.go index e17f7e00637..b0a765befa9 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/volumeattach/requests_test.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/volumeattach/requests_test.go @@ -3,15 +3,44 @@ package volumeattach import ( "testing" + fixtures "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/volumeattach/testing" "github.com/rackspace/gophercloud/pagination" th "github.com/rackspace/gophercloud/testhelper" "github.com/rackspace/gophercloud/testhelper/client" ) +// FirstVolumeAttachment is the first result in ListOutput. +var FirstVolumeAttachment = VolumeAttachment{ + Device: "/dev/vdd", + ID: "a26887c6-c47b-4654-abb5-dfadf7d3f803", + ServerID: "4d8c3732-a248-40ed-bebc-539a6ffd25c0", + VolumeID: "a26887c6-c47b-4654-abb5-dfadf7d3f803", +} + +// SecondVolumeAttachment is the first result in ListOutput. +var SecondVolumeAttachment = VolumeAttachment{ + Device: "/dev/vdc", + ID: "a26887c6-c47b-4654-abb5-dfadf7d3f804", + ServerID: "4d8c3732-a248-40ed-bebc-539a6ffd25c0", + VolumeID: "a26887c6-c47b-4654-abb5-dfadf7d3f804", +} + +// ExpectedVolumeAttachmentSlide is the slice of results that should be parsed +// from ListOutput, in the expected order. +var ExpectedVolumeAttachmentSlice = []VolumeAttachment{FirstVolumeAttachment, SecondVolumeAttachment} + +//CreatedVolumeAttachment is the parsed result from CreatedOutput. +var CreatedVolumeAttachment = VolumeAttachment{ + Device: "/dev/vdc", + ID: "a26887c6-c47b-4654-abb5-dfadf7d3f804", + ServerID: "4d8c3732-a248-40ed-bebc-539a6ffd25c0", + VolumeID: "a26887c6-c47b-4654-abb5-dfadf7d3f804", +} + func TestList(t *testing.T) { th.SetupHTTP() defer th.TeardownHTTP() - HandleListSuccessfully(t) + fixtures.HandleListSuccessfully(t) serverId := "4d8c3732-a248-40ed-bebc-539a6ffd25c0" count := 0 @@ -30,7 +59,7 @@ func TestList(t *testing.T) { func TestCreate(t *testing.T) { th.SetupHTTP() defer th.TeardownHTTP() - HandleCreateSuccessfully(t) + fixtures.HandleCreateSuccessfully(t) serverId := "4d8c3732-a248-40ed-bebc-539a6ffd25c0" actual, err := Create(client.ServiceClient(), serverId, CreateOpts{ @@ -44,7 +73,7 @@ func TestCreate(t *testing.T) { func TestGet(t *testing.T) { th.SetupHTTP() defer th.TeardownHTTP() - HandleGetSuccessfully(t) + fixtures.HandleGetSuccessfully(t) aId := "a26887c6-c47b-4654-abb5-dfadf7d3f804" serverId := "4d8c3732-a248-40ed-bebc-539a6ffd25c0" @@ -56,7 +85,7 @@ func TestGet(t *testing.T) { func TestDelete(t *testing.T) { th.SetupHTTP() defer th.TeardownHTTP() - HandleDeleteSuccessfully(t) + fixtures.HandleDeleteSuccessfully(t) aId := "a26887c6-c47b-4654-abb5-dfadf7d3f804" serverId := "4d8c3732-a248-40ed-bebc-539a6ffd25c0" diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/volumeattach/testing/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/volumeattach/testing/doc.go new file mode 100644 index 00000000000..183391a6291 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/volumeattach/testing/doc.go @@ -0,0 +1,7 @@ +/* +This is package created is to hold fixtures (which imports testing), +so that importing volumeattach package does not inadvertently import testing into production code +More information here: +https://github.com/rackspace/gophercloud/issues/473 +*/ +package testing diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/volumeattach/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/volumeattach/testing/fixtures.go similarity index 74% rename from Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/volumeattach/fixtures.go rename to Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/volumeattach/testing/fixtures.go index a7f03b322c8..c469bfbc859 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/volumeattach/fixtures.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/volumeattach/testing/fixtures.go @@ -1,6 +1,6 @@ // +build fixtures -package volumeattach +package testing import ( "fmt" @@ -55,34 +55,6 @@ const CreateOutput = ` } ` -// FirstVolumeAttachment is the first result in ListOutput. -var FirstVolumeAttachment = VolumeAttachment{ - Device: "/dev/vdd", - ID: "a26887c6-c47b-4654-abb5-dfadf7d3f803", - ServerID: "4d8c3732-a248-40ed-bebc-539a6ffd25c0", - VolumeID: "a26887c6-c47b-4654-abb5-dfadf7d3f803", -} - -// SecondVolumeAttachment is the first result in ListOutput. -var SecondVolumeAttachment = VolumeAttachment{ - Device: "/dev/vdc", - ID: "a26887c6-c47b-4654-abb5-dfadf7d3f804", - ServerID: "4d8c3732-a248-40ed-bebc-539a6ffd25c0", - VolumeID: "a26887c6-c47b-4654-abb5-dfadf7d3f804", -} - -// ExpectedVolumeAttachmentSlide is the slice of results that should be parsed -// from ListOutput, in the expected order. -var ExpectedVolumeAttachmentSlice = []VolumeAttachment{FirstVolumeAttachment, SecondVolumeAttachment} - -// CreatedVolumeAttachment is the parsed result from CreatedOutput. -var CreatedVolumeAttachment = VolumeAttachment{ - Device: "/dev/vdc", - ID: "a26887c6-c47b-4654-abb5-dfadf7d3f804", - ServerID: "4d8c3732-a248-40ed-bebc-539a6ffd25c0", - VolumeID: "a26887c6-c47b-4654-abb5-dfadf7d3f804", -} - // HandleListSuccessfully configures the test server to respond to a List request. func HandleListSuccessfully(t *testing.T) { th.Mux.HandleFunc("/servers/4d8c3732-a248-40ed-bebc-539a6ffd25c0/os-volume_attachments", func(w http.ResponseWriter, r *http.Request) { diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/flavors/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/flavors/requests.go index 586be67ac25..59123aaf76f 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/flavors/requests.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/flavors/requests.go @@ -1,6 +1,8 @@ package flavors import ( + "fmt" + "github.com/rackspace/gophercloud" "github.com/rackspace/gophercloud/pagination" ) @@ -66,3 +68,36 @@ func Get(client *gophercloud.ServiceClient, id string) GetResult { _, res.Err = client.Get(getURL(client, id), &res.Body, nil) return res } + +// IDFromName is a convienience function that returns a flavor's ID given its name. +func IDFromName(client *gophercloud.ServiceClient, name string) (string, error) { + flavorCount := 0 + flavorID := "" + if name == "" { + return "", fmt.Errorf("A flavor name must be provided.") + } + pager := ListDetail(client, nil) + pager.EachPage(func(page pagination.Page) (bool, error) { + flavorList, err := ExtractFlavors(page) + if err != nil { + return false, err + } + + for _, f := range flavorList { + if f.Name == name { + flavorCount++ + flavorID = f.ID + } + } + return true, nil + }) + + switch flavorCount { + case 0: + return "", fmt.Errorf("Unable to find flavor: %s", name) + case 1: + return flavorID, nil + default: + return "", fmt.Errorf("Found %d flavors matching %s", flavorCount, name) + } +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/images/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/images/requests.go index 7ce5139519c..1e021ad4cd0 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/images/requests.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/images/requests.go @@ -1,6 +1,8 @@ package images import ( + "fmt" + "github.com/rackspace/gophercloud" "github.com/rackspace/gophercloud/pagination" ) @@ -70,3 +72,38 @@ func Delete(client *gophercloud.ServiceClient, id string) DeleteResult { _, result.Err = client.Delete(deleteURL(client, id), nil) return result } + +// IDFromName is a convienience function that returns an image's ID given its name. +func IDFromName(client *gophercloud.ServiceClient, name string) (string, error) { + imageCount := 0 + imageID := "" + if name == "" { + return "", fmt.Errorf("An image name must be provided.") + } + pager := ListDetail(client, &ListOpts{ + Name: name, + }) + pager.EachPage(func(page pagination.Page) (bool, error) { + imageList, err := ExtractImages(page) + if err != nil { + return false, err + } + + for _, i := range imageList { + if i.Name == name { + imageCount++ + imageID = i.ID + } + } + return true, nil + }) + + switch imageCount { + case 0: + return "", fmt.Errorf("Unable to find image: %s", name) + case 1: + return imageID, nil + default: + return "", fmt.Errorf("Found %d images matching %s", imageCount, name) + } +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/servers/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/servers/requests.go index aa8c1a87b27..f9839d9f287 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/servers/requests.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/servers/requests.go @@ -2,10 +2,13 @@ package servers import ( "encoding/base64" + "encoding/json" "errors" "fmt" "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/openstack/compute/v2/flavors" + "github.com/rackspace/gophercloud/openstack/compute/v2/images" "github.com/rackspace/gophercloud/pagination" ) @@ -14,6 +17,7 @@ import ( type ListOptsBuilder interface { ToServerListQuery() (string, error) } + // ListOpts allows the filtering and sorting of paginated collections through // the API. Filtering is achieved by passing in struct field values that map to // the server attributes you want to see returned. Marker and Limit are used @@ -45,6 +49,9 @@ type ListOpts struct { // Integer value for the limit of values to return. Limit int `q:"limit"` + + // Bool to show all tenants + AllTenants bool `q:"all_tenants"` } // ToServerListQuery formats a ListOpts into a query string. @@ -95,18 +102,54 @@ type Network struct { FixedIP string } +// Personality is an array of files that are injected into the server at launch. +type Personality []*File + +// File is used within CreateOpts and RebuildOpts to inject a file into the server at launch. +// File implements the json.Marshaler interface, so when a Create or Rebuild operation is requested, +// json.Marshal will call File's MarshalJSON method. +type File struct { + // Path of the file + Path string + // Contents of the file. Maximum content size is 255 bytes. + Contents []byte +} + +// MarshalJSON marshals the escaped file, base64 encoding the contents. +func (f *File) MarshalJSON() ([]byte, error) { + file := struct { + Path string `json:"path"` + Contents string `json:"contents"` + }{ + Path: f.Path, + Contents: base64.StdEncoding.EncodeToString(f.Contents), + } + return json.Marshal(file) +} + // CreateOpts specifies server creation parameters. type CreateOpts struct { // Name [required] is the name to assign to the newly launched server. Name string - // ImageRef [required] is the ID or full URL to the image that contains the server's OS and initial state. - // Optional if using the boot-from-volume extension. + // ImageRef [optional; required if ImageName is not provided] is the ID or full + // URL to the image that contains the server's OS and initial state. + // Also optional if using the boot-from-volume extension. ImageRef string - // FlavorRef [required] is the ID or full URL to the flavor that describes the server's specs. + // ImageName [optional; required if ImageRef is not provided] is the name of the + // image that contains the server's OS and initial state. + // Also optional if using the boot-from-volume extension. + ImageName string + + // FlavorRef [optional; required if FlavorName is not provided] is the ID or + // full URL to the flavor that describes the server's specs. FlavorRef string + // FlavorName [optional; required if FlavorRef is not provided] is the name of + // the flavor that describes the server's specs. + FlavorName string + // SecurityGroups [optional] lists the names of the security groups to which this server should belong. SecurityGroups []string @@ -124,9 +167,9 @@ type CreateOpts struct { // Metadata [optional] contains key-value pairs (up to 255 bytes each) to attach to the server. Metadata map[string]string - // Personality [optional] includes the path and contents of a file to inject into the server at launch. - // The maximum size of the file is 255 bytes (decoded). - Personality []byte + // Personality [optional] includes files to inject into the server at launch. + // Create will base64-encode file contents for you. + Personality Personality // ConfigDrive [optional] enables metadata injection through a configuration drive. ConfigDrive bool @@ -148,16 +191,14 @@ func (opts CreateOpts) ToServerCreateMap() (map[string]interface{}, error) { server["name"] = opts.Name server["imageRef"] = opts.ImageRef + server["imageName"] = opts.ImageName server["flavorRef"] = opts.FlavorRef + server["flavorName"] = opts.FlavorName if opts.UserData != nil { encoded := base64.StdEncoding.EncodeToString(opts.UserData) server["user_data"] = &encoded } - if opts.Personality != nil { - encoded := base64.StdEncoding.EncodeToString(opts.Personality) - server["personality"] = &encoded - } if opts.ConfigDrive { server["config_drive"] = "true" } @@ -202,6 +243,10 @@ func (opts CreateOpts) ToServerCreateMap() (map[string]interface{}, error) { server["networks"] = networks } + if len(opts.Personality) > 0 { + server["personality"] = opts.Personality + } + return map[string]interface{}{"server": server}, nil } @@ -215,6 +260,38 @@ func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateRes return res } + // If ImageRef isn't provided, use ImageName to ascertain the image ID. + if reqBody["server"].(map[string]interface{})["imageRef"].(string) == "" { + imageName := reqBody["server"].(map[string]interface{})["imageName"].(string) + if imageName == "" { + res.Err = errors.New("One and only one of ImageRef and ImageName must be provided.") + return res + } + imageID, err := images.IDFromName(client, imageName) + if err != nil { + res.Err = err + return res + } + reqBody["server"].(map[string]interface{})["imageRef"] = imageID + } + delete(reqBody["server"].(map[string]interface{}), "imageName") + + // If FlavorRef isn't provided, use FlavorName to ascertain the flavor ID. + if reqBody["server"].(map[string]interface{})["flavorRef"].(string) == "" { + flavorName := reqBody["server"].(map[string]interface{})["flavorName"].(string) + if flavorName == "" { + res.Err = errors.New("One and only one of FlavorRef and FlavorName must be provided.") + return res + } + flavorID, err := flavors.IDFromName(client, flavorName) + if err != nil { + res.Err = err + return res + } + reqBody["server"].(map[string]interface{})["flavorRef"] = flavorID + } + delete(reqBody["server"].(map[string]interface{}), "flavorName") + _, res.Err = client.Post(listURL(client), reqBody, &res.Body, nil) return res } @@ -391,9 +468,9 @@ type RebuildOpts struct { // Metadata [optional] contains key-value pairs (up to 255 bytes each) to attach to the server. Metadata map[string]string - // Personality [optional] includes the path and contents of a file to inject into the server at launch. - // The maximum size of the file is 255 bytes (decoded). - Personality []byte + // Personality [optional] includes files to inject into the server at launch. + // Rebuild will base64-encode file contents for you. + Personality Personality } // ToServerRebuildMap formats a RebuildOpts struct into a map for use in JSON @@ -429,9 +506,8 @@ func (opts RebuildOpts) ToServerRebuildMap() (map[string]interface{}, error) { server["metadata"] = opts.Metadata } - if opts.Personality != nil { - encoded := base64.StdEncoding.EncodeToString(opts.Personality) - server["personality"] = &encoded + if len(opts.Personality) > 0 { + server["personality"] = opts.Personality } return map[string]interface{}{"rebuild": server}, nil @@ -678,9 +754,7 @@ func Metadatum(client *gophercloud.ServiceClient, id, key string) GetMetadatumRe // 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 = client.Delete(metadatumURL(client, id, key), &gophercloud.RequestOpts{ - JSONResponse: &res.Body, - }) + _, res.Err = client.Delete(metadatumURL(client, id, key), nil) return res } @@ -741,5 +815,38 @@ func CreateImage(client *gophercloud.ServiceClient, serverId string, opts Create }) res.Err = err res.Header = response.Header - return res + return res +} + +// IDFromName is a convienience function that returns a server's ID given its name. +func IDFromName(client *gophercloud.ServiceClient, name string) (string, error) { + serverCount := 0 + serverID := "" + if name == "" { + return "", fmt.Errorf("A server name must be provided.") + } + pager := List(client, nil) + pager.EachPage(func(page pagination.Page) (bool, error) { + serverList, err := ExtractServers(page) + if err != nil { + return false, err + } + + for _, s := range serverList { + if s.Name == name { + serverCount++ + serverID = s.ID + } + } + return true, nil + }) + + switch serverCount { + case 0: + return "", fmt.Errorf("Unable to find server: %s", name) + case 1: + return serverID, nil + default: + return "", fmt.Errorf("Found %d servers matching %s", serverCount, name) + } } diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/servers/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/servers/requests_test.go index 1f39fe143bd..88cb54dd7bd 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/servers/requests_test.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/servers/requests_test.go @@ -1,6 +1,8 @@ package servers import ( + "encoding/base64" + "encoding/json" "net/http" "testing" @@ -334,3 +336,38 @@ func TestCreateServerImage(t *testing.T) { _, err := CreateImage(client.ServiceClient(), "serverimage", CreateImageOpts{Name: "test"}).ExtractImageID() th.AssertNoErr(t, err) } + +func TestMarshalPersonality(t *testing.T) { + name := "/etc/test" + contents := []byte("asdfasdf") + + personality := Personality{ + &File{ + Path: name, + Contents: contents, + }, + } + + data, err := json.Marshal(personality) + if err != nil { + t.Fatal(err) + } + + var actual []map[string]string + err = json.Unmarshal(data, &actual) + if err != nil { + t.Fatal(err) + } + + if len(actual) != 1 { + t.Fatal("expected personality length 1") + } + + if actual[0]["path"] != name { + t.Fatal("file path incorrect") + } + + if actual[0]["contents"] != base64.StdEncoding.EncodeToString(contents) { + t.Fatal("file contents incorrect") + } +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tokens/errors.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tokens/errors.go index 3a9172e0cc7..3dfdc08dbed 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tokens/errors.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tokens/errors.go @@ -18,7 +18,7 @@ var ( // ErrDomainNameProvided is returned if you attempt to authenticate with a DomainName. ErrDomainNameProvided = unacceptedAttributeErr("DomainName") - // ErrUsernameRequired is returned if you attempt ot authenticate without a Username. + // ErrUsernameRequired is returned if you attempt to authenticate without a Username. ErrUsernameRequired = errors.New("You must supply a Username in your AuthOptions.") // ErrPasswordRequired is returned if you don't provide a password. diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tokens/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tokens/requests.go index efa054fb399..074a89e69c3 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tokens/requests.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tokens/requests.go @@ -1,6 +1,10 @@ package tokens -import "github.com/rackspace/gophercloud" +import ( + "fmt" + + "github.com/rackspace/gophercloud" +) // AuthOptionsBuilder describes any argument that may be passed to the Create call. type AuthOptionsBuilder interface { @@ -38,20 +42,24 @@ func (auth AuthOptions) ToTokenCreateMap() (map[string]interface{}, error) { return nil, ErrDomainNameProvided } - // Username and Password are always required. - if auth.Username == "" { - return nil, ErrUsernameRequired - } - if auth.Password == "" { - return nil, ErrPasswordRequired - } - // Populate the request map. authMap := make(map[string]interface{}) - authMap["passwordCredentials"] = map[string]interface{}{ - "username": auth.Username, - "password": auth.Password, + if auth.Username != "" { + if auth.Password != "" { + authMap["passwordCredentials"] = map[string]interface{}{ + "username": auth.Username, + "password": auth.Password, + } + } else { + return nil, ErrPasswordRequired + } + } else if auth.TokenID != "" { + authMap["token"] = map[string]interface{}{ + "id": auth.TokenID, + } + } else { + return nil, fmt.Errorf("You must provide either username/password or tenantID/token values.") } if auth.TenantID != "" { diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tokens/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tokens/requests_test.go index 2f02825a47a..8b78c855048 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tokens/requests_test.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tokens/requests_test.go @@ -1,6 +1,7 @@ package tokens import ( + "fmt" "testing" "github.com/rackspace/gophercloud" @@ -22,7 +23,7 @@ func tokenPostErr(t *testing.T, options gophercloud.AuthOptions, expectedErr err HandleTokenPost(t, "") actualErr := Create(client.ServiceClient(), AuthOptions{options}).Err - th.CheckEquals(t, expectedErr, actualErr) + th.CheckDeepEquals(t, expectedErr, actualErr) } func TestCreateWithPassword(t *testing.T) { @@ -128,7 +129,7 @@ func TestRequireUsername(t *testing.T) { Password: "thing", } - tokenPostErr(t, options, ErrUsernameRequired) + tokenPostErr(t, options, fmt.Errorf("You must provide either username/password or tenantID/token values.")) } func TestRequirePassword(t *testing.T) { diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/roles/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/roles/doc.go new file mode 100644 index 00000000000..bdbc674d659 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/roles/doc.go @@ -0,0 +1,3 @@ +// Package roles provides information and interaction with the roles API +// resource for the OpenStack Identity service. +package roles diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/roles/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/roles/requests.go new file mode 100644 index 00000000000..d95c1e52f6f --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/roles/requests.go @@ -0,0 +1,50 @@ +package roles + +import ( + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// ListAssignmentsOptsBuilder allows extensions to add additional parameters to +// the ListAssignments request. +type ListAssignmentsOptsBuilder interface { + ToRolesListAssignmentsQuery() (string, error) +} + +// ListAssignmentsOpts allows you to query the ListAssignments method. +// Specify one of or a combination of GroupId, RoleId, ScopeDomainId, ScopeProjectId, +// and/or UserId to search for roles assigned to corresponding entities. +// Effective lists effective assignments at the user, project, and domain level, +// allowing for the effects of group membership. +type ListAssignmentsOpts struct { + GroupId string `q:"group.id"` + RoleId string `q:"role.id"` + ScopeDomainId string `q:"scope.domain.id"` + ScopeProjectId string `q:"scope.project.id"` + UserId string `q:"user.id"` + Effective bool `q:"effective"` +} + +// ToRolesListAssignmentsQuery formats a ListAssignmentsOpts into a query string. +func (opts ListAssignmentsOpts) ToRolesListAssignmentsQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + if err != nil { + return "", err + } + return q.String(), nil +} + +// ListAssignments enumerates the roles assigned to a specified resource. +func ListAssignments(client *gophercloud.ServiceClient, opts ListAssignmentsOptsBuilder) pagination.Pager { + url := listAssignmentsURL(client) + query, err := opts.ToRolesListAssignmentsQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + createPage := func(r pagination.PageResult) pagination.Page { + return RoleAssignmentsPage{pagination.LinkedPageBase{PageResult: r}} + } + + return pagination.NewPager(client, url, createPage) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/roles/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/roles/requests_test.go new file mode 100644 index 00000000000..d62dbff9a2f --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/roles/requests_test.go @@ -0,0 +1,104 @@ +package roles + +import ( + "fmt" + "net/http" + "reflect" + "testing" + + "github.com/rackspace/gophercloud/pagination" + "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestListSinglePage(t *testing.T) { + testhelper.SetupHTTP() + defer testhelper.TeardownHTTP() + + testhelper.Mux.HandleFunc("/role_assignments", func(w http.ResponseWriter, r *http.Request) { + testhelper.TestMethod(t, r, "GET") + testhelper.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, ` + { + "role_assignments": [ + { + "links": { + "assignment": "http://identity:35357/v3/domains/161718/users/313233/roles/123456" + }, + "role": { + "id": "123456" + }, + "scope": { + "domain": { + "id": "161718" + } + }, + "user": { + "id": "313233" + } + }, + { + "links": { + "assignment": "http://identity:35357/v3/projects/456789/groups/101112/roles/123456", + "membership": "http://identity:35357/v3/groups/101112/users/313233" + }, + "role": { + "id": "123456" + }, + "scope": { + "project": { + "id": "456789" + } + }, + "user": { + "id": "313233" + } + } + ], + "links": { + "self": "http://identity:35357/v3/role_assignments?effective", + "previous": null, + "next": null + } + } + `) + }) + + count := 0 + err := ListAssignments(client.ServiceClient(), ListAssignmentsOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractRoleAssignments(page) + if err != nil { + return false, err + } + + expected := []RoleAssignment{ + RoleAssignment{ + Role: Role{ID: "123456"}, + Scope: Scope{Domain: Domain{ID: "161718"}}, + User: User{ID: "313233"}, + Group: Group{}, + }, + RoleAssignment{ + Role: Role{ID: "123456"}, + Scope: Scope{Project: Project{ID: "456789"}}, + User: User{ID: "313233"}, + Group: Group{}, + }, + } + + if !reflect.DeepEqual(expected, actual) { + t.Errorf("Expected %#v, got %#v", expected, actual) + } + + return true, nil + }) + if err != nil { + t.Errorf("Unexpected error while paging: %v", err) + } + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/roles/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/roles/results.go new file mode 100644 index 00000000000..d25abd25d77 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/roles/results.go @@ -0,0 +1,81 @@ +package roles + +import ( + "github.com/rackspace/gophercloud/pagination" + + "github.com/mitchellh/mapstructure" +) + +// RoleAssignment is the result of a role assignments query. +type RoleAssignment struct { + Role Role `json:"role,omitempty"` + Scope Scope `json:"scope,omitempty"` + User User `json:"user,omitempty"` + Group Group `json:"group,omitempty"` +} + +type Role struct { + ID string `json:"id,omitempty"` +} + +type Scope struct { + Domain Domain `json:"domain,omitempty"` + Project Project `json:"domain,omitempty"` +} + +type Domain struct { + ID string `json:"id,omitempty"` +} + +type Project struct { + ID string `json:"id,omitempty"` +} + +type User struct { + ID string `json:"id,omitempty"` +} + +type Group struct { + ID string `json:"id,omitempty"` +} + +// RoleAssignmentsPage is a single page of RoleAssignments results. +type RoleAssignmentsPage struct { + pagination.LinkedPageBase +} + +// IsEmpty returns true if the page contains no results. +func (p RoleAssignmentsPage) IsEmpty() (bool, error) { + roleAssignments, err := ExtractRoleAssignments(p) + if err != nil { + return true, err + } + return len(roleAssignments) == 0, nil +} + +// NextPageURL uses the response's embedded link reference to navigate to the next page of results. +func (page RoleAssignmentsPage) NextPageURL() (string, error) { + type resp struct { + Links struct { + Next string `mapstructure:"next"` + } `mapstructure:"links"` + } + + var r resp + err := mapstructure.Decode(page.Body, &r) + if err != nil { + return "", err + } + + return r.Links.Next, nil +} + +// ExtractRoleAssignments extracts a slice of RoleAssignments from a Collection acquired from List. +func ExtractRoleAssignments(page pagination.Page) ([]RoleAssignment, error) { + var response struct { + RoleAssignments []RoleAssignment `mapstructure:"role_assignments"` + } + + err := mapstructure.Decode(page.(RoleAssignmentsPage).Body, &response) + return response.RoleAssignments, err +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/roles/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/roles/urls.go new file mode 100644 index 00000000000..b009340d096 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/roles/urls.go @@ -0,0 +1,7 @@ +package roles + +import "github.com/rackspace/gophercloud" + +func listAssignmentsURL(client *gophercloud.ServiceClient) string { + return client.ServiceURL("role_assignments") +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/roles/urls_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/roles/urls_test.go new file mode 100644 index 00000000000..04679dae403 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/roles/urls_test.go @@ -0,0 +1,15 @@ +package roles + +import ( + "testing" + + "github.com/rackspace/gophercloud" +) + +func TestListAssignmentsURL(t *testing.T) { + client := gophercloud.ServiceClient{Endpoint: "http://localhost:5000/v3/"} + url := listAssignmentsURL(&client) + if url != "http://localhost:5000/v3/role_assignments" { + t.Errorf("Unexpected list URL generated: [%s]", url) + } +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/fwaas/firewalls/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/fwaas/firewalls/requests_test.go index f24e2835ea9..19d32c5ac30 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/fwaas/firewalls/requests_test.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/fwaas/firewalls/requests_test.go @@ -212,9 +212,9 @@ func TestUpdate(t *testing.T) { "name": "fw", "admin_state_up": false, "tenant_id": "b4eedccc6fb74fa8a7ad6b08382b852b", - "firewall_policy_id": "19ab8c87-4a32-4e6a-a74e-b77fffb89a0c" + "firewall_policy_id": "19ab8c87-4a32-4e6a-a74e-b77fffb89a0c", "id": "ea5b5315-64f6-4ea3-8e58-981cc37c6576", - "description": "OpenStack firewall", + "description": "OpenStack firewall" } } `) diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/pools/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/pools/requests_test.go index 6da29a6b8e6..3b5c7c7105b 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/pools/requests_test.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/pools/requests_test.go @@ -296,6 +296,7 @@ func TestAssociateHealthMonitor(t *testing.T) { w.Header().Add("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) + fmt.Fprintf(w, `{}`) }) _, err := AssociateMonitor(fake.ServiceClient(), "332abe93-f488-41ba-870b-2ac66be7f853", "b624decf-d5d3-4c66-9a3d-f047e7786181").Extract() diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/groups/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/groups/requests.go index 55e4b3b8043..2712ac1621f 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/groups/requests.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/groups/requests.go @@ -45,6 +45,9 @@ type CreateOpts struct { // Required. Human-readable name for the VIP. Does not have to be unique. Name string + // Required for admins. Indicates the owner of the VIP. + TenantID string + // Optional. Describes the security group. Description string } @@ -62,6 +65,7 @@ func Create(c *gophercloud.ServiceClient, opts CreateOpts) CreateResult { type secgroup struct { Name string `json:"name"` + TenantID string `json:"tenant_id,omitempty"` Description string `json:"description,omitempty"` } @@ -71,6 +75,7 @@ func Create(c *gophercloud.ServiceClient, opts CreateOpts) CreateResult { reqBody := request{SecGroup: secgroup{ Name: opts.Name, + TenantID: opts.TenantID, Description: opts.Description, }} @@ -91,3 +96,36 @@ func Delete(c *gophercloud.ServiceClient, id string) DeleteResult { _, res.Err = c.Delete(resourceURL(c, id), nil) return res } + +// IDFromName is a convenience function that returns a security group's ID given its name. +func IDFromName(client *gophercloud.ServiceClient, name string) (string, error) { + securityGroupCount := 0 + securityGroupID := "" + if name == "" { + return "", fmt.Errorf("A security group name must be provided.") + } + pager := List(client, ListOpts{}) + pager.EachPage(func(page pagination.Page) (bool, error) { + securityGroupList, err := ExtractGroups(page) + if err != nil { + return false, err + } + + for _, s := range securityGroupList { + if s.Name == name { + securityGroupCount++ + securityGroupID = s.ID + } + } + return true, nil + }) + + switch securityGroupCount { + case 0: + return "", fmt.Errorf("Unable to find security group: %s", name) + case 1: + return securityGroupID, nil + default: + return "", fmt.Errorf("Found %d security groups matching %s", securityGroupCount, name) + } +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/rules/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/rules/requests.go index 0b2d10b0efe..a80ceb30999 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/rules/requests.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/rules/requests.go @@ -99,6 +99,9 @@ type CreateOpts struct { // attribute matches the specified IP prefix as the source IP address of the // IP packet. RemoteIPPrefix string + + // Required for admins. Indicates the owner of the VIP. + TenantID string } // Create is an operation which provisions a new security group with default @@ -133,6 +136,7 @@ func Create(c *gophercloud.ServiceClient, opts CreateOpts) CreateResult { Protocol string `json:"protocol,omitempty"` RemoteGroupID string `json:"remote_group_id,omitempty"` RemoteIPPrefix string `json:"remote_ip_prefix,omitempty"` + TenantID string `json:"tenant_id,omitempty"` } type request struct { @@ -148,6 +152,7 @@ func Create(c *gophercloud.ServiceClient, opts CreateOpts) CreateResult { Protocol: opts.Protocol, RemoteGroupID: opts.RemoteGroupID, RemoteIPPrefix: opts.RemoteIPPrefix, + TenantID: opts.TenantID, }} _, res.Err = c.Post(rootURL(c), reqBody, &res.Body, nil) diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/networks/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/networks/requests.go index 7be32274006..25ab7a8b645 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/networks/requests.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/networks/requests.go @@ -1,6 +1,8 @@ package networks import ( + "fmt" + "github.com/rackspace/gophercloud" "github.com/rackspace/gophercloud/pagination" ) @@ -189,3 +191,36 @@ func Delete(c *gophercloud.ServiceClient, networkID string) DeleteResult { _, res.Err = c.Delete(deleteURL(c, networkID), nil) return res } + +// IDFromName is a convenience function that returns a network's ID given its name. +func IDFromName(client *gophercloud.ServiceClient, name string) (string, error) { + networkCount := 0 + networkID := "" + if name == "" { + return "", fmt.Errorf("A network name must be provided.") + } + pager := List(client, nil) + pager.EachPage(func(page pagination.Page) (bool, error) { + networkList, err := ExtractNetworks(page) + if err != nil { + return false, err + } + + for _, n := range networkList { + if n.Name == name { + networkCount++ + networkID = n.ID + } + } + return true, nil + }) + + switch networkCount { + case 0: + return "", fmt.Errorf("Unable to find network: %s", name) + case 1: + return networkID, nil + default: + return "", fmt.Errorf("Found %d networks matching %s", networkCount, name) + } +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/networks/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/networks/requests_test.go index a263b7b16b2..81eb79cb541 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/networks/requests_test.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/networks/requests_test.go @@ -204,6 +204,7 @@ func TestCreateWithOptionalFields(t *testing.T) { `) w.WriteHeader(http.StatusCreated) + fmt.Fprintf(w, `{}`) }) iTrue := true diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/ports/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/ports/requests.go index 781a3c3e745..2caf1cace44 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/ports/requests.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/ports/requests.go @@ -1,6 +1,8 @@ package ports import ( + "fmt" + "github.com/rackspace/gophercloud" "github.com/rackspace/gophercloud/pagination" ) @@ -223,3 +225,36 @@ func Delete(c *gophercloud.ServiceClient, id string) DeleteResult { _, res.Err = c.Delete(deleteURL(c, id), nil) return res } + +// IDFromName is a convenience function that returns a port's ID given its name. +func IDFromName(client *gophercloud.ServiceClient, name string) (string, error) { + portCount := 0 + portID := "" + if name == "" { + return "", fmt.Errorf("A port name must be provided.") + } + pager := List(client, nil) + pager.EachPage(func(page pagination.Page) (bool, error) { + portList, err := ExtractPorts(page) + if err != nil { + return false, err + } + + for _, p := range portList { + if p.Name == name { + portCount++ + portID = p.ID + } + } + return true, nil + }) + + switch portCount { + case 0: + return "", fmt.Errorf("Unable to find port: %s", name) + case 1: + return portID, nil + default: + return "", fmt.Errorf("Found %d ports matching %s", portCount, name) + } +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/subnets/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/subnets/requests.go index 6e01f059d75..6cde048ed04 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/subnets/requests.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/subnets/requests.go @@ -1,6 +1,8 @@ package subnets import ( + "fmt" + "github.com/rackspace/gophercloud" "github.com/rackspace/gophercloud/pagination" ) @@ -200,10 +202,10 @@ func (opts UpdateOpts) ToSubnetUpdateMap() (map[string]interface{}, error) { if opts.GatewayIP != "" { s["gateway_ip"] = opts.GatewayIP } - if len(opts.DNSNameservers) != 0 { + if opts.DNSNameservers != nil { s["dns_nameservers"] = opts.DNSNameservers } - if len(opts.HostRoutes) != 0 { + if opts.HostRoutes != nil { s["host_routes"] = opts.HostRoutes } @@ -234,3 +236,36 @@ func Delete(c *gophercloud.ServiceClient, id string) DeleteResult { _, res.Err = c.Delete(deleteURL(c, id), nil) return res } + +// IDFromName is a convenience function that returns a subnet's ID given its name. +func IDFromName(client *gophercloud.ServiceClient, name string) (string, error) { + subnetCount := 0 + subnetID := "" + if name == "" { + return "", fmt.Errorf("A subnet name must be provided.") + } + pager := List(client, nil) + pager.EachPage(func(page pagination.Page) (bool, error) { + subnetList, err := ExtractSubnets(page) + if err != nil { + return false, err + } + + for _, s := range subnetList { + if s.Name == name { + subnetCount++ + subnetID = s.ID + } + } + return true, nil + }) + + switch subnetCount { + case 0: + return "", fmt.Errorf("Unable to find subnet: %s", name) + case 1: + return subnetID, nil + default: + return "", fmt.Errorf("Found %d subnets matching %s", subnetCount, name) + } +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/subnets/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/subnets/results.go index 1910f17dd96..77b956a17ef 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/subnets/results.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/subnets/results.go @@ -55,8 +55,8 @@ type AllocationPool struct { // HostRoute represents a route that should be used by devices with IPs from // a subnet (not including local subnet route). type HostRoute struct { - DestinationCIDR string `json:"destination"` - NextHop string `json:"nexthop"` + DestinationCIDR string `mapstructure:"destination" json:"destination"` + NextHop string `mapstructure:"nexthop" json:"nexthop"` } // Subnet represents a subnet. See package documentation for a top-level diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/subnets/results_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/subnets/results_test.go new file mode 100644 index 00000000000..d4048388cdb --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/subnets/results_test.go @@ -0,0 +1,54 @@ +package subnets + +import ( + "encoding/json" + "github.com/rackspace/gophercloud" + th "github.com/rackspace/gophercloud/testhelper" + "testing" +) + +func TestHostRoute(t *testing.T) { + sejson := []byte(` + {"subnet": { + "name": "test-subnet", + "enable_dhcp": false, + "network_id": "3e66c41e-cbbd-4019-9aab-740b7e4150a0", + "tenant_id": "f86e123198cf42d19c8854c5f80c2f06", + "dns_nameservers": [], + "gateway_ip": "172.16.0.1", + "ipv6_ra_mode": null, + "allocation_pools": [ + { + "start": "172.16.0.2", + "end": "172.16.255.254" + } + ], + "host_routes": [ + { + "destination": "172.20.1.0/24", + "nexthop": "172.16.0.2" + } + ], + "ip_version": 4, + "ipv6_address_mode": null, + "cidr": "172.16.0.0/16", + "id": "6dcaa873-7115-41af-9ef5-915f73636e43", + "subnetpool_id": null + }} +`) + + var dejson interface{} + err := json.Unmarshal(sejson, &dejson) + if err != nil { + t.Fatalf("%s", err) + } + + resp := commonResult{gophercloud.Result{Body: dejson}} + subnet, err := resp.Extract() + if err != nil { + t.Fatalf("%s", err) + } + route := subnet.HostRoutes[0] + th.AssertEquals(t, route.NextHop, "172.16.0.2") + th.AssertEquals(t, route.DestinationCIDR, "172.20.1.0/24") +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/accounts/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/accounts/requests.go index a6451157050..66c46a9787b 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/accounts/requests.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/accounts/requests.go @@ -43,7 +43,9 @@ func Get(c *gophercloud.ServiceClient, opts GetOptsBuilder) GetResult { MoreHeaders: h, OkCodes: []int{204}, }) - res.Header = resp.Header + if resp != nil { + res.Header = resp.Header + } res.Err = err return res } @@ -97,7 +99,9 @@ func Update(c *gophercloud.ServiceClient, opts UpdateOptsBuilder) UpdateResult { MoreHeaders: h, OkCodes: []int{201, 202, 204}, }) - res.Header = resp.Header + if resp != nil { + res.Header = resp.Header + } res.Err = err return res } diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers/requests.go index bbf8cdb952c..50ff9f48f3b 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers/requests.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers/requests.go @@ -114,7 +114,9 @@ func Create(c *gophercloud.ServiceClient, containerName string, opts CreateOptsB MoreHeaders: h, OkCodes: []int{201, 202, 204}, }) - res.Header = resp.Header + if resp != nil { + res.Header = resp.Header + } res.Err = err return res } @@ -180,7 +182,9 @@ func Update(c *gophercloud.ServiceClient, containerName string, opts UpdateOptsB MoreHeaders: h, OkCodes: []int{201, 202, 204}, }) - res.Header = resp.Header + if resp != nil { + res.Header = resp.Header + } res.Err = err return res } @@ -193,7 +197,9 @@ func Get(c *gophercloud.ServiceClient, containerName string) GetResult { resp, err := c.Request("HEAD", getURL(c, containerName), gophercloud.RequestOpts{ OkCodes: []int{200, 204}, }) - res.Header = resp.Header + if resp != nil { + res.Header = resp.Header + } res.Err = err return res } diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects/fixtures.go index ec616371a3c..7a6e6e1ee77 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects/fixtures.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects/fixtures.go @@ -3,7 +3,9 @@ package objects import ( + "crypto/md5" "fmt" + "io" "net/http" "testing" @@ -107,12 +109,18 @@ func HandleListObjectNamesSuccessfully(t *testing.T) { // HandleCreateTextObjectSuccessfully creates an HTTP handler at `/testContainer/testObject` on the test handler mux // that responds with a `Create` response. A Content-Type of "text/plain" is expected. -func HandleCreateTextObjectSuccessfully(t *testing.T) { +func HandleCreateTextObjectSuccessfully(t *testing.T, content string) { th.Mux.HandleFunc("/testContainer/testObject", func(w http.ResponseWriter, r *http.Request) { th.TestMethod(t, r, "PUT") th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) th.TestHeader(t, r, "Content-Type", "text/plain") th.TestHeader(t, r, "Accept", "application/json") + + hash := md5.New() + io.WriteString(hash, content) + localChecksum := hash.Sum(nil) + + w.Header().Set("ETag", fmt.Sprintf("%x", localChecksum)) w.WriteHeader(http.StatusCreated) }) } @@ -120,7 +128,7 @@ func HandleCreateTextObjectSuccessfully(t *testing.T) { // HandleCreateTypelessObjectSuccessfully creates an HTTP handler at `/testContainer/testObject` on the test handler // mux that responds with a `Create` response. No Content-Type header may be present in the request, so that server- // side content-type detection will be triggered properly. -func HandleCreateTypelessObjectSuccessfully(t *testing.T) { +func HandleCreateTypelessObjectSuccessfully(t *testing.T, content string) { th.Mux.HandleFunc("/testContainer/testObject", func(w http.ResponseWriter, r *http.Request) { th.TestMethod(t, r, "PUT") th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) @@ -130,6 +138,11 @@ func HandleCreateTypelessObjectSuccessfully(t *testing.T) { t.Errorf("Expected Content-Type header to be omitted, but was %#v", contentType) } + hash := md5.New() + io.WriteString(hash, content) + localChecksum := hash.Sum(nil) + + w.Header().Set("ETag", fmt.Sprintf("%x", localChecksum)) w.WriteHeader(http.StatusCreated) }) } diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects/requests.go index 7eedde25509..c2fbaae2723 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects/requests.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects/requests.go @@ -1,7 +1,9 @@ package objects import ( + "bytes" "crypto/hmac" + "crypto/md5" "crypto/sha1" "fmt" "io" @@ -134,10 +136,11 @@ func Download(c *gophercloud.ServiceClient, containerName, objectName string, op MoreHeaders: h, OkCodes: []int{200, 304}, }) - - res.Body = resp.Body + if resp != nil { + res.Header = resp.Header + res.Body = resp.Body + } res.Err = err - res.Header = resp.Header return res } @@ -187,8 +190,9 @@ func (opts CreateOpts) ToObjectCreateParams() (map[string]string, string, error) return h, q.String(), nil } -// Create is a function that creates a new object or replaces an existing object. -func Create(c *gophercloud.ServiceClient, containerName, objectName string, content io.Reader, opts CreateOptsBuilder) CreateResult { +// Create is a function that creates a new object or replaces an existing object. If the returned response's ETag +// header fails to match the local checksum, the failed request will automatically be retried up to a maximum of 3 times. +func Create(c *gophercloud.ServiceClient, containerName, objectName string, content io.ReadSeeker, opts CreateOptsBuilder) CreateResult { var res CreateResult url := createURL(c, containerName, objectName) @@ -208,14 +212,37 @@ func Create(c *gophercloud.ServiceClient, containerName, objectName string, cont url += query } + hash := md5.New() + + contentBuffer := bytes.NewBuffer([]byte{}) + _, err := io.Copy(contentBuffer, io.TeeReader(content, hash)) + if err != nil { + res.Err = err + return res + } + + localChecksum := hash.Sum(nil) + h["ETag"] = fmt.Sprintf("%x", localChecksum) + ropts := gophercloud.RequestOpts{ - RawBody: content, + RawBody: strings.NewReader(contentBuffer.String()), MoreHeaders: h, } resp, err := c.Request("PUT", url, ropts) - res.Header = resp.Header - res.Err = err + if err != nil { + res.Err = err + return res + } + if resp != nil { + res.Header = resp.Header + if resp.Header.Get("ETag") == fmt.Sprintf("%x", localChecksum) { + res.Err = err + return res + } + res.Err = fmt.Errorf("Local checksum does not match API ETag header") + } + return res } @@ -270,7 +297,9 @@ func Copy(c *gophercloud.ServiceClient, containerName, objectName string, opts C MoreHeaders: h, OkCodes: []int{201}, }) - res.Header = resp.Header + if resp != nil { + res.Header = resp.Header + } res.Err = err return res } @@ -310,7 +339,9 @@ func Delete(c *gophercloud.ServiceClient, containerName, objectName string, opts } resp, err := c.Delete(url, nil) - res.Header = resp.Header + if resp != nil { + res.Header = resp.Header + } res.Err = err return res } @@ -354,7 +385,9 @@ func Get(c *gophercloud.ServiceClient, containerName, objectName string, opts Ge resp, err := c.Request("HEAD", url, gophercloud.RequestOpts{ OkCodes: []int{200, 204}, }) - res.Header = resp.Header + if resp != nil { + res.Header = resp.Header + } res.Err = err return res } @@ -410,7 +443,9 @@ func Update(c *gophercloud.ServiceClient, containerName, objectName string, opts resp, err := c.Request("POST", url, gophercloud.RequestOpts{ MoreHeaders: h, }) - res.Header = resp.Header + if resp != nil { + res.Header = resp.Header + } res.Err = err return res } diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects/requests_test.go index 6be3534205f..f7d6822913e 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects/requests_test.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects/requests_test.go @@ -2,7 +2,10 @@ package objects import ( "bytes" + "fmt" "io" + "net/http" + "strings" "testing" "github.com/rackspace/gophercloud/pagination" @@ -83,24 +86,44 @@ func TestListObjectNames(t *testing.T) { func TestCreateObject(t *testing.T) { th.SetupHTTP() defer th.TeardownHTTP() - HandleCreateTextObjectSuccessfully(t) - content := bytes.NewBufferString("Did gyre and gimble in the wabe") + content := "Did gyre and gimble in the wabe" + + HandleCreateTextObjectSuccessfully(t, content) + options := &CreateOpts{ContentType: "text/plain"} - res := Create(fake.ServiceClient(), "testContainer", "testObject", content, options) + res := Create(fake.ServiceClient(), "testContainer", "testObject", strings.NewReader(content), options) th.AssertNoErr(t, res.Err) } func TestCreateObjectWithoutContentType(t *testing.T) { th.SetupHTTP() defer th.TeardownHTTP() - HandleCreateTypelessObjectSuccessfully(t) - content := bytes.NewBufferString("The sky was the color of television, tuned to a dead channel.") - res := Create(fake.ServiceClient(), "testContainer", "testObject", content, &CreateOpts{}) + content := "The sky was the color of television, tuned to a dead channel." + + HandleCreateTypelessObjectSuccessfully(t, content) + + res := Create(fake.ServiceClient(), "testContainer", "testObject", strings.NewReader(content), &CreateOpts{}) th.AssertNoErr(t, res.Err) } +func TestErrorIsRaisedForChecksumMismatch(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/testContainer/testObject", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("ETag", "acbd18db4cc2f85cedef654fccc4a4d8") + w.WriteHeader(http.StatusCreated) + }) + + content := strings.NewReader("The sky was the color of television, tuned to a dead channel.") + res := Create(fake.ServiceClient(), "testContainer", "testObject", content, &CreateOpts{}) + + err := fmt.Errorf("Local checksum does not match API ETag header") + th.AssertDeepEquals(t, err, res.Err) +} + func TestCopyObject(t *testing.T) { th.SetupHTTP() defer th.TeardownHTTP() diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/stackevents/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/stackevents/fixtures.go index 016ae003b32..235787a5145 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/stackevents/fixtures.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/stackevents/fixtures.go @@ -67,7 +67,7 @@ const FindOutput = ` "events": [ { "resource_name": "hello_world", - "event_time": "2015-02-05T21:33:11Z", + "event_time": "2015-02-05T21:33:11", "links": [ { "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world/events/06feb26f-9298-4a9b-8749-9d770e5d577a", @@ -90,7 +90,7 @@ const FindOutput = ` }, { "resource_name": "hello_world", - "event_time": "2015-02-05T21:33:27Z", + "event_time": "2015-02-05T21:33:27", "links": [ { "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world/events/93940999-7d40-44ae-8de4-19624e7b8d18", @@ -184,7 +184,7 @@ const ListOutput = ` "events": [ { "resource_name": "hello_world", - "event_time": "2015-02-05T21:33:11Z", + "event_time": "2015-02-05T21:33:11", "links": [ { "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world/events/06feb26f-9298-4a9b-8749-9d770e5d577a", @@ -207,7 +207,7 @@ const ListOutput = ` }, { "resource_name": "hello_world", - "event_time": "2015-02-05T21:33:27Z", + "event_time": "2015-02-05T21:33:27", "links": [ { "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world/events/93940999-7d40-44ae-8de4-19624e7b8d18", @@ -309,7 +309,7 @@ const ListResourceEventsOutput = ` "events": [ { "resource_name": "hello_world", - "event_time": "2015-02-05T21:33:11Z", + "event_time": "2015-02-05T21:33:11", "links": [ { "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world/events/06feb26f-9298-4a9b-8749-9d770e5d577a", @@ -332,7 +332,7 @@ const ListResourceEventsOutput = ` }, { "resource_name": "hello_world", - "event_time": "2015-02-05T21:33:27Z", + "event_time": "2015-02-05T21:33:27", "links": [ { "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world/events/93940999-7d40-44ae-8de4-19624e7b8d18", @@ -408,7 +408,7 @@ const GetOutput = ` { "event":{ "resource_name": "hello_world", - "event_time": "2015-02-05T21:33:27Z", + "event_time": "2015-02-05T21:33:27", "links": [ { "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world/events/93940999-7d40-44ae-8de4-19624e7b8d18", diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/stackevents/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/stackevents/results.go index 3c8f1da4910..cf9e240982d 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/stackevents/results.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/stackevents/results.go @@ -57,7 +57,7 @@ func (r FindResult) Extract() ([]Event, error) { for i, eventRaw := range events { event := eventRaw.(map[string]interface{}) if date, ok := event["event_time"]; ok && date != nil { - t, err := time.Parse(time.RFC3339, date.(string)) + t, err := time.Parse(gophercloud.STACK_TIME_FMT, date.(string)) if err != nil { return nil, err } @@ -121,7 +121,7 @@ func ExtractEvents(page pagination.Page) ([]Event, error) { for i, eventRaw := range events { event := eventRaw.(map[string]interface{}) if date, ok := event["event_time"]; ok && date != nil { - t, err := time.Parse(time.RFC3339, date.(string)) + t, err := time.Parse(gophercloud.STACK_TIME_FMT, date.(string)) if err != nil { return nil, err } @@ -161,7 +161,7 @@ func (r GetResult) Extract() (*Event, error) { event := r.Body.(map[string]interface{})["event"].(map[string]interface{}) if date, ok := event["event_time"]; ok && date != nil { - t, err := time.Parse(time.RFC3339, date.(string)) + t, err := time.Parse(gophercloud.STACK_TIME_FMT, date.(string)) if err != nil { return nil, err } diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/stackresources/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/stackresources/fixtures.go index 0b930f4841a..c3c3d3fbc96 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/stackresources/fixtures.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/stackresources/fixtures.go @@ -53,7 +53,7 @@ const FindOutput = ` ], "logical_resource_id": "hello_world", "resource_status_reason": "state changed", - "updated_time": "2015-02-05T21:33:11Z", + "updated_time": "2015-02-05T21:33:11", "required_by": [], "resource_status": "CREATE_IN_PROGRESS", "physical_resource_id": "49181cd6-169a-4130-9455-31185bbfc5bf", @@ -117,7 +117,7 @@ const ListOutput = `{ ], "logical_resource_id": "hello_world", "resource_status_reason": "state changed", - "updated_time": "2015-02-05T21:33:11Z", + "updated_time": "2015-02-05T21:33:11", "required_by": [], "resource_status": "CREATE_IN_PROGRESS", "physical_resource_id": "49181cd6-169a-4130-9455-31185bbfc5bf", @@ -188,7 +188,7 @@ const GetOutput = ` ], "logical_resource_id": "wordpress_instance", "resource_status": "CREATE_COMPLETE", - "updated_time": "2014-12-10T18:34:35Z", + "updated_time": "2014-12-10T18:34:35", "required_by": [], "resource_status_reason": "state changed", "physical_resource_id": "00e3a2fe-c65d-403c-9483-4db9930dd194", diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/stackresources/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/stackresources/requests.go index ee9c3c250cc..fcb8d8a2f82 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/stackresources/requests.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/stackresources/requests.go @@ -25,12 +25,6 @@ type ListOptsBuilder interface { // ListOpts allows the filtering and sorting of paginated collections through // the API. Marker and Limit are used for pagination. type ListOpts struct { - // The stack resource ID with which to start the listing. - Marker string `q:"marker"` - - // Integer value for the limit of values to return. - Limit int `q:"limit"` - // Include resources from nest stacks up to Depth levels of recursion. Depth int `q:"nested_depth"` } @@ -57,9 +51,7 @@ func List(client *gophercloud.ServiceClient, stackName, stackID string, opts Lis } createPageFn := func(r pagination.PageResult) pagination.Page { - p := ResourcePage{pagination.MarkerPageBase{PageResult: r}} - p.MarkerPageBase.Owner = p - return p + return ResourcePage{pagination.SinglePageBase(r)} } return pagination.NewPager(client, url, createPageFn) diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/stackresources/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/stackresources/results.go index 69f21daef39..df79d582a96 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/stackresources/results.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/stackresources/results.go @@ -48,7 +48,7 @@ func (r FindResult) Extract() ([]Resource, error) { for i, resourceRaw := range resources { resource := resourceRaw.(map[string]interface{}) if date, ok := resource["updated_time"]; ok && date != nil { - t, err := time.Parse(time.RFC3339, date.(string)) + t, err := time.Parse(gophercloud.STACK_TIME_FMT, date.(string)) if err != nil { return nil, err } @@ -63,7 +63,7 @@ func (r FindResult) Extract() ([]Resource, error) { // As OpenStack extensions may freely alter the response bodies of structures returned to the client, you may only safely access the // data provided through the ExtractResources call. type ResourcePage struct { - pagination.MarkerPageBase + pagination.SinglePageBase } // IsEmpty returns true if a page contains no Server results. @@ -109,7 +109,7 @@ func ExtractResources(page pagination.Page) ([]Resource, error) { for i, resourceRaw := range resources { resource := resourceRaw.(map[string]interface{}) if date, ok := resource["updated_time"]; ok && date != nil { - t, err := time.Parse(time.RFC3339, date.(string)) + t, err := time.Parse(gophercloud.STACK_TIME_FMT, date.(string)) if err != nil { return nil, err } @@ -143,7 +143,7 @@ func (r GetResult) Extract() (*Resource, error) { resource := r.Body.(map[string]interface{})["resource"].(map[string]interface{}) if date, ok := resource["updated_time"]; ok && date != nil { - t, err := time.Parse(time.RFC3339, date.(string)) + t, err := time.Parse(gophercloud.STACK_TIME_FMT, date.(string)) if err != nil { return nil, err } diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/stacks/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/stacks/fixtures.go index 6d3e9597a21..3a621dab6c1 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/stacks/fixtures.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/stacks/fixtures.go @@ -95,7 +95,7 @@ const FullListOutput = ` ], "stack_status_reason": "Stack CREATE completed successfully", "stack_name": "postman_stack", - "creation_time": "2015-02-03T20:07:39Z", + "creation_time": "2015-02-03T20:07:39", "updated_time": null, "stack_status": "CREATE_COMPLETE", "id": "16ef0584-4458-41eb-87c8-0dc8d5f66c87" @@ -110,8 +110,8 @@ const FullListOutput = ` ], "stack_status_reason": "Stack successfully updated", "stack_name": "gophercloud-test-stack-2", - "creation_time": "2014-12-11T17:39:16Z", - "updated_time": "2014-12-11T17:40:37Z", + "creation_time": "2014-12-11T17:39:16", + "updated_time": "2014-12-11T17:40:37", "stack_status": "UPDATE_COMPLETE", "id": "db6977b2-27aa-4775-9ae7-6213212d4ada" } @@ -181,7 +181,7 @@ const GetOutput = ` "stack_status_reason": "Stack CREATE completed successfully", "stack_name": "postman_stack", "outputs": [], - "creation_time": "2015-02-03T20:07:39Z", + "creation_time": "2015-02-03T20:07:39", "links": [ { "href": "http://166.76.160.117:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/16ef0584-4458-41eb-87c8-0dc8d5f66c87", diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/stacks/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/stacks/results.go index 04d3f8ea964..dca06e4e0d0 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/stacks/results.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/stacks/results.go @@ -100,7 +100,7 @@ func ExtractStacks(page pagination.Page) ([]ListedStack, error) { thisStack := (rawStacks[i]).(map[string]interface{}) if t, ok := thisStack["creation_time"].(string); ok && t != "" { - creationTime, err := time.Parse(time.RFC3339, t) + creationTime, err := time.Parse(gophercloud.STACK_TIME_FMT, t) if err != nil { return res.Stacks, err } @@ -108,7 +108,7 @@ func ExtractStacks(page pagination.Page) ([]ListedStack, error) { } if t, ok := thisStack["updated_time"].(string); ok && t != "" { - updatedTime, err := time.Parse(time.RFC3339, t) + updatedTime, err := time.Parse(gophercloud.STACK_TIME_FMT, t) if err != nil { return res.Stacks, err } @@ -170,7 +170,7 @@ func (r GetResult) Extract() (*RetrievedStack, error) { b := r.Body.(map[string]interface{})["stack"].(map[string]interface{}) if date, ok := b["creation_time"]; ok && date != nil { - t, err := time.Parse(time.RFC3339, date.(string)) + t, err := time.Parse(gophercloud.STACK_TIME_FMT, date.(string)) if err != nil { return nil, err } @@ -178,7 +178,7 @@ func (r GetResult) Extract() (*RetrievedStack, error) { } if date, ok := b["updated_time"]; ok && date != nil { - t, err := time.Parse(time.RFC3339, date.(string)) + t, err := time.Parse(gophercloud.STACK_TIME_FMT, date.(string)) if err != nil { return nil, err } @@ -249,7 +249,7 @@ func (r PreviewResult) Extract() (*PreviewedStack, error) { b := r.Body.(map[string]interface{})["stack"].(map[string]interface{}) if date, ok := b["creation_time"]; ok && date != nil { - t, err := time.Parse(time.RFC3339, date.(string)) + t, err := time.Parse(gophercloud.STACK_TIME_FMT, date.(string)) if err != nil { return nil, err } @@ -257,7 +257,7 @@ func (r PreviewResult) Extract() (*PreviewedStack, error) { } if date, ok := b["updated_time"]; ok && date != nil { - t, err := time.Parse(time.RFC3339, date.(string)) + t, err := time.Parse(gophercloud.STACK_TIME_FMT, date.(string)) if err != nil { return nil, err } diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/pagination/http.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/pagination/http.go index cabcccd79f3..1b3fe94ab9f 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/pagination/http.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/pagination/http.go @@ -36,13 +36,19 @@ func PageResultFrom(resp *http.Response) (PageResult, error) { parsedBody = rawBody } + return PageResultFromParsed(resp, parsedBody), err +} + +// PageResultFromParsed constructs a PageResult from an HTTP response that has already had its +// body parsed as JSON (and closed). +func PageResultFromParsed(resp *http.Response, body interface{}) PageResult { return PageResult{ Result: gophercloud.Result{ - Body: parsedBody, + Body: body, Header: resp.Header, }, URL: *resp.Request.URL, - }, err + } } // Request performs an HTTP request and extracts the http.Response from the result. diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/pagination/pager.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/pagination/pager.go index ea47c695dc3..a7593ac883d 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/pagination/pager.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/pagination/pager.go @@ -174,8 +174,10 @@ func (p Pager) AllPages() (Page, error) { if err != nil { return nil, err } - // Remove the trailing comma. - pagesSlice = pagesSlice[:len(pagesSlice)-1] + if len(pagesSlice) > 0 { + // Remove the trailing comma. + pagesSlice = pagesSlice[:len(pagesSlice)-1] + } var b []byte // Combine the slice of slices in to a single slice. for _, slice := range pagesSlice { diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/provider_client.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/provider_client.go index 0dff2cfc303..d920913e76c 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/provider_client.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/provider_client.go @@ -85,9 +85,9 @@ type RequestOpts struct { // content type of the request will default to "application/json" unless overridden by MoreHeaders. // It's an error to specify both a JSONBody and a RawBody. JSONBody interface{} - // RawBody contains an io.Reader that will be consumed by the request directly. No content-type + // RawBody contains an io.ReadSeeker that will be consumed by the request directly. No content-type // will be set unless one is provided explicitly by MoreHeaders. - RawBody io.Reader + RawBody io.ReadSeeker // JSONResponse, if provided, will be populated with the contents of the response body parsed as // JSON. @@ -124,11 +124,11 @@ var applicationJSON = "application/json" // Request performs an HTTP request using the ProviderClient's current HTTPClient. An authentication // header will automatically be provided. func (client *ProviderClient) Request(method, url string, options RequestOpts) (*http.Response, error) { - var body io.Reader + var body io.ReadSeeker var contentType *string // Derive the content body by either encoding an arbitrary object as JSON, or by taking a provided - // io.Reader as-is. Default the content-type to application/json. + // io.ReadSeeker as-is. Default the content-type to application/json. if options.JSONBody != nil { if options.RawBody != nil { panic("Please provide only one of JSONBody or RawBody to gophercloud.Request().") @@ -189,6 +189,9 @@ func (client *ProviderClient) Request(method, url string, options RequestOpts) ( if err != nil { return nil, fmt.Errorf("Error trying to re-authenticate: %s", err) } + if options.RawBody != nil { + options.RawBody.Seek(0, 0) + } resp, err = client.Request(method, url, options) if err != nil { return nil, fmt.Errorf("Successfully re-authenticated, but got error executing request: %s", err) @@ -224,7 +227,9 @@ func (client *ProviderClient) Request(method, url string, options RequestOpts) ( // Parse the response body as JSON, if requested to do so. if options.JSONResponse != nil { defer resp.Body.Close() - json.NewDecoder(resp.Body).Decode(options.JSONResponse) + if err := json.NewDecoder(resp.Body).Decode(options.JSONResponse); err != nil { + return nil, err + } } return resp, nil @@ -260,7 +265,7 @@ func (client *ProviderClient) Post(url string, JSONBody interface{}, JSONRespons opts = &RequestOpts{} } - if v, ok := (JSONBody).(io.Reader); ok { + if v, ok := (JSONBody).(io.ReadSeeker); ok { opts.RawBody = v } else if JSONBody != nil { opts.JSONBody = JSONBody @@ -278,7 +283,7 @@ func (client *ProviderClient) Put(url string, JSONBody interface{}, JSONResponse opts = &RequestOpts{} } - if v, ok := (JSONBody).(io.Reader); ok { + if v, ok := (JSONBody).(io.ReadSeeker); ok { opts.RawBody = v } else if JSONBody != nil { opts.JSONBody = JSONBody diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/volumes/delegate_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/volumes/delegate_test.go index b44564cc1f6..b6831f21148 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/volumes/delegate_test.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/volumes/delegate_test.go @@ -3,7 +3,8 @@ package volumes import ( "testing" - os "github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes" + "github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes" + os "github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/testing" "github.com/rackspace/gophercloud/pagination" th "github.com/rackspace/gophercloud/testhelper" fake "github.com/rackspace/gophercloud/testhelper/client" @@ -64,7 +65,7 @@ func TestCreate(t *testing.T) { os.MockCreateResponse(t) - n, err := Create(fake.ServiceClient(), CreateOpts{os.CreateOpts{Size: 75}}).Extract() + n, err := Create(fake.ServiceClient(), CreateOpts{volumes.CreateOpts{Size: 75}}).Extract() th.AssertNoErr(t, err) th.AssertEquals(t, n.Size, 4) @@ -72,12 +73,12 @@ func TestCreate(t *testing.T) { } func TestSizeRange(t *testing.T) { - _, err := Create(fake.ServiceClient(), CreateOpts{os.CreateOpts{Size: 1}}).Extract() + _, err := Create(fake.ServiceClient(), CreateOpts{volumes.CreateOpts{Size: 1}}).Extract() if err == nil { t.Fatalf("Expected error, got none") } - _, err = Create(fake.ServiceClient(), CreateOpts{os.CreateOpts{Size: 2000}}).Extract() + _, err = Create(fake.ServiceClient(), CreateOpts{volumes.CreateOpts{Size: 2000}}).Extract() if err == nil { t.Fatalf("Expected error, got none") } diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/bootfromvolume/delegate_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/bootfromvolume/delegate_test.go index 0b5352751b4..571a1bed2b7 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/bootfromvolume/delegate_test.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/bootfromvolume/delegate_test.go @@ -33,6 +33,8 @@ func TestCreateOpts(t *testing.T) { "name": "createdserver", "imageRef": "asdfasdfasdf", "flavorRef": "performance1-1", + "flavorName": "", + "imageName": "", "block_device_mapping_v2":[ { "uuid":"123456", diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/flavors/delegate.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/flavors/delegate.go index 6bfc20c5644..081ea478cd8 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/flavors/delegate.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/flavors/delegate.go @@ -36,11 +36,8 @@ func ListDetail(client *gophercloud.ServiceClient, opts os.ListOptsBuilder) pagi } // Get returns details about a single flavor, identity by ID. -func Get(client *gophercloud.ServiceClient, id string) os.GetResult { - return os.Get(client, id) -} - -// ExtractFlavors interprets a page of List results as Flavors. -func ExtractFlavors(page pagination.Page) ([]os.Flavor, error) { - return os.ExtractFlavors(page) +func Get(client *gophercloud.ServiceClient, id string) GetResult { + var res GetResult + _, res.Err = client.Get(getURL(client, id), &res.Body, nil) + return res } diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/flavors/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/flavors/fixtures.go index 894f916f675..957dccfd7dd 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/flavors/fixtures.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/flavors/fixtures.go @@ -2,10 +2,6 @@ package flavors -import ( - os "github.com/rackspace/gophercloud/openstack/compute/v2/flavors" -) - // ListOutput is a sample response of a flavor List request. const ListOutput = ` { @@ -103,7 +99,7 @@ const GetOutput = ` // Performance1Flavor is the expected result of parsing GetOutput, or the first element of // ListOutput. -var Performance1Flavor = os.Flavor{ +var Performance1Flavor = Flavor{ ID: "performance1-1", Disk: 20, RAM: 1024, @@ -111,10 +107,16 @@ var Performance1Flavor = os.Flavor{ RxTxFactor: 200.0, Swap: 0, VCPUs: 1, + ExtraSpecs: ExtraSpecs{ + NumDataDisks: 0, + Class: "performance1", + DiskIOIndex: 0, + PolicyClass: "performance_flavor", + }, } // Performance2Flavor is the second result expected from parsing ListOutput. -var Performance2Flavor = os.Flavor{ +var Performance2Flavor = Flavor{ ID: "performance1-2", Disk: 40, RAM: 2048, @@ -122,8 +124,14 @@ var Performance2Flavor = os.Flavor{ RxTxFactor: 400.0, Swap: 0, VCPUs: 2, + ExtraSpecs: ExtraSpecs{ + NumDataDisks: 0, + Class: "performance1", + DiskIOIndex: 0, + PolicyClass: "performance_flavor", + }, } // ExpectedFlavorSlice is the slice of Flavor structs that are expected to be parsed from // ListOutput. -var ExpectedFlavorSlice = []os.Flavor{Performance1Flavor, Performance2Flavor} +var ExpectedFlavorSlice = []Flavor{Performance1Flavor, Performance2Flavor} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/flavors/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/flavors/results.go new file mode 100644 index 00000000000..af444a766d3 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/flavors/results.go @@ -0,0 +1,104 @@ +package flavors + +import ( + "reflect" + + "github.com/rackspace/gophercloud" + "github.com/mitchellh/mapstructure" + os "github.com/rackspace/gophercloud/openstack/compute/v2/flavors" + "github.com/rackspace/gophercloud/pagination" +) + +// ExtraSpecs provide additional information about the flavor. +type ExtraSpecs struct { + // The number of data disks + NumDataDisks int `mapstructure:"number_of_data_disks"` + // The flavor class + Class string `mapstructure:"class"` + // Relative measure of disk I/O performance from 0-99, where higher is faster + DiskIOIndex int `mapstructure:"disk_io_index"` + PolicyClass string `mapstructure:"policy_class"` +} + +// Flavor records represent (virtual) hardware configurations for server resources in a region. +type Flavor struct { + // The Id field contains the flavor's unique identifier. + // For example, this identifier will be useful when specifying which hardware configuration to use for a new server instance. + ID string `mapstructure:"id"` + + // The Disk and RA< fields provide a measure of storage space offered by the flavor, in GB and MB, respectively. + Disk int `mapstructure:"disk"` + RAM int `mapstructure:"ram"` + + // The Name field provides a human-readable moniker for the flavor. + Name string `mapstructure:"name"` + + RxTxFactor float64 `mapstructure:"rxtx_factor"` + + // Swap indicates how much space is reserved for swap. + // If not provided, this field will be set to 0. + Swap int `mapstructure:"swap"` + + // VCPUs indicates how many (virtual) CPUs are available for this flavor. + VCPUs int `mapstructure:"vcpus"` + + // ExtraSpecs provides extra information about the flavor + ExtraSpecs ExtraSpecs `mapstructure:"OS-FLV-WITH-EXT-SPECS:extra_specs"` +} + +// GetResult temporarily holds the response from a Get call. +type GetResult struct { + gophercloud.Result +} + +// Extract provides access to the individual Flavor returned by the Get function. +func (gr GetResult) Extract() (*Flavor, error) { + if gr.Err != nil { + return nil, gr.Err + } + + var result struct { + Flavor Flavor `mapstructure:"flavor"` + } + + cfg := &mapstructure.DecoderConfig{ + DecodeHook: defaulter, + Result: &result, + } + decoder, err := mapstructure.NewDecoder(cfg) + if err != nil { + return nil, err + } + err = decoder.Decode(gr.Body) + return &result.Flavor, err +} + +func defaulter(from, to reflect.Kind, v interface{}) (interface{}, error) { + if (from == reflect.String) && (to == reflect.Int) { + return 0, nil + } + return v, nil +} + +// ExtractFlavors provides access to the list of flavors in a page acquired from the List operation. +func ExtractFlavors(page pagination.Page) ([]Flavor, error) { + casted := page.(os.FlavorPage).Body + var container struct { + Flavors []Flavor `mapstructure:"flavors"` + } + + cfg := &mapstructure.DecoderConfig{ + DecodeHook: defaulter, + Result: &container, + } + decoder, err := mapstructure.NewDecoder(cfg) + if err != nil { + return container.Flavors, err + } + err = decoder.Decode(casted) + if err != nil { + return container.Flavors, err + } + + return container.Flavors, nil +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/flavors/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/flavors/urls.go new file mode 100644 index 00000000000..f4e2c3dac42 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/flavors/urls.go @@ -0,0 +1,9 @@ +package flavors + +import ( + "github.com/rackspace/gophercloud" +) + +func getURL(client *gophercloud.ServiceClient, id string) string { + return client.ServiceURL("flavors", id) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/servers/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/servers/requests.go index 809183ec7c0..d4472a08088 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/servers/requests.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/servers/requests.go @@ -12,13 +12,24 @@ type CreateOpts struct { // Name [required] is the name to assign to the newly launched server. Name string - // ImageRef [required] is the ID or full URL to the image that contains the server's OS and initial state. - // Optional if using the boot-from-volume extension. + // ImageRef [optional; required if ImageName is not provided] is the ID or full + // URL to the image that contains the server's OS and initial state. + // Also optional if using the boot-from-volume extension. ImageRef string - // FlavorRef [required] is the ID or full URL to the flavor that describes the server's specs. + // ImageName [optional; required if ImageRef is not provided] is the name of the + // image that contains the server's OS and initial state. + // Also optional if using the boot-from-volume extension. + ImageName string + + // FlavorRef [optional; required if FlavorName is not provided] is the ID or + // full URL to the flavor that describes the server's specs. FlavorRef string + // FlavorName [optional; required if FlavorRef is not provided] is the name of + // the flavor that describes the server's specs. + FlavorName string + // SecurityGroups [optional] lists the names of the security groups to which this server should belong. SecurityGroups []string @@ -36,9 +47,9 @@ type CreateOpts struct { // Metadata [optional] contains key-value pairs (up to 255 bytes each) to attach to the server. Metadata map[string]string - // Personality [optional] includes the path and contents of a file to inject into the server at launch. - // The maximum size of the file is 255 bytes (decoded). - Personality []byte + // Personality [optional] includes files to inject into the server at launch. + // Create will base64-encode file contents for you. + Personality os.Personality // ConfigDrive [optional] enables metadata injection through a configuration drive. ConfigDrive bool @@ -58,7 +69,7 @@ type CreateOpts struct { DiskConfig diskconfig.DiskConfig // BlockDevice [optional] will create the server from a volume, which is created from an image, - // a snapshot, or an another volume. + // a snapshot, or another volume. BlockDevice []bootfromvolume.BlockDevice } @@ -68,7 +79,9 @@ func (opts CreateOpts) ToServerCreateMap() (map[string]interface{}, error) { base := os.CreateOpts{ Name: opts.Name, ImageRef: opts.ImageRef, + ImageName: opts.ImageName, FlavorRef: opts.FlavorRef, + FlavorName: opts.FlavorName, SecurityGroups: opts.SecurityGroups, UserData: opts.UserData, AvailabilityZone: opts.AvailabilityZone, @@ -104,7 +117,9 @@ func (opts CreateOpts) ToServerCreateMap() (map[string]interface{}, error) { // key_name doesn't actually come from the extension (or at least isn't documented there) so // we need to add it manually. serverMap := res["server"].(map[string]interface{}) - serverMap["key_name"] = opts.KeyPair + if opts.KeyPair != "" { + serverMap["key_name"] = opts.KeyPair + } return res, nil } @@ -130,9 +145,9 @@ type RebuildOpts struct { // Metadata [optional] contains key-value pairs (up to 255 bytes each) to attach to the server. Metadata map[string]string - // Personality [optional] includes the path and contents of a file to inject into the server at launch. - // The maximum size of the file is 255 bytes (decoded). - Personality []byte + // Personality [optional] includes files to inject into the server at launch. + // Rebuild will base64-encode file contents for you. + Personality os.Personality // Rackspace-specific stuff begins here. diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/servers/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/servers/requests_test.go index 3c0f806936f..828b5dc491e 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/servers/requests_test.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/servers/requests_test.go @@ -22,6 +22,8 @@ func TestCreateOpts(t *testing.T) { "name": "createdserver", "imageRef": "image-id", "flavorRef": "flavor-id", + "flavorName": "", + "imageName": "", "key_name": "mykey", "OS-DCF:diskConfig": "MANUAL" } diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/volumeattach/delegate_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/volumeattach/delegate_test.go index e26416cba0d..f7ef45e621a 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/volumeattach/delegate_test.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/volumeattach/delegate_test.go @@ -3,24 +3,53 @@ package volumeattach import ( "testing" - os "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/volumeattach" + "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/volumeattach" + fixtures "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/volumeattach/testing" "github.com/rackspace/gophercloud/pagination" th "github.com/rackspace/gophercloud/testhelper" "github.com/rackspace/gophercloud/testhelper/client" ) +// FirstVolumeAttachment is the first result in ListOutput. +var FirstVolumeAttachment = volumeattach.VolumeAttachment{ + Device: "/dev/vdd", + ID: "a26887c6-c47b-4654-abb5-dfadf7d3f803", + ServerID: "4d8c3732-a248-40ed-bebc-539a6ffd25c0", + VolumeID: "a26887c6-c47b-4654-abb5-dfadf7d3f803", +} + +// SecondVolumeAttachment is the first result in ListOutput. +var SecondVolumeAttachment = volumeattach.VolumeAttachment{ + Device: "/dev/vdc", + ID: "a26887c6-c47b-4654-abb5-dfadf7d3f804", + ServerID: "4d8c3732-a248-40ed-bebc-539a6ffd25c0", + VolumeID: "a26887c6-c47b-4654-abb5-dfadf7d3f804", +} + +// ExpectedVolumeAttachmentSlide is the slice of results that should be parsed +// from ListOutput, in the expected order. +var ExpectedVolumeAttachmentSlice = []volumeattach.VolumeAttachment{FirstVolumeAttachment, SecondVolumeAttachment} + +//CreatedVolumeAttachment is the parsed result from CreatedOutput. +var CreatedVolumeAttachment = volumeattach.VolumeAttachment{ + Device: "/dev/vdc", + ID: "a26887c6-c47b-4654-abb5-dfadf7d3f804", + ServerID: "4d8c3732-a248-40ed-bebc-539a6ffd25c0", + VolumeID: "a26887c6-c47b-4654-abb5-dfadf7d3f804", +} + func TestList(t *testing.T) { th.SetupHTTP() defer th.TeardownHTTP() - os.HandleListSuccessfully(t) + fixtures.HandleListSuccessfully(t) serverId := "4d8c3732-a248-40ed-bebc-539a6ffd25c0" count := 0 err := List(client.ServiceClient(), serverId).EachPage(func(page pagination.Page) (bool, error) { count++ - actual, err := os.ExtractVolumeAttachments(page) + actual, err := volumeattach.ExtractVolumeAttachments(page) th.AssertNoErr(t, err) - th.CheckDeepEquals(t, os.ExpectedVolumeAttachmentSlice, actual) + th.CheckDeepEquals(t, ExpectedVolumeAttachmentSlice, actual) return true, nil }) @@ -31,33 +60,33 @@ func TestList(t *testing.T) { func TestCreate(t *testing.T) { th.SetupHTTP() defer th.TeardownHTTP() - os.HandleCreateSuccessfully(t) + fixtures.HandleCreateSuccessfully(t) serverId := "4d8c3732-a248-40ed-bebc-539a6ffd25c0" - actual, err := Create(client.ServiceClient(), serverId, os.CreateOpts{ + actual, err := Create(client.ServiceClient(), serverId, volumeattach.CreateOpts{ Device: "/dev/vdc", VolumeID: "a26887c6-c47b-4654-abb5-dfadf7d3f804", }).Extract() th.AssertNoErr(t, err) - th.CheckDeepEquals(t, &os.CreatedVolumeAttachment, actual) + th.CheckDeepEquals(t, &CreatedVolumeAttachment, actual) } func TestGet(t *testing.T) { th.SetupHTTP() defer th.TeardownHTTP() - os.HandleGetSuccessfully(t) + fixtures.HandleGetSuccessfully(t) aId := "a26887c6-c47b-4654-abb5-dfadf7d3f804" serverId := "4d8c3732-a248-40ed-bebc-539a6ffd25c0" actual, err := Get(client.ServiceClient(), serverId, aId).Extract() th.AssertNoErr(t, err) - th.CheckDeepEquals(t, &os.SecondVolumeAttachment, actual) + th.CheckDeepEquals(t, &SecondVolumeAttachment, actual) } func TestDelete(t *testing.T) { th.SetupHTTP() defer th.TeardownHTTP() - os.HandleDeleteSuccessfully(t) + fixtures.HandleDeleteSuccessfully(t) aId := "a26887c6-c47b-4654-abb5-dfadf7d3f804" serverId := "4d8c3732-a248-40ed-bebc-539a6ffd25c0" diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/nodes/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/nodes/fixtures.go index 7c85945cafe..8899fc5e975 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/nodes/fixtures.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/nodes/fixtures.go @@ -107,6 +107,42 @@ func mockCreateResponse(t *testing.T, lbID int) { }) } +func mockCreateErrResponse(t *testing.T, lbID int) { + th.Mux.HandleFunc(_rootURL(lbID), func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + th.TestJSONRequest(t, r, ` +{ + "nodes": [ + { + "address": "10.2.2.3", + "port": 80, + "condition": "ENABLED", + "type": "PRIMARY" + }, + { + "address": "10.2.2.4", + "port": 81, + "condition": "ENABLED", + "type": "SECONDARY" + } + ] +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(422) // Unprocessable Entity + + fmt.Fprintf(w, ` +{ + "code": 422, + "message": "Load Balancer '%d' has a status of 'PENDING_UPDATE' and is considered immutable." +} + `, lbID) + }) +} + func mockBatchDeleteResponse(t *testing.T, lbID int, ids []int) { th.Mux.HandleFunc(_rootURL(lbID), func(w http.ResponseWriter, r *http.Request) { th.TestMethod(t, r, "DELETE") diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/nodes/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/nodes/requests.go index 02af86b5c1e..dc2d46c8913 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/nodes/requests.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/nodes/requests.go @@ -119,12 +119,7 @@ func Create(client *gophercloud.ServiceClient, loadBalancerID int, opts CreateOp return res } - pr, err := pagination.PageResultFrom(resp) - if err != nil { - res.Err = err - return res - } - + pr := pagination.PageResultFromParsed(resp, res.Body) return CreateResult{pagination.SinglePageBase(pr)} } diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/nodes/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/nodes/requests_test.go index 003d347c073..a964af8f90d 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/nodes/requests_test.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/nodes/requests_test.go @@ -108,6 +108,38 @@ func TestCreate(t *testing.T) { th.CheckDeepEquals(t, expected, actual) } +func TestCreateErr(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockCreateErrResponse(t, lbID) + + opts := CreateOpts{ + CreateOpt{ + Address: "10.2.2.3", + Port: 80, + Condition: ENABLED, + Type: PRIMARY, + }, + CreateOpt{ + Address: "10.2.2.4", + Port: 81, + Condition: ENABLED, + Type: SECONDARY, + }, + } + + page := Create(client.ServiceClient(), lbID, opts) + + actual, err := page.ExtractNodes() + if err == nil { + t.Fatal("Did not receive expected error from ExtractNodes") + } + if actual != nil { + t.Fatalf("Received non-nil result from failed ExtractNodes: %#v", actual) + } +} + func TestBulkDelete(t *testing.T) { th.SetupHTTP() defer th.TeardownHTTP() diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/nodes/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/nodes/results.go index 916485f2fcb..57835dc4b5d 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/nodes/results.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/nodes/results.go @@ -126,6 +126,9 @@ type CreateResult struct { // ExtractNodes extracts a slice of Node structs from a CreateResult. func (res CreateResult) ExtractNodes() ([]Node, error) { + if res.Err != nil { + return nil, res.Err + } return commonExtractNodes(res.Body) } diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/sessions/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/sessions/fixtures.go index 9596819d16d..077ef047019 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/sessions/fixtures.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/sessions/fixtures.go @@ -46,6 +46,7 @@ func mockEnableResponse(t *testing.T, lbID int) { `) w.WriteHeader(http.StatusAccepted) + fmt.Fprintf(w, `{}`) }) } diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/ssl/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/ssl/fixtures.go index 1d401001250..5a52962d49a 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/ssl/fixtures.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/ssl/fixtures.go @@ -63,6 +63,7 @@ func mockUpdateResponse(t *testing.T, lbID int) { `) w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, `{}`) }) } diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/throttle/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/throttle/fixtures.go index 40223f60a67..f3e49fa6884 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/throttle/fixtures.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/throttle/fixtures.go @@ -49,6 +49,7 @@ func mockCreateResponse(t *testing.T, lbID int) { `) w.WriteHeader(http.StatusAccepted) + fmt.Fprintf(w, `{}`) }) } diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/networking/v2/networks/delegate_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/networking/v2/networks/delegate_test.go index f51c732d436..0b3a6b15eb4 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/networking/v2/networks/delegate_test.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/networking/v2/networks/delegate_test.go @@ -203,8 +203,17 @@ func TestCreateWithOptionalFields(t *testing.T) { } } `) - w.WriteHeader(http.StatusCreated) + fmt.Fprintf(w, ` +{ + "network": { + "name": "sample_network", + "admin_state_up": true, + "shared": true, + "tenant_id": "12345" + } +} + `) }) iTrue := true diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/bulk/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/bulk/requests.go index 898b73b0bd8..0aeec155b77 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/bulk/requests.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/bulk/requests.go @@ -43,7 +43,9 @@ func Delete(c *gophercloud.ServiceClient, opts DeleteOptsBuilder) DeleteResult { JSONBody: reqBody, JSONResponse: &res.Body, }) - res.Header = resp.Header + if resp != nil { + res.Header = resp.Header + } res.Err = err return res } diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdncontainers/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdncontainers/requests.go index 8e4abbe436c..6acebb0aad2 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdncontainers/requests.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdncontainers/requests.go @@ -53,7 +53,9 @@ func Enable(c *gophercloud.ServiceClient, containerName string, opts EnableOptsB MoreHeaders: h, OkCodes: []int{201, 202, 204}, }) - res.Header = resp.Header + if resp != nil { + res.Header = resp.Header + } res.Err = err return res } @@ -66,7 +68,9 @@ func Get(c *gophercloud.ServiceClient, containerName string) GetResult { resp, err := c.Request("HEAD", getURL(c, containerName), gophercloud.RequestOpts{ OkCodes: []int{200, 204}, }) - res.Header = resp.Header + if resp != nil { + res.Header = resp.Header + } res.Err = err return res } @@ -149,7 +153,9 @@ func Update(c *gophercloud.ServiceClient, containerName string, opts UpdateOptsB MoreHeaders: h, OkCodes: []int{202, 204}, }) - res.Header = resp.Header + if resp != nil { + res.Header = resp.Header + } res.Err = err return res } diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/objects/delegate.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/objects/delegate.go index 028d66a0ab7..94c820ba7bd 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/objects/delegate.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/objects/delegate.go @@ -33,7 +33,7 @@ func Download(c *gophercloud.ServiceClient, containerName, objectName string, op } // Create is a function that creates a new object or replaces an existing object. -func Create(c *gophercloud.ServiceClient, containerName, objectName string, content io.Reader, opts os.CreateOptsBuilder) os.CreateResult { +func Create(c *gophercloud.ServiceClient, containerName, objectName string, content io.ReadSeeker, opts os.CreateOptsBuilder) os.CreateResult { return os.Create(c, containerName, objectName, content, opts) } diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/objects/delegate_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/objects/delegate_test.go index 8ab8029c37b..21cd4179a5d 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/objects/delegate_test.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/objects/delegate_test.go @@ -1,7 +1,7 @@ package objects import ( - "bytes" + "strings" "testing" os "github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects" @@ -66,21 +66,23 @@ func TestListObjectNames(t *testing.T) { func TestCreateObject(t *testing.T) { th.SetupHTTP() defer th.TeardownHTTP() - os.HandleCreateTextObjectSuccessfully(t) + + content := "Did gyre and gimble in the wabe" + os.HandleCreateTextObjectSuccessfully(t, content) - content := bytes.NewBufferString("Did gyre and gimble in the wabe") options := &os.CreateOpts{ContentType: "text/plain"} - res := Create(fake.ServiceClient(), "testContainer", "testObject", content, options) + res := Create(fake.ServiceClient(), "testContainer", "testObject", strings.NewReader(content), options) th.AssertNoErr(t, res.Err) } func TestCreateObjectWithoutContentType(t *testing.T) { th.SetupHTTP() defer th.TeardownHTTP() - os.HandleCreateTypelessObjectSuccessfully(t) - content := bytes.NewBufferString("The sky was the color of television, tuned to a dead channel.") - res := Create(fake.ServiceClient(), "testContainer", "testObject", content, &os.CreateOpts{}) + content := "The sky was the color of television, tuned to a dead channel." + os.HandleCreateTypelessObjectSuccessfully(t, content) + + res := Create(fake.ServiceClient(), "testContainer", "testObject", strings.NewReader(content), &os.CreateOpts{}) th.AssertNoErr(t, res.Err) } diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/results.go index 7c86ce46230..27fd1b60fa2 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/results.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/results.go @@ -113,6 +113,9 @@ func DecodeHeader(from, to interface{}) error { // RFC3339Milli describes a common time format used by some API responses. const RFC3339Milli = "2006-01-02T15:04:05.999999Z" +// Time format used in cloud orchestration +const STACK_TIME_FMT = "2006-01-02T15:04:05" + /* Link is an internal type to be used in packages of collection resources that are paginated in a certain way.