update switchq to calling provisioner, fixed a few bugs found while testing at on.labs
Change-Id: I2367669aa54f680b98ff0cbbc8d41a49fb7e7a79
diff --git a/switchq/storage.go b/switchq/storage.go
index d4ec36d..edf6415 100644
--- a/switchq/storage.go
+++ b/switchq/storage.go
@@ -1,12 +1,8 @@
package main
import (
- "encoding/json"
"fmt"
- "log"
"net/url"
- "os"
- "strings"
"time"
)
@@ -17,73 +13,34 @@
}
switch u.Scheme {
case "memory":
- return NewMemoryStorage(u)
+ return NewMemoryStorage()
default:
}
return nil, fmt.Errorf("Unknown storage scheme specified, '%s'", u.Scheme)
}
type Storage interface {
- Switchq(mac string) (bool, error)
LastMACCheck(mac string) (*time.Time, error)
MarkMACCheck(mac string, when *time.Time) error
LastProvisioned(mac string) (*time.Time, error)
MarkProvisioned(mac string, when *time.Time) error
-}
-
-type VendorRec struct {
- Prefix string `json:"prefix"`
- Vendor string `json:"vendor"`
- Provision bool `json:"provision"`
+ ClearProvisioned(mac string) error
}
type MemoryStorage struct {
- Vendors map[string]VendorRec
- Checks map[string]time.Time
- Times map[string]time.Time
+ Checks map[string]time.Time
+ Times map[string]time.Time
}
-func NewMemoryStorage(u *url.URL) (Storage, error) {
+func NewMemoryStorage() (Storage, error) {
- s := MemoryStorage{}
- s.Vendors = make(map[string]VendorRec)
-
- if u.Path != "" {
- file, err := os.Open(u.Path)
- if err != nil {
- return nil, err
- }
- defer file.Close()
-
- data := make([]VendorRec, 0)
- decoder := json.NewDecoder(file)
- err = decoder.Decode(&data)
- if err != nil {
- return nil, err
- }
- for _, rec := range data {
- s.Vendors[rec.Prefix] = rec
- }
- log.Printf("[debug] %v", s.Vendors)
-
- } else {
- log.Printf("[warn] no vendors have been set, no switches will be provisioned")
+ s := MemoryStorage{
+ Checks: make(map[string]time.Time),
+ Times: make(map[string]time.Time),
}
return &s, nil
}
-func (s *MemoryStorage) Switchq(mac string) (bool, error) {
- if len(mac) < 8 {
- return false, nil
- }
- rec, ok := s.Vendors[strings.ToUpper(mac[0:8])]
- if !ok || !rec.Provision {
- return false, nil
- }
-
- return true, nil
-}
-
func (s *MemoryStorage) LastMACCheck(mac string) (*time.Time, error) {
when, ok := s.Checks[mac]
if !ok {
@@ -111,3 +68,8 @@
s.Times[mac] = *when
return nil
}
+
+func (s *MemoryStorage) ClearProvisioned(mac string) error {
+ delete(s.Times, mac)
+ return nil
+}
diff --git a/switchq/switchq.go b/switchq/switchq.go
index 922f1ba..ca10f8f 100644
--- a/switchq/switchq.go
+++ b/switchq/switchq.go
@@ -1,18 +1,27 @@
package main
import (
+ "bytes"
+ "encoding/json"
"fmt"
"github.com/kelseyhightower/envconfig"
"log"
+ "net/http"
"time"
)
type Config struct {
- StorageURL string `default:"memory:///switchq/vendors.json" envconfig:"storage_url"`
- AddressURL string `default:"file:///switchq/dhcp_harvest.inc" envconfig:"address_url"`
- PollInterval string `default:"1m" envconfig:"poll_interval"`
- ProvisionTTL string `default:"1h" envconfig:"check_ttl"`
+ VendorsURL string `default:"file:///switchq/vendors.json" envconfig:"vendors_url"`
+ StorageURL string `default:"memory:" envconfig:"storage_url"`
+ AddressURL string `default:"file:///switchq/dhcp_harvest.inc" envconfig:"address_url"`
+ PollInterval string `default:"1m" envconfig:"poll_interval"`
+ ProvisionTTL string `default:"1h" envconfig:"provision_ttl"`
+ ProvisionURL string `default:"" envconfig:"provision_url"`
+ RoleSelectorURL string `default:"" envconfig:"role_selector_url"`
+ DefaultRole string `default:"fabric-switch" envconfig:"default_role"`
+ Script string `default:"do-ansible"`
+ vendors Vendors
storage Storage
addressSource AddressSource
interval time.Duration
@@ -25,13 +34,98 @@
}
}
-func (c *Config) processRecord(rec AddressRec) error {
- if c.ttl == 0 {
- // One provisioning only please
- return nil
+func (c *Config) provision(rec AddressRec) error {
+ log.Printf("[debug] Verifing that device '%s (%s)' isn't already in a provisioning state",
+ rec.Name, rec.MAC)
+ resp, err := http.Get(c.ProvisionURL + rec.MAC)
+ log.Printf("%s%s", c.ProvisionURL, rec.MAC)
+ if err != nil {
+ log.Printf("[error] Error while retrieving provisioning state for device '%s (%s)' : %s",
+ rec.Name, rec.MAC, err)
+ return err
+ }
+ if resp.StatusCode != 404 && int(resp.StatusCode/100) != 2 {
+ log.Printf("[error] Error while retrieving provisioning state for device '%s (%s)' : %s",
+ rec.Name, rec.MAC, resp.Status)
+ return fmt.Errorf(resp.Status)
+ }
+ defer resp.Body.Close()
+ if resp.StatusCode != 404 {
+ decoder := json.NewDecoder(resp.Body)
+ var raw interface{}
+ err = decoder.Decode(&raw)
+ if err != nil {
+ log.Printf("[error] Unable to unmarshal status response from provisioning service for device '%s (%s)' : %s",
+ rec.Name, rec.MAC, err)
+ return err
+ }
+ status := raw.(map[string]interface{})
+ switch int(status["status"].(float64)) {
+ case 0, 1: // "PENDING", "RUNNING"
+ log.Printf("[info] Device '%s (%s)' is already scheduled to be provisioned",
+ rec.Name, rec.MAC)
+ return nil
+ case 2: // "COMPLETE"
+ // noop
+ case 3: // "FAILED"
+ c.storage.ClearProvisioned(rec.MAC)
+ default:
+ err = fmt.Errorf("unknown provisioning status : %d", status["status"])
+ log.Printf("[error] received unknown provisioning status for device '%s (%s)' : %s",
+ rec.Name, rec.MAC, err)
+ return err
+ }
+ }
+ log.Printf("[info] POSTing to '%s' for provisioning of '%s (%s)'", c.ProvisionURL, rec.Name, rec.MAC)
+ data := map[string]string{
+ "id": rec.MAC,
+ "name": rec.Name,
+ "ip": rec.IP,
+ "mac": rec.MAC,
+ }
+ if c.RoleSelectorURL != "" {
+ data["role_selector"] = c.RoleSelectorURL
+ }
+ if c.DefaultRole != "" {
+ data["role"] = c.DefaultRole
+ }
+ if c.Script != "" {
+ data["script"] = c.Script
}
- ok, err := c.storage.Switchq(rec.MAC)
+ hc := http.Client{}
+ var b []byte
+ b, err = json.Marshal(data)
+ if err != nil {
+ log.Printf("[error] Unable to marshal provisioning data : %s", err)
+ return err
+ }
+ req, err := http.NewRequest("POST", c.ProvisionURL, bytes.NewReader(b))
+ if err != nil {
+ log.Printf("[error] Unable to construct POST request to provisioner : %s", err)
+ return err
+ }
+
+ req.Header.Add("Content-Type", "application/json")
+ resp, err = hc.Do(req)
+ if err != nil {
+ log.Printf("[error] Unable to POST request to provisioner : %s", err)
+ return err
+ }
+
+ defer resp.Body.Close()
+ if resp.StatusCode != http.StatusAccepted {
+ log.Printf("[error] Provisioning request not accepted by provisioner : %s", resp.Status)
+ return err
+ }
+
+ now := time.Now()
+ c.storage.MarkProvisioned(rec.MAC, &now)
+ return nil
+}
+
+func (c *Config) processRecord(rec AddressRec) error {
+ ok, err := c.vendors.Switchq(rec.MAC)
if err != nil {
return fmt.Errorf("unable to determine ventor of MAC '%s' (%s)", rec.MAC, err)
}
@@ -47,8 +141,16 @@
if err != nil {
return err
}
- if last == nil || time.Since(*last) > c.ttl {
- log.Printf("[debug] time to provision %s", rec.MAC)
+
+ // If TTL is 0 then we will only provision a switch once.
+ if last == nil || (c.ttl > 0 && time.Since(*last) > c.ttl) {
+ c.provision(rec)
+ } else if c.ttl == 0 {
+ log.Printf("[debug] device '%s' (%s, %s) has completed its one time provisioning, with a TTL set to %s",
+ rec.Name, rec.IP, rec.MAC, c.ProvisionTTL)
+ } else {
+ log.Printf("[debug] device '%s' (%s, %s) has completed provisioning within the specified TTL of %s",
+ rec.Name, rec.IP, rec.MAC, c.ProvisionTTL)
}
return nil
}
@@ -59,6 +161,9 @@
config := Config{}
envconfig.Process("SWITCHQ", &config)
+ config.vendors, err = NewVendors(config.VendorsURL)
+ checkError(err, "Unable to create known vendors list from specified URL '%s' : %s", config.VendorsURL, err)
+
config.storage, err = NewStorage(config.StorageURL)
checkError(err, "Unable to create require storage for specified URL '%s' : %s", config.StorageURL, err)
@@ -72,11 +177,17 @@
checkError(err, "Unable to parse specified provision TTL value of '%s' : %s", config.ProvisionTTL, err)
log.Printf(`Configuration:
- Storage URL: %s
- Poll Interval: %s
- Address Source: %s
- Provision TTL: %s`,
- config.StorageURL, config.PollInterval, config.AddressURL, config.ProvisionTTL)
+ Vendors URL: %s
+ Storage URL: %s
+ Poll Interval: %s
+ Address Source: %s
+ Provision TTL: %s
+ Provision URL: %s
+ Role Selector URL: %s
+ Default Role: %s
+ Script: %s`,
+ config.VendorsURL, config.StorageURL, config.PollInterval, config.AddressURL, config.ProvisionTTL,
+ config.ProvisionURL, config.RoleSelectorURL, config.DefaultRole, config.Script)
// We use two methods to attempt to find the MAC (hardware) address associated with an IP. The first
// is to look in the table. The second is to send an ARP packet.
diff --git a/switchq/vendors.go b/switchq/vendors.go
new file mode 100644
index 0000000..d2a0cc8
--- /dev/null
+++ b/switchq/vendors.go
@@ -0,0 +1,61 @@
+package main
+
+import (
+ "encoding/json"
+ "log"
+ "strings"
+ "net/http"
+)
+
+type Vendors interface {
+ Switchq(mac string) (bool, error)
+}
+
+type VendorRec struct {
+ Prefix string `json:"prefix"`
+ Vendor string `json:"vendor"`
+ Provision bool `json:"provision"`
+}
+
+type VendorsData struct {
+ Vendors map[string]VendorRec
+}
+
+func NewVendors(spec string) (Vendors, error) {
+ v := VendorsData{}
+ v.Vendors = make(map[string]VendorRec)
+
+ t := &http.Transport{}
+ t.RegisterProtocol("file", http.NewFileTransport(http.Dir("/")))
+ c := &http.Client{Transport: t}
+ res, err := c.Get(spec)
+ if err != nil {
+ return nil, err
+ }
+ defer res.Body.Close()
+
+ data := make([]VendorRec, 0)
+ decoder := json.NewDecoder(res.Body)
+ err = decoder.Decode(&data)
+ if err != nil {
+ return nil, err
+ }
+ for _, rec := range data {
+ v.Vendors[rec.Prefix] = rec
+ }
+ log.Printf("[debug] %v", v.Vendors)
+
+ return &v, nil
+}
+
+func (v *VendorsData) Switchq(mac string) (bool, error) {
+ if len(mac) < 8 {
+ return false, nil
+ }
+ rec, ok := v.Vendors[strings.ToUpper(mac[0:8])]
+ if !ok || !rec.Provision {
+ return false, nil
+ }
+
+ return true, nil
+}
diff --git a/switchq/vendors.json b/switchq/vendors.json
index 03d888f..db14e31 100644
--- a/switchq/vendors.json
+++ b/switchq/vendors.json
@@ -8,10 +8,5 @@
"prefix" : "70:72:CF",
"vendor" : "Edgecore Networks Corportation",
"provision" : true
- },
- {
- "prefix" : "60:5B:B4",
- "vendor" : "Fake",
- "provision" : false
}
]