CORD-239 refactor of harvester uservice
Change-Id: I0fdb587267b6c5fb1c53bb35d77cd5921b937b6d
diff --git a/harvester/parse.go b/harvester/parse.go
new file mode 100644
index 0000000..d61e791
--- /dev/null
+++ b/harvester/parse.go
@@ -0,0 +1,237 @@
+// 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 (
+ "bufio"
+ "fmt"
+ "net"
+ "os"
+ "os/exec"
+ "strconv"
+ "strings"
+ "text/tabwriter"
+ "time"
+)
+
+// leaseFilterFunc provides a mechanism to filter which leases are returned by lease file parser
+type leaseFilterFunc func(lease *Lease) bool
+
+const (
+ // returns if a parse requests is processed or denied because of quiet period
+ responseQuiet uint = 0
+ responseOK uint = 1
+
+ // time format for parsing time stamps in lease file
+ dateTimeLayout = "2006/1/2 15:04:05"
+
+ bindFileFormat = "{{.ClientHostname}}\tIN A {{.IPAddress}}\t; {{.HardwareAddress}}"
+)
+
+// parseLease parses a single lease from the lease file
+func parseLease(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 no client-hostname was specified, generate one
+ if len(lease.ClientHostname) == 0 {
+ lease.ClientHostname = strings.ToUpper("UNK-" +
+ strings.Replace(lease.HardwareAddress.String(), ":", "", -1))
+ }
+ return nil
+ case "client-hostname":
+ lease.ClientHostname = strings.Trim(fields[1], "\";")
+ case "hardware":
+ lease.HardwareAddress, err = net.ParseMAC(strings.Trim(fields[2], ";"))
+ if err != nil {
+ return err
+ }
+ case "binding":
+ lease.BindingState, err = parseBindingState(strings.Trim(fields[2], ";"))
+ if err != nil {
+ return err
+ }
+ case "starts":
+ lease.Starts, err = time.Parse(dateTimeLayout,
+ fields[2]+" "+strings.Trim(fields[3], ";"))
+ if err != nil {
+ return err
+ }
+ case "ends":
+ lease.Ends, err = time.Parse(dateTimeLayout,
+ fields[2]+" "+strings.Trim(fields[3], ";"))
+ if err != nil {
+ return err
+ }
+ }
+ }
+ }
+ return nil
+}
+
+// parseLeaseFile parses the entire lease file
+func parseLeaseFile(filename string, filterFunc leaseFilterFunc) (map[string]*Lease, error) {
+ leases := make(map[string]*Lease)
+
+ 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] == "lease" {
+ lease := Lease{}
+ lease.IPAddress = net.ParseIP(fields[1])
+ parseLease(scanner, &lease)
+ if filterFunc(&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) {
+
+ // track the last time file was processed to enforce quiet period
+ var last *time.Time = nil
+
+ // process requests on the channel
+ for response := range requests {
+ now := time.Now()
+
+ // if the request is made during the quiet period then drop the request to prevent
+ // a storm
+ if last != nil && now.Sub(*last) < app.QuietPeriod {
+ app.log.Warn("Request received during query quiet period, will not harvest")
+ if response != nil {
+ *response <- responseQuiet
+ }
+ continue
+ }
+
+ // process the lease database
+ app.log.Infof("Synchronizing DHCP lease database")
+ leases, err := parseLeaseFile(app.DHCPLeaseFile,
+ func(lease *Lease) bool {
+ return lease.BindingState != Free &&
+ lease.Ends.After(now) &&
+ lease.Starts.Before(now)
+ })
+ if err != nil {
+ 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))
+ }
+ } 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)
+ }
+ 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()
+ }
+ if last == nil {
+ last = &time.Time{}
+ }
+ *last = time.Now()
+
+ if response != nil {
+ *response <- responseOK
+ }
+ }
+}
+
+// syncFromDHCPLeaseFileLoop periodically request a lease file processing
+func (app *application) syncFromDHCPLeaseFileLoop(requests chan *chan uint) {
+ responseChan := make(chan uint)
+ for {
+ requests <- &responseChan
+ select {
+ case _ = <-responseChan:
+ // request completed
+ case <-time.After(app.RequestTimeout):
+ app.log.Error("request to process DHCP lease file timed out")
+ }
+ time.Sleep(app.QueryPeriod)
+ }
+}