Merge pull request #9769 from fatmcgav/state_remote_swift_updates

state/remote/swift: Updates
This commit is contained in:
Joe Topjian 2016-11-02 22:16:54 -06:00 committed by GitHub
commit 866545738d
195 changed files with 767 additions and 21351 deletions

View File

@ -3,14 +3,17 @@ package remote
import (
"bytes"
"crypto/md5"
"crypto/tls"
"fmt"
"log"
"net/http"
"os"
"strings"
"strconv"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/openstack"
"github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers"
"github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects"
"github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/openstack"
"github.com/gophercloud/gophercloud/openstack/objectstorage/v1/containers"
"github.com/gophercloud/gophercloud/openstack/objectstorage/v1/objects"
)
const TFSTATE_NAME = "tfstate.tf"
@ -49,13 +52,38 @@ func (c *SwiftClient) validateConfig(conf map[string]string) (err error) {
return fmt.Errorf("missing 'path' configuration")
}
provider, err := openstack.AuthenticatedClient(gophercloud.AuthOptions{
ao := gophercloud.AuthOptions{
IdentityEndpoint: os.Getenv("OS_AUTH_URL"),
Username: os.Getenv("OS_USERNAME"),
TenantName: os.Getenv("OS_TENANT_NAME"),
Password: os.Getenv("OS_PASSWORD"),
})
DomainName: os.Getenv("OS_DOMAIN_NAME"),
DomainID: os.Getenv("OS_DOMAIN_ID"),
}
provider, err := openstack.NewClient(ao.IdentityEndpoint)
if err != nil {
return err
}
config := &tls.Config{}
insecure := false
if insecure_env := os.Getenv("OS_INSECURE"); insecure_env != "" {
insecure, err = strconv.ParseBool(insecure_env)
if err != nil {
return err
}
}
if insecure {
log.Printf("[DEBUG] Insecure mode set")
config.InsecureSkipVerify = true
}
transport := &http.Transport{Proxy: http.ProxyFromEnvironment, TLSClientConfig: config}
provider.HTTPClient.Transport = transport
err = openstack.Authenticate(provider, ao)
if err != nil {
return err
}
@ -70,12 +98,18 @@ func (c *SwiftClient) validateConfig(conf map[string]string) (err error) {
func (c *SwiftClient) Get() (*Payload, error) {
result := objects.Download(c.client, c.path, TFSTATE_NAME, nil)
bytes, err := result.ExtractContent()
// Extract any errors from result
_, err := result.Extract()
// 404 response is to be expected if the object doesn't already exist!
if _, ok := err.(gophercloud.ErrDefault404); ok {
log.Printf("[DEBUG] Container doesn't exist to download.")
return nil, nil
}
bytes, err := result.ExtractContent()
if err != nil {
if strings.Contains(err.Error(), "but got 404 instead") {
return nil, nil
}
return nil, err
}
@ -94,7 +128,10 @@ func (c *SwiftClient) Put(data []byte) error {
}
reader := bytes.NewReader(data)
result := objects.Create(c.client, c.path, TFSTATE_NAME, reader, nil)
createOpts := objects.CreateOpts{
Content: reader,
}
result := objects.Create(c.client, c.path, TFSTATE_NAME, createOpts)
return result.Err
}

View File

@ -1,6 +1,6 @@
package accounts
import "github.com/rackspace/gophercloud"
import "github.com/gophercloud/gophercloud"
// GetOptsBuilder allows extensions to add additional headers to the Get
// request.
@ -23,31 +23,27 @@ func (opts GetOpts) ToAccountGetMap() (map[string]string, error) {
// custom metadata, call the ExtractMetadata method on the GetResult. To extract
// all the headers that are returned (including the metadata), call the
// ExtractHeader method on the GetResult.
func Get(c *gophercloud.ServiceClient, opts GetOptsBuilder) GetResult {
var res GetResult
func Get(c *gophercloud.ServiceClient, opts GetOptsBuilder) (r GetResult) {
h := make(map[string]string)
if opts != nil {
headers, err := opts.ToAccountGetMap()
if err != nil {
res.Err = err
return res
r.Err = err
return
}
for k, v := range headers {
h[k] = v
}
}
resp, err := c.Request("HEAD", getURL(c), gophercloud.RequestOpts{
resp, err := c.Request("HEAD", getURL(c), &gophercloud.RequestOpts{
MoreHeaders: h,
OkCodes: []int{204},
})
if resp != nil {
res.Header = resp.Header
r.Header = resp.Header
}
res.Err = err
return res
r.Err = err
return
}
// UpdateOptsBuilder allows extensions to add additional headers to the Update
@ -80,28 +76,25 @@ func (opts UpdateOpts) ToAccountUpdateMap() (map[string]string, error) {
// Update is a function that creates, updates, or deletes an account's metadata.
// To extract the headers returned, call the Extract method on the UpdateResult.
func Update(c *gophercloud.ServiceClient, opts UpdateOptsBuilder) UpdateResult {
var res UpdateResult
func Update(c *gophercloud.ServiceClient, opts UpdateOptsBuilder) (r UpdateResult) {
h := make(map[string]string)
if opts != nil {
headers, err := opts.ToAccountUpdateMap()
if err != nil {
res.Err = err
return res
r.Err = err
return
}
for k, v := range headers {
h[k] = v
}
}
resp, err := c.Request("POST", updateURL(c), gophercloud.RequestOpts{
resp, err := c.Request("POST", updateURL(c), &gophercloud.RequestOpts{
MoreHeaders: h,
OkCodes: []int{201, 202, 204},
})
if resp != nil {
res.Header = resp.Header
r.Header = resp.Header
}
res.Err = err
return res
r.Err = err
return
}

View File

@ -0,0 +1,160 @@
package accounts
import (
"encoding/json"
"fmt"
"strconv"
"strings"
"github.com/gophercloud/gophercloud"
)
// UpdateResult is returned from a call to the Update function.
type UpdateResult struct {
gophercloud.HeaderResult
}
// UpdateHeader represents the headers returned in the response from an Update request.
type UpdateHeader struct {
ContentLength int64 `json:"-"`
ContentType string `json:"Content-Type"`
TransID string `json:"X-Trans-Id"`
Date gophercloud.JSONRFC1123 `json:"Date"`
}
func (h *UpdateHeader) UnmarshalJSON(b []byte) error {
type tmp UpdateHeader
var updateHeader *struct {
tmp
ContentLength string `json:"Content-Length"`
}
err := json.Unmarshal(b, &updateHeader)
if err != nil {
return err
}
*h = UpdateHeader(updateHeader.tmp)
switch updateHeader.ContentLength {
case "":
h.ContentLength = 0
default:
h.ContentLength, err = strconv.ParseInt(updateHeader.ContentLength, 10, 64)
if err != nil {
return err
}
}
return nil
}
// Extract will return a struct of headers returned from a call to Get. To obtain
// a map of headers, call the ExtractHeader method on the GetResult.
func (ur UpdateResult) Extract() (*UpdateHeader, error) {
var uh *UpdateHeader
err := ur.ExtractInto(&uh)
return uh, err
}
// GetHeader represents the headers returned in the response from a Get request.
type GetHeader struct {
BytesUsed int64 `json:"-"`
ContainerCount int64 `json:"-"`
ContentLength int64 `json:"-"`
ObjectCount int64 `json:"-"`
ContentType string `json:"Content-Type"`
TransID string `json:"X-Trans-Id"`
TempURLKey string `json:"X-Account-Meta-Temp-URL-Key"`
TempURLKey2 string `json:"X-Account-Meta-Temp-URL-Key-2"`
Date gophercloud.JSONRFC1123 `json:"Date"`
}
func (h *GetHeader) UnmarshalJSON(b []byte) error {
type tmp GetHeader
var getHeader *struct {
tmp
BytesUsed string `json:"X-Account-Bytes-Used"`
ContentLength string `json:"Content-Length"`
ContainerCount string `json:"X-Account-Container-Count"`
ObjectCount string `json:"X-Account-Object-Count"`
}
err := json.Unmarshal(b, &getHeader)
if err != nil {
return err
}
*h = GetHeader(getHeader.tmp)
switch getHeader.BytesUsed {
case "":
h.BytesUsed = 0
default:
h.BytesUsed, err = strconv.ParseInt(getHeader.BytesUsed, 10, 64)
if err != nil {
return err
}
}
fmt.Println("getHeader: ", getHeader.ContentLength)
switch getHeader.ContentLength {
case "":
h.ContentLength = 0
default:
h.ContentLength, err = strconv.ParseInt(getHeader.ContentLength, 10, 64)
if err != nil {
return err
}
}
switch getHeader.ObjectCount {
case "":
h.ObjectCount = 0
default:
h.ObjectCount, err = strconv.ParseInt(getHeader.ObjectCount, 10, 64)
if err != nil {
return err
}
}
switch getHeader.ContainerCount {
case "":
h.ContainerCount = 0
default:
h.ContainerCount, err = strconv.ParseInt(getHeader.ContainerCount, 10, 64)
if err != nil {
return err
}
}
return nil
}
// GetResult is returned from a call to the Get function.
type GetResult struct {
gophercloud.HeaderResult
}
// Extract will return a struct of headers returned from a call to Get. To obtain
// a map of headers, call the ExtractHeader method on the GetResult.
func (r GetResult) Extract() (*GetHeader, error) {
var s *GetHeader
err := r.ExtractInto(&s)
return s, err
}
// ExtractMetadata is a function that takes a GetResult (of type *http.Response)
// and returns the custom metatdata associated with the account.
func (r GetResult) ExtractMetadata() (map[string]string, error) {
if r.Err != nil {
return nil, r.Err
}
metadata := make(map[string]string)
for k, v := range r.Header {
if strings.HasPrefix(k, "X-Account-Meta-") {
key := strings.TrimPrefix(k, "X-Account-Meta-")
metadata[key] = v[0]
}
}
return metadata, nil
}

View File

@ -1,6 +1,6 @@
package accounts
import "github.com/rackspace/gophercloud"
import "github.com/gophercloud/gophercloud"
func getURL(c *gophercloud.ServiceClient) string {
return c.Endpoint

View File

@ -0,0 +1,13 @@
package objects
import "github.com/gophercloud/gophercloud"
// ErrWrongChecksum is the error when the checksum generated for an object
// doesn't match the ETAG header.
type ErrWrongChecksum struct {
gophercloud.BaseError
}
func (e ErrWrongChecksum) Error() string {
return "Local checksum does not match API ETag header"
}

View File

@ -1,19 +1,18 @@
package objects
import (
"bufio"
"bytes"
"crypto/hmac"
"crypto/md5"
"crypto/sha1"
"fmt"
"io"
"io/ioutil"
"strings"
"time"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/openstack/objectstorage/v1/accounts"
"github.com/rackspace/gophercloud/pagination"
"github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/openstack/objectstorage/v1/accounts"
"github.com/gophercloud/gophercloud/pagination"
)
// ListOptsBuilder allows extensions to add additional parameters to the List
@ -42,10 +41,7 @@ type ListOpts struct {
// representing whether to list complete information for each object.
func (opts ListOpts) ToObjectListParams() (bool, string, error) {
q, err := gophercloud.BuildQueryString(opts)
if err != nil {
return false, "", err
}
return opts.Full, q.String(), nil
return opts.Full, q.String(), err
}
// List is a function that retrieves all objects in a container. It also returns the details
@ -67,13 +63,11 @@ func List(c *gophercloud.ServiceClient, containerName string, opts ListOptsBuild
}
}
createPage := func(r pagination.PageResult) pagination.Page {
pager := pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page {
p := ObjectPage{pagination.MarkerPageBase{PageResult: r}}
p.MarkerPageBase.Owner = p
return p
}
pager := pagination.NewPager(c, url, createPage)
})
pager.Headers = headers
return pager
}
@ -113,47 +107,42 @@ func (opts DownloadOpts) ToObjectDownloadParams() (map[string]string, string, er
// Download is a function that retrieves the content and metadata for an object.
// To extract just the content, pass the DownloadResult response to the
// ExtractContent function.
func Download(c *gophercloud.ServiceClient, containerName, objectName string, opts DownloadOptsBuilder) DownloadResult {
var res DownloadResult
func Download(c *gophercloud.ServiceClient, containerName, objectName string, opts DownloadOptsBuilder) (r DownloadResult) {
url := downloadURL(c, containerName, objectName)
h := make(map[string]string)
if opts != nil {
headers, query, err := opts.ToObjectDownloadParams()
if err != nil {
res.Err = err
return res
r.Err = err
return
}
for k, v := range headers {
h[k] = v
}
url += query
}
resp, err := c.Request("GET", url, gophercloud.RequestOpts{
resp, err := c.Get(url, nil, &gophercloud.RequestOpts{
MoreHeaders: h,
OkCodes: []int{200, 304},
})
if resp != nil {
res.Header = resp.Header
res.Body = resp.Body
r.Header = resp.Header
r.Body = resp.Body
}
res.Err = err
return res
r.Err = err
return
}
// CreateOptsBuilder allows extensions to add additional parameters to the
// Create request.
type CreateOptsBuilder interface {
ToObjectCreateParams() (map[string]string, string, error)
ToObjectCreateParams() (io.Reader, map[string]string, string, error)
}
// CreateOpts is a structure that holds parameters for creating an object.
type CreateOpts struct {
Content io.Reader
Metadata map[string]string
CacheControl string `h:"Cache-Control"`
ContentDisposition string `h:"Content-Disposition"`
@ -175,78 +164,60 @@ type CreateOpts struct {
// ToObjectCreateParams formats a CreateOpts into a query string and map of
// headers.
func (opts CreateOpts) ToObjectCreateParams() (map[string]string, string, error) {
func (opts CreateOpts) ToObjectCreateParams() (io.Reader, map[string]string, string, error) {
q, err := gophercloud.BuildQueryString(opts)
if err != nil {
return nil, "", err
return nil, nil, "", err
}
h, err := gophercloud.BuildHeaders(opts)
if err != nil {
return nil, q.String(), err
return nil, nil, "", err
}
for k, v := range opts.Metadata {
h["X-Object-Meta-"+k] = v
}
return h, q.String(), nil
hash := md5.New()
buf := bytes.NewBuffer([]byte{})
_, err = io.Copy(io.MultiWriter(hash, buf), opts.Content)
if err != nil {
return nil, nil, "", err
}
localChecksum := fmt.Sprintf("%x", hash.Sum(nil))
h["ETag"] = localChecksum
return buf, h, q.String(), nil
}
// Create is a function that creates a new object or replaces an existing object. If the returned response's ETag
// header fails to match the local checksum, the failed request will automatically be retried up to a maximum of 3 times.
func Create(c *gophercloud.ServiceClient, containerName, objectName string, content io.ReadSeeker, opts CreateOptsBuilder) CreateResult {
var res CreateResult
func Create(c *gophercloud.ServiceClient, containerName, objectName string, opts CreateOptsBuilder) (r CreateResult) {
url := createURL(c, containerName, objectName)
h := make(map[string]string)
var b io.Reader
if opts != nil {
headers, query, err := opts.ToObjectCreateParams()
tmpB, headers, query, err := opts.ToObjectCreateParams()
if err != nil {
res.Err = err
return res
r.Err = err
return
}
for k, v := range headers {
h[k] = v
}
url += query
b = tmpB
}
hash := md5.New()
bufioReader := bufio.NewReader(io.TeeReader(content, hash))
io.Copy(ioutil.Discard, bufioReader)
localChecksum := hash.Sum(nil)
h["ETag"] = fmt.Sprintf("%x", localChecksum)
_, err := content.Seek(0, 0)
if err != nil {
res.Err = err
return res
}
ropts := gophercloud.RequestOpts{
RawBody: content,
resp, err := c.Put(url, nil, nil, &gophercloud.RequestOpts{
RawBody: b,
MoreHeaders: h,
}
resp, err := c.Request("PUT", url, ropts)
if err != nil {
res.Err = err
return res
}
})
r.Err = err
if resp != nil {
res.Header = resp.Header
if resp.Header.Get("ETag") == fmt.Sprintf("%x", localChecksum) {
res.Err = err
return res
}
res.Err = fmt.Errorf("Local checksum does not match API ETag header")
r.Header = resp.Header
}
return res
return
}
// CopyOptsBuilder allows extensions to add additional parameters to the
@ -262,14 +233,11 @@ type CopyOpts struct {
ContentDisposition string `h:"Content-Disposition"`
ContentEncoding string `h:"Content-Encoding"`
ContentType string `h:"Content-Type"`
Destination string `h:"Destination,required"`
Destination string `h:"Destination" required:"true"`
}
// ToObjectCopyMap formats a CopyOpts into a map of headers.
func (opts CopyOpts) ToObjectCopyMap() (map[string]string, error) {
if opts.Destination == "" {
return nil, fmt.Errorf("Required CopyOpts field 'Destination' not set.")
}
h, err := gophercloud.BuildHeaders(opts)
if err != nil {
return nil, err
@ -281,14 +249,12 @@ func (opts CopyOpts) ToObjectCopyMap() (map[string]string, error) {
}
// Copy is a function that copies one object to another.
func Copy(c *gophercloud.ServiceClient, containerName, objectName string, opts CopyOptsBuilder) CopyResult {
var res CopyResult
func Copy(c *gophercloud.ServiceClient, containerName, objectName string, opts CopyOptsBuilder) (r CopyResult) {
h := make(map[string]string)
headers, err := opts.ToObjectCopyMap()
if err != nil {
res.Err = err
return res
r.Err = err
return
}
for k, v := range headers {
@ -296,15 +262,15 @@ func Copy(c *gophercloud.ServiceClient, containerName, objectName string, opts C
}
url := copyURL(c, containerName, objectName)
resp, err := c.Request("COPY", url, gophercloud.RequestOpts{
resp, err := c.Request("COPY", url, &gophercloud.RequestOpts{
MoreHeaders: h,
OkCodes: []int{201},
})
if resp != nil {
res.Header = resp.Header
r.Header = resp.Header
}
res.Err = err
return res
r.Err = err
return
}
// DeleteOptsBuilder allows extensions to add additional parameters to the
@ -321,32 +287,26 @@ type DeleteOpts struct {
// ToObjectDeleteQuery formats a DeleteOpts into a query string.
func (opts DeleteOpts) ToObjectDeleteQuery() (string, error) {
q, err := gophercloud.BuildQueryString(opts)
if err != nil {
return "", err
}
return q.String(), nil
return q.String(), err
}
// Delete is a function that deletes an object.
func Delete(c *gophercloud.ServiceClient, containerName, objectName string, opts DeleteOptsBuilder) DeleteResult {
var res DeleteResult
func Delete(c *gophercloud.ServiceClient, containerName, objectName string, opts DeleteOptsBuilder) (r DeleteResult) {
url := deleteURL(c, containerName, objectName)
if opts != nil {
query, err := opts.ToObjectDeleteQuery()
if err != nil {
res.Err = err
return res
r.Err = err
return
}
url += query
}
resp, err := c.Delete(url, nil)
if resp != nil {
res.Header = resp.Header
r.Header = resp.Header
}
res.Err = err
return res
r.Err = err
return
}
// GetOptsBuilder allows extensions to add additional parameters to the
@ -364,35 +324,29 @@ type GetOpts struct {
// ToObjectGetQuery formats a GetOpts into a query string.
func (opts GetOpts) ToObjectGetQuery() (string, error) {
q, err := gophercloud.BuildQueryString(opts)
if err != nil {
return "", err
}
return q.String(), nil
return q.String(), err
}
// Get is a function that retrieves the metadata of an object. To extract just the custom
// metadata, pass the GetResult response to the ExtractMetadata function.
func Get(c *gophercloud.ServiceClient, containerName, objectName string, opts GetOptsBuilder) GetResult {
var res GetResult
func Get(c *gophercloud.ServiceClient, containerName, objectName string, opts GetOptsBuilder) (r GetResult) {
url := getURL(c, containerName, objectName)
if opts != nil {
query, err := opts.ToObjectGetQuery()
if err != nil {
res.Err = err
return res
r.Err = err
return
}
url += query
}
resp, err := c.Request("HEAD", url, gophercloud.RequestOpts{
resp, err := c.Request("HEAD", url, &gophercloud.RequestOpts{
OkCodes: []int{200, 204},
})
if resp != nil {
res.Header = resp.Header
r.Header = resp.Header
}
res.Err = err
return res
r.Err = err
return
}
// UpdateOptsBuilder allows extensions to add additional parameters to the
@ -426,31 +380,28 @@ func (opts UpdateOpts) ToObjectUpdateMap() (map[string]string, error) {
}
// Update is a function that creates, updates, or deletes an object's metadata.
func Update(c *gophercloud.ServiceClient, containerName, objectName string, opts UpdateOptsBuilder) UpdateResult {
var res UpdateResult
func Update(c *gophercloud.ServiceClient, containerName, objectName string, opts UpdateOptsBuilder) (r UpdateResult) {
h := make(map[string]string)
if opts != nil {
headers, err := opts.ToObjectUpdateMap()
if err != nil {
res.Err = err
return res
r.Err = err
return
}
for k, v := range headers {
h[k] = v
}
}
url := updateURL(c, containerName, objectName)
resp, err := c.Request("POST", url, gophercloud.RequestOpts{
resp, err := c.Post(url, nil, nil, &gophercloud.RequestOpts{
MoreHeaders: h,
})
if resp != nil {
res.Header = resp.Header
r.Header = resp.Header
}
res.Err = err
return res
r.Err = err
return
}
// HTTPMethod represents an HTTP method string (e.g. "GET").

View File

@ -0,0 +1,441 @@
package objects
import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"strconv"
"strings"
"github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/pagination"
)
// Object is a structure that holds information related to a storage object.
type Object struct {
// Bytes is the total number of bytes that comprise the object.
Bytes int64 `json:"bytes"`
// ContentType is the content type of the object.
ContentType string `json:"content_type"`
// Hash represents the MD5 checksum value of the object's content.
Hash string `json:"hash"`
// LastModified is the RFC3339Milli time the object was last modified, represented
// as a string. For any given object (obj), this value may be parsed to a time.Time:
// lastModified, err := time.Parse(gophercloud.RFC3339Milli, obj.LastModified)
LastModified gophercloud.JSONRFC3339MilliNoZ `json:"last_modified"`
// Name is the unique name for the object.
Name string `json:"name"`
}
// ObjectPage is a single page of objects that is returned from a call to the
// List function.
type ObjectPage struct {
pagination.MarkerPageBase
}
// IsEmpty returns true if a ListResult contains no object names.
func (r ObjectPage) IsEmpty() (bool, error) {
names, err := ExtractNames(r)
return len(names) == 0, err
}
// LastMarker returns the last object name in a ListResult.
func (r ObjectPage) LastMarker() (string, error) {
names, err := ExtractNames(r)
if err != nil {
return "", err
}
if len(names) == 0 {
return "", nil
}
return names[len(names)-1], nil
}
// ExtractInfo is a function that takes a page of objects and returns their full information.
func ExtractInfo(r pagination.Page) ([]Object, error) {
var s []Object
err := (r.(ObjectPage)).ExtractInto(&s)
return s, err
}
// ExtractNames is a function that takes a page of objects and returns only their names.
func ExtractNames(r pagination.Page) ([]string, error) {
casted := r.(ObjectPage)
ct := casted.Header.Get("Content-Type")
switch {
case strings.HasPrefix(ct, "application/json"):
parsed, err := ExtractInfo(r)
if err != nil {
return nil, err
}
names := make([]string, 0, len(parsed))
for _, object := range parsed {
names = append(names, object.Name)
}
return names, nil
case strings.HasPrefix(ct, "text/plain"):
names := make([]string, 0, 50)
body := string(r.(ObjectPage).Body.([]uint8))
for _, name := range strings.Split(body, "\n") {
if len(name) > 0 {
names = append(names, name)
}
}
return names, nil
case strings.HasPrefix(ct, "text/html"):
return []string{}, nil
default:
return nil, fmt.Errorf("Cannot extract names from response with content-type: [%s]", ct)
}
}
// DownloadHeader represents the headers returned in the response from a Download request.
type DownloadHeader struct {
AcceptRanges string `json:"Accept-Ranges"`
ContentDisposition string `json:"Content-Disposition"`
ContentEncoding string `json:"Content-Encoding"`
ContentLength int64 `json:"-"`
ContentType string `json:"Content-Type"`
Date gophercloud.JSONRFC1123 `json:"Date"`
DeleteAt gophercloud.JSONUnix `json:"X-Delete-At"`
ETag string `json:"Etag"`
LastModified gophercloud.JSONRFC1123 `json:"Last-Modified"`
ObjectManifest string `json:"X-Object-Manifest"`
StaticLargeObject bool `json:"X-Static-Large-Object"`
TransID string `json:"X-Trans-Id"`
}
func (h *DownloadHeader) UnmarshalJSON(b []byte) error {
type tmp DownloadHeader
var hTmp *struct {
tmp
ContentLength string `json:"Content-Length"`
}
err := json.Unmarshal(b, &hTmp)
if err != nil {
return err
}
*h = DownloadHeader(hTmp.tmp)
switch hTmp.ContentLength {
case "":
h.ContentLength = 0
default:
h.ContentLength, err = strconv.ParseInt(hTmp.ContentLength, 10, 64)
if err != nil {
return err
}
}
return nil
}
// DownloadResult is a *http.Response that is returned from a call to the Download function.
type DownloadResult struct {
gophercloud.HeaderResult
Body io.ReadCloser
}
// Extract will return a struct of headers returned from a call to Download. To obtain
// a map of headers, call the ExtractHeader method on the DownloadResult.
func (r DownloadResult) Extract() (*DownloadHeader, error) {
var s *DownloadHeader
err := r.ExtractInto(&s)
return s, err
}
// ExtractContent is a function that takes a DownloadResult's io.Reader body
// and reads all available data into a slice of bytes. Please be aware that due
// the nature of io.Reader is forward-only - meaning that it can only be read
// once and not rewound. You can recreate a reader from the output of this
// function by using bytes.NewReader(downloadBytes)
func (r *DownloadResult) ExtractContent() ([]byte, error) {
if r.Err != nil {
return nil, r.Err
}
defer r.Body.Close()
body, err := ioutil.ReadAll(r.Body)
if err != nil {
return nil, err
}
r.Body.Close()
return body, nil
}
// GetHeader represents the headers returned in the response from a Get request.
type GetHeader struct {
ContentDisposition string `json:"Content-Disposition"`
ContentEncoding string `json:"Content-Encoding"`
ContentLength int64 `json:"Content-Length"`
ContentType string `json:"Content-Type"`
Date gophercloud.JSONRFC1123 `json:"Date"`
DeleteAt gophercloud.JSONUnix `json:"X-Delete-At"`
ETag string `json:"Etag"`
LastModified gophercloud.JSONRFC1123 `json:"Last-Modified"`
ObjectManifest string `json:"X-Object-Manifest"`
StaticLargeObject bool `json:"X-Static-Large-Object"`
TransID string `json:"X-Trans-Id"`
}
func (h *GetHeader) UnmarshalJSON(b []byte) error {
type tmp GetHeader
var hTmp *struct {
tmp
ContentLength string `json:"Content-Length"`
}
err := json.Unmarshal(b, &hTmp)
if err != nil {
return err
}
*h = GetHeader(hTmp.tmp)
switch hTmp.ContentLength {
case "":
h.ContentLength = 0
default:
h.ContentLength, err = strconv.ParseInt(hTmp.ContentLength, 10, 64)
if err != nil {
return err
}
}
return nil
}
// GetResult is a *http.Response that is returned from a call to the Get function.
type GetResult struct {
gophercloud.HeaderResult
}
// Extract will return a struct of headers returned from a call to Get. To obtain
// a map of headers, call the ExtractHeader method on the GetResult.
func (r GetResult) Extract() (*GetHeader, error) {
var s *GetHeader
err := r.ExtractInto(&s)
return s, err
}
// ExtractMetadata is a function that takes a GetResult (of type *http.Response)
// and returns the custom metadata associated with the object.
func (r GetResult) ExtractMetadata() (map[string]string, error) {
if r.Err != nil {
return nil, r.Err
}
metadata := make(map[string]string)
for k, v := range r.Header {
if strings.HasPrefix(k, "X-Object-Meta-") {
key := strings.TrimPrefix(k, "X-Object-Meta-")
metadata[key] = v[0]
}
}
return metadata, nil
}
// CreateHeader represents the headers returned in the response from a Create request.
type CreateHeader struct {
ContentLength int64 `json:"Content-Length"`
ContentType string `json:"Content-Type"`
Date gophercloud.JSONRFC1123 `json:"Date"`
ETag string `json:"Etag"`
LastModified gophercloud.JSONRFC1123 `json:"Last-Modified"`
TransID string `json:"X-Trans-Id"`
}
func (h *CreateHeader) UnmarshalJSON(b []byte) error {
type tmp CreateHeader
var hTmp *struct {
tmp
ContentLength string `json:"Content-Length"`
}
err := json.Unmarshal(b, &hTmp)
if err != nil {
return err
}
*h = CreateHeader(hTmp.tmp)
switch hTmp.ContentLength {
case "":
h.ContentLength = 0
default:
h.ContentLength, err = strconv.ParseInt(hTmp.ContentLength, 10, 64)
if err != nil {
return err
}
}
return nil
}
// CreateResult represents the result of a create operation.
type CreateResult struct {
checksum string
gophercloud.HeaderResult
}
// Extract will return a struct of headers returned from a call to Create. To obtain
// a map of headers, call the ExtractHeader method on the CreateResult.
func (r CreateResult) Extract() (*CreateHeader, error) {
//if r.Header.Get("ETag") != fmt.Sprintf("%x", localChecksum) {
// return nil, ErrWrongChecksum{}
//}
var s *CreateHeader
err := r.ExtractInto(&s)
return s, err
}
// UpdateHeader represents the headers returned in the response from a Update request.
type UpdateHeader struct {
ContentLength int64 `json:"Content-Length"`
ContentType string `json:"Content-Type"`
Date gophercloud.JSONRFC1123 `json:"Date"`
TransID string `json:"X-Trans-Id"`
}
func (h *UpdateHeader) UnmarshalJSON(b []byte) error {
type tmp UpdateHeader
var hTmp *struct {
tmp
ContentLength string `json:"Content-Length"`
}
err := json.Unmarshal(b, &hTmp)
if err != nil {
return err
}
*h = UpdateHeader(hTmp.tmp)
switch hTmp.ContentLength {
case "":
h.ContentLength = 0
default:
h.ContentLength, err = strconv.ParseInt(hTmp.ContentLength, 10, 64)
if err != nil {
return err
}
}
return nil
}
// UpdateResult represents the result of an update operation.
type UpdateResult struct {
gophercloud.HeaderResult
}
// Extract will return a struct of headers returned from a call to Update. To obtain
// a map of headers, call the ExtractHeader method on the UpdateResult.
func (r UpdateResult) Extract() (*UpdateHeader, error) {
var s *UpdateHeader
err := r.ExtractInto(&s)
return s, err
}
// DeleteHeader represents the headers returned in the response from a Delete request.
type DeleteHeader struct {
ContentLength int64 `json:"Content-Length"`
ContentType string `json:"Content-Type"`
Date gophercloud.JSONRFC1123 `json:"Date"`
TransID string `json:"X-Trans-Id"`
}
func (h *DeleteHeader) UnmarshalJSON(b []byte) error {
type tmp DeleteHeader
var hTmp *struct {
tmp
ContentLength string `json:"Content-Length"`
}
err := json.Unmarshal(b, &hTmp)
if err != nil {
return err
}
*h = DeleteHeader(hTmp.tmp)
switch hTmp.ContentLength {
case "":
h.ContentLength = 0
default:
h.ContentLength, err = strconv.ParseInt(hTmp.ContentLength, 10, 64)
if err != nil {
return err
}
}
return nil
}
// DeleteResult represents the result of a delete operation.
type DeleteResult struct {
gophercloud.HeaderResult
}
// Extract will return a struct of headers returned from a call to Delete. To obtain
// a map of headers, call the ExtractHeader method on the DeleteResult.
func (r DeleteResult) Extract() (*DeleteHeader, error) {
var s *DeleteHeader
err := r.ExtractInto(&s)
return s, err
}
// CopyHeader represents the headers returned in the response from a Copy request.
type CopyHeader struct {
ContentLength int64 `json:"Content-Length"`
ContentType string `json:"Content-Type"`
CopiedFrom string `json:"X-Copied-From"`
CopiedFromLastModified gophercloud.JSONRFC1123 `json:"X-Copied-From-Last-Modified"`
Date gophercloud.JSONRFC1123 `json:"Date"`
ETag string `json:"Etag"`
LastModified gophercloud.JSONRFC1123 `json:"Last-Modified"`
TransID string `json:"X-Trans-Id"`
}
func (h *CopyHeader) UnmarshalJSON(b []byte) error {
type tmp CopyHeader
var hTmp *struct {
tmp
ContentLength string `json:"Content-Length"`
}
err := json.Unmarshal(b, &hTmp)
if err != nil {
return err
}
*h = CopyHeader(hTmp.tmp)
switch hTmp.ContentLength {
case "":
h.ContentLength = 0
default:
h.ContentLength, err = strconv.ParseInt(hTmp.ContentLength, 10, 64)
if err != nil {
return err
}
}
return nil
}
// CopyResult represents the result of a copy operation.
type CopyResult struct {
gophercloud.HeaderResult
}
// Extract will return a struct of headers returned from a call to Copy. To obtain
// a map of headers, call the ExtractHeader method on the CopyResult.
func (r CopyResult) Extract() (*CopyHeader, error) {
var s *CopyHeader
err := r.ExtractInto(&s)
return s, err
}

View File

@ -1,7 +1,7 @@
package objects
import (
"github.com/rackspace/gophercloud"
"github.com/gophercloud/gophercloud"
)
func listURL(c *gophercloud.ServiceClient, container string) string {

View File

@ -1,275 +0,0 @@
# Contributing to gophercloud
- [Getting started](#getting-started)
- [Tests](#tests)
- [Style guide](#basic-style-guide)
- [5 ways to get involved](#5-ways-to-get-involved)
## Setting up your git workspace
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) but add `-tags "fixtures acceptance"` to
get dependencies for unit and acceptance tests.
2. Move into the directory that houses your local repository:
```bash
cd ${GOPATH}/src/github.com/rackspace/gophercloud
```
3. Fork the `rackspace/gophercloud` repository and update your remote refs. You
will need to rename the `origin` remote branch to `upstream`, and add your
fork as `origin` instead:
```bash
git remote rename origin upstream
git remote add origin git@github.com/<my_username>/gophercloud
```
4. Checkout the latest development branch:
```bash
git checkout master
```
5. If you're working on something (discussed more in detail below), you will
need to checkout a new feature branch:
```bash
git checkout -b my-new-feature
```
Another thing to bear in mind is that you will need to add a few extra
environment variables for acceptance tests - this is documented in our
[acceptance tests readme](/acceptance).
## Tests
When working on a new or existing feature, testing will be the backbone of your
work since it helps uncover and prevent regressions in the codebase. There are
two types of test we use in gophercloud: unit tests and acceptance tests, which
are both described below.
### Unit tests
Unit tests are the fine-grained tests that establish and ensure the behaviour
of individual units of functionality. We usually test on an
operation-by-operation basis (an operation typically being an API action) with
the use of mocking to set up explicit expectations. Each operation will set up
its HTTP response expectation, and then test how the system responds when fed
this controlled, pre-determined input.
To make life easier, we've introduced a bunch of test helpers to simplify the
process of testing expectations with assertions:
```go
import (
"testing"
"github.com/rackspace/gophercloud/testhelper"
)
func TestSomething(t *testing.T) {
result, err := Operation()
testhelper.AssertEquals(t, "foo", result.Bar)
testhelper.AssertNoErr(t, err)
}
func TestSomethingElse(t *testing.T) {
testhelper.CheckEquals(t, "expected", "actual")
}
```
`AssertEquals` and `AssertNoErr` will throw a fatal error if a value does not
match an expected value or if an error has been declared, respectively. You can
also use `CheckEquals` and `CheckNoErr` for the same purpose; the only difference
being that `t.Errorf` is raised rather than `t.Fatalf`.
Here is a truncated example of mocked HTTP responses:
```go
import (
"testing"
th "github.com/rackspace/gophercloud/testhelper"
fake "github.com/rackspace/gophercloud/testhelper/client"
)
func TestGet(t *testing.T) {
// Setup the HTTP request multiplexer and server
th.SetupHTTP()
defer th.TeardownHTTP()
th.Mux.HandleFunc("/networks/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) {
// Test we're using the correct HTTP method
th.TestMethod(t, r, "GET")
// Test we're setting the auth token
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
// Set the appropriate headers for our mocked response
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
// Set the HTTP body
fmt.Fprintf(w, `
{
"network": {
"status": "ACTIVE",
"name": "private-network",
"admin_state_up": true,
"tenant_id": "4fd44f30292945e481c7b8a0c8908869",
"shared": true,
"id": "d32019d3-bc6e-4319-9c1d-6722fc136a22"
}
}
`)
})
// Call our API operation
network, err := Get(fake.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22").Extract()
// Assert no errors and equality
th.AssertNoErr(t, err)
th.AssertEquals(t, n.Status, "ACTIVE")
}
```
### Acceptance tests
As we've already mentioned, unit tests have a very narrow and confined focus -
they test small units of behaviour. Acceptance tests on the other hand have a
far larger scope: they are fully functional tests that test the entire API of a
service in one fell swoop. They don't care about unit isolation or mocking
expectations, they instead do a full run-through and consequently test how the
entire system _integrates_ together. When an API satisfies expectations, it
proves by default that the requirements for a contract have been met.
Please be aware that acceptance tests will hit a live API - and may incur
service charges from your provider. Although most tests handle their own
teardown procedures, it is always worth manually checking that resources are
deleted after the test suite finishes.
### Running tests
To run all tests:
```bash
go test -tags fixtures ./...
```
To run all tests with verbose output:
```bash
go test -v -tags fixtures ./...
```
To run tests that match certain [build tags]():
```bash
go test -tags "fixtures foo bar" ./...
```
To run tests for a particular sub-package:
```bash
cd ./path/to/package && go test -tags fixtures .
```
## Basic style guide
We follow the standard formatting recommendations and language idioms set out
in the [Effective Go](https://golang.org/doc/effective_go.html) guide. It's
definitely worth reading - but the relevant sections are
[formatting](https://golang.org/doc/effective_go.html#formatting)
and [names](https://golang.org/doc/effective_go.html#names).
## 5 ways to get involved
There are five main ways you can get involved in our open-source project, and
each is described briefly below. Once you've made up your mind and decided on
your fix, you will need to follow the same basic steps that all submissions are
required to adhere to:
1. [fork](https://help.github.com/articles/fork-a-repo/) the `rackspace/gophercloud` repository
2. checkout a [new branch](https://github.com/Kunena/Kunena-Forum/wiki/Create-a-new-branch-with-git-and-manage-branches)
3. submit your branch as a [pull request](https://help.github.com/articles/creating-a-pull-request/)
### 1. Providing feedback
On of the easiest ways to get readily involved in our project is to let us know
about your experiences using our SDK. Feedback like this is incredibly useful
to us, because it allows us to refine and change features based on what our
users want and expect of us. There are a bunch of ways to get in contact! You
can [ping us](https://developer.rackspace.com/support/) via e-mail, talk to us on irc
(#rackspace-dev on freenode), [tweet us](https://twitter.com/rackspace), or
submit an issue on our [bug tracker](/issues). Things you might like to tell us
are:
* how easy was it to start using our SDK?
* did it meet your expectations? If not, why not?
* did our documentation help or hinder you?
* what could we improve in general?
### 2. Fixing bugs
If you want to start fixing open bugs, we'd really appreciate that! Bug fixing
is central to any project. The best way to get started is by heading to our
[bug tracker](https://github.com/rackspace/gophercloud/issues) and finding open
bugs that you think nobody is working on. It might be useful to comment on the
thread to see the current state of the issue and if anybody has made any
breakthroughs on it so far.
### 3. Improving documentation
We have three forms of documentation:
* short README documents that briefly introduce a topic
* reference documentation on [godoc.org](http://godoc.org) that is automatically
generated from source code comments
* user documentation on our [homepage](http://gophercloud.io) that includes
getting started guides, installation guides and code samples
If you feel that a certain section could be improved - whether it's to clarify
ambiguity, correct a technical mistake, or to fix a grammatical error - please
feel entitled to do so! We welcome doc pull requests with the same childlike
enthusiasm as any other contribution!
### 4. Optimizing existing features
If you would like to improve or optimize an existing feature, please be aware
that we adhere to [semantic versioning](http://semver.org) - which means that
we cannot introduce breaking changes to the API without a major version change
(v1.x -> v2.x). Making that leap is a big step, so we encourage contributors to
refactor rather than rewrite. Running tests will prevent regression and avoid
the possibility of breaking somebody's current implementation.
Another tip is to keep the focus of your work as small as possible - try not to
introduce a change that affects lots and lots of files because it introduces
added risk and increases the cognitive load on the reviewers checking your
work. Change-sets which are easily understood and will not negatively impact
users are more likely to be integrated quickly.
Lastly, if you're seeking to optimize a particular operation, you should try to
demonstrate a negative performance impact - perhaps using go's inbuilt
[benchmark capabilities](http://dave.cheney.net/2013/06/30/how-to-write-benchmarks-in-go).
### 5. Working on a new feature
If you've found something we've left out, definitely feel free to start work on
introducing that feature. It's always useful to open an issue or submit a pull
request early on to indicate your intent to a core contributor - this enables
quick/early feedback and can help steer you in the right direction by avoiding
known issues. It might also help you avoid losing time implementing something
that might not ever work. One tip is to prefix your Pull Request issue title
with [wip] - then people know it's a work in progress.
You must ensure that all of your work is well tested - both in terms of unit
and acceptance tests. Untested code will not be merged because it introduces
too much of a risk to end-users.
Happy hacking!

View File

@ -1,13 +0,0 @@
Contributors
============
| Name | Email |
| ---- | ----- |
| Samuel A. Falvo II | <sam.falvo@rackspace.com>
| Glen Campbell | <glen.campbell@rackspace.com>
| Jesse Noller | <jesse.noller@rackspace.com>
| Jon Perritt | <jon.perritt@rackspace.com>
| Ash Wilson | <ash.wilson@rackspace.com>
| Jamie Hannaford | <jamie.hannaford@rackspace.com>
| Don Schenck | don.schenck@rackspace.com>
| Joe Topjian | <joe@topjian.net>

View File

@ -1,191 +0,0 @@
Copyright 2012-2013 Rackspace, Inc.
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
this file except in compliance with the License. You may obtain a copy of the
License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed
under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
CONDITIONS OF ANY KIND, either express or implied. See the License for the
specific language governing permissions and limitations under the License.
------
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS

View File

@ -1,160 +0,0 @@
# 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,
including Compute, Block Storage, Object Storage, Networking, and Identity.
Each service API is backed with getting started guides, code samples, reference
documentation, unit tests and acceptance tests.
## Useful links
* [Gophercloud homepage](http://gophercloud.io)
* [Reference documentation](http://godoc.org/github.com/rackspace/gophercloud)
* [Getting started guides](http://gophercloud.io/docs)
* [Effective Go](https://golang.org/doc/effective_go.html)
## How to install
Before installing, you need to ensure that your [GOPATH environment variable](https://golang.org/doc/code.html#GOPATH)
is pointing to an appropriate directory where you want to install Gophercloud:
```bash
mkdir $HOME/go
export GOPATH=$HOME/go
```
To protect yourself against changes in your dependencies, we highly recommend choosing a
[dependency management solution](https://github.com/golang/go/wiki/PackageManagementTools) for
your projects, such as [godep](https://github.com/tools/godep). Once this is set up, you can install
Gophercloud as a dependency like so:
```bash
go get github.com/rackspace/gophercloud
# Edit your code to import relevant packages from "github.com/rackspace/gophercloud"
godep save ./...
```
This will install all the source files you need into a `Godeps/_workspace` directory, which is
referenceable from your own source files when you use the `godep go` command.
## Getting started
### Credentials
Because you'll be hitting an API, you will need to retrieve your OpenStack
credentials and either store them as environment variables or in your local Go
files. The first method is recommended because it decouples credential
information from source code, allowing you to push the latter to your version
control system without any security risk.
You will need to retrieve the following:
* username
* password
* tenant name or tenant ID
* a valid Keystone identity URL
For users that have the OpenStack dashboard installed, there's a shortcut. If
you visit the `project/access_and_security` path in Horizon and click on the
"Download OpenStack RC File" button at the top right hand corner, you will
download a bash file that exports all of your access details to environment
variables. To execute the file, run `source admin-openrc.sh` and you will be
prompted for your password.
### Authentication
Once you have access to your credentials, you can begin plugging them into
Gophercloud. The next step is authentication, and this is handled by a base
"Provider" struct. To get one, you can either pass in your credentials
explicitly, or tell Gophercloud to use environment variables:
```go
import (
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/openstack"
"github.com/rackspace/gophercloud/openstack/utils"
)
// Option 1: Pass in the values yourself
opts := gophercloud.AuthOptions{
IdentityEndpoint: "https://my-openstack.com:5000/v2.0",
Username: "{username}",
Password: "{password}",
TenantID: "{tenant_id}",
}
// Option 2: Use a utility function to retrieve all your environment variables
opts, err := openstack.AuthOptionsFromEnv()
```
Once you have the `opts` variable, you can pass it in and get back a
`ProviderClient` struct:
```go
provider, err := openstack.AuthenticatedClient(opts)
```
The `ProviderClient` is the top-level client that all of your OpenStack services
derive from. The provider contains all of the authentication details that allow
your Go code to access the API - such as the base URL and token ID.
### Provision a server
Once we have a base Provider, we inject it as a dependency into each OpenStack
service. In order to work with the Compute API, we need a Compute service
client; which can be created like so:
```go
client, err := openstack.NewComputeV2(provider, gophercloud.EndpointOpts{
Region: os.Getenv("OS_REGION_NAME"),
})
```
We then use this `client` for any Compute API operation we want. In our case,
we want to provision a new server - so we invoke the `Create` method and pass
in the flavor ID (hardware specification) and image ID (operating system) we're
interested in:
```go
import "github.com/rackspace/gophercloud/openstack/compute/v2/servers"
server, err := servers.Create(client, servers.CreateOpts{
Name: "My new server!",
FlavorRef: "flavor_id",
ImageRef: "image_id",
}).Extract()
```
If you are unsure about what images and flavors are, you can read our [Compute
Getting Started guide](http://gophercloud.io/docs/compute). The above code
sample creates a new server with the parameters, and embodies the new resource
in the `server` variable (a
[`servers.Server`](http://godoc.org/github.com/rackspace/gophercloud) struct).
### Next steps
Cool! You've handled authentication, got your `ProviderClient` and provisioned
a new server. You're now ready to use more OpenStack services.
* [Getting started with Compute](http://gophercloud.io/docs/compute)
* [Getting started with Object Storage](http://gophercloud.io/docs/object-storage)
* [Getting started with Networking](http://gophercloud.io/docs/networking)
* [Getting started with Block Storage](http://gophercloud.io/docs/block-storage)
* [Getting started with Identity](http://gophercloud.io/docs/identity)
## Contributing
Engaging the community and lowering barriers for contributors is something we
care a lot about. For this reason, we've taken the time to write a [contributing
guide](./CONTRIBUTING.md) for folks interested in getting involved in our project.
If you're not sure how you can get involved, feel free to submit an issue or
[contact us](https://developer.rackspace.com/support/). You don't need to be a
Go expert - all members of the community are welcome!
## Help and feedback
If you're struggling with something or have spotted a potential bug, feel free
to submit an issue to our [bug tracker](/issues) or [contact us directly](https://developer.rackspace.com/support/).

View File

@ -1,338 +0,0 @@
# Upgrading to v1.0.0
With the arrival of this new major version increment, the unfortunate news is
that breaking changes have been introduced to existing services. The API
has been completely rewritten from the ground up to make the library more
extensible, maintainable and easy-to-use.
Below we've compiled upgrade instructions for the various services that
existed before. If you have a specific issue that is not addressed below,
please [submit an issue](/issues/new) or
[e-mail our support team](https://developer.rackspace.com/support/).
* [Authentication](#authentication)
* [Servers](#servers)
* [List servers](#list-servers)
* [Get server details](#get-server-details)
* [Create server](#create-server)
* [Resize server](#resize-server)
* [Reboot server](#reboot-server)
* [Update server](#update-server)
* [Rebuild server](#rebuild-server)
* [Change admin password](#change-admin-password)
* [Delete server](#delete-server)
* [Rescue server](#rescue-server)
* [Images and flavors](#images-and-flavors)
* [List images](#list-images)
* [List flavors](#list-flavors)
* [Create/delete image](#createdelete-image)
* [Other](#other)
* [List keypairs](#list-keypairs)
* [Create/delete keypair](#createdelete-keypair)
* [List IP addresses](#list-ip-addresses)
# Authentication
One of the major differences that this release introduces is the level of
sub-packaging to differentiate between services and providers. You now have
the option of authenticating with OpenStack and other providers (like Rackspace).
To authenticate with a vanilla OpenStack installation, you can either specify
your credentials like this:
```go
import (
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/openstack"
)
opts := gophercloud.AuthOptions{
IdentityEndpoint: "https://my-openstack.com:5000/v2.0",
Username: "{username}",
Password: "{password}",
TenantID: "{tenant_id}",
}
```
Or have them pulled in through environment variables, like this:
```go
opts, err := openstack.AuthOptionsFromEnv()
```
Once you have your `AuthOptions` struct, you pass it in to get back a `Provider`,
like so:
```go
provider, err := openstack.AuthenticatedClient(opts)
```
This provider is the top-level structure that all services are created from.
# Servers
Before you can interact with the Compute API, you need to retrieve a
`gophercloud.ServiceClient`. To do this:
```go
// Define your region, etc.
opts := gophercloud.EndpointOpts{Region: "RegionOne"}
client, err := openstack.NewComputeV2(provider, opts)
```
## List servers
All operations that involve API collections (servers, flavors, images) now use
the `pagination.Pager` interface. This interface represents paginated entities
that can be iterated over.
Once you have a Pager, you can then pass a callback function into its `EachPage`
method, and this will allow you to traverse over the collection and execute
arbitrary functionality. So, an example with list servers:
```go
import (
"fmt"
"github.com/rackspace/gophercloud/pagination"
"github.com/rackspace/gophercloud/openstack/compute/v2/servers"
)
// We have the option of filtering the server list. If we want the full
// collection, leave it as an empty struct or nil
opts := servers.ListOpts{Name: "server_1"}
// Retrieve a pager (i.e. a paginated collection)
pager := servers.List(client, opts)
// Define an anonymous function to be executed on each page's iteration
err := pager.EachPage(func(page pagination.Page) (bool, error) {
serverList, err := servers.ExtractServers(page)
// `s' will be a servers.Server struct
for _, s := range serverList {
fmt.Printf("We have a server. ID=%s, Name=%s", s.ID, s.Name)
}
})
```
## Get server details
```go
import "github.com/rackspace/gophercloud/openstack/compute/v2/servers"
// Get the HTTP result
response := servers.Get(client, "server_id")
// Extract a Server struct from the response
server, err := response.Extract()
```
## Create server
```go
import "github.com/rackspace/gophercloud/openstack/compute/v2/servers"
// Define our options
opts := servers.CreateOpts{
Name: "new_server",
FlavorRef: "flavorID",
ImageRef: "imageID",
}
// Get our response
response := servers.Create(client, opts)
// Extract
server, err := response.Extract()
```
## Change admin password
```go
import "github.com/rackspace/gophercloud/openstack/compute/v2/servers"
result := servers.ChangeAdminPassword(client, "server_id", "newPassword_&123")
```
## Resize server
```go
import "github.com/rackspace/gophercloud/openstack/compute/v2/servers"
result := servers.Resize(client, "server_id", "new_flavor_id")
```
## Reboot server
```go
import "github.com/rackspace/gophercloud/openstack/compute/v2/servers"
// You have a choice of two reboot methods: servers.SoftReboot or servers.HardReboot
result := servers.Reboot(client, "server_id", servers.SoftReboot)
```
## Update server
```go
import "github.com/rackspace/gophercloud/openstack/compute/v2/servers"
opts := servers.UpdateOpts{Name: "new_name"}
server, err := servers.Update(client, "server_id", opts).Extract()
```
## Rebuild server
```go
import "github.com/rackspace/gophercloud/openstack/compute/v2/servers"
// You have the option of specifying additional options
opts := RebuildOpts{
Name: "new_name",
AdminPass: "admin_password",
ImageID: "image_id",
Metadata: map[string]string{"owner": "me"},
}
result := servers.Rebuild(client, "server_id", opts)
// You can extract a servers.Server struct from the HTTP response
server, err := result.Extract()
```
## Delete server
```go
import "github.com/rackspace/gophercloud/openstack/compute/v2/servers"
response := servers.Delete(client, "server_id")
```
## Rescue server
The server rescue extension for Compute is not currently supported.
# Images and flavors
## List images
As with listing servers (see above), you first retrieve a Pager, and then pass
in a callback over each page:
```go
import (
"github.com/rackspace/gophercloud/pagination"
"github.com/rackspace/gophercloud/openstack/compute/v2/images"
)
// We have the option of filtering the image list. If we want the full
// collection, leave it as an empty struct
opts := images.ListOpts{ChangesSince: "2014-01-01T01:02:03Z", Name: "Ubuntu 12.04"}
// Retrieve a pager (i.e. a paginated collection)
pager := images.List(client, opts)
// Define an anonymous function to be executed on each page's iteration
err := pager.EachPage(func(page pagination.Page) (bool, error) {
imageList, err := images.ExtractImages(page)
for _, i := range imageList {
// "i" will be an images.Image
}
})
```
## List flavors
```go
import (
"github.com/rackspace/gophercloud/pagination"
"github.com/rackspace/gophercloud/openstack/compute/v2/flavors"
)
// We have the option of filtering the flavor list. If we want the full
// collection, leave it as an empty struct
opts := flavors.ListOpts{ChangesSince: "2014-01-01T01:02:03Z", MinRAM: 4}
// Retrieve a pager (i.e. a paginated collection)
pager := flavors.List(client, opts)
// Define an anonymous function to be executed on each page's iteration
err := pager.EachPage(func(page pagination.Page) (bool, error) {
flavorList, err := networks.ExtractFlavors(page)
for _, f := range flavorList {
// "f" will be a flavors.Flavor
}
})
```
## Create/delete image
Image management has been shifted to Glance, but unfortunately this service is
not supported as of yet. You can, however, list Compute images like so:
```go
import "github.com/rackspace/gophercloud/openstack/compute/v2/images"
// Retrieve a pager (i.e. a paginated collection)
pager := images.List(client, opts)
// Define an anonymous function to be executed on each page's iteration
err := pager.EachPage(func(page pagination.Page) (bool, error) {
imageList, err := images.ExtractImages(page)
for _, i := range imageList {
// "i" will be an images.Image
}
})
```
# Other
## List keypairs
```go
import "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs"
// Retrieve a pager (i.e. a paginated collection)
pager := keypairs.List(client, opts)
// Define an anonymous function to be executed on each page's iteration
err := pager.EachPage(func(page pagination.Page) (bool, error) {
keyList, err := keypairs.ExtractKeyPairs(page)
for _, k := range keyList {
// "k" will be a keypairs.KeyPair
}
})
```
## Create/delete keypairs
To create a new keypair, you need to specify its name and, optionally, a
pregenerated OpenSSH-formatted public key.
```go
import "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs"
opts := keypairs.CreateOpts{
Name: "new_key",
PublicKey: "...",
}
response := keypairs.Create(client, opts)
key, err := response.Extract()
```
To delete an existing keypair:
```go
response := keypairs.Delete(client, "keypair_id")
```
## List IP addresses
This operation is not currently supported.

View File

@ -1,55 +0,0 @@
package gophercloud
/*
AuthOptions stores information needed to authenticate to an OpenStack cluster.
You can populate one manually, or use a provider's AuthOptionsFromEnv() function
to read relevant information from the standard environment variables. Pass one
to a provider's AuthenticatedClient function to authenticate and obtain a
ProviderClient representing an active session on that provider.
Its fields are the union of those recognized by each identity implementation and
provider.
*/
type AuthOptions struct {
// IdentityEndpoint specifies the HTTP endpoint that is required to work with
// the Identity API of the appropriate version. While it's ultimately needed by
// all of the identity services, it will often be populated by a provider-level
// function.
IdentityEndpoint string
// Username is required if using Identity V2 API. Consult with your provider's
// control panel to discover your account's username. In Identity V3, either
// UserID or a combination of Username and DomainID or DomainName are needed.
Username, UserID string
// Exactly one of Password or APIKey is required for the Identity V2 and V3
// APIs. Consult with your provider's control panel to discover your account's
// preferred method of authentication.
Password, APIKey string
// At most one of DomainID and DomainName must be provided if using Username
// with Identity V3. Otherwise, either are optional.
DomainID, DomainName string
// The TenantID and TenantName fields are optional for the Identity V2 API.
// Some providers allow you to specify a TenantName instead of the TenantId.
// Some require both. Your provider's authentication policies will determine
// how these fields influence authentication.
TenantID, TenantName string
// AllowReauth should be set to true if you grant permission for Gophercloud to
// cache your credentials in memory, and to allow Gophercloud to attempt to
// re-authenticate automatically if/when your token expires. If you set it to
// false, it will not cache these settings, but re-authentication will not be
// possible. This setting defaults to false.
//
// NOTE: The reauth function will try to re-authenticate endlessly if left unchecked.
// The way to limit the number of attempts is to provide a custom HTTP client to the provider client
// and provide a transport that implements the RoundTripper interface and stores the number of failed retries.
// For an example of this, see here: https://github.com/rackspace/rack/blob/1.0.0/auth/clients.go#L311
AllowReauth bool
// TokenID allows users to authenticate (possibly as another user) with an
// authentication token ID.
TokenID string
}

View File

@ -1,14 +0,0 @@
package gophercloud
import "time"
// AuthResults [deprecated] is a leftover type from the v0.x days. It was
// intended to describe common functionality among identity service results, but
// is not actually used anywhere.
type AuthResults interface {
// TokenID returns the token's ID value from the authentication response.
TokenID() (string, error)
// ExpiresAt retrieves the token's expiration time.
ExpiresAt() (time.Time, error)
}

View File

@ -1,67 +0,0 @@
/*
Package gophercloud provides a multi-vendor interface to OpenStack-compatible
clouds. The library has a three-level hierarchy: providers, services, and
resources.
Provider structs represent the service providers that offer and manage a
collection of services. Examples of providers include: OpenStack, Rackspace,
HP. These are defined like so:
opts := gophercloud.AuthOptions{
IdentityEndpoint: "https://my-openstack.com:5000/v2.0",
Username: "{username}",
Password: "{password}",
TenantID: "{tenant_id}",
}
provider, err := openstack.AuthenticatedClient(opts)
Service structs are specific to a provider and handle all of the logic and
operations for a particular OpenStack service. Examples of services include:
Compute, Object Storage, Block Storage. In order to define one, you need to
pass in the parent provider, like so:
opts := gophercloud.EndpointOpts{Region: "RegionOne"}
client := openstack.NewComputeV2(provider, opts)
Resource structs are the domain models that services make use of in order
to work with and represent the state of API resources:
server, err := servers.Get(client, "{serverId}").Extract()
Intermediate Result structs are returned for API operations, which allow
generic access to the HTTP headers, response body, and any errors associated
with the network transaction. To turn a result into a usable resource struct,
you must call the Extract method which is chained to the response, or an
Extract function from an applicable extension:
result := servers.Get(client, "{serverId}")
// Attempt to extract the disk configuration from the OS-DCF disk config
// extension:
config, err := diskconfig.ExtractGet(result)
All requests that enumerate a collection return a Pager struct that is used to
iterate through the results one page at a time. Use the EachPage method on that
Pager to handle each successive Page in a closure, then use the appropriate
extraction method from that request's package to interpret that Page as a slice
of results:
err := servers.List(client, nil).EachPage(func (page pagination.Page) (bool, error) {
s, err := servers.ExtractServers(page)
if err != nil {
return false, err
}
// Handle the []servers.Server slice.
// Return "false" or an error to prematurely stop fetching new pages.
return true, nil
})
This top-level package contains utility functions and data types that are used
throughout the provider and service packages. Of particular note for end users
are the AuthOptions and EndpointOpts structs.
*/
package gophercloud

View File

@ -1,92 +0,0 @@
package gophercloud
import "errors"
var (
// ErrServiceNotFound is returned when no service in a service catalog matches
// the provided EndpointOpts. This is generally returned by provider service
// factory methods like "NewComputeV2()" and can mean that a service is not
// enabled for your account.
ErrServiceNotFound = errors.New("No suitable service could be found in the service catalog.")
// ErrEndpointNotFound is returned when no available endpoints match the
// provided EndpointOpts. This is also generally returned by provider service
// factory methods, and usually indicates that a region was specified
// incorrectly.
ErrEndpointNotFound = errors.New("No suitable endpoint could be found in the service catalog.")
)
// Availability indicates to whom a specific service endpoint is accessible:
// the internet at large, internal networks only, or only to administrators.
// Different identity services use different terminology for these. Identity v2
// lists them as different kinds of URLs within the service catalog ("adminURL",
// "internalURL", and "publicURL"), while v3 lists them as "Interfaces" in an
// endpoint's response.
type Availability string
const (
// AvailabilityAdmin indicates that an endpoint is only available to
// administrators.
AvailabilityAdmin Availability = "admin"
// AvailabilityPublic indicates that an endpoint is available to everyone on
// the internet.
AvailabilityPublic Availability = "public"
// AvailabilityInternal indicates that an endpoint is only available within
// the cluster's internal network.
AvailabilityInternal Availability = "internal"
)
// EndpointOpts specifies search criteria used by queries against an
// OpenStack service catalog. The options must contain enough information to
// unambiguously identify one, and only one, endpoint within the catalog.
//
// Usually, these are passed to service client factory functions in a provider
// package, like "rackspace.NewComputeV2()".
type EndpointOpts struct {
// Type [required] is the service type for the client (e.g., "compute",
// "object-store"). Generally, this will be supplied by the service client
// function, but a user-given value will be honored if provided.
Type string
// Name [optional] is the service name for the client (e.g., "nova") as it
// appears in the service catalog. Services can have the same Type but a
// different Name, which is why both Type and Name are sometimes needed.
Name string
// Region [required] is the geographic region in which the endpoint resides,
// generally specifying which datacenter should house your resources.
// Required only for services that span multiple regions.
Region string
// Availability [optional] is the visibility of the endpoint to be returned.
// Valid types include the constants AvailabilityPublic, AvailabilityInternal,
// or AvailabilityAdmin from this package.
//
// Availability is not required, and defaults to AvailabilityPublic. Not all
// providers or services offer all Availability options.
Availability Availability
}
/*
EndpointLocator is an internal function to be used by provider implementations.
It provides an implementation that locates a single endpoint from a service
catalog for a specific ProviderClient based on user-provided EndpointOpts. The
provider then uses it to discover related ServiceClients.
*/
type EndpointLocator func(EndpointOpts) (string, error)
// ApplyDefaults is an internal method to be used by provider implementations.
//
// It sets EndpointOpts fields if not already set, including a default type.
// Currently, EndpointOpts.Availability defaults to the public endpoint.
func (eo *EndpointOpts) ApplyDefaults(t string) {
if eo.Type == "" {
eo.Type = t
}
if eo.Availability == "" {
eo.Availability = AvailabilityPublic
}
}

View File

@ -1,61 +0,0 @@
package openstack
import (
"fmt"
"os"
"github.com/rackspace/gophercloud"
)
var nilOptions = gophercloud.AuthOptions{}
// ErrNoAuthUrl, ErrNoUsername, and ErrNoPassword errors indicate of the required OS_AUTH_URL, OS_USERNAME, or OS_PASSWORD
// environment variables, respectively, remain undefined. See the AuthOptions() function for more details.
var (
ErrNoAuthURL = fmt.Errorf("Environment variable OS_AUTH_URL needs to be set.")
ErrNoUsername = fmt.Errorf("Environment variable OS_USERNAME, OS_USERID, or OS_TOKEN needs to be set.")
ErrNoPassword = fmt.Errorf("Environment variable OS_PASSWORD or OS_TOKEN needs to be set.")
)
// AuthOptionsFromEnv fills out an AuthOptions structure from the environment
// variables: OS_AUTH_URL, OS_USERNAME, OS_USERID, OS_PASSWORD, OS_TENANT_ID,
// OS_TENANT_NAME, OS_DOMAIN_ID, OS_DOMAIN_NAME, OS_TOKEN. It checks that
// (1) OS_AUTH_URL is set, (2) OS_USERNAME, OS_USERID, or OS_TOKEN is set,
// (3) OS_PASSWORD or OS_TOKEN is set.
func AuthOptionsFromEnv() (gophercloud.AuthOptions, error) {
authURL := os.Getenv("OS_AUTH_URL")
username := os.Getenv("OS_USERNAME")
userID := os.Getenv("OS_USERID")
password := os.Getenv("OS_PASSWORD")
tenantID := os.Getenv("OS_TENANT_ID")
tenantName := os.Getenv("OS_TENANT_NAME")
domainID := os.Getenv("OS_DOMAIN_ID")
domainName := os.Getenv("OS_DOMAIN_NAME")
tokenID := os.Getenv("OS_TOKEN")
if authURL == "" {
return nilOptions, ErrNoAuthURL
}
if username == "" && userID == "" && tokenID == "" {
return nilOptions, ErrNoUsername
}
if password == "" && tokenID == "" {
return nilOptions, ErrNoPassword
}
ao := gophercloud.AuthOptions{
IdentityEndpoint: authURL,
UserID: userID,
Username: username,
Password: password,
TenantID: tenantID,
TenantName: tenantName,
DomainID: domainID,
DomainName: domainName,
TokenID: tokenID,
}
return ao, nil
}

View File

@ -1,5 +0,0 @@
// Package volumes provides information and interaction with volumes in the
// OpenStack Block Storage service. A volume is a detachable block storage
// device, akin to a USB hard drive. It can only be attached to one instance at
// a time.
package volumes

View File

@ -1,236 +0,0 @@
package volumes
import (
"fmt"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
)
// CreateOptsBuilder allows extensions to add additional parameters to the
// Create request.
type CreateOptsBuilder interface {
ToVolumeCreateMap() (map[string]interface{}, error)
}
// CreateOpts contains options for creating a Volume. This object is passed to
// the volumes.Create function. For more information about these parameters,
// see the Volume object.
type CreateOpts struct {
// OPTIONAL
Availability string
// OPTIONAL
Description string
// OPTIONAL
Metadata map[string]string
// OPTIONAL
Name string
// REQUIRED
Size int
// OPTIONAL
SnapshotID, SourceVolID, ImageID string
// OPTIONAL
VolumeType string
}
// ToVolumeCreateMap assembles a request body based on the contents of a
// CreateOpts.
func (opts CreateOpts) ToVolumeCreateMap() (map[string]interface{}, error) {
v := make(map[string]interface{})
if opts.Size == 0 {
return nil, fmt.Errorf("Required CreateOpts field 'Size' not set.")
}
v["size"] = opts.Size
if opts.Availability != "" {
v["availability_zone"] = opts.Availability
}
if opts.Description != "" {
v["display_description"] = opts.Description
}
if opts.ImageID != "" {
v["imageRef"] = opts.ImageID
}
if opts.Metadata != nil {
v["metadata"] = opts.Metadata
}
if opts.Name != "" {
v["display_name"] = opts.Name
}
if opts.SourceVolID != "" {
v["source_volid"] = opts.SourceVolID
}
if opts.SnapshotID != "" {
v["snapshot_id"] = opts.SnapshotID
}
if opts.VolumeType != "" {
v["volume_type"] = opts.VolumeType
}
return map[string]interface{}{"volume": v}, nil
}
// Create will create a new Volume based on the values in CreateOpts. To extract
// the Volume object from the response, call the Extract method on the
// CreateResult.
func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult {
var res CreateResult
reqBody, err := opts.ToVolumeCreateMap()
if err != nil {
res.Err = err
return res
}
_, res.Err = client.Post(createURL(client), reqBody, &res.Body, &gophercloud.RequestOpts{
OkCodes: []int{200, 201},
})
return res
}
// Delete will delete the existing Volume with the provided ID.
func Delete(client *gophercloud.ServiceClient, id string) DeleteResult {
var res DeleteResult
_, res.Err = client.Delete(deleteURL(client, id), nil)
return res
}
// Get retrieves the Volume with the provided ID. To extract the Volume object
// from the response, call the Extract method on the GetResult.
func Get(client *gophercloud.ServiceClient, id string) GetResult {
var res GetResult
_, res.Err = client.Get(getURL(client, id), &res.Body, nil)
return res
}
// ListOptsBuilder allows extensions to add additional parameters to the List
// request.
type ListOptsBuilder interface {
ToVolumeListQuery() (string, error)
}
// ListOpts holds options for listing Volumes. It is passed to the volumes.List
// function.
type ListOpts struct {
// admin-only option. Set it to true to see all tenant volumes.
AllTenants bool `q:"all_tenants"`
// List only volumes that contain Metadata.
Metadata map[string]string `q:"metadata"`
// List only volumes that have Name as the display name.
Name string `q:"name"`
// List only volumes that have a status of Status.
Status string `q:"status"`
}
// ToVolumeListQuery formats a ListOpts into a query string.
func (opts ListOpts) ToVolumeListQuery() (string, error) {
q, err := gophercloud.BuildQueryString(opts)
if err != nil {
return "", err
}
return q.String(), nil
}
// List returns Volumes optionally limited by the conditions provided in ListOpts.
func List(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
url := listURL(client)
if opts != nil {
query, err := opts.ToVolumeListQuery()
if err != nil {
return pagination.Pager{Err: err}
}
url += query
}
createPage := func(r pagination.PageResult) pagination.Page {
return ListResult{pagination.SinglePageBase(r)}
}
return pagination.NewPager(client, url, createPage)
}
// UpdateOptsBuilder allows extensions to add additional parameters to the
// Update request.
type UpdateOptsBuilder interface {
ToVolumeUpdateMap() (map[string]interface{}, error)
}
// UpdateOpts contain options for updating an existing Volume. This object is passed
// to the volumes.Update function. For more information about the parameters, see
// the Volume object.
type UpdateOpts struct {
// OPTIONAL
Name string
// OPTIONAL
Description string
// OPTIONAL
Metadata map[string]string
}
// ToVolumeUpdateMap assembles a request body based on the contents of an
// UpdateOpts.
func (opts UpdateOpts) ToVolumeUpdateMap() (map[string]interface{}, error) {
v := make(map[string]interface{})
if opts.Description != "" {
v["display_description"] = opts.Description
}
if opts.Metadata != nil {
v["metadata"] = opts.Metadata
}
if opts.Name != "" {
v["display_name"] = opts.Name
}
return map[string]interface{}{"volume": v}, nil
}
// Update will update the Volume with provided information. To extract the updated
// Volume from the response, call the Extract method on the UpdateResult.
func Update(client *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) UpdateResult {
var res UpdateResult
reqBody, err := opts.ToVolumeUpdateMap()
if err != nil {
res.Err = err
return res
}
_, res.Err = client.Put(updateURL(client, id), reqBody, &res.Body, &gophercloud.RequestOpts{
OkCodes: []int{200},
})
return res
}
// IDFromName is a convienience function that returns a server's ID given its name.
func IDFromName(client *gophercloud.ServiceClient, name string) (string, error) {
volumeCount := 0
volumeID := ""
if name == "" {
return "", fmt.Errorf("A volume name must be provided.")
}
pager := List(client, nil)
pager.EachPage(func(page pagination.Page) (bool, error) {
volumeList, err := ExtractVolumes(page)
if err != nil {
return false, err
}
for _, s := range volumeList {
if s.Name == name {
volumeCount++
volumeID = s.ID
}
}
return true, nil
})
switch volumeCount {
case 0:
return "", fmt.Errorf("Unable to find volume: %s", name)
case 1:
return volumeID, nil
default:
return "", fmt.Errorf("Found %d volumes matching %s", volumeCount, name)
}
}

View File

@ -1,113 +0,0 @@
package volumes
import (
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
"github.com/mitchellh/mapstructure"
)
// Volume contains all the information associated with an OpenStack Volume.
type Volume struct {
// Current status of the volume.
Status string `mapstructure:"status"`
// Human-readable display name for the volume.
Name string `mapstructure:"display_name"`
// Instances onto which the volume is attached.
Attachments []map[string]interface{} `mapstructure:"attachments"`
// This parameter is no longer used.
AvailabilityZone string `mapstructure:"availability_zone"`
// Indicates whether this is a bootable volume.
Bootable string `mapstructure:"bootable"`
// The date when this volume was created.
CreatedAt string `mapstructure:"created_at"`
// Human-readable description for the volume.
Description string `mapstructure:"display_description"`
// The type of volume to create, either SATA or SSD.
VolumeType string `mapstructure:"volume_type"`
// The ID of the snapshot from which the volume was created
SnapshotID string `mapstructure:"snapshot_id"`
// The ID of another block storage volume from which the current volume was created
SourceVolID string `mapstructure:"source_volid"`
// Arbitrary key-value pairs defined by the user.
Metadata map[string]string `mapstructure:"metadata"`
// Unique identifier for the volume.
ID string `mapstructure:"id"`
// Size of the volume in GB.
Size int `mapstructure:"size"`
}
// CreateResult contains the response body and error from a Create request.
type CreateResult struct {
commonResult
}
// GetResult contains the response body and error from a Get request.
type GetResult struct {
commonResult
}
// DeleteResult contains the response body and error from a Delete request.
type DeleteResult struct {
gophercloud.ErrResult
}
// ListResult is a pagination.pager that is returned from a call to the List function.
type ListResult struct {
pagination.SinglePageBase
}
// IsEmpty returns true if a ListResult contains no Volumes.
func (r ListResult) IsEmpty() (bool, error) {
volumes, err := ExtractVolumes(r)
if err != nil {
return true, err
}
return len(volumes) == 0, nil
}
// ExtractVolumes extracts and returns Volumes. It is used while iterating over a volumes.List call.
func ExtractVolumes(page pagination.Page) ([]Volume, error) {
var response struct {
Volumes []Volume `json:"volumes"`
}
err := mapstructure.Decode(page.(ListResult).Body, &response)
return response.Volumes, err
}
// UpdateResult contains the response body and error from an Update request.
type UpdateResult struct {
commonResult
}
type commonResult struct {
gophercloud.Result
}
// Extract will get the Volume object out of the commonResult object.
func (r commonResult) Extract() (*Volume, error) {
if r.Err != nil {
return nil, r.Err
}
var res struct {
Volume *Volume `json:"volume"`
}
err := mapstructure.Decode(r.Body, &res)
return res.Volume, err
}

View File

@ -1,23 +0,0 @@
package volumes
import "github.com/rackspace/gophercloud"
func createURL(c *gophercloud.ServiceClient) string {
return c.ServiceURL("volumes")
}
func listURL(c *gophercloud.ServiceClient) string {
return createURL(c)
}
func deleteURL(c *gophercloud.ServiceClient, id string) string {
return c.ServiceURL("volumes", id)
}
func getURL(c *gophercloud.ServiceClient, id string) string {
return deleteURL(c, id)
}
func updateURL(c *gophercloud.ServiceClient, id string) string {
return deleteURL(c, id)
}

View File

@ -1,22 +0,0 @@
package volumes
import (
"github.com/rackspace/gophercloud"
)
// WaitForStatus will continually poll the resource, checking for a particular
// status. It will do this for the amount of seconds defined.
func WaitForStatus(c *gophercloud.ServiceClient, id, status string, secs int) error {
return gophercloud.WaitFor(secs, func() (bool, error) {
current, err := Get(c, id).Extract()
if err != nil {
return false, err
}
if current.Status == status {
return true, nil
}
return false, nil
})
}

View File

@ -1,5 +0,0 @@
// Package volumes provides information and interaction with volumes in the
// OpenStack Block Storage service. A volume is a detachable block storage
// device, akin to a USB hard drive. It can only be attached to one instance at
// a time.
package volumes

View File

@ -1,204 +0,0 @@
package volumes
import (
"fmt"
"net/http"
"testing"
th "github.com/rackspace/gophercloud/testhelper"
fake "github.com/rackspace/gophercloud/testhelper/client"
)
func MockListResponse(t *testing.T) {
th.Mux.HandleFunc("/volumes/detail", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "GET")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, `
{
"volumes": [
{
"volume_type": "lvmdriver-1",
"created_at": "2015-09-17T03:35:03.000000",
"bootable": "false",
"name": "vol-001",
"os-vol-mig-status-attr:name_id": null,
"consistencygroup_id": null,
"source_volid": null,
"os-volume-replication:driver_data": null,
"multiattach": false,
"snapshot_id": null,
"replication_status": "disabled",
"os-volume-replication:extended_status": null,
"encrypted": false,
"os-vol-host-attr:host": null,
"availability_zone": "nova",
"attachments": [
{
"attachment_id": "03987cd1-0ad5-40d1-9b2a-7cc48295d4fa",
"id": "47e9ecc5-4045-4ee3-9a4b-d859d546a0cf",
"volume_id": "289da7f8-6440-407c-9fb4-7db01ec49164",
"server_id": "d1c4788b-9435-42e2-9b81-29f3be1cd01f",
"host_name": "stack",
"device": "/dev/vdc"
}
],
"id": "289da7f8-6440-407c-9fb4-7db01ec49164",
"size": 75,
"user_id": "ff1ce52c03ab433aaba9108c2e3ef541",
"os-vol-tenant-attr:tenant_id": "304dc00909ac4d0da6c62d816bcb3459",
"os-vol-mig-status-attr:migstat": null,
"metadata": {"foo": "bar"},
"status": "available",
"description": null
},
{
"volume_type": "lvmdriver-1",
"created_at": "2015-09-17T03:32:29.000000",
"bootable": "false",
"name": "vol-002",
"os-vol-mig-status-attr:name_id": null,
"consistencygroup_id": null,
"source_volid": null,
"os-volume-replication:driver_data": null,
"multiattach": false,
"snapshot_id": null,
"replication_status": "disabled",
"os-volume-replication:extended_status": null,
"encrypted": false,
"os-vol-host-attr:host": null,
"availability_zone": "nova",
"attachments": [],
"id": "96c3bda7-c82a-4f50-be73-ca7621794835",
"size": 75,
"user_id": "ff1ce52c03ab433aaba9108c2e3ef541",
"os-vol-tenant-attr:tenant_id": "304dc00909ac4d0da6c62d816bcb3459",
"os-vol-mig-status-attr:migstat": null,
"metadata": {},
"status": "available",
"description": null
}
]
}
`)
})
}
func MockGetResponse(t *testing.T) {
th.Mux.HandleFunc("/volumes/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "GET")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, `
{
"volume": {
"volume_type": "lvmdriver-1",
"created_at": "2015-09-17T03:32:29.000000",
"bootable": "false",
"name": "vol-001",
"os-vol-mig-status-attr:name_id": null,
"consistencygroup_id": null,
"source_volid": null,
"os-volume-replication:driver_data": null,
"multiattach": false,
"snapshot_id": null,
"replication_status": "disabled",
"os-volume-replication:extended_status": null,
"encrypted": false,
"os-vol-host-attr:host": null,
"availability_zone": "nova",
"attachments": [{
"attachment_id": "dbce64e3-f3b9-4423-a44f-a2b15deffa1b",
"id": "3eafc6f5-ed74-456d-90fb-f253f594dbae",
"volume_id": "d32019d3-bc6e-4319-9c1d-6722fc136a22",
"server_id": "d1c4788b-9435-42e2-9b81-29f3be1cd01f",
"host_name": "stack",
"device": "/dev/vdd"
}],
"id": "d32019d3-bc6e-4319-9c1d-6722fc136a22",
"size": 75,
"user_id": "ff1ce52c03ab433aaba9108c2e3ef541",
"os-vol-tenant-attr:tenant_id": "304dc00909ac4d0da6c62d816bcb3459",
"os-vol-mig-status-attr:migstat": null,
"metadata": {},
"status": "available",
"description": null
}
}
`)
})
}
func MockCreateResponse(t *testing.T) {
th.Mux.HandleFunc("/volumes", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "POST")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
th.TestHeader(t, r, "Content-Type", "application/json")
th.TestHeader(t, r, "Accept", "application/json")
th.TestJSONRequest(t, r, `
{
"volume": {
"name": "vol-001",
"size": 75
}
}
`)
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusAccepted)
fmt.Fprintf(w, `
{
"volume": {
"size": 75,
"id": "d32019d3-bc6e-4319-9c1d-6722fc136a22",
"metadata": {},
"created_at": "2015-09-17T03:32:29.044216",
"encrypted": false,
"bootable": "false",
"availability_zone": "nova",
"attachments": [],
"user_id": "ff1ce52c03ab433aaba9108c2e3ef541",
"status": "creating",
"description": null,
"volume_type": "lvmdriver-1",
"name": "vol-001",
"replication_status": "disabled",
"consistencygroup_id": null,
"source_volid": null,
"snapshot_id": null,
"multiattach": false
}
}
`)
})
}
func MockDeleteResponse(t *testing.T) {
th.Mux.HandleFunc("/volumes/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "DELETE")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
w.WriteHeader(http.StatusAccepted)
})
}
func MockUpdateResponse(t *testing.T) {
th.Mux.HandleFunc("/volumes/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "PUT")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, `
{
"volume": {
"name": "vol-002"
}
}
`)
})
}

View File

@ -1,251 +0,0 @@
package volumes
import (
"fmt"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
)
// CreateOptsBuilder allows extensions to add additional parameters to the
// Create request.
type CreateOptsBuilder interface {
ToVolumeCreateMap() (map[string]interface{}, error)
}
// CreateOpts contains options for creating a Volume. This object is passed to
// the volumes.Create function. For more information about these parameters,
// see the Volume object.
type CreateOpts struct {
// The availability zone [OPTIONAL]
AvailabilityZone string
// ConsistencyGroupID is the ID of a consistency group [OPTINAL]
ConsistencyGroupID string
// The volume description [OPTIONAL]
Description string
// One or more metadata key and value pairs to associate with the volume [OPTIONAL]
Metadata map[string]string
// The volume name [OPTIONAL]
Name string
// The size of the volume, in gibibytes (GiB) [REQUIRED]
Size int
// the ID of the existing volume snapshot [OPTIONAL]
SnapshotID string
// SourceReplica is a UUID of an existing volume to replicate with [OPTIONAL]
SourceReplica string
// the ID of the existing volume [OPTIONAL]
SourceVolID string
// The ID of the image from which you want to create the volume.
// Required to create a bootable volume.
ImageID string
// The associated volume type [OPTIONAL]
VolumeType string
}
// ToVolumeCreateMap assembles a request body based on the contents of a
// CreateOpts.
func (opts CreateOpts) ToVolumeCreateMap() (map[string]interface{}, error) {
v := make(map[string]interface{})
if opts.Size == 0 {
return nil, fmt.Errorf("Required CreateOpts field 'Size' not set.")
}
v["size"] = opts.Size
if opts.AvailabilityZone != "" {
v["availability_zone"] = opts.AvailabilityZone
}
if opts.ConsistencyGroupID != "" {
v["consistencygroup_id"] = opts.ConsistencyGroupID
}
if opts.Description != "" {
v["description"] = opts.Description
}
if opts.ImageID != "" {
v["imageRef"] = opts.ImageID
}
if opts.Metadata != nil {
v["metadata"] = opts.Metadata
}
if opts.Name != "" {
v["name"] = opts.Name
}
if opts.SourceReplica != "" {
v["source_replica"] = opts.SourceReplica
}
if opts.SourceVolID != "" {
v["source_volid"] = opts.SourceVolID
}
if opts.SnapshotID != "" {
v["snapshot_id"] = opts.SnapshotID
}
if opts.VolumeType != "" {
v["volume_type"] = opts.VolumeType
}
return map[string]interface{}{"volume": v}, nil
}
// Create will create a new Volume based on the values in CreateOpts. To extract
// the Volume object from the response, call the Extract method on the
// CreateResult.
func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult {
var res CreateResult
reqBody, err := opts.ToVolumeCreateMap()
if err != nil {
res.Err = err
return res
}
_, res.Err = client.Post(createURL(client), reqBody, &res.Body, &gophercloud.RequestOpts{
OkCodes: []int{202},
})
return res
}
// Delete will delete the existing Volume with the provided ID.
func Delete(client *gophercloud.ServiceClient, id string) DeleteResult {
var res DeleteResult
_, res.Err = client.Delete(deleteURL(client, id), nil)
return res
}
// Get retrieves the Volume with the provided ID. To extract the Volume object
// from the response, call the Extract method on the GetResult.
func Get(client *gophercloud.ServiceClient, id string) GetResult {
var res GetResult
_, res.Err = client.Get(getURL(client, id), &res.Body, nil)
return res
}
// ListOptsBuilder allows extensions to add additional parameters to the List
// request.
type ListOptsBuilder interface {
ToVolumeListQuery() (string, error)
}
// ListOpts holds options for listing Volumes. It is passed to the volumes.List
// function.
type ListOpts struct {
// admin-only option. Set it to true to see all tenant volumes.
AllTenants bool `q:"all_tenants"`
// List only volumes that contain Metadata.
Metadata map[string]string `q:"metadata"`
// List only volumes that have Name as the display name.
Name string `q:"name"`
// List only volumes that have a status of Status.
Status string `q:"status"`
}
// ToVolumeListQuery formats a ListOpts into a query string.
func (opts ListOpts) ToVolumeListQuery() (string, error) {
q, err := gophercloud.BuildQueryString(opts)
if err != nil {
return "", err
}
return q.String(), nil
}
// List returns Volumes optionally limited by the conditions provided in ListOpts.
func List(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
url := listURL(client)
if opts != nil {
query, err := opts.ToVolumeListQuery()
if err != nil {
return pagination.Pager{Err: err}
}
url += query
}
createPage := func(r pagination.PageResult) pagination.Page {
return ListResult{pagination.SinglePageBase(r)}
}
return pagination.NewPager(client, url, createPage)
}
// UpdateOptsBuilder allows extensions to add additional parameters to the
// Update request.
type UpdateOptsBuilder interface {
ToVolumeUpdateMap() (map[string]interface{}, error)
}
// UpdateOpts contain options for updating an existing Volume. This object is passed
// to the volumes.Update function. For more information about the parameters, see
// the Volume object.
type UpdateOpts struct {
// OPTIONAL
Name string
// OPTIONAL
Description string
// OPTIONAL
Metadata map[string]string
}
// ToVolumeUpdateMap assembles a request body based on the contents of an
// UpdateOpts.
func (opts UpdateOpts) ToVolumeUpdateMap() (map[string]interface{}, error) {
v := make(map[string]interface{})
if opts.Description != "" {
v["description"] = opts.Description
}
if opts.Metadata != nil {
v["metadata"] = opts.Metadata
}
if opts.Name != "" {
v["name"] = opts.Name
}
return map[string]interface{}{"volume": v}, nil
}
// Update will update the Volume with provided information. To extract the updated
// Volume from the response, call the Extract method on the UpdateResult.
func Update(client *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) UpdateResult {
var res UpdateResult
reqBody, err := opts.ToVolumeUpdateMap()
if err != nil {
res.Err = err
return res
}
_, res.Err = client.Put(updateURL(client, id), reqBody, &res.Body, &gophercloud.RequestOpts{
OkCodes: []int{200},
})
return res
}
// IDFromName is a convienience function that returns a server's ID given its name.
func IDFromName(client *gophercloud.ServiceClient, name string) (string, error) {
volumeCount := 0
volumeID := ""
if name == "" {
return "", fmt.Errorf("A volume name must be provided.")
}
pager := List(client, nil)
pager.EachPage(func(page pagination.Page) (bool, error) {
volumeList, err := ExtractVolumes(page)
if err != nil {
return false, err
}
for _, s := range volumeList {
if s.Name == name {
volumeCount++
volumeID = s.ID
}
}
return true, nil
})
switch volumeCount {
case 0:
return "", fmt.Errorf("Unable to find volume: %s", name)
case 1:
return volumeID, nil
default:
return "", fmt.Errorf("Found %d volumes matching %s", volumeCount, name)
}
}

View File

@ -1,137 +0,0 @@
package volumes
import (
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
"github.com/mitchellh/mapstructure"
)
// Volume contains all the information associated with an OpenStack Volume.
type Volume struct {
// Instances onto which the volume is attached.
Attachments []map[string]interface{} `mapstructure:"attachments"`
// AvailabilityZone is which availability zone the volume is in.
AvailabilityZone string `mapstructure:"availability_zone"`
// Indicates whether this is a bootable volume.
Bootable string `mapstructure:"bootable"`
// ConsistencyGroupID is the consistency group ID.
ConsistencyGroupID string `mapstructure:"consistencygroup_id"`
// The date when this volume was created.
CreatedAt string `mapstructure:"created_at"`
// Human-readable description for the volume.
Description string `mapstructure:"description"`
// Encrypted denotes if the volume is encrypted.
Encrypted bool `mapstructure:"encrypted"`
// Human-readable display name for the volume.
Name string `mapstructure:"name"`
// The type of volume to create, either SATA or SSD.
VolumeType string `mapstructure:"volume_type"`
// ReplicationDriverData contains data about the replication driver.
ReplicationDriverData string `mapstructure:"os-volume-replication:driver_data"`
// ReplicationExtendedStatus contains extended status about replication.
ReplicationExtendedStatus string `mapstructure:"os-volume-replication:extended_status"`
// ReplicationStatus is the status of replication.
ReplicationStatus string `mapstructure:"replication_status"`
// The ID of the snapshot from which the volume was created
SnapshotID string `mapstructure:"snapshot_id"`
// The ID of another block storage volume from which the current volume was created
SourceVolID string `mapstructure:"source_volid"`
// Current status of the volume.
Status string `mapstructure:"status"`
// TenantID is the id of the project that owns the volume.
TenantID string `mapstructure:"os-vol-tenant-attr:tenant_id"`
// Arbitrary key-value pairs defined by the user.
Metadata map[string]string `mapstructure:"metadata"`
// Multiattach denotes if the volume is multi-attach capable.
Multiattach bool `mapstructure:"multiattach"`
// Unique identifier for the volume.
ID string `mapstructure:"id"`
// Size of the volume in GB.
Size int `mapstructure:"size"`
// UserID is the id of the user who created the volume.
UserID string `mapstructure:"user_id"`
}
// CreateResult contains the response body and error from a Create request.
type CreateResult struct {
commonResult
}
// GetResult contains the response body and error from a Get request.
type GetResult struct {
commonResult
}
// DeleteResult contains the response body and error from a Delete request.
type DeleteResult struct {
gophercloud.ErrResult
}
// ListResult is a pagination.pager that is returned from a call to the List function.
type ListResult struct {
pagination.SinglePageBase
}
// IsEmpty returns true if a ListResult contains no Volumes.
func (r ListResult) IsEmpty() (bool, error) {
volumes, err := ExtractVolumes(r)
if err != nil {
return true, err
}
return len(volumes) == 0, nil
}
// ExtractVolumes extracts and returns Volumes. It is used while iterating over a volumes.List call.
func ExtractVolumes(page pagination.Page) ([]Volume, error) {
var response struct {
Volumes []Volume `json:"volumes"`
}
err := mapstructure.Decode(page.(ListResult).Body, &response)
return response.Volumes, err
}
// UpdateResult contains the response body and error from an Update request.
type UpdateResult struct {
commonResult
}
type commonResult struct {
gophercloud.Result
}
// Extract will get the Volume object out of the commonResult object.
func (r commonResult) Extract() (*Volume, error) {
if r.Err != nil {
return nil, r.Err
}
var res struct {
Volume *Volume `json:"volume"`
}
err := mapstructure.Decode(r.Body, &res)
return res.Volume, err
}

View File

@ -1,23 +0,0 @@
package volumes
import "github.com/rackspace/gophercloud"
func createURL(c *gophercloud.ServiceClient) string {
return c.ServiceURL("volumes")
}
func listURL(c *gophercloud.ServiceClient) string {
return c.ServiceURL("volumes", "detail")
}
func deleteURL(c *gophercloud.ServiceClient, id string) string {
return c.ServiceURL("volumes", id)
}
func getURL(c *gophercloud.ServiceClient, id string) string {
return deleteURL(c, id)
}
func updateURL(c *gophercloud.ServiceClient, id string) string {
return deleteURL(c, id)
}

View File

@ -1,22 +0,0 @@
package volumes
import (
"github.com/rackspace/gophercloud"
)
// WaitForStatus will continually poll the resource, checking for a particular
// status. It will do this for the amount of seconds defined.
func WaitForStatus(c *gophercloud.ServiceClient, id, status string, secs int) error {
return gophercloud.WaitFor(secs, func() (bool, error) {
current, err := Get(c, id).Extract()
if err != nil {
return false, err
}
if current.Status == status {
return true, nil
}
return false, nil
})
}

View File

@ -1,346 +0,0 @@
package openstack
import (
"fmt"
"net/url"
"strconv"
"strings"
"github.com/rackspace/gophercloud"
tokens2 "github.com/rackspace/gophercloud/openstack/identity/v2/tokens"
tokens3 "github.com/rackspace/gophercloud/openstack/identity/v3/tokens"
"github.com/rackspace/gophercloud/openstack/utils"
)
const (
v20 = "v2.0"
v30 = "v3.0"
)
// NewClient prepares an unauthenticated ProviderClient instance.
// Most users will probably prefer using the AuthenticatedClient function instead.
// This is useful if you wish to explicitly control the version of the identity service that's used for authentication explicitly,
// for example.
func NewClient(endpoint string) (*gophercloud.ProviderClient, error) {
u, err := url.Parse(endpoint)
if err != nil {
return nil, err
}
u.RawQuery, u.Fragment = "", ""
// Base is url with path
endpoint = gophercloud.NormalizeURL(endpoint)
base := gophercloud.NormalizeURL(u.String())
path := u.Path
if !strings.HasSuffix(path, "/") {
path = path + "/"
}
parts := strings.Split(path[0:len(path)-1], "/")
for index,version := range(parts) {
if 2 <= len(version) && len(version) <= 4 && strings.HasPrefix(version, "v") {
_, err := strconv.ParseFloat(version[1:], 64)
if err == nil {
// post version suffixes in path are not supported
// version must be on the last index
if index < len(parts) - 1 {
return nil, fmt.Errorf("Path suffixes (after version) are not supported.")
}
switch version {
case "v2.0", "v3":
// valid version found, strip from base
return &gophercloud.ProviderClient{
IdentityBase: base[0:len(base)-len(version)-1],
IdentityEndpoint: endpoint,
}, nil
default:
return nil, fmt.Errorf("Invalid identity endpoint version %v. Supported versions: v2.0, v3", version)
}
}
}
}
return &gophercloud.ProviderClient{
IdentityBase: base,
IdentityEndpoint: "",
}, nil
}
// AuthenticatedClient logs in to an OpenStack cloud found at the identity endpoint specified by options, acquires a token, and
// returns a Client instance that's ready to operate.
// It first queries the root identity endpoint to determine which versions of the identity service are supported, then chooses
// the most recent identity service available to proceed.
func AuthenticatedClient(options gophercloud.AuthOptions) (*gophercloud.ProviderClient, error) {
client, err := NewClient(options.IdentityEndpoint)
if err != nil {
return nil, err
}
err = Authenticate(client, options)
if err != nil {
return nil, err
}
return client, nil
}
// Authenticate or re-authenticate against the most recent identity service supported at the provided endpoint.
func Authenticate(client *gophercloud.ProviderClient, options gophercloud.AuthOptions) error {
versions := []*utils.Version{
{ID: v20, Priority: 20, Suffix: "/v2.0/"},
{ID: v30, Priority: 30, Suffix: "/v3/"},
}
chosen, endpoint, err := utils.ChooseVersion(client, versions)
if err != nil {
return err
}
switch chosen.ID {
case v20:
return v2auth(client, endpoint, options)
case v30:
return v3auth(client, endpoint, options)
default:
// The switch statement must be out of date from the versions list.
return fmt.Errorf("Unrecognized identity version: %s", chosen.ID)
}
}
// AuthenticateV2 explicitly authenticates against the identity v2 endpoint.
func AuthenticateV2(client *gophercloud.ProviderClient, options gophercloud.AuthOptions) error {
return v2auth(client, "", options)
}
func v2auth(client *gophercloud.ProviderClient, endpoint string, options gophercloud.AuthOptions) error {
v2Client := NewIdentityV2(client)
if endpoint != "" {
v2Client.Endpoint = endpoint
}
result := tokens2.Create(v2Client, tokens2.AuthOptions{AuthOptions: options})
token, err := result.ExtractToken()
if err != nil {
return err
}
catalog, err := result.ExtractServiceCatalog()
if err != nil {
return err
}
if options.AllowReauth {
client.ReauthFunc = func() error {
client.TokenID = ""
return v2auth(client, endpoint, options)
}
}
client.TokenID = token.ID
client.EndpointLocator = func(opts gophercloud.EndpointOpts) (string, error) {
return V2EndpointURL(catalog, opts)
}
return nil
}
// AuthenticateV3 explicitly authenticates against the identity v3 service.
func AuthenticateV3(client *gophercloud.ProviderClient, options gophercloud.AuthOptions) error {
return v3auth(client, "", options)
}
func v3auth(client *gophercloud.ProviderClient, endpoint string, options gophercloud.AuthOptions) error {
// Override the generated service endpoint with the one returned by the version endpoint.
v3Client := NewIdentityV3(client)
if endpoint != "" {
v3Client.Endpoint = endpoint
}
// copy the auth options to a local variable that we can change. `options`
// needs to stay as-is for reauth purposes
v3Options := options
var scope *tokens3.Scope
if options.TenantID != "" {
scope = &tokens3.Scope{
ProjectID: options.TenantID,
}
v3Options.TenantID = ""
v3Options.TenantName = ""
} else {
if options.TenantName != "" {
scope = &tokens3.Scope{
ProjectName: options.TenantName,
DomainID: options.DomainID,
DomainName: options.DomainName,
}
v3Options.TenantName = ""
}
}
result := tokens3.Create(v3Client, v3Options, scope)
token, err := result.ExtractToken()
if err != nil {
return err
}
catalog, err := result.ExtractServiceCatalog()
if err != nil {
return err
}
client.TokenID = token.ID
if options.AllowReauth {
client.ReauthFunc = func() error {
client.TokenID = ""
return v3auth(client, endpoint, options)
}
}
client.EndpointLocator = func(opts gophercloud.EndpointOpts) (string, error) {
return V3EndpointURL(catalog, opts)
}
return nil
}
// NewIdentityV2 creates a ServiceClient that may be used to interact with the v2 identity service.
func NewIdentityV2(client *gophercloud.ProviderClient) *gophercloud.ServiceClient {
v2Endpoint := client.IdentityBase + "v2.0/"
return &gophercloud.ServiceClient{
ProviderClient: client,
Endpoint: v2Endpoint,
}
}
// NewIdentityV3 creates a ServiceClient that may be used to access the v3 identity service.
func NewIdentityV3(client *gophercloud.ProviderClient) *gophercloud.ServiceClient {
v3Endpoint := client.IdentityBase + "v3/"
return &gophercloud.ServiceClient{
ProviderClient: client,
Endpoint: v3Endpoint,
}
}
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")
url, err := client.EndpointLocator(eo)
if err != nil {
return nil, err
}
return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil
}
// NewComputeV2 creates a ServiceClient that may be used with the v2 compute package.
func NewComputeV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
eo.ApplyDefaults("compute")
url, err := client.EndpointLocator(eo)
if err != nil {
return nil, err
}
return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil
}
// NewNetworkV2 creates a ServiceClient that may be used with the v2 network package.
func NewNetworkV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
eo.ApplyDefaults("network")
url, err := client.EndpointLocator(eo)
if err != nil {
return nil, err
}
return &gophercloud.ServiceClient{
ProviderClient: client,
Endpoint: url,
ResourceBase: url + "v2.0/",
}, nil
}
// NewBlockStorageV1 creates a ServiceClient that may be used to access the v1 block storage service.
func NewBlockStorageV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
eo.ApplyDefaults("volume")
url, err := client.EndpointLocator(eo)
if err != nil {
return nil, err
}
return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil
}
// NewBlockStorageV2 creates a ServiceClient that may be used to access the v2 block storage service.
func NewBlockStorageV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
eo.ApplyDefaults("volumev2")
url, err := client.EndpointLocator(eo)
if err != nil {
return nil, err
}
return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil
}
// NewCDNV1 creates a ServiceClient that may be used to access the OpenStack v1
// CDN service.
func NewCDNV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
eo.ApplyDefaults("cdn")
url, err := client.EndpointLocator(eo)
if err != nil {
return nil, err
}
return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil
}
// NewOrchestrationV1 creates a ServiceClient that may be used to access the v1 orchestration service.
func NewOrchestrationV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
eo.ApplyDefaults("orchestration")
url, err := client.EndpointLocator(eo)
if err != nil {
return nil, err
}
return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil
}
// 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

@ -1,120 +0,0 @@
package bootfromvolume
import (
"errors"
"strconv"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/openstack/compute/v2/servers"
)
// SourceType represents the type of medium being used to create the volume.
type SourceType string
const (
Volume SourceType = "volume"
Snapshot SourceType = "snapshot"
Image SourceType = "image"
Blank SourceType = "blank"
)
// BlockDevice is a structure with options for booting a server instance
// from a volume. The volume may be created from an image, snapshot, or another
// volume.
type BlockDevice struct {
// BootIndex [optional] is the boot index. It defaults to 0.
BootIndex int `json:"boot_index"`
// DeleteOnTermination [optional] specifies whether or not to delete the attached volume
// when the server is deleted. Defaults to `false`.
DeleteOnTermination bool `json:"delete_on_termination"`
// DestinationType [optional] is the type that gets created. Possible values are "volume"
// 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"`
// UUID [required] is the unique identifier for the volume, snapshot, or image (see above)
UUID string `json:"uuid"`
// VolumeSize [optional] is the size of the volume to create (in gigabytes).
VolumeSize int `json:"volume_size"`
}
// CreateOptsExt is a structure that extends the server `CreateOpts` structure
// by allowing for a block device mapping.
type CreateOptsExt struct {
servers.CreateOptsBuilder
BlockDevice []BlockDevice `json:"block_device_mapping_v2,omitempty"`
}
// ToServerCreateMap adds the block device mapping option to the base server
// creation options.
func (opts CreateOptsExt) ToServerCreateMap() (map[string]interface{}, error) {
base, err := opts.CreateOptsBuilder.ToServerCreateMap()
if err != nil {
return nil, err
}
if len(opts.BlockDevice) == 0 {
return nil, errors.New("Required fields UUID and SourceType not set.")
}
serverMap := base["server"].(map[string]interface{})
blockDevice := make([]map[string]interface{}, len(opts.BlockDevice))
for i, bd := range opts.BlockDevice {
if string(bd.SourceType) == "" {
return nil, errors.New("SourceType must be one of: volume, image, snapshot.")
}
blockDevice[i] = make(map[string]interface{})
blockDevice[i]["source_type"] = bd.SourceType
blockDevice[i]["boot_index"] = strconv.Itoa(bd.BootIndex)
blockDevice[i]["delete_on_termination"] = strconv.FormatBool(bd.DeleteOnTermination)
if bd.VolumeSize > 0 {
blockDevice[i]["volume_size"] = strconv.Itoa(bd.VolumeSize)
}
if bd.UUID != "" {
blockDevice[i]["uuid"] = bd.UUID
}
if bd.DestinationType != "" {
blockDevice[i]["destination_type"] = bd.DestinationType
}
if bd.GuestFormat != "" {
blockDevice[i]["guest_format"] = bd.GuestFormat
}
}
serverMap["block_device_mapping_v2"] = blockDevice
return base, nil
}
// Create requests the creation of a server from the given block device mapping.
func Create(client *gophercloud.ServiceClient, opts servers.CreateOptsBuilder) servers.CreateResult {
var res servers.CreateResult
reqBody, err := opts.ToServerCreateMap()
if err != nil {
res.Err = err
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},
})
return res
}

View File

@ -1,10 +0,0 @@
package bootfromvolume
import (
os "github.com/rackspace/gophercloud/openstack/compute/v2/servers"
)
// CreateResult temporarily contains the response from a Create call.
type CreateResult struct {
os.CreateResult
}

View File

@ -1,7 +0,0 @@
package bootfromvolume
import "github.com/rackspace/gophercloud"
func createURL(c *gophercloud.ServiceClient) string {
return c.ServiceURL("os-volumes_boot")
}

View File

@ -1,3 +0,0 @@
// Package floatingip provides the ability to manage floating ips through
// nova-network
package floatingip

View File

@ -1,193 +0,0 @@
// +build fixtures
package floatingip
import (
"fmt"
"net/http"
"testing"
th "github.com/rackspace/gophercloud/testhelper"
"github.com/rackspace/gophercloud/testhelper/client"
)
// ListOutput is a sample response to a List call.
const ListOutput = `
{
"floating_ips": [
{
"fixed_ip": null,
"id": 1,
"instance_id": null,
"ip": "10.10.10.1",
"pool": "nova"
},
{
"fixed_ip": "166.78.185.201",
"id": 2,
"instance_id": "4d8c3732-a248-40ed-bebc-539a6ffd25c0",
"ip": "10.10.10.2",
"pool": "nova"
}
]
}
`
// GetOutput is a sample response to a Get call.
const GetOutput = `
{
"floating_ip": {
"fixed_ip": "166.78.185.201",
"id": 2,
"instance_id": "4d8c3732-a248-40ed-bebc-539a6ffd25c0",
"ip": "10.10.10.2",
"pool": "nova"
}
}
`
// CreateOutput is a sample response to a Post call
const CreateOutput = `
{
"floating_ip": {
"fixed_ip": null,
"id": 1,
"instance_id": null,
"ip": "10.10.10.1",
"pool": "nova"
}
}
`
// FirstFloatingIP is the first result in ListOutput.
var FirstFloatingIP = FloatingIP{
ID: "1",
IP: "10.10.10.1",
Pool: "nova",
}
// SecondFloatingIP is the first result in ListOutput.
var SecondFloatingIP = FloatingIP{
FixedIP: "166.78.185.201",
ID: "2",
InstanceID: "4d8c3732-a248-40ed-bebc-539a6ffd25c0",
IP: "10.10.10.2",
Pool: "nova",
}
// ExpectedFloatingIPsSlice is the slice of results that should be parsed
// from ListOutput, in the expected order.
var ExpectedFloatingIPsSlice = []FloatingIP{FirstFloatingIP, SecondFloatingIP}
// CreatedFloatingIP is the parsed result from CreateOutput.
var CreatedFloatingIP = FloatingIP{
ID: "1",
IP: "10.10.10.1",
Pool: "nova",
}
// HandleListSuccessfully configures the test server to respond to a List request.
func HandleListSuccessfully(t *testing.T) {
th.Mux.HandleFunc("/os-floating-ips", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "GET")
th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
w.Header().Add("Content-Type", "application/json")
fmt.Fprintf(w, ListOutput)
})
}
// HandleGetSuccessfully configures the test server to respond to a Get request
// for an existing floating ip
func HandleGetSuccessfully(t *testing.T) {
th.Mux.HandleFunc("/os-floating-ips/2", 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)
})
}
// HandleCreateSuccessfully configures the test server to respond to a Create request
// for a new floating ip
func HandleCreateSuccessfully(t *testing.T) {
th.Mux.HandleFunc("/os-floating-ips", 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, `
{
"pool": "nova"
}
`)
w.Header().Add("Content-Type", "application/json")
fmt.Fprintf(w, CreateOutput)
})
}
// HandleDeleteSuccessfully configures the test server to respond to a Delete request for a
// an existing floating ip
func HandleDeleteSuccessfully(t *testing.T) {
th.Mux.HandleFunc("/os-floating-ips/1", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "DELETE")
th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
w.WriteHeader(http.StatusAccepted)
})
}
// HandleAssociateSuccessfully configures the test server to respond to a Post request
// to associate an allocated floating IP
func HandleAssociateSuccessfully(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"
}
}
`)
w.WriteHeader(http.StatusAccepted)
})
}
// 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) {
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, `
{
"removeFloatingIp": {
"address": "10.10.10.2"
}
}
`)
w.WriteHeader(http.StatusAccepted)
})
}

View File

@ -1,171 +0,0 @@
package floatingip
import (
"errors"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
)
// List returns a Pager that allows you to iterate over a collection of FloatingIPs.
func List(client *gophercloud.ServiceClient) pagination.Pager {
return pagination.NewPager(client, listURL(client), func(r pagination.PageResult) pagination.Page {
return FloatingIPsPage{pagination.SinglePageBase(r)}
})
}
// CreateOptsBuilder describes struct types that can be accepted by the Create call. Notable, the
// CreateOpts struct in this package does.
type CreateOptsBuilder interface {
ToFloatingIPCreateMap() (map[string]interface{}, error)
}
// CreateOpts specifies a Floating IP allocation request
type CreateOpts struct {
// Pool is the pool of floating IPs to allocate one from
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 == "" {
return nil, errors.New("Missing field required for floating IP creation: Pool")
}
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
reqBody, err := opts.ToFloatingIPCreateMap()
if err != nil {
res.Err = err
return res
}
_, res.Err = client.Post(createURL(client), reqBody, &res.Body, &gophercloud.RequestOpts{
OkCodes: []int{200},
})
return res
}
// Get returns data about a previously created FloatingIP.
func Get(client *gophercloud.ServiceClient, id string) GetResult {
var res GetResult
_, res.Err = client.Get(getURL(client, id), &res.Body, nil)
return res
}
// Delete requests the deletion of a previous allocated FloatingIP.
func Delete(client *gophercloud.ServiceClient, id string) DeleteResult {
var res DeleteResult
_, res.Err = client.Delete(deleteURL(client, id), nil)
return res
}
// 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
addFloatingIp := make(map[string]interface{})
addFloatingIp["address"] = fip
reqBody := map[string]interface{}{"addFloatingIp": addFloatingIp}
_, res.Err = client.Post(associateURL(client, serverId), reqBody, nil, nil)
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
removeFloatingIp := make(map[string]interface{})
removeFloatingIp["address"] = fip
reqBody := map[string]interface{}{"removeFloatingIp": removeFloatingIp}
_, 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

@ -1,99 +0,0 @@
package floatingip
import (
"github.com/mitchellh/mapstructure"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
)
// A FloatingIP is an IP that can be associated with an instance
type FloatingIP struct {
// ID is a unique ID of the Floating IP
ID string `mapstructure:"id"`
// FixedIP is the IP of the instance related to the Floating IP
FixedIP string `mapstructure:"fixed_ip,omitempty"`
// InstanceID is the ID of the instance that is using the Floating IP
InstanceID string `mapstructure:"instance_id"`
// IP is the actual Floating IP
IP string `mapstructure:"ip"`
// Pool is the pool of floating IPs that this floating IP belongs to
Pool string `mapstructure:"pool"`
}
// FloatingIPsPage stores a single, only page of FloatingIPs
// results from a List call.
type FloatingIPsPage struct {
pagination.SinglePageBase
}
// IsEmpty determines whether or not a FloatingIPsPage is empty.
func (page FloatingIPsPage) IsEmpty() (bool, error) {
va, err := ExtractFloatingIPs(page)
return len(va) == 0, err
}
// ExtractFloatingIPs interprets a page of results as a slice of
// FloatingIPs.
func ExtractFloatingIPs(page pagination.Page) ([]FloatingIP, error) {
casted := page.(FloatingIPsPage).Body
var response struct {
FloatingIPs []FloatingIP `mapstructure:"floating_ips"`
}
err := mapstructure.WeakDecode(casted, &response)
return response.FloatingIPs, err
}
type FloatingIPResult struct {
gophercloud.Result
}
// Extract is a method that attempts to interpret any FloatingIP resource
// response as a FloatingIP struct.
func (r FloatingIPResult) Extract() (*FloatingIP, error) {
if r.Err != nil {
return nil, r.Err
}
var res struct {
FloatingIP *FloatingIP `json:"floating_ip" mapstructure:"floating_ip"`
}
err := mapstructure.WeakDecode(r.Body, &res)
return res.FloatingIP, err
}
// CreateResult is the response from a Create operation. Call its Extract method to interpret it
// as a FloatingIP.
type CreateResult struct {
FloatingIPResult
}
// GetResult is the response from a Get operation. Call its Extract method to interpret it
// as a FloatingIP.
type GetResult struct {
FloatingIPResult
}
// DeleteResult is the response from a Delete operation. Call its Extract method to determine if
// the call succeeded or failed.
type DeleteResult struct {
gophercloud.ErrResult
}
// AssociateResult is the response from a Delete operation. Call its Extract method to determine if
// the call succeeded or failed.
type AssociateResult struct {
gophercloud.ErrResult
}
// DisassociateResult is the response from a Delete operation. Call its Extract method to determine if
// the call succeeded or failed.
type DisassociateResult struct {
gophercloud.ErrResult
}

View File

@ -1,37 +0,0 @@
package floatingip
import "github.com/rackspace/gophercloud"
const resourcePath = "os-floating-ips"
func resourceURL(c *gophercloud.ServiceClient) string {
return c.ServiceURL(resourcePath)
}
func listURL(c *gophercloud.ServiceClient) string {
return resourceURL(c)
}
func createURL(c *gophercloud.ServiceClient) string {
return resourceURL(c)
}
func getURL(c *gophercloud.ServiceClient, id string) string {
return c.ServiceURL(resourcePath, id)
}
func deleteURL(c *gophercloud.ServiceClient, id string) string {
return getURL(c, id)
}
func serverURL(c *gophercloud.ServiceClient, serverId string) string {
return c.ServiceURL("servers/" + serverId + "/action")
}
func associateURL(c *gophercloud.ServiceClient, serverId string) string {
return serverURL(c, serverId)
}
func disassociateURL(c *gophercloud.ServiceClient, serverId string) string {
return serverURL(c, serverId)
}

View File

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

View File

@ -1,171 +0,0 @@
// +build fixtures
package keypairs
import (
"fmt"
"net/http"
"testing"
th "github.com/rackspace/gophercloud/testhelper"
"github.com/rackspace/gophercloud/testhelper/client"
)
// ListOutput is a sample response to a List call.
const ListOutput = `
{
"keypairs": [
{
"keypair": {
"fingerprint": "15:b0:f8:b3:f9:48:63:71:cf:7b:5b:38:6d:44:2d:4a",
"name": "firstkey",
"public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC+Eo/RZRngaGTkFs7I62ZjsIlO79KklKbMXi8F+KITD4bVQHHn+kV+4gRgkgCRbdoDqoGfpaDFs877DYX9n4z6FrAIZ4PES8TNKhatifpn9NdQYWA+IkU8CuvlEKGuFpKRi/k7JLos/gHi2hy7QUwgtRvcefvD/vgQZOVw/mGR9Q== Generated by Nova\n"
}
},
{
"keypair": {
"fingerprint": "35:9d:d0:c3:4a:80:d3:d8:86:f1:ca:f7:df:c4:f9:d8",
"name": "secondkey",
"public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC9mC3WZN9UGLxgPBpP7H5jZMc6pKwOoSgre8yun6REFktn/Kz7DUt9jaR1UJyRzHxITfCfAIgSxPdGqB/oF1suMyWgu5i0625vavLB5z5kC8Hq3qZJ9zJO1poE1kyD+htiTtPWJ88e12xuH2XB/CZN9OpEiF98hAagiOE0EnOS5Q== Generated by Nova\n"
}
}
]
}
`
// GetOutput is a sample response to a Get call.
const GetOutput = `
{
"keypair": {
"public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC+Eo/RZRngaGTkFs7I62ZjsIlO79KklKbMXi8F+KITD4bVQHHn+kV+4gRgkgCRbdoDqoGfpaDFs877DYX9n4z6FrAIZ4PES8TNKhatifpn9NdQYWA+IkU8CuvlEKGuFpKRi/k7JLos/gHi2hy7QUwgtRvcefvD/vgQZOVw/mGR9Q== Generated by Nova\n",
"name": "firstkey",
"fingerprint": "15:b0:f8:b3:f9:48:63:71:cf:7b:5b:38:6d:44:2d:4a"
}
}
`
// CreateOutput is a sample response to a Create call.
const CreateOutput = `
{
"keypair": {
"fingerprint": "35:9d:d0:c3:4a:80:d3:d8:86:f1:ca:f7:df:c4:f9:d8",
"name": "createdkey",
"private_key": "-----BEGIN RSA PRIVATE KEY-----\nMIICXAIBAAKBgQC9mC3WZN9UGLxgPBpP7H5jZMc6pKwOoSgre8yun6REFktn/Kz7\nDUt9jaR1UJyRzHxITfCfAIgSxPdGqB/oF1suMyWgu5i0625vavLB5z5kC8Hq3qZJ\n9zJO1poE1kyD+htiTtPWJ88e12xuH2XB/CZN9OpEiF98hAagiOE0EnOS5QIDAQAB\nAoGAE5XO1mDhORy9COvsg+kYPUhB1GsCYxh+v88wG7HeFDKBY6KUc/Kxo6yoGn5T\nTjRjekyi2KoDZHz4VlIzyZPwFS4I1bf3oCunVoAKzgLdmnTtvRNMC5jFOGc2vUgP\n9bSyRj3S1R4ClVk2g0IDeagko/jc8zzLEYuIK+fbkds79YECQQDt3vcevgegnkga\ntF4NsDmmBPRkcSHCqrANP/7vFcBQN3czxeYYWX3DK07alu6GhH1Y4sHbdm616uU0\nll7xbDzxAkEAzAtN2IyftNygV2EGiaGgqLyo/tD9+Vui2qCQplqe4jvWh/5Sparl\nOjmKo+uAW+hLrLVMnHzRWxbWU8hirH5FNQJATO+ZxCK4etXXAnQmG41NCAqANWB2\nB+2HJbH2NcQ2QHvAHUm741JGn/KI/aBlo7KEjFRDWUVUB5ji64BbUwCsMQJBAIku\nLGcjnBf/oLk+XSPZC2eGd2Ph5G5qYmH0Q2vkTx+wtTn3DV+eNsDfgMtWAJVJ5t61\ngU1QSXyhLPVlKpnnxuUCQC+xvvWjWtsLaFtAsZywJiqLxQzHts8XLGZptYJ5tLWV\nrtmYtBcJCN48RrgQHry/xWYeA4K/AFQpXfNPgprQ96Q=\n-----END RSA PRIVATE KEY-----\n",
"public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC9mC3WZN9UGLxgPBpP7H5jZMc6pKwOoSgre8yun6REFktn/Kz7DUt9jaR1UJyRzHxITfCfAIgSxPdGqB/oF1suMyWgu5i0625vavLB5z5kC8Hq3qZJ9zJO1poE1kyD+htiTtPWJ88e12xuH2XB/CZN9OpEiF98hAagiOE0EnOS5Q== Generated by Nova\n",
"user_id": "fake"
}
}
`
// ImportOutput is a sample response to a Create call that provides its own public key.
const ImportOutput = `
{
"keypair": {
"fingerprint": "1e:2c:9b:56:79:4b:45:77:f9:ca:7a:98:2c:b0:d5:3c",
"name": "importedkey",
"public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDx8nkQv/zgGgB4rMYmIf+6A4l6Rr+o/6lHBQdW5aYd44bd8JttDCE/F/pNRr0lRE+PiqSPO8nDPHw0010JeMH9gYgnnFlyY3/OcJ02RhIPyyxYpv9FhY+2YiUkpwFOcLImyrxEsYXpD/0d3ac30bNH6Sw9JD9UZHYcpSxsIbECHw== Generated by Nova",
"user_id": "fake"
}
}
`
// FirstKeyPair is the first result in ListOutput.
var FirstKeyPair = KeyPair{
Name: "firstkey",
Fingerprint: "15:b0:f8:b3:f9:48:63:71:cf:7b:5b:38:6d:44:2d:4a",
PublicKey: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC+Eo/RZRngaGTkFs7I62ZjsIlO79KklKbMXi8F+KITD4bVQHHn+kV+4gRgkgCRbdoDqoGfpaDFs877DYX9n4z6FrAIZ4PES8TNKhatifpn9NdQYWA+IkU8CuvlEKGuFpKRi/k7JLos/gHi2hy7QUwgtRvcefvD/vgQZOVw/mGR9Q== Generated by Nova\n",
}
// SecondKeyPair is the second result in ListOutput.
var SecondKeyPair = KeyPair{
Name: "secondkey",
Fingerprint: "35:9d:d0:c3:4a:80:d3:d8:86:f1:ca:f7:df:c4:f9:d8",
PublicKey: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC9mC3WZN9UGLxgPBpP7H5jZMc6pKwOoSgre8yun6REFktn/Kz7DUt9jaR1UJyRzHxITfCfAIgSxPdGqB/oF1suMyWgu5i0625vavLB5z5kC8Hq3qZJ9zJO1poE1kyD+htiTtPWJ88e12xuH2XB/CZN9OpEiF98hAagiOE0EnOS5Q== Generated by Nova\n",
}
// ExpectedKeyPairSlice is the slice of results that should be parsed from ListOutput, in the expected
// order.
var ExpectedKeyPairSlice = []KeyPair{FirstKeyPair, SecondKeyPair}
// CreatedKeyPair is the parsed result from CreatedOutput.
var CreatedKeyPair = KeyPair{
Name: "createdkey",
Fingerprint: "35:9d:d0:c3:4a:80:d3:d8:86:f1:ca:f7:df:c4:f9:d8",
PublicKey: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC9mC3WZN9UGLxgPBpP7H5jZMc6pKwOoSgre8yun6REFktn/Kz7DUt9jaR1UJyRzHxITfCfAIgSxPdGqB/oF1suMyWgu5i0625vavLB5z5kC8Hq3qZJ9zJO1poE1kyD+htiTtPWJ88e12xuH2XB/CZN9OpEiF98hAagiOE0EnOS5Q== Generated by Nova\n",
PrivateKey: "-----BEGIN RSA PRIVATE KEY-----\nMIICXAIBAAKBgQC9mC3WZN9UGLxgPBpP7H5jZMc6pKwOoSgre8yun6REFktn/Kz7\nDUt9jaR1UJyRzHxITfCfAIgSxPdGqB/oF1suMyWgu5i0625vavLB5z5kC8Hq3qZJ\n9zJO1poE1kyD+htiTtPWJ88e12xuH2XB/CZN9OpEiF98hAagiOE0EnOS5QIDAQAB\nAoGAE5XO1mDhORy9COvsg+kYPUhB1GsCYxh+v88wG7HeFDKBY6KUc/Kxo6yoGn5T\nTjRjekyi2KoDZHz4VlIzyZPwFS4I1bf3oCunVoAKzgLdmnTtvRNMC5jFOGc2vUgP\n9bSyRj3S1R4ClVk2g0IDeagko/jc8zzLEYuIK+fbkds79YECQQDt3vcevgegnkga\ntF4NsDmmBPRkcSHCqrANP/7vFcBQN3czxeYYWX3DK07alu6GhH1Y4sHbdm616uU0\nll7xbDzxAkEAzAtN2IyftNygV2EGiaGgqLyo/tD9+Vui2qCQplqe4jvWh/5Sparl\nOjmKo+uAW+hLrLVMnHzRWxbWU8hirH5FNQJATO+ZxCK4etXXAnQmG41NCAqANWB2\nB+2HJbH2NcQ2QHvAHUm741JGn/KI/aBlo7KEjFRDWUVUB5ji64BbUwCsMQJBAIku\nLGcjnBf/oLk+XSPZC2eGd2Ph5G5qYmH0Q2vkTx+wtTn3DV+eNsDfgMtWAJVJ5t61\ngU1QSXyhLPVlKpnnxuUCQC+xvvWjWtsLaFtAsZywJiqLxQzHts8XLGZptYJ5tLWV\nrtmYtBcJCN48RrgQHry/xWYeA4K/AFQpXfNPgprQ96Q=\n-----END RSA PRIVATE KEY-----\n",
UserID: "fake",
}
// ImportedKeyPair is the parsed result from ImportOutput.
var ImportedKeyPair = KeyPair{
Name: "importedkey",
Fingerprint: "1e:2c:9b:56:79:4b:45:77:f9:ca:7a:98:2c:b0:d5:3c",
PublicKey: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDx8nkQv/zgGgB4rMYmIf+6A4l6Rr+o/6lHBQdW5aYd44bd8JttDCE/F/pNRr0lRE+PiqSPO8nDPHw0010JeMH9gYgnnFlyY3/OcJ02RhIPyyxYpv9FhY+2YiUkpwFOcLImyrxEsYXpD/0d3ac30bNH6Sw9JD9UZHYcpSxsIbECHw== Generated by Nova",
UserID: "fake",
}
// HandleListSuccessfully configures the test server to respond to a List request.
func HandleListSuccessfully(t *testing.T) {
th.Mux.HandleFunc("/os-keypairs", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "GET")
th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
w.Header().Add("Content-Type", "application/json")
fmt.Fprintf(w, ListOutput)
})
}
// HandleGetSuccessfully configures the test server to respond to a Get request for "firstkey".
func HandleGetSuccessfully(t *testing.T) {
th.Mux.HandleFunc("/os-keypairs/firstkey", 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)
})
}
// HandleCreateSuccessfully configures the test server to respond to a Create request for a new
// keypair called "createdkey".
func HandleCreateSuccessfully(t *testing.T) {
th.Mux.HandleFunc("/os-keypairs", 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, `{ "keypair": { "name": "createdkey" } }`)
w.Header().Add("Content-Type", "application/json")
fmt.Fprintf(w, CreateOutput)
})
}
// HandleImportSuccessfully configures the test server to respond to an Import request for an
// existing keypair called "importedkey".
func HandleImportSuccessfully(t *testing.T) {
th.Mux.HandleFunc("/os-keypairs", 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, `
{
"keypair": {
"name": "importedkey",
"public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDx8nkQv/zgGgB4rMYmIf+6A4l6Rr+o/6lHBQdW5aYd44bd8JttDCE/F/pNRr0lRE+PiqSPO8nDPHw0010JeMH9gYgnnFlyY3/OcJ02RhIPyyxYpv9FhY+2YiUkpwFOcLImyrxEsYXpD/0d3ac30bNH6Sw9JD9UZHYcpSxsIbECHw== Generated by Nova"
}
}
`)
w.Header().Add("Content-Type", "application/json")
fmt.Fprintf(w, ImportOutput)
})
}
// HandleDeleteSuccessfully configures the test server to respond to a Delete request for a
// keypair called "deletedkey".
func HandleDeleteSuccessfully(t *testing.T) {
th.Mux.HandleFunc("/os-keypairs/deletedkey", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "DELETE")
th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
w.WriteHeader(http.StatusAccepted)
})
}

View File

@ -1,102 +0,0 @@
package keypairs
import (
"errors"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/openstack/compute/v2/servers"
"github.com/rackspace/gophercloud/pagination"
)
// CreateOptsExt adds a KeyPair option to the base CreateOpts.
type CreateOptsExt struct {
servers.CreateOptsBuilder
KeyName string `json:"key_name,omitempty"`
}
// ToServerCreateMap adds the key_name and, optionally, key_data options to
// the base server creation options.
func (opts CreateOptsExt) ToServerCreateMap() (map[string]interface{}, error) {
base, err := opts.CreateOptsBuilder.ToServerCreateMap()
if err != nil {
return nil, err
}
if opts.KeyName == "" {
return base, nil
}
serverMap := base["server"].(map[string]interface{})
serverMap["key_name"] = opts.KeyName
return base, nil
}
// List returns a Pager that allows you to iterate over a collection of KeyPairs.
func List(client *gophercloud.ServiceClient) pagination.Pager {
return pagination.NewPager(client, listURL(client), func(r pagination.PageResult) pagination.Page {
return KeyPairPage{pagination.SinglePageBase(r)}
})
}
// CreateOptsBuilder describes struct types that can be accepted by the Create call. Notable, the
// CreateOpts struct in this package does.
type CreateOptsBuilder interface {
ToKeyPairCreateMap() (map[string]interface{}, error)
}
// CreateOpts specifies keypair creation or import parameters.
type CreateOpts struct {
// Name [required] is a friendly name to refer to this KeyPair in other services.
Name string
// PublicKey [optional] is a pregenerated OpenSSH-formatted public key. If provided, this key
// will be imported and no new key will be created.
PublicKey string
}
// ToKeyPairCreateMap constructs a request body from CreateOpts.
func (opts CreateOpts) ToKeyPairCreateMap() (map[string]interface{}, error) {
if opts.Name == "" {
return nil, errors.New("Missing field required for keypair creation: Name")
}
keypair := make(map[string]interface{})
keypair["name"] = opts.Name
if opts.PublicKey != "" {
keypair["public_key"] = opts.PublicKey
}
return map[string]interface{}{"keypair": keypair}, nil
}
// Create requests the creation of a new keypair on the server, or to import a pre-existing
// keypair.
func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult {
var res CreateResult
reqBody, err := opts.ToKeyPairCreateMap()
if err != nil {
res.Err = err
return res
}
_, res.Err = client.Post(createURL(client), reqBody, &res.Body, &gophercloud.RequestOpts{
OkCodes: []int{200},
})
return res
}
// Get returns public data about a previously uploaded KeyPair.
func Get(client *gophercloud.ServiceClient, name string) GetResult {
var res GetResult
_, res.Err = client.Get(getURL(client, name), &res.Body, nil)
return res
}
// Delete requests the deletion of a previous stored KeyPair from the server.
func Delete(client *gophercloud.ServiceClient, name string) DeleteResult {
var res DeleteResult
_, res.Err = client.Delete(deleteURL(client, name), nil)
return res
}

View File

@ -1,94 +0,0 @@
package keypairs
import (
"github.com/mitchellh/mapstructure"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
)
// KeyPair is an SSH key known to the OpenStack cluster that is available to be injected into
// servers.
type KeyPair struct {
// Name is used to refer to this keypair from other services within this region.
Name string `mapstructure:"name"`
// Fingerprint is a short sequence of bytes that can be used to authenticate or validate a longer
// public key.
Fingerprint string `mapstructure:"fingerprint"`
// PublicKey is the public key from this pair, in OpenSSH format. "ssh-rsa AAAAB3Nz..."
PublicKey string `mapstructure:"public_key"`
// PrivateKey is the private key from this pair, in PEM format.
// "-----BEGIN RSA PRIVATE KEY-----\nMIICXA..." It is only present if this keypair was just
// returned from a Create call
PrivateKey string `mapstructure:"private_key"`
// UserID is the user who owns this keypair.
UserID string `mapstructure:"user_id"`
}
// KeyPairPage stores a single, only page of KeyPair results from a List call.
type KeyPairPage struct {
pagination.SinglePageBase
}
// IsEmpty determines whether or not a KeyPairPage is empty.
func (page KeyPairPage) IsEmpty() (bool, error) {
ks, err := ExtractKeyPairs(page)
return len(ks) == 0, err
}
// ExtractKeyPairs interprets a page of results as a slice of KeyPairs.
func ExtractKeyPairs(page pagination.Page) ([]KeyPair, error) {
type pair struct {
KeyPair KeyPair `mapstructure:"keypair"`
}
var resp struct {
KeyPairs []pair `mapstructure:"keypairs"`
}
err := mapstructure.Decode(page.(KeyPairPage).Body, &resp)
results := make([]KeyPair, len(resp.KeyPairs))
for i, pair := range resp.KeyPairs {
results[i] = pair.KeyPair
}
return results, err
}
type keyPairResult struct {
gophercloud.Result
}
// Extract is a method that attempts to interpret any KeyPair resource response as a KeyPair struct.
func (r keyPairResult) Extract() (*KeyPair, error) {
if r.Err != nil {
return nil, r.Err
}
var res struct {
KeyPair *KeyPair `json:"keypair" mapstructure:"keypair"`
}
err := mapstructure.Decode(r.Body, &res)
return res.KeyPair, err
}
// CreateResult is the response from a Create operation. Call its Extract method to interpret it
// as a KeyPair.
type CreateResult struct {
keyPairResult
}
// GetResult is the response from a Get operation. Call its Extract method to interpret it
// as a KeyPair.
type GetResult struct {
keyPairResult
}
// DeleteResult is the response from a Delete operation. Call its Extract method to determine if
// the call succeeded or failed.
type DeleteResult struct {
gophercloud.ErrResult
}

View File

@ -1,25 +0,0 @@
package keypairs
import "github.com/rackspace/gophercloud"
const resourcePath = "os-keypairs"
func resourceURL(c *gophercloud.ServiceClient) string {
return c.ServiceURL(resourcePath)
}
func listURL(c *gophercloud.ServiceClient) string {
return resourceURL(c)
}
func createURL(c *gophercloud.ServiceClient) string {
return resourceURL(c)
}
func getURL(c *gophercloud.ServiceClient, name string) string {
return c.ServiceURL(resourcePath, name)
}
func deleteURL(c *gophercloud.ServiceClient, name string) string {
return getURL(c, name)
}

View File

@ -1,3 +0,0 @@
// Package schedulerhints enables instances to provide the OpenStack scheduler
// hints about where they should be placed in the cloud.
package schedulerhints

View File

@ -1,134 +0,0 @@
package schedulerhints
import (
"fmt"
"net"
"regexp"
"strings"
"github.com/rackspace/gophercloud/openstack/compute/v2/servers"
)
// SchedulerHints represents a set of scheduling hints that are passed to the
// OpenStack scheduler
type SchedulerHints struct {
// Group specifies a Server Group to place the instance in.
Group string
// DifferentHost will place the instance on a compute node that does not
// host the given instances.
DifferentHost []string
// SameHost will place the instance on a compute node that hosts the given
// instances.
SameHost []string
// Query is a conditional statement that results in compute nodes able to
// host the instance.
Query []interface{}
// TargetCell specifies a cell name where the instance will be placed.
TargetCell string
// BuildNearHostIP specifies a subnet of compute nodes to host the instance.
BuildNearHostIP string
}
// SchedulerHintsBuilder builds the scheduler hints into a serializable format.
type SchedulerHintsBuilder interface {
ToServerSchedulerHintsMap() (map[string]interface{}, error)
}
// ToServerSchedulerHintsMap builds the scheduler hints into a serializable format.
func (opts SchedulerHints) ToServerSchedulerHintsMap() (map[string]interface{}, error) {
sh := make(map[string]interface{})
uuidRegex, _ := regexp.Compile("^[a-z0-9]{8}-[a-z0-9]{4}-[1-5][a-z0-9]{3}-[a-z0-9]{4}-[a-z0-9]{12}$")
if opts.Group != "" {
if !uuidRegex.MatchString(opts.Group) {
return nil, fmt.Errorf("Group must be a UUID")
}
sh["group"] = opts.Group
}
if len(opts.DifferentHost) > 0 {
for _, diffHost := range opts.DifferentHost {
if !uuidRegex.MatchString(diffHost) {
return nil, fmt.Errorf("The hosts in DifferentHost must be in UUID format.")
}
}
sh["different_host"] = opts.DifferentHost
}
if len(opts.SameHost) > 0 {
for _, sameHost := range opts.SameHost {
if !uuidRegex.MatchString(sameHost) {
return nil, fmt.Errorf("The hosts in SameHost must be in UUID format.")
}
}
sh["same_host"] = opts.SameHost
}
/* Query can be something simple like:
[">=", "$free_ram_mb", 1024]
Or more complex like:
['and',
['>=', '$free_ram_mb', 1024],
['>=', '$free_disk_mb', 200 * 1024]
]
Because of the possible complexity, just make sure the length is a minimum of 3.
*/
if len(opts.Query) > 0 {
if len(opts.Query) < 3 {
return nil, fmt.Errorf("Query must be a conditional statement in the format of [op,variable,value]")
}
sh["query"] = opts.Query
}
if opts.TargetCell != "" {
sh["target_cell"] = opts.TargetCell
}
if opts.BuildNearHostIP != "" {
if _, _, err := net.ParseCIDR(opts.BuildNearHostIP); err != nil {
return nil, fmt.Errorf("BuildNearHostIP must be a valid subnet in the form 192.168.1.1/24")
}
ipParts := strings.Split(opts.BuildNearHostIP, "/")
sh["build_near_host_ip"] = ipParts[0]
sh["cidr"] = "/" + ipParts[1]
}
return sh, nil
}
// CreateOptsExt adds a SchedulerHints option to the base CreateOpts.
type CreateOptsExt struct {
servers.CreateOptsBuilder
// SchedulerHints provides a set of hints to the scheduler.
SchedulerHints SchedulerHintsBuilder
}
// ToServerCreateMap adds the SchedulerHints option to the base server creation options.
func (opts CreateOptsExt) ToServerCreateMap() (map[string]interface{}, error) {
base, err := opts.CreateOptsBuilder.ToServerCreateMap()
if err != nil {
return nil, err
}
schedulerHints, err := opts.SchedulerHints.ToServerSchedulerHintsMap()
if err != nil {
return nil, err
}
if len(schedulerHints) == 0 {
return base, nil
}
base["os:scheduler_hints"] = schedulerHints
return base, nil
}

View File

@ -1 +0,0 @@
package secgroups

View File

@ -1,305 +0,0 @@
// +build fixtures
package secgroups
import (
"fmt"
"net/http"
"testing"
th "github.com/rackspace/gophercloud/testhelper"
fake "github.com/rackspace/gophercloud/testhelper/client"
)
const rootPath = "/os-security-groups"
const listGroupsJSON = `
{
"security_groups": [
{
"description": "default",
"id": "{groupID}",
"name": "default",
"rules": [],
"tenant_id": "openstack"
}
]
}
`
func mockListGroupsResponse(t *testing.T) {
th.Mux.HandleFunc(rootPath, func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "GET")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, listGroupsJSON)
})
}
func mockListGroupsByServerResponse(t *testing.T, serverID string) {
url := fmt.Sprintf("/servers/%s%s", serverID, rootPath)
th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "GET")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, listGroupsJSON)
})
}
func mockCreateGroupResponse(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": {
"name": "test",
"description": "something"
}
}
`)
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, `
{
"security_group": {
"description": "something",
"id": "{groupID}",
"name": "test",
"rules": [],
"tenant_id": "openstack"
}
}
`)
})
}
func mockUpdateGroupResponse(t *testing.T, groupID string) {
url := fmt.Sprintf("%s/%s", rootPath, groupID)
th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "PUT")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
th.TestJSONRequest(t, r, `
{
"security_group": {
"name": "new_name",
"description": "new_desc"
}
}
`)
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, `
{
"security_group": {
"description": "something",
"id": "{groupID}",
"name": "new_name",
"rules": [],
"tenant_id": "openstack"
}
}
`)
})
}
func mockGetGroupsResponse(t *testing.T, groupID string) {
url := fmt.Sprintf("%s/%s", rootPath, groupID)
th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "GET")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, `
{
"security_group": {
"description": "default",
"id": "{groupID}",
"name": "default",
"rules": [
{
"from_port": 80,
"group": {
"tenant_id": "openstack",
"name": "default"
},
"ip_protocol": "TCP",
"to_port": 85,
"parent_group_id": "{groupID}",
"ip_range": {
"cidr": "0.0.0.0"
},
"id": "{ruleID}"
}
],
"tenant_id": "openstack"
}
}
`)
})
}
func mockGetNumericIDGroupResponse(t *testing.T, groupID int) {
url := fmt.Sprintf("%s/%d", rootPath, groupID)
th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "GET")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, `
{
"security_group": {
"id": 12345
}
}
`)
})
}
func mockDeleteGroupResponse(t *testing.T, groupID string) {
url := fmt.Sprintf("%s/%s", rootPath, groupID)
th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "DELETE")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusAccepted)
})
}
func mockAddRuleResponse(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": 22,
"ip_protocol": "TCP",
"to_port": 22,
"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": 22,
"group": {},
"ip_protocol": "TCP",
"to_port": 22,
"parent_group_id": "{groupID}",
"ip_range": {
"cidr": "0.0.0.0/0"
},
"id": "{ruleID}"
}
}`)
})
}
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) {
th.TestMethod(t, r, "DELETE")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusAccepted)
})
}
func mockAddServerToGroupResponse(t *testing.T, serverID string) {
url := fmt.Sprintf("/servers/%s/action", serverID)
th.Mux.HandleFunc(url, 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, `
{
"addSecurityGroup": {
"name": "test"
}
}
`)
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusAccepted)
fmt.Fprintf(w, `{}`)
})
}
func mockRemoveServerFromGroupResponse(t *testing.T, serverID string) {
url := fmt.Sprintf("/servers/%s/action", serverID)
th.Mux.HandleFunc(url, 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, `
{
"removeSecurityGroup": {
"name": "test"
}
}
`)
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusAccepted)
fmt.Fprintf(w, `{}`)
})
}

View File

@ -1,258 +0,0 @@
package secgroups
import (
"errors"
"strings"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
)
func commonList(client *gophercloud.ServiceClient, url string) pagination.Pager {
createPage := func(r pagination.PageResult) pagination.Page {
return SecurityGroupPage{pagination.SinglePageBase(r)}
}
return pagination.NewPager(client, url, createPage)
}
// List will return a collection of all the security groups for a particular
// tenant.
func List(client *gophercloud.ServiceClient) pagination.Pager {
return commonList(client, rootURL(client))
}
// ListByServer will return a collection of all the security groups which are
// associated with a particular server.
func ListByServer(client *gophercloud.ServiceClient, serverID string) pagination.Pager {
return commonList(client, listByServerURL(client, serverID))
}
// GroupOpts is the underlying struct responsible for creating or updating
// security groups. It therefore represents the mutable attributes of a
// security group.
type GroupOpts struct {
// Required - the name of your security group.
Name string `json:"name"`
// Required - the description of your security group.
Description string `json:"description"`
}
// CreateOpts is the struct responsible for creating a security group.
type CreateOpts GroupOpts
// CreateOptsBuilder builds the create options into a serializable format.
type CreateOptsBuilder interface {
ToSecGroupCreateMap() (map[string]interface{}, error)
}
var (
errName = errors.New("Name is a required field")
errDesc = errors.New("Description is a required field")
)
// ToSecGroupCreateMap builds the create options into a serializable format.
func (opts CreateOpts) ToSecGroupCreateMap() (map[string]interface{}, error) {
sg := make(map[string]interface{})
if opts.Name == "" {
return sg, errName
}
if opts.Description == "" {
return sg, errDesc
}
sg["name"] = opts.Name
sg["description"] = opts.Description
return map[string]interface{}{"security_group": sg}, nil
}
// Create will create a new security group.
func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult {
var result CreateResult
reqBody, err := opts.ToSecGroupCreateMap()
if err != nil {
result.Err = err
return result
}
_, result.Err = client.Post(rootURL(client), reqBody, &result.Body, &gophercloud.RequestOpts{
OkCodes: []int{200},
})
return result
}
// UpdateOpts is the struct responsible for updating an existing security group.
type UpdateOpts GroupOpts
// UpdateOptsBuilder builds the update options into a serializable format.
type UpdateOptsBuilder interface {
ToSecGroupUpdateMap() (map[string]interface{}, error)
}
// ToSecGroupUpdateMap builds the update options into a serializable format.
func (opts UpdateOpts) ToSecGroupUpdateMap() (map[string]interface{}, error) {
sg := make(map[string]interface{})
if opts.Name == "" {
return sg, errName
}
if opts.Description == "" {
return sg, errDesc
}
sg["name"] = opts.Name
sg["description"] = opts.Description
return map[string]interface{}{"security_group": sg}, nil
}
// Update will modify the mutable properties of a security group, notably its
// name and description.
func Update(client *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) UpdateResult {
var result UpdateResult
reqBody, err := opts.ToSecGroupUpdateMap()
if err != nil {
result.Err = err
return result
}
_, result.Err = client.Put(resourceURL(client, id), reqBody, &result.Body, &gophercloud.RequestOpts{
OkCodes: []int{200},
})
return result
}
// Get will return details for a particular security group.
func Get(client *gophercloud.ServiceClient, id string) GetResult {
var result GetResult
_, result.Err = client.Get(resourceURL(client, id), &result.Body, nil)
return result
}
// Delete will permanently delete a security group from the project.
func Delete(client *gophercloud.ServiceClient, id string) gophercloud.ErrResult {
var result gophercloud.ErrResult
_, result.Err = client.Delete(resourceURL(client, id), nil)
return result
}
// CreateRuleOpts represents the configuration for adding a new rule to an
// existing security group.
type CreateRuleOpts struct {
// Required - the ID of the group that this rule will be added to.
ParentGroupID string `json:"parent_group_id"`
// Required - the lower bound of the port range that will be opened.
FromPort int `json:"from_port"`
// Required - the upper bound of the port range that will be opened.
ToPort int `json:"to_port"`
// Required - the protocol type that will be allowed, e.g. TCP.
IPProtocol string `json:"ip_protocol"`
// ONLY required if FromGroupID is blank. This represents the IP range that
// will be the source of network traffic to your security group. Use
// 0.0.0.0/0 to allow all IP addresses.
CIDR string `json:"cidr,omitempty"`
// ONLY required if CIDR is blank. This value represents the ID of a group
// that forwards traffic to the parent group. So, instead of accepting
// network traffic from an entire IP range, you can instead refine the
// inbound source by an existing security group.
FromGroupID string `json:"group_id,omitempty"`
}
// CreateRuleOptsBuilder builds the create rule options into a serializable format.
type CreateRuleOptsBuilder interface {
ToRuleCreateMap() (map[string]interface{}, error)
}
// ToRuleCreateMap builds the create rule options into a serializable format.
func (opts CreateRuleOpts) ToRuleCreateMap() (map[string]interface{}, error) {
rule := make(map[string]interface{})
if opts.ParentGroupID == "" {
return rule, errors.New("A ParentGroupID must be set")
}
if opts.FromPort == 0 && strings.ToUpper(opts.IPProtocol) != "ICMP" {
return rule, errors.New("A FromPort must be set")
}
if opts.ToPort == 0 && strings.ToUpper(opts.IPProtocol) != "ICMP" {
return rule, errors.New("A ToPort must be set")
}
if opts.IPProtocol == "" {
return rule, errors.New("A IPProtocol must be set")
}
if opts.CIDR == "" && opts.FromGroupID == "" {
return rule, errors.New("A CIDR or FromGroupID must be set")
}
rule["parent_group_id"] = opts.ParentGroupID
rule["from_port"] = opts.FromPort
rule["to_port"] = opts.ToPort
rule["ip_protocol"] = opts.IPProtocol
if opts.CIDR != "" {
rule["cidr"] = opts.CIDR
}
if opts.FromGroupID != "" {
rule["group_id"] = opts.FromGroupID
}
return map[string]interface{}{"security_group_rule": rule}, nil
}
// CreateRule will add a new rule to an existing security group (whose ID is
// specified in CreateRuleOpts). You have the option of controlling inbound
// traffic from either an IP range (CIDR) or from another security group.
func CreateRule(client *gophercloud.ServiceClient, opts CreateRuleOptsBuilder) CreateRuleResult {
var result CreateRuleResult
reqBody, err := opts.ToRuleCreateMap()
if err != nil {
result.Err = err
return result
}
_, result.Err = client.Post(rootRuleURL(client), reqBody, &result.Body, &gophercloud.RequestOpts{
OkCodes: []int{200},
})
return result
}
// DeleteRule will permanently delete a rule from a security group.
func DeleteRule(client *gophercloud.ServiceClient, id string) gophercloud.ErrResult {
var result gophercloud.ErrResult
_, result.Err = client.Delete(resourceRuleURL(client, id), nil)
return result
}
func actionMap(prefix, groupName string) map[string]map[string]string {
return map[string]map[string]string{
prefix + "SecurityGroup": map[string]string{"name": groupName},
}
}
// AddServerToGroup will associate a server and a security group, enforcing the
// rules of the group on the server.
func AddServerToGroup(client *gophercloud.ServiceClient, serverID, groupName string) gophercloud.ErrResult {
var result gophercloud.ErrResult
_, result.Err = client.Post(serverActionURL(client, serverID), actionMap("add", groupName), &result.Body, nil)
return result
}
// RemoveServerFromGroup will disassociate a server from a security group.
func RemoveServerFromGroup(client *gophercloud.ServiceClient, serverID, groupName string) gophercloud.ErrResult {
var result gophercloud.ErrResult
_, result.Err = client.Post(serverActionURL(client, serverID), actionMap("remove", groupName), &result.Body, nil)
return result
}

View File

@ -1,147 +0,0 @@
package secgroups
import (
"github.com/mitchellh/mapstructure"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
)
// SecurityGroup represents a security group.
type SecurityGroup struct {
// The unique ID of the group. If Neutron is installed, this ID will be
// represented as a string UUID; if Neutron is not installed, it will be a
// numeric ID. For the sake of consistency, we always cast it to a string.
ID string
// The human-readable name of the group, which needs to be unique.
Name string
// The human-readable description of the group.
Description string
// The rules which determine how this security group operates.
Rules []Rule
// The ID of the tenant to which this security group belongs.
TenantID string `mapstructure:"tenant_id"`
}
// Rule represents a security group rule, a policy which determines how a
// security group operates and what inbound traffic it allows in.
type Rule struct {
// The unique ID. If Neutron is installed, this ID will be
// represented as a string UUID; if Neutron is not installed, it will be a
// numeric ID. For the sake of consistency, we always cast it to a string.
ID string
// The lower bound of the port range which this security group should open up
FromPort int `mapstructure:"from_port"`
// The upper bound of the port range which this security group should open up
ToPort int `mapstructure:"to_port"`
// The IP protocol (e.g. TCP) which the security group accepts
IPProtocol string `mapstructure:"ip_protocol"`
// The CIDR IP range whose traffic can be received
IPRange IPRange `mapstructure:"ip_range"`
// The security group ID to which this rule belongs
ParentGroupID string `mapstructure:"parent_group_id"`
// Not documented.
Group Group
}
// IPRange represents the IP range whose traffic will be accepted by the
// security group.
type IPRange struct {
CIDR string
}
// Group represents a group.
type Group struct {
TenantID string `mapstructure:"tenant_id"`
Name string
}
// SecurityGroupPage is a single page of a SecurityGroup collection.
type SecurityGroupPage struct {
pagination.SinglePageBase
}
// IsEmpty determines whether or not a page of Security Groups contains any results.
func (page SecurityGroupPage) IsEmpty() (bool, error) {
users, err := ExtractSecurityGroups(page)
if err != nil {
return false, err
}
return len(users) == 0, nil
}
// ExtractSecurityGroups returns a slice of SecurityGroups contained in a single page of results.
func ExtractSecurityGroups(page pagination.Page) ([]SecurityGroup, error) {
casted := page.(SecurityGroupPage).Body
var response struct {
SecurityGroups []SecurityGroup `mapstructure:"security_groups"`
}
err := mapstructure.WeakDecode(casted, &response)
return response.SecurityGroups, err
}
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
}
// UpdateResult represents the result of an update operation.
type UpdateResult struct {
commonResult
}
// Extract will extract a SecurityGroup struct from most responses.
func (r commonResult) Extract() (*SecurityGroup, error) {
if r.Err != nil {
return nil, r.Err
}
var response struct {
SecurityGroup SecurityGroup `mapstructure:"security_group"`
}
err := mapstructure.WeakDecode(r.Body, &response)
return &response.SecurityGroup, err
}
// CreateRuleResult represents the result when adding rules to a security group.
type CreateRuleResult struct {
gophercloud.Result
}
// Extract will extract a Rule struct from a CreateRuleResult.
func (r CreateRuleResult) Extract() (*Rule, error) {
if r.Err != nil {
return nil, r.Err
}
var response struct {
Rule Rule `mapstructure:"security_group_rule"`
}
err := mapstructure.WeakDecode(r.Body, &response)
return &response.Rule, err
}

View File

@ -1,32 +0,0 @@
package secgroups
import "github.com/rackspace/gophercloud"
const (
secgrouppath = "os-security-groups"
rulepath = "os-security-group-rules"
)
func resourceURL(c *gophercloud.ServiceClient, id string) string {
return c.ServiceURL(secgrouppath, id)
}
func rootURL(c *gophercloud.ServiceClient) string {
return c.ServiceURL(secgrouppath)
}
func listByServerURL(c *gophercloud.ServiceClient, serverID string) string {
return c.ServiceURL("servers", serverID, secgrouppath)
}
func rootRuleURL(c *gophercloud.ServiceClient) string {
return c.ServiceURL(rulepath)
}
func resourceRuleURL(c *gophercloud.ServiceClient, id string) string {
return c.ServiceURL(rulepath, id)
}
func serverActionURL(c *gophercloud.ServiceClient, id string) string {
return c.ServiceURL("servers", id, "action")
}

View File

@ -1,2 +0,0 @@
// Package servergroups provides the ability to manage server groups
package servergroups

View File

@ -1,161 +0,0 @@
// +build fixtures
package servergroups
import (
"fmt"
"net/http"
"testing"
th "github.com/rackspace/gophercloud/testhelper"
"github.com/rackspace/gophercloud/testhelper/client"
)
// ListOutput is a sample response to a List call.
const ListOutput = `
{
"server_groups": [
{
"id": "616fb98f-46ca-475e-917e-2563e5a8cd19",
"name": "test",
"policies": [
"anti-affinity"
],
"members": [],
"metadata": {}
},
{
"id": "4d8c3732-a248-40ed-bebc-539a6ffd25c0",
"name": "test2",
"policies": [
"affinity"
],
"members": [],
"metadata": {}
}
]
}
`
// GetOutput is a sample response to a Get call.
const GetOutput = `
{
"server_group": {
"id": "616fb98f-46ca-475e-917e-2563e5a8cd19",
"name": "test",
"policies": [
"anti-affinity"
],
"members": [],
"metadata": {}
}
}
`
// CreateOutput is a sample response to a Post call
const CreateOutput = `
{
"server_group": {
"id": "616fb98f-46ca-475e-917e-2563e5a8cd19",
"name": "test",
"policies": [
"anti-affinity"
],
"members": [],
"metadata": {}
}
}
`
// FirstServerGroup is the first result in ListOutput.
var FirstServerGroup = ServerGroup{
ID: "616fb98f-46ca-475e-917e-2563e5a8cd19",
Name: "test",
Policies: []string{
"anti-affinity",
},
Members: []string{},
Metadata: map[string]interface{}{},
}
// SecondServerGroup is the second result in ListOutput.
var SecondServerGroup = ServerGroup{
ID: "4d8c3732-a248-40ed-bebc-539a6ffd25c0",
Name: "test2",
Policies: []string{
"affinity",
},
Members: []string{},
Metadata: map[string]interface{}{},
}
// ExpectedServerGroupSlice is the slice of results that should be parsed
// from ListOutput, in the expected order.
var ExpectedServerGroupSlice = []ServerGroup{FirstServerGroup, SecondServerGroup}
// CreatedServerGroup is the parsed result from CreateOutput.
var CreatedServerGroup = ServerGroup{
ID: "616fb98f-46ca-475e-917e-2563e5a8cd19",
Name: "test",
Policies: []string{
"anti-affinity",
},
Members: []string{},
Metadata: map[string]interface{}{},
}
// HandleListSuccessfully configures the test server to respond to a List request.
func HandleListSuccessfully(t *testing.T) {
th.Mux.HandleFunc("/os-server-groups", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "GET")
th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
w.Header().Add("Content-Type", "application/json")
fmt.Fprintf(w, ListOutput)
})
}
// HandleGetSuccessfully configures the test server to respond to a Get request
// for an existing server group
func HandleGetSuccessfully(t *testing.T) {
th.Mux.HandleFunc("/os-server-groups/4d8c3732-a248-40ed-bebc-539a6ffd25c0", 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)
})
}
// HandleCreateSuccessfully configures the test server to respond to a Create request
// for a new server group
func HandleCreateSuccessfully(t *testing.T) {
th.Mux.HandleFunc("/os-server-groups", 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, `
{
"server_group": {
"name": "test",
"policies": [
"anti-affinity"
]
}
}
`)
w.Header().Add("Content-Type", "application/json")
fmt.Fprintf(w, CreateOutput)
})
}
// HandleDeleteSuccessfully configures the test server to respond to a Delete request for a
// an existing server group
func HandleDeleteSuccessfully(t *testing.T) {
th.Mux.HandleFunc("/os-server-groups/616fb98f-46ca-475e-917e-2563e5a8cd19", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "DELETE")
th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
w.WriteHeader(http.StatusAccepted)
})
}

View File

@ -1,77 +0,0 @@
package servergroups
import (
"errors"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
)
// List returns a Pager that allows you to iterate over a collection of ServerGroups.
func List(client *gophercloud.ServiceClient) pagination.Pager {
return pagination.NewPager(client, listURL(client), func(r pagination.PageResult) pagination.Page {
return ServerGroupsPage{pagination.SinglePageBase(r)}
})
}
// CreateOptsBuilder describes struct types that can be accepted by the Create call. Notably, the
// CreateOpts struct in this package does.
type CreateOptsBuilder interface {
ToServerGroupCreateMap() (map[string]interface{}, error)
}
// CreateOpts specifies a Server Group allocation request
type CreateOpts struct {
// Name is the name of the server group
Name string
// Policies are the server group policies
Policies []string
}
// ToServerGroupCreateMap constructs a request body from CreateOpts.
func (opts CreateOpts) ToServerGroupCreateMap() (map[string]interface{}, error) {
if opts.Name == "" {
return nil, errors.New("Missing field required for server group creation: Name")
}
if len(opts.Policies) < 1 {
return nil, errors.New("Missing field required for server group creation: Policies")
}
serverGroup := make(map[string]interface{})
serverGroup["name"] = opts.Name
serverGroup["policies"] = opts.Policies
return map[string]interface{}{"server_group": serverGroup}, nil
}
// Create requests the creation of a new Server Group
func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult {
var res CreateResult
reqBody, err := opts.ToServerGroupCreateMap()
if err != nil {
res.Err = err
return res
}
_, res.Err = client.Post(createURL(client), reqBody, &res.Body, &gophercloud.RequestOpts{
OkCodes: []int{200},
})
return res
}
// Get returns data about a previously created ServerGroup.
func Get(client *gophercloud.ServiceClient, id string) GetResult {
var res GetResult
_, res.Err = client.Get(getURL(client, id), &res.Body, nil)
return res
}
// Delete requests the deletion of a previously allocated ServerGroup.
func Delete(client *gophercloud.ServiceClient, id string) DeleteResult {
var res DeleteResult
_, res.Err = client.Delete(deleteURL(client, id), nil)
return res
}

View File

@ -1,87 +0,0 @@
package servergroups
import (
"github.com/mitchellh/mapstructure"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
)
// A ServerGroup creates a policy for instance placement in the cloud
type ServerGroup struct {
// ID is the unique ID of the Server Group.
ID string `mapstructure:"id"`
// Name is the common name of the server group.
Name string `mapstructure:"name"`
// Polices are the group policies.
Policies []string `mapstructure:"policies"`
// Members are the members of the server group.
Members []string `mapstructure:"members"`
// Metadata includes a list of all user-specified key-value pairs attached to the Server Group.
Metadata map[string]interface{}
}
// ServerGroupsPage stores a single, only page of ServerGroups
// results from a List call.
type ServerGroupsPage struct {
pagination.SinglePageBase
}
// IsEmpty determines whether or not a ServerGroupsPage is empty.
func (page ServerGroupsPage) IsEmpty() (bool, error) {
va, err := ExtractServerGroups(page)
return len(va) == 0, err
}
// ExtractServerGroups interprets a page of results as a slice of
// ServerGroups.
func ExtractServerGroups(page pagination.Page) ([]ServerGroup, error) {
casted := page.(ServerGroupsPage).Body
var response struct {
ServerGroups []ServerGroup `mapstructure:"server_groups"`
}
err := mapstructure.WeakDecode(casted, &response)
return response.ServerGroups, err
}
type ServerGroupResult struct {
gophercloud.Result
}
// Extract is a method that attempts to interpret any Server Group resource
// response as a ServerGroup struct.
func (r ServerGroupResult) Extract() (*ServerGroup, error) {
if r.Err != nil {
return nil, r.Err
}
var res struct {
ServerGroup *ServerGroup `json:"server_group" mapstructure:"server_group"`
}
err := mapstructure.WeakDecode(r.Body, &res)
return res.ServerGroup, err
}
// CreateResult is the response from a Create operation. Call its Extract method to interpret it
// as a ServerGroup.
type CreateResult struct {
ServerGroupResult
}
// GetResult is the response from a Get operation. Call its Extract method to interpret it
// as a ServerGroup.
type GetResult struct {
ServerGroupResult
}
// DeleteResult is the response from a Delete operation. Call its Extract method to determine if
// the call succeeded or failed.
type DeleteResult struct {
gophercloud.ErrResult
}

View File

@ -1,25 +0,0 @@
package servergroups
import "github.com/rackspace/gophercloud"
const resourcePath = "os-server-groups"
func resourceURL(c *gophercloud.ServiceClient) string {
return c.ServiceURL(resourcePath)
}
func listURL(c *gophercloud.ServiceClient) string {
return resourceURL(c)
}
func createURL(c *gophercloud.ServiceClient) string {
return resourceURL(c)
}
func getURL(c *gophercloud.ServiceClient, id string) string {
return c.ServiceURL(resourcePath, id)
}
func deleteURL(c *gophercloud.ServiceClient, id string) string {
return getURL(c, id)
}

View File

@ -1,5 +0,0 @@
/*
Package startstop provides functionality to start and stop servers that have
been provisioned by the OpenStack Compute service.
*/
package startstop

View File

@ -1,29 +0,0 @@
// +build fixtures
package startstop
import (
"net/http"
"testing"
th "github.com/rackspace/gophercloud/testhelper"
"github.com/rackspace/gophercloud/testhelper/client"
)
func mockStartServerResponse(t *testing.T, id string) {
th.Mux.HandleFunc("/servers/"+id+"/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, `{"os-start": null}`)
w.WriteHeader(http.StatusAccepted)
})
}
func mockStopServerResponse(t *testing.T, id string) {
th.Mux.HandleFunc("/servers/"+id+"/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, `{"os-stop": null}`)
w.WriteHeader(http.StatusAccepted)
})
}

View File

@ -1,23 +0,0 @@
package startstop
import "github.com/rackspace/gophercloud"
func actionURL(client *gophercloud.ServiceClient, id string) string {
return client.ServiceURL("servers", id, "action")
}
// Start is the operation responsible for starting a Compute server.
func Start(client *gophercloud.ServiceClient, id string) gophercloud.ErrResult {
var res gophercloud.ErrResult
reqBody := map[string]interface{}{"os-start": nil}
_, res.Err = client.Post(actionURL(client, id), reqBody, nil, nil)
return res
}
// Stop is the operation responsible for stopping a Compute server.
func Stop(client *gophercloud.ServiceClient, id string) gophercloud.ErrResult {
var res gophercloud.ErrResult
reqBody := map[string]interface{}{"os-stop": nil}
_, res.Err = client.Post(actionURL(client, id), reqBody, nil, nil)
return res
}

View File

@ -1,2 +0,0 @@
// Package tenantnetworks provides the ability for tenants to see information about the networks they have access to
package tenantnetworks

View File

@ -1,84 +0,0 @@
// +build fixtures
package tenantnetworks
import (
"fmt"
"net/http"
"testing"
"time"
th "github.com/rackspace/gophercloud/testhelper"
"github.com/rackspace/gophercloud/testhelper/client"
)
// ListOutput is a sample response to a List call.
const ListOutput = `
{
"networks": [
{
"cidr": "10.0.0.0/29",
"id": "20c8acc0-f747-4d71-a389-46d078ebf047",
"label": "mynet_0"
},
{
"cidr": "10.0.0.10/29",
"id": "20c8acc0-f747-4d71-a389-46d078ebf000",
"label": "mynet_1"
}
]
}
`
// GetOutput is a sample response to a Get call.
const GetOutput = `
{
"network": {
"cidr": "10.0.0.10/29",
"id": "20c8acc0-f747-4d71-a389-46d078ebf000",
"label": "mynet_1"
}
}
`
// FirstNetwork is the first result in ListOutput.
var nilTime time.Time
var FirstNetwork = Network{
CIDR: "10.0.0.0/29",
ID: "20c8acc0-f747-4d71-a389-46d078ebf047",
Name: "mynet_0",
}
// SecondNetwork is the second result in ListOutput.
var SecondNetwork = Network{
CIDR: "10.0.0.10/29",
ID: "20c8acc0-f747-4d71-a389-46d078ebf000",
Name: "mynet_1",
}
// ExpectedNetworkSlice is the slice of results that should be parsed
// from ListOutput, in the expected order.
var ExpectedNetworkSlice = []Network{FirstNetwork, SecondNetwork}
// HandleListSuccessfully configures the test server to respond to a List request.
func HandleListSuccessfully(t *testing.T) {
th.Mux.HandleFunc("/os-tenant-networks", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "GET")
th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
w.Header().Add("Content-Type", "application/json")
fmt.Fprintf(w, ListOutput)
})
}
// HandleGetSuccessfully configures the test server to respond to a Get request
// for an existing network.
func HandleGetSuccessfully(t *testing.T) {
th.Mux.HandleFunc("/os-tenant-networks/20c8acc0-f747-4d71-a389-46d078ebf000", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "GET")
th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
w.Header().Add("Content-Type", "application/json")
fmt.Fprintf(w, GetOutput)
})
}

View File

@ -1,22 +0,0 @@
package tenantnetworks
import (
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
)
// List returns a Pager that allows you to iterate over a collection of Network.
func List(client *gophercloud.ServiceClient) pagination.Pager {
url := listURL(client)
createPage := func(r pagination.PageResult) pagination.Page {
return NetworkPage{pagination.SinglePageBase(r)}
}
return pagination.NewPager(client, url, createPage)
}
// Get returns data about a previously created Network.
func Get(client *gophercloud.ServiceClient, id string) GetResult {
var res GetResult
_, res.Err = client.Get(getURL(client, id), &res.Body, nil)
return res
}

View File

@ -1,68 +0,0 @@
package tenantnetworks
import (
"github.com/mitchellh/mapstructure"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
)
// A Network represents a nova-network that an instance communicates on
type Network struct {
// CIDR is the IPv4 subnet.
CIDR string `mapstructure:"cidr"`
// ID is the UUID of the network.
ID string `mapstructure:"id"`
// Name is the common name that the network has.
Name string `mapstructure:"label"`
}
// NetworkPage stores a single, only page of Networks
// results from a List call.
type NetworkPage struct {
pagination.SinglePageBase
}
// IsEmpty determines whether or not a NetworkPage is empty.
func (page NetworkPage) IsEmpty() (bool, error) {
va, err := ExtractNetworks(page)
return len(va) == 0, err
}
// ExtractNetworks interprets a page of results as a slice of Networks
func ExtractNetworks(page pagination.Page) ([]Network, error) {
networks := page.(NetworkPage).Body
var res struct {
Networks []Network `mapstructure:"networks"`
}
err := mapstructure.WeakDecode(networks, &res)
return res.Networks, err
}
type NetworkResult struct {
gophercloud.Result
}
// Extract is a method that attempts to interpret any Network resource
// response as a Network struct.
func (r NetworkResult) Extract() (*Network, error) {
if r.Err != nil {
return nil, r.Err
}
var res struct {
Network *Network `json:"network" mapstructure:"network"`
}
err := mapstructure.Decode(r.Body, &res)
return res.Network, err
}
// GetResult is the response from a Get operation. Call its Extract method to interpret it
// as a Network.
type GetResult struct {
NetworkResult
}

View File

@ -1,17 +0,0 @@
package tenantnetworks
import "github.com/rackspace/gophercloud"
const resourcePath = "os-tenant-networks"
func resourceURL(c *gophercloud.ServiceClient) string {
return c.ServiceURL(resourcePath)
}
func listURL(c *gophercloud.ServiceClient) string {
return resourceURL(c)
}
func getURL(c *gophercloud.ServiceClient, id string) string {
return c.ServiceURL(resourcePath, id)
}

View File

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

View File

@ -1,75 +0,0 @@
package volumeattach
import (
"errors"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
)
// List returns a Pager that allows you to iterate over a collection of VolumeAttachments.
func List(client *gophercloud.ServiceClient, serverId string) pagination.Pager {
return pagination.NewPager(client, listURL(client, serverId), func(r pagination.PageResult) pagination.Page {
return VolumeAttachmentsPage{pagination.SinglePageBase(r)}
})
}
// CreateOptsBuilder describes struct types that can be accepted by the Create call. Notable, the
// CreateOpts struct in this package does.
type CreateOptsBuilder interface {
ToVolumeAttachmentCreateMap() (map[string]interface{}, error)
}
// CreateOpts specifies volume attachment creation or import parameters.
type CreateOpts struct {
// Device is the device that the volume will attach to the instance as. Omit for "auto"
Device string
// VolumeID is the ID of the volume to attach to the instance
VolumeID string
}
// ToVolumeAttachmentCreateMap constructs a request body from CreateOpts.
func (opts CreateOpts) ToVolumeAttachmentCreateMap() (map[string]interface{}, error) {
if opts.VolumeID == "" {
return nil, errors.New("Missing field required for volume attachment creation: VolumeID")
}
volumeAttachment := make(map[string]interface{})
volumeAttachment["volumeId"] = opts.VolumeID
if opts.Device != "" {
volumeAttachment["device"] = opts.Device
}
return map[string]interface{}{"volumeAttachment": volumeAttachment}, nil
}
// Create requests the creation of a new volume attachment on the server
func Create(client *gophercloud.ServiceClient, serverId string, opts CreateOptsBuilder) CreateResult {
var res CreateResult
reqBody, err := opts.ToVolumeAttachmentCreateMap()
if err != nil {
res.Err = err
return res
}
_, res.Err = client.Post(createURL(client, serverId), reqBody, &res.Body, &gophercloud.RequestOpts{
OkCodes: []int{200},
})
return res
}
// Get returns public data about a previously created VolumeAttachment.
func Get(client *gophercloud.ServiceClient, serverId, aId string) GetResult {
var res GetResult
_, res.Err = client.Get(getURL(client, serverId, aId), &res.Body, nil)
return res
}
// Delete requests the deletion of a previous stored VolumeAttachment from the server.
func Delete(client *gophercloud.ServiceClient, serverId, aId string) DeleteResult {
var res DeleteResult
_, res.Err = client.Delete(deleteURL(client, serverId, aId), nil)
return res
}

View File

@ -1,84 +0,0 @@
package volumeattach
import (
"github.com/mitchellh/mapstructure"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
)
// VolumeAttach controls the attachment of a volume to an instance.
type VolumeAttachment struct {
// ID is a unique id of the attachment
ID string `mapstructure:"id"`
// Device is what device the volume is attached as
Device string `mapstructure:"device"`
// VolumeID is the ID of the attached volume
VolumeID string `mapstructure:"volumeId"`
// ServerID is the ID of the instance that has the volume attached
ServerID string `mapstructure:"serverId"`
}
// VolumeAttachmentsPage stores a single, only page of VolumeAttachments
// results from a List call.
type VolumeAttachmentsPage struct {
pagination.SinglePageBase
}
// IsEmpty determines whether or not a VolumeAttachmentsPage is empty.
func (page VolumeAttachmentsPage) IsEmpty() (bool, error) {
va, err := ExtractVolumeAttachments(page)
return len(va) == 0, err
}
// ExtractVolumeAttachments interprets a page of results as a slice of
// VolumeAttachments.
func ExtractVolumeAttachments(page pagination.Page) ([]VolumeAttachment, error) {
casted := page.(VolumeAttachmentsPage).Body
var response struct {
VolumeAttachments []VolumeAttachment `mapstructure:"volumeAttachments"`
}
err := mapstructure.WeakDecode(casted, &response)
return response.VolumeAttachments, err
}
type VolumeAttachmentResult struct {
gophercloud.Result
}
// Extract is a method that attempts to interpret any VolumeAttachment resource
// response as a VolumeAttachment struct.
func (r VolumeAttachmentResult) Extract() (*VolumeAttachment, error) {
if r.Err != nil {
return nil, r.Err
}
var res struct {
VolumeAttachment *VolumeAttachment `json:"volumeAttachment" mapstructure:"volumeAttachment"`
}
err := mapstructure.Decode(r.Body, &res)
return res.VolumeAttachment, err
}
// CreateResult is the response from a Create operation. Call its Extract method to interpret it
// as a VolumeAttachment.
type CreateResult struct {
VolumeAttachmentResult
}
// GetResult is the response from a Get operation. Call its Extract method to interpret it
// as a VolumeAttachment.
type GetResult struct {
VolumeAttachmentResult
}
// DeleteResult is the response from a Delete operation. Call its Extract method to determine if
// the call succeeded or failed.
type DeleteResult struct {
gophercloud.ErrResult
}

View File

@ -1,25 +0,0 @@
package volumeattach
import "github.com/rackspace/gophercloud"
const resourcePath = "os-volume_attachments"
func resourceURL(c *gophercloud.ServiceClient, serverId string) string {
return c.ServiceURL("servers", serverId, resourcePath)
}
func listURL(c *gophercloud.ServiceClient, serverId string) string {
return resourceURL(c, serverId)
}
func createURL(c *gophercloud.ServiceClient, serverId string) string {
return resourceURL(c, serverId)
}
func getURL(c *gophercloud.ServiceClient, serverId, aId string) string {
return c.ServiceURL("servers", serverId, resourcePath, aId)
}
func deleteURL(c *gophercloud.ServiceClient, serverId, aId string) string {
return getURL(c, serverId, aId)
}

View File

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

View File

@ -1,103 +0,0 @@
package flavors
import (
"fmt"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
)
// ListOptsBuilder allows extensions to add additional parameters to the
// List request.
type ListOptsBuilder interface {
ToFlavorListQuery() (string, error)
}
// ListOpts helps control the results returned by the List() function.
// For example, a flavor with a minDisk field of 10 will not be returned if you specify MinDisk set to 20.
// Typically, software will use the last ID of the previous call to List to set the Marker for the current call.
type ListOpts struct {
// ChangesSince, if provided, instructs List to return only those things which have changed since the timestamp provided.
ChangesSince string `q:"changes-since"`
// MinDisk and MinRAM, if provided, elides flavors which do not meet your criteria.
MinDisk int `q:"minDisk"`
MinRAM int `q:"minRam"`
// Marker and Limit control paging.
// Marker instructs List where to start listing from.
Marker string `q:"marker"`
// Limit instructs List to refrain from sending excessively large lists of flavors.
Limit int `q:"limit"`
}
// ToFlavorListQuery formats a ListOpts into a query string.
func (opts ListOpts) ToFlavorListQuery() (string, error) {
q, err := gophercloud.BuildQueryString(opts)
if err != nil {
return "", err
}
return q.String(), nil
}
// ListDetail instructs OpenStack to provide a list of flavors.
// You may provide criteria by which List curtails its results for easier processing.
// See ListOpts for more details.
func ListDetail(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
url := listURL(client)
if opts != nil {
query, err := opts.ToFlavorListQuery()
if err != nil {
return pagination.Pager{Err: err}
}
url += query
}
createPage := func(r pagination.PageResult) pagination.Page {
return FlavorPage{pagination.LinkedPageBase{PageResult: r}}
}
return pagination.NewPager(client, url, createPage)
}
// Get instructs OpenStack to provide details on a single flavor, identified by its ID.
// Use ExtractFlavor to convert its result into a Flavor.
func Get(client *gophercloud.ServiceClient, id string) GetResult {
var res GetResult
_, res.Err = client.Get(getURL(client, id), &res.Body, nil)
return res
}
// IDFromName is a convienience function that returns a flavor's ID given its name.
func IDFromName(client *gophercloud.ServiceClient, name string) (string, error) {
flavorCount := 0
flavorID := ""
if name == "" {
return "", fmt.Errorf("A flavor name must be provided.")
}
pager := ListDetail(client, nil)
pager.EachPage(func(page pagination.Page) (bool, error) {
flavorList, err := ExtractFlavors(page)
if err != nil {
return false, err
}
for _, f := range flavorList {
if f.Name == name {
flavorCount++
flavorID = f.ID
}
}
return true, nil
})
switch flavorCount {
case 0:
return "", fmt.Errorf("Unable to find flavor: %s", name)
case 1:
return flavorID, nil
default:
return "", fmt.Errorf("Found %d flavors matching %s", flavorCount, name)
}
}

View File

@ -1,122 +0,0 @@
package flavors
import (
"errors"
"reflect"
"github.com/mitchellh/mapstructure"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
)
// ErrCannotInterpret is returned by an Extract call if the response body doesn't have the expected structure.
var ErrCannotInterpet = errors.New("Unable to interpret a response body.")
// GetResult temporarily holds the response from a Get call.
type GetResult struct {
gophercloud.Result
}
// Extract provides access to the individual Flavor returned by the Get function.
func (gr GetResult) Extract() (*Flavor, error) {
if gr.Err != nil {
return nil, gr.Err
}
var result struct {
Flavor Flavor `mapstructure:"flavor"`
}
cfg := &mapstructure.DecoderConfig{
DecodeHook: defaulter,
Result: &result,
}
decoder, err := mapstructure.NewDecoder(cfg)
if err != nil {
return nil, err
}
err = decoder.Decode(gr.Body)
return &result.Flavor, err
}
// Flavor records represent (virtual) hardware configurations for server resources in a region.
type Flavor struct {
// The Id field contains the flavor's unique identifier.
// For example, this identifier will be useful when specifying which hardware configuration to use for a new server instance.
ID string `mapstructure:"id"`
// The Disk and RA< fields provide a measure of storage space offered by the flavor, in GB and MB, respectively.
Disk int `mapstructure:"disk"`
RAM int `mapstructure:"ram"`
// The Name field provides a human-readable moniker for the flavor.
Name string `mapstructure:"name"`
RxTxFactor float64 `mapstructure:"rxtx_factor"`
// Swap indicates how much space is reserved for swap.
// If not provided, this field will be set to 0.
Swap int `mapstructure:"swap"`
// VCPUs indicates how many (virtual) CPUs are available for this flavor.
VCPUs int `mapstructure:"vcpus"`
}
// 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)
}
func defaulter(from, to reflect.Kind, v interface{}) (interface{}, error) {
if (from == reflect.String) && (to == reflect.Int) {
return 0, nil
}
return v, nil
}
// ExtractFlavors provides access to the list of flavors in a page acquired from the List operation.
func ExtractFlavors(page pagination.Page) ([]Flavor, error) {
casted := page.(FlavorPage).Body
var container struct {
Flavors []Flavor `mapstructure:"flavors"`
}
cfg := &mapstructure.DecoderConfig{
DecodeHook: defaulter,
Result: &container,
}
decoder, err := mapstructure.NewDecoder(cfg)
if err != nil {
return container.Flavors, err
}
err = decoder.Decode(casted)
if err != nil {
return container.Flavors, err
}
return container.Flavors, nil
}

View File

@ -1,13 +0,0 @@
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", "detail")
}

View File

@ -1,7 +0,0 @@
// Package images provides information and interaction with the image API
// resource in the OpenStack Compute service.
//
// An image is a collection of files used to create or rebuild a server.
// Operators provide a number of pre-built OS images by default. You may also
// create custom images from cloud servers you have launched.
package images

View File

@ -1,109 +0,0 @@
package images
import (
"fmt"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
)
// ListOptsBuilder allows extensions to add additional parameters to the
// List request.
type ListOptsBuilder interface {
ToImageListQuery() (string, error)
}
// ListOpts contain options for limiting the number of Images returned from a call to ListDetail.
type ListOpts struct {
// When the image last changed status (in date-time format).
ChangesSince string `q:"changes-since"`
// The number of Images to return.
Limit int `q:"limit"`
// UUID of the Image at which to set a marker.
Marker string `q:"marker"`
// The name of the Image.
Name string `q:"name"`
// The name of the Server (in URL format).
Server string `q:"server"`
// The current status of the Image.
Status string `q:"status"`
// The value of the type of image (e.g. BASE, SERVER, ALL)
Type string `q:"type"`
}
// ToImageListQuery formats a ListOpts into a query string.
func (opts ListOpts) ToImageListQuery() (string, error) {
q, err := gophercloud.BuildQueryString(opts)
if err != nil {
return "", err
}
return q.String(), nil
}
// ListDetail enumerates the available images.
func ListDetail(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
url := listDetailURL(client)
if opts != nil {
query, err := opts.ToImageListQuery()
if err != nil {
return pagination.Pager{Err: err}
}
url += query
}
createPage := func(r pagination.PageResult) pagination.Page {
return ImagePage{pagination.LinkedPageBase{PageResult: r}}
}
return pagination.NewPager(client, url, createPage)
}
// Get acquires additional detail about a specific image by ID.
// Use ExtractImage() to interpret the result as an openstack Image.
func Get(client *gophercloud.ServiceClient, id string) GetResult {
var result GetResult
_, result.Err = client.Get(getURL(client, id), &result.Body, nil)
return result
}
// Delete deletes the specified image ID.
func Delete(client *gophercloud.ServiceClient, id string) DeleteResult {
var result DeleteResult
_, result.Err = client.Delete(deleteURL(client, id), nil)
return result
}
// IDFromName is a convienience function that returns an image's ID given its name.
func IDFromName(client *gophercloud.ServiceClient, name string) (string, error) {
imageCount := 0
imageID := ""
if name == "" {
return "", fmt.Errorf("An image name must be provided.")
}
pager := ListDetail(client, &ListOpts{
Name: name,
})
pager.EachPage(func(page pagination.Page) (bool, error) {
imageList, err := ExtractImages(page)
if err != nil {
return false, err
}
for _, i := range imageList {
if i.Name == name {
imageCount++
imageID = i.ID
}
}
return true, nil
})
switch imageCount {
case 0:
return "", fmt.Errorf("Unable to find image: %s", name)
case 1:
return imageID, nil
default:
return "", fmt.Errorf("Found %d images matching %s", imageCount, name)
}
}

View File

@ -1,97 +0,0 @@
package images
import (
"github.com/mitchellh/mapstructure"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
)
// GetResult temporarily stores a Get response.
type GetResult struct {
gophercloud.Result
}
// DeleteResult represents the result of an image.Delete operation.
type DeleteResult struct {
gophercloud.ErrResult
}
// Extract interprets a GetResult as an Image.
func (gr GetResult) Extract() (*Image, error) {
if gr.Err != nil {
return nil, gr.Err
}
var decoded struct {
Image Image `mapstructure:"image"`
}
err := mapstructure.Decode(gr.Body, &decoded)
return &decoded.Image, err
}
// Image is used for JSON (un)marshalling.
// It provides a description of an OS image.
type Image struct {
// ID contains the image's unique identifier.
ID string
Created string
// MinDisk and MinRAM specify the minimum resources a server must provide to be able to install the image.
MinDisk int
MinRAM int
// Name provides a human-readable moniker for the OS image.
Name string
// The Progress and Status fields indicate image-creation status.
// Any usable image will have 100% progress.
Progress int
Status string
Updated string
Metadata map[string]string
}
// ImagePage contains a single page of results from a List operation.
// Use ExtractImages to convert it into a slice of usable structs.
type ImagePage struct {
pagination.LinkedPageBase
}
// IsEmpty returns true if a page contains no Image results.
func (page ImagePage) IsEmpty() (bool, error) {
images, err := ExtractImages(page)
if err != nil {
return true, err
}
return len(images) == 0, nil
}
// NextPageURL uses the response's embedded link reference to navigate to the next page of results.
func (page ImagePage) NextPageURL() (string, error) {
type resp struct {
Links []gophercloud.Link `mapstructure:"images_links"`
}
var r resp
err := mapstructure.Decode(page.Body, &r)
if err != nil {
return "", err
}
return gophercloud.ExtractNextURL(r.Links)
}
// ExtractImages converts a page of List results into a slice of usable Image structs.
func ExtractImages(page pagination.Page) ([]Image, error) {
casted := page.(ImagePage).Body
var results struct {
Images []Image `mapstructure:"images"`
}
err := mapstructure.Decode(casted, &results)
return results.Images, err
}

View File

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

View File

@ -1,6 +0,0 @@
// Package servers provides information and interaction with the server API
// resource in the OpenStack Compute service.
//
// A server is a virtual machine instance in the compute system. In order for
// one to be provisioned, a valid flavor and image are required.
package servers

View File

@ -1,692 +0,0 @@
// +build fixtures
package servers
import (
"fmt"
"net/http"
"testing"
th "github.com/rackspace/gophercloud/testhelper"
"github.com/rackspace/gophercloud/testhelper/client"
)
// ServerListBody contains the canned body of a servers.List response.
const ServerListBody = `
{
"servers": [
{
"status": "ACTIVE",
"updated": "2014-09-25T13:10:10Z",
"hostId": "29d3c8c896a45aa4c34e52247875d7fefc3d94bbcc9f622b5d204362",
"OS-EXT-SRV-ATTR:host": "devstack",
"addresses": {
"private": [
{
"OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:7c:1b:2b",
"version": 4,
"addr": "10.0.0.32",
"OS-EXT-IPS:type": "fixed"
}
]
},
"links": [
{
"href": "http://104.130.131.164:8774/v2/fcad67a6189847c4aecfa3c81a05783b/servers/ef079b0c-e610-4dfb-b1aa-b49f07ac48e5",
"rel": "self"
},
{
"href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/servers/ef079b0c-e610-4dfb-b1aa-b49f07ac48e5",
"rel": "bookmark"
}
],
"key_name": null,
"image": {
"id": "f90f6034-2570-4974-8351-6b49732ef2eb",
"links": [
{
"href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/images/f90f6034-2570-4974-8351-6b49732ef2eb",
"rel": "bookmark"
}
]
},
"OS-EXT-STS:task_state": null,
"OS-EXT-STS:vm_state": "active",
"OS-EXT-SRV-ATTR:instance_name": "instance-0000001e",
"OS-SRV-USG:launched_at": "2014-09-25T13:10:10.000000",
"OS-EXT-SRV-ATTR:hypervisor_hostname": "devstack",
"flavor": {
"id": "1",
"links": [
{
"href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/flavors/1",
"rel": "bookmark"
}
]
},
"id": "ef079b0c-e610-4dfb-b1aa-b49f07ac48e5",
"security_groups": [
{
"name": "default"
}
],
"OS-SRV-USG:terminated_at": null,
"OS-EXT-AZ:availability_zone": "nova",
"user_id": "9349aff8be7545ac9d2f1d00999a23cd",
"name": "herp",
"created": "2014-09-25T13:10:02Z",
"tenant_id": "fcad67a6189847c4aecfa3c81a05783b",
"OS-DCF:diskConfig": "MANUAL",
"os-extended-volumes:volumes_attached": [],
"accessIPv4": "",
"accessIPv6": "",
"progress": 0,
"OS-EXT-STS:power_state": 1,
"config_drive": "",
"metadata": {}
},
{
"status": "ACTIVE",
"updated": "2014-09-25T13:04:49Z",
"hostId": "29d3c8c896a45aa4c34e52247875d7fefc3d94bbcc9f622b5d204362",
"OS-EXT-SRV-ATTR:host": "devstack",
"addresses": {
"private": [
{
"OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:9e:89:be",
"version": 4,
"addr": "10.0.0.31",
"OS-EXT-IPS:type": "fixed"
}
]
},
"links": [
{
"href": "http://104.130.131.164:8774/v2/fcad67a6189847c4aecfa3c81a05783b/servers/9e5476bd-a4ec-4653-93d6-72c93aa682ba",
"rel": "self"
},
{
"href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/servers/9e5476bd-a4ec-4653-93d6-72c93aa682ba",
"rel": "bookmark"
}
],
"key_name": null,
"image": {
"id": "f90f6034-2570-4974-8351-6b49732ef2eb",
"links": [
{
"href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/images/f90f6034-2570-4974-8351-6b49732ef2eb",
"rel": "bookmark"
}
]
},
"OS-EXT-STS:task_state": null,
"OS-EXT-STS:vm_state": "active",
"OS-EXT-SRV-ATTR:instance_name": "instance-0000001d",
"OS-SRV-USG:launched_at": "2014-09-25T13:04:49.000000",
"OS-EXT-SRV-ATTR:hypervisor_hostname": "devstack",
"flavor": {
"id": "1",
"links": [
{
"href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/flavors/1",
"rel": "bookmark"
}
]
},
"id": "9e5476bd-a4ec-4653-93d6-72c93aa682ba",
"security_groups": [
{
"name": "default"
}
],
"OS-SRV-USG:terminated_at": null,
"OS-EXT-AZ:availability_zone": "nova",
"user_id": "9349aff8be7545ac9d2f1d00999a23cd",
"name": "derp",
"created": "2014-09-25T13:04:41Z",
"tenant_id": "fcad67a6189847c4aecfa3c81a05783b",
"OS-DCF:diskConfig": "MANUAL",
"os-extended-volumes:volumes_attached": [],
"accessIPv4": "",
"accessIPv6": "",
"progress": 0,
"OS-EXT-STS:power_state": 1,
"config_drive": "",
"metadata": {}
}
]
}
`
// SingleServerBody is the canned body of a Get request on an existing server.
const SingleServerBody = `
{
"server": {
"status": "ACTIVE",
"updated": "2014-09-25T13:04:49Z",
"hostId": "29d3c8c896a45aa4c34e52247875d7fefc3d94bbcc9f622b5d204362",
"OS-EXT-SRV-ATTR:host": "devstack",
"addresses": {
"private": [
{
"OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:9e:89:be",
"version": 4,
"addr": "10.0.0.31",
"OS-EXT-IPS:type": "fixed"
}
]
},
"links": [
{
"href": "http://104.130.131.164:8774/v2/fcad67a6189847c4aecfa3c81a05783b/servers/9e5476bd-a4ec-4653-93d6-72c93aa682ba",
"rel": "self"
},
{
"href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/servers/9e5476bd-a4ec-4653-93d6-72c93aa682ba",
"rel": "bookmark"
}
],
"key_name": null,
"image": {
"id": "f90f6034-2570-4974-8351-6b49732ef2eb",
"links": [
{
"href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/images/f90f6034-2570-4974-8351-6b49732ef2eb",
"rel": "bookmark"
}
]
},
"OS-EXT-STS:task_state": null,
"OS-EXT-STS:vm_state": "active",
"OS-EXT-SRV-ATTR:instance_name": "instance-0000001d",
"OS-SRV-USG:launched_at": "2014-09-25T13:04:49.000000",
"OS-EXT-SRV-ATTR:hypervisor_hostname": "devstack",
"flavor": {
"id": "1",
"links": [
{
"href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/flavors/1",
"rel": "bookmark"
}
]
},
"id": "9e5476bd-a4ec-4653-93d6-72c93aa682ba",
"security_groups": [
{
"name": "default"
}
],
"OS-SRV-USG:terminated_at": null,
"OS-EXT-AZ:availability_zone": "nova",
"user_id": "9349aff8be7545ac9d2f1d00999a23cd",
"name": "derp",
"created": "2014-09-25T13:04:41Z",
"tenant_id": "fcad67a6189847c4aecfa3c81a05783b",
"OS-DCF:diskConfig": "MANUAL",
"os-extended-volumes:volumes_attached": [],
"accessIPv4": "",
"accessIPv6": "",
"progress": 0,
"OS-EXT-STS:power_state": 1,
"config_drive": "",
"metadata": {}
}
}
`
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{
Status: "ACTIVE",
Updated: "2014-09-25T13:10:10Z",
HostID: "29d3c8c896a45aa4c34e52247875d7fefc3d94bbcc9f622b5d204362",
Addresses: map[string]interface{}{
"private": []interface{}{
map[string]interface{}{
"OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:7c:1b:2b",
"version": float64(4),
"addr": "10.0.0.32",
"OS-EXT-IPS:type": "fixed",
},
},
},
Links: []interface{}{
map[string]interface{}{
"href": "http://104.130.131.164:8774/v2/fcad67a6189847c4aecfa3c81a05783b/servers/ef079b0c-e610-4dfb-b1aa-b49f07ac48e5",
"rel": "self",
},
map[string]interface{}{
"href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/servers/ef079b0c-e610-4dfb-b1aa-b49f07ac48e5",
"rel": "bookmark",
},
},
Image: map[string]interface{}{
"id": "f90f6034-2570-4974-8351-6b49732ef2eb",
"links": []interface{}{
map[string]interface{}{
"href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/images/f90f6034-2570-4974-8351-6b49732ef2eb",
"rel": "bookmark",
},
},
},
Flavor: map[string]interface{}{
"id": "1",
"links": []interface{}{
map[string]interface{}{
"href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/flavors/1",
"rel": "bookmark",
},
},
},
ID: "ef079b0c-e610-4dfb-b1aa-b49f07ac48e5",
UserID: "9349aff8be7545ac9d2f1d00999a23cd",
Name: "herp",
Created: "2014-09-25T13:10:02Z",
TenantID: "fcad67a6189847c4aecfa3c81a05783b",
Metadata: map[string]interface{}{},
SecurityGroups: []map[string]interface{}{
map[string]interface{}{
"name": "default",
},
},
}
// ServerDerp is a Server struct that should correspond to the second server in ServerListBody.
ServerDerp = Server{
Status: "ACTIVE",
Updated: "2014-09-25T13:04:49Z",
HostID: "29d3c8c896a45aa4c34e52247875d7fefc3d94bbcc9f622b5d204362",
Addresses: map[string]interface{}{
"private": []interface{}{
map[string]interface{}{
"OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:9e:89:be",
"version": float64(4),
"addr": "10.0.0.31",
"OS-EXT-IPS:type": "fixed",
},
},
},
Links: []interface{}{
map[string]interface{}{
"href": "http://104.130.131.164:8774/v2/fcad67a6189847c4aecfa3c81a05783b/servers/9e5476bd-a4ec-4653-93d6-72c93aa682ba",
"rel": "self",
},
map[string]interface{}{
"href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/servers/9e5476bd-a4ec-4653-93d6-72c93aa682ba",
"rel": "bookmark",
},
},
Image: map[string]interface{}{
"id": "f90f6034-2570-4974-8351-6b49732ef2eb",
"links": []interface{}{
map[string]interface{}{
"href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/images/f90f6034-2570-4974-8351-6b49732ef2eb",
"rel": "bookmark",
},
},
},
Flavor: map[string]interface{}{
"id": "1",
"links": []interface{}{
map[string]interface{}{
"href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/flavors/1",
"rel": "bookmark",
},
},
},
ID: "9e5476bd-a4ec-4653-93d6-72c93aa682ba",
UserID: "9349aff8be7545ac9d2f1d00999a23cd",
Name: "derp",
Created: "2014-09-25T13:04:41Z",
TenantID: "fcad67a6189847c4aecfa3c81a05783b",
Metadata: map[string]interface{}{},
SecurityGroups: []map[string]interface{}{
map[string]interface{}{
"name": "default",
},
},
}
)
// HandleServerCreationSuccessfully sets up the test server to respond to a server creation request
// with a given response.
func HandleServerCreationSuccessfully(t *testing.T, response string) {
th.Mux.HandleFunc("/servers", 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, `{
"server": {
"name": "derp",
"imageRef": "f90f6034-2570-4974-8351-6b49732ef2eb",
"flavorRef": "1"
}
}`)
w.WriteHeader(http.StatusAccepted)
w.Header().Add("Content-Type", "application/json")
fmt.Fprintf(w, response)
})
}
// HandleServerListSuccessfully sets up the test server to respond to a server List request.
func HandleServerListSuccessfully(t *testing.T) {
th.Mux.HandleFunc("/servers/detail", 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")
r.ParseForm()
marker := r.Form.Get("marker")
switch marker {
case "":
fmt.Fprintf(w, ServerListBody)
case "9e5476bd-a4ec-4653-93d6-72c93aa682ba":
fmt.Fprintf(w, `{ "servers": [] }`)
default:
t.Fatalf("/servers/detail invoked with unexpected marker=[%s]", marker)
}
})
}
// HandleServerDeletionSuccessfully sets up the test server to respond to a server deletion request.
func HandleServerDeletionSuccessfully(t *testing.T) {
th.Mux.HandleFunc("/servers/asdfasdfasdf", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "DELETE")
th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
w.WriteHeader(http.StatusNoContent)
})
}
// 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) {
th.TestMethod(t, r, "GET")
th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
th.TestHeader(t, r, "Accept", "application/json")
fmt.Fprintf(w, SingleServerBody)
})
}
// HandleServerUpdateSuccessfully sets up the test server to respond to a server Update request.
func HandleServerUpdateSuccessfully(t *testing.T) {
th.Mux.HandleFunc("/servers/1234asdf", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "PUT")
th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
th.TestHeader(t, r, "Accept", "application/json")
th.TestHeader(t, r, "Content-Type", "application/json")
th.TestJSONRequest(t, r, `{ "server": { "name": "new-name" } }`)
fmt.Fprintf(w, SingleServerBody)
})
}
// HandleAdminPasswordChangeSuccessfully sets up the test server to respond to a server password
// change request.
func HandleAdminPasswordChangeSuccessfully(t *testing.T) {
th.Mux.HandleFunc("/servers/1234asdf/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, `{ "changePassword": { "adminPass": "new-password" } }`)
w.WriteHeader(http.StatusAccepted)
})
}
// HandleRebootSuccessfully sets up the test server to respond to a reboot request with success.
func HandleRebootSuccessfully(t *testing.T) {
th.Mux.HandleFunc("/servers/1234asdf/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, `{ "reboot": { "type": "SOFT" } }`)
w.WriteHeader(http.StatusAccepted)
})
}
// HandleRebuildSuccessfully sets up the test server to respond to a rebuild request with success.
func HandleRebuildSuccessfully(t *testing.T, response string) {
th.Mux.HandleFunc("/servers/1234asdf/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, `
{
"rebuild": {
"name": "new-name",
"adminPass": "swordfish",
"imageRef": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/images/f90f6034-2570-4974-8351-6b49732ef2eb",
"accessIPv4": "1.2.3.4"
}
}
`)
w.WriteHeader(http.StatusAccepted)
w.Header().Add("Content-Type", "application/json")
fmt.Fprintf(w, response)
})
}
// HandleServerRescueSuccessfully sets up the test server to respond to a server Rescue request.
func HandleServerRescueSuccessfully(t *testing.T) {
th.Mux.HandleFunc("/servers/1234asdf/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, `{ "rescue": { "adminPass": "1234567890" } }`)
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{ "adminPass": "1234567890" }`))
})
}
// HandleMetadatumGetSuccessfully sets up the test server to respond to a metadatum Get request.
func HandleMetadatumGetSuccessfully(t *testing.T) {
th.Mux.HandleFunc("/servers/1234asdf/metadata/foo", 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")
w.WriteHeader(http.StatusOK)
w.Header().Add("Content-Type", "application/json")
w.Write([]byte(`{ "meta": {"foo":"bar"}}`))
})
}
// HandleMetadatumCreateSuccessfully sets up the test server to respond to a metadatum Create request.
func HandleMetadatumCreateSuccessfully(t *testing.T) {
th.Mux.HandleFunc("/servers/1234asdf/metadata/foo", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "PUT")
th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
th.TestJSONRequest(t, r, `{
"meta": {
"foo": "bar"
}
}`)
w.WriteHeader(http.StatusOK)
w.Header().Add("Content-Type", "application/json")
w.Write([]byte(`{ "meta": {"foo":"bar"}}`))
})
}
// HandleMetadatumDeleteSuccessfully sets up the test server to respond to a metadatum Delete request.
func HandleMetadatumDeleteSuccessfully(t *testing.T) {
th.Mux.HandleFunc("/servers/1234asdf/metadata/foo", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "DELETE")
th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
w.WriteHeader(http.StatusNoContent)
})
}
// HandleMetadataGetSuccessfully sets up the test server to respond to a metadata Get request.
func HandleMetadataGetSuccessfully(t *testing.T) {
th.Mux.HandleFunc("/servers/1234asdf/metadata", 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")
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{ "metadata": {"foo":"bar", "this":"that"}}`))
})
}
// HandleMetadataResetSuccessfully sets up the test server to respond to a metadata Create request.
func HandleMetadataResetSuccessfully(t *testing.T) {
th.Mux.HandleFunc("/servers/1234asdf/metadata", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "PUT")
th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
th.TestJSONRequest(t, r, `{
"metadata": {
"foo": "bar",
"this": "that"
}
}`)
w.WriteHeader(http.StatusOK)
w.Header().Add("Content-Type", "application/json")
w.Write([]byte(`{ "metadata": {"foo":"bar", "this":"that"}}`))
})
}
// HandleMetadataUpdateSuccessfully sets up the test server to respond to a metadata Update request.
func HandleMetadataUpdateSuccessfully(t *testing.T) {
th.Mux.HandleFunc("/servers/1234asdf/metadata", 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, `{
"metadata": {
"foo": "baz",
"this": "those"
}
}`)
w.WriteHeader(http.StatusOK)
w.Header().Add("Content-Type", "application/json")
w.Write([]byte(`{ "metadata": {"foo":"baz", "this":"those"}}`))
})
}
// ListAddressesExpected represents an expected repsonse from a ListAddresses request.
var ListAddressesExpected = map[string][]Address{
"public": []Address{
Address{
Version: 4,
Address: "80.56.136.39",
},
Address{
Version: 6,
Address: "2001:4800:790e:510:be76:4eff:fe04:82a8",
},
},
"private": []Address{
Address{
Version: 4,
Address: "10.880.3.154",
},
},
}
// HandleAddressListSuccessfully sets up the test server to respond to a ListAddresses request.
func HandleAddressListSuccessfully(t *testing.T) {
th.Mux.HandleFunc("/servers/asdfasdfasdf/ips", 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, `{
"addresses": {
"public": [
{
"version": 4,
"addr": "50.56.176.35"
},
{
"version": 6,
"addr": "2001:4800:780e:510:be76:4eff:fe04:84a8"
}
],
"private": [
{
"version": 4,
"addr": "10.180.3.155"
}
]
}
}`)
})
}
// ListNetworkAddressesExpected represents an expected repsonse from a ListAddressesByNetwork request.
var ListNetworkAddressesExpected = []Address{
Address{
Version: 4,
Address: "50.56.176.35",
},
Address{
Version: 6,
Address: "2001:4800:780e:510:be76:4eff:fe04:84a8",
},
}
// HandleNetworkAddressListSuccessfully sets up the test server to respond to a ListAddressesByNetwork request.
func HandleNetworkAddressListSuccessfully(t *testing.T) {
th.Mux.HandleFunc("/servers/asdfasdfasdf/ips/public", 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, `{
"public": [
{
"version": 4,
"addr": "50.56.176.35"
},
{
"version": 6,
"addr": "2001:4800:780e:510:be76:4eff:fe04:84a8"
}
]
}`)
})
}
// HandleCreateServerImageSuccessfully sets up the test server to respond to a TestCreateServerImage request.
func HandleCreateServerImageSuccessfully(t *testing.T) {
th.Mux.HandleFunc("/servers/serverimage/action", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "POST")
th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
w.Header().Add("Location", "https://0.0.0.0/images/xxxx-xxxxx-xxxxx-xxxx")
w.WriteHeader(http.StatusAccepted)
})
}
// 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

@ -1,872 +0,0 @@
package servers
import (
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/openstack/compute/v2/flavors"
"github.com/rackspace/gophercloud/openstack/compute/v2/images"
"github.com/rackspace/gophercloud/pagination"
)
// ListOptsBuilder allows extensions to add additional parameters to the
// List request.
type ListOptsBuilder interface {
ToServerListQuery() (string, error)
}
// ListOpts allows the filtering and sorting of paginated collections through
// the API. Filtering is achieved by passing in struct field values that map to
// the server attributes you want to see returned. Marker and Limit are used
// for pagination.
type ListOpts struct {
// A time/date stamp for when the server last changed status.
ChangesSince string `q:"changes-since"`
// Name of the image in URL format.
Image string `q:"image"`
// Name of the flavor in URL format.
Flavor string `q:"flavor"`
// Name of the server as a string; can be queried with regular expressions.
// Realize that ?name=bob returns both bob and bobb. If you need to match bob
// only, you can use a regular expression matching the syntax of the
// underlying database server implemented for Compute.
Name string `q:"name"`
// Value of the status of the server so that you can filter on "ACTIVE" for example.
Status string `q:"status"`
// Name of the host as a string.
Host string `q:"host"`
// UUID of the server at which you want to set a marker.
Marker string `q:"marker"`
// Integer value for the limit of values to return.
Limit int `q:"limit"`
// Bool to show all tenants
AllTenants bool `q:"all_tenants"`
}
// ToServerListQuery formats a ListOpts into a query string.
func (opts ListOpts) ToServerListQuery() (string, error) {
q, err := gophercloud.BuildQueryString(opts)
if err != nil {
return "", err
}
return q.String(), nil
}
// List makes a request against the API to list servers accessible to you.
func List(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
url := listDetailURL(client)
if opts != nil {
query, err := opts.ToServerListQuery()
if err != nil {
return pagination.Pager{Err: err}
}
url += query
}
createPageFn := func(r pagination.PageResult) pagination.Page {
return ServerPage{pagination.LinkedPageBase{PageResult: r}}
}
return pagination.NewPager(client, url, createPageFn)
}
// CreateOptsBuilder describes struct types that can be accepted by the Create call.
// The CreateOpts struct in this package does.
type CreateOptsBuilder interface {
ToServerCreateMap() (map[string]interface{}, error)
}
// Network is used within CreateOpts to control a new server's network attachments.
type Network struct {
// UUID of a nova-network to attach to the newly provisioned server.
// Required unless Port is provided.
UUID string
// Port of a neutron network to attach to the newly provisioned server.
// Required unless UUID is provided.
Port string
// FixedIP [optional] specifies a fixed IPv4 address to be used on this network.
FixedIP string
}
// Personality is an array of files that are injected into the server at launch.
type Personality []*File
// File is used within CreateOpts and RebuildOpts to inject a file into the server at launch.
// File implements the json.Marshaler interface, so when a Create or Rebuild operation is requested,
// json.Marshal will call File's MarshalJSON method.
type File struct {
// Path of the file
Path string
// Contents of the file. Maximum content size is 255 bytes.
Contents []byte
}
// MarshalJSON marshals the escaped file, base64 encoding the contents.
func (f *File) MarshalJSON() ([]byte, error) {
file := struct {
Path string `json:"path"`
Contents string `json:"contents"`
}{
Path: f.Path,
Contents: base64.StdEncoding.EncodeToString(f.Contents),
}
return json.Marshal(file)
}
// CreateOpts specifies server creation parameters.
type CreateOpts struct {
// Name [required] is the name to assign to the newly launched server.
Name string
// ImageRef [optional; required if ImageName is not provided] is the ID or full
// URL to the image that contains the server's OS and initial state.
// Also optional if using the boot-from-volume extension.
ImageRef string
// ImageName [optional; required if ImageRef is not provided] is the name of the
// image that contains the server's OS and initial state.
// Also optional if using the boot-from-volume extension.
ImageName string
// FlavorRef [optional; required if FlavorName is not provided] is the ID or
// full URL to the flavor that describes the server's specs.
FlavorRef string
// FlavorName [optional; required if FlavorRef is not provided] is the name of
// the flavor that describes the server's specs.
FlavorName string
// SecurityGroups [optional] lists the names of the security groups to which this server should belong.
SecurityGroups []string
// UserData [optional] contains configuration information or scripts to use upon launch.
// Create will base64-encode it for you.
UserData []byte
// AvailabilityZone [optional] in which to launch the server.
AvailabilityZone string
// Networks [optional] dictates how this server will be attached to available networks.
// By default, the server will be attached to all isolated networks for the tenant.
Networks []Network
// Metadata [optional] contains key-value pairs (up to 255 bytes each) to attach to the server.
Metadata map[string]string
// Personality [optional] includes files to inject into the server at launch.
// Create will base64-encode file contents for you.
Personality Personality
// ConfigDrive [optional] enables metadata injection through a configuration drive.
ConfigDrive bool
// AdminPass [optional] sets the root user password. If not set, a randomly-generated
// password will be created and returned in the response.
AdminPass string
// AccessIPv4 [optional] specifies an IPv4 address for the instance.
AccessIPv4 string
// AccessIPv6 [optional] specifies an IPv6 address for the instance.
AccessIPv6 string
}
// ToServerCreateMap assembles a request body based on the contents of a CreateOpts.
func (opts CreateOpts) ToServerCreateMap() (map[string]interface{}, error) {
server := make(map[string]interface{})
server["name"] = opts.Name
server["imageRef"] = opts.ImageRef
server["imageName"] = opts.ImageName
server["flavorRef"] = opts.FlavorRef
server["flavorName"] = opts.FlavorName
if opts.UserData != nil {
encoded := base64.StdEncoding.EncodeToString(opts.UserData)
server["user_data"] = &encoded
}
if opts.ConfigDrive {
server["config_drive"] = "true"
}
if opts.AvailabilityZone != "" {
server["availability_zone"] = opts.AvailabilityZone
}
if opts.Metadata != nil {
server["metadata"] = opts.Metadata
}
if opts.AdminPass != "" {
server["adminPass"] = opts.AdminPass
}
if opts.AccessIPv4 != "" {
server["accessIPv4"] = opts.AccessIPv4
}
if opts.AccessIPv6 != "" {
server["accessIPv6"] = opts.AccessIPv6
}
if len(opts.SecurityGroups) > 0 {
securityGroups := make([]map[string]interface{}, len(opts.SecurityGroups))
for i, groupName := range opts.SecurityGroups {
securityGroups[i] = map[string]interface{}{"name": groupName}
}
server["security_groups"] = securityGroups
}
if len(opts.Networks) > 0 {
networks := make([]map[string]interface{}, len(opts.Networks))
for i, net := range opts.Networks {
networks[i] = make(map[string]interface{})
if net.UUID != "" {
networks[i]["uuid"] = net.UUID
}
if net.Port != "" {
networks[i]["port"] = net.Port
}
if net.FixedIP != "" {
networks[i]["fixed_ip"] = net.FixedIP
}
}
server["networks"] = networks
}
if len(opts.Personality) > 0 {
server["personality"] = opts.Personality
}
return map[string]interface{}{"server": server}, nil
}
// Create requests a server to be provisioned to the user in the current tenant.
func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult {
var res CreateResult
reqBody, err := opts.ToServerCreateMap()
if err != nil {
res.Err = err
return res
}
// If ImageRef isn't provided, use ImageName to ascertain the image ID.
if reqBody["server"].(map[string]interface{})["imageRef"].(string) == "" {
imageName := reqBody["server"].(map[string]interface{})["imageName"].(string)
if imageName == "" {
res.Err = errors.New("One and only one of ImageRef and ImageName must be provided.")
return res
}
imageID, err := images.IDFromName(client, imageName)
if err != nil {
res.Err = err
return res
}
reqBody["server"].(map[string]interface{})["imageRef"] = imageID
}
delete(reqBody["server"].(map[string]interface{}), "imageName")
// If FlavorRef isn't provided, use FlavorName to ascertain the flavor ID.
if reqBody["server"].(map[string]interface{})["flavorRef"].(string) == "" {
flavorName := reqBody["server"].(map[string]interface{})["flavorName"].(string)
if flavorName == "" {
res.Err = errors.New("One and only one of FlavorRef and FlavorName must be provided.")
return res
}
flavorID, err := flavors.IDFromName(client, flavorName)
if err != nil {
res.Err = err
return res
}
reqBody["server"].(map[string]interface{})["flavorRef"] = flavorID
}
delete(reqBody["server"].(map[string]interface{}), "flavorName")
_, res.Err = client.Post(listURL(client), reqBody, &res.Body, nil)
return res
}
// Delete requests that a server previously provisioned be removed from your account.
func Delete(client *gophercloud.ServiceClient, id string) DeleteResult {
var res DeleteResult
_, res.Err = client.Delete(deleteURL(client, id), nil)
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
_, result.Err = client.Get(getURL(client, id), &result.Body, &gophercloud.RequestOpts{
OkCodes: []int{200, 203},
})
return result
}
// UpdateOptsBuilder allows extensions to add additional attributes to the Update request.
type UpdateOptsBuilder interface {
ToServerUpdateMap() map[string]interface{}
}
// UpdateOpts specifies the base attributes that may be updated on an existing server.
type UpdateOpts struct {
// Name [optional] changes the displayed name of the server.
// The server host name will *not* change.
// Server names are not constrained to be unique, even within the same tenant.
Name string
// AccessIPv4 [optional] provides a new IPv4 address for the instance.
AccessIPv4 string
// AccessIPv6 [optional] provides a new IPv6 address for the instance.
AccessIPv6 string
}
// ToServerUpdateMap formats an UpdateOpts structure into a request body.
func (opts UpdateOpts) ToServerUpdateMap() map[string]interface{} {
server := make(map[string]string)
if opts.Name != "" {
server["name"] = opts.Name
}
if opts.AccessIPv4 != "" {
server["accessIPv4"] = opts.AccessIPv4
}
if opts.AccessIPv6 != "" {
server["accessIPv6"] = opts.AccessIPv6
}
return map[string]interface{}{"server": server}
}
// Update requests that various attributes of the indicated server be changed.
func Update(client *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) UpdateResult {
var result UpdateResult
reqBody := opts.ToServerUpdateMap()
_, result.Err = client.Put(updateURL(client, id), reqBody, &result.Body, &gophercloud.RequestOpts{
OkCodes: []int{200},
})
return result
}
// ChangeAdminPassword alters the administrator or root password for a specified server.
func ChangeAdminPassword(client *gophercloud.ServiceClient, id, newPassword string) ActionResult {
var req struct {
ChangePassword struct {
AdminPass string `json:"adminPass"`
} `json:"changePassword"`
}
req.ChangePassword.AdminPass = newPassword
var res ActionResult
_, res.Err = client.Post(actionURL(client, id), req, nil, nil)
return res
}
// ErrArgument errors occur when an argument supplied to a package function
// fails to fall within acceptable values. For example, the Reboot() function
// expects the "how" parameter to be one of HardReboot or SoftReboot. These
// constants are (currently) strings, leading someone to wonder if they can pass
// other string values instead, perhaps in an effort to break the API of their
// provider. Reboot() returns this error in this situation.
//
// Function identifies which function was called/which function is generating
// the error.
// Argument identifies which formal argument was responsible for producing the
// error.
// Value provides the value as it was passed into the function.
type ErrArgument struct {
Function, Argument string
Value interface{}
}
// Error yields a useful diagnostic for debugging purposes.
func (e *ErrArgument) Error() string {
return fmt.Sprintf("Bad argument in call to %s, formal parameter %s, value %#v", e.Function, e.Argument, e.Value)
}
func (e *ErrArgument) String() string {
return e.Error()
}
// RebootMethod describes the mechanisms by which a server reboot can be requested.
type RebootMethod string
// These constants determine how a server should be rebooted.
// See the Reboot() function for further details.
const (
SoftReboot RebootMethod = "SOFT"
HardReboot RebootMethod = "HARD"
OSReboot = SoftReboot
PowerCycle = HardReboot
)
// Reboot requests that a given server reboot.
// Two methods exist for rebooting a server:
//
// HardReboot (aka PowerCycle) restarts the server instance by physically cutting power to the machine, or if a VM,
// terminating it at the hypervisor level.
// It's done. Caput. Full stop.
// Then, after a brief while, power is restored or the VM instance restarted.
//
// SoftReboot (aka OSReboot) simply tells the OS to restart under its own procedures.
// E.g., in Linux, asking it to enter runlevel 6, or executing "sudo shutdown -r now", or by asking Windows to restart the machine.
func Reboot(client *gophercloud.ServiceClient, id string, how RebootMethod) ActionResult {
var res ActionResult
if (how != SoftReboot) && (how != HardReboot) {
res.Err = &ErrArgument{
Function: "Reboot",
Argument: "how",
Value: how,
}
return res
}
reqBody := struct {
C map[string]string `json:"reboot"`
}{
map[string]string{"type": string(how)},
}
_, res.Err = client.Post(actionURL(client, id), reqBody, nil, nil)
return res
}
// RebuildOptsBuilder is an interface that allows extensions to override the
// default behaviour of rebuild options
type RebuildOptsBuilder interface {
ToServerRebuildMap() (map[string]interface{}, error)
}
// RebuildOpts represents the configuration options used in a server rebuild
// operation
type RebuildOpts struct {
// Required. The ID of the image you want your server to be provisioned on
ImageID string
// Name to set the server to
Name string
// Required. The server's admin password
AdminPass string
// AccessIPv4 [optional] provides a new IPv4 address for the instance.
AccessIPv4 string
// AccessIPv6 [optional] provides a new IPv6 address for the instance.
AccessIPv6 string
// Metadata [optional] contains key-value pairs (up to 255 bytes each) to attach to the server.
Metadata map[string]string
// Personality [optional] includes files to inject into the server at launch.
// Rebuild will base64-encode file contents for you.
Personality Personality
}
// ToServerRebuildMap formats a RebuildOpts struct into a map for use in JSON
func (opts RebuildOpts) ToServerRebuildMap() (map[string]interface{}, error) {
var err error
server := make(map[string]interface{})
if opts.AdminPass == "" {
err = fmt.Errorf("AdminPass is required")
}
if opts.ImageID == "" {
err = fmt.Errorf("ImageID is required")
}
if err != nil {
return server, err
}
server["name"] = opts.Name
server["adminPass"] = opts.AdminPass
server["imageRef"] = opts.ImageID
if opts.AccessIPv4 != "" {
server["accessIPv4"] = opts.AccessIPv4
}
if opts.AccessIPv6 != "" {
server["accessIPv6"] = opts.AccessIPv6
}
if opts.Metadata != nil {
server["metadata"] = opts.Metadata
}
if len(opts.Personality) > 0 {
server["personality"] = opts.Personality
}
return map[string]interface{}{"rebuild": server}, nil
}
// Rebuild will reprovision the server according to the configuration options
// provided in the RebuildOpts struct.
func Rebuild(client *gophercloud.ServiceClient, id string, opts RebuildOptsBuilder) RebuildResult {
var result RebuildResult
if id == "" {
result.Err = fmt.Errorf("ID is required")
return result
}
reqBody, err := opts.ToServerRebuildMap()
if err != nil {
result.Err = err
return result
}
_, result.Err = client.Post(actionURL(client, id), reqBody, &result.Body, nil)
return result
}
// ResizeOptsBuilder is an interface that allows extensions to override the default structure of
// a Resize request.
type ResizeOptsBuilder interface {
ToServerResizeMap() (map[string]interface{}, error)
}
// ResizeOpts represents the configuration options used to control a Resize operation.
type ResizeOpts struct {
// FlavorRef is the ID of the flavor you wish your server to become.
FlavorRef string
}
// ToServerResizeMap formats a ResizeOpts as a map that can be used as a JSON request body for the
// Resize request.
func (opts ResizeOpts) ToServerResizeMap() (map[string]interface{}, error) {
resize := map[string]interface{}{
"flavorRef": opts.FlavorRef,
}
return map[string]interface{}{"resize": resize}, nil
}
// Resize instructs the provider to change the flavor of the server.
// Note that this implies rebuilding it.
// Unfortunately, one cannot pass rebuild parameters to the resize function.
// When the resize completes, the server will be in RESIZE_VERIFY state.
// While in this state, you can explore the use of the new server's configuration.
// If you like it, call ConfirmResize() to commit the resize permanently.
// Otherwise, call RevertResize() to restore the old configuration.
func Resize(client *gophercloud.ServiceClient, id string, opts ResizeOptsBuilder) ActionResult {
var res ActionResult
reqBody, err := opts.ToServerResizeMap()
if err != nil {
res.Err = err
return res
}
_, res.Err = client.Post(actionURL(client, id), reqBody, nil, nil)
return res
}
// ConfirmResize confirms a previous resize operation on a server.
// See Resize() for more details.
func ConfirmResize(client *gophercloud.ServiceClient, id string) ActionResult {
var res ActionResult
reqBody := map[string]interface{}{"confirmResize": nil}
_, res.Err = client.Post(actionURL(client, id), reqBody, nil, &gophercloud.RequestOpts{
OkCodes: []int{201, 202, 204},
})
return res
}
// RevertResize cancels a previous resize operation on a server.
// See Resize() for more details.
func RevertResize(client *gophercloud.ServiceClient, id string) ActionResult {
var res ActionResult
reqBody := map[string]interface{}{"revertResize": nil}
_, res.Err = client.Post(actionURL(client, id), reqBody, nil, nil)
return res
}
// RescueOptsBuilder is an interface that allows extensions to override the
// default structure of a Rescue request.
type RescueOptsBuilder interface {
ToServerRescueMap() (map[string]interface{}, error)
}
// RescueOpts represents the configuration options used to control a Rescue
// option.
type RescueOpts struct {
// AdminPass is the desired administrative password for the instance in
// RESCUE mode. If it's left blank, the server will generate a password.
AdminPass string
}
// ToServerRescueMap formats a RescueOpts as a map that can be used as a JSON
// request body for the Rescue request.
func (opts RescueOpts) ToServerRescueMap() (map[string]interface{}, error) {
server := make(map[string]interface{})
if opts.AdminPass != "" {
server["adminPass"] = opts.AdminPass
}
return map[string]interface{}{"rescue": server}, nil
}
// Rescue instructs the provider to place the server into RESCUE mode.
func Rescue(client *gophercloud.ServiceClient, id string, opts RescueOptsBuilder) RescueResult {
var result RescueResult
if id == "" {
result.Err = fmt.Errorf("ID is required")
return result
}
reqBody, err := opts.ToServerRescueMap()
if err != nil {
result.Err = err
return result
}
_, result.Err = client.Post(actionURL(client, id), reqBody, &result.Body, &gophercloud.RequestOpts{
OkCodes: []int{200},
})
return result
}
// ResetMetadataOptsBuilder allows extensions to add additional parameters to the
// Reset request.
type ResetMetadataOptsBuilder interface {
ToMetadataResetMap() (map[string]interface{}, error)
}
// MetadataOpts is a map that contains key-value pairs.
type MetadataOpts map[string]string
// ToMetadataResetMap assembles a body for a Reset request based on the contents of a MetadataOpts.
func (opts MetadataOpts) ToMetadataResetMap() (map[string]interface{}, error) {
return map[string]interface{}{"metadata": opts}, nil
}
// ToMetadataUpdateMap assembles a body for an Update request based on the contents of a MetadataOpts.
func (opts MetadataOpts) ToMetadataUpdateMap() (map[string]interface{}, error) {
return map[string]interface{}{"metadata": opts}, nil
}
// ResetMetadata will create multiple new key-value pairs for the given server ID.
// Note: Using this operation will erase any already-existing metadata and create
// the new metadata provided. To keep any already-existing metadata, use the
// UpdateMetadatas or UpdateMetadata function.
func ResetMetadata(client *gophercloud.ServiceClient, id string, opts ResetMetadataOptsBuilder) ResetMetadataResult {
var res ResetMetadataResult
metadata, err := opts.ToMetadataResetMap()
if err != nil {
res.Err = err
return res
}
_, res.Err = client.Put(metadataURL(client, id), metadata, &res.Body, &gophercloud.RequestOpts{
OkCodes: []int{200},
})
return res
}
// Metadata requests all the metadata for the given server ID.
func Metadata(client *gophercloud.ServiceClient, id string) GetMetadataResult {
var res GetMetadataResult
_, res.Err = client.Get(metadataURL(client, id), &res.Body, nil)
return res
}
// UpdateMetadataOptsBuilder allows extensions to add additional parameters to the
// Create request.
type UpdateMetadataOptsBuilder interface {
ToMetadataUpdateMap() (map[string]interface{}, error)
}
// UpdateMetadata updates (or creates) all the metadata specified by opts for the given server ID.
// This operation does not affect already-existing metadata that is not specified
// by opts.
func UpdateMetadata(client *gophercloud.ServiceClient, id string, opts UpdateMetadataOptsBuilder) UpdateMetadataResult {
var res UpdateMetadataResult
metadata, err := opts.ToMetadataUpdateMap()
if err != nil {
res.Err = err
return res
}
_, res.Err = client.Post(metadataURL(client, id), metadata, &res.Body, &gophercloud.RequestOpts{
OkCodes: []int{200},
})
return res
}
// MetadatumOptsBuilder allows extensions to add additional parameters to the
// Create request.
type MetadatumOptsBuilder interface {
ToMetadatumCreateMap() (map[string]interface{}, string, error)
}
// MetadatumOpts is a map of length one that contains a key-value pair.
type MetadatumOpts map[string]string
// ToMetadatumCreateMap assembles a body for a Create request based on the contents of a MetadataumOpts.
func (opts MetadatumOpts) ToMetadatumCreateMap() (map[string]interface{}, string, error) {
if len(opts) != 1 {
return nil, "", errors.New("CreateMetadatum operation must have 1 and only 1 key-value pair.")
}
metadatum := map[string]interface{}{"meta": opts}
var key string
for k := range metadatum["meta"].(MetadatumOpts) {
key = k
}
return metadatum, key, nil
}
// CreateMetadatum will create or update the key-value pair with the given key for the given server ID.
func CreateMetadatum(client *gophercloud.ServiceClient, id string, opts MetadatumOptsBuilder) CreateMetadatumResult {
var res CreateMetadatumResult
metadatum, key, err := opts.ToMetadatumCreateMap()
if err != nil {
res.Err = err
return res
}
_, res.Err = client.Put(metadatumURL(client, id, key), metadatum, &res.Body, &gophercloud.RequestOpts{
OkCodes: []int{200},
})
return res
}
// Metadatum requests the key-value pair with the given key for the given server ID.
func Metadatum(client *gophercloud.ServiceClient, id, key string) GetMetadatumResult {
var res GetMetadatumResult
_, res.Err = client.Request("GET", metadatumURL(client, id, key), gophercloud.RequestOpts{
JSONResponse: &res.Body,
})
return res
}
// DeleteMetadatum will delete the key-value pair with the given key for the given server ID.
func DeleteMetadatum(client *gophercloud.ServiceClient, id, key string) DeleteMetadatumResult {
var res DeleteMetadatumResult
_, res.Err = client.Delete(metadatumURL(client, id, key), nil)
return res
}
// ListAddresses makes a request against the API to list the servers IP addresses.
func ListAddresses(client *gophercloud.ServiceClient, id string) pagination.Pager {
createPageFn := func(r pagination.PageResult) pagination.Page {
return AddressPage{pagination.SinglePageBase(r)}
}
return pagination.NewPager(client, listAddressesURL(client, id), createPageFn)
}
// ListAddressesByNetwork makes a request against the API to list the servers IP addresses
// for the given network.
func ListAddressesByNetwork(client *gophercloud.ServiceClient, id, network string) pagination.Pager {
createPageFn := func(r pagination.PageResult) pagination.Page {
return NetworkAddressPage{pagination.SinglePageBase(r)}
}
return pagination.NewPager(client, listAddressesByNetworkURL(client, id, network), createPageFn)
}
type CreateImageOpts struct {
// Name [required] of the image/snapshot
Name string
// Metadata [optional] contains key-value pairs (up to 255 bytes each) to attach to the created image.
Metadata map[string]string
}
type CreateImageOptsBuilder interface {
ToServerCreateImageMap() (map[string]interface{}, error)
}
// ToServerCreateImageMap formats a CreateImageOpts structure into a request body.
func (opts CreateImageOpts) ToServerCreateImageMap() (map[string]interface{}, error) {
var err error
img := make(map[string]interface{})
if opts.Name == "" {
return nil, fmt.Errorf("Cannot create a server image without a name")
}
img["name"] = opts.Name
if opts.Metadata != nil {
img["metadata"] = opts.Metadata
}
createImage := make(map[string]interface{})
createImage["createImage"] = img
return createImage, err
}
// CreateImage makes a request against the nova API to schedule an image to be created of the server
func CreateImage(client *gophercloud.ServiceClient, serverId string, opts CreateImageOptsBuilder) CreateImageResult {
var res CreateImageResult
reqBody, err := opts.ToServerCreateImageMap()
if err != nil {
res.Err = err
return res
}
response, err := client.Post(actionURL(client, serverId), reqBody, nil, &gophercloud.RequestOpts{
OkCodes: []int{202},
})
res.Err = err
res.Header = response.Header
return res
}
// IDFromName is a convienience function that returns a server's ID given its name.
func IDFromName(client *gophercloud.ServiceClient, name string) (string, error) {
serverCount := 0
serverID := ""
if name == "" {
return "", fmt.Errorf("A server name must be provided.")
}
pager := List(client, nil)
pager.EachPage(func(page pagination.Page) (bool, error) {
serverList, err := ExtractServers(page)
if err != nil {
return false, err
}
for _, s := range serverList {
if s.Name == name {
serverCount++
serverID = s.ID
}
}
return true, nil
})
switch serverCount {
case 0:
return "", fmt.Errorf("Unable to find server: %s", name)
case 1:
return serverID, nil
default:
return "", fmt.Errorf("Found %d servers matching %s", serverCount, name)
}
}
// 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,415 +0,0 @@
package servers
import (
"crypto/rsa"
"encoding/base64"
"fmt"
"net/url"
"path"
"reflect"
"github.com/mitchellh/mapstructure"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
)
type serverResult struct {
gophercloud.Result
}
// Extract interprets any serverResult as a Server, if possible.
func (r serverResult) Extract() (*Server, error) {
if r.Err != nil {
return nil, r.Err
}
var response struct {
Server Server `mapstructure:"server"`
}
config := &mapstructure.DecoderConfig{
DecodeHook: toMapFromString,
Result: &response,
}
decoder, err := mapstructure.NewDecoder(config)
if err != nil {
return nil, err
}
err = decoder.Decode(r.Body)
if err != nil {
return nil, err
}
return &response.Server, nil
}
// CreateResult temporarily contains the response from a Create call.
type CreateResult struct {
serverResult
}
// GetResult temporarily contains the response from a Get call.
type GetResult struct {
serverResult
}
// UpdateResult temporarily contains the response from an Update call.
type UpdateResult struct {
serverResult
}
// DeleteResult temporarily contains the response from a Delete call.
type DeleteResult struct {
gophercloud.ErrResult
}
// RebuildResult temporarily contains the response from a Rebuild call.
type RebuildResult struct {
serverResult
}
// ActionResult represents the result of server action operations, like reboot
type ActionResult struct {
gophercloud.ErrResult
}
// RescueResult represents the result of a server rescue operation
type RescueResult struct {
ActionResult
}
// CreateImageResult represents the result of an image creation operation
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 {
return "", res.Err
}
// Get the image id from the header
u, err := url.ParseRequestURI(res.Header.Get("Location"))
if err != nil {
return "", fmt.Errorf("Failed to parse the image id: %s", err.Error())
}
imageId := path.Base(u.Path)
if imageId == "." || imageId == "/" {
return "", fmt.Errorf("Failed to parse the ID of newly created image: %s", u)
}
return imageId, nil
}
// Extract interprets any RescueResult as an AdminPass, if possible.
func (r RescueResult) Extract() (string, error) {
if r.Err != nil {
return "", r.Err
}
var response struct {
AdminPass string `mapstructure:"adminPass"`
}
err := mapstructure.Decode(r.Body, &response)
return response.AdminPass, err
}
// Server exposes only the standard OpenStack fields corresponding to a given server on the user's account.
type Server struct {
// ID uniquely identifies this server amongst all other servers, including those not accessible to the current tenant.
ID string
// TenantID identifies the tenant owning this server resource.
TenantID string `mapstructure:"tenant_id"`
// UserID uniquely identifies the user account owning the tenant.
UserID string `mapstructure:"user_id"`
// Name contains the human-readable name for the server.
Name string
// Updated and Created contain ISO-8601 timestamps of when the state of the server last changed, and when it was created.
Updated string
Created string
HostID string
// Status contains the current operational status of the server, such as IN_PROGRESS or ACTIVE.
Status string
// Progress ranges from 0..100.
// A request made against the server completes only once Progress reaches 100.
Progress int
// AccessIPv4 and AccessIPv6 contain the IP addresses of the server, suitable for remote access for administration.
AccessIPv4, AccessIPv6 string
// Image refers to a JSON object, which itself indicates the OS image used to deploy the server.
Image map[string]interface{}
// Flavor refers to a JSON object, which itself indicates the hardware configuration of the deployed server.
Flavor map[string]interface{}
// Addresses includes a list of all IP addresses assigned to the server, keyed by pool.
Addresses map[string]interface{}
// Metadata includes a list of all user-specified key-value pairs attached to the server.
Metadata map[string]interface{}
// Links includes HTTP references to the itself, useful for passing along to other APIs that might want a server reference.
Links []interface{}
// KeyName indicates which public key was injected into the server on launch.
KeyName string `json:"key_name" mapstructure:"key_name"`
// AdminPass will generally be empty (""). However, it will contain the administrative password chosen when provisioning a new server without a set AdminPass setting in the first place.
// Note that this is the ONLY time this field will be valid.
AdminPass string `json:"adminPass" mapstructure:"adminPass"`
// SecurityGroups includes the security groups that this instance has applied to it
SecurityGroups []map[string]interface{} `json:"security_groups" mapstructure:"security_groups"`
}
// ServerPage abstracts the raw results of making a List() request against the API.
// As OpenStack extensions may freely alter the response bodies of structures returned to the client, you may only safely access the
// data provided through the ExtractServers call.
type ServerPage struct {
pagination.LinkedPageBase
}
// IsEmpty returns true if a page contains no Server results.
func (page ServerPage) IsEmpty() (bool, error) {
servers, err := ExtractServers(page)
if err != nil {
return true, err
}
return len(servers) == 0, nil
}
// NextPageURL uses the response's embedded link reference to navigate to the next page of results.
func (page ServerPage) NextPageURL() (string, error) {
type resp struct {
Links []gophercloud.Link `mapstructure:"servers_links"`
}
var r resp
err := mapstructure.Decode(page.Body, &r)
if err != nil {
return "", err
}
return gophercloud.ExtractNextURL(r.Links)
}
// ExtractServers interprets the results of a single page from a List() call, producing a slice of Server entities.
func ExtractServers(page pagination.Page) ([]Server, error) {
casted := page.(ServerPage).Body
var response struct {
Servers []Server `mapstructure:"servers"`
}
config := &mapstructure.DecoderConfig{
DecodeHook: toMapFromString,
Result: &response,
}
decoder, err := mapstructure.NewDecoder(config)
if err != nil {
return nil, err
}
err = decoder.Decode(casted)
return response.Servers, err
}
// MetadataResult contains the result of a call for (potentially) multiple key-value pairs.
type MetadataResult struct {
gophercloud.Result
}
// GetMetadataResult temporarily contains the response from a metadata Get call.
type GetMetadataResult struct {
MetadataResult
}
// ResetMetadataResult temporarily contains the response from a metadata Reset call.
type ResetMetadataResult struct {
MetadataResult
}
// UpdateMetadataResult temporarily contains the response from a metadata Update call.
type UpdateMetadataResult struct {
MetadataResult
}
// MetadatumResult contains the result of a call for individual a single key-value pair.
type MetadatumResult struct {
gophercloud.Result
}
// GetMetadatumResult temporarily contains the response from a metadatum Get call.
type GetMetadatumResult struct {
MetadatumResult
}
// CreateMetadatumResult temporarily contains the response from a metadatum Create call.
type CreateMetadatumResult struct {
MetadatumResult
}
// DeleteMetadatumResult temporarily contains the response from a metadatum Delete call.
type DeleteMetadatumResult struct {
gophercloud.ErrResult
}
// Extract interprets any MetadataResult as a Metadata, if possible.
func (r MetadataResult) Extract() (map[string]string, error) {
if r.Err != nil {
return nil, r.Err
}
var response struct {
Metadata map[string]string `mapstructure:"metadata"`
}
err := mapstructure.Decode(r.Body, &response)
return response.Metadata, err
}
// Extract interprets any MetadatumResult as a Metadatum, if possible.
func (r MetadatumResult) Extract() (map[string]string, error) {
if r.Err != nil {
return nil, r.Err
}
var response struct {
Metadatum map[string]string `mapstructure:"meta"`
}
err := mapstructure.Decode(r.Body, &response)
return response.Metadatum, err
}
func toMapFromString(from reflect.Kind, to reflect.Kind, data interface{}) (interface{}, error) {
if (from == reflect.String) && (to == reflect.Map) {
return map[string]interface{}{}, nil
}
return data, nil
}
// Address represents an IP address.
type Address struct {
Version int `mapstructure:"version"`
Address string `mapstructure:"addr"`
}
// AddressPage abstracts the raw results of making a ListAddresses() request against the API.
// As OpenStack extensions may freely alter the response bodies of structures returned
// to the client, you may only safely access the data provided through the ExtractAddresses call.
type AddressPage struct {
pagination.SinglePageBase
}
// IsEmpty returns true if an AddressPage contains no networks.
func (r AddressPage) IsEmpty() (bool, error) {
addresses, err := ExtractAddresses(r)
if err != nil {
return true, err
}
return len(addresses) == 0, nil
}
// ExtractAddresses interprets the results of a single page from a ListAddresses() call,
// producing a map of addresses.
func ExtractAddresses(page pagination.Page) (map[string][]Address, error) {
casted := page.(AddressPage).Body
var response struct {
Addresses map[string][]Address `mapstructure:"addresses"`
}
err := mapstructure.Decode(casted, &response)
if err != nil {
return nil, err
}
return response.Addresses, err
}
// NetworkAddressPage abstracts the raw results of making a ListAddressesByNetwork() request against the API.
// As OpenStack extensions may freely alter the response bodies of structures returned
// to the client, you may only safely access the data provided through the ExtractAddresses call.
type NetworkAddressPage struct {
pagination.SinglePageBase
}
// IsEmpty returns true if a NetworkAddressPage contains no addresses.
func (r NetworkAddressPage) IsEmpty() (bool, error) {
addresses, err := ExtractNetworkAddresses(r)
if err != nil {
return true, err
}
return len(addresses) == 0, nil
}
// ExtractNetworkAddresses interprets the results of a single page from a ListAddressesByNetwork() call,
// producing a slice of addresses.
func ExtractNetworkAddresses(page pagination.Page) ([]Address, error) {
casted := page.(NetworkAddressPage).Body
var response map[string][]Address
err := mapstructure.Decode(casted, &response)
if err != nil {
return nil, err
}
var key string
for k := range response {
key = k
}
return response[key], err
}

View File

@ -1,51 +0,0 @@
package servers
import "github.com/rackspace/gophercloud"
func createURL(client *gophercloud.ServiceClient) string {
return client.ServiceURL("servers")
}
func listURL(client *gophercloud.ServiceClient) string {
return createURL(client)
}
func listDetailURL(client *gophercloud.ServiceClient) string {
return client.ServiceURL("servers", "detail")
}
func deleteURL(client *gophercloud.ServiceClient, id string) string {
return client.ServiceURL("servers", id)
}
func getURL(client *gophercloud.ServiceClient, id string) string {
return deleteURL(client, id)
}
func updateURL(client *gophercloud.ServiceClient, id string) string {
return deleteURL(client, id)
}
func actionURL(client *gophercloud.ServiceClient, id string) string {
return client.ServiceURL("servers", id, "action")
}
func metadatumURL(client *gophercloud.ServiceClient, id, key string) string {
return client.ServiceURL("servers", id, "metadata", key)
}
func metadataURL(client *gophercloud.ServiceClient, id string) string {
return client.ServiceURL("servers", id, "metadata")
}
func listAddressesURL(client *gophercloud.ServiceClient, id string) string {
return client.ServiceURL("servers", id, "ips")
}
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

@ -1,20 +0,0 @@
package servers
import "github.com/rackspace/gophercloud"
// WaitForStatus will continually poll a server until it successfully transitions to a specified
// status. It will do this for at most the number of seconds specified.
func WaitForStatus(c *gophercloud.ServiceClient, id, status string, secs int) error {
return gophercloud.WaitFor(secs, func() (bool, error) {
current, err := Get(c, id).Extract()
if err != nil {
return false, err
}
if current.Status == status {
return true, nil
}
return false, nil
})
}

View File

@ -1,91 +0,0 @@
package openstack
import (
"fmt"
"github.com/rackspace/gophercloud"
tokens2 "github.com/rackspace/gophercloud/openstack/identity/v2/tokens"
tokens3 "github.com/rackspace/gophercloud/openstack/identity/v3/tokens"
)
// V2EndpointURL discovers the endpoint URL for a specific service from a ServiceCatalog acquired
// during the v2 identity service. The specified EndpointOpts are used to identify a unique,
// unambiguous endpoint to return. It's an error both when multiple endpoints match the provided
// criteria and when none do. The minimum that can be specified is a Type, but you will also often
// need to specify a Name and/or a Region depending on what's available on your OpenStack
// deployment.
func V2EndpointURL(catalog *tokens2.ServiceCatalog, opts gophercloud.EndpointOpts) (string, error) {
// Extract Endpoints from the catalog entries that match the requested Type, Name if provided, and Region if provided.
var endpoints = make([]tokens2.Endpoint, 0, 1)
for _, entry := range catalog.Entries {
if (entry.Type == opts.Type) && (opts.Name == "" || entry.Name == opts.Name) {
for _, endpoint := range entry.Endpoints {
if opts.Region == "" || endpoint.Region == opts.Region {
endpoints = append(endpoints, endpoint)
}
}
}
}
// Report an error if the options were ambiguous.
if len(endpoints) > 1 {
return "", fmt.Errorf("Discovered %d matching endpoints: %#v", len(endpoints), endpoints)
}
// Extract the appropriate URL from the matching Endpoint.
for _, endpoint := range endpoints {
switch opts.Availability {
case gophercloud.AvailabilityPublic:
return gophercloud.NormalizeURL(endpoint.PublicURL), nil
case gophercloud.AvailabilityInternal:
return gophercloud.NormalizeURL(endpoint.InternalURL), nil
case gophercloud.AvailabilityAdmin:
return gophercloud.NormalizeURL(endpoint.AdminURL), nil
default:
return "", fmt.Errorf("Unexpected availability in endpoint query: %s", opts.Availability)
}
}
// Report an error if there were no matching endpoints.
return "", gophercloud.ErrEndpointNotFound
}
// V3EndpointURL discovers the endpoint URL for a specific service from a Catalog acquired
// during the v3 identity service. The specified EndpointOpts are used to identify a unique,
// unambiguous endpoint to return. It's an error both when multiple endpoints match the provided
// criteria and when none do. The minimum that can be specified is a Type, but you will also often
// need to specify a Name and/or a Region depending on what's available on your OpenStack
// deployment.
func V3EndpointURL(catalog *tokens3.ServiceCatalog, opts gophercloud.EndpointOpts) (string, error) {
// Extract Endpoints from the catalog entries that match the requested Type, Interface,
// Name if provided, and Region if provided.
var endpoints = make([]tokens3.Endpoint, 0, 1)
for _, entry := range catalog.Entries {
if (entry.Type == opts.Type) && (opts.Name == "" || entry.Name == opts.Name) {
for _, endpoint := range entry.Endpoints {
if opts.Availability != gophercloud.AvailabilityAdmin &&
opts.Availability != gophercloud.AvailabilityPublic &&
opts.Availability != gophercloud.AvailabilityInternal {
return "", fmt.Errorf("Unexpected availability in endpoint query: %s", opts.Availability)
}
if (opts.Availability == gophercloud.Availability(endpoint.Interface)) &&
(opts.Region == "" || endpoint.Region == opts.Region) {
endpoints = append(endpoints, endpoint)
}
}
}
}
// Report an error if the options were ambiguous.
if len(endpoints) > 1 {
return "", fmt.Errorf("Discovered %d matching endpoints: %#v", len(endpoints), endpoints)
}
// Extract the URL from the matching Endpoint.
for _, endpoint := range endpoints {
return gophercloud.NormalizeURL(endpoint.URL), nil
}
// Report an error if there were no matching endpoints.
return "", gophercloud.ErrEndpointNotFound
}

View File

@ -1,7 +0,0 @@
// Package tenants provides information and interaction with the
// tenants API resource for the OpenStack Identity service.
//
// See http://developer.openstack.org/api-ref-identity-v2.html#identity-auth-v2
// and http://developer.openstack.org/api-ref-identity-v2.html#admin-tenants
// for more information.
package tenants

View File

@ -1,65 +0,0 @@
// +build fixtures
package tenants
import (
"fmt"
"net/http"
"testing"
th "github.com/rackspace/gophercloud/testhelper"
"github.com/rackspace/gophercloud/testhelper/client"
)
// ListOutput provides a single page of Tenant results.
const ListOutput = `
{
"tenants": [
{
"id": "1234",
"name": "Red Team",
"description": "The team that is red",
"enabled": true
},
{
"id": "9876",
"name": "Blue Team",
"description": "The team that is blue",
"enabled": false
}
]
}
`
// RedTeam is a Tenant fixture.
var RedTeam = Tenant{
ID: "1234",
Name: "Red Team",
Description: "The team that is red",
Enabled: true,
}
// BlueTeam is a Tenant fixture.
var BlueTeam = Tenant{
ID: "9876",
Name: "Blue Team",
Description: "The team that is blue",
Enabled: false,
}
// ExpectedTenantSlice is the slice of tenants expected to be returned from ListOutput.
var ExpectedTenantSlice = []Tenant{RedTeam, BlueTeam}
// HandleListTenantsSuccessfully creates an HTTP handler at `/tenants` on the test handler mux that
// responds with a list of two tenants.
func HandleListTenantsSuccessfully(t *testing.T) {
th.Mux.HandleFunc("/tenants", 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", client.TokenID)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, ListOutput)
})
}

View File

@ -1,33 +0,0 @@
package tenants
import (
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
)
// ListOpts filters the Tenants that are returned by the List call.
type ListOpts struct {
// Marker is the ID of the last Tenant on the previous page.
Marker string `q:"marker"`
// Limit specifies the page size.
Limit int `q:"limit"`
}
// List enumerates the Tenants to which the current token has access.
func List(client *gophercloud.ServiceClient, opts *ListOpts) pagination.Pager {
createPage := func(r pagination.PageResult) pagination.Page {
return TenantPage{pagination.LinkedPageBase{PageResult: r}}
}
url := listURL(client)
if opts != nil {
q, err := gophercloud.BuildQueryString(opts)
if err != nil {
return pagination.Pager{Err: err}
}
url += q.String()
}
return pagination.NewPager(client, url, createPage)
}

View File

@ -1,62 +0,0 @@
package tenants
import (
"github.com/mitchellh/mapstructure"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
)
// Tenant is a grouping of users in the identity service.
type Tenant struct {
// ID is a unique identifier for this tenant.
ID string `mapstructure:"id"`
// Name is a friendlier user-facing name for this tenant.
Name string `mapstructure:"name"`
// Description is a human-readable explanation of this Tenant's purpose.
Description string `mapstructure:"description"`
// Enabled indicates whether or not a tenant is active.
Enabled bool `mapstructure:"enabled"`
}
// TenantPage is a single page of Tenant results.
type TenantPage struct {
pagination.LinkedPageBase
}
// IsEmpty determines whether or not a page of Tenants contains any results.
func (page TenantPage) IsEmpty() (bool, error) {
tenants, err := ExtractTenants(page)
if err != nil {
return false, err
}
return len(tenants) == 0, nil
}
// NextPageURL extracts the "next" link from the tenants_links section of the result.
func (page TenantPage) NextPageURL() (string, error) {
type resp struct {
Links []gophercloud.Link `mapstructure:"tenants_links"`
}
var r resp
err := mapstructure.Decode(page.Body, &r)
if err != nil {
return "", err
}
return gophercloud.ExtractNextURL(r.Links)
}
// ExtractTenants returns a slice of Tenants contained in a single page of results.
func ExtractTenants(page pagination.Page) ([]Tenant, error) {
casted := page.(TenantPage).Body
var response struct {
Tenants []Tenant `mapstructure:"tenants"`
}
err := mapstructure.Decode(casted, &response)
return response.Tenants, err
}

View File

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

View File

@ -1,5 +0,0 @@
// Package tokens provides information and interaction with the token API
// resource for the OpenStack Identity service.
// For more information, see:
// http://developer.openstack.org/api-ref-identity-v2.html#identity-auth-v2
package tokens

View File

@ -1,30 +0,0 @@
package tokens
import (
"errors"
"fmt"
)
var (
// ErrUserIDProvided is returned if you attempt to authenticate with a UserID.
ErrUserIDProvided = unacceptedAttributeErr("UserID")
// ErrAPIKeyProvided is returned if you attempt to authenticate with an APIKey.
ErrAPIKeyProvided = unacceptedAttributeErr("APIKey")
// ErrDomainIDProvided is returned if you attempt to authenticate with a DomainID.
ErrDomainIDProvided = unacceptedAttributeErr("DomainID")
// ErrDomainNameProvided is returned if you attempt to authenticate with a DomainName.
ErrDomainNameProvided = unacceptedAttributeErr("DomainName")
// ErrUsernameRequired is returned if you attempt to authenticate without a Username.
ErrUsernameRequired = errors.New("You must supply a Username in your AuthOptions.")
// ErrPasswordRequired is returned if you don't provide a password.
ErrPasswordRequired = errors.New("Please supply a Password in your AuthOptions.")
)
func unacceptedAttributeErr(attribute string) error {
return fmt.Errorf("The base Identity V2 API does not accept authentication by %s", attribute)
}

View File

@ -1,195 +0,0 @@
// +build fixtures
package tokens
import (
"fmt"
"net/http"
"testing"
"time"
"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.
var ExpectedToken = &Token{
ID: "aaaabbbbccccdddd",
ExpiresAt: time.Date(2014, time.January, 31, 15, 30, 58, 0, time.UTC),
Tenant: tenants.Tenant{
ID: "fc394f2ab2df4114bde39905f800dc57",
Name: "test",
Description: "There are many tenants. This one is yours.",
Enabled: true,
},
}
// ExpectedServiceCatalog is the service catalog that should be parsed from TokenCreationResponse.
var ExpectedServiceCatalog = &ServiceCatalog{
Entries: []CatalogEntry{
CatalogEntry{
Name: "inscrutablewalrus",
Type: "something",
Endpoints: []Endpoint{
Endpoint{
PublicURL: "http://something0:1234/v2/",
Region: "region0",
},
Endpoint{
PublicURL: "http://something1:1234/v2/",
Region: "region1",
},
},
},
CatalogEntry{
Name: "arbitrarypenguin",
Type: "else",
Endpoints: []Endpoint{
Endpoint{
PublicURL: "http://else0:4321/v3/",
Region: "region0",
},
},
},
},
}
// 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 = `
{
"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": [
{
"endpoints": [
{
"publicURL": "http://something0:1234/v2/",
"region": "region0"
},
{
"publicURL": "http://something1:1234/v2/",
"region": "region1"
}
],
"type": "something",
"name": "inscrutablewalrus"
},
{
"endpoints": [
{
"publicURL": "http://else0:4321/v3/",
"region": "region0"
}
],
"type": "else",
"name": "arbitrarypenguin"
}
]
}
}
`
// 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) {
th.Mux.HandleFunc("/tokens", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "POST")
th.TestHeader(t, r, "Content-Type", "application/json")
th.TestHeader(t, r, "Accept", "application/json")
if requestJSON != "" {
th.TestJSONRequest(t, r, requestJSON)
}
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, TokenCreationResponse)
})
}
// 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) {
token, err := result.ExtractToken()
th.AssertNoErr(t, err)
th.CheckDeepEquals(t, ExpectedToken, token)
serviceCatalog, err := result.ExtractServiceCatalog()
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

@ -1,99 +0,0 @@
package tokens
import (
"fmt"
"github.com/rackspace/gophercloud"
)
// AuthOptionsBuilder describes any argument that may be passed to the Create call.
type AuthOptionsBuilder interface {
// ToTokenCreateMap assembles the Create request body, returning an error if parameters are
// missing or inconsistent.
ToTokenCreateMap() (map[string]interface{}, error)
}
// AuthOptions wraps a gophercloud AuthOptions in order to adhere to the AuthOptionsBuilder
// interface.
type AuthOptions struct {
gophercloud.AuthOptions
}
// WrapOptions embeds a root AuthOptions struct in a package-specific one.
func WrapOptions(original gophercloud.AuthOptions) AuthOptions {
return AuthOptions{AuthOptions: original}
}
// ToTokenCreateMap converts AuthOptions into nested maps that can be serialized into a JSON
// request.
func (auth AuthOptions) ToTokenCreateMap() (map[string]interface{}, error) {
// Error out if an unsupported auth option is present.
if auth.UserID != "" {
return nil, ErrUserIDProvided
}
if auth.APIKey != "" {
return nil, ErrAPIKeyProvided
}
if auth.DomainID != "" {
return nil, ErrDomainIDProvided
}
if auth.DomainName != "" {
return nil, ErrDomainNameProvided
}
// Populate the request map.
authMap := make(map[string]interface{})
if auth.Username != "" {
if auth.Password != "" {
authMap["passwordCredentials"] = map[string]interface{}{
"username": auth.Username,
"password": auth.Password,
}
} else {
return nil, ErrPasswordRequired
}
} else if auth.TokenID != "" {
authMap["token"] = map[string]interface{}{
"id": auth.TokenID,
}
} else {
return nil, fmt.Errorf("You must provide either username/password or tenantID/token values.")
}
if auth.TenantID != "" {
authMap["tenantId"] = auth.TenantID
}
if auth.TenantName != "" {
authMap["tenantName"] = auth.TenantName
}
return map[string]interface{}{"auth": authMap}, nil
}
// Create authenticates to the identity service and attempts to acquire a Token.
// If successful, the CreateResult
// Generally, rather than interact with this call directly, end users should call openstack.AuthenticatedClient(),
// which abstracts all of the gory details about navigating service catalogs and such.
func Create(client *gophercloud.ServiceClient, auth AuthOptionsBuilder) CreateResult {
request, err := auth.ToTokenCreateMap()
if err != nil {
return CreateResult{gophercloud.Result{Err: err}}
}
var result CreateResult
_, result.Err = client.Post(CreateURL(client), request, &result.Body, &gophercloud.RequestOpts{
OkCodes: []int{200, 203},
})
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

@ -1,170 +0,0 @@
package tokens
import (
"time"
"github.com/mitchellh/mapstructure"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/openstack/identity/v2/tenants"
)
// Token provides only the most basic information related to an authentication token.
type Token struct {
// ID provides the primary means of identifying a user to the OpenStack API.
// OpenStack defines this field as an opaque value, so do not depend on its content.
// It is safe, however, to compare for equality.
ID string
// ExpiresAt provides a timestamp in ISO 8601 format, indicating when the authentication token becomes invalid.
// After this point in time, future API requests made using this authentication token will respond with errors.
// Either the caller will need to reauthenticate manually, or more preferably, the caller should exploit automatic re-authentication.
// See the AuthOptions structure for more details.
ExpiresAt time.Time
// Tenant provides information about the tenant to which this token grants access.
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.
//
// In addition, the interface offered by the service will have version information associated with it
// through the VersionId, VersionInfo, and VersionList fields, if provided or supported.
//
// In all cases, fields which aren't supported by the provider and service combined will assume a zero-value ("").
type Endpoint struct {
TenantID string `mapstructure:"tenantId"`
PublicURL string `mapstructure:"publicURL"`
InternalURL string `mapstructure:"internalURL"`
AdminURL string `mapstructure:"adminURL"`
Region string `mapstructure:"region"`
VersionID string `mapstructure:"versionId"`
VersionInfo string `mapstructure:"versionInfo"`
VersionList string `mapstructure:"versionList"`
}
// CatalogEntry provides a type-safe interface to an Identity API V2 service catalog listing.
// Each class of service, such as cloud DNS or block storage services, will have a single
// CatalogEntry representing it.
//
// Note: when looking for the desired service, try, whenever possible, to key off the type field.
// Otherwise, you'll tie the representation of the service to a specific provider.
type CatalogEntry struct {
// Name will contain the provider-specified name for the service.
Name string `mapstructure:"name"`
// Type will contain a type string if OpenStack defines a type for the service.
// Otherwise, for provider-specific services, the provider may assign their own type strings.
Type string `mapstructure:"type"`
// Endpoints will let the caller iterate over all the different endpoints that may exist for
// the service.
Endpoints []Endpoint `mapstructure:"endpoints"`
}
// ServiceCatalog provides a view into the service catalog from a previous, successful authentication.
type ServiceCatalog struct {
Entries []CatalogEntry
}
// CreateResult defers the interpretation of a created token.
// Use ExtractToken() to interpret it as a Token, or ExtractServiceCatalog() to interpret it as a service catalog.
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 {
return nil, result.Err
}
var response struct {
Access struct {
Token struct {
Expires string `mapstructure:"expires"`
ID string `mapstructure:"id"`
Tenant tenants.Tenant `mapstructure:"tenant"`
} `mapstructure:"token"`
} `mapstructure:"access"`
}
err := mapstructure.Decode(result.Body, &response)
if err != nil {
return nil, err
}
expiresTs, err := time.Parse(gophercloud.RFC3339Milli, response.Access.Token.Expires)
if err != nil {
return nil, err
}
return &Token{
ID: response.Access.Token.ID,
ExpiresAt: expiresTs,
Tenant: response.Access.Token.Tenant,
}, nil
}
// ExtractServiceCatalog returns the ServiceCatalog that was generated along with the user's Token.
func (result CreateResult) ExtractServiceCatalog() (*ServiceCatalog, error) {
if result.Err != nil {
return nil, result.Err
}
var response struct {
Access struct {
Entries []CatalogEntry `mapstructure:"serviceCatalog"`
} `mapstructure:"access"`
}
err := mapstructure.Decode(result.Body, &response)
if err != nil {
return nil, err
}
return &ServiceCatalog{Entries: response.Access.Entries}, nil
}
// createErr quickly packs an error in a CreateResult.
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

@ -1,13 +0,0 @@
package tokens
import "github.com/rackspace/gophercloud"
// CreateURL generates the URL used to create new Tokens.
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

@ -1,6 +0,0 @@
// Package tokens provides information and interaction with the token API
// resource for the OpenStack Identity service.
//
// For more information, see:
// http://developer.openstack.org/api-ref-identity-v3.html#tokens-v3
package tokens

View File

@ -1,72 +0,0 @@
package tokens
import (
"errors"
"fmt"
)
func unacceptedAttributeErr(attribute string) error {
return fmt.Errorf("The base Identity V3 API does not accept authentication by %s", attribute)
}
func redundantWithTokenErr(attribute string) error {
return fmt.Errorf("%s may not be provided when authenticating with a TokenID", attribute)
}
func redundantWithUserID(attribute string) error {
return fmt.Errorf("%s may not be provided when authenticating with a UserID", attribute)
}
var (
// ErrAPIKeyProvided indicates that an APIKey was provided but can't be used.
ErrAPIKeyProvided = unacceptedAttributeErr("APIKey")
// ErrTenantIDProvided indicates that a TenantID was provided but can't be used.
ErrTenantIDProvided = unacceptedAttributeErr("TenantID")
// ErrTenantNameProvided indicates that a TenantName was provided but can't be used.
ErrTenantNameProvided = unacceptedAttributeErr("TenantName")
// ErrUsernameWithToken indicates that a Username was provided, but token authentication is being used instead.
ErrUsernameWithToken = redundantWithTokenErr("Username")
// ErrUserIDWithToken indicates that a UserID was provided, but token authentication is being used instead.
ErrUserIDWithToken = redundantWithTokenErr("UserID")
// ErrDomainIDWithToken indicates that a DomainID was provided, but token authentication is being used instead.
ErrDomainIDWithToken = redundantWithTokenErr("DomainID")
// ErrDomainNameWithToken indicates that a DomainName was provided, but token authentication is being used instead.s
ErrDomainNameWithToken = redundantWithTokenErr("DomainName")
// ErrUsernameOrUserID indicates that neither username nor userID are specified, or both are at once.
ErrUsernameOrUserID = errors.New("Exactly one of Username and UserID must be provided for password authentication")
// ErrDomainIDWithUserID indicates that a DomainID was provided, but unnecessary because a UserID is being used.
ErrDomainIDWithUserID = redundantWithUserID("DomainID")
// ErrDomainNameWithUserID indicates that a DomainName was provided, but unnecessary because a UserID is being used.
ErrDomainNameWithUserID = redundantWithUserID("DomainName")
// ErrDomainIDOrDomainName indicates that a username was provided, but no domain to scope it.
// It may also indicate that both a DomainID and a DomainName were provided at once.
ErrDomainIDOrDomainName = errors.New("You must provide exactly one of DomainID or DomainName to authenticate by Username")
// ErrMissingPassword indicates that no password and no token were provided and no token is available.
ErrMissingPassword = errors.New("You must provide a password or a token to authenticate")
// ErrScopeDomainIDOrDomainName indicates that a domain ID or Name was required in a Scope, but not present.
ErrScopeDomainIDOrDomainName = errors.New("You must provide exactly one of DomainID or DomainName in a Scope with ProjectName")
// ErrScopeProjectIDOrProjectName indicates that both a ProjectID and a ProjectName were provided in a Scope.
ErrScopeProjectIDOrProjectName = errors.New("You must provide at most one of ProjectID or ProjectName in a Scope")
// ErrScopeProjectIDAlone indicates that a ProjectID was provided with other constraints in a Scope.
ErrScopeProjectIDAlone = errors.New("ProjectID must be supplied alone in a Scope")
// ErrScopeDomainName indicates that a DomainName was provided alone in a Scope.
ErrScopeDomainName = errors.New("DomainName must be supplied with a ProjectName or ProjectID in a Scope.")
// ErrScopeEmpty indicates that no credentials were provided in a Scope.
ErrScopeEmpty = errors.New("You must provide either a Project or Domain in a Scope")
)

View File

@ -1,278 +0,0 @@
package tokens
import (
"net/http"
"github.com/rackspace/gophercloud"
)
// Scope allows a created token to be limited to a specific domain or project.
type Scope struct {
ProjectID string
ProjectName string
DomainID string
DomainName string
}
func subjectTokenHeaders(c *gophercloud.ServiceClient, subjectToken string) map[string]string {
return map[string]string{
"X-Subject-Token": subjectToken,
}
}
// Create authenticates and either generates a new token, or changes the Scope of an existing token.
func Create(c *gophercloud.ServiceClient, options gophercloud.AuthOptions, scope *Scope) CreateResult {
type domainReq struct {
ID *string `json:"id,omitempty"`
Name *string `json:"name,omitempty"`
}
type projectReq struct {
Domain *domainReq `json:"domain,omitempty"`
Name *string `json:"name,omitempty"`
ID *string `json:"id,omitempty"`
}
type userReq struct {
ID *string `json:"id,omitempty"`
Name *string `json:"name,omitempty"`
Password string `json:"password"`
Domain *domainReq `json:"domain,omitempty"`
}
type passwordReq struct {
User userReq `json:"user"`
}
type tokenReq struct {
ID string `json:"id"`
}
type identityReq struct {
Methods []string `json:"methods"`
Password *passwordReq `json:"password,omitempty"`
Token *tokenReq `json:"token,omitempty"`
}
type scopeReq struct {
Domain *domainReq `json:"domain,omitempty"`
Project *projectReq `json:"project,omitempty"`
}
type authReq struct {
Identity identityReq `json:"identity"`
Scope *scopeReq `json:"scope,omitempty"`
}
type request struct {
Auth authReq `json:"auth"`
}
// Populate the request structure based on the provided arguments. Create and return an error
// if insufficient or incompatible information is present.
var req request
// Test first for unrecognized arguments.
if options.APIKey != "" {
return createErr(ErrAPIKeyProvided)
}
if options.TenantID != "" {
return createErr(ErrTenantIDProvided)
}
if options.TenantName != "" {
return createErr(ErrTenantNameProvided)
}
if options.Password == "" {
if options.TokenID != "" {
c.TokenID = options.TokenID
}
if c.TokenID != "" {
// Because we aren't using password authentication, it's an error to also provide any of the user-based authentication
// parameters.
if options.Username != "" {
return createErr(ErrUsernameWithToken)
}
if options.UserID != "" {
return createErr(ErrUserIDWithToken)
}
// Configure the request for Token authentication.
req.Auth.Identity.Methods = []string{"token"}
req.Auth.Identity.Token = &tokenReq{
ID: c.TokenID,
}
} else {
// If no password or token ID are available, authentication can't continue.
return createErr(ErrMissingPassword)
}
} else {
// Password authentication.
req.Auth.Identity.Methods = []string{"password"}
// At least one of Username and UserID must be specified.
if options.Username == "" && options.UserID == "" {
return createErr(ErrUsernameOrUserID)
}
if options.Username != "" {
// If Username is provided, UserID may not be provided.
if options.UserID != "" {
return createErr(ErrUsernameOrUserID)
}
// Either DomainID or DomainName must also be specified.
if options.DomainID == "" && options.DomainName == "" {
return createErr(ErrDomainIDOrDomainName)
}
if options.DomainID != "" {
if options.DomainName != "" {
return createErr(ErrDomainIDOrDomainName)
}
// Configure the request for Username and Password authentication with a DomainID.
req.Auth.Identity.Password = &passwordReq{
User: userReq{
Name: &options.Username,
Password: options.Password,
Domain: &domainReq{ID: &options.DomainID},
},
}
}
if options.DomainName != "" {
// Configure the request for Username and Password authentication with a DomainName.
req.Auth.Identity.Password = &passwordReq{
User: userReq{
Name: &options.Username,
Password: options.Password,
Domain: &domainReq{Name: &options.DomainName},
},
}
}
}
if options.UserID != "" {
// If UserID is specified, neither DomainID nor DomainName may be.
if options.DomainID != "" {
return createErr(ErrDomainIDWithUserID)
}
if options.DomainName != "" {
return createErr(ErrDomainNameWithUserID)
}
// Configure the request for UserID and Password authentication.
req.Auth.Identity.Password = &passwordReq{
User: userReq{ID: &options.UserID, Password: options.Password},
}
}
}
// Add a "scope" element if a Scope has been provided.
if scope != nil {
if scope.ProjectName != "" {
// ProjectName provided: either DomainID or DomainName must also be supplied.
// ProjectID may not be supplied.
if scope.DomainID == "" && scope.DomainName == "" {
return createErr(ErrScopeDomainIDOrDomainName)
}
if scope.ProjectID != "" {
return createErr(ErrScopeProjectIDOrProjectName)
}
if scope.DomainID != "" {
// ProjectName + DomainID
req.Auth.Scope = &scopeReq{
Project: &projectReq{
Name: &scope.ProjectName,
Domain: &domainReq{ID: &scope.DomainID},
},
}
}
if scope.DomainName != "" {
// ProjectName + DomainName
req.Auth.Scope = &scopeReq{
Project: &projectReq{
Name: &scope.ProjectName,
Domain: &domainReq{Name: &scope.DomainName},
},
}
}
} else if scope.ProjectID != "" {
// ProjectID provided. ProjectName, DomainID, and DomainName may not be provided.
if scope.DomainID != "" {
return createErr(ErrScopeProjectIDAlone)
}
if scope.DomainName != "" {
return createErr(ErrScopeProjectIDAlone)
}
// ProjectID
req.Auth.Scope = &scopeReq{
Project: &projectReq{ID: &scope.ProjectID},
}
} else if scope.DomainID != "" {
// DomainID provided. ProjectID, ProjectName, and DomainName may not be provided.
if scope.DomainName != "" {
return createErr(ErrScopeDomainIDOrDomainName)
}
// DomainID
req.Auth.Scope = &scopeReq{
Domain: &domainReq{ID: &scope.DomainID},
}
} else if scope.DomainName != "" {
return createErr(ErrScopeDomainName)
} else {
return createErr(ErrScopeEmpty)
}
}
var result CreateResult
var response *http.Response
response, result.Err = c.Post(tokenURL(c), req, &result.Body, nil)
if result.Err != nil {
return result
}
result.Header = response.Header
return result
}
// Get validates and retrieves information about another token.
func Get(c *gophercloud.ServiceClient, token string) GetResult {
var result GetResult
var response *http.Response
response, result.Err = c.Get(tokenURL(c), &result.Body, &gophercloud.RequestOpts{
MoreHeaders: subjectTokenHeaders(c, token),
OkCodes: []int{200, 203},
})
if result.Err != nil {
return result
}
result.Header = response.Header
return result
}
// Validate determines if a specified token is valid or not.
func Validate(c *gophercloud.ServiceClient, token string) (bool, error) {
response, err := c.Request("HEAD", tokenURL(c), gophercloud.RequestOpts{
MoreHeaders: subjectTokenHeaders(c, token),
OkCodes: []int{204, 404},
})
if err != nil {
return false, err
}
return response.StatusCode == 204, nil
}
// Revoke immediately makes specified token invalid.
func Revoke(c *gophercloud.ServiceClient, token string) RevokeResult {
var res RevokeResult
_, res.Err = c.Delete(tokenURL(c), &gophercloud.RequestOpts{
MoreHeaders: subjectTokenHeaders(c, token),
})
return res
}

View File

@ -1,139 +0,0 @@
package tokens
import (
"time"
"github.com/mitchellh/mapstructure"
"github.com/rackspace/gophercloud"
)
// Endpoint represents a single API endpoint offered by a service.
// It matches either a public, internal or admin URL.
// If supported, it contains a region specifier, again if provided.
// The significance of the Region field will depend upon your provider.
type Endpoint struct {
ID string `mapstructure:"id"`
Region string `mapstructure:"region"`
Interface string `mapstructure:"interface"`
URL string `mapstructure:"url"`
}
// CatalogEntry provides a type-safe interface to an Identity API V3 service catalog listing.
// Each class of service, such as cloud DNS or block storage services, could have multiple
// CatalogEntry representing it (one by interface type, e.g public, admin or internal).
//
// Note: when looking for the desired service, try, whenever possible, to key off the type field.
// Otherwise, you'll tie the representation of the service to a specific provider.
type CatalogEntry struct {
// Service ID
ID string `mapstructure:"id"`
// Name will contain the provider-specified name for the service.
Name string `mapstructure:"name"`
// Type will contain a type string if OpenStack defines a type for the service.
// Otherwise, for provider-specific services, the provider may assign their own type strings.
Type string `mapstructure:"type"`
// Endpoints will let the caller iterate over all the different endpoints that may exist for
// the service.
Endpoints []Endpoint `mapstructure:"endpoints"`
}
// ServiceCatalog provides a view into the service catalog from a previous, successful authentication.
type ServiceCatalog struct {
Entries []CatalogEntry
}
// commonResult is the deferred result of a Create or a Get call.
type commonResult struct {
gophercloud.Result
}
// Extract is a shortcut for ExtractToken.
// This function is deprecated and still present for backward compatibility.
func (r commonResult) Extract() (*Token, error) {
return r.ExtractToken()
}
// ExtractToken interprets a commonResult as a Token.
func (r commonResult) ExtractToken() (*Token, error) {
if r.Err != nil {
return nil, r.Err
}
var response struct {
Token struct {
ExpiresAt string `mapstructure:"expires_at"`
} `mapstructure:"token"`
}
var token Token
// Parse the token itself from the stored headers.
token.ID = r.Header.Get("X-Subject-Token")
err := mapstructure.Decode(r.Body, &response)
if err != nil {
return nil, err
}
// Attempt to parse the timestamp.
token.ExpiresAt, err = time.Parse(gophercloud.RFC3339Milli, response.Token.ExpiresAt)
return &token, err
}
// ExtractServiceCatalog returns the ServiceCatalog that was generated along with the user's Token.
func (result CreateResult) ExtractServiceCatalog() (*ServiceCatalog, error) {
if result.Err != nil {
return nil, result.Err
}
var response struct {
Token struct {
Entries []CatalogEntry `mapstructure:"catalog"`
} `mapstructure:"token"`
}
err := mapstructure.Decode(result.Body, &response)
if err != nil {
return nil, err
}
return &ServiceCatalog{Entries: response.Token.Entries}, nil
}
// CreateResult defers the interpretation of a created token.
// Use ExtractToken() to interpret it as a Token, or ExtractServiceCatalog() to interpret it as a service catalog.
type CreateResult struct {
commonResult
}
// createErr quickly creates a CreateResult that reports an error.
func createErr(err error) CreateResult {
return CreateResult{
commonResult: commonResult{Result: gophercloud.Result{Err: err}},
}
}
// GetResult is the deferred response from a Get call.
type GetResult struct {
commonResult
}
// RevokeResult is the deferred response from a Revoke call.
type RevokeResult struct {
commonResult
}
// Token is a string that grants a user access to a controlled set of services in an OpenStack provider.
// Each Token is valid for a set length of time.
type Token struct {
// ID is the issued token.
ID string
// ExpiresAt is the timestamp at which this token will no longer be accepted.
ExpiresAt time.Time
}

View File

@ -1,7 +0,0 @@
package tokens
import "github.com/rackspace/gophercloud"
func tokenURL(c *gophercloud.ServiceClient) string {
return c.ServiceURL("auth", "tokens")
}

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