blob: 02ccd8cd2557d564f6a3cff1be8854b72493de6a [file] [log] [blame]
Brian O'Connor6a37ea92017-08-03 22:45:59 -07001// Copyright 2016 Open Networking Foundation
David K. Bainbridgedf9df632016-07-07 18:47:46 -07002//
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. Bainbridge4f8143d2016-10-27 17:14:48 -070021 "io/ioutil"
David K. Bainbridgeb5415042016-05-13 17:06:10 -070022 "net/url"
23 "os"
David K. Bainbridge4f8143d2016-10-27 17:14:48 -070024 "regexp"
David K. Bainbridgeb5415042016-05-13 17:06:10 -070025 "time"
David K. Bainbridgeb5415042016-05-13 17:06:10 -070026
27 maas "github.com/juju/gomaasapi"
28)
29
David K. Bainbridge528b3182017-01-23 08:51:59 -080030const appName = "AUTOMATION"
31
David K. Bainbridge6ea57c12016-06-06 23:29:12 -070032type Config struct {
David K. Bainbridge528b3182017-01-23 08:51:59 -080033 PowerHelperUser string `default:"cord" envconfig:"POWER_HELPER_USER" desc:"user when integrating with virtual box power mgmt"`
34 PowerHelperHost string `default:"127.0.0.1" envconfig:"POWER_HELPER_HOST" desc:"virtual box host"`
35 PowerHelperScript string `default:"" envconfig:"POWER_HELPER_SCRIPT" desc:"script for virtual box power mgmt support"`
36 ProvisionUrl string `default:"" envconfig:"PROVISION_URL" desc:"connection string to connect to provisioner uservice"`
37 ProvisionTtl string `default:"1h" envconfig:"PROVISION_TTL" desc:"duration to wait for a provisioning request to complete, before considered a failure"`
38 LogLevel string `default:"warning" envconfig:"LOG_LEVEL" desc:"detail level for logging"`
39 LogFormat string `default:"text" envconfig:"LOG_FORMAT" desc:"log output format, text or json"`
40 ApiKey string `envconfig:"MAAS_API_KEY" required:"true" desc:"API key to access MAAS server"`
David K. Bainbridge4f8143d2016-10-27 17:14:48 -070041 ApiKeyFile string `default:"/secrets/maas_api_key" envconfig:"MAAS_API_KEY_FILE" desc:"file to hold the secret"`
42 ShowApiKey bool `default:"false" envconfig:"MAAS_SHOW_API_KEY" desc:"Show API in clear text in logs"`
David K. Bainbridgee9d7af72016-10-14 08:42:55 -070043 MaasUrl string `default:"http://localhost/MAAS" envconfig:"MAAS_URL" desc:"URL to access MAAS server"`
44 ApiVersion string `default:"1.0" envconfig:"MAAS_API_VERSION" desc:"API version to use with MAAS server"`
45 QueryInterval time.Duration `default:"15s" envconfig:"MAAS_QUERY_INTERVAL" desc:"frequency to query MAAS service for nodes"`
46 PreviewOnly bool `default:"false" envconfig:"PREVIEW_ONLY" desc:"display actions that would be taken, but don't execute them"`
47 AlwaysRename bool `default:"true" envconfig:"ALWAYS_RENAME" desc:"attempt to rename hosts at every stage or workflow"`
48 Mappings string `default:"{}" envconfig:"MAC_TO_NAME_MAPPINGS" desc:"custom MAC address to host name mappings"`
49 FilterSpec string `default:"{\"hosts\":{\"include\":[\".*\"]},\"zones\":{\"include\":[\"default\"]}}" envconfig:"HOST_FILTER_SPEC" desc:"constrain hosts that are automated"`
David K. Bainbridge6ea57c12016-06-06 23:29:12 -070050}
51
David K. Bainbridgeb5415042016-05-13 17:06:10 -070052// checkError if the given err is not nil, then fatally log the message, else
53// return false.
54func checkError(err error, message string, v ...interface{}) bool {
55 if err != nil {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -070056 log.Fatalf(message, v...)
David K. Bainbridgeb5415042016-05-13 17:06:10 -070057 }
58 return false
59}
60
61// checkWarn if the given err is not nil, then log the message as a warning and
62// return true, else return false.
63func checkWarn(err error, message string, v ...interface{}) bool {
64 if err != nil {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -070065 log.Warningf(message, v...)
David K. Bainbridgeb5415042016-05-13 17:06:10 -070066 return true
67 }
68 return false
69}
70
71// fetchNodes do a HTTP GET to the MAAS server to query all the nodes
72func fetchNodes(client *maas.MAASObject) ([]MaasNode, error) {
73 nodeListing := client.GetSubObject("nodes")
74 listNodeObjects, err := nodeListing.CallGet("list", url.Values{})
75 if checkWarn(err, "unable to get the list of all nodes: %s", err) {
76 return nil, err
77 }
78 listNodes, err := listNodeObjects.GetArray()
79 if checkWarn(err, "unable to get the node objects for the list: %s", err) {
80 return nil, err
81 }
82
83 var nodes = make([]MaasNode, len(listNodes))
84 for index, nodeObj := range listNodes {
85 node, err := nodeObj.GetMAASObject()
86 if !checkWarn(err, "unable to retrieve object for node: %s", err) {
87 nodes[index] = MaasNode{node}
88 }
89 }
90 return nodes, nil
91}
92
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -070093var log = logrus.New()
David K. Bainbridge528b3182017-01-23 08:51:59 -080094var appFlags = flag.NewFlagSet("", flag.ContinueOnError)
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -070095
David K. Bainbridgeb5415042016-05-13 17:06:10 -070096func main() {
97
David K. Bainbridge6ea57c12016-06-06 23:29:12 -070098 config := Config{}
David K. Bainbridge528b3182017-01-23 08:51:59 -080099 appFlags.Usage = func() {
100 envconfig.Usage(appName, &config)
101 }
102 if err := appFlags.Parse(os.Args[1:]); err != nil {
103 if err != flag.ErrHelp {
104 os.Exit(1)
105 } else {
106 return
107 }
108 }
109
110 err := envconfig.Process(appName, &config)
111
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700112 if err != nil {
113 log.Fatalf("Unable to parse configuration options : %s", err)
114 }
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700115
116 options := ProcessingOptions{
David K. Bainbridgee9d7af72016-10-14 08:42:55 -0700117 Preview: config.PreviewOnly,
118 AlwaysRename: config.AlwaysRename,
David K. Bainbridge068e87d2016-06-30 13:53:19 -0700119 Provisioner: NewProvisioner(&ProvisionerConfig{Url: config.ProvisionUrl}),
David K. Bainbridge6ea57c12016-06-06 23:29:12 -0700120 ProvisionURL: config.ProvisionUrl,
121 PowerHelper: config.PowerHelperScript,
122 PowerHelperUser: config.PowerHelperUser,
123 PowerHelperHost: config.PowerHelperHost,
David K. Bainbridgeefa951d2016-05-26 10:54:25 -0700124 }
125
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700126 switch config.LogFormat {
127 case "json":
128 log.Formatter = &logrus.JSONFormatter{}
129 default:
130 log.Formatter = &logrus.TextFormatter{
131 FullTimestamp: true,
132 ForceColors: true,
133 }
134 }
135
136 level, err := logrus.ParseLevel(config.LogLevel)
137 if err != nil {
138 level = logrus.WarnLevel
139 }
140 log.Level = level
141
David K. Bainbridge6ea57c12016-06-06 23:29:12 -0700142 options.ProvisionTTL, err = time.ParseDuration(config.ProvisionTtl)
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700143 checkError(err, "unable to parse specified duration of '%s' : %s", err)
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700144
145 // Determine the filter, this can either be specified on the the command
146 // line as a value or a file reference. If none is specified the default
147 // will be used
David K. Bainbridgee9d7af72016-10-14 08:42:55 -0700148 if len(config.FilterSpec) > 0 {
149 if config.FilterSpec[0] == '@' {
150 name := os.ExpandEnv((config.FilterSpec)[1:])
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700151 file, err := os.OpenFile(name, os.O_RDONLY, 0)
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700152 checkError(err, "unable to open file '%s' to load the filter : %s", name, err)
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700153 decoder := json.NewDecoder(file)
154 err = decoder.Decode(&options.Filter)
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700155 checkError(err, "unable to parse filter configuration from file '%s' : %s", name, err)
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700156 } else {
David K. Bainbridgee9d7af72016-10-14 08:42:55 -0700157 err := json.Unmarshal([]byte(config.FilterSpec), &options.Filter)
158 checkError(err, "unable to parse filter specification: '%s' : %s", config.FilterSpec, err)
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700159 }
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700160 }
161
162 // Determine the mac to name mapping, this can either be specified on the the command
163 // line as a value or a file reference. If none is specified the default
164 // will be used
David K. Bainbridgee9d7af72016-10-14 08:42:55 -0700165 if len(config.Mappings) > 0 {
166 if config.Mappings[0] == '@' {
167 name := os.ExpandEnv(config.Mappings[1:])
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700168 file, err := os.OpenFile(name, os.O_RDONLY, 0)
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700169 checkError(err, "unable to open file '%s' to load the mac name mapping : %s", name, err)
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700170 decoder := json.NewDecoder(file)
171 err = decoder.Decode(&options.Mappings)
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700172 checkError(err, "unable to parse filter configuration from file '%s' : %s", name, err)
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700173 } else {
David K. Bainbridgee9d7af72016-10-14 08:42:55 -0700174 err := json.Unmarshal([]byte(config.Mappings), &options.Mappings)
175 checkError(err, "unable to parse mac name mapping: '%s' : %s", config.Mappings, err)
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700176 }
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700177 }
178
David K. Bainbridgee9d7af72016-10-14 08:42:55 -0700179 // Get human readable strings for config output
180 mappingsAsJson, err := json.Marshal(options.Mappings)
181 checkError(err, "Unable to marshal MAC to hostname mappings to JSON : %s", err)
182 mappingsPrefix := ""
183
184 if len(config.Mappings) > 0 && config.Mappings[0] == '@' {
185 mappingsPrefix = "[" + config.Mappings + "]"
186 }
187
188 filterAsJson, err := json.Marshal(options.Filter)
189 checkError(err, "Unable to marshal host filter to JSON : %s", err)
190 filterPrefix := ""
191 if len(config.FilterSpec) > 0 && config.FilterSpec[0] == '@' {
192 filterPrefix = "[" + config.FilterSpec + "]"
193 }
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700194
David K. Bainbridge4f8143d2016-10-27 17:14:48 -0700195 re := regexp.MustCompile("[^:]")
196 pubKey := config.ApiKey
197 if !config.ShowApiKey {
198 pubKey = re.ReplaceAllString(config.ApiKey, "X")
199 }
200
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700201 log.Infof(`Configuration:
David K. Bainbridge4f8143d2016-10-27 17:14:48 -0700202 POWER_HELPER_USER: %s
203 POWER_HELPER_HOST: %s
204 POWER_HELPER_SCRIPT: %s
205 PROVISION_URL: %s
206 PROVISION_TTL: %s
207 MAAS_URL: %s
208 MAAS_SHOW_API_KEY: %t
209 MAAS_API_KEY: %s
210 MAAS_API_KEY_FILE: %s
211 MAAS_API_VERSION: %s
212 MAAS_QUERY_INTERVAL: %s
213 HOST_FILTER_SPEC: %+v
214 MAC_TO_NAME_MAPPINGS: %+v
215 PREVIEW_ONLY: %t
216 ALWAYS_RENAME: %t
217 LOG_LEVEL: %s
218 LOG_FORMAT: %s`,
David K. Bainbridgee9d7af72016-10-14 08:42:55 -0700219 config.PowerHelperUser, config.PowerHelperHost, config.PowerHelperScript,
220 config.ProvisionUrl, config.ProvisionTtl,
David K. Bainbridge4f8143d2016-10-27 17:14:48 -0700221 config.MaasUrl, config.ShowApiKey,
222 pubKey, config.ApiKeyFile, config.ApiVersion, config.QueryInterval,
David K. Bainbridgee9d7af72016-10-14 08:42:55 -0700223 filterPrefix+string(filterAsJson), mappingsPrefix+string(mappingsAsJson),
224 config.PreviewOnly, config.AlwaysRename,
225 config.LogLevel, config.LogFormat)
David K. Bainbridge37ccf1e2016-06-02 12:47:15 -0700226
David K. Bainbridge4f8143d2016-10-27 17:14:48 -0700227 // Attempt to load the API key from a file if it was not set via the environment
228 // and if the file exists
229 if config.ApiKey == "" {
230 log.Debugf("Attempting to read MAAS API key from file '%s', because it was not set via environment", config.ApiKeyFile)
231 keyBytes, err := ioutil.ReadFile(config.ApiKeyFile)
232 if err != nil {
233 log.Warnf("Failed to read MAAS API key from file '%s', was the file mounted as a volume? : %s ",
234 config.ApiKeyFile, err)
235 } else {
236 config.ApiKey = string(keyBytes)
237 if config.ShowApiKey {
238 pubKey = config.ApiKey
239 } else {
240 pubKey = re.ReplaceAllString(config.ApiKey, "X")
241 }
242 }
243 }
244
David K. Bainbridgee9d7af72016-10-14 08:42:55 -0700245 authClient, err := maas.NewAuthenticatedClient(config.MaasUrl, config.ApiKey, config.ApiVersion)
246 checkError(err, "Unable to use specified client key, '%s', to authenticate to the MAAS server: %s",
David K. Bainbridge4f8143d2016-10-27 17:14:48 -0700247 pubKey, err)
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700248
249 // Create an object through which we will communicate with MAAS
250 client := maas.NewMAAS(*authClient)
251
252 // This utility essentially polls the MAAS server for node state and
253 // process the node to the next state. This is done by kicking off the
254 // process every specified duration. This means that the first processing of
255 // nodes will have "period" in the future. This is really not the behavior
256 // we want, we really want, do it now, and then do the next one in "period".
257 // So, the code does one now.
258 nodes, _ := fetchNodes(client)
259 ProcessAll(client, nodes, options)
260
David K. Bainbridgee9d7af72016-10-14 08:42:55 -0700261 if !(config.PreviewOnly) {
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700262 // Create a ticker and fetch and process the nodes every "period"
David K. Bainbridgee9d7af72016-10-14 08:42:55 -0700263 for {
264 log.Infof("query server at %s", time.Now())
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700265 nodes, _ := fetchNodes(client)
266 ProcessAll(client, nodes, options)
David K. Bainbridgee9d7af72016-10-14 08:42:55 -0700267
268 // Sleep for the Interval and then process again.
269 time.Sleep(config.QueryInterval)
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700270 }
271 }
272}