blob: 8518d9487fa77ea8b2c461973fa57318443f771a [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 "fmt"
8 "net/http"
9 "net/url"
10
11 "github.com/juju/errors"
12 "github.com/juju/schema"
13 "github.com/juju/version"
14)
15
16type machine struct {
17 controller *controller
18
19 resourceURI string
20
21 systemID string
22 hostname string
23 fqdn string
24 tags []string
25 ownerData map[string]string
26
27 operatingSystem string
28 distroSeries string
29 architecture string
30 memory int
31 cpuCount int
32
33 ipAddresses []string
34 powerState string
35
36 // NOTE: consider some form of status struct
37 statusName string
38 statusMessage string
39
40 bootInterface *interface_
41 interfaceSet []*interface_
42 zone *zone
43 // Don't really know the difference between these two lists:
44 physicalBlockDevices []*blockdevice
45 blockDevices []*blockdevice
46}
47
48func (m *machine) updateFrom(other *machine) {
49 m.resourceURI = other.resourceURI
50 m.systemID = other.systemID
51 m.hostname = other.hostname
52 m.fqdn = other.fqdn
53 m.operatingSystem = other.operatingSystem
54 m.distroSeries = other.distroSeries
55 m.architecture = other.architecture
56 m.memory = other.memory
57 m.cpuCount = other.cpuCount
58 m.ipAddresses = other.ipAddresses
59 m.powerState = other.powerState
60 m.statusName = other.statusName
61 m.statusMessage = other.statusMessage
62 m.zone = other.zone
63 m.tags = other.tags
64 m.ownerData = other.ownerData
65}
66
67// SystemID implements Machine.
68func (m *machine) SystemID() string {
69 return m.systemID
70}
71
72// Hostname implements Machine.
73func (m *machine) Hostname() string {
74 return m.hostname
75}
76
77// FQDN implements Machine.
78func (m *machine) FQDN() string {
79 return m.fqdn
80}
81
82// Tags implements Machine.
83func (m *machine) Tags() []string {
84 return m.tags
85}
86
87// IPAddresses implements Machine.
88func (m *machine) IPAddresses() []string {
89 return m.ipAddresses
90}
91
92// Memory implements Machine.
93func (m *machine) Memory() int {
94 return m.memory
95}
96
97// CPUCount implements Machine.
98func (m *machine) CPUCount() int {
99 return m.cpuCount
100}
101
102// PowerState implements Machine.
103func (m *machine) PowerState() string {
104 return m.powerState
105}
106
107// Zone implements Machine.
108func (m *machine) Zone() Zone {
109 if m.zone == nil {
110 return nil
111 }
112 return m.zone
113}
114
115// BootInterface implements Machine.
116func (m *machine) BootInterface() Interface {
117 if m.bootInterface == nil {
118 return nil
119 }
120 m.bootInterface.controller = m.controller
121 return m.bootInterface
122}
123
124// InterfaceSet implements Machine.
125func (m *machine) InterfaceSet() []Interface {
126 result := make([]Interface, len(m.interfaceSet))
127 for i, v := range m.interfaceSet {
128 v.controller = m.controller
129 result[i] = v
130 }
131 return result
132}
133
134// Interface implements Machine.
135func (m *machine) Interface(id int) Interface {
136 for _, iface := range m.interfaceSet {
137 if iface.ID() == id {
138 iface.controller = m.controller
139 return iface
140 }
141 }
142 return nil
143}
144
145// OperatingSystem implements Machine.
146func (m *machine) OperatingSystem() string {
147 return m.operatingSystem
148}
149
150// DistroSeries implements Machine.
151func (m *machine) DistroSeries() string {
152 return m.distroSeries
153}
154
155// Architecture implements Machine.
156func (m *machine) Architecture() string {
157 return m.architecture
158}
159
160// StatusName implements Machine.
161func (m *machine) StatusName() string {
162 return m.statusName
163}
164
165// StatusMessage implements Machine.
166func (m *machine) StatusMessage() string {
167 return m.statusMessage
168}
169
170// PhysicalBlockDevices implements Machine.
171func (m *machine) PhysicalBlockDevices() []BlockDevice {
172 result := make([]BlockDevice, len(m.physicalBlockDevices))
173 for i, v := range m.physicalBlockDevices {
174 result[i] = v
175 }
176 return result
177}
178
179// PhysicalBlockDevice implements Machine.
180func (m *machine) PhysicalBlockDevice(id int) BlockDevice {
181 for _, blockDevice := range m.physicalBlockDevices {
182 if blockDevice.ID() == id {
183 return blockDevice
184 }
185 }
186 return nil
187}
188
189// BlockDevices implements Machine.
190func (m *machine) BlockDevices() []BlockDevice {
191 result := make([]BlockDevice, len(m.blockDevices))
192 for i, v := range m.blockDevices {
193 result[i] = v
194 }
195 return result
196}
197
198// Devices implements Machine.
199func (m *machine) Devices(args DevicesArgs) ([]Device, error) {
200 // Perhaps in the future, MAAS will give us a way to query just for the
201 // devices for a particular parent.
202 devices, err := m.controller.Devices(args)
203 if err != nil {
204 return nil, errors.Trace(err)
205 }
206 var result []Device
207 for _, device := range devices {
208 if device.Parent() == m.SystemID() {
209 result = append(result, device)
210 }
211 }
212 return result, nil
213}
214
215// StartArgs is an argument struct for passing parameters to the Machine.Start
216// method.
217type StartArgs struct {
218 // UserData needs to be Base64 encoded user data for cloud-init.
219 UserData string
220 DistroSeries string
221 Kernel string
222 Comment string
223}
224
225// Start implements Machine.
226func (m *machine) Start(args StartArgs) error {
227 params := NewURLParams()
228 params.MaybeAdd("user_data", args.UserData)
229 params.MaybeAdd("distro_series", args.DistroSeries)
230 params.MaybeAdd("hwe_kernel", args.Kernel)
231 params.MaybeAdd("comment", args.Comment)
232 result, err := m.controller.post(m.resourceURI, "deploy", params.Values)
233 if err != nil {
234 if svrErr, ok := errors.Cause(err).(ServerError); ok {
235 switch svrErr.StatusCode {
236 case http.StatusNotFound, http.StatusConflict:
237 return errors.Wrap(err, NewBadRequestError(svrErr.BodyMessage))
238 case http.StatusForbidden:
239 return errors.Wrap(err, NewPermissionError(svrErr.BodyMessage))
240 case http.StatusServiceUnavailable:
241 return errors.Wrap(err, NewCannotCompleteError(svrErr.BodyMessage))
242 }
243 }
244 return NewUnexpectedError(err)
245 }
246
247 machine, err := readMachine(m.controller.apiVersion, result)
248 if err != nil {
249 return errors.Trace(err)
250 }
251 m.updateFrom(machine)
252 return nil
253}
254
255// CreateMachineDeviceArgs is an argument structure for Machine.CreateDevice.
256// Only InterfaceName and MACAddress fields are required, the others are only
257// used if set. If Subnet and VLAN are both set, Subnet.VLAN() must match the
258// given VLAN. On failure, returns an error satisfying errors.IsNotValid().
259type CreateMachineDeviceArgs struct {
260 Hostname string
261 InterfaceName string
262 MACAddress string
263 Subnet Subnet
264 VLAN VLAN
265}
266
267// Validate ensures that all required values are non-emtpy.
268func (a *CreateMachineDeviceArgs) Validate() error {
269 if a.InterfaceName == "" {
270 return errors.NotValidf("missing InterfaceName")
271 }
272
273 if a.MACAddress == "" {
274 return errors.NotValidf("missing MACAddress")
275 }
276
277 if a.Subnet != nil && a.VLAN != nil && a.Subnet.VLAN() != a.VLAN {
278 msg := fmt.Sprintf(
279 "given subnet %q on VLAN %d does not match given VLAN %d",
280 a.Subnet.CIDR(), a.Subnet.VLAN().ID(), a.VLAN.ID(),
281 )
282 return errors.NewNotValid(nil, msg)
283 }
284
285 return nil
286}
287
288// CreateDevice implements Machine
289func (m *machine) CreateDevice(args CreateMachineDeviceArgs) (_ Device, err error) {
290 if err := args.Validate(); err != nil {
291 return nil, errors.Trace(err)
292 }
293 device, err := m.controller.CreateDevice(CreateDeviceArgs{
294 Hostname: args.Hostname,
295 MACAddresses: []string{args.MACAddress},
296 Parent: m.SystemID(),
297 })
298 if err != nil {
299 return nil, errors.Trace(err)
300 }
301
302 defer func(err *error) {
303 // If there is an error return, at least try to delete the device we just created.
304 if *err != nil {
305 if innerErr := device.Delete(); innerErr != nil {
306 logger.Warningf("could not delete device %q", device.SystemID())
307 }
308 }
309 }(&err)
310
311 // Update the VLAN to use for the interface, if given.
312 vlanToUse := args.VLAN
313 if vlanToUse == nil && args.Subnet != nil {
314 vlanToUse = args.Subnet.VLAN()
315 }
316
317 // There should be one interface created for each MAC Address, and since we
318 // only specified one, there should just be one response.
319 interfaces := device.InterfaceSet()
320 if count := len(interfaces); count != 1 {
321 err := errors.Errorf("unexpected interface count for device: %d", count)
322 return nil, NewUnexpectedError(err)
323 }
324 iface := interfaces[0]
325 nameToUse := args.InterfaceName
326
327 if err := m.updateDeviceInterface(iface, nameToUse, vlanToUse); err != nil {
328 return nil, errors.Trace(err)
329 }
330
331 if args.Subnet == nil {
332 // Nothing further to update.
333 return device, nil
334 }
335
336 if err := m.linkDeviceInterfaceToSubnet(iface, args.Subnet); err != nil {
337 return nil, errors.Trace(err)
338 }
339
340 return device, nil
341}
342
343func (m *machine) updateDeviceInterface(iface Interface, nameToUse string, vlanToUse VLAN) error {
344 updateArgs := UpdateInterfaceArgs{}
345 updateArgs.Name = nameToUse
346
347 if vlanToUse != nil {
348 updateArgs.VLAN = vlanToUse
349 }
350
351 if err := iface.Update(updateArgs); err != nil {
352 return errors.Annotatef(err, "updating device interface %q failed", iface.Name())
353 }
354
355 return nil
356}
357
358func (m *machine) linkDeviceInterfaceToSubnet(iface Interface, subnetToUse Subnet) error {
359 err := iface.LinkSubnet(LinkSubnetArgs{
360 Mode: LinkModeStatic,
361 Subnet: subnetToUse,
362 })
363 if err != nil {
364 return errors.Annotatef(
365 err, "linking device interface %q to subnet %q failed",
366 iface.Name(), subnetToUse.CIDR())
367 }
368
369 return nil
370}
371
372// OwnerData implements OwnerDataHolder.
373func (m *machine) OwnerData() map[string]string {
374 result := make(map[string]string)
375 for key, value := range m.ownerData {
376 result[key] = value
377 }
378 return result
379}
380
381// SetOwnerData implements OwnerDataHolder.
382func (m *machine) SetOwnerData(ownerData map[string]string) error {
383 params := make(url.Values)
384 for key, value := range ownerData {
385 params.Add(key, value)
386 }
387 result, err := m.controller.post(m.resourceURI, "set_owner_data", params)
388 if err != nil {
389 return errors.Trace(err)
390 }
391 machine, err := readMachine(m.controller.apiVersion, result)
392 if err != nil {
393 return errors.Trace(err)
394 }
395 m.updateFrom(machine)
396 return nil
397}
398
399func readMachine(controllerVersion version.Number, source interface{}) (*machine, error) {
400 readFunc, err := getMachineDeserializationFunc(controllerVersion)
401 if err != nil {
402 return nil, errors.Trace(err)
403 }
404
405 checker := schema.StringMap(schema.Any())
406 coerced, err := checker.Coerce(source, nil)
407 if err != nil {
408 return nil, WrapWithDeserializationError(err, "machine base schema check failed")
409 }
410 valid := coerced.(map[string]interface{})
411 return readFunc(valid)
412}
413
414func readMachines(controllerVersion version.Number, source interface{}) ([]*machine, error) {
415 readFunc, err := getMachineDeserializationFunc(controllerVersion)
416 if err != nil {
417 return nil, errors.Trace(err)
418 }
419
420 checker := schema.List(schema.StringMap(schema.Any()))
421 coerced, err := checker.Coerce(source, nil)
422 if err != nil {
423 return nil, WrapWithDeserializationError(err, "machine base schema check failed")
424 }
425 valid := coerced.([]interface{})
426 return readMachineList(valid, readFunc)
427}
428
429func getMachineDeserializationFunc(controllerVersion version.Number) (machineDeserializationFunc, error) {
430 var deserialisationVersion version.Number
431 for v := range machineDeserializationFuncs {
432 if v.Compare(deserialisationVersion) > 0 && v.Compare(controllerVersion) <= 0 {
433 deserialisationVersion = v
434 }
435 }
436 if deserialisationVersion == version.Zero {
437 return nil, NewUnsupportedVersionError("no machine read func for version %s", controllerVersion)
438 }
439 return machineDeserializationFuncs[deserialisationVersion], nil
440}
441
442func readMachineList(sourceList []interface{}, readFunc machineDeserializationFunc) ([]*machine, error) {
443 result := make([]*machine, 0, len(sourceList))
444 for i, value := range sourceList {
445 source, ok := value.(map[string]interface{})
446 if !ok {
447 return nil, NewDeserializationError("unexpected value for machine %d, %T", i, value)
448 }
449 machine, err := readFunc(source)
450 if err != nil {
451 return nil, errors.Annotatef(err, "machine %d", i)
452 }
453 result = append(result, machine)
454 }
455 return result, nil
456}
457
458type machineDeserializationFunc func(map[string]interface{}) (*machine, error)
459
460var machineDeserializationFuncs = map[version.Number]machineDeserializationFunc{
461 twoDotOh: machine_2_0,
462}
463
464func machine_2_0(source map[string]interface{}) (*machine, error) {
465 fields := schema.Fields{
466 "resource_uri": schema.String(),
467
468 "system_id": schema.String(),
469 "hostname": schema.String(),
470 "fqdn": schema.String(),
471 "tag_names": schema.List(schema.String()),
472 "owner_data": schema.StringMap(schema.String()),
473
474 "osystem": schema.String(),
475 "distro_series": schema.String(),
476 "architecture": schema.OneOf(schema.Nil(""), schema.String()),
477 "memory": schema.ForceInt(),
478 "cpu_count": schema.ForceInt(),
479
480 "ip_addresses": schema.List(schema.String()),
481 "power_state": schema.String(),
482 "status_name": schema.String(),
483 "status_message": schema.OneOf(schema.Nil(""), schema.String()),
484
485 "boot_interface": schema.OneOf(schema.Nil(""), schema.StringMap(schema.Any())),
486 "interface_set": schema.List(schema.StringMap(schema.Any())),
487 "zone": schema.StringMap(schema.Any()),
488
489 "physicalblockdevice_set": schema.List(schema.StringMap(schema.Any())),
490 "blockdevice_set": schema.List(schema.StringMap(schema.Any())),
491 }
492 defaults := schema.Defaults{
493 "architecture": "",
494 }
495 checker := schema.FieldMap(fields, defaults)
496 coerced, err := checker.Coerce(source, nil)
497 if err != nil {
498 return nil, WrapWithDeserializationError(err, "machine 2.0 schema check failed")
499 }
500 valid := coerced.(map[string]interface{})
501 // From here we know that the map returned from the schema coercion
502 // contains fields of the right type.
503
504 var bootInterface *interface_
505 if ifaceMap, ok := valid["boot_interface"].(map[string]interface{}); ok {
506 bootInterface, err = interface_2_0(ifaceMap)
507 if err != nil {
508 return nil, errors.Trace(err)
509 }
510 }
511
512 interfaceSet, err := readInterfaceList(valid["interface_set"].([]interface{}), interface_2_0)
513 if err != nil {
514 return nil, errors.Trace(err)
515 }
516 zone, err := zone_2_0(valid["zone"].(map[string]interface{}))
517 if err != nil {
518 return nil, errors.Trace(err)
519 }
520 physicalBlockDevices, err := readBlockDeviceList(valid["physicalblockdevice_set"].([]interface{}), blockdevice_2_0)
521 if err != nil {
522 return nil, errors.Trace(err)
523 }
524 blockDevices, err := readBlockDeviceList(valid["blockdevice_set"].([]interface{}), blockdevice_2_0)
525 if err != nil {
526 return nil, errors.Trace(err)
527 }
528 architecture, _ := valid["architecture"].(string)
529 statusMessage, _ := valid["status_message"].(string)
530 result := &machine{
531 resourceURI: valid["resource_uri"].(string),
532
533 systemID: valid["system_id"].(string),
534 hostname: valid["hostname"].(string),
535 fqdn: valid["fqdn"].(string),
536 tags: convertToStringSlice(valid["tag_names"]),
537 ownerData: convertToStringMap(valid["owner_data"]),
538
539 operatingSystem: valid["osystem"].(string),
540 distroSeries: valid["distro_series"].(string),
541 architecture: architecture,
542 memory: valid["memory"].(int),
543 cpuCount: valid["cpu_count"].(int),
544
545 ipAddresses: convertToStringSlice(valid["ip_addresses"]),
546 powerState: valid["power_state"].(string),
547 statusName: valid["status_name"].(string),
548 statusMessage: statusMessage,
549
550 bootInterface: bootInterface,
551 interfaceSet: interfaceSet,
552 zone: zone,
553 physicalBlockDevices: physicalBlockDevices,
554 blockDevices: blockDevices,
555 }
556
557 return result, nil
558}
559
560func convertToStringSlice(field interface{}) []string {
561 if field == nil {
562 return nil
563 }
564 fieldSlice := field.([]interface{})
565 result := make([]string, len(fieldSlice))
566 for i, value := range fieldSlice {
567 result[i] = value.(string)
568 }
569 return result
570}
571
572func convertToStringMap(field interface{}) map[string]string {
573 if field == nil {
574 return nil
575 }
576 // This function is only called after a schema Coerce, so it's
577 // safe.
578 fieldMap := field.(map[string]interface{})
579 result := make(map[string]string)
580 for key, value := range fieldMap {
581 result[key] = value.(string)
582 }
583 return result
584}