Skip to content

Commit

Permalink
Add feature to specify the allowed dhcp cidrs (#51)
Browse files Browse the repository at this point in the history
  • Loading branch information
majst01 authored Sep 5, 2023
1 parent 53380e1 commit 543ab3c
Show file tree
Hide file tree
Showing 10 changed files with 143 additions and 90 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ require (
github.com/stretchr/testify v1.8.4
go.uber.org/zap v1.25.0
golang.org/x/crypto v0.12.0
golang.org/x/sync v0.3.0
)

require (
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,8 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
Expand Down
16 changes: 3 additions & 13 deletions internal/leases/leases.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package leases

import (
"fmt"
"os"
"time"
)
Expand Down Expand Up @@ -31,18 +30,9 @@ func (l Leases) LatestByMac() map[string]Lease {
}

func ReadLeases(leaseFile string) (Leases, error) {
leasesContent := mustRead(leaseFile)
leases, err := Parse(leasesContent)
leasesContent, err := os.ReadFile(leaseFile)
if err != nil {
return nil, fmt.Errorf("could not parse leases file:%w", err)
return nil, err
}
return leases, nil
}

func mustRead(name string) string {
c, err := os.ReadFile(name)
if err != nil {
panic(err)
}
return string(c)
return parse(string(leasesContent))
}
2 changes: 1 addition & 1 deletion internal/leases/leases_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (

func TestFilterActive(t *testing.T) {
assert := assert.New(t)
l, err := Parse(LEASES_CONTENT)
l, err := parse(sampleLeaseContent)
assert.NoError(err)
assert.Equal(Leases{}, l.FilterActive())
}
Expand Down
23 changes: 15 additions & 8 deletions internal/leases/parser.go
Original file line number Diff line number Diff line change
@@ -1,31 +1,35 @@
package leases

import (
"errors"
"regexp"
"time"
)

const DATE_FORMAT = "2006/01/02 15:04:05"
const LEASE_REGEX = `(?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\:]+);.*?}`
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(contents string) (Leases, error) {
func parse(contents string) (Leases, error) {
leases := Leases{}
var re = regexp.MustCompile(LEASE_REGEX)
var re = regexp.MustCompile(leaseRegex)
matches := re.FindAllStringSubmatch(contents, -1)
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]
}
}
begin, err := time.Parse(DATE_FORMAT, rm["begin"])
begin, err := time.Parse(leaseDateFormat, rm["begin"])
if err != nil {
panic(err)
errs = append(errs, err)
}
end, err := time.Parse(DATE_FORMAT, rm["end"])
end, err := time.Parse(leaseDateFormat, rm["end"])
if err != nil {
panic(err)
errs = append(errs, err)
}

l := Lease{
Expand All @@ -36,5 +40,8 @@ func Parse(contents string) (Leases, error) {
}
leases = append(leases, l)
}
if len(errs) > 0 {
return leases, errors.Join(errs...)
}
return leases, nil
}
12 changes: 6 additions & 6 deletions internal/leases/parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (
"github.com/stretchr/testify/assert"
)

var LEASES_CONTENT = `
var sampleLeaseContent = `
lease 192.168.2.27 {
starts 4 2019/06/27 13:30:21;
ends 4 2019/06/27 13:40:21;
Expand All @@ -34,20 +34,20 @@ lease 192.168.2.30 {

func TestParse(t *testing.T) {
assert := assert.New(t)
l, err := Parse(LEASES_CONTENT)
l, err := parse(sampleLeaseContent)
assert.NoError(err)

b, _ := time.Parse(DATE_FORMAT, "2019/06/27 13:30:21")
e, _ := time.Parse(DATE_FORMAT, "2019/06/27 13:40:21")
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,
}

b, _ = time.Parse(DATE_FORMAT, "2019/06/27 06:40:06")
e, _ = time.Parse(DATE_FORMAT, "2019/06/27 06:50:06")
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",
Expand Down
16 changes: 0 additions & 16 deletions internal/leases/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,3 @@ type ReportItem struct {
IndicatorLED *string
PowerMetric *models.V1PowerMetric
}

func NewReportItem(l Lease, log *zap.SugaredLogger) *ReportItem {
return &ReportItem{
Lease: l,
Log: log,
}
}

func (i *ReportItem) MacContainedIn(macs []string) bool {
for _, m := range macs {
if m == i.Mac {
return true
}
}
return false
}
136 changes: 94 additions & 42 deletions internal/reporter/reporter.go
Original file line number Diff line number Diff line change
@@ -1,97 +1,149 @@
package reporter

import (
"fmt"
"net/netip"
"os"
"os/signal"
"sync"
"slices"
"syscall"
"time"

"github.com/metal-stack/metal-bmc/domain"
"github.com/metal-stack/metal-bmc/internal/leases"
"github.com/metal-stack/metal-bmc/pkg/config"
metalgo "github.com/metal-stack/metal-go"
"github.com/metal-stack/metal-go/api/client/machine"
"github.com/metal-stack/metal-go/api/models"
"go.uber.org/zap"
"golang.org/x/sync/errgroup"
"golang.org/x/sync/semaphore"
)

// reporter reports information about bmc, bios and dhcp ip of bmc to metal-api
type reporter struct {
cfg *domain.Config
cfg *config.Config
log *zap.SugaredLogger
client metalgo.Client
sem *semaphore.Weighted
}

// New will create a reporter for MachineIpmiReports
func New(log *zap.SugaredLogger, cfg *domain.Config, client metalgo.Client) (*reporter, error) {
func New(log *zap.SugaredLogger, cfg *config.Config, client metalgo.Client) (*reporter, error) {
return &reporter{
cfg: cfg,
log: log,
client: client,
sem: semaphore.NewWeighted(1),
}, nil
}

func (r reporter) Run() {
periodic := time.NewTicker(r.cfg.ReportInterval)
signals := make(chan os.Signal, 1)
signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM)

for {
select {
case <-periodic.C:
ls, err := leases.ReadLeases(r.cfg.LeaseFile)
if err != nil {
r.log.Fatalw("could not parse leases file", "error", err)
}
active := ls.FilterActive()
byMac := active.LatestByMac()
r.log.Infow("reporting leases to metal-api", "all", len(ls), "active", len(active), "uniqueActive", len(byMac))

mtx := new(sync.Mutex)
var items []*leases.ReportItem

wg := new(sync.WaitGroup)
wg.Add(len(byMac))

for _, l := range byMac {
item := leases.NewReportItem(l, r.log)
go func() {
item.EnrichWithBMCDetails(r.cfg.IpmiPort, r.cfg.IpmiUser, r.cfg.IpmiPassword)
mtx.Lock()
items = append(items, item)
wg.Done()
mtx.Unlock()
}()
}

wg.Wait()

err = r.report(items)
err := r.collectAndReport()
if err != nil {
r.log.Warnw("could not report ipmi addresses", "error", err)
r.log.Errorw("collect and report", "error", err)
}
case <-signals:
return
}
}
}

func (r reporter) collectAndReport() error {
if !r.sem.TryAcquire(1) {
r.log.Warn("lease reporting is still running")
return nil
}
defer r.sem.Release(1)

start := time.Now()
ls, err := leases.ReadLeases(r.cfg.LeaseFile)
if err != nil {
r.log.Errorw("could not parse leases file, partial results will considered", "error", err)
}
if len(ls) == 0 {
r.log.Warn("empty leases returned, nothing to report")
return nil
}
active := ls.FilterActive()
byMac := active.LatestByMac()
r.log.Infow("consider reporting leases to metal-api", "all", len(ls), "active", len(active), "uniqueActive", len(byMac))

var items []*leases.ReportItem
for _, l := range byMac {
l := l
if !r.isInAllowedCidr(l.Ip) {
continue
}

if slices.Contains(r.cfg.IgnoreMacs, l.Mac) {
continue
}

item := &leases.ReportItem{
Lease: l,
Log: r.log,
}
items = append(items, item)
}
r.log.Infow("reporting leases to metal-api", "count", len(items))

g := new(errgroup.Group)
// Allow 20 goroutines run in parallel at max
g.SetLimit(20)
for _, item := range items {
item := item
g.Go(func() error {
item.EnrichWithBMCDetails(r.cfg.IpmiPort, r.cfg.IpmiUser, r.cfg.IpmiPassword)
return nil
})
}
err = g.Wait()
if err != nil {
r.log.Errorw("could not enrich all ipmi details", "error", err)
}

err = r.report(items)
if err != nil {
return fmt.Errorf("could not report ipmi addresses %w", err)
}
r.log.Infow("reporting leases to metal-api", "took", time.Since(start))
return nil
}

func (r reporter) isInAllowedCidr(ip string) bool {
parsedIP, err := netip.ParseAddr(ip)
if err != nil {
r.log.Errorw("given ip is not parsable", "ip", ip, "error", err)
return false
}
for _, cidr := range r.cfg.AllowedCidrs {
cidr := cidr
pfx, err := netip.ParsePrefix(cidr)
if err != nil {
return false
}
if pfx.Contains(parsedIP) {
return true
}
}
return false
}

// report will send all gathered information about machines to the metal-api
func (r reporter) report(items []*leases.ReportItem) error {
partitionID := r.cfg.PartitionID
reports := make(map[string]models.V1MachineIpmiReport)

for _, item := range items {
item := item
mac := item.Mac

if item.MacContainedIn(r.cfg.IgnoreMacs) {
continue
}

ip := item.Ip
if item.UUID == nil {
r.log.Errorw("could not determine uuid of device", "mac", mac, "ip", ip)
r.log.Errorw("could not determine uuid of device", "mac", item.Mac, "ip", item.Ip)
continue
}

Expand Down
8 changes: 6 additions & 2 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ package main
import (
"fmt"

"github.com/metal-stack/metal-bmc/domain"
"github.com/metal-stack/metal-bmc/internal/bmc"
"github.com/metal-stack/metal-bmc/pkg/config"
metalgo "github.com/metal-stack/metal-go"

"github.com/metal-stack/metal-bmc/internal/reporter"
Expand All @@ -16,11 +16,15 @@ import (
)

func main() {
var cfg domain.Config
var cfg config.Config
if err := envconfig.Process("METAL_BMC", &cfg); err != nil {
panic(fmt.Errorf("bad configuration: %w", err))
}

if err := cfg.Validate(); err != nil {
panic(fmt.Errorf("bad configuration: %w", err))
}

level, err := zap.ParseAtomicLevel(cfg.LogLevel)
if err != nil {
panic(fmt.Errorf("can't initialize zap logger: %w", err))
Expand Down
Loading

0 comments on commit 543ab3c

Please sign in to comment.