diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index 64c77bd0b0..d8bfb55165 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -118,6 +118,20 @@ "ImportPath": "github.com/smartystreets/go-aws-auth", "Rev": "1f0db8c0ee6362470abe06a94e3385927ed72a4b" }, + { + "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": "v1.0.0-232-g2e7ab37", + "Rev": "2e7ab378257b8723e02cbceac7410be4db286436" + }, { "ImportPath": "github.com/tent/http-link-go", "Rev": "ac974c61c2f990f4115b119354b5e0b47550e888" diff --git a/Godeps/_workspace/src/github.com/mitchellh/mapstructure/LICENSE b/Godeps/_workspace/src/github.com/mitchellh/mapstructure/LICENSE new file mode 100644 index 0000000000..f9c841a51e --- /dev/null +++ b/Godeps/_workspace/src/github.com/mitchellh/mapstructure/LICENSE @@ -0,0 +1,21 @@ +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. diff --git a/Godeps/_workspace/src/github.com/mitchellh/mapstructure/README.md b/Godeps/_workspace/src/github.com/mitchellh/mapstructure/README.md new file mode 100644 index 0000000000..659d6885fc --- /dev/null +++ b/Godeps/_workspace/src/github.com/mitchellh/mapstructure/README.md @@ -0,0 +1,46 @@ +# 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. diff --git a/Godeps/_workspace/src/github.com/mitchellh/mapstructure/decode_hooks.go b/Godeps/_workspace/src/github.com/mitchellh/mapstructure/decode_hooks.go new file mode 100644 index 0000000000..087a392b91 --- /dev/null +++ b/Godeps/_workspace/src/github.com/mitchellh/mapstructure/decode_hooks.go @@ -0,0 +1,84 @@ +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 +} diff --git a/Godeps/_workspace/src/github.com/mitchellh/mapstructure/decode_hooks_test.go b/Godeps/_workspace/src/github.com/mitchellh/mapstructure/decode_hooks_test.go new file mode 100644 index 0000000000..b417deeb64 --- /dev/null +++ b/Godeps/_workspace/src/github.com/mitchellh/mapstructure/decode_hooks_test.go @@ -0,0 +1,191 @@ +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) + } + } +} diff --git a/Godeps/_workspace/src/github.com/mitchellh/mapstructure/error.go b/Godeps/_workspace/src/github.com/mitchellh/mapstructure/error.go new file mode 100644 index 0000000000..3460799f80 --- /dev/null +++ b/Godeps/_workspace/src/github.com/mitchellh/mapstructure/error.go @@ -0,0 +1,32 @@ +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()) + } +} diff --git a/Godeps/_workspace/src/github.com/mitchellh/mapstructure/mapstructure.go b/Godeps/_workspace/src/github.com/mitchellh/mapstructure/mapstructure.go new file mode 100644 index 0000000000..381ba5d487 --- /dev/null +++ b/Godeps/_workspace/src/github.com/mitchellh/mapstructure/mapstructure.go @@ -0,0 +1,704 @@ +// The mapstructure package exposes functionality to convert an +// abitrary map[string]interface{} into a native Go structure. +// +// The Go structure can be arbitrarily complex, containing slices, +// other structs, etc. and the decoder will properly decode nested +// maps and so on into the proper structures in the native Go struct. +// See the examples to see what the decoder is capable of. +package mapstructure + +import ( + "errors" + "fmt" + "reflect" + "sort" + "strconv" + "strings" +) + +// DecodeHookFunc is the callback function that can be used for +// data transformations. See "DecodeHook" in the DecoderConfig +// struct. +type DecodeHookFunc func( + from reflect.Kind, + to reflect.Kind, + data interface{}) (interface{}, error) + +// DecoderConfig is the configuration that is used to create a new decoder +// and allows customization of various aspects of decoding. +type DecoderConfig struct { + // DecodeHook, if set, will be called before any decoding and any + // type conversion (if WeaklyTypedInput is on). This lets you modify + // the values before they're set down onto the resulting struct. + // + // If an error is returned, the entire decode will fail with that + // error. + DecodeHook DecodeHookFunc + + // If ErrorUnused is true, then it is an error for there to exist + // keys in the original map that were unused in the decoding process + // (extra keys). + ErrorUnused bool + + // If WeaklyTypedInput is true, the decoder will make the following + // "weak" conversions: + // + // - bools to string (true = "1", false = "0") + // - numbers to string (base 10) + // - bools to int/uint (true = 1, false = 0) + // - strings to int/uint (base implied by prefix) + // - int to bool (true if value != 0) + // - string to bool (accepts: 1, t, T, TRUE, true, True, 0, f, F, + // FALSE, false, False. Anything else is an error) + // - empty array = empty map and vice versa + // + WeaklyTypedInput bool + + // Metadata is the struct that will contain extra metadata about + // the decoding. If this is nil, then no metadata will be tracked. + Metadata *Metadata + + // Result is a pointer to the struct that will contain the decoded + // value. + Result interface{} + + // The tag name that mapstructure reads for field names. This + // defaults to "mapstructure" + TagName string +} + +// A Decoder takes a raw interface value and turns it into structured +// data, keeping track of rich error information along the way in case +// anything goes wrong. Unlike the basic top-level Decode method, you can +// more finely control how the Decoder behaves using the DecoderConfig +// structure. The top-level Decode method is just a convenience that sets +// up the most basic Decoder. +type Decoder struct { + config *DecoderConfig +} + +// Metadata contains information about decoding a structure that +// is tedious or difficult to get otherwise. +type Metadata struct { + // Keys are the keys of the structure which were successfully decoded + Keys []string + + // Unused is a slice of keys that were found in the raw value but + // weren't decoded since there was no matching field in the result interface + Unused []string +} + +// Decode takes a map and uses reflection to convert it into the +// given Go native structure. val must be a pointer to a struct. +func Decode(m interface{}, rawVal interface{}) error { + config := &DecoderConfig{ + Metadata: nil, + Result: rawVal, + } + + decoder, err := NewDecoder(config) + if err != nil { + return err + } + + return decoder.Decode(m) +} + +// WeakDecode is the same as Decode but is shorthand to enable +// WeaklyTypedInput. See DecoderConfig for more info. +func WeakDecode(input, output interface{}) error { + config := &DecoderConfig{ + Metadata: nil, + Result: output, + WeaklyTypedInput: true, + } + + decoder, err := NewDecoder(config) + if err != nil { + return err + } + + return decoder.Decode(input) +} + +// NewDecoder returns a new decoder for the given configuration. Once +// a decoder has been returned, the same configuration must not be used +// again. +func NewDecoder(config *DecoderConfig) (*Decoder, error) { + val := reflect.ValueOf(config.Result) + if val.Kind() != reflect.Ptr { + return nil, errors.New("result must be a pointer") + } + + val = val.Elem() + if !val.CanAddr() { + return nil, errors.New("result must be addressable (a pointer)") + } + + if config.Metadata != nil { + if config.Metadata.Keys == nil { + config.Metadata.Keys = make([]string, 0) + } + + if config.Metadata.Unused == nil { + config.Metadata.Unused = make([]string, 0) + } + } + + if config.TagName == "" { + config.TagName = "mapstructure" + } + + result := &Decoder{ + config: config, + } + + return result, nil +} + +// Decode decodes the given raw interface to the target pointer specified +// by the configuration. +func (d *Decoder) Decode(raw interface{}) error { + return d.decode("", raw, reflect.ValueOf(d.config.Result).Elem()) +} + +// Decodes an unknown data type into a specific reflection value. +func (d *Decoder) decode(name string, data interface{}, val reflect.Value) error { + if data == nil { + // If the data is nil, then we don't set anything. + return nil + } + + dataVal := reflect.ValueOf(data) + if !dataVal.IsValid() { + // If the data value is invalid, then we just set the value + // to be the zero value. + val.Set(reflect.Zero(val.Type())) + return nil + } + + if d.config.DecodeHook != nil { + // We have a DecodeHook, so let's pre-process the data. + var err error + data, err = d.config.DecodeHook(getKind(dataVal), getKind(val), data) + if err != nil { + return err + } + } + + var err error + dataKind := getKind(val) + switch dataKind { + case reflect.Bool: + err = d.decodeBool(name, data, val) + case reflect.Interface: + err = d.decodeBasic(name, data, val) + case reflect.String: + err = d.decodeString(name, data, val) + case reflect.Int: + err = d.decodeInt(name, data, val) + case reflect.Uint: + err = d.decodeUint(name, data, val) + case reflect.Float32: + err = d.decodeFloat(name, data, val) + case reflect.Struct: + err = d.decodeStruct(name, data, val) + case reflect.Map: + err = d.decodeMap(name, data, val) + case reflect.Ptr: + err = d.decodePtr(name, data, val) + case reflect.Slice: + err = d.decodeSlice(name, data, val) + default: + // If we reached this point then we weren't able to decode it + return fmt.Errorf("%s: unsupported type: %s", name, dataKind) + } + + // If we reached here, then we successfully decoded SOMETHING, so + // mark the key as used if we're tracking metadata. + if d.config.Metadata != nil && name != "" { + d.config.Metadata.Keys = append(d.config.Metadata.Keys, name) + } + + return err +} + +// This decodes a basic type (bool, int, string, etc.) and sets the +// value to "data" of that type. +func (d *Decoder) decodeBasic(name string, data interface{}, val reflect.Value) error { + dataVal := reflect.ValueOf(data) + dataValType := dataVal.Type() + if !dataValType.AssignableTo(val.Type()) { + return fmt.Errorf( + "'%s' expected type '%s', got '%s'", + name, val.Type(), dataValType) + } + + val.Set(dataVal) + return nil +} + +func (d *Decoder) decodeString(name string, data interface{}, val reflect.Value) error { + dataVal := reflect.ValueOf(data) + dataKind := getKind(dataVal) + + converted := true + switch { + case dataKind == reflect.String: + val.SetString(dataVal.String()) + case dataKind == reflect.Bool && d.config.WeaklyTypedInput: + if dataVal.Bool() { + val.SetString("1") + } else { + val.SetString("0") + } + case dataKind == reflect.Int && d.config.WeaklyTypedInput: + val.SetString(strconv.FormatInt(dataVal.Int(), 10)) + case dataKind == reflect.Uint && d.config.WeaklyTypedInput: + val.SetString(strconv.FormatUint(dataVal.Uint(), 10)) + case dataKind == reflect.Float32 && d.config.WeaklyTypedInput: + val.SetString(strconv.FormatFloat(dataVal.Float(), 'f', -1, 64)) + case dataKind == reflect.Slice && d.config.WeaklyTypedInput: + dataType := dataVal.Type() + elemKind := dataType.Elem().Kind() + switch { + case elemKind == reflect.Uint8: + val.SetString(string(dataVal.Interface().([]uint8))) + default: + converted = false + } + default: + converted = false + } + + if !converted { + return fmt.Errorf( + "'%s' expected type '%s', got unconvertible type '%s'", + name, val.Type(), dataVal.Type()) + } + + return nil +} + +func (d *Decoder) decodeInt(name string, data interface{}, val reflect.Value) error { + dataVal := reflect.ValueOf(data) + dataKind := getKind(dataVal) + + switch { + case dataKind == reflect.Int: + val.SetInt(dataVal.Int()) + case dataKind == reflect.Uint: + val.SetInt(int64(dataVal.Uint())) + case dataKind == reflect.Float32: + val.SetInt(int64(dataVal.Float())) + case dataKind == reflect.Bool && d.config.WeaklyTypedInput: + if dataVal.Bool() { + val.SetInt(1) + } else { + val.SetInt(0) + } + case dataKind == reflect.String && d.config.WeaklyTypedInput: + i, err := strconv.ParseInt(dataVal.String(), 0, val.Type().Bits()) + if err == nil { + val.SetInt(i) + } else { + return fmt.Errorf("cannot parse '%s' as int: %s", name, err) + } + default: + return fmt.Errorf( + "'%s' expected type '%s', got unconvertible type '%s'", + name, val.Type(), dataVal.Type()) + } + + return nil +} + +func (d *Decoder) decodeUint(name string, data interface{}, val reflect.Value) error { + dataVal := reflect.ValueOf(data) + dataKind := getKind(dataVal) + + switch { + case dataKind == reflect.Int: + val.SetUint(uint64(dataVal.Int())) + case dataKind == reflect.Uint: + val.SetUint(dataVal.Uint()) + case dataKind == reflect.Float32: + val.SetUint(uint64(dataVal.Float())) + case dataKind == reflect.Bool && d.config.WeaklyTypedInput: + if dataVal.Bool() { + val.SetUint(1) + } else { + val.SetUint(0) + } + case dataKind == reflect.String && d.config.WeaklyTypedInput: + i, err := strconv.ParseUint(dataVal.String(), 0, val.Type().Bits()) + if err == nil { + val.SetUint(i) + } else { + return fmt.Errorf("cannot parse '%s' as uint: %s", name, err) + } + default: + return fmt.Errorf( + "'%s' expected type '%s', got unconvertible type '%s'", + name, val.Type(), dataVal.Type()) + } + + return nil +} + +func (d *Decoder) decodeBool(name string, data interface{}, val reflect.Value) error { + dataVal := reflect.ValueOf(data) + dataKind := getKind(dataVal) + + switch { + case dataKind == reflect.Bool: + val.SetBool(dataVal.Bool()) + case dataKind == reflect.Int && d.config.WeaklyTypedInput: + val.SetBool(dataVal.Int() != 0) + case dataKind == reflect.Uint && d.config.WeaklyTypedInput: + val.SetBool(dataVal.Uint() != 0) + case dataKind == reflect.Float32 && d.config.WeaklyTypedInput: + val.SetBool(dataVal.Float() != 0) + case dataKind == reflect.String && d.config.WeaklyTypedInput: + b, err := strconv.ParseBool(dataVal.String()) + if err == nil { + val.SetBool(b) + } else if dataVal.String() == "" { + val.SetBool(false) + } else { + return fmt.Errorf("cannot parse '%s' as bool: %s", name, err) + } + default: + return fmt.Errorf( + "'%s' expected type '%s', got unconvertible type '%s'", + name, val.Type(), dataVal.Type()) + } + + return nil +} + +func (d *Decoder) decodeFloat(name string, data interface{}, val reflect.Value) error { + dataVal := reflect.ValueOf(data) + dataKind := getKind(dataVal) + + switch { + case dataKind == reflect.Int: + val.SetFloat(float64(dataVal.Int())) + case dataKind == reflect.Uint: + val.SetFloat(float64(dataVal.Uint())) + case dataKind == reflect.Float32: + val.SetFloat(float64(dataVal.Float())) + case dataKind == reflect.Bool && d.config.WeaklyTypedInput: + if dataVal.Bool() { + val.SetFloat(1) + } else { + val.SetFloat(0) + } + case dataKind == reflect.String && d.config.WeaklyTypedInput: + f, err := strconv.ParseFloat(dataVal.String(), val.Type().Bits()) + if err == nil { + val.SetFloat(f) + } else { + return fmt.Errorf("cannot parse '%s' as float: %s", name, err) + } + default: + return fmt.Errorf( + "'%s' expected type '%s', got unconvertible type '%s'", + name, val.Type(), dataVal.Type()) + } + + return nil +} + +func (d *Decoder) decodeMap(name string, data interface{}, val reflect.Value) error { + valType := val.Type() + valKeyType := valType.Key() + valElemType := valType.Elem() + + // Make a new map to hold our result + mapType := reflect.MapOf(valKeyType, valElemType) + valMap := reflect.MakeMap(mapType) + + // Check input type + dataVal := reflect.Indirect(reflect.ValueOf(data)) + if dataVal.Kind() != reflect.Map { + // Accept empty array/slice instead of an empty map in weakly typed mode + if d.config.WeaklyTypedInput && + (dataVal.Kind() == reflect.Slice || dataVal.Kind() == reflect.Array) && + dataVal.Len() == 0 { + val.Set(valMap) + return nil + } else { + return fmt.Errorf("'%s' expected a map, got '%s'", name, dataVal.Kind()) + } + } + + // Accumulate errors + errors := make([]string, 0) + + for _, k := range dataVal.MapKeys() { + fieldName := fmt.Sprintf("%s[%s]", name, k) + + // First decode the key into the proper type + currentKey := reflect.Indirect(reflect.New(valKeyType)) + if err := d.decode(fieldName, k.Interface(), currentKey); err != nil { + errors = appendErrors(errors, err) + continue + } + + // Next decode the data into the proper type + v := dataVal.MapIndex(k).Interface() + currentVal := reflect.Indirect(reflect.New(valElemType)) + if err := d.decode(fieldName, v, currentVal); err != nil { + errors = appendErrors(errors, err) + continue + } + + valMap.SetMapIndex(currentKey, currentVal) + } + + // Set the built up map to the value + val.Set(valMap) + + // If we had errors, return those + if len(errors) > 0 { + return &Error{errors} + } + + return nil +} + +func (d *Decoder) decodePtr(name string, data interface{}, val reflect.Value) error { + // Create an element of the concrete (non pointer) type and decode + // into that. Then set the value of the pointer to this type. + valType := val.Type() + valElemType := valType.Elem() + realVal := reflect.New(valElemType) + if err := d.decode(name, data, reflect.Indirect(realVal)); err != nil { + return err + } + + val.Set(realVal) + return nil +} + +func (d *Decoder) decodeSlice(name string, data interface{}, val reflect.Value) error { + dataVal := reflect.Indirect(reflect.ValueOf(data)) + dataValKind := dataVal.Kind() + valType := val.Type() + valElemType := valType.Elem() + sliceType := reflect.SliceOf(valElemType) + + // Check input type + if dataValKind != reflect.Array && dataValKind != reflect.Slice { + // Accept empty map instead of array/slice in weakly typed mode + if d.config.WeaklyTypedInput && dataVal.Kind() == reflect.Map && dataVal.Len() == 0 { + val.Set(reflect.MakeSlice(sliceType, 0, 0)) + return nil + } else { + return fmt.Errorf( + "'%s': source data must be an array or slice, got %s", name, dataValKind) + } + } + + // Make a new slice to hold our result, same size as the original data. + valSlice := reflect.MakeSlice(sliceType, dataVal.Len(), dataVal.Len()) + + // Accumulate any errors + errors := make([]string, 0) + + for i := 0; i < dataVal.Len(); i++ { + currentData := dataVal.Index(i).Interface() + currentField := valSlice.Index(i) + + fieldName := fmt.Sprintf("%s[%d]", name, i) + if err := d.decode(fieldName, currentData, currentField); err != nil { + errors = appendErrors(errors, err) + } + } + + // Finally, set the value to the slice we built up + val.Set(valSlice) + + // If there were errors, we return those + if len(errors) > 0 { + return &Error{errors} + } + + return nil +} + +func (d *Decoder) decodeStruct(name string, data interface{}, val reflect.Value) error { + dataVal := reflect.Indirect(reflect.ValueOf(data)) + dataValKind := dataVal.Kind() + if dataValKind != reflect.Map { + return fmt.Errorf("'%s' expected a map, got '%s'", name, dataValKind) + } + + dataValType := dataVal.Type() + if kind := dataValType.Key().Kind(); kind != reflect.String && kind != reflect.Interface { + return fmt.Errorf( + "'%s' needs a map with string keys, has '%s' keys", + name, dataValType.Key().Kind()) + } + + dataValKeys := make(map[reflect.Value]struct{}) + dataValKeysUnused := make(map[interface{}]struct{}) + for _, dataValKey := range dataVal.MapKeys() { + dataValKeys[dataValKey] = struct{}{} + dataValKeysUnused[dataValKey.Interface()] = struct{}{} + } + + errors := make([]string, 0) + + // This slice will keep track of all the structs we'll be decoding. + // There can be more than one struct if there are embedded structs + // that are squashed. + structs := make([]reflect.Value, 1, 5) + structs[0] = val + + // Compile the list of all the fields that we're going to be decoding + // from all the structs. + fields := make(map[*reflect.StructField]reflect.Value) + for len(structs) > 0 { + structVal := structs[0] + structs = structs[1:] + + structType := structVal.Type() + for i := 0; i < structType.NumField(); i++ { + fieldType := structType.Field(i) + + if fieldType.Anonymous { + fieldKind := fieldType.Type.Kind() + if fieldKind != reflect.Struct { + errors = appendErrors(errors, + fmt.Errorf("%s: unsupported type: %s", fieldType.Name, fieldKind)) + continue + } + + // We have an embedded field. We "squash" the fields down + // if specified in the tag. + squash := false + tagParts := strings.Split(fieldType.Tag.Get(d.config.TagName), ",") + for _, tag := range tagParts[1:] { + if tag == "squash" { + squash = true + break + } + } + + if squash { + structs = append(structs, val.FieldByName(fieldType.Name)) + continue + } + } + + // Normal struct field, store it away + fields[&fieldType] = structVal.Field(i) + } + } + + for fieldType, field := range fields { + fieldName := fieldType.Name + + tagValue := fieldType.Tag.Get(d.config.TagName) + tagValue = strings.SplitN(tagValue, ",", 2)[0] + if tagValue != "" { + fieldName = tagValue + } + + rawMapKey := reflect.ValueOf(fieldName) + rawMapVal := dataVal.MapIndex(rawMapKey) + if !rawMapVal.IsValid() { + // Do a slower search by iterating over each key and + // doing case-insensitive search. + for dataValKey, _ := range dataValKeys { + mK, ok := dataValKey.Interface().(string) + if !ok { + // Not a string key + continue + } + + if strings.EqualFold(mK, fieldName) { + rawMapKey = dataValKey + rawMapVal = dataVal.MapIndex(dataValKey) + break + } + } + + if !rawMapVal.IsValid() { + // There was no matching key in the map for the value in + // the struct. Just ignore. + continue + } + } + + // Delete the key we're using from the unused map so we stop tracking + delete(dataValKeysUnused, rawMapKey.Interface()) + + if !field.IsValid() { + // This should never happen + panic("field is not valid") + } + + // If we can't set the field, then it is unexported or something, + // and we just continue onwards. + if !field.CanSet() { + continue + } + + // If the name is empty string, then we're at the root, and we + // don't dot-join the fields. + if name != "" { + fieldName = fmt.Sprintf("%s.%s", name, fieldName) + } + + if err := d.decode(fieldName, rawMapVal.Interface(), field); err != nil { + errors = appendErrors(errors, err) + } + } + + if d.config.ErrorUnused && len(dataValKeysUnused) > 0 { + keys := make([]string, 0, len(dataValKeysUnused)) + for rawKey, _ := range dataValKeysUnused { + keys = append(keys, rawKey.(string)) + } + sort.Strings(keys) + + err := fmt.Errorf("'%s' has invalid keys: %s", name, strings.Join(keys, ", ")) + errors = appendErrors(errors, err) + } + + if len(errors) > 0 { + return &Error{errors} + } + + // Add the unused keys to the list of unused keys if we're tracking metadata + if d.config.Metadata != nil { + for rawKey, _ := range dataValKeysUnused { + key := rawKey.(string) + if name != "" { + key = fmt.Sprintf("%s.%s", name, key) + } + + d.config.Metadata.Unused = append(d.config.Metadata.Unused, key) + } + } + + return nil +} + +func getKind(val reflect.Value) reflect.Kind { + kind := val.Kind() + + switch { + case kind >= reflect.Int && kind <= reflect.Int64: + return reflect.Int + case kind >= reflect.Uint && kind <= reflect.Uint64: + return reflect.Uint + case kind >= reflect.Float32 && kind <= reflect.Float64: + return reflect.Float32 + default: + return kind + } +} diff --git a/Godeps/_workspace/src/github.com/mitchellh/mapstructure/mapstructure_benchmark_test.go b/Godeps/_workspace/src/github.com/mitchellh/mapstructure/mapstructure_benchmark_test.go new file mode 100644 index 0000000000..b50ac36e5d --- /dev/null +++ b/Godeps/_workspace/src/github.com/mitchellh/mapstructure/mapstructure_benchmark_test.go @@ -0,0 +1,243 @@ +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) + } +} diff --git a/Godeps/_workspace/src/github.com/mitchellh/mapstructure/mapstructure_bugs_test.go b/Godeps/_workspace/src/github.com/mitchellh/mapstructure/mapstructure_bugs_test.go new file mode 100644 index 0000000000..7054f1ac9a --- /dev/null +++ b/Godeps/_workspace/src/github.com/mitchellh/mapstructure/mapstructure_bugs_test.go @@ -0,0 +1,47 @@ +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) + } +} diff --git a/Godeps/_workspace/src/github.com/mitchellh/mapstructure/mapstructure_examples_test.go b/Godeps/_workspace/src/github.com/mitchellh/mapstructure/mapstructure_examples_test.go new file mode 100644 index 0000000000..aa393cc572 --- /dev/null +++ b/Godeps/_workspace/src/github.com/mitchellh/mapstructure/mapstructure_examples_test.go @@ -0,0 +1,169 @@ +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} +} diff --git a/Godeps/_workspace/src/github.com/mitchellh/mapstructure/mapstructure_test.go b/Godeps/_workspace/src/github.com/mitchellh/mapstructure/mapstructure_test.go new file mode 100644 index 0000000000..23029c7c4a --- /dev/null +++ b/Godeps/_workspace/src/github.com/mitchellh/mapstructure/mapstructure_test.go @@ -0,0 +1,828 @@ +package mapstructure + +import ( + "reflect" + "sort" + "testing" +) + +type Basic struct { + Vstring string + Vint int + Vuint uint + Vbool bool + Vfloat float64 + Vextra string + vsilent bool + Vdata interface{} +} + +type Embedded struct { + Basic + Vunique string +} + +type EmbeddedPointer struct { + *Basic + Vunique string +} + +type EmbeddedSquash struct { + Basic `mapstructure:",squash"` + Vunique string +} + +type Map struct { + Vfoo string + Vother map[string]string +} + +type MapOfStruct struct { + Value map[string]Basic +} + +type Nested struct { + Vfoo string + Vbar Basic +} + +type NestedPointer struct { + Vfoo string + Vbar *Basic +} + +type Slice struct { + Vfoo string + Vbar []string +} + +type SliceOfStruct struct { + Value []Basic +} + +type Tagged struct { + Extra string `mapstructure:"bar,what,what"` + Value string `mapstructure:"foo"` +} + +type TypeConversionResult struct { + IntToFloat float32 + IntToUint uint + IntToBool bool + IntToString string + UintToInt int + UintToFloat float32 + UintToBool bool + UintToString string + BoolToInt int + BoolToUint uint + BoolToFloat float32 + BoolToString string + FloatToInt int + FloatToUint uint + FloatToBool bool + FloatToString string + SliceUint8ToString string + StringToInt int + StringToUint uint + StringToBool bool + StringToFloat float32 + SliceToMap map[string]interface{} + MapToSlice []interface{} +} + +func TestBasicTypes(t *testing.T) { + t.Parallel() + + input := map[string]interface{}{ + "vstring": "foo", + "vint": 42, + "Vuint": 42, + "vbool": true, + "Vfloat": 42.42, + "vsilent": true, + "vdata": 42, + } + + var result Basic + err := Decode(input, &result) + if err != nil { + t.Errorf("got an err: %s", err.Error()) + t.FailNow() + } + + if result.Vstring != "foo" { + t.Errorf("vstring value should be 'foo': %#v", result.Vstring) + } + + if result.Vint != 42 { + t.Errorf("vint value should be 42: %#v", result.Vint) + } + + if result.Vuint != 42 { + t.Errorf("vuint value should be 42: %#v", result.Vuint) + } + + if result.Vbool != true { + t.Errorf("vbool value should be true: %#v", result.Vbool) + } + + if result.Vfloat != 42.42 { + t.Errorf("vfloat value should be 42.42: %#v", result.Vfloat) + } + + if result.Vextra != "" { + t.Errorf("vextra value should be empty: %#v", result.Vextra) + } + + if result.vsilent != false { + t.Error("vsilent should not be set, it is unexported") + } + + if result.Vdata != 42 { + t.Error("vdata should be valid") + } +} + +func TestBasic_IntWithFloat(t *testing.T) { + t.Parallel() + + input := map[string]interface{}{ + "vint": float64(42), + } + + var result Basic + err := Decode(input, &result) + if err != nil { + t.Fatalf("got an err: %s", err) + } +} + +func TestDecode_Embedded(t *testing.T) { + t.Parallel() + + input := map[string]interface{}{ + "vstring": "foo", + "Basic": map[string]interface{}{ + "vstring": "innerfoo", + }, + "vunique": "bar", + } + + var result Embedded + err := Decode(input, &result) + if err != nil { + t.Fatalf("got an err: %s", err.Error()) + } + + if result.Vstring != "innerfoo" { + t.Errorf("vstring value should be 'innerfoo': %#v", result.Vstring) + } + + if result.Vunique != "bar" { + t.Errorf("vunique value should be 'bar': %#v", result.Vunique) + } +} + +func TestDecode_EmbeddedPointer(t *testing.T) { + t.Parallel() + + input := map[string]interface{}{ + "vstring": "foo", + "Basic": map[string]interface{}{ + "vstring": "innerfoo", + }, + "vunique": "bar", + } + + var result EmbeddedPointer + err := Decode(input, &result) + if err == nil { + t.Fatal("should get error") + } +} + +func TestDecode_EmbeddedSquash(t *testing.T) { + t.Parallel() + + input := map[string]interface{}{ + "vstring": "foo", + "vunique": "bar", + } + + var result EmbeddedSquash + err := Decode(input, &result) + if err != nil { + t.Fatalf("got an err: %s", err.Error()) + } + + if result.Vstring != "foo" { + t.Errorf("vstring value should be 'foo': %#v", result.Vstring) + } + + if result.Vunique != "bar" { + t.Errorf("vunique value should be 'bar': %#v", result.Vunique) + } +} + +func TestDecode_DecodeHook(t *testing.T) { + t.Parallel() + + input := map[string]interface{}{ + "vint": "WHAT", + } + + decodeHook := func(from reflect.Kind, to reflect.Kind, v interface{}) (interface{}, error) { + if from == reflect.String && to != reflect.String { + return 5, nil + } + + return v, nil + } + + var result Basic + config := &DecoderConfig{ + DecodeHook: decodeHook, + Result: &result, + } + + decoder, err := NewDecoder(config) + if err != nil { + t.Fatalf("err: %s", err) + } + + err = decoder.Decode(input) + if err != nil { + t.Fatalf("got an err: %s", err) + } + + if result.Vint != 5 { + t.Errorf("vint should be 5: %#v", result.Vint) + } +} + +func TestDecode_Nil(t *testing.T) { + t.Parallel() + + var input interface{} = nil + result := Basic{ + Vstring: "foo", + } + + err := Decode(input, &result) + if err != nil { + t.Fatalf("err: %s", err) + } + + if result.Vstring != "foo" { + t.Fatalf("bad: %#v", result.Vstring) + } +} + +func TestDecode_NonStruct(t *testing.T) { + t.Parallel() + + input := map[string]interface{}{ + "foo": "bar", + "bar": "baz", + } + + var result map[string]string + err := Decode(input, &result) + if err != nil { + t.Fatalf("err: %s", err) + } + + if result["foo"] != "bar" { + t.Fatal("foo is not bar") + } +} + +func TestDecode_TypeConversion(t *testing.T) { + 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, + "SliceUint8ToString": []uint8("foo"), + "StringToInt": "42", + "StringToUint": "42", + "StringToBool": "1", + "StringToFloat": "42.42", + "SliceToMap": []interface{}{}, + "MapToSlice": map[string]interface{}{}, + } + + expectedResultStrict := TypeConversionResult{ + IntToFloat: 42.0, + IntToUint: 42, + UintToInt: 42, + UintToFloat: 42, + BoolToInt: 0, + BoolToUint: 0, + BoolToFloat: 0, + FloatToInt: 42, + FloatToUint: 42, + } + + expectedResultWeak := TypeConversionResult{ + IntToFloat: 42.0, + IntToUint: 42, + IntToBool: true, + IntToString: "42", + UintToInt: 42, + UintToFloat: 42, + UintToBool: true, + UintToString: "42", + BoolToInt: 1, + BoolToUint: 1, + BoolToFloat: 1, + BoolToString: "1", + FloatToInt: 42, + FloatToUint: 42, + FloatToBool: true, + FloatToString: "42.42", + SliceUint8ToString: "foo", + StringToInt: 42, + StringToUint: 42, + StringToBool: true, + StringToFloat: 42.42, + SliceToMap: map[string]interface{}{}, + MapToSlice: []interface{}{}, + } + + // Test strict type conversion + var resultStrict TypeConversionResult + err := Decode(input, &resultStrict) + if err == nil { + t.Errorf("should return an error") + } + if !reflect.DeepEqual(resultStrict, expectedResultStrict) { + t.Errorf("expected %v, got: %v", expectedResultStrict, resultStrict) + } + + // Test weak type conversion + var decoder *Decoder + var resultWeak TypeConversionResult + + config := &DecoderConfig{ + WeaklyTypedInput: true, + Result: &resultWeak, + } + + decoder, err = NewDecoder(config) + if err != nil { + t.Fatalf("err: %s", err) + } + + err = decoder.Decode(input) + if err != nil { + t.Fatalf("got an err: %s", err) + } + + if !reflect.DeepEqual(resultWeak, expectedResultWeak) { + t.Errorf("expected \n%#v, got: \n%#v", expectedResultWeak, resultWeak) + } +} + +func TestDecoder_ErrorUnused(t *testing.T) { + t.Parallel() + + input := map[string]interface{}{ + "vstring": "hello", + "foo": "bar", + } + + var result Basic + config := &DecoderConfig{ + ErrorUnused: true, + Result: &result, + } + + decoder, err := NewDecoder(config) + if err != nil { + t.Fatalf("err: %s", err) + } + + err = decoder.Decode(input) + if err == nil { + t.Fatal("expected error") + } +} + +func TestMap(t *testing.T) { + t.Parallel() + + input := map[string]interface{}{ + "vfoo": "foo", + "vother": map[interface{}]interface{}{ + "foo": "foo", + "bar": "bar", + }, + } + + var result Map + err := Decode(input, &result) + if err != nil { + t.Fatalf("got an error: %s", err) + } + + if result.Vfoo != "foo" { + t.Errorf("vfoo value should be 'foo': %#v", result.Vfoo) + } + + if result.Vother == nil { + t.Fatal("vother should not be nil") + } + + if len(result.Vother) != 2 { + t.Error("vother should have two items") + } + + if result.Vother["foo"] != "foo" { + t.Errorf("'foo' key should be foo, got: %#v", result.Vother["foo"]) + } + + if result.Vother["bar"] != "bar" { + t.Errorf("'bar' key should be bar, got: %#v", result.Vother["bar"]) + } +} + +func TestMapOfStruct(t *testing.T) { + t.Parallel() + + input := map[string]interface{}{ + "value": map[string]interface{}{ + "foo": map[string]string{"vstring": "one"}, + "bar": map[string]string{"vstring": "two"}, + }, + } + + var result MapOfStruct + err := Decode(input, &result) + if err != nil { + t.Fatalf("got an err: %s", err) + } + + if result.Value == nil { + t.Fatal("value should not be nil") + } + + if len(result.Value) != 2 { + t.Error("value should have two items") + } + + if result.Value["foo"].Vstring != "one" { + t.Errorf("foo value should be 'one', got: %s", result.Value["foo"].Vstring) + } + + if result.Value["bar"].Vstring != "two" { + t.Errorf("bar value should be 'two', got: %s", result.Value["bar"].Vstring) + } +} + +func TestNestedType(t *testing.T) { + t.Parallel() + + input := map[string]interface{}{ + "vfoo": "foo", + "vbar": map[string]interface{}{ + "vstring": "foo", + "vint": 42, + "vbool": true, + }, + } + + var result Nested + err := Decode(input, &result) + if err != nil { + t.Fatalf("got an err: %s", err.Error()) + } + + if result.Vfoo != "foo" { + t.Errorf("vfoo value should be 'foo': %#v", result.Vfoo) + } + + if result.Vbar.Vstring != "foo" { + t.Errorf("vstring value should be 'foo': %#v", result.Vbar.Vstring) + } + + if result.Vbar.Vint != 42 { + t.Errorf("vint value should be 42: %#v", result.Vbar.Vint) + } + + if result.Vbar.Vbool != true { + t.Errorf("vbool value should be true: %#v", result.Vbar.Vbool) + } + + if result.Vbar.Vextra != "" { + t.Errorf("vextra value should be empty: %#v", result.Vbar.Vextra) + } +} + +func TestNestedTypePointer(t *testing.T) { + t.Parallel() + + input := map[string]interface{}{ + "vfoo": "foo", + "vbar": &map[string]interface{}{ + "vstring": "foo", + "vint": 42, + "vbool": true, + }, + } + + var result NestedPointer + err := Decode(input, &result) + if err != nil { + t.Fatalf("got an err: %s", err.Error()) + } + + if result.Vfoo != "foo" { + t.Errorf("vfoo value should be 'foo': %#v", result.Vfoo) + } + + if result.Vbar.Vstring != "foo" { + t.Errorf("vstring value should be 'foo': %#v", result.Vbar.Vstring) + } + + if result.Vbar.Vint != 42 { + t.Errorf("vint value should be 42: %#v", result.Vbar.Vint) + } + + if result.Vbar.Vbool != true { + t.Errorf("vbool value should be true: %#v", result.Vbar.Vbool) + } + + if result.Vbar.Vextra != "" { + t.Errorf("vextra value should be empty: %#v", result.Vbar.Vextra) + } +} + +func TestSlice(t *testing.T) { + t.Parallel() + + inputStringSlice := map[string]interface{}{ + "vfoo": "foo", + "vbar": []string{"foo", "bar", "baz"}, + } + + inputStringSlicePointer := map[string]interface{}{ + "vfoo": "foo", + "vbar": &[]string{"foo", "bar", "baz"}, + } + + outputStringSlice := &Slice{ + "foo", + []string{"foo", "bar", "baz"}, + } + + testSliceInput(t, inputStringSlice, outputStringSlice) + testSliceInput(t, inputStringSlicePointer, outputStringSlice) +} + +func TestInvalidSlice(t *testing.T) { + t.Parallel() + + input := map[string]interface{}{ + "vfoo": "foo", + "vbar": 42, + } + + result := Slice{} + err := Decode(input, &result) + if err == nil { + t.Errorf("expected failure") + } +} + +func TestSliceOfStruct(t *testing.T) { + t.Parallel() + + input := map[string]interface{}{ + "value": []map[string]interface{}{ + {"vstring": "one"}, + {"vstring": "two"}, + }, + } + + var result SliceOfStruct + err := Decode(input, &result) + if err != nil { + t.Fatalf("got unexpected error: %s", err) + } + + if len(result.Value) != 2 { + t.Fatalf("expected two values, got %d", len(result.Value)) + } + + if result.Value[0].Vstring != "one" { + t.Errorf("first value should be 'one', got: %s", result.Value[0].Vstring) + } + + if result.Value[1].Vstring != "two" { + t.Errorf("second value should be 'two', got: %s", result.Value[1].Vstring) + } +} + +func TestInvalidType(t *testing.T) { + t.Parallel() + + input := map[string]interface{}{ + "vstring": 42, + } + + var result Basic + err := Decode(input, &result) + if err == nil { + t.Fatal("error should exist") + } + + derr, ok := err.(*Error) + if !ok { + t.Fatalf("error should be kind of Error, instead: %#v", err) + } + + if derr.Errors[0] != "'Vstring' expected type 'string', got unconvertible type 'int'" { + t.Errorf("got unexpected error: %s", err) + } +} + +func TestMetadata(t *testing.T) { + t.Parallel() + + input := map[string]interface{}{ + "vfoo": "foo", + "vbar": map[string]interface{}{ + "vstring": "foo", + "Vuint": 42, + "foo": "bar", + }, + "bar": "nil", + } + + var md Metadata + var result Nested + config := &DecoderConfig{ + Metadata: &md, + Result: &result, + } + + decoder, err := NewDecoder(config) + if err != nil { + t.Fatalf("err: %s", err) + } + + err = decoder.Decode(input) + if err != nil { + t.Fatalf("err: %s", err.Error()) + } + + expectedKeys := []string{"Vfoo", "Vbar.Vstring", "Vbar.Vuint", "Vbar"} + if !reflect.DeepEqual(md.Keys, expectedKeys) { + t.Fatalf("bad keys: %#v", md.Keys) + } + + expectedUnused := []string{"Vbar.foo", "bar"} + if !reflect.DeepEqual(md.Unused, expectedUnused) { + t.Fatalf("bad unused: %#v", md.Unused) + } +} + +func TestMetadata_Embedded(t *testing.T) { + t.Parallel() + + 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 { + t.Fatalf("err: %s", err) + } + + err = decoder.Decode(input) + if err != nil { + t.Fatalf("err: %s", err.Error()) + } + + expectedKeys := []string{"Vstring", "Vunique"} + + sort.Strings(md.Keys) + if !reflect.DeepEqual(md.Keys, expectedKeys) { + t.Fatalf("bad keys: %#v", md.Keys) + } + + expectedUnused := []string{} + if !reflect.DeepEqual(md.Unused, expectedUnused) { + t.Fatalf("bad unused: %#v", md.Unused) + } +} + +func TestNonPtrValue(t *testing.T) { + t.Parallel() + + err := Decode(map[string]interface{}{}, Basic{}) + if err == nil { + t.Fatal("error should exist") + } + + if err.Error() != "result must be a pointer" { + t.Errorf("got unexpected error: %s", err) + } +} + +func TestTagged(t *testing.T) { + t.Parallel() + + input := map[string]interface{}{ + "foo": "bar", + "bar": "value", + } + + var result Tagged + err := Decode(input, &result) + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + if result.Value != "bar" { + t.Errorf("value should be 'bar', got: %#v", result.Value) + } + + if result.Extra != "value" { + t.Errorf("extra should be 'value', got: %#v", result.Extra) + } +} + +func TestWeakDecode(t *testing.T) { + t.Parallel() + + input := map[string]interface{}{ + "foo": "4", + "bar": "value", + } + + var result struct { + Foo int + Bar string + } + + if err := WeakDecode(input, &result); err != nil { + t.Fatalf("err: %s", err) + } + if result.Foo != 4 { + t.Fatalf("bad: %#v", result) + } + if result.Bar != "value" { + t.Fatalf("bad: %#v", result) + } +} + +func testSliceInput(t *testing.T, input map[string]interface{}, expected *Slice) { + var result Slice + err := Decode(input, &result) + if err != nil { + t.Fatalf("got error: %s", err) + } + + if result.Vfoo != expected.Vfoo { + t.Errorf("Vfoo expected '%s', got '%s'", expected.Vfoo, result.Vfoo) + } + + if result.Vbar == nil { + t.Fatalf("Vbar a slice, got '%#v'", result.Vbar) + } + + if len(result.Vbar) != len(expected.Vbar) { + t.Errorf("Vbar length should be %d, got %d", len(expected.Vbar), len(result.Vbar)) + } + + for i, v := range result.Vbar { + if v != expected.Vbar[i] { + t.Errorf( + "Vbar[%d] should be '%#v', got '%#v'", + i, expected.Vbar[i], v) + } + } +} diff --git a/Godeps/_workspace/src/github.com/racker/perigee/.gitignore b/Godeps/_workspace/src/github.com/racker/perigee/.gitignore new file mode 100644 index 0000000000..49ca32aa20 --- /dev/null +++ b/Godeps/_workspace/src/github.com/racker/perigee/.gitignore @@ -0,0 +1,2 @@ +bin/* +pkg/* diff --git a/Godeps/_workspace/src/github.com/racker/perigee/LICENSE b/Godeps/_workspace/src/github.com/racker/perigee/LICENSE new file mode 100644 index 0000000000..d645695673 --- /dev/null +++ b/Godeps/_workspace/src/github.com/racker/perigee/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/Godeps/_workspace/src/github.com/racker/perigee/README.md b/Godeps/_workspace/src/github.com/racker/perigee/README.md new file mode 100644 index 0000000000..81cbf4a95f --- /dev/null +++ b/Godeps/_workspace/src/github.com/racker/perigee/README.md @@ -0,0 +1,120 @@ +# 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. + diff --git a/Godeps/_workspace/src/github.com/racker/perigee/api.go b/Godeps/_workspace/src/github.com/racker/perigee/api.go new file mode 100644 index 0000000000..0fcbadbee5 --- /dev/null +++ b/Godeps/_workspace/src/github.com/racker/perigee/api.go @@ -0,0 +1,269 @@ +// 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 +} diff --git a/Godeps/_workspace/src/github.com/racker/perigee/api_test.go b/Godeps/_workspace/src/github.com/racker/perigee/api_test.go new file mode 100644 index 0000000000..da943b247b --- /dev/null +++ b/Godeps/_workspace/src/github.com/racker/perigee/api_test.go @@ -0,0 +1,226 @@ +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"]) + } +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/.travis.yml b/Godeps/_workspace/src/github.com/rackspace/gophercloud/.travis.yml new file mode 100644 index 0000000000..cf4f8cafcc --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/.travis.yml @@ -0,0 +1,14 @@ +language: go +install: + - go get -v -tags 'fixtures acceptance' ./... +go: + - 1.1 + - 1.2 + - tip +script: script/cibuild +after_success: + - go get code.google.com/p/go.tools/cmd/cover + - go get github.com/axw/gocov/gocov + - go get github.com/mattn/goveralls + - export PATH=$PATH:$HOME/gopath/bin/ + - goveralls 2k7PTU3xa474Hymwgdj6XjqenNfGTNkO8 diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/CONTRIBUTING.md b/Godeps/_workspace/src/github.com/rackspace/gophercloud/CONTRIBUTING.md new file mode 100644 index 0000000000..93b798e5a2 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/CONTRIBUTING.md @@ -0,0 +1,275 @@ +# Contributing to gophercloud + +- [Getting started](#getting-started) +- [Tests](#tests) +- [Style guide](#basic-style-guide) +- [5 ways to get involved](#5-ways-to-get-involved) + +## Setting up your git workspace + +As a contributor you will need to setup your workspace in a slightly different +way than just downloading it. Here are the basic installation instructions: + +1. Configure your `$GOPATH` and run `go get` as described in the main +[README](/README.md#how-to-install). + +2. Move into the directory that houses your local repository: + + ```bash + cd ${GOPATH}/src/github.com/rackspace/gophercloud + ``` + +3. Fork the `rackspace/gophercloud` repository and update your remote refs. You +will need to rename the `origin` remote branch to `upstream`, and add your +fork as `origin` instead: + + ```bash + git remote rename origin upstream + git remote add origin git@github.com//gophercloud + ``` + +4. Checkout the latest development branch ([click here](/branches) to see all +the branches): + + ```bash + git checkout release/v1.0.1 + ``` + +5. If you're working on something (discussed more in detail below), you will +need to checkout a new feature branch: + + ```bash + git checkout -b my-new-feature + ``` + +Another thing to bear in mind is that you will need to add a few extra +environment variables for acceptance tests - this is documented in our +[acceptance tests readme](/acceptance). + +## Tests + +When working on a new or existing feature, testing will be the backbone of your +work since it helps uncover and prevent regressions in the codebase. There are +two types of test we use in gophercloud: unit tests and acceptance tests, which +are both described below. + +### Unit tests + +Unit tests are the fine-grained tests that establish and ensure the behaviour +of individual units of functionality. We usually test on an +operation-by-operation basis (an operation typically being an API action) with +the use of mocking to set up explicit expectations. Each operation will set up +its HTTP response expectation, and then test how the system responds when fed +this controlled, pre-determined input. + +To make life easier, we've introduced a bunch of test helpers to simplify the +process of testing expectations with assertions: + +```go +import ( + "testing" + + "github.com/rackspace/gophercloud/testhelper" +) + +func TestSomething(t *testing.T) { + result, err := Operation() + + testhelper.AssertEquals(t, "foo", result.Bar) + testhelper.AssertNoErr(t, err) +} + +func TestSomethingElse(t *testing.T) { + testhelper.CheckEquals(t, "expected", "actual") +} +``` + +`AssertEquals` and `AssertNoErr` will throw a fatal error if a value does not +match an expected value or if an error has been declared, respectively. You can +also use `CheckEquals` and `CheckNoErr` for the same purpose; the only difference +being that `t.Errorf` is raised rather than `t.Fatalf`. + +Here is a truncated example of mocked HTTP responses: + +```go +import ( + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestGet(t *testing.T) { + // Setup the HTTP request multiplexer and server + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/networks/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) { + // Test we're using the correct HTTP method + th.TestMethod(t, r, "GET") + + // Test we're setting the auth token + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + // Set the appropriate headers for our mocked response + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + // Set the HTTP body + fmt.Fprintf(w, ` +{ + "network": { + "status": "ACTIVE", + "name": "private-network", + "admin_state_up": true, + "tenant_id": "4fd44f30292945e481c7b8a0c8908869", + "shared": true, + "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22" + } +} + `) + }) + + // Call our API operation + network, err := Get(fake.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22").Extract() + + // Assert no errors and equality + th.AssertNoErr(t, err) + th.AssertEquals(t, n.Status, "ACTIVE") +} +``` + +### Acceptance tests + +As we've already mentioned, unit tests have a very narrow and confined focus - +they test small units of behaviour. Acceptance tests on the other hand have a +far larger scope: they are fully functional tests that test the entire API of a +service in one fell swoop. They don't care about unit isolation or mocking +expectations, they instead do a full run-through and consequently test how the +entire system _integrates_ together. When an API satisfies expectations, it +proves by default that the requirements for a contract have been met. + +Please be aware that acceptance tests will hit a live API - and may incur +service charges from your provider. Although most tests handle their own +teardown procedures, it is always worth manually checking that resources are +deleted after the test suite finishes. + +### Running tests + +To run all tests: + +```bash +go test ./... +``` + +To run all tests with verbose output: + +```bash +go test -v ./... +``` + +To run tests that match certain [build tags](): + +```bash +go test -tags "foo bar" ./... +``` + +To run tests for a particular sub-package: + +```bash +cd ./path/to/package && go test . +``` + +## Basic style guide + +We follow the standard formatting recommendations and language idioms set out +in the [Effective Go](https://golang.org/doc/effective_go.html) guide. It's +definitely worth reading - but the relevant sections are +[formatting](https://golang.org/doc/effective_go.html#formatting) +and [names](https://golang.org/doc/effective_go.html#names). + +## 5 ways to get involved + +There are five main ways you can get involved in our open-source project, and +each is described briefly below. Once you've made up your mind and decided on +your fix, you will need to follow the same basic steps that all submissions are +required to adhere to: + +1. [fork](https://help.github.com/articles/fork-a-repo/) the `rackspace/gophercloud` repository +2. checkout a [new branch](https://github.com/Kunena/Kunena-Forum/wiki/Create-a-new-branch-with-git-and-manage-branches) +3. submit your branch as a [pull request](https://help.github.com/articles/creating-a-pull-request/) + +### 1. Providing feedback + +On of the easiest ways to get readily involved in our project is to let us know +about your experiences using our SDK. Feedback like this is incredibly useful +to us, because it allows us to refine and change features based on what our +users want and expect of us. There are a bunch of ways to get in contact! You +can [ping us](mailto:sdk-support@rackspace.com) via e-mail, talk to us on irc +(#rackspace-dev on freenode), [tweet us](https://twitter.com/rackspace), or +submit an issue on our [bug tracker](/issues). Things you might like to tell us +are: + +* how easy was it to start using our SDK? +* did it meet your expectations? If not, why not? +* did our documentation help or hinder you? +* what could we improve in general? + +### 2. Fixing bugs + +If you want to start fixing open bugs, we'd really appreciate that! Bug fixing +is central to any project. The best way to get started is by heading to our +[bug tracker](https://github.com/rackspace/gophercloud/issues) and finding open +bugs that you think nobody is working on. It might be useful to comment on the +thread to see the current state of the issue and if anybody has made any +breakthroughs on it so far. + +### 3. Improving documentation + +We have three forms of documentation: + +* short README documents that briefly introduce a topic +* reference documentation on [godoc.org](http://godoc.org) that is automatically +generated from source code comments +* user documentation on our [homepage](http://gophercloud.io) that includes +getting started guides, installation guides and code samples + +If you feel that a certain section could be improved - whether it's to clarify +ambiguity, correct a technical mistake, or to fix a grammatical error - please +feel entitled to do so! We welcome doc pull requests with the same childlike +enthusiasm as any other contribution! + +### 4. Optimizing existing features + +If you would like to improve or optimize an existing feature, please be aware +that we adhere to [semantic versioning](http://semver.org) - which means that +we cannot introduce breaking changes to the API without a major version change +(v1.x -> v2.x). Making that leap is a big step, so we encourage contributors to +refactor rather than rewrite. Running tests will prevent regression and avoid +the possibility of breaking somebody's current implementation. + +Another tip is to keep the focus of your work as small as possible - try not to +introduce a change that affects lots and lots of files because it introduces +added risk and increases the cognitive load on the reviewers checking your +work. Change-sets which are easily understood and will not negatively impact +users are more likely to be integrated quickly. + +Lastly, if you're seeking to optimize a particular operation, you should try to +demonstrate a negative performance impact - perhaps using go's inbuilt +[benchmark capabilities](http://dave.cheney.net/2013/06/30/how-to-write-benchmarks-in-go). + +### 5. Working on a new feature + +If you've found something we've left out, definitely feel free to start work on +introducing that feature. It's always useful to open an issue or submit a pull +request early on to indicate your intent to a core contributor - this enables +quick/early feedback and can help steer you in the right direction by avoiding +known issues. It might also help you avoid losing time implementing something +that might not ever work. One tip is to prefix your Pull Request issue title +with [wip] - then people know it's a work in progress. + +You must ensure that all of your work is well tested - both in terms of unit +and acceptance tests. Untested code will not be merged because it introduces +too much of a risk to end-users. + +Happy hacking! diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/CONTRIBUTORS.md b/Godeps/_workspace/src/github.com/rackspace/gophercloud/CONTRIBUTORS.md new file mode 100644 index 0000000000..eb97094b73 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/CONTRIBUTORS.md @@ -0,0 +1,12 @@ +Contributors +============ + +| Name | Email | +| ---- | ----- | +| Samuel A. Falvo II | +| Glen Campbell | +| Jesse Noller | +| Jon Perritt | +| Ash Wilson | +| Jamie Hannaford | +| Don Schenck | don.schenck@rackspace.com> diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/Godeps/Godeps.json b/Godeps/_workspace/src/github.com/rackspace/gophercloud/Godeps/Godeps.json new file mode 100644 index 0000000000..dbe26bbf76 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/Godeps/Godeps.json @@ -0,0 +1,5 @@ +{ + "ImportPath": "github.com/rackspace/gophercloud", + "GoVersion": "go1.3.3", + "Deps": [] +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/Godeps/Readme b/Godeps/_workspace/src/github.com/rackspace/gophercloud/Godeps/Readme new file mode 100644 index 0000000000..4cdaa53d56 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/Godeps/Readme @@ -0,0 +1,5 @@ +This directory tree is generated automatically by godep. + +Please do not edit. + +See https://github.com/tools/godep for more information. diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/LICENSE b/Godeps/_workspace/src/github.com/rackspace/gophercloud/LICENSE new file mode 100644 index 0000000000..fbbbc9e4cb --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/LICENSE @@ -0,0 +1,191 @@ +Copyright 2012-2013 Rackspace, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); you may not use +this file except in compliance with the License. You may obtain a copy of the +License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed +under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. + +------ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/README.md b/Godeps/_workspace/src/github.com/rackspace/gophercloud/README.md new file mode 100644 index 0000000000..9f7552b0d2 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/README.md @@ -0,0 +1,161 @@ +# Gophercloud: the OpenStack SDK for Go +[![Build Status](https://travis-ci.org/rackspace/gophercloud.svg?branch=master)](https://travis-ci.org/rackspace/gophercloud) + +Gophercloud is a flexible SDK that allows you to consume and work with OpenStack +clouds in a simple and idiomatic way using golang. Many services are supported, +including Compute, Block Storage, Object Storage, Networking, and Identity. +Each service API is backed with getting started guides, code samples, reference +documentation, unit tests and acceptance tests. + +## Useful links + +* [Gophercloud homepage](http://gophercloud.io) +* [Reference documentation](http://godoc.org/github.com/rackspace/gophercloud) +* [Getting started guides](http://gophercloud.io/docs) +* [Effective Go](https://golang.org/doc/effective_go.html) + +## How to install + +Before installing, you need to ensure that your [GOPATH environment variable](https://golang.org/doc/code.html#GOPATH) +is pointing to an appropriate directory where you want to install Gophercloud: + +```bash +mkdir $HOME/go +export GOPATH=$HOME/go +``` + +To protect yourself against changes in your dependencies, we highly recommend choosing a +[dependency management solution](https://code.google.com/p/go-wiki/wiki/PackageManagementTools) for +your projects, such as [godep](https://github.com/tools/godep). Once this is set up, you can install +Gophercloud as a dependency like so: + +```bash +go get github.com/rackspace/gophercloud + +# Edit your code to import relevant packages from "github.com/rackspace/gophercloud" + +godep save ./... +``` + +This will install all the source files you need into a `Godeps/_workspace` directory, which is +referenceable from your own source files when you use the `godep go` command. + +## Getting started + +### Credentials + +Because you'll be hitting an API, you will need to retrieve your OpenStack +credentials and either store them as environment variables or in your local Go +files. The first method is recommended because it decouples credential +information from source code, allowing you to push the latter to your version +control system without any security risk. + +You will need to retrieve the following: + +* username +* password +* tenant name or tenant ID +* a valid Keystone identity URL + +For users that have the OpenStack dashboard installed, there's a shortcut. If +you visit the `project/access_and_security` path in Horizon and click on the +"Download OpenStack RC File" button at the top right hand corner, you will +download a bash file that exports all of your access details to environment +variables. To execute the file, run `source admin-openrc.sh` and you will be +prompted for your password. + +### Authentication + +Once you have access to your credentials, you can begin plugging them into +Gophercloud. The next step is authentication, and this is handled by a base +"Provider" struct. To get one, you can either pass in your credentials +explicitly, or tell Gophercloud to use environment variables: + +```go +import ( + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/openstack" + "github.com/rackspace/gophercloud/openstack/utils" +) + +// Option 1: Pass in the values yourself +opts := gophercloud.AuthOptions{ + IdentityEndpoint: "https://my-openstack.com:5000/v2.0", + Username: "{username}", + Password: "{password}", + TenantID: "{tenant_id}", +} + +// Option 2: Use a utility function to retrieve all your environment variables +opts, err := openstack.AuthOptionsFromEnv() +``` + +Once you have the `opts` variable, you can pass it in and get back a +`ProviderClient` struct: + +```go +provider, err := openstack.AuthenticatedClient(opts) +``` + +The `ProviderClient` is the top-level client that all of your OpenStack services +derive from. The provider contains all of the authentication details that allow +your Go code to access the API - such as the base URL and token ID. + +### Provision a server + +Once we have a base Provider, we inject it as a dependency into each OpenStack +service. In order to work with the Compute API, we need a Compute service +client; which can be created like so: + +```go +client, err := openstack.NewComputeV2(provider, gophercloud.EndpointOpts{ + Region: os.Getenv("OS_REGION_NAME"), +}) +``` + +We then use this `client` for any Compute API operation we want. In our case, +we want to provision a new server - so we invoke the `Create` method and pass +in the flavor ID (hardware specification) and image ID (operating system) we're +interested in: + +```go +import "github.com/rackspace/gophercloud/openstack/compute/v2/servers" + +server, err := servers.Create(client, servers.CreateOpts{ + Name: "My new server!", + FlavorRef: "flavor_id", + ImageRef: "image_id", +}).Extract() +``` + +If you are unsure about what images and flavors are, you can read our [Compute +Getting Started guide](http://gophercloud.io/docs/compute). The above code +sample creates a new server with the parameters, and embodies the new resource +in the `server` variable (a +[`servers.Server`](http://godoc.org/github.com/rackspace/gophercloud) struct). + +### Next steps + +Cool! You've handled authentication, got your `ProviderClient` and provisioned +a new server. You're now ready to use more OpenStack services. + +* [Getting started with Compute](http://gophercloud.io/docs/compute) +* [Getting started with Object Storage](http://gophercloud.io/docs/object-storage) +* [Getting started with Networking](http://gophercloud.io/docs/networking) +* [Getting started with Block Storage](http://gophercloud.io/docs/block-storage) +* [Getting started with Identity](http://gophercloud.io/docs/identity) + +## Contributing + +Engaging the community and lowering barriers for contributors is something we +care a lot about. For this reason, we've taken the time to write a [contributing +guide](./CONTRIBUTING.md) for folks interested in getting involved in our project. +If you're not sure how you can get involved, feel free to submit an issue or +[e-mail us](mailto:sdk-support@rackspace.com) privately. You don't need to be a +Go expert - all members of the community are welcome! + +## Help and feedback + +If you're struggling with something or have spotted a potential bug, feel free +to submit an issue to our [bug tracker](/issues) or e-mail us directly at +[sdk-support@rackspace.com](mailto:sdk-support@rackspace.com). diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/UPGRADING.md b/Godeps/_workspace/src/github.com/rackspace/gophercloud/UPGRADING.md new file mode 100644 index 0000000000..a702cfc509 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/UPGRADING.md @@ -0,0 +1,338 @@ +# Upgrading to v1.0.0 + +With the arrival of this new major version increment, the unfortunate news is +that breaking changes have been introduced to existing services. The API +has been completely rewritten from the ground up to make the library more +extensible, maintainable and easy-to-use. + +Below we've compiled upgrade instructions for the various services that +existed before. If you have a specific issue that is not addressed below, +please [submit an issue](/issues/new) or +[e-mail our support team](mailto:sdk-support@rackspace.com). + +* [Authentication](#authentication) +* [Servers](#servers) + * [List servers](#list-servers) + * [Get server details](#get-server-details) + * [Create server](#create-server) + * [Resize server](#resize-server) + * [Reboot server](#reboot-server) + * [Update server](#update-server) + * [Rebuild server](#rebuild-server) + * [Change admin password](#change-admin-password) + * [Delete server](#delete-server) + * [Rescue server](#rescue-server) +* [Images and flavors](#images-and-flavors) + * [List images](#list-images) + * [List flavors](#list-flavors) + * [Create/delete image](#createdelete-image) +* [Other](#other) + * [List keypairs](#list-keypairs) + * [Create/delete keypair](#createdelete-keypair) + * [List IP addresses](#list-ip-addresses) + +# Authentication + +One of the major differences that this release introduces is the level of +sub-packaging to differentiate between services and providers. You now have +the option of authenticating with OpenStack and other providers (like Rackspace). + +To authenticate with a vanilla OpenStack installation, you can either specify +your credentials like this: + +```go +import ( + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/openstack" +) + +opts := gophercloud.AuthOptions{ + IdentityEndpoint: "https://my-openstack.com:5000/v2.0", + Username: "{username}", + Password: "{password}", + TenantID: "{tenant_id}", +} +``` + +Or have them pulled in through environment variables, like this: + +```go +opts, err := openstack.AuthOptionsFromEnv() +``` + +Once you have your `AuthOptions` struct, you pass it in to get back a `Provider`, +like so: + +```go +provider, err := openstack.AuthenticatedClient(opts) +``` + +This provider is the top-level structure that all services are created from. + +# Servers + +Before you can interact with the Compute API, you need to retrieve a +`gophercloud.ServiceClient`. To do this: + +```go +// Define your region, etc. +opts := gophercloud.EndpointOpts{Region: "RegionOne"} + +client, err := openstack.NewComputeV2(provider, opts) +``` + +## List servers + +All operations that involve API collections (servers, flavors, images) now use +the `pagination.Pager` interface. This interface represents paginated entities +that can be iterated over. + +Once you have a Pager, you can then pass a callback function into its `EachPage` +method, and this will allow you to traverse over the collection and execute +arbitrary functionality. So, an example with list servers: + +```go +import ( + "fmt" + "github.com/rackspace/gophercloud/pagination" + "github.com/rackspace/gophercloud/openstack/compute/v2/servers" +) + +// We have the option of filtering the server list. If we want the full +// collection, leave it as an empty struct or nil +opts := servers.ListOpts{Name: "server_1"} + +// Retrieve a pager (i.e. a paginated collection) +pager := servers.List(client, opts) + +// Define an anonymous function to be executed on each page's iteration +err := pager.EachPage(func(page pagination.Page) (bool, error) { + serverList, err := servers.ExtractServers(page) + + // `s' will be a servers.Server struct + for _, s := range serverList { + fmt.Printf("We have a server. ID=%s, Name=%s", s.ID, s.Name) + } +}) +``` + +## Get server details + +```go +import "github.com/rackspace/gophercloud/openstack/compute/v2/servers" + +// Get the HTTP result +response := servers.Get(client, "server_id") + +// Extract a Server struct from the response +server, err := response.Extract() +``` + +## Create server + +```go +import "github.com/rackspace/gophercloud/openstack/compute/v2/servers" + +// Define our options +opts := servers.CreateOpts{ + Name: "new_server", + FlavorRef: "flavorID", + ImageRef: "imageID", +} + +// Get our response +response := servers.Create(client, opts) + +// Extract +server, err := response.Extract() +``` + +## Change admin password + +```go +import "github.com/rackspace/gophercloud/openstack/compute/v2/servers" + +result := servers.ChangeAdminPassword(client, "server_id", "newPassword_&123") +``` + +## Resize server + +```go +import "github.com/rackspace/gophercloud/openstack/compute/v2/servers" + +result := servers.Resize(client, "server_id", "new_flavor_id") +``` + +## Reboot server + +```go +import "github.com/rackspace/gophercloud/openstack/compute/v2/servers" + +// You have a choice of two reboot methods: servers.SoftReboot or servers.HardReboot +result := servers.Reboot(client, "server_id", servers.SoftReboot) +``` + +## Update server + +```go +import "github.com/rackspace/gophercloud/openstack/compute/v2/servers" + +opts := servers.UpdateOpts{Name: "new_name"} + +server, err := servers.Update(client, "server_id", opts).Extract() +``` + +## Rebuild server + +```go +import "github.com/rackspace/gophercloud/openstack/compute/v2/servers" + +// You have the option of specifying additional options +opts := RebuildOpts{ + Name: "new_name", + AdminPass: "admin_password", + ImageID: "image_id", + Metadata: map[string]string{"owner": "me"}, +} + +result := servers.Rebuild(client, "server_id", opts) + +// You can extract a servers.Server struct from the HTTP response +server, err := result.Extract() +``` + +## Delete server + +```go +import "github.com/rackspace/gophercloud/openstack/compute/v2/servers" + +response := servers.Delete(client, "server_id") +``` + +## Rescue server + +The server rescue extension for Compute is not currently supported. + +# Images and flavors + +## List images + +As with listing servers (see above), you first retrieve a Pager, and then pass +in a callback over each page: + +```go +import ( + "github.com/rackspace/gophercloud/pagination" + "github.com/rackspace/gophercloud/openstack/compute/v2/images" +) + +// We have the option of filtering the image list. If we want the full +// collection, leave it as an empty struct +opts := images.ListOpts{ChangesSince: "2014-01-01T01:02:03Z", Name: "Ubuntu 12.04"} + +// Retrieve a pager (i.e. a paginated collection) +pager := images.List(client, opts) + +// Define an anonymous function to be executed on each page's iteration +err := pager.EachPage(func(page pagination.Page) (bool, error) { + imageList, err := images.ExtractImages(page) + + for _, i := range imageList { + // "i" will be an images.Image + } +}) +``` + +## List flavors + +```go +import ( + "github.com/rackspace/gophercloud/pagination" + "github.com/rackspace/gophercloud/openstack/compute/v2/flavors" +) + +// We have the option of filtering the flavor list. If we want the full +// collection, leave it as an empty struct +opts := flavors.ListOpts{ChangesSince: "2014-01-01T01:02:03Z", MinRAM: 4} + +// Retrieve a pager (i.e. a paginated collection) +pager := flavors.List(client, opts) + +// Define an anonymous function to be executed on each page's iteration +err := pager.EachPage(func(page pagination.Page) (bool, error) { + flavorList, err := networks.ExtractFlavors(page) + + for _, f := range flavorList { + // "f" will be a flavors.Flavor + } +}) +``` + +## Create/delete image + +Image management has been shifted to Glance, but unfortunately this service is +not supported as of yet. You can, however, list Compute images like so: + +```go +import "github.com/rackspace/gophercloud/openstack/compute/v2/images" + +// Retrieve a pager (i.e. a paginated collection) +pager := images.List(client, opts) + +// Define an anonymous function to be executed on each page's iteration +err := pager.EachPage(func(page pagination.Page) (bool, error) { + imageList, err := images.ExtractImages(page) + + for _, i := range imageList { + // "i" will be an images.Image + } +}) +``` + +# Other + +## List keypairs + +```go +import "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs" + +// Retrieve a pager (i.e. a paginated collection) +pager := keypairs.List(client, opts) + +// Define an anonymous function to be executed on each page's iteration +err := pager.EachPage(func(page pagination.Page) (bool, error) { + keyList, err := keypairs.ExtractKeyPairs(page) + + for _, k := range keyList { + // "k" will be a keypairs.KeyPair + } +}) +``` + +## Create/delete keypairs + +To create a new keypair, you need to specify its name and, optionally, a +pregenerated OpenSSH-formatted public key. + +```go +import "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs" + +opts := keypairs.CreateOpts{ + Name: "new_key", + PublicKey: "...", +} + +response := keypairs.Create(client, opts) + +key, err := response.Extract() +``` + +To delete an existing keypair: + +```go +response := keypairs.Delete(client, "keypair_id") +``` + +## List IP addresses + +This operation is not currently supported. diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/README.md b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/README.md new file mode 100644 index 0000000000..3199837c20 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/README.md @@ -0,0 +1,57 @@ +# Gophercloud Acceptance tests + +The purpose of these acceptance tests is to validate that SDK features meet +the requirements of a contract - to consumers, other parts of the library, and +to a remote API. + +> **Note:** Because every test will be run against a real API endpoint, you +> may incur bandwidth and service charges for all the resource usage. These +> tests *should* remove their remote products automatically. However, there may +> be certain cases where this does not happen; always double-check to make sure +> you have no stragglers left behind. + +### Step 1. Set environment variables + +A lot of tests rely on environment variables for configuration - so you will need +to set them before running the suite. If you're testing against pure OpenStack APIs, +you can download a file that contains all of these variables for you: just visit +the `project/access_and_security` page in your control panel and click the "Download +OpenStack RC File" button at the top right. For all other providers, you will need +to set them manually. + +#### Authentication + +|Name|Description| +|---|---| +|`OS_USERNAME`|Your API username| +|`OS_PASSWORD`|Your API password| +|`OS_AUTH_URL`|The identity URL you need to authenticate| +|`OS_TENANT_NAME`|Your API tenant name| +|`OS_TENANT_ID`|Your API tenant ID| +|`RS_USERNAME`|Your Rackspace username| +|`RS_API_KEY`|Your Rackspace API key| + +#### General + +|Name|Description| +|---|---| +|`OS_REGION_NAME`|The region you want your resources to reside in| +|`RS_REGION`|Rackspace region you want your resource to reside in| + +#### Compute + +|Name|Description| +|---|---| +|`OS_IMAGE_ID`|The ID of the image your want your server to be based on| +|`OS_FLAVOR_ID`|The ID of the flavor you want your server to be based on| +|`OS_FLAVOR_ID_RESIZE`|The ID of the flavor you want your server to be resized to| +|`RS_IMAGE_ID`|The ID of the image you want servers to be created with| +|`RS_FLAVOR_ID`|The ID of the flavor you want your server to be created with| + +### 2. Run the test suite + +From the root directory, run: + +``` +./script/acceptancetest +``` diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/blockstorage/v1/snapshots_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/blockstorage/v1/snapshots_test.go new file mode 100644 index 0000000000..7741aa9841 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/blockstorage/v1/snapshots_test.go @@ -0,0 +1,70 @@ +// +build acceptance + +package v1 + +import ( + "testing" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots" + "github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestSnapshots(t *testing.T) { + + client, err := newClient(t) + th.AssertNoErr(t, err) + + v, err := volumes.Create(client, &volumes.CreateOpts{ + Name: "gophercloud-test-volume", + Size: 1, + }).Extract() + th.AssertNoErr(t, err) + + err = volumes.WaitForStatus(client, v.ID, "available", 120) + th.AssertNoErr(t, err) + + t.Logf("Created volume: %v\n", v) + + ss, err := snapshots.Create(client, &snapshots.CreateOpts{ + Name: "gophercloud-test-snapshot", + VolumeID: v.ID, + }).Extract() + th.AssertNoErr(t, err) + + err = snapshots.WaitForStatus(client, ss.ID, "available", 120) + th.AssertNoErr(t, err) + + t.Logf("Created snapshot: %+v\n", ss) + + err = snapshots.Delete(client, ss.ID).ExtractErr() + th.AssertNoErr(t, err) + + err = gophercloud.WaitFor(120, func() (bool, error) { + _, err := snapshots.Get(client, ss.ID).Extract() + if err != nil { + return true, nil + } + + return false, nil + }) + th.AssertNoErr(t, err) + + t.Log("Deleted snapshot\n") + + err = volumes.Delete(client, v.ID).ExtractErr() + th.AssertNoErr(t, err) + + err = gophercloud.WaitFor(120, func() (bool, error) { + _, err := volumes.Get(client, v.ID).Extract() + if err != nil { + return true, nil + } + + return false, nil + }) + th.AssertNoErr(t, err) + + t.Log("Deleted volume\n") +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/blockstorage/v1/volumes_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/blockstorage/v1/volumes_test.go new file mode 100644 index 0000000000..7760427f08 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/blockstorage/v1/volumes_test.go @@ -0,0 +1,63 @@ +// +build acceptance blockstorage + +package v1 + +import ( + "os" + "testing" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/openstack" + "github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" +) + +func newClient(t *testing.T) (*gophercloud.ServiceClient, error) { + ao, err := openstack.AuthOptionsFromEnv() + th.AssertNoErr(t, err) + + client, err := openstack.AuthenticatedClient(ao) + th.AssertNoErr(t, err) + + return openstack.NewBlockStorageV1(client, gophercloud.EndpointOpts{ + Region: os.Getenv("OS_REGION_NAME"), + }) +} + +func TestVolumes(t *testing.T) { + client, err := newClient(t) + th.AssertNoErr(t, err) + + cv, err := volumes.Create(client, &volumes.CreateOpts{ + Size: 1, + Name: "gophercloud-test-volume", + }).Extract() + th.AssertNoErr(t, err) + defer func() { + err = volumes.WaitForStatus(client, cv.ID, "available", 60) + th.AssertNoErr(t, err) + err = volumes.Delete(client, cv.ID).ExtractErr() + th.AssertNoErr(t, err) + }() + + _, err = volumes.Update(client, cv.ID, &volumes.UpdateOpts{ + Name: "gophercloud-updated-volume", + }).Extract() + th.AssertNoErr(t, err) + + v, err := volumes.Get(client, cv.ID).Extract() + th.AssertNoErr(t, err) + t.Logf("Got volume: %+v\n", v) + + if v.Name != "gophercloud-updated-volume" { + t.Errorf("Unable to update volume: Expected name: gophercloud-updated-volume\nActual name: %s", v.Name) + } + + err = volumes.List(client, &volumes.ListOpts{Name: "gophercloud-updated-volume"}).EachPage(func(page pagination.Page) (bool, error) { + vols, err := volumes.ExtractVolumes(page) + th.CheckEquals(t, 1, len(vols)) + return true, err + }) + th.AssertNoErr(t, err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/blockstorage/v1/volumetypes_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/blockstorage/v1/volumetypes_test.go new file mode 100644 index 0000000000..000bc01d57 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/blockstorage/v1/volumetypes_test.go @@ -0,0 +1,49 @@ +// +build acceptance + +package v1 + +import ( + "testing" + "time" + + "github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumetypes" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestVolumeTypes(t *testing.T) { + client, err := newClient(t) + th.AssertNoErr(t, err) + + vt, err := volumetypes.Create(client, &volumetypes.CreateOpts{ + ExtraSpecs: map[string]interface{}{ + "capabilities": "gpu", + "priority": 3, + }, + Name: "gophercloud-test-volumeType", + }).Extract() + th.AssertNoErr(t, err) + defer func() { + time.Sleep(10000 * time.Millisecond) + err = volumetypes.Delete(client, vt.ID).ExtractErr() + if err != nil { + t.Error(err) + return + } + }() + t.Logf("Created volume type: %+v\n", vt) + + vt, err = volumetypes.Get(client, vt.ID).Extract() + th.AssertNoErr(t, err) + t.Logf("Got volume type: %+v\n", vt) + + err = volumetypes.List(client).EachPage(func(page pagination.Page) (bool, error) { + volTypes, err := volumetypes.ExtractVolumeTypes(page) + if len(volTypes) != 1 { + t.Errorf("Expected 1 volume type, got %d", len(volTypes)) + } + t.Logf("Listing volume types: %+v\n", volTypes) + return true, err + }) + th.AssertNoErr(t, err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/client_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/client_test.go new file mode 100644 index 0000000000..6e88819d80 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/client_test.go @@ -0,0 +1,40 @@ +// +build acceptance + +package openstack + +import ( + "os" + "testing" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/openstack" +) + +func TestAuthenticatedClient(t *testing.T) { + // Obtain credentials from the environment. + ao, err := openstack.AuthOptionsFromEnv() + if err != nil { + t.Fatalf("Unable to acquire credentials: %v", err) + } + + client, err := openstack.AuthenticatedClient(ao) + if err != nil { + t.Fatalf("Unable to authenticate: %v", err) + } + + if client.TokenID == "" { + t.Errorf("No token ID assigned to the client") + } + + t.Logf("Client successfully acquired a token: %v", client.TokenID) + + // Find the storage service in the service catalog. + storage, err := openstack.NewObjectStorageV1(client, gophercloud.EndpointOpts{ + Region: os.Getenv("OS_REGION_NAME"), + }) + if err != nil { + t.Errorf("Unable to locate a storage service: %v", err) + } else { + t.Logf("Located a storage service at endpoint: [%s]", storage.Endpoint) + } +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/bootfromvolume_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/bootfromvolume_test.go new file mode 100644 index 0000000000..add0e5fc11 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/bootfromvolume_test.go @@ -0,0 +1,55 @@ +// +build acceptance + +package v2 + +import ( + "testing" + + "github.com/rackspace/gophercloud/acceptance/tools" + "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/bootfromvolume" + "github.com/rackspace/gophercloud/openstack/compute/v2/servers" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestBootFromVolume(t *testing.T) { + client, err := newClient() + th.AssertNoErr(t, err) + + if testing.Short() { + t.Skip("Skipping test that requires server creation in short mode.") + } + + choices, err := ComputeChoicesFromEnv() + if err != nil { + t.Fatal(err) + } + + name := tools.RandomString("Gophercloud-", 8) + t.Logf("Creating server [%s].", name) + + bd := []bootfromvolume.BlockDevice{ + bootfromvolume.BlockDevice{ + UUID: choices.ImageID, + SourceType: bootfromvolume.Image, + VolumeSize: 10, + }, + } + + serverCreateOpts := servers.CreateOpts{ + Name: name, + FlavorRef: choices.FlavorID, + ImageRef: choices.ImageID, + } + server, err := bootfromvolume.Create(client, bootfromvolume.CreateOptsExt{ + serverCreateOpts, + bd, + }).Extract() + th.AssertNoErr(t, err) + if err = waitForStatus(client, server, "ACTIVE"); err != nil { + t.Fatal(err) + } + + t.Logf("Created server: %+v\n", server) + defer servers.Delete(client, server.ID) + t.Logf("Deleting server [%s]...", name) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/compute_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/compute_test.go new file mode 100644 index 0000000000..33e49fea9b --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/compute_test.go @@ -0,0 +1,97 @@ +// +build acceptance common + +package v2 + +import ( + "fmt" + "os" + "strings" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/acceptance/tools" + "github.com/rackspace/gophercloud/openstack" + "github.com/rackspace/gophercloud/openstack/compute/v2/servers" +) + +func newClient() (*gophercloud.ServiceClient, error) { + ao, err := openstack.AuthOptionsFromEnv() + if err != nil { + return nil, err + } + + client, err := openstack.AuthenticatedClient(ao) + if err != nil { + return nil, err + } + + return openstack.NewComputeV2(client, gophercloud.EndpointOpts{ + Region: os.Getenv("OS_REGION_NAME"), + }) +} + +func waitForStatus(client *gophercloud.ServiceClient, server *servers.Server, status string) error { + return tools.WaitFor(func() (bool, error) { + latest, err := servers.Get(client, server.ID).Extract() + if err != nil { + return false, err + } + + if latest.Status == status { + // Success! + return true, nil + } + + return false, nil + }) +} + +// ComputeChoices contains image and flavor selections for use by the acceptance tests. +type ComputeChoices struct { + // ImageID contains the ID of a valid image. + ImageID string + + // FlavorID contains the ID of a valid flavor. + FlavorID string + + // FlavorIDResize contains the ID of a different flavor available on the same OpenStack installation, that is distinct + // from FlavorID. + FlavorIDResize string +} + +// ComputeChoicesFromEnv populates a ComputeChoices struct from environment variables. +// If any required state is missing, an `error` will be returned that enumerates the missing properties. +func ComputeChoicesFromEnv() (*ComputeChoices, error) { + imageID := os.Getenv("OS_IMAGE_ID") + flavorID := os.Getenv("OS_FLAVOR_ID") + flavorIDResize := os.Getenv("OS_FLAVOR_ID_RESIZE") + + missing := make([]string, 0, 3) + if imageID == "" { + missing = append(missing, "OS_IMAGE_ID") + } + if flavorID == "" { + missing = append(missing, "OS_FLAVOR_ID") + } + if flavorIDResize == "" { + missing = append(missing, "OS_FLAVOR_ID_RESIZE") + } + + notDistinct := "" + if flavorID == flavorIDResize { + notDistinct = "OS_FLAVOR_ID and OS_FLAVOR_ID_RESIZE must be distinct." + } + + if len(missing) > 0 || notDistinct != "" { + text := "You're missing some important setup:\n" + if len(missing) > 0 { + text += " * These environment variables must be provided: " + strings.Join(missing, ", ") + "\n" + } + if notDistinct != "" { + text += " * " + notDistinct + "\n" + } + + return nil, fmt.Errorf(text) + } + + return &ComputeChoices{ImageID: imageID, FlavorID: flavorID, FlavorIDResize: flavorIDResize}, nil +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/extension_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/extension_test.go new file mode 100644 index 0000000000..1356ffa899 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/extension_test.go @@ -0,0 +1,47 @@ +// +build acceptance compute extensionss + +package v2 + +import ( + "testing" + + "github.com/rackspace/gophercloud/openstack/compute/v2/extensions" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestListExtensions(t *testing.T) { + client, err := newClient() + th.AssertNoErr(t, err) + + err = extensions.List(client).EachPage(func(page pagination.Page) (bool, error) { + t.Logf("--- Page ---") + + exts, err := extensions.ExtractExtensions(page) + th.AssertNoErr(t, err) + + for i, ext := range exts { + t.Logf("[%02d] name=[%s]\n", i, ext.Name) + t.Logf(" alias=[%s]\n", ext.Alias) + t.Logf(" description=[%s]\n", ext.Description) + } + + return true, nil + }) + th.AssertNoErr(t, err) +} + +func TestGetExtension(t *testing.T) { + client, err := newClient() + th.AssertNoErr(t, err) + + ext, err := extensions.Get(client, "os-admin-actions").Extract() + th.AssertNoErr(t, err) + + t.Logf("Extension details:") + t.Logf(" name=[%s]\n", ext.Name) + t.Logf(" namespace=[%s]\n", ext.Namespace) + t.Logf(" alias=[%s]\n", ext.Alias) + t.Logf(" description=[%s]\n", ext.Description) + t.Logf(" updated=[%s]\n", ext.Updated) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/flavors_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/flavors_test.go new file mode 100644 index 0000000000..9f51b12280 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/flavors_test.go @@ -0,0 +1,57 @@ +// +build acceptance compute flavors + +package v2 + +import ( + "testing" + + "github.com/rackspace/gophercloud/openstack/compute/v2/flavors" + "github.com/rackspace/gophercloud/pagination" +) + +func TestListFlavors(t *testing.T) { + client, err := newClient() + if err != nil { + t.Fatalf("Unable to create a compute client: %v", err) + } + + t.Logf("ID\tRegion\tName\tStatus\tCreated") + + pager := flavors.ListDetail(client, nil) + count, pages := 0, 0 + pager.EachPage(func(page pagination.Page) (bool, error) { + t.Logf("---") + pages++ + flavors, err := flavors.ExtractFlavors(page) + if err != nil { + return false, err + } + + for _, f := range flavors { + t.Logf("%s\t%s\t%d\t%d\t%d", f.ID, f.Name, f.RAM, f.Disk, f.VCPUs) + } + + return true, nil + }) + + t.Logf("--------\n%d flavors listed on %d pages.", count, pages) +} + +func TestGetFlavor(t *testing.T) { + client, err := newClient() + if err != nil { + t.Fatalf("Unable to create a compute client: %v", err) + } + + choices, err := ComputeChoicesFromEnv() + if err != nil { + t.Fatal(err) + } + + flavor, err := flavors.Get(client, choices.FlavorID).Extract() + if err != nil { + t.Fatalf("Unable to get flavor information: %v", err) + } + + t.Logf("Flavor: %#v", flavor) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/images_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/images_test.go new file mode 100644 index 0000000000..ceab22fa76 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/images_test.go @@ -0,0 +1,37 @@ +// +build acceptance compute images + +package v2 + +import ( + "testing" + + "github.com/rackspace/gophercloud/openstack/compute/v2/images" + "github.com/rackspace/gophercloud/pagination" +) + +func TestListImages(t *testing.T) { + client, err := newClient() + if err != nil { + t.Fatalf("Unable to create a compute: client: %v", err) + } + + t.Logf("ID\tRegion\tName\tStatus\tCreated") + + pager := images.ListDetail(client, nil) + count, pages := 0, 0 + pager.EachPage(func(page pagination.Page) (bool, error) { + pages++ + images, err := images.ExtractImages(page) + if err != nil { + return false, err + } + + for _, i := range images { + t.Logf("%s\t%s\t%s\t%s", i.ID, i.Name, i.Status, i.Created) + } + + return true, nil + }) + + t.Logf("--------\n%d images listed on %d pages.", count, pages) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/keypairs_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/keypairs_test.go new file mode 100644 index 0000000000..3e12d6b3cd --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/keypairs_test.go @@ -0,0 +1,74 @@ +// +build acceptance + +package v2 + +import ( + "crypto/rand" + "crypto/rsa" + "testing" + + "github.com/rackspace/gophercloud/acceptance/tools" + "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs" + "github.com/rackspace/gophercloud/openstack/compute/v2/servers" + th "github.com/rackspace/gophercloud/testhelper" + + "code.google.com/p/go.crypto/ssh" +) + +const keyName = "gophercloud_test_key_pair" + +func TestCreateServerWithKeyPair(t *testing.T) { + client, err := newClient() + th.AssertNoErr(t, err) + + if testing.Short() { + t.Skip("Skipping test that requires server creation in short mode.") + } + + privateKey, err := rsa.GenerateKey(rand.Reader, 2048) + publicKey := privateKey.PublicKey + pub, err := ssh.NewPublicKey(&publicKey) + th.AssertNoErr(t, err) + pubBytes := ssh.MarshalAuthorizedKey(pub) + pk := string(pubBytes) + + kp, err := keypairs.Create(client, keypairs.CreateOpts{ + Name: keyName, + PublicKey: pk, + }).Extract() + th.AssertNoErr(t, err) + t.Logf("Created key pair: %s\n", kp) + + choices, err := ComputeChoicesFromEnv() + th.AssertNoErr(t, err) + + name := tools.RandomString("Gophercloud-", 8) + t.Logf("Creating server [%s] with key pair.", name) + + serverCreateOpts := servers.CreateOpts{ + Name: name, + FlavorRef: choices.FlavorID, + ImageRef: choices.ImageID, + } + + server, err := servers.Create(client, keypairs.CreateOptsExt{ + serverCreateOpts, + keyName, + }).Extract() + th.AssertNoErr(t, err) + defer servers.Delete(client, server.ID) + if err = waitForStatus(client, server, "ACTIVE"); err != nil { + t.Fatalf("Unable to wait for server: %v", err) + } + + server, err = servers.Get(client, server.ID).Extract() + t.Logf("Created server: %+v\n", server) + th.AssertNoErr(t, err) + th.AssertEquals(t, server.KeyName, keyName) + + t.Logf("Deleting key pair [%s]...", kp.Name) + err = keypairs.Delete(client, keyName).ExtractErr() + th.AssertNoErr(t, err) + + t.Logf("Deleting server [%s]...", name) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/pkg.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/pkg.go new file mode 100644 index 0000000000..bb158c3eec --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/pkg.go @@ -0,0 +1,3 @@ +// The v2 package contains acceptance tests for the Openstack Compute V2 service. + +package v2 diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/secdefrules_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/secdefrules_test.go new file mode 100644 index 0000000000..78b07986bd --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/secdefrules_test.go @@ -0,0 +1,72 @@ +// +build acceptance compute defsecrules + +package v2 + +import ( + "testing" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/acceptance/tools" + dsr "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/defsecrules" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestSecDefRules(t *testing.T) { + client, err := newClient() + th.AssertNoErr(t, err) + + id := createDefRule(t, client) + + listDefRules(t, client) + + getDefRule(t, client, id) + + deleteDefRule(t, client, id) +} + +func createDefRule(t *testing.T, client *gophercloud.ServiceClient) string { + opts := dsr.CreateOpts{ + FromPort: tools.RandomInt(80, 89), + ToPort: tools.RandomInt(90, 99), + IPProtocol: "TCP", + CIDR: "0.0.0.0/0", + } + + rule, err := dsr.Create(client, opts).Extract() + th.AssertNoErr(t, err) + + t.Logf("Created default rule %s", rule.ID) + + return rule.ID +} + +func listDefRules(t *testing.T, client *gophercloud.ServiceClient) { + err := dsr.List(client).EachPage(func(page pagination.Page) (bool, error) { + drList, err := dsr.ExtractDefaultRules(page) + th.AssertNoErr(t, err) + + for _, dr := range drList { + t.Logf("Listing default rule %s: Name [%s] From Port [%s] To Port [%s] Protocol [%s]", + dr.ID, dr.FromPort, dr.ToPort, dr.IPProtocol) + } + + return true, nil + }) + + th.AssertNoErr(t, err) +} + +func getDefRule(t *testing.T, client *gophercloud.ServiceClient, id string) { + rule, err := dsr.Get(client, id).Extract() + th.AssertNoErr(t, err) + + t.Logf("Getting rule %s: %#v", id, rule) +} + +func deleteDefRule(t *testing.T, client *gophercloud.ServiceClient, id string) { + err := dsr.Delete(client, id).ExtractErr() + th.AssertNoErr(t, err) + + t.Logf("Deleted rule %s", id) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/secgroup_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/secgroup_test.go new file mode 100644 index 0000000000..4f50739109 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/secgroup_test.go @@ -0,0 +1,177 @@ +// +build acceptance compute secgroups + +package v2 + +import ( + "testing" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/acceptance/tools" + "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/secgroups" + "github.com/rackspace/gophercloud/openstack/compute/v2/servers" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestSecGroups(t *testing.T) { + client, err := newClient() + th.AssertNoErr(t, err) + + serverID, needsDeletion := findServer(t, client) + + groupID := createSecGroup(t, client) + + listSecGroups(t, client) + + newName := tools.RandomString("secgroup_", 5) + updateSecGroup(t, client, groupID, newName) + + getSecGroup(t, client, groupID) + + addRemoveRules(t, client, groupID) + + addServerToSecGroup(t, client, serverID, newName) + + removeServerFromSecGroup(t, client, serverID, newName) + + if needsDeletion { + servers.Delete(client, serverID) + } + + deleteSecGroup(t, client, groupID) +} + +func createSecGroup(t *testing.T, client *gophercloud.ServiceClient) string { + opts := secgroups.CreateOpts{ + Name: tools.RandomString("secgroup_", 5), + Description: "something", + } + + group, err := secgroups.Create(client, opts).Extract() + th.AssertNoErr(t, err) + + t.Logf("Created secgroup %s %s", group.ID, group.Name) + + return group.ID +} + +func listSecGroups(t *testing.T, client *gophercloud.ServiceClient) { + err := secgroups.List(client).EachPage(func(page pagination.Page) (bool, error) { + secGrpList, err := secgroups.ExtractSecurityGroups(page) + th.AssertNoErr(t, err) + + for _, sg := range secGrpList { + t.Logf("Listing secgroup %s: Name [%s] Desc [%s] TenantID [%s]", sg.ID, + sg.Name, sg.Description, sg.TenantID) + } + + return true, nil + }) + + th.AssertNoErr(t, err) +} + +func updateSecGroup(t *testing.T, client *gophercloud.ServiceClient, id, newName string) { + opts := secgroups.UpdateOpts{ + Name: newName, + Description: tools.RandomString("dec_", 10), + } + group, err := secgroups.Update(client, id, opts).Extract() + th.AssertNoErr(t, err) + + t.Logf("Updated %s's name to %s", group.ID, group.Name) +} + +func getSecGroup(t *testing.T, client *gophercloud.ServiceClient, id string) { + group, err := secgroups.Get(client, id).Extract() + th.AssertNoErr(t, err) + + t.Logf("Getting %s: %#v", id, group) +} + +func addRemoveRules(t *testing.T, client *gophercloud.ServiceClient, id string) { + opts := secgroups.CreateRuleOpts{ + ParentGroupID: id, + FromPort: 22, + ToPort: 22, + IPProtocol: "TCP", + CIDR: "0.0.0.0/0", + } + + rule, err := secgroups.CreateRule(client, opts).Extract() + th.AssertNoErr(t, err) + + t.Logf("Adding rule %s to group %s", rule.ID, id) + + err = secgroups.DeleteRule(client, rule.ID).ExtractErr() + th.AssertNoErr(t, err) + + t.Logf("Deleted rule %s from group %s", rule.ID, id) +} + +func findServer(t *testing.T, client *gophercloud.ServiceClient) (string, bool) { + var serverID string + var needsDeletion bool + + err := servers.List(client, nil).EachPage(func(page pagination.Page) (bool, error) { + sList, err := servers.ExtractServers(page) + th.AssertNoErr(t, err) + + for _, s := range sList { + serverID = s.ID + needsDeletion = false + + t.Logf("Found an existing server: ID [%s]", serverID) + break + } + + return true, nil + }) + th.AssertNoErr(t, err) + + if serverID == "" { + t.Log("No server found, creating one") + + choices, err := ComputeChoicesFromEnv() + th.AssertNoErr(t, err) + + opts := &servers.CreateOpts{ + Name: tools.RandomString("secgroup_test_", 5), + ImageRef: choices.ImageID, + FlavorRef: choices.FlavorID, + } + + s, err := servers.Create(client, opts).Extract() + th.AssertNoErr(t, err) + serverID = s.ID + + t.Logf("Created server %s, waiting for it to build", s.ID) + err = servers.WaitForStatus(client, serverID, "ACTIVE", 300) + th.AssertNoErr(t, err) + + needsDeletion = true + } + + return serverID, needsDeletion +} + +func addServerToSecGroup(t *testing.T, client *gophercloud.ServiceClient, serverID, groupName string) { + err := secgroups.AddServerToGroup(client, serverID, groupName).ExtractErr() + th.AssertNoErr(t, err) + + t.Logf("Adding group %s to server %s", groupName, serverID) +} + +func removeServerFromSecGroup(t *testing.T, client *gophercloud.ServiceClient, serverID, groupName string) { + err := secgroups.RemoveServerFromGroup(client, serverID, groupName).ExtractErr() + th.AssertNoErr(t, err) + + t.Logf("Removing group %s from server %s", groupName, serverID) +} + +func deleteSecGroup(t *testing.T, client *gophercloud.ServiceClient, id string) { + err := secgroups.Delete(client, id).ExtractErr() + th.AssertNoErr(t, err) + + t.Logf("Deleted group %s", id) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/servers_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/servers_test.go new file mode 100644 index 0000000000..d52a9d3537 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/servers_test.go @@ -0,0 +1,450 @@ +// +build acceptance compute servers + +package v2 + +import ( + "os" + "testing" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/acceptance/tools" + "github.com/rackspace/gophercloud/openstack" + "github.com/rackspace/gophercloud/openstack/compute/v2/servers" + "github.com/rackspace/gophercloud/openstack/networking/v2/networks" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestListServers(t *testing.T) { + client, err := newClient() + if err != nil { + t.Fatalf("Unable to create a compute client: %v", err) + } + + t.Logf("ID\tRegion\tName\tStatus\tIPv4\tIPv6") + + pager := servers.List(client, servers.ListOpts{}) + count, pages := 0, 0 + pager.EachPage(func(page pagination.Page) (bool, error) { + pages++ + t.Logf("---") + + servers, err := servers.ExtractServers(page) + if err != nil { + return false, err + } + + for _, s := range servers { + t.Logf("%s\t%s\t%s\t%s\t%s\t\n", s.ID, s.Name, s.Status, s.AccessIPv4, s.AccessIPv6) + count++ + } + + return true, nil + }) + + t.Logf("--------\n%d servers listed on %d pages.\n", count, pages) +} + +func networkingClient() (*gophercloud.ServiceClient, error) { + opts, err := openstack.AuthOptionsFromEnv() + if err != nil { + return nil, err + } + + provider, err := openstack.AuthenticatedClient(opts) + if err != nil { + return nil, err + } + + return openstack.NewNetworkV2(provider, gophercloud.EndpointOpts{ + Name: "neutron", + Region: os.Getenv("OS_REGION_NAME"), + }) +} + +func createServer(t *testing.T, client *gophercloud.ServiceClient, choices *ComputeChoices) (*servers.Server, error) { + if testing.Short() { + t.Skip("Skipping test that requires server creation in short mode.") + } + + var network networks.Network + + networkingClient, err := networkingClient() + if err != nil { + t.Fatalf("Unable to create a networking client: %v", err) + } + + pager := networks.List(networkingClient, networks.ListOpts{Name: "public", Limit: 1}) + pager.EachPage(func(page pagination.Page) (bool, error) { + networks, err := networks.ExtractNetworks(page) + if err != nil { + t.Errorf("Failed to extract networks: %v", err) + return false, err + } + + if len(networks) == 0 { + t.Fatalf("No networks to attach to server") + return false, err + } + + network = networks[0] + + return false, nil + }) + + name := tools.RandomString("ACPTTEST", 16) + t.Logf("Attempting to create server: %s\n", name) + + pwd := tools.MakeNewPassword("") + + server, err := servers.Create(client, servers.CreateOpts{ + Name: name, + FlavorRef: choices.FlavorID, + ImageRef: choices.ImageID, + Networks: []servers.Network{ + servers.Network{UUID: network.ID}, + }, + AdminPass: pwd, + }).Extract() + if err != nil { + t.Fatalf("Unable to create server: %v", err) + } + + th.AssertEquals(t, pwd, server.AdminPass) + + return server, err +} + +func TestCreateDestroyServer(t *testing.T) { + choices, err := ComputeChoicesFromEnv() + if err != nil { + t.Fatal(err) + } + + client, err := newClient() + if err != nil { + t.Fatalf("Unable to create a compute client: %v", err) + } + + server, err := createServer(t, client, choices) + if err != nil { + t.Fatalf("Unable to create server: %v", err) + } + defer func() { + servers.Delete(client, server.ID) + t.Logf("Server deleted.") + }() + + if err = waitForStatus(client, server, "ACTIVE"); err != nil { + t.Fatalf("Unable to wait for server: %v", err) + } +} + +func TestUpdateServer(t *testing.T) { + client, err := newClient() + if err != nil { + t.Fatalf("Unable to create a compute client: %v", err) + } + + choices, err := ComputeChoicesFromEnv() + if err != nil { + t.Fatal(err) + } + + server, err := createServer(t, client, choices) + if err != nil { + t.Fatal(err) + } + defer servers.Delete(client, server.ID) + + if err = waitForStatus(client, server, "ACTIVE"); err != nil { + t.Fatal(err) + } + + alternateName := tools.RandomString("ACPTTEST", 16) + for alternateName == server.Name { + alternateName = tools.RandomString("ACPTTEST", 16) + } + + t.Logf("Attempting to rename the server to %s.", alternateName) + + updated, err := servers.Update(client, server.ID, servers.UpdateOpts{Name: alternateName}).Extract() + if err != nil { + t.Fatalf("Unable to rename server: %v", err) + } + + if updated.ID != server.ID { + t.Errorf("Updated server ID [%s] didn't match original server ID [%s]!", updated.ID, server.ID) + } + + err = tools.WaitFor(func() (bool, error) { + latest, err := servers.Get(client, updated.ID).Extract() + if err != nil { + return false, err + } + + return latest.Name == alternateName, nil + }) +} + +func TestActionChangeAdminPassword(t *testing.T) { + t.Parallel() + + client, err := newClient() + if err != nil { + t.Fatalf("Unable to create a compute client: %v", err) + } + + choices, err := ComputeChoicesFromEnv() + if err != nil { + t.Fatal(err) + } + + server, err := createServer(t, client, choices) + if err != nil { + t.Fatal(err) + } + defer servers.Delete(client, server.ID) + + if err = waitForStatus(client, server, "ACTIVE"); err != nil { + t.Fatal(err) + } + + randomPassword := tools.MakeNewPassword(server.AdminPass) + res := servers.ChangeAdminPassword(client, server.ID, randomPassword) + if res.Err != nil { + t.Fatal(err) + } + + if err = waitForStatus(client, server, "PASSWORD"); err != nil { + t.Fatal(err) + } + + if err = waitForStatus(client, server, "ACTIVE"); err != nil { + t.Fatal(err) + } +} + +func TestActionReboot(t *testing.T) { + t.Parallel() + + client, err := newClient() + if err != nil { + t.Fatalf("Unable to create a compute client: %v", err) + } + + choices, err := ComputeChoicesFromEnv() + if err != nil { + t.Fatal(err) + } + + server, err := createServer(t, client, choices) + if err != nil { + t.Fatal(err) + } + defer servers.Delete(client, server.ID) + + if err = waitForStatus(client, server, "ACTIVE"); err != nil { + t.Fatal(err) + } + + res := servers.Reboot(client, server.ID, "aldhjflaskhjf") + if res.Err == nil { + t.Fatal("Expected the SDK to provide an ArgumentError here") + } + + t.Logf("Attempting reboot of server %s", server.ID) + res = servers.Reboot(client, server.ID, servers.OSReboot) + if res.Err != nil { + t.Fatalf("Unable to reboot server: %v", err) + } + + if err = waitForStatus(client, server, "REBOOT"); err != nil { + t.Fatal(err) + } + + if err = waitForStatus(client, server, "ACTIVE"); err != nil { + t.Fatal(err) + } +} + +func TestActionRebuild(t *testing.T) { + t.Parallel() + + client, err := newClient() + if err != nil { + t.Fatalf("Unable to create a compute client: %v", err) + } + + choices, err := ComputeChoicesFromEnv() + if err != nil { + t.Fatal(err) + } + + server, err := createServer(t, client, choices) + if err != nil { + t.Fatal(err) + } + defer servers.Delete(client, server.ID) + + if err = waitForStatus(client, server, "ACTIVE"); err != nil { + t.Fatal(err) + } + + t.Logf("Attempting to rebuild server %s", server.ID) + + rebuildOpts := servers.RebuildOpts{ + Name: tools.RandomString("ACPTTEST", 16), + AdminPass: tools.MakeNewPassword(server.AdminPass), + ImageID: choices.ImageID, + } + + rebuilt, err := servers.Rebuild(client, server.ID, rebuildOpts).Extract() + if err != nil { + t.Fatal(err) + } + + if rebuilt.ID != server.ID { + t.Errorf("Expected rebuilt server ID of [%s]; got [%s]", server.ID, rebuilt.ID) + } + + if err = waitForStatus(client, rebuilt, "REBUILD"); err != nil { + t.Fatal(err) + } + + if err = waitForStatus(client, rebuilt, "ACTIVE"); err != nil { + t.Fatal(err) + } +} + +func resizeServer(t *testing.T, client *gophercloud.ServiceClient, server *servers.Server, choices *ComputeChoices) { + if err := waitForStatus(client, server, "ACTIVE"); err != nil { + t.Fatal(err) + } + + t.Logf("Attempting to resize server [%s]", server.ID) + + opts := &servers.ResizeOpts{ + FlavorRef: choices.FlavorIDResize, + } + if res := servers.Resize(client, server.ID, opts); res.Err != nil { + t.Fatal(res.Err) + } + + if err := waitForStatus(client, server, "VERIFY_RESIZE"); err != nil { + t.Fatal(err) + } +} + +func TestActionResizeConfirm(t *testing.T) { + t.Parallel() + + choices, err := ComputeChoicesFromEnv() + if err != nil { + t.Fatal(err) + } + + client, err := newClient() + if err != nil { + t.Fatalf("Unable to create a compute client: %v", err) + } + + server, err := createServer(t, client, choices) + if err != nil { + t.Fatal(err) + } + defer servers.Delete(client, server.ID) + resizeServer(t, client, server, choices) + + t.Logf("Attempting to confirm resize for server %s", server.ID) + + if res := servers.ConfirmResize(client, server.ID); res.Err != nil { + t.Fatal(err) + } + + if err = waitForStatus(client, server, "ACTIVE"); err != nil { + t.Fatal(err) + } +} + +func TestActionResizeRevert(t *testing.T) { + t.Parallel() + + choices, err := ComputeChoicesFromEnv() + if err != nil { + t.Fatal(err) + } + + client, err := newClient() + if err != nil { + t.Fatalf("Unable to create a compute client: %v", err) + } + + server, err := createServer(t, client, choices) + if err != nil { + t.Fatal(err) + } + defer servers.Delete(client, server.ID) + resizeServer(t, client, server, choices) + + t.Logf("Attempting to revert resize for server %s", server.ID) + + if res := servers.RevertResize(client, server.ID); res.Err != nil { + t.Fatal(err) + } + + if err = waitForStatus(client, server, "ACTIVE"); err != nil { + t.Fatal(err) + } +} + +func TestServerMetadata(t *testing.T) { + t.Parallel() + + choices, err := ComputeChoicesFromEnv() + th.AssertNoErr(t, err) + + client, err := newClient() + if err != nil { + t.Fatalf("Unable to create a compute client: %v", err) + } + + server, err := createServer(t, client, choices) + if err != nil { + t.Fatal(err) + } + defer servers.Delete(client, server.ID) + if err = waitForStatus(client, server, "ACTIVE"); err != nil { + t.Fatal(err) + } + + metadata, err := servers.UpdateMetadata(client, server.ID, servers.MetadataOpts{ + "foo": "bar", + "this": "that", + }).Extract() + th.AssertNoErr(t, err) + t.Logf("UpdateMetadata result: %+v\n", metadata) + + err = servers.DeleteMetadatum(client, server.ID, "foo").ExtractErr() + th.AssertNoErr(t, err) + + metadata, err = servers.CreateMetadatum(client, server.ID, servers.MetadatumOpts{ + "foo": "baz", + }).Extract() + th.AssertNoErr(t, err) + t.Logf("CreateMetadatum result: %+v\n", metadata) + + metadata, err = servers.Metadatum(client, server.ID, "foo").Extract() + th.AssertNoErr(t, err) + t.Logf("Metadatum result: %+v\n", metadata) + th.AssertEquals(t, "baz", metadata["foo"]) + + metadata, err = servers.Metadata(client, server.ID).Extract() + th.AssertNoErr(t, err) + t.Logf("Metadata result: %+v\n", metadata) + + metadata, err = servers.ResetMetadata(client, server.ID, servers.MetadataOpts{}).Extract() + th.AssertNoErr(t, err) + t.Logf("ResetMetadata result: %+v\n", metadata) + th.AssertDeepEquals(t, map[string]string{}, metadata) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v2/extension_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v2/extension_test.go new file mode 100644 index 0000000000..d1fa1e3dce --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v2/extension_test.go @@ -0,0 +1,46 @@ +// +build acceptance identity + +package v2 + +import ( + "testing" + + extensions2 "github.com/rackspace/gophercloud/openstack/identity/v2/extensions" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestEnumerateExtensions(t *testing.T) { + service := authenticatedClient(t) + + t.Logf("Extensions available on this identity endpoint:") + count := 0 + err := extensions2.List(service).EachPage(func(page pagination.Page) (bool, error) { + t.Logf("--- Page %02d ---", count) + + extensions, err := extensions2.ExtractExtensions(page) + th.AssertNoErr(t, err) + + for i, ext := range extensions { + t.Logf("[%02d] name=[%s] namespace=[%s]", i, ext.Name, ext.Namespace) + t.Logf(" alias=[%s] updated=[%s]", ext.Alias, ext.Updated) + t.Logf(" description=[%s]", ext.Description) + } + + count++ + return true, nil + }) + th.AssertNoErr(t, err) +} + +func TestGetExtension(t *testing.T) { + service := authenticatedClient(t) + + ext, err := extensions2.Get(service, "OS-KSCRUD").Extract() + th.AssertNoErr(t, err) + + th.CheckEquals(t, "OpenStack Keystone User CRUD", ext.Name) + th.CheckEquals(t, "http://docs.openstack.org/identity/api/ext/OS-KSCRUD/v1.0", ext.Namespace) + th.CheckEquals(t, "OS-KSCRUD", ext.Alias) + th.CheckEquals(t, "OpenStack extensions to Keystone v2.0 API enabling User Operations.", ext.Description) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v2/identity_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v2/identity_test.go new file mode 100644 index 0000000000..96bf1fdade --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v2/identity_test.go @@ -0,0 +1,47 @@ +// +build acceptance identity + +package v2 + +import ( + "testing" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/openstack" + th "github.com/rackspace/gophercloud/testhelper" +) + +func v2AuthOptions(t *testing.T) gophercloud.AuthOptions { + // Obtain credentials from the environment. + ao, err := openstack.AuthOptionsFromEnv() + th.AssertNoErr(t, err) + + // Trim out unused fields. Prefer authentication by API key to password. + ao.UserID, ao.DomainID, ao.DomainName = "", "", "" + if ao.APIKey != "" { + ao.Password = "" + } + + return ao +} + +func createClient(t *testing.T, auth bool) *gophercloud.ServiceClient { + ao := v2AuthOptions(t) + + provider, err := openstack.NewClient(ao.IdentityEndpoint) + th.AssertNoErr(t, err) + + if auth { + err = openstack.AuthenticateV2(provider, ao) + th.AssertNoErr(t, err) + } + + return openstack.NewIdentityV2(provider) +} + +func unauthenticatedClient(t *testing.T) *gophercloud.ServiceClient { + return createClient(t, false) +} + +func authenticatedClient(t *testing.T) *gophercloud.ServiceClient { + return createClient(t, true) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v2/pkg.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v2/pkg.go new file mode 100644 index 0000000000..5ec3cc8e83 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v2/pkg.go @@ -0,0 +1 @@ +package v2 diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v2/role_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v2/role_test.go new file mode 100644 index 0000000000..ba243fe02b --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v2/role_test.go @@ -0,0 +1,58 @@ +// +build acceptance identity roles + +package v2 + +import ( + "testing" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/openstack/identity/v2/extensions/admin/roles" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestRoles(t *testing.T) { + client := authenticatedClient(t) + + tenantID := findTenant(t, client) + userID := createUser(t, client, tenantID) + roleID := listRoles(t, client) + + addUserRole(t, client, tenantID, userID, roleID) + + deleteUserRole(t, client, tenantID, userID, roleID) + + deleteUser(t, client, userID) +} + +func listRoles(t *testing.T, client *gophercloud.ServiceClient) string { + var roleID string + + err := roles.List(client).EachPage(func(page pagination.Page) (bool, error) { + roleList, err := roles.ExtractRoles(page) + th.AssertNoErr(t, err) + + for _, role := range roleList { + t.Logf("Listing role: ID [%s] Name [%s]", role.ID, role.Name) + roleID = role.ID + } + + return true, nil + }) + + th.AssertNoErr(t, err) + + return roleID +} + +func addUserRole(t *testing.T, client *gophercloud.ServiceClient, tenantID, userID, roleID string) { + err := roles.AddUserRole(client, tenantID, userID, roleID).ExtractErr() + th.AssertNoErr(t, err) + t.Logf("Added role %s to user %s", roleID, userID) +} + +func deleteUserRole(t *testing.T, client *gophercloud.ServiceClient, tenantID, userID, roleID string) { + err := roles.DeleteUserRole(client, tenantID, userID, roleID).ExtractErr() + th.AssertNoErr(t, err) + t.Logf("Removed role %s from user %s", roleID, userID) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v2/tenant_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v2/tenant_test.go new file mode 100644 index 0000000000..578fc483b8 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v2/tenant_test.go @@ -0,0 +1,32 @@ +// +build acceptance identity + +package v2 + +import ( + "testing" + + tenants2 "github.com/rackspace/gophercloud/openstack/identity/v2/tenants" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestEnumerateTenants(t *testing.T) { + service := authenticatedClient(t) + + t.Logf("Tenants to which your current token grants access:") + count := 0 + err := tenants2.List(service, nil).EachPage(func(page pagination.Page) (bool, error) { + t.Logf("--- Page %02d ---", count) + + tenants, err := tenants2.ExtractTenants(page) + th.AssertNoErr(t, err) + for i, tenant := range tenants { + t.Logf("[%02d] name=[%s] id=[%s] description=[%s] enabled=[%v]", + i, tenant.Name, tenant.ID, tenant.Description, tenant.Enabled) + } + + count++ + return true, nil + }) + th.AssertNoErr(t, err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v2/token_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v2/token_test.go new file mode 100644 index 0000000000..d903140855 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v2/token_test.go @@ -0,0 +1,38 @@ +// +build acceptance identity + +package v2 + +import ( + "testing" + + tokens2 "github.com/rackspace/gophercloud/openstack/identity/v2/tokens" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestAuthenticate(t *testing.T) { + ao := v2AuthOptions(t) + service := unauthenticatedClient(t) + + // Authenticated! + result := tokens2.Create(service, tokens2.WrapOptions(ao)) + + // Extract and print the token. + token, err := result.ExtractToken() + th.AssertNoErr(t, err) + + t.Logf("Acquired token: [%s]", token.ID) + t.Logf("The token will expire at: [%s]", token.ExpiresAt.String()) + t.Logf("The token is valid for tenant: [%#v]", token.Tenant) + + // Extract and print the service catalog. + catalog, err := result.ExtractServiceCatalog() + th.AssertNoErr(t, err) + + t.Logf("Acquired service catalog listing [%d] services", len(catalog.Entries)) + for i, entry := range catalog.Entries { + t.Logf("[%02d]: name=[%s], type=[%s]", i, entry.Name, entry.Type) + for _, endpoint := range entry.Endpoints { + t.Logf(" - region=[%s] publicURL=[%s]", endpoint.Region, endpoint.PublicURL) + } + } +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v2/user_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v2/user_test.go new file mode 100644 index 0000000000..fe73d19898 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v2/user_test.go @@ -0,0 +1,127 @@ +// +build acceptance identity + +package v2 + +import ( + "strconv" + "testing" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/acceptance/tools" + "github.com/rackspace/gophercloud/openstack/identity/v2/tenants" + "github.com/rackspace/gophercloud/openstack/identity/v2/users" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestUsers(t *testing.T) { + client := authenticatedClient(t) + + tenantID := findTenant(t, client) + + userID := createUser(t, client, tenantID) + + listUsers(t, client) + + getUser(t, client, userID) + + updateUser(t, client, userID) + + listUserRoles(t, client, tenantID, userID) + + deleteUser(t, client, userID) +} + +func findTenant(t *testing.T, client *gophercloud.ServiceClient) string { + var tenantID string + err := tenants.List(client, nil).EachPage(func(page pagination.Page) (bool, error) { + tenantList, err := tenants.ExtractTenants(page) + th.AssertNoErr(t, err) + + for _, t := range tenantList { + tenantID = t.ID + break + } + + return true, nil + }) + th.AssertNoErr(t, err) + + return tenantID +} + +func createUser(t *testing.T, client *gophercloud.ServiceClient, tenantID string) string { + t.Log("Creating user") + + opts := users.CreateOpts{ + Name: tools.RandomString("user_", 5), + Enabled: users.Disabled, + TenantID: tenantID, + Email: "new_user@foo.com", + } + + user, err := users.Create(client, opts).Extract() + th.AssertNoErr(t, err) + t.Logf("Created user %s on tenant %s", user.ID, tenantID) + + return user.ID +} + +func listUsers(t *testing.T, client *gophercloud.ServiceClient) { + err := users.List(client).EachPage(func(page pagination.Page) (bool, error) { + userList, err := users.ExtractUsers(page) + th.AssertNoErr(t, err) + + for _, user := range userList { + t.Logf("Listing user: ID [%s] Name [%s] Email [%s] Enabled? [%s]", + user.ID, user.Name, user.Email, strconv.FormatBool(user.Enabled)) + } + + return true, nil + }) + + th.AssertNoErr(t, err) +} + +func getUser(t *testing.T, client *gophercloud.ServiceClient, userID string) { + _, err := users.Get(client, userID).Extract() + th.AssertNoErr(t, err) + t.Logf("Getting user %s", userID) +} + +func updateUser(t *testing.T, client *gophercloud.ServiceClient, userID string) { + opts := users.UpdateOpts{Name: tools.RandomString("new_name", 5), Email: "new@foo.com"} + user, err := users.Update(client, userID, opts).Extract() + th.AssertNoErr(t, err) + t.Logf("Updated user %s: Name [%s] Email [%s]", userID, user.Name, user.Email) +} + +func listUserRoles(t *testing.T, client *gophercloud.ServiceClient, tenantID, userID string) { + count := 0 + err := users.ListRoles(client, tenantID, userID).EachPage(func(page pagination.Page) (bool, error) { + count++ + + roleList, err := users.ExtractRoles(page) + th.AssertNoErr(t, err) + + t.Logf("Listing roles for user %s", userID) + + for _, r := range roleList { + t.Logf("- %s (%s)", r.Name, r.ID) + } + + return true, nil + }) + + if count == 0 { + t.Logf("No roles for user %s", userID) + } + + th.AssertNoErr(t, err) +} + +func deleteUser(t *testing.T, client *gophercloud.ServiceClient, userID string) { + res := users.Delete(client, userID) + th.AssertNoErr(t, res.Err) + t.Logf("Deleted user %s", userID) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v3/endpoint_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v3/endpoint_test.go new file mode 100644 index 0000000000..ea893c2dea --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v3/endpoint_test.go @@ -0,0 +1,111 @@ +// +build acceptance + +package v3 + +import ( + "testing" + + "github.com/rackspace/gophercloud" + endpoints3 "github.com/rackspace/gophercloud/openstack/identity/v3/endpoints" + services3 "github.com/rackspace/gophercloud/openstack/identity/v3/services" + "github.com/rackspace/gophercloud/pagination" +) + +func TestListEndpoints(t *testing.T) { + // Create a service client. + serviceClient := createAuthenticatedClient(t) + if serviceClient == nil { + return + } + + // Use the service to list all available endpoints. + pager := endpoints3.List(serviceClient, endpoints3.ListOpts{}) + err := pager.EachPage(func(page pagination.Page) (bool, error) { + t.Logf("--- Page ---") + + endpoints, err := endpoints3.ExtractEndpoints(page) + if err != nil { + t.Fatalf("Error extracting endpoings: %v", err) + } + + for _, endpoint := range endpoints { + t.Logf("Endpoint: %8s %10s %9s %s", + endpoint.ID, + endpoint.Availability, + endpoint.Name, + endpoint.URL) + } + + return true, nil + }) + if err != nil { + t.Errorf("Unexpected error while iterating endpoint pages: %v", err) + } +} + +func TestNavigateCatalog(t *testing.T) { + // Create a service client. + client := createAuthenticatedClient(t) + if client == nil { + return + } + + var compute *services3.Service + var endpoint *endpoints3.Endpoint + + // Discover the service we're interested in. + servicePager := services3.List(client, services3.ListOpts{ServiceType: "compute"}) + err := servicePager.EachPage(func(page pagination.Page) (bool, error) { + part, err := services3.ExtractServices(page) + if err != nil { + return false, err + } + if compute != nil { + t.Fatalf("Expected one service, got more than one page") + return false, nil + } + if len(part) != 1 { + t.Fatalf("Expected one service, got %d", len(part)) + return false, nil + } + + compute = &part[0] + return true, nil + }) + if err != nil { + t.Fatalf("Unexpected error iterating pages: %v", err) + } + + if compute == nil { + t.Fatalf("No compute service found.") + } + + // Enumerate the endpoints available for this service. + computePager := endpoints3.List(client, endpoints3.ListOpts{ + Availability: gophercloud.AvailabilityPublic, + ServiceID: compute.ID, + }) + err = computePager.EachPage(func(page pagination.Page) (bool, error) { + part, err := endpoints3.ExtractEndpoints(page) + if err != nil { + return false, err + } + if endpoint != nil { + t.Fatalf("Expected one endpoint, got more than one page") + return false, nil + } + if len(part) != 1 { + t.Fatalf("Expected one endpoint, got %d", len(part)) + return false, nil + } + + endpoint = &part[0] + return true, nil + }) + + if endpoint == nil { + t.Fatalf("No endpoint found.") + } + + t.Logf("Success. The compute endpoint is at %s.", endpoint.URL) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v3/identity_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v3/identity_test.go new file mode 100644 index 0000000000..ce64345886 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v3/identity_test.go @@ -0,0 +1,39 @@ +// +build acceptance + +package v3 + +import ( + "testing" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/openstack" + th "github.com/rackspace/gophercloud/testhelper" +) + +func createAuthenticatedClient(t *testing.T) *gophercloud.ServiceClient { + // Obtain credentials from the environment. + ao, err := openstack.AuthOptionsFromEnv() + th.AssertNoErr(t, err) + + // Trim out unused fields. + ao.Username, ao.TenantID, ao.TenantName = "", "", "" + + if ao.UserID == "" { + t.Logf("Skipping identity v3 tests because no OS_USERID is present.") + return nil + } + + // Create a client and manually authenticate against v3. + providerClient, err := openstack.NewClient(ao.IdentityEndpoint) + if err != nil { + t.Fatalf("Unable to instantiate client: %v", err) + } + + err = openstack.AuthenticateV3(providerClient, ao) + if err != nil { + t.Fatalf("Unable to authenticate against identity v3: %v", err) + } + + // Create a service client. + return openstack.NewIdentityV3(providerClient) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v3/pkg.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v3/pkg.go new file mode 100644 index 0000000000..eac3ae96a1 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v3/pkg.go @@ -0,0 +1 @@ +package v3 diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v3/service_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v3/service_test.go new file mode 100644 index 0000000000..082bd11e74 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v3/service_test.go @@ -0,0 +1,36 @@ +// +build acceptance + +package v3 + +import ( + "testing" + + services3 "github.com/rackspace/gophercloud/openstack/identity/v3/services" + "github.com/rackspace/gophercloud/pagination" +) + +func TestListServices(t *testing.T) { + // Create a service client. + serviceClient := createAuthenticatedClient(t) + if serviceClient == nil { + return + } + + // Use the client to list all available services. + pager := services3.List(serviceClient, services3.ListOpts{}) + err := pager.EachPage(func(page pagination.Page) (bool, error) { + parts, err := services3.ExtractServices(page) + if err != nil { + return false, err + } + + t.Logf("--- Page ---") + for _, service := range parts { + t.Logf("Service: %32s %15s %10s %s", service.ID, service.Type, service.Name, *service.Description) + } + return true, nil + }) + if err != nil { + t.Errorf("Unexpected error traversing pages: %v", err) + } +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v3/token_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v3/token_test.go new file mode 100644 index 0000000000..4342ade03c --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v3/token_test.go @@ -0,0 +1,42 @@ +// +build acceptance + +package v3 + +import ( + "testing" + + "github.com/rackspace/gophercloud/openstack" + tokens3 "github.com/rackspace/gophercloud/openstack/identity/v3/tokens" +) + +func TestGetToken(t *testing.T) { + // Obtain credentials from the environment. + ao, err := openstack.AuthOptionsFromEnv() + if err != nil { + t.Fatalf("Unable to acquire credentials: %v", err) + } + + // Trim out unused fields. Skip if we don't have a UserID. + ao.Username, ao.TenantID, ao.TenantName = "", "", "" + if ao.UserID == "" { + t.Logf("Skipping identity v3 tests because no OS_USERID is present.") + return + } + + // Create an unauthenticated client. + provider, err := openstack.NewClient(ao.IdentityEndpoint) + if err != nil { + t.Fatalf("Unable to instantiate client: %v", err) + } + + // Create a service client. + service := openstack.NewIdentityV3(provider) + + // Use the service to create a token. + token, err := tokens3.Create(service, ao, nil).Extract() + if err != nil { + t.Fatalf("Unable to get token: %v", err) + } + + t.Logf("Acquired token: %s", token.ID) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/apiversion_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/apiversion_test.go new file mode 100644 index 0000000000..99e1d01187 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/apiversion_test.go @@ -0,0 +1,51 @@ +// +build acceptance networking + +package v2 + +import ( + "testing" + + "github.com/rackspace/gophercloud/openstack/networking/v2/apiversions" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestListAPIVersions(t *testing.T) { + Setup(t) + defer Teardown() + + pager := apiversions.ListVersions(Client) + err := pager.EachPage(func(page pagination.Page) (bool, error) { + t.Logf("--- Page ---") + + versions, err := apiversions.ExtractAPIVersions(page) + th.AssertNoErr(t, err) + + for _, v := range versions { + t.Logf("API Version: ID [%s] Status [%s]", v.ID, v.Status) + } + + return true, nil + }) + th.CheckNoErr(t, err) +} + +func TestListAPIResources(t *testing.T) { + Setup(t) + defer Teardown() + + pager := apiversions.ListVersionResources(Client, "v2.0") + err := pager.EachPage(func(page pagination.Page) (bool, error) { + t.Logf("--- Page ---") + + vrs, err := apiversions.ExtractVersionResources(page) + th.AssertNoErr(t, err) + + for _, vr := range vrs { + t.Logf("Network: Name [%s] Collection [%s]", vr.Name, vr.Collection) + } + + return true, nil + }) + th.CheckNoErr(t, err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/common.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/common.go new file mode 100644 index 0000000000..1efac2c081 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/common.go @@ -0,0 +1,39 @@ +package v2 + +import ( + "os" + "testing" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/openstack" + th "github.com/rackspace/gophercloud/testhelper" +) + +var Client *gophercloud.ServiceClient + +func NewClient() (*gophercloud.ServiceClient, error) { + opts, err := openstack.AuthOptionsFromEnv() + if err != nil { + return nil, err + } + + provider, err := openstack.AuthenticatedClient(opts) + if err != nil { + return nil, err + } + + return openstack.NewNetworkV2(provider, gophercloud.EndpointOpts{ + Name: "neutron", + Region: os.Getenv("OS_REGION_NAME"), + }) +} + +func Setup(t *testing.T) { + client, err := NewClient() + th.AssertNoErr(t, err) + Client = client +} + +func Teardown() { + Client = nil +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extension_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extension_test.go new file mode 100644 index 0000000000..edcbba4fd1 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extension_test.go @@ -0,0 +1,45 @@ +// +build acceptance networking + +package v2 + +import ( + "testing" + + "github.com/rackspace/gophercloud/openstack/networking/v2/extensions" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestListExts(t *testing.T) { + Setup(t) + defer Teardown() + + pager := extensions.List(Client) + err := pager.EachPage(func(page pagination.Page) (bool, error) { + t.Logf("--- Page ---") + + exts, err := extensions.ExtractExtensions(page) + th.AssertNoErr(t, err) + + for _, ext := range exts { + t.Logf("Extension: Name [%s] Description [%s]", ext.Name, ext.Description) + } + + return true, nil + }) + th.CheckNoErr(t, err) +} + +func TestGetExt(t *testing.T) { + Setup(t) + defer Teardown() + + ext, err := extensions.Get(Client, "service-type").Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, ext.Updated, "2013-01-20T00:00:00-00:00") + th.AssertEquals(t, ext.Name, "Neutron Service Type Management") + th.AssertEquals(t, ext.Namespace, "http://docs.openstack.org/ext/neutron/service-type/api/v1.0") + th.AssertEquals(t, ext.Alias, "service-type") + th.AssertEquals(t, ext.Description, "API for retrieving service providers for Neutron advanced services") +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extensions/layer3_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extensions/layer3_test.go new file mode 100644 index 0000000000..63e0be39d7 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extensions/layer3_test.go @@ -0,0 +1,300 @@ +// +build acceptance networking layer3ext + +package extensions + +import ( + "testing" + + base "github.com/rackspace/gophercloud/acceptance/openstack/networking/v2" + "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/external" + "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/floatingips" + "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/routers" + "github.com/rackspace/gophercloud/openstack/networking/v2/networks" + "github.com/rackspace/gophercloud/openstack/networking/v2/ports" + "github.com/rackspace/gophercloud/openstack/networking/v2/subnets" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" +) + +const ( + cidr1 = "10.0.0.1/24" + cidr2 = "20.0.0.1/24" +) + +func TestAll(t *testing.T) { + base.Setup(t) + defer base.Teardown() + + testRouter(t) + testFloatingIP(t) +} + +func testRouter(t *testing.T) { + // Setup: Create network + networkID := createNetwork(t) + + // Create router + routerID := createRouter(t, networkID) + + // Lists routers + listRouters(t) + + // Update router + updateRouter(t, routerID) + + // Get router + getRouter(t, routerID) + + // Create new subnet. Note: this subnet will be deleted when networkID is deleted + subnetID := createSubnet(t, networkID, cidr2) + + // Add interface + addInterface(t, routerID, subnetID) + + // Remove interface + removeInterface(t, routerID, subnetID) + + // Delete router + deleteRouter(t, routerID) + + // Cleanup + deleteNetwork(t, networkID) +} + +func testFloatingIP(t *testing.T) { + // Setup external network + extNetworkID := createNetwork(t) + + // Setup internal network, subnet and port + intNetworkID, subnetID, portID := createInternalTopology(t) + + // Now the important part: we need to allow the external network to talk to + // the internal subnet. For this we need a router that has an interface to + // the internal subnet. + routerID := bridgeIntSubnetWithExtNetwork(t, extNetworkID, subnetID) + + // Create floating IP + ipID := createFloatingIP(t, extNetworkID, portID) + + // Get floating IP + getFloatingIP(t, ipID) + + // Update floating IP + updateFloatingIP(t, ipID, portID) + + // Delete floating IP + deleteFloatingIP(t, ipID) + + // Remove the internal subnet interface + removeInterface(t, routerID, subnetID) + + // Delete router and external network + deleteRouter(t, routerID) + deleteNetwork(t, extNetworkID) + + // Delete internal port and network + deletePort(t, portID) + deleteNetwork(t, intNetworkID) +} + +func createNetwork(t *testing.T) string { + t.Logf("Creating a network") + + asu := true + opts := external.CreateOpts{ + Parent: networks.CreateOpts{Name: "sample_network", AdminStateUp: &asu}, + External: true, + } + n, err := networks.Create(base.Client, opts).Extract() + + th.AssertNoErr(t, err) + + if n.ID == "" { + t.Fatalf("No ID returned when creating a network") + } + + createSubnet(t, n.ID, cidr1) + + t.Logf("Network created: ID [%s]", n.ID) + + return n.ID +} + +func deleteNetwork(t *testing.T, networkID string) { + t.Logf("Deleting network %s", networkID) + networks.Delete(base.Client, networkID) +} + +func deletePort(t *testing.T, portID string) { + t.Logf("Deleting port %s", portID) + ports.Delete(base.Client, portID) +} + +func createInternalTopology(t *testing.T) (string, string, string) { + t.Logf("Creating an internal network (for port)") + opts := networks.CreateOpts{Name: "internal_network"} + n, err := networks.Create(base.Client, opts).Extract() + th.AssertNoErr(t, err) + + // A subnet is also needed + subnetID := createSubnet(t, n.ID, cidr2) + + t.Logf("Creating an internal port on network %s", n.ID) + p, err := ports.Create(base.Client, ports.CreateOpts{ + NetworkID: n.ID, + Name: "fixed_internal_port", + }).Extract() + th.AssertNoErr(t, err) + + return n.ID, subnetID, p.ID +} + +func bridgeIntSubnetWithExtNetwork(t *testing.T, networkID, subnetID string) string { + // Create router with external gateway info + routerID := createRouter(t, networkID) + + // Add interface for internal subnet + addInterface(t, routerID, subnetID) + + return routerID +} + +func createSubnet(t *testing.T, networkID, cidr string) string { + t.Logf("Creating a subnet for network %s", networkID) + + iFalse := false + s, err := subnets.Create(base.Client, subnets.CreateOpts{ + NetworkID: networkID, + CIDR: cidr, + IPVersion: subnets.IPv4, + Name: "my_subnet", + EnableDHCP: &iFalse, + }).Extract() + + th.AssertNoErr(t, err) + + t.Logf("Subnet created: ID [%s]", s.ID) + + return s.ID +} + +func createRouter(t *testing.T, networkID string) string { + t.Logf("Creating a router for network %s", networkID) + + asu := false + gwi := routers.GatewayInfo{NetworkID: networkID} + r, err := routers.Create(base.Client, routers.CreateOpts{ + Name: "foo_router", + AdminStateUp: &asu, + GatewayInfo: &gwi, + }).Extract() + + th.AssertNoErr(t, err) + + if r.ID == "" { + t.Fatalf("No ID returned when creating a router") + } + + t.Logf("Router created: ID [%s]", r.ID) + + return r.ID +} + +func listRouters(t *testing.T) { + pager := routers.List(base.Client, routers.ListOpts{}) + + err := pager.EachPage(func(page pagination.Page) (bool, error) { + routerList, err := routers.ExtractRouters(page) + th.AssertNoErr(t, err) + + for _, r := range routerList { + t.Logf("Listing router: ID [%s] Name [%s] Status [%s] GatewayInfo [%#v]", + r.ID, r.Name, r.Status, r.GatewayInfo) + } + + return true, nil + }) + + th.AssertNoErr(t, err) +} + +func updateRouter(t *testing.T, routerID string) { + _, err := routers.Update(base.Client, routerID, routers.UpdateOpts{ + Name: "another_name", + }).Extract() + + th.AssertNoErr(t, err) +} + +func getRouter(t *testing.T, routerID string) { + r, err := routers.Get(base.Client, routerID).Extract() + + th.AssertNoErr(t, err) + + t.Logf("Getting router: ID [%s] Name [%s] Status [%s]", r.ID, r.Name, r.Status) +} + +func addInterface(t *testing.T, routerID, subnetID string) { + ir, err := routers.AddInterface(base.Client, routerID, routers.InterfaceOpts{SubnetID: subnetID}).Extract() + + th.AssertNoErr(t, err) + + t.Logf("Interface added to router %s: SubnetID [%s] PortID [%s]", routerID, ir.SubnetID, ir.PortID) +} + +func removeInterface(t *testing.T, routerID, subnetID string) { + ir, err := routers.RemoveInterface(base.Client, routerID, routers.InterfaceOpts{SubnetID: subnetID}).Extract() + + th.AssertNoErr(t, err) + + t.Logf("Interface %s removed from %s", ir.ID, routerID) +} + +func deleteRouter(t *testing.T, routerID string) { + t.Logf("Deleting router %s", routerID) + + res := routers.Delete(base.Client, routerID) + + th.AssertNoErr(t, res.Err) +} + +func createFloatingIP(t *testing.T, networkID, portID string) string { + t.Logf("Creating floating IP on network [%s] with port [%s]", networkID, portID) + + opts := floatingips.CreateOpts{ + FloatingNetworkID: networkID, + PortID: portID, + } + + ip, err := floatingips.Create(base.Client, opts).Extract() + + th.AssertNoErr(t, err) + + t.Logf("Floating IP created: ID [%s] Status [%s] Fixed (internal) IP: [%s] Floating (external) IP: [%s]", + ip.ID, ip.Status, ip.FixedIP, ip.FloatingIP) + + return ip.ID +} + +func getFloatingIP(t *testing.T, ipID string) { + ip, err := floatingips.Get(base.Client, ipID).Extract() + th.AssertNoErr(t, err) + + t.Logf("Getting floating IP: ID [%s] Status [%s]", ip.ID, ip.Status) +} + +func updateFloatingIP(t *testing.T, ipID, portID string) { + t.Logf("Disassociate all ports from IP %s", ipID) + _, err := floatingips.Update(base.Client, ipID, floatingips.UpdateOpts{PortID: ""}).Extract() + th.AssertNoErr(t, err) + + t.Logf("Re-associate the port %s", portID) + _, err = floatingips.Update(base.Client, ipID, floatingips.UpdateOpts{PortID: portID}).Extract() + th.AssertNoErr(t, err) +} + +func deleteFloatingIP(t *testing.T, ipID string) { + t.Logf("Deleting IP %s", ipID) + res := floatingips.Delete(base.Client, ipID) + th.AssertNoErr(t, res.Err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extensions/lbaas/common.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extensions/lbaas/common.go new file mode 100644 index 0000000000..27dfe5f8b7 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extensions/lbaas/common.go @@ -0,0 +1,78 @@ +package lbaas + +import ( + "testing" + + base "github.com/rackspace/gophercloud/acceptance/openstack/networking/v2" + "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/monitors" + "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/pools" + "github.com/rackspace/gophercloud/openstack/networking/v2/networks" + "github.com/rackspace/gophercloud/openstack/networking/v2/subnets" + th "github.com/rackspace/gophercloud/testhelper" +) + +func SetupTopology(t *testing.T) (string, string) { + // create network + n, err := networks.Create(base.Client, networks.CreateOpts{Name: "tmp_network"}).Extract() + th.AssertNoErr(t, err) + + t.Logf("Created network %s", n.ID) + + // create subnet + s, err := subnets.Create(base.Client, subnets.CreateOpts{ + NetworkID: n.ID, + CIDR: "192.168.199.0/24", + IPVersion: subnets.IPv4, + Name: "tmp_subnet", + }).Extract() + th.AssertNoErr(t, err) + + t.Logf("Created subnet %s", s.ID) + + return n.ID, s.ID +} + +func DeleteTopology(t *testing.T, networkID string) { + res := networks.Delete(base.Client, networkID) + th.AssertNoErr(t, res.Err) + t.Logf("Deleted network %s", networkID) +} + +func CreatePool(t *testing.T, subnetID string) string { + p, err := pools.Create(base.Client, pools.CreateOpts{ + LBMethod: pools.LBMethodRoundRobin, + Protocol: "HTTP", + Name: "tmp_pool", + SubnetID: subnetID, + }).Extract() + + th.AssertNoErr(t, err) + + t.Logf("Created pool %s", p.ID) + + return p.ID +} + +func DeletePool(t *testing.T, poolID string) { + res := pools.Delete(base.Client, poolID) + th.AssertNoErr(t, res.Err) + t.Logf("Deleted pool %s", poolID) +} + +func CreateMonitor(t *testing.T) string { + m, err := monitors.Create(base.Client, monitors.CreateOpts{ + Delay: 10, + Timeout: 10, + MaxRetries: 3, + Type: monitors.TypeHTTP, + ExpectedCodes: "200", + URLPath: "/login", + HTTPMethod: "GET", + }).Extract() + + th.AssertNoErr(t, err) + + t.Logf("Created monitor ID [%s]", m.ID) + + return m.ID +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extensions/lbaas/member_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extensions/lbaas/member_test.go new file mode 100644 index 0000000000..9b60582d14 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extensions/lbaas/member_test.go @@ -0,0 +1,95 @@ +// +build acceptance networking lbaas lbaasmember + +package lbaas + +import ( + "testing" + + base "github.com/rackspace/gophercloud/acceptance/openstack/networking/v2" + "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/members" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestMembers(t *testing.T) { + base.Setup(t) + defer base.Teardown() + + // setup + networkID, subnetID := SetupTopology(t) + poolID := CreatePool(t, subnetID) + + // create member + memberID := createMember(t, poolID) + + // list members + listMembers(t) + + // update member + updateMember(t, memberID) + + // get member + getMember(t, memberID) + + // delete member + deleteMember(t, memberID) + + // teardown + DeletePool(t, poolID) + DeleteTopology(t, networkID) +} + +func createMember(t *testing.T, poolID string) string { + m, err := members.Create(base.Client, members.CreateOpts{ + Address: "192.168.199.1", + ProtocolPort: 8080, + PoolID: poolID, + }).Extract() + + th.AssertNoErr(t, err) + + t.Logf("Created member: ID [%s] Status [%s] Weight [%d] Address [%s] Port [%d]", + m.ID, m.Status, m.Weight, m.Address, m.ProtocolPort) + + return m.ID +} + +func listMembers(t *testing.T) { + err := members.List(base.Client, members.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + memberList, err := members.ExtractMembers(page) + if err != nil { + t.Errorf("Failed to extract members: %v", err) + return false, err + } + + for _, m := range memberList { + t.Logf("Listing member: ID [%s] Status [%s]", m.ID, m.Status) + } + + return true, nil + }) + + th.AssertNoErr(t, err) +} + +func updateMember(t *testing.T, memberID string) { + m, err := members.Update(base.Client, memberID, members.UpdateOpts{AdminStateUp: true}).Extract() + + th.AssertNoErr(t, err) + + t.Logf("Updated member ID [%s]", m.ID) +} + +func getMember(t *testing.T, memberID string) { + m, err := members.Get(base.Client, memberID).Extract() + + th.AssertNoErr(t, err) + + t.Logf("Getting member ID [%s]", m.ID) +} + +func deleteMember(t *testing.T, memberID string) { + res := members.Delete(base.Client, memberID) + th.AssertNoErr(t, res.Err) + t.Logf("Deleted member %s", memberID) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extensions/lbaas/monitor_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extensions/lbaas/monitor_test.go new file mode 100644 index 0000000000..9056fff671 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extensions/lbaas/monitor_test.go @@ -0,0 +1,77 @@ +// +build acceptance networking lbaas lbaasmonitor + +package lbaas + +import ( + "testing" + + base "github.com/rackspace/gophercloud/acceptance/openstack/networking/v2" + "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/monitors" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestMonitors(t *testing.T) { + base.Setup(t) + defer base.Teardown() + + // create monitor + monitorID := CreateMonitor(t) + + // list monitors + listMonitors(t) + + // update monitor + updateMonitor(t, monitorID) + + // get monitor + getMonitor(t, monitorID) + + // delete monitor + deleteMonitor(t, monitorID) +} + +func listMonitors(t *testing.T) { + err := monitors.List(base.Client, monitors.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + monitorList, err := monitors.ExtractMonitors(page) + if err != nil { + t.Errorf("Failed to extract monitors: %v", err) + return false, err + } + + for _, m := range monitorList { + t.Logf("Listing monitor: ID [%s] Type [%s] Delay [%ds] Timeout [%d] Retries [%d] Status [%s]", + m.ID, m.Type, m.Delay, m.Timeout, m.MaxRetries, m.Status) + } + + return true, nil + }) + + th.AssertNoErr(t, err) +} + +func updateMonitor(t *testing.T, monitorID string) { + opts := monitors.UpdateOpts{Delay: 10, Timeout: 10, MaxRetries: 3} + m, err := monitors.Update(base.Client, monitorID, opts).Extract() + + th.AssertNoErr(t, err) + + t.Logf("Updated monitor ID [%s]", m.ID) +} + +func getMonitor(t *testing.T, monitorID string) { + m, err := monitors.Get(base.Client, monitorID).Extract() + + th.AssertNoErr(t, err) + + t.Logf("Getting monitor ID [%s]: URL path [%s] HTTP Method [%s] Accepted codes [%s]", + m.ID, m.URLPath, m.HTTPMethod, m.ExpectedCodes) +} + +func deleteMonitor(t *testing.T, monitorID string) { + res := monitors.Delete(base.Client, monitorID) + + th.AssertNoErr(t, res.Err) + + t.Logf("Deleted monitor %s", monitorID) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extensions/lbaas/pkg.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extensions/lbaas/pkg.go new file mode 100644 index 0000000000..f5a7df7b75 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extensions/lbaas/pkg.go @@ -0,0 +1 @@ +package lbaas diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extensions/lbaas/pool_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extensions/lbaas/pool_test.go new file mode 100644 index 0000000000..81940649c5 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extensions/lbaas/pool_test.go @@ -0,0 +1,98 @@ +// +build acceptance networking lbaas lbaaspool + +package lbaas + +import ( + "testing" + + base "github.com/rackspace/gophercloud/acceptance/openstack/networking/v2" + "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/pools" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestPools(t *testing.T) { + base.Setup(t) + defer base.Teardown() + + // setup + networkID, subnetID := SetupTopology(t) + + // create pool + poolID := CreatePool(t, subnetID) + + // list pools + listPools(t) + + // update pool + updatePool(t, poolID) + + // get pool + getPool(t, poolID) + + // create monitor + monitorID := CreateMonitor(t) + + // associate health monitor + associateMonitor(t, poolID, monitorID) + + // disassociate health monitor + disassociateMonitor(t, poolID, monitorID) + + // delete pool + DeletePool(t, poolID) + + // teardown + DeleteTopology(t, networkID) +} + +func listPools(t *testing.T) { + err := pools.List(base.Client, pools.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + poolList, err := pools.ExtractPools(page) + if err != nil { + t.Errorf("Failed to extract pools: %v", err) + return false, err + } + + for _, p := range poolList { + t.Logf("Listing pool: ID [%s] Name [%s] Status [%s] LB algorithm [%s]", p.ID, p.Name, p.Status, p.LBMethod) + } + + return true, nil + }) + + th.AssertNoErr(t, err) +} + +func updatePool(t *testing.T, poolID string) { + opts := pools.UpdateOpts{Name: "SuperPool", LBMethod: pools.LBMethodLeastConnections} + p, err := pools.Update(base.Client, poolID, opts).Extract() + + th.AssertNoErr(t, err) + + t.Logf("Updated pool ID [%s]", p.ID) +} + +func getPool(t *testing.T, poolID string) { + p, err := pools.Get(base.Client, poolID).Extract() + + th.AssertNoErr(t, err) + + t.Logf("Getting pool ID [%s]", p.ID) +} + +func associateMonitor(t *testing.T, poolID, monitorID string) { + res := pools.AssociateMonitor(base.Client, poolID, monitorID) + + th.AssertNoErr(t, res.Err) + + t.Logf("Associated pool %s with monitor %s", poolID, monitorID) +} + +func disassociateMonitor(t *testing.T, poolID, monitorID string) { + res := pools.DisassociateMonitor(base.Client, poolID, monitorID) + + th.AssertNoErr(t, res.Err) + + t.Logf("Disassociated pool %s with monitor %s", poolID, monitorID) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extensions/lbaas/vip_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extensions/lbaas/vip_test.go new file mode 100644 index 0000000000..c8dff2d93f --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extensions/lbaas/vip_test.go @@ -0,0 +1,101 @@ +// +build acceptance networking lbaas lbaasvip + +package lbaas + +import ( + "testing" + + base "github.com/rackspace/gophercloud/acceptance/openstack/networking/v2" + "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/vips" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestVIPs(t *testing.T) { + base.Setup(t) + defer base.Teardown() + + // setup + networkID, subnetID := SetupTopology(t) + poolID := CreatePool(t, subnetID) + + // create VIP + VIPID := createVIP(t, subnetID, poolID) + + // list VIPs + listVIPs(t) + + // update VIP + updateVIP(t, VIPID) + + // get VIP + getVIP(t, VIPID) + + // delete VIP + deleteVIP(t, VIPID) + + // teardown + DeletePool(t, poolID) + DeleteTopology(t, networkID) +} + +func createVIP(t *testing.T, subnetID, poolID string) string { + p, err := vips.Create(base.Client, vips.CreateOpts{ + Protocol: "HTTP", + Name: "New_VIP", + AdminStateUp: vips.Up, + SubnetID: subnetID, + PoolID: poolID, + ProtocolPort: 80, + }).Extract() + + th.AssertNoErr(t, err) + + t.Logf("Created pool %s", p.ID) + + return p.ID +} + +func listVIPs(t *testing.T) { + err := vips.List(base.Client, vips.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + vipList, err := vips.ExtractVIPs(page) + if err != nil { + t.Errorf("Failed to extract VIPs: %v", err) + return false, err + } + + for _, vip := range vipList { + t.Logf("Listing VIP: ID [%s] Name [%s] Address [%s] Port [%s] Connection Limit [%d]", + vip.ID, vip.Name, vip.Address, vip.ProtocolPort, vip.ConnLimit) + } + + return true, nil + }) + + th.AssertNoErr(t, err) +} + +func updateVIP(t *testing.T, VIPID string) { + i1000 := 1000 + _, err := vips.Update(base.Client, VIPID, vips.UpdateOpts{ConnLimit: &i1000}).Extract() + + th.AssertNoErr(t, err) + + t.Logf("Updated VIP ID [%s]", VIPID) +} + +func getVIP(t *testing.T, VIPID string) { + vip, err := vips.Get(base.Client, VIPID).Extract() + + th.AssertNoErr(t, err) + + t.Logf("Getting VIP ID [%s]: Status [%s]", vip.ID, vip.Status) +} + +func deleteVIP(t *testing.T, VIPID string) { + res := vips.Delete(base.Client, VIPID) + + th.AssertNoErr(t, res.Err) + + t.Logf("Deleted VIP %s", VIPID) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extensions/pkg.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extensions/pkg.go new file mode 100644 index 0000000000..aeec0fa756 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extensions/pkg.go @@ -0,0 +1 @@ +package extensions diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extensions/provider_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extensions/provider_test.go new file mode 100644 index 0000000000..f10c9d9bd1 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extensions/provider_test.go @@ -0,0 +1,68 @@ +// +build acceptance networking + +package extensions + +import ( + "strconv" + "testing" + + base "github.com/rackspace/gophercloud/acceptance/openstack/networking/v2" + "github.com/rackspace/gophercloud/openstack/networking/v2/networks" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestNetworkCRUDOperations(t *testing.T) { + base.Setup(t) + defer base.Teardown() + + // Create a network + n, err := networks.Create(base.Client, networks.CreateOpts{Name: "sample_network", AdminStateUp: networks.Up}).Extract() + th.AssertNoErr(t, err) + th.AssertEquals(t, n.Name, "sample_network") + th.AssertEquals(t, n.AdminStateUp, true) + networkID := n.ID + + // List networks + pager := networks.List(base.Client, networks.ListOpts{Limit: 2}) + err = pager.EachPage(func(page pagination.Page) (bool, error) { + t.Logf("--- Page ---") + + networkList, err := networks.ExtractNetworks(page) + th.AssertNoErr(t, err) + + for _, n := range networkList { + t.Logf("Network: ID [%s] Name [%s] Status [%s] Is shared? [%s]", + n.ID, n.Name, n.Status, strconv.FormatBool(n.Shared)) + } + + return true, nil + }) + th.CheckNoErr(t, err) + + // Get a network + if networkID == "" { + t.Fatalf("In order to retrieve a network, the NetworkID must be set") + } + n, err = networks.Get(base.Client, networkID).Extract() + th.AssertNoErr(t, err) + th.AssertEquals(t, n.Status, "ACTIVE") + th.AssertDeepEquals(t, n.Subnets, []string{}) + th.AssertEquals(t, n.Name, "sample_network") + th.AssertEquals(t, n.AdminStateUp, true) + th.AssertEquals(t, n.Shared, false) + th.AssertEquals(t, n.ID, networkID) + + // Update network + n, err = networks.Update(base.Client, networkID, networks.UpdateOpts{Name: "new_network_name"}).Extract() + th.AssertNoErr(t, err) + th.AssertEquals(t, n.Name, "new_network_name") + + // Delete network + res := networks.Delete(base.Client, networkID) + th.AssertNoErr(t, res.Err) +} + +func TestCreateMultipleNetworks(t *testing.T) { + //networks.CreateMany() +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extensions/security_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extensions/security_test.go new file mode 100644 index 0000000000..7d75292f0d --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extensions/security_test.go @@ -0,0 +1,171 @@ +// +build acceptance networking security + +package extensions + +import ( + "testing" + + base "github.com/rackspace/gophercloud/acceptance/openstack/networking/v2" + "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/groups" + "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/rules" + "github.com/rackspace/gophercloud/openstack/networking/v2/networks" + "github.com/rackspace/gophercloud/openstack/networking/v2/ports" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestSecurityGroups(t *testing.T) { + base.Setup(t) + defer base.Teardown() + + // create security group + groupID := createSecGroup(t) + + // delete security group + defer deleteSecGroup(t, groupID) + + // list security group + listSecGroups(t) + + // get security group + getSecGroup(t, groupID) + + // create port with security group + networkID, portID := createPort(t, groupID) + + // teardown + defer deleteNetwork(t, networkID) + + // delete port + defer deletePort(t, portID) +} + +func TestSecurityGroupRules(t *testing.T) { + base.Setup(t) + defer base.Teardown() + + // create security group + groupID := createSecGroup(t) + + defer deleteSecGroup(t, groupID) + + // create security group rule + ruleID := createSecRule(t, groupID) + + // delete security group rule + defer deleteSecRule(t, ruleID) + + // list security group rule + listSecRules(t) + + // get security group rule + getSecRule(t, ruleID) +} + +func createSecGroup(t *testing.T) string { + sg, err := groups.Create(base.Client, groups.CreateOpts{ + Name: "new-webservers", + Description: "security group for webservers", + }).Extract() + + th.AssertNoErr(t, err) + + t.Logf("Created security group %s", sg.ID) + + return sg.ID +} + +func listSecGroups(t *testing.T) { + err := groups.List(base.Client, groups.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + list, err := groups.ExtractGroups(page) + if err != nil { + t.Errorf("Failed to extract secgroups: %v", err) + return false, err + } + + for _, sg := range list { + t.Logf("Listing security group: ID [%s] Name [%s]", sg.ID, sg.Name) + } + + return true, nil + }) + + th.AssertNoErr(t, err) +} + +func getSecGroup(t *testing.T, id string) { + sg, err := groups.Get(base.Client, id).Extract() + th.AssertNoErr(t, err) + t.Logf("Getting security group: ID [%s] Name [%s] Description [%s]", sg.ID, sg.Name, sg.Description) +} + +func createPort(t *testing.T, groupID string) (string, string) { + n, err := networks.Create(base.Client, networks.CreateOpts{Name: "tmp_network"}).Extract() + th.AssertNoErr(t, err) + t.Logf("Created network %s", n.ID) + + opts := ports.CreateOpts{ + NetworkID: n.ID, + Name: "my_port", + SecurityGroups: []string{groupID}, + } + p, err := ports.Create(base.Client, opts).Extract() + th.AssertNoErr(t, err) + t.Logf("Created port %s with security group %s", p.ID, groupID) + + return n.ID, p.ID +} + +func deleteSecGroup(t *testing.T, groupID string) { + res := groups.Delete(base.Client, groupID) + th.AssertNoErr(t, res.Err) + t.Logf("Deleted security group %s", groupID) +} + +func createSecRule(t *testing.T, groupID string) string { + r, err := rules.Create(base.Client, rules.CreateOpts{ + Direction: "ingress", + PortRangeMin: 80, + EtherType: "IPv4", + PortRangeMax: 80, + Protocol: "tcp", + SecGroupID: groupID, + }).Extract() + + th.AssertNoErr(t, err) + + t.Logf("Created security group rule %s", r.ID) + + return r.ID +} + +func listSecRules(t *testing.T) { + err := rules.List(base.Client, rules.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + list, err := rules.ExtractRules(page) + if err != nil { + t.Errorf("Failed to extract sec rules: %v", err) + return false, err + } + + for _, r := range list { + t.Logf("Listing security rule: ID [%s]", r.ID) + } + + return true, nil + }) + + th.AssertNoErr(t, err) +} + +func getSecRule(t *testing.T, id string) { + r, err := rules.Get(base.Client, id).Extract() + th.AssertNoErr(t, err) + t.Logf("Getting security rule: ID [%s] Direction [%s] EtherType [%s] Protocol [%s]", + r.ID, r.Direction, r.EtherType, r.Protocol) +} + +func deleteSecRule(t *testing.T, id string) { + res := rules.Delete(base.Client, id) + th.AssertNoErr(t, res.Err) + t.Logf("Deleted security rule %s", id) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/network_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/network_test.go new file mode 100644 index 0000000000..be8a3a195a --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/network_test.go @@ -0,0 +1,68 @@ +// +build acceptance networking + +package v2 + +import ( + "strconv" + "testing" + + "github.com/rackspace/gophercloud/openstack/networking/v2/networks" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestNetworkCRUDOperations(t *testing.T) { + Setup(t) + defer Teardown() + + // Create a network + n, err := networks.Create(Client, networks.CreateOpts{Name: "sample_network", AdminStateUp: networks.Up}).Extract() + th.AssertNoErr(t, err) + defer networks.Delete(Client, n.ID) + th.AssertEquals(t, n.Name, "sample_network") + th.AssertEquals(t, n.AdminStateUp, true) + networkID := n.ID + + // List networks + pager := networks.List(Client, networks.ListOpts{Limit: 2}) + err = pager.EachPage(func(page pagination.Page) (bool, error) { + t.Logf("--- Page ---") + + networkList, err := networks.ExtractNetworks(page) + th.AssertNoErr(t, err) + + for _, n := range networkList { + t.Logf("Network: ID [%s] Name [%s] Status [%s] Is shared? [%s]", + n.ID, n.Name, n.Status, strconv.FormatBool(n.Shared)) + } + + return true, nil + }) + th.CheckNoErr(t, err) + + // Get a network + if networkID == "" { + t.Fatalf("In order to retrieve a network, the NetworkID must be set") + } + n, err = networks.Get(Client, networkID).Extract() + th.AssertNoErr(t, err) + th.AssertEquals(t, n.Status, "ACTIVE") + th.AssertDeepEquals(t, n.Subnets, []string{}) + th.AssertEquals(t, n.Name, "sample_network") + th.AssertEquals(t, n.AdminStateUp, true) + th.AssertEquals(t, n.Shared, false) + th.AssertEquals(t, n.ID, networkID) + + // Update network + n, err = networks.Update(Client, networkID, networks.UpdateOpts{Name: "new_network_name"}).Extract() + th.AssertNoErr(t, err) + th.AssertEquals(t, n.Name, "new_network_name") + + // Delete network + res := networks.Delete(Client, networkID) + th.AssertNoErr(t, res.Err) +} + +func TestCreateMultipleNetworks(t *testing.T) { + //networks.CreateMany() +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/pkg.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/pkg.go new file mode 100644 index 0000000000..5ec3cc8e83 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/pkg.go @@ -0,0 +1 @@ +package v2 diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/port_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/port_test.go new file mode 100644 index 0000000000..03e8e27842 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/port_test.go @@ -0,0 +1,117 @@ +// +build acceptance networking + +package v2 + +import ( + "testing" + + "github.com/rackspace/gophercloud/openstack/networking/v2/networks" + "github.com/rackspace/gophercloud/openstack/networking/v2/ports" + "github.com/rackspace/gophercloud/openstack/networking/v2/subnets" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestPortCRUD(t *testing.T) { + Setup(t) + defer Teardown() + + // Setup network + t.Log("Setting up network") + networkID, err := createNetwork() + th.AssertNoErr(t, err) + defer networks.Delete(Client, networkID) + + // Setup subnet + t.Logf("Setting up subnet on network %s", networkID) + subnetID, err := createSubnet(networkID) + th.AssertNoErr(t, err) + defer subnets.Delete(Client, subnetID) + + // Create port + t.Logf("Create port based on subnet %s", subnetID) + portID := createPort(t, networkID, subnetID) + + // List ports + t.Logf("Listing all ports") + listPorts(t) + + // Get port + if portID == "" { + t.Fatalf("In order to retrieve a port, the portID must be set") + } + p, err := ports.Get(Client, portID).Extract() + th.AssertNoErr(t, err) + th.AssertEquals(t, p.ID, portID) + + // Update port + p, err = ports.Update(Client, portID, ports.UpdateOpts{Name: "new_port_name"}).Extract() + th.AssertNoErr(t, err) + th.AssertEquals(t, p.Name, "new_port_name") + + // Delete port + res := ports.Delete(Client, portID) + th.AssertNoErr(t, res.Err) +} + +func createPort(t *testing.T, networkID, subnetID string) string { + enable := false + opts := ports.CreateOpts{ + NetworkID: networkID, + Name: "my_port", + AdminStateUp: &enable, + FixedIPs: []ports.IP{ports.IP{SubnetID: subnetID}}, + } + p, err := ports.Create(Client, opts).Extract() + th.AssertNoErr(t, err) + th.AssertEquals(t, p.NetworkID, networkID) + th.AssertEquals(t, p.Name, "my_port") + th.AssertEquals(t, p.AdminStateUp, false) + + return p.ID +} + +func listPorts(t *testing.T) { + count := 0 + pager := ports.List(Client, ports.ListOpts{}) + err := pager.EachPage(func(page pagination.Page) (bool, error) { + count++ + t.Logf("--- Page ---") + + portList, err := ports.ExtractPorts(page) + th.AssertNoErr(t, err) + + for _, p := range portList { + t.Logf("Port: ID [%s] Name [%s] Status [%s] MAC addr [%s] Fixed IPs [%#v] Security groups [%#v]", + p.ID, p.Name, p.Status, p.MACAddress, p.FixedIPs, p.SecurityGroups) + } + + return true, nil + }) + + th.CheckNoErr(t, err) + + if count == 0 { + t.Logf("No pages were iterated over when listing ports") + } +} + +func createNetwork() (string, error) { + res, err := networks.Create(Client, networks.CreateOpts{Name: "tmp_network", AdminStateUp: networks.Up}).Extract() + return res.ID, err +} + +func createSubnet(networkID string) (string, error) { + s, err := subnets.Create(Client, subnets.CreateOpts{ + NetworkID: networkID, + CIDR: "192.168.199.0/24", + IPVersion: subnets.IPv4, + Name: "my_subnet", + EnableDHCP: subnets.Down, + }).Extract() + return s.ID, err +} + +func TestPortBatchCreate(t *testing.T) { + // todo +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/subnet_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/subnet_test.go new file mode 100644 index 0000000000..097a303ede --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/subnet_test.go @@ -0,0 +1,86 @@ +// +build acceptance networking + +package v2 + +import ( + "testing" + + "github.com/rackspace/gophercloud/openstack/networking/v2/networks" + "github.com/rackspace/gophercloud/openstack/networking/v2/subnets" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestList(t *testing.T) { + Setup(t) + defer Teardown() + + pager := subnets.List(Client, subnets.ListOpts{Limit: 2}) + err := pager.EachPage(func(page pagination.Page) (bool, error) { + t.Logf("--- Page ---") + + subnetList, err := subnets.ExtractSubnets(page) + th.AssertNoErr(t, err) + + for _, s := range subnetList { + t.Logf("Subnet: ID [%s] Name [%s] IP Version [%d] CIDR [%s] GatewayIP [%s]", + s.ID, s.Name, s.IPVersion, s.CIDR, s.GatewayIP) + } + + return true, nil + }) + th.CheckNoErr(t, err) +} + +func TestCRUD(t *testing.T) { + Setup(t) + defer Teardown() + + // Setup network + t.Log("Setting up network") + n, err := networks.Create(Client, networks.CreateOpts{Name: "tmp_network", AdminStateUp: networks.Up}).Extract() + th.AssertNoErr(t, err) + networkID := n.ID + defer networks.Delete(Client, networkID) + + // Create subnet + t.Log("Create subnet") + enable := false + opts := subnets.CreateOpts{ + NetworkID: networkID, + CIDR: "192.168.199.0/24", + IPVersion: subnets.IPv4, + Name: "my_subnet", + EnableDHCP: &enable, + } + s, err := subnets.Create(Client, opts).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, s.NetworkID, networkID) + th.AssertEquals(t, s.CIDR, "192.168.199.0/24") + th.AssertEquals(t, s.IPVersion, 4) + th.AssertEquals(t, s.Name, "my_subnet") + th.AssertEquals(t, s.EnableDHCP, false) + subnetID := s.ID + + // Get subnet + t.Log("Getting subnet") + s, err = subnets.Get(Client, subnetID).Extract() + th.AssertNoErr(t, err) + th.AssertEquals(t, s.ID, subnetID) + + // Update subnet + t.Log("Update subnet") + s, err = subnets.Update(Client, subnetID, subnets.UpdateOpts{Name: "new_subnet_name"}).Extract() + th.AssertNoErr(t, err) + th.AssertEquals(t, s.Name, "new_subnet_name") + + // Delete subnet + t.Log("Delete subnet") + res := subnets.Delete(Client, subnetID) + th.AssertNoErr(t, res.Err) +} + +func TestBatchCreate(t *testing.T) { + // todo +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/objectstorage/v1/accounts_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/objectstorage/v1/accounts_test.go new file mode 100644 index 0000000000..f7c01a7c11 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/objectstorage/v1/accounts_test.go @@ -0,0 +1,44 @@ +// +build acceptance + +package v1 + +import ( + "strings" + "testing" + + "github.com/rackspace/gophercloud/openstack/objectstorage/v1/accounts" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestAccounts(t *testing.T) { + // Create a provider client for making the HTTP requests. + // See common.go in this directory for more information. + client := newClient(t) + + // Update an account's metadata. + updateres := accounts.Update(client, accounts.UpdateOpts{Metadata: metadata}) + th.AssertNoErr(t, updateres.Err) + + // Defer the deletion of the metadata set above. + defer func() { + tempMap := make(map[string]string) + for k := range metadata { + tempMap[k] = "" + } + updateres = accounts.Update(client, accounts.UpdateOpts{Metadata: tempMap}) + th.AssertNoErr(t, updateres.Err) + }() + + // Retrieve account metadata. + getres := accounts.Get(client, nil) + th.AssertNoErr(t, getres.Err) + // Extract the custom metadata from the 'Get' response. + am, err := getres.ExtractMetadata() + th.AssertNoErr(t, err) + for k := range metadata { + if am[k] != metadata[strings.Title(k)] { + t.Errorf("Expected custom metadata with key: %s", k) + return + } + } +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/objectstorage/v1/common.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/objectstorage/v1/common.go new file mode 100644 index 0000000000..1eac681b57 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/objectstorage/v1/common.go @@ -0,0 +1,28 @@ +// +build acceptance + +package v1 + +import ( + "os" + "testing" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/openstack" + th "github.com/rackspace/gophercloud/testhelper" +) + +var metadata = map[string]string{"gopher": "cloud"} + +func newClient(t *testing.T) *gophercloud.ServiceClient { + ao, err := openstack.AuthOptionsFromEnv() + th.AssertNoErr(t, err) + + client, err := openstack.AuthenticatedClient(ao) + th.AssertNoErr(t, err) + + c, err := openstack.NewObjectStorageV1(client, gophercloud.EndpointOpts{ + Region: os.Getenv("OS_REGION_NAME"), + }) + th.AssertNoErr(t, err) + return c +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/objectstorage/v1/containers_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/objectstorage/v1/containers_test.go new file mode 100644 index 0000000000..d6832f1914 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/objectstorage/v1/containers_test.go @@ -0,0 +1,89 @@ +// +build acceptance + +package v1 + +import ( + "strings" + "testing" + + "github.com/rackspace/gophercloud/acceptance/tools" + "github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" +) + +// numContainers is the number of containers to create for testing. +var numContainers = 2 + +func TestContainers(t *testing.T) { + // Create a new client to execute the HTTP requests. See common.go for newClient body. + client := newClient(t) + + // Create a slice of random container names. + cNames := make([]string, numContainers) + for i := 0; i < numContainers; i++ { + cNames[i] = tools.RandomString("gophercloud-test-container-", 8) + } + + // Create numContainers containers. + for i := 0; i < len(cNames); i++ { + res := containers.Create(client, cNames[i], nil) + th.AssertNoErr(t, res.Err) + } + // Delete the numContainers containers after function completion. + defer func() { + for i := 0; i < len(cNames); i++ { + res := containers.Delete(client, cNames[i]) + th.AssertNoErr(t, res.Err) + } + }() + + // List the numContainer names that were just created. To just list those, + // the 'prefix' parameter is used. + err := containers.List(client, &containers.ListOpts{Full: true, Prefix: "gophercloud-test-container-"}).EachPage(func(page pagination.Page) (bool, error) { + containerList, err := containers.ExtractInfo(page) + th.AssertNoErr(t, err) + + for _, n := range containerList { + t.Logf("Container: Name [%s] Count [%d] Bytes [%d]", + n.Name, n.Count, n.Bytes) + } + + return true, nil + }) + th.AssertNoErr(t, err) + + // List the info for the numContainer containers that were created. + err = containers.List(client, &containers.ListOpts{Full: false, Prefix: "gophercloud-test-container-"}).EachPage(func(page pagination.Page) (bool, error) { + containerList, err := containers.ExtractNames(page) + th.AssertNoErr(t, err) + for _, n := range containerList { + t.Logf("Container: Name [%s]", n) + } + + return true, nil + }) + th.AssertNoErr(t, err) + + // Update one of the numContainer container metadata. + updateres := containers.Update(client, cNames[0], &containers.UpdateOpts{Metadata: metadata}) + th.AssertNoErr(t, updateres.Err) + // After the tests are done, delete the metadata that was set. + defer func() { + tempMap := make(map[string]string) + for k := range metadata { + tempMap[k] = "" + } + res := containers.Update(client, cNames[0], &containers.UpdateOpts{Metadata: tempMap}) + th.AssertNoErr(t, res.Err) + }() + + // Retrieve a container's metadata. + cm, err := containers.Get(client, cNames[0]).ExtractMetadata() + th.AssertNoErr(t, err) + for k := range metadata { + if cm[k] != metadata[strings.Title(k)] { + t.Errorf("Expected custom metadata with key: %s", k) + } + } +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/objectstorage/v1/objects_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/objectstorage/v1/objects_test.go new file mode 100644 index 0000000000..a8de338c3d --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/objectstorage/v1/objects_test.go @@ -0,0 +1,119 @@ +// +build acceptance + +package v1 + +import ( + "bytes" + "strings" + "testing" + + "github.com/rackspace/gophercloud/acceptance/tools" + "github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers" + "github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" +) + +// numObjects is the number of objects to create for testing. +var numObjects = 2 + +func TestObjects(t *testing.T) { + // Create a provider client for executing the HTTP request. + // See common.go for more information. + client := newClient(t) + + // Make a slice of length numObjects to hold the random object names. + oNames := make([]string, numObjects) + for i := 0; i < len(oNames); i++ { + oNames[i] = tools.RandomString("test-object-", 8) + } + + // Create a container to hold the test objects. + cName := tools.RandomString("test-container-", 8) + header, err := containers.Create(client, cName, nil).ExtractHeader() + th.AssertNoErr(t, err) + t.Logf("Create object headers: %+v\n", header) + + // Defer deletion of the container until after testing. + defer func() { + res := containers.Delete(client, cName) + th.AssertNoErr(t, res.Err) + }() + + // Create a slice of buffers to hold the test object content. + oContents := make([]*bytes.Buffer, numObjects) + for i := 0; i < numObjects; i++ { + oContents[i] = bytes.NewBuffer([]byte(tools.RandomString("", 10))) + res := objects.Create(client, cName, oNames[i], oContents[i], nil) + th.AssertNoErr(t, res.Err) + } + // Delete the objects after testing. + defer func() { + for i := 0; i < numObjects; i++ { + res := objects.Delete(client, cName, oNames[i], nil) + th.AssertNoErr(t, res.Err) + } + }() + + ons := make([]string, 0, len(oNames)) + err = objects.List(client, cName, &objects.ListOpts{Full: false, Prefix: "test-object-"}).EachPage(func(page pagination.Page) (bool, error) { + names, err := objects.ExtractNames(page) + th.AssertNoErr(t, err) + ons = append(ons, names...) + + return true, nil + }) + th.AssertNoErr(t, err) + th.AssertEquals(t, len(ons), len(oNames)) + + ois := make([]objects.Object, 0, len(oNames)) + err = objects.List(client, cName, &objects.ListOpts{Full: true, Prefix: "test-object-"}).EachPage(func(page pagination.Page) (bool, error) { + info, err := objects.ExtractInfo(page) + th.AssertNoErr(t, err) + + ois = append(ois, info...) + + return true, nil + }) + th.AssertNoErr(t, err) + th.AssertEquals(t, len(ois), len(oNames)) + + // Copy the contents of one object to another. + copyres := objects.Copy(client, cName, oNames[0], &objects.CopyOpts{Destination: cName + "/" + oNames[1]}) + th.AssertNoErr(t, copyres.Err) + + // Download one of the objects that was created above. + o1Content, err := objects.Download(client, cName, oNames[0], nil).ExtractContent() + th.AssertNoErr(t, err) + + // Download the another object that was create above. + o2Content, err := objects.Download(client, cName, oNames[1], nil).ExtractContent() + th.AssertNoErr(t, err) + + // Compare the two object's contents to test that the copy worked. + th.AssertEquals(t, string(o2Content), string(o1Content)) + + // Update an object's metadata. + updateres := objects.Update(client, cName, oNames[0], &objects.UpdateOpts{Metadata: metadata}) + th.AssertNoErr(t, updateres.Err) + + // Delete the object's metadata after testing. + defer func() { + tempMap := make(map[string]string) + for k := range metadata { + tempMap[k] = "" + } + res := objects.Update(client, cName, oNames[0], &objects.UpdateOpts{Metadata: tempMap}) + th.AssertNoErr(t, res.Err) + }() + + // Retrieve an object's metadata. + om, err := objects.Get(client, cName, oNames[0], nil).ExtractMetadata() + th.AssertNoErr(t, err) + for k := range metadata { + if om[k] != metadata[strings.Title(k)] { + t.Errorf("Expected custom metadata with key: %s", k) + return + } + } +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/pkg.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/pkg.go new file mode 100644 index 0000000000..3a8ecdb100 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/pkg.go @@ -0,0 +1,4 @@ +// +build acceptance + +package openstack + diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/blockstorage/v1/common.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/blockstorage/v1/common.go new file mode 100644 index 0000000000..e9fdd99205 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/blockstorage/v1/common.go @@ -0,0 +1,38 @@ +// +build acceptance + +package v1 + +import ( + "os" + "testing" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/acceptance/tools" + "github.com/rackspace/gophercloud/rackspace" + th "github.com/rackspace/gophercloud/testhelper" +) + +func newClient() (*gophercloud.ServiceClient, error) { + opts, err := rackspace.AuthOptionsFromEnv() + if err != nil { + return nil, err + } + opts = tools.OnlyRS(opts) + region := os.Getenv("RS_REGION") + + provider, err := rackspace.AuthenticatedClient(opts) + if err != nil { + return nil, err + } + + return rackspace.NewBlockStorageV1(provider, gophercloud.EndpointOpts{ + Region: region, + }) +} + +func setup(t *testing.T) *gophercloud.ServiceClient { + client, err := newClient() + th.AssertNoErr(t, err) + + return client +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/blockstorage/v1/snapshot_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/blockstorage/v1/snapshot_test.go new file mode 100644 index 0000000000..25b2cfeeeb --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/blockstorage/v1/snapshot_test.go @@ -0,0 +1,82 @@ +// +build acceptance blockstorage snapshots + +package v1 + +import ( + "testing" + "time" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" + "github.com/rackspace/gophercloud/rackspace/blockstorage/v1/snapshots" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestSnapshots(t *testing.T) { + client := setup(t) + volID := testVolumeCreate(t, client) + + t.Log("Creating snapshots") + s := testSnapshotCreate(t, client, volID) + id := s.ID + + t.Log("Listing snapshots") + testSnapshotList(t, client) + + t.Logf("Getting snapshot %s", id) + testSnapshotGet(t, client, id) + + t.Logf("Updating snapshot %s", id) + testSnapshotUpdate(t, client, id) + + t.Logf("Deleting snapshot %s", id) + testSnapshotDelete(t, client, id) + s.WaitUntilDeleted(client, -1) + + t.Logf("Deleting volume %s", volID) + testVolumeDelete(t, client, volID) +} + +func testSnapshotCreate(t *testing.T, client *gophercloud.ServiceClient, volID string) *snapshots.Snapshot { + opts := snapshots.CreateOpts{VolumeID: volID, Name: "snapshot-001"} + s, err := snapshots.Create(client, opts).Extract() + th.AssertNoErr(t, err) + t.Logf("Created snapshot %s", s.ID) + + t.Logf("Waiting for new snapshot to become available...") + start := time.Now().Second() + s.WaitUntilComplete(client, -1) + t.Logf("Snapshot completed after %ds", time.Now().Second()-start) + + return s +} + +func testSnapshotList(t *testing.T, client *gophercloud.ServiceClient) { + snapshots.List(client).EachPage(func(page pagination.Page) (bool, error) { + sList, err := snapshots.ExtractSnapshots(page) + th.AssertNoErr(t, err) + + for _, s := range sList { + t.Logf("Snapshot: ID [%s] Name [%s] Volume ID [%s] Progress [%s] Created [%s]", + s.ID, s.Name, s.VolumeID, s.Progress, s.CreatedAt) + } + + return true, nil + }) +} + +func testSnapshotGet(t *testing.T, client *gophercloud.ServiceClient, id string) { + _, err := snapshots.Get(client, id).Extract() + th.AssertNoErr(t, err) +} + +func testSnapshotUpdate(t *testing.T, client *gophercloud.ServiceClient, id string) { + _, err := snapshots.Update(client, id, snapshots.UpdateOpts{Name: "new_name"}).Extract() + th.AssertNoErr(t, err) +} + +func testSnapshotDelete(t *testing.T, client *gophercloud.ServiceClient, id string) { + res := snapshots.Delete(client, id) + th.AssertNoErr(t, res.Err) + t.Logf("Deleted snapshot %s", id) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/blockstorage/v1/volume_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/blockstorage/v1/volume_test.go new file mode 100644 index 0000000000..f86f9adedd --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/blockstorage/v1/volume_test.go @@ -0,0 +1,71 @@ +// +build acceptance blockstorage volumes + +package v1 + +import ( + "testing" + + "github.com/rackspace/gophercloud" + os "github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes" + "github.com/rackspace/gophercloud/pagination" + "github.com/rackspace/gophercloud/rackspace/blockstorage/v1/volumes" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestVolumes(t *testing.T) { + client := setup(t) + + t.Logf("Listing volumes") + testVolumeList(t, client) + + t.Logf("Creating volume") + volumeID := testVolumeCreate(t, client) + + t.Logf("Getting volume %s", volumeID) + testVolumeGet(t, client, volumeID) + + t.Logf("Updating volume %s", volumeID) + testVolumeUpdate(t, client, volumeID) + + t.Logf("Deleting volume %s", volumeID) + testVolumeDelete(t, client, volumeID) +} + +func testVolumeList(t *testing.T, client *gophercloud.ServiceClient) { + volumes.List(client).EachPage(func(page pagination.Page) (bool, error) { + vList, err := volumes.ExtractVolumes(page) + th.AssertNoErr(t, err) + + for _, v := range vList { + t.Logf("Volume: ID [%s] Name [%s] Type [%s] Created [%s]", v.ID, v.Name, + v.VolumeType, v.CreatedAt) + } + + return true, nil + }) +} + +func testVolumeCreate(t *testing.T, client *gophercloud.ServiceClient) string { + vol, err := volumes.Create(client, os.CreateOpts{Size: 75}).Extract() + th.AssertNoErr(t, err) + t.Logf("Created volume: ID [%s] Size [%s]", vol.ID, vol.Size) + return vol.ID +} + +func testVolumeGet(t *testing.T, client *gophercloud.ServiceClient, id string) { + vol, err := volumes.Get(client, id).Extract() + th.AssertNoErr(t, err) + t.Logf("Created volume: ID [%s] Size [%s]", vol.ID, vol.Size) +} + +func testVolumeUpdate(t *testing.T, client *gophercloud.ServiceClient, id string) { + vol, err := volumes.Update(client, id, volumes.UpdateOpts{Name: "new_name"}).Extract() + th.AssertNoErr(t, err) + t.Logf("Created volume: ID [%s] Name [%s]", vol.ID, vol.Name) +} + +func testVolumeDelete(t *testing.T, client *gophercloud.ServiceClient, id string) { + res := volumes.Delete(client, id) + th.AssertNoErr(t, res.Err) + t.Logf("Deleted volume %s", id) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/blockstorage/v1/volume_type_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/blockstorage/v1/volume_type_test.go new file mode 100644 index 0000000000..716f2b9fd5 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/blockstorage/v1/volume_type_test.go @@ -0,0 +1,46 @@ +// +build acceptance blockstorage volumetypes + +package v1 + +import ( + "testing" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" + "github.com/rackspace/gophercloud/rackspace/blockstorage/v1/volumetypes" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestAll(t *testing.T) { + client := setup(t) + + t.Logf("Listing volume types") + id := testList(t, client) + + t.Logf("Getting volume type %s", id) + testGet(t, client, id) +} + +func testList(t *testing.T, client *gophercloud.ServiceClient) string { + var lastID string + + volumetypes.List(client).EachPage(func(page pagination.Page) (bool, error) { + typeList, err := volumetypes.ExtractVolumeTypes(page) + th.AssertNoErr(t, err) + + for _, vt := range typeList { + t.Logf("Volume type: ID [%s] Name [%s]", vt.ID, vt.Name) + lastID = vt.ID + } + + return true, nil + }) + + return lastID +} + +func testGet(t *testing.T, client *gophercloud.ServiceClient, id string) { + vt, err := volumetypes.Get(client, id).Extract() + th.AssertNoErr(t, err) + t.Logf("Volume: ID [%s] Name [%s]", vt.ID, vt.Name) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/client_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/client_test.go new file mode 100644 index 0000000000..61214c047a --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/client_test.go @@ -0,0 +1,28 @@ +// +build acceptance + +package rackspace + +import ( + "testing" + + "github.com/rackspace/gophercloud/acceptance/tools" + "github.com/rackspace/gophercloud/rackspace" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestAuthenticatedClient(t *testing.T) { + // Obtain credentials from the environment. + ao, err := rackspace.AuthOptionsFromEnv() + th.AssertNoErr(t, err) + + client, err := rackspace.AuthenticatedClient(tools.OnlyRS(ao)) + if err != nil { + t.Fatalf("Unable to authenticate: %v", err) + } + + if client.TokenID == "" { + t.Errorf("No token ID assigned to the client") + } + + t.Logf("Client successfully acquired a token: %v", client.TokenID) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/compute/v2/bootfromvolume_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/compute/v2/bootfromvolume_test.go new file mode 100644 index 0000000000..c8c8e21058 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/compute/v2/bootfromvolume_test.go @@ -0,0 +1,46 @@ +// +build acceptance + +package v2 + +import ( + "testing" + + "github.com/rackspace/gophercloud/acceptance/tools" + osBFV "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/bootfromvolume" + "github.com/rackspace/gophercloud/rackspace/compute/v2/bootfromvolume" + "github.com/rackspace/gophercloud/rackspace/compute/v2/servers" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestBootFromVolume(t *testing.T) { + client, err := newClient() + th.AssertNoErr(t, err) + + if testing.Short() { + t.Skip("Skipping test that requires server creation in short mode.") + } + + options, err := optionsFromEnv() + th.AssertNoErr(t, err) + + name := tools.RandomString("Gophercloud-", 8) + t.Logf("Creating server [%s].", name) + + bd := []osBFV.BlockDevice{ + osBFV.BlockDevice{ + UUID: options.imageID, + SourceType: osBFV.Image, + VolumeSize: 10, + }, + } + + server, err := bootfromvolume.Create(client, servers.CreateOpts{ + Name: name, + FlavorRef: "performance1-1", + BlockDevice: bd, + }).Extract() + th.AssertNoErr(t, err) + t.Logf("Created server: %+v\n", server) + //defer deleteServer(t, client, server) + t.Logf("Deleting server [%s]...", name) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/compute/v2/compute_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/compute/v2/compute_test.go new file mode 100644 index 0000000000..3ca6dc9b6c --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/compute/v2/compute_test.go @@ -0,0 +1,60 @@ +// +build acceptance + +package v2 + +import ( + "errors" + "os" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/acceptance/tools" + "github.com/rackspace/gophercloud/rackspace" +) + +func newClient() (*gophercloud.ServiceClient, error) { + // Obtain credentials from the environment. + options, err := rackspace.AuthOptionsFromEnv() + if err != nil { + return nil, err + } + options = tools.OnlyRS(options) + region := os.Getenv("RS_REGION") + + if options.Username == "" { + return nil, errors.New("Please provide a Rackspace username as RS_USERNAME.") + } + if options.APIKey == "" { + return nil, errors.New("Please provide a Rackspace API key as RS_API_KEY.") + } + if region == "" { + return nil, errors.New("Please provide a Rackspace region as RS_REGION.") + } + + client, err := rackspace.AuthenticatedClient(options) + if err != nil { + return nil, err + } + + return rackspace.NewComputeV2(client, gophercloud.EndpointOpts{ + Region: region, + }) +} + +type serverOpts struct { + imageID string + flavorID string +} + +func optionsFromEnv() (*serverOpts, error) { + options := &serverOpts{ + imageID: os.Getenv("RS_IMAGE_ID"), + flavorID: os.Getenv("RS_FLAVOR_ID"), + } + if options.imageID == "" { + return nil, errors.New("Please provide a valid Rackspace image ID as RS_IMAGE_ID") + } + if options.flavorID == "" { + return nil, errors.New("Please provide a valid Rackspace flavor ID as RS_FLAVOR_ID") + } + return options, nil +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/compute/v2/flavors_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/compute/v2/flavors_test.go new file mode 100644 index 0000000000..4618ecc8a9 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/compute/v2/flavors_test.go @@ -0,0 +1,61 @@ +// +build acceptance + +package v2 + +import ( + "testing" + + "github.com/rackspace/gophercloud/pagination" + "github.com/rackspace/gophercloud/rackspace/compute/v2/flavors" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestListFlavors(t *testing.T) { + client, err := newClient() + th.AssertNoErr(t, err) + + count := 0 + err = flavors.ListDetail(client, nil).EachPage(func(page pagination.Page) (bool, error) { + count++ + t.Logf("-- Page %0d --", count) + + fs, err := flavors.ExtractFlavors(page) + th.AssertNoErr(t, err) + + for i, flavor := range fs { + t.Logf("[%02d] id=[%s]", i, flavor.ID) + t.Logf(" name=[%s]", flavor.Name) + t.Logf(" disk=[%d]", flavor.Disk) + t.Logf(" RAM=[%d]", flavor.RAM) + t.Logf(" rxtx_factor=[%f]", flavor.RxTxFactor) + t.Logf(" swap=[%d]", flavor.Swap) + t.Logf(" VCPUs=[%d]", flavor.VCPUs) + } + + return true, nil + }) + th.AssertNoErr(t, err) + if count == 0 { + t.Errorf("No flavors listed!") + } +} + +func TestGetFlavor(t *testing.T) { + client, err := newClient() + th.AssertNoErr(t, err) + + options, err := optionsFromEnv() + th.AssertNoErr(t, err) + + flavor, err := flavors.Get(client, options.flavorID).Extract() + th.AssertNoErr(t, err) + + t.Logf("Requested flavor:") + t.Logf(" id=[%s]", flavor.ID) + t.Logf(" name=[%s]", flavor.Name) + t.Logf(" disk=[%d]", flavor.Disk) + t.Logf(" RAM=[%d]", flavor.RAM) + t.Logf(" rxtx_factor=[%f]", flavor.RxTxFactor) + t.Logf(" swap=[%d]", flavor.Swap) + t.Logf(" VCPUs=[%d]", flavor.VCPUs) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/compute/v2/images_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/compute/v2/images_test.go new file mode 100644 index 0000000000..5e36c2e454 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/compute/v2/images_test.go @@ -0,0 +1,63 @@ +// +build acceptance + +package v2 + +import ( + "testing" + + "github.com/rackspace/gophercloud/pagination" + "github.com/rackspace/gophercloud/rackspace/compute/v2/images" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestListImages(t *testing.T) { + client, err := newClient() + th.AssertNoErr(t, err) + + count := 0 + err = images.ListDetail(client, nil).EachPage(func(page pagination.Page) (bool, error) { + count++ + t.Logf("-- Page %02d --", count) + + is, err := images.ExtractImages(page) + th.AssertNoErr(t, err) + + for i, image := range is { + t.Logf("[%02d] id=[%s]", i, image.ID) + t.Logf(" name=[%s]", image.Name) + t.Logf(" created=[%s]", image.Created) + t.Logf(" updated=[%s]", image.Updated) + t.Logf(" min disk=[%d]", image.MinDisk) + t.Logf(" min RAM=[%d]", image.MinRAM) + t.Logf(" progress=[%d]", image.Progress) + t.Logf(" status=[%s]", image.Status) + } + + return true, nil + }) + th.AssertNoErr(t, err) + if count < 1 { + t.Errorf("Expected at least one page of images.") + } +} + +func TestGetImage(t *testing.T) { + client, err := newClient() + th.AssertNoErr(t, err) + + options, err := optionsFromEnv() + th.AssertNoErr(t, err) + + image, err := images.Get(client, options.imageID).Extract() + th.AssertNoErr(t, err) + + t.Logf("Requested image:") + t.Logf(" id=[%s]", image.ID) + t.Logf(" name=[%s]", image.Name) + t.Logf(" created=[%s]", image.Created) + t.Logf(" updated=[%s]", image.Updated) + t.Logf(" min disk=[%d]", image.MinDisk) + t.Logf(" min RAM=[%d]", image.MinRAM) + t.Logf(" progress=[%d]", image.Progress) + t.Logf(" status=[%s]", image.Status) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/compute/v2/keypairs_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/compute/v2/keypairs_test.go new file mode 100644 index 0000000000..9bd6eb4284 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/compute/v2/keypairs_test.go @@ -0,0 +1,87 @@ +// +build acceptance rackspace + +package v2 + +import ( + "testing" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/acceptance/tools" + os "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs" + "github.com/rackspace/gophercloud/pagination" + "github.com/rackspace/gophercloud/rackspace/compute/v2/keypairs" + th "github.com/rackspace/gophercloud/testhelper" +) + +func deleteKeyPair(t *testing.T, client *gophercloud.ServiceClient, name string) { + err := keypairs.Delete(client, name).ExtractErr() + th.AssertNoErr(t, err) + t.Logf("Successfully deleted key [%s].", name) +} + +func TestCreateKeyPair(t *testing.T) { + client, err := newClient() + th.AssertNoErr(t, err) + + name := tools.RandomString("createdkey-", 8) + k, err := keypairs.Create(client, os.CreateOpts{Name: name}).Extract() + th.AssertNoErr(t, err) + defer deleteKeyPair(t, client, name) + + t.Logf("Created a new keypair:") + t.Logf(" name=[%s]", k.Name) + t.Logf(" fingerprint=[%s]", k.Fingerprint) + t.Logf(" publickey=[%s]", tools.Elide(k.PublicKey)) + t.Logf(" privatekey=[%s]", tools.Elide(k.PrivateKey)) + t.Logf(" userid=[%s]", k.UserID) +} + +func TestImportKeyPair(t *testing.T) { + client, err := newClient() + th.AssertNoErr(t, err) + + name := tools.RandomString("importedkey-", 8) + pubkey := "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDlIQ3r+zd97kb9Hzmujd3V6pbO53eb3Go4q2E8iqVGWQfZTrFdL9KACJnqJIm9HmncfRkUTxE37hqeGCCv8uD+ZPmPiZG2E60OX1mGDjbbzAyReRwYWXgXHopggZTLak5k4mwZYaxwaufbVBDRn847e01lZnaXaszEToLM37NLw+uz29sl3TwYy2R0RGHPwPc160aWmdLjSyd1Nd4c9pvvOP/EoEuBjIC6NJJwg2Rvg9sjjx9jYj0QUgc8CqKLN25oMZ69kNJzlFylKRUoeeVr89txlR59yehJWk6Uw6lYFTdJmcmQOFVAJ12RMmS1hLWCM8UzAgtw+EDa0eqBxBDl smash@winter" + + k, err := keypairs.Create(client, os.CreateOpts{ + Name: name, + PublicKey: pubkey, + }).Extract() + th.AssertNoErr(t, err) + defer deleteKeyPair(t, client, name) + + th.CheckEquals(t, pubkey, k.PublicKey) + th.CheckEquals(t, "", k.PrivateKey) + + t.Logf("Imported an existing keypair:") + t.Logf(" name=[%s]", k.Name) + t.Logf(" fingerprint=[%s]", k.Fingerprint) + t.Logf(" publickey=[%s]", tools.Elide(k.PublicKey)) + t.Logf(" privatekey=[%s]", tools.Elide(k.PrivateKey)) + t.Logf(" userid=[%s]", k.UserID) +} + +func TestListKeyPairs(t *testing.T) { + client, err := newClient() + th.AssertNoErr(t, err) + + count := 0 + err = keypairs.List(client).EachPage(func(page pagination.Page) (bool, error) { + count++ + t.Logf("--- %02d ---", count) + + ks, err := keypairs.ExtractKeyPairs(page) + th.AssertNoErr(t, err) + + for i, keypair := range ks { + t.Logf("[%02d] name=[%s]", i, keypair.Name) + t.Logf(" fingerprint=[%s]", keypair.Fingerprint) + t.Logf(" publickey=[%s]", tools.Elide(keypair.PublicKey)) + t.Logf(" privatekey=[%s]", tools.Elide(keypair.PrivateKey)) + t.Logf(" userid=[%s]", keypair.UserID) + } + + return true, nil + }) + th.AssertNoErr(t, err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/compute/v2/networks_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/compute/v2/networks_test.go new file mode 100644 index 0000000000..e8fc4d37df --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/compute/v2/networks_test.go @@ -0,0 +1,53 @@ +// +build acceptance rackspace + +package v2 + +import ( + "testing" + + "github.com/rackspace/gophercloud/pagination" + "github.com/rackspace/gophercloud/rackspace/compute/v2/networks" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestNetworks(t *testing.T) { + client, err := newClient() + th.AssertNoErr(t, err) + + // Create a network + n, err := networks.Create(client, networks.CreateOpts{Label: "sample_network", CIDR: "172.20.0.0/24"}).Extract() + th.AssertNoErr(t, err) + t.Logf("Created network: %+v\n", n) + defer networks.Delete(client, n.ID) + th.AssertEquals(t, n.Label, "sample_network") + th.AssertEquals(t, n.CIDR, "172.20.0.0/24") + networkID := n.ID + + // List networks + pager := networks.List(client) + err = pager.EachPage(func(page pagination.Page) (bool, error) { + t.Logf("--- Page ---") + + networkList, err := networks.ExtractNetworks(page) + th.AssertNoErr(t, err) + + for _, n := range networkList { + t.Logf("Network: ID [%s] Label [%s] CIDR [%s]", + n.ID, n.Label, n.CIDR) + } + + return true, nil + }) + th.CheckNoErr(t, err) + + // Get a network + if networkID == "" { + t.Fatalf("In order to retrieve a network, the NetworkID must be set") + } + n, err = networks.Get(client, networkID).Extract() + t.Logf("Retrieved Network: %+v\n", n) + th.AssertNoErr(t, err) + th.AssertEquals(t, n.CIDR, "172.20.0.0/24") + th.AssertEquals(t, n.Label, "sample_network") + th.AssertEquals(t, n.ID, networkID) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/compute/v2/pkg.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/compute/v2/pkg.go new file mode 100644 index 0000000000..5ec3cc8e83 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/compute/v2/pkg.go @@ -0,0 +1 @@ +package v2 diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/compute/v2/servers_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/compute/v2/servers_test.go new file mode 100644 index 0000000000..81c8599f3d --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/compute/v2/servers_test.go @@ -0,0 +1,204 @@ +// +build acceptance + +package v2 + +import ( + "testing" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/acceptance/tools" + "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/diskconfig" + oskey "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs" + os "github.com/rackspace/gophercloud/openstack/compute/v2/servers" + "github.com/rackspace/gophercloud/pagination" + "github.com/rackspace/gophercloud/rackspace/compute/v2/keypairs" + "github.com/rackspace/gophercloud/rackspace/compute/v2/servers" + th "github.com/rackspace/gophercloud/testhelper" +) + +func createServerKeyPair(t *testing.T, client *gophercloud.ServiceClient) *oskey.KeyPair { + name := tools.RandomString("importedkey-", 8) + pubkey := "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDlIQ3r+zd97kb9Hzmujd3V6pbO53eb3Go4q2E8iqVGWQfZTrFdL9KACJnqJIm9HmncfRkUTxE37hqeGCCv8uD+ZPmPiZG2E60OX1mGDjbbzAyReRwYWXgXHopggZTLak5k4mwZYaxwaufbVBDRn847e01lZnaXaszEToLM37NLw+uz29sl3TwYy2R0RGHPwPc160aWmdLjSyd1Nd4c9pvvOP/EoEuBjIC6NJJwg2Rvg9sjjx9jYj0QUgc8CqKLN25oMZ69kNJzlFylKRUoeeVr89txlR59yehJWk6Uw6lYFTdJmcmQOFVAJ12RMmS1hLWCM8UzAgtw+EDa0eqBxBDl smash@winter" + + k, err := keypairs.Create(client, oskey.CreateOpts{ + Name: name, + PublicKey: pubkey, + }).Extract() + th.AssertNoErr(t, err) + + return k +} + +func createServer(t *testing.T, client *gophercloud.ServiceClient, keyName string) *os.Server { + if testing.Short() { + t.Skip("Skipping test that requires server creation in short mode.") + } + + options, err := optionsFromEnv() + th.AssertNoErr(t, err) + + name := tools.RandomString("Gophercloud-", 8) + + pwd := tools.MakeNewPassword("") + + opts := &servers.CreateOpts{ + Name: name, + ImageRef: options.imageID, + FlavorRef: options.flavorID, + DiskConfig: diskconfig.Manual, + AdminPass: pwd, + } + + if keyName != "" { + opts.KeyPair = keyName + } + + t.Logf("Creating server [%s].", name) + s, err := servers.Create(client, opts).Extract() + th.AssertNoErr(t, err) + t.Logf("Creating server.") + + err = servers.WaitForStatus(client, s.ID, "ACTIVE", 300) + th.AssertNoErr(t, err) + t.Logf("Server created successfully.") + + th.CheckEquals(t, pwd, s.AdminPass) + + return s +} + +func logServer(t *testing.T, server *os.Server, index int) { + if index == -1 { + t.Logf(" id=[%s]", server.ID) + } else { + t.Logf("[%02d] id=[%s]", index, server.ID) + } + t.Logf(" name=[%s]", server.Name) + t.Logf(" tenant ID=[%s]", server.TenantID) + t.Logf(" user ID=[%s]", server.UserID) + t.Logf(" updated=[%s]", server.Updated) + t.Logf(" created=[%s]", server.Created) + t.Logf(" host ID=[%s]", server.HostID) + t.Logf(" access IPv4=[%s]", server.AccessIPv4) + t.Logf(" access IPv6=[%s]", server.AccessIPv6) + t.Logf(" image=[%v]", server.Image) + t.Logf(" flavor=[%v]", server.Flavor) + t.Logf(" addresses=[%v]", server.Addresses) + t.Logf(" metadata=[%v]", server.Metadata) + t.Logf(" links=[%v]", server.Links) + t.Logf(" keyname=[%s]", server.KeyName) + t.Logf(" admin password=[%s]", server.AdminPass) + t.Logf(" status=[%s]", server.Status) + t.Logf(" progress=[%d]", server.Progress) +} + +func getServer(t *testing.T, client *gophercloud.ServiceClient, server *os.Server) { + t.Logf("> servers.Get") + + details, err := servers.Get(client, server.ID).Extract() + th.AssertNoErr(t, err) + logServer(t, details, -1) +} + +func listServers(t *testing.T, client *gophercloud.ServiceClient) { + t.Logf("> servers.List") + + count := 0 + err := servers.List(client, nil).EachPage(func(page pagination.Page) (bool, error) { + count++ + t.Logf("--- Page %02d ---", count) + + s, err := servers.ExtractServers(page) + th.AssertNoErr(t, err) + for index, server := range s { + logServer(t, &server, index) + } + + return true, nil + }) + th.AssertNoErr(t, err) +} + +func changeAdminPassword(t *testing.T, client *gophercloud.ServiceClient, server *os.Server) { + t.Logf("> servers.ChangeAdminPassword") + + original := server.AdminPass + + t.Logf("Changing server password.") + err := servers.ChangeAdminPassword(client, server.ID, tools.MakeNewPassword(original)).ExtractErr() + th.AssertNoErr(t, err) + + err = servers.WaitForStatus(client, server.ID, "ACTIVE", 300) + th.AssertNoErr(t, err) + t.Logf("Password changed successfully.") +} + +func rebootServer(t *testing.T, client *gophercloud.ServiceClient, server *os.Server) { + t.Logf("> servers.Reboot") + + err := servers.Reboot(client, server.ID, os.HardReboot).ExtractErr() + th.AssertNoErr(t, err) + + err = servers.WaitForStatus(client, server.ID, "ACTIVE", 300) + th.AssertNoErr(t, err) + + t.Logf("Server successfully rebooted.") +} + +func rebuildServer(t *testing.T, client *gophercloud.ServiceClient, server *os.Server) { + t.Logf("> servers.Rebuild") + + options, err := optionsFromEnv() + th.AssertNoErr(t, err) + + opts := servers.RebuildOpts{ + Name: tools.RandomString("RenamedGopher", 16), + AdminPass: tools.MakeNewPassword(server.AdminPass), + ImageID: options.imageID, + DiskConfig: diskconfig.Manual, + } + after, err := servers.Rebuild(client, server.ID, opts).Extract() + th.AssertNoErr(t, err) + th.CheckEquals(t, after.ID, server.ID) + + err = servers.WaitForStatus(client, after.ID, "ACTIVE", 300) + th.AssertNoErr(t, err) + + t.Logf("Server successfully rebuilt.") + logServer(t, after, -1) +} + +func deleteServer(t *testing.T, client *gophercloud.ServiceClient, server *os.Server) { + t.Logf("> servers.Delete") + + res := servers.Delete(client, server.ID) + th.AssertNoErr(t, res.Err) + + t.Logf("Server deleted successfully.") +} + +func deleteServerKeyPair(t *testing.T, client *gophercloud.ServiceClient, k *oskey.KeyPair) { + t.Logf("> keypairs.Delete") + + err := keypairs.Delete(client, k.Name).ExtractErr() + th.AssertNoErr(t, err) + + t.Logf("Keypair deleted successfully.") +} + +func TestServerOperations(t *testing.T) { + client, err := newClient() + th.AssertNoErr(t, err) + + kp := createServerKeyPair(t, client) + defer deleteServerKeyPair(t, client, kp) + + server := createServer(t, client, kp.Name) + defer deleteServer(t, client, server) + + getServer(t, client, server) + listServers(t, client) + changeAdminPassword(t, client, server) + rebootServer(t, client, server) + rebuildServer(t, client, server) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/compute/v2/virtualinterfaces_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/compute/v2/virtualinterfaces_test.go new file mode 100644 index 0000000000..39475e176e --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/compute/v2/virtualinterfaces_test.go @@ -0,0 +1,53 @@ +// +build acceptance rackspace + +package v2 + +import ( + "testing" + + "github.com/rackspace/gophercloud/pagination" + "github.com/rackspace/gophercloud/rackspace/compute/v2/networks" + "github.com/rackspace/gophercloud/rackspace/compute/v2/virtualinterfaces" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestVirtualInterfaces(t *testing.T) { + client, err := newClient() + th.AssertNoErr(t, err) + + // Create a server + server := createServer(t, client, "") + t.Logf("Created Server: %v\n", server) + defer deleteServer(t, client, server) + serverID := server.ID + + // Create a network + n, err := networks.Create(client, networks.CreateOpts{Label: "sample_network", CIDR: "172.20.0.0/24"}).Extract() + th.AssertNoErr(t, err) + t.Logf("Created Network: %v\n", n) + defer networks.Delete(client, n.ID) + networkID := n.ID + + // Create a virtual interface + vi, err := virtualinterfaces.Create(client, serverID, networkID).Extract() + th.AssertNoErr(t, err) + t.Logf("Created virtual interface: %+v\n", vi) + defer virtualinterfaces.Delete(client, serverID, vi.ID) + + // List virtual interfaces + pager := virtualinterfaces.List(client, serverID) + err = pager.EachPage(func(page pagination.Page) (bool, error) { + t.Logf("--- Page ---") + + virtualinterfacesList, err := virtualinterfaces.ExtractVirtualInterfaces(page) + th.AssertNoErr(t, err) + + for _, vi := range virtualinterfacesList { + t.Logf("Virtual Interface: ID [%s] MAC Address [%s] IP Addresses [%v]", + vi.ID, vi.MACAddress, vi.IPAddresses) + } + + return true, nil + }) + th.CheckNoErr(t, err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/identity/v2/extension_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/identity/v2/extension_test.go new file mode 100644 index 0000000000..a50e015522 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/identity/v2/extension_test.go @@ -0,0 +1,54 @@ +// +build acceptance + +package v2 + +import ( + "testing" + + "github.com/rackspace/gophercloud/pagination" + extensions2 "github.com/rackspace/gophercloud/rackspace/identity/v2/extensions" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestExtensions(t *testing.T) { + service := authenticatedClient(t) + + t.Logf("Extensions available on this identity endpoint:") + count := 0 + var chosen string + err := extensions2.List(service).EachPage(func(page pagination.Page) (bool, error) { + t.Logf("--- Page %02d ---", count) + + extensions, err := extensions2.ExtractExtensions(page) + th.AssertNoErr(t, err) + + for i, ext := range extensions { + if chosen == "" { + chosen = ext.Alias + } + + t.Logf("[%02d] name=[%s] namespace=[%s]", i, ext.Name, ext.Namespace) + t.Logf(" alias=[%s] updated=[%s]", ext.Alias, ext.Updated) + t.Logf(" description=[%s]", ext.Description) + } + + count++ + return true, nil + }) + th.AssertNoErr(t, err) + + if chosen == "" { + t.Logf("No extensions found.") + return + } + + ext, err := extensions2.Get(service, chosen).Extract() + th.AssertNoErr(t, err) + + t.Logf("Detail for extension [%s]:", chosen) + t.Logf(" name=[%s]", ext.Name) + t.Logf(" namespace=[%s]", ext.Namespace) + t.Logf(" alias=[%s]", ext.Alias) + t.Logf(" updated=[%s]", ext.Updated) + t.Logf(" description=[%s]", ext.Description) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/identity/v2/identity_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/identity/v2/identity_test.go new file mode 100644 index 0000000000..1182982f44 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/identity/v2/identity_test.go @@ -0,0 +1,50 @@ +// +build acceptance + +package v2 + +import ( + "testing" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/acceptance/tools" + "github.com/rackspace/gophercloud/rackspace" + th "github.com/rackspace/gophercloud/testhelper" +) + +func rackspaceAuthOptions(t *testing.T) gophercloud.AuthOptions { + // Obtain credentials from the environment. + options, err := rackspace.AuthOptionsFromEnv() + th.AssertNoErr(t, err) + options = tools.OnlyRS(options) + + if options.Username == "" { + t.Fatal("Please provide a Rackspace username as RS_USERNAME.") + } + if options.APIKey == "" { + t.Fatal("Please provide a Rackspace API key as RS_API_KEY.") + } + + return options +} + +func createClient(t *testing.T, auth bool) *gophercloud.ServiceClient { + ao := rackspaceAuthOptions(t) + + provider, err := rackspace.NewClient(ao.IdentityEndpoint) + th.AssertNoErr(t, err) + + if auth { + err = rackspace.Authenticate(provider, ao) + th.AssertNoErr(t, err) + } + + return rackspace.NewIdentityV2(provider) +} + +func unauthenticatedClient(t *testing.T) *gophercloud.ServiceClient { + return createClient(t, false) +} + +func authenticatedClient(t *testing.T) *gophercloud.ServiceClient { + return createClient(t, true) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/identity/v2/pkg.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/identity/v2/pkg.go new file mode 100644 index 0000000000..5ec3cc8e83 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/identity/v2/pkg.go @@ -0,0 +1 @@ +package v2 diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/identity/v2/role_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/identity/v2/role_test.go new file mode 100644 index 0000000000..efaeb75cde --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/identity/v2/role_test.go @@ -0,0 +1,59 @@ +// +build acceptance identity roles + +package v2 + +import ( + "testing" + + "github.com/rackspace/gophercloud" + os "github.com/rackspace/gophercloud/openstack/identity/v2/extensions/admin/roles" + + "github.com/rackspace/gophercloud/pagination" + "github.com/rackspace/gophercloud/rackspace/identity/v2/roles" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestRoles(t *testing.T) { + client := authenticatedClient(t) + + userID := createUser(t, client) + roleID := listRoles(t, client) + + addUserRole(t, client, userID, roleID) + + deleteUserRole(t, client, userID, roleID) + + deleteUser(t, client, userID) +} + +func listRoles(t *testing.T, client *gophercloud.ServiceClient) string { + var roleID string + + err := roles.List(client).EachPage(func(page pagination.Page) (bool, error) { + roleList, err := os.ExtractRoles(page) + th.AssertNoErr(t, err) + + for _, role := range roleList { + t.Logf("Listing role: ID [%s] Name [%s]", role.ID, role.Name) + roleID = role.ID + } + + return true, nil + }) + + th.AssertNoErr(t, err) + + return roleID +} + +func addUserRole(t *testing.T, client *gophercloud.ServiceClient, userID, roleID string) { + err := roles.AddUserRole(client, userID, roleID).ExtractErr() + th.AssertNoErr(t, err) + t.Logf("Added role %s to user %s", roleID, userID) +} + +func deleteUserRole(t *testing.T, client *gophercloud.ServiceClient, userID, roleID string) { + err := roles.DeleteUserRole(client, userID, roleID).ExtractErr() + th.AssertNoErr(t, err) + t.Logf("Removed role %s from user %s", roleID, userID) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/identity/v2/tenant_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/identity/v2/tenant_test.go new file mode 100644 index 0000000000..6081a498e3 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/identity/v2/tenant_test.go @@ -0,0 +1,37 @@ +// +build acceptance + +package v2 + +import ( + "testing" + + "github.com/rackspace/gophercloud/pagination" + rstenants "github.com/rackspace/gophercloud/rackspace/identity/v2/tenants" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestTenants(t *testing.T) { + service := authenticatedClient(t) + + t.Logf("Tenants available to the currently issued token:") + count := 0 + err := rstenants.List(service, nil).EachPage(func(page pagination.Page) (bool, error) { + t.Logf("--- Page %02d ---", count) + + tenants, err := rstenants.ExtractTenants(page) + th.AssertNoErr(t, err) + + for i, tenant := range tenants { + t.Logf("[%02d] id=[%s]", i, tenant.ID) + t.Logf(" name=[%s] enabled=[%v]", i, tenant.Name, tenant.Enabled) + t.Logf(" description=[%s]", tenant.Description) + } + + count++ + return true, nil + }) + th.AssertNoErr(t, err) + if count == 0 { + t.Errorf("No tenants listed for your current token.") + } +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/identity/v2/user_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/identity/v2/user_test.go new file mode 100644 index 0000000000..28c0c832be --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/identity/v2/user_test.go @@ -0,0 +1,93 @@ +// +build acceptance identity + +package v2 + +import ( + "strconv" + "testing" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/acceptance/tools" + os "github.com/rackspace/gophercloud/openstack/identity/v2/users" + "github.com/rackspace/gophercloud/pagination" + "github.com/rackspace/gophercloud/rackspace/identity/v2/users" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestUsers(t *testing.T) { + client := authenticatedClient(t) + + userID := createUser(t, client) + + listUsers(t, client) + + getUser(t, client, userID) + + updateUser(t, client, userID) + + resetApiKey(t, client, userID) + + deleteUser(t, client, userID) +} + +func createUser(t *testing.T, client *gophercloud.ServiceClient) string { + t.Log("Creating user") + + opts := users.CreateOpts{ + Username: tools.RandomString("user_", 5), + Enabled: os.Disabled, + Email: "new_user@foo.com", + } + + user, err := users.Create(client, opts).Extract() + th.AssertNoErr(t, err) + t.Logf("Created user %s", user.ID) + + return user.ID +} + +func listUsers(t *testing.T, client *gophercloud.ServiceClient) { + err := users.List(client).EachPage(func(page pagination.Page) (bool, error) { + userList, err := os.ExtractUsers(page) + th.AssertNoErr(t, err) + + for _, user := range userList { + t.Logf("Listing user: ID [%s] Username [%s] Email [%s] Enabled? [%s]", + user.ID, user.Username, user.Email, strconv.FormatBool(user.Enabled)) + } + + return true, nil + }) + + th.AssertNoErr(t, err) +} + +func getUser(t *testing.T, client *gophercloud.ServiceClient, userID string) { + _, err := users.Get(client, userID).Extract() + th.AssertNoErr(t, err) + t.Logf("Getting user %s", userID) +} + +func updateUser(t *testing.T, client *gophercloud.ServiceClient, userID string) { + opts := users.UpdateOpts{Username: tools.RandomString("new_name", 5), Email: "new@foo.com"} + user, err := users.Update(client, userID, opts).Extract() + th.AssertNoErr(t, err) + t.Logf("Updated user %s: Username [%s] Email [%s]", userID, user.Username, user.Email) +} + +func deleteUser(t *testing.T, client *gophercloud.ServiceClient, userID string) { + res := users.Delete(client, userID) + th.AssertNoErr(t, res.Err) + t.Logf("Deleted user %s", userID) +} + +func resetApiKey(t *testing.T, client *gophercloud.ServiceClient, userID string) { + key, err := users.ResetAPIKey(client, userID).Extract() + th.AssertNoErr(t, err) + + if key.APIKey == "" { + t.Fatal("failed to reset API key for user") + } + + t.Logf("Reset API key for user %s to %s", key.Username, key.APIKey) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/lb/v1/acl_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/lb/v1/acl_test.go new file mode 100644 index 0000000000..7a380273c6 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/lb/v1/acl_test.go @@ -0,0 +1,94 @@ +// +build acceptance lbs + +package v1 + +import ( + "testing" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" + "github.com/rackspace/gophercloud/rackspace/lb/v1/acl" + "github.com/rackspace/gophercloud/rackspace/lb/v1/lbs" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestACL(t *testing.T) { + client := setup(t) + + ids := createLB(t, client, 1) + lbID := ids[0] + + createACL(t, client, lbID) + + waitForLB(client, lbID, lbs.ACTIVE) + + networkIDs := showACL(t, client, lbID) + + deleteNetworkItem(t, client, lbID, networkIDs[0]) + waitForLB(client, lbID, lbs.ACTIVE) + + bulkDeleteACL(t, client, lbID, networkIDs[1:2]) + waitForLB(client, lbID, lbs.ACTIVE) + + deleteACL(t, client, lbID) + waitForLB(client, lbID, lbs.ACTIVE) + + deleteLB(t, client, lbID) +} + +func createACL(t *testing.T, client *gophercloud.ServiceClient, lbID int) { + opts := acl.CreateOpts{ + acl.CreateOpt{Address: "206.160.163.21", Type: acl.DENY}, + acl.CreateOpt{Address: "206.160.165.11", Type: acl.DENY}, + acl.CreateOpt{Address: "206.160.165.12", Type: acl.DENY}, + acl.CreateOpt{Address: "206.160.165.13", Type: acl.ALLOW}, + } + + err := acl.Create(client, lbID, opts).ExtractErr() + th.AssertNoErr(t, err) + + t.Logf("Created ACL items for LB %d", lbID) +} + +func showACL(t *testing.T, client *gophercloud.ServiceClient, lbID int) []int { + ids := []int{} + + err := acl.List(client, lbID).EachPage(func(page pagination.Page) (bool, error) { + accessList, err := acl.ExtractAccessList(page) + th.AssertNoErr(t, err) + + for _, i := range accessList { + t.Logf("Listing network item: ID [%s] Address [%s] Type [%s]", i.ID, i.Address, i.Type) + ids = append(ids, i.ID) + } + + return true, nil + }) + th.AssertNoErr(t, err) + + return ids +} + +func deleteNetworkItem(t *testing.T, client *gophercloud.ServiceClient, lbID, itemID int) { + err := acl.Delete(client, lbID, itemID).ExtractErr() + + th.AssertNoErr(t, err) + + t.Logf("Deleted network item %d", itemID) +} + +func bulkDeleteACL(t *testing.T, client *gophercloud.ServiceClient, lbID int, items []int) { + err := acl.BulkDelete(client, lbID, items).ExtractErr() + + th.AssertNoErr(t, err) + + t.Logf("Deleted network items %s", intsToStr(items)) +} + +func deleteACL(t *testing.T, client *gophercloud.ServiceClient, lbID int) { + err := acl.DeleteAll(client, lbID).ExtractErr() + + th.AssertNoErr(t, err) + + t.Logf("Deleted ACL from LB %d", lbID) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/lb/v1/common.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/lb/v1/common.go new file mode 100644 index 0000000000..4ce05e69ca --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/lb/v1/common.go @@ -0,0 +1,62 @@ +// +build acceptance lbs + +package v1 + +import ( + "os" + "strconv" + "strings" + "testing" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/acceptance/tools" + "github.com/rackspace/gophercloud/rackspace" + th "github.com/rackspace/gophercloud/testhelper" +) + +func newProvider() (*gophercloud.ProviderClient, error) { + opts, err := rackspace.AuthOptionsFromEnv() + if err != nil { + return nil, err + } + opts = tools.OnlyRS(opts) + + return rackspace.AuthenticatedClient(opts) +} + +func newClient() (*gophercloud.ServiceClient, error) { + provider, err := newProvider() + if err != nil { + return nil, err + } + + return rackspace.NewLBV1(provider, gophercloud.EndpointOpts{ + Region: os.Getenv("RS_REGION"), + }) +} + +func newComputeClient() (*gophercloud.ServiceClient, error) { + provider, err := newProvider() + if err != nil { + return nil, err + } + + return rackspace.NewComputeV2(provider, gophercloud.EndpointOpts{ + Region: os.Getenv("RS_REGION"), + }) +} + +func setup(t *testing.T) *gophercloud.ServiceClient { + client, err := newClient() + th.AssertNoErr(t, err) + + return client +} + +func intsToStr(ids []int) string { + strIDs := []string{} + for _, id := range ids { + strIDs = append(strIDs, strconv.Itoa(id)) + } + return strings.Join(strIDs, ", ") +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/lb/v1/lb_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/lb/v1/lb_test.go new file mode 100644 index 0000000000..c67ddecad9 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/lb/v1/lb_test.go @@ -0,0 +1,214 @@ +// +build acceptance lbs + +package v1 + +import ( + "strconv" + "testing" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/acceptance/tools" + "github.com/rackspace/gophercloud/pagination" + "github.com/rackspace/gophercloud/rackspace/lb/v1/lbs" + "github.com/rackspace/gophercloud/rackspace/lb/v1/vips" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestLBs(t *testing.T) { + client := setup(t) + + ids := createLB(t, client, 3) + id := ids[0] + + listLBProtocols(t, client) + + listLBAlgorithms(t, client) + + listLBs(t, client) + + getLB(t, client, id) + + checkLBLogging(t, client, id) + + checkErrorPage(t, client, id) + + getStats(t, client, id) + + updateLB(t, client, id) + + deleteLB(t, client, id) + + batchDeleteLBs(t, client, ids[1:]) +} + +func createLB(t *testing.T, client *gophercloud.ServiceClient, count int) []int { + ids := []int{} + + for i := 0; i < count; i++ { + opts := lbs.CreateOpts{ + Name: tools.RandomString("test_", 5), + Port: 80, + Protocol: "HTTP", + VIPs: []vips.VIP{ + vips.VIP{Type: vips.PUBLIC}, + }, + } + + lb, err := lbs.Create(client, opts).Extract() + th.AssertNoErr(t, err) + + t.Logf("Created LB %d - waiting for it to build...", lb.ID) + waitForLB(client, lb.ID, lbs.ACTIVE) + t.Logf("LB %d has reached ACTIVE state", lb.ID) + + ids = append(ids, lb.ID) + } + + return ids +} + +func waitForLB(client *gophercloud.ServiceClient, id int, state lbs.Status) { + gophercloud.WaitFor(60, func() (bool, error) { + lb, err := lbs.Get(client, id).Extract() + if err != nil { + return false, err + } + if lb.Status != state { + return false, nil + } + return true, nil + }) +} + +func listLBProtocols(t *testing.T, client *gophercloud.ServiceClient) { + err := lbs.ListProtocols(client).EachPage(func(page pagination.Page) (bool, error) { + pList, err := lbs.ExtractProtocols(page) + th.AssertNoErr(t, err) + + for _, p := range pList { + t.Logf("Listing protocol: Name [%s]", p.Name) + } + + return true, nil + }) + th.AssertNoErr(t, err) +} + +func listLBAlgorithms(t *testing.T, client *gophercloud.ServiceClient) { + err := lbs.ListAlgorithms(client).EachPage(func(page pagination.Page) (bool, error) { + aList, err := lbs.ExtractAlgorithms(page) + th.AssertNoErr(t, err) + + for _, a := range aList { + t.Logf("Listing algorithm: Name [%s]", a.Name) + } + + return true, nil + }) + th.AssertNoErr(t, err) +} + +func listLBs(t *testing.T, client *gophercloud.ServiceClient) { + err := lbs.List(client, lbs.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + lbList, err := lbs.ExtractLBs(page) + th.AssertNoErr(t, err) + + for _, lb := range lbList { + t.Logf("Listing LB: ID [%d] Name [%s] Protocol [%s] Status [%s] Node count [%d] Port [%d]", + lb.ID, lb.Name, lb.Protocol, lb.Status, lb.NodeCount, lb.Port) + } + + return true, nil + }) + + th.AssertNoErr(t, err) +} + +func getLB(t *testing.T, client *gophercloud.ServiceClient, id int) { + lb, err := lbs.Get(client, id).Extract() + th.AssertNoErr(t, err) + t.Logf("Getting LB %d: Created [%s] VIPs [%#v] Logging [%#v] Persistence [%#v] SourceAddrs [%#v]", + lb.ID, lb.Created, lb.VIPs, lb.ConnectionLogging, lb.SessionPersistence, lb.SourceAddrs) +} + +func updateLB(t *testing.T, client *gophercloud.ServiceClient, id int) { + opts := lbs.UpdateOpts{ + Name: tools.RandomString("new_", 5), + Protocol: "TCP", + HalfClosed: gophercloud.Enabled, + Algorithm: "RANDOM", + Port: 8080, + Timeout: 100, + HTTPSRedirect: gophercloud.Disabled, + } + + err := lbs.Update(client, id, opts).ExtractErr() + th.AssertNoErr(t, err) + + t.Logf("Updating LB %d - waiting for it to finish", id) + waitForLB(client, id, lbs.ACTIVE) + t.Logf("LB %d has reached ACTIVE state", id) +} + +func deleteLB(t *testing.T, client *gophercloud.ServiceClient, id int) { + err := lbs.Delete(client, id).ExtractErr() + th.AssertNoErr(t, err) + t.Logf("Deleted LB %d", id) +} + +func batchDeleteLBs(t *testing.T, client *gophercloud.ServiceClient, ids []int) { + err := lbs.BulkDelete(client, ids).ExtractErr() + th.AssertNoErr(t, err) + t.Logf("Deleted LB %s", intsToStr(ids)) +} + +func checkLBLogging(t *testing.T, client *gophercloud.ServiceClient, id int) { + err := lbs.EnableLogging(client, id).ExtractErr() + th.AssertNoErr(t, err) + t.Logf("Enabled logging for LB %d", id) + + res, err := lbs.IsLoggingEnabled(client, id) + th.AssertNoErr(t, err) + t.Logf("LB %d log enabled? %s", id, strconv.FormatBool(res)) + + waitForLB(client, id, lbs.ACTIVE) + + err = lbs.DisableLogging(client, id).ExtractErr() + th.AssertNoErr(t, err) + t.Logf("Disabled logging for LB %d", id) +} + +func checkErrorPage(t *testing.T, client *gophercloud.ServiceClient, id int) { + content, err := lbs.SetErrorPage(client, id, "New content!").Extract() + t.Logf("Set error page for LB %d", id) + + content, err = lbs.GetErrorPage(client, id).Extract() + th.AssertNoErr(t, err) + t.Logf("Error page for LB %d: %s", id, content) + + err = lbs.DeleteErrorPage(client, id).ExtractErr() + t.Logf("Deleted error page for LB %d", id) +} + +func getStats(t *testing.T, client *gophercloud.ServiceClient, id int) { + waitForLB(client, id, lbs.ACTIVE) + + stats, err := lbs.GetStats(client, id).Extract() + th.AssertNoErr(t, err) + + t.Logf("Stats for LB %d: %#v", id, stats) +} + +func checkCaching(t *testing.T, client *gophercloud.ServiceClient, id int) { + err := lbs.EnableCaching(client, id).ExtractErr() + th.AssertNoErr(t, err) + t.Logf("Enabled caching for LB %d", id) + + res, err := lbs.IsContentCached(client, id) + th.AssertNoErr(t, err) + t.Logf("Is caching enabled for LB? %s", strconv.FormatBool(res)) + + err = lbs.DisableCaching(client, id).ExtractErr() + th.AssertNoErr(t, err) + t.Logf("Disabled caching for LB %d", id) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/lb/v1/monitor_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/lb/v1/monitor_test.go new file mode 100644 index 0000000000..c1a8e24dd9 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/lb/v1/monitor_test.go @@ -0,0 +1,60 @@ +// +build acceptance lbs + +package v1 + +import ( + "testing" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/rackspace/lb/v1/lbs" + "github.com/rackspace/gophercloud/rackspace/lb/v1/monitors" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestMonitors(t *testing.T) { + client := setup(t) + + ids := createLB(t, client, 1) + lbID := ids[0] + + getMonitor(t, client, lbID) + + updateMonitor(t, client, lbID) + + deleteMonitor(t, client, lbID) + + deleteLB(t, client, lbID) +} + +func getMonitor(t *testing.T, client *gophercloud.ServiceClient, lbID int) { + hm, err := monitors.Get(client, lbID).Extract() + th.AssertNoErr(t, err) + t.Logf("Health monitor for LB %d: Type [%s] Delay [%d] Timeout [%d] AttemptLimit [%d]", + lbID, hm.Type, hm.Delay, hm.Timeout, hm.AttemptLimit) +} + +func updateMonitor(t *testing.T, client *gophercloud.ServiceClient, lbID int) { + opts := monitors.UpdateHTTPMonitorOpts{ + AttemptLimit: 3, + Delay: 10, + Timeout: 10, + BodyRegex: "hello is it me you're looking for", + Path: "/foo", + StatusRegex: "200", + Type: monitors.HTTP, + } + + err := monitors.Update(client, lbID, opts).ExtractErr() + th.AssertNoErr(t, err) + + waitForLB(client, lbID, lbs.ACTIVE) + t.Logf("Updated monitor for LB %d", lbID) +} + +func deleteMonitor(t *testing.T, client *gophercloud.ServiceClient, lbID int) { + err := monitors.Delete(client, lbID).ExtractErr() + th.AssertNoErr(t, err) + + waitForLB(client, lbID, lbs.ACTIVE) + t.Logf("Deleted monitor for LB %d", lbID) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/lb/v1/node_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/lb/v1/node_test.go new file mode 100644 index 0000000000..18b9fe71ce --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/lb/v1/node_test.go @@ -0,0 +1,175 @@ +// +build acceptance lbs + +package v1 + +import ( + "os" + "testing" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/acceptance/tools" + "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/diskconfig" + "github.com/rackspace/gophercloud/pagination" + "github.com/rackspace/gophercloud/rackspace/compute/v2/servers" + "github.com/rackspace/gophercloud/rackspace/lb/v1/lbs" + "github.com/rackspace/gophercloud/rackspace/lb/v1/nodes" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestNodes(t *testing.T) { + client := setup(t) + + serverIP := findServer(t) + ids := createLB(t, client, 1) + lbID := ids[0] + + nodeID := addNodes(t, client, lbID, serverIP) + + listNodes(t, client, lbID) + + getNode(t, client, lbID, nodeID) + + updateNode(t, client, lbID, nodeID) + + listEvents(t, client, lbID) + + deleteNode(t, client, lbID, nodeID) + + waitForLB(client, lbID, lbs.ACTIVE) + deleteLB(t, client, lbID) +} + +func findServer(t *testing.T) string { + var serverIP string + + client, err := newComputeClient() + th.AssertNoErr(t, err) + + err = servers.List(client, nil).EachPage(func(page pagination.Page) (bool, error) { + sList, err := servers.ExtractServers(page) + th.AssertNoErr(t, err) + + for _, s := range sList { + serverIP = s.AccessIPv4 + t.Logf("Found an existing server: ID [%s] Public IP [%s]", s.ID, serverIP) + break + } + + return true, nil + }) + th.AssertNoErr(t, err) + + if serverIP == "" { + t.Log("No server found, creating one") + + imageRef := os.Getenv("RS_IMAGE_ID") + if imageRef == "" { + t.Fatalf("OS var RS_IMAGE_ID undefined") + } + flavorRef := os.Getenv("RS_FLAVOR_ID") + if flavorRef == "" { + t.Fatalf("OS var RS_FLAVOR_ID undefined") + } + + opts := &servers.CreateOpts{ + Name: tools.RandomString("lb_test_", 5), + ImageRef: imageRef, + FlavorRef: flavorRef, + DiskConfig: diskconfig.Manual, + } + + s, err := servers.Create(client, opts).Extract() + th.AssertNoErr(t, err) + serverIP = s.AccessIPv4 + + t.Logf("Created server %s, waiting for it to build", s.ID) + err = servers.WaitForStatus(client, s.ID, "ACTIVE", 300) + th.AssertNoErr(t, err) + t.Logf("Server created successfully.") + } + + return serverIP +} + +func addNodes(t *testing.T, client *gophercloud.ServiceClient, lbID int, serverIP string) int { + opts := nodes.CreateOpts{ + nodes.CreateOpt{ + Address: serverIP, + Port: 80, + Condition: nodes.ENABLED, + Type: nodes.PRIMARY, + }, + } + + page := nodes.Create(client, lbID, opts) + + nodeList, err := page.ExtractNodes() + th.AssertNoErr(t, err) + + var nodeID int + for _, n := range nodeList { + nodeID = n.ID + } + if nodeID == 0 { + t.Fatalf("nodeID could not be extracted from create response") + } + + t.Logf("Added node %d to LB %d", nodeID, lbID) + waitForLB(client, lbID, lbs.ACTIVE) + + return nodeID +} + +func listNodes(t *testing.T, client *gophercloud.ServiceClient, lbID int) { + err := nodes.List(client, lbID, nil).EachPage(func(page pagination.Page) (bool, error) { + nodeList, err := nodes.ExtractNodes(page) + th.AssertNoErr(t, err) + + for _, n := range nodeList { + t.Logf("Listing node: ID [%d] Address [%s:%d] Status [%s]", n.ID, n.Address, n.Port, n.Status) + } + + return true, nil + }) + th.AssertNoErr(t, err) +} + +func getNode(t *testing.T, client *gophercloud.ServiceClient, lbID int, nodeID int) { + node, err := nodes.Get(client, lbID, nodeID).Extract() + th.AssertNoErr(t, err) + t.Logf("Getting node %d: Type [%s] Weight [%d]", nodeID, node.Type, node.Weight) +} + +func updateNode(t *testing.T, client *gophercloud.ServiceClient, lbID int, nodeID int) { + opts := nodes.UpdateOpts{ + Weight: gophercloud.IntToPointer(10), + Condition: nodes.DRAINING, + Type: nodes.SECONDARY, + } + err := nodes.Update(client, lbID, nodeID, opts).ExtractErr() + th.AssertNoErr(t, err) + t.Logf("Updated node %d", nodeID) + waitForLB(client, lbID, lbs.ACTIVE) +} + +func listEvents(t *testing.T, client *gophercloud.ServiceClient, lbID int) { + pager := nodes.ListEvents(client, lbID, nodes.ListEventsOpts{}) + err := pager.EachPage(func(page pagination.Page) (bool, error) { + eventList, err := nodes.ExtractNodeEvents(page) + th.AssertNoErr(t, err) + + for _, e := range eventList { + t.Logf("Listing events for node %d: Type [%s] Msg [%s] Severity [%s] Date [%s]", + e.NodeID, e.Type, e.DetailedMessage, e.Severity, e.Created) + } + + return true, nil + }) + th.AssertNoErr(t, err) +} + +func deleteNode(t *testing.T, client *gophercloud.ServiceClient, lbID int, nodeID int) { + err := nodes.Delete(client, lbID, nodeID).ExtractErr() + th.AssertNoErr(t, err) + t.Logf("Deleted node %d", nodeID) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/lb/v1/session_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/lb/v1/session_test.go new file mode 100644 index 0000000000..8d85655f6b --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/lb/v1/session_test.go @@ -0,0 +1,47 @@ +// +build acceptance lbs + +package v1 + +import ( + "testing" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/rackspace/lb/v1/sessions" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestSession(t *testing.T) { + client := setup(t) + + ids := createLB(t, client, 1) + lbID := ids[0] + + getSession(t, client, lbID) + + enableSession(t, client, lbID) + waitForLB(client, lbID, "ACTIVE") + + disableSession(t, client, lbID) + waitForLB(client, lbID, "ACTIVE") + + deleteLB(t, client, lbID) +} + +func getSession(t *testing.T, client *gophercloud.ServiceClient, lbID int) { + sp, err := sessions.Get(client, lbID).Extract() + th.AssertNoErr(t, err) + t.Logf("Session config: Type [%s]", sp.Type) +} + +func enableSession(t *testing.T, client *gophercloud.ServiceClient, lbID int) { + opts := sessions.CreateOpts{Type: sessions.HTTPCOOKIE} + err := sessions.Enable(client, lbID, opts).ExtractErr() + th.AssertNoErr(t, err) + t.Logf("Enable %s sessions for %d", opts.Type, lbID) +} + +func disableSession(t *testing.T, client *gophercloud.ServiceClient, lbID int) { + err := sessions.Disable(client, lbID).ExtractErr() + th.AssertNoErr(t, err) + t.Logf("Disable sessions for %d", lbID) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/lb/v1/throttle_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/lb/v1/throttle_test.go new file mode 100644 index 0000000000..1cc12356ca --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/lb/v1/throttle_test.go @@ -0,0 +1,53 @@ +// +build acceptance lbs + +package v1 + +import ( + "testing" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/rackspace/lb/v1/throttle" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestThrottle(t *testing.T) { + client := setup(t) + + ids := createLB(t, client, 1) + lbID := ids[0] + + getThrottleConfig(t, client, lbID) + + createThrottleConfig(t, client, lbID) + waitForLB(client, lbID, "ACTIVE") + + deleteThrottleConfig(t, client, lbID) + waitForLB(client, lbID, "ACTIVE") + + deleteLB(t, client, lbID) +} + +func getThrottleConfig(t *testing.T, client *gophercloud.ServiceClient, lbID int) { + sp, err := throttle.Get(client, lbID).Extract() + th.AssertNoErr(t, err) + t.Logf("Throttle config: MaxConns [%s]", sp.MaxConnections) +} + +func createThrottleConfig(t *testing.T, client *gophercloud.ServiceClient, lbID int) { + opts := throttle.CreateOpts{ + MaxConnections: 200, + MaxConnectionRate: 100, + MinConnections: 0, + RateInterval: 10, + } + + err := throttle.Create(client, lbID, opts).ExtractErr() + th.AssertNoErr(t, err) + t.Logf("Enable throttling for %d", lbID) +} + +func deleteThrottleConfig(t *testing.T, client *gophercloud.ServiceClient, lbID int) { + err := throttle.Delete(client, lbID).ExtractErr() + th.AssertNoErr(t, err) + t.Logf("Disable throttling for %d", lbID) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/lb/v1/vip_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/lb/v1/vip_test.go new file mode 100644 index 0000000000..bc0c2a89f2 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/lb/v1/vip_test.go @@ -0,0 +1,83 @@ +// +build acceptance lbs + +package v1 + +import ( + "testing" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" + "github.com/rackspace/gophercloud/rackspace/lb/v1/lbs" + "github.com/rackspace/gophercloud/rackspace/lb/v1/vips" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestVIPs(t *testing.T) { + client := setup(t) + + ids := createLB(t, client, 1) + lbID := ids[0] + + listVIPs(t, client, lbID) + + vipIDs := addVIPs(t, client, lbID, 3) + + deleteVIP(t, client, lbID, vipIDs[0]) + + bulkDeleteVIPs(t, client, lbID, vipIDs[1:]) + + waitForLB(client, lbID, lbs.ACTIVE) + deleteLB(t, client, lbID) +} + +func listVIPs(t *testing.T, client *gophercloud.ServiceClient, lbID int) { + err := vips.List(client, lbID).EachPage(func(page pagination.Page) (bool, error) { + vipList, err := vips.ExtractVIPs(page) + th.AssertNoErr(t, err) + + for _, vip := range vipList { + t.Logf("Listing VIP: ID [%s] Address [%s] Type [%s] Version [%s]", + vip.ID, vip.Address, vip.Type, vip.Version) + } + + return true, nil + }) + th.AssertNoErr(t, err) +} + +func addVIPs(t *testing.T, client *gophercloud.ServiceClient, lbID, count int) []int { + ids := []int{} + + for i := 0; i < count; i++ { + opts := vips.CreateOpts{ + Type: vips.PUBLIC, + Version: vips.IPV6, + } + + vip, err := vips.Create(client, lbID, opts).Extract() + th.AssertNoErr(t, err) + + t.Logf("Created VIP %d", vip.ID) + + waitForLB(client, lbID, lbs.ACTIVE) + + ids = append(ids, vip.ID) + } + + return ids +} + +func deleteVIP(t *testing.T, client *gophercloud.ServiceClient, lbID, vipID int) { + err := vips.Delete(client, lbID, vipID).ExtractErr() + th.AssertNoErr(t, err) + + t.Logf("Deleted VIP %d", vipID) + + waitForLB(client, lbID, lbs.ACTIVE) +} + +func bulkDeleteVIPs(t *testing.T, client *gophercloud.ServiceClient, lbID int, ids []int) { + err := vips.BulkDelete(client, lbID, ids).ExtractErr() + th.AssertNoErr(t, err) + t.Logf("Deleted VIPs %s", intsToStr(ids)) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/networking/v2/common.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/networking/v2/common.go new file mode 100644 index 0000000000..81704187fe --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/networking/v2/common.go @@ -0,0 +1,39 @@ +package v2 + +import ( + "os" + "testing" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/rackspace" + th "github.com/rackspace/gophercloud/testhelper" +) + +var Client *gophercloud.ServiceClient + +func NewClient() (*gophercloud.ServiceClient, error) { + opts, err := rackspace.AuthOptionsFromEnv() + if err != nil { + return nil, err + } + + provider, err := rackspace.AuthenticatedClient(opts) + if err != nil { + return nil, err + } + + return rackspace.NewNetworkV2(provider, gophercloud.EndpointOpts{ + Name: "cloudNetworks", + Region: os.Getenv("RS_REGION"), + }) +} + +func Setup(t *testing.T) { + client, err := NewClient() + th.AssertNoErr(t, err) + Client = client +} + +func Teardown() { + Client = nil +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/networking/v2/network_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/networking/v2/network_test.go new file mode 100644 index 0000000000..3862123bfd --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/networking/v2/network_test.go @@ -0,0 +1,65 @@ +// +build acceptance networking + +package v2 + +import ( + "strconv" + "testing" + + os "github.com/rackspace/gophercloud/openstack/networking/v2/networks" + "github.com/rackspace/gophercloud/pagination" + "github.com/rackspace/gophercloud/rackspace/networking/v2/networks" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestNetworkCRUDOperations(t *testing.T) { + Setup(t) + defer Teardown() + + // Create a network + n, err := networks.Create(Client, os.CreateOpts{Name: "sample_network", AdminStateUp: os.Up}).Extract() + th.AssertNoErr(t, err) + defer networks.Delete(Client, n.ID) + th.AssertEquals(t, "sample_network", n.Name) + th.AssertEquals(t, true, n.AdminStateUp) + networkID := n.ID + + // List networks + pager := networks.List(Client, os.ListOpts{Limit: 2}) + err = pager.EachPage(func(page pagination.Page) (bool, error) { + t.Logf("--- Page ---") + + networkList, err := os.ExtractNetworks(page) + th.AssertNoErr(t, err) + + for _, n := range networkList { + t.Logf("Network: ID [%s] Name [%s] Status [%s] Is shared? [%s]", + n.ID, n.Name, n.Status, strconv.FormatBool(n.Shared)) + } + + return true, nil + }) + th.CheckNoErr(t, err) + + // Get a network + if networkID == "" { + t.Fatalf("In order to retrieve a network, the NetworkID must be set") + } + n, err = networks.Get(Client, networkID).Extract() + th.AssertNoErr(t, err) + th.AssertEquals(t, "ACTIVE", n.Status) + th.AssertDeepEquals(t, []string{}, n.Subnets) + th.AssertEquals(t, "sample_network", n.Name) + th.AssertEquals(t, true, n.AdminStateUp) + th.AssertEquals(t, false, n.Shared) + th.AssertEquals(t, networkID, n.ID) + + // Update network + n, err = networks.Update(Client, networkID, os.UpdateOpts{Name: "new_network_name"}).Extract() + th.AssertNoErr(t, err) + th.AssertEquals(t, "new_network_name", n.Name) + + // Delete network + res := networks.Delete(Client, networkID) + th.AssertNoErr(t, res.Err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/networking/v2/port_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/networking/v2/port_test.go new file mode 100644 index 0000000000..3c42bb20cd --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/networking/v2/port_test.go @@ -0,0 +1,116 @@ +// +build acceptance networking + +package v2 + +import ( + "testing" + + osNetworks "github.com/rackspace/gophercloud/openstack/networking/v2/networks" + osPorts "github.com/rackspace/gophercloud/openstack/networking/v2/ports" + osSubnets "github.com/rackspace/gophercloud/openstack/networking/v2/subnets" + "github.com/rackspace/gophercloud/pagination" + "github.com/rackspace/gophercloud/rackspace/networking/v2/networks" + "github.com/rackspace/gophercloud/rackspace/networking/v2/ports" + "github.com/rackspace/gophercloud/rackspace/networking/v2/subnets" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestPortCRUD(t *testing.T) { + Setup(t) + defer Teardown() + + // Setup network + t.Log("Setting up network") + networkID, err := createNetwork() + th.AssertNoErr(t, err) + defer networks.Delete(Client, networkID) + + // Setup subnet + t.Logf("Setting up subnet on network %s", networkID) + subnetID, err := createSubnet(networkID) + th.AssertNoErr(t, err) + defer subnets.Delete(Client, subnetID) + + // Create port + t.Logf("Create port based on subnet %s", subnetID) + portID := createPort(t, networkID, subnetID) + + // List ports + t.Logf("Listing all ports") + listPorts(t) + + // Get port + if portID == "" { + t.Fatalf("In order to retrieve a port, the portID must be set") + } + p, err := ports.Get(Client, portID).Extract() + th.AssertNoErr(t, err) + th.AssertEquals(t, portID, p.ID) + + // Update port + p, err = ports.Update(Client, portID, osPorts.UpdateOpts{Name: "new_port_name"}).Extract() + th.AssertNoErr(t, err) + th.AssertEquals(t, "new_port_name", p.Name) + + // Delete port + res := ports.Delete(Client, portID) + th.AssertNoErr(t, res.Err) +} + +func createPort(t *testing.T, networkID, subnetID string) string { + enable := true + opts := osPorts.CreateOpts{ + NetworkID: networkID, + Name: "my_port", + AdminStateUp: &enable, + FixedIPs: []osPorts.IP{osPorts.IP{SubnetID: subnetID}}, + } + p, err := ports.Create(Client, opts).Extract() + th.AssertNoErr(t, err) + th.AssertEquals(t, networkID, p.NetworkID) + th.AssertEquals(t, "my_port", p.Name) + th.AssertEquals(t, true, p.AdminStateUp) + + return p.ID +} + +func listPorts(t *testing.T) { + count := 0 + pager := ports.List(Client, osPorts.ListOpts{}) + err := pager.EachPage(func(page pagination.Page) (bool, error) { + count++ + t.Logf("--- Page ---") + + portList, err := osPorts.ExtractPorts(page) + th.AssertNoErr(t, err) + + for _, p := range portList { + t.Logf("Port: ID [%s] Name [%s] Status [%s] MAC addr [%s] Fixed IPs [%#v] Security groups [%#v]", + p.ID, p.Name, p.Status, p.MACAddress, p.FixedIPs, p.SecurityGroups) + } + + return true, nil + }) + + th.CheckNoErr(t, err) + + if count == 0 { + t.Logf("No pages were iterated over when listing ports") + } +} + +func createNetwork() (string, error) { + res, err := networks.Create(Client, osNetworks.CreateOpts{Name: "tmp_network", AdminStateUp: osNetworks.Up}).Extract() + return res.ID, err +} + +func createSubnet(networkID string) (string, error) { + s, err := subnets.Create(Client, osSubnets.CreateOpts{ + NetworkID: networkID, + CIDR: "192.168.199.0/24", + IPVersion: osSubnets.IPv4, + Name: "my_subnet", + EnableDHCP: osSubnets.Down, + }).Extract() + return s.ID, err +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/networking/v2/subnet_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/networking/v2/subnet_test.go new file mode 100644 index 0000000000..c4014320a4 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/networking/v2/subnet_test.go @@ -0,0 +1,84 @@ +// +build acceptance networking + +package v2 + +import ( + "testing" + + osNetworks "github.com/rackspace/gophercloud/openstack/networking/v2/networks" + osSubnets "github.com/rackspace/gophercloud/openstack/networking/v2/subnets" + "github.com/rackspace/gophercloud/pagination" + "github.com/rackspace/gophercloud/rackspace/networking/v2/networks" + "github.com/rackspace/gophercloud/rackspace/networking/v2/subnets" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestListSubnets(t *testing.T) { + Setup(t) + defer Teardown() + + pager := subnets.List(Client, osSubnets.ListOpts{Limit: 2}) + err := pager.EachPage(func(page pagination.Page) (bool, error) { + t.Logf("--- Page ---") + + subnetList, err := osSubnets.ExtractSubnets(page) + th.AssertNoErr(t, err) + + for _, s := range subnetList { + t.Logf("Subnet: ID [%s] Name [%s] IP Version [%d] CIDR [%s] GatewayIP [%s]", + s.ID, s.Name, s.IPVersion, s.CIDR, s.GatewayIP) + } + + return true, nil + }) + th.CheckNoErr(t, err) +} + +func TestSubnetCRUD(t *testing.T) { + Setup(t) + defer Teardown() + + // Setup network + t.Log("Setting up network") + n, err := networks.Create(Client, osNetworks.CreateOpts{Name: "tmp_network", AdminStateUp: osNetworks.Up}).Extract() + th.AssertNoErr(t, err) + networkID := n.ID + defer networks.Delete(Client, networkID) + + // Create subnet + t.Log("Create subnet") + enable := false + opts := osSubnets.CreateOpts{ + NetworkID: networkID, + CIDR: "192.168.199.0/24", + IPVersion: osSubnets.IPv4, + Name: "my_subnet", + EnableDHCP: &enable, + } + s, err := subnets.Create(Client, opts).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, networkID, s.NetworkID) + th.AssertEquals(t, "192.168.199.0/24", s.CIDR) + th.AssertEquals(t, 4, s.IPVersion) + th.AssertEquals(t, "my_subnet", s.Name) + th.AssertEquals(t, false, s.EnableDHCP) + subnetID := s.ID + + // Get subnet + t.Log("Getting subnet") + s, err = subnets.Get(Client, subnetID).Extract() + th.AssertNoErr(t, err) + th.AssertEquals(t, subnetID, s.ID) + + // Update subnet + t.Log("Update subnet") + s, err = subnets.Update(Client, subnetID, osSubnets.UpdateOpts{Name: "new_subnet_name"}).Extract() + th.AssertNoErr(t, err) + th.AssertEquals(t, "new_subnet_name", s.Name) + + // Delete subnet + t.Log("Delete subnet") + res := subnets.Delete(Client, subnetID) + th.AssertNoErr(t, res.Err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/objectstorage/v1/accounts_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/objectstorage/v1/accounts_test.go new file mode 100644 index 0000000000..145e4e0482 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/objectstorage/v1/accounts_test.go @@ -0,0 +1,33 @@ +// +build acceptance rackspace + +package v1 + +import ( + "testing" + + raxAccounts "github.com/rackspace/gophercloud/rackspace/objectstorage/v1/accounts" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestAccounts(t *testing.T) { + c, err := createClient(t, false) + th.AssertNoErr(t, err) + + updateres := raxAccounts.Update(c, raxAccounts.UpdateOpts{Metadata: map[string]string{"white": "mountains"}}) + th.AssertNoErr(t, updateres.Err) + t.Logf("Headers from Update Account request: %+v\n", updateres.Header) + defer func() { + updateres = raxAccounts.Update(c, raxAccounts.UpdateOpts{Metadata: map[string]string{"white": ""}}) + th.AssertNoErr(t, updateres.Err) + metadata, err := raxAccounts.Get(c).ExtractMetadata() + th.AssertNoErr(t, err) + t.Logf("Metadata from Get Account request (after update reverted): %+v\n", metadata) + th.CheckEquals(t, metadata["White"], "") + }() + + metadata, err := raxAccounts.Get(c).ExtractMetadata() + th.AssertNoErr(t, err) + t.Logf("Metadata from Get Account request (after update): %+v\n", metadata) + + th.CheckEquals(t, metadata["White"], "mountains") +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/objectstorage/v1/bulk_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/objectstorage/v1/bulk_test.go new file mode 100644 index 0000000000..79013a564a --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/objectstorage/v1/bulk_test.go @@ -0,0 +1,23 @@ +// +build acceptance rackspace objectstorage v1 + +package v1 + +import ( + "testing" + + "github.com/rackspace/gophercloud/rackspace/objectstorage/v1/bulk" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestBulk(t *testing.T) { + c, err := createClient(t, false) + th.AssertNoErr(t, err) + + var options bulk.DeleteOpts + options = append(options, "container/object1") + res := bulk.Delete(c, options) + th.AssertNoErr(t, res.Err) + body, err := res.ExtractBody() + th.AssertNoErr(t, err) + t.Logf("Response body from Bulk Delete Request: %+v\n", body) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/objectstorage/v1/cdncontainers_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/objectstorage/v1/cdncontainers_test.go new file mode 100644 index 0000000000..e1bf38b16f --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/objectstorage/v1/cdncontainers_test.go @@ -0,0 +1,61 @@ +// +build acceptance rackspace objectstorage v1 + +package v1 + +import ( + "testing" + + osContainers "github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers" + "github.com/rackspace/gophercloud/pagination" + raxCDNContainers "github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdncontainers" + raxContainers "github.com/rackspace/gophercloud/rackspace/objectstorage/v1/containers" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestCDNContainers(t *testing.T) { + raxClient, err := createClient(t, false) + th.AssertNoErr(t, err) + + createres := raxContainers.Create(raxClient, "gophercloud-test", nil) + th.AssertNoErr(t, createres.Err) + t.Logf("Headers from Create Container request: %+v\n", createres.Header) + defer func() { + res := raxContainers.Delete(raxClient, "gophercloud-test") + th.AssertNoErr(t, res.Err) + }() + + raxCDNClient, err := createClient(t, true) + th.AssertNoErr(t, err) + + r := raxCDNContainers.Enable(raxCDNClient, "gophercloud-test", raxCDNContainers.EnableOpts{CDNEnabled: true, TTL: 900}) + th.AssertNoErr(t, r.Err) + t.Logf("Headers from Enable CDN Container request: %+v\n", r.Header) + + t.Logf("Container Names available to the currently issued token:") + count := 0 + err = raxCDNContainers.List(raxCDNClient, &osContainers.ListOpts{Full: false}).EachPage(func(page pagination.Page) (bool, error) { + t.Logf("--- Page %02d ---", count) + + names, err := raxCDNContainers.ExtractNames(page) + th.AssertNoErr(t, err) + + for i, name := range names { + t.Logf("[%02d] %s", i, name) + } + + count++ + return true, nil + }) + th.AssertNoErr(t, err) + if count == 0 { + t.Errorf("No CDN containers listed for your current token.") + } + + updateres := raxCDNContainers.Update(raxCDNClient, "gophercloud-test", raxCDNContainers.UpdateOpts{CDNEnabled: false}) + th.AssertNoErr(t, updateres.Err) + t.Logf("Headers from Update CDN Container request: %+v\n", updateres.Header) + + metadata, err := raxCDNContainers.Get(raxCDNClient, "gophercloud-test").ExtractMetadata() + th.AssertNoErr(t, err) + t.Logf("Headers from Get CDN Container request (after update): %+v\n", metadata) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/objectstorage/v1/cdnobjects_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/objectstorage/v1/cdnobjects_test.go new file mode 100644 index 0000000000..6e477ae704 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/objectstorage/v1/cdnobjects_test.go @@ -0,0 +1,46 @@ +// +build acceptance rackspace objectstorage v1 + +package v1 + +import ( + "bytes" + "testing" + + raxCDNContainers "github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdncontainers" + raxCDNObjects "github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdnobjects" + raxContainers "github.com/rackspace/gophercloud/rackspace/objectstorage/v1/containers" + raxObjects "github.com/rackspace/gophercloud/rackspace/objectstorage/v1/objects" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestCDNObjects(t *testing.T) { + raxClient, err := createClient(t, false) + th.AssertNoErr(t, err) + + createContResult := raxContainers.Create(raxClient, "gophercloud-test", nil) + th.AssertNoErr(t, createContResult.Err) + t.Logf("Headers from Create Container request: %+v\n", createContResult.Header) + defer func() { + deleteResult := raxContainers.Delete(raxClient, "gophercloud-test") + th.AssertNoErr(t, deleteResult.Err) + }() + + header, err := raxObjects.Create(raxClient, "gophercloud-test", "test-object", bytes.NewBufferString("gophercloud cdn test"), nil).ExtractHeader() + th.AssertNoErr(t, err) + t.Logf("Headers from Create Object request: %+v\n", header) + defer func() { + deleteResult := raxObjects.Delete(raxClient, "gophercloud-test", "test-object", nil) + th.AssertNoErr(t, deleteResult.Err) + }() + + raxCDNClient, err := createClient(t, true) + th.AssertNoErr(t, err) + + enableResult := raxCDNContainers.Enable(raxCDNClient, "gophercloud-test", raxCDNContainers.EnableOpts{CDNEnabled: true, TTL: 900}) + th.AssertNoErr(t, enableResult.Err) + t.Logf("Headers from Enable CDN Container request: %+v\n", enableResult.Header) + + deleteResult := raxCDNObjects.Delete(raxCDNClient, "gophercloud-test", "test-object", nil) + th.AssertNoErr(t, deleteResult.Err) + t.Logf("Headers from Delete CDN Object request: %+v\n", deleteResult.Err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/objectstorage/v1/common.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/objectstorage/v1/common.go new file mode 100644 index 0000000000..1ae07278cc --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/objectstorage/v1/common.go @@ -0,0 +1,54 @@ +// +build acceptance rackspace objectstorage v1 + +package v1 + +import ( + "os" + "testing" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/acceptance/tools" + "github.com/rackspace/gophercloud/rackspace" + th "github.com/rackspace/gophercloud/testhelper" +) + +func rackspaceAuthOptions(t *testing.T) gophercloud.AuthOptions { + // Obtain credentials from the environment. + options, err := rackspace.AuthOptionsFromEnv() + th.AssertNoErr(t, err) + options = tools.OnlyRS(options) + + if options.Username == "" { + t.Fatal("Please provide a Rackspace username as RS_USERNAME.") + } + if options.APIKey == "" { + t.Fatal("Please provide a Rackspace API key as RS_API_KEY.") + } + + return options +} + +func createClient(t *testing.T, cdn bool) (*gophercloud.ServiceClient, error) { + region := os.Getenv("RS_REGION") + if region == "" { + t.Fatal("Please provide a Rackspace region as RS_REGION") + } + + ao := rackspaceAuthOptions(t) + + provider, err := rackspace.NewClient(ao.IdentityEndpoint) + th.AssertNoErr(t, err) + + err = rackspace.Authenticate(provider, ao) + th.AssertNoErr(t, err) + + if cdn { + return rackspace.NewObjectCDNV1(provider, gophercloud.EndpointOpts{ + Region: region, + }) + } + + return rackspace.NewObjectStorageV1(provider, gophercloud.EndpointOpts{ + Region: region, + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/objectstorage/v1/containers_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/objectstorage/v1/containers_test.go new file mode 100644 index 0000000000..a7339cf388 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/objectstorage/v1/containers_test.go @@ -0,0 +1,85 @@ +// +build acceptance rackspace objectstorage v1 + +package v1 + +import ( + "testing" + + osContainers "github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers" + "github.com/rackspace/gophercloud/pagination" + raxContainers "github.com/rackspace/gophercloud/rackspace/objectstorage/v1/containers" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestContainers(t *testing.T) { + c, err := createClient(t, false) + th.AssertNoErr(t, err) + + t.Logf("Containers Info available to the currently issued token:") + count := 0 + err = raxContainers.List(c, &osContainers.ListOpts{Full: true}).EachPage(func(page pagination.Page) (bool, error) { + t.Logf("--- Page %02d ---", count) + + containers, err := raxContainers.ExtractInfo(page) + th.AssertNoErr(t, err) + + for i, container := range containers { + t.Logf("[%02d] name=[%s]", i, container.Name) + t.Logf(" count=[%d]", container.Count) + t.Logf(" bytes=[%d]", container.Bytes) + } + + count++ + return true, nil + }) + th.AssertNoErr(t, err) + if count == 0 { + t.Errorf("No containers listed for your current token.") + } + + t.Logf("Container Names available to the currently issued token:") + count = 0 + err = raxContainers.List(c, &osContainers.ListOpts{Full: false}).EachPage(func(page pagination.Page) (bool, error) { + t.Logf("--- Page %02d ---", count) + + names, err := raxContainers.ExtractNames(page) + th.AssertNoErr(t, err) + + for i, name := range names { + t.Logf("[%02d] %s", i, name) + } + + count++ + return true, nil + }) + th.AssertNoErr(t, err) + if count == 0 { + t.Errorf("No containers listed for your current token.") + } + + createres := raxContainers.Create(c, "gophercloud-test", nil) + th.AssertNoErr(t, createres.Err) + defer func() { + res := raxContainers.Delete(c, "gophercloud-test") + th.AssertNoErr(t, res.Err) + }() + + updateres := raxContainers.Update(c, "gophercloud-test", raxContainers.UpdateOpts{Metadata: map[string]string{"white": "mountains"}}) + th.AssertNoErr(t, updateres.Err) + t.Logf("Headers from Update Account request: %+v\n", updateres.Header) + defer func() { + res := raxContainers.Update(c, "gophercloud-test", raxContainers.UpdateOpts{Metadata: map[string]string{"white": ""}}) + th.AssertNoErr(t, res.Err) + metadata, err := raxContainers.Get(c, "gophercloud-test").ExtractMetadata() + th.AssertNoErr(t, err) + t.Logf("Metadata from Get Account request (after update reverted): %+v\n", metadata) + th.CheckEquals(t, metadata["White"], "") + }() + + getres := raxContainers.Get(c, "gophercloud-test") + t.Logf("Headers from Get Account request (after update): %+v\n", getres.Header) + metadata, err := getres.ExtractMetadata() + th.AssertNoErr(t, err) + t.Logf("Metadata from Get Account request (after update): %+v\n", metadata) + th.CheckEquals(t, metadata["White"], "mountains") +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/objectstorage/v1/objects_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/objectstorage/v1/objects_test.go new file mode 100644 index 0000000000..462f2847db --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/objectstorage/v1/objects_test.go @@ -0,0 +1,112 @@ +// +build acceptance rackspace objectstorage v1 + +package v1 + +import ( + "bytes" + "testing" + + osObjects "github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects" + "github.com/rackspace/gophercloud/pagination" + raxContainers "github.com/rackspace/gophercloud/rackspace/objectstorage/v1/containers" + raxObjects "github.com/rackspace/gophercloud/rackspace/objectstorage/v1/objects" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestObjects(t *testing.T) { + c, err := createClient(t, false) + th.AssertNoErr(t, err) + + res := raxContainers.Create(c, "gophercloud-test", nil) + th.AssertNoErr(t, res.Err) + + defer func() { + res := raxContainers.Delete(c, "gophercloud-test") + th.AssertNoErr(t, res.Err) + }() + + content := bytes.NewBufferString("Lewis Carroll") + options := &osObjects.CreateOpts{ContentType: "text/plain"} + createres := raxObjects.Create(c, "gophercloud-test", "o1", content, options) + th.AssertNoErr(t, createres.Err) + defer func() { + res := raxObjects.Delete(c, "gophercloud-test", "o1", nil) + th.AssertNoErr(t, res.Err) + }() + + t.Logf("Objects Info available to the currently issued token:") + count := 0 + err = raxObjects.List(c, "gophercloud-test", &osObjects.ListOpts{Full: true}).EachPage(func(page pagination.Page) (bool, error) { + t.Logf("--- Page %02d ---", count) + + objects, err := raxObjects.ExtractInfo(page) + th.AssertNoErr(t, err) + + for i, object := range objects { + t.Logf("[%02d] name=[%s]", i, object.Name) + t.Logf(" content-type=[%s]", object.ContentType) + t.Logf(" bytes=[%d]", object.Bytes) + t.Logf(" last-modified=[%s]", object.LastModified) + t.Logf(" hash=[%s]", object.Hash) + } + + count++ + return true, nil + }) + th.AssertNoErr(t, err) + if count == 0 { + t.Errorf("No objects listed for your current token.") + } + t.Logf("Container Names available to the currently issued token:") + count = 0 + err = raxObjects.List(c, "gophercloud-test", &osObjects.ListOpts{Full: false}).EachPage(func(page pagination.Page) (bool, error) { + t.Logf("--- Page %02d ---", count) + + names, err := raxObjects.ExtractNames(page) + th.AssertNoErr(t, err) + + for i, name := range names { + t.Logf("[%02d] %s", i, name) + } + + count++ + return true, nil + }) + th.AssertNoErr(t, err) + if count == 0 { + t.Errorf("No objects listed for your current token.") + } + + copyres := raxObjects.Copy(c, "gophercloud-test", "o1", &raxObjects.CopyOpts{Destination: "gophercloud-test/o2"}) + th.AssertNoErr(t, copyres.Err) + defer func() { + res := raxObjects.Delete(c, "gophercloud-test", "o2", nil) + th.AssertNoErr(t, res.Err) + }() + + o1Content, err := raxObjects.Download(c, "gophercloud-test", "o1", nil).ExtractContent() + th.AssertNoErr(t, err) + o2Content, err := raxObjects.Download(c, "gophercloud-test", "o2", nil).ExtractContent() + th.AssertNoErr(t, err) + th.AssertEquals(t, string(o2Content), string(o1Content)) + + updateres := raxObjects.Update(c, "gophercloud-test", "o2", osObjects.UpdateOpts{Metadata: map[string]string{"white": "mountains"}}) + th.AssertNoErr(t, updateres.Err) + t.Logf("Headers from Update Account request: %+v\n", updateres.Header) + defer func() { + res := raxObjects.Update(c, "gophercloud-test", "o2", osObjects.UpdateOpts{Metadata: map[string]string{"white": ""}}) + th.AssertNoErr(t, res.Err) + metadata, err := raxObjects.Get(c, "gophercloud-test", "o2", nil).ExtractMetadata() + th.AssertNoErr(t, err) + t.Logf("Metadata from Get Account request (after update reverted): %+v\n", metadata) + th.CheckEquals(t, metadata["White"], "") + }() + + getres := raxObjects.Get(c, "gophercloud-test", "o2", nil) + th.AssertNoErr(t, getres.Err) + t.Logf("Headers from Get Account request (after update): %+v\n", getres.Header) + metadata, err := getres.ExtractMetadata() + th.AssertNoErr(t, err) + t.Logf("Metadata from Get Account request (after update): %+v\n", metadata) + th.CheckEquals(t, metadata["White"], "mountains") +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/pkg.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/pkg.go new file mode 100644 index 0000000000..5d17b32caa --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/pkg.go @@ -0,0 +1 @@ +package rackspace diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/tools/pkg.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/tools/pkg.go new file mode 100644 index 0000000000..f7eca1298a --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/tools/pkg.go @@ -0,0 +1 @@ +package tools diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/tools/tools.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/tools/tools.go new file mode 100644 index 0000000000..35679b728c --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/tools/tools.go @@ -0,0 +1,89 @@ +// +build acceptance common + +package tools + +import ( + "crypto/rand" + "errors" + mrand "math/rand" + "os" + "time" + + "github.com/rackspace/gophercloud" +) + +// ErrTimeout is returned if WaitFor takes longer than 300 second to happen. +var ErrTimeout = errors.New("Timed out") + +// OnlyRS overrides the default Gophercloud behavior of using OS_-prefixed environment variables +// if RS_ variables aren't present. Otherwise, they'll stomp over each other here in the acceptance +// tests, where you need to have both defined. +func OnlyRS(original gophercloud.AuthOptions) gophercloud.AuthOptions { + if os.Getenv("RS_AUTH_URL") == "" { + original.IdentityEndpoint = "" + } + if os.Getenv("RS_USERNAME") == "" { + original.Username = "" + } + if os.Getenv("RS_PASSWORD") == "" { + original.Password = "" + } + if os.Getenv("RS_API_KEY") == "" { + original.APIKey = "" + } + return original +} + +// WaitFor polls a predicate function once per second to wait for a certain state to arrive. +func WaitFor(predicate func() (bool, error)) error { + for i := 0; i < 300; i++ { + time.Sleep(1 * time.Second) + + satisfied, err := predicate() + if err != nil { + return err + } + if satisfied { + return nil + } + } + return ErrTimeout +} + +// MakeNewPassword generates a new string that's guaranteed to be different than the given one. +func MakeNewPassword(oldPass string) string { + randomPassword := RandomString("", 16) + for randomPassword == oldPass { + randomPassword = RandomString("", 16) + } + return randomPassword +} + +// RandomString generates a string of given length, but random content. +// All content will be within the ASCII graphic character set. +// (Implementation from Even Shaw's contribution on +// http://stackoverflow.com/questions/12771930/what-is-the-fastest-way-to-generate-a-long-random-string-in-go). +func RandomString(prefix string, n int) string { + const alphanum = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + var bytes = make([]byte, n) + rand.Read(bytes) + for i, b := range bytes { + bytes[i] = alphanum[b%byte(len(alphanum))] + } + return prefix + string(bytes) +} + +// RandomInt will return a random integer between a specified range. +func RandomInt(min, max int) int { + mrand.Seed(time.Now().Unix()) + return mrand.Intn(max-min) + min +} + +// Elide returns the first bit of its input string with a suffix of "..." if it's longer than +// a comfortable 40 characters. +func Elide(value string) string { + if len(value) > 40 { + return value[0:37] + "..." + } + return value +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/auth_options.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/auth_options.go new file mode 100644 index 0000000000..19ce5d4a99 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/auth_options.go @@ -0,0 +1,48 @@ +package gophercloud + +/* +AuthOptions stores information needed to authenticate to an OpenStack cluster. +You can populate one manually, or use a provider's AuthOptionsFromEnv() function +to read relevant information from the standard environment variables. Pass one +to a provider's AuthenticatedClient function to authenticate and obtain a +ProviderClient representing an active session on that provider. + +Its fields are the union of those recognized by each identity implementation and +provider. +*/ +type AuthOptions struct { + // IdentityEndpoint specifies the HTTP endpoint that is required to work with + // the Identity API of the appropriate version. While it's ultimately needed by + // all of the identity services, it will often be populated by a provider-level + // function. + IdentityEndpoint string + + // Username is required if using Identity V2 API. Consult with your provider's + // control panel to discover your account's username. In Identity V3, either + // UserID or a combination of Username and DomainID or DomainName are needed. + Username, UserID string + + // Exactly one of Password or APIKey is required for the Identity V2 and V3 + // APIs. Consult with your provider's control panel to discover your account's + // preferred method of authentication. + Password, APIKey string + + // At most one of DomainID and DomainName must be provided if using Username + // with Identity V3. Otherwise, either are optional. + DomainID, DomainName string + + // The TenantID and TenantName fields are optional for the Identity V2 API. + // Some providers allow you to specify a TenantName instead of the TenantId. + // Some require both. Your provider's authentication policies will determine + // how these fields influence authentication. + TenantID, TenantName string + + // AllowReauth should be set to true if you grant permission for Gophercloud to + // cache your credentials in memory, and to allow Gophercloud to attempt to + // re-authenticate automatically if/when your token expires. If you set it to + // false, it will not cache these settings, but re-authentication will not be + // possible. This setting defaults to false. + // + // This setting is speculative and is currently not respected! + AllowReauth bool +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/auth_results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/auth_results.go new file mode 100644 index 0000000000..856a23382e --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/auth_results.go @@ -0,0 +1,14 @@ +package gophercloud + +import "time" + +// AuthResults [deprecated] is a leftover type from the v0.x days. It was +// intended to describe common functionality among identity service results, but +// is not actually used anywhere. +type AuthResults interface { + // TokenID returns the token's ID value from the authentication response. + TokenID() (string, error) + + // ExpiresAt retrieves the token's expiration time. + ExpiresAt() (time.Time, error) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/doc.go new file mode 100644 index 0000000000..fb81a9d8f1 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/doc.go @@ -0,0 +1,67 @@ +/* +Package gophercloud provides a multi-vendor interface to OpenStack-compatible +clouds. The library has a three-level hierarchy: providers, services, and +resources. + +Provider structs represent the service providers that offer and manage a +collection of services. Examples of providers include: OpenStack, Rackspace, +HP. These are defined like so: + + opts := gophercloud.AuthOptions{ + IdentityEndpoint: "https://my-openstack.com:5000/v2.0", + Username: "{username}", + Password: "{password}", + TenantID: "{tenant_id}", + } + + provider, err := openstack.AuthenticatedClient(opts) + +Service structs are specific to a provider and handle all of the logic and +operations for a particular OpenStack service. Examples of services include: +Compute, Object Storage, Block Storage. In order to define one, you need to +pass in the parent provider, like so: + + opts := gophercloud.EndpointOpts{Region: "RegionOne"} + + client := openstack.NewComputeV2(provider, opts) + +Resource structs are the domain models that services make use of in order +to work with and represent the state of API resources: + + server, err := servers.Get(client, "{serverId}").Extract() + +Intermediate Result structs are returned for API operations, which allow +generic access to the HTTP headers, response body, and any errors associated +with the network transaction. To turn a result into a usable resource struct, +you must call the Extract method which is chained to the response, or an +Extract function from an applicable extension: + + result := servers.Get(client, "{serverId}") + + // Attempt to extract the disk configuration from the OS-DCF disk config + // extension: + config, err := diskconfig.ExtractGet(result) + +All requests that enumerate a collection return a Pager struct that is used to +iterate through the results one page at a time. Use the EachPage method on that +Pager to handle each successive Page in a closure, then use the appropriate +extraction method from that request's package to interpret that Page as a slice +of results: + + err := servers.List(client, nil).EachPage(func (page pagination.Page) (bool, error) { + s, err := servers.ExtractServers(page) + if err != nil { + return false, err + } + + // Handle the []servers.Server slice. + + // Return "false" or an error to prematurely stop fetching new pages. + return true, nil + }) + +This top-level package contains utility functions and data types that are used +throughout the provider and service packages. Of particular note for end users +are the AuthOptions and EndpointOpts structs. +*/ +package gophercloud diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/endpoint_search.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/endpoint_search.go new file mode 100644 index 0000000000..5189431212 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/endpoint_search.go @@ -0,0 +1,92 @@ +package gophercloud + +import "errors" + +var ( + // ErrServiceNotFound is returned when no service in a service catalog matches + // the provided EndpointOpts. This is generally returned by provider service + // factory methods like "NewComputeV2()" and can mean that a service is not + // enabled for your account. + ErrServiceNotFound = errors.New("No suitable service could be found in the service catalog.") + + // ErrEndpointNotFound is returned when no available endpoints match the + // provided EndpointOpts. This is also generally returned by provider service + // factory methods, and usually indicates that a region was specified + // incorrectly. + ErrEndpointNotFound = errors.New("No suitable endpoint could be found in the service catalog.") +) + +// Availability indicates to whom a specific service endpoint is accessible: +// the internet at large, internal networks only, or only to administrators. +// Different identity services use different terminology for these. Identity v2 +// lists them as different kinds of URLs within the service catalog ("adminURL", +// "internalURL", and "publicURL"), while v3 lists them as "Interfaces" in an +// endpoint's response. +type Availability string + +const ( + // AvailabilityAdmin indicates that an endpoint is only available to + // administrators. + AvailabilityAdmin Availability = "admin" + + // AvailabilityPublic indicates that an endpoint is available to everyone on + // the internet. + AvailabilityPublic Availability = "public" + + // AvailabilityInternal indicates that an endpoint is only available within + // the cluster's internal network. + AvailabilityInternal Availability = "internal" +) + +// EndpointOpts specifies search criteria used by queries against an +// OpenStack service catalog. The options must contain enough information to +// unambiguously identify one, and only one, endpoint within the catalog. +// +// Usually, these are passed to service client factory functions in a provider +// package, like "rackspace.NewComputeV2()". +type EndpointOpts struct { + // Type [required] is the service type for the client (e.g., "compute", + // "object-store"). Generally, this will be supplied by the service client + // function, but a user-given value will be honored if provided. + Type string + + // Name [optional] is the service name for the client (e.g., "nova") as it + // appears in the service catalog. Services can have the same Type but a + // different Name, which is why both Type and Name are sometimes needed. + Name string + + // Region [required] is the geographic region in which the endpoint resides, + // generally specifying which datacenter should house your resources. + // Required only for services that span multiple regions. + Region string + + // Availability [optional] is the visibility of the endpoint to be returned. + // Valid types include the constants AvailabilityPublic, AvailabilityInternal, + // or AvailabilityAdmin from this package. + // + // Availability is not required, and defaults to AvailabilityPublic. Not all + // providers or services offer all Availability options. + Availability Availability +} + +/* +EndpointLocator is an internal function to be used by provider implementations. + +It provides an implementation that locates a single endpoint from a service +catalog for a specific ProviderClient based on user-provided EndpointOpts. The +provider then uses it to discover related ServiceClients. +*/ +type EndpointLocator func(EndpointOpts) (string, error) + +// ApplyDefaults is an internal method to be used by provider implementations. +// +// It sets EndpointOpts fields if not already set, including a default type. +// Currently, EndpointOpts.Availability defaults to the public endpoint. +func (eo *EndpointOpts) ApplyDefaults(t string) { + if eo.Type == "" { + eo.Type = t + } + if eo.Availability == "" { + eo.Availability = AvailabilityPublic + } +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/endpoint_search_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/endpoint_search_test.go new file mode 100644 index 0000000000..3457453427 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/endpoint_search_test.go @@ -0,0 +1,19 @@ +package gophercloud + +import ( + "testing" + + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestApplyDefaultsToEndpointOpts(t *testing.T) { + eo := EndpointOpts{Availability: AvailabilityPublic} + eo.ApplyDefaults("compute") + expected := EndpointOpts{Availability: AvailabilityPublic, Type: "compute"} + th.CheckDeepEquals(t, expected, eo) + + eo = EndpointOpts{Type: "compute"} + eo.ApplyDefaults("object-store") + expected = EndpointOpts{Availability: AvailabilityPublic, Type: "compute"} + th.CheckDeepEquals(t, expected, eo) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/auth_env.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/auth_env.go new file mode 100644 index 0000000000..a4402b6f06 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/auth_env.go @@ -0,0 +1,58 @@ +package openstack + +import ( + "fmt" + "os" + + "github.com/rackspace/gophercloud" +) + +var nilOptions = gophercloud.AuthOptions{} + +// ErrNoAuthUrl, ErrNoUsername, and ErrNoPassword errors indicate of the required OS_AUTH_URL, OS_USERNAME, or OS_PASSWORD +// environment variables, respectively, remain undefined. See the AuthOptions() function for more details. +var ( + ErrNoAuthURL = fmt.Errorf("Environment variable OS_AUTH_URL needs to be set.") + ErrNoUsername = fmt.Errorf("Environment variable OS_USERNAME needs to be set.") + ErrNoPassword = fmt.Errorf("Environment variable OS_PASSWORD needs to be set.") +) + +// AuthOptions fills out an identity.AuthOptions structure with the settings found on the various OpenStack +// OS_* environment variables. The following variables provide sources of truth: OS_AUTH_URL, OS_USERNAME, +// OS_PASSWORD, OS_TENANT_ID, and OS_TENANT_NAME. Of these, OS_USERNAME, OS_PASSWORD, and OS_AUTH_URL must +// have settings, or an error will result. OS_TENANT_ID and OS_TENANT_NAME are optional. +func AuthOptionsFromEnv() (gophercloud.AuthOptions, error) { + authURL := os.Getenv("OS_AUTH_URL") + username := os.Getenv("OS_USERNAME") + userID := os.Getenv("OS_USERID") + password := os.Getenv("OS_PASSWORD") + tenantID := os.Getenv("OS_TENANT_ID") + tenantName := os.Getenv("OS_TENANT_NAME") + domainID := os.Getenv("OS_DOMAIN_ID") + domainName := os.Getenv("OS_DOMAIN_NAME") + + if authURL == "" { + return nilOptions, ErrNoAuthURL + } + + if username == "" && userID == "" { + return nilOptions, ErrNoUsername + } + + if password == "" { + return nilOptions, ErrNoPassword + } + + ao := gophercloud.AuthOptions{ + IdentityEndpoint: authURL, + UserID: userID, + Username: username, + Password: password, + TenantID: tenantID, + TenantName: tenantName, + DomainID: domainID, + DomainName: domainName, + } + + return ao, nil +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/apiversions/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/apiversions/doc.go new file mode 100644 index 0000000000..e3af39f513 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/apiversions/doc.go @@ -0,0 +1,3 @@ +// Package apiversions provides information and interaction with the different +// API versions for the OpenStack Block Storage service, code-named Cinder. +package apiversions diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/apiversions/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/apiversions/requests.go new file mode 100644 index 0000000000..016bf374e3 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/apiversions/requests.go @@ -0,0 +1,28 @@ +package apiversions + +import ( + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" + + "github.com/racker/perigee" +) + +// List lists all the Cinder API versions available to end-users. +func List(c *gophercloud.ServiceClient) pagination.Pager { + return pagination.NewPager(c, listURL(c), func(r pagination.PageResult) pagination.Page { + return APIVersionPage{pagination.SinglePageBase(r)} + }) +} + +// Get will retrieve the volume type with the provided ID. To extract the volume +// type from the result, call the Extract method on the GetResult. +func Get(client *gophercloud.ServiceClient, v string) GetResult { + var res GetResult + _, err := perigee.Request("GET", getURL(client, v), perigee.Options{ + MoreHeaders: client.AuthenticatedHeaders(), + OkCodes: []int{200}, + Results: &res.Body, + }) + res.Err = err + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/apiversions/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/apiversions/requests_test.go new file mode 100644 index 0000000000..56b5e4fc72 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/apiversions/requests_test.go @@ -0,0 +1,145 @@ +package apiversions + +import ( + "fmt" + "net/http" + "testing" + + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestListVersions(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, `{ + "versions": [ + { + "status": "CURRENT", + "updated": "2012-01-04T11:33:21Z", + "id": "v1.0", + "links": [ + { + "href": "http://23.253.228.211:8776/v1/", + "rel": "self" + } + ] + }, + { + "status": "CURRENT", + "updated": "2012-11-21T11:33:21Z", + "id": "v2.0", + "links": [ + { + "href": "http://23.253.228.211:8776/v2/", + "rel": "self" + } + ] + } + ] + }`) + }) + + count := 0 + + List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractAPIVersions(page) + if err != nil { + t.Errorf("Failed to extract API versions: %v", err) + return false, err + } + + expected := []APIVersion{ + APIVersion{ + ID: "v1.0", + Status: "CURRENT", + Updated: "2012-01-04T11:33:21Z", + }, + APIVersion{ + ID: "v2.0", + Status: "CURRENT", + Updated: "2012-11-21T11:33:21Z", + }, + } + + th.AssertDeepEquals(t, expected, actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestAPIInfo(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v1/", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, `{ + "version": { + "status": "CURRENT", + "updated": "2012-01-04T11:33:21Z", + "media-types": [ + { + "base": "application/xml", + "type": "application/vnd.openstack.volume+xml;version=1" + }, + { + "base": "application/json", + "type": "application/vnd.openstack.volume+json;version=1" + } + ], + "id": "v1.0", + "links": [ + { + "href": "http://23.253.228.211:8776/v1/", + "rel": "self" + }, + { + "href": "http://jorgew.github.com/block-storage-api/content/os-block-storage-1.0.pdf", + "type": "application/pdf", + "rel": "describedby" + }, + { + "href": "http://docs.rackspacecloud.com/servers/api/v1.1/application.wadl", + "type": "application/vnd.sun.wadl+xml", + "rel": "describedby" + } + ] + } + }`) + }) + + actual, err := Get(client.ServiceClient(), "v1").Extract() + if err != nil { + t.Errorf("Failed to extract version: %v", err) + } + + expected := APIVersion{ + ID: "v1.0", + Status: "CURRENT", + Updated: "2012-01-04T11:33:21Z", + } + + th.AssertEquals(t, actual.ID, expected.ID) + th.AssertEquals(t, actual.Status, expected.Status) + th.AssertEquals(t, actual.Updated, expected.Updated) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/apiversions/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/apiversions/results.go new file mode 100644 index 0000000000..7b0df115b5 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/apiversions/results.go @@ -0,0 +1,58 @@ +package apiversions + +import ( + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" + + "github.com/mitchellh/mapstructure" +) + +// APIVersion represents an API version for Cinder. +type APIVersion struct { + ID string `json:"id" mapstructure:"id"` // unique identifier + Status string `json:"status" mapstructure:"status"` // current status + Updated string `json:"updated" mapstructure:"updated"` // date last updated +} + +// APIVersionPage is the page returned by a pager when traversing over a +// collection of API versions. +type APIVersionPage struct { + pagination.SinglePageBase +} + +// IsEmpty checks whether an APIVersionPage struct is empty. +func (r APIVersionPage) IsEmpty() (bool, error) { + is, err := ExtractAPIVersions(r) + if err != nil { + return true, err + } + return len(is) == 0, nil +} + +// ExtractAPIVersions takes a collection page, extracts all of the elements, +// and returns them a slice of APIVersion structs. It is effectively a cast. +func ExtractAPIVersions(page pagination.Page) ([]APIVersion, error) { + var resp struct { + Versions []APIVersion `mapstructure:"versions"` + } + + err := mapstructure.Decode(page.(APIVersionPage).Body, &resp) + + return resp.Versions, err +} + +// GetResult represents the result of a get operation. +type GetResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts an API version resource. +func (r GetResult) Extract() (*APIVersion, error) { + var resp struct { + Version *APIVersion `mapstructure:"version"` + } + + err := mapstructure.Decode(r.Body, &resp) + + return resp.Version, err +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/apiversions/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/apiversions/urls.go new file mode 100644 index 0000000000..56f8260a25 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/apiversions/urls.go @@ -0,0 +1,15 @@ +package apiversions + +import ( + "strings" + + "github.com/rackspace/gophercloud" +) + +func getURL(c *gophercloud.ServiceClient, version string) string { + return c.ServiceURL(strings.TrimRight(version, "/") + "/") +} + +func listURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("") +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/apiversions/urls_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/apiversions/urls_test.go new file mode 100644 index 0000000000..37e91425b5 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/apiversions/urls_test.go @@ -0,0 +1,26 @@ +package apiversions + +import ( + "testing" + + "github.com/rackspace/gophercloud" + th "github.com/rackspace/gophercloud/testhelper" +) + +const endpoint = "http://localhost:57909/" + +func endpointClient() *gophercloud.ServiceClient { + return &gophercloud.ServiceClient{Endpoint: endpoint} +} + +func TestGetURL(t *testing.T) { + actual := getURL(endpointClient(), "v1") + expected := endpoint + "v1/" + th.AssertEquals(t, expected, actual) +} + +func TestListURL(t *testing.T) { + actual := listURL(endpointClient()) + expected := endpoint + th.AssertEquals(t, expected, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots/doc.go new file mode 100644 index 0000000000..198f83077c --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots/doc.go @@ -0,0 +1,5 @@ +// Package snapshots provides information and interaction with snapshots in the +// OpenStack Block Storage service. A snapshot is a point in time copy of the +// data contained in an external storage volume, and can be controlled +// programmatically. +package snapshots diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots/fixtures.go new file mode 100644 index 0000000000..d1461fb69d --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots/fixtures.go @@ -0,0 +1,114 @@ +package snapshots + +import ( + "fmt" + "net/http" + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +func MockListResponse(t *testing.T) { + th.Mux.HandleFunc("/snapshots", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` + { + "snapshots": [ + { + "id": "289da7f8-6440-407c-9fb4-7db01ec49164", + "display_name": "snapshot-001" + }, + { + "id": "96c3bda7-c82a-4f50-be73-ca7621794835", + "display_name": "snapshot-002" + } + ] + } + `) + }) +} + +func MockGetResponse(t *testing.T) { + th.Mux.HandleFunc("/snapshots/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, ` +{ + "snapshot": { + "display_name": "snapshot-001", + "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22" + } +} + `) + }) +} + +func MockCreateResponse(t *testing.T) { + th.Mux.HandleFunc("/snapshots", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "snapshot": { + "volume_id": "1234", + "display_name": "snapshot-001" + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, ` +{ + "snapshot": { + "volume_id": "1234", + "display_name": "snapshot-001", + "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22" + } +} + `) + }) +} + +func MockUpdateMetadataResponse(t *testing.T) { + th.Mux.HandleFunc("/snapshots/123/metadata", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestJSONRequest(t, r, ` + { + "metadata": { + "key": "v1" + } + } + `) + + fmt.Fprintf(w, ` + { + "metadata": { + "key": "v1" + } + } + `) + }) +} + +func MockDeleteResponse(t *testing.T) { + th.Mux.HandleFunc("/snapshots/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusNoContent) + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots/requests.go new file mode 100644 index 0000000000..443f696057 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots/requests.go @@ -0,0 +1,188 @@ +package snapshots + +import ( + "fmt" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" + + "github.com/racker/perigee" +) + +// CreateOptsBuilder allows extensions to add additional parameters to the +// Create request. +type CreateOptsBuilder interface { + ToSnapshotCreateMap() (map[string]interface{}, error) +} + +// CreateOpts contains options for creating a Snapshot. This object is passed to +// the snapshots.Create function. For more information about these parameters, +// see the Snapshot object. +type CreateOpts struct { + // OPTIONAL + Description string + // OPTIONAL + Force bool + // OPTIONAL + Metadata map[string]interface{} + // OPTIONAL + Name string + // REQUIRED + VolumeID string +} + +// ToSnapshotCreateMap assembles a request body based on the contents of a +// CreateOpts. +func (opts CreateOpts) ToSnapshotCreateMap() (map[string]interface{}, error) { + s := make(map[string]interface{}) + + if opts.VolumeID == "" { + return nil, fmt.Errorf("Required CreateOpts field 'VolumeID' not set.") + } + s["volume_id"] = opts.VolumeID + + if opts.Description != "" { + s["display_description"] = opts.Description + } + if opts.Force == true { + s["force"] = opts.Force + } + if opts.Metadata != nil { + s["metadata"] = opts.Metadata + } + if opts.Name != "" { + s["display_name"] = opts.Name + } + + return map[string]interface{}{"snapshot": s}, nil +} + +// Create will create a new Snapshot based on the values in CreateOpts. To +// extract the Snapshot object from the response, call the Extract method on the +// CreateResult. +func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult { + var res CreateResult + + reqBody, err := opts.ToSnapshotCreateMap() + if err != nil { + res.Err = err + return res + } + + _, res.Err = perigee.Request("POST", createURL(client), perigee.Options{ + MoreHeaders: client.AuthenticatedHeaders(), + OkCodes: []int{200, 201}, + ReqBody: &reqBody, + Results: &res.Body, + }) + return res +} + +// Delete will delete the existing Snapshot with the provided ID. +func Delete(client *gophercloud.ServiceClient, id string) DeleteResult { + var res DeleteResult + _, res.Err = perigee.Request("DELETE", deleteURL(client, id), perigee.Options{ + MoreHeaders: client.AuthenticatedHeaders(), + OkCodes: []int{202, 204}, + }) + return res +} + +// Get retrieves the Snapshot with the provided ID. To extract the Snapshot +// object from the response, call the Extract method on the GetResult. +func Get(client *gophercloud.ServiceClient, id string) GetResult { + var res GetResult + _, res.Err = perigee.Request("GET", getURL(client, id), perigee.Options{ + Results: &res.Body, + MoreHeaders: client.AuthenticatedHeaders(), + OkCodes: []int{200}, + }) + return res +} + +// ListOptsBuilder allows extensions to add additional parameters to the List +// request. +type ListOptsBuilder interface { + ToSnapshotListQuery() (string, error) +} + +// ListOpts hold options for listing Snapshots. It is passed to the +// snapshots.List function. +type ListOpts struct { + Name string `q:"display_name"` + Status string `q:"status"` + VolumeID string `q:"volume_id"` +} + +// ToSnapshotListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToSnapshotListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + if err != nil { + return "", err + } + return q.String(), nil +} + +// List returns Snapshots optionally limited by the conditions provided in +// ListOpts. +func List(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(client) + if opts != nil { + query, err := opts.ToSnapshotListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + + createPage := func(r pagination.PageResult) pagination.Page { + return ListResult{pagination.SinglePageBase(r)} + } + return pagination.NewPager(client, url, createPage) +} + +// UpdateMetadataOptsBuilder allows extensions to add additional parameters to +// the Update request. +type UpdateMetadataOptsBuilder interface { + ToSnapshotUpdateMetadataMap() (map[string]interface{}, error) +} + +// UpdateMetadataOpts contain options for updating an existing Snapshot. This +// object is passed to the snapshots.Update function. For more information +// about the parameters, see the Snapshot object. +type UpdateMetadataOpts struct { + Metadata map[string]interface{} +} + +// ToSnapshotUpdateMetadataMap assembles a request body based on the contents of +// an UpdateMetadataOpts. +func (opts UpdateMetadataOpts) ToSnapshotUpdateMetadataMap() (map[string]interface{}, error) { + v := make(map[string]interface{}) + + if opts.Metadata != nil { + v["metadata"] = opts.Metadata + } + + return v, nil +} + +// UpdateMetadata will update the Snapshot with provided information. To +// extract the updated Snapshot from the response, call the ExtractMetadata +// method on the UpdateMetadataResult. +func UpdateMetadata(client *gophercloud.ServiceClient, id string, opts UpdateMetadataOptsBuilder) UpdateMetadataResult { + var res UpdateMetadataResult + + reqBody, err := opts.ToSnapshotUpdateMetadataMap() + if err != nil { + res.Err = err + return res + } + + _, res.Err = perigee.Request("PUT", updateMetadataURL(client, id), perigee.Options{ + MoreHeaders: client.AuthenticatedHeaders(), + OkCodes: []int{200}, + ReqBody: &reqBody, + Results: &res.Body, + }) + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots/requests_test.go new file mode 100644 index 0000000000..d0f9e887e8 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots/requests_test.go @@ -0,0 +1,104 @@ +package snapshots + +import ( + "testing" + + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockListResponse(t) + + count := 0 + + List(client.ServiceClient(), &ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractSnapshots(page) + if err != nil { + t.Errorf("Failed to extract snapshots: %v", err) + return false, err + } + + expected := []Snapshot{ + Snapshot{ + ID: "289da7f8-6440-407c-9fb4-7db01ec49164", + Name: "snapshot-001", + }, + Snapshot{ + ID: "96c3bda7-c82a-4f50-be73-ca7621794835", + Name: "snapshot-002", + }, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockGetResponse(t) + + v, err := Get(client.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22").Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, v.Name, "snapshot-001") + th.AssertEquals(t, v.ID, "d32019d3-bc6e-4319-9c1d-6722fc136a22") +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockCreateResponse(t) + + options := CreateOpts{VolumeID: "1234", Name: "snapshot-001"} + n, err := Create(client.ServiceClient(), options).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, n.VolumeID, "1234") + th.AssertEquals(t, n.Name, "snapshot-001") + th.AssertEquals(t, n.ID, "d32019d3-bc6e-4319-9c1d-6722fc136a22") +} + +func TestUpdateMetadata(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockUpdateMetadataResponse(t) + + expected := map[string]interface{}{"key": "v1"} + + options := &UpdateMetadataOpts{ + Metadata: map[string]interface{}{ + "key": "v1", + }, + } + + actual, err := UpdateMetadata(client.ServiceClient(), "123", options).ExtractMetadata() + + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, actual, expected) +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockDeleteResponse(t) + + res := Delete(client.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22") + th.AssertNoErr(t, res.Err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots/results.go new file mode 100644 index 0000000000..e595798e4a --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots/results.go @@ -0,0 +1,123 @@ +package snapshots + +import ( + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" + + "github.com/mitchellh/mapstructure" +) + +// Snapshot contains all the information associated with an OpenStack Snapshot. +type Snapshot struct { + // Currect status of the Snapshot. + Status string `mapstructure:"status"` + + // Display name. + Name string `mapstructure:"display_name"` + + // Instances onto which the Snapshot is attached. + Attachments []string `mapstructure:"attachments"` + + // Logical group. + AvailabilityZone string `mapstructure:"availability_zone"` + + // Is the Snapshot bootable? + Bootable string `mapstructure:"bootable"` + + // Date created. + CreatedAt string `mapstructure:"created_at"` + + // Display description. + Description string `mapstructure:"display_discription"` + + // See VolumeType object for more information. + VolumeType string `mapstructure:"volume_type"` + + // ID of the Snapshot from which this Snapshot was created. + SnapshotID string `mapstructure:"snapshot_id"` + + // ID of the Volume from which this Snapshot was created. + VolumeID string `mapstructure:"volume_id"` + + // User-defined key-value pairs. + Metadata map[string]string `mapstructure:"metadata"` + + // Unique identifier. + ID string `mapstructure:"id"` + + // Size of the Snapshot, in GB. + Size int `mapstructure:"size"` +} + +// CreateResult contains the response body and error from a Create request. +type CreateResult struct { + commonResult +} + +// GetResult contains the response body and error from a Get request. +type GetResult struct { + commonResult +} + +// DeleteResult contains the response body and error from a Delete request. +type DeleteResult struct { + gophercloud.ErrResult +} + +// ListResult is a pagination.Pager that is returned from a call to the List function. +type ListResult struct { + pagination.SinglePageBase +} + +// IsEmpty returns true if a ListResult contains no Snapshots. +func (r ListResult) IsEmpty() (bool, error) { + volumes, err := ExtractSnapshots(r) + if err != nil { + return true, err + } + return len(volumes) == 0, nil +} + +// ExtractSnapshots extracts and returns Snapshots. It is used while iterating over a snapshots.List call. +func ExtractSnapshots(page pagination.Page) ([]Snapshot, error) { + var response struct { + Snapshots []Snapshot `json:"snapshots"` + } + + err := mapstructure.Decode(page.(ListResult).Body, &response) + return response.Snapshots, err +} + +// UpdateMetadataResult contains the response body and error from an UpdateMetadata request. +type UpdateMetadataResult struct { + commonResult +} + +// ExtractMetadata returns the metadata from a response from snapshots.UpdateMetadata. +func (r UpdateMetadataResult) ExtractMetadata() (map[string]interface{}, error) { + if r.Err != nil { + return nil, r.Err + } + + m := r.Body.(map[string]interface{})["metadata"] + return m.(map[string]interface{}), nil +} + +type commonResult struct { + gophercloud.Result +} + +// Extract will get the Snapshot object out of the commonResult object. +func (r commonResult) Extract() (*Snapshot, error) { + if r.Err != nil { + return nil, r.Err + } + + var res struct { + Snapshot *Snapshot `json:"snapshot"` + } + + err := mapstructure.Decode(r.Body, &res) + + return res.Snapshot, err +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots/urls.go new file mode 100644 index 0000000000..4d635e8dd4 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots/urls.go @@ -0,0 +1,27 @@ +package snapshots + +import "github.com/rackspace/gophercloud" + +func createURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("snapshots") +} + +func deleteURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL("snapshots", id) +} + +func getURL(c *gophercloud.ServiceClient, id string) string { + return deleteURL(c, id) +} + +func listURL(c *gophercloud.ServiceClient) string { + return createURL(c) +} + +func metadataURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL("snapshots", id, "metadata") +} + +func updateMetadataURL(c *gophercloud.ServiceClient, id string) string { + return metadataURL(c, id) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots/urls_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots/urls_test.go new file mode 100644 index 0000000000..feacf7f69b --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots/urls_test.go @@ -0,0 +1,50 @@ +package snapshots + +import ( + "testing" + + "github.com/rackspace/gophercloud" + th "github.com/rackspace/gophercloud/testhelper" +) + +const endpoint = "http://localhost:57909" + +func endpointClient() *gophercloud.ServiceClient { + return &gophercloud.ServiceClient{Endpoint: endpoint} +} + +func TestCreateURL(t *testing.T) { + actual := createURL(endpointClient()) + expected := endpoint + "snapshots" + th.AssertEquals(t, expected, actual) +} + +func TestDeleteURL(t *testing.T) { + actual := deleteURL(endpointClient(), "foo") + expected := endpoint + "snapshots/foo" + th.AssertEquals(t, expected, actual) +} + +func TestGetURL(t *testing.T) { + actual := getURL(endpointClient(), "foo") + expected := endpoint + "snapshots/foo" + th.AssertEquals(t, expected, actual) +} + +func TestListURL(t *testing.T) { + actual := listURL(endpointClient()) + expected := endpoint + "snapshots" + th.AssertEquals(t, expected, actual) +} + +func TestMetadataURL(t *testing.T) { + actual := metadataURL(endpointClient(), "foo") + expected := endpoint + "snapshots/foo/metadata" + th.AssertEquals(t, expected, actual) +} + +func TestUpdateMetadataURL(t *testing.T) { + actual := updateMetadataURL(endpointClient(), "foo") + expected := endpoint + "snapshots/foo/metadata" + th.AssertEquals(t, expected, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots/util.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots/util.go new file mode 100644 index 0000000000..64cdc607ec --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots/util.go @@ -0,0 +1,22 @@ +package snapshots + +import ( + "github.com/rackspace/gophercloud" +) + +// WaitForStatus will continually poll the resource, checking for a particular +// status. It will do this for the amount of seconds defined. +func WaitForStatus(c *gophercloud.ServiceClient, id, status string, secs int) error { + return gophercloud.WaitFor(secs, func() (bool, error) { + current, err := Get(c, id).Extract() + if err != nil { + return false, err + } + + if current.Status == status { + return true, nil + } + + return false, nil + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/doc.go new file mode 100644 index 0000000000..307b8b12d2 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/doc.go @@ -0,0 +1,5 @@ +// Package volumes provides information and interaction with volumes in the +// OpenStack Block Storage service. A volume is a detachable block storage +// device, akin to a USB hard drive. It can only be attached to one instance at +// a time. +package volumes diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/fixtures.go new file mode 100644 index 0000000000..a01ad05a38 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/fixtures.go @@ -0,0 +1,105 @@ +package volumes + +import ( + "fmt" + "net/http" + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +func MockListResponse(t *testing.T) { + th.Mux.HandleFunc("/volumes", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` + { + "volumes": [ + { + "id": "289da7f8-6440-407c-9fb4-7db01ec49164", + "display_name": "vol-001" + }, + { + "id": "96c3bda7-c82a-4f50-be73-ca7621794835", + "display_name": "vol-002" + } + ] + } + `) + }) +} + +func MockGetResponse(t *testing.T) { + th.Mux.HandleFunc("/volumes/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, ` +{ + "volume": { + "display_name": "vol-001", + "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22" + } +} + `) + }) +} + +func MockCreateResponse(t *testing.T) { + th.Mux.HandleFunc("/volumes", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "volume": { + "size": 75 + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, ` +{ + "volume": { + "size": 4, + "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22" + } +} + `) + }) +} + +func MockDeleteResponse(t *testing.T) { + th.Mux.HandleFunc("/volumes/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusNoContent) + }) +} + +func MockUpdateResponse(t *testing.T) { + th.Mux.HandleFunc("/volumes/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, ` + { + "volume": { + "display_name": "vol-002", + "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22" + } + } + `) + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/requests.go new file mode 100644 index 0000000000..f4332de657 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/requests.go @@ -0,0 +1,217 @@ +package volumes + +import ( + "fmt" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" + + "github.com/racker/perigee" +) + +// CreateOptsBuilder allows extensions to add additional parameters to the +// Create request. +type CreateOptsBuilder interface { + ToVolumeCreateMap() (map[string]interface{}, error) +} + +// CreateOpts contains options for creating a Volume. This object is passed to +// the volumes.Create function. For more information about these parameters, +// see the Volume object. +type CreateOpts struct { + // OPTIONAL + Availability string + // OPTIONAL + Description string + // OPTIONAL + Metadata map[string]string + // OPTIONAL + Name string + // REQUIRED + Size int + // OPTIONAL + SnapshotID, SourceVolID, ImageID string + // OPTIONAL + VolumeType string +} + +// ToVolumeCreateMap assembles a request body based on the contents of a +// CreateOpts. +func (opts CreateOpts) ToVolumeCreateMap() (map[string]interface{}, error) { + v := make(map[string]interface{}) + + if opts.Size == 0 { + return nil, fmt.Errorf("Required CreateOpts field 'Size' not set.") + } + v["size"] = opts.Size + + if opts.Availability != "" { + v["availability_zone"] = opts.Availability + } + if opts.Description != "" { + v["display_description"] = opts.Description + } + if opts.ImageID != "" { + v["imageRef"] = opts.ImageID + } + if opts.Metadata != nil { + v["metadata"] = opts.Metadata + } + if opts.Name != "" { + v["display_name"] = opts.Name + } + if opts.SourceVolID != "" { + v["source_volid"] = opts.SourceVolID + } + if opts.SnapshotID != "" { + v["snapshot_id"] = opts.SnapshotID + } + if opts.VolumeType != "" { + v["volume_type"] = opts.VolumeType + } + + return map[string]interface{}{"volume": v}, nil +} + +// Create will create a new Volume based on the values in CreateOpts. To extract +// the Volume object from the response, call the Extract method on the +// CreateResult. +func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult { + var res CreateResult + + reqBody, err := opts.ToVolumeCreateMap() + if err != nil { + res.Err = err + return res + } + + _, res.Err = perigee.Request("POST", createURL(client), perigee.Options{ + MoreHeaders: client.AuthenticatedHeaders(), + ReqBody: &reqBody, + Results: &res.Body, + OkCodes: []int{200, 201}, + }) + return res +} + +// Delete will delete the existing Volume with the provided ID. +func Delete(client *gophercloud.ServiceClient, id string) DeleteResult { + var res DeleteResult + _, res.Err = perigee.Request("DELETE", deleteURL(client, id), perigee.Options{ + MoreHeaders: client.AuthenticatedHeaders(), + OkCodes: []int{202, 204}, + }) + return res +} + +// Get retrieves the Volume with the provided ID. To extract the Volume object +// from the response, call the Extract method on the GetResult. +func Get(client *gophercloud.ServiceClient, id string) GetResult { + var res GetResult + _, res.Err = perigee.Request("GET", getURL(client, id), perigee.Options{ + Results: &res.Body, + MoreHeaders: client.AuthenticatedHeaders(), + OkCodes: []int{200}, + }) + return res +} + +// ListOptsBuilder allows extensions to add additional parameters to the List +// request. +type ListOptsBuilder interface { + ToVolumeListQuery() (string, error) +} + +// ListOpts holds options for listing Volumes. It is passed to the volumes.List +// function. +type ListOpts struct { + // admin-only option. Set it to true to see all tenant volumes. + AllTenants bool `q:"all_tenants"` + // List only volumes that contain Metadata. + Metadata map[string]string `q:"metadata"` + // List only volumes that have Name as the display name. + Name string `q:"name"` + // List only volumes that have a status of Status. + Status string `q:"status"` +} + +// ToVolumeListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToVolumeListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + if err != nil { + return "", err + } + return q.String(), nil +} + +// List returns Volumes optionally limited by the conditions provided in ListOpts. +func List(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(client) + if opts != nil { + query, err := opts.ToVolumeListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + createPage := func(r pagination.PageResult) pagination.Page { + return ListResult{pagination.SinglePageBase(r)} + } + return pagination.NewPager(client, url, createPage) +} + +// UpdateOptsBuilder allows extensions to add additional parameters to the +// Update request. +type UpdateOptsBuilder interface { + ToVolumeUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts contain options for updating an existing Volume. This object is passed +// to the volumes.Update function. For more information about the parameters, see +// the Volume object. +type UpdateOpts struct { + // OPTIONAL + Name string + // OPTIONAL + Description string + // OPTIONAL + Metadata map[string]string +} + +// ToVolumeUpdateMap assembles a request body based on the contents of an +// UpdateOpts. +func (opts UpdateOpts) ToVolumeUpdateMap() (map[string]interface{}, error) { + v := make(map[string]interface{}) + + if opts.Description != "" { + v["display_description"] = opts.Description + } + if opts.Metadata != nil { + v["metadata"] = opts.Metadata + } + if opts.Name != "" { + v["display_name"] = opts.Name + } + + return map[string]interface{}{"volume": v}, nil +} + +// Update will update the Volume with provided information. To extract the updated +// Volume from the response, call the Extract method on the UpdateResult. +func Update(client *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) UpdateResult { + var res UpdateResult + + reqBody, err := opts.ToVolumeUpdateMap() + if err != nil { + res.Err = err + return res + } + + _, res.Err = perigee.Request("PUT", updateURL(client, id), perigee.Options{ + MoreHeaders: client.AuthenticatedHeaders(), + OkCodes: []int{200}, + ReqBody: &reqBody, + Results: &res.Body, + }) + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/requests_test.go new file mode 100644 index 0000000000..067f89bdd9 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/requests_test.go @@ -0,0 +1,95 @@ +package volumes + +import ( + "testing" + + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockListResponse(t) + + count := 0 + + List(client.ServiceClient(), &ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractVolumes(page) + if err != nil { + t.Errorf("Failed to extract volumes: %v", err) + return false, err + } + + expected := []Volume{ + Volume{ + ID: "289da7f8-6440-407c-9fb4-7db01ec49164", + Name: "vol-001", + }, + Volume{ + ID: "96c3bda7-c82a-4f50-be73-ca7621794835", + Name: "vol-002", + }, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockGetResponse(t) + + v, err := Get(client.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22").Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, v.Name, "vol-001") + th.AssertEquals(t, v.ID, "d32019d3-bc6e-4319-9c1d-6722fc136a22") +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockCreateResponse(t) + + options := &CreateOpts{Size: 75} + n, err := Create(client.ServiceClient(), options).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, n.Size, 4) + th.AssertEquals(t, n.ID, "d32019d3-bc6e-4319-9c1d-6722fc136a22") +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockDeleteResponse(t) + + res := Delete(client.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22") + th.AssertNoErr(t, res.Err) +} + +func TestUpdate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockUpdateResponse(t) + + options := UpdateOpts{Name: "vol-002"} + v, err := Update(client.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22", options).Extract() + th.AssertNoErr(t, err) + th.CheckEquals(t, "vol-002", v.Name) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/results.go new file mode 100644 index 0000000000..c6ddbb5167 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/results.go @@ -0,0 +1,113 @@ +package volumes + +import ( + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" + + "github.com/mitchellh/mapstructure" +) + +// Volume contains all the information associated with an OpenStack Volume. +type Volume struct { + // Current status of the volume. + Status string `mapstructure:"status"` + + // Human-readable display name for the volume. + Name string `mapstructure:"display_name"` + + // Instances onto which the volume is attached. + Attachments []string `mapstructure:"attachments"` + + // This parameter is no longer used. + AvailabilityZone string `mapstructure:"availability_zone"` + + // Indicates whether this is a bootable volume. + Bootable string `mapstructure:"bootable"` + + // The date when this volume was created. + CreatedAt string `mapstructure:"created_at"` + + // Human-readable description for the volume. + Description string `mapstructure:"display_discription"` + + // The type of volume to create, either SATA or SSD. + VolumeType string `mapstructure:"volume_type"` + + // The ID of the snapshot from which the volume was created + SnapshotID string `mapstructure:"snapshot_id"` + + // The ID of another block storage volume from which the current volume was created + SourceVolID string `mapstructure:"source_volid"` + + // Arbitrary key-value pairs defined by the user. + Metadata map[string]string `mapstructure:"metadata"` + + // Unique identifier for the volume. + ID string `mapstructure:"id"` + + // Size of the volume in GB. + Size int `mapstructure:"size"` +} + +// CreateResult contains the response body and error from a Create request. +type CreateResult struct { + commonResult +} + +// GetResult contains the response body and error from a Get request. +type GetResult struct { + commonResult +} + +// DeleteResult contains the response body and error from a Delete request. +type DeleteResult struct { + gophercloud.ErrResult +} + +// ListResult is a pagination.pager that is returned from a call to the List function. +type ListResult struct { + pagination.SinglePageBase +} + +// IsEmpty returns true if a ListResult contains no Volumes. +func (r ListResult) IsEmpty() (bool, error) { + volumes, err := ExtractVolumes(r) + if err != nil { + return true, err + } + return len(volumes) == 0, nil +} + +// ExtractVolumes extracts and returns Volumes. It is used while iterating over a volumes.List call. +func ExtractVolumes(page pagination.Page) ([]Volume, error) { + var response struct { + Volumes []Volume `json:"volumes"` + } + + err := mapstructure.Decode(page.(ListResult).Body, &response) + return response.Volumes, err +} + +// UpdateResult contains the response body and error from an Update request. +type UpdateResult struct { + commonResult +} + +type commonResult struct { + gophercloud.Result +} + +// Extract will get the Volume object out of the commonResult object. +func (r commonResult) Extract() (*Volume, error) { + if r.Err != nil { + return nil, r.Err + } + + var res struct { + Volume *Volume `json:"volume"` + } + + err := mapstructure.Decode(r.Body, &res) + + return res.Volume, err +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/urls.go new file mode 100644 index 0000000000..29629a1af8 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/urls.go @@ -0,0 +1,23 @@ +package volumes + +import "github.com/rackspace/gophercloud" + +func createURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("volumes") +} + +func listURL(c *gophercloud.ServiceClient) string { + return createURL(c) +} + +func deleteURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL("volumes", id) +} + +func getURL(c *gophercloud.ServiceClient, id string) string { + return deleteURL(c, id) +} + +func updateURL(c *gophercloud.ServiceClient, id string) string { + return deleteURL(c, id) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/urls_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/urls_test.go new file mode 100644 index 0000000000..a95270e14c --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/urls_test.go @@ -0,0 +1,44 @@ +package volumes + +import ( + "testing" + + "github.com/rackspace/gophercloud" + th "github.com/rackspace/gophercloud/testhelper" +) + +const endpoint = "http://localhost:57909" + +func endpointClient() *gophercloud.ServiceClient { + return &gophercloud.ServiceClient{Endpoint: endpoint} +} + +func TestCreateURL(t *testing.T) { + actual := createURL(endpointClient()) + expected := endpoint + "volumes" + th.AssertEquals(t, expected, actual) +} + +func TestListURL(t *testing.T) { + actual := listURL(endpointClient()) + expected := endpoint + "volumes" + th.AssertEquals(t, expected, actual) +} + +func TestDeleteURL(t *testing.T) { + actual := deleteURL(endpointClient(), "foo") + expected := endpoint + "volumes/foo" + th.AssertEquals(t, expected, actual) +} + +func TestGetURL(t *testing.T) { + actual := getURL(endpointClient(), "foo") + expected := endpoint + "volumes/foo" + th.AssertEquals(t, expected, actual) +} + +func TestUpdateURL(t *testing.T) { + actual := updateURL(endpointClient(), "foo") + expected := endpoint + "volumes/foo" + th.AssertEquals(t, expected, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/util.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/util.go new file mode 100644 index 0000000000..1dda695ea0 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/util.go @@ -0,0 +1,22 @@ +package volumes + +import ( + "github.com/rackspace/gophercloud" +) + +// WaitForStatus will continually poll the resource, checking for a particular +// status. It will do this for the amount of seconds defined. +func WaitForStatus(c *gophercloud.ServiceClient, id, status string, secs int) error { + return gophercloud.WaitFor(secs, func() (bool, error) { + current, err := Get(c, id).Extract() + if err != nil { + return false, err + } + + if current.Status == status { + return true, nil + } + + return false, nil + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumetypes/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumetypes/doc.go new file mode 100644 index 0000000000..793084f89b --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumetypes/doc.go @@ -0,0 +1,9 @@ +// Package volumetypes provides information and interaction with volume types +// in the OpenStack Block Storage service. A volume type indicates the type of +// a block storage volume, such as SATA, SCSCI, SSD, etc. These can be +// customized or defined by the OpenStack admin. +// +// You can also define extra_specs associated with your volume types. For +// instance, you could have a VolumeType=SATA, with extra_specs (RPM=10000, +// RAID-Level=5) . Extra_specs are defined and customized by the admin. +package volumetypes diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumetypes/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumetypes/fixtures.go new file mode 100644 index 0000000000..e3326eae14 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumetypes/fixtures.go @@ -0,0 +1,60 @@ +package volumetypes + +import ( + "fmt" + "net/http" + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +func MockListResponse(t *testing.T) { + th.Mux.HandleFunc("/types", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` + { + "volume_types": [ + { + "id": "289da7f8-6440-407c-9fb4-7db01ec49164", + "name": "vol-type-001", + "extra_specs": { + "capabilities": "gpu" + } + }, + { + "id": "96c3bda7-c82a-4f50-be73-ca7621794835", + "name": "vol-type-002", + "extra_specs": {} + } + ] + } + `) + }) +} + +func MockGetResponse(t *testing.T) { + th.Mux.HandleFunc("/types/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, ` +{ + "volume_type": { + "name": "vol-type-001", + "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22", + "extra_specs": { + "serverNumber": "2" + } + } +} + `) + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumetypes/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumetypes/requests.go new file mode 100644 index 0000000000..87e20f6003 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumetypes/requests.go @@ -0,0 +1,87 @@ +package volumetypes + +import ( + "github.com/racker/perigee" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// CreateOptsBuilder allows extensions to add additional parameters to the +// Create request. +type CreateOptsBuilder interface { + ToVolumeTypeCreateMap() (map[string]interface{}, error) +} + +// CreateOpts are options for creating a volume type. +type CreateOpts struct { + // OPTIONAL. See VolumeType. + ExtraSpecs map[string]interface{} + // OPTIONAL. See VolumeType. + Name string +} + +// ToVolumeTypeCreateMap casts a CreateOpts struct to a map. +func (opts CreateOpts) ToVolumeTypeCreateMap() (map[string]interface{}, error) { + vt := make(map[string]interface{}) + + if opts.ExtraSpecs != nil { + vt["extra_specs"] = opts.ExtraSpecs + } + if opts.Name != "" { + vt["name"] = opts.Name + } + + return map[string]interface{}{"volume_type": vt}, nil +} + +// Create will create a new volume. To extract the created volume type object, +// call the Extract method on the CreateResult. +func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult { + var res CreateResult + + reqBody, err := opts.ToVolumeTypeCreateMap() + if err != nil { + res.Err = err + return res + } + + _, res.Err = perigee.Request("POST", createURL(client), perigee.Options{ + MoreHeaders: client.AuthenticatedHeaders(), + OkCodes: []int{200, 201}, + ReqBody: &reqBody, + Results: &res.Body, + }) + return res +} + +// Delete will delete the volume type with the provided ID. +func Delete(client *gophercloud.ServiceClient, id string) DeleteResult { + var res DeleteResult + _, res.Err = perigee.Request("DELETE", deleteURL(client, id), perigee.Options{ + MoreHeaders: client.AuthenticatedHeaders(), + OkCodes: []int{202}, + }) + return res +} + +// Get will retrieve the volume type with the provided ID. To extract the volume +// type from the result, call the Extract method on the GetResult. +func Get(client *gophercloud.ServiceClient, id string) GetResult { + var res GetResult + _, err := perigee.Request("GET", getURL(client, id), perigee.Options{ + MoreHeaders: client.AuthenticatedHeaders(), + OkCodes: []int{200}, + Results: &res.Body, + }) + res.Err = err + return res +} + +// List returns all volume types. +func List(client *gophercloud.ServiceClient) pagination.Pager { + createPage := func(r pagination.PageResult) pagination.Page { + return ListResult{pagination.SinglePageBase(r)} + } + + return pagination.NewPager(client, listURL(client), createPage) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumetypes/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumetypes/requests_test.go new file mode 100644 index 0000000000..8d40bfe1d4 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumetypes/requests_test.go @@ -0,0 +1,118 @@ +package volumetypes + +import ( + "fmt" + "net/http" + "testing" + + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockListResponse(t) + + count := 0 + + List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractVolumeTypes(page) + if err != nil { + t.Errorf("Failed to extract volume types: %v", err) + return false, err + } + + expected := []VolumeType{ + VolumeType{ + ID: "289da7f8-6440-407c-9fb4-7db01ec49164", + Name: "vol-type-001", + ExtraSpecs: map[string]interface{}{ + "capabilities": "gpu", + }, + }, + VolumeType{ + ID: "96c3bda7-c82a-4f50-be73-ca7621794835", + Name: "vol-type-002", + ExtraSpecs: map[string]interface{}{}, + }, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockGetResponse(t) + + vt, err := Get(client.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22").Extract() + th.AssertNoErr(t, err) + + th.AssertDeepEquals(t, vt.ExtraSpecs, map[string]interface{}{"serverNumber": "2"}) + th.AssertEquals(t, vt.Name, "vol-type-001") + th.AssertEquals(t, vt.ID, "d32019d3-bc6e-4319-9c1d-6722fc136a22") +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/types", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "volume_type": { + "name": "vol-type-001" + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, ` +{ + "volume_type": { + "name": "vol-type-001", + "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22" + } +} + `) + }) + + options := &CreateOpts{Name: "vol-type-001"} + n, err := Create(client.ServiceClient(), options).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, n.Name, "vol-type-001") + th.AssertEquals(t, n.ID, "d32019d3-bc6e-4319-9c1d-6722fc136a22") +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/types/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + w.WriteHeader(http.StatusAccepted) + }) + + err := Delete(client.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22").ExtractErr() + th.AssertNoErr(t, err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumetypes/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumetypes/results.go new file mode 100644 index 0000000000..c049a045d8 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumetypes/results.go @@ -0,0 +1,72 @@ +package volumetypes + +import ( + "github.com/mitchellh/mapstructure" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// VolumeType contains all information associated with an OpenStack Volume Type. +type VolumeType struct { + ExtraSpecs map[string]interface{} `json:"extra_specs" mapstructure:"extra_specs"` // user-defined metadata + ID string `json:"id" mapstructure:"id"` // unique identifier + Name string `json:"name" mapstructure:"name"` // display name +} + +// CreateResult contains the response body and error from a Create request. +type CreateResult struct { + commonResult +} + +// GetResult contains the response body and error from a Get request. +type GetResult struct { + commonResult +} + +// DeleteResult contains the response error from a Delete request. +type DeleteResult struct { + gophercloud.ErrResult +} + +// ListResult is a pagination.Pager that is returned from a call to the List function. +type ListResult struct { + pagination.SinglePageBase +} + +// IsEmpty returns true if a ListResult contains no Volume Types. +func (r ListResult) IsEmpty() (bool, error) { + volumeTypes, err := ExtractVolumeTypes(r) + if err != nil { + return true, err + } + return len(volumeTypes) == 0, nil +} + +// ExtractVolumeTypes extracts and returns Volume Types. +func ExtractVolumeTypes(page pagination.Page) ([]VolumeType, error) { + var response struct { + VolumeTypes []VolumeType `mapstructure:"volume_types"` + } + + err := mapstructure.Decode(page.(ListResult).Body, &response) + return response.VolumeTypes, err +} + +type commonResult struct { + gophercloud.Result +} + +// Extract will get the Volume Type object out of the commonResult object. +func (r commonResult) Extract() (*VolumeType, error) { + if r.Err != nil { + return nil, r.Err + } + + var res struct { + VolumeType *VolumeType `json:"volume_type" mapstructure:"volume_type"` + } + + err := mapstructure.Decode(r.Body, &res) + + return res.VolumeType, err +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumetypes/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumetypes/urls.go new file mode 100644 index 0000000000..cf8367bfab --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumetypes/urls.go @@ -0,0 +1,19 @@ +package volumetypes + +import "github.com/rackspace/gophercloud" + +func listURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("types") +} + +func createURL(c *gophercloud.ServiceClient) string { + return listURL(c) +} + +func getURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL("types", id) +} + +func deleteURL(c *gophercloud.ServiceClient, id string) string { + return getURL(c, id) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumetypes/urls_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumetypes/urls_test.go new file mode 100644 index 0000000000..44016e2954 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumetypes/urls_test.go @@ -0,0 +1,38 @@ +package volumetypes + +import ( + "testing" + + "github.com/rackspace/gophercloud" + th "github.com/rackspace/gophercloud/testhelper" +) + +const endpoint = "http://localhost:57909" + +func endpointClient() *gophercloud.ServiceClient { + return &gophercloud.ServiceClient{Endpoint: endpoint} +} + +func TestListURL(t *testing.T) { + actual := listURL(endpointClient()) + expected := endpoint + "types" + th.AssertEquals(t, expected, actual) +} + +func TestCreateURL(t *testing.T) { + actual := createURL(endpointClient()) + expected := endpoint + "types" + th.AssertEquals(t, expected, actual) +} + +func TestGetURL(t *testing.T) { + actual := getURL(endpointClient(), "foo") + expected := endpoint + "types/foo" + th.AssertEquals(t, expected, actual) +} + +func TestDeleteURL(t *testing.T) { + actual := deleteURL(endpointClient(), "foo") + expected := endpoint + "types/foo" + th.AssertEquals(t, expected, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/client.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/client.go new file mode 100644 index 0000000000..99b3d466d3 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/client.go @@ -0,0 +1,205 @@ +package openstack + +import ( + "fmt" + "net/url" + + "github.com/rackspace/gophercloud" + tokens2 "github.com/rackspace/gophercloud/openstack/identity/v2/tokens" + tokens3 "github.com/rackspace/gophercloud/openstack/identity/v3/tokens" + "github.com/rackspace/gophercloud/openstack/utils" +) + +const ( + v20 = "v2.0" + v30 = "v3.0" +) + +// NewClient prepares an unauthenticated ProviderClient instance. +// Most users will probably prefer using the AuthenticatedClient function instead. +// This is useful if you wish to explicitly control the version of the identity service that's used for authentication explicitly, +// for example. +func NewClient(endpoint string) (*gophercloud.ProviderClient, error) { + u, err := url.Parse(endpoint) + if err != nil { + return nil, err + } + hadPath := u.Path != "" + u.Path, u.RawQuery, u.Fragment = "", "", "" + base := u.String() + + endpoint = gophercloud.NormalizeURL(endpoint) + base = gophercloud.NormalizeURL(base) + + if hadPath { + return &gophercloud.ProviderClient{ + IdentityBase: base, + IdentityEndpoint: endpoint, + }, nil + } + + return &gophercloud.ProviderClient{ + IdentityBase: base, + IdentityEndpoint: "", + }, nil +} + +// AuthenticatedClient logs in to an OpenStack cloud found at the identity endpoint specified by options, acquires a token, and +// returns a Client instance that's ready to operate. +// It first queries the root identity endpoint to determine which versions of the identity service are supported, then chooses +// the most recent identity service available to proceed. +func AuthenticatedClient(options gophercloud.AuthOptions) (*gophercloud.ProviderClient, error) { + client, err := NewClient(options.IdentityEndpoint) + if err != nil { + return nil, err + } + + err = Authenticate(client, options) + if err != nil { + return nil, err + } + return client, nil +} + +// Authenticate or re-authenticate against the most recent identity service supported at the provided endpoint. +func Authenticate(client *gophercloud.ProviderClient, options gophercloud.AuthOptions) error { + versions := []*utils.Version{ + &utils.Version{ID: v20, Priority: 20, Suffix: "/v2.0/"}, + &utils.Version{ID: v30, Priority: 30, Suffix: "/v3/"}, + } + + chosen, endpoint, err := utils.ChooseVersion(client.IdentityBase, client.IdentityEndpoint, versions) + if err != nil { + return err + } + + switch chosen.ID { + case v20: + return v2auth(client, endpoint, options) + case v30: + return v3auth(client, endpoint, options) + default: + // The switch statement must be out of date from the versions list. + return fmt.Errorf("Unrecognized identity version: %s", chosen.ID) + } +} + +// AuthenticateV2 explicitly authenticates against the identity v2 endpoint. +func AuthenticateV2(client *gophercloud.ProviderClient, options gophercloud.AuthOptions) error { + return v2auth(client, "", options) +} + +func v2auth(client *gophercloud.ProviderClient, endpoint string, options gophercloud.AuthOptions) error { + v2Client := NewIdentityV2(client) + if endpoint != "" { + v2Client.Endpoint = endpoint + } + + result := tokens2.Create(v2Client, tokens2.AuthOptions{AuthOptions: options}) + + token, err := result.ExtractToken() + if err != nil { + return err + } + + catalog, err := result.ExtractServiceCatalog() + if err != nil { + return err + } + + client.TokenID = token.ID + client.EndpointLocator = func(opts gophercloud.EndpointOpts) (string, error) { + return V2EndpointURL(catalog, opts) + } + + return nil +} + +// AuthenticateV3 explicitly authenticates against the identity v3 service. +func AuthenticateV3(client *gophercloud.ProviderClient, options gophercloud.AuthOptions) error { + return v3auth(client, "", options) +} + +func v3auth(client *gophercloud.ProviderClient, endpoint string, options gophercloud.AuthOptions) error { + // Override the generated service endpoint with the one returned by the version endpoint. + v3Client := NewIdentityV3(client) + if endpoint != "" { + v3Client.Endpoint = endpoint + } + + token, err := tokens3.Create(v3Client, options, nil).Extract() + if err != nil { + return err + } + client.TokenID = token.ID + + client.EndpointLocator = func(opts gophercloud.EndpointOpts) (string, error) { + return V3EndpointURL(v3Client, opts) + } + + return nil +} + +// NewIdentityV2 creates a ServiceClient that may be used to interact with the v2 identity service. +func NewIdentityV2(client *gophercloud.ProviderClient) *gophercloud.ServiceClient { + v2Endpoint := client.IdentityBase + "v2.0/" + + return &gophercloud.ServiceClient{ + ProviderClient: client, + Endpoint: v2Endpoint, + } +} + +// NewIdentityV3 creates a ServiceClient that may be used to access the v3 identity service. +func NewIdentityV3(client *gophercloud.ProviderClient) *gophercloud.ServiceClient { + v3Endpoint := client.IdentityBase + "v3/" + + return &gophercloud.ServiceClient{ + ProviderClient: client, + Endpoint: v3Endpoint, + } +} + +// NewObjectStorageV1 creates a ServiceClient that may be used with the v1 object storage package. +func NewObjectStorageV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + eo.ApplyDefaults("object-store") + url, err := client.EndpointLocator(eo) + if err != nil { + return nil, err + } + return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil +} + +// NewComputeV2 creates a ServiceClient that may be used with the v2 compute package. +func NewComputeV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + eo.ApplyDefaults("compute") + url, err := client.EndpointLocator(eo) + if err != nil { + return nil, err + } + return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil +} + +// NewNetworkV2 creates a ServiceClient that may be used with the v2 network package. +func NewNetworkV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + eo.ApplyDefaults("network") + url, err := client.EndpointLocator(eo) + if err != nil { + return nil, err + } + return &gophercloud.ServiceClient{ + ProviderClient: client, + Endpoint: url, + ResourceBase: url + "v2.0/", + }, nil +} + +// NewBlockStorageV1 creates a ServiceClient that may be used to access the v1 block storage service. +func NewBlockStorageV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + eo.ApplyDefaults("volume") + url, err := client.EndpointLocator(eo) + if err != nil { + return nil, err + } + return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/client_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/client_test.go new file mode 100644 index 0000000000..257260c4e1 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/client_test.go @@ -0,0 +1,161 @@ +package openstack + +import ( + "fmt" + "net/http" + "testing" + + "github.com/rackspace/gophercloud" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestAuthenticatedClientV3(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + const ID = "0123456789" + + th.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, ` + { + "versions": { + "values": [ + { + "status": "stable", + "id": "v3.0", + "links": [ + { "href": "%s", "rel": "self" } + ] + }, + { + "status": "stable", + "id": "v2.0", + "links": [ + { "href": "%s", "rel": "self" } + ] + } + ] + } + } + `, th.Endpoint()+"v3/", th.Endpoint()+"v2.0/") + }) + + th.Mux.HandleFunc("/v3/auth/tokens", func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("X-Subject-Token", ID) + + w.WriteHeader(http.StatusCreated) + fmt.Fprintf(w, `{ "token": { "expires_at": "2013-02-02T18:30:59.000000Z" } }`) + }) + + options := gophercloud.AuthOptions{ + UserID: "me", + Password: "secret", + IdentityEndpoint: th.Endpoint(), + } + client, err := AuthenticatedClient(options) + th.AssertNoErr(t, err) + th.CheckEquals(t, ID, client.TokenID) +} + +func TestAuthenticatedClientV2(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, ` + { + "versions": { + "values": [ + { + "status": "experimental", + "id": "v3.0", + "links": [ + { "href": "%s", "rel": "self" } + ] + }, + { + "status": "stable", + "id": "v2.0", + "links": [ + { "href": "%s", "rel": "self" } + ] + } + ] + } + } + `, th.Endpoint()+"v3/", th.Endpoint()+"v2.0/") + }) + + th.Mux.HandleFunc("/v2.0/tokens", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, ` + { + "access": { + "token": { + "id": "01234567890", + "expires": "2014-10-01T10:00:00.000000Z" + }, + "serviceCatalog": [ + { + "name": "Cloud Servers", + "type": "compute", + "endpoints": [ + { + "tenantId": "t1000", + "publicURL": "https://compute.north.host.com/v1/t1000", + "internalURL": "https://compute.north.internal/v1/t1000", + "region": "North", + "versionId": "1", + "versionInfo": "https://compute.north.host.com/v1/", + "versionList": "https://compute.north.host.com/" + }, + { + "tenantId": "t1000", + "publicURL": "https://compute.north.host.com/v1.1/t1000", + "internalURL": "https://compute.north.internal/v1.1/t1000", + "region": "North", + "versionId": "1.1", + "versionInfo": "https://compute.north.host.com/v1.1/", + "versionList": "https://compute.north.host.com/" + } + ], + "endpoints_links": [] + }, + { + "name": "Cloud Files", + "type": "object-store", + "endpoints": [ + { + "tenantId": "t1000", + "publicURL": "https://storage.north.host.com/v1/t1000", + "internalURL": "https://storage.north.internal/v1/t1000", + "region": "North", + "versionId": "1", + "versionInfo": "https://storage.north.host.com/v1/", + "versionList": "https://storage.north.host.com/" + }, + { + "tenantId": "t1000", + "publicURL": "https://storage.south.host.com/v1/t1000", + "internalURL": "https://storage.south.internal/v1/t1000", + "region": "South", + "versionId": "1", + "versionInfo": "https://storage.south.host.com/v1/", + "versionList": "https://storage.south.host.com/" + } + ] + } + ] + } + } + `) + }) + + options := gophercloud.AuthOptions{ + Username: "me", + Password: "secret", + IdentityEndpoint: th.Endpoint(), + } + client, err := AuthenticatedClient(options) + th.AssertNoErr(t, err) + th.CheckEquals(t, "01234567890", client.TokenID) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/common/README.md b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/common/README.md new file mode 100644 index 0000000000..7b55795d08 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/common/README.md @@ -0,0 +1,3 @@ +# Common Resources + +This directory is for resources that are shared by multiple services. diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/common/extensions/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/common/extensions/doc.go new file mode 100644 index 0000000000..4a168f4b2c --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/common/extensions/doc.go @@ -0,0 +1,15 @@ +// Package extensions provides information and interaction with the different extensions available +// for an OpenStack service. +// +// The purpose of OpenStack API extensions is to: +// +// - Introduce new features in the API without requiring a version change. +// - Introduce vendor-specific niche functionality. +// - Act as a proving ground for experimental functionalities that might be included in a future +// version of the API. +// +// Extensions usually have tags that prevent conflicts with other extensions that define attributes +// or resources with the same names, and with core resources and attributes. +// Because an extension might not be supported by all plug-ins, its availability varies with deployments +// and the specific plug-in. +package extensions diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/common/extensions/errors.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/common/extensions/errors.go new file mode 100644 index 0000000000..aeec0fa756 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/common/extensions/errors.go @@ -0,0 +1 @@ +package extensions diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/common/extensions/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/common/extensions/fixtures.go new file mode 100644 index 0000000000..0ed7de9f1d --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/common/extensions/fixtures.go @@ -0,0 +1,91 @@ +// +build fixtures + +package extensions + +import ( + "fmt" + "net/http" + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +// ListOutput provides a single page of Extension results. +const ListOutput = ` +{ + "extensions": [ + { + "updated": "2013-01-20T00:00:00-00:00", + "name": "Neutron Service Type Management", + "links": [], + "namespace": "http://docs.openstack.org/ext/neutron/service-type/api/v1.0", + "alias": "service-type", + "description": "API for retrieving service providers for Neutron advanced services" + } + ] +}` + +// GetOutput provides a single Extension result. +const GetOutput = ` +{ + "extension": { + "updated": "2013-02-03T10:00:00-00:00", + "name": "agent", + "links": [], + "namespace": "http://docs.openstack.org/ext/agent/api/v2.0", + "alias": "agent", + "description": "The agent management extension." + } +} +` + +// ListedExtension is the Extension that should be parsed from ListOutput. +var ListedExtension = Extension{ + Updated: "2013-01-20T00:00:00-00:00", + Name: "Neutron Service Type Management", + Links: []interface{}{}, + Namespace: "http://docs.openstack.org/ext/neutron/service-type/api/v1.0", + Alias: "service-type", + Description: "API for retrieving service providers for Neutron advanced services", +} + +// ExpectedExtensions is a slice containing the Extension that should be parsed from ListOutput. +var ExpectedExtensions = []Extension{ListedExtension} + +// SingleExtension is the Extension that should be parsed from GetOutput. +var SingleExtension = &Extension{ + Updated: "2013-02-03T10:00:00-00:00", + Name: "agent", + Links: []interface{}{}, + Namespace: "http://docs.openstack.org/ext/agent/api/v2.0", + Alias: "agent", + Description: "The agent management extension.", +} + +// HandleListExtensionsSuccessfully creates an HTTP handler at `/extensions` on the test handler +// mux that response with a list containing a single tenant. +func HandleListExtensionsSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/extensions", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + + fmt.Fprintf(w, ListOutput) + }) +} + +// HandleGetExtensionSuccessfully creates an HTTP handler at `/extensions/agent` that responds with +// a JSON payload corresponding to SingleExtension. +func HandleGetExtensionSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/extensions/agent", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, GetOutput) + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/common/extensions/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/common/extensions/requests.go new file mode 100644 index 0000000000..3ca6e12f19 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/common/extensions/requests.go @@ -0,0 +1,26 @@ +package extensions + +import ( + "github.com/racker/perigee" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// Get retrieves information for a specific extension using its alias. +func Get(c *gophercloud.ServiceClient, alias string) GetResult { + var res GetResult + _, res.Err = perigee.Request("GET", ExtensionURL(c, alias), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + Results: &res.Body, + OkCodes: []int{200}, + }) + return res +} + +// List returns a Pager which allows you to iterate over the full collection of extensions. +// It does not accept query parameters. +func List(c *gophercloud.ServiceClient) pagination.Pager { + return pagination.NewPager(c, ListExtensionURL(c), func(r pagination.PageResult) pagination.Page { + return ExtensionPage{pagination.SinglePageBase(r)} + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/common/extensions/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/common/extensions/requests_test.go new file mode 100644 index 0000000000..6550283df7 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/common/extensions/requests_test.go @@ -0,0 +1,38 @@ +package extensions + +import ( + "testing" + + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListExtensionsSuccessfully(t) + + count := 0 + + List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractExtensions(page) + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, ExpectedExtensions, actual) + + return true, nil + }) + + th.CheckEquals(t, 1, count) +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleGetExtensionSuccessfully(t) + + actual, err := Get(client.ServiceClient(), "agent").Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, SingleExtension, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/common/extensions/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/common/extensions/results.go new file mode 100644 index 0000000000..777d083fa0 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/common/extensions/results.go @@ -0,0 +1,65 @@ +package extensions + +import ( + "github.com/mitchellh/mapstructure" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// GetResult temporarily stores the result of a Get call. +// Use its Extract() method to interpret it as an Extension. +type GetResult struct { + gophercloud.Result +} + +// Extract interprets a GetResult as an Extension. +func (r GetResult) Extract() (*Extension, error) { + if r.Err != nil { + return nil, r.Err + } + + var res struct { + Extension *Extension `json:"extension"` + } + + err := mapstructure.Decode(r.Body, &res) + + return res.Extension, err +} + +// Extension is a struct that represents an OpenStack extension. +type Extension struct { + Updated string `json:"updated" mapstructure:"updated"` + Name string `json:"name" mapstructure:"name"` + Links []interface{} `json:"links" mapstructure:"links"` + Namespace string `json:"namespace" mapstructure:"namespace"` + Alias string `json:"alias" mapstructure:"alias"` + Description string `json:"description" mapstructure:"description"` +} + +// ExtensionPage is the page returned by a pager when traversing over a collection of extensions. +type ExtensionPage struct { + pagination.SinglePageBase +} + +// IsEmpty checks whether an ExtensionPage struct is empty. +func (r ExtensionPage) IsEmpty() (bool, error) { + is, err := ExtractExtensions(r) + if err != nil { + return true, err + } + return len(is) == 0, nil +} + +// ExtractExtensions accepts a Page struct, specifically an ExtensionPage struct, and extracts the +// elements into a slice of Extension structs. +// In other words, a generic collection is mapped into a relevant slice. +func ExtractExtensions(page pagination.Page) ([]Extension, error) { + var resp struct { + Extensions []Extension `mapstructure:"extensions"` + } + + err := mapstructure.Decode(page.(ExtensionPage).Body, &resp) + + return resp.Extensions, err +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/common/extensions/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/common/extensions/urls.go new file mode 100644 index 0000000000..6460c66bc0 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/common/extensions/urls.go @@ -0,0 +1,13 @@ +package extensions + +import "github.com/rackspace/gophercloud" + +// ExtensionURL generates the URL for an extension resource by name. +func ExtensionURL(c *gophercloud.ServiceClient, name string) string { + return c.ServiceURL("extensions", name) +} + +// ListExtensionURL generates the URL for the extensions resource collection. +func ListExtensionURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("extensions") +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/common/extensions/urls_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/common/extensions/urls_test.go new file mode 100644 index 0000000000..3223b1ca8b --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/common/extensions/urls_test.go @@ -0,0 +1,26 @@ +package extensions + +import ( + "testing" + + "github.com/rackspace/gophercloud" + th "github.com/rackspace/gophercloud/testhelper" +) + +const endpoint = "http://localhost:57909/" + +func endpointClient() *gophercloud.ServiceClient { + return &gophercloud.ServiceClient{Endpoint: endpoint} +} + +func TestExtensionURL(t *testing.T) { + actual := ExtensionURL(endpointClient(), "agent") + expected := endpoint + "extensions/agent" + th.AssertEquals(t, expected, actual) +} + +func TestListExtensionURL(t *testing.T) { + actual := ListExtensionURL(endpointClient()) + expected := endpoint + "extensions" + th.AssertEquals(t, expected, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/bootfromvolume/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/bootfromvolume/requests.go new file mode 100644 index 0000000000..5a976d1146 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/bootfromvolume/requests.go @@ -0,0 +1,111 @@ +package bootfromvolume + +import ( + "errors" + "strconv" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/openstack/compute/v2/servers" + + "github.com/racker/perigee" +) + +// SourceType represents the type of medium being used to create the volume. +type SourceType string + +const ( + Volume SourceType = "volume" + Snapshot SourceType = "snapshot" + Image SourceType = "image" +) + +// BlockDevice is a structure with options for booting a server instance +// from a volume. The volume may be created from an image, snapshot, or another +// volume. +type BlockDevice struct { + // BootIndex [optional] is the boot index. It defaults to 0. + BootIndex int `json:"boot_index"` + + // DeleteOnTermination [optional] specifies whether or not to delete the attached volume + // when the server is deleted. Defaults to `false`. + DeleteOnTermination bool `json:"delete_on_termination"` + + // DestinationType [optional] is the type that gets created. Possible values are "volume" + // and "local". + DestinationType string `json:"destination_type"` + + // SourceType [required] must be one of: "volume", "snapshot", "image". + SourceType SourceType `json:"source_type"` + + // UUID [required] is the unique identifier for the volume, snapshot, or image (see above) + UUID string `json:"uuid"` + + // VolumeSize [optional] is the size of the volume to create (in gigabytes). + VolumeSize int `json:"volume_size"` +} + +// CreateOptsExt is a structure that extends the server `CreateOpts` structure +// by allowing for a block device mapping. +type CreateOptsExt struct { + servers.CreateOptsBuilder + BlockDevice []BlockDevice `json:"block_device_mapping_v2,omitempty"` +} + +// ToServerCreateMap adds the block device mapping option to the base server +// creation options. +func (opts CreateOptsExt) ToServerCreateMap() (map[string]interface{}, error) { + base, err := opts.CreateOptsBuilder.ToServerCreateMap() + if err != nil { + return nil, err + } + + if len(opts.BlockDevice) == 0 { + return nil, errors.New("Required fields UUID and SourceType not set.") + } + + serverMap := base["server"].(map[string]interface{}) + + blockDevice := make([]map[string]interface{}, len(opts.BlockDevice)) + + for i, bd := range opts.BlockDevice { + if string(bd.SourceType) == "" { + return nil, errors.New("SourceType must be one of: volume, image, snapshot.") + } + + blockDevice[i] = make(map[string]interface{}) + + blockDevice[i]["source_type"] = bd.SourceType + blockDevice[i]["boot_index"] = strconv.Itoa(bd.BootIndex) + blockDevice[i]["delete_on_termination"] = strconv.FormatBool(bd.DeleteOnTermination) + blockDevice[i]["volume_size"] = strconv.Itoa(bd.VolumeSize) + if bd.UUID != "" { + blockDevice[i]["uuid"] = bd.UUID + } + if bd.DestinationType != "" { + blockDevice[i]["destination_type"] = bd.DestinationType + } + + } + serverMap["block_device_mapping_v2"] = blockDevice + + return base, nil +} + +// Create requests the creation of a server from the given block device mapping. +func Create(client *gophercloud.ServiceClient, opts servers.CreateOptsBuilder) servers.CreateResult { + var res servers.CreateResult + + reqBody, err := opts.ToServerCreateMap() + if err != nil { + res.Err = err + return res + } + + _, res.Err = perigee.Request("POST", createURL(client), perigee.Options{ + MoreHeaders: client.AuthenticatedHeaders(), + ReqBody: reqBody, + Results: &res.Body, + OkCodes: []int{200, 202}, + }) + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/bootfromvolume/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/bootfromvolume/requests_test.go new file mode 100644 index 0000000000..5bf9137906 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/bootfromvolume/requests_test.go @@ -0,0 +1,51 @@ +package bootfromvolume + +import ( + "testing" + + "github.com/rackspace/gophercloud/openstack/compute/v2/servers" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestCreateOpts(t *testing.T) { + base := servers.CreateOpts{ + Name: "createdserver", + ImageRef: "asdfasdfasdf", + FlavorRef: "performance1-1", + } + + ext := CreateOptsExt{ + CreateOptsBuilder: base, + BlockDevice: []BlockDevice{ + BlockDevice{ + UUID: "123456", + SourceType: Image, + DestinationType: "volume", + VolumeSize: 10, + }, + }, + } + + expected := ` + { + "server": { + "name": "createdserver", + "imageRef": "asdfasdfasdf", + "flavorRef": "performance1-1", + "block_device_mapping_v2":[ + { + "uuid":"123456", + "source_type":"image", + "destination_type":"volume", + "boot_index": "0", + "delete_on_termination": "false", + "volume_size": "10" + } + ] + } + } + ` + actual, err := ext.ToServerCreateMap() + th.AssertNoErr(t, err) + th.CheckJSONEquals(t, expected, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/bootfromvolume/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/bootfromvolume/results.go new file mode 100644 index 0000000000..f60329f0f3 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/bootfromvolume/results.go @@ -0,0 +1,10 @@ +package bootfromvolume + +import ( + os "github.com/rackspace/gophercloud/openstack/compute/v2/servers" +) + +// CreateResult temporarily contains the response from a Create call. +type CreateResult struct { + os.CreateResult +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/bootfromvolume/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/bootfromvolume/urls.go new file mode 100644 index 0000000000..0cffe25ffd --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/bootfromvolume/urls.go @@ -0,0 +1,7 @@ +package bootfromvolume + +import "github.com/rackspace/gophercloud" + +func createURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("os-volumes_boot") +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/bootfromvolume/urls_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/bootfromvolume/urls_test.go new file mode 100644 index 0000000000..6ee647732d --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/bootfromvolume/urls_test.go @@ -0,0 +1,16 @@ +package bootfromvolume + +import ( + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestCreateURL(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + c := client.ServiceClient() + + th.CheckEquals(t, c.Endpoint+"os-volumes_boot", createURL(c)) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/defsecrules/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/defsecrules/doc.go new file mode 100644 index 0000000000..2571a1a5a7 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/defsecrules/doc.go @@ -0,0 +1 @@ +package defsecrules diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/defsecrules/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/defsecrules/fixtures.go new file mode 100644 index 0000000000..c28e492d35 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/defsecrules/fixtures.go @@ -0,0 +1,108 @@ +package defsecrules + +import ( + "fmt" + "net/http" + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +const rootPath = "/os-security-group-default-rules" + +func mockListRulesResponse(t *testing.T) { + th.Mux.HandleFunc(rootPath, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "security_group_default_rules": [ + { + "from_port": 80, + "id": "{ruleID}", + "ip_protocol": "TCP", + "ip_range": { + "cidr": "10.10.10.0/24" + }, + "to_port": 80 + } + ] +} + `) + }) +} + +func mockCreateRuleResponse(t *testing.T) { + th.Mux.HandleFunc(rootPath, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + th.TestJSONRequest(t, r, ` +{ + "security_group_default_rule": { + "ip_protocol": "TCP", + "from_port": 80, + "to_port": 80, + "cidr": "10.10.12.0/24" + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "security_group_default_rule": { + "from_port": 80, + "id": "{ruleID}", + "ip_protocol": "TCP", + "ip_range": { + "cidr": "10.10.12.0/24" + }, + "to_port": 80 + } +} +`) + }) +} + +func mockGetRuleResponse(t *testing.T, ruleID string) { + url := rootPath + "/" + ruleID + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "security_group_default_rule": { + "id": "{ruleID}", + "from_port": 80, + "to_port": 80, + "ip_protocol": "TCP", + "ip_range": { + "cidr": "10.10.12.0/24" + } + } +} + `) + }) +} + +func mockDeleteRuleResponse(t *testing.T, ruleID string) { + url := rootPath + "/" + ruleID + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusNoContent) + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/defsecrules/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/defsecrules/requests.go new file mode 100644 index 0000000000..7d19741e10 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/defsecrules/requests.go @@ -0,0 +1,111 @@ +package defsecrules + +import ( + "errors" + + "github.com/racker/perigee" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// List will return a collection of default rules. +func List(client *gophercloud.ServiceClient) pagination.Pager { + createPage := func(r pagination.PageResult) pagination.Page { + return DefaultRulePage{pagination.SinglePageBase(r)} + } + + return pagination.NewPager(client, rootURL(client), createPage) +} + +// CreateOpts represents the configuration for adding a new default rule. +type CreateOpts struct { + // Required - the lower bound of the port range that will be opened. + FromPort int `json:"from_port"` + + // Required - the upper bound of the port range that will be opened. + ToPort int `json:"to_port"` + + // Required - the protocol type that will be allowed, e.g. TCP. + IPProtocol string `json:"ip_protocol"` + + // ONLY required if FromGroupID is blank. This represents the IP range that + // will be the source of network traffic to your security group. Use + // 0.0.0.0/0 to allow all IP addresses. + CIDR string `json:"cidr,omitempty"` +} + +// CreateOptsBuilder builds the create rule options into a serializable format. +type CreateOptsBuilder interface { + ToRuleCreateMap() (map[string]interface{}, error) +} + +// ToRuleCreateMap builds the create rule options into a serializable format. +func (opts CreateOpts) ToRuleCreateMap() (map[string]interface{}, error) { + rule := make(map[string]interface{}) + + if opts.FromPort == 0 { + return rule, errors.New("A FromPort must be set") + } + if opts.ToPort == 0 { + return rule, errors.New("A ToPort must be set") + } + if opts.IPProtocol == "" { + return rule, errors.New("A IPProtocol must be set") + } + if opts.CIDR == "" { + return rule, errors.New("A CIDR must be set") + } + + rule["from_port"] = opts.FromPort + rule["to_port"] = opts.ToPort + rule["ip_protocol"] = opts.IPProtocol + rule["cidr"] = opts.CIDR + + return map[string]interface{}{"security_group_default_rule": rule}, nil +} + +// Create is the operation responsible for creating a new default rule. +func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult { + var result CreateResult + + reqBody, err := opts.ToRuleCreateMap() + if err != nil { + result.Err = err + return result + } + + _, result.Err = perigee.Request("POST", rootURL(client), perigee.Options{ + Results: &result.Body, + ReqBody: &reqBody, + MoreHeaders: client.AuthenticatedHeaders(), + OkCodes: []int{200}, + }) + + return result +} + +// Get will return details for a particular default rule. +func Get(client *gophercloud.ServiceClient, id string) GetResult { + var result GetResult + + _, result.Err = perigee.Request("GET", resourceURL(client, id), perigee.Options{ + Results: &result.Body, + MoreHeaders: client.AuthenticatedHeaders(), + OkCodes: []int{200}, + }) + + return result +} + +// Delete will permanently delete a default rule from the project. +func Delete(client *gophercloud.ServiceClient, id string) gophercloud.ErrResult { + var result gophercloud.ErrResult + + _, result.Err = perigee.Request("DELETE", resourceURL(client, id), perigee.Options{ + MoreHeaders: client.AuthenticatedHeaders(), + OkCodes: []int{204}, + }) + + return result +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/defsecrules/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/defsecrules/requests_test.go new file mode 100644 index 0000000000..d4ebe87c56 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/defsecrules/requests_test.go @@ -0,0 +1,100 @@ +package defsecrules + +import ( + "testing" + + "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/secgroups" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +const ruleID = "{ruleID}" + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockListRulesResponse(t) + + count := 0 + + err := List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractDefaultRules(page) + th.AssertNoErr(t, err) + + expected := []DefaultRule{ + DefaultRule{ + FromPort: 80, + ID: ruleID, + IPProtocol: "TCP", + IPRange: secgroups.IPRange{CIDR: "10.10.10.0/24"}, + ToPort: 80, + }, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + + th.AssertNoErr(t, err) + th.AssertEquals(t, 1, count) +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockCreateRuleResponse(t) + + opts := CreateOpts{ + IPProtocol: "TCP", + FromPort: 80, + ToPort: 80, + CIDR: "10.10.12.0/24", + } + + group, err := Create(client.ServiceClient(), opts).Extract() + th.AssertNoErr(t, err) + + expected := &DefaultRule{ + ID: ruleID, + FromPort: 80, + ToPort: 80, + IPProtocol: "TCP", + IPRange: secgroups.IPRange{CIDR: "10.10.12.0/24"}, + } + th.AssertDeepEquals(t, expected, group) +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockGetRuleResponse(t, ruleID) + + group, err := Get(client.ServiceClient(), ruleID).Extract() + th.AssertNoErr(t, err) + + expected := &DefaultRule{ + ID: ruleID, + FromPort: 80, + ToPort: 80, + IPProtocol: "TCP", + IPRange: secgroups.IPRange{CIDR: "10.10.12.0/24"}, + } + + th.AssertDeepEquals(t, expected, group) +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockDeleteRuleResponse(t, ruleID) + + err := Delete(client.ServiceClient(), ruleID).ExtractErr() + th.AssertNoErr(t, err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/defsecrules/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/defsecrules/results.go new file mode 100644 index 0000000000..e588d3e327 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/defsecrules/results.go @@ -0,0 +1,69 @@ +package defsecrules + +import ( + "github.com/mitchellh/mapstructure" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/secgroups" + "github.com/rackspace/gophercloud/pagination" +) + +// DefaultRule represents a default rule - which is identical to a +// normal security rule. +type DefaultRule secgroups.Rule + +// DefaultRulePage is a single page of a DefaultRule collection. +type DefaultRulePage struct { + pagination.SinglePageBase +} + +// IsEmpty determines whether or not a page of default rules contains any results. +func (page DefaultRulePage) IsEmpty() (bool, error) { + users, err := ExtractDefaultRules(page) + if err != nil { + return false, err + } + return len(users) == 0, nil +} + +// ExtractDefaultRules returns a slice of DefaultRules contained in a single +// page of results. +func ExtractDefaultRules(page pagination.Page) ([]DefaultRule, error) { + casted := page.(DefaultRulePage).Body + var response struct { + Rules []DefaultRule `mapstructure:"security_group_default_rules"` + } + + err := mapstructure.WeakDecode(casted, &response) + + return response.Rules, err +} + +type commonResult struct { + gophercloud.Result +} + +// CreateResult represents the result of a create operation. +type CreateResult struct { + commonResult +} + +// GetResult represents the result of a get operation. +type GetResult struct { + commonResult +} + +// Extract will extract a DefaultRule struct from most responses. +func (r commonResult) Extract() (*DefaultRule, error) { + if r.Err != nil { + return nil, r.Err + } + + var response struct { + Rule DefaultRule `mapstructure:"security_group_default_rule"` + } + + err := mapstructure.WeakDecode(r.Body, &response) + + return &response.Rule, err +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/defsecrules/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/defsecrules/urls.go new file mode 100644 index 0000000000..cc928ab895 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/defsecrules/urls.go @@ -0,0 +1,13 @@ +package defsecrules + +import "github.com/rackspace/gophercloud" + +const rulepath = "os-security-group-default-rules" + +func resourceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(rulepath, id) +} + +func rootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(rulepath) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/delegate.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/delegate.go new file mode 100644 index 0000000000..10079097b6 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/delegate.go @@ -0,0 +1,23 @@ +package extensions + +import ( + "github.com/rackspace/gophercloud" + common "github.com/rackspace/gophercloud/openstack/common/extensions" + "github.com/rackspace/gophercloud/pagination" +) + +// ExtractExtensions interprets a Page as a slice of Extensions. +func ExtractExtensions(page pagination.Page) ([]common.Extension, error) { + return common.ExtractExtensions(page) +} + +// Get retrieves information for a specific extension using its alias. +func Get(c *gophercloud.ServiceClient, alias string) common.GetResult { + return common.Get(c, alias) +} + +// List returns a Pager which allows you to iterate over the full collection of extensions. +// It does not accept query parameters. +func List(c *gophercloud.ServiceClient) pagination.Pager { + return common.List(c) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/delegate_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/delegate_test.go new file mode 100644 index 0000000000..c3c525fa20 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/delegate_test.go @@ -0,0 +1,96 @@ +package extensions + +import ( + "fmt" + "net/http" + "testing" + + common "github.com/rackspace/gophercloud/openstack/common/extensions" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/extensions", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + + fmt.Fprintf(w, ` +{ + "extensions": [ + { + "updated": "2013-01-20T00:00:00-00:00", + "name": "Neutron Service Type Management", + "links": [], + "namespace": "http://docs.openstack.org/ext/neutron/service-type/api/v1.0", + "alias": "service-type", + "description": "API for retrieving service providers for Neutron advanced services" + } + ] +} + `) + }) + + count := 0 + List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractExtensions(page) + th.AssertNoErr(t, err) + + expected := []common.Extension{ + common.Extension{ + Updated: "2013-01-20T00:00:00-00:00", + Name: "Neutron Service Type Management", + Links: []interface{}{}, + Namespace: "http://docs.openstack.org/ext/neutron/service-type/api/v1.0", + Alias: "service-type", + Description: "API for retrieving service providers for Neutron advanced services", + }, + } + th.AssertDeepEquals(t, expected, actual) + + return true, nil + }) + th.CheckEquals(t, 1, count) +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/extensions/agent", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "extension": { + "updated": "2013-02-03T10:00:00-00:00", + "name": "agent", + "links": [], + "namespace": "http://docs.openstack.org/ext/agent/api/v2.0", + "alias": "agent", + "description": "The agent management extension." + } +} + `) + }) + + ext, err := Get(client.ServiceClient(), "agent").Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, ext.Updated, "2013-02-03T10:00:00-00:00") + th.AssertEquals(t, ext.Name, "agent") + th.AssertEquals(t, ext.Namespace, "http://docs.openstack.org/ext/agent/api/v2.0") + th.AssertEquals(t, ext.Alias, "agent") + th.AssertEquals(t, ext.Description, "The agent management extension.") +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/diskconfig/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/diskconfig/doc.go new file mode 100644 index 0000000000..80785faca9 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/diskconfig/doc.go @@ -0,0 +1,3 @@ +// Package diskconfig provides information and interaction with the Disk +// Config extension that works with the OpenStack Compute service. +package diskconfig diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/diskconfig/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/diskconfig/requests.go new file mode 100644 index 0000000000..7407e0d175 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/diskconfig/requests.go @@ -0,0 +1,114 @@ +package diskconfig + +import ( + "errors" + + "github.com/rackspace/gophercloud/openstack/compute/v2/servers" +) + +// DiskConfig represents one of the two possible settings for the DiskConfig option when creating, +// rebuilding, or resizing servers: Auto or Manual. +type DiskConfig string + +const ( + // Auto builds a server with a single partition the size of the target flavor disk and + // automatically adjusts the filesystem to fit the entire partition. Auto may only be used with + // images and servers that use a single EXT3 partition. + Auto DiskConfig = "AUTO" + + // Manual builds a server using whatever partition scheme and filesystem are present in the source + // image. If the target flavor disk is larger, the remaining space is left unpartitioned. This + // enables images to have non-EXT3 filesystems, multiple partitions, and so on, and enables you + // to manage the disk configuration. It also results in slightly shorter boot times. + Manual DiskConfig = "MANUAL" +) + +// ErrInvalidDiskConfig is returned if an invalid string is specified for a DiskConfig option. +var ErrInvalidDiskConfig = errors.New("DiskConfig must be either diskconfig.Auto or diskconfig.Manual.") + +// Validate ensures that a DiskConfig contains an appropriate value. +func (config DiskConfig) validate() error { + switch config { + case Auto, Manual: + return nil + default: + return ErrInvalidDiskConfig + } +} + +// CreateOptsExt adds a DiskConfig option to the base CreateOpts. +type CreateOptsExt struct { + servers.CreateOptsBuilder + + // DiskConfig [optional] controls how the created server's disk is partitioned. + DiskConfig DiskConfig `json:"OS-DCF:diskConfig,omitempty"` +} + +// ToServerCreateMap adds the diskconfig option to the base server creation options. +func (opts CreateOptsExt) ToServerCreateMap() (map[string]interface{}, error) { + base, err := opts.CreateOptsBuilder.ToServerCreateMap() + if err != nil { + return nil, err + } + + if string(opts.DiskConfig) == "" { + return base, nil + } + + serverMap := base["server"].(map[string]interface{}) + serverMap["OS-DCF:diskConfig"] = string(opts.DiskConfig) + + return base, nil +} + +// RebuildOptsExt adds a DiskConfig option to the base RebuildOpts. +type RebuildOptsExt struct { + servers.RebuildOptsBuilder + + // DiskConfig [optional] controls how the rebuilt server's disk is partitioned. + DiskConfig DiskConfig +} + +// ToServerRebuildMap adds the diskconfig option to the base server rebuild options. +func (opts RebuildOptsExt) ToServerRebuildMap() (map[string]interface{}, error) { + err := opts.DiskConfig.validate() + if err != nil { + return nil, err + } + + base, err := opts.RebuildOptsBuilder.ToServerRebuildMap() + if err != nil { + return nil, err + } + + serverMap := base["rebuild"].(map[string]interface{}) + serverMap["OS-DCF:diskConfig"] = string(opts.DiskConfig) + + return base, nil +} + +// ResizeOptsExt adds a DiskConfig option to the base server resize options. +type ResizeOptsExt struct { + servers.ResizeOptsBuilder + + // DiskConfig [optional] controls how the resized server's disk is partitioned. + DiskConfig DiskConfig +} + +// ToServerResizeMap adds the diskconfig option to the base server creation options. +func (opts ResizeOptsExt) ToServerResizeMap() (map[string]interface{}, error) { + err := opts.DiskConfig.validate() + if err != nil { + return nil, err + } + + base, err := opts.ResizeOptsBuilder.ToServerResizeMap() + if err != nil { + return nil, err + } + + serverMap := base["resize"].(map[string]interface{}) + serverMap["OS-DCF:diskConfig"] = string(opts.DiskConfig) + + return base, nil +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/diskconfig/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/diskconfig/requests_test.go new file mode 100644 index 0000000000..e3c26d49a5 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/diskconfig/requests_test.go @@ -0,0 +1,87 @@ +package diskconfig + +import ( + "testing" + + "github.com/rackspace/gophercloud/openstack/compute/v2/servers" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestCreateOpts(t *testing.T) { + base := servers.CreateOpts{ + Name: "createdserver", + ImageRef: "asdfasdfasdf", + FlavorRef: "performance1-1", + } + + ext := CreateOptsExt{ + CreateOptsBuilder: base, + DiskConfig: Manual, + } + + expected := ` + { + "server": { + "name": "createdserver", + "imageRef": "asdfasdfasdf", + "flavorRef": "performance1-1", + "OS-DCF:diskConfig": "MANUAL" + } + } + ` + actual, err := ext.ToServerCreateMap() + th.AssertNoErr(t, err) + th.CheckJSONEquals(t, expected, actual) +} + +func TestRebuildOpts(t *testing.T) { + base := servers.RebuildOpts{ + Name: "rebuiltserver", + AdminPass: "swordfish", + ImageID: "asdfasdfasdf", + } + + ext := RebuildOptsExt{ + RebuildOptsBuilder: base, + DiskConfig: Auto, + } + + actual, err := ext.ToServerRebuildMap() + th.AssertNoErr(t, err) + + expected := ` + { + "rebuild": { + "name": "rebuiltserver", + "imageRef": "asdfasdfasdf", + "adminPass": "swordfish", + "OS-DCF:diskConfig": "AUTO" + } + } + ` + th.CheckJSONEquals(t, expected, actual) +} + +func TestResizeOpts(t *testing.T) { + base := servers.ResizeOpts{ + FlavorRef: "performance1-8", + } + + ext := ResizeOptsExt{ + ResizeOptsBuilder: base, + DiskConfig: Auto, + } + + actual, err := ext.ToServerResizeMap() + th.AssertNoErr(t, err) + + expected := ` + { + "resize": { + "flavorRef": "performance1-8", + "OS-DCF:diskConfig": "AUTO" + } + } + ` + th.CheckJSONEquals(t, expected, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/diskconfig/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/diskconfig/results.go new file mode 100644 index 0000000000..10ec2dafcb --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/diskconfig/results.go @@ -0,0 +1,60 @@ +package diskconfig + +import ( + "github.com/mitchellh/mapstructure" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/openstack/compute/v2/servers" + "github.com/rackspace/gophercloud/pagination" +) + +func commonExtract(result gophercloud.Result) (*DiskConfig, error) { + var resp struct { + Server struct { + DiskConfig string `mapstructure:"OS-DCF:diskConfig"` + } `mapstructure:"server"` + } + + err := mapstructure.Decode(result.Body, &resp) + if err != nil { + return nil, err + } + + config := DiskConfig(resp.Server.DiskConfig) + return &config, nil +} + +// ExtractGet returns the disk configuration from a servers.Get call. +func ExtractGet(result servers.GetResult) (*DiskConfig, error) { + return commonExtract(result.Result) +} + +// ExtractUpdate returns the disk configuration from a servers.Update call. +func ExtractUpdate(result servers.UpdateResult) (*DiskConfig, error) { + return commonExtract(result.Result) +} + +// ExtractRebuild returns the disk configuration from a servers.Rebuild call. +func ExtractRebuild(result servers.RebuildResult) (*DiskConfig, error) { + return commonExtract(result.Result) +} + +// ExtractDiskConfig returns the DiskConfig setting for a specific server acquired from an +// servers.ExtractServers call, while iterating through a Pager. +func ExtractDiskConfig(page pagination.Page, index int) (*DiskConfig, error) { + casted := page.(servers.ServerPage).Body + + type server struct { + DiskConfig string `mapstructure:"OS-DCF:diskConfig"` + } + var response struct { + Servers []server `mapstructure:"servers"` + } + + err := mapstructure.Decode(casted, &response) + if err != nil { + return nil, err + } + + config := DiskConfig(response.Servers[index].DiskConfig) + return &config, nil +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/diskconfig/results_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/diskconfig/results_test.go new file mode 100644 index 0000000000..dd8d2b7dfa --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/diskconfig/results_test.go @@ -0,0 +1,68 @@ +package diskconfig + +import ( + "testing" + + "github.com/rackspace/gophercloud/openstack/compute/v2/servers" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestExtractGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + servers.HandleServerGetSuccessfully(t) + + config, err := ExtractGet(servers.Get(client.ServiceClient(), "1234asdf")) + th.AssertNoErr(t, err) + th.CheckEquals(t, Manual, *config) +} + +func TestExtractUpdate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + servers.HandleServerUpdateSuccessfully(t) + + r := servers.Update(client.ServiceClient(), "1234asdf", servers.UpdateOpts{ + Name: "new-name", + }) + config, err := ExtractUpdate(r) + th.AssertNoErr(t, err) + th.CheckEquals(t, Manual, *config) +} + +func TestExtractRebuild(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + servers.HandleRebuildSuccessfully(t, servers.SingleServerBody) + + r := servers.Rebuild(client.ServiceClient(), "1234asdf", servers.RebuildOpts{ + Name: "new-name", + AdminPass: "swordfish", + ImageID: "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/images/f90f6034-2570-4974-8351-6b49732ef2eb", + AccessIPv4: "1.2.3.4", + }) + config, err := ExtractRebuild(r) + th.AssertNoErr(t, err) + th.CheckEquals(t, Manual, *config) +} + +func TestExtractList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + servers.HandleServerListSuccessfully(t) + + pages := 0 + err := servers.List(client.ServiceClient(), nil).EachPage(func(page pagination.Page) (bool, error) { + pages++ + + config, err := ExtractDiskConfig(page, 0) + th.AssertNoErr(t, err) + th.CheckEquals(t, Manual, *config) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, pages, 1) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/doc.go new file mode 100644 index 0000000000..2b447da1d6 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/doc.go @@ -0,0 +1,3 @@ +// Package extensions provides information and interaction with the +// different extensions available for the OpenStack Compute service. +package extensions diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs/doc.go new file mode 100644 index 0000000000..856f41bacc --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs/doc.go @@ -0,0 +1,3 @@ +// Package keypairs provides information and interaction with the Keypairs +// extension for the OpenStack Compute service. +package keypairs diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs/fixtures.go new file mode 100644 index 0000000000..d10af99d0e --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs/fixtures.go @@ -0,0 +1,171 @@ +// +build fixtures + +package keypairs + +import ( + "fmt" + "net/http" + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +// ListOutput is a sample response to a List call. +const ListOutput = ` +{ + "keypairs": [ + { + "keypair": { + "fingerprint": "15:b0:f8:b3:f9:48:63:71:cf:7b:5b:38:6d:44:2d:4a", + "name": "firstkey", + "public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC+Eo/RZRngaGTkFs7I62ZjsIlO79KklKbMXi8F+KITD4bVQHHn+kV+4gRgkgCRbdoDqoGfpaDFs877DYX9n4z6FrAIZ4PES8TNKhatifpn9NdQYWA+IkU8CuvlEKGuFpKRi/k7JLos/gHi2hy7QUwgtRvcefvD/vgQZOVw/mGR9Q== Generated by Nova\n" + } + }, + { + "keypair": { + "fingerprint": "35:9d:d0:c3:4a:80:d3:d8:86:f1:ca:f7:df:c4:f9:d8", + "name": "secondkey", + "public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC9mC3WZN9UGLxgPBpP7H5jZMc6pKwOoSgre8yun6REFktn/Kz7DUt9jaR1UJyRzHxITfCfAIgSxPdGqB/oF1suMyWgu5i0625vavLB5z5kC8Hq3qZJ9zJO1poE1kyD+htiTtPWJ88e12xuH2XB/CZN9OpEiF98hAagiOE0EnOS5Q== Generated by Nova\n" + } + } + ] +} +` + +// GetOutput is a sample response to a Get call. +const GetOutput = ` +{ + "keypair": { + "public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC+Eo/RZRngaGTkFs7I62ZjsIlO79KklKbMXi8F+KITD4bVQHHn+kV+4gRgkgCRbdoDqoGfpaDFs877DYX9n4z6FrAIZ4PES8TNKhatifpn9NdQYWA+IkU8CuvlEKGuFpKRi/k7JLos/gHi2hy7QUwgtRvcefvD/vgQZOVw/mGR9Q== Generated by Nova\n", + "name": "firstkey", + "fingerprint": "15:b0:f8:b3:f9:48:63:71:cf:7b:5b:38:6d:44:2d:4a" + } +} +` + +// CreateOutput is a sample response to a Create call. +const CreateOutput = ` +{ + "keypair": { + "fingerprint": "35:9d:d0:c3:4a:80:d3:d8:86:f1:ca:f7:df:c4:f9:d8", + "name": "createdkey", + "private_key": "-----BEGIN RSA PRIVATE KEY-----\nMIICXAIBAAKBgQC9mC3WZN9UGLxgPBpP7H5jZMc6pKwOoSgre8yun6REFktn/Kz7\nDUt9jaR1UJyRzHxITfCfAIgSxPdGqB/oF1suMyWgu5i0625vavLB5z5kC8Hq3qZJ\n9zJO1poE1kyD+htiTtPWJ88e12xuH2XB/CZN9OpEiF98hAagiOE0EnOS5QIDAQAB\nAoGAE5XO1mDhORy9COvsg+kYPUhB1GsCYxh+v88wG7HeFDKBY6KUc/Kxo6yoGn5T\nTjRjekyi2KoDZHz4VlIzyZPwFS4I1bf3oCunVoAKzgLdmnTtvRNMC5jFOGc2vUgP\n9bSyRj3S1R4ClVk2g0IDeagko/jc8zzLEYuIK+fbkds79YECQQDt3vcevgegnkga\ntF4NsDmmBPRkcSHCqrANP/7vFcBQN3czxeYYWX3DK07alu6GhH1Y4sHbdm616uU0\nll7xbDzxAkEAzAtN2IyftNygV2EGiaGgqLyo/tD9+Vui2qCQplqe4jvWh/5Sparl\nOjmKo+uAW+hLrLVMnHzRWxbWU8hirH5FNQJATO+ZxCK4etXXAnQmG41NCAqANWB2\nB+2HJbH2NcQ2QHvAHUm741JGn/KI/aBlo7KEjFRDWUVUB5ji64BbUwCsMQJBAIku\nLGcjnBf/oLk+XSPZC2eGd2Ph5G5qYmH0Q2vkTx+wtTn3DV+eNsDfgMtWAJVJ5t61\ngU1QSXyhLPVlKpnnxuUCQC+xvvWjWtsLaFtAsZywJiqLxQzHts8XLGZptYJ5tLWV\nrtmYtBcJCN48RrgQHry/xWYeA4K/AFQpXfNPgprQ96Q=\n-----END RSA PRIVATE KEY-----\n", + "public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC9mC3WZN9UGLxgPBpP7H5jZMc6pKwOoSgre8yun6REFktn/Kz7DUt9jaR1UJyRzHxITfCfAIgSxPdGqB/oF1suMyWgu5i0625vavLB5z5kC8Hq3qZJ9zJO1poE1kyD+htiTtPWJ88e12xuH2XB/CZN9OpEiF98hAagiOE0EnOS5Q== Generated by Nova\n", + "user_id": "fake" + } +} +` + +// ImportOutput is a sample response to a Create call that provides its own public key. +const ImportOutput = ` +{ + "keypair": { + "fingerprint": "1e:2c:9b:56:79:4b:45:77:f9:ca:7a:98:2c:b0:d5:3c", + "name": "importedkey", + "public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDx8nkQv/zgGgB4rMYmIf+6A4l6Rr+o/6lHBQdW5aYd44bd8JttDCE/F/pNRr0lRE+PiqSPO8nDPHw0010JeMH9gYgnnFlyY3/OcJ02RhIPyyxYpv9FhY+2YiUkpwFOcLImyrxEsYXpD/0d3ac30bNH6Sw9JD9UZHYcpSxsIbECHw== Generated by Nova", + "user_id": "fake" + } +} +` + +// FirstKeyPair is the first result in ListOutput. +var FirstKeyPair = KeyPair{ + Name: "firstkey", + Fingerprint: "15:b0:f8:b3:f9:48:63:71:cf:7b:5b:38:6d:44:2d:4a", + PublicKey: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC+Eo/RZRngaGTkFs7I62ZjsIlO79KklKbMXi8F+KITD4bVQHHn+kV+4gRgkgCRbdoDqoGfpaDFs877DYX9n4z6FrAIZ4PES8TNKhatifpn9NdQYWA+IkU8CuvlEKGuFpKRi/k7JLos/gHi2hy7QUwgtRvcefvD/vgQZOVw/mGR9Q== Generated by Nova\n", +} + +// SecondKeyPair is the second result in ListOutput. +var SecondKeyPair = KeyPair{ + Name: "secondkey", + Fingerprint: "35:9d:d0:c3:4a:80:d3:d8:86:f1:ca:f7:df:c4:f9:d8", + PublicKey: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC9mC3WZN9UGLxgPBpP7H5jZMc6pKwOoSgre8yun6REFktn/Kz7DUt9jaR1UJyRzHxITfCfAIgSxPdGqB/oF1suMyWgu5i0625vavLB5z5kC8Hq3qZJ9zJO1poE1kyD+htiTtPWJ88e12xuH2XB/CZN9OpEiF98hAagiOE0EnOS5Q== Generated by Nova\n", +} + +// ExpectedKeyPairSlice is the slice of results that should be parsed from ListOutput, in the expected +// order. +var ExpectedKeyPairSlice = []KeyPair{FirstKeyPair, SecondKeyPair} + +// CreatedKeyPair is the parsed result from CreatedOutput. +var CreatedKeyPair = KeyPair{ + Name: "createdkey", + Fingerprint: "35:9d:d0:c3:4a:80:d3:d8:86:f1:ca:f7:df:c4:f9:d8", + PublicKey: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC9mC3WZN9UGLxgPBpP7H5jZMc6pKwOoSgre8yun6REFktn/Kz7DUt9jaR1UJyRzHxITfCfAIgSxPdGqB/oF1suMyWgu5i0625vavLB5z5kC8Hq3qZJ9zJO1poE1kyD+htiTtPWJ88e12xuH2XB/CZN9OpEiF98hAagiOE0EnOS5Q== Generated by Nova\n", + PrivateKey: "-----BEGIN RSA PRIVATE KEY-----\nMIICXAIBAAKBgQC9mC3WZN9UGLxgPBpP7H5jZMc6pKwOoSgre8yun6REFktn/Kz7\nDUt9jaR1UJyRzHxITfCfAIgSxPdGqB/oF1suMyWgu5i0625vavLB5z5kC8Hq3qZJ\n9zJO1poE1kyD+htiTtPWJ88e12xuH2XB/CZN9OpEiF98hAagiOE0EnOS5QIDAQAB\nAoGAE5XO1mDhORy9COvsg+kYPUhB1GsCYxh+v88wG7HeFDKBY6KUc/Kxo6yoGn5T\nTjRjekyi2KoDZHz4VlIzyZPwFS4I1bf3oCunVoAKzgLdmnTtvRNMC5jFOGc2vUgP\n9bSyRj3S1R4ClVk2g0IDeagko/jc8zzLEYuIK+fbkds79YECQQDt3vcevgegnkga\ntF4NsDmmBPRkcSHCqrANP/7vFcBQN3czxeYYWX3DK07alu6GhH1Y4sHbdm616uU0\nll7xbDzxAkEAzAtN2IyftNygV2EGiaGgqLyo/tD9+Vui2qCQplqe4jvWh/5Sparl\nOjmKo+uAW+hLrLVMnHzRWxbWU8hirH5FNQJATO+ZxCK4etXXAnQmG41NCAqANWB2\nB+2HJbH2NcQ2QHvAHUm741JGn/KI/aBlo7KEjFRDWUVUB5ji64BbUwCsMQJBAIku\nLGcjnBf/oLk+XSPZC2eGd2Ph5G5qYmH0Q2vkTx+wtTn3DV+eNsDfgMtWAJVJ5t61\ngU1QSXyhLPVlKpnnxuUCQC+xvvWjWtsLaFtAsZywJiqLxQzHts8XLGZptYJ5tLWV\nrtmYtBcJCN48RrgQHry/xWYeA4K/AFQpXfNPgprQ96Q=\n-----END RSA PRIVATE KEY-----\n", + UserID: "fake", +} + +// ImportedKeyPair is the parsed result from ImportOutput. +var ImportedKeyPair = KeyPair{ + Name: "importedkey", + Fingerprint: "1e:2c:9b:56:79:4b:45:77:f9:ca:7a:98:2c:b0:d5:3c", + PublicKey: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDx8nkQv/zgGgB4rMYmIf+6A4l6Rr+o/6lHBQdW5aYd44bd8JttDCE/F/pNRr0lRE+PiqSPO8nDPHw0010JeMH9gYgnnFlyY3/OcJ02RhIPyyxYpv9FhY+2YiUkpwFOcLImyrxEsYXpD/0d3ac30bNH6Sw9JD9UZHYcpSxsIbECHw== Generated by Nova", + UserID: "fake", +} + +// HandleListSuccessfully configures the test server to respond to a List request. +func HandleListSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/os-keypairs", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, ListOutput) + }) +} + +// HandleGetSuccessfully configures the test server to respond to a Get request for "firstkey". +func HandleGetSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/os-keypairs/firstkey", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, GetOutput) + }) +} + +// HandleCreateSuccessfully configures the test server to respond to a Create request for a new +// keypair called "createdkey". +func HandleCreateSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/os-keypairs", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, `{ "keypair": { "name": "createdkey" } }`) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, CreateOutput) + }) +} + +// HandleImportSuccessfully configures the test server to respond to an Import request for an +// existing keypair called "importedkey". +func HandleImportSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/os-keypairs", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, ` + { + "keypair": { + "name": "importedkey", + "public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDx8nkQv/zgGgB4rMYmIf+6A4l6Rr+o/6lHBQdW5aYd44bd8JttDCE/F/pNRr0lRE+PiqSPO8nDPHw0010JeMH9gYgnnFlyY3/OcJ02RhIPyyxYpv9FhY+2YiUkpwFOcLImyrxEsYXpD/0d3ac30bNH6Sw9JD9UZHYcpSxsIbECHw== Generated by Nova" + } + } + `) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, ImportOutput) + }) +} + +// HandleDeleteSuccessfully configures the test server to respond to a Delete request for a +// keypair called "deletedkey". +func HandleDeleteSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/os-keypairs/deletedkey", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.WriteHeader(http.StatusAccepted) + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs/requests.go new file mode 100644 index 0000000000..7b372a355f --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs/requests.go @@ -0,0 +1,113 @@ +package keypairs + +import ( + "errors" + + "github.com/racker/perigee" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/openstack/compute/v2/servers" + "github.com/rackspace/gophercloud/pagination" +) + +// CreateOptsExt adds a KeyPair option to the base CreateOpts. +type CreateOptsExt struct { + servers.CreateOptsBuilder + KeyName string `json:"key_name,omitempty"` +} + +// ToServerCreateMap adds the key_name and, optionally, key_data options to +// the base server creation options. +func (opts CreateOptsExt) ToServerCreateMap() (map[string]interface{}, error) { + base, err := opts.CreateOptsBuilder.ToServerCreateMap() + if err != nil { + return nil, err + } + + if opts.KeyName == "" { + return base, nil + } + + serverMap := base["server"].(map[string]interface{}) + serverMap["key_name"] = opts.KeyName + + return base, nil +} + +// List returns a Pager that allows you to iterate over a collection of KeyPairs. +func List(client *gophercloud.ServiceClient) pagination.Pager { + return pagination.NewPager(client, listURL(client), func(r pagination.PageResult) pagination.Page { + return KeyPairPage{pagination.SinglePageBase(r)} + }) +} + +// CreateOptsBuilder describes struct types that can be accepted by the Create call. Notable, the +// CreateOpts struct in this package does. +type CreateOptsBuilder interface { + ToKeyPairCreateMap() (map[string]interface{}, error) +} + +// CreateOpts specifies keypair creation or import parameters. +type CreateOpts struct { + // Name [required] is a friendly name to refer to this KeyPair in other services. + Name string + + // PublicKey [optional] is a pregenerated OpenSSH-formatted public key. If provided, this key + // will be imported and no new key will be created. + PublicKey string +} + +// ToKeyPairCreateMap constructs a request body from CreateOpts. +func (opts CreateOpts) ToKeyPairCreateMap() (map[string]interface{}, error) { + if opts.Name == "" { + return nil, errors.New("Missing field required for keypair creation: Name") + } + + keypair := make(map[string]interface{}) + keypair["name"] = opts.Name + if opts.PublicKey != "" { + keypair["public_key"] = opts.PublicKey + } + + return map[string]interface{}{"keypair": keypair}, nil +} + +// Create requests the creation of a new keypair on the server, or to import a pre-existing +// keypair. +func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult { + var res CreateResult + + reqBody, err := opts.ToKeyPairCreateMap() + if err != nil { + res.Err = err + return res + } + + _, res.Err = perigee.Request("POST", createURL(client), perigee.Options{ + MoreHeaders: client.AuthenticatedHeaders(), + ReqBody: reqBody, + Results: &res.Body, + OkCodes: []int{200}, + }) + return res +} + +// Get returns public data about a previously uploaded KeyPair. +func Get(client *gophercloud.ServiceClient, name string) GetResult { + var res GetResult + _, res.Err = perigee.Request("GET", getURL(client, name), perigee.Options{ + MoreHeaders: client.AuthenticatedHeaders(), + Results: &res.Body, + OkCodes: []int{200}, + }) + return res +} + +// Delete requests the deletion of a previous stored KeyPair from the server. +func Delete(client *gophercloud.ServiceClient, name string) DeleteResult { + var res DeleteResult + _, res.Err = perigee.Request("DELETE", deleteURL(client, name), perigee.Options{ + MoreHeaders: client.AuthenticatedHeaders(), + OkCodes: []int{202}, + }) + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs/requests_test.go new file mode 100644 index 0000000000..67d1833f57 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs/requests_test.go @@ -0,0 +1,71 @@ +package keypairs + +import ( + "testing" + + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListSuccessfully(t) + + count := 0 + err := List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractKeyPairs(page) + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, ExpectedKeyPairSlice, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, 1, count) +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleCreateSuccessfully(t) + + actual, err := Create(client.ServiceClient(), CreateOpts{ + Name: "createdkey", + }).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &CreatedKeyPair, actual) +} + +func TestImport(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleImportSuccessfully(t) + + actual, err := Create(client.ServiceClient(), CreateOpts{ + Name: "importedkey", + PublicKey: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDx8nkQv/zgGgB4rMYmIf+6A4l6Rr+o/6lHBQdW5aYd44bd8JttDCE/F/pNRr0lRE+PiqSPO8nDPHw0010JeMH9gYgnnFlyY3/OcJ02RhIPyyxYpv9FhY+2YiUkpwFOcLImyrxEsYXpD/0d3ac30bNH6Sw9JD9UZHYcpSxsIbECHw== Generated by Nova", + }).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &ImportedKeyPair, actual) +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleGetSuccessfully(t) + + actual, err := Get(client.ServiceClient(), "firstkey").Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &FirstKeyPair, actual) +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleDeleteSuccessfully(t) + + err := Delete(client.ServiceClient(), "deletedkey").ExtractErr() + th.AssertNoErr(t, err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs/results.go new file mode 100644 index 0000000000..f1a0d8e114 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs/results.go @@ -0,0 +1,94 @@ +package keypairs + +import ( + "github.com/mitchellh/mapstructure" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// KeyPair is an SSH key known to the OpenStack cluster that is available to be injected into +// servers. +type KeyPair struct { + // Name is used to refer to this keypair from other services within this region. + Name string `mapstructure:"name"` + + // Fingerprint is a short sequence of bytes that can be used to authenticate or validate a longer + // public key. + Fingerprint string `mapstructure:"fingerprint"` + + // PublicKey is the public key from this pair, in OpenSSH format. "ssh-rsa AAAAB3Nz..." + PublicKey string `mapstructure:"public_key"` + + // PrivateKey is the private key from this pair, in PEM format. + // "-----BEGIN RSA PRIVATE KEY-----\nMIICXA..." It is only present if this keypair was just + // returned from a Create call + PrivateKey string `mapstructure:"private_key"` + + // UserID is the user who owns this keypair. + UserID string `mapstructure:"user_id"` +} + +// KeyPairPage stores a single, only page of KeyPair results from a List call. +type KeyPairPage struct { + pagination.SinglePageBase +} + +// IsEmpty determines whether or not a KeyPairPage is empty. +func (page KeyPairPage) IsEmpty() (bool, error) { + ks, err := ExtractKeyPairs(page) + return len(ks) == 0, err +} + +// ExtractKeyPairs interprets a page of results as a slice of KeyPairs. +func ExtractKeyPairs(page pagination.Page) ([]KeyPair, error) { + type pair struct { + KeyPair KeyPair `mapstructure:"keypair"` + } + + var resp struct { + KeyPairs []pair `mapstructure:"keypairs"` + } + + err := mapstructure.Decode(page.(KeyPairPage).Body, &resp) + results := make([]KeyPair, len(resp.KeyPairs)) + for i, pair := range resp.KeyPairs { + results[i] = pair.KeyPair + } + return results, err +} + +type keyPairResult struct { + gophercloud.Result +} + +// Extract is a method that attempts to interpret any KeyPair resource response as a KeyPair struct. +func (r keyPairResult) Extract() (*KeyPair, error) { + if r.Err != nil { + return nil, r.Err + } + + var res struct { + KeyPair *KeyPair `json:"keypair" mapstructure:"keypair"` + } + + err := mapstructure.Decode(r.Body, &res) + return res.KeyPair, err +} + +// CreateResult is the response from a Create operation. Call its Extract method to interpret it +// as a KeyPair. +type CreateResult struct { + keyPairResult +} + +// GetResult is the response from a Get operation. Call its Extract method to interpret it +// as a KeyPair. +type GetResult struct { + keyPairResult +} + +// DeleteResult is the response from a Delete operation. Call its Extract method to determine if +// the call succeeded or failed. +type DeleteResult struct { + gophercloud.ErrResult +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs/urls.go new file mode 100644 index 0000000000..702f5329e0 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs/urls.go @@ -0,0 +1,25 @@ +package keypairs + +import "github.com/rackspace/gophercloud" + +const resourcePath = "os-keypairs" + +func resourceURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(resourcePath) +} + +func listURL(c *gophercloud.ServiceClient) string { + return resourceURL(c) +} + +func createURL(c *gophercloud.ServiceClient) string { + return resourceURL(c) +} + +func getURL(c *gophercloud.ServiceClient, name string) string { + return c.ServiceURL(resourcePath, name) +} + +func deleteURL(c *gophercloud.ServiceClient, name string) string { + return getURL(c, name) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs/urls_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs/urls_test.go new file mode 100644 index 0000000000..60efd2a5d3 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs/urls_test.go @@ -0,0 +1,40 @@ +package keypairs + +import ( + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestListURL(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + c := client.ServiceClient() + + th.CheckEquals(t, c.Endpoint+"os-keypairs", listURL(c)) +} + +func TestCreateURL(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + c := client.ServiceClient() + + th.CheckEquals(t, c.Endpoint+"os-keypairs", createURL(c)) +} + +func TestGetURL(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + c := client.ServiceClient() + + th.CheckEquals(t, c.Endpoint+"os-keypairs/wat", getURL(c, "wat")) +} + +func TestDeleteURL(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + c := client.ServiceClient() + + th.CheckEquals(t, c.Endpoint+"os-keypairs/wat", deleteURL(c, "wat")) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/secgroups/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/secgroups/doc.go new file mode 100644 index 0000000000..702f32c985 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/secgroups/doc.go @@ -0,0 +1 @@ +package secgroups diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/secgroups/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/secgroups/fixtures.go new file mode 100644 index 0000000000..ca76f686b8 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/secgroups/fixtures.go @@ -0,0 +1,265 @@ +package secgroups + +import ( + "fmt" + "net/http" + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +const rootPath = "/os-security-groups" + +const listGroupsJSON = ` +{ + "security_groups": [ + { + "description": "default", + "id": "{groupID}", + "name": "default", + "rules": [], + "tenant_id": "openstack" + } + ] +} +` + +func mockListGroupsResponse(t *testing.T) { + th.Mux.HandleFunc(rootPath, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, listGroupsJSON) + }) +} + +func mockListGroupsByServerResponse(t *testing.T, serverID string) { + url := fmt.Sprintf("%s/servers/%s%s", rootPath, serverID, rootPath) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, listGroupsJSON) + }) +} + +func mockCreateGroupResponse(t *testing.T) { + th.Mux.HandleFunc(rootPath, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + th.TestJSONRequest(t, r, ` +{ + "security_group": { + "name": "test", + "description": "something" + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "security_group": { + "description": "something", + "id": "{groupID}", + "name": "test", + "rules": [], + "tenant_id": "openstack" + } +} +`) + }) +} + +func mockUpdateGroupResponse(t *testing.T, groupID string) { + url := fmt.Sprintf("%s/%s", rootPath, groupID) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + th.TestJSONRequest(t, r, ` +{ + "security_group": { + "name": "new_name", + "description": "new_desc" + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "security_group": { + "description": "something", + "id": "{groupID}", + "name": "new_name", + "rules": [], + "tenant_id": "openstack" + } +} +`) + }) +} + +func mockGetGroupsResponse(t *testing.T, groupID string) { + url := fmt.Sprintf("%s/%s", rootPath, groupID) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "security_group": { + "description": "default", + "id": "{groupID}", + "name": "default", + "rules": [ + { + "from_port": 80, + "group": { + "tenant_id": "openstack", + "name": "default" + }, + "ip_protocol": "TCP", + "to_port": 85, + "parent_group_id": "{groupID}", + "ip_range": { + "cidr": "0.0.0.0" + }, + "id": "{ruleID}" + } + ], + "tenant_id": "openstack" + } +} + `) + }) +} + +func mockGetNumericIDGroupResponse(t *testing.T, groupID int) { + url := fmt.Sprintf("%s/%d", rootPath, groupID) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "security_group": { + "id": 12345 + } +} + `) + }) +} + +func mockDeleteGroupResponse(t *testing.T, groupID string) { + url := fmt.Sprintf("%s/%s", rootPath, groupID) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusAccepted) + }) +} + +func mockAddRuleResponse(t *testing.T) { + th.Mux.HandleFunc("/os-security-group-rules", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + th.TestJSONRequest(t, r, ` +{ + "security_group_rule": { + "from_port": 22, + "ip_protocol": "TCP", + "to_port": 22, + "parent_group_id": "{groupID}", + "cidr": "0.0.0.0/0" + } +} `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "security_group_rule": { + "from_port": 22, + "group": {}, + "ip_protocol": "TCP", + "to_port": 22, + "parent_group_id": "{groupID}", + "ip_range": { + "cidr": "0.0.0.0/0" + }, + "id": "{ruleID}" + } +}`) + }) +} + +func mockDeleteRuleResponse(t *testing.T, ruleID string) { + url := fmt.Sprintf("/os-security-group-rules/%s", ruleID) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusAccepted) + }) +} + +func mockAddServerToGroupResponse(t *testing.T, serverID string) { + url := fmt.Sprintf("/servers/%s/action", serverID) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + th.TestJSONRequest(t, r, ` +{ + "addSecurityGroup": { + "name": "test" + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusAccepted) + }) +} + +func mockRemoveServerFromGroupResponse(t *testing.T, serverID string) { + url := fmt.Sprintf("/servers/%s/action", serverID) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + th.TestJSONRequest(t, r, ` +{ + "removeSecurityGroup": { + "name": "test" + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusAccepted) + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/secgroups/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/secgroups/requests.go new file mode 100644 index 0000000000..09503d715e --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/secgroups/requests.go @@ -0,0 +1,298 @@ +package secgroups + +import ( + "errors" + + "github.com/racker/perigee" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +func commonList(client *gophercloud.ServiceClient, url string) pagination.Pager { + createPage := func(r pagination.PageResult) pagination.Page { + return SecurityGroupPage{pagination.SinglePageBase(r)} + } + + return pagination.NewPager(client, url, createPage) +} + +// List will return a collection of all the security groups for a particular +// tenant. +func List(client *gophercloud.ServiceClient) pagination.Pager { + return commonList(client, rootURL(client)) +} + +// ListByServer will return a collection of all the security groups which are +// associated with a particular server. +func ListByServer(client *gophercloud.ServiceClient, serverID string) pagination.Pager { + return commonList(client, listByServerURL(client, serverID)) +} + +// GroupOpts is the underlying struct responsible for creating or updating +// security groups. It therefore represents the mutable attributes of a +// security group. +type GroupOpts struct { + // Required - the name of your security group. + Name string `json:"name"` + + // Required - the description of your security group. + Description string `json:"description"` +} + +// CreateOpts is the struct responsible for creating a security group. +type CreateOpts GroupOpts + +// CreateOptsBuilder builds the create options into a serializable format. +type CreateOptsBuilder interface { + ToSecGroupCreateMap() (map[string]interface{}, error) +} + +var ( + errName = errors.New("Name is a required field") + errDesc = errors.New("Description is a required field") +) + +// ToSecGroupCreateMap builds the create options into a serializable format. +func (opts CreateOpts) ToSecGroupCreateMap() (map[string]interface{}, error) { + sg := make(map[string]interface{}) + + if opts.Name == "" { + return sg, errName + } + if opts.Description == "" { + return sg, errDesc + } + + sg["name"] = opts.Name + sg["description"] = opts.Description + + return map[string]interface{}{"security_group": sg}, nil +} + +// Create will create a new security group. +func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult { + var result CreateResult + + reqBody, err := opts.ToSecGroupCreateMap() + if err != nil { + result.Err = err + return result + } + + _, result.Err = perigee.Request("POST", rootURL(client), perigee.Options{ + Results: &result.Body, + ReqBody: &reqBody, + MoreHeaders: client.AuthenticatedHeaders(), + OkCodes: []int{200}, + }) + + return result +} + +// UpdateOpts is the struct responsible for updating an existing security group. +type UpdateOpts GroupOpts + +// UpdateOptsBuilder builds the update options into a serializable format. +type UpdateOptsBuilder interface { + ToSecGroupUpdateMap() (map[string]interface{}, error) +} + +// ToSecGroupUpdateMap builds the update options into a serializable format. +func (opts UpdateOpts) ToSecGroupUpdateMap() (map[string]interface{}, error) { + sg := make(map[string]interface{}) + + if opts.Name == "" { + return sg, errName + } + if opts.Description == "" { + return sg, errDesc + } + + sg["name"] = opts.Name + sg["description"] = opts.Description + + return map[string]interface{}{"security_group": sg}, nil +} + +// Update will modify the mutable properties of a security group, notably its +// name and description. +func Update(client *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) UpdateResult { + var result UpdateResult + + reqBody, err := opts.ToSecGroupUpdateMap() + if err != nil { + result.Err = err + return result + } + + _, result.Err = perigee.Request("PUT", resourceURL(client, id), perigee.Options{ + Results: &result.Body, + ReqBody: &reqBody, + MoreHeaders: client.AuthenticatedHeaders(), + OkCodes: []int{200}, + }) + + return result +} + +// Get will return details for a particular security group. +func Get(client *gophercloud.ServiceClient, id string) GetResult { + var result GetResult + + _, result.Err = perigee.Request("GET", resourceURL(client, id), perigee.Options{ + Results: &result.Body, + MoreHeaders: client.AuthenticatedHeaders(), + OkCodes: []int{200}, + }) + + return result +} + +// Delete will permanently delete a security group from the project. +func Delete(client *gophercloud.ServiceClient, id string) gophercloud.ErrResult { + var result gophercloud.ErrResult + + _, result.Err = perigee.Request("DELETE", resourceURL(client, id), perigee.Options{ + MoreHeaders: client.AuthenticatedHeaders(), + OkCodes: []int{202}, + }) + + return result +} + +// CreateRuleOpts represents the configuration for adding a new rule to an +// existing security group. +type CreateRuleOpts struct { + // Required - the ID of the group that this rule will be added to. + ParentGroupID string `json:"parent_group_id"` + + // Required - the lower bound of the port range that will be opened. + FromPort int `json:"from_port"` + + // Required - the upper bound of the port range that will be opened. + ToPort int `json:"to_port"` + + // Required - the protocol type that will be allowed, e.g. TCP. + IPProtocol string `json:"ip_protocol"` + + // ONLY required if FromGroupID is blank. This represents the IP range that + // will be the source of network traffic to your security group. Use + // 0.0.0.0/0 to allow all IP addresses. + CIDR string `json:"cidr,omitempty"` + + // ONLY required if CIDR is blank. This value represents the ID of a group + // that forwards traffic to the parent group. So, instead of accepting + // network traffic from an entire IP range, you can instead refine the + // inbound source by an existing security group. + FromGroupID string `json:"group_id,omitempty"` +} + +// CreateRuleOptsBuilder builds the create rule options into a serializable format. +type CreateRuleOptsBuilder interface { + ToRuleCreateMap() (map[string]interface{}, error) +} + +// ToRuleCreateMap builds the create rule options into a serializable format. +func (opts CreateRuleOpts) ToRuleCreateMap() (map[string]interface{}, error) { + rule := make(map[string]interface{}) + + if opts.ParentGroupID == "" { + return rule, errors.New("A ParentGroupID must be set") + } + if opts.FromPort == 0 { + return rule, errors.New("A FromPort must be set") + } + if opts.ToPort == 0 { + return rule, errors.New("A ToPort must be set") + } + if opts.IPProtocol == "" { + return rule, errors.New("A IPProtocol must be set") + } + if opts.CIDR == "" && opts.FromGroupID == "" { + return rule, errors.New("A CIDR or FromGroupID must be set") + } + + rule["parent_group_id"] = opts.ParentGroupID + rule["from_port"] = opts.FromPort + rule["to_port"] = opts.ToPort + rule["ip_protocol"] = opts.IPProtocol + + if opts.CIDR != "" { + rule["cidr"] = opts.CIDR + } + if opts.FromGroupID != "" { + rule["from_group_id"] = opts.FromGroupID + } + + return map[string]interface{}{"security_group_rule": rule}, nil +} + +// CreateRule will add a new rule to an existing security group (whose ID is +// specified in CreateRuleOpts). You have the option of controlling inbound +// traffic from either an IP range (CIDR) or from another security group. +func CreateRule(client *gophercloud.ServiceClient, opts CreateRuleOptsBuilder) CreateRuleResult { + var result CreateRuleResult + + reqBody, err := opts.ToRuleCreateMap() + if err != nil { + result.Err = err + return result + } + + _, result.Err = perigee.Request("POST", rootRuleURL(client), perigee.Options{ + Results: &result.Body, + ReqBody: &reqBody, + MoreHeaders: client.AuthenticatedHeaders(), + OkCodes: []int{200}, + }) + + return result +} + +// DeleteRule will permanently delete a rule from a security group. +func DeleteRule(client *gophercloud.ServiceClient, id string) gophercloud.ErrResult { + var result gophercloud.ErrResult + + _, result.Err = perigee.Request("DELETE", resourceRuleURL(client, id), perigee.Options{ + MoreHeaders: client.AuthenticatedHeaders(), + OkCodes: []int{202}, + }) + + return result +} + +func actionMap(prefix, groupName string) map[string]map[string]string { + return map[string]map[string]string{ + prefix + "SecurityGroup": map[string]string{"name": groupName}, + } +} + +// AddServerToGroup will associate a server and a security group, enforcing the +// rules of the group on the server. +func AddServerToGroup(client *gophercloud.ServiceClient, serverID, groupName string) gophercloud.ErrResult { + var result gophercloud.ErrResult + + _, result.Err = perigee.Request("POST", serverActionURL(client, serverID), perigee.Options{ + Results: &result.Body, + ReqBody: actionMap("add", groupName), + MoreHeaders: client.AuthenticatedHeaders(), + OkCodes: []int{202}, + }) + + return result +} + +// RemoveServerFromGroup will disassociate a server from a security group. +func RemoveServerFromGroup(client *gophercloud.ServiceClient, serverID, groupName string) gophercloud.ErrResult { + var result gophercloud.ErrResult + + _, result.Err = perigee.Request("POST", serverActionURL(client, serverID), perigee.Options{ + Results: &result.Body, + ReqBody: actionMap("remove", groupName), + MoreHeaders: client.AuthenticatedHeaders(), + OkCodes: []int{202}, + }) + + return result +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/secgroups/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/secgroups/requests_test.go new file mode 100644 index 0000000000..4e21d5deaa --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/secgroups/requests_test.go @@ -0,0 +1,248 @@ +package secgroups + +import ( + "testing" + + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +const ( + serverID = "{serverID}" + groupID = "{groupID}" + ruleID = "{ruleID}" +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockListGroupsResponse(t) + + count := 0 + + err := List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractSecurityGroups(page) + if err != nil { + t.Errorf("Failed to extract users: %v", err) + return false, err + } + + expected := []SecurityGroup{ + SecurityGroup{ + ID: groupID, + Description: "default", + Name: "default", + Rules: []Rule{}, + TenantID: "openstack", + }, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + + th.AssertNoErr(t, err) + th.AssertEquals(t, 1, count) +} + +func TestListByServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockListGroupsByServerResponse(t, serverID) + + count := 0 + + err := ListByServer(client.ServiceClient(), serverID).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractSecurityGroups(page) + if err != nil { + t.Errorf("Failed to extract users: %v", err) + return false, err + } + + expected := []SecurityGroup{ + SecurityGroup{ + ID: groupID, + Description: "default", + Name: "default", + Rules: []Rule{}, + TenantID: "openstack", + }, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + + th.AssertNoErr(t, err) + th.AssertEquals(t, 1, count) +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockCreateGroupResponse(t) + + opts := CreateOpts{ + Name: "test", + Description: "something", + } + + group, err := Create(client.ServiceClient(), opts).Extract() + th.AssertNoErr(t, err) + + expected := &SecurityGroup{ + ID: groupID, + Name: "test", + Description: "something", + TenantID: "openstack", + Rules: []Rule{}, + } + th.AssertDeepEquals(t, expected, group) +} + +func TestUpdate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockUpdateGroupResponse(t, groupID) + + opts := UpdateOpts{ + Name: "new_name", + Description: "new_desc", + } + + group, err := Update(client.ServiceClient(), groupID, opts).Extract() + th.AssertNoErr(t, err) + + expected := &SecurityGroup{ + ID: groupID, + Name: "new_name", + Description: "something", + TenantID: "openstack", + Rules: []Rule{}, + } + th.AssertDeepEquals(t, expected, group) +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockGetGroupsResponse(t, groupID) + + group, err := Get(client.ServiceClient(), groupID).Extract() + th.AssertNoErr(t, err) + + expected := &SecurityGroup{ + ID: groupID, + Description: "default", + Name: "default", + TenantID: "openstack", + Rules: []Rule{ + Rule{ + FromPort: 80, + ToPort: 85, + IPProtocol: "TCP", + IPRange: IPRange{CIDR: "0.0.0.0"}, + Group: Group{TenantID: "openstack", Name: "default"}, + ParentGroupID: groupID, + ID: ruleID, + }, + }, + } + + th.AssertDeepEquals(t, expected, group) +} + +func TestGetNumericID(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + numericGroupID := 12345 + + mockGetNumericIDGroupResponse(t, numericGroupID) + + group, err := Get(client.ServiceClient(), "12345").Extract() + th.AssertNoErr(t, err) + + expected := &SecurityGroup{ID: "12345"} + th.AssertDeepEquals(t, expected, group) +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockDeleteGroupResponse(t, groupID) + + err := Delete(client.ServiceClient(), groupID).ExtractErr() + th.AssertNoErr(t, err) +} + +func TestAddRule(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockAddRuleResponse(t) + + opts := CreateRuleOpts{ + ParentGroupID: groupID, + FromPort: 22, + ToPort: 22, + IPProtocol: "TCP", + CIDR: "0.0.0.0/0", + } + + rule, err := CreateRule(client.ServiceClient(), opts).Extract() + th.AssertNoErr(t, err) + + expected := &Rule{ + FromPort: 22, + ToPort: 22, + Group: Group{}, + IPProtocol: "TCP", + ParentGroupID: groupID, + IPRange: IPRange{CIDR: "0.0.0.0/0"}, + ID: ruleID, + } + + th.AssertDeepEquals(t, expected, rule) +} + +func TestDeleteRule(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockDeleteRuleResponse(t, ruleID) + + err := DeleteRule(client.ServiceClient(), ruleID).ExtractErr() + th.AssertNoErr(t, err) +} + +func TestAddServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockAddServerToGroupResponse(t, serverID) + + err := AddServerToGroup(client.ServiceClient(), serverID, "test").ExtractErr() + th.AssertNoErr(t, err) +} + +func TestRemoveServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockRemoveServerFromGroupResponse(t, serverID) + + err := RemoveServerFromGroup(client.ServiceClient(), serverID, "test").ExtractErr() + th.AssertNoErr(t, err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/secgroups/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/secgroups/results.go new file mode 100644 index 0000000000..478c5dc097 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/secgroups/results.go @@ -0,0 +1,147 @@ +package secgroups + +import ( + "github.com/mitchellh/mapstructure" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// SecurityGroup represents a security group. +type SecurityGroup struct { + // The unique ID of the group. If Neutron is installed, this ID will be + // represented as a string UUID; if Neutron is not installed, it will be a + // numeric ID. For the sake of consistency, we always cast it to a string. + ID string + + // The human-readable name of the group, which needs to be unique. + Name string + + // The human-readable description of the group. + Description string + + // The rules which determine how this security group operates. + Rules []Rule + + // The ID of the tenant to which this security group belongs. + TenantID string `mapstructure:"tenant_id"` +} + +// Rule represents a security group rule, a policy which determines how a +// security group operates and what inbound traffic it allows in. +type Rule struct { + // The unique ID. If Neutron is installed, this ID will be + // represented as a string UUID; if Neutron is not installed, it will be a + // numeric ID. For the sake of consistency, we always cast it to a string. + ID string + + // The lower bound of the port range which this security group should open up + FromPort int `mapstructure:"from_port"` + + // The upper bound of the port range which this security group should open up + ToPort int `mapstructure:"to_port"` + + // The IP protocol (e.g. TCP) which the security group accepts + IPProtocol string `mapstructure:"ip_protocol"` + + // The CIDR IP range whose traffic can be received + IPRange IPRange `mapstructure:"ip_range"` + + // The security group ID to which this rule belongs + ParentGroupID string `mapstructure:"parent_group_id"` + + // Not documented. + Group Group +} + +// IPRange represents the IP range whose traffic will be accepted by the +// security group. +type IPRange struct { + CIDR string +} + +// Group represents a group. +type Group struct { + TenantID string `mapstructure:"tenant_id"` + Name string +} + +// SecurityGroupPage is a single page of a SecurityGroup collection. +type SecurityGroupPage struct { + pagination.SinglePageBase +} + +// IsEmpty determines whether or not a page of Security Groups contains any results. +func (page SecurityGroupPage) IsEmpty() (bool, error) { + users, err := ExtractSecurityGroups(page) + if err != nil { + return false, err + } + return len(users) == 0, nil +} + +// ExtractSecurityGroups returns a slice of SecurityGroups contained in a single page of results. +func ExtractSecurityGroups(page pagination.Page) ([]SecurityGroup, error) { + casted := page.(SecurityGroupPage).Body + var response struct { + SecurityGroups []SecurityGroup `mapstructure:"security_groups"` + } + + err := mapstructure.WeakDecode(casted, &response) + + return response.SecurityGroups, err +} + +type commonResult struct { + gophercloud.Result +} + +// CreateResult represents the result of a create operation. +type CreateResult struct { + commonResult +} + +// GetResult represents the result of a get operation. +type GetResult struct { + commonResult +} + +// UpdateResult represents the result of an update operation. +type UpdateResult struct { + commonResult +} + +// Extract will extract a SecurityGroup struct from most responses. +func (r commonResult) Extract() (*SecurityGroup, error) { + if r.Err != nil { + return nil, r.Err + } + + var response struct { + SecurityGroup SecurityGroup `mapstructure:"security_group"` + } + + err := mapstructure.WeakDecode(r.Body, &response) + + return &response.SecurityGroup, err +} + +// CreateRuleResult represents the result when adding rules to a security group. +type CreateRuleResult struct { + gophercloud.Result +} + +// Extract will extract a Rule struct from a CreateRuleResult. +func (r CreateRuleResult) Extract() (*Rule, error) { + if r.Err != nil { + return nil, r.Err + } + + var response struct { + Rule Rule `mapstructure:"security_group_rule"` + } + + err := mapstructure.WeakDecode(r.Body, &response) + + return &response.Rule, err +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/secgroups/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/secgroups/urls.go new file mode 100644 index 0000000000..f4760b6f2e --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/secgroups/urls.go @@ -0,0 +1,32 @@ +package secgroups + +import "github.com/rackspace/gophercloud" + +const ( + secgrouppath = "os-security-groups" + rulepath = "os-security-group-rules" +) + +func resourceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(secgrouppath, id) +} + +func rootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(secgrouppath) +} + +func listByServerURL(c *gophercloud.ServiceClient, serverID string) string { + return c.ServiceURL(secgrouppath, "servers", serverID, secgrouppath) +} + +func rootRuleURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(rulepath) +} + +func resourceRuleURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(rulepath, id) +} + +func serverActionURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL("servers", id, "action") +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/startstop/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/startstop/doc.go new file mode 100644 index 0000000000..d2729f8743 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/startstop/doc.go @@ -0,0 +1,5 @@ +/* +Package startstop provides functionality to start and stop servers that have +been provisioned by the OpenStack Compute service. +*/ +package startstop diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/startstop/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/startstop/fixtures.go new file mode 100644 index 0000000000..670828a986 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/startstop/fixtures.go @@ -0,0 +1,27 @@ +package startstop + +import ( + "net/http" + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +func mockStartServerResponse(t *testing.T, id string) { + th.Mux.HandleFunc("/servers/"+id+"/action", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, `{"os-start": null}`) + w.WriteHeader(http.StatusAccepted) + }) +} + +func mockStopServerResponse(t *testing.T, id string) { + th.Mux.HandleFunc("/servers/"+id+"/action", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, `{"os-stop": null}`) + w.WriteHeader(http.StatusAccepted) + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/startstop/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/startstop/requests.go new file mode 100644 index 0000000000..99c91b054a --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/startstop/requests.go @@ -0,0 +1,40 @@ +package startstop + +import ( + "github.com/racker/perigee" + "github.com/rackspace/gophercloud" +) + +func actionURL(client *gophercloud.ServiceClient, id string) string { + return client.ServiceURL("servers", id, "action") +} + +// Start is the operation responsible for starting a Compute server. +func Start(client *gophercloud.ServiceClient, id string) gophercloud.ErrResult { + var res gophercloud.ErrResult + + reqBody := map[string]interface{}{"os-start": nil} + + _, res.Err = perigee.Request("POST", actionURL(client, id), perigee.Options{ + MoreHeaders: client.AuthenticatedHeaders(), + ReqBody: reqBody, + OkCodes: []int{202}, + }) + + return res +} + +// Stop is the operation responsible for stopping a Compute server. +func Stop(client *gophercloud.ServiceClient, id string) gophercloud.ErrResult { + var res gophercloud.ErrResult + + reqBody := map[string]interface{}{"os-stop": nil} + + _, res.Err = perigee.Request("POST", actionURL(client, id), perigee.Options{ + MoreHeaders: client.AuthenticatedHeaders(), + ReqBody: reqBody, + OkCodes: []int{202}, + }) + + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/startstop/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/startstop/requests_test.go new file mode 100644 index 0000000000..97a121b19a --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/startstop/requests_test.go @@ -0,0 +1,30 @@ +package startstop + +import ( + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +const serverID = "{serverId}" + +func TestStart(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockStartServerResponse(t, serverID) + + err := Start(client.ServiceClient(), serverID).ExtractErr() + th.AssertNoErr(t, err) +} + +func TestStop(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockStopServerResponse(t, serverID) + + err := Stop(client.ServiceClient(), serverID).ExtractErr() + th.AssertNoErr(t, err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/flavors/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/flavors/doc.go new file mode 100644 index 0000000000..5822e1bcf6 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/flavors/doc.go @@ -0,0 +1,7 @@ +// Package flavors provides information and interaction with the flavor API +// resource in the OpenStack Compute service. +// +// A flavor is an available hardware configuration for a server. Each flavor +// has a unique combination of disk space, memory capacity and priority for CPU +// time. +package flavors diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/flavors/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/flavors/requests.go new file mode 100644 index 0000000000..065a2ec472 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/flavors/requests.go @@ -0,0 +1,72 @@ +package flavors + +import ( + "github.com/racker/perigee" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToFlavorListQuery() (string, error) +} + +// ListOpts helps control the results returned by the List() function. +// For example, a flavor with a minDisk field of 10 will not be returned if you specify MinDisk set to 20. +// Typically, software will use the last ID of the previous call to List to set the Marker for the current call. +type ListOpts struct { + + // ChangesSince, if provided, instructs List to return only those things which have changed since the timestamp provided. + ChangesSince string `q:"changes-since"` + + // MinDisk and MinRAM, if provided, elides flavors which do not meet your criteria. + MinDisk int `q:"minDisk"` + MinRAM int `q:"minRam"` + + // Marker and Limit control paging. + // Marker instructs List where to start listing from. + Marker string `q:"marker"` + + // Limit instructs List to refrain from sending excessively large lists of flavors. + Limit int `q:"limit"` +} + +// ToFlavorListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToFlavorListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + if err != nil { + return "", err + } + return q.String(), nil +} + +// ListDetail instructs OpenStack to provide a list of flavors. +// You may provide criteria by which List curtails its results for easier processing. +// See ListOpts for more details. +func ListDetail(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(client) + if opts != nil { + query, err := opts.ToFlavorListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + createPage := func(r pagination.PageResult) pagination.Page { + return FlavorPage{pagination.LinkedPageBase{PageResult: r}} + } + + return pagination.NewPager(client, url, createPage) +} + +// Get instructs OpenStack to provide details on a single flavor, identified by its ID. +// Use ExtractFlavor to convert its result into a Flavor. +func Get(client *gophercloud.ServiceClient, id string) GetResult { + var gr GetResult + gr.Err = perigee.Get(getURL(client, id), perigee.Options{ + Results: &gr.Body, + MoreHeaders: client.AuthenticatedHeaders(), + }) + return gr +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/flavors/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/flavors/requests_test.go new file mode 100644 index 0000000000..fbd7c33140 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/flavors/requests_test.go @@ -0,0 +1,129 @@ +package flavors + +import ( + "fmt" + "net/http" + "reflect" + "testing" + + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +const tokenID = "blerb" + +func TestListFlavors(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/flavors/detail", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + r.ParseForm() + marker := r.Form.Get("marker") + switch marker { + case "": + fmt.Fprintf(w, ` + { + "flavors": [ + { + "id": "1", + "name": "m1.tiny", + "disk": 1, + "ram": 512, + "vcpus": 1 + }, + { + "id": "2", + "name": "m2.small", + "disk": 10, + "ram": 1024, + "vcpus": 2 + } + ], + "flavors_links": [ + { + "href": "%s/flavors/detail?marker=2", + "rel": "next" + } + ] + } + `, th.Server.URL) + case "2": + fmt.Fprintf(w, `{ "flavors": [] }`) + default: + t.Fatalf("Unexpected marker: [%s]", marker) + } + }) + + pages := 0 + err := ListDetail(fake.ServiceClient(), nil).EachPage(func(page pagination.Page) (bool, error) { + pages++ + + actual, err := ExtractFlavors(page) + if err != nil { + return false, err + } + + expected := []Flavor{ + Flavor{ID: "1", Name: "m1.tiny", Disk: 1, RAM: 512, VCPUs: 1}, + Flavor{ID: "2", Name: "m2.small", Disk: 10, RAM: 1024, VCPUs: 2}, + } + + if !reflect.DeepEqual(expected, actual) { + t.Errorf("Expected %#v, but was %#v", expected, actual) + } + + return true, nil + }) + if err != nil { + t.Fatal(err) + } + if pages != 1 { + t.Errorf("Expected one page, got %d", pages) + } +} + +func TestGetFlavor(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/flavors/12345", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, ` + { + "flavor": { + "id": "1", + "name": "m1.tiny", + "disk": 1, + "ram": 512, + "vcpus": 1, + "rxtx_factor": 1 + } + } + `) + }) + + actual, err := Get(fake.ServiceClient(), "12345").Extract() + if err != nil { + t.Fatalf("Unable to get flavor: %v", err) + } + + expected := &Flavor{ + ID: "1", + Name: "m1.tiny", + Disk: 1, + RAM: 512, + VCPUs: 1, + RxTxFactor: 1, + } + if !reflect.DeepEqual(expected, actual) { + t.Errorf("Expected %#v, but was %#v", expected, actual) + } +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/flavors/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/flavors/results.go new file mode 100644 index 0000000000..8dddd705c9 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/flavors/results.go @@ -0,0 +1,122 @@ +package flavors + +import ( + "errors" + "reflect" + + "github.com/mitchellh/mapstructure" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// ErrCannotInterpret is returned by an Extract call if the response body doesn't have the expected structure. +var ErrCannotInterpet = errors.New("Unable to interpret a response body.") + +// GetResult temporarily holds the response from a Get call. +type GetResult struct { + gophercloud.Result +} + +// Extract provides access to the individual Flavor returned by the Get function. +func (gr GetResult) Extract() (*Flavor, error) { + if gr.Err != nil { + return nil, gr.Err + } + + var result struct { + Flavor Flavor `mapstructure:"flavor"` + } + + cfg := &mapstructure.DecoderConfig{ + DecodeHook: defaulter, + Result: &result, + } + decoder, err := mapstructure.NewDecoder(cfg) + if err != nil { + return nil, err + } + err = decoder.Decode(gr.Body) + return &result.Flavor, err +} + +// Flavor records represent (virtual) hardware configurations for server resources in a region. +type Flavor struct { + // The Id field contains the flavor's unique identifier. + // For example, this identifier will be useful when specifying which hardware configuration to use for a new server instance. + ID string `mapstructure:"id"` + + // The Disk and RA< fields provide a measure of storage space offered by the flavor, in GB and MB, respectively. + Disk int `mapstructure:"disk"` + RAM int `mapstructure:"ram"` + + // The Name field provides a human-readable moniker for the flavor. + Name string `mapstructure:"name"` + + RxTxFactor float64 `mapstructure:"rxtx_factor"` + + // Swap indicates how much space is reserved for swap. + // If not provided, this field will be set to 0. + Swap int `mapstructure:"swap"` + + // VCPUs indicates how many (virtual) CPUs are available for this flavor. + VCPUs int `mapstructure:"vcpus"` +} + +// FlavorPage contains a single page of the response from a List call. +type FlavorPage struct { + pagination.LinkedPageBase +} + +// IsEmpty determines if a page contains any results. +func (p FlavorPage) IsEmpty() (bool, error) { + flavors, err := ExtractFlavors(p) + if err != nil { + return true, err + } + return len(flavors) == 0, nil +} + +// NextPageURL uses the response's embedded link reference to navigate to the next page of results. +func (p FlavorPage) NextPageURL() (string, error) { + type resp struct { + Links []gophercloud.Link `mapstructure:"flavors_links"` + } + + var r resp + err := mapstructure.Decode(p.Body, &r) + if err != nil { + return "", err + } + + return gophercloud.ExtractNextURL(r.Links) +} + +func defaulter(from, to reflect.Kind, v interface{}) (interface{}, error) { + if (from == reflect.String) && (to == reflect.Int) { + return 0, nil + } + return v, nil +} + +// ExtractFlavors provides access to the list of flavors in a page acquired from the List operation. +func ExtractFlavors(page pagination.Page) ([]Flavor, error) { + casted := page.(FlavorPage).Body + var container struct { + Flavors []Flavor `mapstructure:"flavors"` + } + + cfg := &mapstructure.DecoderConfig{ + DecodeHook: defaulter, + Result: &container, + } + decoder, err := mapstructure.NewDecoder(cfg) + if err != nil { + return container.Flavors, err + } + err = decoder.Decode(casted) + if err != nil { + return container.Flavors, err + } + + return container.Flavors, nil +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/flavors/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/flavors/urls.go new file mode 100644 index 0000000000..683c107dcb --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/flavors/urls.go @@ -0,0 +1,13 @@ +package flavors + +import ( + "github.com/rackspace/gophercloud" +) + +func getURL(client *gophercloud.ServiceClient, id string) string { + return client.ServiceURL("flavors", id) +} + +func listURL(client *gophercloud.ServiceClient) string { + return client.ServiceURL("flavors", "detail") +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/flavors/urls_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/flavors/urls_test.go new file mode 100644 index 0000000000..069da2496e --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/flavors/urls_test.go @@ -0,0 +1,26 @@ +package flavors + +import ( + "testing" + + "github.com/rackspace/gophercloud" + th "github.com/rackspace/gophercloud/testhelper" +) + +const endpoint = "http://localhost:57909/" + +func endpointClient() *gophercloud.ServiceClient { + return &gophercloud.ServiceClient{Endpoint: endpoint} +} + +func TestGetURL(t *testing.T) { + actual := getURL(endpointClient(), "foo") + expected := endpoint + "flavors/foo" + th.CheckEquals(t, expected, actual) +} + +func TestListURL(t *testing.T) { + actual := listURL(endpointClient()) + expected := endpoint + "flavors/detail" + th.CheckEquals(t, expected, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/images/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/images/doc.go new file mode 100644 index 0000000000..0edaa3f025 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/images/doc.go @@ -0,0 +1,7 @@ +// Package images provides information and interaction with the image API +// resource in the OpenStack Compute service. +// +// An image is a collection of files used to create or rebuild a server. +// Operators provide a number of pre-built OS images by default. You may also +// create custom images from cloud servers you have launched. +package images diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/images/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/images/requests.go new file mode 100644 index 0000000000..bc61ddb9df --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/images/requests.go @@ -0,0 +1,71 @@ +package images + +import ( + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" + + "github.com/racker/perigee" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToImageListQuery() (string, error) +} + +// ListOpts contain options for limiting the number of Images returned from a call to ListDetail. +type ListOpts struct { + // When the image last changed status (in date-time format). + ChangesSince string `q:"changes-since"` + // The number of Images to return. + Limit int `q:"limit"` + // UUID of the Image at which to set a marker. + Marker string `q:"marker"` + // The name of the Image. + Name string `q:"name:"` + // The name of the Server (in URL format). + Server string `q:"server"` + // The current status of the Image. + Status string `q:"status"` + // The value of the type of image (e.g. BASE, SERVER, ALL) + Type string `q:"type"` +} + +// ToImageListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToImageListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + if err != nil { + return "", err + } + return q.String(), nil +} + +// ListDetail enumerates the available images. +func ListDetail(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listDetailURL(client) + if opts != nil { + query, err := opts.ToImageListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + + createPage := func(r pagination.PageResult) pagination.Page { + return ImagePage{pagination.LinkedPageBase{PageResult: r}} + } + + return pagination.NewPager(client, url, createPage) +} + +// Get acquires additional detail about a specific image by ID. +// Use ExtractImage() to interpret the result as an openstack Image. +func Get(client *gophercloud.ServiceClient, id string) GetResult { + var result GetResult + _, result.Err = perigee.Request("GET", getURL(client, id), perigee.Options{ + MoreHeaders: client.AuthenticatedHeaders(), + Results: &result.Body, + OkCodes: []int{200}, + }) + return result +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/images/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/images/requests_test.go new file mode 100644 index 0000000000..9a05f97ec0 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/images/requests_test.go @@ -0,0 +1,175 @@ +package images + +import ( + "encoding/json" + "fmt" + "net/http" + "reflect" + "testing" + + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestListImages(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/images/detail", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + r.ParseForm() + marker := r.Form.Get("marker") + switch marker { + case "": + fmt.Fprintf(w, ` + { + "images": [ + { + "status": "ACTIVE", + "updated": "2014-09-23T12:54:56Z", + "id": "f3e4a95d-1f4f-4989-97ce-f3a1fb8c04d7", + "OS-EXT-IMG-SIZE:size": 476704768, + "name": "F17-x86_64-cfntools", + "created": "2014-09-23T12:54:52Z", + "minDisk": 0, + "progress": 100, + "minRam": 0, + "metadata": {} + }, + { + "status": "ACTIVE", + "updated": "2014-09-23T12:51:43Z", + "id": "f90f6034-2570-4974-8351-6b49732ef2eb", + "OS-EXT-IMG-SIZE:size": 13167616, + "name": "cirros-0.3.2-x86_64-disk", + "created": "2014-09-23T12:51:42Z", + "minDisk": 0, + "progress": 100, + "minRam": 0, + "metadata": {} + } + ] + } + `) + case "2": + fmt.Fprintf(w, `{ "images": [] }`) + default: + t.Fatalf("Unexpected marker: [%s]", marker) + } + }) + + pages := 0 + options := &ListOpts{Limit: 2} + err := ListDetail(fake.ServiceClient(), options).EachPage(func(page pagination.Page) (bool, error) { + pages++ + + actual, err := ExtractImages(page) + if err != nil { + return false, err + } + + expected := []Image{ + Image{ + ID: "f3e4a95d-1f4f-4989-97ce-f3a1fb8c04d7", + Name: "F17-x86_64-cfntools", + Created: "2014-09-23T12:54:52Z", + Updated: "2014-09-23T12:54:56Z", + MinDisk: 0, + MinRAM: 0, + Progress: 100, + Status: "ACTIVE", + }, + Image{ + ID: "f90f6034-2570-4974-8351-6b49732ef2eb", + Name: "cirros-0.3.2-x86_64-disk", + Created: "2014-09-23T12:51:42Z", + Updated: "2014-09-23T12:51:43Z", + MinDisk: 0, + MinRAM: 0, + Progress: 100, + Status: "ACTIVE", + }, + } + + if !reflect.DeepEqual(expected, actual) { + t.Errorf("Unexpected page contents: expected %#v, got %#v", expected, actual) + } + + return false, nil + }) + + if err != nil { + t.Fatalf("EachPage error: %v", err) + } + if pages != 1 { + t.Errorf("Expected one page, got %d", pages) + } +} + +func TestGetImage(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/images/12345678", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, ` + { + "image": { + "status": "ACTIVE", + "updated": "2014-09-23T12:54:56Z", + "id": "f3e4a95d-1f4f-4989-97ce-f3a1fb8c04d7", + "OS-EXT-IMG-SIZE:size": 476704768, + "name": "F17-x86_64-cfntools", + "created": "2014-09-23T12:54:52Z", + "minDisk": 0, + "progress": 100, + "minRam": 0, + "metadata": {} + } + } + `) + }) + + actual, err := Get(fake.ServiceClient(), "12345678").Extract() + if err != nil { + t.Fatalf("Unexpected error from Get: %v", err) + } + + expected := &Image{ + Status: "ACTIVE", + Updated: "2014-09-23T12:54:56Z", + ID: "f3e4a95d-1f4f-4989-97ce-f3a1fb8c04d7", + Name: "F17-x86_64-cfntools", + Created: "2014-09-23T12:54:52Z", + MinDisk: 0, + Progress: 100, + MinRAM: 0, + } + + if !reflect.DeepEqual(expected, actual) { + t.Errorf("Expected %#v, but got %#v", expected, actual) + } +} + +func TestNextPageURL(t *testing.T) { + var page ImagePage + var body map[string]interface{} + bodyString := []byte(`{"images":{"links":[{"href":"http://192.154.23.87/12345/images/image3","rel":"bookmark"}]}, "images_links":[{"href":"http://192.154.23.87/12345/images/image4","rel":"next"}]}`) + err := json.Unmarshal(bodyString, &body) + if err != nil { + t.Fatalf("Error unmarshaling data into page body: %v", err) + } + page.Body = body + + expected := "http://192.154.23.87/12345/images/image4" + actual, err := page.NextPageURL() + th.AssertNoErr(t, err) + th.CheckEquals(t, expected, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/images/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/images/results.go new file mode 100644 index 0000000000..493d51192c --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/images/results.go @@ -0,0 +1,90 @@ +package images + +import ( + "github.com/mitchellh/mapstructure" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// GetResult temporarily stores a Get response. +type GetResult struct { + gophercloud.Result +} + +// Extract interprets a GetResult as an Image. +func (gr GetResult) Extract() (*Image, error) { + if gr.Err != nil { + return nil, gr.Err + } + + var decoded struct { + Image Image `mapstructure:"image"` + } + + err := mapstructure.Decode(gr.Body, &decoded) + return &decoded.Image, err +} + +// Image is used for JSON (un)marshalling. +// It provides a description of an OS image. +type Image struct { + // ID contains the image's unique identifier. + ID string + + Created string + + // MinDisk and MinRAM specify the minimum resources a server must provide to be able to install the image. + MinDisk int + MinRAM int + + // Name provides a human-readable moniker for the OS image. + Name string + + // The Progress and Status fields indicate image-creation status. + // Any usable image will have 100% progress. + Progress int + Status string + + Updated string +} + +// ImagePage contains a single page of results from a List operation. +// Use ExtractImages to convert it into a slice of usable structs. +type ImagePage struct { + pagination.LinkedPageBase +} + +// IsEmpty returns true if a page contains no Image results. +func (page ImagePage) IsEmpty() (bool, error) { + images, err := ExtractImages(page) + if err != nil { + return true, err + } + return len(images) == 0, nil +} + +// NextPageURL uses the response's embedded link reference to navigate to the next page of results. +func (page ImagePage) NextPageURL() (string, error) { + type resp struct { + Links []gophercloud.Link `mapstructure:"images_links"` + } + + var r resp + err := mapstructure.Decode(page.Body, &r) + if err != nil { + return "", err + } + + return gophercloud.ExtractNextURL(r.Links) +} + +// ExtractImages converts a page of List results into a slice of usable Image structs. +func ExtractImages(page pagination.Page) ([]Image, error) { + casted := page.(ImagePage).Body + var results struct { + Images []Image `mapstructure:"images"` + } + + err := mapstructure.Decode(casted, &results) + return results.Images, err +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/images/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/images/urls.go new file mode 100644 index 0000000000..9b3c86d435 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/images/urls.go @@ -0,0 +1,11 @@ +package images + +import "github.com/rackspace/gophercloud" + +func listDetailURL(client *gophercloud.ServiceClient) string { + return client.ServiceURL("images", "detail") +} + +func getURL(client *gophercloud.ServiceClient, id string) string { + return client.ServiceURL("images", id) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/images/urls_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/images/urls_test.go new file mode 100644 index 0000000000..b1ab3d6790 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/images/urls_test.go @@ -0,0 +1,26 @@ +package images + +import ( + "testing" + + "github.com/rackspace/gophercloud" + th "github.com/rackspace/gophercloud/testhelper" +) + +const endpoint = "http://localhost:57909/" + +func endpointClient() *gophercloud.ServiceClient { + return &gophercloud.ServiceClient{Endpoint: endpoint} +} + +func TestGetURL(t *testing.T) { + actual := getURL(endpointClient(), "foo") + expected := endpoint + "images/foo" + th.CheckEquals(t, expected, actual) +} + +func TestListDetailURL(t *testing.T) { + actual := listDetailURL(endpointClient()) + expected := endpoint + "images/detail" + th.CheckEquals(t, expected, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/servers/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/servers/doc.go new file mode 100644 index 0000000000..fe4567120c --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/servers/doc.go @@ -0,0 +1,6 @@ +// Package servers provides information and interaction with the server API +// resource in the OpenStack Compute service. +// +// A server is a virtual machine instance in the compute system. In order for +// one to be provisioned, a valid flavor and image are required. +package servers diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/servers/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/servers/fixtures.go new file mode 100644 index 0000000000..01646058e6 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/servers/fixtures.go @@ -0,0 +1,559 @@ +// +build fixtures + +package servers + +import ( + "fmt" + "net/http" + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +// ServerListBody contains the canned body of a servers.List response. +const ServerListBody = ` +{ + "servers": [ + { + "status": "ACTIVE", + "updated": "2014-09-25T13:10:10Z", + "hostId": "29d3c8c896a45aa4c34e52247875d7fefc3d94bbcc9f622b5d204362", + "OS-EXT-SRV-ATTR:host": "devstack", + "addresses": { + "private": [ + { + "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:7c:1b:2b", + "version": 4, + "addr": "10.0.0.32", + "OS-EXT-IPS:type": "fixed" + } + ] + }, + "links": [ + { + "href": "http://104.130.131.164:8774/v2/fcad67a6189847c4aecfa3c81a05783b/servers/ef079b0c-e610-4dfb-b1aa-b49f07ac48e5", + "rel": "self" + }, + { + "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/servers/ef079b0c-e610-4dfb-b1aa-b49f07ac48e5", + "rel": "bookmark" + } + ], + "key_name": null, + "image": { + "id": "f90f6034-2570-4974-8351-6b49732ef2eb", + "links": [ + { + "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/images/f90f6034-2570-4974-8351-6b49732ef2eb", + "rel": "bookmark" + } + ] + }, + "OS-EXT-STS:task_state": null, + "OS-EXT-STS:vm_state": "active", + "OS-EXT-SRV-ATTR:instance_name": "instance-0000001e", + "OS-SRV-USG:launched_at": "2014-09-25T13:10:10.000000", + "OS-EXT-SRV-ATTR:hypervisor_hostname": "devstack", + "flavor": { + "id": "1", + "links": [ + { + "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/flavors/1", + "rel": "bookmark" + } + ] + }, + "id": "ef079b0c-e610-4dfb-b1aa-b49f07ac48e5", + "security_groups": [ + { + "name": "default" + } + ], + "OS-SRV-USG:terminated_at": null, + "OS-EXT-AZ:availability_zone": "nova", + "user_id": "9349aff8be7545ac9d2f1d00999a23cd", + "name": "herp", + "created": "2014-09-25T13:10:02Z", + "tenant_id": "fcad67a6189847c4aecfa3c81a05783b", + "OS-DCF:diskConfig": "MANUAL", + "os-extended-volumes:volumes_attached": [], + "accessIPv4": "", + "accessIPv6": "", + "progress": 0, + "OS-EXT-STS:power_state": 1, + "config_drive": "", + "metadata": {} + }, + { + "status": "ACTIVE", + "updated": "2014-09-25T13:04:49Z", + "hostId": "29d3c8c896a45aa4c34e52247875d7fefc3d94bbcc9f622b5d204362", + "OS-EXT-SRV-ATTR:host": "devstack", + "addresses": { + "private": [ + { + "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:9e:89:be", + "version": 4, + "addr": "10.0.0.31", + "OS-EXT-IPS:type": "fixed" + } + ] + }, + "links": [ + { + "href": "http://104.130.131.164:8774/v2/fcad67a6189847c4aecfa3c81a05783b/servers/9e5476bd-a4ec-4653-93d6-72c93aa682ba", + "rel": "self" + }, + { + "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/servers/9e5476bd-a4ec-4653-93d6-72c93aa682ba", + "rel": "bookmark" + } + ], + "key_name": null, + "image": { + "id": "f90f6034-2570-4974-8351-6b49732ef2eb", + "links": [ + { + "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/images/f90f6034-2570-4974-8351-6b49732ef2eb", + "rel": "bookmark" + } + ] + }, + "OS-EXT-STS:task_state": null, + "OS-EXT-STS:vm_state": "active", + "OS-EXT-SRV-ATTR:instance_name": "instance-0000001d", + "OS-SRV-USG:launched_at": "2014-09-25T13:04:49.000000", + "OS-EXT-SRV-ATTR:hypervisor_hostname": "devstack", + "flavor": { + "id": "1", + "links": [ + { + "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/flavors/1", + "rel": "bookmark" + } + ] + }, + "id": "9e5476bd-a4ec-4653-93d6-72c93aa682ba", + "security_groups": [ + { + "name": "default" + } + ], + "OS-SRV-USG:terminated_at": null, + "OS-EXT-AZ:availability_zone": "nova", + "user_id": "9349aff8be7545ac9d2f1d00999a23cd", + "name": "derp", + "created": "2014-09-25T13:04:41Z", + "tenant_id": "fcad67a6189847c4aecfa3c81a05783b", + "OS-DCF:diskConfig": "MANUAL", + "os-extended-volumes:volumes_attached": [], + "accessIPv4": "", + "accessIPv6": "", + "progress": 0, + "OS-EXT-STS:power_state": 1, + "config_drive": "", + "metadata": {} + } + ] +} +` + +// SingleServerBody is the canned body of a Get request on an existing server. +const SingleServerBody = ` +{ + "server": { + "status": "ACTIVE", + "updated": "2014-09-25T13:04:49Z", + "hostId": "29d3c8c896a45aa4c34e52247875d7fefc3d94bbcc9f622b5d204362", + "OS-EXT-SRV-ATTR:host": "devstack", + "addresses": { + "private": [ + { + "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:9e:89:be", + "version": 4, + "addr": "10.0.0.31", + "OS-EXT-IPS:type": "fixed" + } + ] + }, + "links": [ + { + "href": "http://104.130.131.164:8774/v2/fcad67a6189847c4aecfa3c81a05783b/servers/9e5476bd-a4ec-4653-93d6-72c93aa682ba", + "rel": "self" + }, + { + "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/servers/9e5476bd-a4ec-4653-93d6-72c93aa682ba", + "rel": "bookmark" + } + ], + "key_name": null, + "image": { + "id": "f90f6034-2570-4974-8351-6b49732ef2eb", + "links": [ + { + "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/images/f90f6034-2570-4974-8351-6b49732ef2eb", + "rel": "bookmark" + } + ] + }, + "OS-EXT-STS:task_state": null, + "OS-EXT-STS:vm_state": "active", + "OS-EXT-SRV-ATTR:instance_name": "instance-0000001d", + "OS-SRV-USG:launched_at": "2014-09-25T13:04:49.000000", + "OS-EXT-SRV-ATTR:hypervisor_hostname": "devstack", + "flavor": { + "id": "1", + "links": [ + { + "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/flavors/1", + "rel": "bookmark" + } + ] + }, + "id": "9e5476bd-a4ec-4653-93d6-72c93aa682ba", + "security_groups": [ + { + "name": "default" + } + ], + "OS-SRV-USG:terminated_at": null, + "OS-EXT-AZ:availability_zone": "nova", + "user_id": "9349aff8be7545ac9d2f1d00999a23cd", + "name": "derp", + "created": "2014-09-25T13:04:41Z", + "tenant_id": "fcad67a6189847c4aecfa3c81a05783b", + "OS-DCF:diskConfig": "MANUAL", + "os-extended-volumes:volumes_attached": [], + "accessIPv4": "", + "accessIPv6": "", + "progress": 0, + "OS-EXT-STS:power_state": 1, + "config_drive": "", + "metadata": {} + } +} +` + +var ( + // ServerHerp is a Server struct that should correspond to the first result in ServerListBody. + ServerHerp = Server{ + Status: "ACTIVE", + Updated: "2014-09-25T13:10:10Z", + HostID: "29d3c8c896a45aa4c34e52247875d7fefc3d94bbcc9f622b5d204362", + Addresses: map[string]interface{}{ + "private": []interface{}{ + map[string]interface{}{ + "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:7c:1b:2b", + "version": float64(4), + "addr": "10.0.0.32", + "OS-EXT-IPS:type": "fixed", + }, + }, + }, + Links: []interface{}{ + map[string]interface{}{ + "href": "http://104.130.131.164:8774/v2/fcad67a6189847c4aecfa3c81a05783b/servers/ef079b0c-e610-4dfb-b1aa-b49f07ac48e5", + "rel": "self", + }, + map[string]interface{}{ + "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/servers/ef079b0c-e610-4dfb-b1aa-b49f07ac48e5", + "rel": "bookmark", + }, + }, + Image: map[string]interface{}{ + "id": "f90f6034-2570-4974-8351-6b49732ef2eb", + "links": []interface{}{ + map[string]interface{}{ + "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/images/f90f6034-2570-4974-8351-6b49732ef2eb", + "rel": "bookmark", + }, + }, + }, + Flavor: map[string]interface{}{ + "id": "1", + "links": []interface{}{ + map[string]interface{}{ + "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/flavors/1", + "rel": "bookmark", + }, + }, + }, + ID: "ef079b0c-e610-4dfb-b1aa-b49f07ac48e5", + UserID: "9349aff8be7545ac9d2f1d00999a23cd", + Name: "herp", + Created: "2014-09-25T13:10:02Z", + TenantID: "fcad67a6189847c4aecfa3c81a05783b", + Metadata: map[string]interface{}{}, + } + + // ServerDerp is a Server struct that should correspond to the second server in ServerListBody. + ServerDerp = Server{ + Status: "ACTIVE", + Updated: "2014-09-25T13:04:49Z", + HostID: "29d3c8c896a45aa4c34e52247875d7fefc3d94bbcc9f622b5d204362", + Addresses: map[string]interface{}{ + "private": []interface{}{ + map[string]interface{}{ + "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:9e:89:be", + "version": float64(4), + "addr": "10.0.0.31", + "OS-EXT-IPS:type": "fixed", + }, + }, + }, + Links: []interface{}{ + map[string]interface{}{ + "href": "http://104.130.131.164:8774/v2/fcad67a6189847c4aecfa3c81a05783b/servers/9e5476bd-a4ec-4653-93d6-72c93aa682ba", + "rel": "self", + }, + map[string]interface{}{ + "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/servers/9e5476bd-a4ec-4653-93d6-72c93aa682ba", + "rel": "bookmark", + }, + }, + Image: map[string]interface{}{ + "id": "f90f6034-2570-4974-8351-6b49732ef2eb", + "links": []interface{}{ + map[string]interface{}{ + "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/images/f90f6034-2570-4974-8351-6b49732ef2eb", + "rel": "bookmark", + }, + }, + }, + Flavor: map[string]interface{}{ + "id": "1", + "links": []interface{}{ + map[string]interface{}{ + "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/flavors/1", + "rel": "bookmark", + }, + }, + }, + ID: "9e5476bd-a4ec-4653-93d6-72c93aa682ba", + UserID: "9349aff8be7545ac9d2f1d00999a23cd", + Name: "derp", + Created: "2014-09-25T13:04:41Z", + TenantID: "fcad67a6189847c4aecfa3c81a05783b", + Metadata: map[string]interface{}{}, + } +) + +// HandleServerCreationSuccessfully sets up the test server to respond to a server creation request +// with a given response. +func HandleServerCreationSuccessfully(t *testing.T, response string) { + th.Mux.HandleFunc("/servers", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, `{ + "server": { + "name": "derp", + "imageRef": "f90f6034-2570-4974-8351-6b49732ef2eb", + "flavorRef": "1" + } + }`) + + w.WriteHeader(http.StatusAccepted) + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, response) + }) +} + +// HandleServerListSuccessfully sets up the test server to respond to a server List request. +func HandleServerListSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/detail", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + r.ParseForm() + marker := r.Form.Get("marker") + switch marker { + case "": + fmt.Fprintf(w, ServerListBody) + case "9e5476bd-a4ec-4653-93d6-72c93aa682ba": + fmt.Fprintf(w, `{ "servers": [] }`) + default: + t.Fatalf("/servers/detail invoked with unexpected marker=[%s]", marker) + } + }) +} + +// HandleServerDeletionSuccessfully sets up the test server to respond to a server deletion request. +func HandleServerDeletionSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/asdfasdfasdf", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.WriteHeader(http.StatusNoContent) + }) +} + +// HandleServerGetSuccessfully sets up the test server to respond to a server Get request. +func HandleServerGetSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/1234asdf", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + + fmt.Fprintf(w, SingleServerBody) + }) +} + +// HandleServerUpdateSuccessfully sets up the test server to respond to a server Update request. +func HandleServerUpdateSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/1234asdf", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestJSONRequest(t, r, `{ "server": { "name": "new-name" } }`) + + fmt.Fprintf(w, SingleServerBody) + }) +} + +// HandleAdminPasswordChangeSuccessfully sets up the test server to respond to a server password +// change request. +func HandleAdminPasswordChangeSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/1234asdf/action", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, `{ "changePassword": { "adminPass": "new-password" } }`) + + w.WriteHeader(http.StatusAccepted) + }) +} + +// HandleRebootSuccessfully sets up the test server to respond to a reboot request with success. +func HandleRebootSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/1234asdf/action", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, `{ "reboot": { "type": "SOFT" } }`) + + w.WriteHeader(http.StatusAccepted) + }) +} + +// HandleRebuildSuccessfully sets up the test server to respond to a rebuild request with success. +func HandleRebuildSuccessfully(t *testing.T, response string) { + th.Mux.HandleFunc("/servers/1234asdf/action", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, ` + { + "rebuild": { + "name": "new-name", + "adminPass": "swordfish", + "imageRef": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/images/f90f6034-2570-4974-8351-6b49732ef2eb", + "accessIPv4": "1.2.3.4" + } + } + `) + + w.WriteHeader(http.StatusAccepted) + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, response) + }) +} + +// HandleServerRescueSuccessfully sets up the test server to respond to a server Rescue request. +func HandleServerRescueSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/1234asdf/action", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, `{ "rescue": { "adminPass": "1234567890" } }`) + + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{ "adminPass": "1234567890" }`)) + }) +} + +// HandleMetadatumGetSuccessfully sets up the test server to respond to a metadatum Get request. +func HandleMetadatumGetSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/1234asdf/metadata/foo", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + + w.WriteHeader(http.StatusOK) + w.Header().Add("Content-Type", "application/json") + w.Write([]byte(`{ "meta": {"foo":"bar"}}`)) + }) +} + +// HandleMetadatumCreateSuccessfully sets up the test server to respond to a metadatum Create request. +func HandleMetadatumCreateSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/1234asdf/metadata/foo", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, `{ + "meta": { + "foo": "bar" + } + }`) + + w.WriteHeader(http.StatusOK) + w.Header().Add("Content-Type", "application/json") + w.Write([]byte(`{ "meta": {"foo":"bar"}}`)) + }) +} + +// HandleMetadatumDeleteSuccessfully sets up the test server to respond to a metadatum Delete request. +func HandleMetadatumDeleteSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/1234asdf/metadata/foo", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.WriteHeader(http.StatusNoContent) + }) +} + +// HandleMetadataGetSuccessfully sets up the test server to respond to a metadata Get request. +func HandleMetadataGetSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/1234asdf/metadata", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{ "metadata": {"foo":"bar", "this":"that"}}`)) + }) +} + +// HandleMetadataResetSuccessfully sets up the test server to respond to a metadata Create request. +func HandleMetadataResetSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/1234asdf/metadata", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, `{ + "metadata": { + "foo": "bar", + "this": "that" + } + }`) + + w.WriteHeader(http.StatusOK) + w.Header().Add("Content-Type", "application/json") + w.Write([]byte(`{ "metadata": {"foo":"bar", "this":"that"}}`)) + }) +} + +// HandleMetadataUpdateSuccessfully sets up the test server to respond to a metadata Update request. +func HandleMetadataUpdateSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/1234asdf/metadata", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, `{ + "metadata": { + "foo": "baz", + "this": "those" + } + }`) + + w.WriteHeader(http.StatusOK) + w.Header().Add("Content-Type", "application/json") + w.Write([]byte(`{ "metadata": {"foo":"baz", "this":"those"}}`)) + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/servers/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/servers/requests.go new file mode 100644 index 0000000000..79d7998937 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/servers/requests.go @@ -0,0 +1,726 @@ +package servers + +import ( + "encoding/base64" + "errors" + "fmt" + + "github.com/racker/perigee" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToServerListQuery() (string, error) +} + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the server attributes you want to see returned. Marker and Limit are used +// for pagination. +type ListOpts struct { + // A time/date stamp for when the server last changed status. + ChangesSince string `q:"changes-since"` + + // Name of the image in URL format. + Image string `q:"image"` + + // Name of the flavor in URL format. + Flavor string `q:"flavor"` + + // Name of the server as a string; can be queried with regular expressions. + // Realize that ?name=bob returns both bob and bobb. If you need to match bob + // only, you can use a regular expression matching the syntax of the + // underlying database server implemented for Compute. + Name string `q:"name"` + + // Value of the status of the server so that you can filter on "ACTIVE" for example. + Status string `q:"status"` + + // Name of the host as a string. + Host string `q:"host"` + + // UUID of the server at which you want to set a marker. + Marker string `q:"marker"` + + // Integer value for the limit of values to return. + Limit int `q:"limit"` +} + +// ToServerListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToServerListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + if err != nil { + return "", err + } + return q.String(), nil +} + +// List makes a request against the API to list servers accessible to you. +func List(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listDetailURL(client) + + if opts != nil { + query, err := opts.ToServerListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + + createPageFn := func(r pagination.PageResult) pagination.Page { + return ServerPage{pagination.LinkedPageBase{PageResult: r}} + } + + return pagination.NewPager(client, url, createPageFn) +} + +// CreateOptsBuilder describes struct types that can be accepted by the Create call. +// The CreateOpts struct in this package does. +type CreateOptsBuilder interface { + ToServerCreateMap() (map[string]interface{}, error) +} + +// Network is used within CreateOpts to control a new server's network attachments. +type Network struct { + // UUID of a nova-network to attach to the newly provisioned server. + // Required unless Port is provided. + UUID string + + // Port of a neutron network to attach to the newly provisioned server. + // Required unless UUID is provided. + Port string + + // FixedIP [optional] specifies a fixed IPv4 address to be used on this network. + FixedIP string +} + +// CreateOpts specifies server creation parameters. +type CreateOpts struct { + // Name [required] is the name to assign to the newly launched server. + Name string + + // ImageRef [required] is the ID or full URL to the image that contains the server's OS and initial state. + // Optional if using the boot-from-volume extension. + ImageRef string + + // FlavorRef [required] is the ID or full URL to the flavor that describes the server's specs. + FlavorRef string + + // SecurityGroups [optional] lists the names of the security groups to which this server should belong. + SecurityGroups []string + + // UserData [optional] contains configuration information or scripts to use upon launch. + // Create will base64-encode it for you. + UserData []byte + + // AvailabilityZone [optional] in which to launch the server. + AvailabilityZone string + + // Networks [optional] dictates how this server will be attached to available networks. + // By default, the server will be attached to all isolated networks for the tenant. + Networks []Network + + // Metadata [optional] contains key-value pairs (up to 255 bytes each) to attach to the server. + Metadata map[string]string + + // Personality [optional] includes the path and contents of a file to inject into the server at launch. + // The maximum size of the file is 255 bytes (decoded). + Personality []byte + + // ConfigDrive [optional] enables metadata injection through a configuration drive. + ConfigDrive bool + + // AdminPass [optional] sets the root user password. If not set, a randomly-generated + // password will be created and returned in the response. + AdminPass string +} + +// ToServerCreateMap assembles a request body based on the contents of a CreateOpts. +func (opts CreateOpts) ToServerCreateMap() (map[string]interface{}, error) { + server := make(map[string]interface{}) + + server["name"] = opts.Name + server["imageRef"] = opts.ImageRef + server["flavorRef"] = opts.FlavorRef + + if opts.UserData != nil { + encoded := base64.StdEncoding.EncodeToString(opts.UserData) + server["user_data"] = &encoded + } + if opts.Personality != nil { + encoded := base64.StdEncoding.EncodeToString(opts.Personality) + server["personality"] = &encoded + } + if opts.ConfigDrive { + server["config_drive"] = "true" + } + if opts.AvailabilityZone != "" { + server["availability_zone"] = opts.AvailabilityZone + } + if opts.Metadata != nil { + server["metadata"] = opts.Metadata + } + if opts.AdminPass != "" { + server["adminPass"] = opts.AdminPass + } + + if len(opts.SecurityGroups) > 0 { + securityGroups := make([]map[string]interface{}, len(opts.SecurityGroups)) + for i, groupName := range opts.SecurityGroups { + securityGroups[i] = map[string]interface{}{"name": groupName} + } + server["security_groups"] = securityGroups + } + + if len(opts.Networks) > 0 { + networks := make([]map[string]interface{}, len(opts.Networks)) + for i, net := range opts.Networks { + networks[i] = make(map[string]interface{}) + if net.UUID != "" { + networks[i]["uuid"] = net.UUID + } + if net.Port != "" { + networks[i]["port"] = net.Port + } + if net.FixedIP != "" { + networks[i]["fixed_ip"] = net.FixedIP + } + } + server["networks"] = networks + } + + return map[string]interface{}{"server": server}, nil +} + +// Create requests a server to be provisioned to the user in the current tenant. +func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult { + var res CreateResult + + reqBody, err := opts.ToServerCreateMap() + if err != nil { + res.Err = err + return res + } + + _, res.Err = perigee.Request("POST", listURL(client), perigee.Options{ + Results: &res.Body, + ReqBody: reqBody, + MoreHeaders: client.AuthenticatedHeaders(), + OkCodes: []int{202}, + }) + return res +} + +// Delete requests that a server previously provisioned be removed from your account. +func Delete(client *gophercloud.ServiceClient, id string) DeleteResult { + var res DeleteResult + _, res.Err = perigee.Request("DELETE", deleteURL(client, id), perigee.Options{ + MoreHeaders: client.AuthenticatedHeaders(), + OkCodes: []int{204}, + }) + return res +} + +// Get requests details on a single server, by ID. +func Get(client *gophercloud.ServiceClient, id string) GetResult { + var result GetResult + _, result.Err = perigee.Request("GET", getURL(client, id), perigee.Options{ + Results: &result.Body, + MoreHeaders: client.AuthenticatedHeaders(), + }) + return result +} + +// UpdateOptsBuilder allows extensions to add additional attributes to the Update request. +type UpdateOptsBuilder interface { + ToServerUpdateMap() map[string]interface{} +} + +// UpdateOpts specifies the base attributes that may be updated on an existing server. +type UpdateOpts struct { + // Name [optional] changes the displayed name of the server. + // The server host name will *not* change. + // Server names are not constrained to be unique, even within the same tenant. + Name string + + // AccessIPv4 [optional] provides a new IPv4 address for the instance. + AccessIPv4 string + + // AccessIPv6 [optional] provides a new IPv6 address for the instance. + AccessIPv6 string +} + +// ToServerUpdateMap formats an UpdateOpts structure into a request body. +func (opts UpdateOpts) ToServerUpdateMap() map[string]interface{} { + server := make(map[string]string) + if opts.Name != "" { + server["name"] = opts.Name + } + if opts.AccessIPv4 != "" { + server["accessIPv4"] = opts.AccessIPv4 + } + if opts.AccessIPv6 != "" { + server["accessIPv6"] = opts.AccessIPv6 + } + return map[string]interface{}{"server": server} +} + +// Update requests that various attributes of the indicated server be changed. +func Update(client *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) UpdateResult { + var result UpdateResult + _, result.Err = perigee.Request("PUT", updateURL(client, id), perigee.Options{ + Results: &result.Body, + ReqBody: opts.ToServerUpdateMap(), + MoreHeaders: client.AuthenticatedHeaders(), + }) + return result +} + +// ChangeAdminPassword alters the administrator or root password for a specified server. +func ChangeAdminPassword(client *gophercloud.ServiceClient, id, newPassword string) ActionResult { + var req struct { + ChangePassword struct { + AdminPass string `json:"adminPass"` + } `json:"changePassword"` + } + + req.ChangePassword.AdminPass = newPassword + + var res ActionResult + + _, res.Err = perigee.Request("POST", actionURL(client, id), perigee.Options{ + ReqBody: req, + MoreHeaders: client.AuthenticatedHeaders(), + OkCodes: []int{202}, + }) + + return res +} + +// ErrArgument errors occur when an argument supplied to a package function +// fails to fall within acceptable values. For example, the Reboot() function +// expects the "how" parameter to be one of HardReboot or SoftReboot. These +// constants are (currently) strings, leading someone to wonder if they can pass +// other string values instead, perhaps in an effort to break the API of their +// provider. Reboot() returns this error in this situation. +// +// Function identifies which function was called/which function is generating +// the error. +// Argument identifies which formal argument was responsible for producing the +// error. +// Value provides the value as it was passed into the function. +type ErrArgument struct { + Function, Argument string + Value interface{} +} + +// Error yields a useful diagnostic for debugging purposes. +func (e *ErrArgument) Error() string { + return fmt.Sprintf("Bad argument in call to %s, formal parameter %s, value %#v", e.Function, e.Argument, e.Value) +} + +func (e *ErrArgument) String() string { + return e.Error() +} + +// RebootMethod describes the mechanisms by which a server reboot can be requested. +type RebootMethod string + +// These constants determine how a server should be rebooted. +// See the Reboot() function for further details. +const ( + SoftReboot RebootMethod = "SOFT" + HardReboot RebootMethod = "HARD" + OSReboot = SoftReboot + PowerCycle = HardReboot +) + +// Reboot requests that a given server reboot. +// Two methods exist for rebooting a server: +// +// HardReboot (aka PowerCycle) restarts the server instance by physically cutting power to the machine, or if a VM, +// terminating it at the hypervisor level. +// It's done. Caput. Full stop. +// Then, after a brief while, power is restored or the VM instance restarted. +// +// SoftReboot (aka OSReboot) simply tells the OS to restart under its own procedures. +// E.g., in Linux, asking it to enter runlevel 6, or executing "sudo shutdown -r now", or by asking Windows to restart the machine. +func Reboot(client *gophercloud.ServiceClient, id string, how RebootMethod) ActionResult { + var res ActionResult + + if (how != SoftReboot) && (how != HardReboot) { + res.Err = &ErrArgument{ + Function: "Reboot", + Argument: "how", + Value: how, + } + return res + } + + _, res.Err = perigee.Request("POST", actionURL(client, id), perigee.Options{ + ReqBody: struct { + C map[string]string `json:"reboot"` + }{ + map[string]string{"type": string(how)}, + }, + MoreHeaders: client.AuthenticatedHeaders(), + OkCodes: []int{202}, + }) + + return res +} + +// RebuildOptsBuilder is an interface that allows extensions to override the +// default behaviour of rebuild options +type RebuildOptsBuilder interface { + ToServerRebuildMap() (map[string]interface{}, error) +} + +// RebuildOpts represents the configuration options used in a server rebuild +// operation +type RebuildOpts struct { + // Required. The ID of the image you want your server to be provisioned on + ImageID string + + // Name to set the server to + Name string + + // Required. The server's admin password + AdminPass string + + // AccessIPv4 [optional] provides a new IPv4 address for the instance. + AccessIPv4 string + + // AccessIPv6 [optional] provides a new IPv6 address for the instance. + AccessIPv6 string + + // Metadata [optional] contains key-value pairs (up to 255 bytes each) to attach to the server. + Metadata map[string]string + + // Personality [optional] includes the path and contents of a file to inject into the server at launch. + // The maximum size of the file is 255 bytes (decoded). + Personality []byte +} + +// ToServerRebuildMap formats a RebuildOpts struct into a map for use in JSON +func (opts RebuildOpts) ToServerRebuildMap() (map[string]interface{}, error) { + var err error + server := make(map[string]interface{}) + + if opts.AdminPass == "" { + err = fmt.Errorf("AdminPass is required") + } + + if opts.ImageID == "" { + err = fmt.Errorf("ImageID is required") + } + + if err != nil { + return server, err + } + + server["name"] = opts.Name + server["adminPass"] = opts.AdminPass + server["imageRef"] = opts.ImageID + + if opts.AccessIPv4 != "" { + server["accessIPv4"] = opts.AccessIPv4 + } + + if opts.AccessIPv6 != "" { + server["accessIPv6"] = opts.AccessIPv6 + } + + if opts.Metadata != nil { + server["metadata"] = opts.Metadata + } + + if opts.Personality != nil { + encoded := base64.StdEncoding.EncodeToString(opts.Personality) + server["personality"] = &encoded + } + + return map[string]interface{}{"rebuild": server}, nil +} + +// Rebuild will reprovision the server according to the configuration options +// provided in the RebuildOpts struct. +func Rebuild(client *gophercloud.ServiceClient, id string, opts RebuildOptsBuilder) RebuildResult { + var result RebuildResult + + if id == "" { + result.Err = fmt.Errorf("ID is required") + return result + } + + reqBody, err := opts.ToServerRebuildMap() + if err != nil { + result.Err = err + return result + } + + _, result.Err = perigee.Request("POST", actionURL(client, id), perigee.Options{ + ReqBody: &reqBody, + Results: &result.Body, + MoreHeaders: client.AuthenticatedHeaders(), + OkCodes: []int{202}, + }) + + return result +} + +// ResizeOptsBuilder is an interface that allows extensions to override the default structure of +// a Resize request. +type ResizeOptsBuilder interface { + ToServerResizeMap() (map[string]interface{}, error) +} + +// ResizeOpts represents the configuration options used to control a Resize operation. +type ResizeOpts struct { + // FlavorRef is the ID of the flavor you wish your server to become. + FlavorRef string +} + +// ToServerResizeMap formats a ResizeOpts as a map that can be used as a JSON request body for the +// Resize request. +func (opts ResizeOpts) ToServerResizeMap() (map[string]interface{}, error) { + resize := map[string]interface{}{ + "flavorRef": opts.FlavorRef, + } + + return map[string]interface{}{"resize": resize}, nil +} + +// Resize instructs the provider to change the flavor of the server. +// Note that this implies rebuilding it. +// Unfortunately, one cannot pass rebuild parameters to the resize function. +// When the resize completes, the server will be in RESIZE_VERIFY state. +// While in this state, you can explore the use of the new server's configuration. +// If you like it, call ConfirmResize() to commit the resize permanently. +// Otherwise, call RevertResize() to restore the old configuration. +func Resize(client *gophercloud.ServiceClient, id string, opts ResizeOptsBuilder) ActionResult { + var res ActionResult + reqBody, err := opts.ToServerResizeMap() + if err != nil { + res.Err = err + return res + } + + _, res.Err = perigee.Request("POST", actionURL(client, id), perigee.Options{ + ReqBody: reqBody, + MoreHeaders: client.AuthenticatedHeaders(), + OkCodes: []int{202}, + }) + + return res +} + +// ConfirmResize confirms a previous resize operation on a server. +// See Resize() for more details. +func ConfirmResize(client *gophercloud.ServiceClient, id string) ActionResult { + var res ActionResult + + _, res.Err = perigee.Request("POST", actionURL(client, id), perigee.Options{ + ReqBody: map[string]interface{}{"confirmResize": nil}, + MoreHeaders: client.AuthenticatedHeaders(), + OkCodes: []int{204}, + }) + + return res +} + +// RevertResize cancels a previous resize operation on a server. +// See Resize() for more details. +func RevertResize(client *gophercloud.ServiceClient, id string) ActionResult { + var res ActionResult + + _, res.Err = perigee.Request("POST", actionURL(client, id), perigee.Options{ + ReqBody: map[string]interface{}{"revertResize": nil}, + MoreHeaders: client.AuthenticatedHeaders(), + OkCodes: []int{202}, + }) + + return res +} + +// RescueOptsBuilder is an interface that allows extensions to override the +// default structure of a Rescue request. +type RescueOptsBuilder interface { + ToServerRescueMap() (map[string]interface{}, error) +} + +// RescueOpts represents the configuration options used to control a Rescue +// option. +type RescueOpts struct { + // AdminPass is the desired administrative password for the instance in + // RESCUE mode. If it's left blank, the server will generate a password. + AdminPass string +} + +// ToServerRescueMap formats a RescueOpts as a map that can be used as a JSON +// request body for the Rescue request. +func (opts RescueOpts) ToServerRescueMap() (map[string]interface{}, error) { + server := make(map[string]interface{}) + if opts.AdminPass != "" { + server["adminPass"] = opts.AdminPass + } + return map[string]interface{}{"rescue": server}, nil +} + +// Rescue instructs the provider to place the server into RESCUE mode. +func Rescue(client *gophercloud.ServiceClient, id string, opts RescueOptsBuilder) RescueResult { + var result RescueResult + + if id == "" { + result.Err = fmt.Errorf("ID is required") + return result + } + reqBody, err := opts.ToServerRescueMap() + if err != nil { + result.Err = err + return result + } + + _, result.Err = perigee.Request("POST", actionURL(client, id), perigee.Options{ + Results: &result.Body, + ReqBody: &reqBody, + MoreHeaders: client.AuthenticatedHeaders(), + OkCodes: []int{200}, + }) + + return result +} + +// ResetMetadataOptsBuilder allows extensions to add additional parameters to the +// Reset request. +type ResetMetadataOptsBuilder interface { + ToMetadataResetMap() (map[string]interface{}, error) +} + +// MetadataOpts is a map that contains key-value pairs. +type MetadataOpts map[string]string + +// ToMetadataResetMap assembles a body for a Reset request based on the contents of a MetadataOpts. +func (opts MetadataOpts) ToMetadataResetMap() (map[string]interface{}, error) { + return map[string]interface{}{"metadata": opts}, nil +} + +// ToMetadataUpdateMap assembles a body for an Update request based on the contents of a MetadataOpts. +func (opts MetadataOpts) ToMetadataUpdateMap() (map[string]interface{}, error) { + return map[string]interface{}{"metadata": opts}, nil +} + +// ResetMetadata will create multiple new key-value pairs for the given server ID. +// Note: Using this operation will erase any already-existing metadata and create +// the new metadata provided. To keep any already-existing metadata, use the +// UpdateMetadatas or UpdateMetadata function. +func ResetMetadata(client *gophercloud.ServiceClient, id string, opts ResetMetadataOptsBuilder) ResetMetadataResult { + var res ResetMetadataResult + metadata, err := opts.ToMetadataResetMap() + if err != nil { + res.Err = err + return res + } + _, res.Err = perigee.Request("PUT", metadataURL(client, id), perigee.Options{ + ReqBody: metadata, + Results: &res.Body, + MoreHeaders: client.AuthenticatedHeaders(), + }) + return res +} + +// Metadata requests all the metadata for the given server ID. +func Metadata(client *gophercloud.ServiceClient, id string) GetMetadataResult { + var res GetMetadataResult + _, res.Err = perigee.Request("GET", metadataURL(client, id), perigee.Options{ + Results: &res.Body, + MoreHeaders: client.AuthenticatedHeaders(), + }) + return res +} + +// UpdateMetadataOptsBuilder allows extensions to add additional parameters to the +// Create request. +type UpdateMetadataOptsBuilder interface { + ToMetadataUpdateMap() (map[string]interface{}, error) +} + +// UpdateMetadata updates (or creates) all the metadata specified by opts for the given server ID. +// This operation does not affect already-existing metadata that is not specified +// by opts. +func UpdateMetadata(client *gophercloud.ServiceClient, id string, opts UpdateMetadataOptsBuilder) UpdateMetadataResult { + var res UpdateMetadataResult + metadata, err := opts.ToMetadataUpdateMap() + if err != nil { + res.Err = err + return res + } + _, res.Err = perigee.Request("POST", metadataURL(client, id), perigee.Options{ + ReqBody: metadata, + Results: &res.Body, + MoreHeaders: client.AuthenticatedHeaders(), + }) + return res +} + +// MetadatumOptsBuilder allows extensions to add additional parameters to the +// Create request. +type MetadatumOptsBuilder interface { + ToMetadatumCreateMap() (map[string]interface{}, string, error) +} + +// MetadatumOpts is a map of length one that contains a key-value pair. +type MetadatumOpts map[string]string + +// ToMetadatumCreateMap assembles a body for a Create request based on the contents of a MetadataumOpts. +func (opts MetadatumOpts) ToMetadatumCreateMap() (map[string]interface{}, string, error) { + if len(opts) != 1 { + return nil, "", errors.New("CreateMetadatum operation must have 1 and only 1 key-value pair.") + } + metadatum := map[string]interface{}{"meta": opts} + var key string + for k := range metadatum["meta"].(MetadatumOpts) { + key = k + } + return metadatum, key, nil +} + +// CreateMetadatum will create or update the key-value pair with the given key for the given server ID. +func CreateMetadatum(client *gophercloud.ServiceClient, id string, opts MetadatumOptsBuilder) CreateMetadatumResult { + var res CreateMetadatumResult + metadatum, key, err := opts.ToMetadatumCreateMap() + if err != nil { + res.Err = err + return res + } + + _, res.Err = perigee.Request("PUT", metadatumURL(client, id, key), perigee.Options{ + ReqBody: metadatum, + Results: &res.Body, + MoreHeaders: client.AuthenticatedHeaders(), + }) + return res +} + +// Metadatum requests the key-value pair with the given key for the given server ID. +func Metadatum(client *gophercloud.ServiceClient, id, key string) GetMetadatumResult { + var res GetMetadatumResult + _, res.Err = perigee.Request("GET", metadatumURL(client, id, key), perigee.Options{ + Results: &res.Body, + MoreHeaders: client.AuthenticatedHeaders(), + }) + return res +} + +// DeleteMetadatum will delete the key-value pair with the given key for the given server ID. +func DeleteMetadatum(client *gophercloud.ServiceClient, id, key string) DeleteMetadatumResult { + var res DeleteMetadatumResult + _, res.Err = perigee.Request("DELETE", metadatumURL(client, id, key), perigee.Options{ + Results: &res.Body, + MoreHeaders: client.AuthenticatedHeaders(), + }) + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/servers/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/servers/requests_test.go new file mode 100644 index 0000000000..017e793ccd --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/servers/requests_test.go @@ -0,0 +1,266 @@ +package servers + +import ( + "net/http" + "testing" + + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestListServers(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleServerListSuccessfully(t) + + pages := 0 + err := List(client.ServiceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + pages++ + + actual, err := ExtractServers(page) + if err != nil { + return false, err + } + + if len(actual) != 2 { + t.Fatalf("Expected 2 servers, got %d", len(actual)) + } + th.CheckDeepEquals(t, ServerHerp, actual[0]) + th.CheckDeepEquals(t, ServerDerp, actual[1]) + + return true, nil + }) + + th.AssertNoErr(t, err) + + if pages != 1 { + t.Errorf("Expected 1 page, saw %d", pages) + } +} + +func TestCreateServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleServerCreationSuccessfully(t, SingleServerBody) + + actual, err := Create(client.ServiceClient(), CreateOpts{ + Name: "derp", + ImageRef: "f90f6034-2570-4974-8351-6b49732ef2eb", + FlavorRef: "1", + }).Extract() + th.AssertNoErr(t, err) + + th.CheckDeepEquals(t, ServerDerp, *actual) +} + +func TestDeleteServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleServerDeletionSuccessfully(t) + + res := Delete(client.ServiceClient(), "asdfasdfasdf") + th.AssertNoErr(t, res.Err) +} + +func TestGetServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleServerGetSuccessfully(t) + + client := client.ServiceClient() + actual, err := Get(client, "1234asdf").Extract() + if err != nil { + t.Fatalf("Unexpected Get error: %v", err) + } + + th.CheckDeepEquals(t, ServerDerp, *actual) +} + +func TestUpdateServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleServerUpdateSuccessfully(t) + + client := client.ServiceClient() + actual, err := Update(client, "1234asdf", UpdateOpts{Name: "new-name"}).Extract() + if err != nil { + t.Fatalf("Unexpected Update error: %v", err) + } + + th.CheckDeepEquals(t, ServerDerp, *actual) +} + +func TestChangeServerAdminPassword(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleAdminPasswordChangeSuccessfully(t) + + res := ChangeAdminPassword(client.ServiceClient(), "1234asdf", "new-password") + th.AssertNoErr(t, res.Err) +} + +func TestRebootServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleRebootSuccessfully(t) + + res := Reboot(client.ServiceClient(), "1234asdf", SoftReboot) + th.AssertNoErr(t, res.Err) +} + +func TestRebuildServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleRebuildSuccessfully(t, SingleServerBody) + + opts := RebuildOpts{ + Name: "new-name", + AdminPass: "swordfish", + ImageID: "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/images/f90f6034-2570-4974-8351-6b49732ef2eb", + AccessIPv4: "1.2.3.4", + } + + actual, err := Rebuild(client.ServiceClient(), "1234asdf", opts).Extract() + th.AssertNoErr(t, err) + + th.CheckDeepEquals(t, ServerDerp, *actual) +} + +func TestResizeServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/servers/1234asdf/action", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, `{ "resize": { "flavorRef": "2" } }`) + + w.WriteHeader(http.StatusAccepted) + }) + + res := Resize(client.ServiceClient(), "1234asdf", ResizeOpts{FlavorRef: "2"}) + th.AssertNoErr(t, res.Err) +} + +func TestConfirmResize(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/servers/1234asdf/action", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, `{ "confirmResize": null }`) + + w.WriteHeader(http.StatusNoContent) + }) + + res := ConfirmResize(client.ServiceClient(), "1234asdf") + th.AssertNoErr(t, res.Err) +} + +func TestRevertResize(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/servers/1234asdf/action", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, `{ "revertResize": null }`) + + w.WriteHeader(http.StatusAccepted) + }) + + res := RevertResize(client.ServiceClient(), "1234asdf") + th.AssertNoErr(t, res.Err) +} + +func TestRescue(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleServerRescueSuccessfully(t) + + res := Rescue(client.ServiceClient(), "1234asdf", RescueOpts{ + AdminPass: "1234567890", + }) + th.AssertNoErr(t, res.Err) + adminPass, _ := res.Extract() + th.AssertEquals(t, "1234567890", adminPass) +} + +func TestGetMetadatum(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleMetadatumGetSuccessfully(t) + + expected := map[string]string{"foo": "bar"} + actual, err := Metadatum(client.ServiceClient(), "1234asdf", "foo").Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, expected, actual) +} + +func TestCreateMetadatum(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleMetadatumCreateSuccessfully(t) + + expected := map[string]string{"foo": "bar"} + actual, err := CreateMetadatum(client.ServiceClient(), "1234asdf", MetadatumOpts{"foo": "bar"}).Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, expected, actual) +} + +func TestDeleteMetadatum(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleMetadatumDeleteSuccessfully(t) + + err := DeleteMetadatum(client.ServiceClient(), "1234asdf", "foo").ExtractErr() + th.AssertNoErr(t, err) +} + +func TestGetMetadata(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleMetadataGetSuccessfully(t) + + expected := map[string]string{"foo": "bar", "this": "that"} + actual, err := Metadata(client.ServiceClient(), "1234asdf").Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, expected, actual) +} + +func TestResetMetadata(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleMetadataResetSuccessfully(t) + + expected := map[string]string{"foo": "bar", "this": "that"} + actual, err := ResetMetadata(client.ServiceClient(), "1234asdf", MetadataOpts{ + "foo": "bar", + "this": "that", + }).Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, expected, actual) +} + +func TestUpdateMetadata(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleMetadataUpdateSuccessfully(t) + + expected := map[string]string{"foo": "baz", "this": "those"} + actual, err := UpdateMetadata(client.ServiceClient(), "1234asdf", MetadataOpts{ + "foo": "baz", + "this": "those", + }).Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, expected, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/servers/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/servers/results.go new file mode 100644 index 0000000000..3a145f8007 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/servers/results.go @@ -0,0 +1,237 @@ +package servers + +import ( + "github.com/mitchellh/mapstructure" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +type serverResult struct { + gophercloud.Result +} + +// Extract interprets any serverResult as a Server, if possible. +func (r serverResult) Extract() (*Server, error) { + if r.Err != nil { + return nil, r.Err + } + + var response struct { + Server Server `mapstructure:"server"` + } + + err := mapstructure.Decode(r.Body, &response) + return &response.Server, err +} + +// CreateResult temporarily contains the response from a Create call. +type CreateResult struct { + serverResult +} + +// GetResult temporarily contains the response from a Get call. +type GetResult struct { + serverResult +} + +// UpdateResult temporarily contains the response from an Update call. +type UpdateResult struct { + serverResult +} + +// DeleteResult temporarily contains the response from a Delete call. +type DeleteResult struct { + gophercloud.ErrResult +} + +// RebuildResult temporarily contains the response from a Rebuild call. +type RebuildResult struct { + serverResult +} + +// ActionResult represents the result of server action operations, like reboot +type ActionResult struct { + gophercloud.ErrResult +} + +// RescueResult represents the result of a server rescue operation +type RescueResult struct { + ActionResult +} + +// Extract interprets any RescueResult as an AdminPass, if possible. +func (r RescueResult) Extract() (string, error) { + if r.Err != nil { + return "", r.Err + } + + var response struct { + AdminPass string `mapstructure:"adminPass"` + } + + err := mapstructure.Decode(r.Body, &response) + return response.AdminPass, err +} + +// Server exposes only the standard OpenStack fields corresponding to a given server on the user's account. +type Server struct { + // ID uniquely identifies this server amongst all other servers, including those not accessible to the current tenant. + ID string + + // TenantID identifies the tenant owning this server resource. + TenantID string `mapstructure:"tenant_id"` + + // UserID uniquely identifies the user account owning the tenant. + UserID string `mapstructure:"user_id"` + + // Name contains the human-readable name for the server. + Name string + + // Updated and Created contain ISO-8601 timestamps of when the state of the server last changed, and when it was created. + Updated string + Created string + + HostID string + + // Status contains the current operational status of the server, such as IN_PROGRESS or ACTIVE. + Status string + + // Progress ranges from 0..100. + // A request made against the server completes only once Progress reaches 100. + Progress int + + // AccessIPv4 and AccessIPv6 contain the IP addresses of the server, suitable for remote access for administration. + AccessIPv4, AccessIPv6 string + + // Image refers to a JSON object, which itself indicates the OS image used to deploy the server. + Image map[string]interface{} + + // Flavor refers to a JSON object, which itself indicates the hardware configuration of the deployed server. + Flavor map[string]interface{} + + // Addresses includes a list of all IP addresses assigned to the server, keyed by pool. + Addresses map[string]interface{} + + // Metadata includes a list of all user-specified key-value pairs attached to the server. + Metadata map[string]interface{} + + // Links includes HTTP references to the itself, useful for passing along to other APIs that might want a server reference. + Links []interface{} + + // KeyName indicates which public key was injected into the server on launch. + KeyName string `json:"key_name" mapstructure:"key_name"` + + // AdminPass will generally be empty (""). However, it will contain the administrative password chosen when provisioning a new server without a set AdminPass setting in the first place. + // Note that this is the ONLY time this field will be valid. + AdminPass string `json:"adminPass" mapstructure:"adminPass"` +} + +// ServerPage abstracts the raw results of making a List() request against the API. +// As OpenStack extensions may freely alter the response bodies of structures returned to the client, you may only safely access the +// data provided through the ExtractServers call. +type ServerPage struct { + pagination.LinkedPageBase +} + +// IsEmpty returns true if a page contains no Server results. +func (page ServerPage) IsEmpty() (bool, error) { + servers, err := ExtractServers(page) + if err != nil { + return true, err + } + return len(servers) == 0, nil +} + +// NextPageURL uses the response's embedded link reference to navigate to the next page of results. +func (page ServerPage) NextPageURL() (string, error) { + type resp struct { + Links []gophercloud.Link `mapstructure:"servers_links"` + } + + var r resp + err := mapstructure.Decode(page.Body, &r) + if err != nil { + return "", err + } + + return gophercloud.ExtractNextURL(r.Links) +} + +// ExtractServers interprets the results of a single page from a List() call, producing a slice of Server entities. +func ExtractServers(page pagination.Page) ([]Server, error) { + casted := page.(ServerPage).Body + + var response struct { + Servers []Server `mapstructure:"servers"` + } + err := mapstructure.Decode(casted, &response) + return response.Servers, err +} + +// MetadataResult contains the result of a call for (potentially) multiple key-value pairs. +type MetadataResult struct { + gophercloud.Result +} + +// GetMetadataResult temporarily contains the response from a metadata Get call. +type GetMetadataResult struct { + MetadataResult +} + +// ResetMetadataResult temporarily contains the response from a metadata Reset call. +type ResetMetadataResult struct { + MetadataResult +} + +// UpdateMetadataResult temporarily contains the response from a metadata Update call. +type UpdateMetadataResult struct { + MetadataResult +} + +// MetadatumResult contains the result of a call for individual a single key-value pair. +type MetadatumResult struct { + gophercloud.Result +} + +// GetMetadatumResult temporarily contains the response from a metadatum Get call. +type GetMetadatumResult struct { + MetadatumResult +} + +// CreateMetadatumResult temporarily contains the response from a metadatum Create call. +type CreateMetadatumResult struct { + MetadatumResult +} + +// DeleteMetadatumResult temporarily contains the response from a metadatum Delete call. +type DeleteMetadatumResult struct { + gophercloud.ErrResult +} + +// Extract interprets any MetadataResult as a Metadata, if possible. +func (r MetadataResult) Extract() (map[string]string, error) { + if r.Err != nil { + return nil, r.Err + } + + var response struct { + Metadata map[string]string `mapstructure:"metadata"` + } + + err := mapstructure.Decode(r.Body, &response) + return response.Metadata, err +} + +// Extract interprets any MetadatumResult as a Metadatum, if possible. +func (r MetadatumResult) Extract() (map[string]string, error) { + if r.Err != nil { + return nil, r.Err + } + + var response struct { + Metadatum map[string]string `mapstructure:"meta"` + } + + err := mapstructure.Decode(r.Body, &response) + return response.Metadatum, err +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/servers/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/servers/urls.go new file mode 100644 index 0000000000..4bc6586a50 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/servers/urls.go @@ -0,0 +1,39 @@ +package servers + +import "github.com/rackspace/gophercloud" + +func createURL(client *gophercloud.ServiceClient) string { + return client.ServiceURL("servers") +} + +func listURL(client *gophercloud.ServiceClient) string { + return createURL(client) +} + +func listDetailURL(client *gophercloud.ServiceClient) string { + return client.ServiceURL("servers", "detail") +} + +func deleteURL(client *gophercloud.ServiceClient, id string) string { + return client.ServiceURL("servers", id) +} + +func getURL(client *gophercloud.ServiceClient, id string) string { + return deleteURL(client, id) +} + +func updateURL(client *gophercloud.ServiceClient, id string) string { + return deleteURL(client, id) +} + +func actionURL(client *gophercloud.ServiceClient, id string) string { + return client.ServiceURL("servers", id, "action") +} + +func metadatumURL(client *gophercloud.ServiceClient, id, key string) string { + return client.ServiceURL("servers", id, "metadata", key) +} + +func metadataURL(client *gophercloud.ServiceClient, id string) string { + return client.ServiceURL("servers", id, "metadata") +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/servers/urls_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/servers/urls_test.go new file mode 100644 index 0000000000..17a1d287f2 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/servers/urls_test.go @@ -0,0 +1,68 @@ +package servers + +import ( + "testing" + + "github.com/rackspace/gophercloud" + th "github.com/rackspace/gophercloud/testhelper" +) + +const endpoint = "http://localhost:57909" + +func endpointClient() *gophercloud.ServiceClient { + return &gophercloud.ServiceClient{Endpoint: endpoint} +} + +func TestCreateURL(t *testing.T) { + actual := createURL(endpointClient()) + expected := endpoint + "servers" + th.CheckEquals(t, expected, actual) +} + +func TestListURL(t *testing.T) { + actual := listURL(endpointClient()) + expected := endpoint + "servers" + th.CheckEquals(t, expected, actual) +} + +func TestListDetailURL(t *testing.T) { + actual := listDetailURL(endpointClient()) + expected := endpoint + "servers/detail" + th.CheckEquals(t, expected, actual) +} + +func TestDeleteURL(t *testing.T) { + actual := deleteURL(endpointClient(), "foo") + expected := endpoint + "servers/foo" + th.CheckEquals(t, expected, actual) +} + +func TestGetURL(t *testing.T) { + actual := getURL(endpointClient(), "foo") + expected := endpoint + "servers/foo" + th.CheckEquals(t, expected, actual) +} + +func TestUpdateURL(t *testing.T) { + actual := updateURL(endpointClient(), "foo") + expected := endpoint + "servers/foo" + th.CheckEquals(t, expected, actual) +} + +func TestActionURL(t *testing.T) { + actual := actionURL(endpointClient(), "foo") + expected := endpoint + "servers/foo/action" + th.CheckEquals(t, expected, actual) +} + +func TestMetadatumURL(t *testing.T) { + actual := metadatumURL(endpointClient(), "foo", "bar") + expected := endpoint + "servers/foo/metadata/bar" + th.CheckEquals(t, expected, actual) +} + +func TestMetadataURL(t *testing.T) { + actual := metadataURL(endpointClient(), "foo") + expected := endpoint + "servers/foo/metadata" + th.CheckEquals(t, expected, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/servers/util.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/servers/util.go new file mode 100644 index 0000000000..e6baf74165 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/servers/util.go @@ -0,0 +1,20 @@ +package servers + +import "github.com/rackspace/gophercloud" + +// WaitForStatus will continually poll a server until it successfully transitions to a specified +// status. It will do this for at most the number of seconds specified. +func WaitForStatus(c *gophercloud.ServiceClient, id, status string, secs int) error { + return gophercloud.WaitFor(secs, func() (bool, error) { + current, err := Get(c, id).Extract() + if err != nil { + return false, err + } + + if current.Status == status { + return true, nil + } + + return false, nil + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/endpoint_location.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/endpoint_location.go new file mode 100644 index 0000000000..5a311e4085 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/endpoint_location.go @@ -0,0 +1,124 @@ +package openstack + +import ( + "fmt" + + "github.com/rackspace/gophercloud" + tokens2 "github.com/rackspace/gophercloud/openstack/identity/v2/tokens" + endpoints3 "github.com/rackspace/gophercloud/openstack/identity/v3/endpoints" + services3 "github.com/rackspace/gophercloud/openstack/identity/v3/services" + "github.com/rackspace/gophercloud/pagination" +) + +// V2EndpointURL discovers the endpoint URL for a specific service from a ServiceCatalog acquired +// during the v2 identity service. The specified EndpointOpts are used to identify a unique, +// unambiguous endpoint to return. It's an error both when multiple endpoints match the provided +// criteria and when none do. The minimum that can be specified is a Type, but you will also often +// need to specify a Name and/or a Region depending on what's available on your OpenStack +// deployment. +func V2EndpointURL(catalog *tokens2.ServiceCatalog, opts gophercloud.EndpointOpts) (string, error) { + // Extract Endpoints from the catalog entries that match the requested Type, Name if provided, and Region if provided. + var endpoints = make([]tokens2.Endpoint, 0, 1) + for _, entry := range catalog.Entries { + if (entry.Type == opts.Type) && (opts.Name == "" || entry.Name == opts.Name) { + for _, endpoint := range entry.Endpoints { + if opts.Region == "" || endpoint.Region == opts.Region { + endpoints = append(endpoints, endpoint) + } + } + } + } + + // Report an error if the options were ambiguous. + if len(endpoints) > 1 { + return "", fmt.Errorf("Discovered %d matching endpoints: %#v", len(endpoints), endpoints) + } + + // Extract the appropriate URL from the matching Endpoint. + for _, endpoint := range endpoints { + switch opts.Availability { + case gophercloud.AvailabilityPublic: + return gophercloud.NormalizeURL(endpoint.PublicURL), nil + case gophercloud.AvailabilityInternal: + return gophercloud.NormalizeURL(endpoint.InternalURL), nil + case gophercloud.AvailabilityAdmin: + return gophercloud.NormalizeURL(endpoint.AdminURL), nil + default: + return "", fmt.Errorf("Unexpected availability in endpoint query: %s", opts.Availability) + } + } + + // Report an error if there were no matching endpoints. + return "", gophercloud.ErrEndpointNotFound +} + +// V3EndpointURL discovers the endpoint URL for a specific service using multiple calls against +// an identity v3 service endpoint. The specified EndpointOpts are used to identify a unique, +// unambiguous endpoint to return. It's an error both when multiple endpoints match the provided +// criteria and when none do. The minimum that can be specified is a Type, but you will also often +// need to specify a Name and/or a Region depending on what's available on your OpenStack +// deployment. +func V3EndpointURL(v3Client *gophercloud.ServiceClient, opts gophercloud.EndpointOpts) (string, error) { + // Discover the service we're interested in. + var services = make([]services3.Service, 0, 1) + servicePager := services3.List(v3Client, services3.ListOpts{ServiceType: opts.Type}) + err := servicePager.EachPage(func(page pagination.Page) (bool, error) { + part, err := services3.ExtractServices(page) + if err != nil { + return false, err + } + + for _, service := range part { + if service.Name == opts.Name { + services = append(services, service) + } + } + + return true, nil + }) + if err != nil { + return "", err + } + + if len(services) == 0 { + return "", gophercloud.ErrServiceNotFound + } + if len(services) > 1 { + return "", fmt.Errorf("Discovered %d matching services: %#v", len(services), services) + } + service := services[0] + + // Enumerate the endpoints available for this service. + var endpoints []endpoints3.Endpoint + endpointPager := endpoints3.List(v3Client, endpoints3.ListOpts{ + Availability: opts.Availability, + ServiceID: service.ID, + }) + err = endpointPager.EachPage(func(page pagination.Page) (bool, error) { + part, err := endpoints3.ExtractEndpoints(page) + if err != nil { + return false, err + } + + for _, endpoint := range part { + if opts.Region == "" || endpoint.Region == opts.Region { + endpoints = append(endpoints, endpoint) + } + } + + return true, nil + }) + if err != nil { + return "", err + } + + if len(endpoints) == 0 { + return "", gophercloud.ErrEndpointNotFound + } + if len(endpoints) > 1 { + return "", fmt.Errorf("Discovered %d matching endpoints: %#v", len(endpoints), endpoints) + } + endpoint := endpoints[0] + + return gophercloud.NormalizeURL(endpoint.URL), nil +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/endpoint_location_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/endpoint_location_test.go new file mode 100644 index 0000000000..4e0569ac1f --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/endpoint_location_test.go @@ -0,0 +1,225 @@ +package openstack + +import ( + "fmt" + "net/http" + "strings" + "testing" + + "github.com/rackspace/gophercloud" + tokens2 "github.com/rackspace/gophercloud/openstack/identity/v2/tokens" + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +// Service catalog fixtures take too much vertical space! +var catalog2 = tokens2.ServiceCatalog{ + Entries: []tokens2.CatalogEntry{ + tokens2.CatalogEntry{ + Type: "same", + Name: "same", + Endpoints: []tokens2.Endpoint{ + tokens2.Endpoint{ + Region: "same", + PublicURL: "https://public.correct.com/", + InternalURL: "https://internal.correct.com/", + AdminURL: "https://admin.correct.com/", + }, + tokens2.Endpoint{ + Region: "different", + PublicURL: "https://badregion.com/", + }, + }, + }, + tokens2.CatalogEntry{ + Type: "same", + Name: "different", + Endpoints: []tokens2.Endpoint{ + tokens2.Endpoint{ + Region: "same", + PublicURL: "https://badname.com/", + }, + tokens2.Endpoint{ + Region: "different", + PublicURL: "https://badname.com/+badregion", + }, + }, + }, + tokens2.CatalogEntry{ + Type: "different", + Name: "different", + Endpoints: []tokens2.Endpoint{ + tokens2.Endpoint{ + Region: "same", + PublicURL: "https://badtype.com/+badname", + }, + tokens2.Endpoint{ + Region: "different", + PublicURL: "https://badtype.com/+badregion+badname", + }, + }, + }, + }, +} + +func TestV2EndpointExact(t *testing.T) { + expectedURLs := map[gophercloud.Availability]string{ + gophercloud.AvailabilityPublic: "https://public.correct.com/", + gophercloud.AvailabilityAdmin: "https://admin.correct.com/", + gophercloud.AvailabilityInternal: "https://internal.correct.com/", + } + + for availability, expected := range expectedURLs { + actual, err := V2EndpointURL(&catalog2, gophercloud.EndpointOpts{ + Type: "same", + Name: "same", + Region: "same", + Availability: availability, + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, expected, actual) + } +} + +func TestV2EndpointNone(t *testing.T) { + _, err := V2EndpointURL(&catalog2, gophercloud.EndpointOpts{ + Type: "nope", + Availability: gophercloud.AvailabilityPublic, + }) + th.CheckEquals(t, gophercloud.ErrEndpointNotFound, err) +} + +func TestV2EndpointMultiple(t *testing.T) { + _, err := V2EndpointURL(&catalog2, gophercloud.EndpointOpts{ + Type: "same", + Region: "same", + Availability: gophercloud.AvailabilityPublic, + }) + if !strings.HasPrefix(err.Error(), "Discovered 2 matching endpoints:") { + t.Errorf("Received unexpected error: %v", err) + } +} + +func TestV2EndpointBadAvailability(t *testing.T) { + _, err := V2EndpointURL(&catalog2, gophercloud.EndpointOpts{ + Type: "same", + Name: "same", + Region: "same", + Availability: "wat", + }) + th.CheckEquals(t, err.Error(), "Unexpected availability in endpoint query: wat") +} + +func setupV3Responses(t *testing.T) { + // Mock the service query. + th.Mux.HandleFunc("/services", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, ` + { + "links": { + "next": null, + "previous": null + }, + "services": [ + { + "description": "Correct", + "id": "1234", + "name": "same", + "type": "same" + }, + { + "description": "Bad Name", + "id": "9876", + "name": "different", + "type": "same" + } + ] + } + `) + }) + + // Mock the endpoint query. + th.Mux.HandleFunc("/endpoints", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestFormValues(t, r, map[string]string{ + "service_id": "1234", + "interface": "public", + }) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, ` + { + "endpoints": [ + { + "id": "12", + "interface": "public", + "name": "the-right-one", + "region": "same", + "service_id": "1234", + "url": "https://correct:9000/" + }, + { + "id": "14", + "interface": "public", + "name": "bad-region", + "region": "different", + "service_id": "1234", + "url": "https://bad-region:9001/" + } + ], + "links": { + "next": null, + "previous": null + } + } + `) + }) +} + +func TestV3EndpointExact(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + setupV3Responses(t) + + actual, err := V3EndpointURL(fake.ServiceClient(), gophercloud.EndpointOpts{ + Type: "same", + Name: "same", + Region: "same", + Availability: gophercloud.AvailabilityPublic, + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, actual, "https://correct:9000/") +} + +func TestV3EndpointNoService(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/services", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, ` + { + "links": { + "next": null, + "previous": null + }, + "services": [] + } + `) + }) + + _, err := V3EndpointURL(fake.ServiceClient(), gophercloud.EndpointOpts{ + Type: "nope", + Name: "same", + Region: "same", + Availability: gophercloud.AvailabilityPublic, + }) + th.CheckEquals(t, gophercloud.ErrServiceNotFound, err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/extensions/admin/roles/docs.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/extensions/admin/roles/docs.go new file mode 100644 index 0000000000..8954178716 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/extensions/admin/roles/docs.go @@ -0,0 +1,16 @@ +// Package roles provides functionality to interact with and control roles on +// the API. +// +// A role represents a personality that a user can assume when performing a +// specific set of operations. If a role includes a set of rights and +// privileges, a user assuming that role inherits those rights and privileges. +// +// When a token is generated, the list of roles that user can assume is returned +// back to them. Services that are being called by that user determine how they +// interpret the set of roles a user has and to which operations or resources +// each role grants access. +// +// It is up to individual services such as Compute or Image to assign meaning +// to these roles. As far as the Identity service is concerned, a role is an +// arbitrary name assigned by the user. +package roles diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/extensions/admin/roles/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/extensions/admin/roles/fixtures.go new file mode 100644 index 0000000000..8256f0fe8e --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/extensions/admin/roles/fixtures.go @@ -0,0 +1,48 @@ +package roles + +import ( + "fmt" + "net/http" + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +func MockListRoleResponse(t *testing.T) { + th.Mux.HandleFunc("/OS-KSADM/roles", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "roles": [ + { + "id": "123", + "name": "compute:admin", + "description": "Nova Administrator" + } + ] +} + `) + }) +} + +func MockAddUserRoleResponse(t *testing.T) { + th.Mux.HandleFunc("/tenants/{tenant_id}/users/{user_id}/roles/OS-KSADM/{role_id}", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusCreated) + }) +} + +func MockDeleteUserRoleResponse(t *testing.T) { + th.Mux.HandleFunc("/tenants/{tenant_id}/users/{user_id}/roles/OS-KSADM/{role_id}", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusNoContent) + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/extensions/admin/roles/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/extensions/admin/roles/requests.go new file mode 100644 index 0000000000..152031ac35 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/extensions/admin/roles/requests.go @@ -0,0 +1,44 @@ +package roles + +import ( + "github.com/racker/perigee" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// List is the operation responsible for listing all available global roles +// that a user can adopt. +func List(client *gophercloud.ServiceClient) pagination.Pager { + createPage := func(r pagination.PageResult) pagination.Page { + return RolePage{pagination.SinglePageBase(r)} + } + return pagination.NewPager(client, rootURL(client), createPage) +} + +// AddUserRole is the operation responsible for assigning a particular role to +// a user. This is confined to the scope of the user's tenant - so the tenant +// ID is a required argument. +func AddUserRole(client *gophercloud.ServiceClient, tenantID, userID, roleID string) UserRoleResult { + var result UserRoleResult + + _, result.Err = perigee.Request("PUT", userRoleURL(client, tenantID, userID, roleID), perigee.Options{ + MoreHeaders: client.AuthenticatedHeaders(), + OkCodes: []int{200, 201}, + }) + + return result +} + +// DeleteUserRole is the operation responsible for deleting a particular role +// from a user. This is confined to the scope of the user's tenant - so the +// tenant ID is a required argument. +func DeleteUserRole(client *gophercloud.ServiceClient, tenantID, userID, roleID string) UserRoleResult { + var result UserRoleResult + + _, result.Err = perigee.Request("DELETE", userRoleURL(client, tenantID, userID, roleID), perigee.Options{ + MoreHeaders: client.AuthenticatedHeaders(), + OkCodes: []int{204}, + }) + + return result +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/extensions/admin/roles/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/extensions/admin/roles/requests_test.go new file mode 100644 index 0000000000..7bfeea44a8 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/extensions/admin/roles/requests_test.go @@ -0,0 +1,64 @@ +package roles + +import ( + "testing" + + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestRole(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockListRoleResponse(t) + + count := 0 + + err := List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractRoles(page) + if err != nil { + t.Errorf("Failed to extract users: %v", err) + return false, err + } + + expected := []Role{ + Role{ + ID: "123", + Name: "compute:admin", + Description: "Nova Administrator", + }, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + + th.AssertNoErr(t, err) + th.AssertEquals(t, 1, count) +} + +func TestAddUserRole(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockAddUserRoleResponse(t) + + err := AddUserRole(client.ServiceClient(), "{tenant_id}", "{user_id}", "{role_id}").ExtractErr() + + th.AssertNoErr(t, err) +} + +func TestDeleteUserRole(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockDeleteUserRoleResponse(t) + + err := DeleteUserRole(client.ServiceClient(), "{tenant_id}", "{user_id}", "{role_id}").ExtractErr() + + th.AssertNoErr(t, err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/extensions/admin/roles/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/extensions/admin/roles/results.go new file mode 100644 index 0000000000..ebb3aa530b --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/extensions/admin/roles/results.go @@ -0,0 +1,53 @@ +package roles + +import ( + "github.com/mitchellh/mapstructure" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// Role represents an API role resource. +type Role struct { + // The unique ID for the role. + ID string + + // The human-readable name of the role. + Name string + + // The description of the role. + Description string + + // The associated service for this role. + ServiceID string +} + +// RolePage is a single page of a user Role collection. +type RolePage struct { + pagination.SinglePageBase +} + +// IsEmpty determines whether or not a page of Tenants contains any results. +func (page RolePage) IsEmpty() (bool, error) { + users, err := ExtractRoles(page) + if err != nil { + return false, err + } + return len(users) == 0, nil +} + +// ExtractRoles returns a slice of roles contained in a single page of results. +func ExtractRoles(page pagination.Page) ([]Role, error) { + casted := page.(RolePage).Body + var response struct { + Roles []Role `mapstructure:"roles"` + } + + err := mapstructure.Decode(casted, &response) + return response.Roles, err +} + +// UserRoleResult represents the result of either an AddUserRole or +// a DeleteUserRole operation. +type UserRoleResult struct { + gophercloud.ErrResult +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/extensions/admin/roles/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/extensions/admin/roles/urls.go new file mode 100644 index 0000000000..61b31551dd --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/extensions/admin/roles/urls.go @@ -0,0 +1,21 @@ +package roles + +import "github.com/rackspace/gophercloud" + +const ( + ExtPath = "OS-KSADM" + RolePath = "roles" + UserPath = "users" +) + +func resourceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(ExtPath, RolePath, id) +} + +func rootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(ExtPath, RolePath) +} + +func userRoleURL(c *gophercloud.ServiceClient, tenantID, userID, roleID string) string { + return c.ServiceURL("tenants", tenantID, UserPath, userID, RolePath, ExtPath, roleID) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/extensions/delegate.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/extensions/delegate.go new file mode 100644 index 0000000000..fd6e80ea6f --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/extensions/delegate.go @@ -0,0 +1,52 @@ +package extensions + +import ( + "github.com/mitchellh/mapstructure" + "github.com/rackspace/gophercloud" + common "github.com/rackspace/gophercloud/openstack/common/extensions" + "github.com/rackspace/gophercloud/pagination" +) + +// ExtensionPage is a single page of Extension results. +type ExtensionPage struct { + common.ExtensionPage +} + +// IsEmpty returns true if the current page contains at least one Extension. +func (page ExtensionPage) IsEmpty() (bool, error) { + is, err := ExtractExtensions(page) + if err != nil { + return true, err + } + return len(is) == 0, nil +} + +// ExtractExtensions accepts a Page struct, specifically an ExtensionPage struct, and extracts the +// elements into a slice of Extension structs. +func ExtractExtensions(page pagination.Page) ([]common.Extension, error) { + // Identity v2 adds an intermediate "values" object. + + var resp struct { + Extensions struct { + Values []common.Extension `mapstructure:"values"` + } `mapstructure:"extensions"` + } + + err := mapstructure.Decode(page.(ExtensionPage).Body, &resp) + return resp.Extensions.Values, err +} + +// Get retrieves information for a specific extension using its alias. +func Get(c *gophercloud.ServiceClient, alias string) common.GetResult { + return common.Get(c, alias) +} + +// List returns a Pager which allows you to iterate over the full collection of extensions. +// It does not accept query parameters. +func List(c *gophercloud.ServiceClient) pagination.Pager { + return common.List(c).WithPageCreator(func(r pagination.PageResult) pagination.Page { + return ExtensionPage{ + ExtensionPage: common.ExtensionPage{SinglePageBase: pagination.SinglePageBase(r)}, + } + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/extensions/delegate_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/extensions/delegate_test.go new file mode 100644 index 0000000000..504118a825 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/extensions/delegate_test.go @@ -0,0 +1,38 @@ +package extensions + +import ( + "testing" + + common "github.com/rackspace/gophercloud/openstack/common/extensions" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListExtensionsSuccessfully(t) + + count := 0 + err := List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractExtensions(page) + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, common.ExpectedExtensions, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, 1, count) +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + common.HandleGetExtensionSuccessfully(t) + + actual, err := Get(client.ServiceClient(), "agent").Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, common.SingleExtension, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/extensions/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/extensions/doc.go new file mode 100644 index 0000000000..791e4e391d --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/extensions/doc.go @@ -0,0 +1,3 @@ +// Package extensions provides information and interaction with the +// different extensions available for the OpenStack Identity service. +package extensions diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/extensions/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/extensions/fixtures.go new file mode 100644 index 0000000000..96cb7d24a1 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/extensions/fixtures.go @@ -0,0 +1,60 @@ +// +build fixtures + +package extensions + +import ( + "fmt" + "net/http" + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +// ListOutput provides a single Extension result. It differs from the delegated implementation +// by the introduction of an intermediate "values" member. +const ListOutput = ` +{ + "extensions": { + "values": [ + { + "updated": "2013-01-20T00:00:00-00:00", + "name": "Neutron Service Type Management", + "links": [], + "namespace": "http://docs.openstack.org/ext/neutron/service-type/api/v1.0", + "alias": "service-type", + "description": "API for retrieving service providers for Neutron advanced services" + } + ] + } +} +` + +// HandleListExtensionsSuccessfully creates an HTTP handler that returns ListOutput for a List +// call. +func HandleListExtensionsSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/extensions", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + + fmt.Fprintf(w, ` +{ + "extensions": { + "values": [ + { + "updated": "2013-01-20T00:00:00-00:00", + "name": "Neutron Service Type Management", + "links": [], + "namespace": "http://docs.openstack.org/ext/neutron/service-type/api/v1.0", + "alias": "service-type", + "description": "API for retrieving service providers for Neutron advanced services" + } + ] + } +} + `) + }) + +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tenants/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tenants/doc.go new file mode 100644 index 0000000000..0c2d49d567 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tenants/doc.go @@ -0,0 +1,7 @@ +// Package tenants provides information and interaction with the +// tenants API resource for the OpenStack Identity service. +// +// See http://developer.openstack.org/api-ref-identity-v2.html#identity-auth-v2 +// and http://developer.openstack.org/api-ref-identity-v2.html#admin-tenants +// for more information. +package tenants diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tenants/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tenants/fixtures.go new file mode 100644 index 0000000000..7f044ac3b2 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tenants/fixtures.go @@ -0,0 +1,65 @@ +// +build fixtures + +package tenants + +import ( + "fmt" + "net/http" + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +// ListOutput provides a single page of Tenant results. +const ListOutput = ` +{ + "tenants": [ + { + "id": "1234", + "name": "Red Team", + "description": "The team that is red", + "enabled": true + }, + { + "id": "9876", + "name": "Blue Team", + "description": "The team that is blue", + "enabled": false + } + ] +} +` + +// RedTeam is a Tenant fixture. +var RedTeam = Tenant{ + ID: "1234", + Name: "Red Team", + Description: "The team that is red", + Enabled: true, +} + +// BlueTeam is a Tenant fixture. +var BlueTeam = Tenant{ + ID: "9876", + Name: "Blue Team", + Description: "The team that is blue", + Enabled: false, +} + +// ExpectedTenantSlice is the slice of tenants expected to be returned from ListOutput. +var ExpectedTenantSlice = []Tenant{RedTeam, BlueTeam} + +// HandleListTenantsSuccessfully creates an HTTP handler at `/tenants` on the test handler mux that +// responds with a list of two tenants. +func HandleListTenantsSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/tenants", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "Accept", "application/json") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, ListOutput) + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tenants/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tenants/requests.go new file mode 100644 index 0000000000..5a359f5c9e --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tenants/requests.go @@ -0,0 +1,33 @@ +package tenants + +import ( + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// ListOpts filters the Tenants that are returned by the List call. +type ListOpts struct { + // Marker is the ID of the last Tenant on the previous page. + Marker string `q:"marker"` + + // Limit specifies the page size. + Limit int `q:"limit"` +} + +// List enumerates the Tenants to which the current token has access. +func List(client *gophercloud.ServiceClient, opts *ListOpts) pagination.Pager { + createPage := func(r pagination.PageResult) pagination.Page { + return TenantPage{pagination.LinkedPageBase{PageResult: r}} + } + + url := listURL(client) + if opts != nil { + q, err := gophercloud.BuildQueryString(opts) + if err != nil { + return pagination.Pager{Err: err} + } + url += q.String() + } + + return pagination.NewPager(client, url, createPage) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tenants/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tenants/requests_test.go new file mode 100644 index 0000000000..e8f172dd18 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tenants/requests_test.go @@ -0,0 +1,29 @@ +package tenants + +import ( + "testing" + + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestListTenants(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListTenantsSuccessfully(t) + + count := 0 + err := List(client.ServiceClient(), nil).EachPage(func(page pagination.Page) (bool, error) { + count++ + + actual, err := ExtractTenants(page) + th.AssertNoErr(t, err) + + th.CheckDeepEquals(t, ExpectedTenantSlice, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, count, 1) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tenants/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tenants/results.go new file mode 100644 index 0000000000..c1220c384b --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tenants/results.go @@ -0,0 +1,62 @@ +package tenants + +import ( + "github.com/mitchellh/mapstructure" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// Tenant is a grouping of users in the identity service. +type Tenant struct { + // ID is a unique identifier for this tenant. + ID string `mapstructure:"id"` + + // Name is a friendlier user-facing name for this tenant. + Name string `mapstructure:"name"` + + // Description is a human-readable explanation of this Tenant's purpose. + Description string `mapstructure:"description"` + + // Enabled indicates whether or not a tenant is active. + Enabled bool `mapstructure:"enabled"` +} + +// TenantPage is a single page of Tenant results. +type TenantPage struct { + pagination.LinkedPageBase +} + +// IsEmpty determines whether or not a page of Tenants contains any results. +func (page TenantPage) IsEmpty() (bool, error) { + tenants, err := ExtractTenants(page) + if err != nil { + return false, err + } + return len(tenants) == 0, nil +} + +// NextPageURL extracts the "next" link from the tenants_links section of the result. +func (page TenantPage) NextPageURL() (string, error) { + type resp struct { + Links []gophercloud.Link `mapstructure:"tenants_links"` + } + + var r resp + err := mapstructure.Decode(page.Body, &r) + if err != nil { + return "", err + } + + return gophercloud.ExtractNextURL(r.Links) +} + +// ExtractTenants returns a slice of Tenants contained in a single page of results. +func ExtractTenants(page pagination.Page) ([]Tenant, error) { + casted := page.(TenantPage).Body + var response struct { + Tenants []Tenant `mapstructure:"tenants"` + } + + err := mapstructure.Decode(casted, &response) + return response.Tenants, err +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tenants/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tenants/urls.go new file mode 100644 index 0000000000..1dd6ce023f --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tenants/urls.go @@ -0,0 +1,7 @@ +package tenants + +import "github.com/rackspace/gophercloud" + +func listURL(client *gophercloud.ServiceClient) string { + return client.ServiceURL("tenants") +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tokens/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tokens/doc.go new file mode 100644 index 0000000000..31cacc5e17 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tokens/doc.go @@ -0,0 +1,5 @@ +// Package tokens provides information and interaction with the token API +// resource for the OpenStack Identity service. +// For more information, see: +// http://developer.openstack.org/api-ref-identity-v2.html#identity-auth-v2 +package tokens diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tokens/errors.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tokens/errors.go new file mode 100644 index 0000000000..3a9172e0cc --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tokens/errors.go @@ -0,0 +1,30 @@ +package tokens + +import ( + "errors" + "fmt" +) + +var ( + // ErrUserIDProvided is returned if you attempt to authenticate with a UserID. + ErrUserIDProvided = unacceptedAttributeErr("UserID") + + // ErrAPIKeyProvided is returned if you attempt to authenticate with an APIKey. + ErrAPIKeyProvided = unacceptedAttributeErr("APIKey") + + // ErrDomainIDProvided is returned if you attempt to authenticate with a DomainID. + ErrDomainIDProvided = unacceptedAttributeErr("DomainID") + + // ErrDomainNameProvided is returned if you attempt to authenticate with a DomainName. + ErrDomainNameProvided = unacceptedAttributeErr("DomainName") + + // ErrUsernameRequired is returned if you attempt ot authenticate without a Username. + ErrUsernameRequired = errors.New("You must supply a Username in your AuthOptions.") + + // ErrPasswordRequired is returned if you don't provide a password. + ErrPasswordRequired = errors.New("Please supply a Password in your AuthOptions.") +) + +func unacceptedAttributeErr(attribute string) error { + return fmt.Errorf("The base Identity V2 API does not accept authentication by %s", attribute) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tokens/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tokens/fixtures.go new file mode 100644 index 0000000000..1cb0d0527b --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tokens/fixtures.go @@ -0,0 +1,128 @@ +// +build fixtures + +package tokens + +import ( + "fmt" + "net/http" + "testing" + "time" + + "github.com/rackspace/gophercloud/openstack/identity/v2/tenants" + th "github.com/rackspace/gophercloud/testhelper" +) + +// ExpectedToken is the token that should be parsed from TokenCreationResponse. +var ExpectedToken = &Token{ + ID: "aaaabbbbccccdddd", + ExpiresAt: time.Date(2014, time.January, 31, 15, 30, 58, 0, time.UTC), + Tenant: tenants.Tenant{ + ID: "fc394f2ab2df4114bde39905f800dc57", + Name: "test", + Description: "There are many tenants. This one is yours.", + Enabled: true, + }, +} + +// ExpectedServiceCatalog is the service catalog that should be parsed from TokenCreationResponse. +var ExpectedServiceCatalog = &ServiceCatalog{ + Entries: []CatalogEntry{ + CatalogEntry{ + Name: "inscrutablewalrus", + Type: "something", + Endpoints: []Endpoint{ + Endpoint{ + PublicURL: "http://something0:1234/v2/", + Region: "region0", + }, + Endpoint{ + PublicURL: "http://something1:1234/v2/", + Region: "region1", + }, + }, + }, + CatalogEntry{ + Name: "arbitrarypenguin", + Type: "else", + Endpoints: []Endpoint{ + Endpoint{ + PublicURL: "http://else0:4321/v3/", + Region: "region0", + }, + }, + }, + }, +} + +// TokenCreationResponse is a JSON response that contains ExpectedToken and ExpectedServiceCatalog. +const TokenCreationResponse = ` +{ + "access": { + "token": { + "issued_at": "2014-01-30T15:30:58.000000Z", + "expires": "2014-01-31T15:30:58Z", + "id": "aaaabbbbccccdddd", + "tenant": { + "description": "There are many tenants. This one is yours.", + "enabled": true, + "id": "fc394f2ab2df4114bde39905f800dc57", + "name": "test" + } + }, + "serviceCatalog": [ + { + "endpoints": [ + { + "publicURL": "http://something0:1234/v2/", + "region": "region0" + }, + { + "publicURL": "http://something1:1234/v2/", + "region": "region1" + } + ], + "type": "something", + "name": "inscrutablewalrus" + }, + { + "endpoints": [ + { + "publicURL": "http://else0:4321/v3/", + "region": "region0" + } + ], + "type": "else", + "name": "arbitrarypenguin" + } + ] + } +} +` + +// HandleTokenPost expects a POST against a /tokens handler, ensures that the request body has been +// constructed properly given certain auth options, and returns the result. +func HandleTokenPost(t *testing.T, requestJSON string) { + th.Mux.HandleFunc("/tokens", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + if requestJSON != "" { + th.TestJSONRequest(t, r, requestJSON) + } + + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, TokenCreationResponse) + }) +} + +// IsSuccessful ensures that a CreateResult was successful and contains the correct token and +// service catalog. +func IsSuccessful(t *testing.T, result CreateResult) { + token, err := result.ExtractToken() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, ExpectedToken, token) + + serviceCatalog, err := result.ExtractServiceCatalog() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, ExpectedServiceCatalog, serviceCatalog) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tokens/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tokens/requests.go new file mode 100644 index 0000000000..87c923a2b4 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tokens/requests.go @@ -0,0 +1,87 @@ +package tokens + +import ( + "github.com/racker/perigee" + "github.com/rackspace/gophercloud" +) + +// AuthOptionsBuilder describes any argument that may be passed to the Create call. +type AuthOptionsBuilder interface { + + // ToTokenCreateMap assembles the Create request body, returning an error if parameters are + // missing or inconsistent. + ToTokenCreateMap() (map[string]interface{}, error) +} + +// AuthOptions wraps a gophercloud AuthOptions in order to adhere to the AuthOptionsBuilder +// interface. +type AuthOptions struct { + gophercloud.AuthOptions +} + +// WrapOptions embeds a root AuthOptions struct in a package-specific one. +func WrapOptions(original gophercloud.AuthOptions) AuthOptions { + return AuthOptions{AuthOptions: original} +} + +// ToTokenCreateMap converts AuthOptions into nested maps that can be serialized into a JSON +// request. +func (auth AuthOptions) ToTokenCreateMap() (map[string]interface{}, error) { + // Error out if an unsupported auth option is present. + if auth.UserID != "" { + return nil, ErrUserIDProvided + } + if auth.APIKey != "" { + return nil, ErrAPIKeyProvided + } + if auth.DomainID != "" { + return nil, ErrDomainIDProvided + } + if auth.DomainName != "" { + return nil, ErrDomainNameProvided + } + + // Username and Password are always required. + if auth.Username == "" { + return nil, ErrUsernameRequired + } + if auth.Password == "" { + return nil, ErrPasswordRequired + } + + // Populate the request map. + authMap := make(map[string]interface{}) + + authMap["passwordCredentials"] = map[string]interface{}{ + "username": auth.Username, + "password": auth.Password, + } + + if auth.TenantID != "" { + authMap["tenantId"] = auth.TenantID + } + if auth.TenantName != "" { + authMap["tenantName"] = auth.TenantName + } + + return map[string]interface{}{"auth": authMap}, nil +} + +// Create authenticates to the identity service and attempts to acquire a Token. +// If successful, the CreateResult +// Generally, rather than interact with this call directly, end users should call openstack.AuthenticatedClient(), +// which abstracts all of the gory details about navigating service catalogs and such. +func Create(client *gophercloud.ServiceClient, auth AuthOptionsBuilder) CreateResult { + request, err := auth.ToTokenCreateMap() + if err != nil { + return CreateResult{gophercloud.Result{Err: err}} + } + + var result CreateResult + _, result.Err = perigee.Request("POST", CreateURL(client), perigee.Options{ + ReqBody: &request, + Results: &result.Body, + OkCodes: []int{200, 203}, + }) + return result +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tokens/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tokens/requests_test.go new file mode 100644 index 0000000000..2f02825a47 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tokens/requests_test.go @@ -0,0 +1,140 @@ +package tokens + +import ( + "testing" + + "github.com/rackspace/gophercloud" + th "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +func tokenPost(t *testing.T, options gophercloud.AuthOptions, requestJSON string) CreateResult { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleTokenPost(t, requestJSON) + + return Create(client.ServiceClient(), AuthOptions{options}) +} + +func tokenPostErr(t *testing.T, options gophercloud.AuthOptions, expectedErr error) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleTokenPost(t, "") + + actualErr := Create(client.ServiceClient(), AuthOptions{options}).Err + th.CheckEquals(t, expectedErr, actualErr) +} + +func TestCreateWithPassword(t *testing.T) { + options := gophercloud.AuthOptions{ + Username: "me", + Password: "swordfish", + } + + IsSuccessful(t, tokenPost(t, options, ` + { + "auth": { + "passwordCredentials": { + "username": "me", + "password": "swordfish" + } + } + } + `)) +} + +func TestCreateTokenWithTenantID(t *testing.T) { + options := gophercloud.AuthOptions{ + Username: "me", + Password: "opensesame", + TenantID: "fc394f2ab2df4114bde39905f800dc57", + } + + IsSuccessful(t, tokenPost(t, options, ` + { + "auth": { + "tenantId": "fc394f2ab2df4114bde39905f800dc57", + "passwordCredentials": { + "username": "me", + "password": "opensesame" + } + } + } + `)) +} + +func TestCreateTokenWithTenantName(t *testing.T) { + options := gophercloud.AuthOptions{ + Username: "me", + Password: "opensesame", + TenantName: "demo", + } + + IsSuccessful(t, tokenPost(t, options, ` + { + "auth": { + "tenantName": "demo", + "passwordCredentials": { + "username": "me", + "password": "opensesame" + } + } + } + `)) +} + +func TestProhibitUserID(t *testing.T) { + options := gophercloud.AuthOptions{ + Username: "me", + UserID: "1234", + Password: "thing", + } + + tokenPostErr(t, options, ErrUserIDProvided) +} + +func TestProhibitAPIKey(t *testing.T) { + options := gophercloud.AuthOptions{ + Username: "me", + Password: "thing", + APIKey: "123412341234", + } + + tokenPostErr(t, options, ErrAPIKeyProvided) +} + +func TestProhibitDomainID(t *testing.T) { + options := gophercloud.AuthOptions{ + Username: "me", + Password: "thing", + DomainID: "1234", + } + + tokenPostErr(t, options, ErrDomainIDProvided) +} + +func TestProhibitDomainName(t *testing.T) { + options := gophercloud.AuthOptions{ + Username: "me", + Password: "thing", + DomainName: "wat", + } + + tokenPostErr(t, options, ErrDomainNameProvided) +} + +func TestRequireUsername(t *testing.T) { + options := gophercloud.AuthOptions{ + Password: "thing", + } + + tokenPostErr(t, options, ErrUsernameRequired) +} + +func TestRequirePassword(t *testing.T) { + options := gophercloud.AuthOptions{ + Username: "me", + } + + tokenPostErr(t, options, ErrPasswordRequired) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tokens/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tokens/results.go new file mode 100644 index 0000000000..1eddb9d564 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tokens/results.go @@ -0,0 +1,133 @@ +package tokens + +import ( + "time" + + "github.com/mitchellh/mapstructure" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/openstack/identity/v2/tenants" +) + +// Token provides only the most basic information related to an authentication token. +type Token struct { + // ID provides the primary means of identifying a user to the OpenStack API. + // OpenStack defines this field as an opaque value, so do not depend on its content. + // It is safe, however, to compare for equality. + ID string + + // ExpiresAt provides a timestamp in ISO 8601 format, indicating when the authentication token becomes invalid. + // After this point in time, future API requests made using this authentication token will respond with errors. + // Either the caller will need to reauthenticate manually, or more preferably, the caller should exploit automatic re-authentication. + // See the AuthOptions structure for more details. + ExpiresAt time.Time + + // Tenant provides information about the tenant to which this token grants access. + Tenant tenants.Tenant +} + +// Endpoint represents a single API endpoint offered by a service. +// It provides the public and internal URLs, if supported, along with a region specifier, again if provided. +// The significance of the Region field will depend upon your provider. +// +// In addition, the interface offered by the service will have version information associated with it +// through the VersionId, VersionInfo, and VersionList fields, if provided or supported. +// +// In all cases, fields which aren't supported by the provider and service combined will assume a zero-value (""). +type Endpoint struct { + TenantID string `mapstructure:"tenantId"` + PublicURL string `mapstructure:"publicURL"` + InternalURL string `mapstructure:"internalURL"` + AdminURL string `mapstructure:"adminURL"` + Region string `mapstructure:"region"` + VersionID string `mapstructure:"versionId"` + VersionInfo string `mapstructure:"versionInfo"` + VersionList string `mapstructure:"versionList"` +} + +// CatalogEntry provides a type-safe interface to an Identity API V2 service catalog listing. +// Each class of service, such as cloud DNS or block storage services, will have a single +// CatalogEntry representing it. +// +// Note: when looking for the desired service, try, whenever possible, to key off the type field. +// Otherwise, you'll tie the representation of the service to a specific provider. +type CatalogEntry struct { + // Name will contain the provider-specified name for the service. + Name string `mapstructure:"name"` + + // Type will contain a type string if OpenStack defines a type for the service. + // Otherwise, for provider-specific services, the provider may assign their own type strings. + Type string `mapstructure:"type"` + + // Endpoints will let the caller iterate over all the different endpoints that may exist for + // the service. + Endpoints []Endpoint `mapstructure:"endpoints"` +} + +// ServiceCatalog provides a view into the service catalog from a previous, successful authentication. +type ServiceCatalog struct { + Entries []CatalogEntry +} + +// CreateResult defers the interpretation of a created token. +// Use ExtractToken() to interpret it as a Token, or ExtractServiceCatalog() to interpret it as a service catalog. +type CreateResult struct { + gophercloud.Result +} + +// ExtractToken returns the just-created Token from a CreateResult. +func (result CreateResult) ExtractToken() (*Token, error) { + if result.Err != nil { + return nil, result.Err + } + + var response struct { + Access struct { + Token struct { + Expires string `mapstructure:"expires"` + ID string `mapstructure:"id"` + Tenant tenants.Tenant `mapstructure:"tenant"` + } `mapstructure:"token"` + } `mapstructure:"access"` + } + + err := mapstructure.Decode(result.Body, &response) + if err != nil { + return nil, err + } + + expiresTs, err := time.Parse(gophercloud.RFC3339Milli, response.Access.Token.Expires) + if err != nil { + return nil, err + } + + return &Token{ + ID: response.Access.Token.ID, + ExpiresAt: expiresTs, + Tenant: response.Access.Token.Tenant, + }, nil +} + +// ExtractServiceCatalog returns the ServiceCatalog that was generated along with the user's Token. +func (result CreateResult) ExtractServiceCatalog() (*ServiceCatalog, error) { + if result.Err != nil { + return nil, result.Err + } + + var response struct { + Access struct { + Entries []CatalogEntry `mapstructure:"serviceCatalog"` + } `mapstructure:"access"` + } + + err := mapstructure.Decode(result.Body, &response) + if err != nil { + return nil, err + } + + return &ServiceCatalog{Entries: response.Access.Entries}, nil +} + +// createErr quickly packs an error in a CreateResult. +func createErr(err error) CreateResult { + return CreateResult{gophercloud.Result{Err: err}} +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tokens/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tokens/urls.go new file mode 100644 index 0000000000..cd4c696c7a --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tokens/urls.go @@ -0,0 +1,8 @@ +package tokens + +import "github.com/rackspace/gophercloud" + +// CreateURL generates the URL used to create new Tokens. +func CreateURL(client *gophercloud.ServiceClient) string { + return client.ServiceURL("tokens") +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/users/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/users/doc.go new file mode 100644 index 0000000000..82abcb9fcc --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/users/doc.go @@ -0,0 +1 @@ +package users diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/users/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/users/fixtures.go new file mode 100644 index 0000000000..8941868dd2 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/users/fixtures.go @@ -0,0 +1,163 @@ +package users + +import ( + "fmt" + "net/http" + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +func MockListUserResponse(t *testing.T) { + th.Mux.HandleFunc("/users", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "users":[ + { + "id": "u1000", + "name": "John Smith", + "username": "jqsmith", + "email": "john.smith@example.org", + "enabled": true, + "tenant_id": "12345" + }, + { + "id": "u1001", + "name": "Jane Smith", + "username": "jqsmith", + "email": "jane.smith@example.org", + "enabled": true, + "tenant_id": "12345" + } + ] +} + `) + }) +} + +func mockCreateUserResponse(t *testing.T) { + th.Mux.HandleFunc("/users", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + th.TestJSONRequest(t, r, ` +{ + "user": { + "name": "new_user", + "tenant_id": "12345", + "enabled": false, + "email": "new_user@foo.com" + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "user": { + "name": "new_user", + "tenant_id": "12345", + "enabled": false, + "email": "new_user@foo.com", + "id": "c39e3de9be2d4c779f1dfd6abacc176d" + } +} +`) + }) +} + +func mockGetUserResponse(t *testing.T) { + th.Mux.HandleFunc("/users/new_user", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "user": { + "name": "new_user", + "tenant_id": "12345", + "enabled": false, + "email": "new_user@foo.com", + "id": "c39e3de9be2d4c779f1dfd6abacc176d" + } +} +`) + }) +} + +func mockUpdateUserResponse(t *testing.T) { + th.Mux.HandleFunc("/users/c39e3de9be2d4c779f1dfd6abacc176d", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + th.TestJSONRequest(t, r, ` +{ + "user": { + "name": "new_name", + "enabled": true, + "email": "new_email@foo.com" + } +} +`) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "user": { + "name": "new_name", + "tenant_id": "12345", + "enabled": true, + "email": "new_email@foo.com", + "id": "c39e3de9be2d4c779f1dfd6abacc176d" + } +} +`) + }) +} + +func mockDeleteUserResponse(t *testing.T) { + th.Mux.HandleFunc("/users/c39e3de9be2d4c779f1dfd6abacc176d", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusNoContent) + }) +} + +func mockListRolesResponse(t *testing.T) { + th.Mux.HandleFunc("/tenants/1d8b6120dcc640fda4fc9194ffc80273/users/c39e3de9be2d4c779f1dfd6abacc176d/roles", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "roles": [ + { + "id": "9fe2ff9ee4384b1894a90878d3e92bab", + "name": "foo_role" + }, + { + "id": "1ea3d56793574b668e85960fbf651e13", + "name": "admin" + } + ] +} + `) + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/users/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/users/requests.go new file mode 100644 index 0000000000..4ce395f2dd --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/users/requests.go @@ -0,0 +1,180 @@ +package users + +import ( + "errors" + + "github.com/racker/perigee" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +func List(client *gophercloud.ServiceClient) pagination.Pager { + createPage := func(r pagination.PageResult) pagination.Page { + return UserPage{pagination.SinglePageBase(r)} + } + + return pagination.NewPager(client, rootURL(client), createPage) +} + +// EnabledState represents whether the user is enabled or not. +type EnabledState *bool + +// Useful variables to use when creating or updating users. +var ( + iTrue = true + iFalse = false + + Enabled EnabledState = &iTrue + Disabled EnabledState = &iFalse +) + +// CommonOpts are the parameters that are shared between CreateOpts and +// UpdateOpts +type CommonOpts struct { + // Either a name or username is required. When provided, the value must be + // unique or a 409 conflict error will be returned. If you provide a name but + // omit a username, the latter will be set to the former; and vice versa. + Name, Username string + + // The ID of the tenant to which you want to assign this user. + TenantID string + + // Indicates whether this user is enabled or not. + Enabled EnabledState + + // The email address of this user. + Email string +} + +// CreateOpts represents the options needed when creating new users. +type CreateOpts CommonOpts + +// CreateOptsBuilder describes struct types that can be accepted by the Create call. +type CreateOptsBuilder interface { + ToUserCreateMap() (map[string]interface{}, error) +} + +// ToUserCreateMap assembles a request body based on the contents of a CreateOpts. +func (opts CreateOpts) ToUserCreateMap() (map[string]interface{}, error) { + m := make(map[string]interface{}) + + if opts.Name == "" && opts.Username == "" { + return m, errors.New("Either a Name or Username must be provided") + } + + if opts.Name != "" { + m["name"] = opts.Name + } + if opts.Username != "" { + m["username"] = opts.Username + } + if opts.Enabled != nil { + m["enabled"] = &opts.Enabled + } + if opts.Email != "" { + m["email"] = opts.Email + } + if opts.TenantID != "" { + m["tenant_id"] = opts.TenantID + } + + return map[string]interface{}{"user": m}, nil +} + +// Create is the operation responsible for creating new users. +func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult { + var res CreateResult + + reqBody, err := opts.ToUserCreateMap() + if err != nil { + res.Err = err + return res + } + + _, res.Err = perigee.Request("POST", rootURL(client), perigee.Options{ + Results: &res.Body, + ReqBody: reqBody, + MoreHeaders: client.AuthenticatedHeaders(), + OkCodes: []int{200, 201}, + }) + + return res +} + +// Get requests details on a single user, either by ID. +func Get(client *gophercloud.ServiceClient, id string) GetResult { + var result GetResult + + _, result.Err = perigee.Request("GET", ResourceURL(client, id), perigee.Options{ + Results: &result.Body, + MoreHeaders: client.AuthenticatedHeaders(), + OkCodes: []int{200}, + }) + + return result +} + +// UpdateOptsBuilder allows extensions to add additional attributes to the Update request. +type UpdateOptsBuilder interface { + ToUserUpdateMap() map[string]interface{} +} + +// UpdateOpts specifies the base attributes that may be updated on an existing server. +type UpdateOpts CommonOpts + +// ToUserUpdateMap formats an UpdateOpts structure into a request body. +func (opts UpdateOpts) ToUserUpdateMap() map[string]interface{} { + m := make(map[string]interface{}) + + if opts.Name != "" { + m["name"] = opts.Name + } + if opts.Username != "" { + m["username"] = opts.Username + } + if opts.Enabled != nil { + m["enabled"] = &opts.Enabled + } + if opts.Email != "" { + m["email"] = opts.Email + } + if opts.TenantID != "" { + m["tenant_id"] = opts.TenantID + } + + return map[string]interface{}{"user": m} +} + +// Update is the operation responsible for updating exist users by their UUID. +func Update(client *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) UpdateResult { + var result UpdateResult + + _, result.Err = perigee.Request("PUT", ResourceURL(client, id), perigee.Options{ + Results: &result.Body, + ReqBody: opts.ToUserUpdateMap(), + MoreHeaders: client.AuthenticatedHeaders(), + OkCodes: []int{200}, + }) + + return result +} + +// Delete is the operation responsible for permanently deleting an API user. +func Delete(client *gophercloud.ServiceClient, id string) DeleteResult { + var result DeleteResult + + _, result.Err = perigee.Request("DELETE", ResourceURL(client, id), perigee.Options{ + MoreHeaders: client.AuthenticatedHeaders(), + OkCodes: []int{204}, + }) + + return result +} + +func ListRoles(client *gophercloud.ServiceClient, tenantID, userID string) pagination.Pager { + createPage := func(r pagination.PageResult) pagination.Page { + return RolePage{pagination.SinglePageBase(r)} + } + + return pagination.NewPager(client, listRolesURL(client, tenantID, userID), createPage) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/users/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/users/requests_test.go new file mode 100644 index 0000000000..04f837163a --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/users/requests_test.go @@ -0,0 +1,165 @@ +package users + +import ( + "testing" + + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockListUserResponse(t) + + count := 0 + + err := List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractUsers(page) + if err != nil { + t.Errorf("Failed to extract users: %v", err) + return false, err + } + + expected := []User{ + User{ + ID: "u1000", + Name: "John Smith", + Username: "jqsmith", + Email: "john.smith@example.org", + Enabled: true, + TenantID: "12345", + }, + User{ + ID: "u1001", + Name: "Jane Smith", + Username: "jqsmith", + Email: "jane.smith@example.org", + Enabled: true, + TenantID: "12345", + }, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + + th.AssertNoErr(t, err) + th.AssertEquals(t, 1, count) +} + +func TestCreateUser(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockCreateUserResponse(t) + + opts := CreateOpts{ + Name: "new_user", + TenantID: "12345", + Enabled: Disabled, + Email: "new_user@foo.com", + } + + user, err := Create(client.ServiceClient(), opts).Extract() + + th.AssertNoErr(t, err) + + expected := &User{ + Name: "new_user", + ID: "c39e3de9be2d4c779f1dfd6abacc176d", + Email: "new_user@foo.com", + Enabled: false, + TenantID: "12345", + } + + th.AssertDeepEquals(t, expected, user) +} + +func TestGetUser(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockGetUserResponse(t) + + user, err := Get(client.ServiceClient(), "new_user").Extract() + th.AssertNoErr(t, err) + + expected := &User{ + Name: "new_user", + ID: "c39e3de9be2d4c779f1dfd6abacc176d", + Email: "new_user@foo.com", + Enabled: false, + TenantID: "12345", + } + + th.AssertDeepEquals(t, expected, user) +} + +func TestUpdateUser(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockUpdateUserResponse(t) + + id := "c39e3de9be2d4c779f1dfd6abacc176d" + opts := UpdateOpts{ + Name: "new_name", + Enabled: Enabled, + Email: "new_email@foo.com", + } + + user, err := Update(client.ServiceClient(), id, opts).Extract() + + th.AssertNoErr(t, err) + + expected := &User{ + Name: "new_name", + ID: id, + Email: "new_email@foo.com", + Enabled: true, + TenantID: "12345", + } + + th.AssertDeepEquals(t, expected, user) +} + +func TestDeleteUser(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockDeleteUserResponse(t) + + res := Delete(client.ServiceClient(), "c39e3de9be2d4c779f1dfd6abacc176d") + th.AssertNoErr(t, res.Err) +} + +func TestListingUserRoles(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockListRolesResponse(t) + + tenantID := "1d8b6120dcc640fda4fc9194ffc80273" + userID := "c39e3de9be2d4c779f1dfd6abacc176d" + + err := ListRoles(client.ServiceClient(), tenantID, userID).EachPage(func(page pagination.Page) (bool, error) { + actual, err := ExtractRoles(page) + th.AssertNoErr(t, err) + + expected := []Role{ + Role{ID: "9fe2ff9ee4384b1894a90878d3e92bab", Name: "foo_role"}, + Role{ID: "1ea3d56793574b668e85960fbf651e13", Name: "admin"}, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + + th.AssertNoErr(t, err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/users/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/users/results.go new file mode 100644 index 0000000000..f531d5d023 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/users/results.go @@ -0,0 +1,128 @@ +package users + +import ( + "github.com/mitchellh/mapstructure" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// User represents a user resource that exists on the API. +type User struct { + // The UUID for this user. + ID string + + // The human name for this user. + Name string + + // The username for this user. + Username string + + // Indicates whether the user is enabled (true) or disabled (false). + Enabled bool + + // The email address for this user. + Email string + + // The ID of the tenant to which this user belongs. + TenantID string `mapstructure:"tenant_id"` +} + +// Role assigns specific responsibilities to users, allowing them to accomplish +// certain API operations whilst scoped to a service. +type Role struct { + // UUID of the role + ID string + + // Name of the role + Name string +} + +// UserPage is a single page of a User collection. +type UserPage struct { + pagination.SinglePageBase +} + +// RolePage is a single page of a user Role collection. +type RolePage struct { + pagination.SinglePageBase +} + +// IsEmpty determines whether or not a page of Tenants contains any results. +func (page UserPage) IsEmpty() (bool, error) { + users, err := ExtractUsers(page) + if err != nil { + return false, err + } + return len(users) == 0, nil +} + +// ExtractUsers returns a slice of Tenants contained in a single page of results. +func ExtractUsers(page pagination.Page) ([]User, error) { + casted := page.(UserPage).Body + var response struct { + Users []User `mapstructure:"users"` + } + + err := mapstructure.Decode(casted, &response) + return response.Users, err +} + +// IsEmpty determines whether or not a page of Tenants contains any results. +func (page RolePage) IsEmpty() (bool, error) { + users, err := ExtractRoles(page) + if err != nil { + return false, err + } + return len(users) == 0, nil +} + +// ExtractRoles returns a slice of Roles contained in a single page of results. +func ExtractRoles(page pagination.Page) ([]Role, error) { + casted := page.(RolePage).Body + var response struct { + Roles []Role `mapstructure:"roles"` + } + + err := mapstructure.Decode(casted, &response) + return response.Roles, err +} + +type commonResult struct { + gophercloud.Result +} + +// Extract interprets any commonResult as a User, if possible. +func (r commonResult) Extract() (*User, error) { + if r.Err != nil { + return nil, r.Err + } + + var response struct { + User User `mapstructure:"user"` + } + + err := mapstructure.Decode(r.Body, &response) + + return &response.User, err +} + +// CreateResult represents the result of a Create operation +type CreateResult struct { + commonResult +} + +// GetResult represents the result of a Get operation +type GetResult struct { + commonResult +} + +// UpdateResult represents the result of an Update operation +type UpdateResult struct { + commonResult +} + +// DeleteResult represents the result of a Delete operation +type DeleteResult struct { + commonResult +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/users/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/users/urls.go new file mode 100644 index 0000000000..7ec4385d74 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/users/urls.go @@ -0,0 +1,21 @@ +package users + +import "github.com/rackspace/gophercloud" + +const ( + tenantPath = "tenants" + userPath = "users" + rolePath = "roles" +) + +func ResourceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(userPath, id) +} + +func rootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(userPath) +} + +func listRolesURL(c *gophercloud.ServiceClient, tenantID, userID string) string { + return c.ServiceURL(tenantPath, tenantID, userPath, userID, rolePath) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/endpoints/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/endpoints/doc.go new file mode 100644 index 0000000000..85163949a8 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/endpoints/doc.go @@ -0,0 +1,6 @@ +// Package endpoints provides information and interaction with the service +// endpoints API resource in the OpenStack Identity service. +// +// For more information, see: +// http://developer.openstack.org/api-ref-identity-v3.html#endpoints-v3 +package endpoints diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/endpoints/errors.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/endpoints/errors.go new file mode 100644 index 0000000000..854957ff98 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/endpoints/errors.go @@ -0,0 +1,21 @@ +package endpoints + +import "fmt" + +func requiredAttribute(attribute string) error { + return fmt.Errorf("You must specify %s for this endpoint.", attribute) +} + +var ( + // ErrAvailabilityRequired is reported if an Endpoint is created without an Availability. + ErrAvailabilityRequired = requiredAttribute("an availability") + + // ErrNameRequired is reported if an Endpoint is created without a Name. + ErrNameRequired = requiredAttribute("a name") + + // ErrURLRequired is reported if an Endpoint is created without a URL. + ErrURLRequired = requiredAttribute("a URL") + + // ErrServiceIDRequired is reported if an Endpoint is created without a ServiceID. + ErrServiceIDRequired = requiredAttribute("a serviceID") +) diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/endpoints/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/endpoints/requests.go new file mode 100644 index 0000000000..7bdb7cef2e --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/endpoints/requests.go @@ -0,0 +1,133 @@ +package endpoints + +import ( + "github.com/racker/perigee" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// EndpointOpts contains the subset of Endpoint attributes that should be used to create or update an Endpoint. +type EndpointOpts struct { + Availability gophercloud.Availability + Name string + Region string + URL string + ServiceID string +} + +// Create inserts a new Endpoint into the service catalog. +// Within EndpointOpts, Region may be omitted by being left as "", but all other fields are required. +func Create(client *gophercloud.ServiceClient, opts EndpointOpts) CreateResult { + // Redefined so that Region can be re-typed as a *string, which can be omitted from the JSON output. + type endpoint struct { + Interface string `json:"interface"` + Name string `json:"name"` + Region *string `json:"region,omitempty"` + URL string `json:"url"` + ServiceID string `json:"service_id"` + } + + type request struct { + Endpoint endpoint `json:"endpoint"` + } + + // Ensure that EndpointOpts is fully populated. + if opts.Availability == "" { + return createErr(ErrAvailabilityRequired) + } + if opts.Name == "" { + return createErr(ErrNameRequired) + } + if opts.URL == "" { + return createErr(ErrURLRequired) + } + if opts.ServiceID == "" { + return createErr(ErrServiceIDRequired) + } + + // Populate the request body. + reqBody := request{ + Endpoint: endpoint{ + Interface: string(opts.Availability), + Name: opts.Name, + URL: opts.URL, + ServiceID: opts.ServiceID, + }, + } + reqBody.Endpoint.Region = gophercloud.MaybeString(opts.Region) + + var result CreateResult + _, result.Err = perigee.Request("POST", listURL(client), perigee.Options{ + MoreHeaders: client.AuthenticatedHeaders(), + ReqBody: &reqBody, + Results: &result.Body, + OkCodes: []int{201}, + }) + return result +} + +// ListOpts allows finer control over the endpoints returned by a List call. +// All fields are optional. +type ListOpts struct { + Availability gophercloud.Availability `q:"interface"` + ServiceID string `q:"service_id"` + Page int `q:"page"` + PerPage int `q:"per_page"` +} + +// List enumerates endpoints in a paginated collection, optionally filtered by ListOpts criteria. +func List(client *gophercloud.ServiceClient, opts ListOpts) pagination.Pager { + u := listURL(client) + q, err := gophercloud.BuildQueryString(opts) + if err != nil { + return pagination.Pager{Err: err} + } + u += q.String() + createPage := func(r pagination.PageResult) pagination.Page { + return EndpointPage{pagination.LinkedPageBase{PageResult: r}} + } + + return pagination.NewPager(client, u, createPage) +} + +// Update changes an existing endpoint with new data. +// All fields are optional in the provided EndpointOpts. +func Update(client *gophercloud.ServiceClient, endpointID string, opts EndpointOpts) UpdateResult { + type endpoint struct { + Interface *string `json:"interface,omitempty"` + Name *string `json:"name,omitempty"` + Region *string `json:"region,omitempty"` + URL *string `json:"url,omitempty"` + ServiceID *string `json:"service_id,omitempty"` + } + + type request struct { + Endpoint endpoint `json:"endpoint"` + } + + reqBody := request{Endpoint: endpoint{}} + reqBody.Endpoint.Interface = gophercloud.MaybeString(string(opts.Availability)) + reqBody.Endpoint.Name = gophercloud.MaybeString(opts.Name) + reqBody.Endpoint.Region = gophercloud.MaybeString(opts.Region) + reqBody.Endpoint.URL = gophercloud.MaybeString(opts.URL) + reqBody.Endpoint.ServiceID = gophercloud.MaybeString(opts.ServiceID) + + var result UpdateResult + _, result.Err = perigee.Request("PATCH", endpointURL(client, endpointID), perigee.Options{ + MoreHeaders: client.AuthenticatedHeaders(), + ReqBody: &reqBody, + Results: &result.Body, + OkCodes: []int{200}, + }) + return result +} + +// Delete removes an endpoint from the service catalog. +func Delete(client *gophercloud.ServiceClient, endpointID string) DeleteResult { + var res DeleteResult + _, res.Err = perigee.Request("DELETE", endpointURL(client, endpointID), perigee.Options{ + MoreHeaders: client.AuthenticatedHeaders(), + OkCodes: []int{204}, + }) + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/endpoints/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/endpoints/requests_test.go new file mode 100644 index 0000000000..80687c4cb7 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/endpoints/requests_test.go @@ -0,0 +1,226 @@ +package endpoints + +import ( + "fmt" + "net/http" + "reflect" + "testing" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" + "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestCreateSuccessful(t *testing.T) { + testhelper.SetupHTTP() + defer testhelper.TeardownHTTP() + + testhelper.Mux.HandleFunc("/endpoints", func(w http.ResponseWriter, r *http.Request) { + testhelper.TestMethod(t, r, "POST") + testhelper.TestHeader(t, r, "X-Auth-Token", client.TokenID) + testhelper.TestJSONRequest(t, r, ` + { + "endpoint": { + "interface": "public", + "name": "the-endiest-of-points", + "region": "underground", + "url": "https://1.2.3.4:9000/", + "service_id": "asdfasdfasdfasdf" + } + } + `) + + w.WriteHeader(http.StatusCreated) + fmt.Fprintf(w, ` + { + "endpoint": { + "id": "12", + "interface": "public", + "links": { + "self": "https://localhost:5000/v3/endpoints/12" + }, + "name": "the-endiest-of-points", + "region": "underground", + "service_id": "asdfasdfasdfasdf", + "url": "https://1.2.3.4:9000/" + } + } + `) + }) + + actual, err := Create(client.ServiceClient(), EndpointOpts{ + Availability: gophercloud.AvailabilityPublic, + Name: "the-endiest-of-points", + Region: "underground", + URL: "https://1.2.3.4:9000/", + ServiceID: "asdfasdfasdfasdf", + }).Extract() + if err != nil { + t.Fatalf("Unable to create an endpoint: %v", err) + } + + expected := &Endpoint{ + ID: "12", + Availability: gophercloud.AvailabilityPublic, + Name: "the-endiest-of-points", + Region: "underground", + ServiceID: "asdfasdfasdfasdf", + URL: "https://1.2.3.4:9000/", + } + + if !reflect.DeepEqual(actual, expected) { + t.Errorf("Expected %#v, was %#v", expected, actual) + } +} + +func TestListEndpoints(t *testing.T) { + testhelper.SetupHTTP() + defer testhelper.TeardownHTTP() + + testhelper.Mux.HandleFunc("/endpoints", func(w http.ResponseWriter, r *http.Request) { + testhelper.TestMethod(t, r, "GET") + testhelper.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, ` + { + "endpoints": [ + { + "id": "12", + "interface": "public", + "links": { + "self": "https://localhost:5000/v3/endpoints/12" + }, + "name": "the-endiest-of-points", + "region": "underground", + "service_id": "asdfasdfasdfasdf", + "url": "https://1.2.3.4:9000/" + }, + { + "id": "13", + "interface": "internal", + "links": { + "self": "https://localhost:5000/v3/endpoints/13" + }, + "name": "shhhh", + "region": "underground", + "service_id": "asdfasdfasdfasdf", + "url": "https://1.2.3.4:9001/" + } + ], + "links": { + "next": null, + "previous": null + } + } + `) + }) + + count := 0 + List(client.ServiceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractEndpoints(page) + if err != nil { + t.Errorf("Failed to extract endpoints: %v", err) + return false, err + } + + expected := []Endpoint{ + Endpoint{ + ID: "12", + Availability: gophercloud.AvailabilityPublic, + Name: "the-endiest-of-points", + Region: "underground", + ServiceID: "asdfasdfasdfasdf", + URL: "https://1.2.3.4:9000/", + }, + Endpoint{ + ID: "13", + Availability: gophercloud.AvailabilityInternal, + Name: "shhhh", + Region: "underground", + ServiceID: "asdfasdfasdfasdf", + URL: "https://1.2.3.4:9001/", + }, + } + + if !reflect.DeepEqual(expected, actual) { + t.Errorf("Expected %#v, got %#v", expected, actual) + } + + return true, nil + }) + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestUpdateEndpoint(t *testing.T) { + testhelper.SetupHTTP() + defer testhelper.TeardownHTTP() + + testhelper.Mux.HandleFunc("/endpoints/12", func(w http.ResponseWriter, r *http.Request) { + testhelper.TestMethod(t, r, "PATCH") + testhelper.TestHeader(t, r, "X-Auth-Token", client.TokenID) + testhelper.TestJSONRequest(t, r, ` + { + "endpoint": { + "name": "renamed", + "region": "somewhere-else" + } + } + `) + + fmt.Fprintf(w, ` + { + "endpoint": { + "id": "12", + "interface": "public", + "links": { + "self": "https://localhost:5000/v3/endpoints/12" + }, + "name": "renamed", + "region": "somewhere-else", + "service_id": "asdfasdfasdfasdf", + "url": "https://1.2.3.4:9000/" + } + } + `) + }) + + actual, err := Update(client.ServiceClient(), "12", EndpointOpts{ + Name: "renamed", + Region: "somewhere-else", + }).Extract() + if err != nil { + t.Fatalf("Unexpected error from Update: %v", err) + } + + expected := &Endpoint{ + ID: "12", + Availability: gophercloud.AvailabilityPublic, + Name: "renamed", + Region: "somewhere-else", + ServiceID: "asdfasdfasdfasdf", + URL: "https://1.2.3.4:9000/", + } + if !reflect.DeepEqual(expected, actual) { + t.Errorf("Expected %#v, was %#v", expected, actual) + } +} + +func TestDeleteEndpoint(t *testing.T) { + testhelper.SetupHTTP() + defer testhelper.TeardownHTTP() + + testhelper.Mux.HandleFunc("/endpoints/34", func(w http.ResponseWriter, r *http.Request) { + testhelper.TestMethod(t, r, "DELETE") + testhelper.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.WriteHeader(http.StatusNoContent) + }) + + res := Delete(client.ServiceClient(), "34") + testhelper.AssertNoErr(t, res.Err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/endpoints/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/endpoints/results.go new file mode 100644 index 0000000000..128112295a --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/endpoints/results.go @@ -0,0 +1,82 @@ +package endpoints + +import ( + "github.com/mitchellh/mapstructure" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +type commonResult struct { + gophercloud.Result +} + +// Extract interprets a GetResult, CreateResult or UpdateResult as a concrete Endpoint. +// An error is returned if the original call or the extraction failed. +func (r commonResult) Extract() (*Endpoint, error) { + if r.Err != nil { + return nil, r.Err + } + + var res struct { + Endpoint `json:"endpoint"` + } + + err := mapstructure.Decode(r.Body, &res) + + return &res.Endpoint, err +} + +// CreateResult is the deferred result of a Create call. +type CreateResult struct { + commonResult +} + +// createErr quickly wraps an error in a CreateResult. +func createErr(err error) CreateResult { + return CreateResult{commonResult{gophercloud.Result{Err: err}}} +} + +// UpdateResult is the deferred result of an Update call. +type UpdateResult struct { + commonResult +} + +// DeleteResult is the deferred result of an Delete call. +type DeleteResult struct { + gophercloud.ErrResult +} + +// Endpoint describes the entry point for another service's API. +type Endpoint struct { + ID string `mapstructure:"id" json:"id"` + Availability gophercloud.Availability `mapstructure:"interface" json:"interface"` + Name string `mapstructure:"name" json:"name"` + Region string `mapstructure:"region" json:"region"` + ServiceID string `mapstructure:"service_id" json:"service_id"` + URL string `mapstructure:"url" json:"url"` +} + +// EndpointPage is a single page of Endpoint results. +type EndpointPage struct { + pagination.LinkedPageBase +} + +// IsEmpty returns true if no Endpoints were returned. +func (p EndpointPage) IsEmpty() (bool, error) { + es, err := ExtractEndpoints(p) + if err != nil { + return true, err + } + return len(es) == 0, nil +} + +// ExtractEndpoints extracts an Endpoint slice from a Page. +func ExtractEndpoints(page pagination.Page) ([]Endpoint, error) { + var response struct { + Endpoints []Endpoint `mapstructure:"endpoints"` + } + + err := mapstructure.Decode(page.(EndpointPage).Body, &response) + + return response.Endpoints, err +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/endpoints/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/endpoints/urls.go new file mode 100644 index 0000000000..547d7b102a --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/endpoints/urls.go @@ -0,0 +1,11 @@ +package endpoints + +import "github.com/rackspace/gophercloud" + +func listURL(client *gophercloud.ServiceClient) string { + return client.ServiceURL("endpoints") +} + +func endpointURL(client *gophercloud.ServiceClient, endpointID string) string { + return client.ServiceURL("endpoints", endpointID) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/endpoints/urls_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/endpoints/urls_test.go new file mode 100644 index 0000000000..0b183b7434 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/endpoints/urls_test.go @@ -0,0 +1,23 @@ +package endpoints + +import ( + "testing" + + "github.com/rackspace/gophercloud" +) + +func TestGetListURL(t *testing.T) { + client := gophercloud.ServiceClient{Endpoint: "http://localhost:5000/v3/"} + url := listURL(&client) + if url != "http://localhost:5000/v3/endpoints" { + t.Errorf("Unexpected list URL generated: [%s]", url) + } +} + +func TestGetEndpointURL(t *testing.T) { + client := gophercloud.ServiceClient{Endpoint: "http://localhost:5000/v3/"} + url := endpointURL(&client, "1234") + if url != "http://localhost:5000/v3/endpoints/1234" { + t.Errorf("Unexpected service URL generated: [%s]", url) + } +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/services/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/services/doc.go new file mode 100644 index 0000000000..fa56411856 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/services/doc.go @@ -0,0 +1,3 @@ +// Package services provides information and interaction with the services API +// resource for the OpenStack Identity service. +package services diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/services/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/services/requests.go new file mode 100644 index 0000000000..1d9aaa873a --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/services/requests.go @@ -0,0 +1,91 @@ +package services + +import ( + "github.com/racker/perigee" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +type response struct { + Service Service `json:"service"` +} + +// Create adds a new service of the requested type to the catalog. +func Create(client *gophercloud.ServiceClient, serviceType string) CreateResult { + type request struct { + Type string `json:"type"` + } + + req := request{Type: serviceType} + + var result CreateResult + _, result.Err = perigee.Request("POST", listURL(client), perigee.Options{ + MoreHeaders: client.AuthenticatedHeaders(), + ReqBody: &req, + Results: &result.Body, + OkCodes: []int{201}, + }) + return result +} + +// ListOpts allows you to query the List method. +type ListOpts struct { + ServiceType string `q:"type"` + PerPage int `q:"perPage"` + Page int `q:"page"` +} + +// List enumerates the services available to a specific user. +func List(client *gophercloud.ServiceClient, opts ListOpts) pagination.Pager { + u := listURL(client) + q, err := gophercloud.BuildQueryString(opts) + if err != nil { + return pagination.Pager{Err: err} + } + u += q.String() + createPage := func(r pagination.PageResult) pagination.Page { + return ServicePage{pagination.LinkedPageBase{PageResult: r}} + } + + return pagination.NewPager(client, u, createPage) +} + +// Get returns additional information about a service, given its ID. +func Get(client *gophercloud.ServiceClient, serviceID string) GetResult { + var result GetResult + _, result.Err = perigee.Request("GET", serviceURL(client, serviceID), perigee.Options{ + MoreHeaders: client.AuthenticatedHeaders(), + Results: &result.Body, + OkCodes: []int{200}, + }) + return result +} + +// Update changes the service type of an existing service. +func Update(client *gophercloud.ServiceClient, serviceID string, serviceType string) UpdateResult { + type request struct { + Type string `json:"type"` + } + + req := request{Type: serviceType} + + var result UpdateResult + _, result.Err = perigee.Request("PATCH", serviceURL(client, serviceID), perigee.Options{ + MoreHeaders: client.AuthenticatedHeaders(), + ReqBody: &req, + Results: &result.Body, + OkCodes: []int{200}, + }) + return result +} + +// Delete removes an existing service. +// It either deletes all associated endpoints, or fails until all endpoints are deleted. +func Delete(client *gophercloud.ServiceClient, serviceID string) DeleteResult { + var res DeleteResult + _, res.Err = perigee.Request("DELETE", serviceURL(client, serviceID), perigee.Options{ + MoreHeaders: client.AuthenticatedHeaders(), + OkCodes: []int{204}, + }) + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/services/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/services/requests_test.go new file mode 100644 index 0000000000..42f05d365a --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/services/requests_test.go @@ -0,0 +1,209 @@ +package services + +import ( + "fmt" + "net/http" + "reflect" + "testing" + + "github.com/rackspace/gophercloud/pagination" + "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestCreateSuccessful(t *testing.T) { + testhelper.SetupHTTP() + defer testhelper.TeardownHTTP() + + testhelper.Mux.HandleFunc("/services", func(w http.ResponseWriter, r *http.Request) { + testhelper.TestMethod(t, r, "POST") + testhelper.TestHeader(t, r, "X-Auth-Token", client.TokenID) + testhelper.TestJSONRequest(t, r, `{ "type": "compute" }`) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + fmt.Fprintf(w, `{ + "service": { + "description": "Here's your service", + "id": "1234", + "name": "InscrutableOpenStackProjectName", + "type": "compute" + } + }`) + }) + + result, err := Create(client.ServiceClient(), "compute").Extract() + if err != nil { + t.Fatalf("Unexpected error from Create: %v", err) + } + + if result.Description == nil || *result.Description != "Here's your service" { + t.Errorf("Service description was unexpected [%s]", *result.Description) + } + if result.ID != "1234" { + t.Errorf("Service ID was unexpected [%s]", result.ID) + } + if result.Name != "InscrutableOpenStackProjectName" { + t.Errorf("Service name was unexpected [%s]", result.Name) + } + if result.Type != "compute" { + t.Errorf("Service type was unexpected [%s]", result.Type) + } +} + +func TestListSinglePage(t *testing.T) { + testhelper.SetupHTTP() + defer testhelper.TeardownHTTP() + + testhelper.Mux.HandleFunc("/services", func(w http.ResponseWriter, r *http.Request) { + testhelper.TestMethod(t, r, "GET") + testhelper.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, ` + { + "links": { + "next": null, + "previous": null + }, + "services": [ + { + "description": "Service One", + "id": "1234", + "name": "service-one", + "type": "identity" + }, + { + "description": "Service Two", + "id": "9876", + "name": "service-two", + "type": "compute" + } + ] + } + `) + }) + + count := 0 + err := List(client.ServiceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractServices(page) + if err != nil { + return false, err + } + + desc0 := "Service One" + desc1 := "Service Two" + expected := []Service{ + Service{ + Description: &desc0, + ID: "1234", + Name: "service-one", + Type: "identity", + }, + Service{ + Description: &desc1, + ID: "9876", + Name: "service-two", + Type: "compute", + }, + } + + if !reflect.DeepEqual(expected, actual) { + t.Errorf("Expected %#v, got %#v", expected, actual) + } + + return true, nil + }) + if err != nil { + t.Errorf("Unexpected error while paging: %v", err) + } + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestGetSuccessful(t *testing.T) { + testhelper.SetupHTTP() + defer testhelper.TeardownHTTP() + + testhelper.Mux.HandleFunc("/services/12345", func(w http.ResponseWriter, r *http.Request) { + testhelper.TestMethod(t, r, "GET") + testhelper.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, ` + { + "service": { + "description": "Service One", + "id": "12345", + "name": "service-one", + "type": "identity" + } + } + `) + }) + + result, err := Get(client.ServiceClient(), "12345").Extract() + if err != nil { + t.Fatalf("Error fetching service information: %v", err) + } + + if result.ID != "12345" { + t.Errorf("Unexpected service ID: %s", result.ID) + } + if *result.Description != "Service One" { + t.Errorf("Unexpected service description: [%s]", *result.Description) + } + if result.Name != "service-one" { + t.Errorf("Unexpected service name: [%s]", result.Name) + } + if result.Type != "identity" { + t.Errorf("Unexpected service type: [%s]", result.Type) + } +} + +func TestUpdateSuccessful(t *testing.T) { + testhelper.SetupHTTP() + defer testhelper.TeardownHTTP() + + testhelper.Mux.HandleFunc("/services/12345", func(w http.ResponseWriter, r *http.Request) { + testhelper.TestMethod(t, r, "PATCH") + testhelper.TestHeader(t, r, "X-Auth-Token", client.TokenID) + testhelper.TestJSONRequest(t, r, `{ "type": "lasermagic" }`) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, ` + { + "service": { + "id": "12345", + "type": "lasermagic" + } + } + `) + }) + + result, err := Update(client.ServiceClient(), "12345", "lasermagic").Extract() + if err != nil { + t.Fatalf("Unable to update service: %v", err) + } + + if result.ID != "12345" { + t.Fatalf("Expected ID 12345, was %s", result.ID) + } +} + +func TestDeleteSuccessful(t *testing.T) { + testhelper.SetupHTTP() + defer testhelper.TeardownHTTP() + + testhelper.Mux.HandleFunc("/services/12345", func(w http.ResponseWriter, r *http.Request) { + testhelper.TestMethod(t, r, "DELETE") + testhelper.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.WriteHeader(http.StatusNoContent) + }) + + res := Delete(client.ServiceClient(), "12345") + testhelper.AssertNoErr(t, res.Err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/services/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/services/results.go new file mode 100644 index 0000000000..1d0d141280 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/services/results.go @@ -0,0 +1,80 @@ +package services + +import ( + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" + + "github.com/mitchellh/mapstructure" +) + +type commonResult struct { + gophercloud.Result +} + +// Extract interprets a GetResult, CreateResult or UpdateResult as a concrete Service. +// An error is returned if the original call or the extraction failed. +func (r commonResult) Extract() (*Service, error) { + if r.Err != nil { + return nil, r.Err + } + + var res struct { + Service `json:"service"` + } + + err := mapstructure.Decode(r.Body, &res) + + return &res.Service, err +} + +// CreateResult is the deferred result of a Create call. +type CreateResult struct { + commonResult +} + +// GetResult is the deferred result of a Get call. +type GetResult struct { + commonResult +} + +// UpdateResult is the deferred result of an Update call. +type UpdateResult struct { + commonResult +} + +// DeleteResult is the deferred result of an Delete call. +type DeleteResult struct { + gophercloud.ErrResult +} + +// Service is the result of a list or information query. +type Service struct { + Description *string `json:"description,omitempty"` + ID string `json:"id"` + Name string `json:"name"` + Type string `json:"type"` +} + +// ServicePage is a single page of Service results. +type ServicePage struct { + pagination.LinkedPageBase +} + +// IsEmpty returns true if the page contains no results. +func (p ServicePage) IsEmpty() (bool, error) { + services, err := ExtractServices(p) + if err != nil { + return true, err + } + return len(services) == 0, nil +} + +// ExtractServices extracts a slice of Services from a Collection acquired from List. +func ExtractServices(page pagination.Page) ([]Service, error) { + var response struct { + Services []Service `mapstructure:"services"` + } + + err := mapstructure.Decode(page.(ServicePage).Body, &response) + return response.Services, err +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/services/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/services/urls.go new file mode 100644 index 0000000000..85443a48a0 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/services/urls.go @@ -0,0 +1,11 @@ +package services + +import "github.com/rackspace/gophercloud" + +func listURL(client *gophercloud.ServiceClient) string { + return client.ServiceURL("services") +} + +func serviceURL(client *gophercloud.ServiceClient, serviceID string) string { + return client.ServiceURL("services", serviceID) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/services/urls_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/services/urls_test.go new file mode 100644 index 0000000000..5a31b32316 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/services/urls_test.go @@ -0,0 +1,23 @@ +package services + +import ( + "testing" + + "github.com/rackspace/gophercloud" +) + +func TestListURL(t *testing.T) { + client := gophercloud.ServiceClient{Endpoint: "http://localhost:5000/v3/"} + url := listURL(&client) + if url != "http://localhost:5000/v3/services" { + t.Errorf("Unexpected list URL generated: [%s]", url) + } +} + +func TestServiceURL(t *testing.T) { + client := gophercloud.ServiceClient{Endpoint: "http://localhost:5000/v3/"} + url := serviceURL(&client, "1234") + if url != "http://localhost:5000/v3/services/1234" { + t.Errorf("Unexpected service URL generated: [%s]", url) + } +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/tokens/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/tokens/doc.go new file mode 100644 index 0000000000..76ff5f4738 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/tokens/doc.go @@ -0,0 +1,6 @@ +// Package tokens provides information and interaction with the token API +// resource for the OpenStack Identity service. +// +// For more information, see: +// http://developer.openstack.org/api-ref-identity-v3.html#tokens-v3 +package tokens diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/tokens/errors.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/tokens/errors.go new file mode 100644 index 0000000000..44761092bb --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/tokens/errors.go @@ -0,0 +1,72 @@ +package tokens + +import ( + "errors" + "fmt" +) + +func unacceptedAttributeErr(attribute string) error { + return fmt.Errorf("The base Identity V3 API does not accept authentication by %s", attribute) +} + +func redundantWithTokenErr(attribute string) error { + return fmt.Errorf("%s may not be provided when authenticating with a TokenID", attribute) +} + +func redundantWithUserID(attribute string) error { + return fmt.Errorf("%s may not be provided when authenticating with a UserID", attribute) +} + +var ( + // ErrAPIKeyProvided indicates that an APIKey was provided but can't be used. + ErrAPIKeyProvided = unacceptedAttributeErr("APIKey") + + // ErrTenantIDProvided indicates that a TenantID was provided but can't be used. + ErrTenantIDProvided = unacceptedAttributeErr("TenantID") + + // ErrTenantNameProvided indicates that a TenantName was provided but can't be used. + ErrTenantNameProvided = unacceptedAttributeErr("TenantName") + + // ErrUsernameWithToken indicates that a Username was provided, but token authentication is being used instead. + ErrUsernameWithToken = redundantWithTokenErr("Username") + + // ErrUserIDWithToken indicates that a UserID was provided, but token authentication is being used instead. + ErrUserIDWithToken = redundantWithTokenErr("UserID") + + // ErrDomainIDWithToken indicates that a DomainID was provided, but token authentication is being used instead. + ErrDomainIDWithToken = redundantWithTokenErr("DomainID") + + // ErrDomainNameWithToken indicates that a DomainName was provided, but token authentication is being used instead.s + ErrDomainNameWithToken = redundantWithTokenErr("DomainName") + + // ErrUsernameOrUserID indicates that neither username nor userID are specified, or both are at once. + ErrUsernameOrUserID = errors.New("Exactly one of Username and UserID must be provided for password authentication") + + // ErrDomainIDWithUserID indicates that a DomainID was provided, but unnecessary because a UserID is being used. + ErrDomainIDWithUserID = redundantWithUserID("DomainID") + + // ErrDomainNameWithUserID indicates that a DomainName was provided, but unnecessary because a UserID is being used. + ErrDomainNameWithUserID = redundantWithUserID("DomainName") + + // ErrDomainIDOrDomainName indicates that a username was provided, but no domain to scope it. + // It may also indicate that both a DomainID and a DomainName were provided at once. + ErrDomainIDOrDomainName = errors.New("You must provide exactly one of DomainID or DomainName to authenticate by Username") + + // ErrMissingPassword indicates that no password was provided and no token is available. + ErrMissingPassword = errors.New("You must provide a password to authenticate") + + // ErrScopeDomainIDOrDomainName indicates that a domain ID or Name was required in a Scope, but not present. + ErrScopeDomainIDOrDomainName = errors.New("You must provide exactly one of DomainID or DomainName in a Scope with ProjectName") + + // ErrScopeProjectIDOrProjectName indicates that both a ProjectID and a ProjectName were provided in a Scope. + ErrScopeProjectIDOrProjectName = errors.New("You must provide at most one of ProjectID or ProjectName in a Scope") + + // ErrScopeProjectIDAlone indicates that a ProjectID was provided with other constraints in a Scope. + ErrScopeProjectIDAlone = errors.New("ProjectID must be supplied alone in a Scope") + + // ErrScopeDomainName indicates that a DomainName was provided alone in a Scope. + ErrScopeDomainName = errors.New("DomainName must be supplied with a ProjectName or ProjectID in a Scope.") + + // ErrScopeEmpty indicates that no credentials were provided in a Scope. + ErrScopeEmpty = errors.New("You must provide either a Project or Domain in a Scope") +) diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/tokens/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/tokens/requests.go new file mode 100644 index 0000000000..5ca1031c41 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/tokens/requests.go @@ -0,0 +1,286 @@ +package tokens + +import ( + "github.com/racker/perigee" + "github.com/rackspace/gophercloud" +) + +// Scope allows a created token to be limited to a specific domain or project. +type Scope struct { + ProjectID string + ProjectName string + DomainID string + DomainName string +} + +func subjectTokenHeaders(c *gophercloud.ServiceClient, subjectToken string) map[string]string { + h := c.AuthenticatedHeaders() + h["X-Subject-Token"] = subjectToken + return h +} + +// Create authenticates and either generates a new token, or changes the Scope of an existing token. +func Create(c *gophercloud.ServiceClient, options gophercloud.AuthOptions, scope *Scope) CreateResult { + type domainReq struct { + ID *string `json:"id,omitempty"` + Name *string `json:"name,omitempty"` + } + + type projectReq struct { + Domain *domainReq `json:"domain,omitempty"` + Name *string `json:"name,omitempty"` + ID *string `json:"id,omitempty"` + } + + type userReq struct { + ID *string `json:"id,omitempty"` + Name *string `json:"name,omitempty"` + Password string `json:"password"` + Domain *domainReq `json:"domain,omitempty"` + } + + type passwordReq struct { + User userReq `json:"user"` + } + + type tokenReq struct { + ID string `json:"id"` + } + + type identityReq struct { + Methods []string `json:"methods"` + Password *passwordReq `json:"password,omitempty"` + Token *tokenReq `json:"token,omitempty"` + } + + type scopeReq struct { + Domain *domainReq `json:"domain,omitempty"` + Project *projectReq `json:"project,omitempty"` + } + + type authReq struct { + Identity identityReq `json:"identity"` + Scope *scopeReq `json:"scope,omitempty"` + } + + type request struct { + Auth authReq `json:"auth"` + } + + // Populate the request structure based on the provided arguments. Create and return an error + // if insufficient or incompatible information is present. + var req request + + // Test first for unrecognized arguments. + if options.APIKey != "" { + return createErr(ErrAPIKeyProvided) + } + if options.TenantID != "" { + return createErr(ErrTenantIDProvided) + } + if options.TenantName != "" { + return createErr(ErrTenantNameProvided) + } + + if options.Password == "" { + if c.TokenID != "" { + // Because we aren't using password authentication, it's an error to also provide any of the user-based authentication + // parameters. + if options.Username != "" { + return createErr(ErrUsernameWithToken) + } + if options.UserID != "" { + return createErr(ErrUserIDWithToken) + } + if options.DomainID != "" { + return createErr(ErrDomainIDWithToken) + } + if options.DomainName != "" { + return createErr(ErrDomainNameWithToken) + } + + // Configure the request for Token authentication. + req.Auth.Identity.Methods = []string{"token"} + req.Auth.Identity.Token = &tokenReq{ + ID: c.TokenID, + } + } else { + // If no password or token ID are available, authentication can't continue. + return createErr(ErrMissingPassword) + } + } else { + // Password authentication. + req.Auth.Identity.Methods = []string{"password"} + + // At least one of Username and UserID must be specified. + if options.Username == "" && options.UserID == "" { + return createErr(ErrUsernameOrUserID) + } + + if options.Username != "" { + // If Username is provided, UserID may not be provided. + if options.UserID != "" { + return createErr(ErrUsernameOrUserID) + } + + // Either DomainID or DomainName must also be specified. + if options.DomainID == "" && options.DomainName == "" { + return createErr(ErrDomainIDOrDomainName) + } + + if options.DomainID != "" { + if options.DomainName != "" { + return createErr(ErrDomainIDOrDomainName) + } + + // Configure the request for Username and Password authentication with a DomainID. + req.Auth.Identity.Password = &passwordReq{ + User: userReq{ + Name: &options.Username, + Password: options.Password, + Domain: &domainReq{ID: &options.DomainID}, + }, + } + } + + if options.DomainName != "" { + // Configure the request for Username and Password authentication with a DomainName. + req.Auth.Identity.Password = &passwordReq{ + User: userReq{ + Name: &options.Username, + Password: options.Password, + Domain: &domainReq{Name: &options.DomainName}, + }, + } + } + } + + if options.UserID != "" { + // If UserID is specified, neither DomainID nor DomainName may be. + if options.DomainID != "" { + return createErr(ErrDomainIDWithUserID) + } + if options.DomainName != "" { + return createErr(ErrDomainNameWithUserID) + } + + // Configure the request for UserID and Password authentication. + req.Auth.Identity.Password = &passwordReq{ + User: userReq{ID: &options.UserID, Password: options.Password}, + } + } + } + + // Add a "scope" element if a Scope has been provided. + if scope != nil { + if scope.ProjectName != "" { + // ProjectName provided: either DomainID or DomainName must also be supplied. + // ProjectID may not be supplied. + if scope.DomainID == "" && scope.DomainName == "" { + return createErr(ErrScopeDomainIDOrDomainName) + } + if scope.ProjectID != "" { + return createErr(ErrScopeProjectIDOrProjectName) + } + + if scope.DomainID != "" { + // ProjectName + DomainID + req.Auth.Scope = &scopeReq{ + Project: &projectReq{ + Name: &scope.ProjectName, + Domain: &domainReq{ID: &scope.DomainID}, + }, + } + } + + if scope.DomainName != "" { + // ProjectName + DomainName + req.Auth.Scope = &scopeReq{ + Project: &projectReq{ + Name: &scope.ProjectName, + Domain: &domainReq{Name: &scope.DomainName}, + }, + } + } + } else if scope.ProjectID != "" { + // ProjectID provided. ProjectName, DomainID, and DomainName may not be provided. + if scope.DomainID != "" { + return createErr(ErrScopeProjectIDAlone) + } + if scope.DomainName != "" { + return createErr(ErrScopeProjectIDAlone) + } + + // ProjectID + req.Auth.Scope = &scopeReq{ + Project: &projectReq{ID: &scope.ProjectID}, + } + } else if scope.DomainID != "" { + // DomainID provided. ProjectID, ProjectName, and DomainName may not be provided. + if scope.DomainName != "" { + return createErr(ErrScopeDomainIDOrDomainName) + } + + // DomainID + req.Auth.Scope = &scopeReq{ + Domain: &domainReq{ID: &scope.DomainID}, + } + } else if scope.DomainName != "" { + return createErr(ErrScopeDomainName) + } else { + return createErr(ErrScopeEmpty) + } + } + + var result CreateResult + var response *perigee.Response + response, result.Err = perigee.Request("POST", tokenURL(c), perigee.Options{ + ReqBody: &req, + Results: &result.Body, + OkCodes: []int{201}, + }) + if result.Err != nil { + return result + } + result.Header = response.HttpResponse.Header + return result +} + +// Get validates and retrieves information about another token. +func Get(c *gophercloud.ServiceClient, token string) GetResult { + var result GetResult + var response *perigee.Response + response, result.Err = perigee.Request("GET", tokenURL(c), perigee.Options{ + MoreHeaders: subjectTokenHeaders(c, token), + Results: &result.Body, + OkCodes: []int{200, 203}, + }) + if result.Err != nil { + return result + } + result.Header = response.HttpResponse.Header + return result +} + +// Validate determines if a specified token is valid or not. +func Validate(c *gophercloud.ServiceClient, token string) (bool, error) { + response, err := perigee.Request("HEAD", tokenURL(c), perigee.Options{ + MoreHeaders: subjectTokenHeaders(c, token), + OkCodes: []int{204, 404}, + }) + if err != nil { + return false, err + } + + return response.StatusCode == 204, nil +} + +// Revoke immediately makes specified token invalid. +func Revoke(c *gophercloud.ServiceClient, token string) RevokeResult { + var res RevokeResult + _, res.Err = perigee.Request("DELETE", tokenURL(c), perigee.Options{ + MoreHeaders: subjectTokenHeaders(c, token), + OkCodes: []int{204}, + }) + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/tokens/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/tokens/requests_test.go new file mode 100644 index 0000000000..2b26e4ad36 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/tokens/requests_test.go @@ -0,0 +1,514 @@ +package tokens + +import ( + "fmt" + "net/http" + "testing" + "time" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/testhelper" +) + +// authTokenPost verifies that providing certain AuthOptions and Scope results in an expected JSON structure. +func authTokenPost(t *testing.T, options gophercloud.AuthOptions, scope *Scope, requestJSON string) { + testhelper.SetupHTTP() + defer testhelper.TeardownHTTP() + + client := gophercloud.ServiceClient{ + ProviderClient: &gophercloud.ProviderClient{ + TokenID: "12345abcdef", + }, + Endpoint: testhelper.Endpoint(), + } + + testhelper.Mux.HandleFunc("/auth/tokens", func(w http.ResponseWriter, r *http.Request) { + testhelper.TestMethod(t, r, "POST") + testhelper.TestHeader(t, r, "Content-Type", "application/json") + testhelper.TestHeader(t, r, "Accept", "application/json") + testhelper.TestJSONRequest(t, r, requestJSON) + + w.WriteHeader(http.StatusCreated) + fmt.Fprintf(w, `{ + "token": { + "expires_at": "2014-10-02T13:45:00.000000Z" + } + }`) + }) + + _, err := Create(&client, options, scope).Extract() + if err != nil { + t.Errorf("Create returned an error: %v", err) + } +} + +func authTokenPostErr(t *testing.T, options gophercloud.AuthOptions, scope *Scope, includeToken bool, expectedErr error) { + testhelper.SetupHTTP() + defer testhelper.TeardownHTTP() + + client := gophercloud.ServiceClient{ + ProviderClient: &gophercloud.ProviderClient{}, + Endpoint: testhelper.Endpoint(), + } + if includeToken { + client.TokenID = "abcdef123456" + } + + _, err := Create(&client, options, scope).Extract() + if err == nil { + t.Errorf("Create did NOT return an error") + } + if err != expectedErr { + t.Errorf("Create returned an unexpected error: wanted %v, got %v", expectedErr, err) + } +} + +func TestCreateUserIDAndPassword(t *testing.T) { + authTokenPost(t, gophercloud.AuthOptions{UserID: "me", Password: "squirrel!"}, nil, ` + { + "auth": { + "identity": { + "methods": ["password"], + "password": { + "user": { "id": "me", "password": "squirrel!" } + } + } + } + } + `) +} + +func TestCreateUsernameDomainIDPassword(t *testing.T) { + authTokenPost(t, gophercloud.AuthOptions{Username: "fakey", Password: "notpassword", DomainID: "abc123"}, nil, ` + { + "auth": { + "identity": { + "methods": ["password"], + "password": { + "user": { + "domain": { + "id": "abc123" + }, + "name": "fakey", + "password": "notpassword" + } + } + } + } + } + `) +} + +func TestCreateUsernameDomainNamePassword(t *testing.T) { + authTokenPost(t, gophercloud.AuthOptions{Username: "frank", Password: "swordfish", DomainName: "spork.net"}, nil, ` + { + "auth": { + "identity": { + "methods": ["password"], + "password": { + "user": { + "domain": { + "name": "spork.net" + }, + "name": "frank", + "password": "swordfish" + } + } + } + } + } + `) +} + +func TestCreateTokenID(t *testing.T) { + authTokenPost(t, gophercloud.AuthOptions{}, nil, ` + { + "auth": { + "identity": { + "methods": ["token"], + "token": { + "id": "12345abcdef" + } + } + } + } + `) +} + +func TestCreateProjectIDScope(t *testing.T) { + options := gophercloud.AuthOptions{UserID: "fenris", Password: "g0t0h311"} + scope := &Scope{ProjectID: "123456"} + authTokenPost(t, options, scope, ` + { + "auth": { + "identity": { + "methods": ["password"], + "password": { + "user": { + "id": "fenris", + "password": "g0t0h311" + } + } + }, + "scope": { + "project": { + "id": "123456" + } + } + } + } + `) +} + +func TestCreateDomainIDScope(t *testing.T) { + options := gophercloud.AuthOptions{UserID: "fenris", Password: "g0t0h311"} + scope := &Scope{DomainID: "1000"} + authTokenPost(t, options, scope, ` + { + "auth": { + "identity": { + "methods": ["password"], + "password": { + "user": { + "id": "fenris", + "password": "g0t0h311" + } + } + }, + "scope": { + "domain": { + "id": "1000" + } + } + } + } + `) +} + +func TestCreateProjectNameAndDomainIDScope(t *testing.T) { + options := gophercloud.AuthOptions{UserID: "fenris", Password: "g0t0h311"} + scope := &Scope{ProjectName: "world-domination", DomainID: "1000"} + authTokenPost(t, options, scope, ` + { + "auth": { + "identity": { + "methods": ["password"], + "password": { + "user": { + "id": "fenris", + "password": "g0t0h311" + } + } + }, + "scope": { + "project": { + "domain": { + "id": "1000" + }, + "name": "world-domination" + } + } + } + } + `) +} + +func TestCreateProjectNameAndDomainNameScope(t *testing.T) { + options := gophercloud.AuthOptions{UserID: "fenris", Password: "g0t0h311"} + scope := &Scope{ProjectName: "world-domination", DomainName: "evil-plans"} + authTokenPost(t, options, scope, ` + { + "auth": { + "identity": { + "methods": ["password"], + "password": { + "user": { + "id": "fenris", + "password": "g0t0h311" + } + } + }, + "scope": { + "project": { + "domain": { + "name": "evil-plans" + }, + "name": "world-domination" + } + } + } + } + `) +} + +func TestCreateExtractsTokenFromResponse(t *testing.T) { + testhelper.SetupHTTP() + defer testhelper.TeardownHTTP() + + client := gophercloud.ServiceClient{ + ProviderClient: &gophercloud.ProviderClient{}, + Endpoint: testhelper.Endpoint(), + } + + testhelper.Mux.HandleFunc("/auth/tokens", func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("X-Subject-Token", "aaa111") + + w.WriteHeader(http.StatusCreated) + fmt.Fprintf(w, `{ + "token": { + "expires_at": "2014-10-02T13:45:00.000000Z" + } + }`) + }) + + options := gophercloud.AuthOptions{UserID: "me", Password: "shhh"} + token, err := Create(&client, options, nil).Extract() + if err != nil { + t.Fatalf("Create returned an error: %v", err) + } + + if token.ID != "aaa111" { + t.Errorf("Expected token to be aaa111, but was %s", token.ID) + } +} + +func TestCreateFailureEmptyAuth(t *testing.T) { + authTokenPostErr(t, gophercloud.AuthOptions{}, nil, false, ErrMissingPassword) +} + +func TestCreateFailureAPIKey(t *testing.T) { + authTokenPostErr(t, gophercloud.AuthOptions{APIKey: "something"}, nil, false, ErrAPIKeyProvided) +} + +func TestCreateFailureTenantID(t *testing.T) { + authTokenPostErr(t, gophercloud.AuthOptions{TenantID: "something"}, nil, false, ErrTenantIDProvided) +} + +func TestCreateFailureTenantName(t *testing.T) { + authTokenPostErr(t, gophercloud.AuthOptions{TenantName: "something"}, nil, false, ErrTenantNameProvided) +} + +func TestCreateFailureTokenIDUsername(t *testing.T) { + authTokenPostErr(t, gophercloud.AuthOptions{Username: "something"}, nil, true, ErrUsernameWithToken) +} + +func TestCreateFailureTokenIDUserID(t *testing.T) { + authTokenPostErr(t, gophercloud.AuthOptions{UserID: "something"}, nil, true, ErrUserIDWithToken) +} + +func TestCreateFailureTokenIDDomainID(t *testing.T) { + authTokenPostErr(t, gophercloud.AuthOptions{DomainID: "something"}, nil, true, ErrDomainIDWithToken) +} + +func TestCreateFailureTokenIDDomainName(t *testing.T) { + authTokenPostErr(t, gophercloud.AuthOptions{DomainName: "something"}, nil, true, ErrDomainNameWithToken) +} + +func TestCreateFailureMissingUser(t *testing.T) { + options := gophercloud.AuthOptions{Password: "supersecure"} + authTokenPostErr(t, options, nil, false, ErrUsernameOrUserID) +} + +func TestCreateFailureBothUser(t *testing.T) { + options := gophercloud.AuthOptions{ + Password: "supersecure", + Username: "oops", + UserID: "redundancy", + } + authTokenPostErr(t, options, nil, false, ErrUsernameOrUserID) +} + +func TestCreateFailureMissingDomain(t *testing.T) { + options := gophercloud.AuthOptions{ + Password: "supersecure", + Username: "notuniqueenough", + } + authTokenPostErr(t, options, nil, false, ErrDomainIDOrDomainName) +} + +func TestCreateFailureBothDomain(t *testing.T) { + options := gophercloud.AuthOptions{ + Password: "supersecure", + Username: "someone", + DomainID: "hurf", + DomainName: "durf", + } + authTokenPostErr(t, options, nil, false, ErrDomainIDOrDomainName) +} + +func TestCreateFailureUserIDDomainID(t *testing.T) { + options := gophercloud.AuthOptions{ + UserID: "100", + Password: "stuff", + DomainID: "oops", + } + authTokenPostErr(t, options, nil, false, ErrDomainIDWithUserID) +} + +func TestCreateFailureUserIDDomainName(t *testing.T) { + options := gophercloud.AuthOptions{ + UserID: "100", + Password: "sssh", + DomainName: "oops", + } + authTokenPostErr(t, options, nil, false, ErrDomainNameWithUserID) +} + +func TestCreateFailureScopeProjectNameAlone(t *testing.T) { + options := gophercloud.AuthOptions{UserID: "myself", Password: "swordfish"} + scope := &Scope{ProjectName: "notenough"} + authTokenPostErr(t, options, scope, false, ErrScopeDomainIDOrDomainName) +} + +func TestCreateFailureScopeProjectNameAndID(t *testing.T) { + options := gophercloud.AuthOptions{UserID: "myself", Password: "swordfish"} + scope := &Scope{ProjectName: "whoops", ProjectID: "toomuch", DomainID: "1234"} + authTokenPostErr(t, options, scope, false, ErrScopeProjectIDOrProjectName) +} + +func TestCreateFailureScopeProjectIDAndDomainID(t *testing.T) { + options := gophercloud.AuthOptions{UserID: "myself", Password: "swordfish"} + scope := &Scope{ProjectID: "toomuch", DomainID: "notneeded"} + authTokenPostErr(t, options, scope, false, ErrScopeProjectIDAlone) +} + +func TestCreateFailureScopeProjectIDAndDomainNAme(t *testing.T) { + options := gophercloud.AuthOptions{UserID: "myself", Password: "swordfish"} + scope := &Scope{ProjectID: "toomuch", DomainName: "notneeded"} + authTokenPostErr(t, options, scope, false, ErrScopeProjectIDAlone) +} + +func TestCreateFailureScopeDomainIDAndDomainName(t *testing.T) { + options := gophercloud.AuthOptions{UserID: "myself", Password: "swordfish"} + scope := &Scope{DomainID: "toomuch", DomainName: "notneeded"} + authTokenPostErr(t, options, scope, false, ErrScopeDomainIDOrDomainName) +} + +func TestCreateFailureScopeDomainNameAlone(t *testing.T) { + options := gophercloud.AuthOptions{UserID: "myself", Password: "swordfish"} + scope := &Scope{DomainName: "notenough"} + authTokenPostErr(t, options, scope, false, ErrScopeDomainName) +} + +func TestCreateFailureEmptyScope(t *testing.T) { + options := gophercloud.AuthOptions{UserID: "myself", Password: "swordfish"} + scope := &Scope{} + authTokenPostErr(t, options, scope, false, ErrScopeEmpty) +} + +func TestGetRequest(t *testing.T) { + testhelper.SetupHTTP() + defer testhelper.TeardownHTTP() + + client := gophercloud.ServiceClient{ + ProviderClient: &gophercloud.ProviderClient{ + TokenID: "12345abcdef", + }, + Endpoint: testhelper.Endpoint(), + } + + testhelper.Mux.HandleFunc("/auth/tokens", func(w http.ResponseWriter, r *http.Request) { + testhelper.TestMethod(t, r, "GET") + testhelper.TestHeader(t, r, "Content-Type", "") + testhelper.TestHeader(t, r, "Accept", "application/json") + testhelper.TestHeader(t, r, "X-Auth-Token", "12345abcdef") + testhelper.TestHeader(t, r, "X-Subject-Token", "abcdef12345") + + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, ` + { "token": { "expires_at": "2014-08-29T13:10:01.000000Z" } } + `) + }) + + token, err := Get(&client, "abcdef12345").Extract() + if err != nil { + t.Errorf("Info returned an error: %v", err) + } + + expected, _ := time.Parse(time.UnixDate, "Fri Aug 29 13:10:01 UTC 2014") + if token.ExpiresAt != expected { + t.Errorf("Expected expiration time %s, but was %s", expected.Format(time.UnixDate), token.ExpiresAt.Format(time.UnixDate)) + } +} + +func prepareAuthTokenHandler(t *testing.T, expectedMethod string, status int) gophercloud.ServiceClient { + client := gophercloud.ServiceClient{ + ProviderClient: &gophercloud.ProviderClient{ + TokenID: "12345abcdef", + }, + Endpoint: testhelper.Endpoint(), + } + + testhelper.Mux.HandleFunc("/auth/tokens", func(w http.ResponseWriter, r *http.Request) { + testhelper.TestMethod(t, r, expectedMethod) + testhelper.TestHeader(t, r, "Content-Type", "") + testhelper.TestHeader(t, r, "Accept", "application/json") + testhelper.TestHeader(t, r, "X-Auth-Token", "12345abcdef") + testhelper.TestHeader(t, r, "X-Subject-Token", "abcdef12345") + + w.WriteHeader(status) + }) + + return client +} + +func TestValidateRequestSuccessful(t *testing.T) { + testhelper.SetupHTTP() + defer testhelper.TeardownHTTP() + client := prepareAuthTokenHandler(t, "HEAD", http.StatusNoContent) + + ok, err := Validate(&client, "abcdef12345") + if err != nil { + t.Errorf("Unexpected error from Validate: %v", err) + } + + if !ok { + t.Errorf("Validate returned false for a valid token") + } +} + +func TestValidateRequestFailure(t *testing.T) { + testhelper.SetupHTTP() + defer testhelper.TeardownHTTP() + client := prepareAuthTokenHandler(t, "HEAD", http.StatusNotFound) + + ok, err := Validate(&client, "abcdef12345") + if err != nil { + t.Errorf("Unexpected error from Validate: %v", err) + } + + if ok { + t.Errorf("Validate returned true for an invalid token") + } +} + +func TestValidateRequestError(t *testing.T) { + testhelper.SetupHTTP() + defer testhelper.TeardownHTTP() + client := prepareAuthTokenHandler(t, "HEAD", http.StatusUnauthorized) + + _, err := Validate(&client, "abcdef12345") + if err == nil { + t.Errorf("Missing expected error from Validate") + } +} + +func TestRevokeRequestSuccessful(t *testing.T) { + testhelper.SetupHTTP() + defer testhelper.TeardownHTTP() + client := prepareAuthTokenHandler(t, "DELETE", http.StatusNoContent) + + res := Revoke(&client, "abcdef12345") + testhelper.AssertNoErr(t, res.Err) +} + +func TestRevokeRequestError(t *testing.T) { + testhelper.SetupHTTP() + defer testhelper.TeardownHTTP() + client := prepareAuthTokenHandler(t, "DELETE", http.StatusNotFound) + + res := Revoke(&client, "abcdef12345") + if res.Err == nil { + t.Errorf("Missing expected error from Revoke") + } +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/tokens/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/tokens/results.go new file mode 100644 index 0000000000..d1fff4c2a5 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/tokens/results.go @@ -0,0 +1,73 @@ +package tokens + +import ( + "time" + + "github.com/mitchellh/mapstructure" + "github.com/rackspace/gophercloud" +) + +// commonResult is the deferred result of a Create or a Get call. +type commonResult struct { + gophercloud.Result +} + +// Extract interprets a commonResult as a Token. +func (r commonResult) Extract() (*Token, error) { + if r.Err != nil { + return nil, r.Err + } + + var response struct { + Token struct { + ExpiresAt string `mapstructure:"expires_at"` + } `mapstructure:"token"` + } + + var token Token + + // Parse the token itself from the stored headers. + token.ID = r.Header.Get("X-Subject-Token") + + err := mapstructure.Decode(r.Body, &response) + if err != nil { + return nil, err + } + + // Attempt to parse the timestamp. + token.ExpiresAt, err = time.Parse(gophercloud.RFC3339Milli, response.Token.ExpiresAt) + + return &token, err +} + +// CreateResult is the deferred response from a Create call. +type CreateResult struct { + commonResult +} + +// createErr quickly creates a CreateResult that reports an error. +func createErr(err error) CreateResult { + return CreateResult{ + commonResult: commonResult{Result: gophercloud.Result{Err: err}}, + } +} + +// GetResult is the deferred response from a Get call. +type GetResult struct { + commonResult +} + +// RevokeResult is the deferred response from a Revoke call. +type RevokeResult struct { + commonResult +} + +// Token is a string that grants a user access to a controlled set of services in an OpenStack provider. +// Each Token is valid for a set length of time. +type Token struct { + // ID is the issued token. + ID string + + // ExpiresAt is the timestamp at which this token will no longer be accepted. + ExpiresAt time.Time +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/tokens/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/tokens/urls.go new file mode 100644 index 0000000000..360b60a82f --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/tokens/urls.go @@ -0,0 +1,7 @@ +package tokens + +import "github.com/rackspace/gophercloud" + +func tokenURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("auth", "tokens") +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/tokens/urls_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/tokens/urls_test.go new file mode 100644 index 0000000000..549c398620 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/tokens/urls_test.go @@ -0,0 +1,21 @@ +package tokens + +import ( + "testing" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/testhelper" +) + +func TestTokenURL(t *testing.T) { + testhelper.SetupHTTP() + defer testhelper.TeardownHTTP() + + client := gophercloud.ServiceClient{Endpoint: testhelper.Endpoint()} + + expected := testhelper.Endpoint() + "auth/tokens" + actual := tokenURL(&client) + if actual != expected { + t.Errorf("Expected URL %s, but was %s", expected, actual) + } +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/apiversions/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/apiversions/doc.go new file mode 100644 index 0000000000..0208ee20ec --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/apiversions/doc.go @@ -0,0 +1,4 @@ +// Package apiversions provides information and interaction with the different +// API versions for the OpenStack Neutron service. This functionality is not +// restricted to this particular version. +package apiversions diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/apiversions/errors.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/apiversions/errors.go new file mode 100644 index 0000000000..76bdb14f75 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/apiversions/errors.go @@ -0,0 +1 @@ +package apiversions diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/apiversions/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/apiversions/requests.go new file mode 100644 index 0000000000..9fb6de1411 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/apiversions/requests.go @@ -0,0 +1,21 @@ +package apiversions + +import ( + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// ListVersions lists all the Neutron API versions available to end-users +func ListVersions(c *gophercloud.ServiceClient) pagination.Pager { + return pagination.NewPager(c, apiVersionsURL(c), func(r pagination.PageResult) pagination.Page { + return APIVersionPage{pagination.SinglePageBase(r)} + }) +} + +// ListVersionResources lists all of the different API resources for a particular +// API versions. Typical resources for Neutron might be: networks, subnets, etc. +func ListVersionResources(c *gophercloud.ServiceClient, v string) pagination.Pager { + return pagination.NewPager(c, apiInfoURL(c, v), func(r pagination.PageResult) pagination.Page { + return APIVersionResourcePage{pagination.SinglePageBase(r)} + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/apiversions/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/apiversions/requests_test.go new file mode 100644 index 0000000000..d35af9f0c6 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/apiversions/requests_test.go @@ -0,0 +1,182 @@ +package apiversions + +import ( + "fmt" + "net/http" + "testing" + + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestListVersions(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "versions": [ + { + "status": "CURRENT", + "id": "v2.0", + "links": [ + { + "href": "http://23.253.228.211:9696/v2.0", + "rel": "self" + } + ] + } + ] +}`) + }) + + count := 0 + + ListVersions(fake.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractAPIVersions(page) + if err != nil { + t.Errorf("Failed to extract API versions: %v", err) + return false, err + } + + expected := []APIVersion{ + APIVersion{ + Status: "CURRENT", + ID: "v2.0", + }, + } + + th.AssertDeepEquals(t, expected, actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestNonJSONCannotBeExtractedIntoAPIVersions(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }) + + ListVersions(fake.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) { + if _, err := ExtractAPIVersions(page); err == nil { + t.Fatalf("Expected error, got nil") + } + return true, nil + }) +} + +func TestAPIInfo(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "resources": [ + { + "links": [ + { + "href": "http://23.253.228.211:9696/v2.0/subnets", + "rel": "self" + } + ], + "name": "subnet", + "collection": "subnets" + }, + { + "links": [ + { + "href": "http://23.253.228.211:9696/v2.0/networks", + "rel": "self" + } + ], + "name": "network", + "collection": "networks" + }, + { + "links": [ + { + "href": "http://23.253.228.211:9696/v2.0/ports", + "rel": "self" + } + ], + "name": "port", + "collection": "ports" + } + ] +} + `) + }) + + count := 0 + + ListVersionResources(fake.ServiceClient(), "v2.0").EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractVersionResources(page) + if err != nil { + t.Errorf("Failed to extract version resources: %v", err) + return false, err + } + + expected := []APIVersionResource{ + APIVersionResource{ + Name: "subnet", + Collection: "subnets", + }, + APIVersionResource{ + Name: "network", + Collection: "networks", + }, + APIVersionResource{ + Name: "port", + Collection: "ports", + }, + } + + th.AssertDeepEquals(t, expected, actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestNonJSONCannotBeExtractedIntoAPIVersionResources(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }) + + ListVersionResources(fake.ServiceClient(), "v2.0").EachPage(func(page pagination.Page) (bool, error) { + if _, err := ExtractVersionResources(page); err == nil { + t.Fatalf("Expected error, got nil") + } + return true, nil + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/apiversions/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/apiversions/results.go new file mode 100644 index 0000000000..97159341ff --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/apiversions/results.go @@ -0,0 +1,77 @@ +package apiversions + +import ( + "github.com/mitchellh/mapstructure" + "github.com/rackspace/gophercloud/pagination" +) + +// APIVersion represents an API version for Neutron. It contains the status of +// the API, and its unique ID. +type APIVersion struct { + Status string `mapstructure:"status" json:"status"` + ID string `mapstructure:"id" json:"id"` +} + +// APIVersionPage is the page returned by a pager when traversing over a +// collection of API versions. +type APIVersionPage struct { + pagination.SinglePageBase +} + +// IsEmpty checks whether an APIVersionPage struct is empty. +func (r APIVersionPage) IsEmpty() (bool, error) { + is, err := ExtractAPIVersions(r) + if err != nil { + return true, err + } + return len(is) == 0, nil +} + +// ExtractAPIVersions takes a collection page, extracts all of the elements, +// and returns them a slice of APIVersion structs. It is effectively a cast. +func ExtractAPIVersions(page pagination.Page) ([]APIVersion, error) { + var resp struct { + Versions []APIVersion `mapstructure:"versions"` + } + + err := mapstructure.Decode(page.(APIVersionPage).Body, &resp) + + return resp.Versions, err +} + +// APIVersionResource represents a generic API resource. It contains the name +// of the resource and its plural collection name. +type APIVersionResource struct { + Name string `mapstructure:"name" json:"name"` + Collection string `mapstructure:"collection" json:"collection"` +} + +// APIVersionResourcePage is a concrete type which embeds the common +// SinglePageBase struct, and is used when traversing API versions collections. +type APIVersionResourcePage struct { + pagination.SinglePageBase +} + +// IsEmpty is a concrete function which indicates whether an +// APIVersionResourcePage is empty or not. +func (r APIVersionResourcePage) IsEmpty() (bool, error) { + is, err := ExtractVersionResources(r) + if err != nil { + return true, err + } + return len(is) == 0, nil +} + +// ExtractVersionResources accepts a Page struct, specifically a +// APIVersionResourcePage struct, and extracts the elements into a slice of +// APIVersionResource structs. In other words, the collection is mapped into +// a relevant slice. +func ExtractVersionResources(page pagination.Page) ([]APIVersionResource, error) { + var resp struct { + APIVersionResources []APIVersionResource `mapstructure:"resources"` + } + + err := mapstructure.Decode(page.(APIVersionResourcePage).Body, &resp) + + return resp.APIVersionResources, err +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/apiversions/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/apiversions/urls.go new file mode 100644 index 0000000000..58aa2b61f8 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/apiversions/urls.go @@ -0,0 +1,15 @@ +package apiversions + +import ( + "strings" + + "github.com/rackspace/gophercloud" +) + +func apiVersionsURL(c *gophercloud.ServiceClient) string { + return c.Endpoint +} + +func apiInfoURL(c *gophercloud.ServiceClient, version string) string { + return c.Endpoint + strings.TrimRight(version, "/") + "/" +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/apiversions/urls_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/apiversions/urls_test.go new file mode 100644 index 0000000000..7dd069c94f --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/apiversions/urls_test.go @@ -0,0 +1,26 @@ +package apiversions + +import ( + "testing" + + "github.com/rackspace/gophercloud" + th "github.com/rackspace/gophercloud/testhelper" +) + +const endpoint = "http://localhost:57909/" + +func endpointClient() *gophercloud.ServiceClient { + return &gophercloud.ServiceClient{Endpoint: endpoint} +} + +func TestAPIVersionsURL(t *testing.T) { + actual := apiVersionsURL(endpointClient()) + expected := endpoint + th.AssertEquals(t, expected, actual) +} + +func TestAPIInfoURL(t *testing.T) { + actual := apiInfoURL(endpointClient(), "v2.0") + expected := endpoint + "v2.0/" + th.AssertEquals(t, expected, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/common/common_tests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/common/common_tests.go new file mode 100644 index 0000000000..41603510d6 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/common/common_tests.go @@ -0,0 +1,14 @@ +package common + +import ( + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/testhelper/client" +) + +const TokenID = client.TokenID + +func ServiceClient() *gophercloud.ServiceClient { + sc := client.ServiceClient() + sc.ResourceBase = sc.Endpoint + "v2.0/" + return sc +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/delegate.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/delegate.go new file mode 100644 index 0000000000..d08e1fda97 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/delegate.go @@ -0,0 +1,41 @@ +package extensions + +import ( + "github.com/rackspace/gophercloud" + common "github.com/rackspace/gophercloud/openstack/common/extensions" + "github.com/rackspace/gophercloud/pagination" +) + +// Extension is a single OpenStack extension. +type Extension struct { + common.Extension +} + +// GetResult wraps a GetResult from common. +type GetResult struct { + common.GetResult +} + +// ExtractExtensions interprets a Page as a slice of Extensions. +func ExtractExtensions(page pagination.Page) ([]Extension, error) { + inner, err := common.ExtractExtensions(page) + if err != nil { + return nil, err + } + outer := make([]Extension, len(inner)) + for index, ext := range inner { + outer[index] = Extension{ext} + } + return outer, nil +} + +// Get retrieves information for a specific extension using its alias. +func Get(c *gophercloud.ServiceClient, alias string) GetResult { + return GetResult{common.Get(c, alias)} +} + +// List returns a Pager which allows you to iterate over the full collection of extensions. +// It does not accept query parameters. +func List(c *gophercloud.ServiceClient) pagination.Pager { + return common.List(c) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/delegate_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/delegate_test.go new file mode 100644 index 0000000000..3d2ac78d48 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/delegate_test.go @@ -0,0 +1,105 @@ +package extensions + +import ( + "fmt" + "net/http" + "testing" + + common "github.com/rackspace/gophercloud/openstack/common/extensions" + fake "github.com/rackspace/gophercloud/openstack/networking/v2/common" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/extensions", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + + fmt.Fprintf(w, ` +{ + "extensions": [ + { + "updated": "2013-01-20T00:00:00-00:00", + "name": "Neutron Service Type Management", + "links": [], + "namespace": "http://docs.openstack.org/ext/neutron/service-type/api/v1.0", + "alias": "service-type", + "description": "API for retrieving service providers for Neutron advanced services" + } + ] +} + `) + }) + + count := 0 + + List(fake.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractExtensions(page) + if err != nil { + t.Errorf("Failed to extract extensions: %v", err) + } + + expected := []Extension{ + Extension{ + common.Extension{ + Updated: "2013-01-20T00:00:00-00:00", + Name: "Neutron Service Type Management", + Links: []interface{}{}, + Namespace: "http://docs.openstack.org/ext/neutron/service-type/api/v1.0", + Alias: "service-type", + Description: "API for retrieving service providers for Neutron advanced services", + }, + }, + } + + th.AssertDeepEquals(t, expected, actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/extensions/agent", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "extension": { + "updated": "2013-02-03T10:00:00-00:00", + "name": "agent", + "links": [], + "namespace": "http://docs.openstack.org/ext/agent/api/v2.0", + "alias": "agent", + "description": "The agent management extension." + } +} + `) + }) + + ext, err := Get(fake.ServiceClient(), "agent").Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, ext.Updated, "2013-02-03T10:00:00-00:00") + th.AssertEquals(t, ext.Name, "agent") + th.AssertEquals(t, ext.Namespace, "http://docs.openstack.org/ext/agent/api/v2.0") + th.AssertEquals(t, ext.Alias, "agent") + th.AssertEquals(t, ext.Description, "The agent management extension.") +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/external/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/external/doc.go new file mode 100644 index 0000000000..dad3a844f7 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/external/doc.go @@ -0,0 +1,3 @@ +// Package external provides information and interaction with the external +// extension for the OpenStack Networking service. +package external diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/external/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/external/requests.go new file mode 100644 index 0000000000..2f04593db9 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/external/requests.go @@ -0,0 +1,56 @@ +package external + +import "github.com/rackspace/gophercloud/openstack/networking/v2/networks" + +// AdminState gives users a solid type to work with for create and update +// operations. It is recommended that users use the `Up` and `Down` enums. +type AdminState *bool + +// Convenience vars for AdminStateUp values. +var ( + iTrue = true + iFalse = false + + Up AdminState = &iTrue + Down AdminState = &iFalse +) + +// CreateOpts is the structure used when creating new external network +// resources. It embeds networks.CreateOpts and so inherits all of its required +// and optional fields, with the addition of the External field. +type CreateOpts struct { + Parent networks.CreateOpts + External bool +} + +// ToNetworkCreateMap casts a CreateOpts struct to a map. +func (o CreateOpts) ToNetworkCreateMap() (map[string]interface{}, error) { + outer, err := o.Parent.ToNetworkCreateMap() + if err != nil { + return nil, err + } + + outer["network"].(map[string]interface{})["router:external"] = o.External + + return outer, nil +} + +// UpdateOpts is the structure used when updating existing external network +// resources. It embeds networks.UpdateOpts and so inherits all of its required +// and optional fields, with the addition of the External field. +type UpdateOpts struct { + Parent networks.UpdateOpts + External bool +} + +// ToNetworkUpdateMap casts an UpdateOpts struct to a map. +func (o UpdateOpts) ToNetworkUpdateMap() (map[string]interface{}, error) { + outer, err := o.Parent.ToNetworkUpdateMap() + if err != nil { + return nil, err + } + + outer["network"].(map[string]interface{})["router:external"] = o.External + + return outer, nil +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/external/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/external/results.go new file mode 100644 index 0000000000..1c173c07a3 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/external/results.go @@ -0,0 +1,81 @@ +package external + +import ( + "github.com/mitchellh/mapstructure" + "github.com/rackspace/gophercloud/openstack/networking/v2/networks" + "github.com/rackspace/gophercloud/pagination" +) + +// NetworkExternal represents a decorated form of a Network with based on the +// "external-net" extension. +type NetworkExternal struct { + // UUID for the network + ID string `mapstructure:"id" json:"id"` + + // Human-readable name for the network. Might not be unique. + Name string `mapstructure:"name" json:"name"` + + // The administrative state of network. If false (down), the network does not forward packets. + AdminStateUp bool `mapstructure:"admin_state_up" json:"admin_state_up"` + + // Indicates whether network is currently operational. Possible values include + // `ACTIVE', `DOWN', `BUILD', or `ERROR'. Plug-ins might define additional values. + Status string `mapstructure:"status" json:"status"` + + // Subnets associated with this network. + Subnets []string `mapstructure:"subnets" json:"subnets"` + + // Owner of network. Only admin users can specify a tenant_id other than its own. + TenantID string `mapstructure:"tenant_id" json:"tenant_id"` + + // Specifies whether the network resource can be accessed by any tenant or not. + Shared bool `mapstructure:"shared" json:"shared"` + + // Specifies whether the network is an external network or not. + External bool `mapstructure:"router:external" json:"router:external"` +} + +func commonExtract(e error, response interface{}) (*NetworkExternal, error) { + if e != nil { + return nil, e + } + + var res struct { + Network *NetworkExternal `json:"network"` + } + + err := mapstructure.Decode(response, &res) + + return res.Network, err +} + +// ExtractGet decorates a GetResult struct returned from a networks.Get() +// function with extended attributes. +func ExtractGet(r networks.GetResult) (*NetworkExternal, error) { + return commonExtract(r.Err, r.Body) +} + +// ExtractCreate decorates a CreateResult struct returned from a networks.Create() +// function with extended attributes. +func ExtractCreate(r networks.CreateResult) (*NetworkExternal, error) { + return commonExtract(r.Err, r.Body) +} + +// ExtractUpdate decorates a UpdateResult struct returned from a +// networks.Update() function with extended attributes. +func ExtractUpdate(r networks.UpdateResult) (*NetworkExternal, error) { + return commonExtract(r.Err, r.Body) +} + +// ExtractList accepts a Page struct, specifically a NetworkPage struct, and +// extracts the elements into a slice of NetworkExtAttrs structs. In other +// words, a generic collection is mapped into a relevant slice. +func ExtractList(page pagination.Page) ([]NetworkExternal, error) { + var resp struct { + Networks []NetworkExternal `mapstructure:"networks" json:"networks"` + } + + err := mapstructure.Decode(page.(networks.NetworkPage).Body, &resp) + + return resp.Networks, err +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/external/results_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/external/results_test.go new file mode 100644 index 0000000000..916cd2cfd0 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/external/results_test.go @@ -0,0 +1,254 @@ +package external + +import ( + "errors" + "fmt" + "net/http" + "testing" + + "github.com/rackspace/gophercloud/openstack/networking/v2/networks" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/networks", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "networks": [ + { + "admin_state_up": true, + "id": "0f38d5ad-10a6-428f-a5fc-825cfe0f1970", + "name": "net1", + "router:external": false, + "shared": false, + "status": "ACTIVE", + "subnets": [ + "25778974-48a8-46e7-8998-9dc8c70d2f06" + ], + "tenant_id": "b575417a6c444a6eb5cc3a58eb4f714a" + }, + { + "admin_state_up": true, + "id": "8d05a1b1-297a-46ca-8974-17debf51ca3c", + "name": "ext_net", + "router:external": true, + "shared": false, + "status": "ACTIVE", + "subnets": [ + "2f1fb918-9b0e-4bf9-9a50-6cebbb4db2c5" + ], + "tenant_id": "5eb8995cf717462c9df8d1edfa498010" + } + ] +} + `) + }) + + count := 0 + + networks.List(fake.ServiceClient(), networks.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractList(page) + if err != nil { + t.Errorf("Failed to extract networks: %v", err) + return false, err + } + + expected := []NetworkExternal{ + NetworkExternal{ + Status: "ACTIVE", + Subnets: []string{"25778974-48a8-46e7-8998-9dc8c70d2f06"}, + Name: "net1", + AdminStateUp: true, + TenantID: "b575417a6c444a6eb5cc3a58eb4f714a", + Shared: false, + ID: "0f38d5ad-10a6-428f-a5fc-825cfe0f1970", + External: false, + }, + NetworkExternal{ + Status: "ACTIVE", + Subnets: []string{"2f1fb918-9b0e-4bf9-9a50-6cebbb4db2c5"}, + Name: "ext_net", + AdminStateUp: true, + TenantID: "5eb8995cf717462c9df8d1edfa498010", + Shared: false, + ID: "8d05a1b1-297a-46ca-8974-17debf51ca3c", + External: true, + }, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/networks/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "network": { + "admin_state_up": true, + "id": "8d05a1b1-297a-46ca-8974-17debf51ca3c", + "name": "ext_net", + "router:external": true, + "shared": false, + "status": "ACTIVE", + "subnets": [ + "2f1fb918-9b0e-4bf9-9a50-6cebbb4db2c5" + ], + "tenant_id": "5eb8995cf717462c9df8d1edfa498010" + } +} + `) + }) + + res := networks.Get(fake.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22") + n, err := ExtractGet(res) + + th.AssertNoErr(t, err) + th.AssertEquals(t, true, n.External) +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/networks", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "network": { + "admin_state_up": true, + "name": "ext_net", + "router:external": true + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, ` +{ + "network": { + "admin_state_up": true, + "id": "8d05a1b1-297a-46ca-8974-17debf51ca3c", + "name": "ext_net", + "router:external": true, + "shared": false, + "status": "ACTIVE", + "subnets": [ + "2f1fb918-9b0e-4bf9-9a50-6cebbb4db2c5" + ], + "tenant_id": "5eb8995cf717462c9df8d1edfa498010" + } +} + `) + }) + + options := CreateOpts{networks.CreateOpts{Name: "ext_net", AdminStateUp: Up}, true} + res := networks.Create(fake.ServiceClient(), options) + + n, err := ExtractCreate(res) + + th.AssertNoErr(t, err) + th.AssertEquals(t, true, n.External) +} + +func TestUpdate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/networks/4e8e5957-649f-477b-9e5b-f1f75b21c03c", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "network": { + "router:external": true, + "name": "new_name" + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "network": { + "admin_state_up": true, + "id": "8d05a1b1-297a-46ca-8974-17debf51ca3c", + "name": "new_name", + "router:external": true, + "shared": false, + "status": "ACTIVE", + "subnets": [ + "2f1fb918-9b0e-4bf9-9a50-6cebbb4db2c5" + ], + "tenant_id": "5eb8995cf717462c9df8d1edfa498010" + } +} + `) + }) + + options := UpdateOpts{networks.UpdateOpts{Name: "new_name"}, true} + res := networks.Update(fake.ServiceClient(), "4e8e5957-649f-477b-9e5b-f1f75b21c03c", options) + n, err := ExtractUpdate(res) + + th.AssertNoErr(t, err) + th.AssertEquals(t, true, n.External) +} + +func TestExtractFnsReturnsErrWhenResultContainsErr(t *testing.T) { + gr := networks.GetResult{} + gr.Err = errors.New("") + + if _, err := ExtractGet(gr); err == nil { + t.Fatalf("Expected error, got one") + } + + ur := networks.UpdateResult{} + ur.Err = errors.New("") + + if _, err := ExtractUpdate(ur); err == nil { + t.Fatalf("Expected error, got one") + } + + cr := networks.CreateResult{} + cr.Err = errors.New("") + + if _, err := ExtractCreate(cr); err == nil { + t.Fatalf("Expected error, got one") + } +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/doc.go new file mode 100644 index 0000000000..d533458267 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/doc.go @@ -0,0 +1,5 @@ +// Package layer3 provides access to the Layer-3 networking extension for the +// OpenStack Neutron service. This extension allows API users to route packets +// between subnets, forward packets from internal networks to external ones, +// and access instances from external networks through floating IPs. +package layer3 diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/floatingips/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/floatingips/requests.go new file mode 100644 index 0000000000..d23f9e2b5a --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/floatingips/requests.go @@ -0,0 +1,190 @@ +package floatingips + +import ( + "fmt" + + "github.com/racker/perigee" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the floating IP attributes you want to see returned. SortKey allows you to +// sort by a particular network attribute. SortDir sets the direction, and is +// either `asc' or `desc'. Marker and Limit are used for pagination. +type ListOpts struct { + ID string `q:"id"` + FloatingNetworkID string `q:"floating_network_id"` + PortID string `q:"port_id"` + FixedIP string `q:"fixed_ip_address"` + FloatingIP string `q:"floating_ip_address"` + TenantID string `q:"tenant_id"` + Limit int `q:"limit"` + Marker string `q:"marker"` + SortKey string `q:"sort_key"` + SortDir string `q:"sort_dir"` +} + +// List returns a Pager which allows you to iterate over a collection of +// floating IP resources. It accepts a ListOpts struct, which allows you to +// filter and sort the returned collection for greater efficiency. +func List(c *gophercloud.ServiceClient, opts ListOpts) pagination.Pager { + q, err := gophercloud.BuildQueryString(&opts) + if err != nil { + return pagination.Pager{Err: err} + } + u := rootURL(c) + q.String() + return pagination.NewPager(c, u, func(r pagination.PageResult) pagination.Page { + return FloatingIPPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// CreateOpts contains all the values needed to create a new floating IP +// resource. The only required fields are FloatingNetworkID and PortID which +// refer to the external network and internal port respectively. +type CreateOpts struct { + FloatingNetworkID string + FloatingIP string + PortID string + FixedIP string + TenantID string +} + +var ( + errFloatingNetworkIDRequired = fmt.Errorf("A NetworkID is required") + errPortIDRequired = fmt.Errorf("A PortID is required") +) + +// Create accepts a CreateOpts struct and uses the values provided to create a +// new floating IP resource. You can create floating IPs on external networks +// only. If you provide a FloatingNetworkID which refers to a network that is +// not external (i.e. its `router:external' attribute is False), the operation +// will fail and return a 400 error. +// +// If you do not specify a FloatingIP address value, the operation will +// automatically allocate an available address for the new resource. If you do +// choose to specify one, it must fall within the subnet range for the external +// network - otherwise the operation returns a 400 error. If the FloatingIP +// address is already in use, the operation returns a 409 error code. +// +// You can associate the new resource with an internal port by using the PortID +// field. If you specify a PortID that is not valid, the operation will fail and +// return 404 error code. +// +// You must also configure an IP address for the port associated with the PortID +// you have provided - this is what the FixedIP refers to: an IP fixed to a port. +// Because a port might be associated with multiple IP addresses, you can use +// the FixedIP field to associate a particular IP address rather than have the +// API assume for you. If you specify an IP address that is not valid, the +// operation will fail and return a 400 error code. If the PortID and FixedIP +// are already associated with another resource, the operation will fail and +// returns a 409 error code. +func Create(c *gophercloud.ServiceClient, opts CreateOpts) CreateResult { + var res CreateResult + + // Validate + if opts.FloatingNetworkID == "" { + res.Err = errFloatingNetworkIDRequired + return res + } + if opts.PortID == "" { + res.Err = errPortIDRequired + return res + } + + // Define structures + type floatingIP struct { + FloatingNetworkID string `json:"floating_network_id"` + FloatingIP string `json:"floating_ip_address,omitempty"` + PortID string `json:"port_id"` + FixedIP string `json:"fixed_ip_address,omitempty"` + TenantID string `json:"tenant_id,omitempty"` + } + type request struct { + FloatingIP floatingIP `json:"floatingip"` + } + + // Populate request body + reqBody := request{FloatingIP: floatingIP{ + FloatingNetworkID: opts.FloatingNetworkID, + PortID: opts.PortID, + FixedIP: opts.FixedIP, + TenantID: opts.TenantID, + }} + + // Send request to API + _, res.Err = perigee.Request("POST", rootURL(c), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + ReqBody: &reqBody, + Results: &res.Body, + OkCodes: []int{201}, + }) + + return res +} + +// Get retrieves a particular floating IP resource based on its unique ID. +func Get(c *gophercloud.ServiceClient, id string) GetResult { + var res GetResult + _, res.Err = perigee.Request("GET", resourceURL(c, id), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + Results: &res.Body, + OkCodes: []int{200}, + }) + return res +} + +// UpdateOpts contains the values used when updating a floating IP resource. The +// only value that can be updated is which internal port the floating IP is +// linked to. To associate the floating IP with a new internal port, provide its +// ID. To disassociate the floating IP from all ports, provide an empty string. +type UpdateOpts struct { + PortID string +} + +// Update allows floating IP resources to be updated. Currently, the only way to +// "update" a floating IP is to associate it with a new internal port, or +// disassociated it from all ports. See UpdateOpts for instructions of how to +// do this. +func Update(c *gophercloud.ServiceClient, id string, opts UpdateOpts) UpdateResult { + type floatingIP struct { + PortID *string `json:"port_id"` + } + + type request struct { + FloatingIP floatingIP `json:"floatingip"` + } + + var portID *string + if opts.PortID == "" { + portID = nil + } else { + portID = &opts.PortID + } + + reqBody := request{FloatingIP: floatingIP{PortID: portID}} + + // Send request to API + var res UpdateResult + _, res.Err = perigee.Request("PUT", resourceURL(c, id), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + ReqBody: &reqBody, + Results: &res.Body, + OkCodes: []int{200}, + }) + + return res +} + +// Delete will permanently delete a particular floating IP resource. Please +// ensure this is what you want - you can also disassociate the IP from existing +// internal ports. +func Delete(c *gophercloud.ServiceClient, id string) DeleteResult { + var res DeleteResult + _, res.Err = perigee.Request("DELETE", resourceURL(c, id), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + OkCodes: []int{204}, + }) + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/floatingips/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/floatingips/requests_test.go new file mode 100644 index 0000000000..19614be2ef --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/floatingips/requests_test.go @@ -0,0 +1,306 @@ +package floatingips + +import ( + "fmt" + "net/http" + "testing" + + fake "github.com/rackspace/gophercloud/openstack/networking/v2/common" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/floatingips", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "floatingips": [ + { + "floating_network_id": "6d67c30a-ddb4-49a1-bec3-a65b286b4170", + "router_id": null, + "fixed_ip_address": null, + "floating_ip_address": "192.0.0.4", + "tenant_id": "017d8de156df4177889f31a9bd6edc00", + "status": "DOWN", + "port_id": null, + "id": "2f95fd2b-9f6a-4e8e-9e9a-2cbe286cbf9e" + }, + { + "floating_network_id": "90f742b1-6d17-487b-ba95-71881dbc0b64", + "router_id": "0a24cb83-faf5-4d7f-b723-3144ed8a2167", + "fixed_ip_address": "192.0.0.2", + "floating_ip_address": "10.0.0.3", + "tenant_id": "017d8de156df4177889f31a9bd6edc00", + "status": "DOWN", + "port_id": "74a342ce-8e07-4e91-880c-9f834b68fa25", + "id": "ada25a95-f321-4f59-b0e0-f3a970dd3d63" + } + ] +} + `) + }) + + count := 0 + + List(fake.ServiceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractFloatingIPs(page) + if err != nil { + t.Errorf("Failed to extract floating IPs: %v", err) + return false, err + } + + expected := []FloatingIP{ + FloatingIP{ + FloatingNetworkID: "6d67c30a-ddb4-49a1-bec3-a65b286b4170", + FixedIP: "", + FloatingIP: "192.0.0.4", + TenantID: "017d8de156df4177889f31a9bd6edc00", + Status: "DOWN", + PortID: "", + ID: "2f95fd2b-9f6a-4e8e-9e9a-2cbe286cbf9e", + }, + FloatingIP{ + FloatingNetworkID: "90f742b1-6d17-487b-ba95-71881dbc0b64", + FixedIP: "192.0.0.2", + FloatingIP: "10.0.0.3", + TenantID: "017d8de156df4177889f31a9bd6edc00", + Status: "DOWN", + PortID: "74a342ce-8e07-4e91-880c-9f834b68fa25", + ID: "ada25a95-f321-4f59-b0e0-f3a970dd3d63", + }, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestInvalidNextPageURLs(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/floatingips", func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, `{"floatingips": [{}], "floatingips_links": {}}`) + }) + + List(fake.ServiceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + ExtractFloatingIPs(page) + return true, nil + }) +} + +func TestRequiredFieldsForCreate(t *testing.T) { + res1 := Create(fake.ServiceClient(), CreateOpts{FloatingNetworkID: ""}) + if res1.Err == nil { + t.Fatalf("Expected error, got none") + } + + res2 := Create(fake.ServiceClient(), CreateOpts{FloatingNetworkID: "foo", PortID: ""}) + if res2.Err == nil { + t.Fatalf("Expected error, got none") + } +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/floatingips", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "floatingip": { + "floating_network_id": "376da547-b977-4cfe-9cba-275c80debf57", + "port_id": "ce705c24-c1ef-408a-bda3-7bbd946164ab" + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, ` +{ + "floatingip": { + "router_id": "d23abc8d-2991-4a55-ba98-2aaea84cc72f", + "tenant_id": "4969c491a3c74ee4af974e6d800c62de", + "floating_network_id": "376da547-b977-4cfe-9cba-275c80debf57", + "fixed_ip_address": "10.0.0.3", + "floating_ip_address": "", + "port_id": "ce705c24-c1ef-408a-bda3-7bbd946164ab", + "id": "2f245a7b-796b-4f26-9cf9-9e82d248fda7" + } +} + `) + }) + + options := CreateOpts{ + FloatingNetworkID: "376da547-b977-4cfe-9cba-275c80debf57", + PortID: "ce705c24-c1ef-408a-bda3-7bbd946164ab", + } + + ip, err := Create(fake.ServiceClient(), options).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, "2f245a7b-796b-4f26-9cf9-9e82d248fda7", ip.ID) + th.AssertEquals(t, "4969c491a3c74ee4af974e6d800c62de", ip.TenantID) + th.AssertEquals(t, "376da547-b977-4cfe-9cba-275c80debf57", ip.FloatingNetworkID) + th.AssertEquals(t, "", ip.FloatingIP) + th.AssertEquals(t, "ce705c24-c1ef-408a-bda3-7bbd946164ab", ip.PortID) + th.AssertEquals(t, "10.0.0.3", ip.FixedIP) +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/floatingips/2f245a7b-796b-4f26-9cf9-9e82d248fda7", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "floatingip": { + "floating_network_id": "90f742b1-6d17-487b-ba95-71881dbc0b64", + "fixed_ip_address": "192.0.0.2", + "floating_ip_address": "10.0.0.3", + "tenant_id": "017d8de156df4177889f31a9bd6edc00", + "status": "DOWN", + "port_id": "74a342ce-8e07-4e91-880c-9f834b68fa25", + "id": "2f245a7b-796b-4f26-9cf9-9e82d248fda7" + } +} + `) + }) + + ip, err := Get(fake.ServiceClient(), "2f245a7b-796b-4f26-9cf9-9e82d248fda7").Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, "90f742b1-6d17-487b-ba95-71881dbc0b64", ip.FloatingNetworkID) + th.AssertEquals(t, "10.0.0.3", ip.FloatingIP) + th.AssertEquals(t, "74a342ce-8e07-4e91-880c-9f834b68fa25", ip.PortID) + th.AssertEquals(t, "192.0.0.2", ip.FixedIP) + th.AssertEquals(t, "017d8de156df4177889f31a9bd6edc00", ip.TenantID) + th.AssertEquals(t, "DOWN", ip.Status) + th.AssertEquals(t, "2f245a7b-796b-4f26-9cf9-9e82d248fda7", ip.ID) +} + +func TestAssociate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/floatingips/2f245a7b-796b-4f26-9cf9-9e82d248fda7", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "floatingip": { + "port_id": "423abc8d-2991-4a55-ba98-2aaea84cc72e" + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "floatingip": { + "router_id": "d23abc8d-2991-4a55-ba98-2aaea84cc72f", + "tenant_id": "4969c491a3c74ee4af974e6d800c62de", + "floating_network_id": "376da547-b977-4cfe-9cba-275c80debf57", + "fixed_ip_address": null, + "floating_ip_address": "172.24.4.228", + "port_id": "423abc8d-2991-4a55-ba98-2aaea84cc72e", + "id": "2f245a7b-796b-4f26-9cf9-9e82d248fda7" + } +} + `) + }) + + ip, err := Update(fake.ServiceClient(), "2f245a7b-796b-4f26-9cf9-9e82d248fda7", UpdateOpts{PortID: "423abc8d-2991-4a55-ba98-2aaea84cc72e"}).Extract() + th.AssertNoErr(t, err) + + th.AssertDeepEquals(t, "423abc8d-2991-4a55-ba98-2aaea84cc72e", ip.PortID) +} + +func TestDisassociate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/floatingips/2f245a7b-796b-4f26-9cf9-9e82d248fda7", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "floatingip": { + "port_id": null + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "floatingip": { + "router_id": "d23abc8d-2991-4a55-ba98-2aaea84cc72f", + "tenant_id": "4969c491a3c74ee4af974e6d800c62de", + "floating_network_id": "376da547-b977-4cfe-9cba-275c80debf57", + "fixed_ip_address": null, + "floating_ip_address": "172.24.4.228", + "port_id": null, + "id": "2f245a7b-796b-4f26-9cf9-9e82d248fda7" + } +} + `) + }) + + ip, err := Update(fake.ServiceClient(), "2f245a7b-796b-4f26-9cf9-9e82d248fda7", UpdateOpts{}).Extract() + th.AssertNoErr(t, err) + + th.AssertDeepEquals(t, "", ip.FixedIP) + th.AssertDeepEquals(t, "", ip.PortID) +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/floatingips/2f245a7b-796b-4f26-9cf9-9e82d248fda7", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusNoContent) + }) + + res := Delete(fake.ServiceClient(), "2f245a7b-796b-4f26-9cf9-9e82d248fda7") + th.AssertNoErr(t, res.Err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/floatingips/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/floatingips/results.go new file mode 100644 index 0000000000..a1c7afe2ce --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/floatingips/results.go @@ -0,0 +1,127 @@ +package floatingips + +import ( + "fmt" + + "github.com/mitchellh/mapstructure" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// FloatingIP represents a floating IP resource. A floating IP is an external +// IP address that is mapped to an internal port and, optionally, a specific +// IP address on a private network. In other words, it enables access to an +// instance on a private network from an external network. For this reason, +// floating IPs can only be defined on networks where the `router:external' +// attribute (provided by the external network extension) is set to True. +type FloatingIP struct { + // Unique identifier for the floating IP instance. + ID string `json:"id" mapstructure:"id"` + + // UUID of the external network where the floating IP is to be created. + FloatingNetworkID string `json:"floating_network_id" mapstructure:"floating_network_id"` + + // Address of the floating IP on the external network. + FloatingIP string `json:"floating_ip_address" mapstructure:"floating_ip_address"` + + // UUID of the port on an internal network that is associated with the floating IP. + PortID string `json:"port_id" mapstructure:"port_id"` + + // The specific IP address of the internal port which should be associated + // with the floating IP. + FixedIP string `json:"fixed_ip_address" mapstructure:"fixed_ip_address"` + + // Owner of the floating IP. Only admin users can specify a tenant identifier + // other than its own. + TenantID string `json:"tenant_id" mapstructure:"tenant_id"` + + // The condition of the API resource. + Status string `json:"status" mapstructure:"status"` +} + +type commonResult struct { + gophercloud.Result +} + +// Extract a result and extracts a FloatingIP resource. +func (r commonResult) Extract() (*FloatingIP, error) { + if r.Err != nil { + return nil, r.Err + } + + var res struct { + FloatingIP *FloatingIP `json:"floatingip"` + } + + err := mapstructure.Decode(r.Body, &res) + if err != nil { + return nil, fmt.Errorf("Error decoding Neutron floating IP: %v", err) + } + + return res.FloatingIP, nil +} + +// CreateResult represents the result of a create operation. +type CreateResult struct { + commonResult +} + +// GetResult represents the result of a get operation. +type GetResult struct { + commonResult +} + +// UpdateResult represents the result of an update operation. +type UpdateResult struct { + commonResult +} + +// DeleteResult represents the result of an update operation. +type DeleteResult struct { + gophercloud.ErrResult +} + +// FloatingIPPage is the page returned by a pager when traversing over a +// collection of floating IPs. +type FloatingIPPage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of floating IPs has reached +// the end of a page and the pager seeks to traverse over a new one. In order +// to do this, it needs to construct the next page's URL. +func (p FloatingIPPage) NextPageURL() (string, error) { + type resp struct { + Links []gophercloud.Link `mapstructure:"floatingips_links"` + } + + var r resp + err := mapstructure.Decode(p.Body, &r) + if err != nil { + return "", err + } + + return gophercloud.ExtractNextURL(r.Links) +} + +// IsEmpty checks whether a NetworkPage struct is empty. +func (p FloatingIPPage) IsEmpty() (bool, error) { + is, err := ExtractFloatingIPs(p) + if err != nil { + return true, nil + } + return len(is) == 0, nil +} + +// ExtractFloatingIPs accepts a Page struct, specifically a FloatingIPPage struct, +// and extracts the elements into a slice of FloatingIP structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractFloatingIPs(page pagination.Page) ([]FloatingIP, error) { + var resp struct { + FloatingIPs []FloatingIP `mapstructure:"floatingips" json:"floatingips"` + } + + err := mapstructure.Decode(page.(FloatingIPPage).Body, &resp) + + return resp.FloatingIPs, err +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/floatingips/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/floatingips/urls.go new file mode 100644 index 0000000000..355f20dc09 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/floatingips/urls.go @@ -0,0 +1,13 @@ +package floatingips + +import "github.com/rackspace/gophercloud" + +const resourcePath = "floatingips" + +func rootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(resourcePath) +} + +func resourceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(resourcePath, id) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/routers/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/routers/requests.go new file mode 100644 index 0000000000..e3a144171b --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/routers/requests.go @@ -0,0 +1,246 @@ +package routers + +import ( + "errors" + + "github.com/racker/perigee" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the floating IP attributes you want to see returned. SortKey allows you to +// sort by a particular network attribute. SortDir sets the direction, and is +// either `asc' or `desc'. Marker and Limit are used for pagination. +type ListOpts struct { + ID string `q:"id"` + Name string `q:"name"` + AdminStateUp *bool `q:"admin_state_up"` + Status string `q:"status"` + TenantID string `q:"tenant_id"` + Limit int `q:"limit"` + Marker string `q:"marker"` + SortKey string `q:"sort_key"` + SortDir string `q:"sort_dir"` +} + +// List returns a Pager which allows you to iterate over a collection of +// routers. It accepts a ListOpts struct, which allows you to filter and sort +// the returned collection for greater efficiency. +// +// Default policy settings return only those routers that are owned by the +// tenant who submits the request, unless an admin user submits the request. +func List(c *gophercloud.ServiceClient, opts ListOpts) pagination.Pager { + q, err := gophercloud.BuildQueryString(&opts) + if err != nil { + return pagination.Pager{Err: err} + } + u := rootURL(c) + q.String() + return pagination.NewPager(c, u, func(r pagination.PageResult) pagination.Page { + return RouterPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// CreateOpts contains all the values needed to create a new router. There are +// no required values. +type CreateOpts struct { + Name string + AdminStateUp *bool + TenantID string + GatewayInfo *GatewayInfo +} + +// Create accepts a CreateOpts struct and uses the values to create a new +// logical router. When it is created, the router does not have an internal +// interface - it is not associated to any subnet. +// +// You can optionally specify an external gateway for a router using the +// GatewayInfo struct. The external gateway for the router must be plugged into +// an external network (it is external if its `router:external' field is set to +// true). +func Create(c *gophercloud.ServiceClient, opts CreateOpts) CreateResult { + type router struct { + Name *string `json:"name,omitempty"` + AdminStateUp *bool `json:"admin_state_up,omitempty"` + TenantID *string `json:"tenant_id,omitempty"` + GatewayInfo *GatewayInfo `json:"external_gateway_info,omitempty"` + } + + type request struct { + Router router `json:"router"` + } + + reqBody := request{Router: router{ + Name: gophercloud.MaybeString(opts.Name), + AdminStateUp: opts.AdminStateUp, + TenantID: gophercloud.MaybeString(opts.TenantID), + }} + + if opts.GatewayInfo != nil { + reqBody.Router.GatewayInfo = opts.GatewayInfo + } + + var res CreateResult + _, res.Err = perigee.Request("POST", rootURL(c), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + ReqBody: &reqBody, + Results: &res.Body, + OkCodes: []int{201}, + }) + return res +} + +// Get retrieves a particular router based on its unique ID. +func Get(c *gophercloud.ServiceClient, id string) GetResult { + var res GetResult + _, res.Err = perigee.Request("GET", resourceURL(c, id), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + Results: &res.Body, + OkCodes: []int{200}, + }) + return res +} + +// UpdateOpts contains the values used when updating a router. +type UpdateOpts struct { + Name string + AdminStateUp *bool + GatewayInfo *GatewayInfo +} + +// Update allows routers to be updated. You can update the name, administrative +// state, and the external gateway. For more information about how to set the +// external gateway for a router, see Create. This operation does not enable +// the update of router interfaces. To do this, use the AddInterface and +// RemoveInterface functions. +func Update(c *gophercloud.ServiceClient, id string, opts UpdateOpts) UpdateResult { + type router struct { + Name *string `json:"name,omitempty"` + AdminStateUp *bool `json:"admin_state_up,omitempty"` + GatewayInfo *GatewayInfo `json:"external_gateway_info,omitempty"` + } + + type request struct { + Router router `json:"router"` + } + + reqBody := request{Router: router{ + Name: gophercloud.MaybeString(opts.Name), + AdminStateUp: opts.AdminStateUp, + }} + + if opts.GatewayInfo != nil { + reqBody.Router.GatewayInfo = opts.GatewayInfo + } + + // Send request to API + var res UpdateResult + _, res.Err = perigee.Request("PUT", resourceURL(c, id), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + ReqBody: &reqBody, + Results: &res.Body, + OkCodes: []int{200}, + }) + + return res +} + +// Delete will permanently delete a particular router based on its unique ID. +func Delete(c *gophercloud.ServiceClient, id string) DeleteResult { + var res DeleteResult + _, res.Err = perigee.Request("DELETE", resourceURL(c, id), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + OkCodes: []int{204}, + }) + return res +} + +var errInvalidInterfaceOpts = errors.New("When adding a router interface you must provide either a subnet ID or a port ID") + +// InterfaceOpts allow you to work with operations that either add or remote +// an internal interface from a router. +type InterfaceOpts struct { + SubnetID string + PortID string +} + +// AddInterface attaches a subnet to an internal router interface. You must +// specify either a SubnetID or PortID in the request body. If you specify both, +// the operation will fail and an error will be returned. +// +// If you specify a SubnetID, the gateway IP address for that particular subnet +// is used to create the router interface. Alternatively, if you specify a +// PortID, the IP address associated with the port is used to create the router +// interface. +// +// If you reference a port that is associated with multiple IP addresses, or +// if the port is associated with zero IP addresses, the operation will fail and +// a 400 Bad Request error will be returned. +// +// If you reference a port already in use, the operation will fail and a 409 +// Conflict error will be returned. +// +// The PortID that is returned after using Extract() on the result of this +// operation can either be the same PortID passed in or, on the other hand, the +// identifier of a new port created by this operation. After the operation +// completes, the device ID of the port is set to the router ID, and the +// device owner attribute is set to `network:router_interface'. +func AddInterface(c *gophercloud.ServiceClient, id string, opts InterfaceOpts) InterfaceResult { + var res InterfaceResult + + // Validate + if (opts.SubnetID == "" && opts.PortID == "") || (opts.SubnetID != "" && opts.PortID != "") { + res.Err = errInvalidInterfaceOpts + return res + } + + type request struct { + SubnetID string `json:"subnet_id,omitempty"` + PortID string `json:"port_id,omitempty"` + } + + body := request{SubnetID: opts.SubnetID, PortID: opts.PortID} + + _, res.Err = perigee.Request("PUT", addInterfaceURL(c, id), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + ReqBody: &body, + Results: &res.Body, + OkCodes: []int{200}, + }) + + return res +} + +// RemoveInterface removes an internal router interface, which detaches a +// subnet from the router. You must specify either a SubnetID or PortID, since +// these values are used to identify the router interface to remove. +// +// Unlike AddInterface, you can also specify both a SubnetID and PortID. If you +// choose to specify both, the subnet ID must correspond to the subnet ID of +// the first IP address on the port specified by the port ID. Otherwise, the +// operation will fail and return a 409 Conflict error. +// +// If the router, subnet or port which are referenced do not exist or are not +// visible to you, the operation will fail and a 404 Not Found error will be +// returned. After this operation completes, the port connecting the router +// with the subnet is removed from the subnet for the network. +func RemoveInterface(c *gophercloud.ServiceClient, id string, opts InterfaceOpts) InterfaceResult { + var res InterfaceResult + + type request struct { + SubnetID string `json:"subnet_id,omitempty"` + PortID string `json:"port_id,omitempty"` + } + + body := request{SubnetID: opts.SubnetID, PortID: opts.PortID} + + _, res.Err = perigee.Request("PUT", removeInterfaceURL(c, id), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + ReqBody: &body, + Results: &res.Body, + OkCodes: []int{200}, + }) + + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/routers/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/routers/requests_test.go new file mode 100644 index 0000000000..c34264daee --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/routers/requests_test.go @@ -0,0 +1,338 @@ +package routers + +import ( + "fmt" + "net/http" + "testing" + + fake "github.com/rackspace/gophercloud/openstack/networking/v2/common" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestURLs(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.AssertEquals(t, th.Endpoint()+"v2.0/routers", rootURL(fake.ServiceClient())) +} + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/routers", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "routers": [ + { + "status": "ACTIVE", + "external_gateway_info": null, + "name": "second_routers", + "admin_state_up": true, + "tenant_id": "6b96ff0cb17a4b859e1e575d221683d3", + "id": "7177abc4-5ae9-4bb7-b0d4-89e94a4abf3b" + }, + { + "status": "ACTIVE", + "external_gateway_info": { + "network_id": "3c5bcddd-6af9-4e6b-9c3e-c153e521cab8" + }, + "name": "router1", + "admin_state_up": true, + "tenant_id": "33a40233088643acb66ff6eb0ebea679", + "id": "a9254bdb-2613-4a13-ac4c-adc581fba50d" + } + ] +} + `) + }) + + count := 0 + + List(fake.ServiceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractRouters(page) + if err != nil { + t.Errorf("Failed to extract routers: %v", err) + return false, err + } + + expected := []Router{ + Router{ + Status: "ACTIVE", + GatewayInfo: GatewayInfo{NetworkID: ""}, + AdminStateUp: true, + Name: "second_routers", + ID: "7177abc4-5ae9-4bb7-b0d4-89e94a4abf3b", + TenantID: "6b96ff0cb17a4b859e1e575d221683d3", + }, + Router{ + Status: "ACTIVE", + GatewayInfo: GatewayInfo{NetworkID: "3c5bcddd-6af9-4e6b-9c3e-c153e521cab8"}, + AdminStateUp: true, + Name: "router1", + ID: "a9254bdb-2613-4a13-ac4c-adc581fba50d", + TenantID: "33a40233088643acb66ff6eb0ebea679", + }, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/routers", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "router":{ + "name": "foo_router", + "admin_state_up": false, + "external_gateway_info":{ + "network_id":"8ca37218-28ff-41cb-9b10-039601ea7e6b" + } + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, ` +{ + "router": { + "status": "ACTIVE", + "external_gateway_info": { + "network_id": "8ca37218-28ff-41cb-9b10-039601ea7e6b" + }, + "name": "foo_router", + "admin_state_up": false, + "tenant_id": "6b96ff0cb17a4b859e1e575d221683d3", + "id": "8604a0de-7f6b-409a-a47c-a1cc7bc77b2e" + } +} + `) + }) + + asu := false + gwi := GatewayInfo{NetworkID: "8ca37218-28ff-41cb-9b10-039601ea7e6b"} + + options := CreateOpts{ + Name: "foo_router", + AdminStateUp: &asu, + GatewayInfo: &gwi, + } + r, err := Create(fake.ServiceClient(), options).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, "foo_router", r.Name) + th.AssertEquals(t, false, r.AdminStateUp) + th.AssertDeepEquals(t, GatewayInfo{NetworkID: "8ca37218-28ff-41cb-9b10-039601ea7e6b"}, r.GatewayInfo) +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/routers/a07eea83-7710-4860-931b-5fe220fae533", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "router": { + "status": "ACTIVE", + "external_gateway_info": { + "network_id": "85d76829-6415-48ff-9c63-5c5ca8c61ac6" + }, + "name": "router1", + "admin_state_up": true, + "tenant_id": "d6554fe62e2f41efbb6e026fad5c1542", + "id": "a07eea83-7710-4860-931b-5fe220fae533" + } +} + `) + }) + + n, err := Get(fake.ServiceClient(), "a07eea83-7710-4860-931b-5fe220fae533").Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, n.Status, "ACTIVE") + th.AssertDeepEquals(t, n.GatewayInfo, GatewayInfo{NetworkID: "85d76829-6415-48ff-9c63-5c5ca8c61ac6"}) + th.AssertEquals(t, n.Name, "router1") + th.AssertEquals(t, n.AdminStateUp, true) + th.AssertEquals(t, n.TenantID, "d6554fe62e2f41efbb6e026fad5c1542") + th.AssertEquals(t, n.ID, "a07eea83-7710-4860-931b-5fe220fae533") +} + +func TestUpdate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/routers/4e8e5957-649f-477b-9e5b-f1f75b21c03c", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "router": { + "name": "new_name", + "external_gateway_info": { + "network_id": "8ca37218-28ff-41cb-9b10-039601ea7e6b" + } + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "router": { + "status": "ACTIVE", + "external_gateway_info": { + "network_id": "8ca37218-28ff-41cb-9b10-039601ea7e6b" + }, + "name": "new_name", + "admin_state_up": true, + "tenant_id": "6b96ff0cb17a4b859e1e575d221683d3", + "id": "8604a0de-7f6b-409a-a47c-a1cc7bc77b2e" + } +} + `) + }) + + gwi := GatewayInfo{NetworkID: "8ca37218-28ff-41cb-9b10-039601ea7e6b"} + options := UpdateOpts{Name: "new_name", GatewayInfo: &gwi} + + n, err := Update(fake.ServiceClient(), "4e8e5957-649f-477b-9e5b-f1f75b21c03c", options).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, n.Name, "new_name") + th.AssertDeepEquals(t, n.GatewayInfo, GatewayInfo{NetworkID: "8ca37218-28ff-41cb-9b10-039601ea7e6b"}) +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/routers/4e8e5957-649f-477b-9e5b-f1f75b21c03c", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusNoContent) + }) + + res := Delete(fake.ServiceClient(), "4e8e5957-649f-477b-9e5b-f1f75b21c03c") + th.AssertNoErr(t, res.Err) +} + +func TestAddInterface(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/routers/4e8e5957-649f-477b-9e5b-f1f75b21c03c/add_router_interface", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "subnet_id": "a2f1f29d-571b-4533-907f-5803ab96ead1" +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "subnet_id": "0d32a837-8069-4ec3-84c4-3eef3e10b188", + "tenant_id": "017d8de156df4177889f31a9bd6edc00", + "port_id": "3f990102-4485-4df1-97a0-2c35bdb85b31", + "id": "9a83fa11-8da5-436e-9afe-3d3ac5ce7770" +} +`) + }) + + opts := InterfaceOpts{SubnetID: "a2f1f29d-571b-4533-907f-5803ab96ead1"} + res, err := AddInterface(fake.ServiceClient(), "4e8e5957-649f-477b-9e5b-f1f75b21c03c", opts).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, "0d32a837-8069-4ec3-84c4-3eef3e10b188", res.SubnetID) + th.AssertEquals(t, "017d8de156df4177889f31a9bd6edc00", res.TenantID) + th.AssertEquals(t, "3f990102-4485-4df1-97a0-2c35bdb85b31", res.PortID) + th.AssertEquals(t, "9a83fa11-8da5-436e-9afe-3d3ac5ce7770", res.ID) +} + +func TestAddInterfaceRequiredOpts(t *testing.T) { + _, err := AddInterface(fake.ServiceClient(), "foo", InterfaceOpts{}).Extract() + if err == nil { + t.Fatalf("Expected error, got none") + } + _, err = AddInterface(fake.ServiceClient(), "foo", InterfaceOpts{SubnetID: "bar", PortID: "baz"}).Extract() + if err == nil { + t.Fatalf("Expected error, got none") + } +} + +func TestRemoveInterface(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/routers/4e8e5957-649f-477b-9e5b-f1f75b21c03c/remove_router_interface", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "subnet_id": "a2f1f29d-571b-4533-907f-5803ab96ead1" +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "subnet_id": "0d32a837-8069-4ec3-84c4-3eef3e10b188", + "tenant_id": "017d8de156df4177889f31a9bd6edc00", + "port_id": "3f990102-4485-4df1-97a0-2c35bdb85b31", + "id": "9a83fa11-8da5-436e-9afe-3d3ac5ce7770" +} +`) + }) + + opts := InterfaceOpts{SubnetID: "a2f1f29d-571b-4533-907f-5803ab96ead1"} + res, err := RemoveInterface(fake.ServiceClient(), "4e8e5957-649f-477b-9e5b-f1f75b21c03c", opts).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, "0d32a837-8069-4ec3-84c4-3eef3e10b188", res.SubnetID) + th.AssertEquals(t, "017d8de156df4177889f31a9bd6edc00", res.TenantID) + th.AssertEquals(t, "3f990102-4485-4df1-97a0-2c35bdb85b31", res.PortID) + th.AssertEquals(t, "9a83fa11-8da5-436e-9afe-3d3ac5ce7770", res.ID) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/routers/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/routers/results.go new file mode 100644 index 0000000000..bdad4cb2fd --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/routers/results.go @@ -0,0 +1,161 @@ +package routers + +import ( + "github.com/mitchellh/mapstructure" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// GatewayInfo represents the information of an external gateway for any +// particular network router. +type GatewayInfo struct { + NetworkID string `json:"network_id" mapstructure:"network_id"` +} + +// Router represents a Neutron router. A router is a logical entity that +// forwards packets across internal subnets and NATs (network address +// translation) them on external networks through an appropriate gateway. +// +// A router has an interface for each subnet with which it is associated. By +// default, the IP address of such interface is the subnet's gateway IP. Also, +// whenever a router is associated with a subnet, a port for that router +// interface is added to the subnet's network. +type Router struct { + // Indicates whether or not a router is currently operational. + Status string `json:"status" mapstructure:"status"` + + // Information on external gateway for the router. + GatewayInfo GatewayInfo `json:"external_gateway_info" mapstructure:"external_gateway_info"` + + // Administrative state of the router. + AdminStateUp bool `json:"admin_state_up" mapstructure:"admin_state_up"` + + // Human readable name for the router. Does not have to be unique. + Name string `json:"name" mapstructure:"name"` + + // Unique identifier for the router. + ID string `json:"id" mapstructure:"id"` + + // Owner of the router. Only admin users can specify a tenant identifier + // other than its own. + TenantID string `json:"tenant_id" mapstructure:"tenant_id"` +} + +// RouterPage is the page returned by a pager when traversing over a +// collection of routers. +type RouterPage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of routers has reached +// the end of a page and the pager seeks to traverse over a new one. In order +// to do this, it needs to construct the next page's URL. +func (p RouterPage) NextPageURL() (string, error) { + type resp struct { + Links []gophercloud.Link `mapstructure:"routers_links"` + } + + var r resp + err := mapstructure.Decode(p.Body, &r) + if err != nil { + return "", err + } + + return gophercloud.ExtractNextURL(r.Links) +} + +// IsEmpty checks whether a RouterPage struct is empty. +func (p RouterPage) IsEmpty() (bool, error) { + is, err := ExtractRouters(p) + if err != nil { + return true, nil + } + return len(is) == 0, nil +} + +// ExtractRouters accepts a Page struct, specifically a RouterPage struct, +// and extracts the elements into a slice of Router structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractRouters(page pagination.Page) ([]Router, error) { + var resp struct { + Routers []Router `mapstructure:"routers" json:"routers"` + } + + err := mapstructure.Decode(page.(RouterPage).Body, &resp) + + return resp.Routers, err +} + +type commonResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts a router. +func (r commonResult) Extract() (*Router, error) { + if r.Err != nil { + return nil, r.Err + } + + var res struct { + Router *Router `json:"router"` + } + + err := mapstructure.Decode(r.Body, &res) + + return res.Router, err +} + +// CreateResult represents the result of a create operation. +type CreateResult struct { + commonResult +} + +// GetResult represents the result of a get operation. +type GetResult struct { + commonResult +} + +// UpdateResult represents the result of an update operation. +type UpdateResult struct { + commonResult +} + +// DeleteResult represents the result of a delete operation. +type DeleteResult struct { + gophercloud.ErrResult +} + +// InterfaceInfo represents information about a particular router interface. As +// mentioned above, in order for a router to forward to a subnet, it needs an +// interface. +type InterfaceInfo struct { + // The ID of the subnet which this interface is associated with. + SubnetID string `json:"subnet_id" mapstructure:"subnet_id"` + + // The ID of the port that is a part of the subnet. + PortID string `json:"port_id" mapstructure:"port_id"` + + // The UUID of the interface. + ID string `json:"id" mapstructure:"id"` + + // Owner of the interface. + TenantID string `json:"tenant_id" mapstructure:"tenant_id"` +} + +// InterfaceResult represents the result of interface operations, such as +// AddInterface() and RemoveInterface(). +type InterfaceResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts an information struct. +func (r InterfaceResult) Extract() (*InterfaceInfo, error) { + if r.Err != nil { + return nil, r.Err + } + + var res *InterfaceInfo + err := mapstructure.Decode(r.Body, &res) + + return res, err +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/routers/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/routers/urls.go new file mode 100644 index 0000000000..bc22c2a8a8 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/routers/urls.go @@ -0,0 +1,21 @@ +package routers + +import "github.com/rackspace/gophercloud" + +const resourcePath = "routers" + +func rootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(resourcePath) +} + +func resourceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(resourcePath, id) +} + +func addInterfaceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(resourcePath, id, "add_router_interface") +} + +func removeInterfaceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(resourcePath, id, "remove_router_interface") +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/doc.go new file mode 100644 index 0000000000..bc1fc282f4 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/doc.go @@ -0,0 +1,3 @@ +// Package lbaas provides information and interaction with the Load Balancer +// as a Service extension for the OpenStack Networking service. +package lbaas diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/members/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/members/requests.go new file mode 100644 index 0000000000..58ec580dbc --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/members/requests.go @@ -0,0 +1,139 @@ +package members + +import ( + "github.com/racker/perigee" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the floating IP attributes you want to see returned. SortKey allows you to +// sort by a particular network attribute. SortDir sets the direction, and is +// either `asc' or `desc'. Marker and Limit are used for pagination. +type ListOpts struct { + Status string `q:"status"` + Weight int `q:"weight"` + AdminStateUp *bool `q:"admin_state_up"` + TenantID string `q:"tenant_id"` + PoolID string `q:"pool_id"` + Address string `q:"address"` + ProtocolPort int `q:"protocol_port"` + ID string `q:"id"` + Limit int `q:"limit"` + Marker string `q:"marker"` + SortKey string `q:"sort_key"` + SortDir string `q:"sort_dir"` +} + +// List returns a Pager which allows you to iterate over a collection of +// pools. It accepts a ListOpts struct, which allows you to filter and sort +// the returned collection for greater efficiency. +// +// Default policy settings return only those pools that are owned by the +// tenant who submits the request, unless an admin user submits the request. +func List(c *gophercloud.ServiceClient, opts ListOpts) pagination.Pager { + q, err := gophercloud.BuildQueryString(&opts) + if err != nil { + return pagination.Pager{Err: err} + } + u := rootURL(c) + q.String() + return pagination.NewPager(c, u, func(r pagination.PageResult) pagination.Page { + return MemberPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// CreateOpts contains all the values needed to create a new pool member. +type CreateOpts struct { + // Only required if the caller has an admin role and wants to create a pool + // for another tenant. + TenantID string + + // Required. The IP address of the member. + Address string + + // Required. The port on which the application is hosted. + ProtocolPort int + + // Required. The pool to which this member will belong. + PoolID string +} + +// Create accepts a CreateOpts struct and uses the values to create a new +// load balancer pool member. +func Create(c *gophercloud.ServiceClient, opts CreateOpts) CreateResult { + type member struct { + TenantID string `json:"tenant_id"` + ProtocolPort int `json:"protocol_port"` + Address string `json:"address"` + PoolID string `json:"pool_id"` + } + type request struct { + Member member `json:"member"` + } + + reqBody := request{Member: member{ + Address: opts.Address, + TenantID: opts.TenantID, + ProtocolPort: opts.ProtocolPort, + PoolID: opts.PoolID, + }} + + var res CreateResult + _, res.Err = perigee.Request("POST", rootURL(c), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + ReqBody: &reqBody, + Results: &res.Body, + OkCodes: []int{201}, + }) + return res +} + +// Get retrieves a particular pool member based on its unique ID. +func Get(c *gophercloud.ServiceClient, id string) GetResult { + var res GetResult + _, res.Err = perigee.Request("GET", resourceURL(c, id), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + Results: &res.Body, + OkCodes: []int{200}, + }) + return res +} + +// UpdateOpts contains the values used when updating a pool member. +type UpdateOpts struct { + // The administrative state of the member, which is up (true) or down (false). + AdminStateUp bool +} + +// Update allows members to be updated. +func Update(c *gophercloud.ServiceClient, id string, opts UpdateOpts) UpdateResult { + type member struct { + AdminStateUp bool `json:"admin_state_up"` + } + type request struct { + Member member `json:"member"` + } + + reqBody := request{Member: member{AdminStateUp: opts.AdminStateUp}} + + // Send request to API + var res UpdateResult + _, res.Err = perigee.Request("PUT", resourceURL(c, id), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + ReqBody: &reqBody, + Results: &res.Body, + OkCodes: []int{200}, + }) + return res +} + +// Delete will permanently delete a particular member based on its unique ID. +func Delete(c *gophercloud.ServiceClient, id string) DeleteResult { + var res DeleteResult + _, res.Err = perigee.Request("DELETE", resourceURL(c, id), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + OkCodes: []int{204}, + }) + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/members/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/members/requests_test.go new file mode 100644 index 0000000000..dc1ece321f --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/members/requests_test.go @@ -0,0 +1,243 @@ +package members + +import ( + "fmt" + "net/http" + "testing" + + fake "github.com/rackspace/gophercloud/openstack/networking/v2/common" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestURLs(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.AssertEquals(t, th.Endpoint()+"v2.0/lb/members", rootURL(fake.ServiceClient())) +} + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/lb/members", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "members":[ + { + "status":"ACTIVE", + "weight":1, + "admin_state_up":true, + "tenant_id":"83657cfcdfe44cd5920adaf26c48ceea", + "pool_id":"72741b06-df4d-4715-b142-276b6bce75ab", + "address":"10.0.0.4", + "protocol_port":80, + "id":"701b531b-111a-4f21-ad85-4795b7b12af6" + }, + { + "status":"ACTIVE", + "weight":1, + "admin_state_up":true, + "tenant_id":"83657cfcdfe44cd5920adaf26c48ceea", + "pool_id":"72741b06-df4d-4715-b142-276b6bce75ab", + "address":"10.0.0.3", + "protocol_port":80, + "id":"beb53b4d-230b-4abd-8118-575b8fa006ef" + } + ] +} + `) + }) + + count := 0 + + List(fake.ServiceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractMembers(page) + if err != nil { + t.Errorf("Failed to extract members: %v", err) + return false, err + } + + expected := []Member{ + Member{ + Status: "ACTIVE", + Weight: 1, + AdminStateUp: true, + TenantID: "83657cfcdfe44cd5920adaf26c48ceea", + PoolID: "72741b06-df4d-4715-b142-276b6bce75ab", + Address: "10.0.0.4", + ProtocolPort: 80, + ID: "701b531b-111a-4f21-ad85-4795b7b12af6", + }, + Member{ + Status: "ACTIVE", + Weight: 1, + AdminStateUp: true, + TenantID: "83657cfcdfe44cd5920adaf26c48ceea", + PoolID: "72741b06-df4d-4715-b142-276b6bce75ab", + Address: "10.0.0.3", + ProtocolPort: 80, + ID: "beb53b4d-230b-4abd-8118-575b8fa006ef", + }, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/lb/members", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "member": { + "tenant_id": "453105b9-1754-413f-aab1-55f1af620750", + "pool_id": "foo", + "address": "192.0.2.14", + "protocol_port":8080 + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, ` +{ + "member": { + "id": "975592ca-e308-48ad-8298-731935ee9f45", + "address": "192.0.2.14", + "protocol_port": 8080, + "tenant_id": "453105b9-1754-413f-aab1-55f1af620750", + "admin_state_up":true, + "weight": 1, + "status": "DOWN" + } +} + `) + }) + + options := CreateOpts{ + TenantID: "453105b9-1754-413f-aab1-55f1af620750", + Address: "192.0.2.14", + ProtocolPort: 8080, + PoolID: "foo", + } + _, err := Create(fake.ServiceClient(), options).Extract() + th.AssertNoErr(t, err) +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/lb/members/975592ca-e308-48ad-8298-731935ee9f45", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "member":{ + "id":"975592ca-e308-48ad-8298-731935ee9f45", + "address":"192.0.2.14", + "protocol_port":8080, + "tenant_id":"453105b9-1754-413f-aab1-55f1af620750", + "admin_state_up":true, + "weight":1, + "status":"DOWN" + } +} + `) + }) + + m, err := Get(fake.ServiceClient(), "975592ca-e308-48ad-8298-731935ee9f45").Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, "975592ca-e308-48ad-8298-731935ee9f45", m.ID) + th.AssertEquals(t, "192.0.2.14", m.Address) + th.AssertEquals(t, 8080, m.ProtocolPort) + th.AssertEquals(t, "453105b9-1754-413f-aab1-55f1af620750", m.TenantID) + th.AssertEquals(t, true, m.AdminStateUp) + th.AssertEquals(t, 1, m.Weight) + th.AssertEquals(t, "DOWN", m.Status) +} + +func TestUpdate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/lb/members/332abe93-f488-41ba-870b-2ac66be7f853", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "member":{ + "admin_state_up":false + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "member":{ + "status":"PENDING_UPDATE", + "protocol_port":8080, + "weight":1, + "admin_state_up":false, + "tenant_id":"4fd44f30292945e481c7b8a0c8908869", + "pool_id":"7803631d-f181-4500-b3a2-1b68ba2a75fd", + "address":"10.0.0.5", + "status_description":null, + "id":"48a471ea-64f1-4eb6-9be7-dae6bbe40a0f" + } +} + `) + }) + + options := UpdateOpts{AdminStateUp: false} + + _, err := Update(fake.ServiceClient(), "332abe93-f488-41ba-870b-2ac66be7f853", options).Extract() + th.AssertNoErr(t, err) +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/lb/members/332abe93-f488-41ba-870b-2ac66be7f853", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusNoContent) + }) + + res := Delete(fake.ServiceClient(), "332abe93-f488-41ba-870b-2ac66be7f853") + th.AssertNoErr(t, res.Err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/members/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/members/results.go new file mode 100644 index 0000000000..3cad339b77 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/members/results.go @@ -0,0 +1,122 @@ +package members + +import ( + "github.com/mitchellh/mapstructure" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// Member represents the application running on a backend server. +type Member struct { + // The status of the member. Indicates whether the member is operational. + Status string + + // Weight of member. + Weight int + + // The administrative state of the member, which is up (true) or down (false). + AdminStateUp bool `json:"admin_state_up" mapstructure:"admin_state_up"` + + // Owner of the member. Only an administrative user can specify a tenant ID + // other than its own. + TenantID string `json:"tenant_id" mapstructure:"tenant_id"` + + // The pool to which the member belongs. + PoolID string `json:"pool_id" mapstructure:"pool_id"` + + // The IP address of the member. + Address string + + // The port on which the application is hosted. + ProtocolPort int `json:"protocol_port" mapstructure:"protocol_port"` + + // The unique ID for the member. + ID string +} + +// MemberPage is the page returned by a pager when traversing over a +// collection of pool members. +type MemberPage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of members has reached +// the end of a page and the pager seeks to traverse over a new one. In order +// to do this, it needs to construct the next page's URL. +func (p MemberPage) NextPageURL() (string, error) { + type resp struct { + Links []gophercloud.Link `mapstructure:"members_links"` + } + + var r resp + err := mapstructure.Decode(p.Body, &r) + if err != nil { + return "", err + } + + return gophercloud.ExtractNextURL(r.Links) +} + +// IsEmpty checks whether a MemberPage struct is empty. +func (p MemberPage) IsEmpty() (bool, error) { + is, err := ExtractMembers(p) + if err != nil { + return true, nil + } + return len(is) == 0, nil +} + +// ExtractMembers accepts a Page struct, specifically a MemberPage struct, +// and extracts the elements into a slice of Member structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractMembers(page pagination.Page) ([]Member, error) { + var resp struct { + Members []Member `mapstructure:"members" json:"members"` + } + + err := mapstructure.Decode(page.(MemberPage).Body, &resp) + if err != nil { + return nil, err + } + + return resp.Members, nil +} + +type commonResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts a router. +func (r commonResult) Extract() (*Member, error) { + if r.Err != nil { + return nil, r.Err + } + + var res struct { + Member *Member `json:"member"` + } + + err := mapstructure.Decode(r.Body, &res) + + return res.Member, err +} + +// CreateResult represents the result of a create operation. +type CreateResult struct { + commonResult +} + +// GetResult represents the result of a get operation. +type GetResult struct { + commonResult +} + +// UpdateResult represents the result of an update operation. +type UpdateResult struct { + commonResult +} + +// DeleteResult represents the result of a delete operation. +type DeleteResult struct { + gophercloud.ErrResult +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/members/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/members/urls.go new file mode 100644 index 0000000000..94b57e4c58 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/members/urls.go @@ -0,0 +1,16 @@ +package members + +import "github.com/rackspace/gophercloud" + +const ( + rootPath = "lb" + resourcePath = "members" +) + +func rootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(rootPath, resourcePath) +} + +func resourceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(rootPath, resourcePath, id) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/monitors/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/monitors/requests.go new file mode 100644 index 0000000000..e2b590ecb5 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/monitors/requests.go @@ -0,0 +1,282 @@ +package monitors + +import ( + "fmt" + + "github.com/racker/perigee" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the floating IP attributes you want to see returned. SortKey allows you to +// sort by a particular network attribute. SortDir sets the direction, and is +// either `asc' or `desc'. Marker and Limit are used for pagination. +type ListOpts struct { + ID string `q:"id"` + TenantID string `q:"tenant_id"` + Type string `q:"type"` + Delay int `q:"delay"` + Timeout int `q:"timeout"` + MaxRetries int `q:"max_retries"` + HTTPMethod string `q:"http_method"` + URLPath string `q:"url_path"` + ExpectedCodes string `q:"expected_codes"` + AdminStateUp *bool `q:"admin_state_up"` + Status string `q:"status"` + Limit int `q:"limit"` + Marker string `q:"marker"` + SortKey string `q:"sort_key"` + SortDir string `q:"sort_dir"` +} + +// List returns a Pager which allows you to iterate over a collection of +// routers. It accepts a ListOpts struct, which allows you to filter and sort +// the returned collection for greater efficiency. +// +// Default policy settings return only those routers that are owned by the +// tenant who submits the request, unless an admin user submits the request. +func List(c *gophercloud.ServiceClient, opts ListOpts) pagination.Pager { + q, err := gophercloud.BuildQueryString(&opts) + if err != nil { + return pagination.Pager{Err: err} + } + u := rootURL(c) + q.String() + + return pagination.NewPager(c, u, func(r pagination.PageResult) pagination.Page { + return MonitorPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Constants that represent approved monitoring types. +const ( + TypePING = "PING" + TypeTCP = "TCP" + TypeHTTP = "HTTP" + TypeHTTPS = "HTTPS" +) + +var ( + errValidTypeRequired = fmt.Errorf("A valid Type is required. Supported values are PING, TCP, HTTP and HTTPS") + errDelayRequired = fmt.Errorf("Delay is required") + errTimeoutRequired = fmt.Errorf("Timeout is required") + errMaxRetriesRequired = fmt.Errorf("MaxRetries is required") + errURLPathRequired = fmt.Errorf("URL path is required") + errExpectedCodesRequired = fmt.Errorf("ExpectedCodes is required") + errDelayMustGETimeout = fmt.Errorf("Delay must be greater than or equal to timeout") +) + +// CreateOpts contains all the values needed to create a new health monitor. +type CreateOpts struct { + // Required for admins. Indicates the owner of the VIP. + TenantID string + + // Required. The type of probe, which is PING, TCP, HTTP, or HTTPS, that is + // sent by the load balancer to verify the member state. + Type string + + // Required. The time, in seconds, between sending probes to members. + Delay int + + // Required. Maximum number of seconds for a monitor to wait for a ping reply + // before it times out. The value must be less than the delay value. + Timeout int + + // Required. Number of permissible ping failures before changing the member's + // status to INACTIVE. Must be a number between 1 and 10. + MaxRetries int + + // Required for HTTP(S) types. URI path that will be accessed if monitor type + // is HTTP or HTTPS. + URLPath string + + // Required for HTTP(S) types. The HTTP method used for requests by the + // monitor. If this attribute is not specified, it defaults to "GET". + HTTPMethod string + + // Required for HTTP(S) types. Expected HTTP codes for a passing HTTP(S) + // monitor. You can either specify a single status like "200", or a range + // like "200-202". + ExpectedCodes string + + AdminStateUp *bool +} + +// Create is an operation which provisions a new health monitor. There are +// different types of monitor you can provision: PING, TCP or HTTP(S). Below +// are examples of how to create each one. +// +// Here is an example config struct to use when creating a PING or TCP monitor: +// +// CreateOpts{Type: TypePING, Delay: 20, Timeout: 10, MaxRetries: 3} +// CreateOpts{Type: TypeTCP, Delay: 20, Timeout: 10, MaxRetries: 3} +// +// Here is an example config struct to use when creating a HTTP(S) monitor: +// +// CreateOpts{Type: TypeHTTP, Delay: 20, Timeout: 10, MaxRetries: 3, +// HttpMethod: "HEAD", ExpectedCodes: "200"} +// +func Create(c *gophercloud.ServiceClient, opts CreateOpts) CreateResult { + var res CreateResult + + // Validate inputs + allowed := map[string]bool{TypeHTTP: true, TypeHTTPS: true, TypeTCP: true, TypePING: true} + if opts.Type == "" || allowed[opts.Type] == false { + res.Err = errValidTypeRequired + } + if opts.Delay == 0 { + res.Err = errDelayRequired + } + if opts.Timeout == 0 { + res.Err = errTimeoutRequired + } + if opts.MaxRetries == 0 { + res.Err = errMaxRetriesRequired + } + if opts.Type == TypeHTTP || opts.Type == TypeHTTPS { + if opts.URLPath == "" { + res.Err = errURLPathRequired + } + if opts.ExpectedCodes == "" { + res.Err = errExpectedCodesRequired + } + } + if opts.Delay < opts.Timeout { + res.Err = errDelayMustGETimeout + } + if res.Err != nil { + return res + } + + type monitor struct { + Type string `json:"type"` + Delay int `json:"delay"` + Timeout int `json:"timeout"` + MaxRetries int `json:"max_retries"` + TenantID *string `json:"tenant_id,omitempty"` + URLPath *string `json:"url_path,omitempty"` + ExpectedCodes *string `json:"expected_codes,omitempty"` + HTTPMethod *string `json:"http_method,omitempty"` + AdminStateUp *bool `json:"admin_state_up,omitempty"` + } + + type request struct { + Monitor monitor `json:"health_monitor"` + } + + reqBody := request{Monitor: monitor{ + Type: opts.Type, + Delay: opts.Delay, + Timeout: opts.Timeout, + MaxRetries: opts.MaxRetries, + TenantID: gophercloud.MaybeString(opts.TenantID), + URLPath: gophercloud.MaybeString(opts.URLPath), + ExpectedCodes: gophercloud.MaybeString(opts.ExpectedCodes), + HTTPMethod: gophercloud.MaybeString(opts.HTTPMethod), + AdminStateUp: opts.AdminStateUp, + }} + + _, res.Err = perigee.Request("POST", rootURL(c), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + ReqBody: &reqBody, + Results: &res.Body, + OkCodes: []int{201}, + }) + + return res +} + +// Get retrieves a particular health monitor based on its unique ID. +func Get(c *gophercloud.ServiceClient, id string) GetResult { + var res GetResult + _, res.Err = perigee.Request("GET", resourceURL(c, id), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + Results: &res.Body, + OkCodes: []int{200}, + }) + return res +} + +// UpdateOpts contains all the values needed to update an existing virtual IP. +// Attributes not listed here but appear in CreateOpts are immutable and cannot +// be updated. +type UpdateOpts struct { + // Required. The time, in seconds, between sending probes to members. + Delay int + + // Required. Maximum number of seconds for a monitor to wait for a ping reply + // before it times out. The value must be less than the delay value. + Timeout int + + // Required. Number of permissible ping failures before changing the member's + // status to INACTIVE. Must be a number between 1 and 10. + MaxRetries int + + // Required for HTTP(S) types. URI path that will be accessed if monitor type + // is HTTP or HTTPS. + URLPath string + + // Required for HTTP(S) types. The HTTP method used for requests by the + // monitor. If this attribute is not specified, it defaults to "GET". + HTTPMethod string + + // Required for HTTP(S) types. Expected HTTP codes for a passing HTTP(S) + // monitor. You can either specify a single status like "200", or a range + // like "200-202". + ExpectedCodes string + + AdminStateUp *bool +} + +// Update is an operation which modifies the attributes of the specified monitor. +func Update(c *gophercloud.ServiceClient, id string, opts UpdateOpts) UpdateResult { + var res UpdateResult + + if opts.Delay > 0 && opts.Timeout > 0 && opts.Delay < opts.Timeout { + res.Err = errDelayMustGETimeout + } + + type monitor struct { + Delay int `json:"delay"` + Timeout int `json:"timeout"` + MaxRetries int `json:"max_retries"` + URLPath *string `json:"url_path,omitempty"` + ExpectedCodes *string `json:"expected_codes,omitempty"` + HTTPMethod *string `json:"http_method,omitempty"` + AdminStateUp *bool `json:"admin_state_up,omitempty"` + } + + type request struct { + Monitor monitor `json:"health_monitor"` + } + + reqBody := request{Monitor: monitor{ + Delay: opts.Delay, + Timeout: opts.Timeout, + MaxRetries: opts.MaxRetries, + URLPath: gophercloud.MaybeString(opts.URLPath), + ExpectedCodes: gophercloud.MaybeString(opts.ExpectedCodes), + HTTPMethod: gophercloud.MaybeString(opts.HTTPMethod), + AdminStateUp: opts.AdminStateUp, + }} + + _, res.Err = perigee.Request("PUT", resourceURL(c, id), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + ReqBody: &reqBody, + Results: &res.Body, + OkCodes: []int{200, 202}, + }) + + return res +} + +// Delete will permanently delete a particular monitor based on its unique ID. +func Delete(c *gophercloud.ServiceClient, id string) DeleteResult { + var res DeleteResult + _, res.Err = perigee.Request("DELETE", resourceURL(c, id), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + OkCodes: []int{204}, + }) + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/monitors/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/monitors/requests_test.go new file mode 100644 index 0000000000..79a99bf8a2 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/monitors/requests_test.go @@ -0,0 +1,312 @@ +package monitors + +import ( + "fmt" + "net/http" + "testing" + + fake "github.com/rackspace/gophercloud/openstack/networking/v2/common" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestURLs(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + th.AssertEquals(t, th.Endpoint()+"v2.0/lb/health_monitors", rootURL(fake.ServiceClient())) +} + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/lb/health_monitors", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "health_monitors":[ + { + "admin_state_up":true, + "tenant_id":"83657cfcdfe44cd5920adaf26c48ceea", + "delay":10, + "max_retries":1, + "timeout":1, + "type":"PING", + "id":"466c8345-28d8-4f84-a246-e04380b0461d" + }, + { + "admin_state_up":true, + "tenant_id":"83657cfcdfe44cd5920adaf26c48ceea", + "delay":5, + "expected_codes":"200", + "max_retries":2, + "http_method":"GET", + "timeout":2, + "url_path":"/", + "type":"HTTP", + "id":"5d4b5228-33b0-4e60-b225-9b727c1a20e7" + } + ] +} + `) + }) + + count := 0 + + List(fake.ServiceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractMonitors(page) + if err != nil { + t.Errorf("Failed to extract monitors: %v", err) + return false, err + } + + expected := []Monitor{ + Monitor{ + AdminStateUp: true, + TenantID: "83657cfcdfe44cd5920adaf26c48ceea", + Delay: 10, + MaxRetries: 1, + Timeout: 1, + Type: "PING", + ID: "466c8345-28d8-4f84-a246-e04380b0461d", + }, + Monitor{ + AdminStateUp: true, + TenantID: "83657cfcdfe44cd5920adaf26c48ceea", + Delay: 5, + ExpectedCodes: "200", + MaxRetries: 2, + Timeout: 2, + URLPath: "/", + Type: "HTTP", + HTTPMethod: "GET", + ID: "5d4b5228-33b0-4e60-b225-9b727c1a20e7", + }, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestDelayMustBeGreaterOrEqualThanTimeout(t *testing.T) { + _, err := Create(fake.ServiceClient(), CreateOpts{ + Type: "HTTP", + Delay: 1, + Timeout: 10, + MaxRetries: 5, + URLPath: "/check", + ExpectedCodes: "200-299", + }).Extract() + + if err == nil { + t.Fatalf("Expected error, got none") + } + + _, err = Update(fake.ServiceClient(), "453105b9-1754-413f-aab1-55f1af620750", UpdateOpts{ + Delay: 1, + Timeout: 10, + }).Extract() + + if err == nil { + t.Fatalf("Expected error, got none") + } +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/lb/health_monitors", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "health_monitor":{ + "type":"HTTP", + "tenant_id":"453105b9-1754-413f-aab1-55f1af620750", + "delay":20, + "timeout":10, + "max_retries":5, + "url_path":"/check", + "expected_codes":"200-299" + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, ` +{ + "health_monitor":{ + "id":"f3eeab00-8367-4524-b662-55e64d4cacb5", + "tenant_id":"453105b9-1754-413f-aab1-55f1af620750", + "type":"HTTP", + "delay":20, + "timeout":10, + "max_retries":5, + "http_method":"GET", + "url_path":"/check", + "expected_codes":"200-299", + "admin_state_up":true, + "status":"ACTIVE" + } +} + `) + }) + + _, err := Create(fake.ServiceClient(), CreateOpts{ + Type: "HTTP", + TenantID: "453105b9-1754-413f-aab1-55f1af620750", + Delay: 20, + Timeout: 10, + MaxRetries: 5, + URLPath: "/check", + ExpectedCodes: "200-299", + }).Extract() + + th.AssertNoErr(t, err) +} + +func TestRequiredCreateOpts(t *testing.T) { + res := Create(fake.ServiceClient(), CreateOpts{}) + if res.Err == nil { + t.Fatalf("Expected error, got none") + } + res = Create(fake.ServiceClient(), CreateOpts{Type: TypeHTTP}) + if res.Err == nil { + t.Fatalf("Expected error, got none") + } +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/lb/health_monitors/f3eeab00-8367-4524-b662-55e64d4cacb5", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "health_monitor":{ + "id":"f3eeab00-8367-4524-b662-55e64d4cacb5", + "tenant_id":"453105b9-1754-413f-aab1-55f1af620750", + "type":"HTTP", + "delay":20, + "timeout":10, + "max_retries":5, + "http_method":"GET", + "url_path":"/check", + "expected_codes":"200-299", + "admin_state_up":true, + "status":"ACTIVE" + } +} + `) + }) + + hm, err := Get(fake.ServiceClient(), "f3eeab00-8367-4524-b662-55e64d4cacb5").Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, "f3eeab00-8367-4524-b662-55e64d4cacb5", hm.ID) + th.AssertEquals(t, "453105b9-1754-413f-aab1-55f1af620750", hm.TenantID) + th.AssertEquals(t, "HTTP", hm.Type) + th.AssertEquals(t, 20, hm.Delay) + th.AssertEquals(t, 10, hm.Timeout) + th.AssertEquals(t, 5, hm.MaxRetries) + th.AssertEquals(t, "GET", hm.HTTPMethod) + th.AssertEquals(t, "/check", hm.URLPath) + th.AssertEquals(t, "200-299", hm.ExpectedCodes) + th.AssertEquals(t, true, hm.AdminStateUp) + th.AssertEquals(t, "ACTIVE", hm.Status) +} + +func TestUpdate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/lb/health_monitors/b05e44b5-81f9-4551-b474-711a722698f7", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "health_monitor":{ + "delay": 3, + "timeout": 20, + "max_retries": 10, + "url_path": "/another_check", + "expected_codes": "301" + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusAccepted) + + fmt.Fprintf(w, ` +{ + "health_monitor": { + "admin_state_up": true, + "tenant_id": "4fd44f30292945e481c7b8a0c8908869", + "delay": 3, + "max_retries": 10, + "http_method": "GET", + "timeout": 20, + "pools": [ + { + "status": "PENDING_CREATE", + "status_description": null, + "pool_id": "6e55751f-6ad4-4e53-b8d4-02e442cd21df" + } + ], + "type": "PING", + "id": "b05e44b5-81f9-4551-b474-711a722698f7" + } +} + `) + }) + + _, err := Update(fake.ServiceClient(), "b05e44b5-81f9-4551-b474-711a722698f7", UpdateOpts{ + Delay: 3, + Timeout: 20, + MaxRetries: 10, + URLPath: "/another_check", + ExpectedCodes: "301", + }).Extract() + + th.AssertNoErr(t, err) +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/lb/health_monitors/b05e44b5-81f9-4551-b474-711a722698f7", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusNoContent) + }) + + res := Delete(fake.ServiceClient(), "b05e44b5-81f9-4551-b474-711a722698f7") + th.AssertNoErr(t, res.Err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/monitors/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/monitors/results.go new file mode 100644 index 0000000000..d595abd540 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/monitors/results.go @@ -0,0 +1,147 @@ +package monitors + +import ( + "github.com/mitchellh/mapstructure" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// Monitor represents a load balancer health monitor. A health monitor is used +// to determine whether or not back-end members of the VIP's pool are usable +// for processing a request. A pool can have several health monitors associated +// with it. There are different types of health monitors supported: +// +// PING: used to ping the members using ICMP. +// TCP: used to connect to the members using TCP. +// HTTP: used to send an HTTP request to the member. +// HTTPS: used to send a secure HTTP request to the member. +// +// When a pool has several monitors associated with it, each member of the pool +// is monitored by all these monitors. If any monitor declares the member as +// unhealthy, then the member status is changed to INACTIVE and the member +// won't participate in its pool's load balancing. In other words, ALL monitors +// must declare the member to be healthy for it to stay ACTIVE. +type Monitor struct { + // The unique ID for the VIP. + ID string + + // Owner of the VIP. Only an administrative user can specify a tenant ID + // other than its own. + TenantID string `json:"tenant_id" mapstructure:"tenant_id"` + + // The type of probe sent by the load balancer to verify the member state, + // which is PING, TCP, HTTP, or HTTPS. + Type string + + // The time, in seconds, between sending probes to members. + Delay int + + // The maximum number of seconds for a monitor to wait for a connection to be + // established before it times out. This value must be less than the delay value. + Timeout int + + // Number of allowed connection failures before changing the status of the + // member to INACTIVE. A valid value is from 1 to 10. + MaxRetries int `json:"max_retries" mapstructure:"max_retries"` + + // The HTTP method that the monitor uses for requests. + HTTPMethod string `json:"http_method" mapstructure:"http_method"` + + // The HTTP path of the request sent by the monitor to test the health of a + // member. Must be a string beginning with a forward slash (/). + URLPath string `json:"url_path" mapstructure:"url_path"` + + // Expected HTTP codes for a passing HTTP(S) monitor. + ExpectedCodes string `json:"expected_codes" mapstructure:"expected_codes"` + + // The administrative state of the health monitor, which is up (true) or down (false). + AdminStateUp bool `json:"admin_state_up" mapstructure:"admin_state_up"` + + // The status of the health monitor. Indicates whether the health monitor is + // operational. + Status string +} + +// MonitorPage is the page returned by a pager when traversing over a +// collection of health monitors. +type MonitorPage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of monitors has reached +// the end of a page and the pager seeks to traverse over a new one. In order +// to do this, it needs to construct the next page's URL. +func (p MonitorPage) NextPageURL() (string, error) { + type resp struct { + Links []gophercloud.Link `mapstructure:"health_monitors_links"` + } + + var r resp + err := mapstructure.Decode(p.Body, &r) + if err != nil { + return "", err + } + + return gophercloud.ExtractNextURL(r.Links) +} + +// IsEmpty checks whether a PoolPage struct is empty. +func (p MonitorPage) IsEmpty() (bool, error) { + is, err := ExtractMonitors(p) + if err != nil { + return true, nil + } + return len(is) == 0, nil +} + +// ExtractMonitors accepts a Page struct, specifically a MonitorPage struct, +// and extracts the elements into a slice of Monitor structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractMonitors(page pagination.Page) ([]Monitor, error) { + var resp struct { + Monitors []Monitor `mapstructure:"health_monitors" json:"health_monitors"` + } + + err := mapstructure.Decode(page.(MonitorPage).Body, &resp) + + return resp.Monitors, err +} + +type commonResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts a monitor. +func (r commonResult) Extract() (*Monitor, error) { + if r.Err != nil { + return nil, r.Err + } + + var res struct { + Monitor *Monitor `json:"health_monitor" mapstructure:"health_monitor"` + } + + err := mapstructure.Decode(r.Body, &res) + + return res.Monitor, err +} + +// CreateResult represents the result of a create operation. +type CreateResult struct { + commonResult +} + +// GetResult represents the result of a get operation. +type GetResult struct { + commonResult +} + +// UpdateResult represents the result of an update operation. +type UpdateResult struct { + commonResult +} + +// DeleteResult represents the result of a delete operation. +type DeleteResult struct { + gophercloud.ErrResult +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/monitors/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/monitors/urls.go new file mode 100644 index 0000000000..46e84bbf52 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/monitors/urls.go @@ -0,0 +1,16 @@ +package monitors + +import "github.com/rackspace/gophercloud" + +const ( + rootPath = "lb" + resourcePath = "health_monitors" +) + +func rootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(rootPath, resourcePath) +} + +func resourceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(rootPath, resourcePath, id) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/pools/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/pools/requests.go new file mode 100644 index 0000000000..ca8d33b8d5 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/pools/requests.go @@ -0,0 +1,205 @@ +package pools + +import ( + "github.com/racker/perigee" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the floating IP attributes you want to see returned. SortKey allows you to +// sort by a particular network attribute. SortDir sets the direction, and is +// either `asc' or `desc'. Marker and Limit are used for pagination. +type ListOpts struct { + Status string `q:"status"` + LBMethod string `q:"lb_method"` + Protocol string `q:"protocol"` + SubnetID string `q:"subnet_id"` + TenantID string `q:"tenant_id"` + AdminStateUp *bool `q:"admin_state_up"` + Name string `q:"name"` + ID string `q:"id"` + VIPID string `q:"vip_id"` + Limit int `q:"limit"` + Marker string `q:"marker"` + SortKey string `q:"sort_key"` + SortDir string `q:"sort_dir"` +} + +// List returns a Pager which allows you to iterate over a collection of +// pools. It accepts a ListOpts struct, which allows you to filter and sort +// the returned collection for greater efficiency. +// +// Default policy settings return only those pools that are owned by the +// tenant who submits the request, unless an admin user submits the request. +func List(c *gophercloud.ServiceClient, opts ListOpts) pagination.Pager { + q, err := gophercloud.BuildQueryString(&opts) + if err != nil { + return pagination.Pager{Err: err} + } + u := rootURL(c) + q.String() + return pagination.NewPager(c, u, func(r pagination.PageResult) pagination.Page { + return PoolPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Supported attributes for create/update operations. +const ( + LBMethodRoundRobin = "ROUND_ROBIN" + LBMethodLeastConnections = "LEAST_CONNECTIONS" + + ProtocolTCP = "TCP" + ProtocolHTTP = "HTTP" + ProtocolHTTPS = "HTTPS" +) + +// CreateOpts contains all the values needed to create a new pool. +type CreateOpts struct { + // Only required if the caller has an admin role and wants to create a pool + // for another tenant. + TenantID string + + // Required. Name of the pool. + Name string + + // Required. The protocol used by the pool members, you can use either + // ProtocolTCP, ProtocolHTTP, or ProtocolHTTPS. + Protocol string + + // The network on which the members of the pool will be located. Only members + // that are on this network can be added to the pool. + SubnetID string + + // The algorithm used to distribute load between the members of the pool. The + // current specification supports LBMethodRoundRobin and + // LBMethodLeastConnections as valid values for this attribute. + LBMethod string +} + +// Create accepts a CreateOpts struct and uses the values to create a new +// load balancer pool. +func Create(c *gophercloud.ServiceClient, opts CreateOpts) CreateResult { + type pool struct { + Name string `json:"name"` + TenantID string `json:"tenant_id,omitempty"` + Protocol string `json:"protocol"` + SubnetID string `json:"subnet_id"` + LBMethod string `json:"lb_method"` + } + type request struct { + Pool pool `json:"pool"` + } + + reqBody := request{Pool: pool{ + Name: opts.Name, + TenantID: opts.TenantID, + Protocol: opts.Protocol, + SubnetID: opts.SubnetID, + LBMethod: opts.LBMethod, + }} + + var res CreateResult + _, res.Err = perigee.Request("POST", rootURL(c), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + ReqBody: &reqBody, + Results: &res.Body, + OkCodes: []int{201}, + }) + return res +} + +// Get retrieves a particular pool based on its unique ID. +func Get(c *gophercloud.ServiceClient, id string) GetResult { + var res GetResult + _, res.Err = perigee.Request("GET", resourceURL(c, id), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + Results: &res.Body, + OkCodes: []int{200}, + }) + return res +} + +// UpdateOpts contains the values used when updating a pool. +type UpdateOpts struct { + // Required. Name of the pool. + Name string + + // The algorithm used to distribute load between the members of the pool. The + // current specification supports LBMethodRoundRobin and + // LBMethodLeastConnections as valid values for this attribute. + LBMethod string +} + +// Update allows pools to be updated. +func Update(c *gophercloud.ServiceClient, id string, opts UpdateOpts) UpdateResult { + type pool struct { + Name string `json:"name,"` + LBMethod string `json:"lb_method"` + } + type request struct { + Pool pool `json:"pool"` + } + + reqBody := request{Pool: pool{ + Name: opts.Name, + LBMethod: opts.LBMethod, + }} + + // Send request to API + var res UpdateResult + _, res.Err = perigee.Request("PUT", resourceURL(c, id), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + ReqBody: &reqBody, + Results: &res.Body, + OkCodes: []int{200}, + }) + return res +} + +// Delete will permanently delete a particular pool based on its unique ID. +func Delete(c *gophercloud.ServiceClient, id string) DeleteResult { + var res DeleteResult + _, res.Err = perigee.Request("DELETE", resourceURL(c, id), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + OkCodes: []int{204}, + }) + return res +} + +// AssociateMonitor will associate a health monitor with a particular pool. +// Once associated, the health monitor will start monitoring the members of the +// pool and will deactivate these members if they are deemed unhealthy. A +// member can be deactivated (status set to INACTIVE) if any of health monitors +// finds it unhealthy. +func AssociateMonitor(c *gophercloud.ServiceClient, poolID, monitorID string) AssociateResult { + type hm struct { + ID string `json:"id"` + } + type request struct { + Monitor hm `json:"health_monitor"` + } + + reqBody := request{hm{ID: monitorID}} + + var res AssociateResult + _, res.Err = perigee.Request("POST", associateURL(c, poolID), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + ReqBody: &reqBody, + Results: &res.Body, + OkCodes: []int{201}, + }) + return res +} + +// DisassociateMonitor will disassociate a health monitor with a particular +// pool. When dissociation is successful, the health monitor will no longer +// check for the health of the members of the pool. +func DisassociateMonitor(c *gophercloud.ServiceClient, poolID, monitorID string) AssociateResult { + var res AssociateResult + _, res.Err = perigee.Request("DELETE", disassociateURL(c, poolID, monitorID), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + OkCodes: []int{204}, + }) + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/pools/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/pools/requests_test.go new file mode 100644 index 0000000000..6da29a6b8e --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/pools/requests_test.go @@ -0,0 +1,317 @@ +package pools + +import ( + "fmt" + "net/http" + "testing" + + fake "github.com/rackspace/gophercloud/openstack/networking/v2/common" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestURLs(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.AssertEquals(t, th.Endpoint()+"v2.0/lb/pools", rootURL(fake.ServiceClient())) +} + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/lb/pools", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "pools":[ + { + "status":"ACTIVE", + "lb_method":"ROUND_ROBIN", + "protocol":"HTTP", + "description":"", + "health_monitors":[ + "466c8345-28d8-4f84-a246-e04380b0461d", + "5d4b5228-33b0-4e60-b225-9b727c1a20e7" + ], + "members":[ + "701b531b-111a-4f21-ad85-4795b7b12af6", + "beb53b4d-230b-4abd-8118-575b8fa006ef" + ], + "status_description": null, + "id":"72741b06-df4d-4715-b142-276b6bce75ab", + "vip_id":"4ec89087-d057-4e2c-911f-60a3b47ee304", + "name":"app_pool", + "admin_state_up":true, + "subnet_id":"8032909d-47a1-4715-90af-5153ffe39861", + "tenant_id":"83657cfcdfe44cd5920adaf26c48ceea", + "health_monitors_status": [], + "provider": "haproxy" + } + ] +} + `) + }) + + count := 0 + + List(fake.ServiceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractPools(page) + if err != nil { + t.Errorf("Failed to extract pools: %v", err) + return false, err + } + + expected := []Pool{ + Pool{ + Status: "ACTIVE", + LBMethod: "ROUND_ROBIN", + Protocol: "HTTP", + Description: "", + MonitorIDs: []string{ + "466c8345-28d8-4f84-a246-e04380b0461d", + "5d4b5228-33b0-4e60-b225-9b727c1a20e7", + }, + SubnetID: "8032909d-47a1-4715-90af-5153ffe39861", + TenantID: "83657cfcdfe44cd5920adaf26c48ceea", + AdminStateUp: true, + Name: "app_pool", + MemberIDs: []string{ + "701b531b-111a-4f21-ad85-4795b7b12af6", + "beb53b4d-230b-4abd-8118-575b8fa006ef", + }, + ID: "72741b06-df4d-4715-b142-276b6bce75ab", + VIPID: "4ec89087-d057-4e2c-911f-60a3b47ee304", + Provider: "haproxy", + }, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/lb/pools", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "pool": { + "lb_method": "ROUND_ROBIN", + "protocol": "HTTP", + "name": "Example pool", + "subnet_id": "1981f108-3c48-48d2-b908-30f7d28532c9", + "tenant_id": "2ffc6e22aae24e4795f87155d24c896f" + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, ` +{ + "pool": { + "status": "PENDING_CREATE", + "lb_method": "ROUND_ROBIN", + "protocol": "HTTP", + "description": "", + "health_monitors": [], + "members": [], + "status_description": null, + "id": "69055154-f603-4a28-8951-7cc2d9e54a9a", + "vip_id": null, + "name": "Example pool", + "admin_state_up": true, + "subnet_id": "1981f108-3c48-48d2-b908-30f7d28532c9", + "tenant_id": "2ffc6e22aae24e4795f87155d24c896f", + "health_monitors_status": [] + } +} + `) + }) + + options := CreateOpts{ + LBMethod: LBMethodRoundRobin, + Protocol: "HTTP", + Name: "Example pool", + SubnetID: "1981f108-3c48-48d2-b908-30f7d28532c9", + TenantID: "2ffc6e22aae24e4795f87155d24c896f", + } + p, err := Create(fake.ServiceClient(), options).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, "PENDING_CREATE", p.Status) + th.AssertEquals(t, "ROUND_ROBIN", p.LBMethod) + th.AssertEquals(t, "HTTP", p.Protocol) + th.AssertEquals(t, "", p.Description) + th.AssertDeepEquals(t, []string{}, p.MonitorIDs) + th.AssertDeepEquals(t, []string{}, p.MemberIDs) + th.AssertEquals(t, "69055154-f603-4a28-8951-7cc2d9e54a9a", p.ID) + th.AssertEquals(t, "Example pool", p.Name) + th.AssertEquals(t, "1981f108-3c48-48d2-b908-30f7d28532c9", p.SubnetID) + th.AssertEquals(t, "2ffc6e22aae24e4795f87155d24c896f", p.TenantID) +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/lb/pools/332abe93-f488-41ba-870b-2ac66be7f853", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "pool":{ + "id":"332abe93-f488-41ba-870b-2ac66be7f853", + "tenant_id":"19eaa775-cf5d-49bc-902e-2f85f668d995", + "name":"Example pool", + "description":"", + "protocol":"tcp", + "lb_algorithm":"ROUND_ROBIN", + "session_persistence":{ + }, + "healthmonitor_id":null, + "members":[ + ], + "admin_state_up":true, + "status":"ACTIVE" + } +} + `) + }) + + n, err := Get(fake.ServiceClient(), "332abe93-f488-41ba-870b-2ac66be7f853").Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, n.ID, "332abe93-f488-41ba-870b-2ac66be7f853") +} + +func TestUpdate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/lb/pools/332abe93-f488-41ba-870b-2ac66be7f853", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "pool":{ + "name":"SuperPool", + "lb_method": "LEAST_CONNECTIONS" + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "pool":{ + "status":"PENDING_UPDATE", + "lb_method":"LEAST_CONNECTIONS", + "protocol":"TCP", + "description":"", + "health_monitors":[ + + ], + "subnet_id":"8032909d-47a1-4715-90af-5153ffe39861", + "tenant_id":"83657cfcdfe44cd5920adaf26c48ceea", + "admin_state_up":true, + "name":"SuperPool", + "members":[ + + ], + "id":"61b1f87a-7a21-4ad3-9dda-7f81d249944f", + "vip_id":null + } +} + `) + }) + + options := UpdateOpts{Name: "SuperPool", LBMethod: LBMethodLeastConnections} + + n, err := Update(fake.ServiceClient(), "332abe93-f488-41ba-870b-2ac66be7f853", options).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, "SuperPool", n.Name) + th.AssertDeepEquals(t, "LEAST_CONNECTIONS", n.LBMethod) +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/lb/pools/332abe93-f488-41ba-870b-2ac66be7f853", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusNoContent) + }) + + res := Delete(fake.ServiceClient(), "332abe93-f488-41ba-870b-2ac66be7f853") + th.AssertNoErr(t, res.Err) +} + +func TestAssociateHealthMonitor(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/lb/pools/332abe93-f488-41ba-870b-2ac66be7f853/health_monitors", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "health_monitor":{ + "id":"b624decf-d5d3-4c66-9a3d-f047e7786181" + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + }) + + _, err := AssociateMonitor(fake.ServiceClient(), "332abe93-f488-41ba-870b-2ac66be7f853", "b624decf-d5d3-4c66-9a3d-f047e7786181").Extract() + th.AssertNoErr(t, err) +} + +func TestDisassociateHealthMonitor(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/lb/pools/332abe93-f488-41ba-870b-2ac66be7f853/health_monitors/b624decf-d5d3-4c66-9a3d-f047e7786181", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusNoContent) + }) + + res := DisassociateMonitor(fake.ServiceClient(), "332abe93-f488-41ba-870b-2ac66be7f853", "b624decf-d5d3-4c66-9a3d-f047e7786181") + th.AssertNoErr(t, res.Err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/pools/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/pools/results.go new file mode 100644 index 0000000000..07ec85eda4 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/pools/results.go @@ -0,0 +1,146 @@ +package pools + +import ( + "github.com/mitchellh/mapstructure" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// Pool represents a logical set of devices, such as web servers, that you +// group together to receive and process traffic. The load balancing function +// chooses a member of the pool according to the configured load balancing +// method to handle the new requests or connections received on the VIP address. +// There is only one pool per virtual IP. +type Pool struct { + // The status of the pool. Indicates whether the pool is operational. + Status string + + // The load-balancer algorithm, which is round-robin, least-connections, and + // so on. This value, which must be supported, is dependent on the provider. + // Round-robin must be supported. + LBMethod string `json:"lb_method" mapstructure:"lb_method"` + + // The protocol of the pool, which is TCP, HTTP, or HTTPS. + Protocol string + + // Description for the pool. + Description string + + // The IDs of associated monitors which check the health of the pool members. + MonitorIDs []string `json:"health_monitors" mapstructure:"health_monitors"` + + // The network on which the members of the pool will be located. Only members + // that are on this network can be added to the pool. + SubnetID string `json:"subnet_id" mapstructure:"subnet_id"` + + // Owner of the pool. Only an administrative user can specify a tenant ID + // other than its own. + TenantID string `json:"tenant_id" mapstructure:"tenant_id"` + + // The administrative state of the pool, which is up (true) or down (false). + AdminStateUp bool `json:"admin_state_up" mapstructure:"admin_state_up"` + + // Pool name. Does not have to be unique. + Name string + + // List of member IDs that belong to the pool. + MemberIDs []string `json:"members" mapstructure:"members"` + + // The unique ID for the pool. + ID string + + // The ID of the virtual IP associated with this pool + VIPID string `json:"vip_id" mapstructure:"vip_id"` + + // The provider + Provider string +} + +// PoolPage is the page returned by a pager when traversing over a +// collection of pools. +type PoolPage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of pools has reached +// the end of a page and the pager seeks to traverse over a new one. In order +// to do this, it needs to construct the next page's URL. +func (p PoolPage) NextPageURL() (string, error) { + type resp struct { + Links []gophercloud.Link `mapstructure:"pools_links"` + } + + var r resp + err := mapstructure.Decode(p.Body, &r) + if err != nil { + return "", err + } + + return gophercloud.ExtractNextURL(r.Links) +} + +// IsEmpty checks whether a PoolPage struct is empty. +func (p PoolPage) IsEmpty() (bool, error) { + is, err := ExtractPools(p) + if err != nil { + return true, nil + } + return len(is) == 0, nil +} + +// ExtractPools accepts a Page struct, specifically a RouterPage struct, +// and extracts the elements into a slice of Router structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractPools(page pagination.Page) ([]Pool, error) { + var resp struct { + Pools []Pool `mapstructure:"pools" json:"pools"` + } + + err := mapstructure.Decode(page.(PoolPage).Body, &resp) + + return resp.Pools, err +} + +type commonResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts a router. +func (r commonResult) Extract() (*Pool, error) { + if r.Err != nil { + return nil, r.Err + } + + var res struct { + Pool *Pool `json:"pool"` + } + + err := mapstructure.Decode(r.Body, &res) + + return res.Pool, err +} + +// CreateResult represents the result of a create operation. +type CreateResult struct { + commonResult +} + +// GetResult represents the result of a get operation. +type GetResult struct { + commonResult +} + +// UpdateResult represents the result of an update operation. +type UpdateResult struct { + commonResult +} + +// DeleteResult represents the result of a delete operation. +type DeleteResult struct { + gophercloud.ErrResult +} + +// AssociateResult represents the result of an association operation. +type AssociateResult struct { + commonResult +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/pools/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/pools/urls.go new file mode 100644 index 0000000000..6cd15b0026 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/pools/urls.go @@ -0,0 +1,25 @@ +package pools + +import "github.com/rackspace/gophercloud" + +const ( + rootPath = "lb" + resourcePath = "pools" + monitorPath = "health_monitors" +) + +func rootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(rootPath, resourcePath) +} + +func resourceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(rootPath, resourcePath, id) +} + +func associateURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(rootPath, resourcePath, id, monitorPath) +} + +func disassociateURL(c *gophercloud.ServiceClient, poolID, monitorID string) string { + return c.ServiceURL(rootPath, resourcePath, poolID, monitorPath, monitorID) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/vips/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/vips/requests.go new file mode 100644 index 0000000000..ec929d6397 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/vips/requests.go @@ -0,0 +1,273 @@ +package vips + +import ( + "fmt" + + "github.com/racker/perigee" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// AdminState gives users a solid type to work with for create and update +// operations. It is recommended that users use the `Up` and `Down` enums. +type AdminState *bool + +// Convenience vars for AdminStateUp values. +var ( + iTrue = true + iFalse = false + + Up AdminState = &iTrue + Down AdminState = &iFalse +) + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the floating IP attributes you want to see returned. SortKey allows you to +// sort by a particular network attribute. SortDir sets the direction, and is +// either `asc' or `desc'. Marker and Limit are used for pagination. +type ListOpts struct { + ID string `q:"id"` + Name string `q:"name"` + AdminStateUp *bool `q:"admin_state_up"` + Status string `q:"status"` + TenantID string `q:"tenant_id"` + SubnetID string `q:"subnet_id"` + Address string `q:"address"` + PortID string `q:"port_id"` + Protocol string `q:"protocol"` + ProtocolPort int `q:"protocol_port"` + ConnectionLimit int `q:"connection_limit"` + Limit int `q:"limit"` + Marker string `q:"marker"` + SortKey string `q:"sort_key"` + SortDir string `q:"sort_dir"` +} + +// List returns a Pager which allows you to iterate over a collection of +// routers. It accepts a ListOpts struct, which allows you to filter and sort +// the returned collection for greater efficiency. +// +// Default policy settings return only those routers that are owned by the +// tenant who submits the request, unless an admin user submits the request. +func List(c *gophercloud.ServiceClient, opts ListOpts) pagination.Pager { + q, err := gophercloud.BuildQueryString(&opts) + if err != nil { + return pagination.Pager{Err: err} + } + u := rootURL(c) + q.String() + return pagination.NewPager(c, u, func(r pagination.PageResult) pagination.Page { + return VIPPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +var ( + errNameRequired = fmt.Errorf("Name is required") + errSubnetIDRequried = fmt.Errorf("SubnetID is required") + errProtocolRequired = fmt.Errorf("Protocol is required") + errProtocolPortRequired = fmt.Errorf("Protocol port is required") + errPoolIDRequired = fmt.Errorf("PoolID is required") +) + +// CreateOpts contains all the values needed to create a new virtual IP. +type CreateOpts struct { + // Required. Human-readable name for the VIP. Does not have to be unique. + Name string + + // Required. The network on which to allocate the VIP's address. A tenant can + // only create VIPs on networks authorized by policy (e.g. networks that + // belong to them or networks that are shared). + SubnetID string + + // Required. The protocol - can either be TCP, HTTP or HTTPS. + Protocol string + + // Required. The port on which to listen for client traffic. + ProtocolPort int + + // Required. The ID of the pool with which the VIP is associated. + PoolID string + + // Required for admins. Indicates the owner of the VIP. + TenantID string + + // Optional. The IP address of the VIP. + Address string + + // Optional. Human-readable description for the VIP. + Description string + + // Optional. Omit this field to prevent session persistence. + Persistence *SessionPersistence + + // Optional. The maximum number of connections allowed for the VIP. + ConnLimit *int + + // Optional. The administrative state of the VIP. A valid value is true (UP) + // or false (DOWN). + AdminStateUp *bool +} + +// Create is an operation which provisions a new virtual IP based on the +// configuration defined in the CreateOpts struct. Once the request is +// validated and progress has started on the provisioning process, a +// CreateResult will be returned. +// +// Please note that the PoolID should refer to a pool that is not already +// associated with another vip. If the pool is already used by another vip, +// then the operation will fail with a 409 Conflict error will be returned. +// +// Users with an admin role can create VIPs on behalf of other tenants by +// specifying a TenantID attribute different than their own. +func Create(c *gophercloud.ServiceClient, opts CreateOpts) CreateResult { + var res CreateResult + + // Validate required opts + if opts.Name == "" { + res.Err = errNameRequired + return res + } + if opts.SubnetID == "" { + res.Err = errSubnetIDRequried + return res + } + if opts.Protocol == "" { + res.Err = errProtocolRequired + return res + } + if opts.ProtocolPort == 0 { + res.Err = errProtocolPortRequired + return res + } + if opts.PoolID == "" { + res.Err = errPoolIDRequired + return res + } + + type vip struct { + Name string `json:"name"` + SubnetID string `json:"subnet_id"` + Protocol string `json:"protocol"` + ProtocolPort int `json:"protocol_port"` + PoolID string `json:"pool_id"` + Description *string `json:"description,omitempty"` + TenantID *string `json:"tenant_id,omitempty"` + Address *string `json:"address,omitempty"` + Persistence *SessionPersistence `json:"session_persistence,omitempty"` + ConnLimit *int `json:"connection_limit,omitempty"` + AdminStateUp *bool `json:"admin_state_up,omitempty"` + } + + type request struct { + VirtualIP vip `json:"vip"` + } + + reqBody := request{VirtualIP: vip{ + Name: opts.Name, + SubnetID: opts.SubnetID, + Protocol: opts.Protocol, + ProtocolPort: opts.ProtocolPort, + PoolID: opts.PoolID, + Description: gophercloud.MaybeString(opts.Description), + TenantID: gophercloud.MaybeString(opts.TenantID), + Address: gophercloud.MaybeString(opts.Address), + ConnLimit: opts.ConnLimit, + AdminStateUp: opts.AdminStateUp, + }} + + if opts.Persistence != nil { + reqBody.VirtualIP.Persistence = opts.Persistence + } + + _, res.Err = perigee.Request("POST", rootURL(c), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + ReqBody: &reqBody, + Results: &res.Body, + OkCodes: []int{201}, + }) + + return res +} + +// Get retrieves a particular virtual IP based on its unique ID. +func Get(c *gophercloud.ServiceClient, id string) GetResult { + var res GetResult + _, res.Err = perigee.Request("GET", resourceURL(c, id), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + Results: &res.Body, + OkCodes: []int{200}, + }) + return res +} + +// UpdateOpts contains all the values needed to update an existing virtual IP. +// Attributes not listed here but appear in CreateOpts are immutable and cannot +// be updated. +type UpdateOpts struct { + // Human-readable name for the VIP. Does not have to be unique. + Name string + + // Required. The ID of the pool with which the VIP is associated. + PoolID string + + // Optional. Human-readable description for the VIP. + Description string + + // Optional. Omit this field to prevent session persistence. + Persistence *SessionPersistence + + // Optional. The maximum number of connections allowed for the VIP. + ConnLimit *int + + // Optional. The administrative state of the VIP. A valid value is true (UP) + // or false (DOWN). + AdminStateUp *bool +} + +// Update is an operation which modifies the attributes of the specified VIP. +func Update(c *gophercloud.ServiceClient, id string, opts UpdateOpts) UpdateResult { + type vip struct { + Name string `json:"name,omitempty"` + PoolID string `json:"pool_id,omitempty"` + Description *string `json:"description,omitempty"` + Persistence *SessionPersistence `json:"session_persistence,omitempty"` + ConnLimit *int `json:"connection_limit,omitempty"` + AdminStateUp *bool `json:"admin_state_up,omitempty"` + } + + type request struct { + VirtualIP vip `json:"vip"` + } + + reqBody := request{VirtualIP: vip{ + Name: opts.Name, + PoolID: opts.PoolID, + Description: gophercloud.MaybeString(opts.Description), + ConnLimit: opts.ConnLimit, + AdminStateUp: opts.AdminStateUp, + }} + + if opts.Persistence != nil { + reqBody.VirtualIP.Persistence = opts.Persistence + } + + var res UpdateResult + _, res.Err = perigee.Request("PUT", resourceURL(c, id), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + ReqBody: &reqBody, + Results: &res.Body, + OkCodes: []int{200, 202}, + }) + + return res +} + +// Delete will permanently delete a particular virtual IP based on its unique ID. +func Delete(c *gophercloud.ServiceClient, id string) DeleteResult { + var res DeleteResult + _, res.Err = perigee.Request("DELETE", resourceURL(c, id), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + OkCodes: []int{204}, + }) + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/vips/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/vips/requests_test.go new file mode 100644 index 0000000000..430f1a1eeb --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/vips/requests_test.go @@ -0,0 +1,336 @@ +package vips + +import ( + "fmt" + "net/http" + "testing" + + fake "github.com/rackspace/gophercloud/openstack/networking/v2/common" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestURLs(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.AssertEquals(t, th.Endpoint()+"v2.0/lb/vips", rootURL(fake.ServiceClient())) + th.AssertEquals(t, th.Endpoint()+"v2.0/lb/vips/foo", resourceURL(fake.ServiceClient(), "foo")) +} + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/lb/vips", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "vips":[ + { + "id": "db902c0c-d5ff-4753-b465-668ad9656918", + "tenant_id": "310df60f-2a10-4ee5-9554-98393092194c", + "name": "web_vip", + "description": "lb config for the web tier", + "subnet_id": "96a4386a-f8c3-42ed-afce-d7954eee77b3", + "address" : "10.30.176.47", + "port_id" : "cd1f7a47-4fa6-449c-9ee7-632838aedfea", + "protocol": "HTTP", + "protocol_port": 80, + "pool_id" : "cfc6589d-f949-4c66-99d2-c2da56ef3764", + "admin_state_up": true, + "status": "ACTIVE" + }, + { + "id": "36e08a3e-a78f-4b40-a229-1e7e23eee1ab", + "tenant_id": "310df60f-2a10-4ee5-9554-98393092194c", + "name": "db_vip", + "description": "lb config for the db tier", + "subnet_id": "9cedb85d-0759-4898-8a4b-fa5a5ea10086", + "address" : "10.30.176.48", + "port_id" : "cd1f7a47-4fa6-449c-9ee7-632838aedfea", + "protocol": "TCP", + "protocol_port": 3306, + "pool_id" : "41efe233-7591-43c5-9cf7-923964759f9e", + "session_persistence" : {"type" : "SOURCE_IP"}, + "connection_limit" : 2000, + "admin_state_up": true, + "status": "INACTIVE" + } + ] +} + `) + }) + + count := 0 + + List(fake.ServiceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractVIPs(page) + if err != nil { + t.Errorf("Failed to extract LBs: %v", err) + return false, err + } + + expected := []VirtualIP{ + VirtualIP{ + ID: "db902c0c-d5ff-4753-b465-668ad9656918", + TenantID: "310df60f-2a10-4ee5-9554-98393092194c", + Name: "web_vip", + Description: "lb config for the web tier", + SubnetID: "96a4386a-f8c3-42ed-afce-d7954eee77b3", + Address: "10.30.176.47", + PortID: "cd1f7a47-4fa6-449c-9ee7-632838aedfea", + Protocol: "HTTP", + ProtocolPort: 80, + PoolID: "cfc6589d-f949-4c66-99d2-c2da56ef3764", + Persistence: SessionPersistence{}, + ConnLimit: 0, + AdminStateUp: true, + Status: "ACTIVE", + }, + VirtualIP{ + ID: "36e08a3e-a78f-4b40-a229-1e7e23eee1ab", + TenantID: "310df60f-2a10-4ee5-9554-98393092194c", + Name: "db_vip", + Description: "lb config for the db tier", + SubnetID: "9cedb85d-0759-4898-8a4b-fa5a5ea10086", + Address: "10.30.176.48", + PortID: "cd1f7a47-4fa6-449c-9ee7-632838aedfea", + Protocol: "TCP", + ProtocolPort: 3306, + PoolID: "41efe233-7591-43c5-9cf7-923964759f9e", + Persistence: SessionPersistence{Type: "SOURCE_IP"}, + ConnLimit: 2000, + AdminStateUp: true, + Status: "INACTIVE", + }, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/lb/vips", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "vip": { + "protocol": "HTTP", + "name": "NewVip", + "admin_state_up": true, + "subnet_id": "8032909d-47a1-4715-90af-5153ffe39861", + "pool_id": "61b1f87a-7a21-4ad3-9dda-7f81d249944f", + "protocol_port": 80, + "session_persistence": {"type": "SOURCE_IP"} + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, ` +{ + "vip": { + "status": "PENDING_CREATE", + "protocol": "HTTP", + "description": "", + "admin_state_up": true, + "subnet_id": "8032909d-47a1-4715-90af-5153ffe39861", + "tenant_id": "83657cfcdfe44cd5920adaf26c48ceea", + "connection_limit": -1, + "pool_id": "61b1f87a-7a21-4ad3-9dda-7f81d249944f", + "address": "10.0.0.11", + "protocol_port": 80, + "port_id": "f7e6fe6a-b8b5-43a8-8215-73456b32e0f5", + "id": "c987d2be-9a3c-4ac9-a046-e8716b1350e2", + "name": "NewVip" + } +} + `) + }) + + opts := CreateOpts{ + Protocol: "HTTP", + Name: "NewVip", + AdminStateUp: Up, + SubnetID: "8032909d-47a1-4715-90af-5153ffe39861", + PoolID: "61b1f87a-7a21-4ad3-9dda-7f81d249944f", + ProtocolPort: 80, + Persistence: &SessionPersistence{Type: "SOURCE_IP"}, + } + + r, err := Create(fake.ServiceClient(), opts).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, "PENDING_CREATE", r.Status) + th.AssertEquals(t, "HTTP", r.Protocol) + th.AssertEquals(t, "", r.Description) + th.AssertEquals(t, true, r.AdminStateUp) + th.AssertEquals(t, "8032909d-47a1-4715-90af-5153ffe39861", r.SubnetID) + th.AssertEquals(t, "83657cfcdfe44cd5920adaf26c48ceea", r.TenantID) + th.AssertEquals(t, -1, r.ConnLimit) + th.AssertEquals(t, "61b1f87a-7a21-4ad3-9dda-7f81d249944f", r.PoolID) + th.AssertEquals(t, "10.0.0.11", r.Address) + th.AssertEquals(t, 80, r.ProtocolPort) + th.AssertEquals(t, "f7e6fe6a-b8b5-43a8-8215-73456b32e0f5", r.PortID) + th.AssertEquals(t, "c987d2be-9a3c-4ac9-a046-e8716b1350e2", r.ID) + th.AssertEquals(t, "NewVip", r.Name) +} + +func TestRequiredCreateOpts(t *testing.T) { + res := Create(fake.ServiceClient(), CreateOpts{}) + if res.Err == nil { + t.Fatalf("Expected error, got none") + } + res = Create(fake.ServiceClient(), CreateOpts{Name: "foo"}) + if res.Err == nil { + t.Fatalf("Expected error, got none") + } + res = Create(fake.ServiceClient(), CreateOpts{Name: "foo", SubnetID: "bar"}) + if res.Err == nil { + t.Fatalf("Expected error, got none") + } + res = Create(fake.ServiceClient(), CreateOpts{Name: "foo", SubnetID: "bar", Protocol: "bar"}) + if res.Err == nil { + t.Fatalf("Expected error, got none") + } + res = Create(fake.ServiceClient(), CreateOpts{Name: "foo", SubnetID: "bar", Protocol: "bar", ProtocolPort: 80}) + if res.Err == nil { + t.Fatalf("Expected error, got none") + } +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/lb/vips/4ec89087-d057-4e2c-911f-60a3b47ee304", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "vip": { + "status": "ACTIVE", + "protocol": "HTTP", + "description": "", + "admin_state_up": true, + "subnet_id": "8032909d-47a1-4715-90af-5153ffe39861", + "tenant_id": "83657cfcdfe44cd5920adaf26c48ceea", + "connection_limit": 1000, + "pool_id": "72741b06-df4d-4715-b142-276b6bce75ab", + "session_persistence": { + "cookie_name": "MyAppCookie", + "type": "APP_COOKIE" + }, + "address": "10.0.0.10", + "protocol_port": 80, + "port_id": "b5a743d6-056b-468b-862d-fb13a9aa694e", + "id": "4ec89087-d057-4e2c-911f-60a3b47ee304", + "name": "my-vip" + } +} + `) + }) + + vip, err := Get(fake.ServiceClient(), "4ec89087-d057-4e2c-911f-60a3b47ee304").Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, "ACTIVE", vip.Status) + th.AssertEquals(t, "HTTP", vip.Protocol) + th.AssertEquals(t, "", vip.Description) + th.AssertEquals(t, true, vip.AdminStateUp) + th.AssertEquals(t, 1000, vip.ConnLimit) + th.AssertEquals(t, SessionPersistence{Type: "APP_COOKIE", CookieName: "MyAppCookie"}, vip.Persistence) +} + +func TestUpdate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/lb/vips/4ec89087-d057-4e2c-911f-60a3b47ee304", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "vip": { + "connection_limit": 1000, + "session_persistence": {"type": "SOURCE_IP"} + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusAccepted) + + fmt.Fprintf(w, ` +{ + "vip": { + "status": "PENDING_UPDATE", + "protocol": "HTTP", + "description": "", + "admin_state_up": true, + "subnet_id": "8032909d-47a1-4715-90af-5153ffe39861", + "tenant_id": "83657cfcdfe44cd5920adaf26c48ceea", + "connection_limit": 1000, + "pool_id": "61b1f87a-7a21-4ad3-9dda-7f81d249944f", + "address": "10.0.0.11", + "protocol_port": 80, + "port_id": "f7e6fe6a-b8b5-43a8-8215-73456b32e0f5", + "id": "c987d2be-9a3c-4ac9-a046-e8716b1350e2", + "name": "NewVip" + } +} + `) + }) + + i1000 := 1000 + options := UpdateOpts{ + ConnLimit: &i1000, + Persistence: &SessionPersistence{Type: "SOURCE_IP"}, + } + vip, err := Update(fake.ServiceClient(), "4ec89087-d057-4e2c-911f-60a3b47ee304", options).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, "PENDING_UPDATE", vip.Status) + th.AssertEquals(t, 1000, vip.ConnLimit) +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/lb/vips/4ec89087-d057-4e2c-911f-60a3b47ee304", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusNoContent) + }) + + res := Delete(fake.ServiceClient(), "4ec89087-d057-4e2c-911f-60a3b47ee304") + th.AssertNoErr(t, res.Err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/vips/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/vips/results.go new file mode 100644 index 0000000000..e1092e780e --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/vips/results.go @@ -0,0 +1,166 @@ +package vips + +import ( + "github.com/mitchellh/mapstructure" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// SessionPersistence represents the session persistence feature of the load +// balancing service. It attempts to force connections or requests in the same +// session to be processed by the same member as long as it is ative. Three +// types of persistence are supported: +// +// SOURCE_IP: With this mode, all connections originating from the same source +// IP address, will be handled by the same member of the pool. +// HTTP_COOKIE: With this persistence mode, the load balancing function will +// create a cookie on the first request from a client. Subsequent +// requests containing the same cookie value will be handled by +// the same member of the pool. +// APP_COOKIE: With this persistence mode, the load balancing function will +// rely on a cookie established by the backend application. All +// requests carrying the same cookie value will be handled by the +// same member of the pool. +type SessionPersistence struct { + // The type of persistence mode + Type string `mapstructure:"type" json:"type"` + + // Name of cookie if persistence mode is set appropriately + CookieName string `mapstructure:"cookie_name" json:"cookie_name,omitempty"` +} + +// VirtualIP is the primary load balancing configuration object that specifies +// the virtual IP address and port on which client traffic is received, as well +// as other details such as the load balancing method to be use, protocol, etc. +// This entity is sometimes known in LB products under the name of a "virtual +// server", a "vserver" or a "listener". +type VirtualIP struct { + // The unique ID for the VIP. + ID string `mapstructure:"id" json:"id"` + + // Owner of the VIP. Only an admin user can specify a tenant ID other than its own. + TenantID string `mapstructure:"tenant_id" json:"tenant_id"` + + // Human-readable name for the VIP. Does not have to be unique. + Name string `mapstructure:"name" json:"name"` + + // Human-readable description for the VIP. + Description string `mapstructure:"description" json:"description"` + + // The ID of the subnet on which to allocate the VIP address. + SubnetID string `mapstructure:"subnet_id" json:"subnet_id"` + + // The IP address of the VIP. + Address string `mapstructure:"address" json:"address"` + + // The protocol of the VIP address. A valid value is TCP, HTTP, or HTTPS. + Protocol string `mapstructure:"protocol" json:"protocol"` + + // The port on which to listen to client traffic that is associated with the + // VIP address. A valid value is from 0 to 65535. + ProtocolPort int `mapstructure:"protocol_port" json:"protocol_port"` + + // The ID of the pool with which the VIP is associated. + PoolID string `mapstructure:"pool_id" json:"pool_id"` + + // The ID of the port which belongs to the load balancer + PortID string `mapstructure:"port_id" json:"port_id"` + + // Indicates whether connections in the same session will be processed by the + // same pool member or not. + Persistence SessionPersistence `mapstructure:"session_persistence" json:"session_persistence"` + + // The maximum number of connections allowed for the VIP. Default is -1, + // meaning no limit. + ConnLimit int `mapstructure:"connection_limit" json:"connection_limit"` + + // The administrative state of the VIP. A valid value is true (UP) or false (DOWN). + AdminStateUp bool `mapstructure:"admin_state_up" json:"admin_state_up"` + + // The status of the VIP. Indicates whether the VIP is operational. + Status string `mapstructure:"status" json:"status"` +} + +// VIPPage is the page returned by a pager when traversing over a +// collection of routers. +type VIPPage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of routers has reached +// the end of a page and the pager seeks to traverse over a new one. In order +// to do this, it needs to construct the next page's URL. +func (p VIPPage) NextPageURL() (string, error) { + type resp struct { + Links []gophercloud.Link `mapstructure:"vips_links"` + } + + var r resp + err := mapstructure.Decode(p.Body, &r) + if err != nil { + return "", err + } + + return gophercloud.ExtractNextURL(r.Links) +} + +// IsEmpty checks whether a RouterPage struct is empty. +func (p VIPPage) IsEmpty() (bool, error) { + is, err := ExtractVIPs(p) + if err != nil { + return true, nil + } + return len(is) == 0, nil +} + +// ExtractVIPs accepts a Page struct, specifically a VIPPage struct, +// and extracts the elements into a slice of VirtualIP structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractVIPs(page pagination.Page) ([]VirtualIP, error) { + var resp struct { + VIPs []VirtualIP `mapstructure:"vips" json:"vips"` + } + + err := mapstructure.Decode(page.(VIPPage).Body, &resp) + + return resp.VIPs, err +} + +type commonResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts a router. +func (r commonResult) Extract() (*VirtualIP, error) { + if r.Err != nil { + return nil, r.Err + } + + var res struct { + VirtualIP *VirtualIP `mapstructure:"vip" json:"vip"` + } + + err := mapstructure.Decode(r.Body, &res) + + return res.VirtualIP, err +} + +// CreateResult represents the result of a create operation. +type CreateResult struct { + commonResult +} + +// GetResult represents the result of a get operation. +type GetResult struct { + commonResult +} + +// UpdateResult represents the result of an update operation. +type UpdateResult struct { + commonResult +} + +// DeleteResult represents the result of a delete operation. +type DeleteResult struct { + gophercloud.ErrResult +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/vips/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/vips/urls.go new file mode 100644 index 0000000000..2b6f67e71d --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/vips/urls.go @@ -0,0 +1,16 @@ +package vips + +import "github.com/rackspace/gophercloud" + +const ( + rootPath = "lb" + resourcePath = "vips" +) + +func rootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(rootPath, resourcePath) +} + +func resourceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(rootPath, resourcePath, id) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/provider/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/provider/doc.go new file mode 100644 index 0000000000..373da44f84 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/provider/doc.go @@ -0,0 +1,21 @@ +// Package provider gives access to the provider Neutron plugin, allowing +// network extended attributes. The provider extended attributes for networks +// enable administrative users to specify how network objects map to the +// underlying networking infrastructure. These extended attributes also appear +// when administrative users query networks. +// +// For more information about extended attributes, see the NetworkExtAttrs +// struct. The actual semantics of these attributes depend on the technology +// back end of the particular plug-in. See the plug-in documentation and the +// OpenStack Cloud Administrator Guide to understand which values should be +// specific for each of these attributes when OpenStack Networking is deployed +// with a particular plug-in. The examples shown in this chapter refer to the +// Open vSwitch plug-in. +// +// The default policy settings enable only users with administrative rights to +// specify these parameters in requests and to see their values in responses. By +// default, the provider network extension attributes are completely hidden from +// regular tenants. As a rule of thumb, if these attributes are not visible in a +// GET /networks/ operation, this implies the user submitting the +// request is not authorized to view or manipulate provider network attributes. +package provider diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/provider/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/provider/results.go new file mode 100644 index 0000000000..3453584587 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/provider/results.go @@ -0,0 +1,124 @@ +package provider + +import ( + "github.com/mitchellh/mapstructure" + "github.com/rackspace/gophercloud/openstack/networking/v2/networks" + "github.com/rackspace/gophercloud/pagination" +) + +// AdminState gives users a solid type to work with for create and update +// operations. It is recommended that users use the `Up` and `Down` enums. +type AdminState *bool + +// Convenience vars for AdminStateUp values. +var ( + iTrue = true + iFalse = false + + Up AdminState = &iTrue + Down AdminState = &iFalse +) + +// NetworkExtAttrs represents an extended form of a Network with additional fields. +type NetworkExtAttrs struct { + // UUID for the network + ID string `mapstructure:"id" json:"id"` + + // Human-readable name for the network. Might not be unique. + Name string `mapstructure:"name" json:"name"` + + // The administrative state of network. If false (down), the network does not forward packets. + AdminStateUp bool `mapstructure:"admin_state_up" json:"admin_state_up"` + + // Indicates whether network is currently operational. Possible values include + // `ACTIVE', `DOWN', `BUILD', or `ERROR'. Plug-ins might define additional values. + Status string `mapstructure:"status" json:"status"` + + // Subnets associated with this network. + Subnets []string `mapstructure:"subnets" json:"subnets"` + + // Owner of network. Only admin users can specify a tenant_id other than its own. + TenantID string `mapstructure:"tenant_id" json:"tenant_id"` + + // Specifies whether the network resource can be accessed by any tenant or not. + Shared bool `mapstructure:"shared" json:"shared"` + + // Specifies the nature of the physical network mapped to this network + // resource. Examples are flat, vlan, or gre. + NetworkType string `json:"provider:network_type" mapstructure:"provider:network_type"` + + // Identifies the physical network on top of which this network object is + // being implemented. The OpenStack Networking API does not expose any facility + // for retrieving the list of available physical networks. As an example, in + // the Open vSwitch plug-in this is a symbolic name which is then mapped to + // specific bridges on each compute host through the Open vSwitch plug-in + // configuration file. + PhysicalNetwork string `json:"provider:physical_network" mapstructure:"provider:physical_network"` + + // Identifies an isolated segment on the physical network; the nature of the + // segment depends on the segmentation model defined by network_type. For + // instance, if network_type is vlan, then this is a vlan identifier; + // otherwise, if network_type is gre, then this will be a gre key. + SegmentationID string `json:"provider:segmentation_id" mapstructure:"provider:segmentation_id"` +} + +// ExtractGet decorates a GetResult struct returned from a networks.Get() +// function with extended attributes. +func ExtractGet(r networks.GetResult) (*NetworkExtAttrs, error) { + if r.Err != nil { + return nil, r.Err + } + + var res struct { + Network *NetworkExtAttrs `json:"network"` + } + + err := mapstructure.Decode(r.Body, &res) + + return res.Network, err +} + +// ExtractCreate decorates a CreateResult struct returned from a networks.Create() +// function with extended attributes. +func ExtractCreate(r networks.CreateResult) (*NetworkExtAttrs, error) { + if r.Err != nil { + return nil, r.Err + } + + var res struct { + Network *NetworkExtAttrs `json:"network"` + } + + err := mapstructure.Decode(r.Body, &res) + + return res.Network, err +} + +// ExtractUpdate decorates a UpdateResult struct returned from a +// networks.Update() function with extended attributes. +func ExtractUpdate(r networks.UpdateResult) (*NetworkExtAttrs, error) { + if r.Err != nil { + return nil, r.Err + } + + var res struct { + Network *NetworkExtAttrs `json:"network"` + } + + err := mapstructure.Decode(r.Body, &res) + + return res.Network, err +} + +// ExtractList accepts a Page struct, specifically a NetworkPage struct, and +// extracts the elements into a slice of NetworkExtAttrs structs. In other +// words, a generic collection is mapped into a relevant slice. +func ExtractList(page pagination.Page) ([]NetworkExtAttrs, error) { + var resp struct { + Networks []NetworkExtAttrs `mapstructure:"networks" json:"networks"` + } + + err := mapstructure.Decode(page.(networks.NetworkPage).Body, &resp) + + return resp.Networks, err +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/provider/results_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/provider/results_test.go new file mode 100644 index 0000000000..9801b2e5e3 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/provider/results_test.go @@ -0,0 +1,253 @@ +package provider + +import ( + "fmt" + "net/http" + "testing" + + fake "github.com/rackspace/gophercloud/openstack/networking/v2/common" + "github.com/rackspace/gophercloud/openstack/networking/v2/networks" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/networks", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "networks": [ + { + "status": "ACTIVE", + "subnets": [ + "54d6f61d-db07-451c-9ab3-b9609b6b6f0b" + ], + "name": "private-network", + "admin_state_up": true, + "tenant_id": "4fd44f30292945e481c7b8a0c8908869", + "shared": true, + "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22", + "provider:segmentation_id": null, + "provider:physical_network": null, + "provider:network_type": "local" + }, + { + "status": "ACTIVE", + "subnets": [ + "08eae331-0402-425a-923c-34f7cfe39c1b" + ], + "name": "private", + "admin_state_up": true, + "tenant_id": "26a7980765d0414dbc1fc1f88cdb7e6e", + "shared": true, + "id": "db193ab3-96e3-4cb3-8fc5-05f4296d0324", + "provider:segmentation_id": null, + "provider:physical_network": null, + "provider:network_type": "local" + } + ] +} + `) + }) + + count := 0 + + networks.List(fake.ServiceClient(), networks.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractList(page) + if err != nil { + t.Errorf("Failed to extract networks: %v", err) + return false, err + } + + expected := []NetworkExtAttrs{ + NetworkExtAttrs{ + Status: "ACTIVE", + Subnets: []string{"54d6f61d-db07-451c-9ab3-b9609b6b6f0b"}, + Name: "private-network", + AdminStateUp: true, + TenantID: "4fd44f30292945e481c7b8a0c8908869", + Shared: true, + ID: "d32019d3-bc6e-4319-9c1d-6722fc136a22", + NetworkType: "local", + PhysicalNetwork: "", + SegmentationID: "", + }, + NetworkExtAttrs{ + Status: "ACTIVE", + Subnets: []string{"08eae331-0402-425a-923c-34f7cfe39c1b"}, + Name: "private", + AdminStateUp: true, + TenantID: "26a7980765d0414dbc1fc1f88cdb7e6e", + Shared: true, + ID: "db193ab3-96e3-4cb3-8fc5-05f4296d0324", + NetworkType: "local", + PhysicalNetwork: "", + SegmentationID: "", + }, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/networks/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "network": { + "status": "ACTIVE", + "subnets": [ + "54d6f61d-db07-451c-9ab3-b9609b6b6f0b" + ], + "name": "private-network", + "provider:physical_network": null, + "admin_state_up": true, + "tenant_id": "4fd44f30292945e481c7b8a0c8908869", + "provider:network_type": "local", + "shared": true, + "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22", + "provider:segmentation_id": null + } +} + `) + }) + + res := networks.Get(fake.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22") + n, err := ExtractGet(res) + + th.AssertNoErr(t, err) + + th.AssertEquals(t, "", n.PhysicalNetwork) + th.AssertEquals(t, "local", n.NetworkType) + th.AssertEquals(t, "", n.SegmentationID) +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/networks", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "network": { + "name": "sample_network", + "admin_state_up": true + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, ` +{ + "network": { + "status": "ACTIVE", + "subnets": [ + "54d6f61d-db07-451c-9ab3-b9609b6b6f0b" + ], + "name": "private-network", + "provider:physical_network": null, + "admin_state_up": true, + "tenant_id": "4fd44f30292945e481c7b8a0c8908869", + "provider:network_type": "local", + "shared": true, + "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22", + "provider:segmentation_id": null + } +} + `) + }) + + options := networks.CreateOpts{Name: "sample_network", AdminStateUp: Up} + res := networks.Create(fake.ServiceClient(), options) + n, err := ExtractCreate(res) + + th.AssertNoErr(t, err) + + th.AssertEquals(t, "", n.PhysicalNetwork) + th.AssertEquals(t, "local", n.NetworkType) + th.AssertEquals(t, "", n.SegmentationID) +} + +func TestUpdate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/networks/4e8e5957-649f-477b-9e5b-f1f75b21c03c", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "network": { + "name": "new_network_name", + "admin_state_up": false, + "shared": true + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "network": { + "status": "ACTIVE", + "subnets": [ + "54d6f61d-db07-451c-9ab3-b9609b6b6f0b" + ], + "name": "private-network", + "provider:physical_network": null, + "admin_state_up": true, + "tenant_id": "4fd44f30292945e481c7b8a0c8908869", + "provider:network_type": "local", + "shared": true, + "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22", + "provider:segmentation_id": null + } +} + `) + }) + + iTrue := true + options := networks.UpdateOpts{Name: "new_network_name", AdminStateUp: Down, Shared: &iTrue} + res := networks.Update(fake.ServiceClient(), "4e8e5957-649f-477b-9e5b-f1f75b21c03c", options) + n, err := ExtractUpdate(res) + + th.AssertNoErr(t, err) + + th.AssertEquals(t, "", n.PhysicalNetwork) + th.AssertEquals(t, "local", n.NetworkType) + th.AssertEquals(t, "", n.SegmentationID) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/doc.go new file mode 100644 index 0000000000..8ef455ffb3 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/doc.go @@ -0,0 +1,32 @@ +// Package security contains functionality to work with security group and +// security group rules Neutron resources. +// +// Security groups and security group rules allows administrators and tenants +// the ability to specify the type of traffic and direction (ingress/egress) +// that is allowed to pass through a port. A security group is a container for +// security group rules. +// +// When a port is created in Networking it is associated with a security group. +// If a security group is not specified the port is associated with a 'default' +// security group. By default, this group drops all ingress traffic and allows +// all egress. Rules can be added to this group in order to change the behaviour. +// +// The basic characteristics of Neutron Security Groups are: +// +// For ingress traffic (to an instance) +// - Only traffic matched with security group rules are allowed. +// - When there is no rule defined, all traffic are dropped. +// +// For egress traffic (from an instance) +// - Only traffic matched with security group rules are allowed. +// - When there is no rule defined, all egress traffic are dropped. +// - When a new security group is created, rules to allow all egress traffic +// are automatically added. +// +// "default security group" is defined for each tenant. +// - For the default security group a rule which allows intercommunication +// among hosts associated with the default security group is defined by default. +// - As a result, all egress traffic and intercommunication in the default +// group are allowed and all ingress from outside of the default group is +// dropped by default (in the default security group). +package security diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/groups/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/groups/requests.go new file mode 100644 index 0000000000..0c970ae6f2 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/groups/requests.go @@ -0,0 +1,107 @@ +package groups + +import ( + "fmt" + + "github.com/racker/perigee" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the floating IP attributes you want to see returned. SortKey allows you to +// sort by a particular network attribute. SortDir sets the direction, and is +// either `asc' or `desc'. Marker and Limit are used for pagination. +type ListOpts struct { + ID string `q:"id"` + Name string `q:"name"` + TenantID string `q:"tenant_id"` + Limit int `q:"limit"` + Marker string `q:"marker"` + SortKey string `q:"sort_key"` + SortDir string `q:"sort_dir"` +} + +// List returns a Pager which allows you to iterate over a collection of +// security groups. It accepts a ListOpts struct, which allows you to filter +// and sort the returned collection for greater efficiency. +func List(c *gophercloud.ServiceClient, opts ListOpts) pagination.Pager { + q, err := gophercloud.BuildQueryString(&opts) + if err != nil { + return pagination.Pager{Err: err} + } + u := rootURL(c) + q.String() + return pagination.NewPager(c, u, func(r pagination.PageResult) pagination.Page { + return SecGroupPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +var ( + errNameRequired = fmt.Errorf("Name is required") +) + +// CreateOpts contains all the values needed to create a new security group. +type CreateOpts struct { + // Required. Human-readable name for the VIP. Does not have to be unique. + Name string + + // Optional. Describes the security group. + Description string +} + +// Create is an operation which provisions a new security group with default +// security group rules for the IPv4 and IPv6 ether types. +func Create(c *gophercloud.ServiceClient, opts CreateOpts) CreateResult { + var res CreateResult + + // Validate required opts + if opts.Name == "" { + res.Err = errNameRequired + return res + } + + type secgroup struct { + Name string `json:"name"` + Description string `json:"description,omitempty"` + } + + type request struct { + SecGroup secgroup `json:"security_group"` + } + + reqBody := request{SecGroup: secgroup{ + Name: opts.Name, + Description: opts.Description, + }} + + _, res.Err = perigee.Request("POST", rootURL(c), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + ReqBody: &reqBody, + Results: &res.Body, + OkCodes: []int{201}, + }) + + return res +} + +// Get retrieves a particular security group based on its unique ID. +func Get(c *gophercloud.ServiceClient, id string) GetResult { + var res GetResult + _, res.Err = perigee.Request("GET", resourceURL(c, id), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + Results: &res.Body, + OkCodes: []int{200}, + }) + return res +} + +// Delete will permanently delete a particular security group based on its unique ID. +func Delete(c *gophercloud.ServiceClient, id string) DeleteResult { + var res DeleteResult + _, res.Err = perigee.Request("DELETE", resourceURL(c, id), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + OkCodes: []int{204}, + }) + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/groups/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/groups/requests_test.go new file mode 100644 index 0000000000..5f074c72f3 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/groups/requests_test.go @@ -0,0 +1,213 @@ +package groups + +import ( + "fmt" + "net/http" + "testing" + + fake "github.com/rackspace/gophercloud/openstack/networking/v2/common" + "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/rules" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestURLs(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.AssertEquals(t, th.Endpoint()+"v2.0/security-groups", rootURL(fake.ServiceClient())) + th.AssertEquals(t, th.Endpoint()+"v2.0/security-groups/foo", resourceURL(fake.ServiceClient(), "foo")) +} + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/security-groups", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "security_groups": [ + { + "description": "default", + "id": "85cc3048-abc3-43cc-89b3-377341426ac5", + "name": "default", + "security_group_rules": [], + "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550" + } + ] +} + `) + }) + + count := 0 + + List(fake.ServiceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractGroups(page) + if err != nil { + t.Errorf("Failed to extract secgroups: %v", err) + return false, err + } + + expected := []SecGroup{ + SecGroup{ + Description: "default", + ID: "85cc3048-abc3-43cc-89b3-377341426ac5", + Name: "default", + Rules: []rules.SecGroupRule{}, + TenantID: "e4f50856753b4dc6afee5fa6b9b6c550", + }, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/security-groups", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "security_group": { + "name": "new-webservers", + "description": "security group for webservers" + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, ` +{ + "security_group": { + "description": "security group for webservers", + "id": "2076db17-a522-4506-91de-c6dd8e837028", + "name": "new-webservers", + "security_group_rules": [ + { + "direction": "egress", + "ethertype": "IPv4", + "id": "38ce2d8e-e8f1-48bd-83c2-d33cb9f50c3d", + "port_range_max": null, + "port_range_min": null, + "protocol": null, + "remote_group_id": null, + "remote_ip_prefix": null, + "security_group_id": "2076db17-a522-4506-91de-c6dd8e837028", + "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550" + }, + { + "direction": "egress", + "ethertype": "IPv6", + "id": "565b9502-12de-4ffd-91e9-68885cff6ae1", + "port_range_max": null, + "port_range_min": null, + "protocol": null, + "remote_group_id": null, + "remote_ip_prefix": null, + "security_group_id": "2076db17-a522-4506-91de-c6dd8e837028", + "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550" + } + ], + "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550" + } +} + `) + }) + + opts := CreateOpts{Name: "new-webservers", Description: "security group for webservers"} + _, err := Create(fake.ServiceClient(), opts).Extract() + th.AssertNoErr(t, err) +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/security-groups/85cc3048-abc3-43cc-89b3-377341426ac5", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "security_group": { + "description": "default", + "id": "85cc3048-abc3-43cc-89b3-377341426ac5", + "name": "default", + "security_group_rules": [ + { + "direction": "egress", + "ethertype": "IPv6", + "id": "3c0e45ff-adaf-4124-b083-bf390e5482ff", + "port_range_max": null, + "port_range_min": null, + "protocol": null, + "remote_group_id": null, + "remote_ip_prefix": null, + "security_group_id": "85cc3048-abc3-43cc-89b3-377341426ac5", + "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550" + }, + { + "direction": "egress", + "ethertype": "IPv4", + "id": "93aa42e5-80db-4581-9391-3a608bd0e448", + "port_range_max": null, + "port_range_min": null, + "protocol": null, + "remote_group_id": null, + "remote_ip_prefix": null, + "security_group_id": "85cc3048-abc3-43cc-89b3-377341426ac5", + "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550" + } + ], + "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550" + } +} + `) + }) + + sg, err := Get(fake.ServiceClient(), "85cc3048-abc3-43cc-89b3-377341426ac5").Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, "default", sg.Description) + th.AssertEquals(t, "85cc3048-abc3-43cc-89b3-377341426ac5", sg.ID) + th.AssertEquals(t, "default", sg.Name) + th.AssertEquals(t, 2, len(sg.Rules)) + th.AssertEquals(t, "e4f50856753b4dc6afee5fa6b9b6c550", sg.TenantID) +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/security-groups/4ec89087-d057-4e2c-911f-60a3b47ee304", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusNoContent) + }) + + res := Delete(fake.ServiceClient(), "4ec89087-d057-4e2c-911f-60a3b47ee304") + th.AssertNoErr(t, res.Err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/groups/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/groups/results.go new file mode 100644 index 0000000000..49db261c22 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/groups/results.go @@ -0,0 +1,108 @@ +package groups + +import ( + "github.com/mitchellh/mapstructure" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/rules" + "github.com/rackspace/gophercloud/pagination" +) + +// SecGroup represents a container for security group rules. +type SecGroup struct { + // The UUID for the security group. + ID string + + // Human-readable name for the security group. Might not be unique. Cannot be + // named "default" as that is automatically created for a tenant. + Name string + + // The security group description. + Description string + + // A slice of security group rules that dictate the permitted behaviour for + // traffic entering and leaving the group. + Rules []rules.SecGroupRule `json:"security_group_rules" mapstructure:"security_group_rules"` + + // Owner of the security group. Only admin users can specify a TenantID + // other than their own. + TenantID string `json:"tenant_id" mapstructure:"tenant_id"` +} + +// SecGroupPage is the page returned by a pager when traversing over a +// collection of security groups. +type SecGroupPage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of security groups has +// reached the end of a page and the pager seeks to traverse over a new one. In +// order to do this, it needs to construct the next page's URL. +func (p SecGroupPage) NextPageURL() (string, error) { + type resp struct { + Links []gophercloud.Link `mapstructure:"security_groups_links"` + } + + var r resp + err := mapstructure.Decode(p.Body, &r) + if err != nil { + return "", err + } + + return gophercloud.ExtractNextURL(r.Links) +} + +// IsEmpty checks whether a SecGroupPage struct is empty. +func (p SecGroupPage) IsEmpty() (bool, error) { + is, err := ExtractGroups(p) + if err != nil { + return true, nil + } + return len(is) == 0, nil +} + +// ExtractGroups accepts a Page struct, specifically a SecGroupPage struct, +// and extracts the elements into a slice of SecGroup structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractGroups(page pagination.Page) ([]SecGroup, error) { + var resp struct { + SecGroups []SecGroup `mapstructure:"security_groups" json:"security_groups"` + } + + err := mapstructure.Decode(page.(SecGroupPage).Body, &resp) + + return resp.SecGroups, err +} + +type commonResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts a security group. +func (r commonResult) Extract() (*SecGroup, error) { + if r.Err != nil { + return nil, r.Err + } + + var res struct { + SecGroup *SecGroup `mapstructure:"security_group" json:"security_group"` + } + + err := mapstructure.Decode(r.Body, &res) + + return res.SecGroup, err +} + +// CreateResult represents the result of a create operation. +type CreateResult struct { + commonResult +} + +// GetResult represents the result of a get operation. +type GetResult struct { + commonResult +} + +// DeleteResult represents the result of a delete operation. +type DeleteResult struct { + gophercloud.ErrResult +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/groups/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/groups/urls.go new file mode 100644 index 0000000000..84f7324f09 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/groups/urls.go @@ -0,0 +1,13 @@ +package groups + +import "github.com/rackspace/gophercloud" + +const rootPath = "security-groups" + +func rootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(rootPath) +} + +func resourceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(rootPath, id) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/rules/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/rules/requests.go new file mode 100644 index 0000000000..edaebe82cd --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/rules/requests.go @@ -0,0 +1,183 @@ +package rules + +import ( + "fmt" + + "github.com/racker/perigee" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the security group attributes you want to see returned. SortKey allows you to +// sort by a particular network attribute. SortDir sets the direction, and is +// either `asc' or `desc'. Marker and Limit are used for pagination. +type ListOpts struct { + Direction string `q:"direction"` + EtherType string `q:"ethertype"` + ID string `q:"id"` + PortRangeMax int `q:"port_range_max"` + PortRangeMin int `q:"port_range_min"` + Protocol string `q:"protocol"` + RemoteGroupID string `q:"remote_group_id"` + RemoteIPPrefix string `q:"remote_ip_prefix"` + SecGroupID string `q:"security_group_id"` + TenantID string `q:"tenant_id"` + Limit int `q:"limit"` + Marker string `q:"marker"` + SortKey string `q:"sort_key"` + SortDir string `q:"sort_dir"` +} + +// List returns a Pager which allows you to iterate over a collection of +// security group rules. It accepts a ListOpts struct, which allows you to filter +// and sort the returned collection for greater efficiency. +func List(c *gophercloud.ServiceClient, opts ListOpts) pagination.Pager { + q, err := gophercloud.BuildQueryString(&opts) + if err != nil { + return pagination.Pager{Err: err} + } + u := rootURL(c) + q.String() + return pagination.NewPager(c, u, func(r pagination.PageResult) pagination.Page { + return SecGroupRulePage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Errors +var ( + errValidDirectionRequired = fmt.Errorf("A valid Direction is required") + errValidEtherTypeRequired = fmt.Errorf("A valid EtherType is required") + errSecGroupIDRequired = fmt.Errorf("A valid SecGroupID is required") + errValidProtocolRequired = fmt.Errorf("A valid Protocol is required") +) + +// Constants useful for CreateOpts +const ( + DirIngress = "ingress" + DirEgress = "egress" + Ether4 = "IPv4" + Ether6 = "IPv6" + ProtocolTCP = "tcp" + ProtocolUDP = "udp" + ProtocolICMP = "icmp" +) + +// CreateOpts contains all the values needed to create a new security group rule. +type CreateOpts struct { + // Required. Must be either "ingress" or "egress": the direction in which the + // security group rule is applied. + Direction string + + // Required. Must be "IPv4" or "IPv6", and addresses represented in CIDR must + // match the ingress or egress rules. + EtherType string + + // Required. The security group ID to associate with this security group rule. + SecGroupID string + + // Optional. The maximum port number in the range that is matched by the + // security group rule. The PortRangeMin attribute constrains the PortRangeMax + // attribute. If the protocol is ICMP, this value must be an ICMP type. + PortRangeMax int + + // Optional. The minimum port number in the range that is matched by the + // security group rule. If the protocol is TCP or UDP, this value must be + // less than or equal to the value of the PortRangeMax attribute. If the + // protocol is ICMP, this value must be an ICMP type. + PortRangeMin int + + // Optional. The protocol that is matched by the security group rule. Valid + // values are "tcp", "udp", "icmp" or an empty string. + Protocol string + + // Optional. The remote group ID to be associated with this security group + // rule. You can specify either RemoteGroupID or RemoteIPPrefix. + RemoteGroupID string + + // Optional. The remote IP prefix to be associated with this security group + // rule. You can specify either RemoteGroupID or RemoteIPPrefix. This + // attribute matches the specified IP prefix as the source IP address of the + // IP packet. + RemoteIPPrefix string +} + +// Create is an operation which provisions a new security group with default +// security group rules for the IPv4 and IPv6 ether types. +func Create(c *gophercloud.ServiceClient, opts CreateOpts) CreateResult { + var res CreateResult + + // Validate required opts + if opts.Direction != DirIngress && opts.Direction != DirEgress { + res.Err = errValidDirectionRequired + return res + } + if opts.EtherType != Ether4 && opts.EtherType != Ether6 { + res.Err = errValidEtherTypeRequired + return res + } + if opts.SecGroupID == "" { + res.Err = errSecGroupIDRequired + return res + } + if opts.Protocol != "" && opts.Protocol != ProtocolTCP && opts.Protocol != ProtocolUDP && opts.Protocol != ProtocolICMP { + res.Err = errValidProtocolRequired + return res + } + + type secrule struct { + Direction string `json:"direction"` + EtherType string `json:"ethertype"` + SecGroupID string `json:"security_group_id"` + PortRangeMax int `json:"port_range_max,omitempty"` + PortRangeMin int `json:"port_range_min,omitempty"` + Protocol string `json:"protocol,omitempty"` + RemoteGroupID string `json:"remote_group_id,omitempty"` + RemoteIPPrefix string `json:"remote_ip_prefix,omitempty"` + } + + type request struct { + SecRule secrule `json:"security_group_rule"` + } + + reqBody := request{SecRule: secrule{ + Direction: opts.Direction, + EtherType: opts.EtherType, + SecGroupID: opts.SecGroupID, + PortRangeMax: opts.PortRangeMax, + PortRangeMin: opts.PortRangeMin, + Protocol: opts.Protocol, + RemoteGroupID: opts.RemoteGroupID, + RemoteIPPrefix: opts.RemoteIPPrefix, + }} + + _, res.Err = perigee.Request("POST", rootURL(c), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + ReqBody: &reqBody, + Results: &res.Body, + OkCodes: []int{201}, + }) + + return res +} + +// Get retrieves a particular security group based on its unique ID. +func Get(c *gophercloud.ServiceClient, id string) GetResult { + var res GetResult + _, res.Err = perigee.Request("GET", resourceURL(c, id), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + Results: &res.Body, + OkCodes: []int{200}, + }) + return res +} + +// Delete will permanently delete a particular security group based on its unique ID. +func Delete(c *gophercloud.ServiceClient, id string) DeleteResult { + var res DeleteResult + _, res.Err = perigee.Request("DELETE", resourceURL(c, id), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + OkCodes: []int{204}, + }) + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/rules/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/rules/requests_test.go new file mode 100644 index 0000000000..b5afef31ed --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/rules/requests_test.go @@ -0,0 +1,243 @@ +package rules + +import ( + "fmt" + "net/http" + "testing" + + fake "github.com/rackspace/gophercloud/openstack/networking/v2/common" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestURLs(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.AssertEquals(t, th.Endpoint()+"v2.0/security-group-rules", rootURL(fake.ServiceClient())) + th.AssertEquals(t, th.Endpoint()+"v2.0/security-group-rules/foo", resourceURL(fake.ServiceClient(), "foo")) +} + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/security-group-rules", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "security_group_rules": [ + { + "direction": "egress", + "ethertype": "IPv6", + "id": "3c0e45ff-adaf-4124-b083-bf390e5482ff", + "port_range_max": null, + "port_range_min": null, + "protocol": null, + "remote_group_id": null, + "remote_ip_prefix": null, + "security_group_id": "85cc3048-abc3-43cc-89b3-377341426ac5", + "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550" + }, + { + "direction": "egress", + "ethertype": "IPv4", + "id": "93aa42e5-80db-4581-9391-3a608bd0e448", + "port_range_max": null, + "port_range_min": null, + "protocol": null, + "remote_group_id": null, + "remote_ip_prefix": null, + "security_group_id": "85cc3048-abc3-43cc-89b3-377341426ac5", + "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550" + } + ] +} + `) + }) + + count := 0 + + List(fake.ServiceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractRules(page) + if err != nil { + t.Errorf("Failed to extract secrules: %v", err) + return false, err + } + + expected := []SecGroupRule{ + SecGroupRule{ + Direction: "egress", + EtherType: "IPv6", + ID: "3c0e45ff-adaf-4124-b083-bf390e5482ff", + PortRangeMax: 0, + PortRangeMin: 0, + Protocol: "", + RemoteGroupID: "", + RemoteIPPrefix: "", + SecGroupID: "85cc3048-abc3-43cc-89b3-377341426ac5", + TenantID: "e4f50856753b4dc6afee5fa6b9b6c550", + }, + SecGroupRule{ + Direction: "egress", + EtherType: "IPv4", + ID: "93aa42e5-80db-4581-9391-3a608bd0e448", + PortRangeMax: 0, + PortRangeMin: 0, + Protocol: "", + RemoteGroupID: "", + RemoteIPPrefix: "", + SecGroupID: "85cc3048-abc3-43cc-89b3-377341426ac5", + TenantID: "e4f50856753b4dc6afee5fa6b9b6c550", + }, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/security-group-rules", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "security_group_rule": { + "direction": "ingress", + "port_range_min": 80, + "ethertype": "IPv4", + "port_range_max": 80, + "protocol": "tcp", + "remote_group_id": "85cc3048-abc3-43cc-89b3-377341426ac5", + "security_group_id": "a7734e61-b545-452d-a3cd-0189cbd9747a" + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, ` +{ + "security_group_rule": { + "direction": "ingress", + "ethertype": "IPv4", + "id": "2bc0accf-312e-429a-956e-e4407625eb62", + "port_range_max": 80, + "port_range_min": 80, + "protocol": "tcp", + "remote_group_id": "85cc3048-abc3-43cc-89b3-377341426ac5", + "remote_ip_prefix": null, + "security_group_id": "a7734e61-b545-452d-a3cd-0189cbd9747a", + "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550" + } +} + `) + }) + + opts := CreateOpts{ + Direction: "ingress", + PortRangeMin: 80, + EtherType: "IPv4", + PortRangeMax: 80, + Protocol: "tcp", + RemoteGroupID: "85cc3048-abc3-43cc-89b3-377341426ac5", + SecGroupID: "a7734e61-b545-452d-a3cd-0189cbd9747a", + } + _, err := Create(fake.ServiceClient(), opts).Extract() + th.AssertNoErr(t, err) +} + +func TestRequiredCreateOpts(t *testing.T) { + res := Create(fake.ServiceClient(), CreateOpts{Direction: "something"}) + if res.Err == nil { + t.Fatalf("Expected error, got none") + } + res = Create(fake.ServiceClient(), CreateOpts{Direction: DirIngress, EtherType: "something"}) + if res.Err == nil { + t.Fatalf("Expected error, got none") + } + res = Create(fake.ServiceClient(), CreateOpts{Direction: DirIngress, EtherType: Ether4}) + if res.Err == nil { + t.Fatalf("Expected error, got none") + } + res = Create(fake.ServiceClient(), CreateOpts{Direction: DirIngress, EtherType: Ether4, SecGroupID: "something", Protocol: "foo"}) + if res.Err == nil { + t.Fatalf("Expected error, got none") + } +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/security-group-rules/3c0e45ff-adaf-4124-b083-bf390e5482ff", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "security_group_rule": { + "direction": "egress", + "ethertype": "IPv6", + "id": "3c0e45ff-adaf-4124-b083-bf390e5482ff", + "port_range_max": null, + "port_range_min": null, + "protocol": null, + "remote_group_id": null, + "remote_ip_prefix": null, + "security_group_id": "85cc3048-abc3-43cc-89b3-377341426ac5", + "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550" + } +} + `) + }) + + sr, err := Get(fake.ServiceClient(), "3c0e45ff-adaf-4124-b083-bf390e5482ff").Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, "egress", sr.Direction) + th.AssertEquals(t, "IPv6", sr.EtherType) + th.AssertEquals(t, "3c0e45ff-adaf-4124-b083-bf390e5482ff", sr.ID) + th.AssertEquals(t, 0, sr.PortRangeMax) + th.AssertEquals(t, 0, sr.PortRangeMin) + th.AssertEquals(t, "", sr.Protocol) + th.AssertEquals(t, "", sr.RemoteGroupID) + th.AssertEquals(t, "", sr.RemoteIPPrefix) + th.AssertEquals(t, "85cc3048-abc3-43cc-89b3-377341426ac5", sr.SecGroupID) + th.AssertEquals(t, "e4f50856753b4dc6afee5fa6b9b6c550", sr.TenantID) +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/security-group-rules/4ec89087-d057-4e2c-911f-60a3b47ee304", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusNoContent) + }) + + res := Delete(fake.ServiceClient(), "4ec89087-d057-4e2c-911f-60a3b47ee304") + th.AssertNoErr(t, res.Err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/rules/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/rules/results.go new file mode 100644 index 0000000000..6e13857689 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/rules/results.go @@ -0,0 +1,133 @@ +package rules + +import ( + "github.com/mitchellh/mapstructure" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// SecGroupRule represents a rule to dictate the behaviour of incoming or +// outgoing traffic for a particular security group. +type SecGroupRule struct { + // The UUID for this security group rule. + ID string + + // The direction in which the security group rule is applied. The only values + // allowed are "ingress" or "egress". For a compute instance, an ingress + // security group rule is applied to incoming (ingress) traffic for that + // instance. An egress rule is applied to traffic leaving the instance. + Direction string + + // Must be IPv4 or IPv6, and addresses represented in CIDR must match the + // ingress or egress rules. + EtherType string `json:"ethertype" mapstructure:"ethertype"` + + // The security group ID to associate with this security group rule. + SecGroupID string `json:"security_group_id" mapstructure:"security_group_id"` + + // The minimum port number in the range that is matched by the security group + // rule. If the protocol is TCP or UDP, this value must be less than or equal + // to the value of the PortRangeMax attribute. If the protocol is ICMP, this + // value must be an ICMP type. + PortRangeMin int `json:"port_range_min" mapstructure:"port_range_min"` + + // The maximum port number in the range that is matched by the security group + // rule. The PortRangeMin attribute constrains the PortRangeMax attribute. If + // the protocol is ICMP, this value must be an ICMP type. + PortRangeMax int `json:"port_range_max" mapstructure:"port_range_max"` + + // The protocol that is matched by the security group rule. Valid values are + // "tcp", "udp", "icmp" or an empty string. + Protocol string + + // The remote group ID to be associated with this security group rule. You + // can specify either RemoteGroupID or RemoteIPPrefix. + RemoteGroupID string `json:"remote_group_id" mapstructure:"remote_group_id"` + + // The remote IP prefix to be associated with this security group rule. You + // can specify either RemoteGroupID or RemoteIPPrefix . This attribute + // matches the specified IP prefix as the source IP address of the IP packet. + RemoteIPPrefix string `json:"remote_ip_prefix" mapstructure:"remote_ip_prefix"` + + // The owner of this security group rule. + TenantID string `json:"tenant_id" mapstructure:"tenant_id"` +} + +// SecGroupRulePage is the page returned by a pager when traversing over a +// collection of security group rules. +type SecGroupRulePage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of security group rules has +// reached the end of a page and the pager seeks to traverse over a new one. In +// order to do this, it needs to construct the next page's URL. +func (p SecGroupRulePage) NextPageURL() (string, error) { + type resp struct { + Links []gophercloud.Link `mapstructure:"security_group_rules_links"` + } + + var r resp + err := mapstructure.Decode(p.Body, &r) + if err != nil { + return "", err + } + + return gophercloud.ExtractNextURL(r.Links) +} + +// IsEmpty checks whether a SecGroupRulePage struct is empty. +func (p SecGroupRulePage) IsEmpty() (bool, error) { + is, err := ExtractRules(p) + if err != nil { + return true, nil + } + return len(is) == 0, nil +} + +// ExtractRules accepts a Page struct, specifically a SecGroupRulePage struct, +// and extracts the elements into a slice of SecGroupRule structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractRules(page pagination.Page) ([]SecGroupRule, error) { + var resp struct { + SecGroupRules []SecGroupRule `mapstructure:"security_group_rules" json:"security_group_rules"` + } + + err := mapstructure.Decode(page.(SecGroupRulePage).Body, &resp) + + return resp.SecGroupRules, err +} + +type commonResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts a security rule. +func (r commonResult) Extract() (*SecGroupRule, error) { + if r.Err != nil { + return nil, r.Err + } + + var res struct { + SecGroupRule *SecGroupRule `mapstructure:"security_group_rule" json:"security_group_rule"` + } + + err := mapstructure.Decode(r.Body, &res) + + return res.SecGroupRule, err +} + +// CreateResult represents the result of a create operation. +type CreateResult struct { + commonResult +} + +// GetResult represents the result of a get operation. +type GetResult struct { + commonResult +} + +// DeleteResult represents the result of a delete operation. +type DeleteResult struct { + gophercloud.ErrResult +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/rules/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/rules/urls.go new file mode 100644 index 0000000000..8e2b2bb28d --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/rules/urls.go @@ -0,0 +1,13 @@ +package rules + +import "github.com/rackspace/gophercloud" + +const rootPath = "security-group-rules" + +func rootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(rootPath) +} + +func resourceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(rootPath, id) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/networks/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/networks/doc.go new file mode 100644 index 0000000000..c87a7ce270 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/networks/doc.go @@ -0,0 +1,9 @@ +// Package networks contains functionality for working with Neutron network +// resources. A network is an isolated virtual layer-2 broadcast domain that is +// typically reserved for the tenant who created it (unless you configure the +// network to be shared). Tenants can create multiple networks until the +// thresholds per-tenant quota is reached. +// +// In the v2.0 Networking API, the network is the main entity. Ports and subnets +// are always associated with a network. +package networks diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/networks/errors.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/networks/errors.go new file mode 100644 index 0000000000..83c4a6a868 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/networks/errors.go @@ -0,0 +1 @@ +package networks diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/networks/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/networks/requests.go new file mode 100644 index 0000000000..dedbb252de --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/networks/requests.go @@ -0,0 +1,209 @@ +package networks + +import ( + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" + + "github.com/racker/perigee" +) + +// AdminState gives users a solid type to work with for create and update +// operations. It is recommended that users use the `Up` and `Down` enums. +type AdminState *bool + +// Convenience vars for AdminStateUp values. +var ( + iTrue = true + iFalse = false + + Up AdminState = &iTrue + Down AdminState = &iFalse +) + +type networkOpts struct { + AdminStateUp *bool + Name string + Shared *bool + TenantID string +} + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToNetworkListQuery() (string, error) +} + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the network attributes you want to see returned. SortKey allows you to sort +// by a particular network attribute. SortDir sets the direction, and is either +// `asc' or `desc'. Marker and Limit are used for pagination. +type ListOpts struct { + Status string `q:"status"` + Name string `q:"name"` + AdminStateUp *bool `q:"admin_state_up"` + TenantID string `q:"tenant_id"` + Shared *bool `q:"shared"` + ID string `q:"id"` + Marker string `q:"marker"` + Limit int `q:"limit"` + SortKey string `q:"sort_key"` + SortDir string `q:"sort_dir"` +} + +// ToNetworkListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToNetworkListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + if err != nil { + return "", err + } + return q.String(), nil +} + +// List returns a Pager which allows you to iterate over a collection of +// networks. It accepts a ListOpts struct, which allows you to filter and sort +// the returned collection for greater efficiency. +func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(c) + if opts != nil { + query, err := opts.ToNetworkListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return NetworkPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Get retrieves a specific network based on its unique ID. +func Get(c *gophercloud.ServiceClient, id string) GetResult { + var res GetResult + _, res.Err = perigee.Request("GET", getURL(c, id), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + Results: &res.Body, + OkCodes: []int{200}, + }) + return res +} + +// CreateOptsBuilder is the interface options structs have to satisfy in order +// to be used in the main Create operation in this package. Since many +// extensions decorate or modify the common logic, it is useful for them to +// satisfy a basic interface in order for them to be used. +type CreateOptsBuilder interface { + ToNetworkCreateMap() (map[string]interface{}, error) +} + +// CreateOpts is the common options struct used in this package's Create +// operation. +type CreateOpts networkOpts + +// ToNetworkCreateMap casts a CreateOpts struct to a map. +func (opts CreateOpts) ToNetworkCreateMap() (map[string]interface{}, error) { + n := make(map[string]interface{}) + + if opts.AdminStateUp != nil { + n["admin_state_up"] = &opts.AdminStateUp + } + if opts.Name != "" { + n["name"] = opts.Name + } + if opts.Shared != nil { + n["shared"] = &opts.Shared + } + if opts.TenantID != "" { + n["tenant_id"] = opts.TenantID + } + + return map[string]interface{}{"network": n}, nil +} + +// Create accepts a CreateOpts struct and creates a new network using the values +// provided. This operation does not actually require a request body, i.e. the +// CreateOpts struct argument can be empty. +// +// The tenant ID that is contained in the URI is the tenant that creates the +// network. An admin user, however, has the option of specifying another tenant +// ID in the CreateOpts struct. +func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult { + var res CreateResult + + reqBody, err := opts.ToNetworkCreateMap() + if err != nil { + res.Err = err + return res + } + + // Send request to API + _, res.Err = perigee.Request("POST", createURL(c), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + ReqBody: &reqBody, + Results: &res.Body, + OkCodes: []int{201}, + }) + return res +} + +// UpdateOptsBuilder is the interface options structs have to satisfy in order +// to be used in the main Update operation in this package. Since many +// extensions decorate or modify the common logic, it is useful for them to +// satisfy a basic interface in order for them to be used. +type UpdateOptsBuilder interface { + ToNetworkUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts is the common options struct used in this package's Update +// operation. +type UpdateOpts networkOpts + +// ToNetworkUpdateMap casts a UpdateOpts struct to a map. +func (opts UpdateOpts) ToNetworkUpdateMap() (map[string]interface{}, error) { + n := make(map[string]interface{}) + + if opts.AdminStateUp != nil { + n["admin_state_up"] = &opts.AdminStateUp + } + if opts.Name != "" { + n["name"] = opts.Name + } + if opts.Shared != nil { + n["shared"] = &opts.Shared + } + + return map[string]interface{}{"network": n}, nil +} + +// Update accepts a UpdateOpts struct and updates an existing network using the +// values provided. For more information, see the Create function. +func Update(c *gophercloud.ServiceClient, networkID string, opts UpdateOptsBuilder) UpdateResult { + var res UpdateResult + + reqBody, err := opts.ToNetworkUpdateMap() + if err != nil { + res.Err = err + return res + } + + // Send request to API + _, res.Err = perigee.Request("PUT", updateURL(c, networkID), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + ReqBody: &reqBody, + Results: &res.Body, + OkCodes: []int{200, 201}, + }) + + return res +} + +// Delete accepts a unique ID and deletes the network associated with it. +func Delete(c *gophercloud.ServiceClient, networkID string) DeleteResult { + var res DeleteResult + _, res.Err = perigee.Request("DELETE", deleteURL(c, networkID), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + OkCodes: []int{204}, + }) + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/networks/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/networks/requests_test.go new file mode 100644 index 0000000000..a263b7b16b --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/networks/requests_test.go @@ -0,0 +1,275 @@ +package networks + +import ( + "fmt" + "net/http" + "testing" + + fake "github.com/rackspace/gophercloud/openstack/networking/v2/common" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/networks", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "networks": [ + { + "status": "ACTIVE", + "subnets": [ + "54d6f61d-db07-451c-9ab3-b9609b6b6f0b" + ], + "name": "private-network", + "admin_state_up": true, + "tenant_id": "4fd44f30292945e481c7b8a0c8908869", + "shared": true, + "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22" + }, + { + "status": "ACTIVE", + "subnets": [ + "08eae331-0402-425a-923c-34f7cfe39c1b" + ], + "name": "private", + "admin_state_up": true, + "tenant_id": "26a7980765d0414dbc1fc1f88cdb7e6e", + "shared": true, + "id": "db193ab3-96e3-4cb3-8fc5-05f4296d0324" + } + ] +} + `) + }) + + client := fake.ServiceClient() + count := 0 + + List(client, ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractNetworks(page) + if err != nil { + t.Errorf("Failed to extract networks: %v", err) + return false, err + } + + expected := []Network{ + Network{ + Status: "ACTIVE", + Subnets: []string{"54d6f61d-db07-451c-9ab3-b9609b6b6f0b"}, + Name: "private-network", + AdminStateUp: true, + TenantID: "4fd44f30292945e481c7b8a0c8908869", + Shared: true, + ID: "d32019d3-bc6e-4319-9c1d-6722fc136a22", + }, + Network{ + Status: "ACTIVE", + Subnets: []string{"08eae331-0402-425a-923c-34f7cfe39c1b"}, + Name: "private", + AdminStateUp: true, + TenantID: "26a7980765d0414dbc1fc1f88cdb7e6e", + Shared: true, + ID: "db193ab3-96e3-4cb3-8fc5-05f4296d0324", + }, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/networks/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "network": { + "status": "ACTIVE", + "subnets": [ + "54d6f61d-db07-451c-9ab3-b9609b6b6f0b" + ], + "name": "private-network", + "admin_state_up": true, + "tenant_id": "4fd44f30292945e481c7b8a0c8908869", + "shared": true, + "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22" + } +} + `) + }) + + n, err := Get(fake.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22").Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, n.Status, "ACTIVE") + th.AssertDeepEquals(t, n.Subnets, []string{"54d6f61d-db07-451c-9ab3-b9609b6b6f0b"}) + th.AssertEquals(t, n.Name, "private-network") + th.AssertEquals(t, n.AdminStateUp, true) + th.AssertEquals(t, n.TenantID, "4fd44f30292945e481c7b8a0c8908869") + th.AssertEquals(t, n.Shared, true) + th.AssertEquals(t, n.ID, "d32019d3-bc6e-4319-9c1d-6722fc136a22") +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/networks", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "network": { + "name": "sample_network", + "admin_state_up": true + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, ` +{ + "network": { + "status": "ACTIVE", + "subnets": [], + "name": "net1", + "admin_state_up": true, + "tenant_id": "9bacb3c5d39d41a79512987f338cf177", + "shared": false, + "id": "4e8e5957-649f-477b-9e5b-f1f75b21c03c" + } +} + `) + }) + + iTrue := true + options := CreateOpts{Name: "sample_network", AdminStateUp: &iTrue} + n, err := Create(fake.ServiceClient(), options).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, n.Status, "ACTIVE") + th.AssertDeepEquals(t, n.Subnets, []string{}) + th.AssertEquals(t, n.Name, "net1") + th.AssertEquals(t, n.AdminStateUp, true) + th.AssertEquals(t, n.TenantID, "9bacb3c5d39d41a79512987f338cf177") + th.AssertEquals(t, n.Shared, false) + th.AssertEquals(t, n.ID, "4e8e5957-649f-477b-9e5b-f1f75b21c03c") +} + +func TestCreateWithOptionalFields(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/networks", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "network": { + "name": "sample_network", + "admin_state_up": true, + "shared": true, + "tenant_id": "12345" + } +} + `) + + w.WriteHeader(http.StatusCreated) + }) + + iTrue := true + options := CreateOpts{Name: "sample_network", AdminStateUp: &iTrue, Shared: &iTrue, TenantID: "12345"} + _, err := Create(fake.ServiceClient(), options).Extract() + th.AssertNoErr(t, err) +} + +func TestUpdate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/networks/4e8e5957-649f-477b-9e5b-f1f75b21c03c", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "network": { + "name": "new_network_name", + "admin_state_up": false, + "shared": true + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "network": { + "status": "ACTIVE", + "subnets": [], + "name": "new_network_name", + "admin_state_up": false, + "tenant_id": "4fd44f30292945e481c7b8a0c8908869", + "shared": true, + "id": "4e8e5957-649f-477b-9e5b-f1f75b21c03c" + } +} + `) + }) + + iTrue, iFalse := true, false + options := UpdateOpts{Name: "new_network_name", AdminStateUp: &iFalse, Shared: &iTrue} + n, err := Update(fake.ServiceClient(), "4e8e5957-649f-477b-9e5b-f1f75b21c03c", options).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, n.Name, "new_network_name") + th.AssertEquals(t, n.AdminStateUp, false) + th.AssertEquals(t, n.Shared, true) + th.AssertEquals(t, n.ID, "4e8e5957-649f-477b-9e5b-f1f75b21c03c") +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/networks/4e8e5957-649f-477b-9e5b-f1f75b21c03c", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusNoContent) + }) + + res := Delete(fake.ServiceClient(), "4e8e5957-649f-477b-9e5b-f1f75b21c03c") + th.AssertNoErr(t, res.Err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/networks/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/networks/results.go new file mode 100644 index 0000000000..3ecedde9ac --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/networks/results.go @@ -0,0 +1,116 @@ +package networks + +import ( + "github.com/mitchellh/mapstructure" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +type commonResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts a network resource. +func (r commonResult) Extract() (*Network, error) { + if r.Err != nil { + return nil, r.Err + } + + var res struct { + Network *Network `json:"network"` + } + + err := mapstructure.Decode(r.Body, &res) + + return res.Network, err +} + +// CreateResult represents the result of a create operation. +type CreateResult struct { + commonResult +} + +// GetResult represents the result of a get operation. +type GetResult struct { + commonResult +} + +// UpdateResult represents the result of an update operation. +type UpdateResult struct { + commonResult +} + +// DeleteResult represents the result of a delete operation. +type DeleteResult struct { + gophercloud.ErrResult +} + +// Network represents, well, a network. +type Network struct { + // UUID for the network + ID string `mapstructure:"id" json:"id"` + + // Human-readable name for the network. Might not be unique. + Name string `mapstructure:"name" json:"name"` + + // The administrative state of network. If false (down), the network does not forward packets. + AdminStateUp bool `mapstructure:"admin_state_up" json:"admin_state_up"` + + // Indicates whether network is currently operational. Possible values include + // `ACTIVE', `DOWN', `BUILD', or `ERROR'. Plug-ins might define additional values. + Status string `mapstructure:"status" json:"status"` + + // Subnets associated with this network. + Subnets []string `mapstructure:"subnets" json:"subnets"` + + // Owner of network. Only admin users can specify a tenant_id other than its own. + TenantID string `mapstructure:"tenant_id" json:"tenant_id"` + + // Specifies whether the network resource can be accessed by any tenant or not. + Shared bool `mapstructure:"shared" json:"shared"` +} + +// NetworkPage is the page returned by a pager when traversing over a +// collection of networks. +type NetworkPage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of networks has reached +// the end of a page and the pager seeks to traverse over a new one. In order +// to do this, it needs to construct the next page's URL. +func (p NetworkPage) NextPageURL() (string, error) { + type resp struct { + Links []gophercloud.Link `mapstructure:"networks_links"` + } + + var r resp + err := mapstructure.Decode(p.Body, &r) + if err != nil { + return "", err + } + + return gophercloud.ExtractNextURL(r.Links) +} + +// IsEmpty checks whether a NetworkPage struct is empty. +func (p NetworkPage) IsEmpty() (bool, error) { + is, err := ExtractNetworks(p) + if err != nil { + return true, nil + } + return len(is) == 0, nil +} + +// ExtractNetworks accepts a Page struct, specifically a NetworkPage struct, +// and extracts the elements into a slice of Network structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractNetworks(page pagination.Page) ([]Network, error) { + var resp struct { + Networks []Network `mapstructure:"networks" json:"networks"` + } + + err := mapstructure.Decode(page.(NetworkPage).Body, &resp) + + return resp.Networks, err +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/networks/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/networks/urls.go new file mode 100644 index 0000000000..a9eecc5295 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/networks/urls.go @@ -0,0 +1,31 @@ +package networks + +import "github.com/rackspace/gophercloud" + +func resourceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL("networks", id) +} + +func rootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("networks") +} + +func getURL(c *gophercloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func listURL(c *gophercloud.ServiceClient) string { + return rootURL(c) +} + +func createURL(c *gophercloud.ServiceClient) string { + return rootURL(c) +} + +func updateURL(c *gophercloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func deleteURL(c *gophercloud.ServiceClient, id string) string { + return resourceURL(c, id) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/networks/urls_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/networks/urls_test.go new file mode 100644 index 0000000000..caf77dbe04 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/networks/urls_test.go @@ -0,0 +1,38 @@ +package networks + +import ( + "testing" + + "github.com/rackspace/gophercloud" + th "github.com/rackspace/gophercloud/testhelper" +) + +const endpoint = "http://localhost:57909/" + +func endpointClient() *gophercloud.ServiceClient { + return &gophercloud.ServiceClient{Endpoint: endpoint, ResourceBase: endpoint + "v2.0/"} +} + +func TestGetURL(t *testing.T) { + actual := getURL(endpointClient(), "foo") + expected := endpoint + "v2.0/networks/foo" + th.AssertEquals(t, expected, actual) +} + +func TestCreateURL(t *testing.T) { + actual := createURL(endpointClient()) + expected := endpoint + "v2.0/networks" + th.AssertEquals(t, expected, actual) +} + +func TestListURL(t *testing.T) { + actual := createURL(endpointClient()) + expected := endpoint + "v2.0/networks" + th.AssertEquals(t, expected, actual) +} + +func TestDeleteURL(t *testing.T) { + actual := deleteURL(endpointClient(), "foo") + expected := endpoint + "v2.0/networks/foo" + th.AssertEquals(t, expected, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/ports/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/ports/doc.go new file mode 100644 index 0000000000..f16a4bb01b --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/ports/doc.go @@ -0,0 +1,8 @@ +// Package ports contains functionality for working with Neutron port resources. +// A port represents a virtual switch port on a logical network switch. Virtual +// instances attach their interfaces into ports. The logical port also defines +// the MAC address and the IP address(es) to be assigned to the interfaces +// plugged into them. When IP addresses are associated to a port, this also +// implies the port is associated with a subnet, as the IP address was taken +// from the allocation pool for a specific subnet. +package ports diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/ports/errors.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/ports/errors.go new file mode 100644 index 0000000000..111d977e74 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/ports/errors.go @@ -0,0 +1,11 @@ +package ports + +import "fmt" + +func err(str string) error { + return fmt.Errorf("%s", str) +} + +var ( + errNetworkIDRequired = err("A Network ID is required") +) diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/ports/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/ports/requests.go new file mode 100644 index 0000000000..06d273ef19 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/ports/requests.go @@ -0,0 +1,244 @@ +package ports + +import ( + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" + + "github.com/racker/perigee" +) + +// AdminState gives users a solid type to work with for create and update +// operations. It is recommended that users use the `Up` and `Down` enums. +type AdminState *bool + +// Convenience vars for AdminStateUp values. +var ( + iTrue = true + iFalse = false + + Up AdminState = &iTrue + Down AdminState = &iFalse +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToPortListQuery() (string, error) +} + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the port attributes you want to see returned. SortKey allows you to sort +// by a particular port attribute. SortDir sets the direction, and is either +// `asc' or `desc'. Marker and Limit are used for pagination. +type ListOpts struct { + Status string `q:"status"` + Name string `q:"name"` + AdminStateUp *bool `q:"admin_state_up"` + NetworkID string `q:"network_id"` + TenantID string `q:"tenant_id"` + DeviceOwner string `q:"device_owner"` + MACAddress string `q:"mac_address"` + ID string `q:"id"` + DeviceID string `q:"device_id"` + Limit int `q:"limit"` + Marker string `q:"marker"` + SortKey string `q:"sort_key"` + SortDir string `q:"sort_dir"` +} + +// ToPortListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToPortListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + if err != nil { + return "", err + } + return q.String(), nil +} + +// List returns a Pager which allows you to iterate over a collection of +// ports. It accepts a ListOpts struct, which allows you to filter and sort +// the returned collection for greater efficiency. +// +// Default policy settings return only those ports that are owned by the tenant +// who submits the request, unless the request is submitted by a user with +// administrative rights. +func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(c) + if opts != nil { + query, err := opts.ToPortListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return PortPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Get retrieves a specific port based on its unique ID. +func Get(c *gophercloud.ServiceClient, id string) GetResult { + var res GetResult + _, res.Err = perigee.Request("GET", getURL(c, id), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + Results: &res.Body, + OkCodes: []int{200}, + }) + return res +} + +// CreateOptsBuilder is the interface options structs have to satisfy in order +// to be used in the main Create operation in this package. Since many +// extensions decorate or modify the common logic, it is useful for them to +// satisfy a basic interface in order for them to be used. +type CreateOptsBuilder interface { + ToPortCreateMap() (map[string]interface{}, error) +} + +// CreateOpts represents the attributes used when creating a new port. +type CreateOpts struct { + NetworkID string + Name string + AdminStateUp *bool + MACAddress string + FixedIPs interface{} + DeviceID string + DeviceOwner string + TenantID string + SecurityGroups []string +} + +// ToPortCreateMap casts a CreateOpts struct to a map. +func (opts CreateOpts) ToPortCreateMap() (map[string]interface{}, error) { + p := make(map[string]interface{}) + + if opts.NetworkID == "" { + return nil, errNetworkIDRequired + } + p["network_id"] = opts.NetworkID + + if opts.DeviceID != "" { + p["device_id"] = opts.DeviceID + } + if opts.DeviceOwner != "" { + p["device_owner"] = opts.DeviceOwner + } + if opts.FixedIPs != nil { + p["fixed_ips"] = opts.FixedIPs + } + if opts.SecurityGroups != nil { + p["security_groups"] = opts.SecurityGroups + } + if opts.TenantID != "" { + p["tenant_id"] = opts.TenantID + } + if opts.AdminStateUp != nil { + p["admin_state_up"] = &opts.AdminStateUp + } + if opts.Name != "" { + p["name"] = opts.Name + } + if opts.MACAddress != "" { + p["mac_address"] = opts.MACAddress + } + + return map[string]interface{}{"port": p}, nil +} + +// Create accepts a CreateOpts struct and creates a new network using the values +// provided. You must remember to provide a NetworkID value. +func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult { + var res CreateResult + + reqBody, err := opts.ToPortCreateMap() + if err != nil { + res.Err = err + return res + } + + // Response + _, res.Err = perigee.Request("POST", createURL(c), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + ReqBody: &reqBody, + Results: &res.Body, + OkCodes: []int{201}, + }) + + return res +} + +// UpdateOptsBuilder is the interface options structs have to satisfy in order +// to be used in the main Update operation in this package. Since many +// extensions decorate or modify the common logic, it is useful for them to +// satisfy a basic interface in order for them to be used. +type UpdateOptsBuilder interface { + ToPortUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts represents the attributes used when updating an existing port. +type UpdateOpts struct { + Name string + AdminStateUp *bool + FixedIPs interface{} + DeviceID string + DeviceOwner string + SecurityGroups []string +} + +// ToPortUpdateMap casts an UpdateOpts struct to a map. +func (opts UpdateOpts) ToPortUpdateMap() (map[string]interface{}, error) { + p := make(map[string]interface{}) + + if opts.DeviceID != "" { + p["device_id"] = opts.DeviceID + } + if opts.DeviceOwner != "" { + p["device_owner"] = opts.DeviceOwner + } + if opts.FixedIPs != nil { + p["fixed_ips"] = opts.FixedIPs + } + if opts.SecurityGroups != nil { + p["security_groups"] = opts.SecurityGroups + } + if opts.AdminStateUp != nil { + p["admin_state_up"] = &opts.AdminStateUp + } + if opts.Name != "" { + p["name"] = opts.Name + } + + return map[string]interface{}{"port": p}, nil +} + +// Update accepts a UpdateOpts struct and updates an existing port using the +// values provided. +func Update(c *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) UpdateResult { + var res UpdateResult + + reqBody, err := opts.ToPortUpdateMap() + if err != nil { + res.Err = err + return res + } + + _, res.Err = perigee.Request("PUT", updateURL(c, id), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + ReqBody: &reqBody, + Results: &res.Body, + OkCodes: []int{200, 201}, + }) + return res +} + +// Delete accepts a unique ID and deletes the port associated with it. +func Delete(c *gophercloud.ServiceClient, id string) DeleteResult { + var res DeleteResult + _, res.Err = perigee.Request("DELETE", deleteURL(c, id), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + OkCodes: []int{204}, + }) + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/ports/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/ports/requests_test.go new file mode 100644 index 0000000000..9e323efa3a --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/ports/requests_test.go @@ -0,0 +1,321 @@ +package ports + +import ( + "fmt" + "net/http" + "testing" + + fake "github.com/rackspace/gophercloud/openstack/networking/v2/common" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/ports", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "ports": [ + { + "status": "ACTIVE", + "binding:host_id": "devstack", + "name": "", + "admin_state_up": true, + "network_id": "70c1db1f-b701-45bd-96e0-a313ee3430b3", + "tenant_id": "", + "device_owner": "network:router_gateway", + "mac_address": "fa:16:3e:58:42:ed", + "fixed_ips": [ + { + "subnet_id": "008ba151-0b8c-4a67-98b5-0d2b87666062", + "ip_address": "172.24.4.2" + } + ], + "id": "d80b1a3b-4fc1-49f3-952e-1e2ab7081d8b", + "security_groups": [], + "device_id": "9ae135f4-b6e0-4dad-9e91-3c223e385824" + } + ] +} + `) + }) + + count := 0 + + List(fake.ServiceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractPorts(page) + if err != nil { + t.Errorf("Failed to extract subnets: %v", err) + return false, nil + } + + expected := []Port{ + Port{ + Status: "ACTIVE", + Name: "", + AdminStateUp: true, + NetworkID: "70c1db1f-b701-45bd-96e0-a313ee3430b3", + TenantID: "", + DeviceOwner: "network:router_gateway", + MACAddress: "fa:16:3e:58:42:ed", + FixedIPs: []IP{ + IP{ + SubnetID: "008ba151-0b8c-4a67-98b5-0d2b87666062", + IPAddress: "172.24.4.2", + }, + }, + ID: "d80b1a3b-4fc1-49f3-952e-1e2ab7081d8b", + SecurityGroups: []string{}, + DeviceID: "9ae135f4-b6e0-4dad-9e91-3c223e385824", + }, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/ports/46d4bfb9-b26e-41f3-bd2e-e6dcc1ccedb2", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "port": { + "status": "ACTIVE", + "name": "", + "admin_state_up": true, + "network_id": "a87cc70a-3e15-4acf-8205-9b711a3531b7", + "tenant_id": "7e02058126cc4950b75f9970368ba177", + "device_owner": "network:router_interface", + "mac_address": "fa:16:3e:23:fd:d7", + "fixed_ips": [ + { + "subnet_id": "a0304c3a-4f08-4c43-88af-d796509c97d2", + "ip_address": "10.0.0.1" + } + ], + "id": "46d4bfb9-b26e-41f3-bd2e-e6dcc1ccedb2", + "security_groups": [], + "device_id": "5e3898d7-11be-483e-9732-b2f5eccd2b2e" + } +} + `) + }) + + n, err := Get(fake.ServiceClient(), "46d4bfb9-b26e-41f3-bd2e-e6dcc1ccedb2").Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, n.Status, "ACTIVE") + th.AssertEquals(t, n.Name, "") + th.AssertEquals(t, n.AdminStateUp, true) + th.AssertEquals(t, n.NetworkID, "a87cc70a-3e15-4acf-8205-9b711a3531b7") + th.AssertEquals(t, n.TenantID, "7e02058126cc4950b75f9970368ba177") + th.AssertEquals(t, n.DeviceOwner, "network:router_interface") + th.AssertEquals(t, n.MACAddress, "fa:16:3e:23:fd:d7") + th.AssertDeepEquals(t, n.FixedIPs, []IP{ + IP{SubnetID: "a0304c3a-4f08-4c43-88af-d796509c97d2", IPAddress: "10.0.0.1"}, + }) + th.AssertEquals(t, n.ID, "46d4bfb9-b26e-41f3-bd2e-e6dcc1ccedb2") + th.AssertDeepEquals(t, n.SecurityGroups, []string{}) + th.AssertEquals(t, n.Status, "ACTIVE") + th.AssertEquals(t, n.DeviceID, "5e3898d7-11be-483e-9732-b2f5eccd2b2e") +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/ports", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "port": { + "network_id": "a87cc70a-3e15-4acf-8205-9b711a3531b7", + "name": "private-port", + "admin_state_up": true, + "fixed_ips": [ + { + "subnet_id": "a0304c3a-4f08-4c43-88af-d796509c97d2", + "ip_address": "10.0.0.2" + } + ], + "security_groups": ["foo"] + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, ` +{ + "port": { + "status": "DOWN", + "name": "private-port", + "allowed_address_pairs": [], + "admin_state_up": true, + "network_id": "a87cc70a-3e15-4acf-8205-9b711a3531b7", + "tenant_id": "d6700c0c9ffa4f1cb322cd4a1f3906fa", + "device_owner": "", + "mac_address": "fa:16:3e:c9:cb:f0", + "fixed_ips": [ + { + "subnet_id": "a0304c3a-4f08-4c43-88af-d796509c97d2", + "ip_address": "10.0.0.2" + } + ], + "id": "65c0ee9f-d634-4522-8954-51021b570b0d", + "security_groups": [ + "f0ac4394-7e4a-4409-9701-ba8be283dbc3" + ], + "device_id": "" + } +} + `) + }) + + asu := true + options := CreateOpts{ + Name: "private-port", + AdminStateUp: &asu, + NetworkID: "a87cc70a-3e15-4acf-8205-9b711a3531b7", + FixedIPs: []IP{ + IP{SubnetID: "a0304c3a-4f08-4c43-88af-d796509c97d2", IPAddress: "10.0.0.2"}, + }, + SecurityGroups: []string{"foo"}, + } + n, err := Create(fake.ServiceClient(), options).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, n.Status, "DOWN") + th.AssertEquals(t, n.Name, "private-port") + th.AssertEquals(t, n.AdminStateUp, true) + th.AssertEquals(t, n.NetworkID, "a87cc70a-3e15-4acf-8205-9b711a3531b7") + th.AssertEquals(t, n.TenantID, "d6700c0c9ffa4f1cb322cd4a1f3906fa") + th.AssertEquals(t, n.DeviceOwner, "") + th.AssertEquals(t, n.MACAddress, "fa:16:3e:c9:cb:f0") + th.AssertDeepEquals(t, n.FixedIPs, []IP{ + IP{SubnetID: "a0304c3a-4f08-4c43-88af-d796509c97d2", IPAddress: "10.0.0.2"}, + }) + th.AssertEquals(t, n.ID, "65c0ee9f-d634-4522-8954-51021b570b0d") + th.AssertDeepEquals(t, n.SecurityGroups, []string{"f0ac4394-7e4a-4409-9701-ba8be283dbc3"}) +} + +func TestRequiredCreateOpts(t *testing.T) { + res := Create(fake.ServiceClient(), CreateOpts{}) + if res.Err == nil { + t.Fatalf("Expected error, got none") + } +} + +func TestUpdate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/ports/65c0ee9f-d634-4522-8954-51021b570b0d", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "port": { + "name": "new_port_name", + "fixed_ips": [ + { + "subnet_id": "a0304c3a-4f08-4c43-88af-d796509c97d2", + "ip_address": "10.0.0.3" + } + ], + "security_groups": [ + "f0ac4394-7e4a-4409-9701-ba8be283dbc3" + ] + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "port": { + "status": "DOWN", + "name": "new_port_name", + "admin_state_up": true, + "network_id": "a87cc70a-3e15-4acf-8205-9b711a3531b7", + "tenant_id": "d6700c0c9ffa4f1cb322cd4a1f3906fa", + "device_owner": "", + "mac_address": "fa:16:3e:c9:cb:f0", + "fixed_ips": [ + { + "subnet_id": "a0304c3a-4f08-4c43-88af-d796509c97d2", + "ip_address": "10.0.0.3" + } + ], + "id": "65c0ee9f-d634-4522-8954-51021b570b0d", + "security_groups": [ + "f0ac4394-7e4a-4409-9701-ba8be283dbc3" + ], + "device_id": "" + } +} + `) + }) + + options := UpdateOpts{ + Name: "new_port_name", + FixedIPs: []IP{ + IP{SubnetID: "a0304c3a-4f08-4c43-88af-d796509c97d2", IPAddress: "10.0.0.3"}, + }, + SecurityGroups: []string{"f0ac4394-7e4a-4409-9701-ba8be283dbc3"}, + } + + s, err := Update(fake.ServiceClient(), "65c0ee9f-d634-4522-8954-51021b570b0d", options).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, s.Name, "new_port_name") + th.AssertDeepEquals(t, s.FixedIPs, []IP{ + IP{SubnetID: "a0304c3a-4f08-4c43-88af-d796509c97d2", IPAddress: "10.0.0.3"}, + }) + th.AssertDeepEquals(t, s.SecurityGroups, []string{"f0ac4394-7e4a-4409-9701-ba8be283dbc3"}) +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/ports/65c0ee9f-d634-4522-8954-51021b570b0d", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusNoContent) + }) + + res := Delete(fake.ServiceClient(), "65c0ee9f-d634-4522-8954-51021b570b0d") + th.AssertNoErr(t, res.Err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/ports/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/ports/results.go new file mode 100644 index 0000000000..2511ff53b2 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/ports/results.go @@ -0,0 +1,126 @@ +package ports + +import ( + "github.com/mitchellh/mapstructure" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +type commonResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts a port resource. +func (r commonResult) Extract() (*Port, error) { + if r.Err != nil { + return nil, r.Err + } + + var res struct { + Port *Port `json:"port"` + } + + err := mapstructure.Decode(r.Body, &res) + + return res.Port, err +} + +// CreateResult represents the result of a create operation. +type CreateResult struct { + commonResult +} + +// GetResult represents the result of a get operation. +type GetResult struct { + commonResult +} + +// UpdateResult represents the result of an update operation. +type UpdateResult struct { + commonResult +} + +// DeleteResult represents the result of a delete operation. +type DeleteResult struct { + gophercloud.ErrResult +} + +// IP is a sub-struct that represents an individual IP. +type IP struct { + SubnetID string `mapstructure:"subnet_id" json:"subnet_id"` + IPAddress string `mapstructure:"ip_address" json:"ip_address,omitempty"` +} + +// Port represents a Neutron port. See package documentation for a top-level +// description of what this is. +type Port struct { + // UUID for the port. + ID string `mapstructure:"id" json:"id"` + // Network that this port is associated with. + NetworkID string `mapstructure:"network_id" json:"network_id"` + // Human-readable name for the port. Might not be unique. + Name string `mapstructure:"name" json:"name"` + // Administrative state of port. If false (down), port does not forward packets. + AdminStateUp bool `mapstructure:"admin_state_up" json:"admin_state_up"` + // Indicates whether network is currently operational. Possible values include + // `ACTIVE', `DOWN', `BUILD', or `ERROR'. Plug-ins might define additional values. + Status string `mapstructure:"status" json:"status"` + // Mac address to use on this port. + MACAddress string `mapstructure:"mac_address" json:"mac_address"` + // Specifies IP addresses for the port thus associating the port itself with + // the subnets where the IP addresses are picked from + FixedIPs []IP `mapstructure:"fixed_ips" json:"fixed_ips"` + // Owner of network. Only admin users can specify a tenant_id other than its own. + TenantID string `mapstructure:"tenant_id" json:"tenant_id"` + // Identifies the entity (e.g.: dhcp agent) using this port. + DeviceOwner string `mapstructure:"device_owner" json:"device_owner"` + // Specifies the IDs of any security groups associated with a port. + SecurityGroups []string `mapstructure:"security_groups" json:"security_groups"` + // Identifies the device (e.g., virtual server) using this port. + DeviceID string `mapstructure:"device_id" json:"device_id"` +} + +// PortPage is the page returned by a pager when traversing over a collection +// of network ports. +type PortPage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of ports has reached +// the end of a page and the pager seeks to traverse over a new one. In order +// to do this, it needs to construct the next page's URL. +func (p PortPage) NextPageURL() (string, error) { + type resp struct { + Links []gophercloud.Link `mapstructure:"ports_links"` + } + + var r resp + err := mapstructure.Decode(p.Body, &r) + if err != nil { + return "", err + } + + return gophercloud.ExtractNextURL(r.Links) +} + +// IsEmpty checks whether a PortPage struct is empty. +func (p PortPage) IsEmpty() (bool, error) { + is, err := ExtractPorts(p) + if err != nil { + return true, nil + } + return len(is) == 0, nil +} + +// ExtractPorts accepts a Page struct, specifically a PortPage struct, +// and extracts the elements into a slice of Port structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractPorts(page pagination.Page) ([]Port, error) { + var resp struct { + Ports []Port `mapstructure:"ports" json:"ports"` + } + + err := mapstructure.Decode(page.(PortPage).Body, &resp) + + return resp.Ports, err +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/ports/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/ports/urls.go new file mode 100644 index 0000000000..6d0572f1fb --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/ports/urls.go @@ -0,0 +1,31 @@ +package ports + +import "github.com/rackspace/gophercloud" + +func resourceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL("ports", id) +} + +func rootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("ports") +} + +func listURL(c *gophercloud.ServiceClient) string { + return rootURL(c) +} + +func getURL(c *gophercloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func createURL(c *gophercloud.ServiceClient) string { + return rootURL(c) +} + +func updateURL(c *gophercloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func deleteURL(c *gophercloud.ServiceClient, id string) string { + return resourceURL(c, id) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/ports/urls_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/ports/urls_test.go new file mode 100644 index 0000000000..7fadd4dcb7 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/ports/urls_test.go @@ -0,0 +1,44 @@ +package ports + +import ( + "testing" + + "github.com/rackspace/gophercloud" + th "github.com/rackspace/gophercloud/testhelper" +) + +const endpoint = "http://localhost:57909/" + +func endpointClient() *gophercloud.ServiceClient { + return &gophercloud.ServiceClient{Endpoint: endpoint, ResourceBase: endpoint + "v2.0/"} +} + +func TestListURL(t *testing.T) { + actual := listURL(endpointClient()) + expected := endpoint + "v2.0/ports" + th.AssertEquals(t, expected, actual) +} + +func TestGetURL(t *testing.T) { + actual := getURL(endpointClient(), "foo") + expected := endpoint + "v2.0/ports/foo" + th.AssertEquals(t, expected, actual) +} + +func TestCreateURL(t *testing.T) { + actual := createURL(endpointClient()) + expected := endpoint + "v2.0/ports" + th.AssertEquals(t, expected, actual) +} + +func TestUpdateURL(t *testing.T) { + actual := updateURL(endpointClient(), "foo") + expected := endpoint + "v2.0/ports/foo" + th.AssertEquals(t, expected, actual) +} + +func TestDeleteURL(t *testing.T) { + actual := deleteURL(endpointClient(), "foo") + expected := endpoint + "v2.0/ports/foo" + th.AssertEquals(t, expected, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/subnets/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/subnets/doc.go new file mode 100644 index 0000000000..43e8296c7f --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/subnets/doc.go @@ -0,0 +1,10 @@ +// Package subnets contains functionality for working with Neutron subnet +// resources. A subnet represents an IP address block that can be used to +// assign IP addresses to virtual instances. Each subnet must have a CIDR and +// must be associated with a network. IPs can either be selected from the whole +// subnet CIDR or from allocation pools specified by the user. +// +// A subnet can also have a gateway, a list of DNS name servers, and host routes. +// This information is pushed to instances whose interfaces are associated with +// the subnet. +package subnets diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/subnets/errors.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/subnets/errors.go new file mode 100644 index 0000000000..0db0a6e604 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/subnets/errors.go @@ -0,0 +1,13 @@ +package subnets + +import "fmt" + +func err(str string) error { + return fmt.Errorf("%s", str) +} + +var ( + errNetworkIDRequired = err("A network ID is required") + errCIDRRequired = err("A valid CIDR is required") + errInvalidIPType = err("An IP type must either be 4 or 6") +) diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/subnets/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/subnets/requests.go new file mode 100644 index 0000000000..cd7c663c2d --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/subnets/requests.go @@ -0,0 +1,254 @@ +package subnets + +import ( + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" + + "github.com/racker/perigee" +) + +// AdminState gives users a solid type to work with for create and update +// operations. It is recommended that users use the `Up` and `Down` enums. +type AdminState *bool + +// Convenience vars for AdminStateUp values. +var ( + iTrue = true + iFalse = false + + Up AdminState = &iTrue + Down AdminState = &iFalse +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToSubnetListQuery() (string, error) +} + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the subnet attributes you want to see returned. SortKey allows you to sort +// by a particular subnet attribute. SortDir sets the direction, and is either +// `asc' or `desc'. Marker and Limit are used for pagination. +type ListOpts struct { + Name string `q:"name"` + EnableDHCP *bool `q:"enable_dhcp"` + NetworkID string `q:"network_id"` + TenantID string `q:"tenant_id"` + IPVersion int `q:"ip_version"` + GatewayIP string `q:"gateway_ip"` + CIDR string `q:"cidr"` + ID string `q:"id"` + Limit int `q:"limit"` + Marker string `q:"marker"` + SortKey string `q:"sort_key"` + SortDir string `q:"sort_dir"` +} + +// ToSubnetListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToSubnetListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + if err != nil { + return "", err + } + return q.String(), nil +} + +// List returns a Pager which allows you to iterate over a collection of +// subnets. It accepts a ListOpts struct, which allows you to filter and sort +// the returned collection for greater efficiency. +// +// Default policy settings return only those subnets that are owned by the tenant +// who submits the request, unless the request is submitted by a user with +// administrative rights. +func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(c) + if opts != nil { + query, err := opts.ToSubnetListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return SubnetPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Get retrieves a specific subnet based on its unique ID. +func Get(c *gophercloud.ServiceClient, id string) GetResult { + var res GetResult + _, res.Err = perigee.Request("GET", getURL(c, id), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + Results: &res.Body, + OkCodes: []int{200}, + }) + return res +} + +// Valid IP types +const ( + IPv4 = 4 + IPv6 = 6 +) + +// CreateOptsBuilder is the interface options structs have to satisfy in order +// to be used in the main Create operation in this package. Since many +// extensions decorate or modify the common logic, it is useful for them to +// satisfy a basic interface in order for them to be used. +type CreateOptsBuilder interface { + ToSubnetCreateMap() (map[string]interface{}, error) +} + +// CreateOpts represents the attributes used when creating a new subnet. +type CreateOpts struct { + // Required + NetworkID string + CIDR string + // Optional + Name string + TenantID string + AllocationPools []AllocationPool + GatewayIP string + IPVersion int + EnableDHCP *bool + DNSNameservers []string + HostRoutes []HostRoute +} + +// ToSubnetCreateMap casts a CreateOpts struct to a map. +func (opts CreateOpts) ToSubnetCreateMap() (map[string]interface{}, error) { + s := make(map[string]interface{}) + + if opts.NetworkID == "" { + return nil, errNetworkIDRequired + } + if opts.CIDR == "" { + return nil, errCIDRRequired + } + if opts.IPVersion != 0 && opts.IPVersion != IPv4 && opts.IPVersion != IPv6 { + return nil, errInvalidIPType + } + + s["network_id"] = opts.NetworkID + s["cidr"] = opts.CIDR + + if opts.EnableDHCP != nil { + s["enable_dhcp"] = &opts.EnableDHCP + } + if opts.Name != "" { + s["name"] = opts.Name + } + if opts.GatewayIP != "" { + s["gateway_ip"] = opts.GatewayIP + } + if opts.TenantID != "" { + s["tenant_id"] = opts.TenantID + } + if opts.IPVersion != 0 { + s["ip_version"] = opts.IPVersion + } + if len(opts.AllocationPools) != 0 { + s["allocation_pools"] = opts.AllocationPools + } + if len(opts.DNSNameservers) != 0 { + s["dns_nameservers"] = opts.DNSNameservers + } + if len(opts.HostRoutes) != 0 { + s["host_routes"] = opts.HostRoutes + } + + return map[string]interface{}{"subnet": s}, nil +} + +// Create accepts a CreateOpts struct and creates a new subnet using the values +// provided. You must remember to provide a valid NetworkID, CIDR and IP version. +func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult { + var res CreateResult + + reqBody, err := opts.ToSubnetCreateMap() + if err != nil { + res.Err = err + return res + } + + _, res.Err = perigee.Request("POST", createURL(c), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + ReqBody: &reqBody, + Results: &res.Body, + OkCodes: []int{201}, + }) + + return res +} + +// UpdateOptsBuilder allows extensions to add additional parameters to the +// Update request. +type UpdateOptsBuilder interface { + ToSubnetUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts represents the attributes used when updating an existing subnet. +type UpdateOpts struct { + Name string + GatewayIP string + DNSNameservers []string + HostRoutes []HostRoute + EnableDHCP *bool +} + +// ToSubnetUpdateMap casts an UpdateOpts struct to a map. +func (opts UpdateOpts) ToSubnetUpdateMap() (map[string]interface{}, error) { + s := make(map[string]interface{}) + + if opts.EnableDHCP != nil { + s["enable_dhcp"] = &opts.EnableDHCP + } + if opts.Name != "" { + s["name"] = opts.Name + } + if opts.GatewayIP != "" { + s["gateway_ip"] = opts.GatewayIP + } + if len(opts.DNSNameservers) != 0 { + s["dns_nameservers"] = opts.DNSNameservers + } + if len(opts.HostRoutes) != 0 { + s["host_routes"] = opts.HostRoutes + } + + return map[string]interface{}{"subnet": s}, nil +} + +// Update accepts a UpdateOpts struct and updates an existing subnet using the +// values provided. +func Update(c *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) UpdateResult { + var res UpdateResult + + reqBody, err := opts.ToSubnetUpdateMap() + if err != nil { + res.Err = err + return res + } + + _, res.Err = perigee.Request("PUT", updateURL(c, id), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + ReqBody: &reqBody, + Results: &res.Body, + OkCodes: []int{200, 201}, + }) + + return res +} + +// Delete accepts a unique ID and deletes the subnet associated with it. +func Delete(c *gophercloud.ServiceClient, id string) DeleteResult { + var res DeleteResult + _, res.Err = perigee.Request("DELETE", deleteURL(c, id), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + OkCodes: []int{204}, + }) + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/subnets/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/subnets/requests_test.go new file mode 100644 index 0000000000..987064ada6 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/subnets/requests_test.go @@ -0,0 +1,362 @@ +package subnets + +import ( + "fmt" + "net/http" + "testing" + + fake "github.com/rackspace/gophercloud/openstack/networking/v2/common" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/subnets", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "subnets": [ + { + "name": "private-subnet", + "enable_dhcp": true, + "network_id": "db193ab3-96e3-4cb3-8fc5-05f4296d0324", + "tenant_id": "26a7980765d0414dbc1fc1f88cdb7e6e", + "dns_nameservers": [], + "allocation_pools": [ + { + "start": "10.0.0.2", + "end": "10.0.0.254" + } + ], + "host_routes": [], + "ip_version": 4, + "gateway_ip": "10.0.0.1", + "cidr": "10.0.0.0/24", + "id": "08eae331-0402-425a-923c-34f7cfe39c1b" + }, + { + "name": "my_subnet", + "enable_dhcp": true, + "network_id": "d32019d3-bc6e-4319-9c1d-6722fc136a22", + "tenant_id": "4fd44f30292945e481c7b8a0c8908869", + "dns_nameservers": [], + "allocation_pools": [ + { + "start": "192.0.0.2", + "end": "192.255.255.254" + } + ], + "host_routes": [], + "ip_version": 4, + "gateway_ip": "192.0.0.1", + "cidr": "192.0.0.0/8", + "id": "54d6f61d-db07-451c-9ab3-b9609b6b6f0b" + } + ] +} + `) + }) + + count := 0 + + List(fake.ServiceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractSubnets(page) + if err != nil { + t.Errorf("Failed to extract subnets: %v", err) + return false, nil + } + + expected := []Subnet{ + Subnet{ + Name: "private-subnet", + EnableDHCP: true, + NetworkID: "db193ab3-96e3-4cb3-8fc5-05f4296d0324", + TenantID: "26a7980765d0414dbc1fc1f88cdb7e6e", + DNSNameservers: []string{}, + AllocationPools: []AllocationPool{ + AllocationPool{ + Start: "10.0.0.2", + End: "10.0.0.254", + }, + }, + HostRoutes: []HostRoute{}, + IPVersion: 4, + GatewayIP: "10.0.0.1", + CIDR: "10.0.0.0/24", + ID: "08eae331-0402-425a-923c-34f7cfe39c1b", + }, + Subnet{ + Name: "my_subnet", + EnableDHCP: true, + NetworkID: "d32019d3-bc6e-4319-9c1d-6722fc136a22", + TenantID: "4fd44f30292945e481c7b8a0c8908869", + DNSNameservers: []string{}, + AllocationPools: []AllocationPool{ + AllocationPool{ + Start: "192.0.0.2", + End: "192.255.255.254", + }, + }, + HostRoutes: []HostRoute{}, + IPVersion: 4, + GatewayIP: "192.0.0.1", + CIDR: "192.0.0.0/8", + ID: "54d6f61d-db07-451c-9ab3-b9609b6b6f0b", + }, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/subnets/54d6f61d-db07-451c-9ab3-b9609b6b6f0b", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "subnet": { + "name": "my_subnet", + "enable_dhcp": true, + "network_id": "d32019d3-bc6e-4319-9c1d-6722fc136a22", + "tenant_id": "4fd44f30292945e481c7b8a0c8908869", + "dns_nameservers": [], + "allocation_pools": [ + { + "start": "192.0.0.2", + "end": "192.255.255.254" + } + ], + "host_routes": [], + "ip_version": 4, + "gateway_ip": "192.0.0.1", + "cidr": "192.0.0.0/8", + "id": "54d6f61d-db07-451c-9ab3-b9609b6b6f0b" + } +} + `) + }) + + s, err := Get(fake.ServiceClient(), "54d6f61d-db07-451c-9ab3-b9609b6b6f0b").Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, s.Name, "my_subnet") + th.AssertEquals(t, s.EnableDHCP, true) + th.AssertEquals(t, s.NetworkID, "d32019d3-bc6e-4319-9c1d-6722fc136a22") + th.AssertEquals(t, s.TenantID, "4fd44f30292945e481c7b8a0c8908869") + th.AssertDeepEquals(t, s.DNSNameservers, []string{}) + th.AssertDeepEquals(t, s.AllocationPools, []AllocationPool{ + AllocationPool{ + Start: "192.0.0.2", + End: "192.255.255.254", + }, + }) + th.AssertDeepEquals(t, s.HostRoutes, []HostRoute{}) + th.AssertEquals(t, s.IPVersion, 4) + th.AssertEquals(t, s.GatewayIP, "192.0.0.1") + th.AssertEquals(t, s.CIDR, "192.0.0.0/8") + th.AssertEquals(t, s.ID, "54d6f61d-db07-451c-9ab3-b9609b6b6f0b") +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/subnets", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "subnet": { + "network_id": "d32019d3-bc6e-4319-9c1d-6722fc136a22", + "ip_version": 4, + "cidr": "192.168.199.0/24", + "dns_nameservers": ["foo"], + "allocation_pools": [ + { + "start": "192.168.199.2", + "end": "192.168.199.254" + } + ], + "host_routes": [{"destination":"","nexthop": "bar"}] + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, ` +{ + "subnet": { + "name": "", + "enable_dhcp": true, + "network_id": "d32019d3-bc6e-4319-9c1d-6722fc136a22", + "tenant_id": "4fd44f30292945e481c7b8a0c8908869", + "dns_nameservers": [], + "allocation_pools": [ + { + "start": "192.168.199.2", + "end": "192.168.199.254" + } + ], + "host_routes": [], + "ip_version": 4, + "gateway_ip": "192.168.199.1", + "cidr": "192.168.199.0/24", + "id": "3b80198d-4f7b-4f77-9ef5-774d54e17126" + } +} + `) + }) + + opts := CreateOpts{ + NetworkID: "d32019d3-bc6e-4319-9c1d-6722fc136a22", + IPVersion: 4, + CIDR: "192.168.199.0/24", + AllocationPools: []AllocationPool{ + AllocationPool{ + Start: "192.168.199.2", + End: "192.168.199.254", + }, + }, + DNSNameservers: []string{"foo"}, + HostRoutes: []HostRoute{ + HostRoute{NextHop: "bar"}, + }, + } + s, err := Create(fake.ServiceClient(), opts).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, s.Name, "") + th.AssertEquals(t, s.EnableDHCP, true) + th.AssertEquals(t, s.NetworkID, "d32019d3-bc6e-4319-9c1d-6722fc136a22") + th.AssertEquals(t, s.TenantID, "4fd44f30292945e481c7b8a0c8908869") + th.AssertDeepEquals(t, s.DNSNameservers, []string{}) + th.AssertDeepEquals(t, s.AllocationPools, []AllocationPool{ + AllocationPool{ + Start: "192.168.199.2", + End: "192.168.199.254", + }, + }) + th.AssertDeepEquals(t, s.HostRoutes, []HostRoute{}) + th.AssertEquals(t, s.IPVersion, 4) + th.AssertEquals(t, s.GatewayIP, "192.168.199.1") + th.AssertEquals(t, s.CIDR, "192.168.199.0/24") + th.AssertEquals(t, s.ID, "3b80198d-4f7b-4f77-9ef5-774d54e17126") +} + +func TestRequiredCreateOpts(t *testing.T) { + res := Create(fake.ServiceClient(), CreateOpts{}) + if res.Err == nil { + t.Fatalf("Expected error, got none") + } + + res = Create(fake.ServiceClient(), CreateOpts{NetworkID: "foo"}) + if res.Err == nil { + t.Fatalf("Expected error, got none") + } + + res = Create(fake.ServiceClient(), CreateOpts{NetworkID: "foo", CIDR: "bar", IPVersion: 40}) + if res.Err == nil { + t.Fatalf("Expected error, got none") + } +} + +func TestUpdate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/subnets/08eae331-0402-425a-923c-34f7cfe39c1b", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "subnet": { + "name": "my_new_subnet", + "dns_nameservers": ["foo"], + "host_routes": [{"destination":"","nexthop": "bar"}] + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, ` +{ + "subnet": { + "name": "my_new_subnet", + "enable_dhcp": true, + "network_id": "db193ab3-96e3-4cb3-8fc5-05f4296d0324", + "tenant_id": "26a7980765d0414dbc1fc1f88cdb7e6e", + "dns_nameservers": [], + "allocation_pools": [ + { + "start": "10.0.0.2", + "end": "10.0.0.254" + } + ], + "host_routes": [], + "ip_version": 4, + "gateway_ip": "10.0.0.1", + "cidr": "10.0.0.0/24", + "id": "08eae331-0402-425a-923c-34f7cfe39c1b" + } +} + `) + }) + + opts := UpdateOpts{ + Name: "my_new_subnet", + DNSNameservers: []string{"foo"}, + HostRoutes: []HostRoute{ + HostRoute{NextHop: "bar"}, + }, + } + s, err := Update(fake.ServiceClient(), "08eae331-0402-425a-923c-34f7cfe39c1b", opts).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, s.Name, "my_new_subnet") + th.AssertEquals(t, s.ID, "08eae331-0402-425a-923c-34f7cfe39c1b") +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/subnets/08eae331-0402-425a-923c-34f7cfe39c1b", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusNoContent) + }) + + res := Delete(fake.ServiceClient(), "08eae331-0402-425a-923c-34f7cfe39c1b") + th.AssertNoErr(t, res.Err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/subnets/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/subnets/results.go new file mode 100644 index 0000000000..1910f17dd9 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/subnets/results.go @@ -0,0 +1,132 @@ +package subnets + +import ( + "github.com/mitchellh/mapstructure" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +type commonResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts a subnet resource. +func (r commonResult) Extract() (*Subnet, error) { + if r.Err != nil { + return nil, r.Err + } + + var res struct { + Subnet *Subnet `json:"subnet"` + } + + err := mapstructure.Decode(r.Body, &res) + + return res.Subnet, err +} + +// CreateResult represents the result of a create operation. +type CreateResult struct { + commonResult +} + +// GetResult represents the result of a get operation. +type GetResult struct { + commonResult +} + +// UpdateResult represents the result of an update operation. +type UpdateResult struct { + commonResult +} + +// DeleteResult represents the result of a delete operation. +type DeleteResult struct { + gophercloud.ErrResult +} + +// AllocationPool represents a sub-range of cidr available for dynamic +// allocation to ports, e.g. {Start: "10.0.0.2", End: "10.0.0.254"} +type AllocationPool struct { + Start string `json:"start"` + End string `json:"end"` +} + +// HostRoute represents a route that should be used by devices with IPs from +// a subnet (not including local subnet route). +type HostRoute struct { + DestinationCIDR string `json:"destination"` + NextHop string `json:"nexthop"` +} + +// Subnet represents a subnet. See package documentation for a top-level +// description of what this is. +type Subnet struct { + // UUID representing the subnet + ID string `mapstructure:"id" json:"id"` + // UUID of the parent network + NetworkID string `mapstructure:"network_id" json:"network_id"` + // Human-readable name for the subnet. Might not be unique. + Name string `mapstructure:"name" json:"name"` + // IP version, either `4' or `6' + IPVersion int `mapstructure:"ip_version" json:"ip_version"` + // CIDR representing IP range for this subnet, based on IP version + CIDR string `mapstructure:"cidr" json:"cidr"` + // Default gateway used by devices in this subnet + GatewayIP string `mapstructure:"gateway_ip" json:"gateway_ip"` + // DNS name servers used by hosts in this subnet. + DNSNameservers []string `mapstructure:"dns_nameservers" json:"dns_nameservers"` + // Sub-ranges of CIDR available for dynamic allocation to ports. See AllocationPool. + AllocationPools []AllocationPool `mapstructure:"allocation_pools" json:"allocation_pools"` + // Routes that should be used by devices with IPs from this subnet (not including local subnet route). + HostRoutes []HostRoute `mapstructure:"host_routes" json:"host_routes"` + // Specifies whether DHCP is enabled for this subnet or not. + EnableDHCP bool `mapstructure:"enable_dhcp" json:"enable_dhcp"` + // Owner of network. Only admin users can specify a tenant_id other than its own. + TenantID string `mapstructure:"tenant_id" json:"tenant_id"` +} + +// SubnetPage is the page returned by a pager when traversing over a collection +// of subnets. +type SubnetPage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of subnets has reached +// the end of a page and the pager seeks to traverse over a new one. In order +// to do this, it needs to construct the next page's URL. +func (p SubnetPage) NextPageURL() (string, error) { + type resp struct { + Links []gophercloud.Link `mapstructure:"subnets_links"` + } + + var r resp + err := mapstructure.Decode(p.Body, &r) + if err != nil { + return "", err + } + + return gophercloud.ExtractNextURL(r.Links) +} + +// IsEmpty checks whether a SubnetPage struct is empty. +func (p SubnetPage) IsEmpty() (bool, error) { + is, err := ExtractSubnets(p) + if err != nil { + return true, nil + } + return len(is) == 0, nil +} + +// ExtractSubnets accepts a Page struct, specifically a SubnetPage struct, +// and extracts the elements into a slice of Subnet structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractSubnets(page pagination.Page) ([]Subnet, error) { + var resp struct { + Subnets []Subnet `mapstructure:"subnets" json:"subnets"` + } + + err := mapstructure.Decode(page.(SubnetPage).Body, &resp) + + return resp.Subnets, err +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/subnets/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/subnets/urls.go new file mode 100644 index 0000000000..0d02368941 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/subnets/urls.go @@ -0,0 +1,31 @@ +package subnets + +import "github.com/rackspace/gophercloud" + +func resourceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL("subnets", id) +} + +func rootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("subnets") +} + +func listURL(c *gophercloud.ServiceClient) string { + return rootURL(c) +} + +func getURL(c *gophercloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func createURL(c *gophercloud.ServiceClient) string { + return rootURL(c) +} + +func updateURL(c *gophercloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func deleteURL(c *gophercloud.ServiceClient, id string) string { + return resourceURL(c, id) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/subnets/urls_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/subnets/urls_test.go new file mode 100644 index 0000000000..aeeddf3549 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/subnets/urls_test.go @@ -0,0 +1,44 @@ +package subnets + +import ( + "testing" + + "github.com/rackspace/gophercloud" + th "github.com/rackspace/gophercloud/testhelper" +) + +const endpoint = "http://localhost:57909/" + +func endpointClient() *gophercloud.ServiceClient { + return &gophercloud.ServiceClient{Endpoint: endpoint, ResourceBase: endpoint + "v2.0/"} +} + +func TestListURL(t *testing.T) { + actual := listURL(endpointClient()) + expected := endpoint + "v2.0/subnets" + th.AssertEquals(t, expected, actual) +} + +func TestGetURL(t *testing.T) { + actual := getURL(endpointClient(), "foo") + expected := endpoint + "v2.0/subnets/foo" + th.AssertEquals(t, expected, actual) +} + +func TestCreateURL(t *testing.T) { + actual := createURL(endpointClient()) + expected := endpoint + "v2.0/subnets" + th.AssertEquals(t, expected, actual) +} + +func TestUpdateURL(t *testing.T) { + actual := updateURL(endpointClient(), "foo") + expected := endpoint + "v2.0/subnets/foo" + th.AssertEquals(t, expected, actual) +} + +func TestDeleteURL(t *testing.T) { + actual := deleteURL(endpointClient(), "foo") + expected := endpoint + "v2.0/subnets/foo" + th.AssertEquals(t, expected, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/accounts/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/accounts/doc.go new file mode 100644 index 0000000000..f5f894a9e5 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/accounts/doc.go @@ -0,0 +1,8 @@ +// Package accounts contains functionality for working with Object Storage +// account resources. An account is the top-level resource the object storage +// hierarchy: containers belong to accounts, objects belong to containers. +// +// Another way of thinking of an account is like a namespace for all your +// resources. It is synonymous with a project or tenant in other OpenStack +// services. +package accounts diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/accounts/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/accounts/fixtures.go new file mode 100644 index 0000000000..3dad0c5a9b --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/accounts/fixtures.go @@ -0,0 +1,38 @@ +// +build fixtures + +package accounts + +import ( + "net/http" + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +// HandleGetAccountSuccessfully creates an HTTP handler at `/` on the test handler mux that +// responds with a `Get` response. +func HandleGetAccountSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "X-Account-Meta-Gophercloud-Test", "accounts") + + w.Header().Set("X-Account-Container-Count", "2") + w.Header().Set("X-Account-Bytes-Used", "14") + w.Header().Set("X-Account-Meta-Subject", "books") + + w.WriteHeader(http.StatusNoContent) + }) +} + +// HandleUpdateAccountSuccessfully creates an HTTP handler at `/` on the test handler mux that +// responds with a `Update` response. +func HandleUpdateAccountSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "HEAD") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.Header().Set("X-Account-Meta-Foo", "bar") + w.WriteHeader(http.StatusNoContent) + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/accounts/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/accounts/requests.go new file mode 100644 index 0000000000..e6f5f9594c --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/accounts/requests.go @@ -0,0 +1,106 @@ +package accounts + +import ( + "github.com/racker/perigee" + "github.com/rackspace/gophercloud" +) + +// GetOptsBuilder allows extensions to add additional headers to the Get +// request. +type GetOptsBuilder interface { + ToAccountGetMap() (map[string]string, error) +} + +// GetOpts is a structure that contains parameters for getting an account's +// metadata. +type GetOpts struct { + Newest bool `h:"X-Newest"` +} + +// ToAccountGetMap formats a GetOpts into a map[string]string of headers. +func (opts GetOpts) ToAccountGetMap() (map[string]string, error) { + return gophercloud.BuildHeaders(opts) +} + +// Get is a function that retrieves an account's metadata. To extract just the +// custom metadata, call the ExtractMetadata method on the GetResult. To extract +// all the headers that are returned (including the metadata), call the +// ExtractHeader method on the GetResult. +func Get(c *gophercloud.ServiceClient, opts GetOptsBuilder) GetResult { + var res GetResult + h := c.AuthenticatedHeaders() + + if opts != nil { + headers, err := opts.ToAccountGetMap() + if err != nil { + res.Err = err + return res + } + + for k, v := range headers { + h[k] = v + } + } + + resp, err := perigee.Request("HEAD", getURL(c), perigee.Options{ + MoreHeaders: h, + OkCodes: []int{204}, + }) + res.Header = resp.HttpResponse.Header + res.Err = err + return res +} + +// UpdateOptsBuilder allows extensions to add additional headers to the Update +// request. +type UpdateOptsBuilder interface { + ToAccountUpdateMap() (map[string]string, error) +} + +// UpdateOpts is a structure that contains parameters for updating, creating, or +// deleting an account's metadata. +type UpdateOpts struct { + Metadata map[string]string + ContentType string `h:"Content-Type"` + DetectContentType bool `h:"X-Detect-Content-Type"` + TempURLKey string `h:"X-Account-Meta-Temp-URL-Key"` + TempURLKey2 string `h:"X-Account-Meta-Temp-URL-Key-2"` +} + +// ToAccountUpdateMap formats an UpdateOpts into a map[string]string of headers. +func (opts UpdateOpts) ToAccountUpdateMap() (map[string]string, error) { + headers, err := gophercloud.BuildHeaders(opts) + if err != nil { + return nil, err + } + for k, v := range opts.Metadata { + headers["X-Account-Meta-"+k] = v + } + return headers, err +} + +// Update is a function that creates, updates, or deletes an account's metadata. +// To extract the headers returned, call the Extract method on the UpdateResult. +func Update(c *gophercloud.ServiceClient, opts UpdateOptsBuilder) UpdateResult { + var res UpdateResult + h := c.AuthenticatedHeaders() + + if opts != nil { + headers, err := opts.ToAccountUpdateMap() + if err != nil { + res.Err = err + return res + } + for k, v := range headers { + h[k] = v + } + } + + resp, err := perigee.Request("POST", updateURL(c), perigee.Options{ + MoreHeaders: h, + OkCodes: []int{204}, + }) + res.Header = resp.HttpResponse.Header + res.Err = err + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/accounts/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/accounts/requests_test.go new file mode 100644 index 0000000000..d6dc26b650 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/accounts/requests_test.go @@ -0,0 +1,33 @@ +package accounts + +import ( + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +var metadata = map[string]string{"gophercloud-test": "accounts"} + +func TestUpdateAccount(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleGetAccountSuccessfully(t) + + options := &UpdateOpts{Metadata: map[string]string{"gophercloud-test": "accounts"}} + res := Update(fake.ServiceClient(), options) + th.AssertNoErr(t, res.Err) +} + +func TestGetAccount(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleUpdateAccountSuccessfully(t) + + expected := map[string]string{"Foo": "bar"} + actual, err := Get(fake.ServiceClient(), &GetOpts{}).ExtractMetadata() + if err != nil { + t.Fatalf("Unable to get account metadata: %v", err) + } + th.CheckDeepEquals(t, expected, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/accounts/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/accounts/results.go new file mode 100644 index 0000000000..abae02659c --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/accounts/results.go @@ -0,0 +1,34 @@ +package accounts + +import ( + "strings" + + "github.com/rackspace/gophercloud" +) + +// GetResult is returned from a call to the Get function. +type GetResult struct { + gophercloud.HeaderResult +} + +// ExtractMetadata is a function that takes a GetResult (of type *http.Response) +// and returns the custom metatdata associated with the account. +func (gr GetResult) ExtractMetadata() (map[string]string, error) { + if gr.Err != nil { + return nil, gr.Err + } + + metadata := make(map[string]string) + for k, v := range gr.Header { + if strings.HasPrefix(k, "X-Account-Meta-") { + key := strings.TrimPrefix(k, "X-Account-Meta-") + metadata[key] = v[0] + } + } + return metadata, nil +} + +// UpdateResult is returned from a call to the Update function. +type UpdateResult struct { + gophercloud.HeaderResult +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/accounts/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/accounts/urls.go new file mode 100644 index 0000000000..9952fe4345 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/accounts/urls.go @@ -0,0 +1,11 @@ +package accounts + +import "github.com/rackspace/gophercloud" + +func getURL(c *gophercloud.ServiceClient) string { + return c.Endpoint +} + +func updateURL(c *gophercloud.ServiceClient) string { + return getURL(c) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/accounts/urls_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/accounts/urls_test.go new file mode 100644 index 0000000000..074d52dfd5 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/accounts/urls_test.go @@ -0,0 +1,26 @@ +package accounts + +import ( + "testing" + + "github.com/rackspace/gophercloud" + th "github.com/rackspace/gophercloud/testhelper" +) + +const endpoint = "http://localhost:57909/" + +func endpointClient() *gophercloud.ServiceClient { + return &gophercloud.ServiceClient{Endpoint: endpoint} +} + +func TestGetURL(t *testing.T) { + actual := getURL(endpointClient()) + expected := endpoint + th.CheckEquals(t, expected, actual) +} + +func TestUpdateURL(t *testing.T) { + actual := updateURL(endpointClient()) + expected := endpoint + th.CheckEquals(t, expected, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers/doc.go new file mode 100644 index 0000000000..5fed5537f1 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers/doc.go @@ -0,0 +1,8 @@ +// Package containers contains functionality for working with Object Storage +// container resources. A container serves as a logical namespace for objects +// that are placed inside it - an object with the same name in two different +// containers represents two different objects. +// +// In addition to containing objects, you can also use the container to control +// access to objects by using an access control list (ACL). +package containers diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers/fixtures.go new file mode 100644 index 0000000000..1c0a915cb7 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers/fixtures.go @@ -0,0 +1,132 @@ +// +build fixtures + +package containers + +import ( + "fmt" + "net/http" + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +// ExpectedListInfo is the result expected from a call to `List` when full +// info is requested. +var ExpectedListInfo = []Container{ + Container{ + Count: 0, + Bytes: 0, + Name: "janeausten", + }, + Container{ + Count: 1, + Bytes: 14, + Name: "marktwain", + }, +} + +// ExpectedListNames is the result expected from a call to `List` when just +// container names are requested. +var ExpectedListNames = []string{"janeausten", "marktwain"} + +// HandleListContainerInfoSuccessfully creates an HTTP handler at `/` on the test handler mux that +// responds with a `List` response when full info is requested. +func HandleListContainerInfoSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + + w.Header().Set("Content-Type", "application/json") + r.ParseForm() + marker := r.Form.Get("marker") + switch marker { + case "": + fmt.Fprintf(w, `[ + { + "count": 0, + "bytes": 0, + "name": "janeausten" + }, + { + "count": 1, + "bytes": 14, + "name": "marktwain" + } + ]`) + case "marktwain": + fmt.Fprintf(w, `[]`) + default: + t.Fatalf("Unexpected marker: [%s]", marker) + } + }) +} + +// HandleListContainerNamesSuccessfully creates an HTTP handler at `/` on the test handler mux that +// responds with a `ListNames` response when only container names are requested. +func HandleListContainerNamesSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "text/plain") + + w.Header().Set("Content-Type", "text/plain") + r.ParseForm() + marker := r.Form.Get("marker") + switch marker { + case "": + fmt.Fprintf(w, "janeausten\nmarktwain\n") + case "marktwain": + fmt.Fprintf(w, ``) + default: + t.Fatalf("Unexpected marker: [%s]", marker) + } + }) +} + +// HandleCreateContainerSuccessfully creates an HTTP handler at `/testContainer` on the test handler mux that +// responds with a `Create` response. +func HandleCreateContainerSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/testContainer", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + + w.Header().Add("X-Container-Meta-Foo", "bar") + w.WriteHeader(http.StatusNoContent) + }) +} + +// HandleDeleteContainerSuccessfully creates an HTTP handler at `/testContainer` on the test handler mux that +// responds with a `Delete` response. +func HandleDeleteContainerSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/testContainer", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + w.WriteHeader(http.StatusNoContent) + }) +} + +// HandleUpdateContainerSuccessfully creates an HTTP handler at `/testContainer` on the test handler mux that +// responds with a `Update` response. +func HandleUpdateContainerSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/testContainer", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + w.WriteHeader(http.StatusNoContent) + }) +} + +// HandleGetContainerSuccessfully creates an HTTP handler at `/testContainer` on the test handler mux that +// responds with a `Get` response. +func HandleGetContainerSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/testContainer", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "HEAD") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + w.WriteHeader(http.StatusNoContent) + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers/requests.go new file mode 100644 index 0000000000..9f3b2af0a6 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers/requests.go @@ -0,0 +1,204 @@ +package containers + +import ( + "github.com/racker/perigee" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to the List +// request. +type ListOptsBuilder interface { + ToContainerListParams() (bool, string, error) +} + +// ListOpts is a structure that holds options for listing containers. +type ListOpts struct { + Full bool + Limit int `q:"limit"` + Marker string `q:"marker"` + EndMarker string `q:"end_marker"` + Format string `q:"format"` + Prefix string `q:"prefix"` + Delimiter string `q:"delimiter"` +} + +// ToContainerListParams formats a ListOpts into a query string and boolean +// representing whether to list complete information for each container. +func (opts ListOpts) ToContainerListParams() (bool, string, error) { + q, err := gophercloud.BuildQueryString(opts) + if err != nil { + return false, "", err + } + return opts.Full, q.String(), nil +} + +// List is a function that retrieves containers associated with the account as +// well as account metadata. It returns a pager which can be iterated with the +// EachPage function. +func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + headers := map[string]string{"Accept": "text/plain", "Content-Type": "text/plain"} + + url := listURL(c) + if opts != nil { + full, query, err := opts.ToContainerListParams() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + + if full { + headers = map[string]string{"Accept": "application/json", "Content-Type": "application/json"} + } + } + + createPage := func(r pagination.PageResult) pagination.Page { + p := ContainerPage{pagination.MarkerPageBase{PageResult: r}} + p.MarkerPageBase.Owner = p + return p + } + + pager := pagination.NewPager(c, url, createPage) + pager.Headers = headers + return pager +} + +// CreateOptsBuilder allows extensions to add additional parameters to the +// Create request. +type CreateOptsBuilder interface { + ToContainerCreateMap() (map[string]string, error) +} + +// CreateOpts is a structure that holds parameters for creating a container. +type CreateOpts struct { + Metadata map[string]string + ContainerRead string `h:"X-Container-Read"` + ContainerSyncTo string `h:"X-Container-Sync-To"` + ContainerSyncKey string `h:"X-Container-Sync-Key"` + ContainerWrite string `h:"X-Container-Write"` + ContentType string `h:"Content-Type"` + DetectContentType bool `h:"X-Detect-Content-Type"` + IfNoneMatch string `h:"If-None-Match"` + VersionsLocation string `h:"X-Versions-Location"` +} + +// ToContainerCreateMap formats a CreateOpts into a map of headers. +func (opts CreateOpts) ToContainerCreateMap() (map[string]string, error) { + h, err := gophercloud.BuildHeaders(opts) + if err != nil { + return nil, err + } + for k, v := range opts.Metadata { + h["X-Container-Meta-"+k] = v + } + return h, nil +} + +// Create is a function that creates a new container. +func Create(c *gophercloud.ServiceClient, containerName string, opts CreateOptsBuilder) CreateResult { + var res CreateResult + h := c.AuthenticatedHeaders() + + if opts != nil { + headers, err := opts.ToContainerCreateMap() + if err != nil { + res.Err = err + return res + } + + for k, v := range headers { + h[k] = v + } + } + + resp, err := perigee.Request("PUT", createURL(c, containerName), perigee.Options{ + MoreHeaders: h, + OkCodes: []int{201, 202, 204}, + }) + res.Header = resp.HttpResponse.Header + res.Err = err + return res +} + +// Delete is a function that deletes a container. +func Delete(c *gophercloud.ServiceClient, containerName string) DeleteResult { + var res DeleteResult + _, res.Err = perigee.Request("DELETE", deleteURL(c, containerName), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + OkCodes: []int{202, 204}, + }) + return res +} + +// UpdateOptsBuilder allows extensions to add additional parameters to the +// Update request. +type UpdateOptsBuilder interface { + ToContainerUpdateMap() (map[string]string, error) +} + +// UpdateOpts is a structure that holds parameters for updating, creating, or +// deleting a container's metadata. +type UpdateOpts struct { + Metadata map[string]string + ContainerRead string `h:"X-Container-Read"` + ContainerSyncTo string `h:"X-Container-Sync-To"` + ContainerSyncKey string `h:"X-Container-Sync-Key"` + ContainerWrite string `h:"X-Container-Write"` + ContentType string `h:"Content-Type"` + DetectContentType bool `h:"X-Detect-Content-Type"` + RemoveVersionsLocation string `h:"X-Remove-Versions-Location"` + VersionsLocation string `h:"X-Versions-Location"` +} + +// ToContainerUpdateMap formats a CreateOpts into a map of headers. +func (opts UpdateOpts) ToContainerUpdateMap() (map[string]string, error) { + h, err := gophercloud.BuildHeaders(opts) + if err != nil { + return nil, err + } + for k, v := range opts.Metadata { + h["X-Container-Meta-"+k] = v + } + return h, nil +} + +// Update is a function that creates, updates, or deletes a container's +// metadata. +func Update(c *gophercloud.ServiceClient, containerName string, opts UpdateOptsBuilder) UpdateResult { + var res UpdateResult + h := c.AuthenticatedHeaders() + + if opts != nil { + headers, err := opts.ToContainerUpdateMap() + if err != nil { + res.Err = err + return res + } + + for k, v := range headers { + h[k] = v + } + } + + resp, err := perigee.Request("POST", updateURL(c, containerName), perigee.Options{ + MoreHeaders: h, + OkCodes: []int{202, 204}, + }) + res.Header = resp.HttpResponse.Header + res.Err = err + return res +} + +// Get is a function that retrieves the metadata of a container. To extract just +// the custom metadata, pass the GetResult response to the ExtractMetadata +// function. +func Get(c *gophercloud.ServiceClient, containerName string) GetResult { + var res GetResult + resp, err := perigee.Request("HEAD", getURL(c, containerName), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + OkCodes: []int{200, 204}, + }) + res.Header = resp.HttpResponse.Header + res.Err = err + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers/requests_test.go new file mode 100644 index 0000000000..d0ce7f1e58 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers/requests_test.go @@ -0,0 +1,91 @@ +package containers + +import ( + "testing" + + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +var metadata = map[string]string{"gophercloud-test": "containers"} + +func TestListContainerInfo(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListContainerInfoSuccessfully(t) + + count := 0 + err := List(fake.ServiceClient(), &ListOpts{Full: true}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractInfo(page) + th.AssertNoErr(t, err) + + th.CheckDeepEquals(t, ExpectedListInfo, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, count, 1) +} + +func TestListContainerNames(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListContainerNamesSuccessfully(t) + + count := 0 + err := List(fake.ServiceClient(), &ListOpts{Full: false}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractNames(page) + if err != nil { + t.Errorf("Failed to extract container names: %v", err) + return false, err + } + + th.CheckDeepEquals(t, ExpectedListNames, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, count, 1) +} + +func TestCreateContainer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleCreateContainerSuccessfully(t) + + options := CreateOpts{ContentType: "application/json", Metadata: map[string]string{"foo": "bar"}} + res := Create(fake.ServiceClient(), "testContainer", options) + th.CheckNoErr(t, res.Err) + th.CheckEquals(t, "bar", res.Header["X-Container-Meta-Foo"][0]) +} + +func TestDeleteContainer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleDeleteContainerSuccessfully(t) + + res := Delete(fake.ServiceClient(), "testContainer") + th.CheckNoErr(t, res.Err) +} + +func TestUpateContainer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleUpdateContainerSuccessfully(t) + + options := &UpdateOpts{Metadata: map[string]string{"foo": "bar"}} + res := Update(fake.ServiceClient(), "testContainer", options) + th.CheckNoErr(t, res.Err) +} + +func TestGetContainer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleGetContainerSuccessfully(t) + + _, err := Get(fake.ServiceClient(), "testContainer").ExtractMetadata() + th.CheckNoErr(t, err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers/results.go new file mode 100644 index 0000000000..74f3286046 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers/results.go @@ -0,0 +1,139 @@ +package containers + +import ( + "fmt" + "strings" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" + + "github.com/mitchellh/mapstructure" +) + +// Container represents a container resource. +type Container struct { + // The total number of bytes stored in the container. + Bytes int `json:"bytes" mapstructure:"bytes"` + + // The total number of objects stored in the container. + Count int `json:"count" mapstructure:"count"` + + // The name of the container. + Name string `json:"name" mapstructure:"name"` +} + +// ContainerPage is the page returned by a pager when traversing over a +// collection of containers. +type ContainerPage struct { + pagination.MarkerPageBase +} + +// IsEmpty returns true if a ListResult contains no container names. +func (r ContainerPage) IsEmpty() (bool, error) { + names, err := ExtractNames(r) + if err != nil { + return true, err + } + return len(names) == 0, nil +} + +// LastMarker returns the last container name in a ListResult. +func (r ContainerPage) LastMarker() (string, error) { + names, err := ExtractNames(r) + if err != nil { + return "", err + } + if len(names) == 0 { + return "", nil + } + return names[len(names)-1], nil +} + +// ExtractInfo is a function that takes a ListResult and returns the containers' information. +func ExtractInfo(page pagination.Page) ([]Container, error) { + untyped := page.(ContainerPage).Body.([]interface{}) + results := make([]Container, len(untyped)) + for index, each := range untyped { + container := each.(map[string]interface{}) + err := mapstructure.Decode(container, &results[index]) + if err != nil { + return results, err + } + } + return results, nil +} + +// ExtractNames is a function that takes a ListResult and returns the containers' names. +func ExtractNames(page pagination.Page) ([]string, error) { + casted := page.(ContainerPage) + ct := casted.Header.Get("Content-Type") + + switch { + case strings.HasPrefix(ct, "application/json"): + parsed, err := ExtractInfo(page) + if err != nil { + return nil, err + } + + names := make([]string, 0, len(parsed)) + for _, container := range parsed { + names = append(names, container.Name) + } + return names, nil + case strings.HasPrefix(ct, "text/plain"): + names := make([]string, 0, 50) + + body := string(page.(ContainerPage).Body.([]uint8)) + for _, name := range strings.Split(body, "\n") { + if len(name) > 0 { + names = append(names, name) + } + } + + return names, nil + default: + return nil, fmt.Errorf("Cannot extract names from response with content-type: [%s]", ct) + } +} + +// GetResult represents the result of a get operation. +type GetResult struct { + gophercloud.HeaderResult +} + +// ExtractMetadata is a function that takes a GetResult (of type *http.Response) +// and returns the custom metadata associated with the container. +func (gr GetResult) ExtractMetadata() (map[string]string, error) { + if gr.Err != nil { + return nil, gr.Err + } + metadata := make(map[string]string) + for k, v := range gr.Header { + if strings.HasPrefix(k, "X-Container-Meta-") { + key := strings.TrimPrefix(k, "X-Container-Meta-") + metadata[key] = v[0] + } + } + return metadata, nil +} + +// CreateResult represents the result of a create operation. To extract the +// the headers from the HTTP response, you can invoke the 'ExtractHeader' +// method on the result struct. +type CreateResult struct { + gophercloud.HeaderResult +} + +// UpdateResult represents the result of an update operation. To extract the +// the headers from the HTTP response, you can invoke the 'ExtractHeader' +// method on the result struct. +type UpdateResult struct { + gophercloud.HeaderResult +} + +// DeleteResult represents the result of a delete operation. To extract the +// the headers from the HTTP response, you can invoke the 'ExtractHeader' +// method on the result struct. +type DeleteResult struct { + gophercloud.HeaderResult +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers/urls.go new file mode 100644 index 0000000000..f864f846eb --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers/urls.go @@ -0,0 +1,23 @@ +package containers + +import "github.com/rackspace/gophercloud" + +func listURL(c *gophercloud.ServiceClient) string { + return c.Endpoint +} + +func createURL(c *gophercloud.ServiceClient, container string) string { + return c.ServiceURL(container) +} + +func getURL(c *gophercloud.ServiceClient, container string) string { + return createURL(c, container) +} + +func deleteURL(c *gophercloud.ServiceClient, container string) string { + return createURL(c, container) +} + +func updateURL(c *gophercloud.ServiceClient, container string) string { + return createURL(c, container) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers/urls_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers/urls_test.go new file mode 100644 index 0000000000..d043a2aae5 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers/urls_test.go @@ -0,0 +1,43 @@ +package containers + +import ( + "github.com/rackspace/gophercloud" + th "github.com/rackspace/gophercloud/testhelper" + "testing" +) + +const endpoint = "http://localhost:57909/" + +func endpointClient() *gophercloud.ServiceClient { + return &gophercloud.ServiceClient{Endpoint: endpoint} +} + +func TestListURL(t *testing.T) { + actual := listURL(endpointClient()) + expected := endpoint + th.CheckEquals(t, expected, actual) +} + +func TestCreateURL(t *testing.T) { + actual := createURL(endpointClient(), "foo") + expected := endpoint + "foo" + th.CheckEquals(t, expected, actual) +} + +func TestGetURL(t *testing.T) { + actual := getURL(endpointClient(), "foo") + expected := endpoint + "foo" + th.CheckEquals(t, expected, actual) +} + +func TestDeleteURL(t *testing.T) { + actual := deleteURL(endpointClient(), "foo") + expected := endpoint + "foo" + th.CheckEquals(t, expected, actual) +} + +func TestUpdateURL(t *testing.T) { + actual := updateURL(endpointClient(), "foo") + expected := endpoint + "foo" + th.CheckEquals(t, expected, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects/doc.go new file mode 100644 index 0000000000..30a9adde1c --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects/doc.go @@ -0,0 +1,5 @@ +// Package objects contains functionality for working with Object Storage +// object resources. An object is a resource that represents and contains data +// - such as documents, images, and so on. You can also store custom metadata +// with an object. +package objects diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects/fixtures.go new file mode 100644 index 0000000000..d951160e3a --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects/fixtures.go @@ -0,0 +1,164 @@ +// +build fixtures + +package objects + +import ( + "fmt" + "net/http" + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +// HandleDownloadObjectSuccessfully creates an HTTP handler at `/testContainer/testObject` on the test handler mux that +// responds with a `Download` response. +func HandleDownloadObjectSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/testContainer/testObject", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, "Successful download with Gophercloud") + }) +} + +// ExpectedListInfo is the result expected from a call to `List` when full +// info is requested. +var ExpectedListInfo = []Object{ + Object{ + Hash: "451e372e48e0f6b1114fa0724aa79fa1", + LastModified: "2009-11-10 23:00:00 +0000 UTC", + Bytes: 14, + Name: "goodbye", + ContentType: "application/octet-stream", + }, + Object{ + Hash: "451e372e48e0f6b1114fa0724aa79fa1", + LastModified: "2009-11-10 23:00:00 +0000 UTC", + Bytes: 14, + Name: "hello", + ContentType: "application/octet-stream", + }, +} + +// ExpectedListNames is the result expected from a call to `List` when just +// object names are requested. +var ExpectedListNames = []string{"hello", "goodbye"} + +// HandleListObjectsInfoSuccessfully creates an HTTP handler at `/testContainer` on the test handler mux that +// responds with a `List` response when full info is requested. +func HandleListObjectsInfoSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/testContainer", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + + w.Header().Set("Content-Type", "application/json") + r.ParseForm() + marker := r.Form.Get("marker") + switch marker { + case "": + fmt.Fprintf(w, `[ + { + "hash": "451e372e48e0f6b1114fa0724aa79fa1", + "last_modified": "2009-11-10 23:00:00 +0000 UTC", + "bytes": 14, + "name": "goodbye", + "content_type": "application/octet-stream" + }, + { + "hash": "451e372e48e0f6b1114fa0724aa79fa1", + "last_modified": "2009-11-10 23:00:00 +0000 UTC", + "bytes": 14, + "name": "hello", + "content_type": "application/octet-stream" + } + ]`) + case "hello": + fmt.Fprintf(w, `[]`) + default: + t.Fatalf("Unexpected marker: [%s]", marker) + } + }) +} + +// HandleListObjectNamesSuccessfully creates an HTTP handler at `/testContainer` on the test handler mux that +// responds with a `List` response when only object names are requested. +func HandleListObjectNamesSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/testContainer", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "text/plain") + + w.Header().Set("Content-Type", "text/plain") + r.ParseForm() + marker := r.Form.Get("marker") + switch marker { + case "": + fmt.Fprintf(w, "hello\ngoodbye\n") + case "goodbye": + fmt.Fprintf(w, "") + default: + t.Fatalf("Unexpected marker: [%s]", marker) + } + }) +} + +// HandleCreateObjectSuccessfully creates an HTTP handler at `/testContainer/testObject` on the test handler mux that +// responds with a `Create` response. +func HandleCreateObjectSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/testContainer/testObject", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + w.WriteHeader(http.StatusCreated) + }) +} + +// HandleCopyObjectSuccessfully creates an HTTP handler at `/testContainer/testObject` on the test handler mux that +// responds with a `Copy` response. +func HandleCopyObjectSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/testContainer/testObject", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "COPY") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + th.TestHeader(t, r, "Destination", "/newTestContainer/newTestObject") + w.WriteHeader(http.StatusCreated) + }) +} + +// HandleDeleteObjectSuccessfully creates an HTTP handler at `/testContainer/testObject` on the test handler mux that +// responds with a `Delete` response. +func HandleDeleteObjectSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/testContainer/testObject", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + w.WriteHeader(http.StatusNoContent) + }) +} + +// HandleUpdateObjectSuccessfully creates an HTTP handler at `/testContainer/testObject` on the test handler mux that +// responds with a `Update` response. +func HandleUpdateObjectSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/testContainer/testObject", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + th.TestHeader(t, r, "X-Object-Meta-Gophercloud-Test", "objects") + w.WriteHeader(http.StatusAccepted) + }) +} + +// HandleGetObjectSuccessfully creates an HTTP handler at `/testContainer/testObject` on the test handler mux that +// responds with a `Get` response. +func HandleGetObjectSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/testContainer/testObject", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "HEAD") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + w.Header().Add("X-Object-Meta-Gophercloud-Test", "objects") + w.WriteHeader(http.StatusNoContent) + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects/requests.go new file mode 100644 index 0000000000..7b96fa2fe5 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects/requests.go @@ -0,0 +1,420 @@ +package objects + +import ( + "fmt" + "io" + "time" + + "github.com/racker/perigee" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to the List +// request. +type ListOptsBuilder interface { + ToObjectListParams() (bool, string, error) +} + +// ListOpts is a structure that holds parameters for listing objects. +type ListOpts struct { + // Full is a true/false value that represents the amount of object information + // returned. If Full is set to true, then the content-type, number of bytes, hash + // date last modified, and name are returned. If set to false or not set, then + // only the object names are returned. + Full bool + Limit int `q:"limit"` + Marker string `q:"marker"` + EndMarker string `q:"end_marker"` + Format string `q:"format"` + Prefix string `q:"prefix"` + Delimiter string `q:"delimiter"` + Path string `q:"path"` +} + +// ToObjectListParams formats a ListOpts into a query string and boolean +// representing whether to list complete information for each object. +func (opts ListOpts) ToObjectListParams() (bool, string, error) { + q, err := gophercloud.BuildQueryString(opts) + if err != nil { + return false, "", err + } + return opts.Full, q.String(), nil +} + +// List is a function that retrieves all objects in a container. It also returns the details +// for the container. To extract only the object information or names, pass the ListResult +// response to the ExtractInfo or ExtractNames function, respectively. +func List(c *gophercloud.ServiceClient, containerName string, opts ListOptsBuilder) pagination.Pager { + headers := map[string]string{"Accept": "text/plain", "Content-Type": "text/plain"} + + url := listURL(c, containerName) + if opts != nil { + full, query, err := opts.ToObjectListParams() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + + if full { + headers = map[string]string{"Accept": "application/json", "Content-Type": "application/json"} + } + } + + createPage := func(r pagination.PageResult) pagination.Page { + p := ObjectPage{pagination.MarkerPageBase{PageResult: r}} + p.MarkerPageBase.Owner = p + return p + } + + pager := pagination.NewPager(c, url, createPage) + pager.Headers = headers + return pager +} + +// DownloadOptsBuilder allows extensions to add additional parameters to the +// Download request. +type DownloadOptsBuilder interface { + ToObjectDownloadParams() (map[string]string, string, error) +} + +// DownloadOpts is a structure that holds parameters for downloading an object. +type DownloadOpts struct { + IfMatch string `h:"If-Match"` + IfModifiedSince time.Time `h:"If-Modified-Since"` + IfNoneMatch string `h:"If-None-Match"` + IfUnmodifiedSince time.Time `h:"If-Unmodified-Since"` + Range string `h:"Range"` + Expires string `q:"expires"` + MultipartManifest string `q:"multipart-manifest"` + Signature string `q:"signature"` +} + +// ToObjectDownloadParams formats a DownloadOpts into a query string and map of +// headers. +func (opts DownloadOpts) ToObjectDownloadParams() (map[string]string, string, error) { + q, err := gophercloud.BuildQueryString(opts) + if err != nil { + return nil, "", err + } + h, err := gophercloud.BuildHeaders(opts) + if err != nil { + return nil, q.String(), err + } + return h, q.String(), nil +} + +// Download is a function that retrieves the content and metadata for an object. +// To extract just the content, pass the DownloadResult response to the +// ExtractContent function. +func Download(c *gophercloud.ServiceClient, containerName, objectName string, opts DownloadOptsBuilder) DownloadResult { + var res DownloadResult + + url := downloadURL(c, containerName, objectName) + h := c.AuthenticatedHeaders() + + if opts != nil { + headers, query, err := opts.ToObjectDownloadParams() + if err != nil { + res.Err = err + return res + } + + for k, v := range headers { + h[k] = v + } + + url += query + } + + resp, err := perigee.Request("GET", url, perigee.Options{ + MoreHeaders: h, + OkCodes: []int{200, 304}, + }) + + res.Body = resp.HttpResponse.Body + res.Err = err + res.Header = resp.HttpResponse.Header + + return res +} + +// CreateOptsBuilder allows extensions to add additional parameters to the +// Create request. +type CreateOptsBuilder interface { + ToObjectCreateParams() (map[string]string, string, error) +} + +// CreateOpts is a structure that holds parameters for creating an object. +type CreateOpts struct { + Metadata map[string]string + ContentDisposition string `h:"Content-Disposition"` + ContentEncoding string `h:"Content-Encoding"` + ContentLength int64 `h:"Content-Length"` + ContentType string `h:"Content-Type"` + CopyFrom string `h:"X-Copy-From"` + DeleteAfter int `h:"X-Delete-After"` + DeleteAt int `h:"X-Delete-At"` + DetectContentType string `h:"X-Detect-Content-Type"` + ETag string `h:"ETag"` + IfNoneMatch string `h:"If-None-Match"` + ObjectManifest string `h:"X-Object-Manifest"` + TransferEncoding string `h:"Transfer-Encoding"` + Expires string `q:"expires"` + MultipartManifest string `q:"multiple-manifest"` + Signature string `q:"signature"` +} + +// ToObjectCreateParams formats a CreateOpts into a query string and map of +// headers. +func (opts CreateOpts) ToObjectCreateParams() (map[string]string, string, error) { + q, err := gophercloud.BuildQueryString(opts) + if err != nil { + return nil, "", err + } + h, err := gophercloud.BuildHeaders(opts) + if err != nil { + return nil, q.String(), err + } + + for k, v := range opts.Metadata { + h["X-Object-Meta-"+k] = v + } + + return h, q.String(), nil +} + +// Create is a function that creates a new object or replaces an existing object. +func Create(c *gophercloud.ServiceClient, containerName, objectName string, content io.Reader, opts CreateOptsBuilder) CreateResult { + var res CreateResult + + url := createURL(c, containerName, objectName) + h := c.AuthenticatedHeaders() + + if opts != nil { + headers, query, err := opts.ToObjectCreateParams() + if err != nil { + res.Err = err + return res + } + + for k, v := range headers { + h[k] = v + } + + url += query + } + + contentType := h["Content-Type"] + + resp, err := perigee.Request("PUT", url, perigee.Options{ + ContentType: contentType, + ReqBody: content, + MoreHeaders: h, + OkCodes: []int{201, 202}, + }) + res.Header = resp.HttpResponse.Header + res.Err = err + return res +} + +// CopyOptsBuilder allows extensions to add additional parameters to the +// Copy request. +type CopyOptsBuilder interface { + ToObjectCopyMap() (map[string]string, error) +} + +// CopyOpts is a structure that holds parameters for copying one object to +// another. +type CopyOpts struct { + Metadata map[string]string + ContentDisposition string `h:"Content-Disposition"` + ContentEncoding string `h:"Content-Encoding"` + ContentType string `h:"Content-Type"` + Destination string `h:"Destination,required"` +} + +// ToObjectCopyMap formats a CopyOpts into a map of headers. +func (opts CopyOpts) ToObjectCopyMap() (map[string]string, error) { + if opts.Destination == "" { + return nil, fmt.Errorf("Required CopyOpts field 'Destination' not set.") + } + h, err := gophercloud.BuildHeaders(opts) + if err != nil { + return nil, err + } + for k, v := range opts.Metadata { + h["X-Object-Meta-"+k] = v + } + return h, nil +} + +// Copy is a function that copies one object to another. +func Copy(c *gophercloud.ServiceClient, containerName, objectName string, opts CopyOptsBuilder) CopyResult { + var res CopyResult + h := c.AuthenticatedHeaders() + + headers, err := opts.ToObjectCopyMap() + if err != nil { + res.Err = err + return res + } + + for k, v := range headers { + h[k] = v + } + + url := copyURL(c, containerName, objectName) + resp, err := perigee.Request("COPY", url, perigee.Options{ + MoreHeaders: h, + OkCodes: []int{201}, + }) + res.Header = resp.HttpResponse.Header + res.Err = err + return res +} + +// DeleteOptsBuilder allows extensions to add additional parameters to the +// Delete request. +type DeleteOptsBuilder interface { + ToObjectDeleteQuery() (string, error) +} + +// DeleteOpts is a structure that holds parameters for deleting an object. +type DeleteOpts struct { + MultipartManifest string `q:"multipart-manifest"` +} + +// ToObjectDeleteQuery formats a DeleteOpts into a query string. +func (opts DeleteOpts) ToObjectDeleteQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + if err != nil { + return "", err + } + return q.String(), nil +} + +// Delete is a function that deletes an object. +func Delete(c *gophercloud.ServiceClient, containerName, objectName string, opts DeleteOptsBuilder) DeleteResult { + var res DeleteResult + url := deleteURL(c, containerName, objectName) + + if opts != nil { + query, err := opts.ToObjectDeleteQuery() + if err != nil { + res.Err = err + return res + } + url += query + } + + resp, err := perigee.Request("DELETE", url, perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + OkCodes: []int{204}, + }) + res.Header = resp.HttpResponse.Header + res.Err = err + return res +} + +// GetOptsBuilder allows extensions to add additional parameters to the +// Get request. +type GetOptsBuilder interface { + ToObjectGetQuery() (string, error) +} + +// GetOpts is a structure that holds parameters for getting an object's metadata. +type GetOpts struct { + Expires string `q:"expires"` + Signature string `q:"signature"` +} + +// ToObjectGetQuery formats a GetOpts into a query string. +func (opts GetOpts) ToObjectGetQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + if err != nil { + return "", err + } + return q.String(), nil +} + +// Get is a function that retrieves the metadata of an object. To extract just the custom +// metadata, pass the GetResult response to the ExtractMetadata function. +func Get(c *gophercloud.ServiceClient, containerName, objectName string, opts GetOptsBuilder) GetResult { + var res GetResult + url := getURL(c, containerName, objectName) + + if opts != nil { + query, err := opts.ToObjectGetQuery() + if err != nil { + res.Err = err + return res + } + url += query + } + + resp, err := perigee.Request("HEAD", url, perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + OkCodes: []int{200, 204}, + }) + res.Header = resp.HttpResponse.Header + res.Err = err + return res +} + +// UpdateOptsBuilder allows extensions to add additional parameters to the +// Update request. +type UpdateOptsBuilder interface { + ToObjectUpdateMap() (map[string]string, error) +} + +// UpdateOpts is a structure that holds parameters for updating, creating, or deleting an +// object's metadata. +type UpdateOpts struct { + Metadata map[string]string + ContentDisposition string `h:"Content-Disposition"` + ContentEncoding string `h:"Content-Encoding"` + ContentType string `h:"Content-Type"` + DeleteAfter int `h:"X-Delete-After"` + DeleteAt int `h:"X-Delete-At"` + DetectContentType bool `h:"X-Detect-Content-Type"` +} + +// ToObjectUpdateMap formats a UpdateOpts into a map of headers. +func (opts UpdateOpts) ToObjectUpdateMap() (map[string]string, error) { + h, err := gophercloud.BuildHeaders(opts) + if err != nil { + return nil, err + } + for k, v := range opts.Metadata { + h["X-Object-Meta-"+k] = v + } + return h, nil +} + +// Update is a function that creates, updates, or deletes an object's metadata. +func Update(c *gophercloud.ServiceClient, containerName, objectName string, opts UpdateOptsBuilder) UpdateResult { + var res UpdateResult + h := c.AuthenticatedHeaders() + + if opts != nil { + headers, err := opts.ToObjectUpdateMap() + if err != nil { + res.Err = err + return res + } + + for k, v := range headers { + h[k] = v + } + } + + url := updateURL(c, containerName, objectName) + resp, err := perigee.Request("POST", url, perigee.Options{ + MoreHeaders: h, + OkCodes: []int{202}, + }) + res.Header = resp.HttpResponse.Header + res.Err = err + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects/requests_test.go new file mode 100644 index 0000000000..c3c28a789b --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects/requests_test.go @@ -0,0 +1,132 @@ +package objects + +import ( + "bytes" + "io" + "testing" + + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestDownloadReader(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleDownloadObjectSuccessfully(t) + + response := Download(fake.ServiceClient(), "testContainer", "testObject", nil) + defer response.Body.Close() + + // Check reader + buf := bytes.NewBuffer(make([]byte, 0)) + io.CopyN(buf, response.Body, 10) + th.CheckEquals(t, "Successful", string(buf.Bytes())) +} + +func TestDownloadExtraction(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleDownloadObjectSuccessfully(t) + + response := Download(fake.ServiceClient(), "testContainer", "testObject", nil) + + // Check []byte extraction + bytes, err := response.ExtractContent() + th.AssertNoErr(t, err) + th.CheckEquals(t, "Successful download with Gophercloud", string(bytes)) +} + +func TestListObjectInfo(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListObjectsInfoSuccessfully(t) + + count := 0 + options := &ListOpts{Full: true} + err := List(fake.ServiceClient(), "testContainer", options).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractInfo(page) + th.AssertNoErr(t, err) + + th.CheckDeepEquals(t, ExpectedListInfo, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, count, 1) +} + +func TestListObjectNames(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListObjectNamesSuccessfully(t) + + count := 0 + options := &ListOpts{Full: false} + err := List(fake.ServiceClient(), "testContainer", options).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractNames(page) + if err != nil { + t.Errorf("Failed to extract container names: %v", err) + return false, err + } + + th.CheckDeepEquals(t, ExpectedListNames, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, count, 1) +} + +func TestCreateObject(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleCreateObjectSuccessfully(t) + + content := bytes.NewBufferString("Did gyre and gimble in the wabe") + options := &CreateOpts{ContentType: "application/json"} + res := Create(fake.ServiceClient(), "testContainer", "testObject", content, options) + th.AssertNoErr(t, res.Err) +} + +func TestCopyObject(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleCopyObjectSuccessfully(t) + + options := &CopyOpts{Destination: "/newTestContainer/newTestObject"} + res := Copy(fake.ServiceClient(), "testContainer", "testObject", options) + th.AssertNoErr(t, res.Err) +} + +func TestDeleteObject(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleDeleteObjectSuccessfully(t) + + res := Delete(fake.ServiceClient(), "testContainer", "testObject", nil) + th.AssertNoErr(t, res.Err) +} + +func TestUpateObjectMetadata(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleUpdateObjectSuccessfully(t) + + options := &UpdateOpts{Metadata: map[string]string{"Gophercloud-Test": "objects"}} + res := Update(fake.ServiceClient(), "testContainer", "testObject", options) + th.AssertNoErr(t, res.Err) +} + +func TestGetObject(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleGetObjectSuccessfully(t) + + expected := map[string]string{"Gophercloud-Test": "objects"} + actual, err := Get(fake.ServiceClient(), "testContainer", "testObject", nil).ExtractMetadata() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, expected, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects/results.go new file mode 100644 index 0000000000..b51b840c99 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects/results.go @@ -0,0 +1,173 @@ +package objects + +import ( + "fmt" + "io" + "io/ioutil" + "strings" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" + + "github.com/mitchellh/mapstructure" +) + +// Object is a structure that holds information related to a storage object. +type Object struct { + // Bytes is the total number of bytes that comprise the object. + Bytes int64 `json:"bytes" mapstructure:"bytes"` + + // ContentType is the content type of the object. + ContentType string `json:"content_type" mapstructure:"content_type"` + + // Hash represents the MD5 checksum value of the object's content. + Hash string `json:"hash" mapstructure:"hash"` + + // LastModified is the RFC3339Milli time the object was last modified, represented + // as a string. For any given object (obj), this value may be parsed to a time.Time: + // lastModified, err := time.Parse(gophercloud.RFC3339Milli, obj.LastModified) + LastModified string `json:"last_modified" mapstructure:"last_modified"` + + // Name is the unique name for the object. + Name string `json:"name" mapstructure:"name"` +} + +// ObjectPage is a single page of objects that is returned from a call to the +// List function. +type ObjectPage struct { + pagination.MarkerPageBase +} + +// IsEmpty returns true if a ListResult contains no object names. +func (r ObjectPage) IsEmpty() (bool, error) { + names, err := ExtractNames(r) + if err != nil { + return true, err + } + return len(names) == 0, nil +} + +// LastMarker returns the last object name in a ListResult. +func (r ObjectPage) LastMarker() (string, error) { + names, err := ExtractNames(r) + if err != nil { + return "", err + } + if len(names) == 0 { + return "", nil + } + return names[len(names)-1], nil +} + +// ExtractInfo is a function that takes a page of objects and returns their full information. +func ExtractInfo(page pagination.Page) ([]Object, error) { + untyped := page.(ObjectPage).Body.([]interface{}) + results := make([]Object, len(untyped)) + for index, each := range untyped { + object := each.(map[string]interface{}) + err := mapstructure.Decode(object, &results[index]) + if err != nil { + return results, err + } + } + return results, nil +} + +// ExtractNames is a function that takes a page of objects and returns only their names. +func ExtractNames(page pagination.Page) ([]string, error) { + casted := page.(ObjectPage) + ct := casted.Header.Get("Content-Type") + switch { + case strings.HasPrefix(ct, "application/json"): + parsed, err := ExtractInfo(page) + if err != nil { + return nil, err + } + + names := make([]string, 0, len(parsed)) + for _, object := range parsed { + names = append(names, object.Name) + } + + return names, nil + case strings.HasPrefix(ct, "text/plain"): + names := make([]string, 0, 50) + + body := string(page.(ObjectPage).Body.([]uint8)) + for _, name := range strings.Split(body, "\n") { + if len(name) > 0 { + names = append(names, name) + } + } + + return names, nil + case strings.HasPrefix(ct, "text/html"): + return []string{}, nil + default: + return nil, fmt.Errorf("Cannot extract names from response with content-type: [%s]", ct) + } +} + +// DownloadResult is a *http.Response that is returned from a call to the Download function. +type DownloadResult struct { + gophercloud.HeaderResult + Body io.ReadCloser +} + +// ExtractContent is a function that takes a DownloadResult's io.Reader body +// and reads all available data into a slice of bytes. Please be aware that due +// the nature of io.Reader is forward-only - meaning that it can only be read +// once and not rewound. You can recreate a reader from the output of this +// function by using bytes.NewReader(downloadBytes) +func (dr DownloadResult) ExtractContent() ([]byte, error) { + if dr.Err != nil { + return nil, dr.Err + } + body, err := ioutil.ReadAll(dr.Body) + if err != nil { + return nil, err + } + dr.Body.Close() + return body, nil +} + +// GetResult is a *http.Response that is returned from a call to the Get function. +type GetResult struct { + gophercloud.HeaderResult +} + +// ExtractMetadata is a function that takes a GetResult (of type *http.Response) +// and returns the custom metadata associated with the object. +func (gr GetResult) ExtractMetadata() (map[string]string, error) { + if gr.Err != nil { + return nil, gr.Err + } + metadata := make(map[string]string) + for k, v := range gr.Header { + if strings.HasPrefix(k, "X-Object-Meta-") { + key := strings.TrimPrefix(k, "X-Object-Meta-") + metadata[key] = v[0] + } + } + return metadata, nil +} + +// CreateResult represents the result of a create operation. +type CreateResult struct { + gophercloud.HeaderResult +} + +// UpdateResult represents the result of an update operation. +type UpdateResult struct { + gophercloud.HeaderResult +} + +// DeleteResult represents the result of a delete operation. +type DeleteResult struct { + gophercloud.HeaderResult +} + +// CopyResult represents the result of a copy operation. +type CopyResult struct { + gophercloud.HeaderResult +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects/urls.go new file mode 100644 index 0000000000..d2ec62cff2 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects/urls.go @@ -0,0 +1,33 @@ +package objects + +import ( + "github.com/rackspace/gophercloud" +) + +func listURL(c *gophercloud.ServiceClient, container string) string { + return c.ServiceURL(container) +} + +func copyURL(c *gophercloud.ServiceClient, container, object string) string { + return c.ServiceURL(container, object) +} + +func createURL(c *gophercloud.ServiceClient, container, object string) string { + return copyURL(c, container, object) +} + +func getURL(c *gophercloud.ServiceClient, container, object string) string { + return copyURL(c, container, object) +} + +func deleteURL(c *gophercloud.ServiceClient, container, object string) string { + return copyURL(c, container, object) +} + +func downloadURL(c *gophercloud.ServiceClient, container, object string) string { + return copyURL(c, container, object) +} + +func updateURL(c *gophercloud.ServiceClient, container, object string) string { + return copyURL(c, container, object) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects/urls_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects/urls_test.go new file mode 100644 index 0000000000..1dcfe3543c --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects/urls_test.go @@ -0,0 +1,56 @@ +package objects + +import ( + "testing" + + "github.com/rackspace/gophercloud" + th "github.com/rackspace/gophercloud/testhelper" +) + +const endpoint = "http://localhost:57909/" + +func endpointClient() *gophercloud.ServiceClient { + return &gophercloud.ServiceClient{Endpoint: endpoint} +} + +func TestListURL(t *testing.T) { + actual := listURL(endpointClient(), "foo") + expected := endpoint + "foo" + th.CheckEquals(t, expected, actual) +} + +func TestCopyURL(t *testing.T) { + actual := copyURL(endpointClient(), "foo", "bar") + expected := endpoint + "foo/bar" + th.CheckEquals(t, expected, actual) +} + +func TestCreateURL(t *testing.T) { + actual := createURL(endpointClient(), "foo", "bar") + expected := endpoint + "foo/bar" + th.CheckEquals(t, expected, actual) +} + +func TestGetURL(t *testing.T) { + actual := getURL(endpointClient(), "foo", "bar") + expected := endpoint + "foo/bar" + th.CheckEquals(t, expected, actual) +} + +func TestDeleteURL(t *testing.T) { + actual := deleteURL(endpointClient(), "foo", "bar") + expected := endpoint + "foo/bar" + th.CheckEquals(t, expected, actual) +} + +func TestDownloadURL(t *testing.T) { + actual := downloadURL(endpointClient(), "foo", "bar") + expected := endpoint + "foo/bar" + th.CheckEquals(t, expected, actual) +} + +func TestUpdateURL(t *testing.T) { + actual := updateURL(endpointClient(), "foo", "bar") + expected := endpoint + "foo/bar" + th.CheckEquals(t, expected, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/utils/choose_version.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/utils/choose_version.go new file mode 100644 index 0000000000..a0d5b26468 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/utils/choose_version.go @@ -0,0 +1,114 @@ +package utils + +import ( + "fmt" + "strings" + + "github.com/racker/perigee" +) + +// Version is a supported API version, corresponding to a vN package within the appropriate service. +type Version struct { + ID string + Suffix string + Priority int +} + +var goodStatus = map[string]bool{ + "current": true, + "supported": true, + "stable": true, +} + +// ChooseVersion queries the base endpoint of an API to choose the most recent non-experimental alternative from a service's +// published versions. +// It returns the highest-Priority Version among the alternatives that are provided, as well as its corresponding endpoint. +func ChooseVersion(identityBase string, identityEndpoint string, recognized []*Version) (*Version, string, error) { + type linkResp struct { + Href string `json:"href"` + Rel string `json:"rel"` + } + + type valueResp struct { + ID string `json:"id"` + Status string `json:"status"` + Links []linkResp `json:"links"` + } + + type versionsResp struct { + Values []valueResp `json:"values"` + } + + type response struct { + Versions versionsResp `json:"versions"` + } + + normalize := func(endpoint string) string { + if !strings.HasSuffix(endpoint, "/") { + return endpoint + "/" + } + return endpoint + } + identityEndpoint = normalize(identityEndpoint) + + // If a full endpoint is specified, check version suffixes for a match first. + for _, v := range recognized { + if strings.HasSuffix(identityEndpoint, v.Suffix) { + return v, identityEndpoint, nil + } + } + + var resp response + _, err := perigee.Request("GET", identityBase, perigee.Options{ + Results: &resp, + OkCodes: []int{200, 300}, + }) + + if err != nil { + return nil, "", err + } + + byID := make(map[string]*Version) + for _, version := range recognized { + byID[version.ID] = version + } + + var highest *Version + var endpoint string + + for _, value := range resp.Versions.Values { + href := "" + for _, link := range value.Links { + if link.Rel == "self" { + href = normalize(link.Href) + } + } + + if matching, ok := byID[value.ID]; ok { + // Prefer a version that exactly matches the provided endpoint. + if href == identityEndpoint { + if href == "" { + return nil, "", fmt.Errorf("Endpoint missing in version %s response from %s", value.ID, identityBase) + } + return matching, href, nil + } + + // Otherwise, find the highest-priority version with a whitelisted status. + if goodStatus[strings.ToLower(value.Status)] { + if highest == nil || matching.Priority > highest.Priority { + highest = matching + endpoint = href + } + } + } + } + + if highest == nil { + return nil, "", fmt.Errorf("No supported version available from endpoint %s", identityBase) + } + if endpoint == "" { + return nil, "", fmt.Errorf("Endpoint missing in version %s response from %s", highest.ID, identityBase) + } + + return highest, endpoint, nil +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/utils/choose_version_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/utils/choose_version_test.go new file mode 100644 index 0000000000..9552696232 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/utils/choose_version_test.go @@ -0,0 +1,105 @@ +package utils + +import ( + "fmt" + "net/http" + "testing" + + "github.com/rackspace/gophercloud/testhelper" +) + +func setupVersionHandler() { + testhelper.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, ` + { + "versions": { + "values": [ + { + "status": "stable", + "id": "v3.0", + "links": [ + { "href": "%s/v3.0", "rel": "self" } + ] + }, + { + "status": "stable", + "id": "v2.0", + "links": [ + { "href": "%s/v2.0", "rel": "self" } + ] + } + ] + } + } + `, testhelper.Server.URL, testhelper.Server.URL) + }) +} + +func TestChooseVersion(t *testing.T) { + testhelper.SetupHTTP() + defer testhelper.TeardownHTTP() + setupVersionHandler() + + v2 := &Version{ID: "v2.0", Priority: 2, Suffix: "blarg"} + v3 := &Version{ID: "v3.0", Priority: 3, Suffix: "hargl"} + + v, endpoint, err := ChooseVersion(testhelper.Endpoint(), "", []*Version{v2, v3}) + + if err != nil { + t.Fatalf("Unexpected error from ChooseVersion: %v", err) + } + + if v != v3 { + t.Errorf("Expected %#v to win, but %#v did instead", v3, v) + } + + expected := testhelper.Endpoint() + "v3.0/" + if endpoint != expected { + t.Errorf("Expected endpoint [%s], but was [%s] instead", expected, endpoint) + } +} + +func TestChooseVersionOpinionatedLink(t *testing.T) { + testhelper.SetupHTTP() + defer testhelper.TeardownHTTP() + setupVersionHandler() + + v2 := &Version{ID: "v2.0", Priority: 2, Suffix: "nope"} + v3 := &Version{ID: "v3.0", Priority: 3, Suffix: "northis"} + + v, endpoint, err := ChooseVersion(testhelper.Endpoint(), testhelper.Endpoint()+"v2.0/", []*Version{v2, v3}) + if err != nil { + t.Fatalf("Unexpected error from ChooseVersion: %v", err) + } + + if v != v2 { + t.Errorf("Expected %#v to win, but %#v did instead", v2, v) + } + + expected := testhelper.Endpoint() + "v2.0/" + if endpoint != expected { + t.Errorf("Expected endpoint [%s], but was [%s] instead", expected, endpoint) + } +} + +func TestChooseVersionFromSuffix(t *testing.T) { + testhelper.SetupHTTP() + defer testhelper.TeardownHTTP() + + v2 := &Version{ID: "v2.0", Priority: 2, Suffix: "/v2.0/"} + v3 := &Version{ID: "v3.0", Priority: 3, Suffix: "/v3.0/"} + + v, endpoint, err := ChooseVersion(testhelper.Endpoint(), testhelper.Endpoint()+"v2.0/", []*Version{v2, v3}) + if err != nil { + t.Fatalf("Unexpected error from ChooseVersion: %v", err) + } + + if v != v2 { + t.Errorf("Expected %#v to win, but %#v did instead", v2, v) + } + + expected := testhelper.Endpoint() + "v2.0/" + if endpoint != expected { + t.Errorf("Expected endpoint [%s], but was [%s] instead", expected, endpoint) + } +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/pagination/http.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/pagination/http.go new file mode 100644 index 0000000000..1e108c8039 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/pagination/http.go @@ -0,0 +1,64 @@ +package pagination + +import ( + "encoding/json" + "io/ioutil" + "net/http" + "net/url" + "strings" + + "github.com/racker/perigee" + "github.com/rackspace/gophercloud" +) + +// PageResult stores the HTTP response that returned the current page of results. +type PageResult struct { + gophercloud.Result + url.URL +} + +// PageResultFrom parses an HTTP response as JSON and returns a PageResult containing the +// results, interpreting it as JSON if the content type indicates. +func PageResultFrom(resp http.Response) (PageResult, error) { + var parsedBody interface{} + + defer resp.Body.Close() + rawBody, err := ioutil.ReadAll(resp.Body) + if err != nil { + return PageResult{}, err + } + + if strings.HasPrefix(resp.Header.Get("Content-Type"), "application/json") { + err = json.Unmarshal(rawBody, &parsedBody) + if err != nil { + return PageResult{}, err + } + } else { + parsedBody = rawBody + } + + return PageResult{ + Result: gophercloud.Result{ + Body: parsedBody, + Header: resp.Header, + }, + URL: *resp.Request.URL, + }, err +} + +// Request performs a Perigee request and extracts the http.Response from the result. +func Request(client *gophercloud.ServiceClient, headers map[string]string, url string) (http.Response, error) { + h := client.AuthenticatedHeaders() + for key, value := range headers { + h[key] = value + } + + resp, err := perigee.Request("GET", url, perigee.Options{ + MoreHeaders: h, + OkCodes: []int{200, 204}, + }) + if err != nil { + return http.Response{}, err + } + return resp.HttpResponse, nil +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/pagination/linked.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/pagination/linked.go new file mode 100644 index 0000000000..461fa499af --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/pagination/linked.go @@ -0,0 +1,61 @@ +package pagination + +import "fmt" + +// LinkedPageBase may be embedded to implement a page that provides navigational "Next" and "Previous" links within its result. +type LinkedPageBase struct { + PageResult + + // LinkPath lists the keys that should be traversed within a response to arrive at the "next" pointer. + // If any link along the path is missing, an empty URL will be returned. + // If any link results in an unexpected value type, an error will be returned. + // When left as "nil", []string{"links", "next"} will be used as a default. + LinkPath []string +} + +// NextPageURL extracts the pagination structure from a JSON response and returns the "next" link, if one is present. +// It assumes that the links are available in a "links" element of the top-level response object. +// If this is not the case, override NextPageURL on your result type. +func (current LinkedPageBase) NextPageURL() (string, error) { + var path []string + var key string + + if current.LinkPath == nil { + path = []string{"links", "next"} + } else { + path = current.LinkPath + } + + submap, ok := current.Body.(map[string]interface{}) + if !ok { + return "", fmt.Errorf("Expected an object, but was %#v", current.Body) + } + + for { + key, path = path[0], path[1:len(path)] + + value, ok := submap[key] + if !ok { + return "", nil + } + + if len(path) > 0 { + submap, ok = value.(map[string]interface{}) + if !ok { + return "", fmt.Errorf("Expected an object, but was %#v", value) + } + } else { + if value == nil { + // Actual null element. + return "", nil + } + + url, ok := value.(string) + if !ok { + return "", fmt.Errorf("Expected a string, but was %#v", value) + } + + return url, nil + } + } +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/pagination/linked_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/pagination/linked_test.go new file mode 100644 index 0000000000..4d3248e6ac --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/pagination/linked_test.go @@ -0,0 +1,107 @@ +package pagination + +import ( + "fmt" + "net/http" + "reflect" + "testing" + + "github.com/mitchellh/mapstructure" + "github.com/rackspace/gophercloud/testhelper" +) + +// LinkedPager sample and test cases. + +type LinkedPageResult struct { + LinkedPageBase +} + +func (r LinkedPageResult) IsEmpty() (bool, error) { + is, err := ExtractLinkedInts(r) + if err != nil { + return true, nil + } + return len(is) == 0, nil +} + +func ExtractLinkedInts(page Page) ([]int, error) { + var response struct { + Ints []int `mapstructure:"ints"` + } + + err := mapstructure.Decode(page.(LinkedPageResult).Body, &response) + if err != nil { + return nil, err + } + + return response.Ints, nil +} + +func createLinked(t *testing.T) Pager { + testhelper.SetupHTTP() + + testhelper.Mux.HandleFunc("/page1", func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, `{ "ints": [1, 2, 3], "links": { "next": "%s/page2" } }`, testhelper.Server.URL) + }) + + testhelper.Mux.HandleFunc("/page2", func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, `{ "ints": [4, 5, 6], "links": { "next": "%s/page3" } }`, testhelper.Server.URL) + }) + + testhelper.Mux.HandleFunc("/page3", func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, `{ "ints": [7, 8, 9], "links": { "next": null } }`) + }) + + client := createClient() + + createPage := func(r PageResult) Page { + return LinkedPageResult{LinkedPageBase{PageResult: r}} + } + + return NewPager(client, testhelper.Server.URL+"/page1", createPage) +} + +func TestEnumerateLinked(t *testing.T) { + pager := createLinked(t) + defer testhelper.TeardownHTTP() + + callCount := 0 + err := pager.EachPage(func(page Page) (bool, error) { + actual, err := ExtractLinkedInts(page) + if err != nil { + return false, err + } + + t.Logf("Handler invoked with %v", actual) + + var expected []int + switch callCount { + case 0: + expected = []int{1, 2, 3} + case 1: + expected = []int{4, 5, 6} + case 2: + expected = []int{7, 8, 9} + default: + t.Fatalf("Unexpected call count: %d", callCount) + return false, nil + } + + if !reflect.DeepEqual(expected, actual) { + t.Errorf("Call %d: Expected %#v, but was %#v", callCount, expected, actual) + } + + callCount++ + return true, nil + }) + if err != nil { + t.Errorf("Unexpected error for page iteration: %v", err) + } + + if callCount != 3 { + t.Errorf("Expected 3 calls, but was %d", callCount) + } +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/pagination/marker.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/pagination/marker.go new file mode 100644 index 0000000000..e7688c217b --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/pagination/marker.go @@ -0,0 +1,34 @@ +package pagination + +// MarkerPage is a stricter Page interface that describes additional functionality required for use with NewMarkerPager. +// For convenience, embed the MarkedPageBase struct. +type MarkerPage interface { + Page + + // LastMarker returns the last "marker" value on this page. + LastMarker() (string, error) +} + +// MarkerPageBase is a page in a collection that's paginated by "limit" and "marker" query parameters. +type MarkerPageBase struct { + PageResult + + // Owner is a reference to the embedding struct. + Owner MarkerPage +} + +// NextPageURL generates the URL for the page of results after this one. +func (current MarkerPageBase) NextPageURL() (string, error) { + currentURL := current.URL + + mark, err := current.Owner.LastMarker() + if err != nil { + return "", err + } + + q := currentURL.Query() + q.Set("marker", mark) + currentURL.RawQuery = q.Encode() + + return currentURL.String(), nil +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/pagination/marker_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/pagination/marker_test.go new file mode 100644 index 0000000000..3b1df1d68b --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/pagination/marker_test.go @@ -0,0 +1,113 @@ +package pagination + +import ( + "fmt" + "net/http" + "strings" + "testing" + + "github.com/rackspace/gophercloud/testhelper" +) + +// MarkerPager sample and test cases. + +type MarkerPageResult struct { + MarkerPageBase +} + +func (r MarkerPageResult) IsEmpty() (bool, error) { + results, err := ExtractMarkerStrings(r) + if err != nil { + return true, err + } + return len(results) == 0, err +} + +func (r MarkerPageResult) LastMarker() (string, error) { + results, err := ExtractMarkerStrings(r) + if err != nil { + return "", err + } + if len(results) == 0 { + return "", nil + } + return results[len(results)-1], nil +} + +func createMarkerPaged(t *testing.T) Pager { + testhelper.SetupHTTP() + + testhelper.Mux.HandleFunc("/page", func(w http.ResponseWriter, r *http.Request) { + r.ParseForm() + ms := r.Form["marker"] + switch { + case len(ms) == 0: + fmt.Fprintf(w, "aaa\nbbb\nccc") + case len(ms) == 1 && ms[0] == "ccc": + fmt.Fprintf(w, "ddd\neee\nfff") + case len(ms) == 1 && ms[0] == "fff": + fmt.Fprintf(w, "ggg\nhhh\niii") + case len(ms) == 1 && ms[0] == "iii": + w.WriteHeader(http.StatusNoContent) + default: + t.Errorf("Request with unexpected marker: [%v]", ms) + } + }) + + client := createClient() + + createPage := func(r PageResult) Page { + p := MarkerPageResult{MarkerPageBase{PageResult: r}} + p.MarkerPageBase.Owner = p + return p + } + + return NewPager(client, testhelper.Server.URL+"/page", createPage) +} + +func ExtractMarkerStrings(page Page) ([]string, error) { + content := page.(MarkerPageResult).Body.([]uint8) + parts := strings.Split(string(content), "\n") + results := make([]string, 0, len(parts)) + for _, part := range parts { + if len(part) > 0 { + results = append(results, part) + } + } + return results, nil +} + +func TestEnumerateMarker(t *testing.T) { + pager := createMarkerPaged(t) + defer testhelper.TeardownHTTP() + + callCount := 0 + err := pager.EachPage(func(page Page) (bool, error) { + actual, err := ExtractMarkerStrings(page) + if err != nil { + return false, err + } + + t.Logf("Handler invoked with %v", actual) + + var expected []string + switch callCount { + case 0: + expected = []string{"aaa", "bbb", "ccc"} + case 1: + expected = []string{"ddd", "eee", "fff"} + case 2: + expected = []string{"ggg", "hhh", "iii"} + default: + t.Fatalf("Unexpected call count: %d", callCount) + return false, nil + } + + testhelper.CheckDeepEquals(t, expected, actual) + + callCount++ + return true, nil + }) + testhelper.AssertNoErr(t, err) + testhelper.AssertEquals(t, callCount, 3) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/pagination/null.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/pagination/null.go new file mode 100644 index 0000000000..ae57e1886c --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/pagination/null.go @@ -0,0 +1,20 @@ +package pagination + +// nullPage is an always-empty page that trivially satisfies all Page interfacts. +// It's useful to be returned along with an error. +type nullPage struct{} + +// NextPageURL always returns "" to indicate that there are no more pages to return. +func (p nullPage) NextPageURL() (string, error) { + return "", nil +} + +// IsEmpty always returns true to prevent iteration over nullPages. +func (p nullPage) IsEmpty() (bool, error) { + return true, nil +} + +// LastMark always returns "" because the nullPage contains no items to have a mark. +func (p nullPage) LastMark() (string, error) { + return "", nil +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/pagination/pager.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/pagination/pager.go new file mode 100644 index 0000000000..5c20e16c6c --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/pagination/pager.go @@ -0,0 +1,115 @@ +package pagination + +import ( + "errors" + + "github.com/rackspace/gophercloud" +) + +var ( + // ErrPageNotAvailable is returned from a Pager when a next or previous page is requested, but does not exist. + ErrPageNotAvailable = errors.New("The requested page does not exist.") +) + +// Page must be satisfied by the result type of any resource collection. +// It allows clients to interact with the resource uniformly, regardless of whether or not or how it's paginated. +// Generally, rather than implementing this interface directly, implementors should embed one of the concrete PageBase structs, +// instead. +// Depending on the pagination strategy of a particular resource, there may be an additional subinterface that the result type +// will need to implement. +type Page interface { + + // NextPageURL generates the URL for the page of data that follows this collection. + // Return "" if no such page exists. + NextPageURL() (string, error) + + // IsEmpty returns true if this Page has no items in it. + IsEmpty() (bool, error) +} + +// Pager knows how to advance through a specific resource collection, one page at a time. +type Pager struct { + client *gophercloud.ServiceClient + + initialURL string + + createPage func(r PageResult) Page + + Err error + + // Headers supplies additional HTTP headers to populate on each paged request. + Headers map[string]string +} + +// NewPager constructs a manually-configured pager. +// Supply the URL for the first page, a function that requests a specific page given a URL, and a function that counts a page. +func NewPager(client *gophercloud.ServiceClient, initialURL string, createPage func(r PageResult) Page) Pager { + return Pager{ + client: client, + initialURL: initialURL, + createPage: createPage, + } +} + +// WithPageCreator returns a new Pager that substitutes a different page creation function. This is +// useful for overriding List functions in delegation. +func (p Pager) WithPageCreator(createPage func(r PageResult) Page) Pager { + return Pager{ + client: p.client, + initialURL: p.initialURL, + createPage: createPage, + } +} + +func (p Pager) fetchNextPage(url string) (Page, error) { + resp, err := Request(p.client, p.Headers, url) + if err != nil { + return nil, err + } + + remembered, err := PageResultFrom(resp) + if err != nil { + return nil, err + } + + return p.createPage(remembered), nil +} + +// EachPage iterates over each page returned by a Pager, yielding one at a time to a handler function. +// Return "false" from the handler to prematurely stop iterating. +func (p Pager) EachPage(handler func(Page) (bool, error)) error { + if p.Err != nil { + return p.Err + } + currentURL := p.initialURL + for { + currentPage, err := p.fetchNextPage(currentURL) + if err != nil { + return err + } + + empty, err := currentPage.IsEmpty() + if err != nil { + return err + } + if empty { + return nil + } + + ok, err := handler(currentPage) + if err != nil { + return err + } + if !ok { + return nil + } + + currentURL, err = currentPage.NextPageURL() + if err != nil { + return err + } + if currentURL == "" { + return nil + } + } +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/pagination/pagination_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/pagination/pagination_test.go new file mode 100644 index 0000000000..f3e4de1b04 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/pagination/pagination_test.go @@ -0,0 +1,13 @@ +package pagination + +import ( + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/testhelper" +) + +func createClient() *gophercloud.ServiceClient { + return &gophercloud.ServiceClient{ + ProviderClient: &gophercloud.ProviderClient{TokenID: "abc123"}, + Endpoint: testhelper.Endpoint(), + } +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/pagination/pkg.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/pagination/pkg.go new file mode 100644 index 0000000000..912daea364 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/pagination/pkg.go @@ -0,0 +1,4 @@ +/* +Package pagination contains utilities and convenience structs that implement common pagination idioms within OpenStack APIs. +*/ +package pagination diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/pagination/single.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/pagination/single.go new file mode 100644 index 0000000000..4dd3c5c425 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/pagination/single.go @@ -0,0 +1,9 @@ +package pagination + +// SinglePageBase may be embedded in a Page that contains all of the results from an operation at once. +type SinglePageBase PageResult + +// NextPageURL always returns "" to indicate that there are no more pages to return. +func (current SinglePageBase) NextPageURL() (string, error) { + return "", nil +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/pagination/single_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/pagination/single_test.go new file mode 100644 index 0000000000..8817d570f2 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/pagination/single_test.go @@ -0,0 +1,71 @@ +package pagination + +import ( + "fmt" + "net/http" + "testing" + + "github.com/mitchellh/mapstructure" + "github.com/rackspace/gophercloud/testhelper" +) + +// SinglePage sample and test cases. + +type SinglePageResult struct { + SinglePageBase +} + +func (r SinglePageResult) IsEmpty() (bool, error) { + is, err := ExtractSingleInts(r) + if err != nil { + return true, err + } + return len(is) == 0, nil +} + +func ExtractSingleInts(page Page) ([]int, error) { + var response struct { + Ints []int `mapstructure:"ints"` + } + + err := mapstructure.Decode(page.(SinglePageResult).Body, &response) + if err != nil { + return nil, err + } + + return response.Ints, nil +} + +func setupSinglePaged() Pager { + testhelper.SetupHTTP() + client := createClient() + + testhelper.Mux.HandleFunc("/only", func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, `{ "ints": [1, 2, 3] }`) + }) + + createPage := func(r PageResult) Page { + return SinglePageResult{SinglePageBase(r)} + } + + return NewPager(client, testhelper.Server.URL+"/only", createPage) +} + +func TestEnumerateSinglePaged(t *testing.T) { + callCount := 0 + pager := setupSinglePaged() + defer testhelper.TeardownHTTP() + + err := pager.EachPage(func(page Page) (bool, error) { + callCount++ + + expected := []int{1, 2, 3} + actual, err := ExtractSingleInts(page) + testhelper.AssertNoErr(t, err) + testhelper.CheckDeepEquals(t, expected, actual) + return true, nil + }) + testhelper.CheckNoErr(t, err) + testhelper.CheckEquals(t, 1, callCount) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/params.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/params.go new file mode 100644 index 0000000000..948783b073 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/params.go @@ -0,0 +1,260 @@ +package gophercloud + +import ( + "fmt" + "net/url" + "reflect" + "strconv" + "strings" + "time" +) + +// EnabledState is a convenience type, mostly used in Create and Update +// operations. Because the zero value of a bool is FALSE, we need to use a +// pointer instead to indicate zero-ness. +type EnabledState *bool + +// Convenience vars for EnabledState values. +var ( + iTrue = true + iFalse = false + + Enabled EnabledState = &iTrue + Disabled EnabledState = &iFalse +) + +// IntToPointer is a function for converting integers into integer pointers. +// This is useful when passing in options to operations. +func IntToPointer(i int) *int { + return &i +} + +/* +MaybeString is an internal function to be used by request methods in individual +resource packages. + +It takes a string that might be a zero value and returns either a pointer to its +address or nil. This is useful for allowing users to conveniently omit values +from an options struct by leaving them zeroed, but still pass nil to the JSON +serializer so they'll be omitted from the request body. +*/ +func MaybeString(original string) *string { + if original != "" { + return &original + } + return nil +} + +/* +MaybeInt is an internal function to be used by request methods in individual +resource packages. + +Like MaybeString, it accepts an int that may or may not be a zero value, and +returns either a pointer to its address or nil. It's intended to hint that the +JSON serializer should omit its field. +*/ +func MaybeInt(original int) *int { + if original != 0 { + return &original + } + return nil +} + +var t time.Time + +func isZero(v reflect.Value) bool { + switch v.Kind() { + case reflect.Func, reflect.Map, reflect.Slice: + return v.IsNil() + case reflect.Array: + z := true + for i := 0; i < v.Len(); i++ { + z = z && isZero(v.Index(i)) + } + return z + case reflect.Struct: + if v.Type() == reflect.TypeOf(t) { + if v.Interface().(time.Time).IsZero() { + return true + } + return false + } + z := true + for i := 0; i < v.NumField(); i++ { + z = z && isZero(v.Field(i)) + } + return z + } + // Compare other types directly: + z := reflect.Zero(v.Type()) + return v.Interface() == z.Interface() +} + +/* +BuildQueryString is an internal function to be used by request methods in +individual resource packages. + +It accepts a tagged structure and expands it into a URL struct. Field names are +converted into query parameters based on a "q" tag. For example: + + type struct Something { + Bar string `q:"x_bar"` + Baz int `q:"lorem_ipsum"` + } + + instance := Something{ + Bar: "AAA", + Baz: "BBB", + } + +will be converted into "?x_bar=AAA&lorem_ipsum=BBB". + +The struct's fields may be strings, integers, or boolean values. Fields left at +their type's zero value will be omitted from the query. +*/ +func BuildQueryString(opts interface{}) (*url.URL, error) { + optsValue := reflect.ValueOf(opts) + if optsValue.Kind() == reflect.Ptr { + optsValue = optsValue.Elem() + } + + optsType := reflect.TypeOf(opts) + if optsType.Kind() == reflect.Ptr { + optsType = optsType.Elem() + } + + params := url.Values{} + + if optsValue.Kind() == reflect.Struct { + for i := 0; i < optsValue.NumField(); i++ { + v := optsValue.Field(i) + f := optsType.Field(i) + qTag := f.Tag.Get("q") + + // if the field has a 'q' tag, it goes in the query string + if qTag != "" { + tags := strings.Split(qTag, ",") + + // if the field is set, add it to the slice of query pieces + if !isZero(v) { + switch v.Kind() { + case reflect.String: + params.Add(tags[0], v.String()) + case reflect.Int: + params.Add(tags[0], strconv.FormatInt(v.Int(), 10)) + case reflect.Bool: + params.Add(tags[0], strconv.FormatBool(v.Bool())) + } + } else { + // Otherwise, the field is not set. + if len(tags) == 2 && tags[1] == "required" { + // And the field is required. Return an error. + return nil, fmt.Errorf("Required query parameter [%s] not set.", f.Name) + } + } + } + } + + return &url.URL{RawQuery: params.Encode()}, nil + } + // Return an error if the underlying type of 'opts' isn't a struct. + return nil, fmt.Errorf("Options type is not a struct.") +} + +/* +BuildHeaders is an internal function to be used by request methods in +individual resource packages. + +It accepts an arbitrary tagged structure and produces a string map that's +suitable for use as the HTTP headers of an outgoing request. Field names are +mapped to header names based in "h" tags. + + type struct Something { + Bar string `h:"x_bar"` + Baz int `h:"lorem_ipsum"` + } + + instance := Something{ + Bar: "AAA", + Baz: "BBB", + } + +will be converted into: + + map[string]string{ + "x_bar": "AAA", + "lorem_ipsum": "BBB", + } + +Untagged fields and fields left at their zero values are skipped. Integers, +booleans and string values are supported. +*/ +func BuildHeaders(opts interface{}) (map[string]string, error) { + optsValue := reflect.ValueOf(opts) + if optsValue.Kind() == reflect.Ptr { + optsValue = optsValue.Elem() + } + + optsType := reflect.TypeOf(opts) + if optsType.Kind() == reflect.Ptr { + optsType = optsType.Elem() + } + + optsMap := make(map[string]string) + if optsValue.Kind() == reflect.Struct { + for i := 0; i < optsValue.NumField(); i++ { + v := optsValue.Field(i) + f := optsType.Field(i) + hTag := f.Tag.Get("h") + + // if the field has a 'h' tag, it goes in the header + if hTag != "" { + tags := strings.Split(hTag, ",") + + // if the field is set, add it to the slice of query pieces + if !isZero(v) { + switch v.Kind() { + case reflect.String: + optsMap[tags[0]] = v.String() + case reflect.Int: + optsMap[tags[0]] = strconv.FormatInt(v.Int(), 10) + case reflect.Bool: + optsMap[tags[0]] = strconv.FormatBool(v.Bool()) + } + } else { + // Otherwise, the field is not set. + if len(tags) == 2 && tags[1] == "required" { + // And the field is required. Return an error. + return optsMap, fmt.Errorf("Required header not set.") + } + } + } + + } + return optsMap, nil + } + // Return an error if the underlying type of 'opts' isn't a struct. + return optsMap, fmt.Errorf("Options type is not a struct.") +} + +// IDSliceToQueryString takes a slice of elements and converts them into a query +// string. For example, if name=foo and slice=[]int{20, 40, 60}, then the +// result would be `?name=20&name=40&name=60' +func IDSliceToQueryString(name string, ids []int) string { + str := "" + for k, v := range ids { + if k == 0 { + str += "?" + } else { + str += "&" + } + str += fmt.Sprintf("%s=%s", name, strconv.Itoa(v)) + } + return str +} + +// IntWithinRange returns TRUE if an integer falls within a defined range, and +// FALSE if not. +func IntWithinRange(val, min, max int) bool { + return val > min && val < max +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/params_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/params_test.go new file mode 100644 index 0000000000..4a2c9fe341 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/params_test.go @@ -0,0 +1,155 @@ +package gophercloud + +import ( + "net/url" + "reflect" + "testing" + "time" + + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestMaybeString(t *testing.T) { + testString := "" + var expected *string + actual := MaybeString(testString) + th.CheckDeepEquals(t, expected, actual) + + testString = "carol" + expected = &testString + actual = MaybeString(testString) + th.CheckDeepEquals(t, expected, actual) +} + +func TestMaybeInt(t *testing.T) { + testInt := 0 + var expected *int + actual := MaybeInt(testInt) + th.CheckDeepEquals(t, expected, actual) + + testInt = 4 + expected = &testInt + actual = MaybeInt(testInt) + th.CheckDeepEquals(t, expected, actual) +} + +func TestBuildQueryString(t *testing.T) { + opts := struct { + J int `q:"j"` + R string `q:"r,required"` + C bool `q:"c"` + }{ + J: 2, + R: "red", + C: true, + } + expected := &url.URL{RawQuery: "c=true&j=2&r=red"} + actual, err := BuildQueryString(&opts) + if err != nil { + t.Errorf("Error building query string: %v", err) + } + th.CheckDeepEquals(t, expected, actual) + + opts = struct { + J int `q:"j"` + R string `q:"r,required"` + C bool `q:"c"` + }{ + J: 2, + C: true, + } + _, err = BuildQueryString(&opts) + if err == nil { + t.Errorf("Expected error: 'Required field not set'") + } + th.CheckDeepEquals(t, expected, actual) + + _, err = BuildQueryString(map[string]interface{}{"Number": 4}) + if err == nil { + t.Errorf("Expected error: 'Options type is not a struct'") + } +} + +func TestBuildHeaders(t *testing.T) { + testStruct := struct { + Accept string `h:"Accept"` + Num int `h:"Number,required"` + Style bool `h:"Style"` + }{ + Accept: "application/json", + Num: 4, + Style: true, + } + expected := map[string]string{"Accept": "application/json", "Number": "4", "Style": "true"} + actual, err := BuildHeaders(&testStruct) + th.CheckNoErr(t, err) + th.CheckDeepEquals(t, expected, actual) + + testStruct.Num = 0 + _, err = BuildHeaders(&testStruct) + if err == nil { + t.Errorf("Expected error: 'Required header not set'") + } + + _, err = BuildHeaders(map[string]interface{}{"Number": 4}) + if err == nil { + t.Errorf("Expected error: 'Options type is not a struct'") + } +} + +func TestIsZero(t *testing.T) { + var testMap map[string]interface{} + testMapValue := reflect.ValueOf(testMap) + expected := true + actual := isZero(testMapValue) + th.CheckEquals(t, expected, actual) + testMap = map[string]interface{}{"empty": false} + testMapValue = reflect.ValueOf(testMap) + expected = false + actual = isZero(testMapValue) + th.CheckEquals(t, expected, actual) + + var testArray [2]string + testArrayValue := reflect.ValueOf(testArray) + expected = true + actual = isZero(testArrayValue) + th.CheckEquals(t, expected, actual) + testArray = [2]string{"one", "two"} + testArrayValue = reflect.ValueOf(testArray) + expected = false + actual = isZero(testArrayValue) + th.CheckEquals(t, expected, actual) + + var testStruct struct { + A string + B time.Time + } + testStructValue := reflect.ValueOf(testStruct) + expected = true + actual = isZero(testStructValue) + th.CheckEquals(t, expected, actual) + testStruct = struct { + A string + B time.Time + }{ + B: time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC), + } + testStructValue = reflect.ValueOf(testStruct) + expected = false + actual = isZero(testStructValue) + th.CheckEquals(t, expected, actual) +} + +func TestQueriesAreEscaped(t *testing.T) { + type foo struct { + Name string `q:"something"` + Shape string `q:"else"` + } + + expected := &url.URL{RawQuery: "else=Triangl+e&something=blah%2B%3F%21%21foo"} + + actual, err := BuildQueryString(foo{Name: "blah+?!!foo", Shape: "Triangl e"}) + th.AssertNoErr(t, err) + + th.AssertDeepEquals(t, expected, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/provider_client.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/provider_client.go new file mode 100644 index 0000000000..7754c20812 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/provider_client.go @@ -0,0 +1,33 @@ +package gophercloud + +// ProviderClient stores details that are required to interact with any +// services within a specific provider's API. +// +// Generally, you acquire a ProviderClient by calling the NewClient method in +// the appropriate provider's child package, providing whatever authentication +// credentials are required. +type ProviderClient struct { + // IdentityBase is the base URL used for a particular provider's identity + // service - it will be used when issuing authenticatation requests. It + // should point to the root resource of the identity service, not a specific + // identity version. + IdentityBase string + + // IdentityEndpoint is the identity endpoint. This may be a specific version + // of the identity service. If this is the case, this endpoint is used rather + // than querying versions first. + IdentityEndpoint string + + // TokenID is the ID of the most recently issued valid token. + TokenID string + + // EndpointLocator describes how this provider discovers the endpoints for + // its constituent services. + EndpointLocator EndpointLocator +} + +// AuthenticatedHeaders returns a map of HTTP headers that are common for all +// authenticated service requests. +func (client *ProviderClient) AuthenticatedHeaders() map[string]string { + return map[string]string{"X-Auth-Token": client.TokenID} +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/provider_client_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/provider_client_test.go new file mode 100644 index 0000000000..b260246c5a --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/provider_client_test.go @@ -0,0 +1,16 @@ +package gophercloud + +import ( + "testing" + + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestAuthenticatedHeaders(t *testing.T) { + p := &ProviderClient{ + TokenID: "1234", + } + expected := map[string]string{"X-Auth-Token": "1234"} + actual := p.AuthenticatedHeaders() + th.CheckDeepEquals(t, expected, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/auth_env.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/auth_env.go new file mode 100644 index 0000000000..5852c3ce73 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/auth_env.go @@ -0,0 +1,57 @@ +package rackspace + +import ( + "fmt" + "os" + + "github.com/rackspace/gophercloud" +) + +var nilOptions = gophercloud.AuthOptions{} + +// ErrNoAuthUrl, ErrNoUsername, and ErrNoPassword errors indicate of the +// required RS_AUTH_URL, RS_USERNAME, or RS_PASSWORD environment variables, +// respectively, remain undefined. See the AuthOptions() function for more details. +var ( + ErrNoAuthURL = fmt.Errorf("Environment variable RS_AUTH_URL or OS_AUTH_URL need to be set.") + ErrNoUsername = fmt.Errorf("Environment variable RS_USERNAME or OS_USERNAME need to be set.") + ErrNoPassword = fmt.Errorf("Environment variable RS_API_KEY or RS_PASSWORD needs to be set.") +) + +func prefixedEnv(base string) string { + value := os.Getenv("RS_" + base) + if value == "" { + value = os.Getenv("OS_" + base) + } + return value +} + +// AuthOptionsFromEnv fills out an identity.AuthOptions structure with the +// settings found on the various Rackspace RS_* environment variables. +func AuthOptionsFromEnv() (gophercloud.AuthOptions, error) { + authURL := prefixedEnv("AUTH_URL") + username := prefixedEnv("USERNAME") + password := prefixedEnv("PASSWORD") + apiKey := prefixedEnv("API_KEY") + + if authURL == "" { + return nilOptions, ErrNoAuthURL + } + + if username == "" { + return nilOptions, ErrNoUsername + } + + if password == "" && apiKey == "" { + return nilOptions, ErrNoPassword + } + + ao := gophercloud.AuthOptions{ + IdentityEndpoint: authURL, + Username: username, + Password: password, + APIKey: apiKey, + } + + return ao, nil +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/snapshots/delegate.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/snapshots/delegate.go new file mode 100644 index 0000000000..b338c36b71 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/snapshots/delegate.go @@ -0,0 +1,134 @@ +package snapshots + +import ( + "errors" + + "github.com/racker/perigee" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" + + os "github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots" +) + +func updateURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL("snapshots", id) +} + +// CreateOptsBuilder allows extensions to add additional parameters to the +// Create request. +type CreateOptsBuilder interface { + ToSnapshotCreateMap() (map[string]interface{}, error) +} + +// CreateOpts contains options for creating a Snapshot. This object is passed to +// the snapshots.Create function. For more information about these parameters, +// see the Snapshot object. +type CreateOpts struct { + // REQUIRED + VolumeID string + // OPTIONAL + Description string + // OPTIONAL + Force bool + // OPTIONAL + Name string +} + +// ToSnapshotCreateMap assembles a request body based on the contents of a +// CreateOpts. +func (opts CreateOpts) ToSnapshotCreateMap() (map[string]interface{}, error) { + s := make(map[string]interface{}) + + if opts.VolumeID == "" { + return nil, errors.New("Required CreateOpts field 'VolumeID' not set.") + } + + s["volume_id"] = opts.VolumeID + + if opts.Description != "" { + s["display_description"] = opts.Description + } + if opts.Name != "" { + s["display_name"] = opts.Name + } + if opts.Force { + s["force"] = opts.Force + } + + return map[string]interface{}{"snapshot": s}, nil +} + +// Create will create a new Snapshot based on the values in CreateOpts. To +// extract the Snapshot object from the response, call the Extract method on the +// CreateResult. +func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult { + return CreateResult{os.Create(client, opts)} +} + +// Delete will delete the existing Snapshot with the provided ID. +func Delete(client *gophercloud.ServiceClient, id string) os.DeleteResult { + return os.Delete(client, id) +} + +// Get retrieves the Snapshot with the provided ID. To extract the Snapshot +// object from the response, call the Extract method on the GetResult. +func Get(client *gophercloud.ServiceClient, id string) GetResult { + return GetResult{os.Get(client, id)} +} + +// List returns Snapshots. +func List(client *gophercloud.ServiceClient) pagination.Pager { + return os.List(client, os.ListOpts{}) +} + +// UpdateOptsBuilder is the interface options structs have to satisfy in order +// to be used in the main Update operation in this package. Since many +// extensions decorate or modify the common logic, it is useful for them to +// satisfy a basic interface in order for them to be used. +type UpdateOptsBuilder interface { + ToSnapshotUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts is the common options struct used in this package's Update +// operation. +type UpdateOpts struct { + Name string + Description string +} + +// ToSnapshotUpdateMap casts a UpdateOpts struct to a map. +func (opts UpdateOpts) ToSnapshotUpdateMap() (map[string]interface{}, error) { + s := make(map[string]interface{}) + + if opts.Name != "" { + s["display_name"] = opts.Name + } + if opts.Description != "" { + s["display_description"] = opts.Description + } + + return map[string]interface{}{"snapshot": s}, nil +} + +// Update accepts a UpdateOpts struct and updates an existing snapshot using the +// values provided. +func Update(c *gophercloud.ServiceClient, snapshotID string, opts UpdateOptsBuilder) UpdateResult { + var res UpdateResult + + reqBody, err := opts.ToSnapshotUpdateMap() + if err != nil { + res.Err = err + return res + } + + // Send request to API + _, res.Err = perigee.Request("PUT", updateURL(c, snapshotID), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + ReqBody: &reqBody, + Results: &res.Body, + OkCodes: []int{200, 201}, + }) + + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/snapshots/delegate_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/snapshots/delegate_test.go new file mode 100644 index 0000000000..1a02b46527 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/snapshots/delegate_test.go @@ -0,0 +1,97 @@ +package snapshots + +import ( + "testing" + + "github.com/rackspace/gophercloud" + os "github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +const endpoint = "http://localhost:57909/v1/12345" + +func endpointClient() *gophercloud.ServiceClient { + return &gophercloud.ServiceClient{Endpoint: endpoint} +} + +func TestUpdateURL(t *testing.T) { + actual := updateURL(endpointClient(), "foo") + expected := endpoint + "snapshots/foo" + th.AssertEquals(t, expected, actual) +} + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + os.MockListResponse(t) + + count := 0 + + err := List(fake.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractSnapshots(page) + if err != nil { + t.Errorf("Failed to extract snapshots: %v", err) + return false, err + } + + expected := []Snapshot{ + Snapshot{ + ID: "289da7f8-6440-407c-9fb4-7db01ec49164", + Name: "snapshot-001", + }, + Snapshot{ + ID: "96c3bda7-c82a-4f50-be73-ca7621794835", + Name: "snapshot-002", + }, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + + th.AssertEquals(t, 1, count) + th.AssertNoErr(t, err) +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + os.MockGetResponse(t) + + v, err := Get(fake.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22").Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, v.Name, "snapshot-001") + th.AssertEquals(t, v.ID, "d32019d3-bc6e-4319-9c1d-6722fc136a22") +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + os.MockCreateResponse(t) + + options := &CreateOpts{VolumeID: "1234", Name: "snapshot-001"} + n, err := Create(fake.ServiceClient(), options).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, n.VolumeID, "1234") + th.AssertEquals(t, n.Name, "snapshot-001") + th.AssertEquals(t, n.ID, "d32019d3-bc6e-4319-9c1d-6722fc136a22") +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + os.MockDeleteResponse(t) + + res := Delete(fake.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22") + th.AssertNoErr(t, res.Err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/snapshots/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/snapshots/doc.go new file mode 100644 index 0000000000..ad6064f2af --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/snapshots/doc.go @@ -0,0 +1,3 @@ +// Package snapshots provides information and interaction with the snapshot +// API resource for the Rackspace Block Storage service. +package snapshots diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/snapshots/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/snapshots/results.go new file mode 100644 index 0000000000..0fab2828bc --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/snapshots/results.go @@ -0,0 +1,149 @@ +package snapshots + +import ( + "github.com/racker/perigee" + + "github.com/rackspace/gophercloud" + os "github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots" + "github.com/rackspace/gophercloud/pagination" + + "github.com/mitchellh/mapstructure" +) + +// Status is the type used to represent a snapshot's status +type Status string + +// Constants to use for supported statuses +const ( + Creating Status = "CREATING" + Available Status = "AVAILABLE" + Deleting Status = "DELETING" + Error Status = "ERROR" + DeleteError Status = "ERROR_DELETING" +) + +// Snapshot is the Rackspace representation of an external block storage device. +type Snapshot struct { + // The timestamp when this snapshot was created. + CreatedAt string `mapstructure:"created_at"` + + // The human-readable description for this snapshot. + Description string `mapstructure:"display_description"` + + // The human-readable name for this snapshot. + Name string `mapstructure:"display_name"` + + // The UUID for this snapshot. + ID string `mapstructure:"id"` + + // The random metadata associated with this snapshot. Note: unlike standard + // OpenStack snapshots, this cannot actually be set. + Metadata map[string]string `mapstructure:"metadata"` + + // Indicates the current progress of the snapshot's backup procedure. + Progress string `mapstructure:"os-extended-snapshot-attributes:progress"` + + // The project ID. + ProjectID string `mapstructure:"os-extended-snapshot-attributes:project_id"` + + // The size of the volume which this snapshot backs up. + Size int `mapstructure:"size"` + + // The status of the snapshot. + Status Status `mapstructure:"status"` + + // The ID of the volume which this snapshot seeks to back up. + VolumeID string `mapstructure:"volume_id"` +} + +// CreateResult represents the result of a create operation +type CreateResult struct { + os.CreateResult +} + +// GetResult represents the result of a get operation +type GetResult struct { + os.GetResult +} + +// UpdateResult represents the result of an update operation +type UpdateResult struct { + gophercloud.Result +} + +func commonExtract(resp interface{}, err error) (*Snapshot, error) { + if err != nil { + return nil, err + } + + var respStruct struct { + Snapshot *Snapshot `json:"snapshot"` + } + + err = mapstructure.Decode(resp, &respStruct) + + return respStruct.Snapshot, err +} + +// Extract will get the Snapshot object out of the GetResult object. +func (r GetResult) Extract() (*Snapshot, error) { + return commonExtract(r.Body, r.Err) +} + +// Extract will get the Snapshot object out of the CreateResult object. +func (r CreateResult) Extract() (*Snapshot, error) { + return commonExtract(r.Body, r.Err) +} + +// Extract will get the Snapshot object out of the UpdateResult object. +func (r UpdateResult) Extract() (*Snapshot, error) { + return commonExtract(r.Body, r.Err) +} + +// ExtractSnapshots extracts and returns Snapshots. It is used while iterating over a snapshots.List call. +func ExtractSnapshots(page pagination.Page) ([]Snapshot, error) { + var response struct { + Snapshots []Snapshot `json:"snapshots"` + } + + err := mapstructure.Decode(page.(os.ListResult).Body, &response) + return response.Snapshots, err +} + +// WaitUntilComplete will continually poll a snapshot until it successfully +// transitions to a specified state. It will do this for at most the number of +// seconds specified. +func (snapshot Snapshot) WaitUntilComplete(c *gophercloud.ServiceClient, timeout int) error { + return gophercloud.WaitFor(timeout, func() (bool, error) { + // Poll resource + current, err := Get(c, snapshot.ID).Extract() + if err != nil { + return false, err + } + + // Has it been built yet? + if current.Progress == "100%" { + return true, nil + } + + return false, nil + }) +} + +// WaitUntilDeleted will continually poll a snapshot until it has been +// successfully deleted, i.e. returns a 404 status. +func (snapshot Snapshot) WaitUntilDeleted(c *gophercloud.ServiceClient, timeout int) error { + return gophercloud.WaitFor(timeout, func() (bool, error) { + // Poll resource + _, err := Get(c, snapshot.ID).Extract() + + // Check for a 404 + if casted, ok := err.(*perigee.UnexpectedResponseCodeError); ok && casted.Actual == 404 { + return true, nil + } else if err != nil { + return false, err + } + + return false, nil + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/volumes/delegate.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/volumes/delegate.go new file mode 100644 index 0000000000..438349410a --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/volumes/delegate.go @@ -0,0 +1,75 @@ +package volumes + +import ( + "fmt" + + "github.com/rackspace/gophercloud" + os "github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes" + "github.com/rackspace/gophercloud/pagination" +) + +type CreateOpts struct { + os.CreateOpts +} + +func (opts CreateOpts) ToVolumeCreateMap() (map[string]interface{}, error) { + if opts.Size < 75 || opts.Size > 1024 { + return nil, fmt.Errorf("Size field must be between 75 and 1024") + } + + return opts.CreateOpts.ToVolumeCreateMap() +} + +// Create will create a new Volume based on the values in CreateOpts. To extract +// the Volume object from the response, call the Extract method on the +// CreateResult. +func Create(client *gophercloud.ServiceClient, opts os.CreateOptsBuilder) CreateResult { + return CreateResult{os.Create(client, opts)} +} + +// Delete will delete the existing Volume with the provided ID. +func Delete(client *gophercloud.ServiceClient, id string) os.DeleteResult { + return os.Delete(client, id) +} + +// Get retrieves the Volume with the provided ID. To extract the Volume object +// from the response, call the Extract method on the GetResult. +func Get(client *gophercloud.ServiceClient, id string) GetResult { + return GetResult{os.Get(client, id)} +} + +// List returns volumes optionally limited by the conditions provided in ListOpts. +func List(client *gophercloud.ServiceClient) pagination.Pager { + return os.List(client, os.ListOpts{}) +} + +// UpdateOpts contain options for updating an existing Volume. This object is passed +// to the volumes.Update function. For more information about the parameters, see +// the Volume object. +type UpdateOpts struct { + // OPTIONAL + Name string + // OPTIONAL + Description string +} + +// ToVolumeUpdateMap assembles a request body based on the contents of an +// UpdateOpts. +func (opts UpdateOpts) ToVolumeUpdateMap() (map[string]interface{}, error) { + v := make(map[string]interface{}) + + if opts.Description != "" { + v["display_description"] = opts.Description + } + if opts.Name != "" { + v["display_name"] = opts.Name + } + + return map[string]interface{}{"volume": v}, nil +} + +// Update will update the Volume with provided information. To extract the updated +// Volume from the response, call the Extract method on the UpdateResult. +func Update(client *gophercloud.ServiceClient, id string, opts os.UpdateOptsBuilder) UpdateResult { + return UpdateResult{os.Update(client, id, opts)} +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/volumes/delegate_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/volumes/delegate_test.go new file mode 100644 index 0000000000..b44564cc1f --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/volumes/delegate_test.go @@ -0,0 +1,106 @@ +package volumes + +import ( + "testing" + + os "github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + os.MockListResponse(t) + + count := 0 + + err := List(fake.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractVolumes(page) + if err != nil { + t.Errorf("Failed to extract volumes: %v", err) + return false, err + } + + expected := []Volume{ + Volume{ + ID: "289da7f8-6440-407c-9fb4-7db01ec49164", + Name: "vol-001", + }, + Volume{ + ID: "96c3bda7-c82a-4f50-be73-ca7621794835", + Name: "vol-002", + }, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + + th.AssertEquals(t, 1, count) + th.AssertNoErr(t, err) +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + os.MockGetResponse(t) + + v, err := Get(fake.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22").Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, v.Name, "vol-001") + th.AssertEquals(t, v.ID, "d32019d3-bc6e-4319-9c1d-6722fc136a22") +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + os.MockCreateResponse(t) + + n, err := Create(fake.ServiceClient(), CreateOpts{os.CreateOpts{Size: 75}}).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, n.Size, 4) + th.AssertEquals(t, n.ID, "d32019d3-bc6e-4319-9c1d-6722fc136a22") +} + +func TestSizeRange(t *testing.T) { + _, err := Create(fake.ServiceClient(), CreateOpts{os.CreateOpts{Size: 1}}).Extract() + if err == nil { + t.Fatalf("Expected error, got none") + } + + _, err = Create(fake.ServiceClient(), CreateOpts{os.CreateOpts{Size: 2000}}).Extract() + if err == nil { + t.Fatalf("Expected error, got none") + } +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + os.MockDeleteResponse(t) + + res := Delete(fake.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22") + th.AssertNoErr(t, res.Err) +} + +func TestUpdate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + os.MockUpdateResponse(t) + + options := &UpdateOpts{Name: "vol-002"} + v, err := Update(fake.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22", options).Extract() + th.AssertNoErr(t, err) + th.CheckEquals(t, "vol-002", v.Name) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/volumes/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/volumes/doc.go new file mode 100644 index 0000000000..b2be25c538 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/volumes/doc.go @@ -0,0 +1,3 @@ +// Package volumes provides information and interaction with the volume +// API resource for the Rackspace Block Storage service. +package volumes diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/volumes/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/volumes/results.go new file mode 100644 index 0000000000..c7c2cc4984 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/volumes/results.go @@ -0,0 +1,66 @@ +package volumes + +import ( + os "github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes" + "github.com/rackspace/gophercloud/pagination" + + "github.com/mitchellh/mapstructure" +) + +// Volume wraps an Openstack volume +type Volume os.Volume + +// CreateResult represents the result of a create operation +type CreateResult struct { + os.CreateResult +} + +// GetResult represents the result of a get operation +type GetResult struct { + os.GetResult +} + +// UpdateResult represents the result of an update operation +type UpdateResult struct { + os.UpdateResult +} + +func commonExtract(resp interface{}, err error) (*Volume, error) { + if err != nil { + return nil, err + } + + var respStruct struct { + Volume *Volume `json:"volume"` + } + + err = mapstructure.Decode(resp, &respStruct) + + return respStruct.Volume, err +} + +// Extract will get the Volume object out of the GetResult object. +func (r GetResult) Extract() (*Volume, error) { + return commonExtract(r.Body, r.Err) +} + +// Extract will get the Volume object out of the CreateResult object. +func (r CreateResult) Extract() (*Volume, error) { + return commonExtract(r.Body, r.Err) +} + +// Extract will get the Volume object out of the UpdateResult object. +func (r UpdateResult) Extract() (*Volume, error) { + return commonExtract(r.Body, r.Err) +} + +// ExtractVolumes extracts and returns Volumes. It is used while iterating over a volumes.List call. +func ExtractVolumes(page pagination.Page) ([]Volume, error) { + var response struct { + Volumes []Volume `json:"volumes"` + } + + err := mapstructure.Decode(page.(os.ListResult).Body, &response) + + return response.Volumes, err +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/volumetypes/delegate.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/volumetypes/delegate.go new file mode 100644 index 0000000000..c96b3e4a35 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/volumetypes/delegate.go @@ -0,0 +1,18 @@ +package volumetypes + +import ( + "github.com/rackspace/gophercloud" + os "github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumetypes" + "github.com/rackspace/gophercloud/pagination" +) + +// List returns all volume types. +func List(client *gophercloud.ServiceClient) pagination.Pager { + return os.List(client) +} + +// Get will retrieve the volume type with the provided ID. To extract the volume +// type from the result, call the Extract method on the GetResult. +func Get(client *gophercloud.ServiceClient, id string) GetResult { + return GetResult{os.Get(client, id)} +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/volumetypes/delegate_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/volumetypes/delegate_test.go new file mode 100644 index 0000000000..6e65c904b5 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/volumetypes/delegate_test.go @@ -0,0 +1,64 @@ +package volumetypes + +import ( + "testing" + + os "github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumetypes" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + os.MockListResponse(t) + + count := 0 + + err := List(fake.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractVolumeTypes(page) + if err != nil { + t.Errorf("Failed to extract volume types: %v", err) + return false, err + } + + expected := []VolumeType{ + VolumeType{ + ID: "289da7f8-6440-407c-9fb4-7db01ec49164", + Name: "vol-type-001", + ExtraSpecs: map[string]interface{}{ + "capabilities": "gpu", + }, + }, + VolumeType{ + ID: "96c3bda7-c82a-4f50-be73-ca7621794835", + Name: "vol-type-002", + ExtraSpecs: map[string]interface{}{}, + }, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + + th.AssertEquals(t, 1, count) + th.AssertNoErr(t, err) +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + os.MockGetResponse(t) + + vt, err := Get(fake.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22").Extract() + th.AssertNoErr(t, err) + + th.AssertDeepEquals(t, vt.ExtraSpecs, map[string]interface{}{"serverNumber": "2"}) + th.AssertEquals(t, vt.Name, "vol-type-001") + th.AssertEquals(t, vt.ID, "d32019d3-bc6e-4319-9c1d-6722fc136a22") +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/volumetypes/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/volumetypes/doc.go new file mode 100644 index 0000000000..70122b77c4 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/volumetypes/doc.go @@ -0,0 +1,3 @@ +// Package volumetypes provides information and interaction with the volume type +// API resource for the Rackspace Block Storage service. +package volumetypes diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/volumetypes/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/volumetypes/results.go new file mode 100644 index 0000000000..39c8d6f7fa --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/volumetypes/results.go @@ -0,0 +1,37 @@ +package volumetypes + +import ( + "github.com/mitchellh/mapstructure" + os "github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumetypes" + "github.com/rackspace/gophercloud/pagination" +) + +type VolumeType os.VolumeType + +type GetResult struct { + os.GetResult +} + +// Extract will get the Volume Type struct out of the response. +func (r GetResult) Extract() (*VolumeType, error) { + if r.Err != nil { + return nil, r.Err + } + + var res struct { + VolumeType *VolumeType `json:"volume_type" mapstructure:"volume_type"` + } + + err := mapstructure.Decode(r.Body, &res) + + return res.VolumeType, err +} + +func ExtractVolumeTypes(page pagination.Page) ([]VolumeType, error) { + var response struct { + VolumeTypes []VolumeType `mapstructure:"volume_types"` + } + + err := mapstructure.Decode(page.(os.ListResult).Body, &response) + return response.VolumeTypes, err +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/client.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/client.go new file mode 100644 index 0000000000..7421ff01ad --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/client.go @@ -0,0 +1,178 @@ +package rackspace + +import ( + "fmt" + + "github.com/rackspace/gophercloud" + os "github.com/rackspace/gophercloud/openstack" + "github.com/rackspace/gophercloud/openstack/utils" + tokens2 "github.com/rackspace/gophercloud/rackspace/identity/v2/tokens" +) + +const ( + // RackspaceUSIdentity is an identity endpoint located in the United States. + RackspaceUSIdentity = "https://identity.api.rackspacecloud.com/v2.0/" + + // RackspaceUKIdentity is an identity endpoint located in the UK. + RackspaceUKIdentity = "https://lon.identity.api.rackspacecloud.com/v2.0/" +) + +const ( + v20 = "v2.0" +) + +// NewClient creates a client that's prepared to communicate with the Rackspace API, but is not +// yet authenticated. Most users will probably prefer using the AuthenticatedClient function +// instead. +// +// Provide the base URL of the identity endpoint you wish to authenticate against as "endpoint". +// Often, this will be either RackspaceUSIdentity or RackspaceUKIdentity. +func NewClient(endpoint string) (*gophercloud.ProviderClient, error) { + if endpoint == "" { + return os.NewClient(RackspaceUSIdentity) + } + return os.NewClient(endpoint) +} + +// AuthenticatedClient logs in to Rackspace with the provided credentials and constructs a +// ProviderClient that's ready to operate. +// +// If the provided AuthOptions does not specify an explicit IdentityEndpoint, it will default to +// the canonical, production Rackspace US identity endpoint. +func AuthenticatedClient(options gophercloud.AuthOptions) (*gophercloud.ProviderClient, error) { + client, err := NewClient(options.IdentityEndpoint) + if err != nil { + return nil, err + } + + err = Authenticate(client, options) + if err != nil { + return nil, err + } + return client, nil +} + +// Authenticate or re-authenticate against the most recent identity service supported at the +// provided endpoint. +func Authenticate(client *gophercloud.ProviderClient, options gophercloud.AuthOptions) error { + versions := []*utils.Version{ + &utils.Version{ID: v20, Priority: 20, Suffix: "/v2.0/"}, + } + + chosen, endpoint, err := utils.ChooseVersion(client.IdentityBase, client.IdentityEndpoint, versions) + if err != nil { + return err + } + + switch chosen.ID { + case v20: + return v2auth(client, endpoint, options) + default: + // The switch statement must be out of date from the versions list. + return fmt.Errorf("Unrecognized identity version: %s", chosen.ID) + } +} + +// AuthenticateV2 explicitly authenticates with v2 of the identity service. +func AuthenticateV2(client *gophercloud.ProviderClient, options gophercloud.AuthOptions) error { + return v2auth(client, "", options) +} + +func v2auth(client *gophercloud.ProviderClient, endpoint string, options gophercloud.AuthOptions) error { + v2Client := NewIdentityV2(client) + if endpoint != "" { + v2Client.Endpoint = endpoint + } + + result := tokens2.Create(v2Client, tokens2.WrapOptions(options)) + + token, err := result.ExtractToken() + if err != nil { + return err + } + + catalog, err := result.ExtractServiceCatalog() + if err != nil { + return err + } + + client.TokenID = token.ID + client.EndpointLocator = func(opts gophercloud.EndpointOpts) (string, error) { + return os.V2EndpointURL(catalog, opts) + } + + return nil +} + +// NewIdentityV2 creates a ServiceClient that may be used to access the v2 identity service. +func NewIdentityV2(client *gophercloud.ProviderClient) *gophercloud.ServiceClient { + v2Endpoint := client.IdentityBase + "v2.0/" + + return &gophercloud.ServiceClient{ + ProviderClient: client, + Endpoint: v2Endpoint, + } +} + +// NewComputeV2 creates a ServiceClient that may be used to access the v2 compute service. +func NewComputeV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + eo.ApplyDefaults("compute") + url, err := client.EndpointLocator(eo) + if err != nil { + return nil, err + } + + return &gophercloud.ServiceClient{ + ProviderClient: client, + Endpoint: url, + }, nil +} + +// NewObjectCDNV1 creates a ServiceClient that may be used with the Rackspace v1 CDN. +func NewObjectCDNV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + eo.ApplyDefaults("rax:object-cdn") + url, err := client.EndpointLocator(eo) + if err != nil { + return nil, err + } + return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil +} + +// NewObjectStorageV1 creates a ServiceClient that may be used with the Rackspace v1 object storage package. +func NewObjectStorageV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + return os.NewObjectStorageV1(client, eo) +} + +// NewBlockStorageV1 creates a ServiceClient that can be used to access the +// Rackspace Cloud Block Storage v1 API. +func NewBlockStorageV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + eo.ApplyDefaults("volume") + url, err := client.EndpointLocator(eo) + if err != nil { + return nil, err + } + + return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil +} + +// NewLBV1 creates a ServiceClient that can be used to access the Rackspace +// Cloud Load Balancer v1 API. +func NewLBV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + eo.ApplyDefaults("rax:load-balancer") + url, err := client.EndpointLocator(eo) + if err != nil { + return nil, err + } + return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil +} + +// NewNetworkV2 creates a ServiceClient that can be used to access the Rackspace +// Networking v2 API. +func NewNetworkV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + eo.ApplyDefaults("network") + url, err := client.EndpointLocator(eo) + if err != nil { + return nil, err + } + return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/client_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/client_test.go new file mode 100644 index 0000000000..73b1c886ff --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/client_test.go @@ -0,0 +1,38 @@ +package rackspace + +import ( + "fmt" + "net/http" + "testing" + + "github.com/rackspace/gophercloud" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestAuthenticatedClientV2(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/tokens", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, ` + { + "access": { + "token": { + "id": "01234567890", + "expires": "2014-10-01T10:00:00.000000Z" + }, + "serviceCatalog": [] + } + } + `) + }) + + options := gophercloud.AuthOptions{ + Username: "me", + APIKey: "09876543210", + IdentityEndpoint: th.Endpoint() + "v2.0/", + } + client, err := AuthenticatedClient(options) + th.AssertNoErr(t, err) + th.CheckEquals(t, "01234567890", client.TokenID) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/bootfromvolume/delegate.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/bootfromvolume/delegate.go new file mode 100644 index 0000000000..2580459f07 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/bootfromvolume/delegate.go @@ -0,0 +1,12 @@ +package bootfromvolume + +import ( + "github.com/rackspace/gophercloud" + osBFV "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/bootfromvolume" + osServers "github.com/rackspace/gophercloud/openstack/compute/v2/servers" +) + +// Create requests the creation of a server from the given block device mapping. +func Create(client *gophercloud.ServiceClient, opts osServers.CreateOptsBuilder) osServers.CreateResult { + return osBFV.Create(client, opts) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/bootfromvolume/delegate_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/bootfromvolume/delegate_test.go new file mode 100644 index 0000000000..0b5352751b --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/bootfromvolume/delegate_test.go @@ -0,0 +1,52 @@ +package bootfromvolume + +import ( + "testing" + + osBFV "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/bootfromvolume" + "github.com/rackspace/gophercloud/openstack/compute/v2/servers" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestCreateOpts(t *testing.T) { + base := servers.CreateOpts{ + Name: "createdserver", + ImageRef: "asdfasdfasdf", + FlavorRef: "performance1-1", + } + + ext := osBFV.CreateOptsExt{ + CreateOptsBuilder: base, + BlockDevice: []osBFV.BlockDevice{ + osBFV.BlockDevice{ + UUID: "123456", + SourceType: osBFV.Image, + DestinationType: "volume", + VolumeSize: 10, + }, + }, + } + + expected := ` + { + "server": { + "name": "createdserver", + "imageRef": "asdfasdfasdf", + "flavorRef": "performance1-1", + "block_device_mapping_v2":[ + { + "uuid":"123456", + "source_type":"image", + "destination_type":"volume", + "boot_index": "0", + "delete_on_termination": "false", + "volume_size": "10" + } + ] + } + } + ` + actual, err := ext.ToServerCreateMap() + th.AssertNoErr(t, err) + th.CheckJSONEquals(t, expected, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/flavors/delegate.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/flavors/delegate.go new file mode 100644 index 0000000000..6bfc20c564 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/flavors/delegate.go @@ -0,0 +1,46 @@ +package flavors + +import ( + "github.com/rackspace/gophercloud" + os "github.com/rackspace/gophercloud/openstack/compute/v2/flavors" + "github.com/rackspace/gophercloud/pagination" +) + +// ListOpts helps control the results returned by the List() function. For example, a flavor with a +// minDisk field of 10 will not be returned if you specify MinDisk set to 20. +type ListOpts struct { + + // MinDisk and MinRAM, if provided, elide flavors that do not meet your criteria. + MinDisk int `q:"minDisk"` + MinRAM int `q:"minRam"` + + // Marker specifies the ID of the last flavor in the previous page. + Marker string `q:"marker"` + + // Limit instructs List to refrain from sending excessively large lists of flavors. + Limit int `q:"limit"` +} + +// ToFlavorListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToFlavorListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + if err != nil { + return "", err + } + return q.String(), nil +} + +// ListDetail enumerates the server images available to your account. +func ListDetail(client *gophercloud.ServiceClient, opts os.ListOptsBuilder) pagination.Pager { + return os.ListDetail(client, opts) +} + +// Get returns details about a single flavor, identity by ID. +func Get(client *gophercloud.ServiceClient, id string) os.GetResult { + return os.Get(client, id) +} + +// ExtractFlavors interprets a page of List results as Flavors. +func ExtractFlavors(page pagination.Page) ([]os.Flavor, error) { + return os.ExtractFlavors(page) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/flavors/delegate_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/flavors/delegate_test.go new file mode 100644 index 0000000000..204081dd17 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/flavors/delegate_test.go @@ -0,0 +1,62 @@ +package flavors + +import ( + "fmt" + "net/http" + "testing" + + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestListFlavors(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/flavors/detail", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + r.ParseForm() + marker := r.Form.Get("marker") + switch marker { + case "": + fmt.Fprintf(w, ListOutput) + case "performance1-2": + fmt.Fprintf(w, `{ "flavors": [] }`) + default: + t.Fatalf("Unexpected marker: [%s]", marker) + } + }) + + count := 0 + err := ListDetail(client.ServiceClient(), nil).EachPage(func(page pagination.Page) (bool, error) { + actual, err := ExtractFlavors(page) + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, ExpectedFlavorSlice, actual) + + count++ + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, 1, count) +} + +func TestGetFlavor(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/flavors/performance1-1", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, GetOutput) + }) + + actual, err := Get(client.ServiceClient(), "performance1-1").Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &Performance1Flavor, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/flavors/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/flavors/doc.go new file mode 100644 index 0000000000..278229ab97 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/flavors/doc.go @@ -0,0 +1,3 @@ +// Package flavors provides information and interaction with the flavor +// API resource for the Rackspace Cloud Servers service. +package flavors diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/flavors/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/flavors/fixtures.go new file mode 100644 index 0000000000..894f916f67 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/flavors/fixtures.go @@ -0,0 +1,129 @@ +// +build fixtures + +package flavors + +import ( + os "github.com/rackspace/gophercloud/openstack/compute/v2/flavors" +) + +// ListOutput is a sample response of a flavor List request. +const ListOutput = ` +{ + "flavors": [ + { + "OS-FLV-EXT-DATA:ephemeral": 0, + "OS-FLV-WITH-EXT-SPECS:extra_specs": { + "class": "performance1", + "disk_io_index": "40", + "number_of_data_disks": "0", + "policy_class": "performance_flavor", + "resize_policy_class": "performance_flavor" + }, + "disk": 20, + "id": "performance1-1", + "links": [ + { + "href": "https://iad.servers.api.rackspacecloud.com/v2/864477/flavors/performance1-1", + "rel": "self" + }, + { + "href": "https://iad.servers.api.rackspacecloud.com/864477/flavors/performance1-1", + "rel": "bookmark" + } + ], + "name": "1 GB Performance", + "ram": 1024, + "rxtx_factor": 200, + "swap": "", + "vcpus": 1 + }, + { + "OS-FLV-EXT-DATA:ephemeral": 20, + "OS-FLV-WITH-EXT-SPECS:extra_specs": { + "class": "performance1", + "disk_io_index": "40", + "number_of_data_disks": "1", + "policy_class": "performance_flavor", + "resize_policy_class": "performance_flavor" + }, + "disk": 40, + "id": "performance1-2", + "links": [ + { + "href": "https://iad.servers.api.rackspacecloud.com/v2/864477/flavors/performance1-2", + "rel": "self" + }, + { + "href": "https://iad.servers.api.rackspacecloud.com/864477/flavors/performance1-2", + "rel": "bookmark" + } + ], + "name": "2 GB Performance", + "ram": 2048, + "rxtx_factor": 400, + "swap": "", + "vcpus": 2 + } + ] +}` + +// GetOutput is a sample response from a flavor Get request. Its contents correspond to the +// Performance1Flavor struct. +const GetOutput = ` +{ + "flavor": { + "OS-FLV-EXT-DATA:ephemeral": 0, + "OS-FLV-WITH-EXT-SPECS:extra_specs": { + "class": "performance1", + "disk_io_index": "40", + "number_of_data_disks": "0", + "policy_class": "performance_flavor", + "resize_policy_class": "performance_flavor" + }, + "disk": 20, + "id": "performance1-1", + "links": [ + { + "href": "https://iad.servers.api.rackspacecloud.com/v2/864477/flavors/performance1-1", + "rel": "self" + }, + { + "href": "https://iad.servers.api.rackspacecloud.com/864477/flavors/performance1-1", + "rel": "bookmark" + } + ], + "name": "1 GB Performance", + "ram": 1024, + "rxtx_factor": 200, + "swap": "", + "vcpus": 1 + } +} +` + +// Performance1Flavor is the expected result of parsing GetOutput, or the first element of +// ListOutput. +var Performance1Flavor = os.Flavor{ + ID: "performance1-1", + Disk: 20, + RAM: 1024, + Name: "1 GB Performance", + RxTxFactor: 200.0, + Swap: 0, + VCPUs: 1, +} + +// Performance2Flavor is the second result expected from parsing ListOutput. +var Performance2Flavor = os.Flavor{ + ID: "performance1-2", + Disk: 40, + RAM: 2048, + Name: "2 GB Performance", + RxTxFactor: 400.0, + Swap: 0, + VCPUs: 2, +} + +// ExpectedFlavorSlice is the slice of Flavor structs that are expected to be parsed from +// ListOutput. +var ExpectedFlavorSlice = []os.Flavor{Performance1Flavor, Performance2Flavor} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/images/delegate.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/images/delegate.go new file mode 100644 index 0000000000..18e1f315af --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/images/delegate.go @@ -0,0 +1,22 @@ +package images + +import ( + "github.com/rackspace/gophercloud" + os "github.com/rackspace/gophercloud/openstack/compute/v2/images" + "github.com/rackspace/gophercloud/pagination" +) + +// ListDetail enumerates the available server images. +func ListDetail(client *gophercloud.ServiceClient, opts os.ListOptsBuilder) pagination.Pager { + return os.ListDetail(client, opts) +} + +// Get acquires additional detail about a specific image by ID. +func Get(client *gophercloud.ServiceClient, id string) os.GetResult { + return os.Get(client, id) +} + +// ExtractImages interprets a page as a collection of server images. +func ExtractImages(page pagination.Page) ([]os.Image, error) { + return os.ExtractImages(page) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/images/delegate_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/images/delegate_test.go new file mode 100644 index 0000000000..db0a6e3414 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/images/delegate_test.go @@ -0,0 +1,62 @@ +package images + +import ( + "fmt" + "net/http" + "testing" + + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestListImageDetails(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/images/detail", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + r.ParseForm() + marker := r.Form.Get("marker") + switch marker { + case "": + fmt.Fprintf(w, ListOutput) + case "e19a734c-c7e6-443a-830c-242209c4d65d": + fmt.Fprintf(w, `{ "images": [] }`) + default: + t.Fatalf("Unexpected marker: [%s]", marker) + } + }) + + count := 0 + err := ListDetail(client.ServiceClient(), nil).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractImages(page) + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, ExpectedImageSlice, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, 1, count) +} + +func TestGetImageDetails(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/images/e19a734c-c7e6-443a-830c-242209c4d65d", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, GetOutput) + }) + + actual, err := Get(client.ServiceClient(), "e19a734c-c7e6-443a-830c-242209c4d65d").Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &UbuntuImage, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/images/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/images/doc.go new file mode 100644 index 0000000000..cfae806712 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/images/doc.go @@ -0,0 +1,3 @@ +// Package images provides information and interaction with the image +// API resource for the Rackspace Cloud Servers service. +package images diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/images/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/images/fixtures.go new file mode 100644 index 0000000000..ccfbdc6a1e --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/images/fixtures.go @@ -0,0 +1,200 @@ +// +build fixtures + +package images + +import ( + os "github.com/rackspace/gophercloud/openstack/compute/v2/images" +) + +// ListOutput is an example response from an /images/detail request. +const ListOutput = ` +{ + "images": [ + { + "OS-DCF:diskConfig": "MANUAL", + "OS-EXT-IMG-SIZE:size": 1.017415075e+09, + "created": "2014-10-01T15:49:02Z", + "id": "30aa010e-080e-4d4b-a7f9-09fc55b07d69", + "links": [ + { + "href": "https://iad.servers.api.rackspacecloud.com/v2/111222/images/30aa010e-080e-4d4b-a7f9-09fc55b07d69", + "rel": "self" + }, + { + "href": "https://iad.servers.api.rackspacecloud.com/111222/images/30aa010e-080e-4d4b-a7f9-09fc55b07d69", + "rel": "bookmark" + }, + { + "href": "https://iad.servers.api.rackspacecloud.com/111222/images/30aa010e-080e-4d4b-a7f9-09fc55b07d69", + "rel": "alternate", + "type": "application/vnd.openstack.image" + } + ], + "metadata": { + "auto_disk_config": "disabled", + "cache_in_nova": "True", + "com.rackspace__1__build_core": "1", + "com.rackspace__1__build_managed": "1", + "com.rackspace__1__build_rackconnect": "1", + "com.rackspace__1__options": "0", + "com.rackspace__1__platform_target": "PublicCloud", + "com.rackspace__1__release_build_date": "2014-10-01_15-46-08", + "com.rackspace__1__release_id": "100", + "com.rackspace__1__release_version": "10", + "com.rackspace__1__source": "kickstart", + "com.rackspace__1__visible_core": "1", + "com.rackspace__1__visible_managed": "0", + "com.rackspace__1__visible_rackconnect": "0", + "image_type": "base", + "org.openstack__1__architecture": "x64", + "org.openstack__1__os_distro": "org.archlinux", + "org.openstack__1__os_version": "2014.8", + "os_distro": "arch", + "os_type": "linux", + "vm_mode": "hvm" + }, + "minDisk": 20, + "minRam": 512, + "name": "Arch 2014.10 (PVHVM)", + "progress": 100, + "status": "ACTIVE", + "updated": "2014-10-01T19:37:58Z" + }, + { + "OS-DCF:diskConfig": "AUTO", + "OS-EXT-IMG-SIZE:size": 1.060306463e+09, + "created": "2014-10-01T12:58:11Z", + "id": "e19a734c-c7e6-443a-830c-242209c4d65d", + "links": [ + { + "href": "https://iad.servers.api.rackspacecloud.com/v2/111222/images/e19a734c-c7e6-443a-830c-242209c4d65d", + "rel": "self" + }, + { + "href": "https://iad.servers.api.rackspacecloud.com/111222/images/e19a734c-c7e6-443a-830c-242209c4d65d", + "rel": "bookmark" + }, + { + "href": "https://iad.servers.api.rackspacecloud.com/111222/images/e19a734c-c7e6-443a-830c-242209c4d65d", + "rel": "alternate", + "type": "application/vnd.openstack.image" + } + ], + "metadata": { + "auto_disk_config": "True", + "cache_in_nova": "True", + "com.rackspace__1__build_core": "1", + "com.rackspace__1__build_managed": "1", + "com.rackspace__1__build_rackconnect": "1", + "com.rackspace__1__options": "0", + "com.rackspace__1__platform_target": "PublicCloud", + "com.rackspace__1__release_build_date": "2014-10-01_12-31-03", + "com.rackspace__1__release_id": "1007", + "com.rackspace__1__release_version": "6", + "com.rackspace__1__source": "kickstart", + "com.rackspace__1__visible_core": "1", + "com.rackspace__1__visible_managed": "1", + "com.rackspace__1__visible_rackconnect": "1", + "image_type": "base", + "org.openstack__1__architecture": "x64", + "org.openstack__1__os_distro": "com.ubuntu", + "org.openstack__1__os_version": "14.04", + "os_distro": "ubuntu", + "os_type": "linux", + "vm_mode": "xen" + }, + "minDisk": 20, + "minRam": 512, + "name": "Ubuntu 14.04 LTS (Trusty Tahr)", + "progress": 100, + "status": "ACTIVE", + "updated": "2014-10-01T15:51:44Z" + } + ] +} +` + +// GetOutput is an example response from an /images request. +const GetOutput = ` +{ + "image": { + "OS-DCF:diskConfig": "AUTO", + "OS-EXT-IMG-SIZE:size": 1060306463, + "created": "2014-10-01T12:58:11Z", + "id": "e19a734c-c7e6-443a-830c-242209c4d65d", + "links": [ + { + "href": "https://iad.servers.api.rackspacecloud.com/v2/111222/images/e19a734c-c7e6-443a-830c-242209c4d65d", + "rel": "self" + }, + { + "href": "https://iad.servers.api.rackspacecloud.com/111222/images/e19a734c-c7e6-443a-830c-242209c4d65d", + "rel": "bookmark" + }, + { + "href": "https://iad.servers.api.rackspacecloud.com/111222/images/e19a734c-c7e6-443a-830c-242209c4d65d", + "rel": "alternate", + "type": "application/vnd.openstack.image" + } + ], + "metadata": { + "auto_disk_config": "True", + "cache_in_nova": "True", + "com.rackspace__1__build_core": "1", + "com.rackspace__1__build_managed": "1", + "com.rackspace__1__build_rackconnect": "1", + "com.rackspace__1__options": "0", + "com.rackspace__1__platform_target": "PublicCloud", + "com.rackspace__1__release_build_date": "2014-10-01_12-31-03", + "com.rackspace__1__release_id": "1007", + "com.rackspace__1__release_version": "6", + "com.rackspace__1__source": "kickstart", + "com.rackspace__1__visible_core": "1", + "com.rackspace__1__visible_managed": "1", + "com.rackspace__1__visible_rackconnect": "1", + "image_type": "base", + "org.openstack__1__architecture": "x64", + "org.openstack__1__os_distro": "com.ubuntu", + "org.openstack__1__os_version": "14.04", + "os_distro": "ubuntu", + "os_type": "linux", + "vm_mode": "xen" + }, + "minDisk": 20, + "minRam": 512, + "name": "Ubuntu 14.04 LTS (Trusty Tahr)", + "progress": 100, + "status": "ACTIVE", + "updated": "2014-10-01T15:51:44Z" + } +} +` + +// ArchImage is the first Image structure that should be parsed from ListOutput. +var ArchImage = os.Image{ + ID: "30aa010e-080e-4d4b-a7f9-09fc55b07d69", + Name: "Arch 2014.10 (PVHVM)", + Created: "2014-10-01T15:49:02Z", + Updated: "2014-10-01T19:37:58Z", + MinDisk: 20, + MinRAM: 512, + Progress: 100, + Status: "ACTIVE", +} + +// UbuntuImage is the second Image structure that should be parsed from ListOutput and +// the only image that should be extracted from GetOutput. +var UbuntuImage = os.Image{ + ID: "e19a734c-c7e6-443a-830c-242209c4d65d", + Name: "Ubuntu 14.04 LTS (Trusty Tahr)", + Created: "2014-10-01T12:58:11Z", + Updated: "2014-10-01T15:51:44Z", + MinDisk: 20, + MinRAM: 512, + Progress: 100, + Status: "ACTIVE", +} + +// ExpectedImageSlice is the collection of images that should be parsed from ListOutput, +// in order. +var ExpectedImageSlice = []os.Image{ArchImage, UbuntuImage} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/keypairs/delegate.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/keypairs/delegate.go new file mode 100644 index 0000000000..3e53525dc7 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/keypairs/delegate.go @@ -0,0 +1,33 @@ +package keypairs + +import ( + "github.com/rackspace/gophercloud" + os "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs" + "github.com/rackspace/gophercloud/pagination" +) + +// List returns a Pager that allows you to iterate over a collection of KeyPairs. +func List(client *gophercloud.ServiceClient) pagination.Pager { + return os.List(client) +} + +// Create requests the creation of a new keypair on the server, or to import a pre-existing +// keypair. +func Create(client *gophercloud.ServiceClient, opts os.CreateOptsBuilder) os.CreateResult { + return os.Create(client, opts) +} + +// Get returns public data about a previously uploaded KeyPair. +func Get(client *gophercloud.ServiceClient, name string) os.GetResult { + return os.Get(client, name) +} + +// Delete requests the deletion of a previous stored KeyPair from the server. +func Delete(client *gophercloud.ServiceClient, name string) os.DeleteResult { + return os.Delete(client, name) +} + +// ExtractKeyPairs interprets a page of results as a slice of KeyPairs. +func ExtractKeyPairs(page pagination.Page) ([]os.KeyPair, error) { + return os.ExtractKeyPairs(page) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/keypairs/delegate_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/keypairs/delegate_test.go new file mode 100644 index 0000000000..62e5df950c --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/keypairs/delegate_test.go @@ -0,0 +1,72 @@ +package keypairs + +import ( + "testing" + + os "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleListSuccessfully(t) + + count := 0 + err := List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractKeyPairs(page) + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, os.ExpectedKeyPairSlice, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, 1, count) +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleCreateSuccessfully(t) + + actual, err := Create(client.ServiceClient(), os.CreateOpts{ + Name: "createdkey", + }).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &os.CreatedKeyPair, actual) +} + +func TestImport(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleImportSuccessfully(t) + + actual, err := Create(client.ServiceClient(), os.CreateOpts{ + Name: "importedkey", + PublicKey: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDx8nkQv/zgGgB4rMYmIf+6A4l6Rr+o/6lHBQdW5aYd44bd8JttDCE/F/pNRr0lRE+PiqSPO8nDPHw0010JeMH9gYgnnFlyY3/OcJ02RhIPyyxYpv9FhY+2YiUkpwFOcLImyrxEsYXpD/0d3ac30bNH6Sw9JD9UZHYcpSxsIbECHw== Generated by Nova", + }).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &os.ImportedKeyPair, actual) +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleGetSuccessfully(t) + + actual, err := Get(client.ServiceClient(), "firstkey").Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &os.FirstKeyPair, actual) +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleDeleteSuccessfully(t) + + err := Delete(client.ServiceClient(), "deletedkey").ExtractErr() + th.AssertNoErr(t, err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/keypairs/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/keypairs/doc.go new file mode 100644 index 0000000000..31713752ea --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/keypairs/doc.go @@ -0,0 +1,3 @@ +// Package keypairs provides information and interaction with the keypair +// API resource for the Rackspace Cloud Servers service. +package keypairs diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/networks/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/networks/doc.go new file mode 100644 index 0000000000..8e5c77382d --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/networks/doc.go @@ -0,0 +1,3 @@ +// Package networks provides information and interaction with the network +// API resource for the Rackspace Cloud Servers service. +package networks diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/networks/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/networks/requests.go new file mode 100644 index 0000000000..d3c973ecb2 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/networks/requests.go @@ -0,0 +1,101 @@ +package networks + +import ( + "errors" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" + + "github.com/racker/perigee" +) + +// List returns a Pager which allows you to iterate over a collection of +// networks. It accepts a ListOpts struct, which allows you to filter and sort +// the returned collection for greater efficiency. +func List(c *gophercloud.ServiceClient) pagination.Pager { + createPage := func(r pagination.PageResult) pagination.Page { + return NetworkPage{pagination.SinglePageBase(r)} + } + + return pagination.NewPager(c, listURL(c), createPage) +} + +// Get retrieves a specific network based on its unique ID. +func Get(c *gophercloud.ServiceClient, id string) GetResult { + var res GetResult + _, res.Err = perigee.Request("GET", getURL(c, id), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + Results: &res.Body, + OkCodes: []int{200}, + }) + return res +} + +// CreateOptsBuilder is the interface options structs have to satisfy in order +// to be used in the main Create operation in this package. Since many +// extensions decorate or modify the common logic, it is useful for them to +// satisfy a basic interface in order for them to be used. +type CreateOptsBuilder interface { + ToNetworkCreateMap() (map[string]interface{}, error) +} + +// CreateOpts is the common options struct used in this package's Create +// operation. +type CreateOpts struct { + // REQUIRED. See Network object for more info. + CIDR string + // REQUIRED. See Network object for more info. + Label string +} + +// ToNetworkCreateMap casts a CreateOpts struct to a map. +func (opts CreateOpts) ToNetworkCreateMap() (map[string]interface{}, error) { + n := make(map[string]interface{}) + + if opts.CIDR == "" { + return nil, errors.New("Required field CIDR not set.") + } + if opts.Label == "" { + return nil, errors.New("Required field Label not set.") + } + + n["label"] = opts.Label + n["cidr"] = opts.CIDR + return map[string]interface{}{"network": n}, nil +} + +// Create accepts a CreateOpts struct and creates a new network using the values +// provided. This operation does not actually require a request body, i.e. the +// CreateOpts struct argument can be empty. +// +// The tenant ID that is contained in the URI is the tenant that creates the +// network. An admin user, however, has the option of specifying another tenant +// ID in the CreateOpts struct. +func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult { + var res CreateResult + + reqBody, err := opts.ToNetworkCreateMap() + if err != nil { + res.Err = err + return res + } + + // Send request to API + _, res.Err = perigee.Request("POST", createURL(c), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + ReqBody: &reqBody, + Results: &res.Body, + OkCodes: []int{200, 201, 202}, + }) + return res +} + +// Delete accepts a unique ID and deletes the network associated with it. +func Delete(c *gophercloud.ServiceClient, networkID string) DeleteResult { + var res DeleteResult + _, res.Err = perigee.Request("DELETE", deleteURL(c, networkID), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + OkCodes: []int{204}, + }) + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/networks/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/networks/requests_test.go new file mode 100644 index 0000000000..6f44c1caba --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/networks/requests_test.go @@ -0,0 +1,156 @@ +package networks + +import ( + "fmt" + "net/http" + "testing" + + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/os-networksv2", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "networks": [ + { + "label": "test-network-1", + "cidr": "192.168.100.0/24", + "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22" + }, + { + "label": "test-network-2", + "cidr": "192.30.250.00/18", + "id": "db193ab3-96e3-4cb3-8fc5-05f4296d0324" + } + ] +} + `) + }) + + client := fake.ServiceClient() + count := 0 + + err := List(client).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractNetworks(page) + if err != nil { + t.Errorf("Failed to extract networks: %v", err) + return false, err + } + + expected := []Network{ + Network{ + Label: "test-network-1", + CIDR: "192.168.100.0/24", + ID: "d32019d3-bc6e-4319-9c1d-6722fc136a22", + }, + Network{ + Label: "test-network-2", + CIDR: "192.30.250.00/18", + ID: "db193ab3-96e3-4cb3-8fc5-05f4296d0324", + }, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, 1, count) +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/os-networksv2/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "network": { + "label": "test-network-1", + "cidr": "192.168.100.0/24", + "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22" + } +} + `) + }) + + n, err := Get(fake.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22").Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, n.CIDR, "192.168.100.0/24") + th.AssertEquals(t, n.Label, "test-network-1") + th.AssertEquals(t, n.ID, "d32019d3-bc6e-4319-9c1d-6722fc136a22") +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/os-networksv2", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "network": { + "label": "test-network-1", + "cidr": "192.168.100.0/24" + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, ` +{ + "network": { + "label": "test-network-1", + "cidr": "192.168.100.0/24", + "id": "4e8e5957-649f-477b-9e5b-f1f75b21c03c" + } +} + `) + }) + + options := CreateOpts{Label: "test-network-1", CIDR: "192.168.100.0/24"} + n, err := Create(fake.ServiceClient(), options).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, n.Label, "test-network-1") + th.AssertEquals(t, n.ID, "4e8e5957-649f-477b-9e5b-f1f75b21c03c") +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/os-networksv2/4e8e5957-649f-477b-9e5b-f1f75b21c03c", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusNoContent) + }) + + res := Delete(fake.ServiceClient(), "4e8e5957-649f-477b-9e5b-f1f75b21c03c") + th.AssertNoErr(t, res.Err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/networks/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/networks/results.go new file mode 100644 index 0000000000..eb6a76c008 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/networks/results.go @@ -0,0 +1,81 @@ +package networks + +import ( + "github.com/mitchellh/mapstructure" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +type commonResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts a network resource. +func (r commonResult) Extract() (*Network, error) { + if r.Err != nil { + return nil, r.Err + } + + var res struct { + Network *Network `json:"network"` + } + + err := mapstructure.Decode(r.Body, &res) + + return res.Network, err +} + +// CreateResult represents the result of a create operation. +type CreateResult struct { + commonResult +} + +// GetResult represents the result of a get operation. +type GetResult struct { + commonResult +} + +// DeleteResult represents the result of a delete operation. +type DeleteResult struct { + gophercloud.ErrResult +} + +// Network represents, well, a network. +type Network struct { + // UUID for the network + ID string `mapstructure:"id" json:"id"` + + // Human-readable name for the network. Might not be unique. + Label string `mapstructure:"label" json:"label"` + + // Classless Inter-Domain Routing + CIDR string `mapstructure:"cidr" json:"cidr"` +} + +// NetworkPage is the page returned by a pager when traversing over a +// collection of networks. +type NetworkPage struct { + pagination.SinglePageBase +} + +// IsEmpty returns true if the NetworkPage contains no Networks. +func (r NetworkPage) IsEmpty() (bool, error) { + networks, err := ExtractNetworks(r) + if err != nil { + return true, err + } + return len(networks) == 0, nil +} + +// ExtractNetworks accepts a Page struct, specifically a NetworkPage struct, +// and extracts the elements into a slice of Network structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractNetworks(page pagination.Page) ([]Network, error) { + var resp struct { + Networks []Network `mapstructure:"networks" json:"networks"` + } + + err := mapstructure.Decode(page.(NetworkPage).Body, &resp) + + return resp.Networks, err +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/networks/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/networks/urls.go new file mode 100644 index 0000000000..19a21aa90d --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/networks/urls.go @@ -0,0 +1,27 @@ +package networks + +import "github.com/rackspace/gophercloud" + +func resourceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL("os-networksv2", id) +} + +func rootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("os-networksv2") +} + +func getURL(c *gophercloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func listURL(c *gophercloud.ServiceClient) string { + return rootURL(c) +} + +func createURL(c *gophercloud.ServiceClient) string { + return rootURL(c) +} + +func deleteURL(c *gophercloud.ServiceClient, id string) string { + return resourceURL(c, id) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/networks/urls_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/networks/urls_test.go new file mode 100644 index 0000000000..983992e2b9 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/networks/urls_test.go @@ -0,0 +1,38 @@ +package networks + +import ( + "testing" + + "github.com/rackspace/gophercloud" + th "github.com/rackspace/gophercloud/testhelper" +) + +const endpoint = "http://localhost:57909/" + +func endpointClient() *gophercloud.ServiceClient { + return &gophercloud.ServiceClient{Endpoint: endpoint} +} + +func TestGetURL(t *testing.T) { + actual := getURL(endpointClient(), "foo") + expected := endpoint + "os-networksv2/foo" + th.AssertEquals(t, expected, actual) +} + +func TestCreateURL(t *testing.T) { + actual := createURL(endpointClient()) + expected := endpoint + "os-networksv2" + th.AssertEquals(t, expected, actual) +} + +func TestListURL(t *testing.T) { + actual := createURL(endpointClient()) + expected := endpoint + "os-networksv2" + th.AssertEquals(t, expected, actual) +} + +func TestDeleteURL(t *testing.T) { + actual := deleteURL(endpointClient(), "foo") + expected := endpoint + "os-networksv2/foo" + th.AssertEquals(t, expected, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/servers/delegate.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/servers/delegate.go new file mode 100644 index 0000000000..4c7b24909a --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/servers/delegate.go @@ -0,0 +1,61 @@ +package servers + +import ( + "github.com/rackspace/gophercloud" + os "github.com/rackspace/gophercloud/openstack/compute/v2/servers" + "github.com/rackspace/gophercloud/pagination" +) + +// List makes a request against the API to list servers accessible to you. +func List(client *gophercloud.ServiceClient, opts os.ListOptsBuilder) pagination.Pager { + return os.List(client, opts) +} + +// Create requests a server to be provisioned to the user in the current tenant. +func Create(client *gophercloud.ServiceClient, opts os.CreateOptsBuilder) os.CreateResult { + return os.Create(client, opts) +} + +// Delete requests that a server previously provisioned be removed from your account. +func Delete(client *gophercloud.ServiceClient, id string) os.DeleteResult { + return os.Delete(client, id) +} + +// Get requests details on a single server, by ID. +func Get(client *gophercloud.ServiceClient, id string) os.GetResult { + return os.Get(client, id) +} + +// ChangeAdminPassword alters the administrator or root password for a specified server. +func ChangeAdminPassword(client *gophercloud.ServiceClient, id, newPassword string) os.ActionResult { + return os.ChangeAdminPassword(client, id, newPassword) +} + +// Reboot requests that a given server reboot. Two methods exist for rebooting a server: +// +// os.HardReboot (aka PowerCycle) restarts the server instance by physically cutting power to the +// machine, or if a VM, terminating it at the hypervisor level. It's done. Caput. Full stop. Then, +// after a brief wait, power is restored or the VM instance restarted. +// +// os.SoftReboot (aka OSReboot) simply tells the OS to restart under its own procedures. E.g., in +// Linux, asking it to enter runlevel 6, or executing "sudo shutdown -r now", or by asking Windows to restart the machine. +func Reboot(client *gophercloud.ServiceClient, id string, how os.RebootMethod) os.ActionResult { + return os.Reboot(client, id, how) +} + +// Rebuild will reprovision the server according to the configuration options provided in the +// RebuildOpts struct. +func Rebuild(client *gophercloud.ServiceClient, id string, opts os.RebuildOptsBuilder) os.RebuildResult { + return os.Rebuild(client, id, opts) +} + +// WaitForStatus will continually poll a server until it successfully transitions to a specified +// status. It will do this for at most the number of seconds specified. +func WaitForStatus(c *gophercloud.ServiceClient, id, status string, secs int) error { + return os.WaitForStatus(c, id, status, secs) +} + +// ExtractServers interprets the results of a single page from a List() call, producing a slice of Server entities. +func ExtractServers(page pagination.Page) ([]os.Server, error) { + return os.ExtractServers(page) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/servers/delegate_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/servers/delegate_test.go new file mode 100644 index 0000000000..7f414040f7 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/servers/delegate_test.go @@ -0,0 +1,112 @@ +package servers + +import ( + "fmt" + "net/http" + "testing" + + os "github.com/rackspace/gophercloud/openstack/compute/v2/servers" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestListServers(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/servers/detail", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, ListOutput) + }) + + count := 0 + err := List(client.ServiceClient(), nil).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractServers(page) + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, ExpectedServerSlice, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, 1, count) +} + +func TestCreateServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleServerCreationSuccessfully(t, CreateOutput) + + actual, err := Create(client.ServiceClient(), os.CreateOpts{ + Name: "derp", + ImageRef: "f90f6034-2570-4974-8351-6b49732ef2eb", + FlavorRef: "1", + }).Extract() + th.AssertNoErr(t, err) + + th.CheckDeepEquals(t, &CreatedServer, actual) +} + +func TestDeleteServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleServerDeletionSuccessfully(t) + + res := Delete(client.ServiceClient(), "asdfasdfasdf") + th.AssertNoErr(t, res.Err) +} + +func TestGetServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/servers/8c65cb68-0681-4c30-bc88-6b83a8a26aee", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, GetOutput) + }) + + actual, err := Get(client.ServiceClient(), "8c65cb68-0681-4c30-bc88-6b83a8a26aee").Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &GophercloudServer, actual) +} + +func TestChangeAdminPassword(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleAdminPasswordChangeSuccessfully(t) + + res := ChangeAdminPassword(client.ServiceClient(), "1234asdf", "new-password") + th.AssertNoErr(t, res.Err) +} + +func TestReboot(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleRebootSuccessfully(t) + + res := Reboot(client.ServiceClient(), "1234asdf", os.SoftReboot) + th.AssertNoErr(t, res.Err) +} + +func TestRebuildServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleRebuildSuccessfully(t, GetOutput) + + opts := os.RebuildOpts{ + Name: "new-name", + AdminPass: "swordfish", + ImageID: "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/images/f90f6034-2570-4974-8351-6b49732ef2eb", + AccessIPv4: "1.2.3.4", + } + actual, err := Rebuild(client.ServiceClient(), "1234asdf", opts).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &GophercloudServer, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/servers/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/servers/doc.go new file mode 100644 index 0000000000..c9f77f6945 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/servers/doc.go @@ -0,0 +1,3 @@ +// Package servers provides information and interaction with the server +// API resource for the Rackspace Cloud Servers service. +package servers diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/servers/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/servers/fixtures.go new file mode 100644 index 0000000000..b22a28998d --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/servers/fixtures.go @@ -0,0 +1,439 @@ +// +build fixtures + +package servers + +import ( + os "github.com/rackspace/gophercloud/openstack/compute/v2/servers" +) + +// ListOutput is the recorded output of a Rackspace servers.List request. +const ListOutput = ` +{ + "servers": [ + { + "OS-DCF:diskConfig": "MANUAL", + "OS-EXT-STS:power_state": 1, + "OS-EXT-STS:task_state": null, + "OS-EXT-STS:vm_state": "active", + "accessIPv4": "1.2.3.4", + "accessIPv6": "1111:4822:7818:121:2000:9b5e:7438:a2d0", + "addresses": { + "private": [ + { + "addr": "10.208.230.113", + "version": 4 + } + ], + "public": [ + { + "addr": "2001:4800:7818:101:2000:9b5e:7428:a2d0", + "version": 6 + }, + { + "addr": "104.130.131.164", + "version": 4 + } + ] + }, + "created": "2014-09-23T12:34:58Z", + "flavor": { + "id": "performance1-8", + "links": [ + { + "href": "https://dfw.servers.api.rackspacecloud.com/111111/flavors/performance1-8", + "rel": "bookmark" + } + ] + }, + "hostId": "e8951a524bc465b0898aeac7674da6fe1495e253ae1ea17ddb2c2475", + "id": "59818cee-bc8c-44eb-8073-673ee65105f7", + "image": { + "id": "255df5fb-e3d4-45a3-9a07-c976debf7c14", + "links": [ + { + "href": "https://dfw.servers.api.rackspacecloud.com/111111/images/255df5fb-e3d4-45a3-9a07-c976debf7c14", + "rel": "bookmark" + } + ] + }, + "key_name": "mykey", + "links": [ + { + "href": "https://dfw.servers.api.rackspacecloud.com/v2/111111/servers/59818cee-bc8c-44eb-8073-673ee65105f7", + "rel": "self" + }, + { + "href": "https://dfw.servers.api.rackspacecloud.com/111111/servers/59818cee-bc8c-44eb-8073-673ee65105f7", + "rel": "bookmark" + } + ], + "metadata": {}, + "name": "devstack", + "progress": 100, + "status": "ACTIVE", + "tenant_id": "111111", + "updated": "2014-09-23T12:38:19Z", + "user_id": "14ae7bb21d81422694655f3cc30f2930" + }, + { + "OS-DCF:diskConfig": "MANUAL", + "OS-EXT-STS:power_state": 1, + "OS-EXT-STS:task_state": null, + "OS-EXT-STS:vm_state": "active", + "accessIPv4": "1.1.2.3", + "accessIPv6": "2222:4444:7817:101:be76:4eff:f0e5:9e02", + "addresses": { + "private": [ + { + "addr": "10.10.20.30", + "version": 4 + } + ], + "public": [ + { + "addr": "1.1.2.3", + "version": 4 + }, + { + "addr": "2222:4444:7817:101:be76:4eff:f0e5:9e02", + "version": 6 + } + ] + }, + "created": "2014-07-21T19:32:55Z", + "flavor": { + "id": "performance1-2", + "links": [ + { + "href": "https://dfw.servers.api.rackspacecloud.com/111111/flavors/performance1-2", + "rel": "bookmark" + } + ] + }, + "hostId": "f859679906d6b1a38c1bd516b78f4dcc7d5fcf012578fa3ce460716c", + "id": "25f1c7f5-e00a-4715-b354-16e24b2f4630", + "image": { + "id": "bb02b1a3-bc77-4d17-ab5b-421d89850fca", + "links": [ + { + "href": "https://dfw.servers.api.rackspacecloud.com/111111/images/bb02b1a3-bc77-4d17-ab5b-421d89850fca", + "rel": "bookmark" + } + ] + }, + "key_name": "otherkey", + "links": [ + { + "href": "https://dfw.servers.api.rackspacecloud.com/v2/111111/servers/25f1c7f5-e00a-4715-b355-16e24b2f4630", + "rel": "self" + }, + { + "href": "https://dfw.servers.api.rackspacecloud.com/111111/servers/25f1c7f5-e00a-4715-b355-16e24b2f4630", + "rel": "bookmark" + } + ], + "metadata": {}, + "name": "peril-dfw", + "progress": 100, + "status": "ACTIVE", + "tenant_id": "111111", + "updated": "2014-07-21T19:34:24Z", + "user_id": "14ae7bb21d81422694655f3cc30f2930" + } + ] +} +` + +// GetOutput is the recorded output of a Rackspace servers.Get request. +const GetOutput = ` +{ + "server": { + "OS-DCF:diskConfig": "AUTO", + "OS-EXT-STS:power_state": 1, + "OS-EXT-STS:task_state": null, + "OS-EXT-STS:vm_state": "active", + "accessIPv4": "1.2.4.8", + "accessIPv6": "2001:4800:6666:105:2a0f:c056:f594:7777", + "addresses": { + "private": [ + { + "addr": "10.20.40.80", + "version": 4 + } + ], + "public": [ + { + "addr": "1.2.4.8", + "version": 4 + }, + { + "addr": "2001:4800:6666:105:2a0f:c056:f594:7777", + "version": 6 + } + ] + }, + "created": "2014-10-21T14:42:16Z", + "flavor": { + "id": "performance1-1", + "links": [ + { + "href": "https://dfw.servers.api.rackspacecloud.com/111111/flavors/performance1-1", + "rel": "bookmark" + } + ] + }, + "hostId": "430d2ae02de0a7af77012c94778145eccf67e75b1fac0528aa10d4a7", + "id": "8c65cb68-0681-4c30-bc88-6b83a8a26aee", + "image": { + "id": "e19a734c-c7e6-443a-830c-242209c4d65d", + "links": [ + { + "href": "https://dfw.servers.api.rackspacecloud.com/111111/images/e19a734c-c7e6-443a-830c-242209c4d65d", + "rel": "bookmark" + } + ] + }, + "key_name": null, + "links": [ + { + "href": "https://dfw.servers.api.rackspacecloud.com/v2/111111/servers/8c65cb68-0681-4c30-bc88-6b83a8a26aee", + "rel": "self" + }, + { + "href": "https://dfw.servers.api.rackspacecloud.com/111111/servers/8c65cb68-0681-4c30-bc88-6b83a8a26aee", + "rel": "bookmark" + } + ], + "metadata": {}, + "name": "Gophercloud-pxpGGuey", + "progress": 100, + "status": "ACTIVE", + "tenant_id": "111111", + "updated": "2014-10-21T14:42:57Z", + "user_id": "14ae7bb21d81423694655f4dd30f2930" + } +} +` + +// CreateOutput contains a sample of Rackspace's response to a Create call. +const CreateOutput = ` +{ + "server": { + "OS-DCF:diskConfig": "AUTO", + "adminPass": "v7tADqbE5pr9", + "id": "bb63327b-6a2f-34bc-b0ef-4b6d97ea637e", + "links": [ + { + "href": "https://dfw.servers.api.rackspacecloud.com/v2/111111/servers/bb63327b-6a2f-34bc-b0ef-4b6d97ea637e", + "rel": "self" + }, + { + "href": "https://dfw.servers.api.rackspacecloud.com/111111/servers/bb63327b-6a2f-34bc-b0ef-4b6d97ea637e", + "rel": "bookmark" + } + ] + } +} +` + +// DevstackServer is the expected first result from parsing ListOutput. +var DevstackServer = os.Server{ + ID: "59818cee-bc8c-44eb-8073-673ee65105f7", + Name: "devstack", + TenantID: "111111", + UserID: "14ae7bb21d81422694655f3cc30f2930", + HostID: "e8951a524bc465b0898aeac7674da6fe1495e253ae1ea17ddb2c2475", + Updated: "2014-09-23T12:38:19Z", + Created: "2014-09-23T12:34:58Z", + AccessIPv4: "1.2.3.4", + AccessIPv6: "1111:4822:7818:121:2000:9b5e:7438:a2d0", + Progress: 100, + Status: "ACTIVE", + Image: map[string]interface{}{ + "id": "255df5fb-e3d4-45a3-9a07-c976debf7c14", + "links": []interface{}{ + map[string]interface{}{ + "href": "https://dfw.servers.api.rackspacecloud.com/111111/images/255df5fb-e3d4-45a3-9a07-c976debf7c14", + "rel": "bookmark", + }, + }, + }, + Flavor: map[string]interface{}{ + "id": "performance1-8", + "links": []interface{}{ + map[string]interface{}{ + "href": "https://dfw.servers.api.rackspacecloud.com/111111/flavors/performance1-8", + "rel": "bookmark", + }, + }, + }, + Addresses: map[string]interface{}{ + "private": []interface{}{ + map[string]interface{}{ + "addr": "10.20.30.40", + "version": float64(4.0), + }, + }, + "public": []interface{}{ + map[string]interface{}{ + "addr": "1111:4822:7818:121:2000:9b5e:7438:a2d0", + "version": float64(6.0), + }, + map[string]interface{}{ + "addr": "1.2.3.4", + "version": float64(4.0), + }, + }, + }, + Metadata: map[string]interface{}{}, + Links: []interface{}{ + map[string]interface{}{ + "href": "https://dfw.servers.api.rackspacecloud.com/v2/111111/servers/59918cee-bd9d-44eb-8173-673ee75105f7", + "rel": "self", + }, + map[string]interface{}{ + "href": "https://dfw.servers.api.rackspacecloud.com/v2/111111/servers/59818cee-bc8c-44eb-8073-673ee65105f7", + "rel": "bookmark", + }, + }, + KeyName: "mykey", + AdminPass: "", +} + +// PerilServer is the expected second result from parsing ListOutput. +var PerilServer = os.Server{ + ID: "25f1c7f5-e00a-4715-b354-16e24b2f4630", + Name: "peril-dfw", + TenantID: "111111", + UserID: "14ae7bb21d81422694655f3cc30f2930", + HostID: "f859679906d6b1a38c1bd516b78f4dcc7d5fcf012578fa3ce460716c", + Updated: "2014-07-21T19:34:24Z", + Created: "2014-07-21T19:32:55Z", + AccessIPv4: "1.1.2.3", + AccessIPv6: "2222:4444:7817:101:be76:4eff:f0e5:9e02", + Progress: 100, + Status: "ACTIVE", + Image: map[string]interface{}{ + "id": "bb02b1a3-bc77-4d17-ab5b-421d89850fca", + "links": []interface{}{ + map[string]interface{}{ + "href": "https://dfw.servers.api.rackspacecloud.com/111111/images/bb02b1a3-bc77-4d17-ab5b-421d89850fca", + "rel": "bookmark", + }, + }, + }, + Flavor: map[string]interface{}{ + "id": "performance1-2", + "links": []interface{}{ + map[string]interface{}{ + "href": "https://dfw.servers.api.rackspacecloud.com/111111/flavors/performance1-2", + "rel": "bookmark", + }, + }, + }, + Addresses: map[string]interface{}{ + "private": []interface{}{ + map[string]interface{}{ + "addr": "10.10.20.30", + "version": float64(4.0), + }, + }, + "public": []interface{}{ + map[string]interface{}{ + "addr": "2222:4444:7817:101:be76:4eff:f0e5:9e02", + "version": float64(6.0), + }, + map[string]interface{}{ + "addr": "1.1.2.3", + "version": float64(4.0), + }, + }, + }, + Metadata: map[string]interface{}{}, + Links: []interface{}{ + map[string]interface{}{ + "href": "https://dfw.servers.api.rackspacecloud.com/v2/111111/servers/25f1c7f5-e00a-4715-b355-16e24b2f4630", + "rel": "self", + }, + map[string]interface{}{ + "href": "https://dfw.servers.api.rackspacecloud.com/v2/111111/servers/25f1c7f5-e00a-4715-b355-16e24b2f4630", + "rel": "bookmark", + }, + }, + KeyName: "otherkey", + AdminPass: "", +} + +// GophercloudServer is the expected result from parsing GetOutput. +var GophercloudServer = os.Server{ + ID: "8c65cb68-0681-4c30-bc88-6b83a8a26aee", + Name: "Gophercloud-pxpGGuey", + TenantID: "111111", + UserID: "14ae7bb21d81423694655f4dd30f2930", + HostID: "430d2ae02de0a7af77012c94778145eccf67e75b1fac0528aa10d4a7", + Updated: "2014-10-21T14:42:57Z", + Created: "2014-10-21T14:42:16Z", + AccessIPv4: "1.2.4.8", + AccessIPv6: "2001:4800:6666:105:2a0f:c056:f594:7777", + Progress: 100, + Status: "ACTIVE", + Image: map[string]interface{}{ + "id": "e19a734c-c7e6-443a-830c-242209c4d65d", + "links": []interface{}{ + map[string]interface{}{ + "href": "https://dfw.servers.api.rackspacecloud.com/111111/images/e19a734c-c7e6-443a-830c-242209c4d65d", + "rel": "bookmark", + }, + }, + }, + Flavor: map[string]interface{}{ + "id": "performance1-1", + "links": []interface{}{ + map[string]interface{}{ + "href": "https://dfw.servers.api.rackspacecloud.com/111111/flavors/performance1-1", + "rel": "bookmark", + }, + }, + }, + Addresses: map[string]interface{}{ + "private": []interface{}{ + map[string]interface{}{ + "addr": "10.20.40.80", + "version": float64(4.0), + }, + }, + "public": []interface{}{ + map[string]interface{}{ + "addr": "2001:4800:6666:105:2a0f:c056:f594:7777", + "version": float64(6.0), + }, + map[string]interface{}{ + "addr": "1.2.4.8", + "version": float64(4.0), + }, + }, + }, + Metadata: map[string]interface{}{}, + Links: []interface{}{ + map[string]interface{}{ + "href": "https://dfw.servers.api.rackspacecloud.com/v2/111111/servers/8c65cb68-0681-4c30-bc88-6b83a8a26aee", + "rel": "self", + }, + map[string]interface{}{ + "href": "https://dfw.servers.api.rackspacecloud.com/111111/servers/8c65cb68-0681-4c30-bc88-6b83a8a26aee", + "rel": "bookmark", + }, + }, + KeyName: "", + AdminPass: "", +} + +// CreatedServer is the partial Server struct that can be parsed from CreateOutput. +var CreatedServer = os.Server{ + ID: "bb63327b-6a2f-34bc-b0ef-4b6d97ea637e", + AdminPass: "v7tADqbE5pr9", + Links: []interface{}{}, +} + +// ExpectedServerSlice is the collection of servers, in order, that should be parsed from ListOutput. +var ExpectedServerSlice = []os.Server{DevstackServer, PerilServer} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/servers/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/servers/requests.go new file mode 100644 index 0000000000..809183ec7c --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/servers/requests.go @@ -0,0 +1,163 @@ +package servers + +import ( + "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/bootfromvolume" + "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/diskconfig" + os "github.com/rackspace/gophercloud/openstack/compute/v2/servers" +) + +// CreateOpts specifies all of the options that Rackspace accepts in its Create request, including +// the union of all extensions that Rackspace supports. +type CreateOpts struct { + // Name [required] is the name to assign to the newly launched server. + Name string + + // ImageRef [required] is the ID or full URL to the image that contains the server's OS and initial state. + // Optional if using the boot-from-volume extension. + ImageRef string + + // FlavorRef [required] is the ID or full URL to the flavor that describes the server's specs. + FlavorRef string + + // SecurityGroups [optional] lists the names of the security groups to which this server should belong. + SecurityGroups []string + + // UserData [optional] contains configuration information or scripts to use upon launch. + // Create will base64-encode it for you. + UserData []byte + + // AvailabilityZone [optional] in which to launch the server. + AvailabilityZone string + + // Networks [optional] dictates how this server will be attached to available networks. + // By default, the server will be attached to all isolated networks for the tenant. + Networks []os.Network + + // Metadata [optional] contains key-value pairs (up to 255 bytes each) to attach to the server. + Metadata map[string]string + + // Personality [optional] includes the path and contents of a file to inject into the server at launch. + // The maximum size of the file is 255 bytes (decoded). + Personality []byte + + // ConfigDrive [optional] enables metadata injection through a configuration drive. + ConfigDrive bool + + // AdminPass [optional] sets the root user password. If not set, a randomly-generated + // password will be created and returned in the response. + AdminPass string + + // Rackspace-specific extensions begin here. + + // KeyPair [optional] specifies the name of the SSH KeyPair to be injected into the newly launched + // server. See the "keypairs" extension in OpenStack compute v2. + KeyPair string + + // DiskConfig [optional] controls how the created server's disk is partitioned. See the "diskconfig" + // extension in OpenStack compute v2. + DiskConfig diskconfig.DiskConfig + + // BlockDevice [optional] will create the server from a volume, which is created from an image, + // a snapshot, or an another volume. + BlockDevice []bootfromvolume.BlockDevice +} + +// ToServerCreateMap constructs a request body using all of the OpenStack extensions that are +// active on Rackspace. +func (opts CreateOpts) ToServerCreateMap() (map[string]interface{}, error) { + base := os.CreateOpts{ + Name: opts.Name, + ImageRef: opts.ImageRef, + FlavorRef: opts.FlavorRef, + SecurityGroups: opts.SecurityGroups, + UserData: opts.UserData, + AvailabilityZone: opts.AvailabilityZone, + Networks: opts.Networks, + Metadata: opts.Metadata, + Personality: opts.Personality, + ConfigDrive: opts.ConfigDrive, + AdminPass: opts.AdminPass, + } + + drive := diskconfig.CreateOptsExt{ + CreateOptsBuilder: base, + DiskConfig: opts.DiskConfig, + } + + res, err := drive.ToServerCreateMap() + if err != nil { + return nil, err + } + + if len(opts.BlockDevice) != 0 { + bfv := bootfromvolume.CreateOptsExt{ + CreateOptsBuilder: drive, + BlockDevice: opts.BlockDevice, + } + + res, err = bfv.ToServerCreateMap() + if err != nil { + return nil, err + } + } + + // key_name doesn't actually come from the extension (or at least isn't documented there) so + // we need to add it manually. + serverMap := res["server"].(map[string]interface{}) + serverMap["key_name"] = opts.KeyPair + + return res, nil +} + +// RebuildOpts represents all of the configuration options used in a server rebuild operation that +// are supported by Rackspace. +type RebuildOpts struct { + // Required. The ID of the image you want your server to be provisioned on + ImageID string + + // Name to set the server to + Name string + + // Required. The server's admin password + AdminPass string + + // AccessIPv4 [optional] provides a new IPv4 address for the instance. + AccessIPv4 string + + // AccessIPv6 [optional] provides a new IPv6 address for the instance. + AccessIPv6 string + + // Metadata [optional] contains key-value pairs (up to 255 bytes each) to attach to the server. + Metadata map[string]string + + // Personality [optional] includes the path and contents of a file to inject into the server at launch. + // The maximum size of the file is 255 bytes (decoded). + Personality []byte + + // Rackspace-specific stuff begins here. + + // DiskConfig [optional] controls how the created server's disk is partitioned. See the "diskconfig" + // extension in OpenStack compute v2. + DiskConfig diskconfig.DiskConfig +} + +// ToServerRebuildMap constructs a request body using all of the OpenStack extensions that are +// active on Rackspace. +func (opts RebuildOpts) ToServerRebuildMap() (map[string]interface{}, error) { + base := os.RebuildOpts{ + ImageID: opts.ImageID, + Name: opts.Name, + AdminPass: opts.AdminPass, + AccessIPv4: opts.AccessIPv4, + AccessIPv6: opts.AccessIPv6, + Metadata: opts.Metadata, + Personality: opts.Personality, + } + + drive := diskconfig.RebuildOptsExt{ + RebuildOptsBuilder: base, + DiskConfig: opts.DiskConfig, + } + + return drive.ToServerRebuildMap() +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/servers/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/servers/requests_test.go new file mode 100644 index 0000000000..3c0f806936 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/servers/requests_test.go @@ -0,0 +1,57 @@ +package servers + +import ( + "testing" + + "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/diskconfig" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestCreateOpts(t *testing.T) { + opts := CreateOpts{ + Name: "createdserver", + ImageRef: "image-id", + FlavorRef: "flavor-id", + KeyPair: "mykey", + DiskConfig: diskconfig.Manual, + } + + expected := ` + { + "server": { + "name": "createdserver", + "imageRef": "image-id", + "flavorRef": "flavor-id", + "key_name": "mykey", + "OS-DCF:diskConfig": "MANUAL" + } + } + ` + actual, err := opts.ToServerCreateMap() + th.AssertNoErr(t, err) + th.CheckJSONEquals(t, expected, actual) +} + +func TestRebuildOpts(t *testing.T) { + opts := RebuildOpts{ + Name: "rebuiltserver", + AdminPass: "swordfish", + ImageID: "asdfasdfasdf", + DiskConfig: diskconfig.Auto, + } + + actual, err := opts.ToServerRebuildMap() + th.AssertNoErr(t, err) + + expected := ` + { + "rebuild": { + "name": "rebuiltserver", + "imageRef": "asdfasdfasdf", + "adminPass": "swordfish", + "OS-DCF:diskConfig": "AUTO" + } + } + ` + th.CheckJSONEquals(t, expected, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/virtualinterfaces/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/virtualinterfaces/requests.go new file mode 100644 index 0000000000..bfe3487861 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/virtualinterfaces/requests.go @@ -0,0 +1,51 @@ +package virtualinterfaces + +import ( + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" + + "github.com/racker/perigee" +) + +// List returns a Pager which allows you to iterate over a collection of +// networks. It accepts a ListOpts struct, which allows you to filter and sort +// the returned collection for greater efficiency. +func List(c *gophercloud.ServiceClient, instanceID string) pagination.Pager { + createPage := func(r pagination.PageResult) pagination.Page { + return VirtualInterfacePage{pagination.SinglePageBase(r)} + } + + return pagination.NewPager(c, listURL(c, instanceID), createPage) +} + +// Create creates a new virtual interface for a network and attaches the network +// to the server instance. +func Create(c *gophercloud.ServiceClient, instanceID, networkID string) CreateResult { + var res CreateResult + + reqBody := map[string]map[string]string{ + "virtual_interface": { + "network_id": networkID, + }, + } + + // Send request to API + _, res.Err = perigee.Request("POST", createURL(c, instanceID), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + ReqBody: &reqBody, + Results: &res.Body, + OkCodes: []int{200, 201, 202}, + }) + return res +} + +// Delete deletes the interface with interfaceID attached to the instance with +// instanceID. +func Delete(c *gophercloud.ServiceClient, instanceID, interfaceID string) DeleteResult { + var res DeleteResult + _, res.Err = perigee.Request("DELETE", deleteURL(c, instanceID, interfaceID), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + OkCodes: []int{200, 204}, + }) + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/virtualinterfaces/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/virtualinterfaces/requests_test.go new file mode 100644 index 0000000000..d40af9c462 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/virtualinterfaces/requests_test.go @@ -0,0 +1,165 @@ +package virtualinterfaces + +import ( + "fmt" + "net/http" + "testing" + + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/servers/12345/os-virtual-interfacesv2", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "virtual_interfaces": [ + { + "id": "de7c6d53-b895-4b4a-963c-517ccb0f0775", + "ip_addresses": [ + { + "address": "192.168.0.2", + "network_id": "f212726e-6321-4210-9bae-a13f5a33f83f", + "network_label": "superprivate_xml" + } + ], + "mac_address": "BC:76:4E:04:85:20" + }, + { + "id": "e14e789d-3b98-44a6-9c2d-c23eb1d1465c", + "ip_addresses": [ + { + "address": "10.181.1.30", + "network_id": "3b324a1b-31b8-4db5-9fe5-4a2067f60297", + "network_label": "private" + } + ], + "mac_address": "BC:76:4E:04:81:55" + } + ] +} + `) + }) + + client := fake.ServiceClient() + count := 0 + + err := List(client, "12345").EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractVirtualInterfaces(page) + if err != nil { + t.Errorf("Failed to extract networks: %v", err) + return false, err + } + + expected := []VirtualInterface{ + VirtualInterface{ + MACAddress: "BC:76:4E:04:85:20", + IPAddresses: []IPAddress{ + IPAddress{ + Address: "192.168.0.2", + NetworkID: "f212726e-6321-4210-9bae-a13f5a33f83f", + NetworkLabel: "superprivate_xml", + }, + }, + ID: "de7c6d53-b895-4b4a-963c-517ccb0f0775", + }, + VirtualInterface{ + MACAddress: "BC:76:4E:04:81:55", + IPAddresses: []IPAddress{ + IPAddress{ + Address: "10.181.1.30", + NetworkID: "3b324a1b-31b8-4db5-9fe5-4a2067f60297", + NetworkLabel: "private", + }, + }, + ID: "e14e789d-3b98-44a6-9c2d-c23eb1d1465c", + }, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, 1, count) +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/servers/12345/os-virtual-interfacesv2", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "virtual_interface": { + "network_id": "6789" + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, `{ + "virtual_interfaces": [ + { + "id": "de7c6d53-b895-4b4a-963c-517ccb0f0775", + "ip_addresses": [ + { + "address": "192.168.0.2", + "network_id": "f212726e-6321-4210-9bae-a13f5a33f83f", + "network_label": "superprivate_xml" + } + ], + "mac_address": "BC:76:4E:04:85:20" + } + ] + }`) + }) + + expected := &VirtualInterface{ + MACAddress: "BC:76:4E:04:85:20", + IPAddresses: []IPAddress{ + IPAddress{ + Address: "192.168.0.2", + NetworkID: "f212726e-6321-4210-9bae-a13f5a33f83f", + NetworkLabel: "superprivate_xml", + }, + }, + ID: "de7c6d53-b895-4b4a-963c-517ccb0f0775", + } + + actual, err := Create(fake.ServiceClient(), "12345", "6789").Extract() + th.AssertNoErr(t, err) + + th.CheckDeepEquals(t, expected, actual) +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/servers/12345/os-virtual-interfacesv2/6789", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusNoContent) + }) + + res := Delete(fake.ServiceClient(), "12345", "6789") + th.AssertNoErr(t, res.Err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/virtualinterfaces/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/virtualinterfaces/results.go new file mode 100644 index 0000000000..26fa7f31ce --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/virtualinterfaces/results.go @@ -0,0 +1,81 @@ +package virtualinterfaces + +import ( + "github.com/mitchellh/mapstructure" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +type commonResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts a network resource. +func (r commonResult) Extract() (*VirtualInterface, error) { + if r.Err != nil { + return nil, r.Err + } + + var res struct { + VirtualInterfaces []VirtualInterface `mapstructure:"virtual_interfaces" json:"virtual_interfaces"` + } + + err := mapstructure.Decode(r.Body, &res) + + return &res.VirtualInterfaces[0], err +} + +// CreateResult represents the result of a create operation. +type CreateResult struct { + commonResult +} + +// DeleteResult represents the result of a delete operation. +type DeleteResult struct { + gophercloud.ErrResult +} + +// IPAddress represents a vitual address attached to a VirtualInterface. +type IPAddress struct { + Address string `mapstructure:"address" json:"address"` + NetworkID string `mapstructure:"network_id" json:"network_id"` + NetworkLabel string `mapstructure:"network_label" json:"network_label"` +} + +// VirtualInterface represents a virtual interface. +type VirtualInterface struct { + // UUID for the virtual interface + ID string `mapstructure:"id" json:"id"` + + MACAddress string `mapstructure:"mac_address" json:"mac_address"` + + IPAddresses []IPAddress `mapstructure:"ip_addresses" json:"ip_addresses"` +} + +// VirtualInterfacePage is the page returned by a pager when traversing over a +// collection of virtual interfaces. +type VirtualInterfacePage struct { + pagination.SinglePageBase +} + +// IsEmpty returns true if the NetworkPage contains no Networks. +func (r VirtualInterfacePage) IsEmpty() (bool, error) { + networks, err := ExtractVirtualInterfaces(r) + if err != nil { + return true, err + } + return len(networks) == 0, nil +} + +// ExtractVirtualInterfaces accepts a Page struct, specifically a VirtualInterfacePage struct, +// and extracts the elements into a slice of VirtualInterface structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractVirtualInterfaces(page pagination.Page) ([]VirtualInterface, error) { + var resp struct { + VirtualInterfaces []VirtualInterface `mapstructure:"virtual_interfaces" json:"virtual_interfaces"` + } + + err := mapstructure.Decode(page.(VirtualInterfacePage).Body, &resp) + + return resp.VirtualInterfaces, err +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/virtualinterfaces/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/virtualinterfaces/urls.go new file mode 100644 index 0000000000..9e5693e849 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/virtualinterfaces/urls.go @@ -0,0 +1,15 @@ +package virtualinterfaces + +import "github.com/rackspace/gophercloud" + +func listURL(c *gophercloud.ServiceClient, instanceID string) string { + return c.ServiceURL("servers", instanceID, "os-virtual-interfacesv2") +} + +func createURL(c *gophercloud.ServiceClient, instanceID string) string { + return c.ServiceURL("servers", instanceID, "os-virtual-interfacesv2") +} + +func deleteURL(c *gophercloud.ServiceClient, instanceID, interfaceID string) string { + return c.ServiceURL("servers", instanceID, "os-virtual-interfacesv2", interfaceID) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/virtualinterfaces/urls_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/virtualinterfaces/urls_test.go new file mode 100644 index 0000000000..6732e4ed9f --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/virtualinterfaces/urls_test.go @@ -0,0 +1,32 @@ +package virtualinterfaces + +import ( + "testing" + + "github.com/rackspace/gophercloud" + th "github.com/rackspace/gophercloud/testhelper" +) + +const endpoint = "http://localhost:57909/" + +func endpointClient() *gophercloud.ServiceClient { + return &gophercloud.ServiceClient{Endpoint: endpoint} +} + +func TestCreateURL(t *testing.T) { + actual := createURL(endpointClient(), "12345") + expected := endpoint + "servers/12345/os-virtual-interfacesv2" + th.AssertEquals(t, expected, actual) +} + +func TestListURL(t *testing.T) { + actual := createURL(endpointClient(), "12345") + expected := endpoint + "servers/12345/os-virtual-interfacesv2" + th.AssertEquals(t, expected, actual) +} + +func TestDeleteURL(t *testing.T) { + actual := deleteURL(endpointClient(), "12345", "6789") + expected := endpoint + "servers/12345/os-virtual-interfacesv2/6789" + th.AssertEquals(t, expected, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/extensions/delegate.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/extensions/delegate.go new file mode 100644 index 0000000000..fc547cde5f --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/extensions/delegate.go @@ -0,0 +1,24 @@ +package extensions + +import ( + "github.com/rackspace/gophercloud" + common "github.com/rackspace/gophercloud/openstack/common/extensions" + "github.com/rackspace/gophercloud/pagination" +) + +// ExtractExtensions accepts a Page struct, specifically an ExtensionPage struct, and extracts the +// elements into a slice of os.Extension structs. +func ExtractExtensions(page pagination.Page) ([]common.Extension, error) { + return common.ExtractExtensions(page) +} + +// Get retrieves information for a specific extension using its alias. +func Get(c *gophercloud.ServiceClient, alias string) common.GetResult { + return common.Get(c, alias) +} + +// List returns a Pager which allows you to iterate over the full collection of extensions. +// It does not accept query parameters. +func List(c *gophercloud.ServiceClient) pagination.Pager { + return common.List(c) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/extensions/delegate_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/extensions/delegate_test.go new file mode 100644 index 0000000000..e30f79404d --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/extensions/delegate_test.go @@ -0,0 +1,39 @@ +package extensions + +import ( + "testing" + + common "github.com/rackspace/gophercloud/openstack/common/extensions" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + common.HandleListExtensionsSuccessfully(t) + + count := 0 + + err := List(fake.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractExtensions(page) + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, common.ExpectedExtensions, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, 1, count) +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + common.HandleGetExtensionSuccessfully(t) + + actual, err := Get(fake.ServiceClient(), "agent").Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, common.SingleExtension, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/extensions/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/extensions/doc.go new file mode 100644 index 0000000000..b02a95b534 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/extensions/doc.go @@ -0,0 +1,3 @@ +// Package extensions provides information and interaction with the all the +// extensions available for the Rackspace Identity service. +package extensions diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/roles/delegate.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/roles/delegate.go new file mode 100644 index 0000000000..a6c01e4f2d --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/roles/delegate.go @@ -0,0 +1,53 @@ +package roles + +import ( + "github.com/racker/perigee" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" + + os "github.com/rackspace/gophercloud/openstack/identity/v2/extensions/admin/roles" +) + +// List is the operation responsible for listing all available global roles +// that a user can adopt. +func List(client *gophercloud.ServiceClient) pagination.Pager { + return os.List(client) +} + +// AddUserRole is the operation responsible for assigning a particular role to +// a user. This is confined to the scope of the user's tenant - so the tenant +// ID is a required argument. +func AddUserRole(client *gophercloud.ServiceClient, userID, roleID string) UserRoleResult { + var result UserRoleResult + + _, result.Err = perigee.Request("PUT", userRoleURL(client, userID, roleID), perigee.Options{ + MoreHeaders: client.AuthenticatedHeaders(), + OkCodes: []int{200, 201}, + }) + + return result +} + +// DeleteUserRole is the operation responsible for deleting a particular role +// from a user. This is confined to the scope of the user's tenant - so the +// tenant ID is a required argument. +func DeleteUserRole(client *gophercloud.ServiceClient, userID, roleID string) UserRoleResult { + var result UserRoleResult + + _, result.Err = perigee.Request("DELETE", userRoleURL(client, userID, roleID), perigee.Options{ + MoreHeaders: client.AuthenticatedHeaders(), + OkCodes: []int{204}, + }) + + return result +} + +// UserRoleResult represents the result of either an AddUserRole or +// a DeleteUserRole operation. +type UserRoleResult struct { + gophercloud.ErrResult +} + +func userRoleURL(c *gophercloud.ServiceClient, userID, roleID string) string { + return c.ServiceURL(os.UserPath, userID, os.RolePath, os.ExtPath, roleID) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/roles/delegate_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/roles/delegate_test.go new file mode 100644 index 0000000000..fcee97d0bc --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/roles/delegate_test.go @@ -0,0 +1,66 @@ +package roles + +import ( + "testing" + + os "github.com/rackspace/gophercloud/openstack/identity/v2/extensions/admin/roles" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestRole(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockListRoleResponse(t) + + count := 0 + + err := List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := os.ExtractRoles(page) + if err != nil { + t.Errorf("Failed to extract users: %v", err) + return false, err + } + + expected := []os.Role{ + os.Role{ + ID: "123", + Name: "compute:admin", + Description: "Nova Administrator", + ServiceID: "cke5372ebabeeabb70a0e702a4626977x4406e5", + }, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + + th.AssertNoErr(t, err) + th.AssertEquals(t, 1, count) +} + +func TestAddUserRole(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockAddUserRoleResponse(t) + + err := AddUserRole(client.ServiceClient(), "{user_id}", "{role_id}").ExtractErr() + + th.AssertNoErr(t, err) +} + +func TestDeleteUserRole(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockDeleteUserRoleResponse(t) + + err := DeleteUserRole(client.ServiceClient(), "{user_id}", "{role_id}").ExtractErr() + + th.AssertNoErr(t, err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/roles/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/roles/fixtures.go new file mode 100644 index 0000000000..5f22d0f642 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/roles/fixtures.go @@ -0,0 +1,49 @@ +package roles + +import ( + "fmt" + "net/http" + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +func MockListRoleResponse(t *testing.T) { + th.Mux.HandleFunc("/OS-KSADM/roles", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "roles": [ + { + "id": "123", + "name": "compute:admin", + "description": "Nova Administrator", + "serviceId": "cke5372ebabeeabb70a0e702a4626977x4406e5" + } + ] +} + `) + }) +} + +func MockAddUserRoleResponse(t *testing.T) { + th.Mux.HandleFunc("/users/{user_id}/roles/OS-KSADM/{role_id}", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusCreated) + }) +} + +func MockDeleteUserRoleResponse(t *testing.T) { + th.Mux.HandleFunc("/users/{user_id}/roles/OS-KSADM/{role_id}", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusNoContent) + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/tenants/delegate.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/tenants/delegate.go new file mode 100644 index 0000000000..6cdd0cfbdc --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/tenants/delegate.go @@ -0,0 +1,17 @@ +package tenants + +import ( + "github.com/rackspace/gophercloud" + os "github.com/rackspace/gophercloud/openstack/identity/v2/tenants" + "github.com/rackspace/gophercloud/pagination" +) + +// ExtractTenants interprets a page of List results as a more usable slice of Tenant structs. +func ExtractTenants(page pagination.Page) ([]os.Tenant, error) { + return os.ExtractTenants(page) +} + +// List enumerates the tenants to which the current token grants access. +func List(client *gophercloud.ServiceClient, opts *os.ListOpts) pagination.Pager { + return os.List(client, opts) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/tenants/delegate_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/tenants/delegate_test.go new file mode 100644 index 0000000000..eccbfe23eb --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/tenants/delegate_test.go @@ -0,0 +1,28 @@ +package tenants + +import ( + "testing" + + os "github.com/rackspace/gophercloud/openstack/identity/v2/tenants" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestListTenants(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleListTenantsSuccessfully(t) + + count := 0 + err := List(fake.ServiceClient(), nil).EachPage(func(page pagination.Page) (bool, error) { + actual, err := ExtractTenants(page) + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, os.ExpectedTenantSlice, actual) + + count++ + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, 1, count) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/tenants/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/tenants/doc.go new file mode 100644 index 0000000000..c1825c21dc --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/tenants/doc.go @@ -0,0 +1,3 @@ +// Package tenants provides information and interaction with the tenant +// API resource for the Rackspace Identity service. +package tenants diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/tokens/delegate.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/tokens/delegate.go new file mode 100644 index 0000000000..4f9885af03 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/tokens/delegate.go @@ -0,0 +1,60 @@ +package tokens + +import ( + "errors" + + "github.com/rackspace/gophercloud" + os "github.com/rackspace/gophercloud/openstack/identity/v2/tokens" +) + +var ( + // ErrPasswordProvided is returned if both a password and an API key are provided to Create. + ErrPasswordProvided = errors.New("Please provide either a password or an API key.") +) + +// AuthOptions wraps the OpenStack AuthOptions struct to be able to customize the request body +// when API key authentication is used. +type AuthOptions struct { + os.AuthOptions +} + +// WrapOptions embeds a root AuthOptions struct in a package-specific one. +func WrapOptions(original gophercloud.AuthOptions) AuthOptions { + return AuthOptions{AuthOptions: os.WrapOptions(original)} +} + +// ToTokenCreateMap serializes an AuthOptions into a request body. If an API key is provided, it +// will be used, otherwise +func (auth AuthOptions) ToTokenCreateMap() (map[string]interface{}, error) { + if auth.APIKey == "" { + return auth.AuthOptions.ToTokenCreateMap() + } + + // Verify that other required attributes are present. + if auth.Username == "" { + return nil, os.ErrUsernameRequired + } + + authMap := make(map[string]interface{}) + + authMap["RAX-KSKEY:apiKeyCredentials"] = map[string]interface{}{ + "username": auth.Username, + "apiKey": auth.APIKey, + } + + if auth.TenantID != "" { + authMap["tenantId"] = auth.TenantID + } + if auth.TenantName != "" { + authMap["tenantName"] = auth.TenantName + } + + return map[string]interface{}{"auth": authMap}, nil +} + +// Create authenticates to Rackspace's identity service and attempts to acquire a Token. Rather +// than interact with this service directly, users should generally call +// rackspace.AuthenticatedClient(). +func Create(client *gophercloud.ServiceClient, auth AuthOptions) os.CreateResult { + return os.Create(client, auth) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/tokens/delegate_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/tokens/delegate_test.go new file mode 100644 index 0000000000..6678ff4a7c --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/tokens/delegate_test.go @@ -0,0 +1,36 @@ +package tokens + +import ( + "testing" + + "github.com/rackspace/gophercloud" + os "github.com/rackspace/gophercloud/openstack/identity/v2/tokens" + th "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +func tokenPost(t *testing.T, options gophercloud.AuthOptions, requestJSON string) os.CreateResult { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleTokenPost(t, requestJSON) + + return Create(client.ServiceClient(), WrapOptions(options)) +} + +func TestCreateTokenWithAPIKey(t *testing.T) { + options := gophercloud.AuthOptions{ + Username: "me", + APIKey: "1234567890abcdef", + } + + os.IsSuccessful(t, tokenPost(t, options, ` + { + "auth": { + "RAX-KSKEY:apiKeyCredentials": { + "username": "me", + "apiKey": "1234567890abcdef" + } + } + } + `)) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/tokens/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/tokens/doc.go new file mode 100644 index 0000000000..44043e5e13 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/tokens/doc.go @@ -0,0 +1,3 @@ +// Package tokens provides information and interaction with the token +// API resource for the Rackspace Identity service. +package tokens diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/users/delegate.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/users/delegate.go new file mode 100644 index 0000000000..ae2acde643 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/users/delegate.go @@ -0,0 +1,145 @@ +package users + +import ( + "errors" + + "github.com/racker/perigee" + "github.com/rackspace/gophercloud" + os "github.com/rackspace/gophercloud/openstack/identity/v2/users" + "github.com/rackspace/gophercloud/pagination" +) + +// List returns a pager that allows traversal over a collection of users. +func List(client *gophercloud.ServiceClient) pagination.Pager { + return os.List(client) +} + +// CommonOpts are the options which are shared between CreateOpts and +// UpdateOpts +type CommonOpts struct { + // Required. The username to assign to the user. When provided, the username + // must: + // - start with an alphabetical (A-Za-z) character + // - have a minimum length of 1 character + // + // The username may contain upper and lowercase characters, as well as any of + // the following special character: . - @ _ + Username string + + // Required. Email address for the user account. + Email string + + // Required. Indicates whether the user can authenticate after the user + // account is created. If no value is specified, the default value is true. + Enabled os.EnabledState + + // Optional. The password to assign to the user. If provided, the password + // must: + // - start with an alphabetical (A-Za-z) character + // - have a minimum length of 8 characters + // - contain at least one uppercase character, one lowercase character, and + // one numeric character. + // + // The password may contain any of the following special characters: . - @ _ + Password string +} + +// CreateOpts represents the options needed when creating new users. +type CreateOpts CommonOpts + +// ToUserCreateMap assembles a request body based on the contents of a CreateOpts. +func (opts CreateOpts) ToUserCreateMap() (map[string]interface{}, error) { + m := make(map[string]interface{}) + + if opts.Username == "" { + return m, errors.New("Username is a required field") + } + if opts.Enabled == nil { + return m, errors.New("Enabled is a required field") + } + if opts.Email == "" { + return m, errors.New("Email is a required field") + } + + if opts.Username != "" { + m["username"] = opts.Username + } + if opts.Email != "" { + m["email"] = opts.Email + } + if opts.Enabled != nil { + m["enabled"] = opts.Enabled + } + if opts.Password != "" { + m["OS-KSADM:password"] = opts.Password + } + + return map[string]interface{}{"user": m}, nil +} + +// Create is the operation responsible for creating new users. +func Create(client *gophercloud.ServiceClient, opts os.CreateOptsBuilder) CreateResult { + return CreateResult{os.Create(client, opts)} +} + +// Get requests details on a single user, either by ID. +func Get(client *gophercloud.ServiceClient, id string) GetResult { + return GetResult{os.Get(client, id)} +} + +// UpdateOptsBuilder allows extensions to add additional attributes to the Update request. +type UpdateOptsBuilder interface { + ToUserUpdateMap() map[string]interface{} +} + +// UpdateOpts specifies the base attributes that may be updated on an existing server. +type UpdateOpts CommonOpts + +// ToUserUpdateMap formats an UpdateOpts structure into a request body. +func (opts UpdateOpts) ToUserUpdateMap() map[string]interface{} { + m := make(map[string]interface{}) + + if opts.Username != "" { + m["username"] = opts.Username + } + if opts.Enabled != nil { + m["enabled"] = &opts.Enabled + } + if opts.Email != "" { + m["email"] = opts.Email + } + + return map[string]interface{}{"user": m} +} + +// Update is the operation responsible for updating exist users by their UUID. +func Update(client *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) UpdateResult { + var result UpdateResult + + _, result.Err = perigee.Request("POST", os.ResourceURL(client, id), perigee.Options{ + Results: &result.Body, + ReqBody: opts.ToUserUpdateMap(), + MoreHeaders: client.AuthenticatedHeaders(), + OkCodes: []int{200}, + }) + + return result +} + +// Delete is the operation responsible for permanently deleting an API user. +func Delete(client *gophercloud.ServiceClient, id string) os.DeleteResult { + return os.Delete(client, id) +} + +// ResetAPIKey resets the User's API key. +func ResetAPIKey(client *gophercloud.ServiceClient, id string) ResetAPIKeyResult { + var result ResetAPIKeyResult + + _, result.Err = perigee.Request("POST", resetAPIKeyURL(client, id), perigee.Options{ + Results: &result.Body, + MoreHeaders: client.AuthenticatedHeaders(), + OkCodes: []int{200}, + }) + + return result +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/users/delegate_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/users/delegate_test.go new file mode 100644 index 0000000000..62faf0c5be --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/users/delegate_test.go @@ -0,0 +1,111 @@ +package users + +import ( + "testing" + + os "github.com/rackspace/gophercloud/openstack/identity/v2/users" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockListResponse(t) + + count := 0 + + err := List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) { + count++ + users, err := os.ExtractUsers(page) + + th.AssertNoErr(t, err) + th.AssertEquals(t, "u1000", users[0].ID) + th.AssertEquals(t, "u1001", users[1].ID) + + return true, nil + }) + + th.AssertNoErr(t, err) + th.AssertEquals(t, 1, count) +} + +func TestCreateUser(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockCreateUser(t) + + opts := CreateOpts{ + Username: "new_user", + Enabled: os.Disabled, + Email: "new_user@foo.com", + Password: "foo", + } + + user, err := Create(client.ServiceClient(), opts).Extract() + + th.AssertNoErr(t, err) + + th.AssertEquals(t, "123456", user.ID) + th.AssertEquals(t, "5830280", user.DomainID) + th.AssertEquals(t, "DFW", user.DefaultRegion) +} + +func TestGetUser(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockGetUser(t) + + user, err := Get(client.ServiceClient(), "new_user").Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, true, user.Enabled) + th.AssertEquals(t, true, user.MultiFactorEnabled) +} + +func TestUpdateUser(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockUpdateUser(t) + + id := "c39e3de9be2d4c779f1dfd6abacc176d" + + opts := UpdateOpts{ + Enabled: os.Enabled, + Email: "new_email@foo.com", + } + + user, err := Update(client.ServiceClient(), id, opts).Extract() + + th.AssertNoErr(t, err) + + th.AssertEquals(t, true, user.Enabled) + th.AssertEquals(t, "new_email@foo.com", user.Email) +} + +func TestDeleteServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockDeleteUser(t) + + res := Delete(client.ServiceClient(), "c39e3de9be2d4c779f1dfd6abacc176d") + th.AssertNoErr(t, res.Err) +} + +func TestResetAPIKey(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockResetAPIKey(t) + + apiKey, err := ResetAPIKey(client.ServiceClient(), "99").Extract() + th.AssertNoErr(t, err) + th.AssertEquals(t, "joesmith", apiKey.Username) + th.AssertEquals(t, "mooH1eiLahd5ahYood7r", apiKey.APIKey) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/users/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/users/fixtures.go new file mode 100644 index 0000000000..973f39ea8c --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/users/fixtures.go @@ -0,0 +1,154 @@ +package users + +import ( + "fmt" + "net/http" + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +func mockListResponse(t *testing.T) { + th.Mux.HandleFunc("/users", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "users":[ + { + "id": "u1000", + "username": "jqsmith", + "email": "john.smith@example.org", + "enabled": true + }, + { + "id": "u1001", + "username": "jqsmith", + "email": "jane.smith@example.org", + "enabled": true + } + ] +} + `) + }) +} + +func mockCreateUser(t *testing.T) { + th.Mux.HandleFunc("/users", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + th.TestJSONRequest(t, r, ` +{ + "user": { + "username": "new_user", + "enabled": false, + "email": "new_user@foo.com", + "OS-KSADM:password": "foo" + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "user": { + "RAX-AUTH:defaultRegion": "DFW", + "RAX-AUTH:domainId": "5830280", + "id": "123456", + "username": "new_user", + "email": "new_user@foo.com", + "enabled": false + } +} +`) + }) +} + +func mockGetUser(t *testing.T) { + th.Mux.HandleFunc("/users/new_user", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "user": { + "RAX-AUTH:defaultRegion": "DFW", + "RAX-AUTH:domainId": "5830280", + "RAX-AUTH:multiFactorEnabled": "true", + "id": "c39e3de9be2d4c779f1dfd6abacc176d", + "username": "jqsmith", + "email": "john.smith@example.org", + "enabled": true + } +} +`) + }) +} + +func mockUpdateUser(t *testing.T) { + th.Mux.HandleFunc("/users/c39e3de9be2d4c779f1dfd6abacc176d", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + th.TestJSONRequest(t, r, ` +{ + "user": { + "email": "new_email@foo.com", + "enabled": true + } +} +`) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "user": { + "RAX-AUTH:defaultRegion": "DFW", + "RAX-AUTH:domainId": "5830280", + "RAX-AUTH:multiFactorEnabled": "true", + "id": "123456", + "username": "jqsmith", + "email": "new_email@foo.com", + "enabled": true + } +} +`) + }) +} + +func mockDeleteUser(t *testing.T) { + th.Mux.HandleFunc("/users/c39e3de9be2d4c779f1dfd6abacc176d", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusNoContent) + }) +} + +func mockResetAPIKey(t *testing.T) { + th.Mux.HandleFunc("/users/99/OS-KSADM/credentials/RAX-KSKEY:apiKeyCredentials/RAX-AUTH/reset", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, ` +{ + "RAX-KSKEY:apiKeyCredentials": { + "username": "joesmith", + "apiKey": "mooH1eiLahd5ahYood7r" + } +}`) + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/users/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/users/results.go new file mode 100644 index 0000000000..6936ecba84 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/users/results.go @@ -0,0 +1,129 @@ +package users + +import ( + "strconv" + + "github.com/rackspace/gophercloud" + os "github.com/rackspace/gophercloud/openstack/identity/v2/users" + + "github.com/mitchellh/mapstructure" +) + +// User represents a user resource that exists on the API. +type User struct { + // The UUID for this user. + ID string + + // The human name for this user. + Name string + + // The username for this user. + Username string + + // Indicates whether the user is enabled (true) or disabled (false). + Enabled bool + + // The email address for this user. + Email string + + // The ID of the tenant to which this user belongs. + TenantID string `mapstructure:"tenant_id"` + + // Specifies the default region for the user account. This value is inherited + // from the user administrator when the account is created. + DefaultRegion string `mapstructure:"RAX-AUTH:defaultRegion"` + + // Identifies the domain that contains the user account. This value is + // inherited from the user administrator when the account is created. + DomainID string `mapstructure:"RAX-AUTH:domainId"` + + // The password value that the user needs for authentication. If the Add user + // request included a password value, this attribute is not included in the + // response. + Password string `mapstructure:"OS-KSADM:password"` + + // Indicates whether the user has enabled multi-factor authentication. + MultiFactorEnabled bool `mapstructure:"RAX-AUTH:multiFactorEnabled"` +} + +// CreateResult represents the result of a Create operation +type CreateResult struct { + os.CreateResult +} + +// GetResult represents the result of a Get operation +type GetResult struct { + os.GetResult +} + +// UpdateResult represents the result of an Update operation +type UpdateResult struct { + os.UpdateResult +} + +func commonExtract(resp interface{}, err error) (*User, error) { + if err != nil { + return nil, err + } + + var respStruct struct { + User *User `json:"user"` + } + + // Since the API returns a string instead of a bool, we need to hack the JSON + json := resp.(map[string]interface{}) + user := json["user"].(map[string]interface{}) + if s, ok := user["RAX-AUTH:multiFactorEnabled"].(string); ok && s != "" { + if b, err := strconv.ParseBool(s); err == nil { + user["RAX-AUTH:multiFactorEnabled"] = b + } + } + + err = mapstructure.Decode(json, &respStruct) + + return respStruct.User, err +} + +// Extract will get the Snapshot object out of the GetResult object. +func (r GetResult) Extract() (*User, error) { + return commonExtract(r.Body, r.Err) +} + +// Extract will get the Snapshot object out of the CreateResult object. +func (r CreateResult) Extract() (*User, error) { + return commonExtract(r.Body, r.Err) +} + +// Extract will get the Snapshot object out of the UpdateResult object. +func (r UpdateResult) Extract() (*User, error) { + return commonExtract(r.Body, r.Err) +} + +// ResetAPIKeyResult represents the server response to the ResetAPIKey method. +type ResetAPIKeyResult struct { + gophercloud.Result +} + +// ResetAPIKeyValue represents an API Key that has been reset. +type ResetAPIKeyValue struct { + // The Username for this API Key reset. + Username string `mapstructure:"username"` + + // The new API Key for this user. + APIKey string `mapstructure:"apiKey"` +} + +// Extract will get the Error or ResetAPIKeyValue object out of the ResetAPIKeyResult object. +func (r ResetAPIKeyResult) Extract() (*ResetAPIKeyValue, error) { + if r.Err != nil { + return nil, r.Err + } + + var response struct { + ResetAPIKeyValue ResetAPIKeyValue `mapstructure:"RAX-KSKEY:apiKeyCredentials"` + } + + err := mapstructure.Decode(r.Body, &response) + + return &response.ResetAPIKeyValue, err +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/users/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/users/urls.go new file mode 100644 index 0000000000..bc1aaefb02 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/users/urls.go @@ -0,0 +1,7 @@ +package users + +import "github.com/rackspace/gophercloud" + +func resetAPIKeyURL(client *gophercloud.ServiceClient, id string) string { + return client.ServiceURL("users", id, "OS-KSADM", "credentials", "RAX-KSKEY:apiKeyCredentials", "RAX-AUTH", "reset") +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/acl/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/acl/doc.go new file mode 100644 index 0000000000..42325fe83d --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/acl/doc.go @@ -0,0 +1,12 @@ +/* +Package acl provides information and interaction with the access lists feature +of the Rackspace Cloud Load Balancer service. + +The access list management feature allows fine-grained network access controls +to be applied to the load balancer's virtual IP address. A single IP address, +multiple IP addresses, or entire network subnets can be added. Items that are +configured with the ALLOW type always takes precedence over items with the DENY +type. To reject traffic from all items except for those with the ALLOW type, +add a networkItem with an address of "0.0.0.0/0" and a DENY type. +*/ +package acl diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/acl/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/acl/fixtures.go new file mode 100644 index 0000000000..e3c941c81b --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/acl/fixtures.go @@ -0,0 +1,109 @@ +package acl + +import ( + "fmt" + "net/http" + "strconv" + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +func _rootURL(lbID int) string { + return "/loadbalancers/" + strconv.Itoa(lbID) + "/accesslist" +} + +func mockListResponse(t *testing.T, id int) { + th.Mux.HandleFunc(_rootURL(id), func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "accessList": [ + { + "address": "206.160.163.21", + "id": 21, + "type": "DENY" + }, + { + "address": "206.160.163.22", + "id": 22, + "type": "DENY" + }, + { + "address": "206.160.163.23", + "id": 23, + "type": "DENY" + }, + { + "address": "206.160.163.24", + "id": 24, + "type": "DENY" + } + ] +} + `) + }) +} + +func mockCreateResponse(t *testing.T, lbID int) { + th.Mux.HandleFunc(_rootURL(lbID), func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + th.TestJSONRequest(t, r, ` +{ + "accessList": [ + { + "address": "206.160.163.21", + "type": "DENY" + }, + { + "address": "206.160.165.11", + "type": "DENY" + } + ] +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusAccepted) + }) +} + +func mockDeleteAllResponse(t *testing.T, lbID int) { + th.Mux.HandleFunc(_rootURL(lbID), func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusAccepted) + }) +} + +func mockBatchDeleteResponse(t *testing.T, lbID int, ids []int) { + th.Mux.HandleFunc(_rootURL(lbID), func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + r.ParseForm() + + for k, v := range ids { + fids := r.Form["id"] + th.AssertEquals(t, strconv.Itoa(v), fids[k]) + } + + w.WriteHeader(http.StatusAccepted) + }) +} + +func mockDeleteResponse(t *testing.T, lbID, networkID int) { + th.Mux.HandleFunc(_rootURL(lbID)+"/"+strconv.Itoa(networkID), func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusAccepted) + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/acl/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/acl/requests.go new file mode 100644 index 0000000000..e1e92ac631 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/acl/requests.go @@ -0,0 +1,127 @@ +package acl + +import ( + "errors" + "fmt" + + "github.com/racker/perigee" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// List is the operation responsible for returning a paginated collection of +// network items that define a load balancer's access list. +func List(client *gophercloud.ServiceClient, lbID int) pagination.Pager { + url := rootURL(client, lbID) + + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return AccessListPage{pagination.SinglePageBase(r)} + }) +} + +// CreateOptsBuilder is the interface responsible for generating the JSON +// for a Create operation. +type CreateOptsBuilder interface { + ToAccessListCreateMap() (map[string]interface{}, error) +} + +// CreateOpts is a slice of CreateOpt structs, that allow the user to create +// multiple nodes in a single operation (one node per CreateOpt). +type CreateOpts []CreateOpt + +// CreateOpt represents the options to create a single node. +type CreateOpt struct { + // Required - the IP address or CIDR for item to add to access list. + Address string + + // Required - the type of the node. Either ALLOW or DENY. + Type Type +} + +// ToAccessListCreateMap converts a slice of options into a map that can be +// used for the JSON. +func (opts CreateOpts) ToAccessListCreateMap() (map[string]interface{}, error) { + type itemMap map[string]interface{} + items := []itemMap{} + + for k, v := range opts { + if v.Address == "" { + return itemMap{}, fmt.Errorf("Address is a required attribute, none provided for %d CreateOpt element", k) + } + if v.Type != ALLOW && v.Type != DENY { + return itemMap{}, fmt.Errorf("Type must be ALLOW or DENY") + } + + item := make(itemMap) + item["address"] = v.Address + item["type"] = v.Type + + items = append(items, item) + } + + return itemMap{"accessList": items}, nil +} + +// Create is the operation responsible for adding network items to the access +// rules for a particular load balancer. If network items already exist, the +// new item will be appended. A single IP address or subnet range is considered +// unique and cannot be duplicated. +func Create(client *gophercloud.ServiceClient, loadBalancerID int, opts CreateOptsBuilder) CreateResult { + var res CreateResult + + reqBody, err := opts.ToAccessListCreateMap() + if err != nil { + res.Err = err + return res + } + + _, res.Err = perigee.Request("POST", rootURL(client, loadBalancerID), perigee.Options{ + MoreHeaders: client.AuthenticatedHeaders(), + ReqBody: &reqBody, + OkCodes: []int{202}, + }) + + return res +} + +// BulkDelete will delete multiple network items from a load balancer's access +// list in a single operation. +func BulkDelete(c *gophercloud.ServiceClient, loadBalancerID int, itemIDs []int) DeleteResult { + var res DeleteResult + + if len(itemIDs) > 10 || len(itemIDs) == 0 { + res.Err = errors.New("You must provide a minimum of 1 and a maximum of 10 item IDs") + return res + } + + url := rootURL(c, loadBalancerID) + url += gophercloud.IDSliceToQueryString("id", itemIDs) + + _, res.Err = perigee.Request("DELETE", url, perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + OkCodes: []int{202}, + }) + + return res +} + +// Delete will remove a single network item from a load balancer's access list. +func Delete(c *gophercloud.ServiceClient, lbID, itemID int) DeleteResult { + var res DeleteResult + _, res.Err = perigee.Request("DELETE", resourceURL(c, lbID, itemID), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + OkCodes: []int{202}, + }) + return res +} + +// DeleteAll will delete the entire contents of a load balancer's access list, +// effectively resetting it and allowing all traffic. +func DeleteAll(c *gophercloud.ServiceClient, lbID int) DeleteResult { + var res DeleteResult + _, res.Err = perigee.Request("DELETE", rootURL(c, lbID), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + OkCodes: []int{202}, + }) + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/acl/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/acl/requests_test.go new file mode 100644 index 0000000000..c4961a3dd8 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/acl/requests_test.go @@ -0,0 +1,91 @@ +package acl + +import ( + "testing" + + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +const ( + lbID = 12345 + itemID1 = 67890 + itemID2 = 67891 +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockListResponse(t, lbID) + + count := 0 + + err := List(client.ServiceClient(), lbID).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractAccessList(page) + th.AssertNoErr(t, err) + + expected := AccessList{ + NetworkItem{Address: "206.160.163.21", ID: 21, Type: DENY}, + NetworkItem{Address: "206.160.163.22", ID: 22, Type: DENY}, + NetworkItem{Address: "206.160.163.23", ID: 23, Type: DENY}, + NetworkItem{Address: "206.160.163.24", ID: 24, Type: DENY}, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + + th.AssertNoErr(t, err) + th.AssertEquals(t, 1, count) +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockCreateResponse(t, lbID) + + opts := CreateOpts{ + CreateOpt{Address: "206.160.163.21", Type: DENY}, + CreateOpt{Address: "206.160.165.11", Type: DENY}, + } + + err := Create(client.ServiceClient(), lbID, opts).ExtractErr() + th.AssertNoErr(t, err) +} + +func TestBulkDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + ids := []int{itemID1, itemID2} + + mockBatchDeleteResponse(t, lbID, ids) + + err := BulkDelete(client.ServiceClient(), lbID, ids).ExtractErr() + th.AssertNoErr(t, err) +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockDeleteResponse(t, lbID, itemID1) + + err := Delete(client.ServiceClient(), lbID, itemID1).ExtractErr() + th.AssertNoErr(t, err) +} + +func TestDeleteAll(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockDeleteAllResponse(t, lbID) + + err := DeleteAll(client.ServiceClient(), lbID).ExtractErr() + th.AssertNoErr(t, err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/acl/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/acl/results.go new file mode 100644 index 0000000000..9ea5ea2f4b --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/acl/results.go @@ -0,0 +1,72 @@ +package acl + +import ( + "github.com/mitchellh/mapstructure" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// AccessList represents the rules of network access to a particular load +// balancer. +type AccessList []NetworkItem + +// NetworkItem describes how an IP address or entire subnet may interact with a +// load balancer. +type NetworkItem struct { + // The IP address or subnet (CIDR) that defines the network item. + Address string + + // The numeric unique ID for this item. + ID int + + // Either ALLOW or DENY. + Type Type +} + +// Type defines how an item may connect to the load balancer. +type Type string + +// Convenience consts. +const ( + ALLOW Type = "ALLOW" + DENY Type = "DENY" +) + +// AccessListPage is the page returned by a pager for traversing over a +// collection of network items in an access list. +type AccessListPage struct { + pagination.SinglePageBase +} + +// IsEmpty checks whether an AccessListPage struct is empty. +func (p AccessListPage) IsEmpty() (bool, error) { + is, err := ExtractAccessList(p) + if err != nil { + return true, nil + } + return len(is) == 0, nil +} + +// ExtractAccessList accepts a Page struct, specifically an AccessListPage +// struct, and extracts the elements into a slice of NetworkItem structs. In +// other words, a generic collection is mapped into a relevant slice. +func ExtractAccessList(page pagination.Page) (AccessList, error) { + var resp struct { + List AccessList `mapstructure:"accessList" json:"accessList"` + } + + err := mapstructure.Decode(page.(AccessListPage).Body, &resp) + + return resp.List, err +} + +// CreateResult represents the result of a create operation. +type CreateResult struct { + gophercloud.ErrResult +} + +// DeleteResult represents the result of a delete operation. +type DeleteResult struct { + gophercloud.ErrResult +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/acl/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/acl/urls.go new file mode 100644 index 0000000000..e373fa1d80 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/acl/urls.go @@ -0,0 +1,20 @@ +package acl + +import ( + "strconv" + + "github.com/rackspace/gophercloud" +) + +const ( + path = "loadbalancers" + aclPath = "accesslist" +) + +func resourceURL(c *gophercloud.ServiceClient, lbID, networkID int) string { + return c.ServiceURL(path, strconv.Itoa(lbID), aclPath, strconv.Itoa(networkID)) +} + +func rootURL(c *gophercloud.ServiceClient, lbID int) string { + return c.ServiceURL(path, strconv.Itoa(lbID), aclPath) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/lbs/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/lbs/doc.go new file mode 100644 index 0000000000..05f0032859 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/lbs/doc.go @@ -0,0 +1,44 @@ +/* +Package lbs provides information and interaction with the Load Balancer API +resource for the Rackspace Cloud Load Balancer service. + +A load balancer is a logical device which belongs to a cloud account. It is +used to distribute workloads between multiple back-end systems or services, +based on the criteria defined as part of its configuration. This configuration +is defined using the Create operation, and can be updated with Update. + +To conserve IPv4 address space, it is highly recommended that you share Virtual +IPs between load balancers. If you have at least one load balancer, you may +create subsequent ones that share a single virtual IPv4 and/or a single IPv6 by +passing in a virtual IP ID to the Update operation (instead of a type). This +feature is also highly desirable if you wish to load balance both an insecure +and secure protocol using one IP or DNS name. In order to share a virtual IP, +each Load Balancer must utilize a unique port. + +All load balancers have a Status attribute that shows the current configuration +status of the device. This status is immutable by the caller and is updated +automatically based on state changes within the service. When a load balancer +is first created, it is placed into a BUILD state while the configuration is +being generated and applied based on the request. Once the configuration is +applied and finalized, it is in an ACTIVE status. In the event of a +configuration change or update, the status of the load balancer changes to +PENDING_UPDATE to signify configuration changes are in progress but have not yet +been finalized. Load balancers in a SUSPENDED status are configured to reject +traffic and do not forward requests to back-end nodes. + +An HTTP load balancer has the X-Forwarded-For (XFF) HTTP header set by default. +This header contains the originating IP address of a client connecting to a web +server through an HTTP proxy or load balancer, which many web applications are +already designed to use when determining the source address for a request. + +It also includes the X-Forwarded-Proto (XFP) HTTP header, which has been added +for identifying the originating protocol of an HTTP request as "http" or +"https" depending on which protocol the client requested. This is useful when +using SSL termination. + +Finally, it also includes the X-Forwarded-Port HTTP header, which has been +added for being able to generate secure URLs containing the specified port. +This header, along with the X-Forwarded-For header, provides the needed +information to the underlying application servers. +*/ +package lbs diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/lbs/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/lbs/fixtures.go new file mode 100644 index 0000000000..6325310dbb --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/lbs/fixtures.go @@ -0,0 +1,584 @@ +package lbs + +import ( + "fmt" + "net/http" + "strconv" + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +func mockListLBResponse(t *testing.T) { + th.Mux.HandleFunc("/loadbalancers", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "loadBalancers":[ + { + "name":"lb-site1", + "id":71, + "protocol":"HTTP", + "port":80, + "algorithm":"RANDOM", + "status":"ACTIVE", + "nodeCount":3, + "virtualIps":[ + { + "id":403, + "address":"206.55.130.1", + "type":"PUBLIC", + "ipVersion":"IPV4" + } + ], + "created":{ + "time":"2010-11-30T03:23:42Z" + }, + "updated":{ + "time":"2010-11-30T03:23:44Z" + } + }, + { + "name":"lb-site2", + "id":72, + "created":{ + "time":"2011-11-30T03:23:42Z" + }, + "updated":{ + "time":"2011-11-30T03:23:44Z" + } + }, + { + "name":"lb-site3", + "id":73, + "created":{ + "time":"2012-11-30T03:23:42Z" + }, + "updated":{ + "time":"2012-11-30T03:23:44Z" + } + } + ] +} + `) + }) +} + +func mockCreateLBResponse(t *testing.T) { + th.Mux.HandleFunc("/loadbalancers", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + th.TestJSONRequest(t, r, ` +{ + "loadBalancer": { + "name": "a-new-loadbalancer", + "port": 80, + "protocol": "HTTP", + "virtualIps": [ + { + "id": 2341 + }, + { + "id": 900001 + } + ], + "nodes": [ + { + "address": "10.1.1.1", + "port": 80, + "condition": "ENABLED" + } + ] + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusAccepted) + + fmt.Fprintf(w, ` +{ + "loadBalancer": { + "name": "a-new-loadbalancer", + "id": 144, + "protocol": "HTTP", + "halfClosed": false, + "port": 83, + "algorithm": "RANDOM", + "status": "BUILD", + "timeout": 30, + "cluster": { + "name": "ztm-n01.staging1.lbaas.rackspace.net" + }, + "nodes": [ + { + "address": "10.1.1.1", + "id": 653, + "port": 80, + "status": "ONLINE", + "condition": "ENABLED", + "weight": 1 + } + ], + "virtualIps": [ + { + "address": "206.10.10.210", + "id": 39, + "type": "PUBLIC", + "ipVersion": "IPV4" + }, + { + "address": "2001:4801:79f1:0002:711b:be4c:0000:0021", + "id": 900001, + "type": "PUBLIC", + "ipVersion": "IPV6" + } + ], + "created": { + "time": "2010-11-30T03:23:42Z" + }, + "updated": { + "time": "2010-11-30T03:23:44Z" + }, + "connectionLogging": { + "enabled": false + } + } +} + `) + }) +} + +func mockBatchDeleteLBResponse(t *testing.T, ids []int) { + th.Mux.HandleFunc("/loadbalancers", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + r.ParseForm() + + for k, v := range ids { + fids := r.Form["id"] + th.AssertEquals(t, strconv.Itoa(v), fids[k]) + } + + w.WriteHeader(http.StatusAccepted) + }) +} + +func mockDeleteLBResponse(t *testing.T, id int) { + th.Mux.HandleFunc("/loadbalancers/"+strconv.Itoa(id), func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusAccepted) + }) +} + +func mockGetLBResponse(t *testing.T, id int) { + th.Mux.HandleFunc("/loadbalancers/"+strconv.Itoa(id), func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "loadBalancer": { + "id": 2000, + "name": "sample-loadbalancer", + "protocol": "HTTP", + "port": 80, + "algorithm": "RANDOM", + "status": "ACTIVE", + "timeout": 30, + "connectionLogging": { + "enabled": true + }, + "virtualIps": [ + { + "id": 1000, + "address": "206.10.10.210", + "type": "PUBLIC", + "ipVersion": "IPV4" + } + ], + "nodes": [ + { + "id": 1041, + "address": "10.1.1.1", + "port": 80, + "condition": "ENABLED", + "status": "ONLINE" + }, + { + "id": 1411, + "address": "10.1.1.2", + "port": 80, + "condition": "ENABLED", + "status": "ONLINE" + } + ], + "sessionPersistence": { + "persistenceType": "HTTP_COOKIE" + }, + "connectionThrottle": { + "maxConnections": 100 + }, + "cluster": { + "name": "c1.dfw1" + }, + "created": { + "time": "2010-11-30T03:23:42Z" + }, + "updated": { + "time": "2010-11-30T03:23:44Z" + }, + "sourceAddresses": { + "ipv6Public": "2001:4801:79f1:1::1/64", + "ipv4Servicenet": "10.0.0.0", + "ipv4Public": "10.12.99.28" + } + } +} + `) + }) +} + +func mockUpdateLBResponse(t *testing.T, id int) { + th.Mux.HandleFunc("/loadbalancers/"+strconv.Itoa(id), func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + th.TestJSONRequest(t, r, ` +{ + "loadBalancer": { + "name": "a-new-loadbalancer", + "protocol": "TCP", + "halfClosed": true, + "algorithm": "RANDOM", + "port": 8080, + "timeout": 100, + "httpsRedirect": false + } +} + `) + + w.WriteHeader(http.StatusAccepted) + }) +} + +func mockListProtocolsResponse(t *testing.T) { + th.Mux.HandleFunc("/loadbalancers/protocols", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "protocols": [ + { + "name": "DNS_TCP", + "port": 53 + }, + { + "name": "DNS_UDP", + "port": 53 + }, + { + "name": "FTP", + "port": 21 + }, + { + "name": "HTTP", + "port": 80 + }, + { + "name": "HTTPS", + "port": 443 + }, + { + "name": "IMAPS", + "port": 993 + }, + { + "name": "IMAPv4", + "port": 143 + }, + { + "name": "LDAP", + "port": 389 + }, + { + "name": "LDAPS", + "port": 636 + }, + { + "name": "MYSQL", + "port": 3306 + }, + { + "name": "POP3", + "port": 110 + }, + { + "name": "POP3S", + "port": 995 + }, + { + "name": "SMTP", + "port": 25 + }, + { + "name": "TCP", + "port": 0 + }, + { + "name": "TCP_CLIENT_FIRST", + "port": 0 + }, + { + "name": "UDP", + "port": 0 + }, + { + "name": "UDP_STREAM", + "port": 0 + }, + { + "name": "SFTP", + "port": 22 + }, + { + "name": "TCP_STREAM", + "port": 0 + } + ] +} + `) + }) +} + +func mockListAlgorithmsResponse(t *testing.T) { + th.Mux.HandleFunc("/loadbalancers/algorithms", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "algorithms": [ + { + "name": "LEAST_CONNECTIONS" + }, + { + "name": "RANDOM" + }, + { + "name": "ROUND_ROBIN" + }, + { + "name": "WEIGHTED_LEAST_CONNECTIONS" + }, + { + "name": "WEIGHTED_ROUND_ROBIN" + } + ] +} + `) + }) +} + +func mockGetLoggingResponse(t *testing.T, id int) { + th.Mux.HandleFunc("/loadbalancers/"+strconv.Itoa(id)+"/connectionlogging", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "connectionLogging": { + "enabled": true + } +} + `) + }) +} + +func mockEnableLoggingResponse(t *testing.T, id int) { + th.Mux.HandleFunc("/loadbalancers/"+strconv.Itoa(id)+"/connectionlogging", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + th.TestJSONRequest(t, r, ` +{ + "connectionLogging":{ + "enabled":true + } +} + `) + + w.WriteHeader(http.StatusAccepted) + }) +} + +func mockDisableLoggingResponse(t *testing.T, id int) { + th.Mux.HandleFunc("/loadbalancers/"+strconv.Itoa(id)+"/connectionlogging", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + th.TestJSONRequest(t, r, ` +{ + "connectionLogging":{ + "enabled":false + } +} + `) + + w.WriteHeader(http.StatusAccepted) + }) +} + +func mockGetErrorPageResponse(t *testing.T, id int) { + th.Mux.HandleFunc("/loadbalancers/"+strconv.Itoa(id)+"/errorpage", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "errorpage": { + "content": "DEFAULT ERROR PAGE" + } +} + `) + }) +} + +func mockSetErrorPageResponse(t *testing.T, id int) { + th.Mux.HandleFunc("/loadbalancers/"+strconv.Itoa(id)+"/errorpage", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + th.TestJSONRequest(t, r, ` +{ + "errorpage": { + "content": "New error page" + } +} + `) + + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "errorpage": { + "content": "New error page" + } +} + `) + }) +} + +func mockDeleteErrorPageResponse(t *testing.T, id int) { + th.Mux.HandleFunc("/loadbalancers/"+strconv.Itoa(id)+"/errorpage", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusOK) + }) +} + +func mockGetStatsResponse(t *testing.T, id int) { + th.Mux.HandleFunc("/loadbalancers/"+strconv.Itoa(id)+"/stats", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "connectTimeOut": 10, + "connectError": 20, + "connectFailure": 30, + "dataTimedOut": 40, + "keepAliveTimedOut": 50, + "maxConn": 60, + "currentConn": 40, + "connectTimeOutSsl": 10, + "connectErrorSsl": 20, + "connectFailureSsl": 30, + "dataTimedOutSsl": 40, + "keepAliveTimedOutSsl": 50, + "maxConnSsl": 60, + "currentConnSsl": 40 +} + `) + }) +} + +func mockGetCachingResponse(t *testing.T, id int) { + th.Mux.HandleFunc("/loadbalancers/"+strconv.Itoa(id)+"/contentcaching", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "contentCaching": { + "enabled": true + } +} + `) + }) +} + +func mockEnableCachingResponse(t *testing.T, id int) { + th.Mux.HandleFunc("/loadbalancers/"+strconv.Itoa(id)+"/contentcaching", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + th.TestJSONRequest(t, r, ` +{ + "contentCaching":{ + "enabled":true + } +} + `) + + w.WriteHeader(http.StatusAccepted) + }) +} + +func mockDisableCachingResponse(t *testing.T, id int) { + th.Mux.HandleFunc("/loadbalancers/"+strconv.Itoa(id)+"/contentcaching", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + th.TestJSONRequest(t, r, ` +{ + "contentCaching":{ + "enabled":false + } +} + `) + + w.WriteHeader(http.StatusAccepted) + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/lbs/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/lbs/requests.go new file mode 100644 index 0000000000..342f10790e --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/lbs/requests.go @@ -0,0 +1,574 @@ +package lbs + +import ( + "errors" + + "github.com/mitchellh/mapstructure" + "github.com/racker/perigee" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" + "github.com/rackspace/gophercloud/rackspace/lb/v1/acl" + "github.com/rackspace/gophercloud/rackspace/lb/v1/monitors" + "github.com/rackspace/gophercloud/rackspace/lb/v1/nodes" + "github.com/rackspace/gophercloud/rackspace/lb/v1/sessions" + "github.com/rackspace/gophercloud/rackspace/lb/v1/throttle" + "github.com/rackspace/gophercloud/rackspace/lb/v1/vips" +) + +var ( + errNameRequired = errors.New("Name is a required attribute") + errTimeoutExceeded = errors.New("Timeout must be less than 120") +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToLBListQuery() (string, error) +} + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. +type ListOpts struct { + ChangesSince string `q:"changes-since"` + Status Status `q:"status"` + NodeAddr string `q:"nodeaddress"` + Marker string `q:"marker"` + Limit int `q:"limit"` +} + +// ToLBListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToLBListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + if err != nil { + return "", err + } + return q.String(), nil +} + +// List is the operation responsible for returning a paginated collection of +// load balancers. You may pass in a ListOpts struct to filter results. +func List(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := rootURL(client) + if opts != nil { + query, err := opts.ToLBListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return LBPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// CreateOptsBuilder is the interface options structs have to satisfy in order +// to be used in the main Create operation in this package. Since many +// extensions decorate or modify the common logic, it is useful for them to +// satisfy a basic interface in order for them to be used. +type CreateOptsBuilder interface { + ToLBCreateMap() (map[string]interface{}, error) +} + +// CreateOpts is the common options struct used in this package's Create +// operation. +type CreateOpts struct { + // Required - name of the load balancer to create. The name must be 128 + // characters or fewer in length, and all UTF-8 characters are valid. + Name string + + // Optional - nodes to be added. + Nodes []nodes.Node + + // Required - protocol of the service that is being load balanced. + // See http://docs.rackspace.com/loadbalancers/api/v1.0/clb-devguide/content/protocols.html + // for a full list of supported protocols. + Protocol string + + // Optional - enables or disables Half-Closed support for the load balancer. + // Half-Closed support provides the ability for one end of the connection to + // terminate its output, while still receiving data from the other end. Only + // available for TCP/TCP_CLIENT_FIRST protocols. + HalfClosed gophercloud.EnabledState + + // Optional - the type of virtual IPs you want associated with the load + // balancer. + VIPs []vips.VIP + + // Optional - the access list management feature allows fine-grained network + // access controls to be applied to the load balancer virtual IP address. + AccessList *acl.AccessList + + // Optional - algorithm that defines how traffic should be directed between + // back-end nodes. + Algorithm string + + // Optional - current connection logging configuration. + ConnectionLogging *ConnectionLogging + + // Optional - specifies a limit on the number of connections per IP address + // to help mitigate malicious or abusive traffic to your applications. + ConnThrottle *throttle.ConnectionThrottle + + // Optional - the type of health monitor check to perform to ensure that the + // service is performing properly. + HealthMonitor *monitors.Monitor + + // Optional - arbitrary information that can be associated with each LB. + Metadata map[string]interface{} + + // Optional - port number for the service you are load balancing. + Port int + + // Optional - the timeout value for the load balancer and communications with + // its nodes. Defaults to 30 seconds with a maximum of 120 seconds. + Timeout int + + // Optional - specifies whether multiple requests from clients are directed + // to the same node. + SessionPersistence *sessions.SessionPersistence + + // Optional - enables or disables HTTP to HTTPS redirection for the load + // balancer. When enabled, any HTTP request returns status code 301 (Moved + // Permanently), and the requester is redirected to the requested URL via the + // HTTPS protocol on port 443. For example, http://example.com/page.html + // would be redirected to https://example.com/page.html. Only available for + // HTTPS protocol (port=443), or HTTP protocol with a properly configured SSL + // termination (secureTrafficOnly=true, securePort=443). + HTTPSRedirect gophercloud.EnabledState +} + +// ToLBCreateMap casts a CreateOpts struct to a map. +func (opts CreateOpts) ToLBCreateMap() (map[string]interface{}, error) { + lb := make(map[string]interface{}) + + if opts.Name == "" { + return lb, errNameRequired + } + if opts.Timeout > 120 { + return lb, errTimeoutExceeded + } + + lb["name"] = opts.Name + + if len(opts.Nodes) > 0 { + nodes := []map[string]interface{}{} + for _, n := range opts.Nodes { + nodes = append(nodes, map[string]interface{}{ + "address": n.Address, + "port": n.Port, + "condition": n.Condition, + }) + } + lb["nodes"] = nodes + } + + if opts.Protocol != "" { + lb["protocol"] = opts.Protocol + } + if opts.HalfClosed != nil { + lb["halfClosed"] = opts.HalfClosed + } + if len(opts.VIPs) > 0 { + lb["virtualIps"] = opts.VIPs + } + if opts.AccessList != nil { + lb["accessList"] = &opts.AccessList + } + if opts.Algorithm != "" { + lb["algorithm"] = opts.Algorithm + } + if opts.ConnectionLogging != nil { + lb["connectionLogging"] = &opts.ConnectionLogging + } + if opts.ConnThrottle != nil { + lb["connectionThrottle"] = &opts.ConnThrottle + } + if opts.HealthMonitor != nil { + lb["healthMonitor"] = &opts.HealthMonitor + } + if len(opts.Metadata) != 0 { + lb["metadata"] = opts.Metadata + } + if opts.Port > 0 { + lb["port"] = opts.Port + } + if opts.Timeout > 0 { + lb["timeout"] = opts.Timeout + } + if opts.SessionPersistence != nil { + lb["sessionPersistence"] = &opts.SessionPersistence + } + if opts.HTTPSRedirect != nil { + lb["httpsRedirect"] = &opts.HTTPSRedirect + } + + return map[string]interface{}{"loadBalancer": lb}, nil +} + +// Create is the operation responsible for asynchronously provisioning a new +// load balancer based on the configuration defined in CreateOpts. Once the +// request is validated and progress has started on the provisioning process, a +// response struct is returned. When extracted (with Extract()), you have +// to the load balancer's unique ID and status. +// +// Once an ID is attained, you can check on the progress of the operation by +// calling Get and passing in the ID. If the corresponding request cannot be +// fulfilled due to insufficient or invalid data, an HTTP 400 (Bad Request) +// error response is returned with information regarding the nature of the +// failure in the body of the response. Failures in the validation process are +// non-recoverable and require the caller to correct the cause of the failure. +func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult { + var res CreateResult + + reqBody, err := opts.ToLBCreateMap() + if err != nil { + res.Err = err + return res + } + + _, res.Err = perigee.Request("POST", rootURL(c), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + ReqBody: &reqBody, + Results: &res.Body, + OkCodes: []int{202}, + }) + + return res +} + +// Get is the operation responsible for providing detailed information +// regarding a specific load balancer which is configured and associated with +// your account. This operation is not capable of returning details for a load +// balancer which has been deleted. +func Get(c *gophercloud.ServiceClient, id int) GetResult { + var res GetResult + + _, res.Err = perigee.Request("GET", resourceURL(c, id), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + Results: &res.Body, + OkCodes: []int{200}, + }) + + return res +} + +// BulkDelete removes all the load balancers referenced in the slice of IDs. +// Any and all configuration data associated with these load balancers is +// immediately purged and is not recoverable. +// +// If one of the items in the list cannot be removed due to its current status, +// a 400 Bad Request error is returned along with the IDs of the ones the +// system identified as potential failures for this request. +func BulkDelete(c *gophercloud.ServiceClient, ids []int) DeleteResult { + var res DeleteResult + + if len(ids) > 10 || len(ids) == 0 { + res.Err = errors.New("You must provide a minimum of 1 and a maximum of 10 LB IDs") + return res + } + + url := rootURL(c) + url += gophercloud.IDSliceToQueryString("id", ids) + + _, res.Err = perigee.Request("DELETE", url, perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + OkCodes: []int{202}, + }) + + return res +} + +// Delete removes a single load balancer. +func Delete(c *gophercloud.ServiceClient, id int) DeleteResult { + var res DeleteResult + + _, res.Err = perigee.Request("DELETE", resourceURL(c, id), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + OkCodes: []int{202}, + }) + + return res +} + +// UpdateOptsBuilder represents a type that can be converted into a JSON-like +// map structure. +type UpdateOptsBuilder interface { + ToLBUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts represents the options for updating an existing load balancer. +type UpdateOpts struct { + // Optional - new name of the load balancer. + Name string + + // Optional - the new protocol you want your load balancer to have. + // See http://docs.rackspace.com/loadbalancers/api/v1.0/clb-devguide/content/protocols.html + // for a full list of supported protocols. + Protocol string + + // Optional - see the HalfClosed field in CreateOpts for more information. + HalfClosed gophercloud.EnabledState + + // Optional - see the Algorithm field in CreateOpts for more information. + Algorithm string + + // Optional - see the Port field in CreateOpts for more information. + Port int + + // Optional - see the Timeout field in CreateOpts for more information. + Timeout int + + // Optional - see the HTTPSRedirect field in CreateOpts for more information. + HTTPSRedirect gophercloud.EnabledState +} + +// ToLBUpdateMap casts an UpdateOpts struct to a map. +func (opts UpdateOpts) ToLBUpdateMap() (map[string]interface{}, error) { + lb := make(map[string]interface{}) + + if opts.Name != "" { + lb["name"] = opts.Name + } + if opts.Protocol != "" { + lb["protocol"] = opts.Protocol + } + if opts.HalfClosed != nil { + lb["halfClosed"] = opts.HalfClosed + } + if opts.Algorithm != "" { + lb["algorithm"] = opts.Algorithm + } + if opts.Port > 0 { + lb["port"] = opts.Port + } + if opts.Timeout > 0 { + lb["timeout"] = opts.Timeout + } + if opts.HTTPSRedirect != nil { + lb["httpsRedirect"] = &opts.HTTPSRedirect + } + + return map[string]interface{}{"loadBalancer": lb}, nil +} + +// Update is the operation responsible for asynchronously updating the +// attributes of a specific load balancer. Upon successful validation of the +// request, the service returns a 202 Accepted response, and the load balancer +// enters a PENDING_UPDATE state. A user can poll the load balancer with Get to +// wait for the changes to be applied. When this happens, the load balancer will +// return to an ACTIVE state. +func Update(c *gophercloud.ServiceClient, id int, opts UpdateOptsBuilder) UpdateResult { + var res UpdateResult + + reqBody, err := opts.ToLBUpdateMap() + if err != nil { + res.Err = err + return res + } + + _, res.Err = perigee.Request("PUT", resourceURL(c, id), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + ReqBody: &reqBody, + OkCodes: []int{202}, + }) + + return res +} + +// ListProtocols is the operation responsible for returning a paginated +// collection of load balancer protocols. +func ListProtocols(client *gophercloud.ServiceClient) pagination.Pager { + url := protocolsURL(client) + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return ProtocolPage{pagination.SinglePageBase(r)} + }) +} + +// ListAlgorithms is the operation responsible for returning a paginated +// collection of load balancer algorithms. +func ListAlgorithms(client *gophercloud.ServiceClient) pagination.Pager { + url := algorithmsURL(client) + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return AlgorithmPage{pagination.SinglePageBase(r)} + }) +} + +// IsLoggingEnabled returns true if the load balancer has connection logging +// enabled and false if not. +func IsLoggingEnabled(client *gophercloud.ServiceClient, id int) (bool, error) { + var body interface{} + + _, err := perigee.Request("GET", loggingURL(client, id), perigee.Options{ + MoreHeaders: client.AuthenticatedHeaders(), + Results: &body, + OkCodes: []int{200}, + }) + if err != nil { + return false, err + } + + var resp struct { + CL struct { + Enabled bool `mapstructure:"enabled"` + } `mapstructure:"connectionLogging"` + } + + err = mapstructure.Decode(body, &resp) + return resp.CL.Enabled, err +} + +func toConnLoggingMap(state bool) map[string]map[string]bool { + return map[string]map[string]bool{ + "connectionLogging": map[string]bool{"enabled": state}, + } +} + +// EnableLogging will enable connection logging for a specified load balancer. +func EnableLogging(client *gophercloud.ServiceClient, id int) gophercloud.ErrResult { + reqBody := toConnLoggingMap(true) + var res gophercloud.ErrResult + + _, res.Err = perigee.Request("PUT", loggingURL(client, id), perigee.Options{ + MoreHeaders: client.AuthenticatedHeaders(), + ReqBody: &reqBody, + OkCodes: []int{202}, + }) + + return res +} + +// DisableLogging will disable connection logging for a specified load balancer. +func DisableLogging(client *gophercloud.ServiceClient, id int) gophercloud.ErrResult { + reqBody := toConnLoggingMap(false) + var res gophercloud.ErrResult + + _, res.Err = perigee.Request("PUT", loggingURL(client, id), perigee.Options{ + MoreHeaders: client.AuthenticatedHeaders(), + ReqBody: &reqBody, + OkCodes: []int{202}, + }) + + return res +} + +// GetErrorPage will retrieve the current error page for the load balancer. +func GetErrorPage(client *gophercloud.ServiceClient, id int) ErrorPageResult { + var res ErrorPageResult + + _, res.Err = perigee.Request("GET", errorPageURL(client, id), perigee.Options{ + MoreHeaders: client.AuthenticatedHeaders(), + Results: &res.Body, + OkCodes: []int{200}, + }) + + return res +} + +// SetErrorPage will set the HTML of the load balancer's error page to a +// specific value. +func SetErrorPage(client *gophercloud.ServiceClient, id int, html string) ErrorPageResult { + var res ErrorPageResult + + type stringMap map[string]string + reqBody := map[string]stringMap{"errorpage": stringMap{"content": html}} + + _, res.Err = perigee.Request("PUT", errorPageURL(client, id), perigee.Options{ + MoreHeaders: client.AuthenticatedHeaders(), + Results: &res.Body, + ReqBody: &reqBody, + OkCodes: []int{200}, + }) + + return res +} + +// DeleteErrorPage will delete the current error page for the load balancer. +func DeleteErrorPage(client *gophercloud.ServiceClient, id int) gophercloud.ErrResult { + var res gophercloud.ErrResult + + _, res.Err = perigee.Request("DELETE", errorPageURL(client, id), perigee.Options{ + MoreHeaders: client.AuthenticatedHeaders(), + OkCodes: []int{200}, + }) + + return res +} + +// GetStats will retrieve detailed stats related to the load balancer's usage. +func GetStats(client *gophercloud.ServiceClient, id int) StatsResult { + var res StatsResult + + _, res.Err = perigee.Request("GET", statsURL(client, id), perigee.Options{ + MoreHeaders: client.AuthenticatedHeaders(), + Results: &res.Body, + OkCodes: []int{200}, + }) + + return res +} + +// IsContentCached will check to see whether the specified load balancer caches +// content. When content caching is enabled, recently-accessed files are stored +// on the load balancer for easy retrieval by web clients. Content caching +// improves the performance of high traffic web sites by temporarily storing +// data that was recently accessed. While it's cached, requests for that data +// are served by the load balancer, which in turn reduces load off the back-end +// nodes. The result is improved response times for those requests and less +// load on the web server. +func IsContentCached(client *gophercloud.ServiceClient, id int) (bool, error) { + var body interface{} + + _, err := perigee.Request("GET", cacheURL(client, id), perigee.Options{ + MoreHeaders: client.AuthenticatedHeaders(), + Results: &body, + OkCodes: []int{200}, + }) + if err != nil { + return false, err + } + + var resp struct { + CC struct { + Enabled bool `mapstructure:"enabled"` + } `mapstructure:"contentCaching"` + } + + err = mapstructure.Decode(body, &resp) + return resp.CC.Enabled, err +} + +func toCachingMap(state bool) map[string]map[string]bool { + return map[string]map[string]bool{ + "contentCaching": map[string]bool{"enabled": state}, + } +} + +// EnableCaching will enable content-caching for the specified load balancer. +func EnableCaching(client *gophercloud.ServiceClient, id int) gophercloud.ErrResult { + reqBody := toCachingMap(true) + var res gophercloud.ErrResult + + _, res.Err = perigee.Request("PUT", cacheURL(client, id), perigee.Options{ + MoreHeaders: client.AuthenticatedHeaders(), + ReqBody: &reqBody, + OkCodes: []int{202}, + }) + + return res +} + +// DisableCaching will disable content-caching for the specified load balancer. +func DisableCaching(client *gophercloud.ServiceClient, id int) gophercloud.ErrResult { + reqBody := toCachingMap(false) + var res gophercloud.ErrResult + + _, res.Err = perigee.Request("PUT", cacheURL(client, id), perigee.Options{ + MoreHeaders: client.AuthenticatedHeaders(), + ReqBody: &reqBody, + OkCodes: []int{202}, + }) + + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/lbs/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/lbs/requests_test.go new file mode 100644 index 0000000000..a8ec19e07c --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/lbs/requests_test.go @@ -0,0 +1,438 @@ +package lbs + +import ( + "testing" + "time" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" + "github.com/rackspace/gophercloud/rackspace/lb/v1/nodes" + "github.com/rackspace/gophercloud/rackspace/lb/v1/sessions" + "github.com/rackspace/gophercloud/rackspace/lb/v1/throttle" + "github.com/rackspace/gophercloud/rackspace/lb/v1/vips" + th "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +const ( + id1 = 12345 + id2 = 67890 + ts1 = "2010-11-30T03:23:42Z" + ts2 = "2010-11-30T03:23:44Z" +) + +func toTime(t *testing.T, str string) time.Time { + ts, err := time.Parse(time.RFC3339, str) + if err != nil { + t.Fatalf("Could not parse time: %s", err.Error()) + } + return ts +} + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockListLBResponse(t) + + count := 0 + + err := List(client.ServiceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractLBs(page) + th.AssertNoErr(t, err) + + expected := []LoadBalancer{ + LoadBalancer{ + Name: "lb-site1", + ID: 71, + Protocol: "HTTP", + Port: 80, + Algorithm: "RANDOM", + Status: ACTIVE, + NodeCount: 3, + VIPs: []vips.VIP{ + vips.VIP{ + ID: 403, + Address: "206.55.130.1", + Type: "PUBLIC", + Version: "IPV4", + }, + }, + Created: Datetime{Time: toTime(t, ts1)}, + Updated: Datetime{Time: toTime(t, ts2)}, + }, + LoadBalancer{ + ID: 72, + Name: "lb-site2", + Created: Datetime{Time: toTime(t, "2011-11-30T03:23:42Z")}, + Updated: Datetime{Time: toTime(t, "2011-11-30T03:23:44Z")}, + }, + LoadBalancer{ + ID: 73, + Name: "lb-site3", + Created: Datetime{Time: toTime(t, "2012-11-30T03:23:42Z")}, + Updated: Datetime{Time: toTime(t, "2012-11-30T03:23:44Z")}, + }, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + + th.AssertNoErr(t, err) + th.AssertEquals(t, 1, count) +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockCreateLBResponse(t) + + opts := CreateOpts{ + Name: "a-new-loadbalancer", + Port: 80, + Protocol: "HTTP", + VIPs: []vips.VIP{ + vips.VIP{ID: 2341}, + vips.VIP{ID: 900001}, + }, + Nodes: []nodes.Node{ + nodes.Node{Address: "10.1.1.1", Port: 80, Condition: "ENABLED"}, + }, + } + + lb, err := Create(client.ServiceClient(), opts).Extract() + th.AssertNoErr(t, err) + + expected := &LoadBalancer{ + Name: "a-new-loadbalancer", + ID: 144, + Protocol: "HTTP", + HalfClosed: false, + Port: 83, + Algorithm: "RANDOM", + Status: BUILD, + Timeout: 30, + Cluster: Cluster{Name: "ztm-n01.staging1.lbaas.rackspace.net"}, + Nodes: []nodes.Node{ + nodes.Node{ + Address: "10.1.1.1", + ID: 653, + Port: 80, + Status: "ONLINE", + Condition: "ENABLED", + Weight: 1, + }, + }, + VIPs: []vips.VIP{ + vips.VIP{ + ID: 39, + Address: "206.10.10.210", + Type: vips.PUBLIC, + Version: vips.IPV4, + }, + vips.VIP{ + ID: 900001, + Address: "2001:4801:79f1:0002:711b:be4c:0000:0021", + Type: vips.PUBLIC, + Version: vips.IPV6, + }, + }, + Created: Datetime{Time: toTime(t, ts1)}, + Updated: Datetime{Time: toTime(t, ts2)}, + ConnectionLogging: ConnectionLogging{Enabled: false}, + } + + th.AssertDeepEquals(t, expected, lb) +} + +func TestBulkDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + ids := []int{id1, id2} + + mockBatchDeleteLBResponse(t, ids) + + err := BulkDelete(client.ServiceClient(), ids).ExtractErr() + th.AssertNoErr(t, err) +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockDeleteLBResponse(t, id1) + + err := Delete(client.ServiceClient(), id1).ExtractErr() + th.AssertNoErr(t, err) +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockGetLBResponse(t, id1) + + lb, err := Get(client.ServiceClient(), id1).Extract() + + expected := &LoadBalancer{ + Name: "sample-loadbalancer", + ID: 2000, + Protocol: "HTTP", + Port: 80, + Algorithm: "RANDOM", + Status: ACTIVE, + Timeout: 30, + ConnectionLogging: ConnectionLogging{Enabled: true}, + VIPs: []vips.VIP{ + vips.VIP{ + ID: 1000, + Address: "206.10.10.210", + Type: "PUBLIC", + Version: "IPV4", + }, + }, + Nodes: []nodes.Node{ + nodes.Node{ + Address: "10.1.1.1", + ID: 1041, + Port: 80, + Status: "ONLINE", + Condition: "ENABLED", + }, + nodes.Node{ + Address: "10.1.1.2", + ID: 1411, + Port: 80, + Status: "ONLINE", + Condition: "ENABLED", + }, + }, + SessionPersistence: sessions.SessionPersistence{Type: "HTTP_COOKIE"}, + ConnectionThrottle: throttle.ConnectionThrottle{MaxConnections: 100}, + Cluster: Cluster{Name: "c1.dfw1"}, + Created: Datetime{Time: toTime(t, ts1)}, + Updated: Datetime{Time: toTime(t, ts2)}, + SourceAddrs: SourceAddrs{ + IPv4Public: "10.12.99.28", + IPv4Private: "10.0.0.0", + IPv6Public: "2001:4801:79f1:1::1/64", + }, + } + + th.AssertDeepEquals(t, expected, lb) + th.AssertNoErr(t, err) +} + +func TestUpdate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockUpdateLBResponse(t, id1) + + opts := UpdateOpts{ + Name: "a-new-loadbalancer", + Protocol: "TCP", + HalfClosed: gophercloud.Enabled, + Algorithm: "RANDOM", + Port: 8080, + Timeout: 100, + HTTPSRedirect: gophercloud.Disabled, + } + + err := Update(client.ServiceClient(), id1, opts).ExtractErr() + th.AssertNoErr(t, err) +} + +func TestListProtocols(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockListProtocolsResponse(t) + + count := 0 + + err := ListProtocols(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractProtocols(page) + th.AssertNoErr(t, err) + + expected := []Protocol{ + Protocol{Name: "DNS_TCP", Port: 53}, + Protocol{Name: "DNS_UDP", Port: 53}, + Protocol{Name: "FTP", Port: 21}, + Protocol{Name: "HTTP", Port: 80}, + Protocol{Name: "HTTPS", Port: 443}, + Protocol{Name: "IMAPS", Port: 993}, + Protocol{Name: "IMAPv4", Port: 143}, + } + + th.CheckDeepEquals(t, expected[0:7], actual) + + return true, nil + }) + + th.AssertNoErr(t, err) + th.AssertEquals(t, 1, count) +} + +func TestListAlgorithms(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockListAlgorithmsResponse(t) + + count := 0 + + err := ListAlgorithms(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractAlgorithms(page) + th.AssertNoErr(t, err) + + expected := []Algorithm{ + Algorithm{Name: "LEAST_CONNECTIONS"}, + Algorithm{Name: "RANDOM"}, + Algorithm{Name: "ROUND_ROBIN"}, + Algorithm{Name: "WEIGHTED_LEAST_CONNECTIONS"}, + Algorithm{Name: "WEIGHTED_ROUND_ROBIN"}, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + + th.AssertNoErr(t, err) + th.AssertEquals(t, 1, count) +} + +func TestIsLoggingEnabled(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockGetLoggingResponse(t, id1) + + res, err := IsLoggingEnabled(client.ServiceClient(), id1) + th.AssertNoErr(t, err) + th.AssertEquals(t, true, res) +} + +func TestEnablingLogging(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockEnableLoggingResponse(t, id1) + + err := EnableLogging(client.ServiceClient(), id1).ExtractErr() + th.AssertNoErr(t, err) +} + +func TestDisablingLogging(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockDisableLoggingResponse(t, id1) + + err := DisableLogging(client.ServiceClient(), id1).ExtractErr() + th.AssertNoErr(t, err) +} + +func TestGetErrorPage(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockGetErrorPageResponse(t, id1) + + content, err := GetErrorPage(client.ServiceClient(), id1).Extract() + th.AssertNoErr(t, err) + + expected := &ErrorPage{Content: "DEFAULT ERROR PAGE"} + th.AssertDeepEquals(t, expected, content) +} + +func TestSetErrorPage(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockSetErrorPageResponse(t, id1) + + html := "New error page" + content, err := SetErrorPage(client.ServiceClient(), id1, html).Extract() + th.AssertNoErr(t, err) + + expected := &ErrorPage{Content: html} + th.AssertDeepEquals(t, expected, content) +} + +func TestDeleteErrorPage(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockDeleteErrorPageResponse(t, id1) + + err := DeleteErrorPage(client.ServiceClient(), id1).ExtractErr() + th.AssertNoErr(t, err) +} + +func TestGetStats(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockGetStatsResponse(t, id1) + + content, err := GetStats(client.ServiceClient(), id1).Extract() + th.AssertNoErr(t, err) + + expected := &Stats{ + ConnectTimeout: 10, + ConnectError: 20, + ConnectFailure: 30, + DataTimedOut: 40, + KeepAliveTimedOut: 50, + MaxConnections: 60, + CurrentConnections: 40, + SSLConnectTimeout: 10, + SSLConnectError: 20, + SSLConnectFailure: 30, + SSLDataTimedOut: 40, + SSLKeepAliveTimedOut: 50, + SSLMaxConnections: 60, + SSLCurrentConnections: 40, + } + th.AssertDeepEquals(t, expected, content) +} + +func TestIsCached(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockGetCachingResponse(t, id1) + + res, err := IsContentCached(client.ServiceClient(), id1) + th.AssertNoErr(t, err) + th.AssertEquals(t, true, res) +} + +func TestEnablingCaching(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockEnableCachingResponse(t, id1) + + err := EnableCaching(client.ServiceClient(), id1).ExtractErr() + th.AssertNoErr(t, err) +} + +func TestDisablingCaching(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockDisableCachingResponse(t, id1) + + err := DisableCaching(client.ServiceClient(), id1).ExtractErr() + th.AssertNoErr(t, err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/lbs/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/lbs/results.go new file mode 100644 index 0000000000..98f3962d77 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/lbs/results.go @@ -0,0 +1,420 @@ +package lbs + +import ( + "reflect" + "time" + + "github.com/mitchellh/mapstructure" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" + "github.com/rackspace/gophercloud/rackspace/lb/v1/acl" + "github.com/rackspace/gophercloud/rackspace/lb/v1/nodes" + "github.com/rackspace/gophercloud/rackspace/lb/v1/sessions" + "github.com/rackspace/gophercloud/rackspace/lb/v1/throttle" + "github.com/rackspace/gophercloud/rackspace/lb/v1/vips" +) + +// Protocol represents the network protocol which the load balancer accepts. +type Protocol struct { + // The name of the protocol, e.g. HTTP, LDAP, FTP, etc. + Name string + + // The port number for the protocol. + Port int +} + +// Algorithm defines how traffic should be directed between back-end nodes. +type Algorithm struct { + // The name of the algorithm, e.g RANDOM, ROUND_ROBIN, etc. + Name string +} + +// Status represents the potential state of a load balancer resource. +type Status string + +const ( + // ACTIVE indicates that the LB is configured properly and ready to serve + // traffic to incoming requests via the configured virtual IPs. + ACTIVE Status = "ACTIVE" + + // BUILD indicates that the LB is being provisioned for the first time and + // configuration is being applied to bring the service online. The service + // cannot yet serve incoming requests. + BUILD Status = "BUILD" + + // PENDINGUPDATE indicates that the LB is online but configuration changes + // are being applied to update the service based on a previous request. + PENDINGUPDATE Status = "PENDING_UPDATE" + + // PENDINGDELETE indicates that the LB is online but configuration changes + // are being applied to begin deletion of the service based on a previous + // request. + PENDINGDELETE Status = "PENDING_DELETE" + + // SUSPENDED indicates that the LB has been taken offline and disabled. + SUSPENDED Status = "SUSPENDED" + + // ERROR indicates that the system encountered an error when attempting to + // configure the load balancer. + ERROR Status = "ERROR" + + // DELETED indicates that the LB has been deleted. + DELETED Status = "DELETED" +) + +// Datetime represents the structure of a Created or Updated field. +type Datetime struct { + Time time.Time `mapstructure:"-"` +} + +// LoadBalancer represents a load balancer API resource. +type LoadBalancer struct { + // Human-readable name for the load balancer. + Name string + + // The unique ID for the load balancer. + ID int + + // Represents the service protocol being load balanced. See Protocol type for + // a list of accepted values. + // See http://docs.rackspace.com/loadbalancers/api/v1.0/clb-devguide/content/protocols.html + // for a full list of supported protocols. + Protocol string + + // Defines how traffic should be directed between back-end nodes. The default + // algorithm is RANDOM. See Algorithm type for a list of accepted values. + Algorithm string + + // The current status of the load balancer. + Status Status + + // The number of load balancer nodes. + NodeCount int `mapstructure:"nodeCount"` + + // Slice of virtual IPs associated with this load balancer. + VIPs []vips.VIP `mapstructure:"virtualIps"` + + // Datetime when the LB was created. + Created Datetime + + // Datetime when the LB was created. + Updated Datetime + + // Port number for the service you are load balancing. + Port int + + // HalfClosed provides the ability for one end of the connection to + // terminate its output while still receiving data from the other end. This + // is only available on TCP/TCP_CLIENT_FIRST protocols. + HalfClosed bool + + // Timeout represents the timeout value between a load balancer and its + // nodes. Defaults to 30 seconds with a maximum of 120 seconds. + Timeout int + + // The cluster name. + Cluster Cluster + + // Nodes shows all the back-end nodes which are associated with the load + // balancer. These are the devices which are delivered traffic. + Nodes []nodes.Node + + // Current connection logging configuration. + ConnectionLogging ConnectionLogging + + // SessionPersistence specifies whether multiple requests from clients are + // directed to the same node. + SessionPersistence sessions.SessionPersistence + + // ConnectionThrottle specifies a limit on the number of connections per IP + // address to help mitigate malicious or abusive traffic to your applications. + ConnectionThrottle throttle.ConnectionThrottle + + // The source public and private IP addresses. + SourceAddrs SourceAddrs `mapstructure:"sourceAddresses"` + + // Represents the access rules for this particular load balancer. IP addresses + // or subnet ranges, depending on their type (ALLOW or DENY), can be permitted + // or blocked. + AccessList acl.AccessList +} + +// SourceAddrs represents the source public and private IP addresses. +type SourceAddrs struct { + IPv4Public string `json:"ipv4Public" mapstructure:"ipv4Public"` + IPv4Private string `json:"ipv4Servicenet" mapstructure:"ipv4Servicenet"` + IPv6Public string `json:"ipv6Public" mapstructure:"ipv6Public"` + IPv6Private string `json:"ipv6Servicenet" mapstructure:"ipv6Servicenet"` +} + +// ConnectionLogging - temp +type ConnectionLogging struct { + Enabled bool +} + +// Cluster - temp +type Cluster struct { + Name string +} + +// LBPage is the page returned by a pager when traversing over a collection of +// LBs. +type LBPage struct { + pagination.LinkedPageBase +} + +// IsEmpty checks whether a NetworkPage struct is empty. +func (p LBPage) IsEmpty() (bool, error) { + is, err := ExtractLBs(p) + if err != nil { + return true, nil + } + return len(is) == 0, nil +} + +// ExtractLBs accepts a Page struct, specifically a LBPage struct, and extracts +// the elements into a slice of LoadBalancer structs. In other words, a generic +// collection is mapped into a relevant slice. +func ExtractLBs(page pagination.Page) ([]LoadBalancer, error) { + var resp struct { + LBs []LoadBalancer `mapstructure:"loadBalancers" json:"loadBalancers"` + } + + coll := page.(LBPage).Body + err := mapstructure.Decode(coll, &resp) + + s := reflect.ValueOf(coll.(map[string]interface{})["loadBalancers"]) + + for i := 0; i < s.Len(); i++ { + val := (s.Index(i).Interface()).(map[string]interface{}) + + ts, err := extractTS(val, "created") + if err != nil { + return resp.LBs, err + } + resp.LBs[i].Created.Time = ts + + ts, err = extractTS(val, "updated") + if err != nil { + return resp.LBs, err + } + resp.LBs[i].Updated.Time = ts + } + + return resp.LBs, err +} + +func extractTS(body map[string]interface{}, key string) (time.Time, error) { + val := body[key].(map[string]interface{}) + return time.Parse(time.RFC3339, val["time"].(string)) +} + +type commonResult struct { + gophercloud.Result +} + +// Extract interprets any commonResult as a LB, if possible. +func (r commonResult) Extract() (*LoadBalancer, error) { + if r.Err != nil { + return nil, r.Err + } + + var response struct { + LB LoadBalancer `mapstructure:"loadBalancer"` + } + + err := mapstructure.Decode(r.Body, &response) + + json := r.Body.(map[string]interface{}) + lb := json["loadBalancer"].(map[string]interface{}) + + ts, err := extractTS(lb, "created") + if err != nil { + return nil, err + } + response.LB.Created.Time = ts + + ts, err = extractTS(lb, "updated") + if err != nil { + return nil, err + } + response.LB.Updated.Time = ts + + return &response.LB, err +} + +// CreateResult represents the result of a create operation. +type CreateResult struct { + commonResult +} + +// DeleteResult represents the result of a delete operation. +type DeleteResult struct { + gophercloud.ErrResult +} + +// UpdateResult represents the result of an update operation. +type UpdateResult struct { + gophercloud.ErrResult +} + +// GetResult represents the result of a get operation. +type GetResult struct { + commonResult +} + +// ProtocolPage is the page returned by a pager when traversing over a +// collection of LB protocols. +type ProtocolPage struct { + pagination.SinglePageBase +} + +// IsEmpty checks whether a ProtocolPage struct is empty. +func (p ProtocolPage) IsEmpty() (bool, error) { + is, err := ExtractProtocols(p) + if err != nil { + return true, nil + } + return len(is) == 0, nil +} + +// ExtractProtocols accepts a Page struct, specifically a ProtocolPage struct, +// and extracts the elements into a slice of Protocol structs. In other +// words, a generic collection is mapped into a relevant slice. +func ExtractProtocols(page pagination.Page) ([]Protocol, error) { + var resp struct { + Protocols []Protocol `mapstructure:"protocols" json:"protocols"` + } + err := mapstructure.Decode(page.(ProtocolPage).Body, &resp) + return resp.Protocols, err +} + +// AlgorithmPage is the page returned by a pager when traversing over a +// collection of LB algorithms. +type AlgorithmPage struct { + pagination.SinglePageBase +} + +// IsEmpty checks whether an AlgorithmPage struct is empty. +func (p AlgorithmPage) IsEmpty() (bool, error) { + is, err := ExtractAlgorithms(p) + if err != nil { + return true, nil + } + return len(is) == 0, nil +} + +// ExtractAlgorithms accepts a Page struct, specifically an AlgorithmPage struct, +// and extracts the elements into a slice of Algorithm structs. In other +// words, a generic collection is mapped into a relevant slice. +func ExtractAlgorithms(page pagination.Page) ([]Algorithm, error) { + var resp struct { + Algorithms []Algorithm `mapstructure:"algorithms" json:"algorithms"` + } + err := mapstructure.Decode(page.(AlgorithmPage).Body, &resp) + return resp.Algorithms, err +} + +// ErrorPage represents the HTML file that is shown to an end user who is +// attempting to access a load balancer node that is offline/unavailable. +// +// During provisioning, every load balancer is configured with a default error +// page that gets displayed when traffic is requested for an offline node. +// +// You can add a single custom error page with an HTTP-based protocol to a load +// balancer. Page updates override existing content. If a custom error page is +// deleted, or the load balancer is changed to a non-HTTP protocol, the default +// error page is restored. +type ErrorPage struct { + Content string +} + +// ErrorPageResult represents the result of an error page operation - +// specifically getting or creating one. +type ErrorPageResult struct { + gophercloud.Result +} + +// Extract interprets any commonResult as an ErrorPage, if possible. +func (r ErrorPageResult) Extract() (*ErrorPage, error) { + if r.Err != nil { + return nil, r.Err + } + + var response struct { + ErrorPage ErrorPage `mapstructure:"errorpage"` + } + + err := mapstructure.Decode(r.Body, &response) + + return &response.ErrorPage, err +} + +// Stats represents all the key information about a load balancer's usage. +type Stats struct { + // The number of connections closed by this load balancer because its + // ConnectTimeout interval was exceeded. + ConnectTimeout int `mapstructure:"connectTimeOut"` + + // The number of transaction or protocol errors for this load balancer. + ConnectError int + + // Number of connection failures for this load balancer. + ConnectFailure int + + // Number of connections closed by this load balancer because its Timeout + // interval was exceeded. + DataTimedOut int + + // Number of connections closed by this load balancer because the + // 'keepalive_timeout' interval was exceeded. + KeepAliveTimedOut int + + // The maximum number of simultaneous TCP connections this load balancer has + // processed at any one time. + MaxConnections int `mapstructure:"maxConn"` + + // Number of simultaneous connections active at the time of the request. + CurrentConnections int `mapstructure:"currentConn"` + + // Number of SSL connections closed by this load balancer because the + // ConnectTimeout interval was exceeded. + SSLConnectTimeout int `mapstructure:"connectTimeOutSsl"` + + // Number of SSL transaction or protocol erros in this load balancer. + SSLConnectError int `mapstructure:"connectErrorSsl"` + + // Number of SSL connection failures in this load balancer. + SSLConnectFailure int `mapstructure:"connectFailureSsl"` + + // Number of SSL connections closed by this load balancer because the + // Timeout interval was exceeded. + SSLDataTimedOut int `mapstructure:"dataTimedOutSsl"` + + // Number of SSL connections closed by this load balancer because the + // 'keepalive_timeout' interval was exceeded. + SSLKeepAliveTimedOut int `mapstructure:"keepAliveTimedOutSsl"` + + // Maximum number of simultaneous SSL connections this load balancer has + // processed at any one time. + SSLMaxConnections int `mapstructure:"maxConnSsl"` + + // Number of simultaneous SSL connections active at the time of the request. + SSLCurrentConnections int `mapstructure:"currentConnSsl"` +} + +// StatsResult represents the result of a Stats operation. +type StatsResult struct { + gophercloud.Result +} + +// Extract interprets any commonResult as a Stats struct, if possible. +func (r StatsResult) Extract() (*Stats, error) { + if r.Err != nil { + return nil, r.Err + } + res := &Stats{} + err := mapstructure.Decode(r.Body, res) + return res, err +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/lbs/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/lbs/urls.go new file mode 100644 index 0000000000..471a86b0a7 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/lbs/urls.go @@ -0,0 +1,49 @@ +package lbs + +import ( + "strconv" + + "github.com/rackspace/gophercloud" +) + +const ( + path = "loadbalancers" + protocolsPath = "protocols" + algorithmsPath = "algorithms" + logPath = "connectionlogging" + epPath = "errorpage" + stPath = "stats" + cachePath = "contentcaching" +) + +func resourceURL(c *gophercloud.ServiceClient, id int) string { + return c.ServiceURL(path, strconv.Itoa(id)) +} + +func rootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(path) +} + +func protocolsURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(path, protocolsPath) +} + +func algorithmsURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(path, algorithmsPath) +} + +func loggingURL(c *gophercloud.ServiceClient, id int) string { + return c.ServiceURL(path, strconv.Itoa(id), logPath) +} + +func errorPageURL(c *gophercloud.ServiceClient, id int) string { + return c.ServiceURL(path, strconv.Itoa(id), epPath) +} + +func statsURL(c *gophercloud.ServiceClient, id int) string { + return c.ServiceURL(path, strconv.Itoa(id), stPath) +} + +func cacheURL(c *gophercloud.ServiceClient, id int) string { + return c.ServiceURL(path, strconv.Itoa(id), cachePath) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/monitors/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/monitors/doc.go new file mode 100644 index 0000000000..2c5be75ae4 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/monitors/doc.go @@ -0,0 +1,21 @@ +/* +Package monitors provides information and interaction with the Health Monitor +API resource for the Rackspace Cloud Load Balancer service. + +The load balancing service includes a health monitoring resource that +periodically checks your back-end nodes to ensure they are responding correctly. +If a node does not respond, it is removed from rotation until the health monitor +determines that the node is functional. In addition to being performed +periodically, a health check also executes against every new node that is +added, to ensure that the node is operating properly before allowing it to +service traffic. Only one health monitor is allowed to be enabled on a load +balancer at a time. + +As part of a good strategy for monitoring connections, secondary nodes should +also be created which provide failover for effectively routing traffic in case +the primary node fails. This is an additional feature that ensures that you +remain up in case your primary node fails. + +There are three types of health monitor: CONNECT, HTTP and HTTPS. +*/ +package monitors diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/monitors/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/monitors/fixtures.go new file mode 100644 index 0000000000..a565abced5 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/monitors/fixtures.go @@ -0,0 +1,87 @@ +package monitors + +import ( + "fmt" + "net/http" + "strconv" + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +func _rootURL(lbID int) string { + return "/loadbalancers/" + strconv.Itoa(lbID) + "/healthmonitor" +} + +func mockGetResponse(t *testing.T, lbID int) { + th.Mux.HandleFunc(_rootURL(lbID), func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "healthMonitor": { + "type": "CONNECT", + "delay": 10, + "timeout": 10, + "attemptsBeforeDeactivation": 3 + } +} + `) + }) +} + +func mockUpdateConnectResponse(t *testing.T, lbID int) { + th.Mux.HandleFunc(_rootURL(lbID), func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + th.TestJSONRequest(t, r, ` +{ + "healthMonitor": { + "type": "CONNECT", + "delay": 10, + "timeout": 10, + "attemptsBeforeDeactivation": 3 + } +} + `) + + w.WriteHeader(http.StatusAccepted) + }) +} + +func mockUpdateHTTPResponse(t *testing.T, lbID int) { + th.Mux.HandleFunc(_rootURL(lbID), func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + th.TestJSONRequest(t, r, ` +{ + "healthMonitor": { + "attemptsBeforeDeactivation": 3, + "bodyRegex": "{regex}", + "delay": 10, + "path": "/foo", + "statusRegex": "200", + "timeout": 10, + "type": "HTTPS" + } +} + `) + + w.WriteHeader(http.StatusAccepted) + }) +} + +func mockDeleteResponse(t *testing.T, lbID int) { + th.Mux.HandleFunc(_rootURL(lbID), func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusAccepted) + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/monitors/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/monitors/requests.go new file mode 100644 index 0000000000..cfc35d2ef7 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/monitors/requests.go @@ -0,0 +1,178 @@ +package monitors + +import ( + "errors" + + "github.com/racker/perigee" + + "github.com/rackspace/gophercloud" +) + +var ( + errAttemptLimit = errors.New("AttemptLimit field must be an int greater than 1 and less than 10") + errDelay = errors.New("Delay field must be an int greater than 1 and less than 10") + errTimeout = errors.New("Timeout field must be an int greater than 1 and less than 10") +) + +// UpdateOptsBuilder is the interface options structs have to satisfy in order +// to be used in the main Update operation in this package. +type UpdateOptsBuilder interface { + ToMonitorUpdateMap() (map[string]interface{}, error) +} + +// UpdateConnectMonitorOpts represents the options needed to update a CONNECT +// monitor. +type UpdateConnectMonitorOpts struct { + // Required - number of permissible monitor failures before removing a node + // from rotation. Must be a number between 1 and 10. + AttemptLimit int + + // Required - the minimum number of seconds to wait before executing the + // health monitor. Must be a number between 1 and 3600. + Delay int + + // Required - maximum number of seconds to wait for a connection to be + // established before timing out. Must be a number between 1 and 300. + Timeout int +} + +// ToMonitorUpdateMap produces a map for updating CONNECT monitors. +func (opts UpdateConnectMonitorOpts) ToMonitorUpdateMap() (map[string]interface{}, error) { + type m map[string]interface{} + + if !gophercloud.IntWithinRange(opts.AttemptLimit, 1, 10) { + return m{}, errAttemptLimit + } + if !gophercloud.IntWithinRange(opts.Delay, 1, 3600) { + return m{}, errDelay + } + if !gophercloud.IntWithinRange(opts.Timeout, 1, 300) { + return m{}, errTimeout + } + + return m{"healthMonitor": m{ + "attemptsBeforeDeactivation": opts.AttemptLimit, + "delay": opts.Delay, + "timeout": opts.Timeout, + "type": CONNECT, + }}, nil +} + +// UpdateHTTPMonitorOpts represents the options needed to update a HTTP monitor. +type UpdateHTTPMonitorOpts struct { + // Required - number of permissible monitor failures before removing a node + // from rotation. Must be a number between 1 and 10. + AttemptLimit int `mapstructure:"attemptsBeforeDeactivation"` + + // Required - the minimum number of seconds to wait before executing the + // health monitor. Must be a number between 1 and 3600. + Delay int + + // Required - maximum number of seconds to wait for a connection to be + // established before timing out. Must be a number between 1 and 300. + Timeout int + + // Required - a regular expression that will be used to evaluate the contents + // of the body of the response. + BodyRegex string + + // Required - the HTTP path that will be used in the sample request. + Path string + + // Required - a regular expression that will be used to evaluate the HTTP + // status code returned in the response. + StatusRegex string + + // Optional - the name of a host for which the health monitors will check. + HostHeader string + + // Required - either HTTP or HTTPS + Type Type +} + +// ToMonitorUpdateMap produces a map for updating HTTP(S) monitors. +func (opts UpdateHTTPMonitorOpts) ToMonitorUpdateMap() (map[string]interface{}, error) { + type m map[string]interface{} + + if !gophercloud.IntWithinRange(opts.AttemptLimit, 1, 10) { + return m{}, errAttemptLimit + } + if !gophercloud.IntWithinRange(opts.Delay, 1, 3600) { + return m{}, errDelay + } + if !gophercloud.IntWithinRange(opts.Timeout, 1, 300) { + return m{}, errTimeout + } + if opts.Type != HTTP && opts.Type != HTTPS { + return m{}, errors.New("Type must either by HTTP or HTTPS") + } + if opts.BodyRegex == "" { + return m{}, errors.New("BodyRegex is a required field") + } + if opts.Path == "" { + return m{}, errors.New("Path is a required field") + } + if opts.StatusRegex == "" { + return m{}, errors.New("StatusRegex is a required field") + } + + json := m{ + "attemptsBeforeDeactivation": opts.AttemptLimit, + "delay": opts.Delay, + "timeout": opts.Timeout, + "type": opts.Type, + "bodyRegex": opts.BodyRegex, + "path": opts.Path, + "statusRegex": opts.StatusRegex, + } + + if opts.HostHeader != "" { + json["hostHeader"] = opts.HostHeader + } + + return m{"healthMonitor": json}, nil +} + +// Update is the operation responsible for updating a health monitor. +func Update(c *gophercloud.ServiceClient, id int, opts UpdateOptsBuilder) UpdateResult { + var res UpdateResult + + reqBody, err := opts.ToMonitorUpdateMap() + if err != nil { + res.Err = err + return res + } + + _, res.Err = perigee.Request("PUT", rootURL(c, id), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + ReqBody: &reqBody, + OkCodes: []int{202}, + }) + + return res +} + +// Get is the operation responsible for showing details of a health monitor. +func Get(c *gophercloud.ServiceClient, id int) GetResult { + var res GetResult + + _, res.Err = perigee.Request("GET", rootURL(c, id), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + Results: &res.Body, + OkCodes: []int{200}, + }) + + return res +} + +// Delete is the operation responsible for deleting a health monitor. +func Delete(c *gophercloud.ServiceClient, id int) DeleteResult { + var res DeleteResult + + _, res.Err = perigee.Request("DELETE", rootURL(c, id), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + OkCodes: []int{202}, + }) + + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/monitors/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/monitors/requests_test.go new file mode 100644 index 0000000000..76a60db7f4 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/monitors/requests_test.go @@ -0,0 +1,75 @@ +package monitors + +import ( + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +const lbID = 12345 + +func TestUpdateCONNECT(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockUpdateConnectResponse(t, lbID) + + opts := UpdateConnectMonitorOpts{ + AttemptLimit: 3, + Delay: 10, + Timeout: 10, + } + + err := Update(client.ServiceClient(), lbID, opts).ExtractErr() + th.AssertNoErr(t, err) +} + +func TestUpdateHTTP(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockUpdateHTTPResponse(t, lbID) + + opts := UpdateHTTPMonitorOpts{ + AttemptLimit: 3, + Delay: 10, + Timeout: 10, + BodyRegex: "{regex}", + Path: "/foo", + StatusRegex: "200", + Type: HTTPS, + } + + err := Update(client.ServiceClient(), lbID, opts).ExtractErr() + th.AssertNoErr(t, err) +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockGetResponse(t, lbID) + + m, err := Get(client.ServiceClient(), lbID).Extract() + th.AssertNoErr(t, err) + + expected := &Monitor{ + Type: CONNECT, + Delay: 10, + Timeout: 10, + AttemptLimit: 3, + } + + th.AssertDeepEquals(t, expected, m) +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockDeleteResponse(t, lbID) + + err := Delete(client.ServiceClient(), lbID).ExtractErr() + th.AssertNoErr(t, err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/monitors/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/monitors/results.go new file mode 100644 index 0000000000..eec556f343 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/monitors/results.go @@ -0,0 +1,90 @@ +package monitors + +import ( + "github.com/mitchellh/mapstructure" + + "github.com/rackspace/gophercloud" +) + +// Type represents the type of Monitor. +type Type string + +// Useful constants. +const ( + CONNECT Type = "CONNECT" + HTTP Type = "HTTP" + HTTPS Type = "HTTPS" +) + +// Monitor represents a health monitor API resource. A monitor comes in three +// forms: CONNECT, HTTP or HTTPS. +// +// A CONNECT monitor establishes a basic connection to each node on its defined +// port to ensure that the service is listening properly. The connect monitor +// is the most basic type of health check and does no post-processing or +// protocol-specific health checks. +// +// HTTP and HTTPS health monitors are generally considered more intelligent and +// powerful than CONNECT. It is capable of processing an HTTP or HTTPS response +// to determine the condition of a node. It supports the same basic properties +// as CONNECT and includes additional attributes that are used to evaluate the +// HTTP response. +type Monitor struct { + // Number of permissible monitor failures before removing a node from + // rotation. + AttemptLimit int `mapstructure:"attemptsBeforeDeactivation"` + + // The minimum number of seconds to wait before executing the health monitor. + Delay int + + // Maximum number of seconds to wait for a connection to be established + // before timing out. + Timeout int + + // Type of the health monitor. + Type Type + + // A regular expression that will be used to evaluate the contents of the + // body of the response. + BodyRegex string + + // The name of a host for which the health monitors will check. + HostHeader string + + // The HTTP path that will be used in the sample request. + Path string + + // A regular expression that will be used to evaluate the HTTP status code + // returned in the response. + StatusRegex string +} + +// UpdateResult represents the result of an Update operation. +type UpdateResult struct { + gophercloud.ErrResult +} + +// GetResult represents the result of a Get operation. +type GetResult struct { + gophercloud.Result +} + +// DeleteResult represents the result of an Delete operation. +type DeleteResult struct { + gophercloud.ErrResult +} + +// Extract interprets any GetResult as a Monitor. +func (r GetResult) Extract() (*Monitor, error) { + if r.Err != nil { + return nil, r.Err + } + + var response struct { + M Monitor `mapstructure:"healthMonitor"` + } + + err := mapstructure.Decode(r.Body, &response) + + return &response.M, err +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/monitors/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/monitors/urls.go new file mode 100644 index 0000000000..0a1e6df5f4 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/monitors/urls.go @@ -0,0 +1,16 @@ +package monitors + +import ( + "strconv" + + "github.com/rackspace/gophercloud" +) + +const ( + path = "loadbalancers" + monitorPath = "healthmonitor" +) + +func rootURL(c *gophercloud.ServiceClient, lbID int) string { + return c.ServiceURL(path, strconv.Itoa(lbID), monitorPath) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/nodes/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/nodes/doc.go new file mode 100644 index 0000000000..49c431894a --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/nodes/doc.go @@ -0,0 +1,35 @@ +/* +Package nodes provides information and interaction with the Node API resource +for the Rackspace Cloud Load Balancer service. + +Nodes are responsible for servicing the requests received through the load +balancer's virtual IP. A node is usually a virtual machine. By default, the +load balancer employs a basic health check that ensures the node is listening +on its defined port. The node is checked at the time of addition and at regular +intervals as defined by the load balancer's health check configuration. If a +back-end node is not listening on its port, or does not meet the conditions of +the defined check, then connections will not be forwarded to the node, and its +status is changed to OFFLINE. Only nodes that are in an ONLINE status receive +and can service traffic from the load balancer. + +All nodes have an associated status that indicates whether the node is +ONLINE, OFFLINE, or DRAINING. Only nodes that are in ONLINE status can receive +and service traffic from the load balancer. The OFFLINE status represents a +node that cannot accept or service traffic. A node in DRAINING status +represents a node that stops the traffic manager from sending any additional +new connections to the node, but honors established sessions. If the traffic +manager receives a request and session persistence requires that the node is +used, the traffic manager uses it. The status is determined by the passive or +active health monitors. + +If the WEIGHTED_ROUND_ROBIN load balancer algorithm mode is selected, then the +caller should assign the relevant weights to the node as part of the weight +attribute of the node element. When the algorithm of the load balancer is +changed to WEIGHTED_ROUND_ROBIN and the nodes do not already have an assigned +weight, the service automatically sets the weight to 1 for all nodes. + +One or more secondary nodes can be added to a specified load balancer so that +if all the primary nodes fail, traffic can be redirected to secondary nodes. +The type attribute allows configuring the node as either PRIMARY or SECONDARY. +*/ +package nodes diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/nodes/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/nodes/fixtures.go new file mode 100644 index 0000000000..0aea541036 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/nodes/fixtures.go @@ -0,0 +1,208 @@ +package nodes + +import ( + "fmt" + "net/http" + "strconv" + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +func _rootURL(lbID int) string { + return "/loadbalancers/" + strconv.Itoa(lbID) + "/nodes" +} + +func _nodeURL(lbID, nodeID int) string { + return _rootURL(lbID) + "/" + strconv.Itoa(nodeID) +} + +func mockListResponse(t *testing.T, lbID int) { + th.Mux.HandleFunc(_rootURL(lbID), func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "nodes": [ + { + "id": 410, + "address": "10.1.1.1", + "port": 80, + "condition": "ENABLED", + "status": "ONLINE", + "weight": 3, + "type": "PRIMARY" + }, + { + "id": 411, + "address": "10.1.1.2", + "port": 80, + "condition": "ENABLED", + "status": "ONLINE", + "weight": 8, + "type": "SECONDARY" + } + ] +} + `) + }) +} + +func mockCreateResponse(t *testing.T, lbID int) { + th.Mux.HandleFunc(_rootURL(lbID), func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + th.TestJSONRequest(t, r, ` +{ + "nodes": [ + { + "address": "10.2.2.3", + "port": 80, + "condition": "ENABLED", + "type": "PRIMARY" + }, + { + "address": "10.2.2.4", + "port": 81, + "condition": "ENABLED", + "type": "SECONDARY" + } + ] +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusAccepted) + + fmt.Fprintf(w, ` +{ + "nodes": [ + { + "address": "10.2.2.3", + "id": 185, + "port": 80, + "status": "ONLINE", + "condition": "ENABLED", + "weight": 1, + "type": "PRIMARY" + }, + { + "address": "10.2.2.4", + "id": 186, + "port": 81, + "status": "ONLINE", + "condition": "ENABLED", + "weight": 1, + "type": "SECONDARY" + } + ] +} + `) + }) +} + +func mockBatchDeleteResponse(t *testing.T, lbID int, ids []int) { + th.Mux.HandleFunc(_rootURL(lbID), func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + r.ParseForm() + + for k, v := range ids { + fids := r.Form["id"] + th.AssertEquals(t, strconv.Itoa(v), fids[k]) + } + + w.WriteHeader(http.StatusAccepted) + }) +} + +func mockDeleteResponse(t *testing.T, lbID, nodeID int) { + th.Mux.HandleFunc(_nodeURL(lbID, nodeID), func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusAccepted) + }) +} + +func mockGetResponse(t *testing.T, lbID, nodeID int) { + th.Mux.HandleFunc(_nodeURL(lbID, nodeID), func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "node": { + "id": 410, + "address": "10.1.1.1", + "port": 80, + "condition": "ENABLED", + "status": "ONLINE", + "weight": 12, + "type": "PRIMARY" + } +} + `) + }) +} + +func mockUpdateResponse(t *testing.T, lbID, nodeID int) { + th.Mux.HandleFunc(_nodeURL(lbID, nodeID), func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + th.TestJSONRequest(t, r, ` +{ + "node": { + "address": "1.2.3.4", + "condition": "DRAINING", + "weight": 10, + "type": "SECONDARY" + } +} + `) + + w.WriteHeader(http.StatusAccepted) + }) +} + +func mockListEventsResponse(t *testing.T, lbID int) { + th.Mux.HandleFunc(_rootURL(lbID)+"/events", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "nodeServiceEvents": [ + { + "detailedMessage": "Node is ok", + "nodeId": 373, + "id": 7, + "type": "UPDATE_NODE", + "description": "Node '373' status changed to 'ONLINE' for load balancer '323'", + "category": "UPDATE", + "severity": "INFO", + "relativeUri": "/406271/loadbalancers/323/nodes/373/events", + "accountId": 406271, + "loadbalancerId": 323, + "title": "Node Status Updated", + "author": "Rackspace Cloud", + "created": "10-30-2012 10:18:23" + } + ] +} +`) + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/nodes/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/nodes/requests.go new file mode 100644 index 0000000000..bfd0aed79f --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/nodes/requests.go @@ -0,0 +1,286 @@ +package nodes + +import ( + "errors" + "fmt" + + "github.com/racker/perigee" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// List is the operation responsible for returning a paginated collection of +// load balancer nodes. It requires the node ID, its parent load balancer ID, +// and optional limit integer (passed in either as a pointer or a nil poitner). +func List(client *gophercloud.ServiceClient, loadBalancerID int, limit *int) pagination.Pager { + url := rootURL(client, loadBalancerID) + if limit != nil { + url += fmt.Sprintf("?limit=%d", limit) + } + + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return NodePage{pagination.SinglePageBase(r)} + }) +} + +// CreateOptsBuilder is the interface responsible for generating the JSON +// for a Create operation. +type CreateOptsBuilder interface { + ToNodeCreateMap() (map[string]interface{}, error) +} + +// CreateOpts is a slice of CreateOpt structs, that allow the user to create +// multiple nodes in a single operation (one node per CreateOpt). +type CreateOpts []CreateOpt + +// CreateOpt represents the options to create a single node. +type CreateOpt struct { + // Required - the IP address or CIDR for this back-end node. It can either be + // a private IP (ServiceNet) or a public IP. + Address string + + // Optional - the port on which traffic is sent and received. + Port int + + // Optional - the condition of the node. See the consts in Results.go. + Condition Condition + + // Optional - the type of the node. See the consts in Results.go. + Type Type + + // Optional - a pointer to an integer between 0 and 100. + Weight *int +} + +func validateWeight(weight *int) error { + if weight != nil && (*weight > 100 || *weight < 0) { + return errors.New("Weight must be a valid int between 0 and 100") + } + return nil +} + +// ToNodeCreateMap converts a slice of options into a map that can be used for +// the JSON. +func (opts CreateOpts) ToNodeCreateMap() (map[string]interface{}, error) { + type nodeMap map[string]interface{} + nodes := []nodeMap{} + + for k, v := range opts { + if v.Address == "" { + return nodeMap{}, fmt.Errorf("ID is a required attribute, none provided for %d CreateOpt element", k) + } + if weightErr := validateWeight(v.Weight); weightErr != nil { + return nodeMap{}, weightErr + } + + node := make(map[string]interface{}) + node["address"] = v.Address + + if v.Port > 0 { + node["port"] = v.Port + } + if v.Condition != "" { + node["condition"] = v.Condition + } + if v.Type != "" { + node["type"] = v.Type + } + if v.Weight != nil { + node["weight"] = &v.Weight + } + + nodes = append(nodes, node) + } + + return nodeMap{"nodes": nodes}, nil +} + +// Create is the operation responsible for creating a new node on a load +// balancer. Since every load balancer exists in both ServiceNet and the public +// Internet, both private and public IP addresses can be used for nodes. +// +// If nodes need time to boot up services before they become operational, you +// can temporarily prevent traffic from being sent to that node by setting the +// Condition field to DRAINING. Health checks will still be performed; but once +// your node is ready, you can update its condition to ENABLED and have it +// handle traffic. +func Create(client *gophercloud.ServiceClient, loadBalancerID int, opts CreateOptsBuilder) CreateResult { + var res CreateResult + + reqBody, err := opts.ToNodeCreateMap() + if err != nil { + res.Err = err + return res + } + + resp, err := perigee.Request("POST", rootURL(client, loadBalancerID), perigee.Options{ + MoreHeaders: client.AuthenticatedHeaders(), + ReqBody: &reqBody, + Results: &res.Body, + OkCodes: []int{202}, + }) + if err != nil { + res.Err = err + return res + } + + pr, err := pagination.PageResultFrom(resp.HttpResponse) + if err != nil { + res.Err = err + return res + } + + return CreateResult{pagination.SinglePageBase(pr)} +} + +// BulkDelete is the operation responsible for batch deleting multiple nodes in +// a single operation. It accepts a slice of integer IDs and will remove them +// from the load balancer. The maximum limit is 10 node removals at once. +func BulkDelete(c *gophercloud.ServiceClient, loadBalancerID int, nodeIDs []int) DeleteResult { + var res DeleteResult + + if len(nodeIDs) > 10 || len(nodeIDs) == 0 { + res.Err = errors.New("You must provide a minimum of 1 and a maximum of 10 node IDs") + return res + } + + url := rootURL(c, loadBalancerID) + url += gophercloud.IDSliceToQueryString("id", nodeIDs) + + _, res.Err = perigee.Request("DELETE", url, perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + OkCodes: []int{202}, + }) + + return res +} + +// Get is the operation responsible for showing details for a single node. +func Get(c *gophercloud.ServiceClient, lbID, nodeID int) GetResult { + var res GetResult + + _, res.Err = perigee.Request("GET", resourceURL(c, lbID, nodeID), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + Results: &res.Body, + OkCodes: []int{200}, + }) + + return res +} + +// UpdateOptsBuilder represents a type that can be converted into a JSON-like +// map structure. +type UpdateOptsBuilder interface { + ToNodeUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts represent the options for updating an existing node. +type UpdateOpts struct { + // Optional - the IP address or CIDR for this back-end node. It can either be + // a private IP (ServiceNet) or a public IP. + Address string + + // Optional - the condition of the node. See the consts in Results.go. + Condition Condition + + // Optional - the type of the node. See the consts in Results.go. + Type Type + + // Optional - a pointer to an integer between 0 and 100. + Weight *int +} + +// ToNodeUpdateMap converts an options struct into a JSON-like map. +func (opts UpdateOpts) ToNodeUpdateMap() (map[string]interface{}, error) { + node := make(map[string]interface{}) + + if opts.Address != "" { + node["address"] = opts.Address + } + if opts.Condition != "" { + node["condition"] = opts.Condition + } + if opts.Weight != nil { + if weightErr := validateWeight(opts.Weight); weightErr != nil { + return node, weightErr + } + node["weight"] = &opts.Weight + } + if opts.Type != "" { + node["type"] = opts.Type + } + + return map[string]interface{}{"node": node}, nil +} + +// Update is the operation responsible for updating an existing node. A node's +// IP, port, and status are immutable attributes and cannot be modified. +func Update(c *gophercloud.ServiceClient, lbID, nodeID int, opts UpdateOptsBuilder) UpdateResult { + var res UpdateResult + + reqBody, err := opts.ToNodeUpdateMap() + if err != nil { + res.Err = err + return res + } + + _, res.Err = perigee.Request("PUT", resourceURL(c, lbID, nodeID), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + ReqBody: &reqBody, + OkCodes: []int{202}, + }) + + return res +} + +// Delete is the operation responsible for permanently deleting a node. +func Delete(c *gophercloud.ServiceClient, lbID, nodeID int) DeleteResult { + var res DeleteResult + _, res.Err = perigee.Request("DELETE", resourceURL(c, lbID, nodeID), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + OkCodes: []int{202}, + }) + return res +} + +// ListEventsOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListEventsOptsBuilder interface { + ToEventsListQuery() (string, error) +} + +// ListEventsOpts allows the filtering and sorting of paginated collections through +// the API. +type ListEventsOpts struct { + Marker string `q:"marker"` + Limit int `q:"limit"` +} + +// ToEventsListQuery formats a ListOpts into a query string. +func (opts ListEventsOpts) ToEventsListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + if err != nil { + return "", err + } + return q.String(), nil +} + +// ListEvents is the operation responsible for listing all the events +// associated with the activity between the node and the load balancer. The +// events report errors found with the node. The detailedMessage provides the +// detailed reason for the error. +func ListEvents(client *gophercloud.ServiceClient, loadBalancerID int, opts ListEventsOptsBuilder) pagination.Pager { + url := eventsURL(client, loadBalancerID) + + if opts != nil { + query, err := opts.ToEventsListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return NodeEventPage{pagination.SinglePageBase(r)} + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/nodes/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/nodes/requests_test.go new file mode 100644 index 0000000000..f888a14586 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/nodes/requests_test.go @@ -0,0 +1,212 @@ +package nodes + +import ( + "testing" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +const ( + lbID = 12345 + nodeID = 67890 + nodeID2 = 67891 +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockListResponse(t, lbID) + + count := 0 + + err := List(client.ServiceClient(), lbID, nil).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractNodes(page) + th.AssertNoErr(t, err) + + expected := []Node{ + Node{ + ID: 410, + Address: "10.1.1.1", + Port: 80, + Condition: ENABLED, + Status: ONLINE, + Weight: 3, + Type: PRIMARY, + }, + Node{ + ID: 411, + Address: "10.1.1.2", + Port: 80, + Condition: ENABLED, + Status: ONLINE, + Weight: 8, + Type: SECONDARY, + }, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + + th.AssertNoErr(t, err) + th.AssertEquals(t, 1, count) +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockCreateResponse(t, lbID) + + opts := CreateOpts{ + CreateOpt{ + Address: "10.2.2.3", + Port: 80, + Condition: ENABLED, + Type: PRIMARY, + }, + CreateOpt{ + Address: "10.2.2.4", + Port: 81, + Condition: ENABLED, + Type: SECONDARY, + }, + } + + page := Create(client.ServiceClient(), lbID, opts) + + actual, err := page.ExtractNodes() + th.AssertNoErr(t, err) + + expected := []Node{ + Node{ + ID: 185, + Address: "10.2.2.3", + Port: 80, + Condition: ENABLED, + Status: ONLINE, + Weight: 1, + Type: PRIMARY, + }, + Node{ + ID: 186, + Address: "10.2.2.4", + Port: 81, + Condition: ENABLED, + Status: ONLINE, + Weight: 1, + Type: SECONDARY, + }, + } + + th.CheckDeepEquals(t, expected, actual) +} + +func TestBulkDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + ids := []int{nodeID, nodeID2} + + mockBatchDeleteResponse(t, lbID, ids) + + err := BulkDelete(client.ServiceClient(), lbID, ids).ExtractErr() + th.AssertNoErr(t, err) +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockGetResponse(t, lbID, nodeID) + + node, err := Get(client.ServiceClient(), lbID, nodeID).Extract() + th.AssertNoErr(t, err) + + expected := &Node{ + ID: 410, + Address: "10.1.1.1", + Port: 80, + Condition: ENABLED, + Status: ONLINE, + Weight: 12, + Type: PRIMARY, + } + + th.AssertDeepEquals(t, expected, node) +} + +func TestUpdate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockUpdateResponse(t, lbID, nodeID) + + opts := UpdateOpts{ + Address: "1.2.3.4", + Weight: gophercloud.IntToPointer(10), + Condition: DRAINING, + Type: SECONDARY, + } + + err := Update(client.ServiceClient(), lbID, nodeID, opts).ExtractErr() + th.AssertNoErr(t, err) +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockDeleteResponse(t, lbID, nodeID) + + err := Delete(client.ServiceClient(), lbID, nodeID).ExtractErr() + th.AssertNoErr(t, err) +} + +func TestListEvents(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockListEventsResponse(t, lbID) + + count := 0 + + pager := ListEvents(client.ServiceClient(), lbID, ListEventsOpts{}) + + err := pager.EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractNodeEvents(page) + th.AssertNoErr(t, err) + + expected := []NodeEvent{ + NodeEvent{ + DetailedMessage: "Node is ok", + NodeID: 373, + ID: 7, + Type: "UPDATE_NODE", + Description: "Node '373' status changed to 'ONLINE' for load balancer '323'", + Category: "UPDATE", + Severity: "INFO", + RelativeURI: "/406271/loadbalancers/323/nodes/373/events", + AccountID: 406271, + LoadBalancerID: 323, + Title: "Node Status Updated", + Author: "Rackspace Cloud", + Created: "10-30-2012 10:18:23", + }, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + + th.AssertNoErr(t, err) + th.AssertEquals(t, 1, count) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/nodes/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/nodes/results.go new file mode 100644 index 0000000000..916485f2fc --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/nodes/results.go @@ -0,0 +1,210 @@ +package nodes + +import ( + "github.com/mitchellh/mapstructure" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// Node represents a back-end device, usually a virtual machine, that can +// handle traffic. It is assigned traffic based on its parent load balancer. +type Node struct { + // The IP address or CIDR for this back-end node. + Address string + + // The unique ID for this node. + ID int + + // The port on which traffic is sent and received. + Port int + + // The node's status. + Status Status + + // The node's condition. + Condition Condition + + // The priority at which this node will receive traffic if a weighted + // algorithm is used by its parent load balancer. Ranges from 1 to 100. + Weight int + + // Type of node. + Type Type +} + +// Type indicates whether the node is of a PRIMARY or SECONDARY nature. +type Type string + +const ( + // PRIMARY nodes are in the normal rotation to receive traffic from the load + // balancer. + PRIMARY Type = "PRIMARY" + + // SECONDARY nodes are only in the rotation to receive traffic from the load + // balancer when all the primary nodes fail. This provides a failover feature + // that automatically routes traffic to the secondary node in the event that + // the primary node is disabled or in a failing state. Note that active + // health monitoring must be enabled on the load balancer to enable the + // failover feature to the secondary node. + SECONDARY Type = "SECONDARY" +) + +// Condition represents the condition of a node. +type Condition string + +const ( + // ENABLED indicates that the node is permitted to accept new connections. + ENABLED Condition = "ENABLED" + + // DISABLED indicates that the node is not permitted to accept any new + // connections regardless of session persistence configuration. Existing + // connections are forcibly terminated. + DISABLED Condition = "DISABLED" + + // DRAINING indicates that the node is allowed to service existing + // established connections and connections that are being directed to it as a + // result of the session persistence configuration. + DRAINING Condition = "DRAINING" +) + +// Status indicates whether the node can accept service traffic. If a node is +// not listening on its port or does not meet the conditions of the defined +// active health check for the load balancer, then the load balancer does not +// forward connections, and its status is listed as OFFLINE. +type Status string + +const ( + // ONLINE indicates that the node is healthy and capable of receiving traffic + // from the load balancer. + ONLINE Status = "ONLINE" + + // OFFLINE indicates that the node is not in a position to receive service + // traffic. It is usually switched into this state when a health check is not + // satisfied with the node's response time. + OFFLINE Status = "OFFLINE" +) + +// NodePage is the page returned by a pager when traversing over a collection +// of nodes. +type NodePage struct { + pagination.SinglePageBase +} + +// IsEmpty checks whether a NodePage struct is empty. +func (p NodePage) IsEmpty() (bool, error) { + is, err := ExtractNodes(p) + if err != nil { + return true, nil + } + return len(is) == 0, nil +} + +func commonExtractNodes(body interface{}) ([]Node, error) { + var resp struct { + Nodes []Node `mapstructure:"nodes" json:"nodes"` + } + + err := mapstructure.Decode(body, &resp) + + return resp.Nodes, err +} + +// ExtractNodes accepts a Page struct, specifically a NodePage struct, and +// extracts the elements into a slice of Node structs. In other words, a +// generic collection is mapped into a relevant slice. +func ExtractNodes(page pagination.Page) ([]Node, error) { + return commonExtractNodes(page.(NodePage).Body) +} + +// CreateResult represents the result of a create operation. Since multiple +// nodes can be added in one operation, this result represents multiple nodes +// and should be treated as a typical pagination Page. Use its ExtractNodes +// method to get out a slice of Node structs. +type CreateResult struct { + pagination.SinglePageBase +} + +// ExtractNodes extracts a slice of Node structs from a CreateResult. +func (res CreateResult) ExtractNodes() ([]Node, error) { + return commonExtractNodes(res.Body) +} + +// DeleteResult represents the result of a delete operation. +type DeleteResult struct { + gophercloud.ErrResult +} + +type commonResult struct { + gophercloud.Result +} + +// GetResult represents the result of a get operation. +type GetResult struct { + commonResult +} + +// UpdateResult represents the result of an update operation. +type UpdateResult struct { + gophercloud.ErrResult +} + +func (r commonResult) Extract() (*Node, error) { + if r.Err != nil { + return nil, r.Err + } + + var response struct { + Node Node `mapstructure:"node"` + } + + err := mapstructure.Decode(r.Body, &response) + + return &response.Node, err +} + +// NodeEvent represents a service event that occurred between a node and a +// load balancer. +type NodeEvent struct { + ID int + DetailedMessage string + NodeID int + Type string + Description string + Category string + Severity string + RelativeURI string + AccountID int + LoadBalancerID int + Title string + Author string + Created string +} + +// NodeEventPage is a concrete type which embeds the common SinglePageBase +// struct, and is used when traversing node event collections. +type NodeEventPage struct { + pagination.SinglePageBase +} + +// IsEmpty is a concrete function which indicates whether an NodeEventPage is +// empty or not. +func (r NodeEventPage) IsEmpty() (bool, error) { + is, err := ExtractNodeEvents(r) + if err != nil { + return true, err + } + return len(is) == 0, nil +} + +// ExtractNodeEvents accepts a Page struct, specifically a NodeEventPage +// struct, and extracts the elements into a slice of NodeEvent structs. In +// other words, the collection is mapped into a relevant slice. +func ExtractNodeEvents(page pagination.Page) ([]NodeEvent, error) { + var resp struct { + Events []NodeEvent `mapstructure:"nodeServiceEvents" json:"nodeServiceEvents"` + } + + err := mapstructure.Decode(page.(NodeEventPage).Body, &resp) + + return resp.Events, err +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/nodes/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/nodes/urls.go new file mode 100644 index 0000000000..2cefee2644 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/nodes/urls.go @@ -0,0 +1,25 @@ +package nodes + +import ( + "strconv" + + "github.com/rackspace/gophercloud" +) + +const ( + lbPath = "loadbalancers" + nodePath = "nodes" + eventPath = "events" +) + +func resourceURL(c *gophercloud.ServiceClient, lbID, nodeID int) string { + return c.ServiceURL(lbPath, strconv.Itoa(lbID), nodePath, strconv.Itoa(nodeID)) +} + +func rootURL(c *gophercloud.ServiceClient, lbID int) string { + return c.ServiceURL(lbPath, strconv.Itoa(lbID), nodePath) +} + +func eventsURL(c *gophercloud.ServiceClient, lbID int) string { + return c.ServiceURL(lbPath, strconv.Itoa(lbID), nodePath, eventPath) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/sessions/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/sessions/doc.go new file mode 100644 index 0000000000..dcec0a87e2 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/sessions/doc.go @@ -0,0 +1,30 @@ +/* +Package sessions provides information and interaction with the Session +Persistence feature of the Rackspace Cloud Load Balancer service. + +Session persistence is a feature of the load balancing service that forces +multiple requests from clients (of the same protocol) to be directed to the +same node. This is common with many web applications that do not inherently +share application state between back-end servers. + +There are two modes to choose from: HTTP_COOKIE and SOURCE_IP. You can only set +one of the session persistence modes on a load balancer, and it can only +support one protocol. If you set HTTP_COOKIE mode for an HTTP load balancer, it +supports session persistence for HTTP requests only. Likewise, if you set +SOURCE_IP mode for an HTTPS load balancer, it supports session persistence for +only HTTPS requests. + +To support session persistence for both HTTP and HTTPS requests concurrently, +choose one of the following options: + +- Use two load balancers, one configured for session persistence for HTTP +requests and the other configured for session persistence for HTTPS requests. +That way, the load balancers support session persistence for both HTTP and +HTTPS requests concurrently, with each load balancer supporting one of the +protocols. + +- Use one load balancer, configure it for session persistence for HTTP requests, +and then enable SSL termination for that load balancer. The load balancer +supports session persistence for both HTTP and HTTPS requests concurrently. +*/ +package sessions diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/sessions/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/sessions/fixtures.go new file mode 100644 index 0000000000..9596819d16 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/sessions/fixtures.go @@ -0,0 +1,58 @@ +package sessions + +import ( + "fmt" + "net/http" + "strconv" + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +func _rootURL(id int) string { + return "/loadbalancers/" + strconv.Itoa(id) + "/sessionpersistence" +} + +func mockGetResponse(t *testing.T, lbID int) { + th.Mux.HandleFunc(_rootURL(lbID), func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "sessionPersistence": { + "persistenceType": "HTTP_COOKIE" + } +} +`) + }) +} + +func mockEnableResponse(t *testing.T, lbID int) { + th.Mux.HandleFunc(_rootURL(lbID), func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + th.TestJSONRequest(t, r, ` +{ + "sessionPersistence": { + "persistenceType": "HTTP_COOKIE" + } +} + `) + + w.WriteHeader(http.StatusAccepted) + }) +} + +func mockDisableResponse(t *testing.T, lbID int) { + th.Mux.HandleFunc(_rootURL(lbID), func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusAccepted) + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/sessions/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/sessions/requests.go new file mode 100644 index 0000000000..9853ad1320 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/sessions/requests.go @@ -0,0 +1,82 @@ +package sessions + +import ( + "errors" + + "github.com/racker/perigee" + + "github.com/rackspace/gophercloud" +) + +// CreateOptsBuilder is the interface options structs have to satisfy in order +// to be used in the main Create operation in this package. +type CreateOptsBuilder interface { + ToSPCreateMap() (map[string]interface{}, error) +} + +// CreateOpts is the common options struct used in this package's Create +// operation. +type CreateOpts struct { + // Required - can either be HTTPCOOKIE or SOURCEIP + Type Type +} + +// ToSPCreateMap casts a CreateOpts struct to a map. +func (opts CreateOpts) ToSPCreateMap() (map[string]interface{}, error) { + sp := make(map[string]interface{}) + + if opts.Type == "" { + return sp, errors.New("Type is a required field") + } + + sp["persistenceType"] = opts.Type + return map[string]interface{}{"sessionPersistence": sp}, nil +} + +// Enable is the operation responsible for enabling session persistence for a +// particular load balancer. +func Enable(c *gophercloud.ServiceClient, lbID int, opts CreateOptsBuilder) EnableResult { + var res EnableResult + + reqBody, err := opts.ToSPCreateMap() + if err != nil { + res.Err = err + return res + } + + _, res.Err = perigee.Request("PUT", rootURL(c, lbID), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + ReqBody: &reqBody, + Results: &res.Body, + OkCodes: []int{202}, + }) + + return res +} + +// Get is the operation responsible for showing details of the session +// persistence configuration for a particular load balancer. +func Get(c *gophercloud.ServiceClient, lbID int) GetResult { + var res GetResult + + _, res.Err = perigee.Request("GET", rootURL(c, lbID), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + Results: &res.Body, + OkCodes: []int{200}, + }) + + return res +} + +// Disable is the operation responsible for disabling session persistence for a +// particular load balancer. +func Disable(c *gophercloud.ServiceClient, lbID int) DisableResult { + var res DisableResult + + _, res.Err = perigee.Request("DELETE", rootURL(c, lbID), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + OkCodes: []int{202}, + }) + + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/sessions/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/sessions/requests_test.go new file mode 100644 index 0000000000..f319e540bb --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/sessions/requests_test.go @@ -0,0 +1,44 @@ +package sessions + +import ( + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +const lbID = 12345 + +func TestEnable(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockEnableResponse(t, lbID) + + opts := CreateOpts{Type: HTTPCOOKIE} + err := Enable(client.ServiceClient(), lbID, opts).ExtractErr() + th.AssertNoErr(t, err) +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockGetResponse(t, lbID) + + sp, err := Get(client.ServiceClient(), lbID).Extract() + th.AssertNoErr(t, err) + + expected := &SessionPersistence{Type: HTTPCOOKIE} + th.AssertDeepEquals(t, expected, sp) +} + +func TestDisable(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockDisableResponse(t, lbID) + + err := Disable(client.ServiceClient(), lbID).ExtractErr() + th.AssertNoErr(t, err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/sessions/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/sessions/results.go new file mode 100644 index 0000000000..fe90e722cb --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/sessions/results.go @@ -0,0 +1,58 @@ +package sessions + +import ( + "github.com/mitchellh/mapstructure" + + "github.com/rackspace/gophercloud" +) + +// Type represents the type of session persistence being used. +type Type string + +const ( + // HTTPCOOKIE is a session persistence mechanism that inserts an HTTP cookie + // and is used to determine the destination back-end node. This is supported + // for HTTP load balancing only. + HTTPCOOKIE Type = "HTTP_COOKIE" + + // SOURCEIP is a session persistence mechanism that keeps track of the source + // IP address that is mapped and is able to determine the destination + // back-end node. This is supported for HTTPS pass-through and non-HTTP load + // balancing only. + SOURCEIP Type = "SOURCE_IP" +) + +// SessionPersistence indicates how a load balancer is using session persistence +type SessionPersistence struct { + Type Type `mapstructure:"persistenceType"` +} + +// EnableResult represents the result of an enable operation. +type EnableResult struct { + gophercloud.ErrResult +} + +// DisableResult represents the result of a disable operation. +type DisableResult struct { + gophercloud.ErrResult +} + +// GetResult represents the result of a get operation. +type GetResult struct { + gophercloud.Result +} + +// Extract interprets a GetResult as an SP, if possible. +func (r GetResult) Extract() (*SessionPersistence, error) { + if r.Err != nil { + return nil, r.Err + } + + var response struct { + SP SessionPersistence `mapstructure:"sessionPersistence"` + } + + err := mapstructure.Decode(r.Body, &response) + + return &response.SP, err +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/sessions/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/sessions/urls.go new file mode 100644 index 0000000000..c4a896d905 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/sessions/urls.go @@ -0,0 +1,16 @@ +package sessions + +import ( + "strconv" + + "github.com/rackspace/gophercloud" +) + +const ( + path = "loadbalancers" + spPath = "sessionpersistence" +) + +func rootURL(c *gophercloud.ServiceClient, id int) string { + return c.ServiceURL(path, strconv.Itoa(id), spPath) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/ssl/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/ssl/doc.go new file mode 100644 index 0000000000..6a2c174ae9 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/ssl/doc.go @@ -0,0 +1,22 @@ +/* +Package ssl provides information and interaction with the SSL Termination +feature of the Rackspace Cloud Load Balancer service. + +You may only enable and configure SSL termination on load balancers with +non-secure protocols, such as HTTP, but not HTTPS. + +SSL-terminated load balancers decrypt the traffic at the traffic manager and +pass unencrypted traffic to the back-end node. Because of this, the customer's +back-end nodes don't know what protocol the client requested. For this reason, +the X-Forwarded-Proto (XFP) header has been added for identifying the +originating protocol of an HTTP request as "http" or "https" depending on what +protocol the client requested. + +Not every service returns certificates in the proper order. Please verify that +your chain of certificates matches that of walking up the chain from the domain +to the CA root. + +If used for HTTP to HTTPS redirection, the LoadBalancer's securePort attribute +must be set to 443, and its secureTrafficOnly attribute must be true. +*/ +package ssl diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/ssl/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/ssl/fixtures.go new file mode 100644 index 0000000000..1d40100125 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/ssl/fixtures.go @@ -0,0 +1,195 @@ +package ssl + +import ( + "fmt" + "net/http" + "strconv" + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +func _rootURL(id int) string { + return "/loadbalancers/" + strconv.Itoa(id) + "/ssltermination" +} + +func _certURL(id, certID int) string { + url := _rootURL(id) + "/certificatemappings" + if certID > 0 { + url += "/" + strconv.Itoa(certID) + } + return url +} + +func mockGetResponse(t *testing.T, lbID int) { + th.Mux.HandleFunc(_rootURL(lbID), func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "sslTermination": { + "certificate": "-----BEGIN CERTIFICATE-----\nMIIEXTCCA0WgAwIBAgIGATTEAjK3MA0GCSqGSIb3DQEBBQUAMIGDMRkwFwYDVQQD\nExBUZXN0IENBIFNUdWIgS2V5MRcwFQYDVQQLEw5QbGF0Zm9ybSBMYmFhczEaMBgG\nA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFDASBgNVBAcTC1NhbiBBbnRvbmlvMQ4w\nDAYDVQQIEwVUZXhhczELMAkGA1UEBhMCVVMwHhcNMTIwMTA5MTk0NjQ1WhcNMTQw\nMTA4MTk0NjQ1WjCBgjELMAkGA1UEBhMCVVMxDjAMBgNVBAgTBVRleGFzMRQwEgYD\nVQQHEwtTYW4gQW50b25pbzEaMBgGA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFzAV\nBgNVBAsTDlBsYXRmb3JtIExiYWFzMRgwFgYDVQQDEw9UZXN0IENsaWVudCBLZXkw\nggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDAi51IylFnHtNLT8C0NVfc\nOBfAsP2D5es1qhrOWHCGlgAuDMksBsCc7FPo5PSBOmQ+6z8HtCFbrLoC5/Zx0F5b\nfVegjA+xKjI2HGASsYHHM0BFEH2UjUcJrWiMWtxQuW6Phbqulo7JwjmygMEmIkeK\nf+FtkE9mrq+E8K40/thrjxl3/ZcJD1+3dcp+ZuzVJ2t1E4iGKCx79IZFsysKiuf+\n+E0i6iGvvI6UcbcZxVxQj2TplJkFuoX5kDgClIX9Dr9y6aJ4SCh+GRhvHl+DTaz0\nnCvghachHZtIeztRDqxWApjOOzs93dSelrviMXDr8fqyEAGg7YIhgui0aZBsWCen\nAgMBAAGjgdUwgdIwgbAGA1UdIwSBqDCBpYAUNpx1Pc6cGA7KqEwHMmHBTZMA7lSh\ngYmkgYYwgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVU4IBATAd\nBgNVHQ4EFgQULueOfsjZZOHwJHZwBy6u0swnpccwDQYJKoZIhvcNAQEFBQADggEB\nAFNuqSVUaotUJoWDv4z7Kbi6JFpTjDht5ORw4BdVYlRD4h9DACAFzPrPV2ym/Osp\nhNMdZq6msZku7MdOSQVhdeGWrSNk3M8O9Hg7cVzPNXOF3iNoo3irQ5tURut44xs4\nWw5YWQqS9WyUY5snD8tm7Y1rQTPfhg+678xIq/zWCv/u+FSnfVv1nlhLVQkEeG/Y\ngh1uMaTIpUKTGEjIAGtpGP7wwIcXptR/HyfzhTUSTaWc1Ef7zoKT9LL5z3IV1hC2\njVWz+RwYs98LjMuksJFoHqRfWyYhCIym0jb6GTwaEmpxAjc+d7OLNQdnoEGoUYGP\nYjtfkRYg265ESMA+Kww4Xy8=\n-----END CERTIFICATE-----\n", + "enabled": true, + "secureTrafficOnly": false, + "intermediateCertificate": "-----BEGIN CERTIFICATE-----\nMIIDtTCCAp2gAwIBAgIBATANBgkqhkiG9w0BAQUFADCBgzEZMBcGA1UEAxMQVGVz\ndCBDQSBTVHViIEtleTEXMBUGA1UECxMOUGxhdGZvcm0gTGJhYXMxGjAYBgNVBAoT\nEVJhY2tzcGFjZSBIb3N0aW5nMRQwEgYDVQQHEwtTYW4gQW50b25pbzEOMAwGA1UE\nCBMFVGV4YXMxCzAJBgNVBAYTAlVTMB4XDTEyMDEwOTE5NDU0OVoXDTE0MDEwODE5\nNDU0OVowgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVUzCCASIw\nDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANNh55lwTVwQvNoEZjq1zGdYz9jA\nXXdjizn8AJhjHLOAallfPtvCfTEgKanhdoyz5FnhQE8HbDAop/KNS1lN2UMvdl5f\nZNLTSjJrNtedqxQwxN/i3bpyBxNVejUH2NjV1mmyj+5CJYwCzWalvI/gLPq/A3as\nO2EQqtf3U8unRgn0zXLRdYxV9MrUzNAmdipPNvNrsVdrCgA42rgF/8KsyRVQfJCX\nfN7PGCfrsC3YaUvhymraWxNnXIzMYTNa9wEeBZLUw8SlEtpa1Zsvui+TPXu3USNZ\nVnWH8Lb6ENlnoX0VBwo62fjOG3JzhNKoJawi3bRqyDdINOvafr7iPrrs/T8CAwEA\nAaMyMDAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUNpx1Pc6cGA7KqEwHMmHB\nTZMA7lQwDQYJKoZIhvcNAQEFBQADggEBAMoRgH3iTG3t317viLKoY+lNMHUgHuR7\nb3mn9MidJKyYVewe6hCDIN6WY4fUojmMW9wFJWJIo0hRMNHL3n3tq8HP2j20Mxy8\nacPdfGZJa+jiBw72CrIGdobKaFduIlIEDBA1pNdZIJ+EulrtqrMesnIt92WaypIS\n8JycbIgDMCiyC0ENHEk8UWlC6429c7OZAsplMTbHME/1R4btxjkdfrYZJjdJ2yL2\n8cjZDUDMCPTdW/ycP07Gkq30RB5tACB5aZdaCn2YaKC8FsEdhff4X7xEOfOEHWEq\nSRxADDj8Lx1MT6QpR07hCiDyHfTCtbqzI0iGjX63Oh7xXSa0f+JVTa8=\n-----END CERTIFICATE-----\n", + "securePort": 443 + } +} +`) + }) +} + +func mockUpdateResponse(t *testing.T, lbID int) { + th.Mux.HandleFunc(_rootURL(lbID), func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + th.TestJSONRequest(t, r, ` +{ + "sslTermination": { + "enabled": true, + "securePort": 443, + "secureTrafficOnly": false, + "privateKey": "foo", + "certificate": "-----BEGIN CERTIFICATE-----\nMIIEXTCCA0WgAwIBAgIGATTEAjK3MA0GCSqGSIb3DQEBBQUAMIGDMRkwFwYDVQQD\nExBUZXN0IENBIFNUdWIgS2V5MRcwFQYDVQQLEw5QbGF0Zm9ybSBMYmFhczEaMBgG\nA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFDASBgNVBAcTC1NhbiBBbnRvbmlvMQ4w\nDAYDVQQIEwVUZXhhczELMAkGA1UEBhMCVVMwHhcNMTIwMTA5MTk0NjQ1WhcNMTQw\nMTA4MTk0NjQ1WjCBgjELMAkGA1UEBhMCVVMxDjAMBgNVBAgTBVRleGFzMRQwEgYD\nVQQHEwtTYW4gQW50b25pbzEaMBgGA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFzAV\nBgNVBAsTDlBsYXRmb3JtIExiYWFzMRgwFgYDVQQDEw9UZXN0IENsaWVudCBLZXkw\nggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDAi51IylFnHtNLT8C0NVfc\nOBfAsP2D5es1qhrOWHCGlgAuDMksBsCc7FPo5PSBOmQ+6z8HtCFbrLoC5/Zx0F5b\nfVegjA+xKjI2HGASsYHHM0BFEH2UjUcJrWiMWtxQuW6Phbqulo7JwjmygMEmIkeK\nf+FtkE9mrq+E8K40/thrjxl3/ZcJD1+3dcp+ZuzVJ2t1E4iGKCx79IZFsysKiuf+\n+E0i6iGvvI6UcbcZxVxQj2TplJkFuoX5kDgClIX9Dr9y6aJ4SCh+GRhvHl+DTaz0\nnCvghachHZtIeztRDqxWApjOOzs93dSelrviMXDr8fqyEAGg7YIhgui0aZBsWCen\nAgMBAAGjgdUwgdIwgbAGA1UdIwSBqDCBpYAUNpx1Pc6cGA7KqEwHMmHBTZMA7lSh\ngYmkgYYwgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVU4IBATAd\nBgNVHQ4EFgQULueOfsjZZOHwJHZwBy6u0swnpccwDQYJKoZIhvcNAQEFBQADggEB\nAFNuqSVUaotUJoWDv4z7Kbi6JFpTjDht5ORw4BdVYlRD4h9DACAFzPrPV2ym/Osp\nhNMdZq6msZku7MdOSQVhdeGWrSNk3M8O9Hg7cVzPNXOF3iNoo3irQ5tURut44xs4\nWw5YWQqS9WyUY5snD8tm7Y1rQTPfhg+678xIq/zWCv/u+FSnfVv1nlhLVQkEeG/Y\ngh1uMaTIpUKTGEjIAGtpGP7wwIcXptR/HyfzhTUSTaWc1Ef7zoKT9LL5z3IV1hC2\njVWz+RwYs98LjMuksJFoHqRfWyYhCIym0jb6GTwaEmpxAjc+d7OLNQdnoEGoUYGP\nYjtfkRYg265ESMA+Kww4Xy8=\n-----END CERTIFICATE-----\n", + "intermediateCertificate": "-----BEGIN CERTIFICATE-----\nMIIDtTCCAp2gAwIBAgIBATANBgkqhkiG9w0BAQUFADCBgzEZMBcGA1UEAxMQVGVz\ndCBDQSBTVHViIEtleTEXMBUGA1UECxMOUGxhdGZvcm0gTGJhYXMxGjAYBgNVBAoT\nEVJhY2tzcGFjZSBIb3N0aW5nMRQwEgYDVQQHEwtTYW4gQW50b25pbzEOMAwGA1UE\nCBMFVGV4YXMxCzAJBgNVBAYTAlVTMB4XDTEyMDEwOTE5NDU0OVoXDTE0MDEwODE5\nNDU0OVowgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVUzCCASIw\nDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANNh55lwTVwQvNoEZjq1zGdYz9jA\nXXdjizn8AJhjHLOAallfPtvCfTEgKanhdoyz5FnhQE8HbDAop/KNS1lN2UMvdl5f\nZNLTSjJrNtedqxQwxN/i3bpyBxNVejUH2NjV1mmyj+5CJYwCzWalvI/gLPq/A3as\nO2EQqtf3U8unRgn0zXLRdYxV9MrUzNAmdipPNvNrsVdrCgA42rgF/8KsyRVQfJCX\nfN7PGCfrsC3YaUvhymraWxNnXIzMYTNa9wEeBZLUw8SlEtpa1Zsvui+TPXu3USNZ\nVnWH8Lb6ENlnoX0VBwo62fjOG3JzhNKoJawi3bRqyDdINOvafr7iPrrs/T8CAwEA\nAaMyMDAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUNpx1Pc6cGA7KqEwHMmHB\nTZMA7lQwDQYJKoZIhvcNAQEFBQADggEBAMoRgH3iTG3t317viLKoY+lNMHUgHuR7\nb3mn9MidJKyYVewe6hCDIN6WY4fUojmMW9wFJWJIo0hRMNHL3n3tq8HP2j20Mxy8\nacPdfGZJa+jiBw72CrIGdobKaFduIlIEDBA1pNdZIJ+EulrtqrMesnIt92WaypIS\n8JycbIgDMCiyC0ENHEk8UWlC6429c7OZAsplMTbHME/1R4btxjkdfrYZJjdJ2yL2\n8cjZDUDMCPTdW/ycP07Gkq30RB5tACB5aZdaCn2YaKC8FsEdhff4X7xEOfOEHWEq\nSRxADDj8Lx1MT6QpR07hCiDyHfTCtbqzI0iGjX63Oh7xXSa0f+JVTa8=\n-----END CERTIFICATE-----\n" + } +} + `) + + w.WriteHeader(http.StatusOK) + }) +} + +func mockDeleteResponse(t *testing.T, lbID int) { + th.Mux.HandleFunc(_rootURL(lbID), func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusOK) + }) +} + +func mockListCertsResponse(t *testing.T, lbID int) { + th.Mux.HandleFunc(_certURL(lbID, 0), func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "certificateMappings": [ + { + "certificateMapping": { + "id": 123, + "hostName": "rackspace.com" + } + }, + { + "certificateMapping": { + "id": 124, + "hostName": "*.rackspace.com" + } + } + ] +} +`) + }) +} + +func mockAddCertResponse(t *testing.T, lbID int) { + th.Mux.HandleFunc(_certURL(lbID, 0), func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + th.TestJSONRequest(t, r, ` +{ + "certificateMapping": { + "hostName": "rackspace.com", + "privateKey":"-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEAwIudSMpRZx7TS0/AtDVX3DgXwLD9g+XrNaoazlhwhpYALgzJ\nLAbAnOxT6OT0gTpkPus/B7QhW6y6Auf2cdBeW31XoIwPsSoyNhxgErGBxzNARRB9\nlI1HCa1ojFrcULluj4W6rpaOycI5soDBJiJHin/hbZBPZq6vhPCuNP7Ya48Zd/2X\nCQ9ft3XKfmbs1SdrdROIhigse/SGRbMrCorn/vhNIuohr7yOlHG3GcVcUI9k6ZSZ\nBbqF+ZA4ApSF/Q6/cumieEgofhkYbx5fg02s9Jwr4IWnIR2bSHs7UQ6sVgKYzjs7\nPd3Unpa74jFw6/H6shABoO2CIYLotGmQbFgnpwIDAQABAoIBAQCBCQ+PCIclJHNV\ntUzfeCA5ZR4F9JbxHdRTUnxEbOB8UWotckQfTScoAvj4yvdQ42DrCZxj/UOdvFOs\nPufZvlp91bIz1alugWjE+p8n5+2hIaegoTyHoWZKBfxak0myj5KYfHZvKlbmv1ML\nXV4TwEVRfAIG+v87QTY/UUxuF5vR+BpKIbgUJLfPUFFvJUdl84qsJ44pToxaYUd/\nh5YAGC00U4ay1KVSAUnTkkPNZ0lPG/rWU6w6WcTvNRLMd8DzFLTKLOgQfHhbExAF\n+sXPWjWSzbBRP1O7fHqq96QQh4VFiY/7w9W+sDKQyV6Ul17OSXs6aZ4f+lq4rJTI\n1FG96YiBAoGBAO1tiH0h1oWDBYfJB3KJJ6CQQsDGwtHo/DEgznFVP4XwEVbZ98Ha\nBfBCn3sAybbaikyCV1Hwj7kfHMZPDHbrcUSFX7quu/2zPK+wO3lZKXSyu4YsguSa\nRedInN33PpdnlPhLyQdWSuD5sVHJDF6xn22vlyxeILH3ooLg2WOFMPmVAoGBAM+b\nUG/a7iyfpAQKYyuFAsXz6SeFaDY+ZYeX45L112H8Pu+Ie/qzon+bzLB9FIH8GP6+\nQpQgmm/p37U2gD1zChUv7iW6OfQBKk9rWvMpfRF6d7YHquElejhizfTZ+ntBV/VY\ndOYEczxhrdW7keLpatYaaWUy/VboRZmlz/9JGqVLAoGAHfqNmFc0cgk4IowEj7a3\ntTNh6ltub/i+FynwRykfazcDyXaeLPDtfQe8gVh5H8h6W+y9P9BjJVnDVVrX1RAn\nbiJ1EupLPF5sVDapW8ohTOXgfbGTGXBNUUW+4Nv+IDno+mz/RhjkPYHpnM0I7c/5\ntGzOZsC/2hjNgT8I0+MWav0CgYEAuULdJeQVlKalI6HtW2Gn1uRRVJ49H+LQkY6e\nW3+cw2jo9LI0CMWSphNvNrN3wIMp/vHj0fHCP0pSApDvIWbuQXfzKaGko7UCf7rK\nf6GvZRCHkV4IREBAb97j8bMvThxClMNqFfU0rFZyXP+0MOyhFQyertswrgQ6T+Fi\n2mnvKD8CgYAmJHP3NTDRMoMRyAzonJ6nEaGUbAgNmivTaUWMe0+leCvAdwD89gzC\nTKbm3eDUg/6Va3X6ANh3wsfIOe4RXXxcbcFDk9R4zO2M5gfLSjYm5Q87EBZ2hrdj\nM2gLI7dt6thx0J8lR8xRHBEMrVBdgwp0g1gQzo5dAV88/kpkZVps8Q==\n-----END RSA PRIVATE KEY-----\n", + "certificate":"-----BEGIN CERTIFICATE-----\nMIIEXTCCA0WgAwIBAgIGATTEAjK3MA0GCSqGSIb3DQEBBQUAMIGDMRkwFwYDVQQD\nExBUZXN0IENBIFNUdWIgS2V5MRcwFQYDVQQLEw5QbGF0Zm9ybSBMYmFhczEaMBgG\nA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFDASBgNVBAcTC1NhbiBBbnRvbmlvMQ4w\nDAYDVQQIEwVUZXhhczELMAkGA1UEBhMCVVMwHhcNMTIwMTA5MTk0NjQ1WhcNMTQw\nMTA4MTk0NjQ1WjCBgjELMAkGA1UEBhMCVVMxDjAMBgNVBAgTBVRleGFzMRQwEgYD\nVQQHEwtTYW4gQW50b25pbzEaMBgGA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFzAV\nBgNVBAsTDlBsYXRmb3JtIExiYWFzMRgwFgYDVQQDEw9UZXN0IENsaWVudCBLZXkw\nggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDAi51IylFnHtNLT8C0NVfc\nOBfAsP2D5es1qhrOWHCGlgAuDMksBsCc7FPo5PSBOmQ+6z8HtCFbrLoC5/Zx0F5b\nfVegjA+xKjI2HGASsYHHM0BFEH2UjUcJrWiMWtxQuW6Phbqulo7JwjmygMEmIkeK\nf+FtkE9mrq+E8K40/thrjxl3/ZcJD1+3dcp+ZuzVJ2t1E4iGKCx79IZFsysKiuf+\n+E0i6iGvvI6UcbcZxVxQj2TplJkFuoX5kDgClIX9Dr9y6aJ4SCh+GRhvHl+DTaz0\nnCvghachHZtIeztRDqxWApjOOzs93dSelrviMXDr8fqyEAGg7YIhgui0aZBsWCen\nAgMBAAGjgdUwgdIwgbAGA1UdIwSBqDCBpYAUNpx1Pc6cGA7KqEwHMmHBTZMA7lSh\ngYmkgYYwgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVU4IBATAd\nBgNVHQ4EFgQULueOfsjZZOHwJHZwBy6u0swnpccwDQYJKoZIhvcNAQEFBQADggEB\nAFNuqSVUaotUJoWDv4z7Kbi6JFpTjDht5ORw4BdVYlRD4h9DACAFzPrPV2ym/Osp\nhNMdZq6msZku7MdOSQVhdeGWrSNk3M8O9Hg7cVzPNXOF3iNoo3irQ5tURut44xs4\nWw5YWQqS9WyUY5snD8tm7Y1rQTPfhg+678xIq/zWCv/u+FSnfVv1nlhLVQkEeG/Y\ngh1uMaTIpUKTGEjIAGtpGP7wwIcXptR/HyfzhTUSTaWc1Ef7zoKT9LL5z3IV1hC2\njVWz+RwYs98LjMuksJFoHqRfWyYhCIym0jb6GTwaEmpxAjc+d7OLNQdnoEGoUYGP\nYjtfkRYg265ESMA+Kww4Xy8=\n-----END CERTIFICATE-----\n", + "intermediateCertificate":"-----BEGIN CERTIFICATE-----\nMIIDtTCCAp2gAwIBAgIBATANBgkqhkiG9w0BAQUFADCBgzEZMBcGA1UEAxMQVGVz\ndCBDQSBTVHViIEtleTEXMBUGA1UECxMOUGxhdGZvcm0gTGJhYXMxGjAYBgNVBAoT\nEVJhY2tzcGFjZSBIb3N0aW5nMRQwEgYDVQQHEwtTYW4gQW50b25pbzEOMAwGA1UE\nCBMFVGV4YXMxCzAJBgNVBAYTAlVTMB4XDTEyMDEwOTE5NDU0OVoXDTE0MDEwODE5\nNDU0OVowgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVUzCCASIw\nDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANNh55lwTVwQvNoEZjq1zGdYz9jA\nXXdjizn8AJhjHLOAallfPtvCfTEgKanhdoyz5FnhQE8HbDAop/KNS1lN2UMvdl5f\nZNLTSjJrNtedqxQwxN/i3bpyBxNVejUH2NjV1mmyj+5CJYwCzWalvI/gLPq/A3as\nO2EQqtf3U8unRgn0zXLRdYxV9MrUzNAmdipPNvNrsVdrCgA42rgF/8KsyRVQfJCX\nfN7PGCfrsC3YaUvhymraWxNnXIzMYTNa9wEeBZLUw8SlEtpa1Zsvui+TPXu3USNZ\nVnWH8Lb6ENlnoX0VBwo62fjOG3JzhNKoJawi3bRqyDdINOvafr7iPrrs/T8CAwEA\nAaMyMDAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUNpx1Pc6cGA7KqEwHMmHB\nTZMA7lQwDQYJKoZIhvcNAQEFBQADggEBAMoRgH3iTG3t317viLKoY+lNMHUgHuR7\nb3mn9MidJKyYVewe6hCDIN6WY4fUojmMW9wFJWJIo0hRMNHL3n3tq8HP2j20Mxy8\nacPdfGZJa+jiBw72CrIGdobKaFduIlIEDBA1pNdZIJ+EulrtqrMesnIt92WaypIS\n8JycbIgDMCiyC0ENHEk8UWlC6429c7OZAsplMTbHME/1R4btxjkdfrYZJjdJ2yL2\n8cjZDUDMCPTdW/ycP07Gkq30RB5tACB5aZdaCn2YaKC8FsEdhff4X7xEOfOEHWEq\nSRxADDj8Lx1MT6QpR07hCiDyHfTCtbqzI0iGjX63Oh7xXSa0f+JVTa8=\n-----END CERTIFICATE-----\n" + } +} + `) + + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "certificateMapping": { + "id": 123, + "hostName": "rackspace.com", + "certificate":"-----BEGIN CERTIFICATE-----\nMIIEXTCCA0WgAwIBAgIGATTEAjK3MA0GCSqGSIb3DQEBBQUAMIGDMRkwFwYDVQQD\nExBUZXN0IENBIFNUdWIgS2V5MRcwFQYDVQQLEw5QbGF0Zm9ybSBMYmFhczEaMBgG\nA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFDASBgNVBAcTC1NhbiBBbnRvbmlvMQ4w\nDAYDVQQIEwVUZXhhczELMAkGA1UEBhMCVVMwHhcNMTIwMTA5MTk0NjQ1WhcNMTQw\nMTA4MTk0NjQ1WjCBgjELMAkGA1UEBhMCVVMxDjAMBgNVBAgTBVRleGFzMRQwEgYD\nVQQHEwtTYW4gQW50b25pbzEaMBgGA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFzAV\nBgNVBAsTDlBsYXRmb3JtIExiYWFzMRgwFgYDVQQDEw9UZXN0IENsaWVudCBLZXkw\nggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDAi51IylFnHtNLT8C0NVfc\nOBfAsP2D5es1qhrOWHCGlgAuDMksBsCc7FPo5PSBOmQ+6z8HtCFbrLoC5/Zx0F5b\nfVegjA+xKjI2HGASsYHHM0BFEH2UjUcJrWiMWtxQuW6Phbqulo7JwjmygMEmIkeK\nf+FtkE9mrq+E8K40/thrjxl3/ZcJD1+3dcp+ZuzVJ2t1E4iGKCx79IZFsysKiuf+\n+E0i6iGvvI6UcbcZxVxQj2TplJkFuoX5kDgClIX9Dr9y6aJ4SCh+GRhvHl+DTaz0\nnCvghachHZtIeztRDqxWApjOOzs93dSelrviMXDr8fqyEAGg7YIhgui0aZBsWCen\nAgMBAAGjgdUwgdIwgbAGA1UdIwSBqDCBpYAUNpx1Pc6cGA7KqEwHMmHBTZMA7lSh\ngYmkgYYwgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVU4IBATAd\nBgNVHQ4EFgQULueOfsjZZOHwJHZwBy6u0swnpccwDQYJKoZIhvcNAQEFBQADggEB\nAFNuqSVUaotUJoWDv4z7Kbi6JFpTjDht5ORw4BdVYlRD4h9DACAFzPrPV2ym/Osp\nhNMdZq6msZku7MdOSQVhdeGWrSNk3M8O9Hg7cVzPNXOF3iNoo3irQ5tURut44xs4\nWw5YWQqS9WyUY5snD8tm7Y1rQTPfhg+678xIq/zWCv/u+FSnfVv1nlhLVQkEeG/Y\ngh1uMaTIpUKTGEjIAGtpGP7wwIcXptR/HyfzhTUSTaWc1Ef7zoKT9LL5z3IV1hC2\njVWz+RwYs98LjMuksJFoHqRfWyYhCIym0jb6GTwaEmpxAjc+d7OLNQdnoEGoUYGP\nYjtfkRYg265ESMA+Kww4Xy8=\n-----END CERTIFICATE-----\n", + "intermediateCertificate":"-----BEGIN CERTIFICATE-----\nMIIDtTCCAp2gAwIBAgIBATANBgkqhkiG9w0BAQUFADCBgzEZMBcGA1UEAxMQVGVz\ndCBDQSBTVHViIEtleTEXMBUGA1UECxMOUGxhdGZvcm0gTGJhYXMxGjAYBgNVBAoT\nEVJhY2tzcGFjZSBIb3N0aW5nMRQwEgYDVQQHEwtTYW4gQW50b25pbzEOMAwGA1UE\nCBMFVGV4YXMxCzAJBgNVBAYTAlVTMB4XDTEyMDEwOTE5NDU0OVoXDTE0MDEwODE5\nNDU0OVowgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVUzCCASIw\nDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANNh55lwTVwQvNoEZjq1zGdYz9jA\nXXdjizn8AJhjHLOAallfPtvCfTEgKanhdoyz5FnhQE8HbDAop/KNS1lN2UMvdl5f\nZNLTSjJrNtedqxQwxN/i3bpyBxNVejUH2NjV1mmyj+5CJYwCzWalvI/gLPq/A3as\nO2EQqtf3U8unRgn0zXLRdYxV9MrUzNAmdipPNvNrsVdrCgA42rgF/8KsyRVQfJCX\nfN7PGCfrsC3YaUvhymraWxNnXIzMYTNa9wEeBZLUw8SlEtpa1Zsvui+TPXu3USNZ\nVnWH8Lb6ENlnoX0VBwo62fjOG3JzhNKoJawi3bRqyDdINOvafr7iPrrs/T8CAwEA\nAaMyMDAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUNpx1Pc6cGA7KqEwHMmHB\nTZMA7lQwDQYJKoZIhvcNAQEFBQADggEBAMoRgH3iTG3t317viLKoY+lNMHUgHuR7\nb3mn9MidJKyYVewe6hCDIN6WY4fUojmMW9wFJWJIo0hRMNHL3n3tq8HP2j20Mxy8\nacPdfGZJa+jiBw72CrIGdobKaFduIlIEDBA1pNdZIJ+EulrtqrMesnIt92WaypIS\n8JycbIgDMCiyC0ENHEk8UWlC6429c7OZAsplMTbHME/1R4btxjkdfrYZJjdJ2yL2\n8cjZDUDMCPTdW/ycP07Gkq30RB5tACB5aZdaCn2YaKC8FsEdhff4X7xEOfOEHWEq\nSRxADDj8Lx1MT6QpR07hCiDyHfTCtbqzI0iGjX63Oh7xXSa0f+JVTa8=\n-----END CERTIFICATE-----\n" + } +} + `) + }) +} + +func mockGetCertResponse(t *testing.T, lbID, certID int) { + th.Mux.HandleFunc(_certURL(lbID, certID), func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "certificateMapping": { + "id": 123, + "hostName": "rackspace.com", + "certificate":"-----BEGIN CERTIFICATE-----\nMIIEXTCCA0WgAwIBAgIGATTEAjK3MA0GCSqGSIb3DQEBBQUAMIGDMRkwFwYDVQQD\nExBUZXN0IENBIFNUdWIgS2V5MRcwFQYDVQQLEw5QbGF0Zm9ybSBMYmFhczEaMBgG\nA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFDASBgNVBAcTC1NhbiBBbnRvbmlvMQ4w\nDAYDVQQIEwVUZXhhczELMAkGA1UEBhMCVVMwHhcNMTIwMTA5MTk0NjQ1WhcNMTQw\nMTA4MTk0NjQ1WjCBgjELMAkGA1UEBhMCVVMxDjAMBgNVBAgTBVRleGFzMRQwEgYD\nVQQHEwtTYW4gQW50b25pbzEaMBgGA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFzAV\nBgNVBAsTDlBsYXRmb3JtIExiYWFzMRgwFgYDVQQDEw9UZXN0IENsaWVudCBLZXkw\nggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDAi51IylFnHtNLT8C0NVfc\nOBfAsP2D5es1qhrOWHCGlgAuDMksBsCc7FPo5PSBOmQ+6z8HtCFbrLoC5/Zx0F5b\nfVegjA+xKjI2HGASsYHHM0BFEH2UjUcJrWiMWtxQuW6Phbqulo7JwjmygMEmIkeK\nf+FtkE9mrq+E8K40/thrjxl3/ZcJD1+3dcp+ZuzVJ2t1E4iGKCx79IZFsysKiuf+\n+E0i6iGvvI6UcbcZxVxQj2TplJkFuoX5kDgClIX9Dr9y6aJ4SCh+GRhvHl+DTaz0\nnCvghachHZtIeztRDqxWApjOOzs93dSelrviMXDr8fqyEAGg7YIhgui0aZBsWCen\nAgMBAAGjgdUwgdIwgbAGA1UdIwSBqDCBpYAUNpx1Pc6cGA7KqEwHMmHBTZMA7lSh\ngYmkgYYwgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVU4IBATAd\nBgNVHQ4EFgQULueOfsjZZOHwJHZwBy6u0swnpccwDQYJKoZIhvcNAQEFBQADggEB\nAFNuqSVUaotUJoWDv4z7Kbi6JFpTjDht5ORw4BdVYlRD4h9DACAFzPrPV2ym/Osp\nhNMdZq6msZku7MdOSQVhdeGWrSNk3M8O9Hg7cVzPNXOF3iNoo3irQ5tURut44xs4\nWw5YWQqS9WyUY5snD8tm7Y1rQTPfhg+678xIq/zWCv/u+FSnfVv1nlhLVQkEeG/Y\ngh1uMaTIpUKTGEjIAGtpGP7wwIcXptR/HyfzhTUSTaWc1Ef7zoKT9LL5z3IV1hC2\njVWz+RwYs98LjMuksJFoHqRfWyYhCIym0jb6GTwaEmpxAjc+d7OLNQdnoEGoUYGP\nYjtfkRYg265ESMA+Kww4Xy8=\n-----END CERTIFICATE-----\n", + "intermediateCertificate":"-----BEGIN CERTIFICATE-----\nMIIDtTCCAp2gAwIBAgIBATANBgkqhkiG9w0BAQUFADCBgzEZMBcGA1UEAxMQVGVz\ndCBDQSBTVHViIEtleTEXMBUGA1UECxMOUGxhdGZvcm0gTGJhYXMxGjAYBgNVBAoT\nEVJhY2tzcGFjZSBIb3N0aW5nMRQwEgYDVQQHEwtTYW4gQW50b25pbzEOMAwGA1UE\nCBMFVGV4YXMxCzAJBgNVBAYTAlVTMB4XDTEyMDEwOTE5NDU0OVoXDTE0MDEwODE5\nNDU0OVowgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVUzCCASIw\nDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANNh55lwTVwQvNoEZjq1zGdYz9jA\nXXdjizn8AJhjHLOAallfPtvCfTEgKanhdoyz5FnhQE8HbDAop/KNS1lN2UMvdl5f\nZNLTSjJrNtedqxQwxN/i3bpyBxNVejUH2NjV1mmyj+5CJYwCzWalvI/gLPq/A3as\nO2EQqtf3U8unRgn0zXLRdYxV9MrUzNAmdipPNvNrsVdrCgA42rgF/8KsyRVQfJCX\nfN7PGCfrsC3YaUvhymraWxNnXIzMYTNa9wEeBZLUw8SlEtpa1Zsvui+TPXu3USNZ\nVnWH8Lb6ENlnoX0VBwo62fjOG3JzhNKoJawi3bRqyDdINOvafr7iPrrs/T8CAwEA\nAaMyMDAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUNpx1Pc6cGA7KqEwHMmHB\nTZMA7lQwDQYJKoZIhvcNAQEFBQADggEBAMoRgH3iTG3t317viLKoY+lNMHUgHuR7\nb3mn9MidJKyYVewe6hCDIN6WY4fUojmMW9wFJWJIo0hRMNHL3n3tq8HP2j20Mxy8\nacPdfGZJa+jiBw72CrIGdobKaFduIlIEDBA1pNdZIJ+EulrtqrMesnIt92WaypIS\n8JycbIgDMCiyC0ENHEk8UWlC6429c7OZAsplMTbHME/1R4btxjkdfrYZJjdJ2yL2\n8cjZDUDMCPTdW/ycP07Gkq30RB5tACB5aZdaCn2YaKC8FsEdhff4X7xEOfOEHWEq\nSRxADDj8Lx1MT6QpR07hCiDyHfTCtbqzI0iGjX63Oh7xXSa0f+JVTa8=\n-----END CERTIFICATE-----\n" + } +} +`) + }) +} + +func mockUpdateCertResponse(t *testing.T, lbID, certID int) { + th.Mux.HandleFunc(_certURL(lbID, certID), func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + th.TestJSONRequest(t, r, ` +{ + "certificateMapping": { + "hostName": "rackspace.com", + "privateKey":"-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEAwIudSMpRZx7TS0/AtDVX3DgXwLD9g+XrNaoazlhwhpYALgzJ\nLAbAnOxT6OT0gTpkPus/B7QhW6y6Auf2cdBeW31XoIwPsSoyNhxgErGBxzNARRB9\nlI1HCa1ojFrcULluj4W6rpaOycI5soDBJiJHin/hbZBPZq6vhPCuNP7Ya48Zd/2X\nCQ9ft3XKfmbs1SdrdROIhigse/SGRbMrCorn/vhNIuohr7yOlHG3GcVcUI9k6ZSZ\nBbqF+ZA4ApSF/Q6/cumieEgofhkYbx5fg02s9Jwr4IWnIR2bSHs7UQ6sVgKYzjs7\nPd3Unpa74jFw6/H6shABoO2CIYLotGmQbFgnpwIDAQABAoIBAQCBCQ+PCIclJHNV\ntUzfeCA5ZR4F9JbxHdRTUnxEbOB8UWotckQfTScoAvj4yvdQ42DrCZxj/UOdvFOs\nPufZvlp91bIz1alugWjE+p8n5+2hIaegoTyHoWZKBfxak0myj5KYfHZvKlbmv1ML\nXV4TwEVRfAIG+v87QTY/UUxuF5vR+BpKIbgUJLfPUFFvJUdl84qsJ44pToxaYUd/\nh5YAGC00U4ay1KVSAUnTkkPNZ0lPG/rWU6w6WcTvNRLMd8DzFLTKLOgQfHhbExAF\n+sXPWjWSzbBRP1O7fHqq96QQh4VFiY/7w9W+sDKQyV6Ul17OSXs6aZ4f+lq4rJTI\n1FG96YiBAoGBAO1tiH0h1oWDBYfJB3KJJ6CQQsDGwtHo/DEgznFVP4XwEVbZ98Ha\nBfBCn3sAybbaikyCV1Hwj7kfHMZPDHbrcUSFX7quu/2zPK+wO3lZKXSyu4YsguSa\nRedInN33PpdnlPhLyQdWSuD5sVHJDF6xn22vlyxeILH3ooLg2WOFMPmVAoGBAM+b\nUG/a7iyfpAQKYyuFAsXz6SeFaDY+ZYeX45L112H8Pu+Ie/qzon+bzLB9FIH8GP6+\nQpQgmm/p37U2gD1zChUv7iW6OfQBKk9rWvMpfRF6d7YHquElejhizfTZ+ntBV/VY\ndOYEczxhrdW7keLpatYaaWUy/VboRZmlz/9JGqVLAoGAHfqNmFc0cgk4IowEj7a3\ntTNh6ltub/i+FynwRykfazcDyXaeLPDtfQe8gVh5H8h6W+y9P9BjJVnDVVrX1RAn\nbiJ1EupLPF5sVDapW8ohTOXgfbGTGXBNUUW+4Nv+IDno+mz/RhjkPYHpnM0I7c/5\ntGzOZsC/2hjNgT8I0+MWav0CgYEAuULdJeQVlKalI6HtW2Gn1uRRVJ49H+LQkY6e\nW3+cw2jo9LI0CMWSphNvNrN3wIMp/vHj0fHCP0pSApDvIWbuQXfzKaGko7UCf7rK\nf6GvZRCHkV4IREBAb97j8bMvThxClMNqFfU0rFZyXP+0MOyhFQyertswrgQ6T+Fi\n2mnvKD8CgYAmJHP3NTDRMoMRyAzonJ6nEaGUbAgNmivTaUWMe0+leCvAdwD89gzC\nTKbm3eDUg/6Va3X6ANh3wsfIOe4RXXxcbcFDk9R4zO2M5gfLSjYm5Q87EBZ2hrdj\nM2gLI7dt6thx0J8lR8xRHBEMrVBdgwp0g1gQzo5dAV88/kpkZVps8Q==\n-----END RSA PRIVATE KEY-----\n", + "certificate":"-----BEGIN CERTIFICATE-----\nMIIEXTCCA0WgAwIBAgIGATTEAjK3MA0GCSqGSIb3DQEBBQUAMIGDMRkwFwYDVQQD\nExBUZXN0IENBIFNUdWIgS2V5MRcwFQYDVQQLEw5QbGF0Zm9ybSBMYmFhczEaMBgG\nA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFDASBgNVBAcTC1NhbiBBbnRvbmlvMQ4w\nDAYDVQQIEwVUZXhhczELMAkGA1UEBhMCVVMwHhcNMTIwMTA5MTk0NjQ1WhcNMTQw\nMTA4MTk0NjQ1WjCBgjELMAkGA1UEBhMCVVMxDjAMBgNVBAgTBVRleGFzMRQwEgYD\nVQQHEwtTYW4gQW50b25pbzEaMBgGA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFzAV\nBgNVBAsTDlBsYXRmb3JtIExiYWFzMRgwFgYDVQQDEw9UZXN0IENsaWVudCBLZXkw\nggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDAi51IylFnHtNLT8C0NVfc\nOBfAsP2D5es1qhrOWHCGlgAuDMksBsCc7FPo5PSBOmQ+6z8HtCFbrLoC5/Zx0F5b\nfVegjA+xKjI2HGASsYHHM0BFEH2UjUcJrWiMWtxQuW6Phbqulo7JwjmygMEmIkeK\nf+FtkE9mrq+E8K40/thrjxl3/ZcJD1+3dcp+ZuzVJ2t1E4iGKCx79IZFsysKiuf+\n+E0i6iGvvI6UcbcZxVxQj2TplJkFuoX5kDgClIX9Dr9y6aJ4SCh+GRhvHl+DTaz0\nnCvghachHZtIeztRDqxWApjOOzs93dSelrviMXDr8fqyEAGg7YIhgui0aZBsWCen\nAgMBAAGjgdUwgdIwgbAGA1UdIwSBqDCBpYAUNpx1Pc6cGA7KqEwHMmHBTZMA7lSh\ngYmkgYYwgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVU4IBATAd\nBgNVHQ4EFgQULueOfsjZZOHwJHZwBy6u0swnpccwDQYJKoZIhvcNAQEFBQADggEB\nAFNuqSVUaotUJoWDv4z7Kbi6JFpTjDht5ORw4BdVYlRD4h9DACAFzPrPV2ym/Osp\nhNMdZq6msZku7MdOSQVhdeGWrSNk3M8O9Hg7cVzPNXOF3iNoo3irQ5tURut44xs4\nWw5YWQqS9WyUY5snD8tm7Y1rQTPfhg+678xIq/zWCv/u+FSnfVv1nlhLVQkEeG/Y\ngh1uMaTIpUKTGEjIAGtpGP7wwIcXptR/HyfzhTUSTaWc1Ef7zoKT9LL5z3IV1hC2\njVWz+RwYs98LjMuksJFoHqRfWyYhCIym0jb6GTwaEmpxAjc+d7OLNQdnoEGoUYGP\nYjtfkRYg265ESMA+Kww4Xy8=\n-----END CERTIFICATE-----\n", + "intermediateCertificate":"-----BEGIN CERTIFICATE-----\nMIIDtTCCAp2gAwIBAgIBATANBgkqhkiG9w0BAQUFADCBgzEZMBcGA1UEAxMQVGVz\ndCBDQSBTVHViIEtleTEXMBUGA1UECxMOUGxhdGZvcm0gTGJhYXMxGjAYBgNVBAoT\nEVJhY2tzcGFjZSBIb3N0aW5nMRQwEgYDVQQHEwtTYW4gQW50b25pbzEOMAwGA1UE\nCBMFVGV4YXMxCzAJBgNVBAYTAlVTMB4XDTEyMDEwOTE5NDU0OVoXDTE0MDEwODE5\nNDU0OVowgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVUzCCASIw\nDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANNh55lwTVwQvNoEZjq1zGdYz9jA\nXXdjizn8AJhjHLOAallfPtvCfTEgKanhdoyz5FnhQE8HbDAop/KNS1lN2UMvdl5f\nZNLTSjJrNtedqxQwxN/i3bpyBxNVejUH2NjV1mmyj+5CJYwCzWalvI/gLPq/A3as\nO2EQqtf3U8unRgn0zXLRdYxV9MrUzNAmdipPNvNrsVdrCgA42rgF/8KsyRVQfJCX\nfN7PGCfrsC3YaUvhymraWxNnXIzMYTNa9wEeBZLUw8SlEtpa1Zsvui+TPXu3USNZ\nVnWH8Lb6ENlnoX0VBwo62fjOG3JzhNKoJawi3bRqyDdINOvafr7iPrrs/T8CAwEA\nAaMyMDAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUNpx1Pc6cGA7KqEwHMmHB\nTZMA7lQwDQYJKoZIhvcNAQEFBQADggEBAMoRgH3iTG3t317viLKoY+lNMHUgHuR7\nb3mn9MidJKyYVewe6hCDIN6WY4fUojmMW9wFJWJIo0hRMNHL3n3tq8HP2j20Mxy8\nacPdfGZJa+jiBw72CrIGdobKaFduIlIEDBA1pNdZIJ+EulrtqrMesnIt92WaypIS\n8JycbIgDMCiyC0ENHEk8UWlC6429c7OZAsplMTbHME/1R4btxjkdfrYZJjdJ2yL2\n8cjZDUDMCPTdW/ycP07Gkq30RB5tACB5aZdaCn2YaKC8FsEdhff4X7xEOfOEHWEq\nSRxADDj8Lx1MT6QpR07hCiDyHfTCtbqzI0iGjX63Oh7xXSa0f+JVTa8=\n-----END CERTIFICATE-----\n" + } +} + `) + + w.WriteHeader(http.StatusAccepted) + + fmt.Fprintf(w, ` +{ + "certificateMapping": { + "id": 123, + "hostName": "rackspace.com", + "certificate":"-----BEGIN CERTIFICATE-----\nMIIEXTCCA0WgAwIBAgIGATTEAjK3MA0GCSqGSIb3DQEBBQUAMIGDMRkwFwYDVQQD\nExBUZXN0IENBIFNUdWIgS2V5MRcwFQYDVQQLEw5QbGF0Zm9ybSBMYmFhczEaMBgG\nA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFDASBgNVBAcTC1NhbiBBbnRvbmlvMQ4w\nDAYDVQQIEwVUZXhhczELMAkGA1UEBhMCVVMwHhcNMTIwMTA5MTk0NjQ1WhcNMTQw\nMTA4MTk0NjQ1WjCBgjELMAkGA1UEBhMCVVMxDjAMBgNVBAgTBVRleGFzMRQwEgYD\nVQQHEwtTYW4gQW50b25pbzEaMBgGA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFzAV\nBgNVBAsTDlBsYXRmb3JtIExiYWFzMRgwFgYDVQQDEw9UZXN0IENsaWVudCBLZXkw\nggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDAi51IylFnHtNLT8C0NVfc\nOBfAsP2D5es1qhrOWHCGlgAuDMksBsCc7FPo5PSBOmQ+6z8HtCFbrLoC5/Zx0F5b\nfVegjA+xKjI2HGASsYHHM0BFEH2UjUcJrWiMWtxQuW6Phbqulo7JwjmygMEmIkeK\nf+FtkE9mrq+E8K40/thrjxl3/ZcJD1+3dcp+ZuzVJ2t1E4iGKCx79IZFsysKiuf+\n+E0i6iGvvI6UcbcZxVxQj2TplJkFuoX5kDgClIX9Dr9y6aJ4SCh+GRhvHl+DTaz0\nnCvghachHZtIeztRDqxWApjOOzs93dSelrviMXDr8fqyEAGg7YIhgui0aZBsWCen\nAgMBAAGjgdUwgdIwgbAGA1UdIwSBqDCBpYAUNpx1Pc6cGA7KqEwHMmHBTZMA7lSh\ngYmkgYYwgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVU4IBATAd\nBgNVHQ4EFgQULueOfsjZZOHwJHZwBy6u0swnpccwDQYJKoZIhvcNAQEFBQADggEB\nAFNuqSVUaotUJoWDv4z7Kbi6JFpTjDht5ORw4BdVYlRD4h9DACAFzPrPV2ym/Osp\nhNMdZq6msZku7MdOSQVhdeGWrSNk3M8O9Hg7cVzPNXOF3iNoo3irQ5tURut44xs4\nWw5YWQqS9WyUY5snD8tm7Y1rQTPfhg+678xIq/zWCv/u+FSnfVv1nlhLVQkEeG/Y\ngh1uMaTIpUKTGEjIAGtpGP7wwIcXptR/HyfzhTUSTaWc1Ef7zoKT9LL5z3IV1hC2\njVWz+RwYs98LjMuksJFoHqRfWyYhCIym0jb6GTwaEmpxAjc+d7OLNQdnoEGoUYGP\nYjtfkRYg265ESMA+Kww4Xy8=\n-----END CERTIFICATE-----\n", + "intermediateCertificate":"-----BEGIN CERTIFICATE-----\nMIIDtTCCAp2gAwIBAgIBATANBgkqhkiG9w0BAQUFADCBgzEZMBcGA1UEAxMQVGVz\ndCBDQSBTVHViIEtleTEXMBUGA1UECxMOUGxhdGZvcm0gTGJhYXMxGjAYBgNVBAoT\nEVJhY2tzcGFjZSBIb3N0aW5nMRQwEgYDVQQHEwtTYW4gQW50b25pbzEOMAwGA1UE\nCBMFVGV4YXMxCzAJBgNVBAYTAlVTMB4XDTEyMDEwOTE5NDU0OVoXDTE0MDEwODE5\nNDU0OVowgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVUzCCASIw\nDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANNh55lwTVwQvNoEZjq1zGdYz9jA\nXXdjizn8AJhjHLOAallfPtvCfTEgKanhdoyz5FnhQE8HbDAop/KNS1lN2UMvdl5f\nZNLTSjJrNtedqxQwxN/i3bpyBxNVejUH2NjV1mmyj+5CJYwCzWalvI/gLPq/A3as\nO2EQqtf3U8unRgn0zXLRdYxV9MrUzNAmdipPNvNrsVdrCgA42rgF/8KsyRVQfJCX\nfN7PGCfrsC3YaUvhymraWxNnXIzMYTNa9wEeBZLUw8SlEtpa1Zsvui+TPXu3USNZ\nVnWH8Lb6ENlnoX0VBwo62fjOG3JzhNKoJawi3bRqyDdINOvafr7iPrrs/T8CAwEA\nAaMyMDAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUNpx1Pc6cGA7KqEwHMmHB\nTZMA7lQwDQYJKoZIhvcNAQEFBQADggEBAMoRgH3iTG3t317viLKoY+lNMHUgHuR7\nb3mn9MidJKyYVewe6hCDIN6WY4fUojmMW9wFJWJIo0hRMNHL3n3tq8HP2j20Mxy8\nacPdfGZJa+jiBw72CrIGdobKaFduIlIEDBA1pNdZIJ+EulrtqrMesnIt92WaypIS\n8JycbIgDMCiyC0ENHEk8UWlC6429c7OZAsplMTbHME/1R4btxjkdfrYZJjdJ2yL2\n8cjZDUDMCPTdW/ycP07Gkq30RB5tACB5aZdaCn2YaKC8FsEdhff4X7xEOfOEHWEq\nSRxADDj8Lx1MT6QpR07hCiDyHfTCtbqzI0iGjX63Oh7xXSa0f+JVTa8=\n-----END CERTIFICATE-----\n" + } +} + `) + }) +} + +func mockDeleteCertResponse(t *testing.T, lbID, certID int) { + th.Mux.HandleFunc(_certURL(lbID, certID), func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusOK) + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/ssl/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/ssl/requests.go new file mode 100644 index 0000000000..84b2712172 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/ssl/requests.go @@ -0,0 +1,278 @@ +package ssl + +import ( + "errors" + + "github.com/racker/perigee" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +var ( + errPrivateKey = errors.New("PrivateKey is a required field") + errCertificate = errors.New("Certificate is a required field") + errIntCertificate = errors.New("IntCertificate is a required field") +) + +// UpdateOptsBuilder is the interface options structs have to satisfy in order +// to be used in the main Update operation in this package. +type UpdateOptsBuilder interface { + ToSSLUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts is the common options struct used in this package's Update +// operation. +type UpdateOpts struct { + // Required - consult the SSLTermConfig struct for more info. + SecurePort int + + // Required - consult the SSLTermConfig struct for more info. + PrivateKey string + + // Required - consult the SSLTermConfig struct for more info. + Certificate string + + // Required - consult the SSLTermConfig struct for more info. + IntCertificate string + + // Optional - consult the SSLTermConfig struct for more info. + Enabled *bool + + // Optional - consult the SSLTermConfig struct for more info. + SecureTrafficOnly *bool +} + +// ToSSLUpdateMap casts a CreateOpts struct to a map. +func (opts UpdateOpts) ToSSLUpdateMap() (map[string]interface{}, error) { + ssl := make(map[string]interface{}) + + if opts.SecurePort == 0 { + return ssl, errors.New("SecurePort needs to be an integer greater than 0") + } + if opts.PrivateKey == "" { + return ssl, errPrivateKey + } + if opts.Certificate == "" { + return ssl, errCertificate + } + if opts.IntCertificate == "" { + return ssl, errIntCertificate + } + + ssl["securePort"] = opts.SecurePort + ssl["privateKey"] = opts.PrivateKey + ssl["certificate"] = opts.Certificate + ssl["intermediateCertificate"] = opts.IntCertificate + + if opts.Enabled != nil { + ssl["enabled"] = &opts.Enabled + } + + if opts.SecureTrafficOnly != nil { + ssl["secureTrafficOnly"] = &opts.SecureTrafficOnly + } + + return map[string]interface{}{"sslTermination": ssl}, nil +} + +// Update is the operation responsible for updating the SSL Termination +// configuration for a load balancer. +func Update(c *gophercloud.ServiceClient, lbID int, opts UpdateOptsBuilder) UpdateResult { + var res UpdateResult + + reqBody, err := opts.ToSSLUpdateMap() + if err != nil { + res.Err = err + return res + } + + _, res.Err = perigee.Request("PUT", rootURL(c, lbID), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + ReqBody: &reqBody, + Results: &res.Body, + OkCodes: []int{200}, + }) + + return res +} + +// Get is the operation responsible for showing the details of the SSL +// Termination configuration for a load balancer. +func Get(c *gophercloud.ServiceClient, lbID int) GetResult { + var res GetResult + + _, res.Err = perigee.Request("GET", rootURL(c, lbID), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + Results: &res.Body, + OkCodes: []int{200}, + }) + + return res +} + +// Delete is the operation responsible for deleting the SSL Termination +// configuration for a load balancer. +func Delete(c *gophercloud.ServiceClient, lbID int) DeleteResult { + var res DeleteResult + + _, res.Err = perigee.Request("DELETE", rootURL(c, lbID), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + OkCodes: []int{200}, + }) + + return res +} + +// ListCerts will list all of the certificate mappings associated with a +// SSL-terminated HTTP load balancer. +func ListCerts(c *gophercloud.ServiceClient, lbID int) pagination.Pager { + url := certURL(c, lbID) + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return CertPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// CreateCertOptsBuilder is the interface options structs have to satisfy in +// order to be used in the AddCert operation in this package. +type CreateCertOptsBuilder interface { + ToCertCreateMap() (map[string]interface{}, error) +} + +// CreateCertOpts represents the options used when adding a new certificate mapping. +type CreateCertOpts struct { + HostName string + PrivateKey string + Certificate string + IntCertificate string +} + +// ToCertCreateMap will cast an CreateCertOpts struct to a map for JSON serialization. +func (opts CreateCertOpts) ToCertCreateMap() (map[string]interface{}, error) { + cm := make(map[string]interface{}) + + if opts.HostName == "" { + return cm, errors.New("HostName is a required option") + } + if opts.PrivateKey == "" { + return cm, errPrivateKey + } + if opts.Certificate == "" { + return cm, errCertificate + } + + cm["hostName"] = opts.HostName + cm["privateKey"] = opts.PrivateKey + cm["certificate"] = opts.Certificate + + if opts.IntCertificate != "" { + cm["intermediateCertificate"] = opts.IntCertificate + } + + return map[string]interface{}{"certificateMapping": cm}, nil +} + +// CreateCert will add a new SSL certificate and allow an SSL-terminated HTTP +// load balancer to use it. This feature is useful because it allows multiple +// certificates to be used. The maximum number of certificates that can be +// stored per LB is 20. +func CreateCert(c *gophercloud.ServiceClient, lbID int, opts CreateCertOptsBuilder) CreateCertResult { + var res CreateCertResult + + reqBody, err := opts.ToCertCreateMap() + if err != nil { + res.Err = err + return res + } + + _, res.Err = perigee.Request("POST", certURL(c, lbID), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + ReqBody: &reqBody, + Results: &res.Body, + OkCodes: []int{200}, + }) + + return res +} + +// GetCert will show the details of an existing SSL certificate. +func GetCert(c *gophercloud.ServiceClient, lbID, certID int) GetCertResult { + var res GetCertResult + + _, res.Err = perigee.Request("GET", certResourceURL(c, lbID, certID), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + Results: &res.Body, + OkCodes: []int{200}, + }) + + return res +} + +// UpdateCertOptsBuilder is the interface options structs have to satisfy in +// order to be used in the UpdateCert operation in this package. +type UpdateCertOptsBuilder interface { + ToCertUpdateMap() (map[string]interface{}, error) +} + +// UpdateCertOpts represents the options needed to update a SSL certificate. +type UpdateCertOpts struct { + HostName string + PrivateKey string + Certificate string + IntCertificate string +} + +// ToCertUpdateMap will cast an UpdateCertOpts struct into a map for JSON +// seralization. +func (opts UpdateCertOpts) ToCertUpdateMap() (map[string]interface{}, error) { + cm := make(map[string]interface{}) + + if opts.HostName != "" { + cm["hostName"] = opts.HostName + } + if opts.PrivateKey != "" { + cm["privateKey"] = opts.PrivateKey + } + if opts.Certificate != "" { + cm["certificate"] = opts.Certificate + } + if opts.IntCertificate != "" { + cm["intermediateCertificate"] = opts.IntCertificate + } + + return map[string]interface{}{"certificateMapping": cm}, nil +} + +// UpdateCert is the operation responsible for updating the details of an +// existing SSL certificate. +func UpdateCert(c *gophercloud.ServiceClient, lbID, certID int, opts UpdateCertOptsBuilder) UpdateCertResult { + var res UpdateCertResult + + reqBody, err := opts.ToCertUpdateMap() + if err != nil { + res.Err = err + return res + } + + _, res.Err = perigee.Request("PUT", certResourceURL(c, lbID, certID), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + ReqBody: &reqBody, + Results: &res.Body, + OkCodes: []int{202}, + }) + + return res +} + +// DeleteCert is the operation responsible for permanently removing a SSL +// certificate. +func DeleteCert(c *gophercloud.ServiceClient, lbID, certID int) DeleteResult { + var res DeleteResult + + _, res.Err = perigee.Request("DELETE", certResourceURL(c, lbID, certID), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + OkCodes: []int{200}, + }) + + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/ssl/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/ssl/requests_test.go new file mode 100644 index 0000000000..fb14c4a28d --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/ssl/requests_test.go @@ -0,0 +1,167 @@ +package ssl + +import ( + "testing" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +const ( + lbID = 12345 + certID = 67890 +) + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockGetResponse(t, lbID) + + sp, err := Get(client.ServiceClient(), lbID).Extract() + th.AssertNoErr(t, err) + + expected := &SSLTermConfig{ + Certificate: "-----BEGIN CERTIFICATE-----\nMIIEXTCCA0WgAwIBAgIGATTEAjK3MA0GCSqGSIb3DQEBBQUAMIGDMRkwFwYDVQQD\nExBUZXN0IENBIFNUdWIgS2V5MRcwFQYDVQQLEw5QbGF0Zm9ybSBMYmFhczEaMBgG\nA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFDASBgNVBAcTC1NhbiBBbnRvbmlvMQ4w\nDAYDVQQIEwVUZXhhczELMAkGA1UEBhMCVVMwHhcNMTIwMTA5MTk0NjQ1WhcNMTQw\nMTA4MTk0NjQ1WjCBgjELMAkGA1UEBhMCVVMxDjAMBgNVBAgTBVRleGFzMRQwEgYD\nVQQHEwtTYW4gQW50b25pbzEaMBgGA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFzAV\nBgNVBAsTDlBsYXRmb3JtIExiYWFzMRgwFgYDVQQDEw9UZXN0IENsaWVudCBLZXkw\nggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDAi51IylFnHtNLT8C0NVfc\nOBfAsP2D5es1qhrOWHCGlgAuDMksBsCc7FPo5PSBOmQ+6z8HtCFbrLoC5/Zx0F5b\nfVegjA+xKjI2HGASsYHHM0BFEH2UjUcJrWiMWtxQuW6Phbqulo7JwjmygMEmIkeK\nf+FtkE9mrq+E8K40/thrjxl3/ZcJD1+3dcp+ZuzVJ2t1E4iGKCx79IZFsysKiuf+\n+E0i6iGvvI6UcbcZxVxQj2TplJkFuoX5kDgClIX9Dr9y6aJ4SCh+GRhvHl+DTaz0\nnCvghachHZtIeztRDqxWApjOOzs93dSelrviMXDr8fqyEAGg7YIhgui0aZBsWCen\nAgMBAAGjgdUwgdIwgbAGA1UdIwSBqDCBpYAUNpx1Pc6cGA7KqEwHMmHBTZMA7lSh\ngYmkgYYwgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVU4IBATAd\nBgNVHQ4EFgQULueOfsjZZOHwJHZwBy6u0swnpccwDQYJKoZIhvcNAQEFBQADggEB\nAFNuqSVUaotUJoWDv4z7Kbi6JFpTjDht5ORw4BdVYlRD4h9DACAFzPrPV2ym/Osp\nhNMdZq6msZku7MdOSQVhdeGWrSNk3M8O9Hg7cVzPNXOF3iNoo3irQ5tURut44xs4\nWw5YWQqS9WyUY5snD8tm7Y1rQTPfhg+678xIq/zWCv/u+FSnfVv1nlhLVQkEeG/Y\ngh1uMaTIpUKTGEjIAGtpGP7wwIcXptR/HyfzhTUSTaWc1Ef7zoKT9LL5z3IV1hC2\njVWz+RwYs98LjMuksJFoHqRfWyYhCIym0jb6GTwaEmpxAjc+d7OLNQdnoEGoUYGP\nYjtfkRYg265ESMA+Kww4Xy8=\n-----END CERTIFICATE-----\n", + Enabled: true, + SecureTrafficOnly: false, + IntCertificate: "-----BEGIN CERTIFICATE-----\nMIIDtTCCAp2gAwIBAgIBATANBgkqhkiG9w0BAQUFADCBgzEZMBcGA1UEAxMQVGVz\ndCBDQSBTVHViIEtleTEXMBUGA1UECxMOUGxhdGZvcm0gTGJhYXMxGjAYBgNVBAoT\nEVJhY2tzcGFjZSBIb3N0aW5nMRQwEgYDVQQHEwtTYW4gQW50b25pbzEOMAwGA1UE\nCBMFVGV4YXMxCzAJBgNVBAYTAlVTMB4XDTEyMDEwOTE5NDU0OVoXDTE0MDEwODE5\nNDU0OVowgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVUzCCASIw\nDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANNh55lwTVwQvNoEZjq1zGdYz9jA\nXXdjizn8AJhjHLOAallfPtvCfTEgKanhdoyz5FnhQE8HbDAop/KNS1lN2UMvdl5f\nZNLTSjJrNtedqxQwxN/i3bpyBxNVejUH2NjV1mmyj+5CJYwCzWalvI/gLPq/A3as\nO2EQqtf3U8unRgn0zXLRdYxV9MrUzNAmdipPNvNrsVdrCgA42rgF/8KsyRVQfJCX\nfN7PGCfrsC3YaUvhymraWxNnXIzMYTNa9wEeBZLUw8SlEtpa1Zsvui+TPXu3USNZ\nVnWH8Lb6ENlnoX0VBwo62fjOG3JzhNKoJawi3bRqyDdINOvafr7iPrrs/T8CAwEA\nAaMyMDAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUNpx1Pc6cGA7KqEwHMmHB\nTZMA7lQwDQYJKoZIhvcNAQEFBQADggEBAMoRgH3iTG3t317viLKoY+lNMHUgHuR7\nb3mn9MidJKyYVewe6hCDIN6WY4fUojmMW9wFJWJIo0hRMNHL3n3tq8HP2j20Mxy8\nacPdfGZJa+jiBw72CrIGdobKaFduIlIEDBA1pNdZIJ+EulrtqrMesnIt92WaypIS\n8JycbIgDMCiyC0ENHEk8UWlC6429c7OZAsplMTbHME/1R4btxjkdfrYZJjdJ2yL2\n8cjZDUDMCPTdW/ycP07Gkq30RB5tACB5aZdaCn2YaKC8FsEdhff4X7xEOfOEHWEq\nSRxADDj8Lx1MT6QpR07hCiDyHfTCtbqzI0iGjX63Oh7xXSa0f+JVTa8=\n-----END CERTIFICATE-----\n", + SecurePort: 443, + } + th.AssertDeepEquals(t, expected, sp) +} + +func TestUpdate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockUpdateResponse(t, lbID) + + opts := UpdateOpts{ + Enabled: gophercloud.Enabled, + SecurePort: 443, + SecureTrafficOnly: gophercloud.Disabled, + PrivateKey: "foo", + Certificate: "-----BEGIN CERTIFICATE-----\nMIIEXTCCA0WgAwIBAgIGATTEAjK3MA0GCSqGSIb3DQEBBQUAMIGDMRkwFwYDVQQD\nExBUZXN0IENBIFNUdWIgS2V5MRcwFQYDVQQLEw5QbGF0Zm9ybSBMYmFhczEaMBgG\nA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFDASBgNVBAcTC1NhbiBBbnRvbmlvMQ4w\nDAYDVQQIEwVUZXhhczELMAkGA1UEBhMCVVMwHhcNMTIwMTA5MTk0NjQ1WhcNMTQw\nMTA4MTk0NjQ1WjCBgjELMAkGA1UEBhMCVVMxDjAMBgNVBAgTBVRleGFzMRQwEgYD\nVQQHEwtTYW4gQW50b25pbzEaMBgGA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFzAV\nBgNVBAsTDlBsYXRmb3JtIExiYWFzMRgwFgYDVQQDEw9UZXN0IENsaWVudCBLZXkw\nggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDAi51IylFnHtNLT8C0NVfc\nOBfAsP2D5es1qhrOWHCGlgAuDMksBsCc7FPo5PSBOmQ+6z8HtCFbrLoC5/Zx0F5b\nfVegjA+xKjI2HGASsYHHM0BFEH2UjUcJrWiMWtxQuW6Phbqulo7JwjmygMEmIkeK\nf+FtkE9mrq+E8K40/thrjxl3/ZcJD1+3dcp+ZuzVJ2t1E4iGKCx79IZFsysKiuf+\n+E0i6iGvvI6UcbcZxVxQj2TplJkFuoX5kDgClIX9Dr9y6aJ4SCh+GRhvHl+DTaz0\nnCvghachHZtIeztRDqxWApjOOzs93dSelrviMXDr8fqyEAGg7YIhgui0aZBsWCen\nAgMBAAGjgdUwgdIwgbAGA1UdIwSBqDCBpYAUNpx1Pc6cGA7KqEwHMmHBTZMA7lSh\ngYmkgYYwgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVU4IBATAd\nBgNVHQ4EFgQULueOfsjZZOHwJHZwBy6u0swnpccwDQYJKoZIhvcNAQEFBQADggEB\nAFNuqSVUaotUJoWDv4z7Kbi6JFpTjDht5ORw4BdVYlRD4h9DACAFzPrPV2ym/Osp\nhNMdZq6msZku7MdOSQVhdeGWrSNk3M8O9Hg7cVzPNXOF3iNoo3irQ5tURut44xs4\nWw5YWQqS9WyUY5snD8tm7Y1rQTPfhg+678xIq/zWCv/u+FSnfVv1nlhLVQkEeG/Y\ngh1uMaTIpUKTGEjIAGtpGP7wwIcXptR/HyfzhTUSTaWc1Ef7zoKT9LL5z3IV1hC2\njVWz+RwYs98LjMuksJFoHqRfWyYhCIym0jb6GTwaEmpxAjc+d7OLNQdnoEGoUYGP\nYjtfkRYg265ESMA+Kww4Xy8=\n-----END CERTIFICATE-----\n", + IntCertificate: "-----BEGIN CERTIFICATE-----\nMIIDtTCCAp2gAwIBAgIBATANBgkqhkiG9w0BAQUFADCBgzEZMBcGA1UEAxMQVGVz\ndCBDQSBTVHViIEtleTEXMBUGA1UECxMOUGxhdGZvcm0gTGJhYXMxGjAYBgNVBAoT\nEVJhY2tzcGFjZSBIb3N0aW5nMRQwEgYDVQQHEwtTYW4gQW50b25pbzEOMAwGA1UE\nCBMFVGV4YXMxCzAJBgNVBAYTAlVTMB4XDTEyMDEwOTE5NDU0OVoXDTE0MDEwODE5\nNDU0OVowgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVUzCCASIw\nDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANNh55lwTVwQvNoEZjq1zGdYz9jA\nXXdjizn8AJhjHLOAallfPtvCfTEgKanhdoyz5FnhQE8HbDAop/KNS1lN2UMvdl5f\nZNLTSjJrNtedqxQwxN/i3bpyBxNVejUH2NjV1mmyj+5CJYwCzWalvI/gLPq/A3as\nO2EQqtf3U8unRgn0zXLRdYxV9MrUzNAmdipPNvNrsVdrCgA42rgF/8KsyRVQfJCX\nfN7PGCfrsC3YaUvhymraWxNnXIzMYTNa9wEeBZLUw8SlEtpa1Zsvui+TPXu3USNZ\nVnWH8Lb6ENlnoX0VBwo62fjOG3JzhNKoJawi3bRqyDdINOvafr7iPrrs/T8CAwEA\nAaMyMDAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUNpx1Pc6cGA7KqEwHMmHB\nTZMA7lQwDQYJKoZIhvcNAQEFBQADggEBAMoRgH3iTG3t317viLKoY+lNMHUgHuR7\nb3mn9MidJKyYVewe6hCDIN6WY4fUojmMW9wFJWJIo0hRMNHL3n3tq8HP2j20Mxy8\nacPdfGZJa+jiBw72CrIGdobKaFduIlIEDBA1pNdZIJ+EulrtqrMesnIt92WaypIS\n8JycbIgDMCiyC0ENHEk8UWlC6429c7OZAsplMTbHME/1R4btxjkdfrYZJjdJ2yL2\n8cjZDUDMCPTdW/ycP07Gkq30RB5tACB5aZdaCn2YaKC8FsEdhff4X7xEOfOEHWEq\nSRxADDj8Lx1MT6QpR07hCiDyHfTCtbqzI0iGjX63Oh7xXSa0f+JVTa8=\n-----END CERTIFICATE-----\n", + } + err := Update(client.ServiceClient(), lbID, opts).ExtractErr() + th.AssertNoErr(t, err) +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockDeleteResponse(t, lbID) + + err := Delete(client.ServiceClient(), lbID).ExtractErr() + th.AssertNoErr(t, err) +} + +func TestListCerts(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockListCertsResponse(t, lbID) + + count := 0 + + err := ListCerts(client.ServiceClient(), lbID).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractCerts(page) + th.AssertNoErr(t, err) + + expected := []Certificate{ + Certificate{ID: 123, HostName: "rackspace.com"}, + Certificate{ID: 124, HostName: "*.rackspace.com"}, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + + th.AssertNoErr(t, err) + th.AssertEquals(t, 1, count) +} + +func TestCreateCert(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockAddCertResponse(t, lbID) + + opts := CreateCertOpts{ + HostName: "rackspace.com", + PrivateKey: "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEAwIudSMpRZx7TS0/AtDVX3DgXwLD9g+XrNaoazlhwhpYALgzJ\nLAbAnOxT6OT0gTpkPus/B7QhW6y6Auf2cdBeW31XoIwPsSoyNhxgErGBxzNARRB9\nlI1HCa1ojFrcULluj4W6rpaOycI5soDBJiJHin/hbZBPZq6vhPCuNP7Ya48Zd/2X\nCQ9ft3XKfmbs1SdrdROIhigse/SGRbMrCorn/vhNIuohr7yOlHG3GcVcUI9k6ZSZ\nBbqF+ZA4ApSF/Q6/cumieEgofhkYbx5fg02s9Jwr4IWnIR2bSHs7UQ6sVgKYzjs7\nPd3Unpa74jFw6/H6shABoO2CIYLotGmQbFgnpwIDAQABAoIBAQCBCQ+PCIclJHNV\ntUzfeCA5ZR4F9JbxHdRTUnxEbOB8UWotckQfTScoAvj4yvdQ42DrCZxj/UOdvFOs\nPufZvlp91bIz1alugWjE+p8n5+2hIaegoTyHoWZKBfxak0myj5KYfHZvKlbmv1ML\nXV4TwEVRfAIG+v87QTY/UUxuF5vR+BpKIbgUJLfPUFFvJUdl84qsJ44pToxaYUd/\nh5YAGC00U4ay1KVSAUnTkkPNZ0lPG/rWU6w6WcTvNRLMd8DzFLTKLOgQfHhbExAF\n+sXPWjWSzbBRP1O7fHqq96QQh4VFiY/7w9W+sDKQyV6Ul17OSXs6aZ4f+lq4rJTI\n1FG96YiBAoGBAO1tiH0h1oWDBYfJB3KJJ6CQQsDGwtHo/DEgznFVP4XwEVbZ98Ha\nBfBCn3sAybbaikyCV1Hwj7kfHMZPDHbrcUSFX7quu/2zPK+wO3lZKXSyu4YsguSa\nRedInN33PpdnlPhLyQdWSuD5sVHJDF6xn22vlyxeILH3ooLg2WOFMPmVAoGBAM+b\nUG/a7iyfpAQKYyuFAsXz6SeFaDY+ZYeX45L112H8Pu+Ie/qzon+bzLB9FIH8GP6+\nQpQgmm/p37U2gD1zChUv7iW6OfQBKk9rWvMpfRF6d7YHquElejhizfTZ+ntBV/VY\ndOYEczxhrdW7keLpatYaaWUy/VboRZmlz/9JGqVLAoGAHfqNmFc0cgk4IowEj7a3\ntTNh6ltub/i+FynwRykfazcDyXaeLPDtfQe8gVh5H8h6W+y9P9BjJVnDVVrX1RAn\nbiJ1EupLPF5sVDapW8ohTOXgfbGTGXBNUUW+4Nv+IDno+mz/RhjkPYHpnM0I7c/5\ntGzOZsC/2hjNgT8I0+MWav0CgYEAuULdJeQVlKalI6HtW2Gn1uRRVJ49H+LQkY6e\nW3+cw2jo9LI0CMWSphNvNrN3wIMp/vHj0fHCP0pSApDvIWbuQXfzKaGko7UCf7rK\nf6GvZRCHkV4IREBAb97j8bMvThxClMNqFfU0rFZyXP+0MOyhFQyertswrgQ6T+Fi\n2mnvKD8CgYAmJHP3NTDRMoMRyAzonJ6nEaGUbAgNmivTaUWMe0+leCvAdwD89gzC\nTKbm3eDUg/6Va3X6ANh3wsfIOe4RXXxcbcFDk9R4zO2M5gfLSjYm5Q87EBZ2hrdj\nM2gLI7dt6thx0J8lR8xRHBEMrVBdgwp0g1gQzo5dAV88/kpkZVps8Q==\n-----END RSA PRIVATE KEY-----\n", + Certificate: "-----BEGIN CERTIFICATE-----\nMIIEXTCCA0WgAwIBAgIGATTEAjK3MA0GCSqGSIb3DQEBBQUAMIGDMRkwFwYDVQQD\nExBUZXN0IENBIFNUdWIgS2V5MRcwFQYDVQQLEw5QbGF0Zm9ybSBMYmFhczEaMBgG\nA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFDASBgNVBAcTC1NhbiBBbnRvbmlvMQ4w\nDAYDVQQIEwVUZXhhczELMAkGA1UEBhMCVVMwHhcNMTIwMTA5MTk0NjQ1WhcNMTQw\nMTA4MTk0NjQ1WjCBgjELMAkGA1UEBhMCVVMxDjAMBgNVBAgTBVRleGFzMRQwEgYD\nVQQHEwtTYW4gQW50b25pbzEaMBgGA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFzAV\nBgNVBAsTDlBsYXRmb3JtIExiYWFzMRgwFgYDVQQDEw9UZXN0IENsaWVudCBLZXkw\nggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDAi51IylFnHtNLT8C0NVfc\nOBfAsP2D5es1qhrOWHCGlgAuDMksBsCc7FPo5PSBOmQ+6z8HtCFbrLoC5/Zx0F5b\nfVegjA+xKjI2HGASsYHHM0BFEH2UjUcJrWiMWtxQuW6Phbqulo7JwjmygMEmIkeK\nf+FtkE9mrq+E8K40/thrjxl3/ZcJD1+3dcp+ZuzVJ2t1E4iGKCx79IZFsysKiuf+\n+E0i6iGvvI6UcbcZxVxQj2TplJkFuoX5kDgClIX9Dr9y6aJ4SCh+GRhvHl+DTaz0\nnCvghachHZtIeztRDqxWApjOOzs93dSelrviMXDr8fqyEAGg7YIhgui0aZBsWCen\nAgMBAAGjgdUwgdIwgbAGA1UdIwSBqDCBpYAUNpx1Pc6cGA7KqEwHMmHBTZMA7lSh\ngYmkgYYwgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVU4IBATAd\nBgNVHQ4EFgQULueOfsjZZOHwJHZwBy6u0swnpccwDQYJKoZIhvcNAQEFBQADggEB\nAFNuqSVUaotUJoWDv4z7Kbi6JFpTjDht5ORw4BdVYlRD4h9DACAFzPrPV2ym/Osp\nhNMdZq6msZku7MdOSQVhdeGWrSNk3M8O9Hg7cVzPNXOF3iNoo3irQ5tURut44xs4\nWw5YWQqS9WyUY5snD8tm7Y1rQTPfhg+678xIq/zWCv/u+FSnfVv1nlhLVQkEeG/Y\ngh1uMaTIpUKTGEjIAGtpGP7wwIcXptR/HyfzhTUSTaWc1Ef7zoKT9LL5z3IV1hC2\njVWz+RwYs98LjMuksJFoHqRfWyYhCIym0jb6GTwaEmpxAjc+d7OLNQdnoEGoUYGP\nYjtfkRYg265ESMA+Kww4Xy8=\n-----END CERTIFICATE-----\n", + IntCertificate: "-----BEGIN CERTIFICATE-----\nMIIDtTCCAp2gAwIBAgIBATANBgkqhkiG9w0BAQUFADCBgzEZMBcGA1UEAxMQVGVz\ndCBDQSBTVHViIEtleTEXMBUGA1UECxMOUGxhdGZvcm0gTGJhYXMxGjAYBgNVBAoT\nEVJhY2tzcGFjZSBIb3N0aW5nMRQwEgYDVQQHEwtTYW4gQW50b25pbzEOMAwGA1UE\nCBMFVGV4YXMxCzAJBgNVBAYTAlVTMB4XDTEyMDEwOTE5NDU0OVoXDTE0MDEwODE5\nNDU0OVowgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVUzCCASIw\nDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANNh55lwTVwQvNoEZjq1zGdYz9jA\nXXdjizn8AJhjHLOAallfPtvCfTEgKanhdoyz5FnhQE8HbDAop/KNS1lN2UMvdl5f\nZNLTSjJrNtedqxQwxN/i3bpyBxNVejUH2NjV1mmyj+5CJYwCzWalvI/gLPq/A3as\nO2EQqtf3U8unRgn0zXLRdYxV9MrUzNAmdipPNvNrsVdrCgA42rgF/8KsyRVQfJCX\nfN7PGCfrsC3YaUvhymraWxNnXIzMYTNa9wEeBZLUw8SlEtpa1Zsvui+TPXu3USNZ\nVnWH8Lb6ENlnoX0VBwo62fjOG3JzhNKoJawi3bRqyDdINOvafr7iPrrs/T8CAwEA\nAaMyMDAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUNpx1Pc6cGA7KqEwHMmHB\nTZMA7lQwDQYJKoZIhvcNAQEFBQADggEBAMoRgH3iTG3t317viLKoY+lNMHUgHuR7\nb3mn9MidJKyYVewe6hCDIN6WY4fUojmMW9wFJWJIo0hRMNHL3n3tq8HP2j20Mxy8\nacPdfGZJa+jiBw72CrIGdobKaFduIlIEDBA1pNdZIJ+EulrtqrMesnIt92WaypIS\n8JycbIgDMCiyC0ENHEk8UWlC6429c7OZAsplMTbHME/1R4btxjkdfrYZJjdJ2yL2\n8cjZDUDMCPTdW/ycP07Gkq30RB5tACB5aZdaCn2YaKC8FsEdhff4X7xEOfOEHWEq\nSRxADDj8Lx1MT6QpR07hCiDyHfTCtbqzI0iGjX63Oh7xXSa0f+JVTa8=\n-----END CERTIFICATE-----\n", + } + + cm, err := CreateCert(client.ServiceClient(), lbID, opts).Extract() + th.AssertNoErr(t, err) + + expected := &Certificate{ + ID: 123, + HostName: "rackspace.com", + Certificate: "-----BEGIN CERTIFICATE-----\nMIIEXTCCA0WgAwIBAgIGATTEAjK3MA0GCSqGSIb3DQEBBQUAMIGDMRkwFwYDVQQD\nExBUZXN0IENBIFNUdWIgS2V5MRcwFQYDVQQLEw5QbGF0Zm9ybSBMYmFhczEaMBgG\nA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFDASBgNVBAcTC1NhbiBBbnRvbmlvMQ4w\nDAYDVQQIEwVUZXhhczELMAkGA1UEBhMCVVMwHhcNMTIwMTA5MTk0NjQ1WhcNMTQw\nMTA4MTk0NjQ1WjCBgjELMAkGA1UEBhMCVVMxDjAMBgNVBAgTBVRleGFzMRQwEgYD\nVQQHEwtTYW4gQW50b25pbzEaMBgGA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFzAV\nBgNVBAsTDlBsYXRmb3JtIExiYWFzMRgwFgYDVQQDEw9UZXN0IENsaWVudCBLZXkw\nggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDAi51IylFnHtNLT8C0NVfc\nOBfAsP2D5es1qhrOWHCGlgAuDMksBsCc7FPo5PSBOmQ+6z8HtCFbrLoC5/Zx0F5b\nfVegjA+xKjI2HGASsYHHM0BFEH2UjUcJrWiMWtxQuW6Phbqulo7JwjmygMEmIkeK\nf+FtkE9mrq+E8K40/thrjxl3/ZcJD1+3dcp+ZuzVJ2t1E4iGKCx79IZFsysKiuf+\n+E0i6iGvvI6UcbcZxVxQj2TplJkFuoX5kDgClIX9Dr9y6aJ4SCh+GRhvHl+DTaz0\nnCvghachHZtIeztRDqxWApjOOzs93dSelrviMXDr8fqyEAGg7YIhgui0aZBsWCen\nAgMBAAGjgdUwgdIwgbAGA1UdIwSBqDCBpYAUNpx1Pc6cGA7KqEwHMmHBTZMA7lSh\ngYmkgYYwgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVU4IBATAd\nBgNVHQ4EFgQULueOfsjZZOHwJHZwBy6u0swnpccwDQYJKoZIhvcNAQEFBQADggEB\nAFNuqSVUaotUJoWDv4z7Kbi6JFpTjDht5ORw4BdVYlRD4h9DACAFzPrPV2ym/Osp\nhNMdZq6msZku7MdOSQVhdeGWrSNk3M8O9Hg7cVzPNXOF3iNoo3irQ5tURut44xs4\nWw5YWQqS9WyUY5snD8tm7Y1rQTPfhg+678xIq/zWCv/u+FSnfVv1nlhLVQkEeG/Y\ngh1uMaTIpUKTGEjIAGtpGP7wwIcXptR/HyfzhTUSTaWc1Ef7zoKT9LL5z3IV1hC2\njVWz+RwYs98LjMuksJFoHqRfWyYhCIym0jb6GTwaEmpxAjc+d7OLNQdnoEGoUYGP\nYjtfkRYg265ESMA+Kww4Xy8=\n-----END CERTIFICATE-----\n", + IntCertificate: "-----BEGIN CERTIFICATE-----\nMIIDtTCCAp2gAwIBAgIBATANBgkqhkiG9w0BAQUFADCBgzEZMBcGA1UEAxMQVGVz\ndCBDQSBTVHViIEtleTEXMBUGA1UECxMOUGxhdGZvcm0gTGJhYXMxGjAYBgNVBAoT\nEVJhY2tzcGFjZSBIb3N0aW5nMRQwEgYDVQQHEwtTYW4gQW50b25pbzEOMAwGA1UE\nCBMFVGV4YXMxCzAJBgNVBAYTAlVTMB4XDTEyMDEwOTE5NDU0OVoXDTE0MDEwODE5\nNDU0OVowgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVUzCCASIw\nDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANNh55lwTVwQvNoEZjq1zGdYz9jA\nXXdjizn8AJhjHLOAallfPtvCfTEgKanhdoyz5FnhQE8HbDAop/KNS1lN2UMvdl5f\nZNLTSjJrNtedqxQwxN/i3bpyBxNVejUH2NjV1mmyj+5CJYwCzWalvI/gLPq/A3as\nO2EQqtf3U8unRgn0zXLRdYxV9MrUzNAmdipPNvNrsVdrCgA42rgF/8KsyRVQfJCX\nfN7PGCfrsC3YaUvhymraWxNnXIzMYTNa9wEeBZLUw8SlEtpa1Zsvui+TPXu3USNZ\nVnWH8Lb6ENlnoX0VBwo62fjOG3JzhNKoJawi3bRqyDdINOvafr7iPrrs/T8CAwEA\nAaMyMDAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUNpx1Pc6cGA7KqEwHMmHB\nTZMA7lQwDQYJKoZIhvcNAQEFBQADggEBAMoRgH3iTG3t317viLKoY+lNMHUgHuR7\nb3mn9MidJKyYVewe6hCDIN6WY4fUojmMW9wFJWJIo0hRMNHL3n3tq8HP2j20Mxy8\nacPdfGZJa+jiBw72CrIGdobKaFduIlIEDBA1pNdZIJ+EulrtqrMesnIt92WaypIS\n8JycbIgDMCiyC0ENHEk8UWlC6429c7OZAsplMTbHME/1R4btxjkdfrYZJjdJ2yL2\n8cjZDUDMCPTdW/ycP07Gkq30RB5tACB5aZdaCn2YaKC8FsEdhff4X7xEOfOEHWEq\nSRxADDj8Lx1MT6QpR07hCiDyHfTCtbqzI0iGjX63Oh7xXSa0f+JVTa8=\n-----END CERTIFICATE-----\n", + } + th.AssertDeepEquals(t, expected, cm) +} + +func TestGetCertMapping(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockGetCertResponse(t, lbID, certID) + + sp, err := GetCert(client.ServiceClient(), lbID, certID).Extract() + th.AssertNoErr(t, err) + + expected := &Certificate{ + ID: 123, + HostName: "rackspace.com", + Certificate: "-----BEGIN CERTIFICATE-----\nMIIEXTCCA0WgAwIBAgIGATTEAjK3MA0GCSqGSIb3DQEBBQUAMIGDMRkwFwYDVQQD\nExBUZXN0IENBIFNUdWIgS2V5MRcwFQYDVQQLEw5QbGF0Zm9ybSBMYmFhczEaMBgG\nA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFDASBgNVBAcTC1NhbiBBbnRvbmlvMQ4w\nDAYDVQQIEwVUZXhhczELMAkGA1UEBhMCVVMwHhcNMTIwMTA5MTk0NjQ1WhcNMTQw\nMTA4MTk0NjQ1WjCBgjELMAkGA1UEBhMCVVMxDjAMBgNVBAgTBVRleGFzMRQwEgYD\nVQQHEwtTYW4gQW50b25pbzEaMBgGA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFzAV\nBgNVBAsTDlBsYXRmb3JtIExiYWFzMRgwFgYDVQQDEw9UZXN0IENsaWVudCBLZXkw\nggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDAi51IylFnHtNLT8C0NVfc\nOBfAsP2D5es1qhrOWHCGlgAuDMksBsCc7FPo5PSBOmQ+6z8HtCFbrLoC5/Zx0F5b\nfVegjA+xKjI2HGASsYHHM0BFEH2UjUcJrWiMWtxQuW6Phbqulo7JwjmygMEmIkeK\nf+FtkE9mrq+E8K40/thrjxl3/ZcJD1+3dcp+ZuzVJ2t1E4iGKCx79IZFsysKiuf+\n+E0i6iGvvI6UcbcZxVxQj2TplJkFuoX5kDgClIX9Dr9y6aJ4SCh+GRhvHl+DTaz0\nnCvghachHZtIeztRDqxWApjOOzs93dSelrviMXDr8fqyEAGg7YIhgui0aZBsWCen\nAgMBAAGjgdUwgdIwgbAGA1UdIwSBqDCBpYAUNpx1Pc6cGA7KqEwHMmHBTZMA7lSh\ngYmkgYYwgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVU4IBATAd\nBgNVHQ4EFgQULueOfsjZZOHwJHZwBy6u0swnpccwDQYJKoZIhvcNAQEFBQADggEB\nAFNuqSVUaotUJoWDv4z7Kbi6JFpTjDht5ORw4BdVYlRD4h9DACAFzPrPV2ym/Osp\nhNMdZq6msZku7MdOSQVhdeGWrSNk3M8O9Hg7cVzPNXOF3iNoo3irQ5tURut44xs4\nWw5YWQqS9WyUY5snD8tm7Y1rQTPfhg+678xIq/zWCv/u+FSnfVv1nlhLVQkEeG/Y\ngh1uMaTIpUKTGEjIAGtpGP7wwIcXptR/HyfzhTUSTaWc1Ef7zoKT9LL5z3IV1hC2\njVWz+RwYs98LjMuksJFoHqRfWyYhCIym0jb6GTwaEmpxAjc+d7OLNQdnoEGoUYGP\nYjtfkRYg265ESMA+Kww4Xy8=\n-----END CERTIFICATE-----\n", + IntCertificate: "-----BEGIN CERTIFICATE-----\nMIIDtTCCAp2gAwIBAgIBATANBgkqhkiG9w0BAQUFADCBgzEZMBcGA1UEAxMQVGVz\ndCBDQSBTVHViIEtleTEXMBUGA1UECxMOUGxhdGZvcm0gTGJhYXMxGjAYBgNVBAoT\nEVJhY2tzcGFjZSBIb3N0aW5nMRQwEgYDVQQHEwtTYW4gQW50b25pbzEOMAwGA1UE\nCBMFVGV4YXMxCzAJBgNVBAYTAlVTMB4XDTEyMDEwOTE5NDU0OVoXDTE0MDEwODE5\nNDU0OVowgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVUzCCASIw\nDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANNh55lwTVwQvNoEZjq1zGdYz9jA\nXXdjizn8AJhjHLOAallfPtvCfTEgKanhdoyz5FnhQE8HbDAop/KNS1lN2UMvdl5f\nZNLTSjJrNtedqxQwxN/i3bpyBxNVejUH2NjV1mmyj+5CJYwCzWalvI/gLPq/A3as\nO2EQqtf3U8unRgn0zXLRdYxV9MrUzNAmdipPNvNrsVdrCgA42rgF/8KsyRVQfJCX\nfN7PGCfrsC3YaUvhymraWxNnXIzMYTNa9wEeBZLUw8SlEtpa1Zsvui+TPXu3USNZ\nVnWH8Lb6ENlnoX0VBwo62fjOG3JzhNKoJawi3bRqyDdINOvafr7iPrrs/T8CAwEA\nAaMyMDAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUNpx1Pc6cGA7KqEwHMmHB\nTZMA7lQwDQYJKoZIhvcNAQEFBQADggEBAMoRgH3iTG3t317viLKoY+lNMHUgHuR7\nb3mn9MidJKyYVewe6hCDIN6WY4fUojmMW9wFJWJIo0hRMNHL3n3tq8HP2j20Mxy8\nacPdfGZJa+jiBw72CrIGdobKaFduIlIEDBA1pNdZIJ+EulrtqrMesnIt92WaypIS\n8JycbIgDMCiyC0ENHEk8UWlC6429c7OZAsplMTbHME/1R4btxjkdfrYZJjdJ2yL2\n8cjZDUDMCPTdW/ycP07Gkq30RB5tACB5aZdaCn2YaKC8FsEdhff4X7xEOfOEHWEq\nSRxADDj8Lx1MT6QpR07hCiDyHfTCtbqzI0iGjX63Oh7xXSa0f+JVTa8=\n-----END CERTIFICATE-----\n", + } + th.AssertDeepEquals(t, expected, sp) +} + +func TestUpdateCert(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockUpdateCertResponse(t, lbID, certID) + + opts := UpdateCertOpts{ + HostName: "rackspace.com", + PrivateKey: "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEAwIudSMpRZx7TS0/AtDVX3DgXwLD9g+XrNaoazlhwhpYALgzJ\nLAbAnOxT6OT0gTpkPus/B7QhW6y6Auf2cdBeW31XoIwPsSoyNhxgErGBxzNARRB9\nlI1HCa1ojFrcULluj4W6rpaOycI5soDBJiJHin/hbZBPZq6vhPCuNP7Ya48Zd/2X\nCQ9ft3XKfmbs1SdrdROIhigse/SGRbMrCorn/vhNIuohr7yOlHG3GcVcUI9k6ZSZ\nBbqF+ZA4ApSF/Q6/cumieEgofhkYbx5fg02s9Jwr4IWnIR2bSHs7UQ6sVgKYzjs7\nPd3Unpa74jFw6/H6shABoO2CIYLotGmQbFgnpwIDAQABAoIBAQCBCQ+PCIclJHNV\ntUzfeCA5ZR4F9JbxHdRTUnxEbOB8UWotckQfTScoAvj4yvdQ42DrCZxj/UOdvFOs\nPufZvlp91bIz1alugWjE+p8n5+2hIaegoTyHoWZKBfxak0myj5KYfHZvKlbmv1ML\nXV4TwEVRfAIG+v87QTY/UUxuF5vR+BpKIbgUJLfPUFFvJUdl84qsJ44pToxaYUd/\nh5YAGC00U4ay1KVSAUnTkkPNZ0lPG/rWU6w6WcTvNRLMd8DzFLTKLOgQfHhbExAF\n+sXPWjWSzbBRP1O7fHqq96QQh4VFiY/7w9W+sDKQyV6Ul17OSXs6aZ4f+lq4rJTI\n1FG96YiBAoGBAO1tiH0h1oWDBYfJB3KJJ6CQQsDGwtHo/DEgznFVP4XwEVbZ98Ha\nBfBCn3sAybbaikyCV1Hwj7kfHMZPDHbrcUSFX7quu/2zPK+wO3lZKXSyu4YsguSa\nRedInN33PpdnlPhLyQdWSuD5sVHJDF6xn22vlyxeILH3ooLg2WOFMPmVAoGBAM+b\nUG/a7iyfpAQKYyuFAsXz6SeFaDY+ZYeX45L112H8Pu+Ie/qzon+bzLB9FIH8GP6+\nQpQgmm/p37U2gD1zChUv7iW6OfQBKk9rWvMpfRF6d7YHquElejhizfTZ+ntBV/VY\ndOYEczxhrdW7keLpatYaaWUy/VboRZmlz/9JGqVLAoGAHfqNmFc0cgk4IowEj7a3\ntTNh6ltub/i+FynwRykfazcDyXaeLPDtfQe8gVh5H8h6W+y9P9BjJVnDVVrX1RAn\nbiJ1EupLPF5sVDapW8ohTOXgfbGTGXBNUUW+4Nv+IDno+mz/RhjkPYHpnM0I7c/5\ntGzOZsC/2hjNgT8I0+MWav0CgYEAuULdJeQVlKalI6HtW2Gn1uRRVJ49H+LQkY6e\nW3+cw2jo9LI0CMWSphNvNrN3wIMp/vHj0fHCP0pSApDvIWbuQXfzKaGko7UCf7rK\nf6GvZRCHkV4IREBAb97j8bMvThxClMNqFfU0rFZyXP+0MOyhFQyertswrgQ6T+Fi\n2mnvKD8CgYAmJHP3NTDRMoMRyAzonJ6nEaGUbAgNmivTaUWMe0+leCvAdwD89gzC\nTKbm3eDUg/6Va3X6ANh3wsfIOe4RXXxcbcFDk9R4zO2M5gfLSjYm5Q87EBZ2hrdj\nM2gLI7dt6thx0J8lR8xRHBEMrVBdgwp0g1gQzo5dAV88/kpkZVps8Q==\n-----END RSA PRIVATE KEY-----\n", + Certificate: "-----BEGIN CERTIFICATE-----\nMIIEXTCCA0WgAwIBAgIGATTEAjK3MA0GCSqGSIb3DQEBBQUAMIGDMRkwFwYDVQQD\nExBUZXN0IENBIFNUdWIgS2V5MRcwFQYDVQQLEw5QbGF0Zm9ybSBMYmFhczEaMBgG\nA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFDASBgNVBAcTC1NhbiBBbnRvbmlvMQ4w\nDAYDVQQIEwVUZXhhczELMAkGA1UEBhMCVVMwHhcNMTIwMTA5MTk0NjQ1WhcNMTQw\nMTA4MTk0NjQ1WjCBgjELMAkGA1UEBhMCVVMxDjAMBgNVBAgTBVRleGFzMRQwEgYD\nVQQHEwtTYW4gQW50b25pbzEaMBgGA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFzAV\nBgNVBAsTDlBsYXRmb3JtIExiYWFzMRgwFgYDVQQDEw9UZXN0IENsaWVudCBLZXkw\nggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDAi51IylFnHtNLT8C0NVfc\nOBfAsP2D5es1qhrOWHCGlgAuDMksBsCc7FPo5PSBOmQ+6z8HtCFbrLoC5/Zx0F5b\nfVegjA+xKjI2HGASsYHHM0BFEH2UjUcJrWiMWtxQuW6Phbqulo7JwjmygMEmIkeK\nf+FtkE9mrq+E8K40/thrjxl3/ZcJD1+3dcp+ZuzVJ2t1E4iGKCx79IZFsysKiuf+\n+E0i6iGvvI6UcbcZxVxQj2TplJkFuoX5kDgClIX9Dr9y6aJ4SCh+GRhvHl+DTaz0\nnCvghachHZtIeztRDqxWApjOOzs93dSelrviMXDr8fqyEAGg7YIhgui0aZBsWCen\nAgMBAAGjgdUwgdIwgbAGA1UdIwSBqDCBpYAUNpx1Pc6cGA7KqEwHMmHBTZMA7lSh\ngYmkgYYwgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVU4IBATAd\nBgNVHQ4EFgQULueOfsjZZOHwJHZwBy6u0swnpccwDQYJKoZIhvcNAQEFBQADggEB\nAFNuqSVUaotUJoWDv4z7Kbi6JFpTjDht5ORw4BdVYlRD4h9DACAFzPrPV2ym/Osp\nhNMdZq6msZku7MdOSQVhdeGWrSNk3M8O9Hg7cVzPNXOF3iNoo3irQ5tURut44xs4\nWw5YWQqS9WyUY5snD8tm7Y1rQTPfhg+678xIq/zWCv/u+FSnfVv1nlhLVQkEeG/Y\ngh1uMaTIpUKTGEjIAGtpGP7wwIcXptR/HyfzhTUSTaWc1Ef7zoKT9LL5z3IV1hC2\njVWz+RwYs98LjMuksJFoHqRfWyYhCIym0jb6GTwaEmpxAjc+d7OLNQdnoEGoUYGP\nYjtfkRYg265ESMA+Kww4Xy8=\n-----END CERTIFICATE-----\n", + IntCertificate: "-----BEGIN CERTIFICATE-----\nMIIDtTCCAp2gAwIBAgIBATANBgkqhkiG9w0BAQUFADCBgzEZMBcGA1UEAxMQVGVz\ndCBDQSBTVHViIEtleTEXMBUGA1UECxMOUGxhdGZvcm0gTGJhYXMxGjAYBgNVBAoT\nEVJhY2tzcGFjZSBIb3N0aW5nMRQwEgYDVQQHEwtTYW4gQW50b25pbzEOMAwGA1UE\nCBMFVGV4YXMxCzAJBgNVBAYTAlVTMB4XDTEyMDEwOTE5NDU0OVoXDTE0MDEwODE5\nNDU0OVowgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVUzCCASIw\nDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANNh55lwTVwQvNoEZjq1zGdYz9jA\nXXdjizn8AJhjHLOAallfPtvCfTEgKanhdoyz5FnhQE8HbDAop/KNS1lN2UMvdl5f\nZNLTSjJrNtedqxQwxN/i3bpyBxNVejUH2NjV1mmyj+5CJYwCzWalvI/gLPq/A3as\nO2EQqtf3U8unRgn0zXLRdYxV9MrUzNAmdipPNvNrsVdrCgA42rgF/8KsyRVQfJCX\nfN7PGCfrsC3YaUvhymraWxNnXIzMYTNa9wEeBZLUw8SlEtpa1Zsvui+TPXu3USNZ\nVnWH8Lb6ENlnoX0VBwo62fjOG3JzhNKoJawi3bRqyDdINOvafr7iPrrs/T8CAwEA\nAaMyMDAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUNpx1Pc6cGA7KqEwHMmHB\nTZMA7lQwDQYJKoZIhvcNAQEFBQADggEBAMoRgH3iTG3t317viLKoY+lNMHUgHuR7\nb3mn9MidJKyYVewe6hCDIN6WY4fUojmMW9wFJWJIo0hRMNHL3n3tq8HP2j20Mxy8\nacPdfGZJa+jiBw72CrIGdobKaFduIlIEDBA1pNdZIJ+EulrtqrMesnIt92WaypIS\n8JycbIgDMCiyC0ENHEk8UWlC6429c7OZAsplMTbHME/1R4btxjkdfrYZJjdJ2yL2\n8cjZDUDMCPTdW/ycP07Gkq30RB5tACB5aZdaCn2YaKC8FsEdhff4X7xEOfOEHWEq\nSRxADDj8Lx1MT6QpR07hCiDyHfTCtbqzI0iGjX63Oh7xXSa0f+JVTa8=\n-----END CERTIFICATE-----\n", + } + + cm, err := UpdateCert(client.ServiceClient(), lbID, certID, opts).Extract() + th.AssertNoErr(t, err) + + expected := &Certificate{ + ID: 123, + HostName: "rackspace.com", + Certificate: "-----BEGIN CERTIFICATE-----\nMIIEXTCCA0WgAwIBAgIGATTEAjK3MA0GCSqGSIb3DQEBBQUAMIGDMRkwFwYDVQQD\nExBUZXN0IENBIFNUdWIgS2V5MRcwFQYDVQQLEw5QbGF0Zm9ybSBMYmFhczEaMBgG\nA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFDASBgNVBAcTC1NhbiBBbnRvbmlvMQ4w\nDAYDVQQIEwVUZXhhczELMAkGA1UEBhMCVVMwHhcNMTIwMTA5MTk0NjQ1WhcNMTQw\nMTA4MTk0NjQ1WjCBgjELMAkGA1UEBhMCVVMxDjAMBgNVBAgTBVRleGFzMRQwEgYD\nVQQHEwtTYW4gQW50b25pbzEaMBgGA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFzAV\nBgNVBAsTDlBsYXRmb3JtIExiYWFzMRgwFgYDVQQDEw9UZXN0IENsaWVudCBLZXkw\nggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDAi51IylFnHtNLT8C0NVfc\nOBfAsP2D5es1qhrOWHCGlgAuDMksBsCc7FPo5PSBOmQ+6z8HtCFbrLoC5/Zx0F5b\nfVegjA+xKjI2HGASsYHHM0BFEH2UjUcJrWiMWtxQuW6Phbqulo7JwjmygMEmIkeK\nf+FtkE9mrq+E8K40/thrjxl3/ZcJD1+3dcp+ZuzVJ2t1E4iGKCx79IZFsysKiuf+\n+E0i6iGvvI6UcbcZxVxQj2TplJkFuoX5kDgClIX9Dr9y6aJ4SCh+GRhvHl+DTaz0\nnCvghachHZtIeztRDqxWApjOOzs93dSelrviMXDr8fqyEAGg7YIhgui0aZBsWCen\nAgMBAAGjgdUwgdIwgbAGA1UdIwSBqDCBpYAUNpx1Pc6cGA7KqEwHMmHBTZMA7lSh\ngYmkgYYwgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVU4IBATAd\nBgNVHQ4EFgQULueOfsjZZOHwJHZwBy6u0swnpccwDQYJKoZIhvcNAQEFBQADggEB\nAFNuqSVUaotUJoWDv4z7Kbi6JFpTjDht5ORw4BdVYlRD4h9DACAFzPrPV2ym/Osp\nhNMdZq6msZku7MdOSQVhdeGWrSNk3M8O9Hg7cVzPNXOF3iNoo3irQ5tURut44xs4\nWw5YWQqS9WyUY5snD8tm7Y1rQTPfhg+678xIq/zWCv/u+FSnfVv1nlhLVQkEeG/Y\ngh1uMaTIpUKTGEjIAGtpGP7wwIcXptR/HyfzhTUSTaWc1Ef7zoKT9LL5z3IV1hC2\njVWz+RwYs98LjMuksJFoHqRfWyYhCIym0jb6GTwaEmpxAjc+d7OLNQdnoEGoUYGP\nYjtfkRYg265ESMA+Kww4Xy8=\n-----END CERTIFICATE-----\n", + IntCertificate: "-----BEGIN CERTIFICATE-----\nMIIDtTCCAp2gAwIBAgIBATANBgkqhkiG9w0BAQUFADCBgzEZMBcGA1UEAxMQVGVz\ndCBDQSBTVHViIEtleTEXMBUGA1UECxMOUGxhdGZvcm0gTGJhYXMxGjAYBgNVBAoT\nEVJhY2tzcGFjZSBIb3N0aW5nMRQwEgYDVQQHEwtTYW4gQW50b25pbzEOMAwGA1UE\nCBMFVGV4YXMxCzAJBgNVBAYTAlVTMB4XDTEyMDEwOTE5NDU0OVoXDTE0MDEwODE5\nNDU0OVowgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVUzCCASIw\nDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANNh55lwTVwQvNoEZjq1zGdYz9jA\nXXdjizn8AJhjHLOAallfPtvCfTEgKanhdoyz5FnhQE8HbDAop/KNS1lN2UMvdl5f\nZNLTSjJrNtedqxQwxN/i3bpyBxNVejUH2NjV1mmyj+5CJYwCzWalvI/gLPq/A3as\nO2EQqtf3U8unRgn0zXLRdYxV9MrUzNAmdipPNvNrsVdrCgA42rgF/8KsyRVQfJCX\nfN7PGCfrsC3YaUvhymraWxNnXIzMYTNa9wEeBZLUw8SlEtpa1Zsvui+TPXu3USNZ\nVnWH8Lb6ENlnoX0VBwo62fjOG3JzhNKoJawi3bRqyDdINOvafr7iPrrs/T8CAwEA\nAaMyMDAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUNpx1Pc6cGA7KqEwHMmHB\nTZMA7lQwDQYJKoZIhvcNAQEFBQADggEBAMoRgH3iTG3t317viLKoY+lNMHUgHuR7\nb3mn9MidJKyYVewe6hCDIN6WY4fUojmMW9wFJWJIo0hRMNHL3n3tq8HP2j20Mxy8\nacPdfGZJa+jiBw72CrIGdobKaFduIlIEDBA1pNdZIJ+EulrtqrMesnIt92WaypIS\n8JycbIgDMCiyC0ENHEk8UWlC6429c7OZAsplMTbHME/1R4btxjkdfrYZJjdJ2yL2\n8cjZDUDMCPTdW/ycP07Gkq30RB5tACB5aZdaCn2YaKC8FsEdhff4X7xEOfOEHWEq\nSRxADDj8Lx1MT6QpR07hCiDyHfTCtbqzI0iGjX63Oh7xXSa0f+JVTa8=\n-----END CERTIFICATE-----\n", + } + th.AssertDeepEquals(t, expected, cm) +} + +func TestDeleteCert(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockDeleteCertResponse(t, lbID, certID) + + err := DeleteCert(client.ServiceClient(), lbID, certID).ExtractErr() + th.AssertNoErr(t, err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/ssl/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/ssl/results.go new file mode 100644 index 0000000000..ead9fcd37e --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/ssl/results.go @@ -0,0 +1,148 @@ +package ssl + +import ( + "github.com/mitchellh/mapstructure" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// SSLTermConfig represents the SSL configuration for a particular load balancer. +type SSLTermConfig struct { + // The port on which the SSL termination load balancer listens for secure + // traffic. The value must be unique to the existing LB protocol/port + // combination + SecurePort int `mapstructure:"securePort"` + + // The private key for the SSL certificate which is validated and verified + // against the provided certificates. + PrivateKey string `mapstructure:"privatekey"` + + // The certificate used for SSL termination, which is validated and verified + // against the key and intermediate certificate if provided. + Certificate string + + // The intermediate certificate (for the user). The intermediate certificate + // is validated and verified against the key and certificate credentials + // provided. A user may only provide this value when accompanied by a + // Certificate, PrivateKey, and SecurePort. It may not be added or updated as + // a single attribute in a future operation. + IntCertificate string `mapstructure:"intermediatecertificate"` + + // Determines if the load balancer is enabled to terminate SSL traffic or not. + // If this is set to false, the load balancer retains its specified SSL + // attributes but does not terminate SSL traffic. + Enabled bool + + // Determines if the load balancer can only accept secure traffic. If set to + // true, the load balancer will not accept non-secure traffic. + SecureTrafficOnly bool +} + +// DeleteResult represents the result of a delete operation. +type DeleteResult struct { + gophercloud.ErrResult +} + +// UpdateResult represents the result of an update operation. +type UpdateResult struct { + gophercloud.ErrResult +} + +// GetResult represents the result of a get operation. +type GetResult struct { + gophercloud.Result +} + +// Extract interprets a GetResult as a SSLTermConfig struct, if possible. +func (r GetResult) Extract() (*SSLTermConfig, error) { + if r.Err != nil { + return nil, r.Err + } + + var response struct { + SSL SSLTermConfig `mapstructure:"sslTermination"` + } + + err := mapstructure.Decode(r.Body, &response) + + return &response.SSL, err +} + +// Certificate represents an SSL certificate associated with an SSL-terminated +// HTTP load balancer. +type Certificate struct { + ID int + HostName string + Certificate string + IntCertificate string `mapstructure:"intermediateCertificate"` +} + +// CertPage represents a page of certificates. +type CertPage struct { + pagination.LinkedPageBase +} + +// IsEmpty checks whether a CertMappingPage struct is empty. +func (p CertPage) IsEmpty() (bool, error) { + is, err := ExtractCerts(p) + if err != nil { + return true, nil + } + return len(is) == 0, nil +} + +// ExtractCerts accepts a Page struct, specifically a CertPage struct, and +// extracts the elements into a slice of Cert structs. In other words, a generic +// collection is mapped into a relevant slice. +func ExtractCerts(page pagination.Page) ([]Certificate, error) { + type NestedMap struct { + Cert Certificate `mapstructure:"certificateMapping" json:"certificateMapping"` + } + var resp struct { + Certs []NestedMap `mapstructure:"certificateMappings" json:"certificateMappings"` + } + + err := mapstructure.Decode(page.(CertPage).Body, &resp) + + slice := []Certificate{} + for _, cert := range resp.Certs { + slice = append(slice, cert.Cert) + } + + return slice, err +} + +type certResult struct { + gophercloud.Result +} + +// Extract interprets a result as a CertMapping struct, if possible. +func (r certResult) Extract() (*Certificate, error) { + if r.Err != nil { + return nil, r.Err + } + + var response struct { + Cert Certificate `mapstructure:"certificateMapping"` + } + + err := mapstructure.Decode(r.Body, &response) + + return &response.Cert, err +} + +// CreateCertResult represents the result of an CreateCert operation. +type CreateCertResult struct { + certResult +} + +// GetCertResult represents the result of a GetCert operation. +type GetCertResult struct { + certResult +} + +// UpdateCertResult represents the result of an UpdateCert operation. +type UpdateCertResult struct { + certResult +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/ssl/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/ssl/urls.go new file mode 100644 index 0000000000..aa814b3583 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/ssl/urls.go @@ -0,0 +1,25 @@ +package ssl + +import ( + "strconv" + + "github.com/rackspace/gophercloud" +) + +const ( + path = "loadbalancers" + sslPath = "ssltermination" + certPath = "certificatemappings" +) + +func rootURL(c *gophercloud.ServiceClient, id int) string { + return c.ServiceURL(path, strconv.Itoa(id), sslPath) +} + +func certURL(c *gophercloud.ServiceClient, id int) string { + return c.ServiceURL(path, strconv.Itoa(id), sslPath, certPath) +} + +func certResourceURL(c *gophercloud.ServiceClient, id, certID int) string { + return c.ServiceURL(path, strconv.Itoa(id), sslPath, certPath, strconv.Itoa(certID)) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/throttle/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/throttle/doc.go new file mode 100644 index 0000000000..1ed605d362 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/throttle/doc.go @@ -0,0 +1,5 @@ +/* +Package throttle provides information and interaction with the Connection +Throttling feature of the Rackspace Cloud Load Balancer service. +*/ +package throttle diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/throttle/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/throttle/fixtures.go new file mode 100644 index 0000000000..40223f60a6 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/throttle/fixtures.go @@ -0,0 +1,61 @@ +package throttle + +import ( + "fmt" + "net/http" + "strconv" + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +func _rootURL(id int) string { + return "/loadbalancers/" + strconv.Itoa(id) + "/connectionthrottle" +} + +func mockGetResponse(t *testing.T, lbID int) { + th.Mux.HandleFunc(_rootURL(lbID), func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "connectionThrottle": { + "maxConnections": 100 + } +} +`) + }) +} + +func mockCreateResponse(t *testing.T, lbID int) { + th.Mux.HandleFunc(_rootURL(lbID), func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + th.TestJSONRequest(t, r, ` +{ + "connectionThrottle": { + "maxConnectionRate": 0, + "maxConnections": 200, + "minConnections": 0, + "rateInterval": 0 + } +} + `) + + w.WriteHeader(http.StatusAccepted) + }) +} + +func mockDeleteResponse(t *testing.T, lbID int) { + th.Mux.HandleFunc(_rootURL(lbID), func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusAccepted) + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/throttle/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/throttle/requests.go new file mode 100644 index 0000000000..8c2e4be415 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/throttle/requests.go @@ -0,0 +1,95 @@ +package throttle + +import ( + "errors" + + "github.com/racker/perigee" + + "github.com/rackspace/gophercloud" +) + +// CreateOptsBuilder is the interface options structs have to satisfy in order +// to be used in the main Create operation in this package. +type CreateOptsBuilder interface { + ToCTCreateMap() (map[string]interface{}, error) +} + +// CreateOpts is the common options struct used in this package's Create +// operation. +type CreateOpts struct { + // Required - the maximum amount of connections per IP address to allow per LB. + MaxConnections int + + // Deprecated as of v1.22. + MaxConnectionRate int + + // Deprecated as of v1.22. + MinConnections int + + // Deprecated as of v1.22. + RateInterval int +} + +// ToCTCreateMap casts a CreateOpts struct to a map. +func (opts CreateOpts) ToCTCreateMap() (map[string]interface{}, error) { + ct := make(map[string]interface{}) + + if opts.MaxConnections < 0 || opts.MaxConnections > 100000 { + return ct, errors.New("MaxConnections must be an int between 0 and 100000") + } + + ct["maxConnections"] = opts.MaxConnections + ct["maxConnectionRate"] = opts.MaxConnectionRate + ct["minConnections"] = opts.MinConnections + ct["rateInterval"] = opts.RateInterval + + return map[string]interface{}{"connectionThrottle": ct}, nil +} + +// Create is the operation responsible for creating or updating the connection +// throttling configuration for a load balancer. +func Create(c *gophercloud.ServiceClient, lbID int, opts CreateOptsBuilder) CreateResult { + var res CreateResult + + reqBody, err := opts.ToCTCreateMap() + if err != nil { + res.Err = err + return res + } + + _, res.Err = perigee.Request("PUT", rootURL(c, lbID), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + ReqBody: &reqBody, + Results: &res.Body, + OkCodes: []int{202}, + }) + + return res +} + +// Get is the operation responsible for showing the details of the connection +// throttling configuration for a load balancer. +func Get(c *gophercloud.ServiceClient, lbID int) GetResult { + var res GetResult + + _, res.Err = perigee.Request("GET", rootURL(c, lbID), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + Results: &res.Body, + OkCodes: []int{200}, + }) + + return res +} + +// Delete is the operation responsible for deleting the connection throttling +// configuration for a load balancer. +func Delete(c *gophercloud.ServiceClient, lbID int) DeleteResult { + var res DeleteResult + + _, res.Err = perigee.Request("DELETE", rootURL(c, lbID), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + OkCodes: []int{202}, + }) + + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/throttle/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/throttle/requests_test.go new file mode 100644 index 0000000000..6e9703ffce --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/throttle/requests_test.go @@ -0,0 +1,44 @@ +package throttle + +import ( + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +const lbID = 12345 + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockCreateResponse(t, lbID) + + opts := CreateOpts{MaxConnections: 200} + err := Create(client.ServiceClient(), lbID, opts).ExtractErr() + th.AssertNoErr(t, err) +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockGetResponse(t, lbID) + + sp, err := Get(client.ServiceClient(), lbID).Extract() + th.AssertNoErr(t, err) + + expected := &ConnectionThrottle{MaxConnections: 100} + th.AssertDeepEquals(t, expected, sp) +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockDeleteResponse(t, lbID) + + err := Delete(client.ServiceClient(), lbID).ExtractErr() + th.AssertNoErr(t, err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/throttle/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/throttle/results.go new file mode 100644 index 0000000000..db93c6f3f4 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/throttle/results.go @@ -0,0 +1,43 @@ +package throttle + +import ( + "github.com/mitchellh/mapstructure" + + "github.com/rackspace/gophercloud" +) + +// ConnectionThrottle represents the connection throttle configuration for a +// particular load balancer. +type ConnectionThrottle struct { + MaxConnections int +} + +// CreateResult represents the result of a create operation. +type CreateResult struct { + gophercloud.ErrResult +} + +// DeleteResult represents the result of a delete operation. +type DeleteResult struct { + gophercloud.ErrResult +} + +// GetResult represents the result of a get operation. +type GetResult struct { + gophercloud.Result +} + +// Extract interprets a GetResult as a SP, if possible. +func (r GetResult) Extract() (*ConnectionThrottle, error) { + if r.Err != nil { + return nil, r.Err + } + + var response struct { + CT ConnectionThrottle `mapstructure:"connectionThrottle"` + } + + err := mapstructure.Decode(r.Body, &response) + + return &response.CT, err +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/throttle/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/throttle/urls.go new file mode 100644 index 0000000000..b77f0ac1c7 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/throttle/urls.go @@ -0,0 +1,16 @@ +package throttle + +import ( + "strconv" + + "github.com/rackspace/gophercloud" +) + +const ( + path = "loadbalancers" + ctPath = "connectionthrottle" +) + +func rootURL(c *gophercloud.ServiceClient, id int) string { + return c.ServiceURL(path, strconv.Itoa(id), ctPath) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/vips/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/vips/doc.go new file mode 100644 index 0000000000..5c3846d44d --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/vips/doc.go @@ -0,0 +1,13 @@ +/* +Package vips provides information and interaction with the Virtual IP API +resource for the Rackspace Cloud Load Balancer service. + +A virtual IP (VIP) makes a load balancer accessible by clients. The load +balancing service supports either a public VIP, routable on the public Internet, +or a ServiceNet address, routable only within the region in which the load +balancer resides. + +All load balancers must have at least one virtual IP associated with them at +all times. +*/ +package vips diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/vips/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/vips/fixtures.go new file mode 100644 index 0000000000..158759f7fa --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/vips/fixtures.go @@ -0,0 +1,88 @@ +package vips + +import ( + "fmt" + "net/http" + "strconv" + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +func _rootURL(lbID int) string { + return "/loadbalancers/" + strconv.Itoa(lbID) + "/virtualips" +} + +func mockListResponse(t *testing.T, lbID int) { + th.Mux.HandleFunc(_rootURL(lbID), func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "virtualIps": [ + { + "id": 1000, + "address": "206.10.10.210", + "type": "PUBLIC" + } + ] +} + `) + }) +} + +func mockCreateResponse(t *testing.T, lbID int) { + th.Mux.HandleFunc(_rootURL(lbID), func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + th.TestJSONRequest(t, r, ` +{ + "type":"PUBLIC", + "ipVersion":"IPV6" +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusAccepted) + + fmt.Fprintf(w, ` +{ + "address":"fd24:f480:ce44:91bc:1af2:15ff:0000:0002", + "id":9000134, + "type":"PUBLIC", + "ipVersion":"IPV6" +} + `) + }) +} + +func mockBatchDeleteResponse(t *testing.T, lbID int, ids []int) { + th.Mux.HandleFunc(_rootURL(lbID), func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + r.ParseForm() + + for k, v := range ids { + fids := r.Form["id"] + th.AssertEquals(t, strconv.Itoa(v), fids[k]) + } + + w.WriteHeader(http.StatusAccepted) + }) +} + +func mockDeleteResponse(t *testing.T, lbID, vipID int) { + url := _rootURL(lbID) + "/" + strconv.Itoa(vipID) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusAccepted) + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/vips/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/vips/requests.go new file mode 100644 index 0000000000..42f0c1d071 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/vips/requests.go @@ -0,0 +1,112 @@ +package vips + +import ( + "errors" + + "github.com/racker/perigee" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// List is the operation responsible for returning a paginated collection of +// load balancer virtual IP addresses. +func List(client *gophercloud.ServiceClient, loadBalancerID int) pagination.Pager { + url := rootURL(client, loadBalancerID) + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return VIPPage{pagination.SinglePageBase(r)} + }) +} + +// CreateOptsBuilder is the interface options structs have to satisfy in order +// to be used in the main Create operation in this package. Since many +// extensions decorate or modify the common logic, it is useful for them to +// satisfy a basic interface in order for them to be used. +type CreateOptsBuilder interface { + ToVIPCreateMap() (map[string]interface{}, error) +} + +// CreateOpts is the common options struct used in this package's Create +// operation. +type CreateOpts struct { + // Optional - the ID of an existing virtual IP. By doing this, you are + // allowing load balancers to share IPV6 addresses. + ID string + + // Optional - the type of address. + Type Type + + // Optional - the version of address. + Version Version +} + +// ToVIPCreateMap casts a CreateOpts struct to a map. +func (opts CreateOpts) ToVIPCreateMap() (map[string]interface{}, error) { + lb := make(map[string]interface{}) + + if opts.ID != "" { + lb["id"] = opts.ID + } + if opts.Type != "" { + lb["type"] = opts.Type + } + if opts.Version != "" { + lb["ipVersion"] = opts.Version + } + + return lb, nil +} + +// Create is the operation responsible for assigning a new Virtual IP to an +// existing load balancer resource. Currently, only version 6 IP addresses may +// be added. +func Create(c *gophercloud.ServiceClient, lbID int, opts CreateOptsBuilder) CreateResult { + var res CreateResult + + reqBody, err := opts.ToVIPCreateMap() + if err != nil { + res.Err = err + return res + } + + _, res.Err = perigee.Request("POST", rootURL(c, lbID), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + ReqBody: &reqBody, + Results: &res.Body, + OkCodes: []int{202}, + }) + + return res +} + +// BulkDelete is the operation responsible for batch deleting multiple VIPs in +// a single operation. It accepts a slice of integer IDs and will remove them +// from the load balancer. The maximum limit is 10 VIP removals at once. +func BulkDelete(c *gophercloud.ServiceClient, loadBalancerID int, vipIDs []int) DeleteResult { + var res DeleteResult + + if len(vipIDs) > 10 || len(vipIDs) == 0 { + res.Err = errors.New("You must provide a minimum of 1 and a maximum of 10 VIP IDs") + return res + } + + url := rootURL(c, loadBalancerID) + url += gophercloud.IDSliceToQueryString("id", vipIDs) + + _, res.Err = perigee.Request("DELETE", url, perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + OkCodes: []int{202}, + }) + + return res +} + +// Delete is the operation responsible for permanently deleting a VIP. +func Delete(c *gophercloud.ServiceClient, lbID, vipID int) DeleteResult { + var res DeleteResult + _, res.Err = perigee.Request("DELETE", resourceURL(c, lbID, vipID), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + OkCodes: []int{202}, + }) + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/vips/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/vips/requests_test.go new file mode 100644 index 0000000000..74ac461738 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/vips/requests_test.go @@ -0,0 +1,87 @@ +package vips + +import ( + "testing" + + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +const ( + lbID = 12345 + vipID = 67890 + vipID2 = 67891 +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockListResponse(t, lbID) + + count := 0 + + err := List(client.ServiceClient(), lbID).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractVIPs(page) + th.AssertNoErr(t, err) + + expected := []VIP{ + VIP{ID: 1000, Address: "206.10.10.210", Type: "PUBLIC"}, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + + th.AssertNoErr(t, err) + th.AssertEquals(t, 1, count) +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockCreateResponse(t, lbID) + + opts := CreateOpts{ + Type: "PUBLIC", + Version: "IPV6", + } + + vip, err := Create(client.ServiceClient(), lbID, opts).Extract() + th.AssertNoErr(t, err) + + expected := &VIP{ + Address: "fd24:f480:ce44:91bc:1af2:15ff:0000:0002", + ID: 9000134, + Type: "PUBLIC", + Version: "IPV6", + } + + th.CheckDeepEquals(t, expected, vip) +} + +func TestBulkDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + ids := []int{vipID, vipID2} + + mockBatchDeleteResponse(t, lbID, ids) + + err := BulkDelete(client.ServiceClient(), lbID, ids).ExtractErr() + th.AssertNoErr(t, err) +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockDeleteResponse(t, lbID, vipID) + + err := Delete(client.ServiceClient(), lbID, vipID).ExtractErr() + th.AssertNoErr(t, err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/vips/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/vips/results.go new file mode 100644 index 0000000000..678b2aff79 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/vips/results.go @@ -0,0 +1,89 @@ +package vips + +import ( + "github.com/mitchellh/mapstructure" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// VIP represents a Virtual IP API resource. +type VIP struct { + Address string `json:"address,omitempty"` + ID int `json:"id,omitempty"` + Type Type `json:"type,omitempty"` + Version Version `json:"ipVersion,omitempty" mapstructure:"ipVersion"` +} + +// Version represents the version of a VIP. +type Version string + +// Convenient constants to use for type +const ( + IPV4 Version = "IPV4" + IPV6 Version = "IPV6" +) + +// Type represents the type of a VIP. +type Type string + +const ( + // PUBLIC indicates a VIP type that is routable on the public Internet. + PUBLIC Type = "PUBLIC" + + // PRIVATE indicates a VIP type that is routable only on ServiceNet. + PRIVATE Type = "SERVICENET" +) + +// VIPPage is the page returned by a pager when traversing over a collection +// of VIPs. +type VIPPage struct { + pagination.SinglePageBase +} + +// IsEmpty checks whether a VIPPage struct is empty. +func (p VIPPage) IsEmpty() (bool, error) { + is, err := ExtractVIPs(p) + if err != nil { + return true, nil + } + return len(is) == 0, nil +} + +// ExtractVIPs accepts a Page struct, specifically a VIPPage struct, and +// extracts the elements into a slice of VIP structs. In other words, a +// generic collection is mapped into a relevant slice. +func ExtractVIPs(page pagination.Page) ([]VIP, error) { + var resp struct { + VIPs []VIP `mapstructure:"virtualIps" json:"virtualIps"` + } + + err := mapstructure.Decode(page.(VIPPage).Body, &resp) + + return resp.VIPs, err +} + +type commonResult struct { + gophercloud.Result +} + +func (r commonResult) Extract() (*VIP, error) { + if r.Err != nil { + return nil, r.Err + } + + resp := &VIP{} + err := mapstructure.Decode(r.Body, resp) + + return resp, err +} + +// CreateResult represents the result of a create operation. +type CreateResult struct { + commonResult +} + +// DeleteResult represents the result of a delete operation. +type DeleteResult struct { + gophercloud.ErrResult +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/vips/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/vips/urls.go new file mode 100644 index 0000000000..28f063a0f7 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/vips/urls.go @@ -0,0 +1,20 @@ +package vips + +import ( + "strconv" + + "github.com/rackspace/gophercloud" +) + +const ( + lbPath = "loadbalancers" + vipPath = "virtualips" +) + +func resourceURL(c *gophercloud.ServiceClient, lbID, nodeID int) string { + return c.ServiceURL(lbPath, strconv.Itoa(lbID), vipPath, strconv.Itoa(nodeID)) +} + +func rootURL(c *gophercloud.ServiceClient, lbID int) string { + return c.ServiceURL(lbPath, strconv.Itoa(lbID), vipPath) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/networking/v2/common/common_tests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/networking/v2/common/common_tests.go new file mode 100644 index 0000000000..129cd63aee --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/networking/v2/common/common_tests.go @@ -0,0 +1,12 @@ +package common + +import ( + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/testhelper/client" +) + +const TokenID = client.TokenID + +func ServiceClient() *gophercloud.ServiceClient { + return client.ServiceClient() +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/networking/v2/networks/delegate.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/networking/v2/networks/delegate.go new file mode 100644 index 0000000000..dcb0855dba --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/networking/v2/networks/delegate.go @@ -0,0 +1,41 @@ +package networks + +import ( + "github.com/rackspace/gophercloud" + os "github.com/rackspace/gophercloud/openstack/networking/v2/networks" + "github.com/rackspace/gophercloud/pagination" +) + +// List returns a Pager which allows you to iterate over a collection of +// networks. It accepts a ListOpts struct, which allows you to filter and sort +// the returned collection for greater efficiency. +func List(c *gophercloud.ServiceClient, opts os.ListOptsBuilder) pagination.Pager { + return os.List(c, opts) +} + +// Get retrieves a specific network based on its unique ID. +func Get(c *gophercloud.ServiceClient, networkID string) os.GetResult { + return os.Get(c, networkID) +} + +// Create accepts a CreateOpts struct and creates a new network using the values +// provided. This operation does not actually require a request body, i.e. the +// CreateOpts struct argument can be empty. +// +// The tenant ID that is contained in the URI is the tenant that creates the +// network. An admin user, however, has the option of specifying another tenant +// ID in the CreateOpts struct. +func Create(c *gophercloud.ServiceClient, opts os.CreateOptsBuilder) os.CreateResult { + return os.Create(c, opts) +} + +// Update accepts a UpdateOpts struct and updates an existing network using the +// values provided. For more information, see the Create function. +func Update(c *gophercloud.ServiceClient, networkID string, opts os.UpdateOptsBuilder) os.UpdateResult { + return os.Update(c, networkID, opts) +} + +// Delete accepts a unique ID and deletes the network associated with it. +func Delete(c *gophercloud.ServiceClient, networkID string) os.DeleteResult { + return os.Delete(c, networkID) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/networking/v2/networks/delegate_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/networking/v2/networks/delegate_test.go new file mode 100644 index 0000000000..f51c732d43 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/networking/v2/networks/delegate_test.go @@ -0,0 +1,276 @@ +package networks + +import ( + "fmt" + "net/http" + "testing" + + os "github.com/rackspace/gophercloud/openstack/networking/v2/networks" + "github.com/rackspace/gophercloud/pagination" + fake "github.com/rackspace/gophercloud/rackspace/networking/v2/common" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/networks", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "networks": [ + { + "status": "ACTIVE", + "subnets": [ + "54d6f61d-db07-451c-9ab3-b9609b6b6f0b" + ], + "name": "private-network", + "admin_state_up": true, + "tenant_id": "4fd44f30292945e481c7b8a0c8908869", + "shared": true, + "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22" + }, + { + "status": "ACTIVE", + "subnets": [ + "08eae331-0402-425a-923c-34f7cfe39c1b" + ], + "name": "private", + "admin_state_up": true, + "tenant_id": "26a7980765d0414dbc1fc1f88cdb7e6e", + "shared": true, + "id": "db193ab3-96e3-4cb3-8fc5-05f4296d0324" + } + ] +} + `) + }) + + client := fake.ServiceClient() + count := 0 + + List(client, os.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := os.ExtractNetworks(page) + if err != nil { + t.Errorf("Failed to extract networks: %v", err) + return false, err + } + + expected := []os.Network{ + os.Network{ + Status: "ACTIVE", + Subnets: []string{"54d6f61d-db07-451c-9ab3-b9609b6b6f0b"}, + Name: "private-network", + AdminStateUp: true, + TenantID: "4fd44f30292945e481c7b8a0c8908869", + Shared: true, + ID: "d32019d3-bc6e-4319-9c1d-6722fc136a22", + }, + os.Network{ + Status: "ACTIVE", + Subnets: []string{"08eae331-0402-425a-923c-34f7cfe39c1b"}, + Name: "private", + AdminStateUp: true, + TenantID: "26a7980765d0414dbc1fc1f88cdb7e6e", + Shared: true, + ID: "db193ab3-96e3-4cb3-8fc5-05f4296d0324", + }, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/networks/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "network": { + "status": "ACTIVE", + "subnets": [ + "54d6f61d-db07-451c-9ab3-b9609b6b6f0b" + ], + "name": "private-network", + "admin_state_up": true, + "tenant_id": "4fd44f30292945e481c7b8a0c8908869", + "shared": true, + "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22" + } +} + `) + }) + + n, err := Get(fake.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22").Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, n.Status, "ACTIVE") + th.AssertDeepEquals(t, n.Subnets, []string{"54d6f61d-db07-451c-9ab3-b9609b6b6f0b"}) + th.AssertEquals(t, n.Name, "private-network") + th.AssertEquals(t, n.AdminStateUp, true) + th.AssertEquals(t, n.TenantID, "4fd44f30292945e481c7b8a0c8908869") + th.AssertEquals(t, n.Shared, true) + th.AssertEquals(t, n.ID, "d32019d3-bc6e-4319-9c1d-6722fc136a22") +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/networks", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "network": { + "name": "sample_network", + "admin_state_up": true + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, ` +{ + "network": { + "status": "ACTIVE", + "subnets": [], + "name": "net1", + "admin_state_up": true, + "tenant_id": "9bacb3c5d39d41a79512987f338cf177", + "shared": false, + "id": "4e8e5957-649f-477b-9e5b-f1f75b21c03c" + } +} + `) + }) + + iTrue := true + options := os.CreateOpts{Name: "sample_network", AdminStateUp: &iTrue} + n, err := Create(fake.ServiceClient(), options).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, n.Status, "ACTIVE") + th.AssertDeepEquals(t, n.Subnets, []string{}) + th.AssertEquals(t, n.Name, "net1") + th.AssertEquals(t, n.AdminStateUp, true) + th.AssertEquals(t, n.TenantID, "9bacb3c5d39d41a79512987f338cf177") + th.AssertEquals(t, n.Shared, false) + th.AssertEquals(t, n.ID, "4e8e5957-649f-477b-9e5b-f1f75b21c03c") +} + +func TestCreateWithOptionalFields(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/networks", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "network": { + "name": "sample_network", + "admin_state_up": true, + "shared": true, + "tenant_id": "12345" + } +} + `) + + w.WriteHeader(http.StatusCreated) + }) + + iTrue := true + options := os.CreateOpts{Name: "sample_network", AdminStateUp: &iTrue, Shared: &iTrue, TenantID: "12345"} + _, err := Create(fake.ServiceClient(), options).Extract() + th.AssertNoErr(t, err) +} + +func TestUpdate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/networks/4e8e5957-649f-477b-9e5b-f1f75b21c03c", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "network": { + "name": "new_network_name", + "admin_state_up": false, + "shared": true + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "network": { + "status": "ACTIVE", + "subnets": [], + "name": "new_network_name", + "admin_state_up": false, + "tenant_id": "4fd44f30292945e481c7b8a0c8908869", + "shared": true, + "id": "4e8e5957-649f-477b-9e5b-f1f75b21c03c" + } +} + `) + }) + + iTrue := true + options := os.UpdateOpts{Name: "new_network_name", AdminStateUp: os.Down, Shared: &iTrue} + n, err := Update(fake.ServiceClient(), "4e8e5957-649f-477b-9e5b-f1f75b21c03c", options).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, n.Name, "new_network_name") + th.AssertEquals(t, n.AdminStateUp, false) + th.AssertEquals(t, n.Shared, true) + th.AssertEquals(t, n.ID, "4e8e5957-649f-477b-9e5b-f1f75b21c03c") +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/networks/4e8e5957-649f-477b-9e5b-f1f75b21c03c", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusNoContent) + }) + + res := Delete(fake.ServiceClient(), "4e8e5957-649f-477b-9e5b-f1f75b21c03c") + th.AssertNoErr(t, res.Err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/networking/v2/ports/delegate.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/networking/v2/ports/delegate.go new file mode 100644 index 0000000000..091b99e0f4 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/networking/v2/ports/delegate.go @@ -0,0 +1,40 @@ +package ports + +import ( + "github.com/rackspace/gophercloud" + os "github.com/rackspace/gophercloud/openstack/networking/v2/ports" + "github.com/rackspace/gophercloud/pagination" +) + +// List returns a Pager which allows you to iterate over a collection of +// ports. It accepts a ListOpts struct, which allows you to filter and sort +// the returned collection for greater efficiency. +// +// Default policy settings return only those ports that are owned by the tenant +// who submits the request, unless the request is submitted by a user with +// administrative rights. +func List(c *gophercloud.ServiceClient, opts os.ListOptsBuilder) pagination.Pager { + return os.List(c, opts) +} + +// Get retrieves a specific port based on its unique ID. +func Get(c *gophercloud.ServiceClient, networkID string) os.GetResult { + return os.Get(c, networkID) +} + +// Create accepts a CreateOpts struct and creates a new network using the values +// provided. You must remember to provide a NetworkID value. +func Create(c *gophercloud.ServiceClient, opts os.CreateOptsBuilder) os.CreateResult { + return os.Create(c, opts) +} + +// Update accepts a UpdateOpts struct and updates an existing port using the +// values provided. +func Update(c *gophercloud.ServiceClient, networkID string, opts os.UpdateOptsBuilder) os.UpdateResult { + return os.Update(c, networkID, opts) +} + +// Delete accepts a unique ID and deletes the port associated with it. +func Delete(c *gophercloud.ServiceClient, networkID string) os.DeleteResult { + return os.Delete(c, networkID) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/networking/v2/ports/delegate_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/networking/v2/ports/delegate_test.go new file mode 100644 index 0000000000..f53ff595a0 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/networking/v2/ports/delegate_test.go @@ -0,0 +1,322 @@ +package ports + +import ( + "fmt" + "net/http" + "testing" + + os "github.com/rackspace/gophercloud/openstack/networking/v2/ports" + "github.com/rackspace/gophercloud/pagination" + fake "github.com/rackspace/gophercloud/rackspace/networking/v2/common" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/ports", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "ports": [ + { + "status": "ACTIVE", + "binding:host_id": "devstack", + "name": "", + "admin_state_up": true, + "network_id": "70c1db1f-b701-45bd-96e0-a313ee3430b3", + "tenant_id": "", + "device_owner": "network:router_gateway", + "mac_address": "fa:16:3e:58:42:ed", + "fixed_ips": [ + { + "subnet_id": "008ba151-0b8c-4a67-98b5-0d2b87666062", + "ip_address": "172.24.4.2" + } + ], + "id": "d80b1a3b-4fc1-49f3-952e-1e2ab7081d8b", + "security_groups": [], + "device_id": "9ae135f4-b6e0-4dad-9e91-3c223e385824" + } + ] +} + `) + }) + + count := 0 + + List(fake.ServiceClient(), os.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := os.ExtractPorts(page) + if err != nil { + t.Errorf("Failed to extract subnets: %v", err) + return false, nil + } + + expected := []os.Port{ + os.Port{ + Status: "ACTIVE", + Name: "", + AdminStateUp: true, + NetworkID: "70c1db1f-b701-45bd-96e0-a313ee3430b3", + TenantID: "", + DeviceOwner: "network:router_gateway", + MACAddress: "fa:16:3e:58:42:ed", + FixedIPs: []os.IP{ + os.IP{ + SubnetID: "008ba151-0b8c-4a67-98b5-0d2b87666062", + IPAddress: "172.24.4.2", + }, + }, + ID: "d80b1a3b-4fc1-49f3-952e-1e2ab7081d8b", + SecurityGroups: []string{}, + DeviceID: "9ae135f4-b6e0-4dad-9e91-3c223e385824", + }, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/ports/46d4bfb9-b26e-41f3-bd2e-e6dcc1ccedb2", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "port": { + "status": "ACTIVE", + "name": "", + "admin_state_up": true, + "network_id": "a87cc70a-3e15-4acf-8205-9b711a3531b7", + "tenant_id": "7e02058126cc4950b75f9970368ba177", + "device_owner": "network:router_interface", + "mac_address": "fa:16:3e:23:fd:d7", + "fixed_ips": [ + { + "subnet_id": "a0304c3a-4f08-4c43-88af-d796509c97d2", + "ip_address": "10.0.0.1" + } + ], + "id": "46d4bfb9-b26e-41f3-bd2e-e6dcc1ccedb2", + "security_groups": [], + "device_id": "5e3898d7-11be-483e-9732-b2f5eccd2b2e" + } +} + `) + }) + + n, err := Get(fake.ServiceClient(), "46d4bfb9-b26e-41f3-bd2e-e6dcc1ccedb2").Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, n.Status, "ACTIVE") + th.AssertEquals(t, n.Name, "") + th.AssertEquals(t, n.AdminStateUp, true) + th.AssertEquals(t, n.NetworkID, "a87cc70a-3e15-4acf-8205-9b711a3531b7") + th.AssertEquals(t, n.TenantID, "7e02058126cc4950b75f9970368ba177") + th.AssertEquals(t, n.DeviceOwner, "network:router_interface") + th.AssertEquals(t, n.MACAddress, "fa:16:3e:23:fd:d7") + th.AssertDeepEquals(t, n.FixedIPs, []os.IP{ + os.IP{SubnetID: "a0304c3a-4f08-4c43-88af-d796509c97d2", IPAddress: "10.0.0.1"}, + }) + th.AssertEquals(t, n.ID, "46d4bfb9-b26e-41f3-bd2e-e6dcc1ccedb2") + th.AssertDeepEquals(t, n.SecurityGroups, []string{}) + th.AssertEquals(t, n.Status, "ACTIVE") + th.AssertEquals(t, n.DeviceID, "5e3898d7-11be-483e-9732-b2f5eccd2b2e") +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/ports", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "port": { + "network_id": "a87cc70a-3e15-4acf-8205-9b711a3531b7", + "name": "private-port", + "admin_state_up": true, + "fixed_ips": [ + { + "subnet_id": "a0304c3a-4f08-4c43-88af-d796509c97d2", + "ip_address": "10.0.0.2" + } + ], + "security_groups": ["foo"] + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, ` +{ + "port": { + "status": "DOWN", + "name": "private-port", + "allowed_address_pairs": [], + "admin_state_up": true, + "network_id": "a87cc70a-3e15-4acf-8205-9b711a3531b7", + "tenant_id": "d6700c0c9ffa4f1cb322cd4a1f3906fa", + "device_owner": "", + "mac_address": "fa:16:3e:c9:cb:f0", + "fixed_ips": [ + { + "subnet_id": "a0304c3a-4f08-4c43-88af-d796509c97d2", + "ip_address": "10.0.0.2" + } + ], + "id": "65c0ee9f-d634-4522-8954-51021b570b0d", + "security_groups": [ + "f0ac4394-7e4a-4409-9701-ba8be283dbc3" + ], + "device_id": "" + } +} + `) + }) + + asu := true + options := os.CreateOpts{ + Name: "private-port", + AdminStateUp: &asu, + NetworkID: "a87cc70a-3e15-4acf-8205-9b711a3531b7", + FixedIPs: []os.IP{ + os.IP{SubnetID: "a0304c3a-4f08-4c43-88af-d796509c97d2", IPAddress: "10.0.0.2"}, + }, + SecurityGroups: []string{"foo"}, + } + n, err := Create(fake.ServiceClient(), options).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, n.Status, "DOWN") + th.AssertEquals(t, n.Name, "private-port") + th.AssertEquals(t, n.AdminStateUp, true) + th.AssertEquals(t, n.NetworkID, "a87cc70a-3e15-4acf-8205-9b711a3531b7") + th.AssertEquals(t, n.TenantID, "d6700c0c9ffa4f1cb322cd4a1f3906fa") + th.AssertEquals(t, n.DeviceOwner, "") + th.AssertEquals(t, n.MACAddress, "fa:16:3e:c9:cb:f0") + th.AssertDeepEquals(t, n.FixedIPs, []os.IP{ + os.IP{SubnetID: "a0304c3a-4f08-4c43-88af-d796509c97d2", IPAddress: "10.0.0.2"}, + }) + th.AssertEquals(t, n.ID, "65c0ee9f-d634-4522-8954-51021b570b0d") + th.AssertDeepEquals(t, n.SecurityGroups, []string{"f0ac4394-7e4a-4409-9701-ba8be283dbc3"}) +} + +func TestRequiredCreateOpts(t *testing.T) { + res := Create(fake.ServiceClient(), os.CreateOpts{}) + if res.Err == nil { + t.Fatalf("Expected error, got none") + } +} + +func TestUpdate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/ports/65c0ee9f-d634-4522-8954-51021b570b0d", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "port": { + "name": "new_port_name", + "fixed_ips": [ + { + "subnet_id": "a0304c3a-4f08-4c43-88af-d796509c97d2", + "ip_address": "10.0.0.3" + } + ], + "security_groups": [ + "f0ac4394-7e4a-4409-9701-ba8be283dbc3" + ] + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "port": { + "status": "DOWN", + "name": "new_port_name", + "admin_state_up": true, + "network_id": "a87cc70a-3e15-4acf-8205-9b711a3531b7", + "tenant_id": "d6700c0c9ffa4f1cb322cd4a1f3906fa", + "device_owner": "", + "mac_address": "fa:16:3e:c9:cb:f0", + "fixed_ips": [ + { + "subnet_id": "a0304c3a-4f08-4c43-88af-d796509c97d2", + "ip_address": "10.0.0.3" + } + ], + "id": "65c0ee9f-d634-4522-8954-51021b570b0d", + "security_groups": [ + "f0ac4394-7e4a-4409-9701-ba8be283dbc3" + ], + "device_id": "" + } +} + `) + }) + + options := os.UpdateOpts{ + Name: "new_port_name", + FixedIPs: []os.IP{ + os.IP{SubnetID: "a0304c3a-4f08-4c43-88af-d796509c97d2", IPAddress: "10.0.0.3"}, + }, + SecurityGroups: []string{"f0ac4394-7e4a-4409-9701-ba8be283dbc3"}, + } + + s, err := Update(fake.ServiceClient(), "65c0ee9f-d634-4522-8954-51021b570b0d", options).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, s.Name, "new_port_name") + th.AssertDeepEquals(t, s.FixedIPs, []os.IP{ + os.IP{SubnetID: "a0304c3a-4f08-4c43-88af-d796509c97d2", IPAddress: "10.0.0.3"}, + }) + th.AssertDeepEquals(t, s.SecurityGroups, []string{"f0ac4394-7e4a-4409-9701-ba8be283dbc3"}) +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/ports/65c0ee9f-d634-4522-8954-51021b570b0d", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusNoContent) + }) + + res := Delete(fake.ServiceClient(), "65c0ee9f-d634-4522-8954-51021b570b0d") + th.AssertNoErr(t, res.Err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/networking/v2/subnets/delegate.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/networking/v2/subnets/delegate.go new file mode 100644 index 0000000000..a7fb7bb15f --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/networking/v2/subnets/delegate.go @@ -0,0 +1,40 @@ +package subnets + +import ( + "github.com/rackspace/gophercloud" + os "github.com/rackspace/gophercloud/openstack/networking/v2/subnets" + "github.com/rackspace/gophercloud/pagination" +) + +// List returns a Pager which allows you to iterate over a collection of +// subnets. It accepts a ListOpts struct, which allows you to filter and sort +// the returned collection for greater efficiency. +// +// Default policy settings return only those subnets that are owned by the tenant +// who submits the request, unless the request is submitted by a user with +// administrative rights. +func List(c *gophercloud.ServiceClient, opts os.ListOptsBuilder) pagination.Pager { + return os.List(c, opts) +} + +// Get retrieves a specific subnet based on its unique ID. +func Get(c *gophercloud.ServiceClient, networkID string) os.GetResult { + return os.Get(c, networkID) +} + +// Create accepts a CreateOpts struct and creates a new subnet using the values +// provided. You must remember to provide a valid NetworkID, CIDR and IP version. +func Create(c *gophercloud.ServiceClient, opts os.CreateOptsBuilder) os.CreateResult { + return os.Create(c, opts) +} + +// Update accepts a UpdateOpts struct and updates an existing subnet using the +// values provided. +func Update(c *gophercloud.ServiceClient, networkID string, opts os.UpdateOptsBuilder) os.UpdateResult { + return os.Update(c, networkID, opts) +} + +// Delete accepts a unique ID and deletes the subnet associated with it. +func Delete(c *gophercloud.ServiceClient, networkID string) os.DeleteResult { + return os.Delete(c, networkID) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/networking/v2/subnets/delegate_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/networking/v2/subnets/delegate_test.go new file mode 100644 index 0000000000..fafc6fb302 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/networking/v2/subnets/delegate_test.go @@ -0,0 +1,363 @@ +package subnets + +import ( + "fmt" + "net/http" + "testing" + + os "github.com/rackspace/gophercloud/openstack/networking/v2/subnets" + "github.com/rackspace/gophercloud/pagination" + fake "github.com/rackspace/gophercloud/rackspace/networking/v2/common" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/subnets", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "subnets": [ + { + "name": "private-subnet", + "enable_dhcp": true, + "network_id": "db193ab3-96e3-4cb3-8fc5-05f4296d0324", + "tenant_id": "26a7980765d0414dbc1fc1f88cdb7e6e", + "dns_nameservers": [], + "allocation_pools": [ + { + "start": "10.0.0.2", + "end": "10.0.0.254" + } + ], + "host_routes": [], + "ip_version": 4, + "gateway_ip": "10.0.0.1", + "cidr": "10.0.0.0/24", + "id": "08eae331-0402-425a-923c-34f7cfe39c1b" + }, + { + "name": "my_subnet", + "enable_dhcp": true, + "network_id": "d32019d3-bc6e-4319-9c1d-6722fc136a22", + "tenant_id": "4fd44f30292945e481c7b8a0c8908869", + "dns_nameservers": [], + "allocation_pools": [ + { + "start": "192.0.0.2", + "end": "192.255.255.254" + } + ], + "host_routes": [], + "ip_version": 4, + "gateway_ip": "192.0.0.1", + "cidr": "192.0.0.0/8", + "id": "54d6f61d-db07-451c-9ab3-b9609b6b6f0b" + } + ] +} + `) + }) + + count := 0 + + List(fake.ServiceClient(), os.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := os.ExtractSubnets(page) + if err != nil { + t.Errorf("Failed to extract subnets: %v", err) + return false, nil + } + + expected := []os.Subnet{ + os.Subnet{ + Name: "private-subnet", + EnableDHCP: true, + NetworkID: "db193ab3-96e3-4cb3-8fc5-05f4296d0324", + TenantID: "26a7980765d0414dbc1fc1f88cdb7e6e", + DNSNameservers: []string{}, + AllocationPools: []os.AllocationPool{ + os.AllocationPool{ + Start: "10.0.0.2", + End: "10.0.0.254", + }, + }, + HostRoutes: []os.HostRoute{}, + IPVersion: 4, + GatewayIP: "10.0.0.1", + CIDR: "10.0.0.0/24", + ID: "08eae331-0402-425a-923c-34f7cfe39c1b", + }, + os.Subnet{ + Name: "my_subnet", + EnableDHCP: true, + NetworkID: "d32019d3-bc6e-4319-9c1d-6722fc136a22", + TenantID: "4fd44f30292945e481c7b8a0c8908869", + DNSNameservers: []string{}, + AllocationPools: []os.AllocationPool{ + os.AllocationPool{ + Start: "192.0.0.2", + End: "192.255.255.254", + }, + }, + HostRoutes: []os.HostRoute{}, + IPVersion: 4, + GatewayIP: "192.0.0.1", + CIDR: "192.0.0.0/8", + ID: "54d6f61d-db07-451c-9ab3-b9609b6b6f0b", + }, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/subnets/54d6f61d-db07-451c-9ab3-b9609b6b6f0b", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "subnet": { + "name": "my_subnet", + "enable_dhcp": true, + "network_id": "d32019d3-bc6e-4319-9c1d-6722fc136a22", + "tenant_id": "4fd44f30292945e481c7b8a0c8908869", + "dns_nameservers": [], + "allocation_pools": [ + { + "start": "192.0.0.2", + "end": "192.255.255.254" + } + ], + "host_routes": [], + "ip_version": 4, + "gateway_ip": "192.0.0.1", + "cidr": "192.0.0.0/8", + "id": "54d6f61d-db07-451c-9ab3-b9609b6b6f0b" + } +} + `) + }) + + s, err := Get(fake.ServiceClient(), "54d6f61d-db07-451c-9ab3-b9609b6b6f0b").Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, s.Name, "my_subnet") + th.AssertEquals(t, s.EnableDHCP, true) + th.AssertEquals(t, s.NetworkID, "d32019d3-bc6e-4319-9c1d-6722fc136a22") + th.AssertEquals(t, s.TenantID, "4fd44f30292945e481c7b8a0c8908869") + th.AssertDeepEquals(t, s.DNSNameservers, []string{}) + th.AssertDeepEquals(t, s.AllocationPools, []os.AllocationPool{ + os.AllocationPool{ + Start: "192.0.0.2", + End: "192.255.255.254", + }, + }) + th.AssertDeepEquals(t, s.HostRoutes, []os.HostRoute{}) + th.AssertEquals(t, s.IPVersion, 4) + th.AssertEquals(t, s.GatewayIP, "192.0.0.1") + th.AssertEquals(t, s.CIDR, "192.0.0.0/8") + th.AssertEquals(t, s.ID, "54d6f61d-db07-451c-9ab3-b9609b6b6f0b") +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/subnets", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "subnet": { + "network_id": "d32019d3-bc6e-4319-9c1d-6722fc136a22", + "ip_version": 4, + "cidr": "192.168.199.0/24", + "dns_nameservers": ["foo"], + "allocation_pools": [ + { + "start": "192.168.199.2", + "end": "192.168.199.254" + } + ], + "host_routes": [{"destination":"","nexthop": "bar"}] + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, ` +{ + "subnet": { + "name": "", + "enable_dhcp": true, + "network_id": "d32019d3-bc6e-4319-9c1d-6722fc136a22", + "tenant_id": "4fd44f30292945e481c7b8a0c8908869", + "dns_nameservers": [], + "allocation_pools": [ + { + "start": "192.168.199.2", + "end": "192.168.199.254" + } + ], + "host_routes": [], + "ip_version": 4, + "gateway_ip": "192.168.199.1", + "cidr": "192.168.199.0/24", + "id": "3b80198d-4f7b-4f77-9ef5-774d54e17126" + } +} + `) + }) + + opts := os.CreateOpts{ + NetworkID: "d32019d3-bc6e-4319-9c1d-6722fc136a22", + IPVersion: 4, + CIDR: "192.168.199.0/24", + AllocationPools: []os.AllocationPool{ + os.AllocationPool{ + Start: "192.168.199.2", + End: "192.168.199.254", + }, + }, + DNSNameservers: []string{"foo"}, + HostRoutes: []os.HostRoute{ + os.HostRoute{NextHop: "bar"}, + }, + } + s, err := Create(fake.ServiceClient(), opts).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, s.Name, "") + th.AssertEquals(t, s.EnableDHCP, true) + th.AssertEquals(t, s.NetworkID, "d32019d3-bc6e-4319-9c1d-6722fc136a22") + th.AssertEquals(t, s.TenantID, "4fd44f30292945e481c7b8a0c8908869") + th.AssertDeepEquals(t, s.DNSNameservers, []string{}) + th.AssertDeepEquals(t, s.AllocationPools, []os.AllocationPool{ + os.AllocationPool{ + Start: "192.168.199.2", + End: "192.168.199.254", + }, + }) + th.AssertDeepEquals(t, s.HostRoutes, []os.HostRoute{}) + th.AssertEquals(t, s.IPVersion, 4) + th.AssertEquals(t, s.GatewayIP, "192.168.199.1") + th.AssertEquals(t, s.CIDR, "192.168.199.0/24") + th.AssertEquals(t, s.ID, "3b80198d-4f7b-4f77-9ef5-774d54e17126") +} + +func TestRequiredCreateOpts(t *testing.T) { + res := Create(fake.ServiceClient(), os.CreateOpts{}) + if res.Err == nil { + t.Fatalf("Expected error, got none") + } + + res = Create(fake.ServiceClient(), os.CreateOpts{NetworkID: "foo"}) + if res.Err == nil { + t.Fatalf("Expected error, got none") + } + + res = Create(fake.ServiceClient(), os.CreateOpts{NetworkID: "foo", CIDR: "bar", IPVersion: 40}) + if res.Err == nil { + t.Fatalf("Expected error, got none") + } +} + +func TestUpdate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/subnets/08eae331-0402-425a-923c-34f7cfe39c1b", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "subnet": { + "name": "my_new_subnet", + "dns_nameservers": ["foo"], + "host_routes": [{"destination":"","nexthop": "bar"}] + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, ` +{ + "subnet": { + "name": "my_new_subnet", + "enable_dhcp": true, + "network_id": "db193ab3-96e3-4cb3-8fc5-05f4296d0324", + "tenant_id": "26a7980765d0414dbc1fc1f88cdb7e6e", + "dns_nameservers": [], + "allocation_pools": [ + { + "start": "10.0.0.2", + "end": "10.0.0.254" + } + ], + "host_routes": [], + "ip_version": 4, + "gateway_ip": "10.0.0.1", + "cidr": "10.0.0.0/24", + "id": "08eae331-0402-425a-923c-34f7cfe39c1b" + } +} + `) + }) + + opts := os.UpdateOpts{ + Name: "my_new_subnet", + DNSNameservers: []string{"foo"}, + HostRoutes: []os.HostRoute{ + os.HostRoute{NextHop: "bar"}, + }, + } + s, err := Update(fake.ServiceClient(), "08eae331-0402-425a-923c-34f7cfe39c1b", opts).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, s.Name, "my_new_subnet") + th.AssertEquals(t, s.ID, "08eae331-0402-425a-923c-34f7cfe39c1b") +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/subnets/08eae331-0402-425a-923c-34f7cfe39c1b", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusNoContent) + }) + + res := Delete(fake.ServiceClient(), "08eae331-0402-425a-923c-34f7cfe39c1b") + th.AssertNoErr(t, res.Err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/accounts/delegate.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/accounts/delegate.go new file mode 100644 index 0000000000..94739308fa --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/accounts/delegate.go @@ -0,0 +1,39 @@ +package accounts + +import ( + "github.com/rackspace/gophercloud" + os "github.com/rackspace/gophercloud/openstack/objectstorage/v1/accounts" +) + +// Get is a function that retrieves an account's metadata. To extract just the +// custom metadata, call the ExtractMetadata method on the GetResult. To extract +// all the headers that are returned (including the metadata), call the +// ExtractHeader method on the GetResult. +func Get(c *gophercloud.ServiceClient) os.GetResult { + return os.Get(c, nil) +} + +// UpdateOpts is a structure that contains parameters for updating, creating, or +// deleting an account's metadata. +type UpdateOpts struct { + Metadata map[string]string + TempURLKey string `h:"X-Account-Meta-Temp-URL-Key"` + TempURLKey2 string `h:"X-Account-Meta-Temp-URL-Key-2"` +} + +// ToAccountUpdateMap formats an UpdateOpts into a map[string]string of headers. +func (opts UpdateOpts) ToAccountUpdateMap() (map[string]string, error) { + headers, err := gophercloud.BuildHeaders(opts) + if err != nil { + return nil, err + } + for k, v := range opts.Metadata { + headers["X-Account-Meta-"+k] = v + } + return headers, err +} + +// Update will update an account's metadata with the Metadata in the UpdateOptsBuilder. +func Update(c *gophercloud.ServiceClient, opts os.UpdateOptsBuilder) os.UpdateResult { + return os.Update(c, opts) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/accounts/delegate_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/accounts/delegate_test.go new file mode 100644 index 0000000000..c568bd6e3b --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/accounts/delegate_test.go @@ -0,0 +1,30 @@ +package accounts + +import ( + "testing" + + os "github.com/rackspace/gophercloud/openstack/objectstorage/v1/accounts" + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestGetAccounts(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleGetAccountSuccessfully(t) + + options := &UpdateOpts{Metadata: map[string]string{"gophercloud-test": "accounts"}} + res := Update(fake.ServiceClient(), options) + th.CheckNoErr(t, res.Err) +} + +func TestUpdateAccounts(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleUpdateAccountSuccessfully(t) + + expected := map[string]string{"Foo": "bar"} + actual, err := Get(fake.ServiceClient()).ExtractMetadata() + th.CheckNoErr(t, err) + th.CheckDeepEquals(t, expected, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/accounts/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/accounts/doc.go new file mode 100644 index 0000000000..293a93088a --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/accounts/doc.go @@ -0,0 +1,3 @@ +// Package accounts provides information and interaction with the account +// API resource for the Rackspace Cloud Files service. +package accounts diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/bulk/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/bulk/doc.go new file mode 100644 index 0000000000..9c89e22b21 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/bulk/doc.go @@ -0,0 +1,3 @@ +// Package bulk provides functionality for working with bulk operations in the +// Rackspace Cloud Files service. +package bulk diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/bulk/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/bulk/requests.go new file mode 100644 index 0000000000..d252609d41 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/bulk/requests.go @@ -0,0 +1,51 @@ +package bulk + +import ( + "net/url" + "strings" + + "github.com/racker/perigee" + "github.com/rackspace/gophercloud" +) + +// DeleteOptsBuilder allows extensions to add additional parameters to the +// Delete request. +type DeleteOptsBuilder interface { + ToBulkDeleteBody() (string, error) +} + +// DeleteOpts is a structure that holds parameters for deleting an object. +type DeleteOpts []string + +// ToBulkDeleteBody formats a DeleteOpts into a request body. +func (opts DeleteOpts) ToBulkDeleteBody() (string, error) { + return url.QueryEscape(strings.Join(opts, "\n")), nil +} + +// Delete will delete objects or containers in bulk. +func Delete(c *gophercloud.ServiceClient, opts DeleteOptsBuilder) DeleteResult { + var res DeleteResult + + if opts == nil { + return res + } + + reqString, err := opts.ToBulkDeleteBody() + if err != nil { + res.Err = err + return res + } + + reqBody := strings.NewReader(reqString) + + resp, err := perigee.Request("DELETE", deleteURL(c), perigee.Options{ + ContentType: "text/plain", + MoreHeaders: c.AuthenticatedHeaders(), + OkCodes: []int{200}, + ReqBody: reqBody, + Results: &res.Body, + }) + res.Header = resp.HttpResponse.Header + res.Err = err + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/bulk/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/bulk/requests_test.go new file mode 100644 index 0000000000..8b5578e91e --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/bulk/requests_test.go @@ -0,0 +1,36 @@ +package bulk + +import ( + "fmt" + "net/http" + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestBulkDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + th.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.AssertEquals(t, r.URL.RawQuery, "bulk-delete") + + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, ` + { + "Number Not Found": 1, + "Response Status": "200 OK", + "Errors": [], + "Number Deleted": 1, + "Response Body": "" + } + `) + }) + + options := DeleteOpts{"gophercloud-testcontainer1", "gophercloud-testcontainer2"} + actual, err := Delete(fake.ServiceClient(), options).ExtractBody() + th.AssertNoErr(t, err) + th.AssertEquals(t, actual.NumberDeleted, 1) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/bulk/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/bulk/results.go new file mode 100644 index 0000000000..fddc125ac6 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/bulk/results.go @@ -0,0 +1,28 @@ +package bulk + +import ( + "github.com/rackspace/gophercloud" + + "github.com/mitchellh/mapstructure" +) + +// DeleteResult represents the result of a bulk delete operation. +type DeleteResult struct { + gophercloud.Result +} + +// DeleteRespBody is the form of the response body returned by a bulk delete request. +type DeleteRespBody struct { + NumberNotFound int `mapstructure:"Number Not Found"` + ResponseStatus string `mapstructure:"Response Status"` + Errors []string `mapstructure:"Errors"` + NumberDeleted int `mapstructure:"Number Deleted"` + ResponseBody string `mapstructure:"Response Body"` +} + +// ExtractBody will extract the body returned by the bulk extract request. +func (dr DeleteResult) ExtractBody() (DeleteRespBody, error) { + var resp DeleteRespBody + err := mapstructure.Decode(dr.Body, &resp) + return resp, err +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/bulk/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/bulk/urls.go new file mode 100644 index 0000000000..2e112033be --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/bulk/urls.go @@ -0,0 +1,11 @@ +package bulk + +import "github.com/rackspace/gophercloud" + +func deleteURL(c *gophercloud.ServiceClient) string { + return c.Endpoint + "?bulk-delete" +} + +func extractURL(c *gophercloud.ServiceClient, ext string) string { + return c.Endpoint + "?extract-archive=" + ext +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/bulk/urls_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/bulk/urls_test.go new file mode 100644 index 0000000000..9169e52f16 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/bulk/urls_test.go @@ -0,0 +1,26 @@ +package bulk + +import ( + "testing" + + "github.com/rackspace/gophercloud" + th "github.com/rackspace/gophercloud/testhelper" +) + +const endpoint = "http://localhost:57909/" + +func endpointClient() *gophercloud.ServiceClient { + return &gophercloud.ServiceClient{Endpoint: endpoint} +} + +func TestDeleteURL(t *testing.T) { + actual := deleteURL(endpointClient()) + expected := endpoint + "?bulk-delete" + th.CheckEquals(t, expected, actual) +} + +func TestExtractURL(t *testing.T) { + actual := extractURL(endpointClient(), "tar") + expected := endpoint + "?extract-archive=tar" + th.CheckEquals(t, expected, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdncontainers/delegate.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdncontainers/delegate.go new file mode 100644 index 0000000000..d7eef20255 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdncontainers/delegate.go @@ -0,0 +1,71 @@ +package cdncontainers + +import ( + "strconv" + + "github.com/rackspace/gophercloud" + os "github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers" + "github.com/rackspace/gophercloud/pagination" +) + +// ExtractNames interprets a page of List results when just the container +// names are requested. +func ExtractNames(page pagination.Page) ([]string, error) { + return os.ExtractNames(page) +} + +// ListOpts are options for listing Rackspace CDN containers. +type ListOpts struct { + EndMarker string `q:"end_marker"` + Format string `q:"format"` + Limit int `q:"limit"` + Marker string `q:"marker"` +} + +// ToContainerListParams formats a ListOpts into a query string and boolean +// representing whether to list complete information for each container. +func (opts ListOpts) ToContainerListParams() (bool, string, error) { + q, err := gophercloud.BuildQueryString(opts) + if err != nil { + return false, "", err + } + return false, q.String(), nil +} + +// List is a function that retrieves containers associated with the account as +// well as account metadata. It returns a pager which can be iterated with the +// EachPage function. +func List(c *gophercloud.ServiceClient, opts os.ListOptsBuilder) pagination.Pager { + return os.List(c, opts) +} + +// Get is a function that retrieves the metadata of a container. To extract just +// the custom metadata, pass the GetResult response to the ExtractMetadata +// function. +func Get(c *gophercloud.ServiceClient, containerName string) os.GetResult { + return os.Get(c, containerName) +} + +// UpdateOpts is a structure that holds parameters for updating, creating, or +// deleting a container's metadata. +type UpdateOpts struct { + CDNEnabled bool `h:"X-Cdn-Enabled"` + LogRetention bool `h:"X-Log-Retention"` + TTL int `h:"X-Ttl"` +} + +// ToContainerUpdateMap formats a CreateOpts into a map of headers. +func (opts UpdateOpts) ToContainerUpdateMap() (map[string]string, error) { + h, err := gophercloud.BuildHeaders(opts) + if err != nil { + return nil, err + } + h["X-Cdn-Enabled"] = strconv.FormatBool(opts.CDNEnabled) + return h, nil +} + +// Update is a function that creates, updates, or deletes a container's +// metadata. +func Update(c *gophercloud.ServiceClient, containerName string, opts os.UpdateOptsBuilder) os.UpdateResult { + return os.Update(c, containerName, opts) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdncontainers/delegate_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdncontainers/delegate_test.go new file mode 100644 index 0000000000..02c3c5e150 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdncontainers/delegate_test.go @@ -0,0 +1,50 @@ +package cdncontainers + +import ( + "testing" + + os "github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestListCDNContainers(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleListContainerNamesSuccessfully(t) + + count := 0 + err := List(fake.ServiceClient(), nil).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractNames(page) + th.AssertNoErr(t, err) + + th.CheckDeepEquals(t, os.ExpectedListNames, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, count, 1) +} + +func TestGetCDNContainer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleGetContainerSuccessfully(t) + + _, err := Get(fake.ServiceClient(), "testContainer").ExtractMetadata() + th.CheckNoErr(t, err) + +} + +func TestUpdateCDNContainer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleUpdateContainerSuccessfully(t) + + options := &UpdateOpts{TTL: 3600} + res := Update(fake.ServiceClient(), "testContainer", options) + th.CheckNoErr(t, res.Err) + +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdncontainers/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdncontainers/doc.go new file mode 100644 index 0000000000..7b0930eeea --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdncontainers/doc.go @@ -0,0 +1,3 @@ +// Package cdncontainers provides information and interaction with the CDN +// Container API resource for the Rackspace Cloud Files service. +package cdncontainers diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdncontainers/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdncontainers/requests.go new file mode 100644 index 0000000000..0567833204 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdncontainers/requests.go @@ -0,0 +1,58 @@ +package cdncontainers + +import ( + "github.com/racker/perigee" + "github.com/rackspace/gophercloud" +) + +// EnableOptsBuilder allows extensions to add additional parameters to the Enable +// request. +type EnableOptsBuilder interface { + ToCDNContainerEnableMap() (map[string]string, error) +} + +// EnableOpts is a structure that holds options for enabling a CDN container. +type EnableOpts struct { + // CDNEnabled indicates whether or not the container is CDN enabled. Set to + // `true` to enable the container. Note that changing this setting from true + // to false will disable the container in the CDN but only after the TTL has + // expired. + CDNEnabled bool `h:"X-Cdn-Enabled"` + // TTL is the time-to-live for the container (in seconds). + TTL int `h:"X-Ttl"` +} + +// ToCDNContainerEnableMap formats an EnableOpts into a map of headers. +func (opts EnableOpts) ToCDNContainerEnableMap() (map[string]string, error) { + h, err := gophercloud.BuildHeaders(opts) + if err != nil { + return nil, err + } + return h, nil +} + +// Enable is a function that enables/disables a CDN container. +func Enable(c *gophercloud.ServiceClient, containerName string, opts EnableOptsBuilder) EnableResult { + var res EnableResult + h := c.AuthenticatedHeaders() + + if opts != nil { + headers, err := opts.ToCDNContainerEnableMap() + if err != nil { + res.Err = err + return res + } + + for k, v := range headers { + h[k] = v + } + } + + resp, err := perigee.Request("PUT", enableURL(c, containerName), perigee.Options{ + MoreHeaders: h, + OkCodes: []int{201, 202, 204}, + }) + res.Header = resp.HttpResponse.Header + res.Err = err + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdncontainers/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdncontainers/requests_test.go new file mode 100644 index 0000000000..28b963dace --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdncontainers/requests_test.go @@ -0,0 +1,29 @@ +package cdncontainers + +import ( + "net/http" + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestEnableCDNContainer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + th.Mux.HandleFunc("/testContainer", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + + w.Header().Add("X-Ttl", "259200") + w.Header().Add("X-Cdn-Enabled", "True") + w.WriteHeader(http.StatusNoContent) + }) + + options := &EnableOpts{CDNEnabled: true, TTL: 259200} + actual := Enable(fake.ServiceClient(), "testContainer", options) + th.AssertNoErr(t, actual.Err) + th.CheckEquals(t, actual.Header["X-Ttl"][0], "259200") + th.CheckEquals(t, actual.Header["X-Cdn-Enabled"][0], "True") +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdncontainers/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdncontainers/results.go new file mode 100644 index 0000000000..a5097ca7f6 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdncontainers/results.go @@ -0,0 +1,8 @@ +package cdncontainers + +import "github.com/rackspace/gophercloud" + +// EnableResult represents the result of a get operation. +type EnableResult struct { + gophercloud.HeaderResult +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdncontainers/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdncontainers/urls.go new file mode 100644 index 0000000000..80653f2762 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdncontainers/urls.go @@ -0,0 +1,7 @@ +package cdncontainers + +import "github.com/rackspace/gophercloud" + +func enableURL(c *gophercloud.ServiceClient, containerName string) string { + return c.ServiceURL(containerName) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdncontainers/urls_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdncontainers/urls_test.go new file mode 100644 index 0000000000..aa5bfe68b2 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdncontainers/urls_test.go @@ -0,0 +1,20 @@ +package cdncontainers + +import ( + "testing" + + "github.com/rackspace/gophercloud" + th "github.com/rackspace/gophercloud/testhelper" +) + +const endpoint = "http://localhost:57909/" + +func endpointClient() *gophercloud.ServiceClient { + return &gophercloud.ServiceClient{Endpoint: endpoint} +} + +func TestEnableURL(t *testing.T) { + actual := enableURL(endpointClient(), "foo") + expected := endpoint + "foo" + th.CheckEquals(t, expected, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdnobjects/delegate.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdnobjects/delegate.go new file mode 100644 index 0000000000..e9d2ff1d6f --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdnobjects/delegate.go @@ -0,0 +1,11 @@ +package cdnobjects + +import ( + "github.com/rackspace/gophercloud" + os "github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects" +) + +// Delete is a function that deletes an object from the CDN. +func Delete(c *gophercloud.ServiceClient, containerName, objectName string, opts os.DeleteOptsBuilder) os.DeleteResult { + return os.Delete(c, containerName, objectName, nil) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdnobjects/delegate_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdnobjects/delegate_test.go new file mode 100644 index 0000000000..b5e04a98c3 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdnobjects/delegate_test.go @@ -0,0 +1,19 @@ +package cdnobjects + +import ( + "testing" + + os "github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects" + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestDeleteCDNObject(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleDeleteObjectSuccessfully(t) + + res := Delete(fake.ServiceClient(), "testContainer", "testObject", nil) + th.AssertNoErr(t, res.Err) + +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdnobjects/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdnobjects/doc.go new file mode 100644 index 0000000000..90cd5c97ff --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdnobjects/doc.go @@ -0,0 +1,3 @@ +// Package cdnobjects provides information and interaction with the CDN +// Object API resource for the Rackspace Cloud Files service. +package cdnobjects diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/containers/delegate.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/containers/delegate.go new file mode 100644 index 0000000000..77ed002574 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/containers/delegate.go @@ -0,0 +1,93 @@ +package containers + +import ( + "github.com/rackspace/gophercloud" + os "github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers" + "github.com/rackspace/gophercloud/pagination" +) + +// ExtractInfo interprets a page of List results when full container info +// is requested. +func ExtractInfo(page pagination.Page) ([]os.Container, error) { + return os.ExtractInfo(page) +} + +// ExtractNames interprets a page of List results when just the container +// names are requested. +func ExtractNames(page pagination.Page) ([]string, error) { + return os.ExtractNames(page) +} + +// List is a function that retrieves containers associated with the account as +// well as account metadata. It returns a pager which can be iterated with the +// EachPage function. +func List(c *gophercloud.ServiceClient, opts os.ListOptsBuilder) pagination.Pager { + return os.List(c, opts) +} + +// CreateOpts is a structure that holds parameters for creating a container. +type CreateOpts struct { + Metadata map[string]string + ContainerRead string `h:"X-Container-Read"` + ContainerWrite string `h:"X-Container-Write"` + VersionsLocation string `h:"X-Versions-Location"` +} + +// ToContainerCreateMap formats a CreateOpts into a map of headers. +func (opts CreateOpts) ToContainerCreateMap() (map[string]string, error) { + h, err := gophercloud.BuildHeaders(opts) + if err != nil { + return nil, err + } + for k, v := range opts.Metadata { + h["X-Container-Meta-"+k] = v + } + return h, nil +} + +// Create is a function that creates a new container. +func Create(c *gophercloud.ServiceClient, containerName string, opts os.CreateOptsBuilder) os.CreateResult { + return os.Create(c, containerName, opts) +} + +// Delete is a function that deletes a container. +func Delete(c *gophercloud.ServiceClient, containerName string) os.DeleteResult { + return os.Delete(c, containerName) +} + +// UpdateOpts is a structure that holds parameters for updating or creating a +// container's metadata. +type UpdateOpts struct { + Metadata map[string]string + ContainerRead string `h:"X-Container-Read"` + ContainerWrite string `h:"X-Container-Write"` + ContentType string `h:"Content-Type"` + DetectContentType bool `h:"X-Detect-Content-Type"` + RemoveVersionsLocation string `h:"X-Remove-Versions-Location"` + VersionsLocation string `h:"X-Versions-Location"` +} + +// ToContainerUpdateMap formats a CreateOpts into a map of headers. +func (opts UpdateOpts) ToContainerUpdateMap() (map[string]string, error) { + h, err := gophercloud.BuildHeaders(opts) + if err != nil { + return nil, err + } + for k, v := range opts.Metadata { + h["X-Container-Meta-"+k] = v + } + return h, nil +} + +// Update is a function that creates, updates, or deletes a container's +// metadata. +func Update(c *gophercloud.ServiceClient, containerName string, opts os.UpdateOptsBuilder) os.UpdateResult { + return os.Update(c, containerName, opts) +} + +// Get is a function that retrieves the metadata of a container. To extract just +// the custom metadata, pass the GetResult response to the ExtractMetadata +// function. +func Get(c *gophercloud.ServiceClient, containerName string) os.GetResult { + return os.Get(c, containerName) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/containers/delegate_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/containers/delegate_test.go new file mode 100644 index 0000000000..7ba4eb21c6 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/containers/delegate_test.go @@ -0,0 +1,91 @@ +package containers + +import ( + "testing" + + os "github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestListContainerInfo(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleListContainerInfoSuccessfully(t) + + count := 0 + err := List(fake.ServiceClient(), &os.ListOpts{Full: true}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractInfo(page) + th.AssertNoErr(t, err) + + th.CheckDeepEquals(t, os.ExpectedListInfo, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, count, 1) +} + +func TestListContainerNames(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleListContainerNamesSuccessfully(t) + + count := 0 + err := List(fake.ServiceClient(), &os.ListOpts{Full: false}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractNames(page) + if err != nil { + t.Errorf("Failed to extract container names: %v", err) + return false, err + } + + th.CheckDeepEquals(t, os.ExpectedListNames, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, count, 1) +} + +func TestCreateContainers(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleCreateContainerSuccessfully(t) + + options := os.CreateOpts{ContentType: "application/json", Metadata: map[string]string{"foo": "bar"}} + res := Create(fake.ServiceClient(), "testContainer", options) + th.CheckNoErr(t, res.Err) + th.CheckEquals(t, "bar", res.Header["X-Container-Meta-Foo"][0]) + +} + +func TestDeleteContainers(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleDeleteContainerSuccessfully(t) + + res := Delete(fake.ServiceClient(), "testContainer") + th.CheckNoErr(t, res.Err) +} + +func TestUpdateContainers(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleUpdateContainerSuccessfully(t) + + options := &os.UpdateOpts{Metadata: map[string]string{"foo": "bar"}} + res := Update(fake.ServiceClient(), "testContainer", options) + th.CheckNoErr(t, res.Err) +} + +func TestGetContainers(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleGetContainerSuccessfully(t) + + _, err := Get(fake.ServiceClient(), "testContainer").ExtractMetadata() + th.CheckNoErr(t, err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/containers/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/containers/doc.go new file mode 100644 index 0000000000..d132a07382 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/containers/doc.go @@ -0,0 +1,3 @@ +// Package containers provides information and interaction with the Container +// API resource for the Rackspace Cloud Files service. +package containers diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/objects/delegate.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/objects/delegate.go new file mode 100644 index 0000000000..bd4a4f0835 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/objects/delegate.go @@ -0,0 +1,90 @@ +package objects + +import ( + "io" + + "github.com/rackspace/gophercloud" + os "github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects" + "github.com/rackspace/gophercloud/pagination" +) + +// ExtractInfo is a function that takes a page of objects and returns their full information. +func ExtractInfo(page pagination.Page) ([]os.Object, error) { + return os.ExtractInfo(page) +} + +// ExtractNames is a function that takes a page of objects and returns only their names. +func ExtractNames(page pagination.Page) ([]string, error) { + return os.ExtractNames(page) +} + +// List is a function that retrieves objects in the container as +// well as container metadata. It returns a pager which can be iterated with the +// EachPage function. +func List(c *gophercloud.ServiceClient, containerName string, opts os.ListOptsBuilder) pagination.Pager { + return os.List(c, containerName, opts) +} + +// Download is a function that retrieves the content and metadata for an object. +// To extract just the content, pass the DownloadResult response to the +// ExtractContent function. +func Download(c *gophercloud.ServiceClient, containerName, objectName string, opts os.DownloadOptsBuilder) os.DownloadResult { + return os.Download(c, containerName, objectName, opts) +} + +// Create is a function that creates a new object or replaces an existing object. +func Create(c *gophercloud.ServiceClient, containerName, objectName string, content io.Reader, opts os.CreateOptsBuilder) os.CreateResult { + return os.Create(c, containerName, objectName, content, opts) +} + +// CopyOpts is a structure that holds parameters for copying one object to +// another. +type CopyOpts struct { + Metadata map[string]string + ContentDisposition string `h:"Content-Disposition"` + ContentEncoding string `h:"Content-Encoding"` + ContentLength int `h:"Content-Length"` + ContentType string `h:"Content-Type"` + CopyFrom string `h:"X-Copy_From"` + Destination string `h:"Destination"` + DetectContentType bool `h:"X-Detect-Content-Type"` +} + +// ToObjectCopyMap formats a CopyOpts into a map of headers. +func (opts CopyOpts) ToObjectCopyMap() (map[string]string, error) { + h, err := gophercloud.BuildHeaders(opts) + if err != nil { + return nil, err + } + for k, v := range opts.Metadata { + h["X-Object-Meta-"+k] = v + } + // `Content-Length` is required and a value of "0" is acceptable, but calling `gophercloud.BuildHeaders` + // will remove the `Content-Length` header if it's set to 0 (or equivalently not set). This will add + // the header if it's not already set. + if _, ok := h["Content-Length"]; !ok { + h["Content-Length"] = "0" + } + return h, nil +} + +// Copy is a function that copies one object to another. +func Copy(c *gophercloud.ServiceClient, containerName, objectName string, opts os.CopyOptsBuilder) os.CopyResult { + return os.Copy(c, containerName, objectName, opts) +} + +// Delete is a function that deletes an object. +func Delete(c *gophercloud.ServiceClient, containerName, objectName string, opts os.DeleteOptsBuilder) os.DeleteResult { + return os.Delete(c, containerName, objectName, opts) +} + +// Get is a function that retrieves the metadata of an object. To extract just the custom +// metadata, pass the GetResult response to the ExtractMetadata function. +func Get(c *gophercloud.ServiceClient, containerName, objectName string, opts os.GetOptsBuilder) os.GetResult { + return os.Get(c, containerName, objectName, opts) +} + +// Update is a function that creates, updates, or deletes an object's metadata. +func Update(c *gophercloud.ServiceClient, containerName, objectName string, opts os.UpdateOptsBuilder) os.UpdateResult { + return os.Update(c, containerName, objectName, opts) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/objects/delegate_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/objects/delegate_test.go new file mode 100644 index 0000000000..08831ec56a --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/objects/delegate_test.go @@ -0,0 +1,115 @@ +package objects + +import ( + "bytes" + "testing" + + os "github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestDownloadObject(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleDownloadObjectSuccessfully(t) + + content, err := Download(fake.ServiceClient(), "testContainer", "testObject", nil).ExtractContent() + th.AssertNoErr(t, err) + th.CheckEquals(t, string(content), "Successful download with Gophercloud") +} + +func TestListObjectsInfo(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleListObjectsInfoSuccessfully(t) + + count := 0 + options := &os.ListOpts{Full: true} + err := List(fake.ServiceClient(), "testContainer", options).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractInfo(page) + th.AssertNoErr(t, err) + + th.CheckDeepEquals(t, os.ExpectedListInfo, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, count, 1) +} + +func TestListObjectNames(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleListObjectNamesSuccessfully(t) + + count := 0 + options := &os.ListOpts{Full: false} + err := List(fake.ServiceClient(), "testContainer", options).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractNames(page) + if err != nil { + t.Errorf("Failed to extract container names: %v", err) + return false, err + } + + th.CheckDeepEquals(t, os.ExpectedListNames, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, count, 1) +} + +func TestCreateObject(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleCreateObjectSuccessfully(t) + + content := bytes.NewBufferString("Did gyre and gimble in the wabe") + options := &os.CreateOpts{ContentType: "application/json"} + res := Create(fake.ServiceClient(), "testContainer", "testObject", content, options) + th.AssertNoErr(t, res.Err) +} + +func TestCopyObject(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleCopyObjectSuccessfully(t) + + options := &CopyOpts{Destination: "/newTestContainer/newTestObject"} + res := Copy(fake.ServiceClient(), "testContainer", "testObject", options) + th.AssertNoErr(t, res.Err) +} + +func TestDeleteObject(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleDeleteObjectSuccessfully(t) + + res := Delete(fake.ServiceClient(), "testContainer", "testObject", nil) + th.AssertNoErr(t, res.Err) +} + +func TestUpdateObject(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleUpdateObjectSuccessfully(t) + + options := &os.UpdateOpts{Metadata: map[string]string{"Gophercloud-Test": "objects"}} + res := Update(fake.ServiceClient(), "testContainer", "testObject", options) + th.AssertNoErr(t, res.Err) +} + +func TestGetObject(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleGetObjectSuccessfully(t) + + expected := map[string]string{"Gophercloud-Test": "objects"} + actual, err := Get(fake.ServiceClient(), "testContainer", "testObject", nil).ExtractMetadata() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, expected, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/objects/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/objects/doc.go new file mode 100644 index 0000000000..781984bee2 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/objects/doc.go @@ -0,0 +1,3 @@ +// Package objects provides information and interaction with the Object +// API resource for the Rackspace Cloud Files service. +package objects diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/results.go new file mode 100644 index 0000000000..3fd50296f3 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/results.go @@ -0,0 +1,122 @@ +package gophercloud + +import ( + "encoding/json" + "net/http" +) + +/* +Result is an internal type to be used by individual resource packages, but its +methods will be available on a wide variety of user-facing embedding types. + +It acts as a base struct that other Result types, returned from request +functions, can embed for convenience. All Results capture basic information +from the HTTP transaction that was performed, including the response body, +HTTP headers, and any errors that happened. + +Generally, each Result type will have an Extract method that can be used to +further interpret the result's payload in a specific context. Extensions or +providers can then provide additional extraction functions to pull out +provider- or extension-specific information as well. +*/ +type Result struct { + // Body is the payload of the HTTP response from the server. In most cases, + // this will be the deserialized JSON structure. + Body interface{} + + // Header contains the HTTP header structure from the original response. + Header http.Header + + // Err is an error that occurred during the operation. It's deferred until + // extraction to make it easier to chain the Extract call. + Err error +} + +// PrettyPrintJSON creates a string containing the full response body as +// pretty-printed JSON. It's useful for capturing test fixtures and for +// debugging extraction bugs. If you include its output in an issue related to +// a buggy extraction function, we will all love you forever. +func (r Result) PrettyPrintJSON() string { + pretty, err := json.MarshalIndent(r.Body, "", " ") + if err != nil { + panic(err.Error()) + } + return string(pretty) +} + +// ErrResult is an internal type to be used by individual resource packages, but +// its methods will be available on a wide variety of user-facing embedding +// types. +// +// It represents results that only contain a potential error and +// nothing else. Usually, if the operation executed successfully, the Err field +// will be nil; otherwise it will be stocked with a relevant error. Use the +// ExtractErr method +// to cleanly pull it out. +type ErrResult struct { + Result +} + +// ExtractErr is a function that extracts error information, or nil, from a result. +func (r ErrResult) ExtractErr() error { + return r.Err +} + +/* +HeaderResult is an internal type to be used by individual resource packages, but +its methods will be available on a wide variety of user-facing embedding types. + +It represents a result that only contains an error (possibly nil) and an +http.Header. This is used, for example, by the objectstorage packages in +openstack, because most of the operations don't return response bodies, but do +have relevant information in headers. +*/ +type HeaderResult struct { + Result +} + +// ExtractHeader will return the http.Header and error from the HeaderResult. +// +// header, err := objects.Create(client, "my_container", objects.CreateOpts{}).ExtractHeader() +func (hr HeaderResult) ExtractHeader() (http.Header, error) { + return hr.Header, hr.Err +} + +// RFC3339Milli describes a common time format used by some API responses. +const RFC3339Milli = "2006-01-02T15:04:05.999999Z" + +/* +Link is an internal type to be used in packages of collection resources that are +paginated in a certain way. + +It's a response substructure common to many paginated collection results that is +used to point to related pages. Usually, the one we care about is the one with +Rel field set to "next". +*/ +type Link struct { + Href string `mapstructure:"href"` + Rel string `mapstructure:"rel"` +} + +/* +ExtractNextURL is an internal function useful for packages of collection +resources that are paginated in a certain way. + +It attempts attempts to extract the "next" URL from slice of Link structs, or +"" if no such URL is present. +*/ +func ExtractNextURL(links []Link) (string, error) { + var url string + + for _, l := range links { + if l.Rel == "next" { + url = l.Href + } + } + + if url == "" { + return "", nil + } + + return url, nil +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/script/acceptancetest b/Godeps/_workspace/src/github.com/rackspace/gophercloud/script/acceptancetest new file mode 100644 index 0000000000..f9c89f4dfd --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/script/acceptancetest @@ -0,0 +1,5 @@ +#!/bin/bash +# +# Run the acceptance tests. + +exec go test -p=1 -tags 'acceptance fixtures' github.com/rackspace/gophercloud/acceptance/... $@ diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/script/bootstrap b/Godeps/_workspace/src/github.com/rackspace/gophercloud/script/bootstrap new file mode 100644 index 0000000000..6bae6e8f14 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/script/bootstrap @@ -0,0 +1,26 @@ +#!/bin/bash +# +# This script helps new contributors set up their local workstation for +# gophercloud development and contributions. + +# Create the environment +export GOPATH=$HOME/go/gophercloud +mkdir -p $GOPATH + +# Download gophercloud into that environment +go get github.com/rackspace/gophercloud +cd $GOPATH/src/github.com/rackspace/gophercloud +git checkout master + +# Write out the env.sh convenience file. +cd $GOPATH +cat <env.sh +#!/bin/bash +export GOPATH=$(pwd) +export GOPHERCLOUD=$GOPATH/src/github.com/rackspace/gophercloud +EOF +chmod a+x env.sh + +# Make changes immediately available as a convenience. +. ./env.sh + diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/script/cibuild b/Godeps/_workspace/src/github.com/rackspace/gophercloud/script/cibuild new file mode 100644 index 0000000000..1cb389e7dc --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/script/cibuild @@ -0,0 +1,5 @@ +#!/bin/bash +# +# Test script to be invoked by Travis. + +exec script/unittest -v diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/script/test b/Godeps/_workspace/src/github.com/rackspace/gophercloud/script/test new file mode 100644 index 0000000000..1e03dff8ab --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/script/test @@ -0,0 +1,5 @@ +#!/bin/bash +# +# Run all the tests. + +exec go test -tags 'acceptance fixtures' ./... $@ diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/script/unittest b/Godeps/_workspace/src/github.com/rackspace/gophercloud/script/unittest new file mode 100644 index 0000000000..d3440a902c --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/script/unittest @@ -0,0 +1,5 @@ +#!/bin/bash +# +# Run the unit tests. + +exec go test -tags fixtures ./... $@ diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/service_client.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/service_client.go new file mode 100644 index 0000000000..3490da05f2 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/service_client.go @@ -0,0 +1,32 @@ +package gophercloud + +import "strings" + +// ServiceClient stores details required to interact with a specific service API implemented by a provider. +// Generally, you'll acquire these by calling the appropriate `New` method on a ProviderClient. +type ServiceClient struct { + // ProviderClient is a reference to the provider that implements this service. + *ProviderClient + + // Endpoint is the base URL of the service's API, acquired from a service catalog. + // It MUST end with a /. + Endpoint string + + // ResourceBase is the base URL shared by the resources within a service's API. It should include + // the API version and, like Endpoint, MUST end with a / if set. If not set, the Endpoint is used + // as-is, instead. + ResourceBase string +} + +// ResourceBaseURL returns the base URL of any resources used by this service. It MUST end with a /. +func (client *ServiceClient) ResourceBaseURL() string { + if client.ResourceBase != "" { + return client.ResourceBase + } + return client.Endpoint +} + +// ServiceURL constructs a URL for a resource belonging to this provider. +func (client *ServiceClient) ServiceURL(parts ...string) string { + return client.ResourceBaseURL() + strings.Join(parts, "/") +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/service_client_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/service_client_test.go new file mode 100644 index 0000000000..84beb3f768 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/service_client_test.go @@ -0,0 +1,14 @@ +package gophercloud + +import ( + "testing" + + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestServiceURL(t *testing.T) { + c := &ServiceClient{Endpoint: "http://123.45.67.8/"} + expected := "http://123.45.67.8/more/parts/here" + actual := c.ServiceURL("more", "parts", "here") + th.CheckEquals(t, expected, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/testhelper/client/fake.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/testhelper/client/fake.go new file mode 100644 index 0000000000..5b69b058f1 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/testhelper/client/fake.go @@ -0,0 +1,17 @@ +package client + +import ( + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/testhelper" +) + +// Fake token to use. +const TokenID = "cbc36478b0bd8e67e89469c7749d4127" + +// ServiceClient returns a generic service client for use in tests. +func ServiceClient() *gophercloud.ServiceClient { + return &gophercloud.ServiceClient{ + ProviderClient: &gophercloud.ProviderClient{TokenID: TokenID}, + Endpoint: testhelper.Endpoint(), + } +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/testhelper/convenience.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/testhelper/convenience.go new file mode 100644 index 0000000000..cf33e1ad1a --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/testhelper/convenience.go @@ -0,0 +1,329 @@ +package testhelper + +import ( + "encoding/json" + "fmt" + "path/filepath" + "reflect" + "runtime" + "strings" + "testing" +) + +const ( + logBodyFmt = "\033[1;31m%s %s\033[0m" + greenCode = "\033[0m\033[1;32m" + yellowCode = "\033[0m\033[1;33m" + resetCode = "\033[0m\033[1;31m" +) + +func prefix(depth int) string { + _, file, line, _ := runtime.Caller(depth) + return fmt.Sprintf("Failure in %s, line %d:", filepath.Base(file), line) +} + +func green(str interface{}) string { + return fmt.Sprintf("%s%#v%s", greenCode, str, resetCode) +} + +func yellow(str interface{}) string { + return fmt.Sprintf("%s%#v%s", yellowCode, str, resetCode) +} + +func logFatal(t *testing.T, str string) { + t.Fatalf(logBodyFmt, prefix(3), str) +} + +func logError(t *testing.T, str string) { + t.Errorf(logBodyFmt, prefix(3), str) +} + +type diffLogger func([]string, interface{}, interface{}) + +type visit struct { + a1 uintptr + a2 uintptr + typ reflect.Type +} + +// Recursively visits the structures of "expected" and "actual". The diffLogger function will be +// invoked with each different value encountered, including the reference path that was followed +// to get there. +func deepDiffEqual(expected, actual reflect.Value, visited map[visit]bool, path []string, logDifference diffLogger) { + defer func() { + // Fall back to the regular reflect.DeepEquals function. + if r := recover(); r != nil { + var e, a interface{} + if expected.IsValid() { + e = expected.Interface() + } + if actual.IsValid() { + a = actual.Interface() + } + + if !reflect.DeepEqual(e, a) { + logDifference(path, e, a) + } + } + }() + + if !expected.IsValid() && actual.IsValid() { + logDifference(path, nil, actual.Interface()) + return + } + if expected.IsValid() && !actual.IsValid() { + logDifference(path, expected.Interface(), nil) + return + } + if !expected.IsValid() && !actual.IsValid() { + return + } + + hard := func(k reflect.Kind) bool { + switch k { + case reflect.Array, reflect.Map, reflect.Slice, reflect.Struct: + return true + } + return false + } + + if expected.CanAddr() && actual.CanAddr() && hard(expected.Kind()) { + addr1 := expected.UnsafeAddr() + addr2 := actual.UnsafeAddr() + + if addr1 > addr2 { + addr1, addr2 = addr2, addr1 + } + + if addr1 == addr2 { + // References are identical. We can short-circuit + return + } + + typ := expected.Type() + v := visit{addr1, addr2, typ} + if visited[v] { + // Already visited. + return + } + + // Remember this visit for later. + visited[v] = true + } + + switch expected.Kind() { + case reflect.Array: + for i := 0; i < expected.Len(); i++ { + hop := append(path, fmt.Sprintf("[%d]", i)) + deepDiffEqual(expected.Index(i), actual.Index(i), visited, hop, logDifference) + } + return + case reflect.Slice: + if expected.IsNil() != actual.IsNil() { + logDifference(path, expected.Interface(), actual.Interface()) + return + } + if expected.Len() == actual.Len() && expected.Pointer() == actual.Pointer() { + return + } + for i := 0; i < expected.Len(); i++ { + hop := append(path, fmt.Sprintf("[%d]", i)) + deepDiffEqual(expected.Index(i), actual.Index(i), visited, hop, logDifference) + } + return + case reflect.Interface: + if expected.IsNil() != actual.IsNil() { + logDifference(path, expected.Interface(), actual.Interface()) + return + } + deepDiffEqual(expected.Elem(), actual.Elem(), visited, path, logDifference) + return + case reflect.Ptr: + deepDiffEqual(expected.Elem(), actual.Elem(), visited, path, logDifference) + return + case reflect.Struct: + for i, n := 0, expected.NumField(); i < n; i++ { + field := expected.Type().Field(i) + hop := append(path, "."+field.Name) + deepDiffEqual(expected.Field(i), actual.Field(i), visited, hop, logDifference) + } + return + case reflect.Map: + if expected.IsNil() != actual.IsNil() { + logDifference(path, expected.Interface(), actual.Interface()) + return + } + if expected.Len() == actual.Len() && expected.Pointer() == actual.Pointer() { + return + } + + var keys []reflect.Value + if expected.Len() >= actual.Len() { + keys = expected.MapKeys() + } else { + keys = actual.MapKeys() + } + + for _, k := range keys { + expectedValue := expected.MapIndex(k) + actualValue := expected.MapIndex(k) + + if !expectedValue.IsValid() { + logDifference(path, nil, actual.Interface()) + return + } + if !actualValue.IsValid() { + logDifference(path, expected.Interface(), nil) + return + } + + hop := append(path, fmt.Sprintf("[%v]", k)) + deepDiffEqual(expectedValue, actualValue, visited, hop, logDifference) + } + return + case reflect.Func: + if expected.IsNil() != actual.IsNil() { + logDifference(path, expected.Interface(), actual.Interface()) + } + return + default: + if expected.Interface() != actual.Interface() { + logDifference(path, expected.Interface(), actual.Interface()) + } + } +} + +func deepDiff(expected, actual interface{}, logDifference diffLogger) { + if expected == nil || actual == nil { + logDifference([]string{}, expected, actual) + return + } + + expectedValue := reflect.ValueOf(expected) + actualValue := reflect.ValueOf(actual) + + if expectedValue.Type() != actualValue.Type() { + logDifference([]string{}, expected, actual) + return + } + deepDiffEqual(expectedValue, actualValue, map[visit]bool{}, []string{}, logDifference) +} + +// AssertEquals compares two arbitrary values and performs a comparison. If the +// comparison fails, a fatal error is raised that will fail the test +func AssertEquals(t *testing.T, expected, actual interface{}) { + if expected != actual { + logFatal(t, fmt.Sprintf("expected %s but got %s", green(expected), yellow(actual))) + } +} + +// CheckEquals is similar to AssertEquals, except with a non-fatal error +func CheckEquals(t *testing.T, expected, actual interface{}) { + if expected != actual { + logError(t, fmt.Sprintf("expected %s but got %s", green(expected), yellow(actual))) + } +} + +// AssertDeepEquals - like Equals - performs a comparison - but on more complex +// structures that requires deeper inspection +func AssertDeepEquals(t *testing.T, expected, actual interface{}) { + pre := prefix(2) + + differed := false + deepDiff(expected, actual, func(path []string, expected, actual interface{}) { + differed = true + t.Errorf("\033[1;31m%sat %s expected %s, but got %s\033[0m", + pre, + strings.Join(path, ""), + green(expected), + yellow(actual)) + }) + if differed { + logFatal(t, "The structures were different.") + } +} + +// CheckDeepEquals is similar to AssertDeepEquals, except with a non-fatal error +func CheckDeepEquals(t *testing.T, expected, actual interface{}) { + pre := prefix(2) + + deepDiff(expected, actual, func(path []string, expected, actual interface{}) { + t.Errorf("\033[1;31m%s at %s expected %s, but got %s\033[0m", + pre, + strings.Join(path, ""), + green(expected), + yellow(actual)) + }) +} + +// isJSONEquals is a utility function that implements JSON comparison for AssertJSONEquals and +// CheckJSONEquals. +func isJSONEquals(t *testing.T, expectedJSON string, actual interface{}) bool { + var parsedExpected, parsedActual interface{} + err := json.Unmarshal([]byte(expectedJSON), &parsedExpected) + if err != nil { + t.Errorf("Unable to parse expected value as JSON: %v", err) + return false + } + + jsonActual, err := json.Marshal(actual) + AssertNoErr(t, err) + err = json.Unmarshal(jsonActual, &parsedActual) + AssertNoErr(t, err) + + if !reflect.DeepEqual(parsedExpected, parsedActual) { + prettyExpected, err := json.MarshalIndent(parsedExpected, "", " ") + if err != nil { + t.Logf("Unable to pretty-print expected JSON: %v\n%s", err, expectedJSON) + } else { + // We can't use green() here because %#v prints prettyExpected as a byte array literal, which + // is... unhelpful. Converting it to a string first leaves "\n" uninterpreted for some reason. + t.Logf("Expected JSON:\n%s%s%s", greenCode, prettyExpected, resetCode) + } + + prettyActual, err := json.MarshalIndent(actual, "", " ") + if err != nil { + t.Logf("Unable to pretty-print actual JSON: %v\n%#v", err, actual) + } else { + // We can't use yellow() for the same reason. + t.Logf("Actual JSON:\n%s%s%s", yellowCode, prettyActual, resetCode) + } + + return false + } + return true +} + +// AssertJSONEquals serializes a value as JSON, parses an expected string as JSON, and ensures that +// both are consistent. If they aren't, the expected and actual structures are pretty-printed and +// shown for comparison. +// +// This is useful for comparing structures that are built as nested map[string]interface{} values, +// which are a pain to construct as literals. +func AssertJSONEquals(t *testing.T, expectedJSON string, actual interface{}) { + if !isJSONEquals(t, expectedJSON, actual) { + logFatal(t, "The generated JSON structure differed.") + } +} + +// CheckJSONEquals is similar to AssertJSONEquals, but nonfatal. +func CheckJSONEquals(t *testing.T, expectedJSON string, actual interface{}) { + if !isJSONEquals(t, expectedJSON, actual) { + logError(t, "The generated JSON structure differed.") + } +} + +// AssertNoErr is a convenience function for checking whether an error value is +// an actual error +func AssertNoErr(t *testing.T, e error) { + if e != nil { + logFatal(t, fmt.Sprintf("unexpected error %s", yellow(e.Error()))) + } +} + +// CheckNoErr is similar to AssertNoErr, except with a non-fatal error +func CheckNoErr(t *testing.T, e error) { + if e != nil { + logError(t, fmt.Sprintf("unexpected error %s", yellow(e.Error()))) + } +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/testhelper/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/testhelper/doc.go new file mode 100644 index 0000000000..25b4dfebbb --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/testhelper/doc.go @@ -0,0 +1,4 @@ +/* +Package testhelper container methods that are useful for writing unit tests. +*/ +package testhelper diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/testhelper/http_responses.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/testhelper/http_responses.go new file mode 100644 index 0000000000..e1f1f9ac0e --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/testhelper/http_responses.go @@ -0,0 +1,91 @@ +package testhelper + +import ( + "encoding/json" + "io/ioutil" + "net/http" + "net/http/httptest" + "net/url" + "reflect" + "testing" +) + +var ( + // Mux is a multiplexer that can be used to register handlers. + Mux *http.ServeMux + + // Server is an in-memory HTTP server for testing. + Server *httptest.Server +) + +// SetupHTTP prepares the Mux and Server. +func SetupHTTP() { + Mux = http.NewServeMux() + Server = httptest.NewServer(Mux) +} + +// TeardownHTTP releases HTTP-related resources. +func TeardownHTTP() { + Server.Close() +} + +// Endpoint returns a fake endpoint that will actually target the Mux. +func Endpoint() string { + return Server.URL + "/" +} + +// TestFormValues ensures that all the URL parameters given to the http.Request are the same as values. +func TestFormValues(t *testing.T, r *http.Request, values map[string]string) { + want := url.Values{} + for k, v := range values { + want.Add(k, v) + } + + r.ParseForm() + if !reflect.DeepEqual(want, r.Form) { + t.Errorf("Request parameters = %v, want %v", r.Form, want) + } +} + +// TestMethod checks that the Request has the expected method (e.g. GET, POST). +func TestMethod(t *testing.T, r *http.Request, expected string) { + if expected != r.Method { + t.Errorf("Request method = %v, expected %v", r.Method, expected) + } +} + +// TestHeader checks that the header on the http.Request matches the expected value. +func TestHeader(t *testing.T, r *http.Request, header string, expected string) { + if actual := r.Header.Get(header); expected != actual { + t.Errorf("Header %s = %s, expected %s", header, actual, expected) + } +} + +// TestBody verifies that the request body matches an expected body. +func TestBody(t *testing.T, r *http.Request, expected string) { + b, err := ioutil.ReadAll(r.Body) + if err != nil { + t.Errorf("Unable to read body: %v", err) + } + str := string(b) + if expected != str { + t.Errorf("Body = %s, expected %s", str, expected) + } +} + +// TestJSONRequest verifies that the JSON payload of a request matches an expected structure, without asserting things about +// whitespace or ordering. +func TestJSONRequest(t *testing.T, r *http.Request, expected string) { + b, err := ioutil.ReadAll(r.Body) + if err != nil { + t.Errorf("Unable to read request body: %v", err) + } + + var actualJSON interface{} + err = json.Unmarshal(b, &actualJSON) + if err != nil { + t.Errorf("Unable to parse request body as JSON: %v", err) + } + + CheckJSONEquals(t, expected, actualJSON) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/util.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/util.go new file mode 100644 index 0000000000..fbd9fe9f38 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/util.go @@ -0,0 +1,44 @@ +package gophercloud + +import ( + "errors" + "strings" + "time" +) + +// WaitFor polls a predicate function, once per second, up to a timeout limit. +// It usually does this to wait for a resource to transition to a certain state. +// Resource packages will wrap this in a more convenient function that's +// specific to a certain resource, but it can also be useful on its own. +func WaitFor(timeout int, predicate func() (bool, error)) error { + start := time.Now().Second() + for { + // Force a 1s sleep + time.Sleep(1 * time.Second) + + // If a timeout is set, and that's been exceeded, shut it down + if timeout >= 0 && time.Now().Second()-start >= timeout { + return errors.New("A timeout occurred") + } + + // Execute the function + satisfied, err := predicate() + if err != nil { + return err + } + if satisfied { + return nil + } + } +} + +// NormalizeURL is an internal function to be used by provider clients. +// +// It ensures that each endpoint URL has a closing `/`, as expected by +// ServiceClient's methods. +func NormalizeURL(url string) string { + if !strings.HasSuffix(url, "/") { + return url + "/" + } + return url +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/util_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/util_test.go new file mode 100644 index 0000000000..5a15a005d3 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/util_test.go @@ -0,0 +1,14 @@ +package gophercloud + +import ( + "testing" + + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestWaitFor(t *testing.T) { + err := WaitFor(5, func() (bool, error) { + return true, nil + }) + th.CheckNoErr(t, err) +}