Merge pull request #23131 from AlbertoPeon/up_gophercloud

Auto commit by PR queue bot
This commit is contained in:
k8s-merge-robot 2016-03-23 05:36:21 -07:00
commit 050980b472
113 changed files with 6327 additions and 280 deletions

4
Godeps/Godeps.json generated
View File

@ -908,8 +908,8 @@
},
{
"ImportPath": "github.com/rackspace/gophercloud",
"Comment": "v1.0.0-665-gf928634",
"Rev": "f92863476c034f851073599c09d90cd61ee95b3d"
"Comment": "v1.0.0-842-g8992d74",
"Rev": "8992d7483a06748dea706e4716d042a4a9e73918"
},
{
"ImportPath": "github.com/russross/blackfriday",

View File

@ -1,17 +1,20 @@
language: go
sudo: false
install:
- go get golang.org/x/crypto/ssh
- go get -v -tags 'fixtures acceptance' ./...
go:
- 1.1
- 1.2
- 1.3
- 1.4
- 1.5
- tip
script: script/cibuild
after_success:
- go get golang.org/x/tools/cmd/cover
env:
- COVERALLS_TOKEN=2k7PTU3xa474Hymwgdj6XjqenNfGTNkO8
before_install:
- go get github.com/axw/gocov/gocov
- go get github.com/mattn/goveralls
- export PATH=$PATH:$HOME/gopath/bin/
- goveralls 2k7PTU3xa474Hymwgdj6XjqenNfGTNkO8
sudo: false
- go get github.com/pierrre/gotestcover
- if ! go get github.com/golang/tools/cmd/cover; then go get golang.org/x/tools/cmd/cover; fi
script:
- $HOME/gopath/bin/gotestcover -v -tags=fixtures -coverprofile=cover.out ./...
after_success:
- $HOME/gopath/bin/goveralls -service=travis-ci -coverprofile=cover.out

View File

@ -11,7 +11,8 @@ As a contributor you will need to setup your workspace in a slightly different
way than just downloading it. Here are the basic installation instructions:
1. Configure your `$GOPATH` and run `go get` as described in the main
[README](/README.md#how-to-install).
[README](/README.md#how-to-install) but add `-tags "fixtures acceptance"` to
get dependencies for unit and acceptance tests.
2. Move into the directory that houses your local repository:
@ -158,25 +159,25 @@ deleted after the test suite finishes.
To run all tests:
```bash
go test ./...
go test -tags fixtures ./...
```
To run all tests with verbose output:
```bash
go test -v ./...
go test -v -tags fixtures ./...
```
To run tests that match certain [build tags]():
```bash
go test -tags "foo bar" ./...
go test -tags "fixtures foo bar" ./...
```
To run tests for a particular sub-package:
```bash
cd ./path/to/package && go test .
cd ./path/to/package && go test -tags fixtures .
```
## Basic style guide

View File

@ -1,5 +1,5 @@
# Gophercloud: the OpenStack SDK for Go
[![Build Status](https://travis-ci.org/rackspace/gophercloud.svg?branch=master)](https://travis-ci.org/rackspace/gophercloud)
# Gophercloud: an OpenStack SDK for Go
[![Build Status](https://travis-ci.org/rackspace/gophercloud.svg?branch=master)](https://travis-ci.org/rackspace/gophercloud) [![Coverage Status](https://coveralls.io/repos/rackspace/gophercloud/badge.png)](https://coveralls.io/r/rackspace/gophercloud)
Gophercloud is a flexible SDK that allows you to consume and work with OpenStack
clouds in a simple and idiomatic way using golang. Many services are supported,

View File

@ -0,0 +1,3 @@
// The v1 package contains acceptance tests for the Openstack Cinder V1 service.
package v1

View File

@ -0,0 +1,70 @@
// +build acceptance db
package v1
import (
"os"
"testing"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/openstack"
"github.com/rackspace/gophercloud/openstack/db/v1/instances"
th "github.com/rackspace/gophercloud/testhelper"
)
func newClient(t *testing.T) *gophercloud.ServiceClient {
ao, err := openstack.AuthOptionsFromEnv()
th.AssertNoErr(t, err)
client, err := openstack.AuthenticatedClient(ao)
th.AssertNoErr(t, err)
c, err := openstack.NewDBV1(client, gophercloud.EndpointOpts{
Region: os.Getenv("OS_REGION_NAME"),
})
th.AssertNoErr(t, err)
return c
}
type context struct {
test *testing.T
client *gophercloud.ServiceClient
instanceID string
DBIDs []string
users []string
}
func newContext(t *testing.T) context {
return context{
test: t,
client: newClient(t),
}
}
func (c context) Logf(msg string, args ...interface{}) {
if len(args) > 0 {
c.test.Logf(msg, args...)
} else {
c.test.Log(msg)
}
}
func (c context) AssertNoErr(err error) {
th.AssertNoErr(c.test, err)
}
func (c context) WaitUntilActive(id string) {
err := gophercloud.WaitFor(60, func() (bool, error) {
inst, err := instances.Get(c.client, id).Extract()
if err != nil {
return false, err
}
if inst.Status == "ACTIVE" {
return true, nil
}
return false, nil
})
c.AssertNoErr(err)
}

View File

@ -0,0 +1 @@
package v1

View File

@ -0,0 +1,73 @@
// +build acceptance db rackspace
package v1
import (
"testing"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/acceptance/tools"
"github.com/rackspace/gophercloud/rackspace"
"github.com/rackspace/gophercloud/rackspace/db/v1/instances"
th "github.com/rackspace/gophercloud/testhelper"
)
func newClient(t *testing.T) *gophercloud.ServiceClient {
opts, err := rackspace.AuthOptionsFromEnv()
th.AssertNoErr(t, err)
opts = tools.OnlyRS(opts)
client, err := rackspace.AuthenticatedClient(opts)
th.AssertNoErr(t, err)
c, err := rackspace.NewDBV1(client, gophercloud.EndpointOpts{
Region: "IAD",
})
th.AssertNoErr(t, err)
return c
}
type context struct {
test *testing.T
client *gophercloud.ServiceClient
instanceID string
DBIDs []string
replicaID string
backupID string
configGroupID string
users []string
}
func newContext(t *testing.T) context {
return context{
test: t,
client: newClient(t),
}
}
func (c context) Logf(msg string, args ...interface{}) {
if len(args) > 0 {
c.test.Logf(msg, args...)
} else {
c.test.Log(msg)
}
}
func (c context) AssertNoErr(err error) {
th.AssertNoErr(c.test, err)
}
func (c context) WaitUntilActive(id string) {
err := gophercloud.WaitFor(60, func() (bool, error) {
inst, err := instances.Get(c.client, id).Extract()
if err != nil {
return false, err
}
if inst.Status == "ACTIVE" {
return true, nil
}
return false, nil
})
c.AssertNoErr(err)
}

View File

@ -0,0 +1 @@
package v1

View File

@ -2,6 +2,7 @@ package apiversions
import (
"strings"
"net/url"
"github.com/rackspace/gophercloud"
)
@ -11,5 +12,7 @@ func getURL(c *gophercloud.ServiceClient, version string) string {
}
func listURL(c *gophercloud.ServiceClient) string {
return c.ServiceURL("")
u, _ := url.Parse(c.ServiceURL(""))
u.Path = "/"
return u.String()
}

View File

@ -3,6 +3,7 @@ package openstack
import (
"fmt"
"net/url"
"strings"
"github.com/rackspace/gophercloud"
tokens2 "github.com/rackspace/gophercloud/openstack/identity/v2/tokens"
@ -64,8 +65,8 @@ func AuthenticatedClient(options gophercloud.AuthOptions) (*gophercloud.Provider
// 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/"},
&utils.Version{ID: v30, Priority: 30, Suffix: "/v3/"},
{ID: v20, Priority: 20, Suffix: "/v2.0/"},
{ID: v30, Priority: 30, Suffix: "/v3/"},
}
chosen, endpoint, err := utils.ChooseVersion(client, versions)
@ -110,7 +111,7 @@ func v2auth(client *gophercloud.ProviderClient, endpoint string, options gopherc
if options.AllowReauth {
client.ReauthFunc = func() error {
client.TokenID = ""
return AuthenticateV2(client, options)
return v2auth(client, endpoint, options)
}
}
client.TokenID = token.ID
@ -167,7 +168,8 @@ func v3auth(client *gophercloud.ProviderClient, endpoint string, options gopherc
if options.AllowReauth {
client.ReauthFunc = func() error {
return AuthenticateV3(client, options)
client.TokenID = ""
return v3auth(client, endpoint, options)
}
}
client.EndpointLocator = func(opts gophercloud.EndpointOpts) (string, error) {
@ -197,6 +199,40 @@ func NewIdentityV3(client *gophercloud.ProviderClient) *gophercloud.ServiceClien
}
}
func NewIdentityAdminV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
eo.ApplyDefaults("identity")
eo.Availability = gophercloud.AvailabilityAdmin
url, err := client.EndpointLocator(eo)
if err != nil {
return nil, err
}
// Force using v2 API
if strings.Contains(url, "/v3") {
url = strings.Replace(url, "/v3", "/v2.0", -1)
}
return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil
}
func NewIdentityAdminV3(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
eo.ApplyDefaults("identity")
eo.Availability = gophercloud.AvailabilityAdmin
url, err := client.EndpointLocator(eo)
if err != nil {
return nil, err
}
// Force using v3 API
if strings.Contains(url, "/v2.0") {
url = strings.Replace(url, "/v2.0", "/v3", -1)
}
return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil
}
// NewObjectStorageV1 creates a ServiceClient that may be used with the v1 object storage package.
func NewObjectStorageV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
eo.ApplyDefaults("object-store")
@ -261,3 +297,13 @@ func NewOrchestrationV1(client *gophercloud.ProviderClient, eo gophercloud.Endpo
}
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("database")
url, err := client.EndpointLocator(eo)
if err != nil {
return nil, err
}
return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil
}

View File

@ -15,6 +15,7 @@ const (
Volume SourceType = "volume"
Snapshot SourceType = "snapshot"
Image SourceType = "image"
Blank SourceType = "blank"
)
// BlockDevice is a structure with options for booting a server instance
@ -32,6 +33,9 @@ type BlockDevice struct {
// and "local".
DestinationType string `json:"destination_type"`
// GuestFormat [optional] specifies the format of the block device.
GuestFormat string `json:"guest_format"`
// SourceType [required] must be one of: "volume", "snapshot", "image".
SourceType SourceType `json:"source_type"`
@ -82,6 +86,9 @@ func (opts CreateOptsExt) ToServerCreateMap() (map[string]interface{}, error) {
if bd.DestinationType != "" {
blockDevice[i]["destination_type"] = bd.DestinationType
}
if bd.GuestFormat != "" {
blockDevice[i]["guest_format"] = bd.GuestFormat
}
}
serverMap["block_device_mapping_v2"] = blockDevice
@ -99,6 +106,11 @@ func Create(client *gophercloud.ServiceClient, opts servers.CreateOptsBuilder) s
return res
}
// Delete imageName and flavorName that come from ToServerCreateMap().
// As of Liberty, Boot From Volume is failing if they are passed.
delete(reqBody["server"].(map[string]interface{}), "imageName")
delete(reqBody["server"].(map[string]interface{}), "flavorName")
_, res.Err = client.Post(createURL(client), reqBody, &res.Body, &gophercloud.RequestOpts{
OkCodes: []int{200, 202},
})

View File

@ -72,6 +72,41 @@ func mockCreateRuleResponse(t *testing.T) {
})
}
func mockCreateRuleResponseICMPZero(t *testing.T) {
th.Mux.HandleFunc(rootPath, func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "POST")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
th.TestJSONRequest(t, r, `
{
"security_group_default_rule": {
"ip_protocol": "ICMP",
"from_port": 0,
"to_port": 0,
"cidr": "10.10.12.0/24"
}
}
`)
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, `
{
"security_group_default_rule": {
"from_port": 0,
"id": "{ruleID}",
"ip_protocol": "ICMP",
"ip_range": {
"cidr": "10.10.12.0/24"
},
"to_port": 0
}
}
`)
})
}
func mockGetRuleResponse(t *testing.T, ruleID string) {
url := rootPath + "/" + ruleID
th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) {

View File

@ -2,6 +2,7 @@ package defsecrules
import (
"errors"
"strings"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
@ -42,10 +43,10 @@ type CreateOptsBuilder interface {
func (opts CreateOpts) ToRuleCreateMap() (map[string]interface{}, error) {
rule := make(map[string]interface{})
if opts.FromPort == 0 {
if opts.FromPort == 0 && strings.ToUpper(opts.IPProtocol) != "ICMP" {
return rule, errors.New("A FromPort must be set")
}
if opts.ToPort == 0 {
if opts.ToPort == 0 && strings.ToUpper(opts.IPProtocol) != "ICMP" {
return rule, errors.New("A ToPort must be set")
}
if opts.IPProtocol == "" {

View File

@ -155,6 +155,25 @@ func HandleAssociateSuccessfully(t *testing.T) {
})
}
// HandleFixedAssociateSucessfully configures the test server to respond to a Post request
// to associate an allocated floating IP with a specific fixed IP address
func HandleAssociateFixedSuccessfully(t *testing.T) {
th.Mux.HandleFunc("/servers/4d8c3732-a248-40ed-bebc-539a6ffd25c0/action", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "POST")
th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
th.TestJSONRequest(t, r, `
{
"addFloatingIp": {
"address": "10.10.10.2",
"fixed_address": "166.78.185.201"
}
}
`)
w.WriteHeader(http.StatusAccepted)
})
}
// HandleDisassociateSuccessfully configures the test server to respond to a Post request
// to disassociate an allocated floating IP
func HandleDisassociateSuccessfully(t *testing.T) {

View File

@ -26,6 +26,18 @@ type CreateOpts struct {
Pool string
}
// AssociateOpts specifies the required information to associate or disassociate a floating IP to an instance
type AssociateOpts struct {
// ServerID is the UUID of the server
ServerID string
// FixedIP is an optional fixed IP address of the server
FixedIP string
// FloatingIP is the floating IP to associate with an instance
FloatingIP string
}
// ToFloatingIPCreateMap constructs a request body from CreateOpts.
func (opts CreateOpts) ToFloatingIPCreateMap() (map[string]interface{}, error) {
if opts.Pool == "" {
@ -35,6 +47,26 @@ func (opts CreateOpts) ToFloatingIPCreateMap() (map[string]interface{}, error) {
return map[string]interface{}{"pool": opts.Pool}, nil
}
// ToAssociateMap constructs a request body from AssociateOpts.
func (opts AssociateOpts) ToAssociateMap() (map[string]interface{}, error) {
if opts.ServerID == "" {
return nil, errors.New("Required field missing for floating IP association: ServerID")
}
if opts.FloatingIP == "" {
return nil, errors.New("Required field missing for floating IP association: FloatingIP")
}
associateInfo := map[string]interface{}{
"serverId": opts.ServerID,
"floatingIp": opts.FloatingIP,
"fixedIp": opts.FixedIP,
}
return associateInfo, nil
}
// Create requests the creation of a new floating IP
func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult {
var res CreateResult
@ -68,6 +100,7 @@ func Delete(client *gophercloud.ServiceClient, id string) DeleteResult {
// association / disassociation
// Associate pairs an allocated floating IP with an instance
// Deprecated. Use AssociateInstance.
func Associate(client *gophercloud.ServiceClient, serverId, fip string) AssociateResult {
var res AssociateResult
@ -79,7 +112,33 @@ func Associate(client *gophercloud.ServiceClient, serverId, fip string) Associat
return res
}
// AssociateInstance pairs an allocated floating IP with an instance.
func AssociateInstance(client *gophercloud.ServiceClient, opts AssociateOpts) AssociateResult {
var res AssociateResult
associateInfo, err := opts.ToAssociateMap()
if err != nil {
res.Err = err
return res
}
addFloatingIp := make(map[string]interface{})
addFloatingIp["address"] = associateInfo["floatingIp"].(string)
// fixedIp is not required
if associateInfo["fixedIp"] != "" {
addFloatingIp["fixed_address"] = associateInfo["fixedIp"].(string)
}
serverId := associateInfo["serverId"].(string)
reqBody := map[string]interface{}{"addFloatingIp": addFloatingIp}
_, res.Err = client.Post(associateURL(client, serverId), reqBody, nil, nil)
return res
}
// Disassociate decouples an allocated floating IP from an instance
// Deprecated. Use DisassociateInstance.
func Disassociate(client *gophercloud.ServiceClient, serverId, fip string) DisassociateResult {
var res DisassociateResult
@ -90,3 +149,23 @@ func Disassociate(client *gophercloud.ServiceClient, serverId, fip string) Disas
_, res.Err = client.Post(disassociateURL(client, serverId), reqBody, nil, nil)
return res
}
// DisassociateInstance decouples an allocated floating IP from an instance
func DisassociateInstance(client *gophercloud.ServiceClient, opts AssociateOpts) DisassociateResult {
var res DisassociateResult
associateInfo, err := opts.ToAssociateMap()
if err != nil {
res.Err = err
return res
}
removeFloatingIp := make(map[string]interface{})
removeFloatingIp["address"] = associateInfo["floatingIp"].(string)
reqBody := map[string]interface{}{"removeFloatingIp": removeFloatingIp}
serverId := associateInfo["serverId"].(string)
_, res.Err = client.Post(disassociateURL(client, serverId), reqBody, nil, nil)
return res
}

View File

@ -0,0 +1,3 @@
// Package quotasets provides information and interaction with QuotaSet
// extension for the OpenStack Compute service.
package quotasets

View File

@ -0,0 +1,59 @@
// +build fixtures
package quotasets
import (
"fmt"
"net/http"
"testing"
th "github.com/rackspace/gophercloud/testhelper"
"github.com/rackspace/gophercloud/testhelper/client"
)
// GetOutput is a sample response to a Get call.
const GetOutput = `
{
"quota_set" : {
"instances" : 25,
"security_groups" : 10,
"security_group_rules" : 20,
"cores" : 200,
"injected_file_content_bytes" : 10240,
"injected_files" : 5,
"metadata_items" : 128,
"ram" : 200000,
"keypairs" : 10,
"injected_file_path_bytes" : 255
}
}
`
const FirstTenantID = "555544443333222211110000ffffeeee"
// FirstQuotaSet is the first result in ListOutput.
var FirstQuotaSet = QuotaSet{
FixedIps: 0,
FloatingIps: 0,
InjectedFileContentBytes: 10240,
InjectedFilePathBytes: 255,
InjectedFiles: 5,
KeyPairs: 10,
MetadataItems: 128,
Ram: 200000,
SecurityGroupRules: 20,
SecurityGroups: 10,
Cores: 200,
Instances: 25,
}
// HandleGetSuccessfully configures the test server to respond to a Get request for sample tenant
func HandleGetSuccessfully(t *testing.T) {
th.Mux.HandleFunc("/os-quota-sets/"+FirstTenantID, func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "GET")
th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
w.Header().Add("Content-Type", "application/json")
fmt.Fprintf(w, GetOutput)
})
}

View File

@ -0,0 +1,12 @@
package quotasets
import (
"github.com/rackspace/gophercloud"
)
// Get returns public data about a previously created QuotaSet.
func Get(client *gophercloud.ServiceClient, tenantID string) GetResult {
var res GetResult
_, res.Err = client.Get(getURL(client, tenantID), &res.Body, nil)
return res
}

View File

@ -0,0 +1,86 @@
package quotasets
import (
"github.com/mitchellh/mapstructure"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
)
// QuotaSet is a set of operational limits that allow for control of compute usage.
type QuotaSet struct {
//ID is tenant associated with this quota_set
ID string `mapstructure:"id"`
//FixedIps is number of fixed ips alloted this quota_set
FixedIps int `mapstructure:"fixed_ips"`
// FloatingIps is number of floating ips alloted this quota_set
FloatingIps int `mapstructure:"floating_ips"`
// InjectedFileContentBytes is content bytes allowed for each injected file
InjectedFileContentBytes int `mapstructure:"injected_file_content_bytes"`
// InjectedFilePathBytes is allowed bytes for each injected file path
InjectedFilePathBytes int `mapstructure:"injected_file_path_bytes"`
// InjectedFiles is injected files allowed for each project
InjectedFiles int `mapstructure:"injected_files"`
// KeyPairs is number of ssh keypairs
KeyPairs int `mapstructure:"keypairs"`
// MetadataItems is number of metadata items allowed for each instance
MetadataItems int `mapstructure:"metadata_items"`
// Ram is megabytes allowed for each instance
Ram int `mapstructure:"ram"`
// SecurityGroupRules is rules allowed for each security group
SecurityGroupRules int `mapstructure:"security_group_rules"`
// SecurityGroups security groups allowed for each project
SecurityGroups int `mapstructure:"security_groups"`
// Cores is number of instance cores allowed for each project
Cores int `mapstructure:"cores"`
// Instances is number of instances allowed for each project
Instances int `mapstructure:"instances"`
}
// QuotaSetPage stores a single, only page of QuotaSet results from a List call.
type QuotaSetPage struct {
pagination.SinglePageBase
}
// IsEmpty determines whether or not a QuotaSetsetPage is empty.
func (page QuotaSetPage) IsEmpty() (bool, error) {
ks, err := ExtractQuotaSets(page)
return len(ks) == 0, err
}
// ExtractQuotaSets interprets a page of results as a slice of QuotaSets.
func ExtractQuotaSets(page pagination.Page) ([]QuotaSet, error) {
var resp struct {
QuotaSets []QuotaSet `mapstructure:"quotas"`
}
err := mapstructure.Decode(page.(QuotaSetPage).Body, &resp)
results := make([]QuotaSet, len(resp.QuotaSets))
for i, q := range resp.QuotaSets {
results[i] = q
}
return results, err
}
type quotaResult struct {
gophercloud.Result
}
// Extract is a method that attempts to interpret any QuotaSet resource response as a QuotaSet struct.
func (r quotaResult) Extract() (*QuotaSet, error) {
if r.Err != nil {
return nil, r.Err
}
var res struct {
QuotaSet *QuotaSet `json:"quota_set" mapstructure:"quota_set"`
}
err := mapstructure.Decode(r.Body, &res)
return res.QuotaSet, err
}
// GetResult is the response from a Get operation. Call its Extract method to interpret it
// as a QuotaSet.
type GetResult struct {
quotaResult
}

View File

@ -0,0 +1,13 @@
package quotasets
import "github.com/rackspace/gophercloud"
const resourcePath = "os-quota-sets"
func resourceURL(c *gophercloud.ServiceClient) string {
return c.ServiceURL(resourcePath)
}
func getURL(c *gophercloud.ServiceClient, tenantID string) string {
return c.ServiceURL(resourcePath, tenantID)
}

View File

@ -216,6 +216,42 @@ func mockAddRuleResponse(t *testing.T) {
})
}
func mockAddRuleResponseICMPZero(t *testing.T) {
th.Mux.HandleFunc("/os-security-group-rules", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "POST")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
th.TestJSONRequest(t, r, `
{
"security_group_rule": {
"from_port": 0,
"ip_protocol": "ICMP",
"to_port": 0,
"parent_group_id": "{groupID}",
"cidr": "0.0.0.0/0"
}
} `)
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, `
{
"security_group_rule": {
"from_port": 0,
"group": {},
"ip_protocol": "ICMP",
"to_port": 0,
"parent_group_id": "{groupID}",
"ip_range": {
"cidr": "0.0.0.0/0"
},
"id": "{ruleID}"
}
}`)
})
}
func mockDeleteRuleResponse(t *testing.T, ruleID string) {
url := fmt.Sprintf("/os-security-group-rules/%s", ruleID)
th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) {

View File

@ -2,6 +2,7 @@ package secgroups
import (
"errors"
"strings"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
@ -181,10 +182,10 @@ func (opts CreateRuleOpts) ToRuleCreateMap() (map[string]interface{}, error) {
if opts.ParentGroupID == "" {
return rule, errors.New("A ParentGroupID must be set")
}
if opts.FromPort == 0 {
if opts.FromPort == 0 && strings.ToUpper(opts.IPProtocol) != "ICMP" {
return rule, errors.New("A FromPort must be set")
}
if opts.ToPort == 0 {
if opts.ToPort == 0 && strings.ToUpper(opts.IPProtocol) != "ICMP" {
return rule, errors.New("A ToPort must be set")
}
if opts.IPProtocol == "" {

View File

@ -235,6 +235,12 @@ const SingleServerBody = `
}
`
const ServerPasswordBody = `
{
"password": "xlozO3wLCBRWAa2yDjCCVx8vwNPypxnypmRYDa/zErlQ+EzPe1S/Gz6nfmC52mOlOSCRuUOmG7kqqgejPof6M7bOezS387zjq4LSvvwp28zUknzy4YzfFGhnHAdai3TxUJ26pfQCYrq8UTzmKF2Bq8ioSEtVVzM0A96pDh8W2i7BOz6MdoiVyiev/I1K2LsuipfxSJR7Wdke4zNXJjHHP2RfYsVbZ/k9ANu+Nz4iIH8/7Cacud/pphH7EjrY6a4RZNrjQskrhKYed0YERpotyjYk1eDtRe72GrSiXteqCM4biaQ5w3ruS+AcX//PXk3uJ5kC7d67fPXaVz4WaQRYMg=="
}
`
var (
// ServerHerp is a Server struct that should correspond to the first result in ServerListBody.
ServerHerp = Server{
@ -399,6 +405,18 @@ func HandleServerDeletionSuccessfully(t *testing.T) {
})
}
// HandleServerForceDeletionSuccessfully sets up the test server to respond to a server force deletion
// request.
func HandleServerForceDeletionSuccessfully(t *testing.T) {
th.Mux.HandleFunc("/servers/asdfasdfasdf/action", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "POST")
th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
th.TestJSONRequest(t, r, `{ "forceDelete": "" }`)
w.WriteHeader(http.StatusAccepted)
})
}
// HandleServerGetSuccessfully sets up the test server to respond to a server Get request.
func HandleServerGetSuccessfully(t *testing.T) {
th.Mux.HandleFunc("/servers/1234asdf", func(w http.ResponseWriter, r *http.Request) {
@ -662,3 +680,13 @@ func HandleCreateServerImageSuccessfully(t *testing.T) {
})
}
// HandlePasswordGetSuccessfully sets up the test server to respond to a password Get request.
func HandlePasswordGetSuccessfully(t *testing.T) {
th.Mux.HandleFunc("/servers/1234asdf/os-server-password", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "GET")
th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
th.TestHeader(t, r, "Accept", "application/json")
fmt.Fprintf(w, ServerPasswordBody)
})
}

View File

@ -303,6 +303,17 @@ func Delete(client *gophercloud.ServiceClient, id string) DeleteResult {
return res
}
func ForceDelete(client *gophercloud.ServiceClient, id string) ActionResult {
var req struct {
ForceDelete string `json:"forceDelete"`
}
var res ActionResult
_, res.Err = client.Post(actionURL(client, id), req, nil, nil)
return res
}
// Get requests details on a single server, by ID.
func Get(client *gophercloud.ServiceClient, id string) GetResult {
var result GetResult
@ -850,3 +861,12 @@ func IDFromName(client *gophercloud.ServiceClient, name string) (string, error)
return "", fmt.Errorf("Found %d servers matching %s", serverCount, name)
}
}
// GetPassword makes a request against the nova API to get the encrypted administrative password.
func GetPassword(client *gophercloud.ServiceClient, serverId string) GetPasswordResult {
var res GetPasswordResult
_, res.Err = client.Request("GET", passwordURL(client, serverId), gophercloud.RequestOpts{
JSONResponse: &res.Body,
})
return res
}

View File

@ -1,10 +1,12 @@
package servers
import (
"reflect"
"crypto/rsa"
"encoding/base64"
"fmt"
"path"
"net/url"
"path"
"reflect"
"github.com/mitchellh/mapstructure"
"github.com/rackspace/gophercloud"
@ -82,6 +84,47 @@ type CreateImageResult struct {
gophercloud.Result
}
// GetPasswordResult represent the result of a get os-server-password operation.
type GetPasswordResult struct {
gophercloud.Result
}
// ExtractPassword gets the encrypted password.
// If privateKey != nil the password is decrypted with the private key.
// If privateKey == nil the encrypted password is returned and can be decrypted with:
// echo '<pwd>' | base64 -D | openssl rsautl -decrypt -inkey <private_key>
func (r GetPasswordResult) ExtractPassword(privateKey *rsa.PrivateKey) (string, error) {
if r.Err != nil {
return "", r.Err
}
var response struct {
Password string `mapstructure:"password"`
}
err := mapstructure.Decode(r.Body, &response)
if err == nil && privateKey != nil && response.Password != "" {
return decryptPassword(response.Password, privateKey)
}
return response.Password, err
}
func decryptPassword(encryptedPassword string, privateKey *rsa.PrivateKey) (string, error) {
b64EncryptedPassword := make([]byte, base64.StdEncoding.DecodedLen(len(encryptedPassword)))
n, err := base64.StdEncoding.Decode(b64EncryptedPassword, []byte(encryptedPassword))
if err != nil {
return "", fmt.Errorf("Failed to base64 decode encrypted password: %s", err)
}
password, err := rsa.DecryptPKCS1v15(nil, privateKey, b64EncryptedPassword[0:n])
if err != nil {
return "", fmt.Errorf("Failed to decrypt password: %s", err)
}
return string(password), nil
}
// ExtractImageID gets the ID of the newly created server image from the header
func (res CreateImageResult) ExtractImageID() (string, error) {
if res.Err != nil {

View File

@ -45,3 +45,7 @@ func listAddressesURL(client *gophercloud.ServiceClient, id string) string {
func listAddressesByNetworkURL(client *gophercloud.ServiceClient, id, network string) string {
return client.ServiceURL("servers", id, "ips", network)
}
func passwordURL(client *gophercloud.ServiceClient, id string) string {
return client.ServiceURL("servers", id, "os-server-password")
}

View File

@ -0,0 +1,11 @@
// Package configurations provides information and interaction with the
// configuration API resource in the Rackspace Database service.
//
// A configuration group is a collection of key/value pairs which define how a
// particular database operates. These key/value pairs are specific to each
// datastore type and serve like settings. Some directives are capable of being
// applied dynamically, while other directives require a server restart to take
// effect. The configuration group can be applied to an instance at creation or
// applied to an existing instance to modify the behavior of the running
// datastore on the instance.
package configurations

View File

@ -0,0 +1,157 @@
package configurations
import (
"fmt"
"time"
)
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 = 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 = 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,287 @@
package configurations
import (
"errors"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/openstack/db/v1/instances"
"github.com/rackspace/gophercloud/pagination"
)
// List will list all of the available configurations.
func List(client *gophercloud.ServiceClient) pagination.Pager {
pageFn := func(r pagination.PageResult) pagination.Page {
return ConfigPage{pagination.SinglePageBase(r)}
}
return pagination.NewPager(client, baseURL(client), pageFn)
}
// CreateOptsBuilder is a top-level interface which renders a JSON map.
type CreateOptsBuilder interface {
ToConfigCreateMap() (map[string]interface{}, error)
}
// DatastoreOpts is the primary options struct for creating and modifying
// how configuration resources are associated with datastores.
type DatastoreOpts struct {
// [OPTIONAL] The type of datastore. Defaults to "MySQL".
Type string
// [OPTIONAL] The specific version of a datastore. Defaults to "5.6".
Version string
}
// ToMap renders a JSON map for a datastore setting.
func (opts DatastoreOpts) ToMap() (map[string]string, error) {
datastore := map[string]string{}
if opts.Type != "" {
datastore["type"] = opts.Type
}
if opts.Version != "" {
datastore["version"] = opts.Version
}
return datastore, nil
}
// CreateOpts is the struct responsible for configuring new configurations.
type CreateOpts struct {
// [REQUIRED] The configuration group name
Name string
// [REQUIRED] A map of user-defined configuration settings that will define
// how each associated datastore works. Each key/value pair is specific to a
// datastore type.
Values map[string]interface{}
// [OPTIONAL] Associates the configuration group with a particular datastore.
Datastore *DatastoreOpts
// [OPTIONAL] A human-readable explanation for the group.
Description string
}
// ToConfigCreateMap casts a CreateOpts struct into a JSON map.
func (opts CreateOpts) ToConfigCreateMap() (map[string]interface{}, error) {
if opts.Name == "" {
return nil, errors.New("Name is a required field")
}
if len(opts.Values) == 0 {
return nil, errors.New("Values must be a populated map")
}
config := map[string]interface{}{
"name": opts.Name,
"values": opts.Values,
}
if opts.Datastore != nil {
ds, err := opts.Datastore.ToMap()
if err != nil {
return config, err
}
config["datastore"] = ds
}
if opts.Description != "" {
config["description"] = opts.Description
}
return map[string]interface{}{"configuration": config}, nil
}
// Create will create a new configuration group.
func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult {
var res CreateResult
reqBody, err := opts.ToConfigCreateMap()
if err != nil {
res.Err = err
return res
}
_, res.Err = client.Request("POST", baseURL(client), gophercloud.RequestOpts{
OkCodes: []int{200},
JSONBody: &reqBody,
JSONResponse: &res.Body,
})
return res
}
// Get will retrieve the details for a specified configuration group.
func Get(client *gophercloud.ServiceClient, configID string) GetResult {
var res GetResult
_, res.Err = client.Request("GET", resourceURL(client, configID), gophercloud.RequestOpts{
OkCodes: []int{200},
JSONResponse: &res.Body,
})
return res
}
// UpdateOptsBuilder is the top-level interface for casting update options into
// JSON maps.
type UpdateOptsBuilder interface {
ToConfigUpdateMap() (map[string]interface{}, error)
}
// UpdateOpts is the struct responsible for modifying existing configurations.
type UpdateOpts struct {
// [OPTIONAL] The configuration group name
Name string
// [OPTIONAL] A map of user-defined configuration settings that will define
// how each associated datastore works. Each key/value pair is specific to a
// datastore type.
Values map[string]interface{}
// [OPTIONAL] Associates the configuration group with a particular datastore.
Datastore *DatastoreOpts
// [OPTIONAL] A human-readable explanation for the group.
Description string
}
// ToConfigUpdateMap will cast an UpdateOpts struct into a JSON map.
func (opts UpdateOpts) ToConfigUpdateMap() (map[string]interface{}, error) {
config := map[string]interface{}{}
if opts.Name != "" {
config["name"] = opts.Name
}
if opts.Description != "" {
config["description"] = opts.Description
}
if opts.Datastore != nil {
ds, err := opts.Datastore.ToMap()
if err != nil {
return config, err
}
config["datastore"] = ds
}
if len(opts.Values) > 0 {
config["values"] = opts.Values
}
return map[string]interface{}{"configuration": config}, nil
}
// 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 UpdateOptsBuilder) UpdateResult {
var res UpdateResult
reqBody, err := opts.ToConfigUpdateMap()
if err != nil {
res.Err = err
return res
}
_, res.Err = client.Request("PATCH", resourceURL(client, configID), gophercloud.RequestOpts{
OkCodes: []int{200},
JSONBody: &reqBody,
})
return res
}
// 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 UpdateOptsBuilder) ReplaceResult {
var res ReplaceResult
reqBody, err := opts.ToConfigUpdateMap()
if err != nil {
res.Err = err
return res
}
_, res.Err = client.Request("PUT", resourceURL(client, configID), gophercloud.RequestOpts{
OkCodes: []int{202},
JSONBody: &reqBody,
})
return res
}
// 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) DeleteResult {
var res DeleteResult
_, res.Err = client.Request("DELETE", resourceURL(client, configID), gophercloud.RequestOpts{
OkCodes: []int{202},
})
return res
}
// ListInstances will list all the instances associated with a particular
// configuration group.
func ListInstances(client *gophercloud.ServiceClient, configID string) pagination.Pager {
pageFn := func(r pagination.PageResult) pagination.Page {
return instances.InstancePage{pagination.LinkedPageBase{PageResult: r}}
}
return pagination.NewPager(client, instancesURL(client, configID), pageFn)
}
// 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 {
pageFn := func(r pagination.PageResult) pagination.Page {
return ParamPage{pagination.SinglePageBase(r)}
}
return pagination.NewPager(client, listDSParamsURL(client, datastoreID, versionID), pageFn)
}
// 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) ParamResult {
var res ParamResult
_, res.Err = client.Request("GET", getDSParamURL(client, datastoreID, versionID, paramID), gophercloud.RequestOpts{
OkCodes: []int{200},
JSONResponse: &res.Body,
})
return res
}
// ListGlobalParams is similar to ListDatastoreParams but does not require a
// DatastoreID.
func ListGlobalParams(client *gophercloud.ServiceClient, versionID string) pagination.Pager {
pageFn := func(r pagination.PageResult) pagination.Page {
return ParamPage{pagination.SinglePageBase(r)}
}
return pagination.NewPager(client, listGlobalParamsURL(client, versionID), pageFn)
}
// GetGlobalParam is similar to GetDatastoreParam but does not require a
// DatastoreID.
func GetGlobalParam(client *gophercloud.ServiceClient, versionID, paramID string) ParamResult {
var res ParamResult
_, res.Err = client.Request("GET", getGlobalParamURL(client, versionID, paramID), gophercloud.RequestOpts{
OkCodes: []int{200},
JSONResponse: &res.Body,
})
return res
}

View File

@ -0,0 +1,197 @@
package configurations
import (
"fmt"
"reflect"
"time"
"github.com/mitchellh/mapstructure"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
)
// Config represents a configuration group API resource.
type Config struct {
Created time.Time `mapstructure:"-"`
Updated time.Time `mapstructure:"-"`
DatastoreName string `mapstructure:"datastore_name"`
DatastoreVersionID string `mapstructure:"datastore_version_id"`
DatastoreVersionName string `mapstructure:"datastore_version_name"`
Description string
ID string
Name string
Values map[string]interface{}
}
// ConfigPage contains a page of Config resources in a paginated collection.
type ConfigPage struct {
pagination.SinglePageBase
}
// IsEmpty indicates whether a ConfigPage is empty.
func (r ConfigPage) IsEmpty() (bool, error) {
is, err := ExtractConfigs(r)
if err != nil {
return true, err
}
return len(is) == 0, nil
}
// ExtractConfigs will retrieve a slice of Config structs from a page.
func ExtractConfigs(page pagination.Page) ([]Config, error) {
casted := page.(ConfigPage).Body
var resp struct {
Configs []Config `mapstructure:"configurations" json:"configurations"`
}
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{})["configurations"].([]interface{})
case map[string][]interface{}:
vals = casted.(map[string][]interface{})["configurations"]
default:
return resp.Configs, 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.Configs, err
}
resp.Configs[i].Created = creationTime
}
if t, ok := val["updated"].(string); ok && t != "" {
updatedTime, err := time.Parse(time.RFC3339, t)
if err != nil {
return resp.Configs, err
}
resp.Configs[i].Updated = updatedTime
}
}
return resp.Configs, nil
}
type commonResult struct {
gophercloud.Result
}
// Extract will retrieve a Config resource from an operation result.
func (r commonResult) Extract() (*Config, error) {
if r.Err != nil {
return nil, r.Err
}
var response struct {
Config Config `mapstructure:"configuration"`
}
err := mapstructure.Decode(r.Body, &response)
val := r.Body.(map[string]interface{})["configuration"].(map[string]interface{})
if t, ok := val["created"].(string); ok && t != "" {
creationTime, err := time.Parse(time.RFC3339, t)
if err != nil {
return &response.Config, err
}
response.Config.Created = creationTime
}
if t, ok := val["updated"].(string); ok && t != "" {
updatedTime, err := time.Parse(time.RFC3339, t)
if err != nil {
return &response.Config, err
}
response.Config.Updated = updatedTime
}
return &response.Config, err
}
// GetResult represents the result of a Get operation.
type GetResult struct {
commonResult
}
// CreateResult represents the result of a Create operation.
type CreateResult struct {
commonResult
}
// UpdateResult represents the result of an Update operation.
type UpdateResult struct {
gophercloud.ErrResult
}
// ReplaceResult represents the result of a Replace operation.
type ReplaceResult struct {
gophercloud.ErrResult
}
// DeleteResult represents the result of a Delete operation.
type DeleteResult struct {
gophercloud.ErrResult
}
// Param represents a configuration parameter API resource.
type Param struct {
Max int
Min int
Name string
RestartRequired bool `mapstructure:"restart_required" json:"restart_required"`
Type string
}
// ParamPage contains a page of Param resources in a paginated collection.
type ParamPage struct {
pagination.SinglePageBase
}
// IsEmpty indicates whether a ParamPage is empty.
func (r ParamPage) IsEmpty() (bool, error) {
is, err := ExtractParams(r)
if err != nil {
return true, err
}
return len(is) == 0, nil
}
// ExtractParams will retrieve a slice of Param structs from a page.
func ExtractParams(page pagination.Page) ([]Param, error) {
casted := page.(ParamPage).Body
var resp struct {
Params []Param `mapstructure:"configuration-parameters" json:"configuration-parameters"`
}
err := mapstructure.Decode(casted, &resp)
return resp.Params, err
}
// ParamResult represents the result of an operation which retrieves details
// about a particular configuration param.
type ParamResult struct {
gophercloud.Result
}
// Extract will retrieve a param from an operation result.
func (r ParamResult) Extract() (*Param, error) {
if r.Err != nil {
return nil, r.Err
}
var param Param
err := mapstructure.Decode(r.Body, &param)
return &param, err
}

View File

@ -0,0 +1,31 @@
package configurations
import "github.com/rackspace/gophercloud"
func baseURL(c *gophercloud.ServiceClient) string {
return c.ServiceURL("configurations")
}
func resourceURL(c *gophercloud.ServiceClient, configID string) string {
return c.ServiceURL("configurations", configID)
}
func instancesURL(c *gophercloud.ServiceClient, configID string) string {
return c.ServiceURL("configurations", configID, "instances")
}
func listDSParamsURL(c *gophercloud.ServiceClient, datastoreID, versionID string) string {
return c.ServiceURL("datastores", datastoreID, "versions", versionID, "parameters")
}
func getDSParamURL(c *gophercloud.ServiceClient, datastoreID, versionID, paramID string) string {
return c.ServiceURL("datastores", datastoreID, "versions", versionID, "parameters", paramID)
}
func listGlobalParamsURL(c *gophercloud.ServiceClient, versionID string) string {
return c.ServiceURL("datastores", "versions", versionID, "parameters")
}
func getGlobalParamURL(c *gophercloud.ServiceClient, versionID, paramID string) string {
return c.ServiceURL("datastores", "versions", versionID, "parameters", paramID)
}

View File

@ -0,0 +1,6 @@
// Package flavors provides information and interaction with the database API
// resource in the OpenStack Database service.
//
// A database, when referred to here, refers to the database engine running on
// an instance.
package databases

View File

@ -0,0 +1,61 @@
package databases
import (
"testing"
"github.com/rackspace/gophercloud/testhelper/fixture"
)
var (
instanceID = "{instanceID}"
resURL = "/instances/" + instanceID + "/databases"
)
var createDBsReq = `
{
"databases": [
{
"character_set": "utf8",
"collate": "utf8_general_ci",
"name": "testingdb"
},
{
"name": "sampledb"
}
]
}
`
var listDBsResp = `
{
"databases": [
{
"name": "anotherexampledb"
},
{
"name": "exampledb"
},
{
"name": "nextround"
},
{
"name": "sampledb"
},
{
"name": "testingdb"
}
]
}
`
func HandleCreate(t *testing.T) {
fixture.SetupHandler(t, resURL, "POST", createDBsReq, "", 202)
}
func HandleList(t *testing.T) {
fixture.SetupHandler(t, resURL, "GET", "", listDBsResp, 200)
}
func HandleDelete(t *testing.T) {
fixture.SetupHandler(t, resURL+"/{dbName}", "DELETE", "", "", 202)
}

View File

@ -0,0 +1,115 @@
package databases
import (
"fmt"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
)
// CreateOptsBuilder builds create options
type CreateOptsBuilder interface {
ToDBCreateMap() (map[string]interface{}, error)
}
// DatabaseOpts is the struct responsible for configuring a database; often in
// the context of an instance.
type CreateOpts struct {
// [REQUIRED] Specifies the name of the database. 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.
Name string
// [OPTIONAL] Set of symbols and encodings. The default character set is
// "utf8". See http://dev.mysql.com/doc/refman/5.1/en/charset-mysql.html for
// supported character sets.
CharSet string
// [OPTIONAL] Set of rules for comparing characters in a character set. The
// default value for collate is "utf8_general_ci". See
// http://dev.mysql.com/doc/refman/5.1/en/charset-mysql.html for supported
// collations.
Collate string
}
// ToMap is a helper function to convert individual DB create opt structures
// into sub-maps.
func (opts CreateOpts) ToMap() (map[string]string, error) {
if opts.Name == "" {
return nil, fmt.Errorf("Name is a required field")
}
if len(opts.Name) > 64 {
return nil, fmt.Errorf("Name must be less than 64 chars long")
}
db := map[string]string{"name": opts.Name}
if opts.CharSet != "" {
db["character_set"] = opts.CharSet
}
if opts.Collate != "" {
db["collate"] = opts.Collate
}
return db, nil
}
// BatchCreateOpts allows for multiple databases to created and modified.
type BatchCreateOpts []CreateOpts
// ToDBCreateMap renders a JSON map for creating DBs.
func (opts BatchCreateOpts) ToDBCreateMap() (map[string]interface{}, error) {
dbs := make([]map[string]string, len(opts))
for i, db := range opts {
dbMap, err := db.ToMap()
if err != nil {
return nil, err
}
dbs[i] = dbMap
}
return map[string]interface{}{"databases": dbs}, nil
}
// Create will create a new database within the specified instance. If the
// specified instance does not exist, a 404 error will be returned.
func Create(client *gophercloud.ServiceClient, instanceID string, opts CreateOptsBuilder) CreateResult {
var res CreateResult
reqBody, err := opts.ToDBCreateMap()
if err != nil {
res.Err = err
return res
}
_, res.Err = client.Request("POST", baseURL(client, instanceID), gophercloud.RequestOpts{
JSONBody: &reqBody,
OkCodes: []int{202},
})
return res
}
// List will list all of the databases for a specified instance. Note: this
// operation will only return user-defined databases; it will exclude system
// databases like "mysql", "information_schema", "lost+found" etc.
func List(client *gophercloud.ServiceClient, instanceID string) pagination.Pager {
createPageFn := func(r pagination.PageResult) pagination.Page {
return DBPage{pagination.LinkedPageBase{PageResult: r}}
}
return pagination.NewPager(client, baseURL(client, instanceID), createPageFn)
}
// Delete will permanently delete the database within a specified instance.
// All contained data inside the database will also be permanently deleted.
func Delete(client *gophercloud.ServiceClient, instanceID, dbName string) DeleteResult {
var res DeleteResult
_, res.Err = client.Request("DELETE", dbURL(client, instanceID, dbName), gophercloud.RequestOpts{
OkCodes: []int{202},
})
return res
}

View File

@ -0,0 +1,72 @@
package databases
import (
"github.com/mitchellh/mapstructure"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
)
// Database represents a Database API resource.
type Database struct {
// Specifies the name of the MySQL database.
Name string
// Set of symbols and encodings. The default character set is utf8.
CharSet string
// Set of rules for comparing characters in a character set. The default
// value for collate is utf8_general_ci.
Collate string
}
// 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
}
// DBPage represents a single page of a paginated DB collection.
type DBPage struct {
pagination.LinkedPageBase
}
// IsEmpty checks to see whether the collection is empty.
func (page DBPage) IsEmpty() (bool, error) {
dbs, err := ExtractDBs(page)
if err != nil {
return true, err
}
return len(dbs) == 0, nil
}
// NextPageURL will retrieve the next page URL.
func (page DBPage) 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) ([]Database, error) {
casted := page.(DBPage).Body
var response struct {
Databases []Database `mapstructure:"databases"`
}
err := mapstructure.Decode(casted, &response)
return response.Databases, err
}

View File

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

View File

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

View File

@ -0,0 +1,100 @@
package datastores
import (
"fmt"
"github.com/rackspace/gophercloud"
)
const version1JSON = `
{
"id": "b00000b0-00b0-0b00-00b0-000b000000bb",
"links": [
{
"href": "https://10.240.28.38:8779/v1.0/1234/datastores/versions/b00000b0-00b0-0b00-00b0-000b000000bb",
"rel": "self"
},
{
"href": "https://10.240.28.38:8779/datastores/versions/b00000b0-00b0-0b00-00b0-000b000000bb",
"rel": "bookmark"
}
],
"name": "5.1"
}
`
const version2JSON = `
{
"id": "c00000b0-00c0-0c00-00c0-000b000000cc",
"links": [
{
"href": "https://10.240.28.38:8779/v1.0/1234/datastores/versions/c00000b0-00c0-0c00-00c0-000b000000cc",
"rel": "self"
},
{
"href": "https://10.240.28.38:8779/datastores/versions/c00000b0-00c0-0c00-00c0-000b000000cc",
"rel": "bookmark"
}
],
"name": "5.2"
}
`
var versionsJSON = fmt.Sprintf(`"versions": [%s, %s]`, version1JSON, version2JSON)
var singleDSJSON = fmt.Sprintf(`
{
"default_version": "c00000b0-00c0-0c00-00c0-000b000000cc",
"id": "10000000-0000-0000-0000-000000000001",
"links": [
{
"href": "https://10.240.28.38:8779/v1.0/1234/datastores/10000000-0000-0000-0000-000000000001",
"rel": "self"
},
{
"href": "https://10.240.28.38:8779/datastores/10000000-0000-0000-0000-000000000001",
"rel": "bookmark"
}
],
"name": "mysql",
%s
}
`, versionsJSON)
var (
ListDSResp = fmt.Sprintf(`{"datastores":[%s]}`, singleDSJSON)
GetDSResp = fmt.Sprintf(`{"datastore":%s}`, singleDSJSON)
ListVersionsResp = fmt.Sprintf(`{%s}`, versionsJSON)
GetVersionResp = fmt.Sprintf(`{"version":%s}`, version1JSON)
)
var ExampleVersion1 = Version{
ID: "b00000b0-00b0-0b00-00b0-000b000000bb",
Links: []gophercloud.Link{
gophercloud.Link{Rel: "self", Href: "https://10.240.28.38:8779/v1.0/1234/datastores/versions/b00000b0-00b0-0b00-00b0-000b000000bb"},
gophercloud.Link{Rel: "bookmark", Href: "https://10.240.28.38:8779/datastores/versions/b00000b0-00b0-0b00-00b0-000b000000bb"},
},
Name: "5.1",
}
var exampleVersion2 = Version{
ID: "c00000b0-00c0-0c00-00c0-000b000000cc",
Links: []gophercloud.Link{
gophercloud.Link{Rel: "self", Href: "https://10.240.28.38:8779/v1.0/1234/datastores/versions/c00000b0-00c0-0c00-00c0-000b000000cc"},
gophercloud.Link{Rel: "bookmark", Href: "https://10.240.28.38:8779/datastores/versions/c00000b0-00c0-0c00-00c0-000b000000cc"},
},
Name: "5.2",
}
var ExampleVersions = []Version{ExampleVersion1, exampleVersion2}
var ExampleDatastore = Datastore{
DefaultVersion: "c00000b0-00c0-0c00-00c0-000b000000cc",
ID: "10000000-0000-0000-0000-000000000001",
Links: []gophercloud.Link{
gophercloud.Link{Rel: "self", Href: "https://10.240.28.38:8779/v1.0/1234/datastores/10000000-0000-0000-0000-000000000001"},
gophercloud.Link{Rel: "bookmark", Href: "https://10.240.28.38:8779/datastores/10000000-0000-0000-0000-000000000001"},
},
Name: "mysql",
Versions: ExampleVersions,
}

View File

@ -0,0 +1,47 @@
package datastores
import (
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
)
// List will list all available datastore types that instances can use.
func List(client *gophercloud.ServiceClient) pagination.Pager {
pageFn := func(r pagination.PageResult) pagination.Page {
return DatastorePage{pagination.SinglePageBase(r)}
}
return pagination.NewPager(client, baseURL(client), pageFn)
}
// Get will retrieve the details of a specified datastore type.
func Get(client *gophercloud.ServiceClient, datastoreID string) GetResult {
var res GetResult
_, res.Err = client.Request("GET", resourceURL(client, datastoreID), gophercloud.RequestOpts{
OkCodes: []int{200},
JSONResponse: &res.Body,
})
return res
}
// ListVersions will list all of the available versions for a specified
// datastore type.
func ListVersions(client *gophercloud.ServiceClient, datastoreID string) pagination.Pager {
pageFn := func(r pagination.PageResult) pagination.Page {
return VersionPage{pagination.SinglePageBase(r)}
}
return pagination.NewPager(client, versionsURL(client, datastoreID), pageFn)
}
// GetVersion will retrieve the details of a specified datastore version.
func GetVersion(client *gophercloud.ServiceClient, datastoreID, versionID string) GetVersionResult {
var res GetVersionResult
_, res.Err = client.Request("GET", versionURL(client, datastoreID, versionID), gophercloud.RequestOpts{
OkCodes: []int{200},
JSONResponse: &res.Body,
})
return res
}

View File

@ -0,0 +1,123 @@
package datastores
import (
"github.com/mitchellh/mapstructure"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
)
// Version represents a version API resource. Multiple versions belong to a Datastore.
type Version struct {
ID string
Links []gophercloud.Link
Name string
}
// Datastore represents a Datastore API resource.
type Datastore struct {
DefaultVersion string `json:"default_version" mapstructure:"default_version"`
ID string
Links []gophercloud.Link
Name string
Versions []Version
}
// DatastorePartial is a meta structure which is used in various API responses.
// It is a lightweight and truncated version of a full Datastore resource,
// offering details of the Version, Type and VersionID only.
type DatastorePartial struct {
Version string
Type string
VersionID string `json:"version_id" mapstructure:"version_id"`
}
// GetResult represents the result of a Get operation.
type GetResult struct {
gophercloud.Result
}
// GetVersionResult represents the result of getting a version.
type GetVersionResult struct {
gophercloud.Result
}
// DatastorePage represents a page of datastore resources.
type DatastorePage struct {
pagination.SinglePageBase
}
// IsEmpty indicates whether a Datastore collection is empty.
func (r DatastorePage) IsEmpty() (bool, error) {
is, err := ExtractDatastores(r)
if err != nil {
return true, err
}
return len(is) == 0, nil
}
// ExtractDatastores retrieves a slice of datastore structs from a paginated
// collection.
func ExtractDatastores(page pagination.Page) ([]Datastore, error) {
casted := page.(DatastorePage).Body
var resp struct {
Datastores []Datastore `mapstructure:"datastores" json:"datastores"`
}
err := mapstructure.Decode(casted, &resp)
return resp.Datastores, err
}
// Extract retrieves a single Datastore struct from an operation result.
func (r GetResult) Extract() (*Datastore, error) {
if r.Err != nil {
return nil, r.Err
}
var response struct {
Datastore Datastore `mapstructure:"datastore"`
}
err := mapstructure.Decode(r.Body, &response)
return &response.Datastore, err
}
// DatastorePage represents a page of version resources.
type VersionPage struct {
pagination.SinglePageBase
}
// IsEmpty indicates whether a collection of version resources is empty.
func (r VersionPage) IsEmpty() (bool, error) {
is, err := ExtractVersions(r)
if err != nil {
return true, err
}
return len(is) == 0, nil
}
// ExtractVersions retrieves a slice of versions from a paginated collection.
func ExtractVersions(page pagination.Page) ([]Version, error) {
casted := page.(VersionPage).Body
var resp struct {
Versions []Version `mapstructure:"versions" json:"versions"`
}
err := mapstructure.Decode(casted, &resp)
return resp.Versions, err
}
// Extract retrieves a single Version struct from an operation result.
func (r GetVersionResult) Extract() (*Version, error) {
if r.Err != nil {
return nil, r.Err
}
var response struct {
Version Version `mapstructure:"version"`
}
err := mapstructure.Decode(r.Body, &response)
return &response.Version, err
}

View File

@ -0,0 +1,19 @@
package datastores
import "github.com/rackspace/gophercloud"
func baseURL(c *gophercloud.ServiceClient) string {
return c.ServiceURL("datastores")
}
func resourceURL(c *gophercloud.ServiceClient, dsID string) string {
return c.ServiceURL("datastores", dsID)
}
func versionsURL(c *gophercloud.ServiceClient, dsID string) string {
return c.ServiceURL("datastores", dsID, "versions")
}
func versionURL(c *gophercloud.ServiceClient, dsID, versionID string) string {
return c.ServiceURL("datastores", dsID, "versions", versionID)
}

View File

@ -0,0 +1,7 @@
// Package flavors provides information and interaction with the flavor API
// resource in the OpenStack Database service.
//
// A flavor is an available hardware configuration for a database instance.
// Each flavor has a unique combination of disk space, memory capacity and
// priority for CPU time.
package flavors

View File

@ -0,0 +1,50 @@
package flavors
import (
"fmt"
"testing"
"github.com/rackspace/gophercloud/testhelper/fixture"
)
const flavor = `
{
"id": %d,
"links": [
{
"href": "https://openstack.example.com/v1.0/1234/flavors/%d",
"rel": "self"
},
{
"href": "https://openstack.example.com/flavors/%d",
"rel": "bookmark"
}
],
"name": "%s",
"ram": %d
}
`
var (
flavorID = "{flavorID}"
_baseURL = "/flavors"
resURL = "/flavors/" + flavorID
)
var (
flavor1 = fmt.Sprintf(flavor, 1, 1, 1, "m1.tiny", 512)
flavor2 = fmt.Sprintf(flavor, 2, 2, 2, "m1.small", 1024)
flavor3 = fmt.Sprintf(flavor, 3, 3, 3, "m1.medium", 2048)
flavor4 = fmt.Sprintf(flavor, 4, 4, 4, "m1.large", 4096)
listFlavorsResp = fmt.Sprintf(`{"flavors":[%s, %s, %s, %s]}`, flavor1, flavor2, flavor3, flavor4)
getFlavorResp = fmt.Sprintf(`{"flavor": %s}`, flavor1)
)
func HandleList(t *testing.T) {
fixture.SetupHandler(t, _baseURL, "GET", "", listFlavorsResp, 200)
}
func HandleGet(t *testing.T) {
fixture.SetupHandler(t, resURL, "GET", "", getFlavorResp, 200)
}

View File

@ -0,0 +1,29 @@
package flavors
import (
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
)
// List will list all available hardware flavors that an instance can use. The
// operation is identical to the one supported by the Nova API, but without the
// "disk" property.
func List(client *gophercloud.ServiceClient) pagination.Pager {
createPage := func(r pagination.PageResult) pagination.Page {
return FlavorPage{pagination.LinkedPageBase{PageResult: r}}
}
return pagination.NewPager(client, listURL(client), createPage)
}
// Get will retrieve information for a specified hardware flavor.
func Get(client *gophercloud.ServiceClient, id string) GetResult {
var gr GetResult
_, gr.Err = client.Request("GET", getURL(client, id), gophercloud.RequestOpts{
JSONResponse: &gr.Body,
OkCodes: []int{200},
})
return gr
}

View File

@ -0,0 +1,92 @@
package flavors
import (
"github.com/mitchellh/mapstructure"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
)
// 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"`
}
decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
WeaklyTypedInput: true,
Result: &result,
})
err = decoder.Decode(gr.Body)
return &result.Flavor, err
}
// Flavor records represent (virtual) hardware configurations for server resources in a region.
type Flavor struct {
// The flavor's unique identifier.
ID string `mapstructure:"id"`
// The RAM capacity for the flavor.
RAM int `mapstructure:"ram"`
// The Name field provides a human-readable moniker for the flavor.
Name string `mapstructure:"name"`
// Links to access the flavor.
Links []gophercloud.Link
}
// FlavorPage contains a single page of the response from a List call.
type FlavorPage struct {
pagination.LinkedPageBase
}
// IsEmpty determines if a page contains any results.
func (p FlavorPage) IsEmpty() (bool, error) {
flavors, err := ExtractFlavors(p)
if err != nil {
return true, err
}
return len(flavors) == 0, nil
}
// NextPageURL uses the response's embedded link reference to navigate to the next page of results.
func (p FlavorPage) NextPageURL() (string, error) {
type resp struct {
Links []gophercloud.Link `mapstructure:"flavors_links"`
}
var r resp
err := mapstructure.Decode(p.Body, &r)
if err != nil {
return "", err
}
return gophercloud.ExtractNextURL(r.Links)
}
// 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.(FlavorPage).Body
var container struct {
Flavors []Flavor `mapstructure:"flavors"`
}
decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
WeaklyTypedInput: true,
Result: &container,
})
err = decoder.Decode(casted)
return container.Flavors, err
}

View File

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

View File

@ -0,0 +1,7 @@
// Package instances provides information and interaction with the instance API
// resource in the OpenStack Database service.
//
// A database instance is an isolated database environment with compute and
// storage resources in a single tenant environment on a shared physical host
// machine.
package instances

View File

@ -0,0 +1,169 @@
package instances
import (
"fmt"
"testing"
"time"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/openstack/db/v1/datastores"
"github.com/rackspace/gophercloud/openstack/db/v1/flavors"
"github.com/rackspace/gophercloud/testhelper/fixture"
)
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://my-openstack.com/v1.0/1234/flavors/1",
"rel": "self"
},
{
"href": "https://my-openstack.com/v1.0/1234/flavors/1",
"rel": "bookmark"
}
]
},
"links": [
{
"href": "https://my-openstack.com/v1.0/1234/instances/1",
"rel": "self"
}
],
"hostname": "e09ad9a3f73309469cf1f43d11e79549caf9acf2.my-openstack.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
}
}
}
`
var (
instanceID = "{instanceID}"
rootURL = "/instances"
resURL = rootURL + "/" + instanceID
uRootURL = resURL + "/root"
aURL = resURL + "/action"
)
var (
restartReq = `{"restart": {}}`
resizeReq = `{"resize": {"flavorRef": "2"}}`
resizeVolReq = `{"resize": {"volume": {"size": 4}}}`
)
var (
createResp = fmt.Sprintf(`{"instance": %s}`, instance)
listInstancesResp = fmt.Sprintf(`{"instances":[%s]}`, instance)
getInstanceResp = createResp
enableUserResp = `{"user":{"name":"root","password":"secretsecret"}}`
isUserEnabledResp = `{"rootEnabled":true}`
)
var expectedInstance = Instance{
Created: timeVal,
Updated: timeVal,
Flavor: flavors.Flavor{
ID: "1",
Links: []gophercloud.Link{
gophercloud.Link{Href: "https://my-openstack.com/v1.0/1234/flavors/1", Rel: "self"},
gophercloud.Link{Href: "https://my-openstack.com/v1.0/1234/flavors/1", Rel: "bookmark"},
},
},
Hostname: "e09ad9a3f73309469cf1f43d11e79549caf9acf2.my-openstack.com",
ID: instanceID,
Links: []gophercloud.Link{
gophercloud.Link{Href: "https://my-openstack.com/v1.0/1234/instances/1", Rel: "self"},
},
Name: "json_rack_instance",
Status: "BUILD",
Volume: Volume{Size: 2},
Datastore: datastores.DatastorePartial{
Type: "mysql",
Version: "5.6",
},
}
func HandleCreate(t *testing.T) {
fixture.SetupHandler(t, rootURL, "POST", createReq, createResp, 200)
}
func HandleList(t *testing.T) {
fixture.SetupHandler(t, rootURL, "GET", "", listInstancesResp, 200)
}
func HandleGet(t *testing.T) {
fixture.SetupHandler(t, resURL, "GET", "", getInstanceResp, 200)
}
func HandleDelete(t *testing.T) {
fixture.SetupHandler(t, resURL, "DELETE", "", "", 202)
}
func HandleEnableRoot(t *testing.T) {
fixture.SetupHandler(t, uRootURL, "POST", "", enableUserResp, 200)
}
func HandleIsRootEnabled(t *testing.T) {
fixture.SetupHandler(t, uRootURL, "GET", "", isUserEnabledResp, 200)
}
func HandleRestart(t *testing.T) {
fixture.SetupHandler(t, aURL, "POST", restartReq, "", 202)
}
func HandleResize(t *testing.T) {
fixture.SetupHandler(t, aURL, "POST", resizeReq, "", 202)
}
func HandleResizeVol(t *testing.T) {
fixture.SetupHandler(t, aURL, "POST", resizeVolReq, "", 202)
}

View File

@ -0,0 +1,238 @@
package instances
import (
"fmt"
"github.com/rackspace/gophercloud"
db "github.com/rackspace/gophercloud/openstack/db/v1/databases"
"github.com/rackspace/gophercloud/openstack/db/v1/users"
"github.com/rackspace/gophercloud/pagination"
)
// CreateOptsBuilder is the top-level interface for create options.
type CreateOptsBuilder interface {
ToInstanceCreateMap() (map[string]interface{}, error)
}
// DatastoreOpts represents the configuration for how an instance stores data.
type DatastoreOpts struct {
Version string
Type string
}
func (opts DatastoreOpts) ToMap() (map[string]string, error) {
return map[string]string{
"version": opts.Version,
"type": opts.Type,
}, nil
}
// 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 db.CreateOptsBuilder
// A slice of user information options.
Users users.CreateOptsBuilder
// Options to configure the type of datastore the instance will use. This is
// optional, and if excluded will default to MySQL.
Datastore *DatastoreOpts
}
// ToInstanceCreateMap will render a JSON map.
func (opts CreateOpts) ToInstanceCreateMap() (map[string]interface{}, error) {
if opts.Size > 300 || opts.Size < 1 {
return nil, fmt.Errorf("Size (GB) must be between 1-300")
}
if opts.FlavorRef == "" {
return nil, fmt.Errorf("FlavorRef is a required field")
}
instance := map[string]interface{}{
"volume": map[string]int{"size": opts.Size},
"flavorRef": opts.FlavorRef,
}
if opts.Name != "" {
instance["name"] = opts.Name
}
if opts.Databases != nil {
dbs, err := opts.Databases.ToDBCreateMap()
if err != nil {
return nil, err
}
instance["databases"] = dbs["databases"]
}
if opts.Users != nil {
users, err := opts.Users.ToUserCreateMap()
if err != nil {
return nil, err
}
instance["users"] = users["users"]
}
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 CreateOptsBuilder) CreateResult {
var res CreateResult
reqBody, err := opts.ToInstanceCreateMap()
if err != nil {
res.Err = err
return res
}
_, res.Err = client.Request("POST", baseURL(client), gophercloud.RequestOpts{
JSONBody: &reqBody,
JSONResponse: &res.Body,
OkCodes: []int{200},
})
return res
}
// List retrieves the status and information for all database instances.
func List(client *gophercloud.ServiceClient) pagination.Pager {
createPageFn := func(r pagination.PageResult) pagination.Page {
return InstancePage{pagination.LinkedPageBase{PageResult: r}}
}
return pagination.NewPager(client, baseURL(client), createPageFn)
}
// Get retrieves the status and information for a specified database instance.
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 permanently destroys the database instance.
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
}
// 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) UserRootResult {
var res UserRootResult
_, res.Err = client.Request("POST", userRootURL(client, id), gophercloud.RequestOpts{
JSONResponse: &res.Body,
OkCodes: []int{200},
})
return res
}
// 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) {
var res gophercloud.Result
_, err := client.Request("GET", userRootURL(client, id), gophercloud.RequestOpts{
JSONResponse: &res.Body,
OkCodes: []int{200},
})
return res.Body.(map[string]interface{})["rootEnabled"] == true, err
}
// 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) ActionResult {
var res ActionResult
_, res.Err = client.Request("POST", actionURL(client, id), gophercloud.RequestOpts{
JSONBody: map[string]interface{}{"restart": struct{}{}},
OkCodes: []int{202},
})
return res
}
// 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) ActionResult {
var res ActionResult
type resize struct {
FlavorRef string `json:"flavorRef"`
}
type req struct {
Resize resize `json:"resize"`
}
reqBody := req{Resize: resize{FlavorRef: flavorRef}}
_, res.Err = client.Request("POST", actionURL(client, id), gophercloud.RequestOpts{
JSONBody: reqBody,
OkCodes: []int{202},
})
return res
}
// 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) ActionResult {
var res ActionResult
type volume struct {
Size int `json:"size"`
}
type resize struct {
Volume volume `json:"volume"`
}
type req struct {
Resize resize `json:"resize"`
}
reqBody := req{Resize: resize{Volume: volume{Size: size}}}
_, res.Err = client.Request("POST", actionURL(client, id), gophercloud.RequestOpts{
JSONBody: reqBody,
OkCodes: []int{202},
})
return res
}

View File

@ -0,0 +1,213 @@
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"
"github.com/rackspace/gophercloud/openstack/db/v1/users"
"github.com/rackspace/gophercloud/pagination"
)
// Volume represents information about an attached volume for a database instance.
type Volume struct {
// The size in GB of the volume
Size int
Used float64
}
// 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 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 Volume
// Indicates how the instance stores data.
Datastore datastores.DatastorePartial
}
type commonResult struct {
gophercloud.Result
}
// CreateResult represents the result of a Create operation.
type CreateResult struct {
commonResult
}
// GetResult represents the result of a Get operation.
type GetResult struct {
commonResult
}
// DeleteResult represents the result of a Delete operation.
type DeleteResult struct {
gophercloud.ErrResult
}
// Extract will extract an Instance from various result structs.
func (r commonResult) Extract() (*Instance, error) {
if r.Err != nil {
return nil, r.Err
}
var response struct {
Instance Instance `mapstructure:"instance"`
}
err := mapstructure.Decode(r.Body, &response)
val := r.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
}
// InstancePage represents a single page of a paginated instance collection.
type InstancePage struct {
pagination.LinkedPageBase
}
// IsEmpty checks to see whether the collection is empty.
func (page InstancePage) IsEmpty() (bool, error) {
instances, err := ExtractInstances(page)
if err != nil {
return true, err
}
return len(instances) == 0, nil
}
// NextPageURL will retrieve the next page URL.
func (page InstancePage) NextPageURL() (string, error) {
type resp struct {
Links []gophercloud.Link `mapstructure:"instances_links"`
}
var r resp
err := mapstructure.Decode(page.Body, &r)
if err != nil {
return "", err
}
return gophercloud.ExtractNextURL(r.Links)
}
// ExtractInstances will convert a generic pagination struct into a more
// relevant slice of Instance structs.
func ExtractInstances(page pagination.Page) ([]Instance, error) {
casted := page.(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
}
// UserRootResult represents the result of an operation to enable the root user.
type UserRootResult struct {
gophercloud.Result
}
// Extract will extract root user information from a UserRootResult.
func (r UserRootResult) Extract() (*users.User, error) {
if r.Err != nil {
return nil, r.Err
}
var response struct {
User users.User `mapstructure:"user"`
}
err := mapstructure.Decode(r.Body, &response)
return &response.User, err
}
// ActionResult represents the result of action requests, such as: restarting
// an instance service, resizing its memory allocation, and resizing its
// attached volume size.
type ActionResult struct {
gophercloud.ErrResult
}

View File

@ -0,0 +1,19 @@
package instances
import "github.com/rackspace/gophercloud"
func baseURL(c *gophercloud.ServiceClient) string {
return c.ServiceURL("instances")
}
func resourceURL(c *gophercloud.ServiceClient, id string) string {
return c.ServiceURL("instances", id)
}
func userRootURL(c *gophercloud.ServiceClient, id string) string {
return c.ServiceURL("instances", id, "root")
}
func actionURL(c *gophercloud.ServiceClient, id string) string {
return c.ServiceURL("instances", id, "action")
}

View File

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

View File

@ -0,0 +1,37 @@
package users
import (
"fmt"
"testing"
"github.com/rackspace/gophercloud/testhelper/fixture"
)
const user1 = `
{"databases": [{"name": "databaseA"}],"name": "dbuser3"%s}
`
const user2 = `
{"databases": [{"name": "databaseB"},{"name": "databaseC"}],"name": "dbuser4"%s}
`
var (
instanceID = "{instanceID}"
_rootURL = "/instances/" + instanceID + "/users"
pUser1 = fmt.Sprintf(user1, `,"password":"secretsecret"`)
pUser2 = fmt.Sprintf(user2, `,"password":"secretsecret"`)
createReq = fmt.Sprintf(`{"users":[%s, %s]}`, pUser1, pUser2)
listResp = fmt.Sprintf(`{"users":[%s, %s]}`, fmt.Sprintf(user1, ""), fmt.Sprintf(user2, ""))
)
func HandleCreate(t *testing.T) {
fixture.SetupHandler(t, _rootURL, "POST", createReq, "", 202)
}
func HandleList(t *testing.T) {
fixture.SetupHandler(t, _rootURL, "GET", "", listResp, 200)
}
func HandleDelete(t *testing.T) {
fixture.SetupHandler(t, _rootURL+"/{userName}", "DELETE", "", "", 202)
}

View File

@ -0,0 +1,132 @@
package users
import (
"errors"
"github.com/rackspace/gophercloud"
db "github.com/rackspace/gophercloud/openstack/db/v1/databases"
"github.com/rackspace/gophercloud/pagination"
)
// CreateOptsBuilder is the top-level interface for creating JSON maps.
type CreateOptsBuilder interface {
ToUserCreateMap() (map[string]interface{}, error)
}
// CreateOpts is the struct responsible for configuring a new user; often in the
// context of an instance.
type CreateOpts struct {
// [REQUIRED] 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
// [REQUIRED] Specifies a password for the user.
Password string
// [OPTIONAL] An array of databases that this user will connect to. The
// "name" field is the only requirement for each option.
Databases db.BatchCreateOpts
// [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 CreateOpts) ToMap() (map[string]interface{}, error) {
if opts.Name == "root" {
return nil, errors.New("root is a reserved user name and cannot be used")
}
if opts.Name == "" {
return nil, errors.New("Name is a required field")
}
if opts.Password == "" {
return nil, errors.New("Password is a required field")
}
user := map[string]interface{}{
"name": opts.Name,
"password": opts.Password,
}
if opts.Host != "" {
user["host"] = opts.Host
}
dbs := make([]map[string]string, len(opts.Databases))
for i, db := range opts.Databases {
dbs[i] = map[string]string{"name": db.Name}
}
if len(dbs) > 0 {
user["databases"] = dbs
}
return user, nil
}
// BatchCreateOpts allows multiple users to be created at once.
type BatchCreateOpts []CreateOpts
// ToUserCreateMap will generate a JSON map.
func (opts BatchCreateOpts) ToUserCreateMap() (map[string]interface{}, error) {
users := make([]map[string]interface{}, len(opts))
for i, opt := range opts {
user, err := opt.ToMap()
if err != nil {
return nil, err
}
users[i] = user
}
return map[string]interface{}{"users": users}, nil
}
// Create asynchronously provisions a new user for the specified database
// instance based on the configuration defined in CreateOpts. If databases are
// assigned for a particular user, the user will be granted all privileges
// for those specified databases. "root" is a reserved name and cannot be used.
func Create(client *gophercloud.ServiceClient, instanceID string, opts CreateOptsBuilder) CreateResult {
var res CreateResult
reqBody, err := opts.ToUserCreateMap()
if err != nil {
res.Err = err
return res
}
_, res.Err = client.Request("POST", baseURL(client, instanceID), gophercloud.RequestOpts{
JSONBody: &reqBody,
OkCodes: []int{202},
})
return res
}
// List will list all the users associated with a specified database instance,
// along with their associated databases. This operation will not return any
// system users or administrators for a database.
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)
}
// Delete will permanently delete a user from a specified database instance.
func Delete(client *gophercloud.ServiceClient, instanceID, userName string) DeleteResult {
var res DeleteResult
_, res.Err = client.Request("DELETE", userURL(client, instanceID, userName), gophercloud.RequestOpts{
OkCodes: []int{202},
})
return res
}

View File

@ -0,0 +1,73 @@
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
// The databases associated with this user
Databases []db.Database
}
// 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
}
// 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
}

View File

@ -0,0 +1,11 @@
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)
}

View File

@ -10,6 +10,7 @@ import (
"github.com/rackspace/gophercloud/openstack/identity/v2/tenants"
th "github.com/rackspace/gophercloud/testhelper"
thclient "github.com/rackspace/gophercloud/testhelper/client"
)
// ExpectedToken is the token that should be parsed from TokenCreationResponse.
@ -54,6 +55,14 @@ var ExpectedServiceCatalog = &ServiceCatalog{
},
}
// ExpectedUser is the token that should be parsed from TokenGetResponse.
var ExpectedUser = &User{
ID: "a530fefc3d594c4ba2693a4ecd6be74e",
Name: "apiserver",
Roles: []Role{{"member"}, {"service"}},
UserName: "apiserver",
}
// TokenCreationResponse is a JSON response that contains ExpectedToken and ExpectedServiceCatalog.
const TokenCreationResponse = `
{
@ -99,6 +108,39 @@ const TokenCreationResponse = `
}
`
// TokenGetResponse is a JSON response that contains ExpectedToken and ExpectedUser.
const TokenGetResponse = `
{
"access": {
"token": {
"issued_at": "2014-01-30T15:30:58.000000Z",
"expires": "2014-01-31T15:30:58Z",
"id": "aaaabbbbccccdddd",
"tenant": {
"description": "There are many tenants. This one is yours.",
"enabled": true,
"id": "fc394f2ab2df4114bde39905f800dc57",
"name": "test"
}
},
"serviceCatalog": [],
"user": {
"id": "a530fefc3d594c4ba2693a4ecd6be74e",
"name": "apiserver",
"roles": [
{
"name": "member"
},
{
"name": "service"
}
],
"roles_links": [],
"username": "apiserver"
}
}
}`
// HandleTokenPost expects a POST against a /tokens handler, ensures that the request body has been
// constructed properly given certain auth options, and returns the result.
func HandleTokenPost(t *testing.T, requestJSON string) {
@ -115,6 +157,19 @@ func HandleTokenPost(t *testing.T, requestJSON string) {
})
}
// HandleTokenGet expects a Get against a /tokens handler, ensures that the request body has been
// constructed properly given certain auth options, and returns the result.
func HandleTokenGet(t *testing.T, token string) {
th.Mux.HandleFunc("/tokens/"+token, func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "GET")
th.TestHeader(t, r, "Accept", "application/json")
th.TestHeader(t, r, "X-Auth-Token", thclient.TokenID)
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, TokenGetResponse)
})
}
// IsSuccessful ensures that a CreateResult was successful and contains the correct token and
// service catalog.
func IsSuccessful(t *testing.T, result CreateResult) {
@ -126,3 +181,15 @@ func IsSuccessful(t *testing.T, result CreateResult) {
th.AssertNoErr(t, err)
th.CheckDeepEquals(t, ExpectedServiceCatalog, serviceCatalog)
}
// GetIsSuccessful ensures that a GetResult was successful and contains the correct token and
// User Info.
func GetIsSuccessful(t *testing.T, result GetResult) {
token, err := result.ExtractToken()
th.AssertNoErr(t, err)
th.CheckDeepEquals(t, ExpectedToken, token)
user, err := result.ExtractUser()
th.AssertNoErr(t, err)
th.CheckDeepEquals(t, ExpectedUser, user)
}

View File

@ -88,3 +88,12 @@ func Create(client *gophercloud.ServiceClient, auth AuthOptionsBuilder) CreateRe
})
return result
}
// Validates and retrieves information for user's token.
func Get(client *gophercloud.ServiceClient, token string) GetResult {
var result GetResult
_, result.Err = client.Get(GetURL(client, token), &result.Body, &gophercloud.RequestOpts{
OkCodes: []int{200, 203},
})
return result
}

View File

@ -25,6 +25,17 @@ type Token struct {
Tenant tenants.Tenant
}
// Authorization need user info which can get from token authentication's response
type Role struct {
Name string `mapstructure:"name"`
}
type User struct {
ID string `mapstructure:"id"`
Name string `mapstructure:"name"`
UserName string `mapstructure:"username"`
Roles []Role `mapstructure:"roles"`
}
// Endpoint represents a single API endpoint offered by a service.
// It provides the public and internal URLs, if supported, along with a region specifier, again if provided.
// The significance of the Region field will depend upon your provider.
@ -74,6 +85,12 @@ type CreateResult struct {
gophercloud.Result
}
// GetResult is the deferred response from a Get call, which is the same with a Created token.
// Use ExtractUser() to interpret it as a User.
type GetResult struct {
CreateResult
}
// ExtractToken returns the just-created Token from a CreateResult.
func (result CreateResult) ExtractToken() (*Token, error) {
if result.Err != nil {
@ -131,3 +148,23 @@ func (result CreateResult) ExtractServiceCatalog() (*ServiceCatalog, error) {
func createErr(err error) CreateResult {
return CreateResult{gophercloud.Result{Err: err}}
}
// ExtractUser returns the User from a GetResult.
func (result GetResult) ExtractUser() (*User, error) {
if result.Err != nil {
return nil, result.Err
}
var response struct {
Access struct {
User User `mapstructure:"user"`
} `mapstructure:"access"`
}
err := mapstructure.Decode(result.Body, &response)
if err != nil {
return nil, err
}
return &response.Access.User, nil
}

View File

@ -6,3 +6,8 @@ import "github.com/rackspace/gophercloud"
func CreateURL(client *gophercloud.ServiceClient) string {
return client.ServiceURL("tokens")
}
// GetURL generates the URL used to Validate Tokens.
func GetURL(client *gophercloud.ServiceClient, token string) string {
return client.ServiceURL("tokens", token)
}

View File

@ -15,9 +15,9 @@ type Scope struct {
}
func subjectTokenHeaders(c *gophercloud.ServiceClient, subjectToken string) map[string]string {
h := c.AuthenticatedHeaders()
h["X-Subject-Token"] = subjectToken
return h
return map[string]string{
"X-Subject-Token": subjectToken,
}
}
// Create authenticates and either generates a new token, or changes the Scope of an existing token.

View File

@ -102,6 +102,7 @@ func Create(c *gophercloud.ServiceClient, opts CreateOpts) CreateResult {
// Populate request body
reqBody := request{FloatingIP: floatingIP{
FloatingNetworkID: opts.FloatingNetworkID,
FloatingIP: opts.FloatingIP,
PortID: opts.PortID,
FixedIP: opts.FixedIP,
TenantID: opts.TenantID,

View File

@ -16,6 +16,7 @@ type ListOpts struct {
ID string `q:"id"`
Name string `q:"name"`
AdminStateUp *bool `q:"admin_state_up"`
Distributed *bool `q:"distributed"`
Status string `q:"status"`
TenantID string `q:"tenant_id"`
Limit int `q:"limit"`
@ -46,6 +47,7 @@ func List(c *gophercloud.ServiceClient, opts ListOpts) pagination.Pager {
type CreateOpts struct {
Name string
AdminStateUp *bool
Distributed *bool
TenantID string
GatewayInfo *GatewayInfo
}
@ -62,6 +64,7 @@ func Create(c *gophercloud.ServiceClient, opts CreateOpts) CreateResult {
type router struct {
Name *string `json:"name,omitempty"`
AdminStateUp *bool `json:"admin_state_up,omitempty"`
Distributed *bool `json:"distributed,omitempty"`
TenantID *string `json:"tenant_id,omitempty"`
GatewayInfo *GatewayInfo `json:"external_gateway_info,omitempty"`
}
@ -73,6 +76,7 @@ func Create(c *gophercloud.ServiceClient, opts CreateOpts) CreateResult {
reqBody := request{Router: router{
Name: gophercloud.MaybeString(opts.Name),
AdminStateUp: opts.AdminStateUp,
Distributed: opts.Distributed,
TenantID: gophercloud.MaybeString(opts.TenantID),
}}
@ -96,7 +100,9 @@ func Get(c *gophercloud.ServiceClient, id string) GetResult {
type UpdateOpts struct {
Name string
AdminStateUp *bool
Distributed *bool
GatewayInfo *GatewayInfo
Routes []Route
}
// Update allows routers to be updated. You can update the name, administrative
@ -108,7 +114,9 @@ func Update(c *gophercloud.ServiceClient, id string, opts UpdateOpts) UpdateResu
type router struct {
Name *string `json:"name,omitempty"`
AdminStateUp *bool `json:"admin_state_up,omitempty"`
Distributed *bool `json:"distributed,omitempty"`
GatewayInfo *GatewayInfo `json:"external_gateway_info,omitempty"`
Routes []Route `json:"routes"`
}
type request struct {
@ -118,12 +126,17 @@ func Update(c *gophercloud.ServiceClient, id string, opts UpdateOpts) UpdateResu
reqBody := request{Router: router{
Name: gophercloud.MaybeString(opts.Name),
AdminStateUp: opts.AdminStateUp,
Distributed: opts.Distributed,
}}
if opts.GatewayInfo != nil {
reqBody.Router.GatewayInfo = opts.GatewayInfo
}
if opts.Routes != nil {
reqBody.Router.Routes = opts.Routes
}
// Send request to API
var res UpdateResult
_, res.Err = c.Put(resourceURL(c, id), reqBody, &res.Body, &gophercloud.RequestOpts{

View File

@ -12,6 +12,11 @@ type GatewayInfo struct {
NetworkID string `json:"network_id" mapstructure:"network_id"`
}
type Route struct {
NextHop string `mapstructure:"nexthop" json:"nexthop"`
DestinationCIDR string `mapstructure:"destination" json:"destination"`
}
// Router represents a Neutron router. A router is a logical entity that
// forwards packets across internal subnets and NATs (network address
// translation) them on external networks through an appropriate gateway.
@ -30,6 +35,9 @@ type Router struct {
// Administrative state of the router.
AdminStateUp bool `json:"admin_state_up" mapstructure:"admin_state_up"`
// Whether router is disitrubted or not..
Distributed bool `json:"distributed" mapstructure:"distributed"`
// Human readable name for the router. Does not have to be unique.
Name string `json:"name" mapstructure:"name"`
@ -39,6 +47,8 @@ type Router struct {
// Owner of the router. Only admin users can specify a tenant identifier
// other than its own.
TenantID string `json:"tenant_id" mapstructure:"tenant_id"`
Routes []Route `json:"routes" mapstructure:"routes"`
}
// RouterPage is the page returned by a pager when traversing over a

View File

@ -104,8 +104,8 @@ type CreateOpts struct {
TenantID string
}
// Create is an operation which provisions a new security group with default
// security group rules for the IPv4 and IPv6 ether types.
// Create is an operation which adds a new security group rule and associates it
// with an existing security group (whose ID is specified in CreateOpts).
func Create(c *gophercloud.ServiceClient, opts CreateOpts) CreateResult {
var res CreateResult
@ -159,14 +159,14 @@ func Create(c *gophercloud.ServiceClient, opts CreateOpts) CreateResult {
return res
}
// Get retrieves a particular security group based on its unique ID.
// Get retrieves a particular security group rule based on its unique ID.
func Get(c *gophercloud.ServiceClient, id string) GetResult {
var res GetResult
_, res.Err = c.Get(resourceURL(c, id), &res.Body, nil)
return res
}
// Delete will permanently delete a particular security group based on its unique ID.
// Delete will permanently delete a particular security group rule based on its unique ID.
func Delete(c *gophercloud.ServiceClient, id string) DeleteResult {
var res DeleteResult
_, res.Err = c.Delete(resourceURL(c, id), nil)

View File

@ -95,15 +95,16 @@ type CreateOptsBuilder interface {
// CreateOpts represents the attributes used when creating a new port.
type CreateOpts struct {
NetworkID string
Name string
AdminStateUp *bool
MACAddress string
FixedIPs interface{}
DeviceID string
DeviceOwner string
TenantID string
SecurityGroups []string
NetworkID string
Name string
AdminStateUp *bool
MACAddress string
FixedIPs interface{}
DeviceID string
DeviceOwner string
TenantID string
SecurityGroups []string
AllowedAddressPairs []AddressPair
}
// ToPortCreateMap casts a CreateOpts struct to a map.
@ -139,6 +140,9 @@ func (opts CreateOpts) ToPortCreateMap() (map[string]interface{}, error) {
if opts.MACAddress != "" {
p["mac_address"] = opts.MACAddress
}
if opts.AllowedAddressPairs != nil {
p["allowed_address_pairs"] = opts.AllowedAddressPairs
}
return map[string]interface{}{"port": p}, nil
}
@ -168,12 +172,13 @@ type UpdateOptsBuilder interface {
// UpdateOpts represents the attributes used when updating an existing port.
type UpdateOpts struct {
Name string
AdminStateUp *bool
FixedIPs interface{}
DeviceID string
DeviceOwner string
SecurityGroups []string
Name string
AdminStateUp *bool
FixedIPs interface{}
DeviceID string
DeviceOwner string
SecurityGroups []string
AllowedAddressPairs []AddressPair
}
// ToPortUpdateMap casts an UpdateOpts struct to a map.
@ -198,6 +203,9 @@ func (opts UpdateOpts) ToPortUpdateMap() (map[string]interface{}, error) {
if opts.Name != "" {
p["name"] = opts.Name
}
if opts.AllowedAddressPairs != nil {
p["allowed_address_pairs"] = opts.AllowedAddressPairs
}
return map[string]interface{}{"port": p}, nil
}

View File

@ -19,7 +19,6 @@ func (r commonResult) Extract() (*Port, error) {
var res struct {
Port *Port `json:"port"`
}
err := mapstructure.Decode(r.Body, &res)
return res.Port, err
@ -51,6 +50,11 @@ type IP struct {
IPAddress string `mapstructure:"ip_address" json:"ip_address,omitempty"`
}
type AddressPair struct {
IPAddress string `mapstructure:"ip_address" json:"ip_address,omitempty"`
MACAddress string `mapstructure:"mac_address" json:"mac_address,omitempty"`
}
// Port represents a Neutron port. See package documentation for a top-level
// description of what this is.
type Port struct {
@ -78,6 +82,8 @@ type Port struct {
SecurityGroups []string `mapstructure:"security_groups" json:"security_groups"`
// Identifies the device (e.g., virtual server) using this port.
DeviceID string `mapstructure:"device_id" json:"device_id"`
// Identifies the list of IP addresses the port will recognize/accept
AllowedAddressPairs []AddressPair `mapstructure:"allowed_address_pairs" json:"allowed_address_pairs"`
}
// PortPage is the page returned by a pager when traversing over a collection

View File

@ -1,12 +1,13 @@
package objects
import (
"bytes"
"bufio"
"crypto/hmac"
"crypto/md5"
"crypto/sha1"
"fmt"
"io"
"io/ioutil"
"strings"
"time"
@ -167,7 +168,7 @@ type CreateOpts struct {
ObjectManifest string `h:"X-Object-Manifest"`
TransferEncoding string `h:"Transfer-Encoding"`
Expires string `q:"expires"`
MultipartManifest string `q:"multiple-manifest"`
MultipartManifest string `q:"multipart-manifest"`
Signature string `q:"signature"`
}
@ -213,19 +214,20 @@ func Create(c *gophercloud.ServiceClient, containerName, objectName string, cont
}
hash := md5.New()
bufioReader := bufio.NewReader(io.TeeReader(content, hash))
io.Copy(ioutil.Discard, bufioReader)
localChecksum := hash.Sum(nil)
contentBuffer := bytes.NewBuffer([]byte{})
_, err := io.Copy(contentBuffer, io.TeeReader(content, hash))
h["ETag"] = fmt.Sprintf("%x", localChecksum)
_, err := content.Seek(0, 0)
if err != nil {
res.Err = err
return res
}
localChecksum := hash.Sum(nil)
h["ETag"] = fmt.Sprintf("%x", localChecksum)
ropts := gophercloud.RequestOpts{
RawBody: strings.NewReader(contentBuffer.String()),
RawBody: content,
MoreHeaders: h,
}

View File

@ -163,8 +163,8 @@ type ListResourceEventsOpts struct {
SortDir SortDir `q:"sort_dir"`
}
// ToResourceEventsListQuery formats a ListOpts into a query string.
func (opts ListOpts) ToResourceEventsListQuery() (string, error) {
// ToResourceEventListQuery formats a ListResourceEventsOpts into a query string.
func (opts ListResourceEventsOpts) ToResourceEventListQuery() (string, error) {
q, err := gophercloud.BuildQueryString(opts)
if err != nil {
return "", err

View File

@ -28,10 +28,13 @@ var FindExpected = []Resource{
LogicalID: "hello_world",
StatusReason: "state changed",
UpdatedTime: time.Date(2015, 2, 5, 21, 33, 11, 0, time.UTC),
CreationTime: time.Date(2015, 2, 5, 21, 33, 10, 0, time.UTC),
RequiredBy: []interface{}{},
Status: "CREATE_IN_PROGRESS",
PhysicalID: "49181cd6-169a-4130-9455-31185bbfc5bf",
Type: "OS::Nova::Server",
Attributes: map[string]interface{}{"SXSW": "atx"},
Description: "Some resource",
},
}
@ -40,6 +43,8 @@ const FindOutput = `
{
"resources": [
{
"description": "Some resource",
"attributes": {"SXSW": "atx"},
"resource_name": "hello_world",
"links": [
{
@ -54,6 +59,7 @@ const FindOutput = `
"logical_resource_id": "hello_world",
"resource_status_reason": "state changed",
"updated_time": "2015-02-05T21:33:11",
"creation_time": "2015-02-05T21:33:10",
"required_by": [],
"resource_status": "CREATE_IN_PROGRESS",
"physical_resource_id": "49181cd6-169a-4130-9455-31185bbfc5bf",
@ -93,10 +99,13 @@ var ListExpected = []Resource{
LogicalID: "hello_world",
StatusReason: "state changed",
UpdatedTime: time.Date(2015, 2, 5, 21, 33, 11, 0, time.UTC),
CreationTime: time.Date(2015, 2, 5, 21, 33, 10, 0, time.UTC),
RequiredBy: []interface{}{},
Status: "CREATE_IN_PROGRESS",
PhysicalID: "49181cd6-169a-4130-9455-31185bbfc5bf",
Type: "OS::Nova::Server",
Attributes: map[string]interface{}{"SXSW": "atx"},
Description: "Some resource",
},
}
@ -121,7 +130,10 @@ const ListOutput = `{
"required_by": [],
"resource_status": "CREATE_IN_PROGRESS",
"physical_resource_id": "49181cd6-169a-4130-9455-31185bbfc5bf",
"resource_type": "OS::Nova::Server"
"creation_time": "2015-02-05T21:33:10",
"resource_type": "OS::Nova::Server",
"attributes": {"SXSW": "atx"},
"description": "Some resource"
}
]
}`
@ -162,6 +174,7 @@ var GetExpected = &Resource{
},
},
LogicalID: "wordpress_instance",
Attributes: map[string]interface{}{"SXSW": "atx"},
StatusReason: "state changed",
UpdatedTime: time.Date(2014, 12, 10, 18, 34, 35, 0, time.UTC),
RequiredBy: []interface{}{},
@ -174,6 +187,8 @@ var GetExpected = &Resource{
const GetOutput = `
{
"resource": {
"description": "Some resource",
"attributes": {"SXSW": "atx"},
"resource_name": "wordpress_instance",
"description": "",
"links": [
@ -240,7 +255,7 @@ func HandleMetadataSuccessfully(t *testing.T, output string) {
}
// ListTypesExpected represents the expected object from a ListTypes request.
var ListTypesExpected = []string{
var ListTypesExpected = ResourceTypes{
"OS::Nova::Server",
"OS::Heat::RandomString",
"OS::Swift::Container",
@ -251,6 +266,18 @@ var ListTypesExpected = []string{
"OS::Nova::KeyPair",
}
// same as above, but sorted
var SortedListTypesExpected = ResourceTypes{
"OS::Cinder::VolumeAttachment",
"OS::Heat::RandomString",
"OS::Nova::FloatingIP",
"OS::Nova::FloatingIPAssociation",
"OS::Nova::KeyPair",
"OS::Nova::Server",
"OS::Swift::Container",
"OS::Trove::Instance",
}
// ListTypesOutput represents the response body from a ListTypes request.
const ListTypesOutput = `
{
@ -296,6 +323,11 @@ var GetSchemaExpected = &TypeSchema{
},
},
ResourceType: "OS::Heat::AResourceName",
SupportStatus: map[string]interface{}{
"message": "A status message",
"status": "SUPPORTED",
"version": "2014.1",
},
}
// GetSchemaOutput represents the response body from a Schema request.
@ -314,7 +346,12 @@ const GetSchemaOutput = `
"description": "A resource description."
}
},
"resource_type": "OS::Heat::AResourceName"
"resource_type": "OS::Heat::AResourceName",
"support_status": {
"message": "A status message",
"status": "SUPPORTED",
"version": "2014.1"
}
}`
// HandleGetSchemaSuccessfully creates an HTTP handler at `/resource_types/OS::Heat::AResourceName`
@ -332,56 +369,7 @@ func HandleGetSchemaSuccessfully(t *testing.T, output string) {
}
// GetTemplateExpected represents the expected object from a Template request.
var GetTemplateExpected = &TypeTemplate{
HeatTemplateFormatVersion: "2012-12-12",
Outputs: map[string]interface{}{
"private_key": map[string]interface{}{
"Description": "The private key if it has been saved.",
"Value": "{\"Fn::GetAtt\": [\"KeyPair\", \"private_key\"]}",
},
"public_key": map[string]interface{}{
"Description": "The public key.",
"Value": "{\"Fn::GetAtt\": [\"KeyPair\", \"public_key\"]}",
},
},
Parameters: map[string]interface{}{
"name": map[string]interface{}{
"Description": "The name of the key pair.",
"Type": "String",
},
"public_key": map[string]interface{}{
"Description": "The optional public key. This allows users to supply the public key from a pre-existing key pair. If not supplied, a new key pair will be generated.",
"Type": "String",
},
"save_private_key": map[string]interface{}{
"AllowedValues": []string{
"True",
"true",
"False",
"false",
},
"Default": false,
"Description": "True if the system should remember a generated private key; False otherwise.",
"Type": "String",
},
},
Resources: map[string]interface{}{
"KeyPair": map[string]interface{}{
"Properties": map[string]interface{}{
"name": map[string]interface{}{
"Ref": "name",
},
"public_key": map[string]interface{}{
"Ref": "public_key",
},
"save_private_key": map[string]interface{}{
"Ref": "save_private_key",
},
},
"Type": "OS::Nova::KeyPair",
},
},
}
var GetTemplateExpected = "{\n \"HeatTemplateFormatVersion\": \"2012-12-12\",\n \"Outputs\": {\n \"private_key\": {\n \"Description\": \"The private key if it has been saved.\",\n \"Value\": \"{\\\"Fn::GetAtt\\\": [\\\"KeyPair\\\", \\\"private_key\\\"]}\"\n },\n \"public_key\": {\n \"Description\": \"The public key.\",\n \"Value\": \"{\\\"Fn::GetAtt\\\": [\\\"KeyPair\\\", \\\"public_key\\\"]}\"\n }\n },\n \"Parameters\": {\n \"name\": {\n \"Description\": \"The name of the key pair.\",\n \"Type\": \"String\"\n },\n \"public_key\": {\n \"Description\": \"The optional public key. This allows users to supply the public key from a pre-existing key pair. If not supplied, a new key pair will be generated.\",\n \"Type\": \"String\"\n },\n \"save_private_key\": {\n \"AllowedValues\": [\n \"True\",\n \"true\",\n \"False\",\n \"false\"\n ],\n \"Default\": false,\n \"Description\": \"True if the system should remember a generated private key; False otherwise.\",\n \"Type\": \"String\"\n }\n },\n \"Resources\": {\n \"KeyPair\": {\n \"Properties\": {\n \"name\": {\n \"Ref\": \"name\"\n },\n \"public_key\": {\n \"Ref\": \"public_key\"\n },\n \"save_private_key\": {\n \"Ref\": \"save_private_key\"\n }\n },\n \"Type\": \"OS::Nova::KeyPair\"\n }\n }\n}"
// GetTemplateOutput represents the response body from a Template request.
const GetTemplateOutput = `

View File

@ -1,6 +1,7 @@
package stackresources
import (
"encoding/json"
"fmt"
"reflect"
"time"
@ -12,15 +13,18 @@ import (
// Resource represents a stack resource.
type Resource struct {
Links []gophercloud.Link `mapstructure:"links"`
LogicalID string `mapstructure:"logical_resource_id"`
Name string `mapstructure:"resource_name"`
PhysicalID string `mapstructure:"physical_resource_id"`
RequiredBy []interface{} `mapstructure:"required_by"`
Status string `mapstructure:"resource_status"`
StatusReason string `mapstructure:"resource_status_reason"`
Type string `mapstructure:"resource_type"`
UpdatedTime time.Time `mapstructure:"-"`
Attributes map[string]interface{} `mapstructure:"attributes"`
CreationTime time.Time `mapstructure:"-"`
Description string `mapstructure:"description"`
Links []gophercloud.Link `mapstructure:"links"`
LogicalID string `mapstructure:"logical_resource_id"`
Name string `mapstructure:"resource_name"`
PhysicalID string `mapstructure:"physical_resource_id"`
RequiredBy []interface{} `mapstructure:"required_by"`
Status string `mapstructure:"resource_status"`
StatusReason string `mapstructure:"resource_status_reason"`
Type string `mapstructure:"resource_type"`
UpdatedTime time.Time `mapstructure:"-"`
}
// FindResult represents the result of a Find operation.
@ -54,6 +58,13 @@ func (r FindResult) Extract() ([]Resource, error) {
}
res.Res[i].UpdatedTime = t
}
if date, ok := resource["creation_time"]; ok && date != nil {
t, err := time.Parse(gophercloud.STACK_TIME_FMT, date.(string))
if err != nil {
return nil, err
}
res.Res[i].CreationTime = t
}
}
return res.Res, nil
@ -75,18 +86,6 @@ func (r ResourcePage) IsEmpty() (bool, error) {
return len(resources) == 0, nil
}
// LastMarker returns the last container name in a ListResult.
func (r ResourcePage) LastMarker() (string, error) {
resources, err := ExtractResources(r)
if err != nil {
return "", err
}
if len(resources) == 0 {
return "", nil
}
return resources[len(resources)-1].PhysicalID, nil
}
// ExtractResources interprets the results of a single page from a List() call, producing a slice of Resource entities.
func ExtractResources(page pagination.Page) ([]Resource, error) {
casted := page.(ResourcePage).Body
@ -94,8 +93,9 @@ func ExtractResources(page pagination.Page) ([]Resource, error) {
var response struct {
Resources []Resource `mapstructure:"resources"`
}
err := mapstructure.Decode(casted, &response)
if err := mapstructure.Decode(casted, &response); err != nil {
return nil, err
}
var resources []interface{}
switch casted.(type) {
case map[string]interface{}:
@ -115,9 +115,16 @@ func ExtractResources(page pagination.Page) ([]Resource, error) {
}
response.Resources[i].UpdatedTime = t
}
if date, ok := resource["creation_time"]; ok && date != nil {
t, err := time.Parse(gophercloud.STACK_TIME_FMT, date.(string))
if err != nil {
return nil, err
}
response.Resources[i].CreationTime = t
}
}
return response.Resources, err
return response.Resources, nil
}
// GetResult represents the result of a Get operation.
@ -149,6 +156,13 @@ func (r GetResult) Extract() (*Resource, error) {
}
res.Res.UpdatedTime = t
}
if date, ok := resource["creation_time"]; ok && date != nil {
t, err := time.Parse(gophercloud.STACK_TIME_FMT, date.(string))
if err != nil {
return nil, err
}
res.Res.CreationTime = t
}
return res.Res, nil
}
@ -192,21 +206,42 @@ func (r ResourceTypePage) IsEmpty() (bool, error) {
return len(rts) == 0, nil
}
// ResourceTypes represents the type that holds the result of ExtractResourceTypes.
// We define methods on this type to sort it before output
type ResourceTypes []string
func (r ResourceTypes) Len() int {
return len(r)
}
func (r ResourceTypes) Swap(i, j int) {
r[i], r[j] = r[j], r[i]
}
func (r ResourceTypes) Less(i, j int) bool {
return r[i] < r[j]
}
// ExtractResourceTypes extracts and returns resource types.
func ExtractResourceTypes(page pagination.Page) ([]string, error) {
func ExtractResourceTypes(page pagination.Page) (ResourceTypes, error) {
casted := page.(ResourceTypePage).Body
var response struct {
ResourceTypes []string `mapstructure:"resource_types"`
ResourceTypes ResourceTypes `mapstructure:"resource_types"`
}
err := mapstructure.Decode(page.(ResourceTypePage).Body, &response)
return response.ResourceTypes, err
if err := mapstructure.Decode(casted, &response); err != nil {
return nil, err
}
return response.ResourceTypes, nil
}
// TypeSchema represents a stack resource schema.
type TypeSchema struct {
Attributes map[string]interface{} `mapstructure:"attributes"`
Properties map[string]interface{} `mapstrucutre:"properties"`
ResourceType string `mapstructure:"resource_type"`
Attributes map[string]interface{} `mapstructure:"attributes"`
Properties map[string]interface{} `mapstrucutre:"properties"`
ResourceType string `mapstructure:"resource_type"`
SupportStatus map[string]interface{} `mapstructure:"support_status"`
}
// SchemaResult represents the result of a Schema operation.
@ -230,31 +265,20 @@ func (r SchemaResult) Extract() (*TypeSchema, error) {
return &res, nil
}
// TypeTemplate represents a stack resource template.
type TypeTemplate struct {
HeatTemplateFormatVersion string
Outputs map[string]interface{}
Parameters map[string]interface{}
Resources map[string]interface{}
}
// TemplateResult represents the result of a Template operation.
type TemplateResult struct {
gophercloud.Result
}
// Extract returns a pointer to a TypeTemplate object and is called after a
// Extract returns the template and is called after a
// Template operation.
func (r TemplateResult) Extract() (*TypeTemplate, error) {
func (r TemplateResult) Extract() ([]byte, error) {
if r.Err != nil {
return nil, r.Err
}
var res TypeTemplate
if err := mapstructure.Decode(r.Body, &res); err != nil {
template, err := json.MarshalIndent(r.Body, "", " ")
if err != nil {
return nil, err
}
return &res, nil
return template, nil
}

View File

@ -0,0 +1,137 @@
package stacks
import (
"fmt"
"strings"
)
// Environment is a structure that represents stack environments
type Environment struct {
TE
}
// EnvironmentSections is a map containing allowed sections in a stack environment file
var EnvironmentSections = map[string]bool{
"parameters": true,
"parameter_defaults": true,
"resource_registry": true,
}
// Validate validates the contents of the Environment
func (e *Environment) Validate() error {
if e.Parsed == nil {
if err := e.Parse(); err != nil {
return err
}
}
for key := range e.Parsed {
if _, ok := EnvironmentSections[key]; !ok {
return fmt.Errorf("Environment has wrong section: %s", key)
}
}
return nil
}
// Parse environment file to resolve the URL's of the resources. This is done by
// reading from the `Resource Registry` section, which is why the function is
// named GetRRFileContents.
func (e *Environment) getRRFileContents(ignoreIf igFunc) error {
// initialize environment if empty
if e.Files == nil {
e.Files = make(map[string]string)
}
if e.fileMaps == nil {
e.fileMaps = make(map[string]string)
}
// get the resource registry
rr := e.Parsed["resource_registry"]
// search the resource registry for URLs
switch rr.(type) {
// process further only if the resource registry is a map
case map[string]interface{}, map[interface{}]interface{}:
rrMap, err := toStringKeys(rr)
if err != nil {
return err
}
// the resource registry might contain a base URL for the resource. If
// such a field is present, use it. Otherwise, use the default base URL.
var baseURL string
if val, ok := rrMap["base_url"]; ok {
baseURL = val.(string)
} else {
baseURL = e.baseURL
}
// The contents of the resource may be located in a remote file, which
// will be a template. Instantiate a temporary template to manage the
// contents.
tempTemplate := new(Template)
tempTemplate.baseURL = baseURL
tempTemplate.client = e.client
// Fetch the contents of remote resource URL's
if err = tempTemplate.getFileContents(rr, ignoreIf, false); err != nil {
return err
}
// check the `resources` section (if it exists) for more URL's. Note that
// the previous call to GetFileContents was (deliberately) not recursive
// as we want more control over where to look for URL's
if val, ok := rrMap["resources"]; ok {
switch val.(type) {
// process further only if the contents are a map
case map[string]interface{}, map[interface{}]interface{}:
resourcesMap, err := toStringKeys(val)
if err != nil {
return err
}
for _, v := range resourcesMap {
switch v.(type) {
case map[string]interface{}, map[interface{}]interface{}:
resourceMap, err := toStringKeys(v)
if err != nil {
return err
}
var resourceBaseURL string
// if base_url for the resource type is defined, use it
if val, ok := resourceMap["base_url"]; ok {
resourceBaseURL = val.(string)
} else {
resourceBaseURL = baseURL
}
tempTemplate.baseURL = resourceBaseURL
if err := tempTemplate.getFileContents(v, ignoreIf, false); err != nil {
return err
}
}
}
}
}
// if the resource registry contained any URL's, store them. This can
// then be passed as parameter to api calls to Heat api.
e.Files = tempTemplate.Files
return nil
default:
return nil
}
}
// function to choose keys whose values are other environment files
func ignoreIfEnvironment(key string, value interface{}) bool {
// base_url and hooks refer to components which cannot have urls
if key == "base_url" || key == "hooks" {
return true
}
// if value is not string, it cannot be a URL
valueString, ok := value.(string)
if !ok {
return true
}
// if value contains `::`, it must be a reference to another resource type
// e.g. OS::Nova::Server : Rackspace::Cloud::Server
if strings.Contains(valueString, "::") {
return true
}
return false
}

View File

@ -63,6 +63,7 @@ var ListExpected = []ListedStack{
CreationTime: time.Date(2015, 2, 3, 20, 7, 39, 0, time.UTC),
Status: "CREATE_COMPLETE",
ID: "16ef0584-4458-41eb-87c8-0dc8d5f66c87",
Tags: []string{"rackspace", "atx"},
},
ListedStack{
Description: "Simple template to test heat commands",
@ -78,6 +79,7 @@ var ListExpected = []ListedStack{
UpdatedTime: time.Date(2014, 12, 11, 17, 40, 37, 0, time.UTC),
Status: "UPDATE_COMPLETE",
ID: "db6977b2-27aa-4775-9ae7-6213212d4ada",
Tags: []string{"sfo", "satx"},
},
}
@ -98,7 +100,8 @@ const FullListOutput = `
"creation_time": "2015-02-03T20:07:39",
"updated_time": null,
"stack_status": "CREATE_COMPLETE",
"id": "16ef0584-4458-41eb-87c8-0dc8d5f66c87"
"id": "16ef0584-4458-41eb-87c8-0dc8d5f66c87",
"tags": ["rackspace", "atx"]
},
{
"description": "Simple template to test heat commands",
@ -113,7 +116,8 @@ const FullListOutput = `
"creation_time": "2014-12-11T17:39:16",
"updated_time": "2014-12-11T17:40:37",
"stack_status": "UPDATE_COMPLETE",
"id": "db6977b2-27aa-4775-9ae7-6213212d4ada"
"id": "db6977b2-27aa-4775-9ae7-6213212d4ada",
"tags": ["sfo", "satx"]
}
]
}
@ -165,6 +169,7 @@ var GetExpected = &RetrievedStack{
Status: "CREATE_COMPLETE",
ID: "16ef0584-4458-41eb-87c8-0dc8d5f66c87",
TemplateDescription: "Simple template to test heat commands",
Tags: []string{"rackspace", "atx"},
}
// GetOutput represents the response body from a Get request.
@ -194,7 +199,8 @@ const GetOutput = `
"stack_status": "CREATE_COMPLETE",
"updated_time": null,
"id": "16ef0584-4458-41eb-87c8-0dc8d5f66c87",
"template_description": "Simple template to test heat commands"
"template_description": "Simple template to test heat commands",
"tags": ["rackspace", "atx"]
}
}
`
@ -248,7 +254,6 @@ var PreviewExpected = &PreviewedStack{
"OS::stack_name": "postman_stack",
"OS::stack_id": "16ef0584-4458-41eb-87c8-0dc8d5f66c87",
},
StatusReason: "Stack CREATE completed successfully",
Name: "postman_stack",
CreationTime: time.Date(2015, 2, 3, 20, 7, 39, 0, time.UTC),
Links: []gophercloud.Link{
@ -259,7 +264,6 @@ var PreviewExpected = &PreviewedStack{
},
Capabilities: []interface{}{},
NotificationTopics: []interface{}{},
Status: "CREATE_COMPLETE",
ID: "16ef0584-4458-41eb-87c8-0dc8d5f66c87",
TemplateDescription: "Simple template to test heat commands",
}
@ -316,6 +320,20 @@ var AbandonExpected = &AbandonedStack{
"type": "OS::Nova::Server",
},
},
Files: map[string]string{
"file:///Users/prat8228/go/src/github.com/rackspace/rack/my_nova.yaml": "heat_template_version: 2014-10-16\nparameters:\n flavor:\n type: string\n description: Flavor for the server to be created\n default: 4353\n hidden: true\nresources:\n test_server:\n type: \"OS::Nova::Server\"\n properties:\n name: test-server\n flavor: 2 GB General Purpose v1\n image: Debian 7 (Wheezy) (PVHVM)\n",
},
StackUserProjectID: "897686",
ProjectID: "897686",
Environment: map[string]interface{}{
"encrypted_param_names": make([]map[string]interface{}, 0),
"parameter_defaults": make(map[string]interface{}),
"parameters": make(map[string]interface{}),
"resource_registry": map[string]interface{}{
"file:///Users/prat8228/go/src/github.com/rackspace/rack/my_nova.yaml": "file:///Users/prat8228/go/src/github.com/rackspace/rack/my_nova.yaml",
"resources": make(map[string]interface{}),
},
},
}
// AbandonOutput represents the response body from an Abandon request.
@ -354,21 +372,233 @@ const AbandonOutput = `
"name": "hello_world",
"resource_id": "8a310d36-46fc-436f-8be4-37a696b8ac63",
"action": "CREATE",
"type": "OS::Nova::Server",
"type": "OS::Nova::Server"
}
}
},
"files": {
"file:///Users/prat8228/go/src/github.com/rackspace/rack/my_nova.yaml": "heat_template_version: 2014-10-16\nparameters:\n flavor:\n type: string\n description: Flavor for the server to be created\n default: 4353\n hidden: true\nresources:\n test_server:\n type: \"OS::Nova::Server\"\n properties:\n name: test-server\n flavor: 2 GB General Purpose v1\n image: Debian 7 (Wheezy) (PVHVM)\n"
},
"environment": {
"encrypted_param_names": [],
"parameter_defaults": {},
"parameters": {},
"resource_registry": {
"file:///Users/prat8228/go/src/github.com/rackspace/rack/my_nova.yaml": "file:///Users/prat8228/go/src/github.com/rackspace/rack/my_nova.yaml",
"resources": {}
}
},
"stack_user_project_id": "897686",
"project_id": "897686"
}`
// HandleAbandonSuccessfully creates an HTTP handler at `/stacks/postman_stack/16ef0584-4458-41eb-87c8-0dc8d5f66c87/abandon`
// on the test handler mux that responds with an `Abandon` response.
func HandleAbandonSuccessfully(t *testing.T) {
th.Mux.HandleFunc("/stacks/postman_stack/16ef0584-4458-41eb-87c8-0dc8d5f66c87/abandon", func(w http.ResponseWriter, r *http.Request) {
func HandleAbandonSuccessfully(t *testing.T, output string) {
th.Mux.HandleFunc("/stacks/postman_stack/16ef0584-4458-41eb-87c8-0dc8d5f66c8/abandon", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "DELETE")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
th.TestHeader(t, r, "Accept", "application/json")
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, AbandonOutput)
fmt.Fprintf(w, output)
})
}
// ValidJSONTemplate is a valid OpenStack Heat template in JSON format
const ValidJSONTemplate = `
{
"heat_template_version": "2014-10-16",
"parameters": {
"flavor": {
"default": 4353,
"description": "Flavor for the server to be created",
"hidden": true,
"type": "string"
}
},
"resources": {
"test_server": {
"properties": {
"flavor": "2 GB General Purpose v1",
"image": "Debian 7 (Wheezy) (PVHVM)",
"name": "test-server"
},
"type": "OS::Nova::Server"
}
}
}
`
// ValidJSONTemplateParsed is the expected parsed version of ValidJSONTemplate
var ValidJSONTemplateParsed = map[string]interface{}{
"heat_template_version": "2014-10-16",
"parameters": map[string]interface{}{
"flavor": map[string]interface{}{
"default": 4353,
"description": "Flavor for the server to be created",
"hidden": true,
"type": "string",
},
},
"resources": map[string]interface{}{
"test_server": map[string]interface{}{
"properties": map[string]interface{}{
"flavor": "2 GB General Purpose v1",
"image": "Debian 7 (Wheezy) (PVHVM)",
"name": "test-server",
},
"type": "OS::Nova::Server",
},
},
}
// ValidYAMLTemplate is a valid OpenStack Heat template in YAML format
const ValidYAMLTemplate = `
heat_template_version: 2014-10-16
parameters:
flavor:
type: string
description: Flavor for the server to be created
default: 4353
hidden: true
resources:
test_server:
type: "OS::Nova::Server"
properties:
name: test-server
flavor: 2 GB General Purpose v1
image: Debian 7 (Wheezy) (PVHVM)
`
// InvalidTemplateNoVersion is an invalid template as it has no `version` section
const InvalidTemplateNoVersion = `
parameters:
flavor:
type: string
description: Flavor for the server to be created
default: 4353
hidden: true
resources:
test_server:
type: "OS::Nova::Server"
properties:
name: test-server
flavor: 2 GB General Purpose v1
image: Debian 7 (Wheezy) (PVHVM)
`
// ValidJSONEnvironment is a valid environment for a stack in JSON format
const ValidJSONEnvironment = `
{
"parameters": {
"user_key": "userkey"
},
"resource_registry": {
"My::WP::Server": "file:///home/shardy/git/heat-templates/hot/F18/WordPress_Native.yaml",
"OS::Quantum*": "OS::Neutron*",
"AWS::CloudWatch::Alarm": "file:///etc/heat/templates/AWS_CloudWatch_Alarm.yaml",
"OS::Metering::Alarm": "OS::Ceilometer::Alarm",
"AWS::RDS::DBInstance": "file:///etc/heat/templates/AWS_RDS_DBInstance.yaml",
"resources": {
"my_db_server": {
"OS::DBInstance": "file:///home/mine/all_my_cool_templates/db.yaml"
},
"my_server": {
"OS::DBInstance": "file:///home/mine/all_my_cool_templates/db.yaml",
"hooks": "pre-create"
},
"nested_stack": {
"nested_resource": {
"hooks": "pre-update"
},
"another_resource": {
"hooks": [
"pre-create",
"pre-update"
]
}
}
}
}
}
`
// ValidJSONEnvironmentParsed is the expected parsed version of ValidJSONEnvironment
var ValidJSONEnvironmentParsed = map[string]interface{}{
"parameters": map[string]interface{}{
"user_key": "userkey",
},
"resource_registry": map[string]interface{}{
"My::WP::Server": "file:///home/shardy/git/heat-templates/hot/F18/WordPress_Native.yaml",
"OS::Quantum*": "OS::Neutron*",
"AWS::CloudWatch::Alarm": "file:///etc/heat/templates/AWS_CloudWatch_Alarm.yaml",
"OS::Metering::Alarm": "OS::Ceilometer::Alarm",
"AWS::RDS::DBInstance": "file:///etc/heat/templates/AWS_RDS_DBInstance.yaml",
"resources": map[string]interface{}{
"my_db_server": map[string]interface{}{
"OS::DBInstance": "file:///home/mine/all_my_cool_templates/db.yaml",
},
"my_server": map[string]interface{}{
"OS::DBInstance": "file:///home/mine/all_my_cool_templates/db.yaml",
"hooks": "pre-create",
},
"nested_stack": map[string]interface{}{
"nested_resource": map[string]interface{}{
"hooks": "pre-update",
},
"another_resource": map[string]interface{}{
"hooks": []interface{}{
"pre-create",
"pre-update",
},
},
},
},
},
}
// ValidYAMLEnvironment is a valid environment for a stack in YAML format
const ValidYAMLEnvironment = `
parameters:
user_key: userkey
resource_registry:
My::WP::Server: file:///home/shardy/git/heat-templates/hot/F18/WordPress_Native.yaml
# allow older templates with Quantum in them.
"OS::Quantum*": "OS::Neutron*"
# Choose your implementation of AWS::CloudWatch::Alarm
"AWS::CloudWatch::Alarm": "file:///etc/heat/templates/AWS_CloudWatch_Alarm.yaml"
#"AWS::CloudWatch::Alarm": "OS::Heat::CWLiteAlarm"
"OS::Metering::Alarm": "OS::Ceilometer::Alarm"
"AWS::RDS::DBInstance": "file:///etc/heat/templates/AWS_RDS_DBInstance.yaml"
resources:
my_db_server:
"OS::DBInstance": file:///home/mine/all_my_cool_templates/db.yaml
my_server:
"OS::DBInstance": file:///home/mine/all_my_cool_templates/db.yaml
hooks: pre-create
nested_stack:
nested_resource:
hooks: pre-update
another_resource:
hooks: [pre-create, pre-update]
`
// InvalidEnvironment is an invalid environment as it has an extra section called `resources`
const InvalidEnvironment = `
parameters:
flavor:
type: string
description: Flavor for the server to be created
default: 4353
hidden: true
resources:
test_server:
type: "OS::Nova::Server"
properties:
name: test-server
flavor: 2 GB General Purpose v1
image: Debian 7 (Wheezy) (PVHVM)
parameter_defaults:
KeyName: heat_key
`

View File

@ -2,6 +2,7 @@ package stacks
import (
"errors"
"strings"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
@ -32,9 +33,16 @@ type CreateOptsBuilder interface {
type CreateOpts struct {
// (REQUIRED) The name of the stack. It must start with an alphabetic character.
Name string
// (REQUIRED) A structure that contains either the template file or url. Call the
// associated methods to extract the information relevant to send in a create request.
TemplateOpts *Template
// (DEPRECATED): Please use TemplateOpts for providing the template. If
// TemplateOpts is provided, TemplateURL will be ignored
// (OPTIONAL; REQUIRED IF Template IS EMPTY) The URL of the template to instantiate.
// This value is ignored if Template is supplied inline.
TemplateURL string
// (DEPRECATED): Please use TemplateOpts for providing the template. If
// TemplateOpts is provided, Template will be ignored
// (OPTIONAL; REQUIRED IF TemplateURL IS EMPTY) A template to instantiate. The value
// is a stringified version of the JSON/YAML template. Since the template will likely
// be located in a file, one way to set this variable is by using ioutil.ReadFile:
@ -50,8 +58,14 @@ type CreateOpts struct {
// creation fails. Default is true, meaning all resources are not deleted when
// stack creation fails.
DisableRollback Rollback
// (OPTIONAL) A structure that contains details for the environment of the stack.
EnvironmentOpts *Environment
// (DEPRECATED): Please use EnvironmentOpts to provide Environment data
// (OPTIONAL) A stringified JSON environment for the stack.
Environment string
// (DEPRECATED): Files is automatically determined
// by parsing the template and environment passed as TemplateOpts and
// EnvironmentOpts respectively.
// (OPTIONAL) A map that maps file names to file contents. It can also be used
// to pass provider template contents. Example:
// Files: `{"myfile": "#!/bin/bash\necho 'Hello world' > /root/testfile.txt"}`
@ -60,6 +74,8 @@ type CreateOpts struct {
Parameters map[string]string
// (OPTIONAL) The timeout for stack creation in minutes.
Timeout int
// (OPTIONAL) A list of tags to assosciate with the Stack
Tags []string
}
// ToStackCreateMap casts a CreateOpts struct to a map.
@ -70,25 +86,60 @@ func (opts CreateOpts) ToStackCreateMap() (map[string]interface{}, error) {
return s, errors.New("Required field 'Name' not provided.")
}
s["stack_name"] = opts.Name
if opts.Template != "" {
s["template"] = opts.Template
} else if opts.TemplateURL != "" {
s["template_url"] = opts.TemplateURL
Files := make(map[string]string)
if opts.TemplateOpts == nil {
if opts.Template != "" {
s["template"] = opts.Template
} else if opts.TemplateURL != "" {
s["template_url"] = opts.TemplateURL
} else {
return s, errors.New("Either Template or TemplateURL must be provided.")
}
} else {
return s, errors.New("Either Template or TemplateURL must be provided.")
if err := opts.TemplateOpts.Parse(); err != nil {
return nil, err
}
if err := opts.TemplateOpts.getFileContents(opts.TemplateOpts.Parsed, ignoreIfTemplate, true); err != nil {
return nil, err
}
opts.TemplateOpts.fixFileRefs()
s["template"] = string(opts.TemplateOpts.Bin)
for k, v := range opts.TemplateOpts.Files {
Files[k] = v
}
}
if opts.DisableRollback != nil {
s["disable_rollback"] = &opts.DisableRollback
}
if opts.EnvironmentOpts != nil {
if err := opts.EnvironmentOpts.Parse(); err != nil {
return nil, err
}
if err := opts.EnvironmentOpts.getRRFileContents(ignoreIfEnvironment); err != nil {
return nil, err
}
opts.EnvironmentOpts.fixFileRefs()
for k, v := range opts.EnvironmentOpts.Files {
Files[k] = v
}
s["environment"] = string(opts.EnvironmentOpts.Bin)
} else if opts.Environment != "" {
s["environment"] = opts.Environment
}
if opts.Files != nil {
s["files"] = opts.Files
} else {
s["files"] = Files
}
if opts.DisableRollback != nil {
s["disable_rollback"] = &opts.DisableRollback
}
if opts.Environment != "" {
s["environment"] = opts.Environment
}
if opts.Files != nil {
s["files"] = opts.Files
}
if opts.Parameters != nil {
s["parameters"] = opts.Parameters
}
@ -97,6 +148,9 @@ func (opts CreateOpts) ToStackCreateMap() (map[string]interface{}, error) {
s["timeout_mins"] = opts.Timeout
}
if opts.Tags != nil {
s["tags"] = strings.Join(opts.Tags, ",")
}
return s, nil
}
@ -133,9 +187,16 @@ type AdoptOpts struct {
Name string
// (REQUIRED) The timeout for stack creation in minutes.
Timeout int
// (REQUIRED) A structure that contains either the template file or url. Call the
// associated methods to extract the information relevant to send in a create request.
TemplateOpts *Template
// (DEPRECATED): Please use TemplateOpts for providing the template. If
// TemplateOpts is provided, TemplateURL will be ignored
// (OPTIONAL; REQUIRED IF Template IS EMPTY) The URL of the template to instantiate.
// This value is ignored if Template is supplied inline.
TemplateURL string
// (DEPRECATED): Please use TemplateOpts for providing the template. If
// TemplateOpts is provided, Template will be ignored
// (OPTIONAL; REQUIRED IF TemplateURL IS EMPTY) A template to instantiate. The value
// is a stringified version of the JSON/YAML template. Since the template will likely
// be located in a file, one way to set this variable is by using ioutil.ReadFile:
@ -151,8 +212,14 @@ type AdoptOpts struct {
// creation fails. Default is true, meaning all resources are not deleted when
// stack creation fails.
DisableRollback Rollback
// (OPTIONAL) A structure that contains details for the environment of the stack.
EnvironmentOpts *Environment
// (DEPRECATED): Please use EnvironmentOpts to provide Environment data
// (OPTIONAL) A stringified JSON environment for the stack.
Environment string
// (DEPRECATED): Files is automatically determined
// by parsing the template and environment passed as TemplateOpts and
// EnvironmentOpts respectively.
// (OPTIONAL) A map that maps file names to file contents. It can also be used
// to pass provider template contents. Example:
// Files: `{"myfile": "#!/bin/bash\necho 'Hello world' > /root/testfile.txt"}`
@ -169,40 +236,69 @@ func (opts AdoptOpts) ToStackAdoptMap() (map[string]interface{}, error) {
return s, errors.New("Required field 'Name' not provided.")
}
s["stack_name"] = opts.Name
if opts.Template != "" {
s["template"] = opts.Template
} else if opts.TemplateURL != "" {
s["template_url"] = opts.TemplateURL
Files := make(map[string]string)
if opts.AdoptStackData != "" {
s["adopt_stack_data"] = opts.AdoptStackData
} else if opts.TemplateOpts == nil {
if opts.Template != "" {
s["template"] = opts.Template
} else if opts.TemplateURL != "" {
s["template_url"] = opts.TemplateURL
} else {
return s, errors.New("One of AdoptStackData, Template, TemplateURL or TemplateOpts must be provided.")
}
} else {
return s, errors.New("Either Template or TemplateURL must be provided.")
}
if err := opts.TemplateOpts.Parse(); err != nil {
return nil, err
}
if opts.AdoptStackData == "" {
return s, errors.New("Required field 'AdoptStackData' not provided.")
if err := opts.TemplateOpts.getFileContents(opts.TemplateOpts.Parsed, ignoreIfTemplate, true); err != nil {
return nil, err
}
opts.TemplateOpts.fixFileRefs()
s["template"] = string(opts.TemplateOpts.Bin)
for k, v := range opts.TemplateOpts.Files {
Files[k] = v
}
}
s["adopt_stack_data"] = opts.AdoptStackData
if opts.DisableRollback != nil {
s["disable_rollback"] = &opts.DisableRollback
}
if opts.Environment != "" {
if opts.EnvironmentOpts != nil {
if err := opts.EnvironmentOpts.Parse(); err != nil {
return nil, err
}
if err := opts.EnvironmentOpts.getRRFileContents(ignoreIfEnvironment); err != nil {
return nil, err
}
opts.EnvironmentOpts.fixFileRefs()
for k, v := range opts.EnvironmentOpts.Files {
Files[k] = v
}
s["environment"] = string(opts.EnvironmentOpts.Bin)
} else if opts.Environment != "" {
s["environment"] = opts.Environment
}
if opts.Files != nil {
s["files"] = opts.Files
} else {
s["files"] = Files
}
if opts.Parameters != nil {
s["parameters"] = opts.Parameters
}
if opts.Timeout == 0 {
return nil, errors.New("Required field 'Timeout' not provided.")
if opts.Timeout != 0 {
s["timeout"] = opts.Timeout
}
s["timeout_mins"] = opts.Timeout
return map[string]interface{}{"stack": s}, nil
return s, nil
}
// Adopt accepts an AdoptOpts struct and creates a new stack using the resources
@ -305,9 +401,16 @@ type UpdateOptsBuilder interface {
// UpdateOpts contains the common options struct used in this package's Update
// operation.
type UpdateOpts struct {
// (REQUIRED) A structure that contains either the template file or url. Call the
// associated methods to extract the information relevant to send in a create request.
TemplateOpts *Template
// (DEPRECATED): Please use TemplateOpts for providing the template. If
// TemplateOpts is provided, TemplateURL will be ignored
// (OPTIONAL; REQUIRED IF Template IS EMPTY) The URL of the template to instantiate.
// This value is ignored if Template is supplied inline.
TemplateURL string
// (DEPRECATED): Please use TemplateOpts for providing the template. If
// TemplateOpts is provided, Template will be ignored
// (OPTIONAL; REQUIRED IF TemplateURL IS EMPTY) A template to instantiate. The value
// is a stringified version of the JSON/YAML template. Since the template will likely
// be located in a file, one way to set this variable is by using ioutil.ReadFile:
@ -319,8 +422,14 @@ type UpdateOpts struct {
// }
// opts.Template = string(b)
Template string
// (OPTIONAL) A structure that contains details for the environment of the stack.
EnvironmentOpts *Environment
// (DEPRECATED): Please use EnvironmentOpts to provide Environment data
// (OPTIONAL) A stringified JSON environment for the stack.
Environment string
// (DEPRECATED): Files is automatically determined
// by parsing the template and environment passed as TemplateOpts and
// EnvironmentOpts respectively.
// (OPTIONAL) A map that maps file names to file contents. It can also be used
// to pass provider template contents. Example:
// Files: `{"myfile": "#!/bin/bash\necho 'Hello world' > /root/testfile.txt"}`
@ -329,26 +438,58 @@ type UpdateOpts struct {
Parameters map[string]string
// (OPTIONAL) The timeout for stack creation in minutes.
Timeout int
// (OPTIONAL) A list of tags to assosciate with the Stack
Tags []string
}
// ToStackUpdateMap casts a CreateOpts struct to a map.
func (opts UpdateOpts) ToStackUpdateMap() (map[string]interface{}, error) {
s := make(map[string]interface{})
if opts.Template != "" {
s["template"] = opts.Template
} else if opts.TemplateURL != "" {
s["template_url"] = opts.TemplateURL
Files := make(map[string]string)
if opts.TemplateOpts == nil {
if opts.Template != "" {
s["template"] = opts.Template
} else if opts.TemplateURL != "" {
s["template_url"] = opts.TemplateURL
} else {
return s, errors.New("Either Template or TemplateURL must be provided.")
}
} else {
return s, errors.New("Either Template or TemplateURL must be provided.")
if err := opts.TemplateOpts.Parse(); err != nil {
return nil, err
}
if err := opts.TemplateOpts.getFileContents(opts.TemplateOpts.Parsed, ignoreIfTemplate, true); err != nil {
return nil, err
}
opts.TemplateOpts.fixFileRefs()
s["template"] = string(opts.TemplateOpts.Bin)
for k, v := range opts.TemplateOpts.Files {
Files[k] = v
}
}
if opts.Environment != "" {
if opts.EnvironmentOpts != nil {
if err := opts.EnvironmentOpts.Parse(); err != nil {
return nil, err
}
if err := opts.EnvironmentOpts.getRRFileContents(ignoreIfEnvironment); err != nil {
return nil, err
}
opts.EnvironmentOpts.fixFileRefs()
for k, v := range opts.EnvironmentOpts.Files {
Files[k] = v
}
s["environment"] = string(opts.EnvironmentOpts.Bin)
} else if opts.Environment != "" {
s["environment"] = opts.Environment
}
if opts.Files != nil {
s["files"] = opts.Files
} else {
s["files"] = Files
}
if opts.Parameters != nil {
@ -359,6 +500,10 @@ func (opts UpdateOpts) ToStackUpdateMap() (map[string]interface{}, error) {
s["timeout_mins"] = opts.Timeout
}
if opts.Tags != nil {
s["tags"] = strings.Join(opts.Tags, ",")
}
return s, nil
}
@ -397,9 +542,16 @@ type PreviewOpts struct {
Name string
// (REQUIRED) The timeout for stack creation in minutes.
Timeout int
// (REQUIRED) A structure that contains either the template file or url. Call the
// associated methods to extract the information relevant to send in a create request.
TemplateOpts *Template
// (DEPRECATED): Please use TemplateOpts for providing the template. If
// TemplateOpts is provided, TemplateURL will be ignored
// (OPTIONAL; REQUIRED IF Template IS EMPTY) The URL of the template to instantiate.
// This value is ignored if Template is supplied inline.
TemplateURL string
// (DEPRECATED): Please use TemplateOpts for providing the template. If
// TemplateOpts is provided, Template will be ignored
// (OPTIONAL; REQUIRED IF TemplateURL IS EMPTY) A template to instantiate. The value
// is a stringified version of the JSON/YAML template. Since the template will likely
// be located in a file, one way to set this variable is by using ioutil.ReadFile:
@ -415,8 +567,14 @@ type PreviewOpts struct {
// creation fails. Default is true, meaning all resources are not deleted when
// stack creation fails.
DisableRollback Rollback
// (OPTIONAL) A structure that contains details for the environment of the stack.
EnvironmentOpts *Environment
// (DEPRECATED): Please use EnvironmentOpts to provide Environment data
// (OPTIONAL) A stringified JSON environment for the stack.
Environment string
// (DEPRECATED): Files is automatically determined
// by parsing the template and environment passed as TemplateOpts and
// EnvironmentOpts respectively.
// (OPTIONAL) A map that maps file names to file contents. It can also be used
// to pass provider template contents. Example:
// Files: `{"myfile": "#!/bin/bash\necho 'Hello world' > /root/testfile.txt"}`
@ -433,25 +591,56 @@ func (opts PreviewOpts) ToStackPreviewMap() (map[string]interface{}, error) {
return s, errors.New("Required field 'Name' not provided.")
}
s["stack_name"] = opts.Name
if opts.Template != "" {
s["template"] = opts.Template
} else if opts.TemplateURL != "" {
s["template_url"] = opts.TemplateURL
Files := make(map[string]string)
if opts.TemplateOpts == nil {
if opts.Template != "" {
s["template"] = opts.Template
} else if opts.TemplateURL != "" {
s["template_url"] = opts.TemplateURL
} else {
return s, errors.New("Either Template or TemplateURL must be provided.")
}
} else {
return s, errors.New("Either Template or TemplateURL must be provided.")
}
if err := opts.TemplateOpts.Parse(); err != nil {
return nil, err
}
if err := opts.TemplateOpts.getFileContents(opts.TemplateOpts.Parsed, ignoreIfTemplate, true); err != nil {
return nil, err
}
opts.TemplateOpts.fixFileRefs()
s["template"] = string(opts.TemplateOpts.Bin)
for k, v := range opts.TemplateOpts.Files {
Files[k] = v
}
}
if opts.DisableRollback != nil {
s["disable_rollback"] = &opts.DisableRollback
}
if opts.Environment != "" {
if opts.EnvironmentOpts != nil {
if err := opts.EnvironmentOpts.Parse(); err != nil {
return nil, err
}
if err := opts.EnvironmentOpts.getRRFileContents(ignoreIfEnvironment); err != nil {
return nil, err
}
opts.EnvironmentOpts.fixFileRefs()
for k, v := range opts.EnvironmentOpts.Files {
Files[k] = v
}
s["environment"] = string(opts.EnvironmentOpts.Bin)
} else if opts.Environment != "" {
s["environment"] = opts.Environment
}
if opts.Files != nil {
s["files"] = opts.Files
} else {
s["files"] = Files
}
if opts.Parameters != nil {
s["parameters"] = opts.Parameters
}

View File

@ -69,6 +69,7 @@ type ListedStack struct {
Name string `mapstructure:"stack_name"`
Status string `mapstructure:"stack_status"`
StatusReason string `mapstructure:"stack_status_reason"`
Tags []string `mapstructure:"tags"`
UpdatedTime time.Time `mapstructure:"-"`
}
@ -81,7 +82,7 @@ func ExtractStacks(page pagination.Page) ([]ListedStack, error) {
Stacks []ListedStack `mapstructure:"stacks"`
}
err := mapstructure.Decode(page.(StackPage).Body, &res)
err := mapstructure.Decode(casted, &res)
if err != nil {
return nil, err
}
@ -133,6 +134,7 @@ type RetrievedStack struct {
Name string `mapstructure:"stack_name"`
Status string `mapstructure:"stack_status"`
StatusReason string `mapstructure:"stack_status_reason"`
Tags []string `mapstructure:"tags"`
TemplateDescription string `mapstructure:"template_description"`
Timeout int `mapstructure:"timeout_mins"`
UpdatedTime time.Time `mapstructure:"-"`
@ -200,21 +202,19 @@ type DeleteResult struct {
// PreviewedStack represents the result of a Preview operation.
type PreviewedStack struct {
Capabilities []interface{} `mapstructure:"capabilities"`
CreationTime time.Time `mapstructure:"-"`
Description string `mapstructure:"description"`
DisableRollback bool `mapstructure:"disable_rollback"`
ID string `mapstructure:"id"`
Links []gophercloud.Link `mapstructure:"links"`
Name string `mapstructure:"stack_name"`
NotificationTopics []interface{} `mapstructure:"notification_topics"`
Parameters map[string]string `mapstructure:"parameters"`
Resources []map[string]interface{} `mapstructure:"resources"`
Status string `mapstructure:"stack_status"`
StatusReason string `mapstructure:"stack_status_reason"`
TemplateDescription string `mapstructure:"template_description"`
Timeout int `mapstructure:"timeout_mins"`
UpdatedTime time.Time `mapstructure:"-"`
Capabilities []interface{} `mapstructure:"capabilities"`
CreationTime time.Time `mapstructure:"-"`
Description string `mapstructure:"description"`
DisableRollback bool `mapstructure:"disable_rollback"`
ID string `mapstructure:"id"`
Links []gophercloud.Link `mapstructure:"links"`
Name string `mapstructure:"stack_name"`
NotificationTopics []interface{} `mapstructure:"notification_topics"`
Parameters map[string]string `mapstructure:"parameters"`
Resources []interface{} `mapstructure:"resources"`
TemplateDescription string `mapstructure:"template_description"`
Timeout int `mapstructure:"timeout_mins"`
UpdatedTime time.Time `mapstructure:"-"`
}
// PreviewResult represents the result of a Preview operation.
@ -269,12 +269,16 @@ func (r PreviewResult) Extract() (*PreviewedStack, error) {
// AbandonedStack represents the result of an Abandon operation.
type AbandonedStack struct {
Status string `mapstructure:"status"`
Name string `mapstructure:"name"`
Template map[string]interface{} `mapstructure:"template"`
Action string `mapstructure:"action"`
ID string `mapstructure:"id"`
Resources map[string]interface{} `mapstructure:"resources"`
Status string `mapstructure:"status"`
Name string `mapstructure:"name"`
Template map[string]interface{} `mapstructure:"template"`
Action string `mapstructure:"action"`
ID string `mapstructure:"id"`
Resources map[string]interface{} `mapstructure:"resources"`
Files map[string]string `mapstructure:"files"`
StackUserProjectID string `mapstructure:"stack_user_project_id"`
ProjectID string `mapstructure:"project_id"`
Environment map[string]interface{} `mapstructure:"environment"`
}
// AbandonResult represents the result of an Abandon operation.

View File

@ -0,0 +1,139 @@
package stacks
import (
"fmt"
"github.com/rackspace/gophercloud"
"reflect"
"strings"
)
// Template is a structure that represents OpenStack Heat templates
type Template struct {
TE
}
// TemplateFormatVersions is a map containing allowed variations of the template format version
// Note that this contains the permitted variations of the _keys_ not the values.
var TemplateFormatVersions = map[string]bool{
"HeatTemplateFormatVersion": true,
"heat_template_version": true,
"AWSTemplateFormatVersion": true,
}
// Validate validates the contents of the Template
func (t *Template) Validate() error {
if t.Parsed == nil {
if err := t.Parse(); err != nil {
return err
}
}
for key := range t.Parsed {
if _, ok := TemplateFormatVersions[key]; ok {
return nil
}
}
return fmt.Errorf("Template format version not found.")
}
// GetFileContents recursively parses a template to search for urls. These urls
// are assumed to point to other templates (known in OpenStack Heat as child
// templates). The contents of these urls are fetched and stored in the `Files`
// parameter of the template structure. This is the only way that a user can
// use child templates that are located in their filesystem; urls located on the
// web (e.g. on github or swift) can be fetched directly by Heat engine.
func (t *Template) getFileContents(te interface{}, ignoreIf igFunc, recurse bool) error {
// initialize template if empty
if t.Files == nil {
t.Files = make(map[string]string)
}
if t.fileMaps == nil {
t.fileMaps = make(map[string]string)
}
switch te.(type) {
// if te is a map
case map[string]interface{}, map[interface{}]interface{}:
teMap, err := toStringKeys(te)
if err != nil {
return err
}
for k, v := range teMap {
value, ok := v.(string)
if !ok {
// if the value is not a string, recursively parse that value
if err := t.getFileContents(v, ignoreIf, recurse); err != nil {
return err
}
} else if !ignoreIf(k, value) {
// at this point, the k, v pair has a reference to an external template.
// The assumption of heatclient is that value v is a reference
// to a file in the users environment
// create a new child template
childTemplate := new(Template)
// initialize child template
// get the base location of the child template
baseURL, err := gophercloud.NormalizePathURL(t.baseURL, value)
if err != nil {
return err
}
childTemplate.baseURL = baseURL
childTemplate.client = t.client
// fetch the contents of the child template
if err := childTemplate.Parse(); err != nil {
return err
}
// process child template recursively if required. This is
// required if the child template itself contains references to
// other templates
if recurse {
if err := childTemplate.getFileContents(childTemplate.Parsed, ignoreIf, recurse); err != nil {
return err
}
}
// update parent template with current child templates' content.
// At this point, the child template has been parsed recursively.
t.fileMaps[value] = childTemplate.URL
t.Files[childTemplate.URL] = string(childTemplate.Bin)
}
}
return nil
// if te is a slice, call the function on each element of the slice.
case []interface{}:
teSlice := te.([]interface{})
for i := range teSlice {
if err := t.getFileContents(teSlice[i], ignoreIf, recurse); err != nil {
return err
}
}
// if te is anything else, return
case string, bool, float64, nil, int:
return nil
default:
return fmt.Errorf("%v: Unrecognized type", reflect.TypeOf(te))
}
return nil
}
// function to choose keys whose values are other template files
func ignoreIfTemplate(key string, value interface{}) bool {
// key must be either `get_file` or `type` for value to be a URL
if key != "get_file" && key != "type" {
return true
}
// value must be a string
valueString, ok := value.(string)
if !ok {
return true
}
// `.template` and `.yaml` are allowed suffixes for template URLs when referred to by `type`
if key == "type" && !(strings.HasSuffix(valueString, ".template") || strings.HasSuffix(valueString, ".yaml")) {
return true
}
return false
}

View File

@ -0,0 +1,161 @@
package stacks
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"path/filepath"
"reflect"
"strings"
"github.com/rackspace/gophercloud"
"gopkg.in/yaml.v2"
)
// Client is an interface that expects a Get method similar to http.Get. This
// is needed for unit testing, since we can mock an http client. Thus, the
// client will usually be an http.Client EXCEPT in unit tests.
type Client interface {
Get(string) (*http.Response, error)
}
// TE is a base structure for both Template and Environment
type TE struct {
// Bin stores the contents of the template or environment.
Bin []byte
// URL stores the URL of the template. This is allowed to be a 'file://'
// for local files.
URL string
// Parsed contains a parsed version of Bin. Since there are 2 different
// fields referring to the same value, you must be careful when accessing
// this filed.
Parsed map[string]interface{}
// Files contains a mapping between the urls in templates to their contents.
Files map[string]string
// fileMaps is a map used internally when determining Files.
fileMaps map[string]string
// baseURL represents the location of the template or environment file.
baseURL string
// client is an interface which allows TE to fetch contents from URLS
client Client
}
// Fetch fetches the contents of a TE from its URL. Once a TE structure has a
// URL, call the fetch method to fetch the contents.
func (t *TE) Fetch() error {
// if the baseURL is not provided, use the current directors as the base URL
if t.baseURL == "" {
u, err := getBasePath()
if err != nil {
return err
}
t.baseURL = u
}
// if the contents are already present, do nothing.
if t.Bin != nil {
return nil
}
// get a fqdn from the URL using the baseURL of the TE. For local files,
// the URL's will have the `file` scheme.
u, err := gophercloud.NormalizePathURL(t.baseURL, t.URL)
if err != nil {
return err
}
t.URL = u
// get an HTTP client if none present
if t.client == nil {
t.client = getHTTPClient()
}
// use the client to fetch the contents of the TE
resp, err := t.client.Get(t.URL)
if err != nil {
return err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
t.Bin = body
return nil
}
// get the basepath of the TE
func getBasePath() (string, error) {
basePath, err := filepath.Abs(".")
if err != nil {
return "", err
}
u, err := gophercloud.NormalizePathURL("", basePath)
if err != nil {
return "", err
}
return u, nil
}
// get a an HTTP client to retrieve URL's. This client allows the use of `file`
// scheme since we may need to fetch files from users filesystem
func getHTTPClient() Client {
transport := &http.Transport{}
transport.RegisterProtocol("file", http.NewFileTransport(http.Dir("/")))
return &http.Client{Transport: transport}
}
// Parse will parse the contents and then validate. The contents MUST be either JSON or YAML.
func (t *TE) Parse() error {
if err := t.Fetch(); err != nil {
return err
}
if jerr := json.Unmarshal(t.Bin, &t.Parsed); jerr != nil {
if yerr := yaml.Unmarshal(t.Bin, &t.Parsed); yerr != nil {
return fmt.Errorf("Data in neither json nor yaml format.")
}
}
return t.Validate()
}
// Validate validates the contents of TE
func (t *TE) Validate() error {
return nil
}
// igfunc is a parameter used by GetFileContents and GetRRFileContents to check
// for valid URL's.
type igFunc func(string, interface{}) bool
// convert map[interface{}]interface{} to map[string]interface{}
func toStringKeys(m interface{}) (map[string]interface{}, error) {
switch m.(type) {
case map[string]interface{}, map[interface{}]interface{}:
typedMap := make(map[string]interface{})
if _, ok := m.(map[interface{}]interface{}); ok {
for k, v := range m.(map[interface{}]interface{}) {
typedMap[k.(string)] = v
}
} else {
typedMap = m.(map[string]interface{})
}
return typedMap, nil
default:
return nil, fmt.Errorf("Expected a map of type map[string]interface{} or map[interface{}]interface{}, actual type: %v", reflect.TypeOf(m))
}
}
// fix the reference to files by replacing relative URL's by absolute
// URL's
func (t *TE) fixFileRefs() {
tStr := string(t.Bin)
if t.fileMaps == nil {
return
}
for k, v := range t.fileMaps {
tStr = strings.Replace(tStr, k, v, -1)
}
t.Bin = []byte(tStr)
}

View File

@ -10,29 +10,7 @@ import (
)
// GetExpected represents the expected object from a Get request.
var GetExpected = &Template{
Description: "Simple template to test heat commands",
HeatTemplateVersion: "2013-05-23",
Parameters: map[string]interface{}{
"flavor": map[string]interface{}{
"default": "m1.tiny",
"type": "string",
},
},
Resources: map[string]interface{}{
"hello_world": map[string]interface{}{
"type": "OS::Nova::Server",
"properties": map[string]interface{}{
"key_name": "heat_key",
"flavor": map[string]interface{}{
"get_param": "flavor",
},
"image": "ad091b52-742f-469e-8f3c-fd81cadf0743",
"user_data": "#!/bin/bash -xv\necho \"hello world\" &gt; /root/hello-world.txt\n",
},
},
},
}
var GetExpected = "{\n \"description\": \"Simple template to test heat commands\",\n \"heat_template_version\": \"2013-05-23\",\n \"parameters\": {\n \"flavor\": {\n \"default\": \"m1.tiny\",\n \"type\": \"string\"\n }\n },\n \"resources\": {\n \"hello_world\": {\n \"properties\": {\n \"flavor\": {\n \"get_param\": \"flavor\"\n },\n \"image\": \"ad091b52-742f-469e-8f3c-fd81cadf0743\",\n \"key_name\": \"heat_key\"\n },\n \"type\": \"OS::Nova::Server\"\n }\n }\n}"
// GetOutput represents the response body from a Get request.
const GetOutput = `
@ -53,8 +31,7 @@ const GetOutput = `
"flavor": {
"get_param": "flavor"
},
"image": "ad091b52-742f-469e-8f3c-fd81cadf0743",
"user_data": "#!/bin/bash -xv\necho \"hello world\" &gt; /root/hello-world.txt\n"
"image": "ad091b52-742f-469e-8f3c-fd81cadf0743"
}
}
}

View File

@ -23,14 +23,14 @@ type ValidateOptsBuilder interface {
// ValidateOpts specifies the template validation parameters.
type ValidateOpts struct {
Template map[string]interface{}
Template string
TemplateURL string
}
// ToStackTemplateValidateMap assembles a request body based on the contents of a ValidateOpts.
func (opts ValidateOpts) ToStackTemplateValidateMap() (map[string]interface{}, error) {
vo := make(map[string]interface{})
if opts.Template != nil {
if opts.Template != "" {
vo["template"] = opts.Template
return vo, nil
}

View File

@ -1,42 +1,33 @@
package stacktemplates
import (
"encoding/json"
"github.com/mitchellh/mapstructure"
"github.com/rackspace/gophercloud"
)
// Template represents a stack template.
type Template struct {
Description string `mapstructure:"description"`
HeatTemplateVersion string `mapstructure:"heat_template_version"`
Parameters map[string]interface{} `mapstructure:"parameters"`
Resources map[string]interface{} `mapstructure:"resources"`
}
// GetResult represents the result of a Get operation.
type GetResult struct {
gophercloud.Result
}
// Extract returns a pointer to a Template object and is called after a
// Get operation.
func (r GetResult) Extract() (*Template, error) {
// Extract returns the JSON template and is called after a Get operation.
func (r GetResult) Extract() ([]byte, error) {
if r.Err != nil {
return nil, r.Err
}
var res Template
if err := mapstructure.Decode(r.Body, &res); err != nil {
template, err := json.MarshalIndent(r.Body, "", " ")
if err != nil {
return nil, err
}
return &res, nil
return template, nil
}
// ValidatedTemplate represents the parsed object returned from a Validate request.
type ValidatedTemplate struct {
Description string
Parameters map[string]interface{}
Description string `mapstructure:"Description"`
Parameters map[string]interface{} `mapstructure:"Parameters"`
ParameterGroups map[string]interface{} `mapstructure:"ParameterGroups"`
}
// ValidateResult represents the result of a Validate operation.

View File

@ -177,6 +177,9 @@ func (client *ProviderClient) Request(method, url string, options RequestOpts) (
}
}
// Set connection parameter to close the connection immediately when we've got the response
req.Close = true
// Issue the request.
resp, err := client.HTTPClient.Do(req)
if err != nil {
@ -192,10 +195,13 @@ func (client *ProviderClient) Request(method, url string, options RequestOpts) (
if options.RawBody != nil {
options.RawBody.Seek(0, 0)
}
resp.Body.Close()
resp, err = client.Request(method, url, options)
if err != nil {
return nil, fmt.Errorf("Successfully re-authenticated, but got error executing request: %s", err)
}
return resp, nil
}
}
@ -243,6 +249,8 @@ func defaultOkCodes(method string) []int {
return []int{201, 202}
case method == "PUT":
return []int{201, 202}
case method == "PATCH":
return []int{200, 204}
case method == "DELETE":
return []int{202, 204}
}
@ -296,6 +304,24 @@ func (client *ProviderClient) Put(url string, JSONBody interface{}, JSONResponse
return client.Request("PUT", url, *opts)
}
func (client *ProviderClient) Patch(url string, JSONBody interface{}, JSONResponse *interface{}, opts *RequestOpts) (*http.Response, error) {
if opts == nil {
opts = &RequestOpts{}
}
if v, ok := (JSONBody).(io.ReadSeeker); ok {
opts.RawBody = v
} else if JSONBody != nil {
opts.JSONBody = JSONBody
}
if JSONResponse != nil {
opts.JSONResponse = JSONResponse
}
return client.Request("PATCH", url, *opts)
}
func (client *ProviderClient) Delete(url string, opts *RequestOpts) (*http.Response, error) {
if opts == nil {
opts = &RequestOpts{}

View File

@ -212,3 +212,13 @@ func NewRackConnectV3(client *gophercloud.ProviderClient, eo gophercloud.Endpoin
}
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,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

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