CORD-270 CORD-444 added REST API to get list of switches and addded switches to MAAS

Change-Id: I0f1778b835fed947e19ace4ecff4900d72b405b6
diff --git a/API.md b/API.md
index b6f370e..34090e9 100644
--- a/API.md
+++ b/API.md
@@ -218,7 +218,9 @@
 |SWITCHQ_LOG_FORMAT|"text"|Format of the log messages|
 
 ### REST Resources
-None
+|URI|Operation|Description|
+|-|-|-|
+|/switch/|GET|returns a list of all known switches|
 
 ## Allocator
 **Docker image:** cord-ip-allocator
diff --git a/roles/maas/templates/automation-compose.yml.j2 b/roles/maas/templates/automation-compose.yml.j2
index 85a97c0..b2c0fb6 100644
--- a/roles/maas/templates/automation-compose.yml.j2
+++ b/roles/maas/templates/automation-compose.yml.j2
@@ -72,6 +72,8 @@
       - "SWITCHQ_ADDRESS_URL=file:///switchq/dhcp/dhcp_harvest.inc"
       - "SWITCHQ_LOG_LEVEL=debug"
       - "SWITCHQ_LOG_FORMAT=text"
+      - "SWITCHQ_MAAS_URL=http://{{ mgmt_ip_address.stdout }}/MAAS"
+      - "SWITCHQ_MAAS_API_KEY={{ apikey.stdout }}"
     volumes:
       - "/etc/bind/maas:/switchq/dhcp"
     restart: unless-stopped
diff --git a/switchq/Dockerfile b/switchq/Dockerfile
index e8d1602..49b4c8b 100644
--- a/switchq/Dockerfile
+++ b/switchq/Dockerfile
@@ -11,54 +11,21 @@
 ## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 ## See the License for the specific language governing permissions and
 ## limitations under the License.
-FROM ubuntu:14.04
+FROM golang:1.6-alpine
 MAINTAINER Open Networking Laboratory <info@onlab.us>
 
-# 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 apk --update add openssh-client git
 
 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
+ADD . /go/src/gerrit.opencord.com/maas/switchq
 
-WORKDIR $GOPATH/src/gerrit.opencord.com/maas/switchq
-RUN $GOPATH/bin/godep restore || true
+WORKDIR /go/src/gerrit.opencord.com/maas/switchq
+RUN /go/bin/godep restore || true
 
-WORKDIR $GOPATH
+WORKDIR /go
 RUN go install gerrit.opencord.com/maas/switchq
 
 LABEL org.label-schema.name="switchq" \
diff --git a/switchq/Godeps/Godeps.json b/switchq/Godeps/Godeps.json
index cc3867f..a178cff 100644
--- a/switchq/Godeps/Godeps.json
+++ b/switchq/Godeps/Godeps.json
@@ -4,13 +4,32 @@
 	"GodepVersion": "v72",
 	"Deps": [
 		{
+			"ImportPath": "github.com/juju/gomaasapi",
+			"Rev": "e173bc8d8d3304ff11b0ded5f6d4eea0cb560a40"
+		},
+		{
+			"ImportPath": "gopkg.in/mgo.v2/bson",
+			"Comment": "r2015.12.06-2-g03c9f3e",
+			"Rev": "03c9f3ee4c14c8e51ee521a6a7d0425658dd6f64"
+		},
+		{
+			"ImportPath": "github.com/gorilla/context",
+			"Comment": "v1.1-4-gaed02d1",
+			"Rev": "aed02d124ae4a0e94fea4541c8effd05bf0c8296"
+		},
+		{
+			"ImportPath": "github.com/gorilla/mux",
+			"Comment": "v1.1-13-g9fa818a",
+			"Rev": "9fa818a44c2bf1396a17f9d5a3c0f6dd39d2ff8e"
+		},
+		{
 			"ImportPath": "github.com/kelseyhightower/envconfig",
 			"Comment": "1.1.0-17-g91921eb",
 			"Rev": "91921eb4cf999321cdbeebdba5a03555800d493b"
 		},
-                {
-                        "ImportPath": "github.com/Sirupsen/logrus",
-                        "Rev": "f3cfb454f4c209e6668c95216c4744b8fddb2356"
-                }
+		{
+			"ImportPath": "github.com/Sirupsen/logrus",
+			"Rev": "f3cfb454f4c209e6668c95216c4744b8fddb2356"
+		}
 	]
 }
diff --git a/switchq/address.go b/switchq/address.go
index 8354f59..081afe8 100644
--- a/switchq/address.go
+++ b/switchq/address.go
@@ -36,9 +36,9 @@
 }
 
 type AddressRec struct {
-	Name string
-	IP   string
-	MAC  string
+	Name string `json:"name"`
+	IP   string `json:"ip"`
+	MAC  string `json:"mac"`
 }
 
 type AddressSource interface {
diff --git a/switchq/handlers.go b/switchq/handlers.go
new file mode 100644
index 0000000..3e302bd
--- /dev/null
+++ b/switchq/handlers.go
@@ -0,0 +1,32 @@
+// Copyright 2016 Open Networking Laboratory
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package main
+
+import (
+	"encoding/json"
+	"net/http"
+)
+
+func (c *AppContext) ListSwitchesHandler(w http.ResponseWriter, r *http.Request) {
+	log.Info("Request received for switch list")
+	c.mutex.RLock()
+	bytes, err := json.Marshal(c.publishList)
+	c.mutex.RUnlock()
+
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+	w.Write(bytes)
+}
diff --git a/switchq/ssh-config b/switchq/ssh-config
deleted file mode 100644
index 990a43d..0000000
--- a/switchq/ssh-config
+++ /dev/null
@@ -1,3 +0,0 @@
-Host *
-   StrictHostKeyChecking no
-   UserKnownHostsFile=/dev/null
diff --git a/switchq/switchq.go b/switchq/switchq.go
index d637b74..deddf74 100644
--- a/switchq/switchq.go
+++ b/switchq/switchq.go
@@ -18,8 +18,11 @@
 	"encoding/json"
 	"fmt"
 	"github.com/Sirupsen/logrus"
+	"github.com/gorilla/mux"
+	maas "github.com/juju/gomaasapi"
 	"github.com/kelseyhightower/envconfig"
 	"net/http"
+	"sync"
 	"time"
 )
 
@@ -34,6 +37,10 @@
 	Script          string `default:"do-ansible"`
 	LogLevel        string `default:"warning" envconfig:"LOG_LEVEL"`
 	LogFormat       string `default:"text" envconfig:"LOG_FORMAT"`
+	Listen          string `default:""`
+	Port            int    `default:"4244"`
+	MaasURL         string `default:"http://localhost/MAAS" envconfig:"MAAS_URL"`
+	MaasKey         string `default:"" envconfig:"MAAS_API_KEY"`
 
 	vendors       Vendors
 	addressSource AddressSource
@@ -74,16 +81,31 @@
 	Timestamp int64        `json:"timestamp"`
 }
 
+type AppContext struct {
+	config Config
+
+	maasClient  *maas.MAASObject
+	pushChan    chan []AddressRec
+	mutex       sync.RWMutex
+	nextList    []AddressRec
+	publishList []AddressRec
+}
+
 func checkError(err error, msg string, args ...interface{}) {
 	if err != nil {
 		log.Fatalf(msg, args...)
 	}
 }
 
-func (c *Config) getProvisionedState(rec AddressRec) (*StatusMsg, error) {
+func (c *AppContext) getProvisionedState(rec AddressRec) (*StatusMsg, error) {
+	if len(c.config.ProvisionURL) == 0 {
+		log.Warnf("Unable to fetch provisioning state of device '%s' (%s, %s) as no URL for the provisioner was specified",
+			rec.Name, rec.IP, rec.MAC)
+		return nil, fmt.Errorf("No URL for provisioner specified")
+	}
 	log.Debugf("Fetching provisioned state of device '%s' (%s, %s)",
 		rec.Name, rec.IP, rec.MAC)
-	resp, err := http.Get(c.ProvisionURL + rec.MAC)
+	resp, err := http.Get(c.config.ProvisionURL + rec.MAC)
 	if err != nil {
 		log.Errorf("Error while retrieving provisioning state for device '%s (%s, %s)' : %s",
 			rec.Name, rec.IP, rec.MAC, err)
@@ -112,22 +134,27 @@
 	return nil, nil
 }
 
-func (c *Config) provision(rec AddressRec) error {
-	log.Infof("POSTing to '%s' for provisioning of '%s (%s)'", c.ProvisionURL, rec.Name, rec.MAC)
+func (c *AppContext) provision(rec AddressRec) error {
+	if len(c.config.ProvisionURL) == 0 {
+		log.Warnf("Unable to POST to provisioner for device '%s' (%s, %s) as no URL for the provisioner was specified",
+			rec.Name, rec.IP, rec.MAC)
+		return fmt.Errorf("No URL for provisioner specified")
+	}
+	log.Infof("POSTing to '%s' for provisioning of '%s (%s)'", c.config.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.config.RoleSelectorURL != "" {
+		data["role_selector"] = c.config.RoleSelectorURL
 	}
-	if c.DefaultRole != "" {
-		data["role"] = c.DefaultRole
+	if c.config.DefaultRole != "" {
+		data["role"] = c.config.DefaultRole
 	}
-	if c.Script != "" {
-		data["script"] = c.Script
+	if c.config.Script != "" {
+		data["script"] = c.config.Script
 	}
 
 	hc := http.Client{}
@@ -137,7 +164,7 @@
 		log.Errorf("Unable to marshal provisioning data : %s", err)
 		return err
 	}
-	req, err := http.NewRequest("POST", c.ProvisionURL, bytes.NewReader(b))
+	req, err := http.NewRequest("POST", c.config.ProvisionURL, bytes.NewReader(b))
 	if err != nil {
 		log.Errorf("Unable to construct POST request to provisioner : %s", err)
 		return err
@@ -159,8 +186,8 @@
 	return nil
 }
 
-func (c *Config) processRecord(rec AddressRec) error {
-	ok, err := c.vendors.Switchq(rec.MAC)
+func (c *AppContext) processRecord(rec AddressRec) error {
+	ok, err := c.config.vendors.Switchq(rec.MAC)
 	if err != nil {
 		return fmt.Errorf("unable to determine ventor of MAC '%s' (%s)", rec.MAC, err)
 	}
@@ -172,6 +199,9 @@
 		return nil
 	}
 
+	// Add this IP information to our list of known switches
+	c.nextList = append(c.nextList, rec)
+
 	// Verify if the provision status of the node is complete, if in an error state then TTL means
 	// nothing
 	state, err := c.getProvisionedState(rec)
@@ -199,34 +229,64 @@
 	}
 
 	// If TTL is 0 then we will only provision a switch once.
-	if state == nil || (c.ttl > 0 && time.Since(time.Unix(state.Timestamp, 0)) > c.ttl) {
+	if state == nil || (c.config.ttl > 0 && time.Since(time.Unix(state.Timestamp, 0)) > c.config.ttl) {
 		if state != nil {
 			log.Debugf("device '%s' (%s, %s) TTL expired, reprovisioning",
 				rec.Name, rec.IP, rec.MAC)
 		}
 		c.provision(rec)
-	} else if c.ttl == 0 {
+	} else if c.config.ttl == 0 {
 		log.Debugf("device '%s' (%s, %s) has completed its one time provisioning, with a TTL set to %s",
-			rec.Name, rec.IP, rec.MAC, c.ProvisionTTL)
+			rec.Name, rec.IP, rec.MAC, c.config.ProvisionTTL)
 	} else {
 		log.Debugf("device '%s' (%s, %s) has completed provisioning within the specified TTL of %s",
-			rec.Name, rec.IP, rec.MAC, c.ProvisionTTL)
+			rec.Name, rec.IP, rec.MAC, c.config.ProvisionTTL)
 	}
 	return nil
 }
 
+func (c *AppContext) processLoop() {
+	// 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.Infof("Checking for switches @ %s", time.Now())
+		addresses, err := c.config.addressSource.GetAddresses()
+
+		if err != nil {
+			log.Errorf("unable to read addresses from address source : %s", err)
+		} else {
+			log.Infof("Queried %d addresses from address source", len(addresses))
+
+			c.nextList = make([]AddressRec, 0, len(addresses))
+			for _, rec := range addresses {
+				log.Debugf("Processing %s(%s, %s)", rec.Name, rec.IP, rec.MAC)
+				if err := c.processRecord(rec); err != nil {
+					log.Errorf("Error when processing IP '%s' : %s", rec.IP, err)
+				}
+			}
+			c.mutex.Lock()
+			c.publishList = c.nextList
+			c.nextList = nil
+			c.mutex.Unlock()
+			c.pushChan <- c.publishList
+		}
+
+		time.Sleep(c.config.interval)
+	}
+}
+
 var log = logrus.New()
 
 func main() {
 
 	var err error
-	config := Config{}
-	err = envconfig.Process("SWITCHQ", &config)
+	context := &AppContext{}
+	err = envconfig.Process("SWITCHQ", &context.config)
 	if err != nil {
 		log.Fatalf("Unable to parse configuration options : %s", err)
 	}
 
-	switch config.LogFormat {
+	switch context.config.LogFormat {
 	case "json":
 		log.Formatter = &logrus.JSONFormatter{}
 	default:
@@ -236,24 +296,12 @@
 		}
 	}
 
-	level, err := logrus.ParseLevel(config.LogLevel)
+	level, err := logrus.ParseLevel(context.config.LogLevel)
 	if err != nil {
 		level = logrus.WarnLevel
 	}
 	log.Level = level
 
-	config.vendors, err = NewVendors(config.VendorsURL)
-	checkError(err, "Unable to create known vendors list from specified URL '%s' : %s", config.VendorsURL, 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.Infof(`Configuration:
 		Vendors URL:       %s
 		Poll Interval:     %s
@@ -263,31 +311,50 @@
 		Role Selector URL: %s
 		Default Role:      %s
 		Script:            %s
+		API Listen IP:     %s
+		API Listen Port:   %d
+		MAAS URL:          %s
+		MAAS APIKEY:       %s
 		Log Level:         %s
 		Log Format:        %s`,
-		config.VendorsURL, config.PollInterval, config.AddressURL, config.ProvisionTTL,
-		config.ProvisionURL, config.RoleSelectorURL, config.DefaultRole, config.Script,
-		config.LogLevel, config.LogFormat)
+		context.config.VendorsURL, context.config.PollInterval, context.config.AddressURL, context.config.ProvisionTTL,
+		context.config.ProvisionURL, context.config.RoleSelectorURL, context.config.DefaultRole, context.config.Script,
+		context.config.Listen, context.config.Port, context.config.MaasURL, context.config.MaasKey,
+		context.config.LogLevel, context.config.LogFormat)
 
-	// 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.Infof("Checking for switches @ %s", time.Now())
-		addresses, err := config.addressSource.GetAddresses()
+	context.config.vendors, err = NewVendors(context.config.VendorsURL)
+	checkError(err, "Unable to create known vendors list from specified URL '%s' : %s", context.config.VendorsURL, err)
 
-		if err != nil {
-			log.Errorf("unable to read addresses from address source : %s", err)
-		} else {
-			log.Infof("Queried %d addresses from address source", len(addresses))
+	context.config.addressSource, err = NewAddressSource(context.config.AddressURL)
+	checkError(err, "Unable to create required address source for specified URL '%s' : %s", context.config.AddressURL, err)
 
-			for _, rec := range addresses {
-				log.Debugf("Processing %s(%s, %s)", rec.Name, rec.IP, rec.MAC)
-				if err := config.processRecord(rec); err != nil {
-					log.Errorf("Error when processing IP '%s' : %s", rec.IP, err)
-				}
-			}
-		}
+	context.config.interval, err = time.ParseDuration(context.config.PollInterval)
+	checkError(err, "Unable to parse specified poll interface '%s' : %s", context.config.PollInterval, err)
 
-		time.Sleep(config.interval)
+	context.config.ttl, err = time.ParseDuration(context.config.ProvisionTTL)
+	checkError(err, "Unable to parse specified provision TTL value of '%s' : %s", context.config.ProvisionTTL, err)
+
+	if len(context.config.MaasURL) > 0 {
+
+		// Attempt to connect to MAAS
+		authClient, err := maas.NewAuthenticatedClient(context.config.MaasURL, context.config.MaasKey, "1.0")
+		checkError(err, "Unable to connect to MAAS at '%s' : %s", context.config.MaasURL, err)
+
+		context.maasClient = maas.NewMAAS(*authClient)
+	}
+
+	context.pushChan = make(chan []AddressRec, 1)
+
+	go context.processLoop()
+	go context.syncToMaas(context.pushChan)
+
+	router := mux.NewRouter()
+	router.HandleFunc("/switch/", context.ListSwitchesHandler).Methods("GET")
+	http.Handle("/", router)
+	log.Infof("Listening for HTTP request on '%s:%d'", context.config.Listen, context.config.Port)
+	err = http.ListenAndServe(fmt.Sprintf("%s:%d", context.config.Listen, context.config.Port), nil)
+	if err != nil {
+		checkError(err, "Error while attempting to listen to REST requests on '%s:%d' : %s",
+			context.config.Listen, context.config.Port, err)
 	}
 }
diff --git a/switchq/sync.go b/switchq/sync.go
new file mode 100644
index 0000000..64b0d43
--- /dev/null
+++ b/switchq/sync.go
@@ -0,0 +1,292 @@
+// Copyright 2016 Open Networking Laboratory
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package main
+
+import (
+	"fmt"
+	maas "github.com/juju/gomaasapi"
+	"net"
+	"net/url"
+	"strconv"
+	"strings"
+)
+
+type MatchRec struct {
+	AddressRec
+	ResourceUri string
+}
+
+type subnetRec struct {
+	name   string
+	cidr   *net.IPNet
+	vlanID string
+}
+
+func getSubnetForAddr(subnets []subnetRec, ip string) *subnetRec {
+	asIp := net.ParseIP(ip)
+	for _, rec := range subnets {
+		if rec.cidr.Contains(asIp) {
+			return &rec
+		}
+	}
+	return nil
+}
+
+// process converts the device list from MAAS into a maps to quickly lookup a record by name
+func process(deviceList []maas.JSONObject) (byName map[string]*MatchRec, byMac map[string]*MatchRec, err error) {
+	byName = make(map[string]*MatchRec)
+	byMac = make(map[string]*MatchRec)
+	all := make([]MatchRec, len(deviceList))
+
+	for i, deviceObj := range deviceList {
+		device, err := deviceObj.GetMap()
+		if err != nil {
+			return nil, nil, err
+		}
+
+		uri, err := device["resource_uri"].GetString()
+		if err != nil {
+			return nil, nil, err
+		}
+		all[i].ResourceUri = uri
+
+		name, err := device["hostname"].GetString()
+		if err != nil {
+			return nil, nil, err
+		}
+
+		// Strip the domain from the hostname
+		idx := strings.Index(name, ".")
+		if idx != -1 {
+			name = name[:idx]
+		}
+		all[i].Name = name
+
+		mac_set_arr, err := device["macaddress_set"].GetArray()
+		if len(mac_set_arr) != 1 {
+			return nil, nil, fmt.Errorf("Expecting a single MAC address, recived %d", len(mac_set_arr))
+		}
+
+		mac_obj, err := mac_set_arr[0].GetMap()
+		if err != nil {
+			return nil, nil, err
+		}
+
+		mac, err := mac_obj["mac_address"].GetString()
+		if err != nil {
+			return nil, nil, err
+		}
+		mac = strings.ToUpper(mac)
+		all[i].MAC = mac
+
+		byName[name] = &all[i]
+		byMac[mac] = &all[i]
+	}
+
+	return
+}
+
+// synctoMaas checks to see if the devices is already in MAAS and if not adds it containment is determined by a matching
+// hostname and MAC address. if there is not match then a new devie is POSTed to MAAS
+func (c *AppContext) syncToMaas(request chan []AddressRec) {
+	log.Info("Starting MAAS Switch Synchronizer")
+
+	// Wait for request
+	for list := range request {
+		// Get current device list and convert it to some maps for quick indexing
+		devices := c.maasClient.GetSubObject("devices")
+		deviceObjects, err := devices.CallGet("list", url.Values{})
+		if err != nil {
+			log.Errorf("Unable to synchronize switches to MAAS, unable to get current devices : %s",
+				err)
+			break
+		}
+		deviceList, err := deviceObjects.GetArray()
+		if err != nil {
+			log.Errorf("Unable to synchronize switches to MAAS, unable to deserialize devices : %s",
+				err)
+			break
+		}
+		byName, byMac, err := process(deviceList)
+		if err != nil {
+			log.Errorf("Unable to process current device list : %s", err)
+			return
+		}
+
+		// Get all the subnets from MAAS and store them in a local array for quick access. The subnets
+		// are used to attempt to map the switch into a subnet based on its IP address
+		subnets := c.maasClient.GetSubObject("subnets")
+		subnetObjects, err := subnets.CallGet("", url.Values{})
+		if err != nil {
+			log.Errorf("Unable to retrieve subnets from MAAS : %s", err)
+			return
+		}
+
+		subnetArr, err := subnetObjects.GetArray()
+		if err != nil {
+			log.Errorf("Unable to get subnet array from MAAS : %s", err)
+			return
+		}
+
+		subnetRecs := make([]subnetRec, len(subnetArr))
+		for i, subnetObj := range subnetArr {
+			subnet, err := subnetObj.GetMap()
+			if err != nil {
+				log.Errorf("Unable to process subnet from MAAS : %s", err)
+				return
+			}
+
+			name, err := subnet["name"].GetString()
+			if err != nil {
+				log.Errorf("Unable to get Name from MAAS subnet : %s", err)
+				return
+			}
+
+			s_cidr, err := subnet["cidr"].GetString()
+			if err != nil {
+				log.Errorf("Unable to get CIDR from MAAS subnet : %s", err)
+				return
+			}
+			_, cidr, err := net.ParseCIDR(s_cidr)
+			if err != nil {
+				log.Errorf("Unable to parse CIDR '%s' from MAAS : %s", s_cidr, err)
+				return
+			}
+
+			vlanMap, err := subnet["vlan"].GetMap()
+			if err != nil {
+				log.Errorf("Unable to get vlan for MAAS subnet '%s' : %s", s_cidr, err)
+				return
+			}
+
+			id, err := vlanMap["id"].GetFloat64()
+			if err != nil {
+				log.Errorf("Unable to get VLAN ID for MAAS subnet '%s' : %s", s_cidr, err)
+				return
+			}
+			subnetRecs[i].name = name
+			subnetRecs[i].cidr = cidr
+			subnetRecs[i].vlanID = strconv.Itoa(int(id))
+		}
+
+		// Iterage over the list of devices to sync to MAAS
+		for _, rec := range list {
+			// First check for matching hostname
+			found, ok := byName[rec.Name]
+			if ok {
+				// Found an existing record with a matching hostname. If the MAC matches then
+				// this means this device is already in MAAS and we are good.
+				if strings.ToUpper(rec.MAC) == found.MAC {
+					// All is good
+					log.Infof("Device '%s (%s)' already in MAAS", rec.Name, rec.MAC)
+					continue
+				} else {
+					// Have a matching hostname, but a different MAC address. Can't
+					// push a duplicate hostname to MAAS. As the MAC is considered the
+					// unique identifier we will append the MAC address to the given
+					// hostname and add the device under that host name
+					log.Warnf("Device '%s (%s)' exists in MAAS with a different MAC, augmenting hostname with MAC to form unique hostname",
+						rec.Name, rec.MAC)
+					namePlus := rec.Name + "-" + strings.Replace(strings.ToLower(rec.MAC), ":", "", -1)
+					_, ok = byName[namePlus]
+					if ok {
+						// A record with the name + mac already exists, assume this is the
+						// same record then and all is well
+						log.Infof("Device '%s (%s)' already in MAAS", namePlus, rec.MAC)
+						continue
+					}
+
+					// Modify the record so that it will be created with the new name
+					rec.Name = namePlus
+				}
+			}
+			found, ok = byMac[strings.ToUpper(rec.MAC)]
+			if ok {
+				// Found a record with this MAC address, but a different hostname. Update
+				// the hostname to the correct value
+				log.Infof("Device with matching MAC, but different name found, updating name to '%s (%s)'",
+					rec.Name, rec.MAC)
+				deviceObj := c.maasClient.GetSubObject(found.ResourceUri)
+				_, err := deviceObj.Update(url.Values{
+					"hostname": []string{rec.Name},
+				})
+				if err != nil {
+					log.Errorf("Unable to update hostname for device '%s (%s)' in MAAS : %s",
+						rec.Name, rec.MAC, err)
+				}
+				continue
+			}
+
+			// The device does not currently exist in MAAS, so add it
+			log.Infof("Adding device '%s (%s)' to MAAS", rec.Name, rec.MAC)
+			deviceObj, err := devices.CallPost("new", url.Values{
+				"hostname":      []string{rec.Name},
+				"mac_addresses": []string{rec.MAC},
+			})
+			if err != nil {
+				log.Errorf("Unable to synchronize switch '%s' (%s, %s) to MAAS : %s",
+					rec.Name, rec.IP, rec.MAC, err)
+				continue
+			}
+
+			// Get the interface of the device so if can be assigned to a subnet
+			deviceMap, err := deviceObj.GetMap()
+			if err != nil {
+				log.Errorf("Can't get device object for '%s (%s)' : %s",
+					rec.Name, rec.MAC, err)
+				continue
+			}
+
+			interfaceSetArr, err := deviceMap["interface_set"].GetArray()
+			if err != nil {
+				log.Errorf("Can't get device interface set for '%s (%s)' : %s",
+					rec.Name, rec.MAC, err)
+				continue
+			}
+
+			ifaceMap, err := interfaceSetArr[0].GetMap()
+			if err != nil {
+				log.Errorf("Unable to get first interface for '%s (%s)' : %s",
+					rec.Name, rec.MAC, err)
+				continue
+			}
+
+			ifaceUri, err := ifaceMap["resource_uri"].GetString()
+			if err != nil {
+				log.Errorf("Unable to get interface URI for '%s (%s)' : %s",
+					rec.Name, rec.MAC, err)
+				continue
+			}
+
+			// Get the appropriate subnect for the switches IP. If one cannot be found then
+			// nothing can be done
+			subnetRec := getSubnetForAddr(subnetRecs, rec.IP)
+			if subnetRec == nil {
+				log.Errorf("Unable to find VLAN ID for '%s (%s)' using IP '%s'",
+					rec.Name, rec.MAC, rec.IP)
+				continue
+			}
+
+			// If we have a subnet for the device set it back to MAAS
+			_, err = c.maasClient.GetSubObject(ifaceUri).Update(url.Values{
+				"name": []string{"ma1"},
+				"vlan": []string{subnetRec.vlanID},
+			})
+			if err != nil {
+				log.Errorf("Unable to update interface name of '%s (%s)' : %s",
+					rec.Name, rec.MAC, err)
+			}
+		}
+	}
+}