Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
0984e17
Migrate to Kea DHCPv4 Server
robertvolkmann Jul 18, 2025
75ac449
Merge branch 'master' into kea-dhcpv4
robertvolkmann Jul 18, 2025
4120e64
Ignore file close error
robertvolkmann Jul 18, 2025
1d54092
Merge remote-tracking branch 'origin/gigabyte' into gigabyte-helper-b…
simcod Jul 18, 2025
afe1fe4
Merge remote-tracking branch 'origin/gigabyte' into gigabyte-helper-b…
simcod Jul 18, 2025
52d9f1e
Remove TLS to simplify testing
robertvolkmann Jul 18, 2025
fd741d1
Remove unneeded host key for testing
robertvolkmann Jul 18, 2025
b2661b0
Build gigabyte helper branch
robertvolkmann Jul 18, 2025
920813e
Merge branch 'gigabyte' into gigabyte-helper-branch
simcod Jul 24, 2025
cb80d75
Merge branch 'gigabyte' into gigabyte-helper-branch
simcod Jul 28, 2025
c6c1d33
Return possible error message
simcod Jul 28, 2025
328bde0
Merge branch 'gigabyte' into gigabyte-helper-branch
simcod Jul 29, 2025
7e75635
Merge branch 'gigabyte' into gigabyte-helper-branch
simcod Jul 29, 2025
ac55a0b
Merge branch 'gigabyte' into gigabyte-helper-branch
simcod Jul 29, 2025
c666e5c
Merge branch 'gigabyte' into gigabyte-helper-branch
simcod Jul 29, 2025
27dfac3
Merge branch 'gigabyte' into gigabyte-helper-branch
simcod Jul 29, 2025
42e045b
Merge branch 'gigabyte' into gigabyte-helper-branch
simcod Jul 29, 2025
74c2ec0
Merge branch 'gigabyte' into gigabyte-helper-branch
simcod Aug 5, 2025
fc359a2
Merge branch 'gigabyte' into gigabyte-helper-branch
simcod Aug 6, 2025
9af803c
Merge branch 'gigabyte' into gigabyte-helper-branch
simcod Aug 6, 2025
220ced0
Merge branch 'gigabyte' into gigabyte-helper-branch
simcod Aug 6, 2025
8afc720
Merge branch 'gigabyte' into gigabyte-helper-branch
simcod Aug 6, 2025
0502a25
Merge branch 'gigabyte' into gigabyte-helper-branch
simcod Aug 6, 2025
12e44c7
Merge branch 'gigabyte' into gigabyte-helper-branch
simcod Aug 6, 2025
eaa7cb0
Merge branch 'gigabyte' into gigabyte-helper-branch
simcod Aug 7, 2025
65616c1
Merge branch 'gigabyte' into gigabyte-helper-branch
simcod Aug 7, 2025
f5172a5
Merge branch 'gigabyte' into gigabyte-helper-branch
simcod Aug 7, 2025
6564b3c
Merge branch 'gigabyte' into gigabyte-helper-branch
simcod Aug 7, 2025
ebfce0e
Merge branch 'gigabyte' into gigabyte-helper-branch
simcod Aug 7, 2025
9cec643
Merge branch 'gigabyte' into gigabyte-helper-branch
simcod Aug 7, 2025
d028d82
Merge branch 'gigabyte' into gigabyte-helper-branch
simcod Aug 19, 2025
3a9314e
Merge branch 'gigabyte' into gigabyte-helper-branch
simcod Aug 20, 2025
653822d
Merge branch 'gigabyte' into gigabyte-helper-branch
simcod Sep 4, 2025
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
5 changes: 1 addition & 4 deletions .github/workflows/docker.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,10 @@ name: Docker Build Action
on:
pull_request:
branches:
- master
- gigabyte
release:
types:
- published
push:
branches:
- master

env:
REGISTRY: ghcr.io
Expand Down
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"

53 changes: 8 additions & 45 deletions internal/bmc/console.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
package bmc

import (
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"io"
"log/slog"
"os"
"net"
"strconv"
"strings"

Expand All @@ -18,54 +16,20 @@ import (
"github.com/metal-stack/metal-go/api/client/machine"

"github.com/gliderlabs/ssh"
gossh "golang.org/x/crypto/ssh"
)

type console struct {
log *slog.Logger
tlsConfig *tls.Config
port int
hostKey gossh.Signer
client metalgo.Client
log *slog.Logger
port int
client metalgo.Client
}

func NewConsole(log *slog.Logger, client metalgo.Client, c config.Config) (*console, error) {

caCert, err := os.ReadFile(c.ConsoleCACertFile)
if err != nil {
return nil, fmt.Errorf("failed to load cert: %w", err)
}

caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)

cert, err := tls.LoadX509KeyPair(c.ConsoleCertFile, c.ConsoleKeyFile)
if err != nil {
return nil, err
}

tlsConfig := &tls.Config{
Certificates: []tls.Certificate{cert}, // server certificate which is validated by the client
ClientCAs: caCertPool, // used to verify the client cert is signed by the CA and is therefore valid
ClientAuth: tls.RequireAndVerifyClientCert, // this requires a valid client certificate to be supplied during handshake
MinVersion: tls.VersionTLS13,
}

bb, err := os.ReadFile(c.ConsoleKeyFile)
if err != nil {
return nil, fmt.Errorf("failed to load ssh server key:%w", err)
}
hostKey, err := gossh.ParsePrivateKey(bb)
if err != nil {
return nil, fmt.Errorf("failed to parse ssh server key:%w", err)
}

return &console{
log: log,
tlsConfig: tlsConfig,
port: c.ConsolePort,
hostKey: hostKey,
client: client,
log: log,
port: c.ConsolePort,
client: client,
}, nil
}

Expand All @@ -74,9 +38,8 @@ func (c *console) ListenAndServe() error {
s := &ssh.Server{
Handler: c.sessionHandler,
}
s.AddHostKey(c.hostKey)
addr := fmt.Sprintf(":%d", c.port)
listener, err := tls.Listen("tcp", addr, c.tlsConfig)
listener, err := net.Listen("tcp", addr)
if err != nil {
return fmt.Errorf("failed to create listener: %w", err)
}
Expand Down
40 changes: 7 additions & 33 deletions internal/bmc/nsq.go
Original file line number Diff line number Diff line change
@@ -1,51 +1,21 @@
package bmc

import (
"crypto/tls"
"crypto/x509"
"encoding/json"
"fmt"
"os"
"time"

"github.com/metal-stack/go-hal"
"github.com/nsqio/go-nsq"

"github.com/metal-stack/go-hal"
)

const (
mqChannel = "core"
)

func (b *BMCService) InitConsumer() error {
caCertRaw, err := os.ReadFile(b.mqCACertFile)
if err != nil {
return fmt.Errorf("failed to read ca cert: %w", err)
}

caCertPool, err := x509.SystemCertPool()
if err != nil {
return err
}

ok := caCertPool.AppendCertsFromPEM(caCertRaw)
if !ok {
return fmt.Errorf("unable to add ca to cert pool")
}

cert, err := tls.LoadX509KeyPair(b.mqClientCertFile, b.mqClientCertKeyFile)
if err != nil {
return err
}

config := nsq.NewConfig()
config.TlsConfig = &tls.Config{
Certificates: []tls.Certificate{cert},
ClientCAs: caCertPool,
RootCAs: caCertPool,
ClientAuth: tls.RequireAndVerifyClientCert,
MinVersion: tls.VersionTLS12,
}
config.TlsV1 = true

// Deadlines for network reads and writes
config.ReadTimeout = 10 * time.Second
Expand Down Expand Up @@ -131,7 +101,11 @@ func (b *BMCService) HandleMessage(message *nsq.Message) error {
}
return outBand.PowerCycle()
case ChassisIdentifyLEDOnCmd:
return outBand.IdentifyLEDOn()
err := outBand.IdentifyLEDOn()
if err != nil {
return err
}
return nil
case ChassisIdentifyLEDOffCmd:
return outBand.IdentifyLEDOff()
case UpdateFirmwareCmd:
Expand Down
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
}
Loading