Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 0 additions & 22 deletions dhcpd.leases

This file was deleted.

5 changes: 2 additions & 3 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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"

9 changes: 6 additions & 3 deletions internal/leases/leases.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
3 changes: 2 additions & 1 deletion internal/leases/leases_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package leases

import (
"strings"
"testing"
"time"

Expand All @@ -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())
}
Expand Down
83 changes: 58 additions & 25 deletions internal/leases/parser.go
Original file line number Diff line number Diff line change
@@ -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<ip>[^\s]+)\s+{.*?starts\s\d+\s(?P<begin>[\d\/]+\s[\d\:]+);.*?ends\s\d+\s(?P<end>[\d\/]+\s[\d\:]+);.*?hardware\sethernet\s(?P<mac>[\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
}
47 changes: 11 additions & 36 deletions internal/leases/parser_test.go
Original file line number Diff line number Diff line change
@@ -1,58 +1,33 @@
package leases

import (
"strings"
"testing"
"time"

"github.com/stretchr/testify/assert"
"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)
Expand Down
7 changes: 3 additions & 4 deletions internal/leases/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions kea-leases.csv
Original file line number Diff line number Diff line change
@@ -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,
2 changes: 1 addition & 1 deletion pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down