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
     }
 ]