Moving calls to client helper while I'm at it
diff --git a/_site/CONTRIBUTING.md b/_site/CONTRIBUTING.md
new file mode 100644
index 0000000..a293b50
--- /dev/null
+++ b/_site/CONTRIBUTING.md
@@ -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](/#how-to-install).
+
+2. Move into the directory that houses your local repository:
+
+ ```bash
+ cd ${GOPATH}/src/github.com/rackspace/gophercloud
+ ```
+
+3. Fork the `rackspace/gophercloud` repository and update your remote refs. You
+will need to rename the `origin` remote branch to `upstream`, and add your
+fork as `origin` instead:
+
+ ```bash
+ git remote rename origin upstream
+ git remote add origin git@github.com/<my_username>/gophercloud
+ ```
+
+4. Checkout the latest development branch ([click here](/branches) to see all
+the branches):
+
+ ```bash
+ git checkout v0.2.0
+ ```
+
+5. If you're working on something (discussed more in detail below), you will
+need to checkout a new feature branch:
+
+ ```bash
+ git checkout -b my-new-feature
+ ```
+
+Another thing to bear in mind is that you will need to add a few extra
+environment variables for acceptance tests - this is documented in our
+[acceptance tests readme](/acceptance).
+
+## Tests
+
+When working on a new or existing feature, testing will be the backbone of your
+work since it helps uncover and prevent regressions in the codebase. There are
+two types of test we use in gophercloud: unit tests and acceptance tests, which
+are both described below.
+
+### Unit tests
+
+Unit tests are the fine-grained tests that establish and ensure the behaviour
+of individual units of functionality. We usually test on an
+operation-by-operation basis (an operation typically being an API action) with
+the use of mocking to set up explicit expectations. Each operation will set up
+its HTTP response expectation, and then test how the system responds when fed
+this controlled, pre-determined input.
+
+To make life easier, we've introduced a bunch of test helpers to simplify the
+process of testing expectations with assertions:
+
+```go
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestSomething(t *testing.T) {
+ result, err := Operation()
+
+ testhelper.AssertEquals(t, "foo", result.Bar)
+ testhelper.AssertNoErr(t, err)
+}
+
+func TestSomethingElse(t *testing.T) {
+ testhelper.CheckEquals(t, "expected", "actual")
+}
+```
+
+`AssertEquals` and `AssertNoErr` will throw a fatal error if a value does not
+match an expected value or if an error has been declared, respectively. You can
+also use `CheckEquals` and `CheckNoErr` for the same purpose; the only difference
+being that `t.Errorf` is raised rather than `t.Fatalf`.
+
+Here is a truncated example of mocked HTTP responses:
+
+```go
+import (
+ "testing"
+
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestGet(t *testing.T) {
+ // Setup the HTTP request multiplexer and server
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/networks/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) {
+ // Test we're using the correct HTTP method
+ th.TestMethod(t, r, "GET")
+
+ // Test we're setting the auth token
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+ // Set the appropriate headers for our mocked response
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ // Set the HTTP body
+ fmt.Fprintf(w, `
+{
+ "network": {
+ "status": "ACTIVE",
+ "name": "private-network",
+ "admin_state_up": true,
+ "tenant_id": "4fd44f30292945e481c7b8a0c8908869",
+ "shared": true,
+ "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22"
+ }
+}
+ `)
+ })
+
+ // Call our API operation
+ network, err := Get(fake.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22").Extract()
+
+ // Assert no errors and equality
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, n.Status, "ACTIVE")
+}
+```
+
+### Acceptance tests
+
+As we've already mentioned, unit tests have a very narrow and confined focus -
+they test small units of behaviour. Acceptance tests on the other hand have a
+far larger scope: they are fully functional tests that test the entire API of a
+service in one fell swoop. They don't care about unit isolation or mocking
+expectations, they instead do a full run-through and consequently test how the
+entire system _integrates_ together. When an API satisfies expectations, it
+proves by default that the requirements for a contract have been met.
+
+Please be aware that acceptance tests will hit a live API - and may incur
+service charges from your provider. Although most tests handle their own
+teardown procedures, it is always worth manually checking that resources are
+deleted after the test suite finishes.
+
+### Running tests
+
+To run all tests:
+
+```bash
+go test ./...
+```
+
+To run all tests with verbose output:
+
+```bash
+go test -v ./...
+```
+
+To run tests that match certain [build tags]():
+
+```bash
+go test -tags "foo bar" ./...
+```
+
+To run tests for a particular sub-package:
+
+```bash
+cd ./path/to/package && go test .
+```
+
+## Basic style guide
+
+We follow the standard formatting recommendations and language idioms set out
+in the [Effective Go](https://golang.org/doc/effective_go.html) guide. It's
+definitely worth reading - but the relevant sections are
+[formatting](https://golang.org/doc/effective_go.html#formatting)
+and [names](https://golang.org/doc/effective_go.html#names).
+
+## 5 ways to get involved
+
+There are five main ways you can get involved in our open-source project, and
+each is described briefly below. Once you've made up your mind and decided on
+your fix, you will need to follow the same basic steps that all submissions are
+required to adhere to:
+
+1. [fork](https://help.github.com/articles/fork-a-repo/) the `rackspace/gophercloud` repository
+2. checkout a [new branch](https://github.com/Kunena/Kunena-Forum/wiki/Create-a-new-branch-with-git-and-manage-branches)
+3. submit your branch as a [pull request](https://help.github.com/articles/creating-a-pull-request/)
+
+### 1. Providing feedback
+
+On of the easiest ways to get readily involved in our project is to let us know
+about your experiences using our SDK. Feedback like this is incredibly useful
+to us, because it allows us to refine and change features based on what our
+users want and expect of us. There are a bunch of ways to get in contact! You
+can [ping us](mailto:sdk-support@rackspace.com) 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!
diff --git a/_site/CONTRIBUTORS.md b/_site/CONTRIBUTORS.md
new file mode 100644
index 0000000..eb97094
--- /dev/null
+++ b/_site/CONTRIBUTORS.md
@@ -0,0 +1,12 @@
+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>
diff --git a/_site/LICENSE b/_site/LICENSE
new file mode 100644
index 0000000..fbbbc9e
--- /dev/null
+++ b/_site/LICENSE
@@ -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
diff --git a/_site/README.md b/_site/README.md
new file mode 100644
index 0000000..74a4cd4
--- /dev/null
+++ b/_site/README.md
@@ -0,0 +1,154 @@
+# gophercloud: the OpenStack SDK for Go
+[![Build Status](https://travis-ci.org/rackspace/gophercloud.svg?branch=v0.2.0)](https://travis-ci.org/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
+```
+
+Once this is set up, you can install the gophercloud package like so:
+
+```bash
+go get github.com/rackspace/gophercloud
+```
+
+This will install all the source files you need into a `pkg` directory, which is
+referenceable from your own source files.
+
+## 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 := utils.AuthOptions()
+```
+
+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
+[e-mail us](mailto:sdk-support@rackspace.com) privately. 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 e-mail us directly at
+[sdk-support@rackspace.com](mailto:sdk-support@rackspace.com).
diff --git a/_site/acceptance/README.md b/_site/acceptance/README.md
new file mode 100644
index 0000000..95c174d
--- /dev/null
+++ b/_site/acceptance/README.md
@@ -0,0 +1,56 @@
+# 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|
+
+#### General
+
+|Name|Description|
+|---|---|
+|`OS_REGION_NAME`|The region you want your resources 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|
+
+### 2. Run the test suite
+
+From any directory, run:
+
+```
+go test -v -tags acceptance github.com/rackspace/gophercloud/...
+```
+
+Alternatively, you can execute the above from your nested git folder (i.e. the
+ workspace visible when browsing the Github repository) by replacing
+ `github.com/rackspace/gophercloud/...` with `./...`
diff --git a/_site/acceptance/openstack/blockstorage/v1/snapshots_test.go b/_site/acceptance/openstack/blockstorage/v1/snapshots_test.go
new file mode 100644
index 0000000..5835048
--- /dev/null
+++ b/_site/acceptance/openstack/blockstorage/v1/snapshots_test.go
@@ -0,0 +1,87 @@
+// +build acceptance
+
+package v1
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots"
+ "github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes"
+)
+
+func TestSnapshots(t *testing.T) {
+
+ client, err := newClient()
+ if err != nil {
+ t.Fatalf("Failed to create Block Storage v1 client: %v", err)
+ }
+
+ v, err := volumes.Create(client, &volumes.CreateOpts{
+ Name: "gophercloud-test-volume",
+ Size: 1,
+ }).Extract()
+ if err != nil {
+ t.Fatalf("Failed to create volume: %v\n", err)
+ }
+
+ err = volumes.WaitForStatus(client, v.ID, "available", 120)
+ if err != nil {
+ t.Fatalf("Failed to create volume: %v\n", err)
+ }
+
+ t.Logf("Created volume: %v\n", v)
+
+ ss, err := snapshots.Create(client, &snapshots.CreateOpts{
+ Name: "gophercloud-test-snapshot",
+ VolumeID: v.ID,
+ }).Extract()
+ if err != nil {
+ t.Fatalf("Failed to create snapshot: %v\n", err)
+ }
+
+ err = snapshots.WaitForStatus(client, ss.ID, "available", 120)
+ if err != nil {
+ t.Fatalf("Failed to create snapshot: %v\n", err)
+ }
+
+ t.Logf("Created snapshot: %+v\n", ss)
+
+ err = snapshots.Delete(client, ss.ID)
+ if err != nil {
+ t.Fatalf("Failed to delete snapshot: %v", err)
+ }
+
+ err = gophercloud.WaitFor(120, func() (bool, error) {
+ _, err := snapshots.Get(client, ss.ID).Extract()
+ if err != nil {
+ return true, nil
+ }
+
+ return false, nil
+ })
+ if err != nil {
+ t.Fatalf("Failed to delete snapshot: %v", err)
+ }
+
+ t.Log("Deleted snapshot\n")
+
+ err = volumes.Delete(client, v.ID)
+ if err != nil {
+ t.Errorf("Failed to delete volume: %v", err)
+ }
+
+ err = gophercloud.WaitFor(120, func() (bool, error) {
+ _, err := volumes.Get(client, v.ID).Extract()
+ if err != nil {
+ return true, nil
+ }
+
+ return false, nil
+ })
+ if err != nil {
+ t.Errorf("Failed to delete volume: %v", err)
+ }
+
+ t.Log("Deleted volume\n")
+}
diff --git a/_site/acceptance/openstack/blockstorage/v1/volumes_test.go b/_site/acceptance/openstack/blockstorage/v1/volumes_test.go
new file mode 100644
index 0000000..21a47ac
--- /dev/null
+++ b/_site/acceptance/openstack/blockstorage/v1/volumes_test.go
@@ -0,0 +1,88 @@
+// +build acceptance blockstorage
+
+package v1
+
+import (
+ "fmt"
+ "os"
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/openstack"
+ "github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes"
+ "github.com/rackspace/gophercloud/openstack/utils"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+func newClient() (*gophercloud.ServiceClient, error) {
+ ao, err := utils.AuthOptions()
+ if err != nil {
+ return nil, err
+ }
+
+ client, err := openstack.AuthenticatedClient(ao)
+ if err != nil {
+ return nil, err
+ }
+
+ return openstack.NewBlockStorageV1(client, gophercloud.EndpointOpts{
+ Region: os.Getenv("OS_REGION_NAME"),
+ })
+}
+
+func TestVolumes(t *testing.T) {
+ client, err := newClient()
+ if err != nil {
+ t.Fatalf("Failed to create Block Storage v1 client: %v", err)
+ }
+
+ cv, err := volumes.Create(client, &volumes.CreateOpts{
+ Size: 1,
+ Name: "gophercloud-test-volume",
+ }).Extract()
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ defer func() {
+ err = volumes.WaitForStatus(client, cv.ID, "available", 60)
+ if err != nil {
+ t.Error(err)
+ }
+ err = volumes.Delete(client, cv.ID)
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ }()
+
+ _, err = volumes.Update(client, cv.ID, &volumes.UpdateOpts{
+ Name: "gophercloud-updated-volume",
+ }).Extract()
+ if err != nil {
+ t.Error(err)
+ return
+ }
+
+ v, err := volumes.Get(client, cv.ID).Extract()
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ fmt.Printf("Got volume: %+v\n", v)
+
+ if v.Name != "gophercloud-updated-volume" {
+ t.Errorf("Unable to update volume: Expected name: gophercloud-updated-volume\nActual name: %s", v.Name)
+ }
+
+ err = volumes.List(client, &volumes.ListOpts{Name: "gophercloud-updated-volume"}).EachPage(func(page pagination.Page) (bool, error) {
+ vols, err := volumes.ExtractVolumes(page)
+ if len(vols) != 1 {
+ t.Errorf("Expected 1 volume, got %d", len(vols))
+ }
+ return true, err
+ })
+ if err != nil {
+ t.Errorf("Error listing volumes: %v", err)
+ }
+}
diff --git a/_site/acceptance/openstack/blockstorage/v1/volumetypes_test.go b/_site/acceptance/openstack/blockstorage/v1/volumetypes_test.go
new file mode 100644
index 0000000..416e341
--- /dev/null
+++ b/_site/acceptance/openstack/blockstorage/v1/volumetypes_test.go
@@ -0,0 +1,58 @@
+// +build acceptance
+
+package v1
+
+import (
+ "testing"
+ "time"
+
+ "github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumetypes"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+func TestVolumeTypes(t *testing.T) {
+ client, err := newClient()
+ if err != nil {
+ t.Fatalf("Failed to create Block Storage v1 client: %v", err)
+ }
+
+ vt, err := volumetypes.Create(client, &volumetypes.CreateOpts{
+ ExtraSpecs: map[string]interface{}{
+ "capabilities": "gpu",
+ "priority": 3,
+ },
+ Name: "gophercloud-test-volumeType",
+ }).Extract()
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ defer func() {
+ time.Sleep(10000 * time.Millisecond)
+ err = volumetypes.Delete(client, vt.ID)
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ }()
+ t.Logf("Created volume type: %+v\n", vt)
+
+ vt, err = volumetypes.Get(client, vt.ID).Extract()
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ t.Logf("Got volume type: %+v\n", vt)
+
+ err = volumetypes.List(client).EachPage(func(page pagination.Page) (bool, error) {
+ volTypes, err := volumetypes.ExtractVolumeTypes(page)
+ if len(volTypes) != 1 {
+ t.Errorf("Expected 1 volume type, got %d", len(volTypes))
+ }
+ t.Logf("Listing volume types: %+v\n", volTypes)
+ return true, err
+ })
+ if err != nil {
+ t.Errorf("Error trying to list volume types: %v", err)
+ }
+}
diff --git a/_site/acceptance/openstack/client_test.go b/_site/acceptance/openstack/client_test.go
new file mode 100644
index 0000000..52c0cf5
--- /dev/null
+++ b/_site/acceptance/openstack/client_test.go
@@ -0,0 +1,41 @@
+// +build acceptance
+
+package openstack
+
+import (
+ "os"
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/openstack"
+ "github.com/rackspace/gophercloud/openstack/utils"
+)
+
+func TestAuthenticatedClient(t *testing.T) {
+ // Obtain credentials from the environment.
+ ao, err := utils.AuthOptions()
+ if err != nil {
+ t.Fatalf("Unable to acquire credentials: %v", err)
+ }
+
+ client, err := openstack.AuthenticatedClient(ao)
+ if err != nil {
+ t.Fatalf("Unable to authenticate: %v", err)
+ }
+
+ if client.TokenID == "" {
+ t.Errorf("No token ID assigned to the client")
+ }
+
+ t.Logf("Client successfully acquired a token: %v", client.TokenID)
+
+ // Find the storage service in the service catalog.
+ storage, err := openstack.NewStorageV1(client, gophercloud.EndpointOpts{
+ Region: os.Getenv("OS_REGION_NAME"),
+ })
+ if err != nil {
+ t.Errorf("Unable to locate a storage service: %v", err)
+ } else {
+ t.Logf("Located a storage service at endpoint: [%s]", storage.Endpoint)
+ }
+}
diff --git a/_site/acceptance/openstack/compute/v2/compute_test.go b/_site/acceptance/openstack/compute/v2/compute_test.go
new file mode 100644
index 0000000..15b5163
--- /dev/null
+++ b/_site/acceptance/openstack/compute/v2/compute_test.go
@@ -0,0 +1,98 @@
+// +build acceptance
+
+package v2
+
+import (
+ "fmt"
+ "os"
+ "strings"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/acceptance/tools"
+ "github.com/rackspace/gophercloud/openstack"
+ "github.com/rackspace/gophercloud/openstack/compute/v2/servers"
+ "github.com/rackspace/gophercloud/openstack/utils"
+)
+
+func newClient() (*gophercloud.ServiceClient, error) {
+ ao, err := utils.AuthOptions()
+ if err != nil {
+ return nil, err
+ }
+
+ client, err := openstack.AuthenticatedClient(ao)
+ if err != nil {
+ return nil, err
+ }
+
+ return openstack.NewComputeV2(client, gophercloud.EndpointOpts{
+ Region: os.Getenv("OS_REGION_NAME"),
+ })
+}
+
+func waitForStatus(client *gophercloud.ServiceClient, server *servers.Server, status string) error {
+ return tools.WaitFor(func() (bool, error) {
+ latest, err := servers.Get(client, server.ID).Extract()
+ if err != nil {
+ return false, err
+ }
+
+ if latest.Status == status {
+ // Success!
+ return true, nil
+ }
+
+ return false, nil
+ })
+}
+
+// ComputeChoices contains image and flavor selections for use by the acceptance tests.
+type ComputeChoices struct {
+ // ImageID contains the ID of a valid image.
+ ImageID string
+
+ // FlavorID contains the ID of a valid flavor.
+ FlavorID string
+
+ // FlavorIDResize contains the ID of a different flavor available on the same OpenStack installation, that is distinct
+ // from FlavorID.
+ FlavorIDResize string
+}
+
+// ComputeChoicesFromEnv populates a ComputeChoices struct from environment variables.
+// If any required state is missing, an `error` will be returned that enumerates the missing properties.
+func ComputeChoicesFromEnv() (*ComputeChoices, error) {
+ imageID := os.Getenv("OS_IMAGE_ID")
+ flavorID := os.Getenv("OS_FLAVOR_ID")
+ flavorIDResize := os.Getenv("OS_FLAVOR_ID_RESIZE")
+
+ missing := make([]string, 0, 3)
+ if imageID == "" {
+ missing = append(missing, "OS_IMAGE_ID")
+ }
+ if flavorID == "" {
+ missing = append(missing, "OS_FLAVOR_ID")
+ }
+ if flavorIDResize == "" {
+ missing = append(missing, "OS_FLAVOR_ID_RESIZE")
+ }
+
+ notDistinct := ""
+ if flavorID == flavorIDResize {
+ notDistinct = "OS_FLAVOR_ID and OS_FLAVOR_ID_RESIZE must be distinct."
+ }
+
+ if len(missing) > 0 || notDistinct != "" {
+ text := "You're missing some important setup:\n"
+ if len(missing) > 0 {
+ text += " * These environment variables must be provided: " + strings.Join(missing, ", ") + "\n"
+ }
+ if notDistinct != "" {
+ text += " * " + notDistinct + "\n"
+ }
+
+ return nil, fmt.Errorf(text)
+ }
+
+ return &ComputeChoices{ImageID: imageID, FlavorID: flavorID, FlavorIDResize: flavorIDResize}, nil
+}
diff --git a/_site/acceptance/openstack/compute/v2/flavors_test.go b/_site/acceptance/openstack/compute/v2/flavors_test.go
new file mode 100644
index 0000000..9c8e322
--- /dev/null
+++ b/_site/acceptance/openstack/compute/v2/flavors_test.go
@@ -0,0 +1,57 @@
+// +build acceptance
+
+package v2
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud/openstack/compute/v2/flavors"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+func TestListFlavors(t *testing.T) {
+ client, err := newClient()
+ if err != nil {
+ t.Fatalf("Unable to create a compute client: %v", err)
+ }
+
+ t.Logf("ID\tRegion\tName\tStatus\tCreated")
+
+ pager := flavors.List(client, nil)
+ count, pages := 0, 0
+ pager.EachPage(func(page pagination.Page) (bool, error) {
+ t.Logf("---")
+ pages++
+ flavors, err := flavors.ExtractFlavors(page)
+ if err != nil {
+ return false, err
+ }
+
+ for _, f := range flavors {
+ t.Logf("%s\t%s\t%d\t%d\t%d", f.ID, f.Name, f.RAM, f.Disk, f.VCPUs)
+ }
+
+ return true, nil
+ })
+
+ t.Logf("--------\n%d flavors listed on %d pages.", count, pages)
+}
+
+func TestGetFlavor(t *testing.T) {
+ client, err := newClient()
+ if err != nil {
+ t.Fatalf("Unable to create a compute client: %v", err)
+ }
+
+ choices, err := ComputeChoicesFromEnv()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ flavor, err := flavors.Get(client, choices.FlavorID).Extract()
+ if err != nil {
+ t.Fatalf("Unable to get flavor information: %v", err)
+ }
+
+ t.Logf("Flavor: %#v", flavor)
+}
diff --git a/_site/acceptance/openstack/compute/v2/images_test.go b/_site/acceptance/openstack/compute/v2/images_test.go
new file mode 100644
index 0000000..6166fc8
--- /dev/null
+++ b/_site/acceptance/openstack/compute/v2/images_test.go
@@ -0,0 +1,37 @@
+// +build acceptance
+
+package v2
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud/openstack/compute/v2/images"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+func TestListImages(t *testing.T) {
+ client, err := newClient()
+ if err != nil {
+ t.Fatalf("Unable to create a compute: client: %v", err)
+ }
+
+ t.Logf("ID\tRegion\tName\tStatus\tCreated")
+
+ pager := images.ListDetail(client, nil)
+ count, pages := 0, 0
+ pager.EachPage(func(page pagination.Page) (bool, error) {
+ pages++
+ images, err := images.ExtractImages(page)
+ if err != nil {
+ return false, err
+ }
+
+ for _, i := range images {
+ t.Logf("%s\t%s\t%s\t%s", i.ID, i.Name, i.Status, i.Created)
+ }
+
+ return true, nil
+ })
+
+ t.Logf("--------\n%d images listed on %d pages.", count, pages)
+}
diff --git a/_site/acceptance/openstack/compute/v2/pkg.go b/_site/acceptance/openstack/compute/v2/pkg.go
new file mode 100644
index 0000000..bb158c3
--- /dev/null
+++ b/_site/acceptance/openstack/compute/v2/pkg.go
@@ -0,0 +1,3 @@
+// The v2 package contains acceptance tests for the Openstack Compute V2 service.
+
+package v2
diff --git a/_site/acceptance/openstack/compute/v2/servers_test.go b/_site/acceptance/openstack/compute/v2/servers_test.go
new file mode 100644
index 0000000..5193e4e
--- /dev/null
+++ b/_site/acceptance/openstack/compute/v2/servers_test.go
@@ -0,0 +1,342 @@
+// +build acceptance
+
+package v2
+
+import (
+ "fmt"
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/acceptance/tools"
+ "github.com/rackspace/gophercloud/openstack/compute/v2/servers"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+func TestListServers(t *testing.T) {
+ client, err := newClient()
+ if err != nil {
+ t.Fatalf("Unable to create a compute client: %v", err)
+ }
+
+ t.Logf("ID\tRegion\tName\tStatus\tIPv4\tIPv6")
+
+ pager := servers.List(client, servers.ListOpts{})
+ count, pages := 0, 0
+ pager.EachPage(func(page pagination.Page) (bool, error) {
+ pages++
+ t.Logf("---")
+
+ servers, err := servers.ExtractServers(page)
+ if err != nil {
+ return false, err
+ }
+
+ for _, s := range servers {
+ t.Logf("%s\t%s\t%s\t%s\t%s\t\n", s.ID, s.Name, s.Status, s.AccessIPv4, s.AccessIPv6)
+ count++
+ }
+
+ return true, nil
+ })
+
+ fmt.Printf("--------\n%d servers listed on %d pages.\n", count, pages)
+}
+
+func createServer(t *testing.T, client *gophercloud.ServiceClient, choices *ComputeChoices) (*servers.Server, error) {
+ name := tools.RandomString("ACPTTEST", 16)
+ t.Logf("Attempting to create server: %s\n", name)
+
+ server, err := servers.Create(client, servers.CreateOpts{
+ Name: name,
+ FlavorRef: choices.FlavorID,
+ ImageRef: choices.ImageID,
+ }).Extract()
+ if err != nil {
+ t.Fatalf("Unable to create server: %v", err)
+ }
+
+ return server, err
+}
+
+func TestCreateDestroyServer(t *testing.T) {
+ choices, err := ComputeChoicesFromEnv()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ client, err := newClient()
+ if err != nil {
+ t.Fatalf("Unable to create a compute client: %v", err)
+ }
+
+ name := tools.RandomString("ACPTTEST", 16)
+ t.Logf("Attempting to create server: %s\n", name)
+
+ server, err := createServer(t, client, choices)
+ if err != nil {
+ t.Fatalf("Unable to create server: %v", err)
+ }
+ defer func() {
+ servers.Delete(client, server.ID)
+ t.Logf("Server deleted.")
+ }()
+
+ if err = waitForStatus(client, server, "ACTIVE"); err != nil {
+ t.Fatalf("Unable to wait for server: %v", err)
+ }
+}
+
+func TestUpdateServer(t *testing.T) {
+ client, err := newClient()
+ if err != nil {
+ t.Fatalf("Unable to create a compute client: %v", err)
+ }
+
+ choices, err := ComputeChoicesFromEnv()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ server, err := createServer(t, client, choices)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer servers.Delete(client, server.ID)
+
+ if err = waitForStatus(client, server, "ACTIVE"); err != nil {
+ t.Fatal(err)
+ }
+
+ alternateName := tools.RandomString("ACPTTEST", 16)
+ for alternateName == server.Name {
+ alternateName = tools.RandomString("ACPTTEST", 16)
+ }
+
+ t.Logf("Attempting to rename the server to %s.", alternateName)
+
+ updated, err := servers.Update(client, server.ID, servers.UpdateOpts{Name: alternateName}).Extract()
+ if err != nil {
+ t.Fatalf("Unable to rename server: %v", err)
+ }
+
+ if updated.ID != server.ID {
+ t.Errorf("Updated server ID [%s] didn't match original server ID [%s]!", updated.ID, server.ID)
+ }
+
+ err = tools.WaitFor(func() (bool, error) {
+ latest, err := servers.Get(client, updated.ID).Extract()
+ if err != nil {
+ return false, err
+ }
+
+ return latest.Name == alternateName, nil
+ })
+}
+
+func TestActionChangeAdminPassword(t *testing.T) {
+ t.Parallel()
+
+ client, err := newClient()
+ if err != nil {
+ t.Fatalf("Unable to create a compute client: %v", err)
+ }
+
+ choices, err := ComputeChoicesFromEnv()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ server, err := createServer(t, client, choices)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer servers.Delete(client, server.ID)
+
+ if err = waitForStatus(client, server, "ACTIVE"); err != nil {
+ t.Fatal(err)
+ }
+
+ randomPassword := tools.MakeNewPassword(server.AdminPass)
+ err = servers.ChangeAdminPassword(client, server.ID, randomPassword)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if err = waitForStatus(client, server, "PASSWORD"); err != nil {
+ t.Fatal(err)
+ }
+
+ if err = waitForStatus(client, server, "ACTIVE"); err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestActionReboot(t *testing.T) {
+ t.Parallel()
+
+ client, err := newClient()
+ if err != nil {
+ t.Fatalf("Unable to create a compute client: %v", err)
+ }
+
+ choices, err := ComputeChoicesFromEnv()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ server, err := createServer(t, client, choices)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer servers.Delete(client, server.ID)
+
+ if err = waitForStatus(client, server, "ACTIVE"); err != nil {
+ t.Fatal(err)
+ }
+
+ err = servers.Reboot(client, server.ID, "aldhjflaskhjf")
+ if err == nil {
+ t.Fatal("Expected the SDK to provide an ArgumentError here")
+ }
+
+ t.Logf("Attempting reboot of server %s", server.ID)
+ err = servers.Reboot(client, server.ID, servers.OSReboot)
+ if err != nil {
+ t.Fatalf("Unable to reboot server: %v", err)
+ }
+
+ if err = waitForStatus(client, server, "REBOOT"); err != nil {
+ t.Fatal(err)
+ }
+
+ if err = waitForStatus(client, server, "ACTIVE"); err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestActionRebuild(t *testing.T) {
+ t.Parallel()
+
+ client, err := newClient()
+ if err != nil {
+ t.Fatalf("Unable to create a compute client: %v", err)
+ }
+
+ choices, err := ComputeChoicesFromEnv()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ server, err := createServer(t, client, choices)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer servers.Delete(client, server.ID)
+
+ if err = waitForStatus(client, server, "ACTIVE"); err != nil {
+ t.Fatal(err)
+ }
+
+ t.Logf("Attempting to rebuild server %s", server.ID)
+
+ rebuildOpts := servers.RebuildOpts{
+ Name: tools.RandomString("ACPTTEST", 16),
+ AdminPass: tools.MakeNewPassword(server.AdminPass),
+ ImageID: choices.ImageID,
+ }
+
+ rebuilt, err := servers.Rebuild(client, server.ID, rebuildOpts).Extract()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if rebuilt.ID != server.ID {
+ t.Errorf("Expected rebuilt server ID of [%s]; got [%s]", server.ID, rebuilt.ID)
+ }
+
+ if err = waitForStatus(client, rebuilt, "REBUILD"); err != nil {
+ t.Fatal(err)
+ }
+
+ if err = waitForStatus(client, rebuilt, "ACTIVE"); err != nil {
+ t.Fatal(err)
+ }
+}
+
+func resizeServer(t *testing.T, client *gophercloud.ServiceClient, server *servers.Server, choices *ComputeChoices) {
+ if err := waitForStatus(client, server, "ACTIVE"); err != nil {
+ t.Fatal(err)
+ }
+
+ t.Logf("Attempting to resize server [%s]", server.ID)
+
+ if err := servers.Resize(client, server.ID, choices.FlavorIDResize); err != nil {
+ t.Fatal(err)
+ }
+
+ if err := waitForStatus(client, server, "VERIFY_RESIZE"); err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestActionResizeConfirm(t *testing.T) {
+ t.Parallel()
+
+ choices, err := ComputeChoicesFromEnv()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ client, err := newClient()
+ if err != nil {
+ t.Fatalf("Unable to create a compute client: %v", err)
+ }
+
+ server, err := createServer(t, client, choices)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer servers.Delete(client, server.ID)
+ resizeServer(t, client, server, choices)
+
+ t.Logf("Attempting to confirm resize for server %s", server.ID)
+
+ if err = servers.ConfirmResize(client, server.ID); err != nil {
+ t.Fatal(err)
+ }
+
+ if err = waitForStatus(client, server, "ACTIVE"); err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestActionResizeRevert(t *testing.T) {
+ t.Parallel()
+
+ choices, err := ComputeChoicesFromEnv()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ client, err := newClient()
+ if err != nil {
+ t.Fatalf("Unable to create a compute client: %v", err)
+ }
+
+ server, err := createServer(t, client, choices)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer servers.Delete(client, server.ID)
+ resizeServer(t, client, server, choices)
+
+ t.Logf("Attempting to revert resize for server %s", server.ID)
+
+ if err := servers.RevertResize(client, server.ID); err != nil {
+ t.Fatal(err)
+ }
+
+ if err = waitForStatus(client, server, "ACTIVE"); err != nil {
+ t.Fatal(err)
+ }
+}
diff --git a/_site/acceptance/openstack/identity/v2/extension_test.go b/_site/acceptance/openstack/identity/v2/extension_test.go
new file mode 100644
index 0000000..2b4e062
--- /dev/null
+++ b/_site/acceptance/openstack/identity/v2/extension_test.go
@@ -0,0 +1,46 @@
+// +build acceptance
+
+package v2
+
+import (
+ "testing"
+
+ extensions2 "github.com/rackspace/gophercloud/openstack/identity/v2/extensions"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestEnumerateExtensions(t *testing.T) {
+ service := authenticatedClient(t)
+
+ t.Logf("Extensions available on this identity endpoint:")
+ count := 0
+ err := extensions2.List(service).EachPage(func(page pagination.Page) (bool, error) {
+ t.Logf("--- Page %02d ---", count)
+
+ extensions, err := extensions2.ExtractExtensions(page)
+ th.AssertNoErr(t, err)
+
+ for i, ext := range extensions {
+ t.Logf("[%02d] name=[%s] namespace=[%s]", i, ext.Name, ext.Namespace)
+ t.Logf(" alias=[%s] updated=[%s]", ext.Alias, ext.Updated)
+ t.Logf(" description=[%s]", ext.Description)
+ }
+
+ count++
+ return true, nil
+ })
+ th.AssertNoErr(t, err)
+}
+
+func TestGetExtension(t *testing.T) {
+ service := authenticatedClient(t)
+
+ ext, err := extensions2.Get(service, "OS-KSCRUD").Extract()
+ th.AssertNoErr(t, err)
+
+ th.CheckEquals(t, "OpenStack Keystone User CRUD", ext.Name)
+ th.CheckEquals(t, "http://docs.openstack.org/identity/api/ext/OS-KSCRUD/v1.0", ext.Namespace)
+ th.CheckEquals(t, "OS-KSCRUD", ext.Alias)
+ th.CheckEquals(t, "OpenStack extensions to Keystone v2.0 API enabling User Operations.", ext.Description)
+}
diff --git a/_site/acceptance/openstack/identity/v2/identity_test.go b/_site/acceptance/openstack/identity/v2/identity_test.go
new file mode 100644
index 0000000..2ecd3ca
--- /dev/null
+++ b/_site/acceptance/openstack/identity/v2/identity_test.go
@@ -0,0 +1,48 @@
+// +build acceptance
+
+package v2
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/openstack"
+ "github.com/rackspace/gophercloud/openstack/utils"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func v2AuthOptions(t *testing.T) gophercloud.AuthOptions {
+ // Obtain credentials from the environment.
+ ao, err := utils.AuthOptions()
+ th.AssertNoErr(t, err)
+
+ // Trim out unused fields. Prefer authentication by API key to password.
+ ao.UserID, ao.DomainID, ao.DomainName = "", "", ""
+ if ao.APIKey != "" {
+ ao.Password = ""
+ }
+
+ return ao
+}
+
+func createClient(t *testing.T, auth bool) *gophercloud.ServiceClient {
+ ao := v2AuthOptions(t)
+
+ provider, err := openstack.NewClient(ao.IdentityEndpoint)
+ th.AssertNoErr(t, err)
+
+ if auth {
+ err = openstack.AuthenticateV2(provider, ao)
+ th.AssertNoErr(t, err)
+ }
+
+ return openstack.NewIdentityV2(provider)
+}
+
+func unauthenticatedClient(t *testing.T) *gophercloud.ServiceClient {
+ return createClient(t, false)
+}
+
+func authenticatedClient(t *testing.T) *gophercloud.ServiceClient {
+ return createClient(t, true)
+}
diff --git a/_site/acceptance/openstack/identity/v2/pkg.go b/_site/acceptance/openstack/identity/v2/pkg.go
new file mode 100644
index 0000000..5ec3cc8
--- /dev/null
+++ b/_site/acceptance/openstack/identity/v2/pkg.go
@@ -0,0 +1 @@
+package v2
diff --git a/_site/acceptance/openstack/identity/v2/tenant_test.go b/_site/acceptance/openstack/identity/v2/tenant_test.go
new file mode 100644
index 0000000..2054598
--- /dev/null
+++ b/_site/acceptance/openstack/identity/v2/tenant_test.go
@@ -0,0 +1,32 @@
+// +build acceptance
+
+package v2
+
+import (
+ "testing"
+
+ tenants2 "github.com/rackspace/gophercloud/openstack/identity/v2/tenants"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestEnumerateTenants(t *testing.T) {
+ service := authenticatedClient(t)
+
+ t.Logf("Tenants to which your current token grants access:")
+ count := 0
+ err := tenants2.List(service, nil).EachPage(func(page pagination.Page) (bool, error) {
+ t.Logf("--- Page %02d ---", count)
+
+ tenants, err := tenants2.ExtractTenants(page)
+ th.AssertNoErr(t, err)
+ for i, tenant := range tenants {
+ t.Logf("[%02d] name=[%s] id=[%s] description=[%s] enabled=[%v]",
+ i, tenant.Name, tenant.ID, tenant.Description, tenant.Enabled)
+ }
+
+ count++
+ return true, nil
+ })
+ th.AssertNoErr(t, err)
+}
diff --git a/_site/acceptance/openstack/identity/v2/token_test.go b/_site/acceptance/openstack/identity/v2/token_test.go
new file mode 100644
index 0000000..47381a2
--- /dev/null
+++ b/_site/acceptance/openstack/identity/v2/token_test.go
@@ -0,0 +1,38 @@
+// +build acceptance
+
+package v2
+
+import (
+ "testing"
+
+ tokens2 "github.com/rackspace/gophercloud/openstack/identity/v2/tokens"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestAuthenticate(t *testing.T) {
+ ao := v2AuthOptions(t)
+ service := unauthenticatedClient(t)
+
+ // Authenticated!
+ result := tokens2.Create(service, ao)
+
+ // Extract and print the token.
+ token, err := result.ExtractToken()
+ th.AssertNoErr(t, err)
+
+ t.Logf("Acquired token: [%s]", token.ID)
+ t.Logf("The token will expire at: [%s]", token.ExpiresAt.String())
+ t.Logf("The token is valid for tenant: [%#v]", token.Tenant)
+
+ // Extract and print the service catalog.
+ catalog, err := result.ExtractServiceCatalog()
+ th.AssertNoErr(t, err)
+
+ t.Logf("Acquired service catalog listing [%d] services", len(catalog.Entries))
+ for i, entry := range catalog.Entries {
+ t.Logf("[%02d]: name=[%s], type=[%s]", i, entry.Name, entry.Type)
+ for _, endpoint := range entry.Endpoints {
+ t.Logf(" - region=[%s] publicURL=[%s]", endpoint.Region, endpoint.PublicURL)
+ }
+ }
+}
diff --git a/_site/acceptance/openstack/identity/v3/endpoint_test.go b/_site/acceptance/openstack/identity/v3/endpoint_test.go
new file mode 100644
index 0000000..9032ec3
--- /dev/null
+++ b/_site/acceptance/openstack/identity/v3/endpoint_test.go
@@ -0,0 +1,108 @@
+// +build acceptance
+
+package v3
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ endpoints3 "github.com/rackspace/gophercloud/openstack/identity/v3/endpoints"
+ services3 "github.com/rackspace/gophercloud/openstack/identity/v3/services"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+func TestListEndpoints(t *testing.T) {
+ // Create a service client.
+ serviceClient := createAuthenticatedClient(t)
+ if serviceClient == nil {
+ return
+ }
+
+ // Use the service to list all available endpoints.
+ pager := endpoints3.List(serviceClient, endpoints3.ListOpts{})
+ err := pager.EachPage(func(page pagination.Page) (bool, error) {
+ t.Logf("--- Page ---")
+
+ endpoints, err := endpoints3.ExtractEndpoints(page)
+ if err != nil {
+ t.Fatalf("Error extracting endpoings: %v", err)
+ }
+
+ for _, endpoint := range endpoints {
+ t.Logf("Endpoint: %8s %10s %9s %s",
+ endpoint.ID,
+ endpoint.Availability,
+ endpoint.Name,
+ endpoint.URL)
+ }
+
+ return true, nil
+ })
+ if err != nil {
+ t.Errorf("Unexpected error while iterating endpoint pages: %v", err)
+ }
+}
+
+func TestNavigateCatalog(t *testing.T) {
+ // Create a service client.
+ client := createAuthenticatedClient(t)
+
+ var compute *services3.Service
+ var endpoint *endpoints3.Endpoint
+
+ // Discover the service we're interested in.
+ servicePager := services3.List(client, services3.ListOpts{ServiceType: "compute"})
+ err := servicePager.EachPage(func(page pagination.Page) (bool, error) {
+ part, err := services3.ExtractServices(page)
+ if err != nil {
+ return false, err
+ }
+ if compute != nil {
+ t.Fatalf("Expected one service, got more than one page")
+ return false, nil
+ }
+ if len(part) != 1 {
+ t.Fatalf("Expected one service, got %d", len(part))
+ return false, nil
+ }
+
+ compute = &part[0]
+ return true, nil
+ })
+ if err != nil {
+ t.Fatalf("Unexpected error iterating pages: %v", err)
+ }
+
+ if compute == nil {
+ t.Fatalf("No compute service found.")
+ }
+
+ // Enumerate the endpoints available for this service.
+ computePager := endpoints3.List(client, endpoints3.ListOpts{
+ Availability: gophercloud.AvailabilityPublic,
+ ServiceID: compute.ID,
+ })
+ err = computePager.EachPage(func(page pagination.Page) (bool, error) {
+ part, err := endpoints3.ExtractEndpoints(page)
+ if err != nil {
+ return false, err
+ }
+ if endpoint != nil {
+ t.Fatalf("Expected one endpoint, got more than one page")
+ return false, nil
+ }
+ if len(part) != 1 {
+ t.Fatalf("Expected one endpoint, got %d", len(part))
+ return false, nil
+ }
+
+ endpoint = &part[0]
+ return true, nil
+ })
+
+ if endpoint == nil {
+ t.Fatalf("No endpoint found.")
+ }
+
+ t.Logf("Success. The compute endpoint is at %s.", endpoint.URL)
+}
diff --git a/_site/acceptance/openstack/identity/v3/identity_test.go b/_site/acceptance/openstack/identity/v3/identity_test.go
new file mode 100644
index 0000000..e0503e2
--- /dev/null
+++ b/_site/acceptance/openstack/identity/v3/identity_test.go
@@ -0,0 +1,41 @@
+// +build acceptance
+
+package v3
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/openstack"
+ "github.com/rackspace/gophercloud/openstack/utils"
+)
+
+func createAuthenticatedClient(t *testing.T) *gophercloud.ServiceClient {
+ // Obtain credentials from the environment.
+ ao, err := utils.AuthOptions()
+ if err != nil {
+ t.Fatalf("Unable to acquire credentials: %v", err)
+ }
+
+ // Trim out unused fields.
+ ao.Username, ao.TenantID, ao.TenantName = "", "", ""
+
+ if ao.UserID == "" {
+ t.Logf("Skipping identity v3 tests because no OS_USERID is present.")
+ return nil
+ }
+
+ // Create a client and manually authenticate against v3.
+ providerClient, err := openstack.NewClient(ao.IdentityEndpoint)
+ if err != nil {
+ t.Fatalf("Unable to instantiate client: %v", err)
+ }
+
+ err = openstack.AuthenticateV3(providerClient, ao)
+ if err != nil {
+ t.Fatalf("Unable to authenticate against identity v3: %v", err)
+ }
+
+ // Create a service client.
+ return openstack.NewIdentityV3(providerClient)
+}
diff --git a/_site/acceptance/openstack/identity/v3/pkg.go b/_site/acceptance/openstack/identity/v3/pkg.go
new file mode 100644
index 0000000..d3b5573
--- /dev/null
+++ b/_site/acceptance/openstack/identity/v3/pkg.go
@@ -0,0 +1,2 @@
+// Package v3 contains acceptance tests for identity v3 resources.
+package v3
diff --git a/_site/acceptance/openstack/identity/v3/service_test.go b/_site/acceptance/openstack/identity/v3/service_test.go
new file mode 100644
index 0000000..082bd11
--- /dev/null
+++ b/_site/acceptance/openstack/identity/v3/service_test.go
@@ -0,0 +1,36 @@
+// +build acceptance
+
+package v3
+
+import (
+ "testing"
+
+ services3 "github.com/rackspace/gophercloud/openstack/identity/v3/services"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+func TestListServices(t *testing.T) {
+ // Create a service client.
+ serviceClient := createAuthenticatedClient(t)
+ if serviceClient == nil {
+ return
+ }
+
+ // Use the client to list all available services.
+ pager := services3.List(serviceClient, services3.ListOpts{})
+ err := pager.EachPage(func(page pagination.Page) (bool, error) {
+ parts, err := services3.ExtractServices(page)
+ if err != nil {
+ return false, err
+ }
+
+ t.Logf("--- Page ---")
+ for _, service := range parts {
+ t.Logf("Service: %32s %15s %10s %s", service.ID, service.Type, service.Name, *service.Description)
+ }
+ return true, nil
+ })
+ if err != nil {
+ t.Errorf("Unexpected error traversing pages: %v", err)
+ }
+}
diff --git a/_site/acceptance/openstack/identity/v3/token_test.go b/_site/acceptance/openstack/identity/v3/token_test.go
new file mode 100644
index 0000000..341acb7
--- /dev/null
+++ b/_site/acceptance/openstack/identity/v3/token_test.go
@@ -0,0 +1,43 @@
+// +build acceptance
+
+package v3
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud/openstack"
+ tokens3 "github.com/rackspace/gophercloud/openstack/identity/v3/tokens"
+ "github.com/rackspace/gophercloud/openstack/utils"
+)
+
+func TestGetToken(t *testing.T) {
+ // Obtain credentials from the environment.
+ ao, err := utils.AuthOptions()
+ if err != nil {
+ t.Fatalf("Unable to acquire credentials: %v", err)
+ }
+
+ // Trim out unused fields. Skip if we don't have a UserID.
+ ao.Username, ao.TenantID, ao.TenantName = "", "", ""
+ if ao.UserID == "" {
+ t.Logf("Skipping identity v3 tests because no OS_USERID is present.")
+ return
+ }
+
+ // Create an unauthenticated client.
+ provider, err := openstack.NewClient(ao.IdentityEndpoint)
+ if err != nil {
+ t.Fatalf("Unable to instantiate client: %v", err)
+ }
+
+ // Create a service client.
+ service := openstack.NewIdentityV3(provider)
+
+ // Use the service to create a token.
+ token, err := tokens3.Create(service, ao, nil).Extract()
+ if err != nil {
+ t.Fatalf("Unable to get token: %v", err)
+ }
+
+ t.Logf("Acquired token: %s", token.ID)
+}
diff --git a/_site/acceptance/openstack/networking/v2/apiversion_test.go b/_site/acceptance/openstack/networking/v2/apiversion_test.go
new file mode 100644
index 0000000..99e1d01
--- /dev/null
+++ b/_site/acceptance/openstack/networking/v2/apiversion_test.go
@@ -0,0 +1,51 @@
+// +build acceptance networking
+
+package v2
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud/openstack/networking/v2/apiversions"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestListAPIVersions(t *testing.T) {
+ Setup(t)
+ defer Teardown()
+
+ pager := apiversions.ListVersions(Client)
+ err := pager.EachPage(func(page pagination.Page) (bool, error) {
+ t.Logf("--- Page ---")
+
+ versions, err := apiversions.ExtractAPIVersions(page)
+ th.AssertNoErr(t, err)
+
+ for _, v := range versions {
+ t.Logf("API Version: ID [%s] Status [%s]", v.ID, v.Status)
+ }
+
+ return true, nil
+ })
+ th.CheckNoErr(t, err)
+}
+
+func TestListAPIResources(t *testing.T) {
+ Setup(t)
+ defer Teardown()
+
+ pager := apiversions.ListVersionResources(Client, "v2.0")
+ err := pager.EachPage(func(page pagination.Page) (bool, error) {
+ t.Logf("--- Page ---")
+
+ vrs, err := apiversions.ExtractVersionResources(page)
+ th.AssertNoErr(t, err)
+
+ for _, vr := range vrs {
+ t.Logf("Network: Name [%s] Collection [%s]", vr.Name, vr.Collection)
+ }
+
+ return true, nil
+ })
+ th.CheckNoErr(t, err)
+}
diff --git a/_site/acceptance/openstack/networking/v2/common.go b/_site/acceptance/openstack/networking/v2/common.go
new file mode 100644
index 0000000..6dd58af
--- /dev/null
+++ b/_site/acceptance/openstack/networking/v2/common.go
@@ -0,0 +1,40 @@
+package v2
+
+import (
+ "os"
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/openstack"
+ "github.com/rackspace/gophercloud/openstack/utils"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+var Client *gophercloud.ServiceClient
+
+func NewClient() (*gophercloud.ServiceClient, error) {
+ opts, err := utils.AuthOptions()
+ 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
+}
diff --git a/_site/acceptance/openstack/networking/v2/extension_test.go b/_site/acceptance/openstack/networking/v2/extension_test.go
new file mode 100644
index 0000000..edcbba4
--- /dev/null
+++ b/_site/acceptance/openstack/networking/v2/extension_test.go
@@ -0,0 +1,45 @@
+// +build acceptance networking
+
+package v2
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud/openstack/networking/v2/extensions"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestListExts(t *testing.T) {
+ Setup(t)
+ defer Teardown()
+
+ pager := extensions.List(Client)
+ err := pager.EachPage(func(page pagination.Page) (bool, error) {
+ t.Logf("--- Page ---")
+
+ exts, err := extensions.ExtractExtensions(page)
+ th.AssertNoErr(t, err)
+
+ for _, ext := range exts {
+ t.Logf("Extension: Name [%s] Description [%s]", ext.Name, ext.Description)
+ }
+
+ return true, nil
+ })
+ th.CheckNoErr(t, err)
+}
+
+func TestGetExt(t *testing.T) {
+ Setup(t)
+ defer Teardown()
+
+ ext, err := extensions.Get(Client, "service-type").Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, ext.Updated, "2013-01-20T00:00:00-00:00")
+ th.AssertEquals(t, ext.Name, "Neutron Service Type Management")
+ th.AssertEquals(t, ext.Namespace, "http://docs.openstack.org/ext/neutron/service-type/api/v1.0")
+ th.AssertEquals(t, ext.Alias, "service-type")
+ th.AssertEquals(t, ext.Description, "API for retrieving service providers for Neutron advanced services")
+}
diff --git a/_site/acceptance/openstack/networking/v2/extensions/layer3_test.go b/_site/acceptance/openstack/networking/v2/extensions/layer3_test.go
new file mode 100644
index 0000000..63e0be3
--- /dev/null
+++ b/_site/acceptance/openstack/networking/v2/extensions/layer3_test.go
@@ -0,0 +1,300 @@
+// +build acceptance networking layer3ext
+
+package extensions
+
+import (
+ "testing"
+
+ base "github.com/rackspace/gophercloud/acceptance/openstack/networking/v2"
+ "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/external"
+ "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/floatingips"
+ "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/routers"
+ "github.com/rackspace/gophercloud/openstack/networking/v2/networks"
+ "github.com/rackspace/gophercloud/openstack/networking/v2/ports"
+ "github.com/rackspace/gophercloud/openstack/networking/v2/subnets"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+const (
+ cidr1 = "10.0.0.1/24"
+ cidr2 = "20.0.0.1/24"
+)
+
+func TestAll(t *testing.T) {
+ base.Setup(t)
+ defer base.Teardown()
+
+ testRouter(t)
+ testFloatingIP(t)
+}
+
+func testRouter(t *testing.T) {
+ // Setup: Create network
+ networkID := createNetwork(t)
+
+ // Create router
+ routerID := createRouter(t, networkID)
+
+ // Lists routers
+ listRouters(t)
+
+ // Update router
+ updateRouter(t, routerID)
+
+ // Get router
+ getRouter(t, routerID)
+
+ // Create new subnet. Note: this subnet will be deleted when networkID is deleted
+ subnetID := createSubnet(t, networkID, cidr2)
+
+ // Add interface
+ addInterface(t, routerID, subnetID)
+
+ // Remove interface
+ removeInterface(t, routerID, subnetID)
+
+ // Delete router
+ deleteRouter(t, routerID)
+
+ // Cleanup
+ deleteNetwork(t, networkID)
+}
+
+func testFloatingIP(t *testing.T) {
+ // Setup external network
+ extNetworkID := createNetwork(t)
+
+ // Setup internal network, subnet and port
+ intNetworkID, subnetID, portID := createInternalTopology(t)
+
+ // Now the important part: we need to allow the external network to talk to
+ // the internal subnet. For this we need a router that has an interface to
+ // the internal subnet.
+ routerID := bridgeIntSubnetWithExtNetwork(t, extNetworkID, subnetID)
+
+ // Create floating IP
+ ipID := createFloatingIP(t, extNetworkID, portID)
+
+ // Get floating IP
+ getFloatingIP(t, ipID)
+
+ // Update floating IP
+ updateFloatingIP(t, ipID, portID)
+
+ // Delete floating IP
+ deleteFloatingIP(t, ipID)
+
+ // Remove the internal subnet interface
+ removeInterface(t, routerID, subnetID)
+
+ // Delete router and external network
+ deleteRouter(t, routerID)
+ deleteNetwork(t, extNetworkID)
+
+ // Delete internal port and network
+ deletePort(t, portID)
+ deleteNetwork(t, intNetworkID)
+}
+
+func createNetwork(t *testing.T) string {
+ t.Logf("Creating a network")
+
+ asu := true
+ opts := external.CreateOpts{
+ Parent: networks.CreateOpts{Name: "sample_network", AdminStateUp: &asu},
+ External: true,
+ }
+ n, err := networks.Create(base.Client, opts).Extract()
+
+ th.AssertNoErr(t, err)
+
+ if n.ID == "" {
+ t.Fatalf("No ID returned when creating a network")
+ }
+
+ createSubnet(t, n.ID, cidr1)
+
+ t.Logf("Network created: ID [%s]", n.ID)
+
+ return n.ID
+}
+
+func deleteNetwork(t *testing.T, networkID string) {
+ t.Logf("Deleting network %s", networkID)
+ networks.Delete(base.Client, networkID)
+}
+
+func deletePort(t *testing.T, portID string) {
+ t.Logf("Deleting port %s", portID)
+ ports.Delete(base.Client, portID)
+}
+
+func createInternalTopology(t *testing.T) (string, string, string) {
+ t.Logf("Creating an internal network (for port)")
+ opts := networks.CreateOpts{Name: "internal_network"}
+ n, err := networks.Create(base.Client, opts).Extract()
+ th.AssertNoErr(t, err)
+
+ // A subnet is also needed
+ subnetID := createSubnet(t, n.ID, cidr2)
+
+ t.Logf("Creating an internal port on network %s", n.ID)
+ p, err := ports.Create(base.Client, ports.CreateOpts{
+ NetworkID: n.ID,
+ Name: "fixed_internal_port",
+ }).Extract()
+ th.AssertNoErr(t, err)
+
+ return n.ID, subnetID, p.ID
+}
+
+func bridgeIntSubnetWithExtNetwork(t *testing.T, networkID, subnetID string) string {
+ // Create router with external gateway info
+ routerID := createRouter(t, networkID)
+
+ // Add interface for internal subnet
+ addInterface(t, routerID, subnetID)
+
+ return routerID
+}
+
+func createSubnet(t *testing.T, networkID, cidr string) string {
+ t.Logf("Creating a subnet for network %s", networkID)
+
+ iFalse := false
+ s, err := subnets.Create(base.Client, subnets.CreateOpts{
+ NetworkID: networkID,
+ CIDR: cidr,
+ IPVersion: subnets.IPv4,
+ Name: "my_subnet",
+ EnableDHCP: &iFalse,
+ }).Extract()
+
+ th.AssertNoErr(t, err)
+
+ t.Logf("Subnet created: ID [%s]", s.ID)
+
+ return s.ID
+}
+
+func createRouter(t *testing.T, networkID string) string {
+ t.Logf("Creating a router for network %s", networkID)
+
+ asu := false
+ gwi := routers.GatewayInfo{NetworkID: networkID}
+ r, err := routers.Create(base.Client, routers.CreateOpts{
+ Name: "foo_router",
+ AdminStateUp: &asu,
+ GatewayInfo: &gwi,
+ }).Extract()
+
+ th.AssertNoErr(t, err)
+
+ if r.ID == "" {
+ t.Fatalf("No ID returned when creating a router")
+ }
+
+ t.Logf("Router created: ID [%s]", r.ID)
+
+ return r.ID
+}
+
+func listRouters(t *testing.T) {
+ pager := routers.List(base.Client, routers.ListOpts{})
+
+ err := pager.EachPage(func(page pagination.Page) (bool, error) {
+ routerList, err := routers.ExtractRouters(page)
+ th.AssertNoErr(t, err)
+
+ for _, r := range routerList {
+ t.Logf("Listing router: ID [%s] Name [%s] Status [%s] GatewayInfo [%#v]",
+ r.ID, r.Name, r.Status, r.GatewayInfo)
+ }
+
+ return true, nil
+ })
+
+ th.AssertNoErr(t, err)
+}
+
+func updateRouter(t *testing.T, routerID string) {
+ _, err := routers.Update(base.Client, routerID, routers.UpdateOpts{
+ Name: "another_name",
+ }).Extract()
+
+ th.AssertNoErr(t, err)
+}
+
+func getRouter(t *testing.T, routerID string) {
+ r, err := routers.Get(base.Client, routerID).Extract()
+
+ th.AssertNoErr(t, err)
+
+ t.Logf("Getting router: ID [%s] Name [%s] Status [%s]", r.ID, r.Name, r.Status)
+}
+
+func addInterface(t *testing.T, routerID, subnetID string) {
+ ir, err := routers.AddInterface(base.Client, routerID, routers.InterfaceOpts{SubnetID: subnetID}).Extract()
+
+ th.AssertNoErr(t, err)
+
+ t.Logf("Interface added to router %s: SubnetID [%s] PortID [%s]", routerID, ir.SubnetID, ir.PortID)
+}
+
+func removeInterface(t *testing.T, routerID, subnetID string) {
+ ir, err := routers.RemoveInterface(base.Client, routerID, routers.InterfaceOpts{SubnetID: subnetID}).Extract()
+
+ th.AssertNoErr(t, err)
+
+ t.Logf("Interface %s removed from %s", ir.ID, routerID)
+}
+
+func deleteRouter(t *testing.T, routerID string) {
+ t.Logf("Deleting router %s", routerID)
+
+ res := routers.Delete(base.Client, routerID)
+
+ th.AssertNoErr(t, res.Err)
+}
+
+func createFloatingIP(t *testing.T, networkID, portID string) string {
+ t.Logf("Creating floating IP on network [%s] with port [%s]", networkID, portID)
+
+ opts := floatingips.CreateOpts{
+ FloatingNetworkID: networkID,
+ PortID: portID,
+ }
+
+ ip, err := floatingips.Create(base.Client, opts).Extract()
+
+ th.AssertNoErr(t, err)
+
+ t.Logf("Floating IP created: ID [%s] Status [%s] Fixed (internal) IP: [%s] Floating (external) IP: [%s]",
+ ip.ID, ip.Status, ip.FixedIP, ip.FloatingIP)
+
+ return ip.ID
+}
+
+func getFloatingIP(t *testing.T, ipID string) {
+ ip, err := floatingips.Get(base.Client, ipID).Extract()
+ th.AssertNoErr(t, err)
+
+ t.Logf("Getting floating IP: ID [%s] Status [%s]", ip.ID, ip.Status)
+}
+
+func updateFloatingIP(t *testing.T, ipID, portID string) {
+ t.Logf("Disassociate all ports from IP %s", ipID)
+ _, err := floatingips.Update(base.Client, ipID, floatingips.UpdateOpts{PortID: ""}).Extract()
+ th.AssertNoErr(t, err)
+
+ t.Logf("Re-associate the port %s", portID)
+ _, err = floatingips.Update(base.Client, ipID, floatingips.UpdateOpts{PortID: portID}).Extract()
+ th.AssertNoErr(t, err)
+}
+
+func deleteFloatingIP(t *testing.T, ipID string) {
+ t.Logf("Deleting IP %s", ipID)
+ res := floatingips.Delete(base.Client, ipID)
+ th.AssertNoErr(t, res.Err)
+}
diff --git a/_site/acceptance/openstack/networking/v2/extensions/lbaas/common.go b/_site/acceptance/openstack/networking/v2/extensions/lbaas/common.go
new file mode 100644
index 0000000..a9db1af
--- /dev/null
+++ b/_site/acceptance/openstack/networking/v2/extensions/lbaas/common.go
@@ -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: 5,
+ 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
+}
diff --git a/_site/acceptance/openstack/networking/v2/extensions/lbaas/member_test.go b/_site/acceptance/openstack/networking/v2/extensions/lbaas/member_test.go
new file mode 100644
index 0000000..9b60582
--- /dev/null
+++ b/_site/acceptance/openstack/networking/v2/extensions/lbaas/member_test.go
@@ -0,0 +1,95 @@
+// +build acceptance networking lbaas lbaasmember
+
+package lbaas
+
+import (
+ "testing"
+
+ base "github.com/rackspace/gophercloud/acceptance/openstack/networking/v2"
+ "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/members"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestMembers(t *testing.T) {
+ base.Setup(t)
+ defer base.Teardown()
+
+ // setup
+ networkID, subnetID := SetupTopology(t)
+ poolID := CreatePool(t, subnetID)
+
+ // create member
+ memberID := createMember(t, poolID)
+
+ // list members
+ listMembers(t)
+
+ // update member
+ updateMember(t, memberID)
+
+ // get member
+ getMember(t, memberID)
+
+ // delete member
+ deleteMember(t, memberID)
+
+ // teardown
+ DeletePool(t, poolID)
+ DeleteTopology(t, networkID)
+}
+
+func createMember(t *testing.T, poolID string) string {
+ m, err := members.Create(base.Client, members.CreateOpts{
+ Address: "192.168.199.1",
+ ProtocolPort: 8080,
+ PoolID: poolID,
+ }).Extract()
+
+ th.AssertNoErr(t, err)
+
+ t.Logf("Created member: ID [%s] Status [%s] Weight [%d] Address [%s] Port [%d]",
+ m.ID, m.Status, m.Weight, m.Address, m.ProtocolPort)
+
+ return m.ID
+}
+
+func listMembers(t *testing.T) {
+ err := members.List(base.Client, members.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+ memberList, err := members.ExtractMembers(page)
+ if err != nil {
+ t.Errorf("Failed to extract members: %v", err)
+ return false, err
+ }
+
+ for _, m := range memberList {
+ t.Logf("Listing member: ID [%s] Status [%s]", m.ID, m.Status)
+ }
+
+ return true, nil
+ })
+
+ th.AssertNoErr(t, err)
+}
+
+func updateMember(t *testing.T, memberID string) {
+ m, err := members.Update(base.Client, memberID, members.UpdateOpts{AdminStateUp: true}).Extract()
+
+ th.AssertNoErr(t, err)
+
+ t.Logf("Updated member ID [%s]", m.ID)
+}
+
+func getMember(t *testing.T, memberID string) {
+ m, err := members.Get(base.Client, memberID).Extract()
+
+ th.AssertNoErr(t, err)
+
+ t.Logf("Getting member ID [%s]", m.ID)
+}
+
+func deleteMember(t *testing.T, memberID string) {
+ res := members.Delete(base.Client, memberID)
+ th.AssertNoErr(t, res.Err)
+ t.Logf("Deleted member %s", memberID)
+}
diff --git a/_site/acceptance/openstack/networking/v2/extensions/lbaas/monitor_test.go b/_site/acceptance/openstack/networking/v2/extensions/lbaas/monitor_test.go
new file mode 100644
index 0000000..57e860c
--- /dev/null
+++ b/_site/acceptance/openstack/networking/v2/extensions/lbaas/monitor_test.go
@@ -0,0 +1,77 @@
+// +build acceptance networking lbaas lbaasmonitor
+
+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/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestMonitors(t *testing.T) {
+ base.Setup(t)
+ defer base.Teardown()
+
+ // create monitor
+ monitorID := CreateMonitor(t)
+
+ // list monitors
+ listMonitors(t)
+
+ // update monitor
+ updateMonitor(t, monitorID)
+
+ // get monitor
+ getMonitor(t, monitorID)
+
+ // delete monitor
+ deleteMonitor(t, monitorID)
+}
+
+func listMonitors(t *testing.T) {
+ err := monitors.List(base.Client, monitors.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+ monitorList, err := monitors.ExtractMonitors(page)
+ if err != nil {
+ t.Errorf("Failed to extract monitors: %v", err)
+ return false, err
+ }
+
+ for _, m := range monitorList {
+ t.Logf("Listing monitor: ID [%s] Type [%s] Delay [%ds] Timeout [%d] Retries [%d] Status [%s]",
+ m.ID, m.Type, m.Delay, m.Timeout, m.MaxRetries, m.Status)
+ }
+
+ return true, nil
+ })
+
+ th.AssertNoErr(t, err)
+}
+
+func updateMonitor(t *testing.T, monitorID string) {
+ opts := monitors.UpdateOpts{Delay: 5, Timeout: 10, MaxRetries: 3}
+ m, err := monitors.Update(base.Client, monitorID, opts).Extract()
+
+ th.AssertNoErr(t, err)
+
+ t.Logf("Updated monitor ID [%s]", m.ID)
+}
+
+func getMonitor(t *testing.T, monitorID string) {
+ m, err := monitors.Get(base.Client, monitorID).Extract()
+
+ th.AssertNoErr(t, err)
+
+ t.Logf("Getting monitor ID [%s]: URL path [%s] HTTP Method [%s] Accepted codes [%s]",
+ m.ID, m.URLPath, m.HTTPMethod, m.ExpectedCodes)
+}
+
+func deleteMonitor(t *testing.T, monitorID string) {
+ res := monitors.Delete(base.Client, monitorID)
+
+ th.AssertNoErr(t, res.Err)
+
+ t.Logf("Deleted monitor %s", monitorID)
+}
diff --git a/_site/acceptance/openstack/networking/v2/extensions/lbaas/pkg.go b/_site/acceptance/openstack/networking/v2/extensions/lbaas/pkg.go
new file mode 100644
index 0000000..f5a7df7
--- /dev/null
+++ b/_site/acceptance/openstack/networking/v2/extensions/lbaas/pkg.go
@@ -0,0 +1 @@
+package lbaas
diff --git a/_site/acceptance/openstack/networking/v2/extensions/lbaas/pool_test.go b/_site/acceptance/openstack/networking/v2/extensions/lbaas/pool_test.go
new file mode 100644
index 0000000..8194064
--- /dev/null
+++ b/_site/acceptance/openstack/networking/v2/extensions/lbaas/pool_test.go
@@ -0,0 +1,98 @@
+// +build acceptance networking lbaas lbaaspool
+
+package lbaas
+
+import (
+ "testing"
+
+ base "github.com/rackspace/gophercloud/acceptance/openstack/networking/v2"
+ "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/pools"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestPools(t *testing.T) {
+ base.Setup(t)
+ defer base.Teardown()
+
+ // setup
+ networkID, subnetID := SetupTopology(t)
+
+ // create pool
+ poolID := CreatePool(t, subnetID)
+
+ // list pools
+ listPools(t)
+
+ // update pool
+ updatePool(t, poolID)
+
+ // get pool
+ getPool(t, poolID)
+
+ // create monitor
+ monitorID := CreateMonitor(t)
+
+ // associate health monitor
+ associateMonitor(t, poolID, monitorID)
+
+ // disassociate health monitor
+ disassociateMonitor(t, poolID, monitorID)
+
+ // delete pool
+ DeletePool(t, poolID)
+
+ // teardown
+ DeleteTopology(t, networkID)
+}
+
+func listPools(t *testing.T) {
+ err := pools.List(base.Client, pools.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+ poolList, err := pools.ExtractPools(page)
+ if err != nil {
+ t.Errorf("Failed to extract pools: %v", err)
+ return false, err
+ }
+
+ for _, p := range poolList {
+ t.Logf("Listing pool: ID [%s] Name [%s] Status [%s] LB algorithm [%s]", p.ID, p.Name, p.Status, p.LBMethod)
+ }
+
+ return true, nil
+ })
+
+ th.AssertNoErr(t, err)
+}
+
+func updatePool(t *testing.T, poolID string) {
+ opts := pools.UpdateOpts{Name: "SuperPool", LBMethod: pools.LBMethodLeastConnections}
+ p, err := pools.Update(base.Client, poolID, opts).Extract()
+
+ th.AssertNoErr(t, err)
+
+ t.Logf("Updated pool ID [%s]", p.ID)
+}
+
+func getPool(t *testing.T, poolID string) {
+ p, err := pools.Get(base.Client, poolID).Extract()
+
+ th.AssertNoErr(t, err)
+
+ t.Logf("Getting pool ID [%s]", p.ID)
+}
+
+func associateMonitor(t *testing.T, poolID, monitorID string) {
+ res := pools.AssociateMonitor(base.Client, poolID, monitorID)
+
+ th.AssertNoErr(t, res.Err)
+
+ t.Logf("Associated pool %s with monitor %s", poolID, monitorID)
+}
+
+func disassociateMonitor(t *testing.T, poolID, monitorID string) {
+ res := pools.DisassociateMonitor(base.Client, poolID, monitorID)
+
+ th.AssertNoErr(t, res.Err)
+
+ t.Logf("Disassociated pool %s with monitor %s", poolID, monitorID)
+}
diff --git a/_site/acceptance/openstack/networking/v2/extensions/lbaas/vip_test.go b/_site/acceptance/openstack/networking/v2/extensions/lbaas/vip_test.go
new file mode 100644
index 0000000..c8dff2d
--- /dev/null
+++ b/_site/acceptance/openstack/networking/v2/extensions/lbaas/vip_test.go
@@ -0,0 +1,101 @@
+// +build acceptance networking lbaas lbaasvip
+
+package lbaas
+
+import (
+ "testing"
+
+ base "github.com/rackspace/gophercloud/acceptance/openstack/networking/v2"
+ "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/vips"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestVIPs(t *testing.T) {
+ base.Setup(t)
+ defer base.Teardown()
+
+ // setup
+ networkID, subnetID := SetupTopology(t)
+ poolID := CreatePool(t, subnetID)
+
+ // create VIP
+ VIPID := createVIP(t, subnetID, poolID)
+
+ // list VIPs
+ listVIPs(t)
+
+ // update VIP
+ updateVIP(t, VIPID)
+
+ // get VIP
+ getVIP(t, VIPID)
+
+ // delete VIP
+ deleteVIP(t, VIPID)
+
+ // teardown
+ DeletePool(t, poolID)
+ DeleteTopology(t, networkID)
+}
+
+func createVIP(t *testing.T, subnetID, poolID string) string {
+ p, err := vips.Create(base.Client, vips.CreateOpts{
+ Protocol: "HTTP",
+ Name: "New_VIP",
+ AdminStateUp: vips.Up,
+ SubnetID: subnetID,
+ PoolID: poolID,
+ ProtocolPort: 80,
+ }).Extract()
+
+ th.AssertNoErr(t, err)
+
+ t.Logf("Created pool %s", p.ID)
+
+ return p.ID
+}
+
+func listVIPs(t *testing.T) {
+ err := vips.List(base.Client, vips.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+ vipList, err := vips.ExtractVIPs(page)
+ if err != nil {
+ t.Errorf("Failed to extract VIPs: %v", err)
+ return false, err
+ }
+
+ for _, vip := range vipList {
+ t.Logf("Listing VIP: ID [%s] Name [%s] Address [%s] Port [%s] Connection Limit [%d]",
+ vip.ID, vip.Name, vip.Address, vip.ProtocolPort, vip.ConnLimit)
+ }
+
+ return true, nil
+ })
+
+ th.AssertNoErr(t, err)
+}
+
+func updateVIP(t *testing.T, VIPID string) {
+ i1000 := 1000
+ _, err := vips.Update(base.Client, VIPID, vips.UpdateOpts{ConnLimit: &i1000}).Extract()
+
+ th.AssertNoErr(t, err)
+
+ t.Logf("Updated VIP ID [%s]", VIPID)
+}
+
+func getVIP(t *testing.T, VIPID string) {
+ vip, err := vips.Get(base.Client, VIPID).Extract()
+
+ th.AssertNoErr(t, err)
+
+ t.Logf("Getting VIP ID [%s]: Status [%s]", vip.ID, vip.Status)
+}
+
+func deleteVIP(t *testing.T, VIPID string) {
+ res := vips.Delete(base.Client, VIPID)
+
+ th.AssertNoErr(t, res.Err)
+
+ t.Logf("Deleted VIP %s", VIPID)
+}
diff --git a/_site/acceptance/openstack/networking/v2/extensions/pkg.go b/_site/acceptance/openstack/networking/v2/extensions/pkg.go
new file mode 100644
index 0000000..aeec0fa
--- /dev/null
+++ b/_site/acceptance/openstack/networking/v2/extensions/pkg.go
@@ -0,0 +1 @@
+package extensions
diff --git a/_site/acceptance/openstack/networking/v2/extensions/provider_test.go b/_site/acceptance/openstack/networking/v2/extensions/provider_test.go
new file mode 100644
index 0000000..f10c9d9
--- /dev/null
+++ b/_site/acceptance/openstack/networking/v2/extensions/provider_test.go
@@ -0,0 +1,68 @@
+// +build acceptance networking
+
+package extensions
+
+import (
+ "strconv"
+ "testing"
+
+ base "github.com/rackspace/gophercloud/acceptance/openstack/networking/v2"
+ "github.com/rackspace/gophercloud/openstack/networking/v2/networks"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestNetworkCRUDOperations(t *testing.T) {
+ base.Setup(t)
+ defer base.Teardown()
+
+ // Create a network
+ n, err := networks.Create(base.Client, networks.CreateOpts{Name: "sample_network", AdminStateUp: networks.Up}).Extract()
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, n.Name, "sample_network")
+ th.AssertEquals(t, n.AdminStateUp, true)
+ networkID := n.ID
+
+ // List networks
+ pager := networks.List(base.Client, networks.ListOpts{Limit: 2})
+ err = pager.EachPage(func(page pagination.Page) (bool, error) {
+ t.Logf("--- Page ---")
+
+ networkList, err := networks.ExtractNetworks(page)
+ th.AssertNoErr(t, err)
+
+ for _, n := range networkList {
+ t.Logf("Network: ID [%s] Name [%s] Status [%s] Is shared? [%s]",
+ n.ID, n.Name, n.Status, strconv.FormatBool(n.Shared))
+ }
+
+ return true, nil
+ })
+ th.CheckNoErr(t, err)
+
+ // Get a network
+ if networkID == "" {
+ t.Fatalf("In order to retrieve a network, the NetworkID must be set")
+ }
+ n, err = networks.Get(base.Client, networkID).Extract()
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, n.Status, "ACTIVE")
+ th.AssertDeepEquals(t, n.Subnets, []string{})
+ th.AssertEquals(t, n.Name, "sample_network")
+ th.AssertEquals(t, n.AdminStateUp, true)
+ th.AssertEquals(t, n.Shared, false)
+ th.AssertEquals(t, n.ID, networkID)
+
+ // Update network
+ n, err = networks.Update(base.Client, networkID, networks.UpdateOpts{Name: "new_network_name"}).Extract()
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, n.Name, "new_network_name")
+
+ // Delete network
+ res := networks.Delete(base.Client, networkID)
+ th.AssertNoErr(t, res.Err)
+}
+
+func TestCreateMultipleNetworks(t *testing.T) {
+ //networks.CreateMany()
+}
diff --git a/_site/acceptance/openstack/networking/v2/extensions/security_test.go b/_site/acceptance/openstack/networking/v2/extensions/security_test.go
new file mode 100644
index 0000000..16ecca4
--- /dev/null
+++ b/_site/acceptance/openstack/networking/v2/extensions/security_test.go
@@ -0,0 +1,169 @@
+// +build acceptance networking security
+
+package extensions
+
+import (
+ "testing"
+
+ base "github.com/rackspace/gophercloud/acceptance/openstack/networking/v2"
+ "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/groups"
+ "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/rules"
+ "github.com/rackspace/gophercloud/openstack/networking/v2/networks"
+ "github.com/rackspace/gophercloud/openstack/networking/v2/ports"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestSecurityGroups(t *testing.T) {
+ base.Setup(t)
+ defer base.Teardown()
+
+ // create security group
+ groupID := createSecGroup(t)
+
+ // list security group
+ listSecGroups(t)
+
+ // get security group
+ getSecGroup(t, groupID)
+
+ // create port with security group
+ networkID, portID := createPort(t, groupID)
+
+ // delete port
+ deletePort(t, portID)
+
+ // delete security group
+ deleteSecGroup(t, groupID)
+
+ // teardown
+ deleteNetwork(t, networkID)
+}
+
+func TestSecurityGroupRules(t *testing.T) {
+ base.Setup(t)
+ defer base.Teardown()
+
+ // create security group
+ groupID := createSecGroup(t)
+
+ // create security group rule
+ ruleID := createSecRule(t, groupID)
+
+ // list security group rule
+ listSecRules(t)
+
+ // get security group rule
+ getSecRule(t, ruleID)
+
+ // delete security group rule
+ deleteSecRule(t, ruleID)
+}
+
+func createSecGroup(t *testing.T) string {
+ sg, err := groups.Create(base.Client, groups.CreateOpts{
+ Name: "new-webservers",
+ Description: "security group for webservers",
+ }).Extract()
+
+ th.AssertNoErr(t, err)
+
+ t.Logf("Created security group %s", sg.ID)
+
+ return sg.ID
+}
+
+func listSecGroups(t *testing.T) {
+ err := groups.List(base.Client, groups.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+ list, err := groups.ExtractGroups(page)
+ if err != nil {
+ t.Errorf("Failed to extract secgroups: %v", err)
+ return false, err
+ }
+
+ for _, sg := range list {
+ t.Logf("Listing security group: ID [%s] Name [%s]", sg.ID, sg.Name)
+ }
+
+ return true, nil
+ })
+
+ th.AssertNoErr(t, err)
+}
+
+func getSecGroup(t *testing.T, id string) {
+ sg, err := groups.Get(base.Client, id).Extract()
+ th.AssertNoErr(t, err)
+ t.Logf("Getting security group: ID [%s] Name [%s] Description [%s]", sg.ID, sg.Name, sg.Description)
+}
+
+func createPort(t *testing.T, groupID string) (string, string) {
+ n, err := networks.Create(base.Client, networks.CreateOpts{Name: "tmp_network"}).Extract()
+ th.AssertNoErr(t, err)
+ t.Logf("Created network %s", n.ID)
+
+ opts := ports.CreateOpts{
+ NetworkID: n.ID,
+ Name: "my_port",
+ SecurityGroups: []string{groupID},
+ }
+ p, err := ports.Create(base.Client, opts).Extract()
+ th.AssertNoErr(t, err)
+ t.Logf("Created port %s with security group %s", p.ID, groupID)
+
+ return n.ID, p.ID
+}
+
+func deleteSecGroup(t *testing.T, groupID string) {
+ res := groups.Delete(base.Client, groupID)
+ th.AssertNoErr(t, res.Err)
+ t.Logf("Deleted security group %s", groupID)
+}
+
+func createSecRule(t *testing.T, groupID string) string {
+ r, err := rules.Create(base.Client, rules.CreateOpts{
+ Direction: "ingress",
+ PortRangeMin: 80,
+ EtherType: "IPv4",
+ PortRangeMax: 80,
+ Protocol: "tcp",
+ SecGroupID: groupID,
+ }).Extract()
+
+ th.AssertNoErr(t, err)
+
+ t.Logf("Created security group rule %s", r.ID)
+
+ return r.ID
+}
+
+func listSecRules(t *testing.T) {
+ err := rules.List(base.Client, rules.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+ list, err := rules.ExtractRules(page)
+ if err != nil {
+ t.Errorf("Failed to extract sec rules: %v", err)
+ return false, err
+ }
+
+ for _, r := range list {
+ t.Logf("Listing security rule: ID [%s]", r.ID)
+ }
+
+ return true, nil
+ })
+
+ th.AssertNoErr(t, err)
+}
+
+func getSecRule(t *testing.T, id string) {
+ r, err := rules.Get(base.Client, id).Extract()
+ th.AssertNoErr(t, err)
+ t.Logf("Getting security rule: ID [%s] Direction [%s] EtherType [%s] Protocol [%s]",
+ r.ID, r.Direction, r.EtherType, r.Protocol)
+}
+
+func deleteSecRule(t *testing.T, id string) {
+ res := rules.Delete(base.Client, id)
+ th.AssertNoErr(t, res.Err)
+ t.Logf("Deleted security rule %s", id)
+}
diff --git a/_site/acceptance/openstack/networking/v2/network_test.go b/_site/acceptance/openstack/networking/v2/network_test.go
new file mode 100644
index 0000000..be8a3a1
--- /dev/null
+++ b/_site/acceptance/openstack/networking/v2/network_test.go
@@ -0,0 +1,68 @@
+// +build acceptance networking
+
+package v2
+
+import (
+ "strconv"
+ "testing"
+
+ "github.com/rackspace/gophercloud/openstack/networking/v2/networks"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestNetworkCRUDOperations(t *testing.T) {
+ Setup(t)
+ defer Teardown()
+
+ // Create a network
+ n, err := networks.Create(Client, networks.CreateOpts{Name: "sample_network", AdminStateUp: networks.Up}).Extract()
+ th.AssertNoErr(t, err)
+ defer networks.Delete(Client, n.ID)
+ th.AssertEquals(t, n.Name, "sample_network")
+ th.AssertEquals(t, n.AdminStateUp, true)
+ networkID := n.ID
+
+ // List networks
+ pager := networks.List(Client, networks.ListOpts{Limit: 2})
+ err = pager.EachPage(func(page pagination.Page) (bool, error) {
+ t.Logf("--- Page ---")
+
+ networkList, err := networks.ExtractNetworks(page)
+ th.AssertNoErr(t, err)
+
+ for _, n := range networkList {
+ t.Logf("Network: ID [%s] Name [%s] Status [%s] Is shared? [%s]",
+ n.ID, n.Name, n.Status, strconv.FormatBool(n.Shared))
+ }
+
+ return true, nil
+ })
+ th.CheckNoErr(t, err)
+
+ // Get a network
+ if networkID == "" {
+ t.Fatalf("In order to retrieve a network, the NetworkID must be set")
+ }
+ n, err = networks.Get(Client, networkID).Extract()
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, n.Status, "ACTIVE")
+ th.AssertDeepEquals(t, n.Subnets, []string{})
+ th.AssertEquals(t, n.Name, "sample_network")
+ th.AssertEquals(t, n.AdminStateUp, true)
+ th.AssertEquals(t, n.Shared, false)
+ th.AssertEquals(t, n.ID, networkID)
+
+ // Update network
+ n, err = networks.Update(Client, networkID, networks.UpdateOpts{Name: "new_network_name"}).Extract()
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, n.Name, "new_network_name")
+
+ // Delete network
+ res := networks.Delete(Client, networkID)
+ th.AssertNoErr(t, res.Err)
+}
+
+func TestCreateMultipleNetworks(t *testing.T) {
+ //networks.CreateMany()
+}
diff --git a/_site/acceptance/openstack/networking/v2/pkg.go b/_site/acceptance/openstack/networking/v2/pkg.go
new file mode 100644
index 0000000..5ec3cc8
--- /dev/null
+++ b/_site/acceptance/openstack/networking/v2/pkg.go
@@ -0,0 +1 @@
+package v2
diff --git a/_site/acceptance/openstack/networking/v2/port_test.go b/_site/acceptance/openstack/networking/v2/port_test.go
new file mode 100644
index 0000000..7f22dbd
--- /dev/null
+++ b/_site/acceptance/openstack/networking/v2/port_test.go
@@ -0,0 +1,117 @@
+// +build acceptance networking
+
+package v2
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud/openstack/networking/v2/networks"
+ "github.com/rackspace/gophercloud/openstack/networking/v2/ports"
+ "github.com/rackspace/gophercloud/openstack/networking/v2/subnets"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestPortCRUD(t *testing.T) {
+ Setup(t)
+ defer Teardown()
+
+ // Setup network
+ t.Log("Setting up network")
+ networkID, err := createNetwork()
+ th.AssertNoErr(t, err)
+ defer networks.Delete(Client, networkID)
+
+ // Setup subnet
+ t.Logf("Setting up subnet on network %s", networkID)
+ subnetID, err := createSubnet(networkID)
+ th.AssertNoErr(t, err)
+ defer subnets.Delete(Client, subnetID)
+
+ // Create port
+ t.Logf("Create port based on subnet %s", subnetID)
+ portID := createPort(t, networkID, subnetID)
+
+ // List ports
+ t.Logf("Listing all ports")
+ listPorts(t)
+
+ // Get port
+ if portID == "" {
+ t.Fatalf("In order to retrieve a port, the portID must be set")
+ }
+ p, err := ports.Get(Client, portID).Extract()
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, p.ID, portID)
+
+ // Update port
+ p, err = ports.Update(Client, portID, ports.UpdateOpts{Name: "new_port_name"}).Extract()
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, p.Name, "new_port_name")
+
+ // Delete port
+ res := ports.Delete(Client, portID)
+ th.AssertNoErr(t, res.Err)
+}
+
+func createPort(t *testing.T, networkID, subnetID string) string {
+ enable := false
+ opts := ports.CreateOpts{
+ NetworkID: networkID,
+ Name: "my_port",
+ AdminStateUp: &enable,
+ FixedIPs: []ports.IP{ports.IP{SubnetID: subnetID}},
+ }
+ p, err := ports.Create(Client, opts).Extract()
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, p.NetworkID, networkID)
+ th.AssertEquals(t, p.Name, "my_port")
+ th.AssertEquals(t, p.AdminStateUp, false)
+
+ return p.ID
+}
+
+func listPorts(t *testing.T) {
+ count := 0
+ pager := ports.List(Client, ports.ListOpts{})
+ err := pager.EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ t.Logf("--- Page ---")
+
+ portList, err := ports.ExtractPorts(page)
+ th.AssertNoErr(t, err)
+
+ for _, p := range portList {
+ t.Logf("Port: ID [%s] Name [%s] Status [%d] MAC addr [%s] Fixed IPs [%#v] Security groups [%#v]",
+ p.ID, p.Name, p.Status, p.MACAddress, p.FixedIPs, p.SecurityGroups)
+ }
+
+ return true, nil
+ })
+
+ th.CheckNoErr(t, err)
+
+ if count == 0 {
+ t.Logf("No pages were iterated over when listing ports")
+ }
+}
+
+func createNetwork() (string, error) {
+ res, err := networks.Create(Client, networks.CreateOpts{Name: "tmp_network", AdminStateUp: networks.Up}).Extract()
+ return res.ID, err
+}
+
+func createSubnet(networkID string) (string, error) {
+ s, err := subnets.Create(Client, subnets.CreateOpts{
+ NetworkID: networkID,
+ CIDR: "192.168.199.0/24",
+ IPVersion: subnets.IPv4,
+ Name: "my_subnet",
+ EnableDHCP: subnets.Down,
+ }).Extract()
+ return s.ID, err
+}
+
+func TestPortBatchCreate(t *testing.T) {
+ // todo
+}
diff --git a/_site/acceptance/openstack/networking/v2/subnet_test.go b/_site/acceptance/openstack/networking/v2/subnet_test.go
new file mode 100644
index 0000000..097a303
--- /dev/null
+++ b/_site/acceptance/openstack/networking/v2/subnet_test.go
@@ -0,0 +1,86 @@
+// +build acceptance networking
+
+package v2
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud/openstack/networking/v2/networks"
+ "github.com/rackspace/gophercloud/openstack/networking/v2/subnets"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestList(t *testing.T) {
+ Setup(t)
+ defer Teardown()
+
+ pager := subnets.List(Client, subnets.ListOpts{Limit: 2})
+ err := pager.EachPage(func(page pagination.Page) (bool, error) {
+ t.Logf("--- Page ---")
+
+ subnetList, err := subnets.ExtractSubnets(page)
+ th.AssertNoErr(t, err)
+
+ for _, s := range subnetList {
+ t.Logf("Subnet: ID [%s] Name [%s] IP Version [%d] CIDR [%s] GatewayIP [%s]",
+ s.ID, s.Name, s.IPVersion, s.CIDR, s.GatewayIP)
+ }
+
+ return true, nil
+ })
+ th.CheckNoErr(t, err)
+}
+
+func TestCRUD(t *testing.T) {
+ Setup(t)
+ defer Teardown()
+
+ // Setup network
+ t.Log("Setting up network")
+ n, err := networks.Create(Client, networks.CreateOpts{Name: "tmp_network", AdminStateUp: networks.Up}).Extract()
+ th.AssertNoErr(t, err)
+ networkID := n.ID
+ defer networks.Delete(Client, networkID)
+
+ // Create subnet
+ t.Log("Create subnet")
+ enable := false
+ opts := subnets.CreateOpts{
+ NetworkID: networkID,
+ CIDR: "192.168.199.0/24",
+ IPVersion: subnets.IPv4,
+ Name: "my_subnet",
+ EnableDHCP: &enable,
+ }
+ s, err := subnets.Create(Client, opts).Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, s.NetworkID, networkID)
+ th.AssertEquals(t, s.CIDR, "192.168.199.0/24")
+ th.AssertEquals(t, s.IPVersion, 4)
+ th.AssertEquals(t, s.Name, "my_subnet")
+ th.AssertEquals(t, s.EnableDHCP, false)
+ subnetID := s.ID
+
+ // Get subnet
+ t.Log("Getting subnet")
+ s, err = subnets.Get(Client, subnetID).Extract()
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, s.ID, subnetID)
+
+ // Update subnet
+ t.Log("Update subnet")
+ s, err = subnets.Update(Client, subnetID, subnets.UpdateOpts{Name: "new_subnet_name"}).Extract()
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, s.Name, "new_subnet_name")
+
+ // Delete subnet
+ t.Log("Delete subnet")
+ res := subnets.Delete(Client, subnetID)
+ th.AssertNoErr(t, res.Err)
+}
+
+func TestBatchCreate(t *testing.T) {
+ // todo
+}
diff --git a/_site/acceptance/openstack/objectstorage/v1/accounts_test.go b/_site/acceptance/openstack/objectstorage/v1/accounts_test.go
new file mode 100644
index 0000000..6768927
--- /dev/null
+++ b/_site/acceptance/openstack/objectstorage/v1/accounts_test.go
@@ -0,0 +1,58 @@
+// +build acceptance
+
+package v1
+
+import (
+ "strings"
+ "testing"
+
+ "github.com/rackspace/gophercloud/openstack/objectstorage/v1/accounts"
+)
+
+func TestAccounts(t *testing.T) {
+ // Create a provider client for making the HTTP requests.
+ // See common.go in this directory for more information.
+ client, err := newClient()
+ if err != nil {
+ t.Error(err)
+ return
+ }
+
+ // Update an account's metadata.
+ err = accounts.Update(client, accounts.UpdateOpts{
+ Metadata: metadata,
+ })
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ // Defer the deletion of the metadata set above.
+ defer func() {
+ tempMap := make(map[string]string)
+ for k := range metadata {
+ tempMap[k] = ""
+ }
+ err = accounts.Update(client, accounts.UpdateOpts{
+ Metadata: tempMap,
+ })
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ }()
+
+ // Retrieve account metadata.
+ gr, err := accounts.Get(client, accounts.GetOpts{})
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ // Extract the custom metadata from the 'Get' response.
+ am := accounts.ExtractMetadata(gr)
+ for k := range metadata {
+ if am[k] != metadata[strings.Title(k)] {
+ t.Errorf("Expected custom metadata with key: %s", k)
+ return
+ }
+ }
+}
diff --git a/_site/acceptance/openstack/objectstorage/v1/common.go b/_site/acceptance/openstack/objectstorage/v1/common.go
new file mode 100644
index 0000000..08065a4
--- /dev/null
+++ b/_site/acceptance/openstack/objectstorage/v1/common.go
@@ -0,0 +1,28 @@
+// +build acceptance
+
+package v1
+
+import (
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/openstack"
+ "github.com/rackspace/gophercloud/openstack/utils"
+ "os"
+)
+
+var metadata = map[string]string{"gopher": "cloud"}
+
+func newClient() (*gophercloud.ServiceClient, error) {
+ ao, err := utils.AuthOptions()
+ if err != nil {
+ return nil, err
+ }
+
+ client, err := openstack.AuthenticatedClient(ao)
+ if err != nil {
+ return nil, err
+ }
+
+ return openstack.NewStorageV1(client, gophercloud.EndpointOpts{
+ Region: os.Getenv("OS_REGION_NAME"),
+ })
+}
diff --git a/_site/acceptance/openstack/objectstorage/v1/containers_test.go b/_site/acceptance/openstack/objectstorage/v1/containers_test.go
new file mode 100644
index 0000000..b541307
--- /dev/null
+++ b/_site/acceptance/openstack/objectstorage/v1/containers_test.go
@@ -0,0 +1,108 @@
+// +build acceptance
+
+package v1
+
+import (
+ "strings"
+ "testing"
+
+ "github.com/rackspace/gophercloud/acceptance/tools"
+ "github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// numContainers is the number of containers to create for testing.
+var numContainers = 2
+
+func TestContainers(t *testing.T) {
+ // Create a new client to execute the HTTP requests. See common.go for newClient body.
+ client, err := newClient()
+ if err != nil {
+ t.Error(err)
+ }
+
+ // Create a slice of random container names.
+ cNames := make([]string, numContainers)
+ for i := 0; i < numContainers; i++ {
+ cNames[i] = tools.RandomString("gophercloud-test-container-", 8)
+ }
+
+ // Create numContainers containers.
+ for i := 0; i < len(cNames); i++ {
+ _, err := containers.Create(client, cNames[i], nil).ExtractHeaders()
+ if err != nil {
+ t.Error(err)
+ }
+ }
+ // Delete the numContainers containers after function completion.
+ defer func() {
+ for i := 0; i < len(cNames); i++ {
+ _, err = containers.Delete(client, cNames[i]).ExtractHeaders()
+ if err != nil {
+ t.Error(err)
+ }
+ }
+ }()
+
+ // List the numContainer names that were just created. To just list those,
+ // the 'prefix' parameter is used.
+ err = containers.List(client, &containers.ListOpts{Full: true, Prefix: "gophercloud-test-container-"}).EachPage(func(page pagination.Page) (bool, error) {
+ containerList, err := containers.ExtractInfo(page)
+ if err != nil {
+ t.Error(err)
+ }
+ for _, n := range containerList {
+ t.Logf("Container: Name [%s] Count [%d] Bytes [%d]",
+ n.Name, n.Count, n.Bytes)
+ }
+
+ return true, nil
+ })
+ if err != nil {
+ t.Error(err)
+ }
+
+ // List the info for the numContainer containers that were created.
+ err = containers.List(client, &containers.ListOpts{Full: false, Prefix: "gophercloud-test-container-"}).EachPage(func(page pagination.Page) (bool, error) {
+ containerList, err := containers.ExtractNames(page)
+ if err != nil {
+ return false, err
+ }
+ for _, n := range containerList {
+ t.Logf("Container: Name [%s]", n)
+ }
+
+ return true, nil
+ })
+ if err != nil {
+ t.Error(err)
+ }
+
+ // Update one of the numContainer container metadata.
+ _, err = containers.Update(client, cNames[0], &containers.UpdateOpts{Metadata: metadata}).ExtractHeaders()
+ if err != nil {
+ t.Error(err)
+ }
+ // After the tests are done, delete the metadata that was set.
+ defer func() {
+ tempMap := make(map[string]string)
+ for k := range metadata {
+ tempMap[k] = ""
+ }
+ _, err = containers.Update(client, cNames[0], &containers.UpdateOpts{Metadata: tempMap}).ExtractHeaders()
+ if err != nil {
+ t.Error(err)
+ }
+ }()
+
+ // Retrieve a container's metadata.
+ cm, err := containers.Get(client, cNames[0]).ExtractMetadata()
+ if err != nil {
+ t.Error(err)
+ }
+ for k := range metadata {
+ if cm[k] != metadata[strings.Title(k)] {
+ t.Errorf("Expected custom metadata with key: %s", k)
+ }
+ }
+}
diff --git a/_site/acceptance/openstack/objectstorage/v1/objects_test.go b/_site/acceptance/openstack/objectstorage/v1/objects_test.go
new file mode 100644
index 0000000..5a63a4c
--- /dev/null
+++ b/_site/acceptance/openstack/objectstorage/v1/objects_test.go
@@ -0,0 +1,162 @@
+// +build acceptance
+
+package v1
+
+import (
+ "bytes"
+ "strings"
+ "testing"
+
+ "github.com/rackspace/gophercloud/acceptance/tools"
+ "github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers"
+ "github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// numObjects is the number of objects to create for testing.
+var numObjects = 2
+
+func TestObjects(t *testing.T) {
+ // Create a provider client for executing the HTTP request.
+ // See common.go for more information.
+ client, err := newClient()
+ if err != nil {
+ t.Error(err)
+ return
+ }
+
+ // Make a slice of length numObjects to hold the random object names.
+ oNames := make([]string, numObjects)
+ for i := 0; i < len(oNames); i++ {
+ oNames[i] = tools.RandomString("test-object-", 8)
+ }
+
+ // Create a container to hold the test objects.
+ cName := tools.RandomString("test-container-", 8)
+ _, err = containers.Create(client, cName, nil).ExtractHeaders()
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ // Defer deletion of the container until after testing.
+ defer func() {
+ _, err = containers.Delete(client, cName).ExtractHeaders()
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ }()
+
+ // Create a slice of buffers to hold the test object content.
+ oContents := make([]*bytes.Buffer, numObjects)
+ for i := 0; i < numObjects; i++ {
+ oContents[i] = bytes.NewBuffer([]byte(tools.RandomString("", 10)))
+ _, err = objects.Create(client, cName, oNames[i], oContents[i], nil).ExtractHeaders()
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ }
+ // Delete the objects after testing.
+ defer func() {
+ for i := 0; i < numObjects; i++ {
+ _, err = objects.Delete(client, cName, oNames[i], nil).ExtractHeaders()
+ }
+ }()
+
+ ons := make([]string, 0, len(oNames))
+ err = objects.List(client, cName, &objects.ListOpts{Full: false, Prefix: "test-object-"}).EachPage(func(page pagination.Page) (bool, error) {
+ names, err := objects.ExtractNames(page)
+ if err != nil {
+ return false, err
+ }
+ ons = append(ons, names...)
+
+ return true, nil
+ })
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ if len(ons) != len(oNames) {
+ t.Errorf("Expected %d names and got %d", len(oNames), len(ons))
+ return
+ }
+
+ ois := make([]objects.Object, 0, len(oNames))
+ err = objects.List(client, cName, &objects.ListOpts{Full: true, Prefix: "test-object-"}).EachPage(func(page pagination.Page) (bool, error) {
+ info, err := objects.ExtractInfo(page)
+ if err != nil {
+ return false, nil
+ }
+
+ ois = append(ois, info...)
+
+ return true, nil
+ })
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ if len(ois) != len(oNames) {
+ t.Errorf("Expected %d containers and got %d", len(oNames), len(ois))
+ return
+ }
+
+ // Copy the contents of one object to another.
+ _, err = objects.Copy(client, cName, oNames[0], &objects.CopyOpts{Destination: cName + "/" + oNames[1]}).ExtractHeaders()
+ if err != nil {
+ t.Error(err)
+ return
+ }
+
+ // Download one of the objects that was created above.
+ o1Content, err := objects.Download(client, cName, oNames[0], nil).ExtractContent()
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ // Download the another object that was create above.
+ o2Content, err := objects.Download(client, cName, oNames[1], nil).ExtractContent()
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ // Compare the two object's contents to test that the copy worked.
+ if string(o2Content) != string(o1Content) {
+ t.Errorf("Copy failed. Expected\n%s\nand got\n%s", string(o1Content), string(o2Content))
+ return
+ }
+
+ // Update an object's metadata.
+ _, err = objects.Update(client, cName, oNames[0], &objects.UpdateOpts{Metadata: metadata}).ExtractHeaders()
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ // Delete the object's metadata after testing.
+ defer func() {
+ tempMap := make(map[string]string)
+ for k := range metadata {
+ tempMap[k] = ""
+ }
+ _, err = objects.Update(client, cName, oNames[0], &objects.UpdateOpts{Metadata: tempMap}).ExtractHeaders()
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ }()
+
+ // Retrieve an object's metadata.
+ om, err := objects.Get(client, cName, oNames[0], nil).ExtractMetadata()
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ for k := range metadata {
+ if om[k] != metadata[strings.Title(k)] {
+ t.Errorf("Expected custom metadata with key: %s", k)
+ return
+ }
+ }
+}
diff --git a/_site/acceptance/openstack/pkg.go b/_site/acceptance/openstack/pkg.go
new file mode 100644
index 0000000..3a8ecdb
--- /dev/null
+++ b/_site/acceptance/openstack/pkg.go
@@ -0,0 +1,4 @@
+// +build acceptance
+
+package openstack
+
diff --git a/_site/acceptance/rackspace/client_test.go b/_site/acceptance/rackspace/client_test.go
new file mode 100644
index 0000000..e68aef8
--- /dev/null
+++ b/_site/acceptance/rackspace/client_test.go
@@ -0,0 +1,29 @@
+// +build acceptance
+
+package rackspace
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud/openstack/utils"
+ "github.com/rackspace/gophercloud/rackspace"
+)
+
+func TestAuthenticatedClient(t *testing.T) {
+ // Obtain credentials from the environment.
+ ao, err := utils.AuthOptions()
+ if err != nil {
+ t.Fatalf("Unable to acquire credentials: %v", err)
+ }
+
+ client, err := rackspace.AuthenticatedClient(ao)
+ if err != nil {
+ t.Fatalf("Unable to authenticate: %v", err)
+ }
+
+ if client.TokenID == "" {
+ t.Errorf("No token ID assigned to the client")
+ }
+
+ t.Logf("Client successfully acquired a token: %v", client.TokenID)
+}
diff --git a/_site/acceptance/rackspace/identity/v2/extension_test.go b/_site/acceptance/rackspace/identity/v2/extension_test.go
new file mode 100644
index 0000000..a50e015
--- /dev/null
+++ b/_site/acceptance/rackspace/identity/v2/extension_test.go
@@ -0,0 +1,54 @@
+// +build acceptance
+
+package v2
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud/pagination"
+ extensions2 "github.com/rackspace/gophercloud/rackspace/identity/v2/extensions"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestExtensions(t *testing.T) {
+ service := authenticatedClient(t)
+
+ t.Logf("Extensions available on this identity endpoint:")
+ count := 0
+ var chosen string
+ err := extensions2.List(service).EachPage(func(page pagination.Page) (bool, error) {
+ t.Logf("--- Page %02d ---", count)
+
+ extensions, err := extensions2.ExtractExtensions(page)
+ th.AssertNoErr(t, err)
+
+ for i, ext := range extensions {
+ if chosen == "" {
+ chosen = ext.Alias
+ }
+
+ t.Logf("[%02d] name=[%s] namespace=[%s]", i, ext.Name, ext.Namespace)
+ t.Logf(" alias=[%s] updated=[%s]", ext.Alias, ext.Updated)
+ t.Logf(" description=[%s]", ext.Description)
+ }
+
+ count++
+ return true, nil
+ })
+ th.AssertNoErr(t, err)
+
+ if chosen == "" {
+ t.Logf("No extensions found.")
+ return
+ }
+
+ ext, err := extensions2.Get(service, chosen).Extract()
+ th.AssertNoErr(t, err)
+
+ t.Logf("Detail for extension [%s]:", chosen)
+ t.Logf(" name=[%s]", ext.Name)
+ t.Logf(" namespace=[%s]", ext.Namespace)
+ t.Logf(" alias=[%s]", ext.Alias)
+ t.Logf(" updated=[%s]", ext.Updated)
+ t.Logf(" description=[%s]", ext.Description)
+}
diff --git a/_site/acceptance/rackspace/identity/v2/identity_test.go b/_site/acceptance/rackspace/identity/v2/identity_test.go
new file mode 100644
index 0000000..019a9e6
--- /dev/null
+++ b/_site/acceptance/rackspace/identity/v2/identity_test.go
@@ -0,0 +1,51 @@
+// +build acceptance
+
+package v2
+
+import (
+ "os"
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/rackspace"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func rackspaceAuthOptions(t *testing.T) gophercloud.AuthOptions {
+ // Obtain credentials from the environment.
+ options := gophercloud.AuthOptions{
+ Username: os.Getenv("RS_USERNAME"),
+ APIKey: os.Getenv("RS_APIKEY"),
+ }
+
+ 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_APIKEY.")
+ }
+
+ return options
+}
+
+func createClient(t *testing.T, auth bool) *gophercloud.ServiceClient {
+ ao := rackspaceAuthOptions(t)
+
+ provider, err := rackspace.NewClient(ao.IdentityEndpoint)
+ th.AssertNoErr(t, err)
+
+ if auth {
+ err = rackspace.Authenticate(provider, ao)
+ th.AssertNoErr(t, err)
+ }
+
+ return rackspace.NewIdentityV2(provider)
+}
+
+func unauthenticatedClient(t *testing.T) *gophercloud.ServiceClient {
+ return createClient(t, false)
+}
+
+func authenticatedClient(t *testing.T) *gophercloud.ServiceClient {
+ return createClient(t, true)
+}
diff --git a/_site/acceptance/rackspace/identity/v2/tenant_test.go b/_site/acceptance/rackspace/identity/v2/tenant_test.go
new file mode 100644
index 0000000..6081a49
--- /dev/null
+++ b/_site/acceptance/rackspace/identity/v2/tenant_test.go
@@ -0,0 +1,37 @@
+// +build acceptance
+
+package v2
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud/pagination"
+ rstenants "github.com/rackspace/gophercloud/rackspace/identity/v2/tenants"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestTenants(t *testing.T) {
+ service := authenticatedClient(t)
+
+ t.Logf("Tenants available to the currently issued token:")
+ count := 0
+ err := rstenants.List(service, nil).EachPage(func(page pagination.Page) (bool, error) {
+ t.Logf("--- Page %02d ---", count)
+
+ tenants, err := rstenants.ExtractTenants(page)
+ th.AssertNoErr(t, err)
+
+ for i, tenant := range tenants {
+ t.Logf("[%02d] id=[%s]", i, tenant.ID)
+ t.Logf(" name=[%s] enabled=[%v]", i, tenant.Name, tenant.Enabled)
+ t.Logf(" description=[%s]", tenant.Description)
+ }
+
+ count++
+ return true, nil
+ })
+ th.AssertNoErr(t, err)
+ if count == 0 {
+ t.Errorf("No tenants listed for your current token.")
+ }
+}
diff --git a/_site/acceptance/rackspace/pkg.go b/_site/acceptance/rackspace/pkg.go
new file mode 100644
index 0000000..5d17b32
--- /dev/null
+++ b/_site/acceptance/rackspace/pkg.go
@@ -0,0 +1 @@
+package rackspace
diff --git a/_site/acceptance/tools/tools.go b/_site/acceptance/tools/tools.go
new file mode 100644
index 0000000..4771ebb
--- /dev/null
+++ b/_site/acceptance/tools/tools.go
@@ -0,0 +1,50 @@
+// +build acceptance
+package tools
+
+import (
+ "crypto/rand"
+ "errors"
+ "time"
+)
+
+// ErrTimeout is returned if WaitFor takes longer than 300 second to happen.
+var ErrTimeout = errors.New("Timed out")
+
+// 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)
+}
diff --git a/_site/auth_options.go b/_site/auth_options.go
new file mode 100644
index 0000000..f9e6ee5
--- /dev/null
+++ b/_site/auth_options.go
@@ -0,0 +1,36 @@
+package gophercloud
+
+// AuthOptions lets anyone calling Authenticate() supply the required access credentials.
+// Its fields are the union of those recognized by each identity implementation and provider.
+type AuthOptions struct {
+
+ // IdentityEndpoint specifies the HTTP endpoint offering the Identity API of the appropriate version.
+ // Required by the identity services, but often populated by a provider Client.
+ 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.
+ 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
+}
diff --git a/_site/auth_results.go b/_site/auth_results.go
new file mode 100644
index 0000000..07e0fc7
--- /dev/null
+++ b/_site/auth_results.go
@@ -0,0 +1,16 @@
+package gophercloud
+
+import "time"
+
+// AuthResults encapsulates the raw results from an authentication request. As OpenStack allows
+// extensions to influence the structure returned in ways that Gophercloud cannot predict at
+// compile-time, you should use type-safe accessors to work with the data represented by this type,
+// such as ServiceCatalog() and TokenID().
+type AuthResults interface {
+
+ // Retrieve the authentication token's value from the authentication response.
+ TokenID() (string, error)
+
+ // ExpiresAt retrieves the token's expiration time.
+ ExpiresAt() (time.Time, error)
+}
diff --git a/_site/docs/identity/v2/index.html b/_site/docs/identity/v2/index.html
new file mode 100644
index 0000000..ad5273f
--- /dev/null
+++ b/_site/docs/identity/v2/index.html
@@ -0,0 +1,52 @@
+<h2 id="tokens">Tokens</h2>
+
+<p>A token is an arbitrary bit of text that is used to access resources. Each
+token has a scope that describes which resources are accessible with it. A
+token may be revoked at anytime and is valid for a finite duration.</p>
+
+<h3 id="generate-a-token">Generate a token</h3>
+
+<p>The nature of required and optional auth options will depend on your provider,
+but generally the <code>Username</code> and <code>IdentityEndpoint</code> fields are always
+required. Some providers will insist on a <code>Password</code> instead of an <code>APIKey</code>,
+others will prefer <code>TenantID</code> over <code>TenantName</code> - so it is always worth
+checking before writing your implementation in Go.</p>
+
+<div class="highlight"><pre><code class="language-go" data-lang="go"><span class="kn">import</span> <span class="s">"github.com/rackspace/gophercloud/openstack/identity/v2/tokens"</span>
+
+<span class="nx">opts</span> <span class="o">:=</span> <span class="nx">tokens</span><span class="p">.</span><span class="nx">AuthOptions</span><span class="p">{</span>
+ <span class="nx">IdentityEndpoint</span><span class="p">:</span> <span class="s">"{identityURL}"</span><span class="p">,</span>
+ <span class="nx">Username</span><span class="p">:</span> <span class="s">"{username}"</span><span class="p">,</span>
+ <span class="nx">APIKey</span><span class="p">:</span> <span class="s">"{apiKey}"</span><span class="p">,</span>
+<span class="p">}</span>
+
+<span class="nx">token</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">tokens</span><span class="p">.</span><span class="nx">Create</span><span class="p">(</span><span class="nx">client</span><span class="p">,</span> <span class="nx">opts</span><span class="p">).</span><span class="nx">Extract</span><span class="p">()</span></code></pre></div>
+
+<h2 id="tenants">Tenants</h2>
+
+<p>A tenant is a container used to group or isolate API resources. Depending on
+the provider, a tenant can map to a customer, account, organization, or project.</p>
+
+<h3 id="list-tenants"> List tenants</h3>
+
+<div class="highlight"><pre><code class="language-go" data-lang="go"><span class="kn">import</span> <span class="p">(</span>
+ <span class="s">"github.com/rackspace/gophercloud/pagination"</span>
+ <span class="s">"github.com/rackspace/gophercloud/openstack/identity/v2/tenants"</span>
+<span class="p">)</span>
+
+<span class="c1">// We have the option of filtering the tenant list. If we want the full</span>
+<span class="c1">// collection, leave it as an empty struct</span>
+<span class="nx">opts</span> <span class="o">:=</span> <span class="nx">tenants</span><span class="p">.</span><span class="nx">ListOpts</span><span class="p">{</span><span class="nx">Limit</span><span class="p">:</span> <span class="mi">10</span><span class="p">}</span>
+
+<span class="c1">// Retrieve a pager (i.e. a paginated collection)</span>
+<span class="nx">pager</span> <span class="o">:=</span> <span class="nx">tenants</span><span class="p">.</span><span class="nx">List</span><span class="p">(</span><span class="nx">client</span><span class="p">,</span> <span class="nx">opts</span><span class="p">)</span>
+
+<span class="c1">// Define an anonymous function to be executed on each page's iteration</span>
+<span class="nx">err</span> <span class="o">:=</span> <span class="nx">pager</span><span class="p">.</span><span class="nx">EachPage</span><span class="p">(</span><span class="kd">func</span><span class="p">(</span><span class="nx">page</span> <span class="nx">pagination</span><span class="p">.</span><span class="nx">Page</span><span class="p">)</span> <span class="p">(</span><span class="kt">bool</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span>
+ <span class="nx">tenantList</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">tenants</span><span class="p">.</span><span class="nx">ExtractTenants</span><span class="p">(</span><span class="nx">page</span><span class="p">)</span>
+
+ <span class="k">for</span> <span class="nx">_</span><span class="p">,</span> <span class="nx">t</span> <span class="o">:=</span> <span class="k">range</span> <span class="nx">tenantList</span> <span class="p">{</span>
+ <span class="c1">// "t" will be a tenants.Tenant</span>
+ <span class="p">}</span>
+<span class="p">})</span></code></pre></div>
+
diff --git a/_site/docs/identity/v3/index.html b/_site/docs/identity/v3/index.html
new file mode 100644
index 0000000..f86dccb
--- /dev/null
+++ b/_site/docs/identity/v3/index.html
@@ -0,0 +1,31 @@
+<h2 id="tokens">Tokens</h2>
+
+<h3 id="generate-a-token">Generate a token</h3>
+
+<h3 id="get-a-token">Get a token</h3>
+
+<h3 id="validate-token">Validate token</h3>
+
+<h3 id="revoke-token">Revoke token</h3>
+
+<h2 id="endpoints">Endpoints</h2>
+
+<h3 id="create-an-endpoint"> Create an endpoint</h3>
+
+<h3 id="list-endpoints">List endpoints</h3>
+
+<h3 id="update-endpoint">Update endpoint</h3>
+
+<h3 id="delete-endpoint">Delete endpoint</h3>
+
+<h2 id="services">Services</h2>
+
+<h3 id="create-a-service">Create a service</h3>
+
+<h3 id="list-services">List services</h3>
+
+<h3 id="get-a-service">Get a service</h3>
+
+<h3 id="update-service">Update service</h3>
+
+<h3 id="delete-service"> Delete service</h3>
diff --git a/_site/endpoint_search.go b/_site/endpoint_search.go
new file mode 100644
index 0000000..828bcfd
--- /dev/null
+++ b/_site/endpoint_search.go
@@ -0,0 +1,63 @@
+package gophercloud
+
+import "errors"
+
+var (
+ // ErrServiceNotFound is returned when no service matches the EndpointOpts.
+ ErrServiceNotFound = errors.New("No suitable service could be found in the service catalog.")
+
+ // ErrEndpointNotFound is returned when no available endpoints match the provided EndpointOpts.
+ ErrEndpointNotFound = errors.New("No suitable endpoint could be found in the service catalog.")
+)
+
+// Availability describes the accessibility of a specific service endpoint.
+// Identity v2 lists these as different kinds of URLs ("adminURL", "internalURL", and "publicURL"), while
+// v3 lists them as "Interfaces".
+type Availability string
+
+const (
+ // AvailabilityAdmin makes an endpoint only available to administrators.
+ AvailabilityAdmin Availability = "admin"
+
+ // AvailabilityPublic makes an endpoint available to everyone.
+ AvailabilityPublic Availability = "public"
+
+ // AvailabilityInternal makes an endpoint only available within the cluster.
+ AvailabilityInternal Availability = "internal"
+)
+
+// EndpointOpts contains options for finding an endpoint for an Openstack client.
+type EndpointOpts struct {
+
+ // Type is the service type for the client (e.g., "compute", "object-store").
+ // Type is a required field.
+ Type string
+
+ // Name is the service name for the client (e.g., "nova").
+ // Name is not a required field, but it is used if present.
+ // Services can have the same Type but a different Name, which is one example of when both Type and Name are needed.
+ Name string
+
+ // Region is the region in which the service resides.
+ // Region must be specified for services that span multiple regions.
+ Region string
+
+ // Availability is the visibility of the endpoint to be returned: AvailabilityPublic, AvailabilityInternal, or AvailabilityAdmin.
+ // Availability is not required, and defaults to AvailabilityPublic.
+ // Not all providers or services offer all Availability options.
+ Availability Availability
+}
+
+// EndpointLocator is a function that describes how to locate a single endpoint from a service catalog for a specific ProviderClient.
+// It should be set during ProviderClient authentication and used to discover related ServiceClients.
+type EndpointLocator func(EndpointOpts) (string, error)
+
+// ApplyDefaults sets EndpointOpts fields if not already set. 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
+ }
+}
diff --git a/_site/endpoint_search_test.go b/_site/endpoint_search_test.go
new file mode 100644
index 0000000..3457453
--- /dev/null
+++ b/_site/endpoint_search_test.go
@@ -0,0 +1,19 @@
+package gophercloud
+
+import (
+ "testing"
+
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestApplyDefaultsToEndpointOpts(t *testing.T) {
+ eo := EndpointOpts{Availability: AvailabilityPublic}
+ eo.ApplyDefaults("compute")
+ expected := EndpointOpts{Availability: AvailabilityPublic, Type: "compute"}
+ th.CheckDeepEquals(t, expected, eo)
+
+ eo = EndpointOpts{Type: "compute"}
+ eo.ApplyDefaults("object-store")
+ expected = EndpointOpts{Availability: AvailabilityPublic, Type: "compute"}
+ th.CheckDeepEquals(t, expected, eo)
+}
diff --git a/_site/openstack/blockstorage/v1/apiversions/doc.go b/_site/openstack/blockstorage/v1/apiversions/doc.go
new file mode 100644
index 0000000..c3c486f
--- /dev/null
+++ b/_site/openstack/blockstorage/v1/apiversions/doc.go
@@ -0,0 +1,3 @@
+// Package apiversions provides information and interaction with the different
+// API versions for the OpenStack Cinder service.
+package apiversions
diff --git a/_site/openstack/blockstorage/v1/apiversions/requests.go b/_site/openstack/blockstorage/v1/apiversions/requests.go
new file mode 100644
index 0000000..79a939c
--- /dev/null
+++ b/_site/openstack/blockstorage/v1/apiversions/requests.go
@@ -0,0 +1,28 @@
+package apiversions
+
+import (
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+
+ "github.com/racker/perigee"
+)
+
+// 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.LastHTTPResponse) 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
+ _, err := perigee.Request("GET", getURL(client, v), perigee.Options{
+ MoreHeaders: client.Provider.AuthenticatedHeaders(),
+ OkCodes: []int{200},
+ Results: &res.Resp,
+ })
+ res.Err = err
+ return res
+}
diff --git a/_site/openstack/blockstorage/v1/apiversions/requests_test.go b/_site/openstack/blockstorage/v1/apiversions/requests_test.go
new file mode 100644
index 0000000..c135722
--- /dev/null
+++ b/_site/openstack/blockstorage/v1/apiversions/requests_test.go
@@ -0,0 +1,156 @@
+package apiversions
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+const TokenID = "123"
+
+func ServiceClient() *gophercloud.ServiceClient {
+ return &gophercloud.ServiceClient{
+ Provider: &gophercloud.ProviderClient{
+ TokenID: TokenID,
+ },
+ Endpoint: th.Endpoint(),
+ }
+}
+
+func TestListVersions(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", TokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, `{
+ "versions": [
+ {
+ "status": "CURRENT",
+ "updated": "2012-01-04T11:33:21Z",
+ "id": "v1.0",
+ "links": [
+ {
+ "href": "http://23.253.228.211:8776/v1/",
+ "rel": "self"
+ }
+ ]
+ },
+ {
+ "status": "CURRENT",
+ "updated": "2012-11-21T11:33:21Z",
+ "id": "v2.0",
+ "links": [
+ {
+ "href": "http://23.253.228.211:8776/v2/",
+ "rel": "self"
+ }
+ ]
+ }
+ ]
+ }`)
+ })
+
+ count := 0
+
+ List(ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractAPIVersions(page)
+ if err != nil {
+ t.Errorf("Failed to extract API versions: %v", err)
+ return false, err
+ }
+
+ expected := []APIVersion{
+ APIVersion{
+ ID: "v1.0",
+ Status: "CURRENT",
+ Updated: "2012-01-04T11:33:21Z",
+ },
+ APIVersion{
+ ID: "v2.0",
+ Status: "CURRENT",
+ Updated: "2012-11-21T11:33:21Z",
+ },
+ }
+
+ th.AssertDeepEquals(t, expected, actual)
+
+ return true, nil
+ })
+
+ if count != 1 {
+ t.Errorf("Expected 1 page, got %d", count)
+ }
+}
+
+func TestAPIInfo(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v1/", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", TokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, `{
+ "version": {
+ "status": "CURRENT",
+ "updated": "2012-01-04T11:33:21Z",
+ "media-types": [
+ {
+ "base": "application/xml",
+ "type": "application/vnd.openstack.volume+xml;version=1"
+ },
+ {
+ "base": "application/json",
+ "type": "application/vnd.openstack.volume+json;version=1"
+ }
+ ],
+ "id": "v1.0",
+ "links": [
+ {
+ "href": "http://23.253.228.211:8776/v1/",
+ "rel": "self"
+ },
+ {
+ "href": "http://jorgew.github.com/block-storage-api/content/os-block-storage-1.0.pdf",
+ "type": "application/pdf",
+ "rel": "describedby"
+ },
+ {
+ "href": "http://docs.rackspacecloud.com/servers/api/v1.1/application.wadl",
+ "type": "application/vnd.sun.wadl+xml",
+ "rel": "describedby"
+ }
+ ]
+ }
+ }`)
+ })
+
+ actual, err := Get(ServiceClient(), "v1").Extract()
+ if err != nil {
+ t.Errorf("Failed to extract version: %v", err)
+ }
+
+ expected := APIVersion{
+ ID: "v1.0",
+ Status: "CURRENT",
+ Updated: "2012-01-04T11:33:21Z",
+ }
+
+ th.AssertEquals(t, actual.ID, expected.ID)
+ th.AssertEquals(t, actual.Status, expected.Status)
+ th.AssertEquals(t, actual.Updated, expected.Updated)
+}
diff --git a/_site/openstack/blockstorage/v1/apiversions/results.go b/_site/openstack/blockstorage/v1/apiversions/results.go
new file mode 100644
index 0000000..ea2f7f5
--- /dev/null
+++ b/_site/openstack/blockstorage/v1/apiversions/results.go
@@ -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.CommonResult
+}
+
+// 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.Resp, &resp)
+
+ return resp.Version, err
+}
diff --git a/_site/openstack/blockstorage/v1/apiversions/urls.go b/_site/openstack/blockstorage/v1/apiversions/urls.go
new file mode 100644
index 0000000..56f8260
--- /dev/null
+++ b/_site/openstack/blockstorage/v1/apiversions/urls.go
@@ -0,0 +1,15 @@
+package apiversions
+
+import (
+ "strings"
+
+ "github.com/rackspace/gophercloud"
+)
+
+func getURL(c *gophercloud.ServiceClient, version string) string {
+ return c.ServiceURL(strings.TrimRight(version, "/") + "/")
+}
+
+func listURL(c *gophercloud.ServiceClient) string {
+ return c.ServiceURL("")
+}
diff --git a/_site/openstack/blockstorage/v1/apiversions/urls_test.go b/_site/openstack/blockstorage/v1/apiversions/urls_test.go
new file mode 100644
index 0000000..37e9142
--- /dev/null
+++ b/_site/openstack/blockstorage/v1/apiversions/urls_test.go
@@ -0,0 +1,26 @@
+package apiversions
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+const endpoint = "http://localhost:57909/"
+
+func endpointClient() *gophercloud.ServiceClient {
+ return &gophercloud.ServiceClient{Endpoint: endpoint}
+}
+
+func TestGetURL(t *testing.T) {
+ actual := getURL(endpointClient(), "v1")
+ expected := endpoint + "v1/"
+ th.AssertEquals(t, expected, actual)
+}
+
+func TestListURL(t *testing.T) {
+ actual := listURL(endpointClient())
+ expected := endpoint
+ th.AssertEquals(t, expected, actual)
+}
diff --git a/_site/openstack/blockstorage/v1/snapshots/requests.go b/_site/openstack/blockstorage/v1/snapshots/requests.go
new file mode 100644
index 0000000..7fac925
--- /dev/null
+++ b/_site/openstack/blockstorage/v1/snapshots/requests.go
@@ -0,0 +1,187 @@
+package snapshots
+
+import (
+ "fmt"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+
+ "github.com/racker/perigee"
+)
+
+// 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 = perigee.Request("POST", createURL(client), perigee.Options{
+ MoreHeaders: client.Provider.AuthenticatedHeaders(),
+ OkCodes: []int{200, 201},
+ ReqBody: &reqBody,
+ Results: &res.Resp,
+ })
+ return res
+}
+
+// Delete will delete the existing Snapshot with the provided ID.
+func Delete(client *gophercloud.ServiceClient, id string) error {
+ _, err := perigee.Request("DELETE", deleteURL(client, id), perigee.Options{
+ MoreHeaders: client.Provider.AuthenticatedHeaders(),
+ OkCodes: []int{202, 204},
+ })
+ return err
+}
+
+// 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 = perigee.Request("GET", getURL(client, id), perigee.Options{
+ Results: &res.Resp,
+ MoreHeaders: client.Provider.AuthenticatedHeaders(),
+ OkCodes: []int{200},
+ })
+ 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.LastHTTPResponse) 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 = perigee.Request("PUT", updateMetadataURL(client, id), perigee.Options{
+ MoreHeaders: client.Provider.AuthenticatedHeaders(),
+ OkCodes: []int{200},
+ ReqBody: &reqBody,
+ Results: &res.Resp,
+ })
+ return res
+}
diff --git a/_site/openstack/blockstorage/v1/snapshots/requests_test.go b/_site/openstack/blockstorage/v1/snapshots/requests_test.go
new file mode 100644
index 0000000..ddfa81b
--- /dev/null
+++ b/_site/openstack/blockstorage/v1/snapshots/requests_test.go
@@ -0,0 +1,201 @@
+package snapshots
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+const TokenID = "123"
+
+func ServiceClient() *gophercloud.ServiceClient {
+ return &gophercloud.ServiceClient{
+ Provider: &gophercloud.ProviderClient{
+ TokenID: TokenID,
+ },
+ Endpoint: th.Endpoint(),
+ }
+}
+
+func TestList(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/snapshots", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", 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"
+ }
+ ]
+ }
+ `)
+ })
+
+ client := ServiceClient()
+ count := 0
+
+ List(client, &ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractSnapshots(page)
+ if err != nil {
+ t.Errorf("Failed to extract snapshots: %v", err)
+ return false, err
+ }
+
+ expected := []Snapshot{
+ Snapshot{
+ ID: "289da7f8-6440-407c-9fb4-7db01ec49164",
+ Name: "snapshot-001",
+ },
+ Snapshot{
+ ID: "96c3bda7-c82a-4f50-be73-ca7621794835",
+ Name: "snapshot-002",
+ },
+ }
+
+ th.CheckDeepEquals(t, expected, actual)
+
+ return true, nil
+ })
+
+ if count != 1 {
+ t.Errorf("Expected 1 page, got %d", count)
+ }
+}
+
+func TestGet(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ 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", 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"
+ }
+}
+ `)
+ })
+
+ v, err := Get(ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22").Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, v.Name, "snapshot-001")
+ th.AssertEquals(t, v.ID, "d32019d3-bc6e-4319-9c1d-6722fc136a22")
+}
+
+func TestCreate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/snapshots", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "POST")
+ th.TestHeader(t, r, "X-Auth-Token", 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"
+ }
+}
+ `)
+ })
+
+ options := &CreateOpts{VolumeID: "1234", Name: "snapshot-001"}
+ n, err := Create(ServiceClient(), options).Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, n.VolumeID, "1234")
+ th.AssertEquals(t, n.Name, "snapshot-001")
+ th.AssertEquals(t, n.ID, "d32019d3-bc6e-4319-9c1d-6722fc136a22")
+}
+
+func TestUpdateMetadata(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ 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", TokenID)
+ th.TestHeader(t, r, "Content-Type", "application/json")
+ th.TestJSONRequest(t, r, `
+ {
+ "metadata": {
+ "key": "v1"
+ }
+ }
+ `)
+
+ fmt.Fprintf(w, `
+ {
+ "metadata": {
+ "key": "v1"
+ }
+ }
+ `)
+ })
+
+ expected := map[string]interface{}{"key": "v1"}
+
+ options := &UpdateMetadataOpts{
+ Metadata: map[string]interface{}{
+ "key": "v1",
+ },
+ }
+ actual, err := UpdateMetadata(ServiceClient(), "123", options).ExtractMetadata()
+
+ th.AssertNoErr(t, err)
+ th.AssertDeepEquals(t, actual, expected)
+}
+
+func TestDelete(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ 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", TokenID)
+ w.WriteHeader(http.StatusNoContent)
+ })
+
+ err := Delete(ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22")
+ th.AssertNoErr(t, err)
+}
diff --git a/_site/openstack/blockstorage/v1/snapshots/results.go b/_site/openstack/blockstorage/v1/snapshots/results.go
new file mode 100644
index 0000000..dc94a32
--- /dev/null
+++ b/_site/openstack/blockstorage/v1/snapshots/results.go
@@ -0,0 +1,107 @@
+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
+}
+
+// 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.Resp["metadata"].(map[string]interface{})
+
+ return m, nil
+}
+
+type commonResult struct {
+ gophercloud.CommonResult
+}
+
+// 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.Resp, &res)
+
+ return res.Snapshot, err
+}
diff --git a/_site/openstack/blockstorage/v1/snapshots/urls.go b/_site/openstack/blockstorage/v1/snapshots/urls.go
new file mode 100644
index 0000000..4d635e8
--- /dev/null
+++ b/_site/openstack/blockstorage/v1/snapshots/urls.go
@@ -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)
+}
diff --git a/_site/openstack/blockstorage/v1/snapshots/urls_test.go b/_site/openstack/blockstorage/v1/snapshots/urls_test.go
new file mode 100644
index 0000000..feacf7f
--- /dev/null
+++ b/_site/openstack/blockstorage/v1/snapshots/urls_test.go
@@ -0,0 +1,50 @@
+package snapshots
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+const endpoint = "http://localhost:57909"
+
+func endpointClient() *gophercloud.ServiceClient {
+ return &gophercloud.ServiceClient{Endpoint: endpoint}
+}
+
+func TestCreateURL(t *testing.T) {
+ actual := createURL(endpointClient())
+ expected := endpoint + "snapshots"
+ th.AssertEquals(t, expected, actual)
+}
+
+func TestDeleteURL(t *testing.T) {
+ actual := deleteURL(endpointClient(), "foo")
+ expected := endpoint + "snapshots/foo"
+ th.AssertEquals(t, expected, actual)
+}
+
+func TestGetURL(t *testing.T) {
+ actual := getURL(endpointClient(), "foo")
+ expected := endpoint + "snapshots/foo"
+ th.AssertEquals(t, expected, actual)
+}
+
+func TestListURL(t *testing.T) {
+ actual := listURL(endpointClient())
+ expected := endpoint + "snapshots"
+ th.AssertEquals(t, expected, actual)
+}
+
+func TestMetadataURL(t *testing.T) {
+ actual := metadataURL(endpointClient(), "foo")
+ expected := endpoint + "snapshots/foo/metadata"
+ th.AssertEquals(t, expected, actual)
+}
+
+func TestUpdateMetadataURL(t *testing.T) {
+ actual := updateMetadataURL(endpointClient(), "foo")
+ expected := endpoint + "snapshots/foo/metadata"
+ th.AssertEquals(t, expected, actual)
+}
diff --git a/_site/openstack/blockstorage/v1/snapshots/util.go b/_site/openstack/blockstorage/v1/snapshots/util.go
new file mode 100644
index 0000000..64cdc60
--- /dev/null
+++ b/_site/openstack/blockstorage/v1/snapshots/util.go
@@ -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
+ })
+}
diff --git a/_site/openstack/blockstorage/v1/snapshots/util_test.go b/_site/openstack/blockstorage/v1/snapshots/util_test.go
new file mode 100644
index 0000000..46b452e
--- /dev/null
+++ b/_site/openstack/blockstorage/v1/snapshots/util_test.go
@@ -0,0 +1,37 @@
+package snapshots
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+ "time"
+
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestWaitForStatus(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/snapshots/1234", func(w http.ResponseWriter, r *http.Request) {
+ time.Sleep(2 * time.Second)
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprintf(w, `
+ {
+ "snapshot": {
+ "display_name": "snapshot-001",
+ "id": "1234",
+ "status":"available"
+ }
+ }`)
+ })
+
+ err := WaitForStatus(ServiceClient(), "1234", "available", 0)
+ if err == nil {
+ t.Errorf("Expected error: 'Time Out in WaitFor'")
+ }
+
+ err = WaitForStatus(ServiceClient(), "1234", "available", 3)
+ th.CheckNoErr(t, err)
+}
diff --git a/_site/openstack/blockstorage/v1/volumes/requests.go b/_site/openstack/blockstorage/v1/volumes/requests.go
new file mode 100644
index 0000000..042a33e
--- /dev/null
+++ b/_site/openstack/blockstorage/v1/volumes/requests.go
@@ -0,0 +1,216 @@
+package volumes
+
+import (
+ "fmt"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+
+ "github.com/racker/perigee"
+)
+
+// 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 = perigee.Request("POST", createURL(client), perigee.Options{
+ MoreHeaders: client.Provider.AuthenticatedHeaders(),
+ ReqBody: &reqBody,
+ Results: &res.Resp,
+ OkCodes: []int{200, 201},
+ })
+ return res
+}
+
+// Delete will delete the existing Volume with the provided ID.
+func Delete(client *gophercloud.ServiceClient, id string) error {
+ _, err := perigee.Request("DELETE", deleteURL(client, id), perigee.Options{
+ MoreHeaders: client.Provider.AuthenticatedHeaders(),
+ OkCodes: []int{202, 204},
+ })
+ return err
+}
+
+// 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 = perigee.Request("GET", getURL(client, id), perigee.Options{
+ Results: &res.Resp,
+ MoreHeaders: client.Provider.AuthenticatedHeaders(),
+ OkCodes: []int{200},
+ })
+ 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.LastHTTPResponse) 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 = perigee.Request("PUT", updateURL(client, id), perigee.Options{
+ MoreHeaders: client.Provider.AuthenticatedHeaders(),
+ OkCodes: []int{200},
+ ReqBody: &reqBody,
+ Results: &res.Resp,
+ })
+ return res
+}
diff --git a/_site/openstack/blockstorage/v1/volumes/requests_test.go b/_site/openstack/blockstorage/v1/volumes/requests_test.go
new file mode 100644
index 0000000..7cd37d5
--- /dev/null
+++ b/_site/openstack/blockstorage/v1/volumes/requests_test.go
@@ -0,0 +1,184 @@
+package volumes
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+const TokenID = "123"
+
+func ServiceClient() *gophercloud.ServiceClient {
+ return &gophercloud.ServiceClient{
+ Provider: &gophercloud.ProviderClient{
+ TokenID: TokenID,
+ },
+ Endpoint: th.Endpoint(),
+ }
+}
+
+func TestList(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/volumes", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", 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"
+ }
+ ]
+ }
+ `)
+ })
+
+ client := ServiceClient()
+ count := 0
+
+ List(client, &ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractVolumes(page)
+ if err != nil {
+ t.Errorf("Failed to extract volumes: %v", err)
+ return false, err
+ }
+
+ expected := []Volume{
+ Volume{
+ ID: "289da7f8-6440-407c-9fb4-7db01ec49164",
+ Name: "vol-001",
+ },
+ Volume{
+ ID: "96c3bda7-c82a-4f50-be73-ca7621794835",
+ Name: "vol-002",
+ },
+ }
+
+ th.CheckDeepEquals(t, expected, actual)
+
+ return true, nil
+ })
+
+ if count != 1 {
+ t.Errorf("Expected 1 page, got %d", count)
+ }
+}
+
+func TestGet(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ 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", 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"
+ }
+}
+ `)
+ })
+
+ v, err := Get(ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22").Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, v.Name, "vol-001")
+ th.AssertEquals(t, v.ID, "d32019d3-bc6e-4319-9c1d-6722fc136a22")
+}
+
+func TestCreate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/volumes", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "POST")
+ th.TestHeader(t, r, "X-Auth-Token", TokenID)
+ th.TestHeader(t, r, "Content-Type", "application/json")
+ th.TestHeader(t, r, "Accept", "application/json")
+ th.TestJSONRequest(t, r, `
+{
+ "volume": {
+ "size": 4
+ }
+}
+ `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusCreated)
+
+ fmt.Fprintf(w, `
+{
+ "volume": {
+ "size": 4,
+ "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22"
+ }
+}
+ `)
+ })
+
+ options := &CreateOpts{Size: 4}
+ n, err := Create(ServiceClient(), options).Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, n.Size, 4)
+ th.AssertEquals(t, n.ID, "d32019d3-bc6e-4319-9c1d-6722fc136a22")
+}
+
+func TestDelete(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ 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", TokenID)
+ w.WriteHeader(http.StatusNoContent)
+ })
+
+ err := Delete(ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22")
+ th.AssertNoErr(t, err)
+}
+
+func TestUpdate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ 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", TokenID)
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprintf(w, `
+ {
+ "volume": {
+ "display_name": "vol-002",
+ "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22"
+ }
+ }
+ `)
+ })
+
+ options := &UpdateOpts{Name: "vol-002"}
+ v, err := Update(ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22", options).Extract()
+ th.AssertNoErr(t, err)
+ th.CheckEquals(t, "vol-002", v.Name)
+}
diff --git a/_site/openstack/blockstorage/v1/volumes/results.go b/_site/openstack/blockstorage/v1/volumes/results.go
new file mode 100644
index 0000000..78eb6c1
--- /dev/null
+++ b/_site/openstack/blockstorage/v1/volumes/results.go
@@ -0,0 +1,83 @@
+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 {
+ Status string `mapstructure:"status"` // current status of the Volume
+ Name string `mapstructure:"display_name"` // display name
+ Attachments []string `mapstructure:"attachments"` // instances onto which the Volume is attached
+ AvailabilityZone string `mapstructure:"availability_zone"` // logical group
+ Bootable string `mapstructure:"bootable"` // is the volume bootable
+ CreatedAt string `mapstructure:"created_at"` // date created
+ Description string `mapstructure:"display_discription"` // display description
+ VolumeType string `mapstructure:"volume_type"` // see VolumeType object for more information
+ SnapshotID string `mapstructure:"snapshot_id"` // ID of the Snapshot from which the Volume was created
+ SourceVolID string `mapstructure:"source_volid"` // ID of the Volume from which the Volume was created
+ Metadata map[string]string `mapstructure:"metadata"` // user-defined key-value pairs
+ ID string `mapstructure:"id"` // unique identifier
+ Size int `mapstructure:"size"` // size of the Volume, in GB
+}
+
+// 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
+}
+
+// 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.CommonResult
+}
+
+// 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.Resp, &res)
+
+ return res.Volume, err
+}
diff --git a/_site/openstack/blockstorage/v1/volumes/urls.go b/_site/openstack/blockstorage/v1/volumes/urls.go
new file mode 100644
index 0000000..29629a1
--- /dev/null
+++ b/_site/openstack/blockstorage/v1/volumes/urls.go
@@ -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)
+}
diff --git a/_site/openstack/blockstorage/v1/volumes/urls_test.go b/_site/openstack/blockstorage/v1/volumes/urls_test.go
new file mode 100644
index 0000000..a95270e
--- /dev/null
+++ b/_site/openstack/blockstorage/v1/volumes/urls_test.go
@@ -0,0 +1,44 @@
+package volumes
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+const endpoint = "http://localhost:57909"
+
+func endpointClient() *gophercloud.ServiceClient {
+ return &gophercloud.ServiceClient{Endpoint: endpoint}
+}
+
+func TestCreateURL(t *testing.T) {
+ actual := createURL(endpointClient())
+ expected := endpoint + "volumes"
+ th.AssertEquals(t, expected, actual)
+}
+
+func TestListURL(t *testing.T) {
+ actual := listURL(endpointClient())
+ expected := endpoint + "volumes"
+ th.AssertEquals(t, expected, actual)
+}
+
+func TestDeleteURL(t *testing.T) {
+ actual := deleteURL(endpointClient(), "foo")
+ expected := endpoint + "volumes/foo"
+ th.AssertEquals(t, expected, actual)
+}
+
+func TestGetURL(t *testing.T) {
+ actual := getURL(endpointClient(), "foo")
+ expected := endpoint + "volumes/foo"
+ th.AssertEquals(t, expected, actual)
+}
+
+func TestUpdateURL(t *testing.T) {
+ actual := updateURL(endpointClient(), "foo")
+ expected := endpoint + "volumes/foo"
+ th.AssertEquals(t, expected, actual)
+}
diff --git a/_site/openstack/blockstorage/v1/volumes/util.go b/_site/openstack/blockstorage/v1/volumes/util.go
new file mode 100644
index 0000000..1dda695
--- /dev/null
+++ b/_site/openstack/blockstorage/v1/volumes/util.go
@@ -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
+ })
+}
diff --git a/_site/openstack/blockstorage/v1/volumes/util_test.go b/_site/openstack/blockstorage/v1/volumes/util_test.go
new file mode 100644
index 0000000..7de1326
--- /dev/null
+++ b/_site/openstack/blockstorage/v1/volumes/util_test.go
@@ -0,0 +1,37 @@
+package volumes
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+ "time"
+
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestWaitForStatus(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/volumes/1234", func(w http.ResponseWriter, r *http.Request) {
+ time.Sleep(2 * time.Second)
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprintf(w, `
+ {
+ "volume": {
+ "display_name": "vol-001",
+ "id": "1234",
+ "status":"available"
+ }
+ }`)
+ })
+
+ err := WaitForStatus(ServiceClient(), "1234", "available", 0)
+ if err == nil {
+ t.Errorf("Expected error: 'Time Out in WaitFor'")
+ }
+
+ err = WaitForStatus(ServiceClient(), "1234", "available", 3)
+ th.CheckNoErr(t, err)
+}
diff --git a/_site/openstack/blockstorage/v1/volumetypes/requests.go b/_site/openstack/blockstorage/v1/volumetypes/requests.go
new file mode 100644
index 0000000..d4f880f
--- /dev/null
+++ b/_site/openstack/blockstorage/v1/volumetypes/requests.go
@@ -0,0 +1,86 @@
+package volumetypes
+
+import (
+ "github.com/racker/perigee"
+ "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 = perigee.Request("POST", createURL(client), perigee.Options{
+ MoreHeaders: client.Provider.AuthenticatedHeaders(),
+ OkCodes: []int{200, 201},
+ ReqBody: &reqBody,
+ Results: &res.Resp,
+ })
+ return res
+}
+
+// Delete will delete the volume type with the provided ID.
+func Delete(client *gophercloud.ServiceClient, id string) error {
+ _, err := perigee.Request("DELETE", deleteURL(client, id), perigee.Options{
+ MoreHeaders: client.Provider.AuthenticatedHeaders(),
+ OkCodes: []int{202},
+ })
+ return err
+}
+
+// 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 := perigee.Request("GET", getURL(client, id), perigee.Options{
+ MoreHeaders: client.Provider.AuthenticatedHeaders(),
+ OkCodes: []int{200},
+ Results: &res.Resp,
+ })
+ res.Err = err
+ return res
+}
+
+// List returns all volume types.
+func List(client *gophercloud.ServiceClient) pagination.Pager {
+ createPage := func(r pagination.LastHTTPResponse) pagination.Page {
+ return ListResult{pagination.SinglePageBase(r)}
+ }
+
+ return pagination.NewPager(client, listURL(client), createPage)
+}
diff --git a/_site/openstack/blockstorage/v1/volumetypes/requests_test.go b/_site/openstack/blockstorage/v1/volumetypes/requests_test.go
new file mode 100644
index 0000000..a9c6512
--- /dev/null
+++ b/_site/openstack/blockstorage/v1/volumetypes/requests_test.go
@@ -0,0 +1,172 @@
+package volumetypes
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+const TokenID = "123"
+
+func ServiceClient() *gophercloud.ServiceClient {
+ return &gophercloud.ServiceClient{
+ Provider: &gophercloud.ProviderClient{
+ TokenID: TokenID,
+ },
+ Endpoint: th.Endpoint(),
+ }
+}
+
+func TestList(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/types", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", 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": {}
+ }
+ ]
+ }
+ `)
+ })
+
+ client := ServiceClient()
+ count := 0
+
+ List(client).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractVolumeTypes(page)
+ if err != nil {
+ t.Errorf("Failed to extract volume types: %v", err)
+ return false, err
+ }
+
+ expected := []VolumeType{
+ VolumeType{
+ ID: "289da7f8-6440-407c-9fb4-7db01ec49164",
+ Name: "vol-type-001",
+ ExtraSpecs: map[string]interface{}{
+ "capabilities": "gpu",
+ },
+ },
+ VolumeType{
+ ID: "96c3bda7-c82a-4f50-be73-ca7621794835",
+ Name: "vol-type-002",
+ ExtraSpecs: map[string]interface{}{},
+ },
+ }
+
+ th.CheckDeepEquals(t, expected, actual)
+
+ return true, nil
+ })
+
+ if count != 1 {
+ t.Errorf("Expected 1 page, got %d", count)
+ }
+}
+
+func TestGet(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ 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", 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"
+ }
+ }
+}
+ `)
+ })
+
+ vt, err := Get(ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22").Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertDeepEquals(t, vt.ExtraSpecs, map[string]interface{}{"serverNumber": "2"})
+ th.AssertEquals(t, vt.Name, "vol-type-001")
+ th.AssertEquals(t, vt.ID, "d32019d3-bc6e-4319-9c1d-6722fc136a22")
+}
+
+func TestCreate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/types", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "POST")
+ th.TestHeader(t, r, "X-Auth-Token", TokenID)
+ th.TestHeader(t, r, "Content-Type", "application/json")
+ th.TestHeader(t, r, "Accept", "application/json")
+ th.TestJSONRequest(t, r, `
+{
+ "volume_type": {
+ "name": "vol-type-001"
+ }
+}
+ `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusCreated)
+
+ fmt.Fprintf(w, `
+{
+ "volume_type": {
+ "name": "vol-type-001",
+ "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22"
+ }
+}
+ `)
+ })
+
+ options := &CreateOpts{Name: "vol-type-001"}
+ n, err := Create(ServiceClient(), options).Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, n.Name, "vol-type-001")
+ th.AssertEquals(t, n.ID, "d32019d3-bc6e-4319-9c1d-6722fc136a22")
+}
+
+func TestDelete(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/types/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "DELETE")
+ th.TestHeader(t, r, "X-Auth-Token", TokenID)
+ w.WriteHeader(http.StatusAccepted)
+ })
+
+ err := Delete(ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22")
+ th.AssertNoErr(t, err)
+}
diff --git a/_site/openstack/blockstorage/v1/volumetypes/results.go b/_site/openstack/blockstorage/v1/volumetypes/results.go
new file mode 100644
index 0000000..77cc1f5
--- /dev/null
+++ b/_site/openstack/blockstorage/v1/volumetypes/results.go
@@ -0,0 +1,67 @@
+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
+}
+
+// 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.CommonResult
+}
+
+// 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.Resp, &res)
+
+ return res.VolumeType, err
+}
diff --git a/_site/openstack/blockstorage/v1/volumetypes/urls.go b/_site/openstack/blockstorage/v1/volumetypes/urls.go
new file mode 100644
index 0000000..cf8367b
--- /dev/null
+++ b/_site/openstack/blockstorage/v1/volumetypes/urls.go
@@ -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)
+}
diff --git a/_site/openstack/blockstorage/v1/volumetypes/urls_test.go b/_site/openstack/blockstorage/v1/volumetypes/urls_test.go
new file mode 100644
index 0000000..44016e2
--- /dev/null
+++ b/_site/openstack/blockstorage/v1/volumetypes/urls_test.go
@@ -0,0 +1,38 @@
+package volumetypes
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+const endpoint = "http://localhost:57909"
+
+func endpointClient() *gophercloud.ServiceClient {
+ return &gophercloud.ServiceClient{Endpoint: endpoint}
+}
+
+func TestListURL(t *testing.T) {
+ actual := listURL(endpointClient())
+ expected := endpoint + "types"
+ th.AssertEquals(t, expected, actual)
+}
+
+func TestCreateURL(t *testing.T) {
+ actual := createURL(endpointClient())
+ expected := endpoint + "types"
+ th.AssertEquals(t, expected, actual)
+}
+
+func TestGetURL(t *testing.T) {
+ actual := getURL(endpointClient(), "foo")
+ expected := endpoint + "types/foo"
+ th.AssertEquals(t, expected, actual)
+}
+
+func TestDeleteURL(t *testing.T) {
+ actual := deleteURL(endpointClient(), "foo")
+ expected := endpoint + "types/foo"
+ th.AssertEquals(t, expected, actual)
+}
diff --git a/_site/openstack/client.go b/_site/openstack/client.go
new file mode 100644
index 0000000..97556d6
--- /dev/null
+++ b/_site/openstack/client.go
@@ -0,0 +1,205 @@
+package openstack
+
+import (
+ "fmt"
+ "net/url"
+
+ "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{
+ &utils.Version{ID: v20, Priority: 20, Suffix: "/v2.0/"},
+ &utils.Version{ID: v30, Priority: 30, Suffix: "/v3/"},
+ }
+
+ chosen, endpoint, err := utils.ChooseVersion(client.IdentityBase, client.IdentityEndpoint, 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
+ }
+
+ 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
+ }
+
+ token, err := tokens3.Create(v3Client, options, nil).Extract()
+ if err != nil {
+ return err
+ }
+ client.TokenID = token.ID
+
+ client.EndpointLocator = func(opts gophercloud.EndpointOpts) (string, error) {
+ return V3EndpointURL(v3Client, 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{
+ Provider: 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{
+ Provider: client,
+ Endpoint: v3Endpoint,
+ }
+}
+
+// NewStorageV1 creates a ServiceClient that may be used with the v1 object storage package.
+func NewStorageV1(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{Provider: 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{Provider: 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{
+ Provider: 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{Provider: client, Endpoint: url}, nil
+}
diff --git a/_site/openstack/client_test.go b/_site/openstack/client_test.go
new file mode 100644
index 0000000..257260c
--- /dev/null
+++ b/_site/openstack/client_test.go
@@ -0,0 +1,161 @@
+package openstack
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestAuthenticatedClientV3(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ const ID = "0123456789"
+
+ th.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+ fmt.Fprintf(w, `
+ {
+ "versions": {
+ "values": [
+ {
+ "status": "stable",
+ "id": "v3.0",
+ "links": [
+ { "href": "%s", "rel": "self" }
+ ]
+ },
+ {
+ "status": "stable",
+ "id": "v2.0",
+ "links": [
+ { "href": "%s", "rel": "self" }
+ ]
+ }
+ ]
+ }
+ }
+ `, th.Endpoint()+"v3/", th.Endpoint()+"v2.0/")
+ })
+
+ th.Mux.HandleFunc("/v3/auth/tokens", func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Add("X-Subject-Token", ID)
+
+ w.WriteHeader(http.StatusCreated)
+ fmt.Fprintf(w, `{ "token": { "expires_at": "2013-02-02T18:30:59.000000Z" } }`)
+ })
+
+ options := gophercloud.AuthOptions{
+ UserID: "me",
+ Password: "secret",
+ IdentityEndpoint: th.Endpoint(),
+ }
+ client, err := AuthenticatedClient(options)
+ th.AssertNoErr(t, err)
+ th.CheckEquals(t, ID, client.TokenID)
+}
+
+func TestAuthenticatedClientV2(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+ fmt.Fprintf(w, `
+ {
+ "versions": {
+ "values": [
+ {
+ "status": "experimental",
+ "id": "v3.0",
+ "links": [
+ { "href": "%s", "rel": "self" }
+ ]
+ },
+ {
+ "status": "stable",
+ "id": "v2.0",
+ "links": [
+ { "href": "%s", "rel": "self" }
+ ]
+ }
+ ]
+ }
+ }
+ `, th.Endpoint()+"v3/", th.Endpoint()+"v2.0/")
+ })
+
+ th.Mux.HandleFunc("/v2.0/tokens", func(w http.ResponseWriter, r *http.Request) {
+ fmt.Fprintf(w, `
+ {
+ "access": {
+ "token": {
+ "id": "01234567890",
+ "expires": "2014-10-01T10:00:00.000000Z"
+ },
+ "serviceCatalog": [
+ {
+ "name": "Cloud Servers",
+ "type": "compute",
+ "endpoints": [
+ {
+ "tenantId": "t1000",
+ "publicURL": "https://compute.north.host.com/v1/t1000",
+ "internalURL": "https://compute.north.internal/v1/t1000",
+ "region": "North",
+ "versionId": "1",
+ "versionInfo": "https://compute.north.host.com/v1/",
+ "versionList": "https://compute.north.host.com/"
+ },
+ {
+ "tenantId": "t1000",
+ "publicURL": "https://compute.north.host.com/v1.1/t1000",
+ "internalURL": "https://compute.north.internal/v1.1/t1000",
+ "region": "North",
+ "versionId": "1.1",
+ "versionInfo": "https://compute.north.host.com/v1.1/",
+ "versionList": "https://compute.north.host.com/"
+ }
+ ],
+ "endpoints_links": []
+ },
+ {
+ "name": "Cloud Files",
+ "type": "object-store",
+ "endpoints": [
+ {
+ "tenantId": "t1000",
+ "publicURL": "https://storage.north.host.com/v1/t1000",
+ "internalURL": "https://storage.north.internal/v1/t1000",
+ "region": "North",
+ "versionId": "1",
+ "versionInfo": "https://storage.north.host.com/v1/",
+ "versionList": "https://storage.north.host.com/"
+ },
+ {
+ "tenantId": "t1000",
+ "publicURL": "https://storage.south.host.com/v1/t1000",
+ "internalURL": "https://storage.south.internal/v1/t1000",
+ "region": "South",
+ "versionId": "1",
+ "versionInfo": "https://storage.south.host.com/v1/",
+ "versionList": "https://storage.south.host.com/"
+ }
+ ]
+ }
+ ]
+ }
+ }
+ `)
+ })
+
+ options := gophercloud.AuthOptions{
+ Username: "me",
+ Password: "secret",
+ IdentityEndpoint: th.Endpoint(),
+ }
+ client, err := AuthenticatedClient(options)
+ th.AssertNoErr(t, err)
+ th.CheckEquals(t, "01234567890", client.TokenID)
+}
diff --git a/_site/openstack/common/README.md b/_site/openstack/common/README.md
new file mode 100644
index 0000000..7b55795
--- /dev/null
+++ b/_site/openstack/common/README.md
@@ -0,0 +1,3 @@
+# Common Resources
+
+This directory is for resources that are shared by multiple services.
diff --git a/_site/openstack/common/extensions/doc.go b/_site/openstack/common/extensions/doc.go
new file mode 100644
index 0000000..4a168f4
--- /dev/null
+++ b/_site/openstack/common/extensions/doc.go
@@ -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
diff --git a/_site/openstack/common/extensions/errors.go b/_site/openstack/common/extensions/errors.go
new file mode 100755
index 0000000..aeec0fa
--- /dev/null
+++ b/_site/openstack/common/extensions/errors.go
@@ -0,0 +1 @@
+package extensions
diff --git a/_site/openstack/common/extensions/fixtures.go b/_site/openstack/common/extensions/fixtures.go
new file mode 100644
index 0000000..0ed7de9
--- /dev/null
+++ b/_site/openstack/common/extensions/fixtures.go
@@ -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)
+ })
+}
diff --git a/_site/openstack/common/extensions/requests.go b/_site/openstack/common/extensions/requests.go
new file mode 100755
index 0000000..000151b
--- /dev/null
+++ b/_site/openstack/common/extensions/requests.go
@@ -0,0 +1,26 @@
+package extensions
+
+import (
+ "github.com/racker/perigee"
+ "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 = perigee.Request("GET", ExtensionURL(c, alias), perigee.Options{
+ MoreHeaders: c.Provider.AuthenticatedHeaders(),
+ Results: &res.Resp,
+ OkCodes: []int{200},
+ })
+ 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.LastHTTPResponse) pagination.Page {
+ return ExtensionPage{pagination.SinglePageBase(r)}
+ })
+}
diff --git a/_site/openstack/common/extensions/requests_test.go b/_site/openstack/common/extensions/requests_test.go
new file mode 100644
index 0000000..6550283
--- /dev/null
+++ b/_site/openstack/common/extensions/requests_test.go
@@ -0,0 +1,38 @@
+package extensions
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+ "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestList(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleListExtensionsSuccessfully(t)
+
+ count := 0
+
+ List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractExtensions(page)
+ th.AssertNoErr(t, err)
+ th.AssertDeepEquals(t, ExpectedExtensions, actual)
+
+ return true, nil
+ })
+
+ th.CheckEquals(t, 1, count)
+}
+
+func TestGet(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleGetExtensionSuccessfully(t)
+
+ actual, err := Get(client.ServiceClient(), "agent").Extract()
+ th.AssertNoErr(t, err)
+ th.CheckDeepEquals(t, SingleExtension, actual)
+}
diff --git a/_site/openstack/common/extensions/results.go b/_site/openstack/common/extensions/results.go
new file mode 100755
index 0000000..4827072
--- /dev/null
+++ b/_site/openstack/common/extensions/results.go
@@ -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.CommonResult
+}
+
+// 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.Resp, &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
+}
diff --git a/_site/openstack/common/extensions/urls.go b/_site/openstack/common/extensions/urls.go
new file mode 100644
index 0000000..6460c66
--- /dev/null
+++ b/_site/openstack/common/extensions/urls.go
@@ -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")
+}
diff --git a/_site/openstack/common/extensions/urls_test.go b/_site/openstack/common/extensions/urls_test.go
new file mode 100755
index 0000000..3223b1c
--- /dev/null
+++ b/_site/openstack/common/extensions/urls_test.go
@@ -0,0 +1,26 @@
+package extensions
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+const endpoint = "http://localhost:57909/"
+
+func endpointClient() *gophercloud.ServiceClient {
+ return &gophercloud.ServiceClient{Endpoint: endpoint}
+}
+
+func TestExtensionURL(t *testing.T) {
+ actual := ExtensionURL(endpointClient(), "agent")
+ expected := endpoint + "extensions/agent"
+ th.AssertEquals(t, expected, actual)
+}
+
+func TestListExtensionURL(t *testing.T) {
+ actual := ListExtensionURL(endpointClient())
+ expected := endpoint + "extensions"
+ th.AssertEquals(t, expected, actual)
+}
diff --git a/_site/openstack/compute/v2/flavors/requests.go b/_site/openstack/compute/v2/flavors/requests.go
new file mode 100644
index 0000000..469c69d
--- /dev/null
+++ b/_site/openstack/compute/v2/flavors/requests.go
@@ -0,0 +1,72 @@
+package flavors
+
+import (
+ "github.com/racker/perigee"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// ListOptsBuilder allows extensions to add additional parameters to the
+// List request.
+type ListOptsBuilder interface {
+ ToFlavorListParams() (string, error)
+}
+
+// ListOpts helps control the results returned by the List() function.
+// For example, a flavor with a minDisk field of 10 will not be returned if you specify MinDisk set to 20.
+// Typically, software will use the last ID of the previous call to List to set the Marker for the current call.
+type ListOpts struct {
+
+ // ChangesSince, if provided, instructs List to return only those things which have changed since the timestamp provided.
+ ChangesSince string `q:"changes-since"`
+
+ // MinDisk and MinRAM, if provided, elides flavors which do not meet your criteria.
+ MinDisk int `q:"minDisk"`
+ MinRAM int `q:"minRam"`
+
+ // Marker and Limit control paging.
+ // Marker instructs List where to start listing from.
+ Marker string `q:"marker"`
+
+ // Limit instructs List to refrain from sending excessively large lists of flavors.
+ Limit int `q:"limit"`
+}
+
+// ToFlavorListParams formats a ListOpts into a query string.
+func (opts ListOpts) ToFlavorListParams() (string, error) {
+ q, err := gophercloud.BuildQueryString(opts)
+ if err != nil {
+ return "", err
+ }
+ return q.String(), nil
+}
+
+// List instructs OpenStack to provide a list of flavors.
+// You may provide criteria by which List curtails its results for easier processing.
+// See ListOpts for more details.
+func List(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
+ url := listURL(client)
+ if opts != nil {
+ query, err := opts.ToFlavorListParams()
+ if err != nil {
+ return pagination.Pager{Err: err}
+ }
+ url += query
+ }
+ createPage := func(r pagination.LastHTTPResponse) pagination.Page {
+ return FlavorPage{pagination.LinkedPageBase{LastHTTPResponse: r}}
+ }
+
+ return pagination.NewPager(client, url, createPage)
+}
+
+// Get instructs OpenStack to provide details on a single flavor, identified by its ID.
+// Use ExtractFlavor to convert its result into a Flavor.
+func Get(client *gophercloud.ServiceClient, id string) GetResult {
+ var gr GetResult
+ gr.Err = perigee.Get(getURL(client, id), perigee.Options{
+ Results: &gr.Resp,
+ MoreHeaders: client.Provider.AuthenticatedHeaders(),
+ })
+ return gr
+}
diff --git a/_site/openstack/compute/v2/flavors/requests_test.go b/_site/openstack/compute/v2/flavors/requests_test.go
new file mode 100644
index 0000000..bc9b82e
--- /dev/null
+++ b/_site/openstack/compute/v2/flavors/requests_test.go
@@ -0,0 +1,129 @@
+package flavors
+
+import (
+ "fmt"
+ "net/http"
+ "reflect"
+ "testing"
+
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+const tokenID = "blerb"
+
+func TestListFlavors(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/flavors/detail", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+ r.ParseForm()
+ marker := r.Form.Get("marker")
+ switch marker {
+ case "":
+ fmt.Fprintf(w, `
+ {
+ "flavors": [
+ {
+ "id": "1",
+ "name": "m1.tiny",
+ "disk": 1,
+ "ram": 512,
+ "vcpus": 1
+ },
+ {
+ "id": "2",
+ "name": "m2.small",
+ "disk": 10,
+ "ram": 1024,
+ "vcpus": 2
+ }
+ ],
+ "flavors_links": [
+ {
+ "href": "%s/flavors/detail?marker=2",
+ "rel": "next"
+ }
+ ]
+ }
+ `, th.Server.URL)
+ case "2":
+ fmt.Fprintf(w, `{ "flavors": [] }`)
+ default:
+ t.Fatalf("Unexpected marker: [%s]", marker)
+ }
+ })
+
+ pages := 0
+ err := List(fake.ServiceClient(), &ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+ pages++
+
+ actual, err := ExtractFlavors(page)
+ if err != nil {
+ return false, err
+ }
+
+ expected := []Flavor{
+ Flavor{ID: "1", Name: "m1.tiny", Disk: 1, RAM: 512, VCPUs: 1},
+ Flavor{ID: "2", Name: "m2.small", Disk: 10, RAM: 1024, VCPUs: 2},
+ }
+
+ if !reflect.DeepEqual(expected, actual) {
+ t.Errorf("Expected %#v, but was %#v", expected, actual)
+ }
+
+ return true, nil
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+ if pages != 1 {
+ t.Errorf("Expected one page, got %d", pages)
+ }
+}
+
+func TestGetFlavor(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/flavors/12345", 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")
+ fmt.Fprintf(w, `
+ {
+ "flavor": {
+ "id": "1",
+ "name": "m1.tiny",
+ "disk": 1,
+ "ram": 512,
+ "vcpus": 1,
+ "rxtx_factor": 1
+ }
+ }
+ `)
+ })
+
+ actual, err := Get(fake.ServiceClient(), "12345").Extract()
+ if err != nil {
+ t.Fatalf("Unable to get flavor: %v", err)
+ }
+
+ expected := &Flavor{
+ ID: "1",
+ Name: "m1.tiny",
+ Disk: 1,
+ RAM: 512,
+ VCPUs: 1,
+ RxTxFactor: 1,
+ }
+ if !reflect.DeepEqual(expected, actual) {
+ t.Errorf("Expected %#v, but was %#v", expected, actual)
+ }
+}
diff --git a/_site/openstack/compute/v2/flavors/results.go b/_site/openstack/compute/v2/flavors/results.go
new file mode 100644
index 0000000..1e274e3
--- /dev/null
+++ b/_site/openstack/compute/v2/flavors/results.go
@@ -0,0 +1,122 @@
+package flavors
+
+import (
+ "errors"
+ "reflect"
+
+ "github.com/mitchellh/mapstructure"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// ErrCannotInterpret is returned by an Extract call if the response body doesn't have the expected structure.
+var ErrCannotInterpet = errors.New("Unable to interpret a response body.")
+
+// GetResult temporarily holds the reponse from a Get call.
+type GetResult struct {
+ gophercloud.CommonResult
+}
+
+// Extract provides access to the individual Flavor returned by the Get function.
+func (gr GetResult) Extract() (*Flavor, error) {
+ if gr.Err != nil {
+ return nil, gr.Err
+ }
+
+ var result struct {
+ Flavor Flavor `mapstructure:"flavor"`
+ }
+
+ cfg := &mapstructure.DecoderConfig{
+ DecodeHook: defaulter,
+ Result: &result,
+ }
+ decoder, err := mapstructure.NewDecoder(cfg)
+ if err != nil {
+ return nil, err
+ }
+ err = decoder.Decode(gr.Resp)
+ return &result.Flavor, err
+}
+
+// Flavor records represent (virtual) hardware configurations for server resources in a region.
+type Flavor struct {
+ // The Id field contains the flavor's unique identifier.
+ // For example, this identifier will be useful when specifying which hardware configuration to use for a new server instance.
+ ID string `mapstructure:"id"`
+
+ // The Disk and RA< fields provide a measure of storage space offered by the flavor, in GB and MB, respectively.
+ Disk int `mapstructure:"disk"`
+ RAM int `mapstructure:"ram"`
+
+ // The Name field provides a human-readable moniker for the flavor.
+ Name string `mapstructure:"name"`
+
+ RxTxFactor float64 `mapstructure:"rxtx_factor"`
+
+ // Swap indicates how much space is reserved for swap.
+ // If not provided, this field will be set to 0.
+ Swap int `mapstructure:"swap"`
+
+ // VCPUs indicates how many (virtual) CPUs are available for this flavor.
+ VCPUs int `mapstructure:"vcpus"`
+}
+
+// FlavorPage contains a single page of the response from a List call.
+type FlavorPage struct {
+ pagination.LinkedPageBase
+}
+
+// IsEmpty determines if a page contains any results.
+func (p FlavorPage) IsEmpty() (bool, error) {
+ flavors, err := ExtractFlavors(p)
+ if err != nil {
+ return true, err
+ }
+ return len(flavors) == 0, nil
+}
+
+// NextPageURL uses the response's embedded link reference to navigate to the next page of results.
+func (p FlavorPage) NextPageURL() (string, error) {
+ type resp struct {
+ Links []gophercloud.Link `mapstructure:"flavors_links"`
+ }
+
+ var r resp
+ err := mapstructure.Decode(p.Body, &r)
+ if err != nil {
+ return "", err
+ }
+
+ return gophercloud.ExtractNextURL(r.Links)
+}
+
+func defaulter(from, to reflect.Kind, v interface{}) (interface{}, error) {
+ if (from == reflect.String) && (to == reflect.Int) {
+ return 0, nil
+ }
+ return v, nil
+}
+
+// ExtractFlavors provides access to the list of flavors in a page acquired from the List operation.
+func ExtractFlavors(page pagination.Page) ([]Flavor, error) {
+ casted := page.(FlavorPage).Body
+ var container struct {
+ Flavors []Flavor `mapstructure:"flavors"`
+ }
+
+ cfg := &mapstructure.DecoderConfig{
+ DecodeHook: defaulter,
+ Result: &container,
+ }
+ decoder, err := mapstructure.NewDecoder(cfg)
+ if err != nil {
+ return container.Flavors, err
+ }
+ err = decoder.Decode(casted)
+ if err != nil {
+ return container.Flavors, err
+ }
+
+ return container.Flavors, nil
+}
diff --git a/_site/openstack/compute/v2/flavors/urls.go b/_site/openstack/compute/v2/flavors/urls.go
new file mode 100644
index 0000000..683c107
--- /dev/null
+++ b/_site/openstack/compute/v2/flavors/urls.go
@@ -0,0 +1,13 @@
+package flavors
+
+import (
+ "github.com/rackspace/gophercloud"
+)
+
+func getURL(client *gophercloud.ServiceClient, id string) string {
+ return client.ServiceURL("flavors", id)
+}
+
+func listURL(client *gophercloud.ServiceClient) string {
+ return client.ServiceURL("flavors", "detail")
+}
diff --git a/_site/openstack/compute/v2/flavors/urls_test.go b/_site/openstack/compute/v2/flavors/urls_test.go
new file mode 100644
index 0000000..069da24
--- /dev/null
+++ b/_site/openstack/compute/v2/flavors/urls_test.go
@@ -0,0 +1,26 @@
+package flavors
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+const endpoint = "http://localhost:57909/"
+
+func endpointClient() *gophercloud.ServiceClient {
+ return &gophercloud.ServiceClient{Endpoint: endpoint}
+}
+
+func TestGetURL(t *testing.T) {
+ actual := getURL(endpointClient(), "foo")
+ expected := endpoint + "flavors/foo"
+ th.CheckEquals(t, expected, actual)
+}
+
+func TestListURL(t *testing.T) {
+ actual := listURL(endpointClient())
+ expected := endpoint + "flavors/detail"
+ th.CheckEquals(t, expected, actual)
+}
diff --git a/_site/openstack/compute/v2/images/requests.go b/_site/openstack/compute/v2/images/requests.go
new file mode 100644
index 0000000..d901f6e
--- /dev/null
+++ b/_site/openstack/compute/v2/images/requests.go
@@ -0,0 +1,71 @@
+package images
+
+import (
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+
+ "github.com/racker/perigee"
+)
+
+// ListOptsBuilder allows extensions to add additional parameters to the
+// List request.
+type ListOptsBuilder interface {
+ ToImageListParams() (string, error)
+}
+
+// ListOpts contain options for limiting the number of Images returned from a call to ListDetail.
+type ListOpts struct {
+ // When the image last changed status (in date-time format).
+ ChangesSince string `q:"changes-since"`
+ // The number of Images to return.
+ Limit int `q:"limit"`
+ // UUID of the Image at which to set a marker.
+ Marker string `q:"marker"`
+ // The name of the Image.
+ Name string `q:"name:"`
+ // The name of the Server (in URL format).
+ Server string `q:"server"`
+ // The current status of the Image.
+ Status string `q:"status"`
+ // The value of the type of image (e.g. BASE, SERVER, ALL)
+ Type string `q:"type"`
+}
+
+// ToImageListParams formats a ListOpts into a query string.
+func (opts ListOpts) ToImageListParams() (string, error) {
+ q, err := gophercloud.BuildQueryString(opts)
+ if err != nil {
+ return "", err
+ }
+ return q.String(), nil
+}
+
+// ListDetail enumerates the available images.
+func ListDetail(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
+ url := listDetailURL(client)
+ if opts != nil {
+ query, err := opts.ToImageListParams()
+ if err != nil {
+ return pagination.Pager{Err: err}
+ }
+ url += query
+ }
+
+ createPage := func(r pagination.LastHTTPResponse) pagination.Page {
+ return ImagePage{pagination.LinkedPageBase{LastHTTPResponse: r}}
+ }
+
+ return pagination.NewPager(client, url, createPage)
+}
+
+// Get acquires additional detail about a specific image by ID.
+// Use ExtractImage() to intepret the result as an openstack Image.
+func Get(client *gophercloud.ServiceClient, id string) GetResult {
+ var result GetResult
+ _, result.Err = perigee.Request("GET", getURL(client, id), perigee.Options{
+ MoreHeaders: client.Provider.AuthenticatedHeaders(),
+ Results: &result.Resp,
+ OkCodes: []int{200},
+ })
+ return result
+}
diff --git a/_site/openstack/compute/v2/images/requests_test.go b/_site/openstack/compute/v2/images/requests_test.go
new file mode 100644
index 0000000..2dfa88b
--- /dev/null
+++ b/_site/openstack/compute/v2/images/requests_test.go
@@ -0,0 +1,175 @@
+package images
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "reflect"
+ "testing"
+
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestListImages(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/images/detail", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+ r.ParseForm()
+ marker := r.Form.Get("marker")
+ switch marker {
+ case "":
+ fmt.Fprintf(w, `
+ {
+ "images": [
+ {
+ "status": "ACTIVE",
+ "updated": "2014-09-23T12:54:56Z",
+ "id": "f3e4a95d-1f4f-4989-97ce-f3a1fb8c04d7",
+ "OS-EXT-IMG-SIZE:size": 476704768,
+ "name": "F17-x86_64-cfntools",
+ "created": "2014-09-23T12:54:52Z",
+ "minDisk": 0,
+ "progress": 100,
+ "minRam": 0,
+ "metadata": {}
+ },
+ {
+ "status": "ACTIVE",
+ "updated": "2014-09-23T12:51:43Z",
+ "id": "f90f6034-2570-4974-8351-6b49732ef2eb",
+ "OS-EXT-IMG-SIZE:size": 13167616,
+ "name": "cirros-0.3.2-x86_64-disk",
+ "created": "2014-09-23T12:51:42Z",
+ "minDisk": 0,
+ "progress": 100,
+ "minRam": 0,
+ "metadata": {}
+ }
+ ]
+ }
+ `)
+ case "2":
+ fmt.Fprintf(w, `{ "images": [] }`)
+ default:
+ t.Fatalf("Unexpected marker: [%s]", marker)
+ }
+ })
+
+ pages := 0
+ options := &ListOpts{Limit: 2}
+ err := ListDetail(fake.ServiceClient(), options).EachPage(func(page pagination.Page) (bool, error) {
+ pages++
+
+ actual, err := ExtractImages(page)
+ if err != nil {
+ return false, err
+ }
+
+ expected := []Image{
+ Image{
+ ID: "f3e4a95d-1f4f-4989-97ce-f3a1fb8c04d7",
+ Name: "F17-x86_64-cfntools",
+ Created: "2014-09-23T12:54:52Z",
+ Updated: "2014-09-23T12:54:56Z",
+ MinDisk: 0,
+ MinRAM: 0,
+ Progress: 100,
+ Status: "ACTIVE",
+ },
+ Image{
+ ID: "f90f6034-2570-4974-8351-6b49732ef2eb",
+ Name: "cirros-0.3.2-x86_64-disk",
+ Created: "2014-09-23T12:51:42Z",
+ Updated: "2014-09-23T12:51:43Z",
+ MinDisk: 0,
+ MinRAM: 0,
+ Progress: 100,
+ Status: "ACTIVE",
+ },
+ }
+
+ if !reflect.DeepEqual(expected, actual) {
+ t.Errorf("Unexpected page contents: expected %#v, got %#v", expected, actual)
+ }
+
+ return false, nil
+ })
+
+ if err != nil {
+ t.Fatalf("EachPage error: %v", err)
+ }
+ if pages != 1 {
+ t.Errorf("Expected one page, got %d", pages)
+ }
+}
+
+func TestGetImage(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/images/12345678", 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")
+ fmt.Fprintf(w, `
+ {
+ "image": {
+ "status": "ACTIVE",
+ "updated": "2014-09-23T12:54:56Z",
+ "id": "f3e4a95d-1f4f-4989-97ce-f3a1fb8c04d7",
+ "OS-EXT-IMG-SIZE:size": 476704768,
+ "name": "F17-x86_64-cfntools",
+ "created": "2014-09-23T12:54:52Z",
+ "minDisk": 0,
+ "progress": 100,
+ "minRam": 0,
+ "metadata": {}
+ }
+ }
+ `)
+ })
+
+ actual, err := Get(fake.ServiceClient(), "12345678").Extract()
+ if err != nil {
+ t.Fatalf("Unexpected error from Get: %v", err)
+ }
+
+ expected := &Image{
+ Status: "ACTIVE",
+ Updated: "2014-09-23T12:54:56Z",
+ ID: "f3e4a95d-1f4f-4989-97ce-f3a1fb8c04d7",
+ Name: "F17-x86_64-cfntools",
+ Created: "2014-09-23T12:54:52Z",
+ MinDisk: 0,
+ Progress: 100,
+ MinRAM: 0,
+ }
+
+ if !reflect.DeepEqual(expected, actual) {
+ t.Errorf("Expected %#v, but got %#v", expected, actual)
+ }
+}
+
+func TestNextPageURL(t *testing.T) {
+ var page ImagePage
+ var body map[string]interface{}
+ bodyString := []byte(`{"images":{"links":[{"href":"http://192.154.23.87/12345/images/image3","rel":"next"},{"href":"http://192.154.23.87/12345/images/image1","rel":"previous"}]}}`)
+ err := json.Unmarshal(bodyString, &body)
+ if err != nil {
+ t.Fatalf("Error unmarshaling data into page body: %v", err)
+ }
+ page.Body = body
+
+ expected := "http://192.154.23.87/12345/images/image3"
+ actual, err := page.NextPageURL()
+ th.AssertNoErr(t, err)
+ th.CheckEquals(t, expected, actual)
+}
diff --git a/_site/openstack/compute/v2/images/results.go b/_site/openstack/compute/v2/images/results.go
new file mode 100644
index 0000000..3c22eeb
--- /dev/null
+++ b/_site/openstack/compute/v2/images/results.go
@@ -0,0 +1,90 @@
+package images
+
+import (
+ "github.com/mitchellh/mapstructure"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// GetResult temporarily stores a Get response.
+type GetResult struct {
+ gophercloud.CommonResult
+}
+
+// Extract interprets a GetResult as an Image.
+func (gr GetResult) Extract() (*Image, error) {
+ if gr.Err != nil {
+ return nil, gr.Err
+ }
+
+ var decoded struct {
+ Image Image `mapstructure:"image"`
+ }
+
+ err := mapstructure.Decode(gr.Resp, &decoded)
+ return &decoded.Image, err
+}
+
+// Image is used for JSON (un)marshalling.
+// It provides a description of an OS image.
+type Image struct {
+ // ID contains the image's unique identifier.
+ ID string
+
+ Created string
+
+ // MinDisk and MinRAM specify the minimum resources a server must provide to be able to install the image.
+ MinDisk int
+ MinRAM int
+
+ // Name provides a human-readable moniker for the OS image.
+ Name string
+
+ // The Progress and Status fields indicate image-creation status.
+ // Any usable image will have 100% progress.
+ Progress int
+ Status string
+
+ Updated string
+}
+
+// ImagePage contains a single page of results from a List operation.
+// Use ExtractImages to convert it into a slice of usable structs.
+type ImagePage struct {
+ pagination.LinkedPageBase
+}
+
+// IsEmpty returns true if a page contains no Image results.
+func (page ImagePage) IsEmpty() (bool, error) {
+ images, err := ExtractImages(page)
+ if err != nil {
+ return true, err
+ }
+ return len(images) == 0, nil
+}
+
+// NextPageURL uses the response's embedded link reference to navigate to the next page of results.
+func (page ImagePage) NextPageURL() (string, error) {
+ type resp struct {
+ Links []gophercloud.Link `mapstructure:"images_links"`
+ }
+
+ var r resp
+ err := mapstructure.Decode(page.Body, &r)
+ if err != nil {
+ return "", err
+ }
+
+ return gophercloud.ExtractNextURL(r.Links)
+}
+
+// ExtractImages converts a page of List results into a slice of usable Image structs.
+func ExtractImages(page pagination.Page) ([]Image, error) {
+ casted := page.(ImagePage).Body
+ var results struct {
+ Images []Image `mapstructure:"images"`
+ }
+
+ err := mapstructure.Decode(casted, &results)
+ return results.Images, err
+}
diff --git a/_site/openstack/compute/v2/images/urls.go b/_site/openstack/compute/v2/images/urls.go
new file mode 100644
index 0000000..9b3c86d
--- /dev/null
+++ b/_site/openstack/compute/v2/images/urls.go
@@ -0,0 +1,11 @@
+package images
+
+import "github.com/rackspace/gophercloud"
+
+func listDetailURL(client *gophercloud.ServiceClient) string {
+ return client.ServiceURL("images", "detail")
+}
+
+func getURL(client *gophercloud.ServiceClient, id string) string {
+ return client.ServiceURL("images", id)
+}
diff --git a/_site/openstack/compute/v2/images/urls_test.go b/_site/openstack/compute/v2/images/urls_test.go
new file mode 100644
index 0000000..b1ab3d6
--- /dev/null
+++ b/_site/openstack/compute/v2/images/urls_test.go
@@ -0,0 +1,26 @@
+package images
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+const endpoint = "http://localhost:57909/"
+
+func endpointClient() *gophercloud.ServiceClient {
+ return &gophercloud.ServiceClient{Endpoint: endpoint}
+}
+
+func TestGetURL(t *testing.T) {
+ actual := getURL(endpointClient(), "foo")
+ expected := endpoint + "images/foo"
+ th.CheckEquals(t, expected, actual)
+}
+
+func TestListDetailURL(t *testing.T) {
+ actual := listDetailURL(endpointClient())
+ expected := endpoint + "images/detail"
+ th.CheckEquals(t, expected, actual)
+}
diff --git a/_site/openstack/compute/v2/servers/data_test.go b/_site/openstack/compute/v2/servers/data_test.go
new file mode 100644
index 0000000..d3a0ee0
--- /dev/null
+++ b/_site/openstack/compute/v2/servers/data_test.go
@@ -0,0 +1,328 @@
+package servers
+
+// Recorded responses for the server resource.
+
+const (
+ serverListBody = `
+ {
+ "servers": [
+ {
+ "status": "ACTIVE",
+ "updated": "2014-09-25T13:10:10Z",
+ "hostId": "29d3c8c896a45aa4c34e52247875d7fefc3d94bbcc9f622b5d204362",
+ "OS-EXT-SRV-ATTR:host": "devstack",
+ "addresses": {
+ "private": [
+ {
+ "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:7c:1b:2b",
+ "version": 4,
+ "addr": "10.0.0.32",
+ "OS-EXT-IPS:type": "fixed"
+ }
+ ]
+ },
+ "links": [
+ {
+ "href": "http://104.130.131.164:8774/v2/fcad67a6189847c4aecfa3c81a05783b/servers/ef079b0c-e610-4dfb-b1aa-b49f07ac48e5",
+ "rel": "self"
+ },
+ {
+ "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/servers/ef079b0c-e610-4dfb-b1aa-b49f07ac48e5",
+ "rel": "bookmark"
+ }
+ ],
+ "key_name": null,
+ "image": {
+ "id": "f90f6034-2570-4974-8351-6b49732ef2eb",
+ "links": [
+ {
+ "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/images/f90f6034-2570-4974-8351-6b49732ef2eb",
+ "rel": "bookmark"
+ }
+ ]
+ },
+ "OS-EXT-STS:task_state": null,
+ "OS-EXT-STS:vm_state": "active",
+ "OS-EXT-SRV-ATTR:instance_name": "instance-0000001e",
+ "OS-SRV-USG:launched_at": "2014-09-25T13:10:10.000000",
+ "OS-EXT-SRV-ATTR:hypervisor_hostname": "devstack",
+ "flavor": {
+ "id": "1",
+ "links": [
+ {
+ "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/flavors/1",
+ "rel": "bookmark"
+ }
+ ]
+ },
+ "id": "ef079b0c-e610-4dfb-b1aa-b49f07ac48e5",
+ "security_groups": [
+ {
+ "name": "default"
+ }
+ ],
+ "OS-SRV-USG:terminated_at": null,
+ "OS-EXT-AZ:availability_zone": "nova",
+ "user_id": "9349aff8be7545ac9d2f1d00999a23cd",
+ "name": "herp",
+ "created": "2014-09-25T13:10:02Z",
+ "tenant_id": "fcad67a6189847c4aecfa3c81a05783b",
+ "OS-DCF:diskConfig": "MANUAL",
+ "os-extended-volumes:volumes_attached": [],
+ "accessIPv4": "",
+ "accessIPv6": "",
+ "progress": 0,
+ "OS-EXT-STS:power_state": 1,
+ "config_drive": "",
+ "metadata": {}
+ },
+ {
+ "status": "ACTIVE",
+ "updated": "2014-09-25T13:04:49Z",
+ "hostId": "29d3c8c896a45aa4c34e52247875d7fefc3d94bbcc9f622b5d204362",
+ "OS-EXT-SRV-ATTR:host": "devstack",
+ "addresses": {
+ "private": [
+ {
+ "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:9e:89:be",
+ "version": 4,
+ "addr": "10.0.0.31",
+ "OS-EXT-IPS:type": "fixed"
+ }
+ ]
+ },
+ "links": [
+ {
+ "href": "http://104.130.131.164:8774/v2/fcad67a6189847c4aecfa3c81a05783b/servers/9e5476bd-a4ec-4653-93d6-72c93aa682ba",
+ "rel": "self"
+ },
+ {
+ "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/servers/9e5476bd-a4ec-4653-93d6-72c93aa682ba",
+ "rel": "bookmark"
+ }
+ ],
+ "key_name": null,
+ "image": {
+ "id": "f90f6034-2570-4974-8351-6b49732ef2eb",
+ "links": [
+ {
+ "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/images/f90f6034-2570-4974-8351-6b49732ef2eb",
+ "rel": "bookmark"
+ }
+ ]
+ },
+ "OS-EXT-STS:task_state": null,
+ "OS-EXT-STS:vm_state": "active",
+ "OS-EXT-SRV-ATTR:instance_name": "instance-0000001d",
+ "OS-SRV-USG:launched_at": "2014-09-25T13:04:49.000000",
+ "OS-EXT-SRV-ATTR:hypervisor_hostname": "devstack",
+ "flavor": {
+ "id": "1",
+ "links": [
+ {
+ "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/flavors/1",
+ "rel": "bookmark"
+ }
+ ]
+ },
+ "id": "9e5476bd-a4ec-4653-93d6-72c93aa682ba",
+ "security_groups": [
+ {
+ "name": "default"
+ }
+ ],
+ "OS-SRV-USG:terminated_at": null,
+ "OS-EXT-AZ:availability_zone": "nova",
+ "user_id": "9349aff8be7545ac9d2f1d00999a23cd",
+ "name": "derp",
+ "created": "2014-09-25T13:04:41Z",
+ "tenant_id": "fcad67a6189847c4aecfa3c81a05783b",
+ "OS-DCF:diskConfig": "MANUAL",
+ "os-extended-volumes:volumes_attached": [],
+ "accessIPv4": "",
+ "accessIPv6": "",
+ "progress": 0,
+ "OS-EXT-STS:power_state": 1,
+ "config_drive": "",
+ "metadata": {}
+ }
+ ]
+ }
+ `
+
+ singleServerBody = `
+ {
+ "server": {
+ "status": "ACTIVE",
+ "updated": "2014-09-25T13:04:49Z",
+ "hostId": "29d3c8c896a45aa4c34e52247875d7fefc3d94bbcc9f622b5d204362",
+ "OS-EXT-SRV-ATTR:host": "devstack",
+ "addresses": {
+ "private": [
+ {
+ "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:9e:89:be",
+ "version": 4,
+ "addr": "10.0.0.31",
+ "OS-EXT-IPS:type": "fixed"
+ }
+ ]
+ },
+ "links": [
+ {
+ "href": "http://104.130.131.164:8774/v2/fcad67a6189847c4aecfa3c81a05783b/servers/9e5476bd-a4ec-4653-93d6-72c93aa682ba",
+ "rel": "self"
+ },
+ {
+ "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/servers/9e5476bd-a4ec-4653-93d6-72c93aa682ba",
+ "rel": "bookmark"
+ }
+ ],
+ "key_name": null,
+ "image": {
+ "id": "f90f6034-2570-4974-8351-6b49732ef2eb",
+ "links": [
+ {
+ "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/images/f90f6034-2570-4974-8351-6b49732ef2eb",
+ "rel": "bookmark"
+ }
+ ]
+ },
+ "OS-EXT-STS:task_state": null,
+ "OS-EXT-STS:vm_state": "active",
+ "OS-EXT-SRV-ATTR:instance_name": "instance-0000001d",
+ "OS-SRV-USG:launched_at": "2014-09-25T13:04:49.000000",
+ "OS-EXT-SRV-ATTR:hypervisor_hostname": "devstack",
+ "flavor": {
+ "id": "1",
+ "links": [
+ {
+ "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/flavors/1",
+ "rel": "bookmark"
+ }
+ ]
+ },
+ "id": "9e5476bd-a4ec-4653-93d6-72c93aa682ba",
+ "security_groups": [
+ {
+ "name": "default"
+ }
+ ],
+ "OS-SRV-USG:terminated_at": null,
+ "OS-EXT-AZ:availability_zone": "nova",
+ "user_id": "9349aff8be7545ac9d2f1d00999a23cd",
+ "name": "derp",
+ "created": "2014-09-25T13:04:41Z",
+ "tenant_id": "fcad67a6189847c4aecfa3c81a05783b",
+ "OS-DCF:diskConfig": "MANUAL",
+ "os-extended-volumes:volumes_attached": [],
+ "accessIPv4": "",
+ "accessIPv6": "",
+ "progress": 0,
+ "OS-EXT-STS:power_state": 1,
+ "config_drive": "",
+ "metadata": {}
+ }
+ }
+ `
+)
+
+var (
+ serverHerp = Server{
+ Status: "ACTIVE",
+ Updated: "2014-09-25T13:10:10Z",
+ HostID: "29d3c8c896a45aa4c34e52247875d7fefc3d94bbcc9f622b5d204362",
+ Addresses: map[string]interface{}{
+ "private": []interface{}{
+ map[string]interface{}{
+ "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:7c:1b:2b",
+ "version": float64(4),
+ "addr": "10.0.0.32",
+ "OS-EXT-IPS:type": "fixed",
+ },
+ },
+ },
+ Links: []interface{}{
+ map[string]interface{}{
+ "href": "http://104.130.131.164:8774/v2/fcad67a6189847c4aecfa3c81a05783b/servers/ef079b0c-e610-4dfb-b1aa-b49f07ac48e5",
+ "rel": "self",
+ },
+ map[string]interface{}{
+ "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/servers/ef079b0c-e610-4dfb-b1aa-b49f07ac48e5",
+ "rel": "bookmark",
+ },
+ },
+ Image: map[string]interface{}{
+ "id": "f90f6034-2570-4974-8351-6b49732ef2eb",
+ "links": []interface{}{
+ map[string]interface{}{
+ "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/images/f90f6034-2570-4974-8351-6b49732ef2eb",
+ "rel": "bookmark",
+ },
+ },
+ },
+ Flavor: map[string]interface{}{
+ "id": "1",
+ "links": []interface{}{
+ map[string]interface{}{
+ "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/flavors/1",
+ "rel": "bookmark",
+ },
+ },
+ },
+ ID: "ef079b0c-e610-4dfb-b1aa-b49f07ac48e5",
+ UserID: "9349aff8be7545ac9d2f1d00999a23cd",
+ Name: "herp",
+ Created: "2014-09-25T13:10:02Z",
+ TenantID: "fcad67a6189847c4aecfa3c81a05783b",
+ Metadata: map[string]interface{}{},
+ }
+ serverDerp = Server{
+ Status: "ACTIVE",
+ Updated: "2014-09-25T13:04:49Z",
+ HostID: "29d3c8c896a45aa4c34e52247875d7fefc3d94bbcc9f622b5d204362",
+ Addresses: map[string]interface{}{
+ "private": []interface{}{
+ map[string]interface{}{
+ "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:9e:89:be",
+ "version": float64(4),
+ "addr": "10.0.0.31",
+ "OS-EXT-IPS:type": "fixed",
+ },
+ },
+ },
+ Links: []interface{}{
+ map[string]interface{}{
+ "href": "http://104.130.131.164:8774/v2/fcad67a6189847c4aecfa3c81a05783b/servers/9e5476bd-a4ec-4653-93d6-72c93aa682ba",
+ "rel": "self",
+ },
+ map[string]interface{}{
+ "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/servers/9e5476bd-a4ec-4653-93d6-72c93aa682ba",
+ "rel": "bookmark",
+ },
+ },
+ Image: map[string]interface{}{
+ "id": "f90f6034-2570-4974-8351-6b49732ef2eb",
+ "links": []interface{}{
+ map[string]interface{}{
+ "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/images/f90f6034-2570-4974-8351-6b49732ef2eb",
+ "rel": "bookmark",
+ },
+ },
+ },
+ Flavor: map[string]interface{}{
+ "id": "1",
+ "links": []interface{}{
+ map[string]interface{}{
+ "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/flavors/1",
+ "rel": "bookmark",
+ },
+ },
+ },
+ ID: "9e5476bd-a4ec-4653-93d6-72c93aa682ba",
+ UserID: "9349aff8be7545ac9d2f1d00999a23cd",
+ Name: "derp",
+ Created: "2014-09-25T13:04:41Z",
+ TenantID: "fcad67a6189847c4aecfa3c81a05783b",
+ Metadata: map[string]interface{}{},
+ }
+)
diff --git a/_site/openstack/compute/v2/servers/doc.go b/_site/openstack/compute/v2/servers/doc.go
new file mode 100644
index 0000000..0a1791d
--- /dev/null
+++ b/_site/openstack/compute/v2/servers/doc.go
@@ -0,0 +1,3 @@
+// Package servers provides convenient access to standard, OpenStack-defined
+// compute services.
+package servers
diff --git a/_site/openstack/compute/v2/servers/requests.go b/_site/openstack/compute/v2/servers/requests.go
new file mode 100644
index 0000000..df622bf
--- /dev/null
+++ b/_site/openstack/compute/v2/servers/requests.go
@@ -0,0 +1,489 @@
+package servers
+
+import (
+ "encoding/base64"
+ "fmt"
+
+ "github.com/racker/perigee"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// ListOptsBuilder allows extensions to add additional parameters to the
+// List request.
+type ListOptsBuilder interface {
+ ToServerListQuery() (string, error)
+}
+
+// ListOpts allows the filtering and sorting of paginated collections through
+// the API. Filtering is achieved by passing in struct field values that map to
+// the server attributes you want to see returned. Marker and Limit are used
+// for pagination.
+type ListOpts struct {
+ // A time/date stamp for when the server last changed status.
+ ChangesSince string `q:"changes-since"`
+
+ // Name of the image in URL format.
+ Image string `q:"image"`
+
+ // Name of the flavor in URL format.
+ Flavor string `q:"flavor"`
+
+ // Name of the server as a string; can be queried with regular expressions.
+ // Realize that ?name=bob returns both bob and bobb. If you need to match bob
+ // only, you can use a regular expression matching the syntax of the
+ // underlying database server implemented for Compute.
+ Name string `q:"name"`
+
+ // Value of the status of the server so that you can filter on "ACTIVE" for example.
+ Status string `q:"status"`
+
+ // Name of the host as a string.
+ Host string `q:"host"`
+
+ // UUID of the server at which you want to set a marker.
+ Marker string `q:"marker"`
+
+ // Integer value for the limit of values to return.
+ Limit int `q:"limit"`
+}
+
+// ToServerListQuery formats a ListOpts into a query string.
+func (opts ListOpts) ToServerListQuery() (string, error) {
+ q, err := gophercloud.BuildQueryString(opts)
+ if err != nil {
+ return "", err
+ }
+ return q.String(), nil
+}
+
+// List makes a request against the API to list servers accessible to you.
+func List(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
+ url := listDetailURL(client)
+
+ if opts != nil {
+ query, err := opts.ToServerListQuery()
+ if err != nil {
+ return pagination.Pager{Err: err}
+ }
+ url += query
+ }
+
+ createPageFn := func(r pagination.LastHTTPResponse) pagination.Page {
+ return ServerPage{pagination.LinkedPageBase{LastHTTPResponse: r}}
+ }
+
+ return pagination.NewPager(client, url, createPageFn)
+}
+
+// CreateOptsBuilder describes struct types that can be accepted by the Create call.
+// The CreateOpts struct in this package does.
+type CreateOptsBuilder interface {
+ ToServerCreateMap() map[string]interface{}
+}
+
+// Network is used within CreateOpts to control a new server's network attachments.
+type Network struct {
+ // UUID of a nova-network to attach to the newly provisioned server.
+ // Required unless Port is provided.
+ UUID string
+
+ // Port of a neutron network to attach to the newly provisioned server.
+ // Required unless UUID is provided.
+ Port string
+
+ // FixedIP [optional] specifies a fixed IPv4 address to be used on this network.
+ FixedIP string
+}
+
+// CreateOpts specifies server creation parameters.
+type CreateOpts struct {
+ // Name [required] is the name to assign to the newly launched server.
+ Name string
+
+ // ImageRef [required] is the ID or full URL to the image that contains the server's OS and initial state.
+ // Optional if using the boot-from-volume extension.
+ ImageRef string
+
+ // FlavorRef [required] is the ID or full URL to the flavor that describes the server's specs.
+ FlavorRef string
+
+ // SecurityGroups [optional] lists the names of the security groups to which this server should belong.
+ SecurityGroups []string
+
+ // UserData [optional] contains configuration information or scripts to use upon launch.
+ // Create will base64-encode it for you.
+ UserData []byte
+
+ // AvailabilityZone [optional] in which to launch the server.
+ AvailabilityZone string
+
+ // Networks [optional] dictates how this server will be attached to available networks.
+ // By default, the server will be attached to all isolated networks for the tenant.
+ Networks []Network
+
+ // Metadata [optional] contains key-value pairs (up to 255 bytes each) to attach to the server.
+ Metadata map[string]string
+
+ // Personality [optional] includes the path and contents of a file to inject into the server at launch.
+ // The maximum size of the file is 255 bytes (decoded).
+ Personality []byte
+
+ // ConfigDrive [optional] enables metadata injection through a configuration drive.
+ ConfigDrive bool
+}
+
+// ToServerCreateMap assembles a request body based on the contents of a CreateOpts.
+func (opts CreateOpts) ToServerCreateMap() map[string]interface{} {
+ server := make(map[string]interface{})
+
+ server["name"] = opts.Name
+ server["imageRef"] = opts.ImageRef
+ server["flavorRef"] = opts.FlavorRef
+
+ if opts.UserData != nil {
+ encoded := base64.StdEncoding.EncodeToString(opts.UserData)
+ server["user_data"] = &encoded
+ }
+ if opts.Personality != nil {
+ encoded := base64.StdEncoding.EncodeToString(opts.Personality)
+ server["personality"] = &encoded
+ }
+ if opts.ConfigDrive {
+ server["config_drive"] = "true"
+ }
+ if opts.AvailabilityZone != "" {
+ server["availability_zone"] = opts.AvailabilityZone
+ }
+ if opts.Metadata != nil {
+ server["metadata"] = opts.Metadata
+ }
+
+ if len(opts.SecurityGroups) > 0 {
+ securityGroups := make([]map[string]interface{}, len(opts.SecurityGroups))
+ for i, groupName := range opts.SecurityGroups {
+ securityGroups[i] = map[string]interface{}{"name": groupName}
+ }
+ }
+ if len(opts.Networks) > 0 {
+ networks := make([]map[string]interface{}, len(opts.Networks))
+ for i, net := range opts.Networks {
+ networks[i] = make(map[string]interface{})
+ if net.UUID != "" {
+ networks[i]["uuid"] = net.UUID
+ }
+ if net.Port != "" {
+ networks[i]["port"] = net.Port
+ }
+ if net.FixedIP != "" {
+ networks[i]["fixed_ip"] = net.FixedIP
+ }
+ }
+ }
+
+ return map[string]interface{}{"server": server}
+}
+
+// Create requests a server to be provisioned to the user in the current tenant.
+func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult {
+ var result CreateResult
+ _, result.Err = perigee.Request("POST", listURL(client), perigee.Options{
+ Results: &result.Resp,
+ ReqBody: opts.ToServerCreateMap(),
+ MoreHeaders: client.Provider.AuthenticatedHeaders(),
+ OkCodes: []int{202},
+ })
+ return result
+}
+
+// Delete requests that a server previously provisioned be removed from your account.
+func Delete(client *gophercloud.ServiceClient, id string) error {
+ _, err := perigee.Request("DELETE", deleteURL(client, id), perigee.Options{
+ MoreHeaders: client.Provider.AuthenticatedHeaders(),
+ OkCodes: []int{204},
+ })
+ return err
+}
+
+// Get requests details on a single server, by ID.
+func Get(client *gophercloud.ServiceClient, id string) GetResult {
+ var result GetResult
+ _, result.Err = perigee.Request("GET", getURL(client, id), perigee.Options{
+ Results: &result.Resp,
+ MoreHeaders: client.Provider.AuthenticatedHeaders(),
+ })
+ return result
+}
+
+// UpdateOptsBuilder allows extentions to add additional attributes to the Update request.
+type UpdateOptsBuilder interface {
+ ToServerUpdateMap() map[string]interface{}
+}
+
+// UpdateOpts specifies the base attributes that may be updated on an existing server.
+type UpdateOpts struct {
+ // Name [optional] changes the displayed name of the server.
+ // The server host name will *not* change.
+ // Server names are not constrained to be unique, even within the same tenant.
+ Name string
+
+ // AccessIPv4 [optional] provides a new IPv4 address for the instance.
+ AccessIPv4 string
+
+ // AccessIPv6 [optional] provides a new IPv6 address for the instance.
+ AccessIPv6 string
+}
+
+// ToServerUpdateMap formats an UpdateOpts structure into a request body.
+func (opts UpdateOpts) ToServerUpdateMap() map[string]interface{} {
+ server := make(map[string]string)
+ if opts.Name != "" {
+ server["name"] = opts.Name
+ }
+ if opts.AccessIPv4 != "" {
+ server["accessIPv4"] = opts.AccessIPv4
+ }
+ if opts.AccessIPv6 != "" {
+ server["accessIPv6"] = opts.AccessIPv6
+ }
+ return map[string]interface{}{"server": server}
+}
+
+// Update requests that various attributes of the indicated server be changed.
+func Update(client *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) UpdateResult {
+ var result UpdateResult
+ _, result.Err = perigee.Request("PUT", updateURL(client, id), perigee.Options{
+ Results: &result.Resp,
+ ReqBody: opts.ToServerUpdateMap(),
+ MoreHeaders: client.Provider.AuthenticatedHeaders(),
+ })
+ return result
+}
+
+// ChangeAdminPassword alters the administrator or root password for a specified server.
+func ChangeAdminPassword(client *gophercloud.ServiceClient, id, newPassword string) error {
+ var req struct {
+ ChangePassword struct {
+ AdminPass string `json:"adminPass"`
+ } `json:"changePassword"`
+ }
+
+ req.ChangePassword.AdminPass = newPassword
+
+ _, err := perigee.Request("POST", actionURL(client, id), perigee.Options{
+ ReqBody: req,
+ MoreHeaders: client.Provider.AuthenticatedHeaders(),
+ OkCodes: []int{202},
+ })
+ return err
+}
+
+// ErrArgument errors occur when an argument supplied to a package function
+// fails to fall within acceptable values. For example, the Reboot() function
+// expects the "how" parameter to be one of HardReboot or SoftReboot. These
+// constants are (currently) strings, leading someone to wonder if they can pass
+// other string values instead, perhaps in an effort to break the API of their
+// provider. Reboot() returns this error in this situation.
+//
+// Function identifies which function was called/which function is generating
+// the error.
+// Argument identifies which formal argument was responsible for producing the
+// error.
+// Value provides the value as it was passed into the function.
+type ErrArgument struct {
+ Function, Argument string
+ Value interface{}
+}
+
+// Error yields a useful diagnostic for debugging purposes.
+func (e *ErrArgument) Error() string {
+ return fmt.Sprintf("Bad argument in call to %s, formal parameter %s, value %#v", e.Function, e.Argument, e.Value)
+}
+
+func (e *ErrArgument) String() string {
+ return e.Error()
+}
+
+// RebootMethod describes the mechanisms by which a server reboot can be requested.
+type RebootMethod string
+
+// These constants determine how a server should be rebooted.
+// See the Reboot() function for further details.
+const (
+ SoftReboot RebootMethod = "SOFT"
+ HardReboot RebootMethod = "HARD"
+ OSReboot = SoftReboot
+ PowerCycle = HardReboot
+)
+
+// Reboot requests that a given server reboot.
+// Two methods exist for rebooting a server:
+//
+// HardReboot (aka PowerCycle) restarts the server instance by physically cutting power to the machine, or if a VM,
+// terminating it at the hypervisor level.
+// It's done. Caput. Full stop.
+// Then, after a brief while, power is restored or the VM instance restarted.
+//
+// SoftReboot (aka OSReboot) simply tells the OS to restart under its own procedures.
+// E.g., in Linux, asking it to enter runlevel 6, or executing "sudo shutdown -r now", or by asking Windows to restart the machine.
+func Reboot(client *gophercloud.ServiceClient, id string, how RebootMethod) error {
+ if (how != SoftReboot) && (how != HardReboot) {
+ return &ErrArgument{
+ Function: "Reboot",
+ Argument: "how",
+ Value: how,
+ }
+ }
+
+ _, err := perigee.Request("POST", actionURL(client, id), perigee.Options{
+ ReqBody: struct {
+ C map[string]string `json:"reboot"`
+ }{
+ map[string]string{"type": string(how)},
+ },
+ MoreHeaders: client.Provider.AuthenticatedHeaders(),
+ OkCodes: []int{202},
+ })
+ return err
+}
+
+// RebuildOptsBuilder is an interface that allows extensions to override the
+// default behaviour of rebuild options
+type RebuildOptsBuilder interface {
+ ToServerRebuildMap() (map[string]interface{}, error)
+}
+
+// RebuildOpts represents the configuration options used in a server rebuild
+// operation
+type RebuildOpts struct {
+ // Required. The ID of the image you want your server to be provisioned on
+ ImageID string
+
+ // Name to set the server to
+ Name string
+
+ // Required. The server's admin password
+ AdminPass string
+
+ // AccessIPv4 [optional] provides a new IPv4 address for the instance.
+ AccessIPv4 string
+
+ // AccessIPv6 [optional] provides a new IPv6 address for the instance.
+ AccessIPv6 string
+
+ // Metadata [optional] contains key-value pairs (up to 255 bytes each) to attach to the server.
+ Metadata map[string]string
+
+ // Personality [optional] includes the path and contents of a file to inject into the server at launch.
+ // The maximum size of the file is 255 bytes (decoded).
+ Personality []byte
+}
+
+// ToServerRebuildMap formats a RebuildOpts struct into a map for use in JSON
+func (opts RebuildOpts) ToServerRebuildMap() (map[string]interface{}, error) {
+ var err error
+ server := make(map[string]interface{})
+
+ if opts.AdminPass == "" {
+ err = fmt.Errorf("AdminPass is required")
+ }
+
+ if opts.ImageID == "" {
+ err = fmt.Errorf("ImageID is required")
+ }
+
+ if err != nil {
+ return server, err
+ }
+
+ server["name"] = opts.Name
+ server["adminPass"] = opts.AdminPass
+ server["imageRef"] = opts.ImageID
+
+ if opts.AccessIPv4 != "" {
+ server["accessIPv4"] = opts.AccessIPv4
+ }
+
+ if opts.AccessIPv6 != "" {
+ server["accessIPv6"] = opts.AccessIPv6
+ }
+
+ if opts.Metadata != nil {
+ server["metadata"] = opts.Metadata
+ }
+
+ if opts.Personality != nil {
+ encoded := base64.StdEncoding.EncodeToString(opts.Personality)
+ server["personality"] = &encoded
+ }
+
+ return map[string]interface{}{"rebuild": server}, nil
+}
+
+// Rebuild will reprovision the server according to the configuration options
+// provided in the RebuildOpts struct.
+func Rebuild(client *gophercloud.ServiceClient, id string, opts RebuildOptsBuilder) RebuildResult {
+ var result RebuildResult
+
+ if id == "" {
+ result.Err = fmt.Errorf("ID is required")
+ return result
+ }
+
+ reqBody, err := opts.ToServerRebuildMap()
+ if err != nil {
+ result.Err = err
+ return result
+ }
+
+ _, result.Err = perigee.Request("POST", actionURL(client, id), perigee.Options{
+ ReqBody: &reqBody,
+ Results: &result.Resp,
+ MoreHeaders: client.Provider.AuthenticatedHeaders(),
+ OkCodes: []int{202},
+ })
+
+ return result
+}
+
+// Resize instructs the provider to change the flavor of the server.
+// Note that this implies rebuilding it.
+// Unfortunately, one cannot pass rebuild parameters to the resize function.
+// When the resize completes, the server will be in RESIZE_VERIFY state.
+// While in this state, you can explore the use of the new server's configuration.
+// If you like it, call ConfirmResize() to commit the resize permanently.
+// Otherwise, call RevertResize() to restore the old configuration.
+func Resize(client *gophercloud.ServiceClient, id, flavorRef string) error {
+ _, err := perigee.Request("POST", actionURL(client, id), perigee.Options{
+ ReqBody: struct {
+ R map[string]interface{} `json:"resize"`
+ }{
+ map[string]interface{}{"flavorRef": flavorRef},
+ },
+ MoreHeaders: client.Provider.AuthenticatedHeaders(),
+ OkCodes: []int{202},
+ })
+ return err
+}
+
+// ConfirmResize confirms a previous resize operation on a server.
+// See Resize() for more details.
+func ConfirmResize(client *gophercloud.ServiceClient, id string) error {
+ _, err := perigee.Request("POST", actionURL(client, id), perigee.Options{
+ ReqBody: map[string]interface{}{"confirmResize": nil},
+ MoreHeaders: client.Provider.AuthenticatedHeaders(),
+ OkCodes: []int{204},
+ })
+ return err
+}
+
+// RevertResize cancels a previous resize operation on a server.
+// See Resize() for more details.
+func RevertResize(client *gophercloud.ServiceClient, id string) error {
+ _, err := perigee.Request("POST", actionURL(client, id), perigee.Options{
+ ReqBody: map[string]interface{}{"revertResize": nil},
+ MoreHeaders: client.Provider.AuthenticatedHeaders(),
+ OkCodes: []int{202},
+ })
+ return err
+}
diff --git a/_site/openstack/compute/v2/servers/requests_test.go b/_site/openstack/compute/v2/servers/requests_test.go
new file mode 100644
index 0000000..86fe1e2
--- /dev/null
+++ b/_site/openstack/compute/v2/servers/requests_test.go
@@ -0,0 +1,283 @@
+package servers
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ "github.com/rackspace/gophercloud/pagination"
+ "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestListServers(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ testhelper.Mux.HandleFunc("/servers/detail", func(w http.ResponseWriter, r *http.Request) {
+ testhelper.TestMethod(t, r, "GET")
+ testhelper.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+ r.ParseForm()
+ marker := r.Form.Get("marker")
+ switch marker {
+ case "":
+ fmt.Fprintf(w, serverListBody)
+ case "9e5476bd-a4ec-4653-93d6-72c93aa682ba":
+ fmt.Fprintf(w, `{ "servers": [] }`)
+ default:
+ t.Fatalf("/servers/detail invoked with unexpected marker=[%s]", marker)
+ }
+ })
+
+ pages := 0
+ err := List(fake.ServiceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+ pages++
+
+ actual, err := ExtractServers(page)
+ if err != nil {
+ return false, err
+ }
+
+ if len(actual) != 2 {
+ t.Fatalf("Expected 2 servers, got %d", len(actual))
+ }
+ equalServers(t, serverHerp, actual[0])
+ equalServers(t, serverDerp, actual[1])
+
+ return true, nil
+ })
+
+ testhelper.AssertNoErr(t, err)
+
+ if pages != 1 {
+ t.Errorf("Expected 1 page, saw %d", pages)
+ }
+}
+
+func TestCreateServer(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ testhelper.Mux.HandleFunc("/servers", func(w http.ResponseWriter, r *http.Request) {
+ testhelper.TestMethod(t, r, "POST")
+ testhelper.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ testhelper.TestJSONRequest(t, r, `{
+ "server": {
+ "name": "derp",
+ "imageRef": "f90f6034-2570-4974-8351-6b49732ef2eb",
+ "flavorRef": "1"
+ }
+ }`)
+
+ w.WriteHeader(http.StatusAccepted)
+ w.Header().Add("Content-Type", "application/json")
+ fmt.Fprintf(w, singleServerBody)
+ })
+
+ client := fake.ServiceClient()
+ actual, err := Create(client, CreateOpts{
+ Name: "derp",
+ ImageRef: "f90f6034-2570-4974-8351-6b49732ef2eb",
+ FlavorRef: "1",
+ }).Extract()
+ if err != nil {
+ t.Fatalf("Unexpected Create error: %v", err)
+ }
+
+ equalServers(t, serverDerp, *actual)
+}
+
+func TestDeleteServer(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ testhelper.Mux.HandleFunc("/servers/asdfasdfasdf", func(w http.ResponseWriter, r *http.Request) {
+ testhelper.TestMethod(t, r, "DELETE")
+ testhelper.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+ w.WriteHeader(http.StatusNoContent)
+ })
+
+ client := fake.ServiceClient()
+ err := Delete(client, "asdfasdfasdf")
+ if err != nil {
+ t.Fatalf("Unexpected Delete error: %v", err)
+ }
+}
+
+func TestGetServer(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ testhelper.Mux.HandleFunc("/servers/1234asdf", func(w http.ResponseWriter, r *http.Request) {
+ testhelper.TestMethod(t, r, "GET")
+ testhelper.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ testhelper.TestHeader(t, r, "Accept", "application/json")
+
+ fmt.Fprintf(w, singleServerBody)
+ })
+
+ client := fake.ServiceClient()
+ actual, err := Get(client, "1234asdf").Extract()
+ if err != nil {
+ t.Fatalf("Unexpected Get error: %v", err)
+ }
+
+ equalServers(t, serverDerp, *actual)
+}
+
+func TestUpdateServer(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ testhelper.Mux.HandleFunc("/servers/1234asdf", func(w http.ResponseWriter, r *http.Request) {
+ testhelper.TestMethod(t, r, "PUT")
+ testhelper.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ testhelper.TestHeader(t, r, "Accept", "application/json")
+ testhelper.TestHeader(t, r, "Content-Type", "application/json")
+ testhelper.TestJSONRequest(t, r, `{ "server": { "name": "new-name" } }`)
+
+ fmt.Fprintf(w, singleServerBody)
+ })
+
+ client := fake.ServiceClient()
+ actual, err := Update(client, "1234asdf", UpdateOpts{Name: "new-name"}).Extract()
+ if err != nil {
+ t.Fatalf("Unexpected Update error: %v", err)
+ }
+
+ equalServers(t, serverDerp, *actual)
+}
+
+func TestChangeServerAdminPassword(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ testhelper.Mux.HandleFunc("/servers/1234asdf/action", func(w http.ResponseWriter, r *http.Request) {
+ testhelper.TestMethod(t, r, "POST")
+ testhelper.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ testhelper.TestJSONRequest(t, r, `{ "changePassword": { "adminPass": "new-password" } }`)
+
+ w.WriteHeader(http.StatusAccepted)
+ })
+
+ client := fake.ServiceClient()
+ err := ChangeAdminPassword(client, "1234asdf", "new-password")
+ if err != nil {
+ t.Errorf("Unexpected ChangeAdminPassword error: %v", err)
+ }
+}
+
+func TestRebootServer(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ testhelper.Mux.HandleFunc("/servers/1234asdf/action", func(w http.ResponseWriter, r *http.Request) {
+ testhelper.TestMethod(t, r, "POST")
+ testhelper.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ testhelper.TestJSONRequest(t, r, `{ "reboot": { "type": "SOFT" } }`)
+
+ w.WriteHeader(http.StatusAccepted)
+ })
+
+ client := fake.ServiceClient()
+ err := Reboot(client, "1234asdf", SoftReboot)
+ if err != nil {
+ t.Errorf("Unexpected Reboot error: %v", err)
+ }
+}
+
+func TestRebuildServer(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ testhelper.Mux.HandleFunc("/servers/1234asdf/action", func(w http.ResponseWriter, r *http.Request) {
+ testhelper.TestMethod(t, r, "POST")
+ testhelper.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ testhelper.TestJSONRequest(t, r, `
+ {
+ "rebuild": {
+ "name": "new-name",
+ "adminPass": "swordfish",
+ "imageRef": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/images/f90f6034-2570-4974-8351-6b49732ef2eb",
+ "accessIPv4": "1.2.3.4"
+ }
+ }
+ `)
+
+ w.WriteHeader(http.StatusAccepted)
+ w.Header().Add("Content-Type", "application/json")
+ fmt.Fprintf(w, singleServerBody)
+ })
+
+ opts := RebuildOpts{
+ Name: "new-name",
+ AdminPass: "swordfish",
+ ImageID: "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/images/f90f6034-2570-4974-8351-6b49732ef2eb",
+ AccessIPv4: "1.2.3.4",
+ }
+
+ actual, err := Rebuild(serviceClient(), "1234asdf", opts).Extract()
+ testhelper.AssertNoErr(t, err)
+
+ equalServers(t, serverDerp, *actual)
+}
+
+func TestResizeServer(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ testhelper.Mux.HandleFunc("/servers/1234asdf/action", func(w http.ResponseWriter, r *http.Request) {
+ testhelper.TestMethod(t, r, "POST")
+ testhelper.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ testhelper.TestJSONRequest(t, r, `{ "resize": { "flavorRef": "2" } }`)
+
+ w.WriteHeader(http.StatusAccepted)
+ })
+
+ client := fake.ServiceClient()
+ err := Resize(client, "1234asdf", "2")
+ if err != nil {
+ t.Errorf("Unexpected Reboot error: %v", err)
+ }
+}
+
+func TestConfirmResize(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ testhelper.Mux.HandleFunc("/servers/1234asdf/action", func(w http.ResponseWriter, r *http.Request) {
+ testhelper.TestMethod(t, r, "POST")
+ testhelper.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ testhelper.TestJSONRequest(t, r, `{ "confirmResize": null }`)
+
+ w.WriteHeader(http.StatusNoContent)
+ })
+
+ client := fake.ServiceClient()
+ err := ConfirmResize(client, "1234asdf")
+ if err != nil {
+ t.Errorf("Unexpected ConfirmResize error: %v", err)
+ }
+}
+
+func TestRevertResize(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ testhelper.Mux.HandleFunc("/servers/1234asdf/action", func(w http.ResponseWriter, r *http.Request) {
+ testhelper.TestMethod(t, r, "POST")
+ testhelper.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ testhelper.TestJSONRequest(t, r, `{ "revertResize": null }`)
+
+ w.WriteHeader(http.StatusAccepted)
+ })
+
+ client := fake.ServiceClient()
+ err := RevertResize(client, "1234asdf")
+ if err != nil {
+ t.Errorf("Unexpected RevertResize error: %v", err)
+ }
+}
diff --git a/_site/openstack/compute/v2/servers/results.go b/_site/openstack/compute/v2/servers/results.go
new file mode 100644
index 0000000..d284ed8
--- /dev/null
+++ b/_site/openstack/compute/v2/servers/results.go
@@ -0,0 +1,141 @@
+package servers
+
+import (
+ "github.com/mitchellh/mapstructure"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+type serverResult struct {
+ gophercloud.CommonResult
+}
+
+// Extract interprets any serverResult as a Server, if possible.
+func (r serverResult) Extract() (*Server, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var response struct {
+ Server Server `mapstructure:"server"`
+ }
+
+ err := mapstructure.Decode(r.Resp, &response)
+ return &response.Server, err
+}
+
+// CreateResult temporarily contains the response from a Create call.
+type CreateResult struct {
+ serverResult
+}
+
+// GetResult temporarily contains the response from a Get call.
+type GetResult struct {
+ serverResult
+}
+
+// UpdateResult temporarily contains the response from an Update call.
+type UpdateResult struct {
+ serverResult
+}
+
+// RebuildResult temporarily contains the response from a Rebuild call.
+type RebuildResult struct {
+ serverResult
+}
+
+// Server exposes only the standard OpenStack fields corresponding to a given server on the user's account.
+type Server struct {
+ // ID uniquely identifies this server amongst all other servers, including those not accessible to the current tenant.
+ ID string
+
+ // TenantID identifies the tenant owning this server resource.
+ TenantID string `mapstructure:"tenant_id"`
+
+ // UserID uniquely identifies the user account owning the tenant.
+ UserID string `mapstructure:"user_id"`
+
+ // Name contains the human-readable name for the server.
+ Name string
+
+ // Updated and Created contain ISO-8601 timestamps of when the state of the server last changed, and when it was created.
+ Updated string
+ Created string
+
+ HostID string
+
+ // Status contains the current operational status of the server, such as IN_PROGRESS or ACTIVE.
+ Status string
+
+ // Progress ranges from 0..100.
+ // A request made against the server completes only once Progress reaches 100.
+ Progress int
+
+ // AccessIPv4 and AccessIPv6 contain the IP addresses of the server, suitable for remote access for administration.
+ AccessIPv4 string
+ AccessIPv6 string
+
+ // Image refers to a JSON object, which itself indicates the OS image used to deploy the server.
+ Image map[string]interface{}
+
+ // Flavor refers to a JSON object, which itself indicates the hardware configuration of the deployed server.
+ Flavor map[string]interface{}
+
+ // Addresses includes a list of all IP addresses assigned to the server, keyed by pool.
+ Addresses map[string]interface{}
+
+ // Metadata includes a list of all user-specified key-value pairs attached to the server.
+ Metadata map[string]interface{}
+
+ // Links includes HTTP references to the itself, useful for passing along to other APIs that might want a server reference.
+ Links []interface{}
+
+ // KeyName indicates which public key was injected into the server on launch.
+ KeyName string `mapstructure:"keyname"`
+
+ // AdminPass will generally be empty (""). However, it will contain the administrative password chosen when provisioning a new server without a set AdminPass setting in the first place.
+ // Note that this is the ONLY time this field will be valid.
+ AdminPass string `mapstructure:"adminPass"`
+}
+
+// ServerPage abstracts the raw results of making a List() request against the API.
+// As OpenStack extensions may freely alter the response bodies of structures returned to the client, you may only safely access the
+// data provided through the ExtractServers call.
+type ServerPage struct {
+ pagination.LinkedPageBase
+}
+
+// IsEmpty returns true if a page contains no Server results.
+func (page ServerPage) IsEmpty() (bool, error) {
+ servers, err := ExtractServers(page)
+ if err != nil {
+ return true, err
+ }
+ return len(servers) == 0, nil
+}
+
+// NextPageURL uses the response's embedded link reference to navigate to the next page of results.
+func (page ServerPage) NextPageURL() (string, error) {
+ type resp struct {
+ Links []gophercloud.Link `mapstructure:"servers_links"`
+ }
+
+ var r resp
+ err := mapstructure.Decode(page.Body, &r)
+ if err != nil {
+ return "", err
+ }
+
+ return gophercloud.ExtractNextURL(r.Links)
+}
+
+// ExtractServers interprets the results of a single page from a List() call, producing a slice of Server entities.
+func ExtractServers(page pagination.Page) ([]Server, error) {
+ casted := page.(ServerPage).Body
+
+ var response struct {
+ Servers []Server `mapstructure:"servers"`
+ }
+ err := mapstructure.Decode(casted, &response)
+ return response.Servers, err
+}
diff --git a/_site/openstack/compute/v2/servers/servers_test.go b/_site/openstack/compute/v2/servers/servers_test.go
new file mode 100644
index 0000000..590fc8b
--- /dev/null
+++ b/_site/openstack/compute/v2/servers/servers_test.go
@@ -0,0 +1,65 @@
+package servers
+
+import (
+ "reflect"
+ "testing"
+)
+
+// This provides more fine-grained failures when Servers differ, because Server structs are too damn big to compare by eye.
+// FIXME I should absolutely refactor this into a general-purpose thing in testhelper.
+func equalServers(t *testing.T, expected Server, actual Server) {
+ if expected.ID != actual.ID {
+ t.Errorf("ID differs. expected=[%s], actual=[%s]", expected.ID, actual.ID)
+ }
+ if expected.TenantID != actual.TenantID {
+ t.Errorf("TenantID differs. expected=[%s], actual=[%s]", expected.TenantID, actual.TenantID)
+ }
+ if expected.UserID != actual.UserID {
+ t.Errorf("UserID differs. expected=[%s], actual=[%s]", expected.UserID, actual.UserID)
+ }
+ if expected.Name != actual.Name {
+ t.Errorf("Name differs. expected=[%s], actual=[%s]", expected.Name, actual.Name)
+ }
+ if expected.Updated != actual.Updated {
+ t.Errorf("Updated differs. expected=[%s], actual=[%s]", expected.Updated, actual.Updated)
+ }
+ if expected.Created != actual.Created {
+ t.Errorf("Created differs. expected=[%s], actual=[%s]", expected.Created, actual.Created)
+ }
+ if expected.HostID != actual.HostID {
+ t.Errorf("HostID differs. expected=[%s], actual=[%s]", expected.HostID, actual.HostID)
+ }
+ if expected.Status != actual.Status {
+ t.Errorf("Status differs. expected=[%s], actual=[%s]", expected.Status, actual.Status)
+ }
+ if expected.Progress != actual.Progress {
+ t.Errorf("Progress differs. expected=[%s], actual=[%s]", expected.Progress, actual.Progress)
+ }
+ if expected.AccessIPv4 != actual.AccessIPv4 {
+ t.Errorf("AccessIPv4 differs. expected=[%s], actual=[%s]", expected.AccessIPv4, actual.AccessIPv4)
+ }
+ if expected.AccessIPv6 != actual.AccessIPv6 {
+ t.Errorf("AccessIPv6 differs. expected=[%s], actual=[%s]", expected.AccessIPv6, actual.AccessIPv6)
+ }
+ if !reflect.DeepEqual(expected.Image, actual.Image) {
+ t.Errorf("Image differs. expected=[%s], actual=[%s]", expected.Image, actual.Image)
+ }
+ if !reflect.DeepEqual(expected.Flavor, actual.Flavor) {
+ t.Errorf("Flavor differs. expected=[%s], actual=[%s]", expected.Flavor, actual.Flavor)
+ }
+ if !reflect.DeepEqual(expected.Addresses, actual.Addresses) {
+ t.Errorf("Addresses differ. expected=[%s], actual=[%s]", expected.Addresses, actual.Addresses)
+ }
+ if !reflect.DeepEqual(expected.Metadata, actual.Metadata) {
+ t.Errorf("Metadata differs. expected=[%s], actual=[%s]", expected.Metadata, actual.Metadata)
+ }
+ if !reflect.DeepEqual(expected.Links, actual.Links) {
+ t.Errorf("Links differs. expected=[%s], actual=[%s]", expected.Links, actual.Links)
+ }
+ if expected.KeyName != actual.KeyName {
+ t.Errorf("KeyName differs. expected=[%s], actual=[%s]", expected.KeyName, actual.KeyName)
+ }
+ if expected.AdminPass != actual.AdminPass {
+ t.Errorf("AdminPass differs. expected=[%s], actual=[%s]", expected.AdminPass, actual.AdminPass)
+ }
+}
diff --git a/_site/openstack/compute/v2/servers/urls.go b/_site/openstack/compute/v2/servers/urls.go
new file mode 100644
index 0000000..57587ab
--- /dev/null
+++ b/_site/openstack/compute/v2/servers/urls.go
@@ -0,0 +1,31 @@
+package servers
+
+import "github.com/rackspace/gophercloud"
+
+func createURL(client *gophercloud.ServiceClient) string {
+ return client.ServiceURL("servers")
+}
+
+func listURL(client *gophercloud.ServiceClient) string {
+ return createURL(client)
+}
+
+func listDetailURL(client *gophercloud.ServiceClient) string {
+ return client.ServiceURL("servers", "detail")
+}
+
+func deleteURL(client *gophercloud.ServiceClient, id string) string {
+ return client.ServiceURL("servers", id)
+}
+
+func getURL(client *gophercloud.ServiceClient, id string) string {
+ return deleteURL(client, id)
+}
+
+func updateURL(client *gophercloud.ServiceClient, id string) string {
+ return deleteURL(client, id)
+}
+
+func actionURL(client *gophercloud.ServiceClient, id string) string {
+ return client.ServiceURL("servers", id, "action")
+}
diff --git a/_site/openstack/compute/v2/servers/urls_test.go b/_site/openstack/compute/v2/servers/urls_test.go
new file mode 100644
index 0000000..cc895c9
--- /dev/null
+++ b/_site/openstack/compute/v2/servers/urls_test.go
@@ -0,0 +1,56 @@
+package servers
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+const endpoint = "http://localhost:57909"
+
+func endpointClient() *gophercloud.ServiceClient {
+ return &gophercloud.ServiceClient{Endpoint: endpoint}
+}
+
+func TestCreateURL(t *testing.T) {
+ actual := createURL(endpointClient())
+ expected := endpoint + "servers"
+ th.CheckEquals(t, expected, actual)
+}
+
+func TestListURL(t *testing.T) {
+ actual := listURL(endpointClient())
+ expected := endpoint + "servers"
+ th.CheckEquals(t, expected, actual)
+}
+
+func TestListDetailURL(t *testing.T) {
+ actual := listDetailURL(endpointClient())
+ expected := endpoint + "servers/detail"
+ th.CheckEquals(t, expected, actual)
+}
+
+func TestDeleteURL(t *testing.T) {
+ actual := deleteURL(endpointClient(), "foo")
+ expected := endpoint + "servers/foo"
+ th.CheckEquals(t, expected, actual)
+}
+
+func TestGetURL(t *testing.T) {
+ actual := getURL(endpointClient(), "foo")
+ expected := endpoint + "servers/foo"
+ th.CheckEquals(t, expected, actual)
+}
+
+func TestUpdateURL(t *testing.T) {
+ actual := updateURL(endpointClient(), "foo")
+ expected := endpoint + "servers/foo"
+ th.CheckEquals(t, expected, actual)
+}
+
+func TestActionURL(t *testing.T) {
+ actual := actionURL(endpointClient(), "foo")
+ expected := endpoint + "servers/foo/action"
+ th.CheckEquals(t, expected, actual)
+}
diff --git a/_site/openstack/endpoint_location.go b/_site/openstack/endpoint_location.go
new file mode 100644
index 0000000..5a311e4
--- /dev/null
+++ b/_site/openstack/endpoint_location.go
@@ -0,0 +1,124 @@
+package openstack
+
+import (
+ "fmt"
+
+ "github.com/rackspace/gophercloud"
+ tokens2 "github.com/rackspace/gophercloud/openstack/identity/v2/tokens"
+ endpoints3 "github.com/rackspace/gophercloud/openstack/identity/v3/endpoints"
+ services3 "github.com/rackspace/gophercloud/openstack/identity/v3/services"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// V2EndpointURL discovers the endpoint URL for a specific service from a ServiceCatalog acquired
+// during the v2 identity service. The specified EndpointOpts are used to identify a unique,
+// unambiguous endpoint to return. It's an error both when multiple endpoints match the provided
+// criteria and when none do. The minimum that can be specified is a Type, but you will also often
+// need to specify a Name and/or a Region depending on what's available on your OpenStack
+// deployment.
+func V2EndpointURL(catalog *tokens2.ServiceCatalog, opts gophercloud.EndpointOpts) (string, error) {
+ // Extract Endpoints from the catalog entries that match the requested Type, Name if provided, and Region if provided.
+ var endpoints = make([]tokens2.Endpoint, 0, 1)
+ for _, entry := range catalog.Entries {
+ if (entry.Type == opts.Type) && (opts.Name == "" || entry.Name == opts.Name) {
+ for _, endpoint := range entry.Endpoints {
+ if opts.Region == "" || endpoint.Region == opts.Region {
+ endpoints = append(endpoints, endpoint)
+ }
+ }
+ }
+ }
+
+ // Report an error if the options were ambiguous.
+ if len(endpoints) > 1 {
+ return "", fmt.Errorf("Discovered %d matching endpoints: %#v", len(endpoints), endpoints)
+ }
+
+ // Extract the appropriate URL from the matching Endpoint.
+ for _, endpoint := range endpoints {
+ switch opts.Availability {
+ case gophercloud.AvailabilityPublic:
+ return gophercloud.NormalizeURL(endpoint.PublicURL), nil
+ case gophercloud.AvailabilityInternal:
+ return gophercloud.NormalizeURL(endpoint.InternalURL), nil
+ case gophercloud.AvailabilityAdmin:
+ return gophercloud.NormalizeURL(endpoint.AdminURL), nil
+ default:
+ return "", fmt.Errorf("Unexpected availability in endpoint query: %s", opts.Availability)
+ }
+ }
+
+ // Report an error if there were no matching endpoints.
+ return "", gophercloud.ErrEndpointNotFound
+}
+
+// V3EndpointURL discovers the endpoint URL for a specific service using multiple calls against
+// an identity v3 service endpoint. The specified EndpointOpts are used to identify a unique,
+// unambiguous endpoint to return. It's an error both when multiple endpoints match the provided
+// criteria and when none do. The minimum that can be specified is a Type, but you will also often
+// need to specify a Name and/or a Region depending on what's available on your OpenStack
+// deployment.
+func V3EndpointURL(v3Client *gophercloud.ServiceClient, opts gophercloud.EndpointOpts) (string, error) {
+ // Discover the service we're interested in.
+ var services = make([]services3.Service, 0, 1)
+ servicePager := services3.List(v3Client, services3.ListOpts{ServiceType: opts.Type})
+ err := servicePager.EachPage(func(page pagination.Page) (bool, error) {
+ part, err := services3.ExtractServices(page)
+ if err != nil {
+ return false, err
+ }
+
+ for _, service := range part {
+ if service.Name == opts.Name {
+ services = append(services, service)
+ }
+ }
+
+ return true, nil
+ })
+ if err != nil {
+ return "", err
+ }
+
+ if len(services) == 0 {
+ return "", gophercloud.ErrServiceNotFound
+ }
+ if len(services) > 1 {
+ return "", fmt.Errorf("Discovered %d matching services: %#v", len(services), services)
+ }
+ service := services[0]
+
+ // Enumerate the endpoints available for this service.
+ var endpoints []endpoints3.Endpoint
+ endpointPager := endpoints3.List(v3Client, endpoints3.ListOpts{
+ Availability: opts.Availability,
+ ServiceID: service.ID,
+ })
+ err = endpointPager.EachPage(func(page pagination.Page) (bool, error) {
+ part, err := endpoints3.ExtractEndpoints(page)
+ if err != nil {
+ return false, err
+ }
+
+ for _, endpoint := range part {
+ if opts.Region == "" || endpoint.Region == opts.Region {
+ endpoints = append(endpoints, endpoint)
+ }
+ }
+
+ return true, nil
+ })
+ if err != nil {
+ return "", err
+ }
+
+ if len(endpoints) == 0 {
+ return "", gophercloud.ErrEndpointNotFound
+ }
+ if len(endpoints) > 1 {
+ return "", fmt.Errorf("Discovered %d matching endpoints: %#v", len(endpoints), endpoints)
+ }
+ endpoint := endpoints[0]
+
+ return gophercloud.NormalizeURL(endpoint.URL), nil
+}
diff --git a/_site/openstack/endpoint_location_test.go b/_site/openstack/endpoint_location_test.go
new file mode 100644
index 0000000..4e0569a
--- /dev/null
+++ b/_site/openstack/endpoint_location_test.go
@@ -0,0 +1,225 @@
+package openstack
+
+import (
+ "fmt"
+ "net/http"
+ "strings"
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ tokens2 "github.com/rackspace/gophercloud/openstack/identity/v2/tokens"
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+// Service catalog fixtures take too much vertical space!
+var catalog2 = tokens2.ServiceCatalog{
+ Entries: []tokens2.CatalogEntry{
+ tokens2.CatalogEntry{
+ Type: "same",
+ Name: "same",
+ Endpoints: []tokens2.Endpoint{
+ tokens2.Endpoint{
+ Region: "same",
+ PublicURL: "https://public.correct.com/",
+ InternalURL: "https://internal.correct.com/",
+ AdminURL: "https://admin.correct.com/",
+ },
+ tokens2.Endpoint{
+ Region: "different",
+ PublicURL: "https://badregion.com/",
+ },
+ },
+ },
+ tokens2.CatalogEntry{
+ Type: "same",
+ Name: "different",
+ Endpoints: []tokens2.Endpoint{
+ tokens2.Endpoint{
+ Region: "same",
+ PublicURL: "https://badname.com/",
+ },
+ tokens2.Endpoint{
+ Region: "different",
+ PublicURL: "https://badname.com/+badregion",
+ },
+ },
+ },
+ tokens2.CatalogEntry{
+ Type: "different",
+ Name: "different",
+ Endpoints: []tokens2.Endpoint{
+ tokens2.Endpoint{
+ Region: "same",
+ PublicURL: "https://badtype.com/+badname",
+ },
+ tokens2.Endpoint{
+ Region: "different",
+ PublicURL: "https://badtype.com/+badregion+badname",
+ },
+ },
+ },
+ },
+}
+
+func TestV2EndpointExact(t *testing.T) {
+ expectedURLs := map[gophercloud.Availability]string{
+ gophercloud.AvailabilityPublic: "https://public.correct.com/",
+ gophercloud.AvailabilityAdmin: "https://admin.correct.com/",
+ gophercloud.AvailabilityInternal: "https://internal.correct.com/",
+ }
+
+ for availability, expected := range expectedURLs {
+ actual, err := V2EndpointURL(&catalog2, gophercloud.EndpointOpts{
+ Type: "same",
+ Name: "same",
+ Region: "same",
+ Availability: availability,
+ })
+ th.AssertNoErr(t, err)
+ th.CheckEquals(t, expected, actual)
+ }
+}
+
+func TestV2EndpointNone(t *testing.T) {
+ _, err := V2EndpointURL(&catalog2, gophercloud.EndpointOpts{
+ Type: "nope",
+ Availability: gophercloud.AvailabilityPublic,
+ })
+ th.CheckEquals(t, gophercloud.ErrEndpointNotFound, err)
+}
+
+func TestV2EndpointMultiple(t *testing.T) {
+ _, err := V2EndpointURL(&catalog2, gophercloud.EndpointOpts{
+ Type: "same",
+ Region: "same",
+ Availability: gophercloud.AvailabilityPublic,
+ })
+ if !strings.HasPrefix(err.Error(), "Discovered 2 matching endpoints:") {
+ t.Errorf("Received unexpected error: %v", err)
+ }
+}
+
+func TestV2EndpointBadAvailability(t *testing.T) {
+ _, err := V2EndpointURL(&catalog2, gophercloud.EndpointOpts{
+ Type: "same",
+ Name: "same",
+ Region: "same",
+ Availability: "wat",
+ })
+ th.CheckEquals(t, err.Error(), "Unexpected availability in endpoint query: wat")
+}
+
+func setupV3Responses(t *testing.T) {
+ // Mock the service query.
+ 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().Add("Content-Type", "application/json")
+ fmt.Fprintf(w, `
+ {
+ "links": {
+ "next": null,
+ "previous": null
+ },
+ "services": [
+ {
+ "description": "Correct",
+ "id": "1234",
+ "name": "same",
+ "type": "same"
+ },
+ {
+ "description": "Bad Name",
+ "id": "9876",
+ "name": "different",
+ "type": "same"
+ }
+ ]
+ }
+ `)
+ })
+
+ // Mock the endpoint query.
+ th.Mux.HandleFunc("/endpoints", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ th.TestFormValues(t, r, map[string]string{
+ "service_id": "1234",
+ "interface": "public",
+ })
+
+ w.Header().Add("Content-Type", "application/json")
+ fmt.Fprintf(w, `
+ {
+ "endpoints": [
+ {
+ "id": "12",
+ "interface": "public",
+ "name": "the-right-one",
+ "region": "same",
+ "service_id": "1234",
+ "url": "https://correct:9000/"
+ },
+ {
+ "id": "14",
+ "interface": "public",
+ "name": "bad-region",
+ "region": "different",
+ "service_id": "1234",
+ "url": "https://bad-region:9001/"
+ }
+ ],
+ "links": {
+ "next": null,
+ "previous": null
+ }
+ }
+ `)
+ })
+}
+
+func TestV3EndpointExact(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ setupV3Responses(t)
+
+ actual, err := V3EndpointURL(fake.ServiceClient(), gophercloud.EndpointOpts{
+ Type: "same",
+ Name: "same",
+ Region: "same",
+ Availability: gophercloud.AvailabilityPublic,
+ })
+ th.AssertNoErr(t, err)
+ th.CheckEquals(t, actual, "https://correct:9000/")
+}
+
+func TestV3EndpointNoService(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ 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().Add("Content-Type", "application/json")
+ fmt.Fprintf(w, `
+ {
+ "links": {
+ "next": null,
+ "previous": null
+ },
+ "services": []
+ }
+ `)
+ })
+
+ _, err := V3EndpointURL(fake.ServiceClient(), gophercloud.EndpointOpts{
+ Type: "nope",
+ Name: "same",
+ Region: "same",
+ Availability: gophercloud.AvailabilityPublic,
+ })
+ th.CheckEquals(t, gophercloud.ErrServiceNotFound, err)
+}
diff --git a/_site/openstack/identity/v2/extensions/delegate.go b/_site/openstack/identity/v2/extensions/delegate.go
new file mode 100644
index 0000000..cee275f
--- /dev/null
+++ b/_site/openstack/identity/v2/extensions/delegate.go
@@ -0,0 +1,52 @@
+package extensions
+
+import (
+ "github.com/mitchellh/mapstructure"
+ "github.com/rackspace/gophercloud"
+ common "github.com/rackspace/gophercloud/openstack/common/extensions"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// ExtensionPage is a single page of Extension results.
+type ExtensionPage struct {
+ common.ExtensionPage
+}
+
+// IsEmpty returns true if the current page contains at least one Extension.
+func (page ExtensionPage) IsEmpty() (bool, error) {
+ is, err := ExtractExtensions(page)
+ 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.
+func ExtractExtensions(page pagination.Page) ([]common.Extension, error) {
+ // Identity v2 adds an intermediate "values" object.
+
+ var resp struct {
+ Extensions struct {
+ Values []common.Extension `mapstructure:"values"`
+ } `mapstructure:"extensions"`
+ }
+
+ err := mapstructure.Decode(page.(ExtensionPage).Body, &resp)
+ return resp.Extensions.Values, err
+}
+
+// Get retrieves information for a specific extension using its alias.
+func Get(c *gophercloud.ServiceClient, alias string) common.GetResult {
+ return common.Get(c, alias)
+}
+
+// List returns a Pager which allows you to iterate over the full collection of extensions.
+// It does not accept query parameters.
+func List(c *gophercloud.ServiceClient) pagination.Pager {
+ return common.List(c).WithPageCreator(func(r pagination.LastHTTPResponse) pagination.Page {
+ return ExtensionPage{
+ ExtensionPage: common.ExtensionPage{SinglePageBase: pagination.SinglePageBase(r)},
+ }
+ })
+}
diff --git a/_site/openstack/identity/v2/extensions/delegate_test.go b/_site/openstack/identity/v2/extensions/delegate_test.go
new file mode 100644
index 0000000..504118a
--- /dev/null
+++ b/_site/openstack/identity/v2/extensions/delegate_test.go
@@ -0,0 +1,38 @@
+package extensions
+
+import (
+ "testing"
+
+ common "github.com/rackspace/gophercloud/openstack/common/extensions"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+ "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestList(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleListExtensionsSuccessfully(t)
+
+ count := 0
+ err := List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractExtensions(page)
+ th.AssertNoErr(t, err)
+ th.CheckDeepEquals(t, common.ExpectedExtensions, actual)
+
+ return true, nil
+ })
+ th.AssertNoErr(t, err)
+ th.CheckEquals(t, 1, count)
+}
+
+func TestGet(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ common.HandleGetExtensionSuccessfully(t)
+
+ actual, err := Get(client.ServiceClient(), "agent").Extract()
+ th.AssertNoErr(t, err)
+ th.CheckDeepEquals(t, common.SingleExtension, actual)
+}
diff --git a/_site/openstack/identity/v2/extensions/fixtures.go b/_site/openstack/identity/v2/extensions/fixtures.go
new file mode 100644
index 0000000..96cb7d2
--- /dev/null
+++ b/_site/openstack/identity/v2/extensions/fixtures.go
@@ -0,0 +1,60 @@
+// +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 Extension result. It differs from the delegated implementation
+// by the introduction of an intermediate "values" member.
+const ListOutput = `
+{
+ "extensions": {
+ "values": [
+ {
+ "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"
+ }
+ ]
+ }
+}
+`
+
+// HandleListExtensionsSuccessfully creates an HTTP handler that returns ListOutput for a List
+// call.
+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, `
+{
+ "extensions": {
+ "values": [
+ {
+ "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"
+ }
+ ]
+ }
+}
+ `)
+ })
+
+}
diff --git a/_site/openstack/identity/v2/tenants/doc.go b/_site/openstack/identity/v2/tenants/doc.go
new file mode 100644
index 0000000..473a302
--- /dev/null
+++ b/_site/openstack/identity/v2/tenants/doc.go
@@ -0,0 +1,7 @@
+/*
+Package tenants contains API calls that query for information about tenants on an OpenStack deployment.
+
+See: http://developer.openstack.org/api-ref-identity-v2.html#identity-auth-v2
+And: http://developer.openstack.org/api-ref-identity-v2.html#admin-tenants
+*/
+package tenants
diff --git a/_site/openstack/identity/v2/tenants/fixtures.go b/_site/openstack/identity/v2/tenants/fixtures.go
new file mode 100644
index 0000000..7f044ac
--- /dev/null
+++ b/_site/openstack/identity/v2/tenants/fixtures.go
@@ -0,0 +1,65 @@
+// +build fixtures
+
+package tenants
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ th "github.com/rackspace/gophercloud/testhelper"
+ "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+// ListOutput provides a single page of Tenant results.
+const ListOutput = `
+{
+ "tenants": [
+ {
+ "id": "1234",
+ "name": "Red Team",
+ "description": "The team that is red",
+ "enabled": true
+ },
+ {
+ "id": "9876",
+ "name": "Blue Team",
+ "description": "The team that is blue",
+ "enabled": false
+ }
+ ]
+}
+`
+
+// RedTeam is a Tenant fixture.
+var RedTeam = Tenant{
+ ID: "1234",
+ Name: "Red Team",
+ Description: "The team that is red",
+ Enabled: true,
+}
+
+// BlueTeam is a Tenant fixture.
+var BlueTeam = Tenant{
+ ID: "9876",
+ Name: "Blue Team",
+ Description: "The team that is blue",
+ Enabled: false,
+}
+
+// ExpectedTenantSlice is the slice of tenants expected to be returned from ListOutput.
+var ExpectedTenantSlice = []Tenant{RedTeam, BlueTeam}
+
+// HandleListTenantsSuccessfully creates an HTTP handler at `/tenants` on the test handler mux that
+// responds with a list of two tenants.
+func HandleListTenantsSuccessfully(t *testing.T) {
+ th.Mux.HandleFunc("/tenants", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "Accept", "application/json")
+ th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprintf(w, ListOutput)
+ })
+}
diff --git a/_site/openstack/identity/v2/tenants/requests.go b/_site/openstack/identity/v2/tenants/requests.go
new file mode 100644
index 0000000..5ffeaa7
--- /dev/null
+++ b/_site/openstack/identity/v2/tenants/requests.go
@@ -0,0 +1,33 @@
+package tenants
+
+import (
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// ListOpts filters the Tenants that are returned by the List call.
+type ListOpts struct {
+ // Marker is the ID of the last Tenant on the previous page.
+ Marker string `q:"marker"`
+
+ // Limit specifies the page size.
+ Limit int `q:"limit"`
+}
+
+// List enumerates the Tenants to which the current token has access.
+func List(client *gophercloud.ServiceClient, opts *ListOpts) pagination.Pager {
+ createPage := func(r pagination.LastHTTPResponse) pagination.Page {
+ return TenantPage{pagination.LinkedPageBase{LastHTTPResponse: r}}
+ }
+
+ url := listURL(client)
+ if opts != nil {
+ q, err := gophercloud.BuildQueryString(opts)
+ if err != nil {
+ return pagination.Pager{Err: err}
+ }
+ url += q.String()
+ }
+
+ return pagination.NewPager(client, url, createPage)
+}
diff --git a/_site/openstack/identity/v2/tenants/requests_test.go b/_site/openstack/identity/v2/tenants/requests_test.go
new file mode 100644
index 0000000..e8f172d
--- /dev/null
+++ b/_site/openstack/identity/v2/tenants/requests_test.go
@@ -0,0 +1,29 @@
+package tenants
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+ "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestListTenants(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleListTenantsSuccessfully(t)
+
+ count := 0
+ err := List(client.ServiceClient(), nil).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+
+ actual, err := ExtractTenants(page)
+ th.AssertNoErr(t, err)
+
+ th.CheckDeepEquals(t, ExpectedTenantSlice, actual)
+
+ return true, nil
+ })
+ th.AssertNoErr(t, err)
+ th.CheckEquals(t, count, 1)
+}
diff --git a/_site/openstack/identity/v2/tenants/results.go b/_site/openstack/identity/v2/tenants/results.go
new file mode 100644
index 0000000..c1220c3
--- /dev/null
+++ b/_site/openstack/identity/v2/tenants/results.go
@@ -0,0 +1,62 @@
+package tenants
+
+import (
+ "github.com/mitchellh/mapstructure"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// Tenant is a grouping of users in the identity service.
+type Tenant struct {
+ // ID is a unique identifier for this tenant.
+ ID string `mapstructure:"id"`
+
+ // Name is a friendlier user-facing name for this tenant.
+ Name string `mapstructure:"name"`
+
+ // Description is a human-readable explanation of this Tenant's purpose.
+ Description string `mapstructure:"description"`
+
+ // Enabled indicates whether or not a tenant is active.
+ Enabled bool `mapstructure:"enabled"`
+}
+
+// TenantPage is a single page of Tenant results.
+type TenantPage struct {
+ pagination.LinkedPageBase
+}
+
+// IsEmpty determines whether or not a page of Tenants contains any results.
+func (page TenantPage) IsEmpty() (bool, error) {
+ tenants, err := ExtractTenants(page)
+ if err != nil {
+ return false, err
+ }
+ return len(tenants) == 0, nil
+}
+
+// NextPageURL extracts the "next" link from the tenants_links section of the result.
+func (page TenantPage) NextPageURL() (string, error) {
+ type resp struct {
+ Links []gophercloud.Link `mapstructure:"tenants_links"`
+ }
+
+ var r resp
+ err := mapstructure.Decode(page.Body, &r)
+ if err != nil {
+ return "", err
+ }
+
+ return gophercloud.ExtractNextURL(r.Links)
+}
+
+// ExtractTenants returns a slice of Tenants contained in a single page of results.
+func ExtractTenants(page pagination.Page) ([]Tenant, error) {
+ casted := page.(TenantPage).Body
+ var response struct {
+ Tenants []Tenant `mapstructure:"tenants"`
+ }
+
+ err := mapstructure.Decode(casted, &response)
+ return response.Tenants, err
+}
diff --git a/_site/openstack/identity/v2/tenants/urls.go b/_site/openstack/identity/v2/tenants/urls.go
new file mode 100644
index 0000000..1dd6ce0
--- /dev/null
+++ b/_site/openstack/identity/v2/tenants/urls.go
@@ -0,0 +1,7 @@
+package tenants
+
+import "github.com/rackspace/gophercloud"
+
+func listURL(client *gophercloud.ServiceClient) string {
+ return client.ServiceURL("tenants")
+}
diff --git a/_site/openstack/identity/v2/tokens/doc.go b/_site/openstack/identity/v2/tokens/doc.go
new file mode 100644
index 0000000..d26f642
--- /dev/null
+++ b/_site/openstack/identity/v2/tokens/doc.go
@@ -0,0 +1,6 @@
+/*
+Package tokens contains functions that issue and manipulate identity tokens.
+
+Reference: http://developer.openstack.org/api-ref-identity-v2.html#identity-auth-v2
+*/
+package tokens
diff --git a/_site/openstack/identity/v2/tokens/errors.go b/_site/openstack/identity/v2/tokens/errors.go
new file mode 100644
index 0000000..3a9172e
--- /dev/null
+++ b/_site/openstack/identity/v2/tokens/errors.go
@@ -0,0 +1,30 @@
+package tokens
+
+import (
+ "errors"
+ "fmt"
+)
+
+var (
+ // ErrUserIDProvided is returned if you attempt to authenticate with a UserID.
+ ErrUserIDProvided = unacceptedAttributeErr("UserID")
+
+ // ErrAPIKeyProvided is returned if you attempt to authenticate with an APIKey.
+ ErrAPIKeyProvided = unacceptedAttributeErr("APIKey")
+
+ // ErrDomainIDProvided is returned if you attempt to authenticate with a DomainID.
+ ErrDomainIDProvided = unacceptedAttributeErr("DomainID")
+
+ // ErrDomainNameProvided is returned if you attempt to authenticate with a DomainName.
+ ErrDomainNameProvided = unacceptedAttributeErr("DomainName")
+
+ // ErrUsernameRequired is returned if you attempt ot authenticate without a Username.
+ ErrUsernameRequired = errors.New("You must supply a Username in your AuthOptions.")
+
+ // ErrPasswordRequired is returned if you don't provide a password.
+ ErrPasswordRequired = errors.New("Please supply a Password in your AuthOptions.")
+)
+
+func unacceptedAttributeErr(attribute string) error {
+ return fmt.Errorf("The base Identity V2 API does not accept authentication by %s", attribute)
+}
diff --git a/_site/openstack/identity/v2/tokens/fixtures.go b/_site/openstack/identity/v2/tokens/fixtures.go
new file mode 100644
index 0000000..1cb0d05
--- /dev/null
+++ b/_site/openstack/identity/v2/tokens/fixtures.go
@@ -0,0 +1,128 @@
+// +build fixtures
+
+package tokens
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+ "time"
+
+ "github.com/rackspace/gophercloud/openstack/identity/v2/tenants"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+// ExpectedToken is the token that should be parsed from TokenCreationResponse.
+var ExpectedToken = &Token{
+ ID: "aaaabbbbccccdddd",
+ ExpiresAt: time.Date(2014, time.January, 31, 15, 30, 58, 0, time.UTC),
+ Tenant: tenants.Tenant{
+ ID: "fc394f2ab2df4114bde39905f800dc57",
+ Name: "test",
+ Description: "There are many tenants. This one is yours.",
+ Enabled: true,
+ },
+}
+
+// ExpectedServiceCatalog is the service catalog that should be parsed from TokenCreationResponse.
+var ExpectedServiceCatalog = &ServiceCatalog{
+ Entries: []CatalogEntry{
+ CatalogEntry{
+ Name: "inscrutablewalrus",
+ Type: "something",
+ Endpoints: []Endpoint{
+ Endpoint{
+ PublicURL: "http://something0:1234/v2/",
+ Region: "region0",
+ },
+ Endpoint{
+ PublicURL: "http://something1:1234/v2/",
+ Region: "region1",
+ },
+ },
+ },
+ CatalogEntry{
+ Name: "arbitrarypenguin",
+ Type: "else",
+ Endpoints: []Endpoint{
+ Endpoint{
+ PublicURL: "http://else0:4321/v3/",
+ Region: "region0",
+ },
+ },
+ },
+ },
+}
+
+// TokenCreationResponse is a JSON response that contains ExpectedToken and ExpectedServiceCatalog.
+const TokenCreationResponse = `
+{
+ "access": {
+ "token": {
+ "issued_at": "2014-01-30T15:30:58.000000Z",
+ "expires": "2014-01-31T15:30:58Z",
+ "id": "aaaabbbbccccdddd",
+ "tenant": {
+ "description": "There are many tenants. This one is yours.",
+ "enabled": true,
+ "id": "fc394f2ab2df4114bde39905f800dc57",
+ "name": "test"
+ }
+ },
+ "serviceCatalog": [
+ {
+ "endpoints": [
+ {
+ "publicURL": "http://something0:1234/v2/",
+ "region": "region0"
+ },
+ {
+ "publicURL": "http://something1:1234/v2/",
+ "region": "region1"
+ }
+ ],
+ "type": "something",
+ "name": "inscrutablewalrus"
+ },
+ {
+ "endpoints": [
+ {
+ "publicURL": "http://else0:4321/v3/",
+ "region": "region0"
+ }
+ ],
+ "type": "else",
+ "name": "arbitrarypenguin"
+ }
+ ]
+ }
+}
+`
+
+// HandleTokenPost expects a POST against a /tokens handler, ensures that the request body has been
+// constructed properly given certain auth options, and returns the result.
+func HandleTokenPost(t *testing.T, requestJSON string) {
+ th.Mux.HandleFunc("/tokens", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "POST")
+ th.TestHeader(t, r, "Content-Type", "application/json")
+ th.TestHeader(t, r, "Accept", "application/json")
+ if requestJSON != "" {
+ th.TestJSONRequest(t, r, requestJSON)
+ }
+
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprintf(w, TokenCreationResponse)
+ })
+}
+
+// IsSuccessful ensures that a CreateResult was successful and contains the correct token and
+// service catalog.
+func IsSuccessful(t *testing.T, result CreateResult) {
+ token, err := result.ExtractToken()
+ th.AssertNoErr(t, err)
+ th.CheckDeepEquals(t, ExpectedToken, token)
+
+ serviceCatalog, err := result.ExtractServiceCatalog()
+ th.AssertNoErr(t, err)
+ th.CheckDeepEquals(t, ExpectedServiceCatalog, serviceCatalog)
+}
diff --git a/_site/openstack/identity/v2/tokens/requests.go b/_site/openstack/identity/v2/tokens/requests.go
new file mode 100644
index 0000000..c25a72b
--- /dev/null
+++ b/_site/openstack/identity/v2/tokens/requests.go
@@ -0,0 +1,87 @@
+package tokens
+
+import (
+ "github.com/racker/perigee"
+ "github.com/rackspace/gophercloud"
+)
+
+// AuthOptionsBuilder describes any argument that may be passed to the Create call.
+type AuthOptionsBuilder interface {
+
+ // ToTokenCreateMap assembles the Create request body, returning an error if parameters are
+ // missing or inconsistent.
+ ToTokenCreateMap() (map[string]interface{}, error)
+}
+
+// AuthOptions wraps a gophercloud AuthOptions in order to adhere to the AuthOptionsBuilder
+// interface.
+type AuthOptions struct {
+ gophercloud.AuthOptions
+}
+
+// WrapOptions embeds a root AuthOptions struct in a package-specific one.
+func WrapOptions(original gophercloud.AuthOptions) AuthOptions {
+ return AuthOptions{AuthOptions: original}
+}
+
+// ToTokenCreateMap converts AuthOptions into nested maps that can be serialized into a JSON
+// request.
+func (auth AuthOptions) ToTokenCreateMap() (map[string]interface{}, error) {
+ // Error out if an unsupported auth option is present.
+ if auth.UserID != "" {
+ return nil, ErrUserIDProvided
+ }
+ if auth.APIKey != "" {
+ return nil, ErrAPIKeyProvided
+ }
+ if auth.DomainID != "" {
+ return nil, ErrDomainIDProvided
+ }
+ if auth.DomainName != "" {
+ return nil, ErrDomainNameProvided
+ }
+
+ // Username and Password are always required.
+ if auth.Username == "" {
+ return nil, ErrUsernameRequired
+ }
+ if auth.Password == "" {
+ return nil, ErrPasswordRequired
+ }
+
+ // Populate the request map.
+ authMap := make(map[string]interface{})
+
+ authMap["passwordCredentials"] = map[string]interface{}{
+ "username": auth.Username,
+ "password": auth.Password,
+ }
+
+ if auth.TenantID != "" {
+ authMap["tenantId"] = auth.TenantID
+ }
+ if auth.TenantName != "" {
+ authMap["tenantName"] = auth.TenantName
+ }
+
+ return map[string]interface{}{"auth": authMap}, nil
+}
+
+// Create authenticates to the identity service and attempts to acquire a Token.
+// If successful, the CreateResult
+// Generally, rather than interact with this call directly, end users should call openstack.AuthenticatedClient(),
+// which abstracts all of the gory details about navigating service catalogs and such.
+func Create(client *gophercloud.ServiceClient, auth AuthOptionsBuilder) CreateResult {
+ request, err := auth.ToTokenCreateMap()
+ if err != nil {
+ return CreateResult{gophercloud.CommonResult{Err: err}}
+ }
+
+ var result CreateResult
+ _, result.Err = perigee.Request("POST", CreateURL(client), perigee.Options{
+ ReqBody: &request,
+ Results: &result.Resp,
+ OkCodes: []int{200, 203},
+ })
+ return result
+}
diff --git a/_site/openstack/identity/v2/tokens/requests_test.go b/_site/openstack/identity/v2/tokens/requests_test.go
new file mode 100644
index 0000000..2f02825
--- /dev/null
+++ b/_site/openstack/identity/v2/tokens/requests_test.go
@@ -0,0 +1,140 @@
+package tokens
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ th "github.com/rackspace/gophercloud/testhelper"
+ "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func tokenPost(t *testing.T, options gophercloud.AuthOptions, requestJSON string) CreateResult {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleTokenPost(t, requestJSON)
+
+ return Create(client.ServiceClient(), AuthOptions{options})
+}
+
+func tokenPostErr(t *testing.T, options gophercloud.AuthOptions, expectedErr error) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleTokenPost(t, "")
+
+ actualErr := Create(client.ServiceClient(), AuthOptions{options}).Err
+ th.CheckEquals(t, expectedErr, actualErr)
+}
+
+func TestCreateWithPassword(t *testing.T) {
+ options := gophercloud.AuthOptions{
+ Username: "me",
+ Password: "swordfish",
+ }
+
+ IsSuccessful(t, tokenPost(t, options, `
+ {
+ "auth": {
+ "passwordCredentials": {
+ "username": "me",
+ "password": "swordfish"
+ }
+ }
+ }
+ `))
+}
+
+func TestCreateTokenWithTenantID(t *testing.T) {
+ options := gophercloud.AuthOptions{
+ Username: "me",
+ Password: "opensesame",
+ TenantID: "fc394f2ab2df4114bde39905f800dc57",
+ }
+
+ IsSuccessful(t, tokenPost(t, options, `
+ {
+ "auth": {
+ "tenantId": "fc394f2ab2df4114bde39905f800dc57",
+ "passwordCredentials": {
+ "username": "me",
+ "password": "opensesame"
+ }
+ }
+ }
+ `))
+}
+
+func TestCreateTokenWithTenantName(t *testing.T) {
+ options := gophercloud.AuthOptions{
+ Username: "me",
+ Password: "opensesame",
+ TenantName: "demo",
+ }
+
+ IsSuccessful(t, tokenPost(t, options, `
+ {
+ "auth": {
+ "tenantName": "demo",
+ "passwordCredentials": {
+ "username": "me",
+ "password": "opensesame"
+ }
+ }
+ }
+ `))
+}
+
+func TestProhibitUserID(t *testing.T) {
+ options := gophercloud.AuthOptions{
+ Username: "me",
+ UserID: "1234",
+ Password: "thing",
+ }
+
+ tokenPostErr(t, options, ErrUserIDProvided)
+}
+
+func TestProhibitAPIKey(t *testing.T) {
+ options := gophercloud.AuthOptions{
+ Username: "me",
+ Password: "thing",
+ APIKey: "123412341234",
+ }
+
+ tokenPostErr(t, options, ErrAPIKeyProvided)
+}
+
+func TestProhibitDomainID(t *testing.T) {
+ options := gophercloud.AuthOptions{
+ Username: "me",
+ Password: "thing",
+ DomainID: "1234",
+ }
+
+ tokenPostErr(t, options, ErrDomainIDProvided)
+}
+
+func TestProhibitDomainName(t *testing.T) {
+ options := gophercloud.AuthOptions{
+ Username: "me",
+ Password: "thing",
+ DomainName: "wat",
+ }
+
+ tokenPostErr(t, options, ErrDomainNameProvided)
+}
+
+func TestRequireUsername(t *testing.T) {
+ options := gophercloud.AuthOptions{
+ Password: "thing",
+ }
+
+ tokenPostErr(t, options, ErrUsernameRequired)
+}
+
+func TestRequirePassword(t *testing.T) {
+ options := gophercloud.AuthOptions{
+ Username: "me",
+ }
+
+ tokenPostErr(t, options, ErrPasswordRequired)
+}
diff --git a/_site/openstack/identity/v2/tokens/results.go b/_site/openstack/identity/v2/tokens/results.go
new file mode 100644
index 0000000..e88b2c7
--- /dev/null
+++ b/_site/openstack/identity/v2/tokens/results.go
@@ -0,0 +1,133 @@
+package tokens
+
+import (
+ "time"
+
+ "github.com/mitchellh/mapstructure"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/openstack/identity/v2/tenants"
+)
+
+// Token provides only the most basic information related to an authentication token.
+type Token struct {
+ // ID provides the primary means of identifying a user to the OpenStack API.
+ // OpenStack defines this field as an opaque value, so do not depend on its content.
+ // It is safe, however, to compare for equality.
+ ID string
+
+ // ExpiresAt provides a timestamp in ISO 8601 format, indicating when the authentication token becomes invalid.
+ // After this point in time, future API requests made using this authentication token will respond with errors.
+ // Either the caller will need to reauthenticate manually, or more preferably, the caller should exploit automatic re-authentication.
+ // See the AuthOptions structure for more details.
+ ExpiresAt time.Time
+
+ // Tenant provides information about the tenant to which this token grants access.
+ Tenant tenants.Tenant
+}
+
+// Endpoint represents a single API endpoint offered by a service.
+// It provides the public and internal URLs, if supported, along with a region specifier, again if provided.
+// The significance of the Region field will depend upon your provider.
+//
+// In addition, the interface offered by the service will have version information associated with it
+// through the VersionId, VersionInfo, and VersionList fields, if provided or supported.
+//
+// In all cases, fields which aren't supported by the provider and service combined will assume a zero-value ("").
+type Endpoint struct {
+ TenantID string `mapstructure:"tenantId"`
+ PublicURL string `mapstructure:"publicURL"`
+ InternalURL string `mapstructure:"internalURL"`
+ AdminURL string `mapstructure:"adminURL"`
+ Region string `mapstructure:"region"`
+ VersionID string `mapstructure:"versionId"`
+ VersionInfo string `mapstructure:"versionInfo"`
+ VersionList string `mapstructure:"versionList"`
+}
+
+// CatalogEntry provides a type-safe interface to an Identity API V2 service catalog listing.
+// Each class of service, such as cloud DNS or block storage services, will have a single
+// CatalogEntry representing it.
+//
+// Note: when looking for the desired service, try, whenever possible, to key off the type field.
+// Otherwise, you'll tie the representation of the service to a specific provider.
+type CatalogEntry struct {
+ // Name will contain the provider-specified name for the service.
+ Name string `mapstructure:"name"`
+
+ // Type will contain a type string if OpenStack defines a type for the service.
+ // Otherwise, for provider-specific services, the provider may assign their own type strings.
+ Type string `mapstructure:"type"`
+
+ // Endpoints will let the caller iterate over all the different endpoints that may exist for
+ // the service.
+ Endpoints []Endpoint `mapstructure:"endpoints"`
+}
+
+// ServiceCatalog provides a view into the service catalog from a previous, successful authentication.
+type ServiceCatalog struct {
+ Entries []CatalogEntry
+}
+
+// CreateResult defers the interpretation of a created token.
+// Use ExtractToken() to interpret it as a Token, or ExtractServiceCatalog() to interpret it as a service catalog.
+type CreateResult struct {
+ gophercloud.CommonResult
+}
+
+// ExtractToken returns the just-created Token from a CreateResult.
+func (result CreateResult) ExtractToken() (*Token, error) {
+ if result.Err != nil {
+ return nil, result.Err
+ }
+
+ var response struct {
+ Access struct {
+ Token struct {
+ Expires string `mapstructure:"expires"`
+ ID string `mapstructure:"id"`
+ Tenant tenants.Tenant `mapstructure:"tenant"`
+ } `mapstructure:"token"`
+ } `mapstructure:"access"`
+ }
+
+ err := mapstructure.Decode(result.Resp, &response)
+ if err != nil {
+ return nil, err
+ }
+
+ expiresTs, err := time.Parse(gophercloud.RFC3339Milli, response.Access.Token.Expires)
+ if err != nil {
+ return nil, err
+ }
+
+ return &Token{
+ ID: response.Access.Token.ID,
+ ExpiresAt: expiresTs,
+ Tenant: response.Access.Token.Tenant,
+ }, nil
+}
+
+// ExtractServiceCatalog returns the ServiceCatalog that was generated along with the user's Token.
+func (result CreateResult) ExtractServiceCatalog() (*ServiceCatalog, error) {
+ if result.Err != nil {
+ return nil, result.Err
+ }
+
+ var response struct {
+ Access struct {
+ Entries []CatalogEntry `mapstructure:"serviceCatalog"`
+ } `mapstructure:"access"`
+ }
+
+ err := mapstructure.Decode(result.Resp, &response)
+ if err != nil {
+ return nil, err
+ }
+
+ return &ServiceCatalog{Entries: response.Access.Entries}, nil
+}
+
+// createErr quickly packs an error in a CreateResult.
+func createErr(err error) CreateResult {
+ return CreateResult{gophercloud.CommonResult{Err: err}}
+}
diff --git a/_site/openstack/identity/v2/tokens/urls.go b/_site/openstack/identity/v2/tokens/urls.go
new file mode 100644
index 0000000..cd4c696
--- /dev/null
+++ b/_site/openstack/identity/v2/tokens/urls.go
@@ -0,0 +1,8 @@
+package tokens
+
+import "github.com/rackspace/gophercloud"
+
+// CreateURL generates the URL used to create new Tokens.
+func CreateURL(client *gophercloud.ServiceClient) string {
+ return client.ServiceURL("tokens")
+}
diff --git a/_site/openstack/identity/v3/endpoints/doc.go b/_site/openstack/identity/v3/endpoints/doc.go
new file mode 100644
index 0000000..7d38ee3
--- /dev/null
+++ b/_site/openstack/identity/v3/endpoints/doc.go
@@ -0,0 +1,3 @@
+// Package endpoints queries and manages service endpoints.
+// Reference: http://developer.openstack.org/api-ref-identity-v3.html#endpoints-v3
+package endpoints
diff --git a/_site/openstack/identity/v3/endpoints/errors.go b/_site/openstack/identity/v3/endpoints/errors.go
new file mode 100644
index 0000000..854957f
--- /dev/null
+++ b/_site/openstack/identity/v3/endpoints/errors.go
@@ -0,0 +1,21 @@
+package endpoints
+
+import "fmt"
+
+func requiredAttribute(attribute string) error {
+ return fmt.Errorf("You must specify %s for this endpoint.", attribute)
+}
+
+var (
+ // ErrAvailabilityRequired is reported if an Endpoint is created without an Availability.
+ ErrAvailabilityRequired = requiredAttribute("an availability")
+
+ // ErrNameRequired is reported if an Endpoint is created without a Name.
+ ErrNameRequired = requiredAttribute("a name")
+
+ // ErrURLRequired is reported if an Endpoint is created without a URL.
+ ErrURLRequired = requiredAttribute("a URL")
+
+ // ErrServiceIDRequired is reported if an Endpoint is created without a ServiceID.
+ ErrServiceIDRequired = requiredAttribute("a serviceID")
+)
diff --git a/_site/openstack/identity/v3/endpoints/requests.go b/_site/openstack/identity/v3/endpoints/requests.go
new file mode 100644
index 0000000..eb52573
--- /dev/null
+++ b/_site/openstack/identity/v3/endpoints/requests.go
@@ -0,0 +1,144 @@
+package endpoints
+
+import (
+ "strconv"
+
+ "github.com/racker/perigee"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/openstack/utils"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// EndpointOpts contains the subset of Endpoint attributes that should be used to create or update an Endpoint.
+type EndpointOpts struct {
+ Availability gophercloud.Availability
+ Name string
+ Region string
+ URL string
+ ServiceID string
+}
+
+// Create inserts a new Endpoint into the service catalog.
+// Within EndpointOpts, Region may be omitted by being left as "", but all other fields are required.
+func Create(client *gophercloud.ServiceClient, opts EndpointOpts) CreateResult {
+ // Redefined so that Region can be re-typed as a *string, which can be omitted from the JSON output.
+ type endpoint struct {
+ Interface string `json:"interface"`
+ Name string `json:"name"`
+ Region *string `json:"region,omitempty"`
+ URL string `json:"url"`
+ ServiceID string `json:"service_id"`
+ }
+
+ type request struct {
+ Endpoint endpoint `json:"endpoint"`
+ }
+
+ // Ensure that EndpointOpts is fully populated.
+ if opts.Availability == "" {
+ return createErr(ErrAvailabilityRequired)
+ }
+ if opts.Name == "" {
+ return createErr(ErrNameRequired)
+ }
+ if opts.URL == "" {
+ return createErr(ErrURLRequired)
+ }
+ if opts.ServiceID == "" {
+ return createErr(ErrServiceIDRequired)
+ }
+
+ // Populate the request body.
+ reqBody := request{
+ Endpoint: endpoint{
+ Interface: string(opts.Availability),
+ Name: opts.Name,
+ URL: opts.URL,
+ ServiceID: opts.ServiceID,
+ },
+ }
+ reqBody.Endpoint.Region = gophercloud.MaybeString(opts.Region)
+
+ var result CreateResult
+ _, result.Err = perigee.Request("POST", listURL(client), perigee.Options{
+ MoreHeaders: client.Provider.AuthenticatedHeaders(),
+ ReqBody: &reqBody,
+ Results: &result.Resp,
+ OkCodes: []int{201},
+ })
+ return result
+}
+
+// ListOpts allows finer control over the the endpoints returned by a List call.
+// All fields are optional.
+type ListOpts struct {
+ Availability gophercloud.Availability
+ ServiceID string
+ Page int
+ PerPage int
+}
+
+// List enumerates endpoints in a paginated collection, optionally filtered by ListOpts criteria.
+func List(client *gophercloud.ServiceClient, opts ListOpts) pagination.Pager {
+ q := make(map[string]string)
+ if opts.Availability != "" {
+ q["interface"] = string(opts.Availability)
+ }
+ if opts.ServiceID != "" {
+ q["service_id"] = opts.ServiceID
+ }
+ if opts.Page != 0 {
+ q["page"] = strconv.Itoa(opts.Page)
+ }
+ if opts.PerPage != 0 {
+ q["per_page"] = strconv.Itoa(opts.Page)
+ }
+
+ createPage := func(r pagination.LastHTTPResponse) pagination.Page {
+ return EndpointPage{pagination.LinkedPageBase{LastHTTPResponse: r}}
+ }
+
+ u := listURL(client) + utils.BuildQuery(q)
+ return pagination.NewPager(client, u, createPage)
+}
+
+// Update changes an existing endpoint with new data.
+// All fields are optional in the provided EndpointOpts.
+func Update(client *gophercloud.ServiceClient, endpointID string, opts EndpointOpts) UpdateResult {
+ type endpoint struct {
+ Interface *string `json:"interface,omitempty"`
+ Name *string `json:"name,omitempty"`
+ Region *string `json:"region,omitempty"`
+ URL *string `json:"url,omitempty"`
+ ServiceID *string `json:"service_id,omitempty"`
+ }
+
+ type request struct {
+ Endpoint endpoint `json:"endpoint"`
+ }
+
+ reqBody := request{Endpoint: endpoint{}}
+ reqBody.Endpoint.Interface = gophercloud.MaybeString(string(opts.Availability))
+ reqBody.Endpoint.Name = gophercloud.MaybeString(opts.Name)
+ reqBody.Endpoint.Region = gophercloud.MaybeString(opts.Region)
+ reqBody.Endpoint.URL = gophercloud.MaybeString(opts.URL)
+ reqBody.Endpoint.ServiceID = gophercloud.MaybeString(opts.ServiceID)
+
+ var result UpdateResult
+ _, result.Err = perigee.Request("PATCH", endpointURL(client, endpointID), perigee.Options{
+ MoreHeaders: client.Provider.AuthenticatedHeaders(),
+ ReqBody: &reqBody,
+ Results: &result.Resp,
+ OkCodes: []int{200},
+ })
+ return result
+}
+
+// Delete removes an endpoint from the service catalog.
+func Delete(client *gophercloud.ServiceClient, endpointID string) error {
+ _, err := perigee.Request("DELETE", endpointURL(client, endpointID), perigee.Options{
+ MoreHeaders: client.Provider.AuthenticatedHeaders(),
+ OkCodes: []int{204},
+ })
+ return err
+}
diff --git a/_site/openstack/identity/v3/endpoints/requests_test.go b/_site/openstack/identity/v3/endpoints/requests_test.go
new file mode 100644
index 0000000..c30bd55
--- /dev/null
+++ b/_site/openstack/identity/v3/endpoints/requests_test.go
@@ -0,0 +1,243 @@
+package endpoints
+
+import (
+ "fmt"
+ "net/http"
+ "reflect"
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+ "github.com/rackspace/gophercloud/testhelper"
+)
+
+const tokenID = "abcabcabcabc"
+
+func serviceClient() *gophercloud.ServiceClient {
+ return &gophercloud.ServiceClient{
+ Provider: &gophercloud.ProviderClient{TokenID: tokenID},
+ Endpoint: testhelper.Endpoint(),
+ }
+}
+
+func TestCreateSuccessful(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ testhelper.Mux.HandleFunc("/endpoints", func(w http.ResponseWriter, r *http.Request) {
+ testhelper.TestMethod(t, r, "POST")
+ testhelper.TestHeader(t, r, "X-Auth-Token", tokenID)
+ testhelper.TestJSONRequest(t, r, `
+ {
+ "endpoint": {
+ "interface": "public",
+ "name": "the-endiest-of-points",
+ "region": "underground",
+ "url": "https://1.2.3.4:9000/",
+ "service_id": "asdfasdfasdfasdf"
+ }
+ }
+ `)
+
+ w.WriteHeader(http.StatusCreated)
+ fmt.Fprintf(w, `
+ {
+ "endpoint": {
+ "id": "12",
+ "interface": "public",
+ "links": {
+ "self": "https://localhost:5000/v3/endpoints/12"
+ },
+ "name": "the-endiest-of-points",
+ "region": "underground",
+ "service_id": "asdfasdfasdfasdf",
+ "url": "https://1.2.3.4:9000/"
+ }
+ }
+ `)
+ })
+
+ client := serviceClient()
+
+ actual, err := Create(client, EndpointOpts{
+ Availability: gophercloud.AvailabilityPublic,
+ Name: "the-endiest-of-points",
+ Region: "underground",
+ URL: "https://1.2.3.4:9000/",
+ ServiceID: "asdfasdfasdfasdf",
+ }).Extract()
+ if err != nil {
+ t.Fatalf("Unable to create an endpoint: %v", err)
+ }
+
+ expected := &Endpoint{
+ ID: "12",
+ Availability: gophercloud.AvailabilityPublic,
+ Name: "the-endiest-of-points",
+ Region: "underground",
+ ServiceID: "asdfasdfasdfasdf",
+ URL: "https://1.2.3.4:9000/",
+ }
+
+ if !reflect.DeepEqual(actual, expected) {
+ t.Errorf("Expected %#v, was %#v", expected, actual)
+ }
+}
+
+func TestListEndpoints(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ testhelper.Mux.HandleFunc("/endpoints", func(w http.ResponseWriter, r *http.Request) {
+ testhelper.TestMethod(t, r, "GET")
+ testhelper.TestHeader(t, r, "X-Auth-Token", tokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+ fmt.Fprintf(w, `
+ {
+ "endpoints": [
+ {
+ "id": "12",
+ "interface": "public",
+ "links": {
+ "self": "https://localhost:5000/v3/endpoints/12"
+ },
+ "name": "the-endiest-of-points",
+ "region": "underground",
+ "service_id": "asdfasdfasdfasdf",
+ "url": "https://1.2.3.4:9000/"
+ },
+ {
+ "id": "13",
+ "interface": "internal",
+ "links": {
+ "self": "https://localhost:5000/v3/endpoints/13"
+ },
+ "name": "shhhh",
+ "region": "underground",
+ "service_id": "asdfasdfasdfasdf",
+ "url": "https://1.2.3.4:9001/"
+ }
+ ],
+ "links": {
+ "next": null,
+ "previous": null
+ }
+ }
+ `)
+ })
+
+ client := serviceClient()
+
+ count := 0
+ List(client, ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractEndpoints(page)
+ if err != nil {
+ t.Errorf("Failed to extract endpoints: %v", err)
+ return false, err
+ }
+
+ expected := []Endpoint{
+ Endpoint{
+ ID: "12",
+ Availability: gophercloud.AvailabilityPublic,
+ Name: "the-endiest-of-points",
+ Region: "underground",
+ ServiceID: "asdfasdfasdfasdf",
+ URL: "https://1.2.3.4:9000/",
+ },
+ Endpoint{
+ ID: "13",
+ Availability: gophercloud.AvailabilityInternal,
+ Name: "shhhh",
+ Region: "underground",
+ ServiceID: "asdfasdfasdfasdf",
+ URL: "https://1.2.3.4:9001/",
+ },
+ }
+
+ if !reflect.DeepEqual(expected, actual) {
+ t.Errorf("Expected %#v, got %#v", expected, actual)
+ }
+
+ return true, nil
+ })
+ if count != 1 {
+ t.Errorf("Expected 1 page, got %d", count)
+ }
+}
+
+func TestUpdateEndpoint(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ testhelper.Mux.HandleFunc("/endpoints/12", func(w http.ResponseWriter, r *http.Request) {
+ testhelper.TestMethod(t, r, "PATCH")
+ testhelper.TestHeader(t, r, "X-Auth-Token", tokenID)
+ testhelper.TestJSONRequest(t, r, `
+ {
+ "endpoint": {
+ "name": "renamed",
+ "region": "somewhere-else"
+ }
+ }
+ `)
+
+ fmt.Fprintf(w, `
+ {
+ "endpoint": {
+ "id": "12",
+ "interface": "public",
+ "links": {
+ "self": "https://localhost:5000/v3/endpoints/12"
+ },
+ "name": "renamed",
+ "region": "somewhere-else",
+ "service_id": "asdfasdfasdfasdf",
+ "url": "https://1.2.3.4:9000/"
+ }
+ }
+ `)
+ })
+
+ client := serviceClient()
+ actual, err := Update(client, "12", EndpointOpts{
+ Name: "renamed",
+ Region: "somewhere-else",
+ }).Extract()
+ if err != nil {
+ t.Fatalf("Unexpected error from Update: %v", err)
+ }
+
+ expected := &Endpoint{
+ ID: "12",
+ Availability: gophercloud.AvailabilityPublic,
+ Name: "renamed",
+ Region: "somewhere-else",
+ ServiceID: "asdfasdfasdfasdf",
+ URL: "https://1.2.3.4:9000/",
+ }
+ if !reflect.DeepEqual(expected, actual) {
+ t.Errorf("Expected %#v, was %#v", expected, actual)
+ }
+}
+
+func TestDeleteEndpoint(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ testhelper.Mux.HandleFunc("/endpoints/34", func(w http.ResponseWriter, r *http.Request) {
+ testhelper.TestMethod(t, r, "DELETE")
+ testhelper.TestHeader(t, r, "X-Auth-Token", tokenID)
+
+ w.WriteHeader(http.StatusNoContent)
+ })
+
+ client := serviceClient()
+
+ err := Delete(client, "34")
+ if err != nil {
+ t.Fatalf("Unexpected error from Delete: %v", err)
+ }
+}
diff --git a/_site/openstack/identity/v3/endpoints/results.go b/_site/openstack/identity/v3/endpoints/results.go
new file mode 100644
index 0000000..d1c2472
--- /dev/null
+++ b/_site/openstack/identity/v3/endpoints/results.go
@@ -0,0 +1,77 @@
+package endpoints
+
+import (
+ "github.com/mitchellh/mapstructure"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+type commonResult struct {
+ gophercloud.CommonResult
+}
+
+// Extract interprets a GetResult, CreateResult or UpdateResult as a concrete Endpoint.
+// An error is returned if the original call or the extraction failed.
+func (r commonResult) Extract() (*Endpoint, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var res struct {
+ Endpoint `json:"endpoint"`
+ }
+
+ err := mapstructure.Decode(r.Resp, &res)
+
+ return &res.Endpoint, err
+}
+
+// CreateResult is the deferred result of a Create call.
+type CreateResult struct {
+ commonResult
+}
+
+// createErr quickly wraps an error in a CreateResult.
+func createErr(err error) CreateResult {
+ return CreateResult{commonResult{gophercloud.CommonResult{Err: err}}}
+}
+
+// UpdateResult is the deferred result of an Update call.
+type UpdateResult struct {
+ commonResult
+}
+
+// Endpoint describes the entry point for another service's API.
+type Endpoint struct {
+ ID string `mapstructure:"id" json:"id"`
+ Availability gophercloud.Availability `mapstructure:"interface" json:"interface"`
+ Name string `mapstructure:"name" json:"name"`
+ Region string `mapstructure:"region" json:"region"`
+ ServiceID string `mapstructure:"service_id" json:"service_id"`
+ URL string `mapstructure:"url" json:"url"`
+}
+
+// EndpointPage is a single page of Endpoint results.
+type EndpointPage struct {
+ pagination.LinkedPageBase
+}
+
+// IsEmpty returns true if no Endpoints were returned.
+func (p EndpointPage) IsEmpty() (bool, error) {
+ es, err := ExtractEndpoints(p)
+ if err != nil {
+ return true, err
+ }
+ return len(es) == 0, nil
+}
+
+// ExtractEndpoints extracts an Endpoint slice from a Page.
+func ExtractEndpoints(page pagination.Page) ([]Endpoint, error) {
+ var response struct {
+ Endpoints []Endpoint `mapstructure:"endpoints"`
+ }
+
+ err := mapstructure.Decode(page.(EndpointPage).Body, &response)
+
+ return response.Endpoints, err
+}
diff --git a/_site/openstack/identity/v3/endpoints/urls.go b/_site/openstack/identity/v3/endpoints/urls.go
new file mode 100644
index 0000000..547d7b1
--- /dev/null
+++ b/_site/openstack/identity/v3/endpoints/urls.go
@@ -0,0 +1,11 @@
+package endpoints
+
+import "github.com/rackspace/gophercloud"
+
+func listURL(client *gophercloud.ServiceClient) string {
+ return client.ServiceURL("endpoints")
+}
+
+func endpointURL(client *gophercloud.ServiceClient, endpointID string) string {
+ return client.ServiceURL("endpoints", endpointID)
+}
diff --git a/_site/openstack/identity/v3/endpoints/urls_test.go b/_site/openstack/identity/v3/endpoints/urls_test.go
new file mode 100644
index 0000000..0b183b7
--- /dev/null
+++ b/_site/openstack/identity/v3/endpoints/urls_test.go
@@ -0,0 +1,23 @@
+package endpoints
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+)
+
+func TestGetListURL(t *testing.T) {
+ client := gophercloud.ServiceClient{Endpoint: "http://localhost:5000/v3/"}
+ url := listURL(&client)
+ if url != "http://localhost:5000/v3/endpoints" {
+ t.Errorf("Unexpected list URL generated: [%s]", url)
+ }
+}
+
+func TestGetEndpointURL(t *testing.T) {
+ client := gophercloud.ServiceClient{Endpoint: "http://localhost:5000/v3/"}
+ url := endpointURL(&client, "1234")
+ if url != "http://localhost:5000/v3/endpoints/1234" {
+ t.Errorf("Unexpected service URL generated: [%s]", url)
+ }
+}
diff --git a/_site/openstack/identity/v3/services/doc.go b/_site/openstack/identity/v3/services/doc.go
new file mode 100644
index 0000000..c4772c0
--- /dev/null
+++ b/_site/openstack/identity/v3/services/doc.go
@@ -0,0 +1,4 @@
+/*
+Package services queries and manages the service catalog.
+*/
+package services
diff --git a/_site/openstack/identity/v3/services/requests.go b/_site/openstack/identity/v3/services/requests.go
new file mode 100644
index 0000000..7816aca
--- /dev/null
+++ b/_site/openstack/identity/v3/services/requests.go
@@ -0,0 +1,99 @@
+package services
+
+import (
+ "strconv"
+
+ "github.com/racker/perigee"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/openstack/utils"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+type response struct {
+ Service Service `json:"service"`
+}
+
+// Create adds a new service of the requested type to the catalog.
+func Create(client *gophercloud.ServiceClient, serviceType string) CreateResult {
+ type request struct {
+ Type string `json:"type"`
+ }
+
+ req := request{Type: serviceType}
+
+ var result CreateResult
+ _, result.Err = perigee.Request("POST", listURL(client), perigee.Options{
+ MoreHeaders: client.Provider.AuthenticatedHeaders(),
+ ReqBody: &req,
+ Results: &result.Resp,
+ OkCodes: []int{201},
+ })
+ return result
+}
+
+// ListOpts allows you to query the List method.
+type ListOpts struct {
+ ServiceType string
+ PerPage int
+ Page int
+}
+
+// List enumerates the services available to a specific user.
+func List(client *gophercloud.ServiceClient, opts ListOpts) pagination.Pager {
+ q := make(map[string]string)
+ if opts.ServiceType != "" {
+ q["type"] = opts.ServiceType
+ }
+ if opts.Page != 0 {
+ q["page"] = strconv.Itoa(opts.Page)
+ }
+ if opts.PerPage != 0 {
+ q["perPage"] = strconv.Itoa(opts.PerPage)
+ }
+ u := listURL(client) + utils.BuildQuery(q)
+
+ createPage := func(r pagination.LastHTTPResponse) pagination.Page {
+ return ServicePage{pagination.LinkedPageBase{LastHTTPResponse: r}}
+ }
+
+ return pagination.NewPager(client, u, createPage)
+}
+
+// Get returns additional information about a service, given its ID.
+func Get(client *gophercloud.ServiceClient, serviceID string) GetResult {
+ var result GetResult
+ _, result.Err = perigee.Request("GET", serviceURL(client, serviceID), perigee.Options{
+ MoreHeaders: client.Provider.AuthenticatedHeaders(),
+ Results: &result.Resp,
+ OkCodes: []int{200},
+ })
+ return result
+}
+
+// Update changes the service type of an existing service.
+func Update(client *gophercloud.ServiceClient, serviceID string, serviceType string) UpdateResult {
+ type request struct {
+ Type string `json:"type"`
+ }
+
+ req := request{Type: serviceType}
+
+ var result UpdateResult
+ _, result.Err = perigee.Request("PATCH", serviceURL(client, serviceID), perigee.Options{
+ MoreHeaders: client.Provider.AuthenticatedHeaders(),
+ ReqBody: &req,
+ Results: &result.Resp,
+ OkCodes: []int{200},
+ })
+ return result
+}
+
+// Delete removes an existing service.
+// It either deletes all associated endpoints, or fails until all endpoints are deleted.
+func Delete(client *gophercloud.ServiceClient, serviceID string) error {
+ _, err := perigee.Request("DELETE", serviceURL(client, serviceID), perigee.Options{
+ MoreHeaders: client.Provider.AuthenticatedHeaders(),
+ OkCodes: []int{204},
+ })
+ return err
+}
diff --git a/_site/openstack/identity/v3/services/requests_test.go b/_site/openstack/identity/v3/services/requests_test.go
new file mode 100644
index 0000000..a3d345b
--- /dev/null
+++ b/_site/openstack/identity/v3/services/requests_test.go
@@ -0,0 +1,232 @@
+package services
+
+import (
+ "fmt"
+ "net/http"
+ "reflect"
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+ "github.com/rackspace/gophercloud/testhelper"
+)
+
+const tokenID = "111111"
+
+func serviceClient() *gophercloud.ServiceClient {
+ return &gophercloud.ServiceClient{
+ Provider: &gophercloud.ProviderClient{
+ TokenID: tokenID,
+ },
+ Endpoint: testhelper.Endpoint(),
+ }
+}
+
+func TestCreateSuccessful(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ testhelper.Mux.HandleFunc("/services", func(w http.ResponseWriter, r *http.Request) {
+ testhelper.TestMethod(t, r, "POST")
+ testhelper.TestHeader(t, r, "X-Auth-Token", tokenID)
+ testhelper.TestJSONRequest(t, r, `{ "type": "compute" }`)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusCreated)
+ fmt.Fprintf(w, `{
+ "service": {
+ "description": "Here's your service",
+ "id": "1234",
+ "name": "InscrutableOpenStackProjectName",
+ "type": "compute"
+ }
+ }`)
+ })
+
+ client := serviceClient()
+
+ result, err := Create(client, "compute").Extract()
+ if err != nil {
+ t.Fatalf("Unexpected error from Create: %v", err)
+ }
+
+ if result.Description == nil || *result.Description != "Here's your service" {
+ t.Errorf("Service description was unexpected [%s]", result.Description)
+ }
+ if result.ID != "1234" {
+ t.Errorf("Service ID was unexpected [%s]", result.ID)
+ }
+ if result.Name != "InscrutableOpenStackProjectName" {
+ t.Errorf("Service name was unexpected [%s]", result.Name)
+ }
+ if result.Type != "compute" {
+ t.Errorf("Service type was unexpected [%s]", result.Type)
+ }
+}
+
+func TestListSinglePage(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ testhelper.Mux.HandleFunc("/services", func(w http.ResponseWriter, r *http.Request) {
+ testhelper.TestMethod(t, r, "GET")
+ testhelper.TestHeader(t, r, "X-Auth-Token", tokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+ fmt.Fprintf(w, `
+ {
+ "links": {
+ "next": null,
+ "previous": null
+ },
+ "services": [
+ {
+ "description": "Service One",
+ "id": "1234",
+ "name": "service-one",
+ "type": "identity"
+ },
+ {
+ "description": "Service Two",
+ "id": "9876",
+ "name": "service-two",
+ "type": "compute"
+ }
+ ]
+ }
+ `)
+ })
+
+ client := serviceClient()
+
+ count := 0
+ err := List(client, ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractServices(page)
+ if err != nil {
+ return false, err
+ }
+
+ desc0 := "Service One"
+ desc1 := "Service Two"
+ expected := []Service{
+ Service{
+ Description: &desc0,
+ ID: "1234",
+ Name: "service-one",
+ Type: "identity",
+ },
+ Service{
+ Description: &desc1,
+ ID: "9876",
+ Name: "service-two",
+ Type: "compute",
+ },
+ }
+
+ if !reflect.DeepEqual(expected, actual) {
+ t.Errorf("Expected %#v, got %#v", expected, actual)
+ }
+
+ return true, nil
+ })
+ if err != nil {
+ t.Errorf("Unexpected error while paging: %v", err)
+ }
+ if count != 1 {
+ t.Errorf("Expected 1 page, got %d", count)
+ }
+}
+
+func TestGetSuccessful(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ testhelper.Mux.HandleFunc("/services/12345", func(w http.ResponseWriter, r *http.Request) {
+ testhelper.TestMethod(t, r, "GET")
+ testhelper.TestHeader(t, r, "X-Auth-Token", tokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+ fmt.Fprintf(w, `
+ {
+ "service": {
+ "description": "Service One",
+ "id": "12345",
+ "name": "service-one",
+ "type": "identity"
+ }
+ }
+ `)
+ })
+
+ client := serviceClient()
+
+ result, err := Get(client, "12345").Extract()
+ if err != nil {
+ t.Fatalf("Error fetching service information: %v", err)
+ }
+
+ if result.ID != "12345" {
+ t.Errorf("Unexpected service ID: %s", result.ID)
+ }
+ if *result.Description != "Service One" {
+ t.Errorf("Unexpected service description: [%s]", *result.Description)
+ }
+ if result.Name != "service-one" {
+ t.Errorf("Unexpected service name: [%s]", result.Name)
+ }
+ if result.Type != "identity" {
+ t.Errorf("Unexpected service type: [%s]", result.Type)
+ }
+}
+
+func TestUpdateSuccessful(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ testhelper.Mux.HandleFunc("/services/12345", func(w http.ResponseWriter, r *http.Request) {
+ testhelper.TestMethod(t, r, "PATCH")
+ testhelper.TestHeader(t, r, "X-Auth-Token", tokenID)
+ testhelper.TestJSONRequest(t, r, `{ "type": "lasermagic" }`)
+
+ w.Header().Add("Content-Type", "application/json")
+ fmt.Fprintf(w, `
+ {
+ "service": {
+ "id": "12345",
+ "type": "lasermagic"
+ }
+ }
+ `)
+ })
+
+ client := serviceClient()
+
+ result, err := Update(client, "12345", "lasermagic").Extract()
+ if err != nil {
+ t.Fatalf("Unable to update service: %v", err)
+ }
+
+ if result.ID != "12345" {
+ t.Fatalf("Expected ID 12345, was %s", result.ID)
+ }
+}
+
+func TestDeleteSuccessful(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ testhelper.Mux.HandleFunc("/services/12345", func(w http.ResponseWriter, r *http.Request) {
+ testhelper.TestMethod(t, r, "DELETE")
+ testhelper.TestHeader(t, r, "X-Auth-Token", tokenID)
+
+ w.WriteHeader(http.StatusNoContent)
+ })
+
+ client := serviceClient()
+
+ err := Delete(client, "12345")
+ if err != nil {
+ t.Fatalf("Unable to delete service: %v", err)
+ }
+}
diff --git a/_site/openstack/identity/v3/services/results.go b/_site/openstack/identity/v3/services/results.go
new file mode 100644
index 0000000..e4e068b
--- /dev/null
+++ b/_site/openstack/identity/v3/services/results.go
@@ -0,0 +1,75 @@
+package services
+
+import (
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+
+ "github.com/mitchellh/mapstructure"
+)
+
+type commonResult struct {
+ gophercloud.CommonResult
+}
+
+// Extract interprets a GetResult, CreateResult or UpdateResult as a concrete Service.
+// An error is returned if the original call or the extraction failed.
+func (r commonResult) Extract() (*Service, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var res struct {
+ Service `json:"service"`
+ }
+
+ err := mapstructure.Decode(r.Resp, &res)
+
+ return &res.Service, err
+}
+
+// CreateResult is the deferred result of a Create call.
+type CreateResult struct {
+ commonResult
+}
+
+// GetResult is the deferred result of a Get call.
+type GetResult struct {
+ commonResult
+}
+
+// UpdateResult is the deferred result of an Update call.
+type UpdateResult struct {
+ commonResult
+}
+
+// Service is the result of a list or information query.
+type Service struct {
+ Description *string `json:"description,omitempty"`
+ ID string `json:"id"`
+ Name string `json:"name"`
+ Type string `json:"type"`
+}
+
+// ServicePage is a single page of Service results.
+type ServicePage struct {
+ pagination.LinkedPageBase
+}
+
+// IsEmpty returns true if the page contains no results.
+func (p ServicePage) IsEmpty() (bool, error) {
+ services, err := ExtractServices(p)
+ if err != nil {
+ return true, err
+ }
+ return len(services) == 0, nil
+}
+
+// ExtractServices extracts a slice of Services from a Collection acquired from List.
+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
+}
diff --git a/_site/openstack/identity/v3/services/urls.go b/_site/openstack/identity/v3/services/urls.go
new file mode 100644
index 0000000..85443a4
--- /dev/null
+++ b/_site/openstack/identity/v3/services/urls.go
@@ -0,0 +1,11 @@
+package services
+
+import "github.com/rackspace/gophercloud"
+
+func listURL(client *gophercloud.ServiceClient) string {
+ return client.ServiceURL("services")
+}
+
+func serviceURL(client *gophercloud.ServiceClient, serviceID string) string {
+ return client.ServiceURL("services", serviceID)
+}
diff --git a/_site/openstack/identity/v3/services/urls_test.go b/_site/openstack/identity/v3/services/urls_test.go
new file mode 100644
index 0000000..5a31b32
--- /dev/null
+++ b/_site/openstack/identity/v3/services/urls_test.go
@@ -0,0 +1,23 @@
+package services
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+)
+
+func TestListURL(t *testing.T) {
+ client := gophercloud.ServiceClient{Endpoint: "http://localhost:5000/v3/"}
+ url := listURL(&client)
+ if url != "http://localhost:5000/v3/services" {
+ t.Errorf("Unexpected list URL generated: [%s]", url)
+ }
+}
+
+func TestServiceURL(t *testing.T) {
+ client := gophercloud.ServiceClient{Endpoint: "http://localhost:5000/v3/"}
+ url := serviceURL(&client, "1234")
+ if url != "http://localhost:5000/v3/services/1234" {
+ t.Errorf("Unexpected service URL generated: [%s]", url)
+ }
+}
diff --git a/_site/openstack/identity/v3/tokens/doc.go b/_site/openstack/identity/v3/tokens/doc.go
new file mode 100644
index 0000000..02fce0d
--- /dev/null
+++ b/_site/openstack/identity/v3/tokens/doc.go
@@ -0,0 +1,6 @@
+/*
+Package tokens defines operations performed on the token resource.
+
+Documentation: http://developer.openstack.org/api-ref-identity-v3.html#tokens-v3
+*/
+package tokens
diff --git a/_site/openstack/identity/v3/tokens/errors.go b/_site/openstack/identity/v3/tokens/errors.go
new file mode 100644
index 0000000..4476109
--- /dev/null
+++ b/_site/openstack/identity/v3/tokens/errors.go
@@ -0,0 +1,72 @@
+package tokens
+
+import (
+ "errors"
+ "fmt"
+)
+
+func unacceptedAttributeErr(attribute string) error {
+ return fmt.Errorf("The base Identity V3 API does not accept authentication by %s", attribute)
+}
+
+func redundantWithTokenErr(attribute string) error {
+ return fmt.Errorf("%s may not be provided when authenticating with a TokenID", attribute)
+}
+
+func redundantWithUserID(attribute string) error {
+ return fmt.Errorf("%s may not be provided when authenticating with a UserID", attribute)
+}
+
+var (
+ // ErrAPIKeyProvided indicates that an APIKey was provided but can't be used.
+ ErrAPIKeyProvided = unacceptedAttributeErr("APIKey")
+
+ // ErrTenantIDProvided indicates that a TenantID was provided but can't be used.
+ ErrTenantIDProvided = unacceptedAttributeErr("TenantID")
+
+ // ErrTenantNameProvided indicates that a TenantName was provided but can't be used.
+ ErrTenantNameProvided = unacceptedAttributeErr("TenantName")
+
+ // ErrUsernameWithToken indicates that a Username was provided, but token authentication is being used instead.
+ ErrUsernameWithToken = redundantWithTokenErr("Username")
+
+ // ErrUserIDWithToken indicates that a UserID was provided, but token authentication is being used instead.
+ ErrUserIDWithToken = redundantWithTokenErr("UserID")
+
+ // ErrDomainIDWithToken indicates that a DomainID was provided, but token authentication is being used instead.
+ ErrDomainIDWithToken = redundantWithTokenErr("DomainID")
+
+ // ErrDomainNameWithToken indicates that a DomainName was provided, but token authentication is being used instead.s
+ ErrDomainNameWithToken = redundantWithTokenErr("DomainName")
+
+ // ErrUsernameOrUserID indicates that neither username nor userID are specified, or both are at once.
+ ErrUsernameOrUserID = errors.New("Exactly one of Username and UserID must be provided for password authentication")
+
+ // ErrDomainIDWithUserID indicates that a DomainID was provided, but unnecessary because a UserID is being used.
+ ErrDomainIDWithUserID = redundantWithUserID("DomainID")
+
+ // ErrDomainNameWithUserID indicates that a DomainName was provided, but unnecessary because a UserID is being used.
+ ErrDomainNameWithUserID = redundantWithUserID("DomainName")
+
+ // ErrDomainIDOrDomainName indicates that a username was provided, but no domain to scope it.
+ // It may also indicate that both a DomainID and a DomainName were provided at once.
+ ErrDomainIDOrDomainName = errors.New("You must provide exactly one of DomainID or DomainName to authenticate by Username")
+
+ // ErrMissingPassword indicates that no password was provided and no token is available.
+ ErrMissingPassword = errors.New("You must provide a password to authenticate")
+
+ // ErrScopeDomainIDOrDomainName indicates that a domain ID or Name was required in a Scope, but not present.
+ ErrScopeDomainIDOrDomainName = errors.New("You must provide exactly one of DomainID or DomainName in a Scope with ProjectName")
+
+ // ErrScopeProjectIDOrProjectName indicates that both a ProjectID and a ProjectName were provided in a Scope.
+ ErrScopeProjectIDOrProjectName = errors.New("You must provide at most one of ProjectID or ProjectName in a Scope")
+
+ // ErrScopeProjectIDAlone indicates that a ProjectID was provided with other constraints in a Scope.
+ ErrScopeProjectIDAlone = errors.New("ProjectID must be supplied alone in a Scope")
+
+ // ErrScopeDomainName indicates that a DomainName was provided alone in a Scope.
+ ErrScopeDomainName = errors.New("DomainName must be supplied with a ProjectName or ProjectID in a Scope.")
+
+ // ErrScopeEmpty indicates that no credentials were provided in a Scope.
+ ErrScopeEmpty = errors.New("You must provide either a Project or Domain in a Scope")
+)
diff --git a/_site/openstack/identity/v3/tokens/requests.go b/_site/openstack/identity/v3/tokens/requests.go
new file mode 100644
index 0000000..c8587b6
--- /dev/null
+++ b/_site/openstack/identity/v3/tokens/requests.go
@@ -0,0 +1,285 @@
+package tokens
+
+import (
+ "github.com/racker/perigee"
+ "github.com/rackspace/gophercloud"
+)
+
+// Scope allows a created token to be limited to a specific domain or project.
+type Scope struct {
+ ProjectID string
+ ProjectName string
+ DomainID string
+ DomainName string
+}
+
+func subjectTokenHeaders(c *gophercloud.ServiceClient, subjectToken string) map[string]string {
+ h := c.Provider.AuthenticatedHeaders()
+ h["X-Subject-Token"] = subjectToken
+ return h
+}
+
+// Create authenticates and either generates a new token, or changes the Scope of an existing token.
+func Create(c *gophercloud.ServiceClient, options gophercloud.AuthOptions, scope *Scope) CreateResult {
+ type domainReq struct {
+ ID *string `json:"id,omitempty"`
+ Name *string `json:"name,omitempty"`
+ }
+
+ type projectReq struct {
+ Domain *domainReq `json:"domain,omitempty"`
+ Name *string `json:"name,omitempty"`
+ ID *string `json:"id,omitempty"`
+ }
+
+ type userReq struct {
+ ID *string `json:"id,omitempty"`
+ Name *string `json:"name,omitempty"`
+ Password string `json:"password"`
+ Domain *domainReq `json:"domain,omitempty"`
+ }
+
+ type passwordReq struct {
+ User userReq `json:"user"`
+ }
+
+ type tokenReq struct {
+ ID string `json:"id"`
+ }
+
+ type identityReq struct {
+ Methods []string `json:"methods"`
+ Password *passwordReq `json:"password,omitempty"`
+ Token *tokenReq `json:"token,omitempty"`
+ }
+
+ type scopeReq struct {
+ Domain *domainReq `json:"domain,omitempty"`
+ Project *projectReq `json:"project,omitempty"`
+ }
+
+ type authReq struct {
+ Identity identityReq `json:"identity"`
+ Scope *scopeReq `json:"scope,omitempty"`
+ }
+
+ type request struct {
+ Auth authReq `json:"auth"`
+ }
+
+ // Populate the request structure based on the provided arguments. Create and return an error
+ // if insufficient or incompatible information is present.
+ var req request
+
+ // Test first for unrecognized arguments.
+ if options.APIKey != "" {
+ return createErr(ErrAPIKeyProvided)
+ }
+ if options.TenantID != "" {
+ return createErr(ErrTenantIDProvided)
+ }
+ if options.TenantName != "" {
+ return createErr(ErrTenantNameProvided)
+ }
+
+ if options.Password == "" {
+ if c.Provider.TokenID != "" {
+ // Because we aren't using password authentication, it's an error to also provide any of the user-based authentication
+ // parameters.
+ if options.Username != "" {
+ return createErr(ErrUsernameWithToken)
+ }
+ if options.UserID != "" {
+ return createErr(ErrUserIDWithToken)
+ }
+ if options.DomainID != "" {
+ return createErr(ErrDomainIDWithToken)
+ }
+ if options.DomainName != "" {
+ return createErr(ErrDomainNameWithToken)
+ }
+
+ // Configure the request for Token authentication.
+ req.Auth.Identity.Methods = []string{"token"}
+ req.Auth.Identity.Token = &tokenReq{
+ ID: c.Provider.TokenID,
+ }
+ } else {
+ // If no password or token ID are available, authentication can't continue.
+ return createErr(ErrMissingPassword)
+ }
+ } else {
+ // Password authentication.
+ req.Auth.Identity.Methods = []string{"password"}
+
+ // At least one of Username and UserID must be specified.
+ if options.Username == "" && options.UserID == "" {
+ return createErr(ErrUsernameOrUserID)
+ }
+
+ if options.Username != "" {
+ // If Username is provided, UserID may not be provided.
+ if options.UserID != "" {
+ return createErr(ErrUsernameOrUserID)
+ }
+
+ // Either DomainID or DomainName must also be specified.
+ if options.DomainID == "" && options.DomainName == "" {
+ return createErr(ErrDomainIDOrDomainName)
+ }
+
+ if options.DomainID != "" {
+ if options.DomainName != "" {
+ return createErr(ErrDomainIDOrDomainName)
+ }
+
+ // Configure the request for Username and Password authentication with a DomainID.
+ req.Auth.Identity.Password = &passwordReq{
+ User: userReq{
+ Name: &options.Username,
+ Password: options.Password,
+ Domain: &domainReq{ID: &options.DomainID},
+ },
+ }
+ }
+
+ if options.DomainName != "" {
+ // Configure the request for Username and Password authentication with a DomainName.
+ req.Auth.Identity.Password = &passwordReq{
+ User: userReq{
+ Name: &options.Username,
+ Password: options.Password,
+ Domain: &domainReq{Name: &options.DomainName},
+ },
+ }
+ }
+ }
+
+ if options.UserID != "" {
+ // If UserID is specified, neither DomainID nor DomainName may be.
+ if options.DomainID != "" {
+ return createErr(ErrDomainIDWithUserID)
+ }
+ if options.DomainName != "" {
+ return createErr(ErrDomainNameWithUserID)
+ }
+
+ // Configure the request for UserID and Password authentication.
+ req.Auth.Identity.Password = &passwordReq{
+ User: userReq{ID: &options.UserID, Password: options.Password},
+ }
+ }
+ }
+
+ // Add a "scope" element if a Scope has been provided.
+ if scope != nil {
+ if scope.ProjectName != "" {
+ // ProjectName provided: either DomainID or DomainName must also be supplied.
+ // ProjectID may not be supplied.
+ if scope.DomainID == "" && scope.DomainName == "" {
+ return createErr(ErrScopeDomainIDOrDomainName)
+ }
+ if scope.ProjectID != "" {
+ return createErr(ErrScopeProjectIDOrProjectName)
+ }
+
+ if scope.DomainID != "" {
+ // ProjectName + DomainID
+ req.Auth.Scope = &scopeReq{
+ Project: &projectReq{
+ Name: &scope.ProjectName,
+ Domain: &domainReq{ID: &scope.DomainID},
+ },
+ }
+ }
+
+ if scope.DomainName != "" {
+ // ProjectName + DomainName
+ req.Auth.Scope = &scopeReq{
+ Project: &projectReq{
+ Name: &scope.ProjectName,
+ Domain: &domainReq{Name: &scope.DomainName},
+ },
+ }
+ }
+ } else if scope.ProjectID != "" {
+ // ProjectID provided. ProjectName, DomainID, and DomainName may not be provided.
+ if scope.DomainID != "" {
+ return createErr(ErrScopeProjectIDAlone)
+ }
+ if scope.DomainName != "" {
+ return createErr(ErrScopeProjectIDAlone)
+ }
+
+ // ProjectID
+ req.Auth.Scope = &scopeReq{
+ Project: &projectReq{ID: &scope.ProjectID},
+ }
+ } else if scope.DomainID != "" {
+ // DomainID provided. ProjectID, ProjectName, and DomainName may not be provided.
+ if scope.DomainName != "" {
+ return createErr(ErrScopeDomainIDOrDomainName)
+ }
+
+ // DomainID
+ req.Auth.Scope = &scopeReq{
+ Domain: &domainReq{ID: &scope.DomainID},
+ }
+ } else if scope.DomainName != "" {
+ return createErr(ErrScopeDomainName)
+ } else {
+ return createErr(ErrScopeEmpty)
+ }
+ }
+
+ var result CreateResult
+ var response *perigee.Response
+ response, result.Err = perigee.Request("POST", tokenURL(c), perigee.Options{
+ ReqBody: &req,
+ Results: &result.Resp,
+ OkCodes: []int{201},
+ })
+ if result.Err != nil {
+ return result
+ }
+ result.header = response.HttpResponse.Header
+ return result
+}
+
+// Get validates and retrieves information about another token.
+func Get(c *gophercloud.ServiceClient, token string) GetResult {
+ var result GetResult
+ var response *perigee.Response
+ response, result.Err = perigee.Request("GET", tokenURL(c), perigee.Options{
+ MoreHeaders: subjectTokenHeaders(c, token),
+ Results: &result.Resp,
+ OkCodes: []int{200, 203},
+ })
+ if result.Err != nil {
+ return result
+ }
+ result.header = response.HttpResponse.Header
+ return result
+}
+
+// Validate determines if a specified token is valid or not.
+func Validate(c *gophercloud.ServiceClient, token string) (bool, error) {
+ response, err := perigee.Request("HEAD", tokenURL(c), perigee.Options{
+ MoreHeaders: subjectTokenHeaders(c, token),
+ OkCodes: []int{204, 404},
+ })
+ if err != nil {
+ return false, err
+ }
+
+ return response.StatusCode == 204, nil
+}
+
+// Revoke immediately makes specified token invalid.
+func Revoke(c *gophercloud.ServiceClient, token string) error {
+ _, err := perigee.Request("DELETE", tokenURL(c), perigee.Options{
+ MoreHeaders: subjectTokenHeaders(c, token),
+ OkCodes: []int{204},
+ })
+ return err
+}
diff --git a/_site/openstack/identity/v3/tokens/requests_test.go b/_site/openstack/identity/v3/tokens/requests_test.go
new file mode 100644
index 0000000..367c73c
--- /dev/null
+++ b/_site/openstack/identity/v3/tokens/requests_test.go
@@ -0,0 +1,516 @@
+package tokens
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+ "time"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/testhelper"
+)
+
+// authTokenPost verifies that providing certain AuthOptions and Scope results in an expected JSON structure.
+func authTokenPost(t *testing.T, options gophercloud.AuthOptions, scope *Scope, requestJSON string) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ client := gophercloud.ServiceClient{
+ Provider: &gophercloud.ProviderClient{
+ TokenID: "12345abcdef",
+ },
+ Endpoint: testhelper.Endpoint(),
+ }
+
+ testhelper.Mux.HandleFunc("/auth/tokens", func(w http.ResponseWriter, r *http.Request) {
+ testhelper.TestMethod(t, r, "POST")
+ testhelper.TestHeader(t, r, "Content-Type", "application/json")
+ testhelper.TestHeader(t, r, "Accept", "application/json")
+ testhelper.TestJSONRequest(t, r, requestJSON)
+
+ w.WriteHeader(http.StatusCreated)
+ fmt.Fprintf(w, `{
+ "token": {
+ "expires_at": "2014-10-02T13:45:00.000000Z"
+ }
+ }`)
+ })
+
+ _, err := Create(&client, options, scope).Extract()
+ if err != nil {
+ t.Errorf("Create returned an error: %v", err)
+ }
+}
+
+func authTokenPostErr(t *testing.T, options gophercloud.AuthOptions, scope *Scope, includeToken bool, expectedErr error) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ client := gophercloud.ServiceClient{
+ Provider: &gophercloud.ProviderClient{},
+ Endpoint: testhelper.Endpoint(),
+ }
+ if includeToken {
+ client.Provider.TokenID = "abcdef123456"
+ }
+
+ _, err := Create(&client, options, scope).Extract()
+ if err == nil {
+ t.Errorf("Create did NOT return an error")
+ }
+ if err != expectedErr {
+ t.Errorf("Create returned an unexpected error: wanted %v, got %v", expectedErr, err)
+ }
+}
+
+func TestCreateUserIDAndPassword(t *testing.T) {
+ authTokenPost(t, gophercloud.AuthOptions{UserID: "me", Password: "squirrel!"}, nil, `
+ {
+ "auth": {
+ "identity": {
+ "methods": ["password"],
+ "password": {
+ "user": { "id": "me", "password": "squirrel!" }
+ }
+ }
+ }
+ }
+ `)
+}
+
+func TestCreateUsernameDomainIDPassword(t *testing.T) {
+ authTokenPost(t, gophercloud.AuthOptions{Username: "fakey", Password: "notpassword", DomainID: "abc123"}, nil, `
+ {
+ "auth": {
+ "identity": {
+ "methods": ["password"],
+ "password": {
+ "user": {
+ "domain": {
+ "id": "abc123"
+ },
+ "name": "fakey",
+ "password": "notpassword"
+ }
+ }
+ }
+ }
+ }
+ `)
+}
+
+func TestCreateUsernameDomainNamePassword(t *testing.T) {
+ authTokenPost(t, gophercloud.AuthOptions{Username: "frank", Password: "swordfish", DomainName: "spork.net"}, nil, `
+ {
+ "auth": {
+ "identity": {
+ "methods": ["password"],
+ "password": {
+ "user": {
+ "domain": {
+ "name": "spork.net"
+ },
+ "name": "frank",
+ "password": "swordfish"
+ }
+ }
+ }
+ }
+ }
+ `)
+}
+
+func TestCreateTokenID(t *testing.T) {
+ authTokenPost(t, gophercloud.AuthOptions{}, nil, `
+ {
+ "auth": {
+ "identity": {
+ "methods": ["token"],
+ "token": {
+ "id": "12345abcdef"
+ }
+ }
+ }
+ }
+ `)
+}
+
+func TestCreateProjectIDScope(t *testing.T) {
+ options := gophercloud.AuthOptions{UserID: "fenris", Password: "g0t0h311"}
+ scope := &Scope{ProjectID: "123456"}
+ authTokenPost(t, options, scope, `
+ {
+ "auth": {
+ "identity": {
+ "methods": ["password"],
+ "password": {
+ "user": {
+ "id": "fenris",
+ "password": "g0t0h311"
+ }
+ }
+ },
+ "scope": {
+ "project": {
+ "id": "123456"
+ }
+ }
+ }
+ }
+ `)
+}
+
+func TestCreateDomainIDScope(t *testing.T) {
+ options := gophercloud.AuthOptions{UserID: "fenris", Password: "g0t0h311"}
+ scope := &Scope{DomainID: "1000"}
+ authTokenPost(t, options, scope, `
+ {
+ "auth": {
+ "identity": {
+ "methods": ["password"],
+ "password": {
+ "user": {
+ "id": "fenris",
+ "password": "g0t0h311"
+ }
+ }
+ },
+ "scope": {
+ "domain": {
+ "id": "1000"
+ }
+ }
+ }
+ }
+ `)
+}
+
+func TestCreateProjectNameAndDomainIDScope(t *testing.T) {
+ options := gophercloud.AuthOptions{UserID: "fenris", Password: "g0t0h311"}
+ scope := &Scope{ProjectName: "world-domination", DomainID: "1000"}
+ authTokenPost(t, options, scope, `
+ {
+ "auth": {
+ "identity": {
+ "methods": ["password"],
+ "password": {
+ "user": {
+ "id": "fenris",
+ "password": "g0t0h311"
+ }
+ }
+ },
+ "scope": {
+ "project": {
+ "domain": {
+ "id": "1000"
+ },
+ "name": "world-domination"
+ }
+ }
+ }
+ }
+ `)
+}
+
+func TestCreateProjectNameAndDomainNameScope(t *testing.T) {
+ options := gophercloud.AuthOptions{UserID: "fenris", Password: "g0t0h311"}
+ scope := &Scope{ProjectName: "world-domination", DomainName: "evil-plans"}
+ authTokenPost(t, options, scope, `
+ {
+ "auth": {
+ "identity": {
+ "methods": ["password"],
+ "password": {
+ "user": {
+ "id": "fenris",
+ "password": "g0t0h311"
+ }
+ }
+ },
+ "scope": {
+ "project": {
+ "domain": {
+ "name": "evil-plans"
+ },
+ "name": "world-domination"
+ }
+ }
+ }
+ }
+ `)
+}
+
+func TestCreateExtractsTokenFromResponse(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ client := gophercloud.ServiceClient{
+ Provider: &gophercloud.ProviderClient{},
+ Endpoint: testhelper.Endpoint(),
+ }
+
+ testhelper.Mux.HandleFunc("/auth/tokens", func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Add("X-Subject-Token", "aaa111")
+
+ w.WriteHeader(http.StatusCreated)
+ fmt.Fprintf(w, `{
+ "token": {
+ "expires_at": "2014-10-02T13:45:00.000000Z"
+ }
+ }`)
+ })
+
+ options := gophercloud.AuthOptions{UserID: "me", Password: "shhh"}
+ token, err := Create(&client, options, nil).Extract()
+ if err != nil {
+ t.Fatalf("Create returned an error: %v", err)
+ }
+
+ if token.ID != "aaa111" {
+ t.Errorf("Expected token to be aaa111, but was %s", token.ID)
+ }
+}
+
+func TestCreateFailureEmptyAuth(t *testing.T) {
+ authTokenPostErr(t, gophercloud.AuthOptions{}, nil, false, ErrMissingPassword)
+}
+
+func TestCreateFailureAPIKey(t *testing.T) {
+ authTokenPostErr(t, gophercloud.AuthOptions{APIKey: "something"}, nil, false, ErrAPIKeyProvided)
+}
+
+func TestCreateFailureTenantID(t *testing.T) {
+ authTokenPostErr(t, gophercloud.AuthOptions{TenantID: "something"}, nil, false, ErrTenantIDProvided)
+}
+
+func TestCreateFailureTenantName(t *testing.T) {
+ authTokenPostErr(t, gophercloud.AuthOptions{TenantName: "something"}, nil, false, ErrTenantNameProvided)
+}
+
+func TestCreateFailureTokenIDUsername(t *testing.T) {
+ authTokenPostErr(t, gophercloud.AuthOptions{Username: "something"}, nil, true, ErrUsernameWithToken)
+}
+
+func TestCreateFailureTokenIDUserID(t *testing.T) {
+ authTokenPostErr(t, gophercloud.AuthOptions{UserID: "something"}, nil, true, ErrUserIDWithToken)
+}
+
+func TestCreateFailureTokenIDDomainID(t *testing.T) {
+ authTokenPostErr(t, gophercloud.AuthOptions{DomainID: "something"}, nil, true, ErrDomainIDWithToken)
+}
+
+func TestCreateFailureTokenIDDomainName(t *testing.T) {
+ authTokenPostErr(t, gophercloud.AuthOptions{DomainName: "something"}, nil, true, ErrDomainNameWithToken)
+}
+
+func TestCreateFailureMissingUser(t *testing.T) {
+ options := gophercloud.AuthOptions{Password: "supersecure"}
+ authTokenPostErr(t, options, nil, false, ErrUsernameOrUserID)
+}
+
+func TestCreateFailureBothUser(t *testing.T) {
+ options := gophercloud.AuthOptions{
+ Password: "supersecure",
+ Username: "oops",
+ UserID: "redundancy",
+ }
+ authTokenPostErr(t, options, nil, false, ErrUsernameOrUserID)
+}
+
+func TestCreateFailureMissingDomain(t *testing.T) {
+ options := gophercloud.AuthOptions{
+ Password: "supersecure",
+ Username: "notuniqueenough",
+ }
+ authTokenPostErr(t, options, nil, false, ErrDomainIDOrDomainName)
+}
+
+func TestCreateFailureBothDomain(t *testing.T) {
+ options := gophercloud.AuthOptions{
+ Password: "supersecure",
+ Username: "someone",
+ DomainID: "hurf",
+ DomainName: "durf",
+ }
+ authTokenPostErr(t, options, nil, false, ErrDomainIDOrDomainName)
+}
+
+func TestCreateFailureUserIDDomainID(t *testing.T) {
+ options := gophercloud.AuthOptions{
+ UserID: "100",
+ Password: "stuff",
+ DomainID: "oops",
+ }
+ authTokenPostErr(t, options, nil, false, ErrDomainIDWithUserID)
+}
+
+func TestCreateFailureUserIDDomainName(t *testing.T) {
+ options := gophercloud.AuthOptions{
+ UserID: "100",
+ Password: "sssh",
+ DomainName: "oops",
+ }
+ authTokenPostErr(t, options, nil, false, ErrDomainNameWithUserID)
+}
+
+func TestCreateFailureScopeProjectNameAlone(t *testing.T) {
+ options := gophercloud.AuthOptions{UserID: "myself", Password: "swordfish"}
+ scope := &Scope{ProjectName: "notenough"}
+ authTokenPostErr(t, options, scope, false, ErrScopeDomainIDOrDomainName)
+}
+
+func TestCreateFailureScopeProjectNameAndID(t *testing.T) {
+ options := gophercloud.AuthOptions{UserID: "myself", Password: "swordfish"}
+ scope := &Scope{ProjectName: "whoops", ProjectID: "toomuch", DomainID: "1234"}
+ authTokenPostErr(t, options, scope, false, ErrScopeProjectIDOrProjectName)
+}
+
+func TestCreateFailureScopeProjectIDAndDomainID(t *testing.T) {
+ options := gophercloud.AuthOptions{UserID: "myself", Password: "swordfish"}
+ scope := &Scope{ProjectID: "toomuch", DomainID: "notneeded"}
+ authTokenPostErr(t, options, scope, false, ErrScopeProjectIDAlone)
+}
+
+func TestCreateFailureScopeProjectIDAndDomainNAme(t *testing.T) {
+ options := gophercloud.AuthOptions{UserID: "myself", Password: "swordfish"}
+ scope := &Scope{ProjectID: "toomuch", DomainName: "notneeded"}
+ authTokenPostErr(t, options, scope, false, ErrScopeProjectIDAlone)
+}
+
+func TestCreateFailureScopeDomainIDAndDomainName(t *testing.T) {
+ options := gophercloud.AuthOptions{UserID: "myself", Password: "swordfish"}
+ scope := &Scope{DomainID: "toomuch", DomainName: "notneeded"}
+ authTokenPostErr(t, options, scope, false, ErrScopeDomainIDOrDomainName)
+}
+
+func TestCreateFailureScopeDomainNameAlone(t *testing.T) {
+ options := gophercloud.AuthOptions{UserID: "myself", Password: "swordfish"}
+ scope := &Scope{DomainName: "notenough"}
+ authTokenPostErr(t, options, scope, false, ErrScopeDomainName)
+}
+
+func TestCreateFailureEmptyScope(t *testing.T) {
+ options := gophercloud.AuthOptions{UserID: "myself", Password: "swordfish"}
+ scope := &Scope{}
+ authTokenPostErr(t, options, scope, false, ErrScopeEmpty)
+}
+
+func TestGetRequest(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ client := gophercloud.ServiceClient{
+ Provider: &gophercloud.ProviderClient{
+ TokenID: "12345abcdef",
+ },
+ Endpoint: testhelper.Endpoint(),
+ }
+
+ testhelper.Mux.HandleFunc("/auth/tokens", func(w http.ResponseWriter, r *http.Request) {
+ testhelper.TestMethod(t, r, "GET")
+ testhelper.TestHeader(t, r, "Content-Type", "")
+ testhelper.TestHeader(t, r, "Accept", "application/json")
+ testhelper.TestHeader(t, r, "X-Auth-Token", "12345abcdef")
+ testhelper.TestHeader(t, r, "X-Subject-Token", "abcdef12345")
+
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprintf(w, `
+ { "token": { "expires_at": "2014-08-29T13:10:01.000000Z" } }
+ `)
+ })
+
+ token, err := Get(&client, "abcdef12345").Extract()
+ if err != nil {
+ t.Errorf("Info returned an error: %v", err)
+ }
+
+ expected, _ := time.Parse(time.UnixDate, "Fri Aug 29 13:10:01 UTC 2014")
+ if token.ExpiresAt != expected {
+ t.Errorf("Expected expiration time %s, but was %s", expected.Format(time.UnixDate), token.ExpiresAt.Format(time.UnixDate))
+ }
+}
+
+func prepareAuthTokenHandler(t *testing.T, expectedMethod string, status int) gophercloud.ServiceClient {
+ client := gophercloud.ServiceClient{
+ Provider: &gophercloud.ProviderClient{
+ TokenID: "12345abcdef",
+ },
+ Endpoint: testhelper.Endpoint(),
+ }
+
+ testhelper.Mux.HandleFunc("/auth/tokens", func(w http.ResponseWriter, r *http.Request) {
+ testhelper.TestMethod(t, r, expectedMethod)
+ testhelper.TestHeader(t, r, "Content-Type", "")
+ testhelper.TestHeader(t, r, "Accept", "application/json")
+ testhelper.TestHeader(t, r, "X-Auth-Token", "12345abcdef")
+ testhelper.TestHeader(t, r, "X-Subject-Token", "abcdef12345")
+
+ w.WriteHeader(status)
+ })
+
+ return client
+}
+
+func TestValidateRequestSuccessful(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+ client := prepareAuthTokenHandler(t, "HEAD", http.StatusNoContent)
+
+ ok, err := Validate(&client, "abcdef12345")
+ if err != nil {
+ t.Errorf("Unexpected error from Validate: %v", err)
+ }
+
+ if !ok {
+ t.Errorf("Validate returned false for a valid token")
+ }
+}
+
+func TestValidateRequestFailure(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+ client := prepareAuthTokenHandler(t, "HEAD", http.StatusNotFound)
+
+ ok, err := Validate(&client, "abcdef12345")
+ if err != nil {
+ t.Errorf("Unexpected error from Validate: %v", err)
+ }
+
+ if ok {
+ t.Errorf("Validate returned true for an invalid token")
+ }
+}
+
+func TestValidateRequestError(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+ client := prepareAuthTokenHandler(t, "HEAD", http.StatusUnauthorized)
+
+ _, err := Validate(&client, "abcdef12345")
+ if err == nil {
+ t.Errorf("Missing expected error from Validate")
+ }
+}
+
+func TestRevokeRequestSuccessful(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+ client := prepareAuthTokenHandler(t, "DELETE", http.StatusNoContent)
+
+ err := Revoke(&client, "abcdef12345")
+ if err != nil {
+ t.Errorf("Unexpected error from Revoke: %v", err)
+ }
+}
+
+func TestRevokeRequestError(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+ client := prepareAuthTokenHandler(t, "DELETE", http.StatusNotFound)
+
+ err := Revoke(&client, "abcdef12345")
+ if err == nil {
+ t.Errorf("Missing expected error from Revoke")
+ }
+}
diff --git a/_site/openstack/identity/v3/tokens/results.go b/_site/openstack/identity/v3/tokens/results.go
new file mode 100644
index 0000000..1be98cb
--- /dev/null
+++ b/_site/openstack/identity/v3/tokens/results.go
@@ -0,0 +1,75 @@
+package tokens
+
+import (
+ "net/http"
+ "time"
+
+ "github.com/mitchellh/mapstructure"
+ "github.com/rackspace/gophercloud"
+)
+
+// commonResult is the deferred result of a Create or a Get call.
+type commonResult struct {
+ gophercloud.CommonResult
+
+ // header stores the headers from the original HTTP response because token responses are returned in an X-Subject-Token header.
+ header http.Header
+}
+
+// Extract interprets a commonResult as a Token.
+func (r commonResult) Extract() (*Token, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var response struct {
+ Token struct {
+ ExpiresAt string `mapstructure:"expires_at"`
+ } `mapstructure:"token"`
+ }
+
+ var token Token
+
+ // Parse the token itself from the stored headers.
+ token.ID = r.header.Get("X-Subject-Token")
+
+ err := mapstructure.Decode(r.Resp, &response)
+ if err != nil {
+ return nil, err
+ }
+
+ // Attempt to parse the timestamp.
+ token.ExpiresAt, err = time.Parse(gophercloud.RFC3339Milli, response.Token.ExpiresAt)
+
+ return &token, err
+}
+
+// CreateResult is the deferred response from a Create call.
+type CreateResult struct {
+ commonResult
+}
+
+// createErr quickly creates a CreateResult that reports an error.
+func createErr(err error) CreateResult {
+ return CreateResult{
+ commonResult: commonResult{
+ CommonResult: gophercloud.CommonResult{Err: err},
+ header: nil,
+ },
+ }
+}
+
+// GetResult is the deferred response from a Get call.
+type GetResult struct {
+ commonResult
+}
+
+// Token is a string that grants a user access to a controlled set of services in an OpenStack provider.
+// Each Token is valid for a set length of time.
+type Token struct {
+ // ID is the issued token.
+ ID string
+
+ // ExpiresAt is the timestamp at which this token will no longer be accepted.
+ ExpiresAt time.Time
+}
diff --git a/_site/openstack/identity/v3/tokens/urls.go b/_site/openstack/identity/v3/tokens/urls.go
new file mode 100644
index 0000000..360b60a
--- /dev/null
+++ b/_site/openstack/identity/v3/tokens/urls.go
@@ -0,0 +1,7 @@
+package tokens
+
+import "github.com/rackspace/gophercloud"
+
+func tokenURL(c *gophercloud.ServiceClient) string {
+ return c.ServiceURL("auth", "tokens")
+}
diff --git a/_site/openstack/identity/v3/tokens/urls_test.go b/_site/openstack/identity/v3/tokens/urls_test.go
new file mode 100644
index 0000000..549c398
--- /dev/null
+++ b/_site/openstack/identity/v3/tokens/urls_test.go
@@ -0,0 +1,21 @@
+package tokens
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestTokenURL(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ client := gophercloud.ServiceClient{Endpoint: testhelper.Endpoint()}
+
+ expected := testhelper.Endpoint() + "auth/tokens"
+ actual := tokenURL(&client)
+ if actual != expected {
+ t.Errorf("Expected URL %s, but was %s", expected, actual)
+ }
+}
diff --git a/_site/openstack/networking/v2/apiversions/doc.go b/_site/openstack/networking/v2/apiversions/doc.go
new file mode 100644
index 0000000..0208ee2
--- /dev/null
+++ b/_site/openstack/networking/v2/apiversions/doc.go
@@ -0,0 +1,4 @@
+// Package apiversions provides information and interaction with the different
+// API versions for the OpenStack Neutron service. This functionality is not
+// restricted to this particular version.
+package apiversions
diff --git a/_site/openstack/networking/v2/apiversions/errors.go b/_site/openstack/networking/v2/apiversions/errors.go
new file mode 100644
index 0000000..76bdb14
--- /dev/null
+++ b/_site/openstack/networking/v2/apiversions/errors.go
@@ -0,0 +1 @@
+package apiversions
diff --git a/_site/openstack/networking/v2/apiversions/requests.go b/_site/openstack/networking/v2/apiversions/requests.go
new file mode 100644
index 0000000..b653f51
--- /dev/null
+++ b/_site/openstack/networking/v2/apiversions/requests.go
@@ -0,0 +1,21 @@
+package apiversions
+
+import (
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// ListVersions lists all the Neutron API versions available to end-users
+func ListVersions(c *gophercloud.ServiceClient) pagination.Pager {
+ return pagination.NewPager(c, apiVersionsURL(c), func(r pagination.LastHTTPResponse) pagination.Page {
+ return APIVersionPage{pagination.SinglePageBase(r)}
+ })
+}
+
+// ListVersionResources lists all of the different API resources for a particular
+// API versions. Typical resources for Neutron might be: networks, subnets, etc.
+func ListVersionResources(c *gophercloud.ServiceClient, v string) pagination.Pager {
+ return pagination.NewPager(c, apiInfoURL(c, v), func(r pagination.LastHTTPResponse) pagination.Page {
+ return APIVersionResourcePage{pagination.SinglePageBase(r)}
+ })
+}
diff --git a/_site/openstack/networking/v2/apiversions/requests_test.go b/_site/openstack/networking/v2/apiversions/requests_test.go
new file mode 100644
index 0000000..d35af9f
--- /dev/null
+++ b/_site/openstack/networking/v2/apiversions/requests_test.go
@@ -0,0 +1,182 @@
+package apiversions
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestListVersions(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/", 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, `
+{
+ "versions": [
+ {
+ "status": "CURRENT",
+ "id": "v2.0",
+ "links": [
+ {
+ "href": "http://23.253.228.211:9696/v2.0",
+ "rel": "self"
+ }
+ ]
+ }
+ ]
+}`)
+ })
+
+ count := 0
+
+ ListVersions(fake.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractAPIVersions(page)
+ if err != nil {
+ t.Errorf("Failed to extract API versions: %v", err)
+ return false, err
+ }
+
+ expected := []APIVersion{
+ APIVersion{
+ Status: "CURRENT",
+ ID: "v2.0",
+ },
+ }
+
+ th.AssertDeepEquals(t, expected, actual)
+
+ return true, nil
+ })
+
+ if count != 1 {
+ t.Errorf("Expected 1 page, got %d", count)
+ }
+}
+
+func TestNonJSONCannotBeExtractedIntoAPIVersions(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ })
+
+ ListVersions(fake.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+ if _, err := ExtractAPIVersions(page); err == nil {
+ t.Fatalf("Expected error, got nil")
+ }
+ return true, nil
+ })
+}
+
+func TestAPIInfo(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/", 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, `
+{
+ "resources": [
+ {
+ "links": [
+ {
+ "href": "http://23.253.228.211:9696/v2.0/subnets",
+ "rel": "self"
+ }
+ ],
+ "name": "subnet",
+ "collection": "subnets"
+ },
+ {
+ "links": [
+ {
+ "href": "http://23.253.228.211:9696/v2.0/networks",
+ "rel": "self"
+ }
+ ],
+ "name": "network",
+ "collection": "networks"
+ },
+ {
+ "links": [
+ {
+ "href": "http://23.253.228.211:9696/v2.0/ports",
+ "rel": "self"
+ }
+ ],
+ "name": "port",
+ "collection": "ports"
+ }
+ ]
+}
+ `)
+ })
+
+ count := 0
+
+ ListVersionResources(fake.ServiceClient(), "v2.0").EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractVersionResources(page)
+ if err != nil {
+ t.Errorf("Failed to extract version resources: %v", err)
+ return false, err
+ }
+
+ expected := []APIVersionResource{
+ APIVersionResource{
+ Name: "subnet",
+ Collection: "subnets",
+ },
+ APIVersionResource{
+ Name: "network",
+ Collection: "networks",
+ },
+ APIVersionResource{
+ Name: "port",
+ Collection: "ports",
+ },
+ }
+
+ th.AssertDeepEquals(t, expected, actual)
+
+ return true, nil
+ })
+
+ if count != 1 {
+ t.Errorf("Expected 1 page, got %d", count)
+ }
+}
+
+func TestNonJSONCannotBeExtractedIntoAPIVersionResources(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ })
+
+ ListVersionResources(fake.ServiceClient(), "v2.0").EachPage(func(page pagination.Page) (bool, error) {
+ if _, err := ExtractVersionResources(page); err == nil {
+ t.Fatalf("Expected error, got nil")
+ }
+ return true, nil
+ })
+}
diff --git a/_site/openstack/networking/v2/apiversions/results.go b/_site/openstack/networking/v2/apiversions/results.go
new file mode 100644
index 0000000..9715934
--- /dev/null
+++ b/_site/openstack/networking/v2/apiversions/results.go
@@ -0,0 +1,77 @@
+package apiversions
+
+import (
+ "github.com/mitchellh/mapstructure"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// APIVersion represents an API version for Neutron. It contains the status of
+// the API, and its unique ID.
+type APIVersion struct {
+ Status string `mapstructure:"status" json:"status"`
+ ID string `mapstructure:"id" json:"id"`
+}
+
+// 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
+}
+
+// APIVersionResource represents a generic API resource. It contains the name
+// of the resource and its plural collection name.
+type APIVersionResource struct {
+ Name string `mapstructure:"name" json:"name"`
+ Collection string `mapstructure:"collection" json:"collection"`
+}
+
+// APIVersionResourcePage is a concrete type which embeds the common
+// SinglePageBase struct, and is used when traversing API versions collections.
+type APIVersionResourcePage struct {
+ pagination.SinglePageBase
+}
+
+// IsEmpty is a concrete function which indicates whether an
+// APIVersionResourcePage is empty or not.
+func (r APIVersionResourcePage) IsEmpty() (bool, error) {
+ is, err := ExtractVersionResources(r)
+ if err != nil {
+ return true, err
+ }
+ return len(is) == 0, nil
+}
+
+// ExtractVersionResources accepts a Page struct, specifically a
+// APIVersionResourcePage struct, and extracts the elements into a slice of
+// APIVersionResource structs. In other words, the collection is mapped into
+// a relevant slice.
+func ExtractVersionResources(page pagination.Page) ([]APIVersionResource, error) {
+ var resp struct {
+ APIVersionResources []APIVersionResource `mapstructure:"resources"`
+ }
+
+ err := mapstructure.Decode(page.(APIVersionResourcePage).Body, &resp)
+
+ return resp.APIVersionResources, err
+}
diff --git a/_site/openstack/networking/v2/apiversions/urls.go b/_site/openstack/networking/v2/apiversions/urls.go
new file mode 100644
index 0000000..58aa2b6
--- /dev/null
+++ b/_site/openstack/networking/v2/apiversions/urls.go
@@ -0,0 +1,15 @@
+package apiversions
+
+import (
+ "strings"
+
+ "github.com/rackspace/gophercloud"
+)
+
+func apiVersionsURL(c *gophercloud.ServiceClient) string {
+ return c.Endpoint
+}
+
+func apiInfoURL(c *gophercloud.ServiceClient, version string) string {
+ return c.Endpoint + strings.TrimRight(version, "/") + "/"
+}
diff --git a/_site/openstack/networking/v2/apiversions/urls_test.go b/_site/openstack/networking/v2/apiversions/urls_test.go
new file mode 100644
index 0000000..7dd069c
--- /dev/null
+++ b/_site/openstack/networking/v2/apiversions/urls_test.go
@@ -0,0 +1,26 @@
+package apiversions
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+const endpoint = "http://localhost:57909/"
+
+func endpointClient() *gophercloud.ServiceClient {
+ return &gophercloud.ServiceClient{Endpoint: endpoint}
+}
+
+func TestAPIVersionsURL(t *testing.T) {
+ actual := apiVersionsURL(endpointClient())
+ expected := endpoint
+ th.AssertEquals(t, expected, actual)
+}
+
+func TestAPIInfoURL(t *testing.T) {
+ actual := apiInfoURL(endpointClient(), "v2.0")
+ expected := endpoint + "v2.0/"
+ th.AssertEquals(t, expected, actual)
+}
diff --git a/_site/openstack/networking/v2/common/common_tests.go b/_site/openstack/networking/v2/common/common_tests.go
new file mode 100644
index 0000000..4160351
--- /dev/null
+++ b/_site/openstack/networking/v2/common/common_tests.go
@@ -0,0 +1,14 @@
+package common
+
+import (
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+const TokenID = client.TokenID
+
+func ServiceClient() *gophercloud.ServiceClient {
+ sc := client.ServiceClient()
+ sc.ResourceBase = sc.Endpoint + "v2.0/"
+ return sc
+}
diff --git a/_site/openstack/networking/v2/extensions/delegate.go b/_site/openstack/networking/v2/extensions/delegate.go
new file mode 100644
index 0000000..d08e1fd
--- /dev/null
+++ b/_site/openstack/networking/v2/extensions/delegate.go
@@ -0,0 +1,41 @@
+package extensions
+
+import (
+ "github.com/rackspace/gophercloud"
+ common "github.com/rackspace/gophercloud/openstack/common/extensions"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// Extension is a single OpenStack extension.
+type Extension struct {
+ common.Extension
+}
+
+// GetResult wraps a GetResult from common.
+type GetResult struct {
+ common.GetResult
+}
+
+// ExtractExtensions interprets a Page as a slice of Extensions.
+func ExtractExtensions(page pagination.Page) ([]Extension, error) {
+ inner, err := common.ExtractExtensions(page)
+ if err != nil {
+ return nil, err
+ }
+ outer := make([]Extension, len(inner))
+ for index, ext := range inner {
+ outer[index] = Extension{ext}
+ }
+ return outer, nil
+}
+
+// Get retrieves information for a specific extension using its alias.
+func Get(c *gophercloud.ServiceClient, alias string) GetResult {
+ return GetResult{common.Get(c, alias)}
+}
+
+// List returns a Pager which allows you to iterate over the full collection of extensions.
+// It does not accept query parameters.
+func List(c *gophercloud.ServiceClient) pagination.Pager {
+ return common.List(c)
+}
diff --git a/_site/openstack/networking/v2/extensions/delegate_test.go b/_site/openstack/networking/v2/extensions/delegate_test.go
new file mode 100755
index 0000000..3d2ac78
--- /dev/null
+++ b/_site/openstack/networking/v2/extensions/delegate_test.go
@@ -0,0 +1,105 @@
+package extensions
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ common "github.com/rackspace/gophercloud/openstack/common/extensions"
+ fake "github.com/rackspace/gophercloud/openstack/networking/v2/common"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestList(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/extensions", 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")
+
+ fmt.Fprintf(w, `
+{
+ "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"
+ }
+ ]
+}
+ `)
+ })
+
+ count := 0
+
+ List(fake.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractExtensions(page)
+ if err != nil {
+ t.Errorf("Failed to extract extensions: %v", err)
+ }
+
+ expected := []Extension{
+ Extension{
+ common.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",
+ },
+ },
+ }
+
+ th.AssertDeepEquals(t, expected, actual)
+
+ return true, nil
+ })
+
+ if count != 1 {
+ t.Errorf("Expected 1 page, got %d", count)
+ }
+}
+
+func TestGet(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/extensions/agent", 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, `
+{
+ "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."
+ }
+}
+ `)
+ })
+
+ ext, err := Get(fake.ServiceClient(), "agent").Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, ext.Updated, "2013-02-03T10:00:00-00:00")
+ th.AssertEquals(t, ext.Name, "agent")
+ th.AssertEquals(t, ext.Namespace, "http://docs.openstack.org/ext/agent/api/v2.0")
+ th.AssertEquals(t, ext.Alias, "agent")
+ th.AssertEquals(t, ext.Description, "The agent management extension.")
+}
diff --git a/_site/openstack/networking/v2/extensions/external/doc.go b/_site/openstack/networking/v2/extensions/external/doc.go
new file mode 100755
index 0000000..d244f26
--- /dev/null
+++ b/_site/openstack/networking/v2/extensions/external/doc.go
@@ -0,0 +1 @@
+package external
diff --git a/_site/openstack/networking/v2/extensions/external/requests.go b/_site/openstack/networking/v2/extensions/external/requests.go
new file mode 100644
index 0000000..2f04593
--- /dev/null
+++ b/_site/openstack/networking/v2/extensions/external/requests.go
@@ -0,0 +1,56 @@
+package external
+
+import "github.com/rackspace/gophercloud/openstack/networking/v2/networks"
+
+// AdminState gives users a solid type to work with for create and update
+// operations. It is recommended that users use the `Up` and `Down` enums.
+type AdminState *bool
+
+// Convenience vars for AdminStateUp values.
+var (
+ iTrue = true
+ iFalse = false
+
+ Up AdminState = &iTrue
+ Down AdminState = &iFalse
+)
+
+// CreateOpts is the structure used when creating new external network
+// resources. It embeds networks.CreateOpts and so inherits all of its required
+// and optional fields, with the addition of the External field.
+type CreateOpts struct {
+ Parent networks.CreateOpts
+ External bool
+}
+
+// ToNetworkCreateMap casts a CreateOpts struct to a map.
+func (o CreateOpts) ToNetworkCreateMap() (map[string]interface{}, error) {
+ outer, err := o.Parent.ToNetworkCreateMap()
+ if err != nil {
+ return nil, err
+ }
+
+ outer["network"].(map[string]interface{})["router:external"] = o.External
+
+ return outer, nil
+}
+
+// UpdateOpts is the structure used when updating existing external network
+// resources. It embeds networks.UpdateOpts and so inherits all of its required
+// and optional fields, with the addition of the External field.
+type UpdateOpts struct {
+ Parent networks.UpdateOpts
+ External bool
+}
+
+// ToNetworkUpdateMap casts an UpdateOpts struct to a map.
+func (o UpdateOpts) ToNetworkUpdateMap() (map[string]interface{}, error) {
+ outer, err := o.Parent.ToNetworkUpdateMap()
+ if err != nil {
+ return nil, err
+ }
+
+ outer["network"].(map[string]interface{})["router:external"] = o.External
+
+ return outer, nil
+}
diff --git a/_site/openstack/networking/v2/extensions/external/results.go b/_site/openstack/networking/v2/extensions/external/results.go
new file mode 100644
index 0000000..47f7258
--- /dev/null
+++ b/_site/openstack/networking/v2/extensions/external/results.go
@@ -0,0 +1,81 @@
+package external
+
+import (
+ "github.com/mitchellh/mapstructure"
+ "github.com/rackspace/gophercloud/openstack/networking/v2/networks"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// NetworkExternal represents a decorated form of a Network with based on the
+// "external-net" extension.
+type NetworkExternal struct {
+ // UUID for the network
+ ID string `mapstructure:"id" json:"id"`
+
+ // Human-readable name for the network. Might not be unique.
+ Name string `mapstructure:"name" json:"name"`
+
+ // The administrative state of network. If false (down), the network does not forward packets.
+ AdminStateUp bool `mapstructure:"admin_state_up" json:"admin_state_up"`
+
+ // Indicates whether network is currently operational. Possible values include
+ // `ACTIVE', `DOWN', `BUILD', or `ERROR'. Plug-ins might define additional values.
+ Status string `mapstructure:"status" json:"status"`
+
+ // Subnets associated with this network.
+ Subnets []string `mapstructure:"subnets" json:"subnets"`
+
+ // Owner of network. Only admin users can specify a tenant_id other than its own.
+ TenantID string `mapstructure:"tenant_id" json:"tenant_id"`
+
+ // Specifies whether the network resource can be accessed by any tenant or not.
+ Shared bool `mapstructure:"shared" json:"shared"`
+
+ // Specifies whether the network is an external network or not.
+ External bool `mapstructure:"router:external" json:"router:external"`
+}
+
+func commonExtract(e error, response map[string]interface{}) (*NetworkExternal, error) {
+ if e != nil {
+ return nil, e
+ }
+
+ var res struct {
+ Network *NetworkExternal `json:"network"`
+ }
+
+ err := mapstructure.Decode(response, &res)
+
+ return res.Network, err
+}
+
+// ExtractGet decorates a GetResult struct returned from a networks.Get()
+// function with extended attributes.
+func ExtractGet(r networks.GetResult) (*NetworkExternal, error) {
+ return commonExtract(r.Err, r.Resp)
+}
+
+// ExtractCreate decorates a CreateResult struct returned from a networks.Create()
+// function with extended attributes.
+func ExtractCreate(r networks.CreateResult) (*NetworkExternal, error) {
+ return commonExtract(r.Err, r.Resp)
+}
+
+// ExtractUpdate decorates a UpdateResult struct returned from a
+// networks.Update() function with extended attributes.
+func ExtractUpdate(r networks.UpdateResult) (*NetworkExternal, error) {
+ return commonExtract(r.Err, r.Resp)
+}
+
+// ExtractList accepts a Page struct, specifically a NetworkPage struct, and
+// extracts the elements into a slice of NetworkExtAttrs structs. In other
+// words, a generic collection is mapped into a relevant slice.
+func ExtractList(page pagination.Page) ([]NetworkExternal, error) {
+ var resp struct {
+ Networks []NetworkExternal `mapstructure:"networks" json:"networks"`
+ }
+
+ err := mapstructure.Decode(page.(networks.NetworkPage).Body, &resp)
+
+ return resp.Networks, err
+}
diff --git a/_site/openstack/networking/v2/extensions/external/results_test.go b/_site/openstack/networking/v2/extensions/external/results_test.go
new file mode 100644
index 0000000..916cd2c
--- /dev/null
+++ b/_site/openstack/networking/v2/extensions/external/results_test.go
@@ -0,0 +1,254 @@
+package external
+
+import (
+ "errors"
+ "fmt"
+ "net/http"
+ "testing"
+
+ "github.com/rackspace/gophercloud/openstack/networking/v2/networks"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestList(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/networks", 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, `
+{
+ "networks": [
+ {
+ "admin_state_up": true,
+ "id": "0f38d5ad-10a6-428f-a5fc-825cfe0f1970",
+ "name": "net1",
+ "router:external": false,
+ "shared": false,
+ "status": "ACTIVE",
+ "subnets": [
+ "25778974-48a8-46e7-8998-9dc8c70d2f06"
+ ],
+ "tenant_id": "b575417a6c444a6eb5cc3a58eb4f714a"
+ },
+ {
+ "admin_state_up": true,
+ "id": "8d05a1b1-297a-46ca-8974-17debf51ca3c",
+ "name": "ext_net",
+ "router:external": true,
+ "shared": false,
+ "status": "ACTIVE",
+ "subnets": [
+ "2f1fb918-9b0e-4bf9-9a50-6cebbb4db2c5"
+ ],
+ "tenant_id": "5eb8995cf717462c9df8d1edfa498010"
+ }
+ ]
+}
+ `)
+ })
+
+ count := 0
+
+ networks.List(fake.ServiceClient(), networks.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractList(page)
+ if err != nil {
+ t.Errorf("Failed to extract networks: %v", err)
+ return false, err
+ }
+
+ expected := []NetworkExternal{
+ NetworkExternal{
+ Status: "ACTIVE",
+ Subnets: []string{"25778974-48a8-46e7-8998-9dc8c70d2f06"},
+ Name: "net1",
+ AdminStateUp: true,
+ TenantID: "b575417a6c444a6eb5cc3a58eb4f714a",
+ Shared: false,
+ ID: "0f38d5ad-10a6-428f-a5fc-825cfe0f1970",
+ External: false,
+ },
+ NetworkExternal{
+ Status: "ACTIVE",
+ Subnets: []string{"2f1fb918-9b0e-4bf9-9a50-6cebbb4db2c5"},
+ Name: "ext_net",
+ AdminStateUp: true,
+ TenantID: "5eb8995cf717462c9df8d1edfa498010",
+ Shared: false,
+ ID: "8d05a1b1-297a-46ca-8974-17debf51ca3c",
+ External: true,
+ },
+ }
+
+ th.CheckDeepEquals(t, expected, actual)
+
+ return true, nil
+ })
+
+ if count != 1 {
+ t.Errorf("Expected 1 page, got %d", count)
+ }
+}
+
+func TestGet(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/networks/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, `
+{
+ "network": {
+ "admin_state_up": true,
+ "id": "8d05a1b1-297a-46ca-8974-17debf51ca3c",
+ "name": "ext_net",
+ "router:external": true,
+ "shared": false,
+ "status": "ACTIVE",
+ "subnets": [
+ "2f1fb918-9b0e-4bf9-9a50-6cebbb4db2c5"
+ ],
+ "tenant_id": "5eb8995cf717462c9df8d1edfa498010"
+ }
+}
+ `)
+ })
+
+ res := networks.Get(fake.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22")
+ n, err := ExtractGet(res)
+
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, true, n.External)
+}
+
+func TestCreate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/networks", 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, `
+{
+ "network": {
+ "admin_state_up": true,
+ "name": "ext_net",
+ "router:external": true
+ }
+}
+ `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusCreated)
+
+ fmt.Fprintf(w, `
+{
+ "network": {
+ "admin_state_up": true,
+ "id": "8d05a1b1-297a-46ca-8974-17debf51ca3c",
+ "name": "ext_net",
+ "router:external": true,
+ "shared": false,
+ "status": "ACTIVE",
+ "subnets": [
+ "2f1fb918-9b0e-4bf9-9a50-6cebbb4db2c5"
+ ],
+ "tenant_id": "5eb8995cf717462c9df8d1edfa498010"
+ }
+}
+ `)
+ })
+
+ options := CreateOpts{networks.CreateOpts{Name: "ext_net", AdminStateUp: Up}, true}
+ res := networks.Create(fake.ServiceClient(), options)
+
+ n, err := ExtractCreate(res)
+
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, true, n.External)
+}
+
+func TestUpdate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/networks/4e8e5957-649f-477b-9e5b-f1f75b21c03c", 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.TestHeader(t, r, "Accept", "application/json")
+ th.TestJSONRequest(t, r, `
+{
+ "network": {
+ "router:external": true,
+ "name": "new_name"
+ }
+}
+ `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, `
+{
+ "network": {
+ "admin_state_up": true,
+ "id": "8d05a1b1-297a-46ca-8974-17debf51ca3c",
+ "name": "new_name",
+ "router:external": true,
+ "shared": false,
+ "status": "ACTIVE",
+ "subnets": [
+ "2f1fb918-9b0e-4bf9-9a50-6cebbb4db2c5"
+ ],
+ "tenant_id": "5eb8995cf717462c9df8d1edfa498010"
+ }
+}
+ `)
+ })
+
+ options := UpdateOpts{networks.UpdateOpts{Name: "new_name"}, true}
+ res := networks.Update(fake.ServiceClient(), "4e8e5957-649f-477b-9e5b-f1f75b21c03c", options)
+ n, err := ExtractUpdate(res)
+
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, true, n.External)
+}
+
+func TestExtractFnsReturnsErrWhenResultContainsErr(t *testing.T) {
+ gr := networks.GetResult{}
+ gr.Err = errors.New("")
+
+ if _, err := ExtractGet(gr); err == nil {
+ t.Fatalf("Expected error, got one")
+ }
+
+ ur := networks.UpdateResult{}
+ ur.Err = errors.New("")
+
+ if _, err := ExtractUpdate(ur); err == nil {
+ t.Fatalf("Expected error, got one")
+ }
+
+ cr := networks.CreateResult{}
+ cr.Err = errors.New("")
+
+ if _, err := ExtractCreate(cr); err == nil {
+ t.Fatalf("Expected error, got one")
+ }
+}
diff --git a/_site/openstack/networking/v2/extensions/layer3/doc.go b/_site/openstack/networking/v2/extensions/layer3/doc.go
new file mode 100644
index 0000000..d533458
--- /dev/null
+++ b/_site/openstack/networking/v2/extensions/layer3/doc.go
@@ -0,0 +1,5 @@
+// Package layer3 provides access to the Layer-3 networking extension for the
+// OpenStack Neutron service. This extension allows API users to route packets
+// between subnets, forward packets from internal networks to external ones,
+// and access instances from external networks through floating IPs.
+package layer3
diff --git a/_site/openstack/networking/v2/extensions/layer3/floatingips/requests.go b/_site/openstack/networking/v2/extensions/layer3/floatingips/requests.go
new file mode 100644
index 0000000..22a6cae
--- /dev/null
+++ b/_site/openstack/networking/v2/extensions/layer3/floatingips/requests.go
@@ -0,0 +1,190 @@
+package floatingips
+
+import (
+ "fmt"
+
+ "github.com/racker/perigee"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// ListOpts allows the filtering and sorting of paginated collections through
+// the API. Filtering is achieved by passing in struct field values that map to
+// the floating IP attributes you want to see returned. SortKey allows you to
+// sort by a particular network attribute. SortDir sets the direction, and is
+// either `asc' or `desc'. Marker and Limit are used for pagination.
+type ListOpts struct {
+ ID string `q:"id"`
+ FloatingNetworkID string `q:"floating_network_id"`
+ PortID string `q:"port_id"`
+ FixedIP string `q:"fixed_ip_address"`
+ FloatingIP string `q:"floating_ip_address"`
+ TenantID string `q:"tenant_id"`
+ Limit int `q:"limit"`
+ Marker string `q:"marker"`
+ SortKey string `q:"sort_key"`
+ SortDir string `q:"sort_dir"`
+}
+
+// List returns a Pager which allows you to iterate over a collection of
+// floating IP resources. It accepts a ListOpts struct, which allows you to
+// filter and sort the returned collection for greater efficiency.
+func List(c *gophercloud.ServiceClient, opts ListOpts) pagination.Pager {
+ q, err := gophercloud.BuildQueryString(&opts)
+ if err != nil {
+ return pagination.Pager{Err: err}
+ }
+ u := rootURL(c) + q.String()
+ return pagination.NewPager(c, u, func(r pagination.LastHTTPResponse) pagination.Page {
+ return FloatingIPPage{pagination.LinkedPageBase{LastHTTPResponse: r}}
+ })
+}
+
+// CreateOpts contains all the values needed to create a new floating IP
+// resource. The only required fields are FloatingNetworkID and PortID which
+// refer to the external network and internal port respectively.
+type CreateOpts struct {
+ FloatingNetworkID string
+ FloatingIP string
+ PortID string
+ FixedIP string
+ TenantID string
+}
+
+var (
+ errFloatingNetworkIDRequired = fmt.Errorf("A NetworkID is required")
+ errPortIDRequired = fmt.Errorf("A PortID is required")
+)
+
+// Create accepts a CreateOpts struct and uses the values provided to create a
+// new floating IP resource. You can create floating IPs on external networks
+// only. If you provide a FloatingNetworkID which refers to a network that is
+// not external (i.e. its `router:external' attribute is False), the operation
+// will fail and return a 400 error.
+//
+// If you do not specify a FloatingIP address value, the operation will
+// automatically allocate an available address for the new resource. If you do
+// choose to specify one, it must fall within the subnet range for the external
+// network - otherwise the operation returns a 400 error. If the FloatingIP
+// address is already in use, the operation returns a 409 error code.
+//
+// You can associate the new resource with an internal port by using the PortID
+// field. If you specify a PortID that is not valid, the operation will fail and
+// return 404 error code.
+//
+// You must also configure an IP address for the port associated with the PortID
+// you have provided - this is what the FixedIP refers to: an IP fixed to a port.
+// Because a port might be associated with multiple IP addresses, you can use
+// the FixedIP field to associate a particular IP address rather than have the
+// API assume for you. If you specify an IP address that is not valid, the
+// operation will fail and return a 400 error code. If the PortID and FixedIP
+// are already associated with another resource, the operation will fail and
+// returns a 409 error code.
+func Create(c *gophercloud.ServiceClient, opts CreateOpts) CreateResult {
+ var res CreateResult
+
+ // Validate
+ if opts.FloatingNetworkID == "" {
+ res.Err = errFloatingNetworkIDRequired
+ return res
+ }
+ if opts.PortID == "" {
+ res.Err = errPortIDRequired
+ return res
+ }
+
+ // Define structures
+ type floatingIP struct {
+ FloatingNetworkID string `json:"floating_network_id"`
+ FloatingIP string `json:"floating_ip_address,omitempty"`
+ PortID string `json:"port_id"`
+ FixedIP string `json:"fixed_ip_address,omitempty"`
+ TenantID string `json:"tenant_id,omitempty"`
+ }
+ type request struct {
+ FloatingIP floatingIP `json:"floatingip"`
+ }
+
+ // Populate request body
+ reqBody := request{FloatingIP: floatingIP{
+ FloatingNetworkID: opts.FloatingNetworkID,
+ PortID: opts.PortID,
+ FixedIP: opts.FixedIP,
+ TenantID: opts.TenantID,
+ }}
+
+ // Send request to API
+ _, res.Err = perigee.Request("POST", rootURL(c), perigee.Options{
+ MoreHeaders: c.Provider.AuthenticatedHeaders(),
+ ReqBody: &reqBody,
+ Results: &res.Resp,
+ OkCodes: []int{201},
+ })
+
+ return res
+}
+
+// Get retrieves a particular floating IP resource based on its unique ID.
+func Get(c *gophercloud.ServiceClient, id string) GetResult {
+ var res GetResult
+ _, res.Err = perigee.Request("GET", resourceURL(c, id), perigee.Options{
+ MoreHeaders: c.Provider.AuthenticatedHeaders(),
+ Results: &res.Resp,
+ OkCodes: []int{200},
+ })
+ return res
+}
+
+// UpdateOpts contains the values used when updating a floating IP resource. The
+// only value that can be updated is which internal port the floating IP is
+// linked to. To associate the floating IP with a new internal port, provide its
+// ID. To disassociate the floating IP from all ports, provide an empty string.
+type UpdateOpts struct {
+ PortID string
+}
+
+// Update allows floating IP resources to be updated. Currently, the only way to
+// "update" a floating IP is to associate it with a new internal port, or
+// disassociated it from all ports. See UpdateOpts for instructions of how to
+// do this.
+func Update(c *gophercloud.ServiceClient, id string, opts UpdateOpts) UpdateResult {
+ type floatingIP struct {
+ PortID *string `json:"port_id"`
+ }
+
+ type request struct {
+ FloatingIP floatingIP `json:"floatingip"`
+ }
+
+ var portID *string
+ if opts.PortID == "" {
+ portID = nil
+ } else {
+ portID = &opts.PortID
+ }
+
+ reqBody := request{FloatingIP: floatingIP{PortID: portID}}
+
+ // Send request to API
+ var res UpdateResult
+ _, res.Err = perigee.Request("PUT", resourceURL(c, id), perigee.Options{
+ MoreHeaders: c.Provider.AuthenticatedHeaders(),
+ ReqBody: &reqBody,
+ Results: &res.Resp,
+ OkCodes: []int{200},
+ })
+
+ return res
+}
+
+// Delete will permanently delete a particular floating IP resource. Please
+// ensure this is what you want - you can also disassociate the IP from existing
+// internal ports.
+func Delete(c *gophercloud.ServiceClient, id string) DeleteResult {
+ var res DeleteResult
+ _, res.Err = perigee.Request("DELETE", resourceURL(c, id), perigee.Options{
+ MoreHeaders: c.Provider.AuthenticatedHeaders(),
+ OkCodes: []int{204},
+ })
+ return res
+}
diff --git a/_site/openstack/networking/v2/extensions/layer3/floatingips/requests_test.go b/_site/openstack/networking/v2/extensions/layer3/floatingips/requests_test.go
new file mode 100644
index 0000000..19614be
--- /dev/null
+++ b/_site/openstack/networking/v2/extensions/layer3/floatingips/requests_test.go
@@ -0,0 +1,306 @@
+package floatingips
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ fake "github.com/rackspace/gophercloud/openstack/networking/v2/common"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestList(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/floatingips", 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, `
+{
+ "floatingips": [
+ {
+ "floating_network_id": "6d67c30a-ddb4-49a1-bec3-a65b286b4170",
+ "router_id": null,
+ "fixed_ip_address": null,
+ "floating_ip_address": "192.0.0.4",
+ "tenant_id": "017d8de156df4177889f31a9bd6edc00",
+ "status": "DOWN",
+ "port_id": null,
+ "id": "2f95fd2b-9f6a-4e8e-9e9a-2cbe286cbf9e"
+ },
+ {
+ "floating_network_id": "90f742b1-6d17-487b-ba95-71881dbc0b64",
+ "router_id": "0a24cb83-faf5-4d7f-b723-3144ed8a2167",
+ "fixed_ip_address": "192.0.0.2",
+ "floating_ip_address": "10.0.0.3",
+ "tenant_id": "017d8de156df4177889f31a9bd6edc00",
+ "status": "DOWN",
+ "port_id": "74a342ce-8e07-4e91-880c-9f834b68fa25",
+ "id": "ada25a95-f321-4f59-b0e0-f3a970dd3d63"
+ }
+ ]
+}
+ `)
+ })
+
+ count := 0
+
+ List(fake.ServiceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractFloatingIPs(page)
+ if err != nil {
+ t.Errorf("Failed to extract floating IPs: %v", err)
+ return false, err
+ }
+
+ expected := []FloatingIP{
+ FloatingIP{
+ FloatingNetworkID: "6d67c30a-ddb4-49a1-bec3-a65b286b4170",
+ FixedIP: "",
+ FloatingIP: "192.0.0.4",
+ TenantID: "017d8de156df4177889f31a9bd6edc00",
+ Status: "DOWN",
+ PortID: "",
+ ID: "2f95fd2b-9f6a-4e8e-9e9a-2cbe286cbf9e",
+ },
+ FloatingIP{
+ FloatingNetworkID: "90f742b1-6d17-487b-ba95-71881dbc0b64",
+ FixedIP: "192.0.0.2",
+ FloatingIP: "10.0.0.3",
+ TenantID: "017d8de156df4177889f31a9bd6edc00",
+ Status: "DOWN",
+ PortID: "74a342ce-8e07-4e91-880c-9f834b68fa25",
+ ID: "ada25a95-f321-4f59-b0e0-f3a970dd3d63",
+ },
+ }
+
+ th.CheckDeepEquals(t, expected, actual)
+
+ return true, nil
+ })
+
+ if count != 1 {
+ t.Errorf("Expected 1 page, got %d", count)
+ }
+}
+
+func TestInvalidNextPageURLs(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/floatingips", func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprintf(w, `{"floatingips": [{}], "floatingips_links": {}}`)
+ })
+
+ List(fake.ServiceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+ ExtractFloatingIPs(page)
+ return true, nil
+ })
+}
+
+func TestRequiredFieldsForCreate(t *testing.T) {
+ res1 := Create(fake.ServiceClient(), CreateOpts{FloatingNetworkID: ""})
+ if res1.Err == nil {
+ t.Fatalf("Expected error, got none")
+ }
+
+ res2 := Create(fake.ServiceClient(), CreateOpts{FloatingNetworkID: "foo", PortID: ""})
+ if res2.Err == nil {
+ t.Fatalf("Expected error, got none")
+ }
+}
+
+func TestCreate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/floatingips", 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, `
+{
+ "floatingip": {
+ "floating_network_id": "376da547-b977-4cfe-9cba-275c80debf57",
+ "port_id": "ce705c24-c1ef-408a-bda3-7bbd946164ab"
+ }
+}
+ `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusCreated)
+
+ fmt.Fprintf(w, `
+{
+ "floatingip": {
+ "router_id": "d23abc8d-2991-4a55-ba98-2aaea84cc72f",
+ "tenant_id": "4969c491a3c74ee4af974e6d800c62de",
+ "floating_network_id": "376da547-b977-4cfe-9cba-275c80debf57",
+ "fixed_ip_address": "10.0.0.3",
+ "floating_ip_address": "",
+ "port_id": "ce705c24-c1ef-408a-bda3-7bbd946164ab",
+ "id": "2f245a7b-796b-4f26-9cf9-9e82d248fda7"
+ }
+}
+ `)
+ })
+
+ options := CreateOpts{
+ FloatingNetworkID: "376da547-b977-4cfe-9cba-275c80debf57",
+ PortID: "ce705c24-c1ef-408a-bda3-7bbd946164ab",
+ }
+
+ ip, err := Create(fake.ServiceClient(), options).Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, "2f245a7b-796b-4f26-9cf9-9e82d248fda7", ip.ID)
+ th.AssertEquals(t, "4969c491a3c74ee4af974e6d800c62de", ip.TenantID)
+ th.AssertEquals(t, "376da547-b977-4cfe-9cba-275c80debf57", ip.FloatingNetworkID)
+ th.AssertEquals(t, "", ip.FloatingIP)
+ th.AssertEquals(t, "ce705c24-c1ef-408a-bda3-7bbd946164ab", ip.PortID)
+ th.AssertEquals(t, "10.0.0.3", ip.FixedIP)
+}
+
+func TestGet(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/floatingips/2f245a7b-796b-4f26-9cf9-9e82d248fda7", 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, `
+{
+ "floatingip": {
+ "floating_network_id": "90f742b1-6d17-487b-ba95-71881dbc0b64",
+ "fixed_ip_address": "192.0.0.2",
+ "floating_ip_address": "10.0.0.3",
+ "tenant_id": "017d8de156df4177889f31a9bd6edc00",
+ "status": "DOWN",
+ "port_id": "74a342ce-8e07-4e91-880c-9f834b68fa25",
+ "id": "2f245a7b-796b-4f26-9cf9-9e82d248fda7"
+ }
+}
+ `)
+ })
+
+ ip, err := Get(fake.ServiceClient(), "2f245a7b-796b-4f26-9cf9-9e82d248fda7").Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, "90f742b1-6d17-487b-ba95-71881dbc0b64", ip.FloatingNetworkID)
+ th.AssertEquals(t, "10.0.0.3", ip.FloatingIP)
+ th.AssertEquals(t, "74a342ce-8e07-4e91-880c-9f834b68fa25", ip.PortID)
+ th.AssertEquals(t, "192.0.0.2", ip.FixedIP)
+ th.AssertEquals(t, "017d8de156df4177889f31a9bd6edc00", ip.TenantID)
+ th.AssertEquals(t, "DOWN", ip.Status)
+ th.AssertEquals(t, "2f245a7b-796b-4f26-9cf9-9e82d248fda7", ip.ID)
+}
+
+func TestAssociate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/floatingips/2f245a7b-796b-4f26-9cf9-9e82d248fda7", 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.TestHeader(t, r, "Accept", "application/json")
+ th.TestJSONRequest(t, r, `
+{
+ "floatingip": {
+ "port_id": "423abc8d-2991-4a55-ba98-2aaea84cc72e"
+ }
+}
+ `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, `
+{
+ "floatingip": {
+ "router_id": "d23abc8d-2991-4a55-ba98-2aaea84cc72f",
+ "tenant_id": "4969c491a3c74ee4af974e6d800c62de",
+ "floating_network_id": "376da547-b977-4cfe-9cba-275c80debf57",
+ "fixed_ip_address": null,
+ "floating_ip_address": "172.24.4.228",
+ "port_id": "423abc8d-2991-4a55-ba98-2aaea84cc72e",
+ "id": "2f245a7b-796b-4f26-9cf9-9e82d248fda7"
+ }
+}
+ `)
+ })
+
+ ip, err := Update(fake.ServiceClient(), "2f245a7b-796b-4f26-9cf9-9e82d248fda7", UpdateOpts{PortID: "423abc8d-2991-4a55-ba98-2aaea84cc72e"}).Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertDeepEquals(t, "423abc8d-2991-4a55-ba98-2aaea84cc72e", ip.PortID)
+}
+
+func TestDisassociate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/floatingips/2f245a7b-796b-4f26-9cf9-9e82d248fda7", 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.TestHeader(t, r, "Accept", "application/json")
+ th.TestJSONRequest(t, r, `
+{
+ "floatingip": {
+ "port_id": null
+ }
+}
+ `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, `
+{
+ "floatingip": {
+ "router_id": "d23abc8d-2991-4a55-ba98-2aaea84cc72f",
+ "tenant_id": "4969c491a3c74ee4af974e6d800c62de",
+ "floating_network_id": "376da547-b977-4cfe-9cba-275c80debf57",
+ "fixed_ip_address": null,
+ "floating_ip_address": "172.24.4.228",
+ "port_id": null,
+ "id": "2f245a7b-796b-4f26-9cf9-9e82d248fda7"
+ }
+}
+ `)
+ })
+
+ ip, err := Update(fake.ServiceClient(), "2f245a7b-796b-4f26-9cf9-9e82d248fda7", UpdateOpts{}).Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertDeepEquals(t, "", ip.FixedIP)
+ th.AssertDeepEquals(t, "", ip.PortID)
+}
+
+func TestDelete(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/floatingips/2f245a7b-796b-4f26-9cf9-9e82d248fda7", 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)
+ })
+
+ res := Delete(fake.ServiceClient(), "2f245a7b-796b-4f26-9cf9-9e82d248fda7")
+ th.AssertNoErr(t, res.Err)
+}
diff --git a/_site/openstack/networking/v2/extensions/layer3/floatingips/results.go b/_site/openstack/networking/v2/extensions/layer3/floatingips/results.go
new file mode 100644
index 0000000..43892f0
--- /dev/null
+++ b/_site/openstack/networking/v2/extensions/layer3/floatingips/results.go
@@ -0,0 +1,125 @@
+package floatingips
+
+import (
+ "fmt"
+
+ "github.com/mitchellh/mapstructure"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// FloatingIP represents a floating IP resource. A floating IP is an external
+// IP address that is mapped to an internal port and, optionally, a specific
+// IP address on a private network. In other words, it enables access to an
+// instance on a private network from an external network. For thsi reason,
+// floating IPs can only be defined on networks where the `router:external'
+// attribute (provided by the external network extension) is set to True.
+type FloatingIP struct {
+ // Unique identifier for the floating IP instance.
+ ID string `json:"id" mapstructure:"id"`
+
+ // UUID of the external network where the floating IP is to be created.
+ FloatingNetworkID string `json:"floating_network_id" mapstructure:"floating_network_id"`
+
+ // Address of the floating IP on the external network.
+ FloatingIP string `json:"floating_ip_address" mapstructure:"floating_ip_address"`
+
+ // UUID of the port on an internal network that is associated with the floating IP.
+ PortID string `json:"port_id" mapstructure:"port_id"`
+
+ // The specific IP address of the internal port which should be associated
+ // with the floating IP.
+ FixedIP string `json:"fixed_ip_address" mapstructure:"fixed_ip_address"`
+
+ // Owner of the floating IP. Only admin users can specify a tenant identifier
+ // other than its own.
+ TenantID string `json:"tenant_id" mapstructure:"tenant_id"`
+
+ // The condition of the API resource.
+ Status string `json:"status" mapstructure:"status"`
+}
+
+type commonResult struct {
+ gophercloud.CommonResult
+}
+
+// Extract a result and extracts a FloatingIP resource.
+func (r commonResult) Extract() (*FloatingIP, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var res struct {
+ FloatingIP *FloatingIP `json:"floatingip"`
+ }
+
+ err := mapstructure.Decode(r.Resp, &res)
+ if err != nil {
+ return nil, fmt.Errorf("Error decoding Neutron floating IP: %v", err)
+ }
+
+ return res.FloatingIP, nil
+}
+
+// CreateResult represents the result of a create operation.
+type CreateResult struct {
+ commonResult
+}
+
+// GetResult represents the result of a get operation.
+type GetResult struct {
+ commonResult
+}
+
+// UpdateResult represents the result of an update operation.
+type UpdateResult struct {
+ commonResult
+}
+
+// DeleteResult represents the result of an update operation.
+type DeleteResult commonResult
+
+// FloatingIPPage is the page returned by a pager when traversing over a
+// collection of floating IPs.
+type FloatingIPPage struct {
+ pagination.LinkedPageBase
+}
+
+// NextPageURL is invoked when a paginated collection of floating IPs has reached
+// the end of a page and the pager seeks to traverse over a new one. In order
+// to do this, it needs to construct the next page's URL.
+func (p FloatingIPPage) NextPageURL() (string, error) {
+ type resp struct {
+ Links []gophercloud.Link `mapstructure:"floatingips_links"`
+ }
+
+ var r resp
+ err := mapstructure.Decode(p.Body, &r)
+ if err != nil {
+ return "", err
+ }
+
+ return gophercloud.ExtractNextURL(r.Links)
+}
+
+// IsEmpty checks whether a NetworkPage struct is empty.
+func (p FloatingIPPage) IsEmpty() (bool, error) {
+ is, err := ExtractFloatingIPs(p)
+ if err != nil {
+ return true, nil
+ }
+ return len(is) == 0, nil
+}
+
+// ExtractFloatingIPs accepts a Page struct, specifically a FloatingIPPage struct,
+// and extracts the elements into a slice of FloatingIP structs. In other words,
+// a generic collection is mapped into a relevant slice.
+func ExtractFloatingIPs(page pagination.Page) ([]FloatingIP, error) {
+ var resp struct {
+ FloatingIPs []FloatingIP `mapstructure:"floatingips" json:"floatingips"`
+ }
+
+ err := mapstructure.Decode(page.(FloatingIPPage).Body, &resp)
+
+ return resp.FloatingIPs, err
+}
diff --git a/_site/openstack/networking/v2/extensions/layer3/floatingips/urls.go b/_site/openstack/networking/v2/extensions/layer3/floatingips/urls.go
new file mode 100644
index 0000000..355f20d
--- /dev/null
+++ b/_site/openstack/networking/v2/extensions/layer3/floatingips/urls.go
@@ -0,0 +1,13 @@
+package floatingips
+
+import "github.com/rackspace/gophercloud"
+
+const resourcePath = "floatingips"
+
+func rootURL(c *gophercloud.ServiceClient) string {
+ return c.ServiceURL(resourcePath)
+}
+
+func resourceURL(c *gophercloud.ServiceClient, id string) string {
+ return c.ServiceURL(resourcePath, id)
+}
diff --git a/_site/openstack/networking/v2/extensions/layer3/routers/requests.go b/_site/openstack/networking/v2/extensions/layer3/routers/requests.go
new file mode 100755
index 0000000..dbfd36b
--- /dev/null
+++ b/_site/openstack/networking/v2/extensions/layer3/routers/requests.go
@@ -0,0 +1,246 @@
+package routers
+
+import (
+ "errors"
+
+ "github.com/racker/perigee"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// ListOpts allows the filtering and sorting of paginated collections through
+// the API. Filtering is achieved by passing in struct field values that map to
+// the floating IP attributes you want to see returned. SortKey allows you to
+// sort by a particular network attribute. SortDir sets the direction, and is
+// either `asc' or `desc'. Marker and Limit are used for pagination.
+type ListOpts struct {
+ ID string `q:"id"`
+ Name string `q:"name"`
+ AdminStateUp *bool `q:"admin_state_up"`
+ Status string `q:"status"`
+ TenantID string `q:"tenant_id"`
+ Limit int `q:"limit"`
+ Marker string `q:"marker"`
+ SortKey string `q:"sort_key"`
+ SortDir string `q:"sort_dir"`
+}
+
+// List returns a Pager which allows you to iterate over a collection of
+// routers. It accepts a ListOpts struct, which allows you to filter and sort
+// the returned collection for greater efficiency.
+//
+// Default policy settings return only those routers that are owned by the
+// tenant who submits the request, unless an admin user submits the request.
+func List(c *gophercloud.ServiceClient, opts ListOpts) pagination.Pager {
+ q, err := gophercloud.BuildQueryString(&opts)
+ if err != nil {
+ return pagination.Pager{Err: err}
+ }
+ u := rootURL(c) + q.String()
+ return pagination.NewPager(c, u, func(r pagination.LastHTTPResponse) pagination.Page {
+ return RouterPage{pagination.LinkedPageBase{LastHTTPResponse: r}}
+ })
+}
+
+// CreateOpts contains all the values needed to create a new router. There are
+// no required values.
+type CreateOpts struct {
+ Name string
+ AdminStateUp *bool
+ TenantID string
+ GatewayInfo *GatewayInfo
+}
+
+// Create accepts a CreateOpts struct and uses the values to create a new
+// logical router. When it is created, the router does not have an internal
+// interface - it is not associated to any subnet.
+//
+// You can optionally specify an external gateway for a router using the
+// GatewayInfo struct. The external gateway for the router must be plugged into
+// an external network (it is external if its `router:external' field is set to
+// true).
+func Create(c *gophercloud.ServiceClient, opts CreateOpts) CreateResult {
+ type router struct {
+ Name *string `json:"name,omitempty"`
+ AdminStateUp *bool `json:"admin_state_up,omitempty"`
+ TenantID *string `json:"tenant_id,omitempty"`
+ GatewayInfo *GatewayInfo `json:"external_gateway_info,omitempty"`
+ }
+
+ type request struct {
+ Router router `json:"router"`
+ }
+
+ reqBody := request{Router: router{
+ Name: gophercloud.MaybeString(opts.Name),
+ AdminStateUp: opts.AdminStateUp,
+ TenantID: gophercloud.MaybeString(opts.TenantID),
+ }}
+
+ if opts.GatewayInfo != nil {
+ reqBody.Router.GatewayInfo = opts.GatewayInfo
+ }
+
+ var res CreateResult
+ _, res.Err = perigee.Request("POST", rootURL(c), perigee.Options{
+ MoreHeaders: c.Provider.AuthenticatedHeaders(),
+ ReqBody: &reqBody,
+ Results: &res.Resp,
+ OkCodes: []int{201},
+ })
+ return res
+}
+
+// Get retrieves a particular router based on its unique ID.
+func Get(c *gophercloud.ServiceClient, id string) GetResult {
+ var res GetResult
+ _, res.Err = perigee.Request("GET", resourceURL(c, id), perigee.Options{
+ MoreHeaders: c.Provider.AuthenticatedHeaders(),
+ Results: &res.Resp,
+ OkCodes: []int{200},
+ })
+ return res
+}
+
+// UpdateOpts contains the values used when updating a router.
+type UpdateOpts struct {
+ Name string
+ AdminStateUp *bool
+ GatewayInfo *GatewayInfo
+}
+
+// Update allows routers to be updated. You can update the name, administrative
+// state, and the external gateway. For more information about how to set the
+// external gateway for a router, see Create. This operation does not enable
+// the update of router interfaces. To do this, use the AddInterface and
+// RemoveInterface functions.
+func Update(c *gophercloud.ServiceClient, id string, opts UpdateOpts) UpdateResult {
+ type router struct {
+ Name *string `json:"name,omitempty"`
+ AdminStateUp *bool `json:"admin_state_up,omitempty"`
+ GatewayInfo *GatewayInfo `json:"external_gateway_info,omitempty"`
+ }
+
+ type request struct {
+ Router router `json:"router"`
+ }
+
+ reqBody := request{Router: router{
+ Name: gophercloud.MaybeString(opts.Name),
+ AdminStateUp: opts.AdminStateUp,
+ }}
+
+ if opts.GatewayInfo != nil {
+ reqBody.Router.GatewayInfo = opts.GatewayInfo
+ }
+
+ // Send request to API
+ var res UpdateResult
+ _, res.Err = perigee.Request("PUT", resourceURL(c, id), perigee.Options{
+ MoreHeaders: c.Provider.AuthenticatedHeaders(),
+ ReqBody: &reqBody,
+ Results: &res.Resp,
+ OkCodes: []int{200},
+ })
+
+ return res
+}
+
+// Delete will permanently delete a particular router based on its unique ID.
+func Delete(c *gophercloud.ServiceClient, id string) DeleteResult {
+ var res DeleteResult
+ _, res.Err = perigee.Request("DELETE", resourceURL(c, id), perigee.Options{
+ MoreHeaders: c.Provider.AuthenticatedHeaders(),
+ OkCodes: []int{204},
+ })
+ return res
+}
+
+var errInvalidInterfaceOpts = errors.New("When adding a router interface you must provide either a subnet ID or a port ID")
+
+// InterfaceOpts allow you to work with operations that either add or remote
+// an internal interface from a router.
+type InterfaceOpts struct {
+ SubnetID string
+ PortID string
+}
+
+// AddInterface attaches a subnet to an internal router interface. You must
+// specify either a SubnetID or PortID in the request body. If you specify both,
+// the operation will fail and an error will be returned.
+//
+// If you specify a SubnetID, the gateway IP address for that particular subnet
+// is used to create the router interface. Alternatively, if you specify a
+// PortID, the IP address associated with the port is used to create the router
+// interface.
+//
+// If you reference a port that is associated with multiple IP addresses, or
+// if the port is associated with zero IP addresses, the operation will fail and
+// a 400 Bad Request error will be returned.
+//
+// If you reference a port already in use, the operation will fail and a 409
+// Conflict error will be returned.
+//
+// The PortID that is returned after using Extract() on the result of this
+// operation can either be the same PortID passed in or, on the other hand, the
+// identifier of a new port created by this operation. After the operation
+// completes, the device ID of the port is set to the router ID, and the
+// device owner attribute is set to `network:router_interface'.
+func AddInterface(c *gophercloud.ServiceClient, id string, opts InterfaceOpts) InterfaceResult {
+ var res InterfaceResult
+
+ // Validate
+ if (opts.SubnetID == "" && opts.PortID == "") || (opts.SubnetID != "" && opts.PortID != "") {
+ res.Err = errInvalidInterfaceOpts
+ return res
+ }
+
+ type request struct {
+ SubnetID string `json:"subnet_id,omitempty"`
+ PortID string `json:"port_id,omitempty"`
+ }
+
+ body := request{SubnetID: opts.SubnetID, PortID: opts.PortID}
+
+ _, res.Err = perigee.Request("PUT", addInterfaceURL(c, id), perigee.Options{
+ MoreHeaders: c.Provider.AuthenticatedHeaders(),
+ ReqBody: &body,
+ Results: &res.Resp,
+ OkCodes: []int{200},
+ })
+
+ return res
+}
+
+// RemoveInterface removes an internal router interface, which detaches a
+// subnet from the router. You must specify either a SubnetID or PortID, since
+// these values are used to identify the router interface to remove.
+//
+// Unlike AddInterface, you can also specify both a SubnetID and PortID. If you
+// choose to specify both, the subnet ID must correspond to the subnet ID of
+// the first IP address on the port specified by the port ID. Otherwise, the
+// operation will fail and return a 409 Conflict error.
+//
+// If the router, subnet or port which are referenced do not exist or are not
+// visible to you, the operation will fail and a 404 Not Found error will be
+// returned. After this operation completes, the port connecting the router
+// with the subnet is removed from the subnet for the network.
+func RemoveInterface(c *gophercloud.ServiceClient, id string, opts InterfaceOpts) InterfaceResult {
+ var res InterfaceResult
+
+ type request struct {
+ SubnetID string `json:"subnet_id,omitempty"`
+ PortID string `json:"port_id,omitempty"`
+ }
+
+ body := request{SubnetID: opts.SubnetID, PortID: opts.PortID}
+
+ _, res.Err = perigee.Request("PUT", removeInterfaceURL(c, id), perigee.Options{
+ MoreHeaders: c.Provider.AuthenticatedHeaders(),
+ ReqBody: &body,
+ Results: &res.Resp,
+ OkCodes: []int{200},
+ })
+
+ return res
+}
diff --git a/_site/openstack/networking/v2/extensions/layer3/routers/requests_test.go b/_site/openstack/networking/v2/extensions/layer3/routers/requests_test.go
new file mode 100755
index 0000000..c34264d
--- /dev/null
+++ b/_site/openstack/networking/v2/extensions/layer3/routers/requests_test.go
@@ -0,0 +1,338 @@
+package routers
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ fake "github.com/rackspace/gophercloud/openstack/networking/v2/common"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestURLs(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.AssertEquals(t, th.Endpoint()+"v2.0/routers", rootURL(fake.ServiceClient()))
+}
+
+func TestList(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/routers", 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, `
+{
+ "routers": [
+ {
+ "status": "ACTIVE",
+ "external_gateway_info": null,
+ "name": "second_routers",
+ "admin_state_up": true,
+ "tenant_id": "6b96ff0cb17a4b859e1e575d221683d3",
+ "id": "7177abc4-5ae9-4bb7-b0d4-89e94a4abf3b"
+ },
+ {
+ "status": "ACTIVE",
+ "external_gateway_info": {
+ "network_id": "3c5bcddd-6af9-4e6b-9c3e-c153e521cab8"
+ },
+ "name": "router1",
+ "admin_state_up": true,
+ "tenant_id": "33a40233088643acb66ff6eb0ebea679",
+ "id": "a9254bdb-2613-4a13-ac4c-adc581fba50d"
+ }
+ ]
+}
+ `)
+ })
+
+ count := 0
+
+ List(fake.ServiceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractRouters(page)
+ if err != nil {
+ t.Errorf("Failed to extract routers: %v", err)
+ return false, err
+ }
+
+ expected := []Router{
+ Router{
+ Status: "ACTIVE",
+ GatewayInfo: GatewayInfo{NetworkID: ""},
+ AdminStateUp: true,
+ Name: "second_routers",
+ ID: "7177abc4-5ae9-4bb7-b0d4-89e94a4abf3b",
+ TenantID: "6b96ff0cb17a4b859e1e575d221683d3",
+ },
+ Router{
+ Status: "ACTIVE",
+ GatewayInfo: GatewayInfo{NetworkID: "3c5bcddd-6af9-4e6b-9c3e-c153e521cab8"},
+ AdminStateUp: true,
+ Name: "router1",
+ ID: "a9254bdb-2613-4a13-ac4c-adc581fba50d",
+ TenantID: "33a40233088643acb66ff6eb0ebea679",
+ },
+ }
+
+ th.CheckDeepEquals(t, expected, actual)
+
+ return true, nil
+ })
+
+ if count != 1 {
+ t.Errorf("Expected 1 page, got %d", count)
+ }
+}
+
+func TestCreate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/routers", 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, `
+{
+ "router":{
+ "name": "foo_router",
+ "admin_state_up": false,
+ "external_gateway_info":{
+ "network_id":"8ca37218-28ff-41cb-9b10-039601ea7e6b"
+ }
+ }
+}
+ `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusCreated)
+
+ fmt.Fprintf(w, `
+{
+ "router": {
+ "status": "ACTIVE",
+ "external_gateway_info": {
+ "network_id": "8ca37218-28ff-41cb-9b10-039601ea7e6b"
+ },
+ "name": "foo_router",
+ "admin_state_up": false,
+ "tenant_id": "6b96ff0cb17a4b859e1e575d221683d3",
+ "id": "8604a0de-7f6b-409a-a47c-a1cc7bc77b2e"
+ }
+}
+ `)
+ })
+
+ asu := false
+ gwi := GatewayInfo{NetworkID: "8ca37218-28ff-41cb-9b10-039601ea7e6b"}
+
+ options := CreateOpts{
+ Name: "foo_router",
+ AdminStateUp: &asu,
+ GatewayInfo: &gwi,
+ }
+ r, err := Create(fake.ServiceClient(), options).Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, "foo_router", r.Name)
+ th.AssertEquals(t, false, r.AdminStateUp)
+ th.AssertDeepEquals(t, GatewayInfo{NetworkID: "8ca37218-28ff-41cb-9b10-039601ea7e6b"}, r.GatewayInfo)
+}
+
+func TestGet(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/routers/a07eea83-7710-4860-931b-5fe220fae533", 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, `
+{
+ "router": {
+ "status": "ACTIVE",
+ "external_gateway_info": {
+ "network_id": "85d76829-6415-48ff-9c63-5c5ca8c61ac6"
+ },
+ "name": "router1",
+ "admin_state_up": true,
+ "tenant_id": "d6554fe62e2f41efbb6e026fad5c1542",
+ "id": "a07eea83-7710-4860-931b-5fe220fae533"
+ }
+}
+ `)
+ })
+
+ n, err := Get(fake.ServiceClient(), "a07eea83-7710-4860-931b-5fe220fae533").Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, n.Status, "ACTIVE")
+ th.AssertDeepEquals(t, n.GatewayInfo, GatewayInfo{NetworkID: "85d76829-6415-48ff-9c63-5c5ca8c61ac6"})
+ th.AssertEquals(t, n.Name, "router1")
+ th.AssertEquals(t, n.AdminStateUp, true)
+ th.AssertEquals(t, n.TenantID, "d6554fe62e2f41efbb6e026fad5c1542")
+ th.AssertEquals(t, n.ID, "a07eea83-7710-4860-931b-5fe220fae533")
+}
+
+func TestUpdate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/routers/4e8e5957-649f-477b-9e5b-f1f75b21c03c", 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.TestHeader(t, r, "Accept", "application/json")
+ th.TestJSONRequest(t, r, `
+{
+ "router": {
+ "name": "new_name",
+ "external_gateway_info": {
+ "network_id": "8ca37218-28ff-41cb-9b10-039601ea7e6b"
+ }
+ }
+}
+ `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, `
+{
+ "router": {
+ "status": "ACTIVE",
+ "external_gateway_info": {
+ "network_id": "8ca37218-28ff-41cb-9b10-039601ea7e6b"
+ },
+ "name": "new_name",
+ "admin_state_up": true,
+ "tenant_id": "6b96ff0cb17a4b859e1e575d221683d3",
+ "id": "8604a0de-7f6b-409a-a47c-a1cc7bc77b2e"
+ }
+}
+ `)
+ })
+
+ gwi := GatewayInfo{NetworkID: "8ca37218-28ff-41cb-9b10-039601ea7e6b"}
+ options := UpdateOpts{Name: "new_name", GatewayInfo: &gwi}
+
+ n, err := Update(fake.ServiceClient(), "4e8e5957-649f-477b-9e5b-f1f75b21c03c", options).Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, n.Name, "new_name")
+ th.AssertDeepEquals(t, n.GatewayInfo, GatewayInfo{NetworkID: "8ca37218-28ff-41cb-9b10-039601ea7e6b"})
+}
+
+func TestDelete(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/routers/4e8e5957-649f-477b-9e5b-f1f75b21c03c", 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)
+ })
+
+ res := Delete(fake.ServiceClient(), "4e8e5957-649f-477b-9e5b-f1f75b21c03c")
+ th.AssertNoErr(t, res.Err)
+}
+
+func TestAddInterface(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/routers/4e8e5957-649f-477b-9e5b-f1f75b21c03c/add_router_interface", 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.TestHeader(t, r, "Accept", "application/json")
+ th.TestJSONRequest(t, r, `
+{
+ "subnet_id": "a2f1f29d-571b-4533-907f-5803ab96ead1"
+}
+ `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, `
+{
+ "subnet_id": "0d32a837-8069-4ec3-84c4-3eef3e10b188",
+ "tenant_id": "017d8de156df4177889f31a9bd6edc00",
+ "port_id": "3f990102-4485-4df1-97a0-2c35bdb85b31",
+ "id": "9a83fa11-8da5-436e-9afe-3d3ac5ce7770"
+}
+`)
+ })
+
+ opts := InterfaceOpts{SubnetID: "a2f1f29d-571b-4533-907f-5803ab96ead1"}
+ res, err := AddInterface(fake.ServiceClient(), "4e8e5957-649f-477b-9e5b-f1f75b21c03c", opts).Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, "0d32a837-8069-4ec3-84c4-3eef3e10b188", res.SubnetID)
+ th.AssertEquals(t, "017d8de156df4177889f31a9bd6edc00", res.TenantID)
+ th.AssertEquals(t, "3f990102-4485-4df1-97a0-2c35bdb85b31", res.PortID)
+ th.AssertEquals(t, "9a83fa11-8da5-436e-9afe-3d3ac5ce7770", res.ID)
+}
+
+func TestAddInterfaceRequiredOpts(t *testing.T) {
+ _, err := AddInterface(fake.ServiceClient(), "foo", InterfaceOpts{}).Extract()
+ if err == nil {
+ t.Fatalf("Expected error, got none")
+ }
+ _, err = AddInterface(fake.ServiceClient(), "foo", InterfaceOpts{SubnetID: "bar", PortID: "baz"}).Extract()
+ if err == nil {
+ t.Fatalf("Expected error, got none")
+ }
+}
+
+func TestRemoveInterface(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/routers/4e8e5957-649f-477b-9e5b-f1f75b21c03c/remove_router_interface", 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.TestHeader(t, r, "Accept", "application/json")
+ th.TestJSONRequest(t, r, `
+{
+ "subnet_id": "a2f1f29d-571b-4533-907f-5803ab96ead1"
+}
+ `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, `
+{
+ "subnet_id": "0d32a837-8069-4ec3-84c4-3eef3e10b188",
+ "tenant_id": "017d8de156df4177889f31a9bd6edc00",
+ "port_id": "3f990102-4485-4df1-97a0-2c35bdb85b31",
+ "id": "9a83fa11-8da5-436e-9afe-3d3ac5ce7770"
+}
+`)
+ })
+
+ opts := InterfaceOpts{SubnetID: "a2f1f29d-571b-4533-907f-5803ab96ead1"}
+ res, err := RemoveInterface(fake.ServiceClient(), "4e8e5957-649f-477b-9e5b-f1f75b21c03c", opts).Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, "0d32a837-8069-4ec3-84c4-3eef3e10b188", res.SubnetID)
+ th.AssertEquals(t, "017d8de156df4177889f31a9bd6edc00", res.TenantID)
+ th.AssertEquals(t, "3f990102-4485-4df1-97a0-2c35bdb85b31", res.PortID)
+ th.AssertEquals(t, "9a83fa11-8da5-436e-9afe-3d3ac5ce7770", res.ID)
+}
diff --git a/_site/openstack/networking/v2/extensions/layer3/routers/results.go b/_site/openstack/networking/v2/extensions/layer3/routers/results.go
new file mode 100755
index 0000000..eae647f
--- /dev/null
+++ b/_site/openstack/networking/v2/extensions/layer3/routers/results.go
@@ -0,0 +1,159 @@
+package routers
+
+import (
+ "github.com/mitchellh/mapstructure"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// GatewayInfo represents the information of an external gateway for any
+// particular network router.
+type GatewayInfo struct {
+ NetworkID string `json:"network_id" mapstructure:"network_id"`
+}
+
+// Router represents a Neutron router. A router is a logical entity that
+// forwards packets across internal subnets and NATs (network address
+// translation) them on external networks through an appropriate gateway.
+//
+// A router has an interface for each subnet with which it is associated. By
+// default, the IP address of such interface is the subnet's gateway IP. Also,
+// whenever a router is associated with a subnet, a port for that router
+// interface is added to the subnet's network.
+type Router struct {
+ // Indicates whether or not a router is currently operational.
+ Status string `json:"status" mapstructure:"status"`
+
+ // Information on external gateway for the router.
+ GatewayInfo GatewayInfo `json:"external_gateway_info" mapstructure:"external_gateway_info"`
+
+ // Administrative state of the router.
+ AdminStateUp bool `json:"admin_state_up" mapstructure:"admin_state_up"`
+
+ // Human readable name for the router. Does not have to be unique.
+ Name string `json:"name" mapstructure:"name"`
+
+ // Unique identifier for the router.
+ ID string `json:"id" mapstructure:"id"`
+
+ // Owner of the router. Only admin users can specify a tenant identifier
+ // other than its own.
+ TenantID string `json:"tenant_id" mapstructure:"tenant_id"`
+}
+
+// RouterPage is the page returned by a pager when traversing over a
+// collection of routers.
+type RouterPage struct {
+ pagination.LinkedPageBase
+}
+
+// NextPageURL is invoked when a paginated collection of routers has reached
+// the end of a page and the pager seeks to traverse over a new one. In order
+// to do this, it needs to construct the next page's URL.
+func (p RouterPage) NextPageURL() (string, error) {
+ type resp struct {
+ Links []gophercloud.Link `mapstructure:"routers_links"`
+ }
+
+ var r resp
+ err := mapstructure.Decode(p.Body, &r)
+ if err != nil {
+ return "", err
+ }
+
+ return gophercloud.ExtractNextURL(r.Links)
+}
+
+// IsEmpty checks whether a RouterPage struct is empty.
+func (p RouterPage) IsEmpty() (bool, error) {
+ is, err := ExtractRouters(p)
+ if err != nil {
+ return true, nil
+ }
+ return len(is) == 0, nil
+}
+
+// ExtractRouters accepts a Page struct, specifically a RouterPage struct,
+// and extracts the elements into a slice of Router structs. In other words,
+// a generic collection is mapped into a relevant slice.
+func ExtractRouters(page pagination.Page) ([]Router, error) {
+ var resp struct {
+ Routers []Router `mapstructure:"routers" json:"routers"`
+ }
+
+ err := mapstructure.Decode(page.(RouterPage).Body, &resp)
+
+ return resp.Routers, err
+}
+
+type commonResult struct {
+ gophercloud.CommonResult
+}
+
+// Extract is a function that accepts a result and extracts a router.
+func (r commonResult) Extract() (*Router, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var res struct {
+ Router *Router `json:"router"`
+ }
+
+ err := mapstructure.Decode(r.Resp, &res)
+
+ return res.Router, err
+}
+
+// CreateResult represents the result of a create operation.
+type CreateResult struct {
+ commonResult
+}
+
+// GetResult represents the result of a get operation.
+type GetResult struct {
+ commonResult
+}
+
+// UpdateResult represents the result of an update operation.
+type UpdateResult struct {
+ commonResult
+}
+
+// DeleteResult represents the result of a delete operation.
+type DeleteResult commonResult
+
+// InterfaceInfo represents information about a particular router interface. As
+// mentioned above, in order for a router to forward to a subnet, it needs an
+// interface.
+type InterfaceInfo struct {
+ // The ID of the subnet which this interface is associated with.
+ SubnetID string `json:"subnet_id" mapstructure:"subnet_id"`
+
+ // The ID of the port that is a part of the subnet.
+ PortID string `json:"port_id" mapstructure:"port_id"`
+
+ // The UUID of the interface.
+ ID string `json:"id" mapstructure:"id"`
+
+ // Owner of the interface.
+ TenantID string `json:"tenant_id" mapstructure:"tenant_id"`
+}
+
+// InterfaceResult represents the result of interface operations, such as
+// AddInterface() and RemoveInterface().
+type InterfaceResult struct {
+ gophercloud.CommonResult
+}
+
+// Extract is a function that accepts a result and extracts an information struct.
+func (r InterfaceResult) Extract() (*InterfaceInfo, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var res *InterfaceInfo
+ err := mapstructure.Decode(r.Resp, &res)
+
+ return res, err
+}
diff --git a/_site/openstack/networking/v2/extensions/layer3/routers/urls.go b/_site/openstack/networking/v2/extensions/layer3/routers/urls.go
new file mode 100644
index 0000000..bc22c2a
--- /dev/null
+++ b/_site/openstack/networking/v2/extensions/layer3/routers/urls.go
@@ -0,0 +1,21 @@
+package routers
+
+import "github.com/rackspace/gophercloud"
+
+const resourcePath = "routers"
+
+func rootURL(c *gophercloud.ServiceClient) string {
+ return c.ServiceURL(resourcePath)
+}
+
+func resourceURL(c *gophercloud.ServiceClient, id string) string {
+ return c.ServiceURL(resourcePath, id)
+}
+
+func addInterfaceURL(c *gophercloud.ServiceClient, id string) string {
+ return c.ServiceURL(resourcePath, id, "add_router_interface")
+}
+
+func removeInterfaceURL(c *gophercloud.ServiceClient, id string) string {
+ return c.ServiceURL(resourcePath, id, "remove_router_interface")
+}
diff --git a/_site/openstack/networking/v2/extensions/lbaas/members/requests.go b/_site/openstack/networking/v2/extensions/lbaas/members/requests.go
new file mode 100644
index 0000000..d095706
--- /dev/null
+++ b/_site/openstack/networking/v2/extensions/lbaas/members/requests.go
@@ -0,0 +1,139 @@
+package members
+
+import (
+ "github.com/racker/perigee"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// ListOpts allows the filtering and sorting of paginated collections through
+// the API. Filtering is achieved by passing in struct field values that map to
+// the floating IP attributes you want to see returned. SortKey allows you to
+// sort by a particular network attribute. SortDir sets the direction, and is
+// either `asc' or `desc'. Marker and Limit are used for pagination.
+type ListOpts struct {
+ Status string `q:"status"`
+ Weight int `q:"weight"`
+ AdminStateUp *bool `q:"admin_state_up"`
+ TenantID string `q:"tenant_id"`
+ PoolID string `q:"pool_id"`
+ Address string `q:"address"`
+ ProtocolPort int `q:"protocol_port"`
+ ID string `q:"id"`
+ Limit int `q:"limit"`
+ Marker string `q:"marker"`
+ SortKey string `q:"sort_key"`
+ SortDir string `q:"sort_dir"`
+}
+
+// List returns a Pager which allows you to iterate over a collection of
+// pools. It accepts a ListOpts struct, which allows you to filter and sort
+// the returned collection for greater efficiency.
+//
+// Default policy settings return only those pools that are owned by the
+// tenant who submits the request, unless an admin user submits the request.
+func List(c *gophercloud.ServiceClient, opts ListOpts) pagination.Pager {
+ q, err := gophercloud.BuildQueryString(&opts)
+ if err != nil {
+ return pagination.Pager{Err: err}
+ }
+ u := rootURL(c) + q.String()
+ return pagination.NewPager(c, u, func(r pagination.LastHTTPResponse) pagination.Page {
+ return MemberPage{pagination.LinkedPageBase{LastHTTPResponse: r}}
+ })
+}
+
+// CreateOpts contains all the values needed to create a new pool member.
+type CreateOpts struct {
+ // Only required if the caller has an admin role and wants to create a pool
+ // for another tenant.
+ TenantID string
+
+ // Required. The IP address of the member.
+ Address string
+
+ // Required. The port on which the application is hosted.
+ ProtocolPort int
+
+ // Required. The pool to which this member will belong.
+ PoolID string
+}
+
+// Create accepts a CreateOpts struct and uses the values to create a new
+// load balancer pool member.
+func Create(c *gophercloud.ServiceClient, opts CreateOpts) CreateResult {
+ type member struct {
+ TenantID string `json:"tenant_id"`
+ ProtocolPort int `json:"protocol_port"`
+ Address string `json:"address"`
+ PoolID string `json:"pool_id"`
+ }
+ type request struct {
+ Member member `json:"member"`
+ }
+
+ reqBody := request{Member: member{
+ Address: opts.Address,
+ TenantID: opts.TenantID,
+ ProtocolPort: opts.ProtocolPort,
+ PoolID: opts.PoolID,
+ }}
+
+ var res CreateResult
+ _, res.Err = perigee.Request("POST", rootURL(c), perigee.Options{
+ MoreHeaders: c.Provider.AuthenticatedHeaders(),
+ ReqBody: &reqBody,
+ Results: &res.Resp,
+ OkCodes: []int{201},
+ })
+ return res
+}
+
+// Get retrieves a particular pool member based on its unique ID.
+func Get(c *gophercloud.ServiceClient, id string) GetResult {
+ var res GetResult
+ _, res.Err = perigee.Request("GET", resourceURL(c, id), perigee.Options{
+ MoreHeaders: c.Provider.AuthenticatedHeaders(),
+ Results: &res.Resp,
+ OkCodes: []int{200},
+ })
+ return res
+}
+
+// UpdateOpts contains the values used when updating a pool member.
+type UpdateOpts struct {
+ // The administrative state of the member, which is up (true) or down (false).
+ AdminStateUp bool
+}
+
+// Update allows members to be updated.
+func Update(c *gophercloud.ServiceClient, id string, opts UpdateOpts) UpdateResult {
+ type member struct {
+ AdminStateUp bool `json:"admin_state_up"`
+ }
+ type request struct {
+ Member member `json:"member"`
+ }
+
+ reqBody := request{Member: member{AdminStateUp: opts.AdminStateUp}}
+
+ // Send request to API
+ var res UpdateResult
+ _, res.Err = perigee.Request("PUT", resourceURL(c, id), perigee.Options{
+ MoreHeaders: c.Provider.AuthenticatedHeaders(),
+ ReqBody: &reqBody,
+ Results: &res.Resp,
+ OkCodes: []int{200},
+ })
+ return res
+}
+
+// Delete will permanently delete a particular member based on its unique ID.
+func Delete(c *gophercloud.ServiceClient, id string) DeleteResult {
+ var res DeleteResult
+ _, res.Err = perigee.Request("DELETE", resourceURL(c, id), perigee.Options{
+ MoreHeaders: c.Provider.AuthenticatedHeaders(),
+ OkCodes: []int{204},
+ })
+ return res
+}
diff --git a/_site/openstack/networking/v2/extensions/lbaas/members/requests_test.go b/_site/openstack/networking/v2/extensions/lbaas/members/requests_test.go
new file mode 100644
index 0000000..dc1ece3
--- /dev/null
+++ b/_site/openstack/networking/v2/extensions/lbaas/members/requests_test.go
@@ -0,0 +1,243 @@
+package members
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ fake "github.com/rackspace/gophercloud/openstack/networking/v2/common"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestURLs(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.AssertEquals(t, th.Endpoint()+"v2.0/lb/members", rootURL(fake.ServiceClient()))
+}
+
+func TestList(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/lb/members", 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, `
+{
+ "members":[
+ {
+ "status":"ACTIVE",
+ "weight":1,
+ "admin_state_up":true,
+ "tenant_id":"83657cfcdfe44cd5920adaf26c48ceea",
+ "pool_id":"72741b06-df4d-4715-b142-276b6bce75ab",
+ "address":"10.0.0.4",
+ "protocol_port":80,
+ "id":"701b531b-111a-4f21-ad85-4795b7b12af6"
+ },
+ {
+ "status":"ACTIVE",
+ "weight":1,
+ "admin_state_up":true,
+ "tenant_id":"83657cfcdfe44cd5920adaf26c48ceea",
+ "pool_id":"72741b06-df4d-4715-b142-276b6bce75ab",
+ "address":"10.0.0.3",
+ "protocol_port":80,
+ "id":"beb53b4d-230b-4abd-8118-575b8fa006ef"
+ }
+ ]
+}
+ `)
+ })
+
+ count := 0
+
+ List(fake.ServiceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractMembers(page)
+ if err != nil {
+ t.Errorf("Failed to extract members: %v", err)
+ return false, err
+ }
+
+ expected := []Member{
+ Member{
+ Status: "ACTIVE",
+ Weight: 1,
+ AdminStateUp: true,
+ TenantID: "83657cfcdfe44cd5920adaf26c48ceea",
+ PoolID: "72741b06-df4d-4715-b142-276b6bce75ab",
+ Address: "10.0.0.4",
+ ProtocolPort: 80,
+ ID: "701b531b-111a-4f21-ad85-4795b7b12af6",
+ },
+ Member{
+ Status: "ACTIVE",
+ Weight: 1,
+ AdminStateUp: true,
+ TenantID: "83657cfcdfe44cd5920adaf26c48ceea",
+ PoolID: "72741b06-df4d-4715-b142-276b6bce75ab",
+ Address: "10.0.0.3",
+ ProtocolPort: 80,
+ ID: "beb53b4d-230b-4abd-8118-575b8fa006ef",
+ },
+ }
+
+ th.CheckDeepEquals(t, expected, actual)
+
+ return true, nil
+ })
+
+ if count != 1 {
+ t.Errorf("Expected 1 page, got %d", count)
+ }
+}
+
+func TestCreate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/lb/members", 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, `
+{
+ "member": {
+ "tenant_id": "453105b9-1754-413f-aab1-55f1af620750",
+ "pool_id": "foo",
+ "address": "192.0.2.14",
+ "protocol_port":8080
+ }
+}
+ `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusCreated)
+
+ fmt.Fprintf(w, `
+{
+ "member": {
+ "id": "975592ca-e308-48ad-8298-731935ee9f45",
+ "address": "192.0.2.14",
+ "protocol_port": 8080,
+ "tenant_id": "453105b9-1754-413f-aab1-55f1af620750",
+ "admin_state_up":true,
+ "weight": 1,
+ "status": "DOWN"
+ }
+}
+ `)
+ })
+
+ options := CreateOpts{
+ TenantID: "453105b9-1754-413f-aab1-55f1af620750",
+ Address: "192.0.2.14",
+ ProtocolPort: 8080,
+ PoolID: "foo",
+ }
+ _, err := Create(fake.ServiceClient(), options).Extract()
+ th.AssertNoErr(t, err)
+}
+
+func TestGet(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/lb/members/975592ca-e308-48ad-8298-731935ee9f45", 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, `
+{
+ "member":{
+ "id":"975592ca-e308-48ad-8298-731935ee9f45",
+ "address":"192.0.2.14",
+ "protocol_port":8080,
+ "tenant_id":"453105b9-1754-413f-aab1-55f1af620750",
+ "admin_state_up":true,
+ "weight":1,
+ "status":"DOWN"
+ }
+}
+ `)
+ })
+
+ m, err := Get(fake.ServiceClient(), "975592ca-e308-48ad-8298-731935ee9f45").Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, "975592ca-e308-48ad-8298-731935ee9f45", m.ID)
+ th.AssertEquals(t, "192.0.2.14", m.Address)
+ th.AssertEquals(t, 8080, m.ProtocolPort)
+ th.AssertEquals(t, "453105b9-1754-413f-aab1-55f1af620750", m.TenantID)
+ th.AssertEquals(t, true, m.AdminStateUp)
+ th.AssertEquals(t, 1, m.Weight)
+ th.AssertEquals(t, "DOWN", m.Status)
+}
+
+func TestUpdate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/lb/members/332abe93-f488-41ba-870b-2ac66be7f853", 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.TestHeader(t, r, "Accept", "application/json")
+ th.TestJSONRequest(t, r, `
+{
+ "member":{
+ "admin_state_up":false
+ }
+}
+ `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, `
+{
+ "member":{
+ "status":"PENDING_UPDATE",
+ "protocol_port":8080,
+ "weight":1,
+ "admin_state_up":false,
+ "tenant_id":"4fd44f30292945e481c7b8a0c8908869",
+ "pool_id":"7803631d-f181-4500-b3a2-1b68ba2a75fd",
+ "address":"10.0.0.5",
+ "status_description":null,
+ "id":"48a471ea-64f1-4eb6-9be7-dae6bbe40a0f"
+ }
+}
+ `)
+ })
+
+ options := UpdateOpts{AdminStateUp: false}
+
+ _, err := Update(fake.ServiceClient(), "332abe93-f488-41ba-870b-2ac66be7f853", options).Extract()
+ th.AssertNoErr(t, err)
+}
+
+func TestDelete(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/lb/members/332abe93-f488-41ba-870b-2ac66be7f853", 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)
+ })
+
+ res := Delete(fake.ServiceClient(), "332abe93-f488-41ba-870b-2ac66be7f853")
+ th.AssertNoErr(t, res.Err)
+}
diff --git a/_site/openstack/networking/v2/extensions/lbaas/members/results.go b/_site/openstack/networking/v2/extensions/lbaas/members/results.go
new file mode 100644
index 0000000..b006551
--- /dev/null
+++ b/_site/openstack/networking/v2/extensions/lbaas/members/results.go
@@ -0,0 +1,120 @@
+package members
+
+import (
+ "github.com/mitchellh/mapstructure"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// Member represents the application running on a backend server.
+type Member struct {
+ // The status of the member. Indicates whether the member is operational.
+ Status string
+
+ // Weight of member.
+ Weight int
+
+ // The administrative state of the member, which is up (true) or down (false).
+ AdminStateUp bool `json:"admin_state_up" mapstructure:"admin_state_up"`
+
+ // Owner of the member. Only an administrative user can specify a tenant ID
+ // other than its own.
+ TenantID string `json:"tenant_id" mapstructure:"tenant_id"`
+
+ // The pool to which the member belongs.
+ PoolID string `json:"pool_id" mapstructure:"pool_id"`
+
+ // The IP address of the member.
+ Address string
+
+ // The port on which the application is hosted.
+ ProtocolPort int `json:"protocol_port" mapstructure:"protocol_port"`
+
+ // The unique ID for the member.
+ ID string
+}
+
+// MemberPage is the page returned by a pager when traversing over a
+// collection of pool members.
+type MemberPage struct {
+ pagination.LinkedPageBase
+}
+
+// NextPageURL is invoked when a paginated collection of members has reached
+// the end of a page and the pager seeks to traverse over a new one. In order
+// to do this, it needs to construct the next page's URL.
+func (p MemberPage) NextPageURL() (string, error) {
+ type resp struct {
+ Links []gophercloud.Link `mapstructure:"members_links"`
+ }
+
+ var r resp
+ err := mapstructure.Decode(p.Body, &r)
+ if err != nil {
+ return "", err
+ }
+
+ return gophercloud.ExtractNextURL(r.Links)
+}
+
+// IsEmpty checks whether a MemberPage struct is empty.
+func (p MemberPage) IsEmpty() (bool, error) {
+ is, err := ExtractMembers(p)
+ if err != nil {
+ return true, nil
+ }
+ return len(is) == 0, nil
+}
+
+// ExtractMembers accepts a Page struct, specifically a MemberPage struct,
+// and extracts the elements into a slice of Member structs. In other words,
+// a generic collection is mapped into a relevant slice.
+func ExtractMembers(page pagination.Page) ([]Member, error) {
+ var resp struct {
+ Members []Member `mapstructure:"members" json:"members"`
+ }
+
+ err := mapstructure.Decode(page.(MemberPage).Body, &resp)
+ if err != nil {
+ return nil, err
+ }
+
+ return resp.Members, nil
+}
+
+type commonResult struct {
+ gophercloud.CommonResult
+}
+
+// Extract is a function that accepts a result and extracts a router.
+func (r commonResult) Extract() (*Member, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var res struct {
+ Member *Member `json:"member"`
+ }
+
+ err := mapstructure.Decode(r.Resp, &res)
+
+ return res.Member, err
+}
+
+// CreateResult represents the result of a create operation.
+type CreateResult struct {
+ commonResult
+}
+
+// GetResult represents the result of a get operation.
+type GetResult struct {
+ commonResult
+}
+
+// UpdateResult represents the result of an update operation.
+type UpdateResult struct {
+ commonResult
+}
+
+// DeleteResult represents the result of a delete operation.
+type DeleteResult commonResult
diff --git a/_site/openstack/networking/v2/extensions/lbaas/members/urls.go b/_site/openstack/networking/v2/extensions/lbaas/members/urls.go
new file mode 100644
index 0000000..94b57e4
--- /dev/null
+++ b/_site/openstack/networking/v2/extensions/lbaas/members/urls.go
@@ -0,0 +1,16 @@
+package members
+
+import "github.com/rackspace/gophercloud"
+
+const (
+ rootPath = "lb"
+ resourcePath = "members"
+)
+
+func rootURL(c *gophercloud.ServiceClient) string {
+ return c.ServiceURL(rootPath, resourcePath)
+}
+
+func resourceURL(c *gophercloud.ServiceClient, id string) string {
+ return c.ServiceURL(rootPath, resourcePath, id)
+}
diff --git a/_site/openstack/networking/v2/extensions/lbaas/monitors/requests.go b/_site/openstack/networking/v2/extensions/lbaas/monitors/requests.go
new file mode 100644
index 0000000..fca7199
--- /dev/null
+++ b/_site/openstack/networking/v2/extensions/lbaas/monitors/requests.go
@@ -0,0 +1,274 @@
+package monitors
+
+import (
+ "fmt"
+
+ "github.com/racker/perigee"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// ListOpts allows the filtering and sorting of paginated collections through
+// the API. Filtering is achieved by passing in struct field values that map to
+// the floating IP attributes you want to see returned. SortKey allows you to
+// sort by a particular network attribute. SortDir sets the direction, and is
+// either `asc' or `desc'. Marker and Limit are used for pagination.
+type ListOpts struct {
+ ID string `q:"id"`
+ TenantID string `q:"tenant_id"`
+ Type string `q:"type"`
+ Delay int `q:"delay"`
+ Timeout int `q:"timeout"`
+ MaxRetries int `q:"max_retries"`
+ HTTPMethod string `q:"http_method"`
+ URLPath string `q:"url_path"`
+ ExpectedCodes string `q:"expected_codes"`
+ AdminStateUp *bool `q:"admin_state_up"`
+ Status string `q:"status"`
+ Limit int `q:"limit"`
+ Marker string `q:"marker"`
+ SortKey string `q:"sort_key"`
+ SortDir string `q:"sort_dir"`
+}
+
+// List returns a Pager which allows you to iterate over a collection of
+// routers. It accepts a ListOpts struct, which allows you to filter and sort
+// the returned collection for greater efficiency.
+//
+// Default policy settings return only those routers that are owned by the
+// tenant who submits the request, unless an admin user submits the request.
+func List(c *gophercloud.ServiceClient, opts ListOpts) pagination.Pager {
+ q, err := gophercloud.BuildQueryString(&opts)
+ if err != nil {
+ return pagination.Pager{Err: err}
+ }
+ u := rootURL(c) + q.String()
+
+ return pagination.NewPager(c, u, func(r pagination.LastHTTPResponse) pagination.Page {
+ return MonitorPage{pagination.LinkedPageBase{LastHTTPResponse: r}}
+ })
+}
+
+// Constants that represent approved monitoring types.
+const (
+ TypePING = "PING"
+ TypeTCP = "TCP"
+ TypeHTTP = "HTTP"
+ TypeHTTPS = "HTTPS"
+)
+
+var (
+ errValidTypeRequired = fmt.Errorf("A valid Type is required. Supported values are PING, TCP, HTTP and HTTPS")
+ errDelayRequired = fmt.Errorf("Delay is required")
+ errTimeoutRequired = fmt.Errorf("Timeout is required")
+ errMaxRetriesRequired = fmt.Errorf("MaxRetries is required")
+ errURLPathRequired = fmt.Errorf("URL path is required")
+ errExpectedCodesRequired = fmt.Errorf("ExpectedCodes is required")
+)
+
+// CreateOpts contains all the values needed to create a new health monitor.
+type CreateOpts struct {
+ // Required for admins. Indicates the owner of the VIP.
+ TenantID string
+
+ // Required. The type of probe, which is PING, TCP, HTTP, or HTTPS, that is
+ // sent by the load balancer to verify the member state.
+ Type string
+
+ // Required. The time, in seconds, between sending probes to members.
+ Delay int
+
+ // Required. Maximum number of seconds for a monitor to wait for a ping reply
+ // before it times out. The value must be less than the delay value.
+ Timeout int
+
+ // Required. Number of permissible ping failures before changing the member's
+ // status to INACTIVE. Must be a number between 1 and 10.
+ MaxRetries int
+
+ // Required for HTTP(S) types. URI path that will be accessed if monitor type
+ // is HTTP or HTTPS.
+ URLPath string
+
+ // Required for HTTP(S) types. The HTTP method used for requests by the
+ // monitor. If this attribute is not specified, it defaults to "GET".
+ HTTPMethod string
+
+ // Required for HTTP(S) types. Expected HTTP codes for a passing HTTP(S)
+ // monitor. You can either specify a single status like "200", or a range
+ // like "200-202".
+ ExpectedCodes string
+
+ AdminStateUp *bool
+}
+
+// Create is an operation which provisions a new health monitor. There are
+// different types of monitor you can provision: PING, TCP or HTTP(S). Below
+// are examples of how to create each one.
+//
+// Here is an example config struct to use when creating a PING or TCP monitor:
+//
+// CreateOpts{Type: TypePING, Delay: 20, Timeout: 10, MaxRetries: 3}
+// CreateOpts{Type: TypeTCP, Delay: 20, Timeout: 10, MaxRetries: 3}
+//
+// Here is an example config struct to use when creating a HTTP(S) monitor:
+//
+// CreateOpts{Type: TypeHTTP, Delay: 20, Timeout: 10, MaxRetries: 3,
+// HttpMethod: "HEAD", ExpectedCodes: "200"}
+//
+func Create(c *gophercloud.ServiceClient, opts CreateOpts) CreateResult {
+ var res CreateResult
+
+ // Validate inputs
+ allowed := map[string]bool{TypeHTTP: true, TypeHTTPS: true, TypeTCP: true, TypePING: true}
+ if opts.Type == "" || allowed[opts.Type] == false {
+ res.Err = errValidTypeRequired
+ }
+ if opts.Delay == 0 {
+ res.Err = errDelayRequired
+ }
+ if opts.Timeout == 0 {
+ res.Err = errTimeoutRequired
+ }
+ if opts.MaxRetries == 0 {
+ res.Err = errMaxRetriesRequired
+ }
+ if opts.Type == TypeHTTP || opts.Type == TypeHTTPS {
+ if opts.URLPath == "" {
+ res.Err = errURLPathRequired
+ }
+ if opts.ExpectedCodes == "" {
+ res.Err = errExpectedCodesRequired
+ }
+ }
+ if res.Err != nil {
+ return res
+ }
+
+ type monitor struct {
+ Type string `json:"type"`
+ Delay int `json:"delay"`
+ Timeout int `json:"timeout"`
+ MaxRetries int `json:"max_retries"`
+ TenantID *string `json:"tenant_id,omitempty"`
+ URLPath *string `json:"url_path,omitempty"`
+ ExpectedCodes *string `json:"expected_codes,omitempty"`
+ HTTPMethod *string `json:"http_method,omitempty"`
+ AdminStateUp *bool `json:"admin_state_up,omitempty"`
+ }
+
+ type request struct {
+ Monitor monitor `json:"health_monitor"`
+ }
+
+ reqBody := request{Monitor: monitor{
+ Type: opts.Type,
+ Delay: opts.Delay,
+ Timeout: opts.Timeout,
+ MaxRetries: opts.MaxRetries,
+ TenantID: gophercloud.MaybeString(opts.TenantID),
+ URLPath: gophercloud.MaybeString(opts.URLPath),
+ ExpectedCodes: gophercloud.MaybeString(opts.ExpectedCodes),
+ HTTPMethod: gophercloud.MaybeString(opts.HTTPMethod),
+ AdminStateUp: opts.AdminStateUp,
+ }}
+
+ _, res.Err = perigee.Request("POST", rootURL(c), perigee.Options{
+ MoreHeaders: c.Provider.AuthenticatedHeaders(),
+ ReqBody: &reqBody,
+ Results: &res.Resp,
+ OkCodes: []int{201},
+ })
+
+ return res
+}
+
+// Get retrieves a particular health monitor based on its unique ID.
+func Get(c *gophercloud.ServiceClient, id string) GetResult {
+ var res GetResult
+ _, res.Err = perigee.Request("GET", resourceURL(c, id), perigee.Options{
+ MoreHeaders: c.Provider.AuthenticatedHeaders(),
+ Results: &res.Resp,
+ OkCodes: []int{200},
+ })
+ return res
+}
+
+// UpdateOpts contains all the values needed to update an existing virtual IP.
+// Attributes not listed here but appear in CreateOpts are immutable and cannot
+// be updated.
+type UpdateOpts struct {
+ // Required. The time, in seconds, between sending probes to members.
+ Delay int
+
+ // Required. Maximum number of seconds for a monitor to wait for a ping reply
+ // before it times out. The value must be less than the delay value.
+ Timeout int
+
+ // Required. Number of permissible ping failures before changing the member's
+ // status to INACTIVE. Must be a number between 1 and 10.
+ MaxRetries int
+
+ // Required for HTTP(S) types. URI path that will be accessed if monitor type
+ // is HTTP or HTTPS.
+ URLPath string
+
+ // Required for HTTP(S) types. The HTTP method used for requests by the
+ // monitor. If this attribute is not specified, it defaults to "GET".
+ HTTPMethod string
+
+ // Required for HTTP(S) types. Expected HTTP codes for a passing HTTP(S)
+ // monitor. You can either specify a single status like "200", or a range
+ // like "200-202".
+ ExpectedCodes string
+
+ AdminStateUp *bool
+}
+
+// Update is an operation which modifies the attributes of the specified monitor.
+func Update(c *gophercloud.ServiceClient, id string, opts UpdateOpts) UpdateResult {
+ type monitor struct {
+ Delay int `json:"delay"`
+ Timeout int `json:"timeout"`
+ MaxRetries int `json:"max_retries"`
+ URLPath *string `json:"url_path,omitempty"`
+ ExpectedCodes *string `json:"expected_codes,omitempty"`
+ HTTPMethod *string `json:"http_method,omitempty"`
+ AdminStateUp *bool `json:"admin_state_up,omitempty"`
+ }
+
+ type request struct {
+ Monitor monitor `json:"health_monitor"`
+ }
+
+ reqBody := request{Monitor: monitor{
+ Delay: opts.Delay,
+ Timeout: opts.Timeout,
+ MaxRetries: opts.MaxRetries,
+ URLPath: gophercloud.MaybeString(opts.URLPath),
+ ExpectedCodes: gophercloud.MaybeString(opts.ExpectedCodes),
+ HTTPMethod: gophercloud.MaybeString(opts.HTTPMethod),
+ AdminStateUp: opts.AdminStateUp,
+ }}
+
+ var res UpdateResult
+
+ _, res.Err = perigee.Request("PUT", resourceURL(c, id), perigee.Options{
+ MoreHeaders: c.Provider.AuthenticatedHeaders(),
+ ReqBody: &reqBody,
+ Results: &res.Resp,
+ OkCodes: []int{200, 202},
+ })
+
+ return res
+}
+
+// Delete will permanently delete a particular monitor based on its unique ID.
+func Delete(c *gophercloud.ServiceClient, id string) DeleteResult {
+ var res DeleteResult
+ _, res.Err = perigee.Request("DELETE", resourceURL(c, id), perigee.Options{
+ MoreHeaders: c.Provider.AuthenticatedHeaders(),
+ OkCodes: []int{204},
+ })
+ return res
+}
diff --git a/_site/openstack/networking/v2/extensions/lbaas/monitors/requests_test.go b/_site/openstack/networking/v2/extensions/lbaas/monitors/requests_test.go
new file mode 100644
index 0000000..5c5a1d2
--- /dev/null
+++ b/_site/openstack/networking/v2/extensions/lbaas/monitors/requests_test.go
@@ -0,0 +1,288 @@
+package monitors
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ fake "github.com/rackspace/gophercloud/openstack/networking/v2/common"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestURLs(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ th.AssertEquals(t, th.Endpoint()+"v2.0/lb/health_monitors", rootURL(fake.ServiceClient()))
+}
+
+func TestList(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/lb/health_monitors", 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, `
+{
+ "health_monitors":[
+ {
+ "admin_state_up":true,
+ "tenant_id":"83657cfcdfe44cd5920adaf26c48ceea",
+ "delay":10,
+ "max_retries":1,
+ "timeout":1,
+ "type":"PING",
+ "id":"466c8345-28d8-4f84-a246-e04380b0461d"
+ },
+ {
+ "admin_state_up":true,
+ "tenant_id":"83657cfcdfe44cd5920adaf26c48ceea",
+ "delay":5,
+ "expected_codes":"200",
+ "max_retries":2,
+ "http_method":"GET",
+ "timeout":2,
+ "url_path":"/",
+ "type":"HTTP",
+ "id":"5d4b5228-33b0-4e60-b225-9b727c1a20e7"
+ }
+ ]
+}
+ `)
+ })
+
+ count := 0
+
+ List(fake.ServiceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractMonitors(page)
+ if err != nil {
+ t.Errorf("Failed to extract monitors: %v", err)
+ return false, err
+ }
+
+ expected := []Monitor{
+ Monitor{
+ AdminStateUp: true,
+ TenantID: "83657cfcdfe44cd5920adaf26c48ceea",
+ Delay: 10,
+ MaxRetries: 1,
+ Timeout: 1,
+ Type: "PING",
+ ID: "466c8345-28d8-4f84-a246-e04380b0461d",
+ },
+ Monitor{
+ AdminStateUp: true,
+ TenantID: "83657cfcdfe44cd5920adaf26c48ceea",
+ Delay: 5,
+ ExpectedCodes: "200",
+ MaxRetries: 2,
+ Timeout: 2,
+ URLPath: "/",
+ Type: "HTTP",
+ HTTPMethod: "GET",
+ ID: "5d4b5228-33b0-4e60-b225-9b727c1a20e7",
+ },
+ }
+
+ th.CheckDeepEquals(t, expected, actual)
+
+ return true, nil
+ })
+
+ if count != 1 {
+ t.Errorf("Expected 1 page, got %d", count)
+ }
+}
+
+func TestCreate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/lb/health_monitors", 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, `
+{
+ "health_monitor":{
+ "type":"HTTP",
+ "tenant_id":"453105b9-1754-413f-aab1-55f1af620750",
+ "delay":20,
+ "timeout":10,
+ "max_retries":5,
+ "url_path":"/check",
+ "expected_codes":"200-299"
+ }
+}
+ `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusCreated)
+
+ fmt.Fprintf(w, `
+{
+ "health_monitor":{
+ "id":"f3eeab00-8367-4524-b662-55e64d4cacb5",
+ "tenant_id":"453105b9-1754-413f-aab1-55f1af620750",
+ "type":"HTTP",
+ "delay":20,
+ "timeout":10,
+ "max_retries":5,
+ "http_method":"GET",
+ "url_path":"/check",
+ "expected_codes":"200-299",
+ "admin_state_up":true,
+ "status":"ACTIVE"
+ }
+}
+ `)
+ })
+
+ _, err := Create(fake.ServiceClient(), CreateOpts{
+ Type: "HTTP",
+ TenantID: "453105b9-1754-413f-aab1-55f1af620750",
+ Delay: 20,
+ Timeout: 10,
+ MaxRetries: 5,
+ URLPath: "/check",
+ ExpectedCodes: "200-299",
+ }).Extract()
+
+ th.AssertNoErr(t, err)
+}
+
+func TestRequiredCreateOpts(t *testing.T) {
+ res := Create(fake.ServiceClient(), CreateOpts{})
+ if res.Err == nil {
+ t.Fatalf("Expected error, got none")
+ }
+ res = Create(fake.ServiceClient(), CreateOpts{Type: TypeHTTP})
+ if res.Err == nil {
+ t.Fatalf("Expected error, got none")
+ }
+}
+
+func TestGet(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/lb/health_monitors/f3eeab00-8367-4524-b662-55e64d4cacb5", 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, `
+{
+ "health_monitor":{
+ "id":"f3eeab00-8367-4524-b662-55e64d4cacb5",
+ "tenant_id":"453105b9-1754-413f-aab1-55f1af620750",
+ "type":"HTTP",
+ "delay":20,
+ "timeout":10,
+ "max_retries":5,
+ "http_method":"GET",
+ "url_path":"/check",
+ "expected_codes":"200-299",
+ "admin_state_up":true,
+ "status":"ACTIVE"
+ }
+}
+ `)
+ })
+
+ hm, err := Get(fake.ServiceClient(), "f3eeab00-8367-4524-b662-55e64d4cacb5").Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, "f3eeab00-8367-4524-b662-55e64d4cacb5", hm.ID)
+ th.AssertEquals(t, "453105b9-1754-413f-aab1-55f1af620750", hm.TenantID)
+ th.AssertEquals(t, "HTTP", hm.Type)
+ th.AssertEquals(t, 20, hm.Delay)
+ th.AssertEquals(t, 10, hm.Timeout)
+ th.AssertEquals(t, 5, hm.MaxRetries)
+ th.AssertEquals(t, "GET", hm.HTTPMethod)
+ th.AssertEquals(t, "/check", hm.URLPath)
+ th.AssertEquals(t, "200-299", hm.ExpectedCodes)
+ th.AssertEquals(t, true, hm.AdminStateUp)
+ th.AssertEquals(t, "ACTIVE", hm.Status)
+}
+
+func TestUpdate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/lb/health_monitors/b05e44b5-81f9-4551-b474-711a722698f7", 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.TestHeader(t, r, "Accept", "application/json")
+ th.TestJSONRequest(t, r, `
+{
+ "health_monitor":{
+ "delay": 3,
+ "timeout": 20,
+ "max_retries": 10,
+ "url_path": "/another_check",
+ "expected_codes": "301"
+ }
+}
+ `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusAccepted)
+
+ fmt.Fprintf(w, `
+{
+ "health_monitor": {
+ "admin_state_up": true,
+ "tenant_id": "4fd44f30292945e481c7b8a0c8908869",
+ "delay": 3,
+ "max_retries": 10,
+ "http_method": "GET",
+ "timeout": 20,
+ "pools": [
+ {
+ "status": "PENDING_CREATE",
+ "status_description": null,
+ "pool_id": "6e55751f-6ad4-4e53-b8d4-02e442cd21df"
+ }
+ ],
+ "type": "PING",
+ "id": "b05e44b5-81f9-4551-b474-711a722698f7"
+ }
+}
+ `)
+ })
+
+ _, err := Update(fake.ServiceClient(), "b05e44b5-81f9-4551-b474-711a722698f7", UpdateOpts{
+ Delay: 3,
+ Timeout: 20,
+ MaxRetries: 10,
+ URLPath: "/another_check",
+ ExpectedCodes: "301",
+ }).Extract()
+
+ th.AssertNoErr(t, err)
+}
+
+func TestDelete(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/lb/health_monitors/b05e44b5-81f9-4551-b474-711a722698f7", 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)
+ })
+
+ res := Delete(fake.ServiceClient(), "b05e44b5-81f9-4551-b474-711a722698f7")
+ th.AssertNoErr(t, res.Err)
+}
diff --git a/_site/openstack/networking/v2/extensions/lbaas/monitors/results.go b/_site/openstack/networking/v2/extensions/lbaas/monitors/results.go
new file mode 100644
index 0000000..656ab1d
--- /dev/null
+++ b/_site/openstack/networking/v2/extensions/lbaas/monitors/results.go
@@ -0,0 +1,145 @@
+package monitors
+
+import (
+ "github.com/mitchellh/mapstructure"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// Monitor represents a load balancer health monitor. A health monitor is used
+// to determine whether or not back-end members of the VIP's pool are usable
+// for processing a request. A pool can have several health monitors associated
+// with it. There are different types of health monitors supported:
+//
+// PING: used to ping the members using ICMP.
+// TCP: used to connect to the members using TCP.
+// HTTP: used to send an HTTP request to the member.
+// HTTPS: used to send a secure HTTP request to the member.
+//
+// When a pool has several monitors associated with it, each member of the pool
+// is monitored by all these monitors. If any monitor declares the member as
+// unhealthy, then the member status is changed to INACTIVE and the member
+// won't participate in its pool's load balancing. In other words, ALL monitors
+// must declare the member to be healthy for it to stay ACTIVE.
+type Monitor struct {
+ // The unique ID for the VIP.
+ ID string
+
+ // Owner of the VIP. Only an administrative user can specify a tenant ID
+ // other than its own.
+ TenantID string `json:"tenant_id" mapstructure:"tenant_id"`
+
+ // The type of probe sent by the load balancer to verify the member state,
+ // which is PING, TCP, HTTP, or HTTPS.
+ Type string
+
+ // The time, in seconds, between sending probes to members.
+ Delay int
+
+ // The maximum number of seconds for a monitor to wait for a connection to be
+ // established before it times out. This value must be less than the delay value.
+ Timeout int
+
+ // Number of allowed connection failures before changing the status of the
+ // member to INACTIVE. A valid value is from 1 to 10.
+ MaxRetries int `json:"max_retries" mapstructure:"max_retries"`
+
+ // The HTTP method that the monitor uses for requests.
+ HTTPMethod string `json:"http_method" mapstructure:"http_method"`
+
+ // The HTTP path of the request sent by the monitor to test the health of a
+ // member. Must be a string beginning with a forward slash (/).
+ URLPath string `json:"url_path" mapstructure:"url_path"`
+
+ // Expected HTTP codes for a passing HTTP(S) monitor.
+ ExpectedCodes string `json:"expected_codes" mapstructure:"expected_codes"`
+
+ // The administrative state of the health monitor, which is up (true) or down (false).
+ AdminStateUp bool `json:"admin_state_up" mapstructure:"admin_state_up"`
+
+ // The status of the health monitor. Indicates whether the health monitor is
+ // operational.
+ Status string
+}
+
+// MonitorPage is the page returned by a pager when traversing over a
+// collection of health monitors.
+type MonitorPage struct {
+ pagination.LinkedPageBase
+}
+
+// NextPageURL is invoked when a paginated collection of monitors has reached
+// the end of a page and the pager seeks to traverse over a new one. In order
+// to do this, it needs to construct the next page's URL.
+func (p MonitorPage) NextPageURL() (string, error) {
+ type resp struct {
+ Links []gophercloud.Link `mapstructure:"health_monitors_links"`
+ }
+
+ var r resp
+ err := mapstructure.Decode(p.Body, &r)
+ if err != nil {
+ return "", err
+ }
+
+ return gophercloud.ExtractNextURL(r.Links)
+}
+
+// IsEmpty checks whether a PoolPage struct is empty.
+func (p MonitorPage) IsEmpty() (bool, error) {
+ is, err := ExtractMonitors(p)
+ if err != nil {
+ return true, nil
+ }
+ return len(is) == 0, nil
+}
+
+// ExtractMonitors accepts a Page struct, specifically a MonitorPage struct,
+// and extracts the elements into a slice of Monitor structs. In other words,
+// a generic collection is mapped into a relevant slice.
+func ExtractMonitors(page pagination.Page) ([]Monitor, error) {
+ var resp struct {
+ Monitors []Monitor `mapstructure:"health_monitors" json:"health_monitors"`
+ }
+
+ err := mapstructure.Decode(page.(MonitorPage).Body, &resp)
+
+ return resp.Monitors, err
+}
+
+type commonResult struct {
+ gophercloud.CommonResult
+}
+
+// Extract is a function that accepts a result and extracts a monitor.
+func (r commonResult) Extract() (*Monitor, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var res struct {
+ Monitor *Monitor `json:"health_monitor" mapstructure:"health_monitor"`
+ }
+
+ err := mapstructure.Decode(r.Resp, &res)
+
+ return res.Monitor, err
+}
+
+// CreateResult represents the result of a create operation.
+type CreateResult struct {
+ commonResult
+}
+
+// GetResult represents the result of a get operation.
+type GetResult struct {
+ commonResult
+}
+
+// UpdateResult represents the result of an update operation.
+type UpdateResult struct {
+ commonResult
+}
+
+// DeleteResult represents the result of a delete operation.
+type DeleteResult commonResult
diff --git a/_site/openstack/networking/v2/extensions/lbaas/monitors/urls.go b/_site/openstack/networking/v2/extensions/lbaas/monitors/urls.go
new file mode 100644
index 0000000..46e84bb
--- /dev/null
+++ b/_site/openstack/networking/v2/extensions/lbaas/monitors/urls.go
@@ -0,0 +1,16 @@
+package monitors
+
+import "github.com/rackspace/gophercloud"
+
+const (
+ rootPath = "lb"
+ resourcePath = "health_monitors"
+)
+
+func rootURL(c *gophercloud.ServiceClient) string {
+ return c.ServiceURL(rootPath, resourcePath)
+}
+
+func resourceURL(c *gophercloud.ServiceClient, id string) string {
+ return c.ServiceURL(rootPath, resourcePath, id)
+}
diff --git a/_site/openstack/networking/v2/extensions/lbaas/pools/requests.go b/_site/openstack/networking/v2/extensions/lbaas/pools/requests.go
new file mode 100644
index 0000000..2688350
--- /dev/null
+++ b/_site/openstack/networking/v2/extensions/lbaas/pools/requests.go
@@ -0,0 +1,205 @@
+package pools
+
+import (
+ "github.com/racker/perigee"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// ListOpts allows the filtering and sorting of paginated collections through
+// the API. Filtering is achieved by passing in struct field values that map to
+// the floating IP attributes you want to see returned. SortKey allows you to
+// sort by a particular network attribute. SortDir sets the direction, and is
+// either `asc' or `desc'. Marker and Limit are used for pagination.
+type ListOpts struct {
+ Status string `q:"status"`
+ LBMethod string `q:"lb_method"`
+ Protocol string `q:"protocol"`
+ SubnetID string `q:"subnet_id"`
+ TenantID string `q:"tenant_id"`
+ AdminStateUp *bool `q:"admin_state_up"`
+ Name string `q:"name"`
+ ID string `q:"id"`
+ VIPID string `q:"vip_id"`
+ Limit int `q:"limit"`
+ Marker string `q:"marker"`
+ SortKey string `q:"sort_key"`
+ SortDir string `q:"sort_dir"`
+}
+
+// List returns a Pager which allows you to iterate over a collection of
+// pools. It accepts a ListOpts struct, which allows you to filter and sort
+// the returned collection for greater efficiency.
+//
+// Default policy settings return only those pools that are owned by the
+// tenant who submits the request, unless an admin user submits the request.
+func List(c *gophercloud.ServiceClient, opts ListOpts) pagination.Pager {
+ q, err := gophercloud.BuildQueryString(&opts)
+ if err != nil {
+ return pagination.Pager{Err: err}
+ }
+ u := rootURL(c) + q.String()
+ return pagination.NewPager(c, u, func(r pagination.LastHTTPResponse) pagination.Page {
+ return PoolPage{pagination.LinkedPageBase{LastHTTPResponse: r}}
+ })
+}
+
+// Supported attributes for create/update operations.
+const (
+ LBMethodRoundRobin = "ROUND_ROBIN"
+ LBMethodLeastConnections = "LEAST_CONNECTIONS"
+
+ ProtocolTCP = "TCP"
+ ProtocolHTTP = "HTTP"
+ ProtocolHTTPS = "HTTPS"
+)
+
+// CreateOpts contains all the values needed to create a new pool.
+type CreateOpts struct {
+ // Only required if the caller has an admin role and wants to create a pool
+ // for another tenant.
+ TenantID string
+
+ // Required. Name of the pool.
+ Name string
+
+ // Required. The protocol used by the pool members, you can use either
+ // ProtocolTCP, ProtocolHTTP, or ProtocolHTTPS.
+ Protocol string
+
+ // The network on which the members of the pool will be located. Only members
+ // that are on this network can be added to the pool.
+ SubnetID string
+
+ // The algorithm used to distribute load between the members of the pool. The
+ // current specification supports LBMethodRoundRobin and
+ // LBMethodLeastConnections as valid values for this attribute.
+ LBMethod string
+}
+
+// Create accepts a CreateOpts struct and uses the values to create a new
+// load balancer pool.
+func Create(c *gophercloud.ServiceClient, opts CreateOpts) CreateResult {
+ type pool struct {
+ Name string `json:"name"`
+ TenantID string `json:"tenant_id,omitempty"`
+ Protocol string `json:"protocol"`
+ SubnetID string `json:"subnet_id"`
+ LBMethod string `json:"lb_method"`
+ }
+ type request struct {
+ Pool pool `json:"pool"`
+ }
+
+ reqBody := request{Pool: pool{
+ Name: opts.Name,
+ TenantID: opts.TenantID,
+ Protocol: opts.Protocol,
+ SubnetID: opts.SubnetID,
+ LBMethod: opts.LBMethod,
+ }}
+
+ var res CreateResult
+ _, res.Err = perigee.Request("POST", rootURL(c), perigee.Options{
+ MoreHeaders: c.Provider.AuthenticatedHeaders(),
+ ReqBody: &reqBody,
+ Results: &res.Resp,
+ OkCodes: []int{201},
+ })
+ return res
+}
+
+// Get retrieves a particular pool based on its unique ID.
+func Get(c *gophercloud.ServiceClient, id string) GetResult {
+ var res GetResult
+ _, res.Err = perigee.Request("GET", resourceURL(c, id), perigee.Options{
+ MoreHeaders: c.Provider.AuthenticatedHeaders(),
+ Results: &res.Resp,
+ OkCodes: []int{200},
+ })
+ return res
+}
+
+// UpdateOpts contains the values used when updating a pool.
+type UpdateOpts struct {
+ // Required. Name of the pool.
+ Name string
+
+ // The algorithm used to distribute load between the members of the pool. The
+ // current specification supports LBMethodRoundRobin and
+ // LBMethodLeastConnections as valid values for this attribute.
+ LBMethod string
+}
+
+// Update allows pools to be updated.
+func Update(c *gophercloud.ServiceClient, id string, opts UpdateOpts) UpdateResult {
+ type pool struct {
+ Name string `json:"name,"`
+ LBMethod string `json:"lb_method"`
+ }
+ type request struct {
+ Pool pool `json:"pool"`
+ }
+
+ reqBody := request{Pool: pool{
+ Name: opts.Name,
+ LBMethod: opts.LBMethod,
+ }}
+
+ // Send request to API
+ var res UpdateResult
+ _, res.Err = perigee.Request("PUT", resourceURL(c, id), perigee.Options{
+ MoreHeaders: c.Provider.AuthenticatedHeaders(),
+ ReqBody: &reqBody,
+ Results: &res.Resp,
+ OkCodes: []int{200},
+ })
+ return res
+}
+
+// Delete will permanently delete a particular pool based on its unique ID.
+func Delete(c *gophercloud.ServiceClient, id string) DeleteResult {
+ var res DeleteResult
+ _, res.Err = perigee.Request("DELETE", resourceURL(c, id), perigee.Options{
+ MoreHeaders: c.Provider.AuthenticatedHeaders(),
+ OkCodes: []int{204},
+ })
+ return res
+}
+
+// AssociateMonitor will associate a health monitor with a particular pool.
+// Once associated, the health monitor will start monitoring the members of the
+// pool and will deactivate these members if they are deemed unhealthy. A
+// member can be deactivated (status set to INACTIVE) if any of health monitors
+// finds it unhealthy.
+func AssociateMonitor(c *gophercloud.ServiceClient, poolID, monitorID string) AssociateResult {
+ type hm struct {
+ ID string `json:"id"`
+ }
+ type request struct {
+ Monitor hm `json:"health_monitor"`
+ }
+
+ reqBody := request{hm{ID: monitorID}}
+
+ var res AssociateResult
+ _, res.Err = perigee.Request("POST", associateURL(c, poolID), perigee.Options{
+ MoreHeaders: c.Provider.AuthenticatedHeaders(),
+ ReqBody: &reqBody,
+ Results: &res.Resp,
+ OkCodes: []int{201},
+ })
+ return res
+}
+
+// DisassociateMonitor will disassociate a health monitor with a particular
+// pool. When dissociation is successful, the health monitor will no longer
+// check for the health of the members of the pool.
+func DisassociateMonitor(c *gophercloud.ServiceClient, poolID, monitorID string) AssociateResult {
+ var res AssociateResult
+ _, res.Err = perigee.Request("DELETE", disassociateURL(c, poolID, monitorID), perigee.Options{
+ MoreHeaders: c.Provider.AuthenticatedHeaders(),
+ OkCodes: []int{204},
+ })
+ return res
+}
diff --git a/_site/openstack/networking/v2/extensions/lbaas/pools/requests_test.go b/_site/openstack/networking/v2/extensions/lbaas/pools/requests_test.go
new file mode 100644
index 0000000..6da29a6
--- /dev/null
+++ b/_site/openstack/networking/v2/extensions/lbaas/pools/requests_test.go
@@ -0,0 +1,317 @@
+package pools
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ fake "github.com/rackspace/gophercloud/openstack/networking/v2/common"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestURLs(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.AssertEquals(t, th.Endpoint()+"v2.0/lb/pools", rootURL(fake.ServiceClient()))
+}
+
+func TestList(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/lb/pools", 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, `
+{
+ "pools":[
+ {
+ "status":"ACTIVE",
+ "lb_method":"ROUND_ROBIN",
+ "protocol":"HTTP",
+ "description":"",
+ "health_monitors":[
+ "466c8345-28d8-4f84-a246-e04380b0461d",
+ "5d4b5228-33b0-4e60-b225-9b727c1a20e7"
+ ],
+ "members":[
+ "701b531b-111a-4f21-ad85-4795b7b12af6",
+ "beb53b4d-230b-4abd-8118-575b8fa006ef"
+ ],
+ "status_description": null,
+ "id":"72741b06-df4d-4715-b142-276b6bce75ab",
+ "vip_id":"4ec89087-d057-4e2c-911f-60a3b47ee304",
+ "name":"app_pool",
+ "admin_state_up":true,
+ "subnet_id":"8032909d-47a1-4715-90af-5153ffe39861",
+ "tenant_id":"83657cfcdfe44cd5920adaf26c48ceea",
+ "health_monitors_status": [],
+ "provider": "haproxy"
+ }
+ ]
+}
+ `)
+ })
+
+ count := 0
+
+ List(fake.ServiceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractPools(page)
+ if err != nil {
+ t.Errorf("Failed to extract pools: %v", err)
+ return false, err
+ }
+
+ expected := []Pool{
+ Pool{
+ Status: "ACTIVE",
+ LBMethod: "ROUND_ROBIN",
+ Protocol: "HTTP",
+ Description: "",
+ MonitorIDs: []string{
+ "466c8345-28d8-4f84-a246-e04380b0461d",
+ "5d4b5228-33b0-4e60-b225-9b727c1a20e7",
+ },
+ SubnetID: "8032909d-47a1-4715-90af-5153ffe39861",
+ TenantID: "83657cfcdfe44cd5920adaf26c48ceea",
+ AdminStateUp: true,
+ Name: "app_pool",
+ MemberIDs: []string{
+ "701b531b-111a-4f21-ad85-4795b7b12af6",
+ "beb53b4d-230b-4abd-8118-575b8fa006ef",
+ },
+ ID: "72741b06-df4d-4715-b142-276b6bce75ab",
+ VIPID: "4ec89087-d057-4e2c-911f-60a3b47ee304",
+ Provider: "haproxy",
+ },
+ }
+
+ th.CheckDeepEquals(t, expected, actual)
+
+ return true, nil
+ })
+
+ if count != 1 {
+ t.Errorf("Expected 1 page, got %d", count)
+ }
+}
+
+func TestCreate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/lb/pools", 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, `
+{
+ "pool": {
+ "lb_method": "ROUND_ROBIN",
+ "protocol": "HTTP",
+ "name": "Example pool",
+ "subnet_id": "1981f108-3c48-48d2-b908-30f7d28532c9",
+ "tenant_id": "2ffc6e22aae24e4795f87155d24c896f"
+ }
+}
+ `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusCreated)
+
+ fmt.Fprintf(w, `
+{
+ "pool": {
+ "status": "PENDING_CREATE",
+ "lb_method": "ROUND_ROBIN",
+ "protocol": "HTTP",
+ "description": "",
+ "health_monitors": [],
+ "members": [],
+ "status_description": null,
+ "id": "69055154-f603-4a28-8951-7cc2d9e54a9a",
+ "vip_id": null,
+ "name": "Example pool",
+ "admin_state_up": true,
+ "subnet_id": "1981f108-3c48-48d2-b908-30f7d28532c9",
+ "tenant_id": "2ffc6e22aae24e4795f87155d24c896f",
+ "health_monitors_status": []
+ }
+}
+ `)
+ })
+
+ options := CreateOpts{
+ LBMethod: LBMethodRoundRobin,
+ Protocol: "HTTP",
+ Name: "Example pool",
+ SubnetID: "1981f108-3c48-48d2-b908-30f7d28532c9",
+ TenantID: "2ffc6e22aae24e4795f87155d24c896f",
+ }
+ p, err := Create(fake.ServiceClient(), options).Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, "PENDING_CREATE", p.Status)
+ th.AssertEquals(t, "ROUND_ROBIN", p.LBMethod)
+ th.AssertEquals(t, "HTTP", p.Protocol)
+ th.AssertEquals(t, "", p.Description)
+ th.AssertDeepEquals(t, []string{}, p.MonitorIDs)
+ th.AssertDeepEquals(t, []string{}, p.MemberIDs)
+ th.AssertEquals(t, "69055154-f603-4a28-8951-7cc2d9e54a9a", p.ID)
+ th.AssertEquals(t, "Example pool", p.Name)
+ th.AssertEquals(t, "1981f108-3c48-48d2-b908-30f7d28532c9", p.SubnetID)
+ th.AssertEquals(t, "2ffc6e22aae24e4795f87155d24c896f", p.TenantID)
+}
+
+func TestGet(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/lb/pools/332abe93-f488-41ba-870b-2ac66be7f853", 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, `
+{
+ "pool":{
+ "id":"332abe93-f488-41ba-870b-2ac66be7f853",
+ "tenant_id":"19eaa775-cf5d-49bc-902e-2f85f668d995",
+ "name":"Example pool",
+ "description":"",
+ "protocol":"tcp",
+ "lb_algorithm":"ROUND_ROBIN",
+ "session_persistence":{
+ },
+ "healthmonitor_id":null,
+ "members":[
+ ],
+ "admin_state_up":true,
+ "status":"ACTIVE"
+ }
+}
+ `)
+ })
+
+ n, err := Get(fake.ServiceClient(), "332abe93-f488-41ba-870b-2ac66be7f853").Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, n.ID, "332abe93-f488-41ba-870b-2ac66be7f853")
+}
+
+func TestUpdate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/lb/pools/332abe93-f488-41ba-870b-2ac66be7f853", 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.TestHeader(t, r, "Accept", "application/json")
+ th.TestJSONRequest(t, r, `
+{
+ "pool":{
+ "name":"SuperPool",
+ "lb_method": "LEAST_CONNECTIONS"
+ }
+}
+ `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, `
+{
+ "pool":{
+ "status":"PENDING_UPDATE",
+ "lb_method":"LEAST_CONNECTIONS",
+ "protocol":"TCP",
+ "description":"",
+ "health_monitors":[
+
+ ],
+ "subnet_id":"8032909d-47a1-4715-90af-5153ffe39861",
+ "tenant_id":"83657cfcdfe44cd5920adaf26c48ceea",
+ "admin_state_up":true,
+ "name":"SuperPool",
+ "members":[
+
+ ],
+ "id":"61b1f87a-7a21-4ad3-9dda-7f81d249944f",
+ "vip_id":null
+ }
+}
+ `)
+ })
+
+ options := UpdateOpts{Name: "SuperPool", LBMethod: LBMethodLeastConnections}
+
+ n, err := Update(fake.ServiceClient(), "332abe93-f488-41ba-870b-2ac66be7f853", options).Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, "SuperPool", n.Name)
+ th.AssertDeepEquals(t, "LEAST_CONNECTIONS", n.LBMethod)
+}
+
+func TestDelete(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/lb/pools/332abe93-f488-41ba-870b-2ac66be7f853", 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)
+ })
+
+ res := Delete(fake.ServiceClient(), "332abe93-f488-41ba-870b-2ac66be7f853")
+ th.AssertNoErr(t, res.Err)
+}
+
+func TestAssociateHealthMonitor(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/lb/pools/332abe93-f488-41ba-870b-2ac66be7f853/health_monitors", 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, `
+{
+ "health_monitor":{
+ "id":"b624decf-d5d3-4c66-9a3d-f047e7786181"
+ }
+}
+ `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusCreated)
+ })
+
+ _, err := AssociateMonitor(fake.ServiceClient(), "332abe93-f488-41ba-870b-2ac66be7f853", "b624decf-d5d3-4c66-9a3d-f047e7786181").Extract()
+ th.AssertNoErr(t, err)
+}
+
+func TestDisassociateHealthMonitor(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/lb/pools/332abe93-f488-41ba-870b-2ac66be7f853/health_monitors/b624decf-d5d3-4c66-9a3d-f047e7786181", 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)
+ })
+
+ res := DisassociateMonitor(fake.ServiceClient(), "332abe93-f488-41ba-870b-2ac66be7f853", "b624decf-d5d3-4c66-9a3d-f047e7786181")
+ th.AssertNoErr(t, res.Err)
+}
diff --git a/_site/openstack/networking/v2/extensions/lbaas/pools/results.go b/_site/openstack/networking/v2/extensions/lbaas/pools/results.go
new file mode 100644
index 0000000..4233176
--- /dev/null
+++ b/_site/openstack/networking/v2/extensions/lbaas/pools/results.go
@@ -0,0 +1,144 @@
+package pools
+
+import (
+ "github.com/mitchellh/mapstructure"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// Pool represents a logical set of devices, such as web servers, that you
+// group together to receive and process traffic. The load balancing function
+// chooses a member of the pool according to the configured load balancing
+// method to handle the new requests or connections received on the VIP address.
+// There is only one pool per virtual IP.
+type Pool struct {
+ // The status of the pool. Indicates whether the pool is operational.
+ Status string
+
+ // The load-balancer algorithm, which is round-robin, least-connections, and
+ // so on. This value, which must be supported, is dependent on the provider.
+ // Round-robin must be supported.
+ LBMethod string `json:"lb_method" mapstructure:"lb_method"`
+
+ // The protocol of the pool, which is TCP, HTTP, or HTTPS.
+ Protocol string
+
+ // Description for the pool.
+ Description string
+
+ // The IDs of associated monitors which check the health of the pool members.
+ MonitorIDs []string `json:"health_monitors" mapstructure:"health_monitors"`
+
+ // The network on which the members of the pool will be located. Only members
+ // that are on this network can be added to the pool.
+ SubnetID string `json:"subnet_id" mapstructure:"subnet_id"`
+
+ // Owner of the pool. Only an administrative user can specify a tenant ID
+ // other than its own.
+ TenantID string `json:"tenant_id" mapstructure:"tenant_id"`
+
+ // The administrative state of the pool, which is up (true) or down (false).
+ AdminStateUp bool `json:"admin_state_up" mapstructure:"admin_state_up"`
+
+ // Pool name. Does not have to be unique.
+ Name string
+
+ // List of member IDs that belong to the pool.
+ MemberIDs []string `json:"members" mapstructure:"members"`
+
+ // The unique ID for the pool.
+ ID string
+
+ // The ID of the virtual IP associated with this pool
+ VIPID string `json:"vip_id" mapstructure:"vip_id"`
+
+ // The provider
+ Provider string
+}
+
+// PoolPage is the page returned by a pager when traversing over a
+// collection of pools.
+type PoolPage struct {
+ pagination.LinkedPageBase
+}
+
+// NextPageURL is invoked when a paginated collection of pools has reached
+// the end of a page and the pager seeks to traverse over a new one. In order
+// to do this, it needs to construct the next page's URL.
+func (p PoolPage) NextPageURL() (string, error) {
+ type resp struct {
+ Links []gophercloud.Link `mapstructure:"pools_links"`
+ }
+
+ var r resp
+ err := mapstructure.Decode(p.Body, &r)
+ if err != nil {
+ return "", err
+ }
+
+ return gophercloud.ExtractNextURL(r.Links)
+}
+
+// IsEmpty checks whether a PoolPage struct is empty.
+func (p PoolPage) IsEmpty() (bool, error) {
+ is, err := ExtractPools(p)
+ if err != nil {
+ return true, nil
+ }
+ return len(is) == 0, nil
+}
+
+// ExtractPools accepts a Page struct, specifically a RouterPage struct,
+// and extracts the elements into a slice of Router structs. In other words,
+// a generic collection is mapped into a relevant slice.
+func ExtractPools(page pagination.Page) ([]Pool, error) {
+ var resp struct {
+ Pools []Pool `mapstructure:"pools" json:"pools"`
+ }
+
+ err := mapstructure.Decode(page.(PoolPage).Body, &resp)
+
+ return resp.Pools, err
+}
+
+type commonResult struct {
+ gophercloud.CommonResult
+}
+
+// Extract is a function that accepts a result and extracts a router.
+func (r commonResult) Extract() (*Pool, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var res struct {
+ Pool *Pool `json:"pool"`
+ }
+
+ err := mapstructure.Decode(r.Resp, &res)
+
+ return res.Pool, err
+}
+
+// CreateResult represents the result of a create operation.
+type CreateResult struct {
+ commonResult
+}
+
+// GetResult represents the result of a get operation.
+type GetResult struct {
+ commonResult
+}
+
+// UpdateResult represents the result of an update operation.
+type UpdateResult struct {
+ commonResult
+}
+
+// DeleteResult represents the result of a delete operation.
+type DeleteResult commonResult
+
+// AssociateResult represents the result of an association operation.
+type AssociateResult struct {
+ commonResult
+}
diff --git a/_site/openstack/networking/v2/extensions/lbaas/pools/urls.go b/_site/openstack/networking/v2/extensions/lbaas/pools/urls.go
new file mode 100644
index 0000000..6cd15b0
--- /dev/null
+++ b/_site/openstack/networking/v2/extensions/lbaas/pools/urls.go
@@ -0,0 +1,25 @@
+package pools
+
+import "github.com/rackspace/gophercloud"
+
+const (
+ rootPath = "lb"
+ resourcePath = "pools"
+ monitorPath = "health_monitors"
+)
+
+func rootURL(c *gophercloud.ServiceClient) string {
+ return c.ServiceURL(rootPath, resourcePath)
+}
+
+func resourceURL(c *gophercloud.ServiceClient, id string) string {
+ return c.ServiceURL(rootPath, resourcePath, id)
+}
+
+func associateURL(c *gophercloud.ServiceClient, id string) string {
+ return c.ServiceURL(rootPath, resourcePath, id, monitorPath)
+}
+
+func disassociateURL(c *gophercloud.ServiceClient, poolID, monitorID string) string {
+ return c.ServiceURL(rootPath, resourcePath, poolID, monitorPath, monitorID)
+}
diff --git a/_site/openstack/networking/v2/extensions/lbaas/vips/requests.go b/_site/openstack/networking/v2/extensions/lbaas/vips/requests.go
new file mode 100644
index 0000000..ce4d9e4
--- /dev/null
+++ b/_site/openstack/networking/v2/extensions/lbaas/vips/requests.go
@@ -0,0 +1,273 @@
+package vips
+
+import (
+ "fmt"
+
+ "github.com/racker/perigee"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// AdminState gives users a solid type to work with for create and update
+// operations. It is recommended that users use the `Up` and `Down` enums.
+type AdminState *bool
+
+// Convenience vars for AdminStateUp values.
+var (
+ iTrue = true
+ iFalse = false
+
+ Up AdminState = &iTrue
+ Down AdminState = &iFalse
+)
+
+// ListOpts allows the filtering and sorting of paginated collections through
+// the API. Filtering is achieved by passing in struct field values that map to
+// the floating IP attributes you want to see returned. SortKey allows you to
+// sort by a particular network attribute. SortDir sets the direction, and is
+// either `asc' or `desc'. Marker and Limit are used for pagination.
+type ListOpts struct {
+ ID string `q:"id"`
+ Name string `q:"name"`
+ AdminStateUp *bool `q:"admin_state_up"`
+ Status string `q:"status"`
+ TenantID string `q:"tenant_id"`
+ SubnetID string `q:"subnet_id"`
+ Address string `q:"address"`
+ PortID string `q:"port_id"`
+ Protocol string `q:"protocol"`
+ ProtocolPort int `q:"protocol_port"`
+ ConnectionLimit int `q:"connection_limit"`
+ Limit int `q:"limit"`
+ Marker string `q:"marker"`
+ SortKey string `q:"sort_key"`
+ SortDir string `q:"sort_dir"`
+}
+
+// List returns a Pager which allows you to iterate over a collection of
+// routers. It accepts a ListOpts struct, which allows you to filter and sort
+// the returned collection for greater efficiency.
+//
+// Default policy settings return only those routers that are owned by the
+// tenant who submits the request, unless an admin user submits the request.
+func List(c *gophercloud.ServiceClient, opts ListOpts) pagination.Pager {
+ q, err := gophercloud.BuildQueryString(&opts)
+ if err != nil {
+ return pagination.Pager{Err: err}
+ }
+ u := rootURL(c) + q.String()
+ return pagination.NewPager(c, u, func(r pagination.LastHTTPResponse) pagination.Page {
+ return VIPPage{pagination.LinkedPageBase{LastHTTPResponse: r}}
+ })
+}
+
+var (
+ errNameRequired = fmt.Errorf("Name is required")
+ errSubnetIDRequried = fmt.Errorf("SubnetID is required")
+ errProtocolRequired = fmt.Errorf("Protocol is required")
+ errProtocolPortRequired = fmt.Errorf("Protocol port is required")
+ errPoolIDRequired = fmt.Errorf("PoolID is required")
+)
+
+// CreateOpts contains all the values needed to create a new virtual IP.
+type CreateOpts struct {
+ // Required. Human-readable name for the VIP. Does not have to be unique.
+ Name string
+
+ // Required. The network on which to allocate the VIP's address. A tenant can
+ // only create VIPs on networks authorized by policy (e.g. networks that
+ // belong to them or networks that are shared).
+ SubnetID string
+
+ // Required. The protocol - can either be TCP, HTTP or HTTPS.
+ Protocol string
+
+ // Required. The port on which to listen for client traffic.
+ ProtocolPort int
+
+ // Required. The ID of the pool with which the VIP is associated.
+ PoolID string
+
+ // Required for admins. Indicates the owner of the VIP.
+ TenantID string
+
+ // Optional. The IP address of the VIP.
+ Address string
+
+ // Optional. Human-readable description for the VIP.
+ Description string
+
+ // Optional. Omit this field to prevent session persistence.
+ Persistence *SessionPersistence
+
+ // Optional. The maximum number of connections allowed for the VIP.
+ ConnLimit *int
+
+ // Optional. The administrative state of the VIP. A valid value is true (UP)
+ // or false (DOWN).
+ AdminStateUp *bool
+}
+
+// Create is an operation which provisions a new virtual IP based on the
+// configuration defined in the CreateOpts struct. Once the request is
+// validated and progress has started on the provisioning process, a
+// CreateResult will be returned.
+//
+// Please note that the PoolID should refer to a pool that is not already
+// associated with another vip. If the pool is already used by another vip,
+// then the operation will fail with a 409 Conflict error will be returned.
+//
+// Users with an admin role can create VIPs on behalf of other tenants by
+// specifying a TenantID attribute different than their own.
+func Create(c *gophercloud.ServiceClient, opts CreateOpts) CreateResult {
+ var res CreateResult
+
+ // Validate required opts
+ if opts.Name == "" {
+ res.Err = errNameRequired
+ return res
+ }
+ if opts.SubnetID == "" {
+ res.Err = errSubnetIDRequried
+ return res
+ }
+ if opts.Protocol == "" {
+ res.Err = errProtocolRequired
+ return res
+ }
+ if opts.ProtocolPort == 0 {
+ res.Err = errProtocolPortRequired
+ return res
+ }
+ if opts.PoolID == "" {
+ res.Err = errPoolIDRequired
+ return res
+ }
+
+ type vip struct {
+ Name string `json:"name"`
+ SubnetID string `json:"subnet_id"`
+ Protocol string `json:"protocol"`
+ ProtocolPort int `json:"protocol_port"`
+ PoolID string `json:"pool_id"`
+ Description *string `json:"description,omitempty"`
+ TenantID *string `json:"tenant_id,omitempty"`
+ Address *string `json:"address,omitempty"`
+ Persistence *SessionPersistence `json:"session_persistence,omitempty"`
+ ConnLimit *int `json:"connection_limit,omitempty"`
+ AdminStateUp *bool `json:"admin_state_up,omitempty"`
+ }
+
+ type request struct {
+ VirtualIP vip `json:"vip"`
+ }
+
+ reqBody := request{VirtualIP: vip{
+ Name: opts.Name,
+ SubnetID: opts.SubnetID,
+ Protocol: opts.Protocol,
+ ProtocolPort: opts.ProtocolPort,
+ PoolID: opts.PoolID,
+ Description: gophercloud.MaybeString(opts.Description),
+ TenantID: gophercloud.MaybeString(opts.TenantID),
+ Address: gophercloud.MaybeString(opts.Address),
+ ConnLimit: opts.ConnLimit,
+ AdminStateUp: opts.AdminStateUp,
+ }}
+
+ if opts.Persistence != nil {
+ reqBody.VirtualIP.Persistence = opts.Persistence
+ }
+
+ _, res.Err = perigee.Request("POST", rootURL(c), perigee.Options{
+ MoreHeaders: c.Provider.AuthenticatedHeaders(),
+ ReqBody: &reqBody,
+ Results: &res.Resp,
+ OkCodes: []int{201},
+ })
+
+ return res
+}
+
+// Get retrieves a particular virtual IP based on its unique ID.
+func Get(c *gophercloud.ServiceClient, id string) GetResult {
+ var res GetResult
+ _, res.Err = perigee.Request("GET", resourceURL(c, id), perigee.Options{
+ MoreHeaders: c.Provider.AuthenticatedHeaders(),
+ Results: &res.Resp,
+ OkCodes: []int{200},
+ })
+ return res
+}
+
+// UpdateOpts contains all the values needed to update an existing virtual IP.
+// Attributes not listed here but appear in CreateOpts are immutable and cannot
+// be updated.
+type UpdateOpts struct {
+ // Human-readable name for the VIP. Does not have to be unique.
+ Name string
+
+ // Required. The ID of the pool with which the VIP is associated.
+ PoolID string
+
+ // Optional. Human-readable description for the VIP.
+ Description string
+
+ // Optional. Omit this field to prevent session persistence.
+ Persistence *SessionPersistence
+
+ // Optional. The maximum number of connections allowed for the VIP.
+ ConnLimit *int
+
+ // Optional. The administrative state of the VIP. A valid value is true (UP)
+ // or false (DOWN).
+ AdminStateUp *bool
+}
+
+// Update is an operation which modifies the attributes of the specified VIP.
+func Update(c *gophercloud.ServiceClient, id string, opts UpdateOpts) UpdateResult {
+ type vip struct {
+ Name string `json:"name,omitempty"`
+ PoolID string `json:"pool_id,omitempty"`
+ Description *string `json:"description,omitempty"`
+ Persistence *SessionPersistence `json:"session_persistence,omitempty"`
+ ConnLimit *int `json:"connection_limit,omitempty"`
+ AdminStateUp *bool `json:"admin_state_up,omitempty"`
+ }
+
+ type request struct {
+ VirtualIP vip `json:"vip"`
+ }
+
+ reqBody := request{VirtualIP: vip{
+ Name: opts.Name,
+ PoolID: opts.PoolID,
+ Description: gophercloud.MaybeString(opts.Description),
+ ConnLimit: opts.ConnLimit,
+ AdminStateUp: opts.AdminStateUp,
+ }}
+
+ if opts.Persistence != nil {
+ reqBody.VirtualIP.Persistence = opts.Persistence
+ }
+
+ var res UpdateResult
+ _, res.Err = perigee.Request("PUT", resourceURL(c, id), perigee.Options{
+ MoreHeaders: c.Provider.AuthenticatedHeaders(),
+ ReqBody: &reqBody,
+ Results: &res.Resp,
+ OkCodes: []int{200, 202},
+ })
+
+ return res
+}
+
+// Delete will permanently delete a particular virtual IP based on its unique ID.
+func Delete(c *gophercloud.ServiceClient, id string) DeleteResult {
+ var res DeleteResult
+ _, res.Err = perigee.Request("DELETE", resourceURL(c, id), perigee.Options{
+ MoreHeaders: c.Provider.AuthenticatedHeaders(),
+ OkCodes: []int{204},
+ })
+ return res
+}
diff --git a/_site/openstack/networking/v2/extensions/lbaas/vips/requests_test.go b/_site/openstack/networking/v2/extensions/lbaas/vips/requests_test.go
new file mode 100644
index 0000000..430f1a1
--- /dev/null
+++ b/_site/openstack/networking/v2/extensions/lbaas/vips/requests_test.go
@@ -0,0 +1,336 @@
+package vips
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ fake "github.com/rackspace/gophercloud/openstack/networking/v2/common"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestURLs(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.AssertEquals(t, th.Endpoint()+"v2.0/lb/vips", rootURL(fake.ServiceClient()))
+ th.AssertEquals(t, th.Endpoint()+"v2.0/lb/vips/foo", resourceURL(fake.ServiceClient(), "foo"))
+}
+
+func TestList(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/lb/vips", 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, `
+{
+ "vips":[
+ {
+ "id": "db902c0c-d5ff-4753-b465-668ad9656918",
+ "tenant_id": "310df60f-2a10-4ee5-9554-98393092194c",
+ "name": "web_vip",
+ "description": "lb config for the web tier",
+ "subnet_id": "96a4386a-f8c3-42ed-afce-d7954eee77b3",
+ "address" : "10.30.176.47",
+ "port_id" : "cd1f7a47-4fa6-449c-9ee7-632838aedfea",
+ "protocol": "HTTP",
+ "protocol_port": 80,
+ "pool_id" : "cfc6589d-f949-4c66-99d2-c2da56ef3764",
+ "admin_state_up": true,
+ "status": "ACTIVE"
+ },
+ {
+ "id": "36e08a3e-a78f-4b40-a229-1e7e23eee1ab",
+ "tenant_id": "310df60f-2a10-4ee5-9554-98393092194c",
+ "name": "db_vip",
+ "description": "lb config for the db tier",
+ "subnet_id": "9cedb85d-0759-4898-8a4b-fa5a5ea10086",
+ "address" : "10.30.176.48",
+ "port_id" : "cd1f7a47-4fa6-449c-9ee7-632838aedfea",
+ "protocol": "TCP",
+ "protocol_port": 3306,
+ "pool_id" : "41efe233-7591-43c5-9cf7-923964759f9e",
+ "session_persistence" : {"type" : "SOURCE_IP"},
+ "connection_limit" : 2000,
+ "admin_state_up": true,
+ "status": "INACTIVE"
+ }
+ ]
+}
+ `)
+ })
+
+ count := 0
+
+ List(fake.ServiceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractVIPs(page)
+ if err != nil {
+ t.Errorf("Failed to extract LBs: %v", err)
+ return false, err
+ }
+
+ expected := []VirtualIP{
+ VirtualIP{
+ ID: "db902c0c-d5ff-4753-b465-668ad9656918",
+ TenantID: "310df60f-2a10-4ee5-9554-98393092194c",
+ Name: "web_vip",
+ Description: "lb config for the web tier",
+ SubnetID: "96a4386a-f8c3-42ed-afce-d7954eee77b3",
+ Address: "10.30.176.47",
+ PortID: "cd1f7a47-4fa6-449c-9ee7-632838aedfea",
+ Protocol: "HTTP",
+ ProtocolPort: 80,
+ PoolID: "cfc6589d-f949-4c66-99d2-c2da56ef3764",
+ Persistence: SessionPersistence{},
+ ConnLimit: 0,
+ AdminStateUp: true,
+ Status: "ACTIVE",
+ },
+ VirtualIP{
+ ID: "36e08a3e-a78f-4b40-a229-1e7e23eee1ab",
+ TenantID: "310df60f-2a10-4ee5-9554-98393092194c",
+ Name: "db_vip",
+ Description: "lb config for the db tier",
+ SubnetID: "9cedb85d-0759-4898-8a4b-fa5a5ea10086",
+ Address: "10.30.176.48",
+ PortID: "cd1f7a47-4fa6-449c-9ee7-632838aedfea",
+ Protocol: "TCP",
+ ProtocolPort: 3306,
+ PoolID: "41efe233-7591-43c5-9cf7-923964759f9e",
+ Persistence: SessionPersistence{Type: "SOURCE_IP"},
+ ConnLimit: 2000,
+ AdminStateUp: true,
+ Status: "INACTIVE",
+ },
+ }
+
+ th.CheckDeepEquals(t, expected, actual)
+
+ return true, nil
+ })
+
+ if count != 1 {
+ t.Errorf("Expected 1 page, got %d", count)
+ }
+}
+
+func TestCreate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/lb/vips", 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, `
+{
+ "vip": {
+ "protocol": "HTTP",
+ "name": "NewVip",
+ "admin_state_up": true,
+ "subnet_id": "8032909d-47a1-4715-90af-5153ffe39861",
+ "pool_id": "61b1f87a-7a21-4ad3-9dda-7f81d249944f",
+ "protocol_port": 80,
+ "session_persistence": {"type": "SOURCE_IP"}
+ }
+}
+ `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusCreated)
+
+ fmt.Fprintf(w, `
+{
+ "vip": {
+ "status": "PENDING_CREATE",
+ "protocol": "HTTP",
+ "description": "",
+ "admin_state_up": true,
+ "subnet_id": "8032909d-47a1-4715-90af-5153ffe39861",
+ "tenant_id": "83657cfcdfe44cd5920adaf26c48ceea",
+ "connection_limit": -1,
+ "pool_id": "61b1f87a-7a21-4ad3-9dda-7f81d249944f",
+ "address": "10.0.0.11",
+ "protocol_port": 80,
+ "port_id": "f7e6fe6a-b8b5-43a8-8215-73456b32e0f5",
+ "id": "c987d2be-9a3c-4ac9-a046-e8716b1350e2",
+ "name": "NewVip"
+ }
+}
+ `)
+ })
+
+ opts := CreateOpts{
+ Protocol: "HTTP",
+ Name: "NewVip",
+ AdminStateUp: Up,
+ SubnetID: "8032909d-47a1-4715-90af-5153ffe39861",
+ PoolID: "61b1f87a-7a21-4ad3-9dda-7f81d249944f",
+ ProtocolPort: 80,
+ Persistence: &SessionPersistence{Type: "SOURCE_IP"},
+ }
+
+ r, err := Create(fake.ServiceClient(), opts).Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, "PENDING_CREATE", r.Status)
+ th.AssertEquals(t, "HTTP", r.Protocol)
+ th.AssertEquals(t, "", r.Description)
+ th.AssertEquals(t, true, r.AdminStateUp)
+ th.AssertEquals(t, "8032909d-47a1-4715-90af-5153ffe39861", r.SubnetID)
+ th.AssertEquals(t, "83657cfcdfe44cd5920adaf26c48ceea", r.TenantID)
+ th.AssertEquals(t, -1, r.ConnLimit)
+ th.AssertEquals(t, "61b1f87a-7a21-4ad3-9dda-7f81d249944f", r.PoolID)
+ th.AssertEquals(t, "10.0.0.11", r.Address)
+ th.AssertEquals(t, 80, r.ProtocolPort)
+ th.AssertEquals(t, "f7e6fe6a-b8b5-43a8-8215-73456b32e0f5", r.PortID)
+ th.AssertEquals(t, "c987d2be-9a3c-4ac9-a046-e8716b1350e2", r.ID)
+ th.AssertEquals(t, "NewVip", r.Name)
+}
+
+func TestRequiredCreateOpts(t *testing.T) {
+ res := Create(fake.ServiceClient(), CreateOpts{})
+ if res.Err == nil {
+ t.Fatalf("Expected error, got none")
+ }
+ res = Create(fake.ServiceClient(), CreateOpts{Name: "foo"})
+ if res.Err == nil {
+ t.Fatalf("Expected error, got none")
+ }
+ res = Create(fake.ServiceClient(), CreateOpts{Name: "foo", SubnetID: "bar"})
+ if res.Err == nil {
+ t.Fatalf("Expected error, got none")
+ }
+ res = Create(fake.ServiceClient(), CreateOpts{Name: "foo", SubnetID: "bar", Protocol: "bar"})
+ if res.Err == nil {
+ t.Fatalf("Expected error, got none")
+ }
+ res = Create(fake.ServiceClient(), CreateOpts{Name: "foo", SubnetID: "bar", Protocol: "bar", ProtocolPort: 80})
+ if res.Err == nil {
+ t.Fatalf("Expected error, got none")
+ }
+}
+
+func TestGet(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/lb/vips/4ec89087-d057-4e2c-911f-60a3b47ee304", 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, `
+{
+ "vip": {
+ "status": "ACTIVE",
+ "protocol": "HTTP",
+ "description": "",
+ "admin_state_up": true,
+ "subnet_id": "8032909d-47a1-4715-90af-5153ffe39861",
+ "tenant_id": "83657cfcdfe44cd5920adaf26c48ceea",
+ "connection_limit": 1000,
+ "pool_id": "72741b06-df4d-4715-b142-276b6bce75ab",
+ "session_persistence": {
+ "cookie_name": "MyAppCookie",
+ "type": "APP_COOKIE"
+ },
+ "address": "10.0.0.10",
+ "protocol_port": 80,
+ "port_id": "b5a743d6-056b-468b-862d-fb13a9aa694e",
+ "id": "4ec89087-d057-4e2c-911f-60a3b47ee304",
+ "name": "my-vip"
+ }
+}
+ `)
+ })
+
+ vip, err := Get(fake.ServiceClient(), "4ec89087-d057-4e2c-911f-60a3b47ee304").Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, "ACTIVE", vip.Status)
+ th.AssertEquals(t, "HTTP", vip.Protocol)
+ th.AssertEquals(t, "", vip.Description)
+ th.AssertEquals(t, true, vip.AdminStateUp)
+ th.AssertEquals(t, 1000, vip.ConnLimit)
+ th.AssertEquals(t, SessionPersistence{Type: "APP_COOKIE", CookieName: "MyAppCookie"}, vip.Persistence)
+}
+
+func TestUpdate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/lb/vips/4ec89087-d057-4e2c-911f-60a3b47ee304", 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.TestHeader(t, r, "Accept", "application/json")
+ th.TestJSONRequest(t, r, `
+{
+ "vip": {
+ "connection_limit": 1000,
+ "session_persistence": {"type": "SOURCE_IP"}
+ }
+}
+ `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusAccepted)
+
+ fmt.Fprintf(w, `
+{
+ "vip": {
+ "status": "PENDING_UPDATE",
+ "protocol": "HTTP",
+ "description": "",
+ "admin_state_up": true,
+ "subnet_id": "8032909d-47a1-4715-90af-5153ffe39861",
+ "tenant_id": "83657cfcdfe44cd5920adaf26c48ceea",
+ "connection_limit": 1000,
+ "pool_id": "61b1f87a-7a21-4ad3-9dda-7f81d249944f",
+ "address": "10.0.0.11",
+ "protocol_port": 80,
+ "port_id": "f7e6fe6a-b8b5-43a8-8215-73456b32e0f5",
+ "id": "c987d2be-9a3c-4ac9-a046-e8716b1350e2",
+ "name": "NewVip"
+ }
+}
+ `)
+ })
+
+ i1000 := 1000
+ options := UpdateOpts{
+ ConnLimit: &i1000,
+ Persistence: &SessionPersistence{Type: "SOURCE_IP"},
+ }
+ vip, err := Update(fake.ServiceClient(), "4ec89087-d057-4e2c-911f-60a3b47ee304", options).Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, "PENDING_UPDATE", vip.Status)
+ th.AssertEquals(t, 1000, vip.ConnLimit)
+}
+
+func TestDelete(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/lb/vips/4ec89087-d057-4e2c-911f-60a3b47ee304", 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)
+ })
+
+ res := Delete(fake.ServiceClient(), "4ec89087-d057-4e2c-911f-60a3b47ee304")
+ th.AssertNoErr(t, res.Err)
+}
diff --git a/_site/openstack/networking/v2/extensions/lbaas/vips/results.go b/_site/openstack/networking/v2/extensions/lbaas/vips/results.go
new file mode 100644
index 0000000..5925adc
--- /dev/null
+++ b/_site/openstack/networking/v2/extensions/lbaas/vips/results.go
@@ -0,0 +1,164 @@
+package vips
+
+import (
+ "github.com/mitchellh/mapstructure"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// SessionPersistence represents the session persistence feature of the load
+// balancing service. It attempts to force connections or requests in the same
+// session to be processed by the same member as long as it is ative. Three
+// types of persistence are supported:
+//
+// SOURCE_IP: With this mode, all connections originating from the same source
+// IP address, will be handled by the same member of the pool.
+// HTTP_COOKIE: With this persistence mode, the load balancing function will
+// create a cookie on the first request from a client. Subsequent
+// requests containing the same cookie value will be handled by
+// the same member of the pool.
+// APP_COOKIE: With this persistence mode, the load balancing function will
+// rely on a cookie established by the backend application. All
+// requests carrying the same cookie value will be handled by the
+// same member of the pool.
+type SessionPersistence struct {
+ // The type of persistence mode
+ Type string `mapstructure:"type" json:"type"`
+
+ // Name of cookie if persistence mode is set appropriately
+ CookieName string `mapstructure:"cookie_name" json:"cookie_name,omitempty"`
+}
+
+// VirtualIP is the primary load balancing configuration object that specifies
+// the virtual IP address and port on which client traffic is received, as well
+// as other details such as the load balancing method to be use, protocol, etc.
+// This entity is sometimes known in LB products under the name of a "virtual
+// server", a "vserver" or a "listener".
+type VirtualIP struct {
+ // The unique ID for the VIP.
+ ID string `mapstructure:"id" json:"id"`
+
+ // Owner of the VIP. Only an admin user can specify a tenant ID other than its own.
+ TenantID string `mapstructure:"tenant_id" json:"tenant_id"`
+
+ // Human-readable name for the VIP. Does not have to be unique.
+ Name string `mapstructure:"name" json:"name"`
+
+ // Human-readable description for the VIP.
+ Description string `mapstructure:"description" json:"description"`
+
+ // The ID of the subnet on which to allocate the VIP address.
+ SubnetID string `mapstructure:"subnet_id" json:"subnet_id"`
+
+ // The IP address of the VIP.
+ Address string `mapstructure:"address" json:"address"`
+
+ // The protocol of the VIP address. A valid value is TCP, HTTP, or HTTPS.
+ Protocol string `mapstructure:"protocol" json:"protocol"`
+
+ // The port on which to listen to client traffic that is associated with the
+ // VIP address. A valid value is from 0 to 65535.
+ ProtocolPort int `mapstructure:"protocol_port" json:"protocol_port"`
+
+ // The ID of the pool with which the VIP is associated.
+ PoolID string `mapstructure:"pool_id" json:"pool_id"`
+
+ // The ID of the port which belongs to the load balancer
+ PortID string `mapstructure:"port_id" json:"port_id"`
+
+ // Indicates whether connections in the same session will be processed by the
+ // same pool member or not.
+ Persistence SessionPersistence `mapstructure:"session_persistence" json:"session_persistence"`
+
+ // The maximum number of connections allowed for the VIP. Default is -1,
+ // meaning no limit.
+ ConnLimit int `mapstructure:"connection_limit" json:"connection_limit"`
+
+ // The administrative state of the VIP. A valid value is true (UP) or false (DOWN).
+ AdminStateUp bool `mapstructure:"admin_state_up" json:"admin_state_up"`
+
+ // The status of the VIP. Indicates whether the VIP is operational.
+ Status string `mapstructure:"status" json:"status"`
+}
+
+// VIPPage is the page returned by a pager when traversing over a
+// collection of routers.
+type VIPPage struct {
+ pagination.LinkedPageBase
+}
+
+// NextPageURL is invoked when a paginated collection of routers has reached
+// the end of a page and the pager seeks to traverse over a new one. In order
+// to do this, it needs to construct the next page's URL.
+func (p VIPPage) NextPageURL() (string, error) {
+ type resp struct {
+ Links []gophercloud.Link `mapstructure:"vips_links"`
+ }
+
+ var r resp
+ err := mapstructure.Decode(p.Body, &r)
+ if err != nil {
+ return "", err
+ }
+
+ return gophercloud.ExtractNextURL(r.Links)
+}
+
+// IsEmpty checks whether a RouterPage struct is empty.
+func (p VIPPage) IsEmpty() (bool, error) {
+ is, err := ExtractVIPs(p)
+ if err != nil {
+ return true, nil
+ }
+ return len(is) == 0, nil
+}
+
+// ExtractVIPs accepts a Page struct, specifically a VIPPage struct,
+// and extracts the elements into a slice of VirtualIP structs. In other words,
+// a generic collection is mapped into a relevant slice.
+func ExtractVIPs(page pagination.Page) ([]VirtualIP, error) {
+ var resp struct {
+ VIPs []VirtualIP `mapstructure:"vips" json:"vips"`
+ }
+
+ err := mapstructure.Decode(page.(VIPPage).Body, &resp)
+
+ return resp.VIPs, err
+}
+
+type commonResult struct {
+ gophercloud.CommonResult
+}
+
+// Extract is a function that accepts a result and extracts a router.
+func (r commonResult) Extract() (*VirtualIP, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var res struct {
+ VirtualIP *VirtualIP `mapstructure:"vip" json:"vip"`
+ }
+
+ err := mapstructure.Decode(r.Resp, &res)
+
+ return res.VirtualIP, err
+}
+
+// CreateResult represents the result of a create operation.
+type CreateResult struct {
+ commonResult
+}
+
+// GetResult represents the result of a get operation.
+type GetResult struct {
+ commonResult
+}
+
+// UpdateResult represents the result of an update operation.
+type UpdateResult struct {
+ commonResult
+}
+
+// DeleteResult represents the result of a delete operation.
+type DeleteResult commonResult
diff --git a/_site/openstack/networking/v2/extensions/lbaas/vips/urls.go b/_site/openstack/networking/v2/extensions/lbaas/vips/urls.go
new file mode 100644
index 0000000..2b6f67e
--- /dev/null
+++ b/_site/openstack/networking/v2/extensions/lbaas/vips/urls.go
@@ -0,0 +1,16 @@
+package vips
+
+import "github.com/rackspace/gophercloud"
+
+const (
+ rootPath = "lb"
+ resourcePath = "vips"
+)
+
+func rootURL(c *gophercloud.ServiceClient) string {
+ return c.ServiceURL(rootPath, resourcePath)
+}
+
+func resourceURL(c *gophercloud.ServiceClient, id string) string {
+ return c.ServiceURL(rootPath, resourcePath, id)
+}
diff --git a/_site/openstack/networking/v2/extensions/provider/doc.go b/_site/openstack/networking/v2/extensions/provider/doc.go
new file mode 100755
index 0000000..373da44
--- /dev/null
+++ b/_site/openstack/networking/v2/extensions/provider/doc.go
@@ -0,0 +1,21 @@
+// Package provider gives access to the provider Neutron plugin, allowing
+// network extended attributes. The provider extended attributes for networks
+// enable administrative users to specify how network objects map to the
+// underlying networking infrastructure. These extended attributes also appear
+// when administrative users query networks.
+//
+// For more information about extended attributes, see the NetworkExtAttrs
+// struct. The actual semantics of these attributes depend on the technology
+// back end of the particular plug-in. See the plug-in documentation and the
+// OpenStack Cloud Administrator Guide to understand which values should be
+// specific for each of these attributes when OpenStack Networking is deployed
+// with a particular plug-in. The examples shown in this chapter refer to the
+// Open vSwitch plug-in.
+//
+// The default policy settings enable only users with administrative rights to
+// specify these parameters in requests and to see their values in responses. By
+// default, the provider network extension attributes are completely hidden from
+// regular tenants. As a rule of thumb, if these attributes are not visible in a
+// GET /networks/<network-id> operation, this implies the user submitting the
+// request is not authorized to view or manipulate provider network attributes.
+package provider
diff --git a/_site/openstack/networking/v2/extensions/provider/results.go b/_site/openstack/networking/v2/extensions/provider/results.go
new file mode 100755
index 0000000..8fc21b4
--- /dev/null
+++ b/_site/openstack/networking/v2/extensions/provider/results.go
@@ -0,0 +1,124 @@
+package provider
+
+import (
+ "github.com/mitchellh/mapstructure"
+ "github.com/rackspace/gophercloud/openstack/networking/v2/networks"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// AdminState gives users a solid type to work with for create and update
+// operations. It is recommended that users use the `Up` and `Down` enums.
+type AdminState *bool
+
+// Convenience vars for AdminStateUp values.
+var (
+ iTrue = true
+ iFalse = false
+
+ Up AdminState = &iTrue
+ Down AdminState = &iFalse
+)
+
+// NetworkExtAttrs represents an extended form of a Network with additional fields.
+type NetworkExtAttrs struct {
+ // UUID for the network
+ ID string `mapstructure:"id" json:"id"`
+
+ // Human-readable name for the network. Might not be unique.
+ Name string `mapstructure:"name" json:"name"`
+
+ // The administrative state of network. If false (down), the network does not forward packets.
+ AdminStateUp bool `mapstructure:"admin_state_up" json:"admin_state_up"`
+
+ // Indicates whether network is currently operational. Possible values include
+ // `ACTIVE', `DOWN', `BUILD', or `ERROR'. Plug-ins might define additional values.
+ Status string `mapstructure:"status" json:"status"`
+
+ // Subnets associated with this network.
+ Subnets []string `mapstructure:"subnets" json:"subnets"`
+
+ // Owner of network. Only admin users can specify a tenant_id other than its own.
+ TenantID string `mapstructure:"tenant_id" json:"tenant_id"`
+
+ // Specifies whether the network resource can be accessed by any tenant or not.
+ Shared bool `mapstructure:"shared" json:"shared"`
+
+ // Specifies the nature of the physical network mapped to this network
+ // resource. Examples are flat, vlan, or gre.
+ NetworkType string `json:"provider:network_type" mapstructure:"provider:network_type"`
+
+ // Identifies the physical network on top of which this network object is
+ // being implemented. The OpenStack Networking API does not expose any facility
+ // for retrieving the list of available physical networks. As an example, in
+ // the Open vSwitch plug-in this is a symbolic name which is then mapped to
+ // specific bridges on each compute host through the Open vSwitch plug-in
+ // configuration file.
+ PhysicalNetwork string `json:"provider:physical_network" mapstructure:"provider:physical_network"`
+
+ // Identifies an isolated segment on the physical network; the nature of the
+ // segment depends on the segmentation model defined by network_type. For
+ // instance, if network_type is vlan, then this is a vlan identifier;
+ // otherwise, if network_type is gre, then this will be a gre key.
+ SegmentationID string `json:"provider:segmentation_id" mapstructure:"provider:segmentation_id"`
+}
+
+// ExtractGet decorates a GetResult struct returned from a networks.Get()
+// function with extended attributes.
+func ExtractGet(r networks.GetResult) (*NetworkExtAttrs, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var res struct {
+ Network *NetworkExtAttrs `json:"network"`
+ }
+
+ err := mapstructure.Decode(r.Resp, &res)
+
+ return res.Network, err
+}
+
+// ExtractCreate decorates a CreateResult struct returned from a networks.Create()
+// function with extended attributes.
+func ExtractCreate(r networks.CreateResult) (*NetworkExtAttrs, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var res struct {
+ Network *NetworkExtAttrs `json:"network"`
+ }
+
+ err := mapstructure.Decode(r.Resp, &res)
+
+ return res.Network, err
+}
+
+// ExtractUpdate decorates a UpdateResult struct returned from a
+// networks.Update() function with extended attributes.
+func ExtractUpdate(r networks.UpdateResult) (*NetworkExtAttrs, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var res struct {
+ Network *NetworkExtAttrs `json:"network"`
+ }
+
+ err := mapstructure.Decode(r.Resp, &res)
+
+ return res.Network, err
+}
+
+// ExtractList accepts a Page struct, specifically a NetworkPage struct, and
+// extracts the elements into a slice of NetworkExtAttrs structs. In other
+// words, a generic collection is mapped into a relevant slice.
+func ExtractList(page pagination.Page) ([]NetworkExtAttrs, error) {
+ var resp struct {
+ Networks []NetworkExtAttrs `mapstructure:"networks" json:"networks"`
+ }
+
+ err := mapstructure.Decode(page.(networks.NetworkPage).Body, &resp)
+
+ return resp.Networks, err
+}
diff --git a/_site/openstack/networking/v2/extensions/provider/results_test.go b/_site/openstack/networking/v2/extensions/provider/results_test.go
new file mode 100644
index 0000000..9801b2e
--- /dev/null
+++ b/_site/openstack/networking/v2/extensions/provider/results_test.go
@@ -0,0 +1,253 @@
+package provider
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ fake "github.com/rackspace/gophercloud/openstack/networking/v2/common"
+ "github.com/rackspace/gophercloud/openstack/networking/v2/networks"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestList(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/networks", 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, `
+{
+ "networks": [
+ {
+ "status": "ACTIVE",
+ "subnets": [
+ "54d6f61d-db07-451c-9ab3-b9609b6b6f0b"
+ ],
+ "name": "private-network",
+ "admin_state_up": true,
+ "tenant_id": "4fd44f30292945e481c7b8a0c8908869",
+ "shared": true,
+ "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22",
+ "provider:segmentation_id": null,
+ "provider:physical_network": null,
+ "provider:network_type": "local"
+ },
+ {
+ "status": "ACTIVE",
+ "subnets": [
+ "08eae331-0402-425a-923c-34f7cfe39c1b"
+ ],
+ "name": "private",
+ "admin_state_up": true,
+ "tenant_id": "26a7980765d0414dbc1fc1f88cdb7e6e",
+ "shared": true,
+ "id": "db193ab3-96e3-4cb3-8fc5-05f4296d0324",
+ "provider:segmentation_id": null,
+ "provider:physical_network": null,
+ "provider:network_type": "local"
+ }
+ ]
+}
+ `)
+ })
+
+ count := 0
+
+ networks.List(fake.ServiceClient(), networks.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractList(page)
+ if err != nil {
+ t.Errorf("Failed to extract networks: %v", err)
+ return false, err
+ }
+
+ expected := []NetworkExtAttrs{
+ NetworkExtAttrs{
+ Status: "ACTIVE",
+ Subnets: []string{"54d6f61d-db07-451c-9ab3-b9609b6b6f0b"},
+ Name: "private-network",
+ AdminStateUp: true,
+ TenantID: "4fd44f30292945e481c7b8a0c8908869",
+ Shared: true,
+ ID: "d32019d3-bc6e-4319-9c1d-6722fc136a22",
+ NetworkType: "local",
+ PhysicalNetwork: "",
+ SegmentationID: "",
+ },
+ NetworkExtAttrs{
+ Status: "ACTIVE",
+ Subnets: []string{"08eae331-0402-425a-923c-34f7cfe39c1b"},
+ Name: "private",
+ AdminStateUp: true,
+ TenantID: "26a7980765d0414dbc1fc1f88cdb7e6e",
+ Shared: true,
+ ID: "db193ab3-96e3-4cb3-8fc5-05f4296d0324",
+ NetworkType: "local",
+ PhysicalNetwork: "",
+ SegmentationID: "",
+ },
+ }
+
+ th.CheckDeepEquals(t, expected, actual)
+
+ return true, nil
+ })
+
+ if count != 1 {
+ t.Errorf("Expected 1 page, got %d", count)
+ }
+}
+
+func TestGet(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/networks/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, `
+{
+ "network": {
+ "status": "ACTIVE",
+ "subnets": [
+ "54d6f61d-db07-451c-9ab3-b9609b6b6f0b"
+ ],
+ "name": "private-network",
+ "provider:physical_network": null,
+ "admin_state_up": true,
+ "tenant_id": "4fd44f30292945e481c7b8a0c8908869",
+ "provider:network_type": "local",
+ "shared": true,
+ "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22",
+ "provider:segmentation_id": null
+ }
+}
+ `)
+ })
+
+ res := networks.Get(fake.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22")
+ n, err := ExtractGet(res)
+
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, "", n.PhysicalNetwork)
+ th.AssertEquals(t, "local", n.NetworkType)
+ th.AssertEquals(t, "", n.SegmentationID)
+}
+
+func TestCreate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/networks", 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, `
+{
+ "network": {
+ "name": "sample_network",
+ "admin_state_up": true
+ }
+}
+ `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusCreated)
+
+ fmt.Fprintf(w, `
+{
+ "network": {
+ "status": "ACTIVE",
+ "subnets": [
+ "54d6f61d-db07-451c-9ab3-b9609b6b6f0b"
+ ],
+ "name": "private-network",
+ "provider:physical_network": null,
+ "admin_state_up": true,
+ "tenant_id": "4fd44f30292945e481c7b8a0c8908869",
+ "provider:network_type": "local",
+ "shared": true,
+ "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22",
+ "provider:segmentation_id": null
+ }
+}
+ `)
+ })
+
+ options := networks.CreateOpts{Name: "sample_network", AdminStateUp: Up}
+ res := networks.Create(fake.ServiceClient(), options)
+ n, err := ExtractCreate(res)
+
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, "", n.PhysicalNetwork)
+ th.AssertEquals(t, "local", n.NetworkType)
+ th.AssertEquals(t, "", n.SegmentationID)
+}
+
+func TestUpdate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/networks/4e8e5957-649f-477b-9e5b-f1f75b21c03c", 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.TestHeader(t, r, "Accept", "application/json")
+ th.TestJSONRequest(t, r, `
+{
+ "network": {
+ "name": "new_network_name",
+ "admin_state_up": false,
+ "shared": true
+ }
+}
+ `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, `
+{
+ "network": {
+ "status": "ACTIVE",
+ "subnets": [
+ "54d6f61d-db07-451c-9ab3-b9609b6b6f0b"
+ ],
+ "name": "private-network",
+ "provider:physical_network": null,
+ "admin_state_up": true,
+ "tenant_id": "4fd44f30292945e481c7b8a0c8908869",
+ "provider:network_type": "local",
+ "shared": true,
+ "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22",
+ "provider:segmentation_id": null
+ }
+}
+ `)
+ })
+
+ iTrue := true
+ options := networks.UpdateOpts{Name: "new_network_name", AdminStateUp: Down, Shared: &iTrue}
+ res := networks.Update(fake.ServiceClient(), "4e8e5957-649f-477b-9e5b-f1f75b21c03c", options)
+ n, err := ExtractUpdate(res)
+
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, "", n.PhysicalNetwork)
+ th.AssertEquals(t, "local", n.NetworkType)
+ th.AssertEquals(t, "", n.SegmentationID)
+}
diff --git a/_site/openstack/networking/v2/extensions/security/doc.go b/_site/openstack/networking/v2/extensions/security/doc.go
new file mode 100644
index 0000000..8ef455f
--- /dev/null
+++ b/_site/openstack/networking/v2/extensions/security/doc.go
@@ -0,0 +1,32 @@
+// Package security contains functionality to work with security group and
+// security group rules Neutron resources.
+//
+// Security groups and security group rules allows administrators and tenants
+// the ability to specify the type of traffic and direction (ingress/egress)
+// that is allowed to pass through a port. A security group is a container for
+// security group rules.
+//
+// When a port is created in Networking it is associated with a security group.
+// If a security group is not specified the port is associated with a 'default'
+// security group. By default, this group drops all ingress traffic and allows
+// all egress. Rules can be added to this group in order to change the behaviour.
+//
+// The basic characteristics of Neutron Security Groups are:
+//
+// For ingress traffic (to an instance)
+// - Only traffic matched with security group rules are allowed.
+// - When there is no rule defined, all traffic are dropped.
+//
+// For egress traffic (from an instance)
+// - Only traffic matched with security group rules are allowed.
+// - When there is no rule defined, all egress traffic are dropped.
+// - When a new security group is created, rules to allow all egress traffic
+// are automatically added.
+//
+// "default security group" is defined for each tenant.
+// - For the default security group a rule which allows intercommunication
+// among hosts associated with the default security group is defined by default.
+// - As a result, all egress traffic and intercommunication in the default
+// group are allowed and all ingress from outside of the default group is
+// dropped by default (in the default security group).
+package security
diff --git a/_site/openstack/networking/v2/extensions/security/groups/requests.go b/_site/openstack/networking/v2/extensions/security/groups/requests.go
new file mode 100644
index 0000000..6e9fe33
--- /dev/null
+++ b/_site/openstack/networking/v2/extensions/security/groups/requests.go
@@ -0,0 +1,107 @@
+package groups
+
+import (
+ "fmt"
+
+ "github.com/racker/perigee"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// ListOpts allows the filtering and sorting of paginated collections through
+// the API. Filtering is achieved by passing in struct field values that map to
+// the floating IP attributes you want to see returned. SortKey allows you to
+// sort by a particular network attribute. SortDir sets the direction, and is
+// either `asc' or `desc'. Marker and Limit are used for pagination.
+type ListOpts struct {
+ ID string `q:"id"`
+ Name string `q:"name"`
+ TenantID string `q:"tenant_id"`
+ Limit int `q:"limit"`
+ Marker string `q:"marker"`
+ SortKey string `q:"sort_key"`
+ SortDir string `q:"sort_dir"`
+}
+
+// List returns a Pager which allows you to iterate over a collection of
+// security groups. It accepts a ListOpts struct, which allows you to filter
+// and sort the returned collection for greater efficiency.
+func List(c *gophercloud.ServiceClient, opts ListOpts) pagination.Pager {
+ q, err := gophercloud.BuildQueryString(&opts)
+ if err != nil {
+ return pagination.Pager{Err: err}
+ }
+ u := rootURL(c) + q.String()
+ return pagination.NewPager(c, u, func(r pagination.LastHTTPResponse) pagination.Page {
+ return SecGroupPage{pagination.LinkedPageBase{LastHTTPResponse: r}}
+ })
+}
+
+var (
+ errNameRequired = fmt.Errorf("Name is required")
+)
+
+// CreateOpts contains all the values needed to create a new security group.
+type CreateOpts struct {
+ // Required. Human-readable name for the VIP. Does not have to be unique.
+ Name string
+
+ // Optional. Describes the security group.
+ Description string
+}
+
+// Create is an operation which provisions a new security group with default
+// security group rules for the IPv4 and IPv6 ether types.
+func Create(c *gophercloud.ServiceClient, opts CreateOpts) CreateResult {
+ var res CreateResult
+
+ // Validate required opts
+ if opts.Name == "" {
+ res.Err = errNameRequired
+ return res
+ }
+
+ type secgroup struct {
+ Name string `json:"name"`
+ Description string `json:"description,omitempty"`
+ }
+
+ type request struct {
+ SecGroup secgroup `json:"security_group"`
+ }
+
+ reqBody := request{SecGroup: secgroup{
+ Name: opts.Name,
+ Description: opts.Description,
+ }}
+
+ _, res.Err = perigee.Request("POST", rootURL(c), perigee.Options{
+ MoreHeaders: c.Provider.AuthenticatedHeaders(),
+ ReqBody: &reqBody,
+ Results: &res.Resp,
+ OkCodes: []int{201},
+ })
+
+ return res
+}
+
+// Get retrieves a particular security group based on its unique ID.
+func Get(c *gophercloud.ServiceClient, id string) GetResult {
+ var res GetResult
+ _, res.Err = perigee.Request("GET", resourceURL(c, id), perigee.Options{
+ MoreHeaders: c.Provider.AuthenticatedHeaders(),
+ Results: &res.Resp,
+ OkCodes: []int{200},
+ })
+ return res
+}
+
+// Delete will permanently delete a particular security group based on its unique ID.
+func Delete(c *gophercloud.ServiceClient, id string) DeleteResult {
+ var res DeleteResult
+ _, res.Err = perigee.Request("DELETE", resourceURL(c, id), perigee.Options{
+ MoreHeaders: c.Provider.AuthenticatedHeaders(),
+ OkCodes: []int{204},
+ })
+ return res
+}
diff --git a/_site/openstack/networking/v2/extensions/security/groups/requests_test.go b/_site/openstack/networking/v2/extensions/security/groups/requests_test.go
new file mode 100644
index 0000000..5f074c7
--- /dev/null
+++ b/_site/openstack/networking/v2/extensions/security/groups/requests_test.go
@@ -0,0 +1,213 @@
+package groups
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ fake "github.com/rackspace/gophercloud/openstack/networking/v2/common"
+ "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/rules"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestURLs(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.AssertEquals(t, th.Endpoint()+"v2.0/security-groups", rootURL(fake.ServiceClient()))
+ th.AssertEquals(t, th.Endpoint()+"v2.0/security-groups/foo", resourceURL(fake.ServiceClient(), "foo"))
+}
+
+func TestList(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/security-groups", 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_groups": [
+ {
+ "description": "default",
+ "id": "85cc3048-abc3-43cc-89b3-377341426ac5",
+ "name": "default",
+ "security_group_rules": [],
+ "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550"
+ }
+ ]
+}
+ `)
+ })
+
+ count := 0
+
+ List(fake.ServiceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractGroups(page)
+ if err != nil {
+ t.Errorf("Failed to extract secgroups: %v", err)
+ return false, err
+ }
+
+ expected := []SecGroup{
+ SecGroup{
+ Description: "default",
+ ID: "85cc3048-abc3-43cc-89b3-377341426ac5",
+ Name: "default",
+ Rules: []rules.SecGroupRule{},
+ TenantID: "e4f50856753b4dc6afee5fa6b9b6c550",
+ },
+ }
+
+ th.CheckDeepEquals(t, expected, actual)
+
+ return true, nil
+ })
+
+ if count != 1 {
+ t.Errorf("Expected 1 page, got %d", count)
+ }
+}
+
+func TestCreate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/security-groups", 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, `
+{
+ "security_group": {
+ "name": "new-webservers",
+ "description": "security group for webservers"
+ }
+}
+ `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusCreated)
+
+ fmt.Fprintf(w, `
+{
+ "security_group": {
+ "description": "security group for webservers",
+ "id": "2076db17-a522-4506-91de-c6dd8e837028",
+ "name": "new-webservers",
+ "security_group_rules": [
+ {
+ "direction": "egress",
+ "ethertype": "IPv4",
+ "id": "38ce2d8e-e8f1-48bd-83c2-d33cb9f50c3d",
+ "port_range_max": null,
+ "port_range_min": null,
+ "protocol": null,
+ "remote_group_id": null,
+ "remote_ip_prefix": null,
+ "security_group_id": "2076db17-a522-4506-91de-c6dd8e837028",
+ "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550"
+ },
+ {
+ "direction": "egress",
+ "ethertype": "IPv6",
+ "id": "565b9502-12de-4ffd-91e9-68885cff6ae1",
+ "port_range_max": null,
+ "port_range_min": null,
+ "protocol": null,
+ "remote_group_id": null,
+ "remote_ip_prefix": null,
+ "security_group_id": "2076db17-a522-4506-91de-c6dd8e837028",
+ "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550"
+ }
+ ],
+ "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550"
+ }
+}
+ `)
+ })
+
+ opts := CreateOpts{Name: "new-webservers", Description: "security group for webservers"}
+ _, err := Create(fake.ServiceClient(), opts).Extract()
+ th.AssertNoErr(t, err)
+}
+
+func TestGet(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/security-groups/85cc3048-abc3-43cc-89b3-377341426ac5", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, `
+{
+ "security_group": {
+ "description": "default",
+ "id": "85cc3048-abc3-43cc-89b3-377341426ac5",
+ "name": "default",
+ "security_group_rules": [
+ {
+ "direction": "egress",
+ "ethertype": "IPv6",
+ "id": "3c0e45ff-adaf-4124-b083-bf390e5482ff",
+ "port_range_max": null,
+ "port_range_min": null,
+ "protocol": null,
+ "remote_group_id": null,
+ "remote_ip_prefix": null,
+ "security_group_id": "85cc3048-abc3-43cc-89b3-377341426ac5",
+ "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550"
+ },
+ {
+ "direction": "egress",
+ "ethertype": "IPv4",
+ "id": "93aa42e5-80db-4581-9391-3a608bd0e448",
+ "port_range_max": null,
+ "port_range_min": null,
+ "protocol": null,
+ "remote_group_id": null,
+ "remote_ip_prefix": null,
+ "security_group_id": "85cc3048-abc3-43cc-89b3-377341426ac5",
+ "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550"
+ }
+ ],
+ "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550"
+ }
+}
+ `)
+ })
+
+ sg, err := Get(fake.ServiceClient(), "85cc3048-abc3-43cc-89b3-377341426ac5").Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, "default", sg.Description)
+ th.AssertEquals(t, "85cc3048-abc3-43cc-89b3-377341426ac5", sg.ID)
+ th.AssertEquals(t, "default", sg.Name)
+ th.AssertEquals(t, 2, len(sg.Rules))
+ th.AssertEquals(t, "e4f50856753b4dc6afee5fa6b9b6c550", sg.TenantID)
+}
+
+func TestDelete(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/security-groups/4ec89087-d057-4e2c-911f-60a3b47ee304", 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)
+ })
+
+ res := Delete(fake.ServiceClient(), "4ec89087-d057-4e2c-911f-60a3b47ee304")
+ th.AssertNoErr(t, res.Err)
+}
diff --git a/_site/openstack/networking/v2/extensions/security/groups/results.go b/_site/openstack/networking/v2/extensions/security/groups/results.go
new file mode 100644
index 0000000..617d690
--- /dev/null
+++ b/_site/openstack/networking/v2/extensions/security/groups/results.go
@@ -0,0 +1,106 @@
+package groups
+
+import (
+ "github.com/mitchellh/mapstructure"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/rules"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// SecGroup represents a container for security group rules.
+type SecGroup struct {
+ // The UUID for the security group.
+ ID string
+
+ // Human-readable name for the security group. Might not be unique. Cannot be
+ // named "default" as that is automatically created for a tenant.
+ Name string
+
+ // The security group description.
+ Description string
+
+ // A slice of security group rules that dictate the permitted behaviour for
+ // traffic entering and leaving the group.
+ Rules []rules.SecGroupRule `json:"security_group_rules" mapstructure:"security_group_rules"`
+
+ // Owner of the security group. Only admin users can specify a TenantID
+ // other than their own.
+ TenantID string `json:"tenant_id" mapstructure:"tenant_id"`
+}
+
+// SecGroupPage is the page returned by a pager when traversing over a
+// collection of security groups.
+type SecGroupPage struct {
+ pagination.LinkedPageBase
+}
+
+// NextPageURL is invoked when a paginated collection of security groups has
+// reached the end of a page and the pager seeks to traverse over a new one. In
+// order to do this, it needs to construct the next page's URL.
+func (p SecGroupPage) NextPageURL() (string, error) {
+ type resp struct {
+ Links []gophercloud.Link `mapstructure:"security_groups_links"`
+ }
+
+ var r resp
+ err := mapstructure.Decode(p.Body, &r)
+ if err != nil {
+ return "", err
+ }
+
+ return gophercloud.ExtractNextURL(r.Links)
+}
+
+// IsEmpty checks whether a SecGroupPage struct is empty.
+func (p SecGroupPage) IsEmpty() (bool, error) {
+ is, err := ExtractGroups(p)
+ if err != nil {
+ return true, nil
+ }
+ return len(is) == 0, nil
+}
+
+// ExtractGroups accepts a Page struct, specifically a SecGroupPage struct,
+// and extracts the elements into a slice of SecGroup structs. In other words,
+// a generic collection is mapped into a relevant slice.
+func ExtractGroups(page pagination.Page) ([]SecGroup, error) {
+ var resp struct {
+ SecGroups []SecGroup `mapstructure:"security_groups" json:"security_groups"`
+ }
+
+ err := mapstructure.Decode(page.(SecGroupPage).Body, &resp)
+
+ return resp.SecGroups, err
+}
+
+type commonResult struct {
+ gophercloud.CommonResult
+}
+
+// Extract is a function that accepts a result and extracts a security group.
+func (r commonResult) Extract() (*SecGroup, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var res struct {
+ SecGroup *SecGroup `mapstructure:"security_group" json:"security_group"`
+ }
+
+ err := mapstructure.Decode(r.Resp, &res)
+
+ return res.SecGroup, err
+}
+
+// CreateResult represents the result of a create operation.
+type CreateResult struct {
+ commonResult
+}
+
+// GetResult represents the result of a get operation.
+type GetResult struct {
+ commonResult
+}
+
+// DeleteResult represents the result of a delete operation.
+type DeleteResult commonResult
diff --git a/_site/openstack/networking/v2/extensions/security/groups/urls.go b/_site/openstack/networking/v2/extensions/security/groups/urls.go
new file mode 100644
index 0000000..84f7324
--- /dev/null
+++ b/_site/openstack/networking/v2/extensions/security/groups/urls.go
@@ -0,0 +1,13 @@
+package groups
+
+import "github.com/rackspace/gophercloud"
+
+const rootPath = "security-groups"
+
+func rootURL(c *gophercloud.ServiceClient) string {
+ return c.ServiceURL(rootPath)
+}
+
+func resourceURL(c *gophercloud.ServiceClient, id string) string {
+ return c.ServiceURL(rootPath, id)
+}
diff --git a/_site/openstack/networking/v2/extensions/security/rules/requests.go b/_site/openstack/networking/v2/extensions/security/rules/requests.go
new file mode 100644
index 0000000..ea0f37d
--- /dev/null
+++ b/_site/openstack/networking/v2/extensions/security/rules/requests.go
@@ -0,0 +1,183 @@
+package rules
+
+import (
+ "fmt"
+
+ "github.com/racker/perigee"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// ListOpts allows the filtering and sorting of paginated collections through
+// the API. Filtering is achieved by passing in struct field values that map to
+// the security group attributes you want to see returned. SortKey allows you to
+// sort by a particular network attribute. SortDir sets the direction, and is
+// either `asc' or `desc'. Marker and Limit are used for pagination.
+type ListOpts struct {
+ Direction string `q:"direction"`
+ EtherType string `q:"ethertype"`
+ ID string `q:"id"`
+ PortRangeMax int `q:"port_range_max"`
+ PortRangeMin int `q:"port_range_min"`
+ Protocol string `q:"protocol"`
+ RemoteGroupID string `q:"remote_group_id"`
+ RemoteIPPrefix string `q:"remote_ip_prefix"`
+ SecGroupID string `q:"security_group_id"`
+ TenantID string `q:"tenant_id"`
+ Limit int `q:"limit"`
+ Marker string `q:"marker"`
+ SortKey string `q:"sort_key"`
+ SortDir string `q:"sort_dir"`
+}
+
+// List returns a Pager which allows you to iterate over a collection of
+// security group rules. It accepts a ListOpts struct, which allows you to filter
+// and sort the returned collection for greater efficiency.
+func List(c *gophercloud.ServiceClient, opts ListOpts) pagination.Pager {
+ q, err := gophercloud.BuildQueryString(&opts)
+ if err != nil {
+ return pagination.Pager{Err: err}
+ }
+ u := rootURL(c) + q.String()
+ return pagination.NewPager(c, u, func(r pagination.LastHTTPResponse) pagination.Page {
+ return SecGroupRulePage{pagination.LinkedPageBase{LastHTTPResponse: r}}
+ })
+}
+
+// Errors
+var (
+ errValidDirectionRequired = fmt.Errorf("A valid Direction is required")
+ errValidEtherTypeRequired = fmt.Errorf("A valid EtherType is required")
+ errSecGroupIDRequired = fmt.Errorf("A valid SecGroupID is required")
+ errValidProtocolRequired = fmt.Errorf("A valid Protocol is required")
+)
+
+// Constants useful for CreateOpts
+const (
+ DirIngress = "ingress"
+ DirEgress = "egress"
+ Ether4 = "IPv4"
+ Ether6 = "IPv6"
+ ProtocolTCP = "tcp"
+ ProtocolUDP = "udp"
+ ProtocolICMP = "icmp"
+)
+
+// CreateOpts contains all the values needed to create a new security group rule.
+type CreateOpts struct {
+ // Required. Must be either "ingress" or "egress": the direction in which the
+ // security group rule is applied.
+ Direction string
+
+ // Required. Must be "IPv4" or "IPv6", and addresses represented in CIDR must
+ // match the ingress or egress rules.
+ EtherType string
+
+ // Required. The security group ID to associate with this security group rule.
+ SecGroupID string
+
+ // Optional. The maximum port number in the range that is matched by the
+ // security group rule. The PortRangeMin attribute constrains the PortRangeMax
+ // attribute. If the protocol is ICMP, this value must be an ICMP type.
+ PortRangeMax int
+
+ // Optional. The minimum port number in the range that is matched by the
+ // security group rule. If the protocol is TCP or UDP, this value must be
+ // less than or equal to the value of the PortRangeMax attribute. If the
+ // protocol is ICMP, this value must be an ICMP type.
+ PortRangeMin int
+
+ // Optional. The protocol that is matched by the security group rule. Valid
+ // values are "tcp", "udp", "icmp" or an empty string.
+ Protocol string
+
+ // Optional. The remote group ID to be associated with this security group
+ // rule. You can specify either RemoteGroupID or RemoteIPPrefix.
+ RemoteGroupID string
+
+ // Optional. The remote IP prefix to be associated with this security group
+ // rule. You can specify either RemoteGroupID or RemoteIPPrefix. This
+ // attribute matches the specified IP prefix as the source IP address of the
+ // IP packet.
+ RemoteIPPrefix string
+}
+
+// Create is an operation which provisions a new security group with default
+// security group rules for the IPv4 and IPv6 ether types.
+func Create(c *gophercloud.ServiceClient, opts CreateOpts) CreateResult {
+ var res CreateResult
+
+ // Validate required opts
+ if opts.Direction != DirIngress && opts.Direction != DirEgress {
+ res.Err = errValidDirectionRequired
+ return res
+ }
+ if opts.EtherType != Ether4 && opts.EtherType != Ether6 {
+ res.Err = errValidEtherTypeRequired
+ return res
+ }
+ if opts.SecGroupID == "" {
+ res.Err = errSecGroupIDRequired
+ return res
+ }
+ if opts.Protocol != "" && opts.Protocol != ProtocolTCP && opts.Protocol != ProtocolUDP && opts.Protocol != ProtocolICMP {
+ res.Err = errValidProtocolRequired
+ return res
+ }
+
+ type secrule struct {
+ Direction string `json:"direction"`
+ EtherType string `json:"ethertype"`
+ SecGroupID string `json:"security_group_id"`
+ PortRangeMax int `json:"port_range_max,omitempty"`
+ PortRangeMin int `json:"port_range_min,omitempty"`
+ Protocol string `json:"protocol,omitempty"`
+ RemoteGroupID string `json:"remote_group_id,omitempty"`
+ RemoteIPPrefix string `json:"remote_ip_prefix,omitempty"`
+ }
+
+ type request struct {
+ SecRule secrule `json:"security_group_rule"`
+ }
+
+ reqBody := request{SecRule: secrule{
+ Direction: opts.Direction,
+ EtherType: opts.EtherType,
+ SecGroupID: opts.SecGroupID,
+ PortRangeMax: opts.PortRangeMax,
+ PortRangeMin: opts.PortRangeMin,
+ Protocol: opts.Protocol,
+ RemoteGroupID: opts.RemoteGroupID,
+ RemoteIPPrefix: opts.RemoteIPPrefix,
+ }}
+
+ _, res.Err = perigee.Request("POST", rootURL(c), perigee.Options{
+ MoreHeaders: c.Provider.AuthenticatedHeaders(),
+ ReqBody: &reqBody,
+ Results: &res.Resp,
+ OkCodes: []int{201},
+ })
+
+ return res
+}
+
+// Get retrieves a particular security group based on its unique ID.
+func Get(c *gophercloud.ServiceClient, id string) GetResult {
+ var res GetResult
+ _, res.Err = perigee.Request("GET", resourceURL(c, id), perigee.Options{
+ MoreHeaders: c.Provider.AuthenticatedHeaders(),
+ Results: &res.Resp,
+ OkCodes: []int{200},
+ })
+ return res
+}
+
+// Delete will permanently delete a particular security group based on its unique ID.
+func Delete(c *gophercloud.ServiceClient, id string) DeleteResult {
+ var res DeleteResult
+ _, res.Err = perigee.Request("DELETE", resourceURL(c, id), perigee.Options{
+ MoreHeaders: c.Provider.AuthenticatedHeaders(),
+ OkCodes: []int{204},
+ })
+ return res
+}
diff --git a/_site/openstack/networking/v2/extensions/security/rules/requests_test.go b/_site/openstack/networking/v2/extensions/security/rules/requests_test.go
new file mode 100644
index 0000000..b5afef3
--- /dev/null
+++ b/_site/openstack/networking/v2/extensions/security/rules/requests_test.go
@@ -0,0 +1,243 @@
+package rules
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ fake "github.com/rackspace/gophercloud/openstack/networking/v2/common"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestURLs(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.AssertEquals(t, th.Endpoint()+"v2.0/security-group-rules", rootURL(fake.ServiceClient()))
+ th.AssertEquals(t, th.Endpoint()+"v2.0/security-group-rules/foo", resourceURL(fake.ServiceClient(), "foo"))
+}
+
+func TestList(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/security-group-rules", 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_rules": [
+ {
+ "direction": "egress",
+ "ethertype": "IPv6",
+ "id": "3c0e45ff-adaf-4124-b083-bf390e5482ff",
+ "port_range_max": null,
+ "port_range_min": null,
+ "protocol": null,
+ "remote_group_id": null,
+ "remote_ip_prefix": null,
+ "security_group_id": "85cc3048-abc3-43cc-89b3-377341426ac5",
+ "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550"
+ },
+ {
+ "direction": "egress",
+ "ethertype": "IPv4",
+ "id": "93aa42e5-80db-4581-9391-3a608bd0e448",
+ "port_range_max": null,
+ "port_range_min": null,
+ "protocol": null,
+ "remote_group_id": null,
+ "remote_ip_prefix": null,
+ "security_group_id": "85cc3048-abc3-43cc-89b3-377341426ac5",
+ "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550"
+ }
+ ]
+}
+ `)
+ })
+
+ count := 0
+
+ List(fake.ServiceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractRules(page)
+ if err != nil {
+ t.Errorf("Failed to extract secrules: %v", err)
+ return false, err
+ }
+
+ expected := []SecGroupRule{
+ SecGroupRule{
+ Direction: "egress",
+ EtherType: "IPv6",
+ ID: "3c0e45ff-adaf-4124-b083-bf390e5482ff",
+ PortRangeMax: 0,
+ PortRangeMin: 0,
+ Protocol: "",
+ RemoteGroupID: "",
+ RemoteIPPrefix: "",
+ SecGroupID: "85cc3048-abc3-43cc-89b3-377341426ac5",
+ TenantID: "e4f50856753b4dc6afee5fa6b9b6c550",
+ },
+ SecGroupRule{
+ Direction: "egress",
+ EtherType: "IPv4",
+ ID: "93aa42e5-80db-4581-9391-3a608bd0e448",
+ PortRangeMax: 0,
+ PortRangeMin: 0,
+ Protocol: "",
+ RemoteGroupID: "",
+ RemoteIPPrefix: "",
+ SecGroupID: "85cc3048-abc3-43cc-89b3-377341426ac5",
+ TenantID: "e4f50856753b4dc6afee5fa6b9b6c550",
+ },
+ }
+
+ th.CheckDeepEquals(t, expected, actual)
+
+ return true, nil
+ })
+
+ if count != 1 {
+ t.Errorf("Expected 1 page, got %d", count)
+ }
+}
+
+func TestCreate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/security-group-rules", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "POST")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ th.TestHeader(t, r, "Content-Type", "application/json")
+ th.TestHeader(t, r, "Accept", "application/json")
+ th.TestJSONRequest(t, r, `
+{
+ "security_group_rule": {
+ "direction": "ingress",
+ "port_range_min": 80,
+ "ethertype": "IPv4",
+ "port_range_max": 80,
+ "protocol": "tcp",
+ "remote_group_id": "85cc3048-abc3-43cc-89b3-377341426ac5",
+ "security_group_id": "a7734e61-b545-452d-a3cd-0189cbd9747a"
+ }
+}
+ `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusCreated)
+
+ fmt.Fprintf(w, `
+{
+ "security_group_rule": {
+ "direction": "ingress",
+ "ethertype": "IPv4",
+ "id": "2bc0accf-312e-429a-956e-e4407625eb62",
+ "port_range_max": 80,
+ "port_range_min": 80,
+ "protocol": "tcp",
+ "remote_group_id": "85cc3048-abc3-43cc-89b3-377341426ac5",
+ "remote_ip_prefix": null,
+ "security_group_id": "a7734e61-b545-452d-a3cd-0189cbd9747a",
+ "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550"
+ }
+}
+ `)
+ })
+
+ opts := CreateOpts{
+ Direction: "ingress",
+ PortRangeMin: 80,
+ EtherType: "IPv4",
+ PortRangeMax: 80,
+ Protocol: "tcp",
+ RemoteGroupID: "85cc3048-abc3-43cc-89b3-377341426ac5",
+ SecGroupID: "a7734e61-b545-452d-a3cd-0189cbd9747a",
+ }
+ _, err := Create(fake.ServiceClient(), opts).Extract()
+ th.AssertNoErr(t, err)
+}
+
+func TestRequiredCreateOpts(t *testing.T) {
+ res := Create(fake.ServiceClient(), CreateOpts{Direction: "something"})
+ if res.Err == nil {
+ t.Fatalf("Expected error, got none")
+ }
+ res = Create(fake.ServiceClient(), CreateOpts{Direction: DirIngress, EtherType: "something"})
+ if res.Err == nil {
+ t.Fatalf("Expected error, got none")
+ }
+ res = Create(fake.ServiceClient(), CreateOpts{Direction: DirIngress, EtherType: Ether4})
+ if res.Err == nil {
+ t.Fatalf("Expected error, got none")
+ }
+ res = Create(fake.ServiceClient(), CreateOpts{Direction: DirIngress, EtherType: Ether4, SecGroupID: "something", Protocol: "foo"})
+ if res.Err == nil {
+ t.Fatalf("Expected error, got none")
+ }
+}
+
+func TestGet(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/security-group-rules/3c0e45ff-adaf-4124-b083-bf390e5482ff", 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_rule": {
+ "direction": "egress",
+ "ethertype": "IPv6",
+ "id": "3c0e45ff-adaf-4124-b083-bf390e5482ff",
+ "port_range_max": null,
+ "port_range_min": null,
+ "protocol": null,
+ "remote_group_id": null,
+ "remote_ip_prefix": null,
+ "security_group_id": "85cc3048-abc3-43cc-89b3-377341426ac5",
+ "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550"
+ }
+}
+ `)
+ })
+
+ sr, err := Get(fake.ServiceClient(), "3c0e45ff-adaf-4124-b083-bf390e5482ff").Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, "egress", sr.Direction)
+ th.AssertEquals(t, "IPv6", sr.EtherType)
+ th.AssertEquals(t, "3c0e45ff-adaf-4124-b083-bf390e5482ff", sr.ID)
+ th.AssertEquals(t, 0, sr.PortRangeMax)
+ th.AssertEquals(t, 0, sr.PortRangeMin)
+ th.AssertEquals(t, "", sr.Protocol)
+ th.AssertEquals(t, "", sr.RemoteGroupID)
+ th.AssertEquals(t, "", sr.RemoteIPPrefix)
+ th.AssertEquals(t, "85cc3048-abc3-43cc-89b3-377341426ac5", sr.SecGroupID)
+ th.AssertEquals(t, "e4f50856753b4dc6afee5fa6b9b6c550", sr.TenantID)
+}
+
+func TestDelete(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/security-group-rules/4ec89087-d057-4e2c-911f-60a3b47ee304", 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)
+ })
+
+ res := Delete(fake.ServiceClient(), "4ec89087-d057-4e2c-911f-60a3b47ee304")
+ th.AssertNoErr(t, res.Err)
+}
diff --git a/_site/openstack/networking/v2/extensions/security/rules/results.go b/_site/openstack/networking/v2/extensions/security/rules/results.go
new file mode 100644
index 0000000..ca8435e
--- /dev/null
+++ b/_site/openstack/networking/v2/extensions/security/rules/results.go
@@ -0,0 +1,131 @@
+package rules
+
+import (
+ "github.com/mitchellh/mapstructure"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// SecGroupRule represents a rule to dictate the behaviour of incoming or
+// outgoing traffic for a particular security group.
+type SecGroupRule struct {
+ // The UUID for this security group rule.
+ ID string
+
+ // The direction in which the security group rule is applied. The only values
+ // allowed are "ingress" or "egress". For a compute instance, an ingress
+ // security group rule is applied to incoming (ingress) traffic for that
+ // instance. An egress rule is applied to traffic leaving the instance.
+ Direction string
+
+ // Must be IPv4 or IPv6, and addresses represented in CIDR must match the
+ // ingress or egress rules.
+ EtherType string `json:"ethertype" mapstructure:"ethertype"`
+
+ // The security group ID to associate with this security group rule.
+ SecGroupID string `json:"security_group_id" mapstructure:"security_group_id"`
+
+ // The minimum port number in the range that is matched by the security group
+ // rule. If the protocol is TCP or UDP, this value must be less than or equal
+ // to the value of the PortRangeMax attribute. If the protocol is ICMP, this
+ // value must be an ICMP type.
+ PortRangeMin int `json:"port_range_min" mapstructure:"port_range_min"`
+
+ // The maximum port number in the range that is matched by the security group
+ // rule. The PortRangeMin attribute constrains the PortRangeMax attribute. If
+ // the protocol is ICMP, this value must be an ICMP type.
+ PortRangeMax int `json:"port_range_max" mapstructure:"port_range_max"`
+
+ // The protocol that is matched by the security group rule. Valid values are
+ // "tcp", "udp", "icmp" or an empty string.
+ Protocol string
+
+ // The remote group ID to be associated with this security group rule. You
+ // can specify either RemoteGroupID or RemoteIPPrefix.
+ RemoteGroupID string `json:"remote_group_id" mapstructure:"remote_group_id"`
+
+ // The remote IP prefix to be associated with this security group rule. You
+ // can specify either RemoteGroupID or RemoteIPPrefix . This attribute
+ // matches the specified IP prefix as the source IP address of the IP packet.
+ RemoteIPPrefix string `json:"remote_ip_prefix" mapstructure:"remote_ip_prefix"`
+
+ // The owner of this security group rule.
+ TenantID string `json:"tenant_id" mapstructure:"tenant_id"`
+}
+
+// SecGroupRulePage is the page returned by a pager when traversing over a
+// collection of security group rules.
+type SecGroupRulePage struct {
+ pagination.LinkedPageBase
+}
+
+// NextPageURL is invoked when a paginated collection of security group rules has
+// reached the end of a page and the pager seeks to traverse over a new one. In
+// order to do this, it needs to construct the next page's URL.
+func (p SecGroupRulePage) NextPageURL() (string, error) {
+ type resp struct {
+ Links []gophercloud.Link `mapstructure:"security_group_rules_links"`
+ }
+
+ var r resp
+ err := mapstructure.Decode(p.Body, &r)
+ if err != nil {
+ return "", err
+ }
+
+ return gophercloud.ExtractNextURL(r.Links)
+}
+
+// IsEmpty checks whether a SecGroupRulePage struct is empty.
+func (p SecGroupRulePage) IsEmpty() (bool, error) {
+ is, err := ExtractRules(p)
+ if err != nil {
+ return true, nil
+ }
+ return len(is) == 0, nil
+}
+
+// ExtractRules accepts a Page struct, specifically a SecGroupRulePage struct,
+// and extracts the elements into a slice of SecGroupRule structs. In other words,
+// a generic collection is mapped into a relevant slice.
+func ExtractRules(page pagination.Page) ([]SecGroupRule, error) {
+ var resp struct {
+ SecGroupRules []SecGroupRule `mapstructure:"security_group_rules" json:"security_group_rules"`
+ }
+
+ err := mapstructure.Decode(page.(SecGroupRulePage).Body, &resp)
+
+ return resp.SecGroupRules, err
+}
+
+type commonResult struct {
+ gophercloud.CommonResult
+}
+
+// Extract is a function that accepts a result and extracts a security rule.
+func (r commonResult) Extract() (*SecGroupRule, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var res struct {
+ SecGroupRule *SecGroupRule `mapstructure:"security_group_rule" json:"security_group_rule"`
+ }
+
+ err := mapstructure.Decode(r.Resp, &res)
+
+ return res.SecGroupRule, err
+}
+
+// CreateResult represents the result of a create operation.
+type CreateResult struct {
+ commonResult
+}
+
+// GetResult represents the result of a get operation.
+type GetResult struct {
+ commonResult
+}
+
+// DeleteResult represents the result of a delete operation.
+type DeleteResult commonResult
diff --git a/_site/openstack/networking/v2/extensions/security/rules/urls.go b/_site/openstack/networking/v2/extensions/security/rules/urls.go
new file mode 100644
index 0000000..8e2b2bb
--- /dev/null
+++ b/_site/openstack/networking/v2/extensions/security/rules/urls.go
@@ -0,0 +1,13 @@
+package rules
+
+import "github.com/rackspace/gophercloud"
+
+const rootPath = "security-group-rules"
+
+func rootURL(c *gophercloud.ServiceClient) string {
+ return c.ServiceURL(rootPath)
+}
+
+func resourceURL(c *gophercloud.ServiceClient, id string) string {
+ return c.ServiceURL(rootPath, id)
+}
diff --git a/_site/openstack/networking/v2/networks/doc.go b/_site/openstack/networking/v2/networks/doc.go
new file mode 100644
index 0000000..c87a7ce
--- /dev/null
+++ b/_site/openstack/networking/v2/networks/doc.go
@@ -0,0 +1,9 @@
+// Package networks contains functionality for working with Neutron network
+// resources. A network is an isolated virtual layer-2 broadcast domain that is
+// typically reserved for the tenant who created it (unless you configure the
+// network to be shared). Tenants can create multiple networks until the
+// thresholds per-tenant quota is reached.
+//
+// In the v2.0 Networking API, the network is the main entity. Ports and subnets
+// are always associated with a network.
+package networks
diff --git a/_site/openstack/networking/v2/networks/errors.go b/_site/openstack/networking/v2/networks/errors.go
new file mode 100644
index 0000000..83c4a6a
--- /dev/null
+++ b/_site/openstack/networking/v2/networks/errors.go
@@ -0,0 +1 @@
+package networks
diff --git a/_site/openstack/networking/v2/networks/requests.go b/_site/openstack/networking/v2/networks/requests.go
new file mode 100644
index 0000000..de34989
--- /dev/null
+++ b/_site/openstack/networking/v2/networks/requests.go
@@ -0,0 +1,209 @@
+package networks
+
+import (
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+
+ "github.com/racker/perigee"
+)
+
+// AdminState gives users a solid type to work with for create and update
+// operations. It is recommended that users use the `Up` and `Down` enums.
+type AdminState *bool
+
+// Convenience vars for AdminStateUp values.
+var (
+ iTrue = true
+ iFalse = false
+
+ Up AdminState = &iTrue
+ Down AdminState = &iFalse
+)
+
+type networkOpts struct {
+ AdminStateUp *bool
+ Name string
+ Shared *bool
+ TenantID string
+}
+
+// ListOptsBuilder allows extensions to add additional parameters to the
+// List request.
+type ListOptsBuilder interface {
+ ToNetworkListString() (string, error)
+}
+
+// ListOpts allows the filtering and sorting of paginated collections through
+// the API. Filtering is achieved by passing in struct field values that map to
+// the network attributes you want to see returned. SortKey allows you to sort
+// by a particular network attribute. SortDir sets the direction, and is either
+// `asc' or `desc'. Marker and Limit are used for pagination.
+type ListOpts struct {
+ Status string `q:"status"`
+ Name string `q:"name"`
+ AdminStateUp *bool `q:"admin_state_up"`
+ TenantID string `q:"tenant_id"`
+ Shared *bool `q:"shared"`
+ ID string `q:"id"`
+ Marker string `q:"marker"`
+ Limit int `q:"limit"`
+ SortKey string `q:"sort_key"`
+ SortDir string `q:"sort_dir"`
+}
+
+// ToNetworkListString formats a ListOpts into a query string.
+func (opts ListOpts) ToNetworkListString() (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
+// networks. It accepts a ListOpts struct, which allows you to filter and sort
+// the returned collection for greater efficiency.
+func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
+ url := listURL(c)
+ if opts != nil {
+ query, err := opts.ToNetworkListString()
+ if err != nil {
+ return pagination.Pager{Err: err}
+ }
+ url += query
+ }
+
+ return pagination.NewPager(c, url, func(r pagination.LastHTTPResponse) pagination.Page {
+ return NetworkPage{pagination.LinkedPageBase{LastHTTPResponse: r}}
+ })
+}
+
+// Get retrieves a specific network based on its unique ID.
+func Get(c *gophercloud.ServiceClient, id string) GetResult {
+ var res GetResult
+ _, res.Err = perigee.Request("GET", getURL(c, id), perigee.Options{
+ MoreHeaders: c.Provider.AuthenticatedHeaders(),
+ Results: &res.Resp,
+ OkCodes: []int{200},
+ })
+ return res
+}
+
+// CreateOptsBuilder is the interface options structs have to satisfy in order
+// to be used in the main Create operation in this package. Since many
+// extensions decorate or modify the common logic, it is useful for them to
+// satisfy a basic interface in order for them to be used.
+type CreateOptsBuilder interface {
+ ToNetworkCreateMap() (map[string]interface{}, error)
+}
+
+// CreateOpts is the common options struct used in this package's Create
+// operation.
+type CreateOpts networkOpts
+
+// ToNetworkCreateMap casts a CreateOpts struct to a map.
+func (opts CreateOpts) ToNetworkCreateMap() (map[string]interface{}, error) {
+ n := make(map[string]interface{})
+
+ if opts.AdminStateUp != nil {
+ n["admin_state_up"] = &opts.AdminStateUp
+ }
+ if opts.Name != "" {
+ n["name"] = opts.Name
+ }
+ if opts.Shared != nil {
+ n["shared"] = &opts.Shared
+ }
+ if opts.TenantID != "" {
+ n["tenant_id"] = opts.TenantID
+ }
+
+ return map[string]interface{}{"network": n}, nil
+}
+
+// Create accepts a CreateOpts struct and creates a new network using the values
+// provided. This operation does not actually require a request body, i.e. the
+// CreateOpts struct argument can be empty.
+//
+// The tenant ID that is contained in the URI is the tenant that creates the
+// network. An admin user, however, has the option of specifying another tenant
+// ID in the CreateOpts struct.
+func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult {
+ var res CreateResult
+
+ reqBody, err := opts.ToNetworkCreateMap()
+ if err != nil {
+ res.Err = err
+ return res
+ }
+
+ // Send request to API
+ _, res.Err = perigee.Request("POST", createURL(c), perigee.Options{
+ MoreHeaders: c.Provider.AuthenticatedHeaders(),
+ ReqBody: &reqBody,
+ Results: &res.Resp,
+ OkCodes: []int{201},
+ })
+ return res
+}
+
+// UpdateOptsBuilder is the interface options structs have to satisfy in order
+// to be used in the main Update operation in this package. Since many
+// extensions decorate or modify the common logic, it is useful for them to
+// satisfy a basic interface in order for them to be used.
+type UpdateOptsBuilder interface {
+ ToNetworkUpdateMap() (map[string]interface{}, error)
+}
+
+// UpdateOpts is the common options struct used in this package's Update
+// operation.
+type UpdateOpts networkOpts
+
+// ToNetworkUpdateMap casts a UpdateOpts struct to a map.
+func (opts UpdateOpts) ToNetworkUpdateMap() (map[string]interface{}, error) {
+ n := make(map[string]interface{})
+
+ if opts.AdminStateUp != nil {
+ n["admin_state_up"] = &opts.AdminStateUp
+ }
+ if opts.Name != "" {
+ n["name"] = opts.Name
+ }
+ if opts.Shared != nil {
+ n["shared"] = &opts.Shared
+ }
+
+ return map[string]interface{}{"network": n}, nil
+}
+
+// Update accepts a UpdateOpts struct and updates an existing network using the
+// values provided. For more information, see the Create function.
+func Update(c *gophercloud.ServiceClient, networkID string, opts UpdateOptsBuilder) UpdateResult {
+ var res UpdateResult
+
+ reqBody, err := opts.ToNetworkUpdateMap()
+ if err != nil {
+ res.Err = err
+ return res
+ }
+
+ // Send request to API
+ _, res.Err = perigee.Request("PUT", getURL(c, networkID), perigee.Options{
+ MoreHeaders: c.Provider.AuthenticatedHeaders(),
+ ReqBody: &reqBody,
+ Results: &res.Resp,
+ OkCodes: []int{200, 201},
+ })
+
+ return res
+}
+
+// Delete accepts a unique ID and deletes the network associated with it.
+func Delete(c *gophercloud.ServiceClient, networkID string) DeleteResult {
+ var res DeleteResult
+ _, res.Err = perigee.Request("DELETE", deleteURL(c, networkID), perigee.Options{
+ MoreHeaders: c.Provider.AuthenticatedHeaders(),
+ OkCodes: []int{204},
+ })
+ return res
+}
diff --git a/_site/openstack/networking/v2/networks/requests_test.go b/_site/openstack/networking/v2/networks/requests_test.go
new file mode 100644
index 0000000..a263b7b
--- /dev/null
+++ b/_site/openstack/networking/v2/networks/requests_test.go
@@ -0,0 +1,275 @@
+package networks
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ fake "github.com/rackspace/gophercloud/openstack/networking/v2/common"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestList(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/networks", 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, `
+{
+ "networks": [
+ {
+ "status": "ACTIVE",
+ "subnets": [
+ "54d6f61d-db07-451c-9ab3-b9609b6b6f0b"
+ ],
+ "name": "private-network",
+ "admin_state_up": true,
+ "tenant_id": "4fd44f30292945e481c7b8a0c8908869",
+ "shared": true,
+ "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22"
+ },
+ {
+ "status": "ACTIVE",
+ "subnets": [
+ "08eae331-0402-425a-923c-34f7cfe39c1b"
+ ],
+ "name": "private",
+ "admin_state_up": true,
+ "tenant_id": "26a7980765d0414dbc1fc1f88cdb7e6e",
+ "shared": true,
+ "id": "db193ab3-96e3-4cb3-8fc5-05f4296d0324"
+ }
+ ]
+}
+ `)
+ })
+
+ client := fake.ServiceClient()
+ count := 0
+
+ List(client, ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractNetworks(page)
+ if err != nil {
+ t.Errorf("Failed to extract networks: %v", err)
+ return false, err
+ }
+
+ expected := []Network{
+ Network{
+ Status: "ACTIVE",
+ Subnets: []string{"54d6f61d-db07-451c-9ab3-b9609b6b6f0b"},
+ Name: "private-network",
+ AdminStateUp: true,
+ TenantID: "4fd44f30292945e481c7b8a0c8908869",
+ Shared: true,
+ ID: "d32019d3-bc6e-4319-9c1d-6722fc136a22",
+ },
+ Network{
+ Status: "ACTIVE",
+ Subnets: []string{"08eae331-0402-425a-923c-34f7cfe39c1b"},
+ Name: "private",
+ AdminStateUp: true,
+ TenantID: "26a7980765d0414dbc1fc1f88cdb7e6e",
+ Shared: true,
+ ID: "db193ab3-96e3-4cb3-8fc5-05f4296d0324",
+ },
+ }
+
+ th.CheckDeepEquals(t, expected, actual)
+
+ return true, nil
+ })
+
+ if count != 1 {
+ t.Errorf("Expected 1 page, got %d", count)
+ }
+}
+
+func TestGet(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/networks/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, `
+{
+ "network": {
+ "status": "ACTIVE",
+ "subnets": [
+ "54d6f61d-db07-451c-9ab3-b9609b6b6f0b"
+ ],
+ "name": "private-network",
+ "admin_state_up": true,
+ "tenant_id": "4fd44f30292945e481c7b8a0c8908869",
+ "shared": true,
+ "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22"
+ }
+}
+ `)
+ })
+
+ n, err := Get(fake.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22").Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, n.Status, "ACTIVE")
+ th.AssertDeepEquals(t, n.Subnets, []string{"54d6f61d-db07-451c-9ab3-b9609b6b6f0b"})
+ th.AssertEquals(t, n.Name, "private-network")
+ th.AssertEquals(t, n.AdminStateUp, true)
+ th.AssertEquals(t, n.TenantID, "4fd44f30292945e481c7b8a0c8908869")
+ th.AssertEquals(t, n.Shared, true)
+ th.AssertEquals(t, n.ID, "d32019d3-bc6e-4319-9c1d-6722fc136a22")
+}
+
+func TestCreate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/networks", 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, `
+{
+ "network": {
+ "name": "sample_network",
+ "admin_state_up": true
+ }
+}
+ `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusCreated)
+
+ fmt.Fprintf(w, `
+{
+ "network": {
+ "status": "ACTIVE",
+ "subnets": [],
+ "name": "net1",
+ "admin_state_up": true,
+ "tenant_id": "9bacb3c5d39d41a79512987f338cf177",
+ "shared": false,
+ "id": "4e8e5957-649f-477b-9e5b-f1f75b21c03c"
+ }
+}
+ `)
+ })
+
+ iTrue := true
+ options := CreateOpts{Name: "sample_network", AdminStateUp: &iTrue}
+ n, err := Create(fake.ServiceClient(), options).Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, n.Status, "ACTIVE")
+ th.AssertDeepEquals(t, n.Subnets, []string{})
+ th.AssertEquals(t, n.Name, "net1")
+ th.AssertEquals(t, n.AdminStateUp, true)
+ th.AssertEquals(t, n.TenantID, "9bacb3c5d39d41a79512987f338cf177")
+ th.AssertEquals(t, n.Shared, false)
+ th.AssertEquals(t, n.ID, "4e8e5957-649f-477b-9e5b-f1f75b21c03c")
+}
+
+func TestCreateWithOptionalFields(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/networks", 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, `
+{
+ "network": {
+ "name": "sample_network",
+ "admin_state_up": true,
+ "shared": true,
+ "tenant_id": "12345"
+ }
+}
+ `)
+
+ w.WriteHeader(http.StatusCreated)
+ })
+
+ iTrue := true
+ options := CreateOpts{Name: "sample_network", AdminStateUp: &iTrue, Shared: &iTrue, TenantID: "12345"}
+ _, err := Create(fake.ServiceClient(), options).Extract()
+ th.AssertNoErr(t, err)
+}
+
+func TestUpdate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/networks/4e8e5957-649f-477b-9e5b-f1f75b21c03c", 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.TestHeader(t, r, "Accept", "application/json")
+ th.TestJSONRequest(t, r, `
+{
+ "network": {
+ "name": "new_network_name",
+ "admin_state_up": false,
+ "shared": true
+ }
+}
+ `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, `
+{
+ "network": {
+ "status": "ACTIVE",
+ "subnets": [],
+ "name": "new_network_name",
+ "admin_state_up": false,
+ "tenant_id": "4fd44f30292945e481c7b8a0c8908869",
+ "shared": true,
+ "id": "4e8e5957-649f-477b-9e5b-f1f75b21c03c"
+ }
+}
+ `)
+ })
+
+ iTrue, iFalse := true, false
+ options := UpdateOpts{Name: "new_network_name", AdminStateUp: &iFalse, Shared: &iTrue}
+ n, err := Update(fake.ServiceClient(), "4e8e5957-649f-477b-9e5b-f1f75b21c03c", options).Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, n.Name, "new_network_name")
+ th.AssertEquals(t, n.AdminStateUp, false)
+ th.AssertEquals(t, n.Shared, true)
+ th.AssertEquals(t, n.ID, "4e8e5957-649f-477b-9e5b-f1f75b21c03c")
+}
+
+func TestDelete(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/networks/4e8e5957-649f-477b-9e5b-f1f75b21c03c", 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)
+ })
+
+ res := Delete(fake.ServiceClient(), "4e8e5957-649f-477b-9e5b-f1f75b21c03c")
+ th.AssertNoErr(t, res.Err)
+}
diff --git a/_site/openstack/networking/v2/networks/results.go b/_site/openstack/networking/v2/networks/results.go
new file mode 100644
index 0000000..e605fcf
--- /dev/null
+++ b/_site/openstack/networking/v2/networks/results.go
@@ -0,0 +1,114 @@
+package networks
+
+import (
+ "github.com/mitchellh/mapstructure"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+type commonResult struct {
+ gophercloud.CommonResult
+}
+
+// Extract is a function that accepts a result and extracts a network resource.
+func (r commonResult) Extract() (*Network, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var res struct {
+ Network *Network `json:"network"`
+ }
+
+ err := mapstructure.Decode(r.Resp, &res)
+
+ return res.Network, err
+}
+
+// CreateResult represents the result of a create operation.
+type CreateResult struct {
+ commonResult
+}
+
+// GetResult represents the result of a get operation.
+type GetResult struct {
+ commonResult
+}
+
+// UpdateResult represents the result of an update operation.
+type UpdateResult struct {
+ commonResult
+}
+
+// DeleteResult represents the result of a delete operation.
+type DeleteResult commonResult
+
+// Network represents, well, a network.
+type Network struct {
+ // UUID for the network
+ ID string `mapstructure:"id" json:"id"`
+
+ // Human-readable name for the network. Might not be unique.
+ Name string `mapstructure:"name" json:"name"`
+
+ // The administrative state of network. If false (down), the network does not forward packets.
+ AdminStateUp bool `mapstructure:"admin_state_up" json:"admin_state_up"`
+
+ // Indicates whether network is currently operational. Possible values include
+ // `ACTIVE', `DOWN', `BUILD', or `ERROR'. Plug-ins might define additional values.
+ Status string `mapstructure:"status" json:"status"`
+
+ // Subnets associated with this network.
+ Subnets []string `mapstructure:"subnets" json:"subnets"`
+
+ // Owner of network. Only admin users can specify a tenant_id other than its own.
+ TenantID string `mapstructure:"tenant_id" json:"tenant_id"`
+
+ // Specifies whether the network resource can be accessed by any tenant or not.
+ Shared bool `mapstructure:"shared" json:"shared"`
+}
+
+// NetworkPage is the page returned by a pager when traversing over a
+// collection of networks.
+type NetworkPage struct {
+ pagination.LinkedPageBase
+}
+
+// NextPageURL is invoked when a paginated collection of networks has reached
+// the end of a page and the pager seeks to traverse over a new one. In order
+// to do this, it needs to construct the next page's URL.
+func (p NetworkPage) NextPageURL() (string, error) {
+ type resp struct {
+ Links []gophercloud.Link `mapstructure:"networks_links"`
+ }
+
+ var r resp
+ err := mapstructure.Decode(p.Body, &r)
+ if err != nil {
+ return "", err
+ }
+
+ return gophercloud.ExtractNextURL(r.Links)
+}
+
+// IsEmpty checks whether a NetworkPage struct is empty.
+func (p NetworkPage) IsEmpty() (bool, error) {
+ is, err := ExtractNetworks(p)
+ if err != nil {
+ return true, nil
+ }
+ return len(is) == 0, nil
+}
+
+// ExtractNetworks accepts a Page struct, specifically a NetworkPage struct,
+// and extracts the elements into a slice of Network structs. In other words,
+// a generic collection is mapped into a relevant slice.
+func ExtractNetworks(page pagination.Page) ([]Network, error) {
+ var resp struct {
+ Networks []Network `mapstructure:"networks" json:"networks"`
+ }
+
+ err := mapstructure.Decode(page.(NetworkPage).Body, &resp)
+
+ return resp.Networks, err
+}
diff --git a/_site/openstack/networking/v2/networks/urls.go b/_site/openstack/networking/v2/networks/urls.go
new file mode 100644
index 0000000..33c2387
--- /dev/null
+++ b/_site/openstack/networking/v2/networks/urls.go
@@ -0,0 +1,27 @@
+package networks
+
+import "github.com/rackspace/gophercloud"
+
+func resourceURL(c *gophercloud.ServiceClient, id string) string {
+ return c.ServiceURL("networks", id)
+}
+
+func rootURL(c *gophercloud.ServiceClient) string {
+ return c.ServiceURL("networks")
+}
+
+func getURL(c *gophercloud.ServiceClient, id string) string {
+ return resourceURL(c, id)
+}
+
+func listURL(c *gophercloud.ServiceClient) string {
+ return rootURL(c)
+}
+
+func createURL(c *gophercloud.ServiceClient) string {
+ return rootURL(c)
+}
+
+func deleteURL(c *gophercloud.ServiceClient, id string) string {
+ return resourceURL(c, id)
+}
diff --git a/_site/openstack/networking/v2/networks/urls_test.go b/_site/openstack/networking/v2/networks/urls_test.go
new file mode 100644
index 0000000..caf77db
--- /dev/null
+++ b/_site/openstack/networking/v2/networks/urls_test.go
@@ -0,0 +1,38 @@
+package networks
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+const endpoint = "http://localhost:57909/"
+
+func endpointClient() *gophercloud.ServiceClient {
+ return &gophercloud.ServiceClient{Endpoint: endpoint, ResourceBase: endpoint + "v2.0/"}
+}
+
+func TestGetURL(t *testing.T) {
+ actual := getURL(endpointClient(), "foo")
+ expected := endpoint + "v2.0/networks/foo"
+ th.AssertEquals(t, expected, actual)
+}
+
+func TestCreateURL(t *testing.T) {
+ actual := createURL(endpointClient())
+ expected := endpoint + "v2.0/networks"
+ th.AssertEquals(t, expected, actual)
+}
+
+func TestListURL(t *testing.T) {
+ actual := createURL(endpointClient())
+ expected := endpoint + "v2.0/networks"
+ th.AssertEquals(t, expected, actual)
+}
+
+func TestDeleteURL(t *testing.T) {
+ actual := deleteURL(endpointClient(), "foo")
+ expected := endpoint + "v2.0/networks/foo"
+ th.AssertEquals(t, expected, actual)
+}
diff --git a/_site/openstack/networking/v2/ports/doc.go b/_site/openstack/networking/v2/ports/doc.go
new file mode 100644
index 0000000..f16a4bb
--- /dev/null
+++ b/_site/openstack/networking/v2/ports/doc.go
@@ -0,0 +1,8 @@
+// Package ports contains functionality for working with Neutron port resources.
+// A port represents a virtual switch port on a logical network switch. Virtual
+// instances attach their interfaces into ports. The logical port also defines
+// the MAC address and the IP address(es) to be assigned to the interfaces
+// plugged into them. When IP addresses are associated to a port, this also
+// implies the port is associated with a subnet, as the IP address was taken
+// from the allocation pool for a specific subnet.
+package ports
diff --git a/_site/openstack/networking/v2/ports/errors.go b/_site/openstack/networking/v2/ports/errors.go
new file mode 100644
index 0000000..111d977
--- /dev/null
+++ b/_site/openstack/networking/v2/ports/errors.go
@@ -0,0 +1,11 @@
+package ports
+
+import "fmt"
+
+func err(str string) error {
+ return fmt.Errorf("%s", str)
+}
+
+var (
+ errNetworkIDRequired = err("A Network ID is required")
+)
diff --git a/_site/openstack/networking/v2/ports/requests.go b/_site/openstack/networking/v2/ports/requests.go
new file mode 100644
index 0000000..afa37f5
--- /dev/null
+++ b/_site/openstack/networking/v2/ports/requests.go
@@ -0,0 +1,245 @@
+package ports
+
+import (
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+
+ "github.com/racker/perigee"
+)
+
+// AdminState gives users a solid type to work with for create and update
+// operations. It is recommended that users use the `Up` and `Down` enums.
+type AdminState *bool
+
+// Convenience vars for AdminStateUp values.
+var (
+ iTrue = true
+ iFalse = false
+
+ Up AdminState = &iTrue
+ Down AdminState = &iFalse
+)
+
+// ListOptsBuilder allows extensions to add additional parameters to the
+// List request.
+type ListOptsBuilder interface {
+ ToPortListString() (string, error)
+}
+
+// ListOpts allows the filtering and sorting of paginated collections through
+// the API. Filtering is achieved by passing in struct field values that map to
+// the port attributes you want to see returned. SortKey allows you to sort
+// by a particular port attribute. SortDir sets the direction, and is either
+// `asc' or `desc'. Marker and Limit are used for pagination.
+type ListOpts struct {
+ Status string `q:"status"`
+ Name string `q:"name"`
+ AdminStateUp *bool `q:"admin_state_up"`
+ NetworkID string `q:"network_id"`
+ TenantID string `q:"tenant_id"`
+ DeviceOwner string `q:"device_owner"`
+ MACAddress string `q:"mac_address"`
+ ID string `q:"id"`
+ DeviceID string `q:"device_id"`
+ Limit int `q:"limit"`
+ Marker string `q:"marker"`
+ SortKey string `q:"sort_key"`
+ SortDir string `q:"sort_dir"`
+}
+
+// ToPortListString formats a ListOpts into a query string.
+func (opts ListOpts) ToPortListString() (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
+// ports. It accepts a ListOpts struct, which allows you to filter and sort
+// the returned collection for greater efficiency.
+//
+// Default policy settings return only those ports that are owned by the tenant
+// who submits the request, unless the request is submitted by an user with
+// administrative rights.
+func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
+ url := listURL(c)
+ if opts != nil {
+ query, err := opts.ToPortListString()
+ if err != nil {
+ return pagination.Pager{Err: err}
+ }
+ url += query
+ }
+
+ return pagination.NewPager(c, url, func(r pagination.LastHTTPResponse) pagination.Page {
+ return PortPage{pagination.LinkedPageBase{LastHTTPResponse: r}}
+ })
+}
+
+// Get retrieves a specific port based on its unique ID.
+func Get(c *gophercloud.ServiceClient, id string) GetResult {
+ var res GetResult
+ _, res.Err = perigee.Request("GET", getURL(c, id), perigee.Options{
+ MoreHeaders: c.Provider.AuthenticatedHeaders(),
+ Results: &res.Resp,
+ OkCodes: []int{200},
+ })
+ return res
+}
+
+// CreateOptsBuilder is the interface options structs have to satisfy in order
+// to be used in the main Create operation in this package. Since many
+// extensions decorate or modify the common logic, it is useful for them to
+// satisfy a basic interface in order for them to be used.
+type CreateOptsBuilder interface {
+ ToPortCreateMap() (map[string]interface{}, error)
+}
+
+// CreateOpts represents the attributes used when creating a new port.
+type CreateOpts struct {
+ NetworkID string
+ Name string
+ AdminStateUp *bool
+ MACAddress string
+ FixedIPs interface{}
+ DeviceID string
+ DeviceOwner string
+ TenantID string
+ SecurityGroups []string
+}
+
+// ToPortCreateMap casts a CreateOpts struct to a map.
+func (opts CreateOpts) ToPortCreateMap() (map[string]interface{}, error) {
+ p := make(map[string]interface{})
+
+ if opts.NetworkID == "" {
+ return nil, errNetworkIDRequired
+ }
+ p["network_id"] = opts.NetworkID
+
+ if opts.DeviceID != "" {
+ p["device_id"] = opts.DeviceID
+ }
+ if opts.DeviceOwner != "" {
+ p["device_owner"] = opts.DeviceOwner
+ }
+ if opts.FixedIPs != nil {
+ p["fixed_ips"] = opts.FixedIPs
+ }
+ if opts.SecurityGroups != nil {
+ p["security_groups"] = opts.SecurityGroups
+ }
+ if opts.TenantID != "" {
+ p["tenant_id"] = opts.TenantID
+ }
+ if opts.AdminStateUp != nil {
+ p["admin_state_up"] = &opts.AdminStateUp
+ }
+ if opts.Name != "" {
+ p["name"] = opts.Name
+ }
+ if opts.MACAddress != "" {
+ p["mac_address"] = opts.MACAddress
+ }
+
+ return map[string]interface{}{"port": p}, nil
+}
+
+// Create accepts a CreateOpts struct and creates a new network using the values
+// provided. You must remember to provide a NetworkID value.
+func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult {
+ var res CreateResult
+
+ reqBody, err := opts.ToPortCreateMap()
+ if err != nil {
+ res.Err = err
+ return res
+ }
+
+ // Response
+ _, res.Err = perigee.Request("POST", createURL(c), perigee.Options{
+ MoreHeaders: c.Provider.AuthenticatedHeaders(),
+ ReqBody: &reqBody,
+ Results: &res.Resp,
+ OkCodes: []int{201},
+ DumpReqJson: true,
+ })
+
+ return res
+}
+
+// UpdateOptsBuilder is the interface options structs have to satisfy in order
+// to be used in the main Update operation in this package. Since many
+// extensions decorate or modify the common logic, it is useful for them to
+// satisfy a basic interface in order for them to be used.
+type UpdateOptsBuilder interface {
+ ToPortUpdateMap() (map[string]interface{}, error)
+}
+
+// UpdateOpts represents the attributes used when updating an existing port.
+type UpdateOpts struct {
+ Name string
+ AdminStateUp *bool
+ FixedIPs interface{}
+ DeviceID string
+ DeviceOwner string
+ SecurityGroups []string
+}
+
+// ToPortUpdateMap casts an UpdateOpts struct to a map.
+func (opts UpdateOpts) ToPortUpdateMap() (map[string]interface{}, error) {
+ p := make(map[string]interface{})
+
+ if opts.DeviceID != "" {
+ p["device_id"] = opts.DeviceID
+ }
+ if opts.DeviceOwner != "" {
+ p["device_owner"] = opts.DeviceOwner
+ }
+ if opts.FixedIPs != nil {
+ p["fixed_ips"] = opts.FixedIPs
+ }
+ if opts.SecurityGroups != nil {
+ p["security_groups"] = opts.SecurityGroups
+ }
+ if opts.AdminStateUp != nil {
+ p["admin_state_up"] = &opts.AdminStateUp
+ }
+ if opts.Name != "" {
+ p["name"] = opts.Name
+ }
+
+ return map[string]interface{}{"port": p}, nil
+}
+
+// Update accepts a UpdateOpts struct and updates an existing port using the
+// values provided.
+func Update(c *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) UpdateResult {
+ var res UpdateResult
+
+ reqBody, err := opts.ToPortUpdateMap()
+ if err != nil {
+ res.Err = err
+ return res
+ }
+
+ _, res.Err = perigee.Request("PUT", updateURL(c, id), perigee.Options{
+ MoreHeaders: c.Provider.AuthenticatedHeaders(),
+ ReqBody: &reqBody,
+ Results: &res.Resp,
+ OkCodes: []int{200, 201},
+ })
+ return res
+}
+
+// Delete accepts a unique ID and deletes the port associated with it.
+func Delete(c *gophercloud.ServiceClient, id string) DeleteResult {
+ var res DeleteResult
+ _, res.Err = perigee.Request("DELETE", deleteURL(c, id), perigee.Options{
+ MoreHeaders: c.Provider.AuthenticatedHeaders(),
+ OkCodes: []int{204},
+ })
+ return res
+}
diff --git a/_site/openstack/networking/v2/ports/requests_test.go b/_site/openstack/networking/v2/ports/requests_test.go
new file mode 100644
index 0000000..9e323ef
--- /dev/null
+++ b/_site/openstack/networking/v2/ports/requests_test.go
@@ -0,0 +1,321 @@
+package ports
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ fake "github.com/rackspace/gophercloud/openstack/networking/v2/common"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestList(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/ports", 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, `
+{
+ "ports": [
+ {
+ "status": "ACTIVE",
+ "binding:host_id": "devstack",
+ "name": "",
+ "admin_state_up": true,
+ "network_id": "70c1db1f-b701-45bd-96e0-a313ee3430b3",
+ "tenant_id": "",
+ "device_owner": "network:router_gateway",
+ "mac_address": "fa:16:3e:58:42:ed",
+ "fixed_ips": [
+ {
+ "subnet_id": "008ba151-0b8c-4a67-98b5-0d2b87666062",
+ "ip_address": "172.24.4.2"
+ }
+ ],
+ "id": "d80b1a3b-4fc1-49f3-952e-1e2ab7081d8b",
+ "security_groups": [],
+ "device_id": "9ae135f4-b6e0-4dad-9e91-3c223e385824"
+ }
+ ]
+}
+ `)
+ })
+
+ count := 0
+
+ List(fake.ServiceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractPorts(page)
+ if err != nil {
+ t.Errorf("Failed to extract subnets: %v", err)
+ return false, nil
+ }
+
+ expected := []Port{
+ Port{
+ Status: "ACTIVE",
+ Name: "",
+ AdminStateUp: true,
+ NetworkID: "70c1db1f-b701-45bd-96e0-a313ee3430b3",
+ TenantID: "",
+ DeviceOwner: "network:router_gateway",
+ MACAddress: "fa:16:3e:58:42:ed",
+ FixedIPs: []IP{
+ IP{
+ SubnetID: "008ba151-0b8c-4a67-98b5-0d2b87666062",
+ IPAddress: "172.24.4.2",
+ },
+ },
+ ID: "d80b1a3b-4fc1-49f3-952e-1e2ab7081d8b",
+ SecurityGroups: []string{},
+ DeviceID: "9ae135f4-b6e0-4dad-9e91-3c223e385824",
+ },
+ }
+
+ th.CheckDeepEquals(t, expected, actual)
+
+ return true, nil
+ })
+
+ if count != 1 {
+ t.Errorf("Expected 1 page, got %d", count)
+ }
+}
+
+func TestGet(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/ports/46d4bfb9-b26e-41f3-bd2e-e6dcc1ccedb2", 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, `
+{
+ "port": {
+ "status": "ACTIVE",
+ "name": "",
+ "admin_state_up": true,
+ "network_id": "a87cc70a-3e15-4acf-8205-9b711a3531b7",
+ "tenant_id": "7e02058126cc4950b75f9970368ba177",
+ "device_owner": "network:router_interface",
+ "mac_address": "fa:16:3e:23:fd:d7",
+ "fixed_ips": [
+ {
+ "subnet_id": "a0304c3a-4f08-4c43-88af-d796509c97d2",
+ "ip_address": "10.0.0.1"
+ }
+ ],
+ "id": "46d4bfb9-b26e-41f3-bd2e-e6dcc1ccedb2",
+ "security_groups": [],
+ "device_id": "5e3898d7-11be-483e-9732-b2f5eccd2b2e"
+ }
+}
+ `)
+ })
+
+ n, err := Get(fake.ServiceClient(), "46d4bfb9-b26e-41f3-bd2e-e6dcc1ccedb2").Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, n.Status, "ACTIVE")
+ th.AssertEquals(t, n.Name, "")
+ th.AssertEquals(t, n.AdminStateUp, true)
+ th.AssertEquals(t, n.NetworkID, "a87cc70a-3e15-4acf-8205-9b711a3531b7")
+ th.AssertEquals(t, n.TenantID, "7e02058126cc4950b75f9970368ba177")
+ th.AssertEquals(t, n.DeviceOwner, "network:router_interface")
+ th.AssertEquals(t, n.MACAddress, "fa:16:3e:23:fd:d7")
+ th.AssertDeepEquals(t, n.FixedIPs, []IP{
+ IP{SubnetID: "a0304c3a-4f08-4c43-88af-d796509c97d2", IPAddress: "10.0.0.1"},
+ })
+ th.AssertEquals(t, n.ID, "46d4bfb9-b26e-41f3-bd2e-e6dcc1ccedb2")
+ th.AssertDeepEquals(t, n.SecurityGroups, []string{})
+ th.AssertEquals(t, n.Status, "ACTIVE")
+ th.AssertEquals(t, n.DeviceID, "5e3898d7-11be-483e-9732-b2f5eccd2b2e")
+}
+
+func TestCreate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/ports", 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, `
+{
+ "port": {
+ "network_id": "a87cc70a-3e15-4acf-8205-9b711a3531b7",
+ "name": "private-port",
+ "admin_state_up": true,
+ "fixed_ips": [
+ {
+ "subnet_id": "a0304c3a-4f08-4c43-88af-d796509c97d2",
+ "ip_address": "10.0.0.2"
+ }
+ ],
+ "security_groups": ["foo"]
+ }
+}
+ `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusCreated)
+
+ fmt.Fprintf(w, `
+{
+ "port": {
+ "status": "DOWN",
+ "name": "private-port",
+ "allowed_address_pairs": [],
+ "admin_state_up": true,
+ "network_id": "a87cc70a-3e15-4acf-8205-9b711a3531b7",
+ "tenant_id": "d6700c0c9ffa4f1cb322cd4a1f3906fa",
+ "device_owner": "",
+ "mac_address": "fa:16:3e:c9:cb:f0",
+ "fixed_ips": [
+ {
+ "subnet_id": "a0304c3a-4f08-4c43-88af-d796509c97d2",
+ "ip_address": "10.0.0.2"
+ }
+ ],
+ "id": "65c0ee9f-d634-4522-8954-51021b570b0d",
+ "security_groups": [
+ "f0ac4394-7e4a-4409-9701-ba8be283dbc3"
+ ],
+ "device_id": ""
+ }
+}
+ `)
+ })
+
+ asu := true
+ options := CreateOpts{
+ Name: "private-port",
+ AdminStateUp: &asu,
+ NetworkID: "a87cc70a-3e15-4acf-8205-9b711a3531b7",
+ FixedIPs: []IP{
+ IP{SubnetID: "a0304c3a-4f08-4c43-88af-d796509c97d2", IPAddress: "10.0.0.2"},
+ },
+ SecurityGroups: []string{"foo"},
+ }
+ n, err := Create(fake.ServiceClient(), options).Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, n.Status, "DOWN")
+ th.AssertEquals(t, n.Name, "private-port")
+ th.AssertEquals(t, n.AdminStateUp, true)
+ th.AssertEquals(t, n.NetworkID, "a87cc70a-3e15-4acf-8205-9b711a3531b7")
+ th.AssertEquals(t, n.TenantID, "d6700c0c9ffa4f1cb322cd4a1f3906fa")
+ th.AssertEquals(t, n.DeviceOwner, "")
+ th.AssertEquals(t, n.MACAddress, "fa:16:3e:c9:cb:f0")
+ th.AssertDeepEquals(t, n.FixedIPs, []IP{
+ IP{SubnetID: "a0304c3a-4f08-4c43-88af-d796509c97d2", IPAddress: "10.0.0.2"},
+ })
+ th.AssertEquals(t, n.ID, "65c0ee9f-d634-4522-8954-51021b570b0d")
+ th.AssertDeepEquals(t, n.SecurityGroups, []string{"f0ac4394-7e4a-4409-9701-ba8be283dbc3"})
+}
+
+func TestRequiredCreateOpts(t *testing.T) {
+ res := Create(fake.ServiceClient(), CreateOpts{})
+ if res.Err == nil {
+ t.Fatalf("Expected error, got none")
+ }
+}
+
+func TestUpdate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/ports/65c0ee9f-d634-4522-8954-51021b570b0d", 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.TestHeader(t, r, "Accept", "application/json")
+ th.TestJSONRequest(t, r, `
+{
+ "port": {
+ "name": "new_port_name",
+ "fixed_ips": [
+ {
+ "subnet_id": "a0304c3a-4f08-4c43-88af-d796509c97d2",
+ "ip_address": "10.0.0.3"
+ }
+ ],
+ "security_groups": [
+ "f0ac4394-7e4a-4409-9701-ba8be283dbc3"
+ ]
+ }
+}
+ `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, `
+{
+ "port": {
+ "status": "DOWN",
+ "name": "new_port_name",
+ "admin_state_up": true,
+ "network_id": "a87cc70a-3e15-4acf-8205-9b711a3531b7",
+ "tenant_id": "d6700c0c9ffa4f1cb322cd4a1f3906fa",
+ "device_owner": "",
+ "mac_address": "fa:16:3e:c9:cb:f0",
+ "fixed_ips": [
+ {
+ "subnet_id": "a0304c3a-4f08-4c43-88af-d796509c97d2",
+ "ip_address": "10.0.0.3"
+ }
+ ],
+ "id": "65c0ee9f-d634-4522-8954-51021b570b0d",
+ "security_groups": [
+ "f0ac4394-7e4a-4409-9701-ba8be283dbc3"
+ ],
+ "device_id": ""
+ }
+}
+ `)
+ })
+
+ options := UpdateOpts{
+ Name: "new_port_name",
+ FixedIPs: []IP{
+ IP{SubnetID: "a0304c3a-4f08-4c43-88af-d796509c97d2", IPAddress: "10.0.0.3"},
+ },
+ SecurityGroups: []string{"f0ac4394-7e4a-4409-9701-ba8be283dbc3"},
+ }
+
+ s, err := Update(fake.ServiceClient(), "65c0ee9f-d634-4522-8954-51021b570b0d", options).Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, s.Name, "new_port_name")
+ th.AssertDeepEquals(t, s.FixedIPs, []IP{
+ IP{SubnetID: "a0304c3a-4f08-4c43-88af-d796509c97d2", IPAddress: "10.0.0.3"},
+ })
+ th.AssertDeepEquals(t, s.SecurityGroups, []string{"f0ac4394-7e4a-4409-9701-ba8be283dbc3"})
+}
+
+func TestDelete(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/ports/65c0ee9f-d634-4522-8954-51021b570b0d", 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)
+ })
+
+ res := Delete(fake.ServiceClient(), "65c0ee9f-d634-4522-8954-51021b570b0d")
+ th.AssertNoErr(t, res.Err)
+}
diff --git a/_site/openstack/networking/v2/ports/results.go b/_site/openstack/networking/v2/ports/results.go
new file mode 100644
index 0000000..cedd658
--- /dev/null
+++ b/_site/openstack/networking/v2/ports/results.go
@@ -0,0 +1,124 @@
+package ports
+
+import (
+ "github.com/mitchellh/mapstructure"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+type commonResult struct {
+ gophercloud.CommonResult
+}
+
+// Extract is a function that accepts a result and extracts a port resource.
+func (r commonResult) Extract() (*Port, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var res struct {
+ Port *Port `json:"port"`
+ }
+
+ err := mapstructure.Decode(r.Resp, &res)
+
+ return res.Port, err
+}
+
+// CreateResult represents the result of a create operation.
+type CreateResult struct {
+ commonResult
+}
+
+// GetResult represents the result of a get operation.
+type GetResult struct {
+ commonResult
+}
+
+// UpdateResult represents the result of an update operation.
+type UpdateResult struct {
+ commonResult
+}
+
+// DeleteResult represents the result of a delete operation.
+type DeleteResult commonResult
+
+// IP is a sub-struct that represents an individual IP.
+type IP struct {
+ SubnetID string `mapstructure:"subnet_id" json:"subnet_id"`
+ IPAddress string `mapstructure:"ip_address" json:"ip_address,omitempty"`
+}
+
+// Port represents a Neutron port. See package documentation for a top-level
+// description of what this is.
+type Port struct {
+ // UUID for the port.
+ ID string `mapstructure:"id" json:"id"`
+ // Network that this port is associated with.
+ NetworkID string `mapstructure:"network_id" json:"network_id"`
+ // Human-readable name for the port. Might not be unique.
+ Name string `mapstructure:"name" json:"name"`
+ // Administrative state of port. If false (down), port does not forward packets.
+ AdminStateUp bool `mapstructure:"admin_state_up" json:"admin_state_up"`
+ // Indicates whether network is currently operational. Possible values include
+ // `ACTIVE', `DOWN', `BUILD', or `ERROR'. Plug-ins might define additional values.
+ Status string `mapstructure:"status" json:"status"`
+ // Mac address to use on this port.
+ MACAddress string `mapstructure:"mac_address" json:"mac_address"`
+ // Specifies IP addresses for the port thus associating the port itself with
+ // the subnets where the IP addresses are picked from
+ FixedIPs []IP `mapstructure:"fixed_ips" json:"fixed_ips"`
+ // Owner of network. Only admin users can specify a tenant_id other than its own.
+ TenantID string `mapstructure:"tenant_id" json:"tenant_id"`
+ // Identifies the entity (e.g.: dhcp agent) using this port.
+ DeviceOwner string `mapstructure:"device_owner" json:"device_owner"`
+ // Specifies the IDs of any security groups associated with a port.
+ SecurityGroups []string `mapstructure:"security_groups" json:"security_groups"`
+ // Identifies the device (e.g., virtual server) using this port.
+ DeviceID string `mapstructure:"device_id" json:"device_id"`
+}
+
+// PortPage is the page returned by a pager when traversing over a collection
+// of network ports.
+type PortPage struct {
+ pagination.LinkedPageBase
+}
+
+// NextPageURL is invoked when a paginated collection of ports has reached
+// the end of a page and the pager seeks to traverse over a new one. In order
+// to do this, it needs to construct the next page's URL.
+func (p PortPage) NextPageURL() (string, error) {
+ type resp struct {
+ Links []gophercloud.Link `mapstructure:"ports_links"`
+ }
+
+ var r resp
+ err := mapstructure.Decode(p.Body, &r)
+ if err != nil {
+ return "", err
+ }
+
+ return gophercloud.ExtractNextURL(r.Links)
+}
+
+// IsEmpty checks whether a PortPage struct is empty.
+func (p PortPage) IsEmpty() (bool, error) {
+ is, err := ExtractPorts(p)
+ if err != nil {
+ return true, nil
+ }
+ return len(is) == 0, nil
+}
+
+// ExtractPorts accepts a Page struct, specifically a PortPage struct,
+// and extracts the elements into a slice of Port structs. In other words,
+// a generic collection is mapped into a relevant slice.
+func ExtractPorts(page pagination.Page) ([]Port, error) {
+ var resp struct {
+ Ports []Port `mapstructure:"ports" json:"ports"`
+ }
+
+ err := mapstructure.Decode(page.(PortPage).Body, &resp)
+
+ return resp.Ports, err
+}
diff --git a/_site/openstack/networking/v2/ports/urls.go b/_site/openstack/networking/v2/ports/urls.go
new file mode 100644
index 0000000..6d0572f
--- /dev/null
+++ b/_site/openstack/networking/v2/ports/urls.go
@@ -0,0 +1,31 @@
+package ports
+
+import "github.com/rackspace/gophercloud"
+
+func resourceURL(c *gophercloud.ServiceClient, id string) string {
+ return c.ServiceURL("ports", id)
+}
+
+func rootURL(c *gophercloud.ServiceClient) string {
+ return c.ServiceURL("ports")
+}
+
+func listURL(c *gophercloud.ServiceClient) string {
+ return rootURL(c)
+}
+
+func getURL(c *gophercloud.ServiceClient, id string) string {
+ return resourceURL(c, id)
+}
+
+func createURL(c *gophercloud.ServiceClient) string {
+ return rootURL(c)
+}
+
+func updateURL(c *gophercloud.ServiceClient, id string) string {
+ return resourceURL(c, id)
+}
+
+func deleteURL(c *gophercloud.ServiceClient, id string) string {
+ return resourceURL(c, id)
+}
diff --git a/_site/openstack/networking/v2/ports/urls_test.go b/_site/openstack/networking/v2/ports/urls_test.go
new file mode 100644
index 0000000..7fadd4d
--- /dev/null
+++ b/_site/openstack/networking/v2/ports/urls_test.go
@@ -0,0 +1,44 @@
+package ports
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+const endpoint = "http://localhost:57909/"
+
+func endpointClient() *gophercloud.ServiceClient {
+ return &gophercloud.ServiceClient{Endpoint: endpoint, ResourceBase: endpoint + "v2.0/"}
+}
+
+func TestListURL(t *testing.T) {
+ actual := listURL(endpointClient())
+ expected := endpoint + "v2.0/ports"
+ th.AssertEquals(t, expected, actual)
+}
+
+func TestGetURL(t *testing.T) {
+ actual := getURL(endpointClient(), "foo")
+ expected := endpoint + "v2.0/ports/foo"
+ th.AssertEquals(t, expected, actual)
+}
+
+func TestCreateURL(t *testing.T) {
+ actual := createURL(endpointClient())
+ expected := endpoint + "v2.0/ports"
+ th.AssertEquals(t, expected, actual)
+}
+
+func TestUpdateURL(t *testing.T) {
+ actual := updateURL(endpointClient(), "foo")
+ expected := endpoint + "v2.0/ports/foo"
+ th.AssertEquals(t, expected, actual)
+}
+
+func TestDeleteURL(t *testing.T) {
+ actual := deleteURL(endpointClient(), "foo")
+ expected := endpoint + "v2.0/ports/foo"
+ th.AssertEquals(t, expected, actual)
+}
diff --git a/_site/openstack/networking/v2/subnets/doc.go b/_site/openstack/networking/v2/subnets/doc.go
new file mode 100644
index 0000000..43e8296
--- /dev/null
+++ b/_site/openstack/networking/v2/subnets/doc.go
@@ -0,0 +1,10 @@
+// Package subnets contains functionality for working with Neutron subnet
+// resources. A subnet represents an IP address block that can be used to
+// assign IP addresses to virtual instances. Each subnet must have a CIDR and
+// must be associated with a network. IPs can either be selected from the whole
+// subnet CIDR or from allocation pools specified by the user.
+//
+// A subnet can also have a gateway, a list of DNS name servers, and host routes.
+// This information is pushed to instances whose interfaces are associated with
+// the subnet.
+package subnets
diff --git a/_site/openstack/networking/v2/subnets/errors.go b/_site/openstack/networking/v2/subnets/errors.go
new file mode 100644
index 0000000..0db0a6e
--- /dev/null
+++ b/_site/openstack/networking/v2/subnets/errors.go
@@ -0,0 +1,13 @@
+package subnets
+
+import "fmt"
+
+func err(str string) error {
+ return fmt.Errorf("%s", str)
+}
+
+var (
+ errNetworkIDRequired = err("A network ID is required")
+ errCIDRRequired = err("A valid CIDR is required")
+ errInvalidIPType = err("An IP type must either be 4 or 6")
+)
diff --git a/_site/openstack/networking/v2/subnets/requests.go b/_site/openstack/networking/v2/subnets/requests.go
new file mode 100644
index 0000000..e164583
--- /dev/null
+++ b/_site/openstack/networking/v2/subnets/requests.go
@@ -0,0 +1,254 @@
+package subnets
+
+import (
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+
+ "github.com/racker/perigee"
+)
+
+// AdminState gives users a solid type to work with for create and update
+// operations. It is recommended that users use the `Up` and `Down` enums.
+type AdminState *bool
+
+// Convenience vars for AdminStateUp values.
+var (
+ iTrue = true
+ iFalse = false
+
+ Up AdminState = &iTrue
+ Down AdminState = &iFalse
+)
+
+// ListOptsBuilder allows extensions to add additional parameters to the
+// List request.
+type ListOptsBuilder interface {
+ ToSubnetListString() (string, error)
+}
+
+// ListOpts allows the filtering and sorting of paginated collections through
+// the API. Filtering is achieved by passing in struct field values that map to
+// the subnet attributes you want to see returned. SortKey allows you to sort
+// by a particular subnet attribute. SortDir sets the direction, and is either
+// `asc' or `desc'. Marker and Limit are used for pagination.
+type ListOpts struct {
+ Name string `q:"name"`
+ EnableDHCP *bool `q:"enable_dhcp"`
+ NetworkID string `q:"network_id"`
+ TenantID string `q:"tenant_id"`
+ IPVersion int `q:"ip_version"`
+ GatewayIP string `q:"gateway_ip"`
+ CIDR string `q:"cidr"`
+ ID string `q:"id"`
+ Limit int `q:"limit"`
+ Marker string `q:"marker"`
+ SortKey string `q:"sort_key"`
+ SortDir string `q:"sort_dir"`
+}
+
+// ToSubnetListString formats a ListOpts into a query string.
+func (opts ListOpts) ToSubnetListString() (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
+// subnets. It accepts a ListOpts struct, which allows you to filter and sort
+// the returned collection for greater efficiency.
+//
+// Default policy settings return only those subnets that are owned by the tenant
+// who submits the request, unless the request is submitted by an user with
+// administrative rights.
+func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
+ url := listURL(c)
+ if opts != nil {
+ query, err := opts.ToSubnetListString()
+ if err != nil {
+ return pagination.Pager{Err: err}
+ }
+ url += query
+ }
+
+ return pagination.NewPager(c, url, func(r pagination.LastHTTPResponse) pagination.Page {
+ return SubnetPage{pagination.LinkedPageBase{LastHTTPResponse: r}}
+ })
+}
+
+// Get retrieves a specific subnet based on its unique ID.
+func Get(c *gophercloud.ServiceClient, id string) GetResult {
+ var res GetResult
+ _, res.Err = perigee.Request("GET", getURL(c, id), perigee.Options{
+ MoreHeaders: c.Provider.AuthenticatedHeaders(),
+ Results: &res.Resp,
+ OkCodes: []int{200},
+ })
+ return res
+}
+
+// Valid IP types
+const (
+ IPv4 = 4
+ IPv6 = 6
+)
+
+// 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 {
+ ToSubnetCreateMap() (map[string]interface{}, error)
+}
+
+// CreateOpts represents the attributes used when creating a new subnet.
+type CreateOpts struct {
+ // Required
+ NetworkID string
+ CIDR string
+ // Optional
+ Name string
+ TenantID string
+ AllocationPools []AllocationPool
+ GatewayIP string
+ IPVersion int
+ EnableDHCP *bool
+ DNSNameservers []string
+ HostRoutes []HostRoute
+}
+
+// ToSubnetCreateMap casts a CreateOpts struct to a map.
+func (opts CreateOpts) ToSubnetCreateMap() (map[string]interface{}, error) {
+ s := make(map[string]interface{})
+
+ if opts.NetworkID == "" {
+ return nil, errNetworkIDRequired
+ }
+ if opts.CIDR == "" {
+ return nil, errCIDRRequired
+ }
+ if opts.IPVersion != 0 && opts.IPVersion != IPv4 && opts.IPVersion != IPv6 {
+ return nil, errInvalidIPType
+ }
+
+ s["network_id"] = opts.NetworkID
+ s["cidr"] = opts.CIDR
+
+ if opts.EnableDHCP != nil {
+ s["enable_dhcp"] = &opts.EnableDHCP
+ }
+ if opts.Name != "" {
+ s["name"] = opts.Name
+ }
+ if opts.GatewayIP != "" {
+ s["gateway_ip"] = opts.GatewayIP
+ }
+ if opts.TenantID != "" {
+ s["tenant_id"] = opts.TenantID
+ }
+ if opts.IPVersion != 0 {
+ s["ip_version"] = opts.IPVersion
+ }
+ if len(opts.AllocationPools) != 0 {
+ s["allocation_pools"] = opts.AllocationPools
+ }
+ if len(opts.DNSNameservers) != 0 {
+ s["dns_nameservers"] = opts.DNSNameservers
+ }
+ if len(opts.HostRoutes) != 0 {
+ s["host_routes"] = opts.HostRoutes
+ }
+
+ return map[string]interface{}{"subnet": s}, nil
+}
+
+// Create accepts a CreateOpts struct and creates a new subnet using the values
+// provided. You must remember to provide a valid NetworkID, CIDR and IP version.
+func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult {
+ var res CreateResult
+
+ reqBody, err := opts.ToSubnetCreateMap()
+ if err != nil {
+ res.Err = err
+ return res
+ }
+
+ _, res.Err = perigee.Request("POST", createURL(c), perigee.Options{
+ MoreHeaders: c.Provider.AuthenticatedHeaders(),
+ ReqBody: &reqBody,
+ Results: &res.Resp,
+ OkCodes: []int{201},
+ })
+
+ return res
+}
+
+// UpdateOptsBuilder allows extensions to add additional parameters to the
+// Update request.
+type UpdateOptsBuilder interface {
+ ToSubnetUpdateMap() (map[string]interface{}, error)
+}
+
+// UpdateOpts represents the attributes used when updating an existing subnet.
+type UpdateOpts struct {
+ Name string
+ GatewayIP string
+ DNSNameservers []string
+ HostRoutes []HostRoute
+ EnableDHCP *bool
+}
+
+// ToSubnetUpdateMap casts an UpdateOpts struct to a map.
+func (opts UpdateOpts) ToSubnetUpdateMap() (map[string]interface{}, error) {
+ s := make(map[string]interface{})
+
+ if opts.EnableDHCP != nil {
+ s["enable_dhcp"] = &opts.EnableDHCP
+ }
+ if opts.Name != "" {
+ s["name"] = opts.Name
+ }
+ if opts.GatewayIP != "" {
+ s["gateway_ip"] = opts.GatewayIP
+ }
+ if len(opts.DNSNameservers) != 0 {
+ s["dns_nameservers"] = opts.DNSNameservers
+ }
+ if len(opts.HostRoutes) != 0 {
+ s["host_routes"] = opts.HostRoutes
+ }
+
+ return map[string]interface{}{"subnet": s}, nil
+}
+
+// Update accepts a UpdateOpts struct and updates an existing subnet using the
+// values provided.
+func Update(c *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) UpdateResult {
+ var res UpdateResult
+
+ reqBody, err := opts.ToSubnetUpdateMap()
+ if err != nil {
+ res.Err = err
+ return res
+ }
+
+ _, res.Err = perigee.Request("PUT", updateURL(c, id), perigee.Options{
+ MoreHeaders: c.Provider.AuthenticatedHeaders(),
+ ReqBody: &reqBody,
+ Results: &res.Resp,
+ OkCodes: []int{200, 201},
+ })
+
+ return res
+}
+
+// Delete accepts a unique ID and deletes the subnet associated with it.
+func Delete(c *gophercloud.ServiceClient, id string) DeleteResult {
+ var res DeleteResult
+ _, res.Err = perigee.Request("DELETE", deleteURL(c, id), perigee.Options{
+ MoreHeaders: c.Provider.AuthenticatedHeaders(),
+ OkCodes: []int{204},
+ })
+ return res
+}
diff --git a/_site/openstack/networking/v2/subnets/requests_test.go b/_site/openstack/networking/v2/subnets/requests_test.go
new file mode 100644
index 0000000..987064a
--- /dev/null
+++ b/_site/openstack/networking/v2/subnets/requests_test.go
@@ -0,0 +1,362 @@
+package subnets
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ fake "github.com/rackspace/gophercloud/openstack/networking/v2/common"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestList(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/subnets", 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, `
+{
+ "subnets": [
+ {
+ "name": "private-subnet",
+ "enable_dhcp": true,
+ "network_id": "db193ab3-96e3-4cb3-8fc5-05f4296d0324",
+ "tenant_id": "26a7980765d0414dbc1fc1f88cdb7e6e",
+ "dns_nameservers": [],
+ "allocation_pools": [
+ {
+ "start": "10.0.0.2",
+ "end": "10.0.0.254"
+ }
+ ],
+ "host_routes": [],
+ "ip_version": 4,
+ "gateway_ip": "10.0.0.1",
+ "cidr": "10.0.0.0/24",
+ "id": "08eae331-0402-425a-923c-34f7cfe39c1b"
+ },
+ {
+ "name": "my_subnet",
+ "enable_dhcp": true,
+ "network_id": "d32019d3-bc6e-4319-9c1d-6722fc136a22",
+ "tenant_id": "4fd44f30292945e481c7b8a0c8908869",
+ "dns_nameservers": [],
+ "allocation_pools": [
+ {
+ "start": "192.0.0.2",
+ "end": "192.255.255.254"
+ }
+ ],
+ "host_routes": [],
+ "ip_version": 4,
+ "gateway_ip": "192.0.0.1",
+ "cidr": "192.0.0.0/8",
+ "id": "54d6f61d-db07-451c-9ab3-b9609b6b6f0b"
+ }
+ ]
+}
+ `)
+ })
+
+ count := 0
+
+ List(fake.ServiceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractSubnets(page)
+ if err != nil {
+ t.Errorf("Failed to extract subnets: %v", err)
+ return false, nil
+ }
+
+ expected := []Subnet{
+ Subnet{
+ Name: "private-subnet",
+ EnableDHCP: true,
+ NetworkID: "db193ab3-96e3-4cb3-8fc5-05f4296d0324",
+ TenantID: "26a7980765d0414dbc1fc1f88cdb7e6e",
+ DNSNameservers: []string{},
+ AllocationPools: []AllocationPool{
+ AllocationPool{
+ Start: "10.0.0.2",
+ End: "10.0.0.254",
+ },
+ },
+ HostRoutes: []HostRoute{},
+ IPVersion: 4,
+ GatewayIP: "10.0.0.1",
+ CIDR: "10.0.0.0/24",
+ ID: "08eae331-0402-425a-923c-34f7cfe39c1b",
+ },
+ Subnet{
+ Name: "my_subnet",
+ EnableDHCP: true,
+ NetworkID: "d32019d3-bc6e-4319-9c1d-6722fc136a22",
+ TenantID: "4fd44f30292945e481c7b8a0c8908869",
+ DNSNameservers: []string{},
+ AllocationPools: []AllocationPool{
+ AllocationPool{
+ Start: "192.0.0.2",
+ End: "192.255.255.254",
+ },
+ },
+ HostRoutes: []HostRoute{},
+ IPVersion: 4,
+ GatewayIP: "192.0.0.1",
+ CIDR: "192.0.0.0/8",
+ ID: "54d6f61d-db07-451c-9ab3-b9609b6b6f0b",
+ },
+ }
+
+ th.CheckDeepEquals(t, expected, actual)
+
+ return true, nil
+ })
+
+ if count != 1 {
+ t.Errorf("Expected 1 page, got %d", count)
+ }
+}
+
+func TestGet(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/subnets/54d6f61d-db07-451c-9ab3-b9609b6b6f0b", 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, `
+{
+ "subnet": {
+ "name": "my_subnet",
+ "enable_dhcp": true,
+ "network_id": "d32019d3-bc6e-4319-9c1d-6722fc136a22",
+ "tenant_id": "4fd44f30292945e481c7b8a0c8908869",
+ "dns_nameservers": [],
+ "allocation_pools": [
+ {
+ "start": "192.0.0.2",
+ "end": "192.255.255.254"
+ }
+ ],
+ "host_routes": [],
+ "ip_version": 4,
+ "gateway_ip": "192.0.0.1",
+ "cidr": "192.0.0.0/8",
+ "id": "54d6f61d-db07-451c-9ab3-b9609b6b6f0b"
+ }
+}
+ `)
+ })
+
+ s, err := Get(fake.ServiceClient(), "54d6f61d-db07-451c-9ab3-b9609b6b6f0b").Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, s.Name, "my_subnet")
+ th.AssertEquals(t, s.EnableDHCP, true)
+ th.AssertEquals(t, s.NetworkID, "d32019d3-bc6e-4319-9c1d-6722fc136a22")
+ th.AssertEquals(t, s.TenantID, "4fd44f30292945e481c7b8a0c8908869")
+ th.AssertDeepEquals(t, s.DNSNameservers, []string{})
+ th.AssertDeepEquals(t, s.AllocationPools, []AllocationPool{
+ AllocationPool{
+ Start: "192.0.0.2",
+ End: "192.255.255.254",
+ },
+ })
+ th.AssertDeepEquals(t, s.HostRoutes, []HostRoute{})
+ th.AssertEquals(t, s.IPVersion, 4)
+ th.AssertEquals(t, s.GatewayIP, "192.0.0.1")
+ th.AssertEquals(t, s.CIDR, "192.0.0.0/8")
+ th.AssertEquals(t, s.ID, "54d6f61d-db07-451c-9ab3-b9609b6b6f0b")
+}
+
+func TestCreate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/subnets", 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, `
+{
+ "subnet": {
+ "network_id": "d32019d3-bc6e-4319-9c1d-6722fc136a22",
+ "ip_version": 4,
+ "cidr": "192.168.199.0/24",
+ "dns_nameservers": ["foo"],
+ "allocation_pools": [
+ {
+ "start": "192.168.199.2",
+ "end": "192.168.199.254"
+ }
+ ],
+ "host_routes": [{"destination":"","nexthop": "bar"}]
+ }
+}
+ `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusCreated)
+
+ fmt.Fprintf(w, `
+{
+ "subnet": {
+ "name": "",
+ "enable_dhcp": true,
+ "network_id": "d32019d3-bc6e-4319-9c1d-6722fc136a22",
+ "tenant_id": "4fd44f30292945e481c7b8a0c8908869",
+ "dns_nameservers": [],
+ "allocation_pools": [
+ {
+ "start": "192.168.199.2",
+ "end": "192.168.199.254"
+ }
+ ],
+ "host_routes": [],
+ "ip_version": 4,
+ "gateway_ip": "192.168.199.1",
+ "cidr": "192.168.199.0/24",
+ "id": "3b80198d-4f7b-4f77-9ef5-774d54e17126"
+ }
+}
+ `)
+ })
+
+ opts := CreateOpts{
+ NetworkID: "d32019d3-bc6e-4319-9c1d-6722fc136a22",
+ IPVersion: 4,
+ CIDR: "192.168.199.0/24",
+ AllocationPools: []AllocationPool{
+ AllocationPool{
+ Start: "192.168.199.2",
+ End: "192.168.199.254",
+ },
+ },
+ DNSNameservers: []string{"foo"},
+ HostRoutes: []HostRoute{
+ HostRoute{NextHop: "bar"},
+ },
+ }
+ s, err := Create(fake.ServiceClient(), opts).Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, s.Name, "")
+ th.AssertEquals(t, s.EnableDHCP, true)
+ th.AssertEquals(t, s.NetworkID, "d32019d3-bc6e-4319-9c1d-6722fc136a22")
+ th.AssertEquals(t, s.TenantID, "4fd44f30292945e481c7b8a0c8908869")
+ th.AssertDeepEquals(t, s.DNSNameservers, []string{})
+ th.AssertDeepEquals(t, s.AllocationPools, []AllocationPool{
+ AllocationPool{
+ Start: "192.168.199.2",
+ End: "192.168.199.254",
+ },
+ })
+ th.AssertDeepEquals(t, s.HostRoutes, []HostRoute{})
+ th.AssertEquals(t, s.IPVersion, 4)
+ th.AssertEquals(t, s.GatewayIP, "192.168.199.1")
+ th.AssertEquals(t, s.CIDR, "192.168.199.0/24")
+ th.AssertEquals(t, s.ID, "3b80198d-4f7b-4f77-9ef5-774d54e17126")
+}
+
+func TestRequiredCreateOpts(t *testing.T) {
+ res := Create(fake.ServiceClient(), CreateOpts{})
+ if res.Err == nil {
+ t.Fatalf("Expected error, got none")
+ }
+
+ res = Create(fake.ServiceClient(), CreateOpts{NetworkID: "foo"})
+ if res.Err == nil {
+ t.Fatalf("Expected error, got none")
+ }
+
+ res = Create(fake.ServiceClient(), CreateOpts{NetworkID: "foo", CIDR: "bar", IPVersion: 40})
+ if res.Err == nil {
+ t.Fatalf("Expected error, got none")
+ }
+}
+
+func TestUpdate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/subnets/08eae331-0402-425a-923c-34f7cfe39c1b", 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.TestHeader(t, r, "Accept", "application/json")
+ th.TestJSONRequest(t, r, `
+{
+ "subnet": {
+ "name": "my_new_subnet",
+ "dns_nameservers": ["foo"],
+ "host_routes": [{"destination":"","nexthop": "bar"}]
+ }
+}
+ `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusCreated)
+
+ fmt.Fprintf(w, `
+{
+ "subnet": {
+ "name": "my_new_subnet",
+ "enable_dhcp": true,
+ "network_id": "db193ab3-96e3-4cb3-8fc5-05f4296d0324",
+ "tenant_id": "26a7980765d0414dbc1fc1f88cdb7e6e",
+ "dns_nameservers": [],
+ "allocation_pools": [
+ {
+ "start": "10.0.0.2",
+ "end": "10.0.0.254"
+ }
+ ],
+ "host_routes": [],
+ "ip_version": 4,
+ "gateway_ip": "10.0.0.1",
+ "cidr": "10.0.0.0/24",
+ "id": "08eae331-0402-425a-923c-34f7cfe39c1b"
+ }
+}
+ `)
+ })
+
+ opts := UpdateOpts{
+ Name: "my_new_subnet",
+ DNSNameservers: []string{"foo"},
+ HostRoutes: []HostRoute{
+ HostRoute{NextHop: "bar"},
+ },
+ }
+ s, err := Update(fake.ServiceClient(), "08eae331-0402-425a-923c-34f7cfe39c1b", opts).Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, s.Name, "my_new_subnet")
+ th.AssertEquals(t, s.ID, "08eae331-0402-425a-923c-34f7cfe39c1b")
+}
+
+func TestDelete(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/subnets/08eae331-0402-425a-923c-34f7cfe39c1b", 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)
+ })
+
+ res := Delete(fake.ServiceClient(), "08eae331-0402-425a-923c-34f7cfe39c1b")
+ th.AssertNoErr(t, res.Err)
+}
diff --git a/_site/openstack/networking/v2/subnets/results.go b/_site/openstack/networking/v2/subnets/results.go
new file mode 100644
index 0000000..3c9ef71
--- /dev/null
+++ b/_site/openstack/networking/v2/subnets/results.go
@@ -0,0 +1,130 @@
+package subnets
+
+import (
+ "github.com/mitchellh/mapstructure"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+type commonResult struct {
+ gophercloud.CommonResult
+}
+
+// Extract is a function that accepts a result and extracts a subnet resource.
+func (r commonResult) Extract() (*Subnet, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var res struct {
+ Subnet *Subnet `json:"subnet"`
+ }
+
+ err := mapstructure.Decode(r.Resp, &res)
+
+ return res.Subnet, err
+}
+
+// CreateResult represents the result of a create operation.
+type CreateResult struct {
+ commonResult
+}
+
+// GetResult represents the result of a get operation.
+type GetResult struct {
+ commonResult
+}
+
+// UpdateResult represents the result of an update operation.
+type UpdateResult struct {
+ commonResult
+}
+
+// DeleteResult represents the result of a delete operation.
+type DeleteResult commonResult
+
+// AllocationPool represents a sub-range of cidr available for dynamic
+// allocation to ports, e.g. {Start: "10.0.0.2", End: "10.0.0.254"}
+type AllocationPool struct {
+ Start string `json:"start"`
+ End string `json:"end"`
+}
+
+// HostRoute represents a route that should be used by devices with IPs from
+// a subnet (not including local subnet route).
+type HostRoute struct {
+ DestinationCIDR string `json:"destination"`
+ NextHop string `json:"nexthop"`
+}
+
+// Subnet represents a subnet. See package documentation for a top-level
+// description of what this is.
+type Subnet struct {
+ // UUID representing the subnet
+ ID string `mapstructure:"id" json:"id"`
+ // UUID of the parent network
+ NetworkID string `mapstructure:"network_id" json:"network_id"`
+ // Human-readable name for the subnet. Might not be unique.
+ Name string `mapstructure:"name" json:"name"`
+ // IP version, either `4' or `6'
+ IPVersion int `mapstructure:"ip_version" json:"ip_version"`
+ // CIDR representing IP range for this subnet, based on IP version
+ CIDR string `mapstructure:"cidr" json:"cidr"`
+ // Default gateway used by devices in this subnet
+ GatewayIP string `mapstructure:"gateway_ip" json:"gateway_ip"`
+ // DNS name servers used by hosts in this subnet.
+ DNSNameservers []string `mapstructure:"dns_nameservers" json:"dns_nameservers"`
+ // Sub-ranges of CIDR available for dynamic allocation to ports. See AllocationPool.
+ AllocationPools []AllocationPool `mapstructure:"allocation_pools" json:"allocation_pools"`
+ // Routes that should be used by devices with IPs from this subnet (not including local subnet route).
+ HostRoutes []HostRoute `mapstructure:"host_routes" json:"host_routes"`
+ // Specifies whether DHCP is enabled for this subnet or not.
+ EnableDHCP bool `mapstructure:"enable_dhcp" json:"enable_dhcp"`
+ // Owner of network. Only admin users can specify a tenant_id other than its own.
+ TenantID string `mapstructure:"tenant_id" json:"tenant_id"`
+}
+
+// SubnetPage is the page returned by a pager when traversing over a collection
+// of subnets.
+type SubnetPage struct {
+ pagination.LinkedPageBase
+}
+
+// NextPageURL is invoked when a paginated collection of subnets has reached
+// the end of a page and the pager seeks to traverse over a new one. In order
+// to do this, it needs to construct the next page's URL.
+func (p SubnetPage) NextPageURL() (string, error) {
+ type resp struct {
+ Links []gophercloud.Link `mapstructure:"subnets_links"`
+ }
+
+ var r resp
+ err := mapstructure.Decode(p.Body, &r)
+ if err != nil {
+ return "", err
+ }
+
+ return gophercloud.ExtractNextURL(r.Links)
+}
+
+// IsEmpty checks whether a SubnetPage struct is empty.
+func (p SubnetPage) IsEmpty() (bool, error) {
+ is, err := ExtractSubnets(p)
+ if err != nil {
+ return true, nil
+ }
+ return len(is) == 0, nil
+}
+
+// ExtractSubnets accepts a Page struct, specifically a SubnetPage struct,
+// and extracts the elements into a slice of Subnet structs. In other words,
+// a generic collection is mapped into a relevant slice.
+func ExtractSubnets(page pagination.Page) ([]Subnet, error) {
+ var resp struct {
+ Subnets []Subnet `mapstructure:"subnets" json:"subnets"`
+ }
+
+ err := mapstructure.Decode(page.(SubnetPage).Body, &resp)
+
+ return resp.Subnets, err
+}
diff --git a/_site/openstack/networking/v2/subnets/urls.go b/_site/openstack/networking/v2/subnets/urls.go
new file mode 100644
index 0000000..0d02368
--- /dev/null
+++ b/_site/openstack/networking/v2/subnets/urls.go
@@ -0,0 +1,31 @@
+package subnets
+
+import "github.com/rackspace/gophercloud"
+
+func resourceURL(c *gophercloud.ServiceClient, id string) string {
+ return c.ServiceURL("subnets", id)
+}
+
+func rootURL(c *gophercloud.ServiceClient) string {
+ return c.ServiceURL("subnets")
+}
+
+func listURL(c *gophercloud.ServiceClient) string {
+ return rootURL(c)
+}
+
+func getURL(c *gophercloud.ServiceClient, id string) string {
+ return resourceURL(c, id)
+}
+
+func createURL(c *gophercloud.ServiceClient) string {
+ return rootURL(c)
+}
+
+func updateURL(c *gophercloud.ServiceClient, id string) string {
+ return resourceURL(c, id)
+}
+
+func deleteURL(c *gophercloud.ServiceClient, id string) string {
+ return resourceURL(c, id)
+}
diff --git a/_site/openstack/networking/v2/subnets/urls_test.go b/_site/openstack/networking/v2/subnets/urls_test.go
new file mode 100644
index 0000000..aeeddf3
--- /dev/null
+++ b/_site/openstack/networking/v2/subnets/urls_test.go
@@ -0,0 +1,44 @@
+package subnets
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+const endpoint = "http://localhost:57909/"
+
+func endpointClient() *gophercloud.ServiceClient {
+ return &gophercloud.ServiceClient{Endpoint: endpoint, ResourceBase: endpoint + "v2.0/"}
+}
+
+func TestListURL(t *testing.T) {
+ actual := listURL(endpointClient())
+ expected := endpoint + "v2.0/subnets"
+ th.AssertEquals(t, expected, actual)
+}
+
+func TestGetURL(t *testing.T) {
+ actual := getURL(endpointClient(), "foo")
+ expected := endpoint + "v2.0/subnets/foo"
+ th.AssertEquals(t, expected, actual)
+}
+
+func TestCreateURL(t *testing.T) {
+ actual := createURL(endpointClient())
+ expected := endpoint + "v2.0/subnets"
+ th.AssertEquals(t, expected, actual)
+}
+
+func TestUpdateURL(t *testing.T) {
+ actual := updateURL(endpointClient(), "foo")
+ expected := endpoint + "v2.0/subnets/foo"
+ th.AssertEquals(t, expected, actual)
+}
+
+func TestDeleteURL(t *testing.T) {
+ actual := deleteURL(endpointClient(), "foo")
+ expected := endpoint + "v2.0/subnets/foo"
+ th.AssertEquals(t, expected, actual)
+}
diff --git a/_site/openstack/objectstorage/v1/accounts/doc.go b/_site/openstack/objectstorage/v1/accounts/doc.go
new file mode 100644
index 0000000..f5f894a
--- /dev/null
+++ b/_site/openstack/objectstorage/v1/accounts/doc.go
@@ -0,0 +1,8 @@
+// Package accounts contains functionality for working with Object Storage
+// account resources. An account is the top-level resource the object storage
+// hierarchy: containers belong to accounts, objects belong to containers.
+//
+// Another way of thinking of an account is like a namespace for all your
+// resources. It is synonymous with a project or tenant in other OpenStack
+// services.
+package accounts
diff --git a/_site/openstack/objectstorage/v1/accounts/requests.go b/_site/openstack/objectstorage/v1/accounts/requests.go
new file mode 100644
index 0000000..55fcb05
--- /dev/null
+++ b/_site/openstack/objectstorage/v1/accounts/requests.go
@@ -0,0 +1,107 @@
+package accounts
+
+import (
+ "github.com/racker/perigee"
+ "github.com/rackspace/gophercloud"
+)
+
+// GetOptsBuilder allows extensions to add additional headers to the Get
+// request.
+type GetOptsBuilder interface {
+ ToAccountGetMap() (map[string]string, error)
+}
+
+// GetOpts is a structure that contains parameters for getting an account's
+// metadata.
+type GetOpts struct {
+ Newest bool `h:"X-Newest"`
+}
+
+// ToAccountGetMap formats a GetOpts into a map[string]string of headers.
+func (opts GetOpts) ToAccountGetMap() (map[string]string, error) {
+ return gophercloud.BuildHeaders(opts)
+}
+
+// Get is a function that retrieves an account's metadata. To extract just the
+// custom metadata, call the ExtractMetadata method on the GetResult. To extract
+// all the headers that are returned (including the metadata), call the
+// ExtractHeaders method on the GetResult.
+func Get(c *gophercloud.ServiceClient, opts GetOptsBuilder) GetResult {
+ var res GetResult
+ h := c.Provider.AuthenticatedHeaders()
+
+ if opts != nil {
+ headers, err := opts.ToAccountGetMap()
+ if err != nil {
+ res.Err = err
+ return res
+ }
+
+ for k, v := range headers {
+ h[k] = v
+ }
+ }
+
+ resp, err := perigee.Request("HEAD", getURL(c), perigee.Options{
+ MoreHeaders: h,
+ OkCodes: []int{204},
+ })
+ res.Resp = &resp.HttpResponse
+ res.Err = err
+ return res
+}
+
+// UpdateOptsBuilder allows extensions to add additional headers to the Update
+// request.
+type UpdateOptsBuilder interface {
+ ToAccountUpdateMap() (map[string]string, error)
+}
+
+// UpdateOpts is a structure that contains parameters for updating, creating, or
+// deleting an account's metadata.
+type UpdateOpts struct {
+ Metadata map[string]string
+ ContentType string `h:"Content-Type"`
+ DetectContentType bool `h:"X-Detect-Content-Type"`
+ TempURLKey string `h:"X-Account-Meta-Temp-URL-Key"`
+ TempURLKey2 string `h:"X-Account-Meta-Temp-URL-Key-2"`
+}
+
+// ToAccountUpdateMap formats an UpdateOpts into a map[string]string of headers.
+func (opts UpdateOpts) ToAccountUpdateMap() (map[string]string, error) {
+ headers, err := gophercloud.BuildHeaders(opts)
+ if err != nil {
+ return nil, err
+ }
+ for k, v := range opts.Metadata {
+ headers["X-Account-Meta-"+k] = v
+ }
+ return headers, err
+}
+
+// Update is a function that creates, updates, or deletes an account's metadata.
+// To extract the headers returned, call the ExtractHeaders method on the
+// UpdateResult.
+func Update(c *gophercloud.ServiceClient, opts UpdateOptsBuilder) UpdateResult {
+ var res UpdateResult
+ h := c.Provider.AuthenticatedHeaders()
+
+ if opts != nil {
+ headers, err := opts.ToAccountUpdateMap()
+ if err != nil {
+ res.Err = err
+ return res
+ }
+ for k, v := range headers {
+ h[k] = v
+ }
+ }
+
+ resp, err := perigee.Request("POST", updateURL(c), perigee.Options{
+ MoreHeaders: h,
+ OkCodes: []int{204},
+ })
+ res.Resp = &resp.HttpResponse
+ res.Err = err
+ return res
+}
diff --git a/_site/openstack/objectstorage/v1/accounts/requests_test.go b/_site/openstack/objectstorage/v1/accounts/requests_test.go
new file mode 100644
index 0000000..0090eea
--- /dev/null
+++ b/_site/openstack/objectstorage/v1/accounts/requests_test.go
@@ -0,0 +1,53 @@
+package accounts
+
+import (
+ "net/http"
+ "testing"
+
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+var metadata = map[string]string{"gophercloud-test": "accounts"}
+
+func TestUpdateAccount(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/", 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, "X-Account-Meta-Gophercloud-Test", "accounts")
+
+ w.Header().Set("X-Account-Container-Count", "2")
+ w.Header().Set("X-Account-Bytes-Used", "14")
+ w.Header().Set("X-Account-Meta-Subject", "books")
+
+ w.WriteHeader(http.StatusNoContent)
+ })
+
+ options := &UpdateOpts{Metadata: map[string]string{"gophercloud-test": "accounts"}}
+ _, err := Update(fake.ServiceClient(), options).ExtractHeaders()
+ if err != nil {
+ t.Fatalf("Unable to update account: %v", err)
+ }
+}
+
+func TestGetAccount(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "HEAD")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ w.Header().Set("X-Account-Meta-Foo", "bar")
+ w.WriteHeader(http.StatusNoContent)
+ })
+
+ expected := map[string]string{"Foo": "bar"}
+ actual, err := Get(fake.ServiceClient(), &GetOpts{}).ExtractMetadata()
+ if err != nil {
+ t.Fatalf("Unable to get account metadata: %v", err)
+ }
+ th.CheckDeepEquals(t, expected, actual)
+}
diff --git a/_site/openstack/objectstorage/v1/accounts/results.go b/_site/openstack/objectstorage/v1/accounts/results.go
new file mode 100644
index 0000000..8ff8183
--- /dev/null
+++ b/_site/openstack/objectstorage/v1/accounts/results.go
@@ -0,0 +1,34 @@
+package accounts
+
+import (
+ "strings"
+
+ objectstorage "github.com/rackspace/gophercloud/openstack/objectstorage/v1"
+)
+
+// GetResult is returned from a call to the Get function. See v1.CommonResult.
+type GetResult struct {
+ objectstorage.CommonResult
+}
+
+// ExtractMetadata is a function that takes a GetResult (of type *http.Response)
+// and returns the custom metatdata associated with the account.
+func (gr GetResult) ExtractMetadata() (map[string]string, error) {
+ if gr.Err != nil {
+ return nil, gr.Err
+ }
+
+ metadata := make(map[string]string)
+ for k, v := range gr.Resp.Header {
+ if strings.HasPrefix(k, "X-Account-Meta-") {
+ key := strings.TrimPrefix(k, "X-Account-Meta-")
+ metadata[key] = v[0]
+ }
+ }
+ return metadata, nil
+}
+
+// UpdateResult is returned from a call to the Update function. See v1.CommonResult.
+type UpdateResult struct {
+ objectstorage.CommonResult
+}
diff --git a/_site/openstack/objectstorage/v1/accounts/urls.go b/_site/openstack/objectstorage/v1/accounts/urls.go
new file mode 100644
index 0000000..9952fe4
--- /dev/null
+++ b/_site/openstack/objectstorage/v1/accounts/urls.go
@@ -0,0 +1,11 @@
+package accounts
+
+import "github.com/rackspace/gophercloud"
+
+func getURL(c *gophercloud.ServiceClient) string {
+ return c.Endpoint
+}
+
+func updateURL(c *gophercloud.ServiceClient) string {
+ return getURL(c)
+}
diff --git a/_site/openstack/objectstorage/v1/accounts/urls_test.go b/_site/openstack/objectstorage/v1/accounts/urls_test.go
new file mode 100644
index 0000000..074d52d
--- /dev/null
+++ b/_site/openstack/objectstorage/v1/accounts/urls_test.go
@@ -0,0 +1,26 @@
+package accounts
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+const endpoint = "http://localhost:57909/"
+
+func endpointClient() *gophercloud.ServiceClient {
+ return &gophercloud.ServiceClient{Endpoint: endpoint}
+}
+
+func TestGetURL(t *testing.T) {
+ actual := getURL(endpointClient())
+ expected := endpoint
+ th.CheckEquals(t, expected, actual)
+}
+
+func TestUpdateURL(t *testing.T) {
+ actual := updateURL(endpointClient())
+ expected := endpoint
+ th.CheckEquals(t, expected, actual)
+}
diff --git a/_site/openstack/objectstorage/v1/common.go b/_site/openstack/objectstorage/v1/common.go
new file mode 100644
index 0000000..1a6c44a
--- /dev/null
+++ b/_site/openstack/objectstorage/v1/common.go
@@ -0,0 +1,25 @@
+package v1
+
+import (
+ "net/http"
+)
+
+// CommonResult is a structure that contains the response and error of a call to an
+// object storage endpoint.
+type CommonResult struct {
+ Resp *http.Response
+ Err error
+}
+
+// ExtractHeaders will extract and return the headers from a *http.Response.
+func (cr CommonResult) ExtractHeaders() (http.Header, error) {
+ if cr.Err != nil {
+ return nil, cr.Err
+ }
+
+ var headers http.Header
+ if cr.Err != nil {
+ return headers, cr.Err
+ }
+ return cr.Resp.Header, nil
+}
diff --git a/_site/openstack/objectstorage/v1/containers/doc.go b/_site/openstack/objectstorage/v1/containers/doc.go
new file mode 100644
index 0000000..5fed553
--- /dev/null
+++ b/_site/openstack/objectstorage/v1/containers/doc.go
@@ -0,0 +1,8 @@
+// Package containers contains functionality for working with Object Storage
+// container resources. A container serves as a logical namespace for objects
+// that are placed inside it - an object with the same name in two different
+// containers represents two different objects.
+//
+// In addition to containing objects, you can also use the container to control
+// access to objects by using an access control list (ACL).
+package containers
diff --git a/_site/openstack/objectstorage/v1/containers/requests.go b/_site/openstack/objectstorage/v1/containers/requests.go
new file mode 100644
index 0000000..ce3f540
--- /dev/null
+++ b/_site/openstack/objectstorage/v1/containers/requests.go
@@ -0,0 +1,206 @@
+package containers
+
+import (
+ "github.com/racker/perigee"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// ListOptsBuilder allows extensions to add additional parameters to the List
+// request.
+type ListOptsBuilder interface {
+ ToContainerListParams() (bool, string, error)
+}
+
+// ListOpts is a structure that holds options for listing containers.
+type ListOpts struct {
+ Full bool
+ Limit int `q:"limit"`
+ Marker string `q:"marker"`
+ EndMarker string `q:"end_marker"`
+ Format string `q:"format"`
+ Prefix string `q:"prefix"`
+ Delimiter string `q:"delimiter"`
+}
+
+// ToContainerListParams formats a ListOpts into a query string and boolean
+// representing whether to list complete information for each container.
+func (opts ListOpts) ToContainerListParams() (bool, string, error) {
+ q, err := gophercloud.BuildQueryString(opts)
+ if err != nil {
+ return false, "", err
+ }
+ return opts.Full, q.String(), nil
+}
+
+// List is a function that retrieves containers associated with the account as
+// well as account metadata. It returns a pager which can be iterated with the
+// EachPage function.
+func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
+ headers := map[string]string{"Accept": "text/plain", "Content-Type": "text/plain"}
+
+ url := listURL(c)
+ if opts != nil {
+ full, query, err := opts.ToContainerListParams()
+ if err != nil {
+ return pagination.Pager{Err: err}
+ }
+ url += query
+
+ if full {
+ headers = map[string]string{"Accept": "application/json", "Content-Type": "application/json"}
+ }
+ }
+
+ createPage := func(r pagination.LastHTTPResponse) pagination.Page {
+ p := ContainerPage{pagination.MarkerPageBase{LastHTTPResponse: r}}
+ p.MarkerPageBase.Owner = p
+ return p
+ }
+
+ pager := pagination.NewPager(c, url, createPage)
+ pager.Headers = headers
+ return pager
+}
+
+// CreateOptsBuilder allows extensions to add additional parameters to the
+// Create request.
+type CreateOptsBuilder interface {
+ ToContainerCreateMap() (map[string]string, error)
+}
+
+// CreateOpts is a structure that holds parameters for creating a container.
+type CreateOpts struct {
+ Metadata map[string]string
+ ContainerRead string `h:"X-Container-Read"`
+ ContainerSyncTo string `h:"X-Container-Sync-To"`
+ ContainerSyncKey string `h:"X-Container-Sync-Key"`
+ ContainerWrite string `h:"X-Container-Write"`
+ ContentType string `h:"Content-Type"`
+ DetectContentType bool `h:"X-Detect-Content-Type"`
+ IfNoneMatch string `h:"If-None-Match"`
+ VersionsLocation string `h:"X-Versions-Location"`
+}
+
+// ToContainerCreateMap formats a CreateOpts into a map of headers.
+func (opts CreateOpts) ToContainerCreateMap() (map[string]string, error) {
+ h, err := gophercloud.BuildHeaders(opts)
+ if err != nil {
+ return nil, err
+ }
+ for k, v := range opts.Metadata {
+ h["X-Container-Meta-"+k] = v
+ }
+ return h, nil
+}
+
+// Create is a function that creates a new container.
+func Create(c *gophercloud.ServiceClient, containerName string, opts CreateOptsBuilder) CreateResult {
+ var res CreateResult
+ h := c.Provider.AuthenticatedHeaders()
+
+ if opts != nil {
+ headers, err := opts.ToContainerCreateMap()
+ if err != nil {
+ res.Err = err
+ return res
+ }
+
+ for k, v := range headers {
+ h[k] = v
+ }
+ }
+
+ resp, err := perigee.Request("PUT", createURL(c, containerName), perigee.Options{
+ MoreHeaders: h,
+ OkCodes: []int{201, 204},
+ })
+ res.Resp = &resp.HttpResponse
+ res.Err = err
+ return res
+}
+
+// Delete is a function that deletes a container.
+func Delete(c *gophercloud.ServiceClient, containerName string) DeleteResult {
+ var res DeleteResult
+ resp, err := perigee.Request("DELETE", deleteURL(c, containerName), perigee.Options{
+ MoreHeaders: c.Provider.AuthenticatedHeaders(),
+ OkCodes: []int{204},
+ })
+ res.Resp = &resp.HttpResponse
+ res.Err = err
+ return res
+}
+
+// UpdateOptsBuilder allows extensions to add additional parameters to the
+// Update request.
+type UpdateOptsBuilder interface {
+ ToContainerUpdateMap() (map[string]string, error)
+}
+
+// UpdateOpts is a structure that holds parameters for updating, creating, or
+// deleting a container's metadata.
+type UpdateOpts struct {
+ Metadata map[string]string
+ ContainerRead string `h:"X-Container-Read"`
+ ContainerSyncTo string `h:"X-Container-Sync-To"`
+ ContainerSyncKey string `h:"X-Container-Sync-Key"`
+ ContainerWrite string `h:"X-Container-Write"`
+ ContentType string `h:"Content-Type"`
+ DetectContentType bool `h:"X-Detect-Content-Type"`
+ RemoveVersionsLocation string `h:"X-Remove-Versions-Location"`
+ VersionsLocation string `h:"X-Versions-Location"`
+}
+
+// ToContainerUpdateMap formats a CreateOpts into a map of headers.
+func (opts UpdateOpts) ToContainerUpdateMap() (map[string]string, error) {
+ h, err := gophercloud.BuildHeaders(opts)
+ if err != nil {
+ return nil, err
+ }
+ for k, v := range opts.Metadata {
+ h["X-Container-Meta-"+k] = v
+ }
+ return h, nil
+}
+
+// Update is a function that creates, updates, or deletes a container's
+// metadata.
+func Update(c *gophercloud.ServiceClient, containerName string, opts UpdateOptsBuilder) UpdateResult {
+ var res UpdateResult
+ h := c.Provider.AuthenticatedHeaders()
+
+ if opts != nil {
+ headers, err := opts.ToContainerUpdateMap()
+ if err != nil {
+ res.Err = err
+ return res
+ }
+
+ for k, v := range headers {
+ h[k] = v
+ }
+ }
+
+ resp, err := perigee.Request("POST", updateURL(c, containerName), perigee.Options{
+ MoreHeaders: h,
+ OkCodes: []int{204},
+ })
+ res.Resp = &resp.HttpResponse
+ res.Err = err
+ return res
+}
+
+// Get is a function that retrieves the metadata of a container. To extract just
+// the custom metadata, pass the GetResult response to the ExtractMetadata
+// function.
+func Get(c *gophercloud.ServiceClient, containerName string) GetResult {
+ var res GetResult
+ resp, err := perigee.Request("HEAD", getURL(c, containerName), perigee.Options{
+ MoreHeaders: c.Provider.AuthenticatedHeaders(),
+ OkCodes: []int{204},
+ })
+ res.Resp = &resp.HttpResponse
+ res.Err = err
+ return res
+}
diff --git a/_site/openstack/objectstorage/v1/containers/requests_test.go b/_site/openstack/objectstorage/v1/containers/requests_test.go
new file mode 100644
index 0000000..9562676
--- /dev/null
+++ b/_site/openstack/objectstorage/v1/containers/requests_test.go
@@ -0,0 +1,196 @@
+package containers
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+var metadata = map[string]string{"gophercloud-test": "containers"}
+
+func TestListContainerInfo(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ 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.Header().Set("Content-Type", "application/json")
+ r.ParseForm()
+ marker := r.Form.Get("marker")
+ switch marker {
+ case "":
+ fmt.Fprintf(w, `[
+ {
+ "count": 0,
+ "bytes": 0,
+ "name": "janeausten"
+ },
+ {
+ "count": 1,
+ "bytes": 14,
+ "name": "marktwain"
+ }
+ ]`)
+ case "marktwain":
+ fmt.Fprintf(w, `[]`)
+ default:
+ t.Fatalf("Unexpected marker: [%s]", marker)
+ }
+ })
+
+ count := 0
+
+ List(fake.ServiceClient(), &ListOpts{Full: true}).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractInfo(page)
+ if err != nil {
+ t.Errorf("Failed to extract container info: %v", err)
+ return false, err
+ }
+
+ expected := []Container{
+ Container{
+ Count: 0,
+ Bytes: 0,
+ Name: "janeausten",
+ },
+ Container{
+ Count: 1,
+ Bytes: 14,
+ Name: "marktwain",
+ },
+ }
+
+ th.CheckDeepEquals(t, expected, actual)
+
+ return true, nil
+ })
+
+ if count != 1 {
+ t.Errorf("Expected 1 page, got %d", count)
+ }
+}
+
+func TestListContainerNames(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ 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", "text/plain")
+
+ w.Header().Set("Content-Type", "text/plain")
+ r.ParseForm()
+ marker := r.Form.Get("marker")
+ switch marker {
+ case "":
+ fmt.Fprintf(w, "janeausten\nmarktwain\n")
+ case "marktwain":
+ fmt.Fprintf(w, ``)
+ default:
+ t.Fatalf("Unexpected marker: [%s]", marker)
+ }
+ })
+
+ count := 0
+
+ List(fake.ServiceClient(), &ListOpts{Full: false}).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractNames(page)
+ if err != nil {
+ t.Errorf("Failed to extract container names: %v", err)
+ return false, err
+ }
+
+ expected := []string{"janeausten", "marktwain"}
+
+ th.CheckDeepEquals(t, expected, actual)
+
+ return true, nil
+ })
+
+ if count != 1 {
+ t.Errorf("Expected 1 page, got %d", count)
+ }
+}
+
+func TestCreateContainer(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/testContainer", 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, "Accept", "application/json")
+
+ w.Header().Add("X-Container-Meta-Foo", "bar")
+ w.WriteHeader(http.StatusNoContent)
+ })
+
+ options := CreateOpts{ContentType: "application/json", Metadata: map[string]string{"foo": "bar"}}
+ headers, err := Create(fake.ServiceClient(), "testContainer", options).ExtractHeaders()
+ if err != nil {
+ t.Fatalf("Unexpected error creating container: %v", err)
+ }
+ th.CheckEquals(t, "bar", headers["X-Container-Meta-Foo"][0])
+}
+
+func TestDeleteContainer(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/testContainer", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "DELETE")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ th.TestHeader(t, r, "Accept", "application/json")
+ w.WriteHeader(http.StatusNoContent)
+ })
+
+ _, err := Delete(fake.ServiceClient(), "testContainer").ExtractHeaders()
+ if err != nil {
+ t.Fatalf("Unexpected error deleting container: %v", err)
+ }
+}
+
+func TestUpateContainer(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/testContainer", 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, "Accept", "application/json")
+ w.WriteHeader(http.StatusNoContent)
+ })
+
+ options := &UpdateOpts{Metadata: map[string]string{"foo": "bar"}}
+ _, err := Update(fake.ServiceClient(), "testContainer", options).ExtractHeaders()
+ if err != nil {
+ t.Fatalf("Unexpected error updating container metadata: %v", err)
+ }
+}
+
+func TestGetContainer(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/testContainer", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "HEAD")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ th.TestHeader(t, r, "Accept", "application/json")
+ w.WriteHeader(http.StatusNoContent)
+ })
+
+ _, err := Get(fake.ServiceClient(), "testContainer").ExtractMetadata()
+ if err != nil {
+ t.Fatalf("Unexpected error getting container metadata: %v", err)
+ }
+}
diff --git a/_site/openstack/objectstorage/v1/containers/results.go b/_site/openstack/objectstorage/v1/containers/results.go
new file mode 100644
index 0000000..227a9dc
--- /dev/null
+++ b/_site/openstack/objectstorage/v1/containers/results.go
@@ -0,0 +1,139 @@
+package containers
+
+import (
+ "fmt"
+ "strings"
+
+ objectstorage "github.com/rackspace/gophercloud/openstack/objectstorage/v1"
+ "github.com/rackspace/gophercloud/pagination"
+
+ "github.com/mitchellh/mapstructure"
+)
+
+// Container represents a container resource.
+type Container struct {
+ // The total number of bytes stored in the container.
+ Bytes int `json:"bytes" mapstructure:"bytes"`
+
+ // The total number of objects stored in the container.
+ Count int `json:"count" mapstructure:"count"`
+
+ // The name of the container.
+ Name string `json:"name" mapstructure:"name"`
+}
+
+// ContainerPage is the page returned by a pager when traversing over a
+// collection of containers.
+type ContainerPage struct {
+ pagination.MarkerPageBase
+}
+
+// IsEmpty returns true if a ListResult contains no container names.
+func (r ContainerPage) IsEmpty() (bool, error) {
+ names, err := ExtractNames(r)
+ if err != nil {
+ return true, err
+ }
+ return len(names) == 0, nil
+}
+
+// LastMarker returns the last container name in a ListResult.
+func (r ContainerPage) LastMarker() (string, error) {
+ names, err := ExtractNames(r)
+ if err != nil {
+ return "", err
+ }
+ if len(names) == 0 {
+ return "", nil
+ }
+ return names[len(names)-1], nil
+}
+
+// ExtractInfo is a function that takes a ListResult and returns the containers' information.
+func ExtractInfo(page pagination.Page) ([]Container, error) {
+ untyped := page.(ContainerPage).Body.([]interface{})
+ results := make([]Container, len(untyped))
+ for index, each := range untyped {
+ container := each.(map[string]interface{})
+ err := mapstructure.Decode(container, &results[index])
+ if err != nil {
+ return results, err
+ }
+ }
+ return results, nil
+}
+
+// ExtractNames is a function that takes a ListResult and returns the containers' names.
+func ExtractNames(page pagination.Page) ([]string, error) {
+ casted := page.(ContainerPage)
+ ct := casted.Header.Get("Content-Type")
+
+ switch {
+ case strings.HasPrefix(ct, "application/json"):
+ parsed, err := ExtractInfo(page)
+ if err != nil {
+ return nil, err
+ }
+
+ names := make([]string, 0, len(parsed))
+ for _, container := range parsed {
+ names = append(names, container.Name)
+ }
+ return names, nil
+ case strings.HasPrefix(ct, "text/plain"):
+ names := make([]string, 0, 50)
+
+ body := string(page.(ContainerPage).Body.([]uint8))
+ for _, name := range strings.Split(body, "\n") {
+ if len(name) > 0 {
+ names = append(names, name)
+ }
+ }
+
+ return names, nil
+ default:
+ return nil, fmt.Errorf("Cannot extract names from response with content-type: [%s]", ct)
+ }
+}
+
+// GetResult represents the result of a get operation.
+type GetResult struct {
+ objectstorage.CommonResult
+}
+
+// ExtractMetadata is a function that takes a GetResult (of type *http.Response)
+// and returns the custom metadata associated with the container.
+func (gr GetResult) ExtractMetadata() (map[string]string, error) {
+ if gr.Err != nil {
+ return nil, gr.Err
+ }
+ metadata := make(map[string]string)
+ for k, v := range gr.Resp.Header {
+ if strings.HasPrefix(k, "X-Container-Meta-") {
+ key := strings.TrimPrefix(k, "X-Container-Meta-")
+ metadata[key] = v[0]
+ }
+ }
+ return metadata, nil
+}
+
+// CreateResult represents the result of a create operation. To extract the
+// the headers from the HTTP response, you can invoke the 'ExtractHeaders'
+// method on the result struct.
+type CreateResult struct {
+ objectstorage.CommonResult
+}
+
+// UpdateResult represents the result of an update operation. To extract the
+// the headers from the HTTP response, you can invoke the 'ExtractHeaders'
+// method on the result struct.
+type UpdateResult struct {
+ objectstorage.CommonResult
+}
+
+// DeleteResult represents the result of a delete operation. To extract the
+// the headers from the HTTP response, you can invoke the 'ExtractHeaders'
+// method on the result struct.
+type DeleteResult struct {
+ objectstorage.CommonResult
+}
diff --git a/_site/openstack/objectstorage/v1/containers/urls.go b/_site/openstack/objectstorage/v1/containers/urls.go
new file mode 100644
index 0000000..f864f84
--- /dev/null
+++ b/_site/openstack/objectstorage/v1/containers/urls.go
@@ -0,0 +1,23 @@
+package containers
+
+import "github.com/rackspace/gophercloud"
+
+func listURL(c *gophercloud.ServiceClient) string {
+ return c.Endpoint
+}
+
+func createURL(c *gophercloud.ServiceClient, container string) string {
+ return c.ServiceURL(container)
+}
+
+func getURL(c *gophercloud.ServiceClient, container string) string {
+ return createURL(c, container)
+}
+
+func deleteURL(c *gophercloud.ServiceClient, container string) string {
+ return createURL(c, container)
+}
+
+func updateURL(c *gophercloud.ServiceClient, container string) string {
+ return createURL(c, container)
+}
diff --git a/_site/openstack/objectstorage/v1/containers/urls_test.go b/_site/openstack/objectstorage/v1/containers/urls_test.go
new file mode 100644
index 0000000..d043a2a
--- /dev/null
+++ b/_site/openstack/objectstorage/v1/containers/urls_test.go
@@ -0,0 +1,43 @@
+package containers
+
+import (
+ "github.com/rackspace/gophercloud"
+ th "github.com/rackspace/gophercloud/testhelper"
+ "testing"
+)
+
+const endpoint = "http://localhost:57909/"
+
+func endpointClient() *gophercloud.ServiceClient {
+ return &gophercloud.ServiceClient{Endpoint: endpoint}
+}
+
+func TestListURL(t *testing.T) {
+ actual := listURL(endpointClient())
+ expected := endpoint
+ th.CheckEquals(t, expected, actual)
+}
+
+func TestCreateURL(t *testing.T) {
+ actual := createURL(endpointClient(), "foo")
+ expected := endpoint + "foo"
+ th.CheckEquals(t, expected, actual)
+}
+
+func TestGetURL(t *testing.T) {
+ actual := getURL(endpointClient(), "foo")
+ expected := endpoint + "foo"
+ th.CheckEquals(t, expected, actual)
+}
+
+func TestDeleteURL(t *testing.T) {
+ actual := deleteURL(endpointClient(), "foo")
+ expected := endpoint + "foo"
+ th.CheckEquals(t, expected, actual)
+}
+
+func TestUpdateURL(t *testing.T) {
+ actual := updateURL(endpointClient(), "foo")
+ expected := endpoint + "foo"
+ th.CheckEquals(t, expected, actual)
+}
diff --git a/_site/openstack/objectstorage/v1/objects/doc.go b/_site/openstack/objectstorage/v1/objects/doc.go
new file mode 100644
index 0000000..30a9add
--- /dev/null
+++ b/_site/openstack/objectstorage/v1/objects/doc.go
@@ -0,0 +1,5 @@
+// Package objects contains functionality for working with Object Storage
+// object resources. An object is a resource that represents and contains data
+// - such as documents, images, and so on. You can also store custom metadata
+// with an object.
+package objects
diff --git a/_site/openstack/objectstorage/v1/objects/requests.go b/_site/openstack/objectstorage/v1/objects/requests.go
new file mode 100644
index 0000000..f6e355d
--- /dev/null
+++ b/_site/openstack/objectstorage/v1/objects/requests.go
@@ -0,0 +1,420 @@
+package objects
+
+import (
+ "fmt"
+ "io"
+ "time"
+
+ "github.com/racker/perigee"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// ListOptsBuilder allows extensions to add additional parameters to the List
+// request.
+type ListOptsBuilder interface {
+ ToObjectListParams() (bool, string, error)
+}
+
+// ListOpts is a structure that holds parameters for listing objects.
+type ListOpts struct {
+ Full bool
+ Limit int `q:"limit"`
+ Marker string `q:"marker"`
+ EndMarker string `q:"end_marker"`
+ Format string `q:"format"`
+ Prefix string `q:"prefix"`
+ Delimiter string `q:"delimiter"`
+ Path string `q:"path"`
+}
+
+// ToObjectListParams formats a ListOpts into a query string and boolean
+// representing whether to list complete information for each object.
+func (opts ListOpts) ToObjectListParams() (bool, string, error) {
+ q, err := gophercloud.BuildQueryString(opts)
+ if err != nil {
+ return false, "", err
+ }
+ return opts.Full, q.String(), nil
+}
+
+// List is a function that retrieves all objects in a container. It also returns the details
+// for the container. To extract only the object information or names, pass the ListResult
+// response to the ExtractInfo or ExtractNames function, respectively.
+func List(c *gophercloud.ServiceClient, containerName string, opts ListOptsBuilder) pagination.Pager {
+ headers := map[string]string{"Accept": "text/plain", "Content-Type": "text/plain"}
+
+ url := listURL(c, containerName)
+ if opts != nil {
+ full, query, err := opts.ToObjectListParams()
+ if err != nil {
+ fmt.Printf("Error building query string: %v", err)
+ return pagination.Pager{Err: err}
+ }
+ url += query
+
+ if full {
+ headers = map[string]string{"Accept": "application/json", "Content-Type": "application/json"}
+ }
+ }
+
+ createPage := func(r pagination.LastHTTPResponse) pagination.Page {
+ p := ObjectPage{pagination.MarkerPageBase{LastHTTPResponse: r}}
+ p.MarkerPageBase.Owner = p
+ return p
+ }
+
+ pager := pagination.NewPager(c, url, createPage)
+ pager.Headers = headers
+ return pager
+}
+
+// DownloadOptsBuilder allows extensions to add additional parameters to the
+// Download request.
+type DownloadOptsBuilder interface {
+ ToObjectDownloadParams() (map[string]string, string, error)
+}
+
+// DownloadOpts is a structure that holds parameters for downloading an object.
+type DownloadOpts struct {
+ IfMatch string `h:"If-Match"`
+ IfModifiedSince time.Time `h:"If-Modified-Since"`
+ IfNoneMatch string `h:"If-None-Match"`
+ IfUnmodifiedSince time.Time `h:"If-Unmodified-Since"`
+ Range string `h:"Range"`
+ Expires string `q:"expires"`
+ MultipartManifest string `q:"multipart-manifest"`
+ Signature string `q:"signature"`
+}
+
+// ToObjectDownloadParams formats a DownloadOpts into a query string and map of
+// headers.
+func (opts ListOpts) ToObjectDownloadParams() (map[string]string, string, error) {
+ q, err := gophercloud.BuildQueryString(opts)
+ if err != nil {
+ return nil, "", err
+ }
+ h, err := gophercloud.BuildHeaders(opts)
+ if err != nil {
+ return nil, q.String(), err
+ }
+ return h, q.String(), nil
+}
+
+// Download is a function that retrieves the content and metadata for an object.
+// To extract just the content, pass the DownloadResult response to the
+// ExtractContent function.
+func Download(c *gophercloud.ServiceClient, containerName, objectName string, opts DownloadOptsBuilder) DownloadResult {
+ var res DownloadResult
+
+ url := downloadURL(c, containerName, objectName)
+ h := c.Provider.AuthenticatedHeaders()
+
+ if opts != nil {
+ headers, query, err := opts.ToObjectDownloadParams()
+ if err != nil {
+ res.Err = err
+ return res
+ }
+
+ for k, v := range headers {
+ h[k] = v
+ }
+
+ url += query
+ }
+
+ resp, err := perigee.Request("GET", url, perigee.Options{
+ MoreHeaders: h,
+ OkCodes: []int{200},
+ })
+ res.Err = err
+ res.Resp = &resp.HttpResponse
+ return res
+}
+
+// CreateOptsBuilder allows extensions to add additional parameters to the
+// Create request.
+type CreateOptsBuilder interface {
+ ToObjectCreateParams() (map[string]string, string, error)
+}
+
+// CreateOpts is a structure that holds parameters for creating an object.
+type CreateOpts struct {
+ Metadata map[string]string
+ ContentDisposition string `h:"Content-Disposition"`
+ ContentEncoding string `h:"Content-Encoding"`
+ ContentLength int `h:"Content-Length"`
+ ContentType string `h:"Content-Type"`
+ CopyFrom string `h:"X-Copy-From"`
+ DeleteAfter int `h:"X-Delete-After"`
+ DeleteAt int `h:"X-Delete-At"`
+ DetectContentType string `h:"X-Detect-Content-Type"`
+ ETag string `h:"ETag"`
+ IfNoneMatch string `h:"If-None-Match"`
+ ObjectManifest string `h:"X-Object-Manifest"`
+ TransferEncoding string `h:"Transfer-Encoding"`
+ Expires string `q:"expires"`
+ MultipartManifest string `q:"multiple-manifest"`
+ Signature string `q:"signature"`
+}
+
+// ToObjectCreateParams formats a CreateOpts into a query string and map of
+// headers.
+func (opts CreateOpts) ToObjectCreateParams() (map[string]string, string, error) {
+ q, err := gophercloud.BuildQueryString(opts)
+ if err != nil {
+ return nil, "", err
+ }
+ h, err := gophercloud.BuildHeaders(opts)
+ if err != nil {
+ return nil, q.String(), err
+ }
+
+ for k, v := range opts.Metadata {
+ h["X-Object-Meta-"+k] = v
+ }
+
+ return h, q.String(), nil
+}
+
+// Create is a function that creates a new object or replaces an existing object.
+func Create(c *gophercloud.ServiceClient, containerName, objectName string, content io.Reader, opts CreateOptsBuilder) CreateResult {
+ var res CreateResult
+ var reqBody []byte
+
+ url := createURL(c, containerName, objectName)
+ h := c.Provider.AuthenticatedHeaders()
+
+ if opts != nil {
+ headers, query, err := opts.ToObjectCreateParams()
+ if err != nil {
+ res.Err = err
+ return res
+ }
+
+ for k, v := range headers {
+ h[k] = v
+ }
+
+ url += query
+ }
+
+ if content != nil {
+ reqBody = make([]byte, 0)
+ _, err := content.Read(reqBody)
+ if err != nil {
+ res.Err = err
+ return res
+ }
+ }
+
+ resp, err := perigee.Request("PUT", url, perigee.Options{
+ ReqBody: reqBody,
+ MoreHeaders: h,
+ OkCodes: []int{201},
+ })
+ res.Resp = &resp.HttpResponse
+ res.Err = err
+ return res
+}
+
+// CopyOptsBuilder allows extensions to add additional parameters to the
+// Copy request.
+type CopyOptsBuilder interface {
+ ToObjectCopyMap() (map[string]string, error)
+}
+
+// CopyOpts is a structure that holds parameters for copying one object to
+// another.
+type CopyOpts struct {
+ Metadata map[string]string
+ ContentDisposition string `h:"Content-Disposition"`
+ ContentEncoding string `h:"Content-Encoding"`
+ ContentType string `h:"Content-Type"`
+ Destination string `h:"Destination,required"`
+}
+
+// ToObjectCopyMap formats a CopyOpts into a map of headers.
+func (opts CopyOpts) ToObjectCopyMap() (map[string]string, error) {
+ if opts.Destination == "" {
+ return nil, fmt.Errorf("Required CopyOpts field 'Destination' not set.")
+ }
+ h, err := gophercloud.BuildHeaders(opts)
+ if err != nil {
+ return nil, err
+ }
+ for k, v := range opts.Metadata {
+ h["X-Object-Meta-"+k] = v
+ }
+ return h, nil
+}
+
+// Copy is a function that copies one object to another.
+func Copy(c *gophercloud.ServiceClient, containerName, objectName string, opts CopyOptsBuilder) CopyResult {
+ var res CopyResult
+ h := c.Provider.AuthenticatedHeaders()
+
+ headers, err := opts.ToObjectCopyMap()
+ if err != nil {
+ res.Err = err
+ return res
+ }
+
+ for k, v := range headers {
+ h[k] = v
+ }
+
+ url := copyURL(c, containerName, objectName)
+ resp, err := perigee.Request("COPY", url, perigee.Options{
+ MoreHeaders: h,
+ OkCodes: []int{201},
+ })
+ res.Resp = &resp.HttpResponse
+ return res
+}
+
+// DeleteOptsBuilder allows extensions to add additional parameters to the
+// Delete request.
+type DeleteOptsBuilder interface {
+ ToObjectDeleteString() (string, error)
+}
+
+// DeleteOpts is a structure that holds parameters for deleting an object.
+type DeleteOpts struct {
+ MultipartManifest string `q:"multipart-manifest"`
+}
+
+// ToObjectDeleteString formats a DeleteOpts into a query string.
+func (opts DeleteOpts) ToObjectDeleteString() (string, error) {
+ q, err := gophercloud.BuildQueryString(opts)
+ if err != nil {
+ return "", err
+ }
+ return q.String(), nil
+}
+
+// Delete is a function that deletes an object.
+func Delete(c *gophercloud.ServiceClient, containerName, objectName string, opts DeleteOptsBuilder) DeleteResult {
+ var res DeleteResult
+ url := deleteURL(c, containerName, objectName)
+
+ if opts != nil {
+ query, err := opts.ToObjectDeleteString()
+ if err != nil {
+ res.Err = err
+ return res
+ }
+ url += query
+ }
+
+ resp, err := perigee.Request("DELETE", url, perigee.Options{
+ MoreHeaders: c.Provider.AuthenticatedHeaders(),
+ OkCodes: []int{204},
+ })
+ res.Resp = &resp.HttpResponse
+ res.Err = err
+ return res
+}
+
+// GetOptsBuilder allows extensions to add additional parameters to the
+// Get request.
+type GetOptsBuilder interface {
+ ToObjectGetString() (string, error)
+}
+
+// GetOpts is a structure that holds parameters for getting an object's metadata.
+type GetOpts struct {
+ Expires string `q:"expires"`
+ Signature string `q:"signature"`
+}
+
+// ToObjectGetString formats a GetOpts into a query string.
+func (opts GetOpts) ToObjectGetString() (string, error) {
+ q, err := gophercloud.BuildQueryString(opts)
+ if err != nil {
+ return "", err
+ }
+ return q.String(), nil
+}
+
+// Get is a function that retrieves the metadata of an object. To extract just the custom
+// metadata, pass the GetResult response to the ExtractMetadata function.
+func Get(c *gophercloud.ServiceClient, containerName, objectName string, opts GetOptsBuilder) GetResult {
+ var res GetResult
+ url := getURL(c, containerName, objectName)
+
+ if opts != nil {
+ query, err := opts.ToObjectGetString()
+ if err != nil {
+ res.Err = err
+ return res
+ }
+ url += query
+ }
+
+ resp, err := perigee.Request("HEAD", url, perigee.Options{
+ MoreHeaders: c.Provider.AuthenticatedHeaders(),
+ OkCodes: []int{200, 204},
+ })
+ res.Err = err
+ res.Resp = &resp.HttpResponse
+ return res
+}
+
+// UpdateOptsBuilder allows extensions to add additional parameters to the
+// Update request.
+type UpdateOptsBuilder interface {
+ ToObjectUpdateMap() (map[string]string, error)
+}
+
+// UpdateOpts is a structure that holds parameters for updating, creating, or deleting an
+// object's metadata.
+type UpdateOpts struct {
+ Metadata map[string]string
+ ContentDisposition string `h:"Content-Disposition"`
+ ContentEncoding string `h:"Content-Encoding"`
+ ContentType string `h:"Content-Type"`
+ DeleteAfter int `h:"X-Delete-After"`
+ DeleteAt int `h:"X-Delete-At"`
+ DetectContentType bool `h:"X-Detect-Content-Type"`
+}
+
+// ToObjectUpdateMap formats a UpdateOpts into a map of headers.
+func (opts UpdateOpts) ToObjectUpdateMap() (map[string]string, error) {
+ h, err := gophercloud.BuildHeaders(opts)
+ if err != nil {
+ return nil, err
+ }
+ for k, v := range opts.Metadata {
+ h["X-Object-Meta-"+k] = v
+ }
+ return h, nil
+}
+
+// Update is a function that creates, updates, or deletes an object's metadata.
+func Update(c *gophercloud.ServiceClient, containerName, objectName string, opts UpdateOptsBuilder) UpdateResult {
+ var res UpdateResult
+ h := c.Provider.AuthenticatedHeaders()
+
+ if opts != nil {
+ headers, err := opts.ToObjectUpdateMap()
+ if err != nil {
+ res.Err = err
+ return res
+ }
+
+ for k, v := range headers {
+ h[k] = v
+ }
+ }
+
+ url := updateURL(c, containerName, objectName)
+ resp, err := perigee.Request("POST", url, perigee.Options{
+ MoreHeaders: h,
+ OkCodes: []int{202},
+ })
+ res.Resp = &resp.HttpResponse
+ res.Err = err
+ return res
+}
diff --git a/_site/openstack/objectstorage/v1/objects/requests_test.go b/_site/openstack/objectstorage/v1/objects/requests_test.go
new file mode 100644
index 0000000..11d7c44
--- /dev/null
+++ b/_site/openstack/objectstorage/v1/objects/requests_test.go
@@ -0,0 +1,248 @@
+package objects
+
+import (
+ "bytes"
+ "fmt"
+ "net/http"
+ "testing"
+
+ "github.com/rackspace/gophercloud/pagination"
+ "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+var metadata = map[string]string{"Gophercloud-Test": "objects"}
+
+func TestDownloadObject(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ testhelper.Mux.HandleFunc("/testContainer/testObject", func(w http.ResponseWriter, r *http.Request) {
+ testhelper.TestMethod(t, r, "GET")
+ testhelper.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ testhelper.TestHeader(t, r, "Accept", "application/json")
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprintf(w, "Successful download with Gophercloud")
+ })
+
+ content, err := Download(fake.ServiceClient(), "testContainer", "testObject", nil).ExtractContent()
+ if err != nil {
+ t.Fatalf("Unexpected error downloading object: %v", err)
+ }
+ if string(content) != "Successful download with Gophercloud" {
+ t.Errorf("Expected %s, got %s", "Successful download with Gophercloud", content)
+ }
+}
+
+func TestListObjectInfo(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ testhelper.Mux.HandleFunc("/testContainer", func(w http.ResponseWriter, r *http.Request) {
+ testhelper.TestMethod(t, r, "GET")
+ testhelper.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ testhelper.TestHeader(t, r, "Accept", "application/json")
+
+ w.Header().Set("Content-Type", "application/json")
+ r.ParseForm()
+ marker := r.Form.Get("marker")
+ switch marker {
+ case "":
+ fmt.Fprintf(w, `[
+ {
+ "hash": "451e372e48e0f6b1114fa0724aa79fa1",
+ "last_modified": "2009-11-10 23:00:00 +0000 UTC",
+ "bytes": 14,
+ "name": "goodbye",
+ "content_type": "application/octet-stream"
+ },
+ {
+ "hash": "451e372e48e0f6b1114fa0724aa79fa1",
+ "last_modified": "2009-11-10 23:00:00 +0000 UTC",
+ "bytes": 14,
+ "name": "hello",
+ "content_type": "application/octet-stream"
+ }
+ ]`)
+ case "hello":
+ fmt.Fprintf(w, `[]`)
+ default:
+ t.Fatalf("Unexpected marker: [%s]", marker)
+ }
+ })
+
+ count := 0
+
+ err := List(fake.ServiceClient(), "testContainer", &ListOpts{Full: true}).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractInfo(page)
+ if err != nil {
+ t.Errorf("Failed to extract object info: %v", err)
+ return false, err
+ }
+
+ expected := []Object{
+ Object{
+ Hash: "451e372e48e0f6b1114fa0724aa79fa1",
+ LastModified: "2009-11-10 23:00:00 +0000 UTC",
+ Bytes: 14,
+ Name: "goodbye",
+ ContentType: "application/octet-stream",
+ },
+ Object{
+ Hash: "451e372e48e0f6b1114fa0724aa79fa1",
+ LastModified: "2009-11-10 23:00:00 +0000 UTC",
+ Bytes: 14,
+ Name: "hello",
+ ContentType: "application/octet-stream",
+ },
+ }
+
+ testhelper.CheckDeepEquals(t, actual, expected)
+
+ return true, nil
+ })
+ if err != nil {
+ t.Error(err)
+ }
+
+ if count != 1 {
+ t.Errorf("Expected 1 page, got %d", count)
+ }
+}
+
+func TestListObjectNames(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ testhelper.Mux.HandleFunc("/testContainer", func(w http.ResponseWriter, r *http.Request) {
+ testhelper.TestMethod(t, r, "GET")
+ testhelper.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ testhelper.TestHeader(t, r, "Accept", "text/plain")
+
+ w.Header().Set("Content-Type", "text/plain")
+ r.ParseForm()
+ marker := r.Form.Get("marker")
+ switch marker {
+ case "":
+ fmt.Fprintf(w, "hello\ngoodbye\n")
+ case "goodbye":
+ fmt.Fprintf(w, "")
+ default:
+ t.Fatalf("Unexpected marker: [%s]", marker)
+ }
+ })
+
+ count := 0
+ List(fake.ServiceClient(), "testContainer", &ListOpts{Full: false}).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractNames(page)
+ if err != nil {
+ t.Errorf("Failed to extract object names: %v", err)
+ return false, err
+ }
+
+ expected := []string{"hello", "goodbye"}
+
+ testhelper.CheckDeepEquals(t, expected, actual)
+
+ return true, nil
+ })
+
+ if count != 1 {
+ t.Errorf("Expected 1 page, got %d", count)
+ }
+}
+
+func TestCreateObject(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ testhelper.Mux.HandleFunc("/testContainer/testObject", func(w http.ResponseWriter, r *http.Request) {
+ testhelper.TestMethod(t, r, "PUT")
+ testhelper.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ testhelper.TestHeader(t, r, "Accept", "application/json")
+ w.WriteHeader(http.StatusCreated)
+ })
+
+ content := bytes.NewBufferString("Did gyre and gimble in the wabe")
+ options := &CreateOpts{ContentType: "application/json"}
+ _, err := Create(fake.ServiceClient(), "testContainer", "testObject", content, options).ExtractHeaders()
+ if err != nil {
+ t.Fatalf("Unexpected error creating object: %v", err)
+ }
+}
+
+func TestCopyObject(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ testhelper.Mux.HandleFunc("/testContainer/testObject", func(w http.ResponseWriter, r *http.Request) {
+ testhelper.TestMethod(t, r, "COPY")
+ testhelper.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ testhelper.TestHeader(t, r, "Accept", "application/json")
+ testhelper.TestHeader(t, r, "Destination", "/newTestContainer/newTestObject")
+ w.WriteHeader(http.StatusCreated)
+ })
+
+ options := &CopyOpts{Destination: "/newTestContainer/newTestObject"}
+ _, err := Copy(fake.ServiceClient(), "testContainer", "testObject", options).ExtractHeaders()
+ if err != nil {
+ t.Fatalf("Unexpected error copying object: %v", err)
+ }
+}
+
+func TestDeleteObject(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ testhelper.Mux.HandleFunc("/testContainer/testObject", func(w http.ResponseWriter, r *http.Request) {
+ testhelper.TestMethod(t, r, "DELETE")
+ testhelper.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ testhelper.TestHeader(t, r, "Accept", "application/json")
+ w.WriteHeader(http.StatusNoContent)
+ })
+
+ _, err := Delete(fake.ServiceClient(), "testContainer", "testObject", nil).ExtractHeaders()
+ if err != nil {
+ t.Fatalf("Unexpected error deleting object: %v", err)
+ }
+}
+
+func TestUpateObjectMetadata(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ testhelper.Mux.HandleFunc("/testContainer/testObject", func(w http.ResponseWriter, r *http.Request) {
+ testhelper.TestMethod(t, r, "POST")
+ testhelper.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ testhelper.TestHeader(t, r, "Accept", "application/json")
+ testhelper.TestHeader(t, r, "X-Object-Meta-Gophercloud-Test", "objects")
+ w.WriteHeader(http.StatusAccepted)
+ })
+
+ _, err := Update(fake.ServiceClient(), "testContainer", "testObject", &UpdateOpts{Metadata: metadata}).ExtractHeaders()
+ if err != nil {
+ t.Fatalf("Unexpected error updating object metadata: %v", err)
+ }
+}
+
+func TestGetObject(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ testhelper.Mux.HandleFunc("/testContainer/testObject", func(w http.ResponseWriter, r *http.Request) {
+ testhelper.TestMethod(t, r, "HEAD")
+ testhelper.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ testhelper.TestHeader(t, r, "Accept", "application/json")
+ w.Header().Add("X-Object-Meta-Gophercloud-Test", "objects")
+ w.WriteHeader(http.StatusNoContent)
+ })
+
+ expected := metadata
+ actual, err := Get(fake.ServiceClient(), "testContainer", "testObject", nil).ExtractMetadata()
+ if err != nil {
+ t.Fatalf("Unexpected error getting object metadata: %v", err)
+ }
+ testhelper.CheckDeepEquals(t, expected, actual)
+}
diff --git a/_site/openstack/objectstorage/v1/objects/results.go b/_site/openstack/objectstorage/v1/objects/results.go
new file mode 100644
index 0000000..1dda7a3
--- /dev/null
+++ b/_site/openstack/objectstorage/v1/objects/results.go
@@ -0,0 +1,158 @@
+package objects
+
+import (
+ "fmt"
+ "io/ioutil"
+ "strings"
+
+ objectstorage "github.com/rackspace/gophercloud/openstack/objectstorage/v1"
+ "github.com/rackspace/gophercloud/pagination"
+
+ "github.com/mitchellh/mapstructure"
+)
+
+// Object is a structure that holds information related to a storage object.
+type Object struct {
+ Bytes int `json:"bytes" mapstructure:"bytes"`
+ ContentType string `json:"content_type" mapstructure:"content_type"`
+ Hash string `json:"hash" mapstructure:"hash"`
+ LastModified string `json:"last_modified" mapstructure:"last_modified"`
+ Name string `json:"name" mapstructure:"name"`
+}
+
+// ObjectPage is a single page of objects that is returned from a call to the
+// List function.
+type ObjectPage struct {
+ pagination.MarkerPageBase
+}
+
+// IsEmpty returns true if a ListResult contains no object names.
+func (r ObjectPage) IsEmpty() (bool, error) {
+ names, err := ExtractNames(r)
+ if err != nil {
+ return true, err
+ }
+ return len(names) == 0, nil
+}
+
+// LastMarker returns the last object name in a ListResult.
+func (r ObjectPage) LastMarker() (string, error) {
+ names, err := ExtractNames(r)
+ if err != nil {
+ return "", err
+ }
+ if len(names) == 0 {
+ return "", nil
+ }
+ return names[len(names)-1], nil
+}
+
+// ExtractInfo is a function that takes a page of objects and returns their full information.
+func ExtractInfo(page pagination.Page) ([]Object, error) {
+ untyped := page.(ObjectPage).Body.([]interface{})
+ results := make([]Object, len(untyped))
+ for index, each := range untyped {
+ object := each.(map[string]interface{})
+ err := mapstructure.Decode(object, &results[index])
+ if err != nil {
+ return results, err
+ }
+ }
+ return results, nil
+}
+
+// ExtractNames is a function that takes a page of objects and returns only their names.
+func ExtractNames(page pagination.Page) ([]string, error) {
+ casted := page.(ObjectPage)
+ ct := casted.Header.Get("Content-Type")
+ switch {
+ case strings.HasPrefix(ct, "application/json"):
+ parsed, err := ExtractInfo(page)
+ if err != nil {
+ return nil, err
+ }
+
+ names := make([]string, 0, len(parsed))
+ for _, object := range parsed {
+ names = append(names, object.Name)
+ }
+
+ return names, nil
+ case strings.HasPrefix(ct, "text/plain"):
+ names := make([]string, 0, 50)
+
+ body := string(page.(ObjectPage).Body.([]uint8))
+ for _, name := range strings.Split(body, "\n") {
+ if len(name) > 0 {
+ names = append(names, name)
+ }
+ }
+
+ return names, nil
+ case strings.HasPrefix(ct, "text/html"):
+ return []string{}, nil
+ default:
+ return nil, fmt.Errorf("Cannot extract names from response with content-type: [%s]", ct)
+ }
+}
+
+// DownloadResult is a *http.Response that is returned from a call to the Download function.
+type DownloadResult struct {
+ objectstorage.CommonResult
+}
+
+// ExtractContent is a function that takes a DownloadResult (of type *http.Response)
+// and returns the object's content.
+func (dr DownloadResult) ExtractContent() ([]byte, error) {
+ if dr.Err != nil {
+ return nil, nil
+ }
+ var body []byte
+ defer dr.Resp.Body.Close()
+ body, err := ioutil.ReadAll(dr.Resp.Body)
+ if err != nil {
+ return body, fmt.Errorf("Error trying to read DownloadResult body: %v", err)
+ }
+ return body, nil
+}
+
+// GetResult is a *http.Response that is returned from a call to the Get function.
+type GetResult struct {
+ objectstorage.CommonResult
+}
+
+// ExtractMetadata is a function that takes a GetResult (of type *http.Response)
+// and returns the custom metadata associated with the object.
+func (gr GetResult) ExtractMetadata() (map[string]string, error) {
+ if gr.Err != nil {
+ return nil, gr.Err
+ }
+ metadata := make(map[string]string)
+ for k, v := range gr.Resp.Header {
+ if strings.HasPrefix(k, "X-Object-Meta-") {
+ key := strings.TrimPrefix(k, "X-Object-Meta-")
+ metadata[key] = v[0]
+ }
+ }
+ return metadata, nil
+}
+
+// CreateResult represents the result of a create operation.
+type CreateResult struct {
+ objectstorage.CommonResult
+}
+
+// UpdateResult represents the result of an update operation.
+type UpdateResult struct {
+ objectstorage.CommonResult
+}
+
+// DeleteResult represents the result of a delete operation.
+type DeleteResult struct {
+ objectstorage.CommonResult
+}
+
+// CopyResult represents the result of a copy operation.
+type CopyResult struct {
+ objectstorage.CommonResult
+}
diff --git a/_site/openstack/objectstorage/v1/objects/urls.go b/_site/openstack/objectstorage/v1/objects/urls.go
new file mode 100644
index 0000000..d2ec62c
--- /dev/null
+++ b/_site/openstack/objectstorage/v1/objects/urls.go
@@ -0,0 +1,33 @@
+package objects
+
+import (
+ "github.com/rackspace/gophercloud"
+)
+
+func listURL(c *gophercloud.ServiceClient, container string) string {
+ return c.ServiceURL(container)
+}
+
+func copyURL(c *gophercloud.ServiceClient, container, object string) string {
+ return c.ServiceURL(container, object)
+}
+
+func createURL(c *gophercloud.ServiceClient, container, object string) string {
+ return copyURL(c, container, object)
+}
+
+func getURL(c *gophercloud.ServiceClient, container, object string) string {
+ return copyURL(c, container, object)
+}
+
+func deleteURL(c *gophercloud.ServiceClient, container, object string) string {
+ return copyURL(c, container, object)
+}
+
+func downloadURL(c *gophercloud.ServiceClient, container, object string) string {
+ return copyURL(c, container, object)
+}
+
+func updateURL(c *gophercloud.ServiceClient, container, object string) string {
+ return copyURL(c, container, object)
+}
diff --git a/_site/openstack/objectstorage/v1/objects/urls_test.go b/_site/openstack/objectstorage/v1/objects/urls_test.go
new file mode 100644
index 0000000..1dcfe35
--- /dev/null
+++ b/_site/openstack/objectstorage/v1/objects/urls_test.go
@@ -0,0 +1,56 @@
+package objects
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+const endpoint = "http://localhost:57909/"
+
+func endpointClient() *gophercloud.ServiceClient {
+ return &gophercloud.ServiceClient{Endpoint: endpoint}
+}
+
+func TestListURL(t *testing.T) {
+ actual := listURL(endpointClient(), "foo")
+ expected := endpoint + "foo"
+ th.CheckEquals(t, expected, actual)
+}
+
+func TestCopyURL(t *testing.T) {
+ actual := copyURL(endpointClient(), "foo", "bar")
+ expected := endpoint + "foo/bar"
+ th.CheckEquals(t, expected, actual)
+}
+
+func TestCreateURL(t *testing.T) {
+ actual := createURL(endpointClient(), "foo", "bar")
+ expected := endpoint + "foo/bar"
+ th.CheckEquals(t, expected, actual)
+}
+
+func TestGetURL(t *testing.T) {
+ actual := getURL(endpointClient(), "foo", "bar")
+ expected := endpoint + "foo/bar"
+ th.CheckEquals(t, expected, actual)
+}
+
+func TestDeleteURL(t *testing.T) {
+ actual := deleteURL(endpointClient(), "foo", "bar")
+ expected := endpoint + "foo/bar"
+ th.CheckEquals(t, expected, actual)
+}
+
+func TestDownloadURL(t *testing.T) {
+ actual := downloadURL(endpointClient(), "foo", "bar")
+ expected := endpoint + "foo/bar"
+ th.CheckEquals(t, expected, actual)
+}
+
+func TestUpdateURL(t *testing.T) {
+ actual := updateURL(endpointClient(), "foo", "bar")
+ expected := endpoint + "foo/bar"
+ th.CheckEquals(t, expected, actual)
+}
diff --git a/_site/openstack/utils/choose_version.go b/_site/openstack/utils/choose_version.go
new file mode 100644
index 0000000..753f8f8
--- /dev/null
+++ b/_site/openstack/utils/choose_version.go
@@ -0,0 +1,114 @@
+package utils
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/racker/perigee"
+)
+
+// Version is a supported API version, corresponding to a vN package within the appropriate service.
+type Version struct {
+ ID string
+ Suffix string
+ Priority int
+}
+
+var goodStatus = map[string]bool{
+ "current": true,
+ "supported": true,
+ "stable": true,
+}
+
+// ChooseVersion queries the base endpoint of a API to choose the most recent non-experimental alternative from a service's
+// published versions.
+// It returns the highest-Priority Version among the alternatives that are provided, as well as its corresponding endpoint.
+func ChooseVersion(identityBase string, identityEndpoint string, recognized []*Version) (*Version, string, error) {
+ type linkResp struct {
+ Href string `json:"href"`
+ Rel string `json:"rel"`
+ }
+
+ type valueResp struct {
+ ID string `json:"id"`
+ Status string `json:"status"`
+ Links []linkResp `json:"links"`
+ }
+
+ type versionsResp struct {
+ Values []valueResp `json:"values"`
+ }
+
+ type response struct {
+ Versions versionsResp `json:"versions"`
+ }
+
+ normalize := func(endpoint string) string {
+ if !strings.HasSuffix(endpoint, "/") {
+ return endpoint + "/"
+ }
+ return endpoint
+ }
+ identityEndpoint = normalize(identityEndpoint)
+
+ // If a full endpoint is specified, check version suffixes for a match first.
+ for _, v := range recognized {
+ if strings.HasSuffix(identityEndpoint, v.Suffix) {
+ return v, identityEndpoint, nil
+ }
+ }
+
+ var resp response
+ _, err := perigee.Request("GET", identityBase, perigee.Options{
+ Results: &resp,
+ OkCodes: []int{200, 300},
+ })
+
+ if err != nil {
+ return nil, "", err
+ }
+
+ byID := make(map[string]*Version)
+ for _, version := range recognized {
+ byID[version.ID] = version
+ }
+
+ var highest *Version
+ var endpoint string
+
+ for _, value := range resp.Versions.Values {
+ href := ""
+ for _, link := range value.Links {
+ if link.Rel == "self" {
+ href = normalize(link.Href)
+ }
+ }
+
+ if matching, ok := byID[value.ID]; ok {
+ // Prefer a version that exactly matches the provided endpoint.
+ if href == identityEndpoint {
+ if href == "" {
+ return nil, "", fmt.Errorf("Endpoint missing in version %s response from %s", value.ID, identityBase)
+ }
+ return matching, href, nil
+ }
+
+ // Otherwise, find the highest-priority version with a whitelisted status.
+ if goodStatus[strings.ToLower(value.Status)] {
+ if highest == nil || matching.Priority > highest.Priority {
+ highest = matching
+ endpoint = href
+ }
+ }
+ }
+ }
+
+ if highest == nil {
+ return nil, "", fmt.Errorf("No supported version available from endpoint %s", identityBase)
+ }
+ if endpoint == "" {
+ return nil, "", fmt.Errorf("Endpoint missing in version %s response from %s", highest.ID, identityBase)
+ }
+
+ return highest, endpoint, nil
+}
diff --git a/_site/openstack/utils/choose_version_test.go b/_site/openstack/utils/choose_version_test.go
new file mode 100644
index 0000000..9552696
--- /dev/null
+++ b/_site/openstack/utils/choose_version_test.go
@@ -0,0 +1,105 @@
+package utils
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ "github.com/rackspace/gophercloud/testhelper"
+)
+
+func setupVersionHandler() {
+ testhelper.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+ fmt.Fprintf(w, `
+ {
+ "versions": {
+ "values": [
+ {
+ "status": "stable",
+ "id": "v3.0",
+ "links": [
+ { "href": "%s/v3.0", "rel": "self" }
+ ]
+ },
+ {
+ "status": "stable",
+ "id": "v2.0",
+ "links": [
+ { "href": "%s/v2.0", "rel": "self" }
+ ]
+ }
+ ]
+ }
+ }
+ `, testhelper.Server.URL, testhelper.Server.URL)
+ })
+}
+
+func TestChooseVersion(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+ setupVersionHandler()
+
+ v2 := &Version{ID: "v2.0", Priority: 2, Suffix: "blarg"}
+ v3 := &Version{ID: "v3.0", Priority: 3, Suffix: "hargl"}
+
+ v, endpoint, err := ChooseVersion(testhelper.Endpoint(), "", []*Version{v2, v3})
+
+ if err != nil {
+ t.Fatalf("Unexpected error from ChooseVersion: %v", err)
+ }
+
+ if v != v3 {
+ t.Errorf("Expected %#v to win, but %#v did instead", v3, v)
+ }
+
+ expected := testhelper.Endpoint() + "v3.0/"
+ if endpoint != expected {
+ t.Errorf("Expected endpoint [%s], but was [%s] instead", expected, endpoint)
+ }
+}
+
+func TestChooseVersionOpinionatedLink(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+ setupVersionHandler()
+
+ v2 := &Version{ID: "v2.0", Priority: 2, Suffix: "nope"}
+ v3 := &Version{ID: "v3.0", Priority: 3, Suffix: "northis"}
+
+ v, endpoint, err := ChooseVersion(testhelper.Endpoint(), testhelper.Endpoint()+"v2.0/", []*Version{v2, v3})
+ if err != nil {
+ t.Fatalf("Unexpected error from ChooseVersion: %v", err)
+ }
+
+ if v != v2 {
+ t.Errorf("Expected %#v to win, but %#v did instead", v2, v)
+ }
+
+ expected := testhelper.Endpoint() + "v2.0/"
+ if endpoint != expected {
+ t.Errorf("Expected endpoint [%s], but was [%s] instead", expected, endpoint)
+ }
+}
+
+func TestChooseVersionFromSuffix(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ v2 := &Version{ID: "v2.0", Priority: 2, Suffix: "/v2.0/"}
+ v3 := &Version{ID: "v3.0", Priority: 3, Suffix: "/v3.0/"}
+
+ v, endpoint, err := ChooseVersion(testhelper.Endpoint(), testhelper.Endpoint()+"v2.0/", []*Version{v2, v3})
+ if err != nil {
+ t.Fatalf("Unexpected error from ChooseVersion: %v", err)
+ }
+
+ if v != v2 {
+ t.Errorf("Expected %#v to win, but %#v did instead", v2, v)
+ }
+
+ expected := testhelper.Endpoint() + "v2.0/"
+ if endpoint != expected {
+ t.Errorf("Expected endpoint [%s], but was [%s] instead", expected, endpoint)
+ }
+}
diff --git a/_site/openstack/utils/utils.go b/_site/openstack/utils/utils.go
new file mode 100644
index 0000000..1d09d9e
--- /dev/null
+++ b/_site/openstack/utils/utils.go
@@ -0,0 +1,73 @@
+// Package utils contains utilities which eases working with Gophercloud's OpenStack APIs.
+package utils
+
+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 AuthOptions() (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
+}
+
+// BuildQuery constructs the query section of a URI from a map.
+func BuildQuery(params map[string]string) string {
+ if len(params) == 0 {
+ return ""
+ }
+
+ query := "?"
+ for k, v := range params {
+ query += k + "=" + v + "&"
+ }
+ query = query[:len(query)-1]
+ return query
+}
diff --git a/_site/package.go b/_site/package.go
new file mode 100644
index 0000000..fb6f24a
--- /dev/null
+++ b/_site/package.go
@@ -0,0 +1,4 @@
+// Package gophercloud provides a multi-vendor interface to OpenStack-compatible
+// clouds. The package attempts to follow established community standards and
+// golang idioms. Contributions are welcome!
+package gophercloud
diff --git a/_site/pagination/http.go b/_site/pagination/http.go
new file mode 100644
index 0000000..dd2c2d7
--- /dev/null
+++ b/_site/pagination/http.go
@@ -0,0 +1,65 @@
+package pagination
+
+import (
+ "encoding/json"
+ "io/ioutil"
+ "net/http"
+ "net/url"
+ "strings"
+
+ "github.com/racker/perigee"
+ "github.com/rackspace/gophercloud"
+)
+
+// LastHTTPResponse stores generic information derived from an HTTP response.
+// This exists primarily because the body of an http.Response can only be used once.
+type LastHTTPResponse struct {
+ url.URL
+ http.Header
+ Body interface{}
+}
+
+// RememberHTTPResponse parses an HTTP response as JSON and returns a LastHTTPResponse containing the results.
+// The main reason to do this instead of holding the response directly is that a response body can only be read once.
+// Also, this centralizes the JSON decoding.
+func RememberHTTPResponse(resp http.Response) (LastHTTPResponse, error) {
+ var parsedBody interface{}
+
+ defer resp.Body.Close()
+ rawBody, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ return LastHTTPResponse{}, err
+ }
+
+ if strings.HasPrefix(resp.Header.Get("Content-Type"), "application/json") {
+ err = json.Unmarshal(rawBody, &parsedBody)
+ if err != nil {
+ return LastHTTPResponse{}, err
+ }
+ } else {
+ parsedBody = rawBody
+ }
+
+ return LastHTTPResponse{
+ URL: *resp.Request.URL,
+ Header: resp.Header,
+ Body: parsedBody,
+ }, err
+}
+
+// Request performs a Perigee request and extracts the http.Response from the result.
+func Request(client *gophercloud.ServiceClient, headers map[string]string, url string) (http.Response, error) {
+ h := client.Provider.AuthenticatedHeaders()
+ for key, value := range headers {
+ h[key] = value
+ }
+
+ resp, err := perigee.Request("GET", url, perigee.Options{
+ MoreHeaders: h,
+ OkCodes: []int{200, 204},
+ })
+ if err != nil {
+ return http.Response{}, err
+ }
+ return resp.HttpResponse, nil
+}
diff --git a/_site/pagination/linked.go b/_site/pagination/linked.go
new file mode 100644
index 0000000..0376edb
--- /dev/null
+++ b/_site/pagination/linked.go
@@ -0,0 +1,63 @@
+package pagination
+
+import "fmt"
+
+// LinkedPageBase may be embedded to implement a page that provides navigational "Next" and "Previous" links within its result.
+type LinkedPageBase struct {
+ LastHTTPResponse
+
+ // LinkPath lists the keys that should be traversed within a response to arrive at the "next" pointer.
+ // If any link along the path is missing, an empty URL will be returned.
+ // If any link results in an unexpected value type, an error will be returned.
+ // When left as "nil", []string{"links", "next"} will be used as a default.
+ LinkPath []string
+}
+
+// NextPageURL extracts the pagination structure from a JSON response and returns the "next" link, if one is present.
+// It assumes that the links are available in a "links" element of the top-level response object.
+// If this is not the case, override NextPageURL on your result type.
+func (current LinkedPageBase) NextPageURL() (string, error) {
+ var path []string
+ var key string
+
+ if current.LinkPath == nil {
+ path = []string{"links", "next"}
+ } else {
+ path = current.LinkPath
+ }
+
+ submap, ok := current.Body.(map[string]interface{})
+ if !ok {
+ return "", fmt.Errorf("Expected an object, but was %#v", current.Body)
+ }
+
+ for {
+ key, path = path[0], path[1:len(path)]
+
+ value, ok := submap[key]
+ if !ok {
+ return "", nil
+ }
+
+ fmt.Printf("key = %#v, path = %#v, value = %#v\n", key, path, value)
+
+ if len(path) > 0 {
+ submap, ok = value.(map[string]interface{})
+ if !ok {
+ return "", fmt.Errorf("Expected an object, but was %#v", value)
+ }
+ } else {
+ if value == nil {
+ // Actual null element.
+ return "", nil
+ }
+
+ url, ok := value.(string)
+ if !ok {
+ return "", fmt.Errorf("Expected a string, but was %#v", value)
+ }
+
+ return url, nil
+ }
+ }
+}
diff --git a/_site/pagination/linked_test.go b/_site/pagination/linked_test.go
new file mode 100644
index 0000000..2621f98
--- /dev/null
+++ b/_site/pagination/linked_test.go
@@ -0,0 +1,107 @@
+package pagination
+
+import (
+ "fmt"
+ "net/http"
+ "reflect"
+ "testing"
+
+ "github.com/mitchellh/mapstructure"
+ "github.com/rackspace/gophercloud/testhelper"
+)
+
+// LinkedPager sample and test cases.
+
+type LinkedPageResult struct {
+ LinkedPageBase
+}
+
+func (r LinkedPageResult) IsEmpty() (bool, error) {
+ is, err := ExtractLinkedInts(r)
+ if err != nil {
+ return true, nil
+ }
+ return len(is) == 0, nil
+}
+
+func ExtractLinkedInts(page Page) ([]int, error) {
+ var response struct {
+ Ints []int `mapstructure:"ints"`
+ }
+
+ err := mapstructure.Decode(page.(LinkedPageResult).Body, &response)
+ if err != nil {
+ return nil, err
+ }
+
+ return response.Ints, nil
+}
+
+func createLinked(t *testing.T) Pager {
+ testhelper.SetupHTTP()
+
+ testhelper.Mux.HandleFunc("/page1", func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Add("Content-Type", "application/json")
+ fmt.Fprintf(w, `{ "ints": [1, 2, 3], "links": { "next": "%s/page2" } }`, testhelper.Server.URL)
+ })
+
+ testhelper.Mux.HandleFunc("/page2", func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Add("Content-Type", "application/json")
+ fmt.Fprintf(w, `{ "ints": [4, 5, 6], "links": { "next": "%s/page3" } }`, testhelper.Server.URL)
+ })
+
+ testhelper.Mux.HandleFunc("/page3", func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Add("Content-Type", "application/json")
+ fmt.Fprintf(w, `{ "ints": [7, 8, 9], "links": { "next": null } }`)
+ })
+
+ client := createClient()
+
+ createPage := func(r LastHTTPResponse) Page {
+ return LinkedPageResult{LinkedPageBase{LastHTTPResponse: r}}
+ }
+
+ return NewPager(client, testhelper.Server.URL+"/page1", createPage)
+}
+
+func TestEnumerateLinked(t *testing.T) {
+ pager := createLinked(t)
+ defer testhelper.TeardownHTTP()
+
+ callCount := 0
+ err := pager.EachPage(func(page Page) (bool, error) {
+ actual, err := ExtractLinkedInts(page)
+ if err != nil {
+ return false, err
+ }
+
+ t.Logf("Handler invoked with %v", actual)
+
+ var expected []int
+ switch callCount {
+ case 0:
+ expected = []int{1, 2, 3}
+ case 1:
+ expected = []int{4, 5, 6}
+ case 2:
+ expected = []int{7, 8, 9}
+ default:
+ t.Fatalf("Unexpected call count: %d", callCount)
+ return false, nil
+ }
+
+ if !reflect.DeepEqual(expected, actual) {
+ t.Errorf("Call %d: Expected %#v, but was %#v", callCount, expected, actual)
+ }
+
+ callCount++
+ return true, nil
+ })
+ if err != nil {
+ t.Errorf("Unexpected error for page iteration: %v", err)
+ }
+
+ if callCount != 3 {
+ t.Errorf("Expected 3 calls, but was %d", callCount)
+ }
+}
diff --git a/_site/pagination/marker.go b/_site/pagination/marker.go
new file mode 100644
index 0000000..41b493a
--- /dev/null
+++ b/_site/pagination/marker.go
@@ -0,0 +1,34 @@
+package pagination
+
+// MarkerPage is a stricter Page interface that describes additional functionality required for use with NewMarkerPager.
+// For convenience, embed the MarkedPageBase struct.
+type MarkerPage interface {
+ Page
+
+ // LastMarker returns the last "marker" value on this page.
+ LastMarker() (string, error)
+}
+
+// MarkerPageBase is a page in a collection that's paginated by "limit" and "marker" query parameters.
+type MarkerPageBase struct {
+ LastHTTPResponse
+
+ // Owner is a reference to the embedding struct.
+ Owner MarkerPage
+}
+
+// NextPageURL generates the URL for the page of results after this one.
+func (current MarkerPageBase) NextPageURL() (string, error) {
+ currentURL := current.URL
+
+ mark, err := current.Owner.LastMarker()
+ if err != nil {
+ return "", err
+ }
+
+ q := currentURL.Query()
+ q.Set("marker", mark)
+ currentURL.RawQuery = q.Encode()
+
+ return currentURL.String(), nil
+}
diff --git a/_site/pagination/marker_test.go b/_site/pagination/marker_test.go
new file mode 100644
index 0000000..e30264c
--- /dev/null
+++ b/_site/pagination/marker_test.go
@@ -0,0 +1,113 @@
+package pagination
+
+import (
+ "fmt"
+ "net/http"
+ "strings"
+ "testing"
+
+ "github.com/rackspace/gophercloud/testhelper"
+)
+
+// MarkerPager sample and test cases.
+
+type MarkerPageResult struct {
+ MarkerPageBase
+}
+
+func (r MarkerPageResult) IsEmpty() (bool, error) {
+ results, err := ExtractMarkerStrings(r)
+ if err != nil {
+ return true, err
+ }
+ return len(results) == 0, err
+}
+
+func (r MarkerPageResult) LastMarker() (string, error) {
+ results, err := ExtractMarkerStrings(r)
+ if err != nil {
+ return "", err
+ }
+ if len(results) == 0 {
+ return "", nil
+ }
+ return results[len(results)-1], nil
+}
+
+func createMarkerPaged(t *testing.T) Pager {
+ testhelper.SetupHTTP()
+
+ testhelper.Mux.HandleFunc("/page", func(w http.ResponseWriter, r *http.Request) {
+ r.ParseForm()
+ ms := r.Form["marker"]
+ switch {
+ case len(ms) == 0:
+ fmt.Fprintf(w, "aaa\nbbb\nccc")
+ case len(ms) == 1 && ms[0] == "ccc":
+ fmt.Fprintf(w, "ddd\neee\nfff")
+ case len(ms) == 1 && ms[0] == "fff":
+ fmt.Fprintf(w, "ggg\nhhh\niii")
+ case len(ms) == 1 && ms[0] == "iii":
+ w.WriteHeader(http.StatusNoContent)
+ default:
+ t.Errorf("Request with unexpected marker: [%v]", ms)
+ }
+ })
+
+ client := createClient()
+
+ createPage := func(r LastHTTPResponse) Page {
+ p := MarkerPageResult{MarkerPageBase{LastHTTPResponse: r}}
+ p.MarkerPageBase.Owner = p
+ return p
+ }
+
+ return NewPager(client, testhelper.Server.URL+"/page", createPage)
+}
+
+func ExtractMarkerStrings(page Page) ([]string, error) {
+ content := page.(MarkerPageResult).Body.([]uint8)
+ parts := strings.Split(string(content), "\n")
+ results := make([]string, 0, len(parts))
+ for _, part := range parts {
+ if len(part) > 0 {
+ results = append(results, part)
+ }
+ }
+ return results, nil
+}
+
+func TestEnumerateMarker(t *testing.T) {
+ pager := createMarkerPaged(t)
+ defer testhelper.TeardownHTTP()
+
+ callCount := 0
+ err := pager.EachPage(func(page Page) (bool, error) {
+ actual, err := ExtractMarkerStrings(page)
+ if err != nil {
+ return false, err
+ }
+
+ t.Logf("Handler invoked with %v", actual)
+
+ var expected []string
+ switch callCount {
+ case 0:
+ expected = []string{"aaa", "bbb", "ccc"}
+ case 1:
+ expected = []string{"ddd", "eee", "fff"}
+ case 2:
+ expected = []string{"ggg", "hhh", "iii"}
+ default:
+ t.Fatalf("Unexpected call count: %d", callCount)
+ return false, nil
+ }
+
+ testhelper.CheckDeepEquals(t, expected, actual)
+
+ callCount++
+ return true, nil
+ })
+ testhelper.AssertNoErr(t, err)
+ testhelper.AssertEquals(t, callCount, 3)
+}
diff --git a/_site/pagination/null.go b/_site/pagination/null.go
new file mode 100644
index 0000000..ae57e18
--- /dev/null
+++ b/_site/pagination/null.go
@@ -0,0 +1,20 @@
+package pagination
+
+// nullPage is an always-empty page that trivially satisfies all Page interfacts.
+// It's useful to be returned along with an error.
+type nullPage struct{}
+
+// NextPageURL always returns "" to indicate that there are no more pages to return.
+func (p nullPage) NextPageURL() (string, error) {
+ return "", nil
+}
+
+// IsEmpty always returns true to prevent iteration over nullPages.
+func (p nullPage) IsEmpty() (bool, error) {
+ return true, nil
+}
+
+// LastMark always returns "" because the nullPage contains no items to have a mark.
+func (p nullPage) LastMark() (string, error) {
+ return "", nil
+}
diff --git a/_site/pagination/pager.go b/_site/pagination/pager.go
new file mode 100644
index 0000000..75fe408
--- /dev/null
+++ b/_site/pagination/pager.go
@@ -0,0 +1,115 @@
+package pagination
+
+import (
+ "errors"
+
+ "github.com/rackspace/gophercloud"
+)
+
+var (
+ // ErrPageNotAvailable is returned from a Pager when a next or previous page is requested, but does not exist.
+ ErrPageNotAvailable = errors.New("The requested page does not exist.")
+)
+
+// Page must be satisfied by the result type of any resource collection.
+// It allows clients to interact with the resource uniformly, regardless of whether or not or how it's paginated.
+// Generally, rather than implementing this interface directly, implementors should embed one of the concrete PageBase structs,
+// instead.
+// Depending on the pagination strategy of a particular resource, there may be an additional subinterface that the result type
+// will need to implement.
+type Page interface {
+
+ // NextPageURL generates the URL for the page of data that follows this collection.
+ // Return "" if no such page exists.
+ NextPageURL() (string, error)
+
+ // IsEmpty returns true if this Page has no items in it.
+ IsEmpty() (bool, error)
+}
+
+// Pager knows how to advance through a specific resource collection, one page at a time.
+type Pager struct {
+ client *gophercloud.ServiceClient
+
+ initialURL string
+
+ createPage func(r LastHTTPResponse) Page
+
+ Err error
+
+ // Headers supplies additional HTTP headers to populate on each paged request.
+ Headers map[string]string
+}
+
+// NewPager constructs a manually-configured pager.
+// Supply the URL for the first page, a function that requests a specific page given a URL, and a function that counts a page.
+func NewPager(client *gophercloud.ServiceClient, initialURL string, createPage func(r LastHTTPResponse) Page) Pager {
+ return Pager{
+ client: client,
+ initialURL: initialURL,
+ createPage: createPage,
+ }
+}
+
+// WithPageCreator returns a new Pager that substitutes a different page creation function. This is
+// useful for overriding List functions in delegation.
+func (p Pager) WithPageCreator(createPage func(r LastHTTPResponse) Page) Pager {
+ return Pager{
+ client: p.client,
+ initialURL: p.initialURL,
+ createPage: createPage,
+ }
+}
+
+func (p Pager) fetchNextPage(url string) (Page, error) {
+ resp, err := Request(p.client, p.Headers, url)
+ if err != nil {
+ return nil, err
+ }
+
+ remembered, err := RememberHTTPResponse(resp)
+ if err != nil {
+ return nil, err
+ }
+
+ return p.createPage(remembered), nil
+}
+
+// EachPage iterates over each page returned by a Pager, yielding one at a time to a handler function.
+// Return "false" from the handler to prematurely stop iterating.
+func (p Pager) EachPage(handler func(Page) (bool, error)) error {
+ if p.Err != nil {
+ return p.Err
+ }
+ currentURL := p.initialURL
+ for {
+ currentPage, err := p.fetchNextPage(currentURL)
+ if err != nil {
+ return err
+ }
+
+ empty, err := currentPage.IsEmpty()
+ if err != nil {
+ return err
+ }
+ if empty {
+ return nil
+ }
+
+ ok, err := handler(currentPage)
+ if err != nil {
+ return err
+ }
+ if !ok {
+ return nil
+ }
+
+ currentURL, err = currentPage.NextPageURL()
+ if err != nil {
+ return err
+ }
+ if currentURL == "" {
+ return nil
+ }
+ }
+}
diff --git a/_site/pagination/pagination_test.go b/_site/pagination/pagination_test.go
new file mode 100644
index 0000000..779bd79
--- /dev/null
+++ b/_site/pagination/pagination_test.go
@@ -0,0 +1,13 @@
+package pagination
+
+import (
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/testhelper"
+)
+
+func createClient() *gophercloud.ServiceClient {
+ return &gophercloud.ServiceClient{
+ Provider: &gophercloud.ProviderClient{TokenID: "abc123"},
+ Endpoint: testhelper.Endpoint(),
+ }
+}
diff --git a/_site/pagination/pkg.go b/_site/pagination/pkg.go
new file mode 100644
index 0000000..912daea
--- /dev/null
+++ b/_site/pagination/pkg.go
@@ -0,0 +1,4 @@
+/*
+Package pagination contains utilities and convenience structs that implement common pagination idioms within OpenStack APIs.
+*/
+package pagination
diff --git a/_site/pagination/single.go b/_site/pagination/single.go
new file mode 100644
index 0000000..a7f6fde
--- /dev/null
+++ b/_site/pagination/single.go
@@ -0,0 +1,9 @@
+package pagination
+
+// SinglePageBase may be embedded in a Page that contains all of the results from an operation at once.
+type SinglePageBase LastHTTPResponse
+
+// NextPageURL always returns "" to indicate that there are no more pages to return.
+func (current SinglePageBase) NextPageURL() (string, error) {
+ return "", nil
+}
diff --git a/_site/pagination/single_test.go b/_site/pagination/single_test.go
new file mode 100644
index 0000000..31003e5
--- /dev/null
+++ b/_site/pagination/single_test.go
@@ -0,0 +1,71 @@
+package pagination
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ "github.com/mitchellh/mapstructure"
+ "github.com/rackspace/gophercloud/testhelper"
+)
+
+// SinglePage sample and test cases.
+
+type SinglePageResult struct {
+ SinglePageBase
+}
+
+func (r SinglePageResult) IsEmpty() (bool, error) {
+ is, err := ExtractSingleInts(r)
+ if err != nil {
+ return true, err
+ }
+ return len(is) == 0, nil
+}
+
+func ExtractSingleInts(page Page) ([]int, error) {
+ var response struct {
+ Ints []int `mapstructure:"ints"`
+ }
+
+ err := mapstructure.Decode(page.(SinglePageResult).Body, &response)
+ if err != nil {
+ return nil, err
+ }
+
+ return response.Ints, nil
+}
+
+func setupSinglePaged() Pager {
+ testhelper.SetupHTTP()
+ client := createClient()
+
+ testhelper.Mux.HandleFunc("/only", func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Add("Content-Type", "application/json")
+ fmt.Fprintf(w, `{ "ints": [1, 2, 3] }`)
+ })
+
+ createPage := func(r LastHTTPResponse) Page {
+ return SinglePageResult{SinglePageBase(r)}
+ }
+
+ return NewPager(client, testhelper.Server.URL+"/only", createPage)
+}
+
+func TestEnumerateSinglePaged(t *testing.T) {
+ callCount := 0
+ pager := setupSinglePaged()
+ defer testhelper.TeardownHTTP()
+
+ err := pager.EachPage(func(page Page) (bool, error) {
+ callCount++
+
+ expected := []int{1, 2, 3}
+ actual, err := ExtractSingleInts(page)
+ testhelper.AssertNoErr(t, err)
+ testhelper.CheckDeepEquals(t, expected, actual)
+ return true, nil
+ })
+ testhelper.CheckNoErr(t, err)
+ testhelper.CheckEquals(t, 1, callCount)
+}
diff --git a/_site/params.go b/_site/params.go
new file mode 100644
index 0000000..26c48c0
--- /dev/null
+++ b/_site/params.go
@@ -0,0 +1,182 @@
+package gophercloud
+
+import (
+ "fmt"
+ "net/url"
+ "reflect"
+ "strconv"
+ "strings"
+ "time"
+)
+
+// MaybeString takes a string that might be a zero-value, and either returns a
+// pointer to its address or a nil value (i.e. empty pointer). This is useful
+// for converting zero values in options structs when the end-user hasn't
+// defined values. Those zero values need to be nil in order for the JSON
+// serialization to ignore them.
+func MaybeString(original string) *string {
+ if original != "" {
+ return &original
+ }
+ return nil
+}
+
+// MaybeInt takes an int that might be a zero-value, and either returns a
+// pointer to its address or a nil value (i.e. empty pointer).
+func MaybeInt(original int) *int {
+ if original != 0 {
+ return &original
+ }
+ return nil
+}
+
+var t time.Time
+
+func isZero(v reflect.Value) bool {
+ switch v.Kind() {
+ case reflect.Func, reflect.Map, reflect.Slice:
+ return v.IsNil()
+ case reflect.Array:
+ z := true
+ for i := 0; i < v.Len(); i++ {
+ z = z && isZero(v.Index(i))
+ }
+ return z
+ case reflect.Struct:
+ if v.Type() == reflect.TypeOf(t) {
+ if v.Interface().(time.Time).IsZero() {
+ return true
+ }
+ return false
+ }
+ z := true
+ for i := 0; i < v.NumField(); i++ {
+ z = z && isZero(v.Field(i))
+ }
+ return z
+ }
+ // Compare other types directly:
+ z := reflect.Zero(v.Type())
+ return v.Interface() == z.Interface()
+}
+
+// BuildQueryString accepts a generic structure and parses it URL struct. It
+// converts field names into query names based on tags. So for example, this
+// type:
+//
+// struct {
+// Bar string `q:"x_bar"`
+// Baz int `q:"lorem_ipsum"`
+// }{
+// Bar: "XXX",
+// Baz: "YYY",
+// }
+//
+// will be converted into ?x_bar=XXX&lorem_ipsum=YYYY
+func BuildQueryString(opts interface{}) (*url.URL, error) {
+ optsValue := reflect.ValueOf(opts)
+ if optsValue.Kind() == reflect.Ptr {
+ optsValue = optsValue.Elem()
+ }
+
+ optsType := reflect.TypeOf(opts)
+ if optsType.Kind() == reflect.Ptr {
+ optsType = optsType.Elem()
+ }
+
+ var optsSlice []string
+ if optsValue.Kind() == reflect.Struct {
+ for i := 0; i < optsValue.NumField(); i++ {
+ v := optsValue.Field(i)
+ f := optsType.Field(i)
+ qTag := f.Tag.Get("q")
+
+ // if the field has a 'q' tag, it goes in the query string
+ if qTag != "" {
+ tags := strings.Split(qTag, ",")
+
+ // if the field is set, add it to the slice of query pieces
+ if !isZero(v) {
+ switch v.Kind() {
+ case reflect.String:
+ optsSlice = append(optsSlice, tags[0]+"="+v.String())
+ case reflect.Int:
+ optsSlice = append(optsSlice, tags[0]+"="+strconv.FormatInt(v.Int(), 10))
+ case reflect.Bool:
+ optsSlice = append(optsSlice, tags[0]+"="+strconv.FormatBool(v.Bool()))
+ }
+ } else {
+ // Otherwise, the field is not set.
+ if len(tags) == 2 && tags[1] == "required" {
+ // And the field is required. Return an error.
+ return nil, fmt.Errorf("Required query parameter [%s] not set.", f.Name)
+ }
+ }
+ }
+
+ }
+ // URL encode the string for safety.
+ s := strings.Join(optsSlice, "&")
+ if s != "" {
+ s = "?" + s
+ }
+ u, err := url.Parse(s)
+ if err != nil {
+ return nil, err
+ }
+ return u, nil
+ }
+ // Return an error if the underlying type of 'opts' isn't a struct.
+ return nil, fmt.Errorf("Options type is not a struct.")
+}
+
+// BuildHeaders accepts a generic structure and parses it string map. It
+// converts field names into header names based on "h" tags, and field values
+// into header values by a simple one-to-one mapping.
+func BuildHeaders(opts interface{}) (map[string]string, error) {
+ optsValue := reflect.ValueOf(opts)
+ if optsValue.Kind() == reflect.Ptr {
+ optsValue = optsValue.Elem()
+ }
+
+ optsType := reflect.TypeOf(opts)
+ if optsType.Kind() == reflect.Ptr {
+ optsType = optsType.Elem()
+ }
+
+ optsMap := make(map[string]string)
+ if optsValue.Kind() == reflect.Struct {
+ for i := 0; i < optsValue.NumField(); i++ {
+ v := optsValue.Field(i)
+ f := optsType.Field(i)
+ hTag := f.Tag.Get("h")
+
+ // if the field has a 'h' tag, it goes in the header
+ if hTag != "" {
+ tags := strings.Split(hTag, ",")
+
+ // if the field is set, add it to the slice of query pieces
+ if !isZero(v) {
+ switch v.Kind() {
+ case reflect.String:
+ optsMap[tags[0]] = v.String()
+ case reflect.Int:
+ optsMap[tags[0]] = strconv.FormatInt(v.Int(), 10)
+ case reflect.Bool:
+ optsMap[tags[0]] = strconv.FormatBool(v.Bool())
+ }
+ } else {
+ // Otherwise, the field is not set.
+ if len(tags) == 2 && tags[1] == "required" {
+ // And the field is required. Return an error.
+ return optsMap, fmt.Errorf("Required header not set.")
+ }
+ }
+ }
+
+ }
+ return optsMap, nil
+ }
+ // Return an error if the underlying type of 'opts' isn't a struct.
+ return optsMap, fmt.Errorf("Options type is not a struct.")
+}
diff --git a/_site/params_test.go b/_site/params_test.go
new file mode 100644
index 0000000..9f1d3bd
--- /dev/null
+++ b/_site/params_test.go
@@ -0,0 +1,142 @@
+package gophercloud
+
+import (
+ "net/url"
+ "reflect"
+ "testing"
+ "time"
+
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestMaybeString(t *testing.T) {
+ testString := ""
+ var expected *string
+ actual := MaybeString(testString)
+ th.CheckDeepEquals(t, expected, actual)
+
+ testString = "carol"
+ expected = &testString
+ actual = MaybeString(testString)
+ th.CheckDeepEquals(t, expected, actual)
+}
+
+func TestMaybeInt(t *testing.T) {
+ testInt := 0
+ var expected *int
+ actual := MaybeInt(testInt)
+ th.CheckDeepEquals(t, expected, actual)
+
+ testInt = 4
+ expected = &testInt
+ actual = MaybeInt(testInt)
+ th.CheckDeepEquals(t, expected, actual)
+}
+
+func TestBuildQueryString(t *testing.T) {
+ opts := struct {
+ J int `q:"j"`
+ R string `q:"r,required"`
+ C bool `q:"c"`
+ }{
+ J: 2,
+ R: "red",
+ C: true,
+ }
+ expected := &url.URL{RawQuery: "j=2&r=red&c=true"}
+ actual, err := BuildQueryString(&opts)
+ if err != nil {
+ t.Errorf("Error building query string: %v", err)
+ }
+ th.CheckDeepEquals(t, expected, actual)
+
+ opts = struct {
+ J int `q:"j"`
+ R string `q:"r,required"`
+ C bool `q:"c"`
+ }{
+ J: 2,
+ C: true,
+ }
+ _, err = BuildQueryString(&opts)
+ if err == nil {
+ t.Errorf("Expected error: 'Required field not set'")
+ }
+ th.CheckDeepEquals(t, expected, actual)
+
+ _, err = BuildQueryString(map[string]interface{}{"Number": 4})
+ if err == nil {
+ t.Errorf("Expected error: 'Options type is not a struct'")
+ }
+}
+
+func TestBuildHeaders(t *testing.T) {
+ testStruct := struct {
+ Accept string `h:"Accept"`
+ Num int `h:"Number,required"`
+ Style bool `h:"Style"`
+ }{
+ Accept: "application/json",
+ Num: 4,
+ Style: true,
+ }
+ expected := map[string]string{"Accept": "application/json", "Number": "4", "Style": "true"}
+ actual, err := BuildHeaders(&testStruct)
+ th.CheckNoErr(t, err)
+ th.CheckDeepEquals(t, expected, actual)
+
+ testStruct.Num = 0
+ _, err = BuildHeaders(&testStruct)
+ if err == nil {
+ t.Errorf("Expected error: 'Required header not set'")
+ }
+
+ _, err = BuildHeaders(map[string]interface{}{"Number": 4})
+ if err == nil {
+ t.Errorf("Expected error: 'Options type is not a struct'")
+ }
+}
+
+func TestIsZero(t *testing.T) {
+ var testMap map[string]interface{}
+ testMapValue := reflect.ValueOf(testMap)
+ expected := true
+ actual := isZero(testMapValue)
+ th.CheckEquals(t, expected, actual)
+ testMap = map[string]interface{}{"empty": false}
+ testMapValue = reflect.ValueOf(testMap)
+ expected = false
+ actual = isZero(testMapValue)
+ th.CheckEquals(t, expected, actual)
+
+ var testArray [2]string
+ testArrayValue := reflect.ValueOf(testArray)
+ expected = true
+ actual = isZero(testArrayValue)
+ th.CheckEquals(t, expected, actual)
+ testArray = [2]string{"one", "two"}
+ testArrayValue = reflect.ValueOf(testArray)
+ expected = false
+ actual = isZero(testArrayValue)
+ th.CheckEquals(t, expected, actual)
+
+ var testStruct struct {
+ A string
+ B time.Time
+ }
+ testStructValue := reflect.ValueOf(testStruct)
+ expected = true
+ actual = isZero(testStructValue)
+ th.CheckEquals(t, expected, actual)
+ testStruct = struct {
+ A string
+ B time.Time
+ }{
+ B: time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC),
+ }
+ testStructValue = reflect.ValueOf(testStruct)
+ expected = false
+ actual = isZero(testStructValue)
+ th.CheckEquals(t, expected, actual)
+
+}
diff --git a/_site/provider_client.go b/_site/provider_client.go
new file mode 100644
index 0000000..2be665e
--- /dev/null
+++ b/_site/provider_client.go
@@ -0,0 +1,29 @@
+package gophercloud
+
+// ProviderClient stores details that are required to interact with any services within a specific provider's API.
+//
+// Generally, you acquire a ProviderClient by calling the `NewClient()` method in the appropriate provider's child package,
+// providing whatever authentication credentials are required.
+type ProviderClient struct {
+
+ // IdentityBase is the front door to an openstack provider.
+ // Generally this will be populated when you authenticate.
+ // It should be the *root* resource of the identity service, not of a specific identity version.
+ IdentityBase string
+
+ // IdentityEndpoint is the originally requested identity endpoint.
+ // This may be a specific version of the identity service, in which case that endpoint is used rather than querying the
+ // version-negotiation endpoint.
+ IdentityEndpoint string
+
+ // TokenID is the most recently valid token issued.
+ TokenID string
+
+ // EndpointLocator describes how this provider discovers the endpoints for its constituent services.
+ EndpointLocator EndpointLocator
+}
+
+// AuthenticatedHeaders returns a map of HTTP headers that are common for all authenticated service requests.
+func (client *ProviderClient) AuthenticatedHeaders() map[string]string {
+ return map[string]string{"X-Auth-Token": client.TokenID}
+}
diff --git a/_site/provider_client_test.go b/_site/provider_client_test.go
new file mode 100644
index 0000000..b260246
--- /dev/null
+++ b/_site/provider_client_test.go
@@ -0,0 +1,16 @@
+package gophercloud
+
+import (
+ "testing"
+
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestAuthenticatedHeaders(t *testing.T) {
+ p := &ProviderClient{
+ TokenID: "1234",
+ }
+ expected := map[string]string{"X-Auth-Token": "1234"}
+ actual := p.AuthenticatedHeaders()
+ th.CheckDeepEquals(t, expected, actual)
+}
diff --git a/_site/rackspace/client.go b/_site/rackspace/client.go
new file mode 100644
index 0000000..d8c4a19
--- /dev/null
+++ b/_site/rackspace/client.go
@@ -0,0 +1,115 @@
+package rackspace
+
+import (
+ "fmt"
+
+ "github.com/rackspace/gophercloud"
+ os "github.com/rackspace/gophercloud/openstack"
+ "github.com/rackspace/gophercloud/openstack/utils"
+ tokens2 "github.com/rackspace/gophercloud/rackspace/identity/v2/tokens"
+)
+
+const (
+ // RackspaceUSIdentity is an identity endpoint located in the United States.
+ RackspaceUSIdentity = "https://identity.api.rackspacecloud.com/v2.0/"
+
+ // RackspaceUKIdentity is an identity endpoint located in the UK.
+ RackspaceUKIdentity = "https://lon.identity.api.rackspacecloud.com/v2.0/"
+)
+
+const (
+ v20 = "v2.0"
+)
+
+// NewClient creates a client that's prepared to communicate with the Rackspace API, but is not
+// yet authenticated. Most users will probably prefer using the AuthenticatedClient function
+// instead.
+//
+// Provide the base URL of the identity endpoint you wish to authenticate against as "endpoint".
+// Often, this will be either RackspaceUSIdentity or RackspaceUKIdentity.
+func NewClient(endpoint string) (*gophercloud.ProviderClient, error) {
+ if endpoint == "" {
+ return os.NewClient(RackspaceUSIdentity)
+ }
+ return os.NewClient(endpoint)
+}
+
+// AuthenticatedClient logs in to Rackspace with the provided credentials and constructs a
+// ProviderClient that's ready to operate.
+//
+// If the provided AuthOptions does not specify an explicit IdentityEndpoint, it will default to
+// the canonical, production Rackspace US identity endpoint.
+func AuthenticatedClient(options gophercloud.AuthOptions) (*gophercloud.ProviderClient, error) {
+ client, err := NewClient(options.IdentityEndpoint)
+ if err != nil {
+ return nil, err
+ }
+
+ err = Authenticate(client, options)
+ if err != nil {
+ return nil, err
+ }
+ return client, nil
+}
+
+// Authenticate or re-authenticate against the most recent identity service supported at the
+// provided endpoint.
+func Authenticate(client *gophercloud.ProviderClient, options gophercloud.AuthOptions) error {
+ versions := []*utils.Version{
+ &utils.Version{ID: v20, Priority: 20, Suffix: "/v2.0/"},
+ }
+
+ chosen, endpoint, err := utils.ChooseVersion(client.IdentityBase, client.IdentityEndpoint, versions)
+ if err != nil {
+ return err
+ }
+
+ switch chosen.ID {
+ case v20:
+ return v2auth(client, endpoint, options)
+ default:
+ // The switch statement must be out of date from the versions list.
+ return fmt.Errorf("Unrecognized identity version: %s", chosen.ID)
+ }
+}
+
+// AuthenticateV2 explicitly authenticates with v2 of the identity service.
+func AuthenticateV2(client *gophercloud.ProviderClient, options gophercloud.AuthOptions) error {
+ return v2auth(client, "", options)
+}
+
+func v2auth(client *gophercloud.ProviderClient, endpoint string, options gophercloud.AuthOptions) error {
+ v2Client := NewIdentityV2(client)
+ if endpoint != "" {
+ v2Client.Endpoint = endpoint
+ }
+
+ result := tokens2.Create(v2Client, tokens2.WrapOptions(options))
+
+ token, err := result.ExtractToken()
+ if err != nil {
+ return err
+ }
+
+ catalog, err := result.ExtractServiceCatalog()
+ if err != nil {
+ return err
+ }
+
+ client.TokenID = token.ID
+ client.EndpointLocator = func(opts gophercloud.EndpointOpts) (string, error) {
+ return os.V2EndpointURL(catalog, opts)
+ }
+
+ return nil
+}
+
+// NewIdentityV2 creates a ServiceClient that may be used to access the v2 identity service.
+func NewIdentityV2(client *gophercloud.ProviderClient) *gophercloud.ServiceClient {
+ v2Endpoint := client.IdentityBase + "v2.0/"
+
+ return &gophercloud.ServiceClient{
+ Provider: client,
+ Endpoint: v2Endpoint,
+ }
+}
diff --git a/_site/rackspace/client_test.go b/_site/rackspace/client_test.go
new file mode 100644
index 0000000..73b1c88
--- /dev/null
+++ b/_site/rackspace/client_test.go
@@ -0,0 +1,38 @@
+package rackspace
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestAuthenticatedClientV2(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/tokens", func(w http.ResponseWriter, r *http.Request) {
+ fmt.Fprintf(w, `
+ {
+ "access": {
+ "token": {
+ "id": "01234567890",
+ "expires": "2014-10-01T10:00:00.000000Z"
+ },
+ "serviceCatalog": []
+ }
+ }
+ `)
+ })
+
+ options := gophercloud.AuthOptions{
+ Username: "me",
+ APIKey: "09876543210",
+ IdentityEndpoint: th.Endpoint() + "v2.0/",
+ }
+ client, err := AuthenticatedClient(options)
+ th.AssertNoErr(t, err)
+ th.CheckEquals(t, "01234567890", client.TokenID)
+}
diff --git a/_site/rackspace/identity/v2/extensions/delegate.go b/_site/rackspace/identity/v2/extensions/delegate.go
new file mode 100644
index 0000000..fc547cd
--- /dev/null
+++ b/_site/rackspace/identity/v2/extensions/delegate.go
@@ -0,0 +1,24 @@
+package extensions
+
+import (
+ "github.com/rackspace/gophercloud"
+ common "github.com/rackspace/gophercloud/openstack/common/extensions"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// ExtractExtensions accepts a Page struct, specifically an ExtensionPage struct, and extracts the
+// elements into a slice of os.Extension structs.
+func ExtractExtensions(page pagination.Page) ([]common.Extension, error) {
+ return common.ExtractExtensions(page)
+}
+
+// Get retrieves information for a specific extension using its alias.
+func Get(c *gophercloud.ServiceClient, alias string) common.GetResult {
+ return common.Get(c, alias)
+}
+
+// List returns a Pager which allows you to iterate over the full collection of extensions.
+// It does not accept query parameters.
+func List(c *gophercloud.ServiceClient) pagination.Pager {
+ return common.List(c)
+}
diff --git a/_site/rackspace/identity/v2/extensions/delegate_test.go b/_site/rackspace/identity/v2/extensions/delegate_test.go
new file mode 100644
index 0000000..e30f794
--- /dev/null
+++ b/_site/rackspace/identity/v2/extensions/delegate_test.go
@@ -0,0 +1,39 @@
+package extensions
+
+import (
+ "testing"
+
+ common "github.com/rackspace/gophercloud/openstack/common/extensions"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestList(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ common.HandleListExtensionsSuccessfully(t)
+
+ count := 0
+
+ err := List(fake.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractExtensions(page)
+ th.AssertNoErr(t, err)
+ th.AssertDeepEquals(t, common.ExpectedExtensions, actual)
+
+ return true, nil
+ })
+ th.AssertNoErr(t, err)
+ th.CheckEquals(t, 1, count)
+}
+
+func TestGet(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ common.HandleGetExtensionSuccessfully(t)
+
+ actual, err := Get(fake.ServiceClient(), "agent").Extract()
+ th.AssertNoErr(t, err)
+ th.CheckDeepEquals(t, common.SingleExtension, actual)
+}
diff --git a/_site/rackspace/identity/v2/tenants/delegate.go b/_site/rackspace/identity/v2/tenants/delegate.go
new file mode 100644
index 0000000..6cdd0cf
--- /dev/null
+++ b/_site/rackspace/identity/v2/tenants/delegate.go
@@ -0,0 +1,17 @@
+package tenants
+
+import (
+ "github.com/rackspace/gophercloud"
+ os "github.com/rackspace/gophercloud/openstack/identity/v2/tenants"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// ExtractTenants interprets a page of List results as a more usable slice of Tenant structs.
+func ExtractTenants(page pagination.Page) ([]os.Tenant, error) {
+ return os.ExtractTenants(page)
+}
+
+// List enumerates the tenants to which the current token grants access.
+func List(client *gophercloud.ServiceClient, opts *os.ListOpts) pagination.Pager {
+ return os.List(client, opts)
+}
diff --git a/_site/rackspace/identity/v2/tenants/delegate_test.go b/_site/rackspace/identity/v2/tenants/delegate_test.go
new file mode 100644
index 0000000..eccbfe2
--- /dev/null
+++ b/_site/rackspace/identity/v2/tenants/delegate_test.go
@@ -0,0 +1,28 @@
+package tenants
+
+import (
+ "testing"
+
+ os "github.com/rackspace/gophercloud/openstack/identity/v2/tenants"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestListTenants(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ os.HandleListTenantsSuccessfully(t)
+
+ count := 0
+ err := List(fake.ServiceClient(), nil).EachPage(func(page pagination.Page) (bool, error) {
+ actual, err := ExtractTenants(page)
+ th.AssertNoErr(t, err)
+ th.CheckDeepEquals(t, os.ExpectedTenantSlice, actual)
+
+ count++
+ return true, nil
+ })
+ th.AssertNoErr(t, err)
+ th.CheckEquals(t, 1, count)
+}
diff --git a/_site/rackspace/identity/v2/tokens/delegate.go b/_site/rackspace/identity/v2/tokens/delegate.go
new file mode 100644
index 0000000..4f9885a
--- /dev/null
+++ b/_site/rackspace/identity/v2/tokens/delegate.go
@@ -0,0 +1,60 @@
+package tokens
+
+import (
+ "errors"
+
+ "github.com/rackspace/gophercloud"
+ os "github.com/rackspace/gophercloud/openstack/identity/v2/tokens"
+)
+
+var (
+ // ErrPasswordProvided is returned if both a password and an API key are provided to Create.
+ ErrPasswordProvided = errors.New("Please provide either a password or an API key.")
+)
+
+// AuthOptions wraps the OpenStack AuthOptions struct to be able to customize the request body
+// when API key authentication is used.
+type AuthOptions struct {
+ os.AuthOptions
+}
+
+// WrapOptions embeds a root AuthOptions struct in a package-specific one.
+func WrapOptions(original gophercloud.AuthOptions) AuthOptions {
+ return AuthOptions{AuthOptions: os.WrapOptions(original)}
+}
+
+// ToTokenCreateMap serializes an AuthOptions into a request body. If an API key is provided, it
+// will be used, otherwise
+func (auth AuthOptions) ToTokenCreateMap() (map[string]interface{}, error) {
+ if auth.APIKey == "" {
+ return auth.AuthOptions.ToTokenCreateMap()
+ }
+
+ // Verify that other required attributes are present.
+ if auth.Username == "" {
+ return nil, os.ErrUsernameRequired
+ }
+
+ authMap := make(map[string]interface{})
+
+ authMap["RAX-KSKEY:apiKeyCredentials"] = map[string]interface{}{
+ "username": auth.Username,
+ "apiKey": auth.APIKey,
+ }
+
+ if auth.TenantID != "" {
+ authMap["tenantId"] = auth.TenantID
+ }
+ if auth.TenantName != "" {
+ authMap["tenantName"] = auth.TenantName
+ }
+
+ return map[string]interface{}{"auth": authMap}, nil
+}
+
+// Create authenticates to Rackspace's identity service and attempts to acquire a Token. Rather
+// than interact with this service directly, users should generally call
+// rackspace.AuthenticatedClient().
+func Create(client *gophercloud.ServiceClient, auth AuthOptions) os.CreateResult {
+ return os.Create(client, auth)
+}
diff --git a/_site/rackspace/identity/v2/tokens/delegate_test.go b/_site/rackspace/identity/v2/tokens/delegate_test.go
new file mode 100644
index 0000000..6678ff4
--- /dev/null
+++ b/_site/rackspace/identity/v2/tokens/delegate_test.go
@@ -0,0 +1,36 @@
+package tokens
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ os "github.com/rackspace/gophercloud/openstack/identity/v2/tokens"
+ th "github.com/rackspace/gophercloud/testhelper"
+ "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func tokenPost(t *testing.T, options gophercloud.AuthOptions, requestJSON string) os.CreateResult {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ os.HandleTokenPost(t, requestJSON)
+
+ return Create(client.ServiceClient(), WrapOptions(options))
+}
+
+func TestCreateTokenWithAPIKey(t *testing.T) {
+ options := gophercloud.AuthOptions{
+ Username: "me",
+ APIKey: "1234567890abcdef",
+ }
+
+ os.IsSuccessful(t, tokenPost(t, options, `
+ {
+ "auth": {
+ "RAX-KSKEY:apiKeyCredentials": {
+ "username": "me",
+ "apiKey": "1234567890abcdef"
+ }
+ }
+ }
+ `))
+}
diff --git a/_site/results.go b/_site/results.go
new file mode 100644
index 0000000..647ba46
--- /dev/null
+++ b/_site/results.go
@@ -0,0 +1,37 @@
+package gophercloud
+
+// CommonResult acts as a base struct that other results can embed. It contains
+// the deserialized JSON structure returned from the server (Resp), and any
+// errors that might have occurred during transport or deserialization.
+type CommonResult struct {
+ Resp map[string]interface{}
+ Err error
+}
+
+// RFC3339Milli describes a time format used by API responses.
+const RFC3339Milli = "2006-01-02T15:04:05.999999Z"
+
+// Link represents a structure that enables paginated collections how to
+// traverse backward or forward. The "Rel" field is usually either "next".
+type Link struct {
+ Href string `mapstructure:"href"`
+ Rel string `mapstructure:"rel"`
+}
+
+// ExtractNextURL attempts to extract the next URL from a JSON structure. It
+// follows the common structure of nesting back and next links.
+func ExtractNextURL(links []Link) (string, error) {
+ var url string
+
+ for _, l := range links {
+ if l.Rel == "next" {
+ url = l.Href
+ }
+ }
+
+ if url == "" {
+ return "", nil
+ }
+
+ return url, nil
+}
diff --git a/_site/script/acceptancetest b/_site/script/acceptancetest
new file mode 100755
index 0000000..d8039ae
--- /dev/null
+++ b/_site/script/acceptancetest
@@ -0,0 +1,5 @@
+#!/bin/bash
+#
+# Run the acceptance tests.
+
+exec go test -tags 'acceptance fixtures' ./acceptance/... $@
diff --git a/_site/script/bootstrap b/_site/script/bootstrap
new file mode 100755
index 0000000..6bae6e8
--- /dev/null
+++ b/_site/script/bootstrap
@@ -0,0 +1,26 @@
+#!/bin/bash
+#
+# This script helps new contributors set up their local workstation for
+# gophercloud development and contributions.
+
+# Create the environment
+export GOPATH=$HOME/go/gophercloud
+mkdir -p $GOPATH
+
+# Download gophercloud into that environment
+go get github.com/rackspace/gophercloud
+cd $GOPATH/src/github.com/rackspace/gophercloud
+git checkout master
+
+# Write out the env.sh convenience file.
+cd $GOPATH
+cat <<EOF >env.sh
+#!/bin/bash
+export GOPATH=$(pwd)
+export GOPHERCLOUD=$GOPATH/src/github.com/rackspace/gophercloud
+EOF
+chmod a+x env.sh
+
+# Make changes immediately available as a convenience.
+. ./env.sh
+
diff --git a/_site/script/cibuild b/_site/script/cibuild
new file mode 100755
index 0000000..1cb389e
--- /dev/null
+++ b/_site/script/cibuild
@@ -0,0 +1,5 @@
+#!/bin/bash
+#
+# Test script to be invoked by Travis.
+
+exec script/unittest -v
diff --git a/_site/script/test b/_site/script/test
new file mode 100755
index 0000000..1e03dff
--- /dev/null
+++ b/_site/script/test
@@ -0,0 +1,5 @@
+#!/bin/bash
+#
+# Run all the tests.
+
+exec go test -tags 'acceptance fixtures' ./... $@
diff --git a/_site/script/unittest b/_site/script/unittest
new file mode 100755
index 0000000..d3440a9
--- /dev/null
+++ b/_site/script/unittest
@@ -0,0 +1,5 @@
+#!/bin/bash
+#
+# Run the unit tests.
+
+exec go test -tags fixtures ./... $@
diff --git a/_site/service_client.go b/_site/service_client.go
new file mode 100644
index 0000000..55d3b98
--- /dev/null
+++ b/_site/service_client.go
@@ -0,0 +1,38 @@
+package gophercloud
+
+import "strings"
+
+// ServiceClient stores details required to interact with a specific service API implemented by a provider.
+// Generally, you'll acquire these by calling the appropriate `New` method on a ProviderClient.
+type ServiceClient struct {
+ // Provider is a reference to the provider that implements this service.
+ Provider *ProviderClient
+
+ // Endpoint is the base URL of the service's API, acquired from a service catalog.
+ // It MUST end with a /.
+ Endpoint string
+
+ // ResourceBase is the base URL shared by the resources within a service's API. It should include
+ // the API version and, like Endpoint, MUST end with a / if set. If not set, the Endpoint is used
+ // as-is, instead.
+ ResourceBase string
+}
+
+// ResourceBaseURL returns the base URL of any resources used by this service. It MUST end with a /.
+func (client *ServiceClient) ResourceBaseURL() string {
+ if client.ResourceBase != "" {
+ return client.ResourceBase
+ }
+ return client.Endpoint
+}
+
+// ServiceURL constructs a URL for a resource belonging to this provider.
+func (client *ServiceClient) ServiceURL(parts ...string) string {
+ return client.ResourceBaseURL() + strings.Join(parts, "/")
+}
+
+// AuthenticatedHeaders returns a collection of HTTP request headers that mark a request as
+// belonging to the currently authenticated user.
+func (client *ServiceClient) AuthenticatedHeaders() map[string]string {
+ return client.Provider.AuthenticatedHeaders()
+}
diff --git a/_site/service_client_test.go b/_site/service_client_test.go
new file mode 100644
index 0000000..84beb3f
--- /dev/null
+++ b/_site/service_client_test.go
@@ -0,0 +1,14 @@
+package gophercloud
+
+import (
+ "testing"
+
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestServiceURL(t *testing.T) {
+ c := &ServiceClient{Endpoint: "http://123.45.67.8/"}
+ expected := "http://123.45.67.8/more/parts/here"
+ actual := c.ServiceURL("more", "parts", "here")
+ th.CheckEquals(t, expected, actual)
+}
diff --git a/_site/testhelper/client/fake.go b/_site/testhelper/client/fake.go
new file mode 100644
index 0000000..3eb9e12
--- /dev/null
+++ b/_site/testhelper/client/fake.go
@@ -0,0 +1,17 @@
+package client
+
+import (
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/testhelper"
+)
+
+// Fake token to use.
+const TokenID = "cbc36478b0bd8e67e89469c7749d4127"
+
+// ServiceClient returns a generic service client for use in tests.
+func ServiceClient() *gophercloud.ServiceClient {
+ return &gophercloud.ServiceClient{
+ Provider: &gophercloud.ProviderClient{TokenID: TokenID},
+ Endpoint: testhelper.Endpoint(),
+ }
+}
diff --git a/_site/testhelper/convenience.go b/_site/testhelper/convenience.go
new file mode 100644
index 0000000..f6cb371
--- /dev/null
+++ b/_site/testhelper/convenience.go
@@ -0,0 +1,75 @@
+package testhelper
+
+import (
+ "fmt"
+ "path/filepath"
+ "reflect"
+ "runtime"
+ "testing"
+)
+
+func prefix() string {
+ _, file, line, _ := runtime.Caller(3)
+ return fmt.Sprintf("Failure in %s, line %d:", filepath.Base(file), line)
+}
+
+func green(str interface{}) string {
+ return fmt.Sprintf("\033[0m\033[1;32m%#v\033[0m\033[1;31m", str)
+}
+
+func yellow(str interface{}) string {
+ return fmt.Sprintf("\033[0m\033[1;33m%#v\033[0m\033[1;31m", str)
+}
+
+func logFatal(t *testing.T, str string) {
+ t.Fatalf("\033[1;31m%s %s\033[0m", prefix(), str)
+}
+
+func logError(t *testing.T, str string) {
+ t.Errorf("\033[1;31m%s %s\033[0m", prefix(), str)
+}
+
+// AssertEquals compares two arbitrary values and performs a comparison. If the
+// comparison fails, a fatal error is raised that will fail the test
+func AssertEquals(t *testing.T, expected, actual interface{}) {
+ if expected != actual {
+ logFatal(t, fmt.Sprintf("expected %s but got %s", green(expected), yellow(actual)))
+ }
+}
+
+// CheckEquals is similar to AssertEquals, except with a non-fatal error
+func CheckEquals(t *testing.T, expected, actual interface{}) {
+ if expected != actual {
+ logError(t, fmt.Sprintf("expected %s but got %s", green(expected), yellow(actual)))
+ }
+}
+
+// AssertDeepEquals - like Equals - performs a comparison - but on more complex
+// structures that requires deeper inspection
+func AssertDeepEquals(t *testing.T, expected, actual interface{}) {
+ if !reflect.DeepEqual(expected, actual) {
+ logFatal(t, fmt.Sprintf("expected %s but got %s", green(expected), yellow(actual)))
+ }
+}
+
+// CheckDeepEquals is similar to AssertDeepEquals, except with a non-fatal error
+func CheckDeepEquals(t *testing.T, expected, actual interface{}) {
+ if !reflect.DeepEqual(expected, actual) {
+ logError(t, fmt.Sprintf("expected %s but got %s", green(expected), yellow(actual)))
+ }
+}
+
+// AssertNoErr is a convenience function for checking whether an error value is
+// an actual error
+func AssertNoErr(t *testing.T, e error) {
+ if e != nil {
+ logFatal(t, fmt.Sprintf("unexpected error %s", yellow(e.Error())))
+ }
+}
+
+// CheckNoErr is similar to AssertNoErr, except with a non-fatal error
+func CheckNoErr(t *testing.T, e error) {
+ if e != nil {
+ logError(t, fmt.Sprintf("unexpected error %s", yellow(e.Error())))
+ }
+}
diff --git a/_site/testhelper/doc.go b/_site/testhelper/doc.go
new file mode 100644
index 0000000..25b4dfe
--- /dev/null
+++ b/_site/testhelper/doc.go
@@ -0,0 +1,4 @@
+/*
+Package testhelper container methods that are useful for writing unit tests.
+*/
+package testhelper
diff --git a/_site/testhelper/http_responses.go b/_site/testhelper/http_responses.go
new file mode 100644
index 0000000..481a833
--- /dev/null
+++ b/_site/testhelper/http_responses.go
@@ -0,0 +1,113 @@
+package testhelper
+
+import (
+ "encoding/json"
+ "io/ioutil"
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "reflect"
+ "testing"
+)
+
+var (
+ // Mux is a multiplexer that can be used to register handlers.
+ Mux *http.ServeMux
+
+ // Server is an in-memory HTTP server for testing.
+ Server *httptest.Server
+)
+
+// SetupHTTP prepares the Mux and Server.
+func SetupHTTP() {
+ Mux = http.NewServeMux()
+ Server = httptest.NewServer(Mux)
+}
+
+// TeardownHTTP releases HTTP-related resources.
+func TeardownHTTP() {
+ Server.Close()
+}
+
+// Endpoint returns a fake endpoint that will actually target the Mux.
+func Endpoint() string {
+ return Server.URL + "/"
+}
+
+// TestFormValues ensures that all the URL parameters given to the http.Request are the same as values.
+func TestFormValues(t *testing.T, r *http.Request, values map[string]string) {
+ want := url.Values{}
+ for k, v := range values {
+ want.Add(k, v)
+ }
+
+ r.ParseForm()
+ if !reflect.DeepEqual(want, r.Form) {
+ t.Errorf("Request parameters = %v, want %v", r.Form, want)
+ }
+}
+
+// TestMethod checks that the Request has the expected method (e.g. GET, POST).
+func TestMethod(t *testing.T, r *http.Request, expected string) {
+ if expected != r.Method {
+ t.Errorf("Request method = %v, expected %v", r.Method, expected)
+ }
+}
+
+// TestHeader checks that the header on the http.Request matches the expected value.
+func TestHeader(t *testing.T, r *http.Request, header string, expected string) {
+ if actual := r.Header.Get(header); expected != actual {
+ t.Errorf("Header %s = %s, expected %s", header, actual, expected)
+ }
+}
+
+// TestBody verifies that the request body matches an expected body.
+func TestBody(t *testing.T, r *http.Request, expected string) {
+ b, err := ioutil.ReadAll(r.Body)
+ if err != nil {
+ t.Errorf("Unable to read body: %v", err)
+ }
+ str := string(b)
+ if expected != str {
+ t.Errorf("Body = %s, expected %s", str, expected)
+ }
+}
+
+// TestJSONRequest verifies that the JSON payload of a request matches an expected structure, without asserting things about
+// whitespace or ordering.
+func TestJSONRequest(t *testing.T, r *http.Request, expected string) {
+ b, err := ioutil.ReadAll(r.Body)
+ if err != nil {
+ t.Errorf("Unable to read request body: %v", err)
+ }
+
+ var expectedJSON interface{}
+ err = json.Unmarshal([]byte(expected), &expectedJSON)
+ if err != nil {
+ t.Errorf("Unable to parse expected value as JSON: %v", err)
+ }
+
+ var actualJSON interface{}
+ err = json.Unmarshal(b, &actualJSON)
+ if err != nil {
+ t.Errorf("Unable to parse request body as JSON: %v", err)
+ }
+
+ if !reflect.DeepEqual(expectedJSON, actualJSON) {
+ prettyExpected, err := json.MarshalIndent(expectedJSON, "", " ")
+ if err != nil {
+ t.Logf("Unable to pretty-print expected JSON: %v\n%s", err, expected)
+ } else {
+ t.Logf("Expected JSON:\n%s", prettyExpected)
+ }
+
+ prettyActual, err := json.MarshalIndent(actualJSON, "", " ")
+ if err != nil {
+ t.Logf("Unable to pretty-print actual JSON: %v\n%s", err, b)
+ } else {
+ t.Logf("Actual JSON:\n%s", prettyActual)
+ }
+
+ t.Errorf("Response body did not contain the correct JSON.")
+ }
+}
diff --git a/_site/util.go b/_site/util.go
new file mode 100644
index 0000000..1715458
--- /dev/null
+++ b/_site/util.go
@@ -0,0 +1,31 @@
+package gophercloud
+
+import (
+ "fmt"
+ "strings"
+ "time"
+)
+
+// WaitFor polls a predicate function once per second up to secs times to wait for a certain state to arrive.
+func WaitFor(secs int, predicate func() (bool, error)) error {
+ for i := 0; i < secs; i++ {
+ time.Sleep(1 * time.Second)
+
+ satisfied, err := predicate()
+ if err != nil {
+ return err
+ }
+ if satisfied {
+ return nil
+ }
+ }
+ return fmt.Errorf("Time out in WaitFor.")
+}
+
+// NormalizeURL ensures that each endpoint URL has a closing `/`, as expected by ServiceClient.
+func NormalizeURL(url string) string {
+ if !strings.HasSuffix(url, "/") {
+ return url + "/"
+ }
+ return url
+}
diff --git a/_site/util_test.go b/_site/util_test.go
new file mode 100644
index 0000000..6fbd920
--- /dev/null
+++ b/_site/util_test.go
@@ -0,0 +1,21 @@
+package gophercloud
+
+import (
+ "testing"
+
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestWaitFor(t *testing.T) {
+ err := WaitFor(0, func() (bool, error) {
+ return true, nil
+ })
+ if err == nil {
+ t.Errorf("Expected error: 'Time out in WaitFor'")
+ }
+
+ err = WaitFor(5, func() (bool, error) {
+ return true, nil
+ })
+ th.CheckNoErr(t, err)
+}
diff --git a/docs/identity/v2/index.md b/docs/identity/v2/index.md
new file mode 100644
index 0000000..c0e82bd
--- /dev/null
+++ b/docs/identity/v2/index.md
@@ -0,0 +1,60 @@
+---
+layout: page
+title: Getting Started with Identity v2
+---
+
+## Tokens
+
+A token is an arbitrary bit of text that is used to access resources. Each
+token has a scope that describes which resources are accessible with it. A
+token may be revoked at anytime and is valid for a finite duration.
+
+### Generate a token
+
+The nature of required and optional auth options will depend on your provider,
+but generally the `Username` and `IdentityEndpoint` fields are always
+required. Some providers will insist on a `Password` instead of an `APIKey`,
+others will prefer `TenantID` over `TenantName` - so it is always worth
+checking before writing your implementation in Go.
+
+{% highlight go %}
+import "github.com/rackspace/gophercloud/openstack/identity/v2/tokens"
+
+opts := tokens.AuthOptions{
+ IdentityEndpoint: "{identityURL}",
+ Username: "{username}",
+ APIKey: "{apiKey}",
+}
+
+token, err := tokens.Create(client, opts).Extract()
+{% endhighlight %}
+
+## Tenants
+
+A tenant is a container used to group or isolate API resources. Depending on
+the provider, a tenant can map to a customer, account, organization, or project.
+
+### List tenants
+
+{% highlight go %}
+import (
+ "github.com/rackspace/gophercloud/pagination"
+ "github.com/rackspace/gophercloud/openstack/identity/v2/tenants"
+)
+
+// We have the option of filtering the tenant list. If we want the full
+// collection, leave it as an empty struct
+opts := tenants.ListOpts{Limit: 10}
+
+// Retrieve a pager (i.e. a paginated collection)
+pager := tenants.List(client, opts)
+
+// Define an anonymous function to be executed on each page's iteration
+err := pager.EachPage(func(page pagination.Page) (bool, error) {
+ tenantList, err := tenants.ExtractTenants(page)
+
+ for _, t := range tenantList {
+ // "t" will be a tenants.Tenant
+ }
+})
+{% endhighlight %}
diff --git a/docs/identity/v3/index.md b/docs/identity/v3/index.md
new file mode 100644
index 0000000..ab2da67
--- /dev/null
+++ b/docs/identity/v3/index.md
@@ -0,0 +1,36 @@
+---
+layout: page
+title: Getting Started with Identity v3
+---
+
+## Tokens
+
+### Generate a token
+
+### Get a token
+
+### Validate token
+
+### Revoke token
+
+## Endpoints
+
+### Create an endpoint
+
+### List endpoints
+
+### Update endpoint
+
+### Delete endpoint
+
+## Services
+
+### Create a service
+
+### List services
+
+### Get a service
+
+### Update service
+
+### Delete service
diff --git a/openstack/compute/v2/servers/requests_test.go b/openstack/compute/v2/servers/requests_test.go
index b0de950..86fe1e2 100644
--- a/openstack/compute/v2/servers/requests_test.go
+++ b/openstack/compute/v2/servers/requests_test.go
@@ -5,27 +5,18 @@
"net/http"
"testing"
- "github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
"github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
)
-const tokenID = "bzbzbzbzbz"
-
-func serviceClient() *gophercloud.ServiceClient {
- return &gophercloud.ServiceClient{
- Provider: &gophercloud.ProviderClient{TokenID: tokenID},
- Endpoint: testhelper.Endpoint(),
- }
-}
-
func TestListServers(t *testing.T) {
testhelper.SetupHTTP()
defer testhelper.TeardownHTTP()
testhelper.Mux.HandleFunc("/servers/detail", func(w http.ResponseWriter, r *http.Request) {
testhelper.TestMethod(t, r, "GET")
- testhelper.TestHeader(t, r, "X-Auth-Token", tokenID)
+ testhelper.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
w.Header().Add("Content-Type", "application/json")
r.ParseForm()
@@ -40,9 +31,8 @@
}
})
- client := serviceClient()
pages := 0
- err := List(client, ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+ err := List(fake.ServiceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
pages++
actual, err := ExtractServers(page)
@@ -72,7 +62,7 @@
testhelper.Mux.HandleFunc("/servers", func(w http.ResponseWriter, r *http.Request) {
testhelper.TestMethod(t, r, "POST")
- testhelper.TestHeader(t, r, "X-Auth-Token", tokenID)
+ testhelper.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
testhelper.TestJSONRequest(t, r, `{
"server": {
"name": "derp",
@@ -86,7 +76,7 @@
fmt.Fprintf(w, singleServerBody)
})
- client := serviceClient()
+ client := fake.ServiceClient()
actual, err := Create(client, CreateOpts{
Name: "derp",
ImageRef: "f90f6034-2570-4974-8351-6b49732ef2eb",
@@ -105,12 +95,12 @@
testhelper.Mux.HandleFunc("/servers/asdfasdfasdf", func(w http.ResponseWriter, r *http.Request) {
testhelper.TestMethod(t, r, "DELETE")
- testhelper.TestHeader(t, r, "X-Auth-Token", tokenID)
+ testhelper.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
w.WriteHeader(http.StatusNoContent)
})
- client := serviceClient()
+ client := fake.ServiceClient()
err := Delete(client, "asdfasdfasdf")
if err != nil {
t.Fatalf("Unexpected Delete error: %v", err)
@@ -123,13 +113,13 @@
testhelper.Mux.HandleFunc("/servers/1234asdf", func(w http.ResponseWriter, r *http.Request) {
testhelper.TestMethod(t, r, "GET")
- testhelper.TestHeader(t, r, "X-Auth-Token", tokenID)
+ testhelper.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
testhelper.TestHeader(t, r, "Accept", "application/json")
fmt.Fprintf(w, singleServerBody)
})
- client := serviceClient()
+ client := fake.ServiceClient()
actual, err := Get(client, "1234asdf").Extract()
if err != nil {
t.Fatalf("Unexpected Get error: %v", err)
@@ -144,7 +134,7 @@
testhelper.Mux.HandleFunc("/servers/1234asdf", func(w http.ResponseWriter, r *http.Request) {
testhelper.TestMethod(t, r, "PUT")
- testhelper.TestHeader(t, r, "X-Auth-Token", tokenID)
+ testhelper.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
testhelper.TestHeader(t, r, "Accept", "application/json")
testhelper.TestHeader(t, r, "Content-Type", "application/json")
testhelper.TestJSONRequest(t, r, `{ "server": { "name": "new-name" } }`)
@@ -152,7 +142,7 @@
fmt.Fprintf(w, singleServerBody)
})
- client := serviceClient()
+ client := fake.ServiceClient()
actual, err := Update(client, "1234asdf", UpdateOpts{Name: "new-name"}).Extract()
if err != nil {
t.Fatalf("Unexpected Update error: %v", err)
@@ -167,13 +157,13 @@
testhelper.Mux.HandleFunc("/servers/1234asdf/action", func(w http.ResponseWriter, r *http.Request) {
testhelper.TestMethod(t, r, "POST")
- testhelper.TestHeader(t, r, "X-Auth-Token", tokenID)
+ testhelper.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
testhelper.TestJSONRequest(t, r, `{ "changePassword": { "adminPass": "new-password" } }`)
w.WriteHeader(http.StatusAccepted)
})
- client := serviceClient()
+ client := fake.ServiceClient()
err := ChangeAdminPassword(client, "1234asdf", "new-password")
if err != nil {
t.Errorf("Unexpected ChangeAdminPassword error: %v", err)
@@ -186,13 +176,13 @@
testhelper.Mux.HandleFunc("/servers/1234asdf/action", func(w http.ResponseWriter, r *http.Request) {
testhelper.TestMethod(t, r, "POST")
- testhelper.TestHeader(t, r, "X-Auth-Token", tokenID)
+ testhelper.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
testhelper.TestJSONRequest(t, r, `{ "reboot": { "type": "SOFT" } }`)
w.WriteHeader(http.StatusAccepted)
})
- client := serviceClient()
+ client := fake.ServiceClient()
err := Reboot(client, "1234asdf", SoftReboot)
if err != nil {
t.Errorf("Unexpected Reboot error: %v", err)
@@ -205,7 +195,7 @@
testhelper.Mux.HandleFunc("/servers/1234asdf/action", func(w http.ResponseWriter, r *http.Request) {
testhelper.TestMethod(t, r, "POST")
- testhelper.TestHeader(t, r, "X-Auth-Token", tokenID)
+ testhelper.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
testhelper.TestJSONRequest(t, r, `
{
"rebuild": {
@@ -241,13 +231,13 @@
testhelper.Mux.HandleFunc("/servers/1234asdf/action", func(w http.ResponseWriter, r *http.Request) {
testhelper.TestMethod(t, r, "POST")
- testhelper.TestHeader(t, r, "X-Auth-Token", tokenID)
+ testhelper.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
testhelper.TestJSONRequest(t, r, `{ "resize": { "flavorRef": "2" } }`)
w.WriteHeader(http.StatusAccepted)
})
- client := serviceClient()
+ client := fake.ServiceClient()
err := Resize(client, "1234asdf", "2")
if err != nil {
t.Errorf("Unexpected Reboot error: %v", err)
@@ -260,13 +250,13 @@
testhelper.Mux.HandleFunc("/servers/1234asdf/action", func(w http.ResponseWriter, r *http.Request) {
testhelper.TestMethod(t, r, "POST")
- testhelper.TestHeader(t, r, "X-Auth-Token", tokenID)
+ testhelper.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
testhelper.TestJSONRequest(t, r, `{ "confirmResize": null }`)
w.WriteHeader(http.StatusNoContent)
})
- client := serviceClient()
+ client := fake.ServiceClient()
err := ConfirmResize(client, "1234asdf")
if err != nil {
t.Errorf("Unexpected ConfirmResize error: %v", err)
@@ -279,13 +269,13 @@
testhelper.Mux.HandleFunc("/servers/1234asdf/action", func(w http.ResponseWriter, r *http.Request) {
testhelper.TestMethod(t, r, "POST")
- testhelper.TestHeader(t, r, "X-Auth-Token", tokenID)
+ testhelper.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
testhelper.TestJSONRequest(t, r, `{ "revertResize": null }`)
w.WriteHeader(http.StatusAccepted)
})
- client := serviceClient()
+ client := fake.ServiceClient()
err := RevertResize(client, "1234asdf")
if err != nil {
t.Errorf("Unexpected RevertResize error: %v", err)