cord-776 create build / runtime containers for autmation uservices
Change-Id: I246973192adef56a250ffe93a5f65fff488840c1
diff --git a/automation/vendor/github.com/juju/ansiterm/LICENSE b/automation/vendor/github.com/juju/ansiterm/LICENSE
new file mode 100644
index 0000000..ade9307
--- /dev/null
+++ b/automation/vendor/github.com/juju/ansiterm/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) 2015 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/ansiterm/Makefile b/automation/vendor/github.com/juju/ansiterm/Makefile
new file mode 100644
index 0000000..212fdcb
--- /dev/null
+++ b/automation/vendor/github.com/juju/ansiterm/Makefile
@@ -0,0 +1,14 @@
+# Copyright 2016 Canonical Ltd.
+# Licensed under the LGPLv3, see LICENCE file for details.
+
+default: check
+
+check:
+ go test
+
+docs:
+ godoc2md github.com/juju/ansiterm > README.md
+ sed -i 's|\[godoc-link-here\]|[![GoDoc](https://godoc.org/github.com/juju/ansiterm?status.svg)](https://godoc.org/github.com/juju/ansiterm)|' README.md
+
+
+.PHONY: default check docs
diff --git a/automation/vendor/github.com/juju/ansiterm/README.md b/automation/vendor/github.com/juju/ansiterm/README.md
new file mode 100644
index 0000000..5674387
--- /dev/null
+++ b/automation/vendor/github.com/juju/ansiterm/README.md
@@ -0,0 +1,323 @@
+
+# ansiterm
+ import "github.com/juju/ansiterm"
+
+Package ansiterm provides a Writer that writes out the ANSI escape
+codes for color and styles.
+
+
+
+
+
+
+
+## type Color
+``` go
+type Color int
+```
+Color represents one of the standard 16 ANSI colors.
+
+
+
+``` go
+const (
+ Default Color
+ Black
+ Red
+ Green
+ Yellow
+ Blue
+ Magenta
+ Cyan
+ Gray
+ DarkGray
+ BrightRed
+ BrightGreen
+ BrightYellow
+ BrightBlue
+ BrightMagenta
+ BrightCyan
+ White
+)
+```
+
+
+
+
+
+
+
+
+### func (Color) String
+``` go
+func (c Color) String() string
+```
+String returns the name of the color.
+
+
+
+## type Context
+``` go
+type Context struct {
+ Foreground Color
+ Background Color
+ Styles []Style
+}
+```
+Context provides a way to specify both foreground and background colors
+along with other styles and write text to a Writer with those colors and
+styles.
+
+
+
+
+
+
+
+
+
+### func Background
+``` go
+func Background(color Color) *Context
+```
+Background is a convenience function that creates a Context with the
+specified color as the background color.
+
+
+### func Foreground
+``` go
+func Foreground(color Color) *Context
+```
+Foreground is a convenience function that creates a Context with the
+specified color as the foreground color.
+
+
+### func Styles
+``` go
+func Styles(styles ...Style) *Context
+```
+Styles is a convenience function that creates a Context with the
+specified styles set.
+
+
+
+
+### func (\*Context) Fprint
+``` go
+func (c *Context) Fprint(w sgrWriter, args ...interface{})
+```
+Fprint will set the sgr values of the writer to the specified foreground,
+background and styles, then formats using the default formats for its
+operands and writes to w. Spaces are added between operands when neither is
+a string. It returns the number of bytes written and any write error
+encountered.
+
+
+
+### func (\*Context) Fprintf
+``` go
+func (c *Context) Fprintf(w sgrWriter, format string, args ...interface{})
+```
+Fprintf will set the sgr values of the writer to the specified
+foreground, background and styles, then write the formatted string,
+then reset the writer.
+
+
+
+### func (\*Context) SetBackground
+``` go
+func (c *Context) SetBackground(color Color) *Context
+```
+SetBackground sets the background to the specified color.
+
+
+
+### func (\*Context) SetForeground
+``` go
+func (c *Context) SetForeground(color Color) *Context
+```
+SetForeground sets the foreground to the specified color.
+
+
+
+### func (\*Context) SetStyle
+``` go
+func (c *Context) SetStyle(styles ...Style) *Context
+```
+SetStyle replaces the styles with the new values.
+
+
+
+## type Style
+``` go
+type Style int
+```
+
+
+``` go
+const (
+ Bold Style
+ Faint
+ Italic
+ Underline
+ Blink
+ Reverse
+ Strikethrough
+ Conceal
+)
+```
+
+
+
+
+
+
+
+
+### func (Style) String
+``` go
+func (s Style) String() string
+```
+
+
+## type TabWriter
+``` go
+type TabWriter struct {
+ Writer
+ // contains filtered or unexported fields
+}
+```
+TabWriter is a filter that inserts padding around tab-delimited
+columns in its input to align them in the output.
+
+It also setting of colors and styles over and above the standard
+tabwriter package.
+
+
+
+
+
+
+
+
+
+### func NewTabWriter
+``` go
+func NewTabWriter(output io.Writer, minwidth, tabwidth, padding int, padchar byte, flags uint) *TabWriter
+```
+NewTabWriter returns a writer that is able to set colors and styels.
+The ansi escape codes are stripped for width calculations.
+
+
+
+
+### func (\*TabWriter) Flush
+``` go
+func (t *TabWriter) Flush() error
+```
+Flush should be called after the last call to Write to ensure
+that any data buffered in the Writer is written to output. Any
+incomplete escape sequence at the end is considered
+complete for formatting purposes.
+
+
+
+### func (\*TabWriter) Init
+``` go
+func (t *TabWriter) Init(output io.Writer, minwidth, tabwidth, padding int, padchar byte, flags uint) *TabWriter
+```
+A Writer must be initialized with a call to Init. The first parameter (output)
+specifies the filter output. The remaining parameters control the formatting:
+
+
+ minwidth minimal cell width including any padding
+ tabwidth width of tab characters (equivalent number of spaces)
+ padding padding added to a cell before computing its width
+ padchar ASCII char used for padding
+ if padchar == '\t', the Writer will assume that the
+ width of a '\t' in the formatted output is tabwidth,
+ and cells are left-aligned independent of align_left
+ (for correct-looking results, tabwidth must correspond
+ to the tab width in the viewer displaying the result)
+ flags formatting control
+
+
+
+## type Writer
+``` go
+type Writer struct {
+ io.Writer
+ // contains filtered or unexported fields
+}
+```
+Writer allows colors and styles to be specified. If the io.Writer
+is not a terminal capable of color, all attempts to set colors or
+styles are no-ops.
+
+
+
+
+
+
+
+
+
+### func NewWriter
+``` go
+func NewWriter(w io.Writer) *Writer
+```
+NewWriter returns a Writer that allows the caller to specify colors and
+styles. If the io.Writer is not a terminal capable of color, all attempts
+to set colors or styles are no-ops.
+
+
+
+
+### func (\*Writer) ClearStyle
+``` go
+func (w *Writer) ClearStyle(s Style)
+```
+ClearStyle clears the text style.
+
+
+
+### func (\*Writer) Reset
+``` go
+func (w *Writer) Reset()
+```
+Reset returns the default foreground and background colors with no styles.
+
+
+
+### func (\*Writer) SetBackground
+``` go
+func (w *Writer) SetBackground(c Color)
+```
+SetBackground sets the background color.
+
+
+
+### func (\*Writer) SetForeground
+``` go
+func (w *Writer) SetForeground(c Color)
+```
+SetForeground sets the foreground color.
+
+
+
+### func (\*Writer) SetStyle
+``` go
+func (w *Writer) SetStyle(s Style)
+```
+SetStyle sets the text style.
+
+
+
+
+
+
+
+
+
+- - -
+Generated by [godoc2md](http://godoc.org/github.com/davecheney/godoc2md)
\ No newline at end of file
diff --git a/automation/vendor/github.com/juju/ansiterm/attribute.go b/automation/vendor/github.com/juju/ansiterm/attribute.go
new file mode 100644
index 0000000..f2daa48
--- /dev/null
+++ b/automation/vendor/github.com/juju/ansiterm/attribute.go
@@ -0,0 +1,50 @@
+// Copyright 2016 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+package ansiterm
+
+import (
+ "fmt"
+ "sort"
+ "strings"
+)
+
+type attribute int
+
+const (
+ unknownAttribute attribute = -1
+ reset attribute = 0
+)
+
+// sgr returns the escape sequence for the Select Graphic Rendition
+// for the attribute.
+func (a attribute) sgr() string {
+ if a < 0 {
+ return ""
+ }
+ return fmt.Sprintf("\x1b[%dm", a)
+}
+
+type attributes []attribute
+
+func (a attributes) Len() int { return len(a) }
+func (a attributes) Less(i, j int) bool { return a[i] < a[j] }
+func (a attributes) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
+
+// sgr returns the combined escape sequence for the Select Graphic Rendition
+// for the sequence of attributes.
+func (a attributes) sgr() string {
+ switch len(a) {
+ case 0:
+ return ""
+ case 1:
+ return a[0].sgr()
+ default:
+ sort.Sort(a)
+ var values []string
+ for _, attr := range a {
+ values = append(values, fmt.Sprint(attr))
+ }
+ return fmt.Sprintf("\x1b[%sm", strings.Join(values, ";"))
+ }
+}
diff --git a/automation/vendor/github.com/juju/ansiterm/color.go b/automation/vendor/github.com/juju/ansiterm/color.go
new file mode 100644
index 0000000..0a97de3
--- /dev/null
+++ b/automation/vendor/github.com/juju/ansiterm/color.go
@@ -0,0 +1,119 @@
+// Copyright 2016 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+package ansiterm
+
+const (
+ _ Color = iota
+ Default
+ Black
+ Red
+ Green
+ Yellow
+ Blue
+ Magenta
+ Cyan
+ Gray
+ DarkGray
+ BrightRed
+ BrightGreen
+ BrightYellow
+ BrightBlue
+ BrightMagenta
+ BrightCyan
+ White
+)
+
+// Color represents one of the standard 16 ANSI colors.
+type Color int
+
+// String returns the name of the color.
+func (c Color) String() string {
+ switch c {
+ case Default:
+ return "default"
+ case Black:
+ return "black"
+ case Red:
+ return "red"
+ case Green:
+ return "green"
+ case Yellow:
+ return "yellow"
+ case Blue:
+ return "blue"
+ case Magenta:
+ return "magenta"
+ case Cyan:
+ return "cyan"
+ case Gray:
+ return "gray"
+ case DarkGray:
+ return "darkgray"
+ case BrightRed:
+ return "brightred"
+ case BrightGreen:
+ return "brightgreen"
+ case BrightYellow:
+ return "brightyellow"
+ case BrightBlue:
+ return "brightblue"
+ case BrightMagenta:
+ return "brightmagenta"
+ case BrightCyan:
+ return "brightcyan"
+ case White:
+ return "white"
+ default:
+ return ""
+ }
+}
+
+func (c Color) foreground() attribute {
+ switch c {
+ case Default:
+ return 39
+ case Black:
+ return 30
+ case Red:
+ return 31
+ case Green:
+ return 32
+ case Yellow:
+ return 33
+ case Blue:
+ return 34
+ case Magenta:
+ return 35
+ case Cyan:
+ return 36
+ case Gray:
+ return 37
+ case DarkGray:
+ return 90
+ case BrightRed:
+ return 91
+ case BrightGreen:
+ return 92
+ case BrightYellow:
+ return 93
+ case BrightBlue:
+ return 94
+ case BrightMagenta:
+ return 95
+ case BrightCyan:
+ return 96
+ case White:
+ return 97
+ default:
+ return unknownAttribute
+ }
+}
+
+func (c Color) background() attribute {
+ value := c.foreground()
+ if value != unknownAttribute {
+ return value + 10
+ }
+ return value
+}
diff --git a/automation/vendor/github.com/juju/ansiterm/context.go b/automation/vendor/github.com/juju/ansiterm/context.go
new file mode 100644
index 0000000..e61a867
--- /dev/null
+++ b/automation/vendor/github.com/juju/ansiterm/context.go
@@ -0,0 +1,95 @@
+// Copyright 2016 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+package ansiterm
+
+import (
+ "fmt"
+ "io"
+)
+
+// Context provides a way to specify both foreground and background colors
+// along with other styles and write text to a Writer with those colors and
+// styles.
+type Context struct {
+ Foreground Color
+ Background Color
+ Styles []Style
+}
+
+// Foreground is a convenience function that creates a Context with the
+// specified color as the foreground color.
+func Foreground(color Color) *Context {
+ return &Context{Foreground: color}
+}
+
+// Background is a convenience function that creates a Context with the
+// specified color as the background color.
+func Background(color Color) *Context {
+ return &Context{Background: color}
+}
+
+// Styles is a convenience function that creates a Context with the
+// specified styles set.
+func Styles(styles ...Style) *Context {
+ return &Context{Styles: styles}
+}
+
+// SetForeground sets the foreground to the specified color.
+func (c *Context) SetForeground(color Color) *Context {
+ c.Foreground = color
+ return c
+}
+
+// SetBackground sets the background to the specified color.
+func (c *Context) SetBackground(color Color) *Context {
+ c.Background = color
+ return c
+}
+
+// SetStyle replaces the styles with the new values.
+func (c *Context) SetStyle(styles ...Style) *Context {
+ c.Styles = styles
+ return c
+}
+
+type sgrWriter interface {
+ io.Writer
+ writeSGR(value sgr)
+}
+
+// Fprintf will set the sgr values of the writer to the specified
+// foreground, background and styles, then write the formatted string,
+// then reset the writer.
+func (c *Context) Fprintf(w sgrWriter, format string, args ...interface{}) {
+ w.writeSGR(c)
+ fmt.Fprintf(w, format, args...)
+ w.writeSGR(reset)
+}
+
+// Fprint will set the sgr values of the writer to the specified foreground,
+// background and styles, then formats using the default formats for its
+// operands and writes to w. Spaces are added between operands when neither is
+// a string. It returns the number of bytes written and any write error
+// encountered.
+func (c *Context) Fprint(w sgrWriter, args ...interface{}) {
+ w.writeSGR(c)
+ fmt.Fprint(w, args...)
+ w.writeSGR(reset)
+}
+
+func (c *Context) sgr() string {
+ var values attributes
+ if foreground := c.Foreground.foreground(); foreground != unknownAttribute {
+ values = append(values, foreground)
+ }
+ if background := c.Background.background(); background != unknownAttribute {
+ values = append(values, background)
+ }
+ for _, style := range c.Styles {
+ if value := style.enable(); value != unknownAttribute {
+ values = append(values, value)
+ }
+ }
+ return values.sgr()
+}
diff --git a/automation/vendor/github.com/juju/ansiterm/doc.go b/automation/vendor/github.com/juju/ansiterm/doc.go
new file mode 100644
index 0000000..7827007
--- /dev/null
+++ b/automation/vendor/github.com/juju/ansiterm/doc.go
@@ -0,0 +1,6 @@
+// Copyright 2016 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+// Package ansiterm provides a Writer that writes out the ANSI escape
+// codes for color and styles.
+package ansiterm
diff --git a/automation/vendor/github.com/juju/ansiterm/style.go b/automation/vendor/github.com/juju/ansiterm/style.go
new file mode 100644
index 0000000..0be42da
--- /dev/null
+++ b/automation/vendor/github.com/juju/ansiterm/style.go
@@ -0,0 +1,72 @@
+// Copyright 2016 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+package ansiterm
+
+const (
+ _ Style = iota
+ Bold
+ Faint
+ Italic
+ Underline
+ Blink
+ Reverse
+ Strikethrough
+ Conceal
+)
+
+type Style int
+
+func (s Style) String() string {
+ switch s {
+ case Bold:
+ return "bold"
+ case Faint:
+ return "faint"
+ case Italic:
+ return "italic"
+ case Underline:
+ return "underline"
+ case Blink:
+ return "blink"
+ case Reverse:
+ return "reverse"
+ case Strikethrough:
+ return "strikethrough"
+ case Conceal:
+ return "conceal"
+ default:
+ return ""
+ }
+}
+
+func (s Style) enable() attribute {
+ switch s {
+ case Bold:
+ return 1
+ case Faint:
+ return 2
+ case Italic:
+ return 3
+ case Underline:
+ return 4
+ case Blink:
+ return 5
+ case Reverse:
+ return 7
+ case Conceal:
+ return 8
+ case Strikethrough:
+ return 9
+ default:
+ return unknownAttribute
+ }
+}
+
+func (s Style) disable() attribute {
+ value := s.enable()
+ if value != unknownAttribute {
+ return value + 20
+ }
+ return value
+}
diff --git a/automation/vendor/github.com/juju/ansiterm/tabwriter.go b/automation/vendor/github.com/juju/ansiterm/tabwriter.go
new file mode 100644
index 0000000..1ff6faa
--- /dev/null
+++ b/automation/vendor/github.com/juju/ansiterm/tabwriter.go
@@ -0,0 +1,64 @@
+// Copyright 2016 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+package ansiterm
+
+import (
+ "io"
+
+ "github.com/juju/ansiterm/tabwriter"
+)
+
+// NewTabWriter returns a writer that is able to set colors and styels.
+// The ansi escape codes are stripped for width calculations.
+func NewTabWriter(output io.Writer, minwidth, tabwidth, padding int, padchar byte, flags uint) *TabWriter {
+ return new(TabWriter).Init(output, minwidth, tabwidth, padding, padchar, flags)
+}
+
+// TabWriter is a filter that inserts padding around tab-delimited
+// columns in its input to align them in the output.
+//
+// It also setting of colors and styles over and above the standard
+// tabwriter package.
+type TabWriter struct {
+ Writer
+ tw tabwriter.Writer
+}
+
+// Flush should be called after the last call to Write to ensure
+// that any data buffered in the Writer is written to output. Any
+// incomplete escape sequence at the end is considered
+// complete for formatting purposes.
+//
+func (t *TabWriter) Flush() error {
+ return t.tw.Flush()
+}
+
+// SetColumnAlignRight will mark a particular column as align right.
+// This is reset on the next flush.
+func (t *TabWriter) SetColumnAlignRight(column int) {
+ t.tw.SetColumnAlignRight(column)
+}
+
+// A Writer must be initialized with a call to Init. The first parameter (output)
+// specifies the filter output. The remaining parameters control the formatting:
+//
+// minwidth minimal cell width including any padding
+// tabwidth width of tab characters (equivalent number of spaces)
+// padding padding added to a cell before computing its width
+// padchar ASCII char used for padding
+// if padchar == '\t', the Writer will assume that the
+// width of a '\t' in the formatted output is tabwidth,
+// and cells are left-aligned independent of align_left
+// (for correct-looking results, tabwidth must correspond
+// to the tab width in the viewer displaying the result)
+// flags formatting control
+//
+func (t *TabWriter) Init(output io.Writer, minwidth, tabwidth, padding int, padchar byte, flags uint) *TabWriter {
+ writer, colorCapable := colorEnabledWriter(output)
+ t.Writer = Writer{
+ Writer: t.tw.Init(writer, minwidth, tabwidth, padding, padchar, flags),
+ noColor: !colorCapable,
+ }
+ return t
+}
diff --git a/automation/vendor/github.com/juju/ansiterm/tabwriter/tabwriter.go b/automation/vendor/github.com/juju/ansiterm/tabwriter/tabwriter.go
new file mode 100644
index 0000000..98949d0
--- /dev/null
+++ b/automation/vendor/github.com/juju/ansiterm/tabwriter/tabwriter.go
@@ -0,0 +1,587 @@
+// Copyright 2009 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// This file is mostly a copy of the go standard library text/tabwriter. With
+// the additional stripping of ansi control characters for width calculations.
+
+// Package tabwriter implements a write filter (tabwriter.Writer) that
+// translates tabbed columns in input into properly aligned text.
+//
+// The package is using the Elastic Tabstops algorithm described at
+// http://nickgravgaard.com/elastictabstops/index.html.
+//
+package tabwriter
+
+import (
+ "bytes"
+ "io"
+ "unicode/utf8"
+
+ "github.com/lunixbochs/vtclean"
+)
+
+// ----------------------------------------------------------------------------
+// Filter implementation
+
+// A cell represents a segment of text terminated by tabs or line breaks.
+// The text itself is stored in a separate buffer; cell only describes the
+// segment's size in bytes, its width in runes, and whether it's an htab
+// ('\t') terminated cell.
+//
+type cell struct {
+ size int // cell size in bytes
+ width int // cell width in runes
+ htab bool // true if the cell is terminated by an htab ('\t')
+}
+
+// A Writer is a filter that inserts padding around tab-delimited
+// columns in its input to align them in the output.
+//
+// The Writer treats incoming bytes as UTF-8 encoded text consisting
+// of cells terminated by (horizontal or vertical) tabs or line
+// breaks (newline or formfeed characters). Cells in adjacent lines
+// constitute a column. The Writer inserts padding as needed to
+// make all cells in a column have the same width, effectively
+// aligning the columns. It assumes that all characters have the
+// same width except for tabs for which a tabwidth must be specified.
+// Note that cells are tab-terminated, not tab-separated: trailing
+// non-tab text at the end of a line does not form a column cell.
+//
+// The Writer assumes that all Unicode code points have the same width;
+// this may not be true in some fonts.
+//
+// If DiscardEmptyColumns is set, empty columns that are terminated
+// entirely by vertical (or "soft") tabs are discarded. Columns
+// terminated by horizontal (or "hard") tabs are not affected by
+// this flag.
+//
+// If a Writer is configured to filter HTML, HTML tags and entities
+// are passed through. The widths of tags and entities are
+// assumed to be zero (tags) and one (entities) for formatting purposes.
+//
+// A segment of text may be escaped by bracketing it with Escape
+// characters. The tabwriter passes escaped text segments through
+// unchanged. In particular, it does not interpret any tabs or line
+// breaks within the segment. If the StripEscape flag is set, the
+// Escape characters are stripped from the output; otherwise they
+// are passed through as well. For the purpose of formatting, the
+// width of the escaped text is always computed excluding the Escape
+// characters.
+//
+// The formfeed character ('\f') acts like a newline but it also
+// terminates all columns in the current line (effectively calling
+// Flush). Cells in the next line start new columns. Unless found
+// inside an HTML tag or inside an escaped text segment, formfeed
+// characters appear as newlines in the output.
+//
+// The Writer must buffer input internally, because proper spacing
+// of one line may depend on the cells in future lines. Clients must
+// call Flush when done calling Write.
+//
+type Writer struct {
+ // configuration
+ output io.Writer
+ minwidth int
+ tabwidth int
+ padding int
+ padbytes [8]byte
+ flags uint
+
+ // current state
+ buf bytes.Buffer // collected text excluding tabs or line breaks
+ pos int // buffer position up to which cell.width of incomplete cell has been computed
+ cell cell // current incomplete cell; cell.width is up to buf[pos] excluding ignored sections
+ endChar byte // terminating char of escaped sequence (Escape for escapes, '>', ';' for HTML tags/entities, or 0)
+ lines [][]cell // list of lines; each line is a list of cells
+ widths []int // list of column widths in runes - re-used during formatting
+ alignment map[int]uint // column alignment
+}
+
+func (b *Writer) addLine() { b.lines = append(b.lines, []cell{}) }
+
+// Reset the current state.
+func (b *Writer) reset() {
+ b.buf.Reset()
+ b.pos = 0
+ b.cell = cell{}
+ b.endChar = 0
+ b.lines = b.lines[0:0]
+ b.widths = b.widths[0:0]
+ b.alignment = make(map[int]uint)
+ b.addLine()
+}
+
+// Internal representation (current state):
+//
+// - all text written is appended to buf; tabs and line breaks are stripped away
+// - at any given time there is a (possibly empty) incomplete cell at the end
+// (the cell starts after a tab or line break)
+// - cell.size is the number of bytes belonging to the cell so far
+// - cell.width is text width in runes of that cell from the start of the cell to
+// position pos; html tags and entities are excluded from this width if html
+// filtering is enabled
+// - the sizes and widths of processed text are kept in the lines list
+// which contains a list of cells for each line
+// - the widths list is a temporary list with current widths used during
+// formatting; it is kept in Writer because it's re-used
+//
+// |<---------- size ---------->|
+// | |
+// |<- width ->|<- ignored ->| |
+// | | | |
+// [---processed---tab------------<tag>...</tag>...]
+// ^ ^ ^
+// | | |
+// buf start of incomplete cell pos
+
+// Formatting can be controlled with these flags.
+const (
+ // Ignore html tags and treat entities (starting with '&'
+ // and ending in ';') as single characters (width = 1).
+ FilterHTML uint = 1 << iota
+
+ // Strip Escape characters bracketing escaped text segments
+ // instead of passing them through unchanged with the text.
+ StripEscape
+
+ // Force right-alignment of cell content.
+ // Default is left-alignment.
+ AlignRight
+
+ // Handle empty columns as if they were not present in
+ // the input in the first place.
+ DiscardEmptyColumns
+
+ // Always use tabs for indentation columns (i.e., padding of
+ // leading empty cells on the left) independent of padchar.
+ TabIndent
+
+ // Print a vertical bar ('|') between columns (after formatting).
+ // Discarded columns appear as zero-width columns ("||").
+ Debug
+)
+
+// A Writer must be initialized with a call to Init. The first parameter (output)
+// specifies the filter output. The remaining parameters control the formatting:
+//
+// minwidth minimal cell width including any padding
+// tabwidth width of tab characters (equivalent number of spaces)
+// padding padding added to a cell before computing its width
+// padchar ASCII char used for padding
+// if padchar == '\t', the Writer will assume that the
+// width of a '\t' in the formatted output is tabwidth,
+// and cells are left-aligned independent of align_left
+// (for correct-looking results, tabwidth must correspond
+// to the tab width in the viewer displaying the result)
+// flags formatting control
+//
+func (b *Writer) Init(output io.Writer, minwidth, tabwidth, padding int, padchar byte, flags uint) *Writer {
+ if minwidth < 0 || tabwidth < 0 || padding < 0 {
+ panic("negative minwidth, tabwidth, or padding")
+ }
+ b.output = output
+ b.minwidth = minwidth
+ b.tabwidth = tabwidth
+ b.padding = padding
+ for i := range b.padbytes {
+ b.padbytes[i] = padchar
+ }
+ if padchar == '\t' {
+ // tab padding enforces left-alignment
+ flags &^= AlignRight
+ }
+ b.flags = flags
+
+ b.reset()
+
+ return b
+}
+
+// debugging support (keep code around)
+func (b *Writer) dump() {
+ pos := 0
+ for i, line := range b.lines {
+ print("(", i, ") ")
+ for _, c := range line {
+ print("[", string(b.buf.Bytes()[pos:pos+c.size]), "]")
+ pos += c.size
+ }
+ print("\n")
+ }
+ print("\n")
+}
+
+// local error wrapper so we can distinguish errors we want to return
+// as errors from genuine panics (which we don't want to return as errors)
+type osError struct {
+ err error
+}
+
+func (b *Writer) write0(buf []byte) {
+ n, err := b.output.Write(buf)
+ if n != len(buf) && err == nil {
+ err = io.ErrShortWrite
+ }
+ if err != nil {
+ panic(osError{err})
+ }
+}
+
+func (b *Writer) writeN(src []byte, n int) {
+ for n > len(src) {
+ b.write0(src)
+ n -= len(src)
+ }
+ b.write0(src[0:n])
+}
+
+var (
+ newline = []byte{'\n'}
+ tabs = []byte("\t\t\t\t\t\t\t\t")
+)
+
+func (b *Writer) writePadding(textw, cellw int, useTabs bool) {
+ if b.padbytes[0] == '\t' || useTabs {
+ // padding is done with tabs
+ if b.tabwidth == 0 {
+ return // tabs have no width - can't do any padding
+ }
+ // make cellw the smallest multiple of b.tabwidth
+ cellw = (cellw + b.tabwidth - 1) / b.tabwidth * b.tabwidth
+ n := cellw - textw // amount of padding
+ if n < 0 {
+ panic("internal error")
+ }
+ b.writeN(tabs, (n+b.tabwidth-1)/b.tabwidth)
+ return
+ }
+
+ // padding is done with non-tab characters
+ b.writeN(b.padbytes[0:], cellw-textw)
+}
+
+var vbar = []byte{'|'}
+
+func (b *Writer) writeLines(pos0 int, line0, line1 int) (pos int) {
+ pos = pos0
+ for i := line0; i < line1; i++ {
+ line := b.lines[i]
+
+ // if TabIndent is set, use tabs to pad leading empty cells
+ useTabs := b.flags&TabIndent != 0
+
+ for j, c := range line {
+ if j > 0 && b.flags&Debug != 0 {
+ // indicate column break
+ b.write0(vbar)
+ }
+
+ if c.size == 0 {
+ // empty cell
+ if j < len(b.widths) {
+ b.writePadding(c.width, b.widths[j], useTabs)
+ }
+ } else {
+ // non-empty cell
+ useTabs = false
+ alignColumnRight := b.alignment[j] == AlignRight
+ if (b.flags&AlignRight == 0) && !alignColumnRight { // align left
+ b.write0(b.buf.Bytes()[pos : pos+c.size])
+ pos += c.size
+ if j < len(b.widths) {
+ b.writePadding(c.width, b.widths[j], false)
+ }
+ } else if alignColumnRight && j < len(b.widths) {
+ // just this column
+ internalSize := b.widths[j] - b.padding
+ if j < len(b.widths) {
+ b.writePadding(c.width, internalSize, false)
+ }
+ b.write0(b.buf.Bytes()[pos : pos+c.size])
+ if b.padding > 0 {
+ b.writePadding(0, b.padding, false)
+ }
+ pos += c.size
+ } else { // align right
+ if j < len(b.widths) {
+ b.writePadding(c.width, b.widths[j], false)
+ }
+ b.write0(b.buf.Bytes()[pos : pos+c.size])
+ pos += c.size
+ }
+ }
+ }
+
+ if i+1 == len(b.lines) {
+ // last buffered line - we don't have a newline, so just write
+ // any outstanding buffered data
+ b.write0(b.buf.Bytes()[pos : pos+b.cell.size])
+ pos += b.cell.size
+ } else {
+ // not the last line - write newline
+ b.write0(newline)
+ }
+ }
+ return
+}
+
+// Format the text between line0 and line1 (excluding line1); pos
+// is the buffer position corresponding to the beginning of line0.
+// Returns the buffer position corresponding to the beginning of
+// line1 and an error, if any.
+//
+func (b *Writer) format(pos0 int, line0, line1 int) (pos int) {
+ pos = pos0
+ column := len(b.widths)
+ for this := line0; this < line1; this++ {
+ line := b.lines[this]
+
+ if column < len(line)-1 {
+ // cell exists in this column => this line
+ // has more cells than the previous line
+ // (the last cell per line is ignored because cells are
+ // tab-terminated; the last cell per line describes the
+ // text before the newline/formfeed and does not belong
+ // to a column)
+
+ // print unprinted lines until beginning of block
+ pos = b.writeLines(pos, line0, this)
+ line0 = this
+
+ // column block begin
+ width := b.minwidth // minimal column width
+ discardable := true // true if all cells in this column are empty and "soft"
+ for ; this < line1; this++ {
+ line = b.lines[this]
+ if column < len(line)-1 {
+ // cell exists in this column
+ c := line[column]
+ // update width
+ if w := c.width + b.padding; w > width {
+ width = w
+ }
+ // update discardable
+ if c.width > 0 || c.htab {
+ discardable = false
+ }
+ } else {
+ break
+ }
+ }
+ // column block end
+
+ // discard empty columns if necessary
+ if discardable && b.flags&DiscardEmptyColumns != 0 {
+ width = 0
+ }
+
+ // format and print all columns to the right of this column
+ // (we know the widths of this column and all columns to the left)
+ b.widths = append(b.widths, width) // push width
+ pos = b.format(pos, line0, this)
+ b.widths = b.widths[0 : len(b.widths)-1] // pop width
+ line0 = this
+ }
+ }
+
+ // print unprinted lines until end
+ return b.writeLines(pos, line0, line1)
+}
+
+// Append text to current cell.
+func (b *Writer) append(text []byte) {
+ b.buf.Write(text)
+ b.cell.size += len(text)
+}
+
+// Update the cell width.
+func (b *Writer) updateWidth() {
+ // ---- Changes here -----
+ newChars := b.buf.Bytes()[b.pos:b.buf.Len()]
+ cleaned := vtclean.Clean(string(newChars), false) // false to strip colors
+ b.cell.width += utf8.RuneCount([]byte(cleaned))
+ // --- end of changes ----
+ b.pos = b.buf.Len()
+}
+
+// To escape a text segment, bracket it with Escape characters.
+// For instance, the tab in this string "Ignore this tab: \xff\t\xff"
+// does not terminate a cell and constitutes a single character of
+// width one for formatting purposes.
+//
+// The value 0xff was chosen because it cannot appear in a valid UTF-8 sequence.
+//
+const Escape = '\xff'
+
+// Start escaped mode.
+func (b *Writer) startEscape(ch byte) {
+ switch ch {
+ case Escape:
+ b.endChar = Escape
+ case '<':
+ b.endChar = '>'
+ case '&':
+ b.endChar = ';'
+ }
+}
+
+// Terminate escaped mode. If the escaped text was an HTML tag, its width
+// is assumed to be zero for formatting purposes; if it was an HTML entity,
+// its width is assumed to be one. In all other cases, the width is the
+// unicode width of the text.
+//
+func (b *Writer) endEscape() {
+ switch b.endChar {
+ case Escape:
+ b.updateWidth()
+ if b.flags&StripEscape == 0 {
+ b.cell.width -= 2 // don't count the Escape chars
+ }
+ case '>': // tag of zero width
+ case ';':
+ b.cell.width++ // entity, count as one rune
+ }
+ b.pos = b.buf.Len()
+ b.endChar = 0
+}
+
+// Terminate the current cell by adding it to the list of cells of the
+// current line. Returns the number of cells in that line.
+//
+func (b *Writer) terminateCell(htab bool) int {
+ b.cell.htab = htab
+ line := &b.lines[len(b.lines)-1]
+ *line = append(*line, b.cell)
+ b.cell = cell{}
+ return len(*line)
+}
+
+func handlePanic(err *error, op string) {
+ if e := recover(); e != nil {
+ if nerr, ok := e.(osError); ok {
+ *err = nerr.err
+ return
+ }
+ panic("tabwriter: panic during " + op)
+ }
+}
+
+// Flush should be called after the last call to Write to ensure
+// that any data buffered in the Writer is written to output. Any
+// incomplete escape sequence at the end is considered
+// complete for formatting purposes.
+//
+func (b *Writer) Flush() (err error) {
+ defer b.reset() // even in the presence of errors
+ defer handlePanic(&err, "Flush")
+
+ // add current cell if not empty
+ if b.cell.size > 0 {
+ if b.endChar != 0 {
+ // inside escape - terminate it even if incomplete
+ b.endEscape()
+ }
+ b.terminateCell(false)
+ }
+
+ // format contents of buffer
+ b.format(0, 0, len(b.lines))
+
+ return
+}
+
+var hbar = []byte("---\n")
+
+// SetColumnAlignRight will mark a particular column as align right.
+// This is reset on the next flush.
+func (b *Writer) SetColumnAlignRight(column int) {
+ b.alignment[column] = AlignRight
+}
+
+// Write writes buf to the writer b.
+// The only errors returned are ones encountered
+// while writing to the underlying output stream.
+//
+func (b *Writer) Write(buf []byte) (n int, err error) {
+ defer handlePanic(&err, "Write")
+
+ // split text into cells
+ n = 0
+ for i, ch := range buf {
+ if b.endChar == 0 {
+ // outside escape
+ switch ch {
+ case '\t', '\v', '\n', '\f':
+ // end of cell
+ b.append(buf[n:i])
+ b.updateWidth()
+ n = i + 1 // ch consumed
+ ncells := b.terminateCell(ch == '\t')
+ if ch == '\n' || ch == '\f' {
+ // terminate line
+ b.addLine()
+ if ch == '\f' || ncells == 1 {
+ // A '\f' always forces a flush. Otherwise, if the previous
+ // line has only one cell which does not have an impact on
+ // the formatting of the following lines (the last cell per
+ // line is ignored by format()), thus we can flush the
+ // Writer contents.
+ if err = b.Flush(); err != nil {
+ return
+ }
+ if ch == '\f' && b.flags&Debug != 0 {
+ // indicate section break
+ b.write0(hbar)
+ }
+ }
+ }
+
+ case Escape:
+ // start of escaped sequence
+ b.append(buf[n:i])
+ b.updateWidth()
+ n = i
+ if b.flags&StripEscape != 0 {
+ n++ // strip Escape
+ }
+ b.startEscape(Escape)
+
+ case '<', '&':
+ // possibly an html tag/entity
+ if b.flags&FilterHTML != 0 {
+ // begin of tag/entity
+ b.append(buf[n:i])
+ b.updateWidth()
+ n = i
+ b.startEscape(ch)
+ }
+ }
+
+ } else {
+ // inside escape
+ if ch == b.endChar {
+ // end of tag/entity
+ j := i + 1
+ if ch == Escape && b.flags&StripEscape != 0 {
+ j = i // strip Escape
+ }
+ b.append(buf[n:j])
+ n = i + 1 // ch consumed
+ b.endEscape()
+ }
+ }
+ }
+
+ // append leftover text
+ b.append(buf[n:])
+ n = len(buf)
+ return
+}
+
+// NewWriter allocates and initializes a new tabwriter.Writer.
+// The parameters are the same as for the Init function.
+//
+func NewWriter(output io.Writer, minwidth, tabwidth, padding int, padchar byte, flags uint) *Writer {
+ return new(Writer).Init(output, minwidth, tabwidth, padding, padchar, flags)
+}
diff --git a/automation/vendor/github.com/juju/ansiterm/terminal.go b/automation/vendor/github.com/juju/ansiterm/terminal.go
new file mode 100644
index 0000000..96fd11c
--- /dev/null
+++ b/automation/vendor/github.com/juju/ansiterm/terminal.go
@@ -0,0 +1,32 @@
+// Copyright 2016 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+package ansiterm
+
+import (
+ "io"
+ "os"
+
+ "github.com/mattn/go-colorable"
+ "github.com/mattn/go-isatty"
+)
+
+// colorEnabledWriter returns a writer that can handle the ansi color codes
+// and true if the writer passed in is a terminal capable of color. If the
+// TERM environment variable is set to "dumb", the terminal is not considered
+// color capable.
+func colorEnabledWriter(w io.Writer) (io.Writer, bool) {
+ f, ok := w.(*os.File)
+ if !ok {
+ return w, false
+ }
+ // Check the TERM environment variable specifically
+ // to check for "dumb" terminals.
+ if os.Getenv("TERM") == "dumb" {
+ return w, false
+ }
+ if !isatty.IsTerminal(f.Fd()) {
+ return w, false
+ }
+ return colorable.NewColorable(f), true
+}
diff --git a/automation/vendor/github.com/juju/ansiterm/writer.go b/automation/vendor/github.com/juju/ansiterm/writer.go
new file mode 100644
index 0000000..32437bb
--- /dev/null
+++ b/automation/vendor/github.com/juju/ansiterm/writer.go
@@ -0,0 +1,74 @@
+// Copyright 2016 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+package ansiterm
+
+import (
+ "fmt"
+ "io"
+)
+
+// Writer allows colors and styles to be specified. If the io.Writer
+// is not a terminal capable of color, all attempts to set colors or
+// styles are no-ops.
+type Writer struct {
+ io.Writer
+
+ noColor bool
+}
+
+// NewWriter returns a Writer that allows the caller to specify colors and
+// styles. If the io.Writer is not a terminal capable of color, all attempts
+// to set colors or styles are no-ops.
+func NewWriter(w io.Writer) *Writer {
+ writer, colorCapable := colorEnabledWriter(w)
+ return &Writer{
+ Writer: writer,
+ noColor: !colorCapable,
+ }
+}
+
+// SetColorCapable forces the writer to either write the ANSI escape color
+// if capable is true, or to not write them if capable is false.
+func (w *Writer) SetColorCapable(capable bool) {
+ w.noColor = !capable
+}
+
+// SetForeground sets the foreground color.
+func (w *Writer) SetForeground(c Color) {
+ w.writeSGR(c.foreground())
+}
+
+// SetBackground sets the background color.
+func (w *Writer) SetBackground(c Color) {
+ w.writeSGR(c.background())
+}
+
+// SetStyle sets the text style.
+func (w *Writer) SetStyle(s Style) {
+ w.writeSGR(s.enable())
+}
+
+// ClearStyle clears the text style.
+func (w *Writer) ClearStyle(s Style) {
+ w.writeSGR(s.disable())
+}
+
+// Reset returns the default foreground and background colors with no styles.
+func (w *Writer) Reset() {
+ w.writeSGR(reset)
+}
+
+type sgr interface {
+ // sgr returns the combined escape sequence for the Select Graphic Rendition.
+ sgr() string
+}
+
+// writeSGR takes the appropriate integer SGR parameters
+// and writes out the ANIS escape code.
+func (w *Writer) writeSGR(value sgr) {
+ if w.noColor {
+ return
+ }
+ fmt.Fprint(w, value.sgr())
+}
diff --git a/automation/vendor/github.com/juju/errors/LICENSE b/automation/vendor/github.com/juju/errors/LICENSE
new file mode 100644
index 0000000..ade9307
--- /dev/null
+++ b/automation/vendor/github.com/juju/errors/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) 2015 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/errors/Makefile b/automation/vendor/github.com/juju/errors/Makefile
new file mode 100644
index 0000000..ab7c2e6
--- /dev/null
+++ b/automation/vendor/github.com/juju/errors/Makefile
@@ -0,0 +1,11 @@
+default: check
+
+check:
+ go test && go test -compiler gccgo
+
+docs:
+ godoc2md github.com/juju/errors > README.md
+ sed -i 's|\[godoc-link-here\]|[![GoDoc](https://godoc.org/github.com/juju/errors?status.svg)](https://godoc.org/github.com/juju/errors)|' README.md
+
+
+.PHONY: default check docs
diff --git a/automation/vendor/github.com/juju/errors/README.md b/automation/vendor/github.com/juju/errors/README.md
new file mode 100644
index 0000000..ee24891
--- /dev/null
+++ b/automation/vendor/github.com/juju/errors/README.md
@@ -0,0 +1,536 @@
+
+# errors
+ import "github.com/juju/errors"
+
+[![GoDoc](https://godoc.org/github.com/juju/errors?status.svg)](https://godoc.org/github.com/juju/errors)
+
+The juju/errors provides an easy way to annotate errors without losing the
+orginal error context.
+
+The exported `New` and `Errorf` functions are designed to replace the
+`errors.New` and `fmt.Errorf` functions respectively. The same underlying
+error is there, but the package also records the location at which the error
+was created.
+
+A primary use case for this library is to add extra context any time an
+error is returned from a function.
+
+
+ if err := SomeFunc(); err != nil {
+ return err
+ }
+
+This instead becomes:
+
+
+ if err := SomeFunc(); err != nil {
+ return errors.Trace(err)
+ }
+
+which just records the file and line number of the Trace call, or
+
+
+ if err := SomeFunc(); err != nil {
+ return errors.Annotate(err, "more context")
+ }
+
+which also adds an annotation to the error.
+
+When you want to check to see if an error is of a particular type, a helper
+function is normally exported by the package that returned the error, like the
+`os` package does. The underlying cause of the error is available using the
+`Cause` function.
+
+
+ os.IsNotExist(errors.Cause(err))
+
+The result of the `Error()` call on an annotated error is the annotations joined
+with colons, then the result of the `Error()` method for the underlying error
+that was the cause.
+
+
+ err := errors.Errorf("original")
+ err = errors.Annotatef(err, "context")
+ err = errors.Annotatef(err, "more context")
+ err.Error() -> "more context: context: original"
+
+Obviously recording the file, line and functions is not very useful if you
+cannot get them back out again.
+
+
+ errors.ErrorStack(err)
+
+will return something like:
+
+
+ first error
+ github.com/juju/errors/annotation_test.go:193:
+ github.com/juju/errors/annotation_test.go:194: annotation
+ github.com/juju/errors/annotation_test.go:195:
+ github.com/juju/errors/annotation_test.go:196: more context
+ github.com/juju/errors/annotation_test.go:197:
+
+The first error was generated by an external system, so there was no location
+associated. The second, fourth, and last lines were generated with Trace calls,
+and the other two through Annotate.
+
+Sometimes when responding to an error you want to return a more specific error
+for the situation.
+
+
+ if err := FindField(field); err != nil {
+ return errors.Wrap(err, errors.NotFoundf(field))
+ }
+
+This returns an error where the complete error stack is still available, and
+`errors.Cause()` will return the `NotFound` error.
+
+
+
+
+
+
+## func AlreadyExistsf
+``` go
+func AlreadyExistsf(format string, args ...interface{}) error
+```
+AlreadyExistsf returns an error which satisfies IsAlreadyExists().
+
+
+## func Annotate
+``` go
+func Annotate(other error, message string) error
+```
+Annotate is used to add extra context to an existing error. The location of
+the Annotate call is recorded with the annotations. The file, line and
+function are also recorded.
+
+For example:
+
+
+ if err := SomeFunc(); err != nil {
+ return errors.Annotate(err, "failed to frombulate")
+ }
+
+
+## func Annotatef
+``` go
+func Annotatef(other error, format string, args ...interface{}) error
+```
+Annotatef is used to add extra context to an existing error. The location of
+the Annotate call is recorded with the annotations. The file, line and
+function are also recorded.
+
+For example:
+
+
+ if err := SomeFunc(); err != nil {
+ return errors.Annotatef(err, "failed to frombulate the %s", arg)
+ }
+
+
+## func Cause
+``` go
+func Cause(err error) error
+```
+Cause returns the cause of the given error. This will be either the
+original error, or the result of a Wrap or Mask call.
+
+Cause is the usual way to diagnose errors that may have been wrapped by
+the other errors functions.
+
+
+## func DeferredAnnotatef
+``` go
+func DeferredAnnotatef(err *error, format string, args ...interface{})
+```
+DeferredAnnotatef annotates the given error (when it is not nil) with the given
+format string and arguments (like fmt.Sprintf). If *err is nil, DeferredAnnotatef
+does nothing. This method is used in a defer statement in order to annotate any
+resulting error with the same message.
+
+For example:
+
+
+ defer DeferredAnnotatef(&err, "failed to frombulate the %s", arg)
+
+
+## func Details
+``` go
+func Details(err error) string
+```
+Details returns information about the stack of errors wrapped by err, in
+the format:
+
+
+ [{filename:99: error one} {otherfile:55: cause of error one}]
+
+This is a terse alternative to ErrorStack as it returns a single line.
+
+
+## func ErrorStack
+``` go
+func ErrorStack(err error) string
+```
+ErrorStack returns a string representation of the annotated error. If the
+error passed as the parameter is not an annotated error, the result is
+simply the result of the Error() method on that error.
+
+If the error is an annotated error, a multi-line string is returned where
+each line represents one entry in the annotation stack. The full filename
+from the call stack is used in the output.
+
+
+ first error
+ github.com/juju/errors/annotation_test.go:193:
+ github.com/juju/errors/annotation_test.go:194: annotation
+ github.com/juju/errors/annotation_test.go:195:
+ github.com/juju/errors/annotation_test.go:196: more context
+ github.com/juju/errors/annotation_test.go:197:
+
+
+## func Errorf
+``` go
+func Errorf(format string, args ...interface{}) error
+```
+Errorf creates a new annotated error and records the location that the
+error is created. This should be a drop in replacement for fmt.Errorf.
+
+For example:
+
+
+ return errors.Errorf("validation failed: %s", message)
+
+
+## func IsAlreadyExists
+``` go
+func IsAlreadyExists(err error) bool
+```
+IsAlreadyExists reports whether the error was created with
+AlreadyExistsf() or NewAlreadyExists().
+
+
+## func IsNotFound
+``` go
+func IsNotFound(err error) bool
+```
+IsNotFound reports whether err was created with NotFoundf() or
+NewNotFound().
+
+
+## func IsNotImplemented
+``` go
+func IsNotImplemented(err error) bool
+```
+IsNotImplemented reports whether err was created with
+NotImplementedf() or NewNotImplemented().
+
+
+## func IsNotSupported
+``` go
+func IsNotSupported(err error) bool
+```
+IsNotSupported reports whether the error was created with
+NotSupportedf() or NewNotSupported().
+
+
+## func IsNotValid
+``` go
+func IsNotValid(err error) bool
+```
+IsNotValid reports whether the error was created with NotValidf() or
+NewNotValid().
+
+
+## func IsUnauthorized
+``` go
+func IsUnauthorized(err error) bool
+```
+IsUnauthorized reports whether err was created with Unauthorizedf() or
+NewUnauthorized().
+
+
+## func Mask
+``` go
+func Mask(other error) error
+```
+Mask hides the underlying error type, and records the location of the masking.
+
+
+## func Maskf
+``` go
+func Maskf(other error, format string, args ...interface{}) error
+```
+Mask masks the given error with the given format string and arguments (like
+fmt.Sprintf), returning a new error that maintains the error stack, but
+hides the underlying error type. The error string still contains the full
+annotations. If you want to hide the annotations, call Wrap.
+
+
+## func New
+``` go
+func New(message string) error
+```
+New is a drop in replacement for the standard libary errors module that records
+the location that the error is created.
+
+For example:
+
+
+ return errors.New("validation failed")
+
+
+## func NewAlreadyExists
+``` go
+func NewAlreadyExists(err error, msg string) error
+```
+NewAlreadyExists returns an error which wraps err and satisfies
+IsAlreadyExists().
+
+
+## func NewNotFound
+``` go
+func NewNotFound(err error, msg string) error
+```
+NewNotFound returns an error which wraps err that satisfies
+IsNotFound().
+
+
+## func NewNotImplemented
+``` go
+func NewNotImplemented(err error, msg string) error
+```
+NewNotImplemented returns an error which wraps err and satisfies
+IsNotImplemented().
+
+
+## func NewNotSupported
+``` go
+func NewNotSupported(err error, msg string) error
+```
+NewNotSupported returns an error which wraps err and satisfies
+IsNotSupported().
+
+
+## func NewNotValid
+``` go
+func NewNotValid(err error, msg string) error
+```
+NewNotValid returns an error which wraps err and satisfies IsNotValid().
+
+
+## func NewUnauthorized
+``` go
+func NewUnauthorized(err error, msg string) error
+```
+NewUnauthorized returns an error which wraps err and satisfies
+IsUnauthorized().
+
+
+## func NotFoundf
+``` go
+func NotFoundf(format string, args ...interface{}) error
+```
+NotFoundf returns an error which satisfies IsNotFound().
+
+
+## func NotImplementedf
+``` go
+func NotImplementedf(format string, args ...interface{}) error
+```
+NotImplementedf returns an error which satisfies IsNotImplemented().
+
+
+## func NotSupportedf
+``` go
+func NotSupportedf(format string, args ...interface{}) error
+```
+NotSupportedf returns an error which satisfies IsNotSupported().
+
+
+## func NotValidf
+``` go
+func NotValidf(format string, args ...interface{}) error
+```
+NotValidf returns an error which satisfies IsNotValid().
+
+
+## func Trace
+``` go
+func Trace(other error) error
+```
+Trace adds the location of the Trace call to the stack. The Cause of the
+resulting error is the same as the error parameter. If the other error is
+nil, the result will be nil.
+
+For example:
+
+
+ if err := SomeFunc(); err != nil {
+ return errors.Trace(err)
+ }
+
+
+## func Unauthorizedf
+``` go
+func Unauthorizedf(format string, args ...interface{}) error
+```
+Unauthorizedf returns an error which satisfies IsUnauthorized().
+
+
+## func Wrap
+``` go
+func Wrap(other, newDescriptive error) error
+```
+Wrap changes the Cause of the error. The location of the Wrap call is also
+stored in the error stack.
+
+For example:
+
+
+ if err := SomeFunc(); err != nil {
+ newErr := &packageError{"more context", private_value}
+ return errors.Wrap(err, newErr)
+ }
+
+
+## func Wrapf
+``` go
+func Wrapf(other, newDescriptive error, format string, args ...interface{}) error
+```
+Wrapf changes the Cause of the error, and adds an annotation. The location
+of the Wrap call is also stored in the error stack.
+
+For example:
+
+
+ if err := SomeFunc(); err != nil {
+ return errors.Wrapf(err, simpleErrorType, "invalid value %q", value)
+ }
+
+
+
+## type Err
+``` go
+type Err struct {
+ // contains filtered or unexported fields
+}
+```
+Err holds a description of an error along with information about
+where the error was created.
+
+It may be embedded in custom error types to add extra information that
+this errors package can understand.
+
+
+
+
+
+
+
+
+
+### func NewErr
+``` go
+func NewErr(format string, args ...interface{}) Err
+```
+NewErr is used to return an Err for the purpose of embedding in other
+structures. The location is not specified, and needs to be set with a call
+to SetLocation.
+
+For example:
+
+
+ type FooError struct {
+ errors.Err
+ code int
+ }
+
+ func NewFooError(code int) error {
+ err := &FooError{errors.NewErr("foo"), code}
+ err.SetLocation(1)
+ return err
+ }
+
+
+
+
+### func (\*Err) Cause
+``` go
+func (e *Err) Cause() error
+```
+The Cause of an error is the most recent error in the error stack that
+meets one of these criteria: the original error that was raised; the new
+error that was passed into the Wrap function; the most recently masked
+error; or nil if the error itself is considered the Cause. Normally this
+method is not invoked directly, but instead through the Cause stand alone
+function.
+
+
+
+### func (\*Err) Error
+``` go
+func (e *Err) Error() string
+```
+Error implements error.Error.
+
+
+
+### func (\*Err) Location
+``` go
+func (e *Err) Location() (filename string, line int)
+```
+Location is the file and line of where the error was most recently
+created or annotated.
+
+
+
+### func (\*Err) Message
+``` go
+func (e *Err) Message() string
+```
+Message returns the message stored with the most recent location. This is
+the empty string if the most recent call was Trace, or the message stored
+with Annotate or Mask.
+
+
+
+### func (\*Err) SetLocation
+``` go
+func (e *Err) SetLocation(callDepth int)
+```
+SetLocation records the source location of the error at callDepth stack
+frames above the call.
+
+
+
+### func (\*Err) StackTrace
+``` go
+func (e *Err) StackTrace() []string
+```
+StackTrace returns one string for each location recorded in the stack of
+errors. The first value is the originating error, with a line for each
+other annotation or tracing of the error.
+
+
+
+### func (\*Err) Underlying
+``` go
+func (e *Err) Underlying() error
+```
+Underlying returns the previous error in the error stack, if any. A client
+should not ever really call this method. It is used to build the error
+stack and should not be introspected by client calls. Or more
+specifically, clients should not depend on anything but the `Cause` of an
+error.
+
+
+
+
+
+
+
+
+
+- - -
+Generated by [godoc2md](http://godoc.org/github.com/davecheney/godoc2md)
\ No newline at end of file
diff --git a/automation/vendor/github.com/juju/errors/doc.go b/automation/vendor/github.com/juju/errors/doc.go
new file mode 100644
index 0000000..35b119a
--- /dev/null
+++ b/automation/vendor/github.com/juju/errors/doc.go
@@ -0,0 +1,81 @@
+// Copyright 2013, 2014 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+/*
+[godoc-link-here]
+
+The juju/errors provides an easy way to annotate errors without losing the
+orginal error context.
+
+The exported `New` and `Errorf` functions are designed to replace the
+`errors.New` and `fmt.Errorf` functions respectively. The same underlying
+error is there, but the package also records the location at which the error
+was created.
+
+A primary use case for this library is to add extra context any time an
+error is returned from a function.
+
+ if err := SomeFunc(); err != nil {
+ return err
+ }
+
+This instead becomes:
+
+ if err := SomeFunc(); err != nil {
+ return errors.Trace(err)
+ }
+
+which just records the file and line number of the Trace call, or
+
+ if err := SomeFunc(); err != nil {
+ return errors.Annotate(err, "more context")
+ }
+
+which also adds an annotation to the error.
+
+When you want to check to see if an error is of a particular type, a helper
+function is normally exported by the package that returned the error, like the
+`os` package does. The underlying cause of the error is available using the
+`Cause` function.
+
+ os.IsNotExist(errors.Cause(err))
+
+The result of the `Error()` call on an annotated error is the annotations joined
+with colons, then the result of the `Error()` method for the underlying error
+that was the cause.
+
+ err := errors.Errorf("original")
+ err = errors.Annotatef(err, "context")
+ err = errors.Annotatef(err, "more context")
+ err.Error() -> "more context: context: original"
+
+Obviously recording the file, line and functions is not very useful if you
+cannot get them back out again.
+
+ errors.ErrorStack(err)
+
+will return something like:
+
+ first error
+ github.com/juju/errors/annotation_test.go:193:
+ github.com/juju/errors/annotation_test.go:194: annotation
+ github.com/juju/errors/annotation_test.go:195:
+ github.com/juju/errors/annotation_test.go:196: more context
+ github.com/juju/errors/annotation_test.go:197:
+
+The first error was generated by an external system, so there was no location
+associated. The second, fourth, and last lines were generated with Trace calls,
+and the other two through Annotate.
+
+Sometimes when responding to an error you want to return a more specific error
+for the situation.
+
+ if err := FindField(field); err != nil {
+ return errors.Wrap(err, errors.NotFoundf(field))
+ }
+
+This returns an error where the complete error stack is still available, and
+`errors.Cause()` will return the `NotFound` error.
+
+*/
+package errors
diff --git a/automation/vendor/github.com/juju/errors/error.go b/automation/vendor/github.com/juju/errors/error.go
new file mode 100644
index 0000000..8c51c45
--- /dev/null
+++ b/automation/vendor/github.com/juju/errors/error.go
@@ -0,0 +1,145 @@
+// Copyright 2014 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+package errors
+
+import (
+ "fmt"
+ "reflect"
+ "runtime"
+)
+
+// Err holds a description of an error along with information about
+// where the error was created.
+//
+// It may be embedded in custom error types to add extra information that
+// this errors package can understand.
+type Err struct {
+ // message holds an annotation of the error.
+ message string
+
+ // cause holds the cause of the error as returned
+ // by the Cause method.
+ cause error
+
+ // previous holds the previous error in the error stack, if any.
+ previous error
+
+ // file and line hold the source code location where the error was
+ // created.
+ file string
+ line int
+}
+
+// NewErr is used to return an Err for the purpose of embedding in other
+// structures. The location is not specified, and needs to be set with a call
+// to SetLocation.
+//
+// For example:
+// type FooError struct {
+// errors.Err
+// code int
+// }
+//
+// func NewFooError(code int) error {
+// err := &FooError{errors.NewErr("foo"), code}
+// err.SetLocation(1)
+// return err
+// }
+func NewErr(format string, args ...interface{}) Err {
+ return Err{
+ message: fmt.Sprintf(format, args...),
+ }
+}
+
+// NewErrWithCause is used to return an Err with case by other error for the purpose of embedding in other
+// structures. The location is not specified, and needs to be set with a call
+// to SetLocation.
+//
+// For example:
+// type FooError struct {
+// errors.Err
+// code int
+// }
+//
+// func (e *FooError) Annotate(format string, args ...interface{}) error {
+// err := &FooError{errors.NewErrWithCause(e.Err, format, args...), e.code}
+// err.SetLocation(1)
+// return err
+// })
+func NewErrWithCause(other error, format string, args ...interface{}) Err {
+ return Err{
+ message: fmt.Sprintf(format, args...),
+ cause: Cause(other),
+ previous: other,
+ }
+}
+
+// Location is the file and line of where the error was most recently
+// created or annotated.
+func (e *Err) Location() (filename string, line int) {
+ return e.file, e.line
+}
+
+// Underlying returns the previous error in the error stack, if any. A client
+// should not ever really call this method. It is used to build the error
+// stack and should not be introspected by client calls. Or more
+// specifically, clients should not depend on anything but the `Cause` of an
+// error.
+func (e *Err) Underlying() error {
+ return e.previous
+}
+
+// The Cause of an error is the most recent error in the error stack that
+// meets one of these criteria: the original error that was raised; the new
+// error that was passed into the Wrap function; the most recently masked
+// error; or nil if the error itself is considered the Cause. Normally this
+// method is not invoked directly, but instead through the Cause stand alone
+// function.
+func (e *Err) Cause() error {
+ return e.cause
+}
+
+// Message returns the message stored with the most recent location. This is
+// the empty string if the most recent call was Trace, or the message stored
+// with Annotate or Mask.
+func (e *Err) Message() string {
+ return e.message
+}
+
+// Error implements error.Error.
+func (e *Err) Error() string {
+ // We want to walk up the stack of errors showing the annotations
+ // as long as the cause is the same.
+ err := e.previous
+ if !sameError(Cause(err), e.cause) && e.cause != nil {
+ err = e.cause
+ }
+ switch {
+ case err == nil:
+ return e.message
+ case e.message == "":
+ return err.Error()
+ }
+ return fmt.Sprintf("%s: %v", e.message, err)
+}
+
+// SetLocation records the source location of the error at callDepth stack
+// frames above the call.
+func (e *Err) SetLocation(callDepth int) {
+ _, file, line, _ := runtime.Caller(callDepth + 1)
+ e.file = trimGoPath(file)
+ e.line = line
+}
+
+// StackTrace returns one string for each location recorded in the stack of
+// errors. The first value is the originating error, with a line for each
+// other annotation or tracing of the error.
+func (e *Err) StackTrace() []string {
+ return errorStack(e)
+}
+
+// Ideally we'd have a way to check identity, but deep equals will do.
+func sameError(e1, e2 error) bool {
+ return reflect.DeepEqual(e1, e2)
+}
diff --git a/automation/vendor/github.com/juju/errors/errortypes.go b/automation/vendor/github.com/juju/errors/errortypes.go
new file mode 100644
index 0000000..10b3b19
--- /dev/null
+++ b/automation/vendor/github.com/juju/errors/errortypes.go
@@ -0,0 +1,284 @@
+// Copyright 2014 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+package errors
+
+import (
+ "fmt"
+)
+
+// wrap is a helper to construct an *wrapper.
+func wrap(err error, format, suffix string, args ...interface{}) Err {
+ newErr := Err{
+ message: fmt.Sprintf(format+suffix, args...),
+ previous: err,
+ }
+ newErr.SetLocation(2)
+ return newErr
+}
+
+// notFound represents an error when something has not been found.
+type notFound struct {
+ Err
+}
+
+// NotFoundf returns an error which satisfies IsNotFound().
+func NotFoundf(format string, args ...interface{}) error {
+ return ¬Found{wrap(nil, format, " not found", args...)}
+}
+
+// NewNotFound returns an error which wraps err that satisfies
+// IsNotFound().
+func NewNotFound(err error, msg string) error {
+ return ¬Found{wrap(err, msg, "")}
+}
+
+// IsNotFound reports whether err was created with NotFoundf() or
+// NewNotFound().
+func IsNotFound(err error) bool {
+ err = Cause(err)
+ _, ok := err.(*notFound)
+ return ok
+}
+
+// userNotFound represents an error when an inexistent user is looked up.
+type userNotFound struct {
+ Err
+}
+
+// UserNotFoundf returns an error which satisfies IsUserNotFound().
+func UserNotFoundf(format string, args ...interface{}) error {
+ return &userNotFound{wrap(nil, format, " user not found", args...)}
+}
+
+// NewUserNotFound returns an error which wraps err and satisfies
+// IsUserNotFound().
+func NewUserNotFound(err error, msg string) error {
+ return &userNotFound{wrap(err, msg, "")}
+}
+
+// IsUserNotFound reports whether err was created with UserNotFoundf() or
+// NewUserNotFound().
+func IsUserNotFound(err error) bool {
+ err = Cause(err)
+ _, ok := err.(*userNotFound)
+ return ok
+}
+
+// unauthorized represents an error when an operation is unauthorized.
+type unauthorized struct {
+ Err
+}
+
+// Unauthorizedf returns an error which satisfies IsUnauthorized().
+func Unauthorizedf(format string, args ...interface{}) error {
+ return &unauthorized{wrap(nil, format, "", args...)}
+}
+
+// NewUnauthorized returns an error which wraps err and satisfies
+// IsUnauthorized().
+func NewUnauthorized(err error, msg string) error {
+ return &unauthorized{wrap(err, msg, "")}
+}
+
+// IsUnauthorized reports whether err was created with Unauthorizedf() or
+// NewUnauthorized().
+func IsUnauthorized(err error) bool {
+ err = Cause(err)
+ _, ok := err.(*unauthorized)
+ return ok
+}
+
+// notImplemented represents an error when something is not
+// implemented.
+type notImplemented struct {
+ Err
+}
+
+// NotImplementedf returns an error which satisfies IsNotImplemented().
+func NotImplementedf(format string, args ...interface{}) error {
+ return ¬Implemented{wrap(nil, format, " not implemented", args...)}
+}
+
+// NewNotImplemented returns an error which wraps err and satisfies
+// IsNotImplemented().
+func NewNotImplemented(err error, msg string) error {
+ return ¬Implemented{wrap(err, msg, "")}
+}
+
+// IsNotImplemented reports whether err was created with
+// NotImplementedf() or NewNotImplemented().
+func IsNotImplemented(err error) bool {
+ err = Cause(err)
+ _, ok := err.(*notImplemented)
+ return ok
+}
+
+// alreadyExists represents and error when something already exists.
+type alreadyExists struct {
+ Err
+}
+
+// AlreadyExistsf returns an error which satisfies IsAlreadyExists().
+func AlreadyExistsf(format string, args ...interface{}) error {
+ return &alreadyExists{wrap(nil, format, " already exists", args...)}
+}
+
+// NewAlreadyExists returns an error which wraps err and satisfies
+// IsAlreadyExists().
+func NewAlreadyExists(err error, msg string) error {
+ return &alreadyExists{wrap(err, msg, "")}
+}
+
+// IsAlreadyExists reports whether the error was created with
+// AlreadyExistsf() or NewAlreadyExists().
+func IsAlreadyExists(err error) bool {
+ err = Cause(err)
+ _, ok := err.(*alreadyExists)
+ return ok
+}
+
+// notSupported represents an error when something is not supported.
+type notSupported struct {
+ Err
+}
+
+// NotSupportedf returns an error which satisfies IsNotSupported().
+func NotSupportedf(format string, args ...interface{}) error {
+ return ¬Supported{wrap(nil, format, " not supported", args...)}
+}
+
+// NewNotSupported returns an error which wraps err and satisfies
+// IsNotSupported().
+func NewNotSupported(err error, msg string) error {
+ return ¬Supported{wrap(err, msg, "")}
+}
+
+// IsNotSupported reports whether the error was created with
+// NotSupportedf() or NewNotSupported().
+func IsNotSupported(err error) bool {
+ err = Cause(err)
+ _, ok := err.(*notSupported)
+ return ok
+}
+
+// notValid represents an error when something is not valid.
+type notValid struct {
+ Err
+}
+
+// NotValidf returns an error which satisfies IsNotValid().
+func NotValidf(format string, args ...interface{}) error {
+ return ¬Valid{wrap(nil, format, " not valid", args...)}
+}
+
+// NewNotValid returns an error which wraps err and satisfies IsNotValid().
+func NewNotValid(err error, msg string) error {
+ return ¬Valid{wrap(err, msg, "")}
+}
+
+// IsNotValid reports whether the error was created with NotValidf() or
+// NewNotValid().
+func IsNotValid(err error) bool {
+ err = Cause(err)
+ _, ok := err.(*notValid)
+ return ok
+}
+
+// notProvisioned represents an error when something is not yet provisioned.
+type notProvisioned struct {
+ Err
+}
+
+// NotProvisionedf returns an error which satisfies IsNotProvisioned().
+func NotProvisionedf(format string, args ...interface{}) error {
+ return ¬Provisioned{wrap(nil, format, " not provisioned", args...)}
+}
+
+// NewNotProvisioned returns an error which wraps err that satisfies
+// IsNotProvisioned().
+func NewNotProvisioned(err error, msg string) error {
+ return ¬Provisioned{wrap(err, msg, "")}
+}
+
+// IsNotProvisioned reports whether err was created with NotProvisionedf() or
+// NewNotProvisioned().
+func IsNotProvisioned(err error) bool {
+ err = Cause(err)
+ _, ok := err.(*notProvisioned)
+ return ok
+}
+
+// notAssigned represents an error when something is not yet assigned to
+// something else.
+type notAssigned struct {
+ Err
+}
+
+// NotAssignedf returns an error which satisfies IsNotAssigned().
+func NotAssignedf(format string, args ...interface{}) error {
+ return ¬Assigned{wrap(nil, format, " not assigned", args...)}
+}
+
+// NewNotAssigned returns an error which wraps err that satisfies
+// IsNotAssigned().
+func NewNotAssigned(err error, msg string) error {
+ return ¬Assigned{wrap(err, msg, "")}
+}
+
+// IsNotAssigned reports whether err was created with NotAssignedf() or
+// NewNotAssigned().
+func IsNotAssigned(err error) bool {
+ err = Cause(err)
+ _, ok := err.(*notAssigned)
+ return ok
+}
+
+// badRequest represents an error when a request has bad parameters.
+type badRequest struct {
+ Err
+}
+
+// BadRequestf returns an error which satisfies IsBadRequest().
+func BadRequestf(format string, args ...interface{}) error {
+ return &badRequest{wrap(nil, format, "", args...)}
+}
+
+// NewBadRequest returns an error which wraps err that satisfies
+// IsBadRequest().
+func NewBadRequest(err error, msg string) error {
+ return &badRequest{wrap(err, msg, "")}
+}
+
+// IsBadRequest reports whether err was created with BadRequestf() or
+// NewBadRequest().
+func IsBadRequest(err error) bool {
+ err = Cause(err)
+ _, ok := err.(*badRequest)
+ return ok
+}
+
+// methodNotAllowed represents an error when an HTTP request
+// is made with an inappropriate method.
+type methodNotAllowed struct {
+ Err
+}
+
+// MethodNotAllowedf returns an error which satisfies IsMethodNotAllowed().
+func MethodNotAllowedf(format string, args ...interface{}) error {
+ return &methodNotAllowed{wrap(nil, format, "", args...)}
+}
+
+// NewMethodNotAllowed returns an error which wraps err that satisfies
+// IsMethodNotAllowed().
+func NewMethodNotAllowed(err error, msg string) error {
+ return &methodNotAllowed{wrap(err, msg, "")}
+}
+
+// IsMethodNotAllowed reports whether err was created with MethodNotAllowedf() or
+// NewMethodNotAllowed().
+func IsMethodNotAllowed(err error) bool {
+ err = Cause(err)
+ _, ok := err.(*methodNotAllowed)
+ return ok
+}
diff --git a/automation/vendor/github.com/juju/errors/functions.go b/automation/vendor/github.com/juju/errors/functions.go
new file mode 100644
index 0000000..994208d
--- /dev/null
+++ b/automation/vendor/github.com/juju/errors/functions.go
@@ -0,0 +1,330 @@
+// Copyright 2014 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+package errors
+
+import (
+ "fmt"
+ "strings"
+)
+
+// New is a drop in replacement for the standard libary errors module that records
+// the location that the error is created.
+//
+// For example:
+// return errors.New("validation failed")
+//
+func New(message string) error {
+ err := &Err{message: message}
+ err.SetLocation(1)
+ return err
+}
+
+// Errorf creates a new annotated error and records the location that the
+// error is created. This should be a drop in replacement for fmt.Errorf.
+//
+// For example:
+// return errors.Errorf("validation failed: %s", message)
+//
+func Errorf(format string, args ...interface{}) error {
+ err := &Err{message: fmt.Sprintf(format, args...)}
+ err.SetLocation(1)
+ return err
+}
+
+// Trace adds the location of the Trace call to the stack. The Cause of the
+// resulting error is the same as the error parameter. If the other error is
+// nil, the result will be nil.
+//
+// For example:
+// if err := SomeFunc(); err != nil {
+// return errors.Trace(err)
+// }
+//
+func Trace(other error) error {
+ if other == nil {
+ return nil
+ }
+ err := &Err{previous: other, cause: Cause(other)}
+ err.SetLocation(1)
+ return err
+}
+
+// Annotate is used to add extra context to an existing error. The location of
+// the Annotate call is recorded with the annotations. The file, line and
+// function are also recorded.
+//
+// For example:
+// if err := SomeFunc(); err != nil {
+// return errors.Annotate(err, "failed to frombulate")
+// }
+//
+func Annotate(other error, message string) error {
+ if other == nil {
+ return nil
+ }
+ err := &Err{
+ previous: other,
+ cause: Cause(other),
+ message: message,
+ }
+ err.SetLocation(1)
+ return err
+}
+
+// Annotatef is used to add extra context to an existing error. The location of
+// the Annotate call is recorded with the annotations. The file, line and
+// function are also recorded.
+//
+// For example:
+// if err := SomeFunc(); err != nil {
+// return errors.Annotatef(err, "failed to frombulate the %s", arg)
+// }
+//
+func Annotatef(other error, format string, args ...interface{}) error {
+ if other == nil {
+ return nil
+ }
+ err := &Err{
+ previous: other,
+ cause: Cause(other),
+ message: fmt.Sprintf(format, args...),
+ }
+ err.SetLocation(1)
+ return err
+}
+
+// DeferredAnnotatef annotates the given error (when it is not nil) with the given
+// format string and arguments (like fmt.Sprintf). If *err is nil, DeferredAnnotatef
+// does nothing. This method is used in a defer statement in order to annotate any
+// resulting error with the same message.
+//
+// For example:
+//
+// defer DeferredAnnotatef(&err, "failed to frombulate the %s", arg)
+//
+func DeferredAnnotatef(err *error, format string, args ...interface{}) {
+ if *err == nil {
+ return
+ }
+ newErr := &Err{
+ message: fmt.Sprintf(format, args...),
+ cause: Cause(*err),
+ previous: *err,
+ }
+ newErr.SetLocation(1)
+ *err = newErr
+}
+
+// Wrap changes the Cause of the error. The location of the Wrap call is also
+// stored in the error stack.
+//
+// For example:
+// if err := SomeFunc(); err != nil {
+// newErr := &packageError{"more context", private_value}
+// return errors.Wrap(err, newErr)
+// }
+//
+func Wrap(other, newDescriptive error) error {
+ err := &Err{
+ previous: other,
+ cause: newDescriptive,
+ }
+ err.SetLocation(1)
+ return err
+}
+
+// Wrapf changes the Cause of the error, and adds an annotation. The location
+// of the Wrap call is also stored in the error stack.
+//
+// For example:
+// if err := SomeFunc(); err != nil {
+// return errors.Wrapf(err, simpleErrorType, "invalid value %q", value)
+// }
+//
+func Wrapf(other, newDescriptive error, format string, args ...interface{}) error {
+ err := &Err{
+ message: fmt.Sprintf(format, args...),
+ previous: other,
+ cause: newDescriptive,
+ }
+ err.SetLocation(1)
+ return err
+}
+
+// Mask masks the given error with the given format string and arguments (like
+// fmt.Sprintf), returning a new error that maintains the error stack, but
+// hides the underlying error type. The error string still contains the full
+// annotations. If you want to hide the annotations, call Wrap.
+func Maskf(other error, format string, args ...interface{}) error {
+ if other == nil {
+ return nil
+ }
+ err := &Err{
+ message: fmt.Sprintf(format, args...),
+ previous: other,
+ }
+ err.SetLocation(1)
+ return err
+}
+
+// Mask hides the underlying error type, and records the location of the masking.
+func Mask(other error) error {
+ if other == nil {
+ return nil
+ }
+ err := &Err{
+ previous: other,
+ }
+ err.SetLocation(1)
+ return err
+}
+
+// Cause returns the cause of the given error. This will be either the
+// original error, or the result of a Wrap or Mask call.
+//
+// Cause is the usual way to diagnose errors that may have been wrapped by
+// the other errors functions.
+func Cause(err error) error {
+ var diag error
+ if err, ok := err.(causer); ok {
+ diag = err.Cause()
+ }
+ if diag != nil {
+ return diag
+ }
+ return err
+}
+
+type causer interface {
+ Cause() error
+}
+
+type wrapper interface {
+ // Message returns the top level error message,
+ // not including the message from the Previous
+ // error.
+ Message() string
+
+ // Underlying returns the Previous error, or nil
+ // if there is none.
+ Underlying() error
+}
+
+type locationer interface {
+ Location() (string, int)
+}
+
+var (
+ _ wrapper = (*Err)(nil)
+ _ locationer = (*Err)(nil)
+ _ causer = (*Err)(nil)
+)
+
+// Details returns information about the stack of errors wrapped by err, in
+// the format:
+//
+// [{filename:99: error one} {otherfile:55: cause of error one}]
+//
+// This is a terse alternative to ErrorStack as it returns a single line.
+func Details(err error) string {
+ if err == nil {
+ return "[]"
+ }
+ var s []byte
+ s = append(s, '[')
+ for {
+ s = append(s, '{')
+ if err, ok := err.(locationer); ok {
+ file, line := err.Location()
+ if file != "" {
+ s = append(s, fmt.Sprintf("%s:%d", file, line)...)
+ s = append(s, ": "...)
+ }
+ }
+ if cerr, ok := err.(wrapper); ok {
+ s = append(s, cerr.Message()...)
+ err = cerr.Underlying()
+ } else {
+ s = append(s, err.Error()...)
+ err = nil
+ }
+ s = append(s, '}')
+ if err == nil {
+ break
+ }
+ s = append(s, ' ')
+ }
+ s = append(s, ']')
+ return string(s)
+}
+
+// ErrorStack returns a string representation of the annotated error. If the
+// error passed as the parameter is not an annotated error, the result is
+// simply the result of the Error() method on that error.
+//
+// If the error is an annotated error, a multi-line string is returned where
+// each line represents one entry in the annotation stack. The full filename
+// from the call stack is used in the output.
+//
+// first error
+// github.com/juju/errors/annotation_test.go:193:
+// github.com/juju/errors/annotation_test.go:194: annotation
+// github.com/juju/errors/annotation_test.go:195:
+// github.com/juju/errors/annotation_test.go:196: more context
+// github.com/juju/errors/annotation_test.go:197:
+func ErrorStack(err error) string {
+ return strings.Join(errorStack(err), "\n")
+}
+
+func errorStack(err error) []string {
+ if err == nil {
+ return nil
+ }
+
+ // We want the first error first
+ var lines []string
+ for {
+ var buff []byte
+ if err, ok := err.(locationer); ok {
+ file, line := err.Location()
+ // Strip off the leading GOPATH/src path elements.
+ file = trimGoPath(file)
+ if file != "" {
+ buff = append(buff, fmt.Sprintf("%s:%d", file, line)...)
+ buff = append(buff, ": "...)
+ }
+ }
+ if cerr, ok := err.(wrapper); ok {
+ message := cerr.Message()
+ buff = append(buff, message...)
+ // If there is a cause for this error, and it is different to the cause
+ // of the underlying error, then output the error string in the stack trace.
+ var cause error
+ if err1, ok := err.(causer); ok {
+ cause = err1.Cause()
+ }
+ err = cerr.Underlying()
+ if cause != nil && !sameError(Cause(err), cause) {
+ if message != "" {
+ buff = append(buff, ": "...)
+ }
+ buff = append(buff, cause.Error()...)
+ }
+ } else {
+ buff = append(buff, err.Error()...)
+ err = nil
+ }
+ lines = append(lines, string(buff))
+ if err == nil {
+ break
+ }
+ }
+ // reverse the lines to get the original error, which was at the end of
+ // the list, back to the start.
+ var result []string
+ for i := len(lines); i > 0; i-- {
+ result = append(result, lines[i-1])
+ }
+ return result
+}
diff --git a/automation/vendor/github.com/juju/errors/path.go b/automation/vendor/github.com/juju/errors/path.go
new file mode 100644
index 0000000..a7b726a
--- /dev/null
+++ b/automation/vendor/github.com/juju/errors/path.go
@@ -0,0 +1,38 @@
+// Copyright 2013, 2014 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+package errors
+
+import (
+ "runtime"
+ "strings"
+)
+
+// prefixSize is used internally to trim the user specific path from the
+// front of the returned filenames from the runtime call stack.
+var prefixSize int
+
+// goPath is the deduced path based on the location of this file as compiled.
+var goPath string
+
+func init() {
+ _, file, _, ok := runtime.Caller(0)
+ if file == "?" {
+ return
+ }
+ if ok {
+ // We know that the end of the file should be:
+ // github.com/juju/errors/path.go
+ size := len(file)
+ suffix := len("github.com/juju/errors/path.go")
+ goPath = file[:size-suffix]
+ prefixSize = len(goPath)
+ }
+}
+
+func trimGoPath(filename string) string {
+ if strings.HasPrefix(filename, goPath) {
+ return filename[prefixSize:]
+ }
+ return filename
+}
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
+}
diff --git a/automation/vendor/github.com/juju/loggo/LICENSE b/automation/vendor/github.com/juju/loggo/LICENSE
new file mode 100644
index 0000000..ade9307
--- /dev/null
+++ b/automation/vendor/github.com/juju/loggo/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) 2015 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/loggo/Makefile b/automation/vendor/github.com/juju/loggo/Makefile
new file mode 100644
index 0000000..89afa49
--- /dev/null
+++ b/automation/vendor/github.com/juju/loggo/Makefile
@@ -0,0 +1,11 @@
+default: check
+
+check:
+ go test
+
+docs:
+ godoc2md github.com/juju/loggo > README.md
+ sed -i 's|\[godoc-link-here\]|[![GoDoc](https://godoc.org/github.com/juju/loggo?status.svg)](https://godoc.org/github.com/juju/loggo)|' README.md
+
+
+.PHONY: default check docs
diff --git a/automation/vendor/github.com/juju/loggo/README.md b/automation/vendor/github.com/juju/loggo/README.md
new file mode 100644
index 0000000..a73a9db
--- /dev/null
+++ b/automation/vendor/github.com/juju/loggo/README.md
@@ -0,0 +1,711 @@
+
+# loggo
+ import "github.com/juju/loggo"
+
+[![GoDoc](https://godoc.org/github.com/juju/loggo?status.svg)](https://godoc.org/github.com/juju/loggo)
+
+### Module level logging for Go
+This package provides an alternative to the standard library log package.
+
+The actual logging functions never return errors. If you are logging
+something, you really don't want to be worried about the logging
+having trouble.
+
+Modules have names that are defined by dotted strings.
+
+
+ "first.second.third"
+
+There is a root module that has the name `""`. Each module
+(except the root module) has a parent, identified by the part of
+the name without the last dotted value.
+* the parent of "first.second.third" is "first.second"
+* the parent of "first.second" is "first"
+* the parent of "first" is "" (the root module)
+
+Each module can specify its own severity level. Logging calls that are of
+a lower severity than the module's effective severity level are not written
+out.
+
+Loggers are created using the GetLogger function.
+
+
+ logger := loggo.GetLogger("foo.bar")
+
+By default there is one writer registered, which will write to Stderr,
+and the root module, which will only emit warnings and above.
+If you want to continue using the default
+logger, but have it emit all logging levels you need to do the following.
+
+
+ writer, _, err := loggo.RemoveWriter("default")
+ // err is non-nil if and only if the name isn't found.
+ loggo.RegisterWriter("default", writer, loggo.TRACE)
+
+
+
+
+## Constants
+``` go
+const DefaultWriterName = "default"
+```
+DefaultWriterName is the name of the default writer for
+a Context.
+
+
+## Variables
+``` go
+var (
+ // SeverityColor defines the colors for the levels output by the ColorWriter.
+ SeverityColor = map[Level]*ansiterm.Context{
+ TRACE: ansiterm.Foreground(ansiterm.Default),
+ DEBUG: ansiterm.Foreground(ansiterm.Green),
+ INFO: ansiterm.Foreground(ansiterm.BrightBlue),
+ WARNING: ansiterm.Foreground(ansiterm.Yellow),
+ ERROR: ansiterm.Foreground(ansiterm.BrightRed),
+ CRITICAL: &ansiterm.Context{
+ Foreground: ansiterm.White,
+ Background: ansiterm.Red,
+ },
+ }
+ // LocationColor defines the colors for the location output by the ColorWriter.
+ LocationColor = ansiterm.Foreground(ansiterm.BrightBlue)
+)
+```
+``` go
+var TimeFormat = initTimeFormat()
+```
+TimeFormat is the time format used for the default writer.
+This can be set with the environment variable LOGGO_TIME_FORMAT.
+
+
+## func ConfigureLoggers
+``` go
+func ConfigureLoggers(specification string) error
+```
+ConfigureLoggers configures loggers according to the given string
+specification, which specifies a set of modules and their associated
+logging levels. Loggers are colon- or semicolon-separated; each
+module is specified as <modulename>=<level>. White space outside of
+module names and levels is ignored. The root module is specified
+with the name "<root>".
+
+An example specification:
+
+
+ `<root>=ERROR; foo.bar=WARNING`
+
+
+## func DefaultFormatter
+``` go
+func DefaultFormatter(entry Entry) string
+```
+DefaultFormatter returns the parameters separated by spaces except for
+filename and line which are separated by a colon. The timestamp is shown
+to second resolution in UTC. For example:
+
+
+ 2016-07-02 15:04:05
+
+
+## func LoggerInfo
+``` go
+func LoggerInfo() string
+```
+LoggerInfo returns information about the configured loggers and their
+logging levels. The information is returned in the format expected by
+ConfigureLoggers. Loggers with UNSPECIFIED level will not
+be included.
+
+
+## func RegisterWriter
+``` go
+func RegisterWriter(name string, writer Writer) error
+```
+RegisterWriter adds the writer to the list of writers in the DefaultContext
+that get notified when logging. If there is already a registered writer
+with that name, an error is returned.
+
+
+## func ResetLogging
+``` go
+func ResetLogging()
+```
+ResetLogging iterates through the known modules and sets the levels of all
+to UNSPECIFIED, except for <root> which is set to WARNING. The call also
+removes all writers in the DefaultContext and puts the original default
+writer back as the only writer.
+
+
+## func ResetWriters
+``` go
+func ResetWriters()
+```
+ResetWriters puts the list of writers back into the initial state.
+
+
+
+## type Config
+``` go
+type Config map[string]Level
+```
+Config is a mapping of logger module names to logging severity levels.
+
+
+
+
+
+
+
+
+
+### func ParseConfigString
+``` go
+func ParseConfigString(specification string) (Config, error)
+```
+ParseConfigString parses a logger configuration string into a map of logger
+names and their associated log level. This method is provided to allow
+other programs to pre-validate a configuration string rather than just
+calling ConfigureLoggers.
+
+Logging modules are colon- or semicolon-separated; each module is specified
+as <modulename>=<level>. White space outside of module names and levels is
+ignored. The root module is specified with the name "<root>".
+
+As a special case, a log level may be specified on its own.
+This is equivalent to specifying the level of the root module,
+so "DEBUG" is equivalent to `<root>=DEBUG`
+
+An example specification:
+
+
+ `<root>=ERROR; foo.bar=WARNING`
+
+
+
+
+### func (Config) String
+``` go
+func (c Config) String() string
+```
+String returns a logger configuration string that may be parsed
+using ParseConfigurationString.
+
+
+
+## type Context
+``` go
+type Context struct {
+ // contains filtered or unexported fields
+}
+```
+Context produces loggers for a hierarchy of modules. The context holds
+a collection of hierarchical loggers and their writers.
+
+
+
+
+
+
+
+
+
+### func DefaultContext
+``` go
+func DefaultContext() *Context
+```
+DefaultContext returns the global default logging context.
+
+
+### func NewContext
+``` go
+func NewContext(rootLevel Level) *Context
+```
+NewLoggers returns a new Context with no writers set.
+If the root level is UNSPECIFIED, WARNING is used.
+
+
+
+
+### func (\*Context) AddWriter
+``` go
+func (c *Context) AddWriter(name string, writer Writer) error
+```
+AddWriter adds a writer to the list to be called for each logging call.
+The name cannot be empty, and the writer cannot be nil. If an existing
+writer exists with the specified name, an error is returned.
+
+
+
+### func (\*Context) ApplyConfig
+``` go
+func (c *Context) ApplyConfig(config Config)
+```
+ApplyConfig configures the logging modules according to the provided config.
+
+
+
+### func (\*Context) CompleteConfig
+``` go
+func (c *Context) CompleteConfig() Config
+```
+CompleteConfig returns all the loggers and their defined levels,
+even if that level is UNSPECIFIED.
+
+
+
+### func (\*Context) Config
+``` go
+func (c *Context) Config() Config
+```
+Config returns the current configuration of the Loggers. Loggers
+with UNSPECIFIED level will not be included.
+
+
+
+### func (\*Context) GetLogger
+``` go
+func (c *Context) GetLogger(name string) Logger
+```
+GetLogger returns a Logger for the given module name, creating it and
+its parents if necessary.
+
+
+
+### func (\*Context) RemoveWriter
+``` go
+func (c *Context) RemoveWriter(name string) (Writer, error)
+```
+RemoveWriter remotes the specified writer. If a writer is not found with
+the specified name an error is returned. The writer that was removed is also
+returned.
+
+
+
+### func (\*Context) ReplaceWriter
+``` go
+func (c *Context) ReplaceWriter(name string, writer Writer) (Writer, error)
+```
+ReplaceWriter is a convenience method that does the equivalent of RemoveWriter
+followed by AddWriter with the same name. The replaced writer is returned.
+
+
+
+### func (\*Context) ResetLoggerLevels
+``` go
+func (c *Context) ResetLoggerLevels()
+```
+ResetLoggerLevels iterates through the known logging modules and sets the
+levels of all to UNSPECIFIED, except for <root> which is set to WARNING.
+
+
+
+### func (\*Context) ResetWriters
+``` go
+func (c *Context) ResetWriters()
+```
+ResetWriters is generally only used in testing and removes all the writers.
+
+
+
+## type Entry
+``` go
+type Entry struct {
+ // Level is the severity of the log message.
+ Level Level
+ // Module is the dotted module name from the logger.
+ Module string
+ // Filename is the full path the file that logged the message.
+ Filename string
+ // Line is the line number of the Filename.
+ Line int
+ // Timestamp is when the log message was created
+ Timestamp time.Time
+ // Message is the formatted string from teh log call.
+ Message string
+}
+```
+Entry represents a single log message.
+
+
+
+
+
+
+
+
+
+
+
+## type Level
+``` go
+type Level uint32
+```
+Level holds a severity level.
+
+
+
+``` go
+const (
+ UNSPECIFIED Level = iota
+ TRACE
+ DEBUG
+ INFO
+ WARNING
+ ERROR
+ CRITICAL
+)
+```
+The severity levels. Higher values are more considered more
+important.
+
+
+
+
+
+
+
+### func ParseLevel
+``` go
+func ParseLevel(level string) (Level, bool)
+```
+ParseLevel converts a string representation of a logging level to a
+Level. It returns the level and whether it was valid or not.
+
+
+
+
+### func (Level) Short
+``` go
+func (level Level) Short() string
+```
+Short returns a five character string to use in
+aligned logging output.
+
+
+
+### func (Level) String
+``` go
+func (level Level) String() string
+```
+String implements Stringer.
+
+
+
+## type Logger
+``` go
+type Logger struct {
+ // contains filtered or unexported fields
+}
+```
+A Logger represents a logging module. It has an associated logging
+level which can be changed; messages of lesser severity will
+be dropped. Loggers have a hierarchical relationship - see
+the package documentation.
+
+The zero Logger value is usable - any messages logged
+to it will be sent to the root Logger.
+
+
+
+
+
+
+
+
+
+### func GetLogger
+``` go
+func GetLogger(name string) Logger
+```
+GetLogger returns a Logger for the given module name,
+creating it and its parents if necessary.
+
+
+
+
+### func (Logger) Criticalf
+``` go
+func (logger Logger) Criticalf(message string, args ...interface{})
+```
+Criticalf logs the printf-formatted message at critical level.
+
+
+
+### func (Logger) Debugf
+``` go
+func (logger Logger) Debugf(message string, args ...interface{})
+```
+Debugf logs the printf-formatted message at debug level.
+
+
+
+### func (Logger) EffectiveLogLevel
+``` go
+func (logger Logger) EffectiveLogLevel() Level
+```
+EffectiveLogLevel returns the effective min log level of
+the receiver - that is, messages with a lesser severity
+level will be discarded.
+
+If the log level of the receiver is unspecified,
+it will be taken from the effective log level of its
+parent.
+
+
+
+### func (Logger) Errorf
+``` go
+func (logger Logger) Errorf(message string, args ...interface{})
+```
+Errorf logs the printf-formatted message at error level.
+
+
+
+### func (Logger) Infof
+``` go
+func (logger Logger) Infof(message string, args ...interface{})
+```
+Infof logs the printf-formatted message at info level.
+
+
+
+### func (Logger) IsDebugEnabled
+``` go
+func (logger Logger) IsDebugEnabled() bool
+```
+IsDebugEnabled returns whether debugging is enabled
+at debug level.
+
+
+
+### func (Logger) IsErrorEnabled
+``` go
+func (logger Logger) IsErrorEnabled() bool
+```
+IsErrorEnabled returns whether debugging is enabled
+at error level.
+
+
+
+### func (Logger) IsInfoEnabled
+``` go
+func (logger Logger) IsInfoEnabled() bool
+```
+IsInfoEnabled returns whether debugging is enabled
+at info level.
+
+
+
+### func (Logger) IsLevelEnabled
+``` go
+func (logger Logger) IsLevelEnabled(level Level) bool
+```
+IsLevelEnabled returns whether debugging is enabled
+for the given log level.
+
+
+
+### func (Logger) IsTraceEnabled
+``` go
+func (logger Logger) IsTraceEnabled() bool
+```
+IsTraceEnabled returns whether debugging is enabled
+at trace level.
+
+
+
+### func (Logger) IsWarningEnabled
+``` go
+func (logger Logger) IsWarningEnabled() bool
+```
+IsWarningEnabled returns whether debugging is enabled
+at warning level.
+
+
+
+### func (Logger) LogCallf
+``` go
+func (logger Logger) LogCallf(calldepth int, level Level, message string, args ...interface{})
+```
+LogCallf logs a printf-formatted message at the given level.
+The location of the call is indicated by the calldepth argument.
+A calldepth of 1 means the function that called this function.
+A message will be discarded if level is less than the
+the effective log level of the logger.
+Note that the writers may also filter out messages that
+are less than their registered minimum severity level.
+
+
+
+### func (Logger) LogLevel
+``` go
+func (logger Logger) LogLevel() Level
+```
+LogLevel returns the configured min log level of the logger.
+
+
+
+### func (Logger) Logf
+``` go
+func (logger Logger) Logf(level Level, message string, args ...interface{})
+```
+Logf logs a printf-formatted message at the given level.
+A message will be discarded if level is less than the
+the effective log level of the logger.
+Note that the writers may also filter out messages that
+are less than their registered minimum severity level.
+
+
+
+### func (Logger) Name
+``` go
+func (logger Logger) Name() string
+```
+Name returns the logger's module name.
+
+
+
+### func (Logger) SetLogLevel
+``` go
+func (logger Logger) SetLogLevel(level Level)
+```
+SetLogLevel sets the severity level of the given logger.
+The root logger cannot be set to UNSPECIFIED level.
+See EffectiveLogLevel for how this affects the
+actual messages logged.
+
+
+
+### func (Logger) Tracef
+``` go
+func (logger Logger) Tracef(message string, args ...interface{})
+```
+Tracef logs the printf-formatted message at trace level.
+
+
+
+### func (Logger) Warningf
+``` go
+func (logger Logger) Warningf(message string, args ...interface{})
+```
+Warningf logs the printf-formatted message at warning level.
+
+
+
+## type TestWriter
+``` go
+type TestWriter struct {
+ // contains filtered or unexported fields
+}
+```
+TestWriter is a useful Writer for testing purposes. Each component of the
+logging message is stored in the Log array.
+
+
+
+
+
+
+
+
+
+
+
+### func (\*TestWriter) Clear
+``` go
+func (writer *TestWriter) Clear()
+```
+Clear removes any saved log messages.
+
+
+
+### func (\*TestWriter) Log
+``` go
+func (writer *TestWriter) Log() []Entry
+```
+Log returns a copy of the current logged values.
+
+
+
+### func (\*TestWriter) Write
+``` go
+func (writer *TestWriter) Write(entry Entry)
+```
+Write saves the params as members in the TestLogValues struct appended to the Log array.
+
+
+
+## type Writer
+``` go
+type Writer interface {
+ // Write writes a message to the Writer with the given level and module
+ // name. The filename and line hold the file name and line number of the
+ // code that is generating the log message; the time stamp holds the time
+ // the log message was generated, and message holds the log message
+ // itself.
+ Write(entry Entry)
+}
+```
+Writer is implemented by any recipient of log messages.
+
+
+
+
+
+
+
+
+
+### func NewColorWriter
+``` go
+func NewColorWriter(writer io.Writer) Writer
+```
+NewColorWriter will write out colored severity levels if the writer is
+outputting to a terminal.
+
+
+### func NewMinimumLevelWriter
+``` go
+func NewMinimumLevelWriter(writer Writer, minLevel Level) Writer
+```
+NewMinLevelWriter returns a Writer that will only pass on the Write calls
+to the provided writer if the log level is at or above the specified
+minimum level.
+
+
+### func NewSimpleWriter
+``` go
+func NewSimpleWriter(writer io.Writer, formatter func(entry Entry) string) Writer
+```
+NewSimpleWriter returns a new writer that writes log messages to the given
+io.Writer formatting the messages with the given formatter.
+
+
+### func RemoveWriter
+``` go
+func RemoveWriter(name string) (Writer, error)
+```
+RemoveWriter removes the Writer identified by 'name' and returns it.
+If the Writer is not found, an error is returned.
+
+
+### func ReplaceDefaultWriter
+``` go
+func ReplaceDefaultWriter(writer Writer) (Writer, error)
+```
+ReplaceDefaultWriter is a convenience method that does the equivalent of
+RemoveWriter and then RegisterWriter with the name "default". The previous
+default writer, if any is returned.
+
+
+
+
+
+
+
+
+
+
+- - -
+Generated by [godoc2md](http://godoc.org/github.com/davecheney/godoc2md)
\ No newline at end of file
diff --git a/automation/vendor/github.com/juju/loggo/config.go b/automation/vendor/github.com/juju/loggo/config.go
new file mode 100644
index 0000000..1b3eaa5
--- /dev/null
+++ b/automation/vendor/github.com/juju/loggo/config.go
@@ -0,0 +1,96 @@
+// Copyright 2016 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+package loggo
+
+import (
+ "fmt"
+ "sort"
+ "strings"
+)
+
+// Config is a mapping of logger module names to logging severity levels.
+type Config map[string]Level
+
+// String returns a logger configuration string that may be parsed
+// using ParseConfigurationString.
+func (c Config) String() string {
+ if c == nil {
+ return ""
+ }
+ // output in alphabetical order.
+ names := []string{}
+ for name := range c {
+ names = append(names, name)
+ }
+ sort.Strings(names)
+
+ var entries []string
+ for _, name := range names {
+ level := c[name]
+ if name == "" {
+ name = rootString
+ }
+ entry := fmt.Sprintf("%s=%s", name, level)
+ entries = append(entries, entry)
+ }
+ return strings.Join(entries, ";")
+}
+
+func parseConfigValue(value string) (string, Level, error) {
+ pair := strings.SplitN(value, "=", 2)
+ if len(pair) < 2 {
+ return "", UNSPECIFIED, fmt.Errorf("config value expected '=', found %q", value)
+ }
+ name := strings.TrimSpace(pair[0])
+ if name == "" {
+ return "", UNSPECIFIED, fmt.Errorf("config value %q has missing module name", value)
+ }
+
+ levelStr := strings.TrimSpace(pair[1])
+ level, ok := ParseLevel(levelStr)
+ if !ok {
+ return "", UNSPECIFIED, fmt.Errorf("unknown severity level %q", levelStr)
+ }
+ if name == rootString {
+ name = ""
+ }
+ return name, level, nil
+}
+
+// ParseConfigString parses a logger configuration string into a map of logger
+// names and their associated log level. This method is provided to allow
+// other programs to pre-validate a configuration string rather than just
+// calling ConfigureLoggers.
+//
+// Logging modules are colon- or semicolon-separated; each module is specified
+// as <modulename>=<level>. White space outside of module names and levels is
+// ignored. The root module is specified with the name "<root>".
+//
+// As a special case, a log level may be specified on its own.
+// This is equivalent to specifying the level of the root module,
+// so "DEBUG" is equivalent to `<root>=DEBUG`
+//
+// An example specification:
+// `<root>=ERROR; foo.bar=WARNING`
+func ParseConfigString(specification string) (Config, error) {
+ specification = strings.TrimSpace(specification)
+ if specification == "" {
+ return nil, nil
+ }
+ cfg := make(Config)
+ if level, ok := ParseLevel(specification); ok {
+ cfg[""] = level
+ return cfg, nil
+ }
+
+ values := strings.FieldsFunc(specification, func(r rune) bool { return r == ';' || r == ':' })
+ for _, value := range values {
+ name, level, err := parseConfigValue(value)
+ if err != nil {
+ return nil, err
+ }
+ cfg[name] = level
+ }
+ return cfg, nil
+}
diff --git a/automation/vendor/github.com/juju/loggo/context.go b/automation/vendor/github.com/juju/loggo/context.go
new file mode 100644
index 0000000..f5739d9
--- /dev/null
+++ b/automation/vendor/github.com/juju/loggo/context.go
@@ -0,0 +1,198 @@
+// Copyright 2016 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+package loggo
+
+import (
+ "fmt"
+ "strings"
+ "sync"
+)
+
+// Context produces loggers for a hierarchy of modules. The context holds
+// a collection of hierarchical loggers and their writers.
+type Context struct {
+ root *module
+
+ // Perhaps have one mutex?
+ modulesMutex sync.Mutex
+ modules map[string]*module
+
+ writersMutex sync.Mutex
+ writers map[string]Writer
+
+ // writeMuxtex is used to serialise write operations.
+ writeMutex sync.Mutex
+}
+
+// NewLoggers returns a new Context with no writers set.
+// If the root level is UNSPECIFIED, WARNING is used.
+func NewContext(rootLevel Level) *Context {
+ if rootLevel < TRACE || rootLevel > CRITICAL {
+ rootLevel = WARNING
+ }
+ context := &Context{
+ modules: make(map[string]*module),
+ writers: make(map[string]Writer),
+ }
+ context.root = &module{
+ level: rootLevel,
+ context: context,
+ }
+ context.modules[""] = context.root
+ return context
+}
+
+// GetLogger returns a Logger for the given module name, creating it and
+// its parents if necessary.
+func (c *Context) GetLogger(name string) Logger {
+ name = strings.TrimSpace(strings.ToLower(name))
+ c.modulesMutex.Lock()
+ defer c.modulesMutex.Unlock()
+ return Logger{c.getLoggerModule(name)}
+}
+
+func (c *Context) getLoggerModule(name string) *module {
+ if name == rootString {
+ name = ""
+ }
+ impl, found := c.modules[name]
+ if found {
+ return impl
+ }
+ parentName := ""
+ if i := strings.LastIndex(name, "."); i >= 0 {
+ parentName = name[0:i]
+ }
+ parent := c.getLoggerModule(parentName)
+ impl = &module{name, UNSPECIFIED, parent, c}
+ c.modules[name] = impl
+ return impl
+}
+
+// Config returns the current configuration of the Loggers. Loggers
+// with UNSPECIFIED level will not be included.
+func (c *Context) Config() Config {
+ result := make(Config)
+ c.modulesMutex.Lock()
+ defer c.modulesMutex.Unlock()
+
+ for name, module := range c.modules {
+ if module.level != UNSPECIFIED {
+ result[name] = module.level
+ }
+ }
+ return result
+}
+
+// CompleteConfig returns all the loggers and their defined levels,
+// even if that level is UNSPECIFIED.
+func (c *Context) CompleteConfig() Config {
+ result := make(Config)
+ c.modulesMutex.Lock()
+ defer c.modulesMutex.Unlock()
+
+ for name, module := range c.modules {
+ result[name] = module.level
+ }
+ return result
+}
+
+// ApplyConfig configures the logging modules according to the provided config.
+func (c *Context) ApplyConfig(config Config) {
+ c.modulesMutex.Lock()
+ defer c.modulesMutex.Unlock()
+ for name, level := range config {
+ module := c.getLoggerModule(name)
+ module.setLevel(level)
+ }
+}
+
+// ResetLoggerLevels iterates through the known logging modules and sets the
+// levels of all to UNSPECIFIED, except for <root> which is set to WARNING.
+func (c *Context) ResetLoggerLevels() {
+ c.modulesMutex.Lock()
+ defer c.modulesMutex.Unlock()
+ // Setting the root module to UNSPECIFIED will set it to WARNING.
+ for _, module := range c.modules {
+ module.setLevel(UNSPECIFIED)
+ }
+}
+
+func (c *Context) write(entry Entry) {
+ c.writeMutex.Lock()
+ defer c.writeMutex.Unlock()
+ for _, writer := range c.getWriters() {
+ writer.Write(entry)
+ }
+}
+
+func (c *Context) getWriters() []Writer {
+ c.writersMutex.Lock()
+ defer c.writersMutex.Unlock()
+ var result []Writer
+ for _, writer := range c.writers {
+ result = append(result, writer)
+ }
+ return result
+}
+
+// AddWriter adds a writer to the list to be called for each logging call.
+// The name cannot be empty, and the writer cannot be nil. If an existing
+// writer exists with the specified name, an error is returned.
+func (c *Context) AddWriter(name string, writer Writer) error {
+ if name == "" {
+ return fmt.Errorf("name cannot be empty")
+ }
+ if writer == nil {
+ return fmt.Errorf("writer cannot be nil")
+ }
+ c.writersMutex.Lock()
+ defer c.writersMutex.Unlock()
+ if _, found := c.writers[name]; found {
+ return fmt.Errorf("context already has a writer named %q", name)
+ }
+ c.writers[name] = writer
+ return nil
+}
+
+// RemoveWriter remotes the specified writer. If a writer is not found with
+// the specified name an error is returned. The writer that was removed is also
+// returned.
+func (c *Context) RemoveWriter(name string) (Writer, error) {
+ c.writersMutex.Lock()
+ defer c.writersMutex.Unlock()
+ reg, found := c.writers[name]
+ if !found {
+ return nil, fmt.Errorf("context has no writer named %q", name)
+ }
+ delete(c.writers, name)
+ return reg, nil
+}
+
+// ReplaceWriter is a convenience method that does the equivalent of RemoveWriter
+// followed by AddWriter with the same name. The replaced writer is returned.
+func (c *Context) ReplaceWriter(name string, writer Writer) (Writer, error) {
+ if name == "" {
+ return nil, fmt.Errorf("name cannot be empty")
+ }
+ if writer == nil {
+ return nil, fmt.Errorf("writer cannot be nil")
+ }
+ c.writersMutex.Lock()
+ defer c.writersMutex.Unlock()
+ reg, found := c.writers[name]
+ if !found {
+ return nil, fmt.Errorf("context has no writer named %q", name)
+ }
+ oldWriter := reg
+ c.writers[name] = writer
+ return oldWriter, nil
+}
+
+// ResetWriters is generally only used in testing and removes all the writers.
+func (c *Context) ResetWriters() {
+ c.writersMutex.Lock()
+ defer c.writersMutex.Unlock()
+ c.writers = make(map[string]Writer)
+}
diff --git a/automation/vendor/github.com/juju/loggo/dependencies.tsv b/automation/vendor/github.com/juju/loggo/dependencies.tsv
new file mode 100644
index 0000000..20daf32
--- /dev/null
+++ b/automation/vendor/github.com/juju/loggo/dependencies.tsv
@@ -0,0 +1,5 @@
+github.com/juju/ansiterm git c368f42cb4b32a70389cded05c7345d9ccdce889 2016-08-17T02:52:20Z
+github.com/lunixbochs/vtclean git 4fbf7632a2c6d3fbdb9931439bdbbeded02cbe36 2016-01-25T03:51:06Z
+github.com/mattn/go-colorable git ed8eb9e318d7a84ce5915b495b7d35e0cfe7b5a8 2016-07-31T23:54:17Z
+github.com/mattn/go-isatty git 66b8e73f3f5cda9f96b69efd03dd3d7fc4a5cdb8 2016-08-06T12:27:52Z
+gopkg.in/check.v1 git 4f90aeace3a26ad7021961c297b22c42160c7b25 2016-01-05T16:49:36Z
diff --git a/automation/vendor/github.com/juju/loggo/doc.go b/automation/vendor/github.com/juju/loggo/doc.go
new file mode 100644
index 0000000..bb19cd5
--- /dev/null
+++ b/automation/vendor/github.com/juju/loggo/doc.go
@@ -0,0 +1,42 @@
+// Copyright 2014 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+/*
+[godoc-link-here]
+
+Module level logging for Go
+
+This package provides an alternative to the standard library log package.
+
+The actual logging functions never return errors. If you are logging
+something, you really don't want to be worried about the logging
+having trouble.
+
+Modules have names that are defined by dotted strings.
+ "first.second.third"
+
+There is a root module that has the name `""`. Each module
+(except the root module) has a parent, identified by the part of
+the name without the last dotted value.
+* the parent of "first.second.third" is "first.second"
+* the parent of "first.second" is "first"
+* the parent of "first" is "" (the root module)
+
+Each module can specify its own severity level. Logging calls that are of
+a lower severity than the module's effective severity level are not written
+out.
+
+Loggers are created using the GetLogger function.
+ logger := loggo.GetLogger("foo.bar")
+
+By default there is one writer registered, which will write to Stderr,
+and the root module, which will only emit warnings and above.
+If you want to continue using the default
+logger, but have it emit all logging levels you need to do the following.
+
+ writer, _, err := loggo.RemoveWriter("default")
+ // err is non-nil if and only if the name isn't found.
+ loggo.RegisterWriter("default", writer, loggo.TRACE)
+
+*/
+package loggo
diff --git a/automation/vendor/github.com/juju/loggo/entry.go b/automation/vendor/github.com/juju/loggo/entry.go
new file mode 100644
index 0000000..b61aa54
--- /dev/null
+++ b/automation/vendor/github.com/juju/loggo/entry.go
@@ -0,0 +1,22 @@
+// Copyright 2016 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+package loggo
+
+import "time"
+
+// Entry represents a single log message.
+type Entry struct {
+ // Level is the severity of the log message.
+ Level Level
+ // Module is the dotted module name from the logger.
+ Module string
+ // Filename is the full path the file that logged the message.
+ Filename string
+ // Line is the line number of the Filename.
+ Line int
+ // Timestamp is when the log message was created
+ Timestamp time.Time
+ // Message is the formatted string from teh log call.
+ Message string
+}
diff --git a/automation/vendor/github.com/juju/loggo/formatter.go b/automation/vendor/github.com/juju/loggo/formatter.go
new file mode 100644
index 0000000..ef8aa7a
--- /dev/null
+++ b/automation/vendor/github.com/juju/loggo/formatter.go
@@ -0,0 +1,38 @@
+// Copyright 2014 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+package loggo
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "time"
+)
+
+// DefaultFormatter returns the parameters separated by spaces except for
+// filename and line which are separated by a colon. The timestamp is shown
+// to second resolution in UTC. For example:
+// 2016-07-02 15:04:05
+func DefaultFormatter(entry Entry) string {
+ ts := entry.Timestamp.In(time.UTC).Format("2006-01-02 15:04:05")
+ // Just get the basename from the filename
+ filename := filepath.Base(entry.Filename)
+ return fmt.Sprintf("%s %s %s %s:%d %s", ts, entry.Level, entry.Module, filename, entry.Line, entry.Message)
+}
+
+// TimeFormat is the time format used for the default writer.
+// This can be set with the environment variable LOGGO_TIME_FORMAT.
+var TimeFormat = initTimeFormat()
+
+func initTimeFormat() string {
+ format := os.Getenv("LOGGO_TIME_FORMAT")
+ if format != "" {
+ return format
+ }
+ return "15:04:05"
+}
+
+func formatTime(ts time.Time) string {
+ return ts.Format(TimeFormat)
+}
diff --git a/automation/vendor/github.com/juju/loggo/global.go b/automation/vendor/github.com/juju/loggo/global.go
new file mode 100644
index 0000000..7cf95ca
--- /dev/null
+++ b/automation/vendor/github.com/juju/loggo/global.go
@@ -0,0 +1,85 @@
+// Copyright 2016 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+package loggo
+
+var (
+ defaultContext = newDefaultContxt()
+)
+
+func newDefaultContxt() *Context {
+ ctx := NewContext(WARNING)
+ ctx.AddWriter(DefaultWriterName, defaultWriter())
+ return ctx
+}
+
+// DefaultContext returns the global default logging context.
+func DefaultContext() *Context {
+ return defaultContext
+}
+
+// LoggerInfo returns information about the configured loggers and their
+// logging levels. The information is returned in the format expected by
+// ConfigureLoggers. Loggers with UNSPECIFIED level will not
+// be included.
+func LoggerInfo() string {
+ return defaultContext.Config().String()
+}
+
+// GetLogger returns a Logger for the given module name,
+// creating it and its parents if necessary.
+func GetLogger(name string) Logger {
+ return defaultContext.GetLogger(name)
+}
+
+// ResetLogging iterates through the known modules and sets the levels of all
+// to UNSPECIFIED, except for <root> which is set to WARNING. The call also
+// removes all writers in the DefaultContext and puts the original default
+// writer back as the only writer.
+func ResetLogging() {
+ defaultContext.ResetLoggerLevels()
+ defaultContext.ResetWriters()
+}
+
+// ResetWriters puts the list of writers back into the initial state.
+func ResetWriters() {
+ defaultContext.ResetWriters()
+}
+
+// ReplaceDefaultWriter is a convenience method that does the equivalent of
+// RemoveWriter and then RegisterWriter with the name "default". The previous
+// default writer, if any is returned.
+func ReplaceDefaultWriter(writer Writer) (Writer, error) {
+ return defaultContext.ReplaceWriter(DefaultWriterName, writer)
+}
+
+// RegisterWriter adds the writer to the list of writers in the DefaultContext
+// that get notified when logging. If there is already a registered writer
+// with that name, an error is returned.
+func RegisterWriter(name string, writer Writer) error {
+ return defaultContext.AddWriter(name, writer)
+}
+
+// RemoveWriter removes the Writer identified by 'name' and returns it.
+// If the Writer is not found, an error is returned.
+func RemoveWriter(name string) (Writer, error) {
+ return defaultContext.RemoveWriter(name)
+}
+
+// ConfigureLoggers configures loggers according to the given string
+// specification, which specifies a set of modules and their associated
+// logging levels. Loggers are colon- or semicolon-separated; each
+// module is specified as <modulename>=<level>. White space outside of
+// module names and levels is ignored. The root module is specified
+// with the name "<root>".
+//
+// An example specification:
+// `<root>=ERROR; foo.bar=WARNING`
+func ConfigureLoggers(specification string) error {
+ config, err := ParseConfigString(specification)
+ if err != nil {
+ return err
+ }
+ defaultContext.ApplyConfig(config)
+ return nil
+}
diff --git a/automation/vendor/github.com/juju/loggo/level.go b/automation/vendor/github.com/juju/loggo/level.go
new file mode 100644
index 0000000..f6a5c4f
--- /dev/null
+++ b/automation/vendor/github.com/juju/loggo/level.go
@@ -0,0 +1,102 @@
+// Copyright 2016 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+package loggo
+
+import (
+ "strings"
+ "sync/atomic"
+)
+
+// The severity levels. Higher values are more considered more
+// important.
+const (
+ UNSPECIFIED Level = iota
+ TRACE
+ DEBUG
+ INFO
+ WARNING
+ ERROR
+ CRITICAL
+)
+
+// Level holds a severity level.
+type Level uint32
+
+// ParseLevel converts a string representation of a logging level to a
+// Level. It returns the level and whether it was valid or not.
+func ParseLevel(level string) (Level, bool) {
+ level = strings.ToUpper(level)
+ switch level {
+ case "UNSPECIFIED":
+ return UNSPECIFIED, true
+ case "TRACE":
+ return TRACE, true
+ case "DEBUG":
+ return DEBUG, true
+ case "INFO":
+ return INFO, true
+ case "WARN", "WARNING":
+ return WARNING, true
+ case "ERROR":
+ return ERROR, true
+ case "CRITICAL":
+ return CRITICAL, true
+ default:
+ return UNSPECIFIED, false
+ }
+}
+
+// String implements Stringer.
+func (level Level) String() string {
+ switch level {
+ case UNSPECIFIED:
+ return "UNSPECIFIED"
+ case TRACE:
+ return "TRACE"
+ case DEBUG:
+ return "DEBUG"
+ case INFO:
+ return "INFO"
+ case WARNING:
+ return "WARNING"
+ case ERROR:
+ return "ERROR"
+ case CRITICAL:
+ return "CRITICAL"
+ default:
+ return "<unknown>"
+ }
+}
+
+// Short returns a five character string to use in
+// aligned logging output.
+func (level Level) Short() string {
+ switch level {
+ case TRACE:
+ return "TRACE"
+ case DEBUG:
+ return "DEBUG"
+ case INFO:
+ return "INFO "
+ case WARNING:
+ return "WARN "
+ case ERROR:
+ return "ERROR"
+ case CRITICAL:
+ return "CRITC"
+ default:
+ return " "
+ }
+}
+
+// get atomically gets the value of the given level.
+func (level *Level) get() Level {
+ return Level(atomic.LoadUint32((*uint32)(level)))
+}
+
+// set atomically sets the value of the receiver
+// to the given level.
+func (level *Level) set(newLevel Level) {
+ atomic.StoreUint32((*uint32)(level), uint32(newLevel))
+}
diff --git a/automation/vendor/github.com/juju/loggo/logger.go b/automation/vendor/github.com/juju/loggo/logger.go
new file mode 100644
index 0000000..fbdfd9e
--- /dev/null
+++ b/automation/vendor/github.com/juju/loggo/logger.go
@@ -0,0 +1,176 @@
+// Copyright 2014 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+package loggo
+
+import (
+ "fmt"
+ "runtime"
+ "time"
+)
+
+// A Logger represents a logging module. It has an associated logging
+// level which can be changed; messages of lesser severity will
+// be dropped. Loggers have a hierarchical relationship - see
+// the package documentation.
+//
+// The zero Logger value is usable - any messages logged
+// to it will be sent to the root Logger.
+type Logger struct {
+ impl *module
+}
+
+func (logger Logger) getModule() *module {
+ if logger.impl == nil {
+ return defaultContext.root
+ }
+ return logger.impl
+}
+
+// Name returns the logger's module name.
+func (logger Logger) Name() string {
+ return logger.getModule().Name()
+}
+
+// LogLevel returns the configured min log level of the logger.
+func (logger Logger) LogLevel() Level {
+ return logger.getModule().level
+}
+
+// EffectiveLogLevel returns the effective min log level of
+// the receiver - that is, messages with a lesser severity
+// level will be discarded.
+//
+// If the log level of the receiver is unspecified,
+// it will be taken from the effective log level of its
+// parent.
+func (logger Logger) EffectiveLogLevel() Level {
+ return logger.getModule().getEffectiveLogLevel()
+}
+
+// SetLogLevel sets the severity level of the given logger.
+// The root logger cannot be set to UNSPECIFIED level.
+// See EffectiveLogLevel for how this affects the
+// actual messages logged.
+func (logger Logger) SetLogLevel(level Level) {
+ logger.getModule().setLevel(level)
+}
+
+// Logf logs a printf-formatted message at the given level.
+// A message will be discarded if level is less than the
+// the effective log level of the logger.
+// Note that the writers may also filter out messages that
+// are less than their registered minimum severity level.
+func (logger Logger) Logf(level Level, message string, args ...interface{}) {
+ logger.LogCallf(2, level, message, args...)
+}
+
+// LogCallf logs a printf-formatted message at the given level.
+// The location of the call is indicated by the calldepth argument.
+// A calldepth of 1 means the function that called this function.
+// A message will be discarded if level is less than the
+// the effective log level of the logger.
+// Note that the writers may also filter out messages that
+// are less than their registered minimum severity level.
+func (logger Logger) LogCallf(calldepth int, level Level, message string, args ...interface{}) {
+ module := logger.getModule()
+ if !module.willWrite(level) {
+ return
+ }
+ // Gather time, and filename, line number.
+ now := time.Now() // get this early.
+ // Param to Caller is the call depth. Since this method is called from
+ // the Logger methods, we want the place that those were called from.
+ _, file, line, ok := runtime.Caller(calldepth + 1)
+ if !ok {
+ file = "???"
+ line = 0
+ }
+ // Trim newline off format string, following usual
+ // Go logging conventions.
+ if len(message) > 0 && message[len(message)-1] == '\n' {
+ message = message[0 : len(message)-1]
+ }
+
+ // To avoid having a proliferation of Info/Infof methods,
+ // only use Sprintf if there are any args, and rely on the
+ // `go vet` tool for the obvious cases where someone has forgotten
+ // to provide an arg.
+ formattedMessage := message
+ if len(args) > 0 {
+ formattedMessage = fmt.Sprintf(message, args...)
+ }
+ module.write(Entry{
+ Level: level,
+ Filename: file,
+ Line: line,
+ Timestamp: now,
+ Message: formattedMessage,
+ })
+}
+
+// Criticalf logs the printf-formatted message at critical level.
+func (logger Logger) Criticalf(message string, args ...interface{}) {
+ logger.Logf(CRITICAL, message, args...)
+}
+
+// Errorf logs the printf-formatted message at error level.
+func (logger Logger) Errorf(message string, args ...interface{}) {
+ logger.Logf(ERROR, message, args...)
+}
+
+// Warningf logs the printf-formatted message at warning level.
+func (logger Logger) Warningf(message string, args ...interface{}) {
+ logger.Logf(WARNING, message, args...)
+}
+
+// Infof logs the printf-formatted message at info level.
+func (logger Logger) Infof(message string, args ...interface{}) {
+ logger.Logf(INFO, message, args...)
+}
+
+// Debugf logs the printf-formatted message at debug level.
+func (logger Logger) Debugf(message string, args ...interface{}) {
+ logger.Logf(DEBUG, message, args...)
+}
+
+// Tracef logs the printf-formatted message at trace level.
+func (logger Logger) Tracef(message string, args ...interface{}) {
+ logger.Logf(TRACE, message, args...)
+}
+
+// IsLevelEnabled returns whether debugging is enabled
+// for the given log level.
+func (logger Logger) IsLevelEnabled(level Level) bool {
+ return logger.getModule().willWrite(level)
+}
+
+// IsErrorEnabled returns whether debugging is enabled
+// at error level.
+func (logger Logger) IsErrorEnabled() bool {
+ return logger.IsLevelEnabled(ERROR)
+}
+
+// IsWarningEnabled returns whether debugging is enabled
+// at warning level.
+func (logger Logger) IsWarningEnabled() bool {
+ return logger.IsLevelEnabled(WARNING)
+}
+
+// IsInfoEnabled returns whether debugging is enabled
+// at info level.
+func (logger Logger) IsInfoEnabled() bool {
+ return logger.IsLevelEnabled(INFO)
+}
+
+// IsDebugEnabled returns whether debugging is enabled
+// at debug level.
+func (logger Logger) IsDebugEnabled() bool {
+ return logger.IsLevelEnabled(DEBUG)
+}
+
+// IsTraceEnabled returns whether debugging is enabled
+// at trace level.
+func (logger Logger) IsTraceEnabled() bool {
+ return logger.IsLevelEnabled(TRACE)
+}
diff --git a/automation/vendor/github.com/juju/loggo/module.go b/automation/vendor/github.com/juju/loggo/module.go
new file mode 100644
index 0000000..8153be5
--- /dev/null
+++ b/automation/vendor/github.com/juju/loggo/module.go
@@ -0,0 +1,61 @@
+// Copyright 2016 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+package loggo
+
+// Do not change rootName: modules.resolve() will misbehave if it isn't "".
+const (
+ rootString = "<root>"
+ defaultRootLevel = WARNING
+ defaultLevel = UNSPECIFIED
+)
+
+type module struct {
+ name string
+ level Level
+ parent *module
+ context *Context
+}
+
+// Name returns the module's name.
+func (module *module) Name() string {
+ if module.name == "" {
+ return rootString
+ }
+ return module.name
+}
+
+func (m *module) willWrite(level Level) bool {
+ if level < TRACE || level > CRITICAL {
+ return false
+ }
+ return level >= m.getEffectiveLogLevel()
+}
+
+func (module *module) getEffectiveLogLevel() Level {
+ // Note: the root module is guaranteed to have a
+ // specified logging level, so acts as a suitable sentinel
+ // for this loop.
+ for {
+ if level := module.level.get(); level != UNSPECIFIED {
+ return level
+ }
+ module = module.parent
+ }
+ panic("unreachable")
+}
+
+// setLevel sets the severity level of the given module.
+// The root module cannot be set to UNSPECIFIED level.
+func (module *module) setLevel(level Level) {
+ // The root module can't be unspecified.
+ if module.name == "" && level == UNSPECIFIED {
+ level = WARNING
+ }
+ module.level.set(level)
+}
+
+func (m *module) write(entry Entry) {
+ entry.Module = m.name
+ m.context.write(entry)
+}
diff --git a/automation/vendor/github.com/juju/loggo/testwriter.go b/automation/vendor/github.com/juju/loggo/testwriter.go
new file mode 100644
index 0000000..b20e470
--- /dev/null
+++ b/automation/vendor/github.com/juju/loggo/testwriter.go
@@ -0,0 +1,40 @@
+// Copyright 2014 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+package loggo
+
+import (
+ "path"
+ "sync"
+)
+
+// TestWriter is a useful Writer for testing purposes. Each component of the
+// logging message is stored in the Log array.
+type TestWriter struct {
+ mu sync.Mutex
+ log []Entry
+}
+
+// Write saves the params as members in the TestLogValues struct appended to the Log array.
+func (writer *TestWriter) Write(entry Entry) {
+ writer.mu.Lock()
+ defer writer.mu.Unlock()
+ entry.Filename = path.Base(entry.Filename)
+ writer.log = append(writer.log, entry)
+}
+
+// Clear removes any saved log messages.
+func (writer *TestWriter) Clear() {
+ writer.mu.Lock()
+ defer writer.mu.Unlock()
+ writer.log = nil
+}
+
+// Log returns a copy of the current logged values.
+func (writer *TestWriter) Log() []Entry {
+ writer.mu.Lock()
+ defer writer.mu.Unlock()
+ v := make([]Entry, len(writer.log))
+ copy(v, writer.log)
+ return v
+}
diff --git a/automation/vendor/github.com/juju/loggo/writer.go b/automation/vendor/github.com/juju/loggo/writer.go
new file mode 100644
index 0000000..b3fe3e5
--- /dev/null
+++ b/automation/vendor/github.com/juju/loggo/writer.go
@@ -0,0 +1,113 @@
+// Copyright 2014 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+package loggo
+
+import (
+ "fmt"
+ "io"
+ "os"
+ "path/filepath"
+
+ "github.com/juju/ansiterm"
+)
+
+// DefaultWriterName is the name of the default writer for
+// a Context.
+const DefaultWriterName = "default"
+
+// Writer is implemented by any recipient of log messages.
+type Writer interface {
+ // Write writes a message to the Writer with the given level and module
+ // name. The filename and line hold the file name and line number of the
+ // code that is generating the log message; the time stamp holds the time
+ // the log message was generated, and message holds the log message
+ // itself.
+ Write(entry Entry)
+}
+
+// NewMinLevelWriter returns a Writer that will only pass on the Write calls
+// to the provided writer if the log level is at or above the specified
+// minimum level.
+func NewMinimumLevelWriter(writer Writer, minLevel Level) Writer {
+ return &minLevelWriter{
+ writer: writer,
+ level: minLevel,
+ }
+}
+
+type minLevelWriter struct {
+ writer Writer
+ level Level
+}
+
+// Write writes the log record.
+func (w minLevelWriter) Write(entry Entry) {
+ if entry.Level < w.level {
+ return
+ }
+ w.writer.Write(entry)
+}
+
+type simpleWriter struct {
+ writer io.Writer
+ formatter func(entry Entry) string
+}
+
+// NewSimpleWriter returns a new writer that writes log messages to the given
+// io.Writer formatting the messages with the given formatter.
+func NewSimpleWriter(writer io.Writer, formatter func(entry Entry) string) Writer {
+ if formatter == nil {
+ formatter = DefaultFormatter
+ }
+ return &simpleWriter{writer, formatter}
+}
+
+func (simple *simpleWriter) Write(entry Entry) {
+ logLine := simple.formatter(entry)
+ fmt.Fprintln(simple.writer, logLine)
+}
+
+func defaultWriter() Writer {
+ return NewColorWriter(os.Stderr)
+}
+
+type colorWriter struct {
+ writer *ansiterm.Writer
+}
+
+var (
+ // SeverityColor defines the colors for the levels output by the ColorWriter.
+ SeverityColor = map[Level]*ansiterm.Context{
+ TRACE: ansiterm.Foreground(ansiterm.Default),
+ DEBUG: ansiterm.Foreground(ansiterm.Green),
+ INFO: ansiterm.Foreground(ansiterm.BrightBlue),
+ WARNING: ansiterm.Foreground(ansiterm.Yellow),
+ ERROR: ansiterm.Foreground(ansiterm.BrightRed),
+ CRITICAL: &ansiterm.Context{
+ Foreground: ansiterm.White,
+ Background: ansiterm.Red,
+ },
+ }
+ // LocationColor defines the colors for the location output by the ColorWriter.
+ LocationColor = ansiterm.Foreground(ansiterm.BrightBlue)
+)
+
+// NewColorWriter will write out colored severity levels if the writer is
+// outputting to a terminal.
+func NewColorWriter(writer io.Writer) Writer {
+ return &colorWriter{ansiterm.NewWriter(writer)}
+}
+
+// Write implements Writer.
+func (w *colorWriter) Write(entry Entry) {
+ ts := formatTime(entry.Timestamp)
+ // Just get the basename from the filename
+ filename := filepath.Base(entry.Filename)
+
+ fmt.Fprintf(w.writer, "%s ", ts)
+ SeverityColor[entry.Level].Fprintf(w.writer, entry.Level.Short())
+ fmt.Fprintf(w.writer, " %s ", entry.Module)
+ LocationColor.Fprintf(w.writer, "%s:%d ", filename, entry.Line)
+ fmt.Fprintln(w.writer, entry.Message)
+}
diff --git a/automation/vendor/github.com/juju/schema/LICENSE b/automation/vendor/github.com/juju/schema/LICENSE
new file mode 100644
index 0000000..ade9307
--- /dev/null
+++ b/automation/vendor/github.com/juju/schema/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) 2015 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/schema/README.md b/automation/vendor/github.com/juju/schema/README.md
new file mode 100644
index 0000000..78ad8a1
--- /dev/null
+++ b/automation/vendor/github.com/juju/schema/README.md
@@ -0,0 +1,5 @@
+juju/schema
+===========
+
+This package provides helpers for coercing dynamically typed data structures
+into known forms.
diff --git a/automation/vendor/github.com/juju/schema/checker.go b/automation/vendor/github.com/juju/schema/checker.go
new file mode 100644
index 0000000..8682136
--- /dev/null
+++ b/automation/vendor/github.com/juju/schema/checker.go
@@ -0,0 +1,71 @@
+// Copyright 2015 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+package schema
+
+import (
+ "strings"
+)
+
+// The Coerce method of the Checker interface is called recursively when
+// v is being validated. If err is nil, newv is used as the new value
+// at the recursion point. If err is non-nil, v is taken as invalid and
+// may be either ignored or error out depending on where in the schema
+// checking process the error happened. Checkers like OneOf may continue
+// with an alternative, for instance.
+type Checker interface {
+ Coerce(v interface{}, path []string) (newv interface{}, err error)
+}
+
+// Any returns a Checker that succeeds with any input value and
+// results in the value itself unprocessed.
+func Any() Checker {
+ return anyC{}
+}
+
+type anyC struct{}
+
+func (c anyC) Coerce(v interface{}, path []string) (interface{}, error) {
+ return v, nil
+}
+
+// OneOf returns a Checker that attempts to Coerce the value with each
+// of the provided checkers. The value returned by the first checker
+// that succeeds will be returned by the OneOf checker itself. If no
+// checker succeeds, OneOf will return an error on coercion.
+func OneOf(options ...Checker) Checker {
+ return oneOfC{options}
+}
+
+type oneOfC struct {
+ options []Checker
+}
+
+func (c oneOfC) Coerce(v interface{}, path []string) (interface{}, error) {
+ for _, o := range c.options {
+ newv, err := o.Coerce(v, path)
+ if err == nil {
+ return newv, nil
+ }
+ }
+ return nil, error_{"", v, path}
+}
+
+// pathAsPrefix returns a string consisting of the path elements
+// suitable for using as the prefix of an error message. If path
+// starts with a ".", the dot is omitted.
+func pathAsPrefix(path []string) string {
+ if len(path) == 0 {
+ return ""
+ }
+ var s string
+ if path[0] == "." {
+ s = strings.Join(path[1:], "")
+ } else {
+ s = strings.Join(path, "")
+ }
+ if s == "" {
+ return ""
+ }
+ return s + ": "
+}
diff --git a/automation/vendor/github.com/juju/schema/const.go b/automation/vendor/github.com/juju/schema/const.go
new file mode 100644
index 0000000..cbd03b8
--- /dev/null
+++ b/automation/vendor/github.com/juju/schema/const.go
@@ -0,0 +1,57 @@
+// Copyright 2015 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+package schema
+
+import (
+ "fmt"
+ "reflect"
+)
+
+// Const returns a Checker that only succeeds if the input matches
+// value exactly. The value is compared with reflect.DeepEqual.
+func Const(value interface{}) Checker {
+ return constC{value}
+}
+
+type constC struct {
+ value interface{}
+}
+
+func (c constC) Coerce(v interface{}, path []string) (interface{}, error) {
+ if reflect.DeepEqual(v, c.value) {
+ return v, nil
+ }
+ return nil, error_{fmt.Sprintf("%#v", c.value), v, path}
+}
+
+// Nil returns a Checker that only succeeds if the input is nil. To tweak the
+// error message, valueLabel can contain a label of the value being checked to
+// be empty, e.g. "my special name". If valueLabel is "", "value" will be used
+// as a label instead.
+//
+// Example 1:
+// schema.Nil("widget").Coerce(42, nil) will return an error message
+// like `expected empty widget, got int(42)`.
+//
+// Example 2:
+// schema.Nil("").Coerce("", nil) will return an error message like
+// `expected empty value, got string("")`.
+func Nil(valueLabel string) Checker {
+ if valueLabel == "" {
+ valueLabel = "value"
+ }
+ return nilC{valueLabel}
+}
+
+type nilC struct {
+ valueLabel string
+}
+
+func (c nilC) Coerce(v interface{}, path []string) (interface{}, error) {
+ if reflect.DeepEqual(v, nil) {
+ return v, nil
+ }
+ label := fmt.Sprintf("empty %s", c.valueLabel)
+ return nil, error_{label, v, path}
+}
diff --git a/automation/vendor/github.com/juju/schema/errors.go b/automation/vendor/github.com/juju/schema/errors.go
new file mode 100644
index 0000000..f62f58e
--- /dev/null
+++ b/automation/vendor/github.com/juju/schema/errors.go
@@ -0,0 +1,25 @@
+// Copyright 2015 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+package schema
+
+import (
+ "fmt"
+)
+
+type error_ struct {
+ want string
+ got interface{}
+ path []string
+}
+
+func (e error_) Error() string {
+ path := pathAsPrefix(e.path)
+ if e.want == "" {
+ return fmt.Sprintf("%sunexpected value %#v", path, e.got)
+ }
+ if e.got == nil {
+ return fmt.Sprintf("%sexpected %s, got nothing", path, e.want)
+ }
+ return fmt.Sprintf("%sexpected %s, got %T(%#v)", path, e.want, e.got, e.got)
+}
diff --git a/automation/vendor/github.com/juju/schema/fieldmap.go b/automation/vendor/github.com/juju/schema/fieldmap.go
new file mode 100644
index 0000000..765f7e3
--- /dev/null
+++ b/automation/vendor/github.com/juju/schema/fieldmap.go
@@ -0,0 +1,170 @@
+// Copyright 2015 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+package schema
+
+import (
+ "fmt"
+ "reflect"
+)
+
+// Omit is a marker for FieldMap and StructFieldMap defaults parameter.
+// If a field is not present in the map and defaults to Omit, the missing
+// field will be ommitted from the coerced map as well.
+var Omit omit
+
+type omit struct{}
+
+type Fields map[string]Checker
+type Defaults map[string]interface{}
+
+// FieldMap returns a Checker that accepts a map value with defined
+// string keys. Every key has an independent checker associated,
+// and processing will only succeed if all the values succeed
+// individually. If a field fails to be processed, processing stops
+// and returns with the underlying error.
+//
+// Fields in defaults will be set to the provided value if not present
+// in the coerced map. If the default value is schema.Omit, the
+// missing field will be omitted from the coerced map.
+//
+// The coerced output value has type map[string]interface{}.
+func FieldMap(fields Fields, defaults Defaults) Checker {
+ return fieldMapC{fields, defaults, false}
+}
+
+// StrictFieldMap returns a Checker that acts as the one returned by FieldMap,
+// but the Checker returns an error if it encounters an unknown key.
+func StrictFieldMap(fields Fields, defaults Defaults) Checker {
+ return fieldMapC{fields, defaults, true}
+}
+
+type fieldMapC struct {
+ fields Fields
+ defaults Defaults
+ strict bool
+}
+
+var stringType = reflect.TypeOf("")
+
+func hasStrictStringKeys(rv reflect.Value) bool {
+ if rv.Type().Key() == stringType {
+ return true
+ }
+ if rv.Type().Key().Kind() != reflect.Interface {
+ return false
+ }
+ for _, k := range rv.MapKeys() {
+ if k.Elem().Type() != stringType {
+ return false
+ }
+ }
+ return true
+}
+
+func (c fieldMapC) Coerce(v interface{}, path []string) (interface{}, error) {
+ rv := reflect.ValueOf(v)
+ if rv.Kind() != reflect.Map {
+ return nil, error_{"map", v, path}
+ }
+ if !hasStrictStringKeys(rv) {
+ return nil, error_{"map[string]", v, path}
+ }
+
+ if c.strict {
+ for _, k := range rv.MapKeys() {
+ ks := k.String()
+ if _, ok := c.fields[ks]; !ok {
+ return nil, fmt.Errorf("%sunknown key %q (value %#v)", pathAsPrefix(path), ks, rv.MapIndex(k).Interface())
+ }
+ }
+ }
+
+ vpath := append(path, ".", "?")
+
+ out := make(map[string]interface{}, rv.Len())
+ for k, checker := range c.fields {
+ valuev := rv.MapIndex(reflect.ValueOf(k))
+ var value interface{}
+ if valuev.IsValid() {
+ value = valuev.Interface()
+ } else if dflt, ok := c.defaults[k]; ok {
+ if dflt == Omit {
+ continue
+ }
+ value = dflt
+ }
+ vpath[len(vpath)-1] = k
+ newv, err := checker.Coerce(value, vpath)
+ if err != nil {
+ return nil, err
+ }
+ out[k] = newv
+ }
+ for k, v := range c.defaults {
+ if v == Omit {
+ continue
+ }
+ if _, ok := out[k]; !ok {
+ checker, ok := c.fields[k]
+ if !ok {
+ return nil, fmt.Errorf("got default value for unknown field %q", k)
+ }
+ vpath[len(vpath)-1] = k
+ newv, err := checker.Coerce(v, vpath)
+ if err != nil {
+ return nil, err
+ }
+ out[k] = newv
+ }
+ }
+ return out, nil
+}
+
+// FieldMapSet returns a Checker that accepts a map value checked
+// against one of several FieldMap checkers. The actual checker
+// used is the first one whose checker associated with the selector
+// field processes the map correctly. If no checker processes
+// the selector value correctly, an error is returned.
+//
+// The coerced output value has type map[string]interface{}.
+func FieldMapSet(selector string, maps []Checker) Checker {
+ fmaps := make([]fieldMapC, len(maps))
+ for i, m := range maps {
+ if fmap, ok := m.(fieldMapC); ok {
+ if checker, _ := fmap.fields[selector]; checker == nil {
+ panic("FieldMapSet has a FieldMap with a missing selector")
+ }
+ fmaps[i] = fmap
+ } else {
+ panic("FieldMapSet got a non-FieldMap checker")
+ }
+ }
+ return mapSetC{selector, fmaps}
+}
+
+type mapSetC struct {
+ selector string
+ fmaps []fieldMapC
+}
+
+func (c mapSetC) Coerce(v interface{}, path []string) (interface{}, error) {
+ rv := reflect.ValueOf(v)
+ if rv.Kind() != reflect.Map {
+ return nil, error_{"map", v, path}
+ }
+
+ var selector interface{}
+ selectorv := rv.MapIndex(reflect.ValueOf(c.selector))
+ if selectorv.IsValid() {
+ selector = selectorv.Interface()
+ for _, fmap := range c.fmaps {
+ _, err := fmap.fields[c.selector].Coerce(selector, path)
+ if err != nil {
+ continue
+ }
+ return fmap.Coerce(v, path)
+ }
+ }
+ return nil, error_{"supported selector", selector, append(path, ".", c.selector)}
+}
diff --git a/automation/vendor/github.com/juju/schema/lists.go b/automation/vendor/github.com/juju/schema/lists.go
new file mode 100644
index 0000000..635425a
--- /dev/null
+++ b/automation/vendor/github.com/juju/schema/lists.go
@@ -0,0 +1,44 @@
+// Copyright 2015 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+package schema
+
+import (
+ "reflect"
+ "strconv"
+)
+
+// List returns a Checker that accepts a slice value with values
+// that are processed with the elem checker. If any element of the
+// provided slice value fails to be processed, processing will stop
+// and return with the obtained error.
+//
+// The coerced output value has type []interface{}.
+func List(elem Checker) Checker {
+ return listC{elem}
+}
+
+type listC struct {
+ elem Checker
+}
+
+func (c listC) Coerce(v interface{}, path []string) (interface{}, error) {
+ rv := reflect.ValueOf(v)
+ if rv.Kind() != reflect.Slice {
+ return nil, error_{"list", v, path}
+ }
+
+ path = append(path, "[", "?", "]")
+
+ l := rv.Len()
+ out := make([]interface{}, 0, l)
+ for i := 0; i != l; i++ {
+ path[len(path)-2] = strconv.Itoa(i)
+ elem, err := c.elem.Coerce(rv.Index(i).Interface(), path)
+ if err != nil {
+ return nil, err
+ }
+ out = append(out, elem)
+ }
+ return out, nil
+}
diff --git a/automation/vendor/github.com/juju/schema/maps.go b/automation/vendor/github.com/juju/schema/maps.go
new file mode 100644
index 0000000..31242a0
--- /dev/null
+++ b/automation/vendor/github.com/juju/schema/maps.go
@@ -0,0 +1,93 @@
+// Copyright 2015 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+package schema
+
+import (
+ "fmt"
+ "reflect"
+)
+
+// Map returns a Checker that accepts a map value. Every key and value
+// in the map are processed with the respective checker, and if any
+// value fails to be coerced, processing stops and returns with the
+// underlying error.
+//
+// The coerced output value has type map[interface{}]interface{}.
+func Map(key Checker, value Checker) Checker {
+ return mapC{key, value}
+}
+
+type mapC struct {
+ key Checker
+ value Checker
+}
+
+func (c mapC) Coerce(v interface{}, path []string) (interface{}, error) {
+ rv := reflect.ValueOf(v)
+ if rv.Kind() != reflect.Map {
+ return nil, error_{"map", v, path}
+ }
+
+ vpath := append(path, ".", "?")
+
+ l := rv.Len()
+ out := make(map[interface{}]interface{}, l)
+ keys := rv.MapKeys()
+ for i := 0; i != l; i++ {
+ k := keys[i]
+ newk, err := c.key.Coerce(k.Interface(), path)
+ if err != nil {
+ return nil, err
+ }
+ vpath[len(vpath)-1] = fmt.Sprint(k.Interface())
+ newv, err := c.value.Coerce(rv.MapIndex(k).Interface(), vpath)
+ if err != nil {
+ return nil, err
+ }
+ out[newk] = newv
+ }
+ return out, nil
+}
+
+// StringMap returns a Checker that accepts a map value. Every key in
+// the map must be a string, and every value in the map are processed
+// with the provided checker. If any value fails to be coerced,
+// processing stops and returns with the underlying error.
+//
+// The coerced output value has type map[string]interface{}.
+func StringMap(value Checker) Checker {
+ return stringMapC{value}
+}
+
+type stringMapC struct {
+ value Checker
+}
+
+func (c stringMapC) Coerce(v interface{}, path []string) (interface{}, error) {
+ rv := reflect.ValueOf(v)
+ if rv.Kind() != reflect.Map {
+ return nil, error_{"map", v, path}
+ }
+
+ vpath := append(path, ".", "?")
+ key := String()
+
+ l := rv.Len()
+ out := make(map[string]interface{}, l)
+ keys := rv.MapKeys()
+ for i := 0; i != l; i++ {
+ k := keys[i]
+ newk, err := key.Coerce(k.Interface(), path)
+ if err != nil {
+ return nil, err
+ }
+ vpath[len(vpath)-1] = fmt.Sprint(k.Interface())
+ newv, err := c.value.Coerce(rv.MapIndex(k).Interface(), vpath)
+ if err != nil {
+ return nil, err
+ }
+ out[newk.(string)] = newv
+ }
+ return out, nil
+}
diff --git a/automation/vendor/github.com/juju/schema/numeric.go b/automation/vendor/github.com/juju/schema/numeric.go
new file mode 100644
index 0000000..ec88e56
--- /dev/null
+++ b/automation/vendor/github.com/juju/schema/numeric.go
@@ -0,0 +1,197 @@
+// Copyright 2015 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+package schema
+
+import (
+ "reflect"
+ "strconv"
+)
+
+// Bool returns a Checker that accepts boolean values only.
+func Bool() Checker {
+ return boolC{}
+}
+
+type boolC struct{}
+
+func (c boolC) Coerce(v interface{}, path []string) (interface{}, error) {
+ if v != nil {
+ switch reflect.TypeOf(v).Kind() {
+ case reflect.Bool:
+ return v, nil
+ case reflect.String:
+ val, err := strconv.ParseBool(reflect.ValueOf(v).String())
+ if err == nil {
+ return val, nil
+ }
+ }
+ }
+ return nil, error_{"bool", v, path}
+}
+
+// Int returns a Checker that accepts any integer value, and returns
+// the same value consistently typed as an int64.
+func Int() Checker {
+ return intC{}
+}
+
+type intC struct{}
+
+func (c intC) Coerce(v interface{}, path []string) (interface{}, error) {
+ if v == nil {
+ return nil, error_{"int", v, path}
+ }
+ switch reflect.TypeOf(v).Kind() {
+ case reflect.Int:
+ case reflect.Int8:
+ case reflect.Int16:
+ case reflect.Int32:
+ case reflect.Int64:
+ case reflect.String:
+ val, err := strconv.ParseInt(reflect.ValueOf(v).String(), 0, 64)
+ if err == nil {
+ return val, nil
+ } else {
+ return nil, error_{"int", v, path}
+ }
+ default:
+ return nil, error_{"int", v, path}
+ }
+ return reflect.ValueOf(v).Int(), nil
+}
+
+// Uint returns a Checker that accepts any integer or unsigned value, and
+// returns the same value consistently typed as an uint64. If the integer
+// value is negative an error is raised.
+func Uint() Checker {
+ return uintC{}
+}
+
+type uintC struct{}
+
+func (c uintC) Coerce(v interface{}, path []string) (interface{}, error) {
+ if v == nil {
+ return nil, error_{"uint", v, path}
+ }
+ switch reflect.TypeOf(v).Kind() {
+ case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
+ return reflect.ValueOf(v).Uint(), nil
+ case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+ val := reflect.ValueOf(v).Int()
+ if val < 0 {
+ return nil, error_{"uint", v, path}
+ }
+ // All positive int64 values fit into uint64.
+ return uint64(val), nil
+ case reflect.String:
+ val, err := strconv.ParseUint(reflect.ValueOf(v).String(), 0, 64)
+ if err == nil {
+ return val, nil
+ } else {
+ return nil, error_{"uint", v, path}
+ }
+ default:
+ return nil, error_{"uint", v, path}
+ }
+}
+
+// ForceInt returns a Checker that accepts any integer or float value, and
+// returns the same value consistently typed as an int. This is required
+// in order to handle the interface{}/float64 type conversion performed by
+// the JSON serializer used as part of the API infrastructure.
+func ForceInt() Checker {
+ return forceIntC{}
+}
+
+type forceIntC struct{}
+
+func (c forceIntC) Coerce(v interface{}, path []string) (interface{}, error) {
+ if v != nil {
+ switch vv := reflect.TypeOf(v); vv.Kind() {
+ case reflect.String:
+ vstr := reflect.ValueOf(v).String()
+ intValue, err := strconv.ParseInt(vstr, 0, 64)
+ if err == nil {
+ return int(intValue), nil
+ }
+ floatValue, err := strconv.ParseFloat(vstr, 64)
+ if err == nil {
+ return int(floatValue), nil
+ }
+ case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+ return int(reflect.ValueOf(v).Int()), nil
+ case reflect.Float32, reflect.Float64:
+ return int(reflect.ValueOf(v).Float()), nil
+ }
+ }
+ return nil, error_{"number", v, path}
+}
+
+// ForceUint returns a Checker that accepts any integer or float value, and
+// returns the same value consistently typed as an uint64. This is required
+// in order to handle the interface{}/float64 type conversion performed by
+// the JSON serializer used as part of the API infrastructure. If the integer
+// value is negative an error is raised.
+func ForceUint() Checker {
+ return forceUintC{}
+}
+
+type forceUintC struct{}
+
+func (c forceUintC) Coerce(v interface{}, path []string) (interface{}, error) {
+ if v != nil {
+ switch vv := reflect.TypeOf(v); vv.Kind() {
+ case reflect.String:
+ vstr := reflect.ValueOf(v).String()
+ intValue, err := strconv.ParseUint(vstr, 0, 64)
+ if err == nil {
+ return intValue, nil
+ }
+ floatValue, err := strconv.ParseFloat(vstr, 64)
+ if err == nil {
+ if floatValue < 0 {
+ return nil, error_{"uint", v, path}
+ }
+ return uint64(floatValue), nil
+ }
+ case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
+ return reflect.ValueOf(v).Uint(), nil
+ case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+ val := reflect.ValueOf(v).Int()
+ if val < 0 {
+ return nil, error_{"uint", v, path}
+ }
+ // All positive int64 values fit into uint64.
+ return uint64(val), nil
+ case reflect.Float32, reflect.Float64:
+ val := reflect.ValueOf(v).Float()
+ if val < 0 {
+ return nil, error_{"uint", v, path}
+ }
+ return uint64(val), nil
+ }
+ }
+ return nil, error_{"uint", v, path}
+}
+
+// Float returns a Checker that accepts any float value, and returns
+// the same value consistently typed as a float64.
+func Float() Checker {
+ return floatC{}
+}
+
+type floatC struct{}
+
+func (c floatC) Coerce(v interface{}, path []string) (interface{}, error) {
+ if v == nil {
+ return nil, error_{"float", v, path}
+ }
+ switch reflect.TypeOf(v).Kind() {
+ case reflect.Float32:
+ case reflect.Float64:
+ default:
+ return nil, error_{"float", v, path}
+ }
+ return reflect.ValueOf(v).Float(), nil
+}
diff --git a/automation/vendor/github.com/juju/schema/size.go b/automation/vendor/github.com/juju/schema/size.go
new file mode 100644
index 0000000..1ad0caa
--- /dev/null
+++ b/automation/vendor/github.com/juju/schema/size.go
@@ -0,0 +1,42 @@
+// Copyright 2016 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+package schema
+
+import (
+ "github.com/juju/utils"
+ "reflect"
+)
+
+// Size returns a Checker that accepts a string value, and returns
+// the parsed string as a size in mebibytes see: https://godoc.org/github.com/juju/utils#ParseSize
+func Size() Checker {
+ return sizeC{}
+}
+
+type sizeC struct{}
+
+// Coerce implements Checker Coerce method.
+func (c sizeC) Coerce(v interface{}, path []string) (interface{}, error) {
+ if v == nil {
+ return nil, error_{"string", v, path}
+ }
+
+ typeOf := reflect.TypeOf(v).Kind()
+ if typeOf != reflect.String {
+ return nil, error_{"string", v, path}
+ }
+
+ value := reflect.ValueOf(v).String()
+ if value == "" {
+ return nil, error_{"empty string", v, path}
+ }
+
+ v, err := utils.ParseSize(value)
+
+ if err != nil {
+ return nil, err
+ }
+
+ return v, nil
+}
diff --git a/automation/vendor/github.com/juju/schema/strings.go b/automation/vendor/github.com/juju/schema/strings.go
new file mode 100644
index 0000000..75f6120
--- /dev/null
+++ b/automation/vendor/github.com/juju/schema/strings.go
@@ -0,0 +1,155 @@
+// Copyright 2015 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+package schema
+
+import (
+ "fmt"
+ "net/url"
+ "reflect"
+ "regexp"
+)
+
+// String returns a Checker that accepts a string value only and returns
+// it unprocessed.
+func String() Checker {
+ return stringC{}
+}
+
+type stringC struct{}
+
+func (c stringC) Coerce(v interface{}, path []string) (interface{}, error) {
+ if v != nil && reflect.TypeOf(v).Kind() == reflect.String {
+ return reflect.ValueOf(v).String(), nil
+ }
+ return nil, error_{"string", v, path}
+}
+
+// URL returns a Checker that accepts a string value that must be parseable as a
+// URL, and returns a *net.URL.
+func URL() Checker {
+ return urlC{}
+}
+
+type urlC struct{}
+
+func (c urlC) Coerce(v interface{}, path []string) (interface{}, error) {
+ if v != nil && reflect.TypeOf(v).Kind() == reflect.String {
+ s := reflect.ValueOf(v).String()
+ u, err := url.Parse(s)
+ if err != nil {
+ return nil, error_{"valid url", s, path}
+ }
+ return u, nil
+ }
+ return nil, error_{"url string", v, path}
+}
+
+// SimpleRegexp returns a checker that accepts a string value that is
+// a valid regular expression and returns it unprocessed.
+func SimpleRegexp() Checker {
+ return sregexpC{}
+}
+
+type sregexpC struct{}
+
+func (c sregexpC) Coerce(v interface{}, path []string) (interface{}, error) {
+ // XXX The regexp package happens to be extremely simple right now.
+ // Once exp/regexp goes mainstream, we'll have to update this
+ // logic to use a more widely accepted regexp subset.
+ if v != nil && reflect.TypeOf(v).Kind() == reflect.String {
+ s := reflect.ValueOf(v).String()
+ _, err := regexp.Compile(s)
+ if err != nil {
+ return nil, error_{"valid regexp", s, path}
+ }
+ return v, nil
+ }
+ return nil, error_{"regexp string", v, path}
+}
+
+// UUID returns a Checker that accepts a string value only and returns
+// it unprocessed.
+func UUID() Checker {
+ return uuidC{}
+}
+
+type uuidC struct{}
+
+var uuidregex = regexp.MustCompile(`[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}`)
+
+func (c uuidC) Coerce(v interface{}, path []string) (interface{}, error) {
+ if v != nil && reflect.TypeOf(v).Kind() == reflect.String {
+ uuid := reflect.ValueOf(v).String()
+ if uuidregex.MatchString(uuid) {
+ return uuid, nil
+ }
+ }
+ return nil, error_{"uuid", v, path}
+}
+
+// Stringified returns a checker that accepts a bool/int/float/string
+// value and returns its string. Other value types may be supported by
+// passing in their checkers.
+func Stringified(checkers ...Checker) Checker {
+ return stringifiedC{
+ checkers: checkers,
+ }
+}
+
+type stringifiedC struct {
+ checkers []Checker
+}
+
+func (c stringifiedC) Coerce(v interface{}, path []string) (interface{}, error) {
+ if newStr, err := String().Coerce(v, path); err == nil {
+ return newStr, nil
+ }
+ _, err := OneOf(append(c.checkers,
+ Bool(),
+ Int(),
+ Float(),
+ String(),
+ URL(),
+ )...).Coerce(v, path)
+ if err != nil {
+ return nil, err
+ }
+ return fmt.Sprintf("%#v", v), nil
+}
+
+// NonEmptyString returns a Checker that only accepts non-empty strings. To
+// tweak the error message, valueLabel can contain a label of the value being
+// checked, e.g. "my special name". If valueLabel is "", "string" will be used
+// as a label instead.
+//
+// Example 1:
+// schema.NonEmptyString("widget").Coerce("", nil) will return an error message
+// like `expected non-empty widget, got string("")`.
+//
+// Example 2:
+// schema.NonEmptyString("").Coerce("", nil) will return an error message like
+// `expected non-empty string, got string("")`.
+func NonEmptyString(valueLabel string) Checker {
+ if valueLabel == "" {
+ valueLabel = "string"
+ }
+ return nonEmptyStringC{valueLabel}
+}
+
+type nonEmptyStringC struct {
+ valueLabel string
+}
+
+func (c nonEmptyStringC) Coerce(v interface{}, path []string) (interface{}, error) {
+ label := fmt.Sprintf("non-empty %s", c.valueLabel)
+ invalidError := error_{label, v, path}
+
+ if v == nil || reflect.TypeOf(v).Kind() != reflect.String {
+ return nil, invalidError
+ }
+ if stringValue := reflect.ValueOf(v).String(); stringValue != "" {
+ return stringValue, nil
+ }
+ return nil, invalidError
+}
diff --git a/automation/vendor/github.com/juju/schema/time.go b/automation/vendor/github.com/juju/schema/time.go
new file mode 100644
index 0000000..9521a2a
--- /dev/null
+++ b/automation/vendor/github.com/juju/schema/time.go
@@ -0,0 +1,41 @@
+// Copyright 2016 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+package schema
+
+import (
+ "reflect"
+ "time"
+)
+
+// Time returns a Checker that accepts a string value, and returns
+// the parsed time.Time value. Emtpy strings are considered empty times.
+func Time() Checker {
+ return timeC{}
+}
+
+type timeC struct{}
+
+// Coerce implements Checker Coerce method.
+func (c timeC) Coerce(v interface{}, path []string) (interface{}, error) {
+ if v == nil {
+ return nil, error_{"string or time.Time", v, path}
+ }
+ var empty time.Time
+ switch reflect.TypeOf(v).Kind() {
+ case reflect.TypeOf(empty).Kind():
+ return v, nil
+ case reflect.String:
+ vstr := reflect.ValueOf(v).String()
+ if vstr == "" {
+ return empty, nil
+ }
+ v, err := time.Parse(time.RFC3339Nano, vstr)
+ if err != nil {
+ return nil, err
+ }
+ return v, nil
+ default:
+ return nil, error_{"string or time.Time", v, path}
+ }
+}
diff --git a/automation/vendor/github.com/juju/schema/time_duration.go b/automation/vendor/github.com/juju/schema/time_duration.go
new file mode 100644
index 0000000..71e75fe
--- /dev/null
+++ b/automation/vendor/github.com/juju/schema/time_duration.go
@@ -0,0 +1,42 @@
+// Copyright 2016 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+package schema
+
+import (
+ "reflect"
+ "time"
+)
+
+// TimeDuration returns a Checker that accepts a string value, and returns
+// the parsed time.Duration value. Emtpy strings are considered empty time.Duration
+func TimeDuration() Checker {
+ return timeDurationC{}
+}
+
+type timeDurationC struct{}
+
+// Coerce implements Checker Coerce method.
+func (c timeDurationC) Coerce(v interface{}, path []string) (interface{}, error) {
+ if v == nil {
+ return nil, error_{"string or time.Duration", v, path}
+ }
+
+ var empty time.Duration
+ switch reflect.TypeOf(v).Kind() {
+ case reflect.TypeOf(empty).Kind():
+ return v, nil
+ case reflect.String:
+ vstr := reflect.ValueOf(v).String()
+ if vstr == "" {
+ return empty, nil
+ }
+ v, err := time.ParseDuration(vstr)
+ if err != nil {
+ return nil, err
+ }
+ return v, nil
+ default:
+ return nil, error_{"string or time.Duration", v, path}
+ }
+}
diff --git a/automation/vendor/github.com/juju/utils/LICENSE b/automation/vendor/github.com/juju/utils/LICENSE
new file mode 100644
index 0000000..ade9307
--- /dev/null
+++ b/automation/vendor/github.com/juju/utils/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) 2015 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/utils/LICENSE.golang b/automation/vendor/github.com/juju/utils/LICENSE.golang
new file mode 100644
index 0000000..953076d
--- /dev/null
+++ b/automation/vendor/github.com/juju/utils/LICENSE.golang
@@ -0,0 +1,32 @@
+This licence applies to the following files:
+
+* filepath/stdlib.go
+* filepath/stdlibmatch.go
+
+Copyright (c) 2010 The Go Authors. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/automation/vendor/github.com/juju/utils/Makefile b/automation/vendor/github.com/juju/utils/Makefile
new file mode 100644
index 0000000..9c69f32
--- /dev/null
+++ b/automation/vendor/github.com/juju/utils/Makefile
@@ -0,0 +1,10 @@
+PROJECT := github.com/juju/utils
+
+check-licence:
+ @(fgrep -rl "Licensed under the LGPLv3" .;\
+ fgrep -rl "MACHINE GENERATED BY THE COMMAND ABOVE; DO NOT EDIT" .;\
+ find . -name "*.go") | sed -e 's,\./,,' | sort | uniq -u | \
+ xargs -I {} echo FAIL: licence missed: {}
+
+check: check-licence
+ go test $(PROJECT)/...
\ No newline at end of file
diff --git a/automation/vendor/github.com/juju/utils/README.md b/automation/vendor/github.com/juju/utils/README.md
new file mode 100644
index 0000000..ef08948
--- /dev/null
+++ b/automation/vendor/github.com/juju/utils/README.md
@@ -0,0 +1,4 @@
+juju/utils
+============
+
+This package provides general utility packages and functions.
diff --git a/automation/vendor/github.com/juju/utils/attempt.go b/automation/vendor/github.com/juju/utils/attempt.go
new file mode 100644
index 0000000..3becab2
--- /dev/null
+++ b/automation/vendor/github.com/juju/utils/attempt.go
@@ -0,0 +1,80 @@
+// Copyright 2011, 2012, 2013 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+package utils
+
+import (
+ "time"
+)
+
+// The Attempt and AttemptStrategy types are copied from those in launchpad.net/goamz/aws.
+
+// AttemptStrategy represents a strategy for waiting for an action
+// to complete successfully.
+type AttemptStrategy struct {
+ Total time.Duration // total duration of attempt.
+ Delay time.Duration // interval between each try in the burst.
+ Min int // minimum number of retries; overrides Total
+}
+
+type Attempt struct {
+ strategy AttemptStrategy
+ last time.Time
+ end time.Time
+ force bool
+ count int
+}
+
+// Start begins a new sequence of attempts for the given strategy.
+func (s AttemptStrategy) Start() *Attempt {
+ now := time.Now()
+ return &Attempt{
+ strategy: s,
+ last: now,
+ end: now.Add(s.Total),
+ force: true,
+ }
+}
+
+// Next waits until it is time to perform the next attempt or returns
+// false if it is time to stop trying.
+// It always returns true the first time it is called - we are guaranteed to
+// make at least one attempt.
+func (a *Attempt) Next() bool {
+ now := time.Now()
+ sleep := a.nextSleep(now)
+ if !a.force && !now.Add(sleep).Before(a.end) && a.strategy.Min <= a.count {
+ return false
+ }
+ a.force = false
+ if sleep > 0 && a.count > 0 {
+ time.Sleep(sleep)
+ now = time.Now()
+ }
+ a.count++
+ a.last = now
+ return true
+}
+
+func (a *Attempt) nextSleep(now time.Time) time.Duration {
+ sleep := a.strategy.Delay - now.Sub(a.last)
+ if sleep < 0 {
+ return 0
+ }
+ return sleep
+}
+
+// HasNext returns whether another attempt will be made if the current
+// one fails. If it returns true, the following call to Next is
+// guaranteed to return true.
+func (a *Attempt) HasNext() bool {
+ if a.force || a.strategy.Min > a.count {
+ return true
+ }
+ now := time.Now()
+ if now.Add(a.nextSleep(now)).Before(a.end) {
+ a.force = true
+ return true
+ }
+ return false
+}
diff --git a/automation/vendor/github.com/juju/utils/clock/clock.go b/automation/vendor/github.com/juju/utils/clock/clock.go
new file mode 100644
index 0000000..59a511d
--- /dev/null
+++ b/automation/vendor/github.com/juju/utils/clock/clock.go
@@ -0,0 +1,53 @@
+// Copyright 2015 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+package clock
+
+import "time"
+
+// Clock provides an interface for dealing with clocks.
+type Clock interface {
+ // Now returns the current clock time.
+ Now() time.Time
+
+ // After waits for the duration to elapse and then sends the
+ // current time on the returned channel.
+ After(time.Duration) <-chan time.Time
+
+ // AfterFunc waits for the duration to elapse and then calls f in its own goroutine.
+ // It returns a Timer that can be used to cancel the call using its Stop method.
+ AfterFunc(d time.Duration, f func()) Timer
+
+ // NewTimer creates a new Timer that will send the current time
+ // on its channel after at least duration d.
+ NewTimer(d time.Duration) Timer
+}
+
+// Alarm returns a channel that will have the time sent on it at some point
+// after the supplied time occurs.
+//
+// This is short for c.After(t.Sub(c.Now())).
+func Alarm(c Clock, t time.Time) <-chan time.Time {
+ return c.After(t.Sub(c.Now()))
+}
+
+// The Timer type represents a single event.
+// A Timer must be created with AfterFunc.
+// This interface follows time.Timer's methods but provides easier mocking.
+type Timer interface {
+ // When the Timer expires, the current time will be sent on the
+ // channel returned from Chan, unless the Timer was created by
+ // AfterFunc.
+ Chan() <-chan time.Time
+
+ // Reset changes the timer to expire after duration d.
+ // It returns true if the timer had been active, false if
+ // the timer had expired or been stopped.
+ Reset(time.Duration) bool
+
+ // Stop prevents the Timer from firing. It returns true if
+ // the call stops the timer, false if the timer has already expired or been stopped.
+ // Stop does not close the channel, to prevent a read
+ // from the channel succeeding incorrectly.
+ Stop() bool
+}
diff --git a/automation/vendor/github.com/juju/utils/clock/wall.go b/automation/vendor/github.com/juju/utils/clock/wall.go
new file mode 100644
index 0000000..9bfc351
--- /dev/null
+++ b/automation/vendor/github.com/juju/utils/clock/wall.go
@@ -0,0 +1,47 @@
+// Copyright 2015 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+package clock
+
+import (
+ "time"
+)
+
+// WallClock exposes wall-clock time via the Clock interface.
+var WallClock wallClock
+
+// ensure that WallClock does actually implement the Clock interface.
+var _ Clock = WallClock
+
+// WallClock exposes wall-clock time as returned by time.Now.
+type wallClock struct{}
+
+// Now is part of the Clock interface.
+func (wallClock) Now() time.Time {
+ return time.Now()
+}
+
+// After implements Clock.After.
+func (wallClock) After(d time.Duration) <-chan time.Time {
+ return time.After(d)
+}
+
+// AfterFunc implements Clock.AfterFunc.
+func (wallClock) AfterFunc(d time.Duration, f func()) Timer {
+ return wallTimer{time.AfterFunc(d, f)}
+}
+
+// NewTimer implements Clock.NewTimer.
+func (wallClock) NewTimer(d time.Duration) Timer {
+ return wallTimer{time.NewTimer(d)}
+}
+
+// wallTimer implements the Timer interface.
+type wallTimer struct {
+ *time.Timer
+}
+
+// Chan implements Timer.Chan.
+func (t wallTimer) Chan() <-chan time.Time {
+ return t.C
+}
diff --git a/automation/vendor/github.com/juju/utils/command.go b/automation/vendor/github.com/juju/utils/command.go
new file mode 100644
index 0000000..4bd51cd
--- /dev/null
+++ b/automation/vendor/github.com/juju/utils/command.go
@@ -0,0 +1,19 @@
+// Copyright 2013 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+package utils
+
+import (
+ "os/exec"
+)
+
+// RunCommand executes the command and return the combined output.
+func RunCommand(command string, args ...string) (output string, err error) {
+ cmd := exec.Command(command, args...)
+ out, err := cmd.CombinedOutput()
+ output = string(out)
+ if err != nil {
+ return output, err
+ }
+ return output, nil
+}
diff --git a/automation/vendor/github.com/juju/utils/dependencies.tsv b/automation/vendor/github.com/juju/utils/dependencies.tsv
new file mode 100644
index 0000000..76d098e
--- /dev/null
+++ b/automation/vendor/github.com/juju/utils/dependencies.tsv
@@ -0,0 +1,24 @@
+github.com/juju/cmd git 7c57a7d5a20602e4563a83f2d530283ca0e6f481 2016-08-10T12:53:08Z
+github.com/juju/errors git 1b5e39b83d1835fa480e0c2ddefb040ee82d58b3 2015-09-16T12:56:42Z
+github.com/juju/gnuflag git 4e76c56581859c14d9d87e1ddbe29e1c0f10195f 2016-08-09T16:52:14Z
+github.com/juju/httpprof git 14bf14c307672fd2456bdbf35d19cf0ccd3cf565 2014-12-17T16:00:36Z
+github.com/juju/httprequest git 89d547093c45e293599088cc63e805c6f1205dc0 2016-03-02T10:09:58Z
+github.com/juju/loggo git 15901ae4de786d05edae84a27c93d3fbef66c91e 2016-08-04T22:15:26Z
+github.com/juju/retry git 62c62032529169c7ec02fa48f93349604c345e1f 2015-10-29T02:48:21Z
+github.com/juju/testing git 7177264a582e2c00d08277eaa91d88f8eb0fd869 2016-09-26T12:59:16Z
+github.com/juju/version git 4ae6172c00626779a5a462c3e3d22fc0e889431a 2016-06-03T19:49:58Z
+github.com/julienschmidt/httprouter git 77a895ad01ebc98a4dc95d8355bc825ce80a56f6 2015-10-13T22:55:20Z
+github.com/masterzen/azure-sdk-for-go git ee4f0065d00cd12b542f18f5bc45799e88163b12 2016-07-20T05:16:58Z
+github.com/masterzen/simplexml git 4572e39b1ab9fe03ee513ce6fc7e289e98482190 2016-06-08T18:30:07Z
+github.com/masterzen/winrm git 7a535cd943fccaeed196718896beec3fb51aff41 2016-08-04T09:38:27Z
+github.com/masterzen/xmlpath git 13f4951698adc0fa9c1dda3e275d489a24201161 2014-02-18T18:59:01Z
+github.com/nu7hatch/gouuid git 179d4d0c4d8d407a32af483c2354df1d2c91e6c3 2016-02-18t18:59:01Z
+golang.org/x/crypto git aedad9a179ec1ea11b7064c57cbc6dc30d7724ec 2015-08-30T18:06:42Z
+golang.org/x/net git ea47fc708ee3e20177f3ca3716217c4ab75942cb 2015-08-29T23:03:18Z
+golang.org/x/text git b01949dc0793a9af5e4cb3fce4d42999e76e8ca1 2016-05-25T23:07:23Z
+gopkg.in/check.v1 git 4f90aeace3a26ad7021961c297b22c42160c7b25 2016-01-05T16:49:36Z
+gopkg.in/errgo.v1 git 66cb46252b94c1f3d65646f54ee8043ab38d766c 2015-10-07T15:31:57Z
+gopkg.in/juju/names.v2 git e38bc90539f22af61a9c656d35068bd5f0a5b30a 2016-05-25T23:07:23Z
+gopkg.in/mgo.v2 git 4d04138ffef2791c479c0c8bbffc30b34081b8d9 2015-10-26T16:34:53Z
+gopkg.in/tomb.v1 git dd632973f1e7218eb1089048e0798ec9ae7dceb8 2014-10-24T13:56:13Z
+gopkg.in/yaml.v2 git a83829b6f1293c91addabc89d0571c246397bbf4 2016-03-01T20:40:22Z
diff --git a/automation/vendor/github.com/juju/utils/file.go b/automation/vendor/github.com/juju/utils/file.go
new file mode 100644
index 0000000..75fe5a6
--- /dev/null
+++ b/automation/vendor/github.com/juju/utils/file.go
@@ -0,0 +1,149 @@
+// Copyright 2013 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+package utils
+
+import (
+ "fmt"
+ "io"
+ "io/ioutil"
+ "os"
+ "path"
+ "path/filepath"
+ "regexp"
+)
+
+// UserHomeDir returns the home directory for the specified user, or the
+// home directory for the current user if the specified user is empty.
+func UserHomeDir(userName string) (hDir string, err error) {
+ if userName == "" {
+ // TODO (wallyworld) - fix tests on Windows
+ // Ordinarily, we'd always use user.Current() to get the current user
+ // and then get the HomeDir from that. But our tests rely on poking
+ // a value into $HOME in order to override the normal home dir for the
+ // current user. So we're forced to use Home() to make the tests pass.
+ // All of our tests currently construct paths with the default user in
+ // mind eg "~/foo".
+ return Home(), nil
+ }
+ hDir, err = homeDir(userName)
+ if err != nil {
+ return "", err
+ }
+ return hDir, nil
+}
+
+// Only match paths starting with ~ (~user/test, ~/test). This will prevent
+// accidental expansion on Windows when short form paths are present (C:\users\ADMINI~1\test)
+var userHomePathRegexp = regexp.MustCompile("(^~(?P<user>[^/]*))(?P<path>.*)")
+
+// NormalizePath expands a path containing ~ to its absolute form,
+// and removes any .. or . path elements.
+func NormalizePath(dir string) (string, error) {
+ if userHomePathRegexp.MatchString(dir) {
+ user := userHomePathRegexp.ReplaceAllString(dir, "$user")
+ userHomeDir, err := UserHomeDir(user)
+ if err != nil {
+ return "", err
+ }
+ dir = userHomePathRegexp.ReplaceAllString(dir, fmt.Sprintf("%s$path", userHomeDir))
+ }
+ return filepath.Clean(dir), nil
+}
+
+// EnsureBaseDir ensures that path is always prefixed by baseDir,
+// allowing for the fact that path might have a Window drive letter in
+// it.
+func EnsureBaseDir(baseDir, path string) string {
+ if baseDir == "" {
+ return path
+ }
+ volume := filepath.VolumeName(path)
+ return filepath.Join(baseDir, path[len(volume):])
+}
+
+// JoinServerPath joins any number of path elements into a single path, adding
+// a path separator (based on the current juju server OS) if necessary. The
+// result is Cleaned; in particular, all empty strings are ignored.
+func JoinServerPath(elem ...string) string {
+ return path.Join(elem...)
+}
+
+// UniqueDirectory returns "path/name" if that directory doesn't exist. If it
+// does, the method starts appending .1, .2, etc until a unique name is found.
+func UniqueDirectory(path, name string) (string, error) {
+ dir := filepath.Join(path, name)
+ _, err := os.Stat(dir)
+ if os.IsNotExist(err) {
+ return dir, nil
+ }
+ for i := 1; ; i++ {
+ dir := filepath.Join(path, fmt.Sprintf("%s.%d", name, i))
+ _, err := os.Stat(dir)
+ if os.IsNotExist(err) {
+ return dir, nil
+ } else if err != nil {
+ return "", err
+ }
+ }
+}
+
+// CopyFile writes the contents of the given source file to dest.
+func CopyFile(dest, source string) error {
+ df, err := os.Create(dest)
+ if err != nil {
+ return err
+ }
+ f, err := os.Open(source)
+ if err != nil {
+ return err
+ }
+ defer f.Close()
+ _, err = io.Copy(df, f)
+ return err
+}
+
+// AtomicWriteFileAndChange atomically writes the filename with the
+// given contents and calls the given function after the contents were
+// written, but before the file is renamed.
+func AtomicWriteFileAndChange(filename string, contents []byte, change func(*os.File) error) (err error) {
+ dir, file := filepath.Split(filename)
+ f, err := ioutil.TempFile(dir, file)
+ if err != nil {
+ return fmt.Errorf("cannot create temp file: %v", err)
+ }
+ defer f.Close()
+ defer func() {
+ if err != nil {
+ // Don't leave the temp file lying around on error.
+ // Close the file before removing. Trying to remove an open file on
+ // Windows will fail.
+ f.Close()
+ os.Remove(f.Name())
+ }
+ }()
+ if _, err := f.Write(contents); err != nil {
+ return fmt.Errorf("cannot write %q contents: %v", filename, err)
+ }
+ if err := change(f); err != nil {
+ return err
+ }
+ f.Close()
+ if err := ReplaceFile(f.Name(), filename); err != nil {
+ return fmt.Errorf("cannot replace %q with %q: %v", f.Name(), filename, err)
+ }
+ return nil
+}
+
+// AtomicWriteFile atomically writes the filename with the given
+// contents and permissions, replacing any existing file at the same
+// path.
+func AtomicWriteFile(filename string, contents []byte, perms os.FileMode) (err error) {
+ return AtomicWriteFileAndChange(filename, contents, func(f *os.File) error {
+ // FileMod.Chmod() is not implemented on Windows, however, os.Chmod() is
+ if err := os.Chmod(f.Name(), perms); err != nil {
+ return fmt.Errorf("cannot set permissions: %v", err)
+ }
+ return nil
+ })
+}
diff --git a/automation/vendor/github.com/juju/utils/file_unix.go b/automation/vendor/github.com/juju/utils/file_unix.go
new file mode 100644
index 0000000..4380290
--- /dev/null
+++ b/automation/vendor/github.com/juju/utils/file_unix.go
@@ -0,0 +1,75 @@
+// Copyright 2013 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+// +build !windows
+
+package utils
+
+import (
+ "fmt"
+ "os"
+ "os/user"
+ "strconv"
+ "strings"
+
+ "github.com/juju/errors"
+)
+
+func homeDir(userName string) (string, error) {
+ u, err := user.Lookup(userName)
+ if err != nil {
+ return "", errors.NewUserNotFound(err, "no such user")
+ }
+ return u.HomeDir, nil
+}
+
+// MoveFile atomically moves the source file to the destination, returning
+// whether the file was moved successfully. If the destination already exists,
+// it returns an error rather than overwrite it.
+//
+// On unix systems, an error may occur with a successful move, if the source
+// file location cannot be unlinked.
+func MoveFile(source, destination string) (bool, error) {
+ err := os.Link(source, destination)
+ if err != nil {
+ return false, err
+ }
+ err = os.Remove(source)
+ if err != nil {
+ return true, err
+ }
+ return true, nil
+}
+
+// ReplaceFile atomically replaces the destination file or directory
+// with the source. The errors that are returned are identical to
+// those returned by os.Rename.
+func ReplaceFile(source, destination string) error {
+ return os.Rename(source, destination)
+}
+
+// MakeFileURL returns a file URL if a directory is passed in else it does nothing
+func MakeFileURL(in string) string {
+ if strings.HasPrefix(in, "/") {
+ return "file://" + in
+ }
+ return in
+}
+
+// ChownPath sets the uid and gid of path to match that of the user
+// specified.
+func ChownPath(path, username string) error {
+ u, err := user.Lookup(username)
+ if err != nil {
+ return fmt.Errorf("cannot lookup %q user id: %v", username, err)
+ }
+ uid, err := strconv.Atoi(u.Uid)
+ if err != nil {
+ return fmt.Errorf("invalid user id %q: %v", u.Uid, err)
+ }
+ gid, err := strconv.Atoi(u.Gid)
+ if err != nil {
+ return fmt.Errorf("invalid group id %q: %v", u.Gid, err)
+ }
+ return os.Chown(path, uid, gid)
+}
diff --git a/automation/vendor/github.com/juju/utils/file_windows.go b/automation/vendor/github.com/juju/utils/file_windows.go
new file mode 100644
index 0000000..9bb3936
--- /dev/null
+++ b/automation/vendor/github.com/juju/utils/file_windows.go
@@ -0,0 +1,142 @@
+// Copyright 2013 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+// +build windows
+
+package utils
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "syscall"
+ "unsafe"
+
+ "github.com/juju/errors"
+)
+
+const (
+ movefile_replace_existing = 0x1
+ movefile_write_through = 0x8
+)
+
+//sys moveFileEx(lpExistingFileName *uint16, lpNewFileName *uint16, dwFlags uint32) (err error) = MoveFileExW
+
+// MoveFile atomically moves the source file to the destination, returning
+// whether the file was moved successfully. If the destination already exists,
+// it returns an error rather than overwrite it.
+func MoveFile(source, destination string) (bool, error) {
+ src, err := syscall.UTF16PtrFromString(source)
+ if err != nil {
+ return false, &os.LinkError{"move", source, destination, err}
+ }
+ dest, err := syscall.UTF16PtrFromString(destination)
+ if err != nil {
+ return false, &os.LinkError{"move", source, destination, err}
+ }
+
+ // see http://msdn.microsoft.com/en-us/library/windows/desktop/aa365240(v=vs.85).aspx
+ if err := moveFileEx(src, dest, movefile_write_through); err != nil {
+ return false, &os.LinkError{"move", source, destination, err}
+ }
+ return true, nil
+
+}
+
+// ReplaceFile atomically replaces the destination file or directory with the source.
+// The errors that are returned are identical to those returned by os.Rename.
+func ReplaceFile(source, destination string) error {
+ src, err := syscall.UTF16PtrFromString(source)
+ if err != nil {
+ return &os.LinkError{"replace", source, destination, err}
+ }
+ dest, err := syscall.UTF16PtrFromString(destination)
+ if err != nil {
+ return &os.LinkError{"replace", source, destination, err}
+ }
+
+ // see http://msdn.microsoft.com/en-us/library/windows/desktop/aa365240(v=vs.85).aspx
+ if err := moveFileEx(src, dest, movefile_replace_existing|movefile_write_through); err != nil {
+ return &os.LinkError{"replace", source, destination, err}
+ }
+ return nil
+}
+
+// MakeFileURL returns a proper file URL for the given path/directory
+func MakeFileURL(in string) string {
+ in = filepath.ToSlash(in)
+ // for windows at least should be <letter>: to be considered valid
+ // so we cant do anything with less than that.
+ if len(in) < 2 {
+ return in
+ }
+ if string(in[1]) != ":" {
+ return in
+ }
+ // since go 1.6 http client will only take this format.
+ return "file://" + in
+}
+
+func getUserSID(username string) (string, error) {
+ sid, _, _, e := syscall.LookupSID("", username)
+ if e != nil {
+ return "", e
+ }
+ sidStr, err := sid.String()
+ return sidStr, err
+}
+
+func readRegString(h syscall.Handle, key string) (value string, err error) {
+ var typ uint32
+ var buf uint32
+
+ // Get size of registry key
+ err = syscall.RegQueryValueEx(h, syscall.StringToUTF16Ptr(key), nil, &typ, nil, &buf)
+ if err != nil {
+ return value, err
+ }
+
+ n := make([]uint16, buf/2+1)
+ err = syscall.RegQueryValueEx(h, syscall.StringToUTF16Ptr(key), nil, &typ, (*byte)(unsafe.Pointer(&n[0])), &buf)
+ if err != nil {
+ return value, err
+ }
+ return syscall.UTF16ToString(n[:]), err
+}
+
+func homeFromRegistry(sid string) (string, error) {
+ var h syscall.Handle
+ // This key will exist on all platforms we support the agent on (windows server 2008 and above)
+ keyPath := fmt.Sprintf("Software\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList\\%s", sid)
+ err := syscall.RegOpenKeyEx(syscall.HKEY_LOCAL_MACHINE,
+ syscall.StringToUTF16Ptr(keyPath),
+ 0, syscall.KEY_READ, &h)
+ if err != nil {
+ return "", err
+ }
+ defer syscall.RegCloseKey(h)
+ str, err := readRegString(h, "ProfileImagePath")
+ if err != nil {
+ return "", err
+ }
+ return str, nil
+}
+
+// homeDir returns a local user home dir on Windows
+// user.Lookup() does not populate Gid and HomeDir on Windows,
+// so we get it from the registry
+func homeDir(user string) (string, error) {
+ u, err := getUserSID(user)
+ if err != nil {
+ return "", errors.NewUserNotFound(err, "no such user")
+ }
+ return homeFromRegistry(u)
+}
+
+// ChownPath is not implemented for Windows.
+func ChownPath(path, username string) error {
+ // This only exists to allow building on Windows. User lookup and
+ // file ownership needs to be handled in a completely different
+ // way and hasn't yet been implemented.
+ return nil
+}
diff --git a/automation/vendor/github.com/juju/utils/gomaxprocs.go b/automation/vendor/github.com/juju/utils/gomaxprocs.go
new file mode 100644
index 0000000..5977a86
--- /dev/null
+++ b/automation/vendor/github.com/juju/utils/gomaxprocs.go
@@ -0,0 +1,26 @@
+// Copyright 2014 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+package utils
+
+import (
+ "os"
+ "runtime"
+)
+
+var gomaxprocs = runtime.GOMAXPROCS
+var numCPU = runtime.NumCPU
+
+// UseMultipleCPUs sets GOMAXPROCS to the number of CPU cores unless it has
+// already been overridden by the GOMAXPROCS environment variable.
+func UseMultipleCPUs() {
+ if envGOMAXPROCS := os.Getenv("GOMAXPROCS"); envGOMAXPROCS != "" {
+ n := gomaxprocs(0)
+ logger.Debugf("GOMAXPROCS already set in environment to %q, %d internally",
+ envGOMAXPROCS, n)
+ return
+ }
+ n := numCPU()
+ logger.Debugf("setting GOMAXPROCS to %d", n)
+ gomaxprocs(n)
+}
diff --git a/automation/vendor/github.com/juju/utils/home_unix.go b/automation/vendor/github.com/juju/utils/home_unix.go
new file mode 100644
index 0000000..6b450be
--- /dev/null
+++ b/automation/vendor/github.com/juju/utils/home_unix.go
@@ -0,0 +1,19 @@
+// Copyright 2013 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+// +build !windows
+
+package utils
+
+import (
+ "os"
+)
+
+// Home returns the os-specific home path as specified in the environment.
+func Home() string {
+ return os.Getenv("HOME")
+}
+
+// SetHome sets the os-specific home path in the environment.
+func SetHome(s string) error {
+ return os.Setenv("HOME", s)
+}
diff --git a/automation/vendor/github.com/juju/utils/home_windows.go b/automation/vendor/github.com/juju/utils/home_windows.go
new file mode 100644
index 0000000..e61225c
--- /dev/null
+++ b/automation/vendor/github.com/juju/utils/home_windows.go
@@ -0,0 +1,25 @@
+// Copyright 2013 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+package utils
+
+import (
+ "os"
+ "path/filepath"
+)
+
+// Home returns the os-specific home path as specified in the environment.
+func Home() string {
+ return filepath.Join(os.Getenv("HOMEDRIVE"), os.Getenv("HOMEPATH"))
+}
+
+// SetHome sets the os-specific home path in the environment.
+func SetHome(s string) error {
+ v := filepath.VolumeName(s)
+ if v != "" {
+ if err := os.Setenv("HOMEDRIVE", v); err != nil {
+ return err
+ }
+ }
+ return os.Setenv("HOMEPATH", s[len(v):])
+}
diff --git a/automation/vendor/github.com/juju/utils/http-1_4.go b/automation/vendor/github.com/juju/utils/http-1_4.go
new file mode 100644
index 0000000..87bd5b1
--- /dev/null
+++ b/automation/vendor/github.com/juju/utils/http-1_4.go
@@ -0,0 +1,24 @@
+// Copyright 2016 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+//+build !go1.7
+
+package utils
+
+import (
+ "fmt"
+ "net"
+ "net/http"
+)
+
+// installHTTPDialShim patches the default HTTP transport so
+// that it fails when an attempt is made to dial a non-local
+// host.
+func installHTTPDialShim(t *http.Transport) {
+ t.Dial = func(network, addr string) (net.Conn, error) {
+ if !OutgoingAccessAllowed && !isLocalAddr(addr) {
+ return nil, fmt.Errorf("access to address %q not allowed", addr)
+ }
+ return net.Dial(network, addr)
+ }
+}
diff --git a/automation/vendor/github.com/juju/utils/http-1_7.go b/automation/vendor/github.com/juju/utils/http-1_7.go
new file mode 100644
index 0000000..d676dcb
--- /dev/null
+++ b/automation/vendor/github.com/juju/utils/http-1_7.go
@@ -0,0 +1,35 @@
+// Copyright 2016 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+//+build go1.7
+
+package utils
+
+import (
+ "context"
+ "fmt"
+ "net"
+ "net/http"
+ "time"
+)
+
+var ctxtDialer = &net.Dialer{
+ Timeout: 30 * time.Second,
+ KeepAlive: 30 * time.Second,
+}
+
+// installHTTPDialShim patches the default HTTP transport so
+// that it fails when an attempt is made to dial a non-local
+// host.
+//
+// Note that this is Go version dependent because in Go 1.7 and above,
+// the DialContext field was introduced (and set in http.DefaultTransport)
+// which overrides the Dial field.
+func installHTTPDialShim(t *http.Transport) {
+ t.DialContext = func(ctxt context.Context, network, addr string) (net.Conn, error) {
+ if !OutgoingAccessAllowed && !isLocalAddr(addr) {
+ return nil, fmt.Errorf("access to address %q not allowed", addr)
+ }
+ return ctxtDialer.DialContext(ctxt, network, addr)
+ }
+}
diff --git a/automation/vendor/github.com/juju/utils/http.go b/automation/vendor/github.com/juju/utils/http.go
new file mode 100644
index 0000000..2f5ddf3
--- /dev/null
+++ b/automation/vendor/github.com/juju/utils/http.go
@@ -0,0 +1,117 @@
+// Copyright 2013 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+package utils
+
+import (
+ "crypto/tls"
+ "encoding/base64"
+ "fmt"
+ "net"
+ "net/http"
+ "strings"
+ "sync"
+)
+
+var insecureClient = (*http.Client)(nil)
+var insecureClientMutex = sync.Mutex{}
+
+func init() {
+ defaultTransport := http.DefaultTransport.(*http.Transport)
+ installHTTPDialShim(defaultTransport)
+ registerFileProtocol(defaultTransport)
+}
+
+// registerFileProtocol registers support for file:// URLs on the given transport.
+func registerFileProtocol(transport *http.Transport) {
+ transport.RegisterProtocol("file", http.NewFileTransport(http.Dir("/")))
+}
+
+// SSLHostnameVerification is used as a switch for when a given provider might
+// use self-signed credentials and we should not try to verify the hostname on
+// the TLS/SSL certificates
+type SSLHostnameVerification bool
+
+const (
+ // VerifySSLHostnames ensures we verify the hostname on the certificate
+ // matches the host we are connecting and is signed
+ VerifySSLHostnames = SSLHostnameVerification(true)
+ // NoVerifySSLHostnames informs us to skip verifying the hostname
+ // matches a valid certificate
+ NoVerifySSLHostnames = SSLHostnameVerification(false)
+)
+
+// GetHTTPClient returns either a standard http client or
+// non validating client depending on the value of verify.
+func GetHTTPClient(verify SSLHostnameVerification) *http.Client {
+ if verify == VerifySSLHostnames {
+ return GetValidatingHTTPClient()
+ }
+ return GetNonValidatingHTTPClient()
+}
+
+// GetValidatingHTTPClient returns a new http.Client that
+// verifies the server's certificate chain and hostname.
+func GetValidatingHTTPClient() *http.Client {
+ return &http.Client{}
+}
+
+// GetNonValidatingHTTPClient returns a new http.Client that
+// does not verify the server's certificate chain and hostname.
+func GetNonValidatingHTTPClient() *http.Client {
+ return &http.Client{
+ Transport: NewHttpTLSTransport(&tls.Config{
+ InsecureSkipVerify: true,
+ }),
+ }
+}
+
+// BasicAuthHeader creates a header that contains just the "Authorization"
+// entry. The implementation was originally taked from net/http but this is
+// needed externally from the http request object in order to use this with
+// our websockets. See 2 (end of page 4) http://www.ietf.org/rfc/rfc2617.txt
+// "To receive authorization, the client sends the userid and password,
+// separated by a single colon (":") character, within a base64 encoded string
+// in the credentials."
+func BasicAuthHeader(username, password string) http.Header {
+ auth := username + ":" + password
+ encoded := "Basic " + base64.StdEncoding.EncodeToString([]byte(auth))
+ return http.Header{
+ "Authorization": {encoded},
+ }
+}
+
+// ParseBasicAuth attempts to find an Authorization header in the supplied
+// http.Header and if found parses it as a Basic header. See 2 (end of page 4)
+// http://www.ietf.org/rfc/rfc2617.txt "To receive authorization, the client
+// sends the userid and password, separated by a single colon (":") character,
+// within a base64 encoded string in the credentials."
+func ParseBasicAuthHeader(h http.Header) (userid, password string, err error) {
+ parts := strings.Fields(h.Get("Authorization"))
+ if len(parts) != 2 || parts[0] != "Basic" {
+ return "", "", fmt.Errorf("invalid or missing HTTP auth header")
+ }
+ // Challenge is a base64-encoded "tag:pass" string.
+ // See RFC 2617, Section 2.
+ challenge, err := base64.StdEncoding.DecodeString(parts[1])
+ if err != nil {
+ return "", "", fmt.Errorf("invalid HTTP auth encoding")
+ }
+ tokens := strings.SplitN(string(challenge), ":", 2)
+ if len(tokens) != 2 {
+ return "", "", fmt.Errorf("invalid HTTP auth contents")
+ }
+ return tokens[0], tokens[1], nil
+}
+
+// OutgoingAccessAllowed determines whether connections other than
+// localhost can be dialled.
+var OutgoingAccessAllowed = true
+
+func isLocalAddr(addr string) bool {
+ host, _, err := net.SplitHostPort(addr)
+ if err != nil {
+ return false
+ }
+ return host == "localhost" || net.ParseIP(host).IsLoopback()
+}
diff --git a/automation/vendor/github.com/juju/utils/isubuntu.go b/automation/vendor/github.com/juju/utils/isubuntu.go
new file mode 100644
index 0000000..d85ed9a
--- /dev/null
+++ b/automation/vendor/github.com/juju/utils/isubuntu.go
@@ -0,0 +1,17 @@
+// Copyright 2011, 2012, 2013 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+package utils
+
+import (
+ "strings"
+)
+
+// IsUbuntu executes lxb_release to see if the host OS is Ubuntu.
+func IsUbuntu() bool {
+ out, err := RunCommand("lsb_release", "-i", "-s")
+ if err != nil {
+ return false
+ }
+ return strings.TrimSpace(out) == "Ubuntu"
+}
diff --git a/automation/vendor/github.com/juju/utils/limiter.go b/automation/vendor/github.com/juju/utils/limiter.go
new file mode 100644
index 0000000..60bd066
--- /dev/null
+++ b/automation/vendor/github.com/juju/utils/limiter.go
@@ -0,0 +1,59 @@
+// Copyright 2011, 2012, 2013 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+package utils
+
+import (
+ "fmt"
+)
+
+type empty struct{}
+type limiter chan empty
+
+// Limiter represents a limited resource (eg a semaphore).
+type Limiter interface {
+ // Acquire another unit of the resource.
+ // Acquire returns false to indicate there is no more availability,
+ // until another entity calls Release.
+ Acquire() bool
+ // AcquireWait requests a unit of resource, but blocks until one is
+ // available.
+ AcquireWait()
+ // Release returns a unit of the resource. Calling Release when there
+ // are no units Acquired is an error.
+ Release() error
+}
+
+func NewLimiter(max int) Limiter {
+ return make(limiter, max)
+}
+
+// Acquire requests some resources that you can return later
+// It returns 'true' if there are resources available, but false if they are
+// not. Callers are responsible for calling Release if this returns true, but
+// should not release if this returns false.
+func (l limiter) Acquire() bool {
+ e := empty{}
+ select {
+ case l <- e:
+ return true
+ default:
+ return false
+ }
+}
+
+// AcquireWait waits for the resource to become available before returning.
+func (l limiter) AcquireWait() {
+ e := empty{}
+ l <- e
+}
+
+// Release returns the resource to the available pool.
+func (l limiter) Release() error {
+ select {
+ case <-l:
+ return nil
+ default:
+ return fmt.Errorf("Release without an associated Acquire")
+ }
+}
diff --git a/automation/vendor/github.com/juju/utils/multireader.go b/automation/vendor/github.com/juju/utils/multireader.go
new file mode 100644
index 0000000..b8431f9
--- /dev/null
+++ b/automation/vendor/github.com/juju/utils/multireader.go
@@ -0,0 +1,189 @@
+// Copyright 2016 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+package utils
+
+import (
+ "io"
+ "sort"
+
+ "github.com/juju/errors"
+)
+
+// SizeReaderAt combines io.ReaderAt with a Size method.
+type SizeReaderAt interface {
+ // Size returns the size of the data readable
+ // from the reader.
+ Size() int64
+ io.ReaderAt
+}
+
+// NewMultiReaderAt is like io.MultiReader but produces a ReaderAt
+// (and Size), instead of just a reader.
+//
+// Note: this implementation was taken from a talk given
+// by Brad Fitzpatrick as OSCON 2013.
+//
+// http://talks.golang.org/2013/oscon-dl.slide#49
+// https://github.com/golang/talks/blob/master/2013/oscon-dl/server-compose.go
+func NewMultiReaderAt(parts ...SizeReaderAt) SizeReaderAt {
+ m := &multiReaderAt{
+ parts: make([]offsetAndSource, 0, len(parts)),
+ }
+ var off int64
+ for _, p := range parts {
+ m.parts = append(m.parts, offsetAndSource{off, p})
+ off += p.Size()
+ }
+ m.size = off
+ return m
+}
+
+type offsetAndSource struct {
+ off int64
+ SizeReaderAt
+}
+
+type multiReaderAt struct {
+ parts []offsetAndSource
+ size int64
+}
+
+func (m *multiReaderAt) Size() int64 {
+ return m.size
+}
+
+func (m *multiReaderAt) ReadAt(p []byte, off int64) (n int, err error) {
+ wantN := len(p)
+
+ // Skip past the requested offset.
+ skipParts := sort.Search(len(m.parts), func(i int) bool {
+ // This function returns whether parts[i] will
+ // contribute any bytes to our output.
+ part := m.parts[i]
+ return part.off+part.Size() > off
+ })
+ parts := m.parts[skipParts:]
+
+ // How far to skip in the first part.
+ needSkip := off
+ if len(parts) > 0 {
+ needSkip -= parts[0].off
+ }
+
+ for len(parts) > 0 && len(p) > 0 {
+ readP := p
+ partSize := parts[0].Size()
+ if int64(len(readP)) > partSize-needSkip {
+ readP = readP[:partSize-needSkip]
+ }
+ pn, err0 := parts[0].ReadAt(readP, needSkip)
+ if err0 != nil {
+ return n, err0
+ }
+ n += pn
+ p = p[pn:]
+ if int64(pn)+needSkip == partSize {
+ parts = parts[1:]
+ }
+ needSkip = 0
+ }
+
+ if n != wantN {
+ err = io.ErrUnexpectedEOF
+ }
+ return
+}
+
+// NewMultiReaderSeeker returns an io.ReadSeeker that combines
+// all the given readers into a single one. It assumes that
+// all the seekers are initially positioned at the start.
+func NewMultiReaderSeeker(readers ...io.ReadSeeker) io.ReadSeeker {
+ sreaders := make([]SizeReaderAt, len(readers))
+ for i, r := range readers {
+ r1, err := newSizeReaderAt(r)
+ if err != nil {
+ panic(err)
+ }
+ sreaders[i] = r1
+ }
+ return &readSeeker{
+ r: NewMultiReaderAt(sreaders...),
+ }
+}
+
+// newSizeReaderAt adapts an io.ReadSeeker to a SizeReaderAt.
+// Note that it doesn't strictly adhere to the ReaderAt
+// contract because it's not safe to call ReadAt concurrently.
+// This doesn't matter because io.ReadSeeker doesn't
+// need to be thread-safe and this is only used in that
+// context.
+func newSizeReaderAt(r io.ReadSeeker) (SizeReaderAt, error) {
+ size, err := r.Seek(0, 2)
+ if err != nil {
+ return nil, err
+ }
+ return &sizeReaderAt{
+ r: r,
+ size: size,
+ off: size,
+ }, nil
+}
+
+// sizeReaderAt adapts an io.ReadSeeker to a SizeReaderAt.
+type sizeReaderAt struct {
+ r io.ReadSeeker
+ size int64
+ off int64
+}
+
+// ReadAt implemnts SizeReaderAt.ReadAt.
+func (r *sizeReaderAt) ReadAt(buf []byte, off int64) (n int, err error) {
+ if off != r.off {
+ _, err = r.r.Seek(off, 0)
+ if err != nil {
+ return 0, err
+ }
+ r.off = off
+ }
+ n, err = io.ReadFull(r.r, buf)
+ r.off += int64(n)
+ return n, err
+}
+
+// Size implemnts SizeReaderAt.Size.
+func (r *sizeReaderAt) Size() int64 {
+ return r.size
+}
+
+// readSeeker adapts a SizeReaderAt to an io.ReadSeeker.
+type readSeeker struct {
+ r SizeReaderAt
+ off int64
+}
+
+// Seek implements io.Seeker.Seek.
+func (r *readSeeker) Seek(off int64, whence int) (int64, error) {
+ switch whence {
+ case 0:
+ case 1:
+ off += r.off
+ case 2:
+ off = r.r.Size() + off
+ }
+ if off < 0 {
+ return 0, errors.New("negative position")
+ }
+ r.off = off
+ return off, nil
+}
+
+// Read implements io.Reader.Read.
+func (r *readSeeker) Read(buf []byte) (int, error) {
+ n, err := r.r.ReadAt(buf, r.off)
+ r.off += int64(n)
+ if err == io.ErrUnexpectedEOF {
+ err = io.EOF
+ }
+ return n, err
+}
diff --git a/automation/vendor/github.com/juju/utils/naturalsort.go b/automation/vendor/github.com/juju/utils/naturalsort.go
new file mode 100644
index 0000000..d337093
--- /dev/null
+++ b/automation/vendor/github.com/juju/utils/naturalsort.go
@@ -0,0 +1,95 @@
+// Copyright 2016 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+package utils
+
+import (
+ "fmt"
+ "sort"
+ "strconv"
+ "unicode"
+)
+
+// SortStringsNaturally sorts strings according to their natural sort order.
+func SortStringsNaturally(s []string) []string {
+ sort.Sort(naturally(s))
+ return s
+}
+
+type naturally []string
+
+func (n naturally) Len() int {
+ return len(n)
+}
+
+func (n naturally) Swap(a, b int) {
+ n[a], n[b] = n[b], n[a]
+}
+
+// Less sorts by non-numeric prefix and numeric suffix
+// when one exists.
+func (n naturally) Less(a, b int) bool {
+ aVal := n[a]
+ bVal := n[b]
+
+ for {
+ // If bVal is empty, then aVal can't be less than it.
+ if bVal == "" {
+ return false
+ }
+ // If aVal is empty here, then is must be less than bVal.
+ if aVal == "" {
+ return true
+ }
+
+ aPrefix, aNumber, aRemainder := splitAtNumber(aVal)
+ bPrefix, bNumber, bRemainder := splitAtNumber(bVal)
+ if aPrefix != bPrefix {
+ return aPrefix < bPrefix
+ }
+ if aNumber != bNumber {
+ return aNumber < bNumber
+ }
+
+ // Everything is the same so far, try again with the remainer.
+ aVal = aRemainder
+ bVal = bRemainder
+ }
+}
+
+// splitAtNumber splits given string at the first digit, returning the
+// prefix before the number, the integer represented by the first
+// series of digits, and the remainder of the string after the first
+// series of digits. If no digits are present, the number is returned
+// as -1 and the remainder is empty.
+func splitAtNumber(str string) (string, int, string) {
+ i := indexOfDigit(str)
+ if i == -1 {
+ // no numbers
+ return str, -1, ""
+ }
+ j := i + indexOfNonDigit(str[i:])
+ n, err := strconv.Atoi(str[i:j])
+ if err != nil {
+ panic(fmt.Sprintf("parsing number %v: %v", str[i:j], err)) // should never happen
+ }
+ return str[:i], n, str[j:]
+}
+
+func indexOfDigit(str string) int {
+ for i, rune := range str {
+ if unicode.IsDigit(rune) {
+ return i
+ }
+ }
+ return -1
+}
+
+func indexOfNonDigit(str string) int {
+ for i, rune := range str {
+ if !unicode.IsDigit(rune) {
+ return i
+ }
+ }
+ return len(str)
+}
diff --git a/automation/vendor/github.com/juju/utils/network.go b/automation/vendor/github.com/juju/utils/network.go
new file mode 100644
index 0000000..505a6e9
--- /dev/null
+++ b/automation/vendor/github.com/juju/utils/network.go
@@ -0,0 +1,46 @@
+// Copyright 2013 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+package utils
+
+import (
+ "fmt"
+ "net"
+
+ "github.com/juju/loggo"
+)
+
+var logger = loggo.GetLogger("juju.utils")
+
+// GetIPv4Address iterates through the addresses expecting the format from
+// func (ifi *net.Interface) Addrs() ([]net.Addr, error)
+func GetIPv4Address(addresses []net.Addr) (string, error) {
+ for _, addr := range addresses {
+ ip, _, err := net.ParseCIDR(addr.String())
+ if err != nil {
+ return "", err
+ }
+ ipv4 := ip.To4()
+ if ipv4 == nil {
+ continue
+ }
+ return ipv4.String(), nil
+ }
+ return "", fmt.Errorf("no addresses match")
+}
+
+// GetAddressForInterface looks for the network interface
+// and returns the IPv4 address from the possible addresses.
+func GetAddressForInterface(interfaceName string) (string, error) {
+ iface, err := net.InterfaceByName(interfaceName)
+ if err != nil {
+ logger.Errorf("cannot find network interface %q: %v", interfaceName, err)
+ return "", err
+ }
+ addrs, err := iface.Addrs()
+ if err != nil {
+ logger.Errorf("cannot get addresses for network interface %q: %v", interfaceName, err)
+ return "", err
+ }
+ return GetIPv4Address(addrs)
+}
diff --git a/automation/vendor/github.com/juju/utils/os.go b/automation/vendor/github.com/juju/utils/os.go
new file mode 100644
index 0000000..146f793
--- /dev/null
+++ b/automation/vendor/github.com/juju/utils/os.go
@@ -0,0 +1,41 @@
+// Copyright 2015 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+package utils
+
+// These are the names of the operating systems recognized by Go.
+const (
+ OSWindows = "windows"
+ OSDarwin = "darwin"
+ OSDragonfly = "dragonfly"
+ OSFreebsd = "freebsd"
+ OSLinux = "linux"
+ OSNacl = "nacl"
+ OSNetbsd = "netbsd"
+ OSOpenbsd = "openbsd"
+ OSSolaris = "solaris"
+)
+
+// OSUnix is the list of unix-like operating systems recognized by Go.
+// See http://golang.org/src/path/filepath/path_unix.go.
+var OSUnix = []string{
+ OSDarwin,
+ OSDragonfly,
+ OSFreebsd,
+ OSLinux,
+ OSNacl,
+ OSNetbsd,
+ OSOpenbsd,
+ OSSolaris,
+}
+
+// OSIsUnix determines whether or not the given OS name is one of the
+// unix-like operating systems recognized by Go.
+func OSIsUnix(os string) bool {
+ for _, goos := range OSUnix {
+ if os == goos {
+ return true
+ }
+ }
+ return false
+}
diff --git a/automation/vendor/github.com/juju/utils/password.go b/automation/vendor/github.com/juju/utils/password.go
new file mode 100644
index 0000000..914e8e4
--- /dev/null
+++ b/automation/vendor/github.com/juju/utils/password.go
@@ -0,0 +1,92 @@
+// Copyright 2012, 2013 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+package utils
+
+import (
+ "crypto/rand"
+ "crypto/sha512"
+ "encoding/base64"
+ "fmt"
+ "io"
+
+ "golang.org/x/crypto/pbkdf2"
+)
+
+// CompatSalt is because Juju 1.16 and older used a hard-coded salt to compute
+// the password hash for all users and agents
+var CompatSalt = string([]byte{0x75, 0x82, 0x81, 0xca})
+
+const randomPasswordBytes = 18
+
+// MinAgentPasswordLength describes how long agent passwords should be. We
+// require this length because we assume enough entropy in the Agent password
+// that it is safe to not do extra rounds of iterated hashing.
+var MinAgentPasswordLength = base64.StdEncoding.EncodedLen(randomPasswordBytes)
+
+// RandomBytes returns n random bytes.
+func RandomBytes(n int) ([]byte, error) {
+ buf := make([]byte, n)
+ _, err := io.ReadFull(rand.Reader, buf)
+ if err != nil {
+ return nil, fmt.Errorf("cannot read random bytes: %v", err)
+ }
+ return buf, nil
+}
+
+// RandomPassword generates a random base64-encoded password.
+func RandomPassword() (string, error) {
+ b, err := RandomBytes(randomPasswordBytes)
+ if err != nil {
+ return "", err
+ }
+ return base64.StdEncoding.EncodeToString(b), nil
+}
+
+// RandomSalt generates a random base64 data suitable for using as a password
+// salt The pbkdf2 guideline is to use 8 bytes of salt, so we do 12 raw bytes
+// into 16 base64 bytes. (The alternative is 6 raw into 8 base64).
+func RandomSalt() (string, error) {
+ b, err := RandomBytes(12)
+ if err != nil {
+ return "", err
+ }
+ return base64.StdEncoding.EncodeToString(b), nil
+}
+
+// FastInsecureHash specifies whether a fast, insecure version of the hash
+// algorithm will be used. Changing this will cause PasswordHash to
+// produce incompatible passwords. It should only be changed for
+// testing purposes - to make tests run faster.
+var FastInsecureHash = false
+
+// UserPasswordHash returns base64-encoded one-way hash password that is
+// computationally hard to crack by iterating through possible passwords.
+func UserPasswordHash(password string, salt string) string {
+ if salt == "" {
+ panic("salt is not allowed to be empty")
+ }
+ iter := 8192
+ if FastInsecureHash {
+ iter = 1
+ }
+ // Generate 18 byte passwords because we know that MongoDB
+ // uses the MD5 sum of the password anyway, so there's
+ // no point in using more bytes. (18 so we don't get base 64
+ // padding characters).
+ h := pbkdf2.Key([]byte(password), []byte(salt), iter, 18, sha512.New)
+ return base64.StdEncoding.EncodeToString(h)
+}
+
+// AgentPasswordHash returns base64-encoded one-way hash of password. This is
+// not suitable for User passwords because those will have limited entropy (see
+// UserPasswordHash). However, since we generate long random passwords for
+// agents, we can trust that there is sufficient entropy to prevent brute force
+// search. And using a faster hash allows us to restart the state machines and
+// have 1000s of agents log in in a reasonable amount of time.
+func AgentPasswordHash(password string) string {
+ sum := sha512.New()
+ sum.Write([]byte(password))
+ h := sum.Sum(nil)
+ return base64.StdEncoding.EncodeToString(h[:18])
+}
diff --git a/automation/vendor/github.com/juju/utils/randomstring.go b/automation/vendor/github.com/juju/utils/randomstring.go
new file mode 100644
index 0000000..662f514
--- /dev/null
+++ b/automation/vendor/github.com/juju/utils/randomstring.go
@@ -0,0 +1,42 @@
+// Copyright 2015 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+package utils
+
+import (
+ "math/rand"
+ "sync"
+ "time"
+)
+
+// Can be used as a sane default argument for RandomString
+var (
+ LowerAlpha = []rune("abcdefghijklmnopqrstuvwxyz")
+ UpperAlpha = []rune("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
+ Digits = []rune("0123456789")
+)
+
+var (
+ randomStringMu sync.Mutex
+ randomStringRand *rand.Rand
+)
+
+func init() {
+ randomStringRand = rand.New(
+ rand.NewSource(time.Now().UnixNano()),
+ )
+}
+
+// RandomString will return a string of length n that will only
+// contain runes inside validRunes
+func RandomString(n int, validRunes []rune) string {
+ randomStringMu.Lock()
+ defer randomStringMu.Unlock()
+
+ runes := make([]rune, n)
+ for i := range runes {
+ runes[i] = validRunes[randomStringRand.Intn(len(validRunes))]
+ }
+
+ return string(runes)
+}
diff --git a/automation/vendor/github.com/juju/utils/relativeurl.go b/automation/vendor/github.com/juju/utils/relativeurl.go
new file mode 100644
index 0000000..b70f1d5
--- /dev/null
+++ b/automation/vendor/github.com/juju/utils/relativeurl.go
@@ -0,0 +1,62 @@
+// Copyright 2016 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+package utils
+
+import (
+ "strings"
+
+ "github.com/juju/errors"
+)
+
+// RelativeURLPath returns a relative URL path that is lexically
+// equivalent to targpath when interpreted by url.URL.ResolveReference.
+// On success, the returned path will always be non-empty and relative
+// to basePath, even if basePath and targPath share no elements.
+//
+// It is assumed that both basePath and targPath are normalized
+// (have no . or .. elements).
+//
+// An error is returned if basePath or targPath are not absolute paths.
+func RelativeURLPath(basePath, targPath string) (string, error) {
+ if !strings.HasPrefix(basePath, "/") {
+ return "", errors.New("non-absolute base URL")
+ }
+ if !strings.HasPrefix(targPath, "/") {
+ return "", errors.New("non-absolute target URL")
+ }
+ baseParts := strings.Split(basePath, "/")
+ targParts := strings.Split(targPath, "/")
+
+ // For the purposes of dotdot, the last element of
+ // the paths are irrelevant. We save the last part
+ // of the target path for later.
+ lastElem := targParts[len(targParts)-1]
+ baseParts = baseParts[0 : len(baseParts)-1]
+ targParts = targParts[0 : len(targParts)-1]
+
+ // Find the common prefix between the two paths:
+ var i int
+ for ; i < len(baseParts); i++ {
+ if i >= len(targParts) || baseParts[i] != targParts[i] {
+ break
+ }
+ }
+ dotdotCount := len(baseParts) - i
+ targOnly := targParts[i:]
+ result := make([]string, 0, dotdotCount+len(targOnly)+1)
+ for i := 0; i < dotdotCount; i++ {
+ result = append(result, "..")
+ }
+ result = append(result, targOnly...)
+ result = append(result, lastElem)
+ final := strings.Join(result, "/")
+ if final == "" {
+ // If the final result is empty, the last element must
+ // have been empty, so the target was slash terminated
+ // and there were no previous elements, so "."
+ // is appropriate.
+ final = "."
+ }
+ return final, nil
+}
diff --git a/automation/vendor/github.com/juju/utils/set/ints.go b/automation/vendor/github.com/juju/utils/set/ints.go
new file mode 100644
index 0000000..02009ea
--- /dev/null
+++ b/automation/vendor/github.com/juju/utils/set/ints.go
@@ -0,0 +1,112 @@
+// Copyright 2014 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+package set
+
+import (
+ "sort"
+)
+
+// Ints represents the classic "set" data structure, and contains ints.
+type Ints map[int]bool
+
+// NewInts creates and initializes an Ints and populates it with
+// initial values as specified in the parameters.
+func NewInts(initial ...int) Ints {
+ result := make(Ints)
+ for _, value := range initial {
+ result.Add(value)
+ }
+ return result
+}
+
+// Size returns the number of elements in the set.
+func (is Ints) Size() int {
+ return len(is)
+}
+
+// IsEmpty is true for empty or uninitialized sets.
+func (is Ints) IsEmpty() bool {
+ return len(is) == 0
+}
+
+// Add puts a value into the set.
+func (is Ints) Add(value int) {
+ if is == nil {
+ panic("uninitalised set")
+ }
+ is[value] = true
+}
+
+// Remove takes a value out of the set. If value wasn't in the set to start
+// with, this method silently succeeds.
+func (is Ints) Remove(value int) {
+ delete(is, value)
+}
+
+// Contains returns true if the value is in the set, and false otherwise.
+func (is Ints) Contains(value int) bool {
+ _, exists := is[value]
+ return exists
+}
+
+// Values returns an unordered slice containing all the values in the set.
+func (is Ints) Values() []int {
+ result := make([]int, len(is))
+ i := 0
+ for key := range is {
+ result[i] = key
+ i++
+ }
+ return result
+}
+
+// SortedValues returns an ordered slice containing all the values in the set.
+func (is Ints) SortedValues() []int {
+ values := is.Values()
+ sort.Ints(values)
+ return values
+}
+
+// Union returns a new Ints representing a union of the elments in the
+// method target and the parameter.
+func (is Ints) Union(other Ints) Ints {
+ result := make(Ints)
+ // Use the internal map rather than going through the friendlier functions
+ // to avoid extra allocation of slices.
+ for value := range is {
+ result[value] = true
+ }
+ for value := range other {
+ result[value] = true
+ }
+ return result
+}
+
+// Intersection returns a new Ints representing a intersection of the elments in the
+// method target and the parameter.
+func (is Ints) Intersection(other Ints) Ints {
+ result := make(Ints)
+ // Use the internal map rather than going through the friendlier functions
+ // to avoid extra allocation of slices.
+ for value := range is {
+ if other.Contains(value) {
+ result[value] = true
+ }
+ }
+ return result
+}
+
+// Difference returns a new Ints representing all the values in the
+// target that are not in the parameter.
+func (is Ints) Difference(other Ints) Ints {
+ result := make(Ints)
+ // Use the internal map rather than going through the friendlier functions
+ // to avoid extra allocation of slices.
+ for value := range is {
+ if !other.Contains(value) {
+ result[value] = true
+ }
+ }
+ return result
+}
diff --git a/automation/vendor/github.com/juju/utils/set/strings.go b/automation/vendor/github.com/juju/utils/set/strings.go
new file mode 100644
index 0000000..b89f932
--- /dev/null
+++ b/automation/vendor/github.com/juju/utils/set/strings.go
@@ -0,0 +1,112 @@
+// Copyright 2013 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+package set
+
+import (
+ "sort"
+)
+
+// Strings represents the classic "set" data structure, and contains strings.
+type Strings map[string]bool
+
+// NewStrings creates and initializes a Strings and populates it with
+// initial values as specified in the parameters.
+func NewStrings(initial ...string) Strings {
+ result := make(Strings)
+ for _, value := range initial {
+ result.Add(value)
+ }
+ return result
+}
+
+// Size returns the number of elements in the set.
+func (s Strings) Size() int {
+ return len(s)
+}
+
+// IsEmpty is true for empty or uninitialized sets.
+func (s Strings) IsEmpty() bool {
+ return len(s) == 0
+}
+
+// Add puts a value into the set.
+func (s Strings) Add(value string) {
+ if s == nil {
+ panic("uninitalised set")
+ }
+ s[value] = true
+}
+
+// Remove takes a value out of the set. If value wasn't in the set to start
+// with, this method silently succeeds.
+func (s Strings) Remove(value string) {
+ delete(s, value)
+}
+
+// Contains returns true if the value is in the set, and false otherwise.
+func (s Strings) Contains(value string) bool {
+ _, exists := s[value]
+ return exists
+}
+
+// Values returns an unordered slice containing all the values in the set.
+func (s Strings) Values() []string {
+ result := make([]string, len(s))
+ i := 0
+ for key := range s {
+ result[i] = key
+ i++
+ }
+ return result
+}
+
+// SortedValues returns an ordered slice containing all the values in the set.
+func (s Strings) SortedValues() []string {
+ values := s.Values()
+ sort.Strings(values)
+ return values
+}
+
+// Union returns a new Strings representing a union of the elments in the
+// method target and the parameter.
+func (s Strings) Union(other Strings) Strings {
+ result := make(Strings)
+ // Use the internal map rather than going through the friendlier functions
+ // to avoid extra allocation of slices.
+ for value := range s {
+ result[value] = true
+ }
+ for value := range other {
+ result[value] = true
+ }
+ return result
+}
+
+// Intersection returns a new Strings representing a intersection of the elments in the
+// method target and the parameter.
+func (s Strings) Intersection(other Strings) Strings {
+ result := make(Strings)
+ // Use the internal map rather than going through the friendlier functions
+ // to avoid extra allocation of slices.
+ for value := range s {
+ if other.Contains(value) {
+ result[value] = true
+ }
+ }
+ return result
+}
+
+// Difference returns a new Strings representing all the values in the
+// target that are not in the parameter.
+func (s Strings) Difference(other Strings) Strings {
+ result := make(Strings)
+ // Use the internal map rather than going through the friendlier functions
+ // to avoid extra allocation of slices.
+ for value := range s {
+ if !other.Contains(value) {
+ result[value] = true
+ }
+ }
+ return result
+}
diff --git a/automation/vendor/github.com/juju/utils/set/tags.go b/automation/vendor/github.com/juju/utils/set/tags.go
new file mode 100644
index 0000000..0fb6f87
--- /dev/null
+++ b/automation/vendor/github.com/juju/utils/set/tags.go
@@ -0,0 +1,150 @@
+// Copyright 2014 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+package set
+
+import (
+ "sort"
+
+ "github.com/juju/errors"
+ "gopkg.in/juju/names.v2"
+)
+
+// Tags represents the Set data structure, it implements tagSet
+// and contains names.Tags.
+type Tags map[names.Tag]bool
+
+// NewTags creates and initializes a Tags and populates it with
+// inital values as specified in the parameters.
+func NewTags(initial ...names.Tag) Tags {
+ result := make(Tags)
+ for _, value := range initial {
+ result.Add(value)
+ }
+ return result
+}
+
+// NewTagsFromStrings creates and initializes a Tags and populates it
+// by using names.ParseTag on the initial values specified in the parameters.
+func NewTagsFromStrings(initial ...string) (Tags, error) {
+ result := make(Tags)
+ for _, value := range initial {
+ tag, err := names.ParseTag(value)
+ if err != nil {
+ return result, errors.Trace(err)
+ }
+ result.Add(tag)
+ }
+ return result, nil
+}
+
+// Size returns the number of elements in the set.
+func (t Tags) Size() int {
+ return len(t)
+}
+
+// IsEmpty is true for empty or uninitialized sets.
+func (t Tags) IsEmpty() bool {
+ return len(t) == 0
+}
+
+// Add puts a value into the set.
+func (t Tags) Add(value names.Tag) {
+ if t == nil {
+ panic("uninitalised set")
+ }
+ t[value] = true
+}
+
+// Remove takes a value out of the set. If value wasn't in the set to start
+// with, this method silently succeeds.
+func (t Tags) Remove(value names.Tag) {
+ delete(t, value)
+}
+
+// Contains returns true if the value is in the set, and false otherwise.
+func (t Tags) Contains(value names.Tag) bool {
+ _, exists := t[value]
+ return exists
+}
+
+// Values returns an unordered slice containing all the values in the set.
+func (t Tags) Values() []names.Tag {
+ result := make([]names.Tag, len(t))
+ i := 0
+ for key := range t {
+ result[i] = key
+ i++
+ }
+ return result
+}
+
+// stringValues returns a list of strings that represent a names.Tag
+// Used internally by the SortedValues method.
+func (t Tags) stringValues() []string {
+ result := make([]string, t.Size())
+ i := 0
+ for key := range t {
+ result[i] = key.String()
+ i++
+ }
+ return result
+}
+
+// SortedValues returns an ordered slice containing all the values in the set.
+func (t Tags) SortedValues() []names.Tag {
+ values := t.stringValues()
+ sort.Strings(values)
+
+ result := make([]names.Tag, len(values))
+ for i, value := range values {
+ // We already know only good strings can live in the Tags set
+ // so we can safely ignore the error here.
+ tag, _ := names.ParseTag(value)
+ result[i] = tag
+ }
+ return result
+}
+
+// Union returns a new Tags representing a union of the elments in the
+// method target and the parameter.
+func (t Tags) Union(other Tags) Tags {
+ result := make(Tags)
+ // Use the internal map rather than going through the friendlier functions
+ // to avoid extra allocation of slices.
+ for value := range t {
+ result[value] = true
+ }
+ for value := range other {
+ result[value] = true
+ }
+ return result
+}
+
+// Intersection returns a new Tags representing a intersection of the elments in the
+// method target and the parameter.
+func (t Tags) Intersection(other Tags) Tags {
+ result := make(Tags)
+ // Use the internal map rather than going through the friendlier functions
+ // to avoid extra allocation of slices.
+ for value := range t {
+ if other.Contains(value) {
+ result[value] = true
+ }
+ }
+ return result
+}
+
+// Difference returns a new Tags representing all the values in the
+// target that are not in the parameter.
+func (t Tags) Difference(other Tags) Tags {
+ result := make(Tags)
+ // Use the internal map rather than going through the friendlier functions
+ // to avoid extra allocation of slices.
+ for value := range t {
+ if !other.Contains(value) {
+ result[value] = true
+ }
+ }
+ return result
+}
diff --git a/automation/vendor/github.com/juju/utils/size.go b/automation/vendor/github.com/juju/utils/size.go
new file mode 100644
index 0000000..8f6f88d
--- /dev/null
+++ b/automation/vendor/github.com/juju/utils/size.go
@@ -0,0 +1,78 @@
+// Copyright 2014 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+package utils
+
+import (
+ "math"
+ "strconv"
+ "strings"
+ "unicode"
+
+ "github.com/juju/errors"
+)
+
+// ParseSize parses the string as a size, in mebibytes.
+//
+// The string must be a is a non-negative number with
+// an optional multiplier suffix (M, G, T, P, E, Z, or Y).
+// If the suffix is not specified, "M" is implied.
+func ParseSize(str string) (MB uint64, err error) {
+ // Find the first non-digit/period:
+ i := strings.IndexFunc(str, func(r rune) bool {
+ return r != '.' && !unicode.IsDigit(r)
+ })
+ var multiplier float64 = 1
+ if i > 0 {
+ suffix := str[i:]
+ multiplier = 0
+ for j := 0; j < len(sizeSuffixes); j++ {
+ base := string(sizeSuffixes[j])
+ // M, MB, or MiB are all valid.
+ switch suffix {
+ case base, base + "B", base + "iB":
+ multiplier = float64(sizeSuffixMultiplier(j))
+ break
+ }
+ }
+ if multiplier == 0 {
+ return 0, errors.Errorf("invalid multiplier suffix %q, expected one of %s", suffix, []byte(sizeSuffixes))
+ }
+ str = str[:i]
+ }
+
+ val, err := strconv.ParseFloat(str, 64)
+ if err != nil || val < 0 {
+ return 0, errors.Errorf("expected a non-negative number, got %q", str)
+ }
+ val *= multiplier
+ return uint64(math.Ceil(val)), nil
+}
+
+var sizeSuffixes = "MGTPEZY"
+
+func sizeSuffixMultiplier(i int) int {
+ return 1 << uint(i*10)
+}
+
+// SizeTracker tracks the number of bytes passing through
+// its Write method (which is otherwise a no-op).
+//
+// Use SizeTracker with io.MultiWriter() to track number of bytes
+// written. Use with io.TeeReader() to track number of bytes read.
+type SizeTracker struct {
+ // size is the number of bytes written so far.
+ size int64
+}
+
+// Size returns the number of bytes written so far.
+func (st SizeTracker) Size() int64 {
+ return st.size
+}
+
+// Write implements io.Writer.
+func (st *SizeTracker) Write(data []byte) (n int, err error) {
+ n = len(data)
+ st.size += int64(n)
+ return n, nil
+}
diff --git a/automation/vendor/github.com/juju/utils/systemerrmessages_unix.go b/automation/vendor/github.com/juju/utils/systemerrmessages_unix.go
new file mode 100644
index 0000000..7a0edd4
--- /dev/null
+++ b/automation/vendor/github.com/juju/utils/systemerrmessages_unix.go
@@ -0,0 +1,16 @@
+// Copyright 2014 Canonical Ltd.
+// Copyright 2014 Cloudbase Solutions SRL
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+// +build !windows
+
+package utils
+
+// The following are strings/regex-es which match common Unix error messages
+// that may be returned in case of failed calls to the system.
+// Any extra leading/trailing regex-es are left to be added by the developer.
+const (
+ NoSuchUserErrRegexp = `user: unknown user [a-z0-9_-]*`
+ NoSuchFileErrRegexp = `no such file or directory`
+ MkdirFailErrRegexp = `.* not a directory`
+)
diff --git a/automation/vendor/github.com/juju/utils/systemerrmessages_windows.go b/automation/vendor/github.com/juju/utils/systemerrmessages_windows.go
new file mode 100644
index 0000000..b24d453
--- /dev/null
+++ b/automation/vendor/github.com/juju/utils/systemerrmessages_windows.go
@@ -0,0 +1,14 @@
+// Copyright 2014 Canonical Ltd.
+// Copyright 2014 Cloudbase Solutions SRL
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+package utils
+
+// The following are strings/regex-es which match common Windows error messages
+// that may be returned in case of failed calls to the system.
+// Any extra leading/trailing regex-es are left to be added by the developer.
+const (
+ NoSuchUserErrRegexp = `No mapping between account names and security IDs was done\.`
+ NoSuchFileErrRegexp = `The system cannot find the (file|path) specified\.`
+ MkdirFailErrRegexp = `mkdir .*` + NoSuchFileErrRegexp
+)
diff --git a/automation/vendor/github.com/juju/utils/timeit.go b/automation/vendor/github.com/juju/utils/timeit.go
new file mode 100644
index 0000000..172b593
--- /dev/null
+++ b/automation/vendor/github.com/juju/utils/timeit.go
@@ -0,0 +1,57 @@
+// Copyright 2013 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+package utils
+
+import (
+ "fmt"
+ "os"
+ "time"
+)
+
+type timer struct {
+ action string
+ start time.Time
+ depth int
+ duration time.Duration
+ subActions []*timer
+}
+
+func (t *timer) String() string {
+ this := fmt.Sprintf("%.3fs %*s%s\n", t.duration.Seconds(), t.depth, "", t.action)
+ for _, sub := range t.subActions {
+ this += sub.String()
+ }
+ return this
+}
+
+var stack []*timer
+
+// Start a timer, used for tracking time spent.
+// Generally used with either defer, as in:
+// defer utils.Timeit("my func")()
+// Which will track how much time is spent in your function. Or
+// if you want to track the time spent in a function you are calling
+// then you would use:
+// toc := utils.Timeit("anotherFunc()")
+// anotherFunc()
+// toc()
+// This tracks nested calls by indenting the output, and will print out the
+// full stack of timing when we reach the top of the stack.
+func Timeit(action string) func() {
+ cur := &timer{action: action, start: time.Now(), depth: len(stack)}
+ if len(stack) != 0 {
+ tip := stack[len(stack)-1]
+ tip.subActions = append(tip.subActions, cur)
+ }
+ stack = append(stack, cur)
+ return func() {
+ cur.duration = time.Since(cur.start)
+ if len(stack) == 0 || cur == stack[0] {
+ fmt.Fprint(os.Stderr, cur)
+ stack = nil
+ } else {
+ stack = stack[0 : len(stack)-1]
+ }
+ }
+}
diff --git a/automation/vendor/github.com/juju/utils/timer.go b/automation/vendor/github.com/juju/utils/timer.go
new file mode 100644
index 0000000..6b32f09
--- /dev/null
+++ b/automation/vendor/github.com/juju/utils/timer.go
@@ -0,0 +1,124 @@
+// Copyright 2015 Canonical Ltd.
+// Copyright 2015 Cloudbase Solutions SRL
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+package utils
+
+import (
+ "math/rand"
+ "time"
+
+ "github.com/juju/utils/clock"
+)
+
+// Countdown implements a timer that will call a provided function.
+// after a internally stored duration. The steps as well as min and max
+// durations are declared upon initialization and depend on
+// the particular implementation.
+//
+// TODO(katco): 2016-08-09: This type is deprecated: lp:1611427
+type Countdown interface {
+ // Reset stops the timer and resets its duration to the minimum one.
+ // Start must be called to start the timer again.
+ Reset()
+
+ // Start starts the internal timer.
+ // At the end of the timer, if Reset hasn't been called in the mean time
+ // Func will be called and the duration is increased for the next call.
+ Start()
+}
+
+// NewBackoffTimer creates and initializes a new BackoffTimer
+// A backoff timer starts at min and gets multiplied by factor
+// until it reaches max. Jitter determines whether a small
+// randomization is added to the duration.
+//
+// TODO(katco): 2016-08-09: This type is deprecated: lp:1611427
+func NewBackoffTimer(config BackoffTimerConfig) *BackoffTimer {
+ return &BackoffTimer{
+ config: config,
+ currentDuration: config.Min,
+ }
+}
+
+// BackoffTimer implements Countdown.
+// A backoff timer starts at min and gets multiplied by factor
+// until it reaches max. Jitter determines whether a small
+// randomization is added to the duration.
+//
+// TODO(katco): 2016-08-09: This type is deprecated: lp:1611427
+type BackoffTimer struct {
+ config BackoffTimerConfig
+
+ timer clock.Timer
+ currentDuration time.Duration
+}
+
+// BackoffTimerConfig is a helper struct for backoff timer
+// that encapsulates config information.
+//
+// TODO(katco): 2016-08-09: This type is deprecated: lp:1611427
+type BackoffTimerConfig struct {
+ // The minimum duration after which Func is called.
+ Min time.Duration
+
+ // The maximum duration after which Func is called.
+ Max time.Duration
+
+ // Determines whether a small randomization is applied to
+ // the duration.
+ Jitter bool
+
+ // The factor by which you want the duration to increase
+ // every time.
+ Factor int64
+
+ // Func is the function that will be called when the countdown reaches 0.
+ Func func()
+
+ // Clock provides the AfterFunc function used to call func.
+ // It is exposed here so it's easier to mock it in tests.
+ Clock clock.Clock
+}
+
+// Start implements the Timer interface.
+// Any existing timer execution is stopped before
+// a new one is created.
+func (t *BackoffTimer) Start() {
+ if t.timer != nil {
+ t.timer.Stop()
+ }
+ t.timer = t.config.Clock.AfterFunc(t.currentDuration, t.config.Func)
+
+ // Since it's a backoff timer we will increase
+ // the duration after each signal.
+ t.increaseDuration()
+}
+
+// Reset implements the Timer interface.
+func (t *BackoffTimer) Reset() {
+ if t.timer != nil {
+ t.timer.Stop()
+ }
+ if t.currentDuration > t.config.Min {
+ t.currentDuration = t.config.Min
+ }
+}
+
+// increaseDuration will increase the duration based on
+// the current value and the factor. If jitter is true
+// it will add a 0.3% jitter to the final value.
+func (t *BackoffTimer) increaseDuration() {
+ current := int64(t.currentDuration)
+ nextDuration := time.Duration(current * t.config.Factor)
+ if t.config.Jitter {
+ // Get a factor in [-1; 1].
+ randFactor := (rand.Float64() * 2) - 1
+ jitter := float64(nextDuration) * randFactor * 0.03
+ nextDuration = nextDuration + time.Duration(jitter)
+ }
+ if nextDuration > t.config.Max {
+ nextDuration = t.config.Max
+ }
+ t.currentDuration = nextDuration
+}
diff --git a/automation/vendor/github.com/juju/utils/tls.go b/automation/vendor/github.com/juju/utils/tls.go
new file mode 100644
index 0000000..b805af8
--- /dev/null
+++ b/automation/vendor/github.com/juju/utils/tls.go
@@ -0,0 +1,72 @@
+// Copyright 2016 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+package utils
+
+import (
+ "crypto/tls"
+ "net/http"
+ "time"
+)
+
+// NewHttpTLSTransport returns a new http.Transport constructed with the TLS config
+// and the necessary parameters for Juju.
+func NewHttpTLSTransport(tlsConfig *tls.Config) *http.Transport {
+ // 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.
+ transport := &http.Transport{
+ Proxy: http.ProxyFromEnvironment,
+ TLSClientConfig: tlsConfig,
+ DisableKeepAlives: true,
+ TLSHandshakeTimeout: 10 * time.Second,
+ }
+ installHTTPDialShim(transport)
+ registerFileProtocol(transport)
+ return transport
+}
+
+// knownGoodCipherSuites contains the list of secure cipher suites to use
+// with tls.Config. This list matches those that Go 1.6 implements from
+// https://wiki.mozilla.org/Security/Server_Side_TLS#Recommended_configurations.
+//
+// https://tools.ietf.org/html/rfc7525#section-4.2 excludes RSA exchange completely
+// so we could be more strict if all our clients will support
+// TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256/384. Unfortunately Go's crypto library
+// is limited and doesn't support DHE-RSA-AES256-GCM-SHA384 and
+// DHE-RSA-AES256-SHA256, which are part of the recommended set.
+//
+// Unfortunately we can't drop the RSA algorithms because our servers aren't
+// generating ECDHE keys.
+var knownGoodCipherSuites = []uint16{
+ // These are technically useless for Juju, since we use an RSA certificate,
+ // but they also don't hurt anything, and supporting an ECDSA certificate
+ // could be useful in the future.
+ tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
+ tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
+
+ tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
+ tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
+
+ // Windows doesn't support GCM currently, so we need these for RSA support.
+ tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
+ tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
+
+ // We need this so that we have at least one suite in common
+ // with the default gnutls installed for precise and trusty.
+ tls.TLS_RSA_WITH_AES_256_CBC_SHA,
+}
+
+// SecureTLSConfig returns a tls.Config that conforms to Juju's security
+// standards, so as to avoid known security vulnerabilities in certain
+// configurations.
+//
+// Currently it excludes RC4 implementations from the available ciphersuites,
+// requires ciphersuites that provide forward secrecy, and sets the minimum TLS
+// version to 1.2.
+func SecureTLSConfig() *tls.Config {
+ return &tls.Config{
+ CipherSuites: knownGoodCipherSuites,
+ MinVersion: tls.VersionTLS12,
+ }
+}
diff --git a/automation/vendor/github.com/juju/utils/trivial.go b/automation/vendor/github.com/juju/utils/trivial.go
new file mode 100644
index 0000000..642e213
--- /dev/null
+++ b/automation/vendor/github.com/juju/utils/trivial.go
@@ -0,0 +1,147 @@
+// Copyright 2012, 2013 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+package utils
+
+import (
+ "bytes"
+ "compress/gzip"
+ "crypto/sha256"
+ "encoding/hex"
+ "io"
+ "io/ioutil"
+ "os"
+ "strings"
+ "unicode"
+)
+
+// TODO(ericsnow) Move the quoting helpers into the shell package?
+
+// ShQuote quotes s so that when read by bash, no metacharacters
+// within s will be interpreted as such.
+func ShQuote(s string) string {
+ // single-quote becomes single-quote, double-quote, single-quote, double-quote, single-quote
+ return `'` + strings.Replace(s, `'`, `'"'"'`, -1) + `'`
+}
+
+// WinPSQuote quotes s so that when read by powershell, no metacharacters
+// within s will be interpreted as such.
+func WinPSQuote(s string) string {
+ // See http://ss64.com/ps/syntax-esc.html#quotes.
+ // Double quotes inside single quotes are fine, double single quotes inside
+ // single quotes, not so much so. Having double quoted strings inside single
+ // quoted strings, ensure no expansion happens.
+ return `'` + strings.Replace(s, `'`, `"`, -1) + `'`
+}
+
+// WinCmdQuote quotes s so that when read by cmd.exe, no metacharacters
+// within s will be interpreted as such.
+func WinCmdQuote(s string) string {
+ // See http://blogs.msdn.com/b/twistylittlepassagesallalike/archive/2011/04/23/everyone-quotes-arguments-the-wrong-way.aspx.
+ quoted := winCmdQuote(s)
+ return winCmdEscapeMeta(quoted)
+}
+
+func winCmdQuote(s string) string {
+ var escaped string
+ for _, c := range s {
+ switch c {
+ case '\\', '"':
+ escaped += `\`
+ }
+ escaped += string(c)
+ }
+ return `"` + escaped + `"`
+}
+
+func winCmdEscapeMeta(str string) string {
+ const meta = `()%!^"<>&|`
+ var newStr string
+ for _, c := range str {
+ if strings.Contains(meta, string(c)) {
+ newStr += "^"
+ }
+ newStr += string(c)
+ }
+ return newStr
+}
+
+// CommandString flattens a sequence of command arguments into a
+// string suitable for executing in a shell, escaping slashes,
+// variables and quotes as necessary; each argument is double-quoted
+// if and only if necessary.
+func CommandString(args ...string) string {
+ var buf bytes.Buffer
+ for i, arg := range args {
+ needsQuotes := false
+ var argBuf bytes.Buffer
+ for _, r := range arg {
+ if unicode.IsSpace(r) {
+ needsQuotes = true
+ } else if r == '"' || r == '$' || r == '\\' {
+ needsQuotes = true
+ argBuf.WriteByte('\\')
+ }
+ argBuf.WriteRune(r)
+ }
+ if i > 0 {
+ buf.WriteByte(' ')
+ }
+ if needsQuotes {
+ buf.WriteByte('"')
+ argBuf.WriteTo(&buf)
+ buf.WriteByte('"')
+ } else {
+ argBuf.WriteTo(&buf)
+ }
+ }
+ return buf.String()
+}
+
+// Gzip compresses the given data.
+func Gzip(data []byte) []byte {
+ var buf bytes.Buffer
+ w := gzip.NewWriter(&buf)
+ if _, err := w.Write(data); err != nil {
+ // Compression should never fail unless it fails
+ // to write to the underlying writer, which is a bytes.Buffer
+ // that never fails.
+ panic(err)
+ }
+ if err := w.Close(); err != nil {
+ panic(err)
+ }
+ return buf.Bytes()
+}
+
+// Gunzip uncompresses the given data.
+func Gunzip(data []byte) ([]byte, error) {
+ r, err := gzip.NewReader(bytes.NewReader(data))
+ if err != nil {
+ return nil, err
+ }
+ return ioutil.ReadAll(r)
+}
+
+// ReadSHA256 returns the SHA256 hash of the contents read from source
+// (hex encoded) and the size of the source in bytes.
+func ReadSHA256(source io.Reader) (string, int64, error) {
+ hash := sha256.New()
+ size, err := io.Copy(hash, source)
+ if err != nil {
+ return "", 0, err
+ }
+ digest := hex.EncodeToString(hash.Sum(nil))
+ return digest, size, nil
+}
+
+// ReadFileSHA256 is like ReadSHA256 but reads the contents of the
+// given file.
+func ReadFileSHA256(filename string) (string, int64, error) {
+ f, err := os.Open(filename)
+ if err != nil {
+ return "", 0, err
+ }
+ defer f.Close()
+ return ReadSHA256(f)
+}
diff --git a/automation/vendor/github.com/juju/utils/username.go b/automation/vendor/github.com/juju/utils/username.go
new file mode 100644
index 0000000..5107e3b
--- /dev/null
+++ b/automation/vendor/github.com/juju/utils/username.go
@@ -0,0 +1,77 @@
+// Copyright 2015 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+package utils
+
+import (
+ "os"
+ "os/user"
+
+ "github.com/juju/errors"
+)
+
+// ResolveSudo returns the original username if sudo was used. The
+// original username is extracted from the OS environment.
+func ResolveSudo(username string) string {
+ return resolveSudo(username, os.Getenv)
+}
+
+func resolveSudo(username string, getenvFunc func(string) string) string {
+ if username != "root" {
+ return username
+ }
+ // sudo was probably called, get the original user.
+ if username := getenvFunc("SUDO_USER"); username != "" {
+ return username
+ }
+ return username
+}
+
+// EnvUsername returns the username from the OS environment.
+func EnvUsername() (string, error) {
+ return os.Getenv("USER"), nil
+}
+
+// OSUsername returns the username of the current OS user (based on UID).
+func OSUsername() (string, error) {
+ u, err := user.Current()
+ if err != nil {
+ return "", errors.Trace(err)
+ }
+ return u.Username, nil
+}
+
+// ResolveUsername returns the username determined by the provided
+// functions. The functions are tried in the same order in which they
+// were passed in. An error returned from any of them is immediately
+// returned. If an empty string is returned then that signals that the
+// function did not find the username and the next function is tried.
+// Once a username is found, the provided resolveSudo func (if any) is
+// called with that username and the result is returned. If no username
+// is found then errors.NotFound is returned.
+func ResolveUsername(resolveSudo func(string) string, usernameFuncs ...func() (string, error)) (string, error) {
+ for _, usernameFunc := range usernameFuncs {
+ username, err := usernameFunc()
+ if err != nil {
+ return "", errors.Trace(err)
+ }
+ if username != "" {
+ if resolveSudo != nil {
+ if original := resolveSudo(username); original != "" {
+ username = original
+ }
+ }
+ return username, nil
+ }
+ }
+ return "", errors.NotFoundf("username")
+}
+
+// LocalUsername determines the current username on the local host.
+func LocalUsername() (string, error) {
+ username, err := ResolveUsername(ResolveSudo, EnvUsername, OSUsername)
+ if err != nil {
+ return "", errors.Annotatef(err, "cannot get current user from the environment: %v", os.Environ())
+ }
+ return username, nil
+}
diff --git a/automation/vendor/github.com/juju/utils/uuid.go b/automation/vendor/github.com/juju/utils/uuid.go
new file mode 100644
index 0000000..2404efc
--- /dev/null
+++ b/automation/vendor/github.com/juju/utils/uuid.go
@@ -0,0 +1,90 @@
+// Copyright 2013 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+package utils
+
+import (
+ "crypto/rand"
+ "encoding/hex"
+ "fmt"
+ "io"
+ "regexp"
+ "strings"
+)
+
+// UUID represent a universal identifier with 16 octets.
+type UUID [16]byte
+
+// regex for validating that the UUID matches RFC 4122.
+// This package generates version 4 UUIDs but
+// accepts any UUID version.
+// http://www.ietf.org/rfc/rfc4122.txt
+var (
+ block1 = "[0-9a-f]{8}"
+ block2 = "[0-9a-f]{4}"
+ block3 = "[0-9a-f]{4}"
+ block4 = "[0-9a-f]{4}"
+ block5 = "[0-9a-f]{12}"
+
+ UUIDSnippet = block1 + "-" + block2 + "-" + block3 + "-" + block4 + "-" + block5
+ validUUID = regexp.MustCompile("^" + UUIDSnippet + "$")
+)
+
+func UUIDFromString(s string) (UUID, error) {
+ if !IsValidUUIDString(s) {
+ return UUID{}, fmt.Errorf("invalid UUID: %q", s)
+ }
+ s = strings.Replace(s, "-", "", 4)
+ raw, err := hex.DecodeString(s)
+ if err != nil {
+ return UUID{}, err
+ }
+ var uuid UUID
+ copy(uuid[:], raw)
+ return uuid, nil
+}
+
+// IsValidUUIDString returns true, if the given string matches a valid UUID (version 4, variant 2).
+func IsValidUUIDString(s string) bool {
+ return validUUID.MatchString(s)
+}
+
+// MustNewUUID returns a new uuid, if an error occurs it panics.
+func MustNewUUID() UUID {
+ uuid, err := NewUUID()
+ if err != nil {
+ panic(err)
+ }
+ return uuid
+}
+
+// NewUUID generates a new version 4 UUID relying only on random numbers.
+func NewUUID() (UUID, error) {
+ uuid := UUID{}
+ if _, err := io.ReadFull(rand.Reader, []byte(uuid[0:16])); err != nil {
+ return UUID{}, err
+ }
+ // Set version (4) and variant (2) according to RfC 4122.
+ var version byte = 4 << 4
+ var variant byte = 8 << 4
+ uuid[6] = version | (uuid[6] & 15)
+ uuid[8] = variant | (uuid[8] & 15)
+ return uuid, nil
+}
+
+// Copy returns a copy of the UUID.
+func (uuid UUID) Copy() UUID {
+ uuidCopy := uuid
+ return uuidCopy
+}
+
+// Raw returns a copy of the UUID bytes.
+func (uuid UUID) Raw() [16]byte {
+ return [16]byte(uuid)
+}
+
+// String returns a hexadecimal string representation with
+// standardized separators.
+func (uuid UUID) String() string {
+ return fmt.Sprintf("%x-%x-%x-%x-%x", uuid[0:4], uuid[4:6], uuid[6:8], uuid[8:10], uuid[10:16])
+}
diff --git a/automation/vendor/github.com/juju/utils/yaml.go b/automation/vendor/github.com/juju/utils/yaml.go
new file mode 100644
index 0000000..2d443a0
--- /dev/null
+++ b/automation/vendor/github.com/juju/utils/yaml.go
@@ -0,0 +1,107 @@
+// Copyright 2012, 2013 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+package utils
+
+import (
+ "io/ioutil"
+ "os"
+ "path/filepath"
+
+ "github.com/juju/errors"
+
+ "gopkg.in/yaml.v2"
+)
+
+// WriteYaml marshals obj as yaml to a temporary file in the same directory
+// as path, than atomically replaces path with the temporary file.
+func WriteYaml(path string, obj interface{}) error {
+ data, err := yaml.Marshal(obj)
+ if err != nil {
+ return errors.Trace(err)
+ }
+ dir := filepath.Dir(path)
+ f, err := ioutil.TempFile(dir, "juju")
+ if err != nil {
+ return errors.Trace(err)
+ }
+ tmp := f.Name()
+ if _, err := f.Write(data); err != nil {
+ f.Close() // don't leak file handle
+ os.Remove(tmp) // don't leak half written files on disk
+ return errors.Trace(err)
+ }
+ // Explicitly close the file before moving it. This is needed on Windows
+ // where the OS will not allow us to move a file that still has an open
+ // file handle. Must check the error on close because filesystems can delay
+ // reporting errors until the file is closed.
+ if err := f.Close(); err != nil {
+ os.Remove(tmp) // don't leak half written files on disk
+ return errors.Trace(err)
+ }
+
+ // ioutils.TempFile creates files 0600, but this function has a contract
+ // that files will be world readable, 0644 after replacement.
+ if err := os.Chmod(tmp, 0644); err != nil {
+ os.Remove(tmp) // remove file with incorrect permissions.
+ return errors.Trace(err)
+ }
+
+ return ReplaceFile(tmp, path)
+}
+
+// ReadYaml unmarshals the yaml contained in the file at path into obj. See
+// goyaml.Unmarshal. If path is not found, the error returned will be compatible
+// with os.IsNotExist.
+func ReadYaml(path string, obj interface{}) error {
+ data, err := ioutil.ReadFile(path)
+ if err != nil {
+ return err // cannot wrap here because callers check for NotFound.
+ }
+ return yaml.Unmarshal(data, obj)
+}
+
+// ConformYAML ensures all keys of any nested maps are strings. This is
+// necessary because YAML unmarshals map[interface{}]interface{} in nested
+// maps, which cannot be serialized by json or bson. Also, handle
+// []interface{}. cf. gopkg.in/juju/charm.v4/actions.go cleanse
+func ConformYAML(input interface{}) (interface{}, error) {
+ switch typedInput := input.(type) {
+
+ case map[string]interface{}:
+ newMap := make(map[string]interface{})
+ for key, value := range typedInput {
+ newValue, err := ConformYAML(value)
+ if err != nil {
+ return nil, err
+ }
+ newMap[key] = newValue
+ }
+ return newMap, nil
+
+ case map[interface{}]interface{}:
+ newMap := make(map[string]interface{})
+ for key, value := range typedInput {
+ typedKey, ok := key.(string)
+ if !ok {
+ return nil, errors.New("map keyed with non-string value")
+ }
+ newMap[typedKey] = value
+ }
+ return ConformYAML(newMap)
+
+ case []interface{}:
+ newSlice := make([]interface{}, len(typedInput))
+ for i, sliceValue := range typedInput {
+ newSliceValue, err := ConformYAML(sliceValue)
+ if err != nil {
+ return nil, errors.New("map keyed with non-string value")
+ }
+ newSlice[i] = newSliceValue
+ }
+ return newSlice, nil
+
+ default:
+ return input, nil
+ }
+}
diff --git a/automation/vendor/github.com/juju/utils/zfile_windows.go b/automation/vendor/github.com/juju/utils/zfile_windows.go
new file mode 100644
index 0000000..b1a50f1
--- /dev/null
+++ b/automation/vendor/github.com/juju/utils/zfile_windows.go
@@ -0,0 +1,28 @@
+// Copyright 2013 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+// mksyscall_windows.pl -l32 file_windows.go
+// MACHINE GENERATED BY THE COMMAND ABOVE; DO NOT EDIT
+
+package utils
+
+import "unsafe"
+import "syscall"
+
+var (
+ modkernel32 = syscall.NewLazyDLL("kernel32.dll")
+
+ procMoveFileExW = modkernel32.NewProc("MoveFileExW")
+)
+
+func moveFileEx(lpExistingFileName *uint16, lpNewFileName *uint16, dwFlags uint32) (err error) {
+ r1, _, e1 := syscall.Syscall(procMoveFileExW.Addr(), 3, uintptr(unsafe.Pointer(lpExistingFileName)), uintptr(unsafe.Pointer(lpNewFileName)), uintptr(dwFlags))
+ if r1 == 0 {
+ if e1 != 0 {
+ err = error(e1)
+ } else {
+ err = syscall.EINVAL
+ }
+ }
+ return
+}
diff --git a/automation/vendor/github.com/juju/version/LICENSE b/automation/vendor/github.com/juju/version/LICENSE
new file mode 100644
index 0000000..5cec73f
--- /dev/null
+++ b/automation/vendor/github.com/juju/version/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) 2015 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/version/README.md b/automation/vendor/github.com/juju/version/README.md
new file mode 100644
index 0000000..2d1b48b
--- /dev/null
+++ b/automation/vendor/github.com/juju/version/README.md
@@ -0,0 +1,3 @@
+# Version [![GoDoc](https://godoc.org/github.com/juju/version?status.svg)](https://godoc.org/github.com/juju/version)
+version is a go package for intelligent version comparisons.
+
diff --git a/automation/vendor/github.com/juju/version/version.go b/automation/vendor/github.com/juju/version/version.go
new file mode 100644
index 0000000..026e99f
--- /dev/null
+++ b/automation/vendor/github.com/juju/version/version.go
@@ -0,0 +1,297 @@
+// Copyright 2012, 2013 Canonical Ltd.
+// Licensed under the AGPLv3, see LICENCE file for details.
+
+// Package version implements version parsing.
+package version
+
+import (
+ "encoding/json"
+ "fmt"
+ "regexp"
+ "strconv"
+ "strings"
+
+ "gopkg.in/mgo.v2/bson"
+)
+
+// Number represents a version number.
+type Number struct {
+ Major int
+ Minor int
+ Tag string
+ Patch int
+ Build int
+}
+
+// Zero is occasionally convenient and readable.
+// Please don't change its value.
+var Zero = Number{}
+
+// Binary specifies a binary version of juju.v
+type Binary struct {
+ Number
+ Series string
+ Arch string
+}
+
+// String returns the string representation of the binary version.
+func (b Binary) String() string {
+ return fmt.Sprintf("%v-%s-%s", b.Number, b.Series, b.Arch)
+}
+
+// GetBSON implements bson.Getter.
+func (b Binary) GetBSON() (interface{}, error) {
+ return b.String(), nil
+}
+
+// SetBSON implements bson.Setter.
+func (b *Binary) SetBSON(raw bson.Raw) error {
+ var s string
+ err := raw.Unmarshal(&s)
+ if err != nil {
+ return err
+ }
+ v, err := ParseBinary(s)
+ if err != nil {
+ return err
+ }
+ *b = v
+ return nil
+}
+
+// MarshalJSON implements json.Marshaler.
+func (b Binary) MarshalJSON() ([]byte, error) {
+ return json.Marshal(b.String())
+}
+
+// UnmarshalJSON implements json.Unmarshaler.
+func (b *Binary) UnmarshalJSON(data []byte) error {
+ var s string
+ if err := json.Unmarshal(data, &s); err != nil {
+ return err
+ }
+ v, err := ParseBinary(s)
+ if err != nil {
+ return err
+ }
+ *b = v
+ return nil
+}
+
+// MarshalYAML implements yaml.v2.Marshaller interface.
+func (b Binary) MarshalYAML() (interface{}, error) {
+ return b.String(), nil
+}
+
+// UnmarshalYAML implements the yaml.Unmarshaller interface.
+func (b *Binary) UnmarshalYAML(unmarshal func(interface{}) error) error {
+ var vstr string
+ err := unmarshal(&vstr)
+ if err != nil {
+ return err
+ }
+ v, err := ParseBinary(vstr)
+ if err != nil {
+ return err
+ }
+ *b = v
+ return nil
+}
+
+var (
+ binaryPat = regexp.MustCompile(`^(\d{1,9})\.(\d{1,9})(?:\.|-([a-z]+))(\d{1,9})(\.\d{1,9})?-([^-]+)-([^-]+)$`)
+ numberPat = regexp.MustCompile(`^(\d{1,9})\.(\d{1,9})(?:\.|-([a-z]+))(\d{1,9})(\.\d{1,9})?$`)
+)
+
+// MustParse parses a version and panics if it does
+// not parse correctly.
+func MustParse(s string) Number {
+ v, err := Parse(s)
+ if err != nil {
+ panic(err)
+ }
+ return v
+}
+
+// MustParseBinary parses a binary version and panics if it does
+// not parse correctly.
+func MustParseBinary(s string) Binary {
+ b, err := ParseBinary(s)
+ if err != nil {
+ panic(err)
+ }
+ return b
+}
+
+// ParseBinary parses a binary version of the form "1.2.3-series-arch".
+func ParseBinary(s string) (Binary, error) {
+ m := binaryPat.FindStringSubmatch(s)
+ if m == nil {
+ return Binary{}, fmt.Errorf("invalid binary version %q", s)
+ }
+ var b Binary
+ b.Major = atoi(m[1])
+ b.Minor = atoi(m[2])
+ b.Tag = m[3]
+ b.Patch = atoi(m[4])
+ if m[5] != "" {
+ b.Build = atoi(m[5][1:])
+ }
+ b.Series = m[6]
+ b.Arch = m[7]
+ return b, nil
+}
+
+// Parse parses the version, which is of the form 1.2.3
+// giving the major, minor and release versions
+// respectively.
+func Parse(s string) (Number, error) {
+ m := numberPat.FindStringSubmatch(s)
+ if m == nil {
+ return Number{}, fmt.Errorf("invalid version %q", s)
+ }
+ var n Number
+ n.Major = atoi(m[1])
+ n.Minor = atoi(m[2])
+ n.Tag = m[3]
+ n.Patch = atoi(m[4])
+ if m[5] != "" {
+ n.Build = atoi(m[5][1:])
+ }
+ return n, nil
+}
+
+// atoi is the same as strconv.Atoi but assumes that
+// the string has been verified to be a valid integer.
+func atoi(s string) int {
+ n, err := strconv.Atoi(s)
+ if err != nil {
+ panic(err)
+ }
+ return n
+}
+
+// String returns the string representation of this Number.
+func (n Number) String() string {
+ var s string
+ if n.Tag == "" {
+ s = fmt.Sprintf("%d.%d.%d", n.Major, n.Minor, n.Patch)
+ } else {
+ s = fmt.Sprintf("%d.%d-%s%d", n.Major, n.Minor, n.Tag, n.Patch)
+ }
+ if n.Build > 0 {
+ s += fmt.Sprintf(".%d", n.Build)
+ }
+ return s
+}
+
+// Compare returns -1, 0 or 1 depending on whether
+// n is less than, equal to or greater than other.
+// The comparison compares Major, then Minor, then Patch, then Build, using the first difference as
+func (n Number) Compare(other Number) int {
+ if n == other {
+ return 0
+ }
+ less := false
+ switch {
+ case n.Major != other.Major:
+ less = n.Major < other.Major
+ case n.Minor != other.Minor:
+ less = n.Minor < other.Minor
+ case n.Tag != other.Tag:
+ switch {
+ case n.Tag == "":
+ less = false
+ case other.Tag == "":
+ less = true
+ default:
+ less = n.Tag < other.Tag
+ }
+ case n.Patch != other.Patch:
+ less = n.Patch < other.Patch
+ case n.Build != other.Build:
+ less = n.Build < other.Build
+ }
+ if less {
+ return -1
+ }
+ return 1
+}
+
+// GetBSON implements bson.Getter.
+func (n Number) GetBSON() (interface{}, error) {
+ return n.String(), nil
+}
+
+// SetBSON implements bson.Setter.
+func (n *Number) SetBSON(raw bson.Raw) error {
+ var s string
+ err := raw.Unmarshal(&s)
+ if err != nil {
+ return err
+ }
+ v, err := Parse(s)
+ if err != nil {
+ return err
+ }
+ *n = v
+ return nil
+}
+
+// MarshalJSON implements json.Marshaler.
+func (n Number) MarshalJSON() ([]byte, error) {
+ return json.Marshal(n.String())
+}
+
+// UnmarshalJSON implements json.Unmarshaler.
+func (n *Number) UnmarshalJSON(data []byte) error {
+ var s string
+ if err := json.Unmarshal(data, &s); err != nil {
+ return err
+ }
+ v, err := Parse(s)
+ if err != nil {
+ return err
+ }
+ *n = v
+ return nil
+}
+
+// MarshalYAML implements yaml.v2.Marshaller interface
+func (n Number) MarshalYAML() (interface{}, error) {
+ return n.String(), nil
+}
+
+// UnmarshalYAML implements the yaml.Unmarshaller interface
+func (n *Number) UnmarshalYAML(unmarshal func(interface{}) error) error {
+ var vstr string
+ err := unmarshal(&vstr)
+ if err != nil {
+ return err
+ }
+ v, err := Parse(vstr)
+ if err != nil {
+ return err
+ }
+ *n = v
+ return nil
+}
+
+// ParseMajorMinor takes an argument of the form "major.minor" and returns ints major and minor.
+func ParseMajorMinor(vers string) (int, int, error) {
+ parts := strings.Split(vers, ".")
+ major, err := strconv.Atoi(parts[0])
+ minor := -1
+ if err != nil {
+ return -1, -1, fmt.Errorf("invalid major version number %s: %v", parts[0], err)
+ }
+ if len(parts) == 2 {
+ minor, err = strconv.Atoi(parts[1])
+ if err != nil {
+ return -1, -1, fmt.Errorf("invalid minor version number %s: %v", parts[1], err)
+ }
+ } else if len(parts) > 2 {
+ return -1, -1, fmt.Errorf("invalid major.minor version number %s", vers)
+ }
+ return major, minor, nil
+}