Add github.com/rackspace/gophercloud package as a dependency.

Note this also pulls in:
github.com/kr/text
github.com/mitchellh/mapstructure
github.com/racker/perigee
github.com/tonnerre/golang-pretty
This commit is contained in:
Angus Lees
2014-10-15 14:03:37 +11:00
parent a8ab55b413
commit 9bed117119
97 changed files with 9390 additions and 1 deletions

View File

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

View File

@@ -0,0 +1,14 @@
language: go
install:
- go get -v .
go:
- 1.1
- 1.2
- tip
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

View File

@@ -0,0 +1,6 @@
Contributors
============
Samuel A. Falvo II <sam.falvo@rackspace.com>
Glen Campbell <glen.campbell@rackspace.com>
Jesse Noller <jesse.noller@rackspace.com>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,49 @@
// +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.")
}
})
})
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,66 @@
// +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...)
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,58 @@
// +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...)
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,24 @@
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"`
}

View File

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

View File

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

View File

@@ -0,0 +1,39 @@
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")

View File

@@ -0,0 +1,55 @@
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"`
}

View File

@@ -0,0 +1,88 @@
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"`
}

View File

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

View File

@@ -0,0 +1,106 @@
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"`
}

View File

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

View File

@@ -0,0 +1,98 @@
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"`
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,26 @@
#!/bin/bash
#
# This script helps new contributors set up their local workstation for
# gophercloud development and contributions.
# Create the environment
export GOPATH=$HOME/go/gophercloud
mkdir -p $GOPATH
# Download gophercloud into that environment
go get github.com/rackspace/gophercloud
cd $GOPATH/src/github.com/rackspace/gophercloud
git checkout master
# Write out the env.sh convenience file.
cd $GOPATH
cat <<EOF >env.sh
#!/bin/bash
export GOPATH=$(pwd)
export GOPHERCLOUD=$GOPATH/src/github.com/rackspace/gophercloud
EOF
chmod a+x env.sh
# Make changes immediately available as a convenience.
. ./env.sh

View File

@@ -0,0 +1,37 @@
#!/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

View File

@@ -0,0 +1,807 @@
// 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"`
}

View File

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

View File

@@ -0,0 +1,75 @@
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{}
}

View File

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

View File

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