cord-776 create build / runtime containers for autmation uservices
Change-Id: I246973192adef56a250ffe93a5f65fff488840c1
diff --git a/automation/vendor/github.com/juju/gomaasapi/LICENSE b/automation/vendor/github.com/juju/gomaasapi/LICENSE
new file mode 100644
index 0000000..d5836af
--- /dev/null
+++ b/automation/vendor/github.com/juju/gomaasapi/LICENSE
@@ -0,0 +1,191 @@
+All files in this repository are licensed as follows. If you contribute
+to this repository, it is assumed that you license your contribution
+under the same license unless you state otherwise.
+
+All files Copyright (C) 2012-2016 Canonical Ltd. unless otherwise specified in the file.
+
+This software is licensed under the LGPLv3, included below.
+
+As a special exception to the GNU Lesser General Public License version 3
+("LGPL3"), the copyright holders of this Library give you permission to
+convey to a third party a Combined Work that links statically or dynamically
+to this Library without providing any Minimal Corresponding Source or
+Minimal Application Code as set out in 4d or providing the installation
+information set out in section 4e, provided that you comply with the other
+provisions of LGPL3 and provided that you meet, for the Application the
+terms and conditions of the license(s) which apply to the Application.
+
+Except as stated in this special exception, the provisions of LGPL3 will
+continue to comply in full to this Library. If you modify this Library, you
+may apply this exception to your version of this Library, but you are not
+obliged to do so. If you do not wish to do so, delete this exception
+statement from your version. This exception does not (and cannot) modify any
+license terms which apply to the Application, with which you must still
+comply.
+
+
+ GNU LESSER GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+
+ This version of the GNU Lesser General Public License incorporates
+the terms and conditions of version 3 of the GNU General Public
+License, supplemented by the additional permissions listed below.
+
+ 0. Additional Definitions.
+
+ As used herein, "this License" refers to version 3 of the GNU Lesser
+General Public License, and the "GNU GPL" refers to version 3 of the GNU
+General Public License.
+
+ "The Library" refers to a covered work governed by this License,
+other than an Application or a Combined Work as defined below.
+
+ An "Application" is any work that makes use of an interface provided
+by the Library, but which is not otherwise based on the Library.
+Defining a subclass of a class defined by the Library is deemed a mode
+of using an interface provided by the Library.
+
+ A "Combined Work" is a work produced by combining or linking an
+Application with the Library. The particular version of the Library
+with which the Combined Work was made is also called the "Linked
+Version".
+
+ The "Minimal Corresponding Source" for a Combined Work means the
+Corresponding Source for the Combined Work, excluding any source code
+for portions of the Combined Work that, considered in isolation, are
+based on the Application, and not on the Linked Version.
+
+ The "Corresponding Application Code" for a Combined Work means the
+object code and/or source code for the Application, including any data
+and utility programs needed for reproducing the Combined Work from the
+Application, but excluding the System Libraries of the Combined Work.
+
+ 1. Exception to Section 3 of the GNU GPL.
+
+ You may convey a covered work under sections 3 and 4 of this License
+without being bound by section 3 of the GNU GPL.
+
+ 2. Conveying Modified Versions.
+
+ If you modify a copy of the Library, and, in your modifications, a
+facility refers to a function or data to be supplied by an Application
+that uses the facility (other than as an argument passed when the
+facility is invoked), then you may convey a copy of the modified
+version:
+
+ a) under this License, provided that you make a good faith effort to
+ ensure that, in the event an Application does not supply the
+ function or data, the facility still operates, and performs
+ whatever part of its purpose remains meaningful, or
+
+ b) under the GNU GPL, with none of the additional permissions of
+ this License applicable to that copy.
+
+ 3. Object Code Incorporating Material from Library Header Files.
+
+ The object code form of an Application may incorporate material from
+a header file that is part of the Library. You may convey such object
+code under terms of your choice, provided that, if the incorporated
+material is not limited to numerical parameters, data structure
+layouts and accessors, or small macros, inline functions and templates
+(ten or fewer lines in length), you do both of the following:
+
+ a) Give prominent notice with each copy of the object code that the
+ Library is used in it and that the Library and its use are
+ covered by this License.
+
+ b) Accompany the object code with a copy of the GNU GPL and this license
+ document.
+
+ 4. Combined Works.
+
+ You may convey a Combined Work under terms of your choice that,
+taken together, effectively do not restrict modification of the
+portions of the Library contained in the Combined Work and reverse
+engineering for debugging such modifications, if you also do each of
+the following:
+
+ a) Give prominent notice with each copy of the Combined Work that
+ the Library is used in it and that the Library and its use are
+ covered by this License.
+
+ b) Accompany the Combined Work with a copy of the GNU GPL and this license
+ document.
+
+ c) For a Combined Work that displays copyright notices during
+ execution, include the copyright notice for the Library among
+ these notices, as well as a reference directing the user to the
+ copies of the GNU GPL and this license document.
+
+ d) Do one of the following:
+
+ 0) Convey the Minimal Corresponding Source under the terms of this
+ License, and the Corresponding Application Code in a form
+ suitable for, and under terms that permit, the user to
+ recombine or relink the Application with a modified version of
+ the Linked Version to produce a modified Combined Work, in the
+ manner specified by section 6 of the GNU GPL for conveying
+ Corresponding Source.
+
+ 1) Use a suitable shared library mechanism for linking with the
+ Library. A suitable mechanism is one that (a) uses at run time
+ a copy of the Library already present on the user's computer
+ system, and (b) will operate properly with a modified version
+ of the Library that is interface-compatible with the Linked
+ Version.
+
+ e) Provide Installation Information, but only if you would otherwise
+ be required to provide such information under section 6 of the
+ GNU GPL, and only to the extent that such information is
+ necessary to install and execute a modified version of the
+ Combined Work produced by recombining or relinking the
+ Application with a modified version of the Linked Version. (If
+ you use option 4d0, the Installation Information must accompany
+ the Minimal Corresponding Source and Corresponding Application
+ Code. If you use option 4d1, you must provide the Installation
+ Information in the manner specified by section 6 of the GNU GPL
+ for conveying Corresponding Source.)
+
+ 5. Combined Libraries.
+
+ You may place library facilities that are a work based on the
+Library side by side in a single library together with other library
+facilities that are not Applications and are not covered by this
+License, and convey such a combined library under terms of your
+choice, if you do both of the following:
+
+ a) Accompany the combined library with a copy of the same work based
+ on the Library, uncombined with any other library facilities,
+ conveyed under the terms of this License.
+
+ b) Give prominent notice with the combined library that part of it
+ is a work based on the Library, and explaining where to find the
+ accompanying uncombined form of the same work.
+
+ 6. Revised Versions of the GNU Lesser General Public License.
+
+ The Free Software Foundation may publish revised and/or new versions
+of the GNU Lesser General Public License from time to time. Such new
+versions will be similar in spirit to the present version, but may
+differ in detail to address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Library as you received it specifies that a certain numbered version
+of the GNU Lesser General Public License "or any later version"
+applies to it, you have the option of following the terms and
+conditions either of that published version or of any later version
+published by the Free Software Foundation. If the Library as you
+received it does not specify a version number of the GNU Lesser
+General Public License, you may choose any version of the GNU Lesser
+General Public License ever published by the Free Software Foundation.
+
+ If the Library as you received it specifies that a proxy can decide
+whether future versions of the GNU Lesser General Public License shall
+apply, that proxy's public statement of acceptance of any version is
+permanent authorization for you to choose that version for the
+Library.
diff --git a/automation/vendor/github.com/juju/gomaasapi/Makefile b/automation/vendor/github.com/juju/gomaasapi/Makefile
new file mode 100644
index 0000000..ea6cbb5
--- /dev/null
+++ b/automation/vendor/github.com/juju/gomaasapi/Makefile
@@ -0,0 +1,26 @@
+# Build, and run tests.
+check: examples
+ go test ./...
+
+example_source := $(wildcard example/*.go)
+example_binaries := $(patsubst %.go,%,$(example_source))
+
+# Clean up binaries.
+clean:
+ $(RM) $(example_binaries)
+
+# Reformat the source files to match our layout standards.
+format:
+ gofmt -w .
+
+# Invoke gofmt's "simplify" option to streamline the source code.
+simplify:
+ gofmt -w -s .
+
+# Build the examples (we have no tests for them).
+examples: $(example_binaries)
+
+%: %.go
+ go build -o $@ $<
+
+.PHONY: check clean format examples simplify
diff --git a/automation/vendor/github.com/juju/gomaasapi/README.rst b/automation/vendor/github.com/juju/gomaasapi/README.rst
new file mode 100644
index 0000000..c153cd3
--- /dev/null
+++ b/automation/vendor/github.com/juju/gomaasapi/README.rst
@@ -0,0 +1,12 @@
+.. -*- mode: rst -*-
+
+******************************
+MAAS API client library for Go
+******************************
+
+This library serves as a minimal client for communicating with the MAAS web
+API in Go programs.
+
+For more information see the `project homepage`_.
+
+.. _project homepage: https://github.com/juju/gomaasapi
diff --git a/automation/vendor/github.com/juju/gomaasapi/blockdevice.go b/automation/vendor/github.com/juju/gomaasapi/blockdevice.go
new file mode 100644
index 0000000..ad04f9d
--- /dev/null
+++ b/automation/vendor/github.com/juju/gomaasapi/blockdevice.go
@@ -0,0 +1,176 @@
+// Copyright 2016 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+package gomaasapi
+
+import (
+ "github.com/juju/errors"
+ "github.com/juju/schema"
+ "github.com/juju/version"
+)
+
+type blockdevice struct {
+ resourceURI string
+
+ id int
+ name string
+ model string
+ path string
+ usedFor string
+ tags []string
+
+ blockSize uint64
+ usedSize uint64
+ size uint64
+
+ partitions []*partition
+}
+
+// ID implements BlockDevice.
+func (b *blockdevice) ID() int {
+ return b.id
+}
+
+// Name implements BlockDevice.
+func (b *blockdevice) Name() string {
+ return b.name
+}
+
+// Model implements BlockDevice.
+func (b *blockdevice) Model() string {
+ return b.model
+}
+
+// Path implements BlockDevice.
+func (b *blockdevice) Path() string {
+ return b.path
+}
+
+// UsedFor implements BlockDevice.
+func (b *blockdevice) UsedFor() string {
+ return b.usedFor
+}
+
+// Tags implements BlockDevice.
+func (b *blockdevice) Tags() []string {
+ return b.tags
+}
+
+// BlockSize implements BlockDevice.
+func (b *blockdevice) BlockSize() uint64 {
+ return b.blockSize
+}
+
+// UsedSize implements BlockDevice.
+func (b *blockdevice) UsedSize() uint64 {
+ return b.usedSize
+}
+
+// Size implements BlockDevice.
+func (b *blockdevice) Size() uint64 {
+ return b.size
+}
+
+// Partitions implements BlockDevice.
+func (b *blockdevice) Partitions() []Partition {
+ result := make([]Partition, len(b.partitions))
+ for i, v := range b.partitions {
+ result[i] = v
+ }
+ return result
+}
+
+func readBlockDevices(controllerVersion version.Number, source interface{}) ([]*blockdevice, error) {
+ checker := schema.List(schema.StringMap(schema.Any()))
+ coerced, err := checker.Coerce(source, nil)
+ if err != nil {
+ return nil, WrapWithDeserializationError(err, "blockdevice base schema check failed")
+ }
+ valid := coerced.([]interface{})
+
+ var deserialisationVersion version.Number
+ for v := range blockdeviceDeserializationFuncs {
+ if v.Compare(deserialisationVersion) > 0 && v.Compare(controllerVersion) <= 0 {
+ deserialisationVersion = v
+ }
+ }
+ if deserialisationVersion == version.Zero {
+ return nil, NewUnsupportedVersionError("no blockdevice read func for version %s", controllerVersion)
+ }
+ readFunc := blockdeviceDeserializationFuncs[deserialisationVersion]
+ return readBlockDeviceList(valid, readFunc)
+}
+
+// readBlockDeviceList expects the values of the sourceList to be string maps.
+func readBlockDeviceList(sourceList []interface{}, readFunc blockdeviceDeserializationFunc) ([]*blockdevice, error) {
+ result := make([]*blockdevice, 0, len(sourceList))
+ for i, value := range sourceList {
+ source, ok := value.(map[string]interface{})
+ if !ok {
+ return nil, NewDeserializationError("unexpected value for blockdevice %d, %T", i, value)
+ }
+ blockdevice, err := readFunc(source)
+ if err != nil {
+ return nil, errors.Annotatef(err, "blockdevice %d", i)
+ }
+ result = append(result, blockdevice)
+ }
+ return result, nil
+}
+
+type blockdeviceDeserializationFunc func(map[string]interface{}) (*blockdevice, error)
+
+var blockdeviceDeserializationFuncs = map[version.Number]blockdeviceDeserializationFunc{
+ twoDotOh: blockdevice_2_0,
+}
+
+func blockdevice_2_0(source map[string]interface{}) (*blockdevice, error) {
+ fields := schema.Fields{
+ "resource_uri": schema.String(),
+
+ "id": schema.ForceInt(),
+ "name": schema.String(),
+ "model": schema.OneOf(schema.Nil(""), schema.String()),
+ "path": schema.String(),
+ "used_for": schema.String(),
+ "tags": schema.List(schema.String()),
+
+ "block_size": schema.ForceUint(),
+ "used_size": schema.ForceUint(),
+ "size": schema.ForceUint(),
+
+ "partitions": schema.List(schema.StringMap(schema.Any())),
+ }
+ checker := schema.FieldMap(fields, nil)
+ coerced, err := checker.Coerce(source, nil)
+ if err != nil {
+ return nil, WrapWithDeserializationError(err, "blockdevice 2.0 schema check failed")
+ }
+ valid := coerced.(map[string]interface{})
+ // From here we know that the map returned from the schema coercion
+ // contains fields of the right type.
+
+ partitions, err := readPartitionList(valid["partitions"].([]interface{}), partition_2_0)
+ if err != nil {
+ return nil, errors.Trace(err)
+ }
+
+ model, _ := valid["model"].(string)
+ result := &blockdevice{
+ resourceURI: valid["resource_uri"].(string),
+
+ id: valid["id"].(int),
+ name: valid["name"].(string),
+ model: model,
+ path: valid["path"].(string),
+ usedFor: valid["used_for"].(string),
+ tags: convertToStringSlice(valid["tags"]),
+
+ blockSize: valid["block_size"].(uint64),
+ usedSize: valid["used_size"].(uint64),
+ size: valid["size"].(uint64),
+
+ partitions: partitions,
+ }
+ return result, nil
+}
diff --git a/automation/vendor/github.com/juju/gomaasapi/bootresource.go b/automation/vendor/github.com/juju/gomaasapi/bootresource.go
new file mode 100644
index 0000000..619a2a9
--- /dev/null
+++ b/automation/vendor/github.com/juju/gomaasapi/bootresource.go
@@ -0,0 +1,136 @@
+// Copyright 2016 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+package gomaasapi
+
+import (
+ "strings"
+
+ "github.com/juju/errors"
+ "github.com/juju/schema"
+ "github.com/juju/utils/set"
+ "github.com/juju/version"
+)
+
+type bootResource struct {
+ // Add the controller in when we need to do things with the bootResource.
+ // controller Controller
+
+ resourceURI string
+
+ id int
+ name string
+ type_ string
+ architecture string
+ subArches string
+ kernelFlavor string
+}
+
+// ID implements BootResource.
+func (b *bootResource) ID() int {
+ return b.id
+}
+
+// Name implements BootResource.
+func (b *bootResource) Name() string {
+ return b.name
+}
+
+// Name implements BootResource.
+func (b *bootResource) Type() string {
+ return b.type_
+}
+
+// Name implements BootResource.
+func (b *bootResource) Architecture() string {
+ return b.architecture
+}
+
+// SubArchitectures implements BootResource.
+func (b *bootResource) SubArchitectures() set.Strings {
+ return set.NewStrings(strings.Split(b.subArches, ",")...)
+}
+
+// KernelFlavor implements BootResource.
+func (b *bootResource) KernelFlavor() string {
+ return b.kernelFlavor
+}
+
+func readBootResources(controllerVersion version.Number, source interface{}) ([]*bootResource, error) {
+ checker := schema.List(schema.StringMap(schema.Any()))
+ coerced, err := checker.Coerce(source, nil)
+ if err != nil {
+ return nil, WrapWithDeserializationError(err, "boot resource base schema check failed")
+ }
+ valid := coerced.([]interface{})
+
+ var deserialisationVersion version.Number
+ for v := range bootResourceDeserializationFuncs {
+ if v.Compare(deserialisationVersion) > 0 && v.Compare(controllerVersion) <= 0 {
+ deserialisationVersion = v
+ }
+ }
+ if deserialisationVersion == version.Zero {
+ return nil, NewUnsupportedVersionError("no boot resource read func for version %s", controllerVersion)
+ }
+ readFunc := bootResourceDeserializationFuncs[deserialisationVersion]
+ return readBootResourceList(valid, readFunc)
+}
+
+// readBootResourceList expects the values of the sourceList to be string maps.
+func readBootResourceList(sourceList []interface{}, readFunc bootResourceDeserializationFunc) ([]*bootResource, error) {
+ result := make([]*bootResource, 0, len(sourceList))
+ for i, value := range sourceList {
+ source, ok := value.(map[string]interface{})
+ if !ok {
+ return nil, NewDeserializationError("unexpected value for boot resource %d, %T", i, value)
+ }
+ bootResource, err := readFunc(source)
+ if err != nil {
+ return nil, errors.Annotatef(err, "boot resource %d", i)
+ }
+ result = append(result, bootResource)
+ }
+ return result, nil
+}
+
+type bootResourceDeserializationFunc func(map[string]interface{}) (*bootResource, error)
+
+var bootResourceDeserializationFuncs = map[version.Number]bootResourceDeserializationFunc{
+ twoDotOh: bootResource_2_0,
+}
+
+func bootResource_2_0(source map[string]interface{}) (*bootResource, error) {
+ fields := schema.Fields{
+ "resource_uri": schema.String(),
+ "id": schema.ForceInt(),
+ "name": schema.String(),
+ "type": schema.String(),
+ "architecture": schema.String(),
+ "subarches": schema.String(),
+ "kflavor": schema.String(),
+ }
+ defaults := schema.Defaults{
+ "subarches": "",
+ "kflavor": "",
+ }
+ checker := schema.FieldMap(fields, defaults)
+ coerced, err := checker.Coerce(source, nil)
+ if err != nil {
+ return nil, WrapWithDeserializationError(err, "boot resource 2.0 schema check failed")
+ }
+ valid := coerced.(map[string]interface{})
+ // From here we know that the map returned from the schema coercion
+ // contains fields of the right type.
+
+ result := &bootResource{
+ resourceURI: valid["resource_uri"].(string),
+ id: valid["id"].(int),
+ name: valid["name"].(string),
+ type_: valid["type"].(string),
+ architecture: valid["architecture"].(string),
+ subArches: valid["subarches"].(string),
+ kernelFlavor: valid["kflavor"].(string),
+ }
+ return result, nil
+}
diff --git a/automation/vendor/github.com/juju/gomaasapi/client.go b/automation/vendor/github.com/juju/gomaasapi/client.go
new file mode 100644
index 0000000..ef887e6
--- /dev/null
+++ b/automation/vendor/github.com/juju/gomaasapi/client.go
@@ -0,0 +1,314 @@
+// Copyright 2012-2016 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+package gomaasapi
+
+import (
+ "bytes"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "mime/multipart"
+ "net/http"
+ "net/url"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/juju/errors"
+)
+
+const (
+ // Number of retries performed when the server returns a 503
+ // response with a 'Retry-after' header. A request will be issued
+ // at most NumberOfRetries + 1 times.
+ NumberOfRetries = 4
+
+ RetryAfterHeaderName = "Retry-After"
+)
+
+// Client represents a way to communicating with a MAAS API instance.
+// It is stateless, so it can have concurrent requests in progress.
+type Client struct {
+ APIURL *url.URL
+ Signer OAuthSigner
+}
+
+// ServerError is an http error (or at least, a non-2xx result) received from
+// the server. It contains the numerical HTTP status code as well as an error
+// string and the response's headers.
+type ServerError struct {
+ error
+ StatusCode int
+ Header http.Header
+ BodyMessage string
+}
+
+// GetServerError returns the ServerError from the cause of the error if it is a
+// ServerError, and also returns the bool to indicate if it was a ServerError or
+// not.
+func GetServerError(err error) (ServerError, bool) {
+ svrErr, ok := errors.Cause(err).(ServerError)
+ return svrErr, ok
+}
+
+// readAndClose reads and closes the given ReadCloser.
+//
+// Trying to read from a nil simply returns nil, no error.
+func readAndClose(stream io.ReadCloser) ([]byte, error) {
+ if stream == nil {
+ return nil, nil
+ }
+ defer stream.Close()
+ return ioutil.ReadAll(stream)
+}
+
+// dispatchRequest sends a request to the server, and interprets the response.
+// Client-side errors will return an empty response and a non-nil error. For
+// server-side errors however (i.e. responses with a non 2XX status code), the
+// returned error will be ServerError and the returned body will reflect the
+// server's response. If the server returns a 503 response with a 'Retry-after'
+// header, the request will be transparenty retried.
+func (client Client) dispatchRequest(request *http.Request) ([]byte, error) {
+ // First, store the request's body into a byte[] to be able to restore it
+ // after each request.
+ bodyContent, err := readAndClose(request.Body)
+ if err != nil {
+ return nil, err
+ }
+ for retry := 0; retry < NumberOfRetries; retry++ {
+ // Restore body before issuing request.
+ newBody := ioutil.NopCloser(bytes.NewReader(bodyContent))
+ request.Body = newBody
+ body, err := client.dispatchSingleRequest(request)
+ // If this is a 503 response with a non-void "Retry-After" header: wait
+ // as instructed and retry the request.
+ if err != nil {
+ serverError, ok := errors.Cause(err).(ServerError)
+ if ok && serverError.StatusCode == http.StatusServiceUnavailable {
+ retry_time_int, errConv := strconv.Atoi(serverError.Header.Get(RetryAfterHeaderName))
+ if errConv == nil {
+ select {
+ case <-time.After(time.Duration(retry_time_int) * time.Second):
+ }
+ continue
+ }
+ }
+ }
+ return body, err
+ }
+ // Restore body before issuing request.
+ newBody := ioutil.NopCloser(bytes.NewReader(bodyContent))
+ request.Body = newBody
+ return client.dispatchSingleRequest(request)
+}
+
+func (client Client) dispatchSingleRequest(request *http.Request) ([]byte, error) {
+ client.Signer.OAuthSign(request)
+ httpClient := http.Client{}
+ // See https://code.google.com/p/go/issues/detail?id=4677
+ // We need to force the connection to close each time so that we don't
+ // hit the above Go bug.
+ request.Close = true
+ response, err := httpClient.Do(request)
+ if err != nil {
+ return nil, err
+ }
+ body, err := readAndClose(response.Body)
+ if err != nil {
+ return nil, err
+ }
+ if response.StatusCode < 200 || response.StatusCode > 299 {
+ err := errors.Errorf("ServerError: %v (%s)", response.Status, body)
+ return body, errors.Trace(ServerError{error: err, StatusCode: response.StatusCode, Header: response.Header, BodyMessage: string(body)})
+ }
+ return body, nil
+}
+
+// GetURL returns the URL to a given resource on the API, based on its URI.
+// The resource URI may be absolute or relative; either way the result is a
+// full absolute URL including the network part.
+func (client Client) GetURL(uri *url.URL) *url.URL {
+ return client.APIURL.ResolveReference(uri)
+}
+
+// Get performs an HTTP "GET" to the API. This may be either an API method
+// invocation (if you pass its name in "operation") or plain resource
+// retrieval (if you leave "operation" blank).
+func (client Client) Get(uri *url.URL, operation string, parameters url.Values) ([]byte, error) {
+ if parameters == nil {
+ parameters = make(url.Values)
+ }
+ opParameter := parameters.Get("op")
+ if opParameter != "" {
+ msg := errors.Errorf("reserved parameter 'op' passed (with value '%s')", opParameter)
+ return nil, msg
+ }
+ if operation != "" {
+ parameters.Set("op", operation)
+ }
+ queryUrl := client.GetURL(uri)
+ queryUrl.RawQuery = parameters.Encode()
+ request, err := http.NewRequest("GET", queryUrl.String(), nil)
+ if err != nil {
+ return nil, err
+ }
+ return client.dispatchRequest(request)
+}
+
+// writeMultiPartFiles writes the given files as parts of a multipart message
+// using the given writer.
+func writeMultiPartFiles(writer *multipart.Writer, files map[string][]byte) error {
+ for fileName, fileContent := range files {
+
+ fw, err := writer.CreateFormFile(fileName, fileName)
+ if err != nil {
+ return err
+ }
+ io.Copy(fw, bytes.NewBuffer(fileContent))
+ }
+ return nil
+}
+
+// writeMultiPartParams writes the given parameters as parts of a multipart
+// message using the given writer.
+func writeMultiPartParams(writer *multipart.Writer, parameters url.Values) error {
+ for key, values := range parameters {
+ for _, value := range values {
+ fw, err := writer.CreateFormField(key)
+ if err != nil {
+ return err
+ }
+ buffer := bytes.NewBufferString(value)
+ io.Copy(fw, buffer)
+ }
+ }
+ return nil
+
+}
+
+// nonIdempotentRequestFiles implements the common functionality of PUT and
+// POST requests (but not GET or DELETE requests) when uploading files is
+// needed.
+func (client Client) nonIdempotentRequestFiles(method string, uri *url.URL, parameters url.Values, files map[string][]byte) ([]byte, error) {
+ buf := new(bytes.Buffer)
+ writer := multipart.NewWriter(buf)
+ err := writeMultiPartFiles(writer, files)
+ if err != nil {
+ return nil, err
+ }
+ err = writeMultiPartParams(writer, parameters)
+ if err != nil {
+ return nil, err
+ }
+ writer.Close()
+ url := client.GetURL(uri)
+ request, err := http.NewRequest(method, url.String(), buf)
+ if err != nil {
+ return nil, err
+ }
+ request.Header.Set("Content-Type", writer.FormDataContentType())
+ return client.dispatchRequest(request)
+
+}
+
+// nonIdempotentRequest implements the common functionality of PUT and POST
+// requests (but not GET or DELETE requests).
+func (client Client) nonIdempotentRequest(method string, uri *url.URL, parameters url.Values) ([]byte, error) {
+ url := client.GetURL(uri)
+ request, err := http.NewRequest(method, url.String(), strings.NewReader(string(parameters.Encode())))
+ if err != nil {
+ return nil, err
+ }
+ request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+ return client.dispatchRequest(request)
+}
+
+// Post performs an HTTP "POST" to the API. This may be either an API method
+// invocation (if you pass its name in "operation") or plain resource
+// retrieval (if you leave "operation" blank).
+func (client Client) Post(uri *url.URL, operation string, parameters url.Values, files map[string][]byte) ([]byte, error) {
+ queryParams := url.Values{"op": {operation}}
+ uri.RawQuery = queryParams.Encode()
+ if files != nil {
+ return client.nonIdempotentRequestFiles("POST", uri, parameters, files)
+ }
+ return client.nonIdempotentRequest("POST", uri, parameters)
+}
+
+// Put updates an object on the API, using an HTTP "PUT" request.
+func (client Client) Put(uri *url.URL, parameters url.Values) ([]byte, error) {
+ return client.nonIdempotentRequest("PUT", uri, parameters)
+}
+
+// Delete deletes an object on the API, using an HTTP "DELETE" request.
+func (client Client) Delete(uri *url.URL) error {
+ url := client.GetURL(uri)
+ request, err := http.NewRequest("DELETE", url.String(), strings.NewReader(""))
+ if err != nil {
+ return err
+ }
+ _, err = client.dispatchRequest(request)
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+// Anonymous "signature method" implementation.
+type anonSigner struct{}
+
+func (signer anonSigner) OAuthSign(request *http.Request) error {
+ return nil
+}
+
+// *anonSigner implements the OAuthSigner interface.
+var _ OAuthSigner = anonSigner{}
+
+func composeAPIURL(BaseURL string, apiVersion string) (*url.URL, error) {
+ baseurl := EnsureTrailingSlash(BaseURL)
+ apiurl := fmt.Sprintf("%sapi/%s/", baseurl, apiVersion)
+ return url.Parse(apiurl)
+}
+
+// NewAnonymousClient creates a client that issues anonymous requests.
+// BaseURL should refer to the root of the MAAS server path, e.g.
+// http://my.maas.server.example.com/MAAS/
+// apiVersion should contain the version of the MAAS API that you want to use.
+func NewAnonymousClient(BaseURL string, apiVersion string) (*Client, error) {
+ parsedBaseURL, err := composeAPIURL(BaseURL, apiVersion)
+ if err != nil {
+ return nil, err
+ }
+ return &Client{Signer: &anonSigner{}, APIURL: parsedBaseURL}, nil
+}
+
+// NewAuthenticatedClient parses the given MAAS API key into the individual
+// OAuth tokens and creates an Client that will use these tokens to sign the
+// requests it issues.
+// BaseURL should refer to the root of the MAAS server path, e.g.
+// http://my.maas.server.example.com/MAAS/
+// apiVersion should contain the version of the MAAS API that you want to use.
+func NewAuthenticatedClient(BaseURL string, apiKey string, apiVersion string) (*Client, error) {
+ elements := strings.Split(apiKey, ":")
+ if len(elements) != 3 {
+ errString := fmt.Sprintf("invalid API key %q; expected \"<consumer secret>:<token key>:<token secret>\"", apiKey)
+ return nil, errors.NewNotValid(nil, errString)
+ }
+ token := &OAuthToken{
+ ConsumerKey: elements[0],
+ // The consumer secret is the empty string in MAAS' authentication.
+ ConsumerSecret: "",
+ TokenKey: elements[1],
+ TokenSecret: elements[2],
+ }
+ signer, err := NewPlainTestOAuthSigner(token, "MAAS API")
+ if err != nil {
+ return nil, err
+ }
+ parsedBaseURL, err := composeAPIURL(BaseURL, apiVersion)
+ if err != nil {
+ return nil, err
+ }
+ return &Client{Signer: signer, APIURL: parsedBaseURL}, nil
+}
diff --git a/automation/vendor/github.com/juju/gomaasapi/controller.go b/automation/vendor/github.com/juju/gomaasapi/controller.go
new file mode 100644
index 0000000..3c729c2
--- /dev/null
+++ b/automation/vendor/github.com/juju/gomaasapi/controller.go
@@ -0,0 +1,890 @@
+// Copyright 2016 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+package gomaasapi
+
+import (
+ "encoding/json"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "net/http"
+ "net/url"
+ "path"
+ "strings"
+ "sync/atomic"
+
+ "github.com/juju/errors"
+ "github.com/juju/loggo"
+ "github.com/juju/schema"
+ "github.com/juju/utils/set"
+ "github.com/juju/version"
+)
+
+var (
+ logger = loggo.GetLogger("maas")
+
+ // The supported versions should be ordered from most desirable version to
+ // least as they will be tried in order.
+ supportedAPIVersions = []string{"2.0"}
+
+ // Each of the api versions that change the request or response structure
+ // for any given call should have a value defined for easy definition of
+ // the deserialization functions.
+ twoDotOh = version.Number{Major: 2, Minor: 0}
+
+ // Current request number. Informational only for logging.
+ requestNumber int64
+)
+
+// ControllerArgs is an argument struct for passing the required parameters
+// to the NewController method.
+type ControllerArgs struct {
+ BaseURL string
+ APIKey string
+}
+
+// NewController creates an authenticated client to the MAAS API, and checks
+// the capabilities of the server.
+//
+// If the APIKey is not valid, a NotValid error is returned.
+// If the credentials are incorrect, a PermissionError is returned.
+func NewController(args ControllerArgs) (Controller, error) {
+ // For now we don't need to test multiple versions. It is expected that at
+ // some time in the future, we will try the most up to date version and then
+ // work our way backwards.
+ for _, apiVersion := range supportedAPIVersions {
+ major, minor, err := version.ParseMajorMinor(apiVersion)
+ // We should not get an error here. See the test.
+ if err != nil {
+ return nil, errors.Errorf("bad version defined in supported versions: %q", apiVersion)
+ }
+ client, err := NewAuthenticatedClient(args.BaseURL, args.APIKey, apiVersion)
+ if err != nil {
+ // If the credentials aren't valid, return now.
+ if errors.IsNotValid(err) {
+ return nil, errors.Trace(err)
+ }
+ // Any other error attempting to create the authenticated client
+ // is an unexpected error and return now.
+ return nil, NewUnexpectedError(err)
+ }
+ controllerVersion := version.Number{
+ Major: major,
+ Minor: minor,
+ }
+ controller := &controller{client: client}
+ // The controllerVersion returned from the function will include any patch version.
+ controller.capabilities, controller.apiVersion, err = controller.readAPIVersion(controllerVersion)
+ if err != nil {
+ logger.Debugf("read version failed: %#v", err)
+ continue
+ }
+
+ if err := controller.checkCreds(); err != nil {
+ return nil, errors.Trace(err)
+ }
+ return controller, nil
+ }
+
+ return nil, NewUnsupportedVersionError("controller at %s does not support any of %s", args.BaseURL, supportedAPIVersions)
+}
+
+type controller struct {
+ client *Client
+ apiVersion version.Number
+ capabilities set.Strings
+}
+
+// Capabilities implements Controller.
+func (c *controller) Capabilities() set.Strings {
+ return c.capabilities
+}
+
+// BootResources implements Controller.
+func (c *controller) BootResources() ([]BootResource, error) {
+ source, err := c.get("boot-resources")
+ if err != nil {
+ return nil, NewUnexpectedError(err)
+ }
+ resources, err := readBootResources(c.apiVersion, source)
+ if err != nil {
+ return nil, errors.Trace(err)
+ }
+ var result []BootResource
+ for _, r := range resources {
+ result = append(result, r)
+ }
+ return result, nil
+}
+
+// Fabrics implements Controller.
+func (c *controller) Fabrics() ([]Fabric, error) {
+ source, err := c.get("fabrics")
+ if err != nil {
+ return nil, NewUnexpectedError(err)
+ }
+ fabrics, err := readFabrics(c.apiVersion, source)
+ if err != nil {
+ return nil, errors.Trace(err)
+ }
+ var result []Fabric
+ for _, f := range fabrics {
+ result = append(result, f)
+ }
+ return result, nil
+}
+
+// Spaces implements Controller.
+func (c *controller) Spaces() ([]Space, error) {
+ source, err := c.get("spaces")
+ if err != nil {
+ return nil, NewUnexpectedError(err)
+ }
+ spaces, err := readSpaces(c.apiVersion, source)
+ if err != nil {
+ return nil, errors.Trace(err)
+ }
+ var result []Space
+ for _, space := range spaces {
+ result = append(result, space)
+ }
+ return result, nil
+}
+
+// Zones implements Controller.
+func (c *controller) Zones() ([]Zone, error) {
+ source, err := c.get("zones")
+ if err != nil {
+ return nil, NewUnexpectedError(err)
+ }
+ zones, err := readZones(c.apiVersion, source)
+ if err != nil {
+ return nil, errors.Trace(err)
+ }
+ var result []Zone
+ for _, z := range zones {
+ result = append(result, z)
+ }
+ return result, nil
+}
+
+// DevicesArgs is a argument struct for selecting Devices.
+// Only devices that match the specified criteria are returned.
+type DevicesArgs struct {
+ Hostname []string
+ MACAddresses []string
+ SystemIDs []string
+ Domain string
+ Zone string
+ AgentName string
+}
+
+// Devices implements Controller.
+func (c *controller) Devices(args DevicesArgs) ([]Device, error) {
+ params := NewURLParams()
+ params.MaybeAddMany("hostname", args.Hostname)
+ params.MaybeAddMany("mac_address", args.MACAddresses)
+ params.MaybeAddMany("id", args.SystemIDs)
+ params.MaybeAdd("domain", args.Domain)
+ params.MaybeAdd("zone", args.Zone)
+ params.MaybeAdd("agent_name", args.AgentName)
+ source, err := c.getQuery("devices", params.Values)
+ if err != nil {
+ return nil, NewUnexpectedError(err)
+ }
+ devices, err := readDevices(c.apiVersion, source)
+ if err != nil {
+ return nil, errors.Trace(err)
+ }
+ var result []Device
+ for _, d := range devices {
+ d.controller = c
+ result = append(result, d)
+ }
+ return result, nil
+}
+
+// CreateDeviceArgs is a argument struct for passing information into CreateDevice.
+type CreateDeviceArgs struct {
+ Hostname string
+ MACAddresses []string
+ Domain string
+ Parent string
+}
+
+// Devices implements Controller.
+func (c *controller) CreateDevice(args CreateDeviceArgs) (Device, error) {
+ // There must be at least one mac address.
+ if len(args.MACAddresses) == 0 {
+ return nil, NewBadRequestError("at least one MAC address must be specified")
+ }
+ params := NewURLParams()
+ params.MaybeAdd("hostname", args.Hostname)
+ params.MaybeAdd("domain", args.Domain)
+ params.MaybeAddMany("mac_addresses", args.MACAddresses)
+ params.MaybeAdd("parent", args.Parent)
+ result, err := c.post("devices", "", params.Values)
+ if err != nil {
+ if svrErr, ok := errors.Cause(err).(ServerError); ok {
+ if svrErr.StatusCode == http.StatusBadRequest {
+ return nil, errors.Wrap(err, NewBadRequestError(svrErr.BodyMessage))
+ }
+ }
+ // Translate http errors.
+ return nil, NewUnexpectedError(err)
+ }
+
+ device, err := readDevice(c.apiVersion, result)
+ if err != nil {
+ return nil, errors.Trace(err)
+ }
+ device.controller = c
+ return device, nil
+}
+
+// MachinesArgs is a argument struct for selecting Machines.
+// Only machines that match the specified criteria are returned.
+type MachinesArgs struct {
+ Hostnames []string
+ MACAddresses []string
+ SystemIDs []string
+ Domain string
+ Zone string
+ AgentName string
+ OwnerData map[string]string
+}
+
+// Machines implements Controller.
+func (c *controller) Machines(args MachinesArgs) ([]Machine, error) {
+ params := NewURLParams()
+ params.MaybeAddMany("hostname", args.Hostnames)
+ params.MaybeAddMany("mac_address", args.MACAddresses)
+ params.MaybeAddMany("id", args.SystemIDs)
+ params.MaybeAdd("domain", args.Domain)
+ params.MaybeAdd("zone", args.Zone)
+ params.MaybeAdd("agent_name", args.AgentName)
+ // At the moment the MAAS API doesn't support filtering by owner
+ // data so we do that ourselves below.
+ source, err := c.getQuery("machines", params.Values)
+ if err != nil {
+ return nil, NewUnexpectedError(err)
+ }
+ machines, err := readMachines(c.apiVersion, source)
+ if err != nil {
+ return nil, errors.Trace(err)
+ }
+ var result []Machine
+ for _, m := range machines {
+ m.controller = c
+ if ownerDataMatches(m.ownerData, args.OwnerData) {
+ result = append(result, m)
+ }
+ }
+ return result, nil
+}
+
+func ownerDataMatches(ownerData, filter map[string]string) bool {
+ for key, value := range filter {
+ if ownerData[key] != value {
+ return false
+ }
+ }
+ return true
+}
+
+// StorageSpec represents one element of storage constraints necessary
+// to be satisfied to allocate a machine.
+type StorageSpec struct {
+ // Label is optional and an arbitrary string. Labels need to be unique
+ // across the StorageSpec elements specified in the AllocateMachineArgs.
+ Label string
+ // Size is required and refers to the required minimum size in GB.
+ Size int
+ // Zero or more tags assocated to with the disks.
+ Tags []string
+}
+
+// Validate ensures that there is a positive size and that there are no Empty
+// tag values.
+func (s *StorageSpec) Validate() error {
+ if s.Size <= 0 {
+ return errors.NotValidf("Size value %d", s.Size)
+ }
+ for _, v := range s.Tags {
+ if v == "" {
+ return errors.NotValidf("empty tag")
+ }
+ }
+ return nil
+}
+
+// String returns the string representation of the storage spec.
+func (s *StorageSpec) String() string {
+ label := s.Label
+ if label != "" {
+ label += ":"
+ }
+ tags := strings.Join(s.Tags, ",")
+ if tags != "" {
+ tags = "(" + tags + ")"
+ }
+ return fmt.Sprintf("%s%d%s", label, s.Size, tags)
+}
+
+// InterfaceSpec represents one elemenet of network related constraints.
+type InterfaceSpec struct {
+ // Label is required and an arbitrary string. Labels need to be unique
+ // across the InterfaceSpec elements specified in the AllocateMachineArgs.
+ // The label is returned in the ConstraintMatches response from
+ // AllocateMachine.
+ Label string
+ Space string
+
+ // NOTE: there are other interface spec values that we are not exposing at
+ // this stage that can be added on an as needed basis. Other possible values are:
+ // 'fabric_class', 'not_fabric_class',
+ // 'subnet_cidr', 'not_subnet_cidr',
+ // 'vid', 'not_vid',
+ // 'fabric', 'not_fabric',
+ // 'subnet', 'not_subnet',
+ // 'mode'
+}
+
+// Validate ensures that a Label is specified and that there is at least one
+// Space or NotSpace value set.
+func (a *InterfaceSpec) Validate() error {
+ if a.Label == "" {
+ return errors.NotValidf("missing Label")
+ }
+ // Perhaps at some stage in the future there will be other possible specs
+ // supported (like vid, subnet, etc), but until then, just space to check.
+ if a.Space == "" {
+ return errors.NotValidf("empty Space constraint")
+ }
+ return nil
+}
+
+// String returns the interface spec as MaaS requires it.
+func (a *InterfaceSpec) String() string {
+ return fmt.Sprintf("%s:space=%s", a.Label, a.Space)
+}
+
+// AllocateMachineArgs is an argument struct for passing args into Machine.Allocate.
+type AllocateMachineArgs struct {
+ Hostname string
+ Architecture string
+ MinCPUCount int
+ // MinMemory represented in MB.
+ MinMemory int
+ Tags []string
+ NotTags []string
+ Zone string
+ NotInZone []string
+ // Storage represents the required disks on the Machine. If any are specified
+ // the first value is used for the root disk.
+ Storage []StorageSpec
+ // Interfaces represents a number of required interfaces on the machine.
+ // Each InterfaceSpec relates to an individual network interface.
+ Interfaces []InterfaceSpec
+ // NotSpace is a machine level constraint, and applies to the entire machine
+ // rather than specific interfaces.
+ NotSpace []string
+ AgentName string
+ Comment string
+ DryRun bool
+}
+
+// Validate makes sure that any labels specifed in Storage or Interfaces
+// are unique, and that the required specifications are valid.
+func (a *AllocateMachineArgs) Validate() error {
+ storageLabels := set.NewStrings()
+ for _, spec := range a.Storage {
+ if err := spec.Validate(); err != nil {
+ return errors.Annotate(err, "Storage")
+ }
+ if spec.Label != "" {
+ if storageLabels.Contains(spec.Label) {
+ return errors.NotValidf("reusing storage label %q", spec.Label)
+ }
+ storageLabels.Add(spec.Label)
+ }
+ }
+ interfaceLabels := set.NewStrings()
+ for _, spec := range a.Interfaces {
+ if err := spec.Validate(); err != nil {
+ return errors.Annotate(err, "Interfaces")
+ }
+ if interfaceLabels.Contains(spec.Label) {
+ return errors.NotValidf("reusing interface label %q", spec.Label)
+ }
+ interfaceLabels.Add(spec.Label)
+ }
+ for _, v := range a.NotSpace {
+ if v == "" {
+ return errors.NotValidf("empty NotSpace constraint")
+ }
+ }
+ return nil
+}
+
+func (a *AllocateMachineArgs) storage() string {
+ var values []string
+ for _, spec := range a.Storage {
+ values = append(values, spec.String())
+ }
+ return strings.Join(values, ",")
+}
+
+func (a *AllocateMachineArgs) interfaces() string {
+ var values []string
+ for _, spec := range a.Interfaces {
+ values = append(values, spec.String())
+ }
+ return strings.Join(values, ";")
+}
+
+func (a *AllocateMachineArgs) notSubnets() []string {
+ var values []string
+ for _, v := range a.NotSpace {
+ values = append(values, "space:"+v)
+ }
+ return values
+}
+
+// ConstraintMatches provides a way for the caller of AllocateMachine to determine
+//.how the allocated machine matched the storage and interfaces constraints specified.
+// The labels that were used in the constraints are the keys in the maps.
+type ConstraintMatches struct {
+ // Interface is a mapping of the constraint label specified to the Interfaces
+ // that match that constraint.
+ Interfaces map[string][]Interface
+
+ // Storage is a mapping of the constraint label specified to the BlockDevices
+ // that match that constraint.
+ Storage map[string][]BlockDevice
+}
+
+// AllocateMachine implements Controller.
+//
+// Returns an error that satisfies IsNoMatchError if the requested
+// constraints cannot be met.
+func (c *controller) AllocateMachine(args AllocateMachineArgs) (Machine, ConstraintMatches, error) {
+ var matches ConstraintMatches
+ params := NewURLParams()
+ params.MaybeAdd("name", args.Hostname)
+ params.MaybeAdd("arch", args.Architecture)
+ params.MaybeAddInt("cpu_count", args.MinCPUCount)
+ params.MaybeAddInt("mem", args.MinMemory)
+ params.MaybeAddMany("tags", args.Tags)
+ params.MaybeAddMany("not_tags", args.NotTags)
+ params.MaybeAdd("storage", args.storage())
+ params.MaybeAdd("interfaces", args.interfaces())
+ params.MaybeAddMany("not_subnets", args.notSubnets())
+ params.MaybeAdd("zone", args.Zone)
+ params.MaybeAddMany("not_in_zone", args.NotInZone)
+ params.MaybeAdd("agent_name", args.AgentName)
+ params.MaybeAdd("comment", args.Comment)
+ params.MaybeAddBool("dry_run", args.DryRun)
+ result, err := c.post("machines", "allocate", params.Values)
+ if err != nil {
+ // A 409 Status code is "No Matching Machines"
+ if svrErr, ok := errors.Cause(err).(ServerError); ok {
+ if svrErr.StatusCode == http.StatusConflict {
+ return nil, matches, errors.Wrap(err, NewNoMatchError(svrErr.BodyMessage))
+ }
+ }
+ // Translate http errors.
+ return nil, matches, NewUnexpectedError(err)
+ }
+
+ machine, err := readMachine(c.apiVersion, result)
+ if err != nil {
+ return nil, matches, errors.Trace(err)
+ }
+ machine.controller = c
+
+ // Parse the constraint matches.
+ matches, err = parseAllocateConstraintsResponse(result, machine)
+ if err != nil {
+ return nil, matches, errors.Trace(err)
+ }
+
+ return machine, matches, nil
+}
+
+// ReleaseMachinesArgs is an argument struct for passing the machine system IDs
+// and an optional comment into the ReleaseMachines method.
+type ReleaseMachinesArgs struct {
+ SystemIDs []string
+ Comment string
+}
+
+// ReleaseMachines implements Controller.
+//
+// Release multiple machines at once. Returns
+// - BadRequestError if any of the machines cannot be found
+// - PermissionError if the user does not have permission to release any of the machines
+// - CannotCompleteError if any of the machines could not be released due to their current state
+func (c *controller) ReleaseMachines(args ReleaseMachinesArgs) error {
+ params := NewURLParams()
+ params.MaybeAddMany("machines", args.SystemIDs)
+ params.MaybeAdd("comment", args.Comment)
+ _, err := c.post("machines", "release", params.Values)
+ if err != nil {
+ if svrErr, ok := errors.Cause(err).(ServerError); ok {
+ switch svrErr.StatusCode {
+ case http.StatusBadRequest:
+ return errors.Wrap(err, NewBadRequestError(svrErr.BodyMessage))
+ case http.StatusForbidden:
+ return errors.Wrap(err, NewPermissionError(svrErr.BodyMessage))
+ case http.StatusConflict:
+ return errors.Wrap(err, NewCannotCompleteError(svrErr.BodyMessage))
+ }
+ }
+ return NewUnexpectedError(err)
+ }
+
+ return nil
+}
+
+// Files implements Controller.
+func (c *controller) Files(prefix string) ([]File, error) {
+ params := NewURLParams()
+ params.MaybeAdd("prefix", prefix)
+ source, err := c.getQuery("files", params.Values)
+ if err != nil {
+ return nil, NewUnexpectedError(err)
+ }
+ files, err := readFiles(c.apiVersion, source)
+ if err != nil {
+ return nil, errors.Trace(err)
+ }
+ var result []File
+ for _, f := range files {
+ f.controller = c
+ result = append(result, f)
+ }
+ return result, nil
+}
+
+// GetFile implements Controller.
+func (c *controller) GetFile(filename string) (File, error) {
+ if filename == "" {
+ return nil, errors.NotValidf("missing filename")
+ }
+ source, err := c.get("files/" + filename)
+ if err != nil {
+ if svrErr, ok := errors.Cause(err).(ServerError); ok {
+ if svrErr.StatusCode == http.StatusNotFound {
+ return nil, errors.Wrap(err, NewNoMatchError(svrErr.BodyMessage))
+ }
+ }
+ return nil, NewUnexpectedError(err)
+ }
+ file, err := readFile(c.apiVersion, source)
+ if err != nil {
+ return nil, errors.Trace(err)
+ }
+ file.controller = c
+ return file, nil
+}
+
+// AddFileArgs is a argument struct for passing information into AddFile.
+// One of Content or (Reader, Length) must be specified.
+type AddFileArgs struct {
+ Filename string
+ Content []byte
+ Reader io.Reader
+ Length int64
+}
+
+// Validate checks to make sure the filename has no slashes, and that one of
+// Content or (Reader, Length) is specified.
+func (a *AddFileArgs) Validate() error {
+ dir, _ := path.Split(a.Filename)
+ if dir != "" {
+ return errors.NotValidf("paths in Filename %q", a.Filename)
+ }
+ if a.Filename == "" {
+ return errors.NotValidf("missing Filename")
+ }
+ if a.Content == nil {
+ if a.Reader == nil {
+ return errors.NotValidf("missing Content or Reader")
+ }
+ if a.Length == 0 {
+ return errors.NotValidf("missing Length")
+ }
+ } else {
+ if a.Reader != nil {
+ return errors.NotValidf("specifying Content and Reader")
+ }
+ if a.Length != 0 {
+ return errors.NotValidf("specifying Length and Content")
+ }
+ }
+ return nil
+}
+
+// AddFile implements Controller.
+func (c *controller) AddFile(args AddFileArgs) error {
+ if err := args.Validate(); err != nil {
+ return errors.Trace(err)
+ }
+ fileContent := args.Content
+ if fileContent == nil {
+ content, err := ioutil.ReadAll(io.LimitReader(args.Reader, args.Length))
+ if err != nil {
+ return errors.Annotatef(err, "cannot read file content")
+ }
+ fileContent = content
+ }
+ params := url.Values{"filename": {args.Filename}}
+ _, err := c.postFile("files", "", params, fileContent)
+ if err != nil {
+ if svrErr, ok := errors.Cause(err).(ServerError); ok {
+ if svrErr.StatusCode == http.StatusBadRequest {
+ return errors.Wrap(err, NewBadRequestError(svrErr.BodyMessage))
+ }
+ }
+ return NewUnexpectedError(err)
+ }
+ return nil
+}
+
+func (c *controller) checkCreds() error {
+ if _, err := c.getOp("users", "whoami"); err != nil {
+ if svrErr, ok := errors.Cause(err).(ServerError); ok {
+ if svrErr.StatusCode == http.StatusUnauthorized {
+ return errors.Wrap(err, NewPermissionError(svrErr.BodyMessage))
+ }
+ }
+ return NewUnexpectedError(err)
+ }
+ return nil
+}
+
+func (c *controller) put(path string, params url.Values) (interface{}, error) {
+ path = EnsureTrailingSlash(path)
+ requestID := nextRequestID()
+ logger.Tracef("request %x: PUT %s%s, params: %s", requestID, c.client.APIURL, path, params.Encode())
+ bytes, err := c.client.Put(&url.URL{Path: path}, params)
+ if err != nil {
+ logger.Tracef("response %x: error: %q", requestID, err.Error())
+ logger.Tracef("error detail: %#v", err)
+ return nil, errors.Trace(err)
+ }
+ logger.Tracef("response %x: %s", requestID, string(bytes))
+
+ var parsed interface{}
+ err = json.Unmarshal(bytes, &parsed)
+ if err != nil {
+ return nil, errors.Trace(err)
+ }
+ return parsed, nil
+}
+
+func (c *controller) post(path, op string, params url.Values) (interface{}, error) {
+ bytes, err := c._postRaw(path, op, params, nil)
+ if err != nil {
+ return nil, errors.Trace(err)
+ }
+
+ var parsed interface{}
+ err = json.Unmarshal(bytes, &parsed)
+ if err != nil {
+ return nil, errors.Trace(err)
+ }
+ return parsed, nil
+}
+
+func (c *controller) postFile(path, op string, params url.Values, fileContent []byte) (interface{}, error) {
+ // Only one file is ever sent at a time.
+ files := map[string][]byte{"file": fileContent}
+ return c._postRaw(path, op, params, files)
+}
+
+func (c *controller) _postRaw(path, op string, params url.Values, files map[string][]byte) ([]byte, error) {
+ path = EnsureTrailingSlash(path)
+ requestID := nextRequestID()
+ if logger.IsTraceEnabled() {
+ opArg := ""
+ if op != "" {
+ opArg = "?op=" + op
+ }
+ logger.Tracef("request %x: POST %s%s%s, params=%s", requestID, c.client.APIURL, path, opArg, params.Encode())
+ }
+ bytes, err := c.client.Post(&url.URL{Path: path}, op, params, files)
+ if err != nil {
+ logger.Tracef("response %x: error: %q", requestID, err.Error())
+ logger.Tracef("error detail: %#v", err)
+ return nil, errors.Trace(err)
+ }
+ logger.Tracef("response %x: %s", requestID, string(bytes))
+ return bytes, nil
+}
+
+func (c *controller) delete(path string) error {
+ path = EnsureTrailingSlash(path)
+ requestID := nextRequestID()
+ logger.Tracef("request %x: DELETE %s%s", requestID, c.client.APIURL, path)
+ err := c.client.Delete(&url.URL{Path: path})
+ if err != nil {
+ logger.Tracef("response %x: error: %q", requestID, err.Error())
+ logger.Tracef("error detail: %#v", err)
+ return errors.Trace(err)
+ }
+ logger.Tracef("response %x: complete", requestID)
+ return nil
+}
+
+func (c *controller) getQuery(path string, params url.Values) (interface{}, error) {
+ return c._get(path, "", params)
+}
+
+func (c *controller) get(path string) (interface{}, error) {
+ return c._get(path, "", nil)
+}
+
+func (c *controller) getOp(path, op string) (interface{}, error) {
+ return c._get(path, op, nil)
+}
+
+func (c *controller) _get(path, op string, params url.Values) (interface{}, error) {
+ bytes, err := c._getRaw(path, op, params)
+ if err != nil {
+ return nil, errors.Trace(err)
+ }
+ var parsed interface{}
+ err = json.Unmarshal(bytes, &parsed)
+ if err != nil {
+ return nil, errors.Trace(err)
+ }
+ return parsed, nil
+}
+
+func (c *controller) _getRaw(path, op string, params url.Values) ([]byte, error) {
+ path = EnsureTrailingSlash(path)
+ requestID := nextRequestID()
+ if logger.IsTraceEnabled() {
+ var query string
+ if params != nil {
+ query = "?" + params.Encode()
+ }
+ logger.Tracef("request %x: GET %s%s%s", requestID, c.client.APIURL, path, query)
+ }
+ bytes, err := c.client.Get(&url.URL{Path: path}, op, params)
+ if err != nil {
+ logger.Tracef("response %x: error: %q", requestID, err.Error())
+ logger.Tracef("error detail: %#v", err)
+ return nil, errors.Trace(err)
+ }
+ logger.Tracef("response %x: %s", requestID, string(bytes))
+ return bytes, nil
+}
+
+func nextRequestID() int64 {
+ return atomic.AddInt64(&requestNumber, 1)
+}
+
+func (c *controller) readAPIVersion(apiVersion version.Number) (set.Strings, version.Number, error) {
+ parsed, err := c.get("version")
+ if err != nil {
+ return nil, apiVersion, errors.Trace(err)
+ }
+
+ // As we care about other fields, add them.
+ fields := schema.Fields{
+ "capabilities": schema.List(schema.String()),
+ }
+ checker := schema.FieldMap(fields, nil) // no defaults
+ coerced, err := checker.Coerce(parsed, nil)
+ if err != nil {
+ return nil, apiVersion, WrapWithDeserializationError(err, "version response")
+ }
+ // For now, we don't append any subversion, but as it becomes used, we
+ // should parse and check.
+
+ valid := coerced.(map[string]interface{})
+ // From here we know that the map returned from the schema coercion
+ // contains fields of the right type.
+ capabilities := set.NewStrings()
+ capabilityValues := valid["capabilities"].([]interface{})
+ for _, value := range capabilityValues {
+ capabilities.Add(value.(string))
+ }
+
+ return capabilities, apiVersion, nil
+}
+
+func parseAllocateConstraintsResponse(source interface{}, machine *machine) (ConstraintMatches, error) {
+ var empty ConstraintMatches
+ matchFields := schema.Fields{
+ "storage": schema.StringMap(schema.List(schema.ForceInt())),
+ "interfaces": schema.StringMap(schema.List(schema.ForceInt())),
+ }
+ matchDefaults := schema.Defaults{
+ "storage": schema.Omit,
+ "interfaces": schema.Omit,
+ }
+ fields := schema.Fields{
+ "constraints_by_type": schema.FieldMap(matchFields, matchDefaults),
+ }
+ checker := schema.FieldMap(fields, nil) // no defaults
+ coerced, err := checker.Coerce(source, nil)
+ if err != nil {
+ return empty, WrapWithDeserializationError(err, "allocation constraints response schema check failed")
+ }
+ valid := coerced.(map[string]interface{})
+ constraintsMap := valid["constraints_by_type"].(map[string]interface{})
+ result := ConstraintMatches{
+ Interfaces: make(map[string][]Interface),
+ Storage: make(map[string][]BlockDevice),
+ }
+
+ if interfaceMatches, found := constraintsMap["interfaces"]; found {
+ matches := convertConstraintMatches(interfaceMatches)
+ for label, ids := range matches {
+ interfaces := make([]Interface, len(ids))
+ for index, id := range ids {
+ iface := machine.Interface(id)
+ if iface == nil {
+ return empty, NewDeserializationError("constraint match interface %q: %d does not match an interface for the machine", label, id)
+ }
+ interfaces[index] = iface
+ }
+ result.Interfaces[label] = interfaces
+ }
+ }
+
+ if storageMatches, found := constraintsMap["storage"]; found {
+ matches := convertConstraintMatches(storageMatches)
+ for label, ids := range matches {
+ blockDevices := make([]BlockDevice, len(ids))
+ for index, id := range ids {
+ blockDevice := machine.PhysicalBlockDevice(id)
+ if blockDevice == nil {
+ return empty, NewDeserializationError("constraint match storage %q: %d does not match a physical block device for the machine", label, id)
+ }
+ blockDevices[index] = blockDevice
+ }
+ result.Storage[label] = blockDevices
+ }
+ }
+ return result, nil
+}
+
+func convertConstraintMatches(source interface{}) map[string][]int {
+ // These casts are all safe because of the schema check.
+ result := make(map[string][]int)
+ matchMap := source.(map[string]interface{})
+ for label, values := range matchMap {
+ items := values.([]interface{})
+ result[label] = make([]int, len(items))
+ for index, value := range items {
+ result[label][index] = value.(int)
+ }
+ }
+ return result
+}
diff --git a/automation/vendor/github.com/juju/gomaasapi/dependencies.tsv b/automation/vendor/github.com/juju/gomaasapi/dependencies.tsv
new file mode 100644
index 0000000..4cd966f
--- /dev/null
+++ b/automation/vendor/github.com/juju/gomaasapi/dependencies.tsv
@@ -0,0 +1,11 @@
+github.com/juju/errors git 1b5e39b83d1835fa480e0c2ddefb040ee82d58b3 2015-09-16T12:56:42Z
+github.com/juju/loggo git 8477fc936adf0e382d680310047ca27e128a309a 2015-05-27T03:58:39Z
+github.com/juju/names git 8a0aa0963bbacdc790914892e9ff942e94d6f795 2016-03-30T15:05:33Z
+github.com/juju/schema git 075de04f9b7d7580d60a1e12a0b3f50bb18e6998 2016-04-20T04:42:03Z
+github.com/juju/testing git 162fafccebf20a4207ab93d63b986c230e3f4d2e 2016-04-04T09:43:17Z
+github.com/juju/utils git eb6cb958762135bb61aed1e0951f657c674d427f 2016-04-11T02:40:59Z
+github.com/juju/version git ef897ad7f130870348ce306f61332f5335355063 2015-11-27T20:34:00Z
+golang.org/x/crypto git aedad9a179ec1ea11b7064c57cbc6dc30d7724ec 2015-08-30T18:06:42Z
+gopkg.in/check.v1 git 4f90aeace3a26ad7021961c297b22c42160c7b25 2016-01-05T16:49:36Z
+gopkg.in/mgo.v2 git 4d04138ffef2791c479c0c8bbffc30b34081b8d9 2015-10-26T16:34:53Z
+gopkg.in/yaml.v2 git a83829b6f1293c91addabc89d0571c246397bbf4 2016-03-01T20:40:22Z
diff --git a/automation/vendor/github.com/juju/gomaasapi/device.go b/automation/vendor/github.com/juju/gomaasapi/device.go
new file mode 100644
index 0000000..7c9bc70
--- /dev/null
+++ b/automation/vendor/github.com/juju/gomaasapi/device.go
@@ -0,0 +1,293 @@
+// Copyright 2016 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+package gomaasapi
+
+import (
+ "fmt"
+ "net/http"
+ "strings"
+
+ "github.com/juju/errors"
+ "github.com/juju/schema"
+ "github.com/juju/version"
+)
+
+type device struct {
+ controller *controller
+
+ resourceURI string
+
+ systemID string
+ hostname string
+ fqdn string
+
+ parent string
+ owner string
+
+ ipAddresses []string
+ interfaceSet []*interface_
+ zone *zone
+}
+
+// SystemID implements Device.
+func (d *device) SystemID() string {
+ return d.systemID
+}
+
+// Hostname implements Device.
+func (d *device) Hostname() string {
+ return d.hostname
+}
+
+// FQDN implements Device.
+func (d *device) FQDN() string {
+ return d.fqdn
+}
+
+// Parent implements Device.
+func (d *device) Parent() string {
+ return d.parent
+}
+
+// Owner implements Device.
+func (d *device) Owner() string {
+ return d.owner
+}
+
+// IPAddresses implements Device.
+func (d *device) IPAddresses() []string {
+ return d.ipAddresses
+}
+
+// Zone implements Device.
+func (d *device) Zone() Zone {
+ if d.zone == nil {
+ return nil
+ }
+ return d.zone
+}
+
+// InterfaceSet implements Device.
+func (d *device) InterfaceSet() []Interface {
+ result := make([]Interface, len(d.interfaceSet))
+ for i, v := range d.interfaceSet {
+ v.controller = d.controller
+ result[i] = v
+ }
+ return result
+}
+
+// CreateInterfaceArgs is an argument struct for passing parameters to
+// the Machine.CreateInterface method.
+type CreateInterfaceArgs struct {
+ // Name of the interface (required).
+ Name string
+ // MACAddress is the MAC address of the interface (required).
+ MACAddress string
+ // VLAN is the untagged VLAN the interface is connected to (required).
+ VLAN VLAN
+ // Tags to attach to the interface (optional).
+ Tags []string
+ // MTU - Maximum transmission unit. (optional)
+ MTU int
+ // AcceptRA - Accept router advertisements. (IPv6 only)
+ AcceptRA bool
+ // Autoconf - Perform stateless autoconfiguration. (IPv6 only)
+ Autoconf bool
+}
+
+// Validate checks the required fields are set for the arg structure.
+func (a *CreateInterfaceArgs) Validate() error {
+ if a.Name == "" {
+ return errors.NotValidf("missing Name")
+ }
+ if a.MACAddress == "" {
+ return errors.NotValidf("missing MACAddress")
+ }
+ if a.VLAN == nil {
+ return errors.NotValidf("missing VLAN")
+ }
+ return nil
+}
+
+// interfacesURI used to add interfaces for this device. The operations
+// are on the nodes endpoint, not devices.
+func (d *device) interfacesURI() string {
+ return strings.Replace(d.resourceURI, "devices", "nodes", 1) + "interfaces/"
+}
+
+// CreateInterface implements Device.
+func (d *device) CreateInterface(args CreateInterfaceArgs) (Interface, error) {
+ if err := args.Validate(); err != nil {
+ return nil, errors.Trace(err)
+ }
+ params := NewURLParams()
+ params.Values.Add("name", args.Name)
+ params.Values.Add("mac_address", args.MACAddress)
+ params.Values.Add("vlan", fmt.Sprint(args.VLAN.ID()))
+ params.MaybeAdd("tags", strings.Join(args.Tags, ","))
+ params.MaybeAddInt("mtu", args.MTU)
+ params.MaybeAddBool("accept_ra", args.AcceptRA)
+ params.MaybeAddBool("autoconf", args.Autoconf)
+ result, err := d.controller.post(d.interfacesURI(), "create_physical", params.Values)
+ if err != nil {
+ if svrErr, ok := errors.Cause(err).(ServerError); ok {
+ switch svrErr.StatusCode {
+ case http.StatusNotFound, http.StatusConflict:
+ return nil, errors.Wrap(err, NewBadRequestError(svrErr.BodyMessage))
+ case http.StatusForbidden:
+ return nil, errors.Wrap(err, NewPermissionError(svrErr.BodyMessage))
+ case http.StatusServiceUnavailable:
+ return nil, errors.Wrap(err, NewCannotCompleteError(svrErr.BodyMessage))
+ }
+ }
+ return nil, NewUnexpectedError(err)
+ }
+
+ iface, err := readInterface(d.controller.apiVersion, result)
+ if err != nil {
+ return nil, errors.Trace(err)
+ }
+ iface.controller = d.controller
+
+ // TODO: add to the interfaces for the device when the interfaces are returned.
+ // lp:bug 1567213.
+ return iface, nil
+}
+
+// Delete implements Device.
+func (d *device) Delete() error {
+ err := d.controller.delete(d.resourceURI)
+ if err != nil {
+ if svrErr, ok := errors.Cause(err).(ServerError); ok {
+ switch svrErr.StatusCode {
+ case http.StatusNotFound:
+ return errors.Wrap(err, NewNoMatchError(svrErr.BodyMessage))
+ case http.StatusForbidden:
+ return errors.Wrap(err, NewPermissionError(svrErr.BodyMessage))
+ }
+ }
+ return NewUnexpectedError(err)
+ }
+ return nil
+}
+
+func readDevice(controllerVersion version.Number, source interface{}) (*device, error) {
+ readFunc, err := getDeviceDeserializationFunc(controllerVersion)
+ if err != nil {
+ return nil, errors.Trace(err)
+ }
+
+ checker := schema.StringMap(schema.Any())
+ coerced, err := checker.Coerce(source, nil)
+ if err != nil {
+ return nil, WrapWithDeserializationError(err, "device base schema check failed")
+ }
+ valid := coerced.(map[string]interface{})
+ return readFunc(valid)
+}
+
+func readDevices(controllerVersion version.Number, source interface{}) ([]*device, error) {
+ readFunc, err := getDeviceDeserializationFunc(controllerVersion)
+ if err != nil {
+ return nil, errors.Trace(err)
+ }
+
+ checker := schema.List(schema.StringMap(schema.Any()))
+ coerced, err := checker.Coerce(source, nil)
+ if err != nil {
+ return nil, WrapWithDeserializationError(err, "device base schema check failed")
+ }
+ valid := coerced.([]interface{})
+ return readDeviceList(valid, readFunc)
+}
+
+func getDeviceDeserializationFunc(controllerVersion version.Number) (deviceDeserializationFunc, error) {
+ var deserialisationVersion version.Number
+ for v := range deviceDeserializationFuncs {
+ if v.Compare(deserialisationVersion) > 0 && v.Compare(controllerVersion) <= 0 {
+ deserialisationVersion = v
+ }
+ }
+ if deserialisationVersion == version.Zero {
+ return nil, NewUnsupportedVersionError("no device read func for version %s", controllerVersion)
+ }
+ return deviceDeserializationFuncs[deserialisationVersion], nil
+}
+
+// readDeviceList expects the values of the sourceList to be string maps.
+func readDeviceList(sourceList []interface{}, readFunc deviceDeserializationFunc) ([]*device, error) {
+ result := make([]*device, 0, len(sourceList))
+ for i, value := range sourceList {
+ source, ok := value.(map[string]interface{})
+ if !ok {
+ return nil, NewDeserializationError("unexpected value for device %d, %T", i, value)
+ }
+ device, err := readFunc(source)
+ if err != nil {
+ return nil, errors.Annotatef(err, "device %d", i)
+ }
+ result = append(result, device)
+ }
+ return result, nil
+}
+
+type deviceDeserializationFunc func(map[string]interface{}) (*device, error)
+
+var deviceDeserializationFuncs = map[version.Number]deviceDeserializationFunc{
+ twoDotOh: device_2_0,
+}
+
+func device_2_0(source map[string]interface{}) (*device, error) {
+ fields := schema.Fields{
+ "resource_uri": schema.String(),
+
+ "system_id": schema.String(),
+ "hostname": schema.String(),
+ "fqdn": schema.String(),
+ "parent": schema.OneOf(schema.Nil(""), schema.String()),
+ "owner": schema.OneOf(schema.Nil(""), schema.String()),
+
+ "ip_addresses": schema.List(schema.String()),
+ "interface_set": schema.List(schema.StringMap(schema.Any())),
+ "zone": schema.StringMap(schema.Any()),
+ }
+ defaults := schema.Defaults{
+ "owner": "",
+ "parent": "",
+ }
+ checker := schema.FieldMap(fields, defaults)
+ coerced, err := checker.Coerce(source, nil)
+ if err != nil {
+ return nil, WrapWithDeserializationError(err, "device 2.0 schema check failed")
+ }
+ valid := coerced.(map[string]interface{})
+ // From here we know that the map returned from the schema coercion
+ // contains fields of the right type.
+
+ interfaceSet, err := readInterfaceList(valid["interface_set"].([]interface{}), interface_2_0)
+ if err != nil {
+ return nil, errors.Trace(err)
+ }
+ zone, err := zone_2_0(valid["zone"].(map[string]interface{}))
+ if err != nil {
+ return nil, errors.Trace(err)
+ }
+ owner, _ := valid["owner"].(string)
+ parent, _ := valid["parent"].(string)
+ result := &device{
+ resourceURI: valid["resource_uri"].(string),
+
+ systemID: valid["system_id"].(string),
+ hostname: valid["hostname"].(string),
+ fqdn: valid["fqdn"].(string),
+ parent: parent,
+ owner: owner,
+
+ ipAddresses: convertToStringSlice(valid["ip_addresses"]),
+ interfaceSet: interfaceSet,
+ zone: zone,
+ }
+ return result, nil
+}
diff --git a/automation/vendor/github.com/juju/gomaasapi/enum.go b/automation/vendor/github.com/juju/gomaasapi/enum.go
new file mode 100644
index 0000000..a516d6b
--- /dev/null
+++ b/automation/vendor/github.com/juju/gomaasapi/enum.go
@@ -0,0 +1,57 @@
+// Copyright 2012-2016 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+package gomaasapi
+
+const (
+ // NodeStatus* values represent the vocabulary of a Node‘s possible statuses.
+
+ // The node has been created and has a system ID assigned to it.
+ NodeStatusDeclared = "0"
+
+ //Testing and other commissioning steps are taking place.
+ NodeStatusCommissioning = "1"
+
+ // Smoke or burn-in testing has a found a problem.
+ NodeStatusFailedTests = "2"
+
+ // The node can’t be contacted.
+ NodeStatusMissing = "3"
+
+ // The node is in the general pool ready to be deployed.
+ NodeStatusReady = "4"
+
+ // The node is ready for named deployment.
+ NodeStatusReserved = "5"
+
+ // The node is powering a service from a charm or is ready for use with a fresh Ubuntu install.
+ NodeStatusDeployed = "6"
+
+ // The node has been removed from service manually until an admin overrides the retirement.
+ NodeStatusRetired = "7"
+
+ // The node is broken: a step in the node lifecyle failed. More details
+ // can be found in the node's event log.
+ NodeStatusBroken = "8"
+
+ // The node is being installed.
+ NodeStatusDeploying = "9"
+
+ // The node has been allocated to a user and is ready for deployment.
+ NodeStatusAllocated = "10"
+
+ // The deployment of the node failed.
+ NodeStatusFailedDeployment = "11"
+
+ // The node is powering down after a release request.
+ NodeStatusReleasing = "12"
+
+ // The releasing of the node failed.
+ NodeStatusFailedReleasing = "13"
+
+ // The node is erasing its disks.
+ NodeStatusDiskErasing = "14"
+
+ // The node failed to erase its disks.
+ NodeStatusFailedDiskErasing = "15"
+)
diff --git a/automation/vendor/github.com/juju/gomaasapi/errors.go b/automation/vendor/github.com/juju/gomaasapi/errors.go
new file mode 100644
index 0000000..8931d56
--- /dev/null
+++ b/automation/vendor/github.com/juju/gomaasapi/errors.go
@@ -0,0 +1,161 @@
+// Copyright 2016 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+package gomaasapi
+
+import (
+ "fmt"
+
+ "github.com/juju/errors"
+)
+
+// NoMatchError is returned when the requested action cannot be performed
+// due to being unable to service due to no entities available that match the
+// request.
+type NoMatchError struct {
+ errors.Err
+}
+
+// NewNoMatchError constructs a new NoMatchError and sets the location.
+func NewNoMatchError(message string) error {
+ err := &NoMatchError{Err: errors.NewErr(message)}
+ err.SetLocation(1)
+ return err
+}
+
+// IsNoMatchError returns true if err is a NoMatchError.
+func IsNoMatchError(err error) bool {
+ _, ok := errors.Cause(err).(*NoMatchError)
+ return ok
+}
+
+// UnexpectedError is an error for a condition that hasn't been determined.
+type UnexpectedError struct {
+ errors.Err
+}
+
+// NewUnexpectedError constructs a new UnexpectedError and sets the location.
+func NewUnexpectedError(err error) error {
+ uerr := &UnexpectedError{Err: errors.NewErr("unexpected: %v", err)}
+ uerr.SetLocation(1)
+ return errors.Wrap(err, uerr)
+}
+
+// IsUnexpectedError returns true if err is an UnexpectedError.
+func IsUnexpectedError(err error) bool {
+ _, ok := errors.Cause(err).(*UnexpectedError)
+ return ok
+}
+
+// UnsupportedVersionError refers to calls made to an unsupported api version.
+type UnsupportedVersionError struct {
+ errors.Err
+}
+
+// NewUnsupportedVersionError constructs a new UnsupportedVersionError and sets the location.
+func NewUnsupportedVersionError(format string, args ...interface{}) error {
+ err := &UnsupportedVersionError{Err: errors.NewErr(format, args...)}
+ err.SetLocation(1)
+ return err
+}
+
+// IsUnsupportedVersionError returns true if err is an UnsupportedVersionError.
+func IsUnsupportedVersionError(err error) bool {
+ _, ok := errors.Cause(err).(*UnsupportedVersionError)
+ return ok
+}
+
+// DeserializationError types are returned when the returned JSON data from
+// the controller doesn't match the code's expectations.
+type DeserializationError struct {
+ errors.Err
+}
+
+// NewDeserializationError constructs a new DeserializationError and sets the location.
+func NewDeserializationError(format string, args ...interface{}) error {
+ err := &DeserializationError{Err: errors.NewErr(format, args...)}
+ err.SetLocation(1)
+ return err
+}
+
+// WrapWithDeserializationError constructs a new DeserializationError with the
+// specified message, and sets the location and returns a new error with the
+// full error stack set including the error passed in.
+func WrapWithDeserializationError(err error, format string, args ...interface{}) error {
+ message := fmt.Sprintf(format, args...)
+ // We want the deserialization error message to include the error text of the
+ // previous error, but wrap it in the new type.
+ derr := &DeserializationError{Err: errors.NewErr(message + ": " + err.Error())}
+ derr.SetLocation(1)
+ wrapped := errors.Wrap(err, derr)
+ // We want the location of the wrapped error to be the caller of this function,
+ // not the line above.
+ if errType, ok := wrapped.(*errors.Err); ok {
+ // We know it is because that is what Wrap returns.
+ errType.SetLocation(1)
+ }
+ return wrapped
+}
+
+// IsDeserializationError returns true if err is a DeserializationError.
+func IsDeserializationError(err error) bool {
+ _, ok := errors.Cause(err).(*DeserializationError)
+ return ok
+}
+
+// BadRequestError is returned when the requested action cannot be performed
+// due to bad or incorrect parameters passed to the server.
+type BadRequestError struct {
+ errors.Err
+}
+
+// NewBadRequestError constructs a new BadRequestError and sets the location.
+func NewBadRequestError(message string) error {
+ err := &BadRequestError{Err: errors.NewErr(message)}
+ err.SetLocation(1)
+ return err
+}
+
+// IsBadRequestError returns true if err is a NoMatchError.
+func IsBadRequestError(err error) bool {
+ _, ok := errors.Cause(err).(*BadRequestError)
+ return ok
+}
+
+// PermissionError is returned when the user does not have permission to do the
+// requested action.
+type PermissionError struct {
+ errors.Err
+}
+
+// NewPermissionError constructs a new PermissionError and sets the location.
+func NewPermissionError(message string) error {
+ err := &PermissionError{Err: errors.NewErr(message)}
+ err.SetLocation(1)
+ return err
+}
+
+// IsPermissionError returns true if err is a NoMatchError.
+func IsPermissionError(err error) bool {
+ _, ok := errors.Cause(err).(*PermissionError)
+ return ok
+}
+
+// CannotCompleteError is returned when the requested action is unable to
+// complete for some server side reason.
+type CannotCompleteError struct {
+ errors.Err
+}
+
+// NewCannotCompleteError constructs a new CannotCompleteError and sets the location.
+func NewCannotCompleteError(message string) error {
+ err := &CannotCompleteError{Err: errors.NewErr(message)}
+ err.SetLocation(1)
+ return err
+}
+
+// IsCannotCompleteError returns true if err is a NoMatchError.
+func IsCannotCompleteError(err error) bool {
+ _, ok := errors.Cause(err).(*CannotCompleteError)
+ return ok
+}
diff --git a/automation/vendor/github.com/juju/gomaasapi/fabric.go b/automation/vendor/github.com/juju/gomaasapi/fabric.go
new file mode 100644
index 0000000..e38a61a
--- /dev/null
+++ b/automation/vendor/github.com/juju/gomaasapi/fabric.go
@@ -0,0 +1,128 @@
+// Copyright 2016 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+package gomaasapi
+
+import (
+ "github.com/juju/errors"
+ "github.com/juju/schema"
+ "github.com/juju/version"
+)
+
+type fabric struct {
+ // Add the controller in when we need to do things with the fabric.
+ // controller Controller
+
+ resourceURI string
+
+ id int
+ name string
+ classType string
+
+ vlans []*vlan
+}
+
+// ID implements Fabric.
+func (f *fabric) ID() int {
+ return f.id
+}
+
+// Name implements Fabric.
+func (f *fabric) Name() string {
+ return f.name
+}
+
+// ClassType implements Fabric.
+func (f *fabric) ClassType() string {
+ return f.classType
+}
+
+// VLANs implements Fabric.
+func (f *fabric) VLANs() []VLAN {
+ var result []VLAN
+ for _, v := range f.vlans {
+ result = append(result, v)
+ }
+ return result
+}
+
+func readFabrics(controllerVersion version.Number, source interface{}) ([]*fabric, error) {
+ checker := schema.List(schema.StringMap(schema.Any()))
+ coerced, err := checker.Coerce(source, nil)
+ if err != nil {
+ return nil, errors.Annotatef(err, "fabric base schema check failed")
+ }
+ valid := coerced.([]interface{})
+
+ var deserialisationVersion version.Number
+ for v := range fabricDeserializationFuncs {
+ if v.Compare(deserialisationVersion) > 0 && v.Compare(controllerVersion) <= 0 {
+ deserialisationVersion = v
+ }
+ }
+ if deserialisationVersion == version.Zero {
+ return nil, errors.Errorf("no fabric read func for version %s", controllerVersion)
+ }
+ readFunc := fabricDeserializationFuncs[deserialisationVersion]
+ return readFabricList(valid, readFunc)
+}
+
+// readFabricList expects the values of the sourceList to be string maps.
+func readFabricList(sourceList []interface{}, readFunc fabricDeserializationFunc) ([]*fabric, error) {
+ result := make([]*fabric, 0, len(sourceList))
+ for i, value := range sourceList {
+ source, ok := value.(map[string]interface{})
+ if !ok {
+ return nil, errors.Errorf("unexpected value for fabric %d, %T", i, value)
+ }
+ fabric, err := readFunc(source)
+ if err != nil {
+ return nil, errors.Annotatef(err, "fabric %d", i)
+ }
+ result = append(result, fabric)
+ }
+ return result, nil
+}
+
+type fabricDeserializationFunc func(map[string]interface{}) (*fabric, error)
+
+var fabricDeserializationFuncs = map[version.Number]fabricDeserializationFunc{
+ twoDotOh: fabric_2_0,
+}
+
+func fabric_2_0(source map[string]interface{}) (*fabric, error) {
+ fields := schema.Fields{
+ "resource_uri": schema.String(),
+ "id": schema.ForceInt(),
+ "name": schema.String(),
+ "class_type": schema.OneOf(schema.Nil(""), schema.String()),
+ "vlans": schema.List(schema.StringMap(schema.Any())),
+ }
+ checker := schema.FieldMap(fields, nil) // no defaults
+ coerced, err := checker.Coerce(source, nil)
+ if err != nil {
+ return nil, errors.Annotatef(err, "fabric 2.0 schema check failed")
+ }
+ valid := coerced.(map[string]interface{})
+ // From here we know that the map returned from the schema coercion
+ // contains fields of the right type.
+
+ vlans, err := readVLANList(valid["vlans"].([]interface{}), vlan_2_0)
+ if err != nil {
+ return nil, errors.Trace(err)
+ }
+
+ // Since the class_type is optional, we use the two part cast assignment. If
+ // the cast fails, then we get the default value we care about, which is the
+ // empty string.
+ classType, _ := valid["class_type"].(string)
+
+ result := &fabric{
+ resourceURI: valid["resource_uri"].(string),
+ id: valid["id"].(int),
+ name: valid["name"].(string),
+ classType: classType,
+ vlans: vlans,
+ }
+ return result, nil
+}
diff --git a/automation/vendor/github.com/juju/gomaasapi/file.go b/automation/vendor/github.com/juju/gomaasapi/file.go
new file mode 100644
index 0000000..63fb854
--- /dev/null
+++ b/automation/vendor/github.com/juju/gomaasapi/file.go
@@ -0,0 +1,181 @@
+// Copyright 2016 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+package gomaasapi
+
+import (
+ "encoding/base64"
+ "net/http"
+ "net/url"
+
+ "github.com/juju/errors"
+ "github.com/juju/schema"
+ "github.com/juju/version"
+)
+
+type file struct {
+ controller *controller
+
+ resourceURI string
+ filename string
+ anonymousURI *url.URL
+ content string
+}
+
+// Filename implements File.
+func (f *file) Filename() string {
+ return f.filename
+}
+
+// AnonymousURL implements File.
+func (f *file) AnonymousURL() string {
+ url := f.controller.client.GetURL(f.anonymousURI)
+ return url.String()
+}
+
+// Delete implements File.
+func (f *file) Delete() error {
+ err := f.controller.delete(f.resourceURI)
+ if err != nil {
+ if svrErr, ok := errors.Cause(err).(ServerError); ok {
+ switch svrErr.StatusCode {
+ case http.StatusNotFound:
+ return errors.Wrap(err, NewNoMatchError(svrErr.BodyMessage))
+ case http.StatusForbidden:
+ return errors.Wrap(err, NewPermissionError(svrErr.BodyMessage))
+ }
+ }
+ return NewUnexpectedError(err)
+ }
+ return nil
+}
+
+// ReadAll implements File.
+func (f *file) ReadAll() ([]byte, error) {
+ if f.content == "" {
+ return f.readFromServer()
+ }
+ bytes, err := base64.StdEncoding.DecodeString(f.content)
+ if err != nil {
+ return nil, NewUnexpectedError(err)
+ }
+ return bytes, nil
+}
+
+func (f *file) readFromServer() ([]byte, error) {
+ // If the content is available, it is base64 encoded, so
+ args := make(url.Values)
+ args.Add("filename", f.filename)
+ bytes, err := f.controller._getRaw("files", "get", args)
+ if err != nil {
+ if svrErr, ok := errors.Cause(err).(ServerError); ok {
+ switch svrErr.StatusCode {
+ case http.StatusNotFound:
+ return nil, errors.Wrap(err, NewNoMatchError(svrErr.BodyMessage))
+ case http.StatusForbidden:
+ return nil, errors.Wrap(err, NewPermissionError(svrErr.BodyMessage))
+ }
+ }
+ return nil, NewUnexpectedError(err)
+ }
+ return bytes, nil
+}
+
+func readFiles(controllerVersion version.Number, source interface{}) ([]*file, error) {
+ readFunc, err := getFileDeserializationFunc(controllerVersion)
+ if err != nil {
+ return nil, errors.Trace(err)
+ }
+
+ checker := schema.List(schema.StringMap(schema.Any()))
+ coerced, err := checker.Coerce(source, nil)
+ if err != nil {
+ return nil, WrapWithDeserializationError(err, "file base schema check failed")
+ }
+ valid := coerced.([]interface{})
+ return readFileList(valid, readFunc)
+}
+
+func readFile(controllerVersion version.Number, source interface{}) (*file, error) {
+ readFunc, err := getFileDeserializationFunc(controllerVersion)
+ if err != nil {
+ return nil, errors.Trace(err)
+ }
+
+ checker := schema.StringMap(schema.Any())
+ coerced, err := checker.Coerce(source, nil)
+ if err != nil {
+ return nil, WrapWithDeserializationError(err, "file base schema check failed")
+ }
+ valid := coerced.(map[string]interface{})
+ return readFunc(valid)
+}
+
+func getFileDeserializationFunc(controllerVersion version.Number) (fileDeserializationFunc, error) {
+ var deserialisationVersion version.Number
+ for v := range fileDeserializationFuncs {
+ if v.Compare(deserialisationVersion) > 0 && v.Compare(controllerVersion) <= 0 {
+ deserialisationVersion = v
+ }
+ }
+ if deserialisationVersion == version.Zero {
+ return nil, NewUnsupportedVersionError("no file read func for version %s", controllerVersion)
+ }
+ return fileDeserializationFuncs[deserialisationVersion], nil
+}
+
+// readFileList expects the values of the sourceList to be string maps.
+func readFileList(sourceList []interface{}, readFunc fileDeserializationFunc) ([]*file, error) {
+ result := make([]*file, 0, len(sourceList))
+ for i, value := range sourceList {
+ source, ok := value.(map[string]interface{})
+ if !ok {
+ return nil, NewDeserializationError("unexpected value for file %d, %T", i, value)
+ }
+ file, err := readFunc(source)
+ if err != nil {
+ return nil, errors.Annotatef(err, "file %d", i)
+ }
+ result = append(result, file)
+ }
+ return result, nil
+}
+
+type fileDeserializationFunc func(map[string]interface{}) (*file, error)
+
+var fileDeserializationFuncs = map[version.Number]fileDeserializationFunc{
+ twoDotOh: file_2_0,
+}
+
+func file_2_0(source map[string]interface{}) (*file, error) {
+ fields := schema.Fields{
+ "resource_uri": schema.String(),
+ "filename": schema.String(),
+ "anon_resource_uri": schema.String(),
+ "content": schema.String(),
+ }
+ defaults := schema.Defaults{
+ "content": "",
+ }
+ checker := schema.FieldMap(fields, defaults)
+ coerced, err := checker.Coerce(source, nil)
+ if err != nil {
+ return nil, WrapWithDeserializationError(err, "file 2.0 schema check failed")
+ }
+ valid := coerced.(map[string]interface{})
+ // From here we know that the map returned from the schema coercion
+ // contains fields of the right type.
+
+ anonURI, err := url.ParseRequestURI(valid["anon_resource_uri"].(string))
+ if err != nil {
+ return nil, NewUnexpectedError(err)
+ }
+
+ result := &file{
+ resourceURI: valid["resource_uri"].(string),
+ filename: valid["filename"].(string),
+ anonymousURI: anonURI,
+ content: valid["content"].(string),
+ }
+ return result, nil
+}
diff --git a/automation/vendor/github.com/juju/gomaasapi/filesystem.go b/automation/vendor/github.com/juju/gomaasapi/filesystem.go
new file mode 100644
index 0000000..4514e52
--- /dev/null
+++ b/automation/vendor/github.com/juju/gomaasapi/filesystem.go
@@ -0,0 +1,69 @@
+// Copyright 2016 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+package gomaasapi
+
+import "github.com/juju/schema"
+
+type filesystem struct {
+ fstype string
+ mountPoint string
+ label string
+ uuid string
+ // no idea what the mount_options are as a value type, so ignoring for now.
+}
+
+// Type implements FileSystem.
+func (f *filesystem) Type() string {
+ return f.fstype
+}
+
+// MountPoint implements FileSystem.
+func (f *filesystem) MountPoint() string {
+ return f.mountPoint
+}
+
+// Label implements FileSystem.
+func (f *filesystem) Label() string {
+ return f.label
+}
+
+// UUID implements FileSystem.
+func (f *filesystem) UUID() string {
+ return f.uuid
+}
+
+// There is no need for controller based parsing of filesystems until we need it.
+// Currently the filesystem reading is only called by the Partition parsing.
+
+func filesystem2_0(source map[string]interface{}) (*filesystem, error) {
+ fields := schema.Fields{
+ "fstype": schema.String(),
+ "mount_point": schema.OneOf(schema.Nil(""), schema.String()),
+ "label": schema.OneOf(schema.Nil(""), schema.String()),
+ "uuid": schema.String(),
+ // TODO: mount_options when we know the type (note it can be
+ // nil).
+ }
+ defaults := schema.Defaults{
+ "mount_point": "",
+ "label": "",
+ }
+ checker := schema.FieldMap(fields, defaults)
+ coerced, err := checker.Coerce(source, nil)
+ if err != nil {
+ return nil, WrapWithDeserializationError(err, "filesystem 2.0 schema check failed")
+ }
+ valid := coerced.(map[string]interface{})
+ // From here we know that the map returned from the schema coercion
+ // contains fields of the right type.
+ mount_point, _ := valid["mount_point"].(string)
+ label, _ := valid["label"].(string)
+ result := &filesystem{
+ fstype: valid["fstype"].(string),
+ mountPoint: mount_point,
+ label: label,
+ uuid: valid["uuid"].(string),
+ }
+ return result, nil
+}
diff --git a/automation/vendor/github.com/juju/gomaasapi/gomaasapi.go b/automation/vendor/github.com/juju/gomaasapi/gomaasapi.go
new file mode 100644
index 0000000..f457e29
--- /dev/null
+++ b/automation/vendor/github.com/juju/gomaasapi/gomaasapi.go
@@ -0,0 +1,4 @@
+// Copyright 2012-2016 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+package gomaasapi
diff --git a/automation/vendor/github.com/juju/gomaasapi/interface.go b/automation/vendor/github.com/juju/gomaasapi/interface.go
new file mode 100644
index 0000000..f30a9a8
--- /dev/null
+++ b/automation/vendor/github.com/juju/gomaasapi/interface.go
@@ -0,0 +1,440 @@
+// Copyright 2016 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+package gomaasapi
+
+import (
+ "fmt"
+ "net/http"
+
+ "github.com/juju/errors"
+ "github.com/juju/schema"
+ "github.com/juju/version"
+)
+
+// Can't use interface as a type, so add an underscore. Yay.
+type interface_ struct {
+ controller *controller
+
+ resourceURI string
+
+ id int
+ name string
+ type_ string
+ enabled bool
+ tags []string
+
+ vlan *vlan
+ links []*link
+
+ macAddress string
+ effectiveMTU int
+
+ parents []string
+ children []string
+}
+
+func (i *interface_) updateFrom(other *interface_) {
+ i.resourceURI = other.resourceURI
+ i.id = other.id
+ i.name = other.name
+ i.type_ = other.type_
+ i.enabled = other.enabled
+ i.tags = other.tags
+ i.vlan = other.vlan
+ i.links = other.links
+ i.macAddress = other.macAddress
+ i.effectiveMTU = other.effectiveMTU
+ i.parents = other.parents
+ i.children = other.children
+}
+
+// ID implements Interface.
+func (i *interface_) ID() int {
+ return i.id
+}
+
+// Name implements Interface.
+func (i *interface_) Name() string {
+ return i.name
+}
+
+// Parents implements Interface.
+func (i *interface_) Parents() []string {
+ return i.parents
+}
+
+// Children implements Interface.
+func (i *interface_) Children() []string {
+ return i.children
+}
+
+// Type implements Interface.
+func (i *interface_) Type() string {
+ return i.type_
+}
+
+// Enabled implements Interface.
+func (i *interface_) Enabled() bool {
+ return i.enabled
+}
+
+// Tags implements Interface.
+func (i *interface_) Tags() []string {
+ return i.tags
+}
+
+// VLAN implements Interface.
+func (i *interface_) VLAN() VLAN {
+ if i.vlan == nil {
+ return nil
+ }
+ return i.vlan
+}
+
+// Links implements Interface.
+func (i *interface_) Links() []Link {
+ result := make([]Link, len(i.links))
+ for i, link := range i.links {
+ result[i] = link
+ }
+ return result
+}
+
+// MACAddress implements Interface.
+func (i *interface_) MACAddress() string {
+ return i.macAddress
+}
+
+// EffectiveMTU implements Interface.
+func (i *interface_) EffectiveMTU() int {
+ return i.effectiveMTU
+}
+
+// UpdateInterfaceArgs is an argument struct for calling Interface.Update.
+type UpdateInterfaceArgs struct {
+ Name string
+ MACAddress string
+ VLAN VLAN
+}
+
+func (a *UpdateInterfaceArgs) vlanID() int {
+ if a.VLAN == nil {
+ return 0
+ }
+ return a.VLAN.ID()
+}
+
+// Update implements Interface.
+func (i *interface_) Update(args UpdateInterfaceArgs) error {
+ var empty UpdateInterfaceArgs
+ if args == empty {
+ return nil
+ }
+ params := NewURLParams()
+ params.MaybeAdd("name", args.Name)
+ params.MaybeAdd("mac_address", args.MACAddress)
+ params.MaybeAddInt("vlan", args.vlanID())
+ source, err := i.controller.put(i.resourceURI, params.Values)
+ if err != nil {
+ if svrErr, ok := errors.Cause(err).(ServerError); ok {
+ switch svrErr.StatusCode {
+ case http.StatusNotFound:
+ return errors.Wrap(err, NewNoMatchError(svrErr.BodyMessage))
+ case http.StatusForbidden:
+ return errors.Wrap(err, NewPermissionError(svrErr.BodyMessage))
+ }
+ }
+ return NewUnexpectedError(err)
+ }
+
+ response, err := readInterface(i.controller.apiVersion, source)
+ if err != nil {
+ return errors.Trace(err)
+ }
+ i.updateFrom(response)
+ return nil
+}
+
+// Delete implements Interface.
+func (i *interface_) Delete() error {
+ err := i.controller.delete(i.resourceURI)
+ if err != nil {
+ if svrErr, ok := errors.Cause(err).(ServerError); ok {
+ switch svrErr.StatusCode {
+ case http.StatusNotFound:
+ return errors.Wrap(err, NewNoMatchError(svrErr.BodyMessage))
+ case http.StatusForbidden:
+ return errors.Wrap(err, NewPermissionError(svrErr.BodyMessage))
+ }
+ }
+ return NewUnexpectedError(err)
+ }
+ return nil
+}
+
+// InterfaceLinkMode is the type of the various link mode constants used for
+// LinkSubnetArgs.
+type InterfaceLinkMode string
+
+const (
+ // LinkModeDHCP - Bring the interface up with DHCP on the given subnet. Only
+ // one subnet can be set to DHCP. If the subnet is managed this interface
+ // will pull from the dynamic IP range.
+ LinkModeDHCP InterfaceLinkMode = "DHCP"
+
+ // LinkModeStatic - Bring the interface up with a STATIC IP address on the
+ // given subnet. Any number of STATIC links can exist on an interface.
+ LinkModeStatic InterfaceLinkMode = "STATIC"
+
+ // LinkModeLinkUp - Bring the interface up only on the given subnet. No IP
+ // address will be assigned to this interface. The interface cannot have any
+ // current DHCP or STATIC links.
+ LinkModeLinkUp InterfaceLinkMode = "LINK_UP"
+)
+
+// LinkSubnetArgs is an argument struct for passing parameters to
+// the Interface.LinkSubnet method.
+type LinkSubnetArgs struct {
+ // Mode is used to describe how the address is provided for the Link.
+ // Required field.
+ Mode InterfaceLinkMode
+ // Subnet is the subnet to link to. Required field.
+ Subnet Subnet
+ // IPAddress is only valid when the Mode is set to LinkModeStatic. If
+ // not specified with a Mode of LinkModeStatic, an IP address from the
+ // subnet will be auto selected.
+ IPAddress string
+ // DefaultGateway will set the gateway IP address for the Subnet as the
+ // default gateway for the machine or device the interface belongs to.
+ // Option can only be used with mode LinkModeStatic.
+ DefaultGateway bool
+}
+
+// Validate ensures that the Mode and Subnet are set, and that the other options
+// are consistent with the Mode.
+func (a *LinkSubnetArgs) Validate() error {
+ switch a.Mode {
+ case LinkModeDHCP, LinkModeLinkUp, LinkModeStatic:
+ case "":
+ return errors.NotValidf("missing Mode")
+ default:
+ return errors.NotValidf("unknown Mode value (%q)", a.Mode)
+ }
+ if a.Subnet == nil {
+ return errors.NotValidf("missing Subnet")
+ }
+ if a.IPAddress != "" && a.Mode != LinkModeStatic {
+ return errors.NotValidf("setting IP Address when Mode is not LinkModeStatic")
+ }
+ if a.DefaultGateway && a.Mode != LinkModeStatic {
+ return errors.NotValidf("specifying DefaultGateway for Mode %q", a.Mode)
+ }
+ return nil
+}
+
+// LinkSubnet implements Interface.
+func (i *interface_) LinkSubnet(args LinkSubnetArgs) error {
+ if err := args.Validate(); err != nil {
+ return errors.Trace(err)
+ }
+ params := NewURLParams()
+ params.Values.Add("mode", string(args.Mode))
+ params.Values.Add("subnet", fmt.Sprint(args.Subnet.ID()))
+ params.MaybeAdd("ip_address", args.IPAddress)
+ params.MaybeAddBool("default_gateway", args.DefaultGateway)
+ source, err := i.controller.post(i.resourceURI, "link_subnet", params.Values)
+ if err != nil {
+ if svrErr, ok := errors.Cause(err).(ServerError); ok {
+ switch svrErr.StatusCode {
+ case http.StatusNotFound, http.StatusBadRequest:
+ return errors.Wrap(err, NewBadRequestError(svrErr.BodyMessage))
+ case http.StatusForbidden:
+ return errors.Wrap(err, NewPermissionError(svrErr.BodyMessage))
+ case http.StatusServiceUnavailable:
+ return errors.Wrap(err, NewCannotCompleteError(svrErr.BodyMessage))
+ }
+ }
+ return NewUnexpectedError(err)
+ }
+
+ response, err := readInterface(i.controller.apiVersion, source)
+ if err != nil {
+ return errors.Trace(err)
+ }
+ i.updateFrom(response)
+ return nil
+}
+
+func (i *interface_) linkForSubnet(subnet Subnet) *link {
+ for _, link := range i.links {
+ if s := link.Subnet(); s != nil && s.ID() == subnet.ID() {
+ return link
+ }
+ }
+ return nil
+}
+
+// LinkSubnet implements Interface.
+func (i *interface_) UnlinkSubnet(subnet Subnet) error {
+ if subnet == nil {
+ return errors.NotValidf("missing Subnet")
+ }
+ link := i.linkForSubnet(subnet)
+ if link == nil {
+ return errors.NotValidf("unlinked Subnet")
+ }
+ params := NewURLParams()
+ params.Values.Add("id", fmt.Sprint(link.ID()))
+ source, err := i.controller.post(i.resourceURI, "unlink_subnet", params.Values)
+ if err != nil {
+ if svrErr, ok := errors.Cause(err).(ServerError); ok {
+ switch svrErr.StatusCode {
+ case http.StatusNotFound, http.StatusBadRequest:
+ return errors.Wrap(err, NewBadRequestError(svrErr.BodyMessage))
+ case http.StatusForbidden:
+ return errors.Wrap(err, NewPermissionError(svrErr.BodyMessage))
+ }
+ }
+ return NewUnexpectedError(err)
+ }
+
+ response, err := readInterface(i.controller.apiVersion, source)
+ if err != nil {
+ return errors.Trace(err)
+ }
+ i.updateFrom(response)
+
+ return nil
+}
+
+func readInterface(controllerVersion version.Number, source interface{}) (*interface_, error) {
+ readFunc, err := getInterfaceDeserializationFunc(controllerVersion)
+ if err != nil {
+ return nil, errors.Trace(err)
+ }
+
+ checker := schema.StringMap(schema.Any())
+ coerced, err := checker.Coerce(source, nil)
+ if err != nil {
+ return nil, WrapWithDeserializationError(err, "interface base schema check failed")
+ }
+ valid := coerced.(map[string]interface{})
+ return readFunc(valid)
+}
+
+func readInterfaces(controllerVersion version.Number, source interface{}) ([]*interface_, error) {
+ readFunc, err := getInterfaceDeserializationFunc(controllerVersion)
+ if err != nil {
+ return nil, errors.Trace(err)
+ }
+
+ checker := schema.List(schema.StringMap(schema.Any()))
+ coerced, err := checker.Coerce(source, nil)
+ if err != nil {
+ return nil, WrapWithDeserializationError(err, "interface base schema check failed")
+ }
+ valid := coerced.([]interface{})
+ return readInterfaceList(valid, readFunc)
+}
+
+func getInterfaceDeserializationFunc(controllerVersion version.Number) (interfaceDeserializationFunc, error) {
+ var deserialisationVersion version.Number
+ for v := range interfaceDeserializationFuncs {
+ if v.Compare(deserialisationVersion) > 0 && v.Compare(controllerVersion) <= 0 {
+ deserialisationVersion = v
+ }
+ }
+ if deserialisationVersion == version.Zero {
+ return nil, NewUnsupportedVersionError("no interface read func for version %s", controllerVersion)
+ }
+ return interfaceDeserializationFuncs[deserialisationVersion], nil
+}
+
+func readInterfaceList(sourceList []interface{}, readFunc interfaceDeserializationFunc) ([]*interface_, error) {
+ result := make([]*interface_, 0, len(sourceList))
+ for i, value := range sourceList {
+ source, ok := value.(map[string]interface{})
+ if !ok {
+ return nil, NewDeserializationError("unexpected value for interface %d, %T", i, value)
+ }
+ read, err := readFunc(source)
+ if err != nil {
+ return nil, errors.Annotatef(err, "interface %d", i)
+ }
+ result = append(result, read)
+ }
+ return result, nil
+}
+
+type interfaceDeserializationFunc func(map[string]interface{}) (*interface_, error)
+
+var interfaceDeserializationFuncs = map[version.Number]interfaceDeserializationFunc{
+ twoDotOh: interface_2_0,
+}
+
+func interface_2_0(source map[string]interface{}) (*interface_, error) {
+ fields := schema.Fields{
+ "resource_uri": schema.String(),
+
+ "id": schema.ForceInt(),
+ "name": schema.String(),
+ "type": schema.String(),
+ "enabled": schema.Bool(),
+ "tags": schema.OneOf(schema.Nil(""), schema.List(schema.String())),
+
+ "vlan": schema.OneOf(schema.Nil(""), schema.StringMap(schema.Any())),
+ "links": schema.List(schema.StringMap(schema.Any())),
+
+ "mac_address": schema.OneOf(schema.Nil(""), schema.String()),
+ "effective_mtu": schema.ForceInt(),
+
+ "parents": schema.List(schema.String()),
+ "children": schema.List(schema.String()),
+ }
+ defaults := schema.Defaults{
+ "mac_address": "",
+ }
+ checker := schema.FieldMap(fields, defaults)
+ coerced, err := checker.Coerce(source, nil)
+ if err != nil {
+ return nil, WrapWithDeserializationError(err, "interface 2.0 schema check failed")
+ }
+ valid := coerced.(map[string]interface{})
+ // From here we know that the map returned from the schema coercion
+ // contains fields of the right type.
+
+ var vlan *vlan
+ // If it's not an attribute map then we know it's nil from the schema check.
+ if vlanMap, ok := valid["vlan"].(map[string]interface{}); ok {
+ vlan, err = vlan_2_0(vlanMap)
+ if err != nil {
+ return nil, errors.Trace(err)
+ }
+ }
+
+ links, err := readLinkList(valid["links"].([]interface{}), link_2_0)
+ if err != nil {
+ return nil, errors.Trace(err)
+ }
+ macAddress, _ := valid["mac_address"].(string)
+ result := &interface_{
+ resourceURI: valid["resource_uri"].(string),
+
+ id: valid["id"].(int),
+ name: valid["name"].(string),
+ type_: valid["type"].(string),
+ enabled: valid["enabled"].(bool),
+ tags: convertToStringSlice(valid["tags"]),
+
+ vlan: vlan,
+ links: links,
+
+ macAddress: macAddress,
+ effectiveMTU: valid["effective_mtu"].(int),
+
+ parents: convertToStringSlice(valid["parents"]),
+ children: convertToStringSlice(valid["children"]),
+ }
+ return result, nil
+}
diff --git a/automation/vendor/github.com/juju/gomaasapi/interfaces.go b/automation/vendor/github.com/juju/gomaasapi/interfaces.go
new file mode 100644
index 0000000..6b80115
--- /dev/null
+++ b/automation/vendor/github.com/juju/gomaasapi/interfaces.go
@@ -0,0 +1,362 @@
+// Copyright 2016 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+package gomaasapi
+
+import "github.com/juju/utils/set"
+
+const (
+ // Capability constants.
+ NetworksManagement = "networks-management"
+ StaticIPAddresses = "static-ipaddresses"
+ IPv6DeploymentUbuntu = "ipv6-deployment-ubuntu"
+ DevicesManagement = "devices-management"
+ StorageDeploymentUbuntu = "storage-deployment-ubuntu"
+ NetworkDeploymentUbuntu = "network-deployment-ubuntu"
+)
+
+// Controller represents an API connection to a MAAS Controller. Since the API
+// is restful, there is no long held connection to the API server, but instead
+// HTTP calls are made and JSON response structures parsed.
+type Controller interface {
+
+ // Capabilities returns a set of capabilities as defined by the string
+ // constants.
+ Capabilities() set.Strings
+
+ BootResources() ([]BootResource, error)
+
+ // Fabrics returns the list of Fabrics defined in the MAAS controller.
+ Fabrics() ([]Fabric, error)
+
+ // Spaces returns the list of Spaces defined in the MAAS controller.
+ Spaces() ([]Space, error)
+
+ // Zones lists all the zones known to the MAAS controller.
+ Zones() ([]Zone, error)
+
+ // Machines returns a list of machines that match the params.
+ Machines(MachinesArgs) ([]Machine, error)
+
+ // AllocateMachine will attempt to allocate a machine to the user.
+ // If successful, the allocated machine is returned.
+ AllocateMachine(AllocateMachineArgs) (Machine, ConstraintMatches, error)
+
+ // ReleaseMachines will stop the specified machines, and release them
+ // from the user making them available to be allocated again.
+ ReleaseMachines(ReleaseMachinesArgs) error
+
+ // Devices returns a list of devices that match the params.
+ Devices(DevicesArgs) ([]Device, error)
+
+ // CreateDevice creates and returns a new Device.
+ CreateDevice(CreateDeviceArgs) (Device, error)
+
+ // Files returns all the files that match the specified prefix.
+ Files(prefix string) ([]File, error)
+
+ // Return a single file by its filename.
+ GetFile(filename string) (File, error)
+
+ // AddFile adds or replaces the content of the specified filename.
+ // If or when the MAAS api is able to return metadata about a single
+ // file without sending the content of the file, we can return a File
+ // instance here too.
+ AddFile(AddFileArgs) error
+}
+
+// File represents a file stored in the MAAS controller.
+type File interface {
+ // Filename is the name of the file. No path, just the filename.
+ Filename() string
+
+ // AnonymousURL is a URL that can be used to retrieve the conents of the
+ // file without credentials.
+ AnonymousURL() string
+
+ // Delete removes the file from the MAAS controller.
+ Delete() error
+
+ // ReadAll returns the content of the file.
+ ReadAll() ([]byte, error)
+}
+
+// Fabric represents a set of interconnected VLANs that are capable of mutual
+// communication. A fabric can be thought of as a logical grouping in which
+// VLANs can be considered unique.
+//
+// For example, a distributed network may have a fabric in London containing
+// VLAN 100, while a separate fabric in San Francisco may contain a VLAN 100,
+// whose attached subnets are completely different and unrelated.
+type Fabric interface {
+ ID() int
+ Name() string
+ ClassType() string
+
+ VLANs() []VLAN
+}
+
+// VLAN represents an instance of a Virtual LAN. VLANs are a common way to
+// create logically separate networks using the same physical infrastructure.
+//
+// Managed switches can assign VLANs to each port in either a “tagged” or an
+// “untagged” manner. A VLAN is said to be “untagged” on a particular port when
+// it is the default VLAN for that port, and requires no special configuration
+// in order to access.
+//
+// “Tagged” VLANs (traditionally used by network administrators in order to
+// aggregate multiple networks over inter-switch “trunk” lines) can also be used
+// with nodes in MAAS. That is, if a switch port is configured such that
+// “tagged” VLAN frames can be sent and received by a MAAS node, that MAAS node
+// can be configured to automatically bring up VLAN interfaces, so that the
+// deployed node can make use of them.
+//
+// A “Default VLAN” is created for every Fabric, to which every new VLAN-aware
+// object in the fabric will be associated to by default (unless otherwise
+// specified).
+type VLAN interface {
+ ID() int
+ Name() string
+ Fabric() string
+
+ // VID is the VLAN ID. eth0.10 -> VID = 10.
+ VID() int
+ // MTU (maximum transmission unit) is the largest size packet or frame,
+ // specified in octets (eight-bit bytes), that can be sent.
+ MTU() int
+ DHCP() bool
+
+ PrimaryRack() string
+ SecondaryRack() string
+}
+
+// Zone represents a physical zone that a Machine is in. The meaning of a
+// physical zone is up to you: it could identify e.g. a server rack, a network,
+// or a data centre. Users can then allocate nodes from specific physical zones,
+// to suit their redundancy or performance requirements.
+type Zone interface {
+ Name() string
+ Description() string
+}
+
+// BootResource is the bomb... find something to say here.
+type BootResource interface {
+ ID() int
+ Name() string
+ Type() string
+ Architecture() string
+ SubArchitectures() set.Strings
+ KernelFlavor() string
+}
+
+// Device represents some form of device in MAAS.
+type Device interface {
+ // TODO: add domain
+ SystemID() string
+ Hostname() string
+ FQDN() string
+ IPAddresses() []string
+ Zone() Zone
+
+ // Parent returns the SystemID of the Parent. Most often this will be a
+ // Machine.
+ Parent() string
+
+ // Owner is the username of the user that created the device.
+ Owner() string
+
+ // InterfaceSet returns all the interfaces for the Device.
+ InterfaceSet() []Interface
+
+ // CreateInterface will create a physical interface for this machine.
+ CreateInterface(CreateInterfaceArgs) (Interface, error)
+
+ // Delete will remove this Device.
+ Delete() error
+}
+
+// Machine represents a physical machine.
+type Machine interface {
+ OwnerDataHolder
+
+ SystemID() string
+ Hostname() string
+ FQDN() string
+ Tags() []string
+
+ OperatingSystem() string
+ DistroSeries() string
+ Architecture() string
+ Memory() int
+ CPUCount() int
+
+ IPAddresses() []string
+ PowerState() string
+
+ // Devices returns a list of devices that match the params and have
+ // this Machine as the parent.
+ Devices(DevicesArgs) ([]Device, error)
+
+ // Consider bundling the status values into a single struct.
+ // but need to check for consistent representation if exposed on other
+ // entities.
+
+ StatusName() string
+ StatusMessage() string
+
+ // BootInterface returns the interface that was used to boot the Machine.
+ BootInterface() Interface
+ // InterfaceSet returns all the interfaces for the Machine.
+ InterfaceSet() []Interface
+ // Interface returns the interface for the machine that matches the id
+ // specified. If there is no match, nil is returned.
+ Interface(id int) Interface
+
+ // PhysicalBlockDevices returns all the physical block devices on the machine.
+ PhysicalBlockDevices() []BlockDevice
+ // PhysicalBlockDevice returns the physical block device for the machine
+ // that matches the id specified. If there is no match, nil is returned.
+ PhysicalBlockDevice(id int) BlockDevice
+
+ // BlockDevices returns all the physical and virtual block devices on the machine.
+ BlockDevices() []BlockDevice
+
+ Zone() Zone
+
+ // Start the machine and install the operating system specified in the args.
+ Start(StartArgs) error
+
+ // CreateDevice creates a new Device with this Machine as the parent.
+ // The device will have one interface that is linked to the specified subnet.
+ CreateDevice(CreateMachineDeviceArgs) (Device, error)
+}
+
+// Space is a name for a collection of Subnets.
+type Space interface {
+ ID() int
+ Name() string
+ Subnets() []Subnet
+}
+
+// Subnet refers to an IP range on a VLAN.
+type Subnet interface {
+ ID() int
+ Name() string
+ Space() string
+ VLAN() VLAN
+
+ Gateway() string
+ CIDR() string
+ // dns_mode
+
+ // DNSServers is a list of ip addresses of the DNS servers for the subnet.
+ // This list may be empty.
+ DNSServers() []string
+}
+
+// Interface represents a physical or virtual network interface on a Machine.
+type Interface interface {
+ ID() int
+ Name() string
+ // The parents of an interface are the names of interfaces that must exist
+ // for this interface to exist. For example a parent of "eth0.100" would be
+ // "eth0". Parents may be empty.
+ Parents() []string
+ // The children interfaces are the names of those that are dependent on this
+ // interface existing. Children may be empty.
+ Children() []string
+ Type() string
+ Enabled() bool
+ Tags() []string
+
+ VLAN() VLAN
+ Links() []Link
+
+ MACAddress() string
+ EffectiveMTU() int
+
+ // Params is a JSON field, and defaults to an empty string, but is almost
+ // always a JSON object in practice. Gleefully ignoring it until we need it.
+
+ // Update the name, mac address or VLAN.
+ Update(UpdateInterfaceArgs) error
+
+ // Delete this interface.
+ Delete() error
+
+ // LinkSubnet will attempt to make this interface available on the specified
+ // Subnet.
+ LinkSubnet(LinkSubnetArgs) error
+
+ // UnlinkSubnet will remove the Link to the subnet, and release the IP
+ // address associated if there is one.
+ UnlinkSubnet(Subnet) error
+}
+
+// Link represents a network link between an Interface and a Subnet.
+type Link interface {
+ ID() int
+ Mode() string
+ Subnet() Subnet
+ // IPAddress returns the address if one has been assigned.
+ // If unavailble, the address will be empty.
+ IPAddress() string
+}
+
+// FileSystem represents a formatted filesystem mounted at a location.
+type FileSystem interface {
+ // Type is the format type, e.g. "ext4".
+ Type() string
+
+ MountPoint() string
+ Label() string
+ UUID() string
+}
+
+// Partition represents a partition of a block device. It may be mounted
+// as a filesystem.
+type Partition interface {
+ ID() int
+ Path() string
+ // FileSystem may be nil if not mounted.
+ FileSystem() FileSystem
+ UUID() string
+ // UsedFor is a human readable string.
+ UsedFor() string
+ // Size is the number of bytes in the partition.
+ Size() uint64
+}
+
+// BlockDevice represents an entire block device on the machine.
+type BlockDevice interface {
+ ID() int
+ Name() string
+ Model() string
+ Path() string
+ UsedFor() string
+ Tags() []string
+
+ BlockSize() uint64
+ UsedSize() uint64
+ Size() uint64
+
+ Partitions() []Partition
+
+ // There are some other attributes for block devices, but we can
+ // expose them on an as needed basis.
+}
+
+// OwnerDataHolder represents any MAAS object that can store key/value
+// data.
+type OwnerDataHolder interface {
+ // OwnerData returns a copy of the key/value data stored for this
+ // object.
+ OwnerData() map[string]string
+
+ // SetOwnerData updates the key/value data stored for this object
+ // with the values passed in. Existing keys that aren't specified
+ // in the map passed in will be left in place; to clear a key set
+ // its value to "". All owner data is cleared when the object is
+ // released.
+ SetOwnerData(map[string]string) error
+}
diff --git a/automation/vendor/github.com/juju/gomaasapi/jsonobject.go b/automation/vendor/github.com/juju/gomaasapi/jsonobject.go
new file mode 100644
index 0000000..cdd3dc1
--- /dev/null
+++ b/automation/vendor/github.com/juju/gomaasapi/jsonobject.go
@@ -0,0 +1,215 @@
+// Copyright 2012-2016 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+package gomaasapi
+
+import (
+ "encoding/json"
+ "errors"
+ "fmt"
+)
+
+// JSONObject is a wrapper around a JSON structure which provides
+// methods to extract data from that structure.
+// A JSONObject provides a simple structure consisting of the data types
+// defined in JSON: string, number, object, list, and bool. To get the
+// value you want out of a JSONObject, you must know (or figure out) which
+// kind of value you have, and then call the appropriate Get*() method to
+// get at it. Reading an item as the wrong type will return an error.
+// For instance, if your JSONObject consists of a number, call GetFloat64()
+// to get the value as a float64. If it's a list, call GetArray() to get
+// a slice of JSONObjects. To read any given item from the slice, you'll
+// need to "Get" that as the right type as well.
+// There is one exception: a MAASObject is really a special kind of map,
+// so you can read it as either.
+// Reading a null item is also an error. So before you try obj.Get*(),
+// first check obj.IsNil().
+type JSONObject struct {
+ // Parsed value. May actually be any of the types a JSONObject can
+ // wrap, except raw bytes. If the object can only be interpreted
+ // as raw bytes, this will be nil.
+ value interface{}
+ // Raw bytes, if this object was parsed directly from an API response.
+ // Is nil for sub-objects found within other objects. An object that
+ // was parsed directly from a response can be both raw bytes and some
+ // other value at the same time.
+ // For example, "[]" looks like a JSON list, so you can read it as an
+ // array. But it may also be the raw contents of a file that just
+ // happens to look like JSON, and so you can read it as raw bytes as
+ // well.
+ bytes []byte
+ // Client for further communication with the API.
+ client Client
+ // Is this a JSON null?
+ isNull bool
+}
+
+// Our JSON processor distinguishes a MAASObject from a jsonMap by the fact
+// that it contains a key "resource_uri". (A regular map might contain the
+// same key through sheer coincide, but never mind: you can still treat it
+// as a jsonMap and never notice the difference.)
+const resourceURI = "resource_uri"
+
+// maasify turns a completely untyped json.Unmarshal result into a JSONObject
+// (with the appropriate implementation of course). This function is
+// recursive. Maps and arrays are deep-copied, with each individual value
+// being converted to a JSONObject type.
+func maasify(client Client, value interface{}) JSONObject {
+ if value == nil {
+ return JSONObject{isNull: true}
+ }
+ switch value.(type) {
+ case string, float64, bool:
+ return JSONObject{value: value}
+ case map[string]interface{}:
+ original := value.(map[string]interface{})
+ result := make(map[string]JSONObject, len(original))
+ for key, value := range original {
+ result[key] = maasify(client, value)
+ }
+ return JSONObject{value: result, client: client}
+ case []interface{}:
+ original := value.([]interface{})
+ result := make([]JSONObject, len(original))
+ for index, value := range original {
+ result[index] = maasify(client, value)
+ }
+ return JSONObject{value: result}
+ }
+ msg := fmt.Sprintf("Unknown JSON type, can't be converted to JSONObject: %v", value)
+ panic(msg)
+}
+
+// Parse a JSON blob into a JSONObject.
+func Parse(client Client, input []byte) (JSONObject, error) {
+ var obj JSONObject
+ if input == nil {
+ panic(errors.New("Parse() called with nil input"))
+ }
+ var parsed interface{}
+ err := json.Unmarshal(input, &parsed)
+ if err == nil {
+ obj = maasify(client, parsed)
+ obj.bytes = input
+ } else {
+ switch err.(type) {
+ case *json.InvalidUTF8Error:
+ case *json.SyntaxError:
+ // This isn't JSON. Treat it as raw binary data.
+ default:
+ return obj, err
+ }
+ obj = JSONObject{value: nil, client: client, bytes: input}
+ }
+ return obj, nil
+}
+
+// JSONObjectFromStruct takes a struct and converts it to a JSONObject
+func JSONObjectFromStruct(client Client, input interface{}) (JSONObject, error) {
+ j, err := json.MarshalIndent(input, "", " ")
+ if err != nil {
+ return JSONObject{}, err
+ }
+ return Parse(client, j)
+}
+
+// Return error value for failed type conversion.
+func failConversion(wantedType string, obj JSONObject) error {
+ msg := fmt.Sprintf("Requested %v, got %T.", wantedType, obj.value)
+ return errors.New(msg)
+}
+
+// MarshalJSON tells the standard json package how to serialize a JSONObject
+// back to JSON.
+func (obj JSONObject) MarshalJSON() ([]byte, error) {
+ if obj.IsNil() {
+ return json.Marshal(nil)
+ }
+ return json.MarshalIndent(obj.value, "", " ")
+}
+
+// With MarshalJSON, JSONObject implements json.Marshaler.
+var _ json.Marshaler = (*JSONObject)(nil)
+
+// IsNil tells you whether a JSONObject is a JSON "null."
+// There is one irregularity. If the original JSON blob was actually raw
+// data, not JSON, then its IsNil will return false because the object
+// contains the binary data as a non-nil value. But, if the original JSON
+// blob consisted of a null, then IsNil returns true even though you can
+// still retrieve binary data from it.
+func (obj JSONObject) IsNil() bool {
+ if obj.value != nil {
+ return false
+ }
+ if obj.bytes == nil {
+ return true
+ }
+ // This may be a JSON null. We can't expect every JSON null to look
+ // the same; there may be leading or trailing space.
+ return obj.isNull
+}
+
+// GetString retrieves the object's value as a string. If the value wasn't
+// a JSON string, that's an error.
+func (obj JSONObject) GetString() (value string, err error) {
+ value, ok := obj.value.(string)
+ if !ok {
+ err = failConversion("string", obj)
+ }
+ return
+}
+
+// GetFloat64 retrieves the object's value as a float64. If the value wasn't
+// a JSON number, that's an error.
+func (obj JSONObject) GetFloat64() (value float64, err error) {
+ value, ok := obj.value.(float64)
+ if !ok {
+ err = failConversion("float64", obj)
+ }
+ return
+}
+
+// GetMap retrieves the object's value as a map. If the value wasn't a JSON
+// object, that's an error.
+func (obj JSONObject) GetMap() (value map[string]JSONObject, err error) {
+ value, ok := obj.value.(map[string]JSONObject)
+ if !ok {
+ err = failConversion("map", obj)
+ }
+ return
+}
+
+// GetArray retrieves the object's value as an array. If the value wasn't a
+// JSON list, that's an error.
+func (obj JSONObject) GetArray() (value []JSONObject, err error) {
+ value, ok := obj.value.([]JSONObject)
+ if !ok {
+ err = failConversion("array", obj)
+ }
+ return
+}
+
+// GetBool retrieves the object's value as a bool. If the value wasn't a JSON
+// bool, that's an error.
+func (obj JSONObject) GetBool() (value bool, err error) {
+ value, ok := obj.value.(bool)
+ if !ok {
+ err = failConversion("bool", obj)
+ }
+ return
+}
+
+// GetBytes retrieves the object's value as raw bytes. A JSONObject that was
+// parsed from the original input (as opposed to one that's embedded in
+// another JSONObject) can contain both the raw bytes and the parsed JSON
+// value, but either can be the case without the other.
+// If this object wasn't parsed directly from the original input, that's an
+// error.
+// If the object was parsed from an original input that just said "null", then
+// IsNil will return true but the raw bytes are still available from GetBytes.
+func (obj JSONObject) GetBytes() ([]byte, error) {
+ if obj.bytes == nil {
+ return nil, failConversion("bytes", obj)
+ }
+ return obj.bytes, nil
+}
diff --git a/automation/vendor/github.com/juju/gomaasapi/link.go b/automation/vendor/github.com/juju/gomaasapi/link.go
new file mode 100644
index 0000000..9e930e1
--- /dev/null
+++ b/automation/vendor/github.com/juju/gomaasapi/link.go
@@ -0,0 +1,124 @@
+// Copyright 2016 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+package gomaasapi
+
+import (
+ "github.com/juju/errors"
+ "github.com/juju/schema"
+ "github.com/juju/version"
+)
+
+type link struct {
+ id int
+ mode string
+ subnet *subnet
+ ipAddress string
+}
+
+// NOTE: not using lowercase L as the receiver as it is a horrible idea.
+// Instead using 'k'.
+
+// ID implements Link.
+func (k *link) ID() int {
+ return k.id
+}
+
+// Mode implements Link.
+func (k *link) Mode() string {
+ return k.mode
+}
+
+// Subnet implements Link.
+func (k *link) Subnet() Subnet {
+ if k.subnet == nil {
+ return nil
+ }
+ return k.subnet
+}
+
+// IPAddress implements Link.
+func (k *link) IPAddress() string {
+ return k.ipAddress
+}
+
+func readLinks(controllerVersion version.Number, source interface{}) ([]*link, error) {
+ checker := schema.List(schema.StringMap(schema.Any()))
+ coerced, err := checker.Coerce(source, nil)
+ if err != nil {
+ return nil, WrapWithDeserializationError(err, "link base schema check failed")
+ }
+ valid := coerced.([]interface{})
+
+ var deserialisationVersion version.Number
+ for v := range linkDeserializationFuncs {
+ if v.Compare(deserialisationVersion) > 0 && v.Compare(controllerVersion) <= 0 {
+ deserialisationVersion = v
+ }
+ }
+ if deserialisationVersion == version.Zero {
+ return nil, NewUnsupportedVersionError("no link read func for version %s", controllerVersion)
+ }
+ readFunc := linkDeserializationFuncs[deserialisationVersion]
+ return readLinkList(valid, readFunc)
+}
+
+// readLinkList expects the values of the sourceList to be string maps.
+func readLinkList(sourceList []interface{}, readFunc linkDeserializationFunc) ([]*link, error) {
+ result := make([]*link, 0, len(sourceList))
+ for i, value := range sourceList {
+ source, ok := value.(map[string]interface{})
+ if !ok {
+ return nil, NewDeserializationError("unexpected value for link %d, %T", i, value)
+ }
+ link, err := readFunc(source)
+ if err != nil {
+ return nil, errors.Annotatef(err, "link %d", i)
+ }
+ result = append(result, link)
+ }
+ return result, nil
+}
+
+type linkDeserializationFunc func(map[string]interface{}) (*link, error)
+
+var linkDeserializationFuncs = map[version.Number]linkDeserializationFunc{
+ twoDotOh: link_2_0,
+}
+
+func link_2_0(source map[string]interface{}) (*link, error) {
+ fields := schema.Fields{
+ "id": schema.ForceInt(),
+ "mode": schema.String(),
+ "subnet": schema.StringMap(schema.Any()),
+ "ip_address": schema.String(),
+ }
+ defaults := schema.Defaults{
+ "ip_address": "",
+ "subnet": schema.Omit,
+ }
+ checker := schema.FieldMap(fields, defaults)
+ coerced, err := checker.Coerce(source, nil)
+ if err != nil {
+ return nil, WrapWithDeserializationError(err, "link 2.0 schema check failed")
+ }
+ valid := coerced.(map[string]interface{})
+ // From here we know that the map returned from the schema coercion
+ // contains fields of the right type.
+
+ var subnet *subnet
+ if value, ok := valid["subnet"]; ok {
+ subnet, err = subnet_2_0(value.(map[string]interface{}))
+ if err != nil {
+ return nil, errors.Trace(err)
+ }
+ }
+
+ result := &link{
+ id: valid["id"].(int),
+ mode: valid["mode"].(string),
+ subnet: subnet,
+ ipAddress: valid["ip_address"].(string),
+ }
+ return result, nil
+}
diff --git a/automation/vendor/github.com/juju/gomaasapi/maas.go b/automation/vendor/github.com/juju/gomaasapi/maas.go
new file mode 100644
index 0000000..cd6ce29
--- /dev/null
+++ b/automation/vendor/github.com/juju/gomaasapi/maas.go
@@ -0,0 +1,11 @@
+// Copyright 2012-2016 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+package gomaasapi
+
+// NewMAAS returns an interface to the MAAS API as a *MAASObject.
+func NewMAAS(client Client) *MAASObject {
+ attrs := map[string]interface{}{resourceURI: client.APIURL.String()}
+ obj := newJSONMAASObject(attrs, client)
+ return &obj
+}
diff --git a/automation/vendor/github.com/juju/gomaasapi/maasobject.go b/automation/vendor/github.com/juju/gomaasapi/maasobject.go
new file mode 100644
index 0000000..3978252
--- /dev/null
+++ b/automation/vendor/github.com/juju/gomaasapi/maasobject.go
@@ -0,0 +1,197 @@
+// Copyright 2012-2016 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+package gomaasapi
+
+import (
+ "encoding/json"
+ "errors"
+ "fmt"
+ "net/url"
+)
+
+// MAASObject represents a MAAS object as returned by the MAAS API, such as a
+// Node or a Tag.
+// You can extract a MAASObject out of a JSONObject using
+// JSONObject.GetMAASObject. A MAAS API call will usually return either a
+// MAASObject or a list of MAASObjects. The list itself would be wrapped in
+// a JSONObject, so if an API call returns a list of objects "l," you first
+// obtain the array using l.GetArray(). Then, for each item "i" in the array,
+// obtain the matching MAASObject using i.GetMAASObject().
+type MAASObject struct {
+ values map[string]JSONObject
+ client Client
+ uri *url.URL
+}
+
+// newJSONMAASObject creates a new MAAS object. It will panic if the given map
+// does not contain a valid URL for the 'resource_uri' key.
+func newJSONMAASObject(jmap map[string]interface{}, client Client) MAASObject {
+ obj, err := maasify(client, jmap).GetMAASObject()
+ if err != nil {
+ panic(err)
+ }
+ return obj
+}
+
+// MarshalJSON tells the standard json package how to serialize a MAASObject.
+func (obj MAASObject) MarshalJSON() ([]byte, error) {
+ return json.MarshalIndent(obj.GetMap(), "", " ")
+}
+
+// With MarshalJSON, MAASObject implements json.Marshaler.
+var _ json.Marshaler = (*MAASObject)(nil)
+
+func marshalNode(node MAASObject) string {
+ res, _ := json.MarshalIndent(node, "", " ")
+ return string(res)
+
+}
+
+var noResourceURI = errors.New("not a MAAS object: no 'resource_uri' key")
+
+// extractURI obtains the "resource_uri" string from a JSONObject map.
+func extractURI(attrs map[string]JSONObject) (*url.URL, error) {
+ uriEntry, ok := attrs[resourceURI]
+ if !ok {
+ return nil, noResourceURI
+ }
+ uri, err := uriEntry.GetString()
+ if err != nil {
+ return nil, fmt.Errorf("invalid resource_uri: %v", uri)
+ }
+ resourceURL, err := url.Parse(uri)
+ if err != nil {
+ return nil, fmt.Errorf("resource_uri does not contain a valid URL: %v", uri)
+ }
+ return resourceURL, nil
+}
+
+// JSONObject getter for a MAAS object. From a decoding perspective, a
+// MAASObject is just like a map except it contains a key "resource_uri", and
+// it keeps track of the Client you got it from so that you can invoke API
+// methods directly on their MAAS objects.
+func (obj JSONObject) GetMAASObject() (MAASObject, error) {
+ attrs, err := obj.GetMap()
+ if err != nil {
+ return MAASObject{}, err
+ }
+ uri, err := extractURI(attrs)
+ if err != nil {
+ return MAASObject{}, err
+ }
+ return MAASObject{values: attrs, client: obj.client, uri: uri}, nil
+}
+
+// GetField extracts a string field from this MAAS object.
+func (obj MAASObject) GetField(name string) (string, error) {
+ return obj.values[name].GetString()
+}
+
+// URI is the resource URI for this MAAS object. It is an absolute path, but
+// without a network part.
+func (obj MAASObject) URI() *url.URL {
+ // Duplicate the URL.
+ uri, err := url.Parse(obj.uri.String())
+ if err != nil {
+ panic(err)
+ }
+ return uri
+}
+
+// URL returns a full absolute URL (including network part) for this MAAS
+// object on the API.
+func (obj MAASObject) URL() *url.URL {
+ return obj.client.GetURL(obj.URI())
+}
+
+// GetMap returns all of the object's attributes in the form of a map.
+func (obj MAASObject) GetMap() map[string]JSONObject {
+ return obj.values
+}
+
+// GetSubObject returns a new MAASObject representing the API resource found
+// at a given sub-path of the current object's resource URI.
+func (obj MAASObject) GetSubObject(name string) MAASObject {
+ uri := obj.URI()
+ newURL := url.URL{Path: name}
+ resUrl := uri.ResolveReference(&newURL)
+ resUrl.Path = EnsureTrailingSlash(resUrl.Path)
+ input := map[string]interface{}{resourceURI: resUrl.String()}
+ return newJSONMAASObject(input, obj.client)
+}
+
+var NotImplemented = errors.New("Not implemented")
+
+// Get retrieves a fresh copy of this MAAS object from the API.
+func (obj MAASObject) Get() (MAASObject, error) {
+ uri := obj.URI()
+ result, err := obj.client.Get(uri, "", url.Values{})
+ if err != nil {
+ return MAASObject{}, err
+ }
+ jsonObj, err := Parse(obj.client, result)
+ if err != nil {
+ return MAASObject{}, err
+ }
+ return jsonObj.GetMAASObject()
+}
+
+// Post overwrites this object's existing value on the API with those given
+// in "params." It returns the object's new value as received from the API.
+func (obj MAASObject) Post(params url.Values) (JSONObject, error) {
+ uri := obj.URI()
+ result, err := obj.client.Post(uri, "", params, nil)
+ if err != nil {
+ return JSONObject{}, err
+ }
+ return Parse(obj.client, result)
+}
+
+// Update modifies this object on the API, based on the values given in
+// "params." It returns the object's new value as received from the API.
+func (obj MAASObject) Update(params url.Values) (MAASObject, error) {
+ uri := obj.URI()
+ result, err := obj.client.Put(uri, params)
+ if err != nil {
+ return MAASObject{}, err
+ }
+ jsonObj, err := Parse(obj.client, result)
+ if err != nil {
+ return MAASObject{}, err
+ }
+ return jsonObj.GetMAASObject()
+}
+
+// Delete removes this object on the API.
+func (obj MAASObject) Delete() error {
+ uri := obj.URI()
+ return obj.client.Delete(uri)
+}
+
+// CallGet invokes an idempotent API method on this object.
+func (obj MAASObject) CallGet(operation string, params url.Values) (JSONObject, error) {
+ uri := obj.URI()
+ result, err := obj.client.Get(uri, operation, params)
+ if err != nil {
+ return JSONObject{}, err
+ }
+ return Parse(obj.client, result)
+}
+
+// CallPost invokes a non-idempotent API method on this object.
+func (obj MAASObject) CallPost(operation string, params url.Values) (JSONObject, error) {
+ return obj.CallPostFiles(operation, params, nil)
+}
+
+// CallPostFiles invokes a non-idempotent API method on this object. It is
+// similar to CallPost but has an extra parameter, 'files', which should
+// contain the files that will be uploaded to the API.
+func (obj MAASObject) CallPostFiles(operation string, params url.Values, files map[string][]byte) (JSONObject, error) {
+ uri := obj.URI()
+ result, err := obj.client.Post(uri, operation, params, files)
+ if err != nil {
+ return JSONObject{}, err
+ }
+ return Parse(obj.client, result)
+}
diff --git a/automation/vendor/github.com/juju/gomaasapi/machine.go b/automation/vendor/github.com/juju/gomaasapi/machine.go
new file mode 100644
index 0000000..8518d94
--- /dev/null
+++ b/automation/vendor/github.com/juju/gomaasapi/machine.go
@@ -0,0 +1,584 @@
+// Copyright 2016 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+package gomaasapi
+
+import (
+ "fmt"
+ "net/http"
+ "net/url"
+
+ "github.com/juju/errors"
+ "github.com/juju/schema"
+ "github.com/juju/version"
+)
+
+type machine struct {
+ controller *controller
+
+ resourceURI string
+
+ systemID string
+ hostname string
+ fqdn string
+ tags []string
+ ownerData map[string]string
+
+ operatingSystem string
+ distroSeries string
+ architecture string
+ memory int
+ cpuCount int
+
+ ipAddresses []string
+ powerState string
+
+ // NOTE: consider some form of status struct
+ statusName string
+ statusMessage string
+
+ bootInterface *interface_
+ interfaceSet []*interface_
+ zone *zone
+ // Don't really know the difference between these two lists:
+ physicalBlockDevices []*blockdevice
+ blockDevices []*blockdevice
+}
+
+func (m *machine) updateFrom(other *machine) {
+ m.resourceURI = other.resourceURI
+ m.systemID = other.systemID
+ m.hostname = other.hostname
+ m.fqdn = other.fqdn
+ m.operatingSystem = other.operatingSystem
+ m.distroSeries = other.distroSeries
+ m.architecture = other.architecture
+ m.memory = other.memory
+ m.cpuCount = other.cpuCount
+ m.ipAddresses = other.ipAddresses
+ m.powerState = other.powerState
+ m.statusName = other.statusName
+ m.statusMessage = other.statusMessage
+ m.zone = other.zone
+ m.tags = other.tags
+ m.ownerData = other.ownerData
+}
+
+// SystemID implements Machine.
+func (m *machine) SystemID() string {
+ return m.systemID
+}
+
+// Hostname implements Machine.
+func (m *machine) Hostname() string {
+ return m.hostname
+}
+
+// FQDN implements Machine.
+func (m *machine) FQDN() string {
+ return m.fqdn
+}
+
+// Tags implements Machine.
+func (m *machine) Tags() []string {
+ return m.tags
+}
+
+// IPAddresses implements Machine.
+func (m *machine) IPAddresses() []string {
+ return m.ipAddresses
+}
+
+// Memory implements Machine.
+func (m *machine) Memory() int {
+ return m.memory
+}
+
+// CPUCount implements Machine.
+func (m *machine) CPUCount() int {
+ return m.cpuCount
+}
+
+// PowerState implements Machine.
+func (m *machine) PowerState() string {
+ return m.powerState
+}
+
+// Zone implements Machine.
+func (m *machine) Zone() Zone {
+ if m.zone == nil {
+ return nil
+ }
+ return m.zone
+}
+
+// BootInterface implements Machine.
+func (m *machine) BootInterface() Interface {
+ if m.bootInterface == nil {
+ return nil
+ }
+ m.bootInterface.controller = m.controller
+ return m.bootInterface
+}
+
+// InterfaceSet implements Machine.
+func (m *machine) InterfaceSet() []Interface {
+ result := make([]Interface, len(m.interfaceSet))
+ for i, v := range m.interfaceSet {
+ v.controller = m.controller
+ result[i] = v
+ }
+ return result
+}
+
+// Interface implements Machine.
+func (m *machine) Interface(id int) Interface {
+ for _, iface := range m.interfaceSet {
+ if iface.ID() == id {
+ iface.controller = m.controller
+ return iface
+ }
+ }
+ return nil
+}
+
+// OperatingSystem implements Machine.
+func (m *machine) OperatingSystem() string {
+ return m.operatingSystem
+}
+
+// DistroSeries implements Machine.
+func (m *machine) DistroSeries() string {
+ return m.distroSeries
+}
+
+// Architecture implements Machine.
+func (m *machine) Architecture() string {
+ return m.architecture
+}
+
+// StatusName implements Machine.
+func (m *machine) StatusName() string {
+ return m.statusName
+}
+
+// StatusMessage implements Machine.
+func (m *machine) StatusMessage() string {
+ return m.statusMessage
+}
+
+// PhysicalBlockDevices implements Machine.
+func (m *machine) PhysicalBlockDevices() []BlockDevice {
+ result := make([]BlockDevice, len(m.physicalBlockDevices))
+ for i, v := range m.physicalBlockDevices {
+ result[i] = v
+ }
+ return result
+}
+
+// PhysicalBlockDevice implements Machine.
+func (m *machine) PhysicalBlockDevice(id int) BlockDevice {
+ for _, blockDevice := range m.physicalBlockDevices {
+ if blockDevice.ID() == id {
+ return blockDevice
+ }
+ }
+ return nil
+}
+
+// BlockDevices implements Machine.
+func (m *machine) BlockDevices() []BlockDevice {
+ result := make([]BlockDevice, len(m.blockDevices))
+ for i, v := range m.blockDevices {
+ result[i] = v
+ }
+ return result
+}
+
+// Devices implements Machine.
+func (m *machine) Devices(args DevicesArgs) ([]Device, error) {
+ // Perhaps in the future, MAAS will give us a way to query just for the
+ // devices for a particular parent.
+ devices, err := m.controller.Devices(args)
+ if err != nil {
+ return nil, errors.Trace(err)
+ }
+ var result []Device
+ for _, device := range devices {
+ if device.Parent() == m.SystemID() {
+ result = append(result, device)
+ }
+ }
+ return result, nil
+}
+
+// StartArgs is an argument struct for passing parameters to the Machine.Start
+// method.
+type StartArgs struct {
+ // UserData needs to be Base64 encoded user data for cloud-init.
+ UserData string
+ DistroSeries string
+ Kernel string
+ Comment string
+}
+
+// Start implements Machine.
+func (m *machine) Start(args StartArgs) error {
+ params := NewURLParams()
+ params.MaybeAdd("user_data", args.UserData)
+ params.MaybeAdd("distro_series", args.DistroSeries)
+ params.MaybeAdd("hwe_kernel", args.Kernel)
+ params.MaybeAdd("comment", args.Comment)
+ result, err := m.controller.post(m.resourceURI, "deploy", params.Values)
+ if err != nil {
+ if svrErr, ok := errors.Cause(err).(ServerError); ok {
+ switch svrErr.StatusCode {
+ case http.StatusNotFound, http.StatusConflict:
+ return errors.Wrap(err, NewBadRequestError(svrErr.BodyMessage))
+ case http.StatusForbidden:
+ return errors.Wrap(err, NewPermissionError(svrErr.BodyMessage))
+ case http.StatusServiceUnavailable:
+ return errors.Wrap(err, NewCannotCompleteError(svrErr.BodyMessage))
+ }
+ }
+ return NewUnexpectedError(err)
+ }
+
+ machine, err := readMachine(m.controller.apiVersion, result)
+ if err != nil {
+ return errors.Trace(err)
+ }
+ m.updateFrom(machine)
+ return nil
+}
+
+// CreateMachineDeviceArgs is an argument structure for Machine.CreateDevice.
+// Only InterfaceName and MACAddress fields are required, the others are only
+// used if set. If Subnet and VLAN are both set, Subnet.VLAN() must match the
+// given VLAN. On failure, returns an error satisfying errors.IsNotValid().
+type CreateMachineDeviceArgs struct {
+ Hostname string
+ InterfaceName string
+ MACAddress string
+ Subnet Subnet
+ VLAN VLAN
+}
+
+// Validate ensures that all required values are non-emtpy.
+func (a *CreateMachineDeviceArgs) Validate() error {
+ if a.InterfaceName == "" {
+ return errors.NotValidf("missing InterfaceName")
+ }
+
+ if a.MACAddress == "" {
+ return errors.NotValidf("missing MACAddress")
+ }
+
+ if a.Subnet != nil && a.VLAN != nil && a.Subnet.VLAN() != a.VLAN {
+ msg := fmt.Sprintf(
+ "given subnet %q on VLAN %d does not match given VLAN %d",
+ a.Subnet.CIDR(), a.Subnet.VLAN().ID(), a.VLAN.ID(),
+ )
+ return errors.NewNotValid(nil, msg)
+ }
+
+ return nil
+}
+
+// CreateDevice implements Machine
+func (m *machine) CreateDevice(args CreateMachineDeviceArgs) (_ Device, err error) {
+ if err := args.Validate(); err != nil {
+ return nil, errors.Trace(err)
+ }
+ device, err := m.controller.CreateDevice(CreateDeviceArgs{
+ Hostname: args.Hostname,
+ MACAddresses: []string{args.MACAddress},
+ Parent: m.SystemID(),
+ })
+ if err != nil {
+ return nil, errors.Trace(err)
+ }
+
+ defer func(err *error) {
+ // If there is an error return, at least try to delete the device we just created.
+ if *err != nil {
+ if innerErr := device.Delete(); innerErr != nil {
+ logger.Warningf("could not delete device %q", device.SystemID())
+ }
+ }
+ }(&err)
+
+ // Update the VLAN to use for the interface, if given.
+ vlanToUse := args.VLAN
+ if vlanToUse == nil && args.Subnet != nil {
+ vlanToUse = args.Subnet.VLAN()
+ }
+
+ // There should be one interface created for each MAC Address, and since we
+ // only specified one, there should just be one response.
+ interfaces := device.InterfaceSet()
+ if count := len(interfaces); count != 1 {
+ err := errors.Errorf("unexpected interface count for device: %d", count)
+ return nil, NewUnexpectedError(err)
+ }
+ iface := interfaces[0]
+ nameToUse := args.InterfaceName
+
+ if err := m.updateDeviceInterface(iface, nameToUse, vlanToUse); err != nil {
+ return nil, errors.Trace(err)
+ }
+
+ if args.Subnet == nil {
+ // Nothing further to update.
+ return device, nil
+ }
+
+ if err := m.linkDeviceInterfaceToSubnet(iface, args.Subnet); err != nil {
+ return nil, errors.Trace(err)
+ }
+
+ return device, nil
+}
+
+func (m *machine) updateDeviceInterface(iface Interface, nameToUse string, vlanToUse VLAN) error {
+ updateArgs := UpdateInterfaceArgs{}
+ updateArgs.Name = nameToUse
+
+ if vlanToUse != nil {
+ updateArgs.VLAN = vlanToUse
+ }
+
+ if err := iface.Update(updateArgs); err != nil {
+ return errors.Annotatef(err, "updating device interface %q failed", iface.Name())
+ }
+
+ return nil
+}
+
+func (m *machine) linkDeviceInterfaceToSubnet(iface Interface, subnetToUse Subnet) error {
+ err := iface.LinkSubnet(LinkSubnetArgs{
+ Mode: LinkModeStatic,
+ Subnet: subnetToUse,
+ })
+ if err != nil {
+ return errors.Annotatef(
+ err, "linking device interface %q to subnet %q failed",
+ iface.Name(), subnetToUse.CIDR())
+ }
+
+ return nil
+}
+
+// OwnerData implements OwnerDataHolder.
+func (m *machine) OwnerData() map[string]string {
+ result := make(map[string]string)
+ for key, value := range m.ownerData {
+ result[key] = value
+ }
+ return result
+}
+
+// SetOwnerData implements OwnerDataHolder.
+func (m *machine) SetOwnerData(ownerData map[string]string) error {
+ params := make(url.Values)
+ for key, value := range ownerData {
+ params.Add(key, value)
+ }
+ result, err := m.controller.post(m.resourceURI, "set_owner_data", params)
+ if err != nil {
+ return errors.Trace(err)
+ }
+ machine, err := readMachine(m.controller.apiVersion, result)
+ if err != nil {
+ return errors.Trace(err)
+ }
+ m.updateFrom(machine)
+ return nil
+}
+
+func readMachine(controllerVersion version.Number, source interface{}) (*machine, error) {
+ readFunc, err := getMachineDeserializationFunc(controllerVersion)
+ if err != nil {
+ return nil, errors.Trace(err)
+ }
+
+ checker := schema.StringMap(schema.Any())
+ coerced, err := checker.Coerce(source, nil)
+ if err != nil {
+ return nil, WrapWithDeserializationError(err, "machine base schema check failed")
+ }
+ valid := coerced.(map[string]interface{})
+ return readFunc(valid)
+}
+
+func readMachines(controllerVersion version.Number, source interface{}) ([]*machine, error) {
+ readFunc, err := getMachineDeserializationFunc(controllerVersion)
+ if err != nil {
+ return nil, errors.Trace(err)
+ }
+
+ checker := schema.List(schema.StringMap(schema.Any()))
+ coerced, err := checker.Coerce(source, nil)
+ if err != nil {
+ return nil, WrapWithDeserializationError(err, "machine base schema check failed")
+ }
+ valid := coerced.([]interface{})
+ return readMachineList(valid, readFunc)
+}
+
+func getMachineDeserializationFunc(controllerVersion version.Number) (machineDeserializationFunc, error) {
+ var deserialisationVersion version.Number
+ for v := range machineDeserializationFuncs {
+ if v.Compare(deserialisationVersion) > 0 && v.Compare(controllerVersion) <= 0 {
+ deserialisationVersion = v
+ }
+ }
+ if deserialisationVersion == version.Zero {
+ return nil, NewUnsupportedVersionError("no machine read func for version %s", controllerVersion)
+ }
+ return machineDeserializationFuncs[deserialisationVersion], nil
+}
+
+func readMachineList(sourceList []interface{}, readFunc machineDeserializationFunc) ([]*machine, error) {
+ result := make([]*machine, 0, len(sourceList))
+ for i, value := range sourceList {
+ source, ok := value.(map[string]interface{})
+ if !ok {
+ return nil, NewDeserializationError("unexpected value for machine %d, %T", i, value)
+ }
+ machine, err := readFunc(source)
+ if err != nil {
+ return nil, errors.Annotatef(err, "machine %d", i)
+ }
+ result = append(result, machine)
+ }
+ return result, nil
+}
+
+type machineDeserializationFunc func(map[string]interface{}) (*machine, error)
+
+var machineDeserializationFuncs = map[version.Number]machineDeserializationFunc{
+ twoDotOh: machine_2_0,
+}
+
+func machine_2_0(source map[string]interface{}) (*machine, error) {
+ fields := schema.Fields{
+ "resource_uri": schema.String(),
+
+ "system_id": schema.String(),
+ "hostname": schema.String(),
+ "fqdn": schema.String(),
+ "tag_names": schema.List(schema.String()),
+ "owner_data": schema.StringMap(schema.String()),
+
+ "osystem": schema.String(),
+ "distro_series": schema.String(),
+ "architecture": schema.OneOf(schema.Nil(""), schema.String()),
+ "memory": schema.ForceInt(),
+ "cpu_count": schema.ForceInt(),
+
+ "ip_addresses": schema.List(schema.String()),
+ "power_state": schema.String(),
+ "status_name": schema.String(),
+ "status_message": schema.OneOf(schema.Nil(""), schema.String()),
+
+ "boot_interface": schema.OneOf(schema.Nil(""), schema.StringMap(schema.Any())),
+ "interface_set": schema.List(schema.StringMap(schema.Any())),
+ "zone": schema.StringMap(schema.Any()),
+
+ "physicalblockdevice_set": schema.List(schema.StringMap(schema.Any())),
+ "blockdevice_set": schema.List(schema.StringMap(schema.Any())),
+ }
+ defaults := schema.Defaults{
+ "architecture": "",
+ }
+ checker := schema.FieldMap(fields, defaults)
+ coerced, err := checker.Coerce(source, nil)
+ if err != nil {
+ return nil, WrapWithDeserializationError(err, "machine 2.0 schema check failed")
+ }
+ valid := coerced.(map[string]interface{})
+ // From here we know that the map returned from the schema coercion
+ // contains fields of the right type.
+
+ var bootInterface *interface_
+ if ifaceMap, ok := valid["boot_interface"].(map[string]interface{}); ok {
+ bootInterface, err = interface_2_0(ifaceMap)
+ if err != nil {
+ return nil, errors.Trace(err)
+ }
+ }
+
+ interfaceSet, err := readInterfaceList(valid["interface_set"].([]interface{}), interface_2_0)
+ if err != nil {
+ return nil, errors.Trace(err)
+ }
+ zone, err := zone_2_0(valid["zone"].(map[string]interface{}))
+ if err != nil {
+ return nil, errors.Trace(err)
+ }
+ physicalBlockDevices, err := readBlockDeviceList(valid["physicalblockdevice_set"].([]interface{}), blockdevice_2_0)
+ if err != nil {
+ return nil, errors.Trace(err)
+ }
+ blockDevices, err := readBlockDeviceList(valid["blockdevice_set"].([]interface{}), blockdevice_2_0)
+ if err != nil {
+ return nil, errors.Trace(err)
+ }
+ architecture, _ := valid["architecture"].(string)
+ statusMessage, _ := valid["status_message"].(string)
+ result := &machine{
+ resourceURI: valid["resource_uri"].(string),
+
+ systemID: valid["system_id"].(string),
+ hostname: valid["hostname"].(string),
+ fqdn: valid["fqdn"].(string),
+ tags: convertToStringSlice(valid["tag_names"]),
+ ownerData: convertToStringMap(valid["owner_data"]),
+
+ operatingSystem: valid["osystem"].(string),
+ distroSeries: valid["distro_series"].(string),
+ architecture: architecture,
+ memory: valid["memory"].(int),
+ cpuCount: valid["cpu_count"].(int),
+
+ ipAddresses: convertToStringSlice(valid["ip_addresses"]),
+ powerState: valid["power_state"].(string),
+ statusName: valid["status_name"].(string),
+ statusMessage: statusMessage,
+
+ bootInterface: bootInterface,
+ interfaceSet: interfaceSet,
+ zone: zone,
+ physicalBlockDevices: physicalBlockDevices,
+ blockDevices: blockDevices,
+ }
+
+ return result, nil
+}
+
+func convertToStringSlice(field interface{}) []string {
+ if field == nil {
+ return nil
+ }
+ fieldSlice := field.([]interface{})
+ result := make([]string, len(fieldSlice))
+ for i, value := range fieldSlice {
+ result[i] = value.(string)
+ }
+ return result
+}
+
+func convertToStringMap(field interface{}) map[string]string {
+ if field == nil {
+ return nil
+ }
+ // This function is only called after a schema Coerce, so it's
+ // safe.
+ fieldMap := field.(map[string]interface{})
+ result := make(map[string]string)
+ for key, value := range fieldMap {
+ result[key] = value.(string)
+ }
+ return result
+}
diff --git a/automation/vendor/github.com/juju/gomaasapi/oauth.go b/automation/vendor/github.com/juju/gomaasapi/oauth.go
new file mode 100644
index 0000000..920960d
--- /dev/null
+++ b/automation/vendor/github.com/juju/gomaasapi/oauth.go
@@ -0,0 +1,80 @@
+// Copyright 2012-2016 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+package gomaasapi
+
+import (
+ "crypto/rand"
+ "fmt"
+ "net/http"
+ "net/url"
+ "strconv"
+ "strings"
+ "time"
+)
+
+// Not a true uuidgen, but at least creates same length random
+func generateNonce() (string, error) {
+ randBytes := make([]byte, 16)
+ _, err := rand.Read(randBytes)
+ if err != nil {
+ return "", err
+ }
+ return fmt.Sprintf("%16x", randBytes), nil
+}
+
+func generateTimestamp() string {
+ return strconv.Itoa(int(time.Now().Unix()))
+}
+
+type OAuthSigner interface {
+ OAuthSign(request *http.Request) error
+}
+
+type OAuthToken struct {
+ ConsumerKey string
+ ConsumerSecret string
+ TokenKey string
+ TokenSecret string
+}
+
+// Trick to ensure *plainTextOAuthSigner implements the OAuthSigner interface.
+var _ OAuthSigner = (*plainTextOAuthSigner)(nil)
+
+type plainTextOAuthSigner struct {
+ token *OAuthToken
+ realm string
+}
+
+func NewPlainTestOAuthSigner(token *OAuthToken, realm string) (OAuthSigner, error) {
+ return &plainTextOAuthSigner{token, realm}, nil
+}
+
+// OAuthSignPLAINTEXT signs the provided request using the OAuth PLAINTEXT
+// method: http://oauth.net/core/1.0/#anchor22.
+func (signer plainTextOAuthSigner) OAuthSign(request *http.Request) error {
+
+ signature := signer.token.ConsumerSecret + `&` + signer.token.TokenSecret
+ nonce, err := generateNonce()
+ if err != nil {
+ return err
+ }
+ authData := map[string]string{
+ "realm": signer.realm,
+ "oauth_consumer_key": signer.token.ConsumerKey,
+ "oauth_token": signer.token.TokenKey,
+ "oauth_signature_method": "PLAINTEXT",
+ "oauth_signature": signature,
+ "oauth_timestamp": generateTimestamp(),
+ "oauth_nonce": nonce,
+ "oauth_version": "1.0",
+ }
+ // Build OAuth header.
+ var authHeader []string
+ for key, value := range authData {
+ authHeader = append(authHeader, fmt.Sprintf(`%s="%s"`, key, url.QueryEscape(value)))
+ }
+ strHeader := "OAuth " + strings.Join(authHeader, ", ")
+ request.Header.Add("Authorization", strHeader)
+ return nil
+}
diff --git a/automation/vendor/github.com/juju/gomaasapi/partition.go b/automation/vendor/github.com/juju/gomaasapi/partition.go
new file mode 100644
index 0000000..f6d6afa
--- /dev/null
+++ b/automation/vendor/github.com/juju/gomaasapi/partition.go
@@ -0,0 +1,145 @@
+// Copyright 2016 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+package gomaasapi
+
+import (
+ "github.com/juju/errors"
+ "github.com/juju/schema"
+ "github.com/juju/version"
+)
+
+type partition struct {
+ resourceURI string
+
+ id int
+ path string
+ uuid string
+
+ usedFor string
+ size uint64
+
+ filesystem *filesystem
+}
+
+// ID implements Partition.
+func (p *partition) ID() int {
+ return p.id
+}
+
+// Path implements Partition.
+func (p *partition) Path() string {
+ return p.path
+}
+
+// FileSystem implements Partition.
+func (p *partition) FileSystem() FileSystem {
+ if p.filesystem == nil {
+ return nil
+ }
+ return p.filesystem
+}
+
+// UUID implements Partition.
+func (p *partition) UUID() string {
+ return p.uuid
+}
+
+// UsedFor implements Partition.
+func (p *partition) UsedFor() string {
+ return p.usedFor
+}
+
+// Size implements Partition.
+func (p *partition) Size() uint64 {
+ return p.size
+}
+
+func readPartitions(controllerVersion version.Number, source interface{}) ([]*partition, error) {
+ checker := schema.List(schema.StringMap(schema.Any()))
+ coerced, err := checker.Coerce(source, nil)
+ if err != nil {
+ return nil, WrapWithDeserializationError(err, "partition base schema check failed")
+ }
+ valid := coerced.([]interface{})
+
+ var deserialisationVersion version.Number
+ for v := range partitionDeserializationFuncs {
+ if v.Compare(deserialisationVersion) > 0 && v.Compare(controllerVersion) <= 0 {
+ deserialisationVersion = v
+ }
+ }
+ if deserialisationVersion == version.Zero {
+ return nil, NewUnsupportedVersionError("no partition read func for version %s", controllerVersion)
+ }
+ readFunc := partitionDeserializationFuncs[deserialisationVersion]
+ return readPartitionList(valid, readFunc)
+}
+
+// readPartitionList expects the values of the sourceList to be string maps.
+func readPartitionList(sourceList []interface{}, readFunc partitionDeserializationFunc) ([]*partition, error) {
+ result := make([]*partition, 0, len(sourceList))
+ for i, value := range sourceList {
+ source, ok := value.(map[string]interface{})
+ if !ok {
+ return nil, NewDeserializationError("unexpected value for partition %d, %T", i, value)
+ }
+ partition, err := readFunc(source)
+ if err != nil {
+ return nil, errors.Annotatef(err, "partition %d", i)
+ }
+ result = append(result, partition)
+ }
+ return result, nil
+}
+
+type partitionDeserializationFunc func(map[string]interface{}) (*partition, error)
+
+var partitionDeserializationFuncs = map[version.Number]partitionDeserializationFunc{
+ twoDotOh: partition_2_0,
+}
+
+func partition_2_0(source map[string]interface{}) (*partition, error) {
+ fields := schema.Fields{
+ "resource_uri": schema.String(),
+
+ "id": schema.ForceInt(),
+ "path": schema.String(),
+ "uuid": schema.OneOf(schema.Nil(""), schema.String()),
+
+ "used_for": schema.String(),
+ "size": schema.ForceUint(),
+
+ "filesystem": schema.OneOf(schema.Nil(""), schema.StringMap(schema.Any())),
+ }
+ defaults := schema.Defaults{
+ "uuid": "",
+ }
+ checker := schema.FieldMap(fields, defaults)
+ coerced, err := checker.Coerce(source, nil)
+ if err != nil {
+ return nil, WrapWithDeserializationError(err, "partition 2.0 schema check failed")
+ }
+ valid := coerced.(map[string]interface{})
+ // From here we know that the map returned from the schema coercion
+ // contains fields of the right type.
+
+ var filesystem *filesystem
+ if fsSource := valid["filesystem"]; fsSource != nil {
+ filesystem, err = filesystem2_0(fsSource.(map[string]interface{}))
+ if err != nil {
+ return nil, errors.Trace(err)
+ }
+ }
+ uuid, _ := valid["uuid"].(string)
+ result := &partition{
+ resourceURI: valid["resource_uri"].(string),
+ id: valid["id"].(int),
+ path: valid["path"].(string),
+ uuid: uuid,
+ usedFor: valid["used_for"].(string),
+ size: valid["size"].(uint64),
+ filesystem: filesystem,
+ }
+ return result, nil
+}
diff --git a/automation/vendor/github.com/juju/gomaasapi/space.go b/automation/vendor/github.com/juju/gomaasapi/space.go
new file mode 100644
index 0000000..5b8b8cf
--- /dev/null
+++ b/automation/vendor/github.com/juju/gomaasapi/space.go
@@ -0,0 +1,115 @@
+// Copyright 2016 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+package gomaasapi
+
+import (
+ "github.com/juju/errors"
+ "github.com/juju/schema"
+ "github.com/juju/version"
+)
+
+type space struct {
+ // Add the controller in when we need to do things with the space.
+ // controller Controller
+
+ resourceURI string
+
+ id int
+ name string
+
+ subnets []*subnet
+}
+
+// Id implements Space.
+func (s *space) ID() int {
+ return s.id
+}
+
+// Name implements Space.
+func (s *space) Name() string {
+ return s.name
+}
+
+// Subnets implements Space.
+func (s *space) Subnets() []Subnet {
+ var result []Subnet
+ for _, subnet := range s.subnets {
+ result = append(result, subnet)
+ }
+ return result
+}
+
+func readSpaces(controllerVersion version.Number, source interface{}) ([]*space, error) {
+ checker := schema.List(schema.StringMap(schema.Any()))
+ coerced, err := checker.Coerce(source, nil)
+ if err != nil {
+ return nil, errors.Annotatef(err, "space base schema check failed")
+ }
+ valid := coerced.([]interface{})
+
+ var deserialisationVersion version.Number
+ for v := range spaceDeserializationFuncs {
+ if v.Compare(deserialisationVersion) > 0 && v.Compare(controllerVersion) <= 0 {
+ deserialisationVersion = v
+ }
+ }
+ if deserialisationVersion == version.Zero {
+ return nil, errors.Errorf("no space read func for version %s", controllerVersion)
+ }
+ readFunc := spaceDeserializationFuncs[deserialisationVersion]
+ return readSpaceList(valid, readFunc)
+}
+
+// readSpaceList expects the values of the sourceList to be string maps.
+func readSpaceList(sourceList []interface{}, readFunc spaceDeserializationFunc) ([]*space, error) {
+ result := make([]*space, 0, len(sourceList))
+ for i, value := range sourceList {
+ source, ok := value.(map[string]interface{})
+ if !ok {
+ return nil, errors.Errorf("unexpected value for space %d, %T", i, value)
+ }
+ space, err := readFunc(source)
+ if err != nil {
+ return nil, errors.Annotatef(err, "space %d", i)
+ }
+ result = append(result, space)
+ }
+ return result, nil
+}
+
+type spaceDeserializationFunc func(map[string]interface{}) (*space, error)
+
+var spaceDeserializationFuncs = map[version.Number]spaceDeserializationFunc{
+ twoDotOh: space_2_0,
+}
+
+func space_2_0(source map[string]interface{}) (*space, error) {
+ fields := schema.Fields{
+ "resource_uri": schema.String(),
+ "id": schema.ForceInt(),
+ "name": schema.String(),
+ "subnets": schema.List(schema.StringMap(schema.Any())),
+ }
+ checker := schema.FieldMap(fields, nil) // no defaults
+ coerced, err := checker.Coerce(source, nil)
+ if err != nil {
+ return nil, errors.Annotatef(err, "space 2.0 schema check failed")
+ }
+ valid := coerced.(map[string]interface{})
+ // From here we know that the map returned from the schema coercion
+ // contains fields of the right type.
+
+ subnets, err := readSubnetList(valid["subnets"].([]interface{}), subnet_2_0)
+ if err != nil {
+ return nil, errors.Trace(err)
+ }
+
+ result := &space{
+ resourceURI: valid["resource_uri"].(string),
+ id: valid["id"].(int),
+ name: valid["name"].(string),
+ subnets: subnets,
+ }
+ return result, nil
+}
diff --git a/automation/vendor/github.com/juju/gomaasapi/subnet.go b/automation/vendor/github.com/juju/gomaasapi/subnet.go
new file mode 100644
index 0000000..f509ccd
--- /dev/null
+++ b/automation/vendor/github.com/juju/gomaasapi/subnet.go
@@ -0,0 +1,152 @@
+// Copyright 2016 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+package gomaasapi
+
+import (
+ "github.com/juju/errors"
+ "github.com/juju/schema"
+ "github.com/juju/version"
+)
+
+type subnet struct {
+ // Add the controller in when we need to do things with the subnet.
+ // controller Controller
+
+ resourceURI string
+
+ id int
+ name string
+ space string
+ vlan *vlan
+
+ gateway string
+ cidr string
+
+ dnsServers []string
+}
+
+// ID implements Subnet.
+func (s *subnet) ID() int {
+ return s.id
+}
+
+// Name implements Subnet.
+func (s *subnet) Name() string {
+ return s.name
+}
+
+// Space implements Subnet.
+func (s *subnet) Space() string {
+ return s.space
+}
+
+// VLAN implements Subnet.
+func (s *subnet) VLAN() VLAN {
+ if s.vlan == nil {
+ return nil
+ }
+ return s.vlan
+}
+
+// Gateway implements Subnet.
+func (s *subnet) Gateway() string {
+ return s.gateway
+}
+
+// CIDR implements Subnet.
+func (s *subnet) CIDR() string {
+ return s.cidr
+}
+
+// DNSServers implements Subnet.
+func (s *subnet) DNSServers() []string {
+ return s.dnsServers
+}
+
+func readSubnets(controllerVersion version.Number, source interface{}) ([]*subnet, error) {
+ checker := schema.List(schema.StringMap(schema.Any()))
+ coerced, err := checker.Coerce(source, nil)
+ if err != nil {
+ return nil, errors.Annotatef(err, "subnet base schema check failed")
+ }
+ valid := coerced.([]interface{})
+
+ var deserialisationVersion version.Number
+ for v := range subnetDeserializationFuncs {
+ if v.Compare(deserialisationVersion) > 0 && v.Compare(controllerVersion) <= 0 {
+ deserialisationVersion = v
+ }
+ }
+ if deserialisationVersion == version.Zero {
+ return nil, errors.Errorf("no subnet read func for version %s", controllerVersion)
+ }
+ readFunc := subnetDeserializationFuncs[deserialisationVersion]
+ return readSubnetList(valid, readFunc)
+}
+
+// readSubnetList expects the values of the sourceList to be string maps.
+func readSubnetList(sourceList []interface{}, readFunc subnetDeserializationFunc) ([]*subnet, error) {
+ result := make([]*subnet, 0, len(sourceList))
+ for i, value := range sourceList {
+ source, ok := value.(map[string]interface{})
+ if !ok {
+ return nil, errors.Errorf("unexpected value for subnet %d, %T", i, value)
+ }
+ subnet, err := readFunc(source)
+ if err != nil {
+ return nil, errors.Annotatef(err, "subnet %d", i)
+ }
+ result = append(result, subnet)
+ }
+ return result, nil
+}
+
+type subnetDeserializationFunc func(map[string]interface{}) (*subnet, error)
+
+var subnetDeserializationFuncs = map[version.Number]subnetDeserializationFunc{
+ twoDotOh: subnet_2_0,
+}
+
+func subnet_2_0(source map[string]interface{}) (*subnet, error) {
+ fields := schema.Fields{
+ "resource_uri": schema.String(),
+ "id": schema.ForceInt(),
+ "name": schema.String(),
+ "space": schema.String(),
+ "gateway_ip": schema.OneOf(schema.Nil(""), schema.String()),
+ "cidr": schema.String(),
+ "vlan": schema.StringMap(schema.Any()),
+ "dns_servers": schema.OneOf(schema.Nil(""), schema.List(schema.String())),
+ }
+ checker := schema.FieldMap(fields, nil) // no defaults
+ coerced, err := checker.Coerce(source, nil)
+ if err != nil {
+ return nil, errors.Annotatef(err, "subnet 2.0 schema check failed")
+ }
+ valid := coerced.(map[string]interface{})
+ // From here we know that the map returned from the schema coercion
+ // contains fields of the right type.
+
+ vlan, err := vlan_2_0(valid["vlan"].(map[string]interface{}))
+ if err != nil {
+ return nil, errors.Trace(err)
+ }
+
+ // Since the gateway_ip is optional, we use the two part cast assignment. If
+ // the cast fails, then we get the default value we care about, which is the
+ // empty string.
+ gateway, _ := valid["gateway_ip"].(string)
+
+ result := &subnet{
+ resourceURI: valid["resource_uri"].(string),
+ id: valid["id"].(int),
+ name: valid["name"].(string),
+ space: valid["space"].(string),
+ vlan: vlan,
+ gateway: gateway,
+ cidr: valid["cidr"].(string),
+ dnsServers: convertToStringSlice(valid["dns_servers"]),
+ }
+ return result, nil
+}
diff --git a/automation/vendor/github.com/juju/gomaasapi/testing.go b/automation/vendor/github.com/juju/gomaasapi/testing.go
new file mode 100644
index 0000000..54d67aa
--- /dev/null
+++ b/automation/vendor/github.com/juju/gomaasapi/testing.go
@@ -0,0 +1,222 @@
+// Copyright 2012-2016 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+package gomaasapi
+
+import (
+ "fmt"
+ "net/http"
+ "net/http/httptest"
+ "strings"
+)
+
+type singleServingServer struct {
+ *httptest.Server
+ requestContent *string
+ requestHeader *http.Header
+}
+
+// newSingleServingServer creates a single-serving test http server which will
+// return only one response as defined by the passed arguments.
+func newSingleServingServer(uri string, response string, code int) *singleServingServer {
+ var requestContent string
+ var requestHeader http.Header
+ var requested bool
+ handler := func(writer http.ResponseWriter, request *http.Request) {
+ if requested {
+ http.Error(writer, "Already requested", http.StatusServiceUnavailable)
+ }
+ res, err := readAndClose(request.Body)
+ if err != nil {
+ panic(err)
+ }
+ requestContent = string(res)
+ requestHeader = request.Header
+ if request.URL.String() != uri {
+ errorMsg := fmt.Sprintf("Error 404: page not found (expected '%v', got '%v').", uri, request.URL.String())
+ http.Error(writer, errorMsg, http.StatusNotFound)
+ } else {
+ writer.WriteHeader(code)
+ fmt.Fprint(writer, response)
+ }
+ requested = true
+ }
+ server := httptest.NewServer(http.HandlerFunc(handler))
+ return &singleServingServer{server, &requestContent, &requestHeader}
+}
+
+type flakyServer struct {
+ *httptest.Server
+ nbRequests *int
+ requests *[][]byte
+}
+
+// newFlakyServer creates a "flaky" test http server which will
+// return `nbFlakyResponses` responses with the given code and then a 200 response.
+func newFlakyServer(uri string, code int, nbFlakyResponses int) *flakyServer {
+ nbRequests := 0
+ requests := make([][]byte, nbFlakyResponses+1)
+ handler := func(writer http.ResponseWriter, request *http.Request) {
+ nbRequests += 1
+ body, err := readAndClose(request.Body)
+ if err != nil {
+ panic(err)
+ }
+ requests[nbRequests-1] = body
+ if request.URL.String() != uri {
+ errorMsg := fmt.Sprintf("Error 404: page not found (expected '%v', got '%v').", uri, request.URL.String())
+ http.Error(writer, errorMsg, http.StatusNotFound)
+ } else if nbRequests <= nbFlakyResponses {
+ if code == http.StatusServiceUnavailable {
+ writer.Header().Set("Retry-After", "0")
+ }
+ writer.WriteHeader(code)
+ fmt.Fprint(writer, "flaky")
+ } else {
+ writer.WriteHeader(http.StatusOK)
+ fmt.Fprint(writer, "ok")
+ }
+
+ }
+ server := httptest.NewServer(http.HandlerFunc(handler))
+ return &flakyServer{server, &nbRequests, &requests}
+}
+
+type simpleResponse struct {
+ status int
+ body string
+}
+
+type SimpleTestServer struct {
+ *httptest.Server
+
+ getResponses map[string][]simpleResponse
+ getResponseIndex map[string]int
+ putResponses map[string][]simpleResponse
+ putResponseIndex map[string]int
+ postResponses map[string][]simpleResponse
+ postResponseIndex map[string]int
+ deleteResponses map[string][]simpleResponse
+ deleteResponseIndex map[string]int
+
+ requests []*http.Request
+}
+
+func NewSimpleServer() *SimpleTestServer {
+ server := &SimpleTestServer{
+ getResponses: make(map[string][]simpleResponse),
+ getResponseIndex: make(map[string]int),
+ putResponses: make(map[string][]simpleResponse),
+ putResponseIndex: make(map[string]int),
+ postResponses: make(map[string][]simpleResponse),
+ postResponseIndex: make(map[string]int),
+ deleteResponses: make(map[string][]simpleResponse),
+ deleteResponseIndex: make(map[string]int),
+ }
+ server.Server = httptest.NewUnstartedServer(http.HandlerFunc(server.handler))
+ return server
+}
+
+func (s *SimpleTestServer) AddGetResponse(path string, status int, body string) {
+ logger.Debugf("add get response for: %s, %d", path, status)
+ s.getResponses[path] = append(s.getResponses[path], simpleResponse{status: status, body: body})
+}
+
+func (s *SimpleTestServer) AddPutResponse(path string, status int, body string) {
+ logger.Debugf("add put response for: %s, %d", path, status)
+ s.putResponses[path] = append(s.putResponses[path], simpleResponse{status: status, body: body})
+}
+
+func (s *SimpleTestServer) AddPostResponse(path string, status int, body string) {
+ logger.Debugf("add post response for: %s, %d", path, status)
+ s.postResponses[path] = append(s.postResponses[path], simpleResponse{status: status, body: body})
+}
+
+func (s *SimpleTestServer) AddDeleteResponse(path string, status int, body string) {
+ logger.Debugf("add delete response for: %s, %d", path, status)
+ s.deleteResponses[path] = append(s.deleteResponses[path], simpleResponse{status: status, body: body})
+}
+
+func (s *SimpleTestServer) LastRequest() *http.Request {
+ pos := len(s.requests) - 1
+ if pos < 0 {
+ return nil
+ }
+ return s.requests[pos]
+}
+
+func (s *SimpleTestServer) LastNRequests(n int) []*http.Request {
+ start := len(s.requests) - n
+ if start < 0 {
+ start = 0
+ }
+ return s.requests[start:]
+}
+
+func (s *SimpleTestServer) RequestCount() int {
+ return len(s.requests)
+}
+
+func (s *SimpleTestServer) ResetRequests() {
+ s.requests = nil
+}
+
+func (s *SimpleTestServer) handler(writer http.ResponseWriter, request *http.Request) {
+ method := request.Method
+ var (
+ err error
+ responses map[string][]simpleResponse
+ responseIndex map[string]int
+ )
+ switch method {
+ case "GET":
+ responses = s.getResponses
+ responseIndex = s.getResponseIndex
+ _, err = readAndClose(request.Body)
+ if err != nil {
+ panic(err) // it is a test, panic should be fine
+ }
+ case "PUT":
+ responses = s.putResponses
+ responseIndex = s.putResponseIndex
+ err = request.ParseForm()
+ if err != nil {
+ panic(err)
+ }
+ case "POST":
+ responses = s.postResponses
+ responseIndex = s.postResponseIndex
+ contentType := request.Header.Get("Content-Type")
+ if strings.HasPrefix(contentType, "multipart/form-data;") {
+ err = request.ParseMultipartForm(2 << 20)
+ } else {
+ err = request.ParseForm()
+ }
+ if err != nil {
+ panic(err)
+ }
+ case "DELETE":
+ responses = s.deleteResponses
+ responseIndex = s.deleteResponseIndex
+ _, err := readAndClose(request.Body)
+ if err != nil {
+ panic(err)
+ }
+ default:
+ panic("unsupported method " + method)
+ }
+ s.requests = append(s.requests, request)
+ uri := request.URL.String()
+ testResponses, found := responses[uri]
+ if !found {
+ errorMsg := fmt.Sprintf("Error 404: page not found ('%v').", uri)
+ http.Error(writer, errorMsg, http.StatusNotFound)
+ } else {
+ index := responseIndex[uri]
+ response := testResponses[index]
+ responseIndex[uri] = index + 1
+
+ writer.WriteHeader(response.status)
+ fmt.Fprint(writer, response.body)
+ }
+}
diff --git a/automation/vendor/github.com/juju/gomaasapi/testservice.go b/automation/vendor/github.com/juju/gomaasapi/testservice.go
new file mode 100644
index 0000000..aa582da
--- /dev/null
+++ b/automation/vendor/github.com/juju/gomaasapi/testservice.go
@@ -0,0 +1,1672 @@
+// Copyright 2012-2016 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+package gomaasapi
+
+import (
+ "bufio"
+ "bytes"
+ "encoding/base64"
+ "encoding/json"
+ "fmt"
+ "io/ioutil"
+ "mime/multipart"
+ "net"
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "regexp"
+ "sort"
+ "strconv"
+ "strings"
+ "sync"
+ "text/template"
+ "time"
+
+ "gopkg.in/mgo.v2/bson"
+)
+
+// TestMAASObject is a fake MAAS server MAASObject.
+type TestMAASObject struct {
+ MAASObject
+ TestServer *TestServer
+}
+
+// checkError is a shorthand helper that panics if err is not nil.
+func checkError(err error) {
+ if err != nil {
+ panic(err)
+ }
+}
+
+// NewTestMAAS returns a TestMAASObject that implements the MAASObject
+// interface and thus can be used as a test object instead of the one returned
+// by gomaasapi.NewMAAS().
+func NewTestMAAS(version string) *TestMAASObject {
+ server := NewTestServer(version)
+ authClient, err := NewAnonymousClient(server.URL, version)
+ checkError(err)
+ maas := NewMAAS(*authClient)
+ return &TestMAASObject{*maas, server}
+}
+
+// Close shuts down the test server.
+func (testMAASObject *TestMAASObject) Close() {
+ testMAASObject.TestServer.Close()
+}
+
+// A TestServer is an HTTP server listening on a system-chosen port on the
+// local loopback interface, which simulates the behavior of a MAAS server.
+// It is intendend for use in end-to-end HTTP tests using the gomaasapi
+// library.
+type TestServer struct {
+ *httptest.Server
+ serveMux *http.ServeMux
+ client Client
+ nodes map[string]MAASObject
+ ownedNodes map[string]bool
+ // mapping system_id -> list of operations performed.
+ nodeOperations map[string][]string
+ // list of operations performed at the /nodes/ level.
+ nodesOperations []string
+ // mapping system_id -> list of Values passed when performing
+ // operations
+ nodeOperationRequestValues map[string][]url.Values
+ // list of Values passed when performing operations at the
+ // /nodes/ level.
+ nodesOperationRequestValues []url.Values
+ nodeMetadata map[string]Node
+ files map[string]MAASObject
+ networks map[string]MAASObject
+ networksPerNode map[string][]string
+ ipAddressesPerNetwork map[string][]string
+ version string
+ macAddressesPerNetwork map[string]map[string]JSONObject
+ nodeDetails map[string]string
+ zones map[string]JSONObject
+ // bootImages is a map of nodegroup UUIDs to boot-image objects.
+ bootImages map[string][]JSONObject
+ // nodegroupsInterfaces is a map of nodegroup UUIDs to interface
+ // objects.
+ nodegroupsInterfaces map[string][]JSONObject
+
+ // versionJSON is the response to the /version/ endpoint listing the
+ // capabilities of the MAAS server.
+ versionJSON string
+
+ // devices is a map of device UUIDs to devices.
+ devices map[string]*TestDevice
+
+ subnets map[uint]TestSubnet
+ subnetNameToID map[string]uint
+ nextSubnet uint
+ spaces map[uint]*TestSpace
+ spaceNameToID map[string]uint
+ nextSpace uint
+ vlans map[int]TestVLAN
+ nextVLAN int
+}
+
+type TestDevice struct {
+ IPAddresses []string
+ SystemId string
+ MACAddresses []string
+ Parent string
+ Hostname string
+
+ // Not part of the device definition but used by the template.
+ APIVersion string
+}
+
+func getNodesEndpoint(version string) string {
+ return fmt.Sprintf("/api/%s/nodes/", version)
+}
+
+func getNodeURL(version, systemId string) string {
+ return fmt.Sprintf("/api/%s/nodes/%s/", version, systemId)
+}
+
+func getNodeURLRE(version string) *regexp.Regexp {
+ reString := fmt.Sprintf("^/api/%s/nodes/([^/]*)/$", regexp.QuoteMeta(version))
+ return regexp.MustCompile(reString)
+}
+
+func getDevicesEndpoint(version string) string {
+ return fmt.Sprintf("/api/%s/devices/", version)
+}
+
+func getDeviceURL(version, systemId string) string {
+ return fmt.Sprintf("/api/%s/devices/%s/", version, systemId)
+}
+
+func getDeviceURLRE(version string) *regexp.Regexp {
+ reString := fmt.Sprintf("^/api/%s/devices/([^/]*)/$", regexp.QuoteMeta(version))
+ return regexp.MustCompile(reString)
+}
+
+func getFilesEndpoint(version string) string {
+ return fmt.Sprintf("/api/%s/files/", version)
+}
+
+func getFileURL(version, filename string) string {
+ // Uses URL object so filename is correctly percent-escaped
+ url := url.URL{}
+ url.Path = fmt.Sprintf("/api/%s/files/%s/", version, filename)
+ return url.String()
+}
+
+func getFileURLRE(version string) *regexp.Regexp {
+ reString := fmt.Sprintf("^/api/%s/files/(.*)/$", regexp.QuoteMeta(version))
+ return regexp.MustCompile(reString)
+}
+
+func getNetworksEndpoint(version string) string {
+ return fmt.Sprintf("/api/%s/networks/", version)
+}
+
+func getNetworkURL(version, name string) string {
+ return fmt.Sprintf("/api/%s/networks/%s/", version, name)
+}
+
+func getNetworkURLRE(version string) *regexp.Regexp {
+ reString := fmt.Sprintf("^/api/%s/networks/(.*)/$", regexp.QuoteMeta(version))
+ return regexp.MustCompile(reString)
+}
+
+func getIPAddressesEndpoint(version string) string {
+ return fmt.Sprintf("/api/%s/ipaddresses/", version)
+}
+
+func getMACAddressURL(version, systemId, macAddress string) string {
+ return fmt.Sprintf("/api/%s/nodes/%s/macs/%s/", version, systemId, url.QueryEscape(macAddress))
+}
+
+func getVersionURL(version string) string {
+ return fmt.Sprintf("/api/%s/version/", version)
+}
+
+func getNodegroupsEndpoint(version string) string {
+ return fmt.Sprintf("/api/%s/nodegroups/", version)
+}
+
+func getNodegroupURL(version, uuid string) string {
+ return fmt.Sprintf("/api/%s/nodegroups/%s/", version, uuid)
+}
+
+func getNodegroupsInterfacesURLRE(version string) *regexp.Regexp {
+ reString := fmt.Sprintf("^/api/%s/nodegroups/([^/]*)/interfaces/$", regexp.QuoteMeta(version))
+ return regexp.MustCompile(reString)
+}
+
+func getBootimagesURLRE(version string) *regexp.Regexp {
+ reString := fmt.Sprintf("^/api/%s/nodegroups/([^/]*)/boot-images/$", regexp.QuoteMeta(version))
+ return regexp.MustCompile(reString)
+}
+
+func getZonesEndpoint(version string) string {
+ return fmt.Sprintf("/api/%s/zones/", version)
+}
+
+// Clear clears all the fake data stored and recorded by the test server
+// (nodes, recorded operations, etc.).
+func (server *TestServer) Clear() {
+ server.nodes = make(map[string]MAASObject)
+ server.ownedNodes = make(map[string]bool)
+ server.nodesOperations = make([]string, 0)
+ server.nodeOperations = make(map[string][]string)
+ server.nodesOperationRequestValues = make([]url.Values, 0)
+ server.nodeOperationRequestValues = make(map[string][]url.Values)
+ server.nodeMetadata = make(map[string]Node)
+ server.files = make(map[string]MAASObject)
+ server.networks = make(map[string]MAASObject)
+ server.networksPerNode = make(map[string][]string)
+ server.ipAddressesPerNetwork = make(map[string][]string)
+ server.macAddressesPerNetwork = make(map[string]map[string]JSONObject)
+ server.nodeDetails = make(map[string]string)
+ server.bootImages = make(map[string][]JSONObject)
+ server.nodegroupsInterfaces = make(map[string][]JSONObject)
+ server.zones = make(map[string]JSONObject)
+ server.versionJSON = `{"capabilities": ["networks-management","static-ipaddresses","devices-management","network-deployment-ubuntu"]}`
+ server.devices = make(map[string]*TestDevice)
+ server.subnets = make(map[uint]TestSubnet)
+ server.subnetNameToID = make(map[string]uint)
+ server.nextSubnet = 1
+ server.spaces = make(map[uint]*TestSpace)
+ server.spaceNameToID = make(map[string]uint)
+ server.nextSpace = 1
+ server.vlans = make(map[int]TestVLAN)
+ server.nextVLAN = 1
+}
+
+// SetVersionJSON sets the JSON response (capabilities) returned from the
+// /version/ endpoint.
+func (server *TestServer) SetVersionJSON(json string) {
+ server.versionJSON = json
+}
+
+// NodesOperations returns the list of operations performed at the /nodes/
+// level.
+func (server *TestServer) NodesOperations() []string {
+ return server.nodesOperations
+}
+
+// NodeOperations returns the map containing the list of the operations
+// performed for each node.
+func (server *TestServer) NodeOperations() map[string][]string {
+ return server.nodeOperations
+}
+
+// NodesOperationRequestValues returns the list of url.Values extracted
+// from the request used when performing operations at the /nodes/ level.
+func (server *TestServer) NodesOperationRequestValues() []url.Values {
+ return server.nodesOperationRequestValues
+}
+
+// NodeOperationRequestValues returns the map containing the list of the
+// url.Values extracted from the request used when performing operations
+// on nodes.
+func (server *TestServer) NodeOperationRequestValues() map[string][]url.Values {
+ return server.nodeOperationRequestValues
+}
+
+func parseRequestValues(request *http.Request) url.Values {
+ var requestValues url.Values
+ if request.Header.Get("Content-Type") == "application/x-www-form-urlencoded" {
+ if request.PostForm == nil {
+ if err := request.ParseForm(); err != nil {
+ panic(err)
+ }
+ }
+ requestValues = request.PostForm
+ }
+ return requestValues
+}
+
+func (server *TestServer) addNodesOperation(operation string, request *http.Request) url.Values {
+ requestValues := parseRequestValues(request)
+ server.nodesOperations = append(server.nodesOperations, operation)
+ server.nodesOperationRequestValues = append(server.nodesOperationRequestValues, requestValues)
+ return requestValues
+}
+
+func (server *TestServer) addNodeOperation(systemId, operation string, request *http.Request) url.Values {
+ operations, present := server.nodeOperations[systemId]
+ operationRequestValues, present2 := server.nodeOperationRequestValues[systemId]
+ if present != present2 {
+ panic("inconsistent state: nodeOperations and nodeOperationRequestValues don't have the same keys.")
+ }
+ requestValues := parseRequestValues(request)
+ if !present {
+ operations = []string{operation}
+ operationRequestValues = []url.Values{requestValues}
+ } else {
+ operations = append(operations, operation)
+ operationRequestValues = append(operationRequestValues, requestValues)
+ }
+ server.nodeOperations[systemId] = operations
+ server.nodeOperationRequestValues[systemId] = operationRequestValues
+ return requestValues
+}
+
+// NewNode creates a MAAS node. The provided string should be a valid json
+// string representing a map and contain a string value for the key
+// 'system_id'. e.g. `{"system_id": "mysystemid"}`.
+// If one of these conditions is not met, NewNode panics.
+func (server *TestServer) NewNode(jsonText string) MAASObject {
+ var attrs map[string]interface{}
+ err := json.Unmarshal([]byte(jsonText), &attrs)
+ checkError(err)
+ systemIdEntry, hasSystemId := attrs["system_id"]
+ if !hasSystemId {
+ panic("The given map json string does not contain a 'system_id' value.")
+ }
+ systemId := systemIdEntry.(string)
+ attrs[resourceURI] = getNodeURL(server.version, systemId)
+ if _, hasStatus := attrs["status"]; !hasStatus {
+ attrs["status"] = NodeStatusDeployed
+ }
+ obj := newJSONMAASObject(attrs, server.client)
+ server.nodes[systemId] = obj
+ return obj
+}
+
+// Nodes returns a map associating all the nodes' system ids with the nodes'
+// objects.
+func (server *TestServer) Nodes() map[string]MAASObject {
+ return server.nodes
+}
+
+// OwnedNodes returns a map whose keys represent the nodes that are currently
+// allocated.
+func (server *TestServer) OwnedNodes() map[string]bool {
+ return server.ownedNodes
+}
+
+// NewFile creates a file in the test MAAS server.
+func (server *TestServer) NewFile(filename string, filecontent []byte) MAASObject {
+ attrs := make(map[string]interface{})
+ attrs[resourceURI] = getFileURL(server.version, filename)
+ base64Content := base64.StdEncoding.EncodeToString(filecontent)
+ attrs["content"] = base64Content
+ attrs["filename"] = filename
+
+ // Allocate an arbitrary URL here. It would be nice if the caller
+ // could do this, but that would change the API and require many
+ // changes.
+ escapedName := url.QueryEscape(filename)
+ attrs["anon_resource_uri"] = "/maas/1.0/files/?op=get_by_key&key=" + escapedName + "_key"
+
+ obj := newJSONMAASObject(attrs, server.client)
+ server.files[filename] = obj
+ return obj
+}
+
+func (server *TestServer) Files() map[string]MAASObject {
+ return server.files
+}
+
+// ChangeNode updates a node with the given key/value.
+func (server *TestServer) ChangeNode(systemId, key, value string) {
+ node, found := server.nodes[systemId]
+ if !found {
+ panic("No node with such 'system_id'.")
+ }
+ node.GetMap()[key] = maasify(server.client, value)
+}
+
+// NewIPAddress creates a new static IP address reservation for the
+// given network/subnet and ipAddress. While networks is being deprecated
+// try the given name as both a netowrk and a subnet.
+func (server *TestServer) NewIPAddress(ipAddress, networkOrSubnet string) {
+ _, foundNetwork := server.networks[networkOrSubnet]
+ subnetID, foundSubnet := server.subnetNameToID[networkOrSubnet]
+
+ if (foundNetwork || foundSubnet) == false {
+ panic("No such network or subnet: " + networkOrSubnet)
+ }
+ if foundNetwork {
+ ips, found := server.ipAddressesPerNetwork[networkOrSubnet]
+ if found {
+ ips = append(ips, ipAddress)
+ } else {
+ ips = []string{ipAddress}
+ }
+ server.ipAddressesPerNetwork[networkOrSubnet] = ips
+ } else {
+ subnet := server.subnets[subnetID]
+ netIp := net.ParseIP(ipAddress)
+ if netIp == nil {
+ panic(ipAddress + " is invalid")
+ }
+ ip := IPFromNetIP(netIp)
+ ip.Purpose = []string{"assigned-ip"}
+ subnet.InUseIPAddresses = append(subnet.InUseIPAddresses, ip)
+ server.subnets[subnetID] = subnet
+ }
+}
+
+// RemoveIPAddress removes the given existing ipAddress and returns
+// whether it was actually removed.
+func (server *TestServer) RemoveIPAddress(ipAddress string) bool {
+ for network, ips := range server.ipAddressesPerNetwork {
+ for i, ip := range ips {
+ if ip == ipAddress {
+ ips = append(ips[:i], ips[i+1:]...)
+ server.ipAddressesPerNetwork[network] = ips
+ return true
+ }
+ }
+ }
+ for _, device := range server.devices {
+ for i, addr := range device.IPAddresses {
+ if addr == ipAddress {
+ device.IPAddresses = append(device.IPAddresses[:i], device.IPAddresses[i+1:]...)
+ return true
+ }
+ }
+ }
+ return false
+}
+
+// IPAddresses returns the map with network names as keys and slices
+// of IP addresses belonging to each network as values.
+func (server *TestServer) IPAddresses() map[string][]string {
+ return server.ipAddressesPerNetwork
+}
+
+// NewNetwork creates a network in the test MAAS server
+func (server *TestServer) NewNetwork(jsonText string) MAASObject {
+ var attrs map[string]interface{}
+ err := json.Unmarshal([]byte(jsonText), &attrs)
+ checkError(err)
+ nameEntry, hasName := attrs["name"]
+ _, hasIP := attrs["ip"]
+ _, hasNetmask := attrs["netmask"]
+ if !hasName || !hasIP || !hasNetmask {
+ panic("The given map json string does not contain a 'name', 'ip', or 'netmask' value.")
+ }
+ // TODO(gz): Sanity checking done on other fields
+ name := nameEntry.(string)
+ attrs[resourceURI] = getNetworkURL(server.version, name)
+ obj := newJSONMAASObject(attrs, server.client)
+ server.networks[name] = obj
+ return obj
+}
+
+// NewNodegroupInterface adds a nodegroup-interface, for the specified
+// nodegroup, in the test MAAS server.
+func (server *TestServer) NewNodegroupInterface(uuid, jsonText string) JSONObject {
+ _, ok := server.bootImages[uuid]
+ if !ok {
+ panic("no nodegroup with the given UUID")
+ }
+ var attrs map[string]interface{}
+ err := json.Unmarshal([]byte(jsonText), &attrs)
+ checkError(err)
+ requiredMembers := []string{"ip_range_high", "ip_range_low", "broadcast_ip", "static_ip_range_low", "static_ip_range_high", "name", "ip", "subnet_mask", "management", "interface"}
+ for _, member := range requiredMembers {
+ _, hasMember := attrs[member]
+ if !hasMember {
+ panic(fmt.Sprintf("The given map json string does not contain a required %q", member))
+ }
+ }
+ obj := maasify(server.client, attrs)
+ server.nodegroupsInterfaces[uuid] = append(server.nodegroupsInterfaces[uuid], obj)
+ return obj
+}
+
+func (server *TestServer) ConnectNodeToNetwork(systemId, name string) {
+ _, hasNode := server.nodes[systemId]
+ if !hasNode {
+ panic("no node with the given system id")
+ }
+ _, hasNetwork := server.networks[name]
+ if !hasNetwork {
+ panic("no network with the given name")
+ }
+ networkNames, _ := server.networksPerNode[systemId]
+ server.networksPerNode[systemId] = append(networkNames, name)
+}
+
+func (server *TestServer) ConnectNodeToNetworkWithMACAddress(systemId, networkName, macAddress string) {
+ node, hasNode := server.nodes[systemId]
+ if !hasNode {
+ panic("no node with the given system id")
+ }
+ if _, hasNetwork := server.networks[networkName]; !hasNetwork {
+ panic("no network with the given name")
+ }
+ networkNames, _ := server.networksPerNode[systemId]
+ server.networksPerNode[systemId] = append(networkNames, networkName)
+ attrs := make(map[string]interface{})
+ attrs[resourceURI] = getMACAddressURL(server.version, systemId, macAddress)
+ attrs["mac_address"] = macAddress
+ array := []JSONObject{}
+ if set, ok := node.GetMap()["macaddress_set"]; ok {
+ var err error
+ array, err = set.GetArray()
+ if err != nil {
+ panic(err)
+ }
+ }
+ array = append(array, maasify(server.client, attrs))
+ node.GetMap()["macaddress_set"] = JSONObject{value: array, client: server.client}
+ if _, ok := server.macAddressesPerNetwork[networkName]; !ok {
+ server.macAddressesPerNetwork[networkName] = map[string]JSONObject{}
+ }
+ server.macAddressesPerNetwork[networkName][systemId] = maasify(server.client, attrs)
+}
+
+// AddBootImage adds a boot-image object to the specified nodegroup.
+func (server *TestServer) AddBootImage(nodegroupUUID string, jsonText string) {
+ var attrs map[string]interface{}
+ err := json.Unmarshal([]byte(jsonText), &attrs)
+ checkError(err)
+ if _, ok := attrs["architecture"]; !ok {
+ panic("The boot-image json string does not contain an 'architecture' value.")
+ }
+ if _, ok := attrs["release"]; !ok {
+ panic("The boot-image json string does not contain a 'release' value.")
+ }
+ obj := maasify(server.client, attrs)
+ server.bootImages[nodegroupUUID] = append(server.bootImages[nodegroupUUID], obj)
+}
+
+// AddZone adds a physical zone to the server.
+func (server *TestServer) AddZone(name, description string) {
+ attrs := map[string]interface{}{
+ "name": name,
+ "description": description,
+ }
+ obj := maasify(server.client, attrs)
+ server.zones[name] = obj
+}
+
+func (server *TestServer) AddDevice(device *TestDevice) {
+ server.devices[device.SystemId] = device
+}
+
+func (server *TestServer) Devices() map[string]*TestDevice {
+ return server.devices
+}
+
+// NewTestServer starts and returns a new MAAS test server. The caller should call Close when finished, to shut it down.
+func NewTestServer(version string) *TestServer {
+ server := &TestServer{version: version}
+
+ serveMux := http.NewServeMux()
+ devicesURL := getDevicesEndpoint(server.version)
+ // Register handler for '/api/<version>/devices/*'.
+ serveMux.HandleFunc(devicesURL, func(w http.ResponseWriter, r *http.Request) {
+ devicesHandler(server, w, r)
+ })
+ nodesURL := getNodesEndpoint(server.version)
+ // Register handler for '/api/<version>/nodes/*'.
+ serveMux.HandleFunc(nodesURL, func(w http.ResponseWriter, r *http.Request) {
+ nodesHandler(server, w, r)
+ })
+ filesURL := getFilesEndpoint(server.version)
+ // Register handler for '/api/<version>/files/*'.
+ serveMux.HandleFunc(filesURL, func(w http.ResponseWriter, r *http.Request) {
+ filesHandler(server, w, r)
+ })
+ networksURL := getNetworksEndpoint(server.version)
+ // Register handler for '/api/<version>/networks/'.
+ serveMux.HandleFunc(networksURL, func(w http.ResponseWriter, r *http.Request) {
+ networksHandler(server, w, r)
+ })
+ ipAddressesURL := getIPAddressesEndpoint(server.version)
+ // Register handler for '/api/<version>/ipaddresses/'.
+ serveMux.HandleFunc(ipAddressesURL, func(w http.ResponseWriter, r *http.Request) {
+ ipAddressesHandler(server, w, r)
+ })
+ versionURL := getVersionURL(server.version)
+ // Register handler for '/api/<version>/version/'.
+ serveMux.HandleFunc(versionURL, func(w http.ResponseWriter, r *http.Request) {
+ versionHandler(server, w, r)
+ })
+ // Register handler for '/api/<version>/nodegroups/*'.
+ nodegroupsURL := getNodegroupsEndpoint(server.version)
+ serveMux.HandleFunc(nodegroupsURL, func(w http.ResponseWriter, r *http.Request) {
+ nodegroupsHandler(server, w, r)
+ })
+
+ // Register handler for '/api/<version>/zones/*'.
+ zonesURL := getZonesEndpoint(server.version)
+ serveMux.HandleFunc(zonesURL, func(w http.ResponseWriter, r *http.Request) {
+ zonesHandler(server, w, r)
+ })
+
+ subnetsURL := getSubnetsEndpoint(server.version)
+ serveMux.HandleFunc(subnetsURL, func(w http.ResponseWriter, r *http.Request) {
+ subnetsHandler(server, w, r)
+ })
+
+ spacesURL := getSpacesEndpoint(server.version)
+ serveMux.HandleFunc(spacesURL, func(w http.ResponseWriter, r *http.Request) {
+ spacesHandler(server, w, r)
+ })
+
+ vlansURL := getVLANsEndpoint(server.version)
+ serveMux.HandleFunc(vlansURL, func(w http.ResponseWriter, r *http.Request) {
+ vlansHandler(server, w, r)
+ })
+
+ var mu sync.Mutex
+ singleFile := func(w http.ResponseWriter, req *http.Request) {
+ mu.Lock()
+ defer mu.Unlock()
+ serveMux.ServeHTTP(w, req)
+ }
+
+ newServer := httptest.NewServer(http.HandlerFunc(singleFile))
+ client, err := NewAnonymousClient(newServer.URL, "1.0")
+ checkError(err)
+ server.Server = newServer
+ server.serveMux = serveMux
+ server.client = *client
+ server.Clear()
+ return server
+}
+
+// devicesHandler handles requests for '/api/<version>/devices/*'.
+func devicesHandler(server *TestServer, w http.ResponseWriter, r *http.Request) {
+ values, err := url.ParseQuery(r.URL.RawQuery)
+ checkError(err)
+ op := values.Get("op")
+ deviceURLRE := getDeviceURLRE(server.version)
+ deviceURLMatch := deviceURLRE.FindStringSubmatch(r.URL.Path)
+ devicesURL := getDevicesEndpoint(server.version)
+ switch {
+ case r.URL.Path == devicesURL:
+ devicesTopLevelHandler(server, w, r, op)
+ case deviceURLMatch != nil:
+ // Request for a single device.
+ deviceHandler(server, w, r, deviceURLMatch[1], op)
+ default:
+ // Default handler: not found.
+ http.NotFoundHandler().ServeHTTP(w, r)
+ }
+}
+
+// devicesTopLevelHandler handles a request for /api/<version>/devices/
+// (with no device id following as part of the path).
+func devicesTopLevelHandler(server *TestServer, w http.ResponseWriter, r *http.Request, op string) {
+ switch {
+ case r.Method == "GET" && op == "list":
+ // Device listing operation.
+ deviceListingHandler(server, w, r)
+ case r.Method == "POST" && op == "new":
+ newDeviceHandler(server, w, r)
+ default:
+ w.WriteHeader(http.StatusBadRequest)
+ }
+}
+
+func macMatches(mac string, device *TestDevice) bool {
+ return contains(device.MACAddresses, mac)
+}
+
+// deviceListingHandler handles requests for '/devices/'.
+func deviceListingHandler(server *TestServer, w http.ResponseWriter, r *http.Request) {
+ values, err := url.ParseQuery(r.URL.RawQuery)
+ checkError(err)
+ // TODO(mfoord): support filtering by hostname and id
+ macs, hasMac := values["mac_address"]
+ var matchedDevices []*TestDevice
+ if !hasMac {
+ for _, device := range server.devices {
+ matchedDevices = append(matchedDevices, device)
+ }
+ } else {
+ for _, mac := range macs {
+ for _, device := range server.devices {
+ if macMatches(mac, device) {
+ matchedDevices = append(matchedDevices, device)
+ }
+ }
+ }
+ }
+ deviceChunks := make([]string, len(matchedDevices))
+ for i := range matchedDevices {
+ deviceChunks[i] = renderDevice(matchedDevices[i])
+ }
+ json := fmt.Sprintf("[%v]", strings.Join(deviceChunks, ", "))
+
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprint(w, json)
+}
+
+var templateFuncs = template.FuncMap{
+ "quotedList": func(items []string) string {
+ var pieces []string
+ for _, item := range items {
+ pieces = append(pieces, fmt.Sprintf("%q", item))
+ }
+ return strings.Join(pieces, ", ")
+ },
+ "last": func(items []string) []string {
+ if len(items) == 0 {
+ return []string{}
+ }
+ return items[len(items)-1:]
+ },
+ "allButLast": func(items []string) []string {
+ if len(items) < 2 {
+ return []string{}
+ }
+ return items[0 : len(items)-1]
+ },
+}
+
+const (
+ // The json template for generating new devices.
+ // TODO(mfoord): set resource_uri in MAC addresses
+ deviceTemplate = `{
+ "macaddress_set": [{{range .MACAddresses | allButLast}}
+ {
+ "mac_address": "{{.}}"
+ },{{end}}{{range .MACAddresses | last}}
+ {
+ "mac_address": "{{.}}"
+ }{{end}}
+ ],
+ "zone": {
+ "resource_uri": "/MAAS/api/{{.APIVersion}}/zones/default/",
+ "name": "default",
+ "description": ""
+ },
+ "parent": "{{.Parent}}",
+ "ip_addresses": [{{.IPAddresses | quotedList }}],
+ "hostname": "{{.Hostname}}",
+ "tag_names": [],
+ "owner": "maas-admin",
+ "system_id": "{{.SystemId}}",
+ "resource_uri": "/MAAS/api/{{.APIVersion}}/devices/{{.SystemId}}/"
+}`
+)
+
+func renderDevice(device *TestDevice) string {
+ t := template.New("Device template")
+ t = t.Funcs(templateFuncs)
+ t, err := t.Parse(deviceTemplate)
+ checkError(err)
+ var buf bytes.Buffer
+ err = t.Execute(&buf, device)
+ checkError(err)
+ return buf.String()
+}
+
+func getValue(values url.Values, value string) (string, bool) {
+ result, hasResult := values[value]
+ if !hasResult || len(result) != 1 || result[0] == "" {
+ return "", false
+ }
+ return result[0], true
+}
+
+func getValues(values url.Values, key string) ([]string, bool) {
+ result, hasResult := values[key]
+ if !hasResult {
+ return nil, false
+ }
+ var output []string
+ for _, val := range result {
+ if val != "" {
+ output = append(output, val)
+ }
+ }
+ if len(output) == 0 {
+ return nil, false
+ }
+ return output, true
+}
+
+// newDeviceHandler creates, stores and returns new devices.
+func newDeviceHandler(server *TestServer, w http.ResponseWriter, r *http.Request) {
+ err := r.ParseForm()
+ checkError(err)
+ values := r.PostForm
+
+ // TODO(mfood): generate a "proper" uuid for the system Id.
+ uuid, err := generateNonce()
+ checkError(err)
+ systemId := fmt.Sprintf("node-%v", uuid)
+ // At least one MAC address must be specified.
+ // TODO(mfoord) we only support a single MAC in the test server.
+ macs, hasMacs := getValues(values, "mac_addresses")
+
+ // hostname and parent are optional.
+ // TODO(mfoord): we require both to be set in the test server.
+ hostname, hasHostname := getValue(values, "hostname")
+ parent, hasParent := getValue(values, "parent")
+ if !hasHostname || !hasMacs || !hasParent {
+ w.WriteHeader(http.StatusBadRequest)
+ return
+ }
+
+ device := &TestDevice{
+ MACAddresses: macs,
+ APIVersion: server.version,
+ Parent: parent,
+ Hostname: hostname,
+ SystemId: systemId,
+ }
+
+ deviceJSON := renderDevice(device)
+ server.devices[systemId] = device
+
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprint(w, deviceJSON)
+ return
+}
+
+// deviceHandler handles requests for '/api/<version>/devices/<system_id>/'.
+func deviceHandler(server *TestServer, w http.ResponseWriter, r *http.Request, systemId string, operation string) {
+ device, ok := server.devices[systemId]
+ if !ok {
+ http.NotFoundHandler().ServeHTTP(w, r)
+ return
+ }
+ if r.Method == "GET" {
+ deviceJSON := renderDevice(device)
+ if operation == "" {
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprint(w, deviceJSON)
+ return
+ } else {
+ w.WriteHeader(http.StatusBadRequest)
+ return
+ }
+ }
+ if r.Method == "POST" {
+ if operation == "claim_sticky_ip_address" {
+ err := r.ParseForm()
+ checkError(err)
+ values := r.PostForm
+ // TODO(mfoord): support optional mac_address parameter
+ // TODO(mfoord): requested_address should be optional
+ // and we should generate one if it isn't provided.
+ address, hasAddress := getValue(values, "requested_address")
+ if !hasAddress {
+ w.WriteHeader(http.StatusBadRequest)
+ return
+ }
+ checkError(err)
+ device.IPAddresses = append(device.IPAddresses, address)
+ deviceJSON := renderDevice(device)
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprint(w, deviceJSON)
+ return
+ } else {
+ w.WriteHeader(http.StatusBadRequest)
+ return
+ }
+ } else if r.Method == "DELETE" {
+ delete(server.devices, systemId)
+ w.WriteHeader(http.StatusNoContent)
+ return
+
+ }
+
+ // TODO(mfoord): support PUT method for updating device
+ http.NotFoundHandler().ServeHTTP(w, r)
+}
+
+// nodesHandler handles requests for '/api/<version>/nodes/*'.
+func nodesHandler(server *TestServer, w http.ResponseWriter, r *http.Request) {
+ values, err := url.ParseQuery(r.URL.RawQuery)
+ checkError(err)
+ op := values.Get("op")
+ nodeURLRE := getNodeURLRE(server.version)
+ nodeURLMatch := nodeURLRE.FindStringSubmatch(r.URL.Path)
+ nodesURL := getNodesEndpoint(server.version)
+ switch {
+ case r.URL.Path == nodesURL:
+ nodesTopLevelHandler(server, w, r, op)
+ case nodeURLMatch != nil:
+ // Request for a single node.
+ nodeHandler(server, w, r, nodeURLMatch[1], op)
+ default:
+ // Default handler: not found.
+ http.NotFoundHandler().ServeHTTP(w, r)
+ }
+}
+
+// nodeHandler handles requests for '/api/<version>/nodes/<system_id>/'.
+func nodeHandler(server *TestServer, w http.ResponseWriter, r *http.Request, systemId string, operation string) {
+ node, ok := server.nodes[systemId]
+ if !ok {
+ http.NotFoundHandler().ServeHTTP(w, r)
+ return
+ }
+ UUID, UUIDError := node.values["system_id"].GetString()
+ if UUIDError == nil {
+ i, err := JSONObjectFromStruct(server.client, server.nodeMetadata[UUID].Interfaces)
+ checkError(err)
+ node.values["interface_set"] = i
+ }
+
+ if r.Method == "GET" {
+ if operation == "" {
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprint(w, marshalNode(node))
+ return
+ } else if operation == "details" {
+ nodeDetailsHandler(server, w, r, systemId)
+ return
+ } else {
+ w.WriteHeader(http.StatusBadRequest)
+ return
+ }
+ }
+ if r.Method == "POST" {
+ // The only operations supported are "start", "stop" and "release".
+ if operation == "start" || operation == "stop" || operation == "release" {
+ // Record operation on node.
+ server.addNodeOperation(systemId, operation, r)
+
+ if operation == "release" {
+ delete(server.OwnedNodes(), systemId)
+ }
+
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprint(w, marshalNode(node))
+ return
+ }
+
+ w.WriteHeader(http.StatusBadRequest)
+ return
+ }
+ if r.Method == "DELETE" {
+ delete(server.nodes, systemId)
+ w.WriteHeader(http.StatusOK)
+ return
+ }
+ http.NotFoundHandler().ServeHTTP(w, r)
+}
+
+func contains(slice []string, val string) bool {
+ for _, item := range slice {
+ if item == val {
+ return true
+ }
+ }
+ return false
+}
+
+// nodeListingHandler handles requests for '/nodes/'.
+func nodeListingHandler(server *TestServer, w http.ResponseWriter, r *http.Request) {
+ values, err := url.ParseQuery(r.URL.RawQuery)
+ checkError(err)
+ ids, hasId := values["id"]
+ var convertedNodes = []map[string]JSONObject{}
+ for systemId, node := range server.nodes {
+ if !hasId || contains(ids, systemId) {
+ convertedNodes = append(convertedNodes, node.GetMap())
+ }
+ }
+ res, err := json.MarshalIndent(convertedNodes, "", " ")
+ checkError(err)
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprint(w, string(res))
+}
+
+// nodeDeploymentStatusHandler handles requests for '/nodes/?op=deployment_status'.
+func nodeDeploymentStatusHandler(server *TestServer, w http.ResponseWriter, r *http.Request) {
+ values, err := url.ParseQuery(r.URL.RawQuery)
+ checkError(err)
+ nodes, _ := values["nodes"]
+ var nodeStatus = make(map[string]interface{})
+ for _, systemId := range nodes {
+ node := server.nodes[systemId]
+ field, err := node.GetField("status")
+ if err != nil {
+ continue
+ }
+ switch field {
+ case NodeStatusDeployed:
+ nodeStatus[systemId] = "Deployed"
+ case NodeStatusFailedDeployment:
+ nodeStatus[systemId] = "Failed deployment"
+ default:
+ nodeStatus[systemId] = "Not in Deployment"
+ }
+ }
+ obj := maasify(server.client, nodeStatus)
+ res, err := json.MarshalIndent(obj, "", " ")
+ checkError(err)
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprint(w, string(res))
+}
+
+// findFreeNode looks for a node that is currently available, and
+// matches the specified filter.
+func findFreeNode(server *TestServer, filter url.Values) *MAASObject {
+ for systemID, node := range server.Nodes() {
+ _, present := server.OwnedNodes()[systemID]
+ if !present {
+ var agentName, nodeName, zoneName, mem, cpuCores, arch string
+ for k := range filter {
+ switch k {
+ case "agent_name":
+ agentName = filter.Get(k)
+ case "name":
+ nodeName = filter.Get(k)
+ case "zone":
+ zoneName = filter.Get(k)
+ case "mem":
+ mem = filter.Get(k)
+ case "arch":
+ arch = filter.Get(k)
+ case "cpu-cores":
+ cpuCores = filter.Get(k)
+ }
+ }
+ if nodeName != "" && !matchField(node, "hostname", nodeName) {
+ continue
+ }
+ if zoneName != "" && !matchField(node, "zone", zoneName) {
+ continue
+ }
+ if mem != "" && !matchNumericField(node, "memory", mem) {
+ continue
+ }
+ if arch != "" && !matchArchitecture(node, "architecture", arch) {
+ continue
+ }
+ if cpuCores != "" && !matchNumericField(node, "cpu_count", cpuCores) {
+ continue
+ }
+ if agentName != "" {
+ agentNameObj := maasify(server.client, agentName)
+ node.GetMap()["agent_name"] = agentNameObj
+ } else {
+ delete(node.GetMap(), "agent_name")
+ }
+ return &node
+ }
+ }
+ return nil
+}
+
+func matchArchitecture(node MAASObject, k, v string) bool {
+ field, err := node.GetField(k)
+ if err != nil {
+ return false
+ }
+ baseArch := strings.Split(field, "/")
+ return v == baseArch[0]
+}
+
+func matchNumericField(node MAASObject, k, v string) bool {
+ field, ok := node.GetMap()[k]
+ if !ok {
+ return false
+ }
+ nodeVal, err := field.GetFloat64()
+ if err != nil {
+ return false
+ }
+ constraintVal, err := strconv.ParseFloat(v, 64)
+ if err != nil {
+ return false
+ }
+ return constraintVal <= nodeVal
+}
+
+func matchField(node MAASObject, k, v string) bool {
+ field, err := node.GetField(k)
+ if err != nil {
+ return false
+ }
+ return field == v
+}
+
+// nodesAcquireHandler simulates acquiring a node.
+func nodesAcquireHandler(server *TestServer, w http.ResponseWriter, r *http.Request) {
+ requestValues := server.addNodesOperation("acquire", r)
+ node := findFreeNode(server, requestValues)
+ if node == nil {
+ w.WriteHeader(http.StatusConflict)
+ } else {
+ systemId, err := node.GetField("system_id")
+ checkError(err)
+ server.OwnedNodes()[systemId] = true
+ res, err := json.MarshalIndent(node, "", " ")
+ checkError(err)
+ // Record operation.
+ server.addNodeOperation(systemId, "acquire", r)
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprint(w, string(res))
+ }
+}
+
+// nodesReleaseHandler simulates releasing multiple nodes.
+func nodesReleaseHandler(server *TestServer, w http.ResponseWriter, r *http.Request) {
+ server.addNodesOperation("release", r)
+ values := server.NodesOperationRequestValues()
+ systemIds := values[len(values)-1]["nodes"]
+ var unknown []string
+ for _, systemId := range systemIds {
+ if _, ok := server.Nodes()[systemId]; !ok {
+ unknown = append(unknown, systemId)
+ }
+ }
+ if len(unknown) > 0 {
+ w.WriteHeader(http.StatusBadRequest)
+ fmt.Fprintf(w, "Unknown node(s): %s.", strings.Join(unknown, ", "))
+ return
+ }
+ var releasedNodes = []map[string]JSONObject{}
+ for _, systemId := range systemIds {
+ if _, ok := server.OwnedNodes()[systemId]; !ok {
+ continue
+ }
+ delete(server.OwnedNodes(), systemId)
+ node := server.Nodes()[systemId]
+ releasedNodes = append(releasedNodes, node.GetMap())
+ }
+ res, err := json.MarshalIndent(releasedNodes, "", " ")
+ checkError(err)
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprint(w, string(res))
+}
+
+// nodesTopLevelHandler handles a request for /api/<version>/nodes/
+// (with no node id following as part of the path).
+func nodesTopLevelHandler(server *TestServer, w http.ResponseWriter, r *http.Request, op string) {
+ switch {
+ case r.Method == "GET" && op == "list":
+ // Node listing operation.
+ nodeListingHandler(server, w, r)
+ case r.Method == "GET" && op == "deployment_status":
+ // Node deployment_status operation.
+ nodeDeploymentStatusHandler(server, w, r)
+ case r.Method == "POST" && op == "acquire":
+ nodesAcquireHandler(server, w, r)
+ case r.Method == "POST" && op == "release":
+ nodesReleaseHandler(server, w, r)
+ default:
+ w.WriteHeader(http.StatusBadRequest)
+ }
+}
+
+// AddNodeDetails stores node details, expected in XML format.
+func (server *TestServer) AddNodeDetails(systemId, xmlText string) {
+ _, hasNode := server.nodes[systemId]
+ if !hasNode {
+ panic("no node with the given system id")
+ }
+ server.nodeDetails[systemId] = xmlText
+}
+
+const lldpXML = `
+<?xml version="1.0" encoding="UTF-8"?>
+<lldp label="LLDP neighbors"/>`
+
+// nodeDetailesHandler handles requests for '/api/<version>/nodes/<system_id>/?op=details'.
+func nodeDetailsHandler(server *TestServer, w http.ResponseWriter, r *http.Request, systemId string) {
+ attrs := make(map[string]interface{})
+ attrs["lldp"] = lldpXML
+ xmlText, _ := server.nodeDetails[systemId]
+ attrs["lshw"] = []byte(xmlText)
+ res, err := bson.Marshal(attrs)
+ checkError(err)
+ w.Header().Set("Content-Type", "application/bson")
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprint(w, string(res))
+}
+
+// filesHandler handles requests for '/api/<version>/files/*'.
+func filesHandler(server *TestServer, w http.ResponseWriter, r *http.Request) {
+ values, err := url.ParseQuery(r.URL.RawQuery)
+ checkError(err)
+ op := values.Get("op")
+ fileURLRE := getFileURLRE(server.version)
+ fileURLMatch := fileURLRE.FindStringSubmatch(r.URL.Path)
+ fileListingURL := getFilesEndpoint(server.version)
+ switch {
+ case r.Method == "GET" && op == "list" && r.URL.Path == fileListingURL:
+ // File listing operation.
+ fileListingHandler(server, w, r)
+ case op == "get" && r.Method == "GET" && r.URL.Path == fileListingURL:
+ getFileHandler(server, w, r)
+ case op == "add" && r.Method == "POST" && r.URL.Path == fileListingURL:
+ addFileHandler(server, w, r)
+ case fileURLMatch != nil:
+ // Request for a single file.
+ fileHandler(server, w, r, fileURLMatch[1], op)
+ default:
+ // Default handler: not found.
+ http.NotFoundHandler().ServeHTTP(w, r)
+ }
+
+}
+
+// listFilenames returns the names of those uploaded files whose names start
+// with the given prefix, sorted lexicographically.
+func listFilenames(server *TestServer, prefix string) []string {
+ var filenames = make([]string, 0)
+ for filename := range server.files {
+ if strings.HasPrefix(filename, prefix) {
+ filenames = append(filenames, filename)
+ }
+ }
+ sort.Strings(filenames)
+ return filenames
+}
+
+// stripFileContent copies a map of attributes representing an uploaded file,
+// but with the "content" attribute removed.
+func stripContent(original map[string]JSONObject) map[string]JSONObject {
+ newMap := make(map[string]JSONObject, len(original)-1)
+ for key, value := range original {
+ if key != "content" {
+ newMap[key] = value
+ }
+ }
+ return newMap
+}
+
+// fileListingHandler handles requests for '/api/<version>/files/?op=list'.
+func fileListingHandler(server *TestServer, w http.ResponseWriter, r *http.Request) {
+ values, err := url.ParseQuery(r.URL.RawQuery)
+ checkError(err)
+ prefix := values.Get("prefix")
+ filenames := listFilenames(server, prefix)
+
+ // Build a sorted list of the files as map[string]JSONObject objects.
+ convertedFiles := make([]map[string]JSONObject, 0)
+ for _, filename := range filenames {
+ // The "content" attribute is not in the listing.
+ fileMap := stripContent(server.files[filename].GetMap())
+ convertedFiles = append(convertedFiles, fileMap)
+ }
+ res, err := json.MarshalIndent(convertedFiles, "", " ")
+ checkError(err)
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprint(w, string(res))
+}
+
+// fileHandler handles requests for '/api/<version>/files/<filename>/'.
+func fileHandler(server *TestServer, w http.ResponseWriter, r *http.Request, filename string, operation string) {
+ switch {
+ case r.Method == "DELETE":
+ delete(server.files, filename)
+ w.WriteHeader(http.StatusOK)
+ case r.Method == "GET":
+ // Retrieve a file's information (including content) as a JSON
+ // object.
+ file, ok := server.files[filename]
+ if !ok {
+ http.NotFoundHandler().ServeHTTP(w, r)
+ return
+ }
+ jsonText, err := json.MarshalIndent(file, "", " ")
+ if err != nil {
+ panic(err)
+ }
+ w.WriteHeader(http.StatusOK)
+ w.Write(jsonText)
+ default:
+ // Default handler: not found.
+ http.NotFoundHandler().ServeHTTP(w, r)
+ }
+}
+
+// InternalError replies to the request with an HTTP 500 internal error.
+func InternalError(w http.ResponseWriter, r *http.Request, err error) {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+}
+
+// getFileHandler handles requests for
+// '/api/<version>/files/?op=get&filename=filename'.
+func getFileHandler(server *TestServer, w http.ResponseWriter, r *http.Request) {
+ values, err := url.ParseQuery(r.URL.RawQuery)
+ checkError(err)
+ filename := values.Get("filename")
+ file, found := server.files[filename]
+ if !found {
+ http.NotFoundHandler().ServeHTTP(w, r)
+ return
+ }
+ base64Content, err := file.GetField("content")
+ if err != nil {
+ InternalError(w, r, err)
+ return
+ }
+ content, err := base64.StdEncoding.DecodeString(base64Content)
+ if err != nil {
+ InternalError(w, r, err)
+ return
+ }
+ w.Write(content)
+}
+
+func readMultipart(upload *multipart.FileHeader) ([]byte, error) {
+ file, err := upload.Open()
+ if err != nil {
+ return nil, err
+ }
+ defer file.Close()
+ reader := bufio.NewReader(file)
+ return ioutil.ReadAll(reader)
+}
+
+// filesHandler handles requests for '/api/<version>/files/?op=add&filename=filename'.
+func addFileHandler(server *TestServer, w http.ResponseWriter, r *http.Request) {
+ err := r.ParseMultipartForm(10000000)
+ checkError(err)
+
+ filename := r.Form.Get("filename")
+ if filename == "" {
+ panic("upload has no filename")
+ }
+
+ uploads := r.MultipartForm.File
+ if len(uploads) != 1 {
+ panic("the payload should contain one file and one file only")
+ }
+ var upload *multipart.FileHeader
+ for _, uploadContent := range uploads {
+ upload = uploadContent[0]
+ }
+ content, err := readMultipart(upload)
+ checkError(err)
+ server.NewFile(filename, content)
+ w.WriteHeader(http.StatusOK)
+}
+
+// networkListConnectedMACSHandler handles requests for '/api/<version>/networks/<network>/?op=list_connected_macs'
+func networkListConnectedMACSHandler(server *TestServer, w http.ResponseWriter, r *http.Request) {
+ networkURLRE := getNetworkURLRE(server.version)
+ networkURLREMatch := networkURLRE.FindStringSubmatch(r.URL.Path)
+ if networkURLREMatch == nil {
+ http.NotFoundHandler().ServeHTTP(w, r)
+ return
+ }
+ networkName := networkURLREMatch[1]
+ convertedMacAddresses := []map[string]JSONObject{}
+ if macAddresses, ok := server.macAddressesPerNetwork[networkName]; ok {
+ for _, macAddress := range macAddresses {
+ m, err := macAddress.GetMap()
+ checkError(err)
+ convertedMacAddresses = append(convertedMacAddresses, m)
+ }
+ }
+ res, err := json.MarshalIndent(convertedMacAddresses, "", " ")
+ checkError(err)
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprint(w, string(res))
+}
+
+// networksHandler handles requests for '/api/<version>/networks/?node=system_id'.
+func networksHandler(server *TestServer, w http.ResponseWriter, r *http.Request) {
+ if r.Method != "GET" {
+ panic("only networks GET operation implemented")
+ }
+ values, err := url.ParseQuery(r.URL.RawQuery)
+ checkError(err)
+ op := values.Get("op")
+ systemId := values.Get("node")
+ if op == "list_connected_macs" {
+ networkListConnectedMACSHandler(server, w, r)
+ return
+ }
+ if op != "" {
+ panic("only list_connected_macs and default operations implemented")
+ }
+ if systemId == "" {
+ panic("network missing associated node system id")
+ }
+ networks := []MAASObject{}
+ if networkNames, hasNetworks := server.networksPerNode[systemId]; hasNetworks {
+ networks = make([]MAASObject, len(networkNames))
+ for i, networkName := range networkNames {
+ networks[i] = server.networks[networkName]
+ }
+ }
+ res, err := json.MarshalIndent(networks, "", " ")
+ checkError(err)
+ w.Header().Set("Content-Type", "application/json; charset=utf-8")
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprint(w, string(res))
+}
+
+// ipAddressesHandler handles requests for '/api/<version>/ipaddresses/'.
+func ipAddressesHandler(server *TestServer, w http.ResponseWriter, r *http.Request) {
+ err := r.ParseForm()
+ checkError(err)
+ values := r.Form
+ op := values.Get("op")
+
+ switch r.Method {
+ case "GET":
+ if op != "" {
+ panic("expected empty op for GET, got " + op)
+ }
+ listIPAddressesHandler(server, w, r)
+ return
+ case "POST":
+ switch op {
+ case "reserve":
+ reserveIPAddressHandler(server, w, r, values.Get("network"), values.Get("requested_address"))
+ return
+ case "release":
+ releaseIPAddressHandler(server, w, r, values.Get("ip"))
+ return
+ default:
+ panic("expected op=release|reserve for POST, got " + op)
+ }
+ }
+ http.NotFoundHandler().ServeHTTP(w, r)
+}
+
+func marshalIPAddress(server *TestServer, ipAddress string) (JSONObject, error) {
+ jsonTemplate := `{"alloc_type": 4, "ip": %q, "resource_uri": %q, "created": %q}`
+ uri := getIPAddressesEndpoint(server.version)
+ now := time.Now().UTC().Format(time.RFC3339)
+ bytes := []byte(fmt.Sprintf(jsonTemplate, ipAddress, uri, now))
+ return Parse(server.client, bytes)
+}
+
+func badRequestError(w http.ResponseWriter, err error) {
+ w.WriteHeader(http.StatusBadRequest)
+ fmt.Fprint(w, err.Error())
+}
+
+func listIPAddressesHandler(server *TestServer, w http.ResponseWriter, r *http.Request) {
+ results := []MAASObject{}
+ for _, ips := range server.IPAddresses() {
+ for _, ip := range ips {
+ jsonObj, err := marshalIPAddress(server, ip)
+ if err != nil {
+ badRequestError(w, err)
+ return
+ }
+ maasObj, err := jsonObj.GetMAASObject()
+ if err != nil {
+ badRequestError(w, err)
+ return
+ }
+ results = append(results, maasObj)
+ }
+ }
+ res, err := json.MarshalIndent(results, "", " ")
+ checkError(err)
+ w.Header().Set("Content-Type", "application/json; charset=utf-8")
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprint(w, string(res))
+}
+
+func reserveIPAddressHandler(server *TestServer, w http.ResponseWriter, r *http.Request, network, reqAddress string) {
+ _, ipNet, err := net.ParseCIDR(network)
+ if err != nil {
+ badRequestError(w, fmt.Errorf("Invalid network parameter %s", network))
+ return
+ }
+ if reqAddress != "" {
+ // Validate "requested_address" parameter.
+ reqIP := net.ParseIP(reqAddress)
+ if reqIP == nil {
+ badRequestError(w, fmt.Errorf("failed to detect a valid IP address from u'%s'", reqAddress))
+ return
+ }
+ if !ipNet.Contains(reqIP) {
+ badRequestError(w, fmt.Errorf("%s is not inside the range %s", reqAddress, ipNet.String()))
+ return
+ }
+ }
+ // Find the network name matching the parsed CIDR.
+ foundNetworkName := ""
+ for netName, netObj := range server.networks {
+ // Get the "ip" and "netmask" attributes of the network.
+ netIP, err := netObj.GetField("ip")
+ checkError(err)
+ netMask, err := netObj.GetField("netmask")
+ checkError(err)
+
+ // Convert the netmask string to net.IPMask.
+ parts := strings.Split(netMask, ".")
+ ipMask := make(net.IPMask, len(parts))
+ for i, part := range parts {
+ intPart, err := strconv.Atoi(part)
+ checkError(err)
+ ipMask[i] = byte(intPart)
+ }
+ netNet := &net.IPNet{IP: net.ParseIP(netIP), Mask: ipMask}
+ if netNet.String() == network {
+ // Exact match found.
+ foundNetworkName = netName
+ break
+ }
+ }
+ if foundNetworkName == "" {
+ badRequestError(w, fmt.Errorf("No network found matching %s", network))
+ return
+ }
+ ips, found := server.ipAddressesPerNetwork[foundNetworkName]
+ if !found {
+ // This will be the first address.
+ ips = []string{}
+ }
+ reservedIP := ""
+ if reqAddress != "" {
+ // Use what the user provided. NOTE: Because this is testing
+ // code, no duplicates check is done.
+ reservedIP = reqAddress
+ } else {
+ // Generate an IP in the network range by incrementing the
+ // last byte of the network's IP.
+ firstIP := ipNet.IP
+ firstIP[len(firstIP)-1] += byte(len(ips) + 1)
+ reservedIP = firstIP.String()
+ }
+ ips = append(ips, reservedIP)
+ server.ipAddressesPerNetwork[foundNetworkName] = ips
+ jsonObj, err := marshalIPAddress(server, reservedIP)
+ checkError(err)
+ maasObj, err := jsonObj.GetMAASObject()
+ checkError(err)
+ res, err := json.MarshalIndent(maasObj, "", " ")
+ checkError(err)
+ w.Header().Set("Content-Type", "application/json; charset=utf-8")
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprint(w, string(res))
+}
+
+func releaseIPAddressHandler(server *TestServer, w http.ResponseWriter, r *http.Request, ip string) {
+ if netIP := net.ParseIP(ip); netIP == nil {
+ http.NotFoundHandler().ServeHTTP(w, r)
+ return
+ }
+ if server.RemoveIPAddress(ip) {
+ w.WriteHeader(http.StatusOK)
+ return
+ }
+ http.NotFoundHandler().ServeHTTP(w, r)
+}
+
+// versionHandler handles requests for '/api/<version>/version/'.
+func versionHandler(server *TestServer, w http.ResponseWriter, r *http.Request) {
+ if r.Method != "GET" {
+ panic("only version GET operation implemented")
+ }
+ w.Header().Set("Content-Type", "application/json; charset=utf-8")
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprint(w, server.versionJSON)
+}
+
+// nodegroupsHandler handles requests for '/api/<version>/nodegroups/*'.
+func nodegroupsHandler(server *TestServer, w http.ResponseWriter, r *http.Request) {
+ values, err := url.ParseQuery(r.URL.RawQuery)
+ checkError(err)
+ op := values.Get("op")
+ bootimagesURLRE := getBootimagesURLRE(server.version)
+ bootimagesURLMatch := bootimagesURLRE.FindStringSubmatch(r.URL.Path)
+ nodegroupsInterfacesURLRE := getNodegroupsInterfacesURLRE(server.version)
+ nodegroupsInterfacesURLMatch := nodegroupsInterfacesURLRE.FindStringSubmatch(r.URL.Path)
+ nodegroupsURL := getNodegroupsEndpoint(server.version)
+ switch {
+ case r.URL.Path == nodegroupsURL:
+ nodegroupsTopLevelHandler(server, w, r, op)
+ case bootimagesURLMatch != nil:
+ bootimagesHandler(server, w, r, bootimagesURLMatch[1], op)
+ case nodegroupsInterfacesURLMatch != nil:
+ nodegroupsInterfacesHandler(server, w, r, nodegroupsInterfacesURLMatch[1], op)
+ default:
+ // Default handler: not found.
+ http.NotFoundHandler().ServeHTTP(w, r)
+ }
+}
+
+// nodegroupsTopLevelHandler handles requests for '/api/<version>/nodegroups/'.
+func nodegroupsTopLevelHandler(server *TestServer, w http.ResponseWriter, r *http.Request, op string) {
+ if r.Method != "GET" || op != "list" {
+ w.WriteHeader(http.StatusBadRequest)
+ return
+ }
+
+ nodegroups := []JSONObject{}
+ for uuid := range server.bootImages {
+ attrs := map[string]interface{}{
+ "uuid": uuid,
+ resourceURI: getNodegroupURL(server.version, uuid),
+ }
+ obj := maasify(server.client, attrs)
+ nodegroups = append(nodegroups, obj)
+ }
+
+ res, err := json.MarshalIndent(nodegroups, "", " ")
+ checkError(err)
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprint(w, string(res))
+}
+
+// bootimagesHandler handles requests for '/api/<version>/nodegroups/<uuid>/boot-images/'.
+func bootimagesHandler(server *TestServer, w http.ResponseWriter, r *http.Request, nodegroupUUID, op string) {
+ if r.Method != "GET" {
+ w.WriteHeader(http.StatusBadRequest)
+ return
+ }
+
+ bootImages, ok := server.bootImages[nodegroupUUID]
+ if !ok {
+ http.NotFoundHandler().ServeHTTP(w, r)
+ return
+ }
+
+ res, err := json.MarshalIndent(bootImages, "", " ")
+ checkError(err)
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprint(w, string(res))
+}
+
+// nodegroupsInterfacesHandler handles requests for '/api/<version>/nodegroups/<uuid>/interfaces/'
+func nodegroupsInterfacesHandler(server *TestServer, w http.ResponseWriter, r *http.Request, nodegroupUUID, op string) {
+ if r.Method != "GET" {
+ w.WriteHeader(http.StatusBadRequest)
+ return
+ }
+ _, ok := server.bootImages[nodegroupUUID]
+ if !ok {
+ http.NotFoundHandler().ServeHTTP(w, r)
+ return
+ }
+
+ interfaces, ok := server.nodegroupsInterfaces[nodegroupUUID]
+ if !ok {
+ // we already checked the nodegroup exists, so return an empty list
+ interfaces = []JSONObject{}
+ }
+ res, err := json.MarshalIndent(interfaces, "", " ")
+ checkError(err)
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprint(w, string(res))
+}
+
+// zonesHandler handles requests for '/api/<version>/zones/'.
+func zonesHandler(server *TestServer, w http.ResponseWriter, r *http.Request) {
+ if r.Method != "GET" {
+ w.WriteHeader(http.StatusBadRequest)
+ return
+ }
+
+ if len(server.zones) == 0 {
+ // Until a zone is registered, behave as if the endpoint
+ // does not exist. This way we can simulate older MAAS
+ // servers that do not support zones.
+ http.NotFoundHandler().ServeHTTP(w, r)
+ return
+ }
+
+ zones := make([]JSONObject, 0, len(server.zones))
+ for _, zone := range server.zones {
+ zones = append(zones, zone)
+ }
+ res, err := json.MarshalIndent(zones, "", " ")
+ checkError(err)
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprint(w, string(res))
+}
diff --git a/automation/vendor/github.com/juju/gomaasapi/testservice_spaces.go b/automation/vendor/github.com/juju/gomaasapi/testservice_spaces.go
new file mode 100644
index 0000000..c6c1617
--- /dev/null
+++ b/automation/vendor/github.com/juju/gomaasapi/testservice_spaces.go
@@ -0,0 +1,132 @@
+// Copyright 2012-2016 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+package gomaasapi
+
+import (
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+ "regexp"
+)
+
+func getSpacesEndpoint(version string) string {
+ return fmt.Sprintf("/api/%s/spaces/", version)
+}
+
+// TestSpace is the MAAS API space representation
+type TestSpace struct {
+ Name string `json:"name"`
+ Subnets []TestSubnet `json:"subnets"`
+ ResourceURI string `json:"resource_uri"`
+ ID uint `json:"id"`
+}
+
+// spacesHandler handles requests for '/api/<version>/spaces/'.
+func spacesHandler(server *TestServer, w http.ResponseWriter, r *http.Request) {
+ values, err := url.ParseQuery(r.URL.RawQuery)
+ checkError(err)
+ op := values.Get("op")
+ if op != "" {
+ w.WriteHeader(http.StatusBadRequest)
+ return
+ }
+
+ spacesURLRE := regexp.MustCompile(`/spaces/(.+?)/`)
+ spacesURLMatch := spacesURLRE.FindStringSubmatch(r.URL.Path)
+ spacesURL := getSpacesEndpoint(server.version)
+
+ var ID uint
+ var gotID bool
+ if spacesURLMatch != nil {
+ ID, err = NameOrIDToID(spacesURLMatch[1], server.spaceNameToID, 1, uint(len(server.spaces)))
+
+ if err != nil {
+ http.NotFoundHandler().ServeHTTP(w, r)
+ return
+ }
+
+ gotID = true
+ }
+
+ switch r.Method {
+ case "GET":
+ w.Header().Set("Content-Type", "application/vnd.api+json")
+ if len(server.spaces) == 0 {
+ // Until a space is registered, behave as if the endpoint
+ // does not exist. This way we can simulate older MAAS
+ // servers that do not support spaces.
+ http.NotFoundHandler().ServeHTTP(w, r)
+ return
+ }
+
+ if r.URL.Path == spacesURL {
+ var spaces []*TestSpace
+ // Iterating by id rather than a dictionary iteration
+ // preserves the order of the spaces in the result.
+ for i := uint(1); i < server.nextSpace; i++ {
+ s, ok := server.spaces[i]
+ if ok {
+ server.setSubnetsOnSpace(s)
+ spaces = append(spaces, s)
+ }
+ }
+ err = json.NewEncoder(w).Encode(spaces)
+ } else if gotID == false {
+ w.WriteHeader(http.StatusBadRequest)
+ } else {
+ err = json.NewEncoder(w).Encode(server.spaces[ID])
+ }
+ checkError(err)
+ case "POST":
+ //server.NewSpace(r.Body)
+ case "PUT":
+ //server.UpdateSpace(r.Body)
+ case "DELETE":
+ delete(server.spaces, ID)
+ w.WriteHeader(http.StatusOK)
+ default:
+ w.WriteHeader(http.StatusBadRequest)
+ }
+}
+
+// CreateSpace is used to create new spaces on the server.
+type CreateSpace struct {
+ Name string `json:"name"`
+}
+
+func decodePostedSpace(spaceJSON io.Reader) CreateSpace {
+ var postedSpace CreateSpace
+ decoder := json.NewDecoder(spaceJSON)
+ err := decoder.Decode(&postedSpace)
+ checkError(err)
+ return postedSpace
+}
+
+// NewSpace creates a space in the test server
+func (server *TestServer) NewSpace(spaceJSON io.Reader) *TestSpace {
+ postedSpace := decodePostedSpace(spaceJSON)
+ newSpace := &TestSpace{Name: postedSpace.Name}
+ newSpace.ID = server.nextSpace
+ newSpace.ResourceURI = fmt.Sprintf("/api/%s/spaces/%d/", server.version, int(server.nextSpace))
+ server.spaces[server.nextSpace] = newSpace
+ server.spaceNameToID[newSpace.Name] = newSpace.ID
+
+ server.nextSpace++
+ return newSpace
+}
+
+// setSubnetsOnSpace fetches the subnets for the specified space and adds them
+// to it.
+func (server *TestServer) setSubnetsOnSpace(space *TestSpace) {
+ subnets := []TestSubnet{}
+ for i := uint(1); i < server.nextSubnet; i++ {
+ subnet, ok := server.subnets[i]
+ if ok && subnet.Space == space.Name {
+ subnets = append(subnets, subnet)
+ }
+ }
+ space.Subnets = subnets
+}
diff --git a/automation/vendor/github.com/juju/gomaasapi/testservice_subnets.go b/automation/vendor/github.com/juju/gomaasapi/testservice_subnets.go
new file mode 100644
index 0000000..5438669
--- /dev/null
+++ b/automation/vendor/github.com/juju/gomaasapi/testservice_subnets.go
@@ -0,0 +1,396 @@
+// Copyright 2012-2016 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+package gomaasapi
+
+import (
+ "encoding/json"
+ "fmt"
+ "io"
+ "net"
+ "net/http"
+ "net/url"
+ "regexp"
+ "sort"
+ "strings"
+)
+
+func getSubnetsEndpoint(version string) string {
+ return fmt.Sprintf("/api/%s/subnets/", version)
+}
+
+// CreateSubnet is used to receive new subnets via the MAAS API
+type CreateSubnet struct {
+ DNSServers []string `json:"dns_servers"`
+ Name string `json:"name"`
+ Space string `json:"space"`
+ GatewayIP string `json:"gateway_ip"`
+ CIDR string `json:"cidr"`
+
+ // VLAN this subnet belongs to. Currently ignored.
+ // TODO: Defaults to the default VLAN
+ // for the provided fabric or defaults to the default VLAN
+ // in the default fabric.
+ VLAN *uint `json:"vlan"`
+
+ // Fabric for the subnet. Currently ignored.
+ // TODO: Defaults to the fabric the provided
+ // VLAN belongs to or defaults to the default fabric.
+ Fabric *uint `json:"fabric"`
+
+ // VID of the VLAN this subnet belongs to. Currently ignored.
+ // TODO: Only used when vlan
+ // is not provided. Picks the VLAN with this VID in the provided
+ // fabric or the default fabric if one is not given.
+ VID *uint `json:"vid"`
+
+ // This is used for updates (PUT) and is ignored by create (POST)
+ ID uint `json:"id"`
+}
+
+// TestSubnet is the MAAS API subnet representation
+type TestSubnet struct {
+ DNSServers []string `json:"dns_servers"`
+ Name string `json:"name"`
+ Space string `json:"space"`
+ VLAN TestVLAN `json:"vlan"`
+ GatewayIP string `json:"gateway_ip"`
+ CIDR string `json:"cidr"`
+
+ ResourceURI string `json:"resource_uri"`
+ ID uint `json:"id"`
+ InUseIPAddresses []IP `json:"-"`
+ FixedAddressRanges []AddressRange `json:"-"`
+}
+
+// AddFixedAddressRange adds an AddressRange to the list of fixed address ranges
+// that subnet stores.
+func (server *TestServer) AddFixedAddressRange(subnetID uint, ar AddressRange) {
+ subnet := server.subnets[subnetID]
+ ar.startUint = IPFromString(ar.Start).UInt64()
+ ar.endUint = IPFromString(ar.End).UInt64()
+ subnet.FixedAddressRanges = append(subnet.FixedAddressRanges, ar)
+ server.subnets[subnetID] = subnet
+}
+
+// subnetsHandler handles requests for '/api/<version>/subnets/'.
+func subnetsHandler(server *TestServer, w http.ResponseWriter, r *http.Request) {
+ var err error
+ values, err := url.ParseQuery(r.URL.RawQuery)
+ checkError(err)
+ op := values.Get("op")
+ includeRangesString := strings.ToLower(values.Get("include_ranges"))
+ subnetsURLRE := regexp.MustCompile(`/subnets/(.+?)/`)
+ subnetsURLMatch := subnetsURLRE.FindStringSubmatch(r.URL.Path)
+ subnetsURL := getSubnetsEndpoint(server.version)
+
+ var ID uint
+ var gotID bool
+ if subnetsURLMatch != nil {
+ ID, err = NameOrIDToID(subnetsURLMatch[1], server.subnetNameToID, 1, uint(len(server.subnets)))
+
+ if err != nil {
+ http.NotFoundHandler().ServeHTTP(w, r)
+ return
+ }
+
+ gotID = true
+ }
+
+ var includeRanges bool
+ switch includeRangesString {
+ case "true", "yes", "1":
+ includeRanges = true
+ }
+
+ switch r.Method {
+ case "GET":
+ w.Header().Set("Content-Type", "application/vnd.api+json")
+ if len(server.subnets) == 0 {
+ // Until a subnet is registered, behave as if the endpoint
+ // does not exist. This way we can simulate older MAAS
+ // servers that do not support subnets.
+ http.NotFoundHandler().ServeHTTP(w, r)
+ return
+ }
+
+ if r.URL.Path == subnetsURL {
+ var subnets []TestSubnet
+ for i := uint(1); i < server.nextSubnet; i++ {
+ s, ok := server.subnets[i]
+ if ok {
+ subnets = append(subnets, s)
+ }
+ }
+ PrettyJsonWriter(subnets, w)
+ } else if gotID == false {
+ w.WriteHeader(http.StatusBadRequest)
+ } else {
+ switch op {
+ case "unreserved_ip_ranges":
+ PrettyJsonWriter(server.subnetUnreservedIPRanges(server.subnets[ID]), w)
+ case "reserved_ip_ranges":
+ PrettyJsonWriter(server.subnetReservedIPRanges(server.subnets[ID]), w)
+ case "statistics":
+ PrettyJsonWriter(server.subnetStatistics(server.subnets[ID], includeRanges), w)
+ default:
+ PrettyJsonWriter(server.subnets[ID], w)
+ }
+ }
+ checkError(err)
+ case "POST":
+ server.NewSubnet(r.Body)
+ case "PUT":
+ server.UpdateSubnet(r.Body)
+ case "DELETE":
+ delete(server.subnets, ID)
+ w.WriteHeader(http.StatusOK)
+ default:
+ w.WriteHeader(http.StatusBadRequest)
+ }
+}
+
+type addressList []IP
+
+func (a addressList) Len() int { return len(a) }
+func (a addressList) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
+func (a addressList) Less(i, j int) bool { return a[i].UInt64() < a[j].UInt64() }
+
+// AddressRange is used to generate reserved IP address range lists
+type AddressRange struct {
+ Start string `json:"start"`
+ startUint uint64
+ End string `json:"end"`
+ endUint uint64
+ Purpose []string `json:"purpose,omitempty"`
+ NumAddresses uint `json:"num_addresses"`
+}
+
+// AddressRangeList is a list of AddressRange
+type AddressRangeList struct {
+ ar []AddressRange
+}
+
+// Append appends a new AddressRange to an AddressRangeList
+func (ranges *AddressRangeList) Append(startIP, endIP IP) {
+ var i AddressRange
+ i.Start, i.End = startIP.String(), endIP.String()
+ i.startUint, i.endUint = startIP.UInt64(), endIP.UInt64()
+ i.NumAddresses = uint(1 + endIP.UInt64() - startIP.UInt64())
+ i.Purpose = startIP.Purpose
+ ranges.ar = append(ranges.ar, i)
+}
+
+func appendRangesToIPList(subnet TestSubnet, ipAddresses *[]IP) {
+ for _, r := range subnet.FixedAddressRanges {
+ for v := r.startUint; v <= r.endUint; v++ {
+ ip := IPFromInt64(v)
+ ip.Purpose = r.Purpose
+ *ipAddresses = append(*ipAddresses, ip)
+ }
+ }
+}
+
+func (server *TestServer) subnetUnreservedIPRanges(subnet TestSubnet) []AddressRange {
+ // Make a sorted copy of subnet.InUseIPAddresses
+ ipAddresses := make([]IP, len(subnet.InUseIPAddresses))
+ copy(ipAddresses, subnet.InUseIPAddresses)
+ appendRangesToIPList(subnet, &ipAddresses)
+ sort.Sort(addressList(ipAddresses))
+
+ // We need the first and last address in the subnet
+ var ranges AddressRangeList
+ var startIP, endIP, lastUsableIP IP
+
+ _, ipNet, err := net.ParseCIDR(subnet.CIDR)
+ checkError(err)
+ startIP = IPFromNetIP(ipNet.IP)
+ // Start with the lowest usable address in the range, which is 1 above
+ // what net.ParseCIDR will give back.
+ startIP.SetUInt64(startIP.UInt64() + 1)
+
+ ones, bits := ipNet.Mask.Size()
+ set := ^((^uint64(0)) << uint(bits-ones))
+
+ // The last usable address is one below the broadcast address, which is
+ // what you get by bitwise ORing 'set' with any IP address in the subnet.
+ lastUsableIP.SetUInt64((startIP.UInt64() | set) - 1)
+
+ for _, endIP = range ipAddresses {
+ end := endIP.UInt64()
+
+ if endIP.UInt64() == startIP.UInt64() {
+ if endIP.UInt64() != lastUsableIP.UInt64() {
+ startIP.SetUInt64(end + 1)
+ }
+ continue
+ }
+
+ if end == lastUsableIP.UInt64() {
+ continue
+ }
+
+ ranges.Append(startIP, IPFromInt64(end-1))
+ startIP.SetUInt64(end + 1)
+ }
+
+ if startIP.UInt64() != lastUsableIP.UInt64() {
+ ranges.Append(startIP, lastUsableIP)
+ }
+
+ return ranges.ar
+}
+
+func (server *TestServer) subnetReservedIPRanges(subnet TestSubnet) []AddressRange {
+ var ranges AddressRangeList
+ var startIP, thisIP IP
+
+ // Make a sorted copy of subnet.InUseIPAddresses
+ ipAddresses := make([]IP, len(subnet.InUseIPAddresses))
+ copy(ipAddresses, subnet.InUseIPAddresses)
+ appendRangesToIPList(subnet, &ipAddresses)
+ sort.Sort(addressList(ipAddresses))
+ if len(ipAddresses) == 0 {
+ ar := ranges.ar
+ if ar == nil {
+ ar = []AddressRange{}
+ }
+ return ar
+ }
+
+ startIP = ipAddresses[0]
+ lastIP := ipAddresses[0]
+ for _, thisIP = range ipAddresses {
+ var purposeMissmatch bool
+ for i, p := range thisIP.Purpose {
+ if startIP.Purpose[i] != p {
+ purposeMissmatch = true
+ }
+ }
+ if (thisIP.UInt64() != lastIP.UInt64() && thisIP.UInt64() != lastIP.UInt64()+1) || purposeMissmatch {
+ ranges.Append(startIP, lastIP)
+ startIP = thisIP
+ }
+ lastIP = thisIP
+ }
+
+ if len(ranges.ar) == 0 || ranges.ar[len(ranges.ar)-1].endUint != lastIP.UInt64() {
+ ranges.Append(startIP, lastIP)
+ }
+
+ return ranges.ar
+}
+
+// SubnetStats holds statistics about a subnet
+type SubnetStats struct {
+ NumAvailable uint `json:"num_available"`
+ LargestAvailable uint `json:"largest_available"`
+ NumUnavailable uint `json:"num_unavailable"`
+ TotalAddresses uint `json:"total_addresses"`
+ Usage float32 `json:"usage"`
+ UsageString string `json:"usage_string"`
+ Ranges []AddressRange `json:"ranges"`
+}
+
+func (server *TestServer) subnetStatistics(subnet TestSubnet, includeRanges bool) SubnetStats {
+ var stats SubnetStats
+ _, ipNet, err := net.ParseCIDR(subnet.CIDR)
+ checkError(err)
+
+ ones, bits := ipNet.Mask.Size()
+ stats.TotalAddresses = (1 << uint(bits-ones)) - 2
+ stats.NumUnavailable = uint(len(subnet.InUseIPAddresses))
+ stats.NumAvailable = stats.TotalAddresses - stats.NumUnavailable
+ stats.Usage = float32(stats.NumUnavailable) / float32(stats.TotalAddresses)
+ stats.UsageString = fmt.Sprintf("%0.1f%%", stats.Usage*100)
+
+ // Calculate stats.LargestAvailable - the largest contiguous block of IP addresses available
+ reserved := server.subnetUnreservedIPRanges(subnet)
+ for _, addressRange := range reserved {
+ if addressRange.NumAddresses > stats.LargestAvailable {
+ stats.LargestAvailable = addressRange.NumAddresses
+ }
+ }
+
+ if includeRanges {
+ stats.Ranges = reserved
+ }
+
+ return stats
+}
+
+func decodePostedSubnet(subnetJSON io.Reader) CreateSubnet {
+ var postedSubnet CreateSubnet
+ decoder := json.NewDecoder(subnetJSON)
+ err := decoder.Decode(&postedSubnet)
+ checkError(err)
+ if postedSubnet.DNSServers == nil {
+ postedSubnet.DNSServers = []string{}
+ }
+ return postedSubnet
+}
+
+// UpdateSubnet creates a subnet in the test server
+func (server *TestServer) UpdateSubnet(subnetJSON io.Reader) TestSubnet {
+ postedSubnet := decodePostedSubnet(subnetJSON)
+ updatedSubnet := subnetFromCreateSubnet(postedSubnet)
+ server.subnets[updatedSubnet.ID] = updatedSubnet
+ return updatedSubnet
+}
+
+// NewSubnet creates a subnet in the test server
+func (server *TestServer) NewSubnet(subnetJSON io.Reader) *TestSubnet {
+ postedSubnet := decodePostedSubnet(subnetJSON)
+ newSubnet := subnetFromCreateSubnet(postedSubnet)
+ newSubnet.ID = server.nextSubnet
+ server.subnets[server.nextSubnet] = newSubnet
+ server.subnetNameToID[newSubnet.Name] = newSubnet.ID
+
+ server.nextSubnet++
+ return &newSubnet
+}
+
+// NodeNetworkInterface represents a network interface attached to a node
+type NodeNetworkInterface struct {
+ Name string `json:"name"`
+ Links []NetworkLink `json:"links"`
+}
+
+// Node represents a node
+type Node struct {
+ SystemID string `json:"system_id"`
+ Interfaces []NodeNetworkInterface `json:"interface_set"`
+}
+
+// NetworkLink represents a MAAS network link
+type NetworkLink struct {
+ ID uint `json:"id"`
+ Mode string `json:"mode"`
+ Subnet *TestSubnet `json:"subnet"`
+}
+
+// SetNodeNetworkLink records that the given node + interface are in subnet
+func (server *TestServer) SetNodeNetworkLink(SystemID string, nodeNetworkInterface NodeNetworkInterface) {
+ for i, ni := range server.nodeMetadata[SystemID].Interfaces {
+ if ni.Name == nodeNetworkInterface.Name {
+ server.nodeMetadata[SystemID].Interfaces[i] = nodeNetworkInterface
+ return
+ }
+ }
+ n := server.nodeMetadata[SystemID]
+ n.Interfaces = append(n.Interfaces, nodeNetworkInterface)
+ server.nodeMetadata[SystemID] = n
+}
+
+// subnetFromCreateSubnet creates a subnet in the test server
+func subnetFromCreateSubnet(postedSubnet CreateSubnet) TestSubnet {
+ var newSubnet TestSubnet
+ newSubnet.DNSServers = postedSubnet.DNSServers
+ newSubnet.Name = postedSubnet.Name
+ newSubnet.Space = postedSubnet.Space
+ //TODO: newSubnet.VLAN = server.postedSubnetVLAN
+ newSubnet.GatewayIP = postedSubnet.GatewayIP
+ newSubnet.CIDR = postedSubnet.CIDR
+ newSubnet.ID = postedSubnet.ID
+ return newSubnet
+}
diff --git a/automation/vendor/github.com/juju/gomaasapi/testservice_utils.go b/automation/vendor/github.com/juju/gomaasapi/testservice_utils.go
new file mode 100644
index 0000000..8f941f1
--- /dev/null
+++ b/automation/vendor/github.com/juju/gomaasapi/testservice_utils.go
@@ -0,0 +1,119 @@
+// Copyright 2012-2016 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+package gomaasapi
+
+import (
+ "bytes"
+ "encoding/binary"
+ "encoding/json"
+ "errors"
+ "net"
+ "net/http"
+ "strconv"
+)
+
+// NameOrIDToID takes a string that contains eiter an integer ID or the
+// name of a thing. It returns the integer ID contained or mapped to or panics.
+func NameOrIDToID(v string, nameToID map[string]uint, minID, maxID uint) (ID uint, err error) {
+ ID, ok := nameToID[v]
+ if !ok {
+ intID, err := strconv.Atoi(v)
+ if err != nil {
+ return 0, err
+ }
+ ID = uint(intID)
+ }
+
+ if ID < minID || ID > maxID {
+ return 0, errors.New("ID out of range")
+ }
+
+ return ID, nil
+}
+
+// IP is an enhanced net.IP
+type IP struct {
+ netIP net.IP
+ Purpose []string
+}
+
+// IPFromNetIP creates a IP from a net.IP.
+func IPFromNetIP(netIP net.IP) IP {
+ var ip IP
+ ip.netIP = netIP
+ return ip
+}
+
+// IPFromString creates a new IP from a string IP address representation
+func IPFromString(v string) IP {
+ return IPFromNetIP(net.ParseIP(v))
+}
+
+// IPFromInt64 creates a new IP from a uint64 IP address representation
+func IPFromInt64(v uint64) IP {
+ var ip IP
+ ip.SetUInt64(v)
+ return ip
+}
+
+// To4 converts the IPv4 address ip to a 4-byte representation. If ip is not
+// an IPv4 address, To4 returns nil.
+func (ip IP) To4() net.IP {
+ return ip.netIP.To4()
+}
+
+// To16 converts the IP address ip to a 16-byte representation. If ip is not
+// an IP address (it is the wrong length), To16 returns nil.
+func (ip IP) To16() net.IP {
+ return ip.netIP.To16()
+}
+
+func (ip IP) String() string {
+ return ip.netIP.String()
+}
+
+// UInt64 returns a uint64 holding the IP address
+func (ip IP) UInt64() uint64 {
+ if len(ip.netIP) == 0 {
+ return uint64(0)
+ }
+
+ if ip.To4() != nil {
+ return uint64(binary.BigEndian.Uint32([]byte(ip.To4())))
+ }
+
+ return binary.BigEndian.Uint64([]byte(ip.To16()))
+}
+
+// SetUInt64 sets the IP value to v
+func (ip *IP) SetUInt64(v uint64) {
+ if len(ip.netIP) == 0 {
+ // If we don't have allocated storage make an educated guess
+ // at if the address we received is an IPv4 or IPv6 address.
+ if v == (v & 0x00000000ffffFFFF) {
+ // Guessing IPv4
+ ip.netIP = net.ParseIP("0.0.0.0")
+ } else {
+ ip.netIP = net.ParseIP("2001:4860:0:2001::68")
+ }
+ }
+
+ bb := new(bytes.Buffer)
+ var first int
+ if ip.To4() != nil {
+ binary.Write(bb, binary.BigEndian, uint32(v))
+ first = len(ip.netIP) - 4
+ } else {
+ binary.Write(bb, binary.BigEndian, v)
+ }
+ copy(ip.netIP[first:], bb.Bytes())
+}
+
+func PrettyJsonWriter(thing interface{}, w http.ResponseWriter) {
+ var out bytes.Buffer
+ b, err := json.MarshalIndent(thing, "", " ")
+ checkError(err)
+ out.Write(b)
+ out.WriteTo(w)
+}
diff --git a/automation/vendor/github.com/juju/gomaasapi/testservice_vlan.go b/automation/vendor/github.com/juju/gomaasapi/testservice_vlan.go
new file mode 100644
index 0000000..e81eaaa
--- /dev/null
+++ b/automation/vendor/github.com/juju/gomaasapi/testservice_vlan.go
@@ -0,0 +1,33 @@
+// Copyright 2012-2016 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+package gomaasapi
+
+import (
+ "fmt"
+ "net/http"
+)
+
+func getVLANsEndpoint(version string) string {
+ return fmt.Sprintf("/api/%s/vlans/", version)
+}
+
+// TestVLAN is the MAAS API VLAN representation
+type TestVLAN struct {
+ Name string `json:"name"`
+ Fabric string `json:"fabric"`
+ VID uint `json:"vid"`
+
+ ResourceURI string `json:"resource_uri"`
+ ID uint `json:"id"`
+}
+
+// PostedVLAN is the MAAS API posted VLAN representation
+type PostedVLAN struct {
+ Name string `json:"name"`
+ VID uint `json:"vid"`
+}
+
+func vlansHandler(server *TestServer, w http.ResponseWriter, r *http.Request) {
+ //TODO
+}
diff --git a/automation/vendor/github.com/juju/gomaasapi/urlparams.go b/automation/vendor/github.com/juju/gomaasapi/urlparams.go
new file mode 100644
index 0000000..a6bab6e
--- /dev/null
+++ b/automation/vendor/github.com/juju/gomaasapi/urlparams.go
@@ -0,0 +1,48 @@
+// Copyright 2016 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+package gomaasapi
+
+import (
+ "fmt"
+ "net/url"
+)
+
+// URLParams wraps url.Values to easily add values, but skipping empty ones.
+type URLParams struct {
+ Values url.Values
+}
+
+// NewURLParams allocates a new URLParams type.
+func NewURLParams() *URLParams {
+ return &URLParams{Values: make(url.Values)}
+}
+
+// MaybeAdd adds the (name, value) pair iff value is not empty.
+func (p *URLParams) MaybeAdd(name, value string) {
+ if value != "" {
+ p.Values.Add(name, value)
+ }
+}
+
+// MaybeAddInt adds the (name, value) pair iff value is not zero.
+func (p *URLParams) MaybeAddInt(name string, value int) {
+ if value != 0 {
+ p.Values.Add(name, fmt.Sprint(value))
+ }
+}
+
+// MaybeAddBool adds the (name, value) pair iff value is true.
+func (p *URLParams) MaybeAddBool(name string, value bool) {
+ if value {
+ p.Values.Add(name, fmt.Sprint(value))
+ }
+}
+
+// MaybeAddMany adds the (name, value) for each value in values iff
+// value is not empty.
+func (p *URLParams) MaybeAddMany(name string, values []string) {
+ for _, value := range values {
+ p.MaybeAdd(name, value)
+ }
+}
diff --git a/automation/vendor/github.com/juju/gomaasapi/util.go b/automation/vendor/github.com/juju/gomaasapi/util.go
new file mode 100644
index 0000000..3f95ac9
--- /dev/null
+++ b/automation/vendor/github.com/juju/gomaasapi/util.go
@@ -0,0 +1,27 @@
+// Copyright 2012-2016 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+package gomaasapi
+
+import (
+ "strings"
+)
+
+// JoinURLs joins a base URL and a subpath together.
+// Regardless of whether baseURL ends in a trailing slash (or even multiple
+// trailing slashes), or whether there are any leading slashes at the begining
+// of path, the two will always be joined together by a single slash.
+func JoinURLs(baseURL, path string) string {
+ return strings.TrimRight(baseURL, "/") + "/" + strings.TrimLeft(path, "/")
+}
+
+// EnsureTrailingSlash appends a slash at the end of the given string unless
+// there already is one.
+// This is used to create the kind of normalized URLs that Django expects.
+// (to avoid Django's redirection when an URL does not ends with a slash.)
+func EnsureTrailingSlash(URL string) string {
+ if strings.HasSuffix(URL, "/") {
+ return URL
+ }
+ return URL + "/"
+}
diff --git a/automation/vendor/github.com/juju/gomaasapi/vlan.go b/automation/vendor/github.com/juju/gomaasapi/vlan.go
new file mode 100644
index 0000000..c509d42
--- /dev/null
+++ b/automation/vendor/github.com/juju/gomaasapi/vlan.go
@@ -0,0 +1,154 @@
+// Copyright 2016 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+package gomaasapi
+
+import (
+ "github.com/juju/errors"
+ "github.com/juju/schema"
+ "github.com/juju/version"
+)
+
+type vlan struct {
+ // Add the controller in when we need to do things with the vlan.
+ // controller Controller
+
+ resourceURI string
+
+ id int
+ name string
+ fabric string
+
+ vid int
+ mtu int
+ dhcp bool
+
+ primaryRack string
+ secondaryRack string
+}
+
+// ID implements VLAN.
+func (v *vlan) ID() int {
+ return v.id
+}
+
+// Name implements VLAN.
+func (v *vlan) Name() string {
+ return v.name
+}
+
+// Fabric implements VLAN.
+func (v *vlan) Fabric() string {
+ return v.fabric
+}
+
+// VID implements VLAN.
+func (v *vlan) VID() int {
+ return v.vid
+}
+
+// MTU implements VLAN.
+func (v *vlan) MTU() int {
+ return v.mtu
+}
+
+// DHCP implements VLAN.
+func (v *vlan) DHCP() bool {
+ return v.dhcp
+}
+
+// PrimaryRack implements VLAN.
+func (v *vlan) PrimaryRack() string {
+ return v.primaryRack
+}
+
+// SecondaryRack implements VLAN.
+func (v *vlan) SecondaryRack() string {
+ return v.secondaryRack
+}
+
+func readVLANs(controllerVersion version.Number, source interface{}) ([]*vlan, error) {
+ checker := schema.List(schema.StringMap(schema.Any()))
+ coerced, err := checker.Coerce(source, nil)
+ if err != nil {
+ return nil, errors.Annotatef(err, "vlan base schema check failed")
+ }
+ valid := coerced.([]interface{})
+
+ var deserialisationVersion version.Number
+ for v := range vlanDeserializationFuncs {
+ if v.Compare(deserialisationVersion) > 0 && v.Compare(controllerVersion) <= 0 {
+ deserialisationVersion = v
+ }
+ }
+ if deserialisationVersion == version.Zero {
+ return nil, errors.Errorf("no vlan read func for version %s", controllerVersion)
+ }
+ readFunc := vlanDeserializationFuncs[deserialisationVersion]
+ return readVLANList(valid, readFunc)
+}
+
+func readVLANList(sourceList []interface{}, readFunc vlanDeserializationFunc) ([]*vlan, error) {
+ result := make([]*vlan, 0, len(sourceList))
+ for i, value := range sourceList {
+ source, ok := value.(map[string]interface{})
+ if !ok {
+ return nil, errors.Errorf("unexpected value for vlan %d, %T", i, value)
+ }
+ vlan, err := readFunc(source)
+ if err != nil {
+ return nil, errors.Annotatef(err, "vlan %d", i)
+ }
+ result = append(result, vlan)
+ }
+ return result, nil
+}
+
+type vlanDeserializationFunc func(map[string]interface{}) (*vlan, error)
+
+var vlanDeserializationFuncs = map[version.Number]vlanDeserializationFunc{
+ twoDotOh: vlan_2_0,
+}
+
+func vlan_2_0(source map[string]interface{}) (*vlan, error) {
+ fields := schema.Fields{
+ "id": schema.ForceInt(),
+ "resource_uri": schema.String(),
+ "name": schema.OneOf(schema.Nil(""), schema.String()),
+ "fabric": schema.String(),
+ "vid": schema.ForceInt(),
+ "mtu": schema.ForceInt(),
+ "dhcp_on": schema.Bool(),
+ // racks are not always set.
+ "primary_rack": schema.OneOf(schema.Nil(""), schema.String()),
+ "secondary_rack": schema.OneOf(schema.Nil(""), schema.String()),
+ }
+ checker := schema.FieldMap(fields, nil)
+ coerced, err := checker.Coerce(source, nil)
+ if err != nil {
+ return nil, errors.Annotatef(err, "vlan 2.0 schema check failed")
+ }
+ valid := coerced.(map[string]interface{})
+ // From here we know that the map returned from the schema coercion
+ // contains fields of the right type.
+
+ // Since the primary and secondary racks are optional, we use the two
+ // part cast assignment. If the case fails, then we get the default value
+ // we care about, which is the empty string.
+ primary_rack, _ := valid["primary_rack"].(string)
+ secondary_rack, _ := valid["secondary_rack"].(string)
+ name, _ := valid["name"].(string)
+
+ result := &vlan{
+ resourceURI: valid["resource_uri"].(string),
+ id: valid["id"].(int),
+ name: name,
+ fabric: valid["fabric"].(string),
+ vid: valid["vid"].(int),
+ mtu: valid["mtu"].(int),
+ dhcp: valid["dhcp_on"].(bool),
+ primaryRack: primary_rack,
+ secondaryRack: secondary_rack,
+ }
+ return result, nil
+}
diff --git a/automation/vendor/github.com/juju/gomaasapi/zone.go b/automation/vendor/github.com/juju/gomaasapi/zone.go
new file mode 100644
index 0000000..6f10cb4
--- /dev/null
+++ b/automation/vendor/github.com/juju/gomaasapi/zone.go
@@ -0,0 +1,97 @@
+// Copyright 2016 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+package gomaasapi
+
+import (
+ "github.com/juju/errors"
+ "github.com/juju/schema"
+ "github.com/juju/version"
+)
+
+type zone struct {
+ // Add the controller in when we need to do things with the zone.
+ // controller Controller
+
+ resourceURI string
+
+ name string
+ description string
+}
+
+// Name implements Zone.
+func (z *zone) Name() string {
+ return z.name
+}
+
+// Description implements Zone.
+func (z *zone) Description() string {
+ return z.description
+}
+
+func readZones(controllerVersion version.Number, source interface{}) ([]*zone, error) {
+ checker := schema.List(schema.StringMap(schema.Any()))
+ coerced, err := checker.Coerce(source, nil)
+ if err != nil {
+ return nil, errors.Annotatef(err, "zone base schema check failed")
+ }
+ valid := coerced.([]interface{})
+
+ var deserialisationVersion version.Number
+ for v := range zoneDeserializationFuncs {
+ if v.Compare(deserialisationVersion) > 0 && v.Compare(controllerVersion) <= 0 {
+ deserialisationVersion = v
+ }
+ }
+ if deserialisationVersion == version.Zero {
+ return nil, errors.Errorf("no zone read func for version %s", controllerVersion)
+ }
+ readFunc := zoneDeserializationFuncs[deserialisationVersion]
+ return readZoneList(valid, readFunc)
+}
+
+// readZoneList expects the values of the sourceList to be string maps.
+func readZoneList(sourceList []interface{}, readFunc zoneDeserializationFunc) ([]*zone, error) {
+ result := make([]*zone, 0, len(sourceList))
+ for i, value := range sourceList {
+ source, ok := value.(map[string]interface{})
+ if !ok {
+ return nil, errors.Errorf("unexpected value for zone %d, %T", i, value)
+ }
+ zone, err := readFunc(source)
+ if err != nil {
+ return nil, errors.Annotatef(err, "zone %d", i)
+ }
+ result = append(result, zone)
+ }
+ return result, nil
+}
+
+type zoneDeserializationFunc func(map[string]interface{}) (*zone, error)
+
+var zoneDeserializationFuncs = map[version.Number]zoneDeserializationFunc{
+ twoDotOh: zone_2_0,
+}
+
+func zone_2_0(source map[string]interface{}) (*zone, error) {
+ fields := schema.Fields{
+ "name": schema.String(),
+ "description": schema.String(),
+ "resource_uri": schema.String(),
+ }
+ checker := schema.FieldMap(fields, nil) // no defaults
+ coerced, err := checker.Coerce(source, nil)
+ if err != nil {
+ return nil, errors.Annotatef(err, "zone 2.0 schema check failed")
+ }
+ valid := coerced.(map[string]interface{})
+ // From here we know that the map returned from the schema coercion
+ // contains fields of the right type.
+
+ result := &zone{
+ name: valid["name"].(string),
+ description: valid["description"].(string),
+ resourceURI: valid["resource_uri"].(string),
+ }
+ return result, nil
+}