cord-776 create build / runtime containers for autmation uservices

Change-Id: I246973192adef56a250ffe93a5f65fff488840c1
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
+}