diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index 02e241bc950..076607c0482 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -152,8 +152,8 @@ }, { "ImportPath": "github.com/rackspace/gophercloud", - "Comment": "v0.1.0-31-ge13cda2", - "Rev": "e13cda260ce48d63ce816f4fa72b6c6cd096596d" + "Comment": "v1.0.0", + "Rev": "da56de6a59e53fdd61be1b5d9b87df34c47ac420" }, { "ImportPath": "github.com/skratchdot/open-golang/open", diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/.editorconfig b/Godeps/_workspace/src/github.com/rackspace/gophercloud/.editorconfig deleted file mode 100644 index 2655ebc0839..00000000000 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/.editorconfig +++ /dev/null @@ -1,16 +0,0 @@ -# EditorConfig is awesome: http://EditorConfig.org - -# top-most EditorConfig file -root = true - -# All files -[*] -end_of_line = lf -insert_final_newline = true -charset = utf-8 -trim_trailing_whitespace = true - -# Golang -[*.go] -indent_style = tab -indent_size = 2 diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/.travis.yml b/Godeps/_workspace/src/github.com/rackspace/gophercloud/.travis.yml index 6e1dbd0a839..cf4f8cafcc6 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/.travis.yml +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/.travis.yml @@ -1,14 +1,14 @@ language: go install: - - go get -v . + - go get -v -tags 'fixtures acceptance' ./... go: - 1.1 - 1.2 - tip +script: script/cibuild after_success: - go get code.google.com/p/go.tools/cmd/cover - go get github.com/axw/gocov/gocov - go get github.com/mattn/goveralls - export PATH=$PATH:$HOME/gopath/bin/ - goveralls 2k7PTU3xa474Hymwgdj6XjqenNfGTNkO8 - diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/CONTRIBUTING.md b/Godeps/_workspace/src/github.com/rackspace/gophercloud/CONTRIBUTING.md new file mode 100644 index 00000000000..4f596a1fe4a --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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//gophercloud + ``` + +4. Checkout the latest development branch ([click here](/branches) to see all +the branches): + + ```bash + git checkout release/v1.0.1 + ``` + +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/Godeps/_workspace/src/github.com/rackspace/gophercloud/CONTRIBUTORS.md b/Godeps/_workspace/src/github.com/rackspace/gophercloud/CONTRIBUTORS.md index 9076695c3fc..eb97094b73f 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/CONTRIBUTORS.md +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/CONTRIBUTORS.md @@ -1,6 +1,12 @@ Contributors ============ -Samuel A. Falvo II -Glen Campbell -Jesse Noller +| Name | Email | +| ---- | ----- | +| Samuel A. Falvo II | +| Glen Campbell | +| Jesse Noller | +| Jon Perritt | +| Ash Wilson | +| Jamie Hannaford | +| Don Schenck | don.schenck@rackspace.com> diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/README.asciidoc b/Godeps/_workspace/src/github.com/rackspace/gophercloud/README.asciidoc deleted file mode 100644 index b7a7c016433..00000000000 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/README.asciidoc +++ /dev/null @@ -1,44 +0,0 @@ -== Gophercloud -- V0.1.0 image:https://secure.travis-ci.org/rackspace/gophercloud.png?branch=master["build status",link="https://travis-ci.org/rackspace/gophercloud"] - -Gophercloud currently lets you authenticate with OpenStack providers to create and manage servers. -We are working on extending the API to further include cloud files, block storage, DNS, databases, security groups, and other features. - -WARNING: This library is still in the very early stages of development. Unless you want to contribute, it probably isn't what you want. Yet. - -=== Outstanding Features - -1. Apache 2.0 License, making Gophercloud friendly to commercial and open-source enterprises alike. -2. Gophercloud is one of the most actively maintained Go SDKs for OpenStack. -3. Gophercloud supports Identity V2 and Nova V2 APIs. More coming soon! -4. The up-coming Gophercloud 0.2.0 release supports API extensions, and makes writing support for new extensions easy. -5. Gophercloud supports automatic reauthentication upon auth token timeout, if enabled by your software. -6. Gophercloud is the only SDK implementation with actual acceptance-level integration tests. - -=== What Does it Look Like? - -The Gophercloud 0.1.0 and earlier APIs are now deprecated and obsolete. -No new feature development will occur for 0.1.0 or 0.0.0. -However, we will accept and provide bug fixes for these APIs. -Please refer to the acceptance tests in the master brach for code examples using the v0.1.0 API. -The most up to date documentation for version 0.1.x can be found at link:http://godoc.org/github.com/rackspace/gophercloud[our Godoc.org documentation]. - -We are working on a new API that provides much better support for extensions, pagination, and other features that proved difficult to implement before. -This new API will be substantially more Go-idiomatic as well; one of the complaints received about 0.1.x and earlier is that it didn't "feel" right. -To see what this new API is going to look like, you can look at the code examples up on the link:http://gophercloud.io/docs.html[Gophercloud website]. -If you're interested in tracking progress, note that features for version 0.2.0 will appear in the `v0.2.0` branch until merged to master. - -=== How can I Contribute? - -After using Gophercloud for a while, you might find that it lacks some useful feature, or that existing behavior seems buggy. We welcome contributions -from our users for both missing functionality as well as for bug fixes. We encourage contributors to collaborate with the -link:http://gophercloud.io/community.html[Gophercloud community.] - -Finally, Gophercloud maintains its own link:http://gophercloud.io[announcements and updates blog.] -Feel free to check back now and again to see what's new. - -== License - -Copyright (C) 2013, 2014 Rackspace, Inc. - -Licensed under the Apache License, Version 2.0 - diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/README.md b/Godeps/_workspace/src/github.com/rackspace/gophercloud/README.md new file mode 100644 index 00000000000..9f7552b0d2d --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/README.md @@ -0,0 +1,161 @@ +# Gophercloud: the OpenStack SDK for Go +[![Build Status](https://travis-ci.org/rackspace/gophercloud.svg?branch=master)](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 +``` + +To protect yourself against changes in your dependencies, we highly recommend choosing a +[dependency management solution](https://code.google.com/p/go-wiki/wiki/PackageManagementTools) for +your projects, such as [godep](https://github.com/tools/godep). Once this is set up, you can install +Gophercloud as a dependency like so: + +```bash +go get github.com/rackspace/gophercloud + +# Edit your code to import relevant packages from "github.com/rackspace/gophercloud" + +godep save ./... +``` + +This will install all the source files you need into a `Godeps/_workspace` directory, which is +referenceable from your own source files when you use the `godep go` command. + +## Getting started + +### Credentials + +Because you'll be hitting an API, you will need to retrieve your OpenStack +credentials and either store them as environment variables or in your local Go +files. The first method is recommended because it decouples credential +information from source code, allowing you to push the latter to your version +control system without any security risk. + +You will need to retrieve the following: + +* username +* password +* tenant name or tenant ID +* a valid Keystone identity URL + +For users that have the OpenStack dashboard installed, there's a shortcut. If +you visit the `project/access_and_security` path in Horizon and click on the +"Download OpenStack RC File" button at the top right hand corner, you will +download a bash file that exports all of your access details to environment +variables. To execute the file, run `source admin-openrc.sh` and you will be +prompted for your password. + +### Authentication + +Once you have access to your credentials, you can begin plugging them into +Gophercloud. The next step is authentication, and this is handled by a base +"Provider" struct. To get one, you can either pass in your credentials +explicitly, or tell Gophercloud to use environment variables: + +```go +import ( + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/openstack" + "github.com/rackspace/gophercloud/openstack/utils" +) + +// Option 1: Pass in the values yourself +opts := gophercloud.AuthOptions{ + IdentityEndpoint: "https://my-openstack.com:5000/v2.0", + Username: "{username}", + Password: "{password}", + TenantID: "{tenant_id}", +} + +// Option 2: Use a utility function to retrieve all your environment variables +opts, err := openstack.AuthOptionsFromEnv() +``` + +Once you have the `opts` variable, you can pass it in and get back a +`ProviderClient` struct: + +```go +provider, err := openstack.AuthenticatedClient(opts) +``` + +The `ProviderClient` is the top-level client that all of your OpenStack services +derive from. The provider contains all of the authentication details that allow +your Go code to access the API - such as the base URL and token ID. + +### Provision a server + +Once we have a base Provider, we inject it as a dependency into each OpenStack +service. In order to work with the Compute API, we need a Compute service +client; which can be created like so: + +```go +client, err := openstack.NewComputeV2(provider, gophercloud.EndpointOpts{ + Region: os.Getenv("OS_REGION_NAME"), +}) +``` + +We then use this `client` for any Compute API operation we want. In our case, +we want to provision a new server - so we invoke the `Create` method and pass +in the flavor ID (hardware specification) and image ID (operating system) we're +interested in: + +```go +import "github.com/rackspace/gophercloud/openstack/compute/v2/servers" + +server, err := servers.Create(client, servers.CreateOpts{ + Name: "My new server!", + FlavorRef: "flavor_id", + ImageRef: "image_id", +}).Extract() +``` + +If you are unsure about what images and flavors are, you can read our [Compute +Getting Started guide](http://gophercloud.io/docs/compute). The above code +sample creates a new server with the parameters, and embodies the new resource +in the `server` variable (a +[`servers.Server`](http://godoc.org/github.com/rackspace/gophercloud) struct). + +### Next steps + +Cool! You've handled authentication, got your `ProviderClient` and provisioned +a new server. You're now ready to use more OpenStack services. + +* [Getting started with Compute](http://gophercloud.io/docs/compute) +* [Getting started with Object Storage](http://gophercloud.io/docs/object-storage) +* [Getting started with Networking](http://gophercloud.io/docs/networking) +* [Getting started with Block Storage](http://gophercloud.io/docs/block-storage) +* [Getting started with Identity](http://gophercloud.io/docs/identity) + +## Contributing + +Engaging the community and lowering barriers for contributors is something we +care a lot about. For this reason, we've taken the time to write a [contributing +guide](./CONTRIBUTING.md) for folks interested in getting involved in our project. +If you're not sure how you can get involved, feel free to submit an issue or +[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/Godeps/_workspace/src/github.com/rackspace/gophercloud/UPGRADING.md b/Godeps/_workspace/src/github.com/rackspace/gophercloud/UPGRADING.md new file mode 100644 index 00000000000..da3758ba1cf --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/UPGRADING.md @@ -0,0 +1,338 @@ +# Upgrading to v1.0.0 + +With the arrival of this new major version increment, the unfortunate news is +that breaking changes have been introduced to existing services. The API +has been completely rewritten from the ground up to make the library more +extensible, maintainable and easy-to-use. + +Below we've compiled upgrade instructions for the various services that +existed before. If you have a specific issue that is not addressed below, +please [submit an issue](/issues/new) or +[e-mail our support team](mailto:sdk-support@rackspace.com). + +* [Authentication](#authentication) +* [Servers](#servers) + * [List servers](#list-servers) + * [Get server details](#get-server-details) + * [Create server](#create-server) + * [Resize server](#resize-server) + * [Reboot server](#reboot-server) + * [Update server](#update-server) + * [Rebuild server](#rebuild-server) + * [Change admin password](#change-admin-password) + * [Delete server](#delete-server) + * [Rescue server](#rescue-server) +* [Images and flavors](#images-and-flavors) + * [List images](#list-images) + * [List flavors](#list-flavors) + * [Create/delete image](#createdelete-image) +* [Other](#other) + * [List keypairs](#list-keypairs) + * [Create/delete keypair](#createdelete-keypair) + * [List IP addresses](#list-ip-addresses) + +# Authentication + +One of the major differences that this release introduces is the level of +sub-packaging to differentiate between services and providers. You now have +the option of authenticating with OpenStack and other providers (like Rackspace). + +To authenticate with a vanilla OpenStack installation, you can either specify +your credentials like this: + +```go +import ( + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/openstack" +) + +opts := gophercloud.AuthOptions{ + IdentityEndpoint: "https://my-openstack.com:5000/v2.0", + Username: "{username}", + Password: "{password}", + TenantID: "{tenant_id}", +} +``` + +Or have them pulled in through environment variables, like this: + +```go +opts, err := openstack.AuthOptionsFromEnv() +``` + +Once you have your `AuthOptions` struct, you pass it in to get back a `Provider`, +like so: + +```go +provider, err := openstack.AuthenticatedClient(opts) +``` + +This provider is the top-level structure that all services are created from. + +# Servers + +Before you can interact with the Compute API, you need to retrieve a +`gophercloud.ServiceClient`. To do this: + +```go +// Define your region, etc. +opts := gophercloud.EndpointOpts{Region: "RegionOne"} + +client, err := openstack.NewComputeV2(provider, opts) +``` + +## List servers + +All operations that involve API collections (servers, flavors, images) now use +the `pagination.Pager` interface. This interface represents paginated entities +that can be iterated over. + +Once you have a Pager, you can then pass a callback function into its `EachPage` +method, and this will allow you to traverse over the collection and execute +arbitrary functionality. So, an example with list servers: + +```go +import ( + "fmt" + "github.com/rackspace/gophercloud/pagination" + "github.com/rackspace/gophercloud/openstack/compute/v2/servers" +) + +// We have the option of filtering the server list. If we want the full +// collection, leave it as an empty struct or nil +opts := servers.ListOpts{Name: "server_1"} + +// Retrieve a pager (i.e. a paginated collection) +pager := servers.List(client, opts) + +// Define an anonymous function to be executed on each page's iteration +err := pager.EachPage(func(page pagination.Page) (bool, error) { + serverList, err := servers.ExtractServers(page) + + // `s' will be a servers.Server struct + for _, s := range serverList { + fmt.Printf("We have a server. ID=%s, Name=%s", s.ID, s.Name) + } +}) +``` + +## Get server details + +```go +import "github.com/rackspace/gophercloud/openstack/compute/v2/servers" + +// Get the HTTP result +response := servers.Get(client, "server_id") + +// Extract a Server struct from the response +server, err := response.Extract() +``` + +## Create server + +```go +import "github.com/rackspace/gophercloud/openstack/compute/v2/servers" + +// Define our options +opts := servers.CreateOpts{ + Name: "new_server", + FlavorRef: "flavorID", + ImageRef: "imageID", +} + +// Get our response +response := servers.Create(client, opts) + +// Extract +server, err := response.Extract() +``` + +## Change admin password + +```go +import "github.com/rackspace/gophercloud/openstack/compute/v2/servers" + +result := servers.ChangeAdminPassword(client, "server_id", "newPassword_&123") +``` + +## Resize server + +```go +import "github.com/rackspace/gophercloud/openstack/compute/v2/servers" + +result := servers.Resize(client, "server_id", "new_flavor_id") +``` + +## Reboot server + +```go +import "github.com/rackspace/gophercloud/openstack/compute/v2/servers" + +// You have a choice of two reboot methods: servers.SoftReboot or servers.HardReboot +result := servers.Reboot(client, "server_id", servers.SoftReboot) +``` + +## Update server + +```go +import "github.com/rackspace/gophercloud/openstack/compute/v2/servers" + +opts := servers.UpdateOpts{Name: "new_name"} + +server, err := servers.Update(client, "server_id", opts).Extract() +``` + +## Rebuild server + +```go +import "github.com/rackspace/gophercloud/openstack/compute/v2/servers" + +// You have the option of specifying additional options +opts := RebuildOpts{ + Name: "new_name", + AdminPass: "admin_password", + ImageID: "image_id", + Metadata: map[string]string{"owner": "me"}, +} + +result := servers.Rebuild(client, "server_id", opts) + +// You can extract a servers.Server struct from the HTTP response +server, err := result.Extract() +``` + +## Delete server + +```go +import "github.com/rackspace/gophercloud/openstack/compute/v2/servers" + +response := servers.Delete(client, "server_id") +``` + +## Rescue server + +The server rescue extension for Compute is not currently supported. + +# Images and flavors + +## List images + +As with listing servers (see above), you first retrieve a Pager, and then pass +in a callback over each page: + +```go +import ( + "github.com/rackspace/gophercloud/pagination" + "github.com/rackspace/gophercloud/openstack/compute/v2/images" +) + +// We have the option of filtering the image list. If we want the full +// collection, leave it as an empty struct +opts := images.ListOpts{ChangesSince: "2014-01-01T01:02:03Z", Name: "Ubuntu 12.04"} + +// Retrieve a pager (i.e. a paginated collection) +pager := images.List(client, opts) + +// Define an anonymous function to be executed on each page's iteration +err := pager.EachPage(func(page pagination.Page) (bool, error) { + imageList, err := images.ExtractImages(page) + + for _, i := range imageList { + // "i" will be a images.Image + } +}) +``` + +## List flavors + +```go +import ( + "github.com/rackspace/gophercloud/pagination" + "github.com/rackspace/gophercloud/openstack/compute/v2/flavors" +) + +// We have the option of filtering the flavor list. If we want the full +// collection, leave it as an empty struct +opts := flavors.ListOpts{ChangesSince: "2014-01-01T01:02:03Z", MinRAM: 4} + +// Retrieve a pager (i.e. a paginated collection) +pager := flavors.List(client, opts) + +// Define an anonymous function to be executed on each page's iteration +err := pager.EachPage(func(page pagination.Page) (bool, error) { + flavorList, err := networks.ExtractFlavors(page) + + for _, f := range flavorList { + // "f" will be a flavors.Flavor + } +}) +``` + +## Create/delete image + +Image management has been shifted to Glance, but unfortunately this service is +not supported as of yet. You can, however, list Compute images like so: + +```go +import "github.com/rackspace/gophercloud/openstack/compute/v2/images" + +// Retrieve a pager (i.e. a paginated collection) +pager := images.List(client, opts) + +// Define an anonymous function to be executed on each page's iteration +err := pager.EachPage(func(page pagination.Page) (bool, error) { + imageList, err := images.ExtractImages(page) + + for _, i := range imageList { + // "i" will be a images.Image + } +}) +``` + +# Other + +## List keypairs + +```go +import "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs" + +// Retrieve a pager (i.e. a paginated collection) +pager := keypairs.List(client, opts) + +// Define an anonymous function to be executed on each page's iteration +err := pager.EachPage(func(page pagination.Page) (bool, error) { + keyList, err := keypairs.ExtractKeyPairs(page) + + for _, k := range keyList { + // "k" will be a keypairs.KeyPair + } +}) +``` + +## Create/delete keypairs + +To create a new keypair, you need to specify its name and, optionally, a +pregenerated OpenSSH-formatted public key. + +```go +import "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs" + +opts := keypairs.CreateOpts{ + Name: "new_key", + PublicKey: "...", +} + +response := keypairs.Create(client, opts) + +key, err := response.Extract() +``` + +To delete an existing keypair: + +```go +response := keypairs.Delete(client, "keypair_id") +``` + +## List IP addresses + +This operation is not currently supported. diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/00-authentication.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/00-authentication.go deleted file mode 100644 index 6467203f646..00000000000 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/00-authentication.go +++ /dev/null @@ -1,30 +0,0 @@ -// +build acceptance,old - -package main - -import ( - "fmt" - "github.com/rackspace/gophercloud" - "os" - "strings" -) - -func main() { - provider, username, _, apiKey := getCredentials() - - if !strings.Contains(provider, "rackspace") { - fmt.Fprintf(os.Stdout, "Skipping test because provider doesn't support API_KEYs\n") - return - } - - _, err := gophercloud.Authenticate( - provider, - gophercloud.AuthOptions{ - Username: username, - ApiKey: apiKey, - }, - ) - if err != nil { - panic(err) - } -} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/01-authentication.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/01-authentication.go deleted file mode 100644 index 5cc9d38d5d1..00000000000 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/01-authentication.go +++ /dev/null @@ -1,22 +0,0 @@ -// +build acceptance,old - -package main - -import ( - "github.com/rackspace/gophercloud" -) - -func main() { - provider, username, password, _ := getCredentials() - - _, err := gophercloud.Authenticate( - provider, - gophercloud.AuthOptions{ - Username: username, - Password: password, - }, - ) - if err != nil { - panic(err) - } -} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/02-list-servers.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/02-list-servers.go deleted file mode 100644 index 772852e3949..00000000000 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/02-list-servers.go +++ /dev/null @@ -1,62 +0,0 @@ -// +build acceptance,old - -package main - -import ( - "flag" - "fmt" - "github.com/rackspace/gophercloud" -) - -var quiet = flag.Bool("quiet", false, "Quiet mode, for acceptance testing. $? still indicates errors though.") - -func main() { - flag.Parse() - - withIdentity(false, func(acc gophercloud.AccessProvider) { - withServerApi(acc, func(api gophercloud.CloudServersProvider) { - tryFullDetails(api) - tryLinksOnly(api) - }) - }) -} - -func tryLinksOnly(api gophercloud.CloudServersProvider) { - servers, err := api.ListServersLinksOnly() - if err != nil { - panic(err) - } - - if !*quiet { - fmt.Println("Id,Name") - for _, s := range servers { - if s.AccessIPv4 != "" { - panic("IPv4 not expected") - } - - if s.Status != "" { - panic("Status not expected") - } - - if s.Progress != 0 { - panic("Progress not expected") - } - - fmt.Printf("%s,\"%s\"\n", s.Id, s.Name) - } - } -} - -func tryFullDetails(api gophercloud.CloudServersProvider) { - servers, err := api.ListServers() - if err != nil { - panic(err) - } - - if !*quiet { - fmt.Println("Id,Name,AccessIPv4,Status,Progress") - for _, s := range servers { - fmt.Printf("%s,\"%s\",%s,%s,%d\n", s.Id, s.Name, s.AccessIPv4, s.Status, s.Progress) - } - } -} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/03-get-server-details.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/03-get-server-details.go deleted file mode 100644 index 01140a9d706..00000000000 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/03-get-server-details.go +++ /dev/null @@ -1,134 +0,0 @@ -// +build acceptance,old - -package main - -import ( - "flag" - "fmt" - "github.com/rackspace/gophercloud" - "os" - "github.com/racker/perigee" -) - -var id = flag.String("i", "", "Server ID to get info on. Defaults to first server in your account if unspecified.") -var rgn = flag.String("r", "", "Datacenter region. Leave blank for default region.") -var quiet = flag.Bool("quiet", false, "Run quietly, for acceptance testing. $? non-zero if issue.") - -func main() { - flag.Parse() - - resultCode := 0 - withIdentity(false, func(auth gophercloud.AccessProvider) { - withServerApi(auth, func(servers gophercloud.CloudServersProvider) { - var ( - err error - serverId string - deleteAfterwards bool - ) - - // Figure out which server to provide server details for. - if *id == "" { - deleteAfterwards, serverId, err = locateAServer(servers) - if err != nil { - panic(err) - } - if deleteAfterwards { - defer servers.DeleteServerById(serverId) - } - } else { - serverId = *id - } - - // Grab server details by ID, and provide a report. - s, err := servers.ServerById(serverId) - if err != nil { - panic(err) - } - - configs := []string{ - "Access IPv4: %s\n", - "Access IPv6: %s\n", - " Created: %s\n", - " Flavor: %s\n", - " Host ID: %s\n", - " ID: %s\n", - " Image: %s\n", - " Name: %s\n", - " Progress: %s\n", - " Status: %s\n", - " Tenant ID: %s\n", - " Updated: %s\n", - " User ID: %s\n", - } - - values := []string{ - s.AccessIPv4, - s.AccessIPv6, - s.Created, - s.Flavor.Id, - s.HostId, - s.Id, - s.Image.Id, - s.Name, - fmt.Sprintf("%d", s.Progress), - s.Status, - s.TenantId, - s.Updated, - s.UserId, - } - - if !*quiet { - fmt.Println("Server info:") - for i, _ := range configs { - fmt.Printf(configs[i], values[i]) - } - } - }) - - // Negative test -- We should absolutely never panic for a server that doesn't exist. - withServerApi(auth, func(servers gophercloud.CloudServersProvider) { - _, err := servers.ServerById(randomString("garbage", 32)) - if err == nil { - fmt.Printf("Expected a 404 response when looking for a server known not to exist\n") - resultCode = 1 - } - perigeeError, ok := err.(*perigee.UnexpectedResponseCodeError) - if !ok { - fmt.Printf("Unexpected error type\n") - resultCode = 1 - } else { - if perigeeError.Actual != 404 { - fmt.Printf("Expected a 404 error code\n") - } - } - }) - }) - os.Exit(resultCode) -} - -// locateAServer queries the set of servers owned by the user. If at least one -// exists, the first found is picked, and its ID is returned. Otherwise, a new -// server will be created, and its ID returned. -// -// deleteAfter will be true if the caller should schedule a call to DeleteServerById() -// to clean up. -func locateAServer(servers gophercloud.CloudServersProvider) (deleteAfter bool, id string, err error) { - ss, err := servers.ListServers() - if err != nil { - return false, "", err - } - - if len(ss) > 0 { - // We could just cheat and dump the server details from ss[0]. - // But, that tests ListServers(), and not ServerById(). So, we - // elect not to cheat. - return false, ss[0].Id, nil - } - - serverId, err := createServer(servers, "", "", "", "") - if err != nil { - return false, "", err - } - err = waitForServerState(servers, serverId, "ACTIVE") - return true, serverId, err -} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/04-create-server.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/04-create-server.go deleted file mode 100644 index 03fd606cb7f..00000000000 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/04-create-server.go +++ /dev/null @@ -1,47 +0,0 @@ -// +build acceptance,old - -package main - -import ( - "flag" - "fmt" - "github.com/rackspace/gophercloud" -) - -var region, serverName, imageRef, flavorRef *string -var adminPass = flag.String("a", "", "Administrator password (auto-assigned if none)") -var quiet = flag.Bool("quiet", false, "Quiet mode for acceptance tests. $? non-zero if error.") - -func configure() { - region = flag.String("r", "", "Region in which to create the server. Leave blank for provider-default region.") - serverName = flag.String("n", randomString("ACPTTEST--", 16), "Server name (what you see in the control panel)") - imageRef = flag.String("i", "", "ID of image to deploy onto the server") - flavorRef = flag.String("f", "", "Flavor of server to deploy image upon") - - flag.Parse() -} - -func main() { - configure() - - withIdentity(false, func(auth gophercloud.AccessProvider) { - withServerApi(auth, func(servers gophercloud.CloudServersProvider) { - _, err := createServer(servers, *imageRef, *flavorRef, *serverName, *adminPass) - if err != nil { - panic(err) - } - - allServers, err := servers.ListServers() - if err != nil { - panic(err) - } - - if !*quiet { - fmt.Printf("ID,Name,Status,Progress\n") - for _, i := range allServers { - fmt.Printf("%s,\"%s\",%s,%d\n", i.Id, i.Name, i.Status, i.Progress) - } - } - }) - }) -} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/05-list-images.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/05-list-images.go deleted file mode 100644 index 5ead18beb4d..00000000000 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/05-list-images.go +++ /dev/null @@ -1,32 +0,0 @@ -// +build acceptance,old - -package main - -import ( - "flag" - "fmt" - "github.com/rackspace/gophercloud" -) - -var quiet = flag.Bool("quiet", false, "Quiet mode for acceptance testing. $? non-zero on error though.") -var rgn = flag.String("r", "", "Datacenter region to interrogate. Leave blank for provider-default region.") - -func main() { - flag.Parse() - - withIdentity(false, func(auth gophercloud.AccessProvider) { - withServerApi(auth, func(servers gophercloud.CloudServersProvider) { - images, err := servers.ListImages() - if err != nil { - panic(err) - } - - if !*quiet { - fmt.Println("ID,Name,MinRam,MinDisk") - for _, image := range images { - fmt.Printf("%s,\"%s\",%d,%d\n", image.Id, image.Name, image.MinRam, image.MinDisk) - } - } - }) - }) -} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/06-list-flavors.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/06-list-flavors.go deleted file mode 100644 index 65db7da6de8..00000000000 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/06-list-flavors.go +++ /dev/null @@ -1,32 +0,0 @@ -// +build acceptance,old - -package main - -import ( - "flag" - "fmt" - "github.com/rackspace/gophercloud" -) - -var quiet = flag.Bool("quiet", false, "Quiet mode for acceptance testing. $? non-zero on error though.") -var rgn = flag.String("r", "", "Datacenter region to interrogate. Leave blank for provider-default region.") - -func main() { - flag.Parse() - - withIdentity(false, func(auth gophercloud.AccessProvider) { - withServerApi(auth, func(servers gophercloud.CloudServersProvider) { - flavors, err := servers.ListFlavors() - if err != nil { - panic(err) - } - - if !*quiet { - fmt.Println("ID,Name,MinRam,MinDisk") - for _, f := range flavors { - fmt.Printf("%s,\"%s\",%d,%d\n", f.Id, f.Name, f.Ram, f.Disk) - } - } - }) - }) -} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/07-change-admin-password.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/07-change-admin-password.go deleted file mode 100644 index 880fbe8b8ad..00000000000 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/07-change-admin-password.go +++ /dev/null @@ -1,49 +0,0 @@ -// +build acceptance,old - -package main - -import ( - "flag" - "fmt" - "github.com/rackspace/gophercloud" -) - -var quiet = flag.Bool("quiet", false, "Quiet mode, for acceptance testing. $? still indicates errors though.") -var serverId = flag.String("i", "", "ID of server whose admin password is to be changed.") -var newPass = flag.String("p", "", "New password for the server.") - -func main() { - flag.Parse() - - withIdentity(false, func(acc gophercloud.AccessProvider) { - withServerApi(acc, func(api gophercloud.CloudServersProvider) { - // If user doesn't explicitly provide a server ID, create one dynamically. - if *serverId == "" { - var err error - *serverId, err = createServer(api, "", "", "", "") - if err != nil { - panic(err) - } - waitForServerState(api, *serverId, "ACTIVE") - } - - // If no password is provided, create one dynamically. - if *newPass == "" { - *newPass = randomString("", 16) - } - - // Submit the request for changing the admin password. - // Note that we don't verify this actually completes; - // doing so is beyond the scope of the SDK, and should be - // the responsibility of your specific OpenStack provider. - err := api.SetAdminPassword(*serverId, *newPass) - if err != nil { - panic(err) - } - - if !*quiet { - fmt.Println("Password change request submitted.") - } - }) - }) -} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/08-reauthentication.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/08-reauthentication.go deleted file mode 100644 index c46f5bbc84b..00000000000 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/08-reauthentication.go +++ /dev/null @@ -1,50 +0,0 @@ -// +build acceptance,old - -package main - -import ( - "flag" - "fmt" - "github.com/rackspace/gophercloud" -) - -var quiet = flag.Bool("quiet", false, "Quiet mode for acceptance testing. $? non-zero on error though.") -var rgn = flag.String("r", "", "Datacenter region to interrogate. Leave blank for provider-default region.") - -func main() { - flag.Parse() - - // Invoke withIdentity such that re-auth is enabled. - withIdentity(true, func(auth gophercloud.AccessProvider) { - token1 := auth.AuthToken() - - withServerApi(auth, func(servers gophercloud.CloudServersProvider) { - // Just to confirm everything works, we should be able to list images without error. - _, err := servers.ListImages() - if err != nil { - panic(err) - } - - // Revoke our current authentication token. - auth.Revoke(auth.AuthToken()) - - // Attempt to list images again. This should _succeed_, because we enabled re-authentication. - _, err = servers.ListImages() - if err != nil { - panic(err) - } - - // However, our new authentication token should differ. - token2 := auth.AuthToken() - - if !*quiet { - fmt.Println("Old authentication token: ", token1) - fmt.Println("New authentication token: ", token2) - } - - if token1 == token2 { - panic("Tokens should differ") - } - }) - }) -} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/09-resize-server.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/09-resize-server.go deleted file mode 100644 index a2ef3c87e61..00000000000 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/09-resize-server.go +++ /dev/null @@ -1,102 +0,0 @@ -// +build acceptance,old - -package main - -import ( - "flag" - "fmt" - "github.com/rackspace/gophercloud" - "time" -) - -var quiet = flag.Bool("quiet", false, "Quiet mode, for acceptance testing. $? still indicates errors though.") - -func main() { - flag.Parse() - - withIdentity(false, func(acc gophercloud.AccessProvider) { - withServerApi(acc, func(api gophercloud.CloudServersProvider) { - // These tests are going to take some time to complete. - // So, we'll do two tests at the same time to help amortize test time. - done := make(chan bool) - go resizeRejectTest(api, done) - go resizeAcceptTest(api, done) - _ = <-done - _ = <-done - - if !*quiet { - fmt.Println("Done.") - } - }) - }) -} - -// Perform the resize test, but reject the resize request. -func resizeRejectTest(api gophercloud.CloudServersProvider, done chan bool) { - withServer(api, func(id string) { - newFlavorId := findAlternativeFlavor() - err := api.ResizeServer(id, randomString("ACPTTEST", 24), newFlavorId, "") - if err != nil { - panic(err) - } - - waitForServerState(api, id, "VERIFY_RESIZE") - - err = api.RevertResize(id) - if err != nil { - panic(err) - } - }) - done <- true -} - -// Perform the resize test, but accept the resize request. -func resizeAcceptTest(api gophercloud.CloudServersProvider, done chan bool) { - withServer(api, func(id string) { - newFlavorId := findAlternativeFlavor() - err := api.ResizeServer(id, randomString("ACPTTEST", 24), newFlavorId, "") - if err != nil { - panic(err) - } - - waitForServerState(api, id, "VERIFY_RESIZE") - - err = api.ConfirmResize(id) - if err != nil { - panic(err) - } - }) - done <- true -} - -func withServer(api gophercloud.CloudServersProvider, f func(string)) { - id, err := createServer(api, "", "", "", "") - if err != nil { - panic(err) - } - - for { - s, err := api.ServerById(id) - if err != nil { - panic(err) - } - if s.Status == "ACTIVE" { - break - } - time.Sleep(10 * time.Second) - } - - f(id) - - // I've learned that resizing an instance can fail if a delete request - // comes in prior to its completion. This ends up leaving the server - // in an error state, and neither the resize NOR the delete complete. - // This is a bug in OpenStack, as far as I'm concerned, but thankfully, - // there's an easy work-around -- just wait for your server to return to - // active state first! - waitForServerState(api, id, "ACTIVE") - err = api.DeleteServerById(id) - if err != nil { - panic(err) - } -} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/10-reboot-server.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/10-reboot-server.go deleted file mode 100644 index ba6215a3259..00000000000 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/10-reboot-server.go +++ /dev/null @@ -1,45 +0,0 @@ -// +build acceptance,old - -package main - -import ( - "flag" - "fmt" - "github.com/rackspace/gophercloud" -) - -var quiet = flag.Bool("quiet", false, "Quiet mode, for acceptance testing. $? still indicates errors though.") - -func main() { - flag.Parse() - - withIdentity(false, func(acc gophercloud.AccessProvider) { - withServerApi(acc, func(servers gophercloud.CloudServersProvider) { - log("Creating server") - serverId, err := createServer(servers, "", "", "", "") - if err != nil { - panic(err) - } - waitForServerState(servers, serverId, "ACTIVE") - - log("Soft-rebooting server") - servers.RebootServer(serverId, false) - waitForServerState(servers, serverId, "REBOOT") - waitForServerState(servers, serverId, "ACTIVE") - - log("Hard-rebooting server") - servers.RebootServer(serverId, true) - waitForServerState(servers, serverId, "HARD_REBOOT") - waitForServerState(servers, serverId, "ACTIVE") - - log("Done") - servers.DeleteServerById(serverId) - }) - }) -} - -func log(s string) { - if !*quiet { - fmt.Println(s) - } -} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/11-rescue-unrescue-server.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/11-rescue-unrescue-server.go deleted file mode 100644 index 008ad9d597d..00000000000 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/11-rescue-unrescue-server.go +++ /dev/null @@ -1,52 +0,0 @@ -// +build acceptance,old - -package main - -import ( - "flag" - "fmt" - "github.com/rackspace/gophercloud" -) - -var quiet = flag.Bool("quiet", false, "Quiet mode, for acceptance testing. $? still indicates errors though.") - -func main() { - flag.Parse() - withIdentity(false, func(acc gophercloud.AccessProvider) { - withServerApi(acc, func(servers gophercloud.CloudServersProvider) { - log("Creating server") - id, err := createServer(servers, "", "", "", "") - if err != nil { - panic(err) - } - waitForServerState(servers, id, "ACTIVE") - defer servers.DeleteServerById(id) - - log("Rescuing server") - adminPass, err := servers.RescueServer(id) - if err != nil { - panic(err) - } - log(" Admin password = " + adminPass) - if len(adminPass) < 1 { - panic("Empty admin password") - } - waitForServerState(servers, id, "RESCUE") - - log("Unrescuing server") - err = servers.UnrescueServer(id) - if err != nil { - panic(err) - } - waitForServerState(servers, id, "ACTIVE") - - log("Done") - }) - }) -} - -func log(s string) { - if !*quiet { - fmt.Println(s) - } -} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/12-update-server.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/12-update-server.go deleted file mode 100644 index c0191f1fee1..00000000000 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/12-update-server.go +++ /dev/null @@ -1,46 +0,0 @@ -// +build acceptance,old - -package main - -import ( - "flag" - "fmt" - "github.com/rackspace/gophercloud" -) - -var quiet = flag.Bool("quiet", false, "Quiet mode, for acceptance testing. $? still indicates errors though.") - -func main() { - flag.Parse() - withIdentity(false, func(acc gophercloud.AccessProvider) { - withServerApi(acc, func(servers gophercloud.CloudServersProvider) { - log("Creating server") - id, err := createServer(servers, "", "", "", "") - if err != nil { - panic(err) - } - waitForServerState(servers, id, "ACTIVE") - defer servers.DeleteServerById(id) - - log("Updating name of server") - newName := randomString("ACPTTEST", 32) - newDetails, err := servers.UpdateServer(id, gophercloud.NewServerSettings{ - Name: newName, - }) - if err != nil { - panic(err) - } - if newDetails.Name != newName { - panic("Name change didn't appear to take") - } - - log("Done") - }) - }) -} - -func log(s string) { - if !*quiet { - fmt.Println(s) - } -} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/13-rebuild-server.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/13-rebuild-server.go deleted file mode 100644 index ae7e19f60d0..00000000000 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/13-rebuild-server.go +++ /dev/null @@ -1,46 +0,0 @@ -// +build acceptance,old - -package main - -import ( - "flag" - "fmt" - "github.com/rackspace/gophercloud" -) - -var quiet = flag.Bool("quiet", false, "Quiet mode, for acceptance testing. $? still indicates errors though.") - -func main() { - flag.Parse() - withIdentity(false, func(acc gophercloud.AccessProvider) { - withServerApi(acc, func(servers gophercloud.CloudServersProvider) { - log("Creating server") - id, err := createServer(servers, "", "", "", "") - if err != nil { - panic(err) - } - waitForServerState(servers, id, "ACTIVE") - defer servers.DeleteServerById(id) - - log("Rebuilding server") - newDetails, err := servers.RebuildServer(id, gophercloud.NewServer{ - Name: randomString("ACPTTEST", 32), - ImageRef: findAlternativeImage(), - FlavorRef: findAlternativeFlavor(), - AdminPass: randomString("", 16), - }) - if err != nil { - panic(err) - } - waitForServerState(servers, newDetails.Id, "ACTIVE") - - log("Done") - }) - }) -} - -func log(s string) { - if !*quiet { - fmt.Println(s) - } -} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/14-list-addresses.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/14-list-addresses.go deleted file mode 100644 index 1d7d26b54d1..00000000000 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/14-list-addresses.go +++ /dev/null @@ -1,66 +0,0 @@ -// +build acceptance,old - -package main - -import ( - "flag" - "fmt" - "github.com/rackspace/gophercloud" -) - -var quiet = flag.Bool("quiet", false, "Quiet mode, for acceptance testing. $? still indicates errors though.") - -func main() { - flag.Parse() - withIdentity(false, func(acc gophercloud.AccessProvider) { - withServerApi(acc, func(api gophercloud.CloudServersProvider) { - log("Creating server") - id, err := createServer(api, "", "", "", "") - if err != nil { - panic(err) - } - waitForServerState(api, id, "ACTIVE") - defer api.DeleteServerById(id) - - tryAllAddresses(id, api) - tryAddressesByNetwork("private", id, api) - - log("Done") - }) - }) -} - -func tryAllAddresses(id string, api gophercloud.CloudServersProvider) { - log("Getting list of all addresses...") - addresses, err := api.ListAddresses(id) - if (err != nil) && (err != gophercloud.WarnUnauthoritative) { - panic(err) - } - if err == gophercloud.WarnUnauthoritative { - log("Uh oh -- got a response back, but it's not authoritative for some reason.") - } - if !*quiet { - fmt.Println("Addresses:") - fmt.Printf("%+v\n", addresses) - } -} - -func tryAddressesByNetwork(networkLabel string, id string, api gophercloud.CloudServersProvider) { - log("Getting list of addresses on", networkLabel, "network...") - network, err := api.ListAddressesByNetwork(id, networkLabel) - if (err != nil) && (err != gophercloud.WarnUnauthoritative) { - panic(err) - } - if err == gophercloud.WarnUnauthoritative { - log("Uh oh -- got a response back, but it's not authoritative for some reason.") - } - for _, addr := range network[networkLabel] { - log("Address:", addr.Addr, " IPv", addr.Version) - } -} - -func log(s ...interface{}) { - if !*quiet { - fmt.Println(s...) - } -} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/15-list-keypairs.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/15-list-keypairs.go deleted file mode 100644 index 1a617edd200..00000000000 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/15-list-keypairs.go +++ /dev/null @@ -1,32 +0,0 @@ -// +build acceptance,old - -package main - -import ( - "flag" - "fmt" - "github.com/rackspace/gophercloud" -) - -var quiet = flag.Bool("quiet", false, "Quiet mode for acceptance testing. $? non-zero on error though.") -var rgn = flag.String("r", "", "Datacenter region to interrogate. Leave blank for provider-default region.") - -func main() { - flag.Parse() - - withIdentity(false, func(auth gophercloud.AccessProvider) { - withServerApi(auth, func(servers gophercloud.CloudServersProvider) { - keypairs, err := servers.ListKeyPairs() - if err != nil { - panic(err) - } - - if !*quiet { - fmt.Println("name,fingerprint,publickey") - for _, key := range keypairs { - fmt.Printf("%s,%s,%s\n", key.Name, key.FingerPrint, key.PublicKey) - } - } - }) - }) -} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/16-create-delete-keypair.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/16-create-delete-keypair.go deleted file mode 100644 index f59e51c50f5..00000000000 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/16-create-delete-keypair.go +++ /dev/null @@ -1,45 +0,0 @@ -// +build acceptance,old - -package main - -import ( - "flag" - "fmt" - "github.com/rackspace/gophercloud" -) - -var quiet = flag.Bool("quiet", false, "Quiet mode for acceptance testing. $? non-zero on error though.") -var rgn = flag.String("r", "", "Datacenter region to interrogate. Leave blank for provider-default region.") - -func main() { - flag.Parse() - - withIdentity(false, func(auth gophercloud.AccessProvider) { - withServerApi(auth, func(servers gophercloud.CloudServersProvider) { - name := randomString("ACPTTEST", 16) - kp := gophercloud.NewKeyPair{ - Name: name, - } - keypair, err := servers.CreateKeyPair(kp) - if err != nil { - panic(err) - } - if !*quiet { - fmt.Printf("%s,%s,%s\n", keypair.Name, keypair.FingerPrint, keypair.PublicKey) - } - - keypair, err = servers.ShowKeyPair(name) - if err != nil { - panic(err) - } - if !*quiet { - fmt.Printf("%s,%s,%s\n", keypair.Name, keypair.FingerPrint, keypair.PublicKey) - } - - err = servers.DeleteKeyPair(name) - if err != nil { - panic(err) - } - }) - }) -} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/17-create-delete-image.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/17-create-delete-image.go deleted file mode 100644 index b3d80a37e2f..00000000000 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/17-create-delete-image.go +++ /dev/null @@ -1,52 +0,0 @@ -// +build acceptance,old - -package main - -import ( - "flag" - "fmt" - "github.com/rackspace/gophercloud" -) - -var quiet = flag.Bool("quiet", false, "Quiet mode for acceptance testing. $? non-zero on error though.") -var rgn = flag.String("r", "", "Datacenter region to interrogate. Leave blank for provider-default region.") - -func main() { - flag.Parse() - - withIdentity(false, func(auth gophercloud.AccessProvider) { - withServerApi(auth, func(servers gophercloud.CloudServersProvider) { - log("Creating server") - serverId, err := createServer(servers, "", "", "", "") - if err != nil { - panic(err) - } - waitForServerState(servers, serverId, "ACTIVE") - - log("Creating image") - name := randomString("ACPTTEST", 16) - createImage := gophercloud.CreateImage{ - Name: name, - } - imageId, err := servers.CreateImage(serverId, createImage) - if err != nil { - panic(err) - } - waitForImageState(servers, imageId, "ACTIVE") - - log("Deleting server") - servers.DeleteServerById(serverId) - - log("Deleting image") - servers.DeleteImageById(imageId) - - log("Done") - }) - }) -} - -func log(s string) { - if !*quiet { - fmt.Println(s) - } -} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/18-osutil-authentication.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/18-osutil-authentication.go deleted file mode 100644 index 01ff4e98554..00000000000 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/18-osutil-authentication.go +++ /dev/null @@ -1,19 +0,0 @@ -// +build acceptance,old - -package main - -import ( - "github.com/rackspace/gophercloud" - "github.com/rackspace/gophercloud/osutil" -) - -func main() { - provider, authOptions, err := osutil.AuthOptions() - if err != nil { - panic(err) - } - _, err = gophercloud.Authenticate(provider, authOptions) - if err != nil { - panic(err) - } -} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/19-list-addresses-0.1.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/19-list-addresses-0.1.go deleted file mode 100644 index d60557b031a..00000000000 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/19-list-addresses-0.1.go +++ /dev/null @@ -1,58 +0,0 @@ -// +build acceptance,old - -package main - -import ( - "flag" - "fmt" - "github.com/rackspace/gophercloud" -) - -var quiet = flag.Bool("quiet", false, "Quiet mode, for acceptance testing. $? still indicates errors though.") - -func main() { - flag.Parse() - withIdentity(false, func(acc gophercloud.AccessProvider) { - withServerApi(acc, func(api gophercloud.CloudServersProvider) { - log("Creating server") - id, err := createServer(api, "", "", "", "") - if err != nil { - panic(err) - } - waitForServerState(api, id, "ACTIVE") - defer api.DeleteServerById(id) - - tryAllAddresses(id, api) - - log("Done") - }) - }) -} - -func tryAllAddresses(id string, api gophercloud.CloudServersProvider) { - log("Getting the server instance") - s, err := api.ServerById(id) - if err != nil { - panic(err) - } - - log("Getting the complete set of pools") - ps, err := s.AllAddressPools() - if err != nil { - panic(err) - } - - log("Listing IPs for each pool") - for k, v := range ps { - log(fmt.Sprintf(" Pool %s", k)) - for _, a := range v { - log(fmt.Sprintf(" IP: %s, Version: %d", a.Addr, a.Version)) - } - } -} - -func log(s ...interface{}) { - if !*quiet { - fmt.Println(s...) - } -} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/99-delete-server.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/99-delete-server.go deleted file mode 100644 index 3e38ba4f353..00000000000 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/99-delete-server.go +++ /dev/null @@ -1,48 +0,0 @@ -// +build acceptance,old - -package main - -import ( - "flag" - "fmt" - "github.com/rackspace/gophercloud" -) - -var quiet = flag.Bool("quiet", false, "Quiet operation for acceptance tests. $? non-zero if problem.") -var region = flag.String("r", "", "Datacenter region. Leave blank for provider-default region.") - -func main() { - flag.Parse() - - withIdentity(false, func(auth gophercloud.AccessProvider) { - withServerApi(auth, func(servers gophercloud.CloudServersProvider) { - // Grab a listing of all servers. - ss, err := servers.ListServers() - if err != nil { - panic(err) - } - - // And for each one that starts with the ACPTTEST prefix, delete it. - // These are likely left-overs from previously running acceptance tests. - // Note that 04-create-servers.go is intended to leak servers by intention, - // so as to test this code. :) - n := 0 - for _, s := range ss { - if len(s.Name) < 8 { - continue - } - if s.Name[0:8] == "ACPTTEST" { - err := servers.DeleteServerById(s.Id) - if err != nil { - panic(err) - } - n++ - } - } - - if !*quiet { - fmt.Printf("%d servers removed.\n", n) - } - }) - }) -} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/README.md b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/README.md new file mode 100644 index 00000000000..3199837c20a --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/README.md @@ -0,0 +1,57 @@ +# Gophercloud Acceptance tests + +The purpose of these acceptance tests is to validate that SDK features meet +the requirements of a contract - to consumers, other parts of the library, and +to a remote API. + +> **Note:** Because every test will be run against a real API endpoint, you +> may incur bandwidth and service charges for all the resource usage. These +> tests *should* remove their remote products automatically. However, there may +> be certain cases where this does not happen; always double-check to make sure +> you have no stragglers left behind. + +### Step 1. Set environment variables + +A lot of tests rely on environment variables for configuration - so you will need +to set them before running the suite. If you're testing against pure OpenStack APIs, +you can download a file that contains all of these variables for you: just visit +the `project/access_and_security` page in your control panel and click the "Download +OpenStack RC File" button at the top right. For all other providers, you will need +to set them manually. + +#### Authentication + +|Name|Description| +|---|---| +|`OS_USERNAME`|Your API username| +|`OS_PASSWORD`|Your API password| +|`OS_AUTH_URL`|The identity URL you need to authenticate| +|`OS_TENANT_NAME`|Your API tenant name| +|`OS_TENANT_ID`|Your API tenant ID| +|`RS_USERNAME`|Your Rackspace username| +|`RS_API_KEY`|Your Rackspace API key| + +#### General + +|Name|Description| +|---|---| +|`OS_REGION_NAME`|The region you want your resources to reside in| +|`RS_REGION`|Rackspace region you want your resource to reside in| + +#### Compute + +|Name|Description| +|---|---| +|`OS_IMAGE_ID`|The ID of the image your want your server to be based on| +|`OS_FLAVOR_ID`|The ID of the flavor you want your server to be based on| +|`OS_FLAVOR_ID_RESIZE`|The ID of the flavor you want your server to be resized to| +|`RS_IMAGE_ID`|The ID of the image you want servers to be created with| +|`RS_FLAVOR_ID`|The ID of the flavor you want your server to be created with| + +### 2. Run the test suite + +From the root directory, run: + +``` +./script/acceptancetest +``` diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/libargs.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/libargs.go deleted file mode 100644 index cf234e7e8af..00000000000 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/libargs.go +++ /dev/null @@ -1,239 +0,0 @@ -// +build acceptance,old - -package main - -import ( - "crypto/rand" - "fmt" - "github.com/rackspace/gophercloud" - "os" - "strings" - "time" -) - -// getCredentials will verify existence of needed credential information -// provided through environment variables. This function will not return -// if at least one piece of required information is missing. -func getCredentials() (provider, username, password, apiKey string) { - provider = os.Getenv("SDK_PROVIDER") - username = os.Getenv("SDK_USERNAME") - password = os.Getenv("SDK_PASSWORD") - apiKey = os.Getenv("SDK_API_KEY") - var authURL = os.Getenv("OS_AUTH_URL") - - if (provider == "") || (username == "") || (password == "") { - fmt.Fprintf(os.Stderr, "One or more of the following environment variables aren't set:\n") - fmt.Fprintf(os.Stderr, " SDK_PROVIDER=\"%s\"\n", provider) - fmt.Fprintf(os.Stderr, " SDK_USERNAME=\"%s\"\n", username) - fmt.Fprintf(os.Stderr, " SDK_PASSWORD=\"%s\"\n", password) - os.Exit(1) - } - - if strings.Contains(provider, "rackspace") && (authURL != "") { - provider = authURL + "/v2.0/tokens" - } - - return -} - -// 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) -} - -// aSuitableImage finds a minimal image for use in dynamically creating servers. -// If none can be found, this function will panic. -func aSuitableImage(api gophercloud.CloudServersProvider) string { - images, err := api.ListImages() - if err != nil { - panic(err) - } - - // TODO(sfalvo): - // Works for Rackspace, might not work for your provider! - // Need to figure out why ListImages() provides 0 values for - // Ram and Disk fields. - // - // Until then, just return Ubuntu 12.04 LTS. - for i := 0; i < len(images); i++ { - if strings.Contains(images[i].Name, "Ubuntu 12.04 LTS") { - return images[i].Id - } - } - panic("Image for Ubuntu 12.04 LTS not found.") -} - -// aSuitableFlavor finds the minimum flavor capable of running the test image -// chosen by aSuitableImage. If none can be found, this function will panic. -func aSuitableFlavor(api gophercloud.CloudServersProvider) string { - flavors, err := api.ListFlavors() - if err != nil { - panic(err) - } - - // TODO(sfalvo): - // Works for Rackspace, might not work for your provider! - // Need to figure out why ListFlavors() provides 0 values for - // Ram and Disk fields. - // - // Until then, just return Ubuntu 12.04 LTS. - for i := 0; i < len(flavors); i++ { - if flavors[i].Id == "2" { - return flavors[i].Id - } - } - panic("Flavor 2 (512MB 1-core 20GB machine) not found.") -} - -// createServer creates a new server in a manner compatible with acceptance testing. -// In particular, it ensures that the name of the server always starts with "ACPTTEST--", -// which the delete servers acceptance test relies on to identify servers to delete. -// Passing in empty image and flavor references will force the use of reasonable defaults. -// An empty name string will result in a dynamically created name prefixed with "ACPTTEST--". -// A blank admin password will cause a password to be automatically generated; however, -// at present no means of recovering this password exists, as no acceptance tests yet require -// this data. -func createServer(servers gophercloud.CloudServersProvider, imageRef, flavorRef, name, adminPass string) (string, error) { - if imageRef == "" { - imageRef = aSuitableImage(servers) - } - - if flavorRef == "" { - flavorRef = aSuitableFlavor(servers) - } - - if len(name) < 1 { - name = randomString("ACPTTEST", 16) - } - - if (len(name) < 8) || (name[0:8] != "ACPTTEST") { - name = fmt.Sprintf("ACPTTEST--%s", name) - } - - newServer, err := servers.CreateServer(gophercloud.NewServer{ - Name: name, - ImageRef: imageRef, - FlavorRef: flavorRef, - AdminPass: adminPass, - }) - - if err != nil { - return "", err - } - - return newServer.Id, nil -} - -// findAlternativeFlavor locates a flavor to resize a server to. It is guaranteed to be different -// than what aSuitableFlavor() returns. If none could be found, this function will panic. -func findAlternativeFlavor() string { - return "3" // 1GB image, up from 512MB image -} - -// findAlternativeImage locates an image to resize or rebuild a server with. It is guaranteed to be -// different than what aSuitableImage() returns. If none could be found, this function will panic. -func findAlternativeImage() string { - return "c6f9c411-e708-4952-91e5-62ded5ea4d3e" -} - -// withIdentity authenticates the user against the provider's identity service, and provides an -// accessor for additional services. -func withIdentity(ar bool, f func(gophercloud.AccessProvider)) { - _, _, _, apiKey := getCredentials() - if len(apiKey) == 0 { - withPasswordIdentity(ar, f) - } else { - withAPIKeyIdentity(ar, f) - } -} - -func withPasswordIdentity(ar bool, f func(gophercloud.AccessProvider)) { - provider, username, password, _ := getCredentials() - acc, err := gophercloud.Authenticate( - provider, - gophercloud.AuthOptions{ - Username: username, - Password: password, - AllowReauth: ar, - }, - ) - if err != nil { - panic(err) - } - - f(acc) -} - -func withAPIKeyIdentity(ar bool, f func(gophercloud.AccessProvider)) { - provider, username, _, apiKey := getCredentials() - acc, err := gophercloud.Authenticate( - provider, - gophercloud.AuthOptions{ - Username: username, - ApiKey: apiKey, - AllowReauth: ar, - }, - ) - if err != nil { - panic(err) - } - - f(acc) -} - -// withServerApi acquires the cloud servers API. -func withServerApi(acc gophercloud.AccessProvider, f func(gophercloud.CloudServersProvider)) { - api, err := gophercloud.ServersApi(acc, gophercloud.ApiCriteria{ - Name: "cloudServersOpenStack", - VersionId: "2", - UrlChoice: gophercloud.PublicURL, - }) - if err != nil { - panic(err) - } - - f(api) -} - -// waitForServerState polls, every 10 seconds, for a given server to appear in the indicated state. -// This call will block forever if it never appears in the desired state, so if a timeout is required, -// make sure to call this function in a goroutine. -func waitForServerState(api gophercloud.CloudServersProvider, id, state string) error { - for { - s, err := api.ServerById(id) - if err != nil { - return err - } - if s.Status == state { - return nil - } - time.Sleep(10 * time.Second) - } - panic("Impossible") -} - -// waitForImageState polls, every 10 seconds, for a given image to appear in the indicated state. -// This call will block forever if it never appears in the desired state, so if a timeout is required, -// make sure to call this function in a goroutine. -func waitForImageState(api gophercloud.CloudServersProvider, id, state string) error { - for { - s, err := api.ImageById(id) - if err != nil { - return err - } - if s.Status == state { - return nil - } - time.Sleep(10 * time.Second) - } - panic("Impossible") -} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/blockstorage/v1/snapshots_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/blockstorage/v1/snapshots_test.go new file mode 100644 index 00000000000..9132ee5e702 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/blockstorage/v1/snapshots_test.go @@ -0,0 +1,70 @@ +// +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" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestSnapshots(t *testing.T) { + + client, err := newClient() + th.AssertNoErr(t, err) + + v, err := volumes.Create(client, &volumes.CreateOpts{ + Name: "gophercloud-test-volume", + Size: 1, + }).Extract() + th.AssertNoErr(t, err) + + err = volumes.WaitForStatus(client, v.ID, "available", 120) + th.AssertNoErr(t, err) + + t.Logf("Created volume: %v\n", v) + + ss, err := snapshots.Create(client, &snapshots.CreateOpts{ + Name: "gophercloud-test-snapshot", + VolumeID: v.ID, + }).Extract() + th.AssertNoErr(t, err) + + err = snapshots.WaitForStatus(client, ss.ID, "available", 120) + th.AssertNoErr(t, err) + + t.Logf("Created snapshot: %+v\n", ss) + + err = snapshots.Delete(client, ss.ID).ExtractErr() + th.AssertNoErr(t, err) + + err = gophercloud.WaitFor(120, func() (bool, error) { + _, err := snapshots.Get(client, ss.ID).Extract() + if err != nil { + return true, nil + } + + return false, nil + }) + th.AssertNoErr(t, err) + + t.Log("Deleted snapshot\n") + + err = volumes.Delete(client, v.ID).ExtractErr() + th.AssertNoErr(t, err) + + err = gophercloud.WaitFor(120, func() (bool, error) { + _, err := volumes.Get(client, v.ID).Extract() + if err != nil { + return true, nil + } + + return false, nil + }) + th.AssertNoErr(t, err) + + t.Log("Deleted volume\n") +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/blockstorage/v1/volumes_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/blockstorage/v1/volumes_test.go new file mode 100644 index 00000000000..99da39a9736 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/blockstorage/v1/volumes_test.go @@ -0,0 +1,63 @@ +// +build acceptance blockstorage + +package v1 + +import ( + "os" + "testing" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/openstack" + "github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" +) + +func newClient() (*gophercloud.ServiceClient, error) { + ao, err := openstack.AuthOptionsFromEnv() + th.AssertNoErr(t, err) + + client, err := openstack.AuthenticatedClient(ao) + th.AssertNoErr(t, err) + + return openstack.NewBlockStorageV1(client, gophercloud.EndpointOpts{ + Region: os.Getenv("OS_REGION_NAME"), + }) +} + +func TestVolumes(t *testing.T) { + client, err := newClient() + th.AssertNoErr(t, err) + + cv, err := volumes.Create(client, &volumes.CreateOpts{ + Size: 1, + Name: "gophercloud-test-volume", + }).Extract() + th.AssertNoErr(t, err) + defer func() { + err = volumes.WaitForStatus(client, cv.ID, "available", 60) + th.AssertNoErr(t, err) + err = volumes.Delete(client, cv.ID).ExtractErr() + th.AssertNoErr(t, err) + }() + + _, err = volumes.Update(client, cv.ID, &volumes.UpdateOpts{ + Name: "gophercloud-updated-volume", + }).Extract() + th.AssertNoErr(t, err) + + v, err := volumes.Get(client, cv.ID).Extract() + th.AssertNoErr(t, err) + t.Logf("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) + th.CheckEquals(t, 1, len(vols)) + return true, err + }) + th.AssertNoErr(t, err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/blockstorage/v1/volumetypes_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/blockstorage/v1/volumetypes_test.go new file mode 100644 index 00000000000..5adcd819694 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/blockstorage/v1/volumetypes_test.go @@ -0,0 +1,49 @@ +// +build acceptance + +package v1 + +import ( + "testing" + "time" + + "github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumetypes" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestVolumeTypes(t *testing.T) { + client, err := newClient() + th.AssertNoErr(t, err) + + vt, err := volumetypes.Create(client, &volumetypes.CreateOpts{ + ExtraSpecs: map[string]interface{}{ + "capabilities": "gpu", + "priority": 3, + }, + Name: "gophercloud-test-volumeType", + }).Extract() + th.AssertNoErr(t, err) + defer func() { + time.Sleep(10000 * time.Millisecond) + err = volumetypes.Delete(client, vt.ID).ExtractErr() + if err != nil { + t.Error(err) + return + } + }() + t.Logf("Created volume type: %+v\n", vt) + + vt, err = volumetypes.Get(client, vt.ID).Extract() + th.AssertNoErr(t, err) + 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 + }) + th.AssertNoErr(t, err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/client_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/client_test.go new file mode 100644 index 00000000000..6e88819d80e --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/client_test.go @@ -0,0 +1,40 @@ +// +build acceptance + +package openstack + +import ( + "os" + "testing" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/openstack" +) + +func TestAuthenticatedClient(t *testing.T) { + // Obtain credentials from the environment. + ao, err := openstack.AuthOptionsFromEnv() + 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.NewObjectStorageV1(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/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/bootfromvolume_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/bootfromvolume_test.go new file mode 100644 index 00000000000..d08abe6a543 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/bootfromvolume_test.go @@ -0,0 +1,50 @@ +// +build acceptance + +package v2 + +import ( + "testing" + + "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/bootfromvolume" + "github.com/rackspace/gophercloud/openstack/compute/v2/servers" + th "github.com/rackspace/gophercloud/testhelper" + "github.com/smashwilson/gophercloud/acceptance/tools" +) + +func TestBootFromVolume(t *testing.T) { + client, err := newClient() + th.AssertNoErr(t, err) + + if testing.Short() { + t.Skip("Skipping test that requires server creation in short mode.") + } + + choices, err := ComputeChoicesFromEnv() + if err != nil { + t.Fatal(err) + } + + name := tools.RandomString("Gophercloud-", 8) + t.Logf("Creating server [%s].", name) + + bd := []bootfromvolume.BlockDevice{ + bootfromvolume.BlockDevice{ + UUID: choices.ImageID, + SourceType: bootfromvolume.Image, + VolumeSize: 10, + }, + } + + serverCreateOpts := servers.CreateOpts{ + Name: name, + FlavorRef: "3", + } + server, err := bootfromvolume.Create(client, bootfromvolume.CreateOptsExt{ + serverCreateOpts, + bd, + }).Extract() + th.AssertNoErr(t, err) + t.Logf("Created server: %+v\n", server) + //defer deleteServer(t, client, server) + t.Logf("Deleting server [%s]...", name) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/compute_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/compute_test.go new file mode 100644 index 00000000000..46eb9ff46ec --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/compute_test.go @@ -0,0 +1,97 @@ +// +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" +) + +func newClient() (*gophercloud.ServiceClient, error) { + ao, err := openstack.AuthOptionsFromEnv() + 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/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/extension_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/extension_test.go new file mode 100644 index 00000000000..1356ffa8998 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/extension_test.go @@ -0,0 +1,47 @@ +// +build acceptance compute extensionss + +package v2 + +import ( + "testing" + + "github.com/rackspace/gophercloud/openstack/compute/v2/extensions" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestListExtensions(t *testing.T) { + client, err := newClient() + th.AssertNoErr(t, err) + + err = extensions.List(client).EachPage(func(page pagination.Page) (bool, error) { + t.Logf("--- Page ---") + + exts, err := extensions.ExtractExtensions(page) + th.AssertNoErr(t, err) + + for i, ext := range exts { + t.Logf("[%02d] name=[%s]\n", i, ext.Name) + t.Logf(" alias=[%s]\n", ext.Alias) + t.Logf(" description=[%s]\n", ext.Description) + } + + return true, nil + }) + th.AssertNoErr(t, err) +} + +func TestGetExtension(t *testing.T) { + client, err := newClient() + th.AssertNoErr(t, err) + + ext, err := extensions.Get(client, "os-admin-actions").Extract() + th.AssertNoErr(t, err) + + t.Logf("Extension details:") + t.Logf(" name=[%s]\n", ext.Name) + t.Logf(" namespace=[%s]\n", ext.Namespace) + t.Logf(" alias=[%s]\n", ext.Alias) + t.Logf(" description=[%s]\n", ext.Description) + t.Logf(" updated=[%s]\n", ext.Updated) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/flavors_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/flavors_test.go new file mode 100644 index 00000000000..9f51b12280b --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/flavors_test.go @@ -0,0 +1,57 @@ +// +build acceptance compute flavors + +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.ListDetail(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/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/images_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/images_test.go new file mode 100644 index 00000000000..ceab22fa76d --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/images_test.go @@ -0,0 +1,37 @@ +// +build acceptance compute images + +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/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/pkg.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/pkg.go new file mode 100644 index 00000000000..bb158c3eecc --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/servers_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/servers_test.go new file mode 100644 index 00000000000..e223c18d16a --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/servers_test.go @@ -0,0 +1,393 @@ +// +build acceptance compute servers + +package v2 + +import ( + "os" + "testing" + + "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/networking/v2/networks" + "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 + }) + + t.Logf("--------\n%d servers listed on %d pages.\n", count, pages) +} + +func networkingClient() (*gophercloud.ServiceClient, error) { + opts, err := openstack.AuthOptionsFromEnv() + if err != nil { + return nil, err + } + + provider, err := openstack.AuthenticatedClient(opts) + if err != nil { + return nil, err + } + + return openstack.NewNetworkV2(provider, gophercloud.EndpointOpts{ + Name: "neutron", + Region: os.Getenv("OS_REGION_NAME"), + }) +} + +func createServer(t *testing.T, client *gophercloud.ServiceClient, choices *ComputeChoices) (*servers.Server, error) { + if testing.Short() { + t.Skip("Skipping test that requires server creation in short mode.") + } + + var network networks.Network + + networkingClient, err := networkingClient() + if err != nil { + t.Fatalf("Unable to create a networking client: %v", err) + } + + pager := networks.List(networkingClient, networks.ListOpts{Name: "public", Limit: 1}) + pager.EachPage(func(page pagination.Page) (bool, error) { + networks, err := networks.ExtractNetworks(page) + if err != nil { + t.Errorf("Failed to extract networks: %v", err) + return false, err + } + + if len(networks) == 0 { + t.Fatalf("No networks to attach to server") + return false, err + } + + network = networks[0] + + return false, nil + }) + + 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, + Networks: []servers.Network{ + servers.Network{UUID: network.ID}, + }, + }).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) + } + + 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) + res := servers.ChangeAdminPassword(client, server.ID, randomPassword) + if res.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) + } + + res := servers.Reboot(client, server.ID, "aldhjflaskhjf") + if res.Err == nil { + t.Fatal("Expected the SDK to provide an ArgumentError here") + } + + t.Logf("Attempting reboot of server %s", server.ID) + res = servers.Reboot(client, server.ID, servers.OSReboot) + if res.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) + + opts := &servers.ResizeOpts{ + FlavorRef: choices.FlavorIDResize, + } + if res := servers.Resize(client, server.ID, opts); res.Err != nil { + t.Fatal(res.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 res := servers.ConfirmResize(client, server.ID); res.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 res := servers.RevertResize(client, server.ID); res.Err != nil { + t.Fatal(err) + } + + if err = waitForStatus(client, server, "ACTIVE"); err != nil { + t.Fatal(err) + } +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v2/extension_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v2/extension_test.go new file mode 100644 index 00000000000..2b4e062aef1 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v2/identity_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v2/identity_test.go new file mode 100644 index 00000000000..feae233b486 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v2/identity_test.go @@ -0,0 +1,47 @@ +// +build acceptance + +package v2 + +import ( + "testing" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/openstack" + th "github.com/rackspace/gophercloud/testhelper" +) + +func v2AuthOptions(t *testing.T) gophercloud.AuthOptions { + // Obtain credentials from the environment. + ao, err := openstack.AuthOptionsFromEnv() + 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/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v2/pkg.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v2/pkg.go new file mode 100644 index 00000000000..5ec3cc8e833 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v2/pkg.go @@ -0,0 +1 @@ +package v2 diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v2/tenant_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v2/tenant_test.go new file mode 100644 index 00000000000..2054598295a --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v2/token_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v2/token_test.go new file mode 100644 index 00000000000..0632a48fc99 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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, tokens2.WrapOptions(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/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v3/endpoint_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v3/endpoint_test.go new file mode 100644 index 00000000000..ea893c2deae --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v3/endpoint_test.go @@ -0,0 +1,111 @@ +// +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) + if client == nil { + return + } + + 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/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v3/identity_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v3/identity_test.go new file mode 100644 index 00000000000..ce643458864 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v3/identity_test.go @@ -0,0 +1,39 @@ +// +build acceptance + +package v3 + +import ( + "testing" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/openstack" + th "github.com/rackspace/gophercloud/testhelper" +) + +func createAuthenticatedClient(t *testing.T) *gophercloud.ServiceClient { + // Obtain credentials from the environment. + ao, err := openstack.AuthOptionsFromEnv() + th.AssertNoErr(t, 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/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v3/pkg.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v3/pkg.go new file mode 100644 index 00000000000..eac3ae96a1f --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v3/pkg.go @@ -0,0 +1 @@ +package v3 diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v3/service_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v3/service_test.go new file mode 100644 index 00000000000..082bd11e742 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v3/token_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v3/token_test.go new file mode 100644 index 00000000000..4342ade03cc --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v3/token_test.go @@ -0,0 +1,42 @@ +// +build acceptance + +package v3 + +import ( + "testing" + + "github.com/rackspace/gophercloud/openstack" + tokens3 "github.com/rackspace/gophercloud/openstack/identity/v3/tokens" +) + +func TestGetToken(t *testing.T) { + // Obtain credentials from the environment. + ao, err := openstack.AuthOptionsFromEnv() + 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/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/apiversion_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/apiversion_test.go new file mode 100644 index 00000000000..99e1d011875 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/common.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/common.go new file mode 100644 index 00000000000..1efac2c081a --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/common.go @@ -0,0 +1,39 @@ +package v2 + +import ( + "os" + "testing" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/openstack" + th "github.com/rackspace/gophercloud/testhelper" +) + +var Client *gophercloud.ServiceClient + +func NewClient() (*gophercloud.ServiceClient, error) { + opts, err := openstack.AuthOptionsFromEnv() + if err != nil { + return nil, err + } + + provider, err := openstack.AuthenticatedClient(opts) + if err != nil { + return nil, err + } + + return openstack.NewNetworkV2(provider, gophercloud.EndpointOpts{ + Name: "neutron", + Region: os.Getenv("OS_REGION_NAME"), + }) +} + +func Setup(t *testing.T) { + client, err := NewClient() + th.AssertNoErr(t, err) + Client = client +} + +func Teardown() { + Client = nil +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extension_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extension_test.go new file mode 100644 index 00000000000..edcbba4fd15 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extensions/layer3_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extensions/layer3_test.go new file mode 100644 index 00000000000..63e0be39d7b --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extensions/lbaas/common.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extensions/lbaas/common.go new file mode 100644 index 00000000000..27dfe5f8b7e --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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: 10, + Timeout: 10, + MaxRetries: 3, + Type: monitors.TypeHTTP, + ExpectedCodes: "200", + URLPath: "/login", + HTTPMethod: "GET", + }).Extract() + + th.AssertNoErr(t, err) + + t.Logf("Created monitor ID [%s]", m.ID) + + return m.ID +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extensions/lbaas/member_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extensions/lbaas/member_test.go new file mode 100644 index 00000000000..9b60582d140 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extensions/lbaas/monitor_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extensions/lbaas/monitor_test.go new file mode 100644 index 00000000000..9056fff671b --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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: 10, 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/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extensions/lbaas/pkg.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extensions/lbaas/pkg.go new file mode 100644 index 00000000000..f5a7df7b751 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extensions/lbaas/pkg.go @@ -0,0 +1 @@ +package lbaas diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extensions/lbaas/pool_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extensions/lbaas/pool_test.go new file mode 100644 index 00000000000..81940649c53 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extensions/lbaas/vip_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extensions/lbaas/vip_test.go new file mode 100644 index 00000000000..c8dff2d93ff --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extensions/pkg.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extensions/pkg.go new file mode 100644 index 00000000000..aeec0fa756e --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extensions/pkg.go @@ -0,0 +1 @@ +package extensions diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extensions/provider_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extensions/provider_test.go new file mode 100644 index 00000000000..f10c9d9bd12 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extensions/security_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extensions/security_test.go new file mode 100644 index 00000000000..7d75292f0de --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extensions/security_test.go @@ -0,0 +1,171 @@ +// +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) + + // delete security group + defer deleteSecGroup(t, groupID) + + // list security group + listSecGroups(t) + + // get security group + getSecGroup(t, groupID) + + // create port with security group + networkID, portID := createPort(t, groupID) + + // teardown + defer deleteNetwork(t, networkID) + + // delete port + defer deletePort(t, portID) +} + +func TestSecurityGroupRules(t *testing.T) { + base.Setup(t) + defer base.Teardown() + + // create security group + groupID := createSecGroup(t) + + defer deleteSecGroup(t, groupID) + + // create security group rule + ruleID := createSecRule(t, groupID) + + // delete security group rule + defer deleteSecRule(t, ruleID) + + // list security group rule + listSecRules(t) + + // get security group rule + getSecRule(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/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/network_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/network_test.go new file mode 100644 index 00000000000..be8a3a195a4 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/pkg.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/pkg.go new file mode 100644 index 00000000000..5ec3cc8e833 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/pkg.go @@ -0,0 +1 @@ +package v2 diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/port_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/port_test.go new file mode 100644 index 00000000000..7f22dbd5cd6 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/subnet_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/subnet_test.go new file mode 100644 index 00000000000..097a303edee --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/objectstorage/v1/accounts_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/objectstorage/v1/accounts_test.go new file mode 100644 index 00000000000..f7c01a7c118 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/objectstorage/v1/accounts_test.go @@ -0,0 +1,44 @@ +// +build acceptance + +package v1 + +import ( + "strings" + "testing" + + "github.com/rackspace/gophercloud/openstack/objectstorage/v1/accounts" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestAccounts(t *testing.T) { + // Create a provider client for making the HTTP requests. + // See common.go in this directory for more information. + client := newClient(t) + + // Update an account's metadata. + updateres := accounts.Update(client, accounts.UpdateOpts{Metadata: metadata}) + th.AssertNoErr(t, updateres.Err) + + // Defer the deletion of the metadata set above. + defer func() { + tempMap := make(map[string]string) + for k := range metadata { + tempMap[k] = "" + } + updateres = accounts.Update(client, accounts.UpdateOpts{Metadata: tempMap}) + th.AssertNoErr(t, updateres.Err) + }() + + // Retrieve account metadata. + getres := accounts.Get(client, nil) + th.AssertNoErr(t, getres.Err) + // Extract the custom metadata from the 'Get' response. + am, err := getres.ExtractMetadata() + th.AssertNoErr(t, err) + for k := range metadata { + if am[k] != metadata[strings.Title(k)] { + t.Errorf("Expected custom metadata with key: %s", k) + return + } + } +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/objectstorage/v1/common.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/objectstorage/v1/common.go new file mode 100644 index 00000000000..1eac681b571 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/objectstorage/v1/common.go @@ -0,0 +1,28 @@ +// +build acceptance + +package v1 + +import ( + "os" + "testing" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/openstack" + th "github.com/rackspace/gophercloud/testhelper" +) + +var metadata = map[string]string{"gopher": "cloud"} + +func newClient(t *testing.T) *gophercloud.ServiceClient { + ao, err := openstack.AuthOptionsFromEnv() + th.AssertNoErr(t, err) + + client, err := openstack.AuthenticatedClient(ao) + th.AssertNoErr(t, err) + + c, err := openstack.NewObjectStorageV1(client, gophercloud.EndpointOpts{ + Region: os.Getenv("OS_REGION_NAME"), + }) + th.AssertNoErr(t, err) + return c +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/objectstorage/v1/containers_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/objectstorage/v1/containers_test.go new file mode 100644 index 00000000000..d6832f1914c --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/objectstorage/v1/containers_test.go @@ -0,0 +1,89 @@ +// +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" + th "github.com/rackspace/gophercloud/testhelper" +) + +// 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 := newClient(t) + + // 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++ { + res := containers.Create(client, cNames[i], nil) + th.AssertNoErr(t, res.Err) + } + // Delete the numContainers containers after function completion. + defer func() { + for i := 0; i < len(cNames); i++ { + res := containers.Delete(client, cNames[i]) + th.AssertNoErr(t, res.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) + th.AssertNoErr(t, err) + + for _, n := range containerList { + t.Logf("Container: Name [%s] Count [%d] Bytes [%d]", + n.Name, n.Count, n.Bytes) + } + + return true, nil + }) + th.AssertNoErr(t, 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) + th.AssertNoErr(t, err) + for _, n := range containerList { + t.Logf("Container: Name [%s]", n) + } + + return true, nil + }) + th.AssertNoErr(t, err) + + // Update one of the numContainer container metadata. + updateres := containers.Update(client, cNames[0], &containers.UpdateOpts{Metadata: metadata}) + th.AssertNoErr(t, updateres.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] = "" + } + res := containers.Update(client, cNames[0], &containers.UpdateOpts{Metadata: tempMap}) + th.AssertNoErr(t, res.Err) + }() + + // Retrieve a container's metadata. + cm, err := containers.Get(client, cNames[0]).ExtractMetadata() + th.AssertNoErr(t, err) + for k := range metadata { + if cm[k] != metadata[strings.Title(k)] { + t.Errorf("Expected custom metadata with key: %s", k) + } + } +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/objectstorage/v1/objects_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/objectstorage/v1/objects_test.go new file mode 100644 index 00000000000..a8de338c3dd --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/objectstorage/v1/objects_test.go @@ -0,0 +1,119 @@ +// +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" + th "github.com/rackspace/gophercloud/testhelper" +) + +// 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 := newClient(t) + + // 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) + header, err := containers.Create(client, cName, nil).ExtractHeader() + th.AssertNoErr(t, err) + t.Logf("Create object headers: %+v\n", header) + + // Defer deletion of the container until after testing. + defer func() { + res := containers.Delete(client, cName) + th.AssertNoErr(t, res.Err) + }() + + // 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))) + res := objects.Create(client, cName, oNames[i], oContents[i], nil) + th.AssertNoErr(t, res.Err) + } + // Delete the objects after testing. + defer func() { + for i := 0; i < numObjects; i++ { + res := objects.Delete(client, cName, oNames[i], nil) + th.AssertNoErr(t, res.Err) + } + }() + + 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) + th.AssertNoErr(t, err) + ons = append(ons, names...) + + return true, nil + }) + th.AssertNoErr(t, err) + th.AssertEquals(t, len(ons), len(oNames)) + + 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) + th.AssertNoErr(t, err) + + ois = append(ois, info...) + + return true, nil + }) + th.AssertNoErr(t, err) + th.AssertEquals(t, len(ois), len(oNames)) + + // Copy the contents of one object to another. + copyres := objects.Copy(client, cName, oNames[0], &objects.CopyOpts{Destination: cName + "/" + oNames[1]}) + th.AssertNoErr(t, copyres.Err) + + // Download one of the objects that was created above. + o1Content, err := objects.Download(client, cName, oNames[0], nil).ExtractContent() + th.AssertNoErr(t, err) + + // Download the another object that was create above. + o2Content, err := objects.Download(client, cName, oNames[1], nil).ExtractContent() + th.AssertNoErr(t, err) + + // Compare the two object's contents to test that the copy worked. + th.AssertEquals(t, string(o2Content), string(o1Content)) + + // Update an object's metadata. + updateres := objects.Update(client, cName, oNames[0], &objects.UpdateOpts{Metadata: metadata}) + th.AssertNoErr(t, updateres.Err) + + // Delete the object's metadata after testing. + defer func() { + tempMap := make(map[string]string) + for k := range metadata { + tempMap[k] = "" + } + res := objects.Update(client, cName, oNames[0], &objects.UpdateOpts{Metadata: tempMap}) + th.AssertNoErr(t, res.Err) + }() + + // Retrieve an object's metadata. + om, err := objects.Get(client, cName, oNames[0], nil).ExtractMetadata() + th.AssertNoErr(t, err) + for k := range metadata { + if om[k] != metadata[strings.Title(k)] { + t.Errorf("Expected custom metadata with key: %s", k) + return + } + } +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/pkg.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/pkg.go new file mode 100644 index 00000000000..3a8ecdb100b --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/pkg.go @@ -0,0 +1,4 @@ +// +build acceptance + +package openstack + diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/blockstorage/v1/common.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/blockstorage/v1/common.go new file mode 100644 index 00000000000..e9fdd992059 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/blockstorage/v1/common.go @@ -0,0 +1,38 @@ +// +build acceptance + +package v1 + +import ( + "os" + "testing" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/acceptance/tools" + "github.com/rackspace/gophercloud/rackspace" + th "github.com/rackspace/gophercloud/testhelper" +) + +func newClient() (*gophercloud.ServiceClient, error) { + opts, err := rackspace.AuthOptionsFromEnv() + if err != nil { + return nil, err + } + opts = tools.OnlyRS(opts) + region := os.Getenv("RS_REGION") + + provider, err := rackspace.AuthenticatedClient(opts) + if err != nil { + return nil, err + } + + return rackspace.NewBlockStorageV1(provider, gophercloud.EndpointOpts{ + Region: region, + }) +} + +func setup(t *testing.T) *gophercloud.ServiceClient { + client, err := newClient() + th.AssertNoErr(t, err) + + return client +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/blockstorage/v1/snapshot_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/blockstorage/v1/snapshot_test.go new file mode 100644 index 00000000000..25b2cfeeeb2 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/blockstorage/v1/snapshot_test.go @@ -0,0 +1,82 @@ +// +build acceptance blockstorage snapshots + +package v1 + +import ( + "testing" + "time" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" + "github.com/rackspace/gophercloud/rackspace/blockstorage/v1/snapshots" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestSnapshots(t *testing.T) { + client := setup(t) + volID := testVolumeCreate(t, client) + + t.Log("Creating snapshots") + s := testSnapshotCreate(t, client, volID) + id := s.ID + + t.Log("Listing snapshots") + testSnapshotList(t, client) + + t.Logf("Getting snapshot %s", id) + testSnapshotGet(t, client, id) + + t.Logf("Updating snapshot %s", id) + testSnapshotUpdate(t, client, id) + + t.Logf("Deleting snapshot %s", id) + testSnapshotDelete(t, client, id) + s.WaitUntilDeleted(client, -1) + + t.Logf("Deleting volume %s", volID) + testVolumeDelete(t, client, volID) +} + +func testSnapshotCreate(t *testing.T, client *gophercloud.ServiceClient, volID string) *snapshots.Snapshot { + opts := snapshots.CreateOpts{VolumeID: volID, Name: "snapshot-001"} + s, err := snapshots.Create(client, opts).Extract() + th.AssertNoErr(t, err) + t.Logf("Created snapshot %s", s.ID) + + t.Logf("Waiting for new snapshot to become available...") + start := time.Now().Second() + s.WaitUntilComplete(client, -1) + t.Logf("Snapshot completed after %ds", time.Now().Second()-start) + + return s +} + +func testSnapshotList(t *testing.T, client *gophercloud.ServiceClient) { + snapshots.List(client).EachPage(func(page pagination.Page) (bool, error) { + sList, err := snapshots.ExtractSnapshots(page) + th.AssertNoErr(t, err) + + for _, s := range sList { + t.Logf("Snapshot: ID [%s] Name [%s] Volume ID [%s] Progress [%s] Created [%s]", + s.ID, s.Name, s.VolumeID, s.Progress, s.CreatedAt) + } + + return true, nil + }) +} + +func testSnapshotGet(t *testing.T, client *gophercloud.ServiceClient, id string) { + _, err := snapshots.Get(client, id).Extract() + th.AssertNoErr(t, err) +} + +func testSnapshotUpdate(t *testing.T, client *gophercloud.ServiceClient, id string) { + _, err := snapshots.Update(client, id, snapshots.UpdateOpts{Name: "new_name"}).Extract() + th.AssertNoErr(t, err) +} + +func testSnapshotDelete(t *testing.T, client *gophercloud.ServiceClient, id string) { + res := snapshots.Delete(client, id) + th.AssertNoErr(t, res.Err) + t.Logf("Deleted snapshot %s", id) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/blockstorage/v1/volume_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/blockstorage/v1/volume_test.go new file mode 100644 index 00000000000..f86f9adedd3 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/blockstorage/v1/volume_test.go @@ -0,0 +1,71 @@ +// +build acceptance blockstorage volumes + +package v1 + +import ( + "testing" + + "github.com/rackspace/gophercloud" + os "github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes" + "github.com/rackspace/gophercloud/pagination" + "github.com/rackspace/gophercloud/rackspace/blockstorage/v1/volumes" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestVolumes(t *testing.T) { + client := setup(t) + + t.Logf("Listing volumes") + testVolumeList(t, client) + + t.Logf("Creating volume") + volumeID := testVolumeCreate(t, client) + + t.Logf("Getting volume %s", volumeID) + testVolumeGet(t, client, volumeID) + + t.Logf("Updating volume %s", volumeID) + testVolumeUpdate(t, client, volumeID) + + t.Logf("Deleting volume %s", volumeID) + testVolumeDelete(t, client, volumeID) +} + +func testVolumeList(t *testing.T, client *gophercloud.ServiceClient) { + volumes.List(client).EachPage(func(page pagination.Page) (bool, error) { + vList, err := volumes.ExtractVolumes(page) + th.AssertNoErr(t, err) + + for _, v := range vList { + t.Logf("Volume: ID [%s] Name [%s] Type [%s] Created [%s]", v.ID, v.Name, + v.VolumeType, v.CreatedAt) + } + + return true, nil + }) +} + +func testVolumeCreate(t *testing.T, client *gophercloud.ServiceClient) string { + vol, err := volumes.Create(client, os.CreateOpts{Size: 75}).Extract() + th.AssertNoErr(t, err) + t.Logf("Created volume: ID [%s] Size [%s]", vol.ID, vol.Size) + return vol.ID +} + +func testVolumeGet(t *testing.T, client *gophercloud.ServiceClient, id string) { + vol, err := volumes.Get(client, id).Extract() + th.AssertNoErr(t, err) + t.Logf("Created volume: ID [%s] Size [%s]", vol.ID, vol.Size) +} + +func testVolumeUpdate(t *testing.T, client *gophercloud.ServiceClient, id string) { + vol, err := volumes.Update(client, id, volumes.UpdateOpts{Name: "new_name"}).Extract() + th.AssertNoErr(t, err) + t.Logf("Created volume: ID [%s] Name [%s]", vol.ID, vol.Name) +} + +func testVolumeDelete(t *testing.T, client *gophercloud.ServiceClient, id string) { + res := volumes.Delete(client, id) + th.AssertNoErr(t, res.Err) + t.Logf("Deleted volume %s", id) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/blockstorage/v1/volume_type_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/blockstorage/v1/volume_type_test.go new file mode 100644 index 00000000000..716f2b9fd5b --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/blockstorage/v1/volume_type_test.go @@ -0,0 +1,46 @@ +// +build acceptance blockstorage volumetypes + +package v1 + +import ( + "testing" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" + "github.com/rackspace/gophercloud/rackspace/blockstorage/v1/volumetypes" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestAll(t *testing.T) { + client := setup(t) + + t.Logf("Listing volume types") + id := testList(t, client) + + t.Logf("Getting volume type %s", id) + testGet(t, client, id) +} + +func testList(t *testing.T, client *gophercloud.ServiceClient) string { + var lastID string + + volumetypes.List(client).EachPage(func(page pagination.Page) (bool, error) { + typeList, err := volumetypes.ExtractVolumeTypes(page) + th.AssertNoErr(t, err) + + for _, vt := range typeList { + t.Logf("Volume type: ID [%s] Name [%s]", vt.ID, vt.Name) + lastID = vt.ID + } + + return true, nil + }) + + return lastID +} + +func testGet(t *testing.T, client *gophercloud.ServiceClient, id string) { + vt, err := volumetypes.Get(client, id).Extract() + th.AssertNoErr(t, err) + t.Logf("Volume: ID [%s] Name [%s]", vt.ID, vt.Name) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/client_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/client_test.go new file mode 100644 index 00000000000..61214c047a9 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/client_test.go @@ -0,0 +1,28 @@ +// +build acceptance + +package rackspace + +import ( + "testing" + + "github.com/rackspace/gophercloud/acceptance/tools" + "github.com/rackspace/gophercloud/rackspace" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestAuthenticatedClient(t *testing.T) { + // Obtain credentials from the environment. + ao, err := rackspace.AuthOptionsFromEnv() + th.AssertNoErr(t, err) + + client, err := rackspace.AuthenticatedClient(tools.OnlyRS(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/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/compute/v2/bootfromvolume_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/compute/v2/bootfromvolume_test.go new file mode 100644 index 00000000000..010bf4279cb --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/compute/v2/bootfromvolume_test.go @@ -0,0 +1,46 @@ +// +build acceptance + +package v2 + +import ( + "testing" + + osBFV "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/bootfromvolume" + "github.com/rackspace/gophercloud/rackspace/compute/v2/bootfromvolume" + "github.com/rackspace/gophercloud/rackspace/compute/v2/servers" + th "github.com/rackspace/gophercloud/testhelper" + "github.com/smashwilson/gophercloud/acceptance/tools" +) + +func TestBootFromVolume(t *testing.T) { + client, err := newClient() + th.AssertNoErr(t, err) + + if testing.Short() { + t.Skip("Skipping test that requires server creation in short mode.") + } + + options, err := optionsFromEnv() + th.AssertNoErr(t, err) + + name := tools.RandomString("Gophercloud-", 8) + t.Logf("Creating server [%s].", name) + + bd := []osBFV.BlockDevice{ + osBFV.BlockDevice{ + UUID: options.imageID, + SourceType: osBFV.Image, + VolumeSize: 10, + }, + } + + server, err := bootfromvolume.Create(client, servers.CreateOpts{ + Name: name, + FlavorRef: "performance1-1", + BlockDevice: bd, + }).Extract() + th.AssertNoErr(t, err) + t.Logf("Created server: %+v\n", server) + //defer deleteServer(t, client, server) + t.Logf("Deleting server [%s]...", name) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/compute/v2/compute_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/compute/v2/compute_test.go new file mode 100644 index 00000000000..3ca6dc9b6c8 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/compute/v2/compute_test.go @@ -0,0 +1,60 @@ +// +build acceptance + +package v2 + +import ( + "errors" + "os" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/acceptance/tools" + "github.com/rackspace/gophercloud/rackspace" +) + +func newClient() (*gophercloud.ServiceClient, error) { + // Obtain credentials from the environment. + options, err := rackspace.AuthOptionsFromEnv() + if err != nil { + return nil, err + } + options = tools.OnlyRS(options) + region := os.Getenv("RS_REGION") + + if options.Username == "" { + return nil, errors.New("Please provide a Rackspace username as RS_USERNAME.") + } + if options.APIKey == "" { + return nil, errors.New("Please provide a Rackspace API key as RS_API_KEY.") + } + if region == "" { + return nil, errors.New("Please provide a Rackspace region as RS_REGION.") + } + + client, err := rackspace.AuthenticatedClient(options) + if err != nil { + return nil, err + } + + return rackspace.NewComputeV2(client, gophercloud.EndpointOpts{ + Region: region, + }) +} + +type serverOpts struct { + imageID string + flavorID string +} + +func optionsFromEnv() (*serverOpts, error) { + options := &serverOpts{ + imageID: os.Getenv("RS_IMAGE_ID"), + flavorID: os.Getenv("RS_FLAVOR_ID"), + } + if options.imageID == "" { + return nil, errors.New("Please provide a valid Rackspace image ID as RS_IMAGE_ID") + } + if options.flavorID == "" { + return nil, errors.New("Please provide a valid Rackspace flavor ID as RS_FLAVOR_ID") + } + return options, nil +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/compute/v2/flavors_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/compute/v2/flavors_test.go new file mode 100644 index 00000000000..4618ecc8a9b --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/compute/v2/flavors_test.go @@ -0,0 +1,61 @@ +// +build acceptance + +package v2 + +import ( + "testing" + + "github.com/rackspace/gophercloud/pagination" + "github.com/rackspace/gophercloud/rackspace/compute/v2/flavors" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestListFlavors(t *testing.T) { + client, err := newClient() + th.AssertNoErr(t, err) + + count := 0 + err = flavors.ListDetail(client, nil).EachPage(func(page pagination.Page) (bool, error) { + count++ + t.Logf("-- Page %0d --", count) + + fs, err := flavors.ExtractFlavors(page) + th.AssertNoErr(t, err) + + for i, flavor := range fs { + t.Logf("[%02d] id=[%s]", i, flavor.ID) + t.Logf(" name=[%s]", flavor.Name) + t.Logf(" disk=[%d]", flavor.Disk) + t.Logf(" RAM=[%d]", flavor.RAM) + t.Logf(" rxtx_factor=[%f]", flavor.RxTxFactor) + t.Logf(" swap=[%d]", flavor.Swap) + t.Logf(" VCPUs=[%d]", flavor.VCPUs) + } + + return true, nil + }) + th.AssertNoErr(t, err) + if count == 0 { + t.Errorf("No flavors listed!") + } +} + +func TestGetFlavor(t *testing.T) { + client, err := newClient() + th.AssertNoErr(t, err) + + options, err := optionsFromEnv() + th.AssertNoErr(t, err) + + flavor, err := flavors.Get(client, options.flavorID).Extract() + th.AssertNoErr(t, err) + + t.Logf("Requested flavor:") + t.Logf(" id=[%s]", flavor.ID) + t.Logf(" name=[%s]", flavor.Name) + t.Logf(" disk=[%d]", flavor.Disk) + t.Logf(" RAM=[%d]", flavor.RAM) + t.Logf(" rxtx_factor=[%f]", flavor.RxTxFactor) + t.Logf(" swap=[%d]", flavor.Swap) + t.Logf(" VCPUs=[%d]", flavor.VCPUs) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/compute/v2/images_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/compute/v2/images_test.go new file mode 100644 index 00000000000..5e36c2e4545 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/compute/v2/images_test.go @@ -0,0 +1,63 @@ +// +build acceptance + +package v2 + +import ( + "testing" + + "github.com/rackspace/gophercloud/pagination" + "github.com/rackspace/gophercloud/rackspace/compute/v2/images" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestListImages(t *testing.T) { + client, err := newClient() + th.AssertNoErr(t, err) + + count := 0 + err = images.ListDetail(client, nil).EachPage(func(page pagination.Page) (bool, error) { + count++ + t.Logf("-- Page %02d --", count) + + is, err := images.ExtractImages(page) + th.AssertNoErr(t, err) + + for i, image := range is { + t.Logf("[%02d] id=[%s]", i, image.ID) + t.Logf(" name=[%s]", image.Name) + t.Logf(" created=[%s]", image.Created) + t.Logf(" updated=[%s]", image.Updated) + t.Logf(" min disk=[%d]", image.MinDisk) + t.Logf(" min RAM=[%d]", image.MinRAM) + t.Logf(" progress=[%d]", image.Progress) + t.Logf(" status=[%s]", image.Status) + } + + return true, nil + }) + th.AssertNoErr(t, err) + if count < 1 { + t.Errorf("Expected at least one page of images.") + } +} + +func TestGetImage(t *testing.T) { + client, err := newClient() + th.AssertNoErr(t, err) + + options, err := optionsFromEnv() + th.AssertNoErr(t, err) + + image, err := images.Get(client, options.imageID).Extract() + th.AssertNoErr(t, err) + + t.Logf("Requested image:") + t.Logf(" id=[%s]", image.ID) + t.Logf(" name=[%s]", image.Name) + t.Logf(" created=[%s]", image.Created) + t.Logf(" updated=[%s]", image.Updated) + t.Logf(" min disk=[%d]", image.MinDisk) + t.Logf(" min RAM=[%d]", image.MinRAM) + t.Logf(" progress=[%d]", image.Progress) + t.Logf(" status=[%s]", image.Status) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/compute/v2/keypairs_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/compute/v2/keypairs_test.go new file mode 100644 index 00000000000..9bd6eb42848 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/compute/v2/keypairs_test.go @@ -0,0 +1,87 @@ +// +build acceptance rackspace + +package v2 + +import ( + "testing" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/acceptance/tools" + os "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs" + "github.com/rackspace/gophercloud/pagination" + "github.com/rackspace/gophercloud/rackspace/compute/v2/keypairs" + th "github.com/rackspace/gophercloud/testhelper" +) + +func deleteKeyPair(t *testing.T, client *gophercloud.ServiceClient, name string) { + err := keypairs.Delete(client, name).ExtractErr() + th.AssertNoErr(t, err) + t.Logf("Successfully deleted key [%s].", name) +} + +func TestCreateKeyPair(t *testing.T) { + client, err := newClient() + th.AssertNoErr(t, err) + + name := tools.RandomString("createdkey-", 8) + k, err := keypairs.Create(client, os.CreateOpts{Name: name}).Extract() + th.AssertNoErr(t, err) + defer deleteKeyPair(t, client, name) + + t.Logf("Created a new keypair:") + t.Logf(" name=[%s]", k.Name) + t.Logf(" fingerprint=[%s]", k.Fingerprint) + t.Logf(" publickey=[%s]", tools.Elide(k.PublicKey)) + t.Logf(" privatekey=[%s]", tools.Elide(k.PrivateKey)) + t.Logf(" userid=[%s]", k.UserID) +} + +func TestImportKeyPair(t *testing.T) { + client, err := newClient() + th.AssertNoErr(t, err) + + name := tools.RandomString("importedkey-", 8) + pubkey := "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDlIQ3r+zd97kb9Hzmujd3V6pbO53eb3Go4q2E8iqVGWQfZTrFdL9KACJnqJIm9HmncfRkUTxE37hqeGCCv8uD+ZPmPiZG2E60OX1mGDjbbzAyReRwYWXgXHopggZTLak5k4mwZYaxwaufbVBDRn847e01lZnaXaszEToLM37NLw+uz29sl3TwYy2R0RGHPwPc160aWmdLjSyd1Nd4c9pvvOP/EoEuBjIC6NJJwg2Rvg9sjjx9jYj0QUgc8CqKLN25oMZ69kNJzlFylKRUoeeVr89txlR59yehJWk6Uw6lYFTdJmcmQOFVAJ12RMmS1hLWCM8UzAgtw+EDa0eqBxBDl smash@winter" + + k, err := keypairs.Create(client, os.CreateOpts{ + Name: name, + PublicKey: pubkey, + }).Extract() + th.AssertNoErr(t, err) + defer deleteKeyPair(t, client, name) + + th.CheckEquals(t, pubkey, k.PublicKey) + th.CheckEquals(t, "", k.PrivateKey) + + t.Logf("Imported an existing keypair:") + t.Logf(" name=[%s]", k.Name) + t.Logf(" fingerprint=[%s]", k.Fingerprint) + t.Logf(" publickey=[%s]", tools.Elide(k.PublicKey)) + t.Logf(" privatekey=[%s]", tools.Elide(k.PrivateKey)) + t.Logf(" userid=[%s]", k.UserID) +} + +func TestListKeyPairs(t *testing.T) { + client, err := newClient() + th.AssertNoErr(t, err) + + count := 0 + err = keypairs.List(client).EachPage(func(page pagination.Page) (bool, error) { + count++ + t.Logf("--- %02d ---", count) + + ks, err := keypairs.ExtractKeyPairs(page) + th.AssertNoErr(t, err) + + for i, keypair := range ks { + t.Logf("[%02d] name=[%s]", i, keypair.Name) + t.Logf(" fingerprint=[%s]", keypair.Fingerprint) + t.Logf(" publickey=[%s]", tools.Elide(keypair.PublicKey)) + t.Logf(" privatekey=[%s]", tools.Elide(keypair.PrivateKey)) + t.Logf(" userid=[%s]", keypair.UserID) + } + + return true, nil + }) + th.AssertNoErr(t, err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/compute/v2/networks_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/compute/v2/networks_test.go new file mode 100644 index 00000000000..e8fc4d37dfc --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/compute/v2/networks_test.go @@ -0,0 +1,53 @@ +// +build acceptance rackspace + +package v2 + +import ( + "testing" + + "github.com/rackspace/gophercloud/pagination" + "github.com/rackspace/gophercloud/rackspace/compute/v2/networks" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestNetworks(t *testing.T) { + client, err := newClient() + th.AssertNoErr(t, err) + + // Create a network + n, err := networks.Create(client, networks.CreateOpts{Label: "sample_network", CIDR: "172.20.0.0/24"}).Extract() + th.AssertNoErr(t, err) + t.Logf("Created network: %+v\n", n) + defer networks.Delete(client, n.ID) + th.AssertEquals(t, n.Label, "sample_network") + th.AssertEquals(t, n.CIDR, "172.20.0.0/24") + networkID := n.ID + + // List networks + pager := networks.List(client) + 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] Label [%s] CIDR [%s]", + n.ID, n.Label, n.CIDR) + } + + 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() + t.Logf("Retrieved Network: %+v\n", n) + th.AssertNoErr(t, err) + th.AssertEquals(t, n.CIDR, "172.20.0.0/24") + th.AssertEquals(t, n.Label, "sample_network") + th.AssertEquals(t, n.ID, networkID) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/compute/v2/pkg.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/compute/v2/pkg.go new file mode 100644 index 00000000000..5ec3cc8e833 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/compute/v2/pkg.go @@ -0,0 +1 @@ +package v2 diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/compute/v2/servers_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/compute/v2/servers_test.go new file mode 100644 index 00000000000..511f0a96acd --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/compute/v2/servers_test.go @@ -0,0 +1,199 @@ +// +build acceptance + +package v2 + +import ( + "testing" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/acceptance/tools" + "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/diskconfig" + oskey "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs" + os "github.com/rackspace/gophercloud/openstack/compute/v2/servers" + "github.com/rackspace/gophercloud/pagination" + "github.com/rackspace/gophercloud/rackspace/compute/v2/keypairs" + "github.com/rackspace/gophercloud/rackspace/compute/v2/servers" + th "github.com/rackspace/gophercloud/testhelper" +) + +func createServerKeyPair(t *testing.T, client *gophercloud.ServiceClient) *oskey.KeyPair { + name := tools.RandomString("importedkey-", 8) + pubkey := "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDlIQ3r+zd97kb9Hzmujd3V6pbO53eb3Go4q2E8iqVGWQfZTrFdL9KACJnqJIm9HmncfRkUTxE37hqeGCCv8uD+ZPmPiZG2E60OX1mGDjbbzAyReRwYWXgXHopggZTLak5k4mwZYaxwaufbVBDRn847e01lZnaXaszEToLM37NLw+uz29sl3TwYy2R0RGHPwPc160aWmdLjSyd1Nd4c9pvvOP/EoEuBjIC6NJJwg2Rvg9sjjx9jYj0QUgc8CqKLN25oMZ69kNJzlFylKRUoeeVr89txlR59yehJWk6Uw6lYFTdJmcmQOFVAJ12RMmS1hLWCM8UzAgtw+EDa0eqBxBDl smash@winter" + + k, err := keypairs.Create(client, oskey.CreateOpts{ + Name: name, + PublicKey: pubkey, + }).Extract() + th.AssertNoErr(t, err) + + return k +} + +func createServer(t *testing.T, client *gophercloud.ServiceClient, keyName string) *os.Server { + if testing.Short() { + t.Skip("Skipping test that requires server creation in short mode.") + } + + options, err := optionsFromEnv() + th.AssertNoErr(t, err) + + name := tools.RandomString("Gophercloud-", 8) + + opts := &servers.CreateOpts{ + Name: name, + ImageRef: options.imageID, + FlavorRef: options.flavorID, + DiskConfig: diskconfig.Manual, + } + + if keyName != "" { + opts.KeyPair = keyName + } + + t.Logf("Creating server [%s].", name) + s, err := servers.Create(client, opts).Extract() + th.AssertNoErr(t, err) + t.Logf("Creating server.") + + err = servers.WaitForStatus(client, s.ID, "ACTIVE", 300) + th.AssertNoErr(t, err) + t.Logf("Server created successfully.") + + return s +} + +func logServer(t *testing.T, server *os.Server, index int) { + if index == -1 { + t.Logf(" id=[%s]", server.ID) + } else { + t.Logf("[%02d] id=[%s]", index, server.ID) + } + t.Logf(" name=[%s]", server.Name) + t.Logf(" tenant ID=[%s]", server.TenantID) + t.Logf(" user ID=[%s]", server.UserID) + t.Logf(" updated=[%s]", server.Updated) + t.Logf(" created=[%s]", server.Created) + t.Logf(" host ID=[%s]", server.HostID) + t.Logf(" access IPv4=[%s]", server.AccessIPv4) + t.Logf(" access IPv6=[%s]", server.AccessIPv6) + t.Logf(" image=[%v]", server.Image) + t.Logf(" flavor=[%v]", server.Flavor) + t.Logf(" addresses=[%v]", server.Addresses) + t.Logf(" metadata=[%v]", server.Metadata) + t.Logf(" links=[%v]", server.Links) + t.Logf(" keyname=[%s]", server.KeyName) + t.Logf(" admin password=[%s]", server.AdminPass) + t.Logf(" status=[%s]", server.Status) + t.Logf(" progress=[%d]", server.Progress) +} + +func getServer(t *testing.T, client *gophercloud.ServiceClient, server *os.Server) { + t.Logf("> servers.Get") + + details, err := servers.Get(client, server.ID).Extract() + th.AssertNoErr(t, err) + logServer(t, details, -1) +} + +func listServers(t *testing.T, client *gophercloud.ServiceClient) { + t.Logf("> servers.List") + + count := 0 + err := servers.List(client, nil).EachPage(func(page pagination.Page) (bool, error) { + count++ + t.Logf("--- Page %02d ---", count) + + s, err := servers.ExtractServers(page) + th.AssertNoErr(t, err) + for index, server := range s { + logServer(t, &server, index) + } + + return true, nil + }) + th.AssertNoErr(t, err) +} + +func changeAdminPassword(t *testing.T, client *gophercloud.ServiceClient, server *os.Server) { + t.Logf("> servers.ChangeAdminPassword") + + original := server.AdminPass + + t.Logf("Changing server password.") + err := servers.ChangeAdminPassword(client, server.ID, tools.MakeNewPassword(original)).Extract() + th.AssertNoErr(t, err) + + err = servers.WaitForStatus(client, server.ID, "ACTIVE", 300) + th.AssertNoErr(t, err) + t.Logf("Password changed successfully.") +} + +func rebootServer(t *testing.T, client *gophercloud.ServiceClient, server *os.Server) { + t.Logf("> servers.Reboot") + + err := servers.Reboot(client, server.ID, os.HardReboot).Extract() + th.AssertNoErr(t, err) + + err = servers.WaitForStatus(client, server.ID, "ACTIVE", 300) + th.AssertNoErr(t, err) + + t.Logf("Server successfully rebooted.") +} + +func rebuildServer(t *testing.T, client *gophercloud.ServiceClient, server *os.Server) { + t.Logf("> servers.Rebuild") + + options, err := optionsFromEnv() + th.AssertNoErr(t, err) + + opts := servers.RebuildOpts{ + Name: tools.RandomString("RenamedGopher", 16), + AdminPass: tools.MakeNewPassword(server.AdminPass), + ImageID: options.imageID, + DiskConfig: diskconfig.Manual, + } + after, err := servers.Rebuild(client, server.ID, opts).Extract() + th.AssertNoErr(t, err) + th.CheckEquals(t, after.ID, server.ID) + + err = servers.WaitForStatus(client, after.ID, "ACTIVE", 300) + th.AssertNoErr(t, err) + + t.Logf("Server successfully rebuilt.") + logServer(t, after, -1) +} + +func deleteServer(t *testing.T, client *gophercloud.ServiceClient, server *os.Server) { + t.Logf("> servers.Delete") + + res := servers.Delete(client, server.ID) + th.AssertNoErr(t, res.Err) + + t.Logf("Server deleted successfully.") +} + +func deleteServerKeyPair(t *testing.T, client *gophercloud.ServiceClient, k *oskey.KeyPair) { + t.Logf("> keypairs.Delete") + + err := keypairs.Delete(client, k.Name).ExtractErr() + th.AssertNoErr(t, err) + + t.Logf("Keypair deleted successfully.") +} + +func TestServerOperations(t *testing.T) { + client, err := newClient() + th.AssertNoErr(t, err) + + kp := createServerKeyPair(t, client) + defer deleteServerKeyPair(t, client, kp) + + server := createServer(t, client, kp.Name) + defer deleteServer(t, client, server) + + getServer(t, client, server) + listServers(t, client) + changeAdminPassword(t, client, server) + rebootServer(t, client, server) + rebuildServer(t, client, server) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/compute/v2/virtualinterfaces_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/compute/v2/virtualinterfaces_test.go new file mode 100644 index 00000000000..39475e176e2 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/compute/v2/virtualinterfaces_test.go @@ -0,0 +1,53 @@ +// +build acceptance rackspace + +package v2 + +import ( + "testing" + + "github.com/rackspace/gophercloud/pagination" + "github.com/rackspace/gophercloud/rackspace/compute/v2/networks" + "github.com/rackspace/gophercloud/rackspace/compute/v2/virtualinterfaces" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestVirtualInterfaces(t *testing.T) { + client, err := newClient() + th.AssertNoErr(t, err) + + // Create a server + server := createServer(t, client, "") + t.Logf("Created Server: %v\n", server) + defer deleteServer(t, client, server) + serverID := server.ID + + // Create a network + n, err := networks.Create(client, networks.CreateOpts{Label: "sample_network", CIDR: "172.20.0.0/24"}).Extract() + th.AssertNoErr(t, err) + t.Logf("Created Network: %v\n", n) + defer networks.Delete(client, n.ID) + networkID := n.ID + + // Create a virtual interface + vi, err := virtualinterfaces.Create(client, serverID, networkID).Extract() + th.AssertNoErr(t, err) + t.Logf("Created virtual interface: %+v\n", vi) + defer virtualinterfaces.Delete(client, serverID, vi.ID) + + // List virtual interfaces + pager := virtualinterfaces.List(client, serverID) + err = pager.EachPage(func(page pagination.Page) (bool, error) { + t.Logf("--- Page ---") + + virtualinterfacesList, err := virtualinterfaces.ExtractVirtualInterfaces(page) + th.AssertNoErr(t, err) + + for _, vi := range virtualinterfacesList { + t.Logf("Virtual Interface: ID [%s] MAC Address [%s] IP Addresses [%v]", + vi.ID, vi.MACAddress, vi.IPAddresses) + } + + return true, nil + }) + th.CheckNoErr(t, err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/identity/v2/extension_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/identity/v2/extension_test.go new file mode 100644 index 00000000000..a50e015522d --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/identity/v2/identity_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/identity/v2/identity_test.go new file mode 100644 index 00000000000..1182982f44d --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/identity/v2/identity_test.go @@ -0,0 +1,50 @@ +// +build acceptance + +package v2 + +import ( + "testing" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/acceptance/tools" + "github.com/rackspace/gophercloud/rackspace" + th "github.com/rackspace/gophercloud/testhelper" +) + +func rackspaceAuthOptions(t *testing.T) gophercloud.AuthOptions { + // Obtain credentials from the environment. + options, err := rackspace.AuthOptionsFromEnv() + th.AssertNoErr(t, err) + options = tools.OnlyRS(options) + + if options.Username == "" { + t.Fatal("Please provide a Rackspace username as RS_USERNAME.") + } + if options.APIKey == "" { + t.Fatal("Please provide a Rackspace API key as RS_API_KEY.") + } + + return options +} + +func createClient(t *testing.T, 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/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/identity/v2/tenant_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/identity/v2/tenant_test.go new file mode 100644 index 00000000000..6081a498e34 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/objectstorage/v1/accounts_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/objectstorage/v1/accounts_test.go new file mode 100644 index 00000000000..145e4e0482c --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/objectstorage/v1/accounts_test.go @@ -0,0 +1,33 @@ +// +build acceptance rackspace + +package v1 + +import ( + "testing" + + raxAccounts "github.com/rackspace/gophercloud/rackspace/objectstorage/v1/accounts" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestAccounts(t *testing.T) { + c, err := createClient(t, false) + th.AssertNoErr(t, err) + + updateres := raxAccounts.Update(c, raxAccounts.UpdateOpts{Metadata: map[string]string{"white": "mountains"}}) + th.AssertNoErr(t, updateres.Err) + t.Logf("Headers from Update Account request: %+v\n", updateres.Header) + defer func() { + updateres = raxAccounts.Update(c, raxAccounts.UpdateOpts{Metadata: map[string]string{"white": ""}}) + th.AssertNoErr(t, updateres.Err) + metadata, err := raxAccounts.Get(c).ExtractMetadata() + th.AssertNoErr(t, err) + t.Logf("Metadata from Get Account request (after update reverted): %+v\n", metadata) + th.CheckEquals(t, metadata["White"], "") + }() + + metadata, err := raxAccounts.Get(c).ExtractMetadata() + th.AssertNoErr(t, err) + t.Logf("Metadata from Get Account request (after update): %+v\n", metadata) + + th.CheckEquals(t, metadata["White"], "mountains") +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/objectstorage/v1/bulk_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/objectstorage/v1/bulk_test.go new file mode 100644 index 00000000000..79013a564a9 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/objectstorage/v1/bulk_test.go @@ -0,0 +1,23 @@ +// +build acceptance rackspace objectstorage v1 + +package v1 + +import ( + "testing" + + "github.com/rackspace/gophercloud/rackspace/objectstorage/v1/bulk" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestBulk(t *testing.T) { + c, err := createClient(t, false) + th.AssertNoErr(t, err) + + var options bulk.DeleteOpts + options = append(options, "container/object1") + res := bulk.Delete(c, options) + th.AssertNoErr(t, res.Err) + body, err := res.ExtractBody() + th.AssertNoErr(t, err) + t.Logf("Response body from Bulk Delete Request: %+v\n", body) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/objectstorage/v1/cdncontainers_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/objectstorage/v1/cdncontainers_test.go new file mode 100644 index 00000000000..e1bf38b16fe --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/objectstorage/v1/cdncontainers_test.go @@ -0,0 +1,61 @@ +// +build acceptance rackspace objectstorage v1 + +package v1 + +import ( + "testing" + + osContainers "github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers" + "github.com/rackspace/gophercloud/pagination" + raxCDNContainers "github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdncontainers" + raxContainers "github.com/rackspace/gophercloud/rackspace/objectstorage/v1/containers" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestCDNContainers(t *testing.T) { + raxClient, err := createClient(t, false) + th.AssertNoErr(t, err) + + createres := raxContainers.Create(raxClient, "gophercloud-test", nil) + th.AssertNoErr(t, createres.Err) + t.Logf("Headers from Create Container request: %+v\n", createres.Header) + defer func() { + res := raxContainers.Delete(raxClient, "gophercloud-test") + th.AssertNoErr(t, res.Err) + }() + + raxCDNClient, err := createClient(t, true) + th.AssertNoErr(t, err) + + r := raxCDNContainers.Enable(raxCDNClient, "gophercloud-test", raxCDNContainers.EnableOpts{CDNEnabled: true, TTL: 900}) + th.AssertNoErr(t, r.Err) + t.Logf("Headers from Enable CDN Container request: %+v\n", r.Header) + + t.Logf("Container Names available to the currently issued token:") + count := 0 + err = raxCDNContainers.List(raxCDNClient, &osContainers.ListOpts{Full: false}).EachPage(func(page pagination.Page) (bool, error) { + t.Logf("--- Page %02d ---", count) + + names, err := raxCDNContainers.ExtractNames(page) + th.AssertNoErr(t, err) + + for i, name := range names { + t.Logf("[%02d] %s", i, name) + } + + count++ + return true, nil + }) + th.AssertNoErr(t, err) + if count == 0 { + t.Errorf("No CDN containers listed for your current token.") + } + + updateres := raxCDNContainers.Update(raxCDNClient, "gophercloud-test", raxCDNContainers.UpdateOpts{CDNEnabled: false}) + th.AssertNoErr(t, updateres.Err) + t.Logf("Headers from Update CDN Container request: %+v\n", updateres.Header) + + metadata, err := raxCDNContainers.Get(raxCDNClient, "gophercloud-test").ExtractMetadata() + th.AssertNoErr(t, err) + t.Logf("Headers from Get CDN Container request (after update): %+v\n", metadata) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/objectstorage/v1/cdnobjects_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/objectstorage/v1/cdnobjects_test.go new file mode 100644 index 00000000000..6e477ae7045 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/objectstorage/v1/cdnobjects_test.go @@ -0,0 +1,46 @@ +// +build acceptance rackspace objectstorage v1 + +package v1 + +import ( + "bytes" + "testing" + + raxCDNContainers "github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdncontainers" + raxCDNObjects "github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdnobjects" + raxContainers "github.com/rackspace/gophercloud/rackspace/objectstorage/v1/containers" + raxObjects "github.com/rackspace/gophercloud/rackspace/objectstorage/v1/objects" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestCDNObjects(t *testing.T) { + raxClient, err := createClient(t, false) + th.AssertNoErr(t, err) + + createContResult := raxContainers.Create(raxClient, "gophercloud-test", nil) + th.AssertNoErr(t, createContResult.Err) + t.Logf("Headers from Create Container request: %+v\n", createContResult.Header) + defer func() { + deleteResult := raxContainers.Delete(raxClient, "gophercloud-test") + th.AssertNoErr(t, deleteResult.Err) + }() + + header, err := raxObjects.Create(raxClient, "gophercloud-test", "test-object", bytes.NewBufferString("gophercloud cdn test"), nil).ExtractHeader() + th.AssertNoErr(t, err) + t.Logf("Headers from Create Object request: %+v\n", header) + defer func() { + deleteResult := raxObjects.Delete(raxClient, "gophercloud-test", "test-object", nil) + th.AssertNoErr(t, deleteResult.Err) + }() + + raxCDNClient, err := createClient(t, true) + th.AssertNoErr(t, err) + + enableResult := raxCDNContainers.Enable(raxCDNClient, "gophercloud-test", raxCDNContainers.EnableOpts{CDNEnabled: true, TTL: 900}) + th.AssertNoErr(t, enableResult.Err) + t.Logf("Headers from Enable CDN Container request: %+v\n", enableResult.Header) + + deleteResult := raxCDNObjects.Delete(raxCDNClient, "gophercloud-test", "test-object", nil) + th.AssertNoErr(t, deleteResult.Err) + t.Logf("Headers from Delete CDN Object request: %+v\n", deleteResult.Err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/objectstorage/v1/common.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/objectstorage/v1/common.go new file mode 100644 index 00000000000..1ae07278cce --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/objectstorage/v1/common.go @@ -0,0 +1,54 @@ +// +build acceptance rackspace objectstorage v1 + +package v1 + +import ( + "os" + "testing" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/acceptance/tools" + "github.com/rackspace/gophercloud/rackspace" + th "github.com/rackspace/gophercloud/testhelper" +) + +func rackspaceAuthOptions(t *testing.T) gophercloud.AuthOptions { + // Obtain credentials from the environment. + options, err := rackspace.AuthOptionsFromEnv() + th.AssertNoErr(t, err) + options = tools.OnlyRS(options) + + if options.Username == "" { + t.Fatal("Please provide a Rackspace username as RS_USERNAME.") + } + if options.APIKey == "" { + t.Fatal("Please provide a Rackspace API key as RS_API_KEY.") + } + + return options +} + +func createClient(t *testing.T, cdn bool) (*gophercloud.ServiceClient, error) { + region := os.Getenv("RS_REGION") + if region == "" { + t.Fatal("Please provide a Rackspace region as RS_REGION") + } + + ao := rackspaceAuthOptions(t) + + provider, err := rackspace.NewClient(ao.IdentityEndpoint) + th.AssertNoErr(t, err) + + err = rackspace.Authenticate(provider, ao) + th.AssertNoErr(t, err) + + if cdn { + return rackspace.NewObjectCDNV1(provider, gophercloud.EndpointOpts{ + Region: region, + }) + } + + return rackspace.NewObjectStorageV1(provider, gophercloud.EndpointOpts{ + Region: region, + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/objectstorage/v1/containers_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/objectstorage/v1/containers_test.go new file mode 100644 index 00000000000..a7339cf3884 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/objectstorage/v1/containers_test.go @@ -0,0 +1,85 @@ +// +build acceptance rackspace objectstorage v1 + +package v1 + +import ( + "testing" + + osContainers "github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers" + "github.com/rackspace/gophercloud/pagination" + raxContainers "github.com/rackspace/gophercloud/rackspace/objectstorage/v1/containers" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestContainers(t *testing.T) { + c, err := createClient(t, false) + th.AssertNoErr(t, err) + + t.Logf("Containers Info available to the currently issued token:") + count := 0 + err = raxContainers.List(c, &osContainers.ListOpts{Full: true}).EachPage(func(page pagination.Page) (bool, error) { + t.Logf("--- Page %02d ---", count) + + containers, err := raxContainers.ExtractInfo(page) + th.AssertNoErr(t, err) + + for i, container := range containers { + t.Logf("[%02d] name=[%s]", i, container.Name) + t.Logf(" count=[%d]", container.Count) + t.Logf(" bytes=[%d]", container.Bytes) + } + + count++ + return true, nil + }) + th.AssertNoErr(t, err) + if count == 0 { + t.Errorf("No containers listed for your current token.") + } + + t.Logf("Container Names available to the currently issued token:") + count = 0 + err = raxContainers.List(c, &osContainers.ListOpts{Full: false}).EachPage(func(page pagination.Page) (bool, error) { + t.Logf("--- Page %02d ---", count) + + names, err := raxContainers.ExtractNames(page) + th.AssertNoErr(t, err) + + for i, name := range names { + t.Logf("[%02d] %s", i, name) + } + + count++ + return true, nil + }) + th.AssertNoErr(t, err) + if count == 0 { + t.Errorf("No containers listed for your current token.") + } + + createres := raxContainers.Create(c, "gophercloud-test", nil) + th.AssertNoErr(t, createres.Err) + defer func() { + res := raxContainers.Delete(c, "gophercloud-test") + th.AssertNoErr(t, res.Err) + }() + + updateres := raxContainers.Update(c, "gophercloud-test", raxContainers.UpdateOpts{Metadata: map[string]string{"white": "mountains"}}) + th.AssertNoErr(t, updateres.Err) + t.Logf("Headers from Update Account request: %+v\n", updateres.Header) + defer func() { + res := raxContainers.Update(c, "gophercloud-test", raxContainers.UpdateOpts{Metadata: map[string]string{"white": ""}}) + th.AssertNoErr(t, res.Err) + metadata, err := raxContainers.Get(c, "gophercloud-test").ExtractMetadata() + th.AssertNoErr(t, err) + t.Logf("Metadata from Get Account request (after update reverted): %+v\n", metadata) + th.CheckEquals(t, metadata["White"], "") + }() + + getres := raxContainers.Get(c, "gophercloud-test") + t.Logf("Headers from Get Account request (after update): %+v\n", getres.Header) + metadata, err := getres.ExtractMetadata() + th.AssertNoErr(t, err) + t.Logf("Metadata from Get Account request (after update): %+v\n", metadata) + th.CheckEquals(t, metadata["White"], "mountains") +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/objectstorage/v1/objects_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/objectstorage/v1/objects_test.go new file mode 100644 index 00000000000..462f2847dbc --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/objectstorage/v1/objects_test.go @@ -0,0 +1,112 @@ +// +build acceptance rackspace objectstorage v1 + +package v1 + +import ( + "bytes" + "testing" + + osObjects "github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects" + "github.com/rackspace/gophercloud/pagination" + raxContainers "github.com/rackspace/gophercloud/rackspace/objectstorage/v1/containers" + raxObjects "github.com/rackspace/gophercloud/rackspace/objectstorage/v1/objects" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestObjects(t *testing.T) { + c, err := createClient(t, false) + th.AssertNoErr(t, err) + + res := raxContainers.Create(c, "gophercloud-test", nil) + th.AssertNoErr(t, res.Err) + + defer func() { + res := raxContainers.Delete(c, "gophercloud-test") + th.AssertNoErr(t, res.Err) + }() + + content := bytes.NewBufferString("Lewis Carroll") + options := &osObjects.CreateOpts{ContentType: "text/plain"} + createres := raxObjects.Create(c, "gophercloud-test", "o1", content, options) + th.AssertNoErr(t, createres.Err) + defer func() { + res := raxObjects.Delete(c, "gophercloud-test", "o1", nil) + th.AssertNoErr(t, res.Err) + }() + + t.Logf("Objects Info available to the currently issued token:") + count := 0 + err = raxObjects.List(c, "gophercloud-test", &osObjects.ListOpts{Full: true}).EachPage(func(page pagination.Page) (bool, error) { + t.Logf("--- Page %02d ---", count) + + objects, err := raxObjects.ExtractInfo(page) + th.AssertNoErr(t, err) + + for i, object := range objects { + t.Logf("[%02d] name=[%s]", i, object.Name) + t.Logf(" content-type=[%s]", object.ContentType) + t.Logf(" bytes=[%d]", object.Bytes) + t.Logf(" last-modified=[%s]", object.LastModified) + t.Logf(" hash=[%s]", object.Hash) + } + + count++ + return true, nil + }) + th.AssertNoErr(t, err) + if count == 0 { + t.Errorf("No objects listed for your current token.") + } + t.Logf("Container Names available to the currently issued token:") + count = 0 + err = raxObjects.List(c, "gophercloud-test", &osObjects.ListOpts{Full: false}).EachPage(func(page pagination.Page) (bool, error) { + t.Logf("--- Page %02d ---", count) + + names, err := raxObjects.ExtractNames(page) + th.AssertNoErr(t, err) + + for i, name := range names { + t.Logf("[%02d] %s", i, name) + } + + count++ + return true, nil + }) + th.AssertNoErr(t, err) + if count == 0 { + t.Errorf("No objects listed for your current token.") + } + + copyres := raxObjects.Copy(c, "gophercloud-test", "o1", &raxObjects.CopyOpts{Destination: "gophercloud-test/o2"}) + th.AssertNoErr(t, copyres.Err) + defer func() { + res := raxObjects.Delete(c, "gophercloud-test", "o2", nil) + th.AssertNoErr(t, res.Err) + }() + + o1Content, err := raxObjects.Download(c, "gophercloud-test", "o1", nil).ExtractContent() + th.AssertNoErr(t, err) + o2Content, err := raxObjects.Download(c, "gophercloud-test", "o2", nil).ExtractContent() + th.AssertNoErr(t, err) + th.AssertEquals(t, string(o2Content), string(o1Content)) + + updateres := raxObjects.Update(c, "gophercloud-test", "o2", osObjects.UpdateOpts{Metadata: map[string]string{"white": "mountains"}}) + th.AssertNoErr(t, updateres.Err) + t.Logf("Headers from Update Account request: %+v\n", updateres.Header) + defer func() { + res := raxObjects.Update(c, "gophercloud-test", "o2", osObjects.UpdateOpts{Metadata: map[string]string{"white": ""}}) + th.AssertNoErr(t, res.Err) + metadata, err := raxObjects.Get(c, "gophercloud-test", "o2", nil).ExtractMetadata() + th.AssertNoErr(t, err) + t.Logf("Metadata from Get Account request (after update reverted): %+v\n", metadata) + th.CheckEquals(t, metadata["White"], "") + }() + + getres := raxObjects.Get(c, "gophercloud-test", "o2", nil) + th.AssertNoErr(t, getres.Err) + t.Logf("Headers from Get Account request (after update): %+v\n", getres.Header) + metadata, err := getres.ExtractMetadata() + th.AssertNoErr(t, err) + t.Logf("Metadata from Get Account request (after update): %+v\n", metadata) + th.CheckEquals(t, metadata["White"], "mountains") +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/pkg.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/pkg.go new file mode 100644 index 00000000000..5d17b32caaa --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/pkg.go @@ -0,0 +1 @@ +package rackspace diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/tools/pkg.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/tools/pkg.go new file mode 100644 index 00000000000..f7eca1298a7 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/tools/pkg.go @@ -0,0 +1 @@ +package tools diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/tools/tools.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/tools/tools.go new file mode 100644 index 00000000000..61b1d7a6992 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/tools/tools.go @@ -0,0 +1,82 @@ +// +build acceptance + +package tools + +import ( + "crypto/rand" + "errors" + "os" + "time" + + "github.com/rackspace/gophercloud" +) + +// ErrTimeout is returned if WaitFor takes longer than 300 second to happen. +var ErrTimeout = errors.New("Timed out") + +// OnlyRS overrides the default Gophercloud behavior of using OS_-prefixed environment variables +// if RS_ variables aren't present. Otherwise, they'll stomp over each other here in the acceptance +// tests, where you need to have both defined. +func OnlyRS(original gophercloud.AuthOptions) gophercloud.AuthOptions { + if os.Getenv("RS_AUTH_URL") == "" { + original.IdentityEndpoint = "" + } + if os.Getenv("RS_USERNAME") == "" { + original.Username = "" + } + if os.Getenv("RS_PASSWORD") == "" { + original.Password = "" + } + if os.Getenv("RS_API_KEY") == "" { + original.APIKey = "" + } + return original +} + +// WaitFor polls a predicate function once per second to wait for a certain state to arrive. +func WaitFor(predicate func() (bool, error)) error { + for i := 0; i < 300; i++ { + time.Sleep(1 * time.Second) + + satisfied, err := predicate() + if err != nil { + return err + } + if satisfied { + return nil + } + } + return ErrTimeout +} + +// MakeNewPassword generates a new string that's guaranteed to be different than the given one. +func MakeNewPassword(oldPass string) string { + randomPassword := RandomString("", 16) + for randomPassword == oldPass { + randomPassword = RandomString("", 16) + } + return randomPassword +} + +// RandomString generates a string of given length, but random content. +// All content will be within the ASCII graphic character set. +// (Implementation from Even Shaw's contribution on +// http://stackoverflow.com/questions/12771930/what-is-the-fastest-way-to-generate-a-long-random-string-in-go). +func RandomString(prefix string, n int) string { + const alphanum = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + var bytes = make([]byte, n) + rand.Read(bytes) + for i, b := range bytes { + bytes[i] = alphanum[b%byte(len(alphanum))] + } + return prefix + string(bytes) +} + +// Elide returns the first bit of its input string with a suffix of "..." if it's longer than +// a comfortable 40 characters. +func Elide(value string) string { + if len(value) > 40 { + return value[0:37] + "..." + } + return value +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/api_fetch.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/api_fetch.go deleted file mode 100644 index 196047e8e15..00000000000 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/api_fetch.go +++ /dev/null @@ -1,49 +0,0 @@ -package gophercloud - -import( - "fmt" - "github.com/mitchellh/mapstructure" -) - -//The default generic openstack api -var OpenstackApi = map[string]interface{}{ - "Type": "compute", - "UrlChoice": PublicURL, -} - -// Api for use with rackspace -var RackspaceApi = map[string]interface{}{ - "Name": "cloudServersOpenStack", - "VersionId": "2", - "UrlChoice": PublicURL, -} - - -//Populates an ApiCriteria struct with the api values -//from one of the api maps -func PopulateApi(variant string) (ApiCriteria, error){ - var Api ApiCriteria - var variantMap map[string]interface{} - - switch variant { - case "": - variantMap = OpenstackApi - - case "openstack": - variantMap = OpenstackApi - - case "rackspace": - variantMap = RackspaceApi - - default: - var err = fmt.Errorf( - "PopulateApi: Unknown variant %# v; legal values: \"openstack\", \"rackspace\"", variant) - return Api, err - } - - err := mapstructure.Decode(variantMap,&Api) - if err != nil{ - return Api,err - } - return Api, err -} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/auth_options.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/auth_options.go new file mode 100644 index 00000000000..bc0ef65a91b --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/auth_options.go @@ -0,0 +1,38 @@ +package gophercloud + +// AuthOptions allows anyone calling Authenticate to 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 that is required to work with + // 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/Godeps/_workspace/src/github.com/rackspace/gophercloud/auth_results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/auth_results.go new file mode 100644 index 00000000000..1a1faa5fe0e --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/auth_results.go @@ -0,0 +1,15 @@ +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 { + // TokenID returns the token's ID value from the authentication response. + TokenID() (string, error) + + // ExpiresAt retrieves the token's expiration time. + ExpiresAt() (time.Time, error) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/authenticate.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/authenticate.go deleted file mode 100644 index ff609aad2f3..00000000000 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/authenticate.go +++ /dev/null @@ -1,257 +0,0 @@ -package gophercloud - -import ( - "fmt" - "github.com/racker/perigee" -) - -// AuthOptions lets anyone calling Authenticate() supply the required access credentials. -// At present, only Identity V2 API support exists; therefore, only Username, Password, -// and optionally, TenantId are provided. If future Identity API versions become available, -// alternative fields unique to those versions may appear here. -type AuthOptions struct { - // Username and Password are required if using Identity V2 API. - // Consult with your provider's control panel to discover your - // account's username and password. - Username, Password string - - // ApiKey used for providers that support Api Key authentication - ApiKey string - - // The TenantId field is optional for the Identity V2 API. - TenantId string - - // The TenantName can be specified instead of the 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 -} - -// AuthContainer provides a JSON encoding wrapper for passing credentials to the Identity -// service. You will not work with this structure directly. -type AuthContainer struct { - Auth Auth `json:"auth"` -} - -// Auth provides a JSON encoding wrapper for passing credentials to the Identity -// service. You will not work with this structure directly. -type Auth struct { - PasswordCredentials *PasswordCredentials `json:"passwordCredentials,omitempty"` - ApiKeyCredentials *ApiKeyCredentials `json:"RAX-KSKEY:apiKeyCredentials,omitempty"` - TenantId string `json:"tenantId,omitempty"` - TenantName string `json:"tenantName,omitempty"` -} - -// PasswordCredentials provides a JSON encoding wrapper for passing credentials to the Identity -// service. You will not work with this structure directly. -type PasswordCredentials struct { - Username string `json:"username"` - Password string `json:"password"` -} - -type ApiKeyCredentials struct { - Username string `json:"username"` - ApiKey string `json:"apiKey"` -} - -// Access encapsulates the API token and its relevant fields, as well as the -// services catalog that Identity API returns once authenticated. -type Access struct { - Token Token - ServiceCatalog []CatalogEntry - User User - provider Provider `json:"-"` - options AuthOptions `json:"-"` - context *Context `json:"-"` -} - -// Token encapsulates an authentication token and when it expires. It also includes -// tenant information if available. -type Token struct { - Id, Expires string - Tenant Tenant -} - -// Tenant encapsulates tenant authentication information. If, after authentication, -// no tenant information is supplied, both Id and Name will be "". -type Tenant struct { - Id, Name string -} - -// User encapsulates the user credentials, and provides visibility in what -// the user can do through its role assignments. -type User struct { - Id, Name string - XRaxDefaultRegion string `json:"RAX-AUTH:defaultRegion"` - Roles []Role -} - -// Role encapsulates a permission that a user can rely on. -type Role struct { - Description, Id, Name string -} - -// CatalogEntry encapsulates a service catalog record. -type CatalogEntry struct { - Name, Type string - Endpoints []EntryEndpoint -} - -// EntryEndpoint encapsulates how to get to the API of some service. -type EntryEndpoint struct { - Region, TenantId string - PublicURL, InternalURL string - VersionId, VersionInfo, VersionList string -} - -type AuthError struct { - StatusCode int -} - -func (ae *AuthError) Error() string { - switch ae.StatusCode { - case 401: - return "Auth failed. Bad credentials." - - default: - return fmt.Sprintf("Auth failed. Status code is: %s.", ae.StatusCode) - } -} - -// -func getAuthCredentials(options AuthOptions) Auth { - if options.ApiKey == "" { - return Auth{ - PasswordCredentials: &PasswordCredentials{ - Username: options.Username, - Password: options.Password, - }, - TenantId: options.TenantId, - TenantName: options.TenantName, - } - } else { - return Auth{ - ApiKeyCredentials: &ApiKeyCredentials{ - Username: options.Username, - ApiKey: options.ApiKey, - }, - TenantId: options.TenantId, - TenantName: options.TenantName, - } - } -} - -// papersPlease contains the common logic between authentication and re-authentication. -// The name, obviously a joke on the process of authentication, was chosen because -// of how many other entities exist in the program containing the word Auth or Authorization. -// I didn't need another one. -func (c *Context) papersPlease(p Provider, options AuthOptions) (*Access, error) { - var access *Access - access = new(Access) - - if (options.Username == "") || (options.Password == "" && options.ApiKey == "") { - return nil, ErrCredentials - } - - resp, err := perigee.Request("POST", p.AuthEndpoint, perigee.Options{ - CustomClient: c.httpClient, - ReqBody: &AuthContainer{ - Auth: getAuthCredentials(options), - }, - Results: &struct { - Access **Access `json:"access"` - }{ - &access, - }, - }) - - if err == nil { - switch resp.StatusCode { - case 200: - access.options = options - access.provider = p - access.context = c - - default: - err = &AuthError { - StatusCode: resp.StatusCode, - } - } - } - - return access, err -} - -// Authenticate() grants access to the OpenStack-compatible provider API. -// -// Providers are identified through a unique key string. -// See the RegisterProvider() method for more details. -// -// The supplied AuthOptions instance allows the client to specify only those credentials -// relevant for the authentication request. At present, support exists for OpenStack -// Identity V2 API only; support for V3 will become available as soon as documentation for it -// becomes readily available. -// -// For Identity V2 API requirements, you must provide at least the Username and Password -// options. The TenantId field is optional, and defaults to "". -func (c *Context) Authenticate(provider string, options AuthOptions) (*Access, error) { - p, err := c.ProviderByName(provider) - if err != nil { - return nil, err - } - return c.papersPlease(p, options) -} - -// Reauthenticate attempts to reauthenticate using the configured access credentials, if -// allowed. This method takes no action unless your AuthOptions has the AllowReauth flag -// set to true. -func (a *Access) Reauthenticate() error { - var other *Access - var err error - - if a.options.AllowReauth { - other, err = a.context.papersPlease(a.provider, a.options) - if err == nil { - *a = *other - } - } - return err -} - -// See AccessProvider interface definition for details. -func (a *Access) FirstEndpointUrlByCriteria(ac ApiCriteria) string { - ep := FindFirstEndpointByCriteria(a.ServiceCatalog, ac) - urls := []string{ep.PublicURL, ep.InternalURL} - return urls[ac.UrlChoice] -} - -// See AccessProvider interface definition for details. -func (a *Access) AuthToken() string { - return a.Token.Id -} - -// See AccessProvider interface definition for details. -func (a *Access) Revoke(tok string) error { - url := a.provider.AuthEndpoint + "/" + tok - err := perigee.Delete(url, perigee.Options{ - MoreHeaders: map[string]string{ - "X-Auth-Token": a.AuthToken(), - }, - OkCodes: []int{204}, - }) - return err -} - -// See ServiceCatalogerForIdentityV2 interface definition for details. -// Note that the raw slice is returend; be careful not to alter the fields of any members, -// for other components of Gophercloud may depend upon them. -// If this becomes a problem in the future, -// a future revision may return a deep-copy of the service catalog instead. -func (a *Access) V2ServiceCatalog() []CatalogEntry { - return a.ServiceCatalog -} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/authenticate_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/authenticate_test.go deleted file mode 100644 index b05c7800f13..00000000000 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/authenticate_test.go +++ /dev/null @@ -1,264 +0,0 @@ -package gophercloud - -import ( - "net/http" - "testing" -) - -const SUCCESSFUL_RESPONSE = `{ - "access": { - "serviceCatalog": [{ - "endpoints": [{ - "publicURL": "https://ord.servers.api.rackspacecloud.com/v2/12345", - "region": "ORD", - "tenantId": "12345", - "versionId": "2", - "versionInfo": "https://ord.servers.api.rackspacecloud.com/v2", - "versionList": "https://ord.servers.api.rackspacecloud.com/" - },{ - "publicURL": "https://dfw.servers.api.rackspacecloud.com/v2/12345", - "region": "DFW", - "tenantId": "12345", - "versionId": "2", - "versionInfo": "https://dfw.servers.api.rackspacecloud.com/v2", - "versionList": "https://dfw.servers.api.rackspacecloud.com/" - }], - "name": "cloudServersOpenStack", - "type": "compute" - },{ - "endpoints": [{ - "publicURL": "https://ord.databases.api.rackspacecloud.com/v1.0/12345", - "region": "ORD", - "tenantId": "12345" - }], - "name": "cloudDatabases", - "type": "rax:database" - }], - "token": { - "expires": "2012-04-13T13:15:00.000-05:00", - "id": "aaaaa-bbbbb-ccccc-dddd" - }, - "user": { - "RAX-AUTH:defaultRegion": "DFW", - "id": "161418", - "name": "demoauthor", - "roles": [{ - "description": "User Admin Role.", - "id": "3", - "name": "identity:user-admin" - }] - } - } -} -` - -func TestAuthProvider(t *testing.T) { - tt := newTransport().WithResponse(SUCCESSFUL_RESPONSE) - c := TestContext().UseCustomClient(&http.Client{ - Transport: tt, - }) - - _, err := c.Authenticate("", AuthOptions{}) - if err == nil { - t.Error("Expected error for empty provider string") - return - } - _, err = c.Authenticate("unknown-provider", AuthOptions{Username: "u", Password: "p"}) - if err == nil { - t.Error("Expected error for unknown service provider") - return - } - - err = c.RegisterProvider("provider", Provider{AuthEndpoint: "/"}) - if err != nil { - t.Error(err) - return - } - _, err = c.Authenticate("provider", AuthOptions{Username: "u", Password: "p"}) - if err != nil { - t.Error(err) - return - } - if tt.called != 1 { - t.Error("Expected transport to be called once.") - return - } -} - -func TestTenantIdEncoding(t *testing.T) { - tt := newTransport().WithResponse(SUCCESSFUL_RESPONSE) - c := TestContext(). - UseCustomClient(&http.Client{ - Transport: tt, - }). - WithProvider("provider", Provider{AuthEndpoint: "/"}) - - tt.IgnoreTenantId() - _, err := c.Authenticate("provider", AuthOptions{ - Username: "u", - Password: "p", - }) - if err != nil { - t.Error(err) - return - } - if tt.tenantIdFound { - t.Error("Tenant ID should not have been encoded") - return - } - - tt.ExpectTenantId() - _, err = c.Authenticate("provider", AuthOptions{ - Username: "u", - Password: "p", - TenantId: "t", - }) - if err != nil { - t.Error(err) - return - } - if !tt.tenantIdFound { - t.Error("Tenant ID should have been encoded") - return - } -} - -func TestUserNameAndPassword(t *testing.T) { - c := TestContext(). - WithProvider("provider", Provider{AuthEndpoint: "http://localhost/"}). - UseCustomClient(&http.Client{Transport: newTransport().WithResponse(SUCCESSFUL_RESPONSE)}) - - credentials := []AuthOptions{ - {}, - {Username: "u"}, - {Password: "p"}, - } - for i, auth := range credentials { - _, err := c.Authenticate("provider", auth) - if err == nil { - t.Error("Expected error from missing credentials (%d)", i) - return - } - } - - _, err := c.Authenticate("provider", AuthOptions{Username: "u", Password: "p"}) - if err != nil { - t.Error(err) - return - } -} - -func TestUserNameAndApiKey(t *testing.T) { - c := TestContext(). - WithProvider("provider", Provider{AuthEndpoint: "http://localhost/"}). - UseCustomClient(&http.Client{Transport: newTransport().WithResponse(SUCCESSFUL_RESPONSE)}) - - credentials := []AuthOptions{ - {}, - {Username: "u"}, - {ApiKey: "a"}, - } - for i, auth := range credentials { - _, err := c.Authenticate("provider", auth) - if err == nil { - t.Error("Expected error from missing credentials (%d)", i) - return - } - } - - _, err := c.Authenticate("provider", AuthOptions{Username: "u", ApiKey: "a"}) - if err != nil { - t.Error(err) - return - } -} - -func TestTokenAcquisition(t *testing.T) { - c := TestContext(). - UseCustomClient(&http.Client{Transport: newTransport().WithResponse(SUCCESSFUL_RESPONSE)}). - WithProvider("provider", Provider{AuthEndpoint: "http://localhost/"}) - - acc, err := c.Authenticate("provider", AuthOptions{Username: "u", Password: "p"}) - if err != nil { - t.Error(err) - return - } - - tok := acc.Token - if (tok.Id == "") || (tok.Expires == "") { - t.Error("Expected a valid token for successful login; got %s, %s", tok.Id, tok.Expires) - return - } -} - -func TestServiceCatalogAcquisition(t *testing.T) { - c := TestContext(). - UseCustomClient(&http.Client{Transport: newTransport().WithResponse(SUCCESSFUL_RESPONSE)}). - WithProvider("provider", Provider{AuthEndpoint: "http://localhost/"}) - - acc, err := c.Authenticate("provider", AuthOptions{Username: "u", Password: "p"}) - if err != nil { - t.Error(err) - return - } - - svcs := acc.ServiceCatalog - if len(svcs) < 2 { - t.Error("Expected 2 service catalog entries; got %d", len(svcs)) - return - } - - types := map[string]bool{ - "compute": true, - "rax:database": true, - } - for _, entry := range svcs { - if !types[entry.Type] { - t.Error("Expected to find type %s.", entry.Type) - return - } - } -} - -func TestUserAcquisition(t *testing.T) { - c := TestContext(). - UseCustomClient(&http.Client{Transport: newTransport().WithResponse(SUCCESSFUL_RESPONSE)}). - WithProvider("provider", Provider{AuthEndpoint: "http://localhost/"}) - - acc, err := c.Authenticate("provider", AuthOptions{Username: "u", Password: "p"}) - if err != nil { - t.Error(err) - return - } - - u := acc.User - if u.Id != "161418" { - t.Error("Expected user ID of 16148; got", u.Id) - return - } -} - -func TestAuthenticationNeverReauths(t *testing.T) { - tt := newTransport().WithError(401) - c := TestContext(). - UseCustomClient(&http.Client{Transport: tt}). - WithProvider("provider", Provider{AuthEndpoint: "http://localhost"}) - - _, err := c.Authenticate("provider", AuthOptions{Username: "u", Password: "p"}) - if err == nil { - t.Error("Expected an error from a 401 Unauthorized response") - return - } - - rc, _ := ActualResponseCode(err) - if rc != 401 { - t.Error("Expected a 401 error code") - return - } - - err = tt.VerifyCalls(t, 1) - if err != nil { - // Test object already flagged. - return - } -} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/common_types.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/common_types.go deleted file mode 100644 index 044b308dd71..00000000000 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/common_types.go +++ /dev/null @@ -1,24 +0,0 @@ -package gophercloud - -// Link is used for JSON (un)marshalling. -// It provides RESTful links to a resource. -type Link struct { - Href string `json:"href"` - Rel string `json:"rel"` - Type string `json:"type"` -} - -// FileConfig structures represent a blob of data which must appear at a -// a specific location in a server's filesystem. The file contents are -// base-64 encoded. -type FileConfig struct { - Path string `json:"path"` - Contents string `json:"contents"` -} - -// NetworkConfig structures represent an affinity between a server and a -// specific, uniquely identified network. Networks are identified through -// universally unique IDs. -type NetworkConfig struct { - Uuid string `json:"uuid"` -} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/context.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/context.go deleted file mode 100644 index e753c8b6705..00000000000 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/context.go +++ /dev/null @@ -1,150 +0,0 @@ -package gophercloud - -import ( - "net/http" - "strings" - "fmt" - "github.com/tonnerre/golang-pretty" -) - -// Provider structures exist for each tangible provider of OpenStack service. -// For example, Rackspace, Hewlett-Packard, and NASA might have their own instance of this structure. -// -// At a minimum, a provider must expose an authentication endpoint. -type Provider struct { - AuthEndpoint string -} - -// ReauthHandlerFunc functions are responsible for somehow performing the task of -// reauthentication. -type ReauthHandlerFunc func(AccessProvider) error - -// Context structures encapsulate Gophercloud-global state in a manner which -// facilitates easier unit testing. As a user of this SDK, you'll never -// have to use this structure, except when contributing new code to the SDK. -type Context struct { - // providerMap serves as a directory of supported providers. - providerMap map[string]Provider - - // httpClient refers to the current HTTP client interface to use. - httpClient *http.Client - - // reauthHandler provides the functionality needed to re-authenticate - // if that feature is enabled. Note: in order to allow for automatic - // re-authentication, the Context object will need to remember your - // username, password, and tenant ID as provided in the initial call - // to Authenticate(). If you do not desire this, you'll need to handle - // reauthentication yourself through other means. Two methods exist: - // the first approach is to just handle errors yourself at the application - // layer, and the other is through a custom reauthentication handler - // set through the WithReauthHandler() method. - reauthHandler ReauthHandlerFunc -} - -// TestContext yields a new Context instance, pre-initialized with a barren -// state suitable for per-unit-test customization. This configuration consists -// of: -// -// * An empty provider map. -// -// * An HTTP client built by the net/http package (see http://godoc.org/net/http#Client). -func TestContext() *Context { - return &Context{ - providerMap: make(map[string]Provider), - httpClient: &http.Client{}, - reauthHandler: func(acc AccessProvider) error { - return acc.Reauthenticate() - }, - } -} - -// UseCustomClient configures the context to use a customized HTTP client -// instance. By default, TestContext() will return a Context which uses -// the net/http package's default client instance. -func (c *Context) UseCustomClient(hc *http.Client) *Context { - c.httpClient = hc - return c -} - -// RegisterProvider allows a unit test to register a mythical provider convenient for testing. -// If the provider structure lacks adequate configuration, or the configuration given has some -// detectable error, an ErrConfiguration error will result. -func (c *Context) RegisterProvider(name string, p Provider) error { - if p.AuthEndpoint == "" { - return ErrConfiguration - } - - c.providerMap[name] = p - return nil -} - -// WithProvider offers convenience for unit tests. -func (c *Context) WithProvider(name string, p Provider) *Context { - err := c.RegisterProvider(name, p) - if err != nil { - panic(err) - } - return c -} - -// ProviderByName will locate a provider amongst those previously registered, if it exists. -// If the named provider has not been registered, an ErrProvider error will result. -// -// You may also specify a custom Identity API URL. -// Any provider name that contains the characters "://", in that order, will be treated as a custom Identity API URL. -// Custom URLs, important for private cloud deployments, overrides all provider configurations. -func (c *Context) ProviderByName(name string) (p Provider, err error) { - for provider, descriptor := range c.providerMap { - if name == provider { - return descriptor, nil - } - } - if strings.Contains(name, "://") { - p = Provider{ - AuthEndpoint: name, - } - return p, nil - } - return Provider{}, ErrProvider -} - -func getServiceCatalogFromAccessProvider(provider AccessProvider) ([]CatalogEntry) { - access, found := provider.(*Access) - if found { - return access.ServiceCatalog - } else { - return nil - } -} - -// Instantiates a Cloud Servers API for the provider given. -func (c *Context) ServersApi(provider AccessProvider, criteria ApiCriteria) (CloudServersProvider, error) { - url := provider.FirstEndpointUrlByCriteria(criteria) - if url == "" { - var err = fmt.Errorf( - "Missing endpoint, or insufficient privileges to access endpoint; criteria = %# v; serviceCatalog = %# v", - pretty.Formatter(criteria), - pretty.Formatter(getServiceCatalogFromAccessProvider(provider))) - return nil, err - } - - gcp := &genericServersProvider{ - endpoint: url, - context: c, - access: provider, - } - - return gcp, nil -} - -// WithReauthHandler configures the context to handle reauthentication attempts using the supplied -// funtion. By default, reauthentication happens by invoking Authenticate(), which is unlikely to be -// useful in a unit test. -// -// Do not confuse this function with WithReauth()! Although they work together to support reauthentication, -// WithReauth() actually contains the decision-making logic to determine when to perform a reauth, -// while WithReauthHandler() is used to configure what a reauth actually entails. -func (c *Context) WithReauthHandler(f ReauthHandlerFunc) *Context { - c.reauthHandler = f - return c -} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/context_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/context_test.go deleted file mode 100644 index 2936526401c..00000000000 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/context_test.go +++ /dev/null @@ -1,28 +0,0 @@ -package gophercloud - -import ( - "testing" -) - -func TestProviderRegistry(t *testing.T) { - c := TestContext() - - _, err := c.ProviderByName("aProvider") - if err == nil { - t.Error("Expected error when looking for a provider by non-existant name") - return - } - - err = c.RegisterProvider("aProvider", Provider{}) - if err != ErrConfiguration { - t.Error("Unexpected error/nil when registering a provider w/out an auth endpoint\n %s", err) - return - } - - _ = c.RegisterProvider("aProvider", Provider{AuthEndpoint: "http://localhost/auth"}) - _, err = c.ProviderByName("aProvider") - if err != nil { - t.Error(err) - return - } -} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/endpoint_search.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/endpoint_search.go new file mode 100644 index 00000000000..b6f6b4804fc --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/endpoint_search.go @@ -0,0 +1,65 @@ +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 indicates whether a specific service endpoint is accessible. +// 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"). + // Required. + Type string + + // Name is the service name for the client (e.g., "nova") as it appears in + // the service catalog. Services can have the same Type but a different Name, + // which is why both Type and Name are sometimes needed. Optional. + Name string + + // Region is the geographic region in which the service resides. Required only + // for services that span multiple regions. + Region string + + // Availability is the visibility of the endpoint to be returned. Valid types + // are: 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/Godeps/_workspace/src/github.com/rackspace/gophercloud/endpoint_search_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/endpoint_search_test.go new file mode 100644 index 00000000000..34574534274 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/errors.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/errors.go deleted file mode 100644 index 726ba7e97ca..00000000000 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/errors.go +++ /dev/null @@ -1,39 +0,0 @@ -package gophercloud - -import ( - "fmt" -) - -// ErrNotImplemented should be used only while developing new SDK features. -// No established function or method will ever produce this error. -var ErrNotImplemented = fmt.Errorf("Not implemented") - -// ErrProvider errors occur when attempting to reference an unsupported -// provider. More often than not, this error happens due to a typo in -// the name. -var ErrProvider = fmt.Errorf("Missing or incorrect provider") - -// ErrCredentials errors happen when attempting to authenticate using a -// set of credentials not recognized by the Authenticate() method. -// For example, not providing a username or password when attempting to -// authenticate against an Identity V2 API. -var ErrCredentials = fmt.Errorf("Missing or incomplete credentials") - -// ErrConfiguration errors happen when attempting to add a new provider, and -// the provider added lacks a correct or consistent configuration. -// For example, all providers must expose at least an Identity V2 API -// for authentication; if this endpoint isn't specified, you may receive -// this error when attempting to register it against a context. -var ErrConfiguration = fmt.Errorf("Missing or incomplete configuration") - -// ErrError errors happen when you attempt to discover the response code -// responsible for a previous request bombing with an error, but pass in an -// error interface which doesn't belong to the web client. -var ErrError = fmt.Errorf("Attempt to solicit actual HTTP response code from error entity which doesn't know") - -// WarnUnauthoritative warnings happen when a service believes its response -// to be correct, but is not in a position of knowing for sure at the moment. -// For example, the service could be responding with cached data that has -// exceeded its time-to-live setting, but which has not yet received an official -// update from an authoritative source. -var WarnUnauthoritative = fmt.Errorf("Unauthoritative data") diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/flavors.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/flavors.go deleted file mode 100644 index eb864d5787f..00000000000 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/flavors.go +++ /dev/null @@ -1,55 +0,0 @@ -package gophercloud - -import ( - "github.com/racker/perigee" -) - -// See CloudServersProvider interface for details. -func (gsp *genericServersProvider) ListFlavors() ([]Flavor, error) { - var fs []Flavor - - err := gsp.context.WithReauth(gsp.access, func() error { - url := gsp.endpoint + "/flavors/detail" - return perigee.Get(url, perigee.Options{ - CustomClient: gsp.context.httpClient, - Results: &struct{ Flavors *[]Flavor }{&fs}, - MoreHeaders: map[string]string{ - "X-Auth-Token": gsp.access.AuthToken(), - }, - }) - }) - return fs, err -} - -// FlavorLink provides a reference to a flavor by either ID or by direct URL. -// Some services use just the ID, others use just the URL. -// This structure provides a common means of expressing both in a single field. -type FlavorLink struct { - Id string `json:"id"` - Links []Link `json:"links"` -} - -// Flavor records represent (virtual) hardware configurations for server resources in a region. -// -// 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. -// -// The Disk and Ram fields provide a measure of storage space offered by the flavor, in GB and MB, respectively. -// -// The Name field provides a human-readable moniker for the flavor. -// -// Swap indicates how much space is reserved for swap. -// If not provided, this field will be set to 0. -// -// VCpus indicates how many (virtual) CPUs are available for this flavor. -type Flavor struct { - OsFlvDisabled bool `json:"OS-FLV-DISABLED:disabled"` - Disk int `json:"disk"` - Id string `json:"id"` - Links []Link `json:"links"` - Name string `json:"name"` - Ram int `json:"ram"` - RxTxFactor float64 `json:"rxtx_factor"` - Swap int `json:"swap"` - VCpus int `json:"vcpus"` -} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/floating_ips.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/floating_ips.go deleted file mode 100644 index 11636673ea4..00000000000 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/floating_ips.go +++ /dev/null @@ -1,88 +0,0 @@ -package gophercloud - -import ( - "errors" - "fmt" - "github.com/racker/perigee" -) - -func (gsp *genericServersProvider) ListFloatingIps() ([]FloatingIp, error) { - var fips []FloatingIp - - err := gsp.context.WithReauth(gsp.access, func() error { - url := gsp.endpoint + "/os-floating-ips" - return perigee.Get(url, perigee.Options{ - CustomClient: gsp.context.httpClient, - Results: &struct { - FloatingIps *[]FloatingIp `json:"floating_ips"` - }{&fips}, - MoreHeaders: map[string]string{ - "X-Auth-Token": gsp.access.AuthToken(), - }, - }) - }) - return fips, err -} - -func (gsp *genericServersProvider) CreateFloatingIp(pool string) (FloatingIp, error) { - fip := new(FloatingIp) - - err := gsp.context.WithReauth(gsp.access, func() error { - url := gsp.endpoint + "/os-floating-ips" - return perigee.Post(url, perigee.Options{ - CustomClient: gsp.context.httpClient, - ReqBody: map[string]string{ - "pool": pool, - }, - Results: &struct { - FloatingIp **FloatingIp `json:"floating_ip"` - }{&fip}, - MoreHeaders: map[string]string{ - "X-Auth-Token": gsp.access.AuthToken(), - }, - }) - }) - - if fip.Ip == "" { - return *fip, errors.New("Error creating floating IP") - } - - return *fip, err -} - -func (gsp *genericServersProvider) AssociateFloatingIp(serverId string, ip FloatingIp) error { - return gsp.context.WithReauth(gsp.access, func() error { - ep := fmt.Sprintf("%s/servers/%s/action", gsp.endpoint, serverId) - return perigee.Post(ep, perigee.Options{ - CustomClient: gsp.context.httpClient, - ReqBody: map[string](map[string]string){ - "addFloatingIp": map[string]string{"address": ip.Ip}, - }, - MoreHeaders: map[string]string{ - "X-Auth-Token": gsp.access.AuthToken(), - }, - OkCodes: []int{202}, - }) - }) -} - -func (gsp *genericServersProvider) DeleteFloatingIp(ip FloatingIp) error { - return gsp.context.WithReauth(gsp.access, func() error { - ep := fmt.Sprintf("%s/os-floating-ips/%d", gsp.endpoint, ip.Id) - return perigee.Delete(ep, perigee.Options{ - CustomClient: gsp.context.httpClient, - MoreHeaders: map[string]string{ - "X-Auth-Token": gsp.access.AuthToken(), - }, - OkCodes: []int{202}, - }) - }) -} - -type FloatingIp struct { - Id int `json:"id"` - Pool string `json:"pool"` - Ip string `json:"ip"` - FixedIp string `json:"fixed_ip"` - InstanceId string `json:"instance_id"` -} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/global_context.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/global_context.go deleted file mode 100644 index 89d283b1bc3..00000000000 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/global_context.go +++ /dev/null @@ -1,67 +0,0 @@ -package gophercloud - -import ( - "github.com/racker/perigee" -) - -// globalContext is the, well, "global context." -// Most of this SDK is written in a manner to facilitate easier testing, -// which doesn't require all the configuration a real-world application would require. -// However, for real-world deployments, applications should be able to rely on a consistent configuration of providers, etc. -var globalContext *Context - -// providers is the set of supported providers. -var providers = map[string]Provider{ - "rackspace-us": { - AuthEndpoint: "https://identity.api.rackspacecloud.com/v2.0/tokens", - }, - "rackspace-uk": { - AuthEndpoint: "https://lon.identity.api.rackspacecloud.com/v2.0/tokens", - }, -} - -// Initialize the global context to sane configuration. -// The Go runtime ensures this function is called before main(), -// thus guaranteeing proper configuration before your application ever runs. -func init() { - globalContext = TestContext() - for name, descriptor := range providers { - globalContext.RegisterProvider(name, descriptor) - } -} - -// Authenticate() grants access to the OpenStack-compatible provider API. -// -// Providers are identified through a unique key string. -// Specifying an unsupported provider will result in an ErrProvider error. -// However, you may also specify a custom Identity API URL. -// Any provider name that contains the characters "://", in that order, will be treated as a custom Identity API URL. -// Custom URLs, important for private cloud deployments, overrides all provider configurations. -// -// The supplied AuthOptions instance allows the client to specify only those credentials -// relevant for the authentication request. At present, support exists for OpenStack -// Identity V2 API only; support for V3 will become available as soon as documentation for it -// becomes readily available. -// -// For Identity V2 API requirements, you must provide at least the Username and Password -// options. The TenantId field is optional, and defaults to "". -func Authenticate(provider string, options AuthOptions) (*Access, error) { - return globalContext.Authenticate(provider, options) -} - -// Instantiates a Cloud Servers object for the provider given. -func ServersApi(acc AccessProvider, criteria ApiCriteria) (CloudServersProvider, error) { - return globalContext.ServersApi(acc, criteria) -} - -// ActualResponseCode inspects a returned error, and discovers the actual response actual -// response code that caused the error to be raised. -func ActualResponseCode(e error) (int, error) { - if err, typeOk := e.(*perigee.UnexpectedResponseCodeError); typeOk { - return err.Actual, nil - } else if err, typeOk := e.(*AuthError); typeOk{ - return err.StatusCode, nil - } - - return 0, ErrError -} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/images.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/images.go deleted file mode 100644 index a23e0bbb67d..00000000000 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/images.go +++ /dev/null @@ -1,106 +0,0 @@ -package gophercloud - -import ( - "github.com/racker/perigee" -) - -// See the CloudImagesProvider interface for details. -func (gsp *genericServersProvider) ListImages() ([]Image, error) { - var is []Image - - err := gsp.context.WithReauth(gsp.access, func() error { - url := gsp.endpoint + "/images/detail" - return perigee.Get(url, perigee.Options{ - CustomClient: gsp.context.httpClient, - Results: &struct{ Images *[]Image }{&is}, - MoreHeaders: map[string]string{ - "X-Auth-Token": gsp.access.AuthToken(), - }, - }) - }) - return is, err -} - -func (gsp *genericServersProvider) ImageById(id string) (*Image, error) { - var is *Image - - err := gsp.context.WithReauth(gsp.access, func() error { - url := gsp.endpoint + "/images/" + id - return perigee.Get(url, perigee.Options{ - CustomClient: gsp.context.httpClient, - Results: &struct{ Image **Image }{&is}, - MoreHeaders: map[string]string{ - "X-Auth-Token": gsp.access.AuthToken(), - }, - }) - }) - return is, err -} - -func (gsp *genericServersProvider) DeleteImageById(id string) error { - err := gsp.context.WithReauth(gsp.access, func() error { - url := gsp.endpoint + "/images/" + id - _, err := perigee.Request("DELETE", url, perigee.Options{ - CustomClient: gsp.context.httpClient, - MoreHeaders: map[string]string{ - "X-Auth-Token": gsp.access.AuthToken(), - }, - }) - return err - }) - return err -} - -// ImageLink provides a reference to a image by either ID or by direct URL. -// Some services use just the ID, others use just the URL. -// This structure provides a common means of expressing both in a single field. -type ImageLink struct { - Id string `json:"id"` - Links []Link `json:"links"` -} - -// Image is used for JSON (un)marshalling. -// It provides a description of an OS image. -// -// The Id field contains the image's unique identifier. -// For example, this identifier will be useful for specifying which operating system to install on a new server instance. -// -// The MinDisk and MinRam fields specify the minimum resources a server must provide to be able to install the image. -// -// The Name field provides a human-readable moniker for the OS image. -// -// The Progress and Status fields indicate image-creation status. -// Any usable image will have 100% progress. -// -// The Updated field indicates the last time this image was changed. -// -// OsDcfDiskConfig indicates the server's boot volume configuration. -// Valid values are: -// AUTO -// ---- -// The server is built with a single partition the size of the target flavor disk. -// The file system is automatically adjusted to fit the entire partition. -// This keeps things simple and automated. -// AUTO is valid only for images and servers with a single partition that use the EXT3 file system. -// This is the default setting for applicable Rackspace base images. -// -// MANUAL -// ------ -// The server is built using whatever partition scheme and file system is in the source image. -// If the target flavor disk is larger, -// the remaining disk space is left unpartitioned. -// This enables images to have non-EXT3 file systems, multiple partitions, and so on, -// and enables you to manage the disk configuration. -// -type Image struct { - Created string `json:"created"` - Id string `json:"id"` - Links []Link `json:"links"` - MinDisk int `json:"minDisk"` - MinRam int `json:"minRam"` - Name string `json:"name"` - Progress int `json:"progress"` - Status string `json:"status"` - Updated string `json:"updated"` - OsDcfDiskConfig string `json:"OS-DCF:diskConfig"` -} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/interfaces.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/interfaces.go deleted file mode 100644 index 4c7dbee422a..00000000000 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/interfaces.go +++ /dev/null @@ -1,247 +0,0 @@ -package gophercloud - -import "net/url" - -// AccessProvider instances encapsulate a Keystone authentication interface. -type AccessProvider interface { - // FirstEndpointUrlByCriteria searches through the service catalog for the first - // matching entry endpoint fulfilling the provided criteria. If nothing found, - // return "". Otherwise, return either the public or internal URL for the - // endpoint, depending on both its existence and the setting of the ApiCriteria.UrlChoice - // field. - FirstEndpointUrlByCriteria(ApiCriteria) string - - // AuthToken provides a copy of the current authentication token for the user's credentials. - // Note that AuthToken() will not automatically refresh an expired token. - AuthToken() string - - // Revoke allows you to terminate any program's access to the OpenStack API by token ID. - Revoke(string) error - - // Reauthenticate attempts to acquire a new authentication token, if the feature is enabled by - // AuthOptions.AllowReauth. - Reauthenticate() error -} - -// ServiceCatalogerIdentityV2 interface provides direct access to the service catalog as offered by the Identity V2 API. -// We regret we need to fracture the namespace of what should otherwise be a simple concept; however, -// the OpenStack community saw fit to render V3's service catalog completely incompatible with V2. -type ServiceCatalogerForIdentityV2 interface { - V2ServiceCatalog() []CatalogEntry -} - -// CloudServersProvider instances encapsulate a Cloud Servers API, should one exist in the service catalog -// for your provider. -type CloudServersProvider interface { - // Servers - - // ListServers provides a complete list of servers hosted by the user - // in a given region. This function differs from ListServersLinksOnly() - // in that it returns all available details for each server returned. - ListServers() ([]Server, error) - - // ListServersByFilters provides a list of servers hosted by the user in a - // given region. This function let you requests servers by certain URI - // paramaters defined by the API endpoint. This is sometimes more suitable - // if you have many servers and you only want to pick servers on certain - // criterias. An example usage could be : - // - // filter := url.Values{} - // filter.Set("name", "MyServer") - // filter.Set("status", "ACTIVE") - // - // filteredServers, err := c.ListServersByFilters(filter) - // - // Here, filteredServers only contains servers whose name started with - // "MyServer" and are in "ACTIVE" status. - ListServersByFilter(filter url.Values) ([]Server, error) - - // ListServers provides a complete list of servers hosted by the user - // in a given region. This function differs from ListServers() in that - // it returns only IDs and links to each server returned. - // - // This function should be used only under certain circumstances. - // It's most useful for checking to see if a server with a given ID exists, - // or that you have permission to work with that server. It's also useful - // when the cost of retrieving the server link list plus the overhead of manually - // invoking ServerById() for each of the servers you're interested in is less than - // just calling ListServers() to begin with. This may be a consideration, for - // example, with mobile applications. - // - // In other cases, you probably should just call ListServers() and cache the - // results to conserve overall bandwidth and reduce your access rate on the API. - ListServersLinksOnly() ([]Server, error) - - // ServerById will retrieve a detailed server description given the unique ID - // of a server. The ID can be returned by either ListServers() or by ListServersLinksOnly(). - ServerById(id string) (*Server, error) - - // CreateServer requests a new server to be created by the cloud server provider. - // The user must pass in a pointer to an initialized NewServerContainer structure. - // Please refer to the NewServerContainer documentation for more details. - // - // If the NewServer structure's AdminPass is empty (""), a password will be - // automatically generated by your OpenStack provider, and returned through the - // AdminPass field of the result. Take care, however; this will be the only time - // this happens. No other means exists in the public API to acquire a password - // for a pre-existing server. If you lose it, you'll need to call SetAdminPassword() - // to set a new one. - CreateServer(ns NewServer) (*NewServer, error) - - // DeleteServerById requests that the server with the assigned ID be removed - // from your account. The delete happens asynchronously. - DeleteServerById(id string) error - - // SetAdminPassword requests that the server with the specified ID have its - // administrative password changed. For Linux, BSD, or other POSIX-like - // system, this password corresponds to the root user. For Windows machines, - // the Administrator password will be affected instead. - SetAdminPassword(id string, pw string) error - - // ResizeServer can be a short-hand for RebuildServer where only the size of the server - // changes. Note that after the resize operation is requested, you will need to confirm - // the resize has completed for changes to take effect permanently. Changes will assume - // to be confirmed even without an explicit confirmation after 24 hours from the initial - // request. - ResizeServer(id, newName, newFlavor, newDiskConfig string) error - - // RevertResize will reject a server's resized configuration, thus - // rolling back to the original server. - RevertResize(id string) error - - // ConfirmResizeServer will acknowledge a server's resized configuration. - ConfirmResize(id string) error - - // RebootServer requests that the server with the specified ID be rebooted. - // Two reboot mechanisms exist. - // - // - Hard. This will physically power-cycle the unit. - // - Soft. This will attempt to use the server's software-based mechanisms to restart - // the machine. E.g., "shutdown -r now" on Linux. - RebootServer(id string, hard bool) error - - // RescueServer requests that the server with the specified ID be placed into - // a state of maintenance. The server instance is replaced with a new instance, - // of the same flavor and image. This new image will have the boot volume of the - // original machine mounted as a secondary device, so that repair and administration - // may occur. Use UnrescueServer() to restore the server to its previous state. - // Note also that many providers will impose a time limit for how long a server may - // exist in rescue mode! Consult the API documentation for your provider for - // details. - RescueServer(id string) (string, error) - - // UnrescueServer requests that a server in rescue state be placed into its nominal - // operating state. - UnrescueServer(id string) error - - // UpdateServer alters one or more fields of the identified server's Server record. - // However, not all fields may be altered. Presently, only Name, AccessIPv4, and - // AccessIPv6 fields may be altered. If unspecified, or set to an empty or zero - // value, the corresponding field remains unaltered. - // - // This function returns the new set of server details if successful. - UpdateServer(id string, newValues NewServerSettings) (*Server, error) - - // RebuildServer reprovisions a server to the specifications given by the - // NewServer structure. The following fields are guaranteed to be recognized: - // - // Name (required) AccessIPv4 - // imageRef (required) AccessIPv6 - // AdminPass (required) Metadata - // Personality - // - // Other providers may reserve the right to act on additional fields. - RebuildServer(id string, ns NewServer) (*Server, error) - - // CreateImage will create a new image from the specified server id returning the id of the new image. - CreateImage(id string, ci CreateImage) (string, error) - - // Addresses - - // ListAddresses yields the list of available addresses for the server. - // This information is also returned by ServerById() in the Server.Addresses - // field. However, if you have a lot of servers and all you need are addresses, - // this function might be more efficient. - ListAddresses(id string) (AddressSet, error) - - // ListAddressesByNetwork yields the list of available addresses for a given server id and networkLabel. - // Example: ListAddressesByNetwork("234-4353-4jfrj-43j2s", "private") - ListAddressesByNetwork(id, networkLabel string) (NetworkAddress, error) - - // ListFloatingIps yields the list of all floating IP addresses allocated to the current project. - ListFloatingIps() ([]FloatingIp, error) - - // CreateFloatingIp allocates a new IP from the named pool to the current project. - CreateFloatingIp(pool string) (FloatingIp, error) - - // DeleteFloatingIp returns the specified IP from the current project to the pool. - DeleteFloatingIp(ip FloatingIp) error - - // AssociateFloatingIp associates the given floating IP to the given server id. - AssociateFloatingIp(serverId string, ip FloatingIp) error - - // Images - - // ListImages yields the list of available operating system images. This function - // returns full details for each image, if available. - ListImages() ([]Image, error) - - // ImageById yields details about a specific image. - ImageById(id string) (*Image, error) - - // DeleteImageById will delete the specific image. - DeleteImageById(id string) error - - // Flavors - - // ListFlavors yields the list of available system flavors. This function - // returns full details for each flavor, if available. - ListFlavors() ([]Flavor, error) - - // KeyPairs - - // ListKeyPairs yields the list of available keypairs. - ListKeyPairs() ([]KeyPair, error) - - // CreateKeyPairs will create or generate a new keypair. - CreateKeyPair(nkp NewKeyPair) (KeyPair, error) - - // DeleteKeyPair wil delete a keypair. - DeleteKeyPair(name string) error - - // ShowKeyPair will yield the named keypair. - ShowKeyPair(name string) (KeyPair, error) - - // ListSecurityGroups provides a listing of security groups for the tenant. - // This method works only if the provider supports the os-security-groups extension. - ListSecurityGroups() ([]SecurityGroup, error) - - // CreateSecurityGroup lets a tenant create a new security group. - // Only the SecurityGroup fields which are specified will be marshalled to the API. - // This method works only if the provider supports the os-security-groups extension. - CreateSecurityGroup(desired SecurityGroup) (*SecurityGroup, error) - - // ListSecurityGroupsByServerId provides a list of security groups which apply to the indicated server. - // This method works only if the provider supports the os-security-groups extension. - ListSecurityGroupsByServerId(id string) ([]SecurityGroup, error) - - // SecurityGroupById returns a security group corresponding to the provided ID number. - // This method works only if the provider supports the os-security-groups extension. - SecurityGroupById(id int) (*SecurityGroup, error) - - // DeleteSecurityGroupById disposes of a security group corresponding to the provided ID number. - // This method works only if the provider supports the os-security-groups extension. - DeleteSecurityGroupById(id int) error - - // ListDefaultSGRules lists default security group rules. - // This method only works if the provider supports the os-security-groups-default-rules extension. - ListDefaultSGRules() ([]SGRule, error) - - // CreateDefaultSGRule creates a default security group rule. - // This method only works if the provider supports the os-security-groups-default-rules extension. - CreateDefaultSGRule(SGRule) (*SGRule, error) - - // GetSGRule obtains information for a specified security group rule. - // This method only works if the provider supports the os-security-groups-default-rules extension. - GetSGRule(string) (*SGRule, error) -} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/keypairs.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/keypairs.go deleted file mode 100644 index 8ae8cd39396..00000000000 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/keypairs.go +++ /dev/null @@ -1,98 +0,0 @@ -package gophercloud - -import ( - "github.com/racker/perigee" -) - -// See the CloudImagesProvider interface for details. -func (gsp *genericServersProvider) ListKeyPairs() ([]KeyPair, error) { - type KeyPairs struct { - KeyPairs []struct { - KeyPair KeyPair `json:"keypair"` - } `json:"keypairs"` - } - - var kp KeyPairs - - err := gsp.context.WithReauth(gsp.access, func() error { - url := gsp.endpoint + "/os-keypairs" - return perigee.Get(url, perigee.Options{ - CustomClient: gsp.context.httpClient, - Results: &kp, - MoreHeaders: map[string]string{ - "X-Auth-Token": gsp.access.AuthToken(), - }, - }) - }) - - // Flatten out the list of keypairs - var keypairs []KeyPair - for _, k := range kp.KeyPairs { - keypairs = append(keypairs, k.KeyPair) - } - return keypairs, err -} - -func (gsp *genericServersProvider) CreateKeyPair(nkp NewKeyPair) (KeyPair, error) { - var kp KeyPair - - err := gsp.context.WithReauth(gsp.access, func() error { - url := gsp.endpoint + "/os-keypairs" - return perigee.Post(url, perigee.Options{ - ReqBody: &struct { - KeyPair *NewKeyPair `json:"keypair"` - }{&nkp}, - CustomClient: gsp.context.httpClient, - Results: &struct{ KeyPair *KeyPair }{&kp}, - MoreHeaders: map[string]string{ - "X-Auth-Token": gsp.access.AuthToken(), - }, - OkCodes: []int{200}, - }) - }) - return kp, err -} - -// See the CloudImagesProvider interface for details. -func (gsp *genericServersProvider) DeleteKeyPair(name string) error { - err := gsp.context.WithReauth(gsp.access, func() error { - url := gsp.endpoint + "/os-keypairs/" + name - return perigee.Delete(url, perigee.Options{ - CustomClient: gsp.context.httpClient, - MoreHeaders: map[string]string{ - "X-Auth-Token": gsp.access.AuthToken(), - }, - OkCodes: []int{202}, - }) - }) - return err -} - -func (gsp *genericServersProvider) ShowKeyPair(name string) (KeyPair, error) { - var kp KeyPair - - err := gsp.context.WithReauth(gsp.access, func() error { - url := gsp.endpoint + "/os-keypairs/" + name - return perigee.Get(url, perigee.Options{ - CustomClient: gsp.context.httpClient, - Results: &struct{ KeyPair *KeyPair }{&kp}, - MoreHeaders: map[string]string{ - "X-Auth-Token": gsp.access.AuthToken(), - }, - }) - }) - return kp, err -} - -type KeyPair struct { - FingerPrint string `json:"fingerprint"` - Name string `json:"name"` - PrivateKey string `json:"private_key,omitempty"` - PublicKey string `json:"public_key"` - UserID string `json:"user_id,omitempty"` -} - -type NewKeyPair struct { - Name string `json:"name"` - PublicKey string `json:"public_key,omitempty"` -} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/auth_env.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/auth_env.go new file mode 100644 index 00000000000..a4402b6f06d --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/auth_env.go @@ -0,0 +1,58 @@ +package openstack + +import ( + "fmt" + "os" + + "github.com/rackspace/gophercloud" +) + +var nilOptions = gophercloud.AuthOptions{} + +// ErrNoAuthUrl, ErrNoUsername, and ErrNoPassword errors indicate of the required OS_AUTH_URL, OS_USERNAME, or OS_PASSWORD +// environment variables, respectively, remain undefined. See the AuthOptions() function for more details. +var ( + ErrNoAuthURL = fmt.Errorf("Environment variable OS_AUTH_URL needs to be set.") + ErrNoUsername = fmt.Errorf("Environment variable OS_USERNAME needs to be set.") + ErrNoPassword = fmt.Errorf("Environment variable OS_PASSWORD needs to be set.") +) + +// AuthOptions fills out an identity.AuthOptions structure with the settings found on the various OpenStack +// OS_* environment variables. The following variables provide sources of truth: OS_AUTH_URL, OS_USERNAME, +// OS_PASSWORD, OS_TENANT_ID, and OS_TENANT_NAME. Of these, OS_USERNAME, OS_PASSWORD, and OS_AUTH_URL must +// have settings, or an error will result. OS_TENANT_ID and OS_TENANT_NAME are optional. +func AuthOptionsFromEnv() (gophercloud.AuthOptions, error) { + authURL := os.Getenv("OS_AUTH_URL") + username := os.Getenv("OS_USERNAME") + userID := os.Getenv("OS_USERID") + password := os.Getenv("OS_PASSWORD") + tenantID := os.Getenv("OS_TENANT_ID") + tenantName := os.Getenv("OS_TENANT_NAME") + domainID := os.Getenv("OS_DOMAIN_ID") + domainName := os.Getenv("OS_DOMAIN_NAME") + + if authURL == "" { + return nilOptions, ErrNoAuthURL + } + + if username == "" && userID == "" { + return nilOptions, ErrNoUsername + } + + if password == "" { + return nilOptions, ErrNoPassword + } + + ao := gophercloud.AuthOptions{ + IdentityEndpoint: authURL, + UserID: userID, + Username: username, + Password: password, + TenantID: tenantID, + TenantName: tenantName, + DomainID: domainID, + DomainName: domainName, + } + + return ao, nil +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/apiversions/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/apiversions/doc.go new file mode 100644 index 00000000000..e3af39f513a --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/apiversions/doc.go @@ -0,0 +1,3 @@ +// Package apiversions provides information and interaction with the different +// API versions for the OpenStack Block Storage service, code-named Cinder. +package apiversions diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/apiversions/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/apiversions/requests.go new file mode 100644 index 00000000000..016bf374e31 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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.PageResult) pagination.Page { + return APIVersionPage{pagination.SinglePageBase(r)} + }) +} + +// Get will retrieve the volume type with the provided ID. To extract the volume +// type from the result, call the Extract method on the GetResult. +func Get(client *gophercloud.ServiceClient, v string) GetResult { + var res GetResult + _, err := perigee.Request("GET", getURL(client, v), perigee.Options{ + MoreHeaders: client.AuthenticatedHeaders(), + OkCodes: []int{200}, + Results: &res.Body, + }) + res.Err = err + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/apiversions/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/apiversions/requests_test.go new file mode 100644 index 00000000000..56b5e4fc72b --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/apiversions/requests_test.go @@ -0,0 +1,145 @@ +package apiversions + +import ( + "fmt" + "net/http" + "testing" + + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" + "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", client.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(client.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", client.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(client.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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/apiversions/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/apiversions/results.go new file mode 100644 index 00000000000..7b0df115b50 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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.Result +} + +// Extract is a function that accepts a result and extracts an API version resource. +func (r GetResult) Extract() (*APIVersion, error) { + var resp struct { + Version *APIVersion `mapstructure:"version"` + } + + err := mapstructure.Decode(r.Body, &resp) + + return resp.Version, err +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/apiversions/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/apiversions/urls.go new file mode 100644 index 00000000000..56f8260a25c --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/apiversions/urls_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/apiversions/urls_test.go new file mode 100644 index 00000000000..37e91425b5a --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots/doc.go new file mode 100644 index 00000000000..198f83077c5 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots/doc.go @@ -0,0 +1,5 @@ +// Package snapshots provides information and interaction with snapshots in the +// OpenStack Block Storage service. A snapshot is a point in time copy of the +// data contained in an external storage volume, and can be controlled +// programmatically. +package snapshots diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots/fixtures.go new file mode 100644 index 00000000000..d1461fb69d2 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots/fixtures.go @@ -0,0 +1,114 @@ +package snapshots + +import ( + "fmt" + "net/http" + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +func MockListResponse(t *testing.T) { + th.Mux.HandleFunc("/snapshots", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` + { + "snapshots": [ + { + "id": "289da7f8-6440-407c-9fb4-7db01ec49164", + "display_name": "snapshot-001" + }, + { + "id": "96c3bda7-c82a-4f50-be73-ca7621794835", + "display_name": "snapshot-002" + } + ] + } + `) + }) +} + +func MockGetResponse(t *testing.T) { + th.Mux.HandleFunc("/snapshots/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, ` +{ + "snapshot": { + "display_name": "snapshot-001", + "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22" + } +} + `) + }) +} + +func MockCreateResponse(t *testing.T) { + th.Mux.HandleFunc("/snapshots", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "snapshot": { + "volume_id": "1234", + "display_name": "snapshot-001" + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, ` +{ + "snapshot": { + "volume_id": "1234", + "display_name": "snapshot-001", + "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22" + } +} + `) + }) +} + +func MockUpdateMetadataResponse(t *testing.T) { + th.Mux.HandleFunc("/snapshots/123/metadata", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestJSONRequest(t, r, ` + { + "metadata": { + "key": "v1" + } + } + `) + + fmt.Fprintf(w, ` + { + "metadata": { + "key": "v1" + } + } + `) + }) +} + +func MockDeleteResponse(t *testing.T) { + th.Mux.HandleFunc("/snapshots/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusNoContent) + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots/requests.go new file mode 100644 index 00000000000..443f6960575 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots/requests.go @@ -0,0 +1,188 @@ +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.AuthenticatedHeaders(), + OkCodes: []int{200, 201}, + ReqBody: &reqBody, + Results: &res.Body, + }) + return res +} + +// Delete will delete the existing Snapshot with the provided ID. +func Delete(client *gophercloud.ServiceClient, id string) DeleteResult { + var res DeleteResult + _, res.Err = perigee.Request("DELETE", deleteURL(client, id), perigee.Options{ + MoreHeaders: client.AuthenticatedHeaders(), + OkCodes: []int{202, 204}, + }) + return res +} + +// Get retrieves the Snapshot with the provided ID. To extract the Snapshot +// object from the response, call the Extract method on the GetResult. +func Get(client *gophercloud.ServiceClient, id string) GetResult { + var res GetResult + _, res.Err = perigee.Request("GET", getURL(client, id), perigee.Options{ + Results: &res.Body, + MoreHeaders: client.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.PageResult) pagination.Page { + return ListResult{pagination.SinglePageBase(r)} + } + return pagination.NewPager(client, url, createPage) +} + +// UpdateMetadataOptsBuilder allows extensions to add additional parameters to +// the Update request. +type UpdateMetadataOptsBuilder interface { + ToSnapshotUpdateMetadataMap() (map[string]interface{}, error) +} + +// UpdateMetadataOpts contain options for updating an existing Snapshot. This +// object is passed to the snapshots.Update function. For more information +// about the parameters, see the Snapshot object. +type UpdateMetadataOpts struct { + Metadata map[string]interface{} +} + +// ToSnapshotUpdateMetadataMap assembles a request body based on the contents of +// an UpdateMetadataOpts. +func (opts UpdateMetadataOpts) ToSnapshotUpdateMetadataMap() (map[string]interface{}, error) { + v := make(map[string]interface{}) + + if opts.Metadata != nil { + v["metadata"] = opts.Metadata + } + + return v, nil +} + +// UpdateMetadata will update the Snapshot with provided information. To +// extract the updated Snapshot from the response, call the ExtractMetadata +// method on the UpdateMetadataResult. +func UpdateMetadata(client *gophercloud.ServiceClient, id string, opts UpdateMetadataOptsBuilder) UpdateMetadataResult { + var res UpdateMetadataResult + + reqBody, err := opts.ToSnapshotUpdateMetadataMap() + if err != nil { + res.Err = err + return res + } + + _, res.Err = perigee.Request("PUT", updateMetadataURL(client, id), perigee.Options{ + MoreHeaders: client.AuthenticatedHeaders(), + OkCodes: []int{200}, + ReqBody: &reqBody, + Results: &res.Body, + }) + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots/requests_test.go new file mode 100644 index 00000000000..d0f9e887e81 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots/requests_test.go @@ -0,0 +1,104 @@ +package snapshots + +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() + + MockListResponse(t) + + count := 0 + + List(client.ServiceClient(), &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() + + MockGetResponse(t) + + v, err := Get(client.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() + + MockCreateResponse(t) + + options := CreateOpts{VolumeID: "1234", Name: "snapshot-001"} + n, err := Create(client.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() + + MockUpdateMetadataResponse(t) + + expected := map[string]interface{}{"key": "v1"} + + options := &UpdateMetadataOpts{ + Metadata: map[string]interface{}{ + "key": "v1", + }, + } + + actual, err := UpdateMetadata(client.ServiceClient(), "123", options).ExtractMetadata() + + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, actual, expected) +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockDeleteResponse(t) + + res := Delete(client.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22") + th.AssertNoErr(t, res.Err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots/results.go new file mode 100644 index 00000000000..e595798e4ae --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots/results.go @@ -0,0 +1,123 @@ +package snapshots + +import ( + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" + + "github.com/mitchellh/mapstructure" +) + +// Snapshot contains all the information associated with an OpenStack Snapshot. +type Snapshot struct { + // Currect status of the Snapshot. + Status string `mapstructure:"status"` + + // Display name. + Name string `mapstructure:"display_name"` + + // Instances onto which the Snapshot is attached. + Attachments []string `mapstructure:"attachments"` + + // Logical group. + AvailabilityZone string `mapstructure:"availability_zone"` + + // Is the Snapshot bootable? + Bootable string `mapstructure:"bootable"` + + // Date created. + CreatedAt string `mapstructure:"created_at"` + + // Display description. + Description string `mapstructure:"display_discription"` + + // See VolumeType object for more information. + VolumeType string `mapstructure:"volume_type"` + + // ID of the Snapshot from which this Snapshot was created. + SnapshotID string `mapstructure:"snapshot_id"` + + // ID of the Volume from which this Snapshot was created. + VolumeID string `mapstructure:"volume_id"` + + // User-defined key-value pairs. + Metadata map[string]string `mapstructure:"metadata"` + + // Unique identifier. + ID string `mapstructure:"id"` + + // Size of the Snapshot, in GB. + Size int `mapstructure:"size"` +} + +// CreateResult contains the response body and error from a Create request. +type CreateResult struct { + commonResult +} + +// GetResult contains the response body and error from a Get request. +type GetResult struct { + commonResult +} + +// DeleteResult contains the response body and error from a Delete request. +type DeleteResult struct { + gophercloud.ErrResult +} + +// ListResult is a pagination.Pager that is returned from a call to the List function. +type ListResult struct { + pagination.SinglePageBase +} + +// IsEmpty returns true if a ListResult contains no Snapshots. +func (r ListResult) IsEmpty() (bool, error) { + volumes, err := ExtractSnapshots(r) + if err != nil { + return true, err + } + return len(volumes) == 0, nil +} + +// ExtractSnapshots extracts and returns Snapshots. It is used while iterating over a snapshots.List call. +func ExtractSnapshots(page pagination.Page) ([]Snapshot, error) { + var response struct { + Snapshots []Snapshot `json:"snapshots"` + } + + err := mapstructure.Decode(page.(ListResult).Body, &response) + return response.Snapshots, err +} + +// UpdateMetadataResult contains the response body and error from an UpdateMetadata request. +type UpdateMetadataResult struct { + commonResult +} + +// ExtractMetadata returns the metadata from a response from snapshots.UpdateMetadata. +func (r UpdateMetadataResult) ExtractMetadata() (map[string]interface{}, error) { + if r.Err != nil { + return nil, r.Err + } + + m := r.Body.(map[string]interface{})["metadata"] + return m.(map[string]interface{}), nil +} + +type commonResult struct { + gophercloud.Result +} + +// Extract will get the Snapshot object out of the commonResult object. +func (r commonResult) Extract() (*Snapshot, error) { + if r.Err != nil { + return nil, r.Err + } + + var res struct { + Snapshot *Snapshot `json:"snapshot"` + } + + err := mapstructure.Decode(r.Body, &res) + + return res.Snapshot, err +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots/urls.go new file mode 100644 index 00000000000..4d635e8dd45 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots/urls_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots/urls_test.go new file mode 100644 index 00000000000..feacf7f69b5 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots/util.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots/util.go new file mode 100644 index 00000000000..64cdc607ecd --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots/util_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots/util_test.go new file mode 100644 index 00000000000..a4c4c8282e0 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots/util_test.go @@ -0,0 +1,38 @@ +package snapshots + +import ( + "fmt" + "net/http" + "testing" + "time" + + th "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +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(client.ServiceClient(), "1234", "available", 0) + if err == nil { + t.Errorf("Expected error: 'Time Out in WaitFor'") + } + + err = WaitForStatus(client.ServiceClient(), "1234", "available", 3) + th.CheckNoErr(t, err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/doc.go new file mode 100644 index 00000000000..307b8b12d2f --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/doc.go @@ -0,0 +1,5 @@ +// Package volumes provides information and interaction with volumes in the +// OpenStack Block Storage service. A volume is a detachable block storage +// device, akin to a USB hard drive. It can only be attached to one instance at +// a time. +package volumes diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/fixtures.go new file mode 100644 index 00000000000..a01ad05a383 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/fixtures.go @@ -0,0 +1,105 @@ +package volumes + +import ( + "fmt" + "net/http" + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +func MockListResponse(t *testing.T) { + th.Mux.HandleFunc("/volumes", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` + { + "volumes": [ + { + "id": "289da7f8-6440-407c-9fb4-7db01ec49164", + "display_name": "vol-001" + }, + { + "id": "96c3bda7-c82a-4f50-be73-ca7621794835", + "display_name": "vol-002" + } + ] + } + `) + }) +} + +func MockGetResponse(t *testing.T) { + th.Mux.HandleFunc("/volumes/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, ` +{ + "volume": { + "display_name": "vol-001", + "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22" + } +} + `) + }) +} + +func MockCreateResponse(t *testing.T) { + th.Mux.HandleFunc("/volumes", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "volume": { + "size": 75 + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, ` +{ + "volume": { + "size": 4, + "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22" + } +} + `) + }) +} + +func MockDeleteResponse(t *testing.T) { + th.Mux.HandleFunc("/volumes/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusNoContent) + }) +} + +func MockUpdateResponse(t *testing.T) { + th.Mux.HandleFunc("/volumes/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, ` + { + "volume": { + "display_name": "vol-002", + "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22" + } + } + `) + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/requests.go new file mode 100644 index 00000000000..f4332de6578 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/requests.go @@ -0,0 +1,217 @@ +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.AuthenticatedHeaders(), + ReqBody: &reqBody, + Results: &res.Body, + OkCodes: []int{200, 201}, + }) + return res +} + +// Delete will delete the existing Volume with the provided ID. +func Delete(client *gophercloud.ServiceClient, id string) DeleteResult { + var res DeleteResult + _, res.Err = perigee.Request("DELETE", deleteURL(client, id), perigee.Options{ + MoreHeaders: client.AuthenticatedHeaders(), + OkCodes: []int{202, 204}, + }) + return res +} + +// Get retrieves the Volume with the provided ID. To extract the Volume object +// from the response, call the Extract method on the GetResult. +func Get(client *gophercloud.ServiceClient, id string) GetResult { + var res GetResult + _, res.Err = perigee.Request("GET", getURL(client, id), perigee.Options{ + Results: &res.Body, + MoreHeaders: client.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.PageResult) pagination.Page { + return ListResult{pagination.SinglePageBase(r)} + } + return pagination.NewPager(client, url, createPage) +} + +// UpdateOptsBuilder allows extensions to add additional parameters to the +// Update request. +type UpdateOptsBuilder interface { + ToVolumeUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts contain options for updating an existing Volume. This object is passed +// to the volumes.Update function. For more information about the parameters, see +// the Volume object. +type UpdateOpts struct { + // OPTIONAL + Name string + // OPTIONAL + Description string + // OPTIONAL + Metadata map[string]string +} + +// ToVolumeUpdateMap assembles a request body based on the contents of an +// UpdateOpts. +func (opts UpdateOpts) ToVolumeUpdateMap() (map[string]interface{}, error) { + v := make(map[string]interface{}) + + if opts.Description != "" { + v["display_description"] = opts.Description + } + if opts.Metadata != nil { + v["metadata"] = opts.Metadata + } + if opts.Name != "" { + v["display_name"] = opts.Name + } + + return map[string]interface{}{"volume": v}, nil +} + +// Update will update the Volume with provided information. To extract the updated +// Volume from the response, call the Extract method on the UpdateResult. +func Update(client *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) UpdateResult { + var res UpdateResult + + reqBody, err := opts.ToVolumeUpdateMap() + if err != nil { + res.Err = err + return res + } + + _, res.Err = perigee.Request("PUT", updateURL(client, id), perigee.Options{ + MoreHeaders: client.AuthenticatedHeaders(), + OkCodes: []int{200}, + ReqBody: &reqBody, + Results: &res.Body, + }) + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/requests_test.go new file mode 100644 index 00000000000..067f89bdd93 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/requests_test.go @@ -0,0 +1,95 @@ +package volumes + +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() + + MockListResponse(t) + + count := 0 + + List(client.ServiceClient(), &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() + + MockGetResponse(t) + + v, err := Get(client.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() + + MockCreateResponse(t) + + options := &CreateOpts{Size: 75} + n, err := Create(client.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() + + MockDeleteResponse(t) + + res := Delete(client.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22") + th.AssertNoErr(t, res.Err) +} + +func TestUpdate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockUpdateResponse(t) + + options := UpdateOpts{Name: "vol-002"} + v, err := Update(client.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22", options).Extract() + th.AssertNoErr(t, err) + th.CheckEquals(t, "vol-002", v.Name) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/results.go new file mode 100644 index 00000000000..c6ddbb5167f --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/results.go @@ -0,0 +1,113 @@ +package volumes + +import ( + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" + + "github.com/mitchellh/mapstructure" +) + +// Volume contains all the information associated with an OpenStack Volume. +type Volume struct { + // Current status of the volume. + Status string `mapstructure:"status"` + + // Human-readable display name for the volume. + Name string `mapstructure:"display_name"` + + // Instances onto which the volume is attached. + Attachments []string `mapstructure:"attachments"` + + // This parameter is no longer used. + AvailabilityZone string `mapstructure:"availability_zone"` + + // Indicates whether this is a bootable volume. + Bootable string `mapstructure:"bootable"` + + // The date when this volume was created. + CreatedAt string `mapstructure:"created_at"` + + // Human-readable description for the volume. + Description string `mapstructure:"display_discription"` + + // The type of volume to create, either SATA or SSD. + VolumeType string `mapstructure:"volume_type"` + + // The ID of the snapshot from which the volume was created + SnapshotID string `mapstructure:"snapshot_id"` + + // The ID of another block storage volume from which the current volume was created + SourceVolID string `mapstructure:"source_volid"` + + // Arbitrary key-value pairs defined by the user. + Metadata map[string]string `mapstructure:"metadata"` + + // Unique identifier for the volume. + ID string `mapstructure:"id"` + + // Size of the volume in GB. + Size int `mapstructure:"size"` +} + +// CreateResult contains the response body and error from a Create request. +type CreateResult struct { + commonResult +} + +// GetResult contains the response body and error from a Get request. +type GetResult struct { + commonResult +} + +// DeleteResult contains the response body and error from a Delete request. +type DeleteResult struct { + gophercloud.ErrResult +} + +// ListResult is a pagination.pager that is returned from a call to the List function. +type ListResult struct { + pagination.SinglePageBase +} + +// IsEmpty returns true if a ListResult contains no Volumes. +func (r ListResult) IsEmpty() (bool, error) { + volumes, err := ExtractVolumes(r) + if err != nil { + return true, err + } + return len(volumes) == 0, nil +} + +// ExtractVolumes extracts and returns Volumes. It is used while iterating over a volumes.List call. +func ExtractVolumes(page pagination.Page) ([]Volume, error) { + var response struct { + Volumes []Volume `json:"volumes"` + } + + err := mapstructure.Decode(page.(ListResult).Body, &response) + return response.Volumes, err +} + +// UpdateResult contains the response body and error from an Update request. +type UpdateResult struct { + commonResult +} + +type commonResult struct { + gophercloud.Result +} + +// Extract will get the Volume object out of the commonResult object. +func (r commonResult) Extract() (*Volume, error) { + if r.Err != nil { + return nil, r.Err + } + + var res struct { + Volume *Volume `json:"volume"` + } + + err := mapstructure.Decode(r.Body, &res) + + return res.Volume, err +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/urls.go new file mode 100644 index 00000000000..29629a1af8d --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/urls_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/urls_test.go new file mode 100644 index 00000000000..a95270e14cb --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/util.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/util.go new file mode 100644 index 00000000000..1dda695ea09 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/util_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/util_test.go new file mode 100644 index 00000000000..24ef3b6190c --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/util_test.go @@ -0,0 +1,38 @@ +package volumes + +import ( + "fmt" + "net/http" + "testing" + "time" + + th "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestWaitForStatus(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/volumes/1234", func(w http.ResponseWriter, r *http.Request) { + time.Sleep(3 * 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(client.ServiceClient(), "1234", "available", 0) + if err == nil { + t.Errorf("Expected error: 'Time Out in WaitFor'") + } + + err = WaitForStatus(client.ServiceClient(), "1234", "available", 6) + th.CheckNoErr(t, err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumetypes/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumetypes/doc.go new file mode 100644 index 00000000000..793084f89b2 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumetypes/doc.go @@ -0,0 +1,9 @@ +// Package volumetypes provides information and interaction with volume types +// in the OpenStack Block Storage service. A volume type indicates the type of +// a block storage volume, such as SATA, SCSCI, SSD, etc. These can be +// customized or defined by the OpenStack admin. +// +// You can also define extra_specs associated with your volume types. For +// instance, you could have a VolumeType=SATA, with extra_specs (RPM=10000, +// RAID-Level=5) . Extra_specs are defined and customized by the admin. +package volumetypes diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumetypes/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumetypes/fixtures.go new file mode 100644 index 00000000000..e3326eae146 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumetypes/fixtures.go @@ -0,0 +1,60 @@ +package volumetypes + +import ( + "fmt" + "net/http" + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +func MockListResponse(t *testing.T) { + th.Mux.HandleFunc("/types", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` + { + "volume_types": [ + { + "id": "289da7f8-6440-407c-9fb4-7db01ec49164", + "name": "vol-type-001", + "extra_specs": { + "capabilities": "gpu" + } + }, + { + "id": "96c3bda7-c82a-4f50-be73-ca7621794835", + "name": "vol-type-002", + "extra_specs": {} + } + ] + } + `) + }) +} + +func MockGetResponse(t *testing.T) { + th.Mux.HandleFunc("/types/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, ` +{ + "volume_type": { + "name": "vol-type-001", + "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22", + "extra_specs": { + "serverNumber": "2" + } + } +} + `) + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumetypes/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumetypes/requests.go new file mode 100644 index 00000000000..87e20f60037 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumetypes/requests.go @@ -0,0 +1,87 @@ +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.AuthenticatedHeaders(), + OkCodes: []int{200, 201}, + ReqBody: &reqBody, + Results: &res.Body, + }) + return res +} + +// Delete will delete the volume type with the provided ID. +func Delete(client *gophercloud.ServiceClient, id string) DeleteResult { + var res DeleteResult + _, res.Err = perigee.Request("DELETE", deleteURL(client, id), perigee.Options{ + MoreHeaders: client.AuthenticatedHeaders(), + OkCodes: []int{202}, + }) + return res +} + +// Get will retrieve the volume type with the provided ID. To extract the volume +// type from the result, call the Extract method on the GetResult. +func Get(client *gophercloud.ServiceClient, id string) GetResult { + var res GetResult + _, err := perigee.Request("GET", getURL(client, id), perigee.Options{ + MoreHeaders: client.AuthenticatedHeaders(), + OkCodes: []int{200}, + Results: &res.Body, + }) + res.Err = err + return res +} + +// List returns all volume types. +func List(client *gophercloud.ServiceClient) pagination.Pager { + createPage := func(r pagination.PageResult) pagination.Page { + return ListResult{pagination.SinglePageBase(r)} + } + + return pagination.NewPager(client, listURL(client), createPage) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumetypes/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumetypes/requests_test.go new file mode 100644 index 00000000000..8d40bfe1d48 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumetypes/requests_test.go @@ -0,0 +1,118 @@ +package volumetypes + +import ( + "fmt" + "net/http" + "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() + + MockListResponse(t) + + count := 0 + + List(client.ServiceClient()).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() + + MockGetResponse(t) + + vt, err := Get(client.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", client.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(client.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", client.TokenID) + w.WriteHeader(http.StatusAccepted) + }) + + err := Delete(client.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22").ExtractErr() + th.AssertNoErr(t, err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumetypes/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumetypes/results.go new file mode 100644 index 00000000000..c049a045d8c --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumetypes/results.go @@ -0,0 +1,72 @@ +package volumetypes + +import ( + "github.com/mitchellh/mapstructure" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// VolumeType contains all information associated with an OpenStack Volume Type. +type VolumeType struct { + ExtraSpecs map[string]interface{} `json:"extra_specs" mapstructure:"extra_specs"` // user-defined metadata + ID string `json:"id" mapstructure:"id"` // unique identifier + Name string `json:"name" mapstructure:"name"` // display name +} + +// CreateResult contains the response body and error from a Create request. +type CreateResult struct { + commonResult +} + +// GetResult contains the response body and error from a Get request. +type GetResult struct { + commonResult +} + +// DeleteResult contains the response error from a Delete request. +type DeleteResult struct { + gophercloud.ErrResult +} + +// ListResult is a pagination.Pager that is returned from a call to the List function. +type ListResult struct { + pagination.SinglePageBase +} + +// IsEmpty returns true if a ListResult contains no Volume Types. +func (r ListResult) IsEmpty() (bool, error) { + volumeTypes, err := ExtractVolumeTypes(r) + if err != nil { + return true, err + } + return len(volumeTypes) == 0, nil +} + +// ExtractVolumeTypes extracts and returns Volume Types. +func ExtractVolumeTypes(page pagination.Page) ([]VolumeType, error) { + var response struct { + VolumeTypes []VolumeType `mapstructure:"volume_types"` + } + + err := mapstructure.Decode(page.(ListResult).Body, &response) + return response.VolumeTypes, err +} + +type commonResult struct { + gophercloud.Result +} + +// Extract will get the Volume Type object out of the commonResult object. +func (r commonResult) Extract() (*VolumeType, error) { + if r.Err != nil { + return nil, r.Err + } + + var res struct { + VolumeType *VolumeType `json:"volume_type" mapstructure:"volume_type"` + } + + err := mapstructure.Decode(r.Body, &res) + + return res.VolumeType, err +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumetypes/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumetypes/urls.go new file mode 100644 index 00000000000..cf8367bfab1 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumetypes/urls_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumetypes/urls_test.go new file mode 100644 index 00000000000..44016e29549 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/client.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/client.go new file mode 100644 index 00000000000..99b3d466d39 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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{ + ProviderClient: client, + Endpoint: v2Endpoint, + } +} + +// NewIdentityV3 creates a ServiceClient that may be used to access the v3 identity service. +func NewIdentityV3(client *gophercloud.ProviderClient) *gophercloud.ServiceClient { + v3Endpoint := client.IdentityBase + "v3/" + + return &gophercloud.ServiceClient{ + ProviderClient: client, + Endpoint: v3Endpoint, + } +} + +// NewObjectStorageV1 creates a ServiceClient that may be used with the v1 object storage package. +func NewObjectStorageV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + eo.ApplyDefaults("object-store") + url, err := client.EndpointLocator(eo) + if err != nil { + return nil, err + } + return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil +} + +// NewComputeV2 creates a ServiceClient that may be used with the v2 compute package. +func NewComputeV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + eo.ApplyDefaults("compute") + url, err := client.EndpointLocator(eo) + if err != nil { + return nil, err + } + return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil +} + +// NewNetworkV2 creates a ServiceClient that may be used with the v2 network package. +func NewNetworkV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + eo.ApplyDefaults("network") + url, err := client.EndpointLocator(eo) + if err != nil { + return nil, err + } + return &gophercloud.ServiceClient{ + ProviderClient: client, + Endpoint: url, + ResourceBase: url + "v2.0/", + }, nil +} + +// NewBlockStorageV1 creates a ServiceClient that may be used to access the v1 block storage service. +func NewBlockStorageV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + eo.ApplyDefaults("volume") + url, err := client.EndpointLocator(eo) + if err != nil { + return nil, err + } + return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/client_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/client_test.go new file mode 100644 index 00000000000..257260c4e19 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/common/README.md b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/common/README.md new file mode 100644 index 00000000000..7b55795d08e --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/common/README.md @@ -0,0 +1,3 @@ +# Common Resources + +This directory is for resources that are shared by multiple services. diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/common/extensions/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/common/extensions/doc.go new file mode 100644 index 00000000000..4a168f4b2c8 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/common/extensions/errors.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/common/extensions/errors.go new file mode 100644 index 00000000000..aeec0fa756e --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/common/extensions/errors.go @@ -0,0 +1 @@ +package extensions diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/common/extensions/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/common/extensions/fixtures.go new file mode 100644 index 00000000000..0ed7de9f1d7 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/common/extensions/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/common/extensions/requests.go new file mode 100644 index 00000000000..3ca6e12f195 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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.AuthenticatedHeaders(), + Results: &res.Body, + 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.PageResult) pagination.Page { + return ExtensionPage{pagination.SinglePageBase(r)} + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/common/extensions/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/common/extensions/requests_test.go new file mode 100644 index 00000000000..6550283df71 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/common/extensions/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/common/extensions/results.go new file mode 100644 index 00000000000..777d083fa07 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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.Result +} + +// Extract interprets a GetResult as an Extension. +func (r GetResult) Extract() (*Extension, error) { + if r.Err != nil { + return nil, r.Err + } + + var res struct { + Extension *Extension `json:"extension"` + } + + err := mapstructure.Decode(r.Body, &res) + + return res.Extension, err +} + +// Extension is a struct that represents an OpenStack extension. +type Extension struct { + Updated string `json:"updated" mapstructure:"updated"` + Name string `json:"name" mapstructure:"name"` + Links []interface{} `json:"links" mapstructure:"links"` + Namespace string `json:"namespace" mapstructure:"namespace"` + Alias string `json:"alias" mapstructure:"alias"` + Description string `json:"description" mapstructure:"description"` +} + +// ExtensionPage is the page returned by a pager when traversing over a collection of extensions. +type ExtensionPage struct { + pagination.SinglePageBase +} + +// IsEmpty checks whether an ExtensionPage struct is empty. +func (r ExtensionPage) IsEmpty() (bool, error) { + is, err := ExtractExtensions(r) + if err != nil { + return true, err + } + return len(is) == 0, nil +} + +// ExtractExtensions accepts a Page struct, specifically an ExtensionPage struct, and extracts the +// elements into a slice of Extension structs. +// In other words, a generic collection is mapped into a relevant slice. +func ExtractExtensions(page pagination.Page) ([]Extension, error) { + var resp struct { + Extensions []Extension `mapstructure:"extensions"` + } + + err := mapstructure.Decode(page.(ExtensionPage).Body, &resp) + + return resp.Extensions, err +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/common/extensions/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/common/extensions/urls.go new file mode 100644 index 00000000000..6460c66bc01 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/common/extensions/urls_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/common/extensions/urls_test.go new file mode 100644 index 00000000000..3223b1ca8b0 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/bootfromvolume/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/bootfromvolume/requests.go new file mode 100644 index 00000000000..5a976d11468 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/bootfromvolume/requests.go @@ -0,0 +1,111 @@ +package bootfromvolume + +import ( + "errors" + "strconv" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/openstack/compute/v2/servers" + + "github.com/racker/perigee" +) + +// SourceType represents the type of medium being used to create the volume. +type SourceType string + +const ( + Volume SourceType = "volume" + Snapshot SourceType = "snapshot" + Image SourceType = "image" +) + +// BlockDevice is a structure with options for booting a server instance +// from a volume. The volume may be created from an image, snapshot, or another +// volume. +type BlockDevice struct { + // BootIndex [optional] is the boot index. It defaults to 0. + BootIndex int `json:"boot_index"` + + // DeleteOnTermination [optional] specifies whether or not to delete the attached volume + // when the server is deleted. Defaults to `false`. + DeleteOnTermination bool `json:"delete_on_termination"` + + // DestinationType [optional] is the type that gets created. Possible values are "volume" + // and "local". + DestinationType string `json:"destination_type"` + + // SourceType [required] must be one of: "volume", "snapshot", "image". + SourceType SourceType `json:"source_type"` + + // UUID [required] is the unique identifier for the volume, snapshot, or image (see above) + UUID string `json:"uuid"` + + // VolumeSize [optional] is the size of the volume to create (in gigabytes). + VolumeSize int `json:"volume_size"` +} + +// CreateOptsExt is a structure that extends the server `CreateOpts` structure +// by allowing for a block device mapping. +type CreateOptsExt struct { + servers.CreateOptsBuilder + BlockDevice []BlockDevice `json:"block_device_mapping_v2,omitempty"` +} + +// ToServerCreateMap adds the block device mapping option to the base server +// creation options. +func (opts CreateOptsExt) ToServerCreateMap() (map[string]interface{}, error) { + base, err := opts.CreateOptsBuilder.ToServerCreateMap() + if err != nil { + return nil, err + } + + if len(opts.BlockDevice) == 0 { + return nil, errors.New("Required fields UUID and SourceType not set.") + } + + serverMap := base["server"].(map[string]interface{}) + + blockDevice := make([]map[string]interface{}, len(opts.BlockDevice)) + + for i, bd := range opts.BlockDevice { + if string(bd.SourceType) == "" { + return nil, errors.New("SourceType must be one of: volume, image, snapshot.") + } + + blockDevice[i] = make(map[string]interface{}) + + blockDevice[i]["source_type"] = bd.SourceType + blockDevice[i]["boot_index"] = strconv.Itoa(bd.BootIndex) + blockDevice[i]["delete_on_termination"] = strconv.FormatBool(bd.DeleteOnTermination) + blockDevice[i]["volume_size"] = strconv.Itoa(bd.VolumeSize) + if bd.UUID != "" { + blockDevice[i]["uuid"] = bd.UUID + } + if bd.DestinationType != "" { + blockDevice[i]["destination_type"] = bd.DestinationType + } + + } + serverMap["block_device_mapping_v2"] = blockDevice + + return base, nil +} + +// Create requests the creation of a server from the given block device mapping. +func Create(client *gophercloud.ServiceClient, opts servers.CreateOptsBuilder) servers.CreateResult { + var res servers.CreateResult + + reqBody, err := opts.ToServerCreateMap() + if err != nil { + res.Err = err + return res + } + + _, res.Err = perigee.Request("POST", createURL(client), perigee.Options{ + MoreHeaders: client.AuthenticatedHeaders(), + ReqBody: reqBody, + Results: &res.Body, + OkCodes: []int{200, 202}, + }) + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/bootfromvolume/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/bootfromvolume/requests_test.go new file mode 100644 index 00000000000..5bf9137906c --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/bootfromvolume/requests_test.go @@ -0,0 +1,51 @@ +package bootfromvolume + +import ( + "testing" + + "github.com/rackspace/gophercloud/openstack/compute/v2/servers" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestCreateOpts(t *testing.T) { + base := servers.CreateOpts{ + Name: "createdserver", + ImageRef: "asdfasdfasdf", + FlavorRef: "performance1-1", + } + + ext := CreateOptsExt{ + CreateOptsBuilder: base, + BlockDevice: []BlockDevice{ + BlockDevice{ + UUID: "123456", + SourceType: Image, + DestinationType: "volume", + VolumeSize: 10, + }, + }, + } + + expected := ` + { + "server": { + "name": "createdserver", + "imageRef": "asdfasdfasdf", + "flavorRef": "performance1-1", + "block_device_mapping_v2":[ + { + "uuid":"123456", + "source_type":"image", + "destination_type":"volume", + "boot_index": "0", + "delete_on_termination": "false", + "volume_size": "10" + } + ] + } + } + ` + actual, err := ext.ToServerCreateMap() + th.AssertNoErr(t, err) + th.CheckJSONEquals(t, expected, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/bootfromvolume/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/bootfromvolume/results.go new file mode 100644 index 00000000000..f60329f0f38 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/bootfromvolume/results.go @@ -0,0 +1,10 @@ +package bootfromvolume + +import ( + os "github.com/rackspace/gophercloud/openstack/compute/v2/servers" +) + +// CreateResult temporarily contains the response from a Create call. +type CreateResult struct { + os.CreateResult +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/bootfromvolume/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/bootfromvolume/urls.go new file mode 100644 index 00000000000..0cffe25ffdc --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/bootfromvolume/urls.go @@ -0,0 +1,7 @@ +package bootfromvolume + +import "github.com/rackspace/gophercloud" + +func createURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("os-volumes_boot") +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/bootfromvolume/urls_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/bootfromvolume/urls_test.go new file mode 100644 index 00000000000..6ee647732d4 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/bootfromvolume/urls_test.go @@ -0,0 +1,16 @@ +package bootfromvolume + +import ( + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestCreateURL(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + c := client.ServiceClient() + + th.CheckEquals(t, c.Endpoint+"os-volumes_boot", createURL(c)) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/delegate.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/delegate.go new file mode 100644 index 00000000000..10079097b62 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/delegate.go @@ -0,0 +1,23 @@ +package extensions + +import ( + "github.com/rackspace/gophercloud" + common "github.com/rackspace/gophercloud/openstack/common/extensions" + "github.com/rackspace/gophercloud/pagination" +) + +// ExtractExtensions interprets a Page as a slice of Extensions. +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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/delegate_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/delegate_test.go new file mode 100644 index 00000000000..c3c525fa20e --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/delegate_test.go @@ -0,0 +1,96 @@ +package extensions + +import ( + "fmt" + "net/http" + "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() + + 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": [ + { + "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(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractExtensions(page) + th.AssertNoErr(t, err) + + expected := []common.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 + }) + th.CheckEquals(t, 1, count) +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + 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, ` +{ + "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(client.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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/diskconfig/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/diskconfig/doc.go new file mode 100644 index 00000000000..80785faca9f --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/diskconfig/doc.go @@ -0,0 +1,3 @@ +// Package diskconfig provides information and interaction with the Disk +// Config extension that works with the OpenStack Compute service. +package diskconfig diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/diskconfig/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/diskconfig/requests.go new file mode 100644 index 00000000000..7407e0d175e --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/diskconfig/requests.go @@ -0,0 +1,114 @@ +package diskconfig + +import ( + "errors" + + "github.com/rackspace/gophercloud/openstack/compute/v2/servers" +) + +// DiskConfig represents one of the two possible settings for the DiskConfig option when creating, +// rebuilding, or resizing servers: Auto or Manual. +type DiskConfig string + +const ( + // Auto builds a server with a single partition the size of the target flavor disk and + // automatically adjusts the filesystem to fit the entire partition. Auto may only be used with + // images and servers that use a single EXT3 partition. + Auto DiskConfig = "AUTO" + + // Manual builds a server using whatever partition scheme and filesystem are present in the source + // image. If the target flavor disk is larger, the remaining space is left unpartitioned. This + // enables images to have non-EXT3 filesystems, multiple partitions, and so on, and enables you + // to manage the disk configuration. It also results in slightly shorter boot times. + Manual DiskConfig = "MANUAL" +) + +// ErrInvalidDiskConfig is returned if an invalid string is specified for a DiskConfig option. +var ErrInvalidDiskConfig = errors.New("DiskConfig must be either diskconfig.Auto or diskconfig.Manual.") + +// Validate ensures that a DiskConfig contains an appropriate value. +func (config DiskConfig) validate() error { + switch config { + case Auto, Manual: + return nil + default: + return ErrInvalidDiskConfig + } +} + +// CreateOptsExt adds a DiskConfig option to the base CreateOpts. +type CreateOptsExt struct { + servers.CreateOptsBuilder + + // DiskConfig [optional] controls how the created server's disk is partitioned. + DiskConfig DiskConfig `json:"OS-DCF:diskConfig,omitempty"` +} + +// ToServerCreateMap adds the diskconfig option to the base server creation options. +func (opts CreateOptsExt) ToServerCreateMap() (map[string]interface{}, error) { + base, err := opts.CreateOptsBuilder.ToServerCreateMap() + if err != nil { + return nil, err + } + + if string(opts.DiskConfig) == "" { + return base, nil + } + + serverMap := base["server"].(map[string]interface{}) + serverMap["OS-DCF:diskConfig"] = string(opts.DiskConfig) + + return base, nil +} + +// RebuildOptsExt adds a DiskConfig option to the base RebuildOpts. +type RebuildOptsExt struct { + servers.RebuildOptsBuilder + + // DiskConfig [optional] controls how the rebuilt server's disk is partitioned. + DiskConfig DiskConfig +} + +// ToServerRebuildMap adds the diskconfig option to the base server rebuild options. +func (opts RebuildOptsExt) ToServerRebuildMap() (map[string]interface{}, error) { + err := opts.DiskConfig.validate() + if err != nil { + return nil, err + } + + base, err := opts.RebuildOptsBuilder.ToServerRebuildMap() + if err != nil { + return nil, err + } + + serverMap := base["rebuild"].(map[string]interface{}) + serverMap["OS-DCF:diskConfig"] = string(opts.DiskConfig) + + return base, nil +} + +// ResizeOptsExt adds a DiskConfig option to the base server resize options. +type ResizeOptsExt struct { + servers.ResizeOptsBuilder + + // DiskConfig [optional] controls how the resized server's disk is partitioned. + DiskConfig DiskConfig +} + +// ToServerResizeMap adds the diskconfig option to the base server creation options. +func (opts ResizeOptsExt) ToServerResizeMap() (map[string]interface{}, error) { + err := opts.DiskConfig.validate() + if err != nil { + return nil, err + } + + base, err := opts.ResizeOptsBuilder.ToServerResizeMap() + if err != nil { + return nil, err + } + + serverMap := base["resize"].(map[string]interface{}) + serverMap["OS-DCF:diskConfig"] = string(opts.DiskConfig) + + return base, nil +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/diskconfig/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/diskconfig/requests_test.go new file mode 100644 index 00000000000..e3c26d49a55 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/diskconfig/requests_test.go @@ -0,0 +1,87 @@ +package diskconfig + +import ( + "testing" + + "github.com/rackspace/gophercloud/openstack/compute/v2/servers" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestCreateOpts(t *testing.T) { + base := servers.CreateOpts{ + Name: "createdserver", + ImageRef: "asdfasdfasdf", + FlavorRef: "performance1-1", + } + + ext := CreateOptsExt{ + CreateOptsBuilder: base, + DiskConfig: Manual, + } + + expected := ` + { + "server": { + "name": "createdserver", + "imageRef": "asdfasdfasdf", + "flavorRef": "performance1-1", + "OS-DCF:diskConfig": "MANUAL" + } + } + ` + actual, err := ext.ToServerCreateMap() + th.AssertNoErr(t, err) + th.CheckJSONEquals(t, expected, actual) +} + +func TestRebuildOpts(t *testing.T) { + base := servers.RebuildOpts{ + Name: "rebuiltserver", + AdminPass: "swordfish", + ImageID: "asdfasdfasdf", + } + + ext := RebuildOptsExt{ + RebuildOptsBuilder: base, + DiskConfig: Auto, + } + + actual, err := ext.ToServerRebuildMap() + th.AssertNoErr(t, err) + + expected := ` + { + "rebuild": { + "name": "rebuiltserver", + "imageRef": "asdfasdfasdf", + "adminPass": "swordfish", + "OS-DCF:diskConfig": "AUTO" + } + } + ` + th.CheckJSONEquals(t, expected, actual) +} + +func TestResizeOpts(t *testing.T) { + base := servers.ResizeOpts{ + FlavorRef: "performance1-8", + } + + ext := ResizeOptsExt{ + ResizeOptsBuilder: base, + DiskConfig: Auto, + } + + actual, err := ext.ToServerResizeMap() + th.AssertNoErr(t, err) + + expected := ` + { + "resize": { + "flavorRef": "performance1-8", + "OS-DCF:diskConfig": "AUTO" + } + } + ` + th.CheckJSONEquals(t, expected, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/diskconfig/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/diskconfig/results.go new file mode 100644 index 00000000000..10ec2dafcb8 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/diskconfig/results.go @@ -0,0 +1,60 @@ +package diskconfig + +import ( + "github.com/mitchellh/mapstructure" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/openstack/compute/v2/servers" + "github.com/rackspace/gophercloud/pagination" +) + +func commonExtract(result gophercloud.Result) (*DiskConfig, error) { + var resp struct { + Server struct { + DiskConfig string `mapstructure:"OS-DCF:diskConfig"` + } `mapstructure:"server"` + } + + err := mapstructure.Decode(result.Body, &resp) + if err != nil { + return nil, err + } + + config := DiskConfig(resp.Server.DiskConfig) + return &config, nil +} + +// ExtractGet returns the disk configuration from a servers.Get call. +func ExtractGet(result servers.GetResult) (*DiskConfig, error) { + return commonExtract(result.Result) +} + +// ExtractUpdate returns the disk configuration from a servers.Update call. +func ExtractUpdate(result servers.UpdateResult) (*DiskConfig, error) { + return commonExtract(result.Result) +} + +// ExtractRebuild returns the disk configuration from a servers.Rebuild call. +func ExtractRebuild(result servers.RebuildResult) (*DiskConfig, error) { + return commonExtract(result.Result) +} + +// ExtractDiskConfig returns the DiskConfig setting for a specific server acquired from an +// servers.ExtractServers call, while iterating through a Pager. +func ExtractDiskConfig(page pagination.Page, index int) (*DiskConfig, error) { + casted := page.(servers.ServerPage).Body + + type server struct { + DiskConfig string `mapstructure:"OS-DCF:diskConfig"` + } + var response struct { + Servers []server `mapstructure:"servers"` + } + + err := mapstructure.Decode(casted, &response) + if err != nil { + return nil, err + } + + config := DiskConfig(response.Servers[index].DiskConfig) + return &config, nil +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/diskconfig/results_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/diskconfig/results_test.go new file mode 100644 index 00000000000..dd8d2b7dfa7 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/diskconfig/results_test.go @@ -0,0 +1,68 @@ +package diskconfig + +import ( + "testing" + + "github.com/rackspace/gophercloud/openstack/compute/v2/servers" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestExtractGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + servers.HandleServerGetSuccessfully(t) + + config, err := ExtractGet(servers.Get(client.ServiceClient(), "1234asdf")) + th.AssertNoErr(t, err) + th.CheckEquals(t, Manual, *config) +} + +func TestExtractUpdate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + servers.HandleServerUpdateSuccessfully(t) + + r := servers.Update(client.ServiceClient(), "1234asdf", servers.UpdateOpts{ + Name: "new-name", + }) + config, err := ExtractUpdate(r) + th.AssertNoErr(t, err) + th.CheckEquals(t, Manual, *config) +} + +func TestExtractRebuild(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + servers.HandleRebuildSuccessfully(t, servers.SingleServerBody) + + r := servers.Rebuild(client.ServiceClient(), "1234asdf", servers.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", + }) + config, err := ExtractRebuild(r) + th.AssertNoErr(t, err) + th.CheckEquals(t, Manual, *config) +} + +func TestExtractList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + servers.HandleServerListSuccessfully(t) + + pages := 0 + err := servers.List(client.ServiceClient(), nil).EachPage(func(page pagination.Page) (bool, error) { + pages++ + + config, err := ExtractDiskConfig(page, 0) + th.AssertNoErr(t, err) + th.CheckEquals(t, Manual, *config) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, pages, 1) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/doc.go new file mode 100644 index 00000000000..2b447da1d65 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/doc.go @@ -0,0 +1,3 @@ +// Package extensions provides information and interaction with the +// different extensions available for the OpenStack Compute service. +package extensions diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs/doc.go new file mode 100644 index 00000000000..856f41bacc9 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs/doc.go @@ -0,0 +1,3 @@ +// Package keypairs provides information and interaction with the Keypairs +// extension for the OpenStack Compute service. +package keypairs diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs/fixtures.go new file mode 100644 index 00000000000..d10af99d0eb --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs/fixtures.go @@ -0,0 +1,171 @@ +// +build fixtures + +package keypairs + +import ( + "fmt" + "net/http" + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +// ListOutput is a sample response to a List call. +const ListOutput = ` +{ + "keypairs": [ + { + "keypair": { + "fingerprint": "15:b0:f8:b3:f9:48:63:71:cf:7b:5b:38:6d:44:2d:4a", + "name": "firstkey", + "public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC+Eo/RZRngaGTkFs7I62ZjsIlO79KklKbMXi8F+KITD4bVQHHn+kV+4gRgkgCRbdoDqoGfpaDFs877DYX9n4z6FrAIZ4PES8TNKhatifpn9NdQYWA+IkU8CuvlEKGuFpKRi/k7JLos/gHi2hy7QUwgtRvcefvD/vgQZOVw/mGR9Q== Generated by Nova\n" + } + }, + { + "keypair": { + "fingerprint": "35:9d:d0:c3:4a:80:d3:d8:86:f1:ca:f7:df:c4:f9:d8", + "name": "secondkey", + "public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC9mC3WZN9UGLxgPBpP7H5jZMc6pKwOoSgre8yun6REFktn/Kz7DUt9jaR1UJyRzHxITfCfAIgSxPdGqB/oF1suMyWgu5i0625vavLB5z5kC8Hq3qZJ9zJO1poE1kyD+htiTtPWJ88e12xuH2XB/CZN9OpEiF98hAagiOE0EnOS5Q== Generated by Nova\n" + } + } + ] +} +` + +// GetOutput is a sample response to a Get call. +const GetOutput = ` +{ + "keypair": { + "public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC+Eo/RZRngaGTkFs7I62ZjsIlO79KklKbMXi8F+KITD4bVQHHn+kV+4gRgkgCRbdoDqoGfpaDFs877DYX9n4z6FrAIZ4PES8TNKhatifpn9NdQYWA+IkU8CuvlEKGuFpKRi/k7JLos/gHi2hy7QUwgtRvcefvD/vgQZOVw/mGR9Q== Generated by Nova\n", + "name": "firstkey", + "fingerprint": "15:b0:f8:b3:f9:48:63:71:cf:7b:5b:38:6d:44:2d:4a" + } +} +` + +// CreateOutput is a sample response to a Create call. +const CreateOutput = ` +{ + "keypair": { + "fingerprint": "35:9d:d0:c3:4a:80:d3:d8:86:f1:ca:f7:df:c4:f9:d8", + "name": "createdkey", + "private_key": "-----BEGIN RSA PRIVATE KEY-----\nMIICXAIBAAKBgQC9mC3WZN9UGLxgPBpP7H5jZMc6pKwOoSgre8yun6REFktn/Kz7\nDUt9jaR1UJyRzHxITfCfAIgSxPdGqB/oF1suMyWgu5i0625vavLB5z5kC8Hq3qZJ\n9zJO1poE1kyD+htiTtPWJ88e12xuH2XB/CZN9OpEiF98hAagiOE0EnOS5QIDAQAB\nAoGAE5XO1mDhORy9COvsg+kYPUhB1GsCYxh+v88wG7HeFDKBY6KUc/Kxo6yoGn5T\nTjRjekyi2KoDZHz4VlIzyZPwFS4I1bf3oCunVoAKzgLdmnTtvRNMC5jFOGc2vUgP\n9bSyRj3S1R4ClVk2g0IDeagko/jc8zzLEYuIK+fbkds79YECQQDt3vcevgegnkga\ntF4NsDmmBPRkcSHCqrANP/7vFcBQN3czxeYYWX3DK07alu6GhH1Y4sHbdm616uU0\nll7xbDzxAkEAzAtN2IyftNygV2EGiaGgqLyo/tD9+Vui2qCQplqe4jvWh/5Sparl\nOjmKo+uAW+hLrLVMnHzRWxbWU8hirH5FNQJATO+ZxCK4etXXAnQmG41NCAqANWB2\nB+2HJbH2NcQ2QHvAHUm741JGn/KI/aBlo7KEjFRDWUVUB5ji64BbUwCsMQJBAIku\nLGcjnBf/oLk+XSPZC2eGd2Ph5G5qYmH0Q2vkTx+wtTn3DV+eNsDfgMtWAJVJ5t61\ngU1QSXyhLPVlKpnnxuUCQC+xvvWjWtsLaFtAsZywJiqLxQzHts8XLGZptYJ5tLWV\nrtmYtBcJCN48RrgQHry/xWYeA4K/AFQpXfNPgprQ96Q=\n-----END RSA PRIVATE KEY-----\n", + "public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC9mC3WZN9UGLxgPBpP7H5jZMc6pKwOoSgre8yun6REFktn/Kz7DUt9jaR1UJyRzHxITfCfAIgSxPdGqB/oF1suMyWgu5i0625vavLB5z5kC8Hq3qZJ9zJO1poE1kyD+htiTtPWJ88e12xuH2XB/CZN9OpEiF98hAagiOE0EnOS5Q== Generated by Nova\n", + "user_id": "fake" + } +} +` + +// ImportOutput is a sample response to a Create call that provides its own public key. +const ImportOutput = ` +{ + "keypair": { + "fingerprint": "1e:2c:9b:56:79:4b:45:77:f9:ca:7a:98:2c:b0:d5:3c", + "name": "importedkey", + "public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDx8nkQv/zgGgB4rMYmIf+6A4l6Rr+o/6lHBQdW5aYd44bd8JttDCE/F/pNRr0lRE+PiqSPO8nDPHw0010JeMH9gYgnnFlyY3/OcJ02RhIPyyxYpv9FhY+2YiUkpwFOcLImyrxEsYXpD/0d3ac30bNH6Sw9JD9UZHYcpSxsIbECHw== Generated by Nova", + "user_id": "fake" + } +} +` + +// FirstKeyPair is the first result in ListOutput. +var FirstKeyPair = KeyPair{ + Name: "firstkey", + Fingerprint: "15:b0:f8:b3:f9:48:63:71:cf:7b:5b:38:6d:44:2d:4a", + PublicKey: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC+Eo/RZRngaGTkFs7I62ZjsIlO79KklKbMXi8F+KITD4bVQHHn+kV+4gRgkgCRbdoDqoGfpaDFs877DYX9n4z6FrAIZ4PES8TNKhatifpn9NdQYWA+IkU8CuvlEKGuFpKRi/k7JLos/gHi2hy7QUwgtRvcefvD/vgQZOVw/mGR9Q== Generated by Nova\n", +} + +// SecondKeyPair is the second result in ListOutput. +var SecondKeyPair = KeyPair{ + Name: "secondkey", + Fingerprint: "35:9d:d0:c3:4a:80:d3:d8:86:f1:ca:f7:df:c4:f9:d8", + PublicKey: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC9mC3WZN9UGLxgPBpP7H5jZMc6pKwOoSgre8yun6REFktn/Kz7DUt9jaR1UJyRzHxITfCfAIgSxPdGqB/oF1suMyWgu5i0625vavLB5z5kC8Hq3qZJ9zJO1poE1kyD+htiTtPWJ88e12xuH2XB/CZN9OpEiF98hAagiOE0EnOS5Q== Generated by Nova\n", +} + +// ExpectedKeyPairSlice is the slice of results that should be parsed from ListOutput, in the expected +// order. +var ExpectedKeyPairSlice = []KeyPair{FirstKeyPair, SecondKeyPair} + +// CreatedKeyPair is the parsed result from CreatedOutput. +var CreatedKeyPair = KeyPair{ + Name: "createdkey", + Fingerprint: "35:9d:d0:c3:4a:80:d3:d8:86:f1:ca:f7:df:c4:f9:d8", + PublicKey: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC9mC3WZN9UGLxgPBpP7H5jZMc6pKwOoSgre8yun6REFktn/Kz7DUt9jaR1UJyRzHxITfCfAIgSxPdGqB/oF1suMyWgu5i0625vavLB5z5kC8Hq3qZJ9zJO1poE1kyD+htiTtPWJ88e12xuH2XB/CZN9OpEiF98hAagiOE0EnOS5Q== Generated by Nova\n", + PrivateKey: "-----BEGIN RSA PRIVATE KEY-----\nMIICXAIBAAKBgQC9mC3WZN9UGLxgPBpP7H5jZMc6pKwOoSgre8yun6REFktn/Kz7\nDUt9jaR1UJyRzHxITfCfAIgSxPdGqB/oF1suMyWgu5i0625vavLB5z5kC8Hq3qZJ\n9zJO1poE1kyD+htiTtPWJ88e12xuH2XB/CZN9OpEiF98hAagiOE0EnOS5QIDAQAB\nAoGAE5XO1mDhORy9COvsg+kYPUhB1GsCYxh+v88wG7HeFDKBY6KUc/Kxo6yoGn5T\nTjRjekyi2KoDZHz4VlIzyZPwFS4I1bf3oCunVoAKzgLdmnTtvRNMC5jFOGc2vUgP\n9bSyRj3S1R4ClVk2g0IDeagko/jc8zzLEYuIK+fbkds79YECQQDt3vcevgegnkga\ntF4NsDmmBPRkcSHCqrANP/7vFcBQN3czxeYYWX3DK07alu6GhH1Y4sHbdm616uU0\nll7xbDzxAkEAzAtN2IyftNygV2EGiaGgqLyo/tD9+Vui2qCQplqe4jvWh/5Sparl\nOjmKo+uAW+hLrLVMnHzRWxbWU8hirH5FNQJATO+ZxCK4etXXAnQmG41NCAqANWB2\nB+2HJbH2NcQ2QHvAHUm741JGn/KI/aBlo7KEjFRDWUVUB5ji64BbUwCsMQJBAIku\nLGcjnBf/oLk+XSPZC2eGd2Ph5G5qYmH0Q2vkTx+wtTn3DV+eNsDfgMtWAJVJ5t61\ngU1QSXyhLPVlKpnnxuUCQC+xvvWjWtsLaFtAsZywJiqLxQzHts8XLGZptYJ5tLWV\nrtmYtBcJCN48RrgQHry/xWYeA4K/AFQpXfNPgprQ96Q=\n-----END RSA PRIVATE KEY-----\n", + UserID: "fake", +} + +// ImportedKeyPair is the parsed result from ImportOutput. +var ImportedKeyPair = KeyPair{ + Name: "importedkey", + Fingerprint: "1e:2c:9b:56:79:4b:45:77:f9:ca:7a:98:2c:b0:d5:3c", + PublicKey: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDx8nkQv/zgGgB4rMYmIf+6A4l6Rr+o/6lHBQdW5aYd44bd8JttDCE/F/pNRr0lRE+PiqSPO8nDPHw0010JeMH9gYgnnFlyY3/OcJ02RhIPyyxYpv9FhY+2YiUkpwFOcLImyrxEsYXpD/0d3ac30bNH6Sw9JD9UZHYcpSxsIbECHw== Generated by Nova", + UserID: "fake", +} + +// HandleListSuccessfully configures the test server to respond to a List request. +func HandleListSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/os-keypairs", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, ListOutput) + }) +} + +// HandleGetSuccessfully configures the test server to respond to a Get request for "firstkey". +func HandleGetSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/os-keypairs/firstkey", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, GetOutput) + }) +} + +// HandleCreateSuccessfully configures the test server to respond to a Create request for a new +// keypair called "createdkey". +func HandleCreateSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/os-keypairs", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, `{ "keypair": { "name": "createdkey" } }`) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, CreateOutput) + }) +} + +// HandleImportSuccessfully configures the test server to respond to an Import request for an +// existing keypair called "importedkey". +func HandleImportSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/os-keypairs", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, ` + { + "keypair": { + "name": "importedkey", + "public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDx8nkQv/zgGgB4rMYmIf+6A4l6Rr+o/6lHBQdW5aYd44bd8JttDCE/F/pNRr0lRE+PiqSPO8nDPHw0010JeMH9gYgnnFlyY3/OcJ02RhIPyyxYpv9FhY+2YiUkpwFOcLImyrxEsYXpD/0d3ac30bNH6Sw9JD9UZHYcpSxsIbECHw== Generated by Nova" + } + } + `) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, ImportOutput) + }) +} + +// HandleDeleteSuccessfully configures the test server to respond to a Delete request for a +// keypair called "deletedkey". +func HandleDeleteSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/os-keypairs/deletedkey", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.WriteHeader(http.StatusAccepted) + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs/requests.go new file mode 100644 index 00000000000..7d1a2ac8b0c --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs/requests.go @@ -0,0 +1,88 @@ +package keypairs + +import ( + "errors" + + "github.com/racker/perigee" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// List returns a Pager that allows you to iterate over a collection of KeyPairs. +func List(client *gophercloud.ServiceClient) pagination.Pager { + return pagination.NewPager(client, listURL(client), func(r pagination.PageResult) pagination.Page { + return KeyPairPage{pagination.SinglePageBase(r)} + }) +} + +// CreateOptsBuilder describes struct types that can be accepted by the Create call. Notable, the +// CreateOpts struct in this package does. +type CreateOptsBuilder interface { + ToKeyPairCreateMap() (map[string]interface{}, error) +} + +// CreateOpts species keypair creation or import parameters. +type CreateOpts struct { + // Name [required] is a friendly name to refer to this KeyPair in other services. + Name string + + // PublicKey [optional] is a pregenerated OpenSSH-formatted public key. If provided, this key + // will be imported and no new key will be created. + PublicKey string +} + +// ToKeyPairCreateMap constructs a request body from CreateOpts. +func (opts CreateOpts) ToKeyPairCreateMap() (map[string]interface{}, error) { + if opts.Name == "" { + return nil, errors.New("Missing field required for keypair creation: Name") + } + + keypair := make(map[string]interface{}) + keypair["name"] = opts.Name + if opts.PublicKey != "" { + keypair["public_key"] = opts.PublicKey + } + + return map[string]interface{}{"keypair": keypair}, nil +} + +// Create requests the creation of a new keypair on the server, or to import a pre-existing +// keypair. +func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult { + var res CreateResult + + reqBody, err := opts.ToKeyPairCreateMap() + if err != nil { + res.Err = err + return res + } + + _, res.Err = perigee.Request("POST", createURL(client), perigee.Options{ + MoreHeaders: client.AuthenticatedHeaders(), + ReqBody: reqBody, + Results: &res.Body, + OkCodes: []int{200}, + }) + return res +} + +// Get returns public data about a previously uploaded KeyPair. +func Get(client *gophercloud.ServiceClient, name string) GetResult { + var res GetResult + _, res.Err = perigee.Request("GET", getURL(client, name), perigee.Options{ + MoreHeaders: client.AuthenticatedHeaders(), + Results: &res.Body, + OkCodes: []int{200}, + }) + return res +} + +// Delete requests the deletion of a previous stored KeyPair from the server. +func Delete(client *gophercloud.ServiceClient, name string) DeleteResult { + var res DeleteResult + _, res.Err = perigee.Request("DELETE", deleteURL(client, name), perigee.Options{ + MoreHeaders: client.AuthenticatedHeaders(), + OkCodes: []int{202}, + }) + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs/requests_test.go new file mode 100644 index 00000000000..67d1833f572 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs/requests_test.go @@ -0,0 +1,71 @@ +package keypairs + +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() + HandleListSuccessfully(t) + + count := 0 + err := List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractKeyPairs(page) + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, ExpectedKeyPairSlice, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, 1, count) +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleCreateSuccessfully(t) + + actual, err := Create(client.ServiceClient(), CreateOpts{ + Name: "createdkey", + }).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &CreatedKeyPair, actual) +} + +func TestImport(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleImportSuccessfully(t) + + actual, err := Create(client.ServiceClient(), CreateOpts{ + Name: "importedkey", + PublicKey: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDx8nkQv/zgGgB4rMYmIf+6A4l6Rr+o/6lHBQdW5aYd44bd8JttDCE/F/pNRr0lRE+PiqSPO8nDPHw0010JeMH9gYgnnFlyY3/OcJ02RhIPyyxYpv9FhY+2YiUkpwFOcLImyrxEsYXpD/0d3ac30bNH6Sw9JD9UZHYcpSxsIbECHw== Generated by Nova", + }).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &ImportedKeyPair, actual) +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleGetSuccessfully(t) + + actual, err := Get(client.ServiceClient(), "firstkey").Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &FirstKeyPair, actual) +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleDeleteSuccessfully(t) + + err := Delete(client.ServiceClient(), "deletedkey").ExtractErr() + th.AssertNoErr(t, err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs/results.go new file mode 100644 index 00000000000..f1a0d8e114c --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs/results.go @@ -0,0 +1,94 @@ +package keypairs + +import ( + "github.com/mitchellh/mapstructure" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// KeyPair is an SSH key known to the OpenStack cluster that is available to be injected into +// servers. +type KeyPair struct { + // Name is used to refer to this keypair from other services within this region. + Name string `mapstructure:"name"` + + // Fingerprint is a short sequence of bytes that can be used to authenticate or validate a longer + // public key. + Fingerprint string `mapstructure:"fingerprint"` + + // PublicKey is the public key from this pair, in OpenSSH format. "ssh-rsa AAAAB3Nz..." + PublicKey string `mapstructure:"public_key"` + + // PrivateKey is the private key from this pair, in PEM format. + // "-----BEGIN RSA PRIVATE KEY-----\nMIICXA..." It is only present if this keypair was just + // returned from a Create call + PrivateKey string `mapstructure:"private_key"` + + // UserID is the user who owns this keypair. + UserID string `mapstructure:"user_id"` +} + +// KeyPairPage stores a single, only page of KeyPair results from a List call. +type KeyPairPage struct { + pagination.SinglePageBase +} + +// IsEmpty determines whether or not a KeyPairPage is empty. +func (page KeyPairPage) IsEmpty() (bool, error) { + ks, err := ExtractKeyPairs(page) + return len(ks) == 0, err +} + +// ExtractKeyPairs interprets a page of results as a slice of KeyPairs. +func ExtractKeyPairs(page pagination.Page) ([]KeyPair, error) { + type pair struct { + KeyPair KeyPair `mapstructure:"keypair"` + } + + var resp struct { + KeyPairs []pair `mapstructure:"keypairs"` + } + + err := mapstructure.Decode(page.(KeyPairPage).Body, &resp) + results := make([]KeyPair, len(resp.KeyPairs)) + for i, pair := range resp.KeyPairs { + results[i] = pair.KeyPair + } + return results, err +} + +type keyPairResult struct { + gophercloud.Result +} + +// Extract is a method that attempts to interpret any KeyPair resource response as a KeyPair struct. +func (r keyPairResult) Extract() (*KeyPair, error) { + if r.Err != nil { + return nil, r.Err + } + + var res struct { + KeyPair *KeyPair `json:"keypair" mapstructure:"keypair"` + } + + err := mapstructure.Decode(r.Body, &res) + return res.KeyPair, err +} + +// CreateResult is the response from a Create operation. Call its Extract method to interpret it +// as a KeyPair. +type CreateResult struct { + keyPairResult +} + +// GetResult is the response from a Get operation. Call its Extract method to interpret it +// as a KeyPair. +type GetResult struct { + keyPairResult +} + +// DeleteResult is the response from a Delete operation. Call its Extract method to determine if +// the call succeeded or failed. +type DeleteResult struct { + gophercloud.ErrResult +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs/urls.go new file mode 100644 index 00000000000..702f5329e05 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs/urls.go @@ -0,0 +1,25 @@ +package keypairs + +import "github.com/rackspace/gophercloud" + +const resourcePath = "os-keypairs" + +func resourceURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(resourcePath) +} + +func listURL(c *gophercloud.ServiceClient) string { + return resourceURL(c) +} + +func createURL(c *gophercloud.ServiceClient) string { + return resourceURL(c) +} + +func getURL(c *gophercloud.ServiceClient, name string) string { + return c.ServiceURL(resourcePath, name) +} + +func deleteURL(c *gophercloud.ServiceClient, name string) string { + return getURL(c, name) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs/urls_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs/urls_test.go new file mode 100644 index 00000000000..60efd2a5d33 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs/urls_test.go @@ -0,0 +1,40 @@ +package keypairs + +import ( + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestListURL(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + c := client.ServiceClient() + + th.CheckEquals(t, c.Endpoint+"os-keypairs", listURL(c)) +} + +func TestCreateURL(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + c := client.ServiceClient() + + th.CheckEquals(t, c.Endpoint+"os-keypairs", createURL(c)) +} + +func TestGetURL(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + c := client.ServiceClient() + + th.CheckEquals(t, c.Endpoint+"os-keypairs/wat", getURL(c, "wat")) +} + +func TestDeleteURL(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + c := client.ServiceClient() + + th.CheckEquals(t, c.Endpoint+"os-keypairs/wat", deleteURL(c, "wat")) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/flavors/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/flavors/doc.go new file mode 100644 index 00000000000..5822e1bcf6d --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/flavors/doc.go @@ -0,0 +1,7 @@ +// Package flavors provides information and interaction with the flavor API +// resource in the OpenStack Compute service. +// +// A flavor is an available hardware configuration for a server. Each flavor +// has a unique combination of disk space, memory capacity and priority for CPU +// time. +package flavors diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/flavors/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/flavors/requests.go new file mode 100644 index 00000000000..065a2ec4724 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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 { + ToFlavorListQuery() (string, error) +} + +// ListOpts helps control the results returned by the List() function. +// For example, a flavor with a minDisk field of 10 will not be returned if you specify MinDisk set to 20. +// Typically, software will use the last ID of the previous call to List to set the Marker for the current call. +type ListOpts struct { + + // ChangesSince, if provided, instructs List to return only those things which have changed since the timestamp provided. + ChangesSince string `q:"changes-since"` + + // MinDisk and MinRAM, if provided, elides flavors which do not meet your criteria. + MinDisk int `q:"minDisk"` + MinRAM int `q:"minRam"` + + // Marker and Limit control paging. + // Marker instructs List where to start listing from. + Marker string `q:"marker"` + + // Limit instructs List to refrain from sending excessively large lists of flavors. + Limit int `q:"limit"` +} + +// ToFlavorListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToFlavorListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + if err != nil { + return "", err + } + return q.String(), nil +} + +// ListDetail instructs OpenStack to provide a list of flavors. +// You may provide criteria by which List curtails its results for easier processing. +// See ListOpts for more details. +func ListDetail(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(client) + if opts != nil { + query, err := opts.ToFlavorListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + createPage := func(r pagination.PageResult) pagination.Page { + return FlavorPage{pagination.LinkedPageBase{PageResult: r}} + } + + return pagination.NewPager(client, url, createPage) +} + +// Get instructs OpenStack to provide details on a single flavor, identified by its ID. +// Use ExtractFlavor to convert its result into a Flavor. +func Get(client *gophercloud.ServiceClient, id string) GetResult { + var gr GetResult + gr.Err = perigee.Get(getURL(client, id), perigee.Options{ + Results: &gr.Body, + MoreHeaders: client.AuthenticatedHeaders(), + }) + return gr +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/flavors/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/flavors/requests_test.go new file mode 100644 index 00000000000..fbd7c331402 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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 := ListDetail(fake.ServiceClient(), nil).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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/flavors/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/flavors/results.go new file mode 100644 index 00000000000..8dddd705c93 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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 response from a Get call. +type GetResult struct { + gophercloud.Result +} + +// Extract provides access to the individual Flavor returned by the Get function. +func (gr GetResult) Extract() (*Flavor, error) { + if gr.Err != nil { + return nil, gr.Err + } + + var result struct { + Flavor Flavor `mapstructure:"flavor"` + } + + cfg := &mapstructure.DecoderConfig{ + DecodeHook: defaulter, + Result: &result, + } + decoder, err := mapstructure.NewDecoder(cfg) + if err != nil { + return nil, err + } + err = decoder.Decode(gr.Body) + return &result.Flavor, err +} + +// Flavor records represent (virtual) hardware configurations for server resources in a region. +type Flavor struct { + // The Id field contains the flavor's unique identifier. + // For example, this identifier will be useful when specifying which hardware configuration to use for a new server instance. + ID string `mapstructure:"id"` + + // The Disk and RA< fields provide a measure of storage space offered by the flavor, in GB and MB, respectively. + Disk int `mapstructure:"disk"` + RAM int `mapstructure:"ram"` + + // The Name field provides a human-readable moniker for the flavor. + Name string `mapstructure:"name"` + + RxTxFactor float64 `mapstructure:"rxtx_factor"` + + // Swap indicates how much space is reserved for swap. + // If not provided, this field will be set to 0. + Swap int `mapstructure:"swap"` + + // VCPUs indicates how many (virtual) CPUs are available for this flavor. + VCPUs int `mapstructure:"vcpus"` +} + +// FlavorPage contains a single page of the response from a List call. +type FlavorPage struct { + pagination.LinkedPageBase +} + +// IsEmpty determines if a page contains any results. +func (p FlavorPage) IsEmpty() (bool, error) { + flavors, err := ExtractFlavors(p) + if err != nil { + return true, err + } + return len(flavors) == 0, nil +} + +// NextPageURL uses the response's embedded link reference to navigate to the next page of results. +func (p FlavorPage) NextPageURL() (string, error) { + type resp struct { + Links []gophercloud.Link `mapstructure:"flavors_links"` + } + + var r resp + err := mapstructure.Decode(p.Body, &r) + if err != nil { + return "", err + } + + return gophercloud.ExtractNextURL(r.Links) +} + +func defaulter(from, to reflect.Kind, v interface{}) (interface{}, error) { + if (from == reflect.String) && (to == reflect.Int) { + return 0, nil + } + return v, nil +} + +// ExtractFlavors provides access to the list of flavors in a page acquired from the List operation. +func ExtractFlavors(page pagination.Page) ([]Flavor, error) { + casted := page.(FlavorPage).Body + var container struct { + Flavors []Flavor `mapstructure:"flavors"` + } + + cfg := &mapstructure.DecoderConfig{ + DecodeHook: defaulter, + Result: &container, + } + decoder, err := mapstructure.NewDecoder(cfg) + if err != nil { + return container.Flavors, err + } + err = decoder.Decode(casted) + if err != nil { + return container.Flavors, err + } + + return container.Flavors, nil +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/flavors/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/flavors/urls.go new file mode 100644 index 00000000000..683c107dcbe --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/flavors/urls_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/flavors/urls_test.go new file mode 100644 index 00000000000..069da2496e0 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/images/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/images/doc.go new file mode 100644 index 00000000000..0edaa3f0255 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/images/doc.go @@ -0,0 +1,7 @@ +// Package images provides information and interaction with the image API +// resource in the OpenStack Compute service. +// +// An image is a collection of files used to create or rebuild a server. +// Operators provide a number of pre-built OS images by default. You may also +// create custom images from cloud servers you have launched. +package images diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/images/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/images/requests.go new file mode 100644 index 00000000000..bc61ddb9df3 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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 { + ToImageListQuery() (string, error) +} + +// ListOpts contain options for limiting the number of Images returned from a call to ListDetail. +type ListOpts struct { + // When the image last changed status (in date-time format). + ChangesSince string `q:"changes-since"` + // The number of Images to return. + Limit int `q:"limit"` + // UUID of the Image at which to set a marker. + Marker string `q:"marker"` + // The name of the Image. + Name string `q:"name:"` + // The name of the Server (in URL format). + Server string `q:"server"` + // The current status of the Image. + Status string `q:"status"` + // The value of the type of image (e.g. BASE, SERVER, ALL) + Type string `q:"type"` +} + +// ToImageListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToImageListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + if err != nil { + return "", err + } + return q.String(), nil +} + +// ListDetail enumerates the available images. +func ListDetail(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listDetailURL(client) + if opts != nil { + query, err := opts.ToImageListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + + createPage := func(r pagination.PageResult) pagination.Page { + return ImagePage{pagination.LinkedPageBase{PageResult: r}} + } + + return pagination.NewPager(client, url, createPage) +} + +// Get acquires additional detail about a specific image by ID. +// Use ExtractImage() to interpret the result as an openstack Image. +func Get(client *gophercloud.ServiceClient, id string) GetResult { + var result GetResult + _, result.Err = perigee.Request("GET", getURL(client, id), perigee.Options{ + MoreHeaders: client.AuthenticatedHeaders(), + Results: &result.Body, + OkCodes: []int{200}, + }) + return result +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/images/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/images/requests_test.go new file mode 100644 index 00000000000..9a05f97ec0d --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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":"bookmark"}]}, "images_links":[{"href":"http://192.154.23.87/12345/images/image4","rel":"next"}]}`) + 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/image4" + actual, err := page.NextPageURL() + th.AssertNoErr(t, err) + th.CheckEquals(t, expected, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/images/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/images/results.go new file mode 100644 index 00000000000..493d51192c0 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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.Result +} + +// Extract interprets a GetResult as an Image. +func (gr GetResult) Extract() (*Image, error) { + if gr.Err != nil { + return nil, gr.Err + } + + var decoded struct { + Image Image `mapstructure:"image"` + } + + err := mapstructure.Decode(gr.Body, &decoded) + return &decoded.Image, err +} + +// Image is used for JSON (un)marshalling. +// It provides a description of an OS image. +type Image struct { + // ID contains the image's unique identifier. + ID string + + Created string + + // MinDisk and MinRAM specify the minimum resources a server must provide to be able to install the image. + MinDisk int + MinRAM int + + // Name provides a human-readable moniker for the OS image. + Name string + + // The Progress and Status fields indicate image-creation status. + // Any usable image will have 100% progress. + Progress int + Status string + + Updated string +} + +// 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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/images/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/images/urls.go new file mode 100644 index 00000000000..9b3c86d435f --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/images/urls_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/images/urls_test.go new file mode 100644 index 00000000000..b1ab3d6790c --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/servers/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/servers/doc.go new file mode 100644 index 00000000000..fe4567120c7 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/servers/doc.go @@ -0,0 +1,6 @@ +// Package servers provides information and interaction with the server API +// resource in the OpenStack Compute service. +// +// A server is a virtual machine instance in the compute system. In order for +// one to be provisioned, a valid flavor and image are required. +package servers diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/servers/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/servers/fixtures.go new file mode 100644 index 00000000000..e872b071774 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/servers/fixtures.go @@ -0,0 +1,459 @@ +// +build fixtures + +package servers + +import ( + "fmt" + "net/http" + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +// ServerListBody contains the canned body of a servers.List response. +const ServerListBody = ` +{ + "servers": [ + { + "status": "ACTIVE", + "updated": "2014-09-25T13:10:10Z", + "hostId": "29d3c8c896a45aa4c34e52247875d7fefc3d94bbcc9f622b5d204362", + "OS-EXT-SRV-ATTR:host": "devstack", + "addresses": { + "private": [ + { + "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:7c:1b:2b", + "version": 4, + "addr": "10.0.0.32", + "OS-EXT-IPS:type": "fixed" + } + ] + }, + "links": [ + { + "href": "http://104.130.131.164:8774/v2/fcad67a6189847c4aecfa3c81a05783b/servers/ef079b0c-e610-4dfb-b1aa-b49f07ac48e5", + "rel": "self" + }, + { + "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/servers/ef079b0c-e610-4dfb-b1aa-b49f07ac48e5", + "rel": "bookmark" + } + ], + "key_name": null, + "image": { + "id": "f90f6034-2570-4974-8351-6b49732ef2eb", + "links": [ + { + "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/images/f90f6034-2570-4974-8351-6b49732ef2eb", + "rel": "bookmark" + } + ] + }, + "OS-EXT-STS:task_state": null, + "OS-EXT-STS:vm_state": "active", + "OS-EXT-SRV-ATTR:instance_name": "instance-0000001e", + "OS-SRV-USG:launched_at": "2014-09-25T13:10:10.000000", + "OS-EXT-SRV-ATTR:hypervisor_hostname": "devstack", + "flavor": { + "id": "1", + "links": [ + { + "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/flavors/1", + "rel": "bookmark" + } + ] + }, + "id": "ef079b0c-e610-4dfb-b1aa-b49f07ac48e5", + "security_groups": [ + { + "name": "default" + } + ], + "OS-SRV-USG:terminated_at": null, + "OS-EXT-AZ:availability_zone": "nova", + "user_id": "9349aff8be7545ac9d2f1d00999a23cd", + "name": "herp", + "created": "2014-09-25T13:10:02Z", + "tenant_id": "fcad67a6189847c4aecfa3c81a05783b", + "OS-DCF:diskConfig": "MANUAL", + "os-extended-volumes:volumes_attached": [], + "accessIPv4": "", + "accessIPv6": "", + "progress": 0, + "OS-EXT-STS:power_state": 1, + "config_drive": "", + "metadata": {} + }, + { + "status": "ACTIVE", + "updated": "2014-09-25T13:04:49Z", + "hostId": "29d3c8c896a45aa4c34e52247875d7fefc3d94bbcc9f622b5d204362", + "OS-EXT-SRV-ATTR:host": "devstack", + "addresses": { + "private": [ + { + "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:9e:89:be", + "version": 4, + "addr": "10.0.0.31", + "OS-EXT-IPS:type": "fixed" + } + ] + }, + "links": [ + { + "href": "http://104.130.131.164:8774/v2/fcad67a6189847c4aecfa3c81a05783b/servers/9e5476bd-a4ec-4653-93d6-72c93aa682ba", + "rel": "self" + }, + { + "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/servers/9e5476bd-a4ec-4653-93d6-72c93aa682ba", + "rel": "bookmark" + } + ], + "key_name": null, + "image": { + "id": "f90f6034-2570-4974-8351-6b49732ef2eb", + "links": [ + { + "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/images/f90f6034-2570-4974-8351-6b49732ef2eb", + "rel": "bookmark" + } + ] + }, + "OS-EXT-STS:task_state": null, + "OS-EXT-STS:vm_state": "active", + "OS-EXT-SRV-ATTR:instance_name": "instance-0000001d", + "OS-SRV-USG:launched_at": "2014-09-25T13:04:49.000000", + "OS-EXT-SRV-ATTR:hypervisor_hostname": "devstack", + "flavor": { + "id": "1", + "links": [ + { + "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/flavors/1", + "rel": "bookmark" + } + ] + }, + "id": "9e5476bd-a4ec-4653-93d6-72c93aa682ba", + "security_groups": [ + { + "name": "default" + } + ], + "OS-SRV-USG:terminated_at": null, + "OS-EXT-AZ:availability_zone": "nova", + "user_id": "9349aff8be7545ac9d2f1d00999a23cd", + "name": "derp", + "created": "2014-09-25T13:04:41Z", + "tenant_id": "fcad67a6189847c4aecfa3c81a05783b", + "OS-DCF:diskConfig": "MANUAL", + "os-extended-volumes:volumes_attached": [], + "accessIPv4": "", + "accessIPv6": "", + "progress": 0, + "OS-EXT-STS:power_state": 1, + "config_drive": "", + "metadata": {} + } + ] +} +` + +// SingleServerBody is the canned body of a Get request on an existing server. +const SingleServerBody = ` +{ + "server": { + "status": "ACTIVE", + "updated": "2014-09-25T13:04:49Z", + "hostId": "29d3c8c896a45aa4c34e52247875d7fefc3d94bbcc9f622b5d204362", + "OS-EXT-SRV-ATTR:host": "devstack", + "addresses": { + "private": [ + { + "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:9e:89:be", + "version": 4, + "addr": "10.0.0.31", + "OS-EXT-IPS:type": "fixed" + } + ] + }, + "links": [ + { + "href": "http://104.130.131.164:8774/v2/fcad67a6189847c4aecfa3c81a05783b/servers/9e5476bd-a4ec-4653-93d6-72c93aa682ba", + "rel": "self" + }, + { + "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/servers/9e5476bd-a4ec-4653-93d6-72c93aa682ba", + "rel": "bookmark" + } + ], + "key_name": null, + "image": { + "id": "f90f6034-2570-4974-8351-6b49732ef2eb", + "links": [ + { + "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/images/f90f6034-2570-4974-8351-6b49732ef2eb", + "rel": "bookmark" + } + ] + }, + "OS-EXT-STS:task_state": null, + "OS-EXT-STS:vm_state": "active", + "OS-EXT-SRV-ATTR:instance_name": "instance-0000001d", + "OS-SRV-USG:launched_at": "2014-09-25T13:04:49.000000", + "OS-EXT-SRV-ATTR:hypervisor_hostname": "devstack", + "flavor": { + "id": "1", + "links": [ + { + "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/flavors/1", + "rel": "bookmark" + } + ] + }, + "id": "9e5476bd-a4ec-4653-93d6-72c93aa682ba", + "security_groups": [ + { + "name": "default" + } + ], + "OS-SRV-USG:terminated_at": null, + "OS-EXT-AZ:availability_zone": "nova", + "user_id": "9349aff8be7545ac9d2f1d00999a23cd", + "name": "derp", + "created": "2014-09-25T13:04:41Z", + "tenant_id": "fcad67a6189847c4aecfa3c81a05783b", + "OS-DCF:diskConfig": "MANUAL", + "os-extended-volumes:volumes_attached": [], + "accessIPv4": "", + "accessIPv6": "", + "progress": 0, + "OS-EXT-STS:power_state": 1, + "config_drive": "", + "metadata": {} + } +} +` + +var ( + // ServerHerp is a Server struct that should correspond to the first result in ServerListBody. + ServerHerp = Server{ + Status: "ACTIVE", + Updated: "2014-09-25T13:10:10Z", + HostID: "29d3c8c896a45aa4c34e52247875d7fefc3d94bbcc9f622b5d204362", + Addresses: map[string]interface{}{ + "private": []interface{}{ + map[string]interface{}{ + "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:7c:1b:2b", + "version": float64(4), + "addr": "10.0.0.32", + "OS-EXT-IPS:type": "fixed", + }, + }, + }, + Links: []interface{}{ + map[string]interface{}{ + "href": "http://104.130.131.164:8774/v2/fcad67a6189847c4aecfa3c81a05783b/servers/ef079b0c-e610-4dfb-b1aa-b49f07ac48e5", + "rel": "self", + }, + map[string]interface{}{ + "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/servers/ef079b0c-e610-4dfb-b1aa-b49f07ac48e5", + "rel": "bookmark", + }, + }, + Image: map[string]interface{}{ + "id": "f90f6034-2570-4974-8351-6b49732ef2eb", + "links": []interface{}{ + map[string]interface{}{ + "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/images/f90f6034-2570-4974-8351-6b49732ef2eb", + "rel": "bookmark", + }, + }, + }, + Flavor: map[string]interface{}{ + "id": "1", + "links": []interface{}{ + map[string]interface{}{ + "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/flavors/1", + "rel": "bookmark", + }, + }, + }, + ID: "ef079b0c-e610-4dfb-b1aa-b49f07ac48e5", + UserID: "9349aff8be7545ac9d2f1d00999a23cd", + Name: "herp", + Created: "2014-09-25T13:10:02Z", + TenantID: "fcad67a6189847c4aecfa3c81a05783b", + Metadata: map[string]interface{}{}, + } + + // ServerDerp is a Server struct that should correspond to the second server in ServerListBody. + ServerDerp = Server{ + Status: "ACTIVE", + Updated: "2014-09-25T13:04:49Z", + HostID: "29d3c8c896a45aa4c34e52247875d7fefc3d94bbcc9f622b5d204362", + Addresses: map[string]interface{}{ + "private": []interface{}{ + map[string]interface{}{ + "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:9e:89:be", + "version": float64(4), + "addr": "10.0.0.31", + "OS-EXT-IPS:type": "fixed", + }, + }, + }, + Links: []interface{}{ + map[string]interface{}{ + "href": "http://104.130.131.164:8774/v2/fcad67a6189847c4aecfa3c81a05783b/servers/9e5476bd-a4ec-4653-93d6-72c93aa682ba", + "rel": "self", + }, + map[string]interface{}{ + "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/servers/9e5476bd-a4ec-4653-93d6-72c93aa682ba", + "rel": "bookmark", + }, + }, + Image: map[string]interface{}{ + "id": "f90f6034-2570-4974-8351-6b49732ef2eb", + "links": []interface{}{ + map[string]interface{}{ + "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/images/f90f6034-2570-4974-8351-6b49732ef2eb", + "rel": "bookmark", + }, + }, + }, + Flavor: map[string]interface{}{ + "id": "1", + "links": []interface{}{ + map[string]interface{}{ + "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/flavors/1", + "rel": "bookmark", + }, + }, + }, + ID: "9e5476bd-a4ec-4653-93d6-72c93aa682ba", + UserID: "9349aff8be7545ac9d2f1d00999a23cd", + Name: "derp", + Created: "2014-09-25T13:04:41Z", + TenantID: "fcad67a6189847c4aecfa3c81a05783b", + Metadata: map[string]interface{}{}, + } +) + +// HandleServerCreationSuccessfully sets up the test server to respond to a server creation request +// with a given response. +func HandleServerCreationSuccessfully(t *testing.T, response string) { + th.Mux.HandleFunc("/servers", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, `{ + "server": { + "name": "derp", + "imageRef": "f90f6034-2570-4974-8351-6b49732ef2eb", + "flavorRef": "1" + } + }`) + + w.WriteHeader(http.StatusAccepted) + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, response) + }) +} + +// HandleServerListSuccessfully sets up the test server to respond to a server List request. +func HandleServerListSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/detail", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + r.ParseForm() + marker := r.Form.Get("marker") + switch marker { + case "": + fmt.Fprintf(w, ServerListBody) + case "9e5476bd-a4ec-4653-93d6-72c93aa682ba": + fmt.Fprintf(w, `{ "servers": [] }`) + default: + t.Fatalf("/servers/detail invoked with unexpected marker=[%s]", marker) + } + }) +} + +// HandleServerDeletionSuccessfully sets up the test server to respond to a server deletion request. +func HandleServerDeletionSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/asdfasdfasdf", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.WriteHeader(http.StatusNoContent) + }) +} + +// HandleServerGetSuccessfully sets up the test server to respond to a server Get request. +func HandleServerGetSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/1234asdf", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + + fmt.Fprintf(w, SingleServerBody) + }) +} + +// HandleServerUpdateSuccessfully sets up the test server to respond to a server Update request. +func HandleServerUpdateSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/1234asdf", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestJSONRequest(t, r, `{ "server": { "name": "new-name" } }`) + + fmt.Fprintf(w, SingleServerBody) + }) +} + +// HandleAdminPasswordChangeSuccessfully sets up the test server to respond to a server password +// change request. +func HandleAdminPasswordChangeSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/1234asdf/action", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, `{ "changePassword": { "adminPass": "new-password" } }`) + + w.WriteHeader(http.StatusAccepted) + }) +} + +// HandleRebootSuccessfully sets up the test server to respond to a reboot request with success. +func HandleRebootSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/1234asdf/action", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, `{ "reboot": { "type": "SOFT" } }`) + + w.WriteHeader(http.StatusAccepted) + }) +} + +// HandleRebuildSuccessfully sets up the test server to respond to a rebuild request with success. +func HandleRebuildSuccessfully(t *testing.T, response string) { + th.Mux.HandleFunc("/servers/1234asdf/action", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, ` + { + "rebuild": { + "name": "new-name", + "adminPass": "swordfish", + "imageRef": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/images/f90f6034-2570-4974-8351-6b49732ef2eb", + "accessIPv4": "1.2.3.4" + } + } + `) + + w.WriteHeader(http.StatusAccepted) + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, response) + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/servers/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/servers/requests.go new file mode 100644 index 00000000000..95a4188d5f2 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/servers/requests.go @@ -0,0 +1,538 @@ +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.PageResult) pagination.Page { + return ServerPage{pagination.LinkedPageBase{PageResult: r}} + } + + return pagination.NewPager(client, url, createPageFn) +} + +// CreateOptsBuilder describes struct types that can be accepted by the Create call. +// The CreateOpts struct in this package does. +type CreateOptsBuilder interface { + ToServerCreateMap() (map[string]interface{}, error) +} + +// Network is used within CreateOpts to control a new server's network attachments. +type Network struct { + // UUID of a nova-network to attach to the newly provisioned server. + // Required unless Port is provided. + UUID string + + // Port of a neutron network to attach to the newly provisioned server. + // Required unless UUID is provided. + Port string + + // FixedIP [optional] specifies a fixed IPv4 address to be used on this network. + FixedIP string +} + +// 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{}, error) { + 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 + } + } + server["networks"] = networks + } + + return map[string]interface{}{"server": server}, nil +} + +// Create requests a server to be provisioned to the user in the current tenant. +func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult { + var res CreateResult + + reqBody, err := opts.ToServerCreateMap() + if err != nil { + res.Err = err + return res + } + + _, res.Err = perigee.Request("POST", listURL(client), perigee.Options{ + Results: &res.Body, + ReqBody: reqBody, + MoreHeaders: client.AuthenticatedHeaders(), + OkCodes: []int{202}, + }) + return res +} + +// Delete requests that a server previously provisioned be removed from your account. +func Delete(client *gophercloud.ServiceClient, id string) DeleteResult { + var res DeleteResult + _, res.Err = perigee.Request("DELETE", deleteURL(client, id), perigee.Options{ + MoreHeaders: client.AuthenticatedHeaders(), + OkCodes: []int{204}, + }) + return res +} + +// 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.Body, + MoreHeaders: client.AuthenticatedHeaders(), + }) + return result +} + +// UpdateOptsBuilder allows extensions to add additional attributes to the Update request. +type UpdateOptsBuilder interface { + ToServerUpdateMap() map[string]interface{} +} + +// UpdateOpts specifies the base attributes that may be updated on an existing server. +type UpdateOpts struct { + // Name [optional] changes the displayed name of the server. + // The server host name will *not* change. + // Server names are not constrained to be unique, even within the same tenant. + Name string + + // AccessIPv4 [optional] provides a new IPv4 address for the instance. + AccessIPv4 string + + // AccessIPv6 [optional] provides a new IPv6 address for the instance. + AccessIPv6 string +} + +// ToServerUpdateMap formats an UpdateOpts structure into a request body. +func (opts UpdateOpts) ToServerUpdateMap() map[string]interface{} { + server := make(map[string]string) + if opts.Name != "" { + server["name"] = opts.Name + } + if opts.AccessIPv4 != "" { + server["accessIPv4"] = opts.AccessIPv4 + } + if opts.AccessIPv6 != "" { + server["accessIPv6"] = opts.AccessIPv6 + } + return map[string]interface{}{"server": server} +} + +// Update requests that various attributes of the indicated server be changed. +func Update(client *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) UpdateResult { + var result UpdateResult + _, result.Err = perigee.Request("PUT", updateURL(client, id), perigee.Options{ + Results: &result.Body, + ReqBody: opts.ToServerUpdateMap(), + MoreHeaders: client.AuthenticatedHeaders(), + }) + return result +} + +// ChangeAdminPassword alters the administrator or root password for a specified server. +func ChangeAdminPassword(client *gophercloud.ServiceClient, id, newPassword string) ActionResult { + var req struct { + ChangePassword struct { + AdminPass string `json:"adminPass"` + } `json:"changePassword"` + } + + req.ChangePassword.AdminPass = newPassword + + var res ActionResult + + _, res.Err = perigee.Request("POST", actionURL(client, id), perigee.Options{ + ReqBody: req, + MoreHeaders: client.AuthenticatedHeaders(), + OkCodes: []int{202}, + }) + + return res +} + +// ErrArgument errors occur when an argument supplied to a package function +// fails to fall within acceptable values. For example, the Reboot() function +// expects the "how" parameter to be one of HardReboot or SoftReboot. These +// constants are (currently) strings, leading someone to wonder if they can pass +// other string values instead, perhaps in an effort to break the API of their +// provider. Reboot() returns this error in this situation. +// +// Function identifies which function was called/which function is generating +// the error. +// Argument identifies which formal argument was responsible for producing the +// error. +// Value provides the value as it was passed into the function. +type ErrArgument struct { + Function, Argument string + Value interface{} +} + +// Error yields a useful diagnostic for debugging purposes. +func (e *ErrArgument) Error() string { + return fmt.Sprintf("Bad argument in call to %s, formal parameter %s, value %#v", e.Function, e.Argument, e.Value) +} + +func (e *ErrArgument) String() string { + return e.Error() +} + +// RebootMethod describes the mechanisms by which a server reboot can be requested. +type RebootMethod string + +// These constants determine how a server should be rebooted. +// See the Reboot() function for further details. +const ( + SoftReboot RebootMethod = "SOFT" + HardReboot RebootMethod = "HARD" + OSReboot = SoftReboot + PowerCycle = HardReboot +) + +// Reboot requests that a given server reboot. +// Two methods exist for rebooting a server: +// +// HardReboot (aka PowerCycle) restarts the server instance by physically cutting power to the machine, or if a VM, +// terminating it at the hypervisor level. +// It's done. Caput. Full stop. +// Then, after a brief while, power is restored or the VM instance restarted. +// +// SoftReboot (aka OSReboot) simply tells the OS to restart under its own procedures. +// E.g., in Linux, asking it to enter runlevel 6, or executing "sudo shutdown -r now", or by asking Windows to restart the machine. +func Reboot(client *gophercloud.ServiceClient, id string, how RebootMethod) ActionResult { + var res ActionResult + + if (how != SoftReboot) && (how != HardReboot) { + res.Err = &ErrArgument{ + Function: "Reboot", + Argument: "how", + Value: how, + } + return res + } + + _, res.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.AuthenticatedHeaders(), + OkCodes: []int{202}, + }) + + return res +} + +// RebuildOptsBuilder is an interface that allows extensions to override the +// default behaviour of rebuild options +type RebuildOptsBuilder interface { + ToServerRebuildMap() (map[string]interface{}, error) +} + +// RebuildOpts represents the configuration options used in a server rebuild +// operation +type RebuildOpts struct { + // Required. The ID of the image you want your server to be provisioned on + ImageID string + + // Name to set the server to + Name string + + // Required. The server's admin password + AdminPass string + + // AccessIPv4 [optional] provides a new IPv4 address for the instance. + AccessIPv4 string + + // AccessIPv6 [optional] provides a new IPv6 address for the instance. + AccessIPv6 string + + // Metadata [optional] contains key-value pairs (up to 255 bytes each) to attach to the server. + Metadata map[string]string + + // Personality [optional] includes 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.Body, + MoreHeaders: client.AuthenticatedHeaders(), + OkCodes: []int{202}, + }) + + return result +} + +// ResizeOptsBuilder is an interface that allows extensions to override the default structure of +// a Resize request. +type ResizeOptsBuilder interface { + ToServerResizeMap() (map[string]interface{}, error) +} + +// ResizeOpts represents the configuration options used to control a Resize operation. +type ResizeOpts struct { + // FlavorRef is the ID of the flavor you wish your server to become. + FlavorRef string +} + +// ToServerResizeMap formats a ResizeOpts as a map that can be used as a JSON request body to the +// Resize request. +func (opts ResizeOpts) ToServerResizeMap() (map[string]interface{}, error) { + resize := map[string]interface{}{ + "flavorRef": opts.FlavorRef, + } + + return map[string]interface{}{"resize": resize}, nil +} + +// Resize instructs the provider to change the flavor of the server. +// Note that this implies rebuilding it. +// Unfortunately, one cannot pass rebuild parameters to the resize function. +// When the resize completes, the server will be in RESIZE_VERIFY state. +// While in this state, you can explore the use of the new server's configuration. +// If you like it, call ConfirmResize() to commit the resize permanently. +// Otherwise, call RevertResize() to restore the old configuration. +func Resize(client *gophercloud.ServiceClient, id string, opts ResizeOptsBuilder) ActionResult { + var res ActionResult + reqBody, err := opts.ToServerResizeMap() + if err != nil { + res.Err = err + return res + } + + _, res.Err = perigee.Request("POST", actionURL(client, id), perigee.Options{ + ReqBody: reqBody, + MoreHeaders: client.AuthenticatedHeaders(), + OkCodes: []int{202}, + }) + + return res +} + +// ConfirmResize confirms a previous resize operation on a server. +// See Resize() for more details. +func ConfirmResize(client *gophercloud.ServiceClient, id string) ActionResult { + var res ActionResult + + _, res.Err = perigee.Request("POST", actionURL(client, id), perigee.Options{ + ReqBody: map[string]interface{}{"confirmResize": nil}, + MoreHeaders: client.AuthenticatedHeaders(), + OkCodes: []int{204}, + }) + + return res +} + +// RevertResize cancels a previous resize operation on a server. +// See Resize() for more details. +func RevertResize(client *gophercloud.ServiceClient, id string) ActionResult { + var res ActionResult + + _, res.Err = perigee.Request("POST", actionURL(client, id), perigee.Options{ + ReqBody: map[string]interface{}{"revertResize": nil}, + MoreHeaders: client.AuthenticatedHeaders(), + OkCodes: []int{202}, + }) + + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/servers/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/servers/requests_test.go new file mode 100644 index 00000000000..392e2d88d3d --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/servers/requests_test.go @@ -0,0 +1,176 @@ +package servers + +import ( + "net/http" + "testing" + + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestListServers(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleServerListSuccessfully(t) + + pages := 0 + err := List(client.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)) + } + th.CheckDeepEquals(t, ServerHerp, actual[0]) + th.CheckDeepEquals(t, ServerDerp, actual[1]) + + return true, nil + }) + + th.AssertNoErr(t, err) + + if pages != 1 { + t.Errorf("Expected 1 page, saw %d", pages) + } +} + +func TestCreateServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleServerCreationSuccessfully(t, SingleServerBody) + + actual, err := Create(client.ServiceClient(), CreateOpts{ + Name: "derp", + ImageRef: "f90f6034-2570-4974-8351-6b49732ef2eb", + FlavorRef: "1", + }).Extract() + th.AssertNoErr(t, err) + + th.CheckDeepEquals(t, ServerDerp, *actual) +} + +func TestDeleteServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleServerDeletionSuccessfully(t) + + res := Delete(client.ServiceClient(), "asdfasdfasdf") + th.AssertNoErr(t, res.Err) +} + +func TestGetServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleServerGetSuccessfully(t) + + client := client.ServiceClient() + actual, err := Get(client, "1234asdf").Extract() + if err != nil { + t.Fatalf("Unexpected Get error: %v", err) + } + + th.CheckDeepEquals(t, ServerDerp, *actual) +} + +func TestUpdateServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleServerUpdateSuccessfully(t) + + client := client.ServiceClient() + actual, err := Update(client, "1234asdf", UpdateOpts{Name: "new-name"}).Extract() + if err != nil { + t.Fatalf("Unexpected Update error: %v", err) + } + + th.CheckDeepEquals(t, ServerDerp, *actual) +} + +func TestChangeServerAdminPassword(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleAdminPasswordChangeSuccessfully(t) + + res := ChangeAdminPassword(client.ServiceClient(), "1234asdf", "new-password") + th.AssertNoErr(t, res.Err) +} + +func TestRebootServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleRebootSuccessfully(t) + + res := Reboot(client.ServiceClient(), "1234asdf", SoftReboot) + th.AssertNoErr(t, res.Err) +} + +func TestRebuildServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleRebuildSuccessfully(t, 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(client.ServiceClient(), "1234asdf", opts).Extract() + th.AssertNoErr(t, err) + + th.CheckDeepEquals(t, ServerDerp, *actual) +} + +func TestResizeServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/servers/1234asdf/action", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, `{ "resize": { "flavorRef": "2" } }`) + + w.WriteHeader(http.StatusAccepted) + }) + + res := Resize(client.ServiceClient(), "1234asdf", ResizeOpts{FlavorRef: "2"}) + th.AssertNoErr(t, res.Err) +} + +func TestConfirmResize(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/servers/1234asdf/action", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, `{ "confirmResize": null }`) + + w.WriteHeader(http.StatusNoContent) + }) + + res := ConfirmResize(client.ServiceClient(), "1234asdf") + th.AssertNoErr(t, res.Err) +} + +func TestRevertResize(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/servers/1234asdf/action", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, `{ "revertResize": null }`) + + w.WriteHeader(http.StatusAccepted) + }) + + res := RevertResize(client.ServiceClient(), "1234asdf") + th.AssertNoErr(t, res.Err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/servers/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/servers/results.go new file mode 100644 index 00000000000..53946baf66d --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/servers/results.go @@ -0,0 +1,150 @@ +package servers + +import ( + "github.com/mitchellh/mapstructure" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +type serverResult struct { + gophercloud.Result +} + +// Extract interprets any serverResult as a Server, if possible. +func (r serverResult) Extract() (*Server, error) { + if r.Err != nil { + return nil, r.Err + } + + var response struct { + Server Server `mapstructure:"server"` + } + + err := mapstructure.Decode(r.Body, &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 +} + +// DeleteResult temporarily contains the response from an Delete call. +type DeleteResult struct { + gophercloud.ErrResult +} + +// RebuildResult temporarily contains the response from a Rebuild call. +type RebuildResult struct { + serverResult +} + +// ActionResult represents the result of server action operations, like reboot +type ActionResult struct { + gophercloud.ErrResult +} + +// Server exposes only the standard OpenStack fields corresponding to a given server on the user's account. +type Server struct { + // ID uniquely identifies this server amongst all other servers, including those not accessible to the current tenant. + ID string + + // TenantID identifies the tenant owning this server resource. + TenantID string `mapstructure:"tenant_id"` + + // UserID uniquely identifies the user account owning the tenant. + UserID string `mapstructure:"user_id"` + + // Name contains the human-readable name for the server. + Name string + + // Updated and Created contain ISO-8601 timestamps of when the state of the server last changed, and when it was created. + Updated string + Created string + + HostID string + + // Status contains the current operational status of the server, such as IN_PROGRESS or ACTIVE. + Status string + + // Progress ranges from 0..100. + // A request made against the server completes only once Progress reaches 100. + Progress int + + // AccessIPv4 and AccessIPv6 contain the IP addresses of the server, suitable for remote access for administration. + AccessIPv4, AccessIPv6 string + + // Image refers to a JSON object, which itself indicates the OS image used to deploy the server. + Image map[string]interface{} + + // Flavor refers to a JSON object, which itself indicates the hardware configuration of the deployed server. + Flavor map[string]interface{} + + // Addresses includes a list of all IP addresses assigned to the server, keyed by pool. + Addresses map[string]interface{} + + // Metadata includes a list of all user-specified key-value pairs attached to the server. + Metadata map[string]interface{} + + // Links includes HTTP references to the itself, useful for passing along to other APIs that might want a server reference. + Links []interface{} + + // KeyName indicates which public key was injected into the server on launch. + KeyName string `json:"key_name" mapstructure:"key_name"` + + // AdminPass will generally be empty (""). However, it will contain the administrative password chosen when provisioning a new server without a set AdminPass setting in the first place. + // Note that this is the ONLY time this field will be valid. + AdminPass string `json:"adminPass" mapstructure:"adminPass"` +} + +// 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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/servers/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/servers/urls.go new file mode 100644 index 00000000000..57587abca73 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/servers/urls_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/servers/urls_test.go new file mode 100644 index 00000000000..cc895c90cf2 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/servers/util.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/servers/util.go new file mode 100644 index 00000000000..e6baf74165b --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/servers/util.go @@ -0,0 +1,20 @@ +package servers + +import "github.com/rackspace/gophercloud" + +// WaitForStatus will continually poll a server until it successfully transitions to a specified +// status. It will do this for at most the number of seconds specified. +func WaitForStatus(c *gophercloud.ServiceClient, id, status string, secs int) error { + return gophercloud.WaitFor(secs, func() (bool, error) { + current, err := Get(c, id).Extract() + if err != nil { + return false, err + } + + if current.Status == status { + return true, nil + } + + return false, nil + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/servers/util_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/servers/util_test.go new file mode 100644 index 00000000000..e192ae3bd9e --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/servers/util_test.go @@ -0,0 +1,38 @@ +package servers + +import ( + "fmt" + "net/http" + "testing" + "time" + + th "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestWaitForStatus(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/servers/4321", 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, ` + { + "server": { + "name": "the-server", + "id": "4321", + "status": "ACTIVE" + } + }`) + }) + + err := WaitForStatus(client.ServiceClient(), "4321", "ACTIVE", 0) + if err == nil { + t.Errorf("Expected error: 'Time Out in WaitFor'") + } + + err = WaitForStatus(client.ServiceClient(), "4321", "ACTIVE", 3) + th.CheckNoErr(t, err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/endpoint_location.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/endpoint_location.go new file mode 100644 index 00000000000..5a311e40855 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/endpoint_location_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/endpoint_location_test.go new file mode 100644 index 00000000000..4e0569ac1f8 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/extensions/delegate.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/extensions/delegate.go new file mode 100644 index 00000000000..fd6e80ea6f1 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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.PageResult) pagination.Page { + return ExtensionPage{ + ExtensionPage: common.ExtensionPage{SinglePageBase: pagination.SinglePageBase(r)}, + } + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/extensions/delegate_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/extensions/delegate_test.go new file mode 100644 index 00000000000..504118a825b --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/extensions/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/extensions/doc.go new file mode 100644 index 00000000000..791e4e391da --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/extensions/doc.go @@ -0,0 +1,3 @@ +// Package extensions provides information and interaction with the +// different extensions available for the OpenStack Identity service. +package extensions diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/extensions/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/extensions/fixtures.go new file mode 100644 index 00000000000..96cb7d24a13 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tenants/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tenants/doc.go new file mode 100644 index 00000000000..0c2d49d5670 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tenants/doc.go @@ -0,0 +1,7 @@ +// Package tenants provides information and interaction with the +// tenants API resource for the OpenStack Identity service. +// +// See http://developer.openstack.org/api-ref-identity-v2.html#identity-auth-v2 +// and http://developer.openstack.org/api-ref-identity-v2.html#admin-tenants +// for more information. +package tenants diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tenants/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tenants/fixtures.go new file mode 100644 index 00000000000..7f044ac3b2d --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tenants/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tenants/requests.go new file mode 100644 index 00000000000..5a359f5c9e2 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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.PageResult) pagination.Page { + return TenantPage{pagination.LinkedPageBase{PageResult: r}} + } + + url := listURL(client) + if opts != nil { + q, err := gophercloud.BuildQueryString(opts) + if err != nil { + return pagination.Pager{Err: err} + } + url += q.String() + } + + return pagination.NewPager(client, url, createPage) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tenants/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tenants/requests_test.go new file mode 100644 index 00000000000..e8f172dd183 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tenants/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tenants/results.go new file mode 100644 index 00000000000..c1220c384bc --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tenants/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tenants/urls.go new file mode 100644 index 00000000000..1dd6ce023f9 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tokens/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tokens/doc.go new file mode 100644 index 00000000000..31cacc5e17b --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tokens/doc.go @@ -0,0 +1,5 @@ +// Package tokens provides information and interaction with the token API +// resource for the OpenStack Identity service. +// For more information, see: +// http://developer.openstack.org/api-ref-identity-v2.html#identity-auth-v2 +package tokens diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tokens/errors.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tokens/errors.go new file mode 100644 index 00000000000..3a9172e0cc7 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tokens/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tokens/fixtures.go new file mode 100644 index 00000000000..1cb0d0527b9 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tokens/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tokens/requests.go new file mode 100644 index 00000000000..87c923a2b46 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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.Result{Err: err}} + } + + var result CreateResult + _, result.Err = perigee.Request("POST", CreateURL(client), perigee.Options{ + ReqBody: &request, + Results: &result.Body, + OkCodes: []int{200, 203}, + }) + return result +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tokens/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tokens/requests_test.go new file mode 100644 index 00000000000..2f02825a47a --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tokens/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tokens/results.go new file mode 100644 index 00000000000..1eddb9d5644 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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.Result +} + +// ExtractToken returns the just-created Token from a CreateResult. +func (result CreateResult) ExtractToken() (*Token, error) { + if result.Err != nil { + return nil, result.Err + } + + var response struct { + Access struct { + Token struct { + Expires string `mapstructure:"expires"` + ID string `mapstructure:"id"` + Tenant tenants.Tenant `mapstructure:"tenant"` + } `mapstructure:"token"` + } `mapstructure:"access"` + } + + err := mapstructure.Decode(result.Body, &response) + if err != nil { + return nil, err + } + + expiresTs, err := time.Parse(gophercloud.RFC3339Milli, response.Access.Token.Expires) + if err != nil { + return nil, err + } + + return &Token{ + ID: response.Access.Token.ID, + ExpiresAt: expiresTs, + Tenant: response.Access.Token.Tenant, + }, nil +} + +// ExtractServiceCatalog returns the ServiceCatalog that was generated along with the user's Token. +func (result CreateResult) ExtractServiceCatalog() (*ServiceCatalog, error) { + if result.Err != nil { + return nil, result.Err + } + + var response struct { + Access struct { + Entries []CatalogEntry `mapstructure:"serviceCatalog"` + } `mapstructure:"access"` + } + + err := mapstructure.Decode(result.Body, &response) + if err != nil { + return nil, err + } + + return &ServiceCatalog{Entries: response.Access.Entries}, nil +} + +// createErr quickly packs an error in a CreateResult. +func createErr(err error) CreateResult { + return CreateResult{gophercloud.Result{Err: err}} +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tokens/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tokens/urls.go new file mode 100644 index 00000000000..cd4c696c7a7 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/endpoints/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/endpoints/doc.go new file mode 100644 index 00000000000..85163949a83 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/endpoints/doc.go @@ -0,0 +1,6 @@ +// Package endpoints provides information and interaction with the service +// endpoints API resource in the OpenStack Identity service. +// +// For more information, see: +// http://developer.openstack.org/api-ref-identity-v3.html#endpoints-v3 +package endpoints diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/endpoints/errors.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/endpoints/errors.go new file mode 100644 index 00000000000..854957ff98d --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/endpoints/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/endpoints/requests.go new file mode 100644 index 00000000000..7bdb7cef2e8 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/endpoints/requests.go @@ -0,0 +1,133 @@ +package endpoints + +import ( + "github.com/racker/perigee" + "github.com/rackspace/gophercloud" + "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.AuthenticatedHeaders(), + ReqBody: &reqBody, + Results: &result.Body, + OkCodes: []int{201}, + }) + return result +} + +// ListOpts allows finer control over the endpoints returned by a List call. +// All fields are optional. +type ListOpts struct { + Availability gophercloud.Availability `q:"interface"` + ServiceID string `q:"service_id"` + Page int `q:"page"` + PerPage int `q:"per_page"` +} + +// List enumerates endpoints in a paginated collection, optionally filtered by ListOpts criteria. +func List(client *gophercloud.ServiceClient, opts ListOpts) pagination.Pager { + u := listURL(client) + q, err := gophercloud.BuildQueryString(opts) + if err != nil { + return pagination.Pager{Err: err} + } + u += q.String() + createPage := func(r pagination.PageResult) pagination.Page { + return EndpointPage{pagination.LinkedPageBase{PageResult: r}} + } + + 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.AuthenticatedHeaders(), + ReqBody: &reqBody, + Results: &result.Body, + OkCodes: []int{200}, + }) + return result +} + +// Delete removes an endpoint from the service catalog. +func Delete(client *gophercloud.ServiceClient, endpointID string) DeleteResult { + var res DeleteResult + _, res.Err = perigee.Request("DELETE", endpointURL(client, endpointID), perigee.Options{ + MoreHeaders: client.AuthenticatedHeaders(), + OkCodes: []int{204}, + }) + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/endpoints/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/endpoints/requests_test.go new file mode 100644 index 00000000000..80687c4cb70 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/endpoints/requests_test.go @@ -0,0 +1,226 @@ +package endpoints + +import ( + "fmt" + "net/http" + "reflect" + "testing" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" + "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +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", client.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/" + } + } + `) + }) + + actual, err := Create(client.ServiceClient(), 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", client.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 + } + } + `) + }) + + count := 0 + List(client.ServiceClient(), 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", client.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/" + } + } + `) + }) + + actual, err := Update(client.ServiceClient(), "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", client.TokenID) + + w.WriteHeader(http.StatusNoContent) + }) + + res := Delete(client.ServiceClient(), "34") + testhelper.AssertNoErr(t, res.Err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/endpoints/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/endpoints/results.go new file mode 100644 index 00000000000..128112295a2 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/endpoints/results.go @@ -0,0 +1,82 @@ +package endpoints + +import ( + "github.com/mitchellh/mapstructure" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +type commonResult struct { + gophercloud.Result +} + +// 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.Body, &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.Result{Err: err}}} +} + +// UpdateResult is the deferred result of an Update call. +type UpdateResult struct { + commonResult +} + +// DeleteResult is the deferred result of an Delete call. +type DeleteResult struct { + gophercloud.ErrResult +} + +// 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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/endpoints/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/endpoints/urls.go new file mode 100644 index 00000000000..547d7b102a5 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/endpoints/urls_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/endpoints/urls_test.go new file mode 100644 index 00000000000..0b183b7434a --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/services/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/services/doc.go new file mode 100644 index 00000000000..fa56411856b --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/services/doc.go @@ -0,0 +1,3 @@ +// Package services provides information and interaction with the services API +// resource for the OpenStack Identity service. +package services diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/services/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/services/requests.go new file mode 100644 index 00000000000..1d9aaa873a2 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/services/requests.go @@ -0,0 +1,91 @@ +package services + +import ( + "github.com/racker/perigee" + "github.com/rackspace/gophercloud" + "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.AuthenticatedHeaders(), + ReqBody: &req, + Results: &result.Body, + OkCodes: []int{201}, + }) + return result +} + +// ListOpts allows you to query the List method. +type ListOpts struct { + ServiceType string `q:"type"` + PerPage int `q:"perPage"` + Page int `q:"page"` +} + +// List enumerates the services available to a specific user. +func List(client *gophercloud.ServiceClient, opts ListOpts) pagination.Pager { + u := listURL(client) + q, err := gophercloud.BuildQueryString(opts) + if err != nil { + return pagination.Pager{Err: err} + } + u += q.String() + createPage := func(r pagination.PageResult) pagination.Page { + return ServicePage{pagination.LinkedPageBase{PageResult: 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.AuthenticatedHeaders(), + Results: &result.Body, + 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.AuthenticatedHeaders(), + ReqBody: &req, + Results: &result.Body, + 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) DeleteResult { + var res DeleteResult + _, res.Err = perigee.Request("DELETE", serviceURL(client, serviceID), perigee.Options{ + MoreHeaders: client.AuthenticatedHeaders(), + OkCodes: []int{204}, + }) + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/services/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/services/requests_test.go new file mode 100644 index 00000000000..32e6d1ba7d7 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/services/requests_test.go @@ -0,0 +1,209 @@ +package services + +import ( + "fmt" + "net/http" + "reflect" + "testing" + + "github.com/rackspace/gophercloud/pagination" + "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +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", client.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" + } + }`) + }) + + result, err := Create(client.ServiceClient(), "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", client.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" + } + ] + } + `) + }) + + count := 0 + err := List(client.ServiceClient(), 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", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, ` + { + "service": { + "description": "Service One", + "id": "12345", + "name": "service-one", + "type": "identity" + } + } + `) + }) + + result, err := Get(client.ServiceClient(), "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", client.TokenID) + testhelper.TestJSONRequest(t, r, `{ "type": "lasermagic" }`) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, ` + { + "service": { + "id": "12345", + "type": "lasermagic" + } + } + `) + }) + + result, err := Update(client.ServiceClient(), "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", client.TokenID) + + w.WriteHeader(http.StatusNoContent) + }) + + res := Delete(client.ServiceClient(), "12345") + testhelper.AssertNoErr(t, res.Err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/services/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/services/results.go new file mode 100644 index 00000000000..1d0d1412806 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/services/results.go @@ -0,0 +1,80 @@ +package services + +import ( + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" + + "github.com/mitchellh/mapstructure" +) + +type commonResult struct { + gophercloud.Result +} + +// 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.Body, &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 +} + +// DeleteResult is the deferred result of an Delete call. +type DeleteResult struct { + gophercloud.ErrResult +} + +// 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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/services/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/services/urls.go new file mode 100644 index 00000000000..85443a48a09 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/services/urls_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/services/urls_test.go new file mode 100644 index 00000000000..5a31b32316c --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/tokens/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/tokens/doc.go new file mode 100644 index 00000000000..76ff5f47387 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/tokens/doc.go @@ -0,0 +1,6 @@ +// Package tokens provides information and interaction with the token API +// resource for the OpenStack Identity service. +// +// For more information, see: +// http://developer.openstack.org/api-ref-identity-v3.html#tokens-v3 +package tokens diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/tokens/errors.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/tokens/errors.go new file mode 100644 index 00000000000..44761092bb9 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/tokens/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/tokens/requests.go new file mode 100644 index 00000000000..5ca1031c415 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/tokens/requests.go @@ -0,0 +1,286 @@ +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.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.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.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.Body, + 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.Body, + 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) RevokeResult { + var res RevokeResult + _, res.Err = perigee.Request("DELETE", tokenURL(c), perigee.Options{ + MoreHeaders: subjectTokenHeaders(c, token), + OkCodes: []int{204}, + }) + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/tokens/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/tokens/requests_test.go new file mode 100644 index 00000000000..2b26e4ad368 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/tokens/requests_test.go @@ -0,0 +1,514 @@ +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{ + ProviderClient: &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{ + ProviderClient: &gophercloud.ProviderClient{}, + Endpoint: testhelper.Endpoint(), + } + if includeToken { + client.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{ + ProviderClient: &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{ + ProviderClient: &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{ + ProviderClient: &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) + + res := Revoke(&client, "abcdef12345") + testhelper.AssertNoErr(t, res.Err) +} + +func TestRevokeRequestError(t *testing.T) { + testhelper.SetupHTTP() + defer testhelper.TeardownHTTP() + client := prepareAuthTokenHandler(t, "DELETE", http.StatusNotFound) + + res := Revoke(&client, "abcdef12345") + if res.Err == nil { + t.Errorf("Missing expected error from Revoke") + } +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/tokens/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/tokens/results.go new file mode 100644 index 00000000000..d1fff4c2a5f --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/tokens/results.go @@ -0,0 +1,73 @@ +package tokens + +import ( + "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.Result +} + +// 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.Body, &response) + if err != nil { + return nil, err + } + + // Attempt to parse the timestamp. + token.ExpiresAt, err = time.Parse(gophercloud.RFC3339Milli, response.Token.ExpiresAt) + + return &token, err +} + +// 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{Result: gophercloud.Result{Err: err}}, + } +} + +// GetResult is the deferred response from a Get call. +type GetResult struct { + commonResult +} + +// RevokeResult is the deferred response from a Revoke call. +type RevokeResult struct { + commonResult +} + +// Token is a string that grants a user access to a controlled set of services in an OpenStack provider. +// Each Token is valid for a set length of time. +type Token struct { + // ID is the issued token. + ID string + + // ExpiresAt is the timestamp at which this token will no longer be accepted. + ExpiresAt time.Time +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/tokens/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/tokens/urls.go new file mode 100644 index 00000000000..360b60a82fb --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/tokens/urls_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/tokens/urls_test.go new file mode 100644 index 00000000000..549c398620a --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/apiversions/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/apiversions/doc.go new file mode 100644 index 00000000000..0208ee20ecb --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/apiversions/errors.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/apiversions/errors.go new file mode 100644 index 00000000000..76bdb14f750 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/apiversions/errors.go @@ -0,0 +1 @@ +package apiversions diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/apiversions/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/apiversions/requests.go new file mode 100644 index 00000000000..9fb6de14110 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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.PageResult) 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.PageResult) pagination.Page { + return APIVersionResourcePage{pagination.SinglePageBase(r)} + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/apiversions/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/apiversions/requests_test.go new file mode 100644 index 00000000000..d35af9f0c6b --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/apiversions/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/apiversions/results.go new file mode 100644 index 00000000000..97159341ffb --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/apiversions/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/apiversions/urls.go new file mode 100644 index 00000000000..58aa2b61f8b --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/apiversions/urls_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/apiversions/urls_test.go new file mode 100644 index 00000000000..7dd069c94f5 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/common/common_tests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/common/common_tests.go new file mode 100644 index 00000000000..41603510d62 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/delegate.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/delegate.go new file mode 100644 index 00000000000..d08e1fda977 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/delegate_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/delegate_test.go new file mode 100644 index 00000000000..3d2ac78d482 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/external/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/external/doc.go new file mode 100644 index 00000000000..dad3a844f75 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/external/doc.go @@ -0,0 +1,3 @@ +// Package external provides information and interaction with the external +// extension for the OpenStack Networking service. +package external diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/external/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/external/requests.go new file mode 100644 index 00000000000..2f04593db95 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/external/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/external/results.go new file mode 100644 index 00000000000..1c173c07a34 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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 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.Body) +} + +// 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.Body) +} + +// 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.Body) +} + +// 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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/external/results_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/external/results_test.go new file mode 100644 index 00000000000..916cd2cfd03 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/doc.go new file mode 100644 index 00000000000..d533458267e --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/floatingips/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/floatingips/requests.go new file mode 100644 index 00000000000..d23f9e2b5ac --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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.PageResult) pagination.Page { + return FloatingIPPage{pagination.LinkedPageBase{PageResult: 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.AuthenticatedHeaders(), + ReqBody: &reqBody, + Results: &res.Body, + 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.AuthenticatedHeaders(), + Results: &res.Body, + 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.AuthenticatedHeaders(), + ReqBody: &reqBody, + Results: &res.Body, + 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.AuthenticatedHeaders(), + OkCodes: []int{204}, + }) + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/floatingips/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/floatingips/requests_test.go new file mode 100644 index 00000000000..19614be2ef1 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/floatingips/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/floatingips/results.go new file mode 100644 index 00000000000..a1c7afe2cee --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/floatingips/results.go @@ -0,0 +1,127 @@ +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 this 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.Result +} + +// 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.Body, &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 struct { + gophercloud.ErrResult +} + +// 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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/floatingips/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/floatingips/urls.go new file mode 100644 index 00000000000..355f20dc096 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/routers/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/routers/requests.go new file mode 100644 index 00000000000..e3a144171b1 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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.PageResult) pagination.Page { + return RouterPage{pagination.LinkedPageBase{PageResult: 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.AuthenticatedHeaders(), + ReqBody: &reqBody, + Results: &res.Body, + 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.AuthenticatedHeaders(), + Results: &res.Body, + 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.AuthenticatedHeaders(), + ReqBody: &reqBody, + Results: &res.Body, + 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.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.AuthenticatedHeaders(), + ReqBody: &body, + Results: &res.Body, + 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.AuthenticatedHeaders(), + ReqBody: &body, + Results: &res.Body, + OkCodes: []int{200}, + }) + + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/routers/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/routers/requests_test.go new file mode 100644 index 00000000000..c34264daee5 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/routers/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/routers/results.go new file mode 100644 index 00000000000..bdad4cb2fd6 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/routers/results.go @@ -0,0 +1,161 @@ +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.Result +} + +// 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.Body, &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 struct { + gophercloud.ErrResult +} + +// 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.Result +} + +// 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.Body, &res) + + return res, err +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/routers/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/routers/urls.go new file mode 100644 index 00000000000..bc22c2a8a82 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/doc.go new file mode 100644 index 00000000000..bc1fc282f4e --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/doc.go @@ -0,0 +1,3 @@ +// Package lbaas provides information and interaction with the Load Balancer +// as a Service extension for the OpenStack Networking service. +package lbaas diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/members/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/members/requests.go new file mode 100644 index 00000000000..58ec580dbce --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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.PageResult) pagination.Page { + return MemberPage{pagination.LinkedPageBase{PageResult: 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.AuthenticatedHeaders(), + ReqBody: &reqBody, + Results: &res.Body, + 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.AuthenticatedHeaders(), + Results: &res.Body, + 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.AuthenticatedHeaders(), + ReqBody: &reqBody, + Results: &res.Body, + 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.AuthenticatedHeaders(), + OkCodes: []int{204}, + }) + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/members/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/members/requests_test.go new file mode 100644 index 00000000000..dc1ece321ff --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/members/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/members/results.go new file mode 100644 index 00000000000..3cad339b770 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/members/results.go @@ -0,0 +1,122 @@ +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.Result +} + +// 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.Body, &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 struct { + gophercloud.ErrResult +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/members/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/members/urls.go new file mode 100644 index 00000000000..94b57e4c586 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/monitors/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/monitors/requests.go new file mode 100644 index 00000000000..e2b590ecb5a --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/monitors/requests.go @@ -0,0 +1,282 @@ +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.PageResult) pagination.Page { + return MonitorPage{pagination.LinkedPageBase{PageResult: 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") + errDelayMustGETimeout = fmt.Errorf("Delay must be greater than or equal to timeout") +) + +// 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 opts.Delay < opts.Timeout { + res.Err = errDelayMustGETimeout + } + 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.AuthenticatedHeaders(), + ReqBody: &reqBody, + Results: &res.Body, + 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.AuthenticatedHeaders(), + Results: &res.Body, + 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 { + var res UpdateResult + + if opts.Delay > 0 && opts.Timeout > 0 && opts.Delay < opts.Timeout { + res.Err = errDelayMustGETimeout + } + + 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, + }} + + _, res.Err = perigee.Request("PUT", resourceURL(c, id), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + ReqBody: &reqBody, + Results: &res.Body, + 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.AuthenticatedHeaders(), + OkCodes: []int{204}, + }) + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/monitors/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/monitors/requests_test.go new file mode 100644 index 00000000000..79a99bf8a25 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/monitors/requests_test.go @@ -0,0 +1,312 @@ +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 TestDelayMustBeGreaterOrEqualThanTimeout(t *testing.T) { + _, err := Create(fake.ServiceClient(), CreateOpts{ + Type: "HTTP", + Delay: 1, + Timeout: 10, + MaxRetries: 5, + URLPath: "/check", + ExpectedCodes: "200-299", + }).Extract() + + if err == nil { + t.Fatalf("Expected error, got none") + } + + _, err = Update(fake.ServiceClient(), "453105b9-1754-413f-aab1-55f1af620750", UpdateOpts{ + Delay: 1, + Timeout: 10, + }).Extract() + + if err == nil { + t.Fatalf("Expected error, got none") + } +} + +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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/monitors/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/monitors/results.go new file mode 100644 index 00000000000..d595abd5407 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/monitors/results.go @@ -0,0 +1,147 @@ +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.Result +} + +// 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.Body, &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 struct { + gophercloud.ErrResult +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/monitors/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/monitors/urls.go new file mode 100644 index 00000000000..46e84bbf521 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/pools/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/pools/requests.go new file mode 100644 index 00000000000..ca8d33b8d5d --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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.PageResult) pagination.Page { + return PoolPage{pagination.LinkedPageBase{PageResult: 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.AuthenticatedHeaders(), + ReqBody: &reqBody, + Results: &res.Body, + 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.AuthenticatedHeaders(), + Results: &res.Body, + 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.AuthenticatedHeaders(), + ReqBody: &reqBody, + Results: &res.Body, + 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.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.AuthenticatedHeaders(), + ReqBody: &reqBody, + Results: &res.Body, + 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.AuthenticatedHeaders(), + OkCodes: []int{204}, + }) + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/pools/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/pools/requests_test.go new file mode 100644 index 00000000000..6da29a6b8e6 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/pools/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/pools/results.go new file mode 100644 index 00000000000..07ec85eda4a --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/pools/results.go @@ -0,0 +1,146 @@ +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.Result +} + +// 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.Body, &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 struct { + gophercloud.ErrResult +} + +// AssociateResult represents the result of an association operation. +type AssociateResult struct { + commonResult +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/pools/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/pools/urls.go new file mode 100644 index 00000000000..6cd15b00261 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/vips/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/vips/requests.go new file mode 100644 index 00000000000..ec929d63976 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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.PageResult) pagination.Page { + return VIPPage{pagination.LinkedPageBase{PageResult: 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.AuthenticatedHeaders(), + ReqBody: &reqBody, + Results: &res.Body, + 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.AuthenticatedHeaders(), + Results: &res.Body, + 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.AuthenticatedHeaders(), + ReqBody: &reqBody, + Results: &res.Body, + 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.AuthenticatedHeaders(), + OkCodes: []int{204}, + }) + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/vips/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/vips/requests_test.go new file mode 100644 index 00000000000..430f1a1eebf --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/vips/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/vips/results.go new file mode 100644 index 00000000000..e1092e780ec --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/vips/results.go @@ -0,0 +1,166 @@ +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.Result +} + +// 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.Body, &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 struct { + gophercloud.ErrResult +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/vips/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/vips/urls.go new file mode 100644 index 00000000000..2b6f67e71d6 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/provider/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/provider/doc.go new file mode 100644 index 00000000000..373da44f84d --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/ operation, this implies the user submitting the +// request is not authorized to view or manipulate provider network attributes. +package provider diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/provider/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/provider/results.go new file mode 100644 index 00000000000..34535845876 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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.Body, &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.Body, &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.Body, &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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/provider/results_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/provider/results_test.go new file mode 100644 index 00000000000..9801b2e5e3a --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/doc.go new file mode 100644 index 00000000000..8ef455ffb39 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/groups/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/groups/requests.go new file mode 100644 index 00000000000..0c970ae6f2e --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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.PageResult) pagination.Page { + return SecGroupPage{pagination.LinkedPageBase{PageResult: 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.AuthenticatedHeaders(), + ReqBody: &reqBody, + Results: &res.Body, + 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.AuthenticatedHeaders(), + Results: &res.Body, + 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.AuthenticatedHeaders(), + OkCodes: []int{204}, + }) + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/groups/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/groups/requests_test.go new file mode 100644 index 00000000000..5f074c72f39 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/groups/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/groups/results.go new file mode 100644 index 00000000000..49db261c22e --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/groups/results.go @@ -0,0 +1,108 @@ +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.Result +} + +// 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.Body, &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 struct { + gophercloud.ErrResult +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/groups/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/groups/urls.go new file mode 100644 index 00000000000..84f7324f090 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/rules/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/rules/requests.go new file mode 100644 index 00000000000..edaebe82cd3 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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.PageResult) pagination.Page { + return SecGroupRulePage{pagination.LinkedPageBase{PageResult: 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.AuthenticatedHeaders(), + ReqBody: &reqBody, + Results: &res.Body, + 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.AuthenticatedHeaders(), + Results: &res.Body, + 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.AuthenticatedHeaders(), + OkCodes: []int{204}, + }) + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/rules/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/rules/requests_test.go new file mode 100644 index 00000000000..b5afef31ed8 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/rules/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/rules/results.go new file mode 100644 index 00000000000..6e138576893 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/rules/results.go @@ -0,0 +1,133 @@ +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.Result +} + +// 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.Body, &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 struct { + gophercloud.ErrResult +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/rules/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/rules/urls.go new file mode 100644 index 00000000000..8e2b2bb28d2 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/networks/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/networks/doc.go new file mode 100644 index 00000000000..c87a7ce2708 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/networks/errors.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/networks/errors.go new file mode 100644 index 00000000000..83c4a6a8683 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/networks/errors.go @@ -0,0 +1 @@ +package networks diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/networks/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/networks/requests.go new file mode 100644 index 00000000000..eaa713600ea --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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 { + ToNetworkListQuery() (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"` +} + +// ToNetworkListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToNetworkListQuery() (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.ToNetworkListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return NetworkPage{pagination.LinkedPageBase{PageResult: 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.AuthenticatedHeaders(), + Results: &res.Body, + 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.AuthenticatedHeaders(), + ReqBody: &reqBody, + Results: &res.Body, + 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.AuthenticatedHeaders(), + ReqBody: &reqBody, + Results: &res.Body, + 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.AuthenticatedHeaders(), + OkCodes: []int{204}, + }) + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/networks/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/networks/requests_test.go new file mode 100644 index 00000000000..a263b7b16b2 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/networks/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/networks/results.go new file mode 100644 index 00000000000..3ecedde9ac0 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/networks/results.go @@ -0,0 +1,116 @@ +package networks + +import ( + "github.com/mitchellh/mapstructure" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +type commonResult struct { + gophercloud.Result +} + +// 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.Body, &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 struct { + gophercloud.ErrResult +} + +// 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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/networks/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/networks/urls.go new file mode 100644 index 00000000000..33c23875caa --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/networks/urls_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/networks/urls_test.go new file mode 100644 index 00000000000..caf77dbe041 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/ports/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/ports/doc.go new file mode 100644 index 00000000000..f16a4bb01be --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/ports/errors.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/ports/errors.go new file mode 100644 index 00000000000..111d977e749 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/ports/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/ports/requests.go new file mode 100644 index 00000000000..3399907dd3e --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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 { + ToPortListQuery() (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"` +} + +// ToPortListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToPortListQuery() (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 a user with +// administrative rights. +func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(c) + if opts != nil { + query, err := opts.ToPortListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return PortPage{pagination.LinkedPageBase{PageResult: 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.AuthenticatedHeaders(), + Results: &res.Body, + 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.AuthenticatedHeaders(), + ReqBody: &reqBody, + Results: &res.Body, + 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.AuthenticatedHeaders(), + ReqBody: &reqBody, + Results: &res.Body, + 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.AuthenticatedHeaders(), + OkCodes: []int{204}, + }) + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/ports/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/ports/requests_test.go new file mode 100644 index 00000000000..9e323efa3a9 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/ports/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/ports/results.go new file mode 100644 index 00000000000..2511ff53b21 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/ports/results.go @@ -0,0 +1,126 @@ +package ports + +import ( + "github.com/mitchellh/mapstructure" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +type commonResult struct { + gophercloud.Result +} + +// 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.Body, &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 struct { + gophercloud.ErrResult +} + +// 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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/ports/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/ports/urls.go new file mode 100644 index 00000000000..6d0572f1fb2 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/ports/urls_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/ports/urls_test.go new file mode 100644 index 00000000000..7fadd4dcb70 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/subnets/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/subnets/doc.go new file mode 100644 index 00000000000..43e8296c7f2 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/subnets/errors.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/subnets/errors.go new file mode 100644 index 00000000000..0db0a6e6047 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/subnets/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/subnets/requests.go new file mode 100644 index 00000000000..cd7c663c2dc --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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 { + ToSubnetListQuery() (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"` +} + +// ToSubnetListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToSubnetListQuery() (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 a user with +// administrative rights. +func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(c) + if opts != nil { + query, err := opts.ToSubnetListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return SubnetPage{pagination.LinkedPageBase{PageResult: 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.AuthenticatedHeaders(), + Results: &res.Body, + 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.AuthenticatedHeaders(), + ReqBody: &reqBody, + Results: &res.Body, + 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.AuthenticatedHeaders(), + ReqBody: &reqBody, + Results: &res.Body, + 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.AuthenticatedHeaders(), + OkCodes: []int{204}, + }) + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/subnets/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/subnets/requests_test.go new file mode 100644 index 00000000000..987064ada64 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/subnets/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/subnets/results.go new file mode 100644 index 00000000000..1910f17dd96 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/subnets/results.go @@ -0,0 +1,132 @@ +package subnets + +import ( + "github.com/mitchellh/mapstructure" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +type commonResult struct { + gophercloud.Result +} + +// 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.Body, &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 struct { + gophercloud.ErrResult +} + +// 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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/subnets/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/subnets/urls.go new file mode 100644 index 00000000000..0d023689415 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/subnets/urls_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/subnets/urls_test.go new file mode 100644 index 00000000000..aeeddf35495 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/accounts/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/accounts/doc.go new file mode 100644 index 00000000000..f5f894a9e56 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/accounts/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/accounts/fixtures.go new file mode 100644 index 00000000000..3dad0c5a9be --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/accounts/fixtures.go @@ -0,0 +1,38 @@ +// +build fixtures + +package accounts + +import ( + "net/http" + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +// HandleGetAccountSuccessfully creates an HTTP handler at `/` on the test handler mux that +// responds with a `Get` response. +func HandleGetAccountSuccessfully(t *testing.T) { + 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) + }) +} + +// HandleUpdateAccountSuccessfully creates an HTTP handler at `/` on the test handler mux that +// responds with a `Update` response. +func HandleUpdateAccountSuccessfully(t *testing.T) { + 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) + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/accounts/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/accounts/requests.go new file mode 100644 index 00000000000..e6f5f9594cb --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/accounts/requests.go @@ -0,0 +1,106 @@ +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 +// ExtractHeader method on the GetResult. +func Get(c *gophercloud.ServiceClient, opts GetOptsBuilder) GetResult { + var res GetResult + h := c.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.Header = resp.HttpResponse.Header + 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 Extract method on the UpdateResult. +func Update(c *gophercloud.ServiceClient, opts UpdateOptsBuilder) UpdateResult { + var res UpdateResult + h := c.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.Header = resp.HttpResponse.Header + res.Err = err + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/accounts/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/accounts/requests_test.go new file mode 100644 index 00000000000..d6dc26b650e --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/accounts/requests_test.go @@ -0,0 +1,33 @@ +package accounts + +import ( + "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() + HandleGetAccountSuccessfully(t) + + options := &UpdateOpts{Metadata: map[string]string{"gophercloud-test": "accounts"}} + res := Update(fake.ServiceClient(), options) + th.AssertNoErr(t, res.Err) +} + +func TestGetAccount(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleUpdateAccountSuccessfully(t) + + 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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/accounts/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/accounts/results.go new file mode 100644 index 00000000000..abae02659cf --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/accounts/results.go @@ -0,0 +1,34 @@ +package accounts + +import ( + "strings" + + "github.com/rackspace/gophercloud" +) + +// GetResult is returned from a call to the Get function. +type GetResult struct { + gophercloud.HeaderResult +} + +// 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.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. +type UpdateResult struct { + gophercloud.HeaderResult +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/accounts/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/accounts/urls.go new file mode 100644 index 00000000000..9952fe43451 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/accounts/urls_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/accounts/urls_test.go new file mode 100644 index 00000000000..074d52dfd5c --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers/doc.go new file mode 100644 index 00000000000..5fed5537f13 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers/fixtures.go new file mode 100644 index 00000000000..1c0a915cb7d --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers/fixtures.go @@ -0,0 +1,132 @@ +// +build fixtures + +package containers + +import ( + "fmt" + "net/http" + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +// ExpectedListInfo is the result expected from a call to `List` when full +// info is requested. +var ExpectedListInfo = []Container{ + Container{ + Count: 0, + Bytes: 0, + Name: "janeausten", + }, + Container{ + Count: 1, + Bytes: 14, + Name: "marktwain", + }, +} + +// ExpectedListNames is the result expected from a call to `List` when just +// container names are requested. +var ExpectedListNames = []string{"janeausten", "marktwain"} + +// HandleListContainerInfoSuccessfully creates an HTTP handler at `/` on the test handler mux that +// responds with a `List` response when full info is requested. +func HandleListContainerInfoSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + + w.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) + } + }) +} + +// HandleListContainerNamesSuccessfully creates an HTTP handler at `/` on the test handler mux that +// responds with a `ListNames` response when only container names are requested. +func HandleListContainerNamesSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "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) + } + }) +} + +// HandleCreateContainerSuccessfully creates an HTTP handler at `/testContainer` on the test handler mux that +// responds with a `Create` response. +func HandleCreateContainerSuccessfully(t *testing.T) { + 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) + }) +} + +// HandleDeleteContainerSuccessfully creates an HTTP handler at `/testContainer` on the test handler mux that +// responds with a `Delete` response. +func HandleDeleteContainerSuccessfully(t *testing.T) { + 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) + }) +} + +// HandleUpdateContainerSuccessfully creates an HTTP handler at `/testContainer` on the test handler mux that +// responds with a `Update` response. +func HandleUpdateContainerSuccessfully(t *testing.T) { + 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) + }) +} + +// HandleGetContainerSuccessfully creates an HTTP handler at `/testContainer` on the test handler mux that +// responds with a `Get` response. +func HandleGetContainerSuccessfully(t *testing.T) { + 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) + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers/requests.go new file mode 100644 index 00000000000..9f3b2af0a6f --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers/requests.go @@ -0,0 +1,204 @@ +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.PageResult) pagination.Page { + p := ContainerPage{pagination.MarkerPageBase{PageResult: 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.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, 202, 204}, + }) + res.Header = resp.HttpResponse.Header + res.Err = err + return res +} + +// Delete is a function that deletes a container. +func Delete(c *gophercloud.ServiceClient, containerName string) DeleteResult { + var res DeleteResult + _, res.Err = perigee.Request("DELETE", deleteURL(c, containerName), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + OkCodes: []int{202, 204}, + }) + 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.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{202, 204}, + }) + res.Header = resp.HttpResponse.Header + 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.AuthenticatedHeaders(), + OkCodes: []int{200, 204}, + }) + res.Header = resp.HttpResponse.Header + res.Err = err + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers/requests_test.go new file mode 100644 index 00000000000..d0ce7f1e585 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers/requests_test.go @@ -0,0 +1,91 @@ +package containers + +import ( + "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() + HandleListContainerInfoSuccessfully(t) + + count := 0 + err := List(fake.ServiceClient(), &ListOpts{Full: true}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractInfo(page) + th.AssertNoErr(t, err) + + th.CheckDeepEquals(t, ExpectedListInfo, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, count, 1) +} + +func TestListContainerNames(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListContainerNamesSuccessfully(t) + + count := 0 + err := 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 + } + + th.CheckDeepEquals(t, ExpectedListNames, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, count, 1) +} + +func TestCreateContainer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleCreateContainerSuccessfully(t) + + options := CreateOpts{ContentType: "application/json", Metadata: map[string]string{"foo": "bar"}} + res := Create(fake.ServiceClient(), "testContainer", options) + th.CheckNoErr(t, res.Err) + th.CheckEquals(t, "bar", res.Header["X-Container-Meta-Foo"][0]) +} + +func TestDeleteContainer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleDeleteContainerSuccessfully(t) + + res := Delete(fake.ServiceClient(), "testContainer") + th.CheckNoErr(t, res.Err) +} + +func TestUpateContainer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleUpdateContainerSuccessfully(t) + + options := &UpdateOpts{Metadata: map[string]string{"foo": "bar"}} + res := Update(fake.ServiceClient(), "testContainer", options) + th.CheckNoErr(t, res.Err) +} + +func TestGetContainer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleGetContainerSuccessfully(t) + + _, err := Get(fake.ServiceClient(), "testContainer").ExtractMetadata() + th.CheckNoErr(t, err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers/results.go new file mode 100644 index 00000000000..74f32860465 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers/results.go @@ -0,0 +1,139 @@ +package containers + +import ( + "fmt" + "strings" + + "github.com/rackspace/gophercloud" + "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 { + gophercloud.HeaderResult +} + +// 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.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 'ExtractHeader' +// method on the result struct. +type CreateResult struct { + gophercloud.HeaderResult +} + +// UpdateResult represents the result of an update operation. To extract the +// the headers from the HTTP response, you can invoke the 'ExtractHeader' +// method on the result struct. +type UpdateResult struct { + gophercloud.HeaderResult +} + +// DeleteResult represents the result of a delete operation. To extract the +// the headers from the HTTP response, you can invoke the 'ExtractHeader' +// method on the result struct. +type DeleteResult struct { + gophercloud.HeaderResult +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers/urls.go new file mode 100644 index 00000000000..f864f846eb2 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers/urls_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers/urls_test.go new file mode 100644 index 00000000000..d043a2aae50 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects/doc.go new file mode 100644 index 00000000000..30a9adde1ca --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects/fixtures.go new file mode 100644 index 00000000000..d951160e3a1 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects/fixtures.go @@ -0,0 +1,164 @@ +// +build fixtures + +package objects + +import ( + "fmt" + "net/http" + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +// HandleDownloadObjectSuccessfully creates an HTTP handler at `/testContainer/testObject` on the test handler mux that +// responds with a `Download` response. +func HandleDownloadObjectSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/testContainer/testObject", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, "Successful download with Gophercloud") + }) +} + +// ExpectedListInfo is the result expected from a call to `List` when full +// info is requested. +var ExpectedListInfo = []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", + }, +} + +// ExpectedListNames is the result expected from a call to `List` when just +// object names are requested. +var ExpectedListNames = []string{"hello", "goodbye"} + +// HandleListObjectsInfoSuccessfully creates an HTTP handler at `/testContainer` on the test handler mux that +// responds with a `List` response when full info is requested. +func HandleListObjectsInfoSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/testContainer", 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, `[ + { + "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) + } + }) +} + +// HandleListObjectNamesSuccessfully creates an HTTP handler at `/testContainer` on the test handler mux that +// responds with a `List` response when only object names are requested. +func HandleListObjectNamesSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/testContainer", 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, "hello\ngoodbye\n") + case "goodbye": + fmt.Fprintf(w, "") + default: + t.Fatalf("Unexpected marker: [%s]", marker) + } + }) +} + +// HandleCreateObjectSuccessfully creates an HTTP handler at `/testContainer/testObject` on the test handler mux that +// responds with a `Create` response. +func HandleCreateObjectSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/testContainer/testObject", 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.WriteHeader(http.StatusCreated) + }) +} + +// HandleCopyObjectSuccessfully creates an HTTP handler at `/testContainer/testObject` on the test handler mux that +// responds with a `Copy` response. +func HandleCopyObjectSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/testContainer/testObject", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "COPY") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + th.TestHeader(t, r, "Destination", "/newTestContainer/newTestObject") + w.WriteHeader(http.StatusCreated) + }) +} + +// HandleDeleteObjectSuccessfully creates an HTTP handler at `/testContainer/testObject` on the test handler mux that +// responds with a `Delete` response. +func HandleDeleteObjectSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/testContainer/testObject", 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) + }) +} + +// HandleUpdateObjectSuccessfully creates an HTTP handler at `/testContainer/testObject` on the test handler mux that +// responds with a `Update` response. +func HandleUpdateObjectSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/testContainer/testObject", 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") + th.TestHeader(t, r, "X-Object-Meta-Gophercloud-Test", "objects") + w.WriteHeader(http.StatusAccepted) + }) +} + +// HandleGetObjectSuccessfully creates an HTTP handler at `/testContainer/testObject` on the test handler mux that +// responds with a `Get` response. +func HandleGetObjectSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/testContainer/testObject", 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.Header().Add("X-Object-Meta-Gophercloud-Test", "objects") + w.WriteHeader(http.StatusNoContent) + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects/requests.go new file mode 100644 index 00000000000..9778de3be6d --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects/requests.go @@ -0,0 +1,416 @@ +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 { + return pagination.Pager{Err: err} + } + url += query + + if full { + headers = map[string]string{"Accept": "application/json", "Content-Type": "application/json"} + } + } + + createPage := func(r pagination.PageResult) pagination.Page { + p := ObjectPage{pagination.MarkerPageBase{PageResult: r}} + p.MarkerPageBase.Owner = p + return p + } + + pager := pagination.NewPager(c, url, createPage) + pager.Headers = headers + return pager +} + +// 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.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.Body = resp.HttpResponse.Body + res.Err = err + res.Header = resp.HttpResponse.Header + + 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 + + url := createURL(c, containerName, objectName) + h := c.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 + } + + contentType := h["Content-Type"] + + resp, err := perigee.Request("PUT", url, perigee.Options{ + ContentType: contentType, + ReqBody: content, + MoreHeaders: h, + OkCodes: []int{201, 202}, + }) + res.Header = resp.HttpResponse.Header + 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.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.Header = resp.HttpResponse.Header + res.Err = err + return res +} + +// DeleteOptsBuilder allows extensions to add additional parameters to the +// Delete request. +type DeleteOptsBuilder interface { + ToObjectDeleteQuery() (string, error) +} + +// DeleteOpts is a structure that holds parameters for deleting an object. +type DeleteOpts struct { + MultipartManifest string `q:"multipart-manifest"` +} + +// ToObjectDeleteQuery formats a DeleteOpts into a query string. +func (opts DeleteOpts) ToObjectDeleteQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + if err != nil { + return "", err + } + return q.String(), nil +} + +// 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.ToObjectDeleteQuery() + if err != nil { + res.Err = err + return res + } + url += query + } + + resp, err := perigee.Request("DELETE", url, perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + OkCodes: []int{204}, + }) + res.Header = resp.HttpResponse.Header + res.Err = err + return res +} + +// GetOptsBuilder allows extensions to add additional parameters to the +// Get request. +type GetOptsBuilder interface { + ToObjectGetQuery() (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"` +} + +// ToObjectGetQuery formats a GetOpts into a query string. +func (opts GetOpts) ToObjectGetQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + if err != nil { + return "", err + } + return q.String(), nil +} + +// 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.ToObjectGetQuery() + if err != nil { + res.Err = err + return res + } + url += query + } + + resp, err := perigee.Request("HEAD", url, perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + OkCodes: []int{200, 204}, + }) + res.Header = resp.HttpResponse.Header + res.Err = err + 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.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.Header = resp.HttpResponse.Header + res.Err = err + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects/requests_test.go new file mode 100644 index 00000000000..c3c28a789b9 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects/requests_test.go @@ -0,0 +1,132 @@ +package objects + +import ( + "bytes" + "io" + "testing" + + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestDownloadReader(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleDownloadObjectSuccessfully(t) + + response := Download(fake.ServiceClient(), "testContainer", "testObject", nil) + defer response.Body.Close() + + // Check reader + buf := bytes.NewBuffer(make([]byte, 0)) + io.CopyN(buf, response.Body, 10) + th.CheckEquals(t, "Successful", string(buf.Bytes())) +} + +func TestDownloadExtraction(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleDownloadObjectSuccessfully(t) + + response := Download(fake.ServiceClient(), "testContainer", "testObject", nil) + + // Check []byte extraction + bytes, err := response.ExtractContent() + th.AssertNoErr(t, err) + th.CheckEquals(t, "Successful download with Gophercloud", string(bytes)) +} + +func TestListObjectInfo(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListObjectsInfoSuccessfully(t) + + count := 0 + options := &ListOpts{Full: true} + err := List(fake.ServiceClient(), "testContainer", options).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractInfo(page) + th.AssertNoErr(t, err) + + th.CheckDeepEquals(t, ExpectedListInfo, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, count, 1) +} + +func TestListObjectNames(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListObjectNamesSuccessfully(t) + + count := 0 + options := &ListOpts{Full: false} + err := List(fake.ServiceClient(), "testContainer", options).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 + } + + th.CheckDeepEquals(t, ExpectedListNames, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, count, 1) +} + +func TestCreateObject(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleCreateObjectSuccessfully(t) + + content := bytes.NewBufferString("Did gyre and gimble in the wabe") + options := &CreateOpts{ContentType: "application/json"} + res := Create(fake.ServiceClient(), "testContainer", "testObject", content, options) + th.AssertNoErr(t, res.Err) +} + +func TestCopyObject(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleCopyObjectSuccessfully(t) + + options := &CopyOpts{Destination: "/newTestContainer/newTestObject"} + res := Copy(fake.ServiceClient(), "testContainer", "testObject", options) + th.AssertNoErr(t, res.Err) +} + +func TestDeleteObject(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleDeleteObjectSuccessfully(t) + + res := Delete(fake.ServiceClient(), "testContainer", "testObject", nil) + th.AssertNoErr(t, res.Err) +} + +func TestUpateObjectMetadata(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleUpdateObjectSuccessfully(t) + + options := &UpdateOpts{Metadata: map[string]string{"Gophercloud-Test": "objects"}} + res := Update(fake.ServiceClient(), "testContainer", "testObject", options) + th.AssertNoErr(t, res.Err) +} + +func TestGetObject(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleGetObjectSuccessfully(t) + + expected := map[string]string{"Gophercloud-Test": "objects"} + actual, err := Get(fake.ServiceClient(), "testContainer", "testObject", nil).ExtractMetadata() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, expected, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects/results.go new file mode 100644 index 00000000000..102d94cb37c --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects/results.go @@ -0,0 +1,162 @@ +package objects + +import ( + "fmt" + "io" + "io/ioutil" + "strings" + + "github.com/rackspace/gophercloud" + "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 { + gophercloud.HeaderResult + Body io.ReadCloser +} + +// ExtractContent is a function that takes a DownloadResult's io.Reader body +// and reads all available data into a slice of bytes. Please be aware that due +// the nature of io.Reader is forward-only - meaning that it can only be read +// once and not rewound. You can recreate a reader from the output of this +// function by using bytes.NewReader(downloadBytes) +func (dr DownloadResult) ExtractContent() ([]byte, error) { + if dr.Err != nil { + return nil, dr.Err + } + body, err := ioutil.ReadAll(dr.Body) + if err != nil { + return nil, err + } + dr.Body.Close() + return body, nil +} + +// GetResult is a *http.Response that is returned from a call to the Get function. +type GetResult struct { + gophercloud.HeaderResult +} + +// 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.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 { + gophercloud.HeaderResult +} + +// UpdateResult represents the result of an update operation. +type UpdateResult struct { + gophercloud.HeaderResult +} + +// DeleteResult represents the result of a delete operation. +type DeleteResult struct { + gophercloud.HeaderResult +} + +// CopyResult represents the result of a copy operation. +type CopyResult struct { + gophercloud.HeaderResult +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects/urls.go new file mode 100644 index 00000000000..d2ec62cff22 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects/urls_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects/urls_test.go new file mode 100644 index 00000000000..1dcfe3543c7 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/utils/choose_version.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/utils/choose_version.go new file mode 100644 index 00000000000..a0d5b264688 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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 an 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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/utils/choose_version_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/utils/choose_version_test.go new file mode 100644 index 00000000000..9552696232c --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/osutil/auth.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/osutil/auth.go deleted file mode 100644 index a411b6330b2..00000000000 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/osutil/auth.go +++ /dev/null @@ -1,64 +0,0 @@ -package osutil - -import ( - "fmt" - "github.com/rackspace/gophercloud" - "os" - "strings" -) - -var ( - nilOptions = gophercloud.AuthOptions{} - - // ErrNoAuthUrl errors occur when the value of the OS_AUTH_URL environment variable cannot be determined. - ErrNoAuthUrl = fmt.Errorf("Environment variable OS_AUTH_URL needs to be set.") - - // ErrNoUsername errors occur when the value of the OS_USERNAME environment variable cannot be determined. - ErrNoUsername = fmt.Errorf("Environment variable OS_USERNAME needs to be set.") - - // ErrNoPassword errors occur when the value of the OS_PASSWORD environment variable cannot be determined. - ErrNoPassword = fmt.Errorf("Environment variable OS_PASSWORD or OS_API_KEY needs to be set.") -) - -// AuthOptions fills out a gophercloud.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. -// -// The value of OS_AUTH_URL will be returned directly to the caller, for subsequent use in -// gophercloud.Authenticate()'s Provider parameter. This function will not interpret the value of OS_AUTH_URL, -// so as a convenient extention, you may set OS_AUTH_URL to, e.g., "rackspace-uk", or any other Gophercloud-recognized -// provider shortcuts. For broad compatibility, especially with local installations, you should probably -// avoid the temptation to do this. -func AuthOptions() (string, gophercloud.AuthOptions, error) { - provider := os.Getenv("OS_AUTH_URL") - username := os.Getenv("OS_USERNAME") - password := os.Getenv("OS_PASSWORD") - tenantId := os.Getenv("OS_TENANT_ID") - tenantName := os.Getenv("OS_TENANT_NAME") - - if provider == "" { - return "", nilOptions, ErrNoAuthUrl - } - - if username == "" { - return "", nilOptions, ErrNoUsername - } - - if password == "" { - return "", nilOptions, ErrNoPassword - } - - ao := gophercloud.AuthOptions{ - Username: username, - Password: password, - TenantId: tenantId, - TenantName: tenantName, - } - - if !strings.HasSuffix(provider, "/tokens") { - provider += "/tokens" - } - - return provider, ao, nil -} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/osutil/region.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/osutil/region.go deleted file mode 100644 index f7df507e553..00000000000 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/osutil/region.go +++ /dev/null @@ -1,9 +0,0 @@ -package osutil - -import "os" - -// Region provides a means of querying the OS_REGION_NAME environment variable. -// At present, you may also use os.Getenv("OS_REGION_NAME") as well. -func Region() string { - return os.Getenv("OS_REGION_NAME") -} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/package.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/package.go index 396e5234454..e8c2e82a050 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/package.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/package.go @@ -1,7 +1,38 @@ -// Gophercloud provides a multi-vendor interface to OpenStack-compatible clouds which attempts to follow -// established Go community coding standards and social norms. -// -// Unless you intend on contributing code to the SDK, you will almost certainly never have to use any -// Context structures or any of its methods. Contextual methods exist for easier unit testing only. -// Stick with the global functions unless you know exactly what you're doing, and why. +/* +Package gophercloud provides a multi-vendor interface to OpenStack-compatible +clouds. The library has a three-level hierarchy: providers, services, and +resources. + +Provider structs represent the service providers that offer and manage a +collection of services. Examples of providers include: OpenStack, Rackspace, +HP. These are defined like so: + + opts := gophercloud.AuthOptions{ + IdentityEndpoint: "https://my-openstack.com:5000/v2.0", + Username: "{username}", + Password: "{password}", + TenantID: "{tenant_id}", + } + + provider, err := openstack.AuthenticatedClient(opts) + +Service structs are specific to a provider and handle all of the logic and +operations for a particular OpenStack service. Examples of services include: +Compute, Object Storage, Block Storage. In order to define one, you need to +pass in the parent provider, like so: + + opts := gophercloud.EndpointOpts{Region: "RegionOne"} + + client := openstack.NewComputeV2(provider, opts) + +Resource structs are the domain models that services make use of in order +to work with and represent the state of API resources: + + server, err := servers.Get(client, "{serverId}").Extract() + +Another convention is to return Result structs for API operations, which allow +you to access the HTTP headers, response body, and associated errors with the +network transaction. To get a resource struct, you then call the Extract +method which is chained to the response. +*/ package gophercloud diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/pagination/http.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/pagination/http.go new file mode 100644 index 00000000000..1e108c80391 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/pagination/http.go @@ -0,0 +1,64 @@ +package pagination + +import ( + "encoding/json" + "io/ioutil" + "net/http" + "net/url" + "strings" + + "github.com/racker/perigee" + "github.com/rackspace/gophercloud" +) + +// PageResult stores the HTTP response that returned the current page of results. +type PageResult struct { + gophercloud.Result + url.URL +} + +// PageResultFrom parses an HTTP response as JSON and returns a PageResult containing the +// results, interpreting it as JSON if the content type indicates. +func PageResultFrom(resp http.Response) (PageResult, error) { + var parsedBody interface{} + + defer resp.Body.Close() + rawBody, err := ioutil.ReadAll(resp.Body) + if err != nil { + return PageResult{}, err + } + + if strings.HasPrefix(resp.Header.Get("Content-Type"), "application/json") { + err = json.Unmarshal(rawBody, &parsedBody) + if err != nil { + return PageResult{}, err + } + } else { + parsedBody = rawBody + } + + return PageResult{ + Result: gophercloud.Result{ + Body: parsedBody, + Header: resp.Header, + }, + URL: *resp.Request.URL, + }, 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.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/Godeps/_workspace/src/github.com/rackspace/gophercloud/pagination/linked.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/pagination/linked.go new file mode 100644 index 00000000000..461fa499afc --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/pagination/linked.go @@ -0,0 +1,61 @@ +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 { + PageResult + + // 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 + } + + 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/Godeps/_workspace/src/github.com/rackspace/gophercloud/pagination/linked_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/pagination/linked_test.go new file mode 100644 index 00000000000..4d3248e6ac9 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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 PageResult) Page { + return LinkedPageResult{LinkedPageBase{PageResult: 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/Godeps/_workspace/src/github.com/rackspace/gophercloud/pagination/marker.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/pagination/marker.go new file mode 100644 index 00000000000..e7688c217b4 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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 { + PageResult + + // 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/Godeps/_workspace/src/github.com/rackspace/gophercloud/pagination/marker_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/pagination/marker_test.go new file mode 100644 index 00000000000..3b1df1d68b9 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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 PageResult) Page { + p := MarkerPageResult{MarkerPageBase{PageResult: 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/Godeps/_workspace/src/github.com/rackspace/gophercloud/pagination/null.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/pagination/null.go new file mode 100644 index 00000000000..ae57e1886c9 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/pagination/pager.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/pagination/pager.go new file mode 100644 index 00000000000..5c20e16c6cd --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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 PageResult) 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 PageResult) 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 PageResult) 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 := PageResultFrom(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/Godeps/_workspace/src/github.com/rackspace/gophercloud/pagination/pagination_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/pagination/pagination_test.go new file mode 100644 index 00000000000..f3e4de1b042 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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{ + ProviderClient: &gophercloud.ProviderClient{TokenID: "abc123"}, + Endpoint: testhelper.Endpoint(), + } +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/pagination/pkg.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/pagination/pkg.go new file mode 100644 index 00000000000..912daea3642 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/pagination/single.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/pagination/single.go new file mode 100644 index 00000000000..4dd3c5c425b --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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 PageResult + +// NextPageURL always returns "" to indicate that there are no more pages to return. +func (current SinglePageBase) NextPageURL() (string, error) { + return "", nil +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/pagination/single_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/pagination/single_test.go new file mode 100644 index 00000000000..8817d570f28 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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 PageResult) 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/Godeps/_workspace/src/github.com/rackspace/gophercloud/params.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/params.go new file mode 100644 index 00000000000..5fe3c2cceaa --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/params.go @@ -0,0 +1,184 @@ +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 "q" 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 into a 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/Godeps/_workspace/src/github.com/rackspace/gophercloud/params_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/params_test.go new file mode 100644 index 00000000000..9f1d3bdcb01 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/provider_client.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/provider_client.go new file mode 100644 index 00000000000..7754c208121 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/provider_client.go @@ -0,0 +1,33 @@ +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 base URL used for a particular provider's identity + // service - it will be used when issuing authenticatation requests. It + // should point to the root resource of the identity service, not a specific + // identity version. + IdentityBase string + + // IdentityEndpoint is the identity endpoint. This may be a specific version + // of the identity service. If this is the case, this endpoint is used rather + // than querying versions first. + IdentityEndpoint string + + // TokenID is the ID of the most recently issued valid token. + 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/Godeps/_workspace/src/github.com/rackspace/gophercloud/provider_client_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/provider_client_test.go new file mode 100644 index 00000000000..b260246c5a3 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/auth_env.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/auth_env.go new file mode 100644 index 00000000000..5852c3ce738 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/auth_env.go @@ -0,0 +1,57 @@ +package rackspace + +import ( + "fmt" + "os" + + "github.com/rackspace/gophercloud" +) + +var nilOptions = gophercloud.AuthOptions{} + +// ErrNoAuthUrl, ErrNoUsername, and ErrNoPassword errors indicate of the +// required RS_AUTH_URL, RS_USERNAME, or RS_PASSWORD environment variables, +// respectively, remain undefined. See the AuthOptions() function for more details. +var ( + ErrNoAuthURL = fmt.Errorf("Environment variable RS_AUTH_URL or OS_AUTH_URL need to be set.") + ErrNoUsername = fmt.Errorf("Environment variable RS_USERNAME or OS_USERNAME need to be set.") + ErrNoPassword = fmt.Errorf("Environment variable RS_API_KEY or RS_PASSWORD needs to be set.") +) + +func prefixedEnv(base string) string { + value := os.Getenv("RS_" + base) + if value == "" { + value = os.Getenv("OS_" + base) + } + return value +} + +// AuthOptionsFromEnv fills out an identity.AuthOptions structure with the +// settings found on the various Rackspace RS_* environment variables. +func AuthOptionsFromEnv() (gophercloud.AuthOptions, error) { + authURL := prefixedEnv("AUTH_URL") + username := prefixedEnv("USERNAME") + password := prefixedEnv("PASSWORD") + apiKey := prefixedEnv("API_KEY") + + if authURL == "" { + return nilOptions, ErrNoAuthURL + } + + if username == "" { + return nilOptions, ErrNoUsername + } + + if password == "" && apiKey == "" { + return nilOptions, ErrNoPassword + } + + ao := gophercloud.AuthOptions{ + IdentityEndpoint: authURL, + Username: username, + Password: password, + APIKey: apiKey, + } + + return ao, nil +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/snapshots/delegate.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/snapshots/delegate.go new file mode 100644 index 00000000000..b338c36b71d --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/snapshots/delegate.go @@ -0,0 +1,134 @@ +package snapshots + +import ( + "errors" + + "github.com/racker/perigee" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" + + os "github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots" +) + +func updateURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL("snapshots", id) +} + +// 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 { + // REQUIRED + VolumeID string + // OPTIONAL + Description string + // OPTIONAL + Force bool + // OPTIONAL + Name 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, errors.New("Required CreateOpts field 'VolumeID' not set.") + } + + s["volume_id"] = opts.VolumeID + + if opts.Description != "" { + s["display_description"] = opts.Description + } + if opts.Name != "" { + s["display_name"] = opts.Name + } + if opts.Force { + s["force"] = opts.Force + } + + 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 { + return CreateResult{os.Create(client, opts)} +} + +// Delete will delete the existing Snapshot with the provided ID. +func Delete(client *gophercloud.ServiceClient, id string) os.DeleteResult { + return os.Delete(client, id) +} + +// 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 { + return GetResult{os.Get(client, id)} +} + +// List returns Snapshots. +func List(client *gophercloud.ServiceClient) pagination.Pager { + return os.List(client, os.ListOpts{}) +} + +// 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 { + ToSnapshotUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts is the common options struct used in this package's Update +// operation. +type UpdateOpts struct { + Name string + Description string +} + +// ToSnapshotUpdateMap casts a UpdateOpts struct to a map. +func (opts UpdateOpts) ToSnapshotUpdateMap() (map[string]interface{}, error) { + s := make(map[string]interface{}) + + if opts.Name != "" { + s["display_name"] = opts.Name + } + if opts.Description != "" { + s["display_description"] = opts.Description + } + + return map[string]interface{}{"snapshot": s}, nil +} + +// Update accepts a UpdateOpts struct and updates an existing snapshot using the +// values provided. +func Update(c *gophercloud.ServiceClient, snapshotID string, opts UpdateOptsBuilder) UpdateResult { + var res UpdateResult + + reqBody, err := opts.ToSnapshotUpdateMap() + if err != nil { + res.Err = err + return res + } + + // Send request to API + _, res.Err = perigee.Request("PUT", updateURL(c, snapshotID), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + ReqBody: &reqBody, + Results: &res.Body, + OkCodes: []int{200, 201}, + }) + + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/snapshots/delegate_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/snapshots/delegate_test.go new file mode 100644 index 00000000000..1a02b465279 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/snapshots/delegate_test.go @@ -0,0 +1,97 @@ +package snapshots + +import ( + "testing" + + "github.com/rackspace/gophercloud" + os "github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +const endpoint = "http://localhost:57909/v1/12345" + +func endpointClient() *gophercloud.ServiceClient { + return &gophercloud.ServiceClient{Endpoint: endpoint} +} + +func TestUpdateURL(t *testing.T) { + actual := updateURL(endpointClient(), "foo") + expected := endpoint + "snapshots/foo" + th.AssertEquals(t, expected, actual) +} + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + os.MockListResponse(t) + + count := 0 + + err := List(fake.ServiceClient()).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 + }) + + th.AssertEquals(t, 1, count) + th.AssertNoErr(t, err) +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + os.MockGetResponse(t) + + v, err := Get(fake.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() + + os.MockCreateResponse(t) + + options := &CreateOpts{VolumeID: "1234", Name: "snapshot-001"} + n, err := Create(fake.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 TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + os.MockDeleteResponse(t) + + res := Delete(fake.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22") + th.AssertNoErr(t, res.Err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/snapshots/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/snapshots/doc.go new file mode 100644 index 00000000000..ad6064f2af1 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/snapshots/doc.go @@ -0,0 +1,3 @@ +// Package snapshots provides information and interaction with the snapshot +// API resource for the Rackspace Block Storage service. +package snapshots diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/snapshots/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/snapshots/results.go new file mode 100644 index 00000000000..0fab2828bc2 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/snapshots/results.go @@ -0,0 +1,149 @@ +package snapshots + +import ( + "github.com/racker/perigee" + + "github.com/rackspace/gophercloud" + os "github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots" + "github.com/rackspace/gophercloud/pagination" + + "github.com/mitchellh/mapstructure" +) + +// Status is the type used to represent a snapshot's status +type Status string + +// Constants to use for supported statuses +const ( + Creating Status = "CREATING" + Available Status = "AVAILABLE" + Deleting Status = "DELETING" + Error Status = "ERROR" + DeleteError Status = "ERROR_DELETING" +) + +// Snapshot is the Rackspace representation of an external block storage device. +type Snapshot struct { + // The timestamp when this snapshot was created. + CreatedAt string `mapstructure:"created_at"` + + // The human-readable description for this snapshot. + Description string `mapstructure:"display_description"` + + // The human-readable name for this snapshot. + Name string `mapstructure:"display_name"` + + // The UUID for this snapshot. + ID string `mapstructure:"id"` + + // The random metadata associated with this snapshot. Note: unlike standard + // OpenStack snapshots, this cannot actually be set. + Metadata map[string]string `mapstructure:"metadata"` + + // Indicates the current progress of the snapshot's backup procedure. + Progress string `mapstructure:"os-extended-snapshot-attributes:progress"` + + // The project ID. + ProjectID string `mapstructure:"os-extended-snapshot-attributes:project_id"` + + // The size of the volume which this snapshot backs up. + Size int `mapstructure:"size"` + + // The status of the snapshot. + Status Status `mapstructure:"status"` + + // The ID of the volume which this snapshot seeks to back up. + VolumeID string `mapstructure:"volume_id"` +} + +// CreateResult represents the result of a create operation +type CreateResult struct { + os.CreateResult +} + +// GetResult represents the result of a get operation +type GetResult struct { + os.GetResult +} + +// UpdateResult represents the result of an update operation +type UpdateResult struct { + gophercloud.Result +} + +func commonExtract(resp interface{}, err error) (*Snapshot, error) { + if err != nil { + return nil, err + } + + var respStruct struct { + Snapshot *Snapshot `json:"snapshot"` + } + + err = mapstructure.Decode(resp, &respStruct) + + return respStruct.Snapshot, err +} + +// Extract will get the Snapshot object out of the GetResult object. +func (r GetResult) Extract() (*Snapshot, error) { + return commonExtract(r.Body, r.Err) +} + +// Extract will get the Snapshot object out of the CreateResult object. +func (r CreateResult) Extract() (*Snapshot, error) { + return commonExtract(r.Body, r.Err) +} + +// Extract will get the Snapshot object out of the UpdateResult object. +func (r UpdateResult) Extract() (*Snapshot, error) { + return commonExtract(r.Body, r.Err) +} + +// 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.(os.ListResult).Body, &response) + return response.Snapshots, err +} + +// WaitUntilComplete will continually poll a snapshot until it successfully +// transitions to a specified state. It will do this for at most the number of +// seconds specified. +func (snapshot Snapshot) WaitUntilComplete(c *gophercloud.ServiceClient, timeout int) error { + return gophercloud.WaitFor(timeout, func() (bool, error) { + // Poll resource + current, err := Get(c, snapshot.ID).Extract() + if err != nil { + return false, err + } + + // Has it been built yet? + if current.Progress == "100%" { + return true, nil + } + + return false, nil + }) +} + +// WaitUntilDeleted will continually poll a snapshot until it has been +// successfully deleted, i.e. returns a 404 status. +func (snapshot Snapshot) WaitUntilDeleted(c *gophercloud.ServiceClient, timeout int) error { + return gophercloud.WaitFor(timeout, func() (bool, error) { + // Poll resource + _, err := Get(c, snapshot.ID).Extract() + + // Check for a 404 + if casted, ok := err.(*perigee.UnexpectedResponseCodeError); ok && casted.Actual == 404 { + return true, nil + } else if err != nil { + return false, err + } + + return false, nil + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/volumes/delegate.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/volumes/delegate.go new file mode 100644 index 00000000000..438349410a6 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/volumes/delegate.go @@ -0,0 +1,75 @@ +package volumes + +import ( + "fmt" + + "github.com/rackspace/gophercloud" + os "github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes" + "github.com/rackspace/gophercloud/pagination" +) + +type CreateOpts struct { + os.CreateOpts +} + +func (opts CreateOpts) ToVolumeCreateMap() (map[string]interface{}, error) { + if opts.Size < 75 || opts.Size > 1024 { + return nil, fmt.Errorf("Size field must be between 75 and 1024") + } + + return opts.CreateOpts.ToVolumeCreateMap() +} + +// 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 os.CreateOptsBuilder) CreateResult { + return CreateResult{os.Create(client, opts)} +} + +// Delete will delete the existing Volume with the provided ID. +func Delete(client *gophercloud.ServiceClient, id string) os.DeleteResult { + return os.Delete(client, id) +} + +// 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 { + return GetResult{os.Get(client, id)} +} + +// List returns volumes optionally limited by the conditions provided in ListOpts. +func List(client *gophercloud.ServiceClient) pagination.Pager { + return os.List(client, os.ListOpts{}) +} + +// 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 +} + +// 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.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 os.UpdateOptsBuilder) UpdateResult { + return UpdateResult{os.Update(client, id, opts)} +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/volumes/delegate_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/volumes/delegate_test.go new file mode 100644 index 00000000000..b44564cc1f6 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/volumes/delegate_test.go @@ -0,0 +1,106 @@ +package volumes + +import ( + "testing" + + os "github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes" + "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() + + os.MockListResponse(t) + + count := 0 + + err := List(fake.ServiceClient()).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 + }) + + th.AssertEquals(t, 1, count) + th.AssertNoErr(t, err) +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + os.MockGetResponse(t) + + v, err := Get(fake.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() + + os.MockCreateResponse(t) + + n, err := Create(fake.ServiceClient(), CreateOpts{os.CreateOpts{Size: 75}}).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, n.Size, 4) + th.AssertEquals(t, n.ID, "d32019d3-bc6e-4319-9c1d-6722fc136a22") +} + +func TestSizeRange(t *testing.T) { + _, err := Create(fake.ServiceClient(), CreateOpts{os.CreateOpts{Size: 1}}).Extract() + if err == nil { + t.Fatalf("Expected error, got none") + } + + _, err = Create(fake.ServiceClient(), CreateOpts{os.CreateOpts{Size: 2000}}).Extract() + if err == nil { + t.Fatalf("Expected error, got none") + } +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + os.MockDeleteResponse(t) + + res := Delete(fake.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22") + th.AssertNoErr(t, res.Err) +} + +func TestUpdate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + os.MockUpdateResponse(t) + + options := &UpdateOpts{Name: "vol-002"} + v, err := Update(fake.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22", options).Extract() + th.AssertNoErr(t, err) + th.CheckEquals(t, "vol-002", v.Name) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/volumes/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/volumes/doc.go new file mode 100644 index 00000000000..b2be25c5381 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/volumes/doc.go @@ -0,0 +1,3 @@ +// Package volumes provides information and interaction with the volume +// API resource for the Rackspace Block Storage service. +package volumes diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/volumes/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/volumes/results.go new file mode 100644 index 00000000000..c7c2cc49841 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/volumes/results.go @@ -0,0 +1,66 @@ +package volumes + +import ( + os "github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes" + "github.com/rackspace/gophercloud/pagination" + + "github.com/mitchellh/mapstructure" +) + +// Volume wraps an Openstack volume +type Volume os.Volume + +// CreateResult represents the result of a create operation +type CreateResult struct { + os.CreateResult +} + +// GetResult represents the result of a get operation +type GetResult struct { + os.GetResult +} + +// UpdateResult represents the result of an update operation +type UpdateResult struct { + os.UpdateResult +} + +func commonExtract(resp interface{}, err error) (*Volume, error) { + if err != nil { + return nil, err + } + + var respStruct struct { + Volume *Volume `json:"volume"` + } + + err = mapstructure.Decode(resp, &respStruct) + + return respStruct.Volume, err +} + +// Extract will get the Volume object out of the GetResult object. +func (r GetResult) Extract() (*Volume, error) { + return commonExtract(r.Body, r.Err) +} + +// Extract will get the Volume object out of the CreateResult object. +func (r CreateResult) Extract() (*Volume, error) { + return commonExtract(r.Body, r.Err) +} + +// Extract will get the Volume object out of the UpdateResult object. +func (r UpdateResult) Extract() (*Volume, error) { + return commonExtract(r.Body, r.Err) +} + +// 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.(os.ListResult).Body, &response) + + return response.Volumes, err +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/volumetypes/delegate.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/volumetypes/delegate.go new file mode 100644 index 00000000000..c96b3e4a357 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/volumetypes/delegate.go @@ -0,0 +1,18 @@ +package volumetypes + +import ( + "github.com/rackspace/gophercloud" + os "github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumetypes" + "github.com/rackspace/gophercloud/pagination" +) + +// List returns all volume types. +func List(client *gophercloud.ServiceClient) pagination.Pager { + return os.List(client) +} + +// 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 { + return GetResult{os.Get(client, id)} +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/volumetypes/delegate_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/volumetypes/delegate_test.go new file mode 100644 index 00000000000..6e65c904b52 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/volumetypes/delegate_test.go @@ -0,0 +1,64 @@ +package volumetypes + +import ( + "testing" + + os "github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumetypes" + "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() + + os.MockListResponse(t) + + count := 0 + + err := List(fake.ServiceClient()).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 + }) + + th.AssertEquals(t, 1, count) + th.AssertNoErr(t, err) +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + os.MockGetResponse(t) + + vt, err := Get(fake.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") +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/volumetypes/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/volumetypes/doc.go new file mode 100644 index 00000000000..70122b77c4a --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/volumetypes/doc.go @@ -0,0 +1,3 @@ +// Package volumetypes provides information and interaction with the volume type +// API resource for the Rackspace Block Storage service. +package volumetypes diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/volumetypes/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/volumetypes/results.go new file mode 100644 index 00000000000..39c8d6f7fac --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/volumetypes/results.go @@ -0,0 +1,37 @@ +package volumetypes + +import ( + "github.com/mitchellh/mapstructure" + os "github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumetypes" + "github.com/rackspace/gophercloud/pagination" +) + +type VolumeType os.VolumeType + +type GetResult struct { + os.GetResult +} + +// Extract will get the Volume Type struct out of the response. +func (r GetResult) Extract() (*VolumeType, error) { + if r.Err != nil { + return nil, r.Err + } + + var res struct { + VolumeType *VolumeType `json:"volume_type" mapstructure:"volume_type"` + } + + err := mapstructure.Decode(r.Body, &res) + + return res.VolumeType, err +} + +func ExtractVolumeTypes(page pagination.Page) ([]VolumeType, error) { + var response struct { + VolumeTypes []VolumeType `mapstructure:"volume_types"` + } + + err := mapstructure.Decode(page.(os.ListResult).Body, &response) + return response.VolumeTypes, err +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/client.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/client.go new file mode 100644 index 00000000000..5f739a8b899 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/client.go @@ -0,0 +1,156 @@ +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{ + ProviderClient: client, + Endpoint: v2Endpoint, + } +} + +// NewComputeV2 creates a ServiceClient that may be used to access the v2 compute service. +func NewComputeV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + eo.ApplyDefaults("compute") + url, err := client.EndpointLocator(eo) + if err != nil { + return nil, err + } + + return &gophercloud.ServiceClient{ + ProviderClient: client, + Endpoint: url, + }, nil +} + +// NewObjectCDNV1 creates a ServiceClient that may be used with the Rackspace v1 CDN. +func NewObjectCDNV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + eo.ApplyDefaults("rax:object-cdn") + url, err := client.EndpointLocator(eo) + if err != nil { + return nil, err + } + return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil +} + +// NewObjectStorageV1 creates a ServiceClient that may be used with the Rackspace v1 object storage package. +func NewObjectStorageV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + return os.NewObjectStorageV1(client, eo) +} + +// NewBlockStorageV1 creates a ServiceClient that can be used to access the +// Rackspace Cloud Block Storage v1 API. +func NewBlockStorageV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + eo.ApplyDefaults("volume") + url, err := client.EndpointLocator(eo) + if err != nil { + return nil, err + } + + return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/client_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/client_test.go new file mode 100644 index 00000000000..73b1c886ffa --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/bootfromvolume/delegate.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/bootfromvolume/delegate.go new file mode 100644 index 00000000000..2580459f077 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/bootfromvolume/delegate.go @@ -0,0 +1,12 @@ +package bootfromvolume + +import ( + "github.com/rackspace/gophercloud" + osBFV "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/bootfromvolume" + osServers "github.com/rackspace/gophercloud/openstack/compute/v2/servers" +) + +// Create requests the creation of a server from the given block device mapping. +func Create(client *gophercloud.ServiceClient, opts osServers.CreateOptsBuilder) osServers.CreateResult { + return osBFV.Create(client, opts) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/bootfromvolume/delegate_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/bootfromvolume/delegate_test.go new file mode 100644 index 00000000000..0b5352751b4 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/bootfromvolume/delegate_test.go @@ -0,0 +1,52 @@ +package bootfromvolume + +import ( + "testing" + + osBFV "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/bootfromvolume" + "github.com/rackspace/gophercloud/openstack/compute/v2/servers" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestCreateOpts(t *testing.T) { + base := servers.CreateOpts{ + Name: "createdserver", + ImageRef: "asdfasdfasdf", + FlavorRef: "performance1-1", + } + + ext := osBFV.CreateOptsExt{ + CreateOptsBuilder: base, + BlockDevice: []osBFV.BlockDevice{ + osBFV.BlockDevice{ + UUID: "123456", + SourceType: osBFV.Image, + DestinationType: "volume", + VolumeSize: 10, + }, + }, + } + + expected := ` + { + "server": { + "name": "createdserver", + "imageRef": "asdfasdfasdf", + "flavorRef": "performance1-1", + "block_device_mapping_v2":[ + { + "uuid":"123456", + "source_type":"image", + "destination_type":"volume", + "boot_index": "0", + "delete_on_termination": "false", + "volume_size": "10" + } + ] + } + } + ` + actual, err := ext.ToServerCreateMap() + th.AssertNoErr(t, err) + th.CheckJSONEquals(t, expected, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/flavors/delegate.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/flavors/delegate.go new file mode 100644 index 00000000000..6bfc20c5644 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/flavors/delegate.go @@ -0,0 +1,46 @@ +package flavors + +import ( + "github.com/rackspace/gophercloud" + os "github.com/rackspace/gophercloud/openstack/compute/v2/flavors" + "github.com/rackspace/gophercloud/pagination" +) + +// 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. +type ListOpts struct { + + // MinDisk and MinRAM, if provided, elide flavors that do not meet your criteria. + MinDisk int `q:"minDisk"` + MinRAM int `q:"minRam"` + + // Marker specifies the ID of the last flavor in the previous page. + Marker string `q:"marker"` + + // Limit instructs List to refrain from sending excessively large lists of flavors. + Limit int `q:"limit"` +} + +// ToFlavorListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToFlavorListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + if err != nil { + return "", err + } + return q.String(), nil +} + +// ListDetail enumerates the server images available to your account. +func ListDetail(client *gophercloud.ServiceClient, opts os.ListOptsBuilder) pagination.Pager { + return os.ListDetail(client, opts) +} + +// Get returns details about a single flavor, identity by ID. +func Get(client *gophercloud.ServiceClient, id string) os.GetResult { + return os.Get(client, id) +} + +// ExtractFlavors interprets a page of List results as Flavors. +func ExtractFlavors(page pagination.Page) ([]os.Flavor, error) { + return os.ExtractFlavors(page) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/flavors/delegate_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/flavors/delegate_test.go new file mode 100644 index 00000000000..204081dd179 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/flavors/delegate_test.go @@ -0,0 +1,62 @@ +package flavors + +import ( + "fmt" + "net/http" + "testing" + + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +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", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + r.ParseForm() + marker := r.Form.Get("marker") + switch marker { + case "": + fmt.Fprintf(w, ListOutput) + case "performance1-2": + fmt.Fprintf(w, `{ "flavors": [] }`) + default: + t.Fatalf("Unexpected marker: [%s]", marker) + } + }) + + count := 0 + err := ListDetail(client.ServiceClient(), nil).EachPage(func(page pagination.Page) (bool, error) { + actual, err := ExtractFlavors(page) + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, ExpectedFlavorSlice, actual) + + count++ + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, 1, count) +} + +func TestGetFlavor(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/flavors/performance1-1", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, GetOutput) + }) + + actual, err := Get(client.ServiceClient(), "performance1-1").Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &Performance1Flavor, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/flavors/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/flavors/doc.go new file mode 100644 index 00000000000..278229ab97b --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/flavors/doc.go @@ -0,0 +1,3 @@ +// Package flavors provides information and interaction with the flavor +// API resource for the Rackspace Cloud Servers service. +package flavors diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/flavors/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/flavors/fixtures.go new file mode 100644 index 00000000000..b6dca937caa --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/flavors/fixtures.go @@ -0,0 +1,128 @@ +// +build fixtures +package flavors + +import ( + os "github.com/rackspace/gophercloud/openstack/compute/v2/flavors" +) + +// ListOutput is a sample response of a flavor List request. +const ListOutput = ` +{ + "flavors": [ + { + "OS-FLV-EXT-DATA:ephemeral": 0, + "OS-FLV-WITH-EXT-SPECS:extra_specs": { + "class": "performance1", + "disk_io_index": "40", + "number_of_data_disks": "0", + "policy_class": "performance_flavor", + "resize_policy_class": "performance_flavor" + }, + "disk": 20, + "id": "performance1-1", + "links": [ + { + "href": "https://iad.servers.api.rackspacecloud.com/v2/864477/flavors/performance1-1", + "rel": "self" + }, + { + "href": "https://iad.servers.api.rackspacecloud.com/864477/flavors/performance1-1", + "rel": "bookmark" + } + ], + "name": "1 GB Performance", + "ram": 1024, + "rxtx_factor": 200, + "swap": "", + "vcpus": 1 + }, + { + "OS-FLV-EXT-DATA:ephemeral": 20, + "OS-FLV-WITH-EXT-SPECS:extra_specs": { + "class": "performance1", + "disk_io_index": "40", + "number_of_data_disks": "1", + "policy_class": "performance_flavor", + "resize_policy_class": "performance_flavor" + }, + "disk": 40, + "id": "performance1-2", + "links": [ + { + "href": "https://iad.servers.api.rackspacecloud.com/v2/864477/flavors/performance1-2", + "rel": "self" + }, + { + "href": "https://iad.servers.api.rackspacecloud.com/864477/flavors/performance1-2", + "rel": "bookmark" + } + ], + "name": "2 GB Performance", + "ram": 2048, + "rxtx_factor": 400, + "swap": "", + "vcpus": 2 + } + ] +}` + +// GetOutput is a sample response from a flavor Get request. Its contents correspond to the +// Performance1Flavor struct. +const GetOutput = ` +{ + "flavor": { + "OS-FLV-EXT-DATA:ephemeral": 0, + "OS-FLV-WITH-EXT-SPECS:extra_specs": { + "class": "performance1", + "disk_io_index": "40", + "number_of_data_disks": "0", + "policy_class": "performance_flavor", + "resize_policy_class": "performance_flavor" + }, + "disk": 20, + "id": "performance1-1", + "links": [ + { + "href": "https://iad.servers.api.rackspacecloud.com/v2/864477/flavors/performance1-1", + "rel": "self" + }, + { + "href": "https://iad.servers.api.rackspacecloud.com/864477/flavors/performance1-1", + "rel": "bookmark" + } + ], + "name": "1 GB Performance", + "ram": 1024, + "rxtx_factor": 200, + "swap": "", + "vcpus": 1 + } +} +` + +// Performance1Flavor is the expected result of parsing GetOutput, or the first element of +// ListOutput. +var Performance1Flavor = os.Flavor{ + ID: "performance1-1", + Disk: 20, + RAM: 1024, + Name: "1 GB Performance", + RxTxFactor: 200.0, + Swap: 0, + VCPUs: 1, +} + +// Performance2Flavor is the second result expected from parsing ListOutput. +var Performance2Flavor = os.Flavor{ + ID: "performance1-2", + Disk: 40, + RAM: 2048, + Name: "2 GB Performance", + RxTxFactor: 400.0, + Swap: 0, + VCPUs: 2, +} + +// ExpectedFlavorSlice is the slice of Flavor structs that are expected to be parsed from +// ListOutput. +var ExpectedFlavorSlice = []os.Flavor{Performance1Flavor, Performance2Flavor} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/images/delegate.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/images/delegate.go new file mode 100644 index 00000000000..18e1f315af9 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/images/delegate.go @@ -0,0 +1,22 @@ +package images + +import ( + "github.com/rackspace/gophercloud" + os "github.com/rackspace/gophercloud/openstack/compute/v2/images" + "github.com/rackspace/gophercloud/pagination" +) + +// ListDetail enumerates the available server images. +func ListDetail(client *gophercloud.ServiceClient, opts os.ListOptsBuilder) pagination.Pager { + return os.ListDetail(client, opts) +} + +// Get acquires additional detail about a specific image by ID. +func Get(client *gophercloud.ServiceClient, id string) os.GetResult { + return os.Get(client, id) +} + +// ExtractImages interprets a page as a collection of server images. +func ExtractImages(page pagination.Page) ([]os.Image, error) { + return os.ExtractImages(page) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/images/delegate_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/images/delegate_test.go new file mode 100644 index 00000000000..db0a6e3414c --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/images/delegate_test.go @@ -0,0 +1,62 @@ +package images + +import ( + "fmt" + "net/http" + "testing" + + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestListImageDetails(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", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + r.ParseForm() + marker := r.Form.Get("marker") + switch marker { + case "": + fmt.Fprintf(w, ListOutput) + case "e19a734c-c7e6-443a-830c-242209c4d65d": + fmt.Fprintf(w, `{ "images": [] }`) + default: + t.Fatalf("Unexpected marker: [%s]", marker) + } + }) + + count := 0 + err := ListDetail(client.ServiceClient(), nil).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractImages(page) + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, ExpectedImageSlice, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, 1, count) +} + +func TestGetImageDetails(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/images/e19a734c-c7e6-443a-830c-242209c4d65d", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, GetOutput) + }) + + actual, err := Get(client.ServiceClient(), "e19a734c-c7e6-443a-830c-242209c4d65d").Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &UbuntuImage, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/images/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/images/doc.go new file mode 100644 index 00000000000..cfae8067127 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/images/doc.go @@ -0,0 +1,3 @@ +// Package images provides information and interaction with the image +// API resource for the Rackspace Cloud Servers service. +package images diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/images/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/images/fixtures.go new file mode 100644 index 00000000000..c46d196cbf2 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/images/fixtures.go @@ -0,0 +1,199 @@ +// +build fixtures +package images + +import ( + os "github.com/rackspace/gophercloud/openstack/compute/v2/images" +) + +// ListOutput is an example response from an /images/detail request. +const ListOutput = ` +{ + "images": [ + { + "OS-DCF:diskConfig": "MANUAL", + "OS-EXT-IMG-SIZE:size": 1.017415075e+09, + "created": "2014-10-01T15:49:02Z", + "id": "30aa010e-080e-4d4b-a7f9-09fc55b07d69", + "links": [ + { + "href": "https://iad.servers.api.rackspacecloud.com/v2/111222/images/30aa010e-080e-4d4b-a7f9-09fc55b07d69", + "rel": "self" + }, + { + "href": "https://iad.servers.api.rackspacecloud.com/111222/images/30aa010e-080e-4d4b-a7f9-09fc55b07d69", + "rel": "bookmark" + }, + { + "href": "https://iad.servers.api.rackspacecloud.com/111222/images/30aa010e-080e-4d4b-a7f9-09fc55b07d69", + "rel": "alternate", + "type": "application/vnd.openstack.image" + } + ], + "metadata": { + "auto_disk_config": "disabled", + "cache_in_nova": "True", + "com.rackspace__1__build_core": "1", + "com.rackspace__1__build_managed": "1", + "com.rackspace__1__build_rackconnect": "1", + "com.rackspace__1__options": "0", + "com.rackspace__1__platform_target": "PublicCloud", + "com.rackspace__1__release_build_date": "2014-10-01_15-46-08", + "com.rackspace__1__release_id": "100", + "com.rackspace__1__release_version": "10", + "com.rackspace__1__source": "kickstart", + "com.rackspace__1__visible_core": "1", + "com.rackspace__1__visible_managed": "0", + "com.rackspace__1__visible_rackconnect": "0", + "image_type": "base", + "org.openstack__1__architecture": "x64", + "org.openstack__1__os_distro": "org.archlinux", + "org.openstack__1__os_version": "2014.8", + "os_distro": "arch", + "os_type": "linux", + "vm_mode": "hvm" + }, + "minDisk": 20, + "minRam": 512, + "name": "Arch 2014.10 (PVHVM)", + "progress": 100, + "status": "ACTIVE", + "updated": "2014-10-01T19:37:58Z" + }, + { + "OS-DCF:diskConfig": "AUTO", + "OS-EXT-IMG-SIZE:size": 1.060306463e+09, + "created": "2014-10-01T12:58:11Z", + "id": "e19a734c-c7e6-443a-830c-242209c4d65d", + "links": [ + { + "href": "https://iad.servers.api.rackspacecloud.com/v2/111222/images/e19a734c-c7e6-443a-830c-242209c4d65d", + "rel": "self" + }, + { + "href": "https://iad.servers.api.rackspacecloud.com/111222/images/e19a734c-c7e6-443a-830c-242209c4d65d", + "rel": "bookmark" + }, + { + "href": "https://iad.servers.api.rackspacecloud.com/111222/images/e19a734c-c7e6-443a-830c-242209c4d65d", + "rel": "alternate", + "type": "application/vnd.openstack.image" + } + ], + "metadata": { + "auto_disk_config": "True", + "cache_in_nova": "True", + "com.rackspace__1__build_core": "1", + "com.rackspace__1__build_managed": "1", + "com.rackspace__1__build_rackconnect": "1", + "com.rackspace__1__options": "0", + "com.rackspace__1__platform_target": "PublicCloud", + "com.rackspace__1__release_build_date": "2014-10-01_12-31-03", + "com.rackspace__1__release_id": "1007", + "com.rackspace__1__release_version": "6", + "com.rackspace__1__source": "kickstart", + "com.rackspace__1__visible_core": "1", + "com.rackspace__1__visible_managed": "1", + "com.rackspace__1__visible_rackconnect": "1", + "image_type": "base", + "org.openstack__1__architecture": "x64", + "org.openstack__1__os_distro": "com.ubuntu", + "org.openstack__1__os_version": "14.04", + "os_distro": "ubuntu", + "os_type": "linux", + "vm_mode": "xen" + }, + "minDisk": 20, + "minRam": 512, + "name": "Ubuntu 14.04 LTS (Trusty Tahr)", + "progress": 100, + "status": "ACTIVE", + "updated": "2014-10-01T15:51:44Z" + } + ] +} +` + +// GetOutput is an example response from an /images request. +const GetOutput = ` +{ + "image": { + "OS-DCF:diskConfig": "AUTO", + "OS-EXT-IMG-SIZE:size": 1060306463, + "created": "2014-10-01T12:58:11Z", + "id": "e19a734c-c7e6-443a-830c-242209c4d65d", + "links": [ + { + "href": "https://iad.servers.api.rackspacecloud.com/v2/111222/images/e19a734c-c7e6-443a-830c-242209c4d65d", + "rel": "self" + }, + { + "href": "https://iad.servers.api.rackspacecloud.com/111222/images/e19a734c-c7e6-443a-830c-242209c4d65d", + "rel": "bookmark" + }, + { + "href": "https://iad.servers.api.rackspacecloud.com/111222/images/e19a734c-c7e6-443a-830c-242209c4d65d", + "rel": "alternate", + "type": "application/vnd.openstack.image" + } + ], + "metadata": { + "auto_disk_config": "True", + "cache_in_nova": "True", + "com.rackspace__1__build_core": "1", + "com.rackspace__1__build_managed": "1", + "com.rackspace__1__build_rackconnect": "1", + "com.rackspace__1__options": "0", + "com.rackspace__1__platform_target": "PublicCloud", + "com.rackspace__1__release_build_date": "2014-10-01_12-31-03", + "com.rackspace__1__release_id": "1007", + "com.rackspace__1__release_version": "6", + "com.rackspace__1__source": "kickstart", + "com.rackspace__1__visible_core": "1", + "com.rackspace__1__visible_managed": "1", + "com.rackspace__1__visible_rackconnect": "1", + "image_type": "base", + "org.openstack__1__architecture": "x64", + "org.openstack__1__os_distro": "com.ubuntu", + "org.openstack__1__os_version": "14.04", + "os_distro": "ubuntu", + "os_type": "linux", + "vm_mode": "xen" + }, + "minDisk": 20, + "minRam": 512, + "name": "Ubuntu 14.04 LTS (Trusty Tahr)", + "progress": 100, + "status": "ACTIVE", + "updated": "2014-10-01T15:51:44Z" + } +} +` + +// ArchImage is the first Image structure that should be parsed from ListOutput. +var ArchImage = os.Image{ + ID: "30aa010e-080e-4d4b-a7f9-09fc55b07d69", + Name: "Arch 2014.10 (PVHVM)", + Created: "2014-10-01T15:49:02Z", + Updated: "2014-10-01T19:37:58Z", + MinDisk: 20, + MinRAM: 512, + Progress: 100, + Status: "ACTIVE", +} + +// UbuntuImage is the second Image structure that should be parsed from ListOutput and +// the only image that should be extracted from GetOutput. +var UbuntuImage = os.Image{ + ID: "e19a734c-c7e6-443a-830c-242209c4d65d", + Name: "Ubuntu 14.04 LTS (Trusty Tahr)", + Created: "2014-10-01T12:58:11Z", + Updated: "2014-10-01T15:51:44Z", + MinDisk: 20, + MinRAM: 512, + Progress: 100, + Status: "ACTIVE", +} + +// ExpectedImageSlice is the collection of images that should be parsed from ListOutput, +// in order. +var ExpectedImageSlice = []os.Image{ArchImage, UbuntuImage} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/keypairs/delegate.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/keypairs/delegate.go new file mode 100644 index 00000000000..3e53525dc7c --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/keypairs/delegate.go @@ -0,0 +1,33 @@ +package keypairs + +import ( + "github.com/rackspace/gophercloud" + os "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs" + "github.com/rackspace/gophercloud/pagination" +) + +// List returns a Pager that allows you to iterate over a collection of KeyPairs. +func List(client *gophercloud.ServiceClient) pagination.Pager { + return os.List(client) +} + +// Create requests the creation of a new keypair on the server, or to import a pre-existing +// keypair. +func Create(client *gophercloud.ServiceClient, opts os.CreateOptsBuilder) os.CreateResult { + return os.Create(client, opts) +} + +// Get returns public data about a previously uploaded KeyPair. +func Get(client *gophercloud.ServiceClient, name string) os.GetResult { + return os.Get(client, name) +} + +// Delete requests the deletion of a previous stored KeyPair from the server. +func Delete(client *gophercloud.ServiceClient, name string) os.DeleteResult { + return os.Delete(client, name) +} + +// ExtractKeyPairs interprets a page of results as a slice of KeyPairs. +func ExtractKeyPairs(page pagination.Page) ([]os.KeyPair, error) { + return os.ExtractKeyPairs(page) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/keypairs/delegate_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/keypairs/delegate_test.go new file mode 100644 index 00000000000..62e5df950ce --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/keypairs/delegate_test.go @@ -0,0 +1,72 @@ +package keypairs + +import ( + "testing" + + os "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs" + "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() + os.HandleListSuccessfully(t) + + count := 0 + err := List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractKeyPairs(page) + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, os.ExpectedKeyPairSlice, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, 1, count) +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleCreateSuccessfully(t) + + actual, err := Create(client.ServiceClient(), os.CreateOpts{ + Name: "createdkey", + }).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &os.CreatedKeyPair, actual) +} + +func TestImport(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleImportSuccessfully(t) + + actual, err := Create(client.ServiceClient(), os.CreateOpts{ + Name: "importedkey", + PublicKey: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDx8nkQv/zgGgB4rMYmIf+6A4l6Rr+o/6lHBQdW5aYd44bd8JttDCE/F/pNRr0lRE+PiqSPO8nDPHw0010JeMH9gYgnnFlyY3/OcJ02RhIPyyxYpv9FhY+2YiUkpwFOcLImyrxEsYXpD/0d3ac30bNH6Sw9JD9UZHYcpSxsIbECHw== Generated by Nova", + }).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &os.ImportedKeyPair, actual) +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleGetSuccessfully(t) + + actual, err := Get(client.ServiceClient(), "firstkey").Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &os.FirstKeyPair, actual) +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleDeleteSuccessfully(t) + + err := Delete(client.ServiceClient(), "deletedkey").ExtractErr() + th.AssertNoErr(t, err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/keypairs/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/keypairs/doc.go new file mode 100644 index 00000000000..31713752eae --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/keypairs/doc.go @@ -0,0 +1,3 @@ +// Package keypairs provides information and interaction with the keypair +// API resource for the Rackspace Cloud Servers service. +package keypairs diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/networks/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/networks/doc.go new file mode 100644 index 00000000000..8e5c77382d3 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/networks/doc.go @@ -0,0 +1,3 @@ +// Package networks provides information and interaction with the network +// API resource for the Rackspace Cloud Servers service. +package networks diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/networks/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/networks/requests.go new file mode 100644 index 00000000000..d3c973ecb21 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/networks/requests.go @@ -0,0 +1,101 @@ +package networks + +import ( + "errors" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" + + "github.com/racker/perigee" +) + +// 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) pagination.Pager { + createPage := func(r pagination.PageResult) pagination.Page { + return NetworkPage{pagination.SinglePageBase(r)} + } + + return pagination.NewPager(c, listURL(c), createPage) +} + +// 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.AuthenticatedHeaders(), + Results: &res.Body, + 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 struct { + // REQUIRED. See Network object for more info. + CIDR string + // REQUIRED. See Network object for more info. + Label string +} + +// ToNetworkCreateMap casts a CreateOpts struct to a map. +func (opts CreateOpts) ToNetworkCreateMap() (map[string]interface{}, error) { + n := make(map[string]interface{}) + + if opts.CIDR == "" { + return nil, errors.New("Required field CIDR not set.") + } + if opts.Label == "" { + return nil, errors.New("Required field Label not set.") + } + + n["label"] = opts.Label + n["cidr"] = opts.CIDR + 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.AuthenticatedHeaders(), + ReqBody: &reqBody, + Results: &res.Body, + OkCodes: []int{200, 201, 202}, + }) + 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.AuthenticatedHeaders(), + OkCodes: []int{204}, + }) + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/networks/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/networks/requests_test.go new file mode 100644 index 00000000000..6f44c1caba7 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/networks/requests_test.go @@ -0,0 +1,156 @@ +package networks + +import ( + "fmt" + "net/http" + "testing" + + "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("/os-networksv2", 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": [ + { + "label": "test-network-1", + "cidr": "192.168.100.0/24", + "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22" + }, + { + "label": "test-network-2", + "cidr": "192.30.250.00/18", + "id": "db193ab3-96e3-4cb3-8fc5-05f4296d0324" + } + ] +} + `) + }) + + client := fake.ServiceClient() + count := 0 + + err := List(client).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{ + Label: "test-network-1", + CIDR: "192.168.100.0/24", + ID: "d32019d3-bc6e-4319-9c1d-6722fc136a22", + }, + Network{ + Label: "test-network-2", + CIDR: "192.30.250.00/18", + ID: "db193ab3-96e3-4cb3-8fc5-05f4296d0324", + }, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, 1, count) +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/os-networksv2/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": { + "label": "test-network-1", + "cidr": "192.168.100.0/24", + "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.CIDR, "192.168.100.0/24") + th.AssertEquals(t, n.Label, "test-network-1") + th.AssertEquals(t, n.ID, "d32019d3-bc6e-4319-9c1d-6722fc136a22") +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/os-networksv2", 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": { + "label": "test-network-1", + "cidr": "192.168.100.0/24" + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, ` +{ + "network": { + "label": "test-network-1", + "cidr": "192.168.100.0/24", + "id": "4e8e5957-649f-477b-9e5b-f1f75b21c03c" + } +} + `) + }) + + options := CreateOpts{Label: "test-network-1", CIDR: "192.168.100.0/24"} + n, err := Create(fake.ServiceClient(), options).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, n.Label, "test-network-1") + th.AssertEquals(t, n.ID, "4e8e5957-649f-477b-9e5b-f1f75b21c03c") +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/os-networksv2/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/networks/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/networks/results.go new file mode 100644 index 00000000000..eb6a76c008c --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/networks/results.go @@ -0,0 +1,81 @@ +package networks + +import ( + "github.com/mitchellh/mapstructure" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +type commonResult struct { + gophercloud.Result +} + +// 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.Body, &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 +} + +// DeleteResult represents the result of a delete operation. +type DeleteResult struct { + gophercloud.ErrResult +} + +// 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. + Label string `mapstructure:"label" json:"label"` + + // Classless Inter-Domain Routing + CIDR string `mapstructure:"cidr" json:"cidr"` +} + +// NetworkPage is the page returned by a pager when traversing over a +// collection of networks. +type NetworkPage struct { + pagination.SinglePageBase +} + +// IsEmpty returns true if the NetworkPage contains no Networks. +func (r NetworkPage) IsEmpty() (bool, error) { + networks, err := ExtractNetworks(r) + if err != nil { + return true, err + } + return len(networks) == 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/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/networks/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/networks/urls.go new file mode 100644 index 00000000000..19a21aa90da --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/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("os-networksv2", id) +} + +func rootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("os-networksv2") +} + +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/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/networks/urls_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/networks/urls_test.go new file mode 100644 index 00000000000..983992e2b9b --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/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} +} + +func TestGetURL(t *testing.T) { + actual := getURL(endpointClient(), "foo") + expected := endpoint + "os-networksv2/foo" + th.AssertEquals(t, expected, actual) +} + +func TestCreateURL(t *testing.T) { + actual := createURL(endpointClient()) + expected := endpoint + "os-networksv2" + th.AssertEquals(t, expected, actual) +} + +func TestListURL(t *testing.T) { + actual := createURL(endpointClient()) + expected := endpoint + "os-networksv2" + th.AssertEquals(t, expected, actual) +} + +func TestDeleteURL(t *testing.T) { + actual := deleteURL(endpointClient(), "foo") + expected := endpoint + "os-networksv2/foo" + th.AssertEquals(t, expected, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/servers/delegate.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/servers/delegate.go new file mode 100644 index 00000000000..4c7b24909aa --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/servers/delegate.go @@ -0,0 +1,61 @@ +package servers + +import ( + "github.com/rackspace/gophercloud" + os "github.com/rackspace/gophercloud/openstack/compute/v2/servers" + "github.com/rackspace/gophercloud/pagination" +) + +// List makes a request against the API to list servers accessible to you. +func List(client *gophercloud.ServiceClient, opts os.ListOptsBuilder) pagination.Pager { + return os.List(client, opts) +} + +// Create requests a server to be provisioned to the user in the current tenant. +func Create(client *gophercloud.ServiceClient, opts os.CreateOptsBuilder) os.CreateResult { + return os.Create(client, opts) +} + +// Delete requests that a server previously provisioned be removed from your account. +func Delete(client *gophercloud.ServiceClient, id string) os.DeleteResult { + return os.Delete(client, id) +} + +// Get requests details on a single server, by ID. +func Get(client *gophercloud.ServiceClient, id string) os.GetResult { + return os.Get(client, id) +} + +// ChangeAdminPassword alters the administrator or root password for a specified server. +func ChangeAdminPassword(client *gophercloud.ServiceClient, id, newPassword string) os.ActionResult { + return os.ChangeAdminPassword(client, id, newPassword) +} + +// Reboot requests that a given server reboot. Two methods exist for rebooting a server: +// +// os.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 wait, power is restored or the VM instance restarted. +// +// os.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 os.RebootMethod) os.ActionResult { + return os.Reboot(client, id, how) +} + +// Rebuild will reprovision the server according to the configuration options provided in the +// RebuildOpts struct. +func Rebuild(client *gophercloud.ServiceClient, id string, opts os.RebuildOptsBuilder) os.RebuildResult { + return os.Rebuild(client, id, opts) +} + +// WaitForStatus will continually poll a server until it successfully transitions to a specified +// status. It will do this for at most the number of seconds specified. +func WaitForStatus(c *gophercloud.ServiceClient, id, status string, secs int) error { + return os.WaitForStatus(c, id, status, secs) +} + +// ExtractServers interprets the results of a single page from a List() call, producing a slice of Server entities. +func ExtractServers(page pagination.Page) ([]os.Server, error) { + return os.ExtractServers(page) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/servers/delegate_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/servers/delegate_test.go new file mode 100644 index 00000000000..7f414040f7e --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/servers/delegate_test.go @@ -0,0 +1,112 @@ +package servers + +import ( + "fmt" + "net/http" + "testing" + + os "github.com/rackspace/gophercloud/openstack/compute/v2/servers" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestListServers(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/servers/detail", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, ListOutput) + }) + + count := 0 + err := List(client.ServiceClient(), nil).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractServers(page) + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, ExpectedServerSlice, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, 1, count) +} + +func TestCreateServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleServerCreationSuccessfully(t, CreateOutput) + + actual, err := Create(client.ServiceClient(), os.CreateOpts{ + Name: "derp", + ImageRef: "f90f6034-2570-4974-8351-6b49732ef2eb", + FlavorRef: "1", + }).Extract() + th.AssertNoErr(t, err) + + th.CheckDeepEquals(t, &CreatedServer, actual) +} + +func TestDeleteServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleServerDeletionSuccessfully(t) + + res := Delete(client.ServiceClient(), "asdfasdfasdf") + th.AssertNoErr(t, res.Err) +} + +func TestGetServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/servers/8c65cb68-0681-4c30-bc88-6b83a8a26aee", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, GetOutput) + }) + + actual, err := Get(client.ServiceClient(), "8c65cb68-0681-4c30-bc88-6b83a8a26aee").Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &GophercloudServer, actual) +} + +func TestChangeAdminPassword(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleAdminPasswordChangeSuccessfully(t) + + res := ChangeAdminPassword(client.ServiceClient(), "1234asdf", "new-password") + th.AssertNoErr(t, res.Err) +} + +func TestReboot(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleRebootSuccessfully(t) + + res := Reboot(client.ServiceClient(), "1234asdf", os.SoftReboot) + th.AssertNoErr(t, res.Err) +} + +func TestRebuildServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleRebuildSuccessfully(t, GetOutput) + + opts := os.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(client.ServiceClient(), "1234asdf", opts).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &GophercloudServer, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/servers/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/servers/doc.go new file mode 100644 index 00000000000..c9f77f6945d --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/servers/doc.go @@ -0,0 +1,3 @@ +// Package servers provides information and interaction with the server +// API resource for the Rackspace Cloud Servers service. +package servers diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/servers/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/servers/fixtures.go new file mode 100644 index 00000000000..b22a28998d1 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/servers/fixtures.go @@ -0,0 +1,439 @@ +// +build fixtures + +package servers + +import ( + os "github.com/rackspace/gophercloud/openstack/compute/v2/servers" +) + +// ListOutput is the recorded output of a Rackspace servers.List request. +const ListOutput = ` +{ + "servers": [ + { + "OS-DCF:diskConfig": "MANUAL", + "OS-EXT-STS:power_state": 1, + "OS-EXT-STS:task_state": null, + "OS-EXT-STS:vm_state": "active", + "accessIPv4": "1.2.3.4", + "accessIPv6": "1111:4822:7818:121:2000:9b5e:7438:a2d0", + "addresses": { + "private": [ + { + "addr": "10.208.230.113", + "version": 4 + } + ], + "public": [ + { + "addr": "2001:4800:7818:101:2000:9b5e:7428:a2d0", + "version": 6 + }, + { + "addr": "104.130.131.164", + "version": 4 + } + ] + }, + "created": "2014-09-23T12:34:58Z", + "flavor": { + "id": "performance1-8", + "links": [ + { + "href": "https://dfw.servers.api.rackspacecloud.com/111111/flavors/performance1-8", + "rel": "bookmark" + } + ] + }, + "hostId": "e8951a524bc465b0898aeac7674da6fe1495e253ae1ea17ddb2c2475", + "id": "59818cee-bc8c-44eb-8073-673ee65105f7", + "image": { + "id": "255df5fb-e3d4-45a3-9a07-c976debf7c14", + "links": [ + { + "href": "https://dfw.servers.api.rackspacecloud.com/111111/images/255df5fb-e3d4-45a3-9a07-c976debf7c14", + "rel": "bookmark" + } + ] + }, + "key_name": "mykey", + "links": [ + { + "href": "https://dfw.servers.api.rackspacecloud.com/v2/111111/servers/59818cee-bc8c-44eb-8073-673ee65105f7", + "rel": "self" + }, + { + "href": "https://dfw.servers.api.rackspacecloud.com/111111/servers/59818cee-bc8c-44eb-8073-673ee65105f7", + "rel": "bookmark" + } + ], + "metadata": {}, + "name": "devstack", + "progress": 100, + "status": "ACTIVE", + "tenant_id": "111111", + "updated": "2014-09-23T12:38:19Z", + "user_id": "14ae7bb21d81422694655f3cc30f2930" + }, + { + "OS-DCF:diskConfig": "MANUAL", + "OS-EXT-STS:power_state": 1, + "OS-EXT-STS:task_state": null, + "OS-EXT-STS:vm_state": "active", + "accessIPv4": "1.1.2.3", + "accessIPv6": "2222:4444:7817:101:be76:4eff:f0e5:9e02", + "addresses": { + "private": [ + { + "addr": "10.10.20.30", + "version": 4 + } + ], + "public": [ + { + "addr": "1.1.2.3", + "version": 4 + }, + { + "addr": "2222:4444:7817:101:be76:4eff:f0e5:9e02", + "version": 6 + } + ] + }, + "created": "2014-07-21T19:32:55Z", + "flavor": { + "id": "performance1-2", + "links": [ + { + "href": "https://dfw.servers.api.rackspacecloud.com/111111/flavors/performance1-2", + "rel": "bookmark" + } + ] + }, + "hostId": "f859679906d6b1a38c1bd516b78f4dcc7d5fcf012578fa3ce460716c", + "id": "25f1c7f5-e00a-4715-b354-16e24b2f4630", + "image": { + "id": "bb02b1a3-bc77-4d17-ab5b-421d89850fca", + "links": [ + { + "href": "https://dfw.servers.api.rackspacecloud.com/111111/images/bb02b1a3-bc77-4d17-ab5b-421d89850fca", + "rel": "bookmark" + } + ] + }, + "key_name": "otherkey", + "links": [ + { + "href": "https://dfw.servers.api.rackspacecloud.com/v2/111111/servers/25f1c7f5-e00a-4715-b355-16e24b2f4630", + "rel": "self" + }, + { + "href": "https://dfw.servers.api.rackspacecloud.com/111111/servers/25f1c7f5-e00a-4715-b355-16e24b2f4630", + "rel": "bookmark" + } + ], + "metadata": {}, + "name": "peril-dfw", + "progress": 100, + "status": "ACTIVE", + "tenant_id": "111111", + "updated": "2014-07-21T19:34:24Z", + "user_id": "14ae7bb21d81422694655f3cc30f2930" + } + ] +} +` + +// GetOutput is the recorded output of a Rackspace servers.Get request. +const GetOutput = ` +{ + "server": { + "OS-DCF:diskConfig": "AUTO", + "OS-EXT-STS:power_state": 1, + "OS-EXT-STS:task_state": null, + "OS-EXT-STS:vm_state": "active", + "accessIPv4": "1.2.4.8", + "accessIPv6": "2001:4800:6666:105:2a0f:c056:f594:7777", + "addresses": { + "private": [ + { + "addr": "10.20.40.80", + "version": 4 + } + ], + "public": [ + { + "addr": "1.2.4.8", + "version": 4 + }, + { + "addr": "2001:4800:6666:105:2a0f:c056:f594:7777", + "version": 6 + } + ] + }, + "created": "2014-10-21T14:42:16Z", + "flavor": { + "id": "performance1-1", + "links": [ + { + "href": "https://dfw.servers.api.rackspacecloud.com/111111/flavors/performance1-1", + "rel": "bookmark" + } + ] + }, + "hostId": "430d2ae02de0a7af77012c94778145eccf67e75b1fac0528aa10d4a7", + "id": "8c65cb68-0681-4c30-bc88-6b83a8a26aee", + "image": { + "id": "e19a734c-c7e6-443a-830c-242209c4d65d", + "links": [ + { + "href": "https://dfw.servers.api.rackspacecloud.com/111111/images/e19a734c-c7e6-443a-830c-242209c4d65d", + "rel": "bookmark" + } + ] + }, + "key_name": null, + "links": [ + { + "href": "https://dfw.servers.api.rackspacecloud.com/v2/111111/servers/8c65cb68-0681-4c30-bc88-6b83a8a26aee", + "rel": "self" + }, + { + "href": "https://dfw.servers.api.rackspacecloud.com/111111/servers/8c65cb68-0681-4c30-bc88-6b83a8a26aee", + "rel": "bookmark" + } + ], + "metadata": {}, + "name": "Gophercloud-pxpGGuey", + "progress": 100, + "status": "ACTIVE", + "tenant_id": "111111", + "updated": "2014-10-21T14:42:57Z", + "user_id": "14ae7bb21d81423694655f4dd30f2930" + } +} +` + +// CreateOutput contains a sample of Rackspace's response to a Create call. +const CreateOutput = ` +{ + "server": { + "OS-DCF:diskConfig": "AUTO", + "adminPass": "v7tADqbE5pr9", + "id": "bb63327b-6a2f-34bc-b0ef-4b6d97ea637e", + "links": [ + { + "href": "https://dfw.servers.api.rackspacecloud.com/v2/111111/servers/bb63327b-6a2f-34bc-b0ef-4b6d97ea637e", + "rel": "self" + }, + { + "href": "https://dfw.servers.api.rackspacecloud.com/111111/servers/bb63327b-6a2f-34bc-b0ef-4b6d97ea637e", + "rel": "bookmark" + } + ] + } +} +` + +// DevstackServer is the expected first result from parsing ListOutput. +var DevstackServer = os.Server{ + ID: "59818cee-bc8c-44eb-8073-673ee65105f7", + Name: "devstack", + TenantID: "111111", + UserID: "14ae7bb21d81422694655f3cc30f2930", + HostID: "e8951a524bc465b0898aeac7674da6fe1495e253ae1ea17ddb2c2475", + Updated: "2014-09-23T12:38:19Z", + Created: "2014-09-23T12:34:58Z", + AccessIPv4: "1.2.3.4", + AccessIPv6: "1111:4822:7818:121:2000:9b5e:7438:a2d0", + Progress: 100, + Status: "ACTIVE", + Image: map[string]interface{}{ + "id": "255df5fb-e3d4-45a3-9a07-c976debf7c14", + "links": []interface{}{ + map[string]interface{}{ + "href": "https://dfw.servers.api.rackspacecloud.com/111111/images/255df5fb-e3d4-45a3-9a07-c976debf7c14", + "rel": "bookmark", + }, + }, + }, + Flavor: map[string]interface{}{ + "id": "performance1-8", + "links": []interface{}{ + map[string]interface{}{ + "href": "https://dfw.servers.api.rackspacecloud.com/111111/flavors/performance1-8", + "rel": "bookmark", + }, + }, + }, + Addresses: map[string]interface{}{ + "private": []interface{}{ + map[string]interface{}{ + "addr": "10.20.30.40", + "version": float64(4.0), + }, + }, + "public": []interface{}{ + map[string]interface{}{ + "addr": "1111:4822:7818:121:2000:9b5e:7438:a2d0", + "version": float64(6.0), + }, + map[string]interface{}{ + "addr": "1.2.3.4", + "version": float64(4.0), + }, + }, + }, + Metadata: map[string]interface{}{}, + Links: []interface{}{ + map[string]interface{}{ + "href": "https://dfw.servers.api.rackspacecloud.com/v2/111111/servers/59918cee-bd9d-44eb-8173-673ee75105f7", + "rel": "self", + }, + map[string]interface{}{ + "href": "https://dfw.servers.api.rackspacecloud.com/v2/111111/servers/59818cee-bc8c-44eb-8073-673ee65105f7", + "rel": "bookmark", + }, + }, + KeyName: "mykey", + AdminPass: "", +} + +// PerilServer is the expected second result from parsing ListOutput. +var PerilServer = os.Server{ + ID: "25f1c7f5-e00a-4715-b354-16e24b2f4630", + Name: "peril-dfw", + TenantID: "111111", + UserID: "14ae7bb21d81422694655f3cc30f2930", + HostID: "f859679906d6b1a38c1bd516b78f4dcc7d5fcf012578fa3ce460716c", + Updated: "2014-07-21T19:34:24Z", + Created: "2014-07-21T19:32:55Z", + AccessIPv4: "1.1.2.3", + AccessIPv6: "2222:4444:7817:101:be76:4eff:f0e5:9e02", + Progress: 100, + Status: "ACTIVE", + Image: map[string]interface{}{ + "id": "bb02b1a3-bc77-4d17-ab5b-421d89850fca", + "links": []interface{}{ + map[string]interface{}{ + "href": "https://dfw.servers.api.rackspacecloud.com/111111/images/bb02b1a3-bc77-4d17-ab5b-421d89850fca", + "rel": "bookmark", + }, + }, + }, + Flavor: map[string]interface{}{ + "id": "performance1-2", + "links": []interface{}{ + map[string]interface{}{ + "href": "https://dfw.servers.api.rackspacecloud.com/111111/flavors/performance1-2", + "rel": "bookmark", + }, + }, + }, + Addresses: map[string]interface{}{ + "private": []interface{}{ + map[string]interface{}{ + "addr": "10.10.20.30", + "version": float64(4.0), + }, + }, + "public": []interface{}{ + map[string]interface{}{ + "addr": "2222:4444:7817:101:be76:4eff:f0e5:9e02", + "version": float64(6.0), + }, + map[string]interface{}{ + "addr": "1.1.2.3", + "version": float64(4.0), + }, + }, + }, + Metadata: map[string]interface{}{}, + Links: []interface{}{ + map[string]interface{}{ + "href": "https://dfw.servers.api.rackspacecloud.com/v2/111111/servers/25f1c7f5-e00a-4715-b355-16e24b2f4630", + "rel": "self", + }, + map[string]interface{}{ + "href": "https://dfw.servers.api.rackspacecloud.com/v2/111111/servers/25f1c7f5-e00a-4715-b355-16e24b2f4630", + "rel": "bookmark", + }, + }, + KeyName: "otherkey", + AdminPass: "", +} + +// GophercloudServer is the expected result from parsing GetOutput. +var GophercloudServer = os.Server{ + ID: "8c65cb68-0681-4c30-bc88-6b83a8a26aee", + Name: "Gophercloud-pxpGGuey", + TenantID: "111111", + UserID: "14ae7bb21d81423694655f4dd30f2930", + HostID: "430d2ae02de0a7af77012c94778145eccf67e75b1fac0528aa10d4a7", + Updated: "2014-10-21T14:42:57Z", + Created: "2014-10-21T14:42:16Z", + AccessIPv4: "1.2.4.8", + AccessIPv6: "2001:4800:6666:105:2a0f:c056:f594:7777", + Progress: 100, + Status: "ACTIVE", + Image: map[string]interface{}{ + "id": "e19a734c-c7e6-443a-830c-242209c4d65d", + "links": []interface{}{ + map[string]interface{}{ + "href": "https://dfw.servers.api.rackspacecloud.com/111111/images/e19a734c-c7e6-443a-830c-242209c4d65d", + "rel": "bookmark", + }, + }, + }, + Flavor: map[string]interface{}{ + "id": "performance1-1", + "links": []interface{}{ + map[string]interface{}{ + "href": "https://dfw.servers.api.rackspacecloud.com/111111/flavors/performance1-1", + "rel": "bookmark", + }, + }, + }, + Addresses: map[string]interface{}{ + "private": []interface{}{ + map[string]interface{}{ + "addr": "10.20.40.80", + "version": float64(4.0), + }, + }, + "public": []interface{}{ + map[string]interface{}{ + "addr": "2001:4800:6666:105:2a0f:c056:f594:7777", + "version": float64(6.0), + }, + map[string]interface{}{ + "addr": "1.2.4.8", + "version": float64(4.0), + }, + }, + }, + Metadata: map[string]interface{}{}, + Links: []interface{}{ + map[string]interface{}{ + "href": "https://dfw.servers.api.rackspacecloud.com/v2/111111/servers/8c65cb68-0681-4c30-bc88-6b83a8a26aee", + "rel": "self", + }, + map[string]interface{}{ + "href": "https://dfw.servers.api.rackspacecloud.com/111111/servers/8c65cb68-0681-4c30-bc88-6b83a8a26aee", + "rel": "bookmark", + }, + }, + KeyName: "", + AdminPass: "", +} + +// CreatedServer is the partial Server struct that can be parsed from CreateOutput. +var CreatedServer = os.Server{ + ID: "bb63327b-6a2f-34bc-b0ef-4b6d97ea637e", + AdminPass: "v7tADqbE5pr9", + Links: []interface{}{}, +} + +// ExpectedServerSlice is the collection of servers, in order, that should be parsed from ListOutput. +var ExpectedServerSlice = []os.Server{DevstackServer, PerilServer} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/servers/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/servers/requests.go new file mode 100644 index 00000000000..884b9cb131e --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/servers/requests.go @@ -0,0 +1,158 @@ +package servers + +import ( + "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/bootfromvolume" + "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/diskconfig" + os "github.com/rackspace/gophercloud/openstack/compute/v2/servers" +) + +// CreateOpts specifies all of the options that Rackspace accepts in its Create request, including +// the union of all extensions that Rackspace supports. +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 []os.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 + + // Rackspace-specific extensions begin here. + + // KeyPair [optional] specifies the name of the SSH KeyPair to be injected into the newly launched + // server. See the "keypairs" extension in OpenStack compute v2. + KeyPair string + + // DiskConfig [optional] controls how the created server's disk is partitioned. See the "diskconfig" + // extension in OpenStack compute v2. + DiskConfig diskconfig.DiskConfig + + // BlockDevice [optional] will create the server from a volume, which is created from an image, + // a snapshot, or an another volume. + BlockDevice []bootfromvolume.BlockDevice +} + +// ToServerCreateMap constructs a request body using all of the OpenStack extensions that are +// active on Rackspace. +func (opts CreateOpts) ToServerCreateMap() (map[string]interface{}, error) { + base := os.CreateOpts{ + Name: opts.Name, + ImageRef: opts.ImageRef, + FlavorRef: opts.FlavorRef, + SecurityGroups: opts.SecurityGroups, + UserData: opts.UserData, + AvailabilityZone: opts.AvailabilityZone, + Networks: opts.Networks, + Metadata: opts.Metadata, + Personality: opts.Personality, + ConfigDrive: opts.ConfigDrive, + } + + drive := diskconfig.CreateOptsExt{ + CreateOptsBuilder: base, + DiskConfig: opts.DiskConfig, + } + + res, err := drive.ToServerCreateMap() + if err != nil { + return nil, err + } + + if len(opts.BlockDevice) != 0 { + bfv := bootfromvolume.CreateOptsExt{ + CreateOptsBuilder: drive, + BlockDevice: opts.BlockDevice, + } + + res, err = bfv.ToServerCreateMap() + if err != nil { + return nil, err + } + } + + // key_name doesn't actually come from the extension (or at least isn't documented there) so + // we need to add it manually. + serverMap := res["server"].(map[string]interface{}) + serverMap["key_name"] = opts.KeyPair + + return res, nil +} + +// RebuildOpts represents all of the configuration options used in a server rebuild operation that +// are supported by Rackspace. +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 + + // Rackspace-specific stuff begins here. + + // DiskConfig [optional] controls how the created server's disk is partitioned. See the "diskconfig" + // extension in OpenStack compute v2. + DiskConfig diskconfig.DiskConfig +} + +// ToServerRebuildMap constructs a request body using all of the OpenStack extensions that are +// active on Rackspace. +func (opts RebuildOpts) ToServerRebuildMap() (map[string]interface{}, error) { + base := os.RebuildOpts{ + ImageID: opts.ImageID, + Name: opts.Name, + AdminPass: opts.AdminPass, + AccessIPv4: opts.AccessIPv4, + AccessIPv6: opts.AccessIPv6, + Metadata: opts.Metadata, + Personality: opts.Personality, + } + + drive := diskconfig.RebuildOptsExt{ + RebuildOptsBuilder: base, + DiskConfig: opts.DiskConfig, + } + + return drive.ToServerRebuildMap() +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/servers/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/servers/requests_test.go new file mode 100644 index 00000000000..3c0f806936f --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/servers/requests_test.go @@ -0,0 +1,57 @@ +package servers + +import ( + "testing" + + "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/diskconfig" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestCreateOpts(t *testing.T) { + opts := CreateOpts{ + Name: "createdserver", + ImageRef: "image-id", + FlavorRef: "flavor-id", + KeyPair: "mykey", + DiskConfig: diskconfig.Manual, + } + + expected := ` + { + "server": { + "name": "createdserver", + "imageRef": "image-id", + "flavorRef": "flavor-id", + "key_name": "mykey", + "OS-DCF:diskConfig": "MANUAL" + } + } + ` + actual, err := opts.ToServerCreateMap() + th.AssertNoErr(t, err) + th.CheckJSONEquals(t, expected, actual) +} + +func TestRebuildOpts(t *testing.T) { + opts := RebuildOpts{ + Name: "rebuiltserver", + AdminPass: "swordfish", + ImageID: "asdfasdfasdf", + DiskConfig: diskconfig.Auto, + } + + actual, err := opts.ToServerRebuildMap() + th.AssertNoErr(t, err) + + expected := ` + { + "rebuild": { + "name": "rebuiltserver", + "imageRef": "asdfasdfasdf", + "adminPass": "swordfish", + "OS-DCF:diskConfig": "AUTO" + } + } + ` + th.CheckJSONEquals(t, expected, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/virtualinterfaces/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/virtualinterfaces/requests.go new file mode 100644 index 00000000000..bfe34878611 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/virtualinterfaces/requests.go @@ -0,0 +1,51 @@ +package virtualinterfaces + +import ( + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" + + "github.com/racker/perigee" +) + +// 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, instanceID string) pagination.Pager { + createPage := func(r pagination.PageResult) pagination.Page { + return VirtualInterfacePage{pagination.SinglePageBase(r)} + } + + return pagination.NewPager(c, listURL(c, instanceID), createPage) +} + +// Create creates a new virtual interface for a network and attaches the network +// to the server instance. +func Create(c *gophercloud.ServiceClient, instanceID, networkID string) CreateResult { + var res CreateResult + + reqBody := map[string]map[string]string{ + "virtual_interface": { + "network_id": networkID, + }, + } + + // Send request to API + _, res.Err = perigee.Request("POST", createURL(c, instanceID), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + ReqBody: &reqBody, + Results: &res.Body, + OkCodes: []int{200, 201, 202}, + }) + return res +} + +// Delete deletes the interface with interfaceID attached to the instance with +// instanceID. +func Delete(c *gophercloud.ServiceClient, instanceID, interfaceID string) DeleteResult { + var res DeleteResult + _, res.Err = perigee.Request("DELETE", deleteURL(c, instanceID, interfaceID), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + OkCodes: []int{200, 204}, + }) + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/virtualinterfaces/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/virtualinterfaces/requests_test.go new file mode 100644 index 00000000000..d40af9c4625 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/virtualinterfaces/requests_test.go @@ -0,0 +1,165 @@ +package virtualinterfaces + +import ( + "fmt" + "net/http" + "testing" + + "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("/servers/12345/os-virtual-interfacesv2", 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, ` +{ + "virtual_interfaces": [ + { + "id": "de7c6d53-b895-4b4a-963c-517ccb0f0775", + "ip_addresses": [ + { + "address": "192.168.0.2", + "network_id": "f212726e-6321-4210-9bae-a13f5a33f83f", + "network_label": "superprivate_xml" + } + ], + "mac_address": "BC:76:4E:04:85:20" + }, + { + "id": "e14e789d-3b98-44a6-9c2d-c23eb1d1465c", + "ip_addresses": [ + { + "address": "10.181.1.30", + "network_id": "3b324a1b-31b8-4db5-9fe5-4a2067f60297", + "network_label": "private" + } + ], + "mac_address": "BC:76:4E:04:81:55" + } + ] +} + `) + }) + + client := fake.ServiceClient() + count := 0 + + err := List(client, "12345").EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractVirtualInterfaces(page) + if err != nil { + t.Errorf("Failed to extract networks: %v", err) + return false, err + } + + expected := []VirtualInterface{ + VirtualInterface{ + MACAddress: "BC:76:4E:04:85:20", + IPAddresses: []IPAddress{ + IPAddress{ + Address: "192.168.0.2", + NetworkID: "f212726e-6321-4210-9bae-a13f5a33f83f", + NetworkLabel: "superprivate_xml", + }, + }, + ID: "de7c6d53-b895-4b4a-963c-517ccb0f0775", + }, + VirtualInterface{ + MACAddress: "BC:76:4E:04:81:55", + IPAddresses: []IPAddress{ + IPAddress{ + Address: "10.181.1.30", + NetworkID: "3b324a1b-31b8-4db5-9fe5-4a2067f60297", + NetworkLabel: "private", + }, + }, + ID: "e14e789d-3b98-44a6-9c2d-c23eb1d1465c", + }, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, 1, count) +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/servers/12345/os-virtual-interfacesv2", 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, ` +{ + "virtual_interface": { + "network_id": "6789" + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, `{ + "virtual_interfaces": [ + { + "id": "de7c6d53-b895-4b4a-963c-517ccb0f0775", + "ip_addresses": [ + { + "address": "192.168.0.2", + "network_id": "f212726e-6321-4210-9bae-a13f5a33f83f", + "network_label": "superprivate_xml" + } + ], + "mac_address": "BC:76:4E:04:85:20" + } + ] + }`) + }) + + expected := &VirtualInterface{ + MACAddress: "BC:76:4E:04:85:20", + IPAddresses: []IPAddress{ + IPAddress{ + Address: "192.168.0.2", + NetworkID: "f212726e-6321-4210-9bae-a13f5a33f83f", + NetworkLabel: "superprivate_xml", + }, + }, + ID: "de7c6d53-b895-4b4a-963c-517ccb0f0775", + } + + actual, err := Create(fake.ServiceClient(), "12345", "6789").Extract() + th.AssertNoErr(t, err) + + th.CheckDeepEquals(t, expected, actual) +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/servers/12345/os-virtual-interfacesv2/6789", 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(), "12345", "6789") + th.AssertNoErr(t, res.Err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/virtualinterfaces/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/virtualinterfaces/results.go new file mode 100644 index 00000000000..26fa7f31ce0 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/virtualinterfaces/results.go @@ -0,0 +1,81 @@ +package virtualinterfaces + +import ( + "github.com/mitchellh/mapstructure" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +type commonResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts a network resource. +func (r commonResult) Extract() (*VirtualInterface, error) { + if r.Err != nil { + return nil, r.Err + } + + var res struct { + VirtualInterfaces []VirtualInterface `mapstructure:"virtual_interfaces" json:"virtual_interfaces"` + } + + err := mapstructure.Decode(r.Body, &res) + + return &res.VirtualInterfaces[0], err +} + +// CreateResult represents the result of a create operation. +type CreateResult struct { + commonResult +} + +// DeleteResult represents the result of a delete operation. +type DeleteResult struct { + gophercloud.ErrResult +} + +// IPAddress represents a vitual address attached to a VirtualInterface. +type IPAddress struct { + Address string `mapstructure:"address" json:"address"` + NetworkID string `mapstructure:"network_id" json:"network_id"` + NetworkLabel string `mapstructure:"network_label" json:"network_label"` +} + +// VirtualInterface represents a virtual interface. +type VirtualInterface struct { + // UUID for the virtual interface + ID string `mapstructure:"id" json:"id"` + + MACAddress string `mapstructure:"mac_address" json:"mac_address"` + + IPAddresses []IPAddress `mapstructure:"ip_addresses" json:"ip_addresses"` +} + +// VirtualInterfacePage is the page returned by a pager when traversing over a +// collection of virtual interfaces. +type VirtualInterfacePage struct { + pagination.SinglePageBase +} + +// IsEmpty returns true if the NetworkPage contains no Networks. +func (r VirtualInterfacePage) IsEmpty() (bool, error) { + networks, err := ExtractVirtualInterfaces(r) + if err != nil { + return true, err + } + return len(networks) == 0, nil +} + +// ExtractVirtualInterfaces accepts a Page struct, specifically a VirtualInterfacePage struct, +// and extracts the elements into a slice of VirtualInterface structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractVirtualInterfaces(page pagination.Page) ([]VirtualInterface, error) { + var resp struct { + VirtualInterfaces []VirtualInterface `mapstructure:"virtual_interfaces" json:"virtual_interfaces"` + } + + err := mapstructure.Decode(page.(VirtualInterfacePage).Body, &resp) + + return resp.VirtualInterfaces, err +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/virtualinterfaces/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/virtualinterfaces/urls.go new file mode 100644 index 00000000000..9e5693e8490 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/virtualinterfaces/urls.go @@ -0,0 +1,15 @@ +package virtualinterfaces + +import "github.com/rackspace/gophercloud" + +func listURL(c *gophercloud.ServiceClient, instanceID string) string { + return c.ServiceURL("servers", instanceID, "os-virtual-interfacesv2") +} + +func createURL(c *gophercloud.ServiceClient, instanceID string) string { + return c.ServiceURL("servers", instanceID, "os-virtual-interfacesv2") +} + +func deleteURL(c *gophercloud.ServiceClient, instanceID, interfaceID string) string { + return c.ServiceURL("servers", instanceID, "os-virtual-interfacesv2", interfaceID) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/virtualinterfaces/urls_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/virtualinterfaces/urls_test.go new file mode 100644 index 00000000000..6732e4ed9f4 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/virtualinterfaces/urls_test.go @@ -0,0 +1,32 @@ +package virtualinterfaces + +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(), "12345") + expected := endpoint + "servers/12345/os-virtual-interfacesv2" + th.AssertEquals(t, expected, actual) +} + +func TestListURL(t *testing.T) { + actual := createURL(endpointClient(), "12345") + expected := endpoint + "servers/12345/os-virtual-interfacesv2" + th.AssertEquals(t, expected, actual) +} + +func TestDeleteURL(t *testing.T) { + actual := deleteURL(endpointClient(), "12345", "6789") + expected := endpoint + "servers/12345/os-virtual-interfacesv2/6789" + th.AssertEquals(t, expected, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/extensions/delegate.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/extensions/delegate.go new file mode 100644 index 00000000000..fc547cde5f4 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/extensions/delegate_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/extensions/delegate_test.go new file mode 100644 index 00000000000..e30f79404dc --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/extensions/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/extensions/doc.go new file mode 100644 index 00000000000..b02a95b534c --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/extensions/doc.go @@ -0,0 +1,3 @@ +// Package extensions provides information and interaction with the all the +// extensions available for the Rackspace Identity service. +package extensions diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/tenants/delegate.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/tenants/delegate.go new file mode 100644 index 00000000000..6cdd0cfbdcf --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/tenants/delegate_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/tenants/delegate_test.go new file mode 100644 index 00000000000..eccbfe23eb6 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/tenants/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/tenants/doc.go new file mode 100644 index 00000000000..c1825c21dcf --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/tenants/doc.go @@ -0,0 +1,3 @@ +// Package tenants provides information and interaction with the tenant +// API resource for the Rackspace Identity service. +package tenants diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/tokens/delegate.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/tokens/delegate.go new file mode 100644 index 00000000000..4f9885af03c --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/tokens/delegate_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/tokens/delegate_test.go new file mode 100644 index 00000000000..6678ff4a7c2 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/tokens/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/tokens/doc.go new file mode 100644 index 00000000000..44043e5e13f --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/tokens/doc.go @@ -0,0 +1,3 @@ +// Package tokens provides information and interaction with the token +// API resource for the Rackspace Identity service. +package tokens diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/accounts/delegate.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/accounts/delegate.go new file mode 100644 index 00000000000..94739308fa6 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/accounts/delegate.go @@ -0,0 +1,39 @@ +package accounts + +import ( + "github.com/rackspace/gophercloud" + os "github.com/rackspace/gophercloud/openstack/objectstorage/v1/accounts" +) + +// 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 +// ExtractHeader method on the GetResult. +func Get(c *gophercloud.ServiceClient) os.GetResult { + return os.Get(c, nil) +} + +// UpdateOpts is a structure that contains parameters for updating, creating, or +// deleting an account's metadata. +type UpdateOpts struct { + Metadata map[string]string + 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 will update an account's metadata with the Metadata in the UpdateOptsBuilder. +func Update(c *gophercloud.ServiceClient, opts os.UpdateOptsBuilder) os.UpdateResult { + return os.Update(c, opts) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/accounts/delegate_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/accounts/delegate_test.go new file mode 100644 index 00000000000..c568bd6e3b3 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/accounts/delegate_test.go @@ -0,0 +1,30 @@ +package accounts + +import ( + "testing" + + os "github.com/rackspace/gophercloud/openstack/objectstorage/v1/accounts" + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestGetAccounts(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleGetAccountSuccessfully(t) + + options := &UpdateOpts{Metadata: map[string]string{"gophercloud-test": "accounts"}} + res := Update(fake.ServiceClient(), options) + th.CheckNoErr(t, res.Err) +} + +func TestUpdateAccounts(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleUpdateAccountSuccessfully(t) + + expected := map[string]string{"Foo": "bar"} + actual, err := Get(fake.ServiceClient()).ExtractMetadata() + th.CheckNoErr(t, err) + th.CheckDeepEquals(t, expected, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/accounts/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/accounts/doc.go new file mode 100644 index 00000000000..293a93088a7 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/accounts/doc.go @@ -0,0 +1,3 @@ +// Package accounts provides information and interaction with the account +// API resource for the Rackspace Cloud Files service. +package accounts diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/bulk/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/bulk/doc.go new file mode 100644 index 00000000000..9c89e22b21b --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/bulk/doc.go @@ -0,0 +1,3 @@ +// Package bulk provides functionality for working with bulk operations in the +// Rackspace Cloud Files service. +package bulk diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/bulk/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/bulk/requests.go new file mode 100644 index 00000000000..d252609d418 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/bulk/requests.go @@ -0,0 +1,51 @@ +package bulk + +import ( + "net/url" + "strings" + + "github.com/racker/perigee" + "github.com/rackspace/gophercloud" +) + +// DeleteOptsBuilder allows extensions to add additional parameters to the +// Delete request. +type DeleteOptsBuilder interface { + ToBulkDeleteBody() (string, error) +} + +// DeleteOpts is a structure that holds parameters for deleting an object. +type DeleteOpts []string + +// ToBulkDeleteBody formats a DeleteOpts into a request body. +func (opts DeleteOpts) ToBulkDeleteBody() (string, error) { + return url.QueryEscape(strings.Join(opts, "\n")), nil +} + +// Delete will delete objects or containers in bulk. +func Delete(c *gophercloud.ServiceClient, opts DeleteOptsBuilder) DeleteResult { + var res DeleteResult + + if opts == nil { + return res + } + + reqString, err := opts.ToBulkDeleteBody() + if err != nil { + res.Err = err + return res + } + + reqBody := strings.NewReader(reqString) + + resp, err := perigee.Request("DELETE", deleteURL(c), perigee.Options{ + ContentType: "text/plain", + MoreHeaders: c.AuthenticatedHeaders(), + OkCodes: []int{200}, + ReqBody: reqBody, + Results: &res.Body, + }) + res.Header = resp.HttpResponse.Header + res.Err = err + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/bulk/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/bulk/requests_test.go new file mode 100644 index 00000000000..8b5578e91e8 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/bulk/requests_test.go @@ -0,0 +1,36 @@ +package bulk + +import ( + "fmt" + "net/http" + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestBulkDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + th.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.AssertEquals(t, r.URL.RawQuery, "bulk-delete") + + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, ` + { + "Number Not Found": 1, + "Response Status": "200 OK", + "Errors": [], + "Number Deleted": 1, + "Response Body": "" + } + `) + }) + + options := DeleteOpts{"gophercloud-testcontainer1", "gophercloud-testcontainer2"} + actual, err := Delete(fake.ServiceClient(), options).ExtractBody() + th.AssertNoErr(t, err) + th.AssertEquals(t, actual.NumberDeleted, 1) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/bulk/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/bulk/results.go new file mode 100644 index 00000000000..fddc125ac63 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/bulk/results.go @@ -0,0 +1,28 @@ +package bulk + +import ( + "github.com/rackspace/gophercloud" + + "github.com/mitchellh/mapstructure" +) + +// DeleteResult represents the result of a bulk delete operation. +type DeleteResult struct { + gophercloud.Result +} + +// DeleteRespBody is the form of the response body returned by a bulk delete request. +type DeleteRespBody struct { + NumberNotFound int `mapstructure:"Number Not Found"` + ResponseStatus string `mapstructure:"Response Status"` + Errors []string `mapstructure:"Errors"` + NumberDeleted int `mapstructure:"Number Deleted"` + ResponseBody string `mapstructure:"Response Body"` +} + +// ExtractBody will extract the body returned by the bulk extract request. +func (dr DeleteResult) ExtractBody() (DeleteRespBody, error) { + var resp DeleteRespBody + err := mapstructure.Decode(dr.Body, &resp) + return resp, err +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/bulk/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/bulk/urls.go new file mode 100644 index 00000000000..2e112033bec --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/bulk/urls.go @@ -0,0 +1,11 @@ +package bulk + +import "github.com/rackspace/gophercloud" + +func deleteURL(c *gophercloud.ServiceClient) string { + return c.Endpoint + "?bulk-delete" +} + +func extractURL(c *gophercloud.ServiceClient, ext string) string { + return c.Endpoint + "?extract-archive=" + ext +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/bulk/urls_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/bulk/urls_test.go new file mode 100644 index 00000000000..9169e52f16b --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/bulk/urls_test.go @@ -0,0 +1,26 @@ +package bulk + +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 TestDeleteURL(t *testing.T) { + actual := deleteURL(endpointClient()) + expected := endpoint + "?bulk-delete" + th.CheckEquals(t, expected, actual) +} + +func TestExtractURL(t *testing.T) { + actual := extractURL(endpointClient(), "tar") + expected := endpoint + "?extract-archive=tar" + th.CheckEquals(t, expected, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdncontainers/delegate.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdncontainers/delegate.go new file mode 100644 index 00000000000..d7eef20255d --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdncontainers/delegate.go @@ -0,0 +1,71 @@ +package cdncontainers + +import ( + "strconv" + + "github.com/rackspace/gophercloud" + os "github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers" + "github.com/rackspace/gophercloud/pagination" +) + +// ExtractNames interprets a page of List results when just the container +// names are requested. +func ExtractNames(page pagination.Page) ([]string, error) { + return os.ExtractNames(page) +} + +// ListOpts are options for listing Rackspace CDN containers. +type ListOpts struct { + EndMarker string `q:"end_marker"` + Format string `q:"format"` + Limit int `q:"limit"` + Marker string `q:"marker"` +} + +// 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 false, 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 os.ListOptsBuilder) pagination.Pager { + return os.List(c, opts) +} + +// 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) os.GetResult { + return os.Get(c, containerName) +} + +// UpdateOpts is a structure that holds parameters for updating, creating, or +// deleting a container's metadata. +type UpdateOpts struct { + CDNEnabled bool `h:"X-Cdn-Enabled"` + LogRetention bool `h:"X-Log-Retention"` + TTL int `h:"X-Ttl"` +} + +// 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 + } + h["X-Cdn-Enabled"] = strconv.FormatBool(opts.CDNEnabled) + return h, nil +} + +// Update is a function that creates, updates, or deletes a container's +// metadata. +func Update(c *gophercloud.ServiceClient, containerName string, opts os.UpdateOptsBuilder) os.UpdateResult { + return os.Update(c, containerName, opts) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdncontainers/delegate_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdncontainers/delegate_test.go new file mode 100644 index 00000000000..02c3c5e150a --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdncontainers/delegate_test.go @@ -0,0 +1,50 @@ +package cdncontainers + +import ( + "testing" + + os "github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestListCDNContainers(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleListContainerNamesSuccessfully(t) + + count := 0 + err := List(fake.ServiceClient(), nil).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractNames(page) + th.AssertNoErr(t, err) + + th.CheckDeepEquals(t, os.ExpectedListNames, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, count, 1) +} + +func TestGetCDNContainer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleGetContainerSuccessfully(t) + + _, err := Get(fake.ServiceClient(), "testContainer").ExtractMetadata() + th.CheckNoErr(t, err) + +} + +func TestUpdateCDNContainer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleUpdateContainerSuccessfully(t) + + options := &UpdateOpts{TTL: 3600} + res := Update(fake.ServiceClient(), "testContainer", options) + th.CheckNoErr(t, res.Err) + +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdncontainers/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdncontainers/doc.go new file mode 100644 index 00000000000..7b0930eeea6 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdncontainers/doc.go @@ -0,0 +1,3 @@ +// Package cdncontainers provides information and interaction with the CDN +// Container API resource for the Rackspace Cloud Files service. +package cdncontainers diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdncontainers/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdncontainers/requests.go new file mode 100644 index 00000000000..0567833204c --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdncontainers/requests.go @@ -0,0 +1,58 @@ +package cdncontainers + +import ( + "github.com/racker/perigee" + "github.com/rackspace/gophercloud" +) + +// EnableOptsBuilder allows extensions to add additional parameters to the Enable +// request. +type EnableOptsBuilder interface { + ToCDNContainerEnableMap() (map[string]string, error) +} + +// EnableOpts is a structure that holds options for enabling a CDN container. +type EnableOpts struct { + // CDNEnabled indicates whether or not the container is CDN enabled. Set to + // `true` to enable the container. Note that changing this setting from true + // to false will disable the container in the CDN but only after the TTL has + // expired. + CDNEnabled bool `h:"X-Cdn-Enabled"` + // TTL is the time-to-live for the container (in seconds). + TTL int `h:"X-Ttl"` +} + +// ToCDNContainerEnableMap formats an EnableOpts into a map of headers. +func (opts EnableOpts) ToCDNContainerEnableMap() (map[string]string, error) { + h, err := gophercloud.BuildHeaders(opts) + if err != nil { + return nil, err + } + return h, nil +} + +// Enable is a function that enables/disables a CDN container. +func Enable(c *gophercloud.ServiceClient, containerName string, opts EnableOptsBuilder) EnableResult { + var res EnableResult + h := c.AuthenticatedHeaders() + + if opts != nil { + headers, err := opts.ToCDNContainerEnableMap() + if err != nil { + res.Err = err + return res + } + + for k, v := range headers { + h[k] = v + } + } + + resp, err := perigee.Request("PUT", enableURL(c, containerName), perigee.Options{ + MoreHeaders: h, + OkCodes: []int{201, 202, 204}, + }) + res.Header = resp.HttpResponse.Header + res.Err = err + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdncontainers/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdncontainers/requests_test.go new file mode 100644 index 00000000000..28b963dacef --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdncontainers/requests_test.go @@ -0,0 +1,29 @@ +package cdncontainers + +import ( + "net/http" + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestEnableCDNContainer(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-Ttl", "259200") + w.Header().Add("X-Cdn-Enabled", "True") + w.WriteHeader(http.StatusNoContent) + }) + + options := &EnableOpts{CDNEnabled: true, TTL: 259200} + actual := Enable(fake.ServiceClient(), "testContainer", options) + th.AssertNoErr(t, actual.Err) + th.CheckEquals(t, actual.Header["X-Ttl"][0], "259200") + th.CheckEquals(t, actual.Header["X-Cdn-Enabled"][0], "True") +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdncontainers/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdncontainers/results.go new file mode 100644 index 00000000000..a5097ca7f65 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdncontainers/results.go @@ -0,0 +1,8 @@ +package cdncontainers + +import "github.com/rackspace/gophercloud" + +// EnableResult represents the result of a get operation. +type EnableResult struct { + gophercloud.HeaderResult +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdncontainers/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdncontainers/urls.go new file mode 100644 index 00000000000..80653f27624 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdncontainers/urls.go @@ -0,0 +1,7 @@ +package cdncontainers + +import "github.com/rackspace/gophercloud" + +func enableURL(c *gophercloud.ServiceClient, containerName string) string { + return c.ServiceURL(containerName) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdncontainers/urls_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdncontainers/urls_test.go new file mode 100644 index 00000000000..aa5bfe68b29 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdncontainers/urls_test.go @@ -0,0 +1,20 @@ +package cdncontainers + +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 TestEnableURL(t *testing.T) { + actual := enableURL(endpointClient(), "foo") + expected := endpoint + "foo" + th.CheckEquals(t, expected, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdnobjects/delegate.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdnobjects/delegate.go new file mode 100644 index 00000000000..e9d2ff1d6f3 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdnobjects/delegate.go @@ -0,0 +1,11 @@ +package cdnobjects + +import ( + "github.com/rackspace/gophercloud" + os "github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects" +) + +// Delete is a function that deletes an object from the CDN. +func Delete(c *gophercloud.ServiceClient, containerName, objectName string, opts os.DeleteOptsBuilder) os.DeleteResult { + return os.Delete(c, containerName, objectName, nil) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdnobjects/delegate_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdnobjects/delegate_test.go new file mode 100644 index 00000000000..b5e04a98c3b --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdnobjects/delegate_test.go @@ -0,0 +1,19 @@ +package cdnobjects + +import ( + "testing" + + os "github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects" + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestDeleteCDNObject(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleDeleteObjectSuccessfully(t) + + res := Delete(fake.ServiceClient(), "testContainer", "testObject", nil) + th.AssertNoErr(t, res.Err) + +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdnobjects/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdnobjects/doc.go new file mode 100644 index 00000000000..90cd5c97ffc --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdnobjects/doc.go @@ -0,0 +1,3 @@ +// Package cdnobjects provides information and interaction with the CDN +// Object API resource for the Rackspace Cloud Files service. +package cdnobjects diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/containers/delegate.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/containers/delegate.go new file mode 100644 index 00000000000..77ed0025743 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/containers/delegate.go @@ -0,0 +1,93 @@ +package containers + +import ( + "github.com/rackspace/gophercloud" + os "github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers" + "github.com/rackspace/gophercloud/pagination" +) + +// ExtractInfo interprets a page of List results when full container info +// is requested. +func ExtractInfo(page pagination.Page) ([]os.Container, error) { + return os.ExtractInfo(page) +} + +// ExtractNames interprets a page of List results when just the container +// names are requested. +func ExtractNames(page pagination.Page) ([]string, error) { + return os.ExtractNames(page) +} + +// 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 os.ListOptsBuilder) pagination.Pager { + return os.List(c, opts) +} + +// CreateOpts is a structure that holds parameters for creating a container. +type CreateOpts struct { + Metadata map[string]string + ContainerRead string `h:"X-Container-Read"` + ContainerWrite string `h:"X-Container-Write"` + 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 os.CreateOptsBuilder) os.CreateResult { + return os.Create(c, containerName, opts) +} + +// Delete is a function that deletes a container. +func Delete(c *gophercloud.ServiceClient, containerName string) os.DeleteResult { + return os.Delete(c, containerName) +} + +// UpdateOpts is a structure that holds parameters for updating or creating a +// container's metadata. +type UpdateOpts struct { + Metadata map[string]string + ContainerRead string `h:"X-Container-Read"` + 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 os.UpdateOptsBuilder) os.UpdateResult { + return os.Update(c, containerName, opts) +} + +// 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) os.GetResult { + return os.Get(c, containerName) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/containers/delegate_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/containers/delegate_test.go new file mode 100644 index 00000000000..7ba4eb21c68 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/containers/delegate_test.go @@ -0,0 +1,91 @@ +package containers + +import ( + "testing" + + os "github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestListContainerInfo(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleListContainerInfoSuccessfully(t) + + count := 0 + err := List(fake.ServiceClient(), &os.ListOpts{Full: true}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractInfo(page) + th.AssertNoErr(t, err) + + th.CheckDeepEquals(t, os.ExpectedListInfo, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, count, 1) +} + +func TestListContainerNames(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleListContainerNamesSuccessfully(t) + + count := 0 + err := List(fake.ServiceClient(), &os.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 + } + + th.CheckDeepEquals(t, os.ExpectedListNames, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, count, 1) +} + +func TestCreateContainers(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleCreateContainerSuccessfully(t) + + options := os.CreateOpts{ContentType: "application/json", Metadata: map[string]string{"foo": "bar"}} + res := Create(fake.ServiceClient(), "testContainer", options) + th.CheckNoErr(t, res.Err) + th.CheckEquals(t, "bar", res.Header["X-Container-Meta-Foo"][0]) + +} + +func TestDeleteContainers(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleDeleteContainerSuccessfully(t) + + res := Delete(fake.ServiceClient(), "testContainer") + th.CheckNoErr(t, res.Err) +} + +func TestUpdateContainers(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleUpdateContainerSuccessfully(t) + + options := &os.UpdateOpts{Metadata: map[string]string{"foo": "bar"}} + res := Update(fake.ServiceClient(), "testContainer", options) + th.CheckNoErr(t, res.Err) +} + +func TestGetContainers(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleGetContainerSuccessfully(t) + + _, err := Get(fake.ServiceClient(), "testContainer").ExtractMetadata() + th.CheckNoErr(t, err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/containers/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/containers/doc.go new file mode 100644 index 00000000000..d132a07382a --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/containers/doc.go @@ -0,0 +1,3 @@ +// Package containers provides information and interaction with the Container +// API resource for the Rackspace Cloud Files service. +package containers diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/objects/delegate.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/objects/delegate.go new file mode 100644 index 00000000000..bd4a4f08355 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/objects/delegate.go @@ -0,0 +1,90 @@ +package objects + +import ( + "io" + + "github.com/rackspace/gophercloud" + os "github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects" + "github.com/rackspace/gophercloud/pagination" +) + +// ExtractInfo is a function that takes a page of objects and returns their full information. +func ExtractInfo(page pagination.Page) ([]os.Object, error) { + return os.ExtractInfo(page) +} + +// ExtractNames is a function that takes a page of objects and returns only their names. +func ExtractNames(page pagination.Page) ([]string, error) { + return os.ExtractNames(page) +} + +// List is a function that retrieves objects in the container as +// well as container metadata. It returns a pager which can be iterated with the +// EachPage function. +func List(c *gophercloud.ServiceClient, containerName string, opts os.ListOptsBuilder) pagination.Pager { + return os.List(c, containerName, opts) +} + +// 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 os.DownloadOptsBuilder) os.DownloadResult { + return os.Download(c, containerName, objectName, opts) +} + +// 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 os.CreateOptsBuilder) os.CreateResult { + return os.Create(c, containerName, objectName, content, opts) +} + +// 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"` + ContentLength int `h:"Content-Length"` + ContentType string `h:"Content-Type"` + CopyFrom string `h:"X-Copy_From"` + Destination string `h:"Destination"` + DetectContentType bool `h:"X-Detect-Content-Type"` +} + +// ToObjectCopyMap formats a CopyOpts into a map of headers. +func (opts CopyOpts) ToObjectCopyMap() (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 + } + // `Content-Length` is required and a value of "0" is acceptable, but calling `gophercloud.BuildHeaders` + // will remove the `Content-Length` header if it's set to 0 (or equivalently not set). This will add + // the header if it's not already set. + if _, ok := h["Content-Length"]; !ok { + h["Content-Length"] = "0" + } + return h, nil +} + +// Copy is a function that copies one object to another. +func Copy(c *gophercloud.ServiceClient, containerName, objectName string, opts os.CopyOptsBuilder) os.CopyResult { + return os.Copy(c, containerName, objectName, opts) +} + +// Delete is a function that deletes an object. +func Delete(c *gophercloud.ServiceClient, containerName, objectName string, opts os.DeleteOptsBuilder) os.DeleteResult { + return os.Delete(c, containerName, objectName, opts) +} + +// 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 os.GetOptsBuilder) os.GetResult { + return os.Get(c, containerName, objectName, opts) +} + +// Update is a function that creates, updates, or deletes an object's metadata. +func Update(c *gophercloud.ServiceClient, containerName, objectName string, opts os.UpdateOptsBuilder) os.UpdateResult { + return os.Update(c, containerName, objectName, opts) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/objects/delegate_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/objects/delegate_test.go new file mode 100644 index 00000000000..08831ec56a1 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/objects/delegate_test.go @@ -0,0 +1,115 @@ +package objects + +import ( + "bytes" + "testing" + + os "github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestDownloadObject(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleDownloadObjectSuccessfully(t) + + content, err := Download(fake.ServiceClient(), "testContainer", "testObject", nil).ExtractContent() + th.AssertNoErr(t, err) + th.CheckEquals(t, string(content), "Successful download with Gophercloud") +} + +func TestListObjectsInfo(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleListObjectsInfoSuccessfully(t) + + count := 0 + options := &os.ListOpts{Full: true} + err := List(fake.ServiceClient(), "testContainer", options).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractInfo(page) + th.AssertNoErr(t, err) + + th.CheckDeepEquals(t, os.ExpectedListInfo, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, count, 1) +} + +func TestListObjectNames(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleListObjectNamesSuccessfully(t) + + count := 0 + options := &os.ListOpts{Full: false} + err := List(fake.ServiceClient(), "testContainer", options).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 + } + + th.CheckDeepEquals(t, os.ExpectedListNames, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, count, 1) +} + +func TestCreateObject(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleCreateObjectSuccessfully(t) + + content := bytes.NewBufferString("Did gyre and gimble in the wabe") + options := &os.CreateOpts{ContentType: "application/json"} + res := Create(fake.ServiceClient(), "testContainer", "testObject", content, options) + th.AssertNoErr(t, res.Err) +} + +func TestCopyObject(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleCopyObjectSuccessfully(t) + + options := &CopyOpts{Destination: "/newTestContainer/newTestObject"} + res := Copy(fake.ServiceClient(), "testContainer", "testObject", options) + th.AssertNoErr(t, res.Err) +} + +func TestDeleteObject(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleDeleteObjectSuccessfully(t) + + res := Delete(fake.ServiceClient(), "testContainer", "testObject", nil) + th.AssertNoErr(t, res.Err) +} + +func TestUpdateObject(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleUpdateObjectSuccessfully(t) + + options := &os.UpdateOpts{Metadata: map[string]string{"Gophercloud-Test": "objects"}} + res := Update(fake.ServiceClient(), "testContainer", "testObject", options) + th.AssertNoErr(t, res.Err) +} + +func TestGetObject(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleGetObjectSuccessfully(t) + + expected := map[string]string{"Gophercloud-Test": "objects"} + actual, err := Get(fake.ServiceClient(), "testContainer", "testObject", nil).ExtractMetadata() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, expected, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/objects/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/objects/doc.go new file mode 100644 index 00000000000..781984bee25 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/objects/doc.go @@ -0,0 +1,3 @@ +// Package objects provides information and interaction with the Object +// API resource for the Rackspace Cloud Files service. +package objects diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/reauth.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/reauth.go deleted file mode 100644 index 342aca46108..00000000000 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/reauth.go +++ /dev/null @@ -1,36 +0,0 @@ -package gophercloud - -import ( - "github.com/racker/perigee" -) - -// WithReauth wraps a Perigee request fragment with logic to perform re-authentication -// if it's deemed necessary. -// -// Do not confuse this function with WithReauth()! Although they work together to support reauthentication, -// WithReauth() actually contains the decision-making logic to determine when to perform a reauth, -// while WithReauthHandler() is used to configure what a reauth actually entails. -func (c *Context) WithReauth(ap AccessProvider, f func() error) error { - err := f() - cause, ok := err.(*perigee.UnexpectedResponseCodeError) - if ok && cause.Actual == 401 { - err = c.reauthHandler(ap) - if err == nil { - err = f() - } - } - return err -} - -// This is like WithReauth above but returns a perigee Response object -func (c *Context) ResponseWithReauth(ap AccessProvider, f func() (*perigee.Response, error)) (*perigee.Response, error) { - response, err := f() - cause, ok := err.(*perigee.UnexpectedResponseCodeError) - if ok && cause.Actual == 401 { - err = c.reauthHandler(ap) - if err == nil { - response, err = f() - } - } - return response, err -} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/reauth_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/reauth_test.go deleted file mode 100644 index e3501b87fb3..00000000000 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/reauth_test.go +++ /dev/null @@ -1,133 +0,0 @@ -package gophercloud - -import ( - "github.com/racker/perigee" - "testing" -) - -// This reauth-handler does nothing, and returns no error. -func doNothing(_ AccessProvider) error { - return nil -} - -func TestOtherErrorsPropegate(t *testing.T) { - calls := 0 - c := TestContext().WithReauthHandler(doNothing) - - err := c.WithReauth(nil, func() error { - calls++ - return &perigee.UnexpectedResponseCodeError{ - Expected: []int{204}, - Actual: 404, - } - }) - - if err == nil { - t.Error("Expected MyError to be returned; got nil instead.") - return - } - if _, ok := err.(*perigee.UnexpectedResponseCodeError); !ok { - t.Error("Expected UnexpectedResponseCodeError; got %#v", err) - return - } - if calls != 1 { - t.Errorf("Expected the body to be invoked once; found %d calls instead", calls) - return - } -} - -func Test401ErrorCausesBodyInvokation2ndTime(t *testing.T) { - calls := 0 - c := TestContext().WithReauthHandler(doNothing) - - err := c.WithReauth(nil, func() error { - calls++ - return &perigee.UnexpectedResponseCodeError{ - Expected: []int{204}, - Actual: 401, - } - }) - - if err == nil { - t.Error("Expected MyError to be returned; got nil instead.") - return - } - if calls != 2 { - t.Errorf("Expected the body to be invoked once; found %d calls instead", calls) - return - } -} - -func TestReauthAttemptShouldHappen(t *testing.T) { - calls := 0 - c := TestContext().WithReauthHandler(func(_ AccessProvider) error { - calls++ - return nil - }) - c.WithReauth(nil, func() error { - return &perigee.UnexpectedResponseCodeError{ - Expected: []int{204}, - Actual: 401, - } - }) - - if calls != 1 { - t.Errorf("Expected Reauthenticator to be called once; found %d instead", calls) - return - } -} - -type MyError struct{} - -func (*MyError) Error() string { - return "MyError instance" -} - -func TestReauthErrorShouldPropegate(t *testing.T) { - c := TestContext().WithReauthHandler(func(_ AccessProvider) error { - return &MyError{} - }) - - err := c.WithReauth(nil, func() error { - return &perigee.UnexpectedResponseCodeError{ - Expected: []int{204}, - Actual: 401, - } - }) - - if _, ok := err.(*MyError); !ok { - t.Errorf("Expected a MyError; got %#v", err) - return - } -} - -type MyAccess struct{} - -func (my *MyAccess) FirstEndpointUrlByCriteria(ApiCriteria) string { - return "" -} -func (my *MyAccess) AuthToken() string { - return "" -} -func (my *MyAccess) Revoke(string) error { - return nil -} -func (my *MyAccess) Reauthenticate() error { - return nil -} - -func TestReauthHandlerUsesSameAccessProvider(t *testing.T) { - fakeAccess := &MyAccess{} - c := TestContext().WithReauthHandler(func(acc AccessProvider) error { - if acc != fakeAccess { - t.Errorf("Expected acc = fakeAccess") - } - return nil - }) - c.WithReauth(fakeAccess, func() error { - return &perigee.UnexpectedResponseCodeError{ - Expected: []int{204}, - Actual: 401, - } - }) -} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/results.go new file mode 100644 index 00000000000..f480bc7ca39 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/results.go @@ -0,0 +1,83 @@ +package gophercloud + +import ( + "encoding/json" + "net/http" +) + +// Result acts as a base struct that other results can embed. +type Result struct { + // Body is the payload of the HTTP response from the server. In most cases, this will be the + // deserialized JSON structure. + Body interface{} + + // Header contains the HTTP header structure from the original response. + Header http.Header + + // Err is an error that occurred during the operation. It's deferred until extraction to make + // it easier to chain operations. + Err error +} + +// PrettyPrintJSON creates a string containing the full response body as pretty-printed JSON. +func (r Result) PrettyPrintJSON() string { + pretty, err := json.MarshalIndent(r.Body, "", " ") + if err != nil { + panic(err.Error()) + } + return string(pretty) +} + +// ErrResult represents results that only contain a potential error and +// nothing else. Usually if the operation executed successfully, the Err field +// will be nil; otherwise it will be stocked with a relevant error. +type ErrResult struct { + Result +} + +// ExtractErr is a function that extracts error information from a result. +func (r ErrResult) ExtractErr() error { + return r.Err +} + +// HeaderResult represents a result that only contains an `error` (possibly nil) +// and an http.Header. This is used, for example, by the `objectstorage` packages +// in `openstack`, because most of the operations don't return response bodies. +type HeaderResult struct { + Result +} + +// ExtractHeader will return the http.Header and error from the HeaderResult. +// Usage: header, err := objects.Create(client, "my_container", objects.CreateOpts{}).ExtractHeader() +func (hr HeaderResult) ExtractHeader() (http.Header, error) { + return hr.Header, hr.Err +} + +// 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 convention of nesting back and next URLs in a "links" +// JSON array. +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/Godeps/_workspace/src/github.com/rackspace/gophercloud/script/acceptancetest b/Godeps/_workspace/src/github.com/rackspace/gophercloud/script/acceptancetest new file mode 100644 index 00000000000..f9c89f4dfdd --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/script/acceptancetest @@ -0,0 +1,5 @@ +#!/bin/bash +# +# Run the acceptance tests. + +exec go test -p=1 -tags 'acceptance fixtures' github.com/rackspace/gophercloud/acceptance/... $@ diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/scripts/create-environment.sh b/Godeps/_workspace/src/github.com/rackspace/gophercloud/script/bootstrap similarity index 100% rename from Godeps/_workspace/src/github.com/rackspace/gophercloud/scripts/create-environment.sh rename to Godeps/_workspace/src/github.com/rackspace/gophercloud/script/bootstrap diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/script/cibuild b/Godeps/_workspace/src/github.com/rackspace/gophercloud/script/cibuild new file mode 100644 index 00000000000..1cb389e7dce --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/script/cibuild @@ -0,0 +1,5 @@ +#!/bin/bash +# +# Test script to be invoked by Travis. + +exec script/unittest -v diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/script/test b/Godeps/_workspace/src/github.com/rackspace/gophercloud/script/test new file mode 100644 index 00000000000..1e03dff8ab3 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/script/test @@ -0,0 +1,5 @@ +#!/bin/bash +# +# Run all the tests. + +exec go test -tags 'acceptance fixtures' ./... $@ diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/script/unittest b/Godeps/_workspace/src/github.com/rackspace/gophercloud/script/unittest new file mode 100644 index 00000000000..d3440a902c0 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/script/unittest @@ -0,0 +1,5 @@ +#!/bin/bash +# +# Run the unit tests. + +exec go test -tags fixtures ./... $@ diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/scripts/test-all.sh b/Godeps/_workspace/src/github.com/rackspace/gophercloud/scripts/test-all.sh deleted file mode 100644 index 096736f2595..00000000000 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/scripts/test-all.sh +++ /dev/null @@ -1,37 +0,0 @@ -#!/bin/bash -# -# This script is responsible for executing all the acceptance tests found in -# the acceptance/ directory. - -# Find where _this_ script is running from. -SCRIPTS=$(dirname $0) -SCRIPTS=$(cd $SCRIPTS; pwd) - -# Locate the acceptance test / examples directory. -ACCEPTANCE=$(cd $SCRIPTS/../acceptance; pwd) - -# Go workspace path -WS=$(cd $SCRIPTS/..; pwd) - -# In order to run Go code interactively, we need the GOPATH environment -# to be set. -if [ "x$GOPATH" == "x" ]; then - export GOPATH=$WS - echo "WARNING: You didn't have your GOPATH environment variable set." - echo " I'm assuming $GOPATH as its value." -fi - -# Run all acceptance tests sequentially. -# If any test fails, we fail fast. -LIBS=$(ls $ACCEPTANCE/lib*.go) -for T in $(ls -1 $ACCEPTANCE/[0-9][0-9]*.go); do - if ! [ -x $T ]; then - CMD="go run $T $LIBS -quiet" - echo "$CMD ..." - if ! $CMD ; then - echo "- FAILED. Try re-running w/out the -quiet option to see output." - exit 1 - fi - fi -done - diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/servers.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/servers.go deleted file mode 100644 index 1f6a7a47882..00000000000 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/servers.go +++ /dev/null @@ -1,807 +0,0 @@ -// TODO(sfalvo): Remove Rackspace-specific Server structure fields and refactor them into a provider-specific access method. -// Be sure to update godocs accordingly. - -package gophercloud - -import ( - "fmt" - "net/url" - "strings" - - "github.com/mitchellh/mapstructure" - "github.com/racker/perigee" -) - -// genericServersProvider structures provide the implementation for generic OpenStack-compatible -// CloudServersProvider interfaces. -type genericServersProvider struct { - // endpoint refers to the provider's API endpoint base URL. This will be used to construct - // and issue queries. - endpoint string - - // Test context (if any) in which to issue requests. - context *Context - - // access associates this API provider with a set of credentials, - // which may be automatically renewed if they near expiration. - access AccessProvider -} - -// See the CloudServersProvider interface for details. -func (gcp *genericServersProvider) ListServersByFilter(filter url.Values) ([]Server, error) { - var ss []Server - - err := gcp.context.WithReauth(gcp.access, func() error { - url := gcp.endpoint + "/servers/detail?" + filter.Encode() - return perigee.Get(url, perigee.Options{ - CustomClient: gcp.context.httpClient, - Results: &struct{ Servers *[]Server }{&ss}, - MoreHeaders: map[string]string{ - "X-Auth-Token": gcp.access.AuthToken(), - }, - }) - }) - return ss, err -} - -// See the CloudServersProvider interface for details. -func (gcp *genericServersProvider) ListServersLinksOnly() ([]Server, error) { - var ss []Server - - err := gcp.context.WithReauth(gcp.access, func() error { - url := gcp.endpoint + "/servers" - return perigee.Get(url, perigee.Options{ - CustomClient: gcp.context.httpClient, - Results: &struct{ Servers *[]Server }{&ss}, - MoreHeaders: map[string]string{ - "X-Auth-Token": gcp.access.AuthToken(), - }, - }) - }) - return ss, err -} - -// See the CloudServersProvider interface for details. -func (gcp *genericServersProvider) ListServers() ([]Server, error) { - var ss []Server - - err := gcp.context.WithReauth(gcp.access, func() error { - url := gcp.endpoint + "/servers/detail" - return perigee.Get(url, perigee.Options{ - CustomClient: gcp.context.httpClient, - Results: &struct{ Servers *[]Server }{&ss}, - MoreHeaders: map[string]string{ - "X-Auth-Token": gcp.access.AuthToken(), - }, - }) - }) - - // Compatibility with v0.0.x -- we "map" our public and private - // addresses into a legacy structure field for the benefit of - // earlier software. - - if err != nil { - return ss, err - } - - for _, s := range ss { - err = mapstructure.Decode(s.RawAddresses, &s.Addresses) - if err != nil { - return ss, err - } - } - - return ss, err -} - -// See the CloudServersProvider interface for details. -func (gsp *genericServersProvider) ServerById(id string) (*Server, error) { - var s *Server - - err := gsp.context.WithReauth(gsp.access, func() error { - url := gsp.endpoint + "/servers/" + id - return perigee.Get(url, perigee.Options{ - Results: &struct{ Server **Server }{&s}, - MoreHeaders: map[string]string{ - "X-Auth-Token": gsp.access.AuthToken(), - }, - OkCodes: []int{200}, - }) - }) - - // Compatibility with v0.0.x -- we "map" our public and private - // addresses into a legacy structure field for the benefit of - // earlier software. - - if err != nil { - return s, err - } - - err = mapstructure.Decode(s.RawAddresses, &s.Addresses) - - return s, err -} - -// See the CloudServersProvider interface for details. -func (gsp *genericServersProvider) CreateServer(ns NewServer) (*NewServer, error) { - var s *NewServer - - err := gsp.context.WithReauth(gsp.access, func() error { - ep := gsp.endpoint + "/servers" - return perigee.Post(ep, perigee.Options{ - ReqBody: &struct { - Server *NewServer `json:"server"` - }{&ns}, - Results: &struct{ Server **NewServer }{&s}, - MoreHeaders: map[string]string{ - "X-Auth-Token": gsp.access.AuthToken(), - }, - OkCodes: []int{202}, - }) - }) - - return s, err -} - -// See the CloudServersProvider interface for details. -func (gsp *genericServersProvider) DeleteServerById(id string) error { - err := gsp.context.WithReauth(gsp.access, func() error { - url := gsp.endpoint + "/servers/" + id - return perigee.Delete(url, perigee.Options{ - MoreHeaders: map[string]string{ - "X-Auth-Token": gsp.access.AuthToken(), - }, - OkCodes: []int{204}, - }) - }) - return err -} - -// See the CloudServersProvider interface for details. -func (gsp *genericServersProvider) SetAdminPassword(id, pw string) error { - err := gsp.context.WithReauth(gsp.access, func() error { - url := fmt.Sprintf("%s/servers/%s/action", gsp.endpoint, id) - return perigee.Post(url, perigee.Options{ - ReqBody: &struct { - ChangePassword struct { - AdminPass string `json:"adminPass"` - } `json:"changePassword"` - }{ - struct { - AdminPass string `json:"adminPass"` - }{pw}, - }, - OkCodes: []int{202}, - MoreHeaders: map[string]string{ - "X-Auth-Token": gsp.access.AuthToken(), - }, - }) - }) - return err -} - -// See the CloudServersProvider interface for details. -func (gsp *genericServersProvider) ResizeServer(id, newName, newFlavor, newDiskConfig string) error { - err := gsp.context.WithReauth(gsp.access, func() error { - url := fmt.Sprintf("%s/servers/%s/action", gsp.endpoint, id) - rr := ResizeRequest{ - Name: newName, - FlavorRef: newFlavor, - DiskConfig: newDiskConfig, - } - return perigee.Post(url, perigee.Options{ - ReqBody: &struct { - Resize ResizeRequest `json:"resize"` - }{rr}, - OkCodes: []int{202}, - MoreHeaders: map[string]string{ - "X-Auth-Token": gsp.access.AuthToken(), - }, - }) - }) - return err -} - -// See the CloudServersProvider interface for details. -func (gsp *genericServersProvider) RevertResize(id string) error { - err := gsp.context.WithReauth(gsp.access, func() error { - url := fmt.Sprintf("%s/servers/%s/action", gsp.endpoint, id) - return perigee.Post(url, perigee.Options{ - ReqBody: &struct { - RevertResize *int `json:"revertResize"` - }{nil}, - OkCodes: []int{202}, - MoreHeaders: map[string]string{ - "X-Auth-Token": gsp.access.AuthToken(), - }, - }) - }) - return err -} - -// See the CloudServersProvider interface for details. -func (gsp *genericServersProvider) ConfirmResize(id string) error { - err := gsp.context.WithReauth(gsp.access, func() error { - url := fmt.Sprintf("%s/servers/%s/action", gsp.endpoint, id) - return perigee.Post(url, perigee.Options{ - ReqBody: &struct { - ConfirmResize *int `json:"confirmResize"` - }{nil}, - OkCodes: []int{204}, - MoreHeaders: map[string]string{ - "X-Auth-Token": gsp.access.AuthToken(), - }, - }) - }) - return err -} - -// See the CloudServersProvider interface for details -func (gsp *genericServersProvider) RebootServer(id string, hard bool) error { - return gsp.context.WithReauth(gsp.access, func() error { - url := fmt.Sprintf("%s/servers/%s/action", gsp.endpoint, id) - types := map[bool]string{false: "SOFT", true: "HARD"} - return perigee.Post(url, perigee.Options{ - ReqBody: &struct { - Reboot struct { - Type string `json:"type"` - } `json:"reboot"` - }{ - struct { - Type string `json:"type"` - }{types[hard]}, - }, - OkCodes: []int{202}, - MoreHeaders: map[string]string{ - "X-Auth-Token": gsp.access.AuthToken(), - }, - }) - }) -} - -// See the CloudServersProvider interface for details -func (gsp *genericServersProvider) RescueServer(id string) (string, error) { - var pw *string - - err := gsp.context.WithReauth(gsp.access, func() error { - url := fmt.Sprintf("%s/servers/%s/action", gsp.endpoint, id) - return perigee.Post(url, perigee.Options{ - ReqBody: &struct { - Rescue string `json:"rescue"` - }{"none"}, - MoreHeaders: map[string]string{ - "X-Auth-Token": gsp.access.AuthToken(), - }, - Results: &struct { - AdminPass **string `json:"adminPass"` - }{&pw}, - }) - }) - return *pw, err -} - -// See the CloudServersProvider interface for details -func (gsp *genericServersProvider) UnrescueServer(id string) error { - return gsp.context.WithReauth(gsp.access, func() error { - url := fmt.Sprintf("%s/servers/%s/action", gsp.endpoint, id) - return perigee.Post(url, perigee.Options{ - ReqBody: &struct { - Unrescue *int `json:"unrescue"` - }{nil}, - MoreHeaders: map[string]string{ - "X-Auth-Token": gsp.access.AuthToken(), - }, - OkCodes: []int{202}, - }) - }) -} - -// See the CloudServersProvider interface for details -func (gsp *genericServersProvider) UpdateServer(id string, changes NewServerSettings) (*Server, error) { - var svr *Server - err := gsp.context.WithReauth(gsp.access, func() error { - url := fmt.Sprintf("%s/servers/%s", gsp.endpoint, id) - return perigee.Put(url, perigee.Options{ - ReqBody: &struct { - Server NewServerSettings `json:"server"` - }{changes}, - MoreHeaders: map[string]string{ - "X-Auth-Token": gsp.access.AuthToken(), - }, - Results: &struct { - Server **Server `json:"server"` - }{&svr}, - }) - }) - return svr, err -} - -// See the CloudServersProvider interface for details. -func (gsp *genericServersProvider) RebuildServer(id string, ns NewServer) (*Server, error) { - var s *Server - - err := gsp.context.WithReauth(gsp.access, func() error { - ep := fmt.Sprintf("%s/servers/%s/action", gsp.endpoint, id) - return perigee.Post(ep, perigee.Options{ - ReqBody: &struct { - Rebuild *NewServer `json:"rebuild"` - }{&ns}, - Results: &struct{ Server **Server }{&s}, - MoreHeaders: map[string]string{ - "X-Auth-Token": gsp.access.AuthToken(), - }, - OkCodes: []int{202}, - }) - }) - - return s, err -} - -// See the CloudServersProvider interface for details. -func (gsp *genericServersProvider) ListAddresses(id string) (AddressSet, error) { - var pas *AddressSet - var statusCode int - - err := gsp.context.WithReauth(gsp.access, func() error { - ep := fmt.Sprintf("%s/servers/%s/ips", gsp.endpoint, id) - return perigee.Get(ep, perigee.Options{ - Results: &struct{ Addresses **AddressSet }{&pas}, - MoreHeaders: map[string]string{ - "X-Auth-Token": gsp.access.AuthToken(), - }, - OkCodes: []int{200, 203}, - StatusCode: &statusCode, - }) - }) - - if err != nil { - if statusCode == 203 { - err = WarnUnauthoritative - } - } - - return *pas, err -} - -// See the CloudServersProvider interface for details. -func (gsp *genericServersProvider) ListAddressesByNetwork(id, networkLabel string) (NetworkAddress, error) { - pas := make(NetworkAddress) - var statusCode int - - err := gsp.context.WithReauth(gsp.access, func() error { - ep := fmt.Sprintf("%s/servers/%s/ips/%s", gsp.endpoint, id, networkLabel) - return perigee.Get(ep, perigee.Options{ - Results: &pas, - MoreHeaders: map[string]string{ - "X-Auth-Token": gsp.access.AuthToken(), - }, - OkCodes: []int{200, 203}, - StatusCode: &statusCode, - }) - }) - - if err != nil { - if statusCode == 203 { - err = WarnUnauthoritative - } - } - - return pas, err -} - -// See the CloudServersProvider interface for details. -func (gsp *genericServersProvider) CreateImage(id string, ci CreateImage) (string, error) { - response, err := gsp.context.ResponseWithReauth(gsp.access, func() (*perigee.Response, error) { - ep := fmt.Sprintf("%s/servers/%s/action", gsp.endpoint, id) - return perigee.Request("POST", ep, perigee.Options{ - ReqBody: &struct { - CreateImage *CreateImage `json:"createImage"` - }{&ci}, - MoreHeaders: map[string]string{ - "X-Auth-Token": gsp.access.AuthToken(), - }, - OkCodes: []int{200, 202}, - }) - }) - - if err != nil { - return "", err - } - location, err := response.HttpResponse.Location() - if err != nil { - return "", err - } - - // Return the last element of the location which is the image id - locationArr := strings.Split(location.Path, "/") - return locationArr[len(locationArr)-1], err -} - -// See the CloudServersProvider interface for details. -func (gsp *genericServersProvider) ListSecurityGroups() ([]SecurityGroup, error) { - var sgs []SecurityGroup - - err := gsp.context.WithReauth(gsp.access, func() error { - ep := fmt.Sprintf("%s/os-security-groups", gsp.endpoint) - return perigee.Get(ep, perigee.Options{ - MoreHeaders: map[string]string{ - "X-Auth-Token": gsp.access.AuthToken(), - }, - Results: &struct { - SecurityGroups *[]SecurityGroup `json:"security_groups"` - }{&sgs}, - }) - }) - return sgs, err -} - -// See the CloudServersProvider interface for details. -func (gsp *genericServersProvider) CreateSecurityGroup(desired SecurityGroup) (*SecurityGroup, error) { - var actual *SecurityGroup - - err := gsp.context.WithReauth(gsp.access, func() error { - ep := fmt.Sprintf("%s/os-security-groups", gsp.endpoint) - return perigee.Post(ep, perigee.Options{ - ReqBody: struct { - AddSecurityGroup SecurityGroup `json:"security_group"` - }{desired}, - MoreHeaders: map[string]string{ - "X-Auth-Token": gsp.access.AuthToken(), - }, - Results: &struct { - SecurityGroup **SecurityGroup `json:"security_group"` - }{&actual}, - }) - }) - return actual, err -} - -// See the CloudServersProvider interface for details. -func (gsp *genericServersProvider) ListSecurityGroupsByServerId(id string) ([]SecurityGroup, error) { - var sgs []SecurityGroup - - err := gsp.context.WithReauth(gsp.access, func() error { - ep := fmt.Sprintf("%s/servers/%s/os-security-groups", gsp.endpoint, id) - return perigee.Get(ep, perigee.Options{ - MoreHeaders: map[string]string{ - "X-Auth-Token": gsp.access.AuthToken(), - }, - Results: &struct { - SecurityGroups *[]SecurityGroup `json:"security_groups"` - }{&sgs}, - }) - }) - return sgs, err -} - -// See the CloudServersProvider interface for details. -func (gsp *genericServersProvider) SecurityGroupById(id int) (*SecurityGroup, error) { - var actual *SecurityGroup - - err := gsp.context.WithReauth(gsp.access, func() error { - ep := fmt.Sprintf("%s/os-security-groups/%d", gsp.endpoint, id) - return perigee.Get(ep, perigee.Options{ - MoreHeaders: map[string]string{ - "X-Auth-Token": gsp.access.AuthToken(), - }, - Results: &struct { - SecurityGroup **SecurityGroup `json:"security_group"` - }{&actual}, - }) - }) - return actual, err -} - -// See the CloudServersProvider interface for details. -func (gsp *genericServersProvider) DeleteSecurityGroupById(id int) error { - err := gsp.context.WithReauth(gsp.access, func() error { - ep := fmt.Sprintf("%s/os-security-groups/%d", gsp.endpoint, id) - return perigee.Delete(ep, perigee.Options{ - MoreHeaders: map[string]string{ - "X-Auth-Token": gsp.access.AuthToken(), - }, - OkCodes: []int{202}, - }) - }) - return err -} - -// See the CloudServersProvider interface for details. -func (gsp *genericServersProvider) ListDefaultSGRules() ([]SGRule, error) { - var sgrs []SGRule - err := gsp.context.WithReauth(gsp.access, func() error { - ep := fmt.Sprintf("%s/os-security-group-default-rules", gsp.endpoint) - return perigee.Get(ep, perigee.Options{ - MoreHeaders: map[string]string{ - "X-Auth-Token": gsp.access.AuthToken(), - }, - Results: &struct{ Security_group_default_rules *[]SGRule }{&sgrs}, - }) - }) - return sgrs, err -} - -// See the CloudServersProvider interface for details. -func (gsp *genericServersProvider) CreateDefaultSGRule(r SGRule) (*SGRule, error) { - var sgr *SGRule - err := gsp.context.WithReauth(gsp.access, func() error { - ep := fmt.Sprintf("%s/os-security-group-default-rules", gsp.endpoint) - return perigee.Post(ep, perigee.Options{ - MoreHeaders: map[string]string{ - "X-Auth-Token": gsp.access.AuthToken(), - }, - Results: &struct{ Security_group_default_rule **SGRule }{&sgr}, - ReqBody: struct { - Security_group_default_rule SGRule `json:"security_group_default_rule"` - }{r}, - }) - }) - return sgr, err -} - -// See the CloudServersProvider interface for details. -func (gsp *genericServersProvider) GetSGRule(id string) (*SGRule, error) { - var sgr *SGRule - err := gsp.context.WithReauth(gsp.access, func() error { - ep := fmt.Sprintf("%s/os-security-group-default-rules/%s", gsp.endpoint, id) - return perigee.Get(ep, perigee.Options{ - MoreHeaders: map[string]string{ - "X-Auth-Token": gsp.access.AuthToken(), - }, - Results: &struct{ Security_group_default_rule **SGRule }{&sgr}, - }) - }) - return sgr, err -} - -// SecurityGroup provides a description of a security group, including all its rules. -type SecurityGroup struct { - Description string `json:"description,omitempty"` - Id int `json:"id,omitempty"` - Name string `json:"name,omitempty"` - Rules []SGRule `json:"rules,omitempty"` - TenantId string `json:"tenant_id,omitempty"` -} - -// SGRule encapsulates a single rule which applies to a security group. -// This definition is just a guess, based on the documentation found in another extension here: http://docs.openstack.org/api/openstack-compute/2/content/GET_os-security-group-default-rules-v2_listSecGroupDefaultRules_v2__tenant_id__os-security-group-rules_ext-os-security-group-default-rules.html -type SGRule struct { - FromPort int `json:"from_port,omitempty"` - Id int `json:"id,omitempty"` - IpProtocol string `json:"ip_protocol,omitempty"` - IpRange map[string]interface{} `json:"ip_range,omitempty"` - ToPort int `json:"to_port,omitempty"` -} - -// RaxBandwidth provides measurement of server bandwidth consumed over a given audit interval. -type RaxBandwidth struct { - AuditPeriodEnd string `json:"audit_period_end"` - AuditPeriodStart string `json:"audit_period_start"` - BandwidthInbound int64 `json:"bandwidth_inbound"` - BandwidthOutbound int64 `json:"bandwidth_outbound"` - Interface string `json:"interface"` -} - -// A VersionedAddress denotes either an IPv4 or IPv6 (depending on version indicated) -// address. -type VersionedAddress struct { - Addr string `json:"addr"` - Version int `json:"version"` -} - -// An AddressSet provides a set of public and private IP addresses for a resource. -// Each address has a version to identify if IPv4 or IPv6. -type AddressSet struct { - Public []VersionedAddress `json:"public"` - Private []VersionedAddress `json:"private"` -} - -type NetworkAddress map[string][]VersionedAddress - -// Server records represent (virtual) hardware instances (not configurations) accessible by the user. -// -// The AccessIPv4 / AccessIPv6 fields provides IP addresses for the server in the IPv4 or IPv6 format, respectively. -// -// Addresses provides addresses for any attached isolated networks. -// The version field indicates whether the IP address is version 4 or 6. -// Note: only public and private pools appear here. -// To get the complete set, use the AllAddressPools() method instead. -// -// Created tells when the server entity was created. -// -// The Flavor field includes the flavor ID and flavor links. -// -// The compute provisioning algorithm has an anti-affinity property that -// attempts to spread customer VMs across hosts. -// Under certain situations, -// VMs from the same customer might be placed on the same host. -// The HostId field represents the host your server runs on and -// can be used to determine this scenario if it is relevant to your application. -// Note that HostId is unique only per account; it is not globally unique. -// -// Id provides the server's unique identifier. -// This field must be treated opaquely. -// -// Image indicates which image is installed on the server. -// -// Links provides one or more means of accessing the server. -// -// Metadata provides a small key-value store for application-specific information. -// -// Name provides a human-readable name for the server. -// -// Progress indicates how far along it is towards being provisioned. -// 100 represents complete, while 0 represents just beginning. -// -// Status provides an indication of what the server's doing at the moment. -// A server will be in ACTIVE state if it's ready for use. -// -// OsDcfDiskConfig indicates the server's boot volume configuration. -// Valid values are: -// AUTO -// ---- -// The server is built with a single partition the size of the target flavor disk. -// The file system is automatically adjusted to fit the entire partition. -// This keeps things simple and automated. -// AUTO is valid only for images and servers with a single partition that use the EXT3 file system. -// This is the default setting for applicable Rackspace base images. -// -// MANUAL -// ------ -// The server is built using whatever partition scheme and file system is in the source image. -// If the target flavor disk is larger, -// the remaining disk space is left unpartitioned. -// This enables images to have non-EXT3 file systems, multiple partitions, and so on, -// and enables you to manage the disk configuration. -// -// RaxBandwidth provides measures of the server's inbound and outbound bandwidth per interface. -// -// OsExtStsPowerState provides an indication of the server's power. -// This field appears to be a set of flag bits: -// -// ... 4 3 2 1 0 -// +--//--+---+---+---+---+ -// | .... | 0 | S | 0 | I | -// +--//--+---+---+---+---+ -// | | -// | +--- 0=Instance is down. -// | 1=Instance is up. -// | -// +----------- 0=Server is switched ON. -// 1=Server is switched OFF. -// (note reverse logic.) -// -// Unused bits should be ignored when read, and written as 0 for future compatibility. -// -// OsExtStsTaskState and OsExtStsVmState work together -// to provide visibility in the provisioning process for the instance. -// Consult Rackspace documentation at -// http://docs.rackspace.com/servers/api/v2/cs-devguide/content/ch_extensions.html#ext_status -// for more details. It's too lengthy to include here. -type Server struct { - AccessIPv4 string `json:"accessIPv4"` - AccessIPv6 string `json:"accessIPv6"` - Addresses AddressSet - Created string `json:"created"` - Flavor FlavorLink `json:"flavor"` - HostId string `json:"hostId"` - Id string `json:"id"` - Image ImageLink `json:"image"` - Links []Link `json:"links"` - Metadata map[string]string `json:"metadata"` - Name string `json:"name"` - Progress int `json:"progress"` - Status string `json:"status"` - TenantId string `json:"tenant_id"` - Updated string `json:"updated"` - UserId string `json:"user_id"` - OsDcfDiskConfig string `json:"OS-DCF:diskConfig"` - RaxBandwidth []RaxBandwidth `json:"rax-bandwidth:bandwidth"` - OsExtStsPowerState int `json:"OS-EXT-STS:power_state"` - OsExtStsTaskState string `json:"OS-EXT-STS:task_state"` - OsExtStsVmState string `json:"OS-EXT-STS:vm_state"` - - RawAddresses map[string]interface{} `json:"addresses"` -} - -// AllAddressPools returns a complete set of address pools available on the server. -// The name of each pool supported keys the map. -// The value of the map contains the addresses provided in the corresponding pool. -func (s *Server) AllAddressPools() (map[string][]VersionedAddress, error) { - pools := make(map[string][]VersionedAddress, 0) - for pool, subtree := range s.RawAddresses { - addresses := make([]VersionedAddress, 0) - err := mapstructure.Decode(subtree, &addresses) - if err != nil { - return nil, err - } - pools[pool] = addresses - } - return pools, nil -} - -// NewServerSettings structures record those fields of the Server structure to change -// when updating a server (see UpdateServer method). -type NewServerSettings struct { - Name string `json:"name,omitempty"` - AccessIPv4 string `json:"accessIPv4,omitempty"` - AccessIPv6 string `json:"accessIPv6,omitempty"` -} - -// NewServer structures are used for both requests and responses. -// The fields discussed below are relevent for server-creation purposes. -// -// The Name field contains the desired name of the server. -// Note that (at present) Rackspace permits more than one server with the same name; -// however, software should not depend on this. -// Not only will Rackspace support thank you, so will your own devops engineers. -// A name is required. -// -// The ImageRef field contains the ID of the desired software image to place on the server. -// This ID must be found in the image slice returned by the Images() function. -// This field is required. -// -// The FlavorRef field contains the ID of the server configuration desired for deployment. -// This ID must be found in the flavor slice returned by the Flavors() function. -// This field is required. -// -// For OsDcfDiskConfig, refer to the Image or Server structure documentation. -// This field defaults to "AUTO" if not explicitly provided. -// -// Metadata contains a small key/value association of arbitrary data. -// Neither Rackspace nor OpenStack places significance on this field in any way. -// This field defaults to an empty map if not provided. -// -// Personality specifies the contents of certain files in the server's filesystem. -// The files and their contents are mapped through a slice of FileConfig structures. -// If not provided, all filesystem entities retain their image-specific configuration. -// -// Networks specifies an affinity for the server's various networks and interfaces. -// Networks are identified through UUIDs; see NetworkConfig structure documentation for more details. -// If not provided, network affinity is determined automatically. -// -// The AdminPass field may be used to provide a root- or administrator-password -// during the server provisioning process. -// If not provided, a random password will be automatically generated and returned in this field. -// -// The following fields are intended to be used to communicate certain results about the server being provisioned. -// When attempting to create a new server, these fields MUST not be provided. -// They'll be filled in by the response received from the Rackspace APIs. -// -// The Id field contains the server's unique identifier. -// The identifier's scope is best assumed to be bound by the user's account, unless other arrangements have been made with Rackspace. -// -// The SecurityGroup field allows the user to specify a security group at launch. -// -// Any Links provided are used to refer to the server specifically by URL. -// These links are useful for making additional REST calls not explicitly supported by Gorax. -type NewServer struct { - Name string `json:"name,omitempty"` - ImageRef string `json:"imageRef,omitempty"` - FlavorRef string `json:"flavorRef,omitempty"` - Metadata map[string]string `json:"metadata,omitempty"` - Personality []FileConfig `json:"personality,omitempty"` - Networks []NetworkConfig `json:"networks,omitempty"` - AdminPass string `json:"adminPass,omitempty"` - KeyPairName string `json:"key_name,omitempty"` - Id string `json:"id,omitempty"` - Links []Link `json:"links,omitempty"` - OsDcfDiskConfig string `json:"OS-DCF:diskConfig,omitempty"` - SecurityGroup []map[string]interface{} `json:"security_groups,omitempty"` - ConfigDrive bool `json:"config_drive"` - UserData string `json:"user_data"` -} - -// ResizeRequest structures are used internally to encode to JSON the parameters required to resize a server instance. -// Client applications will not use this structure (no API accepts an instance of this structure). -// See the Region method ResizeServer() for more details on how to resize a server. -type ResizeRequest struct { - Name string `json:"name,omitempty"` - FlavorRef string `json:"flavorRef"` - DiskConfig string `json:"OS-DCF:diskConfig,omitempty"` -} - -type CreateImage struct { - Name string `json:"name"` - Metadata map[string]string `json:"metadata,omitempty"` -} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/servers_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/servers_test.go deleted file mode 100644 index 60c71c889fb..00000000000 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/servers_test.go +++ /dev/null @@ -1,54 +0,0 @@ -package gophercloud - -import ( - "net/http" - "testing" -) - -type testAccess struct { - public, internal string - calledFirstEndpointByCriteria int -} - -func (ta *testAccess) FirstEndpointUrlByCriteria(ac ApiCriteria) string { - ta.calledFirstEndpointByCriteria++ - urls := []string{ta.public, ta.internal} - return urls[ac.UrlChoice] -} - -func (ta *testAccess) AuthToken() string { - return "" -} - -func (ta *testAccess) Revoke(string) error { - return nil -} - -func (ta *testAccess) Reauthenticate() error { - return nil -} - -func TestGetServersApi(t *testing.T) { - c := TestContext().UseCustomClient(&http.Client{Transport: newTransport().WithResponse("Hello")}) - - acc := &testAccess{ - public: "http://localhost:8080", - internal: "http://localhost:8086", - } - - _, err := c.ServersApi(acc, ApiCriteria{ - Name: "cloudComputeOpenStack", - Region: "dfw", - VersionId: "2", - }) - - if err != nil { - t.Error(err) - return - } - - if acc.calledFirstEndpointByCriteria != 1 { - t.Error("Expected FirstEndpointByCriteria to be called") - return - } -} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/service_catalog.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/service_catalog.go deleted file mode 100644 index e6cf4a00eae..00000000000 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/service_catalog.go +++ /dev/null @@ -1,75 +0,0 @@ -package gophercloud - -import ( - "os" - "strings" -) - -// ApiCriteria provides one or more criteria for the SDK to look for appropriate endpoints. -// Fields left unspecified or otherwise set to their zero-values are assumed to not be -// relevant, and do not participate in the endpoint search. -// -// Name specifies the desired service catalog entry name. -// Type specifies the desired service catalog entry type. -// Region specifies the desired endpoint region. -// If unset, Gophercloud will try to use the region set in the -// OS_REGION_NAME environment variable. If that's not set, -// region comparison will not occur. If OS_REGION_NAME is set -// and IgnoreEnvVars is also set, OS_REGION_NAME will be ignored. -// VersionId specifies the desired version of the endpoint. -// Note that this field is matched exactly, and is (at present) -// opaque to Gophercloud. Thus, requesting a version 2 -// endpoint will _not_ match a version 3 endpoint. -// The UrlChoice field inidicates whether or not gophercloud -// should use the public or internal endpoint URL if a -// candidate endpoint is found. -// IgnoreEnvVars instructs Gophercloud to ignore helpful environment variables. -type ApiCriteria struct { - Name string - Type string - Region string - VersionId string - UrlChoice int - IgnoreEnvVars bool -} - -// The choices available for UrlChoice. See the ApiCriteria structure for details. -const ( - PublicURL = iota - InternalURL -) - -// Given a set of criteria to match on, locate the first candidate endpoint -// in the provided service catalog. -// -// If nothing found, the result will be a zero-valued EntryEndpoint (all URLs -// set to ""). -func FindFirstEndpointByCriteria(entries []CatalogEntry, ac ApiCriteria) EntryEndpoint { - rgn := strings.ToUpper(ac.Region) - if (rgn == "") && !ac.IgnoreEnvVars { - rgn = os.Getenv("OS_REGION_NAME") - } - - for _, entry := range entries { - if (ac.Name != "") && (ac.Name != entry.Name) { - continue - } - - if (ac.Type != "") && (ac.Type != entry.Type) { - continue - } - - for _, endpoint := range entry.Endpoints { - if (rgn != "") && (rgn != strings.ToUpper(endpoint.Region)) { - continue - } - - if (ac.VersionId != "") && (ac.VersionId != endpoint.VersionId) { - continue - } - - return endpoint - } - } - return EntryEndpoint{} -} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/service_catalog_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/service_catalog_test.go deleted file mode 100644 index b78f01fced9..00000000000 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/service_catalog_test.go +++ /dev/null @@ -1,190 +0,0 @@ -package gophercloud - -import ( - "os" - "testing" -) - -// TestFFEBCViaEnvVariable exercises only those calls where a region -// parameter is required, but is provided by an environment variable. -func TestFFEBCViaEnvVariable(t *testing.T) { - changeRegion("RGN") - - endpoint := FindFirstEndpointByCriteria( - catalog("test", "compute", "http://localhost", "", ""), - ApiCriteria{Name: "test"}, - ) - if endpoint.PublicURL != "" { - t.Error("If provided, the Region qualifier must exclude endpoints with missing or mismatching regions.") - return - } - - endpoint = FindFirstEndpointByCriteria( - catalog("test", "compute", "http://localhost", "rgn", ""), - ApiCriteria{Name: "test"}, - ) - if endpoint.PublicURL != "http://localhost" { - t.Error("Regions are case insensitive.") - return - } - - endpoint = FindFirstEndpointByCriteria( - catalog("test", "compute", "http://localhost", "rgn", ""), - ApiCriteria{Name: "test", VersionId: "2"}, - ) - if endpoint.PublicURL != "" { - t.Error("Missing version ID means no match.") - return - } - - endpoint = FindFirstEndpointByCriteria( - catalog("test", "compute", "http://localhost", "rgn", "3"), - ApiCriteria{Name: "test", VersionId: "2"}, - ) - if endpoint.PublicURL != "" { - t.Error("Mismatched version ID means no match.") - return - } - - endpoint = FindFirstEndpointByCriteria( - catalog("test", "compute", "http://localhost", "rgn", "2"), - ApiCriteria{Name: "test", VersionId: "2"}, - ) - if endpoint.PublicURL != "http://localhost" { - t.Error("All search criteria met; endpoint expected.") - return - } -} - -// TestFFEBCViaRegionOption exercises only those calls where a region -// parameter is specified explicitly. The region option overrides -// any defined OS_REGION_NAME environment setting. -func TestFFEBCViaRegionOption(t *testing.T) { - changeRegion("Starfleet Command") - - endpoint := FindFirstEndpointByCriteria( - catalog("test", "compute", "http://localhost", "", ""), - ApiCriteria{Name: "test", Region: "RGN"}, - ) - if endpoint.PublicURL != "" { - t.Error("If provided, the Region qualifier must exclude endpoints with missing or mismatching regions.") - return - } - - endpoint = FindFirstEndpointByCriteria( - catalog("test", "compute", "http://localhost", "rgn", ""), - ApiCriteria{Name: "test", Region: "RGN"}, - ) - if endpoint.PublicURL != "http://localhost" { - t.Error("Regions are case insensitive.") - return - } - - endpoint = FindFirstEndpointByCriteria( - catalog("test", "compute", "http://localhost", "rgn", ""), - ApiCriteria{Name: "test", Region: "RGN", VersionId: "2"}, - ) - if endpoint.PublicURL != "" { - t.Error("Missing version ID means no match.") - return - } - - endpoint = FindFirstEndpointByCriteria( - catalog("test", "compute", "http://localhost", "rgn", "3"), - ApiCriteria{Name: "test", Region: "RGN", VersionId: "2"}, - ) - if endpoint.PublicURL != "" { - t.Error("Mismatched version ID means no match.") - return - } - - endpoint = FindFirstEndpointByCriteria( - catalog("test", "compute", "http://localhost", "rgn", "2"), - ApiCriteria{Name: "test", Region: "RGN", VersionId: "2"}, - ) - if endpoint.PublicURL != "http://localhost" { - t.Error("All search criteria met; endpoint expected.") - return - } -} - -// TestFFEBCWithoutRegion exercises only those calls where a region -// is irrelevant. Just to make sure, though, we enforce Gophercloud -// from paying any attention to OS_REGION_NAME if it happens to be set. -func TestFindFirstEndpointByCriteria(t *testing.T) { - endpoint := FindFirstEndpointByCriteria([]CatalogEntry{}, ApiCriteria{Name: "test", IgnoreEnvVars: true}) - if endpoint.PublicURL != "" { - t.Error("Not expecting to find anything in an empty service catalog.") - return - } - - endpoint = FindFirstEndpointByCriteria( - []CatalogEntry{ - {Name: "test"}, - }, - ApiCriteria{Name: "test", IgnoreEnvVars: true}, - ) - if endpoint.PublicURL != "" { - t.Error("Even though we have a matching entry, no endpoints exist") - return - } - - endpoint = FindFirstEndpointByCriteria( - catalog("test", "compute", "http://localhost", "", ""), - ApiCriteria{Name: "test", IgnoreEnvVars: true}, - ) - if endpoint.PublicURL != "http://localhost" { - t.Error("Looking for an endpoint by name but without region or version ID should match first entry endpoint.") - return - } - - endpoint = FindFirstEndpointByCriteria( - catalog("test", "compute", "http://localhost", "", ""), - ApiCriteria{Type: "compute", IgnoreEnvVars: true}, - ) - if endpoint.PublicURL != "http://localhost" { - t.Error("Looking for an endpoint by type but without region or version ID should match first entry endpoint.") - return - } - - endpoint = FindFirstEndpointByCriteria( - catalog("test", "compute", "http://localhost", "", ""), - ApiCriteria{Type: "identity", IgnoreEnvVars: true}, - ) - if endpoint.PublicURL != "" { - t.Error("Returned mismatched type.") - return - } - - endpoint = FindFirstEndpointByCriteria( - catalog("test", "compute", "http://localhost", "ord", "2"), - ApiCriteria{Name: "test", VersionId: "2", IgnoreEnvVars: true}, - ) - if endpoint.PublicURL != "http://localhost" { - t.Error("Sometimes, you might not care what region your stuff is in.") - return - } -} - -func catalog(name, entry_type, url, region, version string) []CatalogEntry { - return []CatalogEntry{ - { - Name: name, - Type: entry_type, - Endpoints: []EntryEndpoint{ - { - PublicURL: url, - Region: region, - VersionId: version, - }, - }, - }, - } -} - -func changeRegion(r string) { - err := os.Setenv("OS_REGION_NAME", r) - if err != nil { - panic(err) - } -} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/service_client.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/service_client.go new file mode 100644 index 00000000000..3490da05f27 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/service_client.go @@ -0,0 +1,32 @@ +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 { + // ProviderClient is a reference to the provider that implements this service. + *ProviderClient + + // Endpoint is the base URL of the service's API, acquired from a service catalog. + // It MUST end with a /. + Endpoint string + + // ResourceBase is the base URL shared by the resources within a service's API. It should include + // the API version and, like Endpoint, MUST end with a / if set. If not set, the Endpoint is used + // as-is, instead. + ResourceBase string +} + +// 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, "/") +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/service_client_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/service_client_test.go new file mode 100644 index 00000000000..84beb3f7681 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/testhelper/client/fake.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/testhelper/client/fake.go new file mode 100644 index 00000000000..5b69b058f1f --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/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{ + ProviderClient: &gophercloud.ProviderClient{TokenID: TokenID}, + Endpoint: testhelper.Endpoint(), + } +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/testhelper/convenience.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/testhelper/convenience.go new file mode 100644 index 00000000000..cf33e1ad1a6 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/testhelper/convenience.go @@ -0,0 +1,329 @@ +package testhelper + +import ( + "encoding/json" + "fmt" + "path/filepath" + "reflect" + "runtime" + "strings" + "testing" +) + +const ( + logBodyFmt = "\033[1;31m%s %s\033[0m" + greenCode = "\033[0m\033[1;32m" + yellowCode = "\033[0m\033[1;33m" + resetCode = "\033[0m\033[1;31m" +) + +func prefix(depth int) string { + _, file, line, _ := runtime.Caller(depth) + return fmt.Sprintf("Failure in %s, line %d:", filepath.Base(file), line) +} + +func green(str interface{}) string { + return fmt.Sprintf("%s%#v%s", greenCode, str, resetCode) +} + +func yellow(str interface{}) string { + return fmt.Sprintf("%s%#v%s", yellowCode, str, resetCode) +} + +func logFatal(t *testing.T, str string) { + t.Fatalf(logBodyFmt, prefix(3), str) +} + +func logError(t *testing.T, str string) { + t.Errorf(logBodyFmt, prefix(3), str) +} + +type diffLogger func([]string, interface{}, interface{}) + +type visit struct { + a1 uintptr + a2 uintptr + typ reflect.Type +} + +// Recursively visits the structures of "expected" and "actual". The diffLogger function will be +// invoked with each different value encountered, including the reference path that was followed +// to get there. +func deepDiffEqual(expected, actual reflect.Value, visited map[visit]bool, path []string, logDifference diffLogger) { + defer func() { + // Fall back to the regular reflect.DeepEquals function. + if r := recover(); r != nil { + var e, a interface{} + if expected.IsValid() { + e = expected.Interface() + } + if actual.IsValid() { + a = actual.Interface() + } + + if !reflect.DeepEqual(e, a) { + logDifference(path, e, a) + } + } + }() + + if !expected.IsValid() && actual.IsValid() { + logDifference(path, nil, actual.Interface()) + return + } + if expected.IsValid() && !actual.IsValid() { + logDifference(path, expected.Interface(), nil) + return + } + if !expected.IsValid() && !actual.IsValid() { + return + } + + hard := func(k reflect.Kind) bool { + switch k { + case reflect.Array, reflect.Map, reflect.Slice, reflect.Struct: + return true + } + return false + } + + if expected.CanAddr() && actual.CanAddr() && hard(expected.Kind()) { + addr1 := expected.UnsafeAddr() + addr2 := actual.UnsafeAddr() + + if addr1 > addr2 { + addr1, addr2 = addr2, addr1 + } + + if addr1 == addr2 { + // References are identical. We can short-circuit + return + } + + typ := expected.Type() + v := visit{addr1, addr2, typ} + if visited[v] { + // Already visited. + return + } + + // Remember this visit for later. + visited[v] = true + } + + switch expected.Kind() { + case reflect.Array: + for i := 0; i < expected.Len(); i++ { + hop := append(path, fmt.Sprintf("[%d]", i)) + deepDiffEqual(expected.Index(i), actual.Index(i), visited, hop, logDifference) + } + return + case reflect.Slice: + if expected.IsNil() != actual.IsNil() { + logDifference(path, expected.Interface(), actual.Interface()) + return + } + if expected.Len() == actual.Len() && expected.Pointer() == actual.Pointer() { + return + } + for i := 0; i < expected.Len(); i++ { + hop := append(path, fmt.Sprintf("[%d]", i)) + deepDiffEqual(expected.Index(i), actual.Index(i), visited, hop, logDifference) + } + return + case reflect.Interface: + if expected.IsNil() != actual.IsNil() { + logDifference(path, expected.Interface(), actual.Interface()) + return + } + deepDiffEqual(expected.Elem(), actual.Elem(), visited, path, logDifference) + return + case reflect.Ptr: + deepDiffEqual(expected.Elem(), actual.Elem(), visited, path, logDifference) + return + case reflect.Struct: + for i, n := 0, expected.NumField(); i < n; i++ { + field := expected.Type().Field(i) + hop := append(path, "."+field.Name) + deepDiffEqual(expected.Field(i), actual.Field(i), visited, hop, logDifference) + } + return + case reflect.Map: + if expected.IsNil() != actual.IsNil() { + logDifference(path, expected.Interface(), actual.Interface()) + return + } + if expected.Len() == actual.Len() && expected.Pointer() == actual.Pointer() { + return + } + + var keys []reflect.Value + if expected.Len() >= actual.Len() { + keys = expected.MapKeys() + } else { + keys = actual.MapKeys() + } + + for _, k := range keys { + expectedValue := expected.MapIndex(k) + actualValue := expected.MapIndex(k) + + if !expectedValue.IsValid() { + logDifference(path, nil, actual.Interface()) + return + } + if !actualValue.IsValid() { + logDifference(path, expected.Interface(), nil) + return + } + + hop := append(path, fmt.Sprintf("[%v]", k)) + deepDiffEqual(expectedValue, actualValue, visited, hop, logDifference) + } + return + case reflect.Func: + if expected.IsNil() != actual.IsNil() { + logDifference(path, expected.Interface(), actual.Interface()) + } + return + default: + if expected.Interface() != actual.Interface() { + logDifference(path, expected.Interface(), actual.Interface()) + } + } +} + +func deepDiff(expected, actual interface{}, logDifference diffLogger) { + if expected == nil || actual == nil { + logDifference([]string{}, expected, actual) + return + } + + expectedValue := reflect.ValueOf(expected) + actualValue := reflect.ValueOf(actual) + + if expectedValue.Type() != actualValue.Type() { + logDifference([]string{}, expected, actual) + return + } + deepDiffEqual(expectedValue, actualValue, map[visit]bool{}, []string{}, logDifference) +} + +// 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{}) { + pre := prefix(2) + + differed := false + deepDiff(expected, actual, func(path []string, expected, actual interface{}) { + differed = true + t.Errorf("\033[1;31m%sat %s expected %s, but got %s\033[0m", + pre, + strings.Join(path, ""), + green(expected), + yellow(actual)) + }) + if differed { + logFatal(t, "The structures were different.") + } +} + +// CheckDeepEquals is similar to AssertDeepEquals, except with a non-fatal error +func CheckDeepEquals(t *testing.T, expected, actual interface{}) { + pre := prefix(2) + + deepDiff(expected, actual, func(path []string, expected, actual interface{}) { + t.Errorf("\033[1;31m%s at %s expected %s, but got %s\033[0m", + pre, + strings.Join(path, ""), + green(expected), + yellow(actual)) + }) +} + +// isJSONEquals is a utility function that implements JSON comparison for AssertJSONEquals and +// CheckJSONEquals. +func isJSONEquals(t *testing.T, expectedJSON string, actual interface{}) bool { + var parsedExpected, parsedActual interface{} + err := json.Unmarshal([]byte(expectedJSON), &parsedExpected) + if err != nil { + t.Errorf("Unable to parse expected value as JSON: %v", err) + return false + } + + jsonActual, err := json.Marshal(actual) + AssertNoErr(t, err) + err = json.Unmarshal(jsonActual, &parsedActual) + AssertNoErr(t, err) + + if !reflect.DeepEqual(parsedExpected, parsedActual) { + prettyExpected, err := json.MarshalIndent(parsedExpected, "", " ") + if err != nil { + t.Logf("Unable to pretty-print expected JSON: %v\n%s", err, expectedJSON) + } else { + // We can't use green() here because %#v prints prettyExpected as a byte array literal, which + // is... unhelpful. Converting it to a string first leaves "\n" uninterpreted for some reason. + t.Logf("Expected JSON:\n%s%s%s", greenCode, prettyExpected, resetCode) + } + + prettyActual, err := json.MarshalIndent(actual, "", " ") + if err != nil { + t.Logf("Unable to pretty-print actual JSON: %v\n%#v", err, actual) + } else { + // We can't use yellow() for the same reason. + t.Logf("Actual JSON:\n%s%s%s", yellowCode, prettyActual, resetCode) + } + + return false + } + return true +} + +// AssertJSONEquals serializes a value as JSON, parses an expected string as JSON, and ensures that +// both are consistent. If they aren't, the expected and actual structures are pretty-printed and +// shown for comparison. +// +// This is useful for comparing structures that are built as nested map[string]interface{} values, +// which are a pain to construct as literals. +func AssertJSONEquals(t *testing.T, expectedJSON string, actual interface{}) { + if !isJSONEquals(t, expectedJSON, actual) { + logFatal(t, "The generated JSON structure differed.") + } +} + +// CheckJSONEquals is similar to AssertJSONEquals, but nonfatal. +func CheckJSONEquals(t *testing.T, expectedJSON string, actual interface{}) { + if !isJSONEquals(t, expectedJSON, actual) { + logError(t, "The generated JSON structure differed.") + } +} + +// 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/Godeps/_workspace/src/github.com/rackspace/gophercloud/testhelper/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/testhelper/doc.go new file mode 100644 index 00000000000..25b4dfebbbe --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/testhelper/doc.go @@ -0,0 +1,4 @@ +/* +Package testhelper container methods that are useful for writing unit tests. +*/ +package testhelper diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/testhelper/http_responses.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/testhelper/http_responses.go new file mode 100644 index 00000000000..e1f1f9ac0e8 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/testhelper/http_responses.go @@ -0,0 +1,91 @@ +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 actualJSON interface{} + err = json.Unmarshal(b, &actualJSON) + if err != nil { + t.Errorf("Unable to parse request body as JSON: %v", err) + } + + CheckJSONEquals(t, expected, actualJSON) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/transport_double_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/transport_double_test.go deleted file mode 100644 index ef7f19a5d8d..00000000000 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/transport_double_test.go +++ /dev/null @@ -1,103 +0,0 @@ -package gophercloud - -import ( - "encoding/json" - "fmt" - "io/ioutil" - "net/http" - "strings" - "testing" -) - -type transport struct { - called int - response string - expectTenantId bool - tenantIdFound bool - status int -} - -func (t *transport) RoundTrip(req *http.Request) (rsp *http.Response, err error) { - var authContainer *AuthContainer - - t.called++ - - headers := make(http.Header) - headers.Add("Content-Type", "application/xml; charset=UTF-8") - - body := ioutil.NopCloser(strings.NewReader(t.response)) - - if t.status == 0 { - t.status = 200 - } - statusMsg := "OK" - if (t.status < 200) || (299 < t.status) { - statusMsg = "Error" - } - - rsp = &http.Response{ - Status: fmt.Sprintf("%d %s", t.status, statusMsg), - StatusCode: t.status, - Proto: "HTTP/1.1", - ProtoMajor: 1, - ProtoMinor: 1, - Header: headers, - Body: body, - ContentLength: -1, - TransferEncoding: nil, - Close: true, - Trailer: nil, - Request: req, - } - - bytes, err := ioutil.ReadAll(req.Body) - if err != nil { - return nil, err - } - err = json.Unmarshal(bytes, &authContainer) - if err != nil { - return nil, err - } - t.tenantIdFound = (authContainer.Auth.TenantId != "") - - if t.tenantIdFound != t.expectTenantId { - rsp.Status = "500 Internal Server Error" - rsp.StatusCode = 500 - } - return -} - -func newTransport() *transport { - return &transport{} -} - -func (t *transport) IgnoreTenantId() *transport { - t.expectTenantId = false - return t -} - -func (t *transport) ExpectTenantId() *transport { - t.expectTenantId = true - return t -} - -func (t *transport) WithResponse(r string) *transport { - t.response = r - t.status = 200 - return t -} - -func (t *transport) WithError(code int) *transport { - t.response = fmt.Sprintf("Error %d", code) - t.status = code - return t -} - -func (t *transport) VerifyCalls(test *testing.T, n int) error { - if t.called != n { - err := fmt.Errorf("Expected Transport to be called %d times; found %d instead", n, t.called) - test.Error(err) - return err - } - return nil -} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/util.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/util.go new file mode 100644 index 00000000000..101fd3953d1 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/util.go @@ -0,0 +1,39 @@ +package gophercloud + +import ( + "errors" + "strings" + "time" +) + +// WaitFor polls a predicate function, once per second, up to a timeout limit. +// It usually does this to wait for the resource to transition to a certain state. +func WaitFor(timeout int, predicate func() (bool, error)) error { + start := time.Now().Second() + for { + // Force a 1s sleep + time.Sleep(1 * time.Second) + + // If a timeout is set, and that's been exceeded, shut it down + if timeout >= 0 && time.Now().Second()-start >= timeout { + return errors.New("A timeout occurred") + } + + // Execute the function + satisfied, err := predicate() + if err != nil { + return err + } + if satisfied { + return nil + } + } +} + +// 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/Godeps/_workspace/src/github.com/rackspace/gophercloud/util_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/util_test.go new file mode 100644 index 00000000000..5a15a005d35 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/util_test.go @@ -0,0 +1,14 @@ +package gophercloud + +import ( + "testing" + + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestWaitFor(t *testing.T) { + err := WaitFor(5, func() (bool, error) { + return true, nil + }) + th.CheckNoErr(t, err) +} diff --git a/cluster/saltbase/salt/cadvisor/cadvisor.manifest b/cluster/saltbase/salt/cadvisor/cadvisor.manifest index 1091eac7f20..d200fe20a79 100644 --- a/cluster/saltbase/salt/cadvisor/cadvisor.manifest +++ b/cluster/saltbase/salt/cadvisor/cadvisor.manifest @@ -26,7 +26,7 @@ volumes: source: hostDir: path: /var/lib/docker - - name: cgroups + - name: sysfs source: hostDir: - path: /sys/fs/cgroup + path: /sys diff --git a/cmd/kube-proxy/proxy.go b/cmd/kube-proxy/proxy.go index 62732afeb63..256dd7987c6 100644 --- a/cmd/kube-proxy/proxy.go +++ b/cmd/kube-proxy/proxy.go @@ -42,6 +42,7 @@ var ( bindAddress = util.IP(net.ParseIP("0.0.0.0")) clientConfig = &client.Config{} healthz_port = flag.Int("healthz_port", 10249, "The port to bind the health check server. Use 0 to disable.") + oomScoreAdj = flag.Int("oom_score_adj", -899, "The oom_score_adj value for kube-proxy process. Values must be within the range [-1000, 1000]") ) func init() { @@ -55,6 +56,10 @@ func main() { util.InitLogs() defer util.FlushLogs() + if err := util.ApplyOomScoreAdj(*oomScoreAdj); err != nil { + glog.Info(err) + } + verflag.PrintAndExitIfRequested() serviceConfig := config.NewServiceConfig() diff --git a/cmd/kubelet/kubelet.go b/cmd/kubelet/kubelet.go index 2c49b94a95e..d50a1dd4f05 100644 --- a/cmd/kubelet/kubelet.go +++ b/cmd/kubelet/kubelet.go @@ -61,6 +61,7 @@ var ( maxContainerCount = flag.Int("maximum_dead_containers_per_container", 5, "Maximum number of old instances of a container to retain per container. Each container takes up some disk space. Default: 5.") authPath = flag.String("auth_path", "", "Path to .kubernetes_auth file, specifying how to authenticate to API server.") cAdvisorPort = flag.Uint("cadvisor_port", 4194, "The port of the localhost cAdvisor endpoint") + oomScoreAdj = flag.Int("oom_score_adj", -900, "The oom_score_adj value for kubelet process. Values must be within the range [-1000, 1000]") apiServerList util.StringList ) @@ -92,6 +93,10 @@ func main() { setupRunOnce() + if err := util.ApplyOomScoreAdj(*oomScoreAdj); err != nil { + glog.Info(err) + } + kcfg := standalone.KubeletConfig{ Address: address, AuthPath: *authPath, diff --git a/hack/e2e-suite/liveness.sh b/hack/e2e-suite/liveness.sh index c70659cb16e..974002b50d4 100755 --- a/hack/e2e-suite/liveness.sh +++ b/hack/e2e-suite/liveness.sh @@ -64,11 +64,18 @@ for test in http exec; do waitForNotPending before=$(${KUBECFG} '-template={{.currentState.info.liveness.restartCount}}' get pods/liveness-${test}) + while [[ "${before}" == "" ]]; do + before=$(${KUBECFG} '-template={{.currentState.info.liveness.restartCount}}' get pods/liveness-${test}) + done + echo "Waiting for restarts." for i in $(seq 1 24); do sleep 10 after=$(${KUBECFG} '-template={{.currentState.info.liveness.restartCount}}' get pods/liveness-${test}) echo "Restarts: ${after} > ${before}" + if [[ "${after}" == "" ]]; then + continue + fi if [[ "${after}" > "${before}" ]]; then break fi diff --git a/hack/e2e-suite/update.sh b/hack/e2e-suite/update.sh index 4c630334514..1b374bcc05c 100755 --- a/hack/e2e-suite/update.sh +++ b/hack/e2e-suite/update.sh @@ -51,21 +51,16 @@ function validate() { for id in "${pod_id_list[@]+${pod_id_list[@]}}"; do local template_string current_status current_image host_ip - # NB: This template string is a little subtle. - # - # Notes: - # - # The 'and' operator will return blank if any of the inputs are non- - # nil/false. If they are all set, then it'll return the last one. - # - # The container is name has a dash in it and so we can't use the simple - # syntax. Instead we need to quote that and use the 'index' operator. - # - # The value here is a structure with just a Time member. This is - # currently always set to a zero time. + # NB: kubectl & kubecfg add the "exists" function to the standard template functions. + # This lets us check to see if the "running" entry exists for each of the containers + # we care about. Exists will never return an error and it's safe to check a chain of + # things, any one of which may not exist. In the below template, all of info, + # containername, and running might be nil, so the normal index function isn't very + # helpful. + # This template is unit-tested in kubec{tl|fg}, so if you change it, update the unit test. # # You can read about the syntax here: http://golang.org/pkg/text/template/ - template_string="{{and ((index .currentState.info \"${CONTROLLER_NAME}\").state.running.startedAt) .currentState.info.net.state.running.startedAt}}" + template_string="{{and (exists . \"currentState\" \"info\" \"${CONTROLLER_NAME}\" \"state\" \"running\") (exists . \"currentState\" \"info\" \"net\" \"state\" \"running\")}}" current_status=$($KUBECFG -template="${template_string}" get "pods/$id") || { if [[ $current_status =~ "pod \"${id}\" not found" ]]; then echo " $id no longer exists" @@ -76,11 +71,13 @@ function validate() { exit -1 fi } - if [[ "$current_status" == "" ]]; then - echo " $id is created but not running ${current_status}" + if [[ "$current_status" == "false" ]]; then + echo " $id is created but not running." continue fi + echo " $id is created and both net and update-demo containers are running: $current_status" + template_string="{{(index .currentState.info \"${CONTROLLER_NAME}\").image}}" current_image=$($KUBECFG -template="${template_string}" get "pods/$id") || true if [[ "$current_image" != "${DOCKER_HUB_USER}/update-demo:${container_image_version}" ]]; then diff --git a/pkg/cloudprovider/openstack/openstack.go b/pkg/cloudprovider/openstack/openstack.go index bc2748acaf1..f744b3b100b 100644 --- a/pkg/cloudprovider/openstack/openstack.go +++ b/pkg/cloudprovider/openstack/openstack.go @@ -21,37 +21,74 @@ import ( "fmt" "io" "net" - "net/url" "regexp" + "time" "code.google.com/p/gcfg" "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/openstack" + "github.com/rackspace/gophercloud/openstack/compute/v2/flavors" + "github.com/rackspace/gophercloud/openstack/compute/v2/servers" + "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/members" + "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/extensions/lbaas/vips" + "github.com/rackspace/gophercloud/pagination" "github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider" "github.com/GoogleCloudPlatform/kubernetes/pkg/util" + "github.com/golang/glog" ) -var ErrServerNotFound = errors.New("Server not found") -var ErrMultipleServersFound = errors.New("Multiple servers matched query") -var ErrFlavorNotFound = errors.New("Flavor not found") +var ErrNotFound = errors.New("Failed to find object") +var ErrMultipleResults = errors.New("Multiple results where only one expected") +var ErrNoAddressFound = errors.New("No address found for host") +var ErrAttrNotFound = errors.New("Expected attribute not found") + +// encoding.TextUnmarshaler interface for time.Duration +type MyDuration struct { + time.Duration +} + +func (d *MyDuration) UnmarshalText(text []byte) error { + res, err := time.ParseDuration(string(text)) + if err != nil { + return err + } + d.Duration = res + return nil +} + +type LoadBalancerOpts struct { + SubnetId string `gcfg:"subnet-id"` // required + CreateMonitor bool `gcfg:"create-monitor"` + MonitorDelay MyDuration `gcfg:"monitor-delay"` + MonitorTimeout MyDuration `gcfg:"monitor-timeout"` + MonitorMaxRetries uint `gcfg:"monitor-max-retries"` +} // OpenStack is an implementation of cloud provider Interface for OpenStack. type OpenStack struct { - provider string - authOpt gophercloud.AuthOptions + provider *gophercloud.ProviderClient region string - access *gophercloud.Access + lbOpts LoadBalancerOpts } type Config struct { Global struct { - AuthUrl string - Username, Password string - ApiKey string - TenantId, TenantName string - Region string + AuthUrl string `gcfg:"auth-url"` + Username string + UserId string `gcfg:"user-id"` + Password string + ApiKey string `gcfg:"api-key"` + TenantId string `gcfg:"tenant-id"` + TenantName string `gcfg:"tenant-name"` + DomainId string `gcfg:"domain-id"` + DomainName string `gcfg:"domain-name"` + Region string } + LoadBalancer LoadBalancerOpts } func init() { @@ -66,11 +103,13 @@ func init() { func (cfg Config) toAuthOptions() gophercloud.AuthOptions { return gophercloud.AuthOptions{ - Username: cfg.Global.Username, - Password: cfg.Global.Password, - ApiKey: cfg.Global.ApiKey, - TenantId: cfg.Global.TenantId, - TenantName: cfg.Global.TenantName, + IdentityEndpoint: cfg.Global.AuthUrl, + Username: cfg.Global.Username, + UserID: cfg.Global.UserId, + Password: cfg.Global.Password, + APIKey: cfg.Global.ApiKey, + TenantID: cfg.Global.TenantId, + TenantName: cfg.Global.TenantName, // Persistent service, so we need to be able to renew tokens AllowReauth: true, @@ -89,133 +128,462 @@ func readConfig(config io.Reader) (Config, error) { } func newOpenStack(cfg Config) (*OpenStack, error) { - os := OpenStack{ - provider: cfg.Global.AuthUrl, - authOpt: cfg.toAuthOptions(), - region: cfg.Global.Region, + provider, err := openstack.AuthenticatedClient(cfg.toAuthOptions()) + if err != nil { + return nil, err } - access, err := gophercloud.Authenticate(os.provider, os.authOpt) - os.access = access - - return &os, err + os := OpenStack{ + provider: provider, + region: cfg.Global.Region, + lbOpts: cfg.LoadBalancer, + } + return &os, nil } type Instances struct { - servers gophercloud.CloudServersProvider + compute *gophercloud.ServiceClient flavor_to_resource map[string]*api.NodeResources // keyed by flavor id } // Instances returns an implementation of Instances for OpenStack. func (os *OpenStack) Instances() (cloudprovider.Instances, bool) { - servers, err := gophercloud.ServersApi(os.access, gophercloud.ApiCriteria{ - Type: "compute", - UrlChoice: gophercloud.PublicURL, - Region: os.region, + glog.V(2).Info("openstack.Instances() called") + + compute, err := openstack.NewComputeV2(os.provider, gophercloud.EndpointOpts{ + Region: os.region, }) - if err != nil { + glog.Warningf("Failed to find compute endpoint: %v", err) return nil, false } - flavors, err := servers.ListFlavors() - if err != nil { - return nil, false - } - flavor_to_resource := make(map[string]*api.NodeResources, len(flavors)) - for _, flavor := range flavors { - rsrc := api.NodeResources{ - Capacity: api.ResourceList{ - "cpu": util.NewIntOrStringFromInt(flavor.VCpus), - "memory": util.NewIntOrStringFromString(fmt.Sprintf("%dMiB", flavor.Ram)), - "openstack.org/disk": util.NewIntOrStringFromString(fmt.Sprintf("%dGB", flavor.Disk)), - "openstack.org/rxTxFactor": util.NewIntOrStringFromInt(int(flavor.RxTxFactor * 1000)), - "openstack.org/swap": util.NewIntOrStringFromString(fmt.Sprintf("%dMiB", flavor.Swap)), - }, + pager := flavors.ListDetail(compute, nil) + + flavor_to_resource := make(map[string]*api.NodeResources) + err = pager.EachPage(func(page pagination.Page) (bool, error) { + flavorList, err := flavors.ExtractFlavors(page) + if err != nil { + return false, err } - flavor_to_resource[flavor.Id] = &rsrc + for _, flavor := range flavorList { + rsrc := api.NodeResources{ + Capacity: api.ResourceList{ + "cpu": util.NewIntOrStringFromInt(flavor.VCPUs), + "memory": util.NewIntOrStringFromString(fmt.Sprintf("%dMiB", flavor.RAM)), + "openstack.org/disk": util.NewIntOrStringFromString(fmt.Sprintf("%dGB", flavor.Disk)), + "openstack.org/rxTxFactor": util.NewIntOrStringFromInt(int(flavor.RxTxFactor * 1000)), + "openstack.org/swap": util.NewIntOrStringFromString(fmt.Sprintf("%dMiB", flavor.Swap)), + }, + } + flavor_to_resource[flavor.ID] = &rsrc + } + return true, nil + }) + if err != nil { + glog.Warningf("Failed to find compute flavors: %v", err) + return nil, false } - return &Instances{servers, flavor_to_resource}, true + glog.V(2).Infof("Found %v compute flavors", len(flavor_to_resource)) + glog.V(1).Info("Claiming to support Instances") + + return &Instances{compute, flavor_to_resource}, true } func (i *Instances) List(name_filter string) ([]string, error) { - filter := url.Values{} - filter.Set("name", name_filter) - filter.Set("status", "ACTIVE") + glog.V(2).Infof("openstack List(%v) called", name_filter) - servers, err := i.servers.ListServersByFilter(filter) + opts := servers.ListOpts{ + Name: name_filter, + Status: "ACTIVE", + } + pager := servers.List(i.compute, opts) + + ret := make([]string, 0) + err := pager.EachPage(func(page pagination.Page) (bool, error) { + sList, err := servers.ExtractServers(page) + if err != nil { + return false, err + } + for _, server := range sList { + ret = append(ret, server.Name) + } + return true, nil + }) if err != nil { return nil, err } - ret := make([]string, len(servers)) - for idx, srv := range servers { - ret[idx] = srv.Name - } + glog.V(2).Infof("Found %v entries: %v", len(ret), ret) + return ret, nil } -func getServerByName(api gophercloud.CloudServersProvider, name string) (*gophercloud.Server, error) { - filter := url.Values{} - filter.Set("name", fmt.Sprintf("^%s$", regexp.QuoteMeta(name))) - filter.Set("status", "ACTIVE") +func getServerByName(client *gophercloud.ServiceClient, name string) (*servers.Server, error) { + opts := servers.ListOpts{ + Name: fmt.Sprintf("^%s$", regexp.QuoteMeta(name)), + Status: "ACTIVE", + } + pager := servers.List(client, opts) - servers, err := api.ListServersByFilter(filter) + serverList := make([]servers.Server, 0, 1) + + err := pager.EachPage(func(page pagination.Page) (bool, error) { + s, err := servers.ExtractServers(page) + if err != nil { + return false, err + } + serverList = append(serverList, s...) + if len(serverList) > 1 { + return false, ErrMultipleResults + } + return true, nil + }) if err != nil { return nil, err } - if len(servers) == 0 { - return nil, ErrServerNotFound - } else if len(servers) > 1 { - return nil, ErrMultipleServersFound + if len(serverList) == 0 { + return nil, ErrNotFound + } else if len(serverList) > 1 { + return nil, ErrMultipleResults } - return &servers[0], nil + return &serverList[0], nil } -func (i *Instances) IPAddress(name string) (net.IP, error) { - srv, err := getServerByName(i.servers, name) +func firstAddr(netblob interface{}) string { + // Run-time types for the win :( + list, ok := netblob.([]interface{}) + if !ok || len(list) < 1 { + return "" + } + props, ok := list[0].(map[string]interface{}) + if !ok { + return "" + } + tmp, ok := props["addr"] + if !ok { + return "" + } + addr, ok := tmp.(string) + if !ok { + return "" + } + return addr +} + +func getAddressByName(api *gophercloud.ServiceClient, name string) (string, error) { + srv, err := getServerByName(api, name) if err != nil { - return nil, err + return "", err } var s string - if len(srv.Addresses.Private) > 0 { - s = srv.Addresses.Private[0].Addr - } else if len(srv.Addresses.Public) > 0 { - s = srv.Addresses.Public[0].Addr - } else if srv.AccessIPv4 != "" { + if s == "" { + s = firstAddr(srv.Addresses["private"]) + } + if s == "" { + s = firstAddr(srv.Addresses["public"]) + } + if s == "" { s = srv.AccessIPv4 - } else { + } + if s == "" { s = srv.AccessIPv6 } - return net.ParseIP(s), nil + if s == "" { + return "", ErrNoAddressFound + } + return s, nil } -func (i *Instances) GetNodeResources(name string) (*api.NodeResources, error) { - srv, err := getServerByName(i.servers, name) +func (i *Instances) IPAddress(name string) (net.IP, error) { + glog.V(2).Infof("IPAddress(%v) called", name) + + ip, err := getAddressByName(i.compute, name) if err != nil { return nil, err } - rsrc, ok := i.flavor_to_resource[srv.Flavor.Id] - if !ok { - return nil, ErrFlavorNotFound + glog.V(2).Infof("IPAddress(%v) => %v", name, ip) + + return net.ParseIP(ip), err +} + +func (i *Instances) GetNodeResources(name string) (*api.NodeResources, error) { + glog.V(2).Infof("GetNodeResources(%v) called", name) + + srv, err := getServerByName(i.compute, name) + if err != nil { + return nil, err } + s, ok := srv.Flavor["id"] + if !ok { + return nil, ErrAttrNotFound + } + flavId, ok := s.(string) + if !ok { + return nil, ErrAttrNotFound + } + rsrc, ok := i.flavor_to_resource[flavId] + if !ok { + return nil, ErrNotFound + } + + glog.V(2).Infof("GetNodeResources(%v) => %v", name, rsrc) + return rsrc, nil } -func (aws *OpenStack) Clusters() (cloudprovider.Clusters, bool) { +func (os *OpenStack) Clusters() (cloudprovider.Clusters, bool) { return nil, false } +type LoadBalancer struct { + network *gophercloud.ServiceClient + compute *gophercloud.ServiceClient + opts LoadBalancerOpts +} + func (os *OpenStack) TCPLoadBalancer() (cloudprovider.TCPLoadBalancer, bool) { - return nil, false + // TODO: Search for and support Rackspace loadbalancer API, and others. + network, err := openstack.NewNetworkV2(os.provider, gophercloud.EndpointOpts{ + Region: os.region, + }) + if err != nil { + glog.Warningf("Failed to find neutron endpoint: %v", err) + return nil, false + } + + compute, err := openstack.NewComputeV2(os.provider, gophercloud.EndpointOpts{ + Region: os.region, + }) + if err != nil { + glog.Warningf("Failed to find compute endpoint: %v", err) + return nil, false + } + + glog.V(1).Info("Claiming to support TCPLoadBalancer") + + return &LoadBalancer{network, compute, os.lbOpts}, true +} + +func getVipByName(client *gophercloud.ServiceClient, name string) (*vips.VirtualIP, error) { + opts := vips.ListOpts{ + Name: name, + } + pager := vips.List(client, opts) + + vipList := make([]vips.VirtualIP, 0, 1) + + err := pager.EachPage(func(page pagination.Page) (bool, error) { + v, err := vips.ExtractVIPs(page) + if err != nil { + return false, err + } + vipList = append(vipList, v...) + if len(vipList) > 1 { + return false, ErrMultipleResults + } + return true, nil + }) + if err != nil { + return nil, err + } + + if len(vipList) == 0 { + return nil, ErrNotFound + } else if len(vipList) > 1 { + return nil, ErrMultipleResults + } + + return &vipList[0], nil +} + +func (lb *LoadBalancer) TCPLoadBalancerExists(name, region string) (bool, error) { + vip, err := getVipByName(lb.network, name) + if err == ErrNotFound { + return false, nil + } + return vip != nil, err +} + +// TODO: This code currently ignores 'region' and always creates a +// loadbalancer in only the current OpenStack region. We should take +// a list of regions (from config) and query/create loadbalancers in +// each region. + +func (lb *LoadBalancer) CreateTCPLoadBalancer(name, region string, externalIP net.IP, port int, hosts []string, affinity api.AffinityType) (net.IP, error) { + glog.V(2).Infof("CreateTCPLoadBalancer(%v, %v, %v, %v, %v)", name, region, externalIP, port, hosts) + if affinity != api.AffinityTypeNone { + return nil, fmt.Errorf("unsupported load balancer affinity: %v", affinity) + } + pool, err := pools.Create(lb.network, pools.CreateOpts{ + Name: name, + Protocol: pools.ProtocolTCP, + SubnetID: lb.opts.SubnetId, + }).Extract() + if err != nil { + return nil, err + } + + for _, host := range hosts { + addr, err := getAddressByName(lb.compute, host) + if err != nil { + return nil, err + } + + _, err = members.Create(lb.network, members.CreateOpts{ + PoolID: pool.ID, + ProtocolPort: port, + Address: addr, + }).Extract() + if err != nil { + pools.Delete(lb.network, pool.ID) + return nil, err + } + } + + var mon *monitors.Monitor + if lb.opts.CreateMonitor { + mon, err = monitors.Create(lb.network, monitors.CreateOpts{ + Type: monitors.TypeTCP, + Delay: int(lb.opts.MonitorDelay.Duration.Seconds()), + Timeout: int(lb.opts.MonitorTimeout.Duration.Seconds()), + MaxRetries: int(lb.opts.MonitorMaxRetries), + }).Extract() + if err != nil { + pools.Delete(lb.network, pool.ID) + return nil, err + } + + _, err = pools.AssociateMonitor(lb.network, pool.ID, mon.ID).Extract() + if err != nil { + monitors.Delete(lb.network, mon.ID) + pools.Delete(lb.network, pool.ID) + return nil, err + } + } + + vip, err := vips.Create(lb.network, vips.CreateOpts{ + Name: name, + Description: fmt.Sprintf("Kubernetes external service %s", name), + Address: externalIP.String(), + Protocol: "TCP", + ProtocolPort: port, + PoolID: pool.ID, + }).Extract() + if err != nil { + if mon != nil { + monitors.Delete(lb.network, mon.ID) + } + pools.Delete(lb.network, pool.ID) + return nil, err + } + + return net.ParseIP(vip.Address), nil +} + +func (lb *LoadBalancer) UpdateTCPLoadBalancer(name, region string, hosts []string) error { + glog.V(2).Infof("UpdateTCPLoadBalancer(%v, %v, %v)", name, region, hosts) + + vip, err := getVipByName(lb.network, name) + if err != nil { + return err + } + + // Set of member (addresses) that _should_ exist + addrs := map[string]bool{} + for _, host := range hosts { + addr, err := getAddressByName(lb.compute, host) + if err != nil { + return err + } + + addrs[addr] = true + } + + // Iterate over members that _do_ exist + pager := members.List(lb.network, members.ListOpts{PoolID: vip.PoolID}) + err = pager.EachPage(func(page pagination.Page) (bool, error) { + memList, err := members.ExtractMembers(page) + if err != nil { + return false, err + } + + for _, member := range memList { + if _, found := addrs[member.Address]; found { + // Member already exists + delete(addrs, member.Address) + } else { + // Member needs to be deleted + err = members.Delete(lb.network, member.ID).ExtractErr() + if err != nil { + return false, err + } + } + } + + return true, nil + }) + if err != nil { + return err + } + + // Anything left in addrs is a new member that needs to be added + for addr := range addrs { + _, err := members.Create(lb.network, members.CreateOpts{ + PoolID: vip.PoolID, + Address: addr, + ProtocolPort: vip.ProtocolPort, + }).Extract() + if err != nil { + return err + } + } + + return nil +} + +func (lb *LoadBalancer) DeleteTCPLoadBalancer(name, region string) error { + glog.V(2).Infof("DeleteTCPLoadBalancer(%v, %v)", name, region) + + vip, err := getVipByName(lb.network, name) + if err != nil { + return err + } + + pool, err := pools.Get(lb.network, vip.PoolID).Extract() + if err != nil { + return err + } + + // Have to delete VIP before pool can be deleted + err = vips.Delete(lb.network, vip.ID).ExtractErr() + if err != nil { + return err + } + + // Ignore errors for everything following here + + for _, monId := range pool.MonitorIDs { + pools.DisassociateMonitor(lb.network, pool.ID, monId) + } + pools.Delete(lb.network, pool.ID) + + return nil } func (os *OpenStack) Zones() (cloudprovider.Zones, bool) { - return nil, false + glog.V(1).Info("Claiming to support Zones") + + return os, true +} +func (os *OpenStack) GetZone() (cloudprovider.Zone, error) { + glog.V(1).Infof("Current zone is %v", os.region) + + return cloudprovider.Zone{Region: os.region}, nil } diff --git a/pkg/cloudprovider/openstack/openstack_test.go b/pkg/cloudprovider/openstack/openstack_test.go index eada1794b7b..2c88d38deb2 100644 --- a/pkg/cloudprovider/openstack/openstack_test.go +++ b/pkg/cloudprovider/openstack/openstack_test.go @@ -20,6 +20,9 @@ import ( "os" "strings" "testing" + "time" + + "github.com/rackspace/gophercloud" ) func TestReadConfig(t *testing.T) { @@ -30,8 +33,13 @@ func TestReadConfig(t *testing.T) { cfg, err := readConfig(strings.NewReader(` [Global] -authurl = http://auth.url +auth-url = http://auth.url username = user +[LoadBalancer] +create-monitor = yes +monitor-delay = 1m +monitor-timeout = 30s +monitor-max-retries = 3 `)) if err != nil { t.Fatalf("Should succeed when a valid config is provided: %s", err) @@ -39,6 +47,19 @@ username = user if cfg.Global.AuthUrl != "http://auth.url" { t.Errorf("incorrect authurl: %s", cfg.Global.AuthUrl) } + + if !cfg.LoadBalancer.CreateMonitor { + t.Errorf("incorrect lb.createmonitor: %s", cfg.LoadBalancer.CreateMonitor) + } + if cfg.LoadBalancer.MonitorDelay.Duration != 1*time.Minute { + t.Errorf("incorrect lb.monitordelay: %s", cfg.LoadBalancer.MonitorDelay) + } + if cfg.LoadBalancer.MonitorTimeout.Duration != 30*time.Second { + t.Errorf("incorrect lb.monitortimeout: %s", cfg.LoadBalancer.MonitorTimeout) + } + if cfg.LoadBalancer.MonitorMaxRetries != 3 { + t.Errorf("incorrect lb.monitormaxretries: %s", cfg.LoadBalancer.MonitorMaxRetries) + } } func TestToAuthOptions(t *testing.T) { @@ -56,14 +77,13 @@ func TestToAuthOptions(t *testing.T) { } } -// This allows testing against an existing OpenStack install, using the -// standard OS_* OpenStack client environment variables. +// This allows acceptance testing against an existing OpenStack +// install, using the standard OS_* OpenStack client environment +// variables. +// FIXME: it would be better to hermetically test against canned JSON +// requests/responses. func configFromEnv() (cfg Config, ok bool) { cfg.Global.AuthUrl = os.Getenv("OS_AUTH_URL") - // gophercloud wants "provider" to point specifically at tokens URL - if !strings.HasSuffix(cfg.Global.AuthUrl, "/tokens") { - cfg.Global.AuthUrl += "/tokens" - } cfg.Global.TenantId = os.Getenv("OS_TENANT_ID") // Rax/nova _insists_ that we don't specify both tenant ID and name @@ -75,11 +95,14 @@ func configFromEnv() (cfg Config, ok bool) { cfg.Global.Password = os.Getenv("OS_PASSWORD") cfg.Global.ApiKey = os.Getenv("OS_API_KEY") cfg.Global.Region = os.Getenv("OS_REGION_NAME") + cfg.Global.DomainId = os.Getenv("OS_DOMAIN_ID") + cfg.Global.DomainName = os.Getenv("OS_DOMAIN_NAME") ok = (cfg.Global.AuthUrl != "" && cfg.Global.Username != "" && (cfg.Global.Password != "" || cfg.Global.ApiKey != "") && - (cfg.Global.TenantId != "" || cfg.Global.TenantName != "")) + (cfg.Global.TenantId != "" || cfg.Global.TenantName != "" || + cfg.Global.DomainId != "" || cfg.Global.DomainName != "")) return } @@ -133,3 +156,51 @@ func TestInstances(t *testing.T) { } t.Logf("Found GetNodeResources(%s) = %s\n", srvs[0], rsrcs) } + +func TestTCPLoadBalancer(t *testing.T) { + cfg, ok := configFromEnv() + if !ok { + t.Skipf("No config found in environment") + } + + os, err := newOpenStack(cfg) + if err != nil { + t.Fatalf("Failed to construct/authenticate OpenStack: %s", err) + } + + lb, ok := os.TCPLoadBalancer() + if !ok { + t.Fatalf("TCPLoadBalancer() returned false - perhaps your stack doesn't support Neutron?") + } + + exists, err := lb.TCPLoadBalancerExists("noexist", "region") + if err != nil { + t.Fatalf("TCPLoadBalancerExists(\"noexist\") returned error: %s", err) + } + if exists { + t.Fatalf("TCPLoadBalancerExists(\"noexist\") returned true") + } +} + +func TestZones(t *testing.T) { + os := OpenStack{ + provider: &gophercloud.ProviderClient{ + IdentityBase: "http://auth.url/", + }, + region: "myRegion", + } + + z, ok := os.Zones() + if !ok { + t.Fatalf("Zones() returned false") + } + + zone, err := z.GetZone() + if err != nil { + t.Fatalf("GetZone() returned error: %s", err) + } + + if zone.Region != "myRegion" { + t.Fatalf("GetZone() returned wrong region (%s)", zone.Region) + } +} diff --git a/pkg/kubecfg/resource_printer.go b/pkg/kubecfg/resource_printer.go index 01837bfe904..094e97f2c4b 100644 --- a/pkg/kubecfg/resource_printer.go +++ b/pkg/kubecfg/resource_printer.go @@ -327,25 +327,38 @@ func (h *HumanReadablePrinter) PrintObj(obj runtime.Object, output io.Writer) er // TemplatePrinter is an implementation of ResourcePrinter which formats data with a Go Template. type TemplatePrinter struct { - template *template.Template + rawTemplate string + template *template.Template } func NewTemplatePrinter(tmpl []byte) (*TemplatePrinter, error) { - t, err := template.New("output").Parse(string(tmpl)) + t, err := template.New("output"). + Funcs(template.FuncMap{"exists": exists}). + Parse(string(tmpl)) if err != nil { return nil, err } - return &TemplatePrinter{t}, nil + return &TemplatePrinter{string(tmpl), t}, nil } // Print parses the data as JSON, and re-formats it with the Go Template. func (t *TemplatePrinter) Print(data []byte, w io.Writer) error { - obj := map[string]interface{}{} - err := json.Unmarshal(data, &obj) + out := map[string]interface{}{} + err := json.Unmarshal(data, &out) if err != nil { return err } - return t.template.Execute(w, obj) + if err := t.safeExecute(w, out); err != nil { + // It is way easier to debug this stuff when it shows up in + // stdout instead of just stdin. So in addition to returning + // a nice error, also print useful stuff with the writer. + fmt.Fprintf(w, "Error executing template: %v\n", err) + fmt.Fprintf(w, "template was:\n%v\n", t.rawTemplate) + fmt.Fprintf(w, "raw data was:\n%v\n", string(data)) + fmt.Fprintf(w, "object given to template engine was:\n%+v\n", out) + return fmt.Errorf("error executing template '%v': '%v'\n----data----\n%#v\n", t.rawTemplate, err, out) + } + return nil } // PrintObj formats the obj with the Go Template. @@ -356,3 +369,93 @@ func (t *TemplatePrinter) PrintObj(obj runtime.Object, w io.Writer) error { } return t.Print(data, w) } + +// safeExecute tries to execute the template, but catches panics and returns an error +// should the template engine panic. +func (p *TemplatePrinter) safeExecute(w io.Writer, obj interface{}) error { + var panicErr error + // Sorry for the double anonymous function. There's probably a clever way + // to do this that has the defer'd func setting the value to be returned, but + // that would be even less obvious. + retErr := func() error { + defer func() { + if x := recover(); x != nil { + panicErr = fmt.Errorf("caught panic: %+v", x) + } + }() + return p.template.Execute(w, obj) + }() + if panicErr != nil { + return panicErr + } + return retErr +} + +// exists returns true if it would be possible to call the index function +// with these arguments. +// +// TODO: how to document this for users? +// +// index returns the result of indexing its first argument by the following +// arguments. Thus "index x 1 2 3" is, in Go syntax, x[1][2][3]. Each +// indexed item must be a map, slice, or array. +func exists(item interface{}, indices ...interface{}) bool { + v := reflect.ValueOf(item) + for _, i := range indices { + index := reflect.ValueOf(i) + var isNil bool + if v, isNil = indirect(v); isNil { + return false + } + switch v.Kind() { + case reflect.Array, reflect.Slice, reflect.String: + var x int64 + switch index.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + x = index.Int() + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + x = int64(index.Uint()) + default: + return false + } + if x < 0 || x >= int64(v.Len()) { + return false + } + v = v.Index(int(x)) + case reflect.Map: + if !index.IsValid() { + index = reflect.Zero(v.Type().Key()) + } + if !index.Type().AssignableTo(v.Type().Key()) { + return false + } + if x := v.MapIndex(index); x.IsValid() { + v = x + } else { + v = reflect.Zero(v.Type().Elem()) + } + default: + return false + } + } + if _, isNil := indirect(v); isNil { + return false + } + return true +} + +// stolen from text/template +// indirect returns the item at the end of indirection, and a bool to indicate if it's nil. +// We indirect through pointers and empty interfaces (only) because +// non-empty interfaces have methods we might need. +func indirect(v reflect.Value) (rv reflect.Value, isNil bool) { + for ; v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface; v = v.Elem() { + if v.IsNil() { + return v, true + } + if v.Kind() == reflect.Interface && v.NumMethod() > 0 { + break + } + } + return v, false +} diff --git a/pkg/kubecfg/resource_printer_test.go b/pkg/kubecfg/resource_printer_test.go index ef5abd96048..d3144b248d9 100644 --- a/pkg/kubecfg/resource_printer_test.go +++ b/pkg/kubecfg/resource_printer_test.go @@ -26,6 +26,8 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest" + "github.com/GoogleCloudPlatform/kubernetes/pkg/util" + "github.com/ghodss/yaml" ) @@ -177,3 +179,127 @@ func TestTemplateEmitsVersionedObjects(t *testing.T) { t.Errorf("Expected %v, got %v", e, a) } } + +func TestTemplatePanic(t *testing.T) { + tmpl := `{{and ((index .currentState.info "update-demo").state.running.startedAt) .currentState.info.net.state.running.startedAt}}` + printer, err := NewTemplatePrinter([]byte(tmpl)) + if err != nil { + t.Fatalf("tmpl fail: %v", err) + } + buffer := &bytes.Buffer{} + err = printer.PrintObj(&api.Pod{}, buffer) + if err == nil { + t.Fatalf("expected that template to crash") + } + if buffer.String() == "" { + t.Errorf("no debugging info was printed") + } +} + +func TestTemplateStrings(t *testing.T) { + // This unit tests the "exists" function as well as the template from update.sh + table := map[string]struct { + pod api.Pod + expect string + }{ + "nilInfo": {api.Pod{}, "false"}, + "emptyInfo": {api.Pod{Status: api.PodStatus{Info: api.PodInfo{}}}, "false"}, + "containerExists": { + api.Pod{ + Status: api.PodStatus{ + Info: api.PodInfo{"update-demo": api.ContainerStatus{}}, + }, + }, + "false", + }, + "netExists": { + api.Pod{ + Status: api.PodStatus{ + Info: api.PodInfo{"net": api.ContainerStatus{}}, + }, + }, + "false", + }, + "bothExist": { + api.Pod{ + Status: api.PodStatus{ + Info: api.PodInfo{ + "update-demo": api.ContainerStatus{}, + "net": api.ContainerStatus{}, + }, + }, + }, + "false", + }, + "oneValid": { + api.Pod{ + Status: api.PodStatus{ + Info: api.PodInfo{ + "update-demo": api.ContainerStatus{}, + "net": api.ContainerStatus{ + State: api.ContainerState{ + Running: &api.ContainerStateRunning{ + StartedAt: util.Time{}, + }, + }, + }, + }, + }, + }, + "false", + }, + "bothValid": { + api.Pod{ + Status: api.PodStatus{ + Info: api.PodInfo{ + "update-demo": api.ContainerStatus{ + State: api.ContainerState{ + Running: &api.ContainerStateRunning{ + StartedAt: util.Time{}, + }, + }, + }, + "net": api.ContainerStatus{ + State: api.ContainerState{ + Running: &api.ContainerStateRunning{ + StartedAt: util.Time{}, + }, + }, + }, + }, + }, + }, + "true", + }, + } + + // The point of this test is to verify that the below template works. If you change this + // template, you need to update hack/e2e-suite/update.sh. + tmpl := + `{{and (exists . "currentState" "info" "update-demo" "state" "running") (exists . "currentState" "info" "net" "state" "running")}}` + useThisToDebug := ` +a: {{exists . "currentState"}} +b: {{exists . "currentState" "info"}} +c: {{exists . "currentState" "info" "update-demo"}} +d: {{exists . "currentState" "info" "update-demo" "state"}} +e: {{exists . "currentState" "info" "update-demo" "state" "running"}} +f: {{exists . "currentState" "info" "update-demo" "state" "running" "startedAt"}}` + _ = useThisToDebug // don't complain about unused var + + printer, err := NewTemplatePrinter([]byte(tmpl)) + if err != nil { + t.Fatalf("tmpl fail: %v", err) + } + + for name, item := range table { + buffer := &bytes.Buffer{} + err = printer.PrintObj(&item.pod, buffer) + if err != nil { + t.Errorf("%v: unexpected err: %v", name, err) + continue + } + if e, a := item.expect, buffer.String(); e != a { + t.Errorf("%v: expected %v, got %v", name, e, a) + } + } +} diff --git a/pkg/kubectl/resource_printer.go b/pkg/kubectl/resource_printer.go index 0f8aa3ba888..797c948b443 100644 --- a/pkg/kubectl/resource_printer.go +++ b/pkg/kubectl/resource_printer.go @@ -390,7 +390,9 @@ type TemplatePrinter struct { } func NewTemplatePrinter(tmpl []byte, asVersion string, convertor runtime.ObjectConvertor) (*TemplatePrinter, error) { - t, err := template.New("output").Parse(string(tmpl)) + t, err := template.New("output"). + Funcs(template.FuncMap{"exists": exists}). + Parse(string(tmpl)) if err != nil { return nil, err } @@ -416,12 +418,40 @@ func (p *TemplatePrinter) PrintObj(obj runtime.Object, w io.Writer) error { if err := json.Unmarshal(data, &out); err != nil { return err } - if err = p.template.Execute(w, out); err != nil { - return fmt.Errorf("error executing template '%v': '%v'\n----data----\n%#v\n", p.rawTemplate, err, out) + if err = p.safeExecute(w, out); err != nil { + // It is way easier to debug this stuff when it shows up in + // stdout instead of just stdin. So in addition to returning + // a nice error, also print useful stuff with the writer. + fmt.Fprintf(w, "Error executing template: %v\n", err) + fmt.Fprintf(w, "template was:\n\t%v\n", p.rawTemplate) + fmt.Fprintf(w, "raw data was:\n\t%v\n", string(data)) + fmt.Fprintf(w, "object given to template engine was:\n\t%+v\n", out) + return fmt.Errorf("error executing template '%v': '%v'\n----data----\n%+v\n", p.rawTemplate, err, out) } return nil } +// safeExecute tries to execute the template, but catches panics and returns an error +// should the template engine panic. +func (p *TemplatePrinter) safeExecute(w io.Writer, obj interface{}) error { + var panicErr error + // Sorry for the double anonymous function. There's probably a clever way + // to do this that has the defer'd func setting the value to be returned, but + // that would be even less obvious. + retErr := func() error { + defer func() { + if x := recover(); x != nil { + panicErr = fmt.Errorf("caught panic: %+v", x) + } + }() + return p.template.Execute(w, obj) + }() + if panicErr != nil { + return panicErr + } + return retErr +} + func tabbedString(f func(io.Writer) error) (string, error) { out := new(tabwriter.Writer) buf := &bytes.Buffer{} @@ -436,3 +466,72 @@ func tabbedString(f func(io.Writer) error) (string, error) { str := string(buf.String()) return str, nil } + +// exists returns true if it would be possible to call the index function +// with these arguments. +// +// TODO: how to document this for users? +// +// index returns the result of indexing its first argument by the following +// arguments. Thus "index x 1 2 3" is, in Go syntax, x[1][2][3]. Each +// indexed item must be a map, slice, or array. +func exists(item interface{}, indices ...interface{}) bool { + v := reflect.ValueOf(item) + for _, i := range indices { + index := reflect.ValueOf(i) + var isNil bool + if v, isNil = indirect(v); isNil { + return false + } + switch v.Kind() { + case reflect.Array, reflect.Slice, reflect.String: + var x int64 + switch index.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + x = index.Int() + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + x = int64(index.Uint()) + default: + return false + } + if x < 0 || x >= int64(v.Len()) { + return false + } + v = v.Index(int(x)) + case reflect.Map: + if !index.IsValid() { + index = reflect.Zero(v.Type().Key()) + } + if !index.Type().AssignableTo(v.Type().Key()) { + return false + } + if x := v.MapIndex(index); x.IsValid() { + v = x + } else { + v = reflect.Zero(v.Type().Elem()) + } + default: + return false + } + } + if _, isNil := indirect(v); isNil { + return false + } + return true +} + +// stolen from text/template +// indirect returns the item at the end of indirection, and a bool to indicate if it's nil. +// We indirect through pointers and empty interfaces (only) because +// non-empty interfaces have methods we might need. +func indirect(v reflect.Value) (rv reflect.Value, isNil bool) { + for ; v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface; v = v.Elem() { + if v.IsNil() { + return v, true + } + if v.Kind() == reflect.Interface && v.NumMethod() > 0 { + break + } + } + return v, false +} diff --git a/pkg/kubectl/resource_printer_test.go b/pkg/kubectl/resource_printer_test.go index a79852775ca..ef5effbe003 100644 --- a/pkg/kubectl/resource_printer_test.go +++ b/pkg/kubectl/resource_printer_test.go @@ -290,6 +290,130 @@ func TestTemplateEmitsVersionedObjects(t *testing.T) { } } +func TestTemplatePanic(t *testing.T) { + tmpl := `{{and ((index .currentState.info "update-demo").state.running.startedAt) .currentState.info.net.state.running.startedAt}}` + printer, err := NewTemplatePrinter([]byte(tmpl), testapi.Version(), api.Scheme) + if err != nil { + t.Fatalf("tmpl fail: %v", err) + } + buffer := &bytes.Buffer{} + err = printer.PrintObj(&api.Pod{}, buffer) + if err == nil { + t.Fatalf("expected that template to crash") + } + if buffer.String() == "" { + t.Errorf("no debugging info was printed") + } +} + +func TestTemplateStrings(t *testing.T) { + // This unit tests the "exists" function as well as the template from update.sh + table := map[string]struct { + pod api.Pod + expect string + }{ + "nilInfo": {api.Pod{}, "false"}, + "emptyInfo": {api.Pod{Status: api.PodStatus{Info: api.PodInfo{}}}, "false"}, + "containerExists": { + api.Pod{ + Status: api.PodStatus{ + Info: api.PodInfo{"update-demo": api.ContainerStatus{}}, + }, + }, + "false", + }, + "netExists": { + api.Pod{ + Status: api.PodStatus{ + Info: api.PodInfo{"net": api.ContainerStatus{}}, + }, + }, + "false", + }, + "bothExist": { + api.Pod{ + Status: api.PodStatus{ + Info: api.PodInfo{ + "update-demo": api.ContainerStatus{}, + "net": api.ContainerStatus{}, + }, + }, + }, + "false", + }, + "oneValid": { + api.Pod{ + Status: api.PodStatus{ + Info: api.PodInfo{ + "update-demo": api.ContainerStatus{}, + "net": api.ContainerStatus{ + State: api.ContainerState{ + Running: &api.ContainerStateRunning{ + StartedAt: util.Time{}, + }, + }, + }, + }, + }, + }, + "false", + }, + "bothValid": { + api.Pod{ + Status: api.PodStatus{ + Info: api.PodInfo{ + "update-demo": api.ContainerStatus{ + State: api.ContainerState{ + Running: &api.ContainerStateRunning{ + StartedAt: util.Time{}, + }, + }, + }, + "net": api.ContainerStatus{ + State: api.ContainerState{ + Running: &api.ContainerStateRunning{ + StartedAt: util.Time{}, + }, + }, + }, + }, + }, + }, + "true", + }, + } + + // The point of this test is to verify that the below template works. If you change this + // template, you need to update hack/e2e-suite/update.sh. + tmpl := + `{{and (exists . "currentState" "info" "update-demo" "state" "running") (exists . "currentState" "info" "net" "state" "running")}}` + useThisToDebug := ` +a: {{exists . "currentState"}} +b: {{exists . "currentState" "info"}} +c: {{exists . "currentState" "info" "update-demo"}} +d: {{exists . "currentState" "info" "update-demo" "state"}} +e: {{exists . "currentState" "info" "update-demo" "state" "running"}} +f: {{exists . "currentState" "info" "update-demo" "state" "running" "startedAt"}}` + _ = useThisToDebug // don't complain about unused var + + printer, err := NewTemplatePrinter([]byte(tmpl), "v1beta1", api.Scheme) + if err != nil { + t.Fatalf("tmpl fail: %v", err) + } + + for name, item := range table { + buffer := &bytes.Buffer{} + err = printer.PrintObj(&item.pod, buffer) + if err != nil { + t.Errorf("%v: unexpected err: %v", name, err) + continue + } + if e, a := item.expect, buffer.String(); e != a { + t.Errorf("%v: expected %v, got %v", name, e, a) + } + } +} + func TestPrinters(t *testing.T) { om := func(name string) api.ObjectMeta { return api.ObjectMeta{Name: name} } templatePrinter, err := NewTemplatePrinter([]byte("{{.name}}"), testapi.Version(), api.Scheme) diff --git a/pkg/registry/pod/rest.go b/pkg/registry/pod/rest.go index 270f1aaa8ec..3b78a068608 100644 --- a/pkg/registry/pod/rest.go +++ b/pkg/registry/pod/rest.go @@ -18,6 +18,7 @@ package pod import ( "fmt" + "strings" "github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors" @@ -175,3 +176,46 @@ func (rs *REST) Update(ctx api.Context, obj runtime.Object) (<-chan apiserver.RE return rs.registry.GetPod(ctx, pod.Name) }), nil } + +// ResourceLocation returns a URL to which one can send traffic for the specified pod. +func (rs *REST) ResourceLocation(ctx api.Context, id string) (string, error) { + // Allow ID as "podname" or "podname:port". If port is not specified, + // try to use the first defined port on the pod. + parts := strings.Split(id, ":") + if len(parts) > 2 { + return "", errors.NewBadRequest(fmt.Sprintf("invalid pod request %q", id)) + } + name := parts[0] + port := "" + if len(parts) == 2 { + // TODO: if port is not a number but a "(container)/(portname)", do a name lookup. + port = parts[1] + } + + obj, err := rs.Get(ctx, name) + if err != nil { + return "", err + } + pod := obj.(*api.Pod) + if pod == nil { + return "", nil + } + + // Try to figure out a port. + if port == "" { + for i := range pod.Spec.Containers { + if len(pod.Spec.Containers[i].Ports) > 0 { + port = fmt.Sprintf("%d", pod.Spec.Containers[i].Ports[0].ContainerPort) + break + } + } + } + + // We leave off the scheme ('http://') because we have no idea what sort of server + // is listening at this endpoint. + loc := pod.Status.PodIP + if port != "" { + loc += fmt.Sprintf(":%s", port) + } + return loc, nil +} diff --git a/pkg/registry/pod/rest_test.go b/pkg/registry/pod/rest_test.go index e93853c69f6..0b63cf726b6 100644 --- a/pkg/registry/pod/rest_test.go +++ b/pkg/registry/pod/rest_test.go @@ -443,15 +443,6 @@ func TestCreatePod(t *testing.T) { } } -type FakePodInfoGetter struct { - info api.PodInfo - err error -} - -func (f *FakePodInfoGetter) GetPodInfo(host, podNamespace string, podID string) (api.PodContainerInfo, error) { - return api.PodContainerInfo{ContainerInfo: f.info}, f.err -} - func TestCreatePodWithConflictingNamespace(t *testing.T) { storage := REST{} pod := &api.Pod{ @@ -487,3 +478,108 @@ func TestUpdatePodWithConflictingNamespace(t *testing.T) { t.Errorf("Expected 'Pod.Namespace does not match the provided context' error, got '%v'", err.Error()) } } + +func TestResourceLocation(t *testing.T) { + expectedIP := "1.2.3.4" + testCases := []struct { + pod api.Pod + query string + location string + }{ + { + pod: api.Pod{ + ObjectMeta: api.ObjectMeta{Name: "foo"}, + }, + query: "foo", + location: expectedIP, + }, + { + pod: api.Pod{ + ObjectMeta: api.ObjectMeta{Name: "foo"}, + }, + query: "foo:12345", + location: expectedIP + ":12345", + }, + { + pod: api.Pod{ + ObjectMeta: api.ObjectMeta{Name: "foo"}, + Spec: api.PodSpec{ + Containers: []api.Container{ + {Name: "ctr"}, + }, + }, + }, + query: "foo", + location: expectedIP, + }, + { + pod: api.Pod{ + ObjectMeta: api.ObjectMeta{Name: "foo"}, + Spec: api.PodSpec{ + Containers: []api.Container{ + {Name: "ctr", Ports: []api.Port{{ContainerPort: 9376}}}, + }, + }, + }, + query: "foo", + location: expectedIP + ":9376", + }, + { + pod: api.Pod{ + ObjectMeta: api.ObjectMeta{Name: "foo"}, + Spec: api.PodSpec{ + Containers: []api.Container{ + {Name: "ctr", Ports: []api.Port{{ContainerPort: 9376}}}, + }, + }, + }, + query: "foo:12345", + location: expectedIP + ":12345", + }, + { + pod: api.Pod{ + ObjectMeta: api.ObjectMeta{Name: "foo"}, + Spec: api.PodSpec{ + Containers: []api.Container{ + {Name: "ctr1"}, + {Name: "ctr2", Ports: []api.Port{{ContainerPort: 9376}}}, + }, + }, + }, + query: "foo", + location: expectedIP + ":9376", + }, + { + pod: api.Pod{ + ObjectMeta: api.ObjectMeta{Name: "foo"}, + Spec: api.PodSpec{ + Containers: []api.Container{ + {Name: "ctr1", Ports: []api.Port{{ContainerPort: 9376}}}, + {Name: "ctr2", Ports: []api.Port{{ContainerPort: 1234}}}, + }, + }, + }, + query: "foo", + location: expectedIP + ":9376", + }, + } + + for _, tc := range testCases { + podRegistry := registrytest.NewPodRegistry(nil) + podRegistry.Pod = &tc.pod + storage := &REST{ + registry: podRegistry, + podCache: &fakeCache{statusToReturn: &api.PodStatus{PodIP: expectedIP}}, + } + + redirector := apiserver.Redirector(storage) + location, err := redirector.ResourceLocation(api.NewDefaultContext(), tc.query) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + if location != tc.location { + t.Errorf("Expected %v, but got %v", tc.location, location) + } + } +} diff --git a/pkg/util/util.go b/pkg/util/util.go index a16118148ce..086ff3881f6 100644 --- a/pkg/util/util.go +++ b/pkg/util/util.go @@ -19,8 +19,10 @@ package util import ( "encoding/json" "fmt" + "io/ioutil" "regexp" "runtime" + "strconv" "time" "github.com/golang/glog" @@ -134,3 +136,16 @@ func CompileRegexps(regexpStrings []string) ([]*regexp.Regexp, error) { } return regexps, nil } + +// Writes 'value' to /proc/self/oom_score_adj. +func ApplyOomScoreAdj(value int) error { + if value < -1000 || value > 1000 { + return fmt.Errorf("invalid value(%d) specified for oom_score_adj. Values must be within the range [-1000, 1000]") + } + + if err := ioutil.WriteFile("/proc/self/oom_score_adj", []byte(strconv.Itoa(value)), 0700); err != nil { + fmt.Errorf("failed to set oom_score_adj to %s - %q", value, err) + } + + return nil +}