Move deps from _workspace/ to vendor/

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

20
vendor/github.com/rackspace/gophercloud/.travis.yml generated vendored Normal file
View File

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

275
vendor/github.com/rackspace/gophercloud/CONTRIBUTING.md generated vendored Normal file
View File

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

View File

@@ -0,0 +1,13 @@
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>

191
vendor/github.com/rackspace/gophercloud/LICENSE generated vendored Normal file
View File

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

160
vendor/github.com/rackspace/gophercloud/README.md generated vendored Normal file
View File

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

338
vendor/github.com/rackspace/gophercloud/UPGRADING.md generated vendored Normal file
View File

@@ -0,0 +1,338 @@
# 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

@@ -0,0 +1,57 @@
# 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

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

View File

@@ -0,0 +1,3 @@
// The v2 package contains acceptance tests for the Openstack Compute V2 service.
package v2

View File

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

View File

@@ -0,0 +1 @@
package v1

View File

@@ -0,0 +1 @@
package v2

View File

@@ -0,0 +1 @@
package v3

View File

@@ -0,0 +1,39 @@
package v2
import (
"os"
"testing"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/openstack"
th "github.com/rackspace/gophercloud/testhelper"
)
var Client *gophercloud.ServiceClient
func NewClient() (*gophercloud.ServiceClient, error) {
opts, err := openstack.AuthOptionsFromEnv()
if err != nil {
return nil, err
}
provider, err := openstack.AuthenticatedClient(opts)
if err != nil {
return nil, err
}
return openstack.NewNetworkV2(provider, gophercloud.EndpointOpts{
Name: "neutron",
Region: os.Getenv("OS_REGION_NAME"),
})
}
func Setup(t *testing.T) {
client, err := NewClient()
th.AssertNoErr(t, err)
Client = client
}
func Teardown() {
Client = nil
}

View File

@@ -0,0 +1 @@
package fwaas

View File

@@ -0,0 +1,78 @@
package lbaas
import (
"testing"
base "github.com/rackspace/gophercloud/acceptance/openstack/networking/v2"
"github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/monitors"
"github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/pools"
"github.com/rackspace/gophercloud/openstack/networking/v2/networks"
"github.com/rackspace/gophercloud/openstack/networking/v2/subnets"
th "github.com/rackspace/gophercloud/testhelper"
)
func SetupTopology(t *testing.T) (string, string) {
// create network
n, err := networks.Create(base.Client, networks.CreateOpts{Name: "tmp_network"}).Extract()
th.AssertNoErr(t, err)
t.Logf("Created network %s", n.ID)
// create subnet
s, err := subnets.Create(base.Client, subnets.CreateOpts{
NetworkID: n.ID,
CIDR: "192.168.199.0/24",
IPVersion: subnets.IPv4,
Name: "tmp_subnet",
}).Extract()
th.AssertNoErr(t, err)
t.Logf("Created subnet %s", s.ID)
return n.ID, s.ID
}
func DeleteTopology(t *testing.T, networkID string) {
res := networks.Delete(base.Client, networkID)
th.AssertNoErr(t, res.Err)
t.Logf("Deleted network %s", networkID)
}
func CreatePool(t *testing.T, subnetID string) string {
p, err := pools.Create(base.Client, pools.CreateOpts{
LBMethod: pools.LBMethodRoundRobin,
Protocol: "HTTP",
Name: "tmp_pool",
SubnetID: subnetID,
}).Extract()
th.AssertNoErr(t, err)
t.Logf("Created pool %s", p.ID)
return p.ID
}
func DeletePool(t *testing.T, poolID string) {
res := pools.Delete(base.Client, poolID)
th.AssertNoErr(t, res.Err)
t.Logf("Deleted pool %s", poolID)
}
func CreateMonitor(t *testing.T) string {
m, err := monitors.Create(base.Client, monitors.CreateOpts{
Delay: 10,
Timeout: 10,
MaxRetries: 3,
Type: monitors.TypeHTTP,
ExpectedCodes: "200",
URLPath: "/login",
HTTPMethod: "GET",
}).Extract()
th.AssertNoErr(t, err)
t.Logf("Created monitor ID [%s]", m.ID)
return m.ID
}

View File

@@ -0,0 +1 @@
package lbaas

View File

@@ -0,0 +1 @@
package extensions

View File

@@ -0,0 +1 @@
package v2

View File

@@ -0,0 +1,28 @@
// +build acceptance
package v1
import (
"os"
"testing"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/openstack"
th "github.com/rackspace/gophercloud/testhelper"
)
var metadata = map[string]string{"gopher": "cloud"}
func newClient(t *testing.T) *gophercloud.ServiceClient {
ao, err := openstack.AuthOptionsFromEnv()
th.AssertNoErr(t, err)
client, err := openstack.AuthenticatedClient(ao)
th.AssertNoErr(t, err)
c, err := openstack.NewObjectStorageV1(client, gophercloud.EndpointOpts{
Region: os.Getenv("OS_REGION_NAME"),
})
th.AssertNoErr(t, err)
return c
}

View File

@@ -0,0 +1,44 @@
// +build acceptance
package v1
import (
"fmt"
"os"
"testing"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/openstack"
th "github.com/rackspace/gophercloud/testhelper"
)
var template = fmt.Sprintf(`
{
"heat_template_version": "2013-05-23",
"description": "Simple template to test heat commands",
"parameters": {},
"resources": {
"hello_world": {
"type":"OS::Nova::Server",
"properties": {
"flavor": "%s",
"image": "%s",
"user_data": "#!/bin/bash -xv\necho \"hello world\" &gt; /root/hello-world.txt\n"
}
}
}
}`, os.Getenv("OS_FLAVOR_ID"), os.Getenv("OS_IMAGE_ID"))
func newClient(t *testing.T) *gophercloud.ServiceClient {
ao, err := openstack.AuthOptionsFromEnv()
th.AssertNoErr(t, err)
client, err := openstack.AuthenticatedClient(ao)
th.AssertNoErr(t, err)
c, err := openstack.NewOrchestrationV1(client, gophercloud.EndpointOpts{
Region: os.Getenv("OS_REGION_NAME"),
})
th.AssertNoErr(t, err)
return c
}

View File

@@ -0,0 +1,13 @@
{
"heat_template_version": "2013-05-23",
"resources": {
"compute_instance": {
"type": "OS::Nova::Server",
"properties": {
"flavor": "m1.small",
"image": "cirros-0.3.2-x86_64-disk",
"name": "Single Compute Instance"
}
}
}
}

View File

@@ -0,0 +1,4 @@
// +build acceptance
package openstack

View File

@@ -0,0 +1,38 @@
// +build acceptance
package v1
import (
"os"
"testing"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/acceptance/tools"
"github.com/rackspace/gophercloud/rackspace"
th "github.com/rackspace/gophercloud/testhelper"
)
func newClient() (*gophercloud.ServiceClient, error) {
opts, err := rackspace.AuthOptionsFromEnv()
if err != nil {
return nil, err
}
opts = tools.OnlyRS(opts)
region := os.Getenv("RS_REGION")
provider, err := rackspace.AuthenticatedClient(opts)
if err != nil {
return nil, err
}
return rackspace.NewBlockStorageV1(provider, gophercloud.EndpointOpts{
Region: region,
})
}
func setup(t *testing.T) *gophercloud.ServiceClient {
client, err := newClient()
th.AssertNoErr(t, err)
return client
}

View File

@@ -0,0 +1,23 @@
// +build acceptance
package v1
import (
"testing"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/rackspace"
th "github.com/rackspace/gophercloud/testhelper"
)
func newClient(t *testing.T) *gophercloud.ServiceClient {
ao, err := rackspace.AuthOptionsFromEnv()
th.AssertNoErr(t, err)
client, err := rackspace.AuthenticatedClient(ao)
th.AssertNoErr(t, err)
c, err := rackspace.NewCDNV1(client, gophercloud.EndpointOpts{})
th.AssertNoErr(t, err)
return c
}

View File

@@ -0,0 +1 @@
package v2

View File

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

View File

@@ -0,0 +1 @@
package v1

View File

@@ -0,0 +1 @@
package v2

View File

@@ -0,0 +1,62 @@
// +build acceptance lbs
package v1
import (
"os"
"strconv"
"strings"
"testing"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/acceptance/tools"
"github.com/rackspace/gophercloud/rackspace"
th "github.com/rackspace/gophercloud/testhelper"
)
func newProvider() (*gophercloud.ProviderClient, error) {
opts, err := rackspace.AuthOptionsFromEnv()
if err != nil {
return nil, err
}
opts = tools.OnlyRS(opts)
return rackspace.AuthenticatedClient(opts)
}
func newClient() (*gophercloud.ServiceClient, error) {
provider, err := newProvider()
if err != nil {
return nil, err
}
return rackspace.NewLBV1(provider, gophercloud.EndpointOpts{
Region: os.Getenv("RS_REGION"),
})
}
func newComputeClient() (*gophercloud.ServiceClient, error) {
provider, err := newProvider()
if err != nil {
return nil, err
}
return rackspace.NewComputeV2(provider, gophercloud.EndpointOpts{
Region: os.Getenv("RS_REGION"),
})
}
func setup(t *testing.T) *gophercloud.ServiceClient {
client, err := newClient()
th.AssertNoErr(t, err)
return client
}
func intsToStr(ids []int) string {
strIDs := []string{}
for _, id := range ids {
strIDs = append(strIDs, strconv.Itoa(id))
}
return strings.Join(strIDs, ", ")
}

View File

@@ -0,0 +1,39 @@
package v2
import (
"os"
"testing"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/rackspace"
th "github.com/rackspace/gophercloud/testhelper"
)
var Client *gophercloud.ServiceClient
func NewClient() (*gophercloud.ServiceClient, error) {
opts, err := rackspace.AuthOptionsFromEnv()
if err != nil {
return nil, err
}
provider, err := rackspace.AuthenticatedClient(opts)
if err != nil {
return nil, err
}
return rackspace.NewNetworkV2(provider, gophercloud.EndpointOpts{
Name: "cloudNetworks",
Region: os.Getenv("RS_REGION"),
})
}
func Setup(t *testing.T) {
client, err := NewClient()
th.AssertNoErr(t, err)
Client = client
}
func Teardown() {
Client = nil
}

View File

@@ -0,0 +1,54 @@
// +build acceptance rackspace objectstorage v1
package v1
import (
"os"
"testing"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/acceptance/tools"
"github.com/rackspace/gophercloud/rackspace"
th "github.com/rackspace/gophercloud/testhelper"
)
func rackspaceAuthOptions(t *testing.T) gophercloud.AuthOptions {
// Obtain credentials from the environment.
options, err := rackspace.AuthOptionsFromEnv()
th.AssertNoErr(t, err)
options = tools.OnlyRS(options)
if options.Username == "" {
t.Fatal("Please provide a Rackspace username as RS_USERNAME.")
}
if options.APIKey == "" {
t.Fatal("Please provide a Rackspace API key as RS_API_KEY.")
}
return options
}
func createClient(t *testing.T, cdn bool) (*gophercloud.ServiceClient, error) {
region := os.Getenv("RS_REGION")
if region == "" {
t.Fatal("Please provide a Rackspace region as RS_REGION")
}
ao := rackspaceAuthOptions(t)
provider, err := rackspace.NewClient(ao.IdentityEndpoint)
th.AssertNoErr(t, err)
err = rackspace.Authenticate(provider, ao)
th.AssertNoErr(t, err)
if cdn {
return rackspace.NewObjectCDNV1(provider, gophercloud.EndpointOpts{
Region: region,
})
}
return rackspace.NewObjectStorageV1(provider, gophercloud.EndpointOpts{
Region: region,
})
}

View File

@@ -0,0 +1,45 @@
// +build acceptance
package v1
import (
"fmt"
"os"
"testing"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/rackspace"
th "github.com/rackspace/gophercloud/testhelper"
)
var template = fmt.Sprintf(`
{
"heat_template_version": "2013-05-23",
"description": "Simple template to test heat commands",
"parameters": {},
"resources": {
"hello_world": {
"type":"OS::Nova::Server",
"properties": {
"flavor": "%s",
"image": "%s",
"user_data": "#!/bin/bash -xv\necho \"hello world\" &gt; /root/hello-world.txt\n"
}
}
}
}
`, os.Getenv("RS_FLAVOR_ID"), os.Getenv("RS_IMAGE_ID"))
func newClient(t *testing.T) *gophercloud.ServiceClient {
ao, err := rackspace.AuthOptionsFromEnv()
th.AssertNoErr(t, err)
client, err := rackspace.AuthenticatedClient(ao)
th.AssertNoErr(t, err)
c, err := rackspace.NewOrchestrationV1(client, gophercloud.EndpointOpts{
Region: os.Getenv("RS_REGION_NAME"),
})
th.AssertNoErr(t, err)
return c
}

View File

@@ -0,0 +1 @@
package rackspace

View File

@@ -0,0 +1,26 @@
// +build acceptance
package v3
import (
"os"
"testing"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/rackspace"
th "github.com/rackspace/gophercloud/testhelper"
)
func newClient(t *testing.T) *gophercloud.ServiceClient {
ao, err := rackspace.AuthOptionsFromEnv()
th.AssertNoErr(t, err)
client, err := rackspace.AuthenticatedClient(ao)
th.AssertNoErr(t, err)
c, err := rackspace.NewRackConnectV3(client, gophercloud.EndpointOpts{
Region: os.Getenv("RS_REGION_NAME"),
})
th.AssertNoErr(t, err)
return c
}

View File

@@ -0,0 +1 @@
package tools

View File

@@ -0,0 +1,89 @@
// +build acceptance common
package tools
import (
"crypto/rand"
"errors"
mrand "math/rand"
"os"
"time"
"github.com/rackspace/gophercloud"
)
// ErrTimeout is returned if WaitFor takes longer than 300 second to happen.
var ErrTimeout = errors.New("Timed out")
// OnlyRS overrides the default Gophercloud behavior of using OS_-prefixed environment variables
// if RS_ variables aren't present. Otherwise, they'll stomp over each other here in the acceptance
// tests, where you need to have both defined.
func OnlyRS(original gophercloud.AuthOptions) gophercloud.AuthOptions {
if os.Getenv("RS_AUTH_URL") == "" {
original.IdentityEndpoint = ""
}
if os.Getenv("RS_USERNAME") == "" {
original.Username = ""
}
if os.Getenv("RS_PASSWORD") == "" {
original.Password = ""
}
if os.Getenv("RS_API_KEY") == "" {
original.APIKey = ""
}
return original
}
// WaitFor polls a predicate function once per second to wait for a certain state to arrive.
func WaitFor(predicate func() (bool, error)) error {
for i := 0; i < 300; i++ {
time.Sleep(1 * time.Second)
satisfied, err := predicate()
if err != nil {
return err
}
if satisfied {
return nil
}
}
return ErrTimeout
}
// MakeNewPassword generates a new string that's guaranteed to be different than the given one.
func MakeNewPassword(oldPass string) string {
randomPassword := RandomString("", 16)
for randomPassword == oldPass {
randomPassword = RandomString("", 16)
}
return randomPassword
}
// RandomString generates a string of given length, but random content.
// All content will be within the ASCII graphic character set.
// (Implementation from Even Shaw's contribution on
// http://stackoverflow.com/questions/12771930/what-is-the-fastest-way-to-generate-a-long-random-string-in-go).
func RandomString(prefix string, n int) string {
const alphanum = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
var bytes = make([]byte, n)
rand.Read(bytes)
for i, b := range bytes {
bytes[i] = alphanum[b%byte(len(alphanum))]
}
return prefix + string(bytes)
}
// RandomInt will return a random integer between a specified range.
func RandomInt(min, max int) int {
mrand.Seed(time.Now().Unix())
return mrand.Intn(max-min) + min
}
// Elide returns the first bit of its input string with a suffix of "..." if it's longer than
// a comfortable 40 characters.
func Elide(value string) string {
if len(value) > 40 {
return value[0:37] + "..."
}
return value
}

View File

@@ -0,0 +1,50 @@
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

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

67
vendor/github.com/rackspace/gophercloud/doc.go generated vendored Normal file
View File

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

View File

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

View File

@@ -0,0 +1,58 @@
package openstack
import (
"fmt"
"os"
"github.com/rackspace/gophercloud"
)
var nilOptions = gophercloud.AuthOptions{}
// ErrNoAuthUrl, ErrNoUsername, and ErrNoPassword errors indicate of the required OS_AUTH_URL, OS_USERNAME, or OS_PASSWORD
// environment variables, respectively, remain undefined. See the AuthOptions() function for more details.
var (
ErrNoAuthURL = fmt.Errorf("Environment variable OS_AUTH_URL needs to be set.")
ErrNoUsername = fmt.Errorf("Environment variable OS_USERNAME 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
// 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.
func AuthOptionsFromEnv() (gophercloud.AuthOptions, error) {
authURL := os.Getenv("OS_AUTH_URL")
username := os.Getenv("OS_USERNAME")
userID := os.Getenv("OS_USERID")
password := os.Getenv("OS_PASSWORD")
tenantID := os.Getenv("OS_TENANT_ID")
tenantName := os.Getenv("OS_TENANT_NAME")
domainID := os.Getenv("OS_DOMAIN_ID")
domainName := os.Getenv("OS_DOMAIN_NAME")
if authURL == "" {
return nilOptions, ErrNoAuthURL
}
if username == "" && userID == "" {
return nilOptions, ErrNoUsername
}
if password == "" {
return nilOptions, ErrNoPassword
}
ao := gophercloud.AuthOptions{
IdentityEndpoint: authURL,
UserID: userID,
Username: username,
Password: password,
TenantID: tenantID,
TenantName: tenantName,
DomainID: domainID,
DomainName: domainName,
}
return ao, nil
}

View File

@@ -0,0 +1,3 @@
// Package apiversions provides information and interaction with the different
// API versions for the OpenStack Block Storage service, code-named Cinder.
package apiversions

View File

@@ -0,0 +1,21 @@
package apiversions
import (
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
)
// List lists all the Cinder API versions available to end-users.
func List(c *gophercloud.ServiceClient) pagination.Pager {
return pagination.NewPager(c, listURL(c), func(r pagination.PageResult) pagination.Page {
return APIVersionPage{pagination.SinglePageBase(r)}
})
}
// Get will retrieve the volume type with the provided ID. To extract the volume
// type from the result, call the Extract method on the GetResult.
func Get(client *gophercloud.ServiceClient, v string) GetResult {
var res GetResult
_, res.Err = client.Get(getURL(client, v), &res.Body, nil)
return res
}

View File

@@ -0,0 +1,58 @@
package apiversions
import (
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
"github.com/mitchellh/mapstructure"
)
// APIVersion represents an API version for Cinder.
type APIVersion struct {
ID string `json:"id" mapstructure:"id"` // unique identifier
Status string `json:"status" mapstructure:"status"` // current status
Updated string `json:"updated" mapstructure:"updated"` // date last updated
}
// APIVersionPage is the page returned by a pager when traversing over a
// collection of API versions.
type APIVersionPage struct {
pagination.SinglePageBase
}
// IsEmpty checks whether an APIVersionPage struct is empty.
func (r APIVersionPage) IsEmpty() (bool, error) {
is, err := ExtractAPIVersions(r)
if err != nil {
return true, err
}
return len(is) == 0, nil
}
// ExtractAPIVersions takes a collection page, extracts all of the elements,
// and returns them a slice of APIVersion structs. It is effectively a cast.
func ExtractAPIVersions(page pagination.Page) ([]APIVersion, error) {
var resp struct {
Versions []APIVersion `mapstructure:"versions"`
}
err := mapstructure.Decode(page.(APIVersionPage).Body, &resp)
return resp.Versions, err
}
// GetResult represents the result of a get operation.
type GetResult struct {
gophercloud.Result
}
// Extract is a function that accepts a result and extracts an API version resource.
func (r GetResult) Extract() (*APIVersion, error) {
var resp struct {
Version *APIVersion `mapstructure:"version"`
}
err := mapstructure.Decode(r.Body, &resp)
return resp.Version, err
}

View File

@@ -0,0 +1,18 @@
package apiversions
import (
"strings"
"net/url"
"github.com/rackspace/gophercloud"
)
func getURL(c *gophercloud.ServiceClient, version string) string {
return c.ServiceURL(strings.TrimRight(version, "/") + "/")
}
func listURL(c *gophercloud.ServiceClient) string {
u, _ := url.Parse(c.ServiceURL(""))
u.Path = "/"
return u.String()
}

View File

@@ -0,0 +1,5 @@
// Package snapshots provides information and interaction with snapshots in the
// OpenStack Block Storage service. A snapshot is a point in time copy of the
// data contained in an external storage volume, and can be controlled
// programmatically.
package snapshots

View File

@@ -0,0 +1,114 @@
package snapshots
import (
"fmt"
"net/http"
"testing"
th "github.com/rackspace/gophercloud/testhelper"
fake "github.com/rackspace/gophercloud/testhelper/client"
)
func MockListResponse(t *testing.T) {
th.Mux.HandleFunc("/snapshots", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "GET")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, `
{
"snapshots": [
{
"id": "289da7f8-6440-407c-9fb4-7db01ec49164",
"display_name": "snapshot-001"
},
{
"id": "96c3bda7-c82a-4f50-be73-ca7621794835",
"display_name": "snapshot-002"
}
]
}
`)
})
}
func MockGetResponse(t *testing.T) {
th.Mux.HandleFunc("/snapshots/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "GET")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, `
{
"snapshot": {
"display_name": "snapshot-001",
"id": "d32019d3-bc6e-4319-9c1d-6722fc136a22"
}
}
`)
})
}
func MockCreateResponse(t *testing.T) {
th.Mux.HandleFunc("/snapshots", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "POST")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
th.TestHeader(t, r, "Content-Type", "application/json")
th.TestHeader(t, r, "Accept", "application/json")
th.TestJSONRequest(t, r, `
{
"snapshot": {
"volume_id": "1234",
"display_name": "snapshot-001"
}
}
`)
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
fmt.Fprintf(w, `
{
"snapshot": {
"volume_id": "1234",
"display_name": "snapshot-001",
"id": "d32019d3-bc6e-4319-9c1d-6722fc136a22"
}
}
`)
})
}
func MockUpdateMetadataResponse(t *testing.T) {
th.Mux.HandleFunc("/snapshots/123/metadata", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "PUT")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
th.TestHeader(t, r, "Content-Type", "application/json")
th.TestJSONRequest(t, r, `
{
"metadata": {
"key": "v1"
}
}
`)
fmt.Fprintf(w, `
{
"metadata": {
"key": "v1"
}
}
`)
})
}
func MockDeleteResponse(t *testing.T) {
th.Mux.HandleFunc("/snapshots/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "DELETE")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
w.WriteHeader(http.StatusNoContent)
})
}

View File

@@ -0,0 +1,206 @@
package snapshots
import (
"fmt"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
)
// CreateOptsBuilder allows extensions to add additional parameters to the
// Create request.
type CreateOptsBuilder interface {
ToSnapshotCreateMap() (map[string]interface{}, error)
}
// CreateOpts contains options for creating a Snapshot. This object is passed to
// the snapshots.Create function. For more information about these parameters,
// see the Snapshot object.
type CreateOpts struct {
// OPTIONAL
Description string
// OPTIONAL
Force bool
// OPTIONAL
Metadata map[string]interface{}
// OPTIONAL
Name string
// REQUIRED
VolumeID string
}
// ToSnapshotCreateMap assembles a request body based on the contents of a
// CreateOpts.
func (opts CreateOpts) ToSnapshotCreateMap() (map[string]interface{}, error) {
s := make(map[string]interface{})
if opts.VolumeID == "" {
return nil, fmt.Errorf("Required CreateOpts field 'VolumeID' not set.")
}
s["volume_id"] = opts.VolumeID
if opts.Description != "" {
s["display_description"] = opts.Description
}
if opts.Force == true {
s["force"] = opts.Force
}
if opts.Metadata != nil {
s["metadata"] = opts.Metadata
}
if opts.Name != "" {
s["display_name"] = opts.Name
}
return map[string]interface{}{"snapshot": s}, nil
}
// Create will create a new Snapshot based on the values in CreateOpts. To
// extract the Snapshot object from the response, call the Extract method on the
// CreateResult.
func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult {
var res CreateResult
reqBody, err := opts.ToSnapshotCreateMap()
if err != nil {
res.Err = err
return res
}
_, res.Err = client.Post(createURL(client), reqBody, &res.Body, &gophercloud.RequestOpts{
OkCodes: []int{200, 201},
})
return res
}
// Delete will delete the existing Snapshot with the provided ID.
func Delete(client *gophercloud.ServiceClient, id string) DeleteResult {
var res DeleteResult
_, res.Err = client.Delete(deleteURL(client, id), nil)
return res
}
// Get retrieves the Snapshot with the provided ID. To extract the Snapshot
// object from the response, call the Extract method on the GetResult.
func Get(client *gophercloud.ServiceClient, id string) GetResult {
var res GetResult
_, res.Err = client.Get(getURL(client, id), &res.Body, nil)
return res
}
// ListOptsBuilder allows extensions to add additional parameters to the List
// request.
type ListOptsBuilder interface {
ToSnapshotListQuery() (string, error)
}
// ListOpts hold options for listing Snapshots. It is passed to the
// snapshots.List function.
type ListOpts struct {
Name string `q:"display_name"`
Status string `q:"status"`
VolumeID string `q:"volume_id"`
}
// ToSnapshotListQuery formats a ListOpts into a query string.
func (opts ListOpts) ToSnapshotListQuery() (string, error) {
q, err := gophercloud.BuildQueryString(opts)
if err != nil {
return "", err
}
return q.String(), nil
}
// List returns Snapshots optionally limited by the conditions provided in
// ListOpts.
func List(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
url := listURL(client)
if opts != nil {
query, err := opts.ToSnapshotListQuery()
if err != nil {
return pagination.Pager{Err: err}
}
url += query
}
createPage := func(r pagination.PageResult) pagination.Page {
return ListResult{pagination.SinglePageBase(r)}
}
return pagination.NewPager(client, url, createPage)
}
// UpdateMetadataOptsBuilder allows extensions to add additional parameters to
// the Update request.
type UpdateMetadataOptsBuilder interface {
ToSnapshotUpdateMetadataMap() (map[string]interface{}, error)
}
// UpdateMetadataOpts contain options for updating an existing Snapshot. This
// object is passed to the snapshots.Update function. For more information
// about the parameters, see the Snapshot object.
type UpdateMetadataOpts struct {
Metadata map[string]interface{}
}
// ToSnapshotUpdateMetadataMap assembles a request body based on the contents of
// an UpdateMetadataOpts.
func (opts UpdateMetadataOpts) ToSnapshotUpdateMetadataMap() (map[string]interface{}, error) {
v := make(map[string]interface{})
if opts.Metadata != nil {
v["metadata"] = opts.Metadata
}
return v, nil
}
// UpdateMetadata will update the Snapshot with provided information. To
// extract the updated Snapshot from the response, call the ExtractMetadata
// method on the UpdateMetadataResult.
func UpdateMetadata(client *gophercloud.ServiceClient, id string, opts UpdateMetadataOptsBuilder) UpdateMetadataResult {
var res UpdateMetadataResult
reqBody, err := opts.ToSnapshotUpdateMetadataMap()
if err != nil {
res.Err = err
return res
}
_, res.Err = client.Put(updateMetadataURL(client, id), reqBody, &res.Body, &gophercloud.RequestOpts{
OkCodes: []int{200},
})
return res
}
// IDFromName is a convienience function that returns a snapshot's ID given its name.
func IDFromName(client *gophercloud.ServiceClient, name string) (string, error) {
snapshotCount := 0
snapshotID := ""
if name == "" {
return "", fmt.Errorf("A snapshot name must be provided.")
}
pager := List(client, nil)
pager.EachPage(func(page pagination.Page) (bool, error) {
snapshotList, err := ExtractSnapshots(page)
if err != nil {
return false, err
}
for _, s := range snapshotList {
if s.Name == name {
snapshotCount++
snapshotID = s.ID
}
}
return true, nil
})
switch snapshotCount {
case 0:
return "", fmt.Errorf("Unable to find snapshot: %s", name)
case 1:
return snapshotID, nil
default:
return "", fmt.Errorf("Found %d snapshots matching %s", snapshotCount, name)
}
}

View File

@@ -0,0 +1,123 @@
package snapshots
import (
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
"github.com/mitchellh/mapstructure"
)
// Snapshot contains all the information associated with an OpenStack Snapshot.
type Snapshot struct {
// Currect status of the Snapshot.
Status string `mapstructure:"status"`
// Display name.
Name string `mapstructure:"display_name"`
// Instances onto which the Snapshot is attached.
Attachments []string `mapstructure:"attachments"`
// Logical group.
AvailabilityZone string `mapstructure:"availability_zone"`
// Is the Snapshot bootable?
Bootable string `mapstructure:"bootable"`
// Date created.
CreatedAt string `mapstructure:"created_at"`
// Display description.
Description string `mapstructure:"display_discription"`
// See VolumeType object for more information.
VolumeType string `mapstructure:"volume_type"`
// ID of the Snapshot from which this Snapshot was created.
SnapshotID string `mapstructure:"snapshot_id"`
// ID of the Volume from which this Snapshot was created.
VolumeID string `mapstructure:"volume_id"`
// User-defined key-value pairs.
Metadata map[string]string `mapstructure:"metadata"`
// Unique identifier.
ID string `mapstructure:"id"`
// Size of the Snapshot, in GB.
Size int `mapstructure:"size"`
}
// CreateResult contains the response body and error from a Create request.
type CreateResult struct {
commonResult
}
// GetResult contains the response body and error from a Get request.
type GetResult struct {
commonResult
}
// DeleteResult contains the response body and error from a Delete request.
type DeleteResult struct {
gophercloud.ErrResult
}
// ListResult is a pagination.Pager that is returned from a call to the List function.
type ListResult struct {
pagination.SinglePageBase
}
// IsEmpty returns true if a ListResult contains no Snapshots.
func (r ListResult) IsEmpty() (bool, error) {
volumes, err := ExtractSnapshots(r)
if err != nil {
return true, err
}
return len(volumes) == 0, nil
}
// ExtractSnapshots extracts and returns Snapshots. It is used while iterating over a snapshots.List call.
func ExtractSnapshots(page pagination.Page) ([]Snapshot, error) {
var response struct {
Snapshots []Snapshot `json:"snapshots"`
}
err := mapstructure.Decode(page.(ListResult).Body, &response)
return response.Snapshots, err
}
// UpdateMetadataResult contains the response body and error from an UpdateMetadata request.
type UpdateMetadataResult struct {
commonResult
}
// ExtractMetadata returns the metadata from a response from snapshots.UpdateMetadata.
func (r UpdateMetadataResult) ExtractMetadata() (map[string]interface{}, error) {
if r.Err != nil {
return nil, r.Err
}
m := r.Body.(map[string]interface{})["metadata"]
return m.(map[string]interface{}), nil
}
type commonResult struct {
gophercloud.Result
}
// Extract will get the Snapshot object out of the commonResult object.
func (r commonResult) Extract() (*Snapshot, error) {
if r.Err != nil {
return nil, r.Err
}
var res struct {
Snapshot *Snapshot `json:"snapshot"`
}
err := mapstructure.Decode(r.Body, &res)
return res.Snapshot, err
}

View File

@@ -0,0 +1,27 @@
package snapshots
import "github.com/rackspace/gophercloud"
func createURL(c *gophercloud.ServiceClient) string {
return c.ServiceURL("snapshots")
}
func deleteURL(c *gophercloud.ServiceClient, id string) string {
return c.ServiceURL("snapshots", id)
}
func getURL(c *gophercloud.ServiceClient, id string) string {
return deleteURL(c, id)
}
func listURL(c *gophercloud.ServiceClient) string {
return createURL(c)
}
func metadataURL(c *gophercloud.ServiceClient, id string) string {
return c.ServiceURL("snapshots", id, "metadata")
}
func updateMetadataURL(c *gophercloud.ServiceClient, id string) string {
return metadataURL(c, id)
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,7 @@
/*
This is package created is to hold fixtures (which imports testing),
so that importing volumes package does not inadvertently import testing into production code
More information here:
https://github.com/rackspace/gophercloud/issues/473
*/
package testing

View File

@@ -0,0 +1,113 @@
package testing
import (
"fmt"
"net/http"
"testing"
th "github.com/rackspace/gophercloud/testhelper"
fake "github.com/rackspace/gophercloud/testhelper/client"
)
func MockListResponse(t *testing.T) {
th.Mux.HandleFunc("/volumes", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "GET")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, `
{
"volumes": [
{
"id": "289da7f8-6440-407c-9fb4-7db01ec49164",
"display_name": "vol-001"
},
{
"id": "96c3bda7-c82a-4f50-be73-ca7621794835",
"display_name": "vol-002"
}
]
}
`)
})
}
func MockGetResponse(t *testing.T) {
th.Mux.HandleFunc("/volumes/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "GET")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, `
{
"volume": {
"display_name": "vol-001",
"id": "d32019d3-bc6e-4319-9c1d-6722fc136a22",
"attachments": [
{
"device": "/dev/vde",
"server_id": "a740d24b-dc5b-4d59-ac75-53971c2920ba",
"id": "d6da11e5-2ed3-413e-88d8-b772ba62193d",
"volume_id": "d6da11e5-2ed3-413e-88d8-b772ba62193d"
}
]
}
}
`)
})
}
func MockCreateResponse(t *testing.T) {
th.Mux.HandleFunc("/volumes", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "POST")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
th.TestHeader(t, r, "Content-Type", "application/json")
th.TestHeader(t, r, "Accept", "application/json")
th.TestJSONRequest(t, r, `
{
"volume": {
"size": 75
}
}
`)
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
fmt.Fprintf(w, `
{
"volume": {
"size": 4,
"id": "d32019d3-bc6e-4319-9c1d-6722fc136a22"
}
}
`)
})
}
func MockDeleteResponse(t *testing.T) {
th.Mux.HandleFunc("/volumes/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "DELETE")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
w.WriteHeader(http.StatusNoContent)
})
}
func MockUpdateResponse(t *testing.T) {
th.Mux.HandleFunc("/volumes/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "PUT")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, `
{
"volume": {
"display_name": "vol-002",
"id": "d32019d3-bc6e-4319-9c1d-6722fc136a22"
}
}
`)
})
}

View File

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

View File

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

View File

@@ -0,0 +1,9 @@
// Package volumetypes provides information and interaction with volume types
// in the OpenStack Block Storage service. A volume type indicates the type of
// a block storage volume, such as SATA, SCSCI, SSD, etc. These can be
// customized or defined by the OpenStack admin.
//
// You can also define extra_specs associated with your volume types. For
// instance, you could have a VolumeType=SATA, with extra_specs (RPM=10000,
// RAID-Level=5) . Extra_specs are defined and customized by the admin.
package volumetypes

View File

@@ -0,0 +1,60 @@
package volumetypes
import (
"fmt"
"net/http"
"testing"
th "github.com/rackspace/gophercloud/testhelper"
fake "github.com/rackspace/gophercloud/testhelper/client"
)
func MockListResponse(t *testing.T) {
th.Mux.HandleFunc("/types", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "GET")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, `
{
"volume_types": [
{
"id": "289da7f8-6440-407c-9fb4-7db01ec49164",
"name": "vol-type-001",
"extra_specs": {
"capabilities": "gpu"
}
},
{
"id": "96c3bda7-c82a-4f50-be73-ca7621794835",
"name": "vol-type-002",
"extra_specs": {}
}
]
}
`)
})
}
func MockGetResponse(t *testing.T) {
th.Mux.HandleFunc("/types/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "GET")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, `
{
"volume_type": {
"name": "vol-type-001",
"id": "d32019d3-bc6e-4319-9c1d-6722fc136a22",
"extra_specs": {
"serverNumber": "2"
}
}
}
`)
})
}

View File

@@ -0,0 +1,76 @@
package volumetypes
import (
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
)
// CreateOptsBuilder allows extensions to add additional parameters to the
// Create request.
type CreateOptsBuilder interface {
ToVolumeTypeCreateMap() (map[string]interface{}, error)
}
// CreateOpts are options for creating a volume type.
type CreateOpts struct {
// OPTIONAL. See VolumeType.
ExtraSpecs map[string]interface{}
// OPTIONAL. See VolumeType.
Name string
}
// ToVolumeTypeCreateMap casts a CreateOpts struct to a map.
func (opts CreateOpts) ToVolumeTypeCreateMap() (map[string]interface{}, error) {
vt := make(map[string]interface{})
if opts.ExtraSpecs != nil {
vt["extra_specs"] = opts.ExtraSpecs
}
if opts.Name != "" {
vt["name"] = opts.Name
}
return map[string]interface{}{"volume_type": vt}, nil
}
// Create will create a new volume. To extract the created volume type object,
// call the Extract method on the CreateResult.
func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult {
var res CreateResult
reqBody, err := opts.ToVolumeTypeCreateMap()
if err != nil {
res.Err = err
return res
}
_, res.Err = client.Post(createURL(client), reqBody, &res.Body, &gophercloud.RequestOpts{
OkCodes: []int{200, 201},
})
return res
}
// Delete will delete the volume type with the provided ID.
func Delete(client *gophercloud.ServiceClient, id string) DeleteResult {
var res DeleteResult
_, res.Err = client.Delete(deleteURL(client, id), nil)
return res
}
// Get will retrieve the volume type with the provided ID. To extract the volume
// type from the result, call the Extract method on the GetResult.
func Get(client *gophercloud.ServiceClient, id string) GetResult {
var res GetResult
_, err := client.Get(getURL(client, id), &res.Body, nil)
res.Err = err
return res
}
// List returns all volume types.
func List(client *gophercloud.ServiceClient) pagination.Pager {
createPage := func(r pagination.PageResult) pagination.Page {
return ListResult{pagination.SinglePageBase(r)}
}
return pagination.NewPager(client, listURL(client), createPage)
}

View File

@@ -0,0 +1,72 @@
package volumetypes
import (
"github.com/mitchellh/mapstructure"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
)
// VolumeType contains all information associated with an OpenStack Volume Type.
type VolumeType struct {
ExtraSpecs map[string]interface{} `json:"extra_specs" mapstructure:"extra_specs"` // user-defined metadata
ID string `json:"id" mapstructure:"id"` // unique identifier
Name string `json:"name" mapstructure:"name"` // display name
}
// CreateResult contains the response body and error from a Create request.
type CreateResult struct {
commonResult
}
// GetResult contains the response body and error from a Get request.
type GetResult struct {
commonResult
}
// DeleteResult contains the response error from a Delete request.
type DeleteResult struct {
gophercloud.ErrResult
}
// ListResult is a pagination.Pager that is returned from a call to the List function.
type ListResult struct {
pagination.SinglePageBase
}
// IsEmpty returns true if a ListResult contains no Volume Types.
func (r ListResult) IsEmpty() (bool, error) {
volumeTypes, err := ExtractVolumeTypes(r)
if err != nil {
return true, err
}
return len(volumeTypes) == 0, nil
}
// ExtractVolumeTypes extracts and returns Volume Types.
func ExtractVolumeTypes(page pagination.Page) ([]VolumeType, error) {
var response struct {
VolumeTypes []VolumeType `mapstructure:"volume_types"`
}
err := mapstructure.Decode(page.(ListResult).Body, &response)
return response.VolumeTypes, err
}
type commonResult struct {
gophercloud.Result
}
// Extract will get the Volume Type object out of the commonResult object.
func (r commonResult) Extract() (*VolumeType, error) {
if r.Err != nil {
return nil, r.Err
}
var res struct {
VolumeType *VolumeType `json:"volume_type" mapstructure:"volume_type"`
}
err := mapstructure.Decode(r.Body, &res)
return res.VolumeType, err
}

View File

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

View File

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

View File

@@ -0,0 +1,53 @@
package base
import (
"fmt"
"net/http"
"testing"
th "github.com/rackspace/gophercloud/testhelper"
fake "github.com/rackspace/gophercloud/testhelper/client"
)
// HandleGetSuccessfully creates an HTTP handler at `/` on the test handler mux
// that responds with a `Get` response.
func HandleGetSuccessfully(t *testing.T) {
th.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "GET")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
th.TestHeader(t, r, "Accept", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, `
{
"resources": {
"rel/cdn": {
"href-template": "services{?marker,limit}",
"href-vars": {
"marker": "param/marker",
"limit": "param/limit"
},
"hints": {
"allow": [
"GET"
],
"formats": {
"application/json": {}
}
}
}
}
}
`)
})
}
// HandlePingSuccessfully creates an HTTP handler at `/ping` on the test handler
// mux that responds with a `Ping` response.
func HandlePingSuccessfully(t *testing.T) {
th.Mux.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "GET")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
w.WriteHeader(http.StatusNoContent)
})
}

View File

@@ -0,0 +1,21 @@
package base
import "github.com/rackspace/gophercloud"
// Get retrieves the home document, allowing the user to discover the
// entire API.
func Get(c *gophercloud.ServiceClient) GetResult {
var res GetResult
_, res.Err = c.Get(getURL(c), &res.Body, nil)
return res
}
// Ping retrieves a ping to the server.
func Ping(c *gophercloud.ServiceClient) PingResult {
var res PingResult
_, res.Err = c.Get(pingURL(c), nil, &gophercloud.RequestOpts{
OkCodes: []int{204},
MoreHeaders: map[string]string{"Accept": ""},
})
return res
}

View File

@@ -0,0 +1,35 @@
package base
import (
"errors"
"github.com/rackspace/gophercloud"
)
// HomeDocument is a resource that contains all the resources for the CDN API.
type HomeDocument map[string]interface{}
// GetResult represents the result of a Get operation.
type GetResult struct {
gophercloud.Result
}
// Extract is a function that accepts a result and extracts a home document resource.
func (r GetResult) Extract() (*HomeDocument, error) {
if r.Err != nil {
return nil, r.Err
}
submap, ok := r.Body.(map[string]interface{})["resources"]
if !ok {
return nil, errors.New("Unexpected HomeDocument structure")
}
casted := HomeDocument(submap.(map[string]interface{}))
return &casted, nil
}
// PingResult represents the result of a Ping operation.
type PingResult struct {
gophercloud.ErrResult
}

View File

@@ -0,0 +1,11 @@
package base
import "github.com/rackspace/gophercloud"
func getURL(c *gophercloud.ServiceClient) string {
return c.ServiceURL()
}
func pingURL(c *gophercloud.ServiceClient) string {
return c.ServiceURL("ping")
}

View File

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

View File

@@ -0,0 +1,82 @@
package flavors
import (
"fmt"
"net/http"
"testing"
th "github.com/rackspace/gophercloud/testhelper"
fake "github.com/rackspace/gophercloud/testhelper/client"
)
// HandleListCDNFlavorsSuccessfully creates an HTTP handler at `/flavors` on the test handler mux
// that responds with a `List` response.
func HandleListCDNFlavorsSuccessfully(t *testing.T) {
th.Mux.HandleFunc("/flavors", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "GET")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, `
{
"flavors": [
{
"id": "europe",
"providers": [
{
"provider": "Fastly",
"links": [
{
"href": "http://www.fastly.com",
"rel": "provider_url"
}
]
}
],
"links": [
{
"href": "https://www.poppycdn.io/v1.0/flavors/europe",
"rel": "self"
}
]
}
]
}
`)
})
}
// HandleGetCDNFlavorSuccessfully creates an HTTP handler at `/flavors/{id}` on the test handler mux
// that responds with a `Get` response.
func HandleGetCDNFlavorSuccessfully(t *testing.T) {
th.Mux.HandleFunc("/flavors/asia", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "GET")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, `
{
"id" : "asia",
"providers" : [
{
"provider" : "ChinaCache",
"links": [
{
"href": "http://www.chinacache.com",
"rel": "provider_url"
}
]
}
],
"links": [
{
"href": "https://www.poppycdn.io/v1.0/flavors/asia",
"rel": "self"
}
]
}
`)
})
}

View File

@@ -0,0 +1,22 @@
package flavors
import (
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
)
// List returns a single page of CDN flavors.
func List(c *gophercloud.ServiceClient) pagination.Pager {
url := listURL(c)
createPage := func(r pagination.PageResult) pagination.Page {
return FlavorPage{pagination.SinglePageBase(r)}
}
return pagination.NewPager(c, url, createPage)
}
// Get retrieves a specific flavor based on its unique ID.
func Get(c *gophercloud.ServiceClient, id string) GetResult {
var res GetResult
_, res.Err = c.Get(getURL(c, id), &res.Body, nil)
return res
}

View File

@@ -0,0 +1,71 @@
package flavors
import (
"github.com/mitchellh/mapstructure"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
)
// Provider represents a provider for a particular flavor.
type Provider struct {
// Specifies the name of the provider. The name must not exceed 64 bytes in
// length and is limited to unicode, digits, underscores, and hyphens.
Provider string `mapstructure:"provider"`
// Specifies a list with an href where rel is provider_url.
Links []gophercloud.Link `mapstructure:"links"`
}
// Flavor represents a mapping configuration to a CDN provider.
type Flavor struct {
// Specifies the name of the flavor. The name must not exceed 64 bytes in
// length and is limited to unicode, digits, underscores, and hyphens.
ID string `mapstructure:"id"`
// Specifies the list of providers mapped to this flavor.
Providers []Provider `mapstructure:"providers"`
// Specifies the self-navigating JSON document paths.
Links []gophercloud.Link `mapstructure:"links"`
}
// FlavorPage is the page returned by a pager when traversing over a
// collection of CDN flavors.
type FlavorPage struct {
pagination.SinglePageBase
}
// IsEmpty returns true if a FlavorPage contains no Flavors.
func (r FlavorPage) IsEmpty() (bool, error) {
flavors, err := ExtractFlavors(r)
if err != nil {
return true, err
}
return len(flavors) == 0, nil
}
// ExtractFlavors extracts and returns Flavors. It is used while iterating over
// a flavors.List call.
func ExtractFlavors(page pagination.Page) ([]Flavor, error) {
var response struct {
Flavors []Flavor `json:"flavors"`
}
err := mapstructure.Decode(page.(FlavorPage).Body, &response)
return response.Flavors, err
}
// GetResult represents the result of a get operation.
type GetResult struct {
gophercloud.Result
}
// Extract is a function that extracts a flavor from a GetResult.
func (r GetResult) Extract() (*Flavor, error) {
if r.Err != nil {
return nil, r.Err
}
var res Flavor
err := mapstructure.Decode(r.Body, &res)
return &res, err
}

View File

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

View File

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

View File

@@ -0,0 +1,19 @@
package serviceassets
import (
"net/http"
"testing"
th "github.com/rackspace/gophercloud/testhelper"
fake "github.com/rackspace/gophercloud/testhelper/client"
)
// HandleDeleteCDNAssetSuccessfully creates an HTTP handler at `/services/{id}/assets` on the test handler mux
// that responds with a `Delete` response.
func HandleDeleteCDNAssetSuccessfully(t *testing.T) {
th.Mux.HandleFunc("/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0/assets", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "DELETE")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
w.WriteHeader(http.StatusAccepted)
})
}

View File

@@ -0,0 +1,48 @@
package serviceassets
import (
"strings"
"github.com/rackspace/gophercloud"
)
// DeleteOptsBuilder allows extensions to add additional parameters to the Delete
// request.
type DeleteOptsBuilder interface {
ToCDNAssetDeleteParams() (string, error)
}
// DeleteOpts is a structure that holds options for deleting CDN service assets.
type DeleteOpts struct {
// If all is set to true, specifies that the delete occurs against all of the
// assets for the service.
All bool `q:"all"`
// Specifies the relative URL of the asset to be deleted.
URL string `q:"url"`
}
// ToCDNAssetDeleteParams formats a DeleteOpts into a query string.
func (opts DeleteOpts) ToCDNAssetDeleteParams() (string, error) {
q, err := gophercloud.BuildQueryString(opts)
if err != nil {
return "", err
}
return q.String(), nil
}
// Delete accepts a unique service ID or URL and deletes the CDN service asset associated with
// it. For example, both "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0" and
// "https://global.cdn.api.rackspacecloud.com/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0"
// are valid options for idOrURL.
func Delete(c *gophercloud.ServiceClient, idOrURL string, opts DeleteOptsBuilder) DeleteResult {
var url string
if strings.Contains(idOrURL, "/") {
url = idOrURL
} else {
url = deleteURL(c, idOrURL)
}
var res DeleteResult
_, res.Err = c.Delete(url, nil)
return res
}

View File

@@ -0,0 +1,8 @@
package serviceassets
import "github.com/rackspace/gophercloud"
// DeleteResult represents the result of a Delete operation.
type DeleteResult struct {
gophercloud.ErrResult
}

View File

@@ -0,0 +1,7 @@
package serviceassets
import "github.com/rackspace/gophercloud"
func deleteURL(c *gophercloud.ServiceClient, id string) string {
return c.ServiceURL("services", id, "assets")
}

View File

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

View File

@@ -0,0 +1,7 @@
package services
import "fmt"
func no(str string) error {
return fmt.Errorf("Required parameter %s not provided", str)
}

View File

@@ -0,0 +1,372 @@
package services
import (
"fmt"
"net/http"
"testing"
th "github.com/rackspace/gophercloud/testhelper"
fake "github.com/rackspace/gophercloud/testhelper/client"
)
// HandleListCDNServiceSuccessfully creates an HTTP handler at `/services` on the test handler mux
// that responds with a `List` response.
func HandleListCDNServiceSuccessfully(t *testing.T) {
th.Mux.HandleFunc("/services", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "GET")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
r.ParseForm()
marker := r.Form.Get("marker")
switch marker {
case "":
fmt.Fprintf(w, `
{
"links": [
{
"rel": "next",
"href": "https://www.poppycdn.io/v1.0/services?marker=96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0&limit=20"
}
],
"services": [
{
"id": "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0",
"name": "mywebsite.com",
"domains": [
{
"domain": "www.mywebsite.com"
}
],
"origins": [
{
"origin": "mywebsite.com",
"port": 80,
"ssl": false
}
],
"caching": [
{
"name": "default",
"ttl": 3600
},
{
"name": "home",
"ttl": 17200,
"rules": [
{
"name": "index",
"request_url": "/index.htm"
}
]
},
{
"name": "images",
"ttl": 12800,
"rules": [
{
"name": "images",
"request_url": "*.png"
}
]
}
],
"restrictions": [
{
"name": "website only",
"rules": [
{
"name": "mywebsite.com",
"referrer": "www.mywebsite.com"
}
]
}
],
"flavor_id": "asia",
"status": "deployed",
"errors" : [],
"links": [
{
"href": "https://www.poppycdn.io/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0",
"rel": "self"
},
{
"href": "mywebsite.com.cdn123.poppycdn.net",
"rel": "access_url"
},
{
"href": "https://www.poppycdn.io/v1.0/flavors/asia",
"rel": "flavor"
}
]
},
{
"id": "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f1",
"name": "myothersite.com",
"domains": [
{
"domain": "www.myothersite.com"
}
],
"origins": [
{
"origin": "44.33.22.11",
"port": 80,
"ssl": false
},
{
"origin": "77.66.55.44",
"port": 80,
"ssl": false,
"rules": [
{
"name": "videos",
"request_url": "^/videos/*.m3u"
}
]
}
],
"caching": [
{
"name": "default",
"ttl": 3600
}
],
"restrictions": [
{}
],
"flavor_id": "europe",
"status": "deployed",
"links": [
{
"href": "https://www.poppycdn.io/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f1",
"rel": "self"
},
{
"href": "myothersite.com.poppycdn.net",
"rel": "access_url"
},
{
"href": "https://www.poppycdn.io/v1.0/flavors/europe",
"rel": "flavor"
}
]
}
]
}
`)
case "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f1":
fmt.Fprintf(w, `{
"services": []
}`)
default:
t.Fatalf("Unexpected marker: [%s]", marker)
}
})
}
// HandleCreateCDNServiceSuccessfully creates an HTTP handler at `/services` on the test handler mux
// that responds with a `Create` response.
func HandleCreateCDNServiceSuccessfully(t *testing.T) {
th.Mux.HandleFunc("/services", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "POST")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
th.TestJSONRequest(t, r, `
{
"name": "mywebsite.com",
"domains": [
{
"domain": "www.mywebsite.com"
},
{
"domain": "blog.mywebsite.com"
}
],
"origins": [
{
"origin": "mywebsite.com",
"port": 80,
"ssl": false
}
],
"restrictions": [
{
"name": "website only",
"rules": [
{
"name": "mywebsite.com",
"referrer": "www.mywebsite.com"
}
]
}
],
"caching": [
{
"name": "default",
"ttl": 3600
}
],
"flavor_id": "cdn"
}
`)
w.Header().Add("Location", "https://global.cdn.api.rackspacecloud.com/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0")
w.WriteHeader(http.StatusAccepted)
})
}
// HandleGetCDNServiceSuccessfully creates an HTTP handler at `/services/{id}` on the test handler mux
// that responds with a `Get` response.
func HandleGetCDNServiceSuccessfully(t *testing.T) {
th.Mux.HandleFunc("/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "GET")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, `
{
"id": "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0",
"name": "mywebsite.com",
"domains": [
{
"domain": "www.mywebsite.com",
"protocol": "http"
}
],
"origins": [
{
"origin": "mywebsite.com",
"port": 80,
"ssl": false
}
],
"caching": [
{
"name": "default",
"ttl": 3600
},
{
"name": "home",
"ttl": 17200,
"rules": [
{
"name": "index",
"request_url": "/index.htm"
}
]
},
{
"name": "images",
"ttl": 12800,
"rules": [
{
"name": "images",
"request_url": "*.png"
}
]
}
],
"restrictions": [
{
"name": "website only",
"rules": [
{
"name": "mywebsite.com",
"referrer": "www.mywebsite.com"
}
]
}
],
"flavor_id": "cdn",
"status": "deployed",
"errors" : [],
"links": [
{
"href": "https://global.cdn.api.rackspacecloud.com/v1.0/110011/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0",
"rel": "self"
},
{
"href": "blog.mywebsite.com.cdn1.raxcdn.com",
"rel": "access_url"
},
{
"href": "https://global.cdn.api.rackspacecloud.com/v1.0/110011/flavors/cdn",
"rel": "flavor"
}
]
}
`)
})
}
// HandleUpdateCDNServiceSuccessfully creates an HTTP handler at `/services/{id}` on the test handler mux
// that responds with a `Update` response.
func HandleUpdateCDNServiceSuccessfully(t *testing.T) {
th.Mux.HandleFunc("/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "PATCH")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
th.TestJSONRequest(t, r, `
[
{
"op": "add",
"path": "/domains/-",
"value": {"domain": "appended.mocksite4.com"}
},
{
"op": "add",
"path": "/domains/4",
"value": {"domain": "inserted.mocksite4.com"}
},
{
"op": "add",
"path": "/domains",
"value": [
{"domain": "bulkadded1.mocksite4.com"},
{"domain": "bulkadded2.mocksite4.com"}
]
},
{
"op": "replace",
"path": "/origins/2",
"value": {"origin": "44.33.22.11", "port": 80, "ssl": false}
},
{
"op": "replace",
"path": "/origins",
"value": [
{"origin": "44.33.22.11", "port": 80, "ssl": false},
{"origin": "55.44.33.22", "port": 443, "ssl": true}
]
},
{
"op": "remove",
"path": "/caching/8"
},
{
"op": "remove",
"path": "/caching"
},
{
"op": "replace",
"path": "/name",
"value": "differentServiceName"
}
]
`)
w.Header().Add("Location", "https://www.poppycdn.io/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0")
w.WriteHeader(http.StatusAccepted)
})
}
// HandleDeleteCDNServiceSuccessfully creates an HTTP handler at `/services/{id}` on the test handler mux
// that responds with a `Delete` response.
func HandleDeleteCDNServiceSuccessfully(t *testing.T) {
th.Mux.HandleFunc("/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "DELETE")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
w.WriteHeader(http.StatusAccepted)
})
}

View File

@@ -0,0 +1,378 @@
package services
import (
"fmt"
"strings"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
)
// ListOptsBuilder allows extensions to add additional parameters to the
// List request.
type ListOptsBuilder interface {
ToCDNServiceListQuery() (string, error)
}
// ListOpts allows the filtering and sorting of paginated collections through
// the API. Marker and Limit are used for pagination.
type ListOpts struct {
Marker string `q:"marker"`
Limit int `q:"limit"`
}
// ToCDNServiceListQuery formats a ListOpts into a query string.
func (opts ListOpts) ToCDNServiceListQuery() (string, error) {
q, err := gophercloud.BuildQueryString(opts)
if err != nil {
return "", err
}
return q.String(), nil
}
// List returns a Pager which allows you to iterate over a collection of
// CDN services. It accepts a ListOpts struct, which allows for pagination via
// marker and limit.
func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
url := listURL(c)
if opts != nil {
query, err := opts.ToCDNServiceListQuery()
if err != nil {
return pagination.Pager{Err: err}
}
url += query
}
createPage := func(r pagination.PageResult) pagination.Page {
p := ServicePage{pagination.MarkerPageBase{PageResult: r}}
p.MarkerPageBase.Owner = p
return p
}
pager := pagination.NewPager(c, url, createPage)
return pager
}
// CreateOptsBuilder is the interface options structs have to satisfy in order
// to be used in the main Create operation in this package. Since many
// extensions decorate or modify the common logic, it is useful for them to
// satisfy a basic interface in order for them to be used.
type CreateOptsBuilder interface {
ToCDNServiceCreateMap() (map[string]interface{}, error)
}
// CreateOpts is the common options struct used in this package's Create
// operation.
type CreateOpts struct {
// REQUIRED. Specifies the name of the service. The minimum length for name is
// 3. The maximum length is 256.
Name string
// REQUIRED. Specifies a list of domains used by users to access their website.
Domains []Domain
// REQUIRED. Specifies a list of origin domains or IP addresses where the
// original assets are stored.
Origins []Origin
// REQUIRED. Specifies the CDN provider flavor ID to use. For a list of
// flavors, see the operation to list the available flavors. The minimum
// length for flavor_id is 1. The maximum length is 256.
FlavorID string
// OPTIONAL. Specifies the TTL rules for the assets under this service. Supports wildcards for fine-grained control.
Caching []CacheRule
// OPTIONAL. Specifies the restrictions that define who can access assets (content from the CDN cache).
Restrictions []Restriction
}
// ToCDNServiceCreateMap casts a CreateOpts struct to a map.
func (opts CreateOpts) ToCDNServiceCreateMap() (map[string]interface{}, error) {
s := make(map[string]interface{})
if opts.Name == "" {
return nil, no("Name")
}
s["name"] = opts.Name
if opts.Domains == nil {
return nil, no("Domains")
}
for _, domain := range opts.Domains {
if domain.Domain == "" {
return nil, no("Domains[].Domain")
}
}
s["domains"] = opts.Domains
if opts.Origins == nil {
return nil, no("Origins")
}
for _, origin := range opts.Origins {
if origin.Origin == "" {
return nil, no("Origins[].Origin")
}
if origin.Rules == nil && len(opts.Origins) > 1 {
return nil, no("Origins[].Rules")
}
for _, rule := range origin.Rules {
if rule.Name == "" {
return nil, no("Origins[].Rules[].Name")
}
if rule.RequestURL == "" {
return nil, no("Origins[].Rules[].RequestURL")
}
}
}
s["origins"] = opts.Origins
if opts.FlavorID == "" {
return nil, no("FlavorID")
}
s["flavor_id"] = opts.FlavorID
if opts.Caching != nil {
for _, cache := range opts.Caching {
if cache.Name == "" {
return nil, no("Caching[].Name")
}
if cache.Rules != nil {
for _, rule := range cache.Rules {
if rule.Name == "" {
return nil, no("Caching[].Rules[].Name")
}
if rule.RequestURL == "" {
return nil, no("Caching[].Rules[].RequestURL")
}
}
}
}
s["caching"] = opts.Caching
}
if opts.Restrictions != nil {
for _, restriction := range opts.Restrictions {
if restriction.Name == "" {
return nil, no("Restrictions[].Name")
}
if restriction.Rules != nil {
for _, rule := range restriction.Rules {
if rule.Name == "" {
return nil, no("Restrictions[].Rules[].Name")
}
}
}
}
s["restrictions"] = opts.Restrictions
}
return s, nil
}
// Create accepts a CreateOpts struct and creates a new CDN service using the
// values provided.
func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult {
var res CreateResult
reqBody, err := opts.ToCDNServiceCreateMap()
if err != nil {
res.Err = err
return res
}
// Send request to API
resp, err := c.Post(createURL(c), &reqBody, nil, nil)
res.Header = resp.Header
res.Err = err
return res
}
// Get retrieves a specific service based on its URL or its unique ID. For
// example, both "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0" and
// "https://global.cdn.api.rackspacecloud.com/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0"
// are valid options for idOrURL.
func Get(c *gophercloud.ServiceClient, idOrURL string) GetResult {
var url string
if strings.Contains(idOrURL, "/") {
url = idOrURL
} else {
url = getURL(c, idOrURL)
}
var res GetResult
_, res.Err = c.Get(url, &res.Body, nil)
return res
}
// Path is a JSON pointer location that indicates which service parameter is being added, replaced,
// or removed.
type Path struct {
baseElement string
}
func (p Path) renderRoot() string {
return "/" + p.baseElement
}
func (p Path) renderDash() string {
return fmt.Sprintf("/%s/-", p.baseElement)
}
func (p Path) renderIndex(index int64) string {
return fmt.Sprintf("/%s/%d", p.baseElement, index)
}
var (
// PathDomains indicates that an update operation is to be performed on a Domain.
PathDomains = Path{baseElement: "domains"}
// PathOrigins indicates that an update operation is to be performed on an Origin.
PathOrigins = Path{baseElement: "origins"}
// PathCaching indicates that an update operation is to be performed on a CacheRule.
PathCaching = Path{baseElement: "caching"}
)
type value interface {
toPatchValue() interface{}
appropriatePath() Path
renderRootOr(func(p Path) string) string
}
// Patch represents a single update to an existing Service. Multiple updates to a service can be
// submitted at the same time.
type Patch interface {
ToCDNServiceUpdateMap() map[string]interface{}
}
// Insertion is a Patch that requests the addition of a value (Domain, Origin, or CacheRule) to
// a Service at a fixed index. Use an Append instead to append the new value to the end of its
// collection. Pass it to the Update function as part of the Patch slice.
type Insertion struct {
Index int64
Value value
}
// ToCDNServiceUpdateMap converts an Insertion into a request body fragment suitable for the
// Update call.
func (i Insertion) ToCDNServiceUpdateMap() map[string]interface{} {
return map[string]interface{}{
"op": "add",
"path": i.Value.renderRootOr(func(p Path) string { return p.renderIndex(i.Index) }),
"value": i.Value.toPatchValue(),
}
}
// Append is a Patch that requests the addition of a value (Domain, Origin, or CacheRule) to a
// Service at the end of its respective collection. Use an Insertion instead to insert the value
// at a fixed index within the collection. Pass this to the Update function as part of its
// Patch slice.
type Append struct {
Value value
}
// ToCDNServiceUpdateMap converts an Append into a request body fragment suitable for the
// Update call.
func (a Append) ToCDNServiceUpdateMap() map[string]interface{} {
return map[string]interface{}{
"op": "add",
"path": a.Value.renderRootOr(func(p Path) string { return p.renderDash() }),
"value": a.Value.toPatchValue(),
}
}
// Replacement is a Patch that alters a specific service parameter (Domain, Origin, or CacheRule)
// in-place by index. Pass it to the Update function as part of the Patch slice.
type Replacement struct {
Value value
Index int64
}
// ToCDNServiceUpdateMap converts a Replacement into a request body fragment suitable for the
// Update call.
func (r Replacement) ToCDNServiceUpdateMap() map[string]interface{} {
return map[string]interface{}{
"op": "replace",
"path": r.Value.renderRootOr(func(p Path) string { return p.renderIndex(r.Index) }),
"value": r.Value.toPatchValue(),
}
}
// NameReplacement specifically updates the Service name. Pass it to the Update function as part
// of the Patch slice.
type NameReplacement struct {
NewName string
}
// ToCDNServiceUpdateMap converts a NameReplacement into a request body fragment suitable for the
// Update call.
func (r NameReplacement) ToCDNServiceUpdateMap() map[string]interface{} {
return map[string]interface{}{
"op": "replace",
"path": "/name",
"value": r.NewName,
}
}
// Removal is a Patch that requests the removal of a service parameter (Domain, Origin, or
// CacheRule) by index. Pass it to the Update function as part of the Patch slice.
type Removal struct {
Path Path
Index int64
All bool
}
// ToCDNServiceUpdateMap converts a Removal into a request body fragment suitable for the
// Update call.
func (r Removal) ToCDNServiceUpdateMap() map[string]interface{} {
result := map[string]interface{}{"op": "remove"}
if r.All {
result["path"] = r.Path.renderRoot()
} else {
result["path"] = r.Path.renderIndex(r.Index)
}
return result
}
type UpdateOpts []Patch
// Update accepts a slice of Patch operations (Insertion, Append, Replacement or Removal) and
// updates an existing CDN service using the values provided. idOrURL can be either the service's
// URL or its ID. For example, both "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0" and
// "https://global.cdn.api.rackspacecloud.com/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0"
// are valid options for idOrURL.
func Update(c *gophercloud.ServiceClient, idOrURL string, opts UpdateOpts) UpdateResult {
var url string
if strings.Contains(idOrURL, "/") {
url = idOrURL
} else {
url = updateURL(c, idOrURL)
}
reqBody := make([]map[string]interface{}, len(opts))
for i, patch := range opts {
reqBody[i] = patch.ToCDNServiceUpdateMap()
}
resp, err := c.Request("PATCH", url, gophercloud.RequestOpts{
JSONBody: &reqBody,
OkCodes: []int{202},
})
var result UpdateResult
result.Header = resp.Header
result.Err = err
return result
}
// Delete accepts a service's ID or its URL and deletes the CDN service
// associated with it. For example, both "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0" and
// "https://global.cdn.api.rackspacecloud.com/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0"
// are valid options for idOrURL.
func Delete(c *gophercloud.ServiceClient, idOrURL string) DeleteResult {
var url string
if strings.Contains(idOrURL, "/") {
url = idOrURL
} else {
url = deleteURL(c, idOrURL)
}
var res DeleteResult
_, res.Err = c.Delete(url, nil)
return res
}

View File

@@ -0,0 +1,316 @@
package services
import (
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
"github.com/mitchellh/mapstructure"
)
// Domain represents a domain used by users to access their website.
type Domain struct {
// Specifies the domain used to access the assets on their website, for which
// a CNAME is given to the CDN provider.
Domain string `mapstructure:"domain" json:"domain"`
// Specifies the protocol used to access the assets on this domain. Only "http"
// or "https" are currently allowed. The default is "http".
Protocol string `mapstructure:"protocol" json:"protocol,omitempty"`
}
func (d Domain) toPatchValue() interface{} {
r := make(map[string]interface{})
r["domain"] = d.Domain
if d.Protocol != "" {
r["protocol"] = d.Protocol
}
return r
}
func (d Domain) appropriatePath() Path {
return PathDomains
}
func (d Domain) renderRootOr(render func(p Path) string) string {
return render(d.appropriatePath())
}
// DomainList provides a useful way to perform bulk operations in a single Patch.
type DomainList []Domain
func (list DomainList) toPatchValue() interface{} {
r := make([]interface{}, len(list))
for i, domain := range list {
r[i] = domain.toPatchValue()
}
return r
}
func (list DomainList) appropriatePath() Path {
return PathDomains
}
func (list DomainList) renderRootOr(_ func(p Path) string) string {
return list.appropriatePath().renderRoot()
}
// OriginRule represents a rule that defines when an origin should be accessed.
type OriginRule struct {
// Specifies the name of this rule.
Name string `mapstructure:"name" json:"name"`
// Specifies the request URL this rule should match for this origin to be used. Regex is supported.
RequestURL string `mapstructure:"request_url" json:"request_url"`
}
// Origin specifies a list of origin domains or IP addresses where the original assets are stored.
type Origin struct {
// Specifies the URL or IP address to pull origin content from.
Origin string `mapstructure:"origin" json:"origin"`
// Specifies the port used to access the origin. The default is port 80.
Port int `mapstructure:"port" json:"port,omitempty"`
// Specifies whether or not to use HTTPS to access the origin. The default
// is false.
SSL bool `mapstructure:"ssl" json:"ssl"`
// Specifies a collection of rules that define the conditions when this origin
// should be accessed. If there is more than one origin, the rules parameter is required.
Rules []OriginRule `mapstructure:"rules" json:"rules,omitempty"`
}
func (o Origin) toPatchValue() interface{} {
r := make(map[string]interface{})
r["origin"] = o.Origin
r["port"] = o.Port
r["ssl"] = o.SSL
if len(o.Rules) > 0 {
r["rules"] = make([]map[string]interface{}, len(o.Rules))
for index, rule := range o.Rules {
submap := r["rules"].([]map[string]interface{})[index]
submap["name"] = rule.Name
submap["request_url"] = rule.RequestURL
}
}
return r
}
func (o Origin) appropriatePath() Path {
return PathOrigins
}
func (o Origin) renderRootOr(render func(p Path) string) string {
return render(o.appropriatePath())
}
// OriginList provides a useful way to perform bulk operations in a single Patch.
type OriginList []Origin
func (list OriginList) toPatchValue() interface{} {
r := make([]interface{}, len(list))
for i, origin := range list {
r[i] = origin.toPatchValue()
}
return r
}
func (list OriginList) appropriatePath() Path {
return PathOrigins
}
func (list OriginList) renderRootOr(_ func(p Path) string) string {
return list.appropriatePath().renderRoot()
}
// TTLRule specifies a rule that determines if a TTL should be applied to an asset.
type TTLRule struct {
// Specifies the name of this rule.
Name string `mapstructure:"name" json:"name"`
// Specifies the request URL this rule should match for this TTL to be used. Regex is supported.
RequestURL string `mapstructure:"request_url" json:"request_url"`
}
// CacheRule specifies the TTL rules for the assets under this service.
type CacheRule struct {
// Specifies the name of this caching rule. Note: 'default' is a reserved name used for the default TTL setting.
Name string `mapstructure:"name" json:"name"`
// Specifies the TTL to apply.
TTL int `mapstructure:"ttl" json:"ttl"`
// Specifies a collection of rules that determine if this TTL should be applied to an asset.
Rules []TTLRule `mapstructure:"rules" json:"rules,omitempty"`
}
func (c CacheRule) toPatchValue() interface{} {
r := make(map[string]interface{})
r["name"] = c.Name
r["ttl"] = c.TTL
r["rules"] = make([]map[string]interface{}, len(c.Rules))
for index, rule := range c.Rules {
submap := r["rules"].([]map[string]interface{})[index]
submap["name"] = rule.Name
submap["request_url"] = rule.RequestURL
}
return r
}
func (c CacheRule) appropriatePath() Path {
return PathCaching
}
func (c CacheRule) renderRootOr(render func(p Path) string) string {
return render(c.appropriatePath())
}
// CacheRuleList provides a useful way to perform bulk operations in a single Patch.
type CacheRuleList []CacheRule
func (list CacheRuleList) toPatchValue() interface{} {
r := make([]interface{}, len(list))
for i, rule := range list {
r[i] = rule.toPatchValue()
}
return r
}
func (list CacheRuleList) appropriatePath() Path {
return PathCaching
}
func (list CacheRuleList) renderRootOr(_ func(p Path) string) string {
return list.appropriatePath().renderRoot()
}
// RestrictionRule specifies a rule that determines if this restriction should be applied to an asset.
type RestrictionRule struct {
// Specifies the name of this rule.
Name string `mapstructure:"name" json:"name"`
// Specifies the http host that requests must come from.
Referrer string `mapstructure:"referrer" json:"referrer,omitempty"`
}
// Restriction specifies a restriction that defines who can access assets (content from the CDN cache).
type Restriction struct {
// Specifies the name of this restriction.
Name string `mapstructure:"name" json:"name"`
// Specifies a collection of rules that determine if this TTL should be applied to an asset.
Rules []RestrictionRule `mapstructure:"rules" json:"rules"`
}
// Error specifies an error that occurred during the previous service action.
type Error struct {
// Specifies an error message detailing why there is an error.
Message string `mapstructure:"message"`
}
// Service represents a CDN service resource.
type Service struct {
// Specifies the service ID that represents distributed content. The value is
// a UUID, such as 96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0, that is generated by the server.
ID string `mapstructure:"id"`
// Specifies the name of the service.
Name string `mapstructure:"name"`
// Specifies a list of domains used by users to access their website.
Domains []Domain `mapstructure:"domains"`
// Specifies a list of origin domains or IP addresses where the original assets are stored.
Origins []Origin `mapstructure:"origins"`
// Specifies the TTL rules for the assets under this service. Supports wildcards for fine grained control.
Caching []CacheRule `mapstructure:"caching"`
// Specifies the restrictions that define who can access assets (content from the CDN cache).
Restrictions []Restriction `mapstructure:"restrictions" json:"restrictions,omitempty"`
// Specifies the CDN provider flavor ID to use. For a list of flavors, see the operation to list the available flavors.
FlavorID string `mapstructure:"flavor_id"`
// Specifies the current status of the service.
Status string `mapstructure:"status"`
// Specifies the list of errors that occurred during the previous service action.
Errors []Error `mapstructure:"errors"`
// Specifies the self-navigating JSON document paths.
Links []gophercloud.Link `mapstructure:"links"`
}
// ServicePage is the page returned by a pager when traversing over a
// collection of CDN services.
type ServicePage struct {
pagination.MarkerPageBase
}
// IsEmpty returns true if a ListResult contains no services.
func (r ServicePage) IsEmpty() (bool, error) {
services, err := ExtractServices(r)
if err != nil {
return true, err
}
return len(services) == 0, nil
}
// LastMarker returns the last service in a ListResult.
func (r ServicePage) LastMarker() (string, error) {
services, err := ExtractServices(r)
if err != nil {
return "", err
}
if len(services) == 0 {
return "", nil
}
return (services[len(services)-1]).ID, nil
}
// ExtractServices is a function that takes a ListResult and returns the services' information.
func ExtractServices(page pagination.Page) ([]Service, error) {
var response struct {
Services []Service `mapstructure:"services"`
}
err := mapstructure.Decode(page.(ServicePage).Body, &response)
return response.Services, err
}
// CreateResult represents the result of a Create operation.
type CreateResult struct {
gophercloud.Result
}
// Extract is a method that extracts the location of a newly created service.
func (r CreateResult) Extract() (string, error) {
if r.Err != nil {
return "", r.Err
}
if l, ok := r.Header["Location"]; ok && len(l) > 0 {
return l[0], nil
}
return "", nil
}
// GetResult represents the result of a get operation.
type GetResult struct {
gophercloud.Result
}
// Extract is a function that extracts a service from a GetResult.
func (r GetResult) Extract() (*Service, error) {
if r.Err != nil {
return nil, r.Err
}
var res Service
err := mapstructure.Decode(r.Body, &res)
return &res, err
}
// UpdateResult represents the result of a Update operation.
type UpdateResult struct {
gophercloud.Result
}
// Extract is a method that extracts the location of an updated service.
func (r UpdateResult) Extract() (string, error) {
if r.Err != nil {
return "", r.Err
}
if l, ok := r.Header["Location"]; ok && len(l) > 0 {
return l[0], nil
}
return "", nil
}
// DeleteResult represents the result of a Delete operation.
type DeleteResult struct {
gophercloud.ErrResult
}

View File

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

View File

@@ -0,0 +1,309 @@
package openstack
import (
"fmt"
"net/url"
"strings"
"github.com/rackspace/gophercloud"
tokens2 "github.com/rackspace/gophercloud/openstack/identity/v2/tokens"
tokens3 "github.com/rackspace/gophercloud/openstack/identity/v3/tokens"
"github.com/rackspace/gophercloud/openstack/utils"
)
const (
v20 = "v2.0"
v30 = "v3.0"
)
// NewClient prepares an unauthenticated ProviderClient instance.
// Most users will probably prefer using the AuthenticatedClient function instead.
// This is useful if you wish to explicitly control the version of the identity service that's used for authentication explicitly,
// for example.
func NewClient(endpoint string) (*gophercloud.ProviderClient, error) {
u, err := url.Parse(endpoint)
if err != nil {
return nil, err
}
hadPath := u.Path != ""
u.Path, u.RawQuery, u.Fragment = "", "", ""
base := u.String()
endpoint = gophercloud.NormalizeURL(endpoint)
base = gophercloud.NormalizeURL(base)
if hadPath {
return &gophercloud.ProviderClient{
IdentityBase: base,
IdentityEndpoint: endpoint,
}, nil
}
return &gophercloud.ProviderClient{
IdentityBase: base,
IdentityEndpoint: "",
}, nil
}
// AuthenticatedClient logs in to an OpenStack cloud found at the identity endpoint specified by options, acquires a token, and
// returns a Client instance that's ready to operate.
// It first queries the root identity endpoint to determine which versions of the identity service are supported, then chooses
// the most recent identity service available to proceed.
func AuthenticatedClient(options gophercloud.AuthOptions) (*gophercloud.ProviderClient, error) {
client, err := NewClient(options.IdentityEndpoint)
if err != nil {
return nil, err
}
err = Authenticate(client, options)
if err != nil {
return nil, err
}
return client, nil
}
// Authenticate or re-authenticate against the most recent identity service supported at the provided endpoint.
func Authenticate(client *gophercloud.ProviderClient, options gophercloud.AuthOptions) error {
versions := []*utils.Version{
{ID: v20, Priority: 20, Suffix: "/v2.0/"},
{ID: v30, Priority: 30, Suffix: "/v3/"},
}
chosen, endpoint, err := utils.ChooseVersion(client, versions)
if err != nil {
return err
}
switch chosen.ID {
case v20:
return v2auth(client, endpoint, options)
case v30:
return v3auth(client, endpoint, options)
default:
// The switch statement must be out of date from the versions list.
return fmt.Errorf("Unrecognized identity version: %s", chosen.ID)
}
}
// AuthenticateV2 explicitly authenticates against the identity v2 endpoint.
func AuthenticateV2(client *gophercloud.ProviderClient, options gophercloud.AuthOptions) error {
return v2auth(client, "", options)
}
func v2auth(client *gophercloud.ProviderClient, endpoint string, options gophercloud.AuthOptions) error {
v2Client := NewIdentityV2(client)
if endpoint != "" {
v2Client.Endpoint = endpoint
}
result := tokens2.Create(v2Client, tokens2.AuthOptions{AuthOptions: options})
token, err := result.ExtractToken()
if err != nil {
return err
}
catalog, err := result.ExtractServiceCatalog()
if err != nil {
return err
}
if options.AllowReauth {
client.ReauthFunc = func() error {
client.TokenID = ""
return v2auth(client, endpoint, options)
}
}
client.TokenID = token.ID
client.EndpointLocator = func(opts gophercloud.EndpointOpts) (string, error) {
return V2EndpointURL(catalog, opts)
}
return nil
}
// AuthenticateV3 explicitly authenticates against the identity v3 service.
func AuthenticateV3(client *gophercloud.ProviderClient, options gophercloud.AuthOptions) error {
return v3auth(client, "", options)
}
func v3auth(client *gophercloud.ProviderClient, endpoint string, options gophercloud.AuthOptions) error {
// Override the generated service endpoint with the one returned by the version endpoint.
v3Client := NewIdentityV3(client)
if endpoint != "" {
v3Client.Endpoint = endpoint
}
var scope *tokens3.Scope
if options.TenantID != "" {
scope = &tokens3.Scope{
ProjectID: options.TenantID,
}
options.TenantID = ""
options.TenantName = ""
} else {
if options.TenantName != "" {
scope = &tokens3.Scope{
ProjectName: options.TenantName,
DomainID: options.DomainID,
DomainName: options.DomainName,
}
options.TenantName = ""
}
}
result := tokens3.Create(v3Client, options, scope)
token, err := result.ExtractToken()
if err != nil {
return err
}
catalog, err := result.ExtractServiceCatalog()
if err != nil {
return err
}
client.TokenID = token.ID
if options.AllowReauth {
client.ReauthFunc = func() error {
client.TokenID = ""
return v3auth(client, endpoint, options)
}
}
client.EndpointLocator = func(opts gophercloud.EndpointOpts) (string, error) {
return V3EndpointURL(catalog, opts)
}
return nil
}
// NewIdentityV2 creates a ServiceClient that may be used to interact with the v2 identity service.
func NewIdentityV2(client *gophercloud.ProviderClient) *gophercloud.ServiceClient {
v2Endpoint := client.IdentityBase + "v2.0/"
return &gophercloud.ServiceClient{
ProviderClient: client,
Endpoint: v2Endpoint,
}
}
// NewIdentityV3 creates a ServiceClient that may be used to access the v3 identity service.
func NewIdentityV3(client *gophercloud.ProviderClient) *gophercloud.ServiceClient {
v3Endpoint := client.IdentityBase + "v3/"
return &gophercloud.ServiceClient{
ProviderClient: client,
Endpoint: v3Endpoint,
}
}
func NewIdentityAdminV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
eo.ApplyDefaults("identity")
eo.Availability = gophercloud.AvailabilityAdmin
url, err := client.EndpointLocator(eo)
if err != nil {
return nil, err
}
// Force using v2 API
if strings.Contains(url, "/v3") {
url = strings.Replace(url, "/v3", "/v2.0", -1)
}
return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil
}
func NewIdentityAdminV3(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
eo.ApplyDefaults("identity")
eo.Availability = gophercloud.AvailabilityAdmin
url, err := client.EndpointLocator(eo)
if err != nil {
return nil, err
}
// Force using v3 API
if strings.Contains(url, "/v2.0") {
url = strings.Replace(url, "/v2.0", "/v3", -1)
}
return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil
}
// NewObjectStorageV1 creates a ServiceClient that may be used with the v1 object storage package.
func NewObjectStorageV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
eo.ApplyDefaults("object-store")
url, err := client.EndpointLocator(eo)
if err != nil {
return nil, err
}
return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil
}
// NewComputeV2 creates a ServiceClient that may be used with the v2 compute package.
func NewComputeV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
eo.ApplyDefaults("compute")
url, err := client.EndpointLocator(eo)
if err != nil {
return nil, err
}
return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil
}
// NewNetworkV2 creates a ServiceClient that may be used with the v2 network package.
func NewNetworkV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
eo.ApplyDefaults("network")
url, err := client.EndpointLocator(eo)
if err != nil {
return nil, err
}
return &gophercloud.ServiceClient{
ProviderClient: client,
Endpoint: url,
ResourceBase: url + "v2.0/",
}, nil
}
// NewBlockStorageV1 creates a ServiceClient that may be used to access the v1 block storage service.
func NewBlockStorageV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
eo.ApplyDefaults("volume")
url, err := client.EndpointLocator(eo)
if err != nil {
return nil, err
}
return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil
}
// NewCDNV1 creates a ServiceClient that may be used to access the OpenStack v1
// CDN service.
func NewCDNV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
eo.ApplyDefaults("cdn")
url, err := client.EndpointLocator(eo)
if err != nil {
return nil, err
}
return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil
}
// NewOrchestrationV1 creates a ServiceClient that may be used to access the v1 orchestration service.
func NewOrchestrationV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
eo.ApplyDefaults("orchestration")
url, err := client.EndpointLocator(eo)
if err != nil {
return nil, err
}
return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil
}
// NewDBV1 creates a ServiceClient that may be used to access the v1 DB service.
func NewDBV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
eo.ApplyDefaults("database")
url, err := client.EndpointLocator(eo)
if err != nil {
return nil, err
}
return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil
}

View File

@@ -0,0 +1,3 @@
# Common Resources
This directory is for resources that are shared by multiple services.

View File

@@ -0,0 +1,15 @@
// Package extensions provides information and interaction with the different extensions available
// for an OpenStack service.
//
// The purpose of OpenStack API extensions is to:
//
// - Introduce new features in the API without requiring a version change.
// - Introduce vendor-specific niche functionality.
// - Act as a proving ground for experimental functionalities that might be included in a future
// version of the API.
//
// Extensions usually have tags that prevent conflicts with other extensions that define attributes
// or resources with the same names, and with core resources and attributes.
// Because an extension might not be supported by all plug-ins, its availability varies with deployments
// and the specific plug-in.
package extensions

View File

@@ -0,0 +1 @@
package extensions

View File

@@ -0,0 +1,91 @@
// +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

@@ -0,0 +1,21 @@
package extensions
import (
"github.com/rackspace/gophercloud"
"github.com/rackspace/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
}
// List returns a Pager which allows you to iterate over the full collection of extensions.
// It does not accept query parameters.
func List(c *gophercloud.ServiceClient) pagination.Pager {
return pagination.NewPager(c, ListExtensionURL(c), func(r pagination.PageResult) pagination.Page {
return ExtensionPage{pagination.SinglePageBase(r)}
})
}

View File

@@ -0,0 +1,65 @@
package extensions
import (
"github.com/mitchellh/mapstructure"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
)
// GetResult temporarily stores the result of a Get call.
// Use its Extract() method to interpret it as an Extension.
type GetResult struct {
gophercloud.Result
}
// Extract interprets a GetResult as an Extension.
func (r GetResult) Extract() (*Extension, error) {
if r.Err != nil {
return nil, r.Err
}
var res struct {
Extension *Extension `json:"extension"`
}
err := mapstructure.Decode(r.Body, &res)
return res.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"`
}
// ExtensionPage is the page returned by a pager when traversing over a collection of extensions.
type ExtensionPage struct {
pagination.SinglePageBase
}
// 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
}
// 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"`
}
err := mapstructure.Decode(page.(ExtensionPage).Body, &resp)
return resp.Extensions, err
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1 @@
package defsecrules

View File

@@ -0,0 +1,143 @@
package defsecrules
import (
"fmt"
"net/http"
"testing"
th "github.com/rackspace/gophercloud/testhelper"
fake "github.com/rackspace/gophercloud/testhelper/client"
)
const rootPath = "/os-security-group-default-rules"
func mockListRulesResponse(t *testing.T) {
th.Mux.HandleFunc(rootPath, func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "GET")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, `
{
"security_group_default_rules": [
{
"from_port": 80,
"id": "{ruleID}",
"ip_protocol": "TCP",
"ip_range": {
"cidr": "10.10.10.0/24"
},
"to_port": 80
}
]
}
`)
})
}
func mockCreateRuleResponse(t *testing.T) {
th.Mux.HandleFunc(rootPath, func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "POST")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
th.TestJSONRequest(t, r, `
{
"security_group_default_rule": {
"ip_protocol": "TCP",
"from_port": 80,
"to_port": 80,
"cidr": "10.10.12.0/24"
}
}
`)
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, `
{
"security_group_default_rule": {
"from_port": 80,
"id": "{ruleID}",
"ip_protocol": "TCP",
"ip_range": {
"cidr": "10.10.12.0/24"
},
"to_port": 80
}
}
`)
})
}
func mockCreateRuleResponseICMPZero(t *testing.T) {
th.Mux.HandleFunc(rootPath, func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "POST")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
th.TestJSONRequest(t, r, `
{
"security_group_default_rule": {
"ip_protocol": "ICMP",
"from_port": 0,
"to_port": 0,
"cidr": "10.10.12.0/24"
}
}
`)
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, `
{
"security_group_default_rule": {
"from_port": 0,
"id": "{ruleID}",
"ip_protocol": "ICMP",
"ip_range": {
"cidr": "10.10.12.0/24"
},
"to_port": 0
}
}
`)
})
}
func mockGetRuleResponse(t *testing.T, ruleID string) {
url := rootPath + "/" + ruleID
th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "GET")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, `
{
"security_group_default_rule": {
"id": "{ruleID}",
"from_port": 80,
"to_port": 80,
"ip_protocol": "TCP",
"ip_range": {
"cidr": "10.10.12.0/24"
}
}
}
`)
})
}
func mockDeleteRuleResponse(t *testing.T, ruleID string) {
url := rootPath + "/" + ruleID
th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "DELETE")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusNoContent)
})
}

View File

@@ -0,0 +1,96 @@
package defsecrules
import (
"errors"
"strings"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
)
// List will return a collection of default rules.
func List(client *gophercloud.ServiceClient) pagination.Pager {
createPage := func(r pagination.PageResult) pagination.Page {
return DefaultRulePage{pagination.SinglePageBase(r)}
}
return pagination.NewPager(client, rootURL(client), createPage)
}
// CreateOpts represents the configuration for adding a new default rule.
type CreateOpts struct {
// Required - the lower bound of the port range that will be opened.
FromPort int `json:"from_port"`
// Required - the upper bound of the port range that will be opened.
ToPort int `json:"to_port"`
// Required - the protocol type that will be allowed, e.g. TCP.
IPProtocol string `json:"ip_protocol"`
// ONLY required if FromGroupID is blank. This represents the IP range that
// will be the source of network traffic to your security group. Use
// 0.0.0.0/0 to allow all IP addresses.
CIDR string `json:"cidr,omitempty"`
}
// CreateOptsBuilder builds the create rule options into a serializable format.
type CreateOptsBuilder interface {
ToRuleCreateMap() (map[string]interface{}, error)
}
// ToRuleCreateMap builds the create rule options into a serializable format.
func (opts CreateOpts) ToRuleCreateMap() (map[string]interface{}, error) {
rule := make(map[string]interface{})
if opts.FromPort == 0 && strings.ToUpper(opts.IPProtocol) != "ICMP" {
return rule, errors.New("A FromPort must be set")
}
if opts.ToPort == 0 && strings.ToUpper(opts.IPProtocol) != "ICMP" {
return rule, errors.New("A ToPort must be set")
}
if opts.IPProtocol == "" {
return rule, errors.New("A IPProtocol must be set")
}
if opts.CIDR == "" {
return rule, errors.New("A CIDR must be set")
}
rule["from_port"] = opts.FromPort
rule["to_port"] = opts.ToPort
rule["ip_protocol"] = opts.IPProtocol
rule["cidr"] = opts.CIDR
return map[string]interface{}{"security_group_default_rule": rule}, nil
}
// Create is the operation responsible for creating a new default rule.
func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult {
var result CreateResult
reqBody, err := opts.ToRuleCreateMap()
if err != nil {
result.Err = err
return result
}
_, result.Err = client.Post(rootURL(client), reqBody, &result.Body, &gophercloud.RequestOpts{
OkCodes: []int{200},
})
return result
}
// Get will return details for a particular default rule.
func Get(client *gophercloud.ServiceClient, id string) GetResult {
var result GetResult
_, result.Err = client.Get(resourceURL(client, id), &result.Body, nil)
return result
}
// Delete will permanently delete a default rule from the project.
func Delete(client *gophercloud.ServiceClient, id string) gophercloud.ErrResult {
var result gophercloud.ErrResult
_, result.Err = client.Delete(resourceURL(client, id), nil)
return result
}

View File

@@ -0,0 +1,69 @@
package defsecrules
import (
"github.com/mitchellh/mapstructure"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/openstack/compute/v2/extensions/secgroups"
"github.com/rackspace/gophercloud/pagination"
)
// DefaultRule represents a default rule - which is identical to a
// normal security rule.
type DefaultRule secgroups.Rule
// DefaultRulePage is a single page of a DefaultRule collection.
type DefaultRulePage struct {
pagination.SinglePageBase
}
// IsEmpty determines whether or not a page of default rules contains any results.
func (page DefaultRulePage) IsEmpty() (bool, error) {
users, err := ExtractDefaultRules(page)
if err != nil {
return false, err
}
return len(users) == 0, nil
}
// ExtractDefaultRules returns a slice of DefaultRules contained in a single
// page of results.
func ExtractDefaultRules(page pagination.Page) ([]DefaultRule, error) {
casted := page.(DefaultRulePage).Body
var response struct {
Rules []DefaultRule `mapstructure:"security_group_default_rules"`
}
err := mapstructure.WeakDecode(casted, &response)
return response.Rules, err
}
type commonResult struct {
gophercloud.Result
}
// CreateResult represents the result of a create operation.
type CreateResult struct {
commonResult
}
// GetResult represents the result of a get operation.
type GetResult struct {
commonResult
}
// Extract will extract a DefaultRule struct from most responses.
func (r commonResult) Extract() (*DefaultRule, error) {
if r.Err != nil {
return nil, r.Err
}
var response struct {
Rule DefaultRule `mapstructure:"security_group_default_rule"`
}
err := mapstructure.WeakDecode(r.Body, &response)
return &response.Rule, err
}

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