blob: 906b23cc7e81c6a2d7f3d791045736f183a6181e [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
139// syncRequestHandler accepts requests to parse the lease file and either processes or ignores because of quiet period
140func (app *application) syncRequestHandler(requests chan *chan uint) {
141
142 // track the last time file was processed to enforce quiet period
143 var last *time.Time = nil
144
145 // process requests on the channel
146 for response := range requests {
147 now := time.Now()
148
149 // if the request is made during the quiet period then drop the request to prevent
150 // a storm
151 if last != nil && now.Sub(*last) < app.QuietPeriod {
152 app.log.Warn("Request received during query quiet period, will not harvest")
153 if response != nil {
154 *response <- responseQuiet
155 }
156 continue
157 }
158
159 // process the lease database
160 app.log.Infof("Synchronizing DHCP lease database")
David K. Bainbridge17911b42017-01-09 20:53:22 -0800161 leases, err := app.parseLeaseFile(app.DHCPLeaseFile,
David K. Bainbridge732957f2016-10-06 22:36:59 -0700162 func(lease *Lease) bool {
163 return lease.BindingState != Free &&
164 lease.Ends.After(now) &&
165 lease.Starts.Before(now)
166 })
167 if err != nil {
168 app.log.Errorf("Unable to parse DHCP lease file at '%s' : %s",
169 app.DHCPLeaseFile, err)
170 } else {
171 // if configured to verify leases with a ping do so
172 if app.VerifyLeases {
173 app.log.Infof("Verifing %d discovered leases", len(leases))
174 _, err := app.verifyLeases(leases)
175 if err != nil {
176 app.log.Errorf("unexpected error while verifing leases : %s", err)
177 app.log.Infof("Discovered %d active, not verified because of error, DHCP leases",
178 len(leases))
179 } else {
180 app.log.Infof("Discovered %d active and verified DHCP leases", len(leases))
181 }
182 } else {
183 app.log.Infof("Discovered %d active, not not verified, DHCP leases", len(leases))
184 }
185
186 // if configured to output the lease information to a file, do so
187 if len(app.OutputFile) > 0 {
188 app.log.Infof("Writing lease information to file '%s'", app.OutputFile)
189 out, err := os.Create(app.OutputFile)
190 if err != nil {
191 app.log.Errorf(
192 "unexpected error while attempting to open file `%s' for output : %s",
193 app.OutputFile, err)
194 } else {
195 table := tabwriter.NewWriter(out, 1, 0, 4, ' ', 0)
196 for _, lease := range leases {
197 if err := app.outputTemplate.Execute(table, lease); err != nil {
198 app.log.Errorf(
199 "unexpected error while writing leases to file '%s' : %s",
200 app.OutputFile, err)
201 break
202 }
203 fmt.Fprintln(table)
204 }
205 table.Flush()
206 }
207 out.Close()
208 }
209
210 // if configured to reload the DNS server, then use the RNDC command to do so
211 if app.RNDCUpdate {
212 cmd := exec.Command("rndc", "-s", app.RNDCAddress, "-p", strconv.Itoa(app.RNDCPort),
213 "-c", app.RNDCKeyFile, "reload", app.RNDCZone)
214 err := cmd.Run()
215 if err != nil {
216 app.log.Errorf("Unexplected error while attempting to reload zone '%s' on DNS server '%s:%d' : %s", app.RNDCZone, app.RNDCAddress, app.RNDCPort, err)
217 } else {
218 app.log.Infof("Successfully reloaded DNS zone '%s' on server '%s:%d' via RNDC command",
219 app.RNDCZone, app.RNDCAddress, app.RNDCPort)
220 }
221 }
222
223 // process the results of the parse to internal data structures
224 app.interchange.Lock()
225 app.leases = leases
226 app.byHostname = make(map[string]*Lease)
227 app.byHardware = make(map[string]*Lease)
228 for _, lease := range leases {
229 app.byHostname[lease.ClientHostname] = lease
230 app.byHardware[lease.HardwareAddress.String()] = lease
231 }
232 leases = nil
233 app.interchange.Unlock()
234 }
235 if last == nil {
236 last = &time.Time{}
237 }
238 *last = time.Now()
239
240 if response != nil {
241 *response <- responseOK
242 }
243 }
244}
245
246// syncFromDHCPLeaseFileLoop periodically request a lease file processing
247func (app *application) syncFromDHCPLeaseFileLoop(requests chan *chan uint) {
248 responseChan := make(chan uint)
249 for {
250 requests <- &responseChan
251 select {
252 case _ = <-responseChan:
253 // request completed
254 case <-time.After(app.RequestTimeout):
255 app.log.Error("request to process DHCP lease file timed out")
256 }
257 time.Sleep(app.QueryPeriod)
258 }
259}