blob: 3c729c2dcfc3ee1959b55e1d0ccf30eb5bece2d8 [file] [log] [blame]
David K. Bainbridge528b3182017-01-23 08:51:59 -08001// Copyright 2016 Canonical Ltd.
2// Licensed under the LGPLv3, see LICENCE file for details.
3
4package gomaasapi
5
6import (
7 "encoding/json"
8 "fmt"
9 "io"
10 "io/ioutil"
11 "net/http"
12 "net/url"
13 "path"
14 "strings"
15 "sync/atomic"
16
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"
22)
23
24var (
25 logger = loggo.GetLogger("maas")
26
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"}
30
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}
35
36 // Current request number. Informational only for logging.
37 requestNumber int64
38)
39
40// ControllerArgs is an argument struct for passing the required parameters
41// to the NewController method.
42type ControllerArgs struct {
43 BaseURL string
44 APIKey string
45}
46
47// NewController creates an authenticated client to the MAAS API, and checks
48// the capabilities of the server.
49//
50// If the APIKey is not valid, a NotValid error is returned.
51// If the credentials are incorrect, a PermissionError is returned.
52func 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.
59 if err != nil {
60 return nil, errors.Errorf("bad version defined in supported versions: %q", apiVersion)
61 }
62 client, err := NewAuthenticatedClient(args.BaseURL, args.APIKey, apiVersion)
63 if err != nil {
64 // If the credentials aren't valid, return now.
65 if errors.IsNotValid(err) {
66 return nil, errors.Trace(err)
67 }
68 // Any other error attempting to create the authenticated client
69 // is an unexpected error and return now.
70 return nil, NewUnexpectedError(err)
71 }
72 controllerVersion := version.Number{
73 Major: major,
74 Minor: minor,
75 }
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)
79 if err != nil {
80 logger.Debugf("read version failed: %#v", err)
81 continue
82 }
83
84 if err := controller.checkCreds(); err != nil {
85 return nil, errors.Trace(err)
86 }
87 return controller, nil
88 }
89
90 return nil, NewUnsupportedVersionError("controller at %s does not support any of %s", args.BaseURL, supportedAPIVersions)
91}
92
93type controller struct {
94 client *Client
95 apiVersion version.Number
96 capabilities set.Strings
97}
98
99// Capabilities implements Controller.
100func (c *controller) Capabilities() set.Strings {
101 return c.capabilities
102}
103
104// BootResources implements Controller.
105func (c *controller) BootResources() ([]BootResource, error) {
106 source, err := c.get("boot-resources")
107 if err != nil {
108 return nil, NewUnexpectedError(err)
109 }
110 resources, err := readBootResources(c.apiVersion, source)
111 if err != nil {
112 return nil, errors.Trace(err)
113 }
114 var result []BootResource
115 for _, r := range resources {
116 result = append(result, r)
117 }
118 return result, nil
119}
120
121// Fabrics implements Controller.
122func (c *controller) Fabrics() ([]Fabric, error) {
123 source, err := c.get("fabrics")
124 if err != nil {
125 return nil, NewUnexpectedError(err)
126 }
127 fabrics, err := readFabrics(c.apiVersion, source)
128 if err != nil {
129 return nil, errors.Trace(err)
130 }
131 var result []Fabric
132 for _, f := range fabrics {
133 result = append(result, f)
134 }
135 return result, nil
136}
137
138// Spaces implements Controller.
139func (c *controller) Spaces() ([]Space, error) {
140 source, err := c.get("spaces")
141 if err != nil {
142 return nil, NewUnexpectedError(err)
143 }
144 spaces, err := readSpaces(c.apiVersion, source)
145 if err != nil {
146 return nil, errors.Trace(err)
147 }
148 var result []Space
149 for _, space := range spaces {
150 result = append(result, space)
151 }
152 return result, nil
153}
154
155// Zones implements Controller.
156func (c *controller) Zones() ([]Zone, error) {
157 source, err := c.get("zones")
158 if err != nil {
159 return nil, NewUnexpectedError(err)
160 }
161 zones, err := readZones(c.apiVersion, source)
162 if err != nil {
163 return nil, errors.Trace(err)
164 }
165 var result []Zone
166 for _, z := range zones {
167 result = append(result, z)
168 }
169 return result, nil
170}
171
172// DevicesArgs is a argument struct for selecting Devices.
173// Only devices that match the specified criteria are returned.
174type DevicesArgs struct {
175 Hostname []string
176 MACAddresses []string
177 SystemIDs []string
178 Domain string
179 Zone string
180 AgentName string
181}
182
183// Devices implements Controller.
184func (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)
193 if err != nil {
194 return nil, NewUnexpectedError(err)
195 }
196 devices, err := readDevices(c.apiVersion, source)
197 if err != nil {
198 return nil, errors.Trace(err)
199 }
200 var result []Device
201 for _, d := range devices {
202 d.controller = c
203 result = append(result, d)
204 }
205 return result, nil
206}
207
208// CreateDeviceArgs is a argument struct for passing information into CreateDevice.
209type CreateDeviceArgs struct {
210 Hostname string
211 MACAddresses []string
212 Domain string
213 Parent string
214}
215
216// Devices implements Controller.
217func (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")
221 }
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)
228 if err != nil {
229 if svrErr, ok := errors.Cause(err).(ServerError); ok {
230 if svrErr.StatusCode == http.StatusBadRequest {
231 return nil, errors.Wrap(err, NewBadRequestError(svrErr.BodyMessage))
232 }
233 }
234 // Translate http errors.
235 return nil, NewUnexpectedError(err)
236 }
237
238 device, err := readDevice(c.apiVersion, result)
239 if err != nil {
240 return nil, errors.Trace(err)
241 }
242 device.controller = c
243 return device, nil
244}
245
246// MachinesArgs is a argument struct for selecting Machines.
247// Only machines that match the specified criteria are returned.
248type MachinesArgs struct {
249 Hostnames []string
250 MACAddresses []string
251 SystemIDs []string
252 Domain string
253 Zone string
254 AgentName string
255 OwnerData map[string]string
256}
257
258// Machines implements Controller.
259func (c *controller) Machines(args MachinesArgs) ([]Machine, error) {
260 params := NewURLParams()
261 params.MaybeAddMany("hostname", args.Hostnames)
262 params.MaybeAddMany("mac_address", args.MACAddresses)
263 params.MaybeAddMany("id", args.SystemIDs)
264 params.MaybeAdd("domain", args.Domain)
265 params.MaybeAdd("zone", args.Zone)
266 params.MaybeAdd("agent_name", args.AgentName)
267 // At the moment the MAAS API doesn't support filtering by owner
268 // data so we do that ourselves below.
269 source, err := c.getQuery("machines", params.Values)
270 if err != nil {
271 return nil, NewUnexpectedError(err)
272 }
273 machines, err := readMachines(c.apiVersion, source)
274 if err != nil {
275 return nil, errors.Trace(err)
276 }
277 var result []Machine
278 for _, m := range machines {
279 m.controller = c
280 if ownerDataMatches(m.ownerData, args.OwnerData) {
281 result = append(result, m)
282 }
283 }
284 return result, nil
285}
286
287func ownerDataMatches(ownerData, filter map[string]string) bool {
288 for key, value := range filter {
289 if ownerData[key] != value {
290 return false
291 }
292 }
293 return true
294}
295
296// StorageSpec represents one element of storage constraints necessary
297// to be satisfied to allocate a machine.
298type StorageSpec struct {
299 // Label is optional and an arbitrary string. Labels need to be unique
300 // across the StorageSpec elements specified in the AllocateMachineArgs.
301 Label string
302 // Size is required and refers to the required minimum size in GB.
303 Size int
304 // Zero or more tags assocated to with the disks.
305 Tags []string
306}
307
308// Validate ensures that there is a positive size and that there are no Empty
309// tag values.
310func (s *StorageSpec) Validate() error {
311 if s.Size <= 0 {
312 return errors.NotValidf("Size value %d", s.Size)
313 }
314 for _, v := range s.Tags {
315 if v == "" {
316 return errors.NotValidf("empty tag")
317 }
318 }
319 return nil
320}
321
322// String returns the string representation of the storage spec.
323func (s *StorageSpec) String() string {
324 label := s.Label
325 if label != "" {
326 label += ":"
327 }
328 tags := strings.Join(s.Tags, ",")
329 if tags != "" {
330 tags = "(" + tags + ")"
331 }
332 return fmt.Sprintf("%s%d%s", label, s.Size, tags)
333}
334
335// InterfaceSpec represents one elemenet of network related constraints.
336type InterfaceSpec struct {
337 // Label is required and an arbitrary string. Labels need to be unique
338 // across the InterfaceSpec elements specified in the AllocateMachineArgs.
339 // The label is returned in the ConstraintMatches response from
340 // AllocateMachine.
341 Label string
342 Space string
343
344 // NOTE: there are other interface spec values that we are not exposing at
345 // this stage that can be added on an as needed basis. Other possible values are:
346 // 'fabric_class', 'not_fabric_class',
347 // 'subnet_cidr', 'not_subnet_cidr',
348 // 'vid', 'not_vid',
349 // 'fabric', 'not_fabric',
350 // 'subnet', 'not_subnet',
351 // 'mode'
352}
353
354// Validate ensures that a Label is specified and that there is at least one
355// Space or NotSpace value set.
356func (a *InterfaceSpec) Validate() error {
357 if a.Label == "" {
358 return errors.NotValidf("missing Label")
359 }
360 // Perhaps at some stage in the future there will be other possible specs
361 // supported (like vid, subnet, etc), but until then, just space to check.
362 if a.Space == "" {
363 return errors.NotValidf("empty Space constraint")
364 }
365 return nil
366}
367
368// String returns the interface spec as MaaS requires it.
369func (a *InterfaceSpec) String() string {
370 return fmt.Sprintf("%s:space=%s", a.Label, a.Space)
371}
372
373// AllocateMachineArgs is an argument struct for passing args into Machine.Allocate.
374type AllocateMachineArgs struct {
375 Hostname string
376 Architecture string
377 MinCPUCount int
378 // MinMemory represented in MB.
379 MinMemory int
380 Tags []string
381 NotTags []string
382 Zone string
383 NotInZone []string
384 // Storage represents the required disks on the Machine. If any are specified
385 // the first value is used for the root disk.
386 Storage []StorageSpec
387 // Interfaces represents a number of required interfaces on the machine.
388 // Each InterfaceSpec relates to an individual network interface.
389 Interfaces []InterfaceSpec
390 // NotSpace is a machine level constraint, and applies to the entire machine
391 // rather than specific interfaces.
392 NotSpace []string
393 AgentName string
394 Comment string
395 DryRun bool
396}
397
398// Validate makes sure that any labels specifed in Storage or Interfaces
399// are unique, and that the required specifications are valid.
400func (a *AllocateMachineArgs) Validate() error {
401 storageLabels := set.NewStrings()
402 for _, spec := range a.Storage {
403 if err := spec.Validate(); err != nil {
404 return errors.Annotate(err, "Storage")
405 }
406 if spec.Label != "" {
407 if storageLabels.Contains(spec.Label) {
408 return errors.NotValidf("reusing storage label %q", spec.Label)
409 }
410 storageLabels.Add(spec.Label)
411 }
412 }
413 interfaceLabels := set.NewStrings()
414 for _, spec := range a.Interfaces {
415 if err := spec.Validate(); err != nil {
416 return errors.Annotate(err, "Interfaces")
417 }
418 if interfaceLabels.Contains(spec.Label) {
419 return errors.NotValidf("reusing interface label %q", spec.Label)
420 }
421 interfaceLabels.Add(spec.Label)
422 }
423 for _, v := range a.NotSpace {
424 if v == "" {
425 return errors.NotValidf("empty NotSpace constraint")
426 }
427 }
428 return nil
429}
430
431func (a *AllocateMachineArgs) storage() string {
432 var values []string
433 for _, spec := range a.Storage {
434 values = append(values, spec.String())
435 }
436 return strings.Join(values, ",")
437}
438
439func (a *AllocateMachineArgs) interfaces() string {
440 var values []string
441 for _, spec := range a.Interfaces {
442 values = append(values, spec.String())
443 }
444 return strings.Join(values, ";")
445}
446
447func (a *AllocateMachineArgs) notSubnets() []string {
448 var values []string
449 for _, v := range a.NotSpace {
450 values = append(values, "space:"+v)
451 }
452 return values
453}
454
455// ConstraintMatches provides a way for the caller of AllocateMachine to determine
456//.how the allocated machine matched the storage and interfaces constraints specified.
457// The labels that were used in the constraints are the keys in the maps.
458type ConstraintMatches struct {
459 // Interface is a mapping of the constraint label specified to the Interfaces
460 // that match that constraint.
461 Interfaces map[string][]Interface
462
463 // Storage is a mapping of the constraint label specified to the BlockDevices
464 // that match that constraint.
465 Storage map[string][]BlockDevice
466}
467
468// AllocateMachine implements Controller.
469//
470// Returns an error that satisfies IsNoMatchError if the requested
471// constraints cannot be met.
472func (c *controller) AllocateMachine(args AllocateMachineArgs) (Machine, ConstraintMatches, error) {
473 var matches ConstraintMatches
474 params := NewURLParams()
475 params.MaybeAdd("name", args.Hostname)
476 params.MaybeAdd("arch", args.Architecture)
477 params.MaybeAddInt("cpu_count", args.MinCPUCount)
478 params.MaybeAddInt("mem", args.MinMemory)
479 params.MaybeAddMany("tags", args.Tags)
480 params.MaybeAddMany("not_tags", args.NotTags)
481 params.MaybeAdd("storage", args.storage())
482 params.MaybeAdd("interfaces", args.interfaces())
483 params.MaybeAddMany("not_subnets", args.notSubnets())
484 params.MaybeAdd("zone", args.Zone)
485 params.MaybeAddMany("not_in_zone", args.NotInZone)
486 params.MaybeAdd("agent_name", args.AgentName)
487 params.MaybeAdd("comment", args.Comment)
488 params.MaybeAddBool("dry_run", args.DryRun)
489 result, err := c.post("machines", "allocate", params.Values)
490 if err != nil {
491 // A 409 Status code is "No Matching Machines"
492 if svrErr, ok := errors.Cause(err).(ServerError); ok {
493 if svrErr.StatusCode == http.StatusConflict {
494 return nil, matches, errors.Wrap(err, NewNoMatchError(svrErr.BodyMessage))
495 }
496 }
497 // Translate http errors.
498 return nil, matches, NewUnexpectedError(err)
499 }
500
501 machine, err := readMachine(c.apiVersion, result)
502 if err != nil {
503 return nil, matches, errors.Trace(err)
504 }
505 machine.controller = c
506
507 // Parse the constraint matches.
508 matches, err = parseAllocateConstraintsResponse(result, machine)
509 if err != nil {
510 return nil, matches, errors.Trace(err)
511 }
512
513 return machine, matches, nil
514}
515
516// ReleaseMachinesArgs is an argument struct for passing the machine system IDs
517// and an optional comment into the ReleaseMachines method.
518type ReleaseMachinesArgs struct {
519 SystemIDs []string
520 Comment string
521}
522
523// ReleaseMachines implements Controller.
524//
525// Release multiple machines at once. Returns
526// - BadRequestError if any of the machines cannot be found
527// - PermissionError if the user does not have permission to release any of the machines
528// - CannotCompleteError if any of the machines could not be released due to their current state
529func (c *controller) ReleaseMachines(args ReleaseMachinesArgs) error {
530 params := NewURLParams()
531 params.MaybeAddMany("machines", args.SystemIDs)
532 params.MaybeAdd("comment", args.Comment)
533 _, err := c.post("machines", "release", params.Values)
534 if err != nil {
535 if svrErr, ok := errors.Cause(err).(ServerError); ok {
536 switch svrErr.StatusCode {
537 case http.StatusBadRequest:
538 return errors.Wrap(err, NewBadRequestError(svrErr.BodyMessage))
539 case http.StatusForbidden:
540 return errors.Wrap(err, NewPermissionError(svrErr.BodyMessage))
541 case http.StatusConflict:
542 return errors.Wrap(err, NewCannotCompleteError(svrErr.BodyMessage))
543 }
544 }
545 return NewUnexpectedError(err)
546 }
547
548 return nil
549}
550
551// Files implements Controller.
552func (c *controller) Files(prefix string) ([]File, error) {
553 params := NewURLParams()
554 params.MaybeAdd("prefix", prefix)
555 source, err := c.getQuery("files", params.Values)
556 if err != nil {
557 return nil, NewUnexpectedError(err)
558 }
559 files, err := readFiles(c.apiVersion, source)
560 if err != nil {
561 return nil, errors.Trace(err)
562 }
563 var result []File
564 for _, f := range files {
565 f.controller = c
566 result = append(result, f)
567 }
568 return result, nil
569}
570
571// GetFile implements Controller.
572func (c *controller) GetFile(filename string) (File, error) {
573 if filename == "" {
574 return nil, errors.NotValidf("missing filename")
575 }
576 source, err := c.get("files/" + filename)
577 if err != nil {
578 if svrErr, ok := errors.Cause(err).(ServerError); ok {
579 if svrErr.StatusCode == http.StatusNotFound {
580 return nil, errors.Wrap(err, NewNoMatchError(svrErr.BodyMessage))
581 }
582 }
583 return nil, NewUnexpectedError(err)
584 }
585 file, err := readFile(c.apiVersion, source)
586 if err != nil {
587 return nil, errors.Trace(err)
588 }
589 file.controller = c
590 return file, nil
591}
592
593// AddFileArgs is a argument struct for passing information into AddFile.
594// One of Content or (Reader, Length) must be specified.
595type AddFileArgs struct {
596 Filename string
597 Content []byte
598 Reader io.Reader
599 Length int64
600}
601
602// Validate checks to make sure the filename has no slashes, and that one of
603// Content or (Reader, Length) is specified.
604func (a *AddFileArgs) Validate() error {
605 dir, _ := path.Split(a.Filename)
606 if dir != "" {
607 return errors.NotValidf("paths in Filename %q", a.Filename)
608 }
609 if a.Filename == "" {
610 return errors.NotValidf("missing Filename")
611 }
612 if a.Content == nil {
613 if a.Reader == nil {
614 return errors.NotValidf("missing Content or Reader")
615 }
616 if a.Length == 0 {
617 return errors.NotValidf("missing Length")
618 }
619 } else {
620 if a.Reader != nil {
621 return errors.NotValidf("specifying Content and Reader")
622 }
623 if a.Length != 0 {
624 return errors.NotValidf("specifying Length and Content")
625 }
626 }
627 return nil
628}
629
630// AddFile implements Controller.
631func (c *controller) AddFile(args AddFileArgs) error {
632 if err := args.Validate(); err != nil {
633 return errors.Trace(err)
634 }
635 fileContent := args.Content
636 if fileContent == nil {
637 content, err := ioutil.ReadAll(io.LimitReader(args.Reader, args.Length))
638 if err != nil {
639 return errors.Annotatef(err, "cannot read file content")
640 }
641 fileContent = content
642 }
643 params := url.Values{"filename": {args.Filename}}
644 _, err := c.postFile("files", "", params, fileContent)
645 if err != nil {
646 if svrErr, ok := errors.Cause(err).(ServerError); ok {
647 if svrErr.StatusCode == http.StatusBadRequest {
648 return errors.Wrap(err, NewBadRequestError(svrErr.BodyMessage))
649 }
650 }
651 return NewUnexpectedError(err)
652 }
653 return nil
654}
655
656func (c *controller) checkCreds() error {
657 if _, err := c.getOp("users", "whoami"); err != nil {
658 if svrErr, ok := errors.Cause(err).(ServerError); ok {
659 if svrErr.StatusCode == http.StatusUnauthorized {
660 return errors.Wrap(err, NewPermissionError(svrErr.BodyMessage))
661 }
662 }
663 return NewUnexpectedError(err)
664 }
665 return nil
666}
667
668func (c *controller) put(path string, params url.Values) (interface{}, error) {
669 path = EnsureTrailingSlash(path)
670 requestID := nextRequestID()
671 logger.Tracef("request %x: PUT %s%s, params: %s", requestID, c.client.APIURL, path, params.Encode())
672 bytes, err := c.client.Put(&url.URL{Path: path}, params)
673 if err != nil {
674 logger.Tracef("response %x: error: %q", requestID, err.Error())
675 logger.Tracef("error detail: %#v", err)
676 return nil, errors.Trace(err)
677 }
678 logger.Tracef("response %x: %s", requestID, string(bytes))
679
680 var parsed interface{}
681 err = json.Unmarshal(bytes, &parsed)
682 if err != nil {
683 return nil, errors.Trace(err)
684 }
685 return parsed, nil
686}
687
688func (c *controller) post(path, op string, params url.Values) (interface{}, error) {
689 bytes, err := c._postRaw(path, op, params, nil)
690 if err != nil {
691 return nil, errors.Trace(err)
692 }
693
694 var parsed interface{}
695 err = json.Unmarshal(bytes, &parsed)
696 if err != nil {
697 return nil, errors.Trace(err)
698 }
699 return parsed, nil
700}
701
702func (c *controller) postFile(path, op string, params url.Values, fileContent []byte) (interface{}, error) {
703 // Only one file is ever sent at a time.
704 files := map[string][]byte{"file": fileContent}
705 return c._postRaw(path, op, params, files)
706}
707
708func (c *controller) _postRaw(path, op string, params url.Values, files map[string][]byte) ([]byte, error) {
709 path = EnsureTrailingSlash(path)
710 requestID := nextRequestID()
711 if logger.IsTraceEnabled() {
712 opArg := ""
713 if op != "" {
714 opArg = "?op=" + op
715 }
716 logger.Tracef("request %x: POST %s%s%s, params=%s", requestID, c.client.APIURL, path, opArg, params.Encode())
717 }
718 bytes, err := c.client.Post(&url.URL{Path: path}, op, params, files)
719 if err != nil {
720 logger.Tracef("response %x: error: %q", requestID, err.Error())
721 logger.Tracef("error detail: %#v", err)
722 return nil, errors.Trace(err)
723 }
724 logger.Tracef("response %x: %s", requestID, string(bytes))
725 return bytes, nil
726}
727
728func (c *controller) delete(path string) error {
729 path = EnsureTrailingSlash(path)
730 requestID := nextRequestID()
731 logger.Tracef("request %x: DELETE %s%s", requestID, c.client.APIURL, path)
732 err := c.client.Delete(&url.URL{Path: path})
733 if err != nil {
734 logger.Tracef("response %x: error: %q", requestID, err.Error())
735 logger.Tracef("error detail: %#v", err)
736 return errors.Trace(err)
737 }
738 logger.Tracef("response %x: complete", requestID)
739 return nil
740}
741
742func (c *controller) getQuery(path string, params url.Values) (interface{}, error) {
743 return c._get(path, "", params)
744}
745
746func (c *controller) get(path string) (interface{}, error) {
747 return c._get(path, "", nil)
748}
749
750func (c *controller) getOp(path, op string) (interface{}, error) {
751 return c._get(path, op, nil)
752}
753
754func (c *controller) _get(path, op string, params url.Values) (interface{}, error) {
755 bytes, err := c._getRaw(path, op, params)
756 if err != nil {
757 return nil, errors.Trace(err)
758 }
759 var parsed interface{}
760 err = json.Unmarshal(bytes, &parsed)
761 if err != nil {
762 return nil, errors.Trace(err)
763 }
764 return parsed, nil
765}
766
767func (c *controller) _getRaw(path, op string, params url.Values) ([]byte, error) {
768 path = EnsureTrailingSlash(path)
769 requestID := nextRequestID()
770 if logger.IsTraceEnabled() {
771 var query string
772 if params != nil {
773 query = "?" + params.Encode()
774 }
775 logger.Tracef("request %x: GET %s%s%s", requestID, c.client.APIURL, path, query)
776 }
777 bytes, err := c.client.Get(&url.URL{Path: path}, op, params)
778 if err != nil {
779 logger.Tracef("response %x: error: %q", requestID, err.Error())
780 logger.Tracef("error detail: %#v", err)
781 return nil, errors.Trace(err)
782 }
783 logger.Tracef("response %x: %s", requestID, string(bytes))
784 return bytes, nil
785}
786
787func nextRequestID() int64 {
788 return atomic.AddInt64(&requestNumber, 1)
789}
790
791func (c *controller) readAPIVersion(apiVersion version.Number) (set.Strings, version.Number, error) {
792 parsed, err := c.get("version")
793 if err != nil {
794 return nil, apiVersion, errors.Trace(err)
795 }
796
797 // As we care about other fields, add them.
798 fields := schema.Fields{
799 "capabilities": schema.List(schema.String()),
800 }
801 checker := schema.FieldMap(fields, nil) // no defaults
802 coerced, err := checker.Coerce(parsed, nil)
803 if err != nil {
804 return nil, apiVersion, WrapWithDeserializationError(err, "version response")
805 }
806 // For now, we don't append any subversion, but as it becomes used, we
807 // should parse and check.
808
809 valid := coerced.(map[string]interface{})
810 // From here we know that the map returned from the schema coercion
811 // contains fields of the right type.
812 capabilities := set.NewStrings()
813 capabilityValues := valid["capabilities"].([]interface{})
814 for _, value := range capabilityValues {
815 capabilities.Add(value.(string))
816 }
817
818 return capabilities, apiVersion, nil
819}
820
821func parseAllocateConstraintsResponse(source interface{}, machine *machine) (ConstraintMatches, error) {
822 var empty ConstraintMatches
823 matchFields := schema.Fields{
824 "storage": schema.StringMap(schema.List(schema.ForceInt())),
825 "interfaces": schema.StringMap(schema.List(schema.ForceInt())),
826 }
827 matchDefaults := schema.Defaults{
828 "storage": schema.Omit,
829 "interfaces": schema.Omit,
830 }
831 fields := schema.Fields{
832 "constraints_by_type": schema.FieldMap(matchFields, matchDefaults),
833 }
834 checker := schema.FieldMap(fields, nil) // no defaults
835 coerced, err := checker.Coerce(source, nil)
836 if err != nil {
837 return empty, WrapWithDeserializationError(err, "allocation constraints response schema check failed")
838 }
839 valid := coerced.(map[string]interface{})
840 constraintsMap := valid["constraints_by_type"].(map[string]interface{})
841 result := ConstraintMatches{
842 Interfaces: make(map[string][]Interface),
843 Storage: make(map[string][]BlockDevice),
844 }
845
846 if interfaceMatches, found := constraintsMap["interfaces"]; found {
847 matches := convertConstraintMatches(interfaceMatches)
848 for label, ids := range matches {
849 interfaces := make([]Interface, len(ids))
850 for index, id := range ids {
851 iface := machine.Interface(id)
852 if iface == nil {
853 return empty, NewDeserializationError("constraint match interface %q: %d does not match an interface for the machine", label, id)
854 }
855 interfaces[index] = iface
856 }
857 result.Interfaces[label] = interfaces
858 }
859 }
860
861 if storageMatches, found := constraintsMap["storage"]; found {
862 matches := convertConstraintMatches(storageMatches)
863 for label, ids := range matches {
864 blockDevices := make([]BlockDevice, len(ids))
865 for index, id := range ids {
866 blockDevice := machine.PhysicalBlockDevice(id)
867 if blockDevice == nil {
868 return empty, NewDeserializationError("constraint match storage %q: %d does not match a physical block device for the machine", label, id)
869 }
870 blockDevices[index] = blockDevice
871 }
872 result.Storage[label] = blockDevices
873 }
874 }
875 return result, nil
876}
877
878func convertConstraintMatches(source interface{}) map[string][]int {
879 // These casts are all safe because of the schema check.
880 result := make(map[string][]int)
881 matchMap := source.(map[string]interface{})
882 for label, values := range matchMap {
883 items := values.([]interface{})
884 result[label] = make([]int, len(items))
885 for index, value := range items {
886 result[label][index] = value.(int)
887 }
888 }
889 return result
890}