Move deps from _workspace/ to vendor/

godep restore
pushd $GOPATH/src/github.com/appc/spec
git co master
popd
go get go4.org/errorutil
rm -rf Godeps
godep save ./...
git add vendor
git add -f $(git ls-files --other vendor/)
git co -- Godeps/LICENSES Godeps/.license_file_state Godeps/OWNERS
This commit is contained in:
Tim Hockin
2016-05-08 20:30:21 -07:00
parent 899f9b4e31
commit 3c0c5ed4e0
4400 changed files with 16739 additions and 376 deletions

View File

@@ -0,0 +1,57 @@
package rackspace
import (
"fmt"
"os"
"github.com/rackspace/gophercloud"
)
var nilOptions = gophercloud.AuthOptions{}
// ErrNoAuthUrl, ErrNoUsername, and ErrNoPassword errors indicate of the
// required RS_AUTH_URL, RS_USERNAME, or RS_PASSWORD environment variables,
// respectively, remain undefined. See the AuthOptions() function for more details.
var (
ErrNoAuthURL = fmt.Errorf("Environment variable RS_AUTH_URL or OS_AUTH_URL need to be set.")
ErrNoUsername = fmt.Errorf("Environment variable RS_USERNAME or OS_USERNAME need to be set.")
ErrNoPassword = fmt.Errorf("Environment variable RS_API_KEY or RS_PASSWORD needs to be set.")
)
func prefixedEnv(base string) string {
value := os.Getenv("RS_" + base)
if value == "" {
value = os.Getenv("OS_" + base)
}
return value
}
// AuthOptionsFromEnv fills out an identity.AuthOptions structure with the
// settings found on the various Rackspace RS_* environment variables.
func AuthOptionsFromEnv() (gophercloud.AuthOptions, error) {
authURL := prefixedEnv("AUTH_URL")
username := prefixedEnv("USERNAME")
password := prefixedEnv("PASSWORD")
apiKey := prefixedEnv("API_KEY")
if authURL == "" {
return nilOptions, ErrNoAuthURL
}
if username == "" {
return nilOptions, ErrNoUsername
}
if password == "" && apiKey == "" {
return nilOptions, ErrNoPassword
}
ao := gophercloud.AuthOptions{
IdentityEndpoint: authURL,
Username: username,
Password: password,
APIKey: apiKey,
}
return ao, nil
}

View File

@@ -0,0 +1,131 @@
package snapshots
import (
"errors"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
os "github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots"
)
func updateURL(c *gophercloud.ServiceClient, id string) string {
return c.ServiceURL("snapshots", id)
}
// CreateOptsBuilder allows extensions to add additional parameters to the
// Create request.
type CreateOptsBuilder interface {
ToSnapshotCreateMap() (map[string]interface{}, error)
}
// CreateOpts contains options for creating a Snapshot. This object is passed to
// the snapshots.Create function. For more information about these parameters,
// see the Snapshot object.
type CreateOpts struct {
// REQUIRED
VolumeID string
// OPTIONAL
Description string
// OPTIONAL
Force bool
// OPTIONAL
Name string
}
// ToSnapshotCreateMap assembles a request body based on the contents of a
// CreateOpts.
func (opts CreateOpts) ToSnapshotCreateMap() (map[string]interface{}, error) {
s := make(map[string]interface{})
if opts.VolumeID == "" {
return nil, errors.New("Required CreateOpts field 'VolumeID' not set.")
}
s["volume_id"] = opts.VolumeID
if opts.Description != "" {
s["display_description"] = opts.Description
}
if opts.Name != "" {
s["display_name"] = opts.Name
}
if opts.Force {
s["force"] = opts.Force
}
return map[string]interface{}{"snapshot": s}, nil
}
// Create will create a new Snapshot based on the values in CreateOpts. To
// extract the Snapshot object from the response, call the Extract method on the
// CreateResult.
func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult {
return CreateResult{os.Create(client, opts)}
}
// Delete will delete the existing Snapshot with the provided ID.
func Delete(client *gophercloud.ServiceClient, id string) os.DeleteResult {
return os.Delete(client, id)
}
// Get retrieves the Snapshot with the provided ID. To extract the Snapshot
// object from the response, call the Extract method on the GetResult.
func Get(client *gophercloud.ServiceClient, id string) GetResult {
return GetResult{os.Get(client, id)}
}
// List returns Snapshots.
func List(client *gophercloud.ServiceClient) pagination.Pager {
return os.List(client, os.ListOpts{})
}
// UpdateOptsBuilder is the interface options structs have to satisfy in order
// to be used in the main Update operation in this package. Since many
// extensions decorate or modify the common logic, it is useful for them to
// satisfy a basic interface in order for them to be used.
type UpdateOptsBuilder interface {
ToSnapshotUpdateMap() (map[string]interface{}, error)
}
// UpdateOpts is the common options struct used in this package's Update
// operation.
type UpdateOpts struct {
Name string
Description string
}
// ToSnapshotUpdateMap casts a UpdateOpts struct to a map.
func (opts UpdateOpts) ToSnapshotUpdateMap() (map[string]interface{}, error) {
s := make(map[string]interface{})
if opts.Name != "" {
s["display_name"] = opts.Name
}
if opts.Description != "" {
s["display_description"] = opts.Description
}
return map[string]interface{}{"snapshot": s}, nil
}
// Update accepts a UpdateOpts struct and updates an existing snapshot using the
// values provided.
func Update(c *gophercloud.ServiceClient, snapshotID string, opts UpdateOptsBuilder) UpdateResult {
var res UpdateResult
reqBody, err := opts.ToSnapshotUpdateMap()
if err != nil {
res.Err = err
return res
}
// Send request to API
_, res.Err = c.Request("PUT", updateURL(c, snapshotID), gophercloud.RequestOpts{
JSONBody: &reqBody,
JSONResponse: &res.Body,
OkCodes: []int{200, 201},
})
return res
}

View File

@@ -0,0 +1,3 @@
// Package snapshots provides information and interaction with the snapshot
// API resource for the Rackspace Block Storage service.
package snapshots

View File

@@ -0,0 +1,147 @@
package snapshots
import (
"github.com/rackspace/gophercloud"
os "github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots"
"github.com/rackspace/gophercloud/pagination"
"github.com/mitchellh/mapstructure"
)
// Status is the type used to represent a snapshot's status
type Status string
// Constants to use for supported statuses
const (
Creating Status = "CREATING"
Available Status = "AVAILABLE"
Deleting Status = "DELETING"
Error Status = "ERROR"
DeleteError Status = "ERROR_DELETING"
)
// Snapshot is the Rackspace representation of an external block storage device.
type Snapshot struct {
// The timestamp when this snapshot was created.
CreatedAt string `mapstructure:"created_at"`
// The human-readable description for this snapshot.
Description string `mapstructure:"display_description"`
// The human-readable name for this snapshot.
Name string `mapstructure:"display_name"`
// The UUID for this snapshot.
ID string `mapstructure:"id"`
// The random metadata associated with this snapshot. Note: unlike standard
// OpenStack snapshots, this cannot actually be set.
Metadata map[string]string `mapstructure:"metadata"`
// Indicates the current progress of the snapshot's backup procedure.
Progress string `mapstructure:"os-extended-snapshot-attributes:progress"`
// The project ID.
ProjectID string `mapstructure:"os-extended-snapshot-attributes:project_id"`
// The size of the volume which this snapshot backs up.
Size int `mapstructure:"size"`
// The status of the snapshot.
Status Status `mapstructure:"status"`
// The ID of the volume which this snapshot seeks to back up.
VolumeID string `mapstructure:"volume_id"`
}
// CreateResult represents the result of a create operation
type CreateResult struct {
os.CreateResult
}
// GetResult represents the result of a get operation
type GetResult struct {
os.GetResult
}
// UpdateResult represents the result of an update operation
type UpdateResult struct {
gophercloud.Result
}
func commonExtract(resp interface{}, err error) (*Snapshot, error) {
if err != nil {
return nil, err
}
var respStruct struct {
Snapshot *Snapshot `json:"snapshot"`
}
err = mapstructure.Decode(resp, &respStruct)
return respStruct.Snapshot, err
}
// Extract will get the Snapshot object out of the GetResult object.
func (r GetResult) Extract() (*Snapshot, error) {
return commonExtract(r.Body, r.Err)
}
// Extract will get the Snapshot object out of the CreateResult object.
func (r CreateResult) Extract() (*Snapshot, error) {
return commonExtract(r.Body, r.Err)
}
// Extract will get the Snapshot object out of the UpdateResult object.
func (r UpdateResult) Extract() (*Snapshot, error) {
return commonExtract(r.Body, r.Err)
}
// ExtractSnapshots extracts and returns Snapshots. It is used while iterating over a snapshots.List call.
func ExtractSnapshots(page pagination.Page) ([]Snapshot, error) {
var response struct {
Snapshots []Snapshot `json:"snapshots"`
}
err := mapstructure.Decode(page.(os.ListResult).Body, &response)
return response.Snapshots, err
}
// WaitUntilComplete will continually poll a snapshot until it successfully
// transitions to a specified state. It will do this for at most the number of
// seconds specified.
func (snapshot Snapshot) WaitUntilComplete(c *gophercloud.ServiceClient, timeout int) error {
return gophercloud.WaitFor(timeout, func() (bool, error) {
// Poll resource
current, err := Get(c, snapshot.ID).Extract()
if err != nil {
return false, err
}
// Has it been built yet?
if current.Progress == "100%" {
return true, nil
}
return false, nil
})
}
// WaitUntilDeleted will continually poll a snapshot until it has been
// successfully deleted, i.e. returns a 404 status.
func (snapshot Snapshot) WaitUntilDeleted(c *gophercloud.ServiceClient, timeout int) error {
return gophercloud.WaitFor(timeout, func() (bool, error) {
// Poll resource
_, err := Get(c, snapshot.ID).Extract()
// Check for a 404
if casted, ok := err.(*gophercloud.UnexpectedResponseCodeError); ok && casted.Actual == 404 {
return true, nil
} else if err != nil {
return false, err
}
return false, nil
})
}

View File

@@ -0,0 +1,75 @@
package volumes
import (
"fmt"
"github.com/rackspace/gophercloud"
os "github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes"
"github.com/rackspace/gophercloud/pagination"
)
type CreateOpts struct {
os.CreateOpts
}
func (opts CreateOpts) ToVolumeCreateMap() (map[string]interface{}, error) {
if opts.Size < 75 || opts.Size > 1024 {
return nil, fmt.Errorf("Size field must be between 75 and 1024")
}
return opts.CreateOpts.ToVolumeCreateMap()
}
// Create will create a new Volume based on the values in CreateOpts. To extract
// the Volume object from the response, call the Extract method on the
// CreateResult.
func Create(client *gophercloud.ServiceClient, opts os.CreateOptsBuilder) CreateResult {
return CreateResult{os.Create(client, opts)}
}
// Delete will delete the existing Volume with the provided ID.
func Delete(client *gophercloud.ServiceClient, id string) os.DeleteResult {
return os.Delete(client, id)
}
// Get retrieves the Volume with the provided ID. To extract the Volume object
// from the response, call the Extract method on the GetResult.
func Get(client *gophercloud.ServiceClient, id string) GetResult {
return GetResult{os.Get(client, id)}
}
// List returns volumes optionally limited by the conditions provided in ListOpts.
func List(client *gophercloud.ServiceClient) pagination.Pager {
return os.List(client, os.ListOpts{})
}
// UpdateOpts contain options for updating an existing Volume. This object is passed
// to the volumes.Update function. For more information about the parameters, see
// the Volume object.
type UpdateOpts struct {
// OPTIONAL
Name string
// OPTIONAL
Description string
}
// ToVolumeUpdateMap assembles a request body based on the contents of an
// UpdateOpts.
func (opts UpdateOpts) ToVolumeUpdateMap() (map[string]interface{}, error) {
v := make(map[string]interface{})
if opts.Description != "" {
v["display_description"] = opts.Description
}
if opts.Name != "" {
v["display_name"] = opts.Name
}
return map[string]interface{}{"volume": v}, nil
}
// Update will update the Volume with provided information. To extract the updated
// Volume from the response, call the Extract method on the UpdateResult.
func Update(client *gophercloud.ServiceClient, id string, opts os.UpdateOptsBuilder) UpdateResult {
return UpdateResult{os.Update(client, id, opts)}
}

View File

@@ -0,0 +1,3 @@
// Package volumes provides information and interaction with the volume
// API resource for the Rackspace Block Storage service.
package volumes

View File

@@ -0,0 +1,66 @@
package volumes
import (
os "github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes"
"github.com/rackspace/gophercloud/pagination"
"github.com/mitchellh/mapstructure"
)
// Volume wraps an Openstack volume
type Volume os.Volume
// CreateResult represents the result of a create operation
type CreateResult struct {
os.CreateResult
}
// GetResult represents the result of a get operation
type GetResult struct {
os.GetResult
}
// UpdateResult represents the result of an update operation
type UpdateResult struct {
os.UpdateResult
}
func commonExtract(resp interface{}, err error) (*Volume, error) {
if err != nil {
return nil, err
}
var respStruct struct {
Volume *Volume `json:"volume"`
}
err = mapstructure.Decode(resp, &respStruct)
return respStruct.Volume, err
}
// Extract will get the Volume object out of the GetResult object.
func (r GetResult) Extract() (*Volume, error) {
return commonExtract(r.Body, r.Err)
}
// Extract will get the Volume object out of the CreateResult object.
func (r CreateResult) Extract() (*Volume, error) {
return commonExtract(r.Body, r.Err)
}
// Extract will get the Volume object out of the UpdateResult object.
func (r UpdateResult) Extract() (*Volume, error) {
return commonExtract(r.Body, r.Err)
}
// ExtractVolumes extracts and returns Volumes. It is used while iterating over a volumes.List call.
func ExtractVolumes(page pagination.Page) ([]Volume, error) {
var response struct {
Volumes []Volume `json:"volumes"`
}
err := mapstructure.Decode(page.(os.ListResult).Body, &response)
return response.Volumes, err
}

View File

@@ -0,0 +1,18 @@
package volumetypes
import (
"github.com/rackspace/gophercloud"
os "github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumetypes"
"github.com/rackspace/gophercloud/pagination"
)
// List returns all volume types.
func List(client *gophercloud.ServiceClient) pagination.Pager {
return os.List(client)
}
// Get will retrieve the volume type with the provided ID. To extract the volume
// type from the result, call the Extract method on the GetResult.
func Get(client *gophercloud.ServiceClient, id string) GetResult {
return GetResult{os.Get(client, id)}
}

View File

@@ -0,0 +1,3 @@
// Package volumetypes provides information and interaction with the volume type
// API resource for the Rackspace Block Storage service.
package volumetypes

View File

@@ -0,0 +1,37 @@
package volumetypes
import (
"github.com/mitchellh/mapstructure"
os "github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumetypes"
"github.com/rackspace/gophercloud/pagination"
)
type VolumeType os.VolumeType
type GetResult struct {
os.GetResult
}
// Extract will get the Volume Type struct out of the response.
func (r GetResult) Extract() (*VolumeType, error) {
if r.Err != nil {
return nil, r.Err
}
var res struct {
VolumeType *VolumeType `json:"volume_type" mapstructure:"volume_type"`
}
err := mapstructure.Decode(r.Body, &res)
return res.VolumeType, err
}
func ExtractVolumeTypes(page pagination.Page) ([]VolumeType, error) {
var response struct {
VolumeTypes []VolumeType `mapstructure:"volume_types"`
}
err := mapstructure.Decode(page.(os.ListResult).Body, &response)
return response.VolumeTypes, err
}

View File

@@ -0,0 +1,18 @@
package base
import (
"github.com/rackspace/gophercloud"
os "github.com/rackspace/gophercloud/openstack/cdn/v1/base"
)
// Get retrieves the home document, allowing the user to discover the
// entire API.
func Get(c *gophercloud.ServiceClient) os.GetResult {
return os.Get(c)
}
// Ping retrieves a ping to the server.
func Ping(c *gophercloud.ServiceClient) os.PingResult {
return os.Ping(c)
}

View File

@@ -0,0 +1,4 @@
// Package base provides information and interaction with the base API
// resource in the Rackspace CDN service. This API resource allows for
// retrieving the Home Document and pinging the root URL.
package base

View File

@@ -0,0 +1,18 @@
package flavors
import (
"github.com/rackspace/gophercloud"
os "github.com/rackspace/gophercloud/openstack/cdn/v1/flavors"
"github.com/rackspace/gophercloud/pagination"
)
// List returns a single page of CDN flavors.
func List(c *gophercloud.ServiceClient) pagination.Pager {
return os.List(c)
}
// Get retrieves a specific flavor based on its unique ID.
func Get(c *gophercloud.ServiceClient, id string) os.GetResult {
return os.Get(c, id)
}

View File

@@ -0,0 +1,6 @@
// Package flavors provides information and interaction with the flavors API
// resource in the Rackspace CDN service. This API resource allows for
// listing flavors and retrieving a specific flavor.
//
// A flavor is a mapping configuration to a CDN provider.
package flavors

View File

@@ -0,0 +1,13 @@
package serviceassets
import (
"github.com/rackspace/gophercloud"
os "github.com/rackspace/gophercloud/openstack/cdn/v1/serviceassets"
)
// Delete accepts a unique ID and deletes the CDN service asset associated with
// it.
func Delete(c *gophercloud.ServiceClient, id string, opts os.DeleteOptsBuilder) os.DeleteResult {
return os.Delete(c, id, opts)
}

View File

@@ -0,0 +1,7 @@
// Package serviceassets provides information and interaction with the
// serviceassets API resource in the Rackspace CDN service. This API resource
// allows for deleting cached assets.
//
// A service distributes assets across the network. Service assets let you
// interrogate properties about these assets and perform certain actions on them.
package serviceassets

View File

@@ -0,0 +1,37 @@
package services
import (
"github.com/rackspace/gophercloud"
os "github.com/rackspace/gophercloud/openstack/cdn/v1/services"
"github.com/rackspace/gophercloud/pagination"
)
// List returns a Pager which allows you to iterate over a collection of
// CDN services. It accepts a ListOpts struct, which allows for pagination via
// marker and limit.
func List(c *gophercloud.ServiceClient, opts os.ListOptsBuilder) pagination.Pager {
return os.List(c, opts)
}
// Create accepts a CreateOpts struct and creates a new CDN service using the
// values provided.
func Create(c *gophercloud.ServiceClient, opts os.CreateOptsBuilder) os.CreateResult {
return os.Create(c, opts)
}
// Get retrieves a specific service based on its unique ID.
func Get(c *gophercloud.ServiceClient, id string) os.GetResult {
return os.Get(c, id)
}
// Update accepts a UpdateOpts struct and updates an existing CDN service using
// the values provided.
func Update(c *gophercloud.ServiceClient, id string, patches []os.Patch) os.UpdateResult {
return os.Update(c, id, patches)
}
// Delete accepts a unique ID and deletes the CDN service associated with it.
func Delete(c *gophercloud.ServiceClient, id string) os.DeleteResult {
return os.Delete(c, id)
}

View File

@@ -0,0 +1,7 @@
// Package services provides information and interaction with the services API
// resource in the Rackspace CDN service. This API resource allows for
// listing, creating, updating, retrieving, and deleting services.
//
// A service represents an application that has its content cached to the edge
// nodes.
package services

View File

@@ -0,0 +1,224 @@
package rackspace
import (
"fmt"
"github.com/rackspace/gophercloud"
os "github.com/rackspace/gophercloud/openstack"
"github.com/rackspace/gophercloud/openstack/utils"
tokens2 "github.com/rackspace/gophercloud/rackspace/identity/v2/tokens"
)
const (
// RackspaceUSIdentity is an identity endpoint located in the United States.
RackspaceUSIdentity = "https://identity.api.rackspacecloud.com/v2.0/"
// RackspaceUKIdentity is an identity endpoint located in the UK.
RackspaceUKIdentity = "https://lon.identity.api.rackspacecloud.com/v2.0/"
)
const (
v20 = "v2.0"
)
// NewClient creates a client that's prepared to communicate with the Rackspace API, but is not
// yet authenticated. Most users will probably prefer using the AuthenticatedClient function
// instead.
//
// Provide the base URL of the identity endpoint you wish to authenticate against as "endpoint".
// Often, this will be either RackspaceUSIdentity or RackspaceUKIdentity.
func NewClient(endpoint string) (*gophercloud.ProviderClient, error) {
if endpoint == "" {
return os.NewClient(RackspaceUSIdentity)
}
return os.NewClient(endpoint)
}
// AuthenticatedClient logs in to Rackspace with the provided credentials and constructs a
// ProviderClient that's ready to operate.
//
// If the provided AuthOptions does not specify an explicit IdentityEndpoint, it will default to
// the canonical, production Rackspace US identity endpoint.
func AuthenticatedClient(options gophercloud.AuthOptions) (*gophercloud.ProviderClient, error) {
client, err := NewClient(options.IdentityEndpoint)
if err != nil {
return nil, err
}
err = Authenticate(client, options)
if err != nil {
return nil, err
}
return client, nil
}
// Authenticate or re-authenticate against the most recent identity service supported at the
// provided endpoint.
func Authenticate(client *gophercloud.ProviderClient, options gophercloud.AuthOptions) error {
versions := []*utils.Version{
&utils.Version{ID: v20, Priority: 20, Suffix: "/v2.0/"},
}
chosen, endpoint, err := utils.ChooseVersion(client, versions)
if err != nil {
return err
}
switch chosen.ID {
case v20:
return v2auth(client, endpoint, options)
default:
// The switch statement must be out of date from the versions list.
return fmt.Errorf("Unrecognized identity version: %s", chosen.ID)
}
}
// AuthenticateV2 explicitly authenticates with v2 of the identity service.
func AuthenticateV2(client *gophercloud.ProviderClient, options gophercloud.AuthOptions) error {
return v2auth(client, "", options)
}
func v2auth(client *gophercloud.ProviderClient, endpoint string, options gophercloud.AuthOptions) error {
v2Client := NewIdentityV2(client)
if endpoint != "" {
v2Client.Endpoint = endpoint
}
result := tokens2.Create(v2Client, tokens2.WrapOptions(options))
token, err := result.ExtractToken()
if err != nil {
return err
}
catalog, err := result.ExtractServiceCatalog()
if err != nil {
return err
}
if options.AllowReauth {
client.ReauthFunc = func() error {
return AuthenticateV2(client, options)
}
}
client.TokenID = token.ID
client.EndpointLocator = func(opts gophercloud.EndpointOpts) (string, error) {
return os.V2EndpointURL(catalog, opts)
}
return nil
}
// NewIdentityV2 creates a ServiceClient that may be used to access the v2 identity service.
func NewIdentityV2(client *gophercloud.ProviderClient) *gophercloud.ServiceClient {
v2Endpoint := client.IdentityBase + "v2.0/"
return &gophercloud.ServiceClient{
ProviderClient: client,
Endpoint: v2Endpoint,
}
}
// NewComputeV2 creates a ServiceClient that may be used to access the v2 compute service.
func NewComputeV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
eo.ApplyDefaults("compute")
url, err := client.EndpointLocator(eo)
if err != nil {
return nil, err
}
return &gophercloud.ServiceClient{
ProviderClient: client,
Endpoint: url,
}, nil
}
// NewObjectCDNV1 creates a ServiceClient that may be used with the Rackspace v1 CDN.
func NewObjectCDNV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
eo.ApplyDefaults("rax:object-cdn")
url, err := client.EndpointLocator(eo)
if err != nil {
return nil, err
}
return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil
}
// NewObjectStorageV1 creates a ServiceClient that may be used with the Rackspace v1 object storage package.
func NewObjectStorageV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
return os.NewObjectStorageV1(client, eo)
}
// NewBlockStorageV1 creates a ServiceClient that can be used to access the
// Rackspace Cloud Block Storage v1 API.
func NewBlockStorageV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
eo.ApplyDefaults("volume")
url, err := client.EndpointLocator(eo)
if err != nil {
return nil, err
}
return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil
}
// NewLBV1 creates a ServiceClient that can be used to access the Rackspace
// Cloud Load Balancer v1 API.
func NewLBV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
eo.ApplyDefaults("rax:load-balancer")
url, err := client.EndpointLocator(eo)
if err != nil {
return nil, err
}
return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil
}
// NewNetworkV2 creates a ServiceClient that can be used to access the Rackspace
// Networking v2 API.
func NewNetworkV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
eo.ApplyDefaults("network")
url, err := client.EndpointLocator(eo)
if err != nil {
return nil, err
}
return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil
}
// NewCDNV1 creates a ServiceClient that may be used to access the Rackspace v1
// CDN service.
func NewCDNV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
eo.ApplyDefaults("rax:cdn")
url, err := client.EndpointLocator(eo)
if err != nil {
return nil, err
}
return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil
}
// NewOrchestrationV1 creates a ServiceClient that may be used to access the v1 orchestration service.
func NewOrchestrationV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
eo.ApplyDefaults("orchestration")
url, err := client.EndpointLocator(eo)
if err != nil {
return nil, err
}
return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil
}
// NewRackConnectV3 creates a ServiceClient that may be used to access the v3 RackConnect service.
func NewRackConnectV3(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
eo.ApplyDefaults("rax:rackconnect")
url, err := client.EndpointLocator(eo)
if err != nil {
return nil, err
}
return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil
}
// NewDBV1 creates a ServiceClient that may be used to access the v1 DB service.
func NewDBV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
eo.ApplyDefaults("rax:database")
url, err := client.EndpointLocator(eo)
if err != nil {
return nil, err
}
return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil
}

View File

@@ -0,0 +1,12 @@
package bootfromvolume
import (
"github.com/rackspace/gophercloud"
osBFV "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/bootfromvolume"
osServers "github.com/rackspace/gophercloud/openstack/compute/v2/servers"
)
// Create requests the creation of a server from the given block device mapping.
func Create(client *gophercloud.ServiceClient, opts osServers.CreateOptsBuilder) osServers.CreateResult {
return osBFV.Create(client, opts)
}

View File

@@ -0,0 +1,43 @@
package flavors
import (
"github.com/rackspace/gophercloud"
os "github.com/rackspace/gophercloud/openstack/compute/v2/flavors"
"github.com/rackspace/gophercloud/pagination"
)
// ListOpts helps control the results returned by the List() function. For example, a flavor with a
// minDisk field of 10 will not be returned if you specify MinDisk set to 20.
type ListOpts struct {
// MinDisk and MinRAM, if provided, elide flavors that do not meet your criteria.
MinDisk int `q:"minDisk"`
MinRAM int `q:"minRam"`
// Marker specifies the ID of the last flavor in the previous page.
Marker string `q:"marker"`
// Limit instructs List to refrain from sending excessively large lists of flavors.
Limit int `q:"limit"`
}
// ToFlavorListQuery formats a ListOpts into a query string.
func (opts ListOpts) ToFlavorListQuery() (string, error) {
q, err := gophercloud.BuildQueryString(opts)
if err != nil {
return "", err
}
return q.String(), nil
}
// ListDetail enumerates the server images available to your account.
func ListDetail(client *gophercloud.ServiceClient, opts os.ListOptsBuilder) pagination.Pager {
return os.ListDetail(client, opts)
}
// Get returns details about a single flavor, identity by ID.
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,3 @@
// Package flavors provides information and interaction with the flavor
// API resource for the Rackspace Cloud Servers service.
package flavors

View File

@@ -0,0 +1,137 @@
// +build fixtures
package flavors
// ListOutput is a sample response of a flavor List request.
const ListOutput = `
{
"flavors": [
{
"OS-FLV-EXT-DATA:ephemeral": 0,
"OS-FLV-WITH-EXT-SPECS:extra_specs": {
"class": "performance1",
"disk_io_index": "40",
"number_of_data_disks": "0",
"policy_class": "performance_flavor",
"resize_policy_class": "performance_flavor"
},
"disk": 20,
"id": "performance1-1",
"links": [
{
"href": "https://iad.servers.api.rackspacecloud.com/v2/864477/flavors/performance1-1",
"rel": "self"
},
{
"href": "https://iad.servers.api.rackspacecloud.com/864477/flavors/performance1-1",
"rel": "bookmark"
}
],
"name": "1 GB Performance",
"ram": 1024,
"rxtx_factor": 200,
"swap": "",
"vcpus": 1
},
{
"OS-FLV-EXT-DATA:ephemeral": 20,
"OS-FLV-WITH-EXT-SPECS:extra_specs": {
"class": "performance1",
"disk_io_index": "40",
"number_of_data_disks": "1",
"policy_class": "performance_flavor",
"resize_policy_class": "performance_flavor"
},
"disk": 40,
"id": "performance1-2",
"links": [
{
"href": "https://iad.servers.api.rackspacecloud.com/v2/864477/flavors/performance1-2",
"rel": "self"
},
{
"href": "https://iad.servers.api.rackspacecloud.com/864477/flavors/performance1-2",
"rel": "bookmark"
}
],
"name": "2 GB Performance",
"ram": 2048,
"rxtx_factor": 400,
"swap": "",
"vcpus": 2
}
]
}`
// GetOutput is a sample response from a flavor Get request. Its contents correspond to the
// Performance1Flavor struct.
const GetOutput = `
{
"flavor": {
"OS-FLV-EXT-DATA:ephemeral": 0,
"OS-FLV-WITH-EXT-SPECS:extra_specs": {
"class": "performance1",
"disk_io_index": "40",
"number_of_data_disks": "0",
"policy_class": "performance_flavor",
"resize_policy_class": "performance_flavor"
},
"disk": 20,
"id": "performance1-1",
"links": [
{
"href": "https://iad.servers.api.rackspacecloud.com/v2/864477/flavors/performance1-1",
"rel": "self"
},
{
"href": "https://iad.servers.api.rackspacecloud.com/864477/flavors/performance1-1",
"rel": "bookmark"
}
],
"name": "1 GB Performance",
"ram": 1024,
"rxtx_factor": 200,
"swap": "",
"vcpus": 1
}
}
`
// Performance1Flavor is the expected result of parsing GetOutput, or the first element of
// ListOutput.
var Performance1Flavor = Flavor{
ID: "performance1-1",
Disk: 20,
RAM: 1024,
Name: "1 GB Performance",
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 = Flavor{
ID: "performance1-2",
Disk: 40,
RAM: 2048,
Name: "2 GB Performance",
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 = []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

@@ -0,0 +1,22 @@
package images
import (
"github.com/rackspace/gophercloud"
os "github.com/rackspace/gophercloud/openstack/compute/v2/images"
"github.com/rackspace/gophercloud/pagination"
)
// ListDetail enumerates the available server images.
func ListDetail(client *gophercloud.ServiceClient, opts os.ListOptsBuilder) pagination.Pager {
return os.ListDetail(client, opts)
}
// Get acquires additional detail about a specific image by ID.
func Get(client *gophercloud.ServiceClient, id string) os.GetResult {
return os.Get(client, id)
}
// ExtractImages interprets a page as a collection of server images.
func ExtractImages(page pagination.Page) ([]os.Image, error) {
return os.ExtractImages(page)
}

View File

@@ -0,0 +1,3 @@
// Package images provides information and interaction with the image
// API resource for the Rackspace Cloud Servers service.
package images

View File

@@ -0,0 +1,200 @@
// +build fixtures
package images
import (
os "github.com/rackspace/gophercloud/openstack/compute/v2/images"
)
// ListOutput is an example response from an /images/detail request.
const ListOutput = `
{
"images": [
{
"OS-DCF:diskConfig": "MANUAL",
"OS-EXT-IMG-SIZE:size": 1.017415075e+09,
"created": "2014-10-01T15:49:02Z",
"id": "30aa010e-080e-4d4b-a7f9-09fc55b07d69",
"links": [
{
"href": "https://iad.servers.api.rackspacecloud.com/v2/111222/images/30aa010e-080e-4d4b-a7f9-09fc55b07d69",
"rel": "self"
},
{
"href": "https://iad.servers.api.rackspacecloud.com/111222/images/30aa010e-080e-4d4b-a7f9-09fc55b07d69",
"rel": "bookmark"
},
{
"href": "https://iad.servers.api.rackspacecloud.com/111222/images/30aa010e-080e-4d4b-a7f9-09fc55b07d69",
"rel": "alternate",
"type": "application/vnd.openstack.image"
}
],
"metadata": {
"auto_disk_config": "disabled",
"cache_in_nova": "True",
"com.rackspace__1__build_core": "1",
"com.rackspace__1__build_managed": "1",
"com.rackspace__1__build_rackconnect": "1",
"com.rackspace__1__options": "0",
"com.rackspace__1__platform_target": "PublicCloud",
"com.rackspace__1__release_build_date": "2014-10-01_15-46-08",
"com.rackspace__1__release_id": "100",
"com.rackspace__1__release_version": "10",
"com.rackspace__1__source": "kickstart",
"com.rackspace__1__visible_core": "1",
"com.rackspace__1__visible_managed": "0",
"com.rackspace__1__visible_rackconnect": "0",
"image_type": "base",
"org.openstack__1__architecture": "x64",
"org.openstack__1__os_distro": "org.archlinux",
"org.openstack__1__os_version": "2014.8",
"os_distro": "arch",
"os_type": "linux",
"vm_mode": "hvm"
},
"minDisk": 20,
"minRam": 512,
"name": "Arch 2014.10 (PVHVM)",
"progress": 100,
"status": "ACTIVE",
"updated": "2014-10-01T19:37:58Z"
},
{
"OS-DCF:diskConfig": "AUTO",
"OS-EXT-IMG-SIZE:size": 1.060306463e+09,
"created": "2014-10-01T12:58:11Z",
"id": "e19a734c-c7e6-443a-830c-242209c4d65d",
"links": [
{
"href": "https://iad.servers.api.rackspacecloud.com/v2/111222/images/e19a734c-c7e6-443a-830c-242209c4d65d",
"rel": "self"
},
{
"href": "https://iad.servers.api.rackspacecloud.com/111222/images/e19a734c-c7e6-443a-830c-242209c4d65d",
"rel": "bookmark"
},
{
"href": "https://iad.servers.api.rackspacecloud.com/111222/images/e19a734c-c7e6-443a-830c-242209c4d65d",
"rel": "alternate",
"type": "application/vnd.openstack.image"
}
],
"metadata": {
"auto_disk_config": "True",
"cache_in_nova": "True",
"com.rackspace__1__build_core": "1",
"com.rackspace__1__build_managed": "1",
"com.rackspace__1__build_rackconnect": "1",
"com.rackspace__1__options": "0",
"com.rackspace__1__platform_target": "PublicCloud",
"com.rackspace__1__release_build_date": "2014-10-01_12-31-03",
"com.rackspace__1__release_id": "1007",
"com.rackspace__1__release_version": "6",
"com.rackspace__1__source": "kickstart",
"com.rackspace__1__visible_core": "1",
"com.rackspace__1__visible_managed": "1",
"com.rackspace__1__visible_rackconnect": "1",
"image_type": "base",
"org.openstack__1__architecture": "x64",
"org.openstack__1__os_distro": "com.ubuntu",
"org.openstack__1__os_version": "14.04",
"os_distro": "ubuntu",
"os_type": "linux",
"vm_mode": "xen"
},
"minDisk": 20,
"minRam": 512,
"name": "Ubuntu 14.04 LTS (Trusty Tahr)",
"progress": 100,
"status": "ACTIVE",
"updated": "2014-10-01T15:51:44Z"
}
]
}
`
// GetOutput is an example response from an /images request.
const GetOutput = `
{
"image": {
"OS-DCF:diskConfig": "AUTO",
"OS-EXT-IMG-SIZE:size": 1060306463,
"created": "2014-10-01T12:58:11Z",
"id": "e19a734c-c7e6-443a-830c-242209c4d65d",
"links": [
{
"href": "https://iad.servers.api.rackspacecloud.com/v2/111222/images/e19a734c-c7e6-443a-830c-242209c4d65d",
"rel": "self"
},
{
"href": "https://iad.servers.api.rackspacecloud.com/111222/images/e19a734c-c7e6-443a-830c-242209c4d65d",
"rel": "bookmark"
},
{
"href": "https://iad.servers.api.rackspacecloud.com/111222/images/e19a734c-c7e6-443a-830c-242209c4d65d",
"rel": "alternate",
"type": "application/vnd.openstack.image"
}
],
"metadata": {
"auto_disk_config": "True",
"cache_in_nova": "True",
"com.rackspace__1__build_core": "1",
"com.rackspace__1__build_managed": "1",
"com.rackspace__1__build_rackconnect": "1",
"com.rackspace__1__options": "0",
"com.rackspace__1__platform_target": "PublicCloud",
"com.rackspace__1__release_build_date": "2014-10-01_12-31-03",
"com.rackspace__1__release_id": "1007",
"com.rackspace__1__release_version": "6",
"com.rackspace__1__source": "kickstart",
"com.rackspace__1__visible_core": "1",
"com.rackspace__1__visible_managed": "1",
"com.rackspace__1__visible_rackconnect": "1",
"image_type": "base",
"org.openstack__1__architecture": "x64",
"org.openstack__1__os_distro": "com.ubuntu",
"org.openstack__1__os_version": "14.04",
"os_distro": "ubuntu",
"os_type": "linux",
"vm_mode": "xen"
},
"minDisk": 20,
"minRam": 512,
"name": "Ubuntu 14.04 LTS (Trusty Tahr)",
"progress": 100,
"status": "ACTIVE",
"updated": "2014-10-01T15:51:44Z"
}
}
`
// ArchImage is the first Image structure that should be parsed from ListOutput.
var ArchImage = os.Image{
ID: "30aa010e-080e-4d4b-a7f9-09fc55b07d69",
Name: "Arch 2014.10 (PVHVM)",
Created: "2014-10-01T15:49:02Z",
Updated: "2014-10-01T19:37:58Z",
MinDisk: 20,
MinRAM: 512,
Progress: 100,
Status: "ACTIVE",
}
// UbuntuImage is the second Image structure that should be parsed from ListOutput and
// the only image that should be extracted from GetOutput.
var UbuntuImage = os.Image{
ID: "e19a734c-c7e6-443a-830c-242209c4d65d",
Name: "Ubuntu 14.04 LTS (Trusty Tahr)",
Created: "2014-10-01T12:58:11Z",
Updated: "2014-10-01T15:51:44Z",
MinDisk: 20,
MinRAM: 512,
Progress: 100,
Status: "ACTIVE",
}
// ExpectedImageSlice is the collection of images that should be parsed from ListOutput,
// in order.
var ExpectedImageSlice = []os.Image{ArchImage, UbuntuImage}

View File

@@ -0,0 +1,33 @@
package keypairs
import (
"github.com/rackspace/gophercloud"
os "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs"
"github.com/rackspace/gophercloud/pagination"
)
// List returns a Pager that allows you to iterate over a collection of KeyPairs.
func List(client *gophercloud.ServiceClient) pagination.Pager {
return os.List(client)
}
// Create requests the creation of a new keypair on the server, or to import a pre-existing
// keypair.
func Create(client *gophercloud.ServiceClient, opts os.CreateOptsBuilder) os.CreateResult {
return os.Create(client, opts)
}
// Get returns public data about a previously uploaded KeyPair.
func Get(client *gophercloud.ServiceClient, name string) os.GetResult {
return os.Get(client, name)
}
// Delete requests the deletion of a previous stored KeyPair from the server.
func Delete(client *gophercloud.ServiceClient, name string) os.DeleteResult {
return os.Delete(client, name)
}
// ExtractKeyPairs interprets a page of results as a slice of KeyPairs.
func ExtractKeyPairs(page pagination.Page) ([]os.KeyPair, error) {
return os.ExtractKeyPairs(page)
}

View File

@@ -0,0 +1,3 @@
// Package keypairs provides information and interaction with the keypair
// API resource for the Rackspace Cloud Servers service.
package keypairs

View File

@@ -0,0 +1,3 @@
// Package networks provides information and interaction with the network
// API resource for the Rackspace Cloud Servers service.
package networks

View File

@@ -0,0 +1,89 @@
package networks
import (
"errors"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
)
// List returns a Pager which allows you to iterate over a collection of
// networks. It accepts a ListOpts struct, which allows you to filter and sort
// the returned collection for greater efficiency.
func List(c *gophercloud.ServiceClient) pagination.Pager {
createPage := func(r pagination.PageResult) pagination.Page {
return NetworkPage{pagination.SinglePageBase(r)}
}
return pagination.NewPager(c, listURL(c), createPage)
}
// Get retrieves a specific network based on its unique ID.
func Get(c *gophercloud.ServiceClient, id string) GetResult {
var res GetResult
_, res.Err = c.Get(getURL(c, id), &res.Body, nil)
return res
}
// CreateOptsBuilder is the interface options structs have to satisfy in order
// to be used in the main Create operation in this package. Since many
// extensions decorate or modify the common logic, it is useful for them to
// satisfy a basic interface in order for them to be used.
type CreateOptsBuilder interface {
ToNetworkCreateMap() (map[string]interface{}, error)
}
// CreateOpts is the common options struct used in this package's Create
// operation.
type CreateOpts struct {
// REQUIRED. See Network object for more info.
CIDR string
// REQUIRED. See Network object for more info.
Label string
}
// ToNetworkCreateMap casts a CreateOpts struct to a map.
func (opts CreateOpts) ToNetworkCreateMap() (map[string]interface{}, error) {
n := make(map[string]interface{})
if opts.CIDR == "" {
return nil, errors.New("Required field CIDR not set.")
}
if opts.Label == "" {
return nil, errors.New("Required field Label not set.")
}
n["label"] = opts.Label
n["cidr"] = opts.CIDR
return map[string]interface{}{"network": n}, nil
}
// Create accepts a CreateOpts struct and creates a new network using the values
// provided. This operation does not actually require a request body, i.e. the
// CreateOpts struct argument can be empty.
//
// The tenant ID that is contained in the URI is the tenant that creates the
// network. An admin user, however, has the option of specifying another tenant
// ID in the CreateOpts struct.
func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult {
var res CreateResult
reqBody, err := opts.ToNetworkCreateMap()
if err != nil {
res.Err = err
return res
}
// Send request to API
_, res.Err = c.Post(createURL(c), reqBody, &res.Body, &gophercloud.RequestOpts{
OkCodes: []int{200, 201, 202},
})
return res
}
// Delete accepts a unique ID and deletes the network associated with it.
func Delete(c *gophercloud.ServiceClient, networkID string) DeleteResult {
var res DeleteResult
_, res.Err = c.Delete(deleteURL(c, networkID), nil)
return res
}

View File

@@ -0,0 +1,81 @@
package networks
import (
"github.com/mitchellh/mapstructure"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
)
type commonResult struct {
gophercloud.Result
}
// Extract is a function that accepts a result and extracts a network resource.
func (r commonResult) Extract() (*Network, error) {
if r.Err != nil {
return nil, r.Err
}
var res struct {
Network *Network `json:"network"`
}
err := mapstructure.Decode(r.Body, &res)
return res.Network, err
}
// CreateResult represents the result of a create operation.
type CreateResult struct {
commonResult
}
// GetResult represents the result of a get operation.
type GetResult struct {
commonResult
}
// DeleteResult represents the result of a delete operation.
type DeleteResult struct {
gophercloud.ErrResult
}
// Network represents, well, a network.
type Network struct {
// UUID for the network
ID string `mapstructure:"id" json:"id"`
// Human-readable name for the network. Might not be unique.
Label string `mapstructure:"label" json:"label"`
// Classless Inter-Domain Routing
CIDR string `mapstructure:"cidr" json:"cidr"`
}
// NetworkPage is the page returned by a pager when traversing over a
// collection of networks.
type NetworkPage struct {
pagination.SinglePageBase
}
// IsEmpty returns true if the NetworkPage contains no Networks.
func (r NetworkPage) IsEmpty() (bool, error) {
networks, err := ExtractNetworks(r)
if err != nil {
return true, err
}
return len(networks) == 0, nil
}
// ExtractNetworks accepts a Page struct, specifically a NetworkPage struct,
// and extracts the elements into a slice of Network structs. In other words,
// a generic collection is mapped into a relevant slice.
func ExtractNetworks(page pagination.Page) ([]Network, error) {
var resp struct {
Networks []Network `mapstructure:"networks" json:"networks"`
}
err := mapstructure.Decode(page.(NetworkPage).Body, &resp)
return resp.Networks, err
}

View File

@@ -0,0 +1,27 @@
package networks
import "github.com/rackspace/gophercloud"
func resourceURL(c *gophercloud.ServiceClient, id string) string {
return c.ServiceURL("os-networksv2", id)
}
func rootURL(c *gophercloud.ServiceClient) string {
return c.ServiceURL("os-networksv2")
}
func getURL(c *gophercloud.ServiceClient, id string) string {
return resourceURL(c, id)
}
func listURL(c *gophercloud.ServiceClient) string {
return rootURL(c)
}
func createURL(c *gophercloud.ServiceClient) string {
return rootURL(c)
}
func deleteURL(c *gophercloud.ServiceClient, id string) string {
return resourceURL(c, id)
}

View File

@@ -0,0 +1,116 @@
package servers
import (
"github.com/rackspace/gophercloud"
os "github.com/rackspace/gophercloud/openstack/compute/v2/servers"
"github.com/rackspace/gophercloud/pagination"
)
// List makes a request against the API to list servers accessible to you.
func List(client *gophercloud.ServiceClient, opts os.ListOptsBuilder) pagination.Pager {
return os.List(client, opts)
}
// Create requests a server to be provisioned to the user in the current tenant.
func Create(client *gophercloud.ServiceClient, opts os.CreateOptsBuilder) os.CreateResult {
return os.Create(client, opts)
}
// Update requests an existing server to be updated with the supplied options.
func Update(client *gophercloud.ServiceClient, id string, opts os.UpdateOptsBuilder) os.UpdateResult {
return os.Update(client, id, opts)
}
// Delete requests that a server previously provisioned be removed from your account.
func Delete(client *gophercloud.ServiceClient, id string) os.DeleteResult {
return os.Delete(client, id)
}
// Get requests details on a single server, by ID.
func Get(client *gophercloud.ServiceClient, id string) os.GetResult {
return os.Get(client, id)
}
// ChangeAdminPassword alters the administrator or root password for a specified server.
func ChangeAdminPassword(client *gophercloud.ServiceClient, id, newPassword string) os.ActionResult {
return os.ChangeAdminPassword(client, id, newPassword)
}
// Reboot requests that a given server reboot. Two methods exist for rebooting a server:
//
// os.HardReboot (aka PowerCycle) restarts the server instance by physically cutting power to the
// machine, or if a VM, terminating it at the hypervisor level. It's done. Caput. Full stop. Then,
// after a brief wait, power is restored or the VM instance restarted.
//
// os.SoftReboot (aka OSReboot) simply tells the OS to restart under its own procedures. E.g., in
// Linux, asking it to enter runlevel 6, or executing "sudo shutdown -r now", or by asking Windows to restart the machine.
func Reboot(client *gophercloud.ServiceClient, id string, how os.RebootMethod) os.ActionResult {
return os.Reboot(client, id, how)
}
// Rebuild will reprovision the server according to the configuration options provided in the
// RebuildOpts struct.
func Rebuild(client *gophercloud.ServiceClient, id string, opts os.RebuildOptsBuilder) os.RebuildResult {
return os.Rebuild(client, id, opts)
}
// Resize instructs the provider to change the flavor of the server.
// Note that this implies rebuilding it.
// Unfortunately, one cannot pass rebuild parameters to the resize function.
// When the resize completes, the server will be in RESIZE_VERIFY state.
// While in this state, you can explore the use of the new server's configuration.
// If you like it, call ConfirmResize() to commit the resize permanently.
// Otherwise, call RevertResize() to restore the old configuration.
func Resize(client *gophercloud.ServiceClient, id string, opts os.ResizeOptsBuilder) os.ActionResult {
return os.Resize(client, id, opts)
}
// ConfirmResize confirms a previous resize operation on a server.
// See Resize() for more details.
func ConfirmResize(client *gophercloud.ServiceClient, id string) os.ActionResult {
return os.ConfirmResize(client, id)
}
// WaitForStatus will continually poll a server until it successfully transitions to a specified
// status. It will do this for at most the number of seconds specified.
func WaitForStatus(c *gophercloud.ServiceClient, id, status string, secs int) error {
return os.WaitForStatus(c, id, status, secs)
}
// ExtractServers interprets the results of a single page from a List() call, producing a slice of Server entities.
func ExtractServers(page pagination.Page) ([]os.Server, error) {
return os.ExtractServers(page)
}
// ListAddresses makes a request against the API to list the servers IP addresses.
func ListAddresses(client *gophercloud.ServiceClient, id string) pagination.Pager {
return os.ListAddresses(client, id)
}
// ExtractAddresses interprets the results of a single page from a ListAddresses() call, producing a map of Address slices.
func ExtractAddresses(page pagination.Page) (map[string][]os.Address, error) {
return os.ExtractAddresses(page)
}
// ListAddressesByNetwork makes a request against the API to list the servers IP addresses
// for the given network.
func ListAddressesByNetwork(client *gophercloud.ServiceClient, id, network string) pagination.Pager {
return os.ListAddressesByNetwork(client, id, network)
}
// ExtractNetworkAddresses interprets the results of a single page from a ListAddressesByNetwork() call, producing a map of Address slices.
func ExtractNetworkAddresses(page pagination.Page) ([]os.Address, error) {
return os.ExtractNetworkAddresses(page)
}
// Metadata requests all the metadata for the given server ID.
func Metadata(client *gophercloud.ServiceClient, id string) os.GetMetadataResult {
return os.Metadata(client, id)
}
// UpdateMetadata updates (or creates) all the metadata specified by opts for the given server ID.
// This operation does not affect already-existing metadata that is not specified
// by opts.
func UpdateMetadata(client *gophercloud.ServiceClient, id string, opts os.UpdateMetadataOptsBuilder) os.UpdateMetadataResult {
return os.UpdateMetadata(client, id, opts)
}

View File

@@ -0,0 +1,3 @@
// Package servers provides information and interaction with the server
// API resource for the Rackspace Cloud Servers service.
package servers

View File

@@ -0,0 +1,574 @@
// +build fixtures
package servers
import (
os "github.com/rackspace/gophercloud/openstack/compute/v2/servers"
)
// ListOutput is the recorded output of a Rackspace servers.List request.
const ListOutput = `
{
"servers": [
{
"OS-DCF:diskConfig": "MANUAL",
"OS-EXT-STS:power_state": 1,
"OS-EXT-STS:task_state": null,
"OS-EXT-STS:vm_state": "active",
"accessIPv4": "1.2.3.4",
"accessIPv6": "1111:4822:7818:121:2000:9b5e:7438:a2d0",
"addresses": {
"private": [
{
"addr": "10.208.230.113",
"version": 4
}
],
"public": [
{
"addr": "2001:4800:7818:101:2000:9b5e:7428:a2d0",
"version": 6
},
{
"addr": "104.130.131.164",
"version": 4
}
]
},
"created": "2014-09-23T12:34:58Z",
"flavor": {
"id": "performance1-8",
"links": [
{
"href": "https://dfw.servers.api.rackspacecloud.com/111111/flavors/performance1-8",
"rel": "bookmark"
}
]
},
"hostId": "e8951a524bc465b0898aeac7674da6fe1495e253ae1ea17ddb2c2475",
"id": "59818cee-bc8c-44eb-8073-673ee65105f7",
"image": {
"id": "255df5fb-e3d4-45a3-9a07-c976debf7c14",
"links": [
{
"href": "https://dfw.servers.api.rackspacecloud.com/111111/images/255df5fb-e3d4-45a3-9a07-c976debf7c14",
"rel": "bookmark"
}
]
},
"key_name": "mykey",
"links": [
{
"href": "https://dfw.servers.api.rackspacecloud.com/v2/111111/servers/59818cee-bc8c-44eb-8073-673ee65105f7",
"rel": "self"
},
{
"href": "https://dfw.servers.api.rackspacecloud.com/111111/servers/59818cee-bc8c-44eb-8073-673ee65105f7",
"rel": "bookmark"
}
],
"metadata": {},
"name": "devstack",
"progress": 100,
"status": "ACTIVE",
"tenant_id": "111111",
"updated": "2014-09-23T12:38:19Z",
"user_id": "14ae7bb21d81422694655f3cc30f2930"
},
{
"OS-DCF:diskConfig": "MANUAL",
"OS-EXT-STS:power_state": 1,
"OS-EXT-STS:task_state": null,
"OS-EXT-STS:vm_state": "active",
"accessIPv4": "1.1.2.3",
"accessIPv6": "2222:4444:7817:101:be76:4eff:f0e5:9e02",
"addresses": {
"private": [
{
"addr": "10.10.20.30",
"version": 4
}
],
"public": [
{
"addr": "1.1.2.3",
"version": 4
},
{
"addr": "2222:4444:7817:101:be76:4eff:f0e5:9e02",
"version": 6
}
]
},
"created": "2014-07-21T19:32:55Z",
"flavor": {
"id": "performance1-2",
"links": [
{
"href": "https://dfw.servers.api.rackspacecloud.com/111111/flavors/performance1-2",
"rel": "bookmark"
}
]
},
"hostId": "f859679906d6b1a38c1bd516b78f4dcc7d5fcf012578fa3ce460716c",
"id": "25f1c7f5-e00a-4715-b354-16e24b2f4630",
"image": {
"id": "bb02b1a3-bc77-4d17-ab5b-421d89850fca",
"links": [
{
"href": "https://dfw.servers.api.rackspacecloud.com/111111/images/bb02b1a3-bc77-4d17-ab5b-421d89850fca",
"rel": "bookmark"
}
]
},
"key_name": "otherkey",
"links": [
{
"href": "https://dfw.servers.api.rackspacecloud.com/v2/111111/servers/25f1c7f5-e00a-4715-b355-16e24b2f4630",
"rel": "self"
},
{
"href": "https://dfw.servers.api.rackspacecloud.com/111111/servers/25f1c7f5-e00a-4715-b355-16e24b2f4630",
"rel": "bookmark"
}
],
"metadata": {},
"name": "peril-dfw",
"progress": 100,
"status": "ACTIVE",
"tenant_id": "111111",
"updated": "2014-07-21T19:34:24Z",
"user_id": "14ae7bb21d81422694655f3cc30f2930"
}
]
}
`
// GetOutput is the recorded output of a Rackspace servers.Get request.
const GetOutput = `
{
"server": {
"OS-DCF:diskConfig": "AUTO",
"OS-EXT-STS:power_state": 1,
"OS-EXT-STS:task_state": null,
"OS-EXT-STS:vm_state": "active",
"accessIPv4": "1.2.4.8",
"accessIPv6": "2001:4800:6666:105:2a0f:c056:f594:7777",
"addresses": {
"private": [
{
"addr": "10.20.40.80",
"version": 4
}
],
"public": [
{
"addr": "1.2.4.8",
"version": 4
},
{
"addr": "2001:4800:6666:105:2a0f:c056:f594:7777",
"version": 6
}
]
},
"created": "2014-10-21T14:42:16Z",
"flavor": {
"id": "performance1-1",
"links": [
{
"href": "https://dfw.servers.api.rackspacecloud.com/111111/flavors/performance1-1",
"rel": "bookmark"
}
]
},
"hostId": "430d2ae02de0a7af77012c94778145eccf67e75b1fac0528aa10d4a7",
"id": "8c65cb68-0681-4c30-bc88-6b83a8a26aee",
"image": {
"id": "e19a734c-c7e6-443a-830c-242209c4d65d",
"links": [
{
"href": "https://dfw.servers.api.rackspacecloud.com/111111/images/e19a734c-c7e6-443a-830c-242209c4d65d",
"rel": "bookmark"
}
]
},
"key_name": null,
"links": [
{
"href": "https://dfw.servers.api.rackspacecloud.com/v2/111111/servers/8c65cb68-0681-4c30-bc88-6b83a8a26aee",
"rel": "self"
},
{
"href": "https://dfw.servers.api.rackspacecloud.com/111111/servers/8c65cb68-0681-4c30-bc88-6b83a8a26aee",
"rel": "bookmark"
}
],
"metadata": {},
"name": "Gophercloud-pxpGGuey",
"progress": 100,
"status": "ACTIVE",
"tenant_id": "111111",
"updated": "2014-10-21T14:42:57Z",
"user_id": "14ae7bb21d81423694655f4dd30f2930"
}
}
`
// UpdateOutput is the recorded output of a Rackspace servers.Update request.
const UpdateOutput = `
{
"server": {
"OS-DCF:diskConfig": "AUTO",
"OS-EXT-STS:power_state": 1,
"OS-EXT-STS:task_state": null,
"OS-EXT-STS:vm_state": "active",
"accessIPv4": "1.2.4.8",
"accessIPv6": "2001:4800:6666:105:2a0f:c056:f594:7777",
"addresses": {
"private": [
{
"addr": "10.20.40.80",
"version": 4
}
],
"public": [
{
"addr": "1.2.4.8",
"version": 4
},
{
"addr": "2001:4800:6666:105:2a0f:c056:f594:7777",
"version": 6
}
]
},
"created": "2014-10-21T14:42:16Z",
"flavor": {
"id": "performance1-1",
"links": [
{
"href": "https://dfw.servers.api.rackspacecloud.com/111111/flavors/performance1-1",
"rel": "bookmark"
}
]
},
"hostId": "430d2ae02de0a7af77012c94778145eccf67e75b1fac0528aa10d4a7",
"id": "8c65cb68-0681-4c30-bc88-6b83a8a26aee",
"image": {
"id": "e19a734c-c7e6-443a-830c-242209c4d65d",
"links": [
{
"href": "https://dfw.servers.api.rackspacecloud.com/111111/images/e19a734c-c7e6-443a-830c-242209c4d65d",
"rel": "bookmark"
}
]
},
"key_name": null,
"links": [
{
"href": "https://dfw.servers.api.rackspacecloud.com/v2/111111/servers/8c65cb68-0681-4c30-bc88-6b83a8a26aee",
"rel": "self"
},
{
"href": "https://dfw.servers.api.rackspacecloud.com/111111/servers/8c65cb68-0681-4c30-bc88-6b83a8a26aee",
"rel": "bookmark"
}
],
"metadata": {},
"name": "test-server-updated",
"progress": 100,
"status": "ACTIVE",
"tenant_id": "111111",
"updated": "2014-10-21T14:42:57Z",
"user_id": "14ae7bb21d81423694655f4dd30f2930"
}
}
`
// CreateOutput contains a sample of Rackspace's response to a Create call.
const CreateOutput = `
{
"server": {
"OS-DCF:diskConfig": "AUTO",
"adminPass": "v7tADqbE5pr9",
"id": "bb63327b-6a2f-34bc-b0ef-4b6d97ea637e",
"links": [
{
"href": "https://dfw.servers.api.rackspacecloud.com/v2/111111/servers/bb63327b-6a2f-34bc-b0ef-4b6d97ea637e",
"rel": "self"
},
{
"href": "https://dfw.servers.api.rackspacecloud.com/111111/servers/bb63327b-6a2f-34bc-b0ef-4b6d97ea637e",
"rel": "bookmark"
}
]
}
}
`
// DevstackServer is the expected first result from parsing ListOutput.
var DevstackServer = os.Server{
ID: "59818cee-bc8c-44eb-8073-673ee65105f7",
Name: "devstack",
TenantID: "111111",
UserID: "14ae7bb21d81422694655f3cc30f2930",
HostID: "e8951a524bc465b0898aeac7674da6fe1495e253ae1ea17ddb2c2475",
Updated: "2014-09-23T12:38:19Z",
Created: "2014-09-23T12:34:58Z",
AccessIPv4: "1.2.3.4",
AccessIPv6: "1111:4822:7818:121:2000:9b5e:7438:a2d0",
Progress: 100,
Status: "ACTIVE",
Image: map[string]interface{}{
"id": "255df5fb-e3d4-45a3-9a07-c976debf7c14",
"links": []interface{}{
map[string]interface{}{
"href": "https://dfw.servers.api.rackspacecloud.com/111111/images/255df5fb-e3d4-45a3-9a07-c976debf7c14",
"rel": "bookmark",
},
},
},
Flavor: map[string]interface{}{
"id": "performance1-8",
"links": []interface{}{
map[string]interface{}{
"href": "https://dfw.servers.api.rackspacecloud.com/111111/flavors/performance1-8",
"rel": "bookmark",
},
},
},
Addresses: map[string]interface{}{
"private": []interface{}{
map[string]interface{}{
"addr": "10.20.30.40",
"version": float64(4.0),
},
},
"public": []interface{}{
map[string]interface{}{
"addr": "1111:4822:7818:121:2000:9b5e:7438:a2d0",
"version": float64(6.0),
},
map[string]interface{}{
"addr": "1.2.3.4",
"version": float64(4.0),
},
},
},
Metadata: map[string]interface{}{},
Links: []interface{}{
map[string]interface{}{
"href": "https://dfw.servers.api.rackspacecloud.com/v2/111111/servers/59918cee-bd9d-44eb-8173-673ee75105f7",
"rel": "self",
},
map[string]interface{}{
"href": "https://dfw.servers.api.rackspacecloud.com/v2/111111/servers/59818cee-bc8c-44eb-8073-673ee65105f7",
"rel": "bookmark",
},
},
KeyName: "mykey",
AdminPass: "",
}
// PerilServer is the expected second result from parsing ListOutput.
var PerilServer = os.Server{
ID: "25f1c7f5-e00a-4715-b354-16e24b2f4630",
Name: "peril-dfw",
TenantID: "111111",
UserID: "14ae7bb21d81422694655f3cc30f2930",
HostID: "f859679906d6b1a38c1bd516b78f4dcc7d5fcf012578fa3ce460716c",
Updated: "2014-07-21T19:34:24Z",
Created: "2014-07-21T19:32:55Z",
AccessIPv4: "1.1.2.3",
AccessIPv6: "2222:4444:7817:101:be76:4eff:f0e5:9e02",
Progress: 100,
Status: "ACTIVE",
Image: map[string]interface{}{
"id": "bb02b1a3-bc77-4d17-ab5b-421d89850fca",
"links": []interface{}{
map[string]interface{}{
"href": "https://dfw.servers.api.rackspacecloud.com/111111/images/bb02b1a3-bc77-4d17-ab5b-421d89850fca",
"rel": "bookmark",
},
},
},
Flavor: map[string]interface{}{
"id": "performance1-2",
"links": []interface{}{
map[string]interface{}{
"href": "https://dfw.servers.api.rackspacecloud.com/111111/flavors/performance1-2",
"rel": "bookmark",
},
},
},
Addresses: map[string]interface{}{
"private": []interface{}{
map[string]interface{}{
"addr": "10.10.20.30",
"version": float64(4.0),
},
},
"public": []interface{}{
map[string]interface{}{
"addr": "2222:4444:7817:101:be76:4eff:f0e5:9e02",
"version": float64(6.0),
},
map[string]interface{}{
"addr": "1.1.2.3",
"version": float64(4.0),
},
},
},
Metadata: map[string]interface{}{},
Links: []interface{}{
map[string]interface{}{
"href": "https://dfw.servers.api.rackspacecloud.com/v2/111111/servers/25f1c7f5-e00a-4715-b355-16e24b2f4630",
"rel": "self",
},
map[string]interface{}{
"href": "https://dfw.servers.api.rackspacecloud.com/v2/111111/servers/25f1c7f5-e00a-4715-b355-16e24b2f4630",
"rel": "bookmark",
},
},
KeyName: "otherkey",
AdminPass: "",
}
// GophercloudServer is the expected result from parsing GetOutput.
var GophercloudServer = os.Server{
ID: "8c65cb68-0681-4c30-bc88-6b83a8a26aee",
Name: "Gophercloud-pxpGGuey",
TenantID: "111111",
UserID: "14ae7bb21d81423694655f4dd30f2930",
HostID: "430d2ae02de0a7af77012c94778145eccf67e75b1fac0528aa10d4a7",
Updated: "2014-10-21T14:42:57Z",
Created: "2014-10-21T14:42:16Z",
AccessIPv4: "1.2.4.8",
AccessIPv6: "2001:4800:6666:105:2a0f:c056:f594:7777",
Progress: 100,
Status: "ACTIVE",
Image: map[string]interface{}{
"id": "e19a734c-c7e6-443a-830c-242209c4d65d",
"links": []interface{}{
map[string]interface{}{
"href": "https://dfw.servers.api.rackspacecloud.com/111111/images/e19a734c-c7e6-443a-830c-242209c4d65d",
"rel": "bookmark",
},
},
},
Flavor: map[string]interface{}{
"id": "performance1-1",
"links": []interface{}{
map[string]interface{}{
"href": "https://dfw.servers.api.rackspacecloud.com/111111/flavors/performance1-1",
"rel": "bookmark",
},
},
},
Addresses: map[string]interface{}{
"private": []interface{}{
map[string]interface{}{
"addr": "10.20.40.80",
"version": float64(4.0),
},
},
"public": []interface{}{
map[string]interface{}{
"addr": "2001:4800:6666:105:2a0f:c056:f594:7777",
"version": float64(6.0),
},
map[string]interface{}{
"addr": "1.2.4.8",
"version": float64(4.0),
},
},
},
Metadata: map[string]interface{}{},
Links: []interface{}{
map[string]interface{}{
"href": "https://dfw.servers.api.rackspacecloud.com/v2/111111/servers/8c65cb68-0681-4c30-bc88-6b83a8a26aee",
"rel": "self",
},
map[string]interface{}{
"href": "https://dfw.servers.api.rackspacecloud.com/111111/servers/8c65cb68-0681-4c30-bc88-6b83a8a26aee",
"rel": "bookmark",
},
},
KeyName: "",
AdminPass: "",
}
// GophercloudUpdatedServer is the expected result from parsing UpdateOutput.
var GophercloudUpdatedServer = os.Server{
ID: "8c65cb68-0681-4c30-bc88-6b83a8a26aee",
Name: "test-server-updated",
TenantID: "111111",
UserID: "14ae7bb21d81423694655f4dd30f2930",
HostID: "430d2ae02de0a7af77012c94778145eccf67e75b1fac0528aa10d4a7",
Updated: "2014-10-21T14:42:57Z",
Created: "2014-10-21T14:42:16Z",
AccessIPv4: "1.2.4.8",
AccessIPv6: "2001:4800:6666:105:2a0f:c056:f594:7777",
Progress: 100,
Status: "ACTIVE",
Image: map[string]interface{}{
"id": "e19a734c-c7e6-443a-830c-242209c4d65d",
"links": []interface{}{
map[string]interface{}{
"href": "https://dfw.servers.api.rackspacecloud.com/111111/images/e19a734c-c7e6-443a-830c-242209c4d65d",
"rel": "bookmark",
},
},
},
Flavor: map[string]interface{}{
"id": "performance1-1",
"links": []interface{}{
map[string]interface{}{
"href": "https://dfw.servers.api.rackspacecloud.com/111111/flavors/performance1-1",
"rel": "bookmark",
},
},
},
Addresses: map[string]interface{}{
"private": []interface{}{
map[string]interface{}{
"addr": "10.20.40.80",
"version": float64(4.0),
},
},
"public": []interface{}{
map[string]interface{}{
"addr": "2001:4800:6666:105:2a0f:c056:f594:7777",
"version": float64(6.0),
},
map[string]interface{}{
"addr": "1.2.4.8",
"version": float64(4.0),
},
},
},
Metadata: map[string]interface{}{},
Links: []interface{}{
map[string]interface{}{
"href": "https://dfw.servers.api.rackspacecloud.com/v2/111111/servers/8c65cb68-0681-4c30-bc88-6b83a8a26aee",
"rel": "self",
},
map[string]interface{}{
"href": "https://dfw.servers.api.rackspacecloud.com/111111/servers/8c65cb68-0681-4c30-bc88-6b83a8a26aee",
"rel": "bookmark",
},
},
KeyName: "",
AdminPass: "",
}
// CreatedServer is the partial Server struct that can be parsed from CreateOutput.
var CreatedServer = os.Server{
ID: "bb63327b-6a2f-34bc-b0ef-4b6d97ea637e",
AdminPass: "v7tADqbE5pr9",
Links: []interface{}{},
}
// ExpectedServerSlice is the collection of servers, in order, that should be parsed from ListOutput.
var ExpectedServerSlice = []os.Server{DevstackServer, PerilServer}

View File

@@ -0,0 +1,178 @@
package servers
import (
"github.com/rackspace/gophercloud/openstack/compute/v2/extensions/bootfromvolume"
"github.com/rackspace/gophercloud/openstack/compute/v2/extensions/diskconfig"
os "github.com/rackspace/gophercloud/openstack/compute/v2/servers"
)
// CreateOpts specifies all of the options that Rackspace accepts in its Create request, including
// the union of all extensions that Rackspace supports.
type CreateOpts struct {
// Name [required] is the name to assign to the newly launched server.
Name string
// 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
// 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
// UserData [optional] contains configuration information or scripts to use upon launch.
// Create will base64-encode it for you.
UserData []byte
// AvailabilityZone [optional] in which to launch the server.
AvailabilityZone string
// Networks [optional] dictates how this server will be attached to available networks.
// By default, the server will be attached to all isolated networks for the tenant.
Networks []os.Network
// Metadata [optional] contains key-value pairs (up to 255 bytes each) to attach to the server.
Metadata map[string]string
// 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
// AdminPass [optional] sets the root user password. If not set, a randomly-generated
// password will be created and returned in the response.
AdminPass string
// Rackspace-specific extensions begin here.
// KeyPair [optional] specifies the name of the SSH KeyPair to be injected into the newly launched
// server. See the "keypairs" extension in OpenStack compute v2.
KeyPair string
// DiskConfig [optional] controls how the created server's disk is partitioned. See the "diskconfig"
// extension in OpenStack compute v2.
DiskConfig diskconfig.DiskConfig
// BlockDevice [optional] will create the server from a volume, which is created from an image,
// a snapshot, or another volume.
BlockDevice []bootfromvolume.BlockDevice
}
// ToServerCreateMap constructs a request body using all of the OpenStack extensions that are
// active on Rackspace.
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,
Networks: opts.Networks,
Metadata: opts.Metadata,
Personality: opts.Personality,
ConfigDrive: opts.ConfigDrive,
AdminPass: opts.AdminPass,
}
drive := diskconfig.CreateOptsExt{
CreateOptsBuilder: base,
DiskConfig: opts.DiskConfig,
}
res, err := drive.ToServerCreateMap()
if err != nil {
return nil, err
}
if len(opts.BlockDevice) != 0 {
bfv := bootfromvolume.CreateOptsExt{
CreateOptsBuilder: drive,
BlockDevice: opts.BlockDevice,
}
res, err = bfv.ToServerCreateMap()
if err != nil {
return nil, err
}
}
// 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
}
// RebuildOpts represents all of the configuration options used in a server rebuild operation that
// are supported by Rackspace.
type RebuildOpts struct {
// Required. The ID of the image you want your server to be provisioned on
ImageID string
// Name to set the server to
Name string
// Required. The server's admin password
AdminPass string
// AccessIPv4 [optional] provides a new IPv4 address for the instance.
AccessIPv4 string
// AccessIPv6 [optional] provides a new IPv6 address for the instance.
AccessIPv6 string
// Metadata [optional] contains key-value pairs (up to 255 bytes each) to attach to the server.
Metadata map[string]string
// 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.
// DiskConfig [optional] controls how the created server's disk is partitioned. See the "diskconfig"
// extension in OpenStack compute v2.
DiskConfig diskconfig.DiskConfig
}
// ToServerRebuildMap constructs a request body using all of the OpenStack extensions that are
// active on Rackspace.
func (opts RebuildOpts) ToServerRebuildMap() (map[string]interface{}, error) {
base := os.RebuildOpts{
ImageID: opts.ImageID,
Name: opts.Name,
AdminPass: opts.AdminPass,
AccessIPv4: opts.AccessIPv4,
AccessIPv6: opts.AccessIPv6,
Metadata: opts.Metadata,
Personality: opts.Personality,
}
drive := diskconfig.RebuildOptsExt{
RebuildOptsBuilder: base,
DiskConfig: opts.DiskConfig,
}
return drive.ToServerRebuildMap()
}

View File

@@ -0,0 +1,45 @@
package virtualinterfaces
import (
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
)
// List returns a Pager which allows you to iterate over a collection of
// networks. It accepts a ListOpts struct, which allows you to filter and sort
// the returned collection for greater efficiency.
func List(c *gophercloud.ServiceClient, instanceID string) pagination.Pager {
createPage := func(r pagination.PageResult) pagination.Page {
return VirtualInterfacePage{pagination.SinglePageBase(r)}
}
return pagination.NewPager(c, listURL(c, instanceID), createPage)
}
// Create creates a new virtual interface for a network and attaches the network
// to the server instance.
func Create(c *gophercloud.ServiceClient, instanceID, networkID string) CreateResult {
var res CreateResult
reqBody := map[string]map[string]string{
"virtual_interface": {
"network_id": networkID,
},
}
// Send request to API
_, res.Err = c.Post(createURL(c, instanceID), reqBody, &res.Body, &gophercloud.RequestOpts{
OkCodes: []int{200, 201, 202},
})
return res
}
// Delete deletes the interface with interfaceID attached to the instance with
// instanceID.
func Delete(c *gophercloud.ServiceClient, instanceID, interfaceID string) DeleteResult {
var res DeleteResult
_, res.Err = c.Delete(deleteURL(c, instanceID, interfaceID), &gophercloud.RequestOpts{
OkCodes: []int{200, 204},
})
return res
}

View File

@@ -0,0 +1,81 @@
package virtualinterfaces
import (
"github.com/mitchellh/mapstructure"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
)
type commonResult struct {
gophercloud.Result
}
// Extract is a function that accepts a result and extracts a network resource.
func (r commonResult) Extract() (*VirtualInterface, error) {
if r.Err != nil {
return nil, r.Err
}
var res struct {
VirtualInterfaces []VirtualInterface `mapstructure:"virtual_interfaces" json:"virtual_interfaces"`
}
err := mapstructure.Decode(r.Body, &res)
return &res.VirtualInterfaces[0], err
}
// CreateResult represents the result of a create operation.
type CreateResult struct {
commonResult
}
// DeleteResult represents the result of a delete operation.
type DeleteResult struct {
gophercloud.ErrResult
}
// IPAddress represents a vitual address attached to a VirtualInterface.
type IPAddress struct {
Address string `mapstructure:"address" json:"address"`
NetworkID string `mapstructure:"network_id" json:"network_id"`
NetworkLabel string `mapstructure:"network_label" json:"network_label"`
}
// VirtualInterface represents a virtual interface.
type VirtualInterface struct {
// UUID for the virtual interface
ID string `mapstructure:"id" json:"id"`
MACAddress string `mapstructure:"mac_address" json:"mac_address"`
IPAddresses []IPAddress `mapstructure:"ip_addresses" json:"ip_addresses"`
}
// VirtualInterfacePage is the page returned by a pager when traversing over a
// collection of virtual interfaces.
type VirtualInterfacePage struct {
pagination.SinglePageBase
}
// IsEmpty returns true if the NetworkPage contains no Networks.
func (r VirtualInterfacePage) IsEmpty() (bool, error) {
networks, err := ExtractVirtualInterfaces(r)
if err != nil {
return true, err
}
return len(networks) == 0, nil
}
// ExtractVirtualInterfaces accepts a Page struct, specifically a VirtualInterfacePage struct,
// and extracts the elements into a slice of VirtualInterface structs. In other words,
// a generic collection is mapped into a relevant slice.
func ExtractVirtualInterfaces(page pagination.Page) ([]VirtualInterface, error) {
var resp struct {
VirtualInterfaces []VirtualInterface `mapstructure:"virtual_interfaces" json:"virtual_interfaces"`
}
err := mapstructure.Decode(page.(VirtualInterfacePage).Body, &resp)
return resp.VirtualInterfaces, err
}

View File

@@ -0,0 +1,15 @@
package virtualinterfaces
import "github.com/rackspace/gophercloud"
func listURL(c *gophercloud.ServiceClient, instanceID string) string {
return c.ServiceURL("servers", instanceID, "os-virtual-interfacesv2")
}
func createURL(c *gophercloud.ServiceClient, instanceID string) string {
return c.ServiceURL("servers", instanceID, "os-virtual-interfacesv2")
}
func deleteURL(c *gophercloud.ServiceClient, instanceID, interfaceID string) string {
return c.ServiceURL("servers", instanceID, "os-virtual-interfacesv2", interfaceID)
}

View File

@@ -0,0 +1,27 @@
package volumeattach
import (
"github.com/rackspace/gophercloud"
os "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/volumeattach"
"github.com/rackspace/gophercloud/pagination"
)
// List returns a Pager that allows you to iterate over a collection of VolumeAttachments.
func List(client *gophercloud.ServiceClient, serverID string) pagination.Pager {
return os.List(client, serverID)
}
// Create requests the creation of a new volume attachment on the server
func Create(client *gophercloud.ServiceClient, serverID string, opts os.CreateOptsBuilder) os.CreateResult {
return os.Create(client, serverID, opts)
}
// Get returns public data about a previously created VolumeAttachment.
func Get(client *gophercloud.ServiceClient, serverID, aID string) os.GetResult {
return os.Get(client, serverID, aID)
}
// Delete requests the deletion of a previous stored VolumeAttachment from the server.
func Delete(client *gophercloud.ServiceClient, serverID, aID string) os.DeleteResult {
return os.Delete(client, serverID, aID)
}

View File

@@ -0,0 +1,3 @@
// Package volumeattach provides the ability to attach and detach volume
// to instances to Rackspace servers
package volumeattach

View File

@@ -0,0 +1,6 @@
// Package backups provides information and interaction with the backup API
// resource in the Rackspace Database service.
//
// A backup is a copy of a database instance that can be used to restore it to
// some defined point in history.
package backups

View File

@@ -0,0 +1,66 @@
package backups
import "time"
var (
timestamp = "2015-11-12T14:22:42Z"
timeVal, _ = time.Parse(time.RFC3339, timestamp)
)
var getResp = `
{
"backup": {
"created": "` + timestamp + `",
"description": "My Backup",
"id": "61f12fef-edb1-4561-8122-e7c00ef26a82",
"instance_id": "d4603f69-ec7e-4e9b-803f-600b9205576f",
"locationRef": null,
"name": "snapshot",
"parent_id": null,
"size": 100,
"status": "NEW",
"datastore": {
"version": "5.1",
"type": "MySQL",
"version_id": "20000000-0000-0000-0000-000000000002"
},
"updated": "` + timestamp + `"
}
}
`
var createReq = `
{
"backup": {
"description": "My Backup",
"instance": "d4603f69-ec7e-4e9b-803f-600b9205576f",
"name": "snapshot"
}
}
`
var createResp = getResp
var listResp = `
{
"backups": [
{
"status": "COMPLETED",
"updated": "` + timestamp + `",
"description": "Backup from Restored Instance",
"datastore": {
"version": "5.1",
"type": "MySQL",
"version_id": "20000000-0000-0000-0000-000000000002"
},
"id": "87972694-4be2-40f5-83f8-501656e0032a",
"size": 0.141026,
"name": "restored_backup",
"created": "` + timestamp + `",
"instance_id": "29af2cd9-0674-48ab-b87a-b160f00208e6",
"parent_id": null,
"locationRef": "http://localhost/path/to/backup"
}
]
}
`

View File

@@ -0,0 +1,138 @@
package backups
import (
"errors"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
)
// CreateOptsBuilder is the top-level interface for creating JSON maps.
type CreateOptsBuilder interface {
ToBackupCreateMap() (map[string]interface{}, error)
}
// CreateOpts is responsible for configuring newly provisioned backups.
type CreateOpts struct {
// [REQUIRED] The name of the backup. The only restriction is the name must
// be less than 64 characters long.
Name string
// [REQUIRED] The ID of the instance being backed up.
InstanceID string
// [OPTIONAL] A human-readable explanation of the backup.
Description string
}
// ToBackupCreateMap will create a JSON map for the Create operation.
func (opts CreateOpts) ToBackupCreateMap() (map[string]interface{}, error) {
if opts.Name == "" {
return nil, errors.New("Name is a required field")
}
if opts.InstanceID == "" {
return nil, errors.New("InstanceID is a required field")
}
backup := map[string]interface{}{
"name": opts.Name,
"instance": opts.InstanceID,
}
if opts.Description != "" {
backup["description"] = opts.Description
}
return map[string]interface{}{"backup": backup}, nil
}
// Create asynchronously creates a new backup for a specified database instance.
// During the backup process, write access on MyISAM databases will be
// temporarily disabled; innoDB databases will be unaffected. During this time,
// you will not be able to add or delete databases or users; nor delete, stop
// or reboot the instance itself. Only one backup is permitted at once.
//
// Backups are not deleted when database instances are deleted; you must
// manually delete any backups created using Delete(). Backups are saved to your
// Cloud Files account in a new container called z_CLOUDDB_BACKUPS. It is
// strongly recommended you do not alter this container or its contents; usual
// storage costs apply.
func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult {
var res CreateResult
reqBody, err := opts.ToBackupCreateMap()
if err != nil {
res.Err = err
return res
}
_, res.Err = client.Request("POST", baseURL(client), gophercloud.RequestOpts{
JSONBody: &reqBody,
JSONResponse: &res.Body,
OkCodes: []int{202},
})
return res
}
// ListOptsBuilder is the top-level interface for creating query strings.
type ListOptsBuilder interface {
ToBackupListQuery() (string, error)
}
// ListOpts allows you to refine a list search by certain parameters.
type ListOpts struct {
// The type of datastore by which to filter.
Datastore string `q:"datastore"`
}
// ToBackupListQuery converts a ListOpts struct into a query string.
func (opts ListOpts) ToBackupListQuery() (string, error) {
q, err := gophercloud.BuildQueryString(opts)
if err != nil {
return "", err
}
return q.String(), nil
}
// List will list all the saved backups for all database instances.
func List(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
url := baseURL(client)
if opts != nil {
query, err := opts.ToBackupListQuery()
if err != nil {
return pagination.Pager{Err: err}
}
url += query
}
pageFn := func(r pagination.PageResult) pagination.Page {
return BackupPage{pagination.SinglePageBase(r)}
}
return pagination.NewPager(client, url, pageFn)
}
// Get will retrieve details for a particular backup based on its unique ID.
func Get(client *gophercloud.ServiceClient, id string) GetResult {
var res GetResult
_, res.Err = client.Request("GET", resourceURL(client, id), gophercloud.RequestOpts{
JSONResponse: &res.Body,
OkCodes: []int{200},
})
return res
}
// Delete will permanently delete a backup.
func Delete(client *gophercloud.ServiceClient, id string) DeleteResult {
var res DeleteResult
_, res.Err = client.Request("DELETE", resourceURL(client, id), gophercloud.RequestOpts{
OkCodes: []int{202},
})
return res
}

View File

@@ -0,0 +1,149 @@
package backups
import (
"fmt"
"reflect"
"time"
"github.com/mitchellh/mapstructure"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/openstack/db/v1/datastores"
"github.com/rackspace/gophercloud/pagination"
)
// Status represents the various states a Backup can be in.
type Status string
// Enum types for the status.
const (
StatusNew Status = "NEW"
StatusBuilding Status = "BUILDING"
StatusCompleted Status = "COMPLETED"
StatusFailed Status = "FAILED"
StatusDeleteFailed Status = "DELETE_FAILED"
)
// Backup represents a Backup API resource.
type Backup struct {
Description string
ID string
InstanceID string `json:"instance_id" mapstructure:"instance_id"`
LocationRef string
Name string
ParentID string `json:"parent_id" mapstructure:"parent_id"`
Size float64
Status Status
Created time.Time `mapstructure:"-"`
Updated time.Time `mapstructure:"-"`
Datastore datastores.DatastorePartial
}
// CreateResult represents the result of a create operation.
type CreateResult struct {
commonResult
}
// GetResult represents the result of a get operation.
type GetResult struct {
commonResult
}
// DeleteResult represents the result of a delete operation.
type DeleteResult struct {
gophercloud.ErrResult
}
type commonResult struct {
gophercloud.Result
}
// Extract will retrieve a Backup struct from an operation's result.
func (r commonResult) Extract() (*Backup, error) {
if r.Err != nil {
return nil, r.Err
}
var response struct {
Backup Backup `mapstructure:"backup"`
}
err := mapstructure.Decode(r.Body, &response)
val := r.Body.(map[string]interface{})["backup"].(map[string]interface{})
if t, ok := val["created"].(string); ok && t != "" {
creationTime, err := time.Parse(time.RFC3339, t)
if err != nil {
return &response.Backup, err
}
response.Backup.Created = creationTime
}
if t, ok := val["updated"].(string); ok && t != "" {
updatedTime, err := time.Parse(time.RFC3339, t)
if err != nil {
return &response.Backup, err
}
response.Backup.Updated = updatedTime
}
return &response.Backup, err
}
// BackupPage represents a page of backups.
type BackupPage struct {
pagination.SinglePageBase
}
// IsEmpty checks whether an BackupPage struct is empty.
func (r BackupPage) IsEmpty() (bool, error) {
is, err := ExtractBackups(r)
if err != nil {
return true, err
}
return len(is) == 0, nil
}
// ExtractBackups will retrieve a slice of Backup structs from a paginated collection.
func ExtractBackups(page pagination.Page) ([]Backup, error) {
casted := page.(BackupPage).Body
var resp struct {
Backups []Backup `mapstructure:"backups" json:"backups"`
}
if err := mapstructure.Decode(casted, &resp); err != nil {
return nil, err
}
var vals []interface{}
switch casted.(type) {
case map[string]interface{}:
vals = casted.(map[string]interface{})["backups"].([]interface{})
case map[string][]interface{}:
vals = casted.(map[string][]interface{})["backups"]
default:
return resp.Backups, fmt.Errorf("Unknown type: %v", reflect.TypeOf(casted))
}
for i, v := range vals {
val := v.(map[string]interface{})
if t, ok := val["created"].(string); ok && t != "" {
creationTime, err := time.Parse(time.RFC3339, t)
if err != nil {
return resp.Backups, err
}
resp.Backups[i].Created = creationTime
}
if t, ok := val["updated"].(string); ok && t != "" {
updatedTime, err := time.Parse(time.RFC3339, t)
if err != nil {
return resp.Backups, err
}
resp.Backups[i].Updated = updatedTime
}
}
return resp.Backups, nil
}

View File

@@ -0,0 +1,11 @@
package backups
import "github.com/rackspace/gophercloud"
func baseURL(c *gophercloud.ServiceClient) string {
return c.ServiceURL("backups")
}
func resourceURL(c *gophercloud.ServiceClient, backupID string) string {
return c.ServiceURL("backups", backupID)
}

View File

@@ -0,0 +1,79 @@
package configurations
import (
"github.com/rackspace/gophercloud"
os "github.com/rackspace/gophercloud/openstack/db/v1/configurations"
"github.com/rackspace/gophercloud/pagination"
)
// List will list all of the available configurations.
func List(client *gophercloud.ServiceClient) pagination.Pager {
return os.List(client)
}
// Create will create a new configuration group.
func Create(client *gophercloud.ServiceClient, opts os.CreateOptsBuilder) os.CreateResult {
return os.Create(client, opts)
}
// Get will retrieve the details for a specified configuration group.
func Get(client *gophercloud.ServiceClient, configID string) os.GetResult {
return os.Get(client, configID)
}
// Update will modify an existing configuration group by performing a merge
// between new and existing values. If the key already exists, the new value
// will overwrite. All other keys will remain unaffected.
func Update(client *gophercloud.ServiceClient, configID string, opts os.UpdateOptsBuilder) os.UpdateResult {
return os.Update(client, configID, opts)
}
// Replace will modify an existing configuration group by overwriting the
// entire parameter group with the new values provided. Any existing keys not
// included in UpdateOptsBuilder will be deleted.
func Replace(client *gophercloud.ServiceClient, configID string, opts os.UpdateOptsBuilder) os.ReplaceResult {
return os.Replace(client, configID, opts)
}
// Delete will permanently delete a configuration group. Please note that
// config groups cannot be deleted whilst still attached to running instances -
// you must detach and then delete them.
func Delete(client *gophercloud.ServiceClient, configID string) os.DeleteResult {
return os.Delete(client, configID)
}
// ListInstances will list all the instances associated with a particular
// configuration group.
func ListInstances(client *gophercloud.ServiceClient, configID string) pagination.Pager {
return os.ListInstances(client, configID)
}
// ListDatastoreParams will list all the available and supported parameters
// that can be used for a particular datastore ID and a particular version.
// For example, if you are wondering how you can configure a MySQL 5.6 instance,
// you can use this operation (you will need to retrieve the MySQL datastore ID
// by using the datastores API).
func ListDatastoreParams(client *gophercloud.ServiceClient, datastoreID, versionID string) pagination.Pager {
return os.ListDatastoreParams(client, datastoreID, versionID)
}
// GetDatastoreParam will retrieve information about a specific configuration
// parameter. For example, you can use this operation to understand more about
// "innodb_file_per_table" configuration param for MySQL datastores. You will
// need the param's ID first, which can be attained by using the ListDatastoreParams
// operation.
func GetDatastoreParam(client *gophercloud.ServiceClient, datastoreID, versionID, paramID string) os.ParamResult {
return os.GetDatastoreParam(client, datastoreID, versionID, paramID)
}
// ListGlobalParams is similar to ListDatastoreParams but does not require a
// DatastoreID.
func ListGlobalParams(client *gophercloud.ServiceClient, versionID string) pagination.Pager {
return os.ListGlobalParams(client, versionID)
}
// GetGlobalParam is similar to GetDatastoreParam but does not require a
// DatastoreID.
func GetGlobalParam(client *gophercloud.ServiceClient, versionID, paramID string) os.ParamResult {
return os.GetGlobalParam(client, versionID, paramID)
}

View File

@@ -0,0 +1 @@
package configurations

View File

@@ -0,0 +1,159 @@
package configurations
import (
"fmt"
"time"
os "github.com/rackspace/gophercloud/openstack/db/v1/configurations"
)
var (
timestamp = "2015-11-12T14:22:42Z"
timeVal, _ = time.Parse(time.RFC3339, timestamp)
)
var singleConfigJSON = `
{
"created": "` + timestamp + `",
"datastore_name": "mysql",
"datastore_version_id": "b00000b0-00b0-0b00-00b0-000b000000bb",
"datastore_version_name": "5.6",
"description": "example_description",
"id": "005a8bb7-a8df-40ee-b0b7-fc144641abc2",
"name": "example-configuration-name",
"updated": "` + timestamp + `"
}
`
var singleConfigWithValuesJSON = `
{
"created": "` + timestamp + `",
"datastore_name": "mysql",
"datastore_version_id": "b00000b0-00b0-0b00-00b0-000b000000bb",
"datastore_version_name": "5.6",
"description": "example description",
"id": "005a8bb7-a8df-40ee-b0b7-fc144641abc2",
"instance_count": 0,
"name": "example-configuration-name",
"updated": "` + timestamp + `",
"values": {
"collation_server": "latin1_swedish_ci",
"connect_timeout": 120
}
}
`
var (
listConfigsJSON = fmt.Sprintf(`{"configurations": [%s]}`, singleConfigJSON)
getConfigJSON = fmt.Sprintf(`{"configuration": %s}`, singleConfigJSON)
createConfigJSON = fmt.Sprintf(`{"configuration": %s}`, singleConfigWithValuesJSON)
)
var createReq = `
{
"configuration": {
"datastore": {
"type": "a00000a0-00a0-0a00-00a0-000a000000aa",
"version": "b00000b0-00b0-0b00-00b0-000b000000bb"
},
"description": "example description",
"name": "example-configuration-name",
"values": {
"collation_server": "latin1_swedish_ci",
"connect_timeout": 120
}
}
}
`
var updateReq = `
{
"configuration": {
"values": {
"connect_timeout": 300
}
}
}
`
var listInstancesJSON = `
{
"instances": [
{
"id": "d4603f69-ec7e-4e9b-803f-600b9205576f",
"name": "json_rack_instance"
}
]
}
`
var listParamsJSON = `
{
"configuration-parameters": [
{
"max": 1,
"min": 0,
"name": "innodb_file_per_table",
"restart_required": true,
"type": "integer"
},
{
"max": 4294967296,
"min": 0,
"name": "key_buffer_size",
"restart_required": false,
"type": "integer"
},
{
"max": 65535,
"min": 2,
"name": "connect_timeout",
"restart_required": false,
"type": "integer"
},
{
"max": 4294967296,
"min": 0,
"name": "join_buffer_size",
"restart_required": false,
"type": "integer"
}
]
}
`
var getParamJSON = `
{
"max": 1,
"min": 0,
"name": "innodb_file_per_table",
"restart_required": true,
"type": "integer"
}
`
var exampleConfig = os.Config{
Created: timeVal,
DatastoreName: "mysql",
DatastoreVersionID: "b00000b0-00b0-0b00-00b0-000b000000bb",
DatastoreVersionName: "5.6",
Description: "example_description",
ID: "005a8bb7-a8df-40ee-b0b7-fc144641abc2",
Name: "example-configuration-name",
Updated: timeVal,
}
var exampleConfigWithValues = os.Config{
Created: timeVal,
DatastoreName: "mysql",
DatastoreVersionID: "b00000b0-00b0-0b00-00b0-000b000000bb",
DatastoreVersionName: "5.6",
Description: "example description",
ID: "005a8bb7-a8df-40ee-b0b7-fc144641abc2",
Name: "example-configuration-name",
Updated: timeVal,
Values: map[string]interface{}{
"collation_server": "latin1_swedish_ci",
"connect_timeout": 120,
},
}

View File

@@ -0,0 +1,19 @@
package databases
import (
"github.com/rackspace/gophercloud"
os "github.com/rackspace/gophercloud/openstack/db/v1/databases"
"github.com/rackspace/gophercloud/pagination"
)
func Create(client *gophercloud.ServiceClient, instanceID string, opts os.CreateOptsBuilder) os.CreateResult {
return os.Create(client, instanceID, opts)
}
func List(client *gophercloud.ServiceClient, instanceID string) pagination.Pager {
return os.List(client, instanceID)
}
func Delete(client *gophercloud.ServiceClient, instanceID, dbName string) os.DeleteResult {
return os.Delete(client, instanceID, dbName)
}

View File

@@ -0,0 +1,3 @@
// Package databases provides information and interaction with the database API
// resource in the Rackspace Database service.
package databases

View File

@@ -0,0 +1 @@
package databases

View File

@@ -0,0 +1,28 @@
package datastores
import (
"github.com/rackspace/gophercloud"
os "github.com/rackspace/gophercloud/openstack/db/v1/datastores"
"github.com/rackspace/gophercloud/pagination"
)
// List will list all available flavors.
func List(client *gophercloud.ServiceClient) pagination.Pager {
return os.List(client)
}
// Get retrieves the details for a particular flavor.
func Get(client *gophercloud.ServiceClient, flavorID string) os.GetResult {
return os.Get(client, flavorID)
}
// ListVersions will list all of the available versions for a specified
// datastore type.
func ListVersions(client *gophercloud.ServiceClient, datastoreID string) pagination.Pager {
return os.ListVersions(client, datastoreID)
}
// GetVersion will retrieve the details of a specified datastore version.
func GetVersion(client *gophercloud.ServiceClient, datastoreID, versionID string) os.GetVersionResult {
return os.GetVersion(client, datastoreID, versionID)
}

View File

@@ -0,0 +1 @@
package datastores

View File

@@ -0,0 +1,17 @@
package flavors
import (
"github.com/rackspace/gophercloud"
os "github.com/rackspace/gophercloud/openstack/db/v1/flavors"
"github.com/rackspace/gophercloud/pagination"
)
// List will list all available flavors.
func List(client *gophercloud.ServiceClient) pagination.Pager {
return os.List(client)
}
// Get retrieves the details for a particular flavor.
func Get(client *gophercloud.ServiceClient, flavorID string) os.GetResult {
return os.Get(client, flavorID)
}

View File

@@ -0,0 +1,3 @@
// Package flavors provides information and interaction with the flavor API
// resource in the Rackspace Database service.
package flavors

View File

@@ -0,0 +1,49 @@
package instances
import (
"github.com/rackspace/gophercloud"
os "github.com/rackspace/gophercloud/openstack/db/v1/instances"
)
// Get retrieves the status and information for a specified database instance.
func Get(client *gophercloud.ServiceClient, id string) GetResult {
return GetResult{os.Get(client, id)}
}
// Delete permanently destroys the database instance.
func Delete(client *gophercloud.ServiceClient, id string) os.DeleteResult {
return os.Delete(client, id)
}
// EnableRootUser enables the login from any host for the root user and
// provides the user with a generated root password.
func EnableRootUser(client *gophercloud.ServiceClient, id string) os.UserRootResult {
return os.EnableRootUser(client, id)
}
// IsRootEnabled checks an instance to see if root access is enabled. It returns
// True if root user is enabled for the specified database instance or False
// otherwise.
func IsRootEnabled(client *gophercloud.ServiceClient, id string) (bool, error) {
return os.IsRootEnabled(client, id)
}
// Restart will restart only the MySQL Instance. Restarting MySQL will
// erase any dynamic configuration settings that you have made within MySQL.
// The MySQL service will be unavailable until the instance restarts.
func Restart(client *gophercloud.ServiceClient, id string) os.ActionResult {
return os.Restart(client, id)
}
// Resize changes the memory size of the instance, assuming a valid
// flavorRef is provided. It will also restart the MySQL service.
func Resize(client *gophercloud.ServiceClient, id, flavorRef string) os.ActionResult {
return os.Resize(client, id, flavorRef)
}
// ResizeVolume will resize the attached volume for an instance. It supports
// only increasing the volume size and does not support decreasing the size.
// The volume size is in gigabytes (GB) and must be an integer.
func ResizeVolume(client *gophercloud.ServiceClient, id string, size int) os.ActionResult {
return os.ResizeVolume(client, id, size)
}

View File

@@ -0,0 +1,3 @@
// Package instances provides information and interaction with the instance API
// resource in the Rackspace Database service.
package instances

View File

@@ -0,0 +1,340 @@
package instances
import (
"fmt"
"time"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/openstack/db/v1/datastores"
"github.com/rackspace/gophercloud/openstack/db/v1/flavors"
os "github.com/rackspace/gophercloud/openstack/db/v1/instances"
)
var (
timestamp = "2015-11-12T14:22:42Z"
timeVal, _ = time.Parse(time.RFC3339, timestamp)
)
var instance = `
{
"created": "` + timestamp + `",
"datastore": {
"type": "mysql",
"version": "5.6"
},
"flavor": {
"id": "1",
"links": [
{
"href": "https://ord.databases.api.rackspacecloud.com/v1.0/1234/flavors/1",
"rel": "self"
},
{
"href": "https://ord.databases.api.rackspacecloud.com/v1.0/1234/flavors/1",
"rel": "bookmark"
}
]
},
"links": [
{
"href": "https://ord.databases.api.rackspacecloud.com/v1.0/1234/flavors/1",
"rel": "self"
}
],
"hostname": "e09ad9a3f73309469cf1f43d11e79549caf9acf2.rackspaceclouddb.com",
"id": "{instanceID}",
"name": "json_rack_instance",
"status": "BUILD",
"updated": "` + timestamp + `",
"volume": {
"size": 2
}
}
`
var createReq = `
{
"instance": {
"databases": [
{
"character_set": "utf8",
"collate": "utf8_general_ci",
"name": "sampledb"
},
{
"name": "nextround"
}
],
"flavorRef": "1",
"name": "json_rack_instance",
"users": [
{
"databases": [
{
"name": "sampledb"
}
],
"name": "demouser",
"password": "demopassword"
}
],
"volume": {
"size": 2
},
"restorePoint": {
"backupRef": "1234567890"
}
}
}
`
var createReplicaReq = `
{
"instance": {
"volume": {
"size": 1
},
"flavorRef": "9",
"name": "t2s1_ALT_GUEST",
"replica_of": "6bdca2fc-418e-40bd-a595-62abda61862d"
}
}
`
var createReplicaResp = `
{
"instance": {
"status": "BUILD",
"updated": "` + timestamp + `",
"name": "t2s1_ALT_GUEST",
"links": [
{
"href": "https://ord.databases.api.rackspacecloud.com/v1.0/5919009/instances/8367c312-7c40-4a66-aab1-5767478914fc",
"rel": "self"
},
{
"href": "https://ord.databases.api.rackspacecloud.com/instances/8367c312-7c40-4a66-aab1-5767478914fc",
"rel": "bookmark"
}
],
"created": "` + timestamp + `",
"id": "8367c312-7c40-4a66-aab1-5767478914fc",
"volume": {
"size": 1
},
"flavor": {
"id": "9"
},
"datastore": {
"version": "5.6",
"type": "mysql"
},
"replica_of": {
"id": "6bdca2fc-418e-40bd-a595-62abda61862d"
}
}
}
`
var listReplicasResp = `
{
"instances": [
{
"status": "ACTIVE",
"name": "t1s1_ALT_GUEST",
"links": [
{
"href": "https://ord.databases.api.rackspacecloud.com/v1.0/1234/instances/3c691f06-bf9a-4618-b7ec-2817ce0cf254",
"rel": "self"
},
{
"href": "https://ord.databases.api.rackspacecloud.com/instances/3c691f06-bf9a-4618-b7ec-2817ce0cf254",
"rel": "bookmark"
}
],
"ip": [
"10.0.0.3"
],
"id": "3c691f06-bf9a-4618-b7ec-2817ce0cf254",
"volume": {
"size": 1
},
"flavor": {
"id": "9"
},
"datastore": {
"version": "5.6",
"type": "mysql"
},
"replica_of": {
"id": "8b499b45-52d6-402d-b398-f9d8f279c69a"
}
}
]
}
`
var getReplicaResp = `
{
"instance": {
"status": "ACTIVE",
"updated": "` + timestamp + `",
"name": "t1_ALT_GUEST",
"created": "` + timestamp + `",
"ip": [
"10.0.0.2"
],
"replicas": [
{
"id": "3c691f06-bf9a-4618-b7ec-2817ce0cf254"
}
],
"id": "8b499b45-52d6-402d-b398-f9d8f279c69a",
"volume": {
"used": 0.54,
"size": 1
},
"flavor": {
"id": "9"
},
"datastore": {
"version": "5.6",
"type": "mysql"
}
}
}
`
var detachReq = `
{
"instance": {
"replica_of": "",
"slave_of": ""
}
}
`
var getConfigResp = `
{
"instance": {
"configuration": {
"basedir": "/usr",
"connect_timeout": "15",
"datadir": "/var/lib/mysql",
"default_storage_engine": "innodb",
"innodb_buffer_pool_instances": "1",
"innodb_buffer_pool_size": "175M",
"innodb_checksum_algorithm": "crc32",
"innodb_data_file_path": "ibdata1:10M:autoextend",
"innodb_file_per_table": "1",
"innodb_io_capacity": "200",
"innodb_log_file_size": "256M",
"innodb_log_files_in_group": "2",
"innodb_open_files": "8192",
"innodb_thread_concurrency": "0",
"join_buffer_size": "1M",
"key_buffer_size": "50M",
"local-infile": "0",
"log-error": "/var/log/mysql/mysqld.log",
"max_allowed_packet": "16M",
"max_connect_errors": "10000",
"max_connections": "40",
"max_heap_table_size": "16M",
"myisam-recover": "BACKUP",
"open_files_limit": "8192",
"performance_schema": "off",
"pid_file": "/var/run/mysqld/mysqld.pid",
"port": "3306",
"query_cache_limit": "1M",
"query_cache_size": "8M",
"query_cache_type": "1",
"read_buffer_size": "256K",
"read_rnd_buffer_size": "1M",
"server_id": "1",
"skip-external-locking": "1",
"skip_name_resolve": "1",
"sort_buffer_size": "256K",
"table_open_cache": "4096",
"thread_stack": "192K",
"tmp_table_size": "16M",
"tmpdir": "/var/tmp",
"user": "mysql",
"wait_timeout": "3600"
}
}
}
`
var associateReq = `{"instance": {"configuration": "{configGroupID}"}}`
var listBackupsResp = `
{
"backups": [
{
"status": "COMPLETED",
"updated": "` + timestamp + `",
"description": "Backup from Restored Instance",
"datastore": {
"version": "5.1",
"type": "MySQL",
"version_id": "20000000-0000-0000-0000-000000000002"
},
"id": "87972694-4be2-40f5-83f8-501656e0032a",
"size": 0.141026,
"name": "restored_backup",
"created": "` + timestamp + `",
"instance_id": "29af2cd9-0674-48ab-b87a-b160f00208e6",
"parent_id": null,
"locationRef": "http://localhost/path/to/backup"
}
]
}
`
var (
createResp = fmt.Sprintf(`{"instance":%s}`, instance)
getResp = fmt.Sprintf(`{"instance":%s}`, instance)
associateResp = fmt.Sprintf(`{"instance":%s}`, instance)
listInstancesResp = fmt.Sprintf(`{"instances":[%s]}`, instance)
)
var instanceID = "{instanceID}"
var expectedInstance = &Instance{
Created: timeVal,
Updated: timeVal,
Datastore: datastores.DatastorePartial{Type: "mysql", Version: "5.6"},
Flavor: flavors.Flavor{
ID: "1",
Links: []gophercloud.Link{
gophercloud.Link{Href: "https://ord.databases.api.rackspacecloud.com/v1.0/1234/flavors/1", Rel: "self"},
gophercloud.Link{Href: "https://ord.databases.api.rackspacecloud.com/v1.0/1234/flavors/1", Rel: "bookmark"},
},
},
Hostname: "e09ad9a3f73309469cf1f43d11e79549caf9acf2.rackspaceclouddb.com",
ID: instanceID,
Links: []gophercloud.Link{
gophercloud.Link{Href: "https://ord.databases.api.rackspacecloud.com/v1.0/1234/flavors/1", Rel: "self"},
},
Name: "json_rack_instance",
Status: "BUILD",
Volume: os.Volume{Size: 2},
}
var expectedReplica = &Instance{
Status: "BUILD",
Updated: timeVal,
Name: "t2s1_ALT_GUEST",
Links: []gophercloud.Link{
gophercloud.Link{Rel: "self", Href: "https://ord.databases.api.rackspacecloud.com/v1.0/5919009/instances/8367c312-7c40-4a66-aab1-5767478914fc"},
gophercloud.Link{Rel: "bookmark", Href: "https://ord.databases.api.rackspacecloud.com/instances/8367c312-7c40-4a66-aab1-5767478914fc"},
},
Created: timeVal,
ID: "8367c312-7c40-4a66-aab1-5767478914fc",
Volume: os.Volume{Size: 1},
Flavor: flavors.Flavor{ID: "9"},
Datastore: datastores.DatastorePartial{Version: "5.6", Type: "mysql"},
ReplicaOf: &Instance{
ID: "6bdca2fc-418e-40bd-a595-62abda61862d",
},
}

View File

@@ -0,0 +1,199 @@
package instances
import (
"github.com/rackspace/gophercloud"
osDBs "github.com/rackspace/gophercloud/openstack/db/v1/databases"
os "github.com/rackspace/gophercloud/openstack/db/v1/instances"
osUsers "github.com/rackspace/gophercloud/openstack/db/v1/users"
"github.com/rackspace/gophercloud/pagination"
"github.com/rackspace/gophercloud/rackspace/db/v1/backups"
)
// CreateOpts is the struct responsible for configuring a new database instance.
type CreateOpts struct {
// Either the integer UUID (in string form) of the flavor, or its URI
// reference as specified in the response from the List() call. Required.
FlavorRef string
// Specifies the volume size in gigabytes (GB). The value must be between 1
// and 300. Required.
Size int
// Name of the instance to create. The length of the name is limited to
// 255 characters and any characters are permitted. Optional.
Name string
// A slice of database information options.
Databases osDBs.CreateOptsBuilder
// A slice of user information options.
Users osUsers.CreateOptsBuilder
// ID of the configuration group to associate with the instance. Optional.
ConfigID string
// Options to configure the type of datastore the instance will use. This is
// optional, and if excluded will default to MySQL.
Datastore *os.DatastoreOpts
// Specifies the backup ID from which to restore the database instance. There
// are some things to be aware of before using this field. When you execute
// the Restore Backup operation, a new database instance is created to store
// the backup whose ID is specified by the restorePoint attribute. This will
// mean that:
// - All users, passwords and access that were on the instance at the time of
// the backup will be restored along with the databases.
// - You can create new users or databases if you want, but they cannot be
// the same as the ones from the instance that was backed up.
RestorePoint string
ReplicaOf string
}
func (opts CreateOpts) ToInstanceCreateMap() (map[string]interface{}, error) {
instance, err := os.CreateOpts{
FlavorRef: opts.FlavorRef,
Size: opts.Size,
Name: opts.Name,
Databases: opts.Databases,
Users: opts.Users,
}.ToInstanceCreateMap()
if err != nil {
return nil, err
}
instance = instance["instance"].(map[string]interface{})
if opts.ConfigID != "" {
instance["configuration"] = opts.ConfigID
}
if opts.Datastore != nil {
ds, err := opts.Datastore.ToMap()
if err != nil {
return nil, err
}
instance["datastore"] = ds
}
if opts.RestorePoint != "" {
instance["restorePoint"] = map[string]string{"backupRef": opts.RestorePoint}
}
if opts.ReplicaOf != "" {
instance["replica_of"] = opts.ReplicaOf
}
return map[string]interface{}{"instance": instance}, nil
}
// Create asynchronously provisions a new database instance. It requires the
// user to specify a flavor and a volume size. The API service then provisions
// the instance with the requested flavor and sets up a volume of the specified
// size, which is the storage for the database instance.
//
// Although this call only allows the creation of 1 instance per request, you
// can create an instance with multiple databases and users. The default
// binding for a MySQL instance is port 3306.
func Create(client *gophercloud.ServiceClient, opts os.CreateOptsBuilder) CreateResult {
return CreateResult{os.Create(client, opts)}
}
// ListOpts specifies all of the query options to be used when returning a list
// of database instances.
type ListOpts struct {
// IncludeHA includes or excludes High Availability instances from the result set
IncludeHA bool `q:"include_ha"`
// IncludeReplicas includes or excludes Replica instances from the result set
IncludeReplicas bool `q:"include_replicas"`
}
// ToInstanceListQuery formats a ListOpts into a query string.
func (opts ListOpts) ToInstanceListQuery() (string, error) {
q, err := gophercloud.BuildQueryString(opts)
if err != nil {
return "", err
}
return q.String(), nil
}
// List retrieves the status and information for all database instances.
func List(client *gophercloud.ServiceClient, opts *ListOpts) pagination.Pager {
url := baseURL(client)
if opts != nil {
query, err := opts.ToInstanceListQuery()
if err != nil {
return pagination.Pager{Err: err}
}
url += query
}
createPageFn := func(r pagination.PageResult) pagination.Page {
return os.InstancePage{pagination.LinkedPageBase{PageResult: r}}
}
return pagination.NewPager(client, url, createPageFn)
}
// GetDefaultConfig lists the default configuration settings from the template
// that was applied to the specified instance. In a sense, this is the vanilla
// configuration setting applied to an instance. Further configuration can be
// applied by associating an instance with a configuration group.
func GetDefaultConfig(client *gophercloud.ServiceClient, id string) ConfigResult {
var res ConfigResult
_, res.Err = client.Request("GET", configURL(client, id), gophercloud.RequestOpts{
JSONResponse: &res.Body,
OkCodes: []int{200},
})
return res
}
// AssociateWithConfigGroup associates a specified instance to a specified
// configuration group. If any of the parameters within a configuration group
// require a restart, then the instance will transition into a restart.
func AssociateWithConfigGroup(client *gophercloud.ServiceClient, instanceID, configGroupID string) UpdateResult {
reqBody := map[string]string{
"configuration": configGroupID,
}
var res UpdateResult
_, res.Err = client.Request("PUT", resourceURL(client, instanceID), gophercloud.RequestOpts{
JSONBody: map[string]map[string]string{"instance": reqBody},
OkCodes: []int{202},
})
return res
}
// DetachFromConfigGroup will detach an instance from all config groups.
func DetachFromConfigGroup(client *gophercloud.ServiceClient, instanceID string) UpdateResult {
return AssociateWithConfigGroup(client, instanceID, "")
}
// ListBackups will list all the backups for a specified database instance.
func ListBackups(client *gophercloud.ServiceClient, instanceID string) pagination.Pager {
pageFn := func(r pagination.PageResult) pagination.Page {
return backups.BackupPage{pagination.SinglePageBase(r)}
}
return pagination.NewPager(client, backupsURL(client, instanceID), pageFn)
}
// DetachReplica will detach a specified replica instance from its source
// instance, effectively allowing it to operate independently. Detaching a
// replica will restart the MySQL service on the instance.
func DetachReplica(client *gophercloud.ServiceClient, replicaID string) DetachResult {
var res DetachResult
_, res.Err = client.Request("PATCH", resourceURL(client, replicaID), gophercloud.RequestOpts{
JSONBody: map[string]interface{}{"instance": map[string]string{"replica_of": "", "slave_of": ""}},
OkCodes: []int{202},
})
return res
}

View File

@@ -0,0 +1,191 @@
package instances
import (
"fmt"
"reflect"
"time"
"github.com/mitchellh/mapstructure"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/openstack/db/v1/datastores"
"github.com/rackspace/gophercloud/openstack/db/v1/flavors"
os "github.com/rackspace/gophercloud/openstack/db/v1/instances"
"github.com/rackspace/gophercloud/pagination"
)
// Instance represents a remote MySQL instance.
type Instance struct {
// Indicates the datetime that the instance was created
Created time.Time `mapstructure:"-"`
// Indicates the most recent datetime that the instance was updated.
Updated time.Time `mapstructure:"-"`
// Indicates how the instance stores data.
Datastore datastores.DatastorePartial
// Indicates the hardware flavor the instance uses.
Flavor flavors.Flavor
// A DNS-resolvable hostname associated with the database instance (rather
// than an IPv4 address). Since the hostname always resolves to the correct
// IP address of the database instance, this relieves the user from the task
// of maintaining the mapping. Note that although the IP address may likely
// change on resizing, migrating, and so forth, the hostname always resolves
// to the correct database instance.
Hostname string
// Indicates the unique identifier for the instance resource.
ID string
// Exposes various links that reference the instance resource.
Links []gophercloud.Link
// The human-readable name of the instance.
Name string
// The build status of the instance.
Status string
// Information about the attached volume of the instance.
Volume os.Volume
// IP indicates the various IP addresses which allow access.
IP []string
// Indicates whether this instance is a replica of another source instance.
ReplicaOf *Instance `mapstructure:"replica_of" json:"replica_of"`
// Indicates whether this instance is the source of other replica instances.
Replicas []Instance
}
func commonExtract(err error, body interface{}) (*Instance, error) {
if err != nil {
return nil, err
}
var response struct {
Instance Instance `mapstructure:"instance"`
}
err = mapstructure.Decode(body, &response)
val := body.(map[string]interface{})["instance"].(map[string]interface{})
if t, ok := val["created"].(string); ok && t != "" {
creationTime, err := time.Parse(time.RFC3339, t)
if err != nil {
return &response.Instance, err
}
response.Instance.Created = creationTime
}
if t, ok := val["updated"].(string); ok && t != "" {
updatedTime, err := time.Parse(time.RFC3339, t)
if err != nil {
return &response.Instance, err
}
response.Instance.Updated = updatedTime
}
return &response.Instance, err
}
// CreateResult represents the result of a Create operation.
type CreateResult struct {
os.CreateResult
}
// Extract will retrieve an instance from a create result.
func (r CreateResult) Extract() (*Instance, error) {
return commonExtract(r.Err, r.Body)
}
// GetResult represents the result of a Get operation.
type GetResult struct {
os.GetResult
}
// Extract will extract an Instance from a GetResult.
func (r GetResult) Extract() (*Instance, error) {
return commonExtract(r.Err, r.Body)
}
// ConfigResult represents the result of getting default configuration for an
// instance.
type ConfigResult struct {
gophercloud.Result
}
// DetachResult represents the result of detaching a replica from its source.
type DetachResult struct {
gophercloud.ErrResult
}
// Extract will extract the configuration information (in the form of a map)
// about a particular instance.
func (r ConfigResult) Extract() (map[string]string, error) {
if r.Err != nil {
return nil, r.Err
}
var response struct {
Instance struct {
Config map[string]string `mapstructure:"configuration"`
} `mapstructure:"instance"`
}
err := mapstructure.Decode(r.Body, &response)
return response.Instance.Config, err
}
// UpdateResult represents the result of an Update operation.
type UpdateResult struct {
gophercloud.ErrResult
}
// ExtractInstances retrieves a slice of instances from a paginated collection.
func ExtractInstances(page pagination.Page) ([]Instance, error) {
casted := page.(os.InstancePage).Body
var resp struct {
Instances []Instance `mapstructure:"instances"`
}
if err := mapstructure.Decode(casted, &resp); err != nil {
return nil, err
}
var vals []interface{}
switch casted.(type) {
case map[string]interface{}:
vals = casted.(map[string]interface{})["instances"].([]interface{})
case map[string][]interface{}:
vals = casted.(map[string][]interface{})["instances"]
default:
return resp.Instances, fmt.Errorf("Unknown type: %v", reflect.TypeOf(casted))
}
for i, v := range vals {
val := v.(map[string]interface{})
if t, ok := val["created"].(string); ok && t != "" {
creationTime, err := time.Parse(time.RFC3339, t)
if err != nil {
return resp.Instances, err
}
resp.Instances[i].Created = creationTime
}
if t, ok := val["updated"].(string); ok && t != "" {
updatedTime, err := time.Parse(time.RFC3339, t)
if err != nil {
return resp.Instances, err
}
resp.Instances[i].Updated = updatedTime
}
}
return resp.Instances, nil
}

View File

@@ -0,0 +1,23 @@
package instances
import "github.com/rackspace/gophercloud"
func baseURL(c *gophercloud.ServiceClient) string {
return c.ServiceURL("instances")
}
func createURL(c *gophercloud.ServiceClient) string {
return baseURL(c)
}
func resourceURL(c *gophercloud.ServiceClient, id string) string {
return c.ServiceURL("instances", id)
}
func configURL(c *gophercloud.ServiceClient, id string) string {
return c.ServiceURL("instances", id, "configuration")
}
func backupsURL(c *gophercloud.ServiceClient, id string) string {
return c.ServiceURL("instances", id, "backups")
}

View File

@@ -0,0 +1,16 @@
package users
import (
"github.com/rackspace/gophercloud"
os "github.com/rackspace/gophercloud/openstack/db/v1/users"
)
// Create will create a new database user for the specified database instance.
func Create(client *gophercloud.ServiceClient, instanceID string, opts os.CreateOptsBuilder) os.CreateResult {
return os.Create(client, instanceID, opts)
}
// Delete will permanently remove a user from a specified database instance.
func Delete(client *gophercloud.ServiceClient, instanceID, userName string) os.DeleteResult {
return os.Delete(client, instanceID, userName)
}

View File

@@ -0,0 +1,3 @@
// Package users provides information and interaction with the user API
// resource in the Rackspace Database service.
package users

View File

@@ -0,0 +1,77 @@
package users
const singleDB = `{"databases": [{"name": "databaseE"}]}`
var changePwdReq = `
{
"users": [
{
"name": "dbuser1",
"password": "newpassword"
},
{
"name": "dbuser2",
"password": "anotherpassword"
}
]
}
`
var updateReq = `
{
"user": {
"name": "new_username",
"password": "new_password"
}
}
`
var getResp = `
{
"user": {
"name": "exampleuser",
"host": "foo",
"databases": [
{
"name": "databaseA"
},
{
"name": "databaseB"
}
]
}
}
`
var listResp = `
{
"users": [
{
"name": "dbuser1",
"host": "localhost",
"databases": [
{
"name": "databaseA"
}
]
},
{
"name": "dbuser2",
"host": "localhost",
"databases": [
{
"name": "databaseB"
},
{
"name": "databaseC"
}
]
}
]
}
`
var (
listUserAccessResp = singleDB
grantUserAccessReq = singleDB
)

View File

@@ -0,0 +1,176 @@
package users
import (
"errors"
"github.com/rackspace/gophercloud"
db "github.com/rackspace/gophercloud/openstack/db/v1/databases"
os "github.com/rackspace/gophercloud/openstack/db/v1/users"
"github.com/rackspace/gophercloud/pagination"
)
// List will list all available users for a specified database instance.
func List(client *gophercloud.ServiceClient, instanceID string) pagination.Pager {
createPageFn := func(r pagination.PageResult) pagination.Page {
return UserPage{pagination.LinkedPageBase{PageResult: r}}
}
return pagination.NewPager(client, baseURL(client, instanceID), createPageFn)
}
/*
ChangePassword changes the password for one or more users. For example, to
change the respective passwords for two users:
opts := os.BatchCreateOpts{
os.CreateOpts{Name: "db_user_1", Password: "new_password_1"},
os.CreateOpts{Name: "db_user_2", Password: "new_password_2"},
}
ChangePassword(client, "instance_id", opts)
*/
func ChangePassword(client *gophercloud.ServiceClient, instanceID string, opts os.CreateOptsBuilder) UpdatePasswordsResult {
var res UpdatePasswordsResult
reqBody, err := opts.ToUserCreateMap()
if err != nil {
res.Err = err
return res
}
_, res.Err = client.Request("PUT", baseURL(client, instanceID), gophercloud.RequestOpts{
JSONBody: &reqBody,
OkCodes: []int{202},
})
return res
}
// UpdateOpts is the struct responsible for updating an existing user.
type UpdateOpts struct {
// [OPTIONAL] Specifies a name for the user. Valid names can be composed
// of the following characters: letters (either case); numbers; these
// characters '@', '?', '#', ' ' but NEVER beginning a name string; '_' is
// permitted anywhere. Prohibited characters that are forbidden include:
// single quotes, double quotes, back quotes, semicolons, commas, backslashes,
// and forward slashes. Spaces at the front or end of a user name are also
// not permitted.
Name string
// [OPTIONAL] Specifies a password for the user.
Password string
// [OPTIONAL] Specifies the host from which a user is allowed to connect to
// the database. Possible values are a string containing an IPv4 address or
// "%" to allow connecting from any host. Optional; the default is "%".
Host string
}
// ToMap is a convenience function for creating sub-maps for individual users.
func (opts UpdateOpts) ToMap() (map[string]interface{}, error) {
if opts.Name == "root" {
return nil, errors.New("root is a reserved user name and cannot be used")
}
user := map[string]interface{}{}
if opts.Name != "" {
user["name"] = opts.Name
}
if opts.Password != "" {
user["password"] = opts.Password
}
if opts.Host != "" {
user["host"] = opts.Host
}
return user, nil
}
// Update will modify the attributes of a specified user. Attributes that can
// be updated are: user name, password, and host.
func Update(client *gophercloud.ServiceClient, instanceID, userName string, opts UpdateOpts) UpdateResult {
var res UpdateResult
reqBody, err := opts.ToMap()
if err != nil {
res.Err = err
return res
}
reqBody = map[string]interface{}{"user": reqBody}
_, res.Err = client.Request("PUT", userURL(client, instanceID, userName), gophercloud.RequestOpts{
JSONBody: &reqBody,
OkCodes: []int{202},
})
return res
}
// Get will retrieve the details for a particular user.
func Get(client *gophercloud.ServiceClient, instanceID, userName string) GetResult {
var res GetResult
_, res.Err = client.Request("GET", userURL(client, instanceID, userName), gophercloud.RequestOpts{
JSONResponse: &res.Body,
OkCodes: []int{200},
})
return res
}
// ListAccess will list all of the databases a user has access to.
func ListAccess(client *gophercloud.ServiceClient, instanceID, userName string) pagination.Pager {
pageFn := func(r pagination.PageResult) pagination.Page {
return AccessPage{pagination.LinkedPageBase{PageResult: r}}
}
return pagination.NewPager(client, dbsURL(client, instanceID, userName), pageFn)
}
/*
GrantAccess for the specified user to one or more databases on a specified
instance. For example, to add a user to multiple databases:
opts := db.BatchCreateOpts{
db.CreateOpts{Name: "database_1"},
db.CreateOpts{Name: "database_3"},
db.CreateOpts{Name: "database_19"},
}
GrantAccess(client, "instance_id", "user_name", opts)
*/
func GrantAccess(client *gophercloud.ServiceClient, instanceID, userName string, opts db.CreateOptsBuilder) GrantAccessResult {
var res GrantAccessResult
reqBody, err := opts.ToDBCreateMap()
if err != nil {
res.Err = err
return res
}
_, res.Err = client.Request("PUT", dbsURL(client, instanceID, userName), gophercloud.RequestOpts{
JSONBody: &reqBody,
OkCodes: []int{202},
})
return res
}
/*
RevokeAccess will revoke access for the specified user to one or more databases
on a specified instance. For example:
RevokeAccess(client, "instance_id", "user_name", "db_name")
*/
func RevokeAccess(client *gophercloud.ServiceClient, instanceID, userName, dbName string) RevokeAccessResult {
var res RevokeAccessResult
_, res.Err = client.Request("DELETE", dbURL(client, instanceID, userName, dbName), gophercloud.RequestOpts{
OkCodes: []int{202},
})
return res
}

View File

@@ -0,0 +1,149 @@
package users
import (
"github.com/mitchellh/mapstructure"
"github.com/rackspace/gophercloud"
db "github.com/rackspace/gophercloud/openstack/db/v1/databases"
"github.com/rackspace/gophercloud/pagination"
)
// User represents a database user
type User struct {
// The user name
Name string
// The user password
Password string
// Specifies the host from which a user is allowed to connect to the database.
// Possible values are a string containing an IPv4 address or "%" to allow
// connecting from any host.
Host string
// The databases associated with this user
Databases []db.Database
}
// UpdatePasswordsResult represents the result of changing a user password.
type UpdatePasswordsResult struct {
gophercloud.ErrResult
}
// UpdateResult represents the result of updating a user.
type UpdateResult struct {
gophercloud.ErrResult
}
// GetResult represents the result of getting a user.
type GetResult struct {
gophercloud.Result
}
// Extract will retrieve a User struct from a getresult.
func (r GetResult) Extract() (*User, error) {
if r.Err != nil {
return nil, r.Err
}
var response struct {
User User `mapstructure:"user"`
}
err := mapstructure.Decode(r.Body, &response)
return &response.User, err
}
// AccessPage represents a single page of a paginated user collection.
type AccessPage struct {
pagination.LinkedPageBase
}
// IsEmpty checks to see whether the collection is empty.
func (page AccessPage) IsEmpty() (bool, error) {
users, err := ExtractDBs(page)
if err != nil {
return true, err
}
return len(users) == 0, nil
}
// NextPageURL will retrieve the next page URL.
func (page AccessPage) NextPageURL() (string, error) {
type resp struct {
Links []gophercloud.Link `mapstructure:"databases_links"`
}
var r resp
err := mapstructure.Decode(page.Body, &r)
if err != nil {
return "", err
}
return gophercloud.ExtractNextURL(r.Links)
}
// ExtractDBs will convert a generic pagination struct into a more
// relevant slice of DB structs.
func ExtractDBs(page pagination.Page) ([]db.Database, error) {
casted := page.(AccessPage).Body
var response struct {
DBs []db.Database `mapstructure:"databases"`
}
err := mapstructure.Decode(casted, &response)
return response.DBs, err
}
// UserPage represents a single page of a paginated user collection.
type UserPage struct {
pagination.LinkedPageBase
}
// IsEmpty checks to see whether the collection is empty.
func (page UserPage) IsEmpty() (bool, error) {
users, err := ExtractUsers(page)
if err != nil {
return true, err
}
return len(users) == 0, nil
}
// NextPageURL will retrieve the next page URL.
func (page UserPage) NextPageURL() (string, error) {
type resp struct {
Links []gophercloud.Link `mapstructure:"users_links"`
}
var r resp
err := mapstructure.Decode(page.Body, &r)
if err != nil {
return "", err
}
return gophercloud.ExtractNextURL(r.Links)
}
// ExtractUsers will convert a generic pagination struct into a more
// relevant slice of User structs.
func ExtractUsers(page pagination.Page) ([]User, error) {
casted := page.(UserPage).Body
var response struct {
Users []User `mapstructure:"users"`
}
err := mapstructure.Decode(casted, &response)
return response.Users, err
}
// GrantAccessResult represents the result of granting access to a user.
type GrantAccessResult struct {
gophercloud.ErrResult
}
// RevokeAccessResult represents the result of revoking access to a user.
type RevokeAccessResult struct {
gophercloud.ErrResult
}

View File

@@ -0,0 +1,19 @@
package users
import "github.com/rackspace/gophercloud"
func baseURL(c *gophercloud.ServiceClient, instanceID string) string {
return c.ServiceURL("instances", instanceID, "users")
}
func userURL(c *gophercloud.ServiceClient, instanceID, userName string) string {
return c.ServiceURL("instances", instanceID, "users", userName)
}
func dbsURL(c *gophercloud.ServiceClient, instanceID, userName string) string {
return c.ServiceURL("instances", instanceID, "users", userName, "databases")
}
func dbURL(c *gophercloud.ServiceClient, instanceID, userName, dbName string) string {
return c.ServiceURL("instances", instanceID, "users", userName, "databases", dbName)
}

View File

@@ -0,0 +1,24 @@
package extensions
import (
"github.com/rackspace/gophercloud"
common "github.com/rackspace/gophercloud/openstack/common/extensions"
"github.com/rackspace/gophercloud/pagination"
)
// ExtractExtensions accepts a Page struct, specifically an ExtensionPage struct, and extracts the
// elements into a slice of os.Extension structs.
func ExtractExtensions(page pagination.Page) ([]common.Extension, error) {
return common.ExtractExtensions(page)
}
// Get retrieves information for a specific extension using its alias.
func Get(c *gophercloud.ServiceClient, alias string) common.GetResult {
return common.Get(c, alias)
}
// List returns a Pager which allows you to iterate over the full collection of extensions.
// It does not accept query parameters.
func List(c *gophercloud.ServiceClient) pagination.Pager {
return common.List(c)
}

View File

@@ -0,0 +1,3 @@
// Package extensions provides information and interaction with the all the
// extensions available for the Rackspace Identity service.
package extensions

View File

@@ -0,0 +1,50 @@
package roles
import (
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
os "github.com/rackspace/gophercloud/openstack/identity/v2/extensions/admin/roles"
)
// List is the operation responsible for listing all available global roles
// that a user can adopt.
func List(client *gophercloud.ServiceClient) pagination.Pager {
return os.List(client)
}
// AddUserRole is the operation responsible for assigning a particular role to
// a user. This is confined to the scope of the user's tenant - so the tenant
// ID is a required argument.
func AddUserRole(client *gophercloud.ServiceClient, userID, roleID string) UserRoleResult {
var result UserRoleResult
_, result.Err = client.Request("PUT", userRoleURL(client, userID, roleID), gophercloud.RequestOpts{
OkCodes: []int{200, 201},
})
return result
}
// DeleteUserRole is the operation responsible for deleting a particular role
// from a user. This is confined to the scope of the user's tenant - so the
// tenant ID is a required argument.
func DeleteUserRole(client *gophercloud.ServiceClient, userID, roleID string) UserRoleResult {
var result UserRoleResult
_, result.Err = client.Request("DELETE", userRoleURL(client, userID, roleID), gophercloud.RequestOpts{
OkCodes: []int{204},
})
return result
}
// UserRoleResult represents the result of either an AddUserRole or
// a DeleteUserRole operation.
type UserRoleResult struct {
gophercloud.ErrResult
}
func userRoleURL(c *gophercloud.ServiceClient, userID, roleID string) string {
return c.ServiceURL(os.UserPath, userID, os.RolePath, os.ExtPath, roleID)
}

View File

@@ -0,0 +1,49 @@
package roles
import (
"fmt"
"net/http"
"testing"
th "github.com/rackspace/gophercloud/testhelper"
fake "github.com/rackspace/gophercloud/testhelper/client"
)
func MockListRoleResponse(t *testing.T) {
th.Mux.HandleFunc("/OS-KSADM/roles", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "GET")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, `
{
"roles": [
{
"id": "123",
"name": "compute:admin",
"description": "Nova Administrator",
"serviceId": "cke5372ebabeeabb70a0e702a4626977x4406e5"
}
]
}
`)
})
}
func MockAddUserRoleResponse(t *testing.T) {
th.Mux.HandleFunc("/users/{user_id}/roles/OS-KSADM/{role_id}", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "PUT")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
w.WriteHeader(http.StatusCreated)
})
}
func MockDeleteUserRoleResponse(t *testing.T) {
th.Mux.HandleFunc("/users/{user_id}/roles/OS-KSADM/{role_id}", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "DELETE")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
w.WriteHeader(http.StatusNoContent)
})
}

View File

@@ -0,0 +1,17 @@
package tenants
import (
"github.com/rackspace/gophercloud"
os "github.com/rackspace/gophercloud/openstack/identity/v2/tenants"
"github.com/rackspace/gophercloud/pagination"
)
// ExtractTenants interprets a page of List results as a more usable slice of Tenant structs.
func ExtractTenants(page pagination.Page) ([]os.Tenant, error) {
return os.ExtractTenants(page)
}
// List enumerates the tenants to which the current token grants access.
func List(client *gophercloud.ServiceClient, opts *os.ListOpts) pagination.Pager {
return os.List(client, opts)
}

View File

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

View File

@@ -0,0 +1,60 @@
package tokens
import (
"errors"
"github.com/rackspace/gophercloud"
os "github.com/rackspace/gophercloud/openstack/identity/v2/tokens"
)
var (
// ErrPasswordProvided is returned if both a password and an API key are provided to Create.
ErrPasswordProvided = errors.New("Please provide either a password or an API key.")
)
// AuthOptions wraps the OpenStack AuthOptions struct to be able to customize the request body
// when API key authentication is used.
type AuthOptions struct {
os.AuthOptions
}
// WrapOptions embeds a root AuthOptions struct in a package-specific one.
func WrapOptions(original gophercloud.AuthOptions) AuthOptions {
return AuthOptions{AuthOptions: os.WrapOptions(original)}
}
// ToTokenCreateMap serializes an AuthOptions into a request body. If an API key is provided, it
// will be used, otherwise
func (auth AuthOptions) ToTokenCreateMap() (map[string]interface{}, error) {
if auth.APIKey == "" {
return auth.AuthOptions.ToTokenCreateMap()
}
// Verify that other required attributes are present.
if auth.Username == "" {
return nil, os.ErrUsernameRequired
}
authMap := make(map[string]interface{})
authMap["RAX-KSKEY:apiKeyCredentials"] = map[string]interface{}{
"username": auth.Username,
"apiKey": auth.APIKey,
}
if auth.TenantID != "" {
authMap["tenantId"] = auth.TenantID
}
if auth.TenantName != "" {
authMap["tenantName"] = auth.TenantName
}
return map[string]interface{}{"auth": authMap}, nil
}
// Create authenticates to Rackspace's identity service and attempts to acquire a Token. Rather
// than interact with this service directly, users should generally call
// rackspace.AuthenticatedClient().
func Create(client *gophercloud.ServiceClient, auth AuthOptions) os.CreateResult {
return os.Create(client, auth)
}

View File

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

View File

@@ -0,0 +1,142 @@
package users
import (
"errors"
"github.com/rackspace/gophercloud"
os "github.com/rackspace/gophercloud/openstack/identity/v2/users"
"github.com/rackspace/gophercloud/pagination"
)
// List returns a pager that allows traversal over a collection of users.
func List(client *gophercloud.ServiceClient) pagination.Pager {
return os.List(client)
}
// CommonOpts are the options which are shared between CreateOpts and
// UpdateOpts
type CommonOpts struct {
// Required. The username to assign to the user. When provided, the username
// must:
// - start with an alphabetical (A-Za-z) character
// - have a minimum length of 1 character
//
// The username may contain upper and lowercase characters, as well as any of
// the following special character: . - @ _
Username string
// Required. Email address for the user account.
Email string
// Required. Indicates whether the user can authenticate after the user
// account is created. If no value is specified, the default value is true.
Enabled os.EnabledState
// Optional. The password to assign to the user. If provided, the password
// must:
// - start with an alphabetical (A-Za-z) character
// - have a minimum length of 8 characters
// - contain at least one uppercase character, one lowercase character, and
// one numeric character.
//
// The password may contain any of the following special characters: . - @ _
Password string
}
// CreateOpts represents the options needed when creating new users.
type CreateOpts CommonOpts
// ToUserCreateMap assembles a request body based on the contents of a CreateOpts.
func (opts CreateOpts) ToUserCreateMap() (map[string]interface{}, error) {
m := make(map[string]interface{})
if opts.Username == "" {
return m, errors.New("Username is a required field")
}
if opts.Enabled == nil {
return m, errors.New("Enabled is a required field")
}
if opts.Email == "" {
return m, errors.New("Email is a required field")
}
if opts.Username != "" {
m["username"] = opts.Username
}
if opts.Email != "" {
m["email"] = opts.Email
}
if opts.Enabled != nil {
m["enabled"] = opts.Enabled
}
if opts.Password != "" {
m["OS-KSADM:password"] = opts.Password
}
return map[string]interface{}{"user": m}, nil
}
// Create is the operation responsible for creating new users.
func Create(client *gophercloud.ServiceClient, opts os.CreateOptsBuilder) CreateResult {
return CreateResult{os.Create(client, opts)}
}
// Get requests details on a single user, either by ID.
func Get(client *gophercloud.ServiceClient, id string) GetResult {
return GetResult{os.Get(client, id)}
}
// UpdateOptsBuilder allows extensions to add additional attributes to the Update request.
type UpdateOptsBuilder interface {
ToUserUpdateMap() map[string]interface{}
}
// UpdateOpts specifies the base attributes that may be updated on an existing server.
type UpdateOpts CommonOpts
// ToUserUpdateMap formats an UpdateOpts structure into a request body.
func (opts UpdateOpts) ToUserUpdateMap() map[string]interface{} {
m := make(map[string]interface{})
if opts.Username != "" {
m["username"] = opts.Username
}
if opts.Enabled != nil {
m["enabled"] = &opts.Enabled
}
if opts.Email != "" {
m["email"] = opts.Email
}
return map[string]interface{}{"user": m}
}
// Update is the operation responsible for updating exist users by their UUID.
func Update(client *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) UpdateResult {
var result UpdateResult
_, result.Err = client.Request("POST", os.ResourceURL(client, id), gophercloud.RequestOpts{
JSONResponse: &result.Body,
JSONBody: opts.ToUserUpdateMap(),
OkCodes: []int{200},
})
return result
}
// Delete is the operation responsible for permanently deleting an API user.
func Delete(client *gophercloud.ServiceClient, id string) os.DeleteResult {
return os.Delete(client, id)
}
// ResetAPIKey resets the User's API key.
func ResetAPIKey(client *gophercloud.ServiceClient, id string) ResetAPIKeyResult {
var result ResetAPIKeyResult
_, result.Err = client.Request("POST", resetAPIKeyURL(client, id), gophercloud.RequestOpts{
JSONResponse: &result.Body,
OkCodes: []int{200},
})
return result
}

View File

@@ -0,0 +1,154 @@
package users
import (
"fmt"
"net/http"
"testing"
th "github.com/rackspace/gophercloud/testhelper"
fake "github.com/rackspace/gophercloud/testhelper/client"
)
func mockListResponse(t *testing.T) {
th.Mux.HandleFunc("/users", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "GET")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, `
{
"users":[
{
"id": "u1000",
"username": "jqsmith",
"email": "john.smith@example.org",
"enabled": true
},
{
"id": "u1001",
"username": "jqsmith",
"email": "jane.smith@example.org",
"enabled": true
}
]
}
`)
})
}
func mockCreateUser(t *testing.T) {
th.Mux.HandleFunc("/users", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "POST")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
th.TestJSONRequest(t, r, `
{
"user": {
"username": "new_user",
"enabled": false,
"email": "new_user@foo.com",
"OS-KSADM:password": "foo"
}
}
`)
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, `
{
"user": {
"RAX-AUTH:defaultRegion": "DFW",
"RAX-AUTH:domainId": "5830280",
"id": "123456",
"username": "new_user",
"email": "new_user@foo.com",
"enabled": false
}
}
`)
})
}
func mockGetUser(t *testing.T) {
th.Mux.HandleFunc("/users/new_user", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "GET")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, `
{
"user": {
"RAX-AUTH:defaultRegion": "DFW",
"RAX-AUTH:domainId": "5830280",
"RAX-AUTH:multiFactorEnabled": "true",
"id": "c39e3de9be2d4c779f1dfd6abacc176d",
"username": "jqsmith",
"email": "john.smith@example.org",
"enabled": true
}
}
`)
})
}
func mockUpdateUser(t *testing.T) {
th.Mux.HandleFunc("/users/c39e3de9be2d4c779f1dfd6abacc176d", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "POST")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
th.TestJSONRequest(t, r, `
{
"user": {
"email": "new_email@foo.com",
"enabled": true
}
}
`)
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, `
{
"user": {
"RAX-AUTH:defaultRegion": "DFW",
"RAX-AUTH:domainId": "5830280",
"RAX-AUTH:multiFactorEnabled": "true",
"id": "123456",
"username": "jqsmith",
"email": "new_email@foo.com",
"enabled": true
}
}
`)
})
}
func mockDeleteUser(t *testing.T) {
th.Mux.HandleFunc("/users/c39e3de9be2d4c779f1dfd6abacc176d", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "DELETE")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
w.WriteHeader(http.StatusNoContent)
})
}
func mockResetAPIKey(t *testing.T) {
th.Mux.HandleFunc("/users/99/OS-KSADM/credentials/RAX-KSKEY:apiKeyCredentials/RAX-AUTH/reset", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "POST")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, `
{
"RAX-KSKEY:apiKeyCredentials": {
"username": "joesmith",
"apiKey": "mooH1eiLahd5ahYood7r"
}
}`)
})
}

View File

@@ -0,0 +1,129 @@
package users
import (
"strconv"
"github.com/rackspace/gophercloud"
os "github.com/rackspace/gophercloud/openstack/identity/v2/users"
"github.com/mitchellh/mapstructure"
)
// User represents a user resource that exists on the API.
type User struct {
// The UUID for this user.
ID string
// The human name for this user.
Name string
// The username for this user.
Username string
// Indicates whether the user is enabled (true) or disabled (false).
Enabled bool
// The email address for this user.
Email string
// The ID of the tenant to which this user belongs.
TenantID string `mapstructure:"tenant_id"`
// Specifies the default region for the user account. This value is inherited
// from the user administrator when the account is created.
DefaultRegion string `mapstructure:"RAX-AUTH:defaultRegion"`
// Identifies the domain that contains the user account. This value is
// inherited from the user administrator when the account is created.
DomainID string `mapstructure:"RAX-AUTH:domainId"`
// The password value that the user needs for authentication. If the Add user
// request included a password value, this attribute is not included in the
// response.
Password string `mapstructure:"OS-KSADM:password"`
// Indicates whether the user has enabled multi-factor authentication.
MultiFactorEnabled bool `mapstructure:"RAX-AUTH:multiFactorEnabled"`
}
// CreateResult represents the result of a Create operation
type CreateResult struct {
os.CreateResult
}
// GetResult represents the result of a Get operation
type GetResult struct {
os.GetResult
}
// UpdateResult represents the result of an Update operation
type UpdateResult struct {
os.UpdateResult
}
func commonExtract(resp interface{}, err error) (*User, error) {
if err != nil {
return nil, err
}
var respStruct struct {
User *User `json:"user"`
}
// Since the API returns a string instead of a bool, we need to hack the JSON
json := resp.(map[string]interface{})
user := json["user"].(map[string]interface{})
if s, ok := user["RAX-AUTH:multiFactorEnabled"].(string); ok && s != "" {
if b, err := strconv.ParseBool(s); err == nil {
user["RAX-AUTH:multiFactorEnabled"] = b
}
}
err = mapstructure.Decode(json, &respStruct)
return respStruct.User, err
}
// Extract will get the Snapshot object out of the GetResult object.
func (r GetResult) Extract() (*User, error) {
return commonExtract(r.Body, r.Err)
}
// Extract will get the Snapshot object out of the CreateResult object.
func (r CreateResult) Extract() (*User, error) {
return commonExtract(r.Body, r.Err)
}
// Extract will get the Snapshot object out of the UpdateResult object.
func (r UpdateResult) Extract() (*User, error) {
return commonExtract(r.Body, r.Err)
}
// ResetAPIKeyResult represents the server response to the ResetAPIKey method.
type ResetAPIKeyResult struct {
gophercloud.Result
}
// ResetAPIKeyValue represents an API Key that has been reset.
type ResetAPIKeyValue struct {
// The Username for this API Key reset.
Username string `mapstructure:"username"`
// The new API Key for this user.
APIKey string `mapstructure:"apiKey"`
}
// Extract will get the Error or ResetAPIKeyValue object out of the ResetAPIKeyResult object.
func (r ResetAPIKeyResult) Extract() (*ResetAPIKeyValue, error) {
if r.Err != nil {
return nil, r.Err
}
var response struct {
ResetAPIKeyValue ResetAPIKeyValue `mapstructure:"RAX-KSKEY:apiKeyCredentials"`
}
err := mapstructure.Decode(r.Body, &response)
return &response.ResetAPIKeyValue, err
}

View File

@@ -0,0 +1,7 @@
package users
import "github.com/rackspace/gophercloud"
func resetAPIKeyURL(client *gophercloud.ServiceClient, id string) string {
return client.ServiceURL("users", id, "OS-KSADM", "credentials", "RAX-KSKEY:apiKeyCredentials", "RAX-AUTH", "reset")
}

View File

@@ -0,0 +1,12 @@
/*
Package acl provides information and interaction with the access lists feature
of the Rackspace Cloud Load Balancer service.
The access list management feature allows fine-grained network access controls
to be applied to the load balancer's virtual IP address. A single IP address,
multiple IP addresses, or entire network subnets can be added. Items that are
configured with the ALLOW type always takes precedence over items with the DENY
type. To reject traffic from all items except for those with the ALLOW type,
add a networkItem with an address of "0.0.0.0/0" and a DENY type.
*/
package acl

View File

@@ -0,0 +1,109 @@
package acl
import (
"fmt"
"net/http"
"strconv"
"testing"
th "github.com/rackspace/gophercloud/testhelper"
fake "github.com/rackspace/gophercloud/testhelper/client"
)
func _rootURL(lbID int) string {
return "/loadbalancers/" + strconv.Itoa(lbID) + "/accesslist"
}
func mockListResponse(t *testing.T, id int) {
th.Mux.HandleFunc(_rootURL(id), func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "GET")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, `
{
"accessList": [
{
"address": "206.160.163.21",
"id": 21,
"type": "DENY"
},
{
"address": "206.160.163.22",
"id": 22,
"type": "DENY"
},
{
"address": "206.160.163.23",
"id": 23,
"type": "DENY"
},
{
"address": "206.160.163.24",
"id": 24,
"type": "DENY"
}
]
}
`)
})
}
func mockCreateResponse(t *testing.T, lbID int) {
th.Mux.HandleFunc(_rootURL(lbID), func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "POST")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
th.TestJSONRequest(t, r, `
{
"accessList": [
{
"address": "206.160.163.21",
"type": "DENY"
},
{
"address": "206.160.165.11",
"type": "DENY"
}
]
}
`)
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusAccepted)
})
}
func mockDeleteAllResponse(t *testing.T, lbID int) {
th.Mux.HandleFunc(_rootURL(lbID), func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "DELETE")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
w.WriteHeader(http.StatusAccepted)
})
}
func mockBatchDeleteResponse(t *testing.T, lbID int, ids []int) {
th.Mux.HandleFunc(_rootURL(lbID), func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "DELETE")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
r.ParseForm()
for k, v := range ids {
fids := r.Form["id"]
th.AssertEquals(t, strconv.Itoa(v), fids[k])
}
w.WriteHeader(http.StatusAccepted)
})
}
func mockDeleteResponse(t *testing.T, lbID, networkID int) {
th.Mux.HandleFunc(_rootURL(lbID)+"/"+strconv.Itoa(networkID), func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "DELETE")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
w.WriteHeader(http.StatusAccepted)
})
}

View File

@@ -0,0 +1,111 @@
package acl
import (
"errors"
"fmt"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
)
// List is the operation responsible for returning a paginated collection of
// network items that define a load balancer's access list.
func List(client *gophercloud.ServiceClient, lbID int) pagination.Pager {
url := rootURL(client, lbID)
return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page {
return AccessListPage{pagination.SinglePageBase(r)}
})
}
// CreateOptsBuilder is the interface responsible for generating the JSON
// for a Create operation.
type CreateOptsBuilder interface {
ToAccessListCreateMap() (map[string]interface{}, error)
}
// CreateOpts is a slice of CreateOpt structs, that allow the user to create
// multiple nodes in a single operation (one node per CreateOpt).
type CreateOpts []CreateOpt
// CreateOpt represents the options to create a single node.
type CreateOpt struct {
// Required - the IP address or CIDR for item to add to access list.
Address string
// Required - the type of the node. Either ALLOW or DENY.
Type Type
}
// ToAccessListCreateMap converts a slice of options into a map that can be
// used for the JSON.
func (opts CreateOpts) ToAccessListCreateMap() (map[string]interface{}, error) {
type itemMap map[string]interface{}
items := []itemMap{}
for k, v := range opts {
if v.Address == "" {
return itemMap{}, fmt.Errorf("Address is a required attribute, none provided for %d CreateOpt element", k)
}
if v.Type != ALLOW && v.Type != DENY {
return itemMap{}, fmt.Errorf("Type must be ALLOW or DENY")
}
item := make(itemMap)
item["address"] = v.Address
item["type"] = v.Type
items = append(items, item)
}
return itemMap{"accessList": items}, nil
}
// Create is the operation responsible for adding network items to the access
// rules for a particular load balancer. If network items already exist, the
// new item will be appended. A single IP address or subnet range is considered
// unique and cannot be duplicated.
func Create(client *gophercloud.ServiceClient, loadBalancerID int, opts CreateOptsBuilder) CreateResult {
var res CreateResult
reqBody, err := opts.ToAccessListCreateMap()
if err != nil {
res.Err = err
return res
}
_, res.Err = client.Post(rootURL(client, loadBalancerID), reqBody, nil, nil)
return res
}
// BulkDelete will delete multiple network items from a load balancer's access
// list in a single operation.
func BulkDelete(c *gophercloud.ServiceClient, loadBalancerID int, itemIDs []int) DeleteResult {
var res DeleteResult
if len(itemIDs) > 10 || len(itemIDs) == 0 {
res.Err = errors.New("You must provide a minimum of 1 and a maximum of 10 item IDs")
return res
}
url := rootURL(c, loadBalancerID)
url += gophercloud.IDSliceToQueryString("id", itemIDs)
_, res.Err = c.Delete(url, nil)
return res
}
// Delete will remove a single network item from a load balancer's access list.
func Delete(c *gophercloud.ServiceClient, lbID, itemID int) DeleteResult {
var res DeleteResult
_, res.Err = c.Delete(resourceURL(c, lbID, itemID), nil)
return res
}
// DeleteAll will delete the entire contents of a load balancer's access list,
// effectively resetting it and allowing all traffic.
func DeleteAll(c *gophercloud.ServiceClient, lbID int) DeleteResult {
var res DeleteResult
_, res.Err = c.Delete(rootURL(c, lbID), nil)
return res
}

View File

@@ -0,0 +1,72 @@
package acl
import (
"github.com/mitchellh/mapstructure"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
)
// AccessList represents the rules of network access to a particular load
// balancer.
type AccessList []NetworkItem
// NetworkItem describes how an IP address or entire subnet may interact with a
// load balancer.
type NetworkItem struct {
// The IP address or subnet (CIDR) that defines the network item.
Address string
// The numeric unique ID for this item.
ID int
// Either ALLOW or DENY.
Type Type
}
// Type defines how an item may connect to the load balancer.
type Type string
// Convenience consts.
const (
ALLOW Type = "ALLOW"
DENY Type = "DENY"
)
// AccessListPage is the page returned by a pager for traversing over a
// collection of network items in an access list.
type AccessListPage struct {
pagination.SinglePageBase
}
// IsEmpty checks whether an AccessListPage struct is empty.
func (p AccessListPage) IsEmpty() (bool, error) {
is, err := ExtractAccessList(p)
if err != nil {
return true, nil
}
return len(is) == 0, nil
}
// ExtractAccessList accepts a Page struct, specifically an AccessListPage
// struct, and extracts the elements into a slice of NetworkItem structs. In
// other words, a generic collection is mapped into a relevant slice.
func ExtractAccessList(page pagination.Page) (AccessList, error) {
var resp struct {
List AccessList `mapstructure:"accessList" json:"accessList"`
}
err := mapstructure.Decode(page.(AccessListPage).Body, &resp)
return resp.List, err
}
// CreateResult represents the result of a create operation.
type CreateResult struct {
gophercloud.ErrResult
}
// DeleteResult represents the result of a delete operation.
type DeleteResult struct {
gophercloud.ErrResult
}

View File

@@ -0,0 +1,20 @@
package acl
import (
"strconv"
"github.com/rackspace/gophercloud"
)
const (
path = "loadbalancers"
aclPath = "accesslist"
)
func resourceURL(c *gophercloud.ServiceClient, lbID, networkID int) string {
return c.ServiceURL(path, strconv.Itoa(lbID), aclPath, strconv.Itoa(networkID))
}
func rootURL(c *gophercloud.ServiceClient, lbID int) string {
return c.ServiceURL(path, strconv.Itoa(lbID), aclPath)
}

View File

@@ -0,0 +1,44 @@
/*
Package lbs provides information and interaction with the Load Balancer API
resource for the Rackspace Cloud Load Balancer service.
A load balancer is a logical device which belongs to a cloud account. It is
used to distribute workloads between multiple back-end systems or services,
based on the criteria defined as part of its configuration. This configuration
is defined using the Create operation, and can be updated with Update.
To conserve IPv4 address space, it is highly recommended that you share Virtual
IPs between load balancers. If you have at least one load balancer, you may
create subsequent ones that share a single virtual IPv4 and/or a single IPv6 by
passing in a virtual IP ID to the Update operation (instead of a type). This
feature is also highly desirable if you wish to load balance both an insecure
and secure protocol using one IP or DNS name. In order to share a virtual IP,
each Load Balancer must utilize a unique port.
All load balancers have a Status attribute that shows the current configuration
status of the device. This status is immutable by the caller and is updated
automatically based on state changes within the service. When a load balancer
is first created, it is placed into a BUILD state while the configuration is
being generated and applied based on the request. Once the configuration is
applied and finalized, it is in an ACTIVE status. In the event of a
configuration change or update, the status of the load balancer changes to
PENDING_UPDATE to signify configuration changes are in progress but have not yet
been finalized. Load balancers in a SUSPENDED status are configured to reject
traffic and do not forward requests to back-end nodes.
An HTTP load balancer has the X-Forwarded-For (XFF) HTTP header set by default.
This header contains the originating IP address of a client connecting to a web
server through an HTTP proxy or load balancer, which many web applications are
already designed to use when determining the source address for a request.
It also includes the X-Forwarded-Proto (XFP) HTTP header, which has been added
for identifying the originating protocol of an HTTP request as "http" or
"https" depending on which protocol the client requested. This is useful when
using SSL termination.
Finally, it also includes the X-Forwarded-Port HTTP header, which has been
added for being able to generate secure URLs containing the specified port.
This header, along with the X-Forwarded-For header, provides the needed
information to the underlying application servers.
*/
package lbs

View File

@@ -0,0 +1,584 @@
package lbs
import (
"fmt"
"net/http"
"strconv"
"testing"
th "github.com/rackspace/gophercloud/testhelper"
fake "github.com/rackspace/gophercloud/testhelper/client"
)
func mockListLBResponse(t *testing.T) {
th.Mux.HandleFunc("/loadbalancers", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "GET")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, `
{
"loadBalancers":[
{
"name":"lb-site1",
"id":71,
"protocol":"HTTP",
"port":80,
"algorithm":"RANDOM",
"status":"ACTIVE",
"nodeCount":3,
"virtualIps":[
{
"id":403,
"address":"206.55.130.1",
"type":"PUBLIC",
"ipVersion":"IPV4"
}
],
"created":{
"time":"2010-11-30T03:23:42Z"
},
"updated":{
"time":"2010-11-30T03:23:44Z"
}
},
{
"name":"lb-site2",
"id":72,
"created":{
"time":"2011-11-30T03:23:42Z"
},
"updated":{
"time":"2011-11-30T03:23:44Z"
}
},
{
"name":"lb-site3",
"id":73,
"created":{
"time":"2012-11-30T03:23:42Z"
},
"updated":{
"time":"2012-11-30T03:23:44Z"
}
}
]
}
`)
})
}
func mockCreateLBResponse(t *testing.T) {
th.Mux.HandleFunc("/loadbalancers", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "POST")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
th.TestJSONRequest(t, r, `
{
"loadBalancer": {
"name": "a-new-loadbalancer",
"port": 80,
"protocol": "HTTP",
"virtualIps": [
{
"id": 2341
},
{
"id": 900001
}
],
"nodes": [
{
"address": "10.1.1.1",
"port": 80,
"condition": "ENABLED"
}
]
}
}
`)
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusAccepted)
fmt.Fprintf(w, `
{
"loadBalancer": {
"name": "a-new-loadbalancer",
"id": 144,
"protocol": "HTTP",
"halfClosed": false,
"port": 83,
"algorithm": "RANDOM",
"status": "BUILD",
"timeout": 30,
"cluster": {
"name": "ztm-n01.staging1.lbaas.rackspace.net"
},
"nodes": [
{
"address": "10.1.1.1",
"id": 653,
"port": 80,
"status": "ONLINE",
"condition": "ENABLED",
"weight": 1
}
],
"virtualIps": [
{
"address": "206.10.10.210",
"id": 39,
"type": "PUBLIC",
"ipVersion": "IPV4"
},
{
"address": "2001:4801:79f1:0002:711b:be4c:0000:0021",
"id": 900001,
"type": "PUBLIC",
"ipVersion": "IPV6"
}
],
"created": {
"time": "2010-11-30T03:23:42Z"
},
"updated": {
"time": "2010-11-30T03:23:44Z"
},
"connectionLogging": {
"enabled": false
}
}
}
`)
})
}
func mockBatchDeleteLBResponse(t *testing.T, ids []int) {
th.Mux.HandleFunc("/loadbalancers", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "DELETE")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
r.ParseForm()
for k, v := range ids {
fids := r.Form["id"]
th.AssertEquals(t, strconv.Itoa(v), fids[k])
}
w.WriteHeader(http.StatusAccepted)
})
}
func mockDeleteLBResponse(t *testing.T, id int) {
th.Mux.HandleFunc("/loadbalancers/"+strconv.Itoa(id), func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "DELETE")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
w.WriteHeader(http.StatusAccepted)
})
}
func mockGetLBResponse(t *testing.T, id int) {
th.Mux.HandleFunc("/loadbalancers/"+strconv.Itoa(id), func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "GET")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, `
{
"loadBalancer": {
"id": 2000,
"name": "sample-loadbalancer",
"protocol": "HTTP",
"port": 80,
"algorithm": "RANDOM",
"status": "ACTIVE",
"timeout": 30,
"connectionLogging": {
"enabled": true
},
"virtualIps": [
{
"id": 1000,
"address": "206.10.10.210",
"type": "PUBLIC",
"ipVersion": "IPV4"
}
],
"nodes": [
{
"id": 1041,
"address": "10.1.1.1",
"port": 80,
"condition": "ENABLED",
"status": "ONLINE"
},
{
"id": 1411,
"address": "10.1.1.2",
"port": 80,
"condition": "ENABLED",
"status": "ONLINE"
}
],
"sessionPersistence": {
"persistenceType": "HTTP_COOKIE"
},
"connectionThrottle": {
"maxConnections": 100
},
"cluster": {
"name": "c1.dfw1"
},
"created": {
"time": "2010-11-30T03:23:42Z"
},
"updated": {
"time": "2010-11-30T03:23:44Z"
},
"sourceAddresses": {
"ipv6Public": "2001:4801:79f1:1::1/64",
"ipv4Servicenet": "10.0.0.0",
"ipv4Public": "10.12.99.28"
}
}
}
`)
})
}
func mockUpdateLBResponse(t *testing.T, id int) {
th.Mux.HandleFunc("/loadbalancers/"+strconv.Itoa(id), func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "PUT")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
th.TestJSONRequest(t, r, `
{
"loadBalancer": {
"name": "a-new-loadbalancer",
"protocol": "TCP",
"halfClosed": true,
"algorithm": "RANDOM",
"port": 8080,
"timeout": 100,
"httpsRedirect": false
}
}
`)
w.WriteHeader(http.StatusAccepted)
})
}
func mockListProtocolsResponse(t *testing.T) {
th.Mux.HandleFunc("/loadbalancers/protocols", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "GET")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, `
{
"protocols": [
{
"name": "DNS_TCP",
"port": 53
},
{
"name": "DNS_UDP",
"port": 53
},
{
"name": "FTP",
"port": 21
},
{
"name": "HTTP",
"port": 80
},
{
"name": "HTTPS",
"port": 443
},
{
"name": "IMAPS",
"port": 993
},
{
"name": "IMAPv4",
"port": 143
},
{
"name": "LDAP",
"port": 389
},
{
"name": "LDAPS",
"port": 636
},
{
"name": "MYSQL",
"port": 3306
},
{
"name": "POP3",
"port": 110
},
{
"name": "POP3S",
"port": 995
},
{
"name": "SMTP",
"port": 25
},
{
"name": "TCP",
"port": 0
},
{
"name": "TCP_CLIENT_FIRST",
"port": 0
},
{
"name": "UDP",
"port": 0
},
{
"name": "UDP_STREAM",
"port": 0
},
{
"name": "SFTP",
"port": 22
},
{
"name": "TCP_STREAM",
"port": 0
}
]
}
`)
})
}
func mockListAlgorithmsResponse(t *testing.T) {
th.Mux.HandleFunc("/loadbalancers/algorithms", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "GET")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, `
{
"algorithms": [
{
"name": "LEAST_CONNECTIONS"
},
{
"name": "RANDOM"
},
{
"name": "ROUND_ROBIN"
},
{
"name": "WEIGHTED_LEAST_CONNECTIONS"
},
{
"name": "WEIGHTED_ROUND_ROBIN"
}
]
}
`)
})
}
func mockGetLoggingResponse(t *testing.T, id int) {
th.Mux.HandleFunc("/loadbalancers/"+strconv.Itoa(id)+"/connectionlogging", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "GET")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, `
{
"connectionLogging": {
"enabled": true
}
}
`)
})
}
func mockEnableLoggingResponse(t *testing.T, id int) {
th.Mux.HandleFunc("/loadbalancers/"+strconv.Itoa(id)+"/connectionlogging", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "PUT")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
th.TestJSONRequest(t, r, `
{
"connectionLogging":{
"enabled":true
}
}
`)
w.WriteHeader(http.StatusAccepted)
})
}
func mockDisableLoggingResponse(t *testing.T, id int) {
th.Mux.HandleFunc("/loadbalancers/"+strconv.Itoa(id)+"/connectionlogging", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "PUT")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
th.TestJSONRequest(t, r, `
{
"connectionLogging":{
"enabled":false
}
}
`)
w.WriteHeader(http.StatusAccepted)
})
}
func mockGetErrorPageResponse(t *testing.T, id int) {
th.Mux.HandleFunc("/loadbalancers/"+strconv.Itoa(id)+"/errorpage", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "GET")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, `
{
"errorpage": {
"content": "<html>DEFAULT ERROR PAGE</html>"
}
}
`)
})
}
func mockSetErrorPageResponse(t *testing.T, id int) {
th.Mux.HandleFunc("/loadbalancers/"+strconv.Itoa(id)+"/errorpage", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "PUT")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
th.TestJSONRequest(t, r, `
{
"errorpage": {
"content": "<html>New error page</html>"
}
}
`)
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, `
{
"errorpage": {
"content": "<html>New error page</html>"
}
}
`)
})
}
func mockDeleteErrorPageResponse(t *testing.T, id int) {
th.Mux.HandleFunc("/loadbalancers/"+strconv.Itoa(id)+"/errorpage", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "DELETE")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
w.WriteHeader(http.StatusOK)
})
}
func mockGetStatsResponse(t *testing.T, id int) {
th.Mux.HandleFunc("/loadbalancers/"+strconv.Itoa(id)+"/stats", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "GET")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, `
{
"connectTimeOut": 10,
"connectError": 20,
"connectFailure": 30,
"dataTimedOut": 40,
"keepAliveTimedOut": 50,
"maxConn": 60,
"currentConn": 40,
"connectTimeOutSsl": 10,
"connectErrorSsl": 20,
"connectFailureSsl": 30,
"dataTimedOutSsl": 40,
"keepAliveTimedOutSsl": 50,
"maxConnSsl": 60,
"currentConnSsl": 40
}
`)
})
}
func mockGetCachingResponse(t *testing.T, id int) {
th.Mux.HandleFunc("/loadbalancers/"+strconv.Itoa(id)+"/contentcaching", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "GET")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, `
{
"contentCaching": {
"enabled": true
}
}
`)
})
}
func mockEnableCachingResponse(t *testing.T, id int) {
th.Mux.HandleFunc("/loadbalancers/"+strconv.Itoa(id)+"/contentcaching", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "PUT")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
th.TestJSONRequest(t, r, `
{
"contentCaching":{
"enabled":true
}
}
`)
w.WriteHeader(http.StatusAccepted)
})
}
func mockDisableCachingResponse(t *testing.T, id int) {
th.Mux.HandleFunc("/loadbalancers/"+strconv.Itoa(id)+"/contentcaching", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "PUT")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
th.TestJSONRequest(t, r, `
{
"contentCaching":{
"enabled":false
}
}
`)
w.WriteHeader(http.StatusAccepted)
})
}

View File

@@ -0,0 +1,497 @@
package lbs
import (
"errors"
"github.com/mitchellh/mapstructure"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
"github.com/rackspace/gophercloud/rackspace/lb/v1/acl"
"github.com/rackspace/gophercloud/rackspace/lb/v1/monitors"
"github.com/rackspace/gophercloud/rackspace/lb/v1/nodes"
"github.com/rackspace/gophercloud/rackspace/lb/v1/sessions"
"github.com/rackspace/gophercloud/rackspace/lb/v1/throttle"
"github.com/rackspace/gophercloud/rackspace/lb/v1/vips"
)
var (
errNameRequired = errors.New("Name is a required attribute")
errTimeoutExceeded = errors.New("Timeout must be less than 120")
)
// ListOptsBuilder allows extensions to add additional parameters to the
// List request.
type ListOptsBuilder interface {
ToLBListQuery() (string, error)
}
// ListOpts allows the filtering and sorting of paginated collections through
// the API.
type ListOpts struct {
ChangesSince string `q:"changes-since"`
Status Status `q:"status"`
NodeAddr string `q:"nodeaddress"`
Marker string `q:"marker"`
Limit int `q:"limit"`
}
// ToLBListQuery formats a ListOpts into a query string.
func (opts ListOpts) ToLBListQuery() (string, error) {
q, err := gophercloud.BuildQueryString(opts)
if err != nil {
return "", err
}
return q.String(), nil
}
// List is the operation responsible for returning a paginated collection of
// load balancers. You may pass in a ListOpts struct to filter results.
func List(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
url := rootURL(client)
if opts != nil {
query, err := opts.ToLBListQuery()
if err != nil {
return pagination.Pager{Err: err}
}
url += query
}
return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page {
return LBPage{pagination.LinkedPageBase{PageResult: r}}
})
}
// CreateOptsBuilder is the interface options structs have to satisfy in order
// to be used in the main Create operation in this package. Since many
// extensions decorate or modify the common logic, it is useful for them to
// satisfy a basic interface in order for them to be used.
type CreateOptsBuilder interface {
ToLBCreateMap() (map[string]interface{}, error)
}
// CreateOpts is the common options struct used in this package's Create
// operation.
type CreateOpts struct {
// Required - name of the load balancer to create. The name must be 128
// characters or fewer in length, and all UTF-8 characters are valid.
Name string
// Optional - nodes to be added.
Nodes []nodes.Node
// Required - protocol of the service that is being load balanced.
// See http://docs.rackspace.com/loadbalancers/api/v1.0/clb-devguide/content/protocols.html
// for a full list of supported protocols.
Protocol string
// Optional - enables or disables Half-Closed support for the load balancer.
// Half-Closed support provides the ability for one end of the connection to
// terminate its output, while still receiving data from the other end. Only
// available for TCP/TCP_CLIENT_FIRST protocols.
HalfClosed gophercloud.EnabledState
// Optional - the type of virtual IPs you want associated with the load
// balancer.
VIPs []vips.VIP
// Optional - the access list management feature allows fine-grained network
// access controls to be applied to the load balancer virtual IP address.
AccessList *acl.AccessList
// Optional - algorithm that defines how traffic should be directed between
// back-end nodes.
Algorithm string
// Optional - current connection logging configuration.
ConnectionLogging *ConnectionLogging
// Optional - specifies a limit on the number of connections per IP address
// to help mitigate malicious or abusive traffic to your applications.
ConnThrottle *throttle.ConnectionThrottle
// Optional - the type of health monitor check to perform to ensure that the
// service is performing properly.
HealthMonitor *monitors.Monitor
// Optional - arbitrary information that can be associated with each LB.
Metadata map[string]interface{}
// Optional - port number for the service you are load balancing.
Port int
// Optional - the timeout value for the load balancer and communications with
// its nodes. Defaults to 30 seconds with a maximum of 120 seconds.
Timeout int
// Optional - specifies whether multiple requests from clients are directed
// to the same node.
SessionPersistence *sessions.SessionPersistence
// Optional - enables or disables HTTP to HTTPS redirection for the load
// balancer. When enabled, any HTTP request returns status code 301 (Moved
// Permanently), and the requester is redirected to the requested URL via the
// HTTPS protocol on port 443. For example, http://example.com/page.html
// would be redirected to https://example.com/page.html. Only available for
// HTTPS protocol (port=443), or HTTP protocol with a properly configured SSL
// termination (secureTrafficOnly=true, securePort=443).
HTTPSRedirect gophercloud.EnabledState
}
// ToLBCreateMap casts a CreateOpts struct to a map.
func (opts CreateOpts) ToLBCreateMap() (map[string]interface{}, error) {
lb := make(map[string]interface{})
if opts.Name == "" {
return lb, errNameRequired
}
if opts.Timeout > 120 {
return lb, errTimeoutExceeded
}
lb["name"] = opts.Name
if len(opts.Nodes) > 0 {
nodes := []map[string]interface{}{}
for _, n := range opts.Nodes {
nodes = append(nodes, map[string]interface{}{
"address": n.Address,
"port": n.Port,
"condition": n.Condition,
})
}
lb["nodes"] = nodes
}
if opts.Protocol != "" {
lb["protocol"] = opts.Protocol
}
if opts.HalfClosed != nil {
lb["halfClosed"] = opts.HalfClosed
}
if len(opts.VIPs) > 0 {
lb["virtualIps"] = opts.VIPs
}
if opts.AccessList != nil {
lb["accessList"] = &opts.AccessList
}
if opts.Algorithm != "" {
lb["algorithm"] = opts.Algorithm
}
if opts.ConnectionLogging != nil {
lb["connectionLogging"] = &opts.ConnectionLogging
}
if opts.ConnThrottle != nil {
lb["connectionThrottle"] = &opts.ConnThrottle
}
if opts.HealthMonitor != nil {
lb["healthMonitor"] = &opts.HealthMonitor
}
if len(opts.Metadata) != 0 {
lb["metadata"] = opts.Metadata
}
if opts.Port > 0 {
lb["port"] = opts.Port
}
if opts.Timeout > 0 {
lb["timeout"] = opts.Timeout
}
if opts.SessionPersistence != nil {
lb["sessionPersistence"] = &opts.SessionPersistence
}
if opts.HTTPSRedirect != nil {
lb["httpsRedirect"] = &opts.HTTPSRedirect
}
return map[string]interface{}{"loadBalancer": lb}, nil
}
// Create is the operation responsible for asynchronously provisioning a new
// load balancer based on the configuration defined in CreateOpts. Once the
// request is validated and progress has started on the provisioning process, a
// response struct is returned. When extracted (with Extract()), you have
// to the load balancer's unique ID and status.
//
// Once an ID is attained, you can check on the progress of the operation by
// calling Get and passing in the ID. If the corresponding request cannot be
// fulfilled due to insufficient or invalid data, an HTTP 400 (Bad Request)
// error response is returned with information regarding the nature of the
// failure in the body of the response. Failures in the validation process are
// non-recoverable and require the caller to correct the cause of the failure.
func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult {
var res CreateResult
reqBody, err := opts.ToLBCreateMap()
if err != nil {
res.Err = err
return res
}
_, res.Err = c.Post(rootURL(c), reqBody, &res.Body, nil)
return res
}
// Get is the operation responsible for providing detailed information
// regarding a specific load balancer which is configured and associated with
// your account. This operation is not capable of returning details for a load
// balancer which has been deleted.
func Get(c *gophercloud.ServiceClient, id int) GetResult {
var res GetResult
_, res.Err = c.Get(resourceURL(c, id), &res.Body, &gophercloud.RequestOpts{
OkCodes: []int{200},
})
return res
}
// BulkDelete removes all the load balancers referenced in the slice of IDs.
// Any and all configuration data associated with these load balancers is
// immediately purged and is not recoverable.
//
// If one of the items in the list cannot be removed due to its current status,
// a 400 Bad Request error is returned along with the IDs of the ones the
// system identified as potential failures for this request.
func BulkDelete(c *gophercloud.ServiceClient, ids []int) DeleteResult {
var res DeleteResult
if len(ids) > 10 || len(ids) == 0 {
res.Err = errors.New("You must provide a minimum of 1 and a maximum of 10 LB IDs")
return res
}
url := rootURL(c)
url += gophercloud.IDSliceToQueryString("id", ids)
_, res.Err = c.Delete(url, nil)
return res
}
// Delete removes a single load balancer.
func Delete(c *gophercloud.ServiceClient, id int) DeleteResult {
var res DeleteResult
_, res.Err = c.Delete(resourceURL(c, id), nil)
return res
}
// UpdateOptsBuilder represents a type that can be converted into a JSON-like
// map structure.
type UpdateOptsBuilder interface {
ToLBUpdateMap() (map[string]interface{}, error)
}
// UpdateOpts represents the options for updating an existing load balancer.
type UpdateOpts struct {
// Optional - new name of the load balancer.
Name string
// Optional - the new protocol you want your load balancer to have.
// See http://docs.rackspace.com/loadbalancers/api/v1.0/clb-devguide/content/protocols.html
// for a full list of supported protocols.
Protocol string
// Optional - see the HalfClosed field in CreateOpts for more information.
HalfClosed gophercloud.EnabledState
// Optional - see the Algorithm field in CreateOpts for more information.
Algorithm string
// Optional - see the Port field in CreateOpts for more information.
Port int
// Optional - see the Timeout field in CreateOpts for more information.
Timeout int
// Optional - see the HTTPSRedirect field in CreateOpts for more information.
HTTPSRedirect gophercloud.EnabledState
}
// ToLBUpdateMap casts an UpdateOpts struct to a map.
func (opts UpdateOpts) ToLBUpdateMap() (map[string]interface{}, error) {
lb := make(map[string]interface{})
if opts.Name != "" {
lb["name"] = opts.Name
}
if opts.Protocol != "" {
lb["protocol"] = opts.Protocol
}
if opts.HalfClosed != nil {
lb["halfClosed"] = opts.HalfClosed
}
if opts.Algorithm != "" {
lb["algorithm"] = opts.Algorithm
}
if opts.Port > 0 {
lb["port"] = opts.Port
}
if opts.Timeout > 0 {
lb["timeout"] = opts.Timeout
}
if opts.HTTPSRedirect != nil {
lb["httpsRedirect"] = &opts.HTTPSRedirect
}
return map[string]interface{}{"loadBalancer": lb}, nil
}
// Update is the operation responsible for asynchronously updating the
// attributes of a specific load balancer. Upon successful validation of the
// request, the service returns a 202 Accepted response, and the load balancer
// enters a PENDING_UPDATE state. A user can poll the load balancer with Get to
// wait for the changes to be applied. When this happens, the load balancer will
// return to an ACTIVE state.
func Update(c *gophercloud.ServiceClient, id int, opts UpdateOptsBuilder) UpdateResult {
var res UpdateResult
reqBody, err := opts.ToLBUpdateMap()
if err != nil {
res.Err = err
return res
}
_, res.Err = c.Put(resourceURL(c, id), reqBody, nil, nil)
return res
}
// ListProtocols is the operation responsible for returning a paginated
// collection of load balancer protocols.
func ListProtocols(client *gophercloud.ServiceClient) pagination.Pager {
url := protocolsURL(client)
return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page {
return ProtocolPage{pagination.SinglePageBase(r)}
})
}
// ListAlgorithms is the operation responsible for returning a paginated
// collection of load balancer algorithms.
func ListAlgorithms(client *gophercloud.ServiceClient) pagination.Pager {
url := algorithmsURL(client)
return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page {
return AlgorithmPage{pagination.SinglePageBase(r)}
})
}
// IsLoggingEnabled returns true if the load balancer has connection logging
// enabled and false if not.
func IsLoggingEnabled(client *gophercloud.ServiceClient, id int) (bool, error) {
var body interface{}
_, err := client.Get(loggingURL(client, id), &body, nil)
if err != nil {
return false, err
}
var resp struct {
CL struct {
Enabled bool `mapstructure:"enabled"`
} `mapstructure:"connectionLogging"`
}
err = mapstructure.Decode(body, &resp)
return resp.CL.Enabled, err
}
func toConnLoggingMap(state bool) map[string]map[string]bool {
return map[string]map[string]bool{
"connectionLogging": map[string]bool{"enabled": state},
}
}
// EnableLogging will enable connection logging for a specified load balancer.
func EnableLogging(client *gophercloud.ServiceClient, id int) gophercloud.ErrResult {
var res gophercloud.ErrResult
_, res.Err = client.Put(loggingURL(client, id), toConnLoggingMap(true), nil, nil)
return res
}
// DisableLogging will disable connection logging for a specified load balancer.
func DisableLogging(client *gophercloud.ServiceClient, id int) gophercloud.ErrResult {
var res gophercloud.ErrResult
_, res.Err = client.Put(loggingURL(client, id), toConnLoggingMap(false), nil, nil)
return res
}
// GetErrorPage will retrieve the current error page for the load balancer.
func GetErrorPage(client *gophercloud.ServiceClient, id int) ErrorPageResult {
var res ErrorPageResult
_, res.Err = client.Get(errorPageURL(client, id), &res.Body, nil)
return res
}
// SetErrorPage will set the HTML of the load balancer's error page to a
// specific value.
func SetErrorPage(client *gophercloud.ServiceClient, id int, html string) ErrorPageResult {
var res ErrorPageResult
type stringMap map[string]string
reqBody := map[string]stringMap{"errorpage": stringMap{"content": html}}
_, res.Err = client.Put(errorPageURL(client, id), reqBody, &res.Body, &gophercloud.RequestOpts{
OkCodes: []int{200},
})
return res
}
// DeleteErrorPage will delete the current error page for the load balancer.
func DeleteErrorPage(client *gophercloud.ServiceClient, id int) gophercloud.ErrResult {
var res gophercloud.ErrResult
_, res.Err = client.Delete(errorPageURL(client, id), &gophercloud.RequestOpts{
OkCodes: []int{200},
})
return res
}
// GetStats will retrieve detailed stats related to the load balancer's usage.
func GetStats(client *gophercloud.ServiceClient, id int) StatsResult {
var res StatsResult
_, res.Err = client.Get(statsURL(client, id), &res.Body, nil)
return res
}
// IsContentCached will check to see whether the specified load balancer caches
// content. When content caching is enabled, recently-accessed files are stored
// on the load balancer for easy retrieval by web clients. Content caching
// improves the performance of high traffic web sites by temporarily storing
// data that was recently accessed. While it's cached, requests for that data
// are served by the load balancer, which in turn reduces load off the back-end
// nodes. The result is improved response times for those requests and less
// load on the web server.
func IsContentCached(client *gophercloud.ServiceClient, id int) (bool, error) {
var body interface{}
_, err := client.Get(cacheURL(client, id), &body, nil)
if err != nil {
return false, err
}
var resp struct {
CC struct {
Enabled bool `mapstructure:"enabled"`
} `mapstructure:"contentCaching"`
}
err = mapstructure.Decode(body, &resp)
return resp.CC.Enabled, err
}
func toCachingMap(state bool) map[string]map[string]bool {
return map[string]map[string]bool{
"contentCaching": map[string]bool{"enabled": state},
}
}
// EnableCaching will enable content-caching for the specified load balancer.
func EnableCaching(client *gophercloud.ServiceClient, id int) gophercloud.ErrResult {
var res gophercloud.ErrResult
_, res.Err = client.Put(cacheURL(client, id), toCachingMap(true), nil, nil)
return res
}
// DisableCaching will disable content-caching for the specified load balancer.
func DisableCaching(client *gophercloud.ServiceClient, id int) gophercloud.ErrResult {
var res gophercloud.ErrResult
_, res.Err = client.Put(cacheURL(client, id), toCachingMap(false), nil, nil)
return res
}

View File

@@ -0,0 +1,420 @@
package lbs
import (
"reflect"
"time"
"github.com/mitchellh/mapstructure"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
"github.com/rackspace/gophercloud/rackspace/lb/v1/acl"
"github.com/rackspace/gophercloud/rackspace/lb/v1/nodes"
"github.com/rackspace/gophercloud/rackspace/lb/v1/sessions"
"github.com/rackspace/gophercloud/rackspace/lb/v1/throttle"
"github.com/rackspace/gophercloud/rackspace/lb/v1/vips"
)
// Protocol represents the network protocol which the load balancer accepts.
type Protocol struct {
// The name of the protocol, e.g. HTTP, LDAP, FTP, etc.
Name string
// The port number for the protocol.
Port int
}
// Algorithm defines how traffic should be directed between back-end nodes.
type Algorithm struct {
// The name of the algorithm, e.g RANDOM, ROUND_ROBIN, etc.
Name string
}
// Status represents the potential state of a load balancer resource.
type Status string
const (
// ACTIVE indicates that the LB is configured properly and ready to serve
// traffic to incoming requests via the configured virtual IPs.
ACTIVE Status = "ACTIVE"
// BUILD indicates that the LB is being provisioned for the first time and
// configuration is being applied to bring the service online. The service
// cannot yet serve incoming requests.
BUILD Status = "BUILD"
// PENDINGUPDATE indicates that the LB is online but configuration changes
// are being applied to update the service based on a previous request.
PENDINGUPDATE Status = "PENDING_UPDATE"
// PENDINGDELETE indicates that the LB is online but configuration changes
// are being applied to begin deletion of the service based on a previous
// request.
PENDINGDELETE Status = "PENDING_DELETE"
// SUSPENDED indicates that the LB has been taken offline and disabled.
SUSPENDED Status = "SUSPENDED"
// ERROR indicates that the system encountered an error when attempting to
// configure the load balancer.
ERROR Status = "ERROR"
// DELETED indicates that the LB has been deleted.
DELETED Status = "DELETED"
)
// Datetime represents the structure of a Created or Updated field.
type Datetime struct {
Time time.Time `mapstructure:"-"`
}
// LoadBalancer represents a load balancer API resource.
type LoadBalancer struct {
// Human-readable name for the load balancer.
Name string
// The unique ID for the load balancer.
ID int
// Represents the service protocol being load balanced. See Protocol type for
// a list of accepted values.
// See http://docs.rackspace.com/loadbalancers/api/v1.0/clb-devguide/content/protocols.html
// for a full list of supported protocols.
Protocol string
// Defines how traffic should be directed between back-end nodes. The default
// algorithm is RANDOM. See Algorithm type for a list of accepted values.
Algorithm string
// The current status of the load balancer.
Status Status
// The number of load balancer nodes.
NodeCount int `mapstructure:"nodeCount"`
// Slice of virtual IPs associated with this load balancer.
VIPs []vips.VIP `mapstructure:"virtualIps"`
// Datetime when the LB was created.
Created Datetime
// Datetime when the LB was created.
Updated Datetime
// Port number for the service you are load balancing.
Port int
// HalfClosed provides the ability for one end of the connection to
// terminate its output while still receiving data from the other end. This
// is only available on TCP/TCP_CLIENT_FIRST protocols.
HalfClosed bool
// Timeout represents the timeout value between a load balancer and its
// nodes. Defaults to 30 seconds with a maximum of 120 seconds.
Timeout int
// The cluster name.
Cluster Cluster
// Nodes shows all the back-end nodes which are associated with the load
// balancer. These are the devices which are delivered traffic.
Nodes []nodes.Node
// Current connection logging configuration.
ConnectionLogging ConnectionLogging
// SessionPersistence specifies whether multiple requests from clients are
// directed to the same node.
SessionPersistence sessions.SessionPersistence
// ConnectionThrottle specifies a limit on the number of connections per IP
// address to help mitigate malicious or abusive traffic to your applications.
ConnectionThrottle throttle.ConnectionThrottle
// The source public and private IP addresses.
SourceAddrs SourceAddrs `mapstructure:"sourceAddresses"`
// Represents the access rules for this particular load balancer. IP addresses
// or subnet ranges, depending on their type (ALLOW or DENY), can be permitted
// or blocked.
AccessList acl.AccessList
}
// SourceAddrs represents the source public and private IP addresses.
type SourceAddrs struct {
IPv4Public string `json:"ipv4Public" mapstructure:"ipv4Public"`
IPv4Private string `json:"ipv4Servicenet" mapstructure:"ipv4Servicenet"`
IPv6Public string `json:"ipv6Public" mapstructure:"ipv6Public"`
IPv6Private string `json:"ipv6Servicenet" mapstructure:"ipv6Servicenet"`
}
// ConnectionLogging - temp
type ConnectionLogging struct {
Enabled bool
}
// Cluster - temp
type Cluster struct {
Name string
}
// LBPage is the page returned by a pager when traversing over a collection of
// LBs.
type LBPage struct {
pagination.LinkedPageBase
}
// IsEmpty checks whether a NetworkPage struct is empty.
func (p LBPage) IsEmpty() (bool, error) {
is, err := ExtractLBs(p)
if err != nil {
return true, nil
}
return len(is) == 0, nil
}
// ExtractLBs accepts a Page struct, specifically a LBPage struct, and extracts
// the elements into a slice of LoadBalancer structs. In other words, a generic
// collection is mapped into a relevant slice.
func ExtractLBs(page pagination.Page) ([]LoadBalancer, error) {
var resp struct {
LBs []LoadBalancer `mapstructure:"loadBalancers" json:"loadBalancers"`
}
coll := page.(LBPage).Body
err := mapstructure.Decode(coll, &resp)
s := reflect.ValueOf(coll.(map[string]interface{})["loadBalancers"])
for i := 0; i < s.Len(); i++ {
val := (s.Index(i).Interface()).(map[string]interface{})
ts, err := extractTS(val, "created")
if err != nil {
return resp.LBs, err
}
resp.LBs[i].Created.Time = ts
ts, err = extractTS(val, "updated")
if err != nil {
return resp.LBs, err
}
resp.LBs[i].Updated.Time = ts
}
return resp.LBs, err
}
func extractTS(body map[string]interface{}, key string) (time.Time, error) {
val := body[key].(map[string]interface{})
return time.Parse(time.RFC3339, val["time"].(string))
}
type commonResult struct {
gophercloud.Result
}
// Extract interprets any commonResult as a LB, if possible.
func (r commonResult) Extract() (*LoadBalancer, error) {
if r.Err != nil {
return nil, r.Err
}
var response struct {
LB LoadBalancer `mapstructure:"loadBalancer"`
}
err := mapstructure.Decode(r.Body, &response)
json := r.Body.(map[string]interface{})
lb := json["loadBalancer"].(map[string]interface{})
ts, err := extractTS(lb, "created")
if err != nil {
return nil, err
}
response.LB.Created.Time = ts
ts, err = extractTS(lb, "updated")
if err != nil {
return nil, err
}
response.LB.Updated.Time = ts
return &response.LB, err
}
// CreateResult represents the result of a create operation.
type CreateResult struct {
commonResult
}
// DeleteResult represents the result of a delete operation.
type DeleteResult struct {
gophercloud.ErrResult
}
// UpdateResult represents the result of an update operation.
type UpdateResult struct {
gophercloud.ErrResult
}
// GetResult represents the result of a get operation.
type GetResult struct {
commonResult
}
// ProtocolPage is the page returned by a pager when traversing over a
// collection of LB protocols.
type ProtocolPage struct {
pagination.SinglePageBase
}
// IsEmpty checks whether a ProtocolPage struct is empty.
func (p ProtocolPage) IsEmpty() (bool, error) {
is, err := ExtractProtocols(p)
if err != nil {
return true, nil
}
return len(is) == 0, nil
}
// ExtractProtocols accepts a Page struct, specifically a ProtocolPage struct,
// and extracts the elements into a slice of Protocol structs. In other
// words, a generic collection is mapped into a relevant slice.
func ExtractProtocols(page pagination.Page) ([]Protocol, error) {
var resp struct {
Protocols []Protocol `mapstructure:"protocols" json:"protocols"`
}
err := mapstructure.Decode(page.(ProtocolPage).Body, &resp)
return resp.Protocols, err
}
// AlgorithmPage is the page returned by a pager when traversing over a
// collection of LB algorithms.
type AlgorithmPage struct {
pagination.SinglePageBase
}
// IsEmpty checks whether an AlgorithmPage struct is empty.
func (p AlgorithmPage) IsEmpty() (bool, error) {
is, err := ExtractAlgorithms(p)
if err != nil {
return true, nil
}
return len(is) == 0, nil
}
// ExtractAlgorithms accepts a Page struct, specifically an AlgorithmPage struct,
// and extracts the elements into a slice of Algorithm structs. In other
// words, a generic collection is mapped into a relevant slice.
func ExtractAlgorithms(page pagination.Page) ([]Algorithm, error) {
var resp struct {
Algorithms []Algorithm `mapstructure:"algorithms" json:"algorithms"`
}
err := mapstructure.Decode(page.(AlgorithmPage).Body, &resp)
return resp.Algorithms, err
}
// ErrorPage represents the HTML file that is shown to an end user who is
// attempting to access a load balancer node that is offline/unavailable.
//
// During provisioning, every load balancer is configured with a default error
// page that gets displayed when traffic is requested for an offline node.
//
// You can add a single custom error page with an HTTP-based protocol to a load
// balancer. Page updates override existing content. If a custom error page is
// deleted, or the load balancer is changed to a non-HTTP protocol, the default
// error page is restored.
type ErrorPage struct {
Content string
}
// ErrorPageResult represents the result of an error page operation -
// specifically getting or creating one.
type ErrorPageResult struct {
gophercloud.Result
}
// Extract interprets any commonResult as an ErrorPage, if possible.
func (r ErrorPageResult) Extract() (*ErrorPage, error) {
if r.Err != nil {
return nil, r.Err
}
var response struct {
ErrorPage ErrorPage `mapstructure:"errorpage"`
}
err := mapstructure.Decode(r.Body, &response)
return &response.ErrorPage, err
}
// Stats represents all the key information about a load balancer's usage.
type Stats struct {
// The number of connections closed by this load balancer because its
// ConnectTimeout interval was exceeded.
ConnectTimeout int `mapstructure:"connectTimeOut"`
// The number of transaction or protocol errors for this load balancer.
ConnectError int
// Number of connection failures for this load balancer.
ConnectFailure int
// Number of connections closed by this load balancer because its Timeout
// interval was exceeded.
DataTimedOut int
// Number of connections closed by this load balancer because the
// 'keepalive_timeout' interval was exceeded.
KeepAliveTimedOut int
// The maximum number of simultaneous TCP connections this load balancer has
// processed at any one time.
MaxConnections int `mapstructure:"maxConn"`
// Number of simultaneous connections active at the time of the request.
CurrentConnections int `mapstructure:"currentConn"`
// Number of SSL connections closed by this load balancer because the
// ConnectTimeout interval was exceeded.
SSLConnectTimeout int `mapstructure:"connectTimeOutSsl"`
// Number of SSL transaction or protocol erros in this load balancer.
SSLConnectError int `mapstructure:"connectErrorSsl"`
// Number of SSL connection failures in this load balancer.
SSLConnectFailure int `mapstructure:"connectFailureSsl"`
// Number of SSL connections closed by this load balancer because the
// Timeout interval was exceeded.
SSLDataTimedOut int `mapstructure:"dataTimedOutSsl"`
// Number of SSL connections closed by this load balancer because the
// 'keepalive_timeout' interval was exceeded.
SSLKeepAliveTimedOut int `mapstructure:"keepAliveTimedOutSsl"`
// Maximum number of simultaneous SSL connections this load balancer has
// processed at any one time.
SSLMaxConnections int `mapstructure:"maxConnSsl"`
// Number of simultaneous SSL connections active at the time of the request.
SSLCurrentConnections int `mapstructure:"currentConnSsl"`
}
// StatsResult represents the result of a Stats operation.
type StatsResult struct {
gophercloud.Result
}
// Extract interprets any commonResult as a Stats struct, if possible.
func (r StatsResult) Extract() (*Stats, error) {
if r.Err != nil {
return nil, r.Err
}
res := &Stats{}
err := mapstructure.Decode(r.Body, res)
return res, err
}

View File

@@ -0,0 +1,49 @@
package lbs
import (
"strconv"
"github.com/rackspace/gophercloud"
)
const (
path = "loadbalancers"
protocolsPath = "protocols"
algorithmsPath = "algorithms"
logPath = "connectionlogging"
epPath = "errorpage"
stPath = "stats"
cachePath = "contentcaching"
)
func resourceURL(c *gophercloud.ServiceClient, id int) string {
return c.ServiceURL(path, strconv.Itoa(id))
}
func rootURL(c *gophercloud.ServiceClient) string {
return c.ServiceURL(path)
}
func protocolsURL(c *gophercloud.ServiceClient) string {
return c.ServiceURL(path, protocolsPath)
}
func algorithmsURL(c *gophercloud.ServiceClient) string {
return c.ServiceURL(path, algorithmsPath)
}
func loggingURL(c *gophercloud.ServiceClient, id int) string {
return c.ServiceURL(path, strconv.Itoa(id), logPath)
}
func errorPageURL(c *gophercloud.ServiceClient, id int) string {
return c.ServiceURL(path, strconv.Itoa(id), epPath)
}
func statsURL(c *gophercloud.ServiceClient, id int) string {
return c.ServiceURL(path, strconv.Itoa(id), stPath)
}
func cacheURL(c *gophercloud.ServiceClient, id int) string {
return c.ServiceURL(path, strconv.Itoa(id), cachePath)
}

View File

@@ -0,0 +1,21 @@
/*
Package monitors provides information and interaction with the Health Monitor
API resource for the Rackspace Cloud Load Balancer service.
The load balancing service includes a health monitoring resource that
periodically checks your back-end nodes to ensure they are responding correctly.
If a node does not respond, it is removed from rotation until the health monitor
determines that the node is functional. In addition to being performed
periodically, a health check also executes against every new node that is
added, to ensure that the node is operating properly before allowing it to
service traffic. Only one health monitor is allowed to be enabled on a load
balancer at a time.
As part of a good strategy for monitoring connections, secondary nodes should
also be created which provide failover for effectively routing traffic in case
the primary node fails. This is an additional feature that ensures that you
remain up in case your primary node fails.
There are three types of health monitor: CONNECT, HTTP and HTTPS.
*/
package monitors

View File

@@ -0,0 +1,87 @@
package monitors
import (
"fmt"
"net/http"
"strconv"
"testing"
th "github.com/rackspace/gophercloud/testhelper"
fake "github.com/rackspace/gophercloud/testhelper/client"
)
func _rootURL(lbID int) string {
return "/loadbalancers/" + strconv.Itoa(lbID) + "/healthmonitor"
}
func mockGetResponse(t *testing.T, lbID int) {
th.Mux.HandleFunc(_rootURL(lbID), func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "GET")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, `
{
"healthMonitor": {
"type": "CONNECT",
"delay": 10,
"timeout": 10,
"attemptsBeforeDeactivation": 3
}
}
`)
})
}
func mockUpdateConnectResponse(t *testing.T, lbID int) {
th.Mux.HandleFunc(_rootURL(lbID), func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "PUT")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
th.TestJSONRequest(t, r, `
{
"healthMonitor": {
"type": "CONNECT",
"delay": 10,
"timeout": 10,
"attemptsBeforeDeactivation": 3
}
}
`)
w.WriteHeader(http.StatusAccepted)
})
}
func mockUpdateHTTPResponse(t *testing.T, lbID int) {
th.Mux.HandleFunc(_rootURL(lbID), func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "PUT")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
th.TestJSONRequest(t, r, `
{
"healthMonitor": {
"attemptsBeforeDeactivation": 3,
"bodyRegex": "{regex}",
"delay": 10,
"path": "/foo",
"statusRegex": "200",
"timeout": 10,
"type": "HTTPS"
}
}
`)
w.WriteHeader(http.StatusAccepted)
})
}
func mockDeleteResponse(t *testing.T, lbID int) {
th.Mux.HandleFunc(_rootURL(lbID), func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "DELETE")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
w.WriteHeader(http.StatusAccepted)
})
}

View File

@@ -0,0 +1,160 @@
package monitors
import (
"errors"
"github.com/rackspace/gophercloud"
)
var (
errAttemptLimit = errors.New("AttemptLimit field must be an int greater than 1 and less than 10")
errDelay = errors.New("Delay field must be an int greater than 1 and less than 10")
errTimeout = errors.New("Timeout field must be an int greater than 1 and less than 10")
)
// UpdateOptsBuilder is the interface options structs have to satisfy in order
// to be used in the main Update operation in this package.
type UpdateOptsBuilder interface {
ToMonitorUpdateMap() (map[string]interface{}, error)
}
// UpdateConnectMonitorOpts represents the options needed to update a CONNECT
// monitor.
type UpdateConnectMonitorOpts struct {
// Required - number of permissible monitor failures before removing a node
// from rotation. Must be a number between 1 and 10.
AttemptLimit int
// Required - the minimum number of seconds to wait before executing the
// health monitor. Must be a number between 1 and 3600.
Delay int
// Required - maximum number of seconds to wait for a connection to be
// established before timing out. Must be a number between 1 and 300.
Timeout int
}
// ToMonitorUpdateMap produces a map for updating CONNECT monitors.
func (opts UpdateConnectMonitorOpts) ToMonitorUpdateMap() (map[string]interface{}, error) {
type m map[string]interface{}
if !gophercloud.IntWithinRange(opts.AttemptLimit, 1, 10) {
return m{}, errAttemptLimit
}
if !gophercloud.IntWithinRange(opts.Delay, 1, 3600) {
return m{}, errDelay
}
if !gophercloud.IntWithinRange(opts.Timeout, 1, 300) {
return m{}, errTimeout
}
return m{"healthMonitor": m{
"attemptsBeforeDeactivation": opts.AttemptLimit,
"delay": opts.Delay,
"timeout": opts.Timeout,
"type": CONNECT,
}}, nil
}
// UpdateHTTPMonitorOpts represents the options needed to update a HTTP monitor.
type UpdateHTTPMonitorOpts struct {
// Required - number of permissible monitor failures before removing a node
// from rotation. Must be a number between 1 and 10.
AttemptLimit int `mapstructure:"attemptsBeforeDeactivation"`
// Required - the minimum number of seconds to wait before executing the
// health monitor. Must be a number between 1 and 3600.
Delay int
// Required - maximum number of seconds to wait for a connection to be
// established before timing out. Must be a number between 1 and 300.
Timeout int
// Required - a regular expression that will be used to evaluate the contents
// of the body of the response.
BodyRegex string
// Required - the HTTP path that will be used in the sample request.
Path string
// Required - a regular expression that will be used to evaluate the HTTP
// status code returned in the response.
StatusRegex string
// Optional - the name of a host for which the health monitors will check.
HostHeader string
// Required - either HTTP or HTTPS
Type Type
}
// ToMonitorUpdateMap produces a map for updating HTTP(S) monitors.
func (opts UpdateHTTPMonitorOpts) ToMonitorUpdateMap() (map[string]interface{}, error) {
type m map[string]interface{}
if !gophercloud.IntWithinRange(opts.AttemptLimit, 1, 10) {
return m{}, errAttemptLimit
}
if !gophercloud.IntWithinRange(opts.Delay, 1, 3600) {
return m{}, errDelay
}
if !gophercloud.IntWithinRange(opts.Timeout, 1, 300) {
return m{}, errTimeout
}
if opts.Type != HTTP && opts.Type != HTTPS {
return m{}, errors.New("Type must either by HTTP or HTTPS")
}
if opts.BodyRegex == "" {
return m{}, errors.New("BodyRegex is a required field")
}
if opts.Path == "" {
return m{}, errors.New("Path is a required field")
}
if opts.StatusRegex == "" {
return m{}, errors.New("StatusRegex is a required field")
}
json := m{
"attemptsBeforeDeactivation": opts.AttemptLimit,
"delay": opts.Delay,
"timeout": opts.Timeout,
"type": opts.Type,
"bodyRegex": opts.BodyRegex,
"path": opts.Path,
"statusRegex": opts.StatusRegex,
}
if opts.HostHeader != "" {
json["hostHeader"] = opts.HostHeader
}
return m{"healthMonitor": json}, nil
}
// Update is the operation responsible for updating a health monitor.
func Update(c *gophercloud.ServiceClient, id int, opts UpdateOptsBuilder) UpdateResult {
var res UpdateResult
reqBody, err := opts.ToMonitorUpdateMap()
if err != nil {
res.Err = err
return res
}
_, res.Err = c.Put(rootURL(c, id), reqBody, nil, nil)
return res
}
// Get is the operation responsible for showing details of a health monitor.
func Get(c *gophercloud.ServiceClient, id int) GetResult {
var res GetResult
_, res.Err = c.Get(rootURL(c, id), &res.Body, nil)
return res
}
// Delete is the operation responsible for deleting a health monitor.
func Delete(c *gophercloud.ServiceClient, id int) DeleteResult {
var res DeleteResult
_, res.Err = c.Delete(rootURL(c, id), nil)
return res
}

View File

@@ -0,0 +1,90 @@
package monitors
import (
"github.com/mitchellh/mapstructure"
"github.com/rackspace/gophercloud"
)
// Type represents the type of Monitor.
type Type string
// Useful constants.
const (
CONNECT Type = "CONNECT"
HTTP Type = "HTTP"
HTTPS Type = "HTTPS"
)
// Monitor represents a health monitor API resource. A monitor comes in three
// forms: CONNECT, HTTP or HTTPS.
//
// A CONNECT monitor establishes a basic connection to each node on its defined
// port to ensure that the service is listening properly. The connect monitor
// is the most basic type of health check and does no post-processing or
// protocol-specific health checks.
//
// HTTP and HTTPS health monitors are generally considered more intelligent and
// powerful than CONNECT. It is capable of processing an HTTP or HTTPS response
// to determine the condition of a node. It supports the same basic properties
// as CONNECT and includes additional attributes that are used to evaluate the
// HTTP response.
type Monitor struct {
// Number of permissible monitor failures before removing a node from
// rotation.
AttemptLimit int `mapstructure:"attemptsBeforeDeactivation"`
// The minimum number of seconds to wait before executing the health monitor.
Delay int
// Maximum number of seconds to wait for a connection to be established
// before timing out.
Timeout int
// Type of the health monitor.
Type Type
// A regular expression that will be used to evaluate the contents of the
// body of the response.
BodyRegex string
// The name of a host for which the health monitors will check.
HostHeader string
// The HTTP path that will be used in the sample request.
Path string
// A regular expression that will be used to evaluate the HTTP status code
// returned in the response.
StatusRegex string
}
// UpdateResult represents the result of an Update operation.
type UpdateResult struct {
gophercloud.ErrResult
}
// GetResult represents the result of a Get operation.
type GetResult struct {
gophercloud.Result
}
// DeleteResult represents the result of an Delete operation.
type DeleteResult struct {
gophercloud.ErrResult
}
// Extract interprets any GetResult as a Monitor.
func (r GetResult) Extract() (*Monitor, error) {
if r.Err != nil {
return nil, r.Err
}
var response struct {
M Monitor `mapstructure:"healthMonitor"`
}
err := mapstructure.Decode(r.Body, &response)
return &response.M, err
}

View File

@@ -0,0 +1,16 @@
package monitors
import (
"strconv"
"github.com/rackspace/gophercloud"
)
const (
path = "loadbalancers"
monitorPath = "healthmonitor"
)
func rootURL(c *gophercloud.ServiceClient, lbID int) string {
return c.ServiceURL(path, strconv.Itoa(lbID), monitorPath)
}

View File

@@ -0,0 +1,35 @@
/*
Package nodes provides information and interaction with the Node API resource
for the Rackspace Cloud Load Balancer service.
Nodes are responsible for servicing the requests received through the load
balancer's virtual IP. A node is usually a virtual machine. By default, the
load balancer employs a basic health check that ensures the node is listening
on its defined port. The node is checked at the time of addition and at regular
intervals as defined by the load balancer's health check configuration. If a
back-end node is not listening on its port, or does not meet the conditions of
the defined check, then connections will not be forwarded to the node, and its
status is changed to OFFLINE. Only nodes that are in an ONLINE status receive
and can service traffic from the load balancer.
All nodes have an associated status that indicates whether the node is
ONLINE, OFFLINE, or DRAINING. Only nodes that are in ONLINE status can receive
and service traffic from the load balancer. The OFFLINE status represents a
node that cannot accept or service traffic. A node in DRAINING status
represents a node that stops the traffic manager from sending any additional
new connections to the node, but honors established sessions. If the traffic
manager receives a request and session persistence requires that the node is
used, the traffic manager uses it. The status is determined by the passive or
active health monitors.
If the WEIGHTED_ROUND_ROBIN load balancer algorithm mode is selected, then the
caller should assign the relevant weights to the node as part of the weight
attribute of the node element. When the algorithm of the load balancer is
changed to WEIGHTED_ROUND_ROBIN and the nodes do not already have an assigned
weight, the service automatically sets the weight to 1 for all nodes.
One or more secondary nodes can be added to a specified load balancer so that
if all the primary nodes fail, traffic can be redirected to secondary nodes.
The type attribute allows configuring the node as either PRIMARY or SECONDARY.
*/
package nodes

View File

@@ -0,0 +1,243 @@
package nodes
import (
"fmt"
"net/http"
"strconv"
"testing"
th "github.com/rackspace/gophercloud/testhelper"
fake "github.com/rackspace/gophercloud/testhelper/client"
)
func _rootURL(lbID int) string {
return "/loadbalancers/" + strconv.Itoa(lbID) + "/nodes"
}
func _nodeURL(lbID, nodeID int) string {
return _rootURL(lbID) + "/" + strconv.Itoa(nodeID)
}
func mockListResponse(t *testing.T, lbID int) {
th.Mux.HandleFunc(_rootURL(lbID), func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "GET")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, `
{
"nodes": [
{
"id": 410,
"address": "10.1.1.1",
"port": 80,
"condition": "ENABLED",
"status": "ONLINE",
"weight": 3,
"type": "PRIMARY"
},
{
"id": 411,
"address": "10.1.1.2",
"port": 80,
"condition": "ENABLED",
"status": "ONLINE",
"weight": 8,
"type": "SECONDARY"
}
]
}
`)
})
}
func mockCreateResponse(t *testing.T, lbID int) {
th.Mux.HandleFunc(_rootURL(lbID), func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "POST")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
th.TestJSONRequest(t, r, `
{
"nodes": [
{
"address": "10.2.2.3",
"port": 80,
"condition": "ENABLED",
"type": "PRIMARY"
},
{
"address": "10.2.2.4",
"port": 81,
"condition": "ENABLED",
"type": "SECONDARY"
}
]
}
`)
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusAccepted)
fmt.Fprintf(w, `
{
"nodes": [
{
"address": "10.2.2.3",
"id": 185,
"port": 80,
"status": "ONLINE",
"condition": "ENABLED",
"weight": 1,
"type": "PRIMARY"
},
{
"address": "10.2.2.4",
"id": 186,
"port": 81,
"status": "ONLINE",
"condition": "ENABLED",
"weight": 1,
"type": "SECONDARY"
}
]
}
`)
})
}
func 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")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
r.ParseForm()
for k, v := range ids {
fids := r.Form["id"]
th.AssertEquals(t, strconv.Itoa(v), fids[k])
}
w.WriteHeader(http.StatusAccepted)
})
}
func mockDeleteResponse(t *testing.T, lbID, nodeID int) {
th.Mux.HandleFunc(_nodeURL(lbID, nodeID), func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "DELETE")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
w.WriteHeader(http.StatusAccepted)
})
}
func mockGetResponse(t *testing.T, lbID, nodeID int) {
th.Mux.HandleFunc(_nodeURL(lbID, nodeID), func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "GET")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, `
{
"node": {
"id": 410,
"address": "10.1.1.1",
"port": 80,
"condition": "ENABLED",
"status": "ONLINE",
"weight": 12,
"type": "PRIMARY"
}
}
`)
})
}
func mockUpdateResponse(t *testing.T, lbID, nodeID int) {
th.Mux.HandleFunc(_nodeURL(lbID, nodeID), func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "PUT")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
th.TestJSONRequest(t, r, `
{
"node": {
"condition": "DRAINING",
"weight": 10,
"type": "SECONDARY"
}
}
`)
w.WriteHeader(http.StatusAccepted)
})
}
func mockListEventsResponse(t *testing.T, lbID int) {
th.Mux.HandleFunc(_rootURL(lbID)+"/events", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "GET")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, `
{
"nodeServiceEvents": [
{
"detailedMessage": "Node is ok",
"nodeId": 373,
"id": 7,
"type": "UPDATE_NODE",
"description": "Node '373' status changed to 'ONLINE' for load balancer '323'",
"category": "UPDATE",
"severity": "INFO",
"relativeUri": "/406271/loadbalancers/323/nodes/373/events",
"accountId": 406271,
"loadbalancerId": 323,
"title": "Node Status Updated",
"author": "Rackspace Cloud",
"created": "10-30-2012 10:18:23"
}
]
}
`)
})
}

View File

@@ -0,0 +1,286 @@
package nodes
import (
"errors"
"fmt"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
)
// List is the operation responsible for returning a paginated collection of
// load balancer nodes. It requires the node ID, its parent load balancer ID,
// and optional limit integer (passed in either as a pointer or a nil poitner).
func List(client *gophercloud.ServiceClient, loadBalancerID int, limit *int) pagination.Pager {
url := rootURL(client, loadBalancerID)
if limit != nil {
url += fmt.Sprintf("?limit=%d", limit)
}
return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page {
return NodePage{pagination.SinglePageBase(r)}
})
}
// CreateOptsBuilder is the interface responsible for generating the JSON
// for a Create operation.
type CreateOptsBuilder interface {
ToNodeCreateMap() (map[string]interface{}, error)
}
// CreateOpts is a slice of CreateOpt structs, that allow the user to create
// multiple nodes in a single operation (one node per CreateOpt).
type CreateOpts []CreateOpt
// CreateOpt represents the options to create a single node.
type CreateOpt struct {
// Required - the IP address or CIDR for this back-end node. It can either be
// a private IP (ServiceNet) or a public IP.
Address string
// Optional - the port on which traffic is sent and received.
Port int
// Optional - the condition of the node. See the consts in Results.go.
Condition Condition
// Optional - the type of the node. See the consts in Results.go.
Type Type
// Optional - a pointer to an integer between 0 and 100.
Weight *int
}
func validateWeight(weight *int) error {
if weight != nil && (*weight > 100 || *weight < 0) {
return errors.New("Weight must be a valid int between 0 and 100")
}
return nil
}
// ToNodeCreateMap converts a slice of options into a map that can be used for
// the JSON.
func (opts CreateOpts) ToNodeCreateMap() (map[string]interface{}, error) {
type nodeMap map[string]interface{}
nodes := []nodeMap{}
for k, v := range opts {
if v.Address == "" {
return nodeMap{}, fmt.Errorf("ID is a required attribute, none provided for %d CreateOpt element", k)
}
if weightErr := validateWeight(v.Weight); weightErr != nil {
return nodeMap{}, weightErr
}
node := make(map[string]interface{})
node["address"] = v.Address
if v.Port > 0 {
node["port"] = v.Port
}
if v.Condition != "" {
node["condition"] = v.Condition
}
if v.Type != "" {
node["type"] = v.Type
}
if v.Weight != nil {
node["weight"] = &v.Weight
}
nodes = append(nodes, node)
}
return nodeMap{"nodes": nodes}, nil
}
// Create is the operation responsible for creating a new node on a load
// balancer. Since every load balancer exists in both ServiceNet and the public
// Internet, both private and public IP addresses can be used for nodes.
//
// If nodes need time to boot up services before they become operational, you
// can temporarily prevent traffic from being sent to that node by setting the
// Condition field to DRAINING. Health checks will still be performed; but once
// your node is ready, you can update its condition to ENABLED and have it
// handle traffic.
func Create(client *gophercloud.ServiceClient, loadBalancerID int, opts CreateOptsBuilder) CreateResult {
var res CreateResult
reqBody, err := opts.ToNodeCreateMap()
if err != nil {
res.Err = err
return res
}
resp, err := client.Post(rootURL(client, loadBalancerID), reqBody, &res.Body, nil)
if err != nil {
res.Err = err
return res
}
pr := pagination.PageResultFromParsed(resp, res.Body)
return CreateResult{pagination.SinglePageBase(pr)}
}
// BulkDelete is the operation responsible for batch deleting multiple nodes in
// a single operation. It accepts a slice of integer IDs and will remove them
// from the load balancer. The maximum limit is 10 node removals at once.
func BulkDelete(c *gophercloud.ServiceClient, loadBalancerID int, nodeIDs []int) DeleteResult {
var res DeleteResult
if len(nodeIDs) > 10 || len(nodeIDs) == 0 {
res.Err = errors.New("You must provide a minimum of 1 and a maximum of 10 node IDs")
return res
}
url := rootURL(c, loadBalancerID)
url += gophercloud.IDSliceToQueryString("id", nodeIDs)
_, res.Err = c.Delete(url, nil)
return res
}
// Get is the operation responsible for showing details for a single node.
func Get(c *gophercloud.ServiceClient, lbID, nodeID int) GetResult {
var res GetResult
_, res.Err = c.Get(resourceURL(c, lbID, nodeID), &res.Body, nil)
return res
}
// UpdateOptsBuilder represents a type that can be converted into a JSON-like
// map structure.
type UpdateOptsBuilder interface {
ToNodeUpdateMap() (map[string]interface{}, error)
}
// UpdateOpts represent the options for updating an existing node.
type UpdateOpts struct {
// Optional - the condition of the node. See the consts in Results.go.
Condition Condition
// Optional - the type of the node. See the consts in Results.go.
Type Type
// Optional - a pointer to an integer between 0 and 100.
Weight *int
}
// ToNodeUpdateMap converts an options struct into a JSON-like map.
func (opts UpdateOpts) ToNodeUpdateMap() (map[string]interface{}, error) {
node := make(map[string]interface{})
if opts.Condition != "" {
node["condition"] = opts.Condition
}
if opts.Weight != nil {
if weightErr := validateWeight(opts.Weight); weightErr != nil {
return node, weightErr
}
node["weight"] = &opts.Weight
}
if opts.Type != "" {
node["type"] = opts.Type
}
return map[string]interface{}{"node": node}, nil
}
// Update is the operation responsible for updating an existing node. A node's
// IP, port, and status are immutable attributes and cannot be modified.
func Update(c *gophercloud.ServiceClient, lbID, nodeID int, opts UpdateOptsBuilder) UpdateResult {
var res UpdateResult
reqBody, err := opts.ToNodeUpdateMap()
if err != nil {
res.Err = err
return res
}
_, res.Err = c.Put(resourceURL(c, lbID, nodeID), reqBody, nil, nil)
return res
}
// Delete is the operation responsible for permanently deleting a node.
func Delete(c *gophercloud.ServiceClient, lbID, nodeID int) DeleteResult {
var res DeleteResult
_, res.Err = c.Delete(resourceURL(c, lbID, nodeID), nil)
return res
}
// ListEventsOptsBuilder allows extensions to add additional parameters to the
// List request.
type ListEventsOptsBuilder interface {
ToEventsListQuery() (string, error)
}
// ListEventsOpts allows the filtering and sorting of paginated collections through
// the API.
type ListEventsOpts struct {
Marker string `q:"marker"`
Limit int `q:"limit"`
}
// ToEventsListQuery formats a ListOpts into a query string.
func (opts ListEventsOpts) ToEventsListQuery() (string, error) {
q, err := gophercloud.BuildQueryString(opts)
if err != nil {
return "", err
}
return q.String(), nil
}
// ListEvents is the operation responsible for listing all the events
// associated with the activity between the node and the load balancer. The
// events report errors found with the node. The detailedMessage provides the
// detailed reason for the error.
func ListEvents(client *gophercloud.ServiceClient, loadBalancerID int, opts ListEventsOptsBuilder) pagination.Pager {
url := eventsURL(client, loadBalancerID)
if opts != nil {
query, err := opts.ToEventsListQuery()
if err != nil {
return pagination.Pager{Err: err}
}
url += query
}
return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page {
return NodeEventPage{pagination.SinglePageBase(r)}
})
}
// GetByIPPort locates a load balancer node by IP and port.
func GetByIPPort(
client *gophercloud.ServiceClient,
loadBalancerID int,
address string,
port int,
) (*Node, error) {
// nil until found
var found *Node
List(client, loadBalancerID, nil).EachPage(func(page pagination.Page) (bool, error) {
lbNodes, err := ExtractNodes(page)
if err != nil {
return false, err
}
for _, trialNode := range lbNodes {
if trialNode.Address == address && trialNode.Port == port {
found = &trialNode
return false, nil
}
}
return true, nil
})
if found == nil {
return nil, errors.New("Unable to get node by IP and Port")
}
return found, nil
}

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