update switchq to calling provisioner, fixed a few bugs found while testing at on.labs

Change-Id: I2367669aa54f680b98ff0cbbc8d41a49fb7e7a79
diff --git a/QUICKSTART.md b/QUICKSTART.md
index de8a5fe..4d1eefc 100644
--- a/QUICKSTART.md
+++ b/QUICKSTART.md
@@ -44,7 +44,7 @@
      via REST interfaces
    - cord-maas-automation - (directory: automation) run on the head node to automate PXE booted servers
      through the MAAS bare metal deployment work flow
-   - cord-maas-dhcp-harvester - (directory: harvester) run on the head node to facilitate CORD / DHCP / DNS
+   - cord-dhcp-harvester - (directory: harvester) run on the head node to facilitate CORD / DHCP / DNS
      integration so that all hosts can be resolved via DNS
 
 ### Build
diff --git a/automation/Dockerfile b/automation/Dockerfile
index ad78874..22ce5f7 100644
--- a/automation/Dockerfile
+++ b/automation/Dockerfile
@@ -4,14 +4,14 @@
 
 WORKDIR /go
 RUN go get github.com/tools/godep
-ADD . /go/src/github.com/ciena/cord-maas-automation
+ADD . /go/src/gerrit.opencord.org/maas/cord-maas-automation
 
-WORKDIR /go/src/github.com/ciena/cord-maas-automation
+WORKDIR /go/src/gerrit.opencord.org/maas/cord-maas-automation
 RUN /go/bin/godep restore
 
 WORKDIR /go
 
-RUN go install github.com/ciena/cord-maas-automation
+RUN go install gerrit.opencord.org/maas/cord-maas-automation
 
 RUN mkdir -p /root/.ssh
 COPY ssh-config /root/.ssh/config
diff --git a/automation/Dockerfile.ansible b/automation/Dockerfile.ansible
index 7606a61..ac3c520 100644
--- a/automation/Dockerfile.ansible
+++ b/automation/Dockerfile.ansible
@@ -33,12 +33,12 @@
 	apt-get install -y git ansible
 
 RUN go get github.com/tools/godep
-ADD . $GOPATH/src/github.com/ciena/cord-maas-automation
+ADD . $GOPATH/src/gerrit.opencord.org/maas/cord-maas-automation
 
-WORKDIR $GOPATH/src/github.com/ciena/cord-maas-automation
+WORKDIR $GOPATH/src/gerrit.opencord.org/maas/cord-maas-automation
 RUN $GOPATH/bin/godep restore
 
 WORKDIR $GOPATH
-RUN go install github.com/ciena/cord-maas-automation
+RUN go install gerrit.opencord.org/maas/cord-maas-automation
 
 ENTRYPOINT ["$GOPATH/bin/cord-maas-automation"]
diff --git a/build.gradle b/build.gradle
index 3f515a6..9f10b41 100644
--- a/build.gradle
+++ b/build.gradle
@@ -32,6 +32,24 @@
     vboxUser = project.hasProperty('vboxUser') ? project.getProperty('vboxUser') : 'cord'
 }
 
+// Switch Configuration Image
+
+task buildSwitchqImage(type: Exec) {
+    commandLine "$dockerPath/docker", 'build', '-t', 'cord-maas-switchq', './switchq'
+}
+
+task tagSwitchqImage(type: Exec) {
+   dependsOn buildSwitchqImage
+   commandLine "$dockerPath/docker", 'tag', 'cord-maas-switchq', "$targetReg/cord-maas-switchq:$targetTag"
+}
+
+task publishSwitchqImage(type: Exec) {
+    dependsOn tagSwitchqImage
+    commandLine "$dockerPath/docker", 'push', "$targetReg/cord-maas-switchq:$targetTag"
+}
+
+// Bootstrap Image
+
 task buildBootstrapImage(type: Exec) {
     commandLine "$dockerPath/docker", 'build', '-t', 'cord-maas-bootstrap', './bootstrap'
 }
@@ -46,6 +64,8 @@
     commandLine "$dockerPath/docker", 'push', "$targetReg/cord-maas-bootstrap:$targetTag"
 }
 
+// IP Allocator Image
+
 task buildAllocationImage(type: Exec) {
     commandLine "$dockerPath/docker", 'build', '-t', 'cord-ip-allocator', './ip-allocator'
 }
@@ -60,6 +80,8 @@
     commandLine "$dockerPath/docker", 'push', "$targetReg/cord-ip-allocator:$targetTag"
 }
 
+// Provisioner Image
+
 task buildProvisionerImage(type: Exec) {
     commandLine "$dockerPath/docker", 'build', '-t', 'cord-provisioner', './provisioner'
 }
@@ -74,6 +96,8 @@
     commandLine "$dockerPath/docker", 'push', "$targetReg/cord-provisioner:$targetTag"
 }
 
+// Automation Images
+
 task buildAutomationImage(type: Exec) {
     commandLine "$dockerPath/docker", 'build', '-t', "cord-maas-automation", "-f", "./automation/Dockerfile", "./automation"
 }
@@ -117,18 +141,20 @@
     dependsOn publishAutomationImageAnsible
 }
 
+// DHCP Harvester Images
+
 task buildHarvesterImage(type: Exec) {
-    commandLine "$dockerPath/docker", 'build', '-t', "cord-maas-dhcp-harvester", "./harvester"
+    commandLine "$dockerPath/docker", 'build', '-t', "cord-dhcp-harvester", "./harvester"
 }
 
 task tagHarvesterImage(type: Exec) {
     dependsOn buildHarvesterImage
-    commandLine "$dockerPath/docker", 'tag', 'cord-maas-dhcp-harvester', "$targetReg/cord-maas-dhcp-harvester:$targetTag"
+    commandLine "$dockerPath/docker", 'tag', 'cord-dhcp-harvester', "$targetReg/cord-dhcp-harvester:$targetTag"
 }
 
 task publishHarvesterImage(type: Exec) {
     dependsOn tagHarvesterImage
-    commandLine "$dockerPath/docker", 'push', "$targetReg/cord-maas-dhcp-harvester:$targetTag"
+    commandLine "$dockerPath/docker", 'push', "$targetReg/cord-dhcp-harvester:$targetTag"
 }
 
 // ~~~~~~~~~~~~~~~~~~~ Global tasks ~~~~~~~~~~~~~~~~~~~~~~~
@@ -149,6 +175,7 @@
     dependsOn buildAutomationImages
     dependsOn buildAllocationImage
     dependsOn buildProvisionerImage
+    dependsOn buildSwitchqImage
 }
 
 task tagImages {
@@ -157,6 +184,7 @@
     dependsOn tagAutomationImages
     dependsOn tagAllocationImage
     dependsOn tagProvisionerImage
+    dependsOn tagSwitchqImage
 }
 
 task publish {
@@ -165,6 +193,7 @@
     dependsOn publishAutomationImages
     dependsOn publishAllocationImage
     dependsOn publishProvisionerImage
+    dependsOn publishSwitchqImage
 }
 
 // ~~~~~~~~~~~~~~~~~~~ Deployment / Test Tasks  ~~~~~~~~~~~~~~~~~~~~~~~
@@ -231,6 +260,11 @@
         .p(config.otherServers.role, "prov_role")
     }
 
+    if (config.docker) {
+        extraVars = extraVars.p(config.docker.registry, "docker_registry")
+            .p(config.docker.imageVersion, "docker_image_version")
+    }
+
     def skipTags = [].p(config.seedServer.skipTags)
 
     args = args.p(skipTags.asParam("skip-tags", ",")).p(extraVars.asParam("extra-vars", " ")) << "head-node.yml"
diff --git a/config/default.yml b/config/default.yml
index d2acb05..4c377a4 100644
--- a/config/default.yml
+++ b/config/default.yml
@@ -34,3 +34,7 @@
   location: 'http://gerrit.opencord.org/maas'
   rolesPath: 'roles'
   role: 'compute-node'
+
+docker:
+  registry: 'opencord'
+  imageVersion: 'latest'
diff --git a/config/develop.yml b/config/develop.yml
new file mode 100644
index 0000000..a42e9e8
--- /dev/null
+++ b/config/develop.yml
@@ -0,0 +1,40 @@
+# Deployment configuration for VirtualBox based head node.
+#
+# This deployment configuration can be utilized with the head node created
+# via `vargrant up headnode` from the gerrit.opencord.org/maas repository.
+---
+seedServer:
+  ip: '10.100.198.202'
+
+  # User name and password used by Ansible to connect to the host for remote
+  # provisioning
+  user: 'vagrant'
+  password: 'vagrant'
+
+  # Specifies tasks within the head node provisioning not to execute, including:
+  #
+  # switch_support -   don't download the switch ONL images as there are no 
+  #                    switches in this configuration
+  # interface_config - don't update the network configuration of the headnode
+  #                    as it is configured by vagrant to the proper settings
+  skipTags:
+    - 'switch_support'
+    - 'interface_config'
+
+  # Specifies the extra settings required for this configuration
+  #
+  # virtualbox_support - install support for managing virtual box based
+  #                      compute nodes
+  extraVars:
+    - 'virtualbox_support=1'
+    - 'external_iface=eth0'
+
+otherServers:
+  # Specifies the configuration for dynamically added compute nodes
+  location: 'http://gerrit.opencord.org/maas'
+  rolesPath: 'roles'
+  role: 'compute-node'
+
+docker:
+  registry: '10.100.198.200:5000/opencord'
+  imageVersion: 'candidate'
diff --git a/ip-allocator/Dockerfile b/ip-allocator/Dockerfile
index 96d97a2..870381c 100644
--- a/ip-allocator/Dockerfile
+++ b/ip-allocator/Dockerfile
@@ -4,13 +4,13 @@
 
 WORKDIR /go
 RUN go get github.com/tools/godep
-ADD . /go/src/github.com/ciena/cord-ip-allocator
+ADD . /go/src/gerrit.opencord.org/maas/cord-ip-allocator
 
-WORKDIR /go/src/github.com/ciena/cord-ip-allocator
+WORKDIR /go/src/gerrit.opencord.org/maas/cord-ip-allocator
 RUN /go/bin/godep restore
 
 WORKDIR /go
 
-RUN go install github.com/ciena/cord-ip-allocator
+RUN go install gerrit.opencord.org/maas/cord-ip-allocator
 
 ENTRYPOINT ["/go/bin/cord-ip-allocator"]
diff --git a/provisioner/Dockerfile b/provisioner/Dockerfile
index cf4a86b..429203a 100644
--- a/provisioner/Dockerfile
+++ b/provisioner/Dockerfile
@@ -24,7 +24,7 @@
 RUN mkdir -p "$GOPATH/src" "$GOPATH/bin" && chmod -R 777 "$GOPATH"
 
 # CORD Provisioner Dockerfile
-WORKDIR $GOPATH
+WORKDIR /go
 
 RUN apt-get update && \
 	apt-get install -y  software-properties-common && \
@@ -36,12 +36,12 @@
 COPY ssh-config /root/.ssh/config
 
 RUN go get github.com/tools/godep
-ADD . $GOPATH/src/github.com/ciena/cord-provisioner
+ADD . /go/src/gerrit.opencord.org/maas/cord-provisioner
 
-WORKDIR $GOPATH/src/github.com/ciena/cord-provisioner
+WORKDIR /go/src/gerrit.opencord.org/maas/cord-provisioner
 RUN $GOPATH/bin/godep restore
 
 WORKDIR $GOPATH
-RUN go install github.com/ciena/cord-provisioner
+RUN go install gerrit.opencord.org/maas/cord-provisioner
 
 ENTRYPOINT ["/go/bin/cord-provisioner"]
diff --git a/provisioner/Godeps/Godeps.json b/provisioner/Godeps/Godeps.json
index a68b4ef..0b4660b 100644
--- a/provisioner/Godeps/Godeps.json
+++ b/provisioner/Godeps/Godeps.json
@@ -1,5 +1,5 @@
 {
-	"ImportPath": "github.com/ciena/provisioner",
+	"ImportPath": "gerrit.opencord.org/maas/cord-provisioner",
 	"GoVersion": "go1.6",
 	"GodepVersion": "v72",
 	"Deps": [
@@ -10,8 +10,8 @@
 		},
 		{
 			"ImportPath": "github.com/gorilla/mux",
-			"Comment": "v1.1-9-gbd09be0",
-			"Rev": "bd09be08ed4377796d312df0a45314e11b8f5dc1"
+			"Comment": "v1.1-13-g9fa818a",
+			"Rev": "9fa818a44c2bf1396a17f9d5a3c0f6dd39d2ff8e"
 		},
 		{
 			"ImportPath": "github.com/kelseyhightower/envconfig",
diff --git a/provisioner/dispatcher.go b/provisioner/dispatcher.go
index fb16458..bf6ebbf 100644
--- a/provisioner/dispatcher.go
+++ b/provisioner/dispatcher.go
@@ -20,10 +20,10 @@
 }
 
 type StatusMsg struct {
-	Request *WorkRequest
-	Worker  int
-	Status  TaskStatus
-	Message string
+	Request *WorkRequest `json:"request"`
+	Worker  int          `json:"worker"`
+	Status  TaskStatus   `json:"status"`
+	Message string       `json:"message"`
 }
 
 func NewWorker(id int, workerQueue chan chan WorkRequest, statusChan chan StatusMsg) Worker {
@@ -49,7 +49,7 @@
 			case work := <-w.Work:
 				// Receive a work request.
 				w.StatusChan <- StatusMsg{&work, w.ID, Running, ""}
-				log.Printf("RUN: %s %s %s %s %s %s",
+				log.Printf("[debug] RUN: %s %s %s %s %s %s",
 					work.Script, work.Info.Id, work.Info.Name,
 					work.Info.Ip, work.Info.Mac, work.Role)
 				err := exec.Command(work.Script, work.Info.Id, work.Info.Name,
@@ -117,12 +117,12 @@
 		for {
 			select {
 			case work := <-d.WorkQueue:
-				log.Println("Received work requeust")
+				log.Println("[debug] Received work requeust")
 				go func() {
 					d.StatusChan <- StatusMsg{&work, -1, Pending, ""}
 					worker := <-d.WorkerQueue
 
-					log.Println("Dispatching work request")
+					log.Println("[debug] Dispatching work request")
 					worker <- work
 				}()
 			case update := <-d.StatusChan:
diff --git a/provisioner/handlers.go b/provisioner/handlers.go
index 42946da..c2e78ba 100644
--- a/provisioner/handlers.go
+++ b/provisioner/handlers.go
@@ -4,22 +4,33 @@
 	"bufio"
 	"encoding/json"
 	"github.com/gorilla/mux"
+	"log"
 	"net/http"
 	"strings"
 )
 
 type RequestInfo struct {
-	Id   string
-	Name string
-	Ip   string
-	Mac  string
+	Id           string `json:"id"`
+	Name         string `json:"name"`
+	Ip           string `json:"ip"`
+	Mac          string `json:"mac"`
+	RoleSelector string `json:"role_selector"`
+	Role         string `json:"role"`
+	Script       string `json:"script"`
 }
 
 func (c *Context) GetRole(info *RequestInfo) (string, error) {
-	if c.config.RoleSelectorURL == "" {
+	if info.Role != "" {
+		return info.Role, nil
+	} else if c.config.RoleSelectorURL == "" && info.RoleSelector == "" {
 		return c.config.DefaultRole, nil
 	}
-	r, err := http.Get(c.config.RoleSelectorURL)
+	selector := c.config.RoleSelectorURL
+	if info.RoleSelector != "" {
+		selector = info.RoleSelector
+	}
+
+	r, err := http.Get(selector)
 	if err != nil {
 		return "", err
 	}
@@ -51,6 +62,7 @@
 		http.Error(w, err.Error(), http.StatusBadRequest)
 		return
 	}
+
 	if !c.validateData(&info) {
 		w.WriteHeader(http.StatusBadRequest)
 		return
@@ -89,6 +101,7 @@
 	}
 	s, err := c.storage.Get(id)
 	if err != nil {
+		log.Printf("[warn] Error while retrieving status for '%s' from strorage : %s", id, err)
 		http.Error(w, err.Error(), http.StatusInternalServerError)
 		return
 	}
@@ -98,6 +111,7 @@
 	}
 	bytes, err := json.Marshal(s)
 	if err != nil {
+		log.Printf("[error] Error while attempting to marshal status for '%s' from storage : %s", id, err)
 		http.Error(w, err.Error(), http.StatusInternalServerError)
 		return
 	}
@@ -105,7 +119,7 @@
 	switch s.Status {
 	case Pending, Running:
 		w.WriteHeader(http.StatusAccepted)
-	case Complete:
+	case Failed, Complete:
 		w.WriteHeader(http.StatusOK)
 	default:
 		w.WriteHeader(http.StatusInternalServerError)
diff --git a/provisioner/storage.go b/provisioner/storage.go
index db64df2..92c5339 100644
--- a/provisioner/storage.go
+++ b/provisioner/storage.go
@@ -1,9 +1,5 @@
 package main
 
-import (
-	"log"
-)
-
 type Storage interface {
 	Put(id string, update StatusMsg) error
 	Get(id string) (*StatusMsg, error)
@@ -22,7 +18,6 @@
 
 func (s *MemoryStorage) Put(id string, update StatusMsg) error {
 	s.Data[id] = update
-	log.Printf("%s : %s", id, update.Status.String())
 	return nil
 }
 
@@ -39,6 +34,7 @@
 	i := 0
 	for _, v := range s.Data {
 		r[i] = v
+		i += 1
 	}
 	return r, nil
 }
diff --git a/roles/maas/templates/automation-compose.yml.j2 b/roles/maas/templates/automation-compose.yml.j2
index 3b452df..35930fa 100644
--- a/roles/maas/templates/automation-compose.yml.j2
+++ b/roles/maas/templates/automation-compose.yml.j2
@@ -1,5 +1,5 @@
 allocator:
-  image: opencord/cord-ip-allocator:latest
+  image: "{{ docker.registry }}/cord-ip-allocator:{{ docker.image_version }}"
   container_name: allocator
   labels:
     - "lab.solution=CORD"
@@ -13,7 +13,7 @@
     - "ALLOCATE_SKIP=2"
 
 provisioner:
-  image: opencord/cord-provisioner:latest
+  image: "{{ docker.registry }}/cord-provisioner:{{ docker.image_version }}"
   container_name: provisioner
   labels:
     - "lab.solution=CORD"
@@ -31,8 +31,24 @@
   volumes:
     - "/etc/maas/ansible:/etc/maas/ansible"
 
+switchq:
+  image: "{{ docker.registry }}/cord-maas-switchq:{{ docker.image_version }}"
+  container_name: switchq
+  labels:
+    - "lab.solution=CORD"
+    - "lab.component=switchq"
+  links:
+    - provisioner
+  environment:
+    - "SWITCHQ_PROVISION_URL=http://provisioner:4243/provision/"
+    - "SWITCHQ_PROVISION_TTL=0s"
+    - "SWITCHQ_DEFAULT_ROLE=fabric-switch"
+    - "SWITCHQ_ADDRESS_URL=file:///switchq/dhcp/dhcp_harvest.inc"
+  volumnes:
+    - "/etc/bind/maas:/switchq/dhcp"
+
 automation:
-  image: opencord/cord-maas-automation:latest
+  image: "{{ docker.registry }}/cord-maas-automation:{{ docker.image_version }}"
   container_name: automation
   labels:
     - "lab.solution=CORD"
diff --git a/roles/maas/templates/harvest-compose.yml.j2 b/roles/maas/templates/harvest-compose.yml.j2
index 226c9e6..533251a 100644
--- a/roles/maas/templates/harvest-compose.yml.j2
+++ b/roles/maas/templates/harvest-compose.yml.j2
@@ -1,5 +1,5 @@
 harvester:
-    image: opencord/cord-dhcp-harvester:latest
+    image: "{{ docker.registry }}/cord-dhcp-harvester:{{ docker.image_version }}"
     container_name: harvester
     restart: always
     labels:
diff --git a/roles/maas/vars/main.yml b/roles/maas/vars/main.yml
index f881137..15d7155 100644
--- a/roles/maas/vars/main.yml
+++ b/roles/maas/vars/main.yml
@@ -49,3 +49,7 @@
     #   'bridge' name of the bride to create that is used when connecting
     #            the VMs created in support of XOS
     bridge_name: "{{ bridge_name | default('mgmtbr') }}"
+
+docker:
+    registry: "{{ docker_registry | default('opencord') }}"
+    image_version: "{{ docker_image_version | default('latest') }}"
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
     }
 ]