Merge pull request #4283 from mitchellh/openstackhacks

builder/openstack: Add image visibility & members
This commit is contained in:
Matthew Hooker 2016-12-14 16:13:21 -08:00 committed by GitHub
commit 88841cb0e4
128 changed files with 4206 additions and 5148 deletions

View File

@ -6,9 +6,9 @@ import (
"net/http"
"os"
"github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/openstack"
"github.com/mitchellh/packer/template/interpolate"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/openstack"
)
// AccessConfig is for common configuration related to openstack access
@ -16,7 +16,6 @@ type AccessConfig struct {
Username string `mapstructure:"username"`
UserID string `mapstructure:"user_id"`
Password string `mapstructure:"password"`
APIKey string `mapstructure:"api_key"`
IdentityEndpoint string `mapstructure:"identity_endpoint"`
TenantID string `mapstructure:"tenant_id"`
TenantName string `mapstructure:"tenant_name"`
@ -42,9 +41,6 @@ func (c *AccessConfig) Prepare(ctx *interpolate.Context) []error {
}
// Legacy RackSpace stuff. We're keeping this around to keep things BC.
if c.APIKey == "" {
c.APIKey = os.Getenv("SDK_API_KEY")
}
if c.Password == "" {
c.Password = os.Getenv("SDK_PASSWORD")
}
@ -71,7 +67,6 @@ func (c *AccessConfig) Prepare(ctx *interpolate.Context) []error {
{&c.Username, &ao.Username},
{&c.UserID, &ao.UserID},
{&c.Password, &ao.Password},
{&c.APIKey, &ao.APIKey},
{&c.IdentityEndpoint, &ao.IdentityEndpoint},
{&c.TenantID, &ao.TenantID},
{&c.TenantName, &ao.TenantName},
@ -115,6 +110,13 @@ func (c *AccessConfig) computeV2Client() (*gophercloud.ServiceClient, error) {
})
}
func (c *AccessConfig) imageV2Client() (*gophercloud.ServiceClient, error) {
return openstack.NewImageServiceV2(c.osClient, gophercloud.EndpointOpts{
Region: c.Region,
Availability: c.getEndpointType(),
})
}
func (c *AccessConfig) getEndpointType() gophercloud.Availability {
if c.EndpointType == "internal" || c.EndpointType == "internalURL" {
return gophercloud.AvailabilityInternal

View File

@ -4,8 +4,8 @@ import (
"fmt"
"log"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/openstack/compute/v2/images"
"github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/openstack/compute/v2/images"
)
// Artifact is an artifact implementation that contains built images.

View File

@ -114,6 +114,8 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
&common.StepProvision{},
&StepStopServer{},
&stepCreateImage{},
&stepUpdateImageVisibility{},
&stepAddImageMembers{},
}
// Run!

View File

@ -2,14 +2,18 @@ package openstack
import (
"fmt"
"strings"
imageservice "github.com/gophercloud/gophercloud/openstack/imageservice/v2/images"
"github.com/mitchellh/packer/template/interpolate"
)
// ImageConfig is for common configuration related to creating Images.
type ImageConfig struct {
ImageName string `mapstructure:"image_name"`
ImageMetadata map[string]string `mapstructure:"metadata"`
ImageName string `mapstructure:"image_name"`
ImageMetadata map[string]string `mapstructure:"metadata"`
ImageVisibility imageservice.ImageVisibility `mapstructure:"image_visibility"`
ImageMembers []string `mapstructure:"image_members"`
}
func (c *ImageConfig) Prepare(ctx *interpolate.Context) []error {
@ -29,6 +33,23 @@ func (c *ImageConfig) Prepare(ctx *interpolate.Context) []error {
c.ImageMetadata["image_type"] = "image"
}
// ImageVisibility values
// https://wiki.openstack.org/wiki/Glance-v2-community-image-visibility-design
if c.ImageVisibility != "" {
validVals := []imageservice.ImageVisibility{"public", "private", "shared", "community"}
valid := false
for _, val := range validVals {
if strings.EqualFold(string(c.ImageVisibility), string(val)) {
valid = true
c.ImageVisibility = val
break
}
}
if !valid {
errs = append(errs, fmt.Errorf("Unknown visibility value %s", c.ImageVisibility))
}
}
if len(errs) > 0 {
return errs
}

View File

@ -6,9 +6,9 @@ import (
"log"
"time"
"github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/openstack/compute/v2/servers"
"github.com/mitchellh/multistep"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/openstack/compute/v2/servers"
)
// StateRefreshFunc is a function type used for StateChangeConf that is
@ -38,14 +38,12 @@ func ServerStateRefreshFunc(
return func() (interface{}, string, int, error) {
serverNew, err := servers.Get(client, s.ID).Extract()
if err != nil {
errCode, ok := err.(*gophercloud.UnexpectedResponseCodeError)
if ok && errCode.Actual == 404 {
if _, ok := err.(gophercloud.ErrDefault404); ok {
log.Printf("[INFO] 404 on ServerStateRefresh, returning DELETED")
return nil, "DELETED", 0, nil
} else {
log.Printf("[ERROR] Error on ServerStateRefresh: %s", err)
return nil, "", 0, err
}
log.Printf("[ERROR] Error on ServerStateRefresh: %s", err)
return nil, "", 0, err
}
return serverNew, serverNew.Status, serverNew.Progress, nil

View File

@ -6,11 +6,11 @@ import (
"log"
"time"
"github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/floatingips"
"github.com/gophercloud/gophercloud/openstack/compute/v2/servers"
"github.com/mitchellh/multistep"
packerssh "github.com/mitchellh/packer/communicator/ssh"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/openstack/compute/v2/extensions/floatingip"
"github.com/rackspace/gophercloud/openstack/compute/v2/servers"
"golang.org/x/crypto/ssh"
)
@ -31,7 +31,7 @@ func CommHost(
}
// If we have a floating IP, use that
ip := state.Get("access_ip").(*floatingip.FloatingIP)
ip := state.Get("access_ip").(*floatingips.FloatingIP)
if ip != nil && ip.IP != "" {
log.Printf("[DEBUG] Using floating IP %s to connect", ip.IP)
return ip.IP, nil

View File

@ -0,0 +1,44 @@
package openstack
import (
"fmt"
"github.com/gophercloud/gophercloud/openstack/imageservice/v2/members"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
)
type stepAddImageMembers struct{}
func (s *stepAddImageMembers) Run(state multistep.StateBag) multistep.StepAction {
imageId := state.Get("image").(string)
ui := state.Get("ui").(packer.Ui)
config := state.Get("config").(Config)
if len(config.ImageMembers) == 0 {
return multistep.ActionContinue
}
imageClient, err := config.imageV2Client()
if err != nil {
err = fmt.Errorf("Error initializing image service client: %s", err)
state.Put("error", err)
return multistep.ActionHalt
}
for _, member := range config.ImageMembers {
ui.Say(fmt.Sprintf("Adding member '%s' to image %s", member, imageId))
r := members.Create(imageClient, imageId, member)
if _, err = r.Extract(); err != nil {
err = fmt.Errorf("Error adding member to image: %s", err)
state.Put("error", err)
return multistep.ActionHalt
}
}
return multistep.ActionContinue
}
func (s *stepAddImageMembers) Cleanup(multistep.StateBag) {
// No cleanup...
}

View File

@ -3,10 +3,10 @@ package openstack
import (
"fmt"
"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/floatingips"
"github.com/gophercloud/gophercloud/openstack/compute/v2/servers"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"github.com/rackspace/gophercloud/openstack/compute/v2/extensions/floatingip"
"github.com/rackspace/gophercloud/openstack/compute/v2/servers"
)
type StepAllocateIp struct {
@ -27,7 +27,7 @@ func (s *StepAllocateIp) Run(state multistep.StateBag) multistep.StepAction {
return multistep.ActionHalt
}
var instanceIp floatingip.FloatingIP
var instanceIp floatingips.FloatingIP
// This is here in case we error out before putting instanceIp into the
// statebag below, because it is requested by Cleanup()
@ -38,7 +38,7 @@ func (s *StepAllocateIp) Run(state multistep.StateBag) multistep.StepAction {
} else if s.FloatingIpPool != "" {
ui.Say(fmt.Sprintf("Creating floating IP..."))
ui.Message(fmt.Sprintf("Pool: %s", s.FloatingIpPool))
newIp, err := floatingip.Create(client, floatingip.CreateOpts{
newIp, err := floatingips.Create(client, floatingips.CreateOpts{
Pool: s.FloatingIpPool,
}).Extract()
if err != nil {
@ -55,7 +55,9 @@ func (s *StepAllocateIp) Run(state multistep.StateBag) multistep.StepAction {
if instanceIp.IP != "" {
ui.Say(fmt.Sprintf("Associating floating IP with server..."))
ui.Message(fmt.Sprintf("IP: %s", instanceIp.IP))
err := floatingip.Associate(client, server.ID, instanceIp.IP).ExtractErr()
err := floatingips.AssociateInstance(client, server.ID, floatingips.AssociateOpts{
FloatingIP: instanceIp.IP,
}).ExtractErr()
if err != nil {
err := fmt.Errorf(
"Error associating floating IP %s with instance: %s",
@ -76,7 +78,7 @@ func (s *StepAllocateIp) Run(state multistep.StateBag) multistep.StepAction {
func (s *StepAllocateIp) Cleanup(state multistep.StateBag) {
config := state.Get("config").(Config)
ui := state.Get("ui").(packer.Ui)
instanceIp := state.Get("access_ip").(*floatingip.FloatingIP)
instanceIp := state.Get("access_ip").(*floatingips.FloatingIP)
// We need the v2 compute client
client, err := config.computeV2Client()
@ -87,7 +89,7 @@ func (s *StepAllocateIp) Cleanup(state multistep.StateBag) {
}
if s.FloatingIpPool != "" && instanceIp.ID != "" {
if err := floatingip.Delete(client, instanceIp.ID).ExtractErr(); err != nil {
if err := floatingips.Delete(client, instanceIp.ID).ExtractErr(); err != nil {
ui.Error(fmt.Sprintf(
"Error deleting temporary floating IP %s", instanceIp.IP))
return

View File

@ -5,11 +5,11 @@ import (
"log"
"time"
"github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/openstack/compute/v2/images"
"github.com/gophercloud/gophercloud/openstack/compute/v2/servers"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/openstack/compute/v2/images"
"github.com/rackspace/gophercloud/openstack/compute/v2/servers"
)
type stepCreateImage struct{}
@ -68,7 +68,7 @@ func WaitForImage(client *gophercloud.ServiceClient, imageId string) error {
for {
image, err := images.Get(client, imageId).Extract()
if err != nil {
errCode, ok := err.(*gophercloud.UnexpectedResponseCodeError)
errCode, ok := err.(*gophercloud.ErrUnexpectedResponseCode)
if ok && (errCode.Actual == 500 || errCode.Actual == 404) {
numErrors++
if numErrors >= maxNumErrors {

View File

@ -6,10 +6,10 @@ import (
"log"
"time"
"github.com/gophercloud/gophercloud/openstack/compute/v2/servers"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/helper/communicator"
"github.com/mitchellh/packer/packer"
"github.com/rackspace/gophercloud/openstack/compute/v2/servers"
"golang.org/x/crypto/ssh"
)

View File

@ -8,10 +8,10 @@ import (
"os/exec"
"runtime"
"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/keypairs"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/common/uuid"
"github.com/mitchellh/packer/packer"
"github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs"
"golang.org/x/crypto/ssh"
)

View File

@ -4,10 +4,10 @@ import (
"fmt"
"log"
"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions"
"github.com/gophercloud/gophercloud/pagination"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"github.com/rackspace/gophercloud/openstack/compute/v2/extensions"
"github.com/rackspace/gophercloud/pagination"
)
// StepLoadExtensions gets the FlavorRef from a Flavor. It first assumes

View File

@ -4,9 +4,9 @@ import (
"fmt"
"log"
"github.com/gophercloud/gophercloud/openstack/compute/v2/flavors"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"github.com/rackspace/gophercloud/openstack/compute/v2/flavors"
)
// StepLoadFlavor gets the FlavorRef from a Flavor. It first assumes

View File

@ -5,10 +5,10 @@ import (
"io/ioutil"
"log"
"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/keypairs"
"github.com/gophercloud/gophercloud/openstack/compute/v2/servers"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs"
"github.com/rackspace/gophercloud/openstack/compute/v2/servers"
)
type StepRunSourceServer struct {
@ -63,7 +63,7 @@ func (s *StepRunSourceServer) Run(state multistep.StateBag) multistep.StepAction
Networks: networks,
AvailabilityZone: s.AvailabilityZone,
UserData: userData,
ConfigDrive: s.ConfigDrive,
ConfigDrive: &s.ConfigDrive,
}
var serverOptsExt servers.CreateOptsBuilder

View File

@ -3,10 +3,10 @@ package openstack
import (
"fmt"
"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/startstop"
"github.com/gophercloud/gophercloud/openstack/compute/v2/servers"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"github.com/rackspace/gophercloud/openstack/compute/v2/extensions/startstop"
"github.com/rackspace/gophercloud/openstack/compute/v2/servers"
)
type StepStopServer struct{}

View File

@ -0,0 +1,50 @@
package openstack
import (
"fmt"
imageservice "github.com/gophercloud/gophercloud/openstack/imageservice/v2/images"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
)
type stepUpdateImageVisibility struct{}
func (s *stepUpdateImageVisibility) Run(state multistep.StateBag) multistep.StepAction {
imageId := state.Get("image").(string)
ui := state.Get("ui").(packer.Ui)
config := state.Get("config").(Config)
if config.ImageVisibility == "" {
return multistep.ActionContinue
}
imageClient, err := config.imageV2Client()
if err != nil {
err = fmt.Errorf("Error initializing image service client: %s", err)
state.Put("error", err)
return multistep.ActionHalt
}
ui.Say(fmt.Sprintf("Updating image visibility to %s", config.ImageVisibility))
r := imageservice.Update(
imageClient,
imageId,
imageservice.UpdateOpts{
imageservice.UpdateVisibility{
Visibility: config.ImageVisibility,
},
},
)
if _, err = r.Extract(); err != nil {
err = fmt.Errorf("Error updating image visibility: %s", err)
state.Put("error", err)
return multistep.ActionHalt
}
return multistep.ActionContinue
}
func (s *stepUpdateImageVisibility) Cleanup(multistep.StateBag) {
// No cleanup...
}

View File

@ -4,9 +4,9 @@ import (
"fmt"
"time"
"github.com/gophercloud/gophercloud/openstack/compute/v2/servers"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"github.com/rackspace/gophercloud/openstack/compute/v2/servers"
)
type StepWaitForRackConnect struct {

View File

32
vendor/github.com/gophercloud/gophercloud/MIGRATING.md generated vendored Normal file
View File

@ -0,0 +1,32 @@
# Compute
## Floating IPs
* `github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/floatingip` is now `github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/floatingips`
* `floatingips.Associate` and `floatingips.Disassociate` have been removed.
* `floatingips.DisassociateOpts` is now required to disassociate a Floating IP.
## Security Groups
* `secgroups.AddServerToGroup` is now `secgroups.AddServer`.
* `secgroups.RemoveServerFromGroup` is now `secgroups.RemoveServer`.
## Servers
* `servers.Reboot` now requires a `servers.RebootOpts` struct:
```golang
rebootOpts := &servers.RebootOpts{
Type: servers.SoftReboot,
}
res := servers.Reboot(client, server.ID, rebootOpts)
```
# Identity
## V3
### Tokens
* `Token.ExpiresAt` is now of type `gophercloud.JSONRFC3339Milli` instead of
`time.Time`

View File

@ -1,17 +1,12 @@
# Gophercloud: an OpenStack SDK for Go
[![Build Status](https://travis-ci.org/rackspace/gophercloud.svg?branch=master)](https://travis-ci.org/rackspace/gophercloud)
[![Build Status](https://travis-ci.org/gophercloud/gophercloud.svg?branch=master)](https://travis-ci.org/gophercloud/gophercloud)
[![Coverage Status](https://coveralls.io/repos/github/gophercloud/gophercloud/badge.svg?branch=master)](https://coveralls.io/github/gophercloud/gophercloud?branch=master)
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.
Gophercloud is an OpenStack Go SDK.
## Useful links
* [Gophercloud homepage](http://gophercloud.io)
* [Reference documentation](http://godoc.org/github.com/rackspace/gophercloud)
* [Getting started guides](http://gophercloud.io/docs)
* [Reference documentation](http://godoc.org/github.com/gophercloud/gophercloud)
* [Effective Go](https://golang.org/doc/effective_go.html)
## How to install
@ -30,9 +25,9 @@ your projects, such as [godep](https://github.com/tools/godep). Once this is set
Gophercloud as a dependency like so:
```bash
go get github.com/rackspace/gophercloud
go get github.com/gophercloud/gophercloud
# Edit your code to import relevant packages from "github.com/rackspace/gophercloud"
# Edit your code to import relevant packages from "github.com/gophercloud/gophercloud"
godep save ./...
```
@ -54,7 +49,6 @@ 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
@ -73,9 +67,9 @@ 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"
"github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/openstack"
"github.com/gophercloud/gophercloud/openstack/utils"
)
// Option 1: Pass in the values yourself
@ -83,7 +77,6 @@ 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
@ -119,7 +112,7 @@ 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"
import "github.com/gophercloud/gophercloud/openstack/compute/v2/servers"
server, err := servers.Create(client, servers.CreateOpts{
Name: "My new server!",
@ -128,33 +121,19 @@ server, err := servers.Create(client, servers.CreateOpts{
}).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).
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/gophercloud/gophercloud) struct).
### Next steps
## Backwards-Compatibility Guarantees
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)
None. Vendor it and write tests covering the parts you use.
## 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!
See the [contributing guide](./.github/CONTRIBUTING.md).
## 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/).
to submit an issue to our [bug tracker](/issues).

View File

@ -0,0 +1,68 @@
## On Pull Requests
- Before you start a PR there needs to be a Github issue and a discussion about it
on that issue with a core contributor, even if it's just a 'SGTM'.
- A PR's description must reference the issue it closes with a `For <ISSUE NUMBER>` (e.g. For #293).
- A PR's description must contain link(s) to the line(s) in the OpenStack
source code (on Github) that prove(s) the PR code to be valid. Links to documentation
are not good enough. The link(s) should be to a non-`master` branch. For example,
a pull request implementing the creation of a Neutron v2 subnet might put the
following link in the description:
https://github.com/openstack/neutron/blob/stable/mitaka/neutron/api/v2/attributes.py#L749
From that link, a reviewer (or user) can verify the fields in the request/response
objects in the PR.
- A PR that is in-progress should have `[wip]` in front of the PR's title. When
ready for review, remove the `[wip]` and ping a core contributor with an `@`.
- A PR should be small. Even if you intend on implementing an entire
service, a PR should only be one route of that service
(e.g. create server or get server, but not both).
- Unless explicitly asked, do not squash commits in the middle of a review; only
append. It makes it difficult for the reviewer to see what's changed from one
review to the next.
## On Code
- In re design: follow as closely as is reasonable the code already in the library.
Most operations (e.g. create, delete) admit the same design.
- Unit tests and acceptance (integration) tests must be written to cover each PR.
Tests for operations with several options (e.g. list, create) should include all
the options in the tests. This will allow users to verify an operation on their
own infrastructure and see an example of usage.
- If in doubt, ask in-line on the PR.
### File Structure
- The following should be used in most cases:
- `requests.go`: contains all the functions that make HTTP requests and the
types associated with the HTTP request (parameters for URL, body, etc)
- `results.go`: contains all the response objects and their methods
- `urls.go`: contains the endpoints to which the requests are made
### Naming
- For methods on a type in `response.go`, the receiver should be named `r` and the
variable into which it will be unmarshalled `s`.
- Functions in `requests.go`, with the exception of functions that return a
`pagination.Pager`, should be named returns of the name `r`.
- Functions in `requests.go` that accept request bodies should accept as their
last parameter an `interface` named `<Action>OptsBuilder` (eg `CreateOptsBuilder`).
This `interface` should have at the least a method named `To<Resource><Action>Map`
(eg `ToPortCreateMap`).
- Functions in `requests.go` that accept query strings should accept as their
last parameter an `interface` named `<Action>OptsBuilder` (eg `ListOptsBuilder`).
This `interface` should have at the least a method named `To<Resource><Action>Query`
(eg `ToServerListQuery`).

View File

@ -0,0 +1,331 @@
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 `json:"-"`
// 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 string `json:"username,omitempty"`
UserID string `json:"id,omitempty"`
Password string `json:"password,omitempty"`
// At most one of DomainID and DomainName must be provided if using Username
// with Identity V3. Otherwise, either are optional.
DomainID string `json:"id,omitempty"`
DomainName string `json:"name,omitempty"`
// 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 string `json:"tenantId,omitempty"`
TenantName string `json:"tenantName,omitempty"`
// 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 `json:"-"`
// TokenID allows users to authenticate (possibly as another user) with an
// authentication token ID.
TokenID string `json:"-"`
}
// ToTokenV2CreateMap allows AuthOptions to satisfy the AuthOptionsBuilder
// interface in the v2 tokens package
func (opts AuthOptions) ToTokenV2CreateMap() (map[string]interface{}, error) {
// Populate the request map.
authMap := make(map[string]interface{})
if opts.Username != "" {
if opts.Password != "" {
authMap["passwordCredentials"] = map[string]interface{}{
"username": opts.Username,
"password": opts.Password,
}
} else {
return nil, ErrMissingInput{Argument: "Password"}
}
} else if opts.TokenID != "" {
authMap["token"] = map[string]interface{}{
"id": opts.TokenID,
}
} else {
return nil, ErrMissingInput{Argument: "Username"}
}
if opts.TenantID != "" {
authMap["tenantId"] = opts.TenantID
}
if opts.TenantName != "" {
authMap["tenantName"] = opts.TenantName
}
return map[string]interface{}{"auth": authMap}, nil
}
func (opts *AuthOptions) ToTokenV3CreateMap(scope map[string]interface{}) (map[string]interface{}, error) {
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 authReq struct {
Identity identityReq `json:"identity"`
}
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 opts.TenantID != "" {
return nil, ErrTenantIDProvided{}
}
if opts.TenantName != "" {
return nil, ErrTenantNameProvided{}
}
if opts.Password == "" {
if opts.TokenID != "" {
// Because we aren't using password authentication, it's an error to also provide any of the user-based authentication
// parameters.
if opts.Username != "" {
return nil, ErrUsernameWithToken{}
}
if opts.UserID != "" {
return nil, ErrUserIDWithToken{}
}
if opts.DomainID != "" {
return nil, ErrDomainIDWithToken{}
}
if opts.DomainName != "" {
return nil, ErrDomainNameWithToken{}
}
// Configure the request for Token authentication.
req.Auth.Identity.Methods = []string{"token"}
req.Auth.Identity.Token = &tokenReq{
ID: opts.TokenID,
}
} else {
// If no password or token ID are available, authentication can't continue.
return nil, ErrMissingPassword{}
}
} else {
// Password authentication.
req.Auth.Identity.Methods = []string{"password"}
// At least one of Username and UserID must be specified.
if opts.Username == "" && opts.UserID == "" {
return nil, ErrUsernameOrUserID{}
}
if opts.Username != "" {
// If Username is provided, UserID may not be provided.
if opts.UserID != "" {
return nil, ErrUsernameOrUserID{}
}
// Either DomainID or DomainName must also be specified.
if opts.DomainID == "" && opts.DomainName == "" {
return nil, ErrDomainIDOrDomainName{}
}
if opts.DomainID != "" {
if opts.DomainName != "" {
return nil, ErrDomainIDOrDomainName{}
}
// Configure the request for Username and Password authentication with a DomainID.
req.Auth.Identity.Password = &passwordReq{
User: userReq{
Name: &opts.Username,
Password: opts.Password,
Domain: &domainReq{ID: &opts.DomainID},
},
}
}
if opts.DomainName != "" {
// Configure the request for Username and Password authentication with a DomainName.
req.Auth.Identity.Password = &passwordReq{
User: userReq{
Name: &opts.Username,
Password: opts.Password,
Domain: &domainReq{Name: &opts.DomainName},
},
}
}
}
if opts.UserID != "" {
// If UserID is specified, neither DomainID nor DomainName may be.
if opts.DomainID != "" {
return nil, ErrDomainIDWithUserID{}
}
if opts.DomainName != "" {
return nil, ErrDomainNameWithUserID{}
}
// Configure the request for UserID and Password authentication.
req.Auth.Identity.Password = &passwordReq{
User: userReq{ID: &opts.UserID, Password: opts.Password},
}
}
}
b, err := BuildRequestBody(req, "")
if err != nil {
return nil, err
}
if len(scope) != 0 {
b["auth"].(map[string]interface{})["scope"] = scope
}
return b, nil
}
func (opts *AuthOptions) ToTokenV3ScopeMap() (map[string]interface{}, error) {
var scope struct {
ProjectID string
ProjectName string
DomainID string
DomainName string
}
if opts.TenantID != "" {
scope.ProjectID = opts.TenantID
opts.TenantID = ""
opts.TenantName = ""
} else {
if opts.TenantName != "" {
scope.ProjectName = opts.TenantName
scope.DomainID = opts.DomainID
scope.DomainName = opts.DomainName
}
opts.TenantName = ""
}
if scope.ProjectName != "" {
// ProjectName provided: either DomainID or DomainName must also be supplied.
// ProjectID may not be supplied.
if scope.DomainID == "" && scope.DomainName == "" {
return nil, ErrScopeDomainIDOrDomainName{}
}
if scope.ProjectID != "" {
return nil, ErrScopeProjectIDOrProjectName{}
}
if scope.DomainID != "" {
// ProjectName + DomainID
return map[string]interface{}{
"project": map[string]interface{}{
"name": &scope.ProjectName,
"domain": map[string]interface{}{"id": &scope.DomainID},
},
}, nil
}
if scope.DomainName != "" {
// ProjectName + DomainName
return map[string]interface{}{
"project": map[string]interface{}{
"name": &scope.ProjectName,
"domain": map[string]interface{}{"name": &scope.DomainName},
},
}, nil
}
} else if scope.ProjectID != "" {
// ProjectID provided. ProjectName, DomainID, and DomainName may not be provided.
if scope.DomainID != "" {
return nil, ErrScopeProjectIDAlone{}
}
if scope.DomainName != "" {
return nil, ErrScopeProjectIDAlone{}
}
// ProjectID
return map[string]interface{}{
"project": map[string]interface{}{
"id": &scope.ProjectID,
},
}, nil
} else if scope.DomainID != "" {
// DomainID provided. ProjectID, ProjectName, and DomainName may not be provided.
if scope.DomainName != "" {
return nil, ErrScopeDomainIDOrDomainName{}
}
// DomainID
return map[string]interface{}{
"domain": map[string]interface{}{
"id": &scope.DomainID,
},
}, nil
} else if scope.DomainName != "" {
return nil, ErrScopeDomainName{}
}
return nil, nil
}
func (opts AuthOptions) CanReauth() bool {
return opts.AllowReauth
}

View File

@ -1,21 +1,5 @@
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

408
vendor/github.com/gophercloud/gophercloud/errors.go generated vendored Normal file
View File

@ -0,0 +1,408 @@
package gophercloud
import "fmt"
// BaseError is an error type that all other error types embed.
type BaseError struct {
DefaultErrString string
Info string
}
func (e BaseError) Error() string {
e.DefaultErrString = "An error occurred while executing a Gophercloud request."
return e.choseErrString()
}
func (e BaseError) choseErrString() string {
if e.Info != "" {
return e.Info
}
return e.DefaultErrString
}
// ErrMissingInput is the error when input is required in a particular
// situation but not provided by the user
type ErrMissingInput struct {
BaseError
Argument string
}
func (e ErrMissingInput) Error() string {
e.DefaultErrString = fmt.Sprintf("Missing input for argument [%s]", e.Argument)
return e.choseErrString()
}
// ErrInvalidInput is an error type used for most non-HTTP Gophercloud errors.
type ErrInvalidInput struct {
ErrMissingInput
Value interface{}
}
func (e ErrInvalidInput) Error() string {
e.DefaultErrString = fmt.Sprintf("Invalid input provided for argument [%s]: [%+v]", e.Argument, e.Value)
return e.choseErrString()
}
// ErrUnexpectedResponseCode is returned by the Request method when a response code other than
// those listed in OkCodes is encountered.
type ErrUnexpectedResponseCode struct {
BaseError
URL string
Method string
Expected []int
Actual int
Body []byte
}
func (e ErrUnexpectedResponseCode) Error() string {
e.DefaultErrString = fmt.Sprintf(
"Expected HTTP response code %v when accessing [%s %s], but got %d instead\n%s",
e.Expected, e.Method, e.URL, e.Actual, e.Body,
)
return e.choseErrString()
}
// ErrDefault400 is the default error type returned on a 400 HTTP response code.
type ErrDefault400 struct {
ErrUnexpectedResponseCode
}
// ErrDefault401 is the default error type returned on a 401 HTTP response code.
type ErrDefault401 struct {
ErrUnexpectedResponseCode
}
// ErrDefault404 is the default error type returned on a 404 HTTP response code.
type ErrDefault404 struct {
ErrUnexpectedResponseCode
}
// ErrDefault405 is the default error type returned on a 405 HTTP response code.
type ErrDefault405 struct {
ErrUnexpectedResponseCode
}
// ErrDefault408 is the default error type returned on a 408 HTTP response code.
type ErrDefault408 struct {
ErrUnexpectedResponseCode
}
// ErrDefault429 is the default error type returned on a 429 HTTP response code.
type ErrDefault429 struct {
ErrUnexpectedResponseCode
}
// ErrDefault500 is the default error type returned on a 500 HTTP response code.
type ErrDefault500 struct {
ErrUnexpectedResponseCode
}
// ErrDefault503 is the default error type returned on a 503 HTTP response code.
type ErrDefault503 struct {
ErrUnexpectedResponseCode
}
func (e ErrDefault400) Error() string {
return "Invalid request due to incorrect syntax or missing required parameters."
}
func (e ErrDefault401) Error() string {
return "Authentication failed"
}
func (e ErrDefault404) Error() string {
return "Resource not found"
}
func (e ErrDefault405) Error() string {
return "Method not allowed"
}
func (e ErrDefault408) Error() string {
return "The server timed out waiting for the request"
}
func (e ErrDefault429) Error() string {
return "Too many requests have been sent in a given amount of time. Pause" +
" requests, wait up to one minute, and try again."
}
func (e ErrDefault500) Error() string {
return "Internal Server Error"
}
func (e ErrDefault503) Error() string {
return "The service is currently unable to handle the request due to a temporary" +
" overloading or maintenance. This is a temporary condition. Try again later."
}
// Err400er is the interface resource error types implement to override the error message
// from a 400 error.
type Err400er interface {
Error400(ErrUnexpectedResponseCode) error
}
// Err401er is the interface resource error types implement to override the error message
// from a 401 error.
type Err401er interface {
Error401(ErrUnexpectedResponseCode) error
}
// Err404er is the interface resource error types implement to override the error message
// from a 404 error.
type Err404er interface {
Error404(ErrUnexpectedResponseCode) error
}
// Err405er is the interface resource error types implement to override the error message
// from a 405 error.
type Err405er interface {
Error405(ErrUnexpectedResponseCode) error
}
// Err408er is the interface resource error types implement to override the error message
// from a 408 error.
type Err408er interface {
Error408(ErrUnexpectedResponseCode) error
}
// Err429er is the interface resource error types implement to override the error message
// from a 429 error.
type Err429er interface {
Error429(ErrUnexpectedResponseCode) error
}
// Err500er is the interface resource error types implement to override the error message
// from a 500 error.
type Err500er interface {
Error500(ErrUnexpectedResponseCode) error
}
// Err503er is the interface resource error types implement to override the error message
// from a 503 error.
type Err503er interface {
Error503(ErrUnexpectedResponseCode) error
}
// ErrTimeOut is the error type returned when an operations times out.
type ErrTimeOut struct {
BaseError
}
func (e ErrTimeOut) Error() string {
e.DefaultErrString = "A time out occurred"
return e.choseErrString()
}
// ErrUnableToReauthenticate is the error type returned when reauthentication fails.
type ErrUnableToReauthenticate struct {
BaseError
ErrOriginal error
}
func (e ErrUnableToReauthenticate) Error() string {
e.DefaultErrString = fmt.Sprintf("Unable to re-authenticate: %s", e.ErrOriginal)
return e.choseErrString()
}
// ErrErrorAfterReauthentication is the error type returned when reauthentication
// succeeds, but an error occurs afterword (usually an HTTP error).
type ErrErrorAfterReauthentication struct {
BaseError
ErrOriginal error
}
func (e ErrErrorAfterReauthentication) Error() string {
e.DefaultErrString = fmt.Sprintf("Successfully re-authenticated, but got error executing request: %s", e.ErrOriginal)
return e.choseErrString()
}
// 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.
type ErrServiceNotFound struct {
BaseError
}
func (e ErrServiceNotFound) Error() string {
e.DefaultErrString = "No suitable service could be found in the service catalog."
return e.choseErrString()
}
// 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.
type ErrEndpointNotFound struct {
BaseError
}
func (e ErrEndpointNotFound) Error() string {
e.DefaultErrString = "No suitable endpoint could be found in the service catalog."
return e.choseErrString()
}
// ErrResourceNotFound is the error when trying to retrieve a resource's
// ID by name and the resource doesn't exist.
type ErrResourceNotFound struct {
BaseError
Name string
ResourceType string
}
func (e ErrResourceNotFound) Error() string {
e.DefaultErrString = fmt.Sprintf("Unable to find %s with name %s", e.ResourceType, e.Name)
return e.choseErrString()
}
// ErrMultipleResourcesFound is the error when trying to retrieve a resource's
// ID by name and multiple resources have the user-provided name.
type ErrMultipleResourcesFound struct {
BaseError
Name string
Count int
ResourceType string
}
func (e ErrMultipleResourcesFound) Error() string {
e.DefaultErrString = fmt.Sprintf("Found %d %ss matching %s", e.Count, e.ResourceType, e.Name)
return e.choseErrString()
}
// ErrUnexpectedType is the error when an unexpected type is encountered
type ErrUnexpectedType struct {
BaseError
Expected string
Actual string
}
func (e ErrUnexpectedType) Error() string {
e.DefaultErrString = fmt.Sprintf("Expected %s but got %s", e.Expected, e.Actual)
return e.choseErrString()
}
func unacceptedAttributeErr(attribute string) string {
return fmt.Sprintf("The base Identity V3 API does not accept authentication by %s", attribute)
}
func redundantWithTokenErr(attribute string) string {
return fmt.Sprintf("%s may not be provided when authenticating with a TokenID", attribute)
}
func redundantWithUserID(attribute string) string {
return fmt.Sprintf("%s may not be provided when authenticating with a UserID", attribute)
}
// ErrAPIKeyProvided indicates that an APIKey was provided but can't be used.
type ErrAPIKeyProvided struct{ BaseError }
func (e ErrAPIKeyProvided) Error() string {
return unacceptedAttributeErr("APIKey")
}
// ErrTenantIDProvided indicates that a TenantID was provided but can't be used.
type ErrTenantIDProvided struct{ BaseError }
func (e ErrTenantIDProvided) Error() string {
return unacceptedAttributeErr("TenantID")
}
// ErrTenantNameProvided indicates that a TenantName was provided but can't be used.
type ErrTenantNameProvided struct{ BaseError }
func (e ErrTenantNameProvided) Error() string {
return unacceptedAttributeErr("TenantName")
}
// ErrUsernameWithToken indicates that a Username was provided, but token authentication is being used instead.
type ErrUsernameWithToken struct{ BaseError }
func (e ErrUsernameWithToken) Error() string {
return redundantWithTokenErr("Username")
}
// ErrUserIDWithToken indicates that a UserID was provided, but token authentication is being used instead.
type ErrUserIDWithToken struct{ BaseError }
func (e ErrUserIDWithToken) Error() string {
return redundantWithTokenErr("UserID")
}
// ErrDomainIDWithToken indicates that a DomainID was provided, but token authentication is being used instead.
type ErrDomainIDWithToken struct{ BaseError }
func (e ErrDomainIDWithToken) Error() string {
return redundantWithTokenErr("DomainID")
}
// ErrDomainNameWithToken indicates that a DomainName was provided, but token authentication is being used instead.s
type ErrDomainNameWithToken struct{ BaseError }
func (e ErrDomainNameWithToken) Error() string {
return redundantWithTokenErr("DomainName")
}
// ErrUsernameOrUserID indicates that neither username nor userID are specified, or both are at once.
type ErrUsernameOrUserID struct{ BaseError }
func (e ErrUsernameOrUserID) Error() string {
return "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.
type ErrDomainIDWithUserID struct{ BaseError }
func (e ErrDomainIDWithUserID) Error() string {
return redundantWithUserID("DomainID")
}
// ErrDomainNameWithUserID indicates that a DomainName was provided, but unnecessary because a UserID is being used.
type ErrDomainNameWithUserID struct{ BaseError }
func (e ErrDomainNameWithUserID) Error() string {
return 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.
type ErrDomainIDOrDomainName struct{ BaseError }
func (e ErrDomainIDOrDomainName) Error() string {
return "You must provide exactly one of DomainID or DomainName to authenticate by Username"
}
// ErrMissingPassword indicates that no password was provided and no token is available.
type ErrMissingPassword struct{ BaseError }
func (e ErrMissingPassword) Error() string {
return "You must provide a password to authenticate"
}
// ErrScopeDomainIDOrDomainName indicates that a domain ID or Name was required in a Scope, but not present.
type ErrScopeDomainIDOrDomainName struct{ BaseError }
func (e ErrScopeDomainIDOrDomainName) Error() string {
return "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.
type ErrScopeProjectIDOrProjectName struct{ BaseError }
func (e ErrScopeProjectIDOrProjectName) Error() string {
return "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.
type ErrScopeProjectIDAlone struct{ BaseError }
func (e ErrScopeProjectIDAlone) Error() string {
return "ProjectID must be supplied alone in a Scope"
}
// ErrScopeDomainName indicates that a DomainName was provided alone in a Scope.
type ErrScopeDomainName struct{ BaseError }
func (e ErrScopeDomainName) Error() string {
return "DomainName must be supplied with a ProjectName or ProjectID in a Scope"
}
// ErrScopeEmpty indicates that no credentials were provided in a Scope.
type ErrScopeEmpty struct{ BaseError }
func (e ErrScopeEmpty) Error() string {
return "You must provide either a Project or Domain in a Scope"
}

View File

@ -1,23 +1,14 @@
package openstack
import (
"fmt"
"os"
"github.com/rackspace/gophercloud"
"github.com/gophercloud/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 needs to be set.")
ErrNoPassword = fmt.Errorf("Environment variable OS_PASSWORD needs to be set.")
)
// AuthOptions fills out an identity.AuthOptions structure with the settings found on the various OpenStack
// AuthOptionsFromEnv fills out an identity.AuthOptions structure with the settings found on the various OpenStack
// OS_* environment variables. The following variables provide sources of truth: OS_AUTH_URL, OS_USERNAME,
// OS_PASSWORD, OS_TENANT_ID, and OS_TENANT_NAME. Of these, OS_USERNAME, OS_PASSWORD, and OS_AUTH_URL must
// have settings, or an error will result. OS_TENANT_ID and OS_TENANT_NAME are optional.
@ -32,15 +23,18 @@ func AuthOptionsFromEnv() (gophercloud.AuthOptions, error) {
domainName := os.Getenv("OS_DOMAIN_NAME")
if authURL == "" {
return nilOptions, ErrNoAuthURL
err := gophercloud.ErrMissingInput{Argument: "authURL"}
return nilOptions, err
}
if username == "" && userID == "" {
return nilOptions, ErrNoUsername
err := gophercloud.ErrMissingInput{Argument: "username"}
return nilOptions, err
}
if password == "" {
return nilOptions, ErrNoPassword
err := gophercloud.ErrMissingInput{Argument: "password"}
return nilOptions, err
}
ao := gophercloud.AuthOptions{

View File

@ -3,12 +3,12 @@ package openstack
import (
"fmt"
"net/url"
"strings"
"reflect"
"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"
"github.com/gophercloud/gophercloud"
tokens2 "github.com/gophercloud/gophercloud/openstack/identity/v2/tokens"
tokens3 "github.com/gophercloud/gophercloud/openstack/identity/v3/tokens"
"github.com/gophercloud/gophercloud/openstack/utils"
)
const (
@ -76,9 +76,9 @@ func Authenticate(client *gophercloud.ProviderClient, options gophercloud.AuthOp
switch chosen.ID {
case v20:
return v2auth(client, endpoint, options)
return v2auth(client, endpoint, options, gophercloud.EndpointOpts{})
case v30:
return v3auth(client, endpoint, options)
return v3auth(client, endpoint, &options, gophercloud.EndpointOpts{})
default:
// The switch statement must be out of date from the versions list.
return fmt.Errorf("Unrecognized identity version: %s", chosen.ID)
@ -86,17 +86,31 @@ func Authenticate(client *gophercloud.ProviderClient, options gophercloud.AuthOp
}
// AuthenticateV2 explicitly authenticates against the identity v2 endpoint.
func AuthenticateV2(client *gophercloud.ProviderClient, options gophercloud.AuthOptions) error {
return v2auth(client, "", options)
func AuthenticateV2(client *gophercloud.ProviderClient, options gophercloud.AuthOptions, eo gophercloud.EndpointOpts) error {
return v2auth(client, "", options, eo)
}
func v2auth(client *gophercloud.ProviderClient, endpoint string, options gophercloud.AuthOptions) error {
v2Client := NewIdentityV2(client)
func v2auth(client *gophercloud.ProviderClient, endpoint string, options gophercloud.AuthOptions, eo gophercloud.EndpointOpts) error {
v2Client, err := NewIdentityV2(client, eo)
if err != nil {
return err
}
if endpoint != "" {
v2Client.Endpoint = endpoint
}
result := tokens2.Create(v2Client, tokens2.AuthOptions{AuthOptions: options})
v2Opts := tokens2.AuthOptions{
IdentityEndpoint: options.IdentityEndpoint,
Username: options.Username,
Password: options.Password,
TenantID: options.TenantID,
TenantName: options.TenantName,
AllowReauth: options.AllowReauth,
TokenID: options.TokenID,
}
result := tokens2.Create(v2Client, v2Opts)
token, err := result.ExtractToken()
if err != nil {
@ -111,7 +125,7 @@ func v2auth(client *gophercloud.ProviderClient, endpoint string, options gopherc
if options.AllowReauth {
client.ReauthFunc = func() error {
client.TokenID = ""
return v2auth(client, endpoint, options)
return v2auth(client, endpoint, options, eo)
}
}
client.TokenID = token.ID
@ -123,40 +137,22 @@ func v2auth(client *gophercloud.ProviderClient, endpoint string, options gopherc
}
// AuthenticateV3 explicitly authenticates against the identity v3 service.
func AuthenticateV3(client *gophercloud.ProviderClient, options gophercloud.AuthOptions) error {
return v3auth(client, "", options)
func AuthenticateV3(client *gophercloud.ProviderClient, options tokens3.AuthOptionsBuilder, eo gophercloud.EndpointOpts) error {
return v3auth(client, "", options, eo)
}
func v3auth(client *gophercloud.ProviderClient, endpoint string, options gophercloud.AuthOptions) error {
func v3auth(client *gophercloud.ProviderClient, endpoint string, opts tokens3.AuthOptionsBuilder, eo gophercloud.EndpointOpts) error {
// Override the generated service endpoint with the one returned by the version endpoint.
v3Client := NewIdentityV3(client)
v3Client, err := NewIdentityV3(client, eo)
if err != nil {
return err
}
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)
result := tokens3.Create(v3Client, opts)
token, err := result.ExtractToken()
if err != nil {
@ -170,10 +166,10 @@ func v3auth(client *gophercloud.ProviderClient, endpoint string, options gopherc
client.TokenID = token.ID
if options.AllowReauth {
if opts.CanReauth() {
client.ReauthFunc = func() error {
client.TokenID = ""
return v3auth(client, endpoint, options)
return v3auth(client, endpoint, opts, eo)
}
}
client.EndpointLocator = func(opts gophercloud.EndpointOpts) (string, error) {
@ -184,57 +180,39 @@ func v3auth(client *gophercloud.ProviderClient, endpoint string, options gopherc
}
// 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/"
func NewIdentityV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
endpoint := client.IdentityBase + "v2.0/"
var err error
if !reflect.DeepEqual(eo, gophercloud.EndpointOpts{}) {
eo.ApplyDefaults("identity")
endpoint, err = client.EndpointLocator(eo)
if err != nil {
return nil, err
}
}
return &gophercloud.ServiceClient{
ProviderClient: client,
Endpoint: v2Endpoint,
}
Endpoint: endpoint,
}, nil
}
// 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/"
func NewIdentityV3(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
endpoint := client.IdentityBase + "v3/"
var err error
if !reflect.DeepEqual(eo, gophercloud.EndpointOpts{}) {
eo.ApplyDefaults("identity")
endpoint, err = client.EndpointLocator(eo)
if err != nil {
return nil, err
}
}
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
Endpoint: endpoint,
}, nil
}
// NewObjectStorageV1 creates a ServiceClient that may be used with the v1 object storage package.
@ -281,6 +259,26 @@ func NewBlockStorageV1(client *gophercloud.ProviderClient, eo gophercloud.Endpoi
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
}
// NewSharedFileSystemV2 creates a ServiceClient that may be used to access the v2 shared file system service.
func NewSharedFileSystemV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
eo.ApplyDefaults("sharev2")
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) {
@ -311,3 +309,15 @@ func NewDBV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*
}
return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil
}
// NewImageServiceV2 creates a ServiceClient that may be used to access the v2 image service.
func NewImageServiceV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
eo.ApplyDefaults("image")
url, err := client.EndpointLocator(eo)
if err != nil {
return nil, err
}
return &gophercloud.ServiceClient{ProviderClient: client,
Endpoint: url,
ResourceBase: url + "v2/"}, nil
}

View File

@ -1,15 +1,14 @@
package extensions
import (
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
"github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/pagination"
)
// Get retrieves information for a specific extension using its alias.
func Get(c *gophercloud.ServiceClient, alias string) GetResult {
var res GetResult
_, res.Err = c.Get(ExtensionURL(c, alias), &res.Body, nil)
return res
func Get(c *gophercloud.ServiceClient, alias string) (r GetResult) {
_, r.Err = c.Get(ExtensionURL(c, alias), &r.Body, nil)
return
}
// List returns a Pager which allows you to iterate over the full collection of extensions.

View File

@ -1,9 +1,8 @@
package extensions
import (
"github.com/mitchellh/mapstructure"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
"github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/pagination"
)
// GetResult temporarily stores the result of a Get call.
@ -14,27 +13,21 @@ type GetResult struct {
// Extract interprets a GetResult as an Extension.
func (r GetResult) Extract() (*Extension, error) {
if r.Err != nil {
return nil, r.Err
}
var res struct {
var s struct {
Extension *Extension `json:"extension"`
}
err := mapstructure.Decode(r.Body, &res)
return res.Extension, err
err := r.ExtractInto(&s)
return s.Extension, err
}
// Extension is a struct that represents an OpenStack extension.
type Extension struct {
Updated string `json:"updated" mapstructure:"updated"`
Name string `json:"name" mapstructure:"name"`
Links []interface{} `json:"links" mapstructure:"links"`
Namespace string `json:"namespace" mapstructure:"namespace"`
Alias string `json:"alias" mapstructure:"alias"`
Description string `json:"description" mapstructure:"description"`
Updated string `json:"updated"`
Name string `json:"name"`
Links []interface{} `json:"links"`
Namespace string `json:"namespace"`
Alias string `json:"alias"`
Description string `json:"description"`
}
// ExtensionPage is the page returned by a pager when traversing over a collection of extensions.
@ -45,21 +38,16 @@ type ExtensionPage struct {
// IsEmpty checks whether an ExtensionPage struct is empty.
func (r ExtensionPage) IsEmpty() (bool, error) {
is, err := ExtractExtensions(r)
if err != nil {
return true, err
}
return len(is) == 0, nil
return len(is) == 0, err
}
// ExtractExtensions accepts a Page struct, specifically an ExtensionPage struct, and extracts the
// elements into a slice of Extension structs.
// In other words, a generic collection is mapped into a relevant slice.
func ExtractExtensions(page pagination.Page) ([]Extension, error) {
var resp struct {
Extensions []Extension `mapstructure:"extensions"`
func ExtractExtensions(r pagination.Page) ([]Extension, error) {
var s struct {
Extensions []Extension `json:"extensions"`
}
err := mapstructure.Decode(page.(ExtensionPage).Body, &resp)
return resp.Extensions, err
err := (r.(ExtensionPage)).ExtractInto(&s)
return s.Extensions, err
}

View File

@ -1,6 +1,6 @@
package extensions
import "github.com/rackspace/gophercloud"
import "github.com/gophercloud/gophercloud"
// ExtensionURL generates the URL for an extension resource by name.
func ExtensionURL(c *gophercloud.ServiceClient, name string) string {

View File

@ -1,9 +1,9 @@
package extensions
import (
"github.com/rackspace/gophercloud"
common "github.com/rackspace/gophercloud/openstack/common/extensions"
"github.com/rackspace/gophercloud/pagination"
"github.com/gophercloud/gophercloud"
common "github.com/gophercloud/gophercloud/openstack/common/extensions"
"github.com/gophercloud/gophercloud/pagination"
)
// ExtractExtensions interprets a Page as a slice of Extensions.

View File

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

View File

@ -0,0 +1,112 @@
package floatingips
import (
"github.com/gophercloud/gophercloud"
"github.com/gophercloud/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 FloatingIPPage{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 `json:"pool" required:"true"`
}
// ToFloatingIPCreateMap constructs a request body from CreateOpts.
func (opts CreateOpts) ToFloatingIPCreateMap() (map[string]interface{}, error) {
return gophercloud.BuildRequestBody(opts, "")
}
// Create requests the creation of a new floating IP
func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) {
b, err := opts.ToFloatingIPCreateMap()
if err != nil {
r.Err = err
return
}
_, r.Err = client.Post(createURL(client), b, &r.Body, &gophercloud.RequestOpts{
OkCodes: []int{200},
})
return
}
// Get returns data about a previously created FloatingIP.
func Get(client *gophercloud.ServiceClient, id string) (r GetResult) {
_, r.Err = client.Get(getURL(client, id), &r.Body, nil)
return
}
// Delete requests the deletion of a previous allocated FloatingIP.
func Delete(client *gophercloud.ServiceClient, id string) (r DeleteResult) {
_, r.Err = client.Delete(deleteURL(client, id), nil)
return
}
// AssociateOptsBuilder is the interface types must satfisfy to be used as
// Associate options
type AssociateOptsBuilder interface {
ToFloatingIPAssociateMap() (map[string]interface{}, error)
}
// AssociateOpts specifies the required information to associate a floating IP with an instance
type AssociateOpts struct {
// FloatingIP is the floating IP to associate with an instance
FloatingIP string `json:"address" required:"true"`
// FixedIP is an optional fixed IP address of the server
FixedIP string `json:"fixed_address,omitempty"`
}
// ToFloatingIPAssociateMap constructs a request body from AssociateOpts.
func (opts AssociateOpts) ToFloatingIPAssociateMap() (map[string]interface{}, error) {
return gophercloud.BuildRequestBody(opts, "addFloatingIp")
}
// AssociateInstance pairs an allocated floating IP with an instance.
func AssociateInstance(client *gophercloud.ServiceClient, serverID string, opts AssociateOptsBuilder) (r AssociateResult) {
b, err := opts.ToFloatingIPAssociateMap()
if err != nil {
r.Err = err
return
}
_, r.Err = client.Post(associateURL(client, serverID), b, nil, nil)
return
}
// DisassociateOptsBuilder is the interface types must satfisfy to be used as
// Disassociate options
type DisassociateOptsBuilder interface {
ToFloatingIPDisassociateMap() (map[string]interface{}, error)
}
// DisassociateOpts specifies the required information to disassociate a floating IP with an instance
type DisassociateOpts struct {
FloatingIP string `json:"address" required:"true"`
}
// ToFloatingIPDisassociateMap constructs a request body from AssociateOpts.
func (opts DisassociateOpts) ToFloatingIPDisassociateMap() (map[string]interface{}, error) {
return gophercloud.BuildRequestBody(opts, "removeFloatingIp")
}
// DisassociateInstance decouples an allocated floating IP from an instance
func DisassociateInstance(client *gophercloud.ServiceClient, serverID string, opts DisassociateOptsBuilder) (r DisassociateResult) {
b, err := opts.ToFloatingIPDisassociateMap()
if err != nil {
r.Err = err
return
}
_, r.Err = client.Post(disassociateURL(client, serverID), b, nil, nil)
return
}

View File

@ -1,54 +1,51 @@
package floatingip
package floatingips
import (
"github.com/mitchellh/mapstructure"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
"github.com/gophercloud/gophercloud"
"github.com/gophercloud/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"`
ID string `json:"id"`
// FixedIP is the IP of the instance related to the Floating IP
FixedIP string `mapstructure:"fixed_ip,omitempty"`
FixedIP string `json:"fixed_ip,omitempty"`
// InstanceID is the ID of the instance that is using the Floating IP
InstanceID string `mapstructure:"instance_id"`
InstanceID string `json:"instance_id"`
// IP is the actual Floating IP
IP string `mapstructure:"ip"`
IP string `json:"ip"`
// Pool is the pool of floating IPs that this floating IP belongs to
Pool string `mapstructure:"pool"`
Pool string `json:"pool"`
}
// FloatingIPsPage stores a single, only page of FloatingIPs
// FloatingIPPage stores a single, only page of FloatingIPs
// results from a List call.
type FloatingIPsPage struct {
type FloatingIPPage struct {
pagination.SinglePageBase
}
// IsEmpty determines whether or not a FloatingIPsPage is empty.
func (page FloatingIPsPage) IsEmpty() (bool, error) {
func (page FloatingIPPage) 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"`
func ExtractFloatingIPs(r pagination.Page) ([]FloatingIP, error) {
var s struct {
FloatingIPs []FloatingIP `json:"floating_ips"`
}
err := mapstructure.WeakDecode(casted, &response)
return response.FloatingIPs, err
err := (r.(FloatingIPPage)).ExtractInto(&s)
return s.FloatingIPs, err
}
// FloatingIPResult is the raw result from a FloatingIP request.
type FloatingIPResult struct {
gophercloud.Result
}
@ -56,16 +53,11 @@ type FloatingIPResult struct {
// 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 s struct {
FloatingIP *FloatingIP `json:"floating_ip"`
}
var res struct {
FloatingIP *FloatingIP `json:"floating_ip" mapstructure:"floating_ip"`
}
err := mapstructure.WeakDecode(r.Body, &res)
return res.FloatingIP, err
err := r.ExtractInto(&s)
return s.FloatingIP, err
}
// CreateResult is the response from a Create operation. Call its Extract method to interpret it

View File

@ -1,6 +1,6 @@
package floatingip
package floatingips
import "github.com/rackspace/gophercloud"
import "github.com/gophercloud/gophercloud"
const resourcePath = "os-floating-ips"
@ -24,14 +24,14 @@ 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 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 associateURL(c *gophercloud.ServiceClient, serverID string) string {
return serverURL(c, serverID)
}
func disassociateURL(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,11 +1,9 @@
package keypairs
import (
"errors"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/openstack/compute/v2/servers"
"github.com/rackspace/gophercloud/pagination"
"github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/openstack/compute/v2/servers"
"github.com/gophercloud/gophercloud/pagination"
)
// CreateOptsExt adds a KeyPair option to the base CreateOpts.
@ -47,56 +45,40 @@ type CreateOptsBuilder interface {
// 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
// Name is a friendly name to refer to this KeyPair in other services.
Name string `json:"name" required:"true"`
// 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
PublicKey string `json:"public_key,omitempty"`
}
// 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
return gophercloud.BuildRequestBody(opts, "keypair")
}
// 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()
func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) {
b, err := opts.ToKeyPairCreateMap()
if err != nil {
res.Err = err
return res
r.Err = err
return
}
_, res.Err = client.Post(createURL(client), reqBody, &res.Body, &gophercloud.RequestOpts{
_, r.Err = client.Post(createURL(client), b, &r.Body, &gophercloud.RequestOpts{
OkCodes: []int{200},
})
return res
return
}
// 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
func Get(client *gophercloud.ServiceClient, name string) (r GetResult) {
_, r.Err = client.Get(getURL(client, name), &r.Body, nil)
return
}
// 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
func Delete(client *gophercloud.ServiceClient, name string) (r DeleteResult) {
_, r.Err = client.Delete(deleteURL(client, name), nil)
return
}

View File

@ -1,31 +1,30 @@
package keypairs
import (
"github.com/mitchellh/mapstructure"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
"github.com/gophercloud/gophercloud"
"github.com/gophercloud/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"`
Name string `json:"name"`
// Fingerprint is a short sequence of bytes that can be used to authenticate or validate a longer
// public key.
Fingerprint string `mapstructure:"fingerprint"`
Fingerprint string `json:"fingerprint"`
// PublicKey is the public key from this pair, in OpenSSH format. "ssh-rsa AAAAB3Nz..."
PublicKey string `mapstructure:"public_key"`
PublicKey string `json:"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"`
PrivateKey string `json:"private_key"`
// UserID is the user who owns this keypair.
UserID string `mapstructure:"user_id"`
UserID string `json:"user_id"`
}
// KeyPairPage stores a single, only page of KeyPair results from a List call.
@ -40,18 +39,16 @@ func (page KeyPairPage) IsEmpty() (bool, error) {
}
// ExtractKeyPairs interprets a page of results as a slice of KeyPairs.
func ExtractKeyPairs(page pagination.Page) ([]KeyPair, error) {
func ExtractKeyPairs(r pagination.Page) ([]KeyPair, error) {
type pair struct {
KeyPair KeyPair `mapstructure:"keypair"`
KeyPair KeyPair `json:"keypair"`
}
var resp struct {
KeyPairs []pair `mapstructure:"keypairs"`
var s struct {
KeyPairs []pair `json:"keypairs"`
}
err := mapstructure.Decode(page.(KeyPairPage).Body, &resp)
results := make([]KeyPair, len(resp.KeyPairs))
for i, pair := range resp.KeyPairs {
err := (r.(KeyPairPage)).ExtractInto(&s)
results := make([]KeyPair, len(s.KeyPairs))
for i, pair := range s.KeyPairs {
results[i] = pair.KeyPair
}
return results, err
@ -63,16 +60,11 @@ type keyPairResult struct {
// 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 s struct {
KeyPair *KeyPair `json:"keypair"`
}
var res struct {
KeyPair *KeyPair `json:"keypair" mapstructure:"keypair"`
}
err := mapstructure.Decode(r.Body, &res)
return res.KeyPair, err
err := r.ExtractInto(&s)
return s.KeyPair, err
}
// CreateResult is the response from a Create operation. Call its Extract method to interpret it

View File

@ -1,6 +1,6 @@
package keypairs
import "github.com/rackspace/gophercloud"
import "github.com/gophercloud/gophercloud"
const resourcePath = "os-keypairs"

View File

@ -0,0 +1,19 @@
package startstop
import "github.com/gophercloud/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) (r gophercloud.ErrResult) {
_, r.Err = client.Post(actionURL(client, id), map[string]interface{}{"os-start": nil}, nil, nil)
return
}
// Stop is the operation responsible for stopping a Compute server.
func Stop(client *gophercloud.ServiceClient, id string) (r gophercloud.ErrResult) {
_, r.Err = client.Post(actionURL(client, id), map[string]interface{}{"os-stop": nil}, nil, nil)
return
}

View File

@ -1,10 +1,8 @@
package flavors
import (
"fmt"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
"github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/pagination"
)
// ListOptsBuilder allows extensions to add additional parameters to the
@ -36,10 +34,7 @@ type ListOpts struct {
// 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
return q.String(), err
}
// ListDetail instructs OpenStack to provide a list of flavors.
@ -54,50 +49,52 @@ func ListDetail(client *gophercloud.ServiceClient, opts ListOptsBuilder) paginat
}
url += query
}
createPage := func(r pagination.PageResult) pagination.Page {
return pagination.NewPager(client, url, 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
func Get(client *gophercloud.ServiceClient, id string) (r GetResult) {
_, r.Err = client.Get(getURL(client, id), &r.Body, nil)
return
}
// 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.")
count := 0
id := ""
allPages, err := ListDetail(client, nil).AllPages()
if err != nil {
return "", err
}
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
})
all, err := ExtractFlavors(allPages)
if err != nil {
return "", err
}
switch flavorCount {
for _, f := range all {
if f.Name == name {
count++
id = f.ID
}
}
switch count {
case 0:
return "", fmt.Errorf("Unable to find flavor: %s", name)
err := &gophercloud.ErrResourceNotFound{}
err.ResourceType = "flavor"
err.Name = name
return "", err
case 1:
return flavorID, nil
return id, nil
default:
return "", fmt.Errorf("Found %d flavors matching %s", flavorCount, name)
err := &gophercloud.ErrMultipleResourcesFound{}
err.ResourceType = "flavor"
err.Name = name
err.Count = count
return "", err
}
}

View File

@ -0,0 +1,114 @@
package flavors
import (
"encoding/json"
"strconv"
"github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/pagination"
)
// GetResult temporarily holds the response from a Get call.
type GetResult struct {
gophercloud.Result
}
// Extract provides access to the individual Flavor returned by the Get function.
func (r GetResult) Extract() (*Flavor, error) {
var s struct {
Flavor *Flavor `json:"flavor"`
}
err := r.ExtractInto(&s)
return s.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 `json:"id"`
// The Disk and RA< fields provide a measure of storage space offered by the flavor, in GB and MB, respectively.
Disk int `json:"disk"`
RAM int `json:"ram"`
// The Name field provides a human-readable moniker for the flavor.
Name string `json:"name"`
RxTxFactor float64 `json:"rxtx_factor"`
// Swap indicates how much space is reserved for swap.
// If not provided, this field will be set to 0.
Swap int `json:"swap"`
// VCPUs indicates how many (virtual) CPUs are available for this flavor.
VCPUs int `json:"vcpus"`
}
func (f *Flavor) UnmarshalJSON(b []byte) error {
var flavor struct {
ID string `json:"id"`
Disk int `json:"disk"`
RAM int `json:"ram"`
Name string `json:"name"`
RxTxFactor float64 `json:"rxtx_factor"`
Swap interface{} `json:"swap"`
VCPUs int `json:"vcpus"`
}
err := json.Unmarshal(b, &flavor)
if err != nil {
return err
}
f.ID = flavor.ID
f.Disk = flavor.Disk
f.RAM = flavor.RAM
f.Name = flavor.Name
f.RxTxFactor = flavor.RxTxFactor
f.VCPUs = flavor.VCPUs
switch t := flavor.Swap.(type) {
case float64:
f.Swap = int(t)
case string:
switch t {
case "":
f.Swap = 0
default:
swap, err := strconv.ParseFloat(t, 64)
if err != nil {
return err
}
f.Swap = int(swap)
}
}
return nil
}
// 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 (page FlavorPage) IsEmpty() (bool, error) {
flavors, err := ExtractFlavors(page)
return len(flavors) == 0, err
}
// NextPageURL uses the response's embedded link reference to navigate to the next page of results.
func (page FlavorPage) NextPageURL() (string, error) {
var s struct {
Links []gophercloud.Link `json:"flavors_links"`
}
err := page.ExtractInto(&s)
if err != nil {
return "", err
}
return gophercloud.ExtractNextURL(s.Links)
}
// ExtractFlavors provides access to the list of flavors in a page acquired from the List operation.
func ExtractFlavors(r pagination.Page) ([]Flavor, error) {
var s struct {
Flavors []Flavor `json:"flavors"`
}
err := (r.(FlavorPage)).ExtractInto(&s)
return s.Flavors, err
}

View File

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

View File

@ -1,10 +1,8 @@
package images
import (
"fmt"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
"github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/pagination"
)
// ListOptsBuilder allows extensions to add additional parameters to the
@ -34,10 +32,7 @@ type ListOpts struct {
// 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
return q.String(), err
}
// ListDetail enumerates the available images.
@ -50,60 +45,58 @@ func ListDetail(client *gophercloud.ServiceClient, opts ListOptsBuilder) paginat
}
url += query
}
createPage := func(r pagination.PageResult) pagination.Page {
return pagination.NewPager(client, url, 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
func Get(client *gophercloud.ServiceClient, id string) (r GetResult) {
_, r.Err = client.Get(getURL(client, id), &r.Body, nil)
return
}
// 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
func Delete(client *gophercloud.ServiceClient, id string) (r DeleteResult) {
_, r.Err = client.Delete(deleteURL(client, id), nil)
return
}
// 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.")
count := 0
id := ""
allPages, err := ListDetail(client, nil).AllPages()
if err != nil {
return "", err
}
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
})
all, err := ExtractImages(allPages)
if err != nil {
return "", err
}
switch imageCount {
for _, f := range all {
if f.Name == name {
count++
id = f.ID
}
}
switch count {
case 0:
return "", fmt.Errorf("Unable to find image: %s", name)
err := &gophercloud.ErrResourceNotFound{}
err.ResourceType = "image"
err.Name = name
return "", err
case 1:
return imageID, nil
return id, nil
default:
return "", fmt.Errorf("Found %d images matching %s", imageCount, name)
err := &gophercloud.ErrMultipleResourcesFound{}
err.ResourceType = "image"
err.Name = name
err.Count = count
return "", err
}
}

View File

@ -1,9 +1,8 @@
package images
import (
"github.com/mitchellh/mapstructure"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
"github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/pagination"
)
// GetResult temporarily stores a Get response.
@ -17,17 +16,12 @@ type DeleteResult struct {
}
// Extract interprets a GetResult as an Image.
func (gr GetResult) Extract() (*Image, error) {
if gr.Err != nil {
return nil, gr.Err
func (r GetResult) Extract() (*Image, error) {
var s struct {
Image *Image `json:"image"`
}
var decoded struct {
Image Image `mapstructure:"image"`
}
err := mapstructure.Decode(gr.Body, &decoded)
return &decoded.Image, err
err := r.ExtractInto(&s)
return s.Image, err
}
// Image is used for JSON (un)marshalling.
@ -51,6 +45,8 @@ type Image struct {
Status string
Updated string
Metadata map[string]string
}
// ImagePage contains a single page of results from a List operation.
@ -62,34 +58,26 @@ type ImagePage struct {
// 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
return len(images) == 0, err
}
// 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 s struct {
Links []gophercloud.Link `json:"images_links"`
}
var r resp
err := mapstructure.Decode(page.Body, &r)
err := page.ExtractInto(&s)
if err != nil {
return "", err
}
return gophercloud.ExtractNextURL(r.Links)
return gophercloud.ExtractNextURL(s.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"`
func ExtractImages(r pagination.Page) ([]Image, error) {
var s struct {
Images []Image `json:"images"`
}
err := mapstructure.Decode(casted, &results)
return results.Images, err
err := (r.(ImagePage)).ExtractInto(&s)
return s.Images, err
}

View File

@ -1,6 +1,6 @@
package images
import "github.com/rackspace/gophercloud"
import "github.com/gophercloud/gophercloud"
func listDetailURL(client *gophercloud.ServiceClient) string {
return client.ServiceURL("images", "detail")

View File

@ -0,0 +1,71 @@
package servers
import (
"fmt"
"github.com/gophercloud/gophercloud"
)
// ErrNeitherImageIDNorImageNameProvided is the error when neither the image
// ID nor the image name is provided for a server operation
type ErrNeitherImageIDNorImageNameProvided struct{ gophercloud.ErrMissingInput }
func (e ErrNeitherImageIDNorImageNameProvided) Error() string {
return "One and only one of the image ID and the image name must be provided."
}
// ErrNeitherFlavorIDNorFlavorNameProvided is the error when neither the flavor
// ID nor the flavor name is provided for a server operation
type ErrNeitherFlavorIDNorFlavorNameProvided struct{ gophercloud.ErrMissingInput }
func (e ErrNeitherFlavorIDNorFlavorNameProvided) Error() string {
return "One and only one of the flavor ID and the flavor name must be provided."
}
type ErrNoClientProvidedForIDByName struct{ gophercloud.ErrMissingInput }
func (e ErrNoClientProvidedForIDByName) Error() string {
return "A service client must be provided to find a resource ID by name."
}
// ErrInvalidHowParameterProvided is the error when an unknown value is given
// for the `how` argument
type ErrInvalidHowParameterProvided struct{ gophercloud.ErrInvalidInput }
// ErrNoAdminPassProvided is the error when an administrative password isn't
// provided for a server operation
type ErrNoAdminPassProvided struct{ gophercloud.ErrMissingInput }
// ErrNoImageIDProvided is the error when an image ID isn't provided for a server
// operation
type ErrNoImageIDProvided struct{ gophercloud.ErrMissingInput }
// ErrNoIDProvided is the error when a server ID isn't provided for a server
// operation
type ErrNoIDProvided struct{ gophercloud.ErrMissingInput }
// ErrServer is a generic error type for servers HTTP operations.
type ErrServer struct {
gophercloud.ErrUnexpectedResponseCode
ID string
}
func (se ErrServer) Error() string {
return fmt.Sprintf("Error while executing HTTP request for server [%s]", se.ID)
}
// Error404 overrides the generic 404 error message.
func (se ErrServer) Error404(e gophercloud.ErrUnexpectedResponseCode) error {
se.ErrUnexpectedResponseCode = e
return &ErrServerNotFound{se}
}
// ErrServerNotFound is the error when a 404 is received during server HTTP
// operations.
type ErrServerNotFound struct {
ErrServer
}
func (e ErrServerNotFound) Error() string {
return fmt.Sprintf("I couldn't find server [%s]", e.ID)
}

View File

@ -3,14 +3,13 @@ package servers
import (
"crypto/rsa"
"encoding/base64"
"encoding/json"
"fmt"
"net/url"
"path"
"reflect"
"github.com/mitchellh/mapstructure"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
"github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/pagination"
)
type serverResult struct {
@ -19,29 +18,11 @@ type serverResult struct {
// Extract interprets any serverResult as a Server, if possible.
func (r serverResult) Extract() (*Server, error) {
if r.Err != nil {
return nil, r.Err
var s struct {
Server *Server `json:"server"`
}
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
err := r.ExtractInto(&s)
return s.Server, err
}
// CreateResult temporarily contains the response from a Create call.
@ -94,20 +75,14 @@ type GetPasswordResult struct {
// 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 s struct {
Password string `json:"password"`
}
var response struct {
Password string `mapstructure:"password"`
err := r.ExtractInto(&s)
if err == nil && privateKey != nil && s.Password != "" {
return decryptPassword(s.Password, privateKey)
}
err := mapstructure.Decode(r.Body, &response)
if err == nil && privateKey != nil && response.Password != "" {
return decryptPassword(response.Password, privateKey)
}
return response.Password, err
return s.Password, err
}
func decryptPassword(encryptedPassword string, privateKey *rsa.PrivateKey) (string, error) {
@ -133,83 +108,88 @@ func (res CreateImageResult) ExtractImageID() (string, error) {
// 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())
return "", err
}
imageId := path.Base(u.Path)
if imageId == "." || imageId == "/" {
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
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 s struct {
AdminPass string `json:"adminPass"`
}
var response struct {
AdminPass string `mapstructure:"adminPass"`
}
err := mapstructure.Decode(r.Body, &response)
return response.AdminPass, err
err := r.ExtractInto(&s)
return s.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
ID string `json:"id"`
// TenantID identifies the tenant owning this server resource.
TenantID string `mapstructure:"tenant_id"`
TenantID string `json:"tenant_id"`
// UserID uniquely identifies the user account owning the tenant.
UserID string `mapstructure:"user_id"`
UserID string `json:"user_id"`
// Name contains the human-readable name for the server.
Name string
Name string `json:"name"`
// 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
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{}
Metadata map[string]string
// 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"`
KeyName string `json:"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"`
AdminPass string `json:"adminPass"`
// SecurityGroups includes the security groups that this instance has applied to it
SecurityGroups []map[string]interface{} `json:"security_groups" mapstructure:"security_groups"`
SecurityGroups []map[string]interface{} `json:"security_groups"`
}
func (s *Server) UnmarshalJSON(b []byte) error {
type tmp Server
var server *struct {
tmp
Image interface{}
}
err := json.Unmarshal(b, &server)
if err != nil {
return err
}
*s = Server(server.tmp)
switch t := server.Image.(type) {
case map[string]interface{}:
s.Image = t
case string:
switch t {
case "":
s.Image = nil
}
}
return nil
}
// ServerPage abstracts the raw results of making a List() request against the API.
@ -222,47 +202,28 @@ type ServerPage struct {
// 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
return len(servers) == 0, err
}
// 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 s struct {
Links []gophercloud.Link `json:"servers_links"`
}
var r resp
err := mapstructure.Decode(page.Body, &r)
err := page.ExtractInto(&s)
if err != nil {
return "", err
}
return gophercloud.ExtractNextURL(r.Links)
return gophercloud.ExtractNextURL(s.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"`
func ExtractServers(r pagination.Page) ([]Server, error) {
var s struct {
Servers []Server `json:"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
err := (r.(ServerPage)).ExtractInto(&s)
return s.Servers, err
}
// MetadataResult contains the result of a call for (potentially) multiple key-value pairs.
@ -307,43 +268,26 @@ type DeleteMetadatumResult struct {
// 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 s struct {
Metadata map[string]string `json:"metadata"`
}
var response struct {
Metadata map[string]string `mapstructure:"metadata"`
}
err := mapstructure.Decode(r.Body, &response)
return response.Metadata, err
err := r.ExtractInto(&s)
return s.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 s struct {
Metadatum map[string]string `json:"meta"`
}
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
err := r.ExtractInto(&s)
return s.Metadatum, err
}
// Address represents an IP address.
type Address struct {
Version int `mapstructure:"version"`
Address string `mapstructure:"addr"`
Version int `json:"version"`
Address string `json:"addr"`
}
// AddressPage abstracts the raw results of making a ListAddresses() request against the API.
@ -356,27 +300,17 @@ type AddressPage struct {
// 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
return len(addresses) == 0, err
}
// 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"`
func ExtractAddresses(r pagination.Page) (map[string][]Address, error) {
var s struct {
Addresses map[string][]Address `json:"addresses"`
}
err := mapstructure.Decode(casted, &response)
if err != nil {
return nil, err
}
return response.Addresses, err
err := (r.(AddressPage)).ExtractInto(&s)
return s.Addresses, err
}
// NetworkAddressPage abstracts the raw results of making a ListAddressesByNetwork() request against the API.
@ -389,27 +323,22 @@ type NetworkAddressPage struct {
// 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
return len(addresses) == 0, err
}
// 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)
func ExtractNetworkAddresses(r pagination.Page) ([]Address, error) {
var s map[string][]Address
err := (r.(NetworkAddressPage)).ExtractInto(&s)
if err != nil {
return nil, err
}
var key string
for k := range response {
for k := range s {
key = k
}
return response[key], err
return s[key], err
}

View File

@ -1,6 +1,6 @@
package servers
import "github.com/rackspace/gophercloud"
import "github.com/gophercloud/gophercloud"
func createURL(client *gophercloud.ServiceClient) string {
return client.ServiceURL("servers")

View File

@ -1,6 +1,6 @@
package servers
import "github.com/rackspace/gophercloud"
import "github.com/gophercloud/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.

View File

@ -1,11 +1,9 @@
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"
"github.com/gophercloud/gophercloud"
tokens2 "github.com/gophercloud/gophercloud/openstack/identity/v2/tokens"
tokens3 "github.com/gophercloud/gophercloud/openstack/identity/v3/tokens"
)
// V2EndpointURL discovers the endpoint URL for a specific service from a ServiceCatalog acquired
@ -29,7 +27,9 @@ func V2EndpointURL(catalog *tokens2.ServiceCatalog, opts gophercloud.EndpointOpt
// Report an error if the options were ambiguous.
if len(endpoints) > 1 {
return "", fmt.Errorf("Discovered %d matching endpoints: %#v", len(endpoints), endpoints)
err := &ErrMultipleMatchingEndpointsV2{}
err.Endpoints = endpoints
return "", err
}
// Extract the appropriate URL from the matching Endpoint.
@ -42,12 +42,16 @@ func V2EndpointURL(catalog *tokens2.ServiceCatalog, opts gophercloud.EndpointOpt
case gophercloud.AvailabilityAdmin:
return gophercloud.NormalizeURL(endpoint.AdminURL), nil
default:
return "", fmt.Errorf("Unexpected availability in endpoint query: %s", opts.Availability)
err := &ErrInvalidAvailabilityProvided{}
err.Argument = "Availability"
err.Value = opts.Availability
return "", err
}
}
// Report an error if there were no matching endpoints.
return "", gophercloud.ErrEndpointNotFound
err := &gophercloud.ErrEndpointNotFound{}
return "", err
}
// V3EndpointURL discovers the endpoint URL for a specific service from a Catalog acquired
@ -66,7 +70,10 @@ func V3EndpointURL(catalog *tokens3.ServiceCatalog, opts gophercloud.EndpointOpt
if opts.Availability != gophercloud.AvailabilityAdmin &&
opts.Availability != gophercloud.AvailabilityPublic &&
opts.Availability != gophercloud.AvailabilityInternal {
return "", fmt.Errorf("Unexpected availability in endpoint query: %s", opts.Availability)
err := &ErrInvalidAvailabilityProvided{}
err.Argument = "Availability"
err.Value = opts.Availability
return "", err
}
if (opts.Availability == gophercloud.Availability(endpoint.Interface)) &&
(opts.Region == "" || endpoint.Region == opts.Region) {
@ -78,7 +85,7 @@ func V3EndpointURL(catalog *tokens3.ServiceCatalog, opts gophercloud.EndpointOpt
// Report an error if the options were ambiguous.
if len(endpoints) > 1 {
return "", fmt.Errorf("Discovered %d matching endpoints: %#v", len(endpoints), endpoints)
return "", ErrMultipleMatchingEndpointsV3{Endpoints: endpoints}
}
// Extract the URL from the matching Endpoint.
@ -87,5 +94,6 @@ func V3EndpointURL(catalog *tokens3.ServiceCatalog, opts gophercloud.EndpointOpt
}
// Report an error if there were no matching endpoints.
return "", gophercloud.ErrEndpointNotFound
err := &gophercloud.ErrEndpointNotFound{}
return "", err
}

View File

@ -0,0 +1,71 @@
package openstack
import (
"fmt"
"github.com/gophercloud/gophercloud"
tokens2 "github.com/gophercloud/gophercloud/openstack/identity/v2/tokens"
tokens3 "github.com/gophercloud/gophercloud/openstack/identity/v3/tokens"
)
// ErrEndpointNotFound is the error when no suitable endpoint can be found
// in the user's catalog
type ErrEndpointNotFound struct{ gophercloud.BaseError }
func (e ErrEndpointNotFound) Error() string {
return "No suitable endpoint could be found in the service catalog."
}
// ErrInvalidAvailabilityProvided is the error when an invalid endpoint
// availability is provided
type ErrInvalidAvailabilityProvided struct{ gophercloud.ErrInvalidInput }
func (e ErrInvalidAvailabilityProvided) Error() string {
return fmt.Sprintf("Unexpected availability in endpoint query: %s", e.Value)
}
// ErrMultipleMatchingEndpointsV2 is the error when more than one endpoint
// for the given options is found in the v2 catalog
type ErrMultipleMatchingEndpointsV2 struct {
gophercloud.BaseError
Endpoints []tokens2.Endpoint
}
func (e ErrMultipleMatchingEndpointsV2) Error() string {
return fmt.Sprintf("Discovered %d matching endpoints: %#v", len(e.Endpoints), e.Endpoints)
}
// ErrMultipleMatchingEndpointsV3 is the error when more than one endpoint
// for the given options is found in the v3 catalog
type ErrMultipleMatchingEndpointsV3 struct {
gophercloud.BaseError
Endpoints []tokens3.Endpoint
}
func (e ErrMultipleMatchingEndpointsV3) Error() string {
return fmt.Sprintf("Discovered %d matching endpoints: %#v", len(e.Endpoints), e.Endpoints)
}
// ErrNoAuthURL is the error when the OS_AUTH_URL environment variable is not
// found
type ErrNoAuthURL struct{ gophercloud.ErrInvalidInput }
func (e ErrNoAuthURL) Error() string {
return "Environment variable OS_AUTH_URL needs to be set."
}
// ErrNoUsername is the error when the OS_USERNAME environment variable is not
// found
type ErrNoUsername struct{ gophercloud.ErrInvalidInput }
func (e ErrNoUsername) Error() string {
return "Environment variable OS_USERNAME needs to be set."
}
// ErrNoPassword is the error when the OS_PASSWORD environment variable is not
// found
type ErrNoPassword struct{ gophercloud.ErrInvalidInput }
func (e ErrNoPassword) Error() string {
return "Environment variable OS_PASSWORD needs to be set."
}

View File

@ -1,25 +1,20 @@
package tenants
import (
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
"github.com/gophercloud/gophercloud"
"github.com/gophercloud/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)
@ -28,6 +23,7 @@ func List(client *gophercloud.ServiceClient, opts *ListOpts) pagination.Pager {
}
url += q.String()
}
return pagination.NewPager(client, url, createPage)
return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page {
return TenantPage{pagination.LinkedPageBase{PageResult: r}}
})
}

View File

@ -0,0 +1,53 @@
package tenants
import (
"github.com/gophercloud/gophercloud"
"github.com/gophercloud/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 `json:"id"`
// Name is a friendlier user-facing name for this tenant.
Name string `json:"name"`
// Description is a human-readable explanation of this Tenant's purpose.
Description string `json:"description"`
// Enabled indicates whether or not a tenant is active.
Enabled bool `json:"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 (r TenantPage) IsEmpty() (bool, error) {
tenants, err := ExtractTenants(r)
return len(tenants) == 0, err
}
// NextPageURL extracts the "next" link from the tenants_links section of the result.
func (r TenantPage) NextPageURL() (string, error) {
var s struct {
Links []gophercloud.Link `json:"tenants_links"`
}
err := r.ExtractInto(&s)
if err != nil {
return "", err
}
return gophercloud.ExtractNextURL(s.Links)
}
// ExtractTenants returns a slice of Tenants contained in a single page of results.
func ExtractTenants(r pagination.Page) ([]Tenant, error) {
var s struct {
Tenants []Tenant `json:"tenants"`
}
err := (r.(TenantPage)).ExtractInto(&s)
return s.Tenants, err
}

View File

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

View File

@ -0,0 +1,99 @@
package tokens
import "github.com/gophercloud/gophercloud"
type PasswordCredentialsV2 struct {
Username string `json:"username" required:"true"`
Password string `json:"password" required:"true"`
}
type TokenCredentialsV2 struct {
ID string `json:"id,omitempty" required:"true"`
}
// AuthOptionsV2 wraps a gophercloud AuthOptions in order to adhere to the AuthOptionsBuilder
// interface.
type AuthOptionsV2 struct {
PasswordCredentials *PasswordCredentialsV2 `json:"passwordCredentials,omitempty" xor:"TokenCredentials"`
// 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 string `json:"tenantId,omitempty"`
TenantName string `json:"tenantName,omitempty"`
// TokenCredentials allows users to authenticate (possibly as another user) with an
// authentication token ID.
TokenCredentials *TokenCredentialsV2 `json:"token,omitempty" xor:"PasswordCredentials"`
}
// 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.
ToTokenV2CreateMap() (map[string]interface{}, error)
}
// AuthOptions are the valid options for Openstack Identity v2 authentication.
// For field descriptions, see gophercloud.AuthOptions.
type AuthOptions struct {
IdentityEndpoint string `json:"-"`
Username string `json:"username,omitempty"`
Password string `json:"password,omitempty"`
TenantID string `json:"tenantId,omitempty"`
TenantName string `json:"tenantName,omitempty"`
AllowReauth bool `json:"-"`
TokenID string
}
// ToTokenV2CreateMap allows AuthOptions to satisfy the AuthOptionsBuilder
// interface in the v2 tokens package
func (opts AuthOptions) ToTokenV2CreateMap() (map[string]interface{}, error) {
v2Opts := AuthOptionsV2{
TenantID: opts.TenantID,
TenantName: opts.TenantName,
}
if opts.Password != "" {
v2Opts.PasswordCredentials = &PasswordCredentialsV2{
Username: opts.Username,
Password: opts.Password,
}
} else {
v2Opts.TokenCredentials = &TokenCredentialsV2{
ID: opts.TokenID,
}
}
b, err := gophercloud.BuildRequestBody(v2Opts, "auth")
if err != nil {
return nil, err
}
return b, 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) (r CreateResult) {
b, err := auth.ToTokenV2CreateMap()
if err != nil {
r.Err = err
return
}
_, r.Err = client.Post(CreateURL(client), b, &r.Body, &gophercloud.RequestOpts{
OkCodes: []int{200, 203},
MoreHeaders: map[string]string{"X-Auth-Token": ""},
})
return
}
// Get validates and retrieves information for user's token.
func Get(client *gophercloud.ServiceClient, token string) (r GetResult) {
_, r.Err = client.Get(GetURL(client, token), &r.Body, &gophercloud.RequestOpts{
OkCodes: []int{200, 203},
})
return
}

View File

@ -3,9 +3,8 @@ package tokens
import (
"time"
"github.com/mitchellh/mapstructure"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/openstack/identity/v2/tenants"
"github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/openstack/identity/v2/tenants"
)
// Token provides only the most basic information related to an authentication token.
@ -25,15 +24,17 @@ type Token struct {
Tenant tenants.Tenant
}
// Authorization need user info which can get from token authentication's response
// Role is a role for a user.
type Role struct {
Name string `mapstructure:"name"`
Name string `json:"name"`
}
// User is an OpenStack user.
type User struct {
ID string `mapstructure:"id"`
Name string `mapstructure:"name"`
UserName string `mapstructure:"username"`
Roles []Role `mapstructure:"roles"`
ID string `json:"id"`
Name string `json:"name"`
UserName string `json:"username"`
Roles []Role `json:"roles"`
}
// Endpoint represents a single API endpoint offered by a service.
@ -45,14 +46,14 @@ type User struct {
//
// 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"`
TenantID string `json:"tenantId"`
PublicURL string `json:"publicURL"`
InternalURL string `json:"internalURL"`
AdminURL string `json:"adminURL"`
Region string `json:"region"`
VersionID string `json:"versionId"`
VersionInfo string `json:"versionInfo"`
VersionList string `json:"versionList"`
}
// CatalogEntry provides a type-safe interface to an Identity API V2 service catalog listing.
@ -63,15 +64,15 @@ type Endpoint struct {
// 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"`
Name string `json:"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"`
Type string `json:"type"`
// Endpoints will let the caller iterate over all the different endpoints that may exist for
// the service.
Endpoints []Endpoint `mapstructure:"endpoints"`
Endpoints []Endpoint `json:"endpoints"`
}
// ServiceCatalog provides a view into the service catalog from a previous, successful authentication.
@ -92,56 +93,43 @@ type GetResult struct {
}
// 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 {
func (r CreateResult) ExtractToken() (*Token, error) {
var s struct {
Access struct {
Token struct {
Expires string `mapstructure:"expires"`
ID string `mapstructure:"id"`
Tenant tenants.Tenant `mapstructure:"tenant"`
} `mapstructure:"token"`
} `mapstructure:"access"`
Expires string `json:"expires"`
ID string `json:"id"`
Tenant tenants.Tenant `json:"tenant"`
} `json:"token"`
} `json:"access"`
}
err := mapstructure.Decode(result.Body, &response)
err := r.ExtractInto(&s)
if err != nil {
return nil, err
}
expiresTs, err := time.Parse(gophercloud.RFC3339Milli, response.Access.Token.Expires)
expiresTs, err := time.Parse(gophercloud.RFC3339Milli, s.Access.Token.Expires)
if err != nil {
return nil, err
}
return &Token{
ID: response.Access.Token.ID,
ID: s.Access.Token.ID,
ExpiresAt: expiresTs,
Tenant: response.Access.Token.Tenant,
Tenant: s.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 {
func (r CreateResult) ExtractServiceCatalog() (*ServiceCatalog, error) {
var s struct {
Access struct {
Entries []CatalogEntry `mapstructure:"serviceCatalog"`
} `mapstructure:"access"`
Entries []CatalogEntry `json:"serviceCatalog"`
} `json:"access"`
}
err := mapstructure.Decode(result.Body, &response)
if err != nil {
return nil, err
}
return &ServiceCatalog{Entries: response.Access.Entries}, nil
err := r.ExtractInto(&s)
return &ServiceCatalog{Entries: s.Access.Entries}, err
}
// createErr quickly packs an error in a CreateResult.
@ -150,21 +138,12 @@ func createErr(err error) CreateResult {
}
// ExtractUser returns the User from a GetResult.
func (result GetResult) ExtractUser() (*User, error) {
if result.Err != nil {
return nil, result.Err
}
var response struct {
func (r GetResult) ExtractUser() (*User, error) {
var s struct {
Access struct {
User User `mapstructure:"user"`
} `mapstructure:"access"`
User User `json:"user"`
} `json:"access"`
}
err := mapstructure.Decode(result.Body, &response)
if err != nil {
return nil, err
}
return &response.Access.User, nil
err := r.ExtractInto(&s)
return &s.Access.User, err
}

View File

@ -1,6 +1,6 @@
package tokens
import "github.com/rackspace/gophercloud"
import "github.com/gophercloud/gophercloud"
// CreateURL generates the URL used to create new Tokens.
func CreateURL(client *gophercloud.ServiceClient) string {

View File

@ -0,0 +1,200 @@
package tokens
import "github.com/gophercloud/gophercloud"
// Scope allows a created token to be limited to a specific domain or project.
type Scope struct {
ProjectID string `json:"scope.project.id,omitempty" not:"ProjectName,DomainID,DomainName"`
ProjectName string `json:"scope.project.name,omitempty"`
DomainID string `json:"scope.project.id,omitempty" not:"ProjectName,ProjectID,DomainName"`
DomainName string `json:"scope.project.id,omitempty"`
}
// AuthOptionsBuilder describes any argument that may be passed to the Create call.
type AuthOptionsBuilder interface {
// ToTokenV3CreateMap assembles the Create request body, returning an error if parameters are
// missing or inconsistent.
ToTokenV3CreateMap(map[string]interface{}) (map[string]interface{}, error)
ToTokenV3ScopeMap() (map[string]interface{}, error)
CanReauth() bool
}
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 `json:"-"`
// 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 string `json:"username,omitempty"`
UserID string `json:"id,omitempty"`
Password string `json:"password,omitempty"`
// At most one of DomainID and DomainName must be provided if using Username
// with Identity V3. Otherwise, either are optional.
DomainID string `json:"id,omitempty"`
DomainName string `json:"name,omitempty"`
// 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.
AllowReauth bool `json:"-"`
// TokenID allows users to authenticate (possibly as another user) with an
// authentication token ID.
TokenID string `json:"-"`
Scope Scope `json:"-"`
}
func (opts *AuthOptions) ToTokenV3CreateMap(scope map[string]interface{}) (map[string]interface{}, error) {
gophercloudAuthOpts := gophercloud.AuthOptions{
Username: opts.Username,
UserID: opts.UserID,
Password: opts.Password,
DomainID: opts.DomainID,
DomainName: opts.DomainName,
AllowReauth: opts.AllowReauth,
TokenID: opts.TokenID,
}
return gophercloudAuthOpts.ToTokenV3CreateMap(scope)
}
func (opts *AuthOptions) ToTokenV3ScopeMap() (map[string]interface{}, error) {
if opts.Scope.ProjectName != "" {
// ProjectName provided: either DomainID or DomainName must also be supplied.
// ProjectID may not be supplied.
if opts.Scope.DomainID == "" && opts.Scope.DomainName == "" {
return nil, gophercloud.ErrScopeDomainIDOrDomainName{}
}
if opts.Scope.ProjectID != "" {
return nil, gophercloud.ErrScopeProjectIDOrProjectName{}
}
if opts.Scope.DomainID != "" {
// ProjectName + DomainID
return map[string]interface{}{
"project": map[string]interface{}{
"name": &opts.Scope.ProjectName,
"domain": map[string]interface{}{"id": &opts.Scope.DomainID},
},
}, nil
}
if opts.Scope.DomainName != "" {
// ProjectName + DomainName
return map[string]interface{}{
"project": map[string]interface{}{
"name": &opts.Scope.ProjectName,
"domain": map[string]interface{}{"name": &opts.Scope.DomainName},
},
}, nil
}
} else if opts.Scope.ProjectID != "" {
// ProjectID provided. ProjectName, DomainID, and DomainName may not be provided.
if opts.Scope.DomainID != "" {
return nil, gophercloud.ErrScopeProjectIDAlone{}
}
if opts.Scope.DomainName != "" {
return nil, gophercloud.ErrScopeProjectIDAlone{}
}
// ProjectID
return map[string]interface{}{
"project": map[string]interface{}{
"id": &opts.Scope.ProjectID,
},
}, nil
} else if opts.Scope.DomainID != "" {
// DomainID provided. ProjectID, ProjectName, and DomainName may not be provided.
if opts.Scope.DomainName != "" {
return nil, gophercloud.ErrScopeDomainIDOrDomainName{}
}
// DomainID
return map[string]interface{}{
"domain": map[string]interface{}{
"id": &opts.Scope.DomainID,
},
}, nil
} else if opts.Scope.DomainName != "" {
return nil, gophercloud.ErrScopeDomainName{}
}
return nil, nil
}
func (opts *AuthOptions) CanReauth() bool {
return opts.AllowReauth
}
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, opts AuthOptionsBuilder) (r CreateResult) {
scope, err := opts.ToTokenV3ScopeMap()
if err != nil {
r.Err = err
return
}
b, err := opts.ToTokenV3CreateMap(scope)
if err != nil {
r.Err = err
return
}
resp, err := c.Post(tokenURL(c), b, &r.Body, &gophercloud.RequestOpts{
MoreHeaders: map[string]string{"X-Auth-Token": ""},
})
r.Err = err
if resp != nil {
r.Header = resp.Header
}
return
}
// Get validates and retrieves information about another token.
func Get(c *gophercloud.ServiceClient, token string) (r GetResult) {
resp, err := c.Get(tokenURL(c), &r.Body, &gophercloud.RequestOpts{
MoreHeaders: subjectTokenHeaders(c, token),
OkCodes: []int{200, 203},
})
if resp != nil {
r.Err = err
r.Header = resp.Header
}
return
}
// Validate determines if a specified token is valid or not.
func Validate(c *gophercloud.ServiceClient, token string) (bool, error) {
resp, err := c.Request("HEAD", tokenURL(c), &gophercloud.RequestOpts{
MoreHeaders: subjectTokenHeaders(c, token),
OkCodes: []int{204, 404},
})
if err != nil {
return false, err
}
return resp.StatusCode == 204, nil
}
// Revoke immediately makes specified token invalid.
func Revoke(c *gophercloud.ServiceClient, token string) (r RevokeResult) {
_, r.Err = c.Delete(tokenURL(c), &gophercloud.RequestOpts{
MoreHeaders: subjectTokenHeaders(c, token),
})
return
}

View File

@ -1,21 +1,17 @@
package tokens
import (
"time"
"github.com/mitchellh/mapstructure"
"github.com/rackspace/gophercloud"
)
import "errors"
import "github.com/gophercloud/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"`
ID string `json:"id"`
Region string `json:"region"`
Interface string `json:"interface"`
URL string `json:"url"`
}
// CatalogEntry provides a type-safe interface to an Identity API V3 service catalog listing.
@ -25,20 +21,16 @@ type Endpoint struct {
// 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"`
ID string `json:"id"`
// Name will contain the provider-specified name for the service.
Name string `mapstructure:"name"`
Name string `json:"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"`
Type string `json:"type"`
// Endpoints will let the caller iterate over all the different endpoints that may exist for
// the service.
Endpoints []Endpoint `mapstructure:"endpoints"`
Endpoints []Endpoint `json:"endpoints"`
}
// ServiceCatalog provides a view into the service catalog from a previous, successful authentication.
@ -59,50 +51,34 @@ func (r commonResult) Extract() (*Token, error) {
// ExtractToken interprets a commonResult as a Token.
func (r commonResult) ExtractToken() (*Token, error) {
if r.Err != nil {
return nil, r.Err
var s struct {
Token *Token `json:"token"`
}
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)
err := r.ExtractInto(&s)
if err != nil {
return nil, err
}
// Attempt to parse the timestamp.
token.ExpiresAt, err = time.Parse(gophercloud.RFC3339Milli, response.Token.ExpiresAt)
if s.Token == nil {
return nil, errors.New("'token' missing in JSON response")
}
return &token, err
// Parse the token itself from the stored headers.
s.Token.ID = r.Header.Get("X-Subject-Token")
return s.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 {
func (r CreateResult) ExtractServiceCatalog() (*ServiceCatalog, error) {
var s struct {
Token struct {
Entries []CatalogEntry `mapstructure:"catalog"`
} `mapstructure:"token"`
Entries []CatalogEntry `json:"catalog"`
} `json:"token"`
}
err := mapstructure.Decode(result.Body, &response)
if err != nil {
return nil, err
}
return &ServiceCatalog{Entries: response.Token.Entries}, nil
err := r.ExtractInto(&s)
return &ServiceCatalog{Entries: s.Token.Entries}, err
}
// CreateResult defers the interpretation of a created token.
@ -132,8 +108,7 @@ type RevokeResult struct {
// Each Token is valid for a set length of time.
type Token struct {
// ID is the issued token.
ID string
ID string `json:"id"`
// ExpiresAt is the timestamp at which this token will no longer be accepted.
ExpiresAt time.Time
ExpiresAt gophercloud.JSONRFC3339Milli `json:"expires_at"`
}

View File

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

View File

@ -0,0 +1,238 @@
package images
import (
"github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/pagination"
)
// ListOptsBuilder allows extensions to add additional parameters to the
// List request.
type ListOptsBuilder interface {
ToImageListQuery() (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.
//http://developer.openstack.org/api-ref-image-v2.html
type ListOpts struct {
// Integer value for the limit of values to return.
Limit int `q:"limit"`
// UUID of the server at which you want to set a marker.
Marker string `q:"marker"`
Name string `q:"name"`
Visibility ImageVisibility `q:"visibility"`
MemberStatus ImageMemberStatus `q:"member_status"`
Owner string `q:"owner"`
Status ImageStatus `q:"status"`
SizeMin int64 `q:"size_min"`
SizeMax int64 `q:"size_max"`
SortKey string `q:"sort_key"`
SortDir string `q:"sort_dir"`
Tag string `q:"tag"`
}
// ToImageListQuery formats a ListOpts into a query string.
func (opts ListOpts) ToImageListQuery() (string, error) {
q, err := gophercloud.BuildQueryString(opts)
return q.String(), err
}
// List implements image list request
func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
url := listURL(c)
if opts != nil {
query, err := opts.ToImageListQuery()
if err != nil {
return pagination.Pager{Err: err}
}
url += query
}
return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page {
return ImagePage{pagination.LinkedPageBase{PageResult: r}}
})
}
// CreateOptsBuilder describes struct types that can be accepted by the Create call.
// The CreateOpts struct in this package does.
type CreateOptsBuilder interface {
// Returns value that can be passed to json.Marshal
ToImageCreateMap() (map[string]interface{}, error)
}
// CreateOpts implements CreateOptsBuilder
type CreateOpts struct {
// Name is the name of the new image.
Name string `json:"name" required:"true"`
// Id is the the image ID.
ID string `json:"id,omitempty"`
// Visibility defines who can see/use the image.
Visibility *ImageVisibility `json:"visibility,omitempty"`
// Tags is a set of image tags.
Tags []string `json:"tags,omitempty"`
// ContainerFormat is the format of the
// container. Valid values are ami, ari, aki, bare, and ovf.
ContainerFormat string `json:"container_format,omitempty"`
// DiskFormat is the format of the disk. If set,
// valid values are ami, ari, aki, vhd, vmdk, raw, qcow2, vdi,
// and iso.
DiskFormat string `json:"disk_format,omitempty"`
// MinDisk is the amount of disk space in
// GB that is required to boot the image.
MinDisk int `json:"min_disk,omitempty"`
// MinRAM is the amount of RAM in MB that
// is required to boot the image.
MinRAM int `json:"min_ram,omitempty"`
// protected is whether the image is not deletable.
Protected *bool `json:"protected,omitempty"`
// properties is a set of properties, if any, that
// are associated with the image.
Properties map[string]string `json:"-,omitempty"`
}
// ToImageCreateMap assembles a request body based on the contents of
// a CreateOpts.
func (opts CreateOpts) ToImageCreateMap() (map[string]interface{}, error) {
b, err := gophercloud.BuildRequestBody(opts, "")
if err != nil {
return nil, err
}
if opts.Properties != nil {
for k, v := range opts.Properties {
b[k] = v
}
}
return b, nil
}
// Create implements create image request
func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) {
b, err := opts.ToImageCreateMap()
if err != nil {
r.Err = err
return r
}
_, r.Err = client.Post(createURL(client), b, &r.Body, &gophercloud.RequestOpts{OkCodes: []int{201}})
return
}
// Delete implements image delete request
func Delete(client *gophercloud.ServiceClient, id string) (r DeleteResult) {
_, r.Err = client.Delete(deleteURL(client, id), nil)
return
}
// Get implements image get request
func Get(client *gophercloud.ServiceClient, id string) (r GetResult) {
_, r.Err = client.Get(getURL(client, id), &r.Body, nil)
return
}
// Update implements image updated request
func Update(client *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) {
b, err := opts.ToImageUpdateMap()
if err != nil {
r.Err = err
return r
}
_, r.Err = client.Patch(updateURL(client, id), b, &r.Body, &gophercloud.RequestOpts{
OkCodes: []int{200},
MoreHeaders: map[string]string{"Content-Type": "application/openstack-images-v2.1-json-patch"},
})
return
}
// UpdateOptsBuilder implements UpdateOptsBuilder
type UpdateOptsBuilder interface {
// returns value implementing json.Marshaler which when marshaled matches the patch schema:
// http://specs.openstack.org/openstack/glance-specs/specs/api/v2/http-patch-image-api-v2.html
ToImageUpdateMap() ([]interface{}, error)
}
// UpdateOpts implements UpdateOpts
type UpdateOpts []Patch
// ToImageUpdateMap builder
func (opts UpdateOpts) ToImageUpdateMap() ([]interface{}, error) {
m := make([]interface{}, len(opts))
for i, patch := range opts {
patchJSON := patch.ToImagePatchMap()
m[i] = patchJSON
}
return m, nil
}
// Patch represents a single update to an existing image. Multiple updates to an image can be
// submitted at the same time.
type Patch interface {
ToImagePatchMap() map[string]interface{}
}
// UpdateVisibility updated visibility
type UpdateVisibility struct {
Visibility ImageVisibility
}
// ToImagePatchMap builder
func (u UpdateVisibility) ToImagePatchMap() map[string]interface{} {
return map[string]interface{}{
"op": "replace",
"path": "/visibility",
"value": u.Visibility,
}
}
// ReplaceImageName implements Patch
type ReplaceImageName struct {
NewName string
}
// ToImagePatchMap builder
func (r ReplaceImageName) ToImagePatchMap() map[string]interface{} {
return map[string]interface{}{
"op": "replace",
"path": "/name",
"value": r.NewName,
}
}
// ReplaceImageChecksum implements Patch
type ReplaceImageChecksum struct {
Checksum string
}
// ReplaceImageChecksum builder
func (rc ReplaceImageChecksum) ToImagePatchMap() map[string]interface{} {
return map[string]interface{}{
"op": "replace",
"path": "/checksum",
"value": rc.Checksum,
}
}
// ReplaceImageTags implements Patch
type ReplaceImageTags struct {
NewTags []string
}
// ToImagePatchMap builder
func (r ReplaceImageTags) ToImagePatchMap() map[string]interface{} {
return map[string]interface{}{
"op": "replace",
"path": "/tags",
"value": r.NewTags,
}
}

View File

@ -0,0 +1,176 @@
package images
import (
"encoding/json"
"fmt"
"reflect"
"time"
"github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/pagination"
)
// Image model
// Does not include the literal image data; just metadata.
// returned by listing images, and by fetching a specific image.
type Image struct {
// ID is the image UUID
ID string `json:"id"`
// Name is the human-readable display name for the image.
Name string `json:"name"`
// Status is the image status. It can be "queued" or "active"
// See imageservice/v2/images/type.go
Status ImageStatus `json:"status"`
// Tags is a list of image tags. Tags are arbitrarily defined strings
// attached to an image.
Tags []string `json:"tags"`
// ContainerFormat is the format of the container.
// Valid values are ami, ari, aki, bare, and ovf.
ContainerFormat string `json:"container_format"`
// DiskFormat is the format of the disk.
// If set, valid values are ami, ari, aki, vhd, vmdk, raw, qcow2, vdi, and iso.
DiskFormat string `json:"disk_format"`
// MinDiskGigabytes is the amount of disk space in GB that is required to boot the image.
MinDiskGigabytes int `json:"min_disk"`
// MinRAMMegabytes [optional] is the amount of RAM in MB that is required to boot the image.
MinRAMMegabytes int `json:"min_ram"`
// Owner is the tenant the image belongs to.
Owner string `json:"owner"`
// Protected is whether the image is deletable or not.
Protected bool `json:"protected"`
// Visibility defines who can see/use the image.
Visibility ImageVisibility `json:"visibility"`
// Checksum is the checksum of the data that's associated with the image
Checksum string `json:"checksum"`
// SizeBytes is the size of the data that's associated with the image.
SizeBytes int64 `json:"size"`
// Metadata is a set of metadata associated with the image.
// Image metadata allow for meaningfully define the image properties
// and tags. See http://docs.openstack.org/developer/glance/metadefs-concepts.html.
Metadata map[string]string `json:"metadata"`
// Properties is a set of key-value pairs, if any, that are associated with the image.
Properties map[string]string `json:"properties"`
// CreatedAt is the date when the image has been created.
CreatedAt time.Time `json:"-"`
// UpdatedAt is the date when the last change has been made to the image or it's properties.
UpdatedAt time.Time `json:"-"`
// File is the trailing path after the glance endpoint that represent the location
// of the image or the path to retrieve it.
File string `json:"file"`
// Schema is the path to the JSON-schema that represent the image or image entity.
Schema string `json:"schema"`
}
func (s *Image) UnmarshalJSON(b []byte) error {
type tmp Image
var p *struct {
tmp
SizeBytes interface{} `json:"size"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
err := json.Unmarshal(b, &p)
if err != nil {
return err
}
*s = Image(p.tmp)
switch t := p.SizeBytes.(type) {
case nil:
return nil
case float32:
s.SizeBytes = int64(t)
case float64:
s.SizeBytes = int64(t)
default:
return fmt.Errorf("Unknown type for SizeBytes: %v (value: %v)", reflect.TypeOf(t), t)
}
s.CreatedAt, err = time.Parse(time.RFC3339, p.CreatedAt)
if err != nil {
return err
}
s.UpdatedAt, err = time.Parse(time.RFC3339, p.UpdatedAt)
return err
}
type commonResult struct {
gophercloud.Result
}
// Extract interprets any commonResult as an Image.
func (r commonResult) Extract() (*Image, error) {
var s *Image
err := r.ExtractInto(&s)
return s, err
}
// CreateResult represents the result of a Create operation
type CreateResult struct {
commonResult
}
// UpdateResult represents the result of an Update operation
type UpdateResult struct {
commonResult
}
// GetResult represents the result of a Get operation
type GetResult struct {
commonResult
}
//DeleteResult model
type DeleteResult struct {
gophercloud.ErrResult
}
// ImagePage represents page
type ImagePage struct {
pagination.LinkedPageBase
}
// IsEmpty returns true if a page contains no Images results.
func (r ImagePage) IsEmpty() (bool, error) {
images, err := ExtractImages(r)
return len(images) == 0, err
}
// NextPageURL uses the response's embedded link reference to navigate to the next page of results.
func (r ImagePage) NextPageURL() (string, error) {
var s struct {
Next string `json:"next"`
}
err := r.ExtractInto(&s)
if err != nil {
return "", err
}
return nextPageURL(r.URL.String(), s.Next), nil
}
// ExtractImages interprets the results of a single page from a List() call, producing a slice of Image entities.
func ExtractImages(r pagination.Page) ([]Image, error) {
var s struct {
Images []Image `json:"images"`
}
err := (r.(ImagePage)).ExtractInto(&s)
return s.Images, err
}

View File

@ -0,0 +1,75 @@
package images
// ImageStatus image statuses
// http://docs.openstack.org/developer/glance/statuses.html
type ImageStatus string
const (
// ImageStatusQueued is a status for an image which identifier has
// been reserved for an image in the image registry.
ImageStatusQueued ImageStatus = "queued"
// ImageStatusSaving denotes that an images raw data is currently being uploaded to Glance
ImageStatusSaving ImageStatus = "saving"
// ImageStatusActive denotes an image that is fully available in Glance.
ImageStatusActive ImageStatus = "active"
// ImageStatusKilled denotes that an error occurred during the uploading
// of an images data, and that the image is not readable.
ImageStatusKilled ImageStatus = "killed"
// ImageStatusDeleted is used for an image that is no longer available to use.
// The image information is retained in the image registry.
ImageStatusDeleted ImageStatus = "deleted"
// ImageStatusPendingDelete is similar to Delete, but the image is not yet deleted.
ImageStatusPendingDelete ImageStatus = "pending_delete"
// ImageStatusDeactivated denotes that access to image data is not allowed to any non-admin user.
ImageStatusDeactivated ImageStatus = "deactivated"
)
// ImageVisibility denotes an image that is fully available in Glance.
// This occurs when the image data is uploaded, or the image size
// is explicitly set to zero on creation.
// According to design
// https://wiki.openstack.org/wiki/Glance-v2-community-image-visibility-design
type ImageVisibility string
const (
// ImageVisibilityPublic all users
ImageVisibilityPublic ImageVisibility = "public"
// ImageVisibilityPrivate users with tenantId == tenantId(owner)
ImageVisibilityPrivate ImageVisibility = "private"
// ImageVisibilityShared images are visible to:
// - users with tenantId == tenantId(owner)
// - users with tenantId in the member-list of the image
// - users with tenantId in the member-list with member_status == 'accepted'
ImageVisibilityShared ImageVisibility = "shared"
// ImageVisibilityCommunity images:
// - all users can see and boot it
// - users with tenantId in the member-list of the image with member_status == 'accepted'
// have this image in their default image-list
ImageVisibilityCommunity ImageVisibility = "community"
)
// MemberStatus is a status for adding a new member (tenant) to an image member list.
type ImageMemberStatus string
const (
// ImageMemberStatusAccepted is the status for an accepted image member.
ImageMemberStatusAccepted ImageMemberStatus = "accepted"
// ImageMemberStatusPending shows that the member addition is pending
ImageMemberStatusPending ImageMemberStatus = "pending"
// ImageMemberStatusAccepted is the status for a rejected image member
ImageMemberStatusRejected ImageMemberStatus = "rejected"
// ImageMemberStatusAll
ImageMemberStatusAll ImageMemberStatus = "all"
)

View File

@ -0,0 +1,44 @@
package images
import (
"strings"
"github.com/gophercloud/gophercloud"
)
// `listURL` is a pure function. `listURL(c)` is a URL for which a GET
// request will respond with a list of images in the service `c`.
func listURL(c *gophercloud.ServiceClient) string {
return c.ServiceURL("images")
}
func createURL(c *gophercloud.ServiceClient) string {
return c.ServiceURL("images")
}
// `imageURL(c,i)` is the URL for the image identified by ID `i` in
// the service `c`.
func imageURL(c *gophercloud.ServiceClient, imageID string) string {
return c.ServiceURL("images", imageID)
}
// `getURL(c,i)` is a URL for which a GET request will respond with
// information about the image identified by ID `i` in the service
// `c`.
func getURL(c *gophercloud.ServiceClient, imageID string) string {
return imageURL(c, imageID)
}
func updateURL(c *gophercloud.ServiceClient, imageID string) string {
return imageURL(c, imageID)
}
func deleteURL(c *gophercloud.ServiceClient, imageID string) string {
return imageURL(c, imageID)
}
// builds next page full url based on current url
func nextPageURL(currentURL string, next string) string {
base := currentURL[:strings.Index(currentURL, "/images")]
return base + next
}

View File

@ -0,0 +1,77 @@
package members
import (
"github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/pagination"
)
// Create member for specific image
//
// Preconditions
// The specified images must exist.
// You can only add a new member to an image which 'visibility' attribute is private.
// You must be the owner of the specified image.
// Synchronous Postconditions
// With correct permissions, you can see the member status of the image as pending through API calls.
//
// More details here: http://developer.openstack.org/api-ref-image-v2.html#createImageMember-v2
func Create(client *gophercloud.ServiceClient, id string, member string) (r CreateResult) {
b := map[string]interface{}{"member": member}
_, r.Err = client.Post(createMemberURL(client, id), b, &r.Body, &gophercloud.RequestOpts{
OkCodes: []int{200, 409, 403},
})
return
}
// List members returns list of members for specifed image id
// More details: http://developer.openstack.org/api-ref-image-v2.html#listImageMembers-v2
func List(client *gophercloud.ServiceClient, id string) pagination.Pager {
return pagination.NewPager(client, listMembersURL(client, id), func(r pagination.PageResult) pagination.Page {
return MemberPage{pagination.SinglePageBase(r)}
})
}
// Get image member details.
// More details: http://developer.openstack.org/api-ref-image-v2.html#getImageMember-v2
func Get(client *gophercloud.ServiceClient, imageID string, memberID string) (r DetailsResult) {
_, r.Err = client.Get(getMemberURL(client, imageID, memberID), &r.Body, &gophercloud.RequestOpts{OkCodes: []int{200}})
return
}
// Delete membership for given image.
// Callee should be image owner
// More details: http://developer.openstack.org/api-ref-image-v2.html#deleteImageMember-v2
func Delete(client *gophercloud.ServiceClient, imageID string, memberID string) (r DeleteResult) {
_, r.Err = client.Delete(deleteMemberURL(client, imageID, memberID), &gophercloud.RequestOpts{OkCodes: []int{204, 403}})
return
}
// UpdateOptsBuilder allows extensions to add additional attributes to the Update request.
type UpdateOptsBuilder interface {
ToImageMemberUpdateMap() (map[string]interface{}, error)
}
// UpdateOpts implements UpdateOptsBuilder
type UpdateOpts struct {
Status string
}
// ToMemberUpdateMap formats an UpdateOpts structure into a request body.
func (opts UpdateOpts) ToImageMemberUpdateMap() (map[string]interface{}, error) {
return map[string]interface{}{
"status": opts.Status,
}, nil
}
// Update function updates member
// More details: http://developer.openstack.org/api-ref-image-v2.html#updateImageMember-v2
func Update(client *gophercloud.ServiceClient, imageID string, memberID string, opts UpdateOptsBuilder) (r UpdateResult) {
b, err := opts.ToImageMemberUpdateMap()
if err != nil {
r.Err = err
return
}
_, r.Err = client.Put(updateMemberURL(client, imageID, memberID), b, &r.Body,
&gophercloud.RequestOpts{OkCodes: []int{200}})
return
}

View File

@ -0,0 +1,91 @@
package members
import (
"encoding/json"
"time"
"github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/pagination"
)
// Member model
type Member struct {
CreatedAt time.Time `json:"-"`
ImageID string `json:"image_id"`
MemberID string `json:"member_id"`
Schema string `json:"schema"`
Status string `json:"status"`
UpdatedAt time.Time `json:"-"`
}
func (s *Member) UnmarshalJSON(b []byte) error {
type tmp Member
var p *struct {
tmp
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
err := json.Unmarshal(b, &p)
if err != nil {
return err
}
*s = Member(p.tmp)
s.CreatedAt, err = time.Parse(time.RFC3339, p.CreatedAt)
if err != nil {
return err
}
s.UpdatedAt, err = time.Parse(time.RFC3339, p.UpdatedAt)
return err
}
// Extract Member model from request if possible
func (r commonResult) Extract() (*Member, error) {
var s *Member
err := r.ExtractInto(&s)
return s, err
}
// MemberPage is a single page of Members results.
type MemberPage struct {
pagination.SinglePageBase
}
// ExtractMembers returns a slice of Members contained in a single page of results.
func ExtractMembers(r pagination.Page) ([]Member, error) {
var s struct {
Members []Member `json:"members"`
}
err := r.(MemberPage).ExtractInto(&s)
return s.Members, err
}
// IsEmpty determines whether or not a page of Members contains any results.
func (r MemberPage) IsEmpty() (bool, error) {
members, err := ExtractMembers(r)
return len(members) == 0, err
}
type commonResult struct {
gophercloud.Result
}
// CreateResult result model
type CreateResult struct {
commonResult
}
// DetailsResult model
type DetailsResult struct {
commonResult
}
// UpdateResult model
type UpdateResult struct {
commonResult
}
// DeleteResult model
type DeleteResult struct {
gophercloud.ErrResult
}

View File

@ -0,0 +1,31 @@
package members
import "github.com/gophercloud/gophercloud"
func imageMembersURL(c *gophercloud.ServiceClient, imageID string) string {
return c.ServiceURL("images", imageID, "members")
}
func listMembersURL(c *gophercloud.ServiceClient, imageID string) string {
return imageMembersURL(c, imageID)
}
func createMemberURL(c *gophercloud.ServiceClient, imageID string) string {
return imageMembersURL(c, imageID)
}
func imageMemberURL(c *gophercloud.ServiceClient, imageID string, memberID string) string {
return c.ServiceURL("images", imageID, "members", memberID)
}
func getMemberURL(c *gophercloud.ServiceClient, imageID string, memberID string) string {
return imageMemberURL(c, imageID, memberID)
}
func updateMemberURL(c *gophercloud.ServiceClient, imageID string, memberID string) string {
return imageMemberURL(c, imageID, memberID)
}
func deleteMemberURL(c *gophercloud.ServiceClient, imageID string, memberID string) string {
return imageMemberURL(c, imageID, memberID)
}

View File

@ -4,7 +4,7 @@ import (
"fmt"
"strings"
"github.com/rackspace/gophercloud"
"github.com/gophercloud/gophercloud"
)
// Version is a supported API version, corresponding to a vN package within the appropriate service.
@ -59,7 +59,7 @@ func ChooseVersion(client *gophercloud.ProviderClient, recognized []*Version) (*
}
var resp response
_, err := client.Request("GET", client.IdentityBase, gophercloud.RequestOpts{
_, err := client.Request("GET", client.IdentityBase, &gophercloud.RequestOpts{
JSONResponse: &resp,
OkCodes: []int{200, 300},
})

View File

@ -7,7 +7,7 @@ import (
"net/url"
"strings"
"github.com/rackspace/gophercloud"
"github.com/gophercloud/gophercloud"
)
// PageResult stores the HTTP response that returned the current page of results.
@ -53,7 +53,7 @@ func PageResultFromParsed(resp *http.Response, body interface{}) PageResult {
// Request performs an HTTP request and extracts the http.Response from the result.
func Request(client *gophercloud.ServiceClient, headers map[string]string, url string) (*http.Response, error) {
return client.Request("GET", url, gophercloud.RequestOpts{
return client.Get(url, nil, &gophercloud.RequestOpts{
MoreHeaders: headers,
OkCodes: []int{200, 204},
})

View File

@ -1,6 +1,11 @@
package pagination
import "fmt"
import (
"fmt"
"reflect"
"github.com/gophercloud/gophercloud"
)
// LinkedPageBase may be embedded to implement a page that provides navigational "Next" and "Previous" links within its result.
type LinkedPageBase struct {
@ -28,7 +33,10 @@ func (current LinkedPageBase) NextPageURL() (string, error) {
submap, ok := current.Body.(map[string]interface{})
if !ok {
return "", fmt.Errorf("Expected an object, but was %#v", current.Body)
err := gophercloud.ErrUnexpectedType{}
err.Expected = "map[string]interface{}"
err.Actual = fmt.Sprintf("%v", reflect.TypeOf(current.Body))
return "", err
}
for {
@ -42,7 +50,10 @@ func (current LinkedPageBase) NextPageURL() (string, error) {
if len(path) > 0 {
submap, ok = value.(map[string]interface{})
if !ok {
return "", fmt.Errorf("Expected an object, but was %#v", value)
err := gophercloud.ErrUnexpectedType{}
err.Expected = "map[string]interface{}"
err.Actual = fmt.Sprintf("%v", reflect.TypeOf(value))
return "", err
}
} else {
if value == nil {
@ -52,7 +63,10 @@ func (current LinkedPageBase) NextPageURL() (string, error) {
url, ok := value.(string)
if !ok {
return "", fmt.Errorf("Expected a string, but was %#v", value)
err := gophercloud.ErrUnexpectedType{}
err.Expected = "string"
err.Actual = fmt.Sprintf("%v", reflect.TypeOf(value))
return "", err
}
return url, nil
@ -60,6 +74,17 @@ func (current LinkedPageBase) NextPageURL() (string, error) {
}
}
// IsEmpty satisifies the IsEmpty method of the Page interface
func (current LinkedPageBase) IsEmpty() (bool, error) {
if b, ok := current.Body.([]interface{}); ok {
return len(b) == 0, nil
}
err := gophercloud.ErrUnexpectedType{}
err.Expected = "[]interface{}"
err.Actual = fmt.Sprintf("%v", reflect.TypeOf(current.Body))
return true, err
}
// GetBody returns the linked page's body. This method is needed to satisfy the
// Page interface.
func (current LinkedPageBase) GetBody() interface{} {

View File

@ -1,5 +1,12 @@
package pagination
import (
"fmt"
"reflect"
"github.com/gophercloud/gophercloud"
)
// MarkerPage is a stricter Page interface that describes additional functionality required for use with NewMarkerPager.
// For convenience, embed the MarkedPageBase struct.
type MarkerPage interface {
@ -33,6 +40,17 @@ func (current MarkerPageBase) NextPageURL() (string, error) {
return currentURL.String(), nil
}
// IsEmpty satisifies the IsEmpty method of the Page interface
func (current MarkerPageBase) IsEmpty() (bool, error) {
if b, ok := current.Body.([]interface{}); ok {
return len(b) == 0, nil
}
err := gophercloud.ErrUnexpectedType{}
err.Expected = "[]interface{}"
err.Actual = fmt.Sprintf("%v", reflect.TypeOf(current.Body))
return true, err
}
// GetBody returns the linked page's body. This method is needed to satisfy the
// Page interface.
func (current MarkerPageBase) GetBody() interface{} {

View File

@ -7,7 +7,7 @@ import (
"reflect"
"strings"
"github.com/rackspace/gophercloud"
"github.com/gophercloud/gophercloud"
)
var (
@ -138,6 +138,11 @@ func (p Pager) AllPages() (Page, error) {
// that type.
pageType := reflect.TypeOf(testPage)
// if it's a single page, just return the testPage (first page)
if _, found := pageType.FieldByName("SinglePageBase"); found {
return testPage, nil
}
// Switch on the page body type. Recognized types are `map[string]interface{}`,
// `[]byte`, and `[]interface{}`.
switch testPage.GetBody().(type) {
@ -145,7 +150,7 @@ func (p Pager) AllPages() (Page, error) {
// key is the map key for the page body if the body type is `map[string]interface{}`.
var key string
// Iterate over the pages to concatenate the bodies.
err := p.EachPage(func(page Page) (bool, error) {
err = p.EachPage(func(page Page) (bool, error) {
b := page.GetBody().(map[string]interface{})
for k := range b {
// If it's a linked page, we don't want the `links`, we want the other one.
@ -153,7 +158,14 @@ func (p Pager) AllPages() (Page, error) {
key = k
}
}
pagesSlice = append(pagesSlice, b[key].([]interface{})...)
switch keyType := b[key].(type) {
case map[string]interface{}:
pagesSlice = append(pagesSlice, keyType)
case []interface{}:
pagesSlice = append(pagesSlice, b[key].([]interface{})...)
default:
return false, fmt.Errorf("Unsupported page body type: %+v", keyType)
}
return true, nil
})
if err != nil {
@ -164,7 +176,7 @@ func (p Pager) AllPages() (Page, error) {
body.SetMapIndex(reflect.ValueOf(key), reflect.ValueOf(pagesSlice))
case []byte:
// Iterate over the pages to concatenate the bodies.
err := p.EachPage(func(page Page) (bool, error) {
err = p.EachPage(func(page Page) (bool, error) {
b := page.GetBody().([]byte)
pagesSlice = append(pagesSlice, b)
// seperate pages with a comma
@ -188,7 +200,7 @@ func (p Pager) AllPages() (Page, error) {
body.SetBytes(b)
case []interface{}:
// Iterate over the pages to concatenate the bodies.
err := p.EachPage(func(page Page) (bool, error) {
err = p.EachPage(func(page Page) (bool, error) {
b := page.GetBody().([]interface{})
pagesSlice = append(pagesSlice, b...)
return true, nil
@ -202,7 +214,10 @@ func (p Pager) AllPages() (Page, error) {
body.Index(i).Set(reflect.ValueOf(s))
}
default:
return nil, fmt.Errorf("Page body has unrecognized type.")
err := gophercloud.ErrUnexpectedType{}
err.Expected = "map[string]interface{}/[]byte/[]interface{}"
err.Actual = fmt.Sprintf("%v", reflect.TypeOf(testPage.GetBody()))
return nil, err
}
// Each `Extract*` function is expecting a specific type of page coming back,

View File

@ -1,5 +1,12 @@
package pagination
import (
"fmt"
"reflect"
"github.com/gophercloud/gophercloud"
)
// SinglePageBase may be embedded in a Page that contains all of the results from an operation at once.
type SinglePageBase PageResult
@ -8,6 +15,17 @@ func (current SinglePageBase) NextPageURL() (string, error) {
return "", nil
}
// IsEmpty satisifies the IsEmpty method of the Page interface
func (current SinglePageBase) IsEmpty() (bool, error) {
if b, ok := current.Body.([]interface{}); ok {
return len(b) == 0, nil
}
err := gophercloud.ErrUnexpectedType{}
err.Expected = "[]interface{}"
err.Actual = fmt.Sprintf("%v", reflect.TypeOf(current.Body))
return true, err
}
// GetBody returns the single page's body. This method is needed to satisfy the
// Page interface.
func (current SinglePageBase) GetBody() interface{} {

View File

@ -1,6 +1,7 @@
package gophercloud
import (
"encoding/json"
"fmt"
"net/url"
"reflect"
@ -9,6 +10,146 @@ import (
"time"
)
// BuildRequestBody builds a map[string]interface from the given `struct`. If
// parent is not the empty string, the final map[string]interface returned will
// encapsulate the built one
//
func BuildRequestBody(opts interface{}, parent string) (map[string]interface{}, error) {
optsValue := reflect.ValueOf(opts)
if optsValue.Kind() == reflect.Ptr {
optsValue = optsValue.Elem()
}
optsType := reflect.TypeOf(opts)
if optsType.Kind() == reflect.Ptr {
optsType = optsType.Elem()
}
optsMap := make(map[string]interface{})
if optsValue.Kind() == reflect.Struct {
//fmt.Printf("optsValue.Kind() is a reflect.Struct: %+v\n", optsValue.Kind())
for i := 0; i < optsValue.NumField(); i++ {
v := optsValue.Field(i)
f := optsType.Field(i)
if f.Name != strings.Title(f.Name) {
//fmt.Printf("Skipping field: %s...\n", f.Name)
continue
}
//fmt.Printf("Starting on field: %s...\n", f.Name)
zero := isZero(v)
//fmt.Printf("v is zero?: %v\n", zero)
// if the field has a required tag that's set to "true"
if requiredTag := f.Tag.Get("required"); requiredTag == "true" {
//fmt.Printf("Checking required field [%s]:\n\tv: %+v\n\tisZero:%v\n", f.Name, v.Interface(), zero)
// if the field's value is zero, return a missing-argument error
if zero {
// if the field has a 'required' tag, it can't have a zero-value
err := ErrMissingInput{}
err.Argument = f.Name
return nil, err
}
}
if xorTag := f.Tag.Get("xor"); xorTag != "" {
//fmt.Printf("Checking `xor` tag for field [%s] with value %+v:\n\txorTag: %s\n", f.Name, v, xorTag)
xorField := optsValue.FieldByName(xorTag)
var xorFieldIsZero bool
if reflect.ValueOf(xorField.Interface()) == reflect.Zero(xorField.Type()) {
xorFieldIsZero = true
} else {
if xorField.Kind() == reflect.Ptr {
xorField = xorField.Elem()
}
xorFieldIsZero = isZero(xorField)
}
if !(zero != xorFieldIsZero) {
err := ErrMissingInput{}
err.Argument = fmt.Sprintf("%s/%s", f.Name, xorTag)
err.Info = fmt.Sprintf("Exactly one of %s and %s must be provided", f.Name, xorTag)
return nil, err
}
}
if orTag := f.Tag.Get("or"); orTag != "" {
//fmt.Printf("Checking `or` tag for field with:\n\tname: %+v\n\torTag:%s\n", f.Name, orTag)
//fmt.Printf("field is zero?: %v\n", zero)
if zero {
orField := optsValue.FieldByName(orTag)
var orFieldIsZero bool
if reflect.ValueOf(orField.Interface()) == reflect.Zero(orField.Type()) {
orFieldIsZero = true
} else {
if orField.Kind() == reflect.Ptr {
orField = orField.Elem()
}
orFieldIsZero = isZero(orField)
}
if orFieldIsZero {
err := ErrMissingInput{}
err.Argument = fmt.Sprintf("%s/%s", f.Name, orTag)
err.Info = fmt.Sprintf("At least one of %s and %s must be provided", f.Name, orTag)
return nil, err
}
}
}
if v.Kind() == reflect.Struct || (v.Kind() == reflect.Ptr && v.Elem().Kind() == reflect.Struct) {
if zero {
//fmt.Printf("value before change: %+v\n", optsValue.Field(i))
if jsonTag := f.Tag.Get("json"); jsonTag != "" {
jsonTagPieces := strings.Split(jsonTag, ",")
if len(jsonTagPieces) > 1 && jsonTagPieces[1] == "omitempty" {
if v.CanSet() {
if !v.IsNil() {
if v.Kind() == reflect.Ptr {
v.Set(reflect.Zero(v.Type()))
}
}
//fmt.Printf("value after change: %+v\n", optsValue.Field(i))
}
}
}
continue
}
//fmt.Printf("Calling BuildRequestBody with:\n\tv: %+v\n\tf.Name:%s\n", v.Interface(), f.Name)
_, err := BuildRequestBody(v.Interface(), f.Name)
if err != nil {
return nil, err
}
}
}
//fmt.Printf("opts: %+v \n", opts)
b, err := json.Marshal(opts)
if err != nil {
return nil, err
}
//fmt.Printf("string(b): %s\n", string(b))
err = json.Unmarshal(b, &optsMap)
if err != nil {
return nil, err
}
//fmt.Printf("optsMap: %+v\n", optsMap)
if parent != "" {
optsMap = map[string]interface{}{parent: optsMap}
}
//fmt.Printf("optsMap after parent added: %+v\n", optsMap)
return optsMap, nil
}
// Return an error if the underlying type of 'opts' isn't a struct.
return nil, fmt.Errorf("Options type is not a struct.")
}
// EnabledState is a convenience type, mostly used in Create and Update
// operations. Because the zero value of a bool is FALSE, we need to use a
// pointer instead to indicate zero-ness.
@ -23,6 +164,17 @@ var (
Disabled EnabledState = &iFalse
)
// IPVersion is a type for the possible IP address versions. Valid instances
// are IPv4 and IPv6
type IPVersion int
const (
// IPv4 is used for IP version 4 addresses
IPv4 IPVersion = 4
// IPv6 is used for IP version 6 addresses
IPv6 IPVersion = 6
)
// IntToPointer is a function for converting integers into integer pointers.
// This is useful when passing in options to operations.
func IntToPointer(i int) *int {
@ -60,10 +212,27 @@ func MaybeInt(original int) *int {
return nil
}
/*
func isUnderlyingStructZero(v reflect.Value) bool {
switch v.Kind() {
case reflect.Ptr:
return isUnderlyingStructZero(v.Elem())
default:
return isZero(v)
}
}
*/
var t time.Time
func isZero(v reflect.Value) bool {
//fmt.Printf("\n\nchecking isZero for value: %+v\n", v)
switch v.Kind() {
case reflect.Ptr:
if v.IsNil() {
return true
}
return false
case reflect.Func, reflect.Map, reflect.Slice:
return v.IsNil()
case reflect.Array:
@ -87,6 +256,7 @@ func isZero(v reflect.Value) bool {
}
// Compare other types directly:
z := reflect.Zero(v.Type())
//fmt.Printf("zero type for value: %+v\n\n\n", z)
return v.Interface() == z.Interface()
}
@ -137,7 +307,11 @@ func BuildQueryString(opts interface{}) (*url.URL, error) {
// if the field is set, add it to the slice of query pieces
if !isZero(v) {
loop:
switch v.Kind() {
case reflect.Ptr:
v = v.Elem()
goto loop
case reflect.String:
params.Add(tags[0], v.String())
case reflect.Int:

View File

@ -3,7 +3,6 @@ package gophercloud
import (
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
@ -11,7 +10,7 @@ import (
)
// DefaultUserAgent is the default User-Agent string set in the request header.
const DefaultUserAgent = "gophercloud/1.0.0"
const DefaultUserAgent = "gophercloud/2.0.0"
// UserAgent represents a User-Agent header.
type UserAgent struct {
@ -68,6 +67,8 @@ type ProviderClient struct {
// fails with a 401 HTTP response code. This a needed because there may be multiple
// authentication functions for different Identity service versions.
ReauthFunc func() error
Debug bool
}
// AuthenticatedHeaders returns a map of HTTP headers that are common for all
@ -85,46 +86,30 @@ type RequestOpts struct {
// content type of the request will default to "application/json" unless overridden by MoreHeaders.
// It's an error to specify both a JSONBody and a RawBody.
JSONBody interface{}
// RawBody contains an io.ReadSeeker that will be consumed by the request directly. No content-type
// RawBody contains an io.Reader that will be consumed by the request directly. No content-type
// will be set unless one is provided explicitly by MoreHeaders.
RawBody io.ReadSeeker
RawBody io.Reader
// JSONResponse, if provided, will be populated with the contents of the response body parsed as
// JSON.
JSONResponse interface{}
// OkCodes contains a list of numeric HTTP status codes that should be interpreted as success. If
// the response has a different code, an error will be returned.
OkCodes []int
// MoreHeaders specifies additional HTTP headers to be provide on the request. If a header is
// provided with a blank value (""), that header will be *omitted* instead: use this to suppress
// the default Accept header or an inferred Content-Type, for example.
MoreHeaders map[string]string
}
// UnexpectedResponseCodeError is returned by the Request method when a response code other than
// those listed in OkCodes is encountered.
type UnexpectedResponseCodeError struct {
URL string
Method string
Expected []int
Actual int
Body []byte
}
func (err *UnexpectedResponseCodeError) Error() string {
return fmt.Sprintf(
"Expected HTTP response code %v when accessing [%s %s], but got %d instead\n%s",
err.Expected, err.Method, err.URL, err.Actual, err.Body,
)
// ErrorContext specifies the resource error type to return if an error is encountered.
// This lets resources override default error messages based on the response status code.
ErrorContext error
}
var applicationJSON = "application/json"
// Request performs an HTTP request using the ProviderClient's current HTTPClient. An authentication
// header will automatically be provided.
func (client *ProviderClient) Request(method, url string, options RequestOpts) (*http.Response, error) {
var body io.ReadSeeker
func (client *ProviderClient) Request(method, url string, options *RequestOpts) (*http.Response, error) {
var body io.Reader
var contentType *string
// Derive the content body by either encoding an arbitrary object as JSON, or by taking a provided
@ -179,32 +164,13 @@ func (client *ProviderClient) Request(method, url string, options RequestOpts) (
// Set connection parameter to close the connection immediately when we've got the response
req.Close = true
// Issue the request.
resp, err := client.HTTPClient.Do(req)
if err != nil {
return nil, err
}
if resp.StatusCode == http.StatusUnauthorized {
if client.ReauthFunc != nil {
err = client.ReauthFunc()
if err != nil {
return nil, fmt.Errorf("Error trying to re-authenticate: %s", err)
}
if options.RawBody != nil {
options.RawBody.Seek(0, 0)
}
resp.Body.Close()
resp, err = client.Request(method, url, options)
if err != nil {
return nil, fmt.Errorf("Successfully re-authenticated, but got error executing request: %s", err)
}
return resp, nil
}
}
// Allow default OkCodes if none explicitly set
if options.OkCodes == nil {
options.OkCodes = defaultOkCodes(method)
@ -218,16 +184,98 @@ func (client *ProviderClient) Request(method, url string, options RequestOpts) (
break
}
}
if !ok {
body, _ := ioutil.ReadAll(resp.Body)
resp.Body.Close()
return resp, &UnexpectedResponseCodeError{
//pc := make([]uintptr, 1)
//runtime.Callers(2, pc)
//f := runtime.FuncForPC(pc[0])
respErr := ErrUnexpectedResponseCode{
URL: url,
Method: method,
Expected: options.OkCodes,
Actual: resp.StatusCode,
Body: body,
}
//respErr.Function = "gophercloud.ProviderClient.Request"
errType := options.ErrorContext
switch resp.StatusCode {
case http.StatusBadRequest:
err = ErrDefault400{respErr}
if error400er, ok := errType.(Err400er); ok {
err = error400er.Error400(respErr)
}
case http.StatusUnauthorized:
if client.ReauthFunc != nil {
err = client.ReauthFunc()
if err != nil {
e := &ErrUnableToReauthenticate{}
e.ErrOriginal = respErr
return nil, e
}
if options.RawBody != nil {
if seeker, ok := options.RawBody.(io.Seeker); ok {
seeker.Seek(0, 0)
}
}
resp, err = client.Request(method, url, options)
if err != nil {
switch err.(type) {
case *ErrUnexpectedResponseCode:
e := &ErrErrorAfterReauthentication{}
e.ErrOriginal = err.(*ErrUnexpectedResponseCode)
return nil, e
default:
e := &ErrErrorAfterReauthentication{}
e.ErrOriginal = err
return nil, e
}
}
return resp, nil
}
err = ErrDefault401{respErr}
if error401er, ok := errType.(Err401er); ok {
err = error401er.Error401(respErr)
}
case http.StatusNotFound:
err = ErrDefault404{respErr}
if error404er, ok := errType.(Err404er); ok {
err = error404er.Error404(respErr)
}
case http.StatusMethodNotAllowed:
err = ErrDefault405{respErr}
if error405er, ok := errType.(Err405er); ok {
err = error405er.Error405(respErr)
}
case http.StatusRequestTimeout:
err = ErrDefault408{respErr}
if error408er, ok := errType.(Err408er); ok {
err = error408er.Error408(respErr)
}
case 429:
err = ErrDefault429{respErr}
if error429er, ok := errType.(Err429er); ok {
err = error429er.Error429(respErr)
}
case http.StatusInternalServerError:
err = ErrDefault500{respErr}
if error500er, ok := errType.(Err500er); ok {
err = error500er.Error500(respErr)
}
case http.StatusServiceUnavailable:
err = ErrDefault503{respErr}
if error503er, ok := errType.(Err503er); ok {
err = error503er.Error503(respErr)
}
}
if err == nil {
err = respErr
}
return resp, err
}
// Parse the response body as JSON, if requested to do so.
@ -257,75 +305,3 @@ func defaultOkCodes(method string) []int {
return []int{}
}
func (client *ProviderClient) Get(url string, JSONResponse *interface{}, opts *RequestOpts) (*http.Response, error) {
if opts == nil {
opts = &RequestOpts{}
}
if JSONResponse != nil {
opts.JSONResponse = JSONResponse
}
return client.Request("GET", url, *opts)
}
func (client *ProviderClient) Post(url string, JSONBody interface{}, JSONResponse *interface{}, opts *RequestOpts) (*http.Response, error) {
if opts == nil {
opts = &RequestOpts{}
}
if v, ok := (JSONBody).(io.ReadSeeker); ok {
opts.RawBody = v
} else if JSONBody != nil {
opts.JSONBody = JSONBody
}
if JSONResponse != nil {
opts.JSONResponse = JSONResponse
}
return client.Request("POST", url, *opts)
}
func (client *ProviderClient) Put(url string, JSONBody interface{}, JSONResponse *interface{}, opts *RequestOpts) (*http.Response, error) {
if opts == nil {
opts = &RequestOpts{}
}
if v, ok := (JSONBody).(io.ReadSeeker); ok {
opts.RawBody = v
} else if JSONBody != nil {
opts.JSONBody = JSONBody
}
if JSONResponse != nil {
opts.JSONResponse = JSONResponse
}
return client.Request("PUT", url, *opts)
}
func (client *ProviderClient) Patch(url string, JSONBody interface{}, JSONResponse *interface{}, opts *RequestOpts) (*http.Response, error) {
if opts == nil {
opts = &RequestOpts{}
}
if v, ok := (JSONBody).(io.ReadSeeker); ok {
opts.RawBody = v
} else if JSONBody != nil {
opts.JSONBody = JSONBody
}
if JSONResponse != nil {
opts.JSONResponse = JSONResponse
}
return client.Request("PATCH", url, *opts)
}
func (client *ProviderClient) Delete(url string, opts *RequestOpts) (*http.Response, error) {
if opts == nil {
opts = &RequestOpts{}
}
return client.Request("DELETE", url, *opts)
}

336
vendor/github.com/gophercloud/gophercloud/results.go generated vendored Normal file
View File

@ -0,0 +1,336 @@
package gophercloud
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"reflect"
"strconv"
"time"
)
/*
Result is an internal type to be used by individual resource packages, but its
methods will be available on a wide variety of user-facing embedding types.
It acts as a base struct that other Result types, returned from request
functions, can embed for convenience. All Results capture basic information
from the HTTP transaction that was performed, including the response body,
HTTP headers, and any errors that happened.
Generally, each Result type will have an Extract method that can be used to
further interpret the result's payload in a specific context. Extensions or
providers can then provide additional extraction functions to pull out
provider- or extension-specific information as well.
*/
type Result struct {
// Body is the payload of the HTTP response from the server. In most cases,
// this will be the deserialized JSON structure.
Body interface{}
// Header contains the HTTP header structure from the original response.
Header http.Header
// Err is an error that occurred during the operation. It's deferred until
// extraction to make it easier to chain the Extract call.
Err error
}
// ExtractInto allows users to provide an object into which `Extract` will extract
// the `Result.Body`. This would be useful for OpenStack providers that have
// different fields in the response object than OpenStack proper.
func (r Result) ExtractInto(to interface{}) error {
if r.Err != nil {
return r.Err
}
if reader, ok := r.Body.(io.Reader); ok {
if readCloser, ok := reader.(io.Closer); ok {
defer readCloser.Close()
}
return json.NewDecoder(reader).Decode(to)
}
b, err := json.Marshal(r.Body)
if err != nil {
return err
}
err = json.Unmarshal(b, to)
return err
}
func (r Result) extractIntoPtr(to interface{}, label string) error {
if label == "" {
return r.ExtractInto(&to)
}
var m map[string]interface{}
err := r.ExtractInto(&m)
if err != nil {
return err
}
b, err := json.Marshal(m[label])
if err != nil {
return err
}
err = json.Unmarshal(b, &to)
return err
}
// ExtractIntoStructPtr will unmarshal the Result (r) into the provided
// interface{} (to).
//
// NOTE: For internal use only
//
// `to` must be a pointer to an underlying struct type
//
// If provided, `label` will be filtered out of the response
// body prior to `r` being unmarshalled into `to`.
func (r Result) ExtractIntoStructPtr(to interface{}, label string) error {
if r.Err != nil {
return r.Err
}
t := reflect.TypeOf(to)
if k := t.Kind(); k != reflect.Ptr {
return fmt.Errorf("Expected pointer, got %v", k)
}
switch t.Elem().Kind() {
case reflect.Struct:
return r.extractIntoPtr(to, label)
default:
return fmt.Errorf("Expected pointer to struct, got: %v", t)
}
}
// ExtractIntoSlicePtr will unmarshal the Result (r) into the provided
// interface{} (to).
//
// NOTE: For internal use only
//
// `to` must be a pointer to an underlying slice type
//
// If provided, `label` will be filtered out of the response
// body prior to `r` being unmarshalled into `to`.
func (r Result) ExtractIntoSlicePtr(to interface{}, label string) error {
if r.Err != nil {
return r.Err
}
t := reflect.TypeOf(to)
if k := t.Kind(); k != reflect.Ptr {
return fmt.Errorf("Expected pointer, got %v", k)
}
switch t.Elem().Kind() {
case reflect.Slice:
return r.extractIntoPtr(to, label)
default:
return fmt.Errorf("Expected pointer to slice, got: %v", t)
}
}
// PrettyPrintJSON creates a string containing the full response body as
// pretty-printed JSON. It's useful for capturing test fixtures and for
// debugging extraction bugs. If you include its output in an issue related to
// a buggy extraction function, we will all love you forever.
func (r Result) PrettyPrintJSON() string {
pretty, err := json.MarshalIndent(r.Body, "", " ")
if err != nil {
panic(err.Error())
}
return string(pretty)
}
// ErrResult is an internal type to be used by individual resource packages, but
// its methods will be available on a wide variety of user-facing embedding
// types.
//
// It represents results that only contain a potential error and
// nothing else. Usually, if the operation executed successfully, the Err field
// will be nil; otherwise it will be stocked with a relevant error. Use the
// ExtractErr method
// to cleanly pull it out.
type ErrResult struct {
Result
}
// ExtractErr is a function that extracts error information, or nil, from a result.
func (r ErrResult) ExtractErr() error {
return r.Err
}
/*
HeaderResult is an internal type to be used by individual resource packages, but
its methods will be available on a wide variety of user-facing embedding types.
It represents a result that only contains an error (possibly nil) and an
http.Header. This is used, for example, by the objectstorage packages in
openstack, because most of the operations don't return response bodies, but do
have relevant information in headers.
*/
type HeaderResult struct {
Result
}
// ExtractHeader will return the http.Header and error from the HeaderResult.
//
// header, err := objects.Create(client, "my_container", objects.CreateOpts{}).ExtractHeader()
func (r HeaderResult) ExtractInto(to interface{}) error {
if r.Err != nil {
return r.Err
}
tmpHeaderMap := map[string]string{}
for k, v := range r.Header {
if len(v) > 0 {
tmpHeaderMap[k] = v[0]
}
}
b, err := json.Marshal(tmpHeaderMap)
if err != nil {
return err
}
err = json.Unmarshal(b, to)
return err
}
// RFC3339Milli describes a common time format used by some API responses.
const RFC3339Milli = "2006-01-02T15:04:05.999999Z"
type JSONRFC3339Milli time.Time
func (jt *JSONRFC3339Milli) UnmarshalJSON(data []byte) error {
b := bytes.NewBuffer(data)
dec := json.NewDecoder(b)
var s string
if err := dec.Decode(&s); err != nil {
return err
}
t, err := time.Parse(RFC3339Milli, s)
if err != nil {
return err
}
*jt = JSONRFC3339Milli(t)
return nil
}
const RFC3339MilliNoZ = "2006-01-02T15:04:05.999999"
type JSONRFC3339MilliNoZ time.Time
func (jt *JSONRFC3339MilliNoZ) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err != nil {
return err
}
if s == "" {
return nil
}
t, err := time.Parse(RFC3339MilliNoZ, s)
if err != nil {
return err
}
*jt = JSONRFC3339MilliNoZ(t)
return nil
}
type JSONRFC1123 time.Time
func (jt *JSONRFC1123) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err != nil {
return err
}
if s == "" {
return nil
}
t, err := time.Parse(time.RFC1123, s)
if err != nil {
return err
}
*jt = JSONRFC1123(t)
return nil
}
type JSONUnix time.Time
func (jt *JSONUnix) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err != nil {
return err
}
if s == "" {
return nil
}
unix, err := strconv.ParseInt(s, 10, 64)
if err != nil {
return err
}
t = time.Unix(unix, 0)
*jt = JSONUnix(t)
return nil
}
// RFC3339NoZ is the time format used in Heat (Orchestration).
const RFC3339NoZ = "2006-01-02T15:04:05"
type JSONRFC3339NoZ time.Time
func (jt *JSONRFC3339NoZ) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err != nil {
return err
}
if s == "" {
return nil
}
t, err := time.Parse(RFC3339NoZ, s)
if err != nil {
return err
}
*jt = JSONRFC3339NoZ(t)
return nil
}
/*
Link is an internal type to be used in packages of collection resources that are
paginated in a certain way.
It's a response substructure common to many paginated collection results that is
used to point to related pages. Usually, the one we care about is the one with
Rel field set to "next".
*/
type Link struct {
Href string `json:"href"`
Rel string `json:"rel"`
}
/*
ExtractNextURL is an internal function useful for packages of collection
resources that are paginated in a certain way.
It attempts to extract the "next" URL from slice of Link structs, or
"" if no such URL is present.
*/
func ExtractNextURL(links []Link) (string, error) {
var url string
for _, l := range links {
if l.Rel == "next" {
url = l.Href
}
}
if url == "" {
return "", nil
}
return url, nil
}

View File

@ -0,0 +1,141 @@
package gophercloud
import (
"io"
"net/http"
"strings"
)
// ServiceClient stores details required to interact with a specific service API implemented by a provider.
// Generally, you'll acquire these by calling the appropriate `New` method on a ProviderClient.
type ServiceClient struct {
// ProviderClient is a reference to the provider that implements this service.
*ProviderClient
// Endpoint is the base URL of the service's API, acquired from a service catalog.
// It MUST end with a /.
Endpoint string
// ResourceBase is the base URL shared by the resources within a service's API. It should include
// the API version and, like Endpoint, MUST end with a / if set. If not set, the Endpoint is used
// as-is, instead.
ResourceBase string
Microversion string
}
// ResourceBaseURL returns the base URL of any resources used by this service. It MUST end with a /.
func (client *ServiceClient) ResourceBaseURL() string {
if client.ResourceBase != "" {
return client.ResourceBase
}
return client.Endpoint
}
// ServiceURL constructs a URL for a resource belonging to this provider.
func (client *ServiceClient) ServiceURL(parts ...string) string {
return client.ResourceBaseURL() + strings.Join(parts, "/")
}
// Get calls `Request` with the "GET" HTTP verb.
func (client *ServiceClient) Get(url string, JSONResponse interface{}, opts *RequestOpts) (*http.Response, error) {
if opts == nil {
opts = &RequestOpts{}
}
if JSONResponse != nil {
opts.JSONResponse = JSONResponse
}
if opts.MoreHeaders == nil {
opts.MoreHeaders = make(map[string]string)
}
opts.MoreHeaders["X-OpenStack-Nova-API-Version"] = client.Microversion
return client.Request("GET", url, opts)
}
// Post calls `Request` with the "POST" HTTP verb.
func (client *ServiceClient) Post(url string, JSONBody interface{}, JSONResponse interface{}, opts *RequestOpts) (*http.Response, error) {
if opts == nil {
opts = &RequestOpts{}
}
if v, ok := (JSONBody).(io.Reader); ok {
opts.RawBody = v
} else if JSONBody != nil {
opts.JSONBody = JSONBody
}
if JSONResponse != nil {
opts.JSONResponse = JSONResponse
}
if opts.MoreHeaders == nil {
opts.MoreHeaders = make(map[string]string)
}
opts.MoreHeaders["X-OpenStack-Nova-API-Version"] = client.Microversion
return client.Request("POST", url, opts)
}
// Put calls `Request` with the "PUT" HTTP verb.
func (client *ServiceClient) Put(url string, JSONBody interface{}, JSONResponse interface{}, opts *RequestOpts) (*http.Response, error) {
if opts == nil {
opts = &RequestOpts{}
}
if v, ok := (JSONBody).(io.Reader); ok {
opts.RawBody = v
} else if JSONBody != nil {
opts.JSONBody = JSONBody
}
if JSONResponse != nil {
opts.JSONResponse = JSONResponse
}
if opts.MoreHeaders == nil {
opts.MoreHeaders = make(map[string]string)
}
opts.MoreHeaders["X-OpenStack-Nova-API-Version"] = client.Microversion
return client.Request("PUT", url, opts)
}
// Patch calls `Request` with the "PATCH" HTTP verb.
func (client *ServiceClient) Patch(url string, JSONBody interface{}, JSONResponse interface{}, opts *RequestOpts) (*http.Response, error) {
if opts == nil {
opts = &RequestOpts{}
}
if v, ok := (JSONBody).(io.Reader); ok {
opts.RawBody = v
} else if JSONBody != nil {
opts.JSONBody = JSONBody
}
if JSONResponse != nil {
opts.JSONResponse = JSONResponse
}
if opts.MoreHeaders == nil {
opts.MoreHeaders = make(map[string]string)
}
opts.MoreHeaders["X-OpenStack-Nova-API-Version"] = client.Microversion
return client.Request("PATCH", url, opts)
}
// Delete calls `Request` with the "DELETE" HTTP verb.
func (client *ServiceClient) Delete(url string, opts *RequestOpts) (*http.Response, error) {
if opts == nil {
opts = &RequestOpts{}
}
if opts.MoreHeaders == nil {
opts.MoreHeaders = make(map[string]string)
}
opts.MoreHeaders["X-OpenStack-Nova-API-Version"] = client.Microversion
return client.Request("DELETE", url, opts)
}

View File

@ -1,274 +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).
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 ./...
```
To run all tests with verbose output:
```bash
go test -v ./...
```
To run tests that match certain [build tags]():
```bash
go test -tags "foo bar" ./...
```
To run tests for a particular sub-package:
```bash
cd ./path/to/package && go test .
```
## 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,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,57 +0,0 @@
# Gophercloud Acceptance tests
The purpose of these acceptance tests is to validate that SDK features meet
the requirements of a contract - to consumers, other parts of the library, and
to a remote API.
> **Note:** Because every test will be run against a real API endpoint, you
> may incur bandwidth and service charges for all the resource usage. These
> tests *should* remove their remote products automatically. However, there may
> be certain cases where this does not happen; always double-check to make sure
> you have no stragglers left behind.
### Step 1. Set environment variables
A lot of tests rely on environment variables for configuration - so you will need
to set them before running the suite. If you're testing against pure OpenStack APIs,
you can download a file that contains all of these variables for you: just visit
the `project/access_and_security` page in your control panel and click the "Download
OpenStack RC File" button at the top right. For all other providers, you will need
to set them manually.
#### Authentication
|Name|Description|
|---|---|
|`OS_USERNAME`|Your API username|
|`OS_PASSWORD`|Your API password|
|`OS_AUTH_URL`|The identity URL you need to authenticate|
|`OS_TENANT_NAME`|Your API tenant name|
|`OS_TENANT_ID`|Your API tenant ID|
|`RS_USERNAME`|Your Rackspace username|
|`RS_API_KEY`|Your Rackspace API key|
#### General
|Name|Description|
|---|---|
|`OS_REGION_NAME`|The region you want your resources to reside in|
|`RS_REGION`|Rackspace region you want your resource to reside in|
#### Compute
|Name|Description|
|---|---|
|`OS_IMAGE_ID`|The ID of the image your want your server to be based on|
|`OS_FLAVOR_ID`|The ID of the flavor you want your server to be based on|
|`OS_FLAVOR_ID_RESIZE`|The ID of the flavor you want your server to be resized to|
|`RS_IMAGE_ID`|The ID of the image you want servers to be created with|
|`RS_FLAVOR_ID`|The ID of the flavor you want your server to be created with|
### 2. Run the test suite
From the root directory, run:
```
./script/acceptancetest
```

View File

@ -1,50 +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.
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,91 +0,0 @@
// +build fixtures
package extensions
import (
"fmt"
"net/http"
"testing"
th "github.com/rackspace/gophercloud/testhelper"
"github.com/rackspace/gophercloud/testhelper/client"
)
// ListOutput provides a single page of Extension results.
const ListOutput = `
{
"extensions": [
{
"updated": "2013-01-20T00:00:00-00:00",
"name": "Neutron Service Type Management",
"links": [],
"namespace": "http://docs.openstack.org/ext/neutron/service-type/api/v1.0",
"alias": "service-type",
"description": "API for retrieving service providers for Neutron advanced services"
}
]
}`
// GetOutput provides a single Extension result.
const GetOutput = `
{
"extension": {
"updated": "2013-02-03T10:00:00-00:00",
"name": "agent",
"links": [],
"namespace": "http://docs.openstack.org/ext/agent/api/v2.0",
"alias": "agent",
"description": "The agent management extension."
}
}
`
// ListedExtension is the Extension that should be parsed from ListOutput.
var ListedExtension = Extension{
Updated: "2013-01-20T00:00:00-00:00",
Name: "Neutron Service Type Management",
Links: []interface{}{},
Namespace: "http://docs.openstack.org/ext/neutron/service-type/api/v1.0",
Alias: "service-type",
Description: "API for retrieving service providers for Neutron advanced services",
}
// ExpectedExtensions is a slice containing the Extension that should be parsed from ListOutput.
var ExpectedExtensions = []Extension{ListedExtension}
// SingleExtension is the Extension that should be parsed from GetOutput.
var SingleExtension = &Extension{
Updated: "2013-02-03T10:00:00-00:00",
Name: "agent",
Links: []interface{}{},
Namespace: "http://docs.openstack.org/ext/agent/api/v2.0",
Alias: "agent",
Description: "The agent management extension.",
}
// HandleListExtensionsSuccessfully creates an HTTP handler at `/extensions` on the test handler
// mux that response with a list containing a single tenant.
func HandleListExtensionsSuccessfully(t *testing.T) {
th.Mux.HandleFunc("/extensions", 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)
})
}
// HandleGetExtensionSuccessfully creates an HTTP handler at `/extensions/agent` that responds with
// a JSON payload corresponding to SingleExtension.
func HandleGetExtensionSuccessfully(t *testing.T) {
th.Mux.HandleFunc("/extensions/agent", 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")
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, GetOutput)
})
}

View File

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

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