blob: 372e4b856f7fedd94b2e064a620aa4abdb366fac [file] [log] [blame]
Brian O'Connor6a37ea92017-08-03 22:45:59 -07001// Copyright 2016 Open Networking Foundation
David K. Bainbridgedf9df632016-07-07 18:47:46 -07002//
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},
Cem Turkerf203f932018-02-21 23:42:32 +000082 "Ready": []Action{Reset, Allocate},
David K. Bainbridgeefa951d2016-05-26 10:54:25 -070083 "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},
Cem Turkerf203f932018-02-21 23:42:32 +000090 "Testing" : []Action{Reset, Wait},
David K. Bainbridgeefa951d2016-05-26 10:54:25 -070091 "Missing": []Action{Reset, Fail},
92 "FailedReleasing": []Action{Reset, Fail},
93 "FailedDiskErasing": []Action{Reset, Fail},
94 "FailedDeployment": []Action{Reset, Fail},
95 "Broken": []Action{Reset, Fail},
96 "FailedCommissioning": []Action{Reset, Fail},
Cem Turkerf203f932018-02-21 23:42:32 +000097 "FailedTesting": []Action{Reset, Fail},
David K. Bainbridgeb5415042016-05-13 17:06:10 -070098 },
99}
100
101const (
102 // defaultStateMachine Would be nice to drive from a graph language
103 defaultStateMachine string = `
David K. Bainbridge7b1f18c2016-11-17 09:27:07 -0800104 (New)->(Commissioning)
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700105 (Commissioning)->(FailedCommissioning)
106 (FailedCommissioning)->(New)
107 (Commissioning)->(Ready)
108 (Ready)->(Deploying)
109 (Ready)->(Allocated)
Cem Turkerf203f932018-02-21 23:42:32 +0000110 (Allocated)->(Testing)
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700111 (Allocated)->(Deploying)
Cem Turkerf203f932018-02-21 23:42:32 +0000112 (Testing)->(Deploying)
113 (Testing)->(FailedTesting)
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700114 (Deploying)->(Deployed)
115 (Deploying)->(FailedDeployment)
116 (FailedDeployment)->(Broken)
Cem Turkerf203f932018-02-21 23:42:32 +0000117 (FailedTesting)->(Broken)
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700118 (Deployed)->(Releasing)
119 (Releasing)->(FailedReleasing)
120 (FailedReleasing)->(Broken)
121 (Releasing)->(DiskErasing)
122 (DiskErasing)->(FailedEraseDisk)
123 (FailedEraseDisk)->(Broken)
124 (Releasing)->(Ready)
125 (DiskErasing)->(Ready)
David K. Bainbridgeefa951d2016-05-26 10:54:25 -0700126 (Broken)->(Ready)
David K. Bainbridged9b966f2016-05-31 13:30:05 -0700127 (Deployed)->(Provisioning)
David K. Bainbridge7b1f18c2016-11-17 09:27:07 -0800128 (Provisioning)->(HTTP PUT)
129 (HTTP PUT)->(HTTP GET)
130 (HTTP GET)->(HTTP GET)
131 (HTTP GET)->|b|
132 |b|->(Provisioned)
133 |b|->(ProvisionError)
David K. Bainbridged9b966f2016-05-31 13:30:05 -0700134 (ProvisionError)->(Provisioning)`
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700135)
136
137// updateName - changes the name of the MAAS node based on the configuration file
138func updateNodeName(client *maas.MAASObject, node MaasNode, options ProcessingOptions) error {
139 macs := node.MACs()
140
141 // Get current node name and strip off domain name
142 current := node.Hostname()
143 if i := strings.IndexRune(current, '.'); i != -1 {
144 current = current[:i]
145 }
146 for _, mac := range macs {
David K. Bainbridgee9d7af72016-10-14 08:42:55 -0700147 if name, ok := options.Mappings[mac]; ok {
148 if current != name {
Cem Turkerf203f932018-02-21 23:42:32 +0000149 machineObj := client.GetSubObject("machines").GetSubObject(node.ID())
David K. Bainbridgee9d7af72016-10-14 08:42:55 -0700150 log.Infof("RENAME '%s' to '%s'\n", node.Hostname(), name)
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700151
152 if !options.Preview {
Cem Turkerf203f932018-02-21 23:42:32 +0000153 machineObj.Update(url.Values{"hostname": []string{name}})
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700154 }
155 }
156 }
157 }
158 return nil
159}
160
David K. Bainbridgeefa951d2016-05-26 10:54:25 -0700161// Reset we are at the target state, nothing to do
162var Reset = func(client *maas.MAASObject, node MaasNode, options ProcessingOptions) error {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700163 log.Debugf("RESET: %s", node.Hostname())
David K. Bainbridgeefa951d2016-05-26 10:54:25 -0700164
165 if options.AlwaysRename {
166 updateNodeName(client, node, options)
167 }
168
David K. Bainbridge84918ec2016-07-08 09:12:35 -0700169 err := options.Provisioner.Clear(node.ID())
170 if err != nil {
171 log.Errorf("Attempting to clear provisioning state of node '%s' : %s", node.ID(), err)
172 }
173 return err
David K. Bainbridgeefa951d2016-05-26 10:54:25 -0700174}
175
176// Provision we are at the target state, nothing to do
177var Provision = func(client *maas.MAASObject, node MaasNode, options ProcessingOptions) error {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700178 log.Debugf("CHECK PROVISION: %s", node.Hostname())
David K. Bainbridgeefa951d2016-05-26 10:54:25 -0700179
180 if options.AlwaysRename {
181 updateNodeName(client, node, options)
182 }
183
David K. Bainbridge068e87d2016-06-30 13:53:19 -0700184 record, err := options.Provisioner.Get(node.ID())
David K. Bainbridgeefa951d2016-05-26 10:54:25 -0700185 if err != nil {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700186 log.Warningf("unable to retrieve provisioning state of node '%s' : %s", node.Hostname(), err)
David K. Bainbridge068e87d2016-06-30 13:53:19 -0700187 } else if record == nil || record.Status == Failed {
188 var label string
189 if record == nil {
190 label = "NotFound"
191 } else {
192 label = record.Status.String()
193 }
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700194 log.Debugf("Current state of node '%s' is '%s'", node.Hostname(), label)
David K. Bainbridge068e87d2016-06-30 13:53:19 -0700195 ips := node.IPs()
196 ip := ""
197 if len(ips) > 0 {
198 ip = ips[0]
David K. Bainbridgeceea4f22016-11-17 12:13:42 -0800199 } else {
200 // An IP is required by the provisioner, so if we don't have one then attempt
201 // to resolve the name
202 log.Debugf("MAAS did not return the IP address of host '%s', attempting to resolve independently",
203 node.Hostname())
204 addrs, err := net.LookupHost(node.Hostname())
205 if err != nil || len(addrs) == 0 {
206 log.Errorf("Unable to determine IP address of '%s', thus unable to provision node '%s'",
207 node.Hostname(), node.ID())
208 if err == nil {
209 err = fmt.Errorf("Unable to determine IP address of host '%s'", node.Hostname)
210 } else {
211 err = fmt.Errorf("Unable to determine IP address of host '%s' : %s",
212 node.Hostname, err)
213 }
214 return err
215 }
216 ip = addrs[0]
217 log.Debugf("Resolved hostname '%s' to IP address '%s'", node.Hostname(), ip)
David K. Bainbridgeefa951d2016-05-26 10:54:25 -0700218 }
David K. Bainbridge068e87d2016-06-30 13:53:19 -0700219 macs := node.MACs()
220 mac := ""
221 if len(macs) > 0 {
222 mac = macs[0]
223 }
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700224 log.Debugf("POSTing '%s' (%s) to '%s'", node.Hostname(), node.ID(), options.ProvisionURL)
David K. Bainbridge068e87d2016-06-30 13:53:19 -0700225 err = options.Provisioner.Provision(&ProvisionRequest{
226 Id: node.ID(),
227 Name: node.Hostname(),
228 Ip: ip,
229 Mac: mac,
230 })
David K. Bainbridgeefa951d2016-05-26 10:54:25 -0700231
232 if err != nil {
David K. Bainbridge11850cb2016-10-28 14:05:59 -0700233 log.Errorf("unable to provision '%s' (%s) : %s", node.ID(), node.Hostname(), err)
David K. Bainbridgeefa951d2016-05-26 10:54:25 -0700234 }
David K. Bainbridge068e87d2016-06-30 13:53:19 -0700235
236 } else if options.ProvisionTTL > 0 &&
237 record.Status == Running && time.Since(time.Unix(record.Timestamp, 0)) > options.ProvisionTTL {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700238 log.Errorf("Provisioning of node '%s' has passed provisioning TTL of '%v'",
David K. Bainbridgeefa951d2016-05-26 10:54:25 -0700239 node.Hostname(), options.ProvisionTTL)
David K. Bainbridge068e87d2016-06-30 13:53:19 -0700240 options.Provisioner.Clear(node.ID())
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700241 } else {
242 log.Debugf("Not invoking provisioning for '%s', current state is '%s'", node.Hostname(),
David K. Bainbridge068e87d2016-06-30 13:53:19 -0700243 record.Status.String())
David K. Bainbridgeefa951d2016-05-26 10:54:25 -0700244 }
245
246 return nil
247}
248
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700249// Done we are at the target state, nothing to do
250var Done = func(client *maas.MAASObject, node MaasNode, options ProcessingOptions) error {
251 // As devices are normally in the "COMPLETED" state we don't want to
252 // log this fact unless we are in verbose mode. I suspect it would be
253 // nice to log it once when the device transitions from a non COMPLETE
254 // state to a complete state, but that would require keeping state.
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700255 log.Debugf("COMPLETE: %s", node.Hostname())
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700256
257 if options.AlwaysRename {
258 updateNodeName(client, node, options)
259 }
260
261 return nil
262}
263
264// Deploy cause a node to deploy
265var Deploy = func(client *maas.MAASObject, node MaasNode, options ProcessingOptions) error {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700266 log.Infof("DEPLOY: %s", node.Hostname())
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700267
268 if options.AlwaysRename {
269 updateNodeName(client, node, options)
270 }
271
272 if !options.Preview {
Cem Turkerf203f932018-02-21 23:42:32 +0000273 machineObj := client.GetSubObject("machines").GetSubObject(node.ID())
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700274 // Start the node with the trusty distro. This should really be looked up or
275 // a parameter default
Cem Turkerf203f932018-02-21 23:42:32 +0000276 _, err := machineObj.CallPost("deploy", url.Values{"distro_series": []string{"xenial"}})
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700277 if err != nil {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700278 log.Errorf("DEPLOY '%s' : '%s'", node.Hostname(), err)
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700279 return err
280 }
281 }
282 return nil
283}
284
Cem Turkerf203f932018-02-21 23:42:32 +0000285// Acquire a machine to a specific operator
286// Acquire is renamed to allocate for maas 2.0
287var Allocate = func(client *maas.MAASObject, node MaasNode, options ProcessingOptions) error {
288 log.Infof("ALLOCATE: %s", node.Hostname())
289 machinesObj := client.GetSubObject("machines")
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700290
291 if options.AlwaysRename {
292 updateNodeName(client, node, options)
293 }
294
295 if !options.Preview {
296 // With a new version of MAAS we have to make sure the node is linked
297 // to the subnet vid DHCP before we move to the Aquire state. To do this
298 // We need to unlink the interface to the subnet and then relink it.
299 //
300 // Iterate through all the interfaces on the node, searching for ones
301 // that are valid and not DHCP and move them to DHCP
302 ifcsObj := client.GetSubObject("nodes").GetSubObject(node.ID()).GetSubObject("interfaces")
303 ifcsListObj, err := ifcsObj.CallGet("", url.Values{})
304 if err != nil {
305 return err
306 }
307
308 ifcsArray, err := ifcsListObj.GetArray()
309 if err != nil {
310 return err
311 }
312
313 for _, ifc := range ifcsArray {
314 ifcMap, err := ifc.GetMap()
315 if err != nil {
316 return err
317 }
318
319 // Iterate over the links assocated with the interface, looking for
320 // links with a subnect as well as a mode of "auto"
321 links, ok := ifcMap["links"]
322 if ok {
323 linkArray, err := links.GetArray()
324 if err != nil {
325 return err
326 }
327
328 for _, link := range linkArray {
329 linkMap, err := link.GetMap()
330 if err != nil {
331 return err
332 }
333 subnet, ok := linkMap["subnet"]
334 if ok {
335 subnetMap, err := subnet.GetMap()
336 if err != nil {
337 return err
338 }
339
340 val, err := linkMap["mode"].GetString()
341 if err != nil {
342 return err
343 }
344
345 if val == "auto" {
346 // Found one we like, so grab the subnet from the data and
347 // then relink this as DHCP
348 cidr, err := subnetMap["cidr"].GetString()
349 if err != nil {
350 return err
351 }
352
353 fifcID, err := ifcMap["id"].GetFloat64()
354 if err != nil {
355 return err
356 }
357 ifcID := strconv.Itoa(int(fifcID))
358
359 flID, err := linkMap["id"].GetFloat64()
360 if err != nil {
361 return err
362 }
363 lID := strconv.Itoa(int(flID))
364
365 ifcObj := ifcsObj.GetSubObject(ifcID)
366 _, err = ifcObj.CallPost("unlink_subnet", url.Values{"id": []string{lID}})
367 if err != nil {
368 return err
369 }
370 _, err = ifcObj.CallPost("link_subnet", url.Values{"mode": []string{"DHCP"}, "subnet": []string{cidr}})
371 if err != nil {
372 return err
373 }
374 }
375 }
376 }
377 }
378 }
Cem Turkerf203f932018-02-21 23:42:32 +0000379 _, err = machinesObj.CallPost("allocate",
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700380 url.Values{"name": []string{node.Hostname()}})
381 if err != nil {
Cem Turkerf203f932018-02-21 23:42:32 +0000382 log.Errorf("ALLOCATE '%s' : '%s'", node.Hostname(), err)
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700383 return err
384 }
385 }
386 return nil
387}
388
389// Commission cause a node to be commissioned
390var Commission = func(client *maas.MAASObject, node MaasNode, options ProcessingOptions) error {
391 updateNodeName(client, node, options)
392
393 // Need to understand the power state of the node. We only want to move to "Commissioning" if the node
394 // power is off. If the node power is not off, then turn it off.
395 state := node.PowerState()
396 switch state {
397 case "on":
398 // Attempt to turn the node off
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700399 log.Infof("POWER DOWN: %s", node.Hostname())
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700400 if !options.Preview {
Cem Turkerf203f932018-02-21 23:42:32 +0000401 //POST /api/2.0/machines/{system_id}/ op=stop
402 machineObj := client.GetSubObject("machines").GetSubObject(node.ID())
403 _, err := machineObj.CallPost("power_off", url.Values{"stop_mode": []string{"soft"}})
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700404 if err != nil {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700405 log.Errorf("Commission '%s' : changing power start to off : '%s'", node.Hostname(), err)
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700406 }
407 return err
408 }
409 break
410 case "off":
411 // We are off so move to commissioning
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700412 log.Infof("COMISSION: %s", node.Hostname())
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700413 if !options.Preview {
Cem Turkerf203f932018-02-21 23:42:32 +0000414 machineObj := client.GetSubObject("machines").GetSubObject(node.ID())
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700415
416 updateNodeName(client, node, options)
417
Cem Turkerf203f932018-02-21 23:42:32 +0000418 _, err := machineObj.CallPost("commission", url.Values{})
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700419 if err != nil {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700420 log.Errorf("Commission '%s' : '%s'", node.Hostname(), err)
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700421 }
422 return err
423 }
424 break
425 default:
426 // We are in a state from which we can't move forward.
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700427 log.Warningf("%s has invalid power state '%s'", node.Hostname(), state)
David K. Bainbridge6ea57c12016-06-06 23:29:12 -0700428
429 // If a power helper script is set, we have an unknown power state, and
430 // we have not power type then attempt to use the helper script to discover
431 // and set the power settings
432 if options.PowerHelper != "" && node.PowerType() == "" {
433 cmd := exec.Command(options.PowerHelper,
434 append([]string{options.PowerHelperUser, options.PowerHelperHost},
435 node.MACs()...)...)
436 stdout, err := cmd.Output()
437 if err != nil {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700438 log.Errorf("Failed while executing power helper script '%s' : %s",
David K. Bainbridge6ea57c12016-06-06 23:29:12 -0700439 options.PowerHelper, err)
440 return err
441 }
442 power := Power{}
443 err = json.Unmarshal(stdout, &power)
444 if err != nil {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700445 log.Errorf("Failed to parse output of power helper script '%s' : %s",
David K. Bainbridge6ea57c12016-06-06 23:29:12 -0700446 options.PowerHelper, err)
447 return err
448 }
449 switch power.Name {
450 case "amt":
451 params := map[string]string{
452 "mac_address": power.MacAddress,
453 "power_pass": power.PowerPassword,
454 "power_address": power.PowerAddress,
455 }
Cem Turkerf203f932018-02-21 23:42:32 +0000456 machineNodeObj := client.GetSubObject("machines").GetSubObject(node.ID())
457 machineObj, err := machineNodeObj.Get();
458 if err != nil {
459 log.Errorf("Unable to get %s machine object %s ",node.ID(),err)
460 return err
461 }
462 machineNode := MaasNode{machineObj}
463 machineNode.UpdatePowerParameters(power.Name, params)
David K. Bainbridge6ea57c12016-06-06 23:29:12 -0700464 default:
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700465 log.Warningf("Unsupported power type discovered '%s'", power.Name)
David K. Bainbridge6ea57c12016-06-06 23:29:12 -0700466 }
467 }
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700468 break
469 }
470 return nil
471}
472
473// Wait a do nothing state, while work is being done
474var Wait = func(client *maas.MAASObject, node MaasNode, options ProcessingOptions) error {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700475 log.Infof("WAIT: %s", node.Hostname())
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700476 return nil
477}
478
479// Fail a state from which we cannot, currently, automatically recover
480var Fail = func(client *maas.MAASObject, node MaasNode, options ProcessingOptions) error {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700481 log.Infof("FAIL: %s", node.Hostname())
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700482 return nil
483}
484
485// AdminState an administrative state from which we should make no automatic transition
486var AdminState = func(client *maas.MAASObject, node MaasNode, options ProcessingOptions) error {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700487 log.Infof("ADMIN: %s", node.Hostname())
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700488 return nil
489}
490
David K. Bainbridgeefa951d2016-05-26 10:54:25 -0700491func findActions(target string, current string) ([]Action, error) {
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700492 targets, ok := Transitions[target]
493 if !ok {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700494 log.Warningf("unable to find transitions to target state '%s'", target)
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700495 return nil, fmt.Errorf("Could not find transition to target state '%s'", target)
496 }
497
David K. Bainbridgeefa951d2016-05-26 10:54:25 -0700498 actions, ok := targets[current]
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700499 if !ok {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700500 log.Warningf("unable to find transition from current state '%s' to target state '%s'",
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700501 current, target)
502 return nil, fmt.Errorf("Could not find transition from current state '%s' to target state '%s'",
503 current, target)
504 }
505
David K. Bainbridgeefa951d2016-05-26 10:54:25 -0700506 return actions, nil
507}
508
509// ProcessActions
510func ProcessActions(actions []Action, client *maas.MAASObject, node MaasNode, options ProcessingOptions) error {
511 var err error
512 for _, action := range actions {
513 if err = action(client, node, options); err != nil {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700514 log.Errorf("Error while processing action for node '%s' : %s",
David K. Bainbridge6ea57c12016-06-06 23:29:12 -0700515 node.Hostname(), err)
David K. Bainbridgeefa951d2016-05-26 10:54:25 -0700516 break
517 }
518 }
519 return err
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700520}
521
522// ProcessNode something
523func ProcessNode(client *maas.MAASObject, node MaasNode, options ProcessingOptions) error {
Cem Turkerf203f932018-02-21 23:42:32 +0000524 status, err := node.GetInteger("status")
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700525 if err != nil {
526 return err
527 }
Cem Turkerf203f932018-02-21 23:42:32 +0000528 actions, err := findActions("Deployed", MaasNodeStatus(status).String())
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700529 if err != nil {
530 return err
531 }
532
533 if options.Preview {
David K. Bainbridgeefa951d2016-05-26 10:54:25 -0700534 ProcessActions(actions, client, node, options)
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700535 } else {
David K. Bainbridgeefa951d2016-05-26 10:54:25 -0700536 go ProcessActions(actions, client, node, options)
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700537 }
538 return nil
539}
540
541func buildFilter(filter []string) ([]*regexp.Regexp, error) {
542
543 results := make([]*regexp.Regexp, len(filter))
544 for i, v := range filter {
545 r, err := regexp.Compile(v)
546 if err != nil {
547 return nil, err
548 }
549 results[i] = r
550 }
551 return results, nil
552}
553
554func matchedFilter(include []*regexp.Regexp, target string) bool {
555 for _, e := range include {
556 if e.MatchString(target) {
557 return true
558 }
559 }
560 return false
561}
562
563// ProcessAll something
564func ProcessAll(client *maas.MAASObject, nodes []MaasNode, options ProcessingOptions) []error {
565 errors := make([]error, len(nodes))
566 includeHosts, err := buildFilter(options.Filter.Hosts.Include)
567 if err != nil {
568 log.Fatalf("[error] invalid regular expression for include filter '%s' : %s", options.Filter.Hosts.Include, err)
569 }
570
571 includeZones, err := buildFilter(options.Filter.Zones.Include)
572 if err != nil {
573 log.Fatalf("[error] invalid regular expression for include filter '%v' : %s", options.Filter.Zones.Include, err)
574 }
575
576 for i, node := range nodes {
577 // For hostnames we always match on an empty filter
578 if len(includeHosts) >= 0 && matchedFilter(includeHosts, node.Hostname()) {
579
580 // For zones we don't match on an empty filter
581 if len(includeZones) >= 0 && matchedFilter(includeZones, node.Zone()) {
582 err := ProcessNode(client, node, options)
583 if err != nil {
584 errors[i] = err
585 } else {
586 errors[i] = nil
587 }
588 } else {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700589 log.Debugf("ignoring node '%s' as its zone '%s' didn't match include zone name filter '%v'",
590 node.Hostname(), node.Zone(), options.Filter.Zones.Include)
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700591 }
592 } else {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700593 log.Debugf("ignoring node '%s' as it didn't match include hostname filter '%v'",
594 node.Hostname(), options.Filter.Hosts.Include)
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700595 }
596 }
597 return errors
598}