diff --git a/dhcpd.leases b/dhcpd.leases deleted file mode 100644 index 70f0749..0000000 --- a/dhcpd.leases +++ /dev/null @@ -1,22 +0,0 @@ -lease 192.168.2.27 { - starts 4 2019/06/27 06:40:21; - ends 4 2020/06/27 06:50:21; - cltt 4 2020/06/27 06:40:21; - binding state active; - next binding state free; - rewind binding state free; - hardware ethernet ac:1f:6b:35:ac:62; - uid "\001\254\037k5\254b"; - set vendor-class-identifier = "udhcp 1.23.1"; -} -lease 192.168.2.30 { - starts 4 2019/06/27 06:40:06; - ends 4 2020/06/27 06:50:06; - cltt 4 2019/06/27 06:40:06; - binding state active; - next binding state free; - rewind binding state free; - hardware ethernet ac:1f:6b:35:ab:2d; - uid "\001\254\037k5\253-"; - set vendor-class-identifier = "udhcp 1.23.1"; -} \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml index db01b34..d229414 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -8,11 +8,10 @@ services: image: metalstack/metal-bmc:latest network_mode: host volumes: - - ${PWD}/dhcpd.leases:/dhcpd.leases + - ${PWD}/kea-leases.csv:/kea-leases.csv environment: - METAL_BMC_LEASE_FILE: /dhcpd.leases + METAL_BMC_LEASE_FILE: /kea-leases.csv METAL_BMC_PARTITION_ID: partition METAL_BMC_METAL_API_URL: http://localhost:8080 METAL_BMC_METAL_API_HMAC_KEY: test METAL_BMC_IGNORE_MACS: "aa:aa:aa:aa:aa:aa" - diff --git a/internal/leases/leases.go b/internal/leases/leases.go index 033e763..69eb571 100644 --- a/internal/leases/leases.go +++ b/internal/leases/leases.go @@ -29,10 +29,13 @@ func (l Leases) LatestByMac() map[string]Lease { return byMac } -func ReadLeases(leaseFile string) (Leases, error) { - leasesContent, err := os.ReadFile(leaseFile) +func ReadLeases(filename string) (Leases, error) { + file, err := os.Open(filename) if err != nil { return nil, err } - return parse(string(leasesContent)) + defer func(file *os.File) { + _ = file.Close() + }(file) + return parse(file) } diff --git a/internal/leases/leases_test.go b/internal/leases/leases_test.go index 33844e2..605870f 100644 --- a/internal/leases/leases_test.go +++ b/internal/leases/leases_test.go @@ -1,6 +1,7 @@ package leases import ( + "strings" "testing" "time" @@ -9,7 +10,7 @@ import ( ) func TestFilterActive(t *testing.T) { - l, err := parse(sampleLeaseContent) + l, err := parse(strings.NewReader(sampleLeaseContent)) require.NoError(t, err) assert.Equal(t, Leases{}, l.FilterActive()) } diff --git a/internal/leases/parser.go b/internal/leases/parser.go index 5e68ac9..dbe848c 100644 --- a/internal/leases/parser.go +++ b/internal/leases/parser.go @@ -1,47 +1,80 @@ package leases import ( + "encoding/csv" "errors" - "regexp" + "fmt" + "io" + "strconv" + "strings" "time" ) -const ( - leaseDateFormat = "2006/01/02 15:04:05" - leaseRegex = `(?ms)lease\s+(?P[^\s]+)\s+{.*?starts\s\d+\s(?P[\d\/]+\s[\d\:]+);.*?ends\s\d+\s(?P[\d\/]+\s[\d\:]+);.*?hardware\sethernet\s(?P[\w\:]+);.*?}` -) +func parse(r io.Reader) (Leases, error) { + reader := csv.NewReader(r) + + header, err := reader.Read() + if err != nil { + return nil, fmt.Errorf("failed to read header: %w", err) + } + + if len(header) < 5 || header[0] != "address" || header[1] != "hwaddr" { + return nil, fmt.Errorf("invalid Kea lease file format") + } -func parse(contents string) (Leases, error) { - leases := Leases{} - var re = regexp.MustCompile(leaseRegex) - matches := re.FindAllStringSubmatch(contents, -1) + var leases Leases var errs []error - for _, m := range matches { - rm := make(map[string]string) - for i, name := range re.SubexpNames() { - if i != 0 && name != "" { - rm[name] = m[i] - } + + for { + record, err := reader.Read() + if err == io.EOF { + break } - begin, err := time.Parse(leaseDateFormat, rm["begin"]) if err != nil { - errs = append(errs, err) + line, _ := reader.FieldPos(0) + errs = append(errs, fmt.Errorf("line %d: failed to read CSV record: %v", line, err)) + continue + } + + if len(record) < 5 { + line, _ := reader.FieldPos(0) + errs = append(errs, fmt.Errorf("line %d: incomplete record, expected at least 5 fields, got %d", line, len(record))) + continue } - end, err := time.Parse(leaseDateFormat, rm["end"]) + + expireStr := strings.TrimSpace(record[4]) + expireTs, err := strconv.ParseInt(expireStr, 10, 64) if err != nil { - errs = append(errs, err) + line, col := reader.FieldPos(4) + errs = append(errs, fmt.Errorf("line %d, column %d: invalid expire timestamp '%s': %v", line, col, expireStr, err)) + continue + } + + ip := strings.TrimSpace(record[0]) + if ip == "" { + line, col := reader.FieldPos(0) + errs = append(errs, fmt.Errorf("line %d, column %d: empty Ip address", line, col)) + continue } - l := Lease{ - Mac: rm["mac"], - Ip: rm["ip"], - Begin: begin, - End: end, + mac := strings.TrimSpace(record[1]) + if mac == "" { + line, col := reader.FieldPos(1) + errs = append(errs, fmt.Errorf("line %d, column %d: empty Mac address", line, col)) + continue } - leases = append(leases, l) + + lease := Lease{ + Mac: mac, + Ip: ip, + End: time.Unix(expireTs, 0), + } + leases = append(leases, lease) } + if len(errs) > 0 { return leases, errors.Join(errs...) } + return leases, nil } diff --git a/internal/leases/parser_test.go b/internal/leases/parser_test.go index eaad54b..6af5a56 100644 --- a/internal/leases/parser_test.go +++ b/internal/leases/parser_test.go @@ -1,6 +1,7 @@ package leases import ( + "strings" "testing" "time" @@ -8,51 +9,25 @@ import ( "github.com/stretchr/testify/require" ) -var sampleLeaseContent = ` -lease 192.168.2.27 { - starts 4 2019/06/27 13:30:21; - ends 4 2019/06/27 13:40:21; - cltt 4 2019/06/27 13:30:21; - binding state active; - next binding state free; - rewind binding state free; - hardware ethernet ac:1f:6b:35:ac:62; - uid "\001\254\037k5\254b"; - set vendor-class-identifier = "udhcp 1.23.1"; -} -lease 192.168.2.30 { - starts 4 2019/06/27 06:40:06; - ends 4 2019/06/27 06:50:06; - cltt 4 2019/06/27 06:40:06; - binding state active; - next binding state free; - rewind binding state free; - hardware ethernet ac:1f:6b:35:ab:2d; - uid "\001\254\037k5\253-"; - set vendor-class-identifier = "udhcp 1.23.1"; -} +var sampleLeaseContent = `address,hwaddr,client_id,valid_lifetime,expire,subnet_id,fqdn_fwd,fqdn_rev,hostname,state,user_context +192.168.2.27,ac:1f:6b:35:ac:62,01:ac:1f:6b:35:ac:62,3600,1593243021,1,0,0,,1, +192.168.2.30,ac:1f:6b:35:ab:2d,01:ac:1f:6b:35:ab:2d,3600,1593243006,1,0,0,,1, ` func TestParse(t *testing.T) { - l, err := parse(sampleLeaseContent) + l, err := parse(strings.NewReader(sampleLeaseContent)) require.NoError(t, err) - b, _ := time.Parse(leaseDateFormat, "2019/06/27 13:30:21") - e, _ := time.Parse(leaseDateFormat, "2019/06/27 13:40:21") lease1 := Lease{ - Mac: "ac:1f:6b:35:ac:62", - Ip: "192.168.2.27", - Begin: b, - End: e, + Mac: "ac:1f:6b:35:ac:62", + Ip: "192.168.2.27", + End: time.Unix(1593243021, 0), } - b, _ = time.Parse(leaseDateFormat, "2019/06/27 06:40:06") - e, _ = time.Parse(leaseDateFormat, "2019/06/27 06:50:06") lease2 := Lease{ - Mac: "ac:1f:6b:35:ab:2d", - Ip: "192.168.2.30", - Begin: b, - End: e, + Mac: "ac:1f:6b:35:ab:2d", + Ip: "192.168.2.30", + End: time.Unix(1593243006, 0), } assert.Equal(t, Leases{lease1, lease2}, l) diff --git a/internal/leases/types.go b/internal/leases/types.go index d5824fe..74f1d27 100644 --- a/internal/leases/types.go +++ b/internal/leases/types.go @@ -8,10 +8,9 @@ import ( ) type Lease struct { - Mac string - Ip string - Begin time.Time - End time.Time + Mac string + Ip string + End time.Time } type Leases []Lease diff --git a/kea-leases.csv b/kea-leases.csv new file mode 100644 index 0000000..2b1139d --- /dev/null +++ b/kea-leases.csv @@ -0,0 +1,3 @@ +address,hwaddr,client_id,valid_lifetime,expire,subnet_id,fqdn_fwd,fqdn_rev,hostname,state,user_context +192.168.2.27,ac:1f:6b:35:ac:62,01:ac:1f:6b:35:ac:62,3600,1593243021,1,0,0,,1, +192.168.2.30,ac:1f:6b:35:ab:2d,01:ac:1f:6b:35:ab:2d,3600,1593243006,1,0,0,,1, diff --git a/pkg/config/config.go b/pkg/config/config.go index cc896e9..ab9dfc8 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -12,7 +12,7 @@ type Config struct { PartitionID string `required:"true" desc:"set the partition ID" envconfig:"partition_id"` // ipmi details reporting parameters - LeaseFile string `required:"false" default:"/var/lib/dhcp/dhcpd.leases" desc:"the dhcp lease file to read" split_words:"true"` + LeaseFile string `required:"false" default:"/var/lib/kea/kea-leases.csv" desc:"the dhcp lease file to read" split_words:"true"` ReportInterval time.Duration `required:"false" default:"5m" desc:"the interval for periodical reports" split_words:"true"` MetalAPIURL *url.URL `required:"true" desc:"endpoint for the metal-api" envconfig:"metal_api_url"` MetalAPIHMACKey string `required:"true" desc:"the preshared key for the hmac calculation" envconfig:"metal_api_hmac_key"`