blob: d61e791acd571660f7a4e23ae6e844391543f413 [file] [log] [blame]
David K. Bainbridge732957f2016-10-06 22:36:59 -07001// Copyright 2016 Open Networking Laboratory
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14package main
15
16import (
17 "bufio"
18 "fmt"
19 "net"
20 "os"
21 "os/exec"
22 "strconv"
23 "strings"
24 "text/tabwriter"
25 "time"
26)
27
28// leaseFilterFunc provides a mechanism to filter which leases are returned by lease file parser
29type leaseFilterFunc func(lease *Lease) bool
30
31const (
32 // returns if a parse requests is processed or denied because of quiet period
33 responseQuiet uint = 0
34 responseOK uint = 1
35
36 // time format for parsing time stamps in lease file
37 dateTimeLayout = "2006/1/2 15:04:05"
38
39 bindFileFormat = "{{.ClientHostname}}\tIN A {{.IPAddress}}\t; {{.HardwareAddress}}"
40)
41
42// parseLease parses a single lease from the lease file
43func parseLease(scanner *bufio.Scanner, lease *Lease) error {
44 var err error
45 for scanner.Scan() {
46 fields := strings.Fields(scanner.Text())
47 if len(fields) > 0 {
48 switch fields[0] {
49 case "}":
50 // If no client-hostname was specified, generate one
51 if len(lease.ClientHostname) == 0 {
52 lease.ClientHostname = strings.ToUpper("UNK-" +
53 strings.Replace(lease.HardwareAddress.String(), ":", "", -1))
54 }
55 return nil
56 case "client-hostname":
57 lease.ClientHostname = strings.Trim(fields[1], "\";")
58 case "hardware":
59 lease.HardwareAddress, err = net.ParseMAC(strings.Trim(fields[2], ";"))
60 if err != nil {
61 return err
62 }
63 case "binding":
64 lease.BindingState, err = parseBindingState(strings.Trim(fields[2], ";"))
65 if err != nil {
66 return err
67 }
68 case "starts":
69 lease.Starts, err = time.Parse(dateTimeLayout,
70 fields[2]+" "+strings.Trim(fields[3], ";"))
71 if err != nil {
72 return err
73 }
74 case "ends":
75 lease.Ends, err = time.Parse(dateTimeLayout,
76 fields[2]+" "+strings.Trim(fields[3], ";"))
77 if err != nil {
78 return err
79 }
80 }
81 }
82 }
83 return nil
84}
85
86// parseLeaseFile parses the entire lease file
87func parseLeaseFile(filename string, filterFunc leaseFilterFunc) (map[string]*Lease, error) {
88 leases := make(map[string]*Lease)
89
90 file, err := os.Open(filename)
91 if err != nil {
92 return nil, err
93 }
94 defer file.Close()
95
96 scanner := bufio.NewScanner(file)
97 scanner.Split(bufio.ScanLines)
98 for scanner.Scan() {
99 fields := strings.Fields(scanner.Text())
100 if len(fields) > 0 && fields[0] == "lease" {
101 lease := Lease{}
102 lease.IPAddress = net.ParseIP(fields[1])
103 parseLease(scanner, &lease)
104 if filterFunc(&lease) {
105 leases[lease.IPAddress.String()] = &lease
106 }
107 }
108 }
109
110 if err = scanner.Err(); err != nil {
111 return nil, err
112 }
113
114 return leases, nil
115}
116
117// syncRequestHandler accepts requests to parse the lease file and either processes or ignores because of quiet period
118func (app *application) syncRequestHandler(requests chan *chan uint) {
119
120 // track the last time file was processed to enforce quiet period
121 var last *time.Time = nil
122
123 // process requests on the channel
124 for response := range requests {
125 now := time.Now()
126
127 // if the request is made during the quiet period then drop the request to prevent
128 // a storm
129 if last != nil && now.Sub(*last) < app.QuietPeriod {
130 app.log.Warn("Request received during query quiet period, will not harvest")
131 if response != nil {
132 *response <- responseQuiet
133 }
134 continue
135 }
136
137 // process the lease database
138 app.log.Infof("Synchronizing DHCP lease database")
139 leases, err := parseLeaseFile(app.DHCPLeaseFile,
140 func(lease *Lease) bool {
141 return lease.BindingState != Free &&
142 lease.Ends.After(now) &&
143 lease.Starts.Before(now)
144 })
145 if err != nil {
146 app.log.Errorf("Unable to parse DHCP lease file at '%s' : %s",
147 app.DHCPLeaseFile, err)
148 } else {
149 // if configured to verify leases with a ping do so
150 if app.VerifyLeases {
151 app.log.Infof("Verifing %d discovered leases", len(leases))
152 _, err := app.verifyLeases(leases)
153 if err != nil {
154 app.log.Errorf("unexpected error while verifing leases : %s", err)
155 app.log.Infof("Discovered %d active, not verified because of error, DHCP leases",
156 len(leases))
157 } else {
158 app.log.Infof("Discovered %d active and verified DHCP leases", len(leases))
159 }
160 } else {
161 app.log.Infof("Discovered %d active, not not verified, DHCP leases", len(leases))
162 }
163
164 // if configured to output the lease information to a file, do so
165 if len(app.OutputFile) > 0 {
166 app.log.Infof("Writing lease information to file '%s'", app.OutputFile)
167 out, err := os.Create(app.OutputFile)
168 if err != nil {
169 app.log.Errorf(
170 "unexpected error while attempting to open file `%s' for output : %s",
171 app.OutputFile, err)
172 } else {
173 table := tabwriter.NewWriter(out, 1, 0, 4, ' ', 0)
174 for _, lease := range leases {
175 if err := app.outputTemplate.Execute(table, lease); err != nil {
176 app.log.Errorf(
177 "unexpected error while writing leases to file '%s' : %s",
178 app.OutputFile, err)
179 break
180 }
181 fmt.Fprintln(table)
182 }
183 table.Flush()
184 }
185 out.Close()
186 }
187
188 // if configured to reload the DNS server, then use the RNDC command to do so
189 if app.RNDCUpdate {
190 cmd := exec.Command("rndc", "-s", app.RNDCAddress, "-p", strconv.Itoa(app.RNDCPort),
191 "-c", app.RNDCKeyFile, "reload", app.RNDCZone)
192 err := cmd.Run()
193 if err != nil {
194 app.log.Errorf("Unexplected error while attempting to reload zone '%s' on DNS server '%s:%d' : %s", app.RNDCZone, app.RNDCAddress, app.RNDCPort, err)
195 } else {
196 app.log.Infof("Successfully reloaded DNS zone '%s' on server '%s:%d' via RNDC command",
197 app.RNDCZone, app.RNDCAddress, app.RNDCPort)
198 }
199 }
200
201 // process the results of the parse to internal data structures
202 app.interchange.Lock()
203 app.leases = leases
204 app.byHostname = make(map[string]*Lease)
205 app.byHardware = make(map[string]*Lease)
206 for _, lease := range leases {
207 app.byHostname[lease.ClientHostname] = lease
208 app.byHardware[lease.HardwareAddress.String()] = lease
209 }
210 leases = nil
211 app.interchange.Unlock()
212 }
213 if last == nil {
214 last = &time.Time{}
215 }
216 *last = time.Now()
217
218 if response != nil {
219 *response <- responseOK
220 }
221 }
222}
223
224// syncFromDHCPLeaseFileLoop periodically request a lease file processing
225func (app *application) syncFromDHCPLeaseFileLoop(requests chan *chan uint) {
226 responseChan := make(chan uint)
227 for {
228 requests <- &responseChan
229 select {
230 case _ = <-responseChan:
231 // request completed
232 case <-time.After(app.RequestTimeout):
233 app.log.Error("request to process DHCP lease file timed out")
234 }
235 time.Sleep(app.QueryPeriod)
236 }
237}