blob: deddf7464d7f101994409f4b226ae76ea78d781c [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. Bainbridge97ee8052016-06-14 00:52:07 -070024 "net/http"
David K. Bainbridge3569d622016-09-16 08:40:54 -070025 "sync"
David K. Bainbridgef694f5a2016-06-10 16:21:27 -070026 "time"
27)
28
29type Config struct {
David K. Bainbridge97ee8052016-06-14 00:52:07 -070030 VendorsURL string `default:"file:///switchq/vendors.json" envconfig:"vendors_url"`
David K. Bainbridge97ee8052016-06-14 00:52:07 -070031 AddressURL string `default:"file:///switchq/dhcp_harvest.inc" envconfig:"address_url"`
32 PollInterval string `default:"1m" envconfig:"poll_interval"`
33 ProvisionTTL string `default:"1h" envconfig:"provision_ttl"`
34 ProvisionURL string `default:"" envconfig:"provision_url"`
35 RoleSelectorURL string `default:"" envconfig:"role_selector_url"`
36 DefaultRole string `default:"fabric-switch" envconfig:"default_role"`
37 Script string `default:"do-ansible"`
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -070038 LogLevel string `default:"warning" envconfig:"LOG_LEVEL"`
39 LogFormat string `default:"text" envconfig:"LOG_FORMAT"`
David K. Bainbridge3569d622016-09-16 08:40:54 -070040 Listen string `default:""`
41 Port int `default:"4244"`
42 MaasURL string `default:"http://localhost/MAAS" envconfig:"MAAS_URL"`
43 MaasKey string `default:"" envconfig:"MAAS_API_KEY"`
David K. Bainbridgef694f5a2016-06-10 16:21:27 -070044
David K. Bainbridge97ee8052016-06-14 00:52:07 -070045 vendors Vendors
David K. Bainbridgef694f5a2016-06-10 16:21:27 -070046 addressSource AddressSource
47 interval time.Duration
48 ttl time.Duration
49}
50
David K. Bainbridge52f29542016-07-27 22:28:15 -070051const (
52 Pending TaskStatus = iota
53 Running
54 Complete
55 Failed
56)
57
58type RequestInfo struct {
59 Id string `json:"id"`
60 Name string `json:"name"`
61 Ip string `json:"ip"`
62 Mac string `json:"mac"`
63 RoleSelector string `json:"role_selector"`
64 Role string `json:"role"`
65 Script string `json:"script"`
66}
67
68type TaskStatus uint8
69
70type WorkRequest struct {
71 Info *RequestInfo
72 Script string
73 Role string
74}
75
76type StatusMsg struct {
77 Request *WorkRequest `json:"request"`
78 Worker int `json:"worker"`
79 Status TaskStatus `json:"status"`
80 Message string `json:"message"`
81 Timestamp int64 `json:"timestamp"`
82}
83
David K. Bainbridge3569d622016-09-16 08:40:54 -070084type AppContext struct {
85 config Config
86
87 maasClient *maas.MAASObject
88 pushChan chan []AddressRec
89 mutex sync.RWMutex
90 nextList []AddressRec
91 publishList []AddressRec
92}
93
David K. Bainbridgef694f5a2016-06-10 16:21:27 -070094func checkError(err error, msg string, args ...interface{}) {
95 if err != nil {
96 log.Fatalf(msg, args...)
97 }
98}
99
David K. Bainbridge3569d622016-09-16 08:40:54 -0700100func (c *AppContext) getProvisionedState(rec AddressRec) (*StatusMsg, error) {
101 if len(c.config.ProvisionURL) == 0 {
102 log.Warnf("Unable to fetch provisioning state of device '%s' (%s, %s) as no URL for the provisioner was specified",
103 rec.Name, rec.IP, rec.MAC)
104 return nil, fmt.Errorf("No URL for provisioner specified")
105 }
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700106 log.Debugf("Fetching provisioned state of device '%s' (%s, %s)",
David K. Bainbridgec809ef72016-06-22 21:18:07 -0700107 rec.Name, rec.IP, rec.MAC)
David K. Bainbridge3569d622016-09-16 08:40:54 -0700108 resp, err := http.Get(c.config.ProvisionURL + rec.MAC)
David K. Bainbridge97ee8052016-06-14 00:52:07 -0700109 if err != nil {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700110 log.Errorf("Error while retrieving provisioning state for device '%s (%s, %s)' : %s",
David K. Bainbridgec809ef72016-06-22 21:18:07 -0700111 rec.Name, rec.IP, rec.MAC, err)
David K. Bainbridge52f29542016-07-27 22:28:15 -0700112 return nil, err
David K. Bainbridge97ee8052016-06-14 00:52:07 -0700113 }
114 if resp.StatusCode != 404 && int(resp.StatusCode/100) != 2 {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700115 log.Errorf("Error while retrieving provisioning state for device '%s (%s, %s)' : %s",
David K. Bainbridgec809ef72016-06-22 21:18:07 -0700116 rec.Name, rec.IP, rec.MAC, resp.Status)
David K. Bainbridge52f29542016-07-27 22:28:15 -0700117 return nil, fmt.Errorf(resp.Status)
David K. Bainbridge97ee8052016-06-14 00:52:07 -0700118 }
119 defer resp.Body.Close()
120 if resp.StatusCode != 404 {
121 decoder := json.NewDecoder(resp.Body)
David K. Bainbridge52f29542016-07-27 22:28:15 -0700122 var status StatusMsg
123 err = decoder.Decode(&status)
David K. Bainbridge97ee8052016-06-14 00:52:07 -0700124 if err != nil {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700125 log.Errorf("Unmarshal provisioning service response for device '%s (%s, %s)' : %s",
David K. Bainbridgec809ef72016-06-22 21:18:07 -0700126 rec.Name, rec.IP, rec.MAC, err)
David K. Bainbridge52f29542016-07-27 22:28:15 -0700127 return nil, err
David K. Bainbridge97ee8052016-06-14 00:52:07 -0700128 }
David K. Bainbridge52f29542016-07-27 22:28:15 -0700129 return &status, nil
David K. Bainbridge97ee8052016-06-14 00:52:07 -0700130 }
David K. Bainbridgec809ef72016-06-22 21:18:07 -0700131
132 // If we end up here that means that no record was found in the provisioning, so return
133 // a status of -1, w/o an error
David K. Bainbridge52f29542016-07-27 22:28:15 -0700134 return nil, nil
David K. Bainbridgec809ef72016-06-22 21:18:07 -0700135}
136
David K. Bainbridge3569d622016-09-16 08:40:54 -0700137func (c *AppContext) provision(rec AddressRec) error {
138 if len(c.config.ProvisionURL) == 0 {
139 log.Warnf("Unable to POST to provisioner for device '%s' (%s, %s) as no URL for the provisioner was specified",
140 rec.Name, rec.IP, rec.MAC)
141 return fmt.Errorf("No URL for provisioner specified")
142 }
143 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 -0700144 data := map[string]string{
145 "id": rec.MAC,
146 "name": rec.Name,
147 "ip": rec.IP,
148 "mac": rec.MAC,
149 }
David K. Bainbridge3569d622016-09-16 08:40:54 -0700150 if c.config.RoleSelectorURL != "" {
151 data["role_selector"] = c.config.RoleSelectorURL
David K. Bainbridge97ee8052016-06-14 00:52:07 -0700152 }
David K. Bainbridge3569d622016-09-16 08:40:54 -0700153 if c.config.DefaultRole != "" {
154 data["role"] = c.config.DefaultRole
David K. Bainbridge97ee8052016-06-14 00:52:07 -0700155 }
David K. Bainbridge3569d622016-09-16 08:40:54 -0700156 if c.config.Script != "" {
157 data["script"] = c.config.Script
David K. Bainbridgef694f5a2016-06-10 16:21:27 -0700158 }
159
David K. Bainbridge97ee8052016-06-14 00:52:07 -0700160 hc := http.Client{}
161 var b []byte
David K. Bainbridgec809ef72016-06-22 21:18:07 -0700162 b, err := json.Marshal(data)
David K. Bainbridge97ee8052016-06-14 00:52:07 -0700163 if err != nil {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700164 log.Errorf("Unable to marshal provisioning data : %s", err)
David K. Bainbridge97ee8052016-06-14 00:52:07 -0700165 return err
166 }
David K. Bainbridge3569d622016-09-16 08:40:54 -0700167 req, err := http.NewRequest("POST", c.config.ProvisionURL, bytes.NewReader(b))
David K. Bainbridge97ee8052016-06-14 00:52:07 -0700168 if err != nil {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700169 log.Errorf("Unable to construct POST request to provisioner : %s", err)
David K. Bainbridge97ee8052016-06-14 00:52:07 -0700170 return err
171 }
172
173 req.Header.Add("Content-Type", "application/json")
David K. Bainbridgec809ef72016-06-22 21:18:07 -0700174 resp, err := hc.Do(req)
David K. Bainbridge97ee8052016-06-14 00:52:07 -0700175 if err != nil {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700176 log.Errorf("Unable to POST request to provisioner : %s", err)
David K. Bainbridge97ee8052016-06-14 00:52:07 -0700177 return err
178 }
179
180 defer resp.Body.Close()
181 if resp.StatusCode != http.StatusAccepted {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700182 log.Errorf("Provisioning request not accepted by provisioner : %s", resp.Status)
David K. Bainbridge97ee8052016-06-14 00:52:07 -0700183 return err
184 }
185
David K. Bainbridge97ee8052016-06-14 00:52:07 -0700186 return nil
187}
188
David K. Bainbridge3569d622016-09-16 08:40:54 -0700189func (c *AppContext) processRecord(rec AddressRec) error {
190 ok, err := c.config.vendors.Switchq(rec.MAC)
David K. Bainbridgef694f5a2016-06-10 16:21:27 -0700191 if err != nil {
192 return fmt.Errorf("unable to determine ventor of MAC '%s' (%s)", rec.MAC, err)
193 }
194
195 if !ok {
196 // Not something we care about
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700197 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 -0700198 rec.IP, rec.MAC, rec.Name)
199 return nil
200 }
201
David K. Bainbridge3569d622016-09-16 08:40:54 -0700202 // Add this IP information to our list of known switches
203 c.nextList = append(c.nextList, rec)
204
David K. Bainbridgec809ef72016-06-22 21:18:07 -0700205 // Verify if the provision status of the node is complete, if in an error state then TTL means
206 // nothing
David K. Bainbridge52f29542016-07-27 22:28:15 -0700207 state, err := c.getProvisionedState(rec)
208 if state != nil {
209 switch state.Status {
210 case Pending, Running: // Pending or Running
211 log.Debugf("device '%s' (%s, %s) is being provisioned",
212 rec.Name, rec.IP, rec.MAC)
David K. Bainbridgec809ef72016-06-22 21:18:07 -0700213 return nil
David K. Bainbridge52f29542016-07-27 22:28:15 -0700214 case Complete: // Complete
215 log.Debugf("device '%s' (%s, %s) has completed provisioning",
216 rec.Name, rec.IP, rec.MAC)
217 case Failed: // Failed
218 log.Debugf("device '%s' (%s, %s) failed last provisioning with message '%s', reattempt",
219 rec.Name, rec.IP, rec.MAC, state.Message)
David K. Bainbridge98bbc042016-08-22 17:35:28 -0700220 state = nil
David K. Bainbridge52f29542016-07-27 22:28:15 -0700221 default: // Unknown state
222 log.Debugf("device '%s' (%s, %s) has unknown provisioning state '%d', will provision",
223 rec.Name, rec.IP, rec.MAC, state.Status)
David K. Bainbridge98bbc042016-08-22 17:35:28 -0700224 state = nil
David K. Bainbridgec809ef72016-06-22 21:18:07 -0700225 }
David K. Bainbridge52f29542016-07-27 22:28:15 -0700226 } else {
227 log.Debugf("device '%s' (%s, %s) has no provisioning record",
228 rec.Name, rec.IP, rec.MAC)
David K. Bainbridgec809ef72016-06-22 21:18:07 -0700229 }
230
David K. Bainbridge97ee8052016-06-14 00:52:07 -0700231 // If TTL is 0 then we will only provision a switch once.
David K. Bainbridge3569d622016-09-16 08:40:54 -0700232 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 -0700233 if state != nil {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700234 log.Debugf("device '%s' (%s, %s) TTL expired, reprovisioning",
David K. Bainbridgec809ef72016-06-22 21:18:07 -0700235 rec.Name, rec.IP, rec.MAC)
236 }
David K. Bainbridge97ee8052016-06-14 00:52:07 -0700237 c.provision(rec)
David K. Bainbridge3569d622016-09-16 08:40:54 -0700238 } else if c.config.ttl == 0 {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700239 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 -0700240 rec.Name, rec.IP, rec.MAC, c.config.ProvisionTTL)
David K. Bainbridge97ee8052016-06-14 00:52:07 -0700241 } else {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700242 log.Debugf("device '%s' (%s, %s) has completed provisioning within the specified TTL of %s",
David K. Bainbridge3569d622016-09-16 08:40:54 -0700243 rec.Name, rec.IP, rec.MAC, c.config.ProvisionTTL)
David K. Bainbridgef694f5a2016-06-10 16:21:27 -0700244 }
245 return nil
246}
247
David K. Bainbridge3569d622016-09-16 08:40:54 -0700248func (c *AppContext) processLoop() {
249 // We use two methods to attempt to find the MAC (hardware) address associated with an IP. The first
250 // is to look in the table. The second is to send an ARP packet.
251 for {
252 log.Infof("Checking for switches @ %s", time.Now())
253 addresses, err := c.config.addressSource.GetAddresses()
254
255 if err != nil {
256 log.Errorf("unable to read addresses from address source : %s", err)
257 } else {
258 log.Infof("Queried %d addresses from address source", len(addresses))
259
260 c.nextList = make([]AddressRec, 0, len(addresses))
261 for _, rec := range addresses {
262 log.Debugf("Processing %s(%s, %s)", rec.Name, rec.IP, rec.MAC)
263 if err := c.processRecord(rec); err != nil {
264 log.Errorf("Error when processing IP '%s' : %s", rec.IP, err)
265 }
266 }
267 c.mutex.Lock()
268 c.publishList = c.nextList
269 c.nextList = nil
270 c.mutex.Unlock()
271 c.pushChan <- c.publishList
272 }
273
274 time.Sleep(c.config.interval)
275 }
276}
277
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700278var log = logrus.New()
279
David K. Bainbridgef694f5a2016-06-10 16:21:27 -0700280func main() {
281
282 var err error
David K. Bainbridge3569d622016-09-16 08:40:54 -0700283 context := &AppContext{}
284 err = envconfig.Process("SWITCHQ", &context.config)
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700285 if err != nil {
286 log.Fatalf("Unable to parse configuration options : %s", err)
287 }
288
David K. Bainbridge3569d622016-09-16 08:40:54 -0700289 switch context.config.LogFormat {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700290 case "json":
291 log.Formatter = &logrus.JSONFormatter{}
292 default:
293 log.Formatter = &logrus.TextFormatter{
294 FullTimestamp: true,
295 ForceColors: true,
296 }
297 }
298
David K. Bainbridge3569d622016-09-16 08:40:54 -0700299 level, err := logrus.ParseLevel(context.config.LogLevel)
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700300 if err != nil {
301 level = logrus.WarnLevel
302 }
303 log.Level = level
David K. Bainbridgef694f5a2016-06-10 16:21:27 -0700304
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700305 log.Infof(`Configuration:
David K. Bainbridge97ee8052016-06-14 00:52:07 -0700306 Vendors URL: %s
David K. Bainbridge97ee8052016-06-14 00:52:07 -0700307 Poll Interval: %s
308 Address Source: %s
309 Provision TTL: %s
310 Provision URL: %s
311 Role Selector URL: %s
312 Default Role: %s
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700313 Script: %s
David K. Bainbridge3569d622016-09-16 08:40:54 -0700314 API Listen IP: %s
315 API Listen Port: %d
316 MAAS URL: %s
317 MAAS APIKEY: %s
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700318 Log Level: %s
319 Log Format: %s`,
David K. Bainbridge3569d622016-09-16 08:40:54 -0700320 context.config.VendorsURL, context.config.PollInterval, context.config.AddressURL, context.config.ProvisionTTL,
321 context.config.ProvisionURL, context.config.RoleSelectorURL, context.config.DefaultRole, context.config.Script,
322 context.config.Listen, context.config.Port, context.config.MaasURL, context.config.MaasKey,
323 context.config.LogLevel, context.config.LogFormat)
David K. Bainbridgef694f5a2016-06-10 16:21:27 -0700324
David K. Bainbridge3569d622016-09-16 08:40:54 -0700325 context.config.vendors, err = NewVendors(context.config.VendorsURL)
326 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 -0700327
David K. Bainbridge3569d622016-09-16 08:40:54 -0700328 context.config.addressSource, err = NewAddressSource(context.config.AddressURL)
329 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 -0700330
David K. Bainbridge3569d622016-09-16 08:40:54 -0700331 context.config.interval, err = time.ParseDuration(context.config.PollInterval)
332 checkError(err, "Unable to parse specified poll interface '%s' : %s", context.config.PollInterval, err)
David K. Bainbridgef694f5a2016-06-10 16:21:27 -0700333
David K. Bainbridge3569d622016-09-16 08:40:54 -0700334 context.config.ttl, err = time.ParseDuration(context.config.ProvisionTTL)
335 checkError(err, "Unable to parse specified provision TTL value of '%s' : %s", context.config.ProvisionTTL, err)
336
337 if len(context.config.MaasURL) > 0 {
338
339 // Attempt to connect to MAAS
340 authClient, err := maas.NewAuthenticatedClient(context.config.MaasURL, context.config.MaasKey, "1.0")
341 checkError(err, "Unable to connect to MAAS at '%s' : %s", context.config.MaasURL, err)
342
343 context.maasClient = maas.NewMAAS(*authClient)
344 }
345
346 context.pushChan = make(chan []AddressRec, 1)
347
348 go context.processLoop()
349 go context.syncToMaas(context.pushChan)
350
351 router := mux.NewRouter()
352 router.HandleFunc("/switch/", context.ListSwitchesHandler).Methods("GET")
353 http.Handle("/", router)
354 log.Infof("Listening for HTTP request on '%s:%d'", context.config.Listen, context.config.Port)
355 err = http.ListenAndServe(fmt.Sprintf("%s:%d", context.config.Listen, context.config.Port), nil)
356 if err != nil {
357 checkError(err, "Error while attempting to listen to REST requests on '%s:%d' : %s",
358 context.config.Listen, context.config.Port, err)
David K. Bainbridgef694f5a2016-06-10 16:21:27 -0700359 }
360}