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",
"Comment": "v1.0.0-569-gf3ced00",
"Rev": "f3ced00552c1c7d4a6184500af9062cfb4ff4463"
"Comment": "v1.0.0-665-gf928634",
"Rev": "f92863476c034f851073599c09d90cd61ee95b3d"
},
{
"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
```
4. Checkout the latest development branch ([click here](/branches) to see all
the branches):
4. Checkout the latest development branch:
```bash
git checkout release/v1.0.1
git checkout master
```
5. If you're working on something (discussed more in detail below), you will

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
import (
"fmt"
"testing"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/acceptance/tools"
"github.com/rackspace/gophercloud/openstack/compute/v2/extensions/schedulerhints"
"github.com/rackspace/gophercloud/openstack/compute/v2/extensions/servergroups"
"github.com/rackspace/gophercloud/openstack/compute/v2/servers"
th "github.com/rackspace/gophercloud/testhelper"
)
func createServerGroup(t *testing.T, computeClient *gophercloud.ServiceClient) (*servergroups.ServerGroup, error) {
@@ -36,7 +41,53 @@ func getServerGroup(t *testing.T, computeClient *gophercloud.ServiceClient, sgID
return nil
}
func createServerInGroup(t *testing.T, computeClient *gophercloud.ServiceClient, choices *ComputeChoices, serverGroup *servergroups.ServerGroup) (*servers.Server, error) {
if testing.Short() {
t.Skip("Skipping test that requires server creation in short mode.")
}
name := tools.RandomString("ACPTTEST", 16)
t.Logf("Attempting to create server: %s\n", name)
pwd := tools.MakeNewPassword("")
serverCreateOpts := servers.CreateOpts{
Name: name,
FlavorRef: choices.FlavorID,
ImageRef: choices.ImageID,
AdminPass: pwd,
}
server, err := servers.Create(computeClient, schedulerhints.CreateOptsExt{
serverCreateOpts,
schedulerhints.SchedulerHints{
Group: serverGroup.ID,
},
}).Extract()
if err != nil {
t.Fatalf("Unable to create server: %v", err)
}
th.AssertEquals(t, pwd, server.AdminPass)
return server, err
}
func verifySchedulerWorked(t *testing.T, firstServer, secondServer *servers.Server) error {
t.Logf("First server hostID: %v", firstServer.HostID)
t.Logf("Second server hostID: %v", secondServer.HostID)
if firstServer.HostID == secondServer.HostID {
return nil
}
return fmt.Errorf("%s and %s were not scheduled on the same host.", firstServer.ID, secondServer.ID)
}
func TestServerGroups(t *testing.T) {
choices, err := ComputeChoicesFromEnv()
if err != nil {
t.Fatal(err)
}
computeClient, err := newClient()
if err != nil {
t.Fatalf("Unable to create a compute client: %v", err)
@@ -48,11 +99,45 @@ func TestServerGroups(t *testing.T) {
}
defer func() {
servergroups.Delete(computeClient, sg.ID)
t.Logf("ServerGroup deleted.")
t.Logf("Server Group deleted.")
}()
err = getServerGroup(t, computeClient, sg.ID)
if err != nil {
t.Fatalf("Unable to get server group: %v", err)
}
firstServer, err := createServerInGroup(t, computeClient, choices, sg)
if err != nil {
t.Fatalf("Unable to create server: %v", err)
}
defer func() {
servers.Delete(computeClient, firstServer.ID)
t.Logf("Server deleted.")
}()
if err = waitForStatus(computeClient, firstServer, "ACTIVE"); err != nil {
t.Fatalf("Unable to wait for server: %v", err)
}
firstServer, err = servers.Get(computeClient, firstServer.ID).Extract()
secondServer, err := createServerInGroup(t, computeClient, choices, sg)
if err != nil {
t.Fatalf("Unable to create server: %v", err)
}
defer func() {
servers.Delete(computeClient, secondServer.ID)
t.Logf("Server deleted.")
}()
if err = waitForStatus(computeClient, secondServer, "ACTIVE"); err != nil {
t.Fatalf("Unable to wait for server: %v", err)
}
secondServer, err = servers.Get(computeClient, secondServer.ID).Extract()
if err = verifySchedulerWorked(t, firstServer, secondServer); err != nil {
t.Fatalf("Scheduling did not work: %v", err)
}
}

View File

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

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

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",
"imageRef": "asdfasdfasdf",
"flavorRef": "performance1-1",
"flavorName": "",
"imageName": "",
"block_device_mapping_v2":[
{
"uuid":"123456",

View File

@@ -25,6 +25,8 @@ func TestCreateOpts(t *testing.T) {
"name": "createdserver",
"imageRef": "asdfasdfasdf",
"flavorRef": "performance1-1",
"flavorName": "",
"imageName": "",
"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.WriteHeader(http.StatusAccepted)
fmt.Fprintf(w, `{}`)
})
}
@@ -261,5 +262,6 @@ func mockRemoveServerFromGroupResponse(t *testing.T, serverID string) {
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusAccepted)
fmt.Fprintf(w, `{}`)
})
}

View File

@@ -3,15 +3,44 @@ package volumeattach
import (
"testing"
fixtures "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/volumeattach/testing"
"github.com/rackspace/gophercloud/pagination"
th "github.com/rackspace/gophercloud/testhelper"
"github.com/rackspace/gophercloud/testhelper/client"
)
// FirstVolumeAttachment is the first result in ListOutput.
var FirstVolumeAttachment = VolumeAttachment{
Device: "/dev/vdd",
ID: "a26887c6-c47b-4654-abb5-dfadf7d3f803",
ServerID: "4d8c3732-a248-40ed-bebc-539a6ffd25c0",
VolumeID: "a26887c6-c47b-4654-abb5-dfadf7d3f803",
}
// SecondVolumeAttachment is the first result in ListOutput.
var SecondVolumeAttachment = VolumeAttachment{
Device: "/dev/vdc",
ID: "a26887c6-c47b-4654-abb5-dfadf7d3f804",
ServerID: "4d8c3732-a248-40ed-bebc-539a6ffd25c0",
VolumeID: "a26887c6-c47b-4654-abb5-dfadf7d3f804",
}
// ExpectedVolumeAttachmentSlide is the slice of results that should be parsed
// from ListOutput, in the expected order.
var ExpectedVolumeAttachmentSlice = []VolumeAttachment{FirstVolumeAttachment, SecondVolumeAttachment}
//CreatedVolumeAttachment is the parsed result from CreatedOutput.
var CreatedVolumeAttachment = VolumeAttachment{
Device: "/dev/vdc",
ID: "a26887c6-c47b-4654-abb5-dfadf7d3f804",
ServerID: "4d8c3732-a248-40ed-bebc-539a6ffd25c0",
VolumeID: "a26887c6-c47b-4654-abb5-dfadf7d3f804",
}
func TestList(t *testing.T) {
th.SetupHTTP()
defer th.TeardownHTTP()
HandleListSuccessfully(t)
fixtures.HandleListSuccessfully(t)
serverId := "4d8c3732-a248-40ed-bebc-539a6ffd25c0"
count := 0
@@ -30,7 +59,7 @@ func TestList(t *testing.T) {
func TestCreate(t *testing.T) {
th.SetupHTTP()
defer th.TeardownHTTP()
HandleCreateSuccessfully(t)
fixtures.HandleCreateSuccessfully(t)
serverId := "4d8c3732-a248-40ed-bebc-539a6ffd25c0"
actual, err := Create(client.ServiceClient(), serverId, CreateOpts{
@@ -44,7 +73,7 @@ func TestCreate(t *testing.T) {
func TestGet(t *testing.T) {
th.SetupHTTP()
defer th.TeardownHTTP()
HandleGetSuccessfully(t)
fixtures.HandleGetSuccessfully(t)
aId := "a26887c6-c47b-4654-abb5-dfadf7d3f804"
serverId := "4d8c3732-a248-40ed-bebc-539a6ffd25c0"
@@ -56,7 +85,7 @@ func TestGet(t *testing.T) {
func TestDelete(t *testing.T) {
th.SetupHTTP()
defer th.TeardownHTTP()
HandleDeleteSuccessfully(t)
fixtures.HandleDeleteSuccessfully(t)
aId := "a26887c6-c47b-4654-abb5-dfadf7d3f804"
serverId := "4d8c3732-a248-40ed-bebc-539a6ffd25c0"

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
package volumeattach
package testing
import (
"fmt"
@@ -55,34 +55,6 @@ const CreateOutput = `
}
`
// FirstVolumeAttachment is the first result in ListOutput.
var FirstVolumeAttachment = VolumeAttachment{
Device: "/dev/vdd",
ID: "a26887c6-c47b-4654-abb5-dfadf7d3f803",
ServerID: "4d8c3732-a248-40ed-bebc-539a6ffd25c0",
VolumeID: "a26887c6-c47b-4654-abb5-dfadf7d3f803",
}
// SecondVolumeAttachment is the first result in ListOutput.
var SecondVolumeAttachment = VolumeAttachment{
Device: "/dev/vdc",
ID: "a26887c6-c47b-4654-abb5-dfadf7d3f804",
ServerID: "4d8c3732-a248-40ed-bebc-539a6ffd25c0",
VolumeID: "a26887c6-c47b-4654-abb5-dfadf7d3f804",
}
// ExpectedVolumeAttachmentSlide is the slice of results that should be parsed
// from ListOutput, in the expected order.
var ExpectedVolumeAttachmentSlice = []VolumeAttachment{FirstVolumeAttachment, SecondVolumeAttachment}
// CreatedVolumeAttachment is the parsed result from CreatedOutput.
var CreatedVolumeAttachment = VolumeAttachment{
Device: "/dev/vdc",
ID: "a26887c6-c47b-4654-abb5-dfadf7d3f804",
ServerID: "4d8c3732-a248-40ed-bebc-539a6ffd25c0",
VolumeID: "a26887c6-c47b-4654-abb5-dfadf7d3f804",
}
// HandleListSuccessfully configures the test server to respond to a List request.
func HandleListSuccessfully(t *testing.T) {
th.Mux.HandleFunc("/servers/4d8c3732-a248-40ed-bebc-539a6ffd25c0/os-volume_attachments", func(w http.ResponseWriter, r *http.Request) {

View File

@@ -1,6 +1,8 @@
package flavors
import (
"fmt"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
)
@@ -66,3 +68,36 @@ func Get(client *gophercloud.ServiceClient, id string) GetResult {
_, res.Err = client.Get(getURL(client, id), &res.Body, nil)
return res
}
// IDFromName is a convienience function that returns a flavor's ID given its name.
func IDFromName(client *gophercloud.ServiceClient, name string) (string, error) {
flavorCount := 0
flavorID := ""
if name == "" {
return "", fmt.Errorf("A flavor name must be provided.")
}
pager := ListDetail(client, nil)
pager.EachPage(func(page pagination.Page) (bool, error) {
flavorList, err := ExtractFlavors(page)
if err != nil {
return false, err
}
for _, f := range flavorList {
if f.Name == name {
flavorCount++
flavorID = f.ID
}
}
return true, nil
})
switch flavorCount {
case 0:
return "", fmt.Errorf("Unable to find flavor: %s", name)
case 1:
return flavorID, nil
default:
return "", fmt.Errorf("Found %d flavors matching %s", flavorCount, name)
}
}

View File

@@ -1,6 +1,8 @@
package images
import (
"fmt"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
)
@@ -70,3 +72,38 @@ func Delete(client *gophercloud.ServiceClient, id string) DeleteResult {
_, result.Err = client.Delete(deleteURL(client, id), nil)
return result
}
// IDFromName is a convienience function that returns an image's ID given its name.
func IDFromName(client *gophercloud.ServiceClient, name string) (string, error) {
imageCount := 0
imageID := ""
if name == "" {
return "", fmt.Errorf("An image name must be provided.")
}
pager := ListDetail(client, &ListOpts{
Name: name,
})
pager.EachPage(func(page pagination.Page) (bool, error) {
imageList, err := ExtractImages(page)
if err != nil {
return false, err
}
for _, i := range imageList {
if i.Name == name {
imageCount++
imageID = i.ID
}
}
return true, nil
})
switch imageCount {
case 0:
return "", fmt.Errorf("Unable to find image: %s", name)
case 1:
return imageID, nil
default:
return "", fmt.Errorf("Found %d images matching %s", imageCount, name)
}
}

View File

@@ -2,10 +2,13 @@ package servers
import (
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/openstack/compute/v2/flavors"
"github.com/rackspace/gophercloud/openstack/compute/v2/images"
"github.com/rackspace/gophercloud/pagination"
)
@@ -14,6 +17,7 @@ import (
type ListOptsBuilder interface {
ToServerListQuery() (string, error)
}
// ListOpts allows the filtering and sorting of paginated collections through
// the API. Filtering is achieved by passing in struct field values that map to
// the server attributes you want to see returned. Marker and Limit are used
@@ -45,6 +49,9 @@ type ListOpts struct {
// Integer value for the limit of values to return.
Limit int `q:"limit"`
// Bool to show all tenants
AllTenants bool `q:"all_tenants"`
}
// ToServerListQuery formats a ListOpts into a query string.
@@ -95,18 +102,54 @@ type Network struct {
FixedIP string
}
// Personality is an array of files that are injected into the server at launch.
type Personality []*File
// File is used within CreateOpts and RebuildOpts to inject a file into the server at launch.
// File implements the json.Marshaler interface, so when a Create or Rebuild operation is requested,
// json.Marshal will call File's MarshalJSON method.
type File struct {
// Path of the file
Path string
// Contents of the file. Maximum content size is 255 bytes.
Contents []byte
}
// MarshalJSON marshals the escaped file, base64 encoding the contents.
func (f *File) MarshalJSON() ([]byte, error) {
file := struct {
Path string `json:"path"`
Contents string `json:"contents"`
}{
Path: f.Path,
Contents: base64.StdEncoding.EncodeToString(f.Contents),
}
return json.Marshal(file)
}
// CreateOpts specifies server creation parameters.
type CreateOpts struct {
// Name [required] is the name to assign to the newly launched server.
Name string
// ImageRef [required] is the ID or full URL to the image that contains the server's OS and initial state.
// Optional if using the boot-from-volume extension.
// ImageRef [optional; required if ImageName is not provided] is the ID or full
// URL to the image that contains the server's OS and initial state.
// Also optional if using the boot-from-volume extension.
ImageRef string
// FlavorRef [required] is the ID or full URL to the flavor that describes the server's specs.
// ImageName [optional; required if ImageRef is not provided] is the name of the
// image that contains the server's OS and initial state.
// Also optional if using the boot-from-volume extension.
ImageName string
// FlavorRef [optional; required if FlavorName is not provided] is the ID or
// full URL to the flavor that describes the server's specs.
FlavorRef string
// FlavorName [optional; required if FlavorRef is not provided] is the name of
// the flavor that describes the server's specs.
FlavorName string
// SecurityGroups [optional] lists the names of the security groups to which this server should belong.
SecurityGroups []string
@@ -124,9 +167,9 @@ type CreateOpts struct {
// Metadata [optional] contains key-value pairs (up to 255 bytes each) to attach to the server.
Metadata map[string]string
// Personality [optional] includes the path and contents of a file to inject into the server at launch.
// The maximum size of the file is 255 bytes (decoded).
Personality []byte
// Personality [optional] includes files to inject into the server at launch.
// Create will base64-encode file contents for you.
Personality Personality
// ConfigDrive [optional] enables metadata injection through a configuration drive.
ConfigDrive bool
@@ -148,16 +191,14 @@ func (opts CreateOpts) ToServerCreateMap() (map[string]interface{}, error) {
server["name"] = opts.Name
server["imageRef"] = opts.ImageRef
server["imageName"] = opts.ImageName
server["flavorRef"] = opts.FlavorRef
server["flavorName"] = opts.FlavorName
if opts.UserData != nil {
encoded := base64.StdEncoding.EncodeToString(opts.UserData)
server["user_data"] = &encoded
}
if opts.Personality != nil {
encoded := base64.StdEncoding.EncodeToString(opts.Personality)
server["personality"] = &encoded
}
if opts.ConfigDrive {
server["config_drive"] = "true"
}
@@ -202,6 +243,10 @@ func (opts CreateOpts) ToServerCreateMap() (map[string]interface{}, error) {
server["networks"] = networks
}
if len(opts.Personality) > 0 {
server["personality"] = opts.Personality
}
return map[string]interface{}{"server": server}, nil
}
@@ -215,6 +260,38 @@ func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateRes
return res
}
// If ImageRef isn't provided, use ImageName to ascertain the image ID.
if reqBody["server"].(map[string]interface{})["imageRef"].(string) == "" {
imageName := reqBody["server"].(map[string]interface{})["imageName"].(string)
if imageName == "" {
res.Err = errors.New("One and only one of ImageRef and ImageName must be provided.")
return res
}
imageID, err := images.IDFromName(client, imageName)
if err != nil {
res.Err = err
return res
}
reqBody["server"].(map[string]interface{})["imageRef"] = imageID
}
delete(reqBody["server"].(map[string]interface{}), "imageName")
// If FlavorRef isn't provided, use FlavorName to ascertain the flavor ID.
if reqBody["server"].(map[string]interface{})["flavorRef"].(string) == "" {
flavorName := reqBody["server"].(map[string]interface{})["flavorName"].(string)
if flavorName == "" {
res.Err = errors.New("One and only one of FlavorRef and FlavorName must be provided.")
return res
}
flavorID, err := flavors.IDFromName(client, flavorName)
if err != nil {
res.Err = err
return res
}
reqBody["server"].(map[string]interface{})["flavorRef"] = flavorID
}
delete(reqBody["server"].(map[string]interface{}), "flavorName")
_, res.Err = client.Post(listURL(client), reqBody, &res.Body, nil)
return res
}
@@ -391,9 +468,9 @@ type RebuildOpts struct {
// Metadata [optional] contains key-value pairs (up to 255 bytes each) to attach to the server.
Metadata map[string]string
// Personality [optional] includes the path and contents of a file to inject into the server at launch.
// The maximum size of the file is 255 bytes (decoded).
Personality []byte
// Personality [optional] includes files to inject into the server at launch.
// Rebuild will base64-encode file contents for you.
Personality Personality
}
// ToServerRebuildMap formats a RebuildOpts struct into a map for use in JSON
@@ -429,9 +506,8 @@ func (opts RebuildOpts) ToServerRebuildMap() (map[string]interface{}, error) {
server["metadata"] = opts.Metadata
}
if opts.Personality != nil {
encoded := base64.StdEncoding.EncodeToString(opts.Personality)
server["personality"] = &encoded
if len(opts.Personality) > 0 {
server["personality"] = opts.Personality
}
return map[string]interface{}{"rebuild": server}, nil
@@ -678,9 +754,7 @@ func Metadatum(client *gophercloud.ServiceClient, id, key string) GetMetadatumRe
// DeleteMetadatum will delete the key-value pair with the given key for the given server ID.
func DeleteMetadatum(client *gophercloud.ServiceClient, id, key string) DeleteMetadatumResult {
var res DeleteMetadatumResult
_, res.Err = client.Delete(metadatumURL(client, id, key), &gophercloud.RequestOpts{
JSONResponse: &res.Body,
})
_, res.Err = client.Delete(metadatumURL(client, id, key), nil)
return res
}
@@ -743,3 +817,36 @@ func CreateImage(client *gophercloud.ServiceClient, serverId string, opts Create
res.Header = response.Header
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
import (
"encoding/base64"
"encoding/json"
"net/http"
"testing"
@@ -334,3 +336,38 @@ func TestCreateServerImage(t *testing.T) {
_, err := CreateImage(client.ServiceClient(), "serverimage", CreateImageOpts{Name: "test"}).ExtractImageID()
th.AssertNoErr(t, err)
}
func TestMarshalPersonality(t *testing.T) {
name := "/etc/test"
contents := []byte("asdfasdf")
personality := Personality{
&File{
Path: name,
Contents: contents,
},
}
data, err := json.Marshal(personality)
if err != nil {
t.Fatal(err)
}
var actual []map[string]string
err = json.Unmarshal(data, &actual)
if err != nil {
t.Fatal(err)
}
if len(actual) != 1 {
t.Fatal("expected personality length 1")
}
if actual[0]["path"] != name {
t.Fatal("file path incorrect")
}
if actual[0]["contents"] != base64.StdEncoding.EncodeToString(contents) {
t.Fatal("file contents incorrect")
}
}

View File

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

View File

@@ -1,6 +1,10 @@
package tokens
import "github.com/rackspace/gophercloud"
import (
"fmt"
"github.com/rackspace/gophercloud"
)
// AuthOptionsBuilder describes any argument that may be passed to the Create call.
type AuthOptionsBuilder interface {
@@ -38,21 +42,25 @@ func (auth AuthOptions) ToTokenCreateMap() (map[string]interface{}, error) {
return nil, ErrDomainNameProvided
}
// Username and Password are always required.
if auth.Username == "" {
return nil, ErrUsernameRequired
}
if auth.Password == "" {
return nil, ErrPasswordRequired
}
// Populate the request map.
authMap := make(map[string]interface{})
if auth.Username != "" {
if auth.Password != "" {
authMap["passwordCredentials"] = map[string]interface{}{
"username": auth.Username,
"password": auth.Password,
}
} else {
return nil, ErrPasswordRequired
}
} else if auth.TokenID != "" {
authMap["token"] = map[string]interface{}{
"id": auth.TokenID,
}
} else {
return nil, fmt.Errorf("You must provide either username/password or tenantID/token values.")
}
if auth.TenantID != "" {
authMap["tenantId"] = auth.TenantID

View File

@@ -1,6 +1,7 @@
package tokens
import (
"fmt"
"testing"
"github.com/rackspace/gophercloud"
@@ -22,7 +23,7 @@ func tokenPostErr(t *testing.T, options gophercloud.AuthOptions, expectedErr err
HandleTokenPost(t, "")
actualErr := Create(client.ServiceClient(), AuthOptions{options}).Err
th.CheckEquals(t, expectedErr, actualErr)
th.CheckDeepEquals(t, expectedErr, actualErr)
}
func TestCreateWithPassword(t *testing.T) {
@@ -128,7 +129,7 @@ func TestRequireUsername(t *testing.T) {
Password: "thing",
}
tokenPostErr(t, options, ErrUsernameRequired)
tokenPostErr(t, options, fmt.Errorf("You must provide either username/password or tenantID/token values."))
}
func TestRequirePassword(t *testing.T) {

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",
"admin_state_up": false,
"tenant_id": "b4eedccc6fb74fa8a7ad6b08382b852b",
"firewall_policy_id": "19ab8c87-4a32-4e6a-a74e-b77fffb89a0c"
"firewall_policy_id": "19ab8c87-4a32-4e6a-a74e-b77fffb89a0c",
"id": "ea5b5315-64f6-4ea3-8e58-981cc37c6576",
"description": "OpenStack firewall",
"description": "OpenStack firewall"
}
}
`)

View File

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

View File

@@ -45,6 +45,9 @@ type CreateOpts struct {
// Required. Human-readable name for the VIP. Does not have to be unique.
Name string
// Required for admins. Indicates the owner of the VIP.
TenantID string
// Optional. Describes the security group.
Description string
}
@@ -62,6 +65,7 @@ func Create(c *gophercloud.ServiceClient, opts CreateOpts) CreateResult {
type secgroup struct {
Name string `json:"name"`
TenantID string `json:"tenant_id,omitempty"`
Description string `json:"description,omitempty"`
}
@@ -71,6 +75,7 @@ func Create(c *gophercloud.ServiceClient, opts CreateOpts) CreateResult {
reqBody := request{SecGroup: secgroup{
Name: opts.Name,
TenantID: opts.TenantID,
Description: opts.Description,
}}
@@ -91,3 +96,36 @@ func Delete(c *gophercloud.ServiceClient, id string) DeleteResult {
_, res.Err = c.Delete(resourceURL(c, id), nil)
return res
}
// IDFromName is a convenience function that returns a security group's ID given its name.
func IDFromName(client *gophercloud.ServiceClient, name string) (string, error) {
securityGroupCount := 0
securityGroupID := ""
if name == "" {
return "", fmt.Errorf("A security group name must be provided.")
}
pager := List(client, ListOpts{})
pager.EachPage(func(page pagination.Page) (bool, error) {
securityGroupList, err := ExtractGroups(page)
if err != nil {
return false, err
}
for _, s := range securityGroupList {
if s.Name == name {
securityGroupCount++
securityGroupID = s.ID
}
}
return true, nil
})
switch securityGroupCount {
case 0:
return "", fmt.Errorf("Unable to find security group: %s", name)
case 1:
return securityGroupID, nil
default:
return "", fmt.Errorf("Found %d security groups matching %s", securityGroupCount, name)
}
}

View File

@@ -99,6 +99,9 @@ type CreateOpts struct {
// attribute matches the specified IP prefix as the source IP address of the
// IP packet.
RemoteIPPrefix string
// Required for admins. Indicates the owner of the VIP.
TenantID string
}
// Create is an operation which provisions a new security group with default
@@ -133,6 +136,7 @@ func Create(c *gophercloud.ServiceClient, opts CreateOpts) CreateResult {
Protocol string `json:"protocol,omitempty"`
RemoteGroupID string `json:"remote_group_id,omitempty"`
RemoteIPPrefix string `json:"remote_ip_prefix,omitempty"`
TenantID string `json:"tenant_id,omitempty"`
}
type request struct {
@@ -148,6 +152,7 @@ func Create(c *gophercloud.ServiceClient, opts CreateOpts) CreateResult {
Protocol: opts.Protocol,
RemoteGroupID: opts.RemoteGroupID,
RemoteIPPrefix: opts.RemoteIPPrefix,
TenantID: opts.TenantID,
}}
_, res.Err = c.Post(rootURL(c), reqBody, &res.Body, nil)

View File

@@ -1,6 +1,8 @@
package networks
import (
"fmt"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
)
@@ -189,3 +191,36 @@ func Delete(c *gophercloud.ServiceClient, networkID string) DeleteResult {
_, res.Err = c.Delete(deleteURL(c, networkID), nil)
return res
}
// IDFromName is a convenience function that returns a network's ID given its name.
func IDFromName(client *gophercloud.ServiceClient, name string) (string, error) {
networkCount := 0
networkID := ""
if name == "" {
return "", fmt.Errorf("A network name must be provided.")
}
pager := List(client, nil)
pager.EachPage(func(page pagination.Page) (bool, error) {
networkList, err := ExtractNetworks(page)
if err != nil {
return false, err
}
for _, n := range networkList {
if n.Name == name {
networkCount++
networkID = n.ID
}
}
return true, nil
})
switch networkCount {
case 0:
return "", fmt.Errorf("Unable to find network: %s", name)
case 1:
return networkID, nil
default:
return "", fmt.Errorf("Found %d networks matching %s", networkCount, name)
}
}

View File

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

View File

@@ -1,6 +1,8 @@
package ports
import (
"fmt"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
)
@@ -223,3 +225,36 @@ func Delete(c *gophercloud.ServiceClient, id string) DeleteResult {
_, res.Err = c.Delete(deleteURL(c, id), nil)
return res
}
// IDFromName is a convenience function that returns a port's ID given its name.
func IDFromName(client *gophercloud.ServiceClient, name string) (string, error) {
portCount := 0
portID := ""
if name == "" {
return "", fmt.Errorf("A port name must be provided.")
}
pager := List(client, nil)
pager.EachPage(func(page pagination.Page) (bool, error) {
portList, err := ExtractPorts(page)
if err != nil {
return false, err
}
for _, p := range portList {
if p.Name == name {
portCount++
portID = p.ID
}
}
return true, nil
})
switch portCount {
case 0:
return "", fmt.Errorf("Unable to find port: %s", name)
case 1:
return portID, nil
default:
return "", fmt.Errorf("Found %d ports matching %s", portCount, name)
}
}

View File

@@ -1,6 +1,8 @@
package subnets
import (
"fmt"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
)
@@ -200,10 +202,10 @@ func (opts UpdateOpts) ToSubnetUpdateMap() (map[string]interface{}, error) {
if opts.GatewayIP != "" {
s["gateway_ip"] = opts.GatewayIP
}
if len(opts.DNSNameservers) != 0 {
if opts.DNSNameservers != nil {
s["dns_nameservers"] = opts.DNSNameservers
}
if len(opts.HostRoutes) != 0 {
if opts.HostRoutes != nil {
s["host_routes"] = opts.HostRoutes
}
@@ -234,3 +236,36 @@ func Delete(c *gophercloud.ServiceClient, id string) DeleteResult {
_, res.Err = c.Delete(deleteURL(c, id), nil)
return res
}
// IDFromName is a convenience function that returns a subnet's ID given its name.
func IDFromName(client *gophercloud.ServiceClient, name string) (string, error) {
subnetCount := 0
subnetID := ""
if name == "" {
return "", fmt.Errorf("A subnet name must be provided.")
}
pager := List(client, nil)
pager.EachPage(func(page pagination.Page) (bool, error) {
subnetList, err := ExtractSubnets(page)
if err != nil {
return false, err
}
for _, s := range subnetList {
if s.Name == name {
subnetCount++
subnetID = s.ID
}
}
return true, nil
})
switch subnetCount {
case 0:
return "", fmt.Errorf("Unable to find subnet: %s", name)
case 1:
return subnetID, nil
default:
return "", fmt.Errorf("Found %d subnets matching %s", subnetCount, name)
}
}

View File

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

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

View File

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

View File

@@ -3,7 +3,9 @@
package objects
import (
"crypto/md5"
"fmt"
"io"
"net/http"
"testing"
@@ -107,12 +109,18 @@ func HandleListObjectNamesSuccessfully(t *testing.T) {
// HandleCreateTextObjectSuccessfully creates an HTTP handler at `/testContainer/testObject` on the test handler mux
// that responds with a `Create` response. A Content-Type of "text/plain" is expected.
func HandleCreateTextObjectSuccessfully(t *testing.T) {
func HandleCreateTextObjectSuccessfully(t *testing.T, content string) {
th.Mux.HandleFunc("/testContainer/testObject", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "PUT")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
th.TestHeader(t, r, "Content-Type", "text/plain")
th.TestHeader(t, r, "Accept", "application/json")
hash := md5.New()
io.WriteString(hash, content)
localChecksum := hash.Sum(nil)
w.Header().Set("ETag", fmt.Sprintf("%x", localChecksum))
w.WriteHeader(http.StatusCreated)
})
}
@@ -120,7 +128,7 @@ func HandleCreateTextObjectSuccessfully(t *testing.T) {
// HandleCreateTypelessObjectSuccessfully creates an HTTP handler at `/testContainer/testObject` on the test handler
// mux that responds with a `Create` response. No Content-Type header may be present in the request, so that server-
// side content-type detection will be triggered properly.
func HandleCreateTypelessObjectSuccessfully(t *testing.T) {
func HandleCreateTypelessObjectSuccessfully(t *testing.T, content string) {
th.Mux.HandleFunc("/testContainer/testObject", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "PUT")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
@@ -130,6 +138,11 @@ func HandleCreateTypelessObjectSuccessfully(t *testing.T) {
t.Errorf("Expected Content-Type header to be omitted, but was %#v", contentType)
}
hash := md5.New()
io.WriteString(hash, content)
localChecksum := hash.Sum(nil)
w.Header().Set("ETag", fmt.Sprintf("%x", localChecksum))
w.WriteHeader(http.StatusCreated)
})
}

View File

@@ -1,7 +1,9 @@
package objects
import (
"bytes"
"crypto/hmac"
"crypto/md5"
"crypto/sha1"
"fmt"
"io"
@@ -134,10 +136,11 @@ func Download(c *gophercloud.ServiceClient, containerName, objectName string, op
MoreHeaders: h,
OkCodes: []int{200, 304},
})
res.Body = resp.Body
res.Err = err
if resp != nil {
res.Header = resp.Header
res.Body = resp.Body
}
res.Err = err
return res
}
@@ -187,8 +190,9 @@ func (opts CreateOpts) ToObjectCreateParams() (map[string]string, string, error)
return h, q.String(), nil
}
// Create is a function that creates a new object or replaces an existing object.
func Create(c *gophercloud.ServiceClient, containerName, objectName string, content io.Reader, opts CreateOptsBuilder) CreateResult {
// Create is a function that creates a new object or replaces an existing object. If the returned response's ETag
// header fails to match the local checksum, the failed request will automatically be retried up to a maximum of 3 times.
func Create(c *gophercloud.ServiceClient, containerName, objectName string, content io.ReadSeeker, opts CreateOptsBuilder) CreateResult {
var res CreateResult
url := createURL(c, containerName, objectName)
@@ -208,14 +212,37 @@ func Create(c *gophercloud.ServiceClient, containerName, objectName string, cont
url += query
}
hash := md5.New()
contentBuffer := bytes.NewBuffer([]byte{})
_, err := io.Copy(contentBuffer, io.TeeReader(content, hash))
if err != nil {
res.Err = err
return res
}
localChecksum := hash.Sum(nil)
h["ETag"] = fmt.Sprintf("%x", localChecksum)
ropts := gophercloud.RequestOpts{
RawBody: content,
RawBody: strings.NewReader(contentBuffer.String()),
MoreHeaders: h,
}
resp, err := c.Request("PUT", url, ropts)
res.Header = resp.Header
if err != nil {
res.Err = err
return res
}
if resp != nil {
res.Header = resp.Header
if resp.Header.Get("ETag") == fmt.Sprintf("%x", localChecksum) {
res.Err = err
return res
}
res.Err = fmt.Errorf("Local checksum does not match API ETag header")
}
return res
}
@@ -270,7 +297,9 @@ func Copy(c *gophercloud.ServiceClient, containerName, objectName string, opts C
MoreHeaders: h,
OkCodes: []int{201},
})
if resp != nil {
res.Header = resp.Header
}
res.Err = err
return res
}
@@ -310,7 +339,9 @@ func Delete(c *gophercloud.ServiceClient, containerName, objectName string, opts
}
resp, err := c.Delete(url, nil)
if resp != nil {
res.Header = resp.Header
}
res.Err = err
return res
}
@@ -354,7 +385,9 @@ func Get(c *gophercloud.ServiceClient, containerName, objectName string, opts Ge
resp, err := c.Request("HEAD", url, gophercloud.RequestOpts{
OkCodes: []int{200, 204},
})
if resp != nil {
res.Header = resp.Header
}
res.Err = err
return res
}
@@ -410,7 +443,9 @@ func Update(c *gophercloud.ServiceClient, containerName, objectName string, opts
resp, err := c.Request("POST", url, gophercloud.RequestOpts{
MoreHeaders: h,
})
if resp != nil {
res.Header = resp.Header
}
res.Err = err
return res
}

View File

@@ -2,7 +2,10 @@ package objects
import (
"bytes"
"fmt"
"io"
"net/http"
"strings"
"testing"
"github.com/rackspace/gophercloud/pagination"
@@ -83,24 +86,44 @@ func TestListObjectNames(t *testing.T) {
func TestCreateObject(t *testing.T) {
th.SetupHTTP()
defer th.TeardownHTTP()
HandleCreateTextObjectSuccessfully(t)
content := bytes.NewBufferString("Did gyre and gimble in the wabe")
content := "Did gyre and gimble in the wabe"
HandleCreateTextObjectSuccessfully(t, content)
options := &CreateOpts{ContentType: "text/plain"}
res := Create(fake.ServiceClient(), "testContainer", "testObject", content, options)
res := Create(fake.ServiceClient(), "testContainer", "testObject", strings.NewReader(content), options)
th.AssertNoErr(t, res.Err)
}
func TestCreateObjectWithoutContentType(t *testing.T) {
th.SetupHTTP()
defer th.TeardownHTTP()
HandleCreateTypelessObjectSuccessfully(t)
content := bytes.NewBufferString("The sky was the color of television, tuned to a dead channel.")
res := Create(fake.ServiceClient(), "testContainer", "testObject", content, &CreateOpts{})
content := "The sky was the color of television, tuned to a dead channel."
HandleCreateTypelessObjectSuccessfully(t, content)
res := Create(fake.ServiceClient(), "testContainer", "testObject", strings.NewReader(content), &CreateOpts{})
th.AssertNoErr(t, res.Err)
}
func TestErrorIsRaisedForChecksumMismatch(t *testing.T) {
th.SetupHTTP()
defer th.TeardownHTTP()
th.Mux.HandleFunc("/testContainer/testObject", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("ETag", "acbd18db4cc2f85cedef654fccc4a4d8")
w.WriteHeader(http.StatusCreated)
})
content := strings.NewReader("The sky was the color of television, tuned to a dead channel.")
res := Create(fake.ServiceClient(), "testContainer", "testObject", content, &CreateOpts{})
err := fmt.Errorf("Local checksum does not match API ETag header")
th.AssertDeepEquals(t, err, res.Err)
}
func TestCopyObject(t *testing.T) {
th.SetupHTTP()
defer th.TeardownHTTP()

View File

@@ -67,7 +67,7 @@ const FindOutput = `
"events": [
{
"resource_name": "hello_world",
"event_time": "2015-02-05T21:33:11Z",
"event_time": "2015-02-05T21:33:11",
"links": [
{
"href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world/events/06feb26f-9298-4a9b-8749-9d770e5d577a",
@@ -90,7 +90,7 @@ const FindOutput = `
},
{
"resource_name": "hello_world",
"event_time": "2015-02-05T21:33:27Z",
"event_time": "2015-02-05T21:33:27",
"links": [
{
"href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world/events/93940999-7d40-44ae-8de4-19624e7b8d18",
@@ -184,7 +184,7 @@ const ListOutput = `
"events": [
{
"resource_name": "hello_world",
"event_time": "2015-02-05T21:33:11Z",
"event_time": "2015-02-05T21:33:11",
"links": [
{
"href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world/events/06feb26f-9298-4a9b-8749-9d770e5d577a",
@@ -207,7 +207,7 @@ const ListOutput = `
},
{
"resource_name": "hello_world",
"event_time": "2015-02-05T21:33:27Z",
"event_time": "2015-02-05T21:33:27",
"links": [
{
"href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world/events/93940999-7d40-44ae-8de4-19624e7b8d18",
@@ -309,7 +309,7 @@ const ListResourceEventsOutput = `
"events": [
{
"resource_name": "hello_world",
"event_time": "2015-02-05T21:33:11Z",
"event_time": "2015-02-05T21:33:11",
"links": [
{
"href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world/events/06feb26f-9298-4a9b-8749-9d770e5d577a",
@@ -332,7 +332,7 @@ const ListResourceEventsOutput = `
},
{
"resource_name": "hello_world",
"event_time": "2015-02-05T21:33:27Z",
"event_time": "2015-02-05T21:33:27",
"links": [
{
"href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world/events/93940999-7d40-44ae-8de4-19624e7b8d18",
@@ -408,7 +408,7 @@ const GetOutput = `
{
"event":{
"resource_name": "hello_world",
"event_time": "2015-02-05T21:33:27Z",
"event_time": "2015-02-05T21:33:27",
"links": [
{
"href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world/events/93940999-7d40-44ae-8de4-19624e7b8d18",

View File

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

View File

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

View File

@@ -25,12 +25,6 @@ type ListOptsBuilder interface {
// ListOpts allows the filtering and sorting of paginated collections through
// the API. Marker and Limit are used for pagination.
type ListOpts struct {
// The stack resource ID with which to start the listing.
Marker string `q:"marker"`
// Integer value for the limit of values to return.
Limit int `q:"limit"`
// Include resources from nest stacks up to Depth levels of recursion.
Depth int `q:"nested_depth"`
}
@@ -57,9 +51,7 @@ func List(client *gophercloud.ServiceClient, stackName, stackID string, opts Lis
}
createPageFn := func(r pagination.PageResult) pagination.Page {
p := ResourcePage{pagination.MarkerPageBase{PageResult: r}}
p.MarkerPageBase.Owner = p
return p
return ResourcePage{pagination.SinglePageBase(r)}
}
return pagination.NewPager(client, url, createPageFn)

View File

@@ -48,7 +48,7 @@ func (r FindResult) Extract() ([]Resource, error) {
for i, resourceRaw := range resources {
resource := resourceRaw.(map[string]interface{})
if date, ok := resource["updated_time"]; ok && date != nil {
t, err := time.Parse(time.RFC3339, date.(string))
t, err := time.Parse(gophercloud.STACK_TIME_FMT, date.(string))
if err != nil {
return nil, err
}
@@ -63,7 +63,7 @@ func (r FindResult) Extract() ([]Resource, error) {
// As OpenStack extensions may freely alter the response bodies of structures returned to the client, you may only safely access the
// data provided through the ExtractResources call.
type ResourcePage struct {
pagination.MarkerPageBase
pagination.SinglePageBase
}
// IsEmpty returns true if a page contains no Server results.
@@ -109,7 +109,7 @@ func ExtractResources(page pagination.Page) ([]Resource, error) {
for i, resourceRaw := range resources {
resource := resourceRaw.(map[string]interface{})
if date, ok := resource["updated_time"]; ok && date != nil {
t, err := time.Parse(time.RFC3339, date.(string))
t, err := time.Parse(gophercloud.STACK_TIME_FMT, date.(string))
if err != nil {
return nil, err
}
@@ -143,7 +143,7 @@ func (r GetResult) Extract() (*Resource, error) {
resource := r.Body.(map[string]interface{})["resource"].(map[string]interface{})
if date, ok := resource["updated_time"]; ok && date != nil {
t, err := time.Parse(time.RFC3339, date.(string))
t, err := time.Parse(gophercloud.STACK_TIME_FMT, date.(string))
if err != nil {
return nil, err
}

View File

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

View File

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

View File

@@ -36,13 +36,19 @@ func PageResultFrom(resp *http.Response) (PageResult, error) {
parsedBody = rawBody
}
return PageResultFromParsed(resp, parsedBody), err
}
// PageResultFromParsed constructs a PageResult from an HTTP response that has already had its
// body parsed as JSON (and closed).
func PageResultFromParsed(resp *http.Response, body interface{}) PageResult {
return PageResult{
Result: gophercloud.Result{
Body: parsedBody,
Body: body,
Header: resp.Header,
},
URL: *resp.Request.URL,
}, err
}
}
// Request performs an HTTP request and extracts the http.Response from the result.

View File

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

View File

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

View File

@@ -3,7 +3,8 @@ package volumes
import (
"testing"
os "github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes"
"github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes"
os "github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/testing"
"github.com/rackspace/gophercloud/pagination"
th "github.com/rackspace/gophercloud/testhelper"
fake "github.com/rackspace/gophercloud/testhelper/client"
@@ -64,7 +65,7 @@ func TestCreate(t *testing.T) {
os.MockCreateResponse(t)
n, err := Create(fake.ServiceClient(), CreateOpts{os.CreateOpts{Size: 75}}).Extract()
n, err := Create(fake.ServiceClient(), CreateOpts{volumes.CreateOpts{Size: 75}}).Extract()
th.AssertNoErr(t, err)
th.AssertEquals(t, n.Size, 4)
@@ -72,12 +73,12 @@ func TestCreate(t *testing.T) {
}
func TestSizeRange(t *testing.T) {
_, err := Create(fake.ServiceClient(), CreateOpts{os.CreateOpts{Size: 1}}).Extract()
_, err := Create(fake.ServiceClient(), CreateOpts{volumes.CreateOpts{Size: 1}}).Extract()
if err == nil {
t.Fatalf("Expected error, got none")
}
_, err = Create(fake.ServiceClient(), CreateOpts{os.CreateOpts{Size: 2000}}).Extract()
_, err = Create(fake.ServiceClient(), CreateOpts{volumes.CreateOpts{Size: 2000}}).Extract()
if err == nil {
t.Fatalf("Expected error, got none")
}

View File

@@ -33,6 +33,8 @@ func TestCreateOpts(t *testing.T) {
"name": "createdserver",
"imageRef": "asdfasdfasdf",
"flavorRef": "performance1-1",
"flavorName": "",
"imageName": "",
"block_device_mapping_v2":[
{
"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.
func Get(client *gophercloud.ServiceClient, id string) os.GetResult {
return os.Get(client, id)
}
// ExtractFlavors interprets a page of List results as Flavors.
func ExtractFlavors(page pagination.Page) ([]os.Flavor, error) {
return os.ExtractFlavors(page)
func Get(client *gophercloud.ServiceClient, id string) GetResult {
var res GetResult
_, res.Err = client.Get(getURL(client, id), &res.Body, nil)
return res
}

View File

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

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

View File

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

View File

@@ -3,24 +3,53 @@ package volumeattach
import (
"testing"
os "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/volumeattach"
"github.com/rackspace/gophercloud/openstack/compute/v2/extensions/volumeattach"
fixtures "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/volumeattach/testing"
"github.com/rackspace/gophercloud/pagination"
th "github.com/rackspace/gophercloud/testhelper"
"github.com/rackspace/gophercloud/testhelper/client"
)
// FirstVolumeAttachment is the first result in ListOutput.
var FirstVolumeAttachment = volumeattach.VolumeAttachment{
Device: "/dev/vdd",
ID: "a26887c6-c47b-4654-abb5-dfadf7d3f803",
ServerID: "4d8c3732-a248-40ed-bebc-539a6ffd25c0",
VolumeID: "a26887c6-c47b-4654-abb5-dfadf7d3f803",
}
// SecondVolumeAttachment is the first result in ListOutput.
var SecondVolumeAttachment = volumeattach.VolumeAttachment{
Device: "/dev/vdc",
ID: "a26887c6-c47b-4654-abb5-dfadf7d3f804",
ServerID: "4d8c3732-a248-40ed-bebc-539a6ffd25c0",
VolumeID: "a26887c6-c47b-4654-abb5-dfadf7d3f804",
}
// ExpectedVolumeAttachmentSlide is the slice of results that should be parsed
// from ListOutput, in the expected order.
var ExpectedVolumeAttachmentSlice = []volumeattach.VolumeAttachment{FirstVolumeAttachment, SecondVolumeAttachment}
//CreatedVolumeAttachment is the parsed result from CreatedOutput.
var CreatedVolumeAttachment = volumeattach.VolumeAttachment{
Device: "/dev/vdc",
ID: "a26887c6-c47b-4654-abb5-dfadf7d3f804",
ServerID: "4d8c3732-a248-40ed-bebc-539a6ffd25c0",
VolumeID: "a26887c6-c47b-4654-abb5-dfadf7d3f804",
}
func TestList(t *testing.T) {
th.SetupHTTP()
defer th.TeardownHTTP()
os.HandleListSuccessfully(t)
fixtures.HandleListSuccessfully(t)
serverId := "4d8c3732-a248-40ed-bebc-539a6ffd25c0"
count := 0
err := List(client.ServiceClient(), serverId).EachPage(func(page pagination.Page) (bool, error) {
count++
actual, err := os.ExtractVolumeAttachments(page)
actual, err := volumeattach.ExtractVolumeAttachments(page)
th.AssertNoErr(t, err)
th.CheckDeepEquals(t, os.ExpectedVolumeAttachmentSlice, actual)
th.CheckDeepEquals(t, ExpectedVolumeAttachmentSlice, actual)
return true, nil
})
@@ -31,33 +60,33 @@ func TestList(t *testing.T) {
func TestCreate(t *testing.T) {
th.SetupHTTP()
defer th.TeardownHTTP()
os.HandleCreateSuccessfully(t)
fixtures.HandleCreateSuccessfully(t)
serverId := "4d8c3732-a248-40ed-bebc-539a6ffd25c0"
actual, err := Create(client.ServiceClient(), serverId, os.CreateOpts{
actual, err := Create(client.ServiceClient(), serverId, volumeattach.CreateOpts{
Device: "/dev/vdc",
VolumeID: "a26887c6-c47b-4654-abb5-dfadf7d3f804",
}).Extract()
th.AssertNoErr(t, err)
th.CheckDeepEquals(t, &os.CreatedVolumeAttachment, actual)
th.CheckDeepEquals(t, &CreatedVolumeAttachment, actual)
}
func TestGet(t *testing.T) {
th.SetupHTTP()
defer th.TeardownHTTP()
os.HandleGetSuccessfully(t)
fixtures.HandleGetSuccessfully(t)
aId := "a26887c6-c47b-4654-abb5-dfadf7d3f804"
serverId := "4d8c3732-a248-40ed-bebc-539a6ffd25c0"
actual, err := Get(client.ServiceClient(), serverId, aId).Extract()
th.AssertNoErr(t, err)
th.CheckDeepEquals(t, &os.SecondVolumeAttachment, actual)
th.CheckDeepEquals(t, &SecondVolumeAttachment, actual)
}
func TestDelete(t *testing.T) {
th.SetupHTTP()
defer th.TeardownHTTP()
os.HandleDeleteSuccessfully(t)
fixtures.HandleDeleteSuccessfully(t)
aId := "a26887c6-c47b-4654-abb5-dfadf7d3f804"
serverId := "4d8c3732-a248-40ed-bebc-539a6ffd25c0"

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) {
th.Mux.HandleFunc(_rootURL(lbID), func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "DELETE")

View File

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

View File

@@ -108,6 +108,38 @@ func TestCreate(t *testing.T) {
th.CheckDeepEquals(t, expected, actual)
}
func TestCreateErr(t *testing.T) {
th.SetupHTTP()
defer th.TeardownHTTP()
mockCreateErrResponse(t, lbID)
opts := CreateOpts{
CreateOpt{
Address: "10.2.2.3",
Port: 80,
Condition: ENABLED,
Type: PRIMARY,
},
CreateOpt{
Address: "10.2.2.4",
Port: 81,
Condition: ENABLED,
Type: SECONDARY,
},
}
page := Create(client.ServiceClient(), lbID, opts)
actual, err := page.ExtractNodes()
if err == nil {
t.Fatal("Did not receive expected error from ExtractNodes")
}
if actual != nil {
t.Fatalf("Received non-nil result from failed ExtractNodes: %#v", actual)
}
}
func TestBulkDelete(t *testing.T) {
th.SetupHTTP()
defer th.TeardownHTTP()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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