blob: 5890b31c1fb362576cb9c4cc4a8714ae00a5f6be [file] [log] [blame]
David K. Bainbridgeb5415042016-05-13 17:06:10 -07001package main
2
3import (
David K. Bainbridgeefa951d2016-05-26 10:54:25 -07004 "bytes"
5 "encoding/json"
David K. Bainbridgeb5415042016-05-13 17:06:10 -07006 "fmt"
7 "log"
David K. Bainbridgeefa951d2016-05-26 10:54:25 -07008 "net/http"
David K. Bainbridgeb5415042016-05-13 17:06:10 -07009 "net/url"
David K. Bainbridgeefa951d2016-05-26 10:54:25 -070010 "os/exec"
David K. Bainbridgeb5415042016-05-13 17:06:10 -070011 "regexp"
12 "strconv"
13 "strings"
David K. Bainbridgeefa951d2016-05-26 10:54:25 -070014 "time"
David K. Bainbridgeb5415042016-05-13 17:06:10 -070015
16 maas "github.com/juju/gomaasapi"
17)
18
19// Action how to get from there to here
20type Action func(*maas.MAASObject, MaasNode, ProcessingOptions) error
21
22// Transition the map from where i want to be from where i might be
23type Transition struct {
24 Target string
25 Current string
26 Using Action
27}
28
David K. Bainbridge6ea57c12016-06-06 23:29:12 -070029type Power struct {
30 Name string `json:"name"`
31 MacAddress string `json:"mac_address"`
32 PowerPassword string `json:"power_password"`
33 PowerAddress string `json:"power_address"`
34}
35
David K. Bainbridgeb5415042016-05-13 17:06:10 -070036// ProcessingOptions used to determine on what hosts to operate
37type ProcessingOptions struct {
38 Filter struct {
39 Zones struct {
40 Include []string
41 Exclude []string
42 }
43 Hosts struct {
44 Include []string
45 Exclude []string
46 }
47 }
David K. Bainbridge6ea57c12016-06-06 23:29:12 -070048 Mappings map[string]interface{}
49 Verbose bool
50 Preview bool
51 AlwaysRename bool
52 ProvTracker Tracker
53 ProvisionURL string
54 ProvisionTTL time.Duration
55 PowerHelper string
56 PowerHelperUser string
57 PowerHelperHost string
David K. Bainbridgeb5415042016-05-13 17:06:10 -070058}
59
60// Transitions the actual map
61//
62// Currently this is a hand compiled / optimized "next step" table. This should
63// really be generated from the state machine chart input. Once this has been
64// accomplished you should be able to determine the action to take given your
65// target state and your current state.
David K. Bainbridgeefa951d2016-05-26 10:54:25 -070066var Transitions = map[string]map[string][]Action{
David K. Bainbridgeb5415042016-05-13 17:06:10 -070067 "Deployed": {
David K. Bainbridgeefa951d2016-05-26 10:54:25 -070068 "New": []Action{Reset, Commission},
69 "Deployed": []Action{Provision, Done},
70 "Ready": []Action{Reset, Aquire},
71 "Allocated": []Action{Reset, Deploy},
72 "Retired": []Action{Reset, AdminState},
73 "Reserved": []Action{Reset, AdminState},
74 "Releasing": []Action{Reset, Wait},
75 "DiskErasing": []Action{Reset, Wait},
76 "Deploying": []Action{Reset, Wait},
77 "Commissioning": []Action{Reset, Wait},
78 "Missing": []Action{Reset, Fail},
79 "FailedReleasing": []Action{Reset, Fail},
80 "FailedDiskErasing": []Action{Reset, Fail},
81 "FailedDeployment": []Action{Reset, Fail},
82 "Broken": []Action{Reset, Fail},
83 "FailedCommissioning": []Action{Reset, Fail},
David K. Bainbridgeb5415042016-05-13 17:06:10 -070084 },
85}
86
87const (
88 // defaultStateMachine Would be nice to drive from a graph language
89 defaultStateMachine string = `
David K. Bainbridged9b966f2016-05-31 13:30:05 -070090 (New)->(Commissioning)
David K. Bainbridgeb5415042016-05-13 17:06:10 -070091 (Commissioning)->(FailedCommissioning)
92 (FailedCommissioning)->(New)
93 (Commissioning)->(Ready)
94 (Ready)->(Deploying)
95 (Ready)->(Allocated)
96 (Allocated)->(Deploying)
97 (Deploying)->(Deployed)
98 (Deploying)->(FailedDeployment)
99 (FailedDeployment)->(Broken)
100 (Deployed)->(Releasing)
101 (Releasing)->(FailedReleasing)
102 (FailedReleasing)->(Broken)
103 (Releasing)->(DiskErasing)
104 (DiskErasing)->(FailedEraseDisk)
105 (FailedEraseDisk)->(Broken)
106 (Releasing)->(Ready)
107 (DiskErasing)->(Ready)
David K. Bainbridgeefa951d2016-05-26 10:54:25 -0700108 (Broken)->(Ready)
David K. Bainbridged9b966f2016-05-31 13:30:05 -0700109 (Deployed)->(Provisioning)
110 (Provisioning)->|a|
111 |a|->(Execute Script)->|b|
112 |a|->(HTTP PUT)
113 (HTTP PUT)->(HTTP GET)
114 (HTTP GET)->(HTTP GET)
115 (HTTP GET)->|b|
116 |b|->(Provisioned)
117 |b|->(ProvisionError)
118 (ProvisionError)->(Provisioning)`
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700119)
120
121// updateName - changes the name of the MAAS node based on the configuration file
122func updateNodeName(client *maas.MAASObject, node MaasNode, options ProcessingOptions) error {
123 macs := node.MACs()
124
125 // Get current node name and strip off domain name
126 current := node.Hostname()
127 if i := strings.IndexRune(current, '.'); i != -1 {
128 current = current[:i]
129 }
130 for _, mac := range macs {
131 if entry, ok := options.Mappings[mac]; ok {
132 if name, ok := entry.(map[string]interface{})["hostname"]; ok && current != name.(string) {
133 nodesObj := client.GetSubObject("nodes")
134 nodeObj := nodesObj.GetSubObject(node.ID())
135 log.Printf("RENAME '%s' to '%s'\n", node.Hostname(), name.(string))
136
137 if !options.Preview {
138 nodeObj.Update(url.Values{"hostname": []string{name.(string)}})
139 }
140 }
141 }
142 }
143 return nil
144}
145
David K. Bainbridgeefa951d2016-05-26 10:54:25 -0700146// Reset we are at the target state, nothing to do
147var Reset = func(client *maas.MAASObject, node MaasNode, options ProcessingOptions) error {
148 if options.Verbose {
149 log.Printf("RESET: %s", node.Hostname())
150 }
151
152 if options.AlwaysRename {
153 updateNodeName(client, node, options)
154 }
155
156 options.ProvTracker.Clear(node.ID())
157
158 return nil
159}
160
161// Provision we are at the target state, nothing to do
162var Provision = func(client *maas.MAASObject, node MaasNode, options ProcessingOptions) error {
163 if options.Verbose {
164 log.Printf("CHECK PROVISION: %s", node.Hostname())
165 }
166
167 if options.AlwaysRename {
168 updateNodeName(client, node, options)
169 }
170
171 record, err := options.ProvTracker.Get(node.ID())
David K. Bainbridgeefa951d2016-05-26 10:54:25 -0700172 if err != nil {
173 log.Printf("[warn] unable to retrieve provisioning state of node '%s' : %s", node.Hostname(), err)
174 } else if record.State == Unprovisioned || record.State == ProvisionError {
David K. Bainbridge3ee76412016-06-15 18:56:08 -0700175 if options.Verbose {
176 log.Printf("[info] Current state of node '%s' is '%s'", node.Hostname(), record.State.String())
177 }
David K. Bainbridgeefa951d2016-05-26 10:54:25 -0700178 var err error = nil
179 var callout *url.URL
180 log.Printf("PROVISION '%s'", node.Hostname())
181 if len(options.ProvisionURL) > 0 {
182 if options.Verbose {
183 log.Printf("[info] Provisioning callout to '%s'", options.ProvisionURL)
184 }
185 callout, err = url.Parse(options.ProvisionURL)
186 if err != nil {
187 log.Printf("[error] Failed to parse provisioning URL '%s' : %s", options.ProvisionURL, err)
188 } else {
189 ips := node.IPs()
190 ip := ""
191 if len(ips) > 0 {
192 ip = ips[0]
193 }
David K. Bainbridged9b966f2016-05-31 13:30:05 -0700194 macs := node.MACs()
195 mac := ""
196 if len(macs) > 0 {
197 mac = macs[0]
198 }
David K. Bainbridgeefa951d2016-05-26 10:54:25 -0700199 switch callout.Scheme {
200 // If the scheme is a file, then we will execute the refereced file
201 case "", "file":
202 if options.Verbose {
203 log.Printf("[info] executing local script file '%s'", callout.Path)
204 }
205 record.State = Provisioning
206 record.Timestamp = time.Now().Unix()
207 options.ProvTracker.Set(node.ID(), record)
David K. Bainbridged9b966f2016-05-31 13:30:05 -0700208 err = exec.Command(callout.Path, node.ID(), node.Hostname(), ip, mac).Run()
David K. Bainbridgeefa951d2016-05-26 10:54:25 -0700209 if err != nil {
210 log.Printf("[error] Failed to execute '%s' : %s", options.ProvisionURL, err)
211 } else {
212 if options.Verbose {
213 log.Printf("[info] Marking node '%s' with ID '%s' as provisioned",
214 node.Hostname(), node.ID())
215 }
216 record.State = Provisioned
217 options.ProvTracker.Set(node.ID(), record)
218 }
219
220 default:
221 if options.Verbose {
222 log.Printf("[info] POSTing to '%s'", options.ProvisionURL)
223 }
224 data := map[string]string{
225 "id": node.ID(),
226 "name": node.Hostname(),
227 "ip": ip,
David K. Bainbridged9b966f2016-05-31 13:30:05 -0700228 "mac": mac,
David K. Bainbridgeefa951d2016-05-26 10:54:25 -0700229 }
230 hc := http.Client{}
231 var b []byte
232 b, err = json.Marshal(data)
233 if err != nil {
234 log.Printf("[error] Unable to marshal node data : %s", err)
235 } else {
236 var req *http.Request
237 var resp *http.Response
238 if options.Verbose {
239 log.Printf("[debug] POSTing data '%s'", string(b))
240 }
241 req, err = http.NewRequest("POST", options.ProvisionURL, bytes.NewReader(b))
242 if err != nil {
243 log.Printf("[error] Unable to construct POST request to provisioner : %s",
244 err)
245 } else {
246 req.Header.Add("Content-Type", "application/json")
247 resp, err = hc.Do(req)
248 if err != nil {
249 log.Printf("[error] Unable to process POST request : %s",
250 err)
251 } else {
252 defer resp.Body.Close()
David K. Bainbridge8352c592016-06-02 12:48:37 -0700253 if resp.StatusCode == http.StatusAccepted {
David K. Bainbridgeefa951d2016-05-26 10:54:25 -0700254 record.State = Provisioning
255 } else {
256 record.State = ProvisionError
257 }
David K. Bainbridge8352c592016-06-02 12:48:37 -0700258 record.Timestamp = time.Now().Unix()
David K. Bainbridgeefa951d2016-05-26 10:54:25 -0700259 options.ProvTracker.Set(node.ID(), record)
260 }
261 }
262 }
263 }
264 }
265 }
266
267 if err != nil {
268 if options.Verbose {
269 log.Printf("[warn] Not marking node '%s' with ID '%s' as provisioned, because of error '%s'",
270 node.Hostname(), node.ID(), err)
271 record.State = ProvisionError
272 options.ProvTracker.Set(node.ID(), record)
273 }
274 }
275 } else if record.State == Provisioning && time.Since(time.Unix(record.Timestamp, 0)) > options.ProvisionTTL {
276 log.Printf("[error] Provisioning of node '%s' has passed provisioning TTL of '%v'",
277 node.Hostname(), options.ProvisionTTL)
278 record.State = ProvisionError
279 options.ProvTracker.Set(node.ID(), record)
280 } else if record.State == Provisioning {
281 callout, err := url.Parse(options.ProvisionURL)
282 if err != nil {
283 log.Printf("[error] Unable to parse provisioning URL '%s' : %s", options.ProvisionURL, err)
284 } else if callout.Scheme != "file" {
285 var req *http.Request
286 var resp *http.Response
287 if options.Verbose {
288 log.Printf("[info] Fetching provisioning state for node '%s'", node.Hostname())
289 }
290 req, err = http.NewRequest("GET", options.ProvisionURL+"/"+node.ID(), nil)
291 if err != nil {
292 log.Printf("[error] Unable to construct GET request to provisioner : %s", err)
293 } else {
294 hc := http.Client{}
295 resp, err = hc.Do(req)
296 if err != nil {
297 log.Printf("[error] Failed to quest provision state for node '%s' : %s",
298 node.Hostname(), err)
299 } else {
David K. Bainbridge2f456b82016-06-14 22:32:51 -0700300 defer resp.Body.Close()
David K. Bainbridge3ee76412016-06-15 18:56:08 -0700301 if options.Verbose {
302 log.Printf("[debug] Got status '%s' for node '%s'", resp.Status, node.Hostname())
303 }
David K. Bainbridgeefa951d2016-05-26 10:54:25 -0700304 switch resp.StatusCode {
David K. Bainbridge2f456b82016-06-14 22:32:51 -0700305 case http.StatusOK: // provisioning completed or failed
306 decoder := json.NewDecoder(resp.Body)
307 var raw interface{}
308 err = decoder.Decode(&raw)
309 if err != nil {
310 log.Printf("[error] Unable to unmarshal response from provisioner for '%s': %s",
311 node.Hostname(), err)
David K. Bainbridgeefa951d2016-05-26 10:54:25 -0700312 }
David K. Bainbridge2f456b82016-06-14 22:32:51 -0700313 status := raw.(map[string]interface{})
314 switch int(status["status"].(float64)) {
315 case 0, 1: // PENDING, RUNNING ... should never really get here
316 // noop, already in this state
317 case 2: // COMPLETE
318 if options.Verbose {
319 log.Printf("[info] Marking node '%s' with ID '%s' as provisioned",
320 node.Hostname(), node.ID())
321 }
322 record.State = Provisioned
323 options.ProvTracker.Set(node.ID(), record)
324 case 3: // FAILED
325 if options.Verbose {
326 log.Printf("[info] Marking node '%s' with ID '%s' as failed provisioning",
327 node.Hostname(), node.ID())
328 }
329 record.State = ProvisionError
330 options.ProvTracker.Set(node.ID(), record)
331 default:
332 log.Printf("[error] unknown status state for node '%s' : %d",
333 node.Hostname(), int(status["status"].(float64)))
334 }
David K. Bainbridge8352c592016-06-02 12:48:37 -0700335 case http.StatusAccepted: // in the provisioning state
David K. Bainbridgeefa951d2016-05-26 10:54:25 -0700336 // Noop, presumably alread in this state
David K. Bainbridge3ee76412016-06-15 18:56:08 -0700337 case http.StatusNotFound:
338 // Noop, but not an error
David K. Bainbridgeefa951d2016-05-26 10:54:25 -0700339 default: // Consider anything else an erorr
David K. Bainbridge8352c592016-06-02 12:48:37 -0700340 log.Printf("[warn] Node '%s' with ID '%s' failed provisioning, will retry",
341 node.Hostname(), node.ID())
David K. Bainbridgeefa951d2016-05-26 10:54:25 -0700342 record.State = ProvisionError
343 options.ProvTracker.Set(node.ID(), record)
344 }
345 }
346 }
347 }
348 } else if options.Verbose {
David K. Bainbridge218fdd62016-06-15 10:31:38 -0700349 log.Printf("[info] Not invoking provisioning for '%s', current state is '%s'", node.Hostname(),
David K. Bainbridgeefa951d2016-05-26 10:54:25 -0700350 record.State.String())
351 }
352
353 return nil
354}
355
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700356// Done we are at the target state, nothing to do
357var Done = func(client *maas.MAASObject, node MaasNode, options ProcessingOptions) error {
358 // As devices are normally in the "COMPLETED" state we don't want to
359 // log this fact unless we are in verbose mode. I suspect it would be
360 // nice to log it once when the device transitions from a non COMPLETE
361 // state to a complete state, but that would require keeping state.
362 if options.Verbose {
363 log.Printf("COMPLETE: %s", node.Hostname())
364 }
365
366 if options.AlwaysRename {
367 updateNodeName(client, node, options)
368 }
369
370 return nil
371}
372
373// Deploy cause a node to deploy
374var Deploy = func(client *maas.MAASObject, node MaasNode, options ProcessingOptions) error {
375 log.Printf("DEPLOY: %s", node.Hostname())
376
377 if options.AlwaysRename {
378 updateNodeName(client, node, options)
379 }
380
381 if !options.Preview {
382 nodesObj := client.GetSubObject("nodes")
383 myNode := nodesObj.GetSubObject(node.ID())
384 // Start the node with the trusty distro. This should really be looked up or
385 // a parameter default
David K. Bainbridgeefa951d2016-05-26 10:54:25 -0700386 _, err := myNode.CallPost("start", url.Values{"distro_series": []string{"trusty"}})
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700387 if err != nil {
388 log.Printf("ERROR: DEPLOY '%s' : '%s'", node.Hostname(), err)
389 return err
390 }
391 }
392 return nil
393}
394
395// Aquire aquire a machine to a specific operator
396var Aquire = func(client *maas.MAASObject, node MaasNode, options ProcessingOptions) error {
397 log.Printf("AQUIRE: %s", node.Hostname())
398 nodesObj := client.GetSubObject("nodes")
399
400 if options.AlwaysRename {
401 updateNodeName(client, node, options)
402 }
403
404 if !options.Preview {
405 // With a new version of MAAS we have to make sure the node is linked
406 // to the subnet vid DHCP before we move to the Aquire state. To do this
407 // We need to unlink the interface to the subnet and then relink it.
408 //
409 // Iterate through all the interfaces on the node, searching for ones
410 // that are valid and not DHCP and move them to DHCP
411 ifcsObj := client.GetSubObject("nodes").GetSubObject(node.ID()).GetSubObject("interfaces")
412 ifcsListObj, err := ifcsObj.CallGet("", url.Values{})
413 if err != nil {
414 return err
415 }
416
417 ifcsArray, err := ifcsListObj.GetArray()
418 if err != nil {
419 return err
420 }
421
422 for _, ifc := range ifcsArray {
423 ifcMap, err := ifc.GetMap()
424 if err != nil {
425 return err
426 }
427
428 // Iterate over the links assocated with the interface, looking for
429 // links with a subnect as well as a mode of "auto"
430 links, ok := ifcMap["links"]
431 if ok {
432 linkArray, err := links.GetArray()
433 if err != nil {
434 return err
435 }
436
437 for _, link := range linkArray {
438 linkMap, err := link.GetMap()
439 if err != nil {
440 return err
441 }
442 subnet, ok := linkMap["subnet"]
443 if ok {
444 subnetMap, err := subnet.GetMap()
445 if err != nil {
446 return err
447 }
448
449 val, err := linkMap["mode"].GetString()
450 if err != nil {
451 return err
452 }
453
454 if val == "auto" {
455 // Found one we like, so grab the subnet from the data and
456 // then relink this as DHCP
457 cidr, err := subnetMap["cidr"].GetString()
458 if err != nil {
459 return err
460 }
461
462 fifcID, err := ifcMap["id"].GetFloat64()
463 if err != nil {
464 return err
465 }
466 ifcID := strconv.Itoa(int(fifcID))
467
468 flID, err := linkMap["id"].GetFloat64()
469 if err != nil {
470 return err
471 }
472 lID := strconv.Itoa(int(flID))
473
474 ifcObj := ifcsObj.GetSubObject(ifcID)
475 _, err = ifcObj.CallPost("unlink_subnet", url.Values{"id": []string{lID}})
476 if err != nil {
477 return err
478 }
479 _, err = ifcObj.CallPost("link_subnet", url.Values{"mode": []string{"DHCP"}, "subnet": []string{cidr}})
480 if err != nil {
481 return err
482 }
483 }
484 }
485 }
486 }
487 }
488 _, err = nodesObj.CallPost("acquire",
489 url.Values{"name": []string{node.Hostname()}})
490 if err != nil {
491 log.Printf("ERROR: AQUIRE '%s' : '%s'", node.Hostname(), err)
492 return err
493 }
494 }
495 return nil
496}
497
498// Commission cause a node to be commissioned
499var Commission = func(client *maas.MAASObject, node MaasNode, options ProcessingOptions) error {
500 updateNodeName(client, node, options)
501
502 // Need to understand the power state of the node. We only want to move to "Commissioning" if the node
503 // power is off. If the node power is not off, then turn it off.
504 state := node.PowerState()
505 switch state {
506 case "on":
507 // Attempt to turn the node off
508 log.Printf("POWER DOWN: %s", node.Hostname())
509 if !options.Preview {
David K. Bainbridgeefa951d2016-05-26 10:54:25 -0700510 //POST /api/1.0/nodes/{system_id}/ op=stop
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700511 nodesObj := client.GetSubObject("nodes")
512 nodeObj := nodesObj.GetSubObject(node.ID())
David K. Bainbridgeefa951d2016-05-26 10:54:25 -0700513 _, err := nodeObj.CallPost("stop", url.Values{"stop_mode": []string{"soft"}})
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700514 if err != nil {
515 log.Printf("ERROR: Commission '%s' : changing power start to off : '%s'", node.Hostname(), err)
516 }
517 return err
518 }
519 break
520 case "off":
521 // We are off so move to commissioning
522 log.Printf("COMISSION: %s", node.Hostname())
523 if !options.Preview {
524 nodesObj := client.GetSubObject("nodes")
525 nodeObj := nodesObj.GetSubObject(node.ID())
526
527 updateNodeName(client, node, options)
528
529 _, err := nodeObj.CallPost("commission", url.Values{})
530 if err != nil {
531 log.Printf("ERROR: Commission '%s' : '%s'", node.Hostname(), err)
532 }
533 return err
534 }
535 break
536 default:
537 // We are in a state from which we can't move forward.
David K. Bainbridge6ea57c12016-06-06 23:29:12 -0700538 log.Printf("[warn]: %s has invalid power state '%s'", node.Hostname(), state)
539
540 // If a power helper script is set, we have an unknown power state, and
541 // we have not power type then attempt to use the helper script to discover
542 // and set the power settings
543 if options.PowerHelper != "" && node.PowerType() == "" {
544 cmd := exec.Command(options.PowerHelper,
545 append([]string{options.PowerHelperUser, options.PowerHelperHost},
546 node.MACs()...)...)
547 stdout, err := cmd.Output()
548 if err != nil {
549 log.Printf("[error] Failed while executing power helper script '%s' : %s",
550 options.PowerHelper, err)
551 return err
552 }
553 power := Power{}
554 err = json.Unmarshal(stdout, &power)
555 if err != nil {
556 log.Printf("[error] Failed to parse output of power helper script '%s' : %s",
557 options.PowerHelper, err)
558 return err
559 }
560 switch power.Name {
561 case "amt":
562 params := map[string]string{
563 "mac_address": power.MacAddress,
564 "power_pass": power.PowerPassword,
565 "power_address": power.PowerAddress,
566 }
567 node.UpdatePowerParameters(power.Name, params)
568 default:
569 log.Printf("[warn] Unsupported power type discovered '%s'", power.Name)
570 }
571 }
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700572 break
573 }
574 return nil
575}
576
577// Wait a do nothing state, while work is being done
578var Wait = func(client *maas.MAASObject, node MaasNode, options ProcessingOptions) error {
579 log.Printf("WAIT: %s", node.Hostname())
580 return nil
581}
582
583// Fail a state from which we cannot, currently, automatically recover
584var Fail = func(client *maas.MAASObject, node MaasNode, options ProcessingOptions) error {
585 log.Printf("FAIL: %s", node.Hostname())
586 return nil
587}
588
589// AdminState an administrative state from which we should make no automatic transition
590var AdminState = func(client *maas.MAASObject, node MaasNode, options ProcessingOptions) error {
591 log.Printf("ADMIN: %s", node.Hostname())
592 return nil
593}
594
David K. Bainbridgeefa951d2016-05-26 10:54:25 -0700595func findActions(target string, current string) ([]Action, error) {
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700596 targets, ok := Transitions[target]
597 if !ok {
598 log.Printf("[warn] unable to find transitions to target state '%s'", target)
599 return nil, fmt.Errorf("Could not find transition to target state '%s'", target)
600 }
601
David K. Bainbridgeefa951d2016-05-26 10:54:25 -0700602 actions, ok := targets[current]
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700603 if !ok {
604 log.Printf("[warn] unable to find transition from current state '%s' to target state '%s'",
605 current, target)
606 return nil, fmt.Errorf("Could not find transition from current state '%s' to target state '%s'",
607 current, target)
608 }
609
David K. Bainbridgeefa951d2016-05-26 10:54:25 -0700610 return actions, nil
611}
612
613// ProcessActions
614func ProcessActions(actions []Action, client *maas.MAASObject, node MaasNode, options ProcessingOptions) error {
615 var err error
616 for _, action := range actions {
617 if err = action(client, node, options); err != nil {
David K. Bainbridge6ea57c12016-06-06 23:29:12 -0700618 log.Printf("[error] Error while processing action for node '%s' : %s",
619 node.Hostname(), err)
David K. Bainbridgeefa951d2016-05-26 10:54:25 -0700620 break
621 }
622 }
623 return err
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700624}
625
626// ProcessNode something
627func ProcessNode(client *maas.MAASObject, node MaasNode, options ProcessingOptions) error {
628 substatus, err := node.GetInteger("substatus")
629 if err != nil {
630 return err
631 }
David K. Bainbridgeefa951d2016-05-26 10:54:25 -0700632 actions, err := findActions("Deployed", MaasNodeStatus(substatus).String())
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700633 if err != nil {
634 return err
635 }
636
637 if options.Preview {
David K. Bainbridgeefa951d2016-05-26 10:54:25 -0700638 ProcessActions(actions, client, node, options)
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700639 } else {
David K. Bainbridgeefa951d2016-05-26 10:54:25 -0700640 go ProcessActions(actions, client, node, options)
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700641 }
642 return nil
643}
644
645func buildFilter(filter []string) ([]*regexp.Regexp, error) {
646
647 results := make([]*regexp.Regexp, len(filter))
648 for i, v := range filter {
649 r, err := regexp.Compile(v)
650 if err != nil {
651 return nil, err
652 }
653 results[i] = r
654 }
655 return results, nil
656}
657
658func matchedFilter(include []*regexp.Regexp, target string) bool {
659 for _, e := range include {
660 if e.MatchString(target) {
661 return true
662 }
663 }
664 return false
665}
666
667// ProcessAll something
668func ProcessAll(client *maas.MAASObject, nodes []MaasNode, options ProcessingOptions) []error {
669 errors := make([]error, len(nodes))
670 includeHosts, err := buildFilter(options.Filter.Hosts.Include)
671 if err != nil {
672 log.Fatalf("[error] invalid regular expression for include filter '%s' : %s", options.Filter.Hosts.Include, err)
673 }
674
675 includeZones, err := buildFilter(options.Filter.Zones.Include)
676 if err != nil {
677 log.Fatalf("[error] invalid regular expression for include filter '%v' : %s", options.Filter.Zones.Include, err)
678 }
679
680 for i, node := range nodes {
681 // For hostnames we always match on an empty filter
682 if len(includeHosts) >= 0 && matchedFilter(includeHosts, node.Hostname()) {
683
684 // For zones we don't match on an empty filter
685 if len(includeZones) >= 0 && matchedFilter(includeZones, node.Zone()) {
686 err := ProcessNode(client, node, options)
687 if err != nil {
688 errors[i] = err
689 } else {
690 errors[i] = nil
691 }
692 } else {
693 if options.Verbose {
694 log.Printf("[info] ignoring node '%s' as its zone '%s' didn't match include zone name filter '%v'",
695 node.Hostname(), node.Zone(), options.Filter.Zones.Include)
696 }
697 }
698 } else {
699 if options.Verbose {
700 log.Printf("[info] ignoring node '%s' as it didn't match include hostname filter '%v'",
701 node.Hostname(), options.Filter.Hosts.Include)
702 }
703 }
704 }
705 return errors
706}