blob: d4d095b551a46c5de98a4e4cb3a556274817bcba [file] [log] [blame]
David K. Bainbridgedf9df632016-07-07 18:47:46 -07001// Copyright 2016 Open Networking Laboratory
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
David K. Bainbridgeb5415042016-05-13 17:06:10 -070014package main
15
16import (
David K. Bainbridgeefa951d2016-05-26 10:54:25 -070017 "encoding/json"
David K. Bainbridgeb5415042016-05-13 17:06:10 -070018 "fmt"
David K. Bainbridgeceea4f22016-11-17 12:13:42 -080019 "net"
David K. Bainbridgeb5415042016-05-13 17:06:10 -070020 "net/url"
David K. Bainbridgeefa951d2016-05-26 10:54:25 -070021 "os/exec"
David K. Bainbridgeb5415042016-05-13 17:06:10 -070022 "regexp"
23 "strconv"
24 "strings"
David K. Bainbridgeefa951d2016-05-26 10:54:25 -070025 "time"
David K. Bainbridgeb5415042016-05-13 17:06:10 -070026
27 maas "github.com/juju/gomaasapi"
28)
29
30// Action how to get from there to here
31type Action func(*maas.MAASObject, MaasNode, ProcessingOptions) error
32
33// Transition the map from where i want to be from where i might be
34type Transition struct {
35 Target string
36 Current string
37 Using Action
38}
39
David K. Bainbridge6ea57c12016-06-06 23:29:12 -070040type Power struct {
41 Name string `json:"name"`
42 MacAddress string `json:"mac_address"`
43 PowerPassword string `json:"power_password"`
44 PowerAddress string `json:"power_address"`
45}
46
David K. Bainbridgee9d7af72016-10-14 08:42:55 -070047type HostFilter struct {
48 Zones struct {
49 Include []string `json:"include,omitempty"`
50 Exclude []string `json:"exclude,omitempty"`
51 } `json:"zones,omitempty"`
52 Hosts struct {
53 Include []string `json:"include,omitempty"`
54 Exclude []string `json:"exclude,omitempty"`
55 } `json:"hosts,omitempty"`
56}
57
David K. Bainbridgeb5415042016-05-13 17:06:10 -070058// ProcessingOptions used to determine on what hosts to operate
59type ProcessingOptions struct {
David K. Bainbridgee9d7af72016-10-14 08:42:55 -070060 Filter HostFilter
61 Mappings map[string]string
David K. Bainbridge6ea57c12016-06-06 23:29:12 -070062 Preview bool
63 AlwaysRename bool
David K. Bainbridge068e87d2016-06-30 13:53:19 -070064 Provisioner Provisioner
David K. Bainbridge6ea57c12016-06-06 23:29:12 -070065 ProvisionURL string
66 ProvisionTTL time.Duration
67 PowerHelper string
68 PowerHelperUser string
69 PowerHelperHost string
David K. Bainbridgeb5415042016-05-13 17:06:10 -070070}
71
72// Transitions the actual map
73//
74// Currently this is a hand compiled / optimized "next step" table. This should
75// really be generated from the state machine chart input. Once this has been
76// accomplished you should be able to determine the action to take given your
77// target state and your current state.
David K. Bainbridgeefa951d2016-05-26 10:54:25 -070078var Transitions = map[string]map[string][]Action{
David K. Bainbridgeb5415042016-05-13 17:06:10 -070079 "Deployed": {
David K. Bainbridgeefa951d2016-05-26 10:54:25 -070080 "New": []Action{Reset, Commission},
81 "Deployed": []Action{Provision, Done},
82 "Ready": []Action{Reset, Aquire},
83 "Allocated": []Action{Reset, Deploy},
84 "Retired": []Action{Reset, AdminState},
85 "Reserved": []Action{Reset, AdminState},
86 "Releasing": []Action{Reset, Wait},
87 "DiskErasing": []Action{Reset, Wait},
88 "Deploying": []Action{Reset, Wait},
89 "Commissioning": []Action{Reset, Wait},
90 "Missing": []Action{Reset, Fail},
91 "FailedReleasing": []Action{Reset, Fail},
92 "FailedDiskErasing": []Action{Reset, Fail},
93 "FailedDeployment": []Action{Reset, Fail},
94 "Broken": []Action{Reset, Fail},
95 "FailedCommissioning": []Action{Reset, Fail},
David K. Bainbridgeb5415042016-05-13 17:06:10 -070096 },
97}
98
99const (
100 // defaultStateMachine Would be nice to drive from a graph language
101 defaultStateMachine string = `
David K. Bainbridge7b1f18c2016-11-17 09:27:07 -0800102 (New)->(Commissioning)
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700103 (Commissioning)->(FailedCommissioning)
104 (FailedCommissioning)->(New)
105 (Commissioning)->(Ready)
106 (Ready)->(Deploying)
107 (Ready)->(Allocated)
108 (Allocated)->(Deploying)
109 (Deploying)->(Deployed)
110 (Deploying)->(FailedDeployment)
111 (FailedDeployment)->(Broken)
112 (Deployed)->(Releasing)
113 (Releasing)->(FailedReleasing)
114 (FailedReleasing)->(Broken)
115 (Releasing)->(DiskErasing)
116 (DiskErasing)->(FailedEraseDisk)
117 (FailedEraseDisk)->(Broken)
118 (Releasing)->(Ready)
119 (DiskErasing)->(Ready)
David K. Bainbridgeefa951d2016-05-26 10:54:25 -0700120 (Broken)->(Ready)
David K. Bainbridged9b966f2016-05-31 13:30:05 -0700121 (Deployed)->(Provisioning)
David K. Bainbridge7b1f18c2016-11-17 09:27:07 -0800122 (Provisioning)->(HTTP PUT)
123 (HTTP PUT)->(HTTP GET)
124 (HTTP GET)->(HTTP GET)
125 (HTTP GET)->|b|
126 |b|->(Provisioned)
127 |b|->(ProvisionError)
David K. Bainbridged9b966f2016-05-31 13:30:05 -0700128 (ProvisionError)->(Provisioning)`
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700129)
130
131// updateName - changes the name of the MAAS node based on the configuration file
132func updateNodeName(client *maas.MAASObject, node MaasNode, options ProcessingOptions) error {
133 macs := node.MACs()
134
135 // Get current node name and strip off domain name
136 current := node.Hostname()
137 if i := strings.IndexRune(current, '.'); i != -1 {
138 current = current[:i]
139 }
140 for _, mac := range macs {
David K. Bainbridgee9d7af72016-10-14 08:42:55 -0700141 if name, ok := options.Mappings[mac]; ok {
142 if current != name {
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700143 nodesObj := client.GetSubObject("nodes")
144 nodeObj := nodesObj.GetSubObject(node.ID())
David K. Bainbridgee9d7af72016-10-14 08:42:55 -0700145 log.Infof("RENAME '%s' to '%s'\n", node.Hostname(), name)
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700146
147 if !options.Preview {
David K. Bainbridgee9d7af72016-10-14 08:42:55 -0700148 nodeObj.Update(url.Values{"hostname": []string{name}})
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700149 }
150 }
151 }
152 }
153 return nil
154}
155
David K. Bainbridgeefa951d2016-05-26 10:54:25 -0700156// Reset we are at the target state, nothing to do
157var Reset = func(client *maas.MAASObject, node MaasNode, options ProcessingOptions) error {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700158 log.Debugf("RESET: %s", node.Hostname())
David K. Bainbridgeefa951d2016-05-26 10:54:25 -0700159
160 if options.AlwaysRename {
161 updateNodeName(client, node, options)
162 }
163
David K. Bainbridge84918ec2016-07-08 09:12:35 -0700164 err := options.Provisioner.Clear(node.ID())
165 if err != nil {
166 log.Errorf("Attempting to clear provisioning state of node '%s' : %s", node.ID(), err)
167 }
168 return err
David K. Bainbridgeefa951d2016-05-26 10:54:25 -0700169}
170
171// Provision we are at the target state, nothing to do
172var Provision = func(client *maas.MAASObject, node MaasNode, options ProcessingOptions) error {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700173 log.Debugf("CHECK PROVISION: %s", node.Hostname())
David K. Bainbridgeefa951d2016-05-26 10:54:25 -0700174
175 if options.AlwaysRename {
176 updateNodeName(client, node, options)
177 }
178
David K. Bainbridge068e87d2016-06-30 13:53:19 -0700179 record, err := options.Provisioner.Get(node.ID())
David K. Bainbridgeefa951d2016-05-26 10:54:25 -0700180 if err != nil {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700181 log.Warningf("unable to retrieve provisioning state of node '%s' : %s", node.Hostname(), err)
David K. Bainbridge068e87d2016-06-30 13:53:19 -0700182 } else if record == nil || record.Status == Failed {
183 var label string
184 if record == nil {
185 label = "NotFound"
186 } else {
187 label = record.Status.String()
188 }
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700189 log.Debugf("Current state of node '%s' is '%s'", node.Hostname(), label)
David K. Bainbridge068e87d2016-06-30 13:53:19 -0700190 ips := node.IPs()
191 ip := ""
192 if len(ips) > 0 {
193 ip = ips[0]
David K. Bainbridgeceea4f22016-11-17 12:13:42 -0800194 } else {
195 // An IP is required by the provisioner, so if we don't have one then attempt
196 // to resolve the name
197 log.Debugf("MAAS did not return the IP address of host '%s', attempting to resolve independently",
198 node.Hostname())
199 addrs, err := net.LookupHost(node.Hostname())
200 if err != nil || len(addrs) == 0 {
201 log.Errorf("Unable to determine IP address of '%s', thus unable to provision node '%s'",
202 node.Hostname(), node.ID())
203 if err == nil {
204 err = fmt.Errorf("Unable to determine IP address of host '%s'", node.Hostname)
205 } else {
206 err = fmt.Errorf("Unable to determine IP address of host '%s' : %s",
207 node.Hostname, err)
208 }
209 return err
210 }
211 ip = addrs[0]
212 log.Debugf("Resolved hostname '%s' to IP address '%s'", node.Hostname(), ip)
David K. Bainbridgeefa951d2016-05-26 10:54:25 -0700213 }
David K. Bainbridge068e87d2016-06-30 13:53:19 -0700214 macs := node.MACs()
215 mac := ""
216 if len(macs) > 0 {
217 mac = macs[0]
218 }
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700219 log.Debugf("POSTing '%s' (%s) to '%s'", node.Hostname(), node.ID(), options.ProvisionURL)
David K. Bainbridge068e87d2016-06-30 13:53:19 -0700220 err = options.Provisioner.Provision(&ProvisionRequest{
221 Id: node.ID(),
222 Name: node.Hostname(),
223 Ip: ip,
224 Mac: mac,
225 })
David K. Bainbridgeefa951d2016-05-26 10:54:25 -0700226
227 if err != nil {
David K. Bainbridge11850cb2016-10-28 14:05:59 -0700228 log.Errorf("unable to provision '%s' (%s) : %s", node.ID(), node.Hostname(), err)
David K. Bainbridgeefa951d2016-05-26 10:54:25 -0700229 }
David K. Bainbridge068e87d2016-06-30 13:53:19 -0700230
231 } else if options.ProvisionTTL > 0 &&
232 record.Status == Running && time.Since(time.Unix(record.Timestamp, 0)) > options.ProvisionTTL {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700233 log.Errorf("Provisioning of node '%s' has passed provisioning TTL of '%v'",
David K. Bainbridgeefa951d2016-05-26 10:54:25 -0700234 node.Hostname(), options.ProvisionTTL)
David K. Bainbridge068e87d2016-06-30 13:53:19 -0700235 options.Provisioner.Clear(node.ID())
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700236 } else {
237 log.Debugf("Not invoking provisioning for '%s', current state is '%s'", node.Hostname(),
David K. Bainbridge068e87d2016-06-30 13:53:19 -0700238 record.Status.String())
David K. Bainbridgeefa951d2016-05-26 10:54:25 -0700239 }
240
241 return nil
242}
243
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700244// Done we are at the target state, nothing to do
245var Done = func(client *maas.MAASObject, node MaasNode, options ProcessingOptions) error {
246 // As devices are normally in the "COMPLETED" state we don't want to
247 // log this fact unless we are in verbose mode. I suspect it would be
248 // nice to log it once when the device transitions from a non COMPLETE
249 // state to a complete state, but that would require keeping state.
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700250 log.Debugf("COMPLETE: %s", node.Hostname())
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700251
252 if options.AlwaysRename {
253 updateNodeName(client, node, options)
254 }
255
256 return nil
257}
258
259// Deploy cause a node to deploy
260var Deploy = func(client *maas.MAASObject, node MaasNode, options ProcessingOptions) error {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700261 log.Infof("DEPLOY: %s", node.Hostname())
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700262
263 if options.AlwaysRename {
264 updateNodeName(client, node, options)
265 }
266
267 if !options.Preview {
268 nodesObj := client.GetSubObject("nodes")
269 myNode := nodesObj.GetSubObject(node.ID())
270 // Start the node with the trusty distro. This should really be looked up or
271 // a parameter default
David K. Bainbridgeefa951d2016-05-26 10:54:25 -0700272 _, err := myNode.CallPost("start", url.Values{"distro_series": []string{"trusty"}})
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700273 if err != nil {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700274 log.Errorf("DEPLOY '%s' : '%s'", node.Hostname(), err)
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700275 return err
276 }
277 }
278 return nil
279}
280
281// Aquire aquire a machine to a specific operator
282var Aquire = func(client *maas.MAASObject, node MaasNode, options ProcessingOptions) error {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700283 log.Infof("AQUIRE: %s", node.Hostname())
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700284 nodesObj := client.GetSubObject("nodes")
285
286 if options.AlwaysRename {
287 updateNodeName(client, node, options)
288 }
289
290 if !options.Preview {
291 // With a new version of MAAS we have to make sure the node is linked
292 // to the subnet vid DHCP before we move to the Aquire state. To do this
293 // We need to unlink the interface to the subnet and then relink it.
294 //
295 // Iterate through all the interfaces on the node, searching for ones
296 // that are valid and not DHCP and move them to DHCP
297 ifcsObj := client.GetSubObject("nodes").GetSubObject(node.ID()).GetSubObject("interfaces")
298 ifcsListObj, err := ifcsObj.CallGet("", url.Values{})
299 if err != nil {
300 return err
301 }
302
303 ifcsArray, err := ifcsListObj.GetArray()
304 if err != nil {
305 return err
306 }
307
308 for _, ifc := range ifcsArray {
309 ifcMap, err := ifc.GetMap()
310 if err != nil {
311 return err
312 }
313
314 // Iterate over the links assocated with the interface, looking for
315 // links with a subnect as well as a mode of "auto"
316 links, ok := ifcMap["links"]
317 if ok {
318 linkArray, err := links.GetArray()
319 if err != nil {
320 return err
321 }
322
323 for _, link := range linkArray {
324 linkMap, err := link.GetMap()
325 if err != nil {
326 return err
327 }
328 subnet, ok := linkMap["subnet"]
329 if ok {
330 subnetMap, err := subnet.GetMap()
331 if err != nil {
332 return err
333 }
334
335 val, err := linkMap["mode"].GetString()
336 if err != nil {
337 return err
338 }
339
340 if val == "auto" {
341 // Found one we like, so grab the subnet from the data and
342 // then relink this as DHCP
343 cidr, err := subnetMap["cidr"].GetString()
344 if err != nil {
345 return err
346 }
347
348 fifcID, err := ifcMap["id"].GetFloat64()
349 if err != nil {
350 return err
351 }
352 ifcID := strconv.Itoa(int(fifcID))
353
354 flID, err := linkMap["id"].GetFloat64()
355 if err != nil {
356 return err
357 }
358 lID := strconv.Itoa(int(flID))
359
360 ifcObj := ifcsObj.GetSubObject(ifcID)
361 _, err = ifcObj.CallPost("unlink_subnet", url.Values{"id": []string{lID}})
362 if err != nil {
363 return err
364 }
365 _, err = ifcObj.CallPost("link_subnet", url.Values{"mode": []string{"DHCP"}, "subnet": []string{cidr}})
366 if err != nil {
367 return err
368 }
369 }
370 }
371 }
372 }
373 }
374 _, err = nodesObj.CallPost("acquire",
375 url.Values{"name": []string{node.Hostname()}})
376 if err != nil {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700377 log.Errorf("AQUIRE '%s' : '%s'", node.Hostname(), err)
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700378 return err
379 }
380 }
381 return nil
382}
383
384// Commission cause a node to be commissioned
385var Commission = func(client *maas.MAASObject, node MaasNode, options ProcessingOptions) error {
386 updateNodeName(client, node, options)
387
388 // Need to understand the power state of the node. We only want to move to "Commissioning" if the node
389 // power is off. If the node power is not off, then turn it off.
390 state := node.PowerState()
391 switch state {
392 case "on":
393 // Attempt to turn the node off
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700394 log.Infof("POWER DOWN: %s", node.Hostname())
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700395 if !options.Preview {
David K. Bainbridgeefa951d2016-05-26 10:54:25 -0700396 //POST /api/1.0/nodes/{system_id}/ op=stop
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700397 nodesObj := client.GetSubObject("nodes")
398 nodeObj := nodesObj.GetSubObject(node.ID())
David K. Bainbridgeefa951d2016-05-26 10:54:25 -0700399 _, err := nodeObj.CallPost("stop", url.Values{"stop_mode": []string{"soft"}})
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700400 if err != nil {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700401 log.Errorf("Commission '%s' : changing power start to off : '%s'", node.Hostname(), err)
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700402 }
403 return err
404 }
405 break
406 case "off":
407 // We are off so move to commissioning
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700408 log.Infof("COMISSION: %s", node.Hostname())
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700409 if !options.Preview {
410 nodesObj := client.GetSubObject("nodes")
411 nodeObj := nodesObj.GetSubObject(node.ID())
412
413 updateNodeName(client, node, options)
414
415 _, err := nodeObj.CallPost("commission", url.Values{})
416 if err != nil {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700417 log.Errorf("Commission '%s' : '%s'", node.Hostname(), err)
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700418 }
419 return err
420 }
421 break
422 default:
423 // We are in a state from which we can't move forward.
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700424 log.Warningf("%s has invalid power state '%s'", node.Hostname(), state)
David K. Bainbridge6ea57c12016-06-06 23:29:12 -0700425
426 // If a power helper script is set, we have an unknown power state, and
427 // we have not power type then attempt to use the helper script to discover
428 // and set the power settings
429 if options.PowerHelper != "" && node.PowerType() == "" {
430 cmd := exec.Command(options.PowerHelper,
431 append([]string{options.PowerHelperUser, options.PowerHelperHost},
432 node.MACs()...)...)
433 stdout, err := cmd.Output()
434 if err != nil {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700435 log.Errorf("Failed while executing power helper script '%s' : %s",
David K. Bainbridge6ea57c12016-06-06 23:29:12 -0700436 options.PowerHelper, err)
437 return err
438 }
439 power := Power{}
440 err = json.Unmarshal(stdout, &power)
441 if err != nil {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700442 log.Errorf("Failed to parse output of power helper script '%s' : %s",
David K. Bainbridge6ea57c12016-06-06 23:29:12 -0700443 options.PowerHelper, err)
444 return err
445 }
446 switch power.Name {
447 case "amt":
448 params := map[string]string{
449 "mac_address": power.MacAddress,
450 "power_pass": power.PowerPassword,
451 "power_address": power.PowerAddress,
452 }
453 node.UpdatePowerParameters(power.Name, params)
454 default:
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700455 log.Warningf("Unsupported power type discovered '%s'", power.Name)
David K. Bainbridge6ea57c12016-06-06 23:29:12 -0700456 }
457 }
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700458 break
459 }
460 return nil
461}
462
463// Wait a do nothing state, while work is being done
464var Wait = func(client *maas.MAASObject, node MaasNode, options ProcessingOptions) error {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700465 log.Infof("WAIT: %s", node.Hostname())
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700466 return nil
467}
468
469// Fail a state from which we cannot, currently, automatically recover
470var Fail = func(client *maas.MAASObject, node MaasNode, options ProcessingOptions) error {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700471 log.Infof("FAIL: %s", node.Hostname())
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700472 return nil
473}
474
475// AdminState an administrative state from which we should make no automatic transition
476var AdminState = func(client *maas.MAASObject, node MaasNode, options ProcessingOptions) error {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700477 log.Infof("ADMIN: %s", node.Hostname())
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700478 return nil
479}
480
David K. Bainbridgeefa951d2016-05-26 10:54:25 -0700481func findActions(target string, current string) ([]Action, error) {
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700482 targets, ok := Transitions[target]
483 if !ok {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700484 log.Warningf("unable to find transitions to target state '%s'", target)
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700485 return nil, fmt.Errorf("Could not find transition to target state '%s'", target)
486 }
487
David K. Bainbridgeefa951d2016-05-26 10:54:25 -0700488 actions, ok := targets[current]
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700489 if !ok {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700490 log.Warningf("unable to find transition from current state '%s' to target state '%s'",
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700491 current, target)
492 return nil, fmt.Errorf("Could not find transition from current state '%s' to target state '%s'",
493 current, target)
494 }
495
David K. Bainbridgeefa951d2016-05-26 10:54:25 -0700496 return actions, nil
497}
498
499// ProcessActions
500func ProcessActions(actions []Action, client *maas.MAASObject, node MaasNode, options ProcessingOptions) error {
501 var err error
502 for _, action := range actions {
503 if err = action(client, node, options); err != nil {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700504 log.Errorf("Error while processing action for node '%s' : %s",
David K. Bainbridge6ea57c12016-06-06 23:29:12 -0700505 node.Hostname(), err)
David K. Bainbridgeefa951d2016-05-26 10:54:25 -0700506 break
507 }
508 }
509 return err
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700510}
511
512// ProcessNode something
513func ProcessNode(client *maas.MAASObject, node MaasNode, options ProcessingOptions) error {
514 substatus, err := node.GetInteger("substatus")
515 if err != nil {
516 return err
517 }
David K. Bainbridgeefa951d2016-05-26 10:54:25 -0700518 actions, err := findActions("Deployed", MaasNodeStatus(substatus).String())
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700519 if err != nil {
520 return err
521 }
522
523 if options.Preview {
David K. Bainbridgeefa951d2016-05-26 10:54:25 -0700524 ProcessActions(actions, client, node, options)
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700525 } else {
David K. Bainbridgeefa951d2016-05-26 10:54:25 -0700526 go ProcessActions(actions, client, node, options)
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700527 }
528 return nil
529}
530
531func buildFilter(filter []string) ([]*regexp.Regexp, error) {
532
533 results := make([]*regexp.Regexp, len(filter))
534 for i, v := range filter {
535 r, err := regexp.Compile(v)
536 if err != nil {
537 return nil, err
538 }
539 results[i] = r
540 }
541 return results, nil
542}
543
544func matchedFilter(include []*regexp.Regexp, target string) bool {
545 for _, e := range include {
546 if e.MatchString(target) {
547 return true
548 }
549 }
550 return false
551}
552
553// ProcessAll something
554func ProcessAll(client *maas.MAASObject, nodes []MaasNode, options ProcessingOptions) []error {
555 errors := make([]error, len(nodes))
556 includeHosts, err := buildFilter(options.Filter.Hosts.Include)
557 if err != nil {
558 log.Fatalf("[error] invalid regular expression for include filter '%s' : %s", options.Filter.Hosts.Include, err)
559 }
560
561 includeZones, err := buildFilter(options.Filter.Zones.Include)
562 if err != nil {
563 log.Fatalf("[error] invalid regular expression for include filter '%v' : %s", options.Filter.Zones.Include, err)
564 }
565
566 for i, node := range nodes {
567 // For hostnames we always match on an empty filter
568 if len(includeHosts) >= 0 && matchedFilter(includeHosts, node.Hostname()) {
569
570 // For zones we don't match on an empty filter
571 if len(includeZones) >= 0 && matchedFilter(includeZones, node.Zone()) {
572 err := ProcessNode(client, node, options)
573 if err != nil {
574 errors[i] = err
575 } else {
576 errors[i] = nil
577 }
578 } else {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700579 log.Debugf("ignoring node '%s' as its zone '%s' didn't match include zone name filter '%v'",
580 node.Hostname(), node.Zone(), options.Filter.Zones.Include)
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700581 }
582 } else {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700583 log.Debugf("ignoring node '%s' as it didn't match include hostname filter '%v'",
584 node.Hostname(), options.Filter.Hosts.Include)
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700585 }
586 }
587 return errors
588}