blob: 84337aaaa7f2097d6e83c3ee629d216c0b123a9d [file] [log] [blame]
David K. Bainbridgeb5415042016-05-13 17:06:10 -07001package main
2
3import (
4 "encoding/json"
5 "flag"
David K. Bainbridge6ea57c12016-06-06 23:29:12 -07006 "github.com/kelseyhightower/envconfig"
David K. Bainbridgeb5415042016-05-13 17:06:10 -07007 "log"
8 "net/url"
9 "os"
10 "strings"
11 "time"
12 "unicode"
13
14 maas "github.com/juju/gomaasapi"
15)
16
17const (
18 // defaultFilter specifies the default filter to use when none is specified
19 defaultFilter = `{
20 "hosts" : {
21 "include" : [ ".*" ],
22 "exclude" : []
23 },
24 "zones" : {
25 "include" : ["default"],
26 "exclude" : []
27 }
28 }`
29 defaultMapping = "{}"
30)
31
David K. Bainbridge6ea57c12016-06-06 23:29:12 -070032type Config struct {
33 PowerHelperUser string `default:"cord" envconfig:"POWER_HELPER_USER"`
34 PowerHelperHost string `default:"127.0.0.1" envconfig:"POWER_HELPER_HOST"`
35 PowerHelperScript string `default:"" envconfig:"POWER_HELPER_SCRIPT"`
36 ProvisionUrl string `default:"" envconfig:"PROVISION_URL"`
37 ProvisionTtl string `default:"30m" envconfig:"PROVISION_TTL"`
38}
39
David K. Bainbridgeb5415042016-05-13 17:06:10 -070040var apiKey = flag.String("apikey", "", "key with which to access MAAS server")
41var maasURL = flag.String("maas", "http://localhost/MAAS", "url over which to access MAAS")
42var apiVersion = flag.String("apiVersion", "1.0", "version of the API to access")
43var queryPeriod = flag.String("period", "15s", "frequency the MAAS service is polled for node states")
44var 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")
45var mappings = flag.String("mappings", "{}", "the mac to name mappings")
46var always = flag.Bool("always-rename", true, "attempt to rename at every stage of workflow")
47var verbose = flag.Bool("verbose", false, "display verbose logging")
48var filterSpec = flag.String("filter", strings.Map(func(r rune) rune {
49 if unicode.IsSpace(r) {
50 return -1
51 }
52 return r
53}, defaultFilter), "constrain by hostname what will be automated")
54
55// checkError if the given err is not nil, then fatally log the message, else
56// return false.
57func checkError(err error, message string, v ...interface{}) bool {
58 if err != nil {
59 log.Fatalf("[error] "+message, v)
60 }
61 return false
62}
63
64// checkWarn if the given err is not nil, then log the message as a warning and
65// return true, else return false.
66func checkWarn(err error, message string, v ...interface{}) bool {
67 if err != nil {
68 log.Printf("[warn] "+message, v)
69 return true
70 }
71 return false
72}
73
74// fetchNodes do a HTTP GET to the MAAS server to query all the nodes
75func fetchNodes(client *maas.MAASObject) ([]MaasNode, error) {
76 nodeListing := client.GetSubObject("nodes")
77 listNodeObjects, err := nodeListing.CallGet("list", url.Values{})
78 if checkWarn(err, "unable to get the list of all nodes: %s", err) {
79 return nil, err
80 }
81 listNodes, err := listNodeObjects.GetArray()
82 if checkWarn(err, "unable to get the node objects for the list: %s", err) {
83 return nil, err
84 }
85
86 var nodes = make([]MaasNode, len(listNodes))
87 for index, nodeObj := range listNodes {
88 node, err := nodeObj.GetMAASObject()
89 if !checkWarn(err, "unable to retrieve object for node: %s", err) {
90 nodes[index] = MaasNode{node}
91 }
92 }
93 return nodes, nil
94}
95
96func main() {
97
98 flag.Parse()
David K. Bainbridge6ea57c12016-06-06 23:29:12 -070099 config := Config{}
100 err := envconfig.Process("AUTOMATION", &config)
101 checkError(err, "[error] unable to parse environment options : %s", err)
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700102
103 options := ProcessingOptions{
David K. Bainbridge6ea57c12016-06-06 23:29:12 -0700104 Preview: *preview,
105 Verbose: *verbose,
106 AlwaysRename: *always,
107 ProvTracker: NewTracker(),
108 ProvisionURL: config.ProvisionUrl,
109 PowerHelper: config.PowerHelperScript,
110 PowerHelperUser: config.PowerHelperUser,
111 PowerHelperHost: config.PowerHelperHost,
David K. Bainbridgeefa951d2016-05-26 10:54:25 -0700112 }
113
David K. Bainbridge6ea57c12016-06-06 23:29:12 -0700114 options.ProvisionTTL, err = time.ParseDuration(config.ProvisionTtl)
115 checkError(err, "[error] unable to parse specified duration of '%s' : %s", err)
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700116
117 // Determine the filter, 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(*filterSpec) > 0 {
121 if (*filterSpec)[0] == '@' {
122 name := os.ExpandEnv((*filterSpec)[1:])
123 file, err := os.OpenFile(name, os.O_RDONLY, 0)
124 checkError(err, "[error] unable to open file '%s' to load the filter : %s", name, err)
125 decoder := json.NewDecoder(file)
126 err = decoder.Decode(&options.Filter)
127 checkError(err, "[error] unable to parse filter configuration from file '%s' : %s", name, err)
128 } else {
129 err := json.Unmarshal([]byte(*filterSpec), &options.Filter)
130 checkError(err, "[error] unable to parse filter specification: '%s' : %s", *filterSpec, err)
131 }
132 } else {
133 err := json.Unmarshal([]byte(defaultFilter), &options.Filter)
134 checkError(err, "[error] unable to parse default filter specificiation: '%s' : %s", defaultFilter, err)
135 }
136
137 // Determine the mac to name mapping, this can either be specified on the the command
138 // line as a value or a file reference. If none is specified the default
139 // will be used
140 if len(*mappings) > 0 {
141 if (*mappings)[0] == '@' {
142 name := os.ExpandEnv((*mappings)[1:])
143 file, err := os.OpenFile(name, os.O_RDONLY, 0)
144 checkError(err, "[error] unable to open file '%s' to load the mac name mapping : %s", name, err)
145 decoder := json.NewDecoder(file)
146 err = decoder.Decode(&options.Mappings)
147 checkError(err, "[error] unable to parse filter configuration from file '%s' : %s", name, err)
148 } else {
149 err := json.Unmarshal([]byte(*mappings), &options.Mappings)
150 checkError(err, "[error] unable to parse mac name mapping: '%s' : %s", *mappings, err)
151 }
152 } else {
153 err := json.Unmarshal([]byte(defaultMapping), &options.Mappings)
154 checkError(err, "[error] unable to parse default mac name mappings: '%s' : %s", defaultMapping, err)
155 }
156
157 // Verify the specified period for queries can be converted into a Go duration
158 period, err := time.ParseDuration(*queryPeriod)
159 checkError(err, "[error] unable to parse specified query period duration: '%s': %s", queryPeriod, err)
160
David K. Bainbridge37ccf1e2016-06-02 12:47:15 -0700161 log.Printf(`Configuration:
162 MAAS URL: %s
163 MAAS API Version: %s
164 MAAS Query Interval: %s
David K. Bainbridge6ea57c12016-06-06 23:29:12 -0700165 Node Filter: %s
David K. Bainbridge37ccf1e2016-06-02 12:47:15 -0700166 Node Name Mappings: %s
167 Preview: %v
168 Verbose: %v
169 Always Rename: %v
David K. Bainbridge6ea57c12016-06-06 23:29:12 -0700170 Provision URL: %s
171 Provision TTL: %s
172 Power Helper: %s
173 Power Helper User: %s
174 Power Helper Host: %s`,
David K. Bainbridge37ccf1e2016-06-02 12:47:15 -0700175 *maasURL, *apiVersion, *queryPeriod, *filterSpec, *mappings, options.Preview,
David K. Bainbridge6ea57c12016-06-06 23:29:12 -0700176 options.Verbose, options.AlwaysRename, options.ProvisionURL, options.ProvisionTTL,
177 options.PowerHelper, options.PowerHelperUser, options.PowerHelperHost)
David K. Bainbridge37ccf1e2016-06-02 12:47:15 -0700178
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700179 authClient, err := maas.NewAuthenticatedClient(*maasURL, *apiKey, *apiVersion)
180 if err != nil {
181 checkError(err, "[error] Unable to use specified client key, '%s', to authenticate to the MAAS server: %s", *apiKey, err)
182 }
183
184 // Create an object through which we will communicate with MAAS
185 client := maas.NewMAAS(*authClient)
186
187 // This utility essentially polls the MAAS server for node state and
188 // process the node to the next state. This is done by kicking off the
189 // process every specified duration. This means that the first processing of
190 // nodes will have "period" in the future. This is really not the behavior
191 // we want, we really want, do it now, and then do the next one in "period".
192 // So, the code does one now.
193 nodes, _ := fetchNodes(client)
194 ProcessAll(client, nodes, options)
195
196 if !(*preview) {
197 // Create a ticker and fetch and process the nodes every "period"
198 ticker := time.NewTicker(period)
199 for t := range ticker.C {
200 log.Printf("[info] query server at %s", t)
201 nodes, _ := fetchNodes(client)
202 ProcessAll(client, nodes, options)
203 }
204 }
205}