blob: 2c084ed3b5a0482bce0e336d13bf2b58d910d18f [file] [log] [blame]
David K. Bainbridgef694f5a2016-06-10 16:21:27 -07001package main
2
3import (
David K. Bainbridge97ee8052016-06-14 00:52:07 -07004 "bytes"
5 "encoding/json"
David K. Bainbridgef694f5a2016-06-10 16:21:27 -07006 "fmt"
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -07007 "github.com/Sirupsen/logrus"
David K. Bainbridgef694f5a2016-06-10 16:21:27 -07008 "github.com/kelseyhightower/envconfig"
David K. Bainbridge97ee8052016-06-14 00:52:07 -07009 "net/http"
David K. Bainbridgef694f5a2016-06-10 16:21:27 -070010 "time"
11)
12
13type Config struct {
David K. Bainbridge97ee8052016-06-14 00:52:07 -070014 VendorsURL string `default:"file:///switchq/vendors.json" envconfig:"vendors_url"`
15 StorageURL string `default:"memory:" envconfig:"storage_url"`
16 AddressURL string `default:"file:///switchq/dhcp_harvest.inc" envconfig:"address_url"`
17 PollInterval string `default:"1m" envconfig:"poll_interval"`
18 ProvisionTTL string `default:"1h" envconfig:"provision_ttl"`
19 ProvisionURL string `default:"" envconfig:"provision_url"`
20 RoleSelectorURL string `default:"" envconfig:"role_selector_url"`
21 DefaultRole string `default:"fabric-switch" envconfig:"default_role"`
22 Script string `default:"do-ansible"`
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -070023 LogLevel string `default:"warning" envconfig:"LOG_LEVEL"`
24 LogFormat string `default:"text" envconfig:"LOG_FORMAT"`
David K. Bainbridgef694f5a2016-06-10 16:21:27 -070025
David K. Bainbridge97ee8052016-06-14 00:52:07 -070026 vendors Vendors
David K. Bainbridgef694f5a2016-06-10 16:21:27 -070027 storage Storage
28 addressSource AddressSource
29 interval time.Duration
30 ttl time.Duration
31}
32
33func checkError(err error, msg string, args ...interface{}) {
34 if err != nil {
35 log.Fatalf(msg, args...)
36 }
37}
38
David K. Bainbridgec809ef72016-06-22 21:18:07 -070039func (c *Config) getProvisionedState(rec AddressRec) (int, string, error) {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -070040 log.Debugf("Fetching provisioned state of device '%s' (%s, %s)",
David K. Bainbridgec809ef72016-06-22 21:18:07 -070041 rec.Name, rec.IP, rec.MAC)
David K. Bainbridge97ee8052016-06-14 00:52:07 -070042 resp, err := http.Get(c.ProvisionURL + rec.MAC)
David K. Bainbridge97ee8052016-06-14 00:52:07 -070043 if err != nil {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -070044 log.Errorf("Error while retrieving provisioning state for device '%s (%s, %s)' : %s",
David K. Bainbridgec809ef72016-06-22 21:18:07 -070045 rec.Name, rec.IP, rec.MAC, err)
46 return -1, "", err
David K. Bainbridge97ee8052016-06-14 00:52:07 -070047 }
48 if resp.StatusCode != 404 && int(resp.StatusCode/100) != 2 {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -070049 log.Errorf("Error while retrieving provisioning state for device '%s (%s, %s)' : %s",
David K. Bainbridgec809ef72016-06-22 21:18:07 -070050 rec.Name, rec.IP, rec.MAC, resp.Status)
51 return -1, "", fmt.Errorf(resp.Status)
David K. Bainbridge97ee8052016-06-14 00:52:07 -070052 }
53 defer resp.Body.Close()
54 if resp.StatusCode != 404 {
55 decoder := json.NewDecoder(resp.Body)
56 var raw interface{}
57 err = decoder.Decode(&raw)
58 if err != nil {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -070059 log.Errorf("Unmarshal provisioning service response for device '%s (%s, %s)' : %s",
David K. Bainbridgec809ef72016-06-22 21:18:07 -070060 rec.Name, rec.IP, rec.MAC, err)
61 return -1, "", err
David K. Bainbridge97ee8052016-06-14 00:52:07 -070062 }
63 status := raw.(map[string]interface{})
64 switch int(status["status"].(float64)) {
65 case 0, 1: // "PENDING", "RUNNING"
David K. Bainbridgec809ef72016-06-22 21:18:07 -070066 return int(status["status"].(float64)), "", nil
David K. Bainbridge97ee8052016-06-14 00:52:07 -070067 case 2: // "COMPLETE"
David K. Bainbridgec809ef72016-06-22 21:18:07 -070068 return 2, "", nil
David K. Bainbridge97ee8052016-06-14 00:52:07 -070069 case 3: // "FAILED"
David K. Bainbridgec809ef72016-06-22 21:18:07 -070070 return 3, status["message"].(string), nil
David K. Bainbridge97ee8052016-06-14 00:52:07 -070071 default:
72 err = fmt.Errorf("unknown provisioning status : %d", status["status"])
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -070073 log.Errorf("received unknown provisioning status for device '%s (%s)' : %s",
David K. Bainbridge97ee8052016-06-14 00:52:07 -070074 rec.Name, rec.MAC, err)
David K. Bainbridgec809ef72016-06-22 21:18:07 -070075 return -1, "", err
David K. Bainbridge97ee8052016-06-14 00:52:07 -070076 }
77 }
David K. Bainbridgec809ef72016-06-22 21:18:07 -070078
79 // If we end up here that means that no record was found in the provisioning, so return
80 // a status of -1, w/o an error
81 return -1, "", nil
82}
83
84func (c *Config) provision(rec AddressRec) error {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -070085 log.Infof("POSTing to '%s' for provisioning of '%s (%s)'", c.ProvisionURL, rec.Name, rec.MAC)
David K. Bainbridge97ee8052016-06-14 00:52:07 -070086 data := map[string]string{
87 "id": rec.MAC,
88 "name": rec.Name,
89 "ip": rec.IP,
90 "mac": rec.MAC,
91 }
92 if c.RoleSelectorURL != "" {
93 data["role_selector"] = c.RoleSelectorURL
94 }
95 if c.DefaultRole != "" {
96 data["role"] = c.DefaultRole
97 }
98 if c.Script != "" {
99 data["script"] = c.Script
David K. Bainbridgef694f5a2016-06-10 16:21:27 -0700100 }
101
David K. Bainbridge97ee8052016-06-14 00:52:07 -0700102 hc := http.Client{}
103 var b []byte
David K. Bainbridgec809ef72016-06-22 21:18:07 -0700104 b, err := json.Marshal(data)
David K. Bainbridge97ee8052016-06-14 00:52:07 -0700105 if err != nil {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700106 log.Errorf("Unable to marshal provisioning data : %s", err)
David K. Bainbridge97ee8052016-06-14 00:52:07 -0700107 return err
108 }
109 req, err := http.NewRequest("POST", c.ProvisionURL, bytes.NewReader(b))
110 if err != nil {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700111 log.Errorf("Unable to construct POST request to provisioner : %s", err)
David K. Bainbridge97ee8052016-06-14 00:52:07 -0700112 return err
113 }
114
115 req.Header.Add("Content-Type", "application/json")
David K. Bainbridgec809ef72016-06-22 21:18:07 -0700116 resp, err := hc.Do(req)
David K. Bainbridge97ee8052016-06-14 00:52:07 -0700117 if err != nil {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700118 log.Errorf("Unable to POST request to provisioner : %s", err)
David K. Bainbridge97ee8052016-06-14 00:52:07 -0700119 return err
120 }
121
122 defer resp.Body.Close()
123 if resp.StatusCode != http.StatusAccepted {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700124 log.Errorf("Provisioning request not accepted by provisioner : %s", resp.Status)
David K. Bainbridge97ee8052016-06-14 00:52:07 -0700125 return err
126 }
127
David K. Bainbridge97ee8052016-06-14 00:52:07 -0700128 return nil
129}
130
131func (c *Config) processRecord(rec AddressRec) error {
132 ok, err := c.vendors.Switchq(rec.MAC)
David K. Bainbridgef694f5a2016-06-10 16:21:27 -0700133 if err != nil {
134 return fmt.Errorf("unable to determine ventor of MAC '%s' (%s)", rec.MAC, err)
135 }
136
137 if !ok {
138 // Not something we care about
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700139 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 -0700140 rec.IP, rec.MAC, rec.Name)
141 return nil
142 }
143
144 last, err := c.storage.LastProvisioned(rec.MAC)
145 if err != nil {
146 return err
147 }
David K. Bainbridge97ee8052016-06-14 00:52:07 -0700148
David K. Bainbridgec809ef72016-06-22 21:18:07 -0700149 if last == nil {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700150 log.Debugf("no TTL for device '%s' (%s, %s)",
David K. Bainbridgec809ef72016-06-22 21:18:07 -0700151 rec.Name, rec.IP, rec.MAC)
152 } else {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700153 log.Debugf("TTL for device '%s' (%s, %s) is %v",
David K. Bainbridgec809ef72016-06-22 21:18:07 -0700154 rec.Name, rec.IP, rec.MAC, *last)
155 }
156
157 // Verify if the provision status of the node is complete, if in an error state then TTL means
158 // nothing
159 state, message, err := c.getProvisionedState(rec)
160 switch state {
161 case 0, 1: // Pending or Running
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700162 log.Debugf("device '%s' (%s, %s) is being provisioned",
David K. Bainbridgec809ef72016-06-22 21:18:07 -0700163 rec.Name, rec.IP, rec.MAC)
164 return nil
165 case 2: // Complete
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700166 log.Debugf("device '%s' (%s, %s) has completed provisioning",
David K. Bainbridgec809ef72016-06-22 21:18:07 -0700167 rec.Name, rec.IP, rec.MAC)
168 // If no last record then set the TTL
169 if last == nil {
170 now := time.Now()
171 last = &now
172 c.storage.MarkProvisioned(rec.MAC, last)
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700173 log.Debugf("Storing TTL for device '%s' (%s, %s) as %v",
David K. Bainbridgec809ef72016-06-22 21:18:07 -0700174 rec.Name, rec.IP, rec.MAC, now)
175 return nil
176 }
177 case 3: // Failed
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700178 log.Debugf("device '%s' (%s, %s) failed last provisioning with message '%s', reattempt",
David K. Bainbridgec809ef72016-06-22 21:18:07 -0700179 rec.Name, rec.IP, rec.MAC, message)
180 c.storage.ClearProvisioned(rec.MAC)
181 last = nil
182 default: // No record
183 }
184
David K. Bainbridge97ee8052016-06-14 00:52:07 -0700185 // If TTL is 0 then we will only provision a switch once.
186 if last == nil || (c.ttl > 0 && time.Since(*last) > c.ttl) {
David K. Bainbridgec809ef72016-06-22 21:18:07 -0700187 if last != nil {
188 c.storage.ClearProvisioned(rec.MAC)
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700189 log.Debugf("device '%s' (%s, %s) TTL expired, reprovisioning",
David K. Bainbridgec809ef72016-06-22 21:18:07 -0700190 rec.Name, rec.IP, rec.MAC)
191 }
David K. Bainbridge97ee8052016-06-14 00:52:07 -0700192 c.provision(rec)
193 } else if c.ttl == 0 {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700194 log.Debugf("device '%s' (%s, %s) has completed its one time provisioning, with a TTL set to %s",
David K. Bainbridge97ee8052016-06-14 00:52:07 -0700195 rec.Name, rec.IP, rec.MAC, c.ProvisionTTL)
196 } else {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700197 log.Debugf("device '%s' (%s, %s) has completed provisioning within the specified TTL of %s",
David K. Bainbridge97ee8052016-06-14 00:52:07 -0700198 rec.Name, rec.IP, rec.MAC, c.ProvisionTTL)
David K. Bainbridgef694f5a2016-06-10 16:21:27 -0700199 }
200 return nil
201}
202
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700203var log = logrus.New()
204
David K. Bainbridgef694f5a2016-06-10 16:21:27 -0700205func main() {
206
207 var err error
208 config := Config{}
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700209 err = envconfig.Process("SWITCHQ", &config)
210 if err != nil {
211 log.Fatalf("Unable to parse configuration options : %s", err)
212 }
213
214 switch config.LogFormat {
215 case "json":
216 log.Formatter = &logrus.JSONFormatter{}
217 default:
218 log.Formatter = &logrus.TextFormatter{
219 FullTimestamp: true,
220 ForceColors: true,
221 }
222 }
223
224 level, err := logrus.ParseLevel(config.LogLevel)
225 if err != nil {
226 level = logrus.WarnLevel
227 }
228 log.Level = level
David K. Bainbridgef694f5a2016-06-10 16:21:27 -0700229
David K. Bainbridge97ee8052016-06-14 00:52:07 -0700230 config.vendors, err = NewVendors(config.VendorsURL)
231 checkError(err, "Unable to create known vendors list from specified URL '%s' : %s", config.VendorsURL, err)
232
David K. Bainbridgef694f5a2016-06-10 16:21:27 -0700233 config.storage, err = NewStorage(config.StorageURL)
234 checkError(err, "Unable to create require storage for specified URL '%s' : %s", config.StorageURL, err)
235
236 config.addressSource, err = NewAddressSource(config.AddressURL)
237 checkError(err, "Unable to create required address source for specified URL '%s' : %s", config.AddressURL, err)
238
239 config.interval, err = time.ParseDuration(config.PollInterval)
240 checkError(err, "Unable to parse specified poll interface '%s' : %s", config.PollInterval, err)
241
242 config.ttl, err = time.ParseDuration(config.ProvisionTTL)
243 checkError(err, "Unable to parse specified provision TTL value of '%s' : %s", config.ProvisionTTL, err)
244
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700245 log.Infof(`Configuration:
David K. Bainbridge97ee8052016-06-14 00:52:07 -0700246 Vendors URL: %s
247 Storage URL: %s
248 Poll Interval: %s
249 Address Source: %s
250 Provision TTL: %s
251 Provision URL: %s
252 Role Selector URL: %s
253 Default Role: %s
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700254 Script: %s
255 Log Level: %s
256 Log Format: %s`,
David K. Bainbridge97ee8052016-06-14 00:52:07 -0700257 config.VendorsURL, config.StorageURL, config.PollInterval, config.AddressURL, config.ProvisionTTL,
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700258 config.ProvisionURL, config.RoleSelectorURL, config.DefaultRole, config.Script,
259 config.LogLevel, config.LogFormat)
David K. Bainbridgef694f5a2016-06-10 16:21:27 -0700260
261 // We use two methods to attempt to find the MAC (hardware) address associated with an IP. The first
262 // is to look in the table. The second is to send an ARP packet.
263 for {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700264 log.Infof("Checking for switches @ %s", time.Now())
David K. Bainbridgef694f5a2016-06-10 16:21:27 -0700265 addresses, err := config.addressSource.GetAddresses()
266
267 if err != nil {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700268 log.Errorf("unable to read addresses from address source : %s", err)
David K. Bainbridgef694f5a2016-06-10 16:21:27 -0700269 } else {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700270 log.Infof("Queried %d addresses from address source", len(addresses))
David K. Bainbridgef694f5a2016-06-10 16:21:27 -0700271
272 for _, rec := range addresses {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700273 log.Debugf("Processing %s(%s, %s)", rec.Name, rec.IP, rec.MAC)
David K. Bainbridgef694f5a2016-06-10 16:21:27 -0700274 if err := config.processRecord(rec); err != nil {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700275 log.Errorf("Error when processing IP '%s' : %s", rec.IP, err)
David K. Bainbridgef694f5a2016-06-10 16:21:27 -0700276 }
277 }
278 }
279
280 time.Sleep(config.interval)
281 }
282}