CORD-735 rewrite client hostnames that are invalid

Change-Id: I895019f3165fb7a05e350a34d305c2fd62b96df1
(cherry picked from commit 17911b4da7c47062d26b0d400ae613e322ebffae)
diff --git a/harvester/harvester.go b/harvester/harvester.go
index 9d47830..7e10599 100644
--- a/harvester/harvester.go
+++ b/harvester/harvester.go
@@ -19,7 +19,9 @@
 	"github.com/gorilla/mux"
 	"github.com/kelseyhightower/envconfig"
 	"net/http"
+	"regexp"
 	"strconv"
+	"strings"
 	"sync"
 	"text/template"
 	"time"
@@ -27,32 +29,36 @@
 
 // application application configuration and internal state
 type application struct {
-	Port           int           `default:"4246" desc:"port on which the service will listen for requests"`
-	Listen         string        `default:"0.0.0.0" desc:"IP on which the service will listen for requests"`
-	LogLevel       string        `default:"warning" envconfig:"LOG_LEVEL" desc:"log output level"`
-	LogFormat      string        `default:"text" envconfig:"LOG_FORMAT" desc:"format of log messages"`
-	DHCPLeaseFile  string        `default:"/harvester/dhcpd.leases" envconfig:"DHCP_LEASE_FILE" desc:"lease file to parse for lease information"`
-	OutputFile     string        `envconfig:"OUTPUT_FILE" desc:"name of file to output discovered lease in bind9 format"`
-	OutputFormat   string        `default:"{{.ClientHostname}}\tIN A {{.IPAddress}}\t; {{.HardwareAddress}}" envconfig:"OUTPUT_FORMAT" desc:"specifies the single entry format when outputing to a file"`
-	VerifyLeases   bool          `default:"true" envconfig:"VERIFY_LEASES" desc:"verifies leases with a ping"`
-	VerifyTimeout  time.Duration `default:"1s" envconfig:"VERIFY_TIMEOUT" desc:"max timeout (RTT) to wait for verification pings"`
-	VerifyWithUDP  bool          `default:"false" envconfig:"VERIFY_WITH_UDP" desc:"use UDP instead of raw sockets for ping verification"`
-	QueryPeriod    time.Duration `default:"30s" envconfig:"QUERY_PERIOD" desc:"period at which the DHCP lease file is processed"`
-	QuietPeriod    time.Duration `default:"2s" envconfing:"QUIET_PERIOD" desc:"period to wait between accepting parse requests"`
-	RequestTimeout time.Duration `default:"10s" envconfig:"REQUEST_TIMEOUT" desc:"period to wait for processing when requesting a DHCP lease database parsing"`
-	RNDCUpdate     bool          `default:"false" envconfig:"RNDC_UPDATE" desc:"determines if the harvester reloads the DNS servers after harvest"`
-	RNDCAddress    string        `default:"127.0.0.1" envconfig:"RNDC_ADDRESS" desc:"IP address of the DNS server to contact via RNDC"`
-	RNDCPort       int           `default:"954" envconfig:"RNDC_PORT" desc:"port of the DNS server to contact via RNDC"`
-	RNDCKeyFile    string        `default:"/key/rndc.conf.maas" envconfig:"RNDC_KEY_FILE" desc:"key file, with default, to contact DNS server"`
-	RNDCZone       string        `default:"cord.lab" envconfig:"RNDC_ZONE" desc:"zone to reload"`
+	Port               int           `default:"4246" desc:"port on which the service will listen for requests"`
+	Listen             string        `default:"0.0.0.0" desc:"IP on which the service will listen for requests"`
+	LogLevel           string        `default:"warning" envconfig:"LOG_LEVEL" desc:"log output level"`
+	LogFormat          string        `default:"text" envconfig:"LOG_FORMAT" desc:"format of log messages"`
+	DHCPLeaseFile      string        `default:"/harvester/dhcpd.leases" envconfig:"DHCP_LEASE_FILE" desc:"lease file to parse for lease information"`
+	OutputFile         string        `envconfig:"OUTPUT_FILE" desc:"name of file to output discovered lease in bind9 format"`
+	OutputFormat       string        `default:"{{.ClientHostname}}\tIN A {{.IPAddress}}\t; {{.HardwareAddress}}" envconfig:"OUTPUT_FORMAT" desc:"specifies the single entry format when outputing to a file"`
+	VerifyLeases       bool          `default:"true" envconfig:"VERIFY_LEASES" desc:"verifies leases with a ping"`
+	VerifyTimeout      time.Duration `default:"1s" envconfig:"VERIFY_TIMEOUT" desc:"max timeout (RTT) to wait for verification pings"`
+	VerifyWithUDP      bool          `default:"false" envconfig:"VERIFY_WITH_UDP" desc:"use UDP instead of raw sockets for ping verification"`
+	QueryPeriod        time.Duration `default:"30s" envconfig:"QUERY_PERIOD" desc:"period at which the DHCP lease file is processed"`
+	QuietPeriod        time.Duration `default:"2s" envconfing:"QUIET_PERIOD" desc:"period to wait between accepting parse requests"`
+	RequestTimeout     time.Duration `default:"10s" envconfig:"REQUEST_TIMEOUT" desc:"period to wait for processing when requesting a DHCP lease database parsing"`
+	RNDCUpdate         bool          `default:"false" envconfig:"RNDC_UPDATE" desc:"determines if the harvester reloads the DNS servers after harvest"`
+	RNDCAddress        string        `default:"127.0.0.1" envconfig:"RNDC_ADDRESS" desc:"IP address of the DNS server to contact via RNDC"`
+	RNDCPort           int           `default:"954" envconfig:"RNDC_PORT" desc:"port of the DNS server to contact via RNDC"`
+	RNDCKeyFile        string        `default:"/key/rndc.conf.maas" envconfig:"RNDC_KEY_FILE" desc:"key file, with default, to contact DNS server"`
+	RNDCZone           string        `default:"cord.lab" envconfig:"RNDC_ZONE" desc:"zone to reload"`
+	BadClientNames     []string      `default:"localhost" envconfig:"BAD_CLIENT_NAMES" desc:"list of invalid hostnames for clients"`
+	ClientNameTemplate string        `default:"UKN-{{with $x:=.HardwareAddress|print}}{{regex $x \":\" \"\"}}{{end}}" envconfig:"CLIENT_NAME_TEMPLATE" desc:"template for generated host name"`
 
-	log            *logrus.Logger     `ignored:"true"`
-	interchange    sync.RWMutex       `ignored:"true"`
-	leases         map[string]*Lease  `ignored:"true"`
-	byHardware     map[string]*Lease  `ignored:"true"`
-	byHostname     map[string]*Lease  `ignored:"true"`
-	outputTemplate *template.Template `ignored:"true"`
-	requests       chan *chan uint    `ignored:"true"`
+	log                *logrus.Logger     `ignored:"true"`
+	interchange        sync.RWMutex       `ignored:"true"`
+	leases             map[string]*Lease  `ignored:"true"`
+	byHardware         map[string]*Lease  `ignored:"true"`
+	byHostname         map[string]*Lease  `ignored:"true"`
+	outputTemplate     *template.Template `ignored:"true"`
+	requests           chan *chan uint    `ignored:"true"`
+	clientNameTemplate *template.Template `ignored:"true"`
+	badClientNames     map[string]bool    `ignored:"true"`
 }
 
 func main() {
@@ -89,30 +95,48 @@
 
 	// output the configuration
 	app.log.Infof(`Configuration:
-           LISTEN:          %s
-           PORT:            %d
-           LOG_LEVEL:       %s
-           LOG_FORMAT:      %s
-           DHCP_LEASE_FILE: %s
-           OUTPUT_FILE:     %s
-           OUTPUT_FORMAT:   %s
-           VERIFY_LEASES:   %t
-           VERIFY_TIMEOUT:  %s
-           VERIFY_WITH_UDP: %t
-           QUERY_PERIOD:    %s
-           QUIET_PERIOD:    %s
-           REQUEST_TIMEOUT: %s
-           RNDC_UPDATE:     %t
-           RNDC_ADDRESS:    %s
-           RNDC_PORT:       %d
-           RNDC_KEY_FILE:   %s
-           RNDC_ZONE:       %s`,
+           LISTEN:               %s
+           PORT:                 %d
+           LOG_LEVEL:            %s
+           LOG_FORMAT:           %s
+           DHCP_LEASE_FILE:      %s
+           OUTPUT_FILE:          %s
+           OUTPUT_FORMAT:        %s
+           VERIFY_LEASES:        %t
+           VERIFY_TIMEOUT:       %s
+           VERIFY_WITH_UDP:      %t
+           QUERY_PERIOD:         %s
+           QUIET_PERIOD:         %s
+           REQUEST_TIMEOUT:      %s
+           RNDC_UPDATE:          %t
+           RNDC_ADDRESS:         %s
+           RNDC_PORT:            %d
+           RNDC_KEY_FILE:        %s
+           RNDC_ZONE:            %s
+	   BAD_CLIENT_NAMES:     %s
+	   CLIENT_NAME_TEMPLATE: %s`,
 		app.Listen, app.Port,
 		app.LogLevel, app.LogFormat,
 		app.DHCPLeaseFile, app.OutputFile, strconv.Quote(app.OutputFormat),
 		app.VerifyLeases, app.VerifyTimeout, app.VerifyWithUDP,
 		app.QueryPeriod, app.QuietPeriod, app.RequestTimeout,
-		app.RNDCUpdate, app.RNDCAddress, app.RNDCPort, app.RNDCKeyFile, app.RNDCZone)
+		app.RNDCUpdate, app.RNDCAddress, app.RNDCPort, app.RNDCKeyFile, app.RNDCZone,
+		strings.Join(app.BadClientNames[:], ","), app.ClientNameTemplate)
+
+	app.clientNameTemplate, err = template.New("harvester").Funcs(template.FuncMap{
+		"regex": func(target, match, replace string) string {
+			re := regexp.MustCompile(match)
+			return re.ReplaceAllString(target, replace)
+		},
+	}).Parse(app.ClientNameTemplate)
+	if err != nil {
+		app.log.Fatalf("Unable to parse client host name template %s", err)
+	}
+
+	app.badClientNames = make(map[string]bool)
+	for _, bad := range app.BadClientNames {
+		app.badClientNames[bad] = true
+	}
 
 	// establish REST end points
 	router := mux.NewRouter()
diff --git a/harvester/parse.go b/harvester/parse.go
index d61e791..906b23c 100644
--- a/harvester/parse.go
+++ b/harvester/parse.go
@@ -15,6 +15,7 @@
 
 import (
 	"bufio"
+	"bytes"
 	"fmt"
 	"net"
 	"os"
@@ -39,8 +40,25 @@
 	bindFileFormat = "{{.ClientHostname}}\tIN A {{.IPAddress}}\t; {{.HardwareAddress}}"
 )
 
+// generateClientHostname generates a client name based on hardware address
+func (app *application) generateClientHostname(lease *Lease) string {
+	var buf bytes.Buffer
+
+	app.log.Debugf("Generating client-hostname for MAC '%s'", lease.HardwareAddress.String())
+
+	err := app.clientNameTemplate.Execute(&buf, lease)
+	if err != nil {
+		app.log.Errorf("Unable to generate client host name for lease with HW address '%s' : %s",
+			lease.HardwareAddress.String(), err)
+		return strings.ToUpper("UNK-" +
+			strings.Replace(lease.HardwareAddress.String(), ":", "", -1))
+	}
+
+	return buf.String()
+}
+
 // parseLease parses a single lease from the lease file
-func parseLease(scanner *bufio.Scanner, lease *Lease) error {
+func (app *application) parseLease(scanner *bufio.Scanner, lease *Lease) error {
 	var err error
 	for scanner.Scan() {
 		fields := strings.Fields(scanner.Text())
@@ -49,12 +67,16 @@
 			case "}":
 				// If no client-hostname was specified, generate one
 				if len(lease.ClientHostname) == 0 {
-					lease.ClientHostname = strings.ToUpper("UNK-" +
-						strings.Replace(lease.HardwareAddress.String(), ":", "", -1))
+					lease.ClientHostname = app.generateClientHostname(lease)
 				}
 				return nil
 			case "client-hostname":
 				lease.ClientHostname = strings.Trim(fields[1], "\";")
+
+				// Validate client-hostname
+				if _, ok := app.badClientNames[lease.ClientHostname]; ok {
+					lease.ClientHostname = app.generateClientHostname(lease)
+				}
 			case "hardware":
 				lease.HardwareAddress, err = net.ParseMAC(strings.Trim(fields[2], ";"))
 				if err != nil {
@@ -84,7 +106,7 @@
 }
 
 // parseLeaseFile parses the entire lease file
-func parseLeaseFile(filename string, filterFunc leaseFilterFunc) (map[string]*Lease, error) {
+func (app *application) parseLeaseFile(filename string, filterFunc leaseFilterFunc) (map[string]*Lease, error) {
 	leases := make(map[string]*Lease)
 
 	file, err := os.Open(filename)
@@ -100,7 +122,7 @@
 		if len(fields) > 0 && fields[0] == "lease" {
 			lease := Lease{}
 			lease.IPAddress = net.ParseIP(fields[1])
-			parseLease(scanner, &lease)
+			app.parseLease(scanner, &lease)
 			if filterFunc(&lease) {
 				leases[lease.IPAddress.String()] = &lease
 			}
@@ -136,7 +158,7 @@
 
 		// process the lease database
 		app.log.Infof("Synchronizing DHCP lease database")
-		leases, err := parseLeaseFile(app.DHCPLeaseFile,
+		leases, err := app.parseLeaseFile(app.DHCPLeaseFile,
 			func(lease *Lease) bool {
 				return lease.BindingState != Free &&
 					lease.Ends.After(now) &&