blob: b586d7558e7204656a0a5b619b59b62f178733c6 [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"
7 "github.com/kelseyhightower/envconfig"
8 "log"
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. Bainbridgef694f5a2016-06-10 16:21:27 -070023
David K. Bainbridge97ee8052016-06-14 00:52:07 -070024 vendors Vendors
David K. Bainbridgef694f5a2016-06-10 16:21:27 -070025 storage Storage
26 addressSource AddressSource
27 interval time.Duration
28 ttl time.Duration
29}
30
31func checkError(err error, msg string, args ...interface{}) {
32 if err != nil {
33 log.Fatalf(msg, args...)
34 }
35}
36
David K. Bainbridgec809ef72016-06-22 21:18:07 -070037func (c *Config) getProvisionedState(rec AddressRec) (int, string, error) {
38 log.Printf("[debug] Fetching provisioned state of device '%s' (%s, %s)",
39 rec.Name, rec.IP, rec.MAC)
David K. Bainbridge97ee8052016-06-14 00:52:07 -070040 resp, err := http.Get(c.ProvisionURL + rec.MAC)
David K. Bainbridge97ee8052016-06-14 00:52:07 -070041 if err != nil {
David K. Bainbridgec809ef72016-06-22 21:18:07 -070042 log.Printf("[error] Error while retrieving provisioning state for device '%s (%s, %s)' : %s",
43 rec.Name, rec.IP, rec.MAC, err)
44 return -1, "", err
David K. Bainbridge97ee8052016-06-14 00:52:07 -070045 }
46 if resp.StatusCode != 404 && int(resp.StatusCode/100) != 2 {
David K. Bainbridgec809ef72016-06-22 21:18:07 -070047 log.Printf("[error] Error while retrieving provisioning state for device '%s (%s, %s)' : %s",
48 rec.Name, rec.IP, rec.MAC, resp.Status)
49 return -1, "", fmt.Errorf(resp.Status)
David K. Bainbridge97ee8052016-06-14 00:52:07 -070050 }
51 defer resp.Body.Close()
52 if resp.StatusCode != 404 {
53 decoder := json.NewDecoder(resp.Body)
54 var raw interface{}
55 err = decoder.Decode(&raw)
56 if err != nil {
David K. Bainbridgec809ef72016-06-22 21:18:07 -070057 log.Printf("[error] Unmarshal provisioning service response for device '%s (%s, %s)' : %s",
58 rec.Name, rec.IP, rec.MAC, err)
59 return -1, "", err
David K. Bainbridge97ee8052016-06-14 00:52:07 -070060 }
61 status := raw.(map[string]interface{})
62 switch int(status["status"].(float64)) {
63 case 0, 1: // "PENDING", "RUNNING"
David K. Bainbridgec809ef72016-06-22 21:18:07 -070064 return int(status["status"].(float64)), "", nil
David K. Bainbridge97ee8052016-06-14 00:52:07 -070065 case 2: // "COMPLETE"
David K. Bainbridgec809ef72016-06-22 21:18:07 -070066 return 2, "", nil
David K. Bainbridge97ee8052016-06-14 00:52:07 -070067 case 3: // "FAILED"
David K. Bainbridgec809ef72016-06-22 21:18:07 -070068 return 3, status["message"].(string), nil
David K. Bainbridge97ee8052016-06-14 00:52:07 -070069 default:
70 err = fmt.Errorf("unknown provisioning status : %d", status["status"])
71 log.Printf("[error] received unknown provisioning status for device '%s (%s)' : %s",
72 rec.Name, rec.MAC, err)
David K. Bainbridgec809ef72016-06-22 21:18:07 -070073 return -1, "", err
David K. Bainbridge97ee8052016-06-14 00:52:07 -070074 }
75 }
David K. Bainbridgec809ef72016-06-22 21:18:07 -070076
77 // If we end up here that means that no record was found in the provisioning, so return
78 // a status of -1, w/o an error
79 return -1, "", nil
80}
81
82func (c *Config) provision(rec AddressRec) error {
David K. Bainbridge97ee8052016-06-14 00:52:07 -070083 log.Printf("[info] POSTing to '%s' for provisioning of '%s (%s)'", c.ProvisionURL, rec.Name, rec.MAC)
84 data := map[string]string{
85 "id": rec.MAC,
86 "name": rec.Name,
87 "ip": rec.IP,
88 "mac": rec.MAC,
89 }
90 if c.RoleSelectorURL != "" {
91 data["role_selector"] = c.RoleSelectorURL
92 }
93 if c.DefaultRole != "" {
94 data["role"] = c.DefaultRole
95 }
96 if c.Script != "" {
97 data["script"] = c.Script
David K. Bainbridgef694f5a2016-06-10 16:21:27 -070098 }
99
David K. Bainbridge97ee8052016-06-14 00:52:07 -0700100 hc := http.Client{}
101 var b []byte
David K. Bainbridgec809ef72016-06-22 21:18:07 -0700102 b, err := json.Marshal(data)
David K. Bainbridge97ee8052016-06-14 00:52:07 -0700103 if err != nil {
104 log.Printf("[error] Unable to marshal provisioning data : %s", err)
105 return err
106 }
107 req, err := http.NewRequest("POST", c.ProvisionURL, bytes.NewReader(b))
108 if err != nil {
109 log.Printf("[error] Unable to construct POST request to provisioner : %s", err)
110 return err
111 }
112
113 req.Header.Add("Content-Type", "application/json")
David K. Bainbridgec809ef72016-06-22 21:18:07 -0700114 resp, err := hc.Do(req)
David K. Bainbridge97ee8052016-06-14 00:52:07 -0700115 if err != nil {
116 log.Printf("[error] Unable to POST request to provisioner : %s", err)
117 return err
118 }
119
120 defer resp.Body.Close()
121 if resp.StatusCode != http.StatusAccepted {
122 log.Printf("[error] Provisioning request not accepted by provisioner : %s", resp.Status)
123 return err
124 }
125
David K. Bainbridge97ee8052016-06-14 00:52:07 -0700126 return nil
127}
128
129func (c *Config) processRecord(rec AddressRec) error {
130 ok, err := c.vendors.Switchq(rec.MAC)
David K. Bainbridgef694f5a2016-06-10 16:21:27 -0700131 if err != nil {
132 return fmt.Errorf("unable to determine ventor of MAC '%s' (%s)", rec.MAC, err)
133 }
134
135 if !ok {
136 // Not something we care about
137 log.Printf("[debug] host with IP '%s' and MAC '%s' and named '%s' not a known switch type",
138 rec.IP, rec.MAC, rec.Name)
139 return nil
140 }
141
142 last, err := c.storage.LastProvisioned(rec.MAC)
143 if err != nil {
144 return err
145 }
David K. Bainbridge97ee8052016-06-14 00:52:07 -0700146
David K. Bainbridgec809ef72016-06-22 21:18:07 -0700147 if last == nil {
148 log.Printf("[debug] no TTL for device '%s' (%s, %s)",
149 rec.Name, rec.IP, rec.MAC)
150 } else {
151 log.Printf("[debug] TTL for device '%s' (%s, %s) is %v",
152 rec.Name, rec.IP, rec.MAC, *last)
153 }
154
155 // Verify if the provision status of the node is complete, if in an error state then TTL means
156 // nothing
157 state, message, err := c.getProvisionedState(rec)
158 switch state {
159 case 0, 1: // Pending or Running
160 log.Printf("[debug] device '%s' (%s, %s) is being provisioned",
161 rec.Name, rec.IP, rec.MAC)
162 return nil
163 case 2: // Complete
164 log.Printf("[debug] device '%s' (%s, %s) has completed provisioning",
165 rec.Name, rec.IP, rec.MAC)
166 // If no last record then set the TTL
167 if last == nil {
168 now := time.Now()
169 last = &now
170 c.storage.MarkProvisioned(rec.MAC, last)
171 log.Printf("[debug] Storing TTL for device '%s' (%s, %s) as %v",
172 rec.Name, rec.IP, rec.MAC, now)
173 return nil
174 }
175 case 3: // Failed
176 log.Printf("[debug] device '%s' (%s, %s) failed last provisioning with message '%s', reattempt",
177 rec.Name, rec.IP, rec.MAC, message)
178 c.storage.ClearProvisioned(rec.MAC)
179 last = nil
180 default: // No record
181 }
182
David K. Bainbridge97ee8052016-06-14 00:52:07 -0700183 // If TTL is 0 then we will only provision a switch once.
184 if last == nil || (c.ttl > 0 && time.Since(*last) > c.ttl) {
David K. Bainbridgec809ef72016-06-22 21:18:07 -0700185 if last != nil {
186 c.storage.ClearProvisioned(rec.MAC)
187 log.Printf("[debug] device '%s' (%s, %s) TTL expired, reprovisioning",
188 rec.Name, rec.IP, rec.MAC)
189 }
David K. Bainbridge97ee8052016-06-14 00:52:07 -0700190 c.provision(rec)
191 } else if c.ttl == 0 {
192 log.Printf("[debug] device '%s' (%s, %s) has completed its one time provisioning, with a TTL set to %s",
193 rec.Name, rec.IP, rec.MAC, c.ProvisionTTL)
194 } else {
195 log.Printf("[debug] device '%s' (%s, %s) has completed provisioning within the specified TTL of %s",
196 rec.Name, rec.IP, rec.MAC, c.ProvisionTTL)
David K. Bainbridgef694f5a2016-06-10 16:21:27 -0700197 }
198 return nil
199}
200
201func main() {
202
203 var err error
204 config := Config{}
205 envconfig.Process("SWITCHQ", &config)
206
David K. Bainbridge97ee8052016-06-14 00:52:07 -0700207 config.vendors, err = NewVendors(config.VendorsURL)
208 checkError(err, "Unable to create known vendors list from specified URL '%s' : %s", config.VendorsURL, err)
209
David K. Bainbridgef694f5a2016-06-10 16:21:27 -0700210 config.storage, err = NewStorage(config.StorageURL)
211 checkError(err, "Unable to create require storage for specified URL '%s' : %s", config.StorageURL, err)
212
213 config.addressSource, err = NewAddressSource(config.AddressURL)
214 checkError(err, "Unable to create required address source for specified URL '%s' : %s", config.AddressURL, err)
215
216 config.interval, err = time.ParseDuration(config.PollInterval)
217 checkError(err, "Unable to parse specified poll interface '%s' : %s", config.PollInterval, err)
218
219 config.ttl, err = time.ParseDuration(config.ProvisionTTL)
220 checkError(err, "Unable to parse specified provision TTL value of '%s' : %s", config.ProvisionTTL, err)
221
222 log.Printf(`Configuration:
David K. Bainbridge97ee8052016-06-14 00:52:07 -0700223 Vendors URL: %s
224 Storage URL: %s
225 Poll Interval: %s
226 Address Source: %s
227 Provision TTL: %s
228 Provision URL: %s
229 Role Selector URL: %s
230 Default Role: %s
231 Script: %s`,
232 config.VendorsURL, config.StorageURL, config.PollInterval, config.AddressURL, config.ProvisionTTL,
233 config.ProvisionURL, config.RoleSelectorURL, config.DefaultRole, config.Script)
David K. Bainbridgef694f5a2016-06-10 16:21:27 -0700234
235 // We use two methods to attempt to find the MAC (hardware) address associated with an IP. The first
236 // is to look in the table. The second is to send an ARP packet.
237 for {
238 log.Printf("[info] Checking for switches @ %s", time.Now())
239 addresses, err := config.addressSource.GetAddresses()
240
241 if err != nil {
242 log.Printf("[error] unable to read addresses from address source : %s", err)
243 } else {
244 log.Printf("[info] Queried %d addresses from address source", len(addresses))
245
246 for _, rec := range addresses {
247 log.Printf("[debug] Processing %s(%s, %s)", rec.Name, rec.IP, rec.MAC)
248 if err := config.processRecord(rec); err != nil {
249 log.Printf("[error] Error when processing IP '%s' : %s", rec.IP, err)
250 }
251 }
252 }
253
254 time.Sleep(config.interval)
255 }
256}