CORD-313 refactor configuration generator

Change-Id: I4428ff0b67ee8d6ebb9b7009cd82413416c25a84
diff --git a/config-generator/configGen.go b/config-generator/configGen.go
index b0460f0..c23ce53 100644
--- a/config-generator/configGen.go
+++ b/config-generator/configGen.go
@@ -14,83 +14,29 @@
 package main
 
 import (
-	"encoding/json"
-	"errors"
 	"fmt"
+	"net/http"
+
+	"github.com/Sirupsen/logrus"
+
 	"github.com/gorilla/mux"
 	"github.com/kelseyhightower/envconfig"
-	"io/ioutil"
-	"log"
-	"net/http"
-	"os"
-	"strings"
-	"text/template"
 )
 
-var unmarshalError = errors.New("Error Unmarshaling the JSON Data\n")
-
 type Config struct {
-	Port             string `default:"8181"`
-	IP               string `default:"127.0.0.1"`
-	SwitchCount      int    `default:"0"`
-	HostCount        int    `default:"0"`
-	Username         string `default:"karaf"`
-	Password         string `default:"karaf"`
-	LogLevel         string `default:"warning" envconfig:"LOG_LEVEL"`
-	LogFormat        string `default:"text" envconfig:"LOG_FORMAT"`
-	ConfigServerPort string `default:"1337"`
-	ConfigServerIP   string `default:"127.0.0.1"`
-}
+	Port       int    `default:"1337"`
+	Listen     string `default:"0.0.0.0"`
+	Controller string `default:"http://%s:%s@127.0.0.1:8181"`
+	Username   string `default:"karaf"`
+	Password   string `default:"karaf"`
+	LogLevel   string `default:"warning" envconfig:"LOG_LEVEL"`
+	LogFormat  string `default:"text" envconfig:"LOG_FORMAT"`
 
-type hosts struct {
-	Host []struct {
-		Mac         string   `json:"mac"`
-		IpAddresses []string `json:"ipAddresses"`
-		Location    struct {
-			ElementID string `json:"elementId`
-			Port      string `json:"port"`
-		} `json:"location"`
-		Comma   string
-		Gateway string
-	} `json:"hosts"`
-}
-
-type devices struct {
-	Device []struct {
-		Id          string `json:"id"`
-		ChassisId   string `json:"chassisId"`
-		Annotations struct {
-			ManagementAddress string `json:"managementAddress"`
-		} `json:"annotations"`
-		Comma string `default:","`
-	} `json:"devices"`
-}
-
-type onosLinks struct {
-	Links []struct {
-		Src struct {
-			Port   string `json:"port"`
-			Device string `json:"device"`
-		} `json:"src"`
-		Dst struct {
-			Port   string `json:"port"`
-			Device string `json:"device"`
-		} `json:"dst"`
-	} `json:"links"`
-}
-
-type linkStructJSON struct {
-	Val   string
-	Comma string
-}
-
-type ConfigParam struct {
-	SwitchCount int    `json:"switchcount"`
-	HostCount   int    `json:"hostcount"`
-	ONOSIP      string `json:"onosip"`
+	connect string
 }
 
 var c Config
+var log = logrus.New()
 
 func main() {
 
@@ -99,207 +45,39 @@
 		log.Fatalf("[ERROR] Unable to parse configuration options : %s", err)
 	}
 
+	switch c.LogFormat {
+	case "json":
+		log.Formatter = &logrus.JSONFormatter{}
+	default:
+		log.Formatter = &logrus.TextFormatter{
+			FullTimestamp: true,
+			ForceColors:   true,
+		}
+	}
+
+	level, err := logrus.ParseLevel(c.LogLevel)
+	if err != nil {
+		level = logrus.WarnLevel
+	}
+	log.Level = level
+
+	log.Infof(`Configuration:
+        LISTEN:     %s
+        PORT:       %d
+        CONTROLLER: %s
+        USERNAME:   %s
+        PASSWORD:   %s
+        LOG_LEVEL:  %s
+        LOG_FORMAT: %s`,
+		c.Listen, c.Port, c.Controller,
+		c.Username, c.Password,
+		c.LogLevel, c.LogFormat)
+
 	router := mux.NewRouter()
-	router.HandleFunc("/config/", ConfigGenHandler).Methods("POST")
+	router.HandleFunc("/config/", c.configGenHandler).Methods("POST")
 	http.Handle("/", router)
 
-	fmt.Println("Config Generator server listening at: " + c.ConfigServerIP + ":" + c.ConfigServerPort)
+	c.connect = fmt.Sprintf(c.Controller, c.Username, c.Password)
 
-	http.ListenAndServe(c.ConfigServerIP+":"+c.ConfigServerPort, nil)
-
-}
-
-func ConfigGenHandler(w http.ResponseWriter, r *http.Request) {
-	var para ConfigParam
-
-	decoder := json.NewDecoder(r.Body)
-	defer r.Body.Close()
-	if err := decoder.Decode(&para); err != nil {
-		fmt.Errorf("Unable to decode request to provision : %s", err)
-		http.Error(w, err.Error(), http.StatusBadRequest)
-		return
-	}
-
-	c.HostCount = para.HostCount
-	c.SwitchCount = para.SwitchCount
-	c.IP = para.ONOSIP
-
-	onos := "http://" + c.Username + ":" + c.Password + "@" + c.IP + ":" + c.Port
-
-	err := os.Remove("network-cfg.json")
-	if err != nil {
-		log.Println("Warning: no file called network-cfg.json (ignore if this is the first run)")
-	}
-	err = generateDevicesJSON(onos)
-	if err != nil {
-		w.WriteHeader(http.StatusInternalServerError)
-		fmt.Fprintf(w, err.Error())
-		return
-	}
-	err = generateLinkJSON(onos)
-	if err != nil {
-		w.WriteHeader(http.StatusInternalServerError)
-		fmt.Fprintf(w, err.Error())
-		return
-	}
-	err = generateHostJSON(onos)
-	if err != nil {
-		w.WriteHeader(http.StatusInternalServerError)
-		fmt.Fprintf(w, err.Error())
-		return
-	}
-
-	fmt.Println("Config file generated: network-cfg.json")
-
-	data, err := ioutil.ReadFile("network-cfg.json")
-	check(err)
-
-	w.WriteHeader(http.StatusAccepted)
-	fmt.Fprintf(w, string(data))
-
-}
-
-func writeToFile(object interface{}, t string) {
-	f, err := os.OpenFile("network-cfg.json", os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600)
-	if err != nil {
-		panic(err)
-	}
-
-	defer f.Close()
-
-	tpl, err := template.ParseFiles(t)
-	check(err)
-	err = tpl.Execute(f, object)
-	check(err)
-}
-
-func generateDevicesJSON(onos string) error {
-	ds, err := getData(onos + "/onos/v1/devices")
-	if err != nil {
-		return err
-	}
-
-	var d devices
-	err = json.Unmarshal(ds, &d)
-	if err != nil {
-		return unmarshalError
-	}
-
-	if len(d.Device) != c.SwitchCount {
-		_ = os.Remove("network-cfg.json")
-		log.Println("[INFO] Cleaning up unfinished config file")
-		e := fmt.Sprintf("[ERROR] Number of switches configured don't match actual switches connected to the controller. Configured: %d, connected: %d", c.SwitchCount, len(d.Device))
-		log.Println(e)
-		return errors.New(e)
-	}
-
-	for k, _ := range d.Device {
-		d.Device[k].Comma = ","
-		if k >= len(d.Device)-1 {
-			d.Device[k].Comma = ""
-		}
-	}
-
-	writeToFile(d.Device, "devices.tpl")
-	return nil
-
-}
-
-func generateHostJSON(onos string) error {
-	hs, err := getData(onos + "/onos/v1/hosts")
-	if err != nil {
-		return err
-	}
-	var h hosts
-	err = json.Unmarshal(hs, &h)
-	if err != nil {
-		return unmarshalError
-	}
-
-	if len(h.Host) != c.HostCount {
-		_ = os.Remove("network-cfg.json")
-		log.Println("[INFO] Cleaning up unfinished config file")
-		e := fmt.Sprintf("[ERROR] Number of hosts configured don't match actual hosts visible to the controller. Configured: %d, connected: %d", c.HostCount, len(h.Host))
-		log.Println(e)
-		return errors.New(e)
-	}
-
-	for k, _ := range h.Host {
-
-		h.Host[k].Comma = ","
-		if k >= len(h.Host)-1 {
-			h.Host[k].Comma = ""
-		}
-
-		parts := strings.Split(h.Host[k].IpAddresses[0], ".")
-		ip := ""
-		for _, v := range parts[:len(parts)-1] {
-			ip = ip + v + "."
-		}
-		h.Host[k].Gateway = ip
-	}
-
-	writeToFile(h.Host, "ports.tpl")
-
-	writeToFile(h.Host, "hosts.tpl")
-	return nil
-
-}
-
-func generateLinkJSON(onos string) error {
-
-	links, err := getData(onos + "/onos/v1/links")
-	if err != nil {
-		return err
-	}
-
-	var l onosLinks
-	err = json.Unmarshal(links, &l)
-	if err != nil {
-		return unmarshalError
-	}
-
-	var in []linkStructJSON
-
-	for k, v := range l.Links {
-
-		comma := ","
-		val := fmt.Sprint(v.Src.Device + "/" + v.Src.Port + "-" + v.Dst.Device + "/" + v.Dst.Port)
-		if k >= len(l.Links)-1 {
-			comma = ""
-		}
-
-		tmp := linkStructJSON{val, comma}
-		in = append(in, tmp)
-
-	}
-
-	writeToFile(in, "links.tpl")
-
-	return nil
-
-}
-
-func getData(url string) ([]byte, error) {
-
-	resp, err := http.Get(url)
-	if err != nil {
-		return nil, errors.New("Error getting data from the URL\n")
-	}
-
-	defer resp.Body.Close()
-
-	body, err := ioutil.ReadAll(resp.Body)
-	if err != nil {
-		return nil, errors.New("Error reading data from response body\n")
-	}
-
-	return body, nil
-
-}
-
-func check(e error) {
-	if e != nil {
-		panic(e)
-	}
+	panic(http.ListenAndServe(fmt.Sprintf(":%d", c.Port), nil))
 }
diff --git a/config-generator/devices.tpl b/config-generator/devices.tpl
deleted file mode 100644
index 66ef4d9..0000000
--- a/config-generator/devices.tpl
+++ /dev/null
@@ -1,15 +0,0 @@
-{
- "devices" : {
-{{ range $index, $element := . }}
-    "{{ .Id}}" : {
-            "segmentrouting" : {
-                "name" : "device-{{ .ChassisId }}",
-                "nodeSid" : 10{{ $index }},
-                "routerIp" : "{{ .Annotations.ManagementAddress }}",
-                "routerMac" : "cc:37:ab:00:00:0{{ $index }}",
-                "isEdgeRouter" : true,
-                "adjacencySids" : []
-            }
-    }{{ .Comma }}{{ end }}
- },
-
diff --git a/config-generator/handlers.go b/config-generator/handlers.go
new file mode 100644
index 0000000..eabdbe3
--- /dev/null
+++ b/config-generator/handlers.go
@@ -0,0 +1,168 @@
+// 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 (
+	"bytes"
+	"encoding/json"
+	"net/http"
+	"strings"
+	"text/template"
+)
+
+type GenerationOptions struct {
+	SwitchCount int `json:"switchcount"`
+	HostCount   int `json:"hostcount"`
+}
+
+func (c *Config) configGenHandler(w http.ResponseWriter, r *http.Request) {
+	var options GenerationOptions
+
+	deviceMap := make(map[string]*onosDevice)
+
+	decoder := json.NewDecoder(r.Body)
+	defer r.Body.Close()
+	if err := decoder.Decode(&options); err != nil {
+		log.Errorf("Unable to decode provisioning request options: %s", err)
+		http.Error(w, err.Error(), http.StatusBadRequest)
+		return
+	}
+
+	var devices onosDevices
+	err := c.fetch("/onos/v1/devices", &devices)
+	if err != nil {
+		log.Errorf("Unable to retrieve device information from controller: %s", err)
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+
+	// If the request specified the number of switches, validate we have that
+	// exact number
+	if options.SwitchCount > 0 && len(devices.Devices) != options.SwitchCount {
+		log.Errorf("Expecting %d switch(es), found %d, no configuration generated",
+			options.SwitchCount, len(devices.Devices))
+		http.Error(w, "Expected switch count mismatch",
+			http.StatusInternalServerError)
+		return
+	}
+
+	for _, device := range devices.Devices {
+		deviceMap[device.Id] = device
+		device.Mac = splitString(device.ChassisId, 2, ":")
+	}
+
+	var hosts onosHosts
+	err = c.fetch("/onos/v1/hosts", &hosts)
+	if err != nil {
+		log.Errorf("Unable to retrieve host information from controller: %s", err)
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+
+	}
+
+	// If the request specified the number of hosts, validate we have that
+	// exact number
+	if options.HostCount > 0 && len(hosts.Hosts) != options.HostCount {
+		log.Errorf("Expecting %d host(s), found %d, no configuration generaged",
+			options.HostCount, len(hosts.Hosts))
+		http.Error(w, "Expected host count mismatch",
+			http.StatusInternalServerError)
+		return
+	}
+
+	// Use a simple heuristic to determine which switches are edge routers
+	// and which are not
+	markEdgeRouters(deviceMap, hosts)
+
+	// Generate the configuration file
+	cfg := onosConfig{
+		Devices: devices.Devices,
+		Hosts:   hosts.Hosts,
+	}
+
+	funcMap := template.FuncMap{
+		// The name "inc" is what the function will be called in the template text.
+		"add": func(a, b int) int {
+			return a + b
+		},
+		"gateway": func(ips []string) string {
+			if len(ips) > 0 {
+				parts := strings.Split(ips[0], ".")
+				ip := ""
+				for _, v := range parts[:len(parts)-1] {
+					ip = ip + v + "."
+				}
+				return ip + "254/24"
+			} else {
+				return "0.0.0.254/24"
+			}
+		},
+	}
+
+	tpl, err := template.New("netconfig.tpl").Funcs(funcMap).ParseFiles("netconfig.tpl")
+	if err != nil {
+		log.Errorf("Unable to parse template: %s", err)
+		http.Error(w, "Template parse error", http.StatusInternalServerError)
+		return
+	}
+
+	// Write template to buffer, so if there is an error we can return an
+	// http error
+	buf := new(bytes.Buffer)
+	err = tpl.Execute(buf, cfg)
+	if err != nil {
+		log.Errorf("Unexpected error while processing template: %s", err)
+		http.Error(w, "Template processing error", http.StatusInternalServerError)
+	}
+
+	w.Write(buf.Bytes())
+}
+
+// markEdgeRouters use hueristic to determine and mark switches that act
+// as edge routers
+func markEdgeRouters(dm map[string]*onosDevice, hosts onosHosts) {
+	// Walk the list of know compute nodes (hosts) and if the compute node
+	// is connected to a switch, then that switch is an edge router
+	for _, host := range hosts.Hosts {
+		if device, ok := dm[host.Location.ElementID]; ok {
+			(*device).IsEdgeRouter = true
+		}
+	}
+}
+
+// splitString used to convert a string to a psudeo MAC address by
+// splitting and separating it with a colon
+func splitString(src string, n int, sep string) string {
+	r := ""
+	for i, c := range src {
+		if i > 0 && i%n == 0 {
+			r += sep
+		}
+		r += string(c)
+	}
+	return r
+}
+
+// fetch fetch the specified data from ONOS
+func (c *Config) fetch(path string, data interface{}) error {
+	resp, err := http.Get(c.connect + path)
+	if err != nil {
+		return err
+	}
+
+	defer resp.Body.Close()
+	decoder := json.NewDecoder(resp.Body)
+	err = decoder.Decode(data)
+	return err
+}
diff --git a/config-generator/hosts.tpl b/config-generator/hosts.tpl
deleted file mode 100644
index 3fb102c..0000000
--- a/config-generator/hosts.tpl
+++ /dev/null
@@ -1,17 +0,0 @@
- "hosts" : {
-{{ range . }}
-	"{{ .Mac }}/-1" : {
-		"basic" : {
-			"ips" : ["{{ range $element := .IpAddresses }}{{ $element }}{{ end}}"],
-			"location" : "{{ .Location.ElementID }}/{{ .Location.Port }}"
-		}
-	}{{ .Comma }}{{ end }}
- },
- "apps" : {
-        "org.onosproject.core" : {
-            "core" : {
-                "linkDiscoveryMode" : "STRICT"
-            }
-        }
-    }
-}
\ No newline at end of file
diff --git a/config-generator/links.tpl b/config-generator/links.tpl
deleted file mode 100644
index 4f39412..0000000
--- a/config-generator/links.tpl
+++ /dev/null
@@ -1,7 +0,0 @@
- "links": {
-{{ range . }}
-    "{{ .Val }}": {
-         "basic": {}
-     }{{ .Comma }}{{ end }}
- },
-
diff --git a/config-generator/netconfig.tpl b/config-generator/netconfig.tpl
new file mode 100644
index 0000000..62d175f
--- /dev/null
+++ b/config-generator/netconfig.tpl
@@ -0,0 +1,34 @@
+{
+    "devices": {
+        {{ range $index, $element := .Devices }}{{ if $index }},
+        {{ end }}"{{ .Id }}": {
+            "segmentrouting": {
+                "name": "device-{{ .ChassisId }}",
+                "nodeSid": {{ add 100 $index }},
+                "routerIp": "{{ .Annotations.ManagementAddress }}",
+                "routerMac": "{{ .Mac }}",
+                "isEdgeRouter": {{ .IsEdgeRouter }},
+                "adjacencySids": []
+            }
+        }{{ end }}
+    },
+    "hosts": {
+        {{ range $index, $element := .Hosts }}{{ if $index }},
+        {{ end }}"{{ .Mac }}": {
+            "ips": ["{{ range $ip := .IpAddresses }}{{ $ip }}{{ end }}"],
+            "location": "{{ .Location.ElementID }}/{{ .Location.Port }}"
+        }{{ end }}
+    },
+    "ports": {
+        {{ range $index, $element := .Hosts }}{{ if $index }},
+        {{ end }}"{{ .Location.ElementID }}/{{ .Location.Port }}": {
+            "interfaces": [
+                {
+                    "ips": [ "{{ gateway .IpAddresses }}" ]
+                }
+            ]
+        }{{ end }}
+    },
+    "links": {},
+    "apps": {}
+}
diff --git a/config-generator/onos_types.go b/config-generator/onos_types.go
new file mode 100644
index 0000000..b5d3c59
--- /dev/null
+++ b/config-generator/onos_types.go
@@ -0,0 +1,63 @@
+// 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
+
+type onosHost struct {
+	Id          string   `json:"id"`
+	Mac         string   `json:"mac"`
+	IpAddresses []string `json:"ipAddresses"`
+	Location    struct {
+		ElementID string `json:"elementId`
+		Port      string `json:"port"`
+	} `json:"location"`
+}
+
+type onosHosts struct {
+	Hosts []*onosHost `json:"hosts"`
+}
+
+type onosDevice struct {
+	Id           string `json:"id"`
+	ChassisId    string `json:"chassisId"`
+	IsEdgeRouter bool   `json:"isEdgeRouter"`
+	Annotations  struct {
+		ManagementAddress string `json:"managementAddress"`
+	} `json:"annotations"`
+	Mac string `json:"-"`
+}
+
+type onosDevices struct {
+	Devices []*onosDevice `json:"devices"`
+}
+
+type onosLink struct {
+	Src struct {
+		Port   string `json:"port"`
+		Device string `json:"device"`
+	} `json:"src"`
+	Dst struct {
+		Port   string `json:"port"`
+		Device string `json:"device"`
+	} `json:"dst"`
+}
+
+type onosLinks struct {
+	Links []*onosLink `json:"links"`
+}
+
+type onosConfig struct {
+	Devices []*onosDevice
+	Hosts   []*onosHost
+	Links   []*onosLink
+}
diff --git a/config-generator/ports.tpl b/config-generator/ports.tpl
deleted file mode 100644
index 1252024..0000000
--- a/config-generator/ports.tpl
+++ /dev/null
@@ -1,11 +0,0 @@
-"ports": {
-{{ range . }}
-  "{{ .Location.ElementID }}/{{ .Location.Port }}" : {
-    	"interfaces": [
-    		{
-        		"ips" : [ "{{ .Gateway }}254/24" ]
-    		}
-    	]
-	}{{ .Comma }}{{ end }}
- },
-