blob: 1f501b95efcb0c7d81d8fd405f40ece5d3b30bac [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 = "{}"
David K. Bainbridgeefa951d2016-05-26 10:54:25 -070029 PROVISION_URL = "PROVISION_URL"
30 PROVISION_TTL = "PROVISION_TTL"
31 DEFAULT_TTL = "30m"
David K. Bainbridgeb5415042016-05-13 17:06:10 -070032)
33
34var apiKey = flag.String("apikey", "", "key with which to access MAAS server")
35var maasURL = flag.String("maas", "http://localhost/MAAS", "url over which to access MAAS")
36var apiVersion = flag.String("apiVersion", "1.0", "version of the API to access")
37var queryPeriod = flag.String("period", "15s", "frequency the MAAS service is polled for node states")
38var 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")
39var mappings = flag.String("mappings", "{}", "the mac to name mappings")
40var always = flag.Bool("always-rename", true, "attempt to rename at every stage of workflow")
41var verbose = flag.Bool("verbose", false, "display verbose logging")
42var filterSpec = flag.String("filter", strings.Map(func(r rune) rune {
43 if unicode.IsSpace(r) {
44 return -1
45 }
46 return r
47}, defaultFilter), "constrain by hostname what will be automated")
48
49// checkError if the given err is not nil, then fatally log the message, else
50// return false.
51func checkError(err error, message string, v ...interface{}) bool {
52 if err != nil {
53 log.Fatalf("[error] "+message, v)
54 }
55 return false
56}
57
58// checkWarn if the given err is not nil, then log the message as a warning and
59// return true, else return false.
60func checkWarn(err error, message string, v ...interface{}) bool {
61 if err != nil {
62 log.Printf("[warn] "+message, v)
63 return true
64 }
65 return false
66}
67
68// fetchNodes do a HTTP GET to the MAAS server to query all the nodes
69func fetchNodes(client *maas.MAASObject) ([]MaasNode, error) {
70 nodeListing := client.GetSubObject("nodes")
71 listNodeObjects, err := nodeListing.CallGet("list", url.Values{})
72 if checkWarn(err, "unable to get the list of all nodes: %s", err) {
73 return nil, err
74 }
75 listNodes, err := listNodeObjects.GetArray()
76 if checkWarn(err, "unable to get the node objects for the list: %s", err) {
77 return nil, err
78 }
79
80 var nodes = make([]MaasNode, len(listNodes))
81 for index, nodeObj := range listNodes {
82 node, err := nodeObj.GetMAASObject()
83 if !checkWarn(err, "unable to retrieve object for node: %s", err) {
84 nodes[index] = MaasNode{node}
85 }
86 }
87 return nodes, nil
88}
89
90func main() {
91
92 flag.Parse()
93
94 options := ProcessingOptions{
95 Preview: *preview,
96 Verbose: *verbose,
97 AlwaysRename: *always,
David K. Bainbridgeefa951d2016-05-26 10:54:25 -070098 ProvTracker: NewTracker(),
99 ProvisionURL: os.Getenv(PROVISION_URL),
100 }
101
102 var ttl string
103 if ttl = os.Getenv(PROVISION_TTL); ttl == "" {
104 ttl = "30m"
105 }
106
107 var err error
108 options.ProvisionTTL, err = time.ParseDuration(ttl)
109 if err != nil {
110 log.Printf("[warn] unable to parse specified duration of '%s', defaulting to '%s'",
111 ttl, DEFAULT_TTL)
David K. Bainbridge37ccf1e2016-06-02 12:47:15 -0700112 options.ProvisionTTL, err = time.ParseDuration("30m")
113 checkError(err, "[error] unable to parse default TTL duration of '30m' : %s", err)
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700114 }
115
116 // Determine the filter, this can either be specified on the the command
117 // line as a value or a file reference. If none is specified the default
118 // will be used
119 if len(*filterSpec) > 0 {
120 if (*filterSpec)[0] == '@' {
121 name := os.ExpandEnv((*filterSpec)[1:])
122 file, err := os.OpenFile(name, os.O_RDONLY, 0)
123 checkError(err, "[error] unable to open file '%s' to load the filter : %s", name, err)
124 decoder := json.NewDecoder(file)
125 err = decoder.Decode(&options.Filter)
126 checkError(err, "[error] unable to parse filter configuration from file '%s' : %s", name, err)
127 } else {
128 err := json.Unmarshal([]byte(*filterSpec), &options.Filter)
129 checkError(err, "[error] unable to parse filter specification: '%s' : %s", *filterSpec, err)
130 }
131 } else {
132 err := json.Unmarshal([]byte(defaultFilter), &options.Filter)
133 checkError(err, "[error] unable to parse default filter specificiation: '%s' : %s", defaultFilter, err)
134 }
135
136 // Determine the mac to name mapping, this can either be specified on the the command
137 // line as a value or a file reference. If none is specified the default
138 // will be used
139 if len(*mappings) > 0 {
140 if (*mappings)[0] == '@' {
141 name := os.ExpandEnv((*mappings)[1:])
142 file, err := os.OpenFile(name, os.O_RDONLY, 0)
143 checkError(err, "[error] unable to open file '%s' to load the mac name mapping : %s", name, err)
144 decoder := json.NewDecoder(file)
145 err = decoder.Decode(&options.Mappings)
146 checkError(err, "[error] unable to parse filter configuration from file '%s' : %s", name, err)
147 } else {
148 err := json.Unmarshal([]byte(*mappings), &options.Mappings)
149 checkError(err, "[error] unable to parse mac name mapping: '%s' : %s", *mappings, err)
150 }
151 } else {
152 err := json.Unmarshal([]byte(defaultMapping), &options.Mappings)
153 checkError(err, "[error] unable to parse default mac name mappings: '%s' : %s", defaultMapping, err)
154 }
155
156 // Verify the specified period for queries can be converted into a Go duration
157 period, err := time.ParseDuration(*queryPeriod)
158 checkError(err, "[error] unable to parse specified query period duration: '%s': %s", queryPeriod, err)
159
David K. Bainbridge37ccf1e2016-06-02 12:47:15 -0700160 log.Printf(`Configuration:
161 MAAS URL: %s
162 MAAS API Version: %s
163 MAAS Query Interval: %s
164 Node Filtter: %s
165 Node Name Mappings: %s
166 Preview: %v
167 Verbose: %v
168 Always Rename: %v
169 Provision URL: %s `,
170 *maasURL, *apiVersion, *queryPeriod, *filterSpec, *mappings, options.Preview,
171 options.Verbose, options.AlwaysRename, options.ProvisionURL)
172
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700173 authClient, err := maas.NewAuthenticatedClient(*maasURL, *apiKey, *apiVersion)
174 if err != nil {
175 checkError(err, "[error] Unable to use specified client key, '%s', to authenticate to the MAAS server: %s", *apiKey, err)
176 }
177
178 // Create an object through which we will communicate with MAAS
179 client := maas.NewMAAS(*authClient)
180
181 // This utility essentially polls the MAAS server for node state and
182 // process the node to the next state. This is done by kicking off the
183 // process every specified duration. This means that the first processing of
184 // nodes will have "period" in the future. This is really not the behavior
185 // we want, we really want, do it now, and then do the next one in "period".
186 // So, the code does one now.
187 nodes, _ := fetchNodes(client)
188 ProcessAll(client, nodes, options)
189
190 if !(*preview) {
191 // Create a ticker and fetch and process the nodes every "period"
192 ticker := time.NewTicker(period)
193 for t := range ticker.C {
194 log.Printf("[info] query server at %s", t)
195 nodes, _ := fetchNodes(client)
196 ProcessAll(client, nodes, options)
197 }
198 }
199}