blob: e946ff12e157bad79cc8904164305ac3c17f35f8 [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. Bainbridgef694f5a2016-06-10 16:21:27 -070014package main
15
16import (
David K. Bainbridge97ee8052016-06-14 00:52:07 -070017 "bytes"
18 "encoding/json"
David K. Bainbridgef694f5a2016-06-10 16:21:27 -070019 "fmt"
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -070020 "github.com/Sirupsen/logrus"
David K. Bainbridge3569d622016-09-16 08:40:54 -070021 "github.com/gorilla/mux"
22 maas "github.com/juju/gomaasapi"
David K. Bainbridgef694f5a2016-06-10 16:21:27 -070023 "github.com/kelseyhightower/envconfig"
David K. Bainbridge11850cb2016-10-28 14:05:59 -070024 "io/ioutil"
David K. Bainbridge97ee8052016-06-14 00:52:07 -070025 "net/http"
David K. Bainbridge11850cb2016-10-28 14:05:59 -070026 "regexp"
David K. Bainbridge3569d622016-09-16 08:40:54 -070027 "sync"
David K. Bainbridgef694f5a2016-06-10 16:21:27 -070028 "time"
29)
30
31type Config struct {
David K. Bainbridge11850cb2016-10-28 14:05:59 -070032 VendorsURL string `default:"file:///switchq/vendors.json" envconfig:"VENDORS_URL"`
33 AddressURL string `default:"file:///switchq/dhcp_harvest.inc" envconfig:"ADDRESS_URL"`
34 PollInterval string `default:"1m" envconfig:"POLL_INTERVAL"`
35 ProvisionTTL string `default:"1h" envconfig:"PROVISION_TTL"`
36 ProvisionURL string `default:"" envconfig:"PROVISION_URL"`
37 RoleSelectorURL string `default:"" envconfig:"ROLE_SELECTOR_URL"`
38 DefaultRole string `default:"fabric-switch" envconfig:"DEFAULT_ROLE"`
David K. Bainbridge97ee8052016-06-14 00:52:07 -070039 Script string `default:"do-ansible"`
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -070040 LogLevel string `default:"warning" envconfig:"LOG_LEVEL"`
41 LogFormat string `default:"text" envconfig:"LOG_FORMAT"`
David K. Bainbridge3569d622016-09-16 08:40:54 -070042 Listen string `default:""`
43 Port int `default:"4244"`
44 MaasURL string `default:"http://localhost/MAAS" envconfig:"MAAS_URL"`
45 MaasKey string `default:"" envconfig:"MAAS_API_KEY"`
David K. Bainbridge11850cb2016-10-28 14:05:59 -070046 ShowApiKey bool `default:"false" envconfig:"MAAS_SHOW_API_KEY"`
47 ApiKeyFile string `default:"/secrets/maas_api_key" envconfig:"MAAS_API_KEY_FILE"`
David K. Bainbridgef694f5a2016-06-10 16:21:27 -070048
David K. Bainbridge97ee8052016-06-14 00:52:07 -070049 vendors Vendors
David K. Bainbridgef694f5a2016-06-10 16:21:27 -070050 addressSource AddressSource
51 interval time.Duration
52 ttl time.Duration
53}
54
David K. Bainbridge52f29542016-07-27 22:28:15 -070055const (
56 Pending TaskStatus = iota
57 Running
58 Complete
59 Failed
60)
61
62type RequestInfo struct {
63 Id string `json:"id"`
64 Name string `json:"name"`
65 Ip string `json:"ip"`
66 Mac string `json:"mac"`
67 RoleSelector string `json:"role_selector"`
68 Role string `json:"role"`
69 Script string `json:"script"`
70}
71
72type TaskStatus uint8
73
74type WorkRequest struct {
75 Info *RequestInfo
76 Script string
77 Role string
78}
79
80type StatusMsg struct {
81 Request *WorkRequest `json:"request"`
82 Worker int `json:"worker"`
83 Status TaskStatus `json:"status"`
84 Message string `json:"message"`
85 Timestamp int64 `json:"timestamp"`
86}
87
David K. Bainbridge3569d622016-09-16 08:40:54 -070088type AppContext struct {
89 config Config
90
91 maasClient *maas.MAASObject
92 pushChan chan []AddressRec
93 mutex sync.RWMutex
94 nextList []AddressRec
95 publishList []AddressRec
96}
97
David K. Bainbridgef694f5a2016-06-10 16:21:27 -070098func checkError(err error, msg string, args ...interface{}) {
99 if err != nil {
100 log.Fatalf(msg, args...)
101 }
102}
103
David K. Bainbridge3569d622016-09-16 08:40:54 -0700104func (c *AppContext) getProvisionedState(rec AddressRec) (*StatusMsg, error) {
105 if len(c.config.ProvisionURL) == 0 {
106 log.Warnf("Unable to fetch provisioning state of device '%s' (%s, %s) as no URL for the provisioner was specified",
107 rec.Name, rec.IP, rec.MAC)
108 return nil, fmt.Errorf("No URL for provisioner specified")
109 }
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700110 log.Debugf("Fetching provisioned state of device '%s' (%s, %s)",
David K. Bainbridgec809ef72016-06-22 21:18:07 -0700111 rec.Name, rec.IP, rec.MAC)
David K. Bainbridge3569d622016-09-16 08:40:54 -0700112 resp, err := http.Get(c.config.ProvisionURL + rec.MAC)
David K. Bainbridge97ee8052016-06-14 00:52:07 -0700113 if err != nil {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700114 log.Errorf("Error while retrieving provisioning state for device '%s (%s, %s)' : %s",
David K. Bainbridgec809ef72016-06-22 21:18:07 -0700115 rec.Name, rec.IP, rec.MAC, err)
David K. Bainbridge52f29542016-07-27 22:28:15 -0700116 return nil, err
David K. Bainbridge97ee8052016-06-14 00:52:07 -0700117 }
118 if resp.StatusCode != 404 && int(resp.StatusCode/100) != 2 {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700119 log.Errorf("Error while retrieving provisioning state for device '%s (%s, %s)' : %s",
David K. Bainbridgec809ef72016-06-22 21:18:07 -0700120 rec.Name, rec.IP, rec.MAC, resp.Status)
David K. Bainbridge52f29542016-07-27 22:28:15 -0700121 return nil, fmt.Errorf(resp.Status)
David K. Bainbridge97ee8052016-06-14 00:52:07 -0700122 }
123 defer resp.Body.Close()
124 if resp.StatusCode != 404 {
125 decoder := json.NewDecoder(resp.Body)
David K. Bainbridge52f29542016-07-27 22:28:15 -0700126 var status StatusMsg
127 err = decoder.Decode(&status)
David K. Bainbridge97ee8052016-06-14 00:52:07 -0700128 if err != nil {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700129 log.Errorf("Unmarshal provisioning service response for device '%s (%s, %s)' : %s",
David K. Bainbridgec809ef72016-06-22 21:18:07 -0700130 rec.Name, rec.IP, rec.MAC, err)
David K. Bainbridge52f29542016-07-27 22:28:15 -0700131 return nil, err
David K. Bainbridge97ee8052016-06-14 00:52:07 -0700132 }
David K. Bainbridge52f29542016-07-27 22:28:15 -0700133 return &status, nil
David K. Bainbridge97ee8052016-06-14 00:52:07 -0700134 }
David K. Bainbridgec809ef72016-06-22 21:18:07 -0700135
136 // If we end up here that means that no record was found in the provisioning, so return
137 // a status of -1, w/o an error
David K. Bainbridge52f29542016-07-27 22:28:15 -0700138 return nil, nil
David K. Bainbridgec809ef72016-06-22 21:18:07 -0700139}
140
David K. Bainbridge3569d622016-09-16 08:40:54 -0700141func (c *AppContext) provision(rec AddressRec) error {
142 if len(c.config.ProvisionURL) == 0 {
143 log.Warnf("Unable to POST to provisioner for device '%s' (%s, %s) as no URL for the provisioner was specified",
144 rec.Name, rec.IP, rec.MAC)
145 return fmt.Errorf("No URL for provisioner specified")
146 }
147 log.Infof("POSTing to '%s' for provisioning of '%s (%s)'", c.config.ProvisionURL, rec.Name, rec.MAC)
David K. Bainbridge97ee8052016-06-14 00:52:07 -0700148 data := map[string]string{
149 "id": rec.MAC,
150 "name": rec.Name,
151 "ip": rec.IP,
152 "mac": rec.MAC,
153 }
David K. Bainbridge3569d622016-09-16 08:40:54 -0700154 if c.config.RoleSelectorURL != "" {
155 data["role_selector"] = c.config.RoleSelectorURL
David K. Bainbridge97ee8052016-06-14 00:52:07 -0700156 }
David K. Bainbridge3569d622016-09-16 08:40:54 -0700157 if c.config.DefaultRole != "" {
158 data["role"] = c.config.DefaultRole
David K. Bainbridge97ee8052016-06-14 00:52:07 -0700159 }
David K. Bainbridge3569d622016-09-16 08:40:54 -0700160 if c.config.Script != "" {
161 data["script"] = c.config.Script
David K. Bainbridgef694f5a2016-06-10 16:21:27 -0700162 }
163
David K. Bainbridge97ee8052016-06-14 00:52:07 -0700164 hc := http.Client{}
165 var b []byte
David K. Bainbridgec809ef72016-06-22 21:18:07 -0700166 b, err := json.Marshal(data)
David K. Bainbridge97ee8052016-06-14 00:52:07 -0700167 if err != nil {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700168 log.Errorf("Unable to marshal provisioning data : %s", err)
David K. Bainbridge97ee8052016-06-14 00:52:07 -0700169 return err
170 }
David K. Bainbridge3569d622016-09-16 08:40:54 -0700171 req, err := http.NewRequest("POST", c.config.ProvisionURL, bytes.NewReader(b))
David K. Bainbridge97ee8052016-06-14 00:52:07 -0700172 if err != nil {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700173 log.Errorf("Unable to construct POST request to provisioner : %s", err)
David K. Bainbridge97ee8052016-06-14 00:52:07 -0700174 return err
175 }
176
177 req.Header.Add("Content-Type", "application/json")
David K. Bainbridgec809ef72016-06-22 21:18:07 -0700178 resp, err := hc.Do(req)
David K. Bainbridge97ee8052016-06-14 00:52:07 -0700179 if err != nil {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700180 log.Errorf("Unable to POST request to provisioner : %s", err)
David K. Bainbridge97ee8052016-06-14 00:52:07 -0700181 return err
182 }
183
184 defer resp.Body.Close()
185 if resp.StatusCode != http.StatusAccepted {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700186 log.Errorf("Provisioning request not accepted by provisioner : %s", resp.Status)
David K. Bainbridge97ee8052016-06-14 00:52:07 -0700187 return err
188 }
189
David K. Bainbridge97ee8052016-06-14 00:52:07 -0700190 return nil
191}
192
David K. Bainbridge3569d622016-09-16 08:40:54 -0700193func (c *AppContext) processRecord(rec AddressRec) error {
194 ok, err := c.config.vendors.Switchq(rec.MAC)
David K. Bainbridgef694f5a2016-06-10 16:21:27 -0700195 if err != nil {
196 return fmt.Errorf("unable to determine ventor of MAC '%s' (%s)", rec.MAC, err)
197 }
198
199 if !ok {
200 // Not something we care about
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700201 log.Debugf("host with IP '%s' and MAC '%s' and named '%s' not a known switch type",
David K. Bainbridgef694f5a2016-06-10 16:21:27 -0700202 rec.IP, rec.MAC, rec.Name)
203 return nil
204 }
205
David K. Bainbridge3569d622016-09-16 08:40:54 -0700206 // Add this IP information to our list of known switches
207 c.nextList = append(c.nextList, rec)
208
David K. Bainbridgec809ef72016-06-22 21:18:07 -0700209 // Verify if the provision status of the node is complete, if in an error state then TTL means
210 // nothing
David K. Bainbridge52f29542016-07-27 22:28:15 -0700211 state, err := c.getProvisionedState(rec)
212 if state != nil {
213 switch state.Status {
214 case Pending, Running: // Pending or Running
215 log.Debugf("device '%s' (%s, %s) is being provisioned",
216 rec.Name, rec.IP, rec.MAC)
David K. Bainbridgec809ef72016-06-22 21:18:07 -0700217 return nil
David K. Bainbridge52f29542016-07-27 22:28:15 -0700218 case Complete: // Complete
219 log.Debugf("device '%s' (%s, %s) has completed provisioning",
220 rec.Name, rec.IP, rec.MAC)
221 case Failed: // Failed
222 log.Debugf("device '%s' (%s, %s) failed last provisioning with message '%s', reattempt",
223 rec.Name, rec.IP, rec.MAC, state.Message)
David K. Bainbridge98bbc042016-08-22 17:35:28 -0700224 state = nil
David K. Bainbridge52f29542016-07-27 22:28:15 -0700225 default: // Unknown state
226 log.Debugf("device '%s' (%s, %s) has unknown provisioning state '%d', will provision",
227 rec.Name, rec.IP, rec.MAC, state.Status)
David K. Bainbridge98bbc042016-08-22 17:35:28 -0700228 state = nil
David K. Bainbridgec809ef72016-06-22 21:18:07 -0700229 }
David K. Bainbridge52f29542016-07-27 22:28:15 -0700230 } else {
231 log.Debugf("device '%s' (%s, %s) has no provisioning record",
232 rec.Name, rec.IP, rec.MAC)
David K. Bainbridgec809ef72016-06-22 21:18:07 -0700233 }
234
David K. Bainbridge97ee8052016-06-14 00:52:07 -0700235 // If TTL is 0 then we will only provision a switch once.
David K. Bainbridge3569d622016-09-16 08:40:54 -0700236 if state == nil || (c.config.ttl > 0 && time.Since(time.Unix(state.Timestamp, 0)) > c.config.ttl) {
David K. Bainbridge52f29542016-07-27 22:28:15 -0700237 if state != nil {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700238 log.Debugf("device '%s' (%s, %s) TTL expired, reprovisioning",
David K. Bainbridgec809ef72016-06-22 21:18:07 -0700239 rec.Name, rec.IP, rec.MAC)
240 }
David K. Bainbridge97ee8052016-06-14 00:52:07 -0700241 c.provision(rec)
David K. Bainbridge3569d622016-09-16 08:40:54 -0700242 } else if c.config.ttl == 0 {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700243 log.Debugf("device '%s' (%s, %s) has completed its one time provisioning, with a TTL set to %s",
David K. Bainbridge3569d622016-09-16 08:40:54 -0700244 rec.Name, rec.IP, rec.MAC, c.config.ProvisionTTL)
David K. Bainbridge97ee8052016-06-14 00:52:07 -0700245 } else {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700246 log.Debugf("device '%s' (%s, %s) has completed provisioning within the specified TTL of %s",
David K. Bainbridge3569d622016-09-16 08:40:54 -0700247 rec.Name, rec.IP, rec.MAC, c.config.ProvisionTTL)
David K. Bainbridgef694f5a2016-06-10 16:21:27 -0700248 }
249 return nil
250}
251
David K. Bainbridge3569d622016-09-16 08:40:54 -0700252func (c *AppContext) processLoop() {
253 // We use two methods to attempt to find the MAC (hardware) address associated with an IP. The first
254 // is to look in the table. The second is to send an ARP packet.
255 for {
256 log.Infof("Checking for switches @ %s", time.Now())
257 addresses, err := c.config.addressSource.GetAddresses()
258
259 if err != nil {
260 log.Errorf("unable to read addresses from address source : %s", err)
261 } else {
262 log.Infof("Queried %d addresses from address source", len(addresses))
263
264 c.nextList = make([]AddressRec, 0, len(addresses))
265 for _, rec := range addresses {
266 log.Debugf("Processing %s(%s, %s)", rec.Name, rec.IP, rec.MAC)
267 if err := c.processRecord(rec); err != nil {
268 log.Errorf("Error when processing IP '%s' : %s", rec.IP, err)
269 }
270 }
271 c.mutex.Lock()
272 c.publishList = c.nextList
273 c.nextList = nil
274 c.mutex.Unlock()
275 c.pushChan <- c.publishList
276 }
277
278 time.Sleep(c.config.interval)
279 }
280}
281
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700282var log = logrus.New()
283
David K. Bainbridgef694f5a2016-06-10 16:21:27 -0700284func main() {
285
286 var err error
David K. Bainbridge3569d622016-09-16 08:40:54 -0700287 context := &AppContext{}
288 err = envconfig.Process("SWITCHQ", &context.config)
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700289 if err != nil {
290 log.Fatalf("Unable to parse configuration options : %s", err)
291 }
292
David K. Bainbridge3569d622016-09-16 08:40:54 -0700293 switch context.config.LogFormat {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700294 case "json":
295 log.Formatter = &logrus.JSONFormatter{}
296 default:
297 log.Formatter = &logrus.TextFormatter{
298 FullTimestamp: true,
299 ForceColors: true,
300 }
301 }
302
David K. Bainbridge3569d622016-09-16 08:40:54 -0700303 level, err := logrus.ParseLevel(context.config.LogLevel)
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700304 if err != nil {
305 level = logrus.WarnLevel
306 }
307 log.Level = level
David K. Bainbridgef694f5a2016-06-10 16:21:27 -0700308
David K. Bainbridge11850cb2016-10-28 14:05:59 -0700309 re := regexp.MustCompile("[^:]")
310 pubKey := context.config.MaasKey
311 if !context.config.ShowApiKey {
312 pubKey = re.ReplaceAllString(context.config.MaasKey, "X")
313 }
314
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700315 log.Infof(`Configuration:
David K. Bainbridge11850cb2016-10-28 14:05:59 -0700316 VENDORS_URL: %s
317 POLL_INTERVAL: %s
318 ADDRESS_URL: %s
319 PROVISION_TTL: %s
320 PROVISION_URL: %s
321 ROLE_SELECTOR_URL: %s
322 DEFAULT_ROLE: %s
323 SCRIPT: %s
324 LISTEN: %s
325 PORT: %d
326 MAAS_URL: %s
327 MAAS_SHOW_API_KEY %t
328 MAAS_API_KEY: %s
329 MAAS_API_KEY_FILE: %s
330 LOG_LEVEL: %s
331 LOG_FORMAT: %s`,
David K. Bainbridge3569d622016-09-16 08:40:54 -0700332 context.config.VendorsURL, context.config.PollInterval, context.config.AddressURL, context.config.ProvisionTTL,
333 context.config.ProvisionURL, context.config.RoleSelectorURL, context.config.DefaultRole, context.config.Script,
David K. Bainbridge11850cb2016-10-28 14:05:59 -0700334 context.config.Listen, context.config.Port, context.config.MaasURL, context.config.ShowApiKey, pubKey,
335 context.config.ApiKeyFile, context.config.LogLevel, context.config.LogFormat)
David K. Bainbridgef694f5a2016-06-10 16:21:27 -0700336
David K. Bainbridge3569d622016-09-16 08:40:54 -0700337 context.config.vendors, err = NewVendors(context.config.VendorsURL)
338 checkError(err, "Unable to create known vendors list from specified URL '%s' : %s", context.config.VendorsURL, err)
David K. Bainbridgef694f5a2016-06-10 16:21:27 -0700339
David K. Bainbridge3569d622016-09-16 08:40:54 -0700340 context.config.addressSource, err = NewAddressSource(context.config.AddressURL)
341 checkError(err, "Unable to create required address source for specified URL '%s' : %s", context.config.AddressURL, err)
David K. Bainbridgef694f5a2016-06-10 16:21:27 -0700342
David K. Bainbridge3569d622016-09-16 08:40:54 -0700343 context.config.interval, err = time.ParseDuration(context.config.PollInterval)
344 checkError(err, "Unable to parse specified poll interface '%s' : %s", context.config.PollInterval, err)
David K. Bainbridgef694f5a2016-06-10 16:21:27 -0700345
David K. Bainbridge3569d622016-09-16 08:40:54 -0700346 context.config.ttl, err = time.ParseDuration(context.config.ProvisionTTL)
347 checkError(err, "Unable to parse specified provision TTL value of '%s' : %s", context.config.ProvisionTTL, err)
348
David K. Bainbridge11850cb2016-10-28 14:05:59 -0700349 // Attempt to load the API key from a file if it was not set via the environment
350 // and if the file exists
351 if context.config.MaasKey == "" {
352 log.Debugf("Attempting to read MAAS API key from file '%s', because it was not set via environment", context.config.ApiKeyFile)
353 keyBytes, err := ioutil.ReadFile(context.config.ApiKeyFile)
354 if err != nil {
355 log.Warnf("Failed to read MAAS API key from file '%s', was the file mounted as a volume? : %s ",
356 context.config.ApiKeyFile, err)
357 } else {
358 context.config.MaasKey = string(keyBytes)
359 if context.config.ShowApiKey {
360 pubKey = context.config.MaasKey
361 } else {
362 pubKey = re.ReplaceAllString(context.config.MaasKey, "X")
363 }
364 }
365 }
366
David K. Bainbridge3569d622016-09-16 08:40:54 -0700367 if len(context.config.MaasURL) > 0 {
368
369 // Attempt to connect to MAAS
370 authClient, err := maas.NewAuthenticatedClient(context.config.MaasURL, context.config.MaasKey, "1.0")
371 checkError(err, "Unable to connect to MAAS at '%s' : %s", context.config.MaasURL, err)
372
373 context.maasClient = maas.NewMAAS(*authClient)
374 }
375
376 context.pushChan = make(chan []AddressRec, 1)
377
378 go context.processLoop()
379 go context.syncToMaas(context.pushChan)
380
381 router := mux.NewRouter()
382 router.HandleFunc("/switch/", context.ListSwitchesHandler).Methods("GET")
383 http.Handle("/", router)
384 log.Infof("Listening for HTTP request on '%s:%d'", context.config.Listen, context.config.Port)
385 err = http.ListenAndServe(fmt.Sprintf("%s:%d", context.config.Listen, context.config.Port), nil)
386 if err != nil {
387 checkError(err, "Error while attempting to listen to REST requests on '%s:%d' : %s",
388 context.config.Listen, context.config.Port, err)
David K. Bainbridgef694f5a2016-06-10 16:21:27 -0700389 }
390}