blob: 7bacddbecbe14ca5b99cae4a4f43486c472ceba1 [file] [log] [blame]
David K. Bainbridgeb5415042016-05-13 17:06:10 -07001package main
2
3import (
4 "encoding/json"
5 "flag"
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -07006 "github.com/Sirupsen/logrus"
David K. Bainbridge6ea57c12016-06-06 23:29:12 -07007 "github.com/kelseyhightower/envconfig"
David K. Bainbridgeb5415042016-05-13 17:06:10 -07008 "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"`
David K. Bainbridge068e87d2016-06-30 13:53:19 -070037 ProvisionTtl string `default:"1h" envconfig:"PROVISION_TTL"`
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -070038 LogLevel string `default:"warning" envconfig:"LOG_LEVEL"`
39 LogFormat string `default:"text" envconfig:"LOG_FORMAT"`
David K. Bainbridge6ea57c12016-06-06 23:29:12 -070040}
41
David K. Bainbridgeb5415042016-05-13 17:06:10 -070042var apiKey = flag.String("apikey", "", "key with which to access MAAS server")
43var maasURL = flag.String("maas", "http://localhost/MAAS", "url over which to access MAAS")
44var apiVersion = flag.String("apiVersion", "1.0", "version of the API to access")
45var queryPeriod = flag.String("period", "15s", "frequency the MAAS service is polled for node states")
46var 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")
47var mappings = flag.String("mappings", "{}", "the mac to name mappings")
48var always = flag.Bool("always-rename", true, "attempt to rename at every stage of workflow")
David K. Bainbridgeb5415042016-05-13 17:06:10 -070049var filterSpec = flag.String("filter", strings.Map(func(r rune) rune {
50 if unicode.IsSpace(r) {
51 return -1
52 }
53 return r
54}, defaultFilter), "constrain by hostname what will be automated")
55
56// checkError if the given err is not nil, then fatally log the message, else
57// return false.
58func checkError(err error, message string, v ...interface{}) bool {
59 if err != nil {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -070060 log.Fatalf(message, v...)
David K. Bainbridgeb5415042016-05-13 17:06:10 -070061 }
62 return false
63}
64
65// checkWarn if the given err is not nil, then log the message as a warning and
66// return true, else return false.
67func checkWarn(err error, message string, v ...interface{}) bool {
68 if err != nil {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -070069 log.Warningf(message, v...)
David K. Bainbridgeb5415042016-05-13 17:06:10 -070070 return true
71 }
72 return false
73}
74
75// fetchNodes do a HTTP GET to the MAAS server to query all the nodes
76func fetchNodes(client *maas.MAASObject) ([]MaasNode, error) {
77 nodeListing := client.GetSubObject("nodes")
78 listNodeObjects, err := nodeListing.CallGet("list", url.Values{})
79 if checkWarn(err, "unable to get the list of all nodes: %s", err) {
80 return nil, err
81 }
82 listNodes, err := listNodeObjects.GetArray()
83 if checkWarn(err, "unable to get the node objects for the list: %s", err) {
84 return nil, err
85 }
86
87 var nodes = make([]MaasNode, len(listNodes))
88 for index, nodeObj := range listNodes {
89 node, err := nodeObj.GetMAASObject()
90 if !checkWarn(err, "unable to retrieve object for node: %s", err) {
91 nodes[index] = MaasNode{node}
92 }
93 }
94 return nodes, nil
95}
96
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -070097var log = logrus.New()
98
David K. Bainbridgeb5415042016-05-13 17:06:10 -070099func main() {
100
101 flag.Parse()
David K. Bainbridge6ea57c12016-06-06 23:29:12 -0700102 config := Config{}
103 err := envconfig.Process("AUTOMATION", &config)
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700104 if err != nil {
105 log.Fatalf("Unable to parse configuration options : %s", err)
106 }
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700107
108 options := ProcessingOptions{
David K. Bainbridge6ea57c12016-06-06 23:29:12 -0700109 Preview: *preview,
David K. Bainbridge6ea57c12016-06-06 23:29:12 -0700110 AlwaysRename: *always,
David K. Bainbridge068e87d2016-06-30 13:53:19 -0700111 Provisioner: NewProvisioner(&ProvisionerConfig{Url: config.ProvisionUrl}),
David K. Bainbridge6ea57c12016-06-06 23:29:12 -0700112 ProvisionURL: config.ProvisionUrl,
113 PowerHelper: config.PowerHelperScript,
114 PowerHelperUser: config.PowerHelperUser,
115 PowerHelperHost: config.PowerHelperHost,
David K. Bainbridgeefa951d2016-05-26 10:54:25 -0700116 }
117
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700118 switch config.LogFormat {
119 case "json":
120 log.Formatter = &logrus.JSONFormatter{}
121 default:
122 log.Formatter = &logrus.TextFormatter{
123 FullTimestamp: true,
124 ForceColors: true,
125 }
126 }
127
128 level, err := logrus.ParseLevel(config.LogLevel)
129 if err != nil {
130 level = logrus.WarnLevel
131 }
132 log.Level = level
133
David K. Bainbridge6ea57c12016-06-06 23:29:12 -0700134 options.ProvisionTTL, err = time.ParseDuration(config.ProvisionTtl)
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700135 checkError(err, "unable to parse specified duration of '%s' : %s", err)
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700136
137 // Determine the filter, 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(*filterSpec) > 0 {
141 if (*filterSpec)[0] == '@' {
142 name := os.ExpandEnv((*filterSpec)[1:])
143 file, err := os.OpenFile(name, os.O_RDONLY, 0)
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700144 checkError(err, "unable to open file '%s' to load the filter : %s", name, err)
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700145 decoder := json.NewDecoder(file)
146 err = decoder.Decode(&options.Filter)
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700147 checkError(err, "unable to parse filter configuration from file '%s' : %s", name, err)
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700148 } else {
149 err := json.Unmarshal([]byte(*filterSpec), &options.Filter)
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700150 checkError(err, "unable to parse filter specification: '%s' : %s", *filterSpec, err)
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700151 }
152 } else {
153 err := json.Unmarshal([]byte(defaultFilter), &options.Filter)
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700154 checkError(err, "unable to parse default filter specificiation: '%s' : %s", defaultFilter, err)
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700155 }
156
157 // Determine the mac to name mapping, this can either be specified on the the command
158 // line as a value or a file reference. If none is specified the default
159 // will be used
160 if len(*mappings) > 0 {
161 if (*mappings)[0] == '@' {
162 name := os.ExpandEnv((*mappings)[1:])
163 file, err := os.OpenFile(name, os.O_RDONLY, 0)
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700164 checkError(err, "unable to open file '%s' to load the mac name mapping : %s", name, err)
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700165 decoder := json.NewDecoder(file)
166 err = decoder.Decode(&options.Mappings)
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700167 checkError(err, "unable to parse filter configuration from file '%s' : %s", name, err)
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700168 } else {
169 err := json.Unmarshal([]byte(*mappings), &options.Mappings)
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700170 checkError(err, "unable to parse mac name mapping: '%s' : %s", *mappings, err)
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700171 }
172 } else {
173 err := json.Unmarshal([]byte(defaultMapping), &options.Mappings)
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700174 checkError(err, "unable to parse default mac name mappings: '%s' : %s", defaultMapping, err)
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700175 }
176
177 // Verify the specified period for queries can be converted into a Go duration
178 period, err := time.ParseDuration(*queryPeriod)
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700179 checkError(err, "unable to parse specified query period duration: '%s': %s", queryPeriod, err)
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700180
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700181 log.Infof(`Configuration:
David K. Bainbridge37ccf1e2016-06-02 12:47:15 -0700182 MAAS URL: %s
183 MAAS API Version: %s
184 MAAS Query Interval: %s
David K. Bainbridge6ea57c12016-06-06 23:29:12 -0700185 Node Filter: %s
David K. Bainbridge37ccf1e2016-06-02 12:47:15 -0700186 Node Name Mappings: %s
187 Preview: %v
David K. Bainbridge37ccf1e2016-06-02 12:47:15 -0700188 Always Rename: %v
David K. Bainbridge6ea57c12016-06-06 23:29:12 -0700189 Provision URL: %s
190 Provision TTL: %s
191 Power Helper: %s
192 Power Helper User: %s
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700193 Power Helper Host: %s
194 Log Level: %s
195 Log Format: %s`,
David K. Bainbridge37ccf1e2016-06-02 12:47:15 -0700196 *maasURL, *apiVersion, *queryPeriod, *filterSpec, *mappings, options.Preview,
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700197 options.AlwaysRename, options.ProvisionURL, options.ProvisionTTL,
198 options.PowerHelper, options.PowerHelperUser, options.PowerHelperHost, config.LogLevel, config.LogFormat)
David K. Bainbridge37ccf1e2016-06-02 12:47:15 -0700199
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700200 authClient, err := maas.NewAuthenticatedClient(*maasURL, *apiKey, *apiVersion)
201 if err != nil {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700202 checkError(err, "Unable to use specified client key, '%s', to authenticate to the MAAS server: %s", *apiKey, err)
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700203 }
204
205 // Create an object through which we will communicate with MAAS
206 client := maas.NewMAAS(*authClient)
207
208 // This utility essentially polls the MAAS server for node state and
209 // process the node to the next state. This is done by kicking off the
210 // process every specified duration. This means that the first processing of
211 // nodes will have "period" in the future. This is really not the behavior
212 // we want, we really want, do it now, and then do the next one in "period".
213 // So, the code does one now.
214 nodes, _ := fetchNodes(client)
215 ProcessAll(client, nodes, options)
216
217 if !(*preview) {
218 // Create a ticker and fetch and process the nodes every "period"
219 ticker := time.NewTicker(period)
220 for t := range ticker.C {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700221 log.Infof("query server at %s", t)
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700222 nodes, _ := fetchNodes(client)
223 ProcessAll(client, nodes, options)
224 }
225 }
226}