Commit 595d4b4a authored by Daniel Smith's avatar Daniel Smith

Merge pull request #1676 from anguslees/openstack-provider

Add OpenStack cloud provider
parents 1dcb25e2 fffa0527
{ {
"ImportPath": "github.com/GoogleCloudPlatform/kubernetes", "ImportPath": "github.com/GoogleCloudPlatform/kubernetes",
"GoVersion": "go1.3", "GoVersion": "go1.3.1",
"Packages": [ "Packages": [
"./..." "./..."
], ],
...@@ -82,6 +82,10 @@ ...@@ -82,6 +82,10 @@
"Rev": "aef70dacbc78771e35beb261bb3a72986adf7906" "Rev": "aef70dacbc78771e35beb261bb3a72986adf7906"
}, },
{ {
"ImportPath": "github.com/kr/text",
"Rev": "6807e777504f54ad073ecef66747de158294b639"
},
{
"ImportPath": "github.com/mitchellh/goamz/aws", "ImportPath": "github.com/mitchellh/goamz/aws",
"Rev": "9cad7da945e699385c1a3e115aa255211921c9bb" "Rev": "9cad7da945e699385c1a3e115aa255211921c9bb"
}, },
...@@ -90,6 +94,20 @@ ...@@ -90,6 +94,20 @@
"Rev": "9cad7da945e699385c1a3e115aa255211921c9bb" "Rev": "9cad7da945e699385c1a3e115aa255211921c9bb"
}, },
{ {
"ImportPath": "github.com/mitchellh/mapstructure",
"Rev": "740c764bc6149d3f1806231418adb9f52c11bcbf"
},
{
"ImportPath": "github.com/racker/perigee",
"Comment": "v0.0.0-18-g0c00cb0",
"Rev": "0c00cb0a026b71034ebc8205263c77dad3577db5"
},
{
"ImportPath": "github.com/rackspace/gophercloud",
"Comment": "v0.1.0-31-ge13cda2",
"Rev": "e13cda260ce48d63ce816f4fa72b6c6cd096596d"
},
{
"ImportPath": "github.com/skratchdot/open-golang/open", "ImportPath": "github.com/skratchdot/open-golang/open",
"Rev": "ba570a111973b539baf23c918213059543b5bb6e" "Rev": "ba570a111973b539baf23c918213059543b5bb6e"
}, },
...@@ -106,6 +124,11 @@ ...@@ -106,6 +124,11 @@
"Rev": "37614ac27794505bf7867ca93aac883cadb6a5f7" "Rev": "37614ac27794505bf7867ca93aac883cadb6a5f7"
}, },
{ {
"ImportPath": "github.com/tonnerre/golang-pretty",
"Comment": "debian/0.0_git20130613-1-1-ge7fccc0",
"Rev": "e7fccc03e91bad289b96c21aa3312a220689bdd7"
},
{
"ImportPath": "github.com/vaughan0/go-ini", "ImportPath": "github.com/vaughan0/go-ini",
"Rev": "a98ad7ee00ec53921f08832bc06ecf7fd600e6a1" "Rev": "a98ad7ee00ec53921f08832bc06ecf7fd600e6a1"
}, },
......
Copyright 2012 Keith Rarick
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
This is a Go package for manipulating paragraphs of text.
See http://go.pkgdoc.org/github.com/kr/text for full documentation.
Package colwriter provides a write filter that formats
input lines in multiple columns.
The package is a straightforward translation from
/src/cmd/draw/mc.c in Plan 9 from User Space.
// Package colwriter provides a write filter that formats
// input lines in multiple columns.
//
// The package is a straightforward translation from
// /src/cmd/draw/mc.c in Plan 9 from User Space.
package colwriter
import (
"bytes"
"io"
"unicode/utf8"
)
const (
tab = 4
)
const (
// Print each input line ending in a colon ':' separately.
BreakOnColon uint = 1 << iota
)
// A Writer is a filter that arranges input lines in as many columns as will
// fit in its width. Tab '\t' chars in the input are translated to sequences
// of spaces ending at multiples of 4 positions.
//
// If BreakOnColon is set, each input line ending in a colon ':' is written
// separately.
//
// The Writer assumes that all Unicode code points have the same width; this
// may not be true in some fonts.
type Writer struct {
w io.Writer
buf []byte
width int
flag uint
}
// NewWriter allocates and initializes a new Writer writing to w.
// Parameter width controls the total number of characters on each line
// across all columns.
func NewWriter(w io.Writer, width int, flag uint) *Writer {
return &Writer{
w: w,
width: width,
flag: flag,
}
}
// Write writes p to the writer w. The only errors returned are ones
// encountered while writing to the underlying output stream.
func (w *Writer) Write(p []byte) (n int, err error) {
var linelen int
var lastWasColon bool
for i, c := range p {
w.buf = append(w.buf, c)
linelen++
if c == '\t' {
w.buf[len(w.buf)-1] = ' '
for linelen%tab != 0 {
w.buf = append(w.buf, ' ')
linelen++
}
}
if w.flag&BreakOnColon != 0 && c == ':' {
lastWasColon = true
} else if lastWasColon {
if c == '\n' {
pos := bytes.LastIndex(w.buf[:len(w.buf)-1], []byte{'\n'})
if pos < 0 {
pos = 0
}
line := w.buf[pos:]
w.buf = w.buf[:pos]
if err = w.columnate(); err != nil {
if len(line) < i {
return i - len(line), err
}
return 0, err
}
if n, err := w.w.Write(line); err != nil {
if r := len(line) - n; r < i {
return i - r, err
}
return 0, err
}
}
lastWasColon = false
}
if c == '\n' {
linelen = 0
}
}
return len(p), nil
}
// Flush should be called after the last call to Write to ensure that any data
// buffered in the Writer is written to output.
func (w *Writer) Flush() error {
return w.columnate()
}
func (w *Writer) columnate() error {
words := bytes.Split(w.buf, []byte{'\n'})
w.buf = nil
if len(words[len(words)-1]) == 0 {
words = words[:len(words)-1]
}
maxwidth := 0
for _, wd := range words {
if n := utf8.RuneCount(wd); n > maxwidth {
maxwidth = n
}
}
maxwidth++ // space char
wordsPerLine := w.width / maxwidth
if wordsPerLine <= 0 {
wordsPerLine = 1
}
nlines := (len(words) + wordsPerLine - 1) / wordsPerLine
for i := 0; i < nlines; i++ {
col := 0
endcol := 0
for j := i; j < len(words); j += nlines {
endcol += maxwidth
_, err := w.w.Write(words[j])
if err != nil {
return err
}
col += utf8.RuneCount(words[j])
if j+nlines < len(words) {
for col < endcol {
_, err := w.w.Write([]byte{' '})
if err != nil {
return err
}
col++
}
}
}
_, err := w.w.Write([]byte{'\n'})
if err != nil {
return err
}
}
return nil
}
package colwriter
import (
"bytes"
"testing"
)
var src = `
.git
.gitignore
.godir
Procfile:
README.md
api.go
apps.go
auth.go
darwin.go
data.go
dyno.go:
env.go
git.go
help.go
hkdist
linux.go
ls.go
main.go
plugin.go
run.go
scale.go
ssh.go
tail.go
term
unix.go
update.go
version.go
windows.go
`[1:]
var tests = []struct{
wid int
flag uint
src string
want string
}{
{80, 0, "", ""},
{80, 0, src, `
.git README.md darwin.go git.go ls.go scale.go unix.go
.gitignore api.go data.go help.go main.go ssh.go update.go
.godir apps.go dyno.go: hkdist plugin.go tail.go version.go
Procfile: auth.go env.go linux.go run.go term windows.go
`[1:]},
{80, BreakOnColon, src, `
.git .gitignore .godir
Procfile:
README.md api.go apps.go auth.go darwin.go data.go
dyno.go:
env.go hkdist main.go scale.go term version.go
git.go linux.go plugin.go ssh.go unix.go windows.go
help.go ls.go run.go tail.go update.go
`[1:]},
{20, 0, `
Hello
Γειά σου
안녕
今日は
`[1:], `
Hello 안녕
Γειά σου 今日は
`[1:]},
}
func TestWriter(t *testing.T) {
for _, test := range tests {
b := new(bytes.Buffer)
w := NewWriter(b, test.wid, test.flag)
if _, err := w.Write([]byte(test.src)); err != nil {
t.Error(err)
}
if err := w.Flush(); err != nil {
t.Error(err)
}
if g := b.String(); test.want != g {
t.Log("\n" + test.want)
t.Log("\n" + g)
t.Errorf("%q != %q", test.want, g)
}
}
}
// Package text provides rudimentary functions for manipulating text in
// paragraphs.
package text
package text
import (
"io"
)
// Indent inserts prefix at the beginning of each non-empty line of s. The
// end-of-line marker is NL.
func Indent(s, prefix string) string {
return string(IndentBytes([]byte(s), []byte(prefix)))
}
// IndentBytes inserts prefix at the beginning of each non-empty line of b.
// The end-of-line marker is NL.
func IndentBytes(b, prefix []byte) []byte {
var res []byte
bol := true
for _, c := range b {
if bol && c != '\n' {
res = append(res, prefix...)
}
res = append(res, c)
bol = c == '\n'
}
return res
}
// Writer indents each line of its input.
type indentWriter struct {
w io.Writer
bol bool
pre [][]byte
sel int
off int
}
// NewIndentWriter makes a new write filter that indents the input
// lines. Each line is prefixed in order with the corresponding
// element of pre. If there are more lines than elements, the last
// element of pre is repeated for each subsequent line.
func NewIndentWriter(w io.Writer, pre ...[]byte) io.Writer {
return &indentWriter{
w: w,
pre: pre,
bol: true,
}
}
// The only errors returned are from the underlying indentWriter.
func (w *indentWriter) Write(p []byte) (n int, err error) {
for _, c := range p {
if w.bol {
var i int
i, err = w.w.Write(w.pre[w.sel][w.off:])
w.off += i
if err != nil {
return n, err
}
}
_, err = w.w.Write([]byte{c})
if err != nil {
return n, err
}
n++
w.bol = c == '\n'
if w.bol {
w.off = 0
if w.sel < len(w.pre)-1 {
w.sel++
}
}
}
return n, nil
}
package text
import (
"bytes"
"testing"
)
type T struct {
inp, exp, pre string
}
var tests = []T{
{
"The quick brown fox\njumps over the lazy\ndog.\nBut not quickly.\n",
"xxxThe quick brown fox\nxxxjumps over the lazy\nxxxdog.\nxxxBut not quickly.\n",
"xxx",
},
{
"The quick brown fox\njumps over the lazy\ndog.\n\nBut not quickly.",
"xxxThe quick brown fox\nxxxjumps over the lazy\nxxxdog.\n\nxxxBut not quickly.",
"xxx",
},
}
func TestIndent(t *testing.T) {
for _, test := range tests {
got := Indent(test.inp, test.pre)
if got != test.exp {
t.Errorf("mismatch %q != %q", got, test.exp)
}
}
}
type IndentWriterTest struct {
inp, exp string
pre []string
}
var ts = []IndentWriterTest{
{
`
The quick brown fox
jumps over the lazy
dog.
But not quickly.
`[1:],
`
xxxThe quick brown fox
xxxjumps over the lazy
xxxdog.
xxxBut not quickly.
`[1:],
[]string{"xxx"},
},
{
`
The quick brown fox
jumps over the lazy
dog.
But not quickly.
`[1:],
`
xxaThe quick brown fox
xxxjumps over the lazy
xxxdog.
xxxBut not quickly.
`[1:],
[]string{"xxa", "xxx"},
},
{
`
The quick brown fox
jumps over the lazy
dog.
But not quickly.
`[1:],
`
xxaThe quick brown fox
xxbjumps over the lazy
xxcdog.
xxxBut not quickly.
`[1:],
[]string{"xxa", "xxb", "xxc", "xxx"},
},
{
`
The quick brown fox
jumps over the lazy
dog.
But not quickly.`[1:],
`
xxaThe quick brown fox
xxxjumps over the lazy
xxxdog.
xxx
xxxBut not quickly.`[1:],
[]string{"xxa", "xxx"},
},
}
func TestIndentWriter(t *testing.T) {
for _, test := range ts {
b := new(bytes.Buffer)
pre := make([][]byte, len(test.pre))
for i := range test.pre {
pre[i] = []byte(test.pre[i])
}
w := NewIndentWriter(b, pre...)
if _, err := w.Write([]byte(test.inp)); err != nil {
t.Error(err)
}
if got := b.String(); got != test.exp {
t.Errorf("mismatch %q != %q", got, test.exp)
t.Log(got)
t.Log(test.exp)
}
}
}
Command mc prints in multiple columns.
Usage: mc [-] [-N] [file...]
Mc splits the input into as many columns as will fit in N
print positions. If the output is a tty, the default N is
the number of characters in a terminal line; otherwise the
default N is 80. Under option - each input line ending in
a colon ':' is printed separately.
// Command mc prints in multiple columns.
//
// Usage: mc [-] [-N] [file...]
//
// Mc splits the input into as many columns as will fit in N
// print positions. If the output is a tty, the default N is
// the number of characters in a terminal line; otherwise the
// default N is 80. Under option - each input line ending in
// a colon ':' is printed separately.
package main
import (
"github.com/kr/pty"
"github.com/kr/text/colwriter"
"io"
"log"
"os"
"strconv"
)
func main() {
var width int
var flag uint
args := os.Args[1:]
for len(args) > 0 && len(args[0]) > 0 && args[0][0] == '-' {
if len(args[0]) > 1 {
width, _ = strconv.Atoi(args[0][1:])
} else {
flag |= colwriter.BreakOnColon
}
args = args[1:]
}
if width < 1 {
_, width, _ = pty.Getsize(os.Stdout)
}
if width < 1 {
width = 80
}
w := colwriter.NewWriter(os.Stdout, width, flag)
if len(args) > 0 {
for _, s := range args {
if f, err := os.Open(s); err == nil {
copyin(w, f)
f.Close()
} else {
log.Println(err)
}
}
} else {
copyin(w, os.Stdin)
}
}
func copyin(w *colwriter.Writer, r io.Reader) {
if _, err := io.Copy(w, r); err != nil {
log.Println(err)
}
if err := w.Flush(); err != nil {
log.Println(err)
}
}
package text
import (
"bytes"
"math"
)
var (
nl = []byte{'\n'}
sp = []byte{' '}
)
const defaultPenalty = 1e5
// Wrap wraps s into a paragraph of lines of length lim, with minimal
// raggedness.
func Wrap(s string, lim int) string {
return string(WrapBytes([]byte(s), lim))
}
// WrapBytes wraps b into a paragraph of lines of length lim, with minimal
// raggedness.
func WrapBytes(b []byte, lim int) []byte {
words := bytes.Split(bytes.Replace(bytes.TrimSpace(b), nl, sp, -1), sp)
var lines [][]byte
for _, line := range WrapWords(words, 1, lim, defaultPenalty) {
lines = append(lines, bytes.Join(line, sp))
}
return bytes.Join(lines, nl)
}
// WrapWords is the low-level line-breaking algorithm, useful if you need more
// control over the details of the text wrapping process. For most uses, either
// Wrap or WrapBytes will be sufficient and more convenient.
//
// WrapWords splits a list of words into lines with minimal "raggedness",
// treating each byte as one unit, accounting for spc units between adjacent
// words on each line, and attempting to limit lines to lim units. Raggedness
// is the total error over all lines, where error is the square of the
// difference of the length of the line and lim. Too-long lines (which only
// happen when a single word is longer than lim units) have pen penalty units
// added to the error.
func WrapWords(words [][]byte, spc, lim, pen int) [][][]byte {
n := len(words)
length := make([][]int, n)
for i := 0; i < n; i++ {
length[i] = make([]int, n)
length[i][i] = len(words[i])
for j := i + 1; j < n; j++ {
length[i][j] = length[i][j-1] + spc + len(words[j])
}
}
nbrk := make([]int, n)
cost := make([]int, n)
for i := range cost {
cost[i] = math.MaxInt32
}
for i := n - 1; i >= 0; i-- {
if length[i][n-1] <= lim {
cost[i] = 0
nbrk[i] = n
} else {
for j := i + 1; j < n; j++ {
d := lim - length[i][j-1]
c := d*d + cost[j]
if length[i][j-1] > lim {
c += pen // too-long lines get a worse penalty
}
if c < cost[i] {
cost[i] = c
nbrk[i] = j
}
}
}
}
var lines [][][]byte
i := 0
for i < n {
lines = append(lines, words[i:nbrk[i]])
i = nbrk[i]
}
return lines
}
package text
import (
"bytes"
"testing"
)
var text = "The quick brown fox jumps over the lazy dog."
func TestWrap(t *testing.T) {
exp := [][]string{
{"The", "quick", "brown", "fox"},
{"jumps", "over", "the", "lazy", "dog."},
}
words := bytes.Split([]byte(text), sp)
got := WrapWords(words, 1, 24, defaultPenalty)
if len(exp) != len(got) {
t.Fail()
}
for i := range exp {
if len(exp[i]) != len(got[i]) {
t.Fail()
}
for j := range exp[i] {
if exp[i][j] != string(got[i][j]) {
t.Fatal(i, exp[i][j], got[i][j])
}
}
}
}
func TestWrapNarrow(t *testing.T) {
exp := "The\nquick\nbrown\nfox\njumps\nover\nthe\nlazy\ndog."
if Wrap(text, 5) != exp {
t.Fail()
}
}
func TestWrapOneLine(t *testing.T) {
exp := "The quick brown fox jumps over the lazy dog."
if Wrap(text, 500) != exp {
t.Fail()
}
}
The MIT License (MIT)
Copyright (c) 2013 Mitchell Hashimoto
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
# mapstructure
mapstructure is a Go library for decoding generic map values to structures
and vice versa, while providing helpful error handling.
This library is most useful when decoding values from some data stream (JSON,
Gob, etc.) where you don't _quite_ know the structure of the underlying data
until you read a part of it. You can therefore read a `map[string]interface{}`
and use this library to decode it into the proper underlying native Go
structure.
## Installation
Standard `go get`:
```
$ go get github.com/mitchellh/mapstructure
```
## Usage & Example
For usage and examples see the [Godoc](http://godoc.org/github.com/mitchellh/mapstructure).
The `Decode` function has examples associated with it there.
## But Why?!
Go offers fantastic standard libraries for decoding formats such as JSON.
The standard method is to have a struct pre-created, and populate that struct
from the bytes of the encoded format. This is great, but the problem is if
you have configuration or an encoding that changes slightly depending on
specific fields. For example, consider this JSON:
```json
{
"type": "person",
"name": "Mitchell"
}
```
Perhaps we can't populate a specific structure without first reading
the "type" field from the JSON. We could always do two passes over the
decoding of the JSON (reading the "type" first, and the rest later).
However, it is much simpler to just decode this into a `map[string]interface{}`
structure, read the "type" key, then use something like this library
to decode it into the proper structure.
package mapstructure
import (
"reflect"
"strconv"
"strings"
)
// ComposeDecodeHookFunc creates a single DecodeHookFunc that
// automatically composes multiple DecodeHookFuncs.
//
// The composed funcs are called in order, with the result of the
// previous transformation.
func ComposeDecodeHookFunc(fs ...DecodeHookFunc) DecodeHookFunc {
return func(
f reflect.Kind,
t reflect.Kind,
data interface{}) (interface{}, error) {
var err error
for _, f1 := range fs {
data, err = f1(f, t, data)
if err != nil {
return nil, err
}
// Modify the from kind to be correct with the new data
f = getKind(reflect.ValueOf(data))
}
return data, nil
}
}
// StringToSliceHookFunc returns a DecodeHookFunc that converts
// string to []string by splitting on the given sep.
func StringToSliceHookFunc(sep string) DecodeHookFunc {
return func(
f reflect.Kind,
t reflect.Kind,
data interface{}) (interface{}, error) {
if f != reflect.String || t != reflect.Slice {
return data, nil
}
raw := data.(string)
if raw == "" {
return []string{}, nil
}
return strings.Split(raw, sep), nil
}
}
func WeaklyTypedHook(
f reflect.Kind,
t reflect.Kind,
data interface{}) (interface{}, error) {
dataVal := reflect.ValueOf(data)
switch t {
case reflect.String:
switch f {
case reflect.Bool:
if dataVal.Bool() {
return "1", nil
} else {
return "0", nil
}
case reflect.Float32:
return strconv.FormatFloat(dataVal.Float(), 'f', -1, 64), nil
case reflect.Int:
return strconv.FormatInt(dataVal.Int(), 10), nil
case reflect.Slice:
dataType := dataVal.Type()
elemKind := dataType.Elem().Kind()
if elemKind == reflect.Uint8 {
return string(dataVal.Interface().([]uint8)), nil
}
case reflect.Uint:
return strconv.FormatUint(dataVal.Uint(), 10), nil
}
}
return data, nil
}
package mapstructure
import (
"errors"
"reflect"
"testing"
)
func TestComposeDecodeHookFunc(t *testing.T) {
f1 := func(
f reflect.Kind,
t reflect.Kind,
data interface{}) (interface{}, error) {
return data.(string) + "foo", nil
}
f2 := func(
f reflect.Kind,
t reflect.Kind,
data interface{}) (interface{}, error) {
return data.(string) + "bar", nil
}
f := ComposeDecodeHookFunc(f1, f2)
result, err := f(reflect.String, reflect.Slice, "")
if err != nil {
t.Fatalf("bad: %s", err)
}
if result.(string) != "foobar" {
t.Fatalf("bad: %#v", result)
}
}
func TestComposeDecodeHookFunc_err(t *testing.T) {
f1 := func(reflect.Kind, reflect.Kind, interface{}) (interface{}, error) {
return nil, errors.New("foo")
}
f2 := func(reflect.Kind, reflect.Kind, interface{}) (interface{}, error) {
panic("NOPE")
}
f := ComposeDecodeHookFunc(f1, f2)
_, err := f(reflect.String, reflect.Slice, 42)
if err.Error() != "foo" {
t.Fatalf("bad: %s", err)
}
}
func TestComposeDecodeHookFunc_kinds(t *testing.T) {
var f2From reflect.Kind
f1 := func(
f reflect.Kind,
t reflect.Kind,
data interface{}) (interface{}, error) {
return int(42), nil
}
f2 := func(
f reflect.Kind,
t reflect.Kind,
data interface{}) (interface{}, error) {
f2From = f
return data, nil
}
f := ComposeDecodeHookFunc(f1, f2)
_, err := f(reflect.String, reflect.Slice, "")
if err != nil {
t.Fatalf("bad: %s", err)
}
if f2From != reflect.Int {
t.Fatalf("bad: %#v", f2From)
}
}
func TestStringToSliceHookFunc(t *testing.T) {
f := StringToSliceHookFunc(",")
cases := []struct {
f, t reflect.Kind
data interface{}
result interface{}
err bool
}{
{reflect.Slice, reflect.Slice, 42, 42, false},
{reflect.String, reflect.String, 42, 42, false},
{
reflect.String,
reflect.Slice,
"foo,bar,baz",
[]string{"foo", "bar", "baz"},
false,
},
{
reflect.String,
reflect.Slice,
"",
[]string{},
false,
},
}
for i, tc := range cases {
actual, err := f(tc.f, tc.t, tc.data)
if tc.err != (err != nil) {
t.Fatalf("case %d: expected err %#v", i, tc.err)
}
if !reflect.DeepEqual(actual, tc.result) {
t.Fatalf(
"case %d: expected %#v, got %#v",
i, tc.result, actual)
}
}
}
func TestWeaklyTypedHook(t *testing.T) {
var f DecodeHookFunc = WeaklyTypedHook
cases := []struct {
f, t reflect.Kind
data interface{}
result interface{}
err bool
}{
// TO STRING
{
reflect.Bool,
reflect.String,
false,
"0",
false,
},
{
reflect.Bool,
reflect.String,
true,
"1",
false,
},
{
reflect.Float32,
reflect.String,
float32(7),
"7",
false,
},
{
reflect.Int,
reflect.String,
int(7),
"7",
false,
},
{
reflect.Slice,
reflect.String,
[]uint8("foo"),
"foo",
false,
},
{
reflect.Uint,
reflect.String,
uint(7),
"7",
false,
},
}
for i, tc := range cases {
actual, err := f(tc.f, tc.t, tc.data)
if tc.err != (err != nil) {
t.Fatalf("case %d: expected err %#v", i, tc.err)
}
if !reflect.DeepEqual(actual, tc.result) {
t.Fatalf(
"case %d: expected %#v, got %#v",
i, tc.result, actual)
}
}
}
package mapstructure
import (
"fmt"
"strings"
)
// Error implements the error interface and can represents multiple
// errors that occur in the course of a single decode.
type Error struct {
Errors []string
}
func (e *Error) Error() string {
points := make([]string, len(e.Errors))
for i, err := range e.Errors {
points[i] = fmt.Sprintf("* %s", err)
}
return fmt.Sprintf(
"%d error(s) decoding:\n\n%s",
len(e.Errors), strings.Join(points, "\n"))
}
func appendErrors(errors []string, err error) []string {
switch e := err.(type) {
case *Error:
return append(errors, e.Errors...)
default:
return append(errors, e.Error())
}
}
package mapstructure
import (
"testing"
)
func Benchmark_Decode(b *testing.B) {
type Person struct {
Name string
Age int
Emails []string
Extra map[string]string
}
input := map[string]interface{}{
"name": "Mitchell",
"age": 91,
"emails": []string{"one", "two", "three"},
"extra": map[string]string{
"twitter": "mitchellh",
},
}
var result Person
for i := 0; i < b.N; i++ {
Decode(input, &result)
}
}
func Benchmark_DecodeBasic(b *testing.B) {
input := map[string]interface{}{
"vstring": "foo",
"vint": 42,
"Vuint": 42,
"vbool": true,
"Vfloat": 42.42,
"vsilent": true,
"vdata": 42,
}
var result Basic
for i := 0; i < b.N; i++ {
Decode(input, &result)
}
}
func Benchmark_DecodeEmbedded(b *testing.B) {
input := map[string]interface{}{
"vstring": "foo",
"Basic": map[string]interface{}{
"vstring": "innerfoo",
},
"vunique": "bar",
}
var result Embedded
for i := 0; i < b.N; i++ {
Decode(input, &result)
}
}
func Benchmark_DecodeTypeConversion(b *testing.B) {
input := map[string]interface{}{
"IntToFloat": 42,
"IntToUint": 42,
"IntToBool": 1,
"IntToString": 42,
"UintToInt": 42,
"UintToFloat": 42,
"UintToBool": 42,
"UintToString": 42,
"BoolToInt": true,
"BoolToUint": true,
"BoolToFloat": true,
"BoolToString": true,
"FloatToInt": 42.42,
"FloatToUint": 42.42,
"FloatToBool": 42.42,
"FloatToString": 42.42,
"StringToInt": "42",
"StringToUint": "42",
"StringToBool": "1",
"StringToFloat": "42.42",
"SliceToMap": []interface{}{},
"MapToSlice": map[string]interface{}{},
}
var resultStrict TypeConversionResult
for i := 0; i < b.N; i++ {
Decode(input, &resultStrict)
}
}
func Benchmark_DecodeMap(b *testing.B) {
input := map[string]interface{}{
"vfoo": "foo",
"vother": map[interface{}]interface{}{
"foo": "foo",
"bar": "bar",
},
}
var result Map
for i := 0; i < b.N; i++ {
Decode(input, &result)
}
}
func Benchmark_DecodeMapOfStruct(b *testing.B) {
input := map[string]interface{}{
"value": map[string]interface{}{
"foo": map[string]string{"vstring": "one"},
"bar": map[string]string{"vstring": "two"},
},
}
var result MapOfStruct
for i := 0; i < b.N; i++ {
Decode(input, &result)
}
}
func Benchmark_DecodeSlice(b *testing.B) {
input := map[string]interface{}{
"vfoo": "foo",
"vbar": []string{"foo", "bar", "baz"},
}
var result Slice
for i := 0; i < b.N; i++ {
Decode(input, &result)
}
}
func Benchmark_DecodeSliceOfStruct(b *testing.B) {
input := map[string]interface{}{
"value": []map[string]interface{}{
{"vstring": "one"},
{"vstring": "two"},
},
}
var result SliceOfStruct
for i := 0; i < b.N; i++ {
Decode(input, &result)
}
}
func Benchmark_DecodeWeaklyTypedInput(b *testing.B) {
type Person struct {
Name string
Age int
Emails []string
}
// This input can come from anywhere, but typically comes from
// something like decoding JSON, generated by a weakly typed language
// such as PHP.
input := map[string]interface{}{
"name": 123, // number => string
"age": "42", // string => number
"emails": map[string]interface{}{}, // empty map => empty array
}
var result Person
config := &DecoderConfig{
WeaklyTypedInput: true,
Result: &result,
}
decoder, err := NewDecoder(config)
if err != nil {
panic(err)
}
for i := 0; i < b.N; i++ {
decoder.Decode(input)
}
}
func Benchmark_DecodeMetadata(b *testing.B) {
type Person struct {
Name string
Age int
}
input := map[string]interface{}{
"name": "Mitchell",
"age": 91,
"email": "foo@bar.com",
}
var md Metadata
var result Person
config := &DecoderConfig{
Metadata: &md,
Result: &result,
}
decoder, err := NewDecoder(config)
if err != nil {
panic(err)
}
for i := 0; i < b.N; i++ {
decoder.Decode(input)
}
}
func Benchmark_DecodeMetadataEmbedded(b *testing.B) {
input := map[string]interface{}{
"vstring": "foo",
"vunique": "bar",
}
var md Metadata
var result EmbeddedSquash
config := &DecoderConfig{
Metadata: &md,
Result: &result,
}
decoder, err := NewDecoder(config)
if err != nil {
b.Fatalf("err: %s", err)
}
for i := 0; i < b.N; i++ {
decoder.Decode(input)
}
}
func Benchmark_DecodeTagged(b *testing.B) {
input := map[string]interface{}{
"foo": "bar",
"bar": "value",
}
var result Tagged
for i := 0; i < b.N; i++ {
Decode(input, &result)
}
}
package mapstructure
import "testing"
// GH-1
func TestDecode_NilValue(t *testing.T) {
input := map[string]interface{}{
"vfoo": nil,
"vother": nil,
}
var result Map
err := Decode(input, &result)
if err != nil {
t.Fatalf("should not error: %s", err)
}
if result.Vfoo != "" {
t.Fatalf("value should be default: %s", result.Vfoo)
}
if result.Vother != nil {
t.Fatalf("Vother should be nil: %s", result.Vother)
}
}
// GH-10
func TestDecode_mapInterfaceInterface(t *testing.T) {
input := map[interface{}]interface{}{
"vfoo": nil,
"vother": nil,
}
var result Map
err := Decode(input, &result)
if err != nil {
t.Fatalf("should not error: %s", err)
}
if result.Vfoo != "" {
t.Fatalf("value should be default: %s", result.Vfoo)
}
if result.Vother != nil {
t.Fatalf("Vother should be nil: %s", result.Vother)
}
}
package mapstructure
import (
"fmt"
)
func ExampleDecode() {
type Person struct {
Name string
Age int
Emails []string
Extra map[string]string
}
// This input can come from anywhere, but typically comes from
// something like decoding JSON where we're not quite sure of the
// struct initially.
input := map[string]interface{}{
"name": "Mitchell",
"age": 91,
"emails": []string{"one", "two", "three"},
"extra": map[string]string{
"twitter": "mitchellh",
},
}
var result Person
err := Decode(input, &result)
if err != nil {
panic(err)
}
fmt.Printf("%#v", result)
// Output:
// mapstructure.Person{Name:"Mitchell", Age:91, Emails:[]string{"one", "two", "three"}, Extra:map[string]string{"twitter":"mitchellh"}}
}
func ExampleDecode_errors() {
type Person struct {
Name string
Age int
Emails []string
Extra map[string]string
}
// This input can come from anywhere, but typically comes from
// something like decoding JSON where we're not quite sure of the
// struct initially.
input := map[string]interface{}{
"name": 123,
"age": "bad value",
"emails": []int{1, 2, 3},
}
var result Person
err := Decode(input, &result)
if err == nil {
panic("should have an error")
}
fmt.Println(err.Error())
// Output:
// 5 error(s) decoding:
//
// * 'Name' expected type 'string', got unconvertible type 'int'
// * 'Age' expected type 'int', got unconvertible type 'string'
// * 'Emails[0]' expected type 'string', got unconvertible type 'int'
// * 'Emails[1]' expected type 'string', got unconvertible type 'int'
// * 'Emails[2]' expected type 'string', got unconvertible type 'int'
}
func ExampleDecode_metadata() {
type Person struct {
Name string
Age int
}
// This input can come from anywhere, but typically comes from
// something like decoding JSON where we're not quite sure of the
// struct initially.
input := map[string]interface{}{
"name": "Mitchell",
"age": 91,
"email": "foo@bar.com",
}
// For metadata, we make a more advanced DecoderConfig so we can
// more finely configure the decoder that is used. In this case, we
// just tell the decoder we want to track metadata.
var md Metadata
var result Person
config := &DecoderConfig{
Metadata: &md,
Result: &result,
}
decoder, err := NewDecoder(config)
if err != nil {
panic(err)
}
if err := decoder.Decode(input); err != nil {
panic(err)
}
fmt.Printf("Unused keys: %#v", md.Unused)
// Output:
// Unused keys: []string{"email"}
}
func ExampleDecode_weaklyTypedInput() {
type Person struct {
Name string
Age int
Emails []string
}
// This input can come from anywhere, but typically comes from
// something like decoding JSON, generated by a weakly typed language
// such as PHP.
input := map[string]interface{}{
"name": 123, // number => string
"age": "42", // string => number
"emails": map[string]interface{}{}, // empty map => empty array
}
var result Person
config := &DecoderConfig{
WeaklyTypedInput: true,
Result: &result,
}
decoder, err := NewDecoder(config)
if err != nil {
panic(err)
}
err = decoder.Decode(input)
if err != nil {
panic(err)
}
fmt.Printf("%#v", result)
// Output: mapstructure.Person{Name:"123", Age:42, Emails:[]string{}}
}
func ExampleDecode_tags() {
// Note that the mapstructure tags defined in the struct type
// can indicate which fields the values are mapped to.
type Person struct {
Name string `mapstructure:"person_name"`
Age int `mapstructure:"person_age"`
}
input := map[string]interface{}{
"person_name": "Mitchell",
"person_age": 91,
}
var result Person
err := Decode(input, &result)
if err != nil {
panic(err)
}
fmt.Printf("%#v", result)
// Output:
// mapstructure.Person{Name:"Mitchell", Age:91}
}
# perigee
Perigee provides a REST client that, while it should be generic enough to use with most any RESTful API, is nonetheless optimized to the needs of the OpenStack APIs.
Perigee grew out of the need to refactor out common API access code from the [gorax](http://github.com/racker/gorax) project.
Several things influenced the name of the project.
Numerous elements of the OpenStack ecosystem are named after astronomical artifacts.
Additionally, perigee occurs when two orbiting bodies are closest to each other.
Perigee seemed appropriate for something aiming to bring OpenStack and other RESTful services closer to the end-user.
**This library is still in the very early stages of development. Unless you want to contribute, it probably isn't what you want**
## Installation and Testing
To install:
```bash
go get github.com/racker/perigee
```
To run unit tests:
```bash
go test github.com/racker/perigee
```
## Contributing
The following guidelines are preliminary, as this project is just starting out.
However, this should serve as a working first-draft.
### Branching
The master branch must always be a valid build.
The `go get` command will not work otherwise.
Therefore, development must occur on a different branch.
When creating a feature branch, do so off the master branch:
```bash
git checkout master
git pull
git checkout -b featureBranch
git checkout -b featureBranch-wip # optional
```
Perform all your editing and testing in the WIP-branch.
Feel free to make as many commits as you see fit.
You may even open "WIP" pull requests from your feature branch to seek feedback.
WIP pull requests will **never** be merged, however.
To get code merged, you'll need to "squash" your changes into one or more clean commits in the feature branch.
These steps should be followed:
```bash
git checkout featureBranch
git merge --squash featureBranch-wip
git commit -a
git push origin featureBranch
```
You may now open a nice, clean, self-contained pull request from featureBranch to master.
The `git commit -a` command above will open a text editor so that
you may provide a comprehensive description of the changes.
In general, when submitting a pull request against master,
be sure to answer the following questions:
- What is the problem?
- Why is it a problem?
- What is your solution?
- How does your solution work? (Recommended for non-trivial changes.)
- Why should we use your solution over someone elses? (Recommended especially if multiple solutions being discussed.)
Remember that monster-sized pull requests are a bear to code-review,
so having helpful commit logs are an absolute must to review changes as quickly as possible.
Finally, (s)he who breaks master is ultimately responsible for fixing master.
### Source Representation
The Go community firmly believes in a consistent representation for all Go source code.
We do too.
Make sure all source code is passed through "go fmt" *before* you create your pull request.
Please note, however, that we fully acknowledge and recognize that we no longer rely upon punch-cards for representing source files.
Therefore, no 80-column limit exists.
However, if a line exceeds 132 columns, you may want to consider splitting the line.
### Unit and Integration Tests
Pull requests that include non-trivial code changes without accompanying unit tests will be flatly rejected.
While we have no way of enforcing this practice,
you can ensure your code is thoroughly tested by always [writing tests first by intention.](http://en.wikipedia.org/wiki/Test-driven_development)
When creating a pull request, if even one test fails, the PR will be rejected.
Make sure all unit tests pass.
Make sure all integration tests pass.
### Documentation
Private functions and methods which are obvious to anyone unfamiliar with gorax needn't be accompanied by documentation.
However, this is a code-smell; if submitting a PR, expect to justify your decision.
Public functions, regardless of how obvious, **must** have accompanying godoc-style documentation.
This is not to suggest you should provide a tome for each function, however.
Sometimes a link to more information is more appropriate, provided the link is stable, reliable, and pertinent.
Changing documentation often results in bizarre diffs in pull requests, due to text often spanning multiple lines.
To work around this, put [one logical thought or sentence on a single line.](http://rhodesmill.org/brandon/2012/one-sentence-per-line/)
While this looks weird in a plain-text editor,
remember that both godoc and HTML viewers will reflow text.
The source code and its comments should be easy to edit with minimal diff pollution.
Let software dedicated to presenting the documentation to human readers deal with its presentation.
## Examples
t.b.d.
// vim: ts=8 sw=8 noet ai
package perigee
import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"strings"
)
// The UnexpectedResponseCodeError structure represents a mismatch in understanding between server and client in terms of response codes.
// Most often, this is due to an actual error condition (e.g., getting a 404 for a resource when you expect a 200).
// However, it needn't always be the case (e.g., getting a 204 (No Content) response back when a 200 is expected).
type UnexpectedResponseCodeError struct {
Url string
Expected []int
Actual int
Body []byte
}
func (err *UnexpectedResponseCodeError) Error() string {
return fmt.Sprintf("Expected HTTP response code %d when accessing URL(%s); got %d instead with the following body:\n%s", err.Expected, err.Url, err.Actual, string(err.Body))
}
// Request issues an HTTP request, marshaling parameters, and unmarshaling results, as configured in the provided Options parameter.
// The Response structure returned, if any, will include accumulated results recovered from the HTTP server.
// See the Response structure for more details.
func Request(method string, url string, opts Options) (*Response, error) {
var body io.Reader
var response Response
client := opts.CustomClient
if client == nil {
client = new(http.Client)
}
contentType := opts.ContentType
body = nil
if opts.ReqBody != nil {
if contentType == "" {
contentType = "application/json"
}
if contentType == "application/json" {
bodyText, err := json.Marshal(opts.ReqBody)
if err != nil {
return nil, err
}
body = strings.NewReader(string(bodyText))
if opts.DumpReqJson {
log.Printf("Making request:\n%#v\n", string(bodyText))
}
} else {
// assume opts.ReqBody implements the correct interface
body = opts.ReqBody.(io.Reader)
}
}
req, err := http.NewRequest(method, url, body)
if err != nil {
return nil, err
}
if contentType != "" {
req.Header.Add("Content-Type", contentType)
}
if opts.ContentLength > 0 {
req.ContentLength = opts.ContentLength
req.Header.Add("Content-Length", string(opts.ContentLength))
}
if opts.MoreHeaders != nil {
for k, v := range opts.MoreHeaders {
req.Header.Add(k, v)
}
}
if accept := req.Header.Get("Accept"); accept == "" {
accept = opts.Accept
if accept == "" {
accept = "application/json"
}
req.Header.Add("Accept", accept)
}
if opts.SetHeaders != nil {
err = opts.SetHeaders(req)
if err != nil {
return &response, err
}
}
httpResponse, err := client.Do(req)
if httpResponse != nil {
response.HttpResponse = *httpResponse
response.StatusCode = httpResponse.StatusCode
}
if err != nil {
return &response, err
}
// This if-statement is legacy code, preserved for backward compatibility.
if opts.StatusCode != nil {
*opts.StatusCode = httpResponse.StatusCode
}
acceptableResponseCodes := opts.OkCodes
if len(acceptableResponseCodes) != 0 {
if not_in(httpResponse.StatusCode, acceptableResponseCodes) {
b, _ := ioutil.ReadAll(httpResponse.Body)
httpResponse.Body.Close()
return &response, &UnexpectedResponseCodeError{
Url: url,
Expected: acceptableResponseCodes,
Actual: httpResponse.StatusCode,
Body: b,
}
}
}
if opts.Results != nil {
defer httpResponse.Body.Close()
jsonResult, err := ioutil.ReadAll(httpResponse.Body)
response.JsonResult = jsonResult
if err != nil {
return &response, err
}
err = json.Unmarshal(jsonResult, opts.Results)
// This if-statement is legacy code, preserved for backward compatibility.
if opts.ResponseJson != nil {
*opts.ResponseJson = jsonResult
}
}
return &response, err
}
// not_in returns false if, and only if, the provided needle is _not_
// in the given set of integers.
func not_in(needle int, haystack []int) bool {
for _, straw := range haystack {
if needle == straw {
return false
}
}
return true
}
// Post makes a POST request against a server using the provided HTTP client.
// The url must be a fully-formed URL string.
// DEPRECATED. Use Request() instead.
func Post(url string, opts Options) error {
r, err := Request("POST", url, opts)
if opts.Response != nil {
*opts.Response = r
}
return err
}
// Get makes a GET request against a server using the provided HTTP client.
// The url must be a fully-formed URL string.
// DEPRECATED. Use Request() instead.
func Get(url string, opts Options) error {
r, err := Request("GET", url, opts)
if opts.Response != nil {
*opts.Response = r
}
return err
}
// Delete makes a DELETE request against a server using the provided HTTP client.
// The url must be a fully-formed URL string.
// DEPRECATED. Use Request() instead.
func Delete(url string, opts Options) error {
r, err := Request("DELETE", url, opts)
if opts.Response != nil {
*opts.Response = r
}
return err
}
// Put makes a PUT request against a server using the provided HTTP client.
// The url must be a fully-formed URL string.
// DEPRECATED. Use Request() instead.
func Put(url string, opts Options) error {
r, err := Request("PUT", url, opts)
if opts.Response != nil {
*opts.Response = r
}
return err
}
// Options describes a set of optional parameters to the various request calls.
//
// The custom client can be used for a variety of purposes beyond selecting encrypted versus unencrypted channels.
// Transports can be defined to provide augmented logging, header manipulation, et. al.
//
// If the ReqBody field is provided, it will be embedded as a JSON object.
// Otherwise, provide nil.
//
// If JSON output is to be expected from the response,
// provide either a pointer to the container structure in Results,
// or a pointer to a nil-initialized pointer variable.
// The latter method will cause the unmarshaller to allocate the container type for you.
// If no response is expected, provide a nil Results value.
//
// The MoreHeaders map, if non-nil or empty, provides a set of headers to add to those
// already present in the request. At present, only Accepted and Content-Type are set
// by default.
//
// OkCodes provides a set of acceptable, positive responses.
//
// If provided, StatusCode specifies a pointer to an integer, which will receive the
// returned HTTP status code, successful or not. DEPRECATED; use the Response.StatusCode field instead for new software.
//
// ResponseJson, if specified, provides a means for returning the raw JSON. This is
// most useful for diagnostics. DEPRECATED; use the Response.JsonResult field instead for new software.
//
// DumpReqJson, if set to true, will cause the request to appear to stdout for debugging purposes.
// This attribute may be removed at any time in the future; DO NOT use this attribute in production software.
//
// Response, if set, provides a way to communicate the complete set of HTTP response, raw JSON, status code, and
// other useful attributes back to the caller. Note that the Request() method returns a Response structure as part
// of its public interface; you don't need to set the Response field here to use this structure. The Response field
// exists primarily for legacy or deprecated functions.
//
// SetHeaders allows the caller to provide code to set any custom headers programmatically. Typically, this
// facility can invoke, e.g., SetBasicAuth() on the request to easily set up authentication.
// Any error generated will terminate the request and will propegate back to the caller.
type Options struct {
CustomClient *http.Client
ReqBody interface{}
Results interface{}
MoreHeaders map[string]string
OkCodes []int
StatusCode *int `DEPRECATED`
DumpReqJson bool `UNSUPPORTED`
ResponseJson *[]byte `DEPRECATED`
Response **Response
ContentType string `json:"Content-Type,omitempty"`
ContentLength int64 `json:"Content-Length,omitempty"`
Accept string `json:"Accept,omitempty"`
SetHeaders func(r *http.Request) error
}
// Response contains return values from the various request calls.
//
// HttpResponse will return the http response from the request call.
// Note: HttpResponse.Body is always closed and will not be available from this return value.
//
// StatusCode specifies the returned HTTP status code, successful or not.
//
// If Results is specified in the Options:
// - JsonResult will contain the raw return from the request call
// This is most useful for diagnostics.
// - Result will contain the unmarshalled json either in the Result passed in
// or the unmarshaller will allocate the container type for you.
type Response struct {
HttpResponse http.Response
JsonResult []byte
Results interface{}
StatusCode int
}
package perigee
import (
"bytes"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
func TestNormal(t *testing.T) {
handler := http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("testing"))
})
ts := httptest.NewServer(handler)
defer ts.Close()
response, err := Request("GET", ts.URL, Options{})
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if response.StatusCode != 200 {
t.Fatalf("response code %d is not 200", response.StatusCode)
}
}
func TestOKCodes(t *testing.T) {
expectCode := 201
handler := http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(expectCode)
w.Write([]byte("testing"))
})
ts := httptest.NewServer(handler)
defer ts.Close()
options := Options{
OkCodes: []int{expectCode},
}
results, err := Request("GET", ts.URL, options)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if results.StatusCode != expectCode {
t.Fatalf("response code %d is not %d", results.StatusCode, expectCode)
}
}
func TestLocation(t *testing.T) {
newLocation := "http://www.example.com"
handler := http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Location", newLocation)
w.Write([]byte("testing"))
})
ts := httptest.NewServer(handler)
defer ts.Close()
response, err := Request("GET", ts.URL, Options{})
if err != nil {
t.Fatalf("should not have error: %s", err)
}
location, err := response.HttpResponse.Location()
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if location.String() != newLocation {
t.Fatalf("location returned \"%s\" is not \"%s\"", location.String(), newLocation)
}
}
func TestHeaders(t *testing.T) {
newLocation := "http://www.example.com"
handler := http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Location", newLocation)
w.Write([]byte("testing"))
})
ts := httptest.NewServer(handler)
defer ts.Close()
response, err := Request("GET", ts.URL, Options{})
if err != nil {
t.Fatalf("should not have error: %s", err)
}
location := response.HttpResponse.Header.Get("Location")
if location == "" {
t.Fatalf("Location should not empty")
}
if location != newLocation {
t.Fatalf("location returned \"%s\" is not \"%s\"", location, newLocation)
}
}
func TestCustomHeaders(t *testing.T) {
var contentType, accept, contentLength string
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
m := map[string][]string(r.Header)
contentType = m["Content-Type"][0]
accept = m["Accept"][0]
contentLength = m["Content-Length"][0]
})
ts := httptest.NewServer(handler)
defer ts.Close()
_, err := Request("GET", ts.URL, Options{
ContentLength: 5,
ContentType: "x-application/vb",
Accept: "x-application/c",
ReqBody: strings.NewReader("Hello"),
})
if err != nil {
t.Fatalf(err.Error())
}
if contentType != "x-application/vb" {
t.Fatalf("I expected x-application/vb; got ", contentType)
}
if contentLength != "5" {
t.Fatalf("I expected 5 byte content length; got ", contentLength)
}
if accept != "x-application/c" {
t.Fatalf("I expected x-application/c; got ", accept)
}
}
func TestJson(t *testing.T) {
newLocation := "http://www.example.com"
jsonBytes := []byte(`{"foo": {"bar": "baz"}}`)
handler := http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Location", newLocation)
w.Write(jsonBytes)
})
ts := httptest.NewServer(handler)
defer ts.Close()
type Data struct {
Foo struct {
Bar string `json:"bar"`
} `json:"foo"`
}
var data Data
response, err := Request("GET", ts.URL, Options{Results: &data})
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if bytes.Compare(jsonBytes, response.JsonResult) != 0 {
t.Fatalf("json returned \"%s\" is not \"%s\"", response.JsonResult, jsonBytes)
}
if data.Foo.Bar != "baz" {
t.Fatalf("Results returned %v", data)
}
}
func TestSetHeaders(t *testing.T) {
var wasCalled bool
handler := http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hi"))
})
ts := httptest.NewServer(handler)
defer ts.Close()
_, err := Request("GET", ts.URL, Options{
SetHeaders: func(r *http.Request) error {
wasCalled = true
return nil
},
})
if err != nil {
t.Fatal(err)
}
if !wasCalled {
t.Fatal("I expected header setter callback to be called, but it wasn't")
}
myError := fmt.Errorf("boo")
_, err = Request("GET", ts.URL, Options{
SetHeaders: func(r *http.Request) error {
return myError
},
})
if err != myError {
t.Fatal("I expected errors to propegate back to the caller.")
}
}
func TestBodilessMethodsAreSentWithoutContentHeaders(t *testing.T) {
var h map[string][]string
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
h = r.Header
})
ts := httptest.NewServer(handler)
defer ts.Close()
_, err := Request("GET", ts.URL, Options{})
if err != nil {
t.Fatalf(err.Error())
}
if len(h["Content-Type"]) != 0 {
t.Fatalf("I expected nothing for Content-Type but got ", h["Content-Type"])
}
if len(h["Content-Length"]) != 0 {
t.Fatalf("I expected nothing for Content-Length but got ", h["Content-Length"])
}
}
# 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
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
Contributors
============
Samuel A. Falvo II <sam.falvo@rackspace.com>
Glen Campbell <glen.campbell@rackspace.com>
Jesse Noller <jesse.noller@rackspace.com>
== 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
// +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)
}
}
// +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)
}
}
// +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)
}
}
}
// +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
}
// +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)
}
}
})
})
}
// +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)
}
}
})
})
}
// +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)
}
}
})
})
}
// +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.")
}
})
})
}
// +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")
}
})
})
}
// +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)
}
}
// +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)
}
}
// +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)
}
}
// +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)
}
}
// +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)
}
}
// +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...)
}
}
// +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)
}
}
})
})
}
// +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)
}
})
})
}
// +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)
}
}
// +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)
}
}
// +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...)
}
}
// +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)
}
})
})
}
// +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")
}
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
}
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
}
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
}
}
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"`
}
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
}
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
}
}
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")
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"`
}
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"`
}
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
}
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"`
}
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"`
}
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
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment