blob: f443cefe23876d35ef0b7b7b83aca7e16abd7969 [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. Bainbridgeb5415042016-05-13 17:06:10 -0700112 }
113
114 // Determine the filter, this can either be specified on the the command
115 // line as a value or a file reference. If none is specified the default
116 // will be used
117 if len(*filterSpec) > 0 {
118 if (*filterSpec)[0] == '@' {
119 name := os.ExpandEnv((*filterSpec)[1:])
120 file, err := os.OpenFile(name, os.O_RDONLY, 0)
121 checkError(err, "[error] unable to open file '%s' to load the filter : %s", name, err)
122 decoder := json.NewDecoder(file)
123 err = decoder.Decode(&options.Filter)
124 checkError(err, "[error] unable to parse filter configuration from file '%s' : %s", name, err)
125 } else {
126 err := json.Unmarshal([]byte(*filterSpec), &options.Filter)
127 checkError(err, "[error] unable to parse filter specification: '%s' : %s", *filterSpec, err)
128 }
129 } else {
130 err := json.Unmarshal([]byte(defaultFilter), &options.Filter)
131 checkError(err, "[error] unable to parse default filter specificiation: '%s' : %s", defaultFilter, err)
132 }
133
134 // Determine the mac to name mapping, this can either be specified on the the command
135 // line as a value or a file reference. If none is specified the default
136 // will be used
137 if len(*mappings) > 0 {
138 if (*mappings)[0] == '@' {
139 name := os.ExpandEnv((*mappings)[1:])
140 file, err := os.OpenFile(name, os.O_RDONLY, 0)
141 checkError(err, "[error] unable to open file '%s' to load the mac name mapping : %s", name, err)
142 decoder := json.NewDecoder(file)
143 err = decoder.Decode(&options.Mappings)
144 checkError(err, "[error] unable to parse filter configuration from file '%s' : %s", name, err)
145 } else {
146 err := json.Unmarshal([]byte(*mappings), &options.Mappings)
147 checkError(err, "[error] unable to parse mac name mapping: '%s' : %s", *mappings, err)
148 }
149 } else {
150 err := json.Unmarshal([]byte(defaultMapping), &options.Mappings)
151 checkError(err, "[error] unable to parse default mac name mappings: '%s' : %s", defaultMapping, err)
152 }
153
154 // Verify the specified period for queries can be converted into a Go duration
155 period, err := time.ParseDuration(*queryPeriod)
156 checkError(err, "[error] unable to parse specified query period duration: '%s': %s", queryPeriod, err)
157
158 authClient, err := maas.NewAuthenticatedClient(*maasURL, *apiKey, *apiVersion)
159 if err != nil {
160 checkError(err, "[error] Unable to use specified client key, '%s', to authenticate to the MAAS server: %s", *apiKey, err)
161 }
162
163 // Create an object through which we will communicate with MAAS
164 client := maas.NewMAAS(*authClient)
165
166 // This utility essentially polls the MAAS server for node state and
167 // process the node to the next state. This is done by kicking off the
168 // process every specified duration. This means that the first processing of
169 // nodes will have "period" in the future. This is really not the behavior
170 // we want, we really want, do it now, and then do the next one in "period".
171 // So, the code does one now.
172 nodes, _ := fetchNodes(client)
173 ProcessAll(client, nodes, options)
174
175 if !(*preview) {
176 // Create a ticker and fetch and process the nodes every "period"
177 ticker := time.NewTicker(period)
178 for t := range ticker.C {
179 log.Printf("[info] query server at %s", t)
180 nodes, _ := fetchNodes(client)
181 ProcessAll(client, nodes, options)
182 }
183 }
184}