check point of code to identify and provision switches

Change-Id: I692d83127cbb718f03d359911689d0f688fa4a3d
diff --git a/switchq/Dockerfile b/switchq/Dockerfile
new file mode 100644
index 0000000..a0966e8
--- /dev/null
+++ b/switchq/Dockerfile
@@ -0,0 +1,50 @@
+FROM ubuntu:14.04
+
+# Base image information borrowed by official golang wheezy Dockerfile
+RUN apt-get update && apt-get install -y --no-install-recommends \
+		g++ \
+		gcc \
+		libc6-dev \
+		make \
+                curl \
+	&& rm -rf /var/lib/apt/lists/*
+
+ENV GOLANG_VERSION 1.6.2
+ENV GOLANG_DOWNLOAD_URL https://golang.org/dl/go$GOLANG_VERSION.linux-amd64.tar.gz
+ENV GOLANG_DOWNLOAD_SHA256 e40c36ae71756198478624ed1bb4ce17597b3c19d243f3f0899bb5740d56212a
+
+RUN curl -kfsSL "$GOLANG_DOWNLOAD_URL" -o golang.tar.gz \
+	&& echo "$GOLANG_DOWNLOAD_SHA256  golang.tar.gz" | sha256sum -c - \
+	&& tar -C /usr/local -xzf golang.tar.gz \
+	&& rm golang.tar.gz
+
+ENV GOPATH /go
+ENV PATH $GOPATH/bin:/usr/local/go/bin:$PATH
+
+RUN mkdir -p "$GOPATH/src" "$GOPATH/bin" && chmod -R 777 "$GOPATH"
+
+# CORD Provisioner Dockerfile
+WORKDIR $GOPATH
+
+RUN apt-get update && \
+	apt-get install -y  software-properties-common && \
+	apt-add-repository ppa:ansible/ansible && \
+	apt-get update -y  -m && \
+	apt-get install -y git ansible
+
+RUN mkdir -p /root/.ssh
+COPY ssh-config /root/.ssh/config
+
+RUN mkdir -p /switchq
+COPY vendors.json /switchq/vendors.json
+
+RUN go get github.com/tools/godep
+ADD . $GOPATH/src/gerrit.opencord.com/maas/switchq
+
+WORKDIR $GOPATH/src/gerrit.opencord.com/maas/switchq
+RUN $GOPATH/bin/godep restore
+
+WORKDIR $GOPATH
+RUN go install gerrit.opencord.com/maas/switchq
+
+ENTRYPOINT ["/go/bin/switchq"]
diff --git a/switchq/Godeps/Godeps.json b/switchq/Godeps/Godeps.json
new file mode 100644
index 0000000..00c42a5
--- /dev/null
+++ b/switchq/Godeps/Godeps.json
@@ -0,0 +1,12 @@
+{
+	"ImportPath": "gerrit.opencord.org/maas/swtichq",
+	"GoVersion": "go1.6",
+	"GodepVersion": "v72",
+	"Deps": [
+		{
+			"ImportPath": "github.com/kelseyhightower/envconfig",
+			"Comment": "1.1.0-17-g91921eb",
+			"Rev": "91921eb4cf999321cdbeebdba5a03555800d493b"
+		}
+	]
+}
diff --git a/switchq/address.go b/switchq/address.go
new file mode 100644
index 0000000..26c662c
--- /dev/null
+++ b/switchq/address.go
@@ -0,0 +1,84 @@
+package main
+
+import (
+	"bufio"
+	"fmt"
+	"net/url"
+	"os"
+	"strings"
+)
+
+func NewAddressSource(spec string) (AddressSource, error) {
+	u, err := url.Parse(spec)
+	if err != nil {
+		return nil, err
+	}
+
+	switch u.Scheme {
+	case "file":
+		return NewFileAddressSource(u)
+	default:
+	}
+	return nil, fmt.Errorf("Unknown address source scheme specified '%s'", spec)
+}
+
+type AddressRec struct {
+	Name string
+	IP   string
+	MAC  string
+}
+
+type AddressSource interface {
+	GetAddresses() ([]AddressRec, error)
+}
+
+type FileAddressSource struct {
+	Path string
+}
+
+func NewFileAddressSource(connect *url.URL) (AddressSource, error) {
+	// Validate file exists before returning a source
+	if _, err := os.Stat(connect.Path); os.IsNotExist(err) {
+		return nil, err
+	}
+	source := FileAddressSource{}
+	source.Path = connect.Path
+	return &source, nil
+}
+
+func (s *FileAddressSource) GetAddresses() ([]AddressRec, error) {
+	// Read the file
+	file, err := os.Open(s.Path)
+	defer file.Close()
+	if err != nil {
+		return nil, err
+	}
+
+	capacity := 20
+	result := make([]AddressRec, capacity)
+	idx := 0
+
+	scanner := bufio.NewScanner(file)
+	for scanner.Scan() {
+		parts := strings.Fields(scanner.Text())
+
+		// Only process lines with the correct number of parts
+		if len(parts) == 6 {
+			result[idx].Name = parts[0]
+			result[idx].IP = parts[3]
+			result[idx].MAC = parts[5]
+			idx += 1
+			if idx >= capacity {
+				capacity += 20
+				tmp, result := result, make([]AddressRec, capacity)
+				copy(result, tmp)
+			}
+		}
+	}
+
+	if err := scanner.Err(); err != nil {
+		return nil, err
+	}
+
+	return result[:idx], nil
+}
diff --git a/switchq/data.txt b/switchq/data.txt
new file mode 100644
index 0000000..a42563f
--- /dev/null
+++ b/switchq/data.txt
@@ -0,0 +1,9 @@
+xos             IN A 172.18.0.100 ; 52:54:00:47:42:21
+leaf-1          IN A 10.6.0.7 ; cc:37:ab:7c:bd:e6
+rabbitmq-server IN A 172.18.0.91 ; 52:54:00:73:da:98
+leaf-2          IN A 10.6.0.10 ; cc:37:ab:7c:ba:58
+sample		IN A 172.42.42.4 ; 52:54:00:1f:10:1f
+3938-128PC      IN A 10.6.0.14 ; 00:25:90:5c:f7:48
+onos-fabric     IN A 172.18.0.102 ; 52:54:00:1f:10:1f
+spine-1         IN A 10.6.0.6 ; cc:37:ab:7c:b7:4c
+spine-2         IN A 10.6.0.8 ; cc:37:ab:7c:bf:6c
diff --git a/switchq/ssh-config b/switchq/ssh-config
new file mode 100644
index 0000000..990a43d
--- /dev/null
+++ b/switchq/ssh-config
@@ -0,0 +1,3 @@
+Host *
+   StrictHostKeyChecking no
+   UserKnownHostsFile=/dev/null
diff --git a/switchq/storage.go b/switchq/storage.go
new file mode 100644
index 0000000..d4ec36d
--- /dev/null
+++ b/switchq/storage.go
@@ -0,0 +1,113 @@
+package main
+
+import (
+	"encoding/json"
+	"fmt"
+	"log"
+	"net/url"
+	"os"
+	"strings"
+	"time"
+)
+
+func NewStorage(spec string) (Storage, error) {
+	u, err := url.Parse(spec)
+	if err != nil {
+		return nil, err
+	}
+	switch u.Scheme {
+	case "memory":
+		return NewMemoryStorage(u)
+	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"`
+}
+
+type MemoryStorage struct {
+	Vendors map[string]VendorRec
+	Checks  map[string]time.Time
+	Times   map[string]time.Time
+}
+
+func NewMemoryStorage(u *url.URL) (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")
+	}
+	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 {
+		return nil, nil
+	}
+	result := when
+	return &result, nil
+}
+
+func (s *MemoryStorage) MarkMACCheck(mac string, when *time.Time) error {
+	s.Checks[mac] = *when
+	return nil
+}
+
+func (s *MemoryStorage) LastProvisioned(mac string) (*time.Time, error) {
+	when, ok := s.Times[mac]
+	if !ok {
+		return nil, nil
+	}
+	result := when
+	return &result, nil
+}
+
+func (s *MemoryStorage) MarkProvisioned(mac string, when *time.Time) error {
+	s.Times[mac] = *when
+	return nil
+}
diff --git a/switchq/switchq.go b/switchq/switchq.go
new file mode 100644
index 0000000..922f1ba
--- /dev/null
+++ b/switchq/switchq.go
@@ -0,0 +1,102 @@
+package main
+
+import (
+	"fmt"
+	"github.com/kelseyhightower/envconfig"
+	"log"
+	"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"`
+
+	storage       Storage
+	addressSource AddressSource
+	interval      time.Duration
+	ttl           time.Duration
+}
+
+func checkError(err error, msg string, args ...interface{}) {
+	if err != nil {
+		log.Fatalf(msg, args...)
+	}
+}
+
+func (c *Config) processRecord(rec AddressRec) error {
+	if c.ttl == 0 {
+		// One provisioning only please
+		return nil
+	}
+
+	ok, err := c.storage.Switchq(rec.MAC)
+	if err != nil {
+		return fmt.Errorf("unable to determine ventor of MAC '%s' (%s)", rec.MAC, err)
+	}
+
+	if !ok {
+		// Not something we care about
+		log.Printf("[debug] host with IP '%s' and MAC '%s' and named '%s' not a known switch type",
+			rec.IP, rec.MAC, rec.Name)
+		return nil
+	}
+
+	last, err := c.storage.LastProvisioned(rec.MAC)
+	if err != nil {
+		return err
+	}
+	if last == nil || time.Since(*last) > c.ttl {
+		log.Printf("[debug] time to provision %s", rec.MAC)
+	}
+	return nil
+}
+
+func main() {
+
+	var err error
+	config := Config{}
+	envconfig.Process("SWITCHQ", &config)
+
+	config.storage, err = NewStorage(config.StorageURL)
+	checkError(err, "Unable to create require storage for specified URL '%s' : %s", config.StorageURL, err)
+
+	config.addressSource, err = NewAddressSource(config.AddressURL)
+	checkError(err, "Unable to create required address source for specified URL '%s' : %s", config.AddressURL, err)
+
+	config.interval, err = time.ParseDuration(config.PollInterval)
+	checkError(err, "Unable to parse specified poll interface '%s' : %s", config.PollInterval, err)
+
+	config.ttl, err = time.ParseDuration(config.ProvisionTTL)
+	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)
+
+	// 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.
+	for {
+		log.Printf("[info] Checking for switches @ %s", time.Now())
+		addresses, err := config.addressSource.GetAddresses()
+
+		if err != nil {
+			log.Printf("[error] unable to read addresses from address source : %s", err)
+		} else {
+			log.Printf("[info] Queried %d addresses from address source", len(addresses))
+
+			for _, rec := range addresses {
+				log.Printf("[debug] Processing %s(%s, %s)", rec.Name, rec.IP, rec.MAC)
+				if err := config.processRecord(rec); err != nil {
+					log.Printf("[error] Error when processing IP '%s' : %s", rec.IP, err)
+				}
+			}
+		}
+
+		time.Sleep(config.interval)
+	}
+}
diff --git a/switchq/vendors.json b/switchq/vendors.json
new file mode 100644
index 0000000..03d888f
--- /dev/null
+++ b/switchq/vendors.json
@@ -0,0 +1,17 @@
+[
+    {
+	"prefix" : "CC:37:AB",
+	"vendor" : "Edgecore Networks Corportation",
+	"provision" : true
+    },
+    {
+        "prefix" : "70:72:CF",
+        "vendor" : "Edgecore Networks Corportation",
+        "provision" : true
+    },
+    {
+        "prefix" : "60:5B:B4",
+        "vendor" : "Fake",
+        "provision" : false
+    }
+]