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

Change-Id: I0f1778b835fed947e19ace4ecff4900d72b405b6
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)
 	}
 }