blob: 4b94089cabf863384d20fe23124c875d10d18ba4 [file] [log] [blame]
David K. Bainbridgeb5415042016-05-13 17:06:10 -07001package main
2
3import (
4 "fmt"
5 "log"
6 "net/url"
7 "regexp"
8 "strconv"
9 "strings"
10
11 maas "github.com/juju/gomaasapi"
12)
13
14// Action how to get from there to here
15type Action func(*maas.MAASObject, MaasNode, ProcessingOptions) error
16
17// Transition the map from where i want to be from where i might be
18type Transition struct {
19 Target string
20 Current string
21 Using Action
22}
23
24// ProcessingOptions used to determine on what hosts to operate
25type ProcessingOptions struct {
26 Filter struct {
27 Zones struct {
28 Include []string
29 Exclude []string
30 }
31 Hosts struct {
32 Include []string
33 Exclude []string
34 }
35 }
36 Mappings map[string]interface{}
37 Verbose bool
38 Preview bool
39 AlwaysRename bool
40}
41
42// Transitions the actual map
43//
44// Currently this is a hand compiled / optimized "next step" table. This should
45// really be generated from the state machine chart input. Once this has been
46// accomplished you should be able to determine the action to take given your
47// target state and your current state.
48var Transitions = map[string]map[string]Action{
49 "Deployed": {
50 "New": Commission,
51 "Deployed": Done,
52 "Ready": Aquire,
53 "Allocated": Deploy,
54 "Retired": AdminState,
55 "Reserved": AdminState,
56 "Releasing": Wait,
57 "DiskErasing": Wait,
58 "Deploying": Wait,
59 "Commissioning": Wait,
60 "Missing": Fail,
61 "FailedReleasing": Fail,
62 "FailedDiskErasing": Fail,
63 "FailedDeployment": Fail,
64 "Broken": Fail,
65 "FailedCommissioning": Fail,
66 },
67}
68
69const (
70 // defaultStateMachine Would be nice to drive from a graph language
71 defaultStateMachine string = `
72 (New)->(Commissioning)
73 (Commissioning)->(FailedCommissioning)
74 (FailedCommissioning)->(New)
75 (Commissioning)->(Ready)
76 (Ready)->(Deploying)
77 (Ready)->(Allocated)
78 (Allocated)->(Deploying)
79 (Deploying)->(Deployed)
80 (Deploying)->(FailedDeployment)
81 (FailedDeployment)->(Broken)
82 (Deployed)->(Releasing)
83 (Releasing)->(FailedReleasing)
84 (FailedReleasing)->(Broken)
85 (Releasing)->(DiskErasing)
86 (DiskErasing)->(FailedEraseDisk)
87 (FailedEraseDisk)->(Broken)
88 (Releasing)->(Ready)
89 (DiskErasing)->(Ready)
90 (Broken)->(Ready)`
91)
92
93// updateName - changes the name of the MAAS node based on the configuration file
94func updateNodeName(client *maas.MAASObject, node MaasNode, options ProcessingOptions) error {
95 macs := node.MACs()
96
97 // Get current node name and strip off domain name
98 current := node.Hostname()
99 if i := strings.IndexRune(current, '.'); i != -1 {
100 current = current[:i]
101 }
102 for _, mac := range macs {
103 if entry, ok := options.Mappings[mac]; ok {
104 if name, ok := entry.(map[string]interface{})["hostname"]; ok && current != name.(string) {
105 nodesObj := client.GetSubObject("nodes")
106 nodeObj := nodesObj.GetSubObject(node.ID())
107 log.Printf("RENAME '%s' to '%s'\n", node.Hostname(), name.(string))
108
109 if !options.Preview {
110 nodeObj.Update(url.Values{"hostname": []string{name.(string)}})
111 }
112 }
113 }
114 }
115 return nil
116}
117
118// Done we are at the target state, nothing to do
119var Done = func(client *maas.MAASObject, node MaasNode, options ProcessingOptions) error {
120 // As devices are normally in the "COMPLETED" state we don't want to
121 // log this fact unless we are in verbose mode. I suspect it would be
122 // nice to log it once when the device transitions from a non COMPLETE
123 // state to a complete state, but that would require keeping state.
124 if options.Verbose {
125 log.Printf("COMPLETE: %s", node.Hostname())
126 }
127
128 if options.AlwaysRename {
129 updateNodeName(client, node, options)
130 }
131
132 return nil
133}
134
135// Deploy cause a node to deploy
136var Deploy = func(client *maas.MAASObject, node MaasNode, options ProcessingOptions) error {
137 log.Printf("DEPLOY: %s", node.Hostname())
138
139 if options.AlwaysRename {
140 updateNodeName(client, node, options)
141 }
142
143 if !options.Preview {
144 nodesObj := client.GetSubObject("nodes")
145 myNode := nodesObj.GetSubObject(node.ID())
146 // Start the node with the trusty distro. This should really be looked up or
147 // a parameter default
148 _, err := myNode.CallPost("start", url.Values {"distro_series" : []string{"trusty"}})
149 if err != nil {
150 log.Printf("ERROR: DEPLOY '%s' : '%s'", node.Hostname(), err)
151 return err
152 }
153 }
154 return nil
155}
156
157// Aquire aquire a machine to a specific operator
158var Aquire = func(client *maas.MAASObject, node MaasNode, options ProcessingOptions) error {
159 log.Printf("AQUIRE: %s", node.Hostname())
160 nodesObj := client.GetSubObject("nodes")
161
162 if options.AlwaysRename {
163 updateNodeName(client, node, options)
164 }
165
166 if !options.Preview {
167 // With a new version of MAAS we have to make sure the node is linked
168 // to the subnet vid DHCP before we move to the Aquire state. To do this
169 // We need to unlink the interface to the subnet and then relink it.
170 //
171 // Iterate through all the interfaces on the node, searching for ones
172 // that are valid and not DHCP and move them to DHCP
173 ifcsObj := client.GetSubObject("nodes").GetSubObject(node.ID()).GetSubObject("interfaces")
174 ifcsListObj, err := ifcsObj.CallGet("", url.Values{})
175 if err != nil {
176 return err
177 }
178
179 ifcsArray, err := ifcsListObj.GetArray()
180 if err != nil {
181 return err
182 }
183
184 for _, ifc := range ifcsArray {
185 ifcMap, err := ifc.GetMap()
186 if err != nil {
187 return err
188 }
189
190 // Iterate over the links assocated with the interface, looking for
191 // links with a subnect as well as a mode of "auto"
192 links, ok := ifcMap["links"]
193 if ok {
194 linkArray, err := links.GetArray()
195 if err != nil {
196 return err
197 }
198
199 for _, link := range linkArray {
200 linkMap, err := link.GetMap()
201 if err != nil {
202 return err
203 }
204 subnet, ok := linkMap["subnet"]
205 if ok {
206 subnetMap, err := subnet.GetMap()
207 if err != nil {
208 return err
209 }
210
211 val, err := linkMap["mode"].GetString()
212 if err != nil {
213 return err
214 }
215
216 if val == "auto" {
217 // Found one we like, so grab the subnet from the data and
218 // then relink this as DHCP
219 cidr, err := subnetMap["cidr"].GetString()
220 if err != nil {
221 return err
222 }
223
224 fifcID, err := ifcMap["id"].GetFloat64()
225 if err != nil {
226 return err
227 }
228 ifcID := strconv.Itoa(int(fifcID))
229
230 flID, err := linkMap["id"].GetFloat64()
231 if err != nil {
232 return err
233 }
234 lID := strconv.Itoa(int(flID))
235
236 ifcObj := ifcsObj.GetSubObject(ifcID)
237 _, err = ifcObj.CallPost("unlink_subnet", url.Values{"id": []string{lID}})
238 if err != nil {
239 return err
240 }
241 _, err = ifcObj.CallPost("link_subnet", url.Values{"mode": []string{"DHCP"}, "subnet": []string{cidr}})
242 if err != nil {
243 return err
244 }
245 }
246 }
247 }
248 }
249 }
250 _, err = nodesObj.CallPost("acquire",
251 url.Values{"name": []string{node.Hostname()}})
252 if err != nil {
253 log.Printf("ERROR: AQUIRE '%s' : '%s'", node.Hostname(), err)
254 return err
255 }
256 }
257 return nil
258}
259
260// Commission cause a node to be commissioned
261var Commission = func(client *maas.MAASObject, node MaasNode, options ProcessingOptions) error {
262 updateNodeName(client, node, options)
263
264 // Need to understand the power state of the node. We only want to move to "Commissioning" if the node
265 // power is off. If the node power is not off, then turn it off.
266 state := node.PowerState()
267 switch state {
268 case "on":
269 // Attempt to turn the node off
270 log.Printf("POWER DOWN: %s", node.Hostname())
271 if !options.Preview {
272 //POST /api/1.0/nodes/{system_id}/ op=stop
273 nodesObj := client.GetSubObject("nodes")
274 nodeObj := nodesObj.GetSubObject(node.ID())
275 _, err := nodeObj.CallPost("stop", url.Values{"stop_mode" : []string{"soft"}})
276 if err != nil {
277 log.Printf("ERROR: Commission '%s' : changing power start to off : '%s'", node.Hostname(), err)
278 }
279 return err
280 }
281 break
282 case "off":
283 // We are off so move to commissioning
284 log.Printf("COMISSION: %s", node.Hostname())
285 if !options.Preview {
286 nodesObj := client.GetSubObject("nodes")
287 nodeObj := nodesObj.GetSubObject(node.ID())
288
289 updateNodeName(client, node, options)
290
291 _, err := nodeObj.CallPost("commission", url.Values{})
292 if err != nil {
293 log.Printf("ERROR: Commission '%s' : '%s'", node.Hostname(), err)
294 }
295 return err
296 }
297 break
298 default:
299 // We are in a state from which we can't move forward.
300 log.Printf("ERROR: %s has invalid power state '%s'", node.Hostname(), state)
301 break
302 }
303 return nil
304}
305
306// Wait a do nothing state, while work is being done
307var Wait = func(client *maas.MAASObject, node MaasNode, options ProcessingOptions) error {
308 log.Printf("WAIT: %s", node.Hostname())
309 return nil
310}
311
312// Fail a state from which we cannot, currently, automatically recover
313var Fail = func(client *maas.MAASObject, node MaasNode, options ProcessingOptions) error {
314 log.Printf("FAIL: %s", node.Hostname())
315 return nil
316}
317
318// AdminState an administrative state from which we should make no automatic transition
319var AdminState = func(client *maas.MAASObject, node MaasNode, options ProcessingOptions) error {
320 log.Printf("ADMIN: %s", node.Hostname())
321 return nil
322}
323
324func findAction(target string, current string) (Action, error) {
325 targets, ok := Transitions[target]
326 if !ok {
327 log.Printf("[warn] unable to find transitions to target state '%s'", target)
328 return nil, fmt.Errorf("Could not find transition to target state '%s'", target)
329 }
330
331 action, ok := targets[current]
332 if !ok {
333 log.Printf("[warn] unable to find transition from current state '%s' to target state '%s'",
334 current, target)
335 return nil, fmt.Errorf("Could not find transition from current state '%s' to target state '%s'",
336 current, target)
337 }
338
339 return action, nil
340}
341
342// ProcessNode something
343func ProcessNode(client *maas.MAASObject, node MaasNode, options ProcessingOptions) error {
344 substatus, err := node.GetInteger("substatus")
345 if err != nil {
346 return err
347 }
348 action, err := findAction("Deployed", MaasNodeStatus(substatus).String())
349 if err != nil {
350 return err
351 }
352
353 if options.Preview {
354 action(client, node, options)
355 } else {
356 go action(client, node, options)
357 }
358 return nil
359}
360
361func buildFilter(filter []string) ([]*regexp.Regexp, error) {
362
363 results := make([]*regexp.Regexp, len(filter))
364 for i, v := range filter {
365 r, err := regexp.Compile(v)
366 if err != nil {
367 return nil, err
368 }
369 results[i] = r
370 }
371 return results, nil
372}
373
374func matchedFilter(include []*regexp.Regexp, target string) bool {
375 for _, e := range include {
376 if e.MatchString(target) {
377 return true
378 }
379 }
380 return false
381}
382
383// ProcessAll something
384func ProcessAll(client *maas.MAASObject, nodes []MaasNode, options ProcessingOptions) []error {
385 errors := make([]error, len(nodes))
386 includeHosts, err := buildFilter(options.Filter.Hosts.Include)
387 if err != nil {
388 log.Fatalf("[error] invalid regular expression for include filter '%s' : %s", options.Filter.Hosts.Include, err)
389 }
390
391 includeZones, err := buildFilter(options.Filter.Zones.Include)
392 if err != nil {
393 log.Fatalf("[error] invalid regular expression for include filter '%v' : %s", options.Filter.Zones.Include, err)
394 }
395
396 for i, node := range nodes {
397 // For hostnames we always match on an empty filter
398 if len(includeHosts) >= 0 && matchedFilter(includeHosts, node.Hostname()) {
399
400 // For zones we don't match on an empty filter
401 if len(includeZones) >= 0 && matchedFilter(includeZones, node.Zone()) {
402 err := ProcessNode(client, node, options)
403 if err != nil {
404 errors[i] = err
405 } else {
406 errors[i] = nil
407 }
408 } else {
409 if options.Verbose {
410 log.Printf("[info] ignoring node '%s' as its zone '%s' didn't match include zone name filter '%v'",
411 node.Hostname(), node.Zone(), options.Filter.Zones.Include)
412 }
413 }
414 } else {
415 if options.Verbose {
416 log.Printf("[info] ignoring node '%s' as it didn't match include hostname filter '%v'",
417 node.Hostname(), options.Filter.Hosts.Include)
418 }
419 }
420 }
421 return errors
422}