updated gophercloud

This commit is contained in:
Sreekanth Pothanis
2015-09-16 12:00:25 -07:00
parent 6dbb781093
commit 63ba88a274
88 changed files with 2372 additions and 217 deletions

4
Godeps/Godeps.json generated
View File

@@ -489,8 +489,8 @@
}, },
{ {
"ImportPath": "github.com/rackspace/gophercloud", "ImportPath": "github.com/rackspace/gophercloud",
"Comment": "v1.0.0-569-gf3ced00", "Comment": "v1.0.0-665-gf928634",
"Rev": "f3ced00552c1c7d4a6184500af9062cfb4ff4463" "Rev": "f92863476c034f851073599c09d90cd61ee95b3d"
}, },
{ {
"ImportPath": "github.com/russross/blackfriday", "ImportPath": "github.com/russross/blackfriday",

View File

@@ -28,11 +28,10 @@ fork as `origin` instead:
git remote add origin git@github.com/<my_username>/gophercloud git remote add origin git@github.com/<my_username>/gophercloud
``` ```
4. Checkout the latest development branch ([click here](/branches) to see all 4. Checkout the latest development branch:
the branches):
```bash ```bash
git checkout release/v1.0.1 git checkout master
``` ```
5. If you're working on something (discussed more in detail below), you will 5. If you're working on something (discussed more in detail below), you will

View File

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

View File

@@ -3,10 +3,15 @@
package v2 package v2
import ( import (
"fmt"
"testing" "testing"
"github.com/rackspace/gophercloud" "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/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) { 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 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) { func TestServerGroups(t *testing.T) {
choices, err := ComputeChoicesFromEnv()
if err != nil {
t.Fatal(err)
}
computeClient, err := newClient() computeClient, err := newClient()
if err != nil { if err != nil {
t.Fatalf("Unable to create a compute client: %v", err) t.Fatalf("Unable to create a compute client: %v", err)
@@ -48,11 +99,45 @@ func TestServerGroups(t *testing.T) {
} }
defer func() { defer func() {
servergroups.Delete(computeClient, sg.ID) servergroups.Delete(computeClient, sg.ID)
t.Logf("ServerGroup deleted.") t.Logf("Server Group deleted.")
}() }()
err = getServerGroup(t, computeClient, sg.ID) err = getServerGroup(t, computeClient, sg.ID)
if err != nil { if err != nil {
t.Fatalf("Unable to get server group: %v", err) 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)
}
} }

View File

@@ -107,6 +107,12 @@ func createServer(t *testing.T, client *gophercloud.ServiceClient, choices *Comp
servers.Network{UUID: network.ID}, servers.Network{UUID: network.ID},
}, },
AdminPass: pwd, AdminPass: pwd,
Personality: servers.Personality{
&servers.File{
Path: "/etc/test",
Contents: []byte("hello world"),
},
},
}).Extract() }).Extract()
if err != nil { if err != nil {
t.Fatalf("Unable to create server: %v", err) t.Fatalf("Unable to create server: %v", err)

View File

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

View File

@@ -43,4 +43,8 @@ type AuthOptions struct {
// false, it will not cache these settings, but re-authentication will not be // false, it will not cache these settings, but re-authentication will not be
// possible. This setting defaults to false. // possible. This setting defaults to false.
AllowReauth bool AllowReauth bool
// TokenID allows users to authenticate (possibly as another user) with an
// authentication token ID.
TokenID string
} }

View File

@@ -171,3 +171,36 @@ func UpdateMetadata(client *gophercloud.ServiceClient, id string, opts UpdateMet
}) })
return res 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)
}
}

View File

@@ -201,3 +201,36 @@ func Update(client *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder
}) })
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) {
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)
}
}

View File

@@ -3,6 +3,7 @@ package volumes
import ( import (
"testing" "testing"
fixtures "github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/testing"
"github.com/rackspace/gophercloud/pagination" "github.com/rackspace/gophercloud/pagination"
th "github.com/rackspace/gophercloud/testhelper" th "github.com/rackspace/gophercloud/testhelper"
"github.com/rackspace/gophercloud/testhelper/client" "github.com/rackspace/gophercloud/testhelper/client"
@@ -12,7 +13,7 @@ func TestList(t *testing.T) {
th.SetupHTTP() th.SetupHTTP()
defer th.TeardownHTTP() defer th.TeardownHTTP()
MockListResponse(t) fixtures.MockListResponse(t)
count := 0 count := 0
@@ -49,7 +50,7 @@ func TestListAll(t *testing.T) {
th.SetupHTTP() th.SetupHTTP()
defer th.TeardownHTTP() defer th.TeardownHTTP()
MockListResponse(t) fixtures.MockListResponse(t)
allPages, err := List(client.ServiceClient(), &ListOpts{}).AllPages() allPages, err := List(client.ServiceClient(), &ListOpts{}).AllPages()
th.AssertNoErr(t, err) th.AssertNoErr(t, err)
@@ -75,7 +76,7 @@ func TestGet(t *testing.T) {
th.SetupHTTP() th.SetupHTTP()
defer th.TeardownHTTP() defer th.TeardownHTTP()
MockGetResponse(t) fixtures.MockGetResponse(t)
v, err := Get(client.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22").Extract() v, err := Get(client.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22").Extract()
th.AssertNoErr(t, err) th.AssertNoErr(t, err)
@@ -89,7 +90,7 @@ func TestCreate(t *testing.T) {
th.SetupHTTP() th.SetupHTTP()
defer th.TeardownHTTP() defer th.TeardownHTTP()
MockCreateResponse(t) fixtures.MockCreateResponse(t)
options := &CreateOpts{Size: 75} options := &CreateOpts{Size: 75}
n, err := Create(client.ServiceClient(), options).Extract() n, err := Create(client.ServiceClient(), options).Extract()
@@ -103,7 +104,7 @@ func TestDelete(t *testing.T) {
th.SetupHTTP() th.SetupHTTP()
defer th.TeardownHTTP() defer th.TeardownHTTP()
MockDeleteResponse(t) fixtures.MockDeleteResponse(t)
res := Delete(client.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22") res := Delete(client.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22")
th.AssertNoErr(t, res.Err) th.AssertNoErr(t, res.Err)
@@ -113,7 +114,7 @@ func TestUpdate(t *testing.T) {
th.SetupHTTP() th.SetupHTTP()
defer th.TeardownHTTP() defer th.TeardownHTTP()
MockUpdateResponse(t) fixtures.MockUpdateResponse(t)
options := UpdateOpts{Name: "vol-002"} options := UpdateOpts{Name: "vol-002"}
v, err := Update(client.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22", options).Extract() v, err := Update(client.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22", options).Extract()

View File

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

View File

@@ -32,6 +32,8 @@ func TestCreateOpts(t *testing.T) {
"name": "createdserver", "name": "createdserver",
"imageRef": "asdfasdfasdf", "imageRef": "asdfasdfasdf",
"flavorRef": "performance1-1", "flavorRef": "performance1-1",
"flavorName": "",
"imageName": "",
"block_device_mapping_v2":[ "block_device_mapping_v2":[
{ {
"uuid":"123456", "uuid":"123456",

View File

@@ -25,6 +25,8 @@ func TestCreateOpts(t *testing.T) {
"name": "createdserver", "name": "createdserver",
"imageRef": "asdfasdfasdf", "imageRef": "asdfasdfasdf",
"flavorRef": "performance1-1", "flavorRef": "performance1-1",
"flavorName": "",
"imageName": "",
"OS-DCF:diskConfig": "MANUAL" "OS-DCF:diskConfig": "MANUAL"
} }
} }

View File

@@ -0,0 +1,2 @@
// Package network provides the ability to manage nova-networks
package networks

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -242,6 +242,7 @@ func mockAddServerToGroupResponse(t *testing.T, serverID string) {
w.Header().Add("Content-Type", "application/json") w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusAccepted) 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.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusAccepted) w.WriteHeader(http.StatusAccepted)
fmt.Fprintf(w, `{}`)
}) })
} }

View File

@@ -3,15 +3,44 @@ package volumeattach
import ( import (
"testing" "testing"
fixtures "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/volumeattach/testing"
"github.com/rackspace/gophercloud/pagination" "github.com/rackspace/gophercloud/pagination"
th "github.com/rackspace/gophercloud/testhelper" th "github.com/rackspace/gophercloud/testhelper"
"github.com/rackspace/gophercloud/testhelper/client" "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) { func TestList(t *testing.T) {
th.SetupHTTP() th.SetupHTTP()
defer th.TeardownHTTP() defer th.TeardownHTTP()
HandleListSuccessfully(t) fixtures.HandleListSuccessfully(t)
serverId := "4d8c3732-a248-40ed-bebc-539a6ffd25c0" serverId := "4d8c3732-a248-40ed-bebc-539a6ffd25c0"
count := 0 count := 0
@@ -30,7 +59,7 @@ func TestList(t *testing.T) {
func TestCreate(t *testing.T) { func TestCreate(t *testing.T) {
th.SetupHTTP() th.SetupHTTP()
defer th.TeardownHTTP() defer th.TeardownHTTP()
HandleCreateSuccessfully(t) fixtures.HandleCreateSuccessfully(t)
serverId := "4d8c3732-a248-40ed-bebc-539a6ffd25c0" serverId := "4d8c3732-a248-40ed-bebc-539a6ffd25c0"
actual, err := Create(client.ServiceClient(), serverId, CreateOpts{ actual, err := Create(client.ServiceClient(), serverId, CreateOpts{
@@ -44,7 +73,7 @@ func TestCreate(t *testing.T) {
func TestGet(t *testing.T) { func TestGet(t *testing.T) {
th.SetupHTTP() th.SetupHTTP()
defer th.TeardownHTTP() defer th.TeardownHTTP()
HandleGetSuccessfully(t) fixtures.HandleGetSuccessfully(t)
aId := "a26887c6-c47b-4654-abb5-dfadf7d3f804" aId := "a26887c6-c47b-4654-abb5-dfadf7d3f804"
serverId := "4d8c3732-a248-40ed-bebc-539a6ffd25c0" serverId := "4d8c3732-a248-40ed-bebc-539a6ffd25c0"
@@ -56,7 +85,7 @@ func TestGet(t *testing.T) {
func TestDelete(t *testing.T) { func TestDelete(t *testing.T) {
th.SetupHTTP() th.SetupHTTP()
defer th.TeardownHTTP() defer th.TeardownHTTP()
HandleDeleteSuccessfully(t) fixtures.HandleDeleteSuccessfully(t)
aId := "a26887c6-c47b-4654-abb5-dfadf7d3f804" aId := "a26887c6-c47b-4654-abb5-dfadf7d3f804"
serverId := "4d8c3732-a248-40ed-bebc-539a6ffd25c0" serverId := "4d8c3732-a248-40ed-bebc-539a6ffd25c0"

View File

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

View File

@@ -1,6 +1,6 @@
// +build fixtures // +build fixtures
package volumeattach package testing
import ( import (
"fmt" "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. // HandleListSuccessfully configures the test server to respond to a List request.
func HandleListSuccessfully(t *testing.T) { func HandleListSuccessfully(t *testing.T) {
th.Mux.HandleFunc("/servers/4d8c3732-a248-40ed-bebc-539a6ffd25c0/os-volume_attachments", func(w http.ResponseWriter, r *http.Request) { th.Mux.HandleFunc("/servers/4d8c3732-a248-40ed-bebc-539a6ffd25c0/os-volume_attachments", func(w http.ResponseWriter, r *http.Request) {

View File

@@ -1,6 +1,8 @@
package flavors package flavors
import ( import (
"fmt"
"github.com/rackspace/gophercloud" "github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination" "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) _, res.Err = client.Get(getURL(client, id), &res.Body, nil)
return res 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)
}
}

View File

@@ -1,6 +1,8 @@
package images package images
import ( import (
"fmt"
"github.com/rackspace/gophercloud" "github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination" "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) _, result.Err = client.Delete(deleteURL(client, id), nil)
return result 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)
}
}

View File

@@ -2,10 +2,13 @@ package servers
import ( import (
"encoding/base64" "encoding/base64"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"github.com/rackspace/gophercloud" "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" "github.com/rackspace/gophercloud/pagination"
) )
@@ -14,6 +17,7 @@ import (
type ListOptsBuilder interface { type ListOptsBuilder interface {
ToServerListQuery() (string, error) ToServerListQuery() (string, error)
} }
// ListOpts allows the filtering and sorting of paginated collections through // 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 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 // 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. // Integer value for the limit of values to return.
Limit int `q:"limit"` Limit int `q:"limit"`
// Bool to show all tenants
AllTenants bool `q:"all_tenants"`
} }
// ToServerListQuery formats a ListOpts into a query string. // ToServerListQuery formats a ListOpts into a query string.
@@ -95,18 +102,54 @@ type Network struct {
FixedIP string 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. // CreateOpts specifies server creation parameters.
type CreateOpts struct { type CreateOpts struct {
// Name [required] is the name to assign to the newly launched server. // Name [required] is the name to assign to the newly launched server.
Name string Name string
// ImageRef [required] is the ID or full URL to the image that contains the server's OS and initial state. // ImageRef [optional; required if ImageName is not provided] is the ID or full
// Optional if using the boot-from-volume extension. // URL to the image that contains the server's OS and initial state.
// Also optional if using the boot-from-volume extension.
ImageRef string 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 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 [optional] lists the names of the security groups to which this server should belong.
SecurityGroups []string 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 [optional] contains key-value pairs (up to 255 bytes each) to attach to the server.
Metadata map[string]string Metadata map[string]string
// Personality [optional] includes the path and contents of a file to inject into the server at launch. // Personality [optional] includes files to inject into the server at launch.
// The maximum size of the file is 255 bytes (decoded). // Create will base64-encode file contents for you.
Personality []byte Personality Personality
// ConfigDrive [optional] enables metadata injection through a configuration drive. // ConfigDrive [optional] enables metadata injection through a configuration drive.
ConfigDrive bool ConfigDrive bool
@@ -148,16 +191,14 @@ func (opts CreateOpts) ToServerCreateMap() (map[string]interface{}, error) {
server["name"] = opts.Name server["name"] = opts.Name
server["imageRef"] = opts.ImageRef server["imageRef"] = opts.ImageRef
server["imageName"] = opts.ImageName
server["flavorRef"] = opts.FlavorRef server["flavorRef"] = opts.FlavorRef
server["flavorName"] = opts.FlavorName
if opts.UserData != nil { if opts.UserData != nil {
encoded := base64.StdEncoding.EncodeToString(opts.UserData) encoded := base64.StdEncoding.EncodeToString(opts.UserData)
server["user_data"] = &encoded server["user_data"] = &encoded
} }
if opts.Personality != nil {
encoded := base64.StdEncoding.EncodeToString(opts.Personality)
server["personality"] = &encoded
}
if opts.ConfigDrive { if opts.ConfigDrive {
server["config_drive"] = "true" server["config_drive"] = "true"
} }
@@ -202,6 +243,10 @@ func (opts CreateOpts) ToServerCreateMap() (map[string]interface{}, error) {
server["networks"] = networks server["networks"] = networks
} }
if len(opts.Personality) > 0 {
server["personality"] = opts.Personality
}
return map[string]interface{}{"server": server}, nil return map[string]interface{}{"server": server}, nil
} }
@@ -215,6 +260,38 @@ func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateRes
return res 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) _, res.Err = client.Post(listURL(client), reqBody, &res.Body, nil)
return res 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 [optional] contains key-value pairs (up to 255 bytes each) to attach to the server.
Metadata map[string]string Metadata map[string]string
// Personality [optional] includes the path and contents of a file to inject into the server at launch. // Personality [optional] includes files to inject into the server at launch.
// The maximum size of the file is 255 bytes (decoded). // Rebuild will base64-encode file contents for you.
Personality []byte Personality Personality
} }
// ToServerRebuildMap formats a RebuildOpts struct into a map for use in JSON // 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 server["metadata"] = opts.Metadata
} }
if opts.Personality != nil { if len(opts.Personality) > 0 {
encoded := base64.StdEncoding.EncodeToString(opts.Personality) server["personality"] = opts.Personality
server["personality"] = &encoded
} }
return map[string]interface{}{"rebuild": server}, nil 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. // 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 { func DeleteMetadatum(client *gophercloud.ServiceClient, id, key string) DeleteMetadatumResult {
var res DeleteMetadatumResult var res DeleteMetadatumResult
_, res.Err = client.Delete(metadatumURL(client, id, key), &gophercloud.RequestOpts{ _, res.Err = client.Delete(metadatumURL(client, id, key), nil)
JSONResponse: &res.Body,
})
return res return res
} }
@@ -741,5 +815,38 @@ func CreateImage(client *gophercloud.ServiceClient, serverId string, opts Create
}) })
res.Err = err res.Err = err
res.Header = response.Header 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)
}
} }

View File

@@ -1,6 +1,8 @@
package servers package servers
import ( import (
"encoding/base64"
"encoding/json"
"net/http" "net/http"
"testing" "testing"
@@ -334,3 +336,38 @@ func TestCreateServerImage(t *testing.T) {
_, err := CreateImage(client.ServiceClient(), "serverimage", CreateImageOpts{Name: "test"}).ExtractImageID() _, err := CreateImage(client.ServiceClient(), "serverimage", CreateImageOpts{Name: "test"}).ExtractImageID()
th.AssertNoErr(t, err) 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")
}
}

View File

@@ -18,7 +18,7 @@ var (
// ErrDomainNameProvided is returned if you attempt to authenticate with a DomainName. // ErrDomainNameProvided is returned if you attempt to authenticate with a DomainName.
ErrDomainNameProvided = unacceptedAttributeErr("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.") ErrUsernameRequired = errors.New("You must supply a Username in your AuthOptions.")
// ErrPasswordRequired is returned if you don't provide a password. // ErrPasswordRequired is returned if you don't provide a password.

View File

@@ -1,6 +1,10 @@
package tokens 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. // AuthOptionsBuilder describes any argument that may be passed to the Create call.
type AuthOptionsBuilder interface { type AuthOptionsBuilder interface {
@@ -38,20 +42,24 @@ func (auth AuthOptions) ToTokenCreateMap() (map[string]interface{}, error) {
return nil, ErrDomainNameProvided 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. // Populate the request map.
authMap := make(map[string]interface{}) authMap := make(map[string]interface{})
authMap["passwordCredentials"] = map[string]interface{}{ if auth.Username != "" {
"username": auth.Username, if auth.Password != "" {
"password": 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 != "" { if auth.TenantID != "" {

View File

@@ -1,6 +1,7 @@
package tokens package tokens
import ( import (
"fmt"
"testing" "testing"
"github.com/rackspace/gophercloud" "github.com/rackspace/gophercloud"
@@ -22,7 +23,7 @@ func tokenPostErr(t *testing.T, options gophercloud.AuthOptions, expectedErr err
HandleTokenPost(t, "") HandleTokenPost(t, "")
actualErr := Create(client.ServiceClient(), AuthOptions{options}).Err actualErr := Create(client.ServiceClient(), AuthOptions{options}).Err
th.CheckEquals(t, expectedErr, actualErr) th.CheckDeepEquals(t, expectedErr, actualErr)
} }
func TestCreateWithPassword(t *testing.T) { func TestCreateWithPassword(t *testing.T) {
@@ -128,7 +129,7 @@ func TestRequireUsername(t *testing.T) {
Password: "thing", 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) { func TestRequirePassword(t *testing.T) {

View File

@@ -0,0 +1,3 @@
// Package roles provides information and interaction with the roles API
// resource for the OpenStack Identity service.
package roles

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,7 @@
package roles
import "github.com/rackspace/gophercloud"
func listAssignmentsURL(client *gophercloud.ServiceClient) string {
return client.ServiceURL("role_assignments")
}

View File

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

View File

@@ -212,9 +212,9 @@ func TestUpdate(t *testing.T) {
"name": "fw", "name": "fw",
"admin_state_up": false, "admin_state_up": false,
"tenant_id": "b4eedccc6fb74fa8a7ad6b08382b852b", "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", "id": "ea5b5315-64f6-4ea3-8e58-981cc37c6576",
"description": "OpenStack firewall", "description": "OpenStack firewall"
} }
} }
`) `)

View File

@@ -296,6 +296,7 @@ func TestAssociateHealthMonitor(t *testing.T) {
w.Header().Add("Content-Type", "application/json") w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated) w.WriteHeader(http.StatusCreated)
fmt.Fprintf(w, `{}`)
}) })
_, err := AssociateMonitor(fake.ServiceClient(), "332abe93-f488-41ba-870b-2ac66be7f853", "b624decf-d5d3-4c66-9a3d-f047e7786181").Extract() _, err := AssociateMonitor(fake.ServiceClient(), "332abe93-f488-41ba-870b-2ac66be7f853", "b624decf-d5d3-4c66-9a3d-f047e7786181").Extract()

View File

@@ -45,6 +45,9 @@ type CreateOpts struct {
// Required. Human-readable name for the VIP. Does not have to be unique. // Required. Human-readable name for the VIP. Does not have to be unique.
Name string Name string
// Required for admins. Indicates the owner of the VIP.
TenantID string
// Optional. Describes the security group. // Optional. Describes the security group.
Description string Description string
} }
@@ -62,6 +65,7 @@ func Create(c *gophercloud.ServiceClient, opts CreateOpts) CreateResult {
type secgroup struct { type secgroup struct {
Name string `json:"name"` Name string `json:"name"`
TenantID string `json:"tenant_id,omitempty"`
Description string `json:"description,omitempty"` Description string `json:"description,omitempty"`
} }
@@ -71,6 +75,7 @@ func Create(c *gophercloud.ServiceClient, opts CreateOpts) CreateResult {
reqBody := request{SecGroup: secgroup{ reqBody := request{SecGroup: secgroup{
Name: opts.Name, Name: opts.Name,
TenantID: opts.TenantID,
Description: opts.Description, Description: opts.Description,
}} }}
@@ -91,3 +96,36 @@ func Delete(c *gophercloud.ServiceClient, id string) DeleteResult {
_, res.Err = c.Delete(resourceURL(c, id), nil) _, res.Err = c.Delete(resourceURL(c, id), nil)
return res 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)
}
}

View File

@@ -99,6 +99,9 @@ type CreateOpts struct {
// attribute matches the specified IP prefix as the source IP address of the // attribute matches the specified IP prefix as the source IP address of the
// IP packet. // IP packet.
RemoteIPPrefix string 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 // 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"` Protocol string `json:"protocol,omitempty"`
RemoteGroupID string `json:"remote_group_id,omitempty"` RemoteGroupID string `json:"remote_group_id,omitempty"`
RemoteIPPrefix string `json:"remote_ip_prefix,omitempty"` RemoteIPPrefix string `json:"remote_ip_prefix,omitempty"`
TenantID string `json:"tenant_id,omitempty"`
} }
type request struct { type request struct {
@@ -148,6 +152,7 @@ func Create(c *gophercloud.ServiceClient, opts CreateOpts) CreateResult {
Protocol: opts.Protocol, Protocol: opts.Protocol,
RemoteGroupID: opts.RemoteGroupID, RemoteGroupID: opts.RemoteGroupID,
RemoteIPPrefix: opts.RemoteIPPrefix, RemoteIPPrefix: opts.RemoteIPPrefix,
TenantID: opts.TenantID,
}} }}
_, res.Err = c.Post(rootURL(c), reqBody, &res.Body, nil) _, res.Err = c.Post(rootURL(c), reqBody, &res.Body, nil)

View File

@@ -1,6 +1,8 @@
package networks package networks
import ( import (
"fmt"
"github.com/rackspace/gophercloud" "github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination" "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) _, res.Err = c.Delete(deleteURL(c, networkID), nil)
return res 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)
}
}

View File

@@ -204,6 +204,7 @@ func TestCreateWithOptionalFields(t *testing.T) {
`) `)
w.WriteHeader(http.StatusCreated) w.WriteHeader(http.StatusCreated)
fmt.Fprintf(w, `{}`)
}) })
iTrue := true iTrue := true

View File

@@ -1,6 +1,8 @@
package ports package ports
import ( import (
"fmt"
"github.com/rackspace/gophercloud" "github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination" "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) _, res.Err = c.Delete(deleteURL(c, id), nil)
return res 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)
}
}

View File

@@ -1,6 +1,8 @@
package subnets package subnets
import ( import (
"fmt"
"github.com/rackspace/gophercloud" "github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination" "github.com/rackspace/gophercloud/pagination"
) )
@@ -200,10 +202,10 @@ func (opts UpdateOpts) ToSubnetUpdateMap() (map[string]interface{}, error) {
if opts.GatewayIP != "" { if opts.GatewayIP != "" {
s["gateway_ip"] = opts.GatewayIP s["gateway_ip"] = opts.GatewayIP
} }
if len(opts.DNSNameservers) != 0 { if opts.DNSNameservers != nil {
s["dns_nameservers"] = opts.DNSNameservers s["dns_nameservers"] = opts.DNSNameservers
} }
if len(opts.HostRoutes) != 0 { if opts.HostRoutes != nil {
s["host_routes"] = opts.HostRoutes 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) _, res.Err = c.Delete(deleteURL(c, id), nil)
return res 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)
}
}

View File

@@ -55,8 +55,8 @@ type AllocationPool struct {
// HostRoute represents a route that should be used by devices with IPs from // HostRoute represents a route that should be used by devices with IPs from
// a subnet (not including local subnet route). // a subnet (not including local subnet route).
type HostRoute struct { type HostRoute struct {
DestinationCIDR string `json:"destination"` DestinationCIDR string `mapstructure:"destination" json:"destination"`
NextHop string `json:"nexthop"` NextHop string `mapstructure:"nexthop" json:"nexthop"`
} }
// Subnet represents a subnet. See package documentation for a top-level // Subnet represents a subnet. See package documentation for a top-level

View File

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

View File

@@ -43,7 +43,9 @@ func Get(c *gophercloud.ServiceClient, opts GetOptsBuilder) GetResult {
MoreHeaders: h, MoreHeaders: h,
OkCodes: []int{204}, OkCodes: []int{204},
}) })
res.Header = resp.Header if resp != nil {
res.Header = resp.Header
}
res.Err = err res.Err = err
return res return res
} }
@@ -97,7 +99,9 @@ func Update(c *gophercloud.ServiceClient, opts UpdateOptsBuilder) UpdateResult {
MoreHeaders: h, MoreHeaders: h,
OkCodes: []int{201, 202, 204}, OkCodes: []int{201, 202, 204},
}) })
res.Header = resp.Header if resp != nil {
res.Header = resp.Header
}
res.Err = err res.Err = err
return res return res
} }

View File

@@ -114,7 +114,9 @@ func Create(c *gophercloud.ServiceClient, containerName string, opts CreateOptsB
MoreHeaders: h, MoreHeaders: h,
OkCodes: []int{201, 202, 204}, OkCodes: []int{201, 202, 204},
}) })
res.Header = resp.Header if resp != nil {
res.Header = resp.Header
}
res.Err = err res.Err = err
return res return res
} }
@@ -180,7 +182,9 @@ func Update(c *gophercloud.ServiceClient, containerName string, opts UpdateOptsB
MoreHeaders: h, MoreHeaders: h,
OkCodes: []int{201, 202, 204}, OkCodes: []int{201, 202, 204},
}) })
res.Header = resp.Header if resp != nil {
res.Header = resp.Header
}
res.Err = err res.Err = err
return res return res
} }
@@ -193,7 +197,9 @@ func Get(c *gophercloud.ServiceClient, containerName string) GetResult {
resp, err := c.Request("HEAD", getURL(c, containerName), gophercloud.RequestOpts{ resp, err := c.Request("HEAD", getURL(c, containerName), gophercloud.RequestOpts{
OkCodes: []int{200, 204}, OkCodes: []int{200, 204},
}) })
res.Header = resp.Header if resp != nil {
res.Header = resp.Header
}
res.Err = err res.Err = err
return res return res
} }

View File

@@ -3,7 +3,9 @@
package objects package objects
import ( import (
"crypto/md5"
"fmt" "fmt"
"io"
"net/http" "net/http"
"testing" "testing"
@@ -107,12 +109,18 @@ func HandleListObjectNamesSuccessfully(t *testing.T) {
// HandleCreateTextObjectSuccessfully creates an HTTP handler at `/testContainer/testObject` on the test handler mux // 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. // 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.Mux.HandleFunc("/testContainer/testObject", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "PUT") th.TestMethod(t, r, "PUT")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
th.TestHeader(t, r, "Content-Type", "text/plain") th.TestHeader(t, r, "Content-Type", "text/plain")
th.TestHeader(t, r, "Accept", "application/json") 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) 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 // 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- // 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. // 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.Mux.HandleFunc("/testContainer/testObject", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "PUT") th.TestMethod(t, r, "PUT")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) 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) 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) w.WriteHeader(http.StatusCreated)
}) })
} }

View File

@@ -1,7 +1,9 @@
package objects package objects
import ( import (
"bytes"
"crypto/hmac" "crypto/hmac"
"crypto/md5"
"crypto/sha1" "crypto/sha1"
"fmt" "fmt"
"io" "io"
@@ -134,10 +136,11 @@ func Download(c *gophercloud.ServiceClient, containerName, objectName string, op
MoreHeaders: h, MoreHeaders: h,
OkCodes: []int{200, 304}, OkCodes: []int{200, 304},
}) })
if resp != nil {
res.Body = resp.Body res.Header = resp.Header
res.Body = resp.Body
}
res.Err = err res.Err = err
res.Header = resp.Header
return res return res
} }
@@ -187,8 +190,9 @@ func (opts CreateOpts) ToObjectCreateParams() (map[string]string, string, error)
return h, q.String(), nil return h, q.String(), nil
} }
// Create is a function that creates a new object or replaces an existing object. // Create is a function that creates a new object or replaces an existing object. If the returned response's ETag
func Create(c *gophercloud.ServiceClient, containerName, objectName string, content io.Reader, opts CreateOptsBuilder) CreateResult { // 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 var res CreateResult
url := createURL(c, containerName, objectName) url := createURL(c, containerName, objectName)
@@ -208,14 +212,37 @@ func Create(c *gophercloud.ServiceClient, containerName, objectName string, cont
url += query 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{ ropts := gophercloud.RequestOpts{
RawBody: content, RawBody: strings.NewReader(contentBuffer.String()),
MoreHeaders: h, MoreHeaders: h,
} }
resp, err := c.Request("PUT", url, ropts) resp, err := c.Request("PUT", url, ropts)
res.Header = resp.Header if err != nil {
res.Err = err 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 return res
} }
@@ -270,7 +297,9 @@ func Copy(c *gophercloud.ServiceClient, containerName, objectName string, opts C
MoreHeaders: h, MoreHeaders: h,
OkCodes: []int{201}, OkCodes: []int{201},
}) })
res.Header = resp.Header if resp != nil {
res.Header = resp.Header
}
res.Err = err res.Err = err
return res return res
} }
@@ -310,7 +339,9 @@ func Delete(c *gophercloud.ServiceClient, containerName, objectName string, opts
} }
resp, err := c.Delete(url, nil) resp, err := c.Delete(url, nil)
res.Header = resp.Header if resp != nil {
res.Header = resp.Header
}
res.Err = err res.Err = err
return res return res
} }
@@ -354,7 +385,9 @@ func Get(c *gophercloud.ServiceClient, containerName, objectName string, opts Ge
resp, err := c.Request("HEAD", url, gophercloud.RequestOpts{ resp, err := c.Request("HEAD", url, gophercloud.RequestOpts{
OkCodes: []int{200, 204}, OkCodes: []int{200, 204},
}) })
res.Header = resp.Header if resp != nil {
res.Header = resp.Header
}
res.Err = err res.Err = err
return res return res
} }
@@ -410,7 +443,9 @@ func Update(c *gophercloud.ServiceClient, containerName, objectName string, opts
resp, err := c.Request("POST", url, gophercloud.RequestOpts{ resp, err := c.Request("POST", url, gophercloud.RequestOpts{
MoreHeaders: h, MoreHeaders: h,
}) })
res.Header = resp.Header if resp != nil {
res.Header = resp.Header
}
res.Err = err res.Err = err
return res return res
} }

View File

@@ -2,7 +2,10 @@ package objects
import ( import (
"bytes" "bytes"
"fmt"
"io" "io"
"net/http"
"strings"
"testing" "testing"
"github.com/rackspace/gophercloud/pagination" "github.com/rackspace/gophercloud/pagination"
@@ -83,24 +86,44 @@ func TestListObjectNames(t *testing.T) {
func TestCreateObject(t *testing.T) { func TestCreateObject(t *testing.T) {
th.SetupHTTP() th.SetupHTTP()
defer th.TeardownHTTP() 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"} 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) th.AssertNoErr(t, res.Err)
} }
func TestCreateObjectWithoutContentType(t *testing.T) { func TestCreateObjectWithoutContentType(t *testing.T) {
th.SetupHTTP() th.SetupHTTP()
defer th.TeardownHTTP() defer th.TeardownHTTP()
HandleCreateTypelessObjectSuccessfully(t)
content := bytes.NewBufferString("The sky was the color of television, tuned to a dead channel.") content := "The sky was the color of television, tuned to a dead channel."
res := Create(fake.ServiceClient(), "testContainer", "testObject", content, &CreateOpts{})
HandleCreateTypelessObjectSuccessfully(t, content)
res := Create(fake.ServiceClient(), "testContainer", "testObject", strings.NewReader(content), &CreateOpts{})
th.AssertNoErr(t, res.Err) 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) { func TestCopyObject(t *testing.T) {
th.SetupHTTP() th.SetupHTTP()
defer th.TeardownHTTP() defer th.TeardownHTTP()

View File

@@ -67,7 +67,7 @@ const FindOutput = `
"events": [ "events": [
{ {
"resource_name": "hello_world", "resource_name": "hello_world",
"event_time": "2015-02-05T21:33:11Z", "event_time": "2015-02-05T21:33:11",
"links": [ "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", "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", "resource_name": "hello_world",
"event_time": "2015-02-05T21:33:27Z", "event_time": "2015-02-05T21:33:27",
"links": [ "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", "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": [ "events": [
{ {
"resource_name": "hello_world", "resource_name": "hello_world",
"event_time": "2015-02-05T21:33:11Z", "event_time": "2015-02-05T21:33:11",
"links": [ "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", "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", "resource_name": "hello_world",
"event_time": "2015-02-05T21:33:27Z", "event_time": "2015-02-05T21:33:27",
"links": [ "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", "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": [ "events": [
{ {
"resource_name": "hello_world", "resource_name": "hello_world",
"event_time": "2015-02-05T21:33:11Z", "event_time": "2015-02-05T21:33:11",
"links": [ "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", "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", "resource_name": "hello_world",
"event_time": "2015-02-05T21:33:27Z", "event_time": "2015-02-05T21:33:27",
"links": [ "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", "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":{ "event":{
"resource_name": "hello_world", "resource_name": "hello_world",
"event_time": "2015-02-05T21:33:27Z", "event_time": "2015-02-05T21:33:27",
"links": [ "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", "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",

View File

@@ -57,7 +57,7 @@ func (r FindResult) Extract() ([]Event, error) {
for i, eventRaw := range events { for i, eventRaw := range events {
event := eventRaw.(map[string]interface{}) event := eventRaw.(map[string]interface{})
if date, ok := event["event_time"]; ok && date != nil { 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 { if err != nil {
return nil, err return nil, err
} }
@@ -121,7 +121,7 @@ func ExtractEvents(page pagination.Page) ([]Event, error) {
for i, eventRaw := range events { for i, eventRaw := range events {
event := eventRaw.(map[string]interface{}) event := eventRaw.(map[string]interface{})
if date, ok := event["event_time"]; ok && date != nil { 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 { if err != nil {
return nil, err return nil, err
} }
@@ -161,7 +161,7 @@ func (r GetResult) Extract() (*Event, error) {
event := r.Body.(map[string]interface{})["event"].(map[string]interface{}) event := r.Body.(map[string]interface{})["event"].(map[string]interface{})
if date, ok := event["event_time"]; ok && date != nil { 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 { if err != nil {
return nil, err return nil, err
} }

View File

@@ -53,7 +53,7 @@ const FindOutput = `
], ],
"logical_resource_id": "hello_world", "logical_resource_id": "hello_world",
"resource_status_reason": "state changed", "resource_status_reason": "state changed",
"updated_time": "2015-02-05T21:33:11Z", "updated_time": "2015-02-05T21:33:11",
"required_by": [], "required_by": [],
"resource_status": "CREATE_IN_PROGRESS", "resource_status": "CREATE_IN_PROGRESS",
"physical_resource_id": "49181cd6-169a-4130-9455-31185bbfc5bf", "physical_resource_id": "49181cd6-169a-4130-9455-31185bbfc5bf",
@@ -117,7 +117,7 @@ const ListOutput = `{
], ],
"logical_resource_id": "hello_world", "logical_resource_id": "hello_world",
"resource_status_reason": "state changed", "resource_status_reason": "state changed",
"updated_time": "2015-02-05T21:33:11Z", "updated_time": "2015-02-05T21:33:11",
"required_by": [], "required_by": [],
"resource_status": "CREATE_IN_PROGRESS", "resource_status": "CREATE_IN_PROGRESS",
"physical_resource_id": "49181cd6-169a-4130-9455-31185bbfc5bf", "physical_resource_id": "49181cd6-169a-4130-9455-31185bbfc5bf",
@@ -188,7 +188,7 @@ const GetOutput = `
], ],
"logical_resource_id": "wordpress_instance", "logical_resource_id": "wordpress_instance",
"resource_status": "CREATE_COMPLETE", "resource_status": "CREATE_COMPLETE",
"updated_time": "2014-12-10T18:34:35Z", "updated_time": "2014-12-10T18:34:35",
"required_by": [], "required_by": [],
"resource_status_reason": "state changed", "resource_status_reason": "state changed",
"physical_resource_id": "00e3a2fe-c65d-403c-9483-4db9930dd194", "physical_resource_id": "00e3a2fe-c65d-403c-9483-4db9930dd194",

View File

@@ -25,12 +25,6 @@ type ListOptsBuilder interface {
// ListOpts allows the filtering and sorting of paginated collections through // ListOpts allows the filtering and sorting of paginated collections through
// the API. Marker and Limit are used for pagination. // the API. Marker and Limit are used for pagination.
type ListOpts struct { 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. // Include resources from nest stacks up to Depth levels of recursion.
Depth int `q:"nested_depth"` 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 { createPageFn := func(r pagination.PageResult) pagination.Page {
p := ResourcePage{pagination.MarkerPageBase{PageResult: r}} return ResourcePage{pagination.SinglePageBase(r)}
p.MarkerPageBase.Owner = p
return p
} }
return pagination.NewPager(client, url, createPageFn) return pagination.NewPager(client, url, createPageFn)

View File

@@ -48,7 +48,7 @@ func (r FindResult) Extract() ([]Resource, error) {
for i, resourceRaw := range resources { for i, resourceRaw := range resources {
resource := resourceRaw.(map[string]interface{}) resource := resourceRaw.(map[string]interface{})
if date, ok := resource["updated_time"]; ok && date != nil { 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 { if err != nil {
return nil, err 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 // 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. // data provided through the ExtractResources call.
type ResourcePage struct { type ResourcePage struct {
pagination.MarkerPageBase pagination.SinglePageBase
} }
// IsEmpty returns true if a page contains no Server results. // 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 { for i, resourceRaw := range resources {
resource := resourceRaw.(map[string]interface{}) resource := resourceRaw.(map[string]interface{})
if date, ok := resource["updated_time"]; ok && date != nil { 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 { if err != nil {
return nil, err return nil, err
} }
@@ -143,7 +143,7 @@ func (r GetResult) Extract() (*Resource, error) {
resource := r.Body.(map[string]interface{})["resource"].(map[string]interface{}) resource := r.Body.(map[string]interface{})["resource"].(map[string]interface{})
if date, ok := resource["updated_time"]; ok && date != nil { 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 { if err != nil {
return nil, err return nil, err
} }

View File

@@ -95,7 +95,7 @@ const FullListOutput = `
], ],
"stack_status_reason": "Stack CREATE completed successfully", "stack_status_reason": "Stack CREATE completed successfully",
"stack_name": "postman_stack", "stack_name": "postman_stack",
"creation_time": "2015-02-03T20:07:39Z", "creation_time": "2015-02-03T20:07:39",
"updated_time": null, "updated_time": null,
"stack_status": "CREATE_COMPLETE", "stack_status": "CREATE_COMPLETE",
"id": "16ef0584-4458-41eb-87c8-0dc8d5f66c87" "id": "16ef0584-4458-41eb-87c8-0dc8d5f66c87"
@@ -110,8 +110,8 @@ const FullListOutput = `
], ],
"stack_status_reason": "Stack successfully updated", "stack_status_reason": "Stack successfully updated",
"stack_name": "gophercloud-test-stack-2", "stack_name": "gophercloud-test-stack-2",
"creation_time": "2014-12-11T17:39:16Z", "creation_time": "2014-12-11T17:39:16",
"updated_time": "2014-12-11T17:40:37Z", "updated_time": "2014-12-11T17:40:37",
"stack_status": "UPDATE_COMPLETE", "stack_status": "UPDATE_COMPLETE",
"id": "db6977b2-27aa-4775-9ae7-6213212d4ada" "id": "db6977b2-27aa-4775-9ae7-6213212d4ada"
} }
@@ -181,7 +181,7 @@ const GetOutput = `
"stack_status_reason": "Stack CREATE completed successfully", "stack_status_reason": "Stack CREATE completed successfully",
"stack_name": "postman_stack", "stack_name": "postman_stack",
"outputs": [], "outputs": [],
"creation_time": "2015-02-03T20:07:39Z", "creation_time": "2015-02-03T20:07:39",
"links": [ "links": [
{ {
"href": "http://166.76.160.117:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/16ef0584-4458-41eb-87c8-0dc8d5f66c87", "href": "http://166.76.160.117:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/16ef0584-4458-41eb-87c8-0dc8d5f66c87",

View File

@@ -100,7 +100,7 @@ func ExtractStacks(page pagination.Page) ([]ListedStack, error) {
thisStack := (rawStacks[i]).(map[string]interface{}) thisStack := (rawStacks[i]).(map[string]interface{})
if t, ok := thisStack["creation_time"].(string); ok && t != "" { 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 { if err != nil {
return res.Stacks, err return res.Stacks, err
} }
@@ -108,7 +108,7 @@ func ExtractStacks(page pagination.Page) ([]ListedStack, error) {
} }
if t, ok := thisStack["updated_time"].(string); ok && t != "" { 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 { if err != nil {
return res.Stacks, err return res.Stacks, err
} }
@@ -170,7 +170,7 @@ func (r GetResult) Extract() (*RetrievedStack, error) {
b := r.Body.(map[string]interface{})["stack"].(map[string]interface{}) b := r.Body.(map[string]interface{})["stack"].(map[string]interface{})
if date, ok := b["creation_time"]; ok && date != nil { 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 { if err != nil {
return nil, err return nil, err
} }
@@ -178,7 +178,7 @@ func (r GetResult) Extract() (*RetrievedStack, error) {
} }
if date, ok := b["updated_time"]; ok && date != nil { 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 { if err != nil {
return nil, err return nil, err
} }
@@ -249,7 +249,7 @@ func (r PreviewResult) Extract() (*PreviewedStack, error) {
b := r.Body.(map[string]interface{})["stack"].(map[string]interface{}) b := r.Body.(map[string]interface{})["stack"].(map[string]interface{})
if date, ok := b["creation_time"]; ok && date != nil { 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 { if err != nil {
return nil, err return nil, err
} }
@@ -257,7 +257,7 @@ func (r PreviewResult) Extract() (*PreviewedStack, error) {
} }
if date, ok := b["updated_time"]; ok && date != nil { 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 { if err != nil {
return nil, err return nil, err
} }

View File

@@ -36,13 +36,19 @@ func PageResultFrom(resp *http.Response) (PageResult, error) {
parsedBody = rawBody 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{ return PageResult{
Result: gophercloud.Result{ Result: gophercloud.Result{
Body: parsedBody, Body: body,
Header: resp.Header, Header: resp.Header,
}, },
URL: *resp.Request.URL, URL: *resp.Request.URL,
}, err }
} }
// Request performs an HTTP request and extracts the http.Response from the result. // Request performs an HTTP request and extracts the http.Response from the result.

View File

@@ -174,8 +174,10 @@ func (p Pager) AllPages() (Page, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
// Remove the trailing comma. if len(pagesSlice) > 0 {
pagesSlice = pagesSlice[:len(pagesSlice)-1] // Remove the trailing comma.
pagesSlice = pagesSlice[:len(pagesSlice)-1]
}
var b []byte var b []byte
// Combine the slice of slices in to a single slice. // Combine the slice of slices in to a single slice.
for _, slice := range pagesSlice { for _, slice := range pagesSlice {

View File

@@ -85,9 +85,9 @@ type RequestOpts struct {
// content type of the request will default to "application/json" unless overridden by MoreHeaders. // 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. // It's an error to specify both a JSONBody and a RawBody.
JSONBody interface{} 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. // 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 // JSONResponse, if provided, will be populated with the contents of the response body parsed as
// JSON. // JSON.
@@ -124,11 +124,11 @@ var applicationJSON = "application/json"
// Request performs an HTTP request using the ProviderClient's current HTTPClient. An authentication // Request performs an HTTP request using the ProviderClient's current HTTPClient. An authentication
// header will automatically be provided. // header will automatically be provided.
func (client *ProviderClient) Request(method, url string, options RequestOpts) (*http.Response, error) { func (client *ProviderClient) Request(method, url string, options RequestOpts) (*http.Response, error) {
var body io.Reader var body io.ReadSeeker
var contentType *string var contentType *string
// Derive the content body by either encoding an arbitrary object as JSON, or by taking a provided // 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.JSONBody != nil {
if options.RawBody != nil { if options.RawBody != nil {
panic("Please provide only one of JSONBody or RawBody to gophercloud.Request().") 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 { if err != nil {
return nil, fmt.Errorf("Error trying to re-authenticate: %s", err) 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) resp, err = client.Request(method, url, options)
if err != nil { if err != nil {
return nil, fmt.Errorf("Successfully re-authenticated, but got error executing request: %s", err) 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. // Parse the response body as JSON, if requested to do so.
if options.JSONResponse != nil { if options.JSONResponse != nil {
defer resp.Body.Close() 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 return resp, nil
@@ -260,7 +265,7 @@ func (client *ProviderClient) Post(url string, JSONBody interface{}, JSONRespons
opts = &RequestOpts{} opts = &RequestOpts{}
} }
if v, ok := (JSONBody).(io.Reader); ok { if v, ok := (JSONBody).(io.ReadSeeker); ok {
opts.RawBody = v opts.RawBody = v
} else if JSONBody != nil { } else if JSONBody != nil {
opts.JSONBody = JSONBody opts.JSONBody = JSONBody
@@ -278,7 +283,7 @@ func (client *ProviderClient) Put(url string, JSONBody interface{}, JSONResponse
opts = &RequestOpts{} opts = &RequestOpts{}
} }
if v, ok := (JSONBody).(io.Reader); ok { if v, ok := (JSONBody).(io.ReadSeeker); ok {
opts.RawBody = v opts.RawBody = v
} else if JSONBody != nil { } else if JSONBody != nil {
opts.JSONBody = JSONBody opts.JSONBody = JSONBody

View File

@@ -3,7 +3,8 @@ package volumes
import ( import (
"testing" "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" "github.com/rackspace/gophercloud/pagination"
th "github.com/rackspace/gophercloud/testhelper" th "github.com/rackspace/gophercloud/testhelper"
fake "github.com/rackspace/gophercloud/testhelper/client" fake "github.com/rackspace/gophercloud/testhelper/client"
@@ -64,7 +65,7 @@ func TestCreate(t *testing.T) {
os.MockCreateResponse(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.AssertNoErr(t, err)
th.AssertEquals(t, n.Size, 4) th.AssertEquals(t, n.Size, 4)
@@ -72,12 +73,12 @@ func TestCreate(t *testing.T) {
} }
func TestSizeRange(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 { if err == nil {
t.Fatalf("Expected error, got none") 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 { if err == nil {
t.Fatalf("Expected error, got none") t.Fatalf("Expected error, got none")
} }

View File

@@ -33,6 +33,8 @@ func TestCreateOpts(t *testing.T) {
"name": "createdserver", "name": "createdserver",
"imageRef": "asdfasdfasdf", "imageRef": "asdfasdfasdf",
"flavorRef": "performance1-1", "flavorRef": "performance1-1",
"flavorName": "",
"imageName": "",
"block_device_mapping_v2":[ "block_device_mapping_v2":[
{ {
"uuid":"123456", "uuid":"123456",

View File

@@ -36,11 +36,8 @@ func ListDetail(client *gophercloud.ServiceClient, opts os.ListOptsBuilder) pagi
} }
// Get returns details about a single flavor, identity by ID. // Get returns details about a single flavor, identity by ID.
func Get(client *gophercloud.ServiceClient, id string) os.GetResult { func Get(client *gophercloud.ServiceClient, id string) GetResult {
return os.Get(client, id) var res GetResult
} _, res.Err = client.Get(getURL(client, id), &res.Body, nil)
return res
// ExtractFlavors interprets a page of List results as Flavors.
func ExtractFlavors(page pagination.Page) ([]os.Flavor, error) {
return os.ExtractFlavors(page)
} }

View File

@@ -2,10 +2,6 @@
package flavors package flavors
import (
os "github.com/rackspace/gophercloud/openstack/compute/v2/flavors"
)
// ListOutput is a sample response of a flavor List request. // ListOutput is a sample response of a flavor List request.
const ListOutput = ` const ListOutput = `
{ {
@@ -103,7 +99,7 @@ const GetOutput = `
// Performance1Flavor is the expected result of parsing GetOutput, or the first element of // Performance1Flavor is the expected result of parsing GetOutput, or the first element of
// ListOutput. // ListOutput.
var Performance1Flavor = os.Flavor{ var Performance1Flavor = Flavor{
ID: "performance1-1", ID: "performance1-1",
Disk: 20, Disk: 20,
RAM: 1024, RAM: 1024,
@@ -111,10 +107,16 @@ var Performance1Flavor = os.Flavor{
RxTxFactor: 200.0, RxTxFactor: 200.0,
Swap: 0, Swap: 0,
VCPUs: 1, VCPUs: 1,
ExtraSpecs: ExtraSpecs{
NumDataDisks: 0,
Class: "performance1",
DiskIOIndex: 0,
PolicyClass: "performance_flavor",
},
} }
// Performance2Flavor is the second result expected from parsing ListOutput. // Performance2Flavor is the second result expected from parsing ListOutput.
var Performance2Flavor = os.Flavor{ var Performance2Flavor = Flavor{
ID: "performance1-2", ID: "performance1-2",
Disk: 40, Disk: 40,
RAM: 2048, RAM: 2048,
@@ -122,8 +124,14 @@ var Performance2Flavor = os.Flavor{
RxTxFactor: 400.0, RxTxFactor: 400.0,
Swap: 0, Swap: 0,
VCPUs: 2, 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 // ExpectedFlavorSlice is the slice of Flavor structs that are expected to be parsed from
// ListOutput. // ListOutput.
var ExpectedFlavorSlice = []os.Flavor{Performance1Flavor, Performance2Flavor} var ExpectedFlavorSlice = []Flavor{Performance1Flavor, Performance2Flavor}

View File

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

View File

@@ -0,0 +1,9 @@
package flavors
import (
"github.com/rackspace/gophercloud"
)
func getURL(client *gophercloud.ServiceClient, id string) string {
return client.ServiceURL("flavors", id)
}

View File

@@ -12,13 +12,24 @@ type CreateOpts struct {
// Name [required] is the name to assign to the newly launched server. // Name [required] is the name to assign to the newly launched server.
Name string Name string
// ImageRef [required] is the ID or full URL to the image that contains the server's OS and initial state. // ImageRef [optional; required if ImageName is not provided] is the ID or full
// Optional if using the boot-from-volume extension. // URL to the image that contains the server's OS and initial state.
// Also optional if using the boot-from-volume extension.
ImageRef string 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 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 [optional] lists the names of the security groups to which this server should belong.
SecurityGroups []string 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 [optional] contains key-value pairs (up to 255 bytes each) to attach to the server.
Metadata map[string]string Metadata map[string]string
// Personality [optional] includes the path and contents of a file to inject into the server at launch. // Personality [optional] includes files to inject into the server at launch.
// The maximum size of the file is 255 bytes (decoded). // Create will base64-encode file contents for you.
Personality []byte Personality os.Personality
// ConfigDrive [optional] enables metadata injection through a configuration drive. // ConfigDrive [optional] enables metadata injection through a configuration drive.
ConfigDrive bool ConfigDrive bool
@@ -58,7 +69,7 @@ type CreateOpts struct {
DiskConfig diskconfig.DiskConfig DiskConfig diskconfig.DiskConfig
// BlockDevice [optional] will create the server from a volume, which is created from an image, // 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 BlockDevice []bootfromvolume.BlockDevice
} }
@@ -68,7 +79,9 @@ func (opts CreateOpts) ToServerCreateMap() (map[string]interface{}, error) {
base := os.CreateOpts{ base := os.CreateOpts{
Name: opts.Name, Name: opts.Name,
ImageRef: opts.ImageRef, ImageRef: opts.ImageRef,
ImageName: opts.ImageName,
FlavorRef: opts.FlavorRef, FlavorRef: opts.FlavorRef,
FlavorName: opts.FlavorName,
SecurityGroups: opts.SecurityGroups, SecurityGroups: opts.SecurityGroups,
UserData: opts.UserData, UserData: opts.UserData,
AvailabilityZone: opts.AvailabilityZone, 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 // key_name doesn't actually come from the extension (or at least isn't documented there) so
// we need to add it manually. // we need to add it manually.
serverMap := res["server"].(map[string]interface{}) serverMap := res["server"].(map[string]interface{})
serverMap["key_name"] = opts.KeyPair if opts.KeyPair != "" {
serverMap["key_name"] = opts.KeyPair
}
return res, nil 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 [optional] contains key-value pairs (up to 255 bytes each) to attach to the server.
Metadata map[string]string Metadata map[string]string
// Personality [optional] includes the path and contents of a file to inject into the server at launch. // Personality [optional] includes files to inject into the server at launch.
// The maximum size of the file is 255 bytes (decoded). // Rebuild will base64-encode file contents for you.
Personality []byte Personality os.Personality
// Rackspace-specific stuff begins here. // Rackspace-specific stuff begins here.

View File

@@ -22,6 +22,8 @@ func TestCreateOpts(t *testing.T) {
"name": "createdserver", "name": "createdserver",
"imageRef": "image-id", "imageRef": "image-id",
"flavorRef": "flavor-id", "flavorRef": "flavor-id",
"flavorName": "",
"imageName": "",
"key_name": "mykey", "key_name": "mykey",
"OS-DCF:diskConfig": "MANUAL" "OS-DCF:diskConfig": "MANUAL"
} }

View File

@@ -3,24 +3,53 @@ package volumeattach
import ( import (
"testing" "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" "github.com/rackspace/gophercloud/pagination"
th "github.com/rackspace/gophercloud/testhelper" th "github.com/rackspace/gophercloud/testhelper"
"github.com/rackspace/gophercloud/testhelper/client" "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) { func TestList(t *testing.T) {
th.SetupHTTP() th.SetupHTTP()
defer th.TeardownHTTP() defer th.TeardownHTTP()
os.HandleListSuccessfully(t) fixtures.HandleListSuccessfully(t)
serverId := "4d8c3732-a248-40ed-bebc-539a6ffd25c0" serverId := "4d8c3732-a248-40ed-bebc-539a6ffd25c0"
count := 0 count := 0
err := List(client.ServiceClient(), serverId).EachPage(func(page pagination.Page) (bool, error) { err := List(client.ServiceClient(), serverId).EachPage(func(page pagination.Page) (bool, error) {
count++ count++
actual, err := os.ExtractVolumeAttachments(page) actual, err := volumeattach.ExtractVolumeAttachments(page)
th.AssertNoErr(t, err) th.AssertNoErr(t, err)
th.CheckDeepEquals(t, os.ExpectedVolumeAttachmentSlice, actual) th.CheckDeepEquals(t, ExpectedVolumeAttachmentSlice, actual)
return true, nil return true, nil
}) })
@@ -31,33 +60,33 @@ func TestList(t *testing.T) {
func TestCreate(t *testing.T) { func TestCreate(t *testing.T) {
th.SetupHTTP() th.SetupHTTP()
defer th.TeardownHTTP() defer th.TeardownHTTP()
os.HandleCreateSuccessfully(t) fixtures.HandleCreateSuccessfully(t)
serverId := "4d8c3732-a248-40ed-bebc-539a6ffd25c0" 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", Device: "/dev/vdc",
VolumeID: "a26887c6-c47b-4654-abb5-dfadf7d3f804", VolumeID: "a26887c6-c47b-4654-abb5-dfadf7d3f804",
}).Extract() }).Extract()
th.AssertNoErr(t, err) th.AssertNoErr(t, err)
th.CheckDeepEquals(t, &os.CreatedVolumeAttachment, actual) th.CheckDeepEquals(t, &CreatedVolumeAttachment, actual)
} }
func TestGet(t *testing.T) { func TestGet(t *testing.T) {
th.SetupHTTP() th.SetupHTTP()
defer th.TeardownHTTP() defer th.TeardownHTTP()
os.HandleGetSuccessfully(t) fixtures.HandleGetSuccessfully(t)
aId := "a26887c6-c47b-4654-abb5-dfadf7d3f804" aId := "a26887c6-c47b-4654-abb5-dfadf7d3f804"
serverId := "4d8c3732-a248-40ed-bebc-539a6ffd25c0" serverId := "4d8c3732-a248-40ed-bebc-539a6ffd25c0"
actual, err := Get(client.ServiceClient(), serverId, aId).Extract() actual, err := Get(client.ServiceClient(), serverId, aId).Extract()
th.AssertNoErr(t, err) th.AssertNoErr(t, err)
th.CheckDeepEquals(t, &os.SecondVolumeAttachment, actual) th.CheckDeepEquals(t, &SecondVolumeAttachment, actual)
} }
func TestDelete(t *testing.T) { func TestDelete(t *testing.T) {
th.SetupHTTP() th.SetupHTTP()
defer th.TeardownHTTP() defer th.TeardownHTTP()
os.HandleDeleteSuccessfully(t) fixtures.HandleDeleteSuccessfully(t)
aId := "a26887c6-c47b-4654-abb5-dfadf7d3f804" aId := "a26887c6-c47b-4654-abb5-dfadf7d3f804"
serverId := "4d8c3732-a248-40ed-bebc-539a6ffd25c0" serverId := "4d8c3732-a248-40ed-bebc-539a6ffd25c0"

View File

@@ -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) { func mockBatchDeleteResponse(t *testing.T, lbID int, ids []int) {
th.Mux.HandleFunc(_rootURL(lbID), func(w http.ResponseWriter, r *http.Request) { th.Mux.HandleFunc(_rootURL(lbID), func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "DELETE") th.TestMethod(t, r, "DELETE")

View File

@@ -119,12 +119,7 @@ func Create(client *gophercloud.ServiceClient, loadBalancerID int, opts CreateOp
return res return res
} }
pr, err := pagination.PageResultFrom(resp) pr := pagination.PageResultFromParsed(resp, res.Body)
if err != nil {
res.Err = err
return res
}
return CreateResult{pagination.SinglePageBase(pr)} return CreateResult{pagination.SinglePageBase(pr)}
} }

View File

@@ -108,6 +108,38 @@ func TestCreate(t *testing.T) {
th.CheckDeepEquals(t, expected, actual) 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) { func TestBulkDelete(t *testing.T) {
th.SetupHTTP() th.SetupHTTP()
defer th.TeardownHTTP() defer th.TeardownHTTP()

View File

@@ -126,6 +126,9 @@ type CreateResult struct {
// ExtractNodes extracts a slice of Node structs from a CreateResult. // ExtractNodes extracts a slice of Node structs from a CreateResult.
func (res CreateResult) ExtractNodes() ([]Node, error) { func (res CreateResult) ExtractNodes() ([]Node, error) {
if res.Err != nil {
return nil, res.Err
}
return commonExtractNodes(res.Body) return commonExtractNodes(res.Body)
} }

View File

@@ -46,6 +46,7 @@ func mockEnableResponse(t *testing.T, lbID int) {
`) `)
w.WriteHeader(http.StatusAccepted) w.WriteHeader(http.StatusAccepted)
fmt.Fprintf(w, `{}`)
}) })
} }

View File

@@ -63,6 +63,7 @@ func mockUpdateResponse(t *testing.T, lbID int) {
`) `)
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, `{}`)
}) })
} }

View File

@@ -49,6 +49,7 @@ func mockCreateResponse(t *testing.T, lbID int) {
`) `)
w.WriteHeader(http.StatusAccepted) w.WriteHeader(http.StatusAccepted)
fmt.Fprintf(w, `{}`)
}) })
} }

View File

@@ -203,8 +203,17 @@ func TestCreateWithOptionalFields(t *testing.T) {
} }
} }
`) `)
w.WriteHeader(http.StatusCreated) w.WriteHeader(http.StatusCreated)
fmt.Fprintf(w, `
{
"network": {
"name": "sample_network",
"admin_state_up": true,
"shared": true,
"tenant_id": "12345"
}
}
`)
}) })
iTrue := true iTrue := true

View File

@@ -43,7 +43,9 @@ func Delete(c *gophercloud.ServiceClient, opts DeleteOptsBuilder) DeleteResult {
JSONBody: reqBody, JSONBody: reqBody,
JSONResponse: &res.Body, JSONResponse: &res.Body,
}) })
res.Header = resp.Header if resp != nil {
res.Header = resp.Header
}
res.Err = err res.Err = err
return res return res
} }

View File

@@ -53,7 +53,9 @@ func Enable(c *gophercloud.ServiceClient, containerName string, opts EnableOptsB
MoreHeaders: h, MoreHeaders: h,
OkCodes: []int{201, 202, 204}, OkCodes: []int{201, 202, 204},
}) })
res.Header = resp.Header if resp != nil {
res.Header = resp.Header
}
res.Err = err res.Err = err
return res return res
} }
@@ -66,7 +68,9 @@ func Get(c *gophercloud.ServiceClient, containerName string) GetResult {
resp, err := c.Request("HEAD", getURL(c, containerName), gophercloud.RequestOpts{ resp, err := c.Request("HEAD", getURL(c, containerName), gophercloud.RequestOpts{
OkCodes: []int{200, 204}, OkCodes: []int{200, 204},
}) })
res.Header = resp.Header if resp != nil {
res.Header = resp.Header
}
res.Err = err res.Err = err
return res return res
} }
@@ -149,7 +153,9 @@ func Update(c *gophercloud.ServiceClient, containerName string, opts UpdateOptsB
MoreHeaders: h, MoreHeaders: h,
OkCodes: []int{202, 204}, OkCodes: []int{202, 204},
}) })
res.Header = resp.Header if resp != nil {
res.Header = resp.Header
}
res.Err = err res.Err = err
return res return res
} }

View File

@@ -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. // 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) return os.Create(c, containerName, objectName, content, opts)
} }

View File

@@ -1,7 +1,7 @@
package objects package objects
import ( import (
"bytes" "strings"
"testing" "testing"
os "github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects" os "github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects"
@@ -66,21 +66,23 @@ func TestListObjectNames(t *testing.T) {
func TestCreateObject(t *testing.T) { func TestCreateObject(t *testing.T) {
th.SetupHTTP() th.SetupHTTP()
defer th.TeardownHTTP() 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"} 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) th.AssertNoErr(t, res.Err)
} }
func TestCreateObjectWithoutContentType(t *testing.T) { func TestCreateObjectWithoutContentType(t *testing.T) {
th.SetupHTTP() th.SetupHTTP()
defer th.TeardownHTTP() defer th.TeardownHTTP()
os.HandleCreateTypelessObjectSuccessfully(t)
content := bytes.NewBufferString("The sky was the color of television, tuned to a dead channel.") content := "The sky was the color of television, tuned to a dead channel."
res := Create(fake.ServiceClient(), "testContainer", "testObject", content, &os.CreateOpts{}) os.HandleCreateTypelessObjectSuccessfully(t, content)
res := Create(fake.ServiceClient(), "testContainer", "testObject", strings.NewReader(content), &os.CreateOpts{})
th.AssertNoErr(t, res.Err) th.AssertNoErr(t, res.Err)
} }

View File

@@ -113,6 +113,9 @@ func DecodeHeader(from, to interface{}) error {
// RFC3339Milli describes a common time format used by some API responses. // RFC3339Milli describes a common time format used by some API responses.
const RFC3339Milli = "2006-01-02T15:04:05.999999Z" 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 Link is an internal type to be used in packages of collection resources that are
paginated in a certain way. paginated in a certain way.