blob: 059bf8eb672217db830829566eb044e7a0a857f1 [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"
David K. Bainbridge17911b42017-01-09 20:53:22 -080018 "bytes"
David K. Bainbridge732957f2016-10-06 22:36:59 -070019 "fmt"
20 "net"
21 "os"
22 "os/exec"
23 "strconv"
24 "strings"
25 "text/tabwriter"
26 "time"
27)
28
29// leaseFilterFunc provides a mechanism to filter which leases are returned by lease file parser
30type leaseFilterFunc func(lease *Lease) bool
31
32const (
33 // returns if a parse requests is processed or denied because of quiet period
34 responseQuiet uint = 0
35 responseOK uint = 1
36
37 // time format for parsing time stamps in lease file
38 dateTimeLayout = "2006/1/2 15:04:05"
39
40 bindFileFormat = "{{.ClientHostname}}\tIN A {{.IPAddress}}\t; {{.HardwareAddress}}"
41)
42
David K. Bainbridge17911b42017-01-09 20:53:22 -080043// generateClientHostname generates a client name based on hardware address
44func (app *application) generateClientHostname(lease *Lease) string {
45 var buf bytes.Buffer
46
47 app.log.Debugf("Generating client-hostname for MAC '%s'", lease.HardwareAddress.String())
48
49 err := app.clientNameTemplate.Execute(&buf, lease)
50 if err != nil {
51 app.log.Errorf("Unable to generate client host name for lease with HW address '%s' : %s",
52 lease.HardwareAddress.String(), err)
53 return strings.ToUpper("UNK-" +
54 strings.Replace(lease.HardwareAddress.String(), ":", "", -1))
55 }
56
57 return buf.String()
58}
59
David K. Bainbridge732957f2016-10-06 22:36:59 -070060// parseLease parses a single lease from the lease file
David K. Bainbridge17911b42017-01-09 20:53:22 -080061func (app *application) parseLease(scanner *bufio.Scanner, lease *Lease) error {
David K. Bainbridge732957f2016-10-06 22:36:59 -070062 var err error
63 for scanner.Scan() {
64 fields := strings.Fields(scanner.Text())
65 if len(fields) > 0 {
66 switch fields[0] {
67 case "}":
68 // If no client-hostname was specified, generate one
69 if len(lease.ClientHostname) == 0 {
David K. Bainbridge17911b42017-01-09 20:53:22 -080070 lease.ClientHostname = app.generateClientHostname(lease)
David K. Bainbridge732957f2016-10-06 22:36:59 -070071 }
72 return nil
73 case "client-hostname":
74 lease.ClientHostname = strings.Trim(fields[1], "\";")
David K. Bainbridge17911b42017-01-09 20:53:22 -080075
76 // Validate client-hostname
77 if _, ok := app.badClientNames[lease.ClientHostname]; ok {
78 lease.ClientHostname = app.generateClientHostname(lease)
79 }
David K. Bainbridge732957f2016-10-06 22:36:59 -070080 case "hardware":
81 lease.HardwareAddress, err = net.ParseMAC(strings.Trim(fields[2], ";"))
82 if err != nil {
83 return err
84 }
85 case "binding":
86 lease.BindingState, err = parseBindingState(strings.Trim(fields[2], ";"))
87 if err != nil {
88 return err
89 }
90 case "starts":
91 lease.Starts, err = time.Parse(dateTimeLayout,
92 fields[2]+" "+strings.Trim(fields[3], ";"))
93 if err != nil {
94 return err
95 }
96 case "ends":
97 lease.Ends, err = time.Parse(dateTimeLayout,
98 fields[2]+" "+strings.Trim(fields[3], ";"))
99 if err != nil {
100 return err
101 }
102 }
103 }
104 }
105 return nil
106}
107
108// parseLeaseFile parses the entire lease file
David K. Bainbridge17911b42017-01-09 20:53:22 -0800109func (app *application) parseLeaseFile(filename string, filterFunc leaseFilterFunc) (map[string]*Lease, error) {
David K. Bainbridge732957f2016-10-06 22:36:59 -0700110 leases := make(map[string]*Lease)
111
112 file, err := os.Open(filename)
113 if err != nil {
114 return nil, err
115 }
116 defer file.Close()
117
118 scanner := bufio.NewScanner(file)
119 scanner.Split(bufio.ScanLines)
120 for scanner.Scan() {
121 fields := strings.Fields(scanner.Text())
122 if len(fields) > 0 && fields[0] == "lease" {
123 lease := Lease{}
124 lease.IPAddress = net.ParseIP(fields[1])
David K. Bainbridge17911b42017-01-09 20:53:22 -0800125 app.parseLease(scanner, &lease)
David K. Bainbridge732957f2016-10-06 22:36:59 -0700126 if filterFunc(&lease) {
127 leases[lease.IPAddress.String()] = &lease
128 }
129 }
130 }
131
132 if err = scanner.Err(); err != nil {
133 return nil, err
134 }
135
136 return leases, nil
137}
138
David K. Bainbridge5896c302017-05-08 16:21:56 -0700139// parseReservation parses a single reservation entry
140func (app *application) parseReservation(scanner *bufio.Scanner, lease *Lease) error {
141 var err error
142 for scanner.Scan() {
143 fields := strings.Fields(scanner.Text())
144 if len(fields) > 0 {
145 switch fields[0] {
146 case "}":
147 // If not IP or MAC specified then return error
148 if len(lease.HardwareAddress) == 0 {
149 return fmt.Errorf("Reservation requires hardware address")
150 }
151 if len(lease.IPAddress) == 0 {
152 return fmt.Errorf("Reservation requires IP address")
153 }
154 return nil
155 case "hardware":
156 lease.HardwareAddress, err = net.ParseMAC(strings.Trim(fields[2], ";"))
157 if err != nil {
158 return err
159 }
160 case "fixed-address":
161 lease.IPAddress = net.ParseIP(strings.Trim(fields[1], ";"))
162 if lease.IPAddress == nil {
163 return fmt.Errorf("Invalid IP Address")
164 }
165 }
166 }
167 }
168 return nil
169}
170
171// parseReservationFile parses the reservation file to include reservation IPs in IP information
172func (app *application) parseReservationFile(filename string, leases map[string]*Lease) (map[string]*Lease, error) {
173 // If no filename was specified, nothing to parse
174 if len(filename) == 0 {
175 return leases, nil
176 }
177
178 file, err := os.Open(filename)
179 if err != nil {
180 return nil, err
181 }
182 defer file.Close()
183
184 scanner := bufio.NewScanner(file)
185 scanner.Split(bufio.ScanLines)
186 for scanner.Scan() {
187 fields := strings.Fields(scanner.Text())
188 if len(fields) > 0 && fields[0] == "host" {
189 lease := Lease{}
190 lease.ClientHostname = fields[1]
191 app.parseReservation(scanner, &lease)
192 leases[lease.IPAddress.String()] = &lease
193 }
194 }
195
196 if err = scanner.Err(); err != nil {
197 return nil, err
198 }
199
200 return leases, nil
201}
202
David K. Bainbridge732957f2016-10-06 22:36:59 -0700203// syncRequestHandler accepts requests to parse the lease file and either processes or ignores because of quiet period
204func (app *application) syncRequestHandler(requests chan *chan uint) {
205
206 // track the last time file was processed to enforce quiet period
207 var last *time.Time = nil
208
209 // process requests on the channel
210 for response := range requests {
211 now := time.Now()
212
213 // if the request is made during the quiet period then drop the request to prevent
214 // a storm
215 if last != nil && now.Sub(*last) < app.QuietPeriod {
216 app.log.Warn("Request received during query quiet period, will not harvest")
217 if response != nil {
218 *response <- responseQuiet
219 }
220 continue
221 }
222
223 // process the lease database
224 app.log.Infof("Synchronizing DHCP lease database")
David K. Bainbridge17911b42017-01-09 20:53:22 -0800225 leases, err := app.parseLeaseFile(app.DHCPLeaseFile,
David K. Bainbridge732957f2016-10-06 22:36:59 -0700226 func(lease *Lease) bool {
227 return lease.BindingState != Free &&
228 lease.Ends.After(now) &&
229 lease.Starts.Before(now)
230 })
231 if err != nil {
232 app.log.Errorf("Unable to parse DHCP lease file at '%s' : %s",
233 app.DHCPLeaseFile, err)
234 } else {
David K. Bainbridge5896c302017-05-08 16:21:56 -0700235 leaseCount := len(leases)
236 app.log.Infof("Read %d leases from lease file", leaseCount)
237 // Process the reservation file, if specified
238 app.log.Info("Synchronizing DHCP reservation file")
239 leases, err = app.parseReservationFile(app.DHCPReservationFile, leases)
240 if err != nil {
241 app.log.Errorf("Unable to parse reservation file '%s' : '%s'",
242 app.DHCPReservationFile, err)
David K. Bainbridge732957f2016-10-06 22:36:59 -0700243 } else {
David K. Bainbridge5896c302017-05-08 16:21:56 -0700244 app.log.Infof("Read %d reservations from reservation file",
245 len(leases)-leaseCount)
246 // if configured to verify leases with a ping do so
247 if app.VerifyLeases {
248 app.log.Infof("Verifing %d discovered leases", len(leases))
249 _, err := app.verifyLeases(leases)
250 if err != nil {
251 app.log.Errorf("unexpected error while verifing leases : %s", err)
252 app.log.Infof("Discovered %d active, not verified because of error, DHCP leases",
253 len(leases))
254 } else {
255 app.log.Infof("Discovered %d active and verified DHCP leases", len(leases))
David K. Bainbridge732957f2016-10-06 22:36:59 -0700256 }
David K. Bainbridge732957f2016-10-06 22:36:59 -0700257 } else {
David K. Bainbridge5896c302017-05-08 16:21:56 -0700258 app.log.Infof("Discovered %d active, not not verified, DHCP leases", len(leases))
David K. Bainbridge732957f2016-10-06 22:36:59 -0700259 }
David K. Bainbridge732957f2016-10-06 22:36:59 -0700260
David K. Bainbridge5896c302017-05-08 16:21:56 -0700261 // if configured to output the lease information to a file, do so
262 if len(app.OutputFile) > 0 {
263 app.log.Infof("Writing lease information to file '%s'", app.OutputFile)
264 out, err := os.Create(app.OutputFile)
265 if err != nil {
266 app.log.Errorf(
267 "unexpected error while attempting to open file `%s' for output : %s",
268 app.OutputFile, err)
269 } else {
270 table := tabwriter.NewWriter(out, 1, 0, 4, ' ', 0)
271 for _, lease := range leases {
272 if err := app.outputTemplate.Execute(table, lease); err != nil {
273 app.log.Errorf(
274 "unexpected error while writing leases to file '%s' : %s",
275 app.OutputFile, err)
276 break
277 }
278 fmt.Fprintln(table)
279 }
280 table.Flush()
281 }
282 out.Close()
283 }
284
285 // if configured to reload the DNS server, then use the RNDC command to do so
286 if app.RNDCUpdate {
287 cmd := exec.Command("rndc", "-s", app.RNDCAddress, "-p", strconv.Itoa(app.RNDCPort),
288 "-c", app.RNDCKeyFile, "reload", app.RNDCZone)
289 err := cmd.Run()
290 if err != nil {
291 app.log.Errorf("Unexplected error while attempting to reload zone '%s' on DNS server '%s:%d' : %s", app.RNDCZone, app.RNDCAddress, app.RNDCPort, err)
292 } else {
293 app.log.Infof("Successfully reloaded DNS zone '%s' on server '%s:%d' via RNDC command",
294 app.RNDCZone, app.RNDCAddress, app.RNDCPort)
295 }
296 }
297
298 // process the results of the parse to internal data structures
299 app.interchange.Lock()
300 app.leases = leases
301 app.byHostname = make(map[string]*Lease)
302 app.byHardware = make(map[string]*Lease)
303 for _, lease := range leases {
304 app.byHostname[lease.ClientHostname] = lease
305 app.byHardware[lease.HardwareAddress.String()] = lease
306 }
307 leases = nil
308 app.interchange.Unlock()
David K. Bainbridge732957f2016-10-06 22:36:59 -0700309 }
David K. Bainbridge732957f2016-10-06 22:36:59 -0700310 }
311 if last == nil {
312 last = &time.Time{}
313 }
314 *last = time.Now()
315
316 if response != nil {
317 *response <- responseOK
318 }
319 }
320}
321
322// syncFromDHCPLeaseFileLoop periodically request a lease file processing
323func (app *application) syncFromDHCPLeaseFileLoop(requests chan *chan uint) {
324 responseChan := make(chan uint)
325 for {
326 requests <- &responseChan
327 select {
328 case _ = <-responseChan:
329 // request completed
330 case <-time.After(app.RequestTimeout):
331 app.log.Error("request to process DHCP lease file timed out")
332 }
333 time.Sleep(app.QueryPeriod)
334 }
335}