blob: 4e0dd1fc357748481929cba9eec1d2b20e7bdf76 [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
29// ProcessingOptions used to determine on what hosts to operate
30type ProcessingOptions struct {
31 Filter struct {
32 Zones struct {
33 Include []string
34 Exclude []string
35 }
36 Hosts struct {
37 Include []string
38 Exclude []string
39 }
40 }
41 Mappings map[string]interface{}
42 Verbose bool
43 Preview bool
44 AlwaysRename bool
David K. Bainbridgeefa951d2016-05-26 10:54:25 -070045 ProvTracker Tracker
46 ProvisionURL string
47 ProvisionTTL time.Duration
David K. Bainbridgeb5415042016-05-13 17:06:10 -070048}
49
50// Transitions the actual map
51//
52// Currently this is a hand compiled / optimized "next step" table. This should
53// really be generated from the state machine chart input. Once this has been
54// accomplished you should be able to determine the action to take given your
55// target state and your current state.
David K. Bainbridgeefa951d2016-05-26 10:54:25 -070056var Transitions = map[string]map[string][]Action{
David K. Bainbridgeb5415042016-05-13 17:06:10 -070057 "Deployed": {
David K. Bainbridgeefa951d2016-05-26 10:54:25 -070058 "New": []Action{Reset, Commission},
59 "Deployed": []Action{Provision, Done},
60 "Ready": []Action{Reset, Aquire},
61 "Allocated": []Action{Reset, Deploy},
62 "Retired": []Action{Reset, AdminState},
63 "Reserved": []Action{Reset, AdminState},
64 "Releasing": []Action{Reset, Wait},
65 "DiskErasing": []Action{Reset, Wait},
66 "Deploying": []Action{Reset, Wait},
67 "Commissioning": []Action{Reset, Wait},
68 "Missing": []Action{Reset, Fail},
69 "FailedReleasing": []Action{Reset, Fail},
70 "FailedDiskErasing": []Action{Reset, Fail},
71 "FailedDeployment": []Action{Reset, Fail},
72 "Broken": []Action{Reset, Fail},
73 "FailedCommissioning": []Action{Reset, Fail},
David K. Bainbridgeb5415042016-05-13 17:06:10 -070074 },
75}
76
77const (
78 // defaultStateMachine Would be nice to drive from a graph language
79 defaultStateMachine string = `
David K. Bainbridged9b966f2016-05-31 13:30:05 -070080 (New)->(Commissioning)
David K. Bainbridgeb5415042016-05-13 17:06:10 -070081 (Commissioning)->(FailedCommissioning)
82 (FailedCommissioning)->(New)
83 (Commissioning)->(Ready)
84 (Ready)->(Deploying)
85 (Ready)->(Allocated)
86 (Allocated)->(Deploying)
87 (Deploying)->(Deployed)
88 (Deploying)->(FailedDeployment)
89 (FailedDeployment)->(Broken)
90 (Deployed)->(Releasing)
91 (Releasing)->(FailedReleasing)
92 (FailedReleasing)->(Broken)
93 (Releasing)->(DiskErasing)
94 (DiskErasing)->(FailedEraseDisk)
95 (FailedEraseDisk)->(Broken)
96 (Releasing)->(Ready)
97 (DiskErasing)->(Ready)
David K. Bainbridgeefa951d2016-05-26 10:54:25 -070098 (Broken)->(Ready)
David K. Bainbridged9b966f2016-05-31 13:30:05 -070099 (Deployed)->(Provisioning)
100 (Provisioning)->|a|
101 |a|->(Execute Script)->|b|
102 |a|->(HTTP PUT)
103 (HTTP PUT)->(HTTP GET)
104 (HTTP GET)->(HTTP GET)
105 (HTTP GET)->|b|
106 |b|->(Provisioned)
107 |b|->(ProvisionError)
108 (ProvisionError)->(Provisioning)`
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700109)
110
111// updateName - changes the name of the MAAS node based on the configuration file
112func updateNodeName(client *maas.MAASObject, node MaasNode, options ProcessingOptions) error {
113 macs := node.MACs()
114
115 // Get current node name and strip off domain name
116 current := node.Hostname()
117 if i := strings.IndexRune(current, '.'); i != -1 {
118 current = current[:i]
119 }
120 for _, mac := range macs {
121 if entry, ok := options.Mappings[mac]; ok {
122 if name, ok := entry.(map[string]interface{})["hostname"]; ok && current != name.(string) {
123 nodesObj := client.GetSubObject("nodes")
124 nodeObj := nodesObj.GetSubObject(node.ID())
125 log.Printf("RENAME '%s' to '%s'\n", node.Hostname(), name.(string))
126
127 if !options.Preview {
128 nodeObj.Update(url.Values{"hostname": []string{name.(string)}})
129 }
130 }
131 }
132 }
133 return nil
134}
135
David K. Bainbridgeefa951d2016-05-26 10:54:25 -0700136// Reset we are at the target state, nothing to do
137var Reset = func(client *maas.MAASObject, node MaasNode, options ProcessingOptions) error {
138 if options.Verbose {
139 log.Printf("RESET: %s", node.Hostname())
140 }
141
142 if options.AlwaysRename {
143 updateNodeName(client, node, options)
144 }
145
146 options.ProvTracker.Clear(node.ID())
147
148 return nil
149}
150
151// Provision we are at the target state, nothing to do
152var Provision = func(client *maas.MAASObject, node MaasNode, options ProcessingOptions) error {
153 if options.Verbose {
154 log.Printf("CHECK PROVISION: %s", node.Hostname())
155 }
156
157 if options.AlwaysRename {
158 updateNodeName(client, node, options)
159 }
160
161 record, err := options.ProvTracker.Get(node.ID())
162 if options.Verbose {
163 log.Printf("[info] Current state of node '%s' is '%s'", node.Hostname(), record.State.String())
164 }
165 if err != nil {
166 log.Printf("[warn] unable to retrieve provisioning state of node '%s' : %s", node.Hostname(), err)
167 } else if record.State == Unprovisioned || record.State == ProvisionError {
168 var err error = nil
169 var callout *url.URL
170 log.Printf("PROVISION '%s'", node.Hostname())
171 if len(options.ProvisionURL) > 0 {
172 if options.Verbose {
173 log.Printf("[info] Provisioning callout to '%s'", options.ProvisionURL)
174 }
175 callout, err = url.Parse(options.ProvisionURL)
176 if err != nil {
177 log.Printf("[error] Failed to parse provisioning URL '%s' : %s", options.ProvisionURL, err)
178 } else {
179 ips := node.IPs()
180 ip := ""
181 if len(ips) > 0 {
182 ip = ips[0]
183 }
David K. Bainbridged9b966f2016-05-31 13:30:05 -0700184 macs := node.MACs()
185 mac := ""
186 if len(macs) > 0 {
187 mac = macs[0]
188 }
David K. Bainbridgeefa951d2016-05-26 10:54:25 -0700189 switch callout.Scheme {
190 // If the scheme is a file, then we will execute the refereced file
191 case "", "file":
192 if options.Verbose {
193 log.Printf("[info] executing local script file '%s'", callout.Path)
194 }
195 record.State = Provisioning
196 record.Timestamp = time.Now().Unix()
197 options.ProvTracker.Set(node.ID(), record)
David K. Bainbridged9b966f2016-05-31 13:30:05 -0700198 err = exec.Command(callout.Path, node.ID(), node.Hostname(), ip, mac).Run()
David K. Bainbridgeefa951d2016-05-26 10:54:25 -0700199 if err != nil {
200 log.Printf("[error] Failed to execute '%s' : %s", options.ProvisionURL, err)
201 } else {
202 if options.Verbose {
203 log.Printf("[info] Marking node '%s' with ID '%s' as provisioned",
204 node.Hostname(), node.ID())
205 }
206 record.State = Provisioned
207 options.ProvTracker.Set(node.ID(), record)
208 }
209
210 default:
211 if options.Verbose {
212 log.Printf("[info] POSTing to '%s'", options.ProvisionURL)
213 }
214 data := map[string]string{
215 "id": node.ID(),
216 "name": node.Hostname(),
217 "ip": ip,
David K. Bainbridged9b966f2016-05-31 13:30:05 -0700218 "mac": mac,
David K. Bainbridgeefa951d2016-05-26 10:54:25 -0700219 }
220 hc := http.Client{}
221 var b []byte
222 b, err = json.Marshal(data)
223 if err != nil {
224 log.Printf("[error] Unable to marshal node data : %s", err)
225 } else {
226 var req *http.Request
227 var resp *http.Response
228 if options.Verbose {
229 log.Printf("[debug] POSTing data '%s'", string(b))
230 }
231 req, err = http.NewRequest("POST", options.ProvisionURL, bytes.NewReader(b))
232 if err != nil {
233 log.Printf("[error] Unable to construct POST request to provisioner : %s",
234 err)
235 } else {
236 req.Header.Add("Content-Type", "application/json")
237 resp, err = hc.Do(req)
238 if err != nil {
239 log.Printf("[error] Unable to process POST request : %s",
240 err)
241 } else {
242 defer resp.Body.Close()
David K. Bainbridge8352c592016-06-02 12:48:37 -0700243 if resp.StatusCode == http.StatusAccepted {
David K. Bainbridgeefa951d2016-05-26 10:54:25 -0700244 record.State = Provisioning
245 } else {
246 record.State = ProvisionError
247 }
David K. Bainbridge8352c592016-06-02 12:48:37 -0700248 record.Timestamp = time.Now().Unix()
David K. Bainbridgeefa951d2016-05-26 10:54:25 -0700249 options.ProvTracker.Set(node.ID(), record)
250 }
251 }
252 }
253 }
254 }
255 }
256
257 if err != nil {
258 if options.Verbose {
259 log.Printf("[warn] Not marking node '%s' with ID '%s' as provisioned, because of error '%s'",
260 node.Hostname(), node.ID(), err)
261 record.State = ProvisionError
262 options.ProvTracker.Set(node.ID(), record)
263 }
264 }
265 } else if record.State == Provisioning && time.Since(time.Unix(record.Timestamp, 0)) > options.ProvisionTTL {
266 log.Printf("[error] Provisioning of node '%s' has passed provisioning TTL of '%v'",
267 node.Hostname(), options.ProvisionTTL)
268 record.State = ProvisionError
269 options.ProvTracker.Set(node.ID(), record)
270 } else if record.State == Provisioning {
271 callout, err := url.Parse(options.ProvisionURL)
272 if err != nil {
273 log.Printf("[error] Unable to parse provisioning URL '%s' : %s", options.ProvisionURL, err)
274 } else if callout.Scheme != "file" {
275 var req *http.Request
276 var resp *http.Response
277 if options.Verbose {
278 log.Printf("[info] Fetching provisioning state for node '%s'", node.Hostname())
279 }
280 req, err = http.NewRequest("GET", options.ProvisionURL+"/"+node.ID(), nil)
281 if err != nil {
282 log.Printf("[error] Unable to construct GET request to provisioner : %s", err)
283 } else {
284 hc := http.Client{}
285 resp, err = hc.Do(req)
286 if err != nil {
287 log.Printf("[error] Failed to quest provision state for node '%s' : %s",
288 node.Hostname(), err)
289 } else {
290 switch resp.StatusCode {
David K. Bainbridge8352c592016-06-02 12:48:37 -0700291 case http.StatusOK: // provisioning completed
David K. Bainbridgeefa951d2016-05-26 10:54:25 -0700292 if options.Verbose {
293 log.Printf("[info] Marking node '%s' with ID '%s' as provisioned",
294 node.Hostname(), node.ID())
295 }
296 record.State = Provisioned
297 options.ProvTracker.Set(node.ID(), record)
David K. Bainbridge8352c592016-06-02 12:48:37 -0700298 case http.StatusAccepted: // in the provisioning state
David K. Bainbridgeefa951d2016-05-26 10:54:25 -0700299 // Noop, presumably alread in this state
300 default: // Consider anything else an erorr
David K. Bainbridge8352c592016-06-02 12:48:37 -0700301 log.Printf("[warn] Node '%s' with ID '%s' failed provisioning, will retry",
302 node.Hostname(), node.ID())
David K. Bainbridgeefa951d2016-05-26 10:54:25 -0700303 record.State = ProvisionError
304 options.ProvTracker.Set(node.ID(), record)
305 }
306 }
307 }
308 }
309 } else if options.Verbose {
310 log.Printf("[info] Not invoking provisioning for '%s', currned state is '%s'", node.Hostname(),
311 record.State.String())
312 }
313
314 return nil
315}
316
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700317// Done we are at the target state, nothing to do
318var Done = func(client *maas.MAASObject, node MaasNode, options ProcessingOptions) error {
319 // As devices are normally in the "COMPLETED" state we don't want to
320 // log this fact unless we are in verbose mode. I suspect it would be
321 // nice to log it once when the device transitions from a non COMPLETE
322 // state to a complete state, but that would require keeping state.
323 if options.Verbose {
324 log.Printf("COMPLETE: %s", node.Hostname())
325 }
326
327 if options.AlwaysRename {
328 updateNodeName(client, node, options)
329 }
330
331 return nil
332}
333
334// Deploy cause a node to deploy
335var Deploy = func(client *maas.MAASObject, node MaasNode, options ProcessingOptions) error {
336 log.Printf("DEPLOY: %s", node.Hostname())
337
338 if options.AlwaysRename {
339 updateNodeName(client, node, options)
340 }
341
342 if !options.Preview {
343 nodesObj := client.GetSubObject("nodes")
344 myNode := nodesObj.GetSubObject(node.ID())
345 // Start the node with the trusty distro. This should really be looked up or
346 // a parameter default
David K. Bainbridgeefa951d2016-05-26 10:54:25 -0700347 _, err := myNode.CallPost("start", url.Values{"distro_series": []string{"trusty"}})
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700348 if err != nil {
349 log.Printf("ERROR: DEPLOY '%s' : '%s'", node.Hostname(), err)
350 return err
351 }
352 }
353 return nil
354}
355
356// Aquire aquire a machine to a specific operator
357var Aquire = func(client *maas.MAASObject, node MaasNode, options ProcessingOptions) error {
358 log.Printf("AQUIRE: %s", node.Hostname())
359 nodesObj := client.GetSubObject("nodes")
360
361 if options.AlwaysRename {
362 updateNodeName(client, node, options)
363 }
364
365 if !options.Preview {
366 // With a new version of MAAS we have to make sure the node is linked
367 // to the subnet vid DHCP before we move to the Aquire state. To do this
368 // We need to unlink the interface to the subnet and then relink it.
369 //
370 // Iterate through all the interfaces on the node, searching for ones
371 // that are valid and not DHCP and move them to DHCP
372 ifcsObj := client.GetSubObject("nodes").GetSubObject(node.ID()).GetSubObject("interfaces")
373 ifcsListObj, err := ifcsObj.CallGet("", url.Values{})
374 if err != nil {
375 return err
376 }
377
378 ifcsArray, err := ifcsListObj.GetArray()
379 if err != nil {
380 return err
381 }
382
383 for _, ifc := range ifcsArray {
384 ifcMap, err := ifc.GetMap()
385 if err != nil {
386 return err
387 }
388
389 // Iterate over the links assocated with the interface, looking for
390 // links with a subnect as well as a mode of "auto"
391 links, ok := ifcMap["links"]
392 if ok {
393 linkArray, err := links.GetArray()
394 if err != nil {
395 return err
396 }
397
398 for _, link := range linkArray {
399 linkMap, err := link.GetMap()
400 if err != nil {
401 return err
402 }
403 subnet, ok := linkMap["subnet"]
404 if ok {
405 subnetMap, err := subnet.GetMap()
406 if err != nil {
407 return err
408 }
409
410 val, err := linkMap["mode"].GetString()
411 if err != nil {
412 return err
413 }
414
415 if val == "auto" {
416 // Found one we like, so grab the subnet from the data and
417 // then relink this as DHCP
418 cidr, err := subnetMap["cidr"].GetString()
419 if err != nil {
420 return err
421 }
422
423 fifcID, err := ifcMap["id"].GetFloat64()
424 if err != nil {
425 return err
426 }
427 ifcID := strconv.Itoa(int(fifcID))
428
429 flID, err := linkMap["id"].GetFloat64()
430 if err != nil {
431 return err
432 }
433 lID := strconv.Itoa(int(flID))
434
435 ifcObj := ifcsObj.GetSubObject(ifcID)
436 _, err = ifcObj.CallPost("unlink_subnet", url.Values{"id": []string{lID}})
437 if err != nil {
438 return err
439 }
440 _, err = ifcObj.CallPost("link_subnet", url.Values{"mode": []string{"DHCP"}, "subnet": []string{cidr}})
441 if err != nil {
442 return err
443 }
444 }
445 }
446 }
447 }
448 }
449 _, err = nodesObj.CallPost("acquire",
450 url.Values{"name": []string{node.Hostname()}})
451 if err != nil {
452 log.Printf("ERROR: AQUIRE '%s' : '%s'", node.Hostname(), err)
453 return err
454 }
455 }
456 return nil
457}
458
459// Commission cause a node to be commissioned
460var Commission = func(client *maas.MAASObject, node MaasNode, options ProcessingOptions) error {
461 updateNodeName(client, node, options)
462
463 // Need to understand the power state of the node. We only want to move to "Commissioning" if the node
464 // power is off. If the node power is not off, then turn it off.
465 state := node.PowerState()
466 switch state {
467 case "on":
468 // Attempt to turn the node off
469 log.Printf("POWER DOWN: %s", node.Hostname())
470 if !options.Preview {
David K. Bainbridgeefa951d2016-05-26 10:54:25 -0700471 //POST /api/1.0/nodes/{system_id}/ op=stop
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700472 nodesObj := client.GetSubObject("nodes")
473 nodeObj := nodesObj.GetSubObject(node.ID())
David K. Bainbridgeefa951d2016-05-26 10:54:25 -0700474 _, err := nodeObj.CallPost("stop", url.Values{"stop_mode": []string{"soft"}})
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700475 if err != nil {
476 log.Printf("ERROR: Commission '%s' : changing power start to off : '%s'", node.Hostname(), err)
477 }
478 return err
479 }
480 break
481 case "off":
482 // We are off so move to commissioning
483 log.Printf("COMISSION: %s", node.Hostname())
484 if !options.Preview {
485 nodesObj := client.GetSubObject("nodes")
486 nodeObj := nodesObj.GetSubObject(node.ID())
487
488 updateNodeName(client, node, options)
489
490 _, err := nodeObj.CallPost("commission", url.Values{})
491 if err != nil {
492 log.Printf("ERROR: Commission '%s' : '%s'", node.Hostname(), err)
493 }
494 return err
495 }
496 break
497 default:
498 // We are in a state from which we can't move forward.
499 log.Printf("ERROR: %s has invalid power state '%s'", node.Hostname(), state)
500 break
501 }
502 return nil
503}
504
505// Wait a do nothing state, while work is being done
506var Wait = func(client *maas.MAASObject, node MaasNode, options ProcessingOptions) error {
507 log.Printf("WAIT: %s", node.Hostname())
508 return nil
509}
510
511// Fail a state from which we cannot, currently, automatically recover
512var Fail = func(client *maas.MAASObject, node MaasNode, options ProcessingOptions) error {
513 log.Printf("FAIL: %s", node.Hostname())
514 return nil
515}
516
517// AdminState an administrative state from which we should make no automatic transition
518var AdminState = func(client *maas.MAASObject, node MaasNode, options ProcessingOptions) error {
519 log.Printf("ADMIN: %s", node.Hostname())
520 return nil
521}
522
David K. Bainbridgeefa951d2016-05-26 10:54:25 -0700523func findActions(target string, current string) ([]Action, error) {
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700524 targets, ok := Transitions[target]
525 if !ok {
526 log.Printf("[warn] unable to find transitions to target state '%s'", target)
527 return nil, fmt.Errorf("Could not find transition to target state '%s'", target)
528 }
529
David K. Bainbridgeefa951d2016-05-26 10:54:25 -0700530 actions, ok := targets[current]
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700531 if !ok {
532 log.Printf("[warn] unable to find transition from current state '%s' to target state '%s'",
533 current, target)
534 return nil, fmt.Errorf("Could not find transition from current state '%s' to target state '%s'",
535 current, target)
536 }
537
David K. Bainbridgeefa951d2016-05-26 10:54:25 -0700538 return actions, nil
539}
540
541// ProcessActions
542func ProcessActions(actions []Action, client *maas.MAASObject, node MaasNode, options ProcessingOptions) error {
543 var err error
544 for _, action := range actions {
545 if err = action(client, node, options); err != nil {
546 log.Printf("[error] Error while processing action for node '%s' : %s", node.Hostname, err)
547 break
548 }
549 }
550 return err
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700551}
552
553// ProcessNode something
554func ProcessNode(client *maas.MAASObject, node MaasNode, options ProcessingOptions) error {
555 substatus, err := node.GetInteger("substatus")
556 if err != nil {
557 return err
558 }
David K. Bainbridgeefa951d2016-05-26 10:54:25 -0700559 actions, err := findActions("Deployed", MaasNodeStatus(substatus).String())
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700560 if err != nil {
561 return err
562 }
563
564 if options.Preview {
David K. Bainbridgeefa951d2016-05-26 10:54:25 -0700565 ProcessActions(actions, client, node, options)
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700566 } else {
David K. Bainbridgeefa951d2016-05-26 10:54:25 -0700567 go ProcessActions(actions, client, node, options)
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700568 }
569 return nil
570}
571
572func buildFilter(filter []string) ([]*regexp.Regexp, error) {
573
574 results := make([]*regexp.Regexp, len(filter))
575 for i, v := range filter {
576 r, err := regexp.Compile(v)
577 if err != nil {
578 return nil, err
579 }
580 results[i] = r
581 }
582 return results, nil
583}
584
585func matchedFilter(include []*regexp.Regexp, target string) bool {
586 for _, e := range include {
587 if e.MatchString(target) {
588 return true
589 }
590 }
591 return false
592}
593
594// ProcessAll something
595func ProcessAll(client *maas.MAASObject, nodes []MaasNode, options ProcessingOptions) []error {
596 errors := make([]error, len(nodes))
597 includeHosts, err := buildFilter(options.Filter.Hosts.Include)
598 if err != nil {
599 log.Fatalf("[error] invalid regular expression for include filter '%s' : %s", options.Filter.Hosts.Include, err)
600 }
601
602 includeZones, err := buildFilter(options.Filter.Zones.Include)
603 if err != nil {
604 log.Fatalf("[error] invalid regular expression for include filter '%v' : %s", options.Filter.Zones.Include, err)
605 }
606
607 for i, node := range nodes {
608 // For hostnames we always match on an empty filter
609 if len(includeHosts) >= 0 && matchedFilter(includeHosts, node.Hostname()) {
610
611 // For zones we don't match on an empty filter
612 if len(includeZones) >= 0 && matchedFilter(includeZones, node.Zone()) {
613 err := ProcessNode(client, node, options)
614 if err != nil {
615 errors[i] = err
616 } else {
617 errors[i] = nil
618 }
619 } else {
620 if options.Verbose {
621 log.Printf("[info] ignoring node '%s' as its zone '%s' didn't match include zone name filter '%v'",
622 node.Hostname(), node.Zone(), options.Filter.Zones.Include)
623 }
624 }
625 } else {
626 if options.Verbose {
627 log.Printf("[info] ignoring node '%s' as it didn't match include hostname filter '%v'",
628 node.Hostname(), options.Filter.Hosts.Include)
629 }
630 }
631 }
632 return errors
633}