blob: 69939710f84b42ea8c7f92765ee8efadef827659 [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"
David K. Bainbridgeb5415042016-05-13 17:06:10 -070023 "time"
David K. Bainbridgeb5415042016-05-13 17:06:10 -070024
25 maas "github.com/juju/gomaasapi"
26)
27
David K. Bainbridge6ea57c12016-06-06 23:29:12 -070028type Config struct {
David K. Bainbridgee9d7af72016-10-14 08:42:55 -070029 PowerHelperUser string `default:"cord" envconfig:"POWER_HELPER_USER"`
30 PowerHelperHost string `default:"127.0.0.1" envconfig:"POWER_HELPER_HOST"`
31 PowerHelperScript string `default:"" envconfig:"POWER_HELPER_SCRIPT"`
32 ProvisionUrl string `default:"" envconfig:"PROVISION_URL"`
33 ProvisionTtl string `default:"1h" envconfig:"PROVISION_TTL"`
34 LogLevel string `default:"warning" envconfig:"LOG_LEVEL"`
35 LogFormat string `default:"text" envconfig:"LOG_FORMAT"`
36 ApiKey string `envconfig:"MAAS_API_KEY" required:"true" desc:"API key to access MAAS server"`
37 MaasUrl string `default:"http://localhost/MAAS" envconfig:"MAAS_URL" desc:"URL to access MAAS server"`
38 ApiVersion string `default:"1.0" envconfig:"MAAS_API_VERSION" desc:"API version to use with MAAS server"`
39 QueryInterval time.Duration `default:"15s" envconfig:"MAAS_QUERY_INTERVAL" desc:"frequency to query MAAS service for nodes"`
40 PreviewOnly bool `default:"false" envconfig:"PREVIEW_ONLY" desc:"display actions that would be taken, but don't execute them"`
41 AlwaysRename bool `default:"true" envconfig:"ALWAYS_RENAME" desc:"attempt to rename hosts at every stage or workflow"`
42 Mappings string `default:"{}" envconfig:"MAC_TO_NAME_MAPPINGS" desc:"custom MAC address to host name mappings"`
43 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 -070044}
45
David K. Bainbridgeb5415042016-05-13 17:06:10 -070046// checkError if the given err is not nil, then fatally log the message, else
47// return false.
48func checkError(err error, message string, v ...interface{}) bool {
49 if err != nil {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -070050 log.Fatalf(message, v...)
David K. Bainbridgeb5415042016-05-13 17:06:10 -070051 }
52 return false
53}
54
55// checkWarn if the given err is not nil, then log the message as a warning and
56// return true, else return false.
57func checkWarn(err error, message string, v ...interface{}) bool {
58 if err != nil {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -070059 log.Warningf(message, v...)
David K. Bainbridgeb5415042016-05-13 17:06:10 -070060 return true
61 }
62 return false
63}
64
65// fetchNodes do a HTTP GET to the MAAS server to query all the nodes
66func fetchNodes(client *maas.MAASObject) ([]MaasNode, error) {
67 nodeListing := client.GetSubObject("nodes")
68 listNodeObjects, err := nodeListing.CallGet("list", url.Values{})
69 if checkWarn(err, "unable to get the list of all nodes: %s", err) {
70 return nil, err
71 }
72 listNodes, err := listNodeObjects.GetArray()
73 if checkWarn(err, "unable to get the node objects for the list: %s", err) {
74 return nil, err
75 }
76
77 var nodes = make([]MaasNode, len(listNodes))
78 for index, nodeObj := range listNodes {
79 node, err := nodeObj.GetMAASObject()
80 if !checkWarn(err, "unable to retrieve object for node: %s", err) {
81 nodes[index] = MaasNode{node}
82 }
83 }
84 return nodes, nil
85}
86
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -070087var log = logrus.New()
88
David K. Bainbridgeb5415042016-05-13 17:06:10 -070089func main() {
90
91 flag.Parse()
David K. Bainbridge6ea57c12016-06-06 23:29:12 -070092 config := Config{}
93 err := envconfig.Process("AUTOMATION", &config)
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -070094 if err != nil {
95 log.Fatalf("Unable to parse configuration options : %s", err)
96 }
David K. Bainbridgeb5415042016-05-13 17:06:10 -070097
98 options := ProcessingOptions{
David K. Bainbridgee9d7af72016-10-14 08:42:55 -070099 Preview: config.PreviewOnly,
100 AlwaysRename: config.AlwaysRename,
David K. Bainbridge068e87d2016-06-30 13:53:19 -0700101 Provisioner: NewProvisioner(&ProvisionerConfig{Url: config.ProvisionUrl}),
David K. Bainbridge6ea57c12016-06-06 23:29:12 -0700102 ProvisionURL: config.ProvisionUrl,
103 PowerHelper: config.PowerHelperScript,
104 PowerHelperUser: config.PowerHelperUser,
105 PowerHelperHost: config.PowerHelperHost,
David K. Bainbridgeefa951d2016-05-26 10:54:25 -0700106 }
107
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700108 switch config.LogFormat {
109 case "json":
110 log.Formatter = &logrus.JSONFormatter{}
111 default:
112 log.Formatter = &logrus.TextFormatter{
113 FullTimestamp: true,
114 ForceColors: true,
115 }
116 }
117
118 level, err := logrus.ParseLevel(config.LogLevel)
119 if err != nil {
120 level = logrus.WarnLevel
121 }
122 log.Level = level
123
David K. Bainbridge6ea57c12016-06-06 23:29:12 -0700124 options.ProvisionTTL, err = time.ParseDuration(config.ProvisionTtl)
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700125 checkError(err, "unable to parse specified duration of '%s' : %s", err)
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700126
127 // Determine the filter, this can either be specified on the the command
128 // line as a value or a file reference. If none is specified the default
129 // will be used
David K. Bainbridgee9d7af72016-10-14 08:42:55 -0700130 if len(config.FilterSpec) > 0 {
131 if config.FilterSpec[0] == '@' {
132 name := os.ExpandEnv((config.FilterSpec)[1:])
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700133 file, err := os.OpenFile(name, os.O_RDONLY, 0)
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700134 checkError(err, "unable to open file '%s' to load the filter : %s", name, err)
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700135 decoder := json.NewDecoder(file)
136 err = decoder.Decode(&options.Filter)
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700137 checkError(err, "unable to parse filter configuration from file '%s' : %s", name, err)
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700138 } else {
David K. Bainbridgee9d7af72016-10-14 08:42:55 -0700139 err := json.Unmarshal([]byte(config.FilterSpec), &options.Filter)
140 checkError(err, "unable to parse filter specification: '%s' : %s", config.FilterSpec, err)
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700141 }
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700142 }
143
144 // Determine the mac to name mapping, this can either be specified on the the command
145 // line as a value or a file reference. If none is specified the default
146 // will be used
David K. Bainbridgee9d7af72016-10-14 08:42:55 -0700147 if len(config.Mappings) > 0 {
148 if config.Mappings[0] == '@' {
149 name := os.ExpandEnv(config.Mappings[1:])
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700150 file, err := os.OpenFile(name, os.O_RDONLY, 0)
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700151 checkError(err, "unable to open file '%s' to load the mac name mapping : %s", name, err)
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700152 decoder := json.NewDecoder(file)
153 err = decoder.Decode(&options.Mappings)
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700154 checkError(err, "unable to parse filter configuration from file '%s' : %s", name, err)
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700155 } else {
David K. Bainbridgee9d7af72016-10-14 08:42:55 -0700156 err := json.Unmarshal([]byte(config.Mappings), &options.Mappings)
157 checkError(err, "unable to parse mac name mapping: '%s' : %s", config.Mappings, err)
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700158 }
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700159 }
160
David K. Bainbridgee9d7af72016-10-14 08:42:55 -0700161 // Get human readable strings for config output
162 mappingsAsJson, err := json.Marshal(options.Mappings)
163 checkError(err, "Unable to marshal MAC to hostname mappings to JSON : %s", err)
164 mappingsPrefix := ""
165
166 if len(config.Mappings) > 0 && config.Mappings[0] == '@' {
167 mappingsPrefix = "[" + config.Mappings + "]"
168 }
169
170 filterAsJson, err := json.Marshal(options.Filter)
171 checkError(err, "Unable to marshal host filter to JSON : %s", err)
172 filterPrefix := ""
173 if len(config.FilterSpec) > 0 && config.FilterSpec[0] == '@' {
174 filterPrefix = "[" + config.FilterSpec + "]"
175 }
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700176
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700177 log.Infof(`Configuration:
David K. Bainbridgee9d7af72016-10-14 08:42:55 -0700178 POWER_HELPER_USER: %s
179 POWER_HELPER_HOST: %s
180 POWER_HELPER_SCRIPT: %s
181 PROVISION_URL: %s
182 PROVISION_TTL: %s
183 MAAS_URL: %s
184 MAAS_API_KEY: %s
185 MAAS_API_VERSION: %s
186 MAAS_QUERY_INTERVAL: %s
187 HOST_FILTER_SPEC: %+v
188 MAC_TO_NAME_MAPPINGS:%+v
189 PREVIEW_ONLY: %t
190 ALWAYS_RENAME: %t
191 LOG_LEVEL: %s
192 LOG_FORMAT: %s`,
193 config.PowerHelperUser, config.PowerHelperHost, config.PowerHelperScript,
194 config.ProvisionUrl, config.ProvisionTtl,
195 config.MaasUrl, config.ApiKey, config.ApiVersion, config.QueryInterval,
196 filterPrefix+string(filterAsJson), mappingsPrefix+string(mappingsAsJson),
197 config.PreviewOnly, config.AlwaysRename,
198 config.LogLevel, config.LogFormat)
David K. Bainbridge37ccf1e2016-06-02 12:47:15 -0700199
David K. Bainbridgee9d7af72016-10-14 08:42:55 -0700200 authClient, err := maas.NewAuthenticatedClient(config.MaasUrl, config.ApiKey, config.ApiVersion)
201 checkError(err, "Unable to use specified client key, '%s', to authenticate to the MAAS server: %s",
202 config.ApiKey, err)
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700203
204 // Create an object through which we will communicate with MAAS
205 client := maas.NewMAAS(*authClient)
206
207 // This utility essentially polls the MAAS server for node state and
208 // process the node to the next state. This is done by kicking off the
209 // process every specified duration. This means that the first processing of
210 // nodes will have "period" in the future. This is really not the behavior
211 // we want, we really want, do it now, and then do the next one in "period".
212 // So, the code does one now.
213 nodes, _ := fetchNodes(client)
214 ProcessAll(client, nodes, options)
215
David K. Bainbridgee9d7af72016-10-14 08:42:55 -0700216 if !(config.PreviewOnly) {
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700217 // Create a ticker and fetch and process the nodes every "period"
David K. Bainbridgee9d7af72016-10-14 08:42:55 -0700218 for {
219 log.Infof("query server at %s", time.Now())
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700220 nodes, _ := fetchNodes(client)
221 ProcessAll(client, nodes, options)
David K. Bainbridgee9d7af72016-10-14 08:42:55 -0700222
223 // Sleep for the Interval and then process again.
224 time.Sleep(config.QueryInterval)
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700225 }
226 }
227}