Skip to content

Commit 91b9eaa

Browse files
committed
Merge master
2 parents 61e5912 + 060d2ab commit 91b9eaa

File tree

12 files changed

+1384
-89
lines changed

12 files changed

+1384
-89
lines changed

.github/workflows/docker.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ jobs:
2929
password: ${{ secrets.DOCKER_REGISTRY_TOKEN }}
3030

3131
- name: Checkout
32-
uses: actions/checkout@v5
32+
uses: actions/checkout@v6
3333

3434
- name: Set up Go 1.25
3535
uses: actions/setup-go@v5
@@ -38,7 +38,7 @@ jobs:
3838
cache: false
3939

4040
- name: Lint
41-
uses: golangci/golangci-lint-action@v8
41+
uses: golangci/golangci-lint-action@v9
4242
with:
4343
args: --build-tags integration -D protogetter --timeout=3m
4444

go.mod

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ 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/api v0.0.40-0.20260116144635-78c74c55ac7e
910
github.com/metal-stack/go-hal v0.6.1
@@ -19,23 +20,31 @@ require (
1920
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20251209175733-2a1774d88802.1 // indirect
2021
connectrpc.com/connect v1.19.1 // indirect
2122
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
23+
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
2224
github.com/avast/retry-go/v4 v4.7.0 // indirect
2325
github.com/creack/pty v1.1.24 // indirect
2426
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
27+
github.com/go-openapi/errors v0.22.0 // indirect
28+
github.com/go-openapi/strfmt v0.23.0 // indirect
2529
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
2630
github.com/golang/snappy v1.0.0 // indirect
2731
github.com/google/uuid v1.6.0 // indirect
2832
github.com/klauspost/compress v1.18.3 // indirect
2933
github.com/klauspost/connect-compress/v2 v2.1.0 // indirect
3034
github.com/minio/minlz v1.0.1 // indirect
35+
github.com/mitchellh/mapstructure v1.5.0 // indirect
36+
github.com/oklog/ulid v1.3.1 // indirect
3137
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
3238
github.com/rogpeppe/go-internal v1.14.1 // indirect
3339
github.com/sethvargo/go-password v0.3.1 // indirect
3440
github.com/stmcginnis/gofish v0.20.0 // indirect
41+
github.com/stretchr/objx v0.5.3 // indirect
3542
github.com/vmware/goipmi v0.0.0-20181114221114-2333cd82d702 // indirect
43+
go.mongodb.org/mongo-driver v1.17.1 // indirect
3644
golang.org/x/net v0.49.0 // indirect
3745
golang.org/x/sys v0.40.0 // indirect
3846
golang.org/x/text v0.33.0 // indirect
3947
google.golang.org/protobuf v1.36.11 // indirect
48+
gopkg.in/inf.v0 v0.9.1 // indirect
4049
gopkg.in/yaml.v3 v3.0.1 // indirect
4150
)

go.sum

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ connectrpc.com/connect v1.19.1 h1:R5M57z05+90EfEvCY1b7hBxDVOUl45PrtXtAV2fOC14=
44
connectrpc.com/connect v1.19.1/go.mod h1:tN20fjdGlewnSFeZxLKb0xwIZ6ozc3OQs2hTXy4du9w=
55
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
66
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
7+
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
8+
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
79
github.com/avast/retry-go/v4 v4.7.0 h1:yjDs35SlGvKwRNSykujfjdMxMhMQQM0TnIjJaHB+Zio=
810
github.com/avast/retry-go/v4 v4.7.0/go.mod h1:ZMPDa3sY2bKgpLtap9JRUgk2yTAba7cgiFhqxY2Sg6Q=
911
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
@@ -13,6 +15,10 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
1315
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
1416
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
1517
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
18+
github.com/go-openapi/errors v0.22.0 h1:c4xY/OLxUBSTiepAg3j/MHuAv5mJhnf53LLMWFB+u/w=
19+
github.com/go-openapi/errors v0.22.0/go.mod h1:J3DmZScxCDufmIMsdOuDHxJbdOGC0xtUynjIx092vXE=
20+
github.com/go-openapi/strfmt v0.23.0 h1:nlUS6BCqcnAk0pyhi9Y+kdDVZdZMHfEKQiS4HaMgO/c=
21+
github.com/go-openapi/strfmt v0.23.0/go.mod h1:NrtIpfKtWIygRkKVsxh7XQMDQW5HKQl6S5ik2elW+K4=
1622
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
1723
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
1824
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
@@ -42,8 +48,12 @@ github.com/metal-stack/v v1.0.3 h1:Sh2oBlnxrCUD+mVpzfC8HiqL045YWkxs0gpTvkjppqs=
4248
github.com/metal-stack/v v1.0.3/go.mod h1:YTahEu7/ishwpYKnp/VaW/7nf8+PInogkfGwLcGPdXg=
4349
github.com/minio/minlz v1.0.1 h1:OUZUzXcib8diiX+JYxyRLIdomyZYzHct6EShOKtQY2A=
4450
github.com/minio/minlz v1.0.1/go.mod h1:qT0aEB35q79LLornSzeDH75LBf3aH1MV+jB5w9Wasec=
51+
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
52+
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
4553
github.com/nsqio/go-nsq v1.1.0 h1:PQg+xxiUjA7V+TLdXw7nVrJ5Jbl3sN86EhGCQj4+FYE=
4654
github.com/nsqio/go-nsq v1.1.0/go.mod h1:vKq36oyeVXgsS5Q8YEO7WghqidAVXQlcFxzQbQTuDEY=
55+
github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
56+
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
4757
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
4858
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
4959
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@@ -53,11 +63,15 @@ github.com/sethvargo/go-password v0.3.1 h1:WqrLTjo7X6AcVYfC6R7GtSyuUQR9hGyAj/f1P
5363
github.com/sethvargo/go-password v0.3.1/go.mod h1:rXofC1zT54N7R8K/h1WDUdkf9BOx5OptoxrMBcrXzvs=
5464
github.com/stmcginnis/gofish v0.20.0 h1:hH2V2Qe898F2wWT1loApnkDUrXXiLKqbSlMaH3Y1n08=
5565
github.com/stmcginnis/gofish v0.20.0/go.mod h1:PzF5i8ecRG9A2ol8XT64npKUunyraJ+7t0kYMpQAtqU=
66+
github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=
67+
github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=
5668
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
5769
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
5870
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
5971
github.com/vmware/goipmi v0.0.0-20181114221114-2333cd82d702 h1:yx587LNBbOpIxzCBHBiI94Wx8ryIAFlu1w0lDwm64cA=
6072
github.com/vmware/goipmi v0.0.0-20181114221114-2333cd82d702/go.mod h1:YiWonbS/PuCtti3wt9jl+FvNEJ7c0nvmjGoEYxdjyk0=
73+
go.mongodb.org/mongo-driver v1.17.1 h1:Wic5cJIwJgSpBhe3lx3+/RybR5PiYRMpVFgO7cOHyIM=
74+
go.mongodb.org/mongo-driver v1.17.1/go.mod h1:wwWm/+BuOddhcq3n68LKRmgk2wXzmF6s0SFOa0GINL4=
6175
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
6276
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
6377
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
@@ -75,5 +89,7 @@ google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j
7589
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
7690
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
7791
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
92+
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
93+
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
7894
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
7995
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
apiv2 "github.com/metal-stack/api/go/metalstack/api/v2"
57
"github.com/metal-stack/go-hal"
68
"github.com/metal-stack/go-hal/connect"
79
halslog "github.com/metal-stack/go-hal/pkg/logger/slog"
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

@@ -66,7 +68,7 @@ func (i *ReportItem) EnrichWithBMCDetails(ipmiPort int, ipmiUser, ipmiPassword s
6668
str := u.String()
6769
i.UUID = &str
6870
} else {
69-
i.Log.Warn("could not determine uuid of device", "mac", i.Lease.Mac, "ip", i.Lease.Ip, "err", err)
71+
log.Warn("could not determine uuid of device", "mac", i.Lease.Mac, "ip", i.Lease.Ip, "err", err)
7072
return err
7173
}
7274
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)