CORD-1181 include DHCP reservations when harvesting from DHCP

Change-Id: I5e148a5d92f275455681033d402b413beef8a537
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{}