blob: ca10f8f4c31cebcc687f17a1c8c124b0930ead23 [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. Bainbridge97ee8052016-06-14 00:52:07 -070037func (c *Config) provision(rec AddressRec) error {
38 log.Printf("[debug] Verifing that device '%s (%s)' isn't already in a provisioning state",
39 rec.Name, rec.MAC)
40 resp, err := http.Get(c.ProvisionURL + rec.MAC)
41 log.Printf("%s%s", c.ProvisionURL, rec.MAC)
42 if err != nil {
43 log.Printf("[error] Error while retrieving provisioning state for device '%s (%s)' : %s",
44 rec.Name, rec.MAC, err)
45 return err
46 }
47 if resp.StatusCode != 404 && int(resp.StatusCode/100) != 2 {
48 log.Printf("[error] Error while retrieving provisioning state for device '%s (%s)' : %s",
49 rec.Name, rec.MAC, resp.Status)
50 return fmt.Errorf(resp.Status)
51 }
52 defer resp.Body.Close()
53 if resp.StatusCode != 404 {
54 decoder := json.NewDecoder(resp.Body)
55 var raw interface{}
56 err = decoder.Decode(&raw)
57 if err != nil {
58 log.Printf("[error] Unable to unmarshal status response from provisioning service for device '%s (%s)' : %s",
59 rec.Name, rec.MAC, err)
60 return err
61 }
62 status := raw.(map[string]interface{})
63 switch int(status["status"].(float64)) {
64 case 0, 1: // "PENDING", "RUNNING"
65 log.Printf("[info] Device '%s (%s)' is already scheduled to be provisioned",
66 rec.Name, rec.MAC)
67 return nil
68 case 2: // "COMPLETE"
69 // noop
70 case 3: // "FAILED"
71 c.storage.ClearProvisioned(rec.MAC)
72 default:
73 err = fmt.Errorf("unknown provisioning status : %d", status["status"])
74 log.Printf("[error] received unknown provisioning status for device '%s (%s)' : %s",
75 rec.Name, rec.MAC, err)
76 return err
77 }
78 }
79 log.Printf("[info] POSTing to '%s' for provisioning of '%s (%s)'", c.ProvisionURL, rec.Name, rec.MAC)
80 data := map[string]string{
81 "id": rec.MAC,
82 "name": rec.Name,
83 "ip": rec.IP,
84 "mac": rec.MAC,
85 }
86 if c.RoleSelectorURL != "" {
87 data["role_selector"] = c.RoleSelectorURL
88 }
89 if c.DefaultRole != "" {
90 data["role"] = c.DefaultRole
91 }
92 if c.Script != "" {
93 data["script"] = c.Script
David K. Bainbridgef694f5a2016-06-10 16:21:27 -070094 }
95
David K. Bainbridge97ee8052016-06-14 00:52:07 -070096 hc := http.Client{}
97 var b []byte
98 b, err = json.Marshal(data)
99 if err != nil {
100 log.Printf("[error] Unable to marshal provisioning data : %s", err)
101 return err
102 }
103 req, err := http.NewRequest("POST", c.ProvisionURL, bytes.NewReader(b))
104 if err != nil {
105 log.Printf("[error] Unable to construct POST request to provisioner : %s", err)
106 return err
107 }
108
109 req.Header.Add("Content-Type", "application/json")
110 resp, err = hc.Do(req)
111 if err != nil {
112 log.Printf("[error] Unable to POST request to provisioner : %s", err)
113 return err
114 }
115
116 defer resp.Body.Close()
117 if resp.StatusCode != http.StatusAccepted {
118 log.Printf("[error] Provisioning request not accepted by provisioner : %s", resp.Status)
119 return err
120 }
121
122 now := time.Now()
123 c.storage.MarkProvisioned(rec.MAC, &now)
124 return nil
125}
126
127func (c *Config) processRecord(rec AddressRec) error {
128 ok, err := c.vendors.Switchq(rec.MAC)
David K. Bainbridgef694f5a2016-06-10 16:21:27 -0700129 if err != nil {
130 return fmt.Errorf("unable to determine ventor of MAC '%s' (%s)", rec.MAC, err)
131 }
132
133 if !ok {
134 // Not something we care about
135 log.Printf("[debug] host with IP '%s' and MAC '%s' and named '%s' not a known switch type",
136 rec.IP, rec.MAC, rec.Name)
137 return nil
138 }
139
140 last, err := c.storage.LastProvisioned(rec.MAC)
141 if err != nil {
142 return err
143 }
David K. Bainbridge97ee8052016-06-14 00:52:07 -0700144
145 // If TTL is 0 then we will only provision a switch once.
146 if last == nil || (c.ttl > 0 && time.Since(*last) > c.ttl) {
147 c.provision(rec)
148 } else if c.ttl == 0 {
149 log.Printf("[debug] device '%s' (%s, %s) has completed its one time provisioning, with a TTL set to %s",
150 rec.Name, rec.IP, rec.MAC, c.ProvisionTTL)
151 } else {
152 log.Printf("[debug] device '%s' (%s, %s) has completed provisioning within the specified TTL of %s",
153 rec.Name, rec.IP, rec.MAC, c.ProvisionTTL)
David K. Bainbridgef694f5a2016-06-10 16:21:27 -0700154 }
155 return nil
156}
157
158func main() {
159
160 var err error
161 config := Config{}
162 envconfig.Process("SWITCHQ", &config)
163
David K. Bainbridge97ee8052016-06-14 00:52:07 -0700164 config.vendors, err = NewVendors(config.VendorsURL)
165 checkError(err, "Unable to create known vendors list from specified URL '%s' : %s", config.VendorsURL, err)
166
David K. Bainbridgef694f5a2016-06-10 16:21:27 -0700167 config.storage, err = NewStorage(config.StorageURL)
168 checkError(err, "Unable to create require storage for specified URL '%s' : %s", config.StorageURL, err)
169
170 config.addressSource, err = NewAddressSource(config.AddressURL)
171 checkError(err, "Unable to create required address source for specified URL '%s' : %s", config.AddressURL, err)
172
173 config.interval, err = time.ParseDuration(config.PollInterval)
174 checkError(err, "Unable to parse specified poll interface '%s' : %s", config.PollInterval, err)
175
176 config.ttl, err = time.ParseDuration(config.ProvisionTTL)
177 checkError(err, "Unable to parse specified provision TTL value of '%s' : %s", config.ProvisionTTL, err)
178
179 log.Printf(`Configuration:
David K. Bainbridge97ee8052016-06-14 00:52:07 -0700180 Vendors URL: %s
181 Storage URL: %s
182 Poll Interval: %s
183 Address Source: %s
184 Provision TTL: %s
185 Provision URL: %s
186 Role Selector URL: %s
187 Default Role: %s
188 Script: %s`,
189 config.VendorsURL, config.StorageURL, config.PollInterval, config.AddressURL, config.ProvisionTTL,
190 config.ProvisionURL, config.RoleSelectorURL, config.DefaultRole, config.Script)
David K. Bainbridgef694f5a2016-06-10 16:21:27 -0700191
192 // We use two methods to attempt to find the MAC (hardware) address associated with an IP. The first
193 // is to look in the table. The second is to send an ARP packet.
194 for {
195 log.Printf("[info] Checking for switches @ %s", time.Now())
196 addresses, err := config.addressSource.GetAddresses()
197
198 if err != nil {
199 log.Printf("[error] unable to read addresses from address source : %s", err)
200 } else {
201 log.Printf("[info] Queried %d addresses from address source", len(addresses))
202
203 for _, rec := range addresses {
204 log.Printf("[debug] Processing %s(%s, %s)", rec.Name, rec.IP, rec.MAC)
205 if err := config.processRecord(rec); err != nil {
206 log.Printf("[error] Error when processing IP '%s' : %s", rec.IP, err)
207 }
208 }
209 }
210
211 time.Sleep(config.interval)
212 }
213}