| // 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" |
| "bytes" |
| "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}}" |
| ) |
| |
| // 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 (app *application) 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 = 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 { |
| 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 (app *application) 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]) |
| app.parseLease(scanner, &lease) |
| if filterFunc(&lease) { |
| leases[lease.IPAddress.String()] = &lease |
| } |
| } |
| } |
| |
| if err = scanner.Err(); err != nil { |
| return nil, err |
| } |
| |
| 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) { |
| |
| // 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 := app.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 { |
| 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("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)) |
| } |
| } 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) |
| } |
| } |