blob: 1514e32aed073ce3e7f078600143ea6e683ddefd [file] [log] [blame]
David K. Bainbridgeb5415042016-05-13 17:06:10 -07001package main
2
3import (
4 "encoding/json"
5 "flag"
6 "log"
7 "net/url"
8 "os"
9 "strings"
10 "time"
11 "unicode"
12
13 maas "github.com/juju/gomaasapi"
14)
15
16const (
17 // defaultFilter specifies the default filter to use when none is specified
18 defaultFilter = `{
19 "hosts" : {
20 "include" : [ ".*" ],
21 "exclude" : []
22 },
23 "zones" : {
24 "include" : ["default"],
25 "exclude" : []
26 }
27 }`
28 defaultMapping = "{}"
29)
30
31var apiKey = flag.String("apikey", "", "key with which to access MAAS server")
32var maasURL = flag.String("maas", "http://localhost/MAAS", "url over which to access MAAS")
33var apiVersion = flag.String("apiVersion", "1.0", "version of the API to access")
34var queryPeriod = flag.String("period", "15s", "frequency the MAAS service is polled for node states")
35var preview = flag.Bool("preview", false, "displays the action that would be taken, but does not do the action, in this mode the nodes are processed only once")
36var mappings = flag.String("mappings", "{}", "the mac to name mappings")
37var always = flag.Bool("always-rename", true, "attempt to rename at every stage of workflow")
38var verbose = flag.Bool("verbose", false, "display verbose logging")
39var filterSpec = flag.String("filter", strings.Map(func(r rune) rune {
40 if unicode.IsSpace(r) {
41 return -1
42 }
43 return r
44}, defaultFilter), "constrain by hostname what will be automated")
45
46// checkError if the given err is not nil, then fatally log the message, else
47// return false.
48func checkError(err error, message string, v ...interface{}) bool {
49 if err != nil {
50 log.Fatalf("[error] "+message, v)
51 }
52 return false
53}
54
55// checkWarn if the given err is not nil, then log the message as a warning and
56// return true, else return false.
57func checkWarn(err error, message string, v ...interface{}) bool {
58 if err != nil {
59 log.Printf("[warn] "+message, v)
60 return true
61 }
62 return false
63}
64
65// fetchNodes do a HTTP GET to the MAAS server to query all the nodes
66func fetchNodes(client *maas.MAASObject) ([]MaasNode, error) {
67 nodeListing := client.GetSubObject("nodes")
68 listNodeObjects, err := nodeListing.CallGet("list", url.Values{})
69 if checkWarn(err, "unable to get the list of all nodes: %s", err) {
70 return nil, err
71 }
72 listNodes, err := listNodeObjects.GetArray()
73 if checkWarn(err, "unable to get the node objects for the list: %s", err) {
74 return nil, err
75 }
76
77 var nodes = make([]MaasNode, len(listNodes))
78 for index, nodeObj := range listNodes {
79 node, err := nodeObj.GetMAASObject()
80 if !checkWarn(err, "unable to retrieve object for node: %s", err) {
81 nodes[index] = MaasNode{node}
82 }
83 }
84 return nodes, nil
85}
86
87func main() {
88
89 flag.Parse()
90
91 options := ProcessingOptions{
92 Preview: *preview,
93 Verbose: *verbose,
94 AlwaysRename: *always,
95 }
96
97 // Determine the filter, this can either be specified on the the command
98 // line as a value or a file reference. If none is specified the default
99 // will be used
100 if len(*filterSpec) > 0 {
101 if (*filterSpec)[0] == '@' {
102 name := os.ExpandEnv((*filterSpec)[1:])
103 file, err := os.OpenFile(name, os.O_RDONLY, 0)
104 checkError(err, "[error] unable to open file '%s' to load the filter : %s", name, err)
105 decoder := json.NewDecoder(file)
106 err = decoder.Decode(&options.Filter)
107 checkError(err, "[error] unable to parse filter configuration from file '%s' : %s", name, err)
108 } else {
109 err := json.Unmarshal([]byte(*filterSpec), &options.Filter)
110 checkError(err, "[error] unable to parse filter specification: '%s' : %s", *filterSpec, err)
111 }
112 } else {
113 err := json.Unmarshal([]byte(defaultFilter), &options.Filter)
114 checkError(err, "[error] unable to parse default filter specificiation: '%s' : %s", defaultFilter, err)
115 }
116
117 // Determine the mac to name mapping, this can either be specified on the the command
118 // line as a value or a file reference. If none is specified the default
119 // will be used
120 if len(*mappings) > 0 {
121 if (*mappings)[0] == '@' {
122 name := os.ExpandEnv((*mappings)[1:])
123 file, err := os.OpenFile(name, os.O_RDONLY, 0)
124 checkError(err, "[error] unable to open file '%s' to load the mac name mapping : %s", name, err)
125 decoder := json.NewDecoder(file)
126 err = decoder.Decode(&options.Mappings)
127 checkError(err, "[error] unable to parse filter configuration from file '%s' : %s", name, err)
128 } else {
129 err := json.Unmarshal([]byte(*mappings), &options.Mappings)
130 checkError(err, "[error] unable to parse mac name mapping: '%s' : %s", *mappings, err)
131 }
132 } else {
133 err := json.Unmarshal([]byte(defaultMapping), &options.Mappings)
134 checkError(err, "[error] unable to parse default mac name mappings: '%s' : %s", defaultMapping, err)
135 }
136
137 // Verify the specified period for queries can be converted into a Go duration
138 period, err := time.ParseDuration(*queryPeriod)
139 checkError(err, "[error] unable to parse specified query period duration: '%s': %s", queryPeriod, err)
140
141 authClient, err := maas.NewAuthenticatedClient(*maasURL, *apiKey, *apiVersion)
142 if err != nil {
143 checkError(err, "[error] Unable to use specified client key, '%s', to authenticate to the MAAS server: %s", *apiKey, err)
144 }
145
146 // Create an object through which we will communicate with MAAS
147 client := maas.NewMAAS(*authClient)
148
149 // This utility essentially polls the MAAS server for node state and
150 // process the node to the next state. This is done by kicking off the
151 // process every specified duration. This means that the first processing of
152 // nodes will have "period" in the future. This is really not the behavior
153 // we want, we really want, do it now, and then do the next one in "period".
154 // So, the code does one now.
155 nodes, _ := fetchNodes(client)
156 ProcessAll(client, nodes, options)
157
158 if !(*preview) {
159 // Create a ticker and fetch and process the nodes every "period"
160 ticker := time.NewTicker(period)
161 for t := range ticker.C {
162 log.Printf("[info] query server at %s", t)
163 nodes, _ := fetchNodes(client)
164 ProcessAll(client, nodes, options)
165 }
166 }
167}