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)
+ }
+ }
+ }
+}