Merge "removing extaneous bond slaves config"
diff --git a/QUICKSTART.md b/QUICKSTART.md
index e108394..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
@@ -131,13 +131,22 @@
 Canonical MAAS provides the PXE and other bare metal provisioning services for CORD and will be deployed on the
 head node via `Ansible`. To initiate this deployment issue the following `gradle` command. This `gradle` command
 executes `ansible-playbook -i 10.100.198.202, --skip-tags=switch_support,interface_config --extra-vars=external_iface=eth0`.
+
 The IP address, `10.100.198.202` is the IP address assigned to the head node on a private network. The
 `skip-tags` option excludes Ansible tasks not required when utilizing the Vagrant based head node. The
 `extra-vars` option overrides the default role vars for the external interface and is needed for the VirtualBox
 based environment. Traffic from the compute nodes will be NAT-ed through this interface on the head node.
 
+The default MAAS deployment does not support power management for virtual box based hosts. As part of the MAAS
+installation support was added for power management, but it does require some additional configuration. This
+additional configuration is detailed below, but is mentioned here because when deploying the head node an
+additional parameter must be set. This parameter specified the username on the host machine that should be
+used when SSHing from the head node to the host machine to remotely execute the `vboxmanage` command. This
+is typically the username used when logging into your laptop or desktop development machine. This should
+be specified on the deploy command line using the `-PvboxUser` option.
+
 ```
-./gradlew deployMaas
+./gradlew -PvboxUser=<username> deploy
 ```
 
 This task can take some time so be patient. It should complete without errors, so if an error is encountered
@@ -200,22 +209,18 @@
 hostname made up, in the Canonical way, of a adjective and an noun, such as `popular-feast.cord.lab`. _The 
 name will be different for everyone._ The new node will be in the `New` state.
 
-For _real_ hosts, automation that leverages the MAAS APIs will transition the node from `New` through the
-states of `Commissioning` and `Acquired` to `Deployed`.
+If you have properly configured power management for virtualbox (see below) the host will be automatically
+transitioned from `New` through the start of `Comissioning` and `Acquired` to `Deployed`.
 
-#### Bad News
-For hosts that support IPMI, which includes the hosts recommended for the CORD POD, MAAS will automatically
-discover remote power management information. However, MAAS is not able to detect or manage power for 
-VirtualBox machines.
+#### Virtual Box Power Management
+Virtual box power management is implemented via helper scripts that SSH to the virtual box host and 
+execute `vboxmanage` commands. For this to work The scripts must be configured with a username and host
+to utilize when SSHing and that account must allow SSH from the head node guest to the host using
+SSH keys such that no password entry is required.
 
-A work-a-round has been created to support VirtualBox based VMs. This is accomplished by overriding the
-support script for the `Intel AMT` power support module; but unfortunately it is not fully automated at this
-point.
-
-The work-a-round uses SSH from the MAAS head node to the machine that is running VirtualBox. To enable this,
-assuming that VirtualBox is running on a Linux based system, you can copy the MAAS ssh public key from
-`/var/lib/maas/.ssh/id_rsa.pub` on the head known to your accounts `authorized_keys` files. You can verify
-that this is working by issuing the following commands from your host machine:
+To enable SSH key based login, assuming that VirtualBox is running on a Linux based system, you can copy
+the MAAS ssh public key from `/var/lib/maas/.ssh/id_rsa.pub` on the head known to your accounts `authorized_keys`
+files. You can verify that this is working by issuing the following commands from your host machine:
 ```
 vagrant ssh headnode
 sudo su - maas
@@ -224,53 +229,9 @@
 
 If you are able to accomplish these commands the VirtualBox power management should operate correctly.
 
-To utilize this work-a-round the power management settings must be manual configured on the node. To 
-accomplish this the VirtualBox ID for the compute node must first be discovered. To accomplish this issue
-the following command on the machine on which VirtualBox is running:
-```
-vboxmanage list vms
-```
-
-This will return to a list of all VMs being managed by VirtualBox. In this list will be an entry similar
-to the following for the compute node:
-```
-"maas_computenode_1463279637475_74274" {d18af821-91af-4d20-b3b2-67ed85e23c13}
-```
-
-The important part of this entry is the last 12 characters of the VM ID, `67ed85e23c13` in this example. This
-will be used as a fake MAC address for power management.
-
-To set the power settings for the compute node visit the MAAS UI page for the compute node. From there select
-the `Power type` to `Intel AMT`. This will display the additional fields: `MAC Address`, `Power Password`, and
-`Power Address`. The values of these fields should be set as follows:
-
-   - MAC Address - the previously discovered last 12 characters of the VM ID, formatted like a MAC address,
-                   `67:ed:85:e2:3c:13` from the example above.
-   - Power Password - the user name to use to ssh from the head node to the host on which VirtualBox is 
-                      executing.
-   - Power Address - the IP address of the host on which VirtualBox is executing.
-
-Once this information is saved the automation will eventually start the the compute node should transition
-to `Deployed` state. This will include several reboots and shutdowns of the compute node.
-
 ### Post Deployment Provisioning of the Compute Node
-Once the node is in the `Deployed` state, it can be provisioned for use in a CORD POD. Eventually, this action
-will be automated and triggered when a node moves to the `Deployed` state. Currently, this must be manually 
-invoked. The post deployment provisioning consists of configuring the networking on the node to comply with
-best practices of a CORD POD installation and leverages an Ansible play boot to accomplish the provisioning.
-
-To provision the compute node `ssh` to the head node,  change to the `/etc/maas/ansible` directory, and invoke
-the playbook on the new compute node. To ssh to the head node, you can use `vagrant ssh headnode` from the 
-host system for the virtual machines. Once __on__ the head node the following commands can invoke the
-provisioning of the compute node, __the name of the compute node is an example name, the actual name in the
-deployment can be found in the nodes list of the MAAS UI.__:
-
-```
-cd /etc/maas/ansible
-ansible-playbook -i popular-feast.cord.lab, compute-node.yml --skip-tags=interface_config
-```
-
-_NOTE: the `skip-tags` option is required for the VirtualBox based environment_
+Once the node is in the `Deployed` state, it will be provisioned for use in a CORD POD by the execution of 
+an `Ansible` playbook.
 
 ### Complete
 Once the compute node is in the `Deployed` state and post deployment provisioning on the compute node is
diff --git a/automation/Dockerfile b/automation/Dockerfile
index d1b6318..22ce5f7 100644
--- a/automation/Dockerfile
+++ b/automation/Dockerfile
@@ -1,16 +1,19 @@
 FROM golang:alpine
 
-RUN apk --update add git
+RUN apk --update add openssh-client git
 
 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
 
 ENTRYPOINT ["/go/bin/cord-maas-automation"]
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/automation/Godeps/Godeps.json b/automation/Godeps/Godeps.json
index cd9aa7e..a267c3c 100644
--- a/automation/Godeps/Godeps.json
+++ b/automation/Godeps/Godeps.json
@@ -24,6 +24,11 @@
 			"ImportPath": "github.com/hashicorp/consul",
 			"Comment": "v0.6.4-341-g22938ab",
 			"Rev": "22938ab8b3ca069e490a4897a8ec13308ac61605"
-		}
+		},
+                {
+                        "ImportPath": "github.com/kelseyhightower/envconfig",
+                        "Comment": "1.1.0-17-g91921eb",
+                        "Rev": "91921eb4cf999321cdbeebdba5a03555800d493b"
+                }
 	]
 }
diff --git a/automation/maas-flow.go b/automation/maas-flow.go
index 1f501b9..84337aa 100644
--- a/automation/maas-flow.go
+++ b/automation/maas-flow.go
@@ -3,6 +3,7 @@
 import (
 	"encoding/json"
 	"flag"
+	"github.com/kelseyhightower/envconfig"
 	"log"
 	"net/url"
 	"os"
@@ -26,11 +27,16 @@
            }
 	}`
 	defaultMapping = "{}"
-	PROVISION_URL  = "PROVISION_URL"
-	PROVISION_TTL  = "PROVISION_TTL"
-	DEFAULT_TTL    = "30m"
 )
 
+type Config struct {
+	PowerHelperUser   string `default:"cord" envconfig:"POWER_HELPER_USER"`
+	PowerHelperHost   string `default:"127.0.0.1" envconfig:"POWER_HELPER_HOST"`
+	PowerHelperScript string `default:"" envconfig:"POWER_HELPER_SCRIPT"`
+	ProvisionUrl      string `default:"" envconfig:"PROVISION_URL"`
+	ProvisionTtl      string `default:"30m" envconfig:"PROVISION_TTL"`
+}
+
 var apiKey = flag.String("apikey", "", "key with which to access MAAS server")
 var maasURL = flag.String("maas", "http://localhost/MAAS", "url over which to access MAAS")
 var apiVersion = flag.String("apiVersion", "1.0", "version of the API to access")
@@ -90,28 +96,23 @@
 func main() {
 
 	flag.Parse()
+	config := Config{}
+	err := envconfig.Process("AUTOMATION", &config)
+	checkError(err, "[error] unable to parse environment options : %s", err)
 
 	options := ProcessingOptions{
-		Preview:      *preview,
-		Verbose:      *verbose,
-		AlwaysRename: *always,
-		ProvTracker:  NewTracker(),
-		ProvisionURL: os.Getenv(PROVISION_URL),
+		Preview:         *preview,
+		Verbose:         *verbose,
+		AlwaysRename:    *always,
+		ProvTracker:     NewTracker(),
+		ProvisionURL:    config.ProvisionUrl,
+		PowerHelper:     config.PowerHelperScript,
+		PowerHelperUser: config.PowerHelperUser,
+		PowerHelperHost: config.PowerHelperHost,
 	}
 
-	var ttl string
-	if ttl = os.Getenv(PROVISION_TTL); ttl == "" {
-		ttl = "30m"
-	}
-
-	var err error
-	options.ProvisionTTL, err = time.ParseDuration(ttl)
-	if err != nil {
-		log.Printf("[warn] unable to parse specified duration of '%s', defaulting to '%s'",
-			ttl, DEFAULT_TTL)
-		options.ProvisionTTL, err = time.ParseDuration("30m")
-		checkError(err, "[error] unable to parse default TTL duration of '30m' : %s", err)
-	}
+	options.ProvisionTTL, err = time.ParseDuration(config.ProvisionTtl)
+	checkError(err, "[error] unable to parse specified duration of '%s' : %s", err)
 
 	// Determine the filter, this can either be specified on the the command
 	// line as a value or a file reference. If none is specified the default
@@ -161,14 +162,19 @@
 	    MAAS URL:            %s
 	    MAAS API Version:    %s
 	    MAAS Query Interval: %s
-	    Node Filtter:        %s
+	    Node Filter:         %s
 	    Node Name Mappings:  %s
 	    Preview:             %v
 	    Verbose:             %v
 	    Always Rename:       %v
-	    Provision URL:       %s `,
+	    Provision URL:       %s
+	    Provision TTL:       %s
+	    Power Helper:        %s
+	    Power Helper User:   %s
+	    Power Helper Host:   %s`,
 		*maasURL, *apiVersion, *queryPeriod, *filterSpec, *mappings, options.Preview,
-		options.Verbose, options.AlwaysRename, options.ProvisionURL)
+		options.Verbose, options.AlwaysRename, options.ProvisionURL, options.ProvisionTTL,
+		options.PowerHelper, options.PowerHelperUser, options.PowerHelperHost)
 
 	authClient, err := maas.NewAuthenticatedClient(*maasURL, *apiKey, *apiVersion)
 	if err != nil {
diff --git a/automation/node.go b/automation/node.go
index fe61add..9e045e6 100644
--- a/automation/node.go
+++ b/automation/node.go
@@ -2,8 +2,10 @@
 
 import (
 	"fmt"
+	"log"
 
 	maas "github.com/juju/gomaasapi"
+	"net/url"
 )
 
 // MaasNodeStatus MAAS lifecycle status for nodes
@@ -69,11 +71,28 @@
 	return id
 }
 
+func (n *MaasNode) PowerType() string {
+	ptype, _ := n.GetString("power_type")
+	return ptype
+}
+
 func (n *MaasNode) PowerState() string {
 	state, _ := n.GetString("power_state")
 	return state
 }
 
+func (n *MaasNode) UpdatePowerParameters(ptype string, params map[string]string) {
+	values := url.Values{}
+	values.Add("power_type", ptype)
+	for k, v := range params {
+		values.Add("power_parameters_"+k, v)
+	}
+	_, err := n.Update(values)
+	if err != nil {
+		log.Printf("[error] error updating power settings : %s", err.Error())
+	}
+}
+
 // Hostname get the hostname
 func (n *MaasNode) Hostname() string {
 	hn, _ := n.GetString("hostname")
@@ -94,7 +113,9 @@
 			linkObj, _ := link.GetMap()
 			ipObj, _ := linkObj["ip_address"]
 			ip, _ := ipObj.GetString()
-			result = append(result, ip)
+			if ip != "" {
+				result = append(result, ip)
+			}
 		}
 	}
 
diff --git a/automation/ssh-config b/automation/ssh-config
new file mode 100644
index 0000000..990a43d
--- /dev/null
+++ b/automation/ssh-config
@@ -0,0 +1,3 @@
+Host *
+   StrictHostKeyChecking no
+   UserKnownHostsFile=/dev/null
diff --git a/automation/state.go b/automation/state.go
index 4e0dd1f..30d3f06 100644
--- a/automation/state.go
+++ b/automation/state.go
@@ -26,6 +26,13 @@
 	Using   Action
 }
 
+type Power struct {
+	Name          string `json:"name"`
+	MacAddress    string `json:"mac_address"`
+	PowerPassword string `json:"power_password"`
+	PowerAddress  string `json:"power_address"`
+}
+
 // ProcessingOptions used to determine on what hosts to operate
 type ProcessingOptions struct {
 	Filter struct {
@@ -38,13 +45,16 @@
 			Exclude []string
 		}
 	}
-	Mappings     map[string]interface{}
-	Verbose      bool
-	Preview      bool
-	AlwaysRename bool
-	ProvTracker  Tracker
-	ProvisionURL string
-	ProvisionTTL time.Duration
+	Mappings        map[string]interface{}
+	Verbose         bool
+	Preview         bool
+	AlwaysRename    bool
+	ProvTracker     Tracker
+	ProvisionURL    string
+	ProvisionTTL    time.Duration
+	PowerHelper     string
+	PowerHelperUser string
+	PowerHelperHost string
 }
 
 // Transitions the actual map
@@ -287,14 +297,38 @@
 					log.Printf("[error] Failed to quest provision state for node '%s' : %s",
 						node.Hostname(), err)
 				} else {
+					defer resp.Body.Close()
 					switch resp.StatusCode {
-					case http.StatusOK: // provisioning completed
-						if options.Verbose {
-							log.Printf("[info] Marking node '%s' with ID '%s' as provisioned",
-								node.Hostname(), node.ID())
+					case http.StatusOK: // provisioning completed or failed
+						decoder := json.NewDecoder(resp.Body)
+						var raw interface{}
+						err = decoder.Decode(&raw)
+						if err != nil {
+							log.Printf("[error] Unable to unmarshal response from provisioner for '%s': %s",
+								node.Hostname(), err)
 						}
-						record.State = Provisioned
-						options.ProvTracker.Set(node.ID(), record)
+						status := raw.(map[string]interface{})
+						switch int(status["status"].(float64)) {
+						case 0, 1: // PENDING, RUNNING ... should never really get here
+							// noop, already in this state
+						case 2: // COMPLETE
+							if options.Verbose {
+								log.Printf("[info] Marking node '%s' with ID '%s' as provisioned",
+									node.Hostname(), node.ID())
+							}
+							record.State = Provisioned
+							options.ProvTracker.Set(node.ID(), record)
+						case 3: // FAILED
+							if options.Verbose {
+								log.Printf("[info] Marking node '%s' with ID '%s' as failed provisioning",
+									node.Hostname(), node.ID())
+							}
+							record.State = ProvisionError
+							options.ProvTracker.Set(node.ID(), record)
+						default:
+							log.Printf("[error] unknown status state for node '%s' : %d",
+								node.Hostname(), int(status["status"].(float64)))
+						}
 					case http.StatusAccepted: // in the provisioning state
 						// Noop, presumably alread in this state
 					default: // Consider anything else an erorr
@@ -307,7 +341,7 @@
 			}
 		}
 	} else if options.Verbose {
-		log.Printf("[info] Not invoking provisioning for '%s', currned state is '%s'", node.Hostname(),
+		log.Printf("[info] Not invoking provisioning for '%s', current state is '%s'", node.Hostname(),
 			record.State.String())
 	}
 
@@ -496,7 +530,40 @@
 		break
 	default:
 		// We are in a state from which we can't move forward.
-		log.Printf("ERROR: %s has invalid power state '%s'", node.Hostname(), state)
+		log.Printf("[warn]: %s has invalid power state '%s'", node.Hostname(), state)
+
+		// If a power helper script is set, we have an unknown power state, and
+		// we have not power type then attempt to use the helper script to discover
+		// and set the power settings
+		if options.PowerHelper != "" && node.PowerType() == "" {
+			cmd := exec.Command(options.PowerHelper,
+				append([]string{options.PowerHelperUser, options.PowerHelperHost},
+					node.MACs()...)...)
+			stdout, err := cmd.Output()
+			if err != nil {
+				log.Printf("[error] Failed while executing power helper script '%s' : %s",
+					options.PowerHelper, err)
+				return err
+			}
+			power := Power{}
+			err = json.Unmarshal(stdout, &power)
+			if err != nil {
+				log.Printf("[error] Failed to parse output of power helper script '%s' : %s",
+					options.PowerHelper, err)
+				return err
+			}
+			switch power.Name {
+			case "amt":
+				params := map[string]string{
+					"mac_address":   power.MacAddress,
+					"power_pass":    power.PowerPassword,
+					"power_address": power.PowerAddress,
+				}
+				node.UpdatePowerParameters(power.Name, params)
+			default:
+				log.Printf("[warn] Unsupported power type discovered '%s'", power.Name)
+			}
+		}
 		break
 	}
 	return nil
@@ -543,7 +610,8 @@
 	var err error
 	for _, action := range actions {
 		if err = action(client, node, options); err != nil {
-			log.Printf("[error] Error while processing action for node '%s' : %s", node.Hostname, err)
+			log.Printf("[error] Error while processing action for node '%s' : %s",
+				node.Hostname(), err)
 			break
 		}
 	}
diff --git a/build.gradle b/build.gradle
index f5120d3..1ed7930 100644
--- a/build.gradle
+++ b/build.gradle
@@ -28,8 +28,28 @@
     deployConfig = project.hasProperty('deployConfig') ? project.getProperty('deployConfig') : './config/default.yml'
 
     dockerPath = project.hasProperty('dockerPath') ? project.getProperty('dockerPath') : '/usr/bin'
+
+    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'
 }
@@ -44,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'
 }
@@ -58,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'
 }
@@ -72,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"
 }
@@ -115,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 ~~~~~~~~~~~~~~~~~~~~~~~
@@ -147,6 +175,7 @@
     dependsOn buildAutomationImages
     dependsOn buildAllocationImage
     dependsOn buildProvisionerImage
+    dependsOn buildSwitchqImage
 }
 
 task tagImages {
@@ -155,6 +184,7 @@
     dependsOn tagAutomationImages
     dependsOn tagAllocationImage
     dependsOn tagProvisionerImage
+    dependsOn tagSwitchqImage
 }
 
 task publish {
@@ -163,6 +193,7 @@
     dependsOn publishAutomationImages
     dependsOn publishAllocationImage
     dependsOn publishProvisionerImage
+    dependsOn publishSwitchqImage
 }
 
 // ~~~~~~~~~~~~~~~~~~~ Deployment / Test Tasks  ~~~~~~~~~~~~~~~~~~~~~~~
@@ -216,7 +247,21 @@
             .p(config.seedServer.sudoPassword, "ansible_sudo_pass")
             .p(config.seedServer.fabric_ip, "fabric_ip")
 	    .p(config.seedServer.management_ip, "management_ip")
+            .p(config.seedServer.management_network, "management_network")
+	    .p(config.seedServer.management_iface, "management_iface")
 	    .p(config.seedServer.external_ip, "external_ip")
+            .p(config.seedServer.external_network, "external_network")
+            .p(config.seedServer.external_iface, "external_iface")
+	    .p(config.seedServer.fabric_ip, "fabric_ip")
+	    .p(config.seedServer.fabric_network, "fabric_network")
+	    .p(config.seedServer.fabric_iface, "fabric_iface")
+            .p(config.seedServer.domain, "domain")
+	    .p(config.seedServer.power_helper_user, "power_helper_user")
+	    .p(config.seedServer.power_helper_host, "power_helper_host")
+    }
+
+    if (vboxUser != "") {
+        extraVars = extraVars.p(vboxUser, "power_helper_user")
     }
 
     if (config.otherServers) {
@@ -225,6 +270,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/harvester/dhcpharvester.py b/harvester/dhcpharvester.py
index cc4e372..5b3f145 100755
--- a/harvester/dhcpharvester.py
+++ b/harvester/dhcpharvester.py
@@ -459,7 +459,7 @@
             name = "UNK-" + lease['hardware'].translate(None, ':').upper()
 
         if ifilter != None and name in ifilter or matched(rfilter, name):
-            out.write(format(name, '<'+str(size)) + ' IN A ' + lease['ip_address'] + '\n')
+            out.write(format(name, '<'+str(size)) + ' IN A ' + lease['ip_address'] + ' ; ' + lease['hardware'] +'\n')
     if options.dest != '-':
         out.close()
     return count
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/compute-node/files/config b/roles/compute-node/files/config
new file mode 100644
index 0000000..990a43d
--- /dev/null
+++ b/roles/compute-node/files/config
@@ -0,0 +1,3 @@
+Host *
+   StrictHostKeyChecking no
+   UserKnownHostsFile=/dev/null
diff --git a/roles/compute-node/tasks/main.yml b/roles/compute-node/tasks/main.yml
index 31c25ba..1b7fb9b 100644
--- a/roles/compute-node/tasks/main.yml
+++ b/roles/compute-node/tasks/main.yml
@@ -53,6 +53,7 @@
   with_items:
     - id_rsa
     - id_rsa.pub
+    - config
 
 - name: Ensure CORD SUDO
   become: yes
diff --git a/roles/docker/tasks/main.yml b/roles/docker/tasks/main.yml
index 880617a..cd609fa 100644
--- a/roles/docker/tasks/main.yml
+++ b/roles/docker/tasks/main.yml
@@ -25,7 +25,7 @@
     state: latest
     force: yes
 
-- name: Docker Exposed vi TCP
+- name: Docker Exposed via TCP
   become: yes
   lineinfile:
     dest=/etc/default/docker
diff --git a/roles/maas/files/amt.template b/roles/maas/files/amt.template
index 2f2df8c..22c0ccc 100755
--- a/roles/maas/files/amt.template
+++ b/roles/maas/files/amt.template
@@ -9,11 +9,11 @@
 
 get_uuid () {
     local DATA=$(echo -n "$1" | sed -e 's/://g')
-    echo $(ssh $POWER_PASS@$POWER_ADDRESS vboxmanage list vms | grep "$DATA" | awk '{print $NF}' | sed -e 's/[{}]//g')
+    echo $(ssh $POWER_PASS@$POWER_ADDRESS vboxmanage list vms 2> /tmp/power_err.last | grep "$DATA" | awk '{print $NF}' | sed -e 's/[{}]//g')
 }
 
 query_state () {
-    local state=$(ssh $POWER_PASS@$POWER_ADDRESS vboxmanage showvminfo $1 | grep "^State" | grep -i running | wc -l)
+    local state=$(ssh $POWER_PASS@$POWER_ADDRESS vboxmanage showvminfo $1 2> /tmp/power_err.last | grep "^State" | grep -i running | wc -l)
     if [ $state -eq 1 ]; then
         echo 'on'
     else
diff --git a/roles/maas/files/power_discovery b/roles/maas/files/power_discovery
new file mode 100755
index 0000000..b691010
--- /dev/null
+++ b/roles/maas/files/power_discovery
@@ -0,0 +1,62 @@
+#!/bin/ash
+
+USERNAME=$1; shift
+HOSTIP=$1; shift
+SSH_KEY="-i /etc/maas/virtualbox/id_rsa"
+
+LIST=$(echo $* | awk '{print toupper($0)}')
+MATCH=
+
+best() {
+	local i
+	local BEST=
+	local BEST_CNT=0
+	for i in $MATCH; do
+		local CNT=$(echo $i | cut -d- -f2)
+		if [ $CNT > $BEST_CNT ]; then
+			BEST=$(echo $i | cut -d- -f1)
+			BEST_CNT=$CNT
+		fi
+	done
+	echo $BEST
+}
+
+inc() {
+	FOUND=$(echo $MATCH | grep "$1" |  wc -l)
+	if [ $FOUND -eq 0 ]; then
+		MATCH="$MATCH $1-1"
+	else
+		NEXT=
+		local i=
+		for  i in $MATCH; do
+			FOUND=$(echo $i | grep "$1" | wc -l)
+			if [ $FOUND -eq 1 ]; then
+				COUNT=$(echo $i | cut -d- -f2)
+				COUNT=$(expr $COUNT + 1)
+				NEXT="$NEXT $1-$COUNT"
+			else
+				NEXT="$NEXT $i"
+			fi
+		done
+		MATCH=$NEXT
+	fi
+}
+
+for i in $(ssh $SSH_KEY $USERNAME@$HOSTIP /usr/local/bin/vboxmanage list vms | awk '{print $NF}' | sed -e 's/[{}]//g'); do
+	for m in $(ssh $SSH_KEY $USERNAME@$HOSTIP /usr/local/bin/vboxmanage showvminfo --machinereadable $i | grep -i macaddress | cut -d= -f2 | sed -e 's/"//g' -e 's/\(..\)/\1:/g' -e 's/:$//g'); do
+		p=$(echo $i | cut -d- -f5)
+		for t in $LIST; do
+			if [ "$t" == "$m" ]; then
+				inc $p
+			fi
+		done
+	done
+done
+
+BEST=$(best)
+if [ "$BEST x" == " x" ]; then
+	echo "{}"
+else
+	MAC=$(echo $BEST | cut -d- -f5 | sed -e 's/"//g' -e 's/\(..\)/\1:/g' -e 's/:$//g')
+        echo "{\"name\":\"amt\",\"mac_address\":\"$MAC\",\"power_password\":\"$USERNAME\",\"power_address\":\"$HOSTIP\"}"
+fi
diff --git a/roles/maas/files/ssh_config b/roles/maas/files/ssh_config
index f30d239..990a43d 100644
--- a/roles/maas/files/ssh_config
+++ b/roles/maas/files/ssh_config
@@ -1,2 +1,3 @@
 Host *
-    StrictHostKeyChecking no
+   StrictHostKeyChecking no
+   UserKnownHostsFile=/dev/null
diff --git a/roles/maas/tasks/main.yml b/roles/maas/tasks/main.yml
index 5e7e349..e6f3ab2 100644
--- a/roles/maas/tasks/main.yml
+++ b/roles/maas/tasks/main.yml
@@ -158,6 +158,7 @@
   shell: ifconfig {{ interfaces.management }} 2>&1 | grep "inet addr:" | sed -e 's/.*:\([.0-9]*\)[ ]*Bcast.*/\1/g'
   register: mgmt_ip_address
   changed_when: false
+  failed_when: mgmt_ip_address.rc != 0 or mgmt_ip_address.stdout == ""
 
 # Currently the onie image is being stored in **Dropbox** on a personal account, which is not really what we want to
 # have in production, but it works for the time being.
@@ -210,7 +211,7 @@
   command: docker run opencord/cord-maas-bootstrap:latest --apikey='{{apikey.stdout}}' --sshkey='{{maas.user_sshkey}}' --url='http://{{mgmt_ip_address.stdout}}/MAAS/api/1.0' --network='{{networks.management}}' --interface='{{interfaces.management}}' --zone='administrative' --cluster='Cluster master' --domain='{{maas.domain}}' --bridge='{{networks.bridge_name}}' --bridge-subnet='{{networks.bridge}}'
   register: maas_config_result
   changed_when: maas_config_result.stdout.find("CHANGED") != -1
-  failed_when: "'ERROR' in maas_config_result.stdout"
+  failed_when: "maas_config_result.rc != 0 or 'ERROR' in maas_config_result.stdout"
 
 - name: Custom MAAS Configuration Template
   become: yes
@@ -229,6 +230,22 @@
     - { src: 'dhcp_harvest.inc', dest: '/etc/maas/templates/dns' }
     - { src: 'zone.template', dest: '/tmp' }
 
+- name: Ensure RNDC Listens
+  become: yes
+  lineinfile: 
+    dest: /etc/bind/maas/named.conf.rndc.maas
+    regexp: 'inet .* port 954'
+    line: '        inet 0.0.0.0 port 954'
+    state: present
+
+- name: Ensure ENDC Allows Trusted
+  become: yes
+  lineinfile:
+    dest: /etc/bind/maas/named.conf.rndc.maas
+    regexp: 'allow { .* } keys { "rndc-maas-key"; };'
+    line: '                allow { trusted; } keys { "rndc-maas-key"; };'
+    state: present
+
 - name: Custom DNS Zone Template
   become: yes
   script: files/update_dns_template.sh {{ networks.management }} {{ maas.domain }}
@@ -292,6 +309,20 @@
   tags:
     - interface_config
 
+- name: Default VirtualBox Host
+  become: no
+  set_fact: 
+    virtualbox_host: "{{ virtualbox.power_helper_host }}"
+  when: virtualbox_support is defined
+  changed_when: false
+
+- name: Override VirtualBox Host
+  become: no
+  set_fact:
+    virtualbox_host: "{{ discovered_vbox_host.stdout }}"
+  when: virtualbox_support is defined and virtualbox_host == ''
+  changed_when: false
+  
 - name: Custom Automation Compose Configurations
   become: yes
   template:
diff --git a/roles/maas/tasks/virtualbox.yml b/roles/maas/tasks/virtualbox.yml
index b263886..88dcba7 100644
--- a/roles/maas/tasks/virtualbox.yml
+++ b/roles/maas/tasks/virtualbox.yml
@@ -14,7 +14,34 @@
     group: maas
     mode: 0755
 
-- name: Ensure SSH Directory
+- name: Ensure SSH Discovery Directory
+  become: yes
+  file:
+    path: /etc/maas/virtualbox
+    state: directory
+    owner: maas
+    group: maas
+    mode: 0755
+
+- name: VirtualBox Power Discovery Support
+  become: yes
+  copy:
+    src: files/cord_id_rsa
+    dest: /etc/maas/virtualbox/id_rsa
+    owner: root
+    group: root
+    mode: 0600
+
+- name: VirtualBox Power Discovery Script
+  become: yes
+  copy:
+    src: files/power_discovery
+    dest: /etc/maas/virtualbox/power_discovery
+    owner: maas
+    group: maas
+    mode: 0755
+
+- name: Ensure SSH Power Script Directory
   become: yes
   file:
     path: /var/lib/maas/.ssh
@@ -23,7 +50,7 @@
     group: maas
     mode: 0700
 
-- name: VirtualBox SSH Support
+- name: VirtualBox Power Script Support
   become: yes
   copy:
     src: files/{{ item.src }}
@@ -36,3 +63,9 @@
     - { src: cord_id_rsa.pub, dest: id_rsa.pub }
     - { src: ssh_config, dest: config }
 
+- name: Discover VirtualBox Host
+  become: yes
+  shell: netstat -rn | grep "^0.0.0.0 " | cut -d " " -f10
+  register: discovered_vbox_host
+  changed_when: false
+
diff --git a/roles/maas/templates/automation-compose.yml.j2 b/roles/maas/templates/automation-compose.yml.j2
index d014bec..825b1c3 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"
+  volumes:
+    - "/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"
@@ -42,8 +58,16 @@
   environment:
     # need to explicitly set the resolver, else go will skip the /etc/hosts file
     - "GODEBUG=netdns=go"
-    - "PROVISION_URL=http://provisioner:4243/provision/"
-    - "PROVISION_TTL=30m"
+    - "AUTOMATION_PROVISION_URL=http://provisioner:4243/provision/"
+    - "AUTOMATION_PROVISION_TTL=30m"
+{% if virtualbox_support is defined and virtualbox_support == "1" %}
+    - "AUTOMATION_POWER_HELPER_SCRIPT=/etc/maas/virtualbox/power_discovery"
+    - "AUTOMATION_POWER_HELPER_USER={{ virtualbox.power_helper_user }}"
+    - "AUTOMATION_POWER_HELPER_HOST={{ virtualbox_host }}"
+{% endif %}
   volumes:
     - "/etc/maas:/mappings"
+{% if virtualbox_support is defined and virtualbox_support == "1" %}
+    - "/etc/maas/virtualbox:/etc/maas/virtualbox"
+{% endif %}
   command: [ "-apiVersion", "1.0", "-apikey", "{{ apikey.stdout }}", "-maas", "http://{{ mgmt_ip_address.stdout }}/MAAS", "-period", "30s", "-mappings", "@/mappings/mappings.json", "-always-rename" ]
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 b7f5a64..15d7155 100644
--- a/roles/maas/vars/main.yml
+++ b/roles/maas/vars/main.yml
@@ -1,6 +1,15 @@
 accton_as5712_54x: 'http://178.33.61.6/putstorage/DownloadFileHash/AA90EC2D3A5A4A5QQWE3332069EWQS/onie-installer-x86_64-accton_as6712_32x-r0'
 accton_as6712_32x: 'http://178.33.61.6/putstorage/DownloadFileHash/AA90EC2D3A5A4A5QQWE3332069EWQS/onie-installer-x86_64-accton_as6712_32x-r0'
 
+virtualbox:
+    # CHANGE:
+    #   'power_helper_user' specifies the user to use when SSHing to the host
+    #                       that is running virtualbox
+    #   'power_helper_host' specifies the IP address of host that is running
+    #                       virtualbox. will be dynamically set if empty string
+    power_helper_user: "{{ power_helper_user | default('cord') }}"
+    power_helper_host: "{{ power_helper_host | default('') }}"
+
 maas:
     admin_email: "{{ admin_email | default('admin@cord.lab') }}"
     user: "{{ maas_user | default('cord') }}"
@@ -40,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/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..edf6415
--- /dev/null
+++ b/switchq/storage.go
@@ -0,0 +1,75 @@
+package main
+
+import (
+	"fmt"
+	"net/url"
+	"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()
+	default:
+	}
+	return nil, fmt.Errorf("Unknown storage scheme specified, '%s'", u.Scheme)
+}
+
+type Storage interface {
+	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
+	ClearProvisioned(mac string) error
+}
+
+type MemoryStorage struct {
+	Checks map[string]time.Time
+	Times  map[string]time.Time
+}
+
+func NewMemoryStorage() (Storage, error) {
+
+	s := MemoryStorage{
+		Checks: make(map[string]time.Time),
+		Times:  make(map[string]time.Time),
+	}
+	return &s, 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
+}
+
+func (s *MemoryStorage) ClearProvisioned(mac string) error {
+	delete(s.Times, mac)
+	return nil
+}
diff --git a/switchq/switchq.go b/switchq/switchq.go
new file mode 100644
index 0000000..ca10f8f
--- /dev/null
+++ b/switchq/switchq.go
@@ -0,0 +1,213 @@
+package main
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"github.com/kelseyhightower/envconfig"
+	"log"
+	"net/http"
+	"time"
+)
+
+type Config struct {
+	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
+	ttl           time.Duration
+}
+
+func checkError(err error, msg string, args ...interface{}) {
+	if err != nil {
+		log.Fatalf(msg, args...)
+	}
+}
+
+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
+	}
+
+	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)
+	}
+
+	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 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
+}
+
+func main() {
+
+	var err error
+	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)
+
+	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:
+		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.
+	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.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
new file mode 100644
index 0000000..db14e31
--- /dev/null
+++ b/switchq/vendors.json
@@ -0,0 +1,12 @@
+[
+    {
+	"prefix" : "CC:37:AB",
+	"vendor" : "Edgecore Networks Corportation",
+	"provision" : true
+    },
+    {
+        "prefix" : "70:72:CF",
+        "vendor" : "Edgecore Networks Corportation",
+        "provision" : true
+    }
+]