Skip to content

Commit 5737f57

Browse files
authored
Fix regex parsing of lease file mixing up lease entries (#90)
1 parent e05ebe0 commit 5737f57

File tree

11 files changed

+1365
-88
lines changed

11 files changed

+1365
-88
lines changed

go.mod

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@ go 1.25
44

55
require (
66
github.com/gliderlabs/ssh v0.3.8
7+
github.com/google/go-cmp v0.7.0
78
github.com/kelseyhightower/envconfig v1.4.0
89
github.com/metal-stack/go-hal v0.6.0
910
github.com/metal-stack/metal-go v0.42.1
11+
github.com/metal-stack/metal-lib v0.23.4
1012
github.com/metal-stack/v v1.0.3
1113
github.com/nsqio/go-nsq v1.1.0
1214
github.com/stretchr/testify v1.11.1
@@ -49,7 +51,6 @@ require (
4951
github.com/lestrrat-go/jwx/v2 v2.1.6 // indirect
5052
github.com/lestrrat-go/option v1.0.1 // indirect
5153
github.com/mailru/easyjson v0.9.0 // indirect
52-
github.com/metal-stack/metal-lib v0.23.4 // indirect
5354
github.com/metal-stack/security v0.9.4 // indirect
5455
github.com/mitchellh/mapstructure v1.5.0 // indirect
5556
github.com/oklog/ulid v1.3.1 // indirect
@@ -58,6 +59,7 @@ require (
5859
github.com/segmentio/asm v1.2.0 // indirect
5960
github.com/sethvargo/go-password v0.3.1 // indirect
6061
github.com/stmcginnis/gofish v0.20.0 // indirect
62+
github.com/stretchr/objx v0.5.2 // indirect
6163
github.com/vmware/goipmi v0.0.0-20181114221114-2333cd82d702 // indirect
6264
go.mongodb.org/mongo-driver v1.17.4 // indirect
6365
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
@@ -68,5 +70,6 @@ require (
6870
golang.org/x/oauth2 v0.30.0 // indirect
6971
golang.org/x/sys v0.35.0 // indirect
7072
golang.org/x/text v0.28.0 // indirect
73+
gopkg.in/inf.v0 v0.9.1 // indirect
7174
gopkg.in/yaml.v3 v3.0.1 // indirect
7275
)

go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,8 @@ github.com/sethvargo/go-password v0.3.1/go.mod h1:rXofC1zT54N7R8K/h1WDUdkf9BOx5O
112112
github.com/stmcginnis/gofish v0.20.0 h1:hH2V2Qe898F2wWT1loApnkDUrXXiLKqbSlMaH3Y1n08=
113113
github.com/stmcginnis/gofish v0.20.0/go.mod h1:PzF5i8ecRG9A2ol8XT64npKUunyraJ+7t0kYMpQAtqU=
114114
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
115+
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
116+
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
115117
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
116118
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
117119
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
@@ -149,6 +151,8 @@ golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
149151
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
150152
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
151153
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
154+
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
155+
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
152156
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
153157
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
154158
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

internal/leases/bmc.go

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
package leases
22

33
import (
4+
"log/slog"
5+
46
"github.com/metal-stack/go-hal"
57
"github.com/metal-stack/go-hal/connect"
68
halslog "github.com/metal-stack/go-hal/pkg/logger/slog"
79
"github.com/metal-stack/metal-go/api/models"
810
)
911

10-
func (i *ReportItem) EnrichWithBMCDetails(ipmiPort int, ipmiUser, ipmiPassword string) error {
11-
ob, err := connect.OutBand(i.Lease.Ip, ipmiPort, ipmiUser, ipmiPassword, halslog.New(i.Log))
12+
func (i *ReportItem) EnrichWithBMCDetails(log *slog.Logger, ipmiPort int, ipmiUser, ipmiPassword string) error {
13+
ob, err := connect.OutBand(i.Lease.Ip, ipmiPort, ipmiUser, ipmiPassword, halslog.New(log))
1214
if err != nil {
13-
i.Log.Error("could not establish outband connection to device bmc", "mac", i.Lease.Mac, "ip", i.Lease.Ip, "err", err)
15+
log.Error("could not establish outband connection to device bmc", "mac", i.Lease.Mac, "ip", i.Lease.Ip, "err", err)
1416
return err
1517
}
1618

@@ -28,7 +30,7 @@ func (i *ReportItem) EnrichWithBMCDetails(ipmiPort int, ipmiUser, ipmiPassword s
2830
ProductSerial: bmcDetails.ProductSerial,
2931
}
3032
} else {
31-
i.Log.Warn("could not retrieve bmc details of device", "mac", i.Lease.Mac, "ip", i.Lease.Ip, "err", err)
33+
log.Warn("could not retrieve bmc details of device", "mac", i.Lease.Mac, "ip", i.Lease.Ip, "err", err)
3234
return err
3335
}
3436

@@ -68,7 +70,7 @@ func (i *ReportItem) EnrichWithBMCDetails(ipmiPort int, ipmiUser, ipmiPassword s
6870
str := u.String()
6971
i.UUID = &str
7072
} else {
71-
i.Log.Warn("could not determine uuid of device", "mac", i.Lease.Mac, "ip", i.Lease.Ip, "err", err)
73+
log.Warn("could not determine uuid of device", "mac", i.Lease.Mac, "ip", i.Lease.Ip, "err", err)
7274
return err
7375
}
7476
return nil

internal/leases/leases.go

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package leases
22

33
import (
4+
"fmt"
5+
"log/slog"
46
"os"
57
"time"
68
)
@@ -29,10 +31,16 @@ func (l Leases) LatestByMac() map[string]Lease {
2931
return byMac
3032
}
3133

32-
func ReadLeases(leaseFile string) (Leases, error) {
33-
leasesContent, err := os.ReadFile(leaseFile)
34+
func ReadLeases(log *slog.Logger, leaseFilePath string) (Leases, error) {
35+
data, err := os.ReadFile(leaseFilePath)
3436
if err != nil {
3537
return nil, err
3638
}
37-
return parse(string(leasesContent))
39+
40+
leases, err := parseLeasesFile(log, string(data))
41+
if err != nil {
42+
return nil, fmt.Errorf("unable to parse lease file: %w", err)
43+
}
44+
45+
return leases, nil
3846
}

internal/leases/leases_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package leases
22

33
import (
4+
"log/slog"
45
"testing"
56
"time"
67

@@ -9,7 +10,7 @@ import (
910
)
1011

1112
func TestFilterActive(t *testing.T) {
12-
l, err := parse(sampleLeaseContent)
13+
l, err := parseLeasesFile(slog.Default(), sampleLeaseContent)
1314
require.NoError(t, err)
1415
assert.Equal(t, Leases{}, l.FilterActive())
1516
}

internal/leases/parser.go

Lines changed: 105 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,122 @@
11
package leases
22

33
import (
4-
"errors"
5-
"regexp"
4+
"fmt"
5+
"log/slog"
6+
"net/netip"
7+
"strings"
68
"time"
79
)
810

911
const (
1012
leaseDateFormat = "2006/01/02 15:04:05"
11-
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\:]+);.*?}`
1213
)
1314

14-
func parse(contents string) (Leases, error) {
15-
leases := Leases{}
16-
var re = regexp.MustCompile(leaseRegex)
17-
matches := re.FindAllStringSubmatch(contents, -1)
18-
var errs []error
19-
for _, m := range matches {
20-
rm := make(map[string]string)
21-
for i, name := range re.SubexpNames() {
22-
if i != 0 && name != "" {
23-
rm[name] = m[i]
24-
}
25-
}
26-
begin, err := time.Parse(leaseDateFormat, rm["begin"])
27-
if err != nil {
28-
errs = append(errs, err)
29-
}
30-
end, err := time.Parse(leaseDateFormat, rm["end"])
31-
if err != nil {
32-
errs = append(errs, err)
15+
func parseLeasesFile(log *slog.Logger, data string) (Leases, error) {
16+
var (
17+
leases Leases
18+
current *Lease
19+
)
20+
21+
for i, line := range strings.Split(data, "\n") {
22+
line = strings.TrimSpace(line)
23+
24+
tokens := strings.Fields(line)
25+
if len(tokens) == 0 {
26+
continue
3327
}
3428

35-
l := Lease{
36-
Mac: rm["mac"],
37-
Ip: rm["ip"],
38-
Begin: begin,
39-
End: end,
29+
switch tokens[0] {
30+
case "lease":
31+
// lease 1.2.3.4 {
32+
if len(tokens) != 3 {
33+
return nil, fmt.Errorf(`expecting "lease <ip> {" on line %d, got: %s`, i+1, line)
34+
}
35+
36+
if tokens[2] != "{" {
37+
return nil, fmt.Errorf("missing opening brace on line %d: %s", i+1, line)
38+
}
39+
40+
if _, err := netip.ParseAddr(tokens[1]); err != nil {
41+
return nil, fmt.Errorf("invalid ip address on line %d: %w", i+1, err)
42+
}
43+
44+
current = &Lease{
45+
Ip: tokens[1],
46+
}
47+
48+
case "}":
49+
if current == nil {
50+
return nil, fmt.Errorf("unexpected closing brace on line %d: %s", i+1, line)
51+
}
52+
53+
switch {
54+
case current.Begin.IsZero(), current.End.IsZero():
55+
log.Warn("incomplete lease entry (missing begin and end time), skipping entry", "line", i+1)
56+
continue
57+
case current.Ip == "":
58+
log.Warn("incomplete lease entry (missing ip address), skipping entry", "line", i+1)
59+
continue
60+
case current.Mac == "":
61+
log.Warn("incomplete lease entry (missing mac address), skipping entry", "line", i+1)
62+
continue
63+
default:
64+
leases = append(leases, *current)
65+
current = nil
66+
}
67+
68+
case "starts", "ends":
69+
// starts 5 2026/01/09 12:35:39;
70+
if current == nil {
71+
return nil, fmt.Errorf("unexpected date field on line %d: %s", i+1, line)
72+
}
73+
74+
if len(tokens) != 4 {
75+
return nil, fmt.Errorf(`expecting "%s <whatever-number> <date> <time>;" on line %d, got: %s`, tokens[0], i+1, line)
76+
}
77+
78+
if !strings.HasSuffix(tokens[3], ";") {
79+
return nil, fmt.Errorf("missing semicolon on line %d: %s", i+1, line)
80+
}
81+
82+
tokens[3] = strings.TrimRight(tokens[3], ";")
83+
84+
t, err := time.Parse(leaseDateFormat, tokens[2]+" "+tokens[3])
85+
if err != nil {
86+
return nil, fmt.Errorf("invalid time format on line %d: %w", i+1, err)
87+
}
88+
89+
if tokens[0] == "starts" {
90+
current.Begin = t
91+
} else {
92+
current.End = t
93+
}
94+
95+
case "hardware":
96+
// hardware ethernet 50:7c:6f:3e:8d:59;
97+
if current == nil {
98+
return nil, fmt.Errorf("unexpected hardware field on line %d: %s", i+1, line)
99+
}
100+
101+
if len(tokens) != 3 {
102+
return nil, fmt.Errorf(`expecting "hardware ethernet <mac>;" on line %d, got: %s`, i+1, line)
103+
}
104+
105+
if tokens[1] != "ethernet" {
106+
continue
107+
}
108+
109+
if !strings.HasSuffix(tokens[2], ";") {
110+
return nil, fmt.Errorf("missing semicolon on line %d: %s", i+1, line)
111+
}
112+
113+
current.Mac = strings.TrimRight(tokens[2], ";")
40114
}
41-
leases = append(leases, l)
42115
}
43-
if len(errs) > 0 {
44-
return leases, errors.Join(errs...)
116+
117+
if current != nil {
118+
return nil, fmt.Errorf("lease entry was not closed")
45119
}
120+
46121
return leases, nil
47122
}

0 commit comments

Comments
 (0)