CORD-1181 include DHCP reservations when harvesting from DHCP

Change-Id: I5e148a5d92f275455681033d402b413beef8a537
diff --git a/harvester/Dockerfile b/harvester/Dockerfile
index f0233ec..4cdc692 100644
--- a/harvester/Dockerfile
+++ b/harvester/Dockerfile
@@ -22,7 +22,7 @@
 RUN go build -o /service/entry-point gerrit.opencord.com/maas/harvester
 
 LABEL org.label-schema.name="harvester" \
-      org.label-schema.description="Provides DHCP havesting and insertion into DNS" \
+      org.label-schema.description="Provides DHCP harvesting and insertion into DNS" \
       org.label-schema.vcs-url="https://gerrit.opencord.org/maas" \
       org.label-schema.vendor="Open Networking Laboratory" \
       org.label-schema.schema-version="1.0"
diff --git a/harvester/Dockerfile.release b/harvester/Dockerfile.release
index b6d636d..1089ddc 100644
--- a/harvester/Dockerfile.release
+++ b/harvester/Dockerfile.release
@@ -19,7 +19,7 @@
 ADD entry-point /service/entry-point
 
 LABEL org.label-schema.name="harvester" \
-      org.label-schema.description="Provides DHCP havesting and insertion into DNS" \
+      org.label-schema.description="Provides DHCP harvesting and insertion into DNS" \
       org.label-schema.vcs-url="https://gerrit.opencord.org/maas" \
       org.label-schema.vendor="Open Networking Laboratory" \
       org.label-schema.schema-version="1.0"
diff --git a/harvester/harvester.go b/harvester/harvester.go
index e0314e6..3a16817 100644
--- a/harvester/harvester.go
+++ b/harvester/harvester.go
@@ -33,26 +33,27 @@
 
 // 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"`
-	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"`
+	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"`
+	DHCPReservationFile string        `default:"/reservations/dhcpd.reservations" envconfig:"DHCP_RESERVATION_FILE" desc:"lease reservation file for IP 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"`
 
 	appFlags           *flag.FlagSet      `ignored:"true"`
 	log                *logrus.Logger     `ignored:"true"`
@@ -113,29 +114,30 @@
 
 	// 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
-	   BAD_CLIENT_NAMES:     %s
-	   CLIENT_NAME_TEMPLATE: %s`,
+           LISTEN:                %s
+           PORT:                  %d
+           LOG_LEVEL:             %s
+           LOG_FORMAT:            %s
+           DHCP_LEASE_FILE:       %s
+           DHCP_RESERVATION_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.DHCPLeaseFile, app.DHCPReservationFile, 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,
diff --git a/harvester/parse.go b/harvester/parse.go
index 906b23c..059bf8e 100644
--- a/harvester/parse.go
+++ b/harvester/parse.go
@@ -136,6 +136,70 @@
 	return leases, nil
 }
 
+// parseReservation parses a single reservation entry
+func (app *application) parseReservation(scanner *bufio.Scanner, lease *Lease) error {
+	var err error
+	for scanner.Scan() {
+		fields := strings.Fields(scanner.Text())
+		if len(fields) > 0 {
+			switch fields[0] {
+			case "}":
+				// If not IP or MAC specified then return error
+				if len(lease.HardwareAddress) == 0 {
+					return fmt.Errorf("Reservation requires hardware address")
+				}
+				if len(lease.IPAddress) == 0 {
+					return fmt.Errorf("Reservation requires IP address")
+				}
+				return nil
+			case "hardware":
+				lease.HardwareAddress, err = net.ParseMAC(strings.Trim(fields[2], ";"))
+				if err != nil {
+					return err
+				}
+			case "fixed-address":
+				lease.IPAddress = net.ParseIP(strings.Trim(fields[1], ";"))
+				if lease.IPAddress == nil {
+					return fmt.Errorf("Invalid IP Address")
+				}
+			}
+		}
+	}
+	return nil
+}
+
+// parseReservationFile parses the reservation file to include reservation IPs in IP information
+func (app *application) parseReservationFile(filename string, leases map[string]*Lease) (map[string]*Lease, error) {
+	// If no filename was specified, nothing to parse
+	if len(filename) == 0 {
+		return leases, nil
+	}
+
+	file, err := os.Open(filename)
+	if err != nil {
+		return nil, err
+	}
+	defer file.Close()
+
+	scanner := bufio.NewScanner(file)
+	scanner.Split(bufio.ScanLines)
+	for scanner.Scan() {
+		fields := strings.Fields(scanner.Text())
+		if len(fields) > 0 && fields[0] == "host" {
+			lease := Lease{}
+			lease.ClientHostname = fields[1]
+			app.parseReservation(scanner, &lease)
+			leases[lease.IPAddress.String()] = &lease
+		}
+	}
+
+	if err = scanner.Err(); err != nil {
+		return nil, err
+	}
+
+	return leases, nil
+}
+
 // syncRequestHandler accepts requests to parse the lease file and either processes or ignores because of quiet period
 func (app *application) syncRequestHandler(requests chan *chan uint) {
 
@@ -168,69 +232,81 @@
 			app.log.Errorf("Unable to parse DHCP lease file at '%s' : %s",
 				app.DHCPLeaseFile, err)
 		} else {
-			// if configured to verify leases with a ping do so
-			if app.VerifyLeases {
-				app.log.Infof("Verifing %d discovered leases", len(leases))
-				_, err := app.verifyLeases(leases)
-				if err != nil {
-					app.log.Errorf("unexpected error while verifing leases : %s", err)
-					app.log.Infof("Discovered %d active, not verified because of error, DHCP leases",
-						len(leases))
-				} else {
-					app.log.Infof("Discovered %d active and verified DHCP leases", len(leases))
-				}
+			leaseCount := len(leases)
+			app.log.Infof("Read %d leases from lease file", leaseCount)
+			// Process the reservation file, if specified
+			app.log.Info("Synchronizing DHCP reservation file")
+			leases, err = app.parseReservationFile(app.DHCPReservationFile, leases)
+			if err != nil {
+				app.log.Errorf("Unable to parse reservation file '%s' : '%s'",
+					app.DHCPReservationFile, err)
 			} else {
-				app.log.Infof("Discovered %d active, not not verified, DHCP leases", len(leases))
-			}
-
-			// if configured to output the lease information to a file, do so
-			if len(app.OutputFile) > 0 {
-				app.log.Infof("Writing lease information to file '%s'", app.OutputFile)
-				out, err := os.Create(app.OutputFile)
-				if err != nil {
-					app.log.Errorf(
-						"unexpected error while attempting to open file `%s' for output : %s",
-						app.OutputFile, err)
-				} else {
-					table := tabwriter.NewWriter(out, 1, 0, 4, ' ', 0)
-					for _, lease := range leases {
-						if err := app.outputTemplate.Execute(table, lease); err != nil {
-							app.log.Errorf(
-								"unexpected error while writing leases to file '%s' : %s",
-								app.OutputFile, err)
-							break
-						}
-						fmt.Fprintln(table)
+				app.log.Infof("Read %d reservations from reservation file",
+					len(leases)-leaseCount)
+				// if configured to verify leases with a ping do so
+				if app.VerifyLeases {
+					app.log.Infof("Verifing %d discovered leases", len(leases))
+					_, err := app.verifyLeases(leases)
+					if err != nil {
+						app.log.Errorf("unexpected error while verifing leases : %s", err)
+						app.log.Infof("Discovered %d active, not verified because of error, DHCP leases",
+							len(leases))
+					} else {
+						app.log.Infof("Discovered %d active and verified DHCP leases", len(leases))
 					}
-					table.Flush()
-				}
-				out.Close()
-			}
-
-			// if configured to reload the DNS server, then use the RNDC command to do so
-			if app.RNDCUpdate {
-				cmd := exec.Command("rndc", "-s", app.RNDCAddress, "-p", strconv.Itoa(app.RNDCPort),
-					"-c", app.RNDCKeyFile, "reload", app.RNDCZone)
-				err := cmd.Run()
-				if err != nil {
-					app.log.Errorf("Unexplected error while attempting to reload zone '%s' on DNS server '%s:%d' : %s", app.RNDCZone, app.RNDCAddress, app.RNDCPort, err)
 				} else {
-					app.log.Infof("Successfully reloaded DNS zone '%s' on server '%s:%d' via RNDC command",
-						app.RNDCZone, app.RNDCAddress, app.RNDCPort)
+					app.log.Infof("Discovered %d active, not not verified, DHCP leases", len(leases))
 				}
-			}
 
-			// process the results of the parse to internal data structures
-			app.interchange.Lock()
-			app.leases = leases
-			app.byHostname = make(map[string]*Lease)
-			app.byHardware = make(map[string]*Lease)
-			for _, lease := range leases {
-				app.byHostname[lease.ClientHostname] = lease
-				app.byHardware[lease.HardwareAddress.String()] = lease
+				// if configured to output the lease information to a file, do so
+				if len(app.OutputFile) > 0 {
+					app.log.Infof("Writing lease information to file '%s'", app.OutputFile)
+					out, err := os.Create(app.OutputFile)
+					if err != nil {
+						app.log.Errorf(
+							"unexpected error while attempting to open file `%s' for output : %s",
+							app.OutputFile, err)
+					} else {
+						table := tabwriter.NewWriter(out, 1, 0, 4, ' ', 0)
+						for _, lease := range leases {
+							if err := app.outputTemplate.Execute(table, lease); err != nil {
+								app.log.Errorf(
+									"unexpected error while writing leases to file '%s' : %s",
+									app.OutputFile, err)
+								break
+							}
+							fmt.Fprintln(table)
+						}
+						table.Flush()
+					}
+					out.Close()
+				}
+
+				// if configured to reload the DNS server, then use the RNDC command to do so
+				if app.RNDCUpdate {
+					cmd := exec.Command("rndc", "-s", app.RNDCAddress, "-p", strconv.Itoa(app.RNDCPort),
+						"-c", app.RNDCKeyFile, "reload", app.RNDCZone)
+					err := cmd.Run()
+					if err != nil {
+						app.log.Errorf("Unexplected error while attempting to reload zone '%s' on DNS server '%s:%d' : %s", app.RNDCZone, app.RNDCAddress, app.RNDCPort, err)
+					} else {
+						app.log.Infof("Successfully reloaded DNS zone '%s' on server '%s:%d' via RNDC command",
+							app.RNDCZone, app.RNDCAddress, app.RNDCPort)
+					}
+				}
+
+				// process the results of the parse to internal data structures
+				app.interchange.Lock()
+				app.leases = leases
+				app.byHostname = make(map[string]*Lease)
+				app.byHardware = make(map[string]*Lease)
+				for _, lease := range leases {
+					app.byHostname[lease.ClientHostname] = lease
+					app.byHardware[lease.HardwareAddress.String()] = lease
+				}
+				leases = nil
+				app.interchange.Unlock()
 			}
-			leases = nil
-			app.interchange.Unlock()
 		}
 		if last == nil {
 			last = &time.Time{}
diff --git a/roles/maas/templates/automation-compose.yml.j2 b/roles/maas/templates/automation-compose.yml.j2
index 3da052e..237b430 100644
--- a/roles/maas/templates/automation-compose.yml.j2
+++ b/roles/maas/templates/automation-compose.yml.j2
@@ -132,6 +132,7 @@
           - "lab.solution=cord"
           - "lab.component=harvester"
       volumes:
+          - "/etc/dhcp:/reservations"
           - "/var/lib/maas/dhcp:/harvester"
           - "/etc/bind/maas:/bind"
           - "/etc/bind/maas:/key"