Skip to content
Draft
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
a69a434
wip
iljarotar Aug 21, 2026
9c3ea28
merge
iljarotar Aug 27, 2026
65e8136
pin api
iljarotar Aug 27, 2026
5ec5ddb
Merge branch 'main' of github.com:metal-stack/metal-apiserver into ex…
iljarotar Aug 31, 2026
3e3d262
list external members
iljarotar Aug 31, 2026
ce46444
add external member
iljarotar Sep 1, 2026
20de5bd
remove external member
iljarotar Sep 2, 2026
8b81e9c
fix test
iljarotar Sep 2, 2026
868f2c4
fix integer pool test
iljarotar Sep 2, 2026
0f17c9b
Revert "fix test"
iljarotar Sep 2, 2026
25c7f1e
fix network test
iljarotar Sep 2, 2026
1ce01f1
fix tokens again
iljarotar Sep 2, 2026
e059711
remove debug log
iljarotar Sep 2, 2026
b17e252
pin api
iljarotar Sep 2, 2026
108fe3a
add switch nic memberships
iljarotar Sep 3, 2026
95c1963
use explicit memberships for external network members
iljarotar Sep 3, 2026
af95e36
merge
iljarotar Sep 4, 2026
87ef703
use test logger
iljarotar Sep 4, 2026
1df592f
fix register test
iljarotar Sep 4, 2026
438114b
fix update nics test
iljarotar Sep 4, 2026
6ef5367
fix connect machine with switches test
iljarotar Sep 7, 2026
29f006f
fix switch service tests
iljarotar Sep 7, 2026
4756b81
Do not test infra twice
majst01 Sep 8, 2026
0597945
fix machine connections test
iljarotar Sep 8, 2026
6425b78
fix boot service tests
iljarotar Sep 8, 2026
6c8fbbf
reverse network type validation condition
iljarotar Sep 8, 2026
0c5369b
fix switch repo tests
iljarotar Sep 8, 2026
0b2cd26
validate machine can't connect to external ports
iljarotar Sep 8, 2026
9a69401
make ports for deleted machines unmanaged
iljarotar Sep 8, 2026
bfc386b
return network
iljarotar Sep 8, 2026
8b67321
fix network service test
iljarotar Sep 8, 2026
b4e5ee6
format
iljarotar Sep 8, 2026
d8485fb
return network and members from all network member funcs
iljarotar Sep 9, 2026
06505b3
also return rack and partition of each member
iljarotar Sep 9, 2026
136e5d3
rack is not a pointer
iljarotar Sep 9, 2026
6af7174
merge
iljarotar Sep 9, 2026
1ff4ab7
update nic memberships during heartbeat
iljarotar Sep 9, 2026
bbcb414
default metal nic memberships
iljarotar Sep 9, 2026
d28ab79
quick fix vrf
iljarotar Sep 9, 2026
8ce41d4
set membership when not set but allow unspecified in convert funcs
iljarotar Sep 10, 2026
2104b8d
make bgp neighbor optional
iljarotar Sep 10, 2026
9e89809
merge
iljarotar Sep 10, 2026
b828753
fix tests
iljarotar Sep 10, 2026
7ba80d0
fix test
iljarotar Sep 10, 2026
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
2 changes: 1 addition & 1 deletion cmd/server/datastore-cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ func newDatastoreCmd() *cli.Command {
return fmt.Errorf("unable to create logger %w", err)
}

err = generic.Initialize(
_, err = generic.Initialize(
ctx,
log.WithGroup("datastore"),
rethinkdb.ConnectOpts{
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ require (
github.com/lestrrat-go/jwx/v4 v4.4.0
github.com/looplab/fsm v1.0.3
github.com/markbates/goth v1.82.0
github.com/metal-stack/api v0.5.3
github.com/metal-stack/api v0.5.4-0.20260831144703-b85c7f5a5a62
Comment thread
iljarotar marked this conversation as resolved.
Outdated
github.com/metal-stack/go-ipam v1.15.2
github.com/metal-stack/metal-lib v0.26.3
github.com/metal-stack/tenant-api v0.2.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -375,8 +375,8 @@ github.com/mdlayher/sdnotify v1.0.0 h1:Ma9XeLVN/l0qpyx1tNeMSeTjCPH6NtuD6/N9XdTlQ
github.com/mdlayher/sdnotify v1.0.0/go.mod h1:HQUmpM4XgYkhDLtd+Uad8ZFK1T9D5+pNxnXQjCeJlGE=
github.com/mdlayher/socket v0.6.1 h1:M7uj2NtuujUY4mYr1C57NmfNiRHbkKpnBxO856lsc3A=
github.com/mdlayher/socket v0.6.1/go.mod h1:+/SGtqc9V+5dAuRgQsU0fGBI+oRDiW7O2Obx10OIWfg=
github.com/metal-stack/api v0.5.3 h1:H6dh71kGvvL9z6X03A31LTIaiJHFe8L6y+xgtLcqVDg=
github.com/metal-stack/api v0.5.3/go.mod h1:U8c+awSMxXaRJjzWpo1IMcvlNxOi1ewu/fk1BzDM7hI=
github.com/metal-stack/api v0.5.4-0.20260831144703-b85c7f5a5a62 h1:KjeZSdjc5WjFelOIS3Hzra9EahbS3zzVdM16RgyfaYI=
github.com/metal-stack/api v0.5.4-0.20260831144703-b85c7f5a5a62/go.mod h1:U8c+awSMxXaRJjzWpo1IMcvlNxOi1ewu/fk1BzDM7hI=
github.com/metal-stack/go-ipam v1.15.2 h1:5okodNdZzGhSM8dGnxto36L1jacES8EcGNNJC9qXYWw=
github.com/metal-stack/go-ipam v1.15.2/go.mod h1:MKxv0M2h0T853h+MycmXLT2RmHVvfkjHrvQX/bX7Rl4=
github.com/metal-stack/goth v0.1.0 h1:sdadAH9QG+xAjLNKAJq8+esfXct6icTs58juoR4BKrQ=
Expand Down
28 changes: 14 additions & 14 deletions pkg/db/generic/initialize.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,12 @@ func AsnPoolRange(min, max uint) dataStoreOption {
}
}

func Initialize(ctx context.Context, log *slog.Logger, opts r.ConnectOpts, dsOpts ...dataStoreOption) error {
func Initialize(ctx context.Context, log *slog.Logger, opts r.ConnectOpts, dsOpts ...dataStoreOption) (*datastore, error) {
db := r.DB(opts.Database)

session, err := r.Connect(opts)
if err != nil {
return fmt.Errorf("unable to connect to database: %w", err)
return nil, fmt.Errorf("unable to connect to database: %w", err)
}

log.Info("creating / updating runtime user metal")
Expand All @@ -46,7 +46,7 @@ func Initialize(ctx context.Context, log *slog.Logger, opts r.ConnectOpts, dsOpt
Conflict: "update",
}).RunWrite(session, r.RunOpts{Context: ctx})
if err != nil {
return fmt.Errorf("unable to ensure runtime user metal: %w", err)
return nil, fmt.Errorf("unable to ensure runtime user metal: %w", err)
}

log.Info("initializing database", "database", opts.Database)
Expand All @@ -55,42 +55,42 @@ func Initialize(ctx context.Context, log *slog.Logger, opts r.ConnectOpts, dsOpt
return r.Branch(row, nil, r.DBCreate(opts.Database))
}).Exec(session, r.ExecOpts{Context: ctx})
if err != nil {
return fmt.Errorf("cannot create database: %w", err)
return nil, fmt.Errorf("cannot create database: %w", err)
}

log.Info("ensuring demoted user can read and write")

_, err = db.Grant(demotedUser, map[string]any{"read": true, "write": true}).RunWrite(session, r.RunOpts{Context: ctx})
if err != nil {
return fmt.Errorf("unable to grant read / write permissions to metal user on database %s: %w", opts.Database, err)
return nil, fmt.Errorf("unable to grant read / write permissions to metal user on database %s: %w", opts.Database, err)
}
_, err = r.DB("rethinkdb").Grant(demotedUser, map[string]any{"read": true}).RunWrite(session, r.RunOpts{Context: ctx})
if err != nil {
return fmt.Errorf("unable to grant read / write permissions to metal user on rethinkdb database: %w", err)
return nil, fmt.Errorf("unable to grant read / write permissions to metal user on rethinkdb database: %w", err)
}

log.Info("initializing tables")

ds, err := New(log, opts, dsOpts...)
if err != nil {
return fmt.Errorf("unable to create datastore: %w", err)
return nil, fmt.Errorf("unable to create datastore: %w", err)
}

ds.queryExecutor = session // the metal user cannot create tables

err = ds.createTable(ctx, migrationTableName)
if err != nil {
return fmt.Errorf("cannot create migration table: %w", err)
return nil, fmt.Errorf("cannot create migration table: %w", err)
}

err = ds.createTable(ctx, sharedMutexTableName)
if err != nil {
return fmt.Errorf("cannot create shared mutex table: %w", err)
return nil, fmt.Errorf("cannot create shared mutex table: %w", err)
}

for _, tableName := range ds.tableNames {
if err := ds.createTable(ctx, tableName); err != nil {
return fmt.Errorf("cannot create %s table: %w", tableName, err)
return nil, fmt.Errorf("cannot create %s table: %w", tableName, err)
}
}

Expand All @@ -99,7 +99,7 @@ func Initialize(ctx context.Context, log *slog.Logger, opts r.ConnectOpts, dsOpt
// be graceful after table creation and wait until ready
res, err := db.Wait().Run(session, r.RunOpts{Context: ctx})
if err != nil {
return fmt.Errorf("unable to wait for database creation")
return nil, fmt.Errorf("unable to wait for database creation")
}
defer func() {
if err := res.Close(); err != nil {
Expand All @@ -110,15 +110,15 @@ func Initialize(ctx context.Context, log *slog.Logger, opts r.ConnectOpts, dsOpt
ds.log.Info("initializing pools")

if err := ds.asnPool.initialize(); err != nil {
return fmt.Errorf("unable to initialize asn pool: %w", err)
return nil, fmt.Errorf("unable to initialize asn pool: %w", err)
}
if err := ds.vrfPool.initialize(); err != nil {
return fmt.Errorf("unable to initialize vrf pool: %w", err)
return nil, fmt.Errorf("unable to initialize vrf pool: %w", err)
}

ds.log.Info("database init complete")

return nil
return ds, nil
}

func (ds *datastore) createTable(ctx context.Context, tableName string) error {
Expand Down
2 changes: 2 additions & 0 deletions pkg/db/generic/integer_pool.go
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,8 @@ func (ip *integerPool) genericAcquire(ctx context.Context, term *r.Term) (uint,
return 0, err
}

fmt.Printf("############### %v", count)
Comment thread
majst01 marked this conversation as resolved.
Outdated

if count <= 0 {
return 0, errorutil.Internal("acquisition of a value failed for exhausted pool")
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/db/generic/integer_pool_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ func Test_AcquireAndReleaseUniqueInteger(t *testing.T) {
{
name: "verify validation of input fails",
acquire: 524288,
acquireErr: errors.New("value '524288' is outside of the allowed range '1 - 131072'"),
acquireErr: errors.New("value '524288' is outside of the allowed range '1 - 100'"),
},
}

Expand Down
176 changes: 176 additions & 0 deletions pkg/repository/network.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"
"net/netip"
"slices"
"strconv"
"strings"

"github.com/hibiken/asynq"
Expand All @@ -18,6 +19,7 @@ import (
"github.com/metal-stack/metal-apiserver/pkg/db/metal"
"github.com/metal-stack/metal-apiserver/pkg/db/queries"
"github.com/metal-stack/metal-lib/pkg/pointer"
"github.com/samber/lo"
"google.golang.org/protobuf/types/known/timestamppb"
)

Expand Down Expand Up @@ -462,6 +464,180 @@ func (r *networkRepository) convertToProto(ctx context.Context, e *metal.Network
return nw, nil
}

func (r *networkRepository) ListExternalMembers(ctx context.Context, req *adminv2.NetworkServiceListExternalMembersRequest) ([]*apiv2.ExternalNetworkMember, error) {
var members []*apiv2.ExternalNetworkMember

query := &apiv2.SwitchQuery{
Id: req.Query.Switch,
Partition: req.Query.Partition,
Rack: req.Query.Rack,
}

switches, err := r.s.Switch().AdditionalMethods().list(ctx, query)
if err != nil {
return nil, errorutil.Internal("failed to list switches: %w", err)
}

for _, sw := range switches {
member := &apiv2.ExternalNetworkMember{
Switch: sw.ID,
}

for _, nic := range sw.Nics {
if nic.Vrf == "" || nic.Vrf == "default" {
continue
}
m, err := r.s.Switch().AdditionalMethods().getConnectedMachineForNic(ctx, nic, sw.MachineConnections)
if err != nil {
return nil, errorutil.Internal("failed to check machine connections for nic %s: %w", nic.Name, err)
}
if m != nil {
continue
}
member.Ports = append(member.Ports, nic.Name)
}

if len(member.Ports) > 0 {
members = append(members, member)
}
}

return members, nil
}

func (r *networkRepository) AddExternalMembers(ctx context.Context, req *adminv2.NetworkServiceAddExternalMembersRequest) ([]*apiv2.Switch, error) {
var switches []*apiv2.Switch

nw, err := r.s.ds.Network().Get(ctx, req.Network)
if err != nil {
return nil, err
}

rackSwitches, err := r.s.Switch().List(ctx, &apiv2.SwitchQuery{Rack: &req.Rack})
if err != nil {
return nil, errorutil.Internal("failed to list switches in rack %q: %w", req.Rack, err)
}

if len(rackSwitches) < 1 {
return nil, errorutil.NotFound("no switches in rack %q found", req.Rack)
}

if nw.PartitionID != "" && nw.PartitionID != rackSwitches[0].Partition {
return nil, errorutil.InvalidArgument("cannot add switches of partition %q as members to network scoped to partition %q", rackSwitches[0].Partition, nw.PartitionID)
}

for _, sw := range rackSwitches {
for _, port := range req.Ports {
nic, found := lo.Find(sw.Nics, func(n *apiv2.SwitchNic) bool {
return n.Name == port
})
if !found {
return nil, errorutil.NotFound("port %q not found on switch %q", port, sw.Id)
}

for _, con := range sw.MachineConnections {
if con.Nic.Name == port {
return nil, errorutil.InvalidArgument("only ports whose neighbors aren't registered machines can be added as external members but port %q of the switches in rack %q is connected to machine %q", port, req.Rack, con.MachineId)
}
}

if pointer.SafeDeref(nic.Vrf) != "" {
vrfNumString := strings.TrimPrefix(pointer.SafeDeref(nic.Vrf), "Vrf")
vrf, err := strconv.Atoi(vrfNumString)
if err != nil {
return nil, errorutil.Internal("failed to parse vrf number from %q", pointer.SafeDeref(nic.Vrf))
}

nicNetwork, err := r.find(ctx, &apiv2.NetworkQuery{
Vrf: new(uint32(vrf)),
})
if err != nil {
return nil, errorutil.Internal("failed to find network for vrf %q: %w", pointer.SafeDeref(nic.Vrf), err)
}

return nil, errorutil.InvalidArgument("port %q of switches in rack %q is already member of network %q", port, req.Rack, nicNetwork.ID)
}

nic.Vrf = new(fmt.Sprintf("Vrf%d", nw.Vrf))
}

switches = append(switches, sw)
}

for _, sw := range switches {
_, err := r.s.Switch().Update(ctx, sw.Id, &adminv2.SwitchServiceUpdateRequest{
Id: sw.Id,
Nics: sw.Nics,
UpdateMeta: &apiv2.UpdateMeta{
LockingStrategy: apiv2.OptimisticLockingStrategy_OPTIMISTIC_LOCKING_STRATEGY_SERVER,
},
})
if err != nil {
return nil, errorutil.Internal("failed to update switch %q: %w", sw.Id, err)
}
}

return switches, nil
}

func (r *networkRepository) RemoveExternalMembers(ctx context.Context, req *adminv2.NetworkServiceRemoveExternalMembersRequest) ([]*apiv2.Switch, error) {
var switches []*apiv2.Switch

nw, err := r.s.ds.Network().Get(ctx, req.Network)
if err != nil {
return nil, err
}

rackSwitches, err := r.s.Switch().List(ctx, &apiv2.SwitchQuery{Rack: &req.Rack})
if err != nil {
return nil, errorutil.Internal("failed to list switches in rack %q: %w", req.Rack, err)
}

if len(rackSwitches) < 1 {
return nil, errorutil.NotFound("no switches in rack %q found", req.Rack)
}

for _, sw := range rackSwitches {
for _, port := range req.Ports {
nic, found := lo.Find(sw.Nics, func(n *apiv2.SwitchNic) bool {
return n.Name == port
})
if !found {
return nil, errorutil.NotFound("port %q not found on switch %q", port, sw.Id)
}

if pointer.SafeDeref(nic.Vrf) != fmt.Sprintf("Vrf%d", nw.Vrf) {
return nil, errorutil.InvalidArgument("port %q is not a member of network %q", port, nw.ID)
}

for _, con := range sw.MachineConnections {
if con.Nic.Name == port {
return nil, errorutil.InvalidArgument("port %q of rack %q is not an external member as it is connected to machine %q", port, req.Rack, con.MachineId)
}
}

nic.Vrf = nil
}

switches = append(switches, sw)
}

for _, sw := range switches {
_, err := r.s.Switch().Update(ctx, sw.Id, &adminv2.SwitchServiceUpdateRequest{
Id: sw.Id,
Nics: sw.Nics,
UpdateMeta: &apiv2.UpdateMeta{
LockingStrategy: apiv2.OptimisticLockingStrategy_OPTIMISTIC_LOCKING_STRATEGY_SERVER,
},
})
if err != nil {
return nil, errorutil.Internal("failed to update switch %q: %w", sw.Id, err)
}
}

return switches, nil
}

func (r *networkRepository) toProtoChildPrefixLength(childPrefixLength metal.ChildPrefixLength) (*apiv2.ChildPrefixLength, error) {
var result *apiv2.ChildPrefixLength
for af, length := range childPrefixLength {
Expand Down
4 changes: 2 additions & 2 deletions pkg/request/tokenpermissions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,13 @@ func Test_getTokenPermissions(t *testing.T) {
"/metalstack.admin.v2.MachineService/List": {"*": {}},
"/metalstack.admin.v2.MachineService/ListBMC": {"*": {}},
"/metalstack.admin.v2.MachineService/SetState": {"*": {}},
"/metalstack.admin.v2.NetworkService/AddExternalMember": {"*": {}},
"/metalstack.admin.v2.NetworkService/AddExternalMembers": {"*": {}},
"/metalstack.admin.v2.NetworkService/Create": {"*": {}},
"/metalstack.admin.v2.NetworkService/Delete": {"*": {}},
"/metalstack.admin.v2.NetworkService/Get": {"*": {}},
"/metalstack.admin.v2.NetworkService/List": {"*": {}},
"/metalstack.admin.v2.NetworkService/ListExternalMembers": {"*": {}},
"/metalstack.admin.v2.NetworkService/RemoveExternalMember": {"*": {}},
"/metalstack.admin.v2.NetworkService/RemoveExternalMembers": {"*": {}},
"/metalstack.admin.v2.NetworkService/Update": {"*": {}},
"/metalstack.admin.v2.PartitionService/Capacity": {"*": {}},
"/metalstack.admin.v2.PartitionService/Create": {"*": {}},
Expand Down
Loading