blob: 5d09dc41af2d25cdcc10db4be727343275bb4284 [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. 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. Bainbridge6ea57c12016-06-06 23:29:12 -070030type Config struct {
David K. Bainbridgee9d7af72016-10-14 08:42:55 -070031 PowerHelperUser string `default:"cord" envconfig:"POWER_HELPER_USER"`
32 PowerHelperHost string `default:"127.0.0.1" envconfig:"POWER_HELPER_HOST"`
33 PowerHelperScript string `default:"" envconfig:"POWER_HELPER_SCRIPT"`
34 ProvisionUrl string `default:"" envconfig:"PROVISION_URL"`
35 ProvisionTtl string `default:"1h" envconfig:"PROVISION_TTL"`
36 LogLevel string `default:"warning" envconfig:"LOG_LEVEL"`
37 LogFormat string `default:"text" envconfig:"LOG_FORMAT"`
David K. Bainbridge4f8143d2016-10-27 17:14:48 -070038 ApiKey string `envconfig:"MAAS_API_KEY" desc:"API key to access MAAS server"`
39 ApiKeyFile string `default:"/secrets/maas_api_key" envconfig:"MAAS_API_KEY_FILE" desc:"file to hold the secret"`
40 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 -070041 MaasUrl string `default:"http://localhost/MAAS" envconfig:"MAAS_URL" desc:"URL to access MAAS server"`
42 ApiVersion string `default:"1.0" envconfig:"MAAS_API_VERSION" desc:"API version to use with MAAS server"`
43 QueryInterval time.Duration `default:"15s" envconfig:"MAAS_QUERY_INTERVAL" desc:"frequency to query MAAS service for nodes"`
44 PreviewOnly bool `default:"false" envconfig:"PREVIEW_ONLY" desc:"display actions that would be taken, but don't execute them"`
45 AlwaysRename bool `default:"true" envconfig:"ALWAYS_RENAME" desc:"attempt to rename hosts at every stage or workflow"`
46 Mappings string `default:"{}" envconfig:"MAC_TO_NAME_MAPPINGS" desc:"custom MAC address to host name mappings"`
47 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 -070048}
49
David K. Bainbridgeb5415042016-05-13 17:06:10 -070050// checkError if the given err is not nil, then fatally log the message, else
51// return false.
52func checkError(err error, message string, v ...interface{}) bool {
53 if err != nil {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -070054 log.Fatalf(message, v...)
David K. Bainbridgeb5415042016-05-13 17:06:10 -070055 }
56 return false
57}
58
59// checkWarn if the given err is not nil, then log the message as a warning and
60// return true, else return false.
61func checkWarn(err error, message string, v ...interface{}) bool {
62 if err != nil {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -070063 log.Warningf(message, v...)
David K. Bainbridgeb5415042016-05-13 17:06:10 -070064 return true
65 }
66 return false
67}
68
69// fetchNodes do a HTTP GET to the MAAS server to query all the nodes
70func fetchNodes(client *maas.MAASObject) ([]MaasNode, error) {
71 nodeListing := client.GetSubObject("nodes")
72 listNodeObjects, err := nodeListing.CallGet("list", url.Values{})
73 if checkWarn(err, "unable to get the list of all nodes: %s", err) {
74 return nil, err
75 }
76 listNodes, err := listNodeObjects.GetArray()
77 if checkWarn(err, "unable to get the node objects for the list: %s", err) {
78 return nil, err
79 }
80
81 var nodes = make([]MaasNode, len(listNodes))
82 for index, nodeObj := range listNodes {
83 node, err := nodeObj.GetMAASObject()
84 if !checkWarn(err, "unable to retrieve object for node: %s", err) {
85 nodes[index] = MaasNode{node}
86 }
87 }
88 return nodes, nil
89}
90
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -070091var log = logrus.New()
92
David K. Bainbridgeb5415042016-05-13 17:06:10 -070093func main() {
94
95 flag.Parse()
David K. Bainbridge6ea57c12016-06-06 23:29:12 -070096 config := Config{}
97 err := envconfig.Process("AUTOMATION", &config)
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -070098 if err != nil {
99 log.Fatalf("Unable to parse configuration options : %s", err)
100 }
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700101
102 options := ProcessingOptions{
David K. Bainbridgee9d7af72016-10-14 08:42:55 -0700103 Preview: config.PreviewOnly,
104 AlwaysRename: config.AlwaysRename,
David K. Bainbridge068e87d2016-06-30 13:53:19 -0700105 Provisioner: NewProvisioner(&ProvisionerConfig{Url: config.ProvisionUrl}),
David K. Bainbridge6ea57c12016-06-06 23:29:12 -0700106 ProvisionURL: config.ProvisionUrl,
107 PowerHelper: config.PowerHelperScript,
108 PowerHelperUser: config.PowerHelperUser,
109 PowerHelperHost: config.PowerHelperHost,
David K. Bainbridgeefa951d2016-05-26 10:54:25 -0700110 }
111
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700112 switch config.LogFormat {
113 case "json":
114 log.Formatter = &logrus.JSONFormatter{}
115 default:
116 log.Formatter = &logrus.TextFormatter{
117 FullTimestamp: true,
118 ForceColors: true,
119 }
120 }
121
122 level, err := logrus.ParseLevel(config.LogLevel)
123 if err != nil {
124 level = logrus.WarnLevel
125 }
126 log.Level = level
127
David K. Bainbridge6ea57c12016-06-06 23:29:12 -0700128 options.ProvisionTTL, err = time.ParseDuration(config.ProvisionTtl)
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700129 checkError(err, "unable to parse specified duration of '%s' : %s", err)
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700130
131 // Determine the filter, this can either be specified on the the command
132 // line as a value or a file reference. If none is specified the default
133 // will be used
David K. Bainbridgee9d7af72016-10-14 08:42:55 -0700134 if len(config.FilterSpec) > 0 {
135 if config.FilterSpec[0] == '@' {
136 name := os.ExpandEnv((config.FilterSpec)[1:])
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700137 file, err := os.OpenFile(name, os.O_RDONLY, 0)
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700138 checkError(err, "unable to open file '%s' to load the filter : %s", name, err)
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700139 decoder := json.NewDecoder(file)
140 err = decoder.Decode(&options.Filter)
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700141 checkError(err, "unable to parse filter configuration from file '%s' : %s", name, err)
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700142 } else {
David K. Bainbridgee9d7af72016-10-14 08:42:55 -0700143 err := json.Unmarshal([]byte(config.FilterSpec), &options.Filter)
144 checkError(err, "unable to parse filter specification: '%s' : %s", config.FilterSpec, err)
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700145 }
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700146 }
147
148 // Determine the mac to name mapping, this can either be specified on the the command
149 // line as a value or a file reference. If none is specified the default
150 // will be used
David K. Bainbridgee9d7af72016-10-14 08:42:55 -0700151 if len(config.Mappings) > 0 {
152 if config.Mappings[0] == '@' {
153 name := os.ExpandEnv(config.Mappings[1:])
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700154 file, err := os.OpenFile(name, os.O_RDONLY, 0)
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700155 checkError(err, "unable to open file '%s' to load the mac name mapping : %s", name, err)
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700156 decoder := json.NewDecoder(file)
157 err = decoder.Decode(&options.Mappings)
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700158 checkError(err, "unable to parse filter configuration from file '%s' : %s", name, err)
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700159 } else {
David K. Bainbridgee9d7af72016-10-14 08:42:55 -0700160 err := json.Unmarshal([]byte(config.Mappings), &options.Mappings)
161 checkError(err, "unable to parse mac name mapping: '%s' : %s", config.Mappings, err)
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700162 }
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700163 }
164
David K. Bainbridgee9d7af72016-10-14 08:42:55 -0700165 // Get human readable strings for config output
166 mappingsAsJson, err := json.Marshal(options.Mappings)
167 checkError(err, "Unable to marshal MAC to hostname mappings to JSON : %s", err)
168 mappingsPrefix := ""
169
170 if len(config.Mappings) > 0 && config.Mappings[0] == '@' {
171 mappingsPrefix = "[" + config.Mappings + "]"
172 }
173
174 filterAsJson, err := json.Marshal(options.Filter)
175 checkError(err, "Unable to marshal host filter to JSON : %s", err)
176 filterPrefix := ""
177 if len(config.FilterSpec) > 0 && config.FilterSpec[0] == '@' {
178 filterPrefix = "[" + config.FilterSpec + "]"
179 }
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700180
David K. Bainbridge4f8143d2016-10-27 17:14:48 -0700181 re := regexp.MustCompile("[^:]")
182 pubKey := config.ApiKey
183 if !config.ShowApiKey {
184 pubKey = re.ReplaceAllString(config.ApiKey, "X")
185 }
186
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700187 log.Infof(`Configuration:
David K. Bainbridge4f8143d2016-10-27 17:14:48 -0700188 POWER_HELPER_USER: %s
189 POWER_HELPER_HOST: %s
190 POWER_HELPER_SCRIPT: %s
191 PROVISION_URL: %s
192 PROVISION_TTL: %s
193 MAAS_URL: %s
194 MAAS_SHOW_API_KEY: %t
195 MAAS_API_KEY: %s
196 MAAS_API_KEY_FILE: %s
197 MAAS_API_VERSION: %s
198 MAAS_QUERY_INTERVAL: %s
199 HOST_FILTER_SPEC: %+v
200 MAC_TO_NAME_MAPPINGS: %+v
201 PREVIEW_ONLY: %t
202 ALWAYS_RENAME: %t
203 LOG_LEVEL: %s
204 LOG_FORMAT: %s`,
David K. Bainbridgee9d7af72016-10-14 08:42:55 -0700205 config.PowerHelperUser, config.PowerHelperHost, config.PowerHelperScript,
206 config.ProvisionUrl, config.ProvisionTtl,
David K. Bainbridge4f8143d2016-10-27 17:14:48 -0700207 config.MaasUrl, config.ShowApiKey,
208 pubKey, config.ApiKeyFile, config.ApiVersion, config.QueryInterval,
David K. Bainbridgee9d7af72016-10-14 08:42:55 -0700209 filterPrefix+string(filterAsJson), mappingsPrefix+string(mappingsAsJson),
210 config.PreviewOnly, config.AlwaysRename,
211 config.LogLevel, config.LogFormat)
David K. Bainbridge37ccf1e2016-06-02 12:47:15 -0700212
David K. Bainbridge4f8143d2016-10-27 17:14:48 -0700213 // Attempt to load the API key from a file if it was not set via the environment
214 // and if the file exists
215 if config.ApiKey == "" {
216 log.Debugf("Attempting to read MAAS API key from file '%s', because it was not set via environment", config.ApiKeyFile)
217 keyBytes, err := ioutil.ReadFile(config.ApiKeyFile)
218 if err != nil {
219 log.Warnf("Failed to read MAAS API key from file '%s', was the file mounted as a volume? : %s ",
220 config.ApiKeyFile, err)
221 } else {
222 config.ApiKey = string(keyBytes)
223 if config.ShowApiKey {
224 pubKey = config.ApiKey
225 } else {
226 pubKey = re.ReplaceAllString(config.ApiKey, "X")
227 }
228 }
229 }
230
David K. Bainbridgee9d7af72016-10-14 08:42:55 -0700231 authClient, err := maas.NewAuthenticatedClient(config.MaasUrl, config.ApiKey, config.ApiVersion)
232 checkError(err, "Unable to use specified client key, '%s', to authenticate to the MAAS server: %s",
David K. Bainbridge4f8143d2016-10-27 17:14:48 -0700233 pubKey, err)
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700234
235 // Create an object through which we will communicate with MAAS
236 client := maas.NewMAAS(*authClient)
237
238 // This utility essentially polls the MAAS server for node state and
239 // process the node to the next state. This is done by kicking off the
240 // process every specified duration. This means that the first processing of
241 // nodes will have "period" in the future. This is really not the behavior
242 // we want, we really want, do it now, and then do the next one in "period".
243 // So, the code does one now.
244 nodes, _ := fetchNodes(client)
245 ProcessAll(client, nodes, options)
246
David K. Bainbridgee9d7af72016-10-14 08:42:55 -0700247 if !(config.PreviewOnly) {
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700248 // Create a ticker and fetch and process the nodes every "period"
David K. Bainbridgee9d7af72016-10-14 08:42:55 -0700249 for {
250 log.Infof("query server at %s", time.Now())
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700251 nodes, _ := fetchNodes(client)
252 ProcessAll(client, nodes, options)
David K. Bainbridgee9d7af72016-10-14 08:42:55 -0700253
254 // Sleep for the Interval and then process again.
255 time.Sleep(config.QueryInterval)
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700256 }
257 }
258}