blob: 98c786b032d7de04552f5b931346ab0bdde8ccce [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
Cem Turkerf203f932018-02-21 23:42:32 +000030const (
31 appName = "AUTOMATION"
32 maasApiVersion = "2.0"
33)
David K. Bainbridge528b3182017-01-23 08:51:59 -080034
David K. Bainbridge6ea57c12016-06-06 23:29:12 -070035type Config struct {
David K. Bainbridge528b3182017-01-23 08:51:59 -080036 PowerHelperUser string `default:"cord" envconfig:"POWER_HELPER_USER" desc:"user when integrating with virtual box power mgmt"`
37 PowerHelperHost string `default:"127.0.0.1" envconfig:"POWER_HELPER_HOST" desc:"virtual box host"`
38 PowerHelperScript string `default:"" envconfig:"POWER_HELPER_SCRIPT" desc:"script for virtual box power mgmt support"`
39 ProvisionUrl string `default:"" envconfig:"PROVISION_URL" desc:"connection string to connect to provisioner uservice"`
40 ProvisionTtl string `default:"1h" envconfig:"PROVISION_TTL" desc:"duration to wait for a provisioning request to complete, before considered a failure"`
41 LogLevel string `default:"warning" envconfig:"LOG_LEVEL" desc:"detail level for logging"`
42 LogFormat string `default:"text" envconfig:"LOG_FORMAT" desc:"log output format, text or json"`
43 ApiKey string `envconfig:"MAAS_API_KEY" required:"true" desc:"API key to access MAAS server"`
David K. Bainbridge4f8143d2016-10-27 17:14:48 -070044 ApiKeyFile string `default:"/secrets/maas_api_key" envconfig:"MAAS_API_KEY_FILE" desc:"file to hold the secret"`
45 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 -070046 MaasUrl string `default:"http://localhost/MAAS" envconfig:"MAAS_URL" desc:"URL to access MAAS server"`
David K. Bainbridgee9d7af72016-10-14 08:42:55 -070047 QueryInterval time.Duration `default:"15s" envconfig:"MAAS_QUERY_INTERVAL" desc:"frequency to query MAAS service for nodes"`
48 PreviewOnly bool `default:"false" envconfig:"PREVIEW_ONLY" desc:"display actions that would be taken, but don't execute them"`
49 AlwaysRename bool `default:"true" envconfig:"ALWAYS_RENAME" desc:"attempt to rename hosts at every stage or workflow"`
50 Mappings string `default:"{}" envconfig:"MAC_TO_NAME_MAPPINGS" desc:"custom MAC address to host name mappings"`
51 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 -070052}
53
David K. Bainbridgeb5415042016-05-13 17:06:10 -070054// checkError if the given err is not nil, then fatally log the message, else
55// return false.
56func checkError(err error, message string, v ...interface{}) bool {
57 if err != nil {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -070058 log.Fatalf(message, v...)
David K. Bainbridgeb5415042016-05-13 17:06:10 -070059 }
60 return false
61}
62
63// checkWarn if the given err is not nil, then log the message as a warning and
64// return true, else return false.
65func checkWarn(err error, message string, v ...interface{}) bool {
66 if err != nil {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -070067 log.Warningf(message, v...)
David K. Bainbridgeb5415042016-05-13 17:06:10 -070068 return true
69 }
70 return false
71}
72
73// fetchNodes do a HTTP GET to the MAAS server to query all the nodes
74func fetchNodes(client *maas.MAASObject) ([]MaasNode, error) {
75 nodeListing := client.GetSubObject("nodes")
Cem Turkerf203f932018-02-21 23:42:32 +000076 listNodeObjects, err := nodeListing.CallGet("", url.Values{})
David K. Bainbridgeb5415042016-05-13 17:06:10 -070077 if checkWarn(err, "unable to get the list of all nodes: %s", err) {
78 return nil, err
79 }
80 listNodes, err := listNodeObjects.GetArray()
81 if checkWarn(err, "unable to get the node objects for the list: %s", err) {
82 return nil, err
83 }
84
85 var nodes = make([]MaasNode, len(listNodes))
86 for index, nodeObj := range listNodes {
87 node, err := nodeObj.GetMAASObject()
88 if !checkWarn(err, "unable to retrieve object for node: %s", err) {
89 nodes[index] = MaasNode{node}
90 }
91 }
92 return nodes, nil
93}
94
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -070095var log = logrus.New()
David K. Bainbridge528b3182017-01-23 08:51:59 -080096var appFlags = flag.NewFlagSet("", flag.ContinueOnError)
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -070097
David K. Bainbridgeb5415042016-05-13 17:06:10 -070098func main() {
99
David K. Bainbridge6ea57c12016-06-06 23:29:12 -0700100 config := Config{}
David K. Bainbridge528b3182017-01-23 08:51:59 -0800101 appFlags.Usage = func() {
102 envconfig.Usage(appName, &config)
103 }
104 if err := appFlags.Parse(os.Args[1:]); err != nil {
105 if err != flag.ErrHelp {
106 os.Exit(1)
107 } else {
108 return
109 }
110 }
111
112 err := envconfig.Process(appName, &config)
113
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700114 if err != nil {
115 log.Fatalf("Unable to parse configuration options : %s", err)
116 }
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700117
118 options := ProcessingOptions{
David K. Bainbridgee9d7af72016-10-14 08:42:55 -0700119 Preview: config.PreviewOnly,
120 AlwaysRename: config.AlwaysRename,
David K. Bainbridge068e87d2016-06-30 13:53:19 -0700121 Provisioner: NewProvisioner(&ProvisionerConfig{Url: config.ProvisionUrl}),
David K. Bainbridge6ea57c12016-06-06 23:29:12 -0700122 ProvisionURL: config.ProvisionUrl,
123 PowerHelper: config.PowerHelperScript,
124 PowerHelperUser: config.PowerHelperUser,
125 PowerHelperHost: config.PowerHelperHost,
David K. Bainbridgeefa951d2016-05-26 10:54:25 -0700126 }
127
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700128 switch config.LogFormat {
129 case "json":
130 log.Formatter = &logrus.JSONFormatter{}
131 default:
132 log.Formatter = &logrus.TextFormatter{
133 FullTimestamp: true,
134 ForceColors: true,
135 }
136 }
137
138 level, err := logrus.ParseLevel(config.LogLevel)
139 if err != nil {
140 level = logrus.WarnLevel
141 }
142 log.Level = level
143
David K. Bainbridge6ea57c12016-06-06 23:29:12 -0700144 options.ProvisionTTL, err = time.ParseDuration(config.ProvisionTtl)
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700145 checkError(err, "unable to parse specified duration of '%s' : %s", err)
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700146
147 // Determine the filter, this can either be specified on the the command
148 // line as a value or a file reference. If none is specified the default
149 // will be used
David K. Bainbridgee9d7af72016-10-14 08:42:55 -0700150 if len(config.FilterSpec) > 0 {
151 if config.FilterSpec[0] == '@' {
152 name := os.ExpandEnv((config.FilterSpec)[1:])
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700153 file, err := os.OpenFile(name, os.O_RDONLY, 0)
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700154 checkError(err, "unable to open file '%s' to load the filter : %s", name, err)
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700155 decoder := json.NewDecoder(file)
156 err = decoder.Decode(&options.Filter)
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700157 checkError(err, "unable to parse filter configuration from file '%s' : %s", name, err)
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700158 } else {
David K. Bainbridgee9d7af72016-10-14 08:42:55 -0700159 err := json.Unmarshal([]byte(config.FilterSpec), &options.Filter)
160 checkError(err, "unable to parse filter specification: '%s' : %s", config.FilterSpec, err)
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700161 }
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700162 }
163
164 // Determine the mac to name mapping, this can either be specified on the the command
165 // line as a value or a file reference. If none is specified the default
166 // will be used
David K. Bainbridgee9d7af72016-10-14 08:42:55 -0700167 if len(config.Mappings) > 0 {
168 if config.Mappings[0] == '@' {
169 name := os.ExpandEnv(config.Mappings[1:])
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700170 file, err := os.OpenFile(name, os.O_RDONLY, 0)
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700171 checkError(err, "unable to open file '%s' to load the mac name mapping : %s", name, err)
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700172 decoder := json.NewDecoder(file)
173 err = decoder.Decode(&options.Mappings)
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700174 checkError(err, "unable to parse filter configuration from file '%s' : %s", name, err)
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700175 } else {
David K. Bainbridgee9d7af72016-10-14 08:42:55 -0700176 err := json.Unmarshal([]byte(config.Mappings), &options.Mappings)
177 checkError(err, "unable to parse mac name mapping: '%s' : %s", config.Mappings, err)
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700178 }
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700179 }
180
David K. Bainbridgee9d7af72016-10-14 08:42:55 -0700181 // Get human readable strings for config output
182 mappingsAsJson, err := json.Marshal(options.Mappings)
183 checkError(err, "Unable to marshal MAC to hostname mappings to JSON : %s", err)
184 mappingsPrefix := ""
185
186 if len(config.Mappings) > 0 && config.Mappings[0] == '@' {
187 mappingsPrefix = "[" + config.Mappings + "]"
188 }
189
190 filterAsJson, err := json.Marshal(options.Filter)
191 checkError(err, "Unable to marshal host filter to JSON : %s", err)
192 filterPrefix := ""
193 if len(config.FilterSpec) > 0 && config.FilterSpec[0] == '@' {
194 filterPrefix = "[" + config.FilterSpec + "]"
195 }
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700196
David K. Bainbridge4f8143d2016-10-27 17:14:48 -0700197 re := regexp.MustCompile("[^:]")
198 pubKey := config.ApiKey
199 if !config.ShowApiKey {
200 pubKey = re.ReplaceAllString(config.ApiKey, "X")
201 }
202
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700203 log.Infof(`Configuration:
David K. Bainbridge4f8143d2016-10-27 17:14:48 -0700204 POWER_HELPER_USER: %s
205 POWER_HELPER_HOST: %s
206 POWER_HELPER_SCRIPT: %s
207 PROVISION_URL: %s
208 PROVISION_TTL: %s
209 MAAS_URL: %s
210 MAAS_SHOW_API_KEY: %t
211 MAAS_API_KEY: %s
212 MAAS_API_KEY_FILE: %s
David K. Bainbridge4f8143d2016-10-27 17:14:48 -0700213 MAAS_QUERY_INTERVAL: %s
214 HOST_FILTER_SPEC: %+v
215 MAC_TO_NAME_MAPPINGS: %+v
216 PREVIEW_ONLY: %t
217 ALWAYS_RENAME: %t
218 LOG_LEVEL: %s
219 LOG_FORMAT: %s`,
David K. Bainbridgee9d7af72016-10-14 08:42:55 -0700220 config.PowerHelperUser, config.PowerHelperHost, config.PowerHelperScript,
221 config.ProvisionUrl, config.ProvisionTtl,
David K. Bainbridge4f8143d2016-10-27 17:14:48 -0700222 config.MaasUrl, config.ShowApiKey,
Cem Turkerf203f932018-02-21 23:42:32 +0000223 pubKey, config.ApiKeyFile, config.QueryInterval,
David K. Bainbridgee9d7af72016-10-14 08:42:55 -0700224 filterPrefix+string(filterAsJson), mappingsPrefix+string(mappingsAsJson),
225 config.PreviewOnly, config.AlwaysRename,
226 config.LogLevel, config.LogFormat)
David K. Bainbridge37ccf1e2016-06-02 12:47:15 -0700227
David K. Bainbridge4f8143d2016-10-27 17:14:48 -0700228 // Attempt to load the API key from a file if it was not set via the environment
229 // and if the file exists
230 if config.ApiKey == "" {
231 log.Debugf("Attempting to read MAAS API key from file '%s', because it was not set via environment", config.ApiKeyFile)
232 keyBytes, err := ioutil.ReadFile(config.ApiKeyFile)
233 if err != nil {
234 log.Warnf("Failed to read MAAS API key from file '%s', was the file mounted as a volume? : %s ",
235 config.ApiKeyFile, err)
236 } else {
237 config.ApiKey = string(keyBytes)
238 if config.ShowApiKey {
239 pubKey = config.ApiKey
240 } else {
241 pubKey = re.ReplaceAllString(config.ApiKey, "X")
242 }
243 }
244 }
245
Cem Turkerf203f932018-02-21 23:42:32 +0000246 authClient, err := maas.NewAuthenticatedClient(config.MaasUrl, config.ApiKey, maasApiVersion)
David K. Bainbridgee9d7af72016-10-14 08:42:55 -0700247 checkError(err, "Unable to use specified client key, '%s', to authenticate to the MAAS server: %s",
David K. Bainbridge4f8143d2016-10-27 17:14:48 -0700248 pubKey, err)
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700249
250 // Create an object through which we will communicate with MAAS
251 client := maas.NewMAAS(*authClient)
252
253 // This utility essentially polls the MAAS server for node state and
254 // process the node to the next state. This is done by kicking off the
255 // process every specified duration. This means that the first processing of
256 // nodes will have "period" in the future. This is really not the behavior
257 // we want, we really want, do it now, and then do the next one in "period".
258 // So, the code does one now.
259 nodes, _ := fetchNodes(client)
260 ProcessAll(client, nodes, options)
261
David K. Bainbridgee9d7af72016-10-14 08:42:55 -0700262 if !(config.PreviewOnly) {
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700263 // Create a ticker and fetch and process the nodes every "period"
David K. Bainbridgee9d7af72016-10-14 08:42:55 -0700264 for {
265 log.Infof("query server at %s", time.Now())
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700266 nodes, _ := fetchNodes(client)
267 ProcessAll(client, nodes, options)
David K. Bainbridgee9d7af72016-10-14 08:42:55 -0700268
269 // Sleep for the Interval and then process again.
270 time.Sleep(config.QueryInterval)
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700271 }
272 }
273}