~nskaggs/+junk/xenial-test

« back to all changes in this revision

Viewing changes to src/github.com/juju/gomaasapi/controller.go

  • Committer: Nicholas Skaggs
  • Date: 2016-10-24 20:56:05 UTC
  • Revision ID: nicholas.skaggs@canonical.com-20161024205605-z8lta0uvuhtxwzwl
Initi with beta15

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
// Copyright 2016 Canonical Ltd.
 
2
// Licensed under the LGPLv3, see LICENCE file for details.
 
3
 
 
4
package gomaasapi
 
5
 
 
6
import (
 
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
 
 
24
var (
 
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.
 
42
type 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.
 
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.
 
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
 
 
93
type controller struct {
 
94
        client       *Client
 
95
        apiVersion   version.Number
 
96
        capabilities set.Strings
 
97
}
 
98
 
 
99
// Capabilities implements Controller.
 
100
func (c *controller) Capabilities() set.Strings {
 
101
        return c.capabilities
 
102
}
 
103
 
 
104
// BootResources implements Controller.
 
105
func (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.
 
122
func (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.
 
139
func (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.
 
156
func (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.
 
174
type 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.
 
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)
 
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.
 
209
type CreateDeviceArgs struct {
 
210
        Hostname     string
 
211
        MACAddresses []string
 
212
        Domain       string
 
213
        Parent       string
 
214
}
 
215
 
 
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")
 
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.
 
248
type MachinesArgs struct {
 
249
        Hostnames    []string
 
250
        MACAddresses []string
 
251
        SystemIDs    []string
 
252
        Domain       string
 
253
        Zone         string
 
254
        AgentName    string
 
255
}
 
256
 
 
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)
 
267
        if err != nil {
 
268
                return nil, NewUnexpectedError(err)
 
269
        }
 
270
        machines, err := readMachines(c.apiVersion, source)
 
271
        if err != nil {
 
272
                return nil, errors.Trace(err)
 
273
        }
 
274
        var result []Machine
 
275
        for _, m := range machines {
 
276
                m.controller = c
 
277
                result = append(result, m)
 
278
        }
 
279
        return result, nil
 
280
}
 
281
 
 
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.
 
287
        Label string
 
288
        // Size is required and refers to the required minimum size in GB.
 
289
        Size int
 
290
        // Zero or more tags assocated to with the disks.
 
291
        Tags []string
 
292
}
 
293
 
 
294
// Validate ensures that there is a positive size and that there are no Empty
 
295
// tag values.
 
296
func (s *StorageSpec) Validate() error {
 
297
        if s.Size <= 0 {
 
298
                return errors.NotValidf("Size value %d", s.Size)
 
299
        }
 
300
        for _, v := range s.Tags {
 
301
                if v == "" {
 
302
                        return errors.NotValidf("empty tag")
 
303
                }
 
304
        }
 
305
        return nil
 
306
}
 
307
 
 
308
// String returns the string representation of the storage spec.
 
309
func (s *StorageSpec) String() string {
 
310
        label := s.Label
 
311
        if label != "" {
 
312
                label += ":"
 
313
        }
 
314
        tags := strings.Join(s.Tags, ",")
 
315
        if tags != "" {
 
316
                tags = "(" + tags + ")"
 
317
        }
 
318
        return fmt.Sprintf("%s%d%s", label, s.Size, tags)
 
319
}
 
320
 
 
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
 
326
        // AllocateMachine.
 
327
        Label string
 
328
        Space string
 
329
 
 
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',
 
334
        //     'vid', 'not_vid',
 
335
        //     'fabric', 'not_fabric',
 
336
        //     'subnet', 'not_subnet',
 
337
        //     'mode'
 
338
}
 
339
 
 
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 {
 
343
        if a.Label == "" {
 
344
                return errors.NotValidf("missing Label")
 
345
        }
 
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.
 
348
        if a.Space == "" {
 
349
                return errors.NotValidf("empty Space constraint")
 
350
        }
 
351
        return nil
 
352
}
 
353
 
 
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)
 
357
}
 
358
 
 
359
// AllocateMachineArgs is an argument struct for passing args into Machine.Allocate.
 
360
type AllocateMachineArgs struct {
 
361
        Hostname     string
 
362
        Architecture string
 
363
        MinCPUCount  int
 
364
        // MinMemory represented in MB.
 
365
        MinMemory int
 
366
        Tags      []string
 
367
        NotTags   []string
 
368
        Zone      string
 
369
        NotInZone []string
 
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.
 
378
        NotSpace  []string
 
379
        AgentName string
 
380
        Comment   string
 
381
        DryRun    bool
 
382
}
 
383
 
 
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")
 
391
                }
 
392
                if spec.Label != "" {
 
393
                        if storageLabels.Contains(spec.Label) {
 
394
                                return errors.NotValidf("reusing storage label %q", spec.Label)
 
395
                        }
 
396
                        storageLabels.Add(spec.Label)
 
397
                }
 
398
        }
 
399
        interfaceLabels := set.NewStrings()
 
400
        for _, spec := range a.Interfaces {
 
401
                if err := spec.Validate(); err != nil {
 
402
                        return errors.Annotate(err, "Interfaces")
 
403
                }
 
404
                if interfaceLabels.Contains(spec.Label) {
 
405
                        return errors.NotValidf("reusing interface label %q", spec.Label)
 
406
                }
 
407
                interfaceLabels.Add(spec.Label)
 
408
        }
 
409
        for _, v := range a.NotSpace {
 
410
                if v == "" {
 
411
                        return errors.NotValidf("empty NotSpace constraint")
 
412
                }
 
413
        }
 
414
        return nil
 
415
}
 
416
 
 
417
func (a *AllocateMachineArgs) storage() string {
 
418
        var values []string
 
419
        for _, spec := range a.Storage {
 
420
                values = append(values, spec.String())
 
421
        }
 
422
        return strings.Join(values, ",")
 
423
}
 
424
 
 
425
func (a *AllocateMachineArgs) interfaces() string {
 
426
        var values []string
 
427
        for _, spec := range a.Interfaces {
 
428
                values = append(values, spec.String())
 
429
        }
 
430
        return strings.Join(values, ";")
 
431
}
 
432
 
 
433
func (a *AllocateMachineArgs) notNetworks() string {
 
434
        var values []string
 
435
        for _, v := range a.NotSpace {
 
436
                values = append(values, "space:"+v)
 
437
        }
 
438
        return strings.Join(values, ",")
 
439
}
 
440
 
 
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
 
448
 
 
449
        // Storage is a mapping of the constraint label specified to the BlockDevices
 
450
        // that match that constraint.
 
451
        Storage map[string][]BlockDevice
 
452
}
 
453
 
 
454
// AllocateMachine implements Controller.
 
455
//
 
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)
 
476
        if err != nil {
 
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))
 
481
                        }
 
482
                }
 
483
                // Translate http errors.
 
484
                return nil, matches, NewUnexpectedError(err)
 
485
        }
 
486
 
 
487
        machine, err := readMachine(c.apiVersion, result)
 
488
        if err != nil {
 
489
                return nil, matches, errors.Trace(err)
 
490
        }
 
491
        machine.controller = c
 
492
 
 
493
        // Parse the constraint matches.
 
494
        matches, err = parseAllocateConstraintsResponse(result, machine)
 
495
        if err != nil {
 
496
                return nil, matches, errors.Trace(err)
 
497
        }
 
498
 
 
499
        return machine, matches, nil
 
500
}
 
501
 
 
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 {
 
505
        SystemIDs []string
 
506
        Comment   string
 
507
}
 
508
 
 
509
// ReleaseMachines implements Controller.
 
510
//
 
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)
 
520
        if err != nil {
 
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))
 
529
                        }
 
530
                }
 
531
                return NewUnexpectedError(err)
 
532
        }
 
533
 
 
534
        return nil
 
535
}
 
536
 
 
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)
 
542
        if err != nil {
 
543
                return nil, NewUnexpectedError(err)
 
544
        }
 
545
        files, err := readFiles(c.apiVersion, source)
 
546
        if err != nil {
 
547
                return nil, errors.Trace(err)
 
548
        }
 
549
        var result []File
 
550
        for _, f := range files {
 
551
                f.controller = c
 
552
                result = append(result, f)
 
553
        }
 
554
        return result, nil
 
555
}
 
556
 
 
557
// GetFile implements Controller.
 
558
func (c *controller) GetFile(filename string) (File, error) {
 
559
        if filename == "" {
 
560
                return nil, errors.NotValidf("missing filename")
 
561
        }
 
562
        source, err := c.get("files/" + filename)
 
563
        if err != nil {
 
564
                if svrErr, ok := errors.Cause(err).(ServerError); ok {
 
565
                        if svrErr.StatusCode == http.StatusNotFound {
 
566
                                return nil, errors.Wrap(err, NewNoMatchError(svrErr.BodyMessage))
 
567
                        }
 
568
                }
 
569
                return nil, NewUnexpectedError(err)
 
570
        }
 
571
        file, err := readFile(c.apiVersion, source)
 
572
        if err != nil {
 
573
                return nil, errors.Trace(err)
 
574
        }
 
575
        file.controller = c
 
576
        return file, nil
 
577
}
 
578
 
 
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 {
 
582
        Filename string
 
583
        Content  []byte
 
584
        Reader   io.Reader
 
585
        Length   int64
 
586
}
 
587
 
 
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)
 
592
        if dir != "" {
 
593
                return errors.NotValidf("paths in Filename %q", a.Filename)
 
594
        }
 
595
        if a.Filename == "" {
 
596
                return errors.NotValidf("missing Filename")
 
597
        }
 
598
        if a.Content == nil {
 
599
                if a.Reader == nil {
 
600
                        return errors.NotValidf("missing Content or Reader")
 
601
                }
 
602
                if a.Length == 0 {
 
603
                        return errors.NotValidf("missing Length")
 
604
                }
 
605
        } else {
 
606
                if a.Reader != nil {
 
607
                        return errors.NotValidf("specifying Content and Reader")
 
608
                }
 
609
                if a.Length != 0 {
 
610
                        return errors.NotValidf("specifying Length and Content")
 
611
                }
 
612
        }
 
613
        return nil
 
614
}
 
615
 
 
616
// AddFile implements Controller.
 
617
func (c *controller) AddFile(args AddFileArgs) error {
 
618
        if err := args.Validate(); err != nil {
 
619
                return errors.Trace(err)
 
620
        }
 
621
        fileContent := args.Content
 
622
        if fileContent == nil {
 
623
                content, err := ioutil.ReadAll(io.LimitReader(args.Reader, args.Length))
 
624
                if err != nil {
 
625
                        return errors.Annotatef(err, "cannot read file content")
 
626
                }
 
627
                fileContent = content
 
628
        }
 
629
        params := url.Values{"filename": {args.Filename}}
 
630
        _, err := c.postFile("files", "", params, fileContent)
 
631
        if err != nil {
 
632
                if svrErr, ok := errors.Cause(err).(ServerError); ok {
 
633
                        if svrErr.StatusCode == http.StatusBadRequest {
 
634
                                return errors.Wrap(err, NewBadRequestError(svrErr.BodyMessage))
 
635
                        }
 
636
                }
 
637
                return NewUnexpectedError(err)
 
638
        }
 
639
        return nil
 
640
}
 
641
 
 
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))
 
647
                        }
 
648
                }
 
649
                return NewUnexpectedError(err)
 
650
        }
 
651
        return nil
 
652
}
 
653
 
 
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)
 
659
        if err != nil {
 
660
                logger.Tracef("response %x: error: %q", requestID, err.Error())
 
661
                logger.Tracef("error detail: %#v", err)
 
662
                return nil, errors.Trace(err)
 
663
        }
 
664
        logger.Tracef("response %x: %s", requestID, string(bytes))
 
665
 
 
666
        var parsed interface{}
 
667
        err = json.Unmarshal(bytes, &parsed)
 
668
        if err != nil {
 
669
                return nil, errors.Trace(err)
 
670
        }
 
671
        return parsed, nil
 
672
}
 
673
 
 
674
func (c *controller) post(path, op string, params url.Values) (interface{}, error) {
 
675
        bytes, err := c._postRaw(path, op, params, nil)
 
676
        if err != nil {
 
677
                return nil, errors.Trace(err)
 
678
        }
 
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
 
 
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)
 
692
}
 
693
 
 
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() {
 
698
                opArg := ""
 
699
                if op != "" {
 
700
                        opArg = "?op=" + op
 
701
                }
 
702
                logger.Tracef("request %x: POST %s%s%s, params=%s", requestID, c.client.APIURL, path, opArg, params.Encode())
 
703
        }
 
704
        bytes, err := c.client.Post(&url.URL{Path: path}, op, params, files)
 
705
        if err != nil {
 
706
                logger.Tracef("response %x: error: %q", requestID, err.Error())
 
707
                logger.Tracef("error detail: %#v", err)
 
708
                return nil, errors.Trace(err)
 
709
        }
 
710
        logger.Tracef("response %x: %s", requestID, string(bytes))
 
711
        return bytes, nil
 
712
}
 
713
 
 
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})
 
719
        if err != nil {
 
720
                logger.Tracef("response %x: error: %q", requestID, err.Error())
 
721
                logger.Tracef("error detail: %#v", err)
 
722
                return errors.Trace(err)
 
723
        }
 
724
        logger.Tracef("response %x: complete", requestID)
 
725
        return nil
 
726
}
 
727
 
 
728
func (c *controller) getQuery(path string, params url.Values) (interface{}, error) {
 
729
        return c._get(path, "", params)
 
730
}
 
731
 
 
732
func (c *controller) get(path string) (interface{}, error) {
 
733
        return c._get(path, "", nil)
 
734
}
 
735
 
 
736
func (c *controller) getOp(path, op string) (interface{}, error) {
 
737
        return c._get(path, op, nil)
 
738
}
 
739
 
 
740
func (c *controller) _get(path, op string, params url.Values) (interface{}, error) {
 
741
        bytes, err := c._getRaw(path, op, params)
 
742
        if err != nil {
 
743
                return nil, errors.Trace(err)
 
744
        }
 
745
        var parsed interface{}
 
746
        err = json.Unmarshal(bytes, &parsed)
 
747
        if err != nil {
 
748
                return nil, errors.Trace(err)
 
749
        }
 
750
        return parsed, nil
 
751
}
 
752
 
 
753
func (c *controller) _getRaw(path, op string, params url.Values) ([]byte, error) {
 
754
        path = EnsureTrailingSlash(path)
 
755
        requestID := nextRequestID()
 
756
        if logger.IsTraceEnabled() {
 
757
                var query string
 
758
                if params != nil {
 
759
                        query = "?" + params.Encode()
 
760
                }
 
761
                logger.Tracef("request %x: GET %s%s%s", requestID, c.client.APIURL, path, query)
 
762
        }
 
763
        bytes, err := c.client.Get(&url.URL{Path: path}, op, params)
 
764
        if err != nil {
 
765
                logger.Tracef("response %x: error: %q", requestID, err.Error())
 
766
                logger.Tracef("error detail: %#v", err)
 
767
                return nil, errors.Trace(err)
 
768
        }
 
769
        logger.Tracef("response %x: %s", requestID, string(bytes))
 
770
        return bytes, nil
 
771
}
 
772
 
 
773
func nextRequestID() int64 {
 
774
        return atomic.AddInt64(&requestNumber, 1)
 
775
}
 
776
 
 
777
func (c *controller) readAPIVersion(apiVersion version.Number) (set.Strings, version.Number, error) {
 
778
        parsed, err := c.get("version")
 
779
        if err != nil {
 
780
                return nil, apiVersion, errors.Trace(err)
 
781
        }
 
782
 
 
783
        // As we care about other fields, add them.
 
784
        fields := schema.Fields{
 
785
                "capabilities": schema.List(schema.String()),
 
786
        }
 
787
        checker := schema.FieldMap(fields, nil) // no defaults
 
788
        coerced, err := checker.Coerce(parsed, nil)
 
789
        if err != nil {
 
790
                return nil, apiVersion, WrapWithDeserializationError(err, "version response")
 
791
        }
 
792
        // For now, we don't append any subversion, but as it becomes used, we
 
793
        // should parse and check.
 
794
 
 
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))
 
802
        }
 
803
 
 
804
        return capabilities, apiVersion, nil
 
805
}
 
806
 
 
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())),
 
812
        }
 
813
        matchDefaults := schema.Defaults{
 
814
                "storage":    schema.Omit,
 
815
                "interfaces": schema.Omit,
 
816
        }
 
817
        fields := schema.Fields{
 
818
                "constraints_by_type": schema.FieldMap(matchFields, matchDefaults),
 
819
        }
 
820
        checker := schema.FieldMap(fields, nil) // no defaults
 
821
        coerced, err := checker.Coerce(source, nil)
 
822
        if err != nil {
 
823
                return empty, WrapWithDeserializationError(err, "allocation constraints response schema check failed")
 
824
        }
 
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),
 
830
        }
 
831
 
 
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)
 
838
                                if iface == nil {
 
839
                                        return empty, NewDeserializationError("constraint match interface %q: %d does not match an interface for the machine", label, id)
 
840
                                }
 
841
                                interfaces[index] = iface
 
842
                        }
 
843
                        result.Interfaces[label] = interfaces
 
844
                }
 
845
        }
 
846
 
 
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)
 
855
                                }
 
856
                                blockDevices[index] = blockDevice
 
857
                        }
 
858
                        result.Storage[label] = blockDevices
 
859
                }
 
860
        }
 
861
        return result, nil
 
862
}
 
863
 
 
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)
 
873
                }
 
874
        }
 
875
        return result
 
876
}