1
// Copyright 2016 Canonical Ltd.
2
// Licensed under the LGPLv3, see LICENCE file for details.
17
"github.com/juju/errors"
18
"github.com/juju/loggo"
19
"github.com/juju/schema"
20
"github.com/juju/utils/set"
21
"github.com/juju/version"
25
logger = loggo.GetLogger("maas")
27
// The supported versions should be ordered from most desirable version to
28
// least as they will be tried in order.
29
supportedAPIVersions = []string{"2.0"}
31
// Each of the api versions that change the request or response structure
32
// for any given call should have a value defined for easy definition of
33
// the deserialization functions.
34
twoDotOh = version.Number{Major: 2, Minor: 0}
36
// Current request number. Informational only for logging.
40
// ControllerArgs is an argument struct for passing the required parameters
41
// to the NewController method.
42
type ControllerArgs struct {
47
// NewController creates an authenticated client to the MAAS API, and checks
48
// the capabilities of the server.
50
// If the APIKey is not valid, a NotValid error is returned.
51
// If the credentials are incorrect, a PermissionError is returned.
52
func NewController(args ControllerArgs) (Controller, error) {
53
// For now we don't need to test multiple versions. It is expected that at
54
// some time in the future, we will try the most up to date version and then
55
// work our way backwards.
56
for _, apiVersion := range supportedAPIVersions {
57
major, minor, err := version.ParseMajorMinor(apiVersion)
58
// We should not get an error here. See the test.
60
return nil, errors.Errorf("bad version defined in supported versions: %q", apiVersion)
62
client, err := NewAuthenticatedClient(args.BaseURL, args.APIKey, apiVersion)
64
// If the credentials aren't valid, return now.
65
if errors.IsNotValid(err) {
66
return nil, errors.Trace(err)
68
// Any other error attempting to create the authenticated client
69
// is an unexpected error and return now.
70
return nil, NewUnexpectedError(err)
72
controllerVersion := version.Number{
76
controller := &controller{client: client}
77
// The controllerVersion returned from the function will include any patch version.
78
controller.capabilities, controller.apiVersion, err = controller.readAPIVersion(controllerVersion)
80
logger.Debugf("read version failed: %#v", err)
84
if err := controller.checkCreds(); err != nil {
85
return nil, errors.Trace(err)
87
return controller, nil
90
return nil, NewUnsupportedVersionError("controller at %s does not support any of %s", args.BaseURL, supportedAPIVersions)
93
type controller struct {
95
apiVersion version.Number
96
capabilities set.Strings
99
// Capabilities implements Controller.
100
func (c *controller) Capabilities() set.Strings {
101
return c.capabilities
104
// BootResources implements Controller.
105
func (c *controller) BootResources() ([]BootResource, error) {
106
source, err := c.get("boot-resources")
108
return nil, NewUnexpectedError(err)
110
resources, err := readBootResources(c.apiVersion, source)
112
return nil, errors.Trace(err)
114
var result []BootResource
115
for _, r := range resources {
116
result = append(result, r)
121
// Fabrics implements Controller.
122
func (c *controller) Fabrics() ([]Fabric, error) {
123
source, err := c.get("fabrics")
125
return nil, NewUnexpectedError(err)
127
fabrics, err := readFabrics(c.apiVersion, source)
129
return nil, errors.Trace(err)
132
for _, f := range fabrics {
133
result = append(result, f)
138
// Spaces implements Controller.
139
func (c *controller) Spaces() ([]Space, error) {
140
source, err := c.get("spaces")
142
return nil, NewUnexpectedError(err)
144
spaces, err := readSpaces(c.apiVersion, source)
146
return nil, errors.Trace(err)
149
for _, space := range spaces {
150
result = append(result, space)
155
// Zones implements Controller.
156
func (c *controller) Zones() ([]Zone, error) {
157
source, err := c.get("zones")
159
return nil, NewUnexpectedError(err)
161
zones, err := readZones(c.apiVersion, source)
163
return nil, errors.Trace(err)
166
for _, z := range zones {
167
result = append(result, z)
172
// DevicesArgs is a argument struct for selecting Devices.
173
// Only devices that match the specified criteria are returned.
174
type DevicesArgs struct {
176
MACAddresses []string
183
// Devices implements Controller.
184
func (c *controller) Devices(args DevicesArgs) ([]Device, error) {
185
params := NewURLParams()
186
params.MaybeAddMany("hostname", args.Hostname)
187
params.MaybeAddMany("mac_address", args.MACAddresses)
188
params.MaybeAddMany("id", args.SystemIDs)
189
params.MaybeAdd("domain", args.Domain)
190
params.MaybeAdd("zone", args.Zone)
191
params.MaybeAdd("agent_name", args.AgentName)
192
source, err := c.getQuery("devices", params.Values)
194
return nil, NewUnexpectedError(err)
196
devices, err := readDevices(c.apiVersion, source)
198
return nil, errors.Trace(err)
201
for _, d := range devices {
203
result = append(result, d)
208
// CreateDeviceArgs is a argument struct for passing information into CreateDevice.
209
type CreateDeviceArgs struct {
211
MACAddresses []string
216
// Devices implements Controller.
217
func (c *controller) CreateDevice(args CreateDeviceArgs) (Device, error) {
218
// There must be at least one mac address.
219
if len(args.MACAddresses) == 0 {
220
return nil, NewBadRequestError("at least one MAC address must be specified")
222
params := NewURLParams()
223
params.MaybeAdd("hostname", args.Hostname)
224
params.MaybeAdd("domain", args.Domain)
225
params.MaybeAddMany("mac_addresses", args.MACAddresses)
226
params.MaybeAdd("parent", args.Parent)
227
result, err := c.post("devices", "", params.Values)
229
if svrErr, ok := errors.Cause(err).(ServerError); ok {
230
if svrErr.StatusCode == http.StatusBadRequest {
231
return nil, errors.Wrap(err, NewBadRequestError(svrErr.BodyMessage))
234
// Translate http errors.
235
return nil, NewUnexpectedError(err)
238
device, err := readDevice(c.apiVersion, result)
240
return nil, errors.Trace(err)
242
device.controller = c
246
// MachinesArgs is a argument struct for selecting Machines.
247
// Only machines that match the specified criteria are returned.
248
type MachinesArgs struct {
250
MACAddresses []string
257
// Machines implements Controller.
258
func (c *controller) Machines(args MachinesArgs) ([]Machine, error) {
259
params := NewURLParams()
260
params.MaybeAddMany("hostname", args.Hostnames)
261
params.MaybeAddMany("mac_address", args.MACAddresses)
262
params.MaybeAddMany("id", args.SystemIDs)
263
params.MaybeAdd("domain", args.Domain)
264
params.MaybeAdd("zone", args.Zone)
265
params.MaybeAdd("agent_name", args.AgentName)
266
source, err := c.getQuery("machines", params.Values)
268
return nil, NewUnexpectedError(err)
270
machines, err := readMachines(c.apiVersion, source)
272
return nil, errors.Trace(err)
275
for _, m := range machines {
277
result = append(result, m)
282
// StorageSpec represents one element of storage constraints necessary
283
// to be satisfied to allocate a machine.
284
type StorageSpec struct {
285
// Label is optional and an arbitrary string. Labels need to be unique
286
// across the StorageSpec elements specified in the AllocateMachineArgs.
288
// Size is required and refers to the required minimum size in GB.
290
// Zero or more tags assocated to with the disks.
294
// Validate ensures that there is a positive size and that there are no Empty
296
func (s *StorageSpec) Validate() error {
298
return errors.NotValidf("Size value %d", s.Size)
300
for _, v := range s.Tags {
302
return errors.NotValidf("empty tag")
308
// String returns the string representation of the storage spec.
309
func (s *StorageSpec) String() string {
314
tags := strings.Join(s.Tags, ",")
316
tags = "(" + tags + ")"
318
return fmt.Sprintf("%s%d%s", label, s.Size, tags)
321
// InterfaceSpec represents one elemenet of network related constraints.
322
type InterfaceSpec struct {
323
// Label is required and an arbitrary string. Labels need to be unique
324
// across the InterfaceSpec elements specified in the AllocateMachineArgs.
325
// The label is returned in the ConstraintMatches response from
330
// NOTE: there are other interface spec values that we are not exposing at
331
// this stage that can be added on an as needed basis. Other possible values are:
332
// 'fabric_class', 'not_fabric_class',
333
// 'subnet_cidr', 'not_subnet_cidr',
335
// 'fabric', 'not_fabric',
336
// 'subnet', 'not_subnet',
340
// Validate ensures that a Label is specified and that there is at least one
341
// Space or NotSpace value set.
342
func (a *InterfaceSpec) Validate() error {
344
return errors.NotValidf("missing Label")
346
// Perhaps at some stage in the future there will be other possible specs
347
// supported (like vid, subnet, etc), but until then, just space to check.
349
return errors.NotValidf("empty Space constraint")
354
// String returns the interface spec as MaaS requires it.
355
func (a *InterfaceSpec) String() string {
356
return fmt.Sprintf("%s:space=%s", a.Label, a.Space)
359
// AllocateMachineArgs is an argument struct for passing args into Machine.Allocate.
360
type AllocateMachineArgs struct {
364
// MinMemory represented in MB.
370
// Storage represents the required disks on the Machine. If any are specified
371
// the first value is used for the root disk.
372
Storage []StorageSpec
373
// Interfaces represents a number of required interfaces on the machine.
374
// Each InterfaceSpec relates to an individual network interface.
375
Interfaces []InterfaceSpec
376
// NotSpace is a machine level constraint, and applies to the entire machine
377
// rather than specific interfaces.
384
// Validate makes sure that any labels specifed in Storage or Interfaces
385
// are unique, and that the required specifications are valid.
386
func (a *AllocateMachineArgs) Validate() error {
387
storageLabels := set.NewStrings()
388
for _, spec := range a.Storage {
389
if err := spec.Validate(); err != nil {
390
return errors.Annotate(err, "Storage")
392
if spec.Label != "" {
393
if storageLabels.Contains(spec.Label) {
394
return errors.NotValidf("reusing storage label %q", spec.Label)
396
storageLabels.Add(spec.Label)
399
interfaceLabels := set.NewStrings()
400
for _, spec := range a.Interfaces {
401
if err := spec.Validate(); err != nil {
402
return errors.Annotate(err, "Interfaces")
404
if interfaceLabels.Contains(spec.Label) {
405
return errors.NotValidf("reusing interface label %q", spec.Label)
407
interfaceLabels.Add(spec.Label)
409
for _, v := range a.NotSpace {
411
return errors.NotValidf("empty NotSpace constraint")
417
func (a *AllocateMachineArgs) storage() string {
419
for _, spec := range a.Storage {
420
values = append(values, spec.String())
422
return strings.Join(values, ",")
425
func (a *AllocateMachineArgs) interfaces() string {
427
for _, spec := range a.Interfaces {
428
values = append(values, spec.String())
430
return strings.Join(values, ";")
433
func (a *AllocateMachineArgs) notNetworks() string {
435
for _, v := range a.NotSpace {
436
values = append(values, "space:"+v)
438
return strings.Join(values, ",")
441
// ConstraintMatches provides a way for the caller of AllocateMachine to determine
442
//.how the allocated machine matched the storage and interfaces constraints specified.
443
// The labels that were used in the constraints are the keys in the maps.
444
type ConstraintMatches struct {
445
// Interface is a mapping of the constraint label specified to the Interfaces
446
// that match that constraint.
447
Interfaces map[string][]Interface
449
// Storage is a mapping of the constraint label specified to the BlockDevices
450
// that match that constraint.
451
Storage map[string][]BlockDevice
454
// AllocateMachine implements Controller.
456
// Returns an error that satisfies IsNoMatchError if the requested
457
// constraints cannot be met.
458
func (c *controller) AllocateMachine(args AllocateMachineArgs) (Machine, ConstraintMatches, error) {
459
var matches ConstraintMatches
460
params := NewURLParams()
461
params.MaybeAdd("name", args.Hostname)
462
params.MaybeAdd("arch", args.Architecture)
463
params.MaybeAddInt("cpu_count", args.MinCPUCount)
464
params.MaybeAddInt("mem", args.MinMemory)
465
params.MaybeAddMany("tags", args.Tags)
466
params.MaybeAddMany("not_tags", args.NotTags)
467
params.MaybeAdd("storage", args.storage())
468
params.MaybeAdd("interfaces", args.interfaces())
469
params.MaybeAdd("not_networks", args.notNetworks())
470
params.MaybeAdd("zone", args.Zone)
471
params.MaybeAddMany("not_in_zone", args.NotInZone)
472
params.MaybeAdd("agent_name", args.AgentName)
473
params.MaybeAdd("comment", args.Comment)
474
params.MaybeAddBool("dry_run", args.DryRun)
475
result, err := c.post("machines", "allocate", params.Values)
477
// A 409 Status code is "No Matching Machines"
478
if svrErr, ok := errors.Cause(err).(ServerError); ok {
479
if svrErr.StatusCode == http.StatusConflict {
480
return nil, matches, errors.Wrap(err, NewNoMatchError(svrErr.BodyMessage))
483
// Translate http errors.
484
return nil, matches, NewUnexpectedError(err)
487
machine, err := readMachine(c.apiVersion, result)
489
return nil, matches, errors.Trace(err)
491
machine.controller = c
493
// Parse the constraint matches.
494
matches, err = parseAllocateConstraintsResponse(result, machine)
496
return nil, matches, errors.Trace(err)
499
return machine, matches, nil
502
// ReleaseMachinesArgs is an argument struct for passing the machine system IDs
503
// and an optional comment into the ReleaseMachines method.
504
type ReleaseMachinesArgs struct {
509
// ReleaseMachines implements Controller.
511
// Release multiple machines at once. Returns
512
// - BadRequestError if any of the machines cannot be found
513
// - PermissionError if the user does not have permission to release any of the machines
514
// - CannotCompleteError if any of the machines could not be released due to their current state
515
func (c *controller) ReleaseMachines(args ReleaseMachinesArgs) error {
516
params := NewURLParams()
517
params.MaybeAddMany("machines", args.SystemIDs)
518
params.MaybeAdd("comment", args.Comment)
519
_, err := c.post("machines", "release", params.Values)
521
if svrErr, ok := errors.Cause(err).(ServerError); ok {
522
switch svrErr.StatusCode {
523
case http.StatusBadRequest:
524
return errors.Wrap(err, NewBadRequestError(svrErr.BodyMessage))
525
case http.StatusForbidden:
526
return errors.Wrap(err, NewPermissionError(svrErr.BodyMessage))
527
case http.StatusConflict:
528
return errors.Wrap(err, NewCannotCompleteError(svrErr.BodyMessage))
531
return NewUnexpectedError(err)
537
// Files implements Controller.
538
func (c *controller) Files(prefix string) ([]File, error) {
539
params := NewURLParams()
540
params.MaybeAdd("prefix", prefix)
541
source, err := c.getQuery("files", params.Values)
543
return nil, NewUnexpectedError(err)
545
files, err := readFiles(c.apiVersion, source)
547
return nil, errors.Trace(err)
550
for _, f := range files {
552
result = append(result, f)
557
// GetFile implements Controller.
558
func (c *controller) GetFile(filename string) (File, error) {
560
return nil, errors.NotValidf("missing filename")
562
source, err := c.get("files/" + filename)
564
if svrErr, ok := errors.Cause(err).(ServerError); ok {
565
if svrErr.StatusCode == http.StatusNotFound {
566
return nil, errors.Wrap(err, NewNoMatchError(svrErr.BodyMessage))
569
return nil, NewUnexpectedError(err)
571
file, err := readFile(c.apiVersion, source)
573
return nil, errors.Trace(err)
579
// AddFileArgs is a argument struct for passing information into AddFile.
580
// One of Content or (Reader, Length) must be specified.
581
type AddFileArgs struct {
588
// Validate checks to make sure the filename has no slashes, and that one of
589
// Content or (Reader, Length) is specified.
590
func (a *AddFileArgs) Validate() error {
591
dir, _ := path.Split(a.Filename)
593
return errors.NotValidf("paths in Filename %q", a.Filename)
595
if a.Filename == "" {
596
return errors.NotValidf("missing Filename")
598
if a.Content == nil {
600
return errors.NotValidf("missing Content or Reader")
603
return errors.NotValidf("missing Length")
607
return errors.NotValidf("specifying Content and Reader")
610
return errors.NotValidf("specifying Length and Content")
616
// AddFile implements Controller.
617
func (c *controller) AddFile(args AddFileArgs) error {
618
if err := args.Validate(); err != nil {
619
return errors.Trace(err)
621
fileContent := args.Content
622
if fileContent == nil {
623
content, err := ioutil.ReadAll(io.LimitReader(args.Reader, args.Length))
625
return errors.Annotatef(err, "cannot read file content")
627
fileContent = content
629
params := url.Values{"filename": {args.Filename}}
630
_, err := c.postFile("files", "", params, fileContent)
632
if svrErr, ok := errors.Cause(err).(ServerError); ok {
633
if svrErr.StatusCode == http.StatusBadRequest {
634
return errors.Wrap(err, NewBadRequestError(svrErr.BodyMessage))
637
return NewUnexpectedError(err)
642
func (c *controller) checkCreds() error {
643
if _, err := c.getOp("users", "whoami"); err != nil {
644
if svrErr, ok := errors.Cause(err).(ServerError); ok {
645
if svrErr.StatusCode == http.StatusUnauthorized {
646
return errors.Wrap(err, NewPermissionError(svrErr.BodyMessage))
649
return NewUnexpectedError(err)
654
func (c *controller) put(path string, params url.Values) (interface{}, error) {
655
path = EnsureTrailingSlash(path)
656
requestID := nextRequestID()
657
logger.Tracef("request %x: PUT %s%s, params: %s", requestID, c.client.APIURL, path, params.Encode())
658
bytes, err := c.client.Put(&url.URL{Path: path}, params)
660
logger.Tracef("response %x: error: %q", requestID, err.Error())
661
logger.Tracef("error detail: %#v", err)
662
return nil, errors.Trace(err)
664
logger.Tracef("response %x: %s", requestID, string(bytes))
666
var parsed interface{}
667
err = json.Unmarshal(bytes, &parsed)
669
return nil, errors.Trace(err)
674
func (c *controller) post(path, op string, params url.Values) (interface{}, error) {
675
bytes, err := c._postRaw(path, op, params, nil)
677
return nil, errors.Trace(err)
680
var parsed interface{}
681
err = json.Unmarshal(bytes, &parsed)
683
return nil, errors.Trace(err)
688
func (c *controller) postFile(path, op string, params url.Values, fileContent []byte) (interface{}, error) {
689
// Only one file is ever sent at a time.
690
files := map[string][]byte{"file": fileContent}
691
return c._postRaw(path, op, params, files)
694
func (c *controller) _postRaw(path, op string, params url.Values, files map[string][]byte) ([]byte, error) {
695
path = EnsureTrailingSlash(path)
696
requestID := nextRequestID()
697
if logger.IsTraceEnabled() {
702
logger.Tracef("request %x: POST %s%s%s, params=%s", requestID, c.client.APIURL, path, opArg, params.Encode())
704
bytes, err := c.client.Post(&url.URL{Path: path}, op, params, files)
706
logger.Tracef("response %x: error: %q", requestID, err.Error())
707
logger.Tracef("error detail: %#v", err)
708
return nil, errors.Trace(err)
710
logger.Tracef("response %x: %s", requestID, string(bytes))
714
func (c *controller) delete(path string) error {
715
path = EnsureTrailingSlash(path)
716
requestID := nextRequestID()
717
logger.Tracef("request %x: DELETE %s%s", requestID, c.client.APIURL, path)
718
err := c.client.Delete(&url.URL{Path: path})
720
logger.Tracef("response %x: error: %q", requestID, err.Error())
721
logger.Tracef("error detail: %#v", err)
722
return errors.Trace(err)
724
logger.Tracef("response %x: complete", requestID)
728
func (c *controller) getQuery(path string, params url.Values) (interface{}, error) {
729
return c._get(path, "", params)
732
func (c *controller) get(path string) (interface{}, error) {
733
return c._get(path, "", nil)
736
func (c *controller) getOp(path, op string) (interface{}, error) {
737
return c._get(path, op, nil)
740
func (c *controller) _get(path, op string, params url.Values) (interface{}, error) {
741
bytes, err := c._getRaw(path, op, params)
743
return nil, errors.Trace(err)
745
var parsed interface{}
746
err = json.Unmarshal(bytes, &parsed)
748
return nil, errors.Trace(err)
753
func (c *controller) _getRaw(path, op string, params url.Values) ([]byte, error) {
754
path = EnsureTrailingSlash(path)
755
requestID := nextRequestID()
756
if logger.IsTraceEnabled() {
759
query = "?" + params.Encode()
761
logger.Tracef("request %x: GET %s%s%s", requestID, c.client.APIURL, path, query)
763
bytes, err := c.client.Get(&url.URL{Path: path}, op, params)
765
logger.Tracef("response %x: error: %q", requestID, err.Error())
766
logger.Tracef("error detail: %#v", err)
767
return nil, errors.Trace(err)
769
logger.Tracef("response %x: %s", requestID, string(bytes))
773
func nextRequestID() int64 {
774
return atomic.AddInt64(&requestNumber, 1)
777
func (c *controller) readAPIVersion(apiVersion version.Number) (set.Strings, version.Number, error) {
778
parsed, err := c.get("version")
780
return nil, apiVersion, errors.Trace(err)
783
// As we care about other fields, add them.
784
fields := schema.Fields{
785
"capabilities": schema.List(schema.String()),
787
checker := schema.FieldMap(fields, nil) // no defaults
788
coerced, err := checker.Coerce(parsed, nil)
790
return nil, apiVersion, WrapWithDeserializationError(err, "version response")
792
// For now, we don't append any subversion, but as it becomes used, we
793
// should parse and check.
795
valid := coerced.(map[string]interface{})
796
// From here we know that the map returned from the schema coercion
797
// contains fields of the right type.
798
capabilities := set.NewStrings()
799
capabilityValues := valid["capabilities"].([]interface{})
800
for _, value := range capabilityValues {
801
capabilities.Add(value.(string))
804
return capabilities, apiVersion, nil
807
func parseAllocateConstraintsResponse(source interface{}, machine *machine) (ConstraintMatches, error) {
808
var empty ConstraintMatches
809
matchFields := schema.Fields{
810
"storage": schema.StringMap(schema.List(schema.ForceInt())),
811
"interfaces": schema.StringMap(schema.List(schema.ForceInt())),
813
matchDefaults := schema.Defaults{
814
"storage": schema.Omit,
815
"interfaces": schema.Omit,
817
fields := schema.Fields{
818
"constraints_by_type": schema.FieldMap(matchFields, matchDefaults),
820
checker := schema.FieldMap(fields, nil) // no defaults
821
coerced, err := checker.Coerce(source, nil)
823
return empty, WrapWithDeserializationError(err, "allocation constraints response schema check failed")
825
valid := coerced.(map[string]interface{})
826
constraintsMap := valid["constraints_by_type"].(map[string]interface{})
827
result := ConstraintMatches{
828
Interfaces: make(map[string][]Interface),
829
Storage: make(map[string][]BlockDevice),
832
if interfaceMatches, found := constraintsMap["interfaces"]; found {
833
matches := convertConstraintMatches(interfaceMatches)
834
for label, ids := range matches {
835
interfaces := make([]Interface, len(ids))
836
for index, id := range ids {
837
iface := machine.Interface(id)
839
return empty, NewDeserializationError("constraint match interface %q: %d does not match an interface for the machine", label, id)
841
interfaces[index] = iface
843
result.Interfaces[label] = interfaces
847
if storageMatches, found := constraintsMap["storage"]; found {
848
matches := convertConstraintMatches(storageMatches)
849
for label, ids := range matches {
850
blockDevices := make([]BlockDevice, len(ids))
851
for index, id := range ids {
852
blockDevice := machine.PhysicalBlockDevice(id)
853
if blockDevice == nil {
854
return empty, NewDeserializationError("constraint match storage %q: %d does not match a physical block device for the machine", label, id)
856
blockDevices[index] = blockDevice
858
result.Storage[label] = blockDevices
864
func convertConstraintMatches(source interface{}) map[string][]int {
865
// These casts are all safe because of the schema check.
866
result := make(map[string][]int)
867
matchMap := source.(map[string]interface{})
868
for label, values := range matchMap {
869
items := values.([]interface{})
870
result[label] = make([]int, len(items))
871
for index, value := range items {
872
result[label][index] = value.(int)