blob: e2710d7410dab7f72995e9bec827179311bd375c [file] [log] [blame]
David K. Bainbridgedf9df632016-07-07 18:47:46 -07001// Copyright 2016 Open Networking Laboratory
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
David K. Bainbridgeb5415042016-05-13 17:06:10 -070014package main
15
16import (
17 "encoding/json"
18 "flag"
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -070019 "github.com/Sirupsen/logrus"
David K. Bainbridge6ea57c12016-06-06 23:29:12 -070020 "github.com/kelseyhightower/envconfig"
David K. Bainbridgeb5415042016-05-13 17:06:10 -070021 "net/url"
22 "os"
23 "strings"
24 "time"
25 "unicode"
26
27 maas "github.com/juju/gomaasapi"
28)
29
30const (
31 // defaultFilter specifies the default filter to use when none is specified
32 defaultFilter = `{
33 "hosts" : {
34 "include" : [ ".*" ],
35 "exclude" : []
36 },
37 "zones" : {
38 "include" : ["default"],
39 "exclude" : []
40 }
41 }`
42 defaultMapping = "{}"
43)
44
David K. Bainbridge6ea57c12016-06-06 23:29:12 -070045type Config struct {
46 PowerHelperUser string `default:"cord" envconfig:"POWER_HELPER_USER"`
47 PowerHelperHost string `default:"127.0.0.1" envconfig:"POWER_HELPER_HOST"`
48 PowerHelperScript string `default:"" envconfig:"POWER_HELPER_SCRIPT"`
49 ProvisionUrl string `default:"" envconfig:"PROVISION_URL"`
David K. Bainbridge068e87d2016-06-30 13:53:19 -070050 ProvisionTtl string `default:"1h" envconfig:"PROVISION_TTL"`
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -070051 LogLevel string `default:"warning" envconfig:"LOG_LEVEL"`
52 LogFormat string `default:"text" envconfig:"LOG_FORMAT"`
David K. Bainbridge6ea57c12016-06-06 23:29:12 -070053}
54
David K. Bainbridgeb5415042016-05-13 17:06:10 -070055var apiKey = flag.String("apikey", "", "key with which to access MAAS server")
56var maasURL = flag.String("maas", "http://localhost/MAAS", "url over which to access MAAS")
57var apiVersion = flag.String("apiVersion", "1.0", "version of the API to access")
58var queryPeriod = flag.String("period", "15s", "frequency the MAAS service is polled for node states")
59var 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")
60var mappings = flag.String("mappings", "{}", "the mac to name mappings")
61var always = flag.Bool("always-rename", true, "attempt to rename at every stage of workflow")
David K. Bainbridgeb5415042016-05-13 17:06:10 -070062var filterSpec = flag.String("filter", strings.Map(func(r rune) rune {
63 if unicode.IsSpace(r) {
64 return -1
65 }
66 return r
67}, defaultFilter), "constrain by hostname what will be automated")
68
69// checkError if the given err is not nil, then fatally log the message, else
70// return false.
71func checkError(err error, message string, v ...interface{}) bool {
72 if err != nil {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -070073 log.Fatalf(message, v...)
David K. Bainbridgeb5415042016-05-13 17:06:10 -070074 }
75 return false
76}
77
78// checkWarn if the given err is not nil, then log the message as a warning and
79// return true, else return false.
80func checkWarn(err error, message string, v ...interface{}) bool {
81 if err != nil {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -070082 log.Warningf(message, v...)
David K. Bainbridgeb5415042016-05-13 17:06:10 -070083 return true
84 }
85 return false
86}
87
88// fetchNodes do a HTTP GET to the MAAS server to query all the nodes
89func fetchNodes(client *maas.MAASObject) ([]MaasNode, error) {
90 nodeListing := client.GetSubObject("nodes")
91 listNodeObjects, err := nodeListing.CallGet("list", url.Values{})
92 if checkWarn(err, "unable to get the list of all nodes: %s", err) {
93 return nil, err
94 }
95 listNodes, err := listNodeObjects.GetArray()
96 if checkWarn(err, "unable to get the node objects for the list: %s", err) {
97 return nil, err
98 }
99
100 var nodes = make([]MaasNode, len(listNodes))
101 for index, nodeObj := range listNodes {
102 node, err := nodeObj.GetMAASObject()
103 if !checkWarn(err, "unable to retrieve object for node: %s", err) {
104 nodes[index] = MaasNode{node}
105 }
106 }
107 return nodes, nil
108}
109
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700110var log = logrus.New()
111
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700112func main() {
113
114 flag.Parse()
David K. Bainbridge6ea57c12016-06-06 23:29:12 -0700115 config := Config{}
116 err := envconfig.Process("AUTOMATION", &config)
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700117 if err != nil {
118 log.Fatalf("Unable to parse configuration options : %s", err)
119 }
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700120
121 options := ProcessingOptions{
David K. Bainbridge6ea57c12016-06-06 23:29:12 -0700122 Preview: *preview,
David K. Bainbridge6ea57c12016-06-06 23:29:12 -0700123 AlwaysRename: *always,
David K. Bainbridge068e87d2016-06-30 13:53:19 -0700124 Provisioner: NewProvisioner(&ProvisionerConfig{Url: config.ProvisionUrl}),
David K. Bainbridge6ea57c12016-06-06 23:29:12 -0700125 ProvisionURL: config.ProvisionUrl,
126 PowerHelper: config.PowerHelperScript,
127 PowerHelperUser: config.PowerHelperUser,
128 PowerHelperHost: config.PowerHelperHost,
David K. Bainbridgeefa951d2016-05-26 10:54:25 -0700129 }
130
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700131 switch config.LogFormat {
132 case "json":
133 log.Formatter = &logrus.JSONFormatter{}
134 default:
135 log.Formatter = &logrus.TextFormatter{
136 FullTimestamp: true,
137 ForceColors: true,
138 }
139 }
140
141 level, err := logrus.ParseLevel(config.LogLevel)
142 if err != nil {
143 level = logrus.WarnLevel
144 }
145 log.Level = level
146
David K. Bainbridge6ea57c12016-06-06 23:29:12 -0700147 options.ProvisionTTL, err = time.ParseDuration(config.ProvisionTtl)
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700148 checkError(err, "unable to parse specified duration of '%s' : %s", err)
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700149
150 // Determine the filter, this can either be specified on the the command
151 // line as a value or a file reference. If none is specified the default
152 // will be used
153 if len(*filterSpec) > 0 {
154 if (*filterSpec)[0] == '@' {
155 name := os.ExpandEnv((*filterSpec)[1:])
156 file, err := os.OpenFile(name, os.O_RDONLY, 0)
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700157 checkError(err, "unable to open file '%s' to load the filter : %s", name, err)
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700158 decoder := json.NewDecoder(file)
159 err = decoder.Decode(&options.Filter)
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700160 checkError(err, "unable to parse filter configuration from file '%s' : %s", name, err)
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700161 } else {
162 err := json.Unmarshal([]byte(*filterSpec), &options.Filter)
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700163 checkError(err, "unable to parse filter specification: '%s' : %s", *filterSpec, err)
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700164 }
165 } else {
166 err := json.Unmarshal([]byte(defaultFilter), &options.Filter)
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700167 checkError(err, "unable to parse default filter specificiation: '%s' : %s", defaultFilter, err)
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700168 }
169
170 // Determine the mac to name mapping, this can either be specified on the the command
171 // line as a value or a file reference. If none is specified the default
172 // will be used
173 if len(*mappings) > 0 {
174 if (*mappings)[0] == '@' {
175 name := os.ExpandEnv((*mappings)[1:])
176 file, err := os.OpenFile(name, os.O_RDONLY, 0)
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700177 checkError(err, "unable to open file '%s' to load the mac name mapping : %s", name, err)
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700178 decoder := json.NewDecoder(file)
179 err = decoder.Decode(&options.Mappings)
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700180 checkError(err, "unable to parse filter configuration from file '%s' : %s", name, err)
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700181 } else {
182 err := json.Unmarshal([]byte(*mappings), &options.Mappings)
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700183 checkError(err, "unable to parse mac name mapping: '%s' : %s", *mappings, err)
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700184 }
185 } else {
186 err := json.Unmarshal([]byte(defaultMapping), &options.Mappings)
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700187 checkError(err, "unable to parse default mac name mappings: '%s' : %s", defaultMapping, err)
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700188 }
189
190 // Verify the specified period for queries can be converted into a Go duration
191 period, err := time.ParseDuration(*queryPeriod)
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700192 checkError(err, "unable to parse specified query period duration: '%s': %s", queryPeriod, err)
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700193
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700194 log.Infof(`Configuration:
David K. Bainbridge37ccf1e2016-06-02 12:47:15 -0700195 MAAS URL: %s
196 MAAS API Version: %s
197 MAAS Query Interval: %s
David K. Bainbridge6ea57c12016-06-06 23:29:12 -0700198 Node Filter: %s
David K. Bainbridge37ccf1e2016-06-02 12:47:15 -0700199 Node Name Mappings: %s
200 Preview: %v
David K. Bainbridge37ccf1e2016-06-02 12:47:15 -0700201 Always Rename: %v
David K. Bainbridge6ea57c12016-06-06 23:29:12 -0700202 Provision URL: %s
203 Provision TTL: %s
204 Power Helper: %s
205 Power Helper User: %s
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700206 Power Helper Host: %s
207 Log Level: %s
208 Log Format: %s`,
David K. Bainbridge37ccf1e2016-06-02 12:47:15 -0700209 *maasURL, *apiVersion, *queryPeriod, *filterSpec, *mappings, options.Preview,
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700210 options.AlwaysRename, options.ProvisionURL, options.ProvisionTTL,
211 options.PowerHelper, options.PowerHelperUser, options.PowerHelperHost, config.LogLevel, config.LogFormat)
David K. Bainbridge37ccf1e2016-06-02 12:47:15 -0700212
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700213 authClient, err := maas.NewAuthenticatedClient(*maasURL, *apiKey, *apiVersion)
214 if err != nil {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700215 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 -0700216 }
217
218 // Create an object through which we will communicate with MAAS
219 client := maas.NewMAAS(*authClient)
220
221 // This utility essentially polls the MAAS server for node state and
222 // process the node to the next state. This is done by kicking off the
223 // process every specified duration. This means that the first processing of
224 // nodes will have "period" in the future. This is really not the behavior
225 // we want, we really want, do it now, and then do the next one in "period".
226 // So, the code does one now.
227 nodes, _ := fetchNodes(client)
228 ProcessAll(client, nodes, options)
229
230 if !(*preview) {
231 // Create a ticker and fetch and process the nodes every "period"
232 ticker := time.NewTicker(period)
233 for t := range ticker.C {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700234 log.Infof("query server at %s", t)
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700235 nodes, _ := fetchNodes(client)
236 ProcessAll(client, nodes, options)
237 }
238 }
239}