diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 79e5bc9b..45912b58 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -81,7 +81,7 @@ jobs: grep -v "metal-apiserver/pkg/service/admin/tenant$" | grep -v "metal-apiserver/pkg/service/admin/switch$" | grep -v "metal-apiserver/pkg/service/admin/vpn$" | - grep -v "metal-apiserver/pkg/service/infra$" | + grep -v "metal-apiserver/pkg/service/infra" | tr '\n' ' ') steps: diff --git a/cmd/server/datastore-cmd.go b/cmd/server/datastore-cmd.go index 4659dea8..9fad9f7c 100644 --- a/cmd/server/datastore-cmd.go +++ b/cmd/server/datastore-cmd.go @@ -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{ diff --git a/go.mod b/go.mod index e211117a..3838ad3b 100644 --- a/go.mod +++ b/go.mod @@ -27,7 +27,7 @@ require ( github.com/lestrrat-go/jwx/v4 v4.4.0 github.com/looplab/fsm v1.0.4 github.com/markbates/goth v1.82.0 - github.com/metal-stack/api v0.5.3 + github.com/metal-stack/api v0.5.6-0.20260909090323-c58acb0d1dba 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.1 diff --git a/go.sum b/go.sum index fe9f81dc..305279fd 100644 --- a/go.sum +++ b/go.sum @@ -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.6-0.20260909090323-c58acb0d1dba h1:4Dyhunb5seTQSY3hgGqZ5C0QT638xkEx5yS58YVGjG8= +github.com/metal-stack/api v0.5.6-0.20260909090323-c58acb0d1dba/go.mod h1:rWdj0dTAg2w/WuQhIZ+JVp9b68v6VVCm1k7sLx4U+PQ= 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= diff --git a/pkg/db/generic/initialize.go b/pkg/db/generic/initialize.go index 5f8af74f..0fe17be4 100644 --- a/pkg/db/generic/initialize.go +++ b/pkg/db/generic/initialize.go @@ -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") @@ -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) @@ -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) } } @@ -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 { @@ -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 { diff --git a/pkg/db/generic/integer_pool_test.go b/pkg/db/generic/integer_pool_test.go index 7963ca87..e90126a3 100644 --- a/pkg/db/generic/integer_pool_test.go +++ b/pkg/db/generic/integer_pool_test.go @@ -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'"), }, } diff --git a/pkg/db/metal/nic.go b/pkg/db/metal/nic.go index 419429aa..e51d0dc4 100644 --- a/pkg/db/metal/nic.go +++ b/pkg/db/metal/nic.go @@ -4,14 +4,15 @@ import "github.com/samber/lo" type ( Nic struct { - MacAddress string `rethinkdb:"macAddress"` - Name string `rethinkdb:"name"` - Identifier string `rethinkdb:"identifier"` - Vrf string `rethinkdb:"vrf"` - Neighbors Nics `rethinkdb:"neighbors"` - Hostname string `rethinkdb:"hostname"` - State *NicState `rethinkdb:"state"` - BGPPortState *SwitchBGPPortState `rethinkdb:"bgpPortState"` + MacAddress string `rethinkdb:"macAddress"` + Name string `rethinkdb:"name"` + Identifier string `rethinkdb:"identifier"` + Vrf string `rethinkdb:"vrf"` + Neighbors Nics `rethinkdb:"neighbors"` + Hostname string `rethinkdb:"hostname"` + State *NicState `rethinkdb:"state"` + BGPPortState *SwitchBGPPortState `rethinkdb:"bgpPortState"` + Membership SwitchPortMembership `rethinkdb:"membership"` } Nics []Nic @@ -23,8 +24,9 @@ type ( Actual SwitchPortStatus `rethinkdb:"actual"` } - BGPState string - SwitchPortStatus string + BGPState string + SwitchPortStatus string + SwitchPortMembership string ) const ( @@ -37,9 +39,15 @@ const ( ) const ( - SwitchPortStatusUnknown SwitchPortStatus = "UNKNOWN" - SwitchPortStatusUp SwitchPortStatus = "UP" - SwitchPortStatusDown SwitchPortStatus = "DOWN" + SwitchPortStatusUnknown = SwitchPortStatus("UNKNOWN") + SwitchPortStatusUp = SwitchPortStatus("UP") + SwitchPortStatusDown = SwitchPortStatus("DOWN") +) + +const ( + SwitchPortMembershipUnmanaged = SwitchPortMembership("unmanaged") + SwitchPortMembershipInternal = SwitchPortMembership("internal") + SwitchPortMembershipExternal = SwitchPortMembership("external") ) func (nics Nics) MapByIdentifier() NicMap { diff --git a/pkg/db/metal/switch.go b/pkg/db/metal/switch.go index d32d39a7..e7716f20 100644 --- a/pkg/db/metal/switch.go +++ b/pkg/db/metal/switch.go @@ -145,6 +145,22 @@ func FromBGPState(state BGPState) (apiv2.BGPState, error) { return apiv2State, nil } +func ToMembership(membership apiv2.SwitchPortMembership) (SwitchPortMembership, error) { + strVal, err := enum.GetStringValue(membership) + if err != nil { + return SwitchPortMembership(""), err + } + return SwitchPortMembership(*strVal), nil +} + +func FromMembership(membership SwitchPortMembership) (apiv2.SwitchPortMembership, error) { + apiv2Membership, err := enum.GetEnum[apiv2.SwitchPortMembership](string(membership)) + if err != nil { + return apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_UNSPECIFIED, err + } + return apiv2Membership, nil +} + func (c ConnectionMap) ByNicName() (map[string]Connection, error) { res := make(map[string]Connection) for _, cons := range c { @@ -159,18 +175,49 @@ func (c ConnectionMap) ByNicName() (map[string]Connection, error) { } func (s *Switch) ConnectMachine(machineID string, machineNics Nics) (int, error) { - physicalConnections := s.getPhysicalMachineConnections(machineID, machineNics) + newCons := s.getConnectionsFromMachineNics(machineID, machineNics) - if len(physicalConnections) < 1 { + if len(newCons) < 1 { if _, exists := s.MachineConnections[machineID]; exists { return 0, fmt.Errorf("machine connection between machine %s and switch %s exists in the database but not physically; if you are attempting migrate the machine from one rack to another delete it first", machineID, s.ID) } return 0, nil } - delete(s.MachineConnections, machineID) - s.MachineConnections[machineID] = append(s.MachineConnections[machineID], physicalConnections...) - return len(physicalConnections), nil + oldCons := s.MachineConnections[machineID] + + for _, con := range oldCons { + nic, idx, found := lo.FindIndexOf(s.Nics, func(n Nic) bool { + return n.Name == con.Nic.Name + }) + if !found { + return 0, fmt.Errorf("nic %s found in machine connections but not in switch nics", con.Nic.Name) + } + nic.Membership = SwitchPortMembershipUnmanaged + s.Nics[idx] = nic + } + + for i, con := range newCons { + nic, idx, found := lo.FindIndexOf(s.Nics, func(n Nic) bool { + return n.Name == con.Nic.Name + }) + if !found { + return 0, fmt.Errorf("nic %s found in machine connections but not in switch nics", con.Nic.Name) + } + + if nic.Membership == SwitchPortMembershipExternal { + return 0, fmt.Errorf("nic %s is an external member of a network", nic.Name) + } + + nic.Membership = SwitchPortMembershipInternal + s.Nics[idx] = nic + + con.Nic.Membership = SwitchPortMembershipInternal + newCons[i] = con + } + + s.MachineConnections[machineID] = newCons + return len(newCons), nil } func (s *Switch) SetVrfOfMachine(m *Machine, vrf string) { @@ -392,9 +439,10 @@ func cumulusPortByLineNumber(line int, allLines []int) string { return fmt.Sprintf("swp%d", line/4+1) } -// getPhysicalMachineConnections correlates machine nic information with the nics on the switch to figure out physical connections -func (s *Switch) getPhysicalMachineConnections(machineID string, machineNics Nics) Connections { - connections := make(Connections, 0) +func (s *Switch) getConnectionsFromMachineNics(machineID string, machineNics Nics) Connections { + var ( + connections Connections + ) for _, machineNic := range machineNics { neighMap := machineNic.Neighbors.FilterByHostname(s.ID).MapByIdentifier() @@ -407,5 +455,8 @@ func (s *Switch) getPhysicalMachineConnections(machineID string, machineNics Nic } } } + + // FIXME: shouldn't we return an error if a machine sees a neighbor but we can't find the nic on the switch? + return connections } diff --git a/pkg/db/metal/switch_test.go b/pkg/db/metal/switch_test.go index 30bffb56..a01c0ff1 100644 --- a/pkg/db/metal/switch_test.go +++ b/pkg/db/metal/switch_test.go @@ -529,7 +529,7 @@ func Test_cumulusPortByLineNumber(t *testing.T) { } } -func TestSwitch_getPhysicalMachineConnection(t *testing.T) { +func TestSwitch_getConnectionsFromMachineNics(t *testing.T) { tests := []struct { name string s *Switch @@ -599,15 +599,13 @@ func TestSwitch_getPhysicalMachineConnection(t *testing.T) { }, }, }, - - want: Connections{}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got := tt.s.getPhysicalMachineConnections(tt.machineID, tt.machineNics) + got := tt.s.getConnectionsFromMachineNics(tt.machineID, tt.machineNics) if diff := cmp.Diff(tt.want, got); diff != "" { - t.Errorf("Switch.getPhysicalMachineConnection() diff = %v", diff) + t.Errorf("Switch.getConnectionsFromMachineNics() diff = %v", diff) } }) } @@ -621,6 +619,7 @@ func TestSwitch_ConnectMachine(t *testing.T) { machineNics Nics want int wantConnections ConnectionMap + wantNics Nics wantErr bool }{ { @@ -633,6 +632,7 @@ func TestSwitch_ConnectMachine(t *testing.T) { Name: "Ethernet12", Identifier: "Eth4", Hostname: "sw1", + Membership: SwitchPortMembershipInternal, }, }, MachineConnections: ConnectionMap{ @@ -643,6 +643,7 @@ func TestSwitch_ConnectMachine(t *testing.T) { Name: "Ethernet12", Identifier: "Eth4", Hostname: "sw1", + Membership: SwitchPortMembershipInternal, }, MachineID: "m2", }, @@ -671,11 +672,21 @@ func TestSwitch_ConnectMachine(t *testing.T) { Name: "Ethernet12", Identifier: "Eth4", Hostname: "sw1", + Membership: SwitchPortMembershipInternal, }, MachineID: "m2", }, }, }, + wantNics: Nics{ + { + MacAddress: "aa:aa:aa:aa:aa:aa", + Name: "Ethernet12", + Identifier: "Eth4", + Hostname: "sw1", + Membership: SwitchPortMembershipInternal, + }, + }, wantErr: false, }, { @@ -688,6 +699,7 @@ func TestSwitch_ConnectMachine(t *testing.T) { Name: "Ethernet12", Identifier: "Eth4", Hostname: "sw1", + Membership: SwitchPortMembershipInternal, }, }, MachineConnections: ConnectionMap{ @@ -697,6 +709,7 @@ func TestSwitch_ConnectMachine(t *testing.T) { MacAddress: "aa:aa:aa:aa:aa:aa", Name: "Ethernet12", Identifier: "Eth4", + Membership: SwitchPortMembershipInternal, }, MachineID: "m1", }, @@ -721,11 +734,56 @@ func TestSwitch_ConnectMachine(t *testing.T) { MacAddress: "aa:aa:aa:aa:aa:aa", Name: "Ethernet12", Identifier: "Eth4", + Membership: SwitchPortMembershipInternal, }, MachineID: "m1", }, }, }, + wantNics: Nics{ + { + MacAddress: "aa:aa:aa:aa:aa:aa", + Name: "Ethernet12", + Identifier: "Eth4", + Hostname: "sw1", + Membership: SwitchPortMembershipInternal, + }, + }, + wantErr: true, + }, + { + name: "cannot connect machine with external port of a switch", + s: &Switch{ + ID: "sw1", + Nics: Nics{ + { + Name: "Ethernet0", + Identifier: "Ethernet0", + Membership: SwitchPortMembershipExternal, + }, + }, + }, + machineID: "m1", + machineNics: Nics{ + { + Neighbors: Nics{ + { + MacAddress: "bb:bb:bb:bb:bb:bb", + Name: "Ethernet0", + Identifier: "Ethernet0", + Hostname: "sw1", + }, + }, + }, + }, + want: 0, + wantNics: Nics{ + { + Name: "Ethernet0", + Identifier: "Ethernet0", + Membership: SwitchPortMembershipExternal, + }, + }, wantErr: true, }, { @@ -733,10 +791,23 @@ func TestSwitch_ConnectMachine(t *testing.T) { s: &Switch{ ID: "sw1", Nics: Nics{ + { + MacAddress: "aa:aa:aa:aa:aa:aa", + Name: "Ethernet12", + Identifier: "Eth4", + Membership: SwitchPortMembershipInternal, + }, { MacAddress: "bb:bb:bb:bb:bb:bb", Name: "Ethernet16", Identifier: "Eth5", + Membership: SwitchPortMembershipUnmanaged, + }, + { + MacAddress: "cc:cc:cc:cc:cc:cc", + Name: "Ethernet20", + Identifier: "Eth6", + Membership: SwitchPortMembershipInternal, }, }, MachineConnections: ConnectionMap{ @@ -746,8 +817,9 @@ func TestSwitch_ConnectMachine(t *testing.T) { MacAddress: "aa:aa:aa:aa:aa:aa", Name: "Ethernet12", Identifier: "Eth4", + Membership: SwitchPortMembershipInternal, }, - MachineID: "", + MachineID: "m1", }, }, "m2": { @@ -756,6 +828,7 @@ func TestSwitch_ConnectMachine(t *testing.T) { MacAddress: "cc:cc:cc:cc:cc:cc", Name: "Ethernet20", Identifier: "Eth6", + Membership: SwitchPortMembershipInternal, }, MachineID: "m2", }, @@ -783,6 +856,7 @@ func TestSwitch_ConnectMachine(t *testing.T) { MacAddress: "bb:bb:bb:bb:bb:bb", Name: "Ethernet16", Identifier: "Eth5", + Membership: SwitchPortMembershipInternal, }, MachineID: "m1", }, @@ -793,11 +867,32 @@ func TestSwitch_ConnectMachine(t *testing.T) { MacAddress: "cc:cc:cc:cc:cc:cc", Name: "Ethernet20", Identifier: "Eth6", + Membership: SwitchPortMembershipInternal, }, MachineID: "m2", }, }, }, + wantNics: Nics{ + { + MacAddress: "aa:aa:aa:aa:aa:aa", + Name: "Ethernet12", + Identifier: "Eth4", + Membership: SwitchPortMembershipUnmanaged, + }, + { + MacAddress: "bb:bb:bb:bb:bb:bb", + Name: "Ethernet16", + Identifier: "Eth5", + Membership: SwitchPortMembershipInternal, + }, + { + MacAddress: "cc:cc:cc:cc:cc:cc", + Name: "Ethernet20", + Identifier: "Eth6", + Membership: SwitchPortMembershipInternal, + }, + }, wantErr: false, }, } @@ -816,7 +911,11 @@ func TestSwitch_ConnectMachine(t *testing.T) { } if diff := cmp.Diff(tt.wantConnections, tt.s.MachineConnections); diff != "" { - t.Errorf("Switch.ConnectMachine() diff = %v", diff) + t.Errorf("Switch.ConnectMachine() connections diff = %v", diff) + } + + if diff := cmp.Diff(tt.wantNics, tt.s.Nics); diff != "" { + t.Errorf("Switch.ConnectMachine() nics diff = %v", diff) } }) } diff --git a/pkg/repository/network.go b/pkg/repository/network.go index 4322d317..fcd2d3e6 100644 --- a/pkg/repository/network.go +++ b/pkg/repository/network.go @@ -6,6 +6,7 @@ import ( "fmt" "net/netip" "slices" + "strconv" "strings" "github.com/hibiken/asynq" @@ -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" ) @@ -390,6 +392,7 @@ func (r *networkRepository) list(ctx context.Context, query *apiv2.NetworkQuery) return nws, nil } + func (r *networkRepository) convertToInternal(ctx context.Context, msg *apiv2.Network) (*metal.Network, error) { return nil, errorutil.Unimplemented("") } @@ -462,6 +465,234 @@ func (r *networkRepository) convertToProto(ctx context.Context, e *metal.Network return nw, nil } +func (r *networkRepository) ListExternalMembers(ctx context.Context, req *adminv2.NetworkServiceListExternalMembersRequest) (*adminv2.NetworkServiceListExternalMembersResponse, error) { + var ( + members []*apiv2.ExternalNetworkMember + query *apiv2.SwitchQuery + ) + + if req.Query != nil { + query = &apiv2.SwitchQuery{ + Id: req.Query.Switch, + Partition: req.Query.Partition, + Rack: req.Query.Rack, + } + } + + nw, err := r.s.UnscopedNetwork().Get(ctx, req.Network) + if err != nil { + return nil, err + } + + switches, err := r.s.Switch().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, + Partition: sw.Partition, + Rack: pointer.SafeDeref(sw.Rack), + } + + for _, nic := range sw.Nics { + if pointer.SafeDeref(nic.Vrf) == "" || pointer.SafeDeref(nic.Vrf) == "default" { + continue + } + + if pointer.SafeDeref(nic.Vrf) != fmt.Sprintf("vrf%d", pointer.SafeDeref(nw.Vrf)) { + continue + } + + if nic.Membership != apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_EXTERNAL { + continue + } + + member.Ports = append(member.Ports, nic.Name) + } + + if len(member.Ports) > 0 { + members = append(members, member) + } + } + + return &adminv2.NetworkServiceListExternalMembersResponse{ + Network: nw, + Members: members, + }, nil +} + +func (r *networkRepository) AddExternalMembers(ctx context.Context, req *adminv2.NetworkServiceAddExternalMembersRequest) (*adminv2.NetworkServiceAddExternalMembersResponse, error) { + var ( + switches []*apiv2.Switch + members []*apiv2.ExternalNetworkMember + ) + + nw, err := r.s.UnscopedNetwork().Get(ctx, req.Network) + if err != nil { + return nil, err + } + + switch nw.Type { + case apiv2.NetworkType_NETWORK_TYPE_CHILD, apiv2.NetworkType_NETWORK_TYPE_CHILD_SHARED, apiv2.NetworkType_NETWORK_TYPE_EXTERNAL: + // noop + default: + return nil, errorutil.InvalidArgument("cannot add external members to network of type %q", nw.Type) + } + + 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 pointer.SafeDeref(nw.Partition) != "" && pointer.SafeDeref(nw.Partition) != rackSwitches[0].Partition { + return nil, errorutil.InvalidArgument("cannot add switches of partition %q as members to network scoped to partition %q", rackSwitches[0].Partition, pointer.SafeDeref(nw.Partition)) + } + + for _, sw := range rackSwitches { + member := &apiv2.ExternalNetworkMember{ + Switch: sw.Id, + Partition: sw.Partition, + Rack: pointer.SafeDeref(sw.Rack), + } + + 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 nic.Membership == apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_INTERNAL { + return nil, errorutil.InvalidArgument(`cannot add internal port %q of rack %q as external member`, port, req.Rack) + } + + if nic.Membership == apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_EXTERNAL { + 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", pointer.SafeDeref(nw.Vrf))) + nic.Membership = apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_EXTERNAL + member.Ports = append(member.Ports, nic.Name) + } + + if len(member.Ports) > 0 { + members = append(members, member) + 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 &adminv2.NetworkServiceAddExternalMembersResponse{ + Network: nw, + Members: members, + }, nil +} + +func (r *networkRepository) RemoveExternalMembers(ctx context.Context, req *adminv2.NetworkServiceRemoveExternalMembersRequest) (*adminv2.NetworkServiceRemoveExternalMembersResponse, error) { + var ( + switches []*apiv2.Switch + members []*apiv2.ExternalNetworkMember + ) + + nw, err := r.s.UnscopedNetwork().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 { + member := &apiv2.ExternalNetworkMember{ + Switch: sw.Id, + Partition: sw.Partition, + Rack: pointer.SafeDeref(sw.Rack), + } + + 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", pointer.SafeDeref(nw.Vrf)) { + return nil, errorutil.InvalidArgument("port %q is not a member of network %q", port, nw.Id) + } + + if nic.Membership != apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_EXTERNAL { + return nil, errorutil.InvalidArgument("port %q of rack %q is not an external member", port, req.Rack) + } + + nic.Vrf = nil + nic.Membership = apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_UNMANAGED + member.Ports = append(member.Ports, nic.Name) + } + + if len(member.Ports) > 0 { + members = append(members, member) + 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 &adminv2.NetworkServiceRemoveExternalMembersResponse{ + Network: nw, + Members: members, + }, nil +} + func (r *networkRepository) toProtoChildPrefixLength(childPrefixLength metal.ChildPrefixLength) (*apiv2.ChildPrefixLength, error) { var result *apiv2.ChildPrefixLength for af, length := range childPrefixLength { diff --git a/pkg/repository/switch-validation.go b/pkg/repository/switch-validation.go index eb635d6a..66fd1dfd 100644 --- a/pkg/repository/switch-validation.go +++ b/pkg/repository/switch-validation.go @@ -15,8 +15,12 @@ import ( ) func (r *switchRepository) validateCreate(ctx context.Context, req *api.SwitchServiceCreateRequest) error { - var errs []error + if req == nil || req.Switch == nil { + return nil + } + defaultNicMemberships(req.Switch.Nics, req.Switch.MachineConnections) + var errs []error _, err := r.s.ds.Partition().Get(ctx, req.Switch.Partition) if err != nil { errs = append(errs, errorutil.NewInternal(err)) @@ -41,13 +45,16 @@ func (r *switchRepository) validateCreate(ctx context.Context, req *api.SwitchSe } func (r *switchRepository) validateUpdate(ctx context.Context, req *adminv2.SwitchServiceUpdateRequest, oldSwitch *metal.Switch) error { - var errs []error + if req == nil { + return nil + } sw, err := r.s.ds.Switch().Get(ctx, req.Id) if err != nil { return errorutil.NewInternal(err) } + var errs []error _, err = r.s.ds.Partition().Get(ctx, sw.Partition) if err != nil { errs = append(errs, errorutil.NewInternal(err)) diff --git a/pkg/repository/switch.go b/pkg/repository/switch.go index 8e30cf93..abede4af 100644 --- a/pkg/repository/switch.go +++ b/pkg/repository/switch.go @@ -37,10 +37,11 @@ func (r *switchRepository) Register(ctx context.Context, req *infrav2.SwitchServ return nil, errorutil.InvalidArgument("empty request") } - sw, err := r.get(ctx, req.Switch.Id) + metalSwitch, err := r.get(ctx, req.Switch.Id) if err != nil && !errorutil.IsNotFound(err) { return nil, err } + if errorutil.IsNotFound(err) { if req.Switch.ReplaceMode == apiv2.SwitchReplaceMode_SWITCH_REPLACE_MODE_UNSPECIFIED { req.Switch.ReplaceMode = apiv2.SwitchReplaceMode_SWITCH_REPLACE_MODE_OPERATIONAL @@ -49,12 +50,14 @@ func (r *switchRepository) Register(ctx context.Context, req *infrav2.SwitchServ } new := req.Switch - old, err := r.convertToProto(ctx, sw) + defaultNicMemberships(new.Nics, new.MachineConnections) + defaultMetalNicMemberships(metalSwitch) + old, err := r.convertToProto(ctx, metalSwitch) if err != nil { return nil, err } - if sw.ReplaceMode == metal.SwitchReplaceModeReplace { + if metalSwitch.ReplaceMode == metal.SwitchReplaceModeReplace { sw, err := r.replace(ctx, old, new) if err != nil { return nil, err @@ -82,16 +85,16 @@ func (r *switchRepository) Register(ctx context.Context, req *infrav2.SwitchServ } // lazy migration because in the past replace mode was allowed to be unspecified. - if req.Switch.ReplaceMode == apiv2.SwitchReplaceMode_SWITCH_REPLACE_MODE_UNSPECIFIED && sw.ReplaceMode == "" { + if new.ReplaceMode == apiv2.SwitchReplaceMode_SWITCH_REPLACE_MODE_UNSPECIFIED && metalSwitch.ReplaceMode == "" { updateReq.ReplaceMode = apiv2.SwitchReplaceMode_SWITCH_REPLACE_MODE_OPERATIONAL.Enum() } - err = r.validateUpdate(ctx, updateReq, sw) + err = r.validateUpdate(ctx, updateReq, metalSwitch) if err != nil { return nil, err } - updated, err := r.updateOnRegister(ctx, sw, updateReq) + updated, err := r.updateOnRegister(ctx, metalSwitch, updateReq) if err != nil { return nil, err } @@ -335,19 +338,12 @@ func (r *switchRepository) ConnectMachineWithSwitches(ctx context.Context, m *ap }), )) - prev, _ := lo.Difference(oldNeighs, neighs) - for _, id := range prev { - s, err := r.get(ctx, id) - if err != nil { - return fmt.Errorf("failed to remove machine connection from switch %s: %w", id, err) - } + if len(oldNeighs) > 0 { + slices.Sort(neighs) + slices.Sort(oldNeighs) - cons := s.MachineConnections - delete(cons, m.Uuid) - - err = r.s.ds.Switch().Update(ctx, s) - if err != nil { - return fmt.Errorf("failed to remove machine connection from switch %s: %w", id, err) + if diff := cmp.Diff(neighs, oldNeighs); diff != "" { + return errorutil.FailedPrecondition("cannot connect machine %q to different switches than it was previously connected to; current: %v, previous: %v; if you want to migrate machine connections from one switch to another call 'switch mirgate' first", metalMachine.ID, neighs, oldNeighs) } } } @@ -375,12 +371,10 @@ func (r *switchRepository) ConnectMachineWithSwitches(ctx context.Context, m *ap } var orphanedSwitchNames []string - for _, sw := range sws { if sw.Rack == m.Rack { continue } - orphanedSwitchNames = append(orphanedSwitchNames, sw.ID) } @@ -452,7 +446,6 @@ func (r *switchRepository) ConnectMachineWithSwitches(ctx context.Context, m *ap } func (r *switchRepository) RemoveMachineFromSwitches(ctx context.Context, m *apiv2.Machine) error { - switches, err := r.s.ds.Switch().List(ctx, queries.SwitchFilter(&apiv2.SwitchQuery{ ConnectedMachineId: &m.Uuid, })) @@ -460,6 +453,17 @@ func (r *switchRepository) RemoveMachineFromSwitches(ctx context.Context, m *api return fmt.Errorf("unable to query switches: %w", err) } for _, sw := range switches { + for _, con := range sw.MachineConnections[m.Uuid] { + nic, idx, found := lo.FindIndexOf(sw.Nics, func(n metal.Nic) bool { + return n.Name == con.Nic.Name + }) + if !found { + return errorutil.Internal("cannot find nic %s on switch %s", con.Nic.Name, sw.ID) + } + nic.Membership = metal.SwitchPortMembershipUnmanaged + sw.Nics[idx] = nic + } + delete(sw.MachineConnections, m.Uuid) if err := r.s.ds.Switch().Update(ctx, sw); err != nil { @@ -617,6 +621,7 @@ func (r *switchRepository) create(ctx context.Context, req *api.SwitchServiceCre if req.Switch == nil { return nil, nil } + sw, err := r.convertToInternal(ctx, req.Switch) if err != nil { return nil, err @@ -906,7 +911,7 @@ func (r *switchRepository) updateOnRegister(ctx context.Context, sw *metal.Switc if err != nil { return nil, err } - sw.Nics = updateNicNames(sw.Nics, nics) + sw.Nics = updateNicsOnRegister(sw.Nics, nics) } err = r.s.ds.Switch().Update(ctx, sw) @@ -1039,6 +1044,11 @@ func (r *switchRepository) convertToSwitchNics(ctx context.Context, sw *metal.Sw return nil, errorutil.FailedPrecondition("both, identifier and mac address, of nic %s are empty which is not allowed", nic.Name) } + membership, err := metal.FromMembership(nic.Membership) + if err != nil { + return nil, errorutil.Internal("failed to convert membership of nic %q: %w", nic.Name, err) + } + switchNics = append(switchNics, &apiv2.SwitchNic{ Name: nic.Name, Identifier: identifier, @@ -1050,6 +1060,7 @@ func (r *switchRepository) convertToSwitchNics(ctx context.Context, sw *metal.Sw }, BgpFilter: filter, BgpPortState: bgpPortState, + Membership: membership, }) } @@ -1122,7 +1133,7 @@ func convertMachineConnections(machineConnections metal.ConnectionMap, nics []*a return connections, nil } -func updateNicNames(old, new metal.Nics) metal.Nics { +func updateNicsOnRegister(old, new metal.Nics) metal.Nics { var ( updated metal.Nics oldNics = old.MapByIdentifier() @@ -1132,12 +1143,20 @@ func updateNicNames(old, new metal.Nics) metal.Nics { for id, newNic := range newNics { oldNic, ok := oldNics[id] if !ok { + if newNic.Membership == "" { + newNic.Membership = metal.SwitchPortMembershipUnmanaged + } updated = append(updated, *newNic) continue } updatedNic := *oldNic updatedNic.Name = newNic.Name + + if updatedNic.Membership == "" { + updatedNic.Membership = metal.SwitchPortMembershipUnmanaged + } + updated = append(updated, updatedNic) } @@ -1340,6 +1359,11 @@ func toMetalNic(switchNic *apiv2.SwitchNic, hostname string) (*metal.Nic, error) return nil, fmt.Errorf("failed to convert port state: %w", err) } + membership, err := metal.ToMembership(switchNic.Membership) + if err != nil { + return nil, fmt.Errorf("failed to convert membership of nic %q: %w", switchNic.Name, err) + } + return &metal.Nic{ Name: switchNic.Name, Hostname: hostname, @@ -1348,6 +1372,7 @@ func toMetalNic(switchNic *apiv2.SwitchNic, hostname string) (*metal.Nic, error) Vrf: pointer.SafeDeref(switchNic.Vrf), State: nicState, BGPPortState: bgpPortState, + Membership: membership, }, nil } @@ -1470,6 +1495,7 @@ func adoptNics(twin, newSwitch *metal.Switch) (metal.Nics, error) { for name, nic := range newNicMap { if twinNic, ok := twinNicsByName[name]; ok { nic.Vrf = twinNic.Vrf + nic.Membership = twinNic.Membership } newNics = append(newNics, *nic) } @@ -1587,3 +1613,32 @@ func nicInConnections(name string, mac string, connections metal.Connections) bo } return false } + +func defaultNicMemberships(switchNics []*apiv2.SwitchNic, connections []*apiv2.MachineConnection) { + for _, nic := range switchNics { + if nic.Membership == apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_UNSPECIFIED { + nic.Membership = apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_UNMANAGED + } + } + for _, con := range connections { + if con.Nic.Membership == apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_UNSPECIFIED { + con.Nic.Membership = apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_UNMANAGED + } + } +} + +func defaultMetalNicMemberships(sw *metal.Switch) { + for i, nic := range sw.Nics { + if nic.Membership == "" { + sw.Nics[i].Membership = metal.SwitchPortMembershipUnmanaged + } + } + for mid, cons := range sw.MachineConnections { + for i, con := range cons { + if con.Nic.Membership == "" { + cons[i].Nic.Membership = metal.SwitchPortMembershipUnmanaged + } + } + sw.MachineConnections[mid] = cons + } +} diff --git a/pkg/repository/switch_test.go b/pkg/repository/switch_test.go index 8567a0de..c51297cd 100644 --- a/pkg/repository/switch_test.go +++ b/pkg/repository/switch_test.go @@ -15,7 +15,7 @@ import ( "google.golang.org/protobuf/types/known/timestamppb" ) -func Test_updateNics(t *testing.T) { +func Test_updateNicsOnRegister(t *testing.T) { tests := []struct { name string old metal.Nics @@ -40,6 +40,7 @@ func Test_updateNics(t *testing.T) { { Identifier: "Eth1/2", Name: "Ethernet1", + Membership: metal.SwitchPortMembershipUnmanaged, }, }, }, @@ -50,12 +51,14 @@ func Test_updateNics(t *testing.T) { Identifier: "Eth1/1", Name: "Ethernet0", Vrf: "Vrf100", + Membership: metal.SwitchPortMembershipExternal, }, }, new: metal.Nics{ { Identifier: "Eth1/1", Name: "Ethernet2", + Membership: metal.SwitchPortMembershipInternal, }, { Identifier: "Eth1/2", @@ -67,17 +70,19 @@ func Test_updateNics(t *testing.T) { Identifier: "Eth1/1", Name: "Ethernet2", Vrf: "Vrf100", + Membership: metal.SwitchPortMembershipExternal, }, { Identifier: "Eth1/2", Name: "Ethernet1", + Membership: metal.SwitchPortMembershipUnmanaged, }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got := updateNicNames(tt.old, tt.new) + got := updateNicsOnRegister(tt.old, tt.new) if diff := cmp.Diff(tt.want, got); diff != "" { t.Errorf("updateNics() diff = %s", diff) } @@ -720,6 +725,7 @@ func TestToMetalNics(t *testing.T) { Name: "Ethernet0", Identifier: "Eth1/1", Mac: new("11:11:11:11:11:11"), + Membership: apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_INTERNAL, State: &apiv2.NicState{ Desired: apiv2.SwitchPortStatus_SWITCH_PORT_STATUS_UP.Enum(), Actual: apiv2.SwitchPortStatus_SWITCH_PORT_STATUS_DOWN, @@ -730,6 +736,7 @@ func TestToMetalNics(t *testing.T) { Identifier: "Eth1/2", Mac: new("22:22:22:22:22:22"), Vrf: new("Vrf100"), + Membership: apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_EXTERNAL, State: &apiv2.NicState{ Desired: apiv2.SwitchPortStatus_SWITCH_PORT_STATUS_UP.Enum(), Actual: apiv2.SwitchPortStatus_SWITCH_PORT_STATUS_UP, @@ -750,6 +757,7 @@ func TestToMetalNics(t *testing.T) { MacAddress: "11:11:11:11:11:11", Name: "Ethernet0", Identifier: "Eth1/1", + Membership: metal.SwitchPortMembershipInternal, State: &metal.NicState{ Desired: new(metal.SwitchPortStatusUp), Actual: metal.SwitchPortStatusDown, @@ -760,6 +768,7 @@ func TestToMetalNics(t *testing.T) { Name: "Ethernet1", Identifier: "Eth1/2", Vrf: "Vrf100", + Membership: metal.SwitchPortMembershipExternal, State: &metal.NicState{ Desired: new(metal.SwitchPortStatusUp), Actual: metal.SwitchPortStatusUp, @@ -807,12 +816,14 @@ func TestToMachineConnections(t *testing.T) { MachineId: "machine-a", Nic: &apiv2.SwitchNic{ Identifier: "Eth1/1", + Membership: apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_UNMANAGED, }, }, { MachineId: "machine-b", Nic: &apiv2.SwitchNic{ Identifier: "Eth1/2", + Membership: apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_INTERNAL, }, }, }, @@ -821,6 +832,7 @@ func TestToMachineConnections(t *testing.T) { { Nic: metal.Nic{ Identifier: "Eth1/1", + Membership: metal.SwitchPortMembershipUnmanaged, }, MachineID: "machine-a", }, @@ -829,6 +841,7 @@ func TestToMachineConnections(t *testing.T) { { Nic: metal.Nic{ Identifier: "Eth1/2", + Membership: metal.SwitchPortMembershipInternal, }, MachineID: "machine-b", }, @@ -843,18 +856,21 @@ func TestToMachineConnections(t *testing.T) { MachineId: "machine-a", Nic: &apiv2.SwitchNic{ Identifier: "Eth1/1", + Membership: apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_INTERNAL, }, }, { MachineId: "machine-b", Nic: &apiv2.SwitchNic{ Identifier: "Eth1/2", + Membership: apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_INTERNAL, }, }, { MachineId: "machine-b", Nic: &apiv2.SwitchNic{ Identifier: "Eth1/3", + Membership: apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_INTERNAL, }, }, }, @@ -863,6 +879,7 @@ func TestToMachineConnections(t *testing.T) { { Nic: metal.Nic{ Identifier: "Eth1/1", + Membership: metal.SwitchPortMembershipInternal, }, MachineID: "machine-a", }, @@ -871,12 +888,14 @@ func TestToMachineConnections(t *testing.T) { { Nic: metal.Nic{ Identifier: "Eth1/2", + Membership: metal.SwitchPortMembershipInternal, }, MachineID: "machine-b", }, { Nic: metal.Nic{ Identifier: "Eth1/3", + Membership: metal.SwitchPortMembershipInternal, }, MachineID: "machine-b", }, @@ -891,12 +910,14 @@ func TestToMachineConnections(t *testing.T) { MachineId: "machine-a", Nic: &apiv2.SwitchNic{ Identifier: "Eth1/1", + Membership: apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_INTERNAL, }, }, { MachineId: "machine-b", Nic: &apiv2.SwitchNic{ Identifier: "Eth1/1", + Membership: apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_INTERNAL, }, }, }, diff --git a/pkg/request/tokenpermissions_test.go b/pkg/request/tokenpermissions_test.go index b530ac56..66cca9cf 100644 --- a/pkg/request/tokenpermissions_test.go +++ b/pkg/request/tokenpermissions_test.go @@ -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": {"*": {}}, diff --git a/pkg/service/admin/machine/machine-service_test.go b/pkg/service/admin/machine/machine-service_test.go index fea478f6..97a18e01 100644 --- a/pkg/service/admin/machine/machine-service_test.go +++ b/pkg/service/admin/machine/machine-service_test.go @@ -1206,9 +1206,11 @@ func Test_machineServiceServer_Delete(t *testing.T) { Switches: func(switches map[string]*apiv2.Switch) { sw1 := switches[sc.P02Rack01Switch1] sw1.MachineConnections = nil + sw1.Nics[0].Membership = apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_UNMANAGED switches[sc.P02Rack01Switch1] = sw1 sw2 := switches[sc.P02Rack01Switch2] sw2.MachineConnections = nil + sw2.Nics[0].Membership = apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_UNMANAGED switches[sc.P02Rack01Switch2] = sw2 }, } diff --git a/pkg/service/admin/network/network-service.go b/pkg/service/admin/network/network-service.go index ec9f39ac..39e9d451 100644 --- a/pkg/service/admin/network/network-service.go +++ b/pkg/service/admin/network/network-service.go @@ -4,7 +4,6 @@ import ( "context" "log/slog" - "github.com/metal-stack/api/go/errorutil" adminv2 "github.com/metal-stack/api/go/metalstack/admin/v2" "github.com/metal-stack/api/go/metalstack/admin/v2/adminv2connect" "github.com/metal-stack/metal-apiserver/pkg/repository" @@ -20,21 +19,6 @@ type networkServiceServer struct { repo *repository.Store } -// AddExternalMember implements [adminv2connect.NetworkServiceHandler]. -func (n *networkServiceServer) AddExternalMember(context.Context, *adminv2.NetworkServiceAddExternalMemberRequest) (*adminv2.NetworkServiceAddExternalMemberResponse, error) { - return nil, errorutil.Unimplemented("") -} - -// ListExternalMembers implements [adminv2connect.NetworkServiceHandler]. -func (n *networkServiceServer) ListExternalMembers(context.Context, *adminv2.NetworkServiceListExternalMembersRequest) (*adminv2.NetworkServiceListExternalMembersResponse, error) { - return nil, errorutil.Unimplemented("") -} - -// RemoveExternalMember implements [adminv2connect.NetworkServiceHandler]. -func (n *networkServiceServer) RemoveExternalMember(context.Context, *adminv2.NetworkServiceRemoveExternalMemberRequest) (*adminv2.NetworkServiceRemoveExternalMemberResponse, error) { - return nil, errorutil.Unimplemented("") -} - func New(c Config) adminv2connect.NetworkServiceHandler { return &networkServiceServer{ log: c.Log.WithGroup("adminNetworkService"), @@ -91,3 +75,27 @@ func (n *networkServiceServer) Update(ctx context.Context, req *adminv2.NetworkS return &adminv2.NetworkServiceUpdateResponse{Network: nw}, nil } + +func (n *networkServiceServer) ListExternalMembers(ctx context.Context, req *adminv2.NetworkServiceListExternalMembersRequest) (*adminv2.NetworkServiceListExternalMembersResponse, error) { + res, err := n.repo.UnscopedNetwork().AdditionalMethods().ListExternalMembers(ctx, req) + if err != nil { + return nil, err + } + return res, nil +} + +func (n *networkServiceServer) AddExternalMembers(ctx context.Context, req *adminv2.NetworkServiceAddExternalMembersRequest) (*adminv2.NetworkServiceAddExternalMembersResponse, error) { + res, err := n.repo.UnscopedNetwork().AdditionalMethods().AddExternalMembers(ctx, req) + if err != nil { + return nil, err + } + return res, nil +} + +func (n *networkServiceServer) RemoveExternalMembers(ctx context.Context, req *adminv2.NetworkServiceRemoveExternalMembersRequest) (*adminv2.NetworkServiceRemoveExternalMembersResponse, error) { + res, err := n.repo.UnscopedNetwork().AdditionalMethods().RemoveExternalMembers(ctx, req) + if err != nil { + return nil, err + } + return res, nil +} diff --git a/pkg/service/admin/network/network-service_test.go b/pkg/service/admin/network/network-service_test.go index 30426486..2a87086d 100644 --- a/pkg/service/admin/network/network-service_test.go +++ b/pkg/service/admin/network/network-service_test.go @@ -5,8 +5,13 @@ import ( "fmt" "net/http" "net/http/httptest" + "slices" + "strings" "testing" + sc "github.com/metal-stack/metal-apiserver/pkg/test/scenarios" + "github.com/metal-stack/metal-lib/pkg/pointer" + "github.com/google/go-cmp/cmp" "github.com/metal-stack/api/go/errorutil" adminv2 "github.com/metal-stack/api/go/metalstack/admin/v2" @@ -14,6 +19,7 @@ import ( "github.com/metal-stack/metal-apiserver/pkg/test" "github.com/samber/lo" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "google.golang.org/protobuf/testing/protocmp" "google.golang.org/protobuf/types/known/timestamppb" ) @@ -2416,3 +2422,664 @@ func Test_networkServiceServer_Update(t *testing.T) { }) } } + +func Test_networkServiceServer_ListExternalMembers(t *testing.T) { + ctx := t.Context() + + spec, err := sc.SwitchesWithExternalNetworkMembers.DeepCopy() + require.NoError(t, err) + + require.NoError(t, sc.AddNicsToVRF(spec.Switches[0], "Vrf100", apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_INTERNAL, "Ethernet0")) + require.NoError(t, sc.AddNicsToVRF(spec.Switches[0], "Vrf100", apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_EXTERNAL, "Ethernet1")) + require.NoError(t, sc.AddNicsToVRF(spec.Switches[1], "Vrf100", apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_INTERNAL, "Ethernet0")) + require.NoError(t, sc.AddNicsToVRF(spec.Switches[1], "Vrf100", apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_EXTERNAL, "Ethernet1")) + require.NoError(t, sc.AddNicsToVRF(spec.Switches[1], "Vrf99", apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_EXTERNAL, "Ethernet120")) + + require.NoError(t, sc.AddNicsToVRF(spec.Switches[2], "Vrf100", apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_EXTERNAL, "Ethernet0", "Ethernet1")) + require.NoError(t, sc.AddNicsToVRF(spec.Switches[3], "Vrf100", apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_EXTERNAL, "Ethernet0", "Ethernet1")) + + require.NoError(t, sc.AddNicsToVRF(spec.Switches[4], "Vrf100", apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_EXTERNAL, "Ethernet0")) + require.NoError(t, sc.AddNicsToVRF(spec.Switches[5], "Vrf100", apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_EXTERNAL, "Ethernet0")) + + dc := test.NewDatacenter(t) + log := dc.GetTestStore().GetLogger() + defer dc.Close() + dc.Create(spec) + + tests := []struct { + name string + req func() *adminv2.NetworkServiceListExternalMembersRequest + want func() *adminv2.NetworkServiceListExternalMembersResponse + wantErr error + }{ + { + name: "network not found", + req: func() *adminv2.NetworkServiceListExternalMembersRequest { + return &adminv2.NetworkServiceListExternalMembersRequest{ + Network: "unknown-network", + } + }, + wantErr: errorutil.NotFound(`no network with id "unknown-network" found`), + }, + { + name: "list all", + req: func() *adminv2.NetworkServiceListExternalMembersRequest { + return &adminv2.NetworkServiceListExternalMembersRequest{ + Network: sc.NetworkExternal, + } + }, + want: func() *adminv2.NetworkServiceListExternalMembersResponse { + return &adminv2.NetworkServiceListExternalMembersResponse{ + Network: dc.GetNetworks()[sc.NetworkExternal], + Members: []*apiv2.ExternalNetworkMember{ + { + Switch: sc.P01Rack01Switch1, + Partition: sc.Partition1, + Rack: sc.P01Rack01, + Ports: []string{"Ethernet1"}, + }, + { + Switch: sc.P01Rack01Switch2, + Partition: sc.Partition1, + Rack: sc.P01Rack01, + Ports: []string{"Ethernet1"}, + }, + { + Switch: sc.P01Rack02Switch1, + Partition: sc.Partition1, + Rack: sc.P01Rack02, + Ports: []string{"Ethernet0", "Ethernet1"}, + }, + { + Switch: sc.P01Rack02Switch2, + Partition: sc.Partition1, + Rack: sc.P01Rack02, + Ports: []string{"Ethernet0", "Ethernet1"}, + }, + { + Switch: sc.P02Rack01Switch1, + Partition: sc.Partition2, + Rack: sc.P02Rack01, + Ports: []string{"Ethernet0"}, + }, + { + Switch: sc.P02Rack01Switch2, + Partition: sc.Partition2, + Rack: sc.P02Rack01, + Ports: []string{"Ethernet0"}, + }, + }, + } + }, + }, + { + name: "list by partition", + req: func() *adminv2.NetworkServiceListExternalMembersRequest { + return &adminv2.NetworkServiceListExternalMembersRequest{ + Network: sc.NetworkExternal, + Query: &apiv2.ExternalNetworkMemberQuery{ + Partition: new(sc.Partition1), + }, + } + }, + want: func() *adminv2.NetworkServiceListExternalMembersResponse { + return &adminv2.NetworkServiceListExternalMembersResponse{ + Network: dc.GetNetworks()[sc.NetworkExternal], + Members: []*apiv2.ExternalNetworkMember{ + { + Switch: sc.P01Rack01Switch1, + Partition: sc.Partition1, + Rack: sc.P01Rack01, + Ports: []string{"Ethernet1"}, + }, + { + Switch: sc.P01Rack01Switch2, + Partition: sc.Partition1, + Rack: sc.P01Rack01, + Ports: []string{"Ethernet1"}, + }, + { + Switch: sc.P01Rack02Switch1, + Partition: sc.Partition1, + Rack: sc.P01Rack02, + Ports: []string{"Ethernet0", "Ethernet1"}, + }, + { + Switch: sc.P01Rack02Switch2, + Partition: sc.Partition1, + Rack: sc.P01Rack02, + Ports: []string{"Ethernet0", "Ethernet1"}, + }, + }, + } + }, + }, + { + name: "list by rack", + req: func() *adminv2.NetworkServiceListExternalMembersRequest { + return &adminv2.NetworkServiceListExternalMembersRequest{ + Network: sc.NetworkExternal, + Query: &apiv2.ExternalNetworkMemberQuery{ + Rack: new(sc.P01Rack02), + }, + } + }, + want: func() *adminv2.NetworkServiceListExternalMembersResponse { + return &adminv2.NetworkServiceListExternalMembersResponse{ + Network: dc.GetNetworks()[sc.NetworkExternal], + Members: []*apiv2.ExternalNetworkMember{ + { + Switch: sc.P01Rack02Switch1, + Partition: sc.Partition1, + Rack: sc.P01Rack02, + Ports: []string{"Ethernet0", "Ethernet1"}, + }, + { + Switch: sc.P01Rack02Switch2, + Partition: sc.Partition1, + Rack: sc.P01Rack02, + Ports: []string{"Ethernet0", "Ethernet1"}, + }, + }, + } + }, + }, + { + name: "list by switch", + req: func() *adminv2.NetworkServiceListExternalMembersRequest { + return &adminv2.NetworkServiceListExternalMembersRequest{ + Network: sc.NetworkExternal, + Query: &apiv2.ExternalNetworkMemberQuery{ + Switch: new(sc.P01Rack02Switch2), + }, + } + }, + want: func() *adminv2.NetworkServiceListExternalMembersResponse { + return &adminv2.NetworkServiceListExternalMembersResponse{ + Network: dc.GetNetworks()[sc.NetworkExternal], + Members: []*apiv2.ExternalNetworkMember{ + { + Switch: sc.P01Rack02Switch2, + Partition: sc.Partition1, + Rack: sc.P01Rack02, + Ports: []string{"Ethernet0", "Ethernet1"}, + }, + }, + } + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + n := &networkServiceServer{ + log: log, + repo: dc.GetTestStore().Store, + } + + var ( + req *adminv2.NetworkServiceListExternalMembersRequest + want *adminv2.NetworkServiceListExternalMembersResponse + ) + + if tt.req != nil { + req = tt.req() + } + if tt.want != nil { + want = tt.want() + } + + test.Validate(t, req) + got, err := n.ListExternalMembers(ctx, req) + if diff := cmp.Diff(tt.wantErr, err, errorutil.ConnectErrorComparer()); diff != "" { + t.Errorf("networkServiceServer.ListExternalMembers() error diff = %s", diff) + return + } + + if tt.wantErr != nil { + return + } + + slices.SortFunc(got.Members, func(a, b *apiv2.ExternalNetworkMember) int { + return strings.Compare(a.Switch, b.Switch) + }) + + if diff := cmp.Diff(want, got, protocmp.Transform()); diff != "" { + t.Errorf("networkServiceServer.ListExternalMembers() diff = %s", diff) + } + + err = dc.Assert(nil) + require.NoError(t, err) + }) + } +} + +func Test_networkServiceServer_AddExternalMember(t *testing.T) { + ctx := t.Context() + + dc := test.NewDatacenter(t) + log := dc.GetTestStore().GetLogger() + defer dc.Close() + + spec, err := sc.SwitchesWithExternalNetworkMembers.DeepCopy() + require.NoError(t, err) + + require.NoError(t, sc.AddNicsToVRF(spec.Switches[0], "Vrf100", apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_INTERNAL, "Ethernet0")) + require.NoError(t, sc.AddNicsToVRF(spec.Switches[1], "Vrf100", apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_INTERNAL, "Ethernet0")) + require.NoError(t, sc.AddNicsToVRF(spec.Switches[4], "Vrf100", apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_EXTERNAL, "Ethernet0")) + require.NoError(t, sc.AddNicsToVRF(spec.Switches[5], "Vrf100", apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_EXTERNAL, "Ethernet0")) + + dc.Create(spec) + + tests := []struct { + name string + req func() *adminv2.NetworkServiceAddExternalMembersRequest + mods func() *test.Asserters + want func() *adminv2.NetworkServiceAddExternalMembersResponse + wantErr error + }{ + { + name: "network not found", + req: func() *adminv2.NetworkServiceAddExternalMembersRequest { + return &adminv2.NetworkServiceAddExternalMembersRequest{ + Network: "unknown-network", + Rack: sc.P01Rack01, + } + }, + wantErr: errorutil.NotFound(`no network with id "unknown-network" found`), + }, + { + name: "cannot add external members to underlay network", + req: func() *adminv2.NetworkServiceAddExternalMembersRequest { + return &adminv2.NetworkServiceAddExternalMembersRequest{ + Network: sc.NetworkUnderlayPartition1, + Rack: sc.P01Rack02, + Ports: []string{"Ethernet0", "Ethernet1"}, + } + }, + wantErr: errorutil.InvalidArgument(`cannot add external members to network of type %q`, apiv2.NetworkType_NETWORK_TYPE_UNDERLAY), + }, + { + name: "cannot add external members to super network", + req: func() *adminv2.NetworkServiceAddExternalMembersRequest { + return &adminv2.NetworkServiceAddExternalMembersRequest{ + Network: sc.NetworkSuper, + Rack: sc.P01Rack02, + Ports: []string{"Ethernet0", "Ethernet1"}, + } + }, + wantErr: errorutil.InvalidArgument(`cannot add external members to network of type %q`, apiv2.NetworkType_NETWORK_TYPE_SUPER), + }, + { + name: "cannot add external members to super namespaced network", + req: func() *adminv2.NetworkServiceAddExternalMembersRequest { + return &adminv2.NetworkServiceAddExternalMembersRequest{ + Network: sc.NetworkSuperNamespaced, + Rack: sc.P01Rack02, + Ports: []string{"Ethernet0", "Ethernet1"}, + } + }, + wantErr: errorutil.InvalidArgument(`cannot add external members to network of type %q`, apiv2.NetworkType_NETWORK_TYPE_SUPER_NAMESPACED), + }, + { + name: "no switches in rack found", + req: func() *adminv2.NetworkServiceAddExternalMembersRequest { + return &adminv2.NetworkServiceAddExternalMembersRequest{ + Network: sc.NetworkNameTenantPartition1, + Rack: "unknown-rack", + } + }, + wantErr: errorutil.NotFound(`no switches in rack "unknown-rack" found`), + }, + { + name: "add switches of different partition to partition scoped network fails", + req: func() *adminv2.NetworkServiceAddExternalMembersRequest { + return &adminv2.NetworkServiceAddExternalMembersRequest{ + Network: sc.NetworkNameTenantPartition1, + Rack: sc.P02Rack01, + Ports: []string{"Ethernet0"}, + } + }, + wantErr: errorutil.InvalidArgument("cannot add switches of partition %q as members to network scoped to partition %q", sc.Partition2, sc.Partition1), + }, + { + name: "invalid port", + req: func() *adminv2.NetworkServiceAddExternalMembersRequest { + return &adminv2.NetworkServiceAddExternalMembersRequest{ + Network: sc.NetworkExternal, + Rack: sc.P01Rack01, + Ports: []string{"Ethernet120"}, + } + }, + wantErr: errorutil.NotFound(`port "Ethernet120" not found on switch %q`, sc.P01Rack01Switch1), + }, + { + name: "cannot add internal port as external member", + req: func() *adminv2.NetworkServiceAddExternalMembersRequest { + return &adminv2.NetworkServiceAddExternalMembersRequest{ + Network: sc.NetworkExternal, + Rack: sc.P01Rack01, + Ports: []string{"Ethernet0"}, + } + }, + wantErr: errorutil.InvalidArgument(`cannot add internal port "Ethernet0" of rack %q as external member`, sc.P01Rack01), + }, + { + name: "trying to add port that is already members of a network fails", + req: func() *adminv2.NetworkServiceAddExternalMembersRequest { + return &adminv2.NetworkServiceAddExternalMembersRequest{ + Network: sc.NetworkExternal, + Rack: sc.P02Rack01, + Ports: []string{"Ethernet0"}, + } + }, + wantErr: errorutil.InvalidArgument(`port "Ethernet0" of switches in rack %q is already member of network %q`, sc.P02Rack01, sc.NetworkExternal), + }, + { + name: "successfully add externmal members", + req: func() *adminv2.NetworkServiceAddExternalMembersRequest { + return &adminv2.NetworkServiceAddExternalMembersRequest{ + Network: sc.NetworkNameTenantPartition1, + Rack: sc.P01Rack02, + Ports: []string{"Ethernet0", "Ethernet1"}, + } + }, + mods: func() *test.Asserters { + return &test.Asserters{ + Switches: func(switches map[string]*apiv2.Switch) { + sw1 := switches[sc.P01Rack02Switch1] + require.NotNil(t, sw1) + sw1.Nics[0].Vrf = new("Vrf99") + sw1.Nics[0].Membership = apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_EXTERNAL + sw1.Nics[1].Vrf = new("Vrf99") + sw1.Nics[1].Membership = apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_EXTERNAL + + sw2 := switches[sc.P01Rack02Switch2] + require.NotNil(t, sw2) + sw2.Nics[0].Vrf = new("Vrf99") + sw2.Nics[0].Membership = apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_EXTERNAL + sw2.Nics[1].Vrf = new("Vrf99") + sw2.Nics[1].Membership = apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_EXTERNAL + }, + } + }, + want: func() *adminv2.NetworkServiceAddExternalMembersResponse { + allSwitches := dc.GetSwitches() + + sw1 := allSwitches[sc.P01Rack02Switch1] + require.NotNil(t, sw1) + sw1.Nics[0].Vrf = new("Vrf99") + sw1.Nics[0].Membership = apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_EXTERNAL + sw1.Nics[1].Vrf = new("Vrf99") + sw1.Nics[1].Membership = apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_EXTERNAL + + sw2 := allSwitches[sc.P01Rack02Switch2] + require.NotNil(t, sw2) + sw2.Nics[0].Vrf = new("Vrf99") + sw2.Nics[0].Membership = apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_EXTERNAL + sw2.Nics[1].Vrf = new("Vrf99") + sw2.Nics[1].Membership = apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_EXTERNAL + + return &adminv2.NetworkServiceAddExternalMembersResponse{ + Network: dc.GetNetworks()[sc.NetworkNameTenantPartition1], + Members: []*apiv2.ExternalNetworkMember{ + { + Switch: sw1.Id, + Partition: sw1.Partition, + Rack: pointer.SafeDeref(sw1.Rack), + Ports: []string{"Ethernet0", "Ethernet1"}, + }, + { + Switch: sw2.Id, + Partition: sw2.Partition, + Rack: pointer.SafeDeref(sw2.Rack), + Ports: []string{"Ethernet0", "Ethernet1"}, + }, + }, + } + }, + wantErr: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + n := &networkServiceServer{ + log: log, + repo: dc.GetTestStore().Store, + } + + var ( + req *adminv2.NetworkServiceAddExternalMembersRequest + want *adminv2.NetworkServiceAddExternalMembersResponse + ) + + if tt.req != nil { + req = tt.req() + } + if tt.want != nil { + want = tt.want() + } + + test.Validate(t, req) + got, err := n.AddExternalMembers(ctx, req) + if diff := cmp.Diff(tt.wantErr, err, errorutil.ConnectErrorComparer()); diff != "" { + t.Errorf("networkServiceServer.AddExternalMembers() error diff = %s", diff) + return + } + + if tt.wantErr != nil { + return + } + + slices.SortFunc(got.Members, func(a, b *apiv2.ExternalNetworkMember) int { + return strings.Compare(a.Switch, b.Switch) + }) + + if diff := cmp.Diff(want, got, + protocmp.Transform(), + protocmp.IgnoreFields(&apiv2.Meta{}, "created_at", "updated_at"), + ); diff != "" { + t.Errorf("networkServiceServer.AddExternalMembers() diff = %s", diff) + return + } + + var mods *test.Asserters + if tt.mods != nil { + mods = tt.mods() + } + err = dc.Assert(mods) + require.NoError(t, err) + }) + } +} + +func Test_networkServiceServer_RemoveExternalMember(t *testing.T) { + ctx := t.Context() + + dc := test.NewDatacenter(t) + log := dc.GetTestStore().GetLogger() + defer dc.Close() + + spec, err := sc.SwitchesWithExternalNetworkMembers.DeepCopy() + require.NoError(t, err) + + require.NoError(t, sc.AddNicsToVRF(spec.Switches[0], "Vrf100", apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_INTERNAL, "Ethernet0")) + require.NoError(t, sc.AddNicsToVRF(spec.Switches[1], "Vrf100", apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_INTERNAL, "Ethernet0")) + require.NoError(t, sc.AddNicsToVRF(spec.Switches[1], "Vrf100", apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_EXTERNAL, "Ethernet120")) + require.NoError(t, sc.AddNicsToVRF(spec.Switches[4], "Vrf100", apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_EXTERNAL, "Ethernet0", "Ethernet1")) + require.NoError(t, sc.AddNicsToVRF(spec.Switches[5], "Vrf100", apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_EXTERNAL, "Ethernet0", "Ethernet1")) + dc.Create(spec) + + tests := []struct { + name string + mods func() *test.Asserters + req func() *adminv2.NetworkServiceRemoveExternalMembersRequest + want func() *adminv2.NetworkServiceRemoveExternalMembersResponse + wantErr error + }{ + { + name: "network not found", + req: func() *adminv2.NetworkServiceRemoveExternalMembersRequest { + return &adminv2.NetworkServiceRemoveExternalMembersRequest{ + Network: "unknown-network", + Rack: sc.P01Rack01, + } + }, + wantErr: errorutil.NotFound(`no network with id "unknown-network" found`), + }, + { + name: "no switches in rack found", + req: func() *adminv2.NetworkServiceRemoveExternalMembersRequest { + return &adminv2.NetworkServiceRemoveExternalMembersRequest{ + Network: sc.NetworkExternal, + Rack: "unknown-rack", + } + }, + wantErr: errorutil.NotFound(`no switches in rack "unknown-rack" found`), + }, + { + name: "invalid port", + req: func() *adminv2.NetworkServiceRemoveExternalMembersRequest { + return &adminv2.NetworkServiceRemoveExternalMembersRequest{ + Network: sc.NetworkExternal, + Rack: sc.P01Rack01, + Ports: []string{"Ethernet120"}, + } + }, + wantErr: errorutil.NotFound(`port "Ethernet120" not found on switch %q`, sc.P01Rack01Switch1), + }, + { + name: "port is not member of the network", + req: func() *adminv2.NetworkServiceRemoveExternalMembersRequest { + return &adminv2.NetworkServiceRemoveExternalMembersRequest{ + Network: sc.NetworkNameTenantPartition1, + Rack: sc.P02Rack01, + Ports: []string{"Ethernet0"}, + } + }, + wantErr: errorutil.InvalidArgument(`port "Ethernet0" is not a member of network %q`, sc.NetworkNameTenantPartition1), + }, + { + name: "cannot remove port that is connected to a machine", + req: func() *adminv2.NetworkServiceRemoveExternalMembersRequest { + return &adminv2.NetworkServiceRemoveExternalMembersRequest{ + Network: sc.NetworkExternal, + Rack: sc.P01Rack01, + Ports: []string{"Ethernet0"}, + } + }, + wantErr: errorutil.InvalidArgument(`port "Ethernet0" of rack %q is not an external member`, sc.P01Rack01), + }, + { + name: "successfully remove network members", + mods: func() *test.Asserters { + return &test.Asserters{ + Switches: func(switches map[string]*apiv2.Switch) { + sw1 := switches[sc.P02Rack01Switch1] + require.NotNil(t, sw1) + sw1.Nics[0].Vrf = nil + sw1.Nics[0].Membership = apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_UNMANAGED + sw1.Nics[1].Vrf = nil + sw1.Nics[1].Membership = apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_UNMANAGED + + sw2 := switches[sc.P02Rack01Switch2] + require.NotNil(t, sw2) + sw2.Nics[0].Vrf = nil + sw2.Nics[0].Membership = apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_UNMANAGED + sw2.Nics[1].Vrf = nil + sw2.Nics[1].Membership = apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_UNMANAGED + }, + } + }, + req: func() *adminv2.NetworkServiceRemoveExternalMembersRequest { + return &adminv2.NetworkServiceRemoveExternalMembersRequest{ + Network: sc.NetworkExternal, + Rack: sc.P02Rack01, + Ports: []string{"Ethernet0", "Ethernet1"}, + } + }, + want: func() *adminv2.NetworkServiceRemoveExternalMembersResponse { + allSwitches := dc.GetSwitches() + + sw1 := allSwitches[sc.P02Rack01Switch1] + require.NotNil(t, sw1) + sw1.Nics[0].Vrf = nil + sw1.Nics[0].Membership = apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_UNMANAGED + sw1.Nics[1].Vrf = nil + sw1.Nics[1].Membership = apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_UNMANAGED + + sw2 := allSwitches[sc.P02Rack01Switch2] + require.NotNil(t, sw2) + sw2.Nics[0].Vrf = nil + sw2.Nics[0].Membership = apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_UNMANAGED + sw2.Nics[1].Vrf = nil + sw2.Nics[1].Membership = apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_UNMANAGED + + return &adminv2.NetworkServiceRemoveExternalMembersResponse{ + Network: dc.GetNetworks()[sc.NetworkExternal], + Members: []*apiv2.ExternalNetworkMember{ + { + Switch: sw1.Id, + Partition: sw1.Partition, + Rack: pointer.SafeDeref(sw1.Rack), + Ports: []string{"Ethernet0", "Ethernet1"}, + }, + { + Switch: sw2.Id, + Partition: sw2.Partition, + Rack: pointer.SafeDeref(sw2.Rack), + Ports: []string{"Ethernet0", "Ethernet1"}, + }, + }, + } + }, + wantErr: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + n := &networkServiceServer{ + log: log, + repo: dc.GetTestStore().Store, + } + + var ( + req *adminv2.NetworkServiceRemoveExternalMembersRequest + want *adminv2.NetworkServiceRemoveExternalMembersResponse + ) + + if tt.req != nil { + req = tt.req() + } + if tt.want != nil { + want = tt.want() + } + + test.Validate(t, req) + got, err := n.RemoveExternalMembers(ctx, req) + if diff := cmp.Diff(tt.wantErr, err, errorutil.ConnectErrorComparer()); diff != "" { + t.Errorf("networkServiceServer.RemoveExternalMembers() error diff = %s", diff) + return + } + + if tt.wantErr != nil { + return + } + + slices.SortFunc(got.Members, func(a, b *apiv2.ExternalNetworkMember) int { + return strings.Compare(a.Switch, b.Switch) + }) + + if diff := cmp.Diff(want, got, protocmp.Transform()); diff != "" { + t.Errorf("networkServiceServer.RemoveExternalMembers() diff = %s", diff) + } + + var mods *test.Asserters + if tt.mods != nil { + mods = tt.mods() + } + err = dc.Assert(mods) + require.NoError(t, err) + }) + } +} diff --git a/pkg/service/admin/switch/switch-service_test.go b/pkg/service/admin/switch/switch-service_test.go index 8e5a4d16..67a73f12 100644 --- a/pkg/service/admin/switch/switch-service_test.go +++ b/pkg/service/admin/switch/switch-service_test.go @@ -288,6 +288,7 @@ func Test_switchServiceServer_Update(t *testing.T) { Mac: new("11:11:11:11:11:11"), Vrf: new("Vrf100"), BgpFilter: &apiv2.BGPFilter{}, + Membership: apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_INTERNAL, State: &apiv2.NicState{ Actual: apiv2.SwitchPortStatus_SWITCH_PORT_STATUS_UP, }, @@ -307,6 +308,7 @@ func Test_switchServiceServer_Update(t *testing.T) { Mac: new("aa:aa:aa:aa:aa:aa"), Vrf: nil, BgpFilter: &apiv2.BGPFilter{}, + Membership: apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_UNMANAGED, State: &apiv2.NicState{ Desired: apiv2.SwitchPortStatus_SWITCH_PORT_STATUS_UP.Enum(), Actual: apiv2.SwitchPortStatus_SWITCH_PORT_STATUS_UP, @@ -328,6 +330,7 @@ func Test_switchServiceServer_Update(t *testing.T) { Mac: new("11:11:11:11:11:11"), Vrf: new("Vrf100"), BgpFilter: &apiv2.BGPFilter{}, + Membership: apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_INTERNAL, State: &apiv2.NicState{ Actual: apiv2.SwitchPortStatus_SWITCH_PORT_STATUS_UP, }, @@ -347,6 +350,7 @@ func Test_switchServiceServer_Update(t *testing.T) { Mac: new("aa:aa:aa:aa:aa:aa"), Vrf: nil, BgpFilter: &apiv2.BGPFilter{}, + Membership: apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_UNMANAGED, State: &apiv2.NicState{ Desired: apiv2.SwitchPortStatus_SWITCH_PORT_STATUS_UP.Enum(), Actual: apiv2.SwitchPortStatus_SWITCH_PORT_STATUS_UP, @@ -381,6 +385,7 @@ func Test_switchServiceServer_Update(t *testing.T) { Mac: new("11:11:11:11:11:11"), Vrf: new("Vrf100"), BgpFilter: &apiv2.BGPFilter{}, + Membership: apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_INTERNAL, State: &apiv2.NicState{ Actual: apiv2.SwitchPortStatus_SWITCH_PORT_STATUS_UP, }, @@ -400,6 +405,7 @@ func Test_switchServiceServer_Update(t *testing.T) { Mac: new("aa:aa:aa:aa:aa:aa"), Vrf: nil, BgpFilter: &apiv2.BGPFilter{}, + Membership: apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_UNMANAGED, State: &apiv2.NicState{ Desired: apiv2.SwitchPortStatus_SWITCH_PORT_STATUS_UP.Enum(), Actual: apiv2.SwitchPortStatus_SWITCH_PORT_STATUS_UP, @@ -694,6 +700,12 @@ func Test_switchServiceServer_Port(t *testing.T) { want: func(dc *test.Datacenter) *adminv2.SwitchServicePortResponse { sw := dc.GetSwitches()[sc.P01Rack01Switch1] sw.Nics[0].State.Desired = apiv2.SwitchPortStatus_SWITCH_PORT_STATUS_DOWN.Enum() + con, found := lo.Find(sw.MachineConnections, func(c *apiv2.MachineConnection) bool { + return c.Nic.Name == "Ethernet0" + }) + require.True(t, found) + con.Nic.State.Desired = apiv2.SwitchPortStatus_SWITCH_PORT_STATUS_DOWN.Enum() + return &adminv2.SwitchServicePortResponse{ Switch: sw, } @@ -802,6 +814,7 @@ func Test_switchServiceServer_Migrate(t *testing.T) { return n.Name == "Ethernet0" }) require.True(t, found) + nic.Membership = apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_INTERNAL sw.MachineConnections = []*apiv2.MachineConnection{ { MachineId: sc.Machine2, @@ -824,6 +837,7 @@ func Test_switchServiceServer_Migrate(t *testing.T) { return n.Name == "Ethernet0" }) require.True(t, found) + nic.Membership = apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_INTERNAL sw2.MachineConnections = []*apiv2.MachineConnection{ { MachineId: sc.Machine2, @@ -849,6 +863,7 @@ func Test_switchServiceServer_Migrate(t *testing.T) { return n.Name == "swp1s0" }) require.True(t, found) + nic.Membership = apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_INTERNAL sw.MachineConnections = []*apiv2.MachineConnection{ { MachineId: sc.Machine3, @@ -870,6 +885,7 @@ func Test_switchServiceServer_Migrate(t *testing.T) { return n.Name == "swp1s0" }) require.True(t, found) + nic.Membership = apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_INTERNAL sw1.MachineConnections = []*apiv2.MachineConnection{} sw2.MachineConnections = []*apiv2.MachineConnection{ { @@ -959,6 +975,7 @@ func Test_switchServiceServer_ConnectedMachines(t *testing.T) { State: &apiv2.NicState{ Actual: apiv2.SwitchPortStatus_SWITCH_PORT_STATUS_UP, }, + Membership: apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_INTERNAL, }, Machine: dc.GetMachines()[sc.Machine1], Fru: &apiv2.MachineFRU{ @@ -984,6 +1001,7 @@ func Test_switchServiceServer_ConnectedMachines(t *testing.T) { Name: "Ethernet0", Identifier: "Ethernet0", BgpFilter: &apiv2.BGPFilter{}, + Membership: apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_INTERNAL, State: &apiv2.NicState{ Actual: apiv2.SwitchPortStatus_SWITCH_PORT_STATUS_UP, }, @@ -1017,6 +1035,7 @@ func Test_switchServiceServer_ConnectedMachines(t *testing.T) { Name: "swp1s0", Identifier: "swp1s0", BgpFilter: &apiv2.BGPFilter{}, + Membership: apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_INTERNAL, State: &apiv2.NicState{ Actual: apiv2.SwitchPortStatus_SWITCH_PORT_STATUS_UP, }, @@ -1050,6 +1069,7 @@ func Test_switchServiceServer_ConnectedMachines(t *testing.T) { Name: "Ethernet0", Identifier: "Ethernet0", BgpFilter: &apiv2.BGPFilter{}, + Membership: apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_INTERNAL, State: &apiv2.NicState{ Actual: apiv2.SwitchPortStatus_SWITCH_PORT_STATUS_UP, }, @@ -1093,6 +1113,7 @@ func Test_switchServiceServer_ConnectedMachines(t *testing.T) { Name: "Ethernet0", Identifier: "Ethernet0", BgpFilter: &apiv2.BGPFilter{}, + Membership: apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_INTERNAL, State: &apiv2.NicState{ Actual: apiv2.SwitchPortStatus_SWITCH_PORT_STATUS_UP, }, @@ -1121,6 +1142,7 @@ func Test_switchServiceServer_ConnectedMachines(t *testing.T) { Name: "Ethernet0", Identifier: "Ethernet0", BgpFilter: &apiv2.BGPFilter{}, + Membership: apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_INTERNAL, State: &apiv2.NicState{ Actual: apiv2.SwitchPortStatus_SWITCH_PORT_STATUS_UP, }, @@ -1154,6 +1176,7 @@ func Test_switchServiceServer_ConnectedMachines(t *testing.T) { Name: "Ethernet0", Identifier: "Ethernet0", BgpFilter: &apiv2.BGPFilter{}, + Membership: apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_INTERNAL, State: &apiv2.NicState{ Actual: apiv2.SwitchPortStatus_SWITCH_PORT_STATUS_UP, }, @@ -1175,6 +1198,7 @@ func Test_switchServiceServer_ConnectedMachines(t *testing.T) { Name: "Ethernet1", Identifier: "Ethernet1", BgpFilter: &apiv2.BGPFilter{}, + Membership: apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_INTERNAL, State: &apiv2.NicState{ Actual: apiv2.SwitchPortStatus_SWITCH_PORT_STATUS_UP, }, @@ -1203,6 +1227,7 @@ func Test_switchServiceServer_ConnectedMachines(t *testing.T) { Name: "Ethernet0", Identifier: "Ethernet0", BgpFilter: &apiv2.BGPFilter{}, + Membership: apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_INTERNAL, State: &apiv2.NicState{ Actual: apiv2.SwitchPortStatus_SWITCH_PORT_STATUS_UP, }, @@ -1224,6 +1249,7 @@ func Test_switchServiceServer_ConnectedMachines(t *testing.T) { Name: "Ethernet1", Identifier: "Ethernet1", BgpFilter: &apiv2.BGPFilter{}, + Membership: apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_INTERNAL, State: &apiv2.NicState{ Actual: apiv2.SwitchPortStatus_SWITCH_PORT_STATUS_UP, }, @@ -1252,6 +1278,7 @@ func Test_switchServiceServer_ConnectedMachines(t *testing.T) { Name: "Ethernet0", Identifier: "Ethernet0", BgpFilter: &apiv2.BGPFilter{}, + Membership: apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_INTERNAL, State: &apiv2.NicState{ Actual: apiv2.SwitchPortStatus_SWITCH_PORT_STATUS_UP, }, @@ -1280,6 +1307,7 @@ func Test_switchServiceServer_ConnectedMachines(t *testing.T) { Name: "Ethernet0", Identifier: "Ethernet0", BgpFilter: &apiv2.BGPFilter{}, + Membership: apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_INTERNAL, State: &apiv2.NicState{ Actual: apiv2.SwitchPortStatus_SWITCH_PORT_STATUS_UP, }, @@ -1308,6 +1336,7 @@ func Test_switchServiceServer_ConnectedMachines(t *testing.T) { Name: "swp1s0", Identifier: "swp1s0", BgpFilter: &apiv2.BGPFilter{}, + Membership: apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_INTERNAL, State: &apiv2.NicState{ Actual: apiv2.SwitchPortStatus_SWITCH_PORT_STATUS_UP, }, @@ -1350,6 +1379,7 @@ func Test_switchServiceServer_ConnectedMachines(t *testing.T) { Name: "Ethernet0", Identifier: "Ethernet0", BgpFilter: &apiv2.BGPFilter{}, + Membership: apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_INTERNAL, State: &apiv2.NicState{ Actual: apiv2.SwitchPortStatus_SWITCH_PORT_STATUS_UP, }, @@ -1392,6 +1422,7 @@ func Test_switchServiceServer_ConnectedMachines(t *testing.T) { Name: "Ethernet1", Identifier: "Ethernet1", BgpFilter: &apiv2.BGPFilter{}, + Membership: apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_INTERNAL, State: &apiv2.NicState{ Actual: apiv2.SwitchPortStatus_SWITCH_PORT_STATUS_UP, }, @@ -1420,6 +1451,7 @@ func Test_switchServiceServer_ConnectedMachines(t *testing.T) { Name: "Ethernet1", Identifier: "Ethernet1", BgpFilter: &apiv2.BGPFilter{}, + Membership: apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_INTERNAL, State: &apiv2.NicState{ Actual: apiv2.SwitchPortStatus_SWITCH_PORT_STATUS_UP, }, @@ -1463,6 +1495,7 @@ func Test_switchServiceServer_ConnectedMachines(t *testing.T) { Name: "Ethernet0", Identifier: "Ethernet0", BgpFilter: &apiv2.BGPFilter{}, + Membership: apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_INTERNAL, State: &apiv2.NicState{ Actual: apiv2.SwitchPortStatus_SWITCH_PORT_STATUS_UP, }, @@ -1491,6 +1524,7 @@ func Test_switchServiceServer_ConnectedMachines(t *testing.T) { Name: "Ethernet0", Identifier: "Ethernet0", BgpFilter: &apiv2.BGPFilter{}, + Membership: apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_INTERNAL, State: &apiv2.NicState{ Actual: apiv2.SwitchPortStatus_SWITCH_PORT_STATUS_UP, }, @@ -1524,6 +1558,7 @@ func Test_switchServiceServer_ConnectedMachines(t *testing.T) { Name: "swp1s0", Identifier: "swp1s0", BgpFilter: &apiv2.BGPFilter{}, + Membership: apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_INTERNAL, State: &apiv2.NicState{ Actual: apiv2.SwitchPortStatus_SWITCH_PORT_STATUS_UP, }, @@ -1557,6 +1592,7 @@ func Test_switchServiceServer_ConnectedMachines(t *testing.T) { Name: "Ethernet0", Identifier: "Ethernet0", BgpFilter: &apiv2.BGPFilter{}, + Membership: apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_INTERNAL, State: &apiv2.NicState{ Actual: apiv2.SwitchPortStatus_SWITCH_PORT_STATUS_UP, }, diff --git a/pkg/service/infra/boot/boot-service_test.go b/pkg/service/infra/boot/boot-service_test.go index bdc7ed6a..90f67abc 100644 --- a/pkg/service/infra/boot/boot-service_test.go +++ b/pkg/service/infra/boot/boot-service_test.go @@ -475,9 +475,15 @@ func Test_bootServiceServer_Register(t *testing.T) { Rack: new("r01"), ReplaceMode: apiv2.SwitchReplaceMode_SWITCH_REPLACE_MODE_OPERATIONAL, MachineConnections: []*apiv2.MachineConnection{ - {MachineId: m99, Nic: &apiv2.SwitchNic{ - Name: "Ethernet0", Identifier: "Eth1/1", BgpFilter: &apiv2.BGPFilter{}, State: &apiv2.NicState{Actual: apiv2.SwitchPortStatus_SWITCH_PORT_STATUS_UP}, - }}, + { + MachineId: m99, Nic: &apiv2.SwitchNic{ + Name: "Ethernet0", + Identifier: "Eth1/1", + BgpFilter: &apiv2.BGPFilter{}, + State: &apiv2.NicState{Actual: apiv2.SwitchPortStatus_SWITCH_PORT_STATUS_UP}, + Membership: apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_INTERNAL, + }, + }, }, Nics: []*apiv2.SwitchNic{ { @@ -487,6 +493,7 @@ func Test_bootServiceServer_Register(t *testing.T) { State: &apiv2.NicState{ Actual: apiv2.SwitchPortStatus_SWITCH_PORT_STATUS_UP, }, + Membership: apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_INTERNAL, }, { Name: "Ethernet1", @@ -495,6 +502,7 @@ func Test_bootServiceServer_Register(t *testing.T) { State: &apiv2.NicState{ Actual: apiv2.SwitchPortStatus_SWITCH_PORT_STATUS_UP, }, + Membership: apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_UNMANAGED, }, }, Os: &apiv2.SwitchOS{ @@ -508,9 +516,15 @@ func Test_bootServiceServer_Register(t *testing.T) { Rack: new("r01"), ReplaceMode: apiv2.SwitchReplaceMode_SWITCH_REPLACE_MODE_OPERATIONAL, MachineConnections: []*apiv2.MachineConnection{ - {MachineId: m99, Nic: &apiv2.SwitchNic{ - Name: "Ethernet0", Identifier: "Eth1/1", BgpFilter: &apiv2.BGPFilter{}, State: &apiv2.NicState{Actual: apiv2.SwitchPortStatus_SWITCH_PORT_STATUS_UP}, - }}, + { + MachineId: m99, Nic: &apiv2.SwitchNic{ + Name: "Ethernet0", + Identifier: "Eth1/1", + BgpFilter: &apiv2.BGPFilter{}, + State: &apiv2.NicState{Actual: apiv2.SwitchPortStatus_SWITCH_PORT_STATUS_UP}, + Membership: apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_INTERNAL, + }, + }, }, Nics: []*apiv2.SwitchNic{ { @@ -520,6 +534,7 @@ func Test_bootServiceServer_Register(t *testing.T) { State: &apiv2.NicState{ Actual: apiv2.SwitchPortStatus_SWITCH_PORT_STATUS_UP, }, + Membership: apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_INTERNAL, }, { Name: "Ethernet1", @@ -528,6 +543,7 @@ func Test_bootServiceServer_Register(t *testing.T) { State: &apiv2.NicState{ Actual: apiv2.SwitchPortStatus_SWITCH_PORT_STATUS_UP, }, + Membership: apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_UNMANAGED, }, }, Os: &apiv2.SwitchOS{ @@ -610,8 +626,24 @@ func Test_bootServiceServer_Register(t *testing.T) { Rack: new("r01"), ReplaceMode: apiv2.SwitchReplaceMode_SWITCH_REPLACE_MODE_OPERATIONAL, MachineConnections: []*apiv2.MachineConnection{ - {MachineId: m1, Nic: &apiv2.SwitchNic{Name: "Ethernet1", Identifier: "Eth1/2", BgpFilter: &apiv2.BGPFilter{}, State: &apiv2.NicState{Actual: apiv2.SwitchPortStatus_SWITCH_PORT_STATUS_UP}}}, - {MachineId: m99, Nic: &apiv2.SwitchNic{Name: "Ethernet0", Identifier: "Eth1/1", BgpFilter: &apiv2.BGPFilter{}, State: &apiv2.NicState{Actual: apiv2.SwitchPortStatus_SWITCH_PORT_STATUS_UP}}}, + { + MachineId: m1, + Nic: &apiv2.SwitchNic{Name: "Ethernet1", + Identifier: "Eth1/2", + BgpFilter: &apiv2.BGPFilter{}, + State: &apiv2.NicState{Actual: apiv2.SwitchPortStatus_SWITCH_PORT_STATUS_UP}, + Membership: apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_INTERNAL, + }, + }, + { + MachineId: m99, + Nic: &apiv2.SwitchNic{Name: "Ethernet0", + Identifier: "Eth1/1", + BgpFilter: &apiv2.BGPFilter{}, + State: &apiv2.NicState{Actual: apiv2.SwitchPortStatus_SWITCH_PORT_STATUS_UP}, + Membership: apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_INTERNAL, + }, + }, }, Nics: []*apiv2.SwitchNic{ { @@ -621,6 +653,7 @@ func Test_bootServiceServer_Register(t *testing.T) { State: &apiv2.NicState{ Actual: apiv2.SwitchPortStatus_SWITCH_PORT_STATUS_UP, }, + Membership: apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_INTERNAL, }, { Name: "Ethernet1", @@ -629,6 +662,7 @@ func Test_bootServiceServer_Register(t *testing.T) { State: &apiv2.NicState{ Actual: apiv2.SwitchPortStatus_SWITCH_PORT_STATUS_UP, }, + Membership: apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_INTERNAL, }, }, Os: &apiv2.SwitchOS{ @@ -642,8 +676,24 @@ func Test_bootServiceServer_Register(t *testing.T) { Rack: new("r01"), ReplaceMode: apiv2.SwitchReplaceMode_SWITCH_REPLACE_MODE_OPERATIONAL, MachineConnections: []*apiv2.MachineConnection{ - {MachineId: m1, Nic: &apiv2.SwitchNic{Name: "Ethernet1", Identifier: "Eth1/2", BgpFilter: &apiv2.BGPFilter{}, State: &apiv2.NicState{Actual: apiv2.SwitchPortStatus_SWITCH_PORT_STATUS_UP}}}, - {MachineId: m99, Nic: &apiv2.SwitchNic{Name: "Ethernet0", Identifier: "Eth1/1", BgpFilter: &apiv2.BGPFilter{}, State: &apiv2.NicState{Actual: apiv2.SwitchPortStatus_SWITCH_PORT_STATUS_UP}}}, + { + MachineId: m1, + Nic: &apiv2.SwitchNic{Name: "Ethernet1", + Identifier: "Eth1/2", + BgpFilter: &apiv2.BGPFilter{}, + State: &apiv2.NicState{Actual: apiv2.SwitchPortStatus_SWITCH_PORT_STATUS_UP}, + Membership: apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_INTERNAL, + }, + }, + { + MachineId: m99, + Nic: &apiv2.SwitchNic{Name: "Ethernet0", + Identifier: "Eth1/1", + BgpFilter: &apiv2.BGPFilter{}, + State: &apiv2.NicState{Actual: apiv2.SwitchPortStatus_SWITCH_PORT_STATUS_UP}, + Membership: apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_INTERNAL, + }, + }, }, Nics: []*apiv2.SwitchNic{ { @@ -653,6 +703,7 @@ func Test_bootServiceServer_Register(t *testing.T) { State: &apiv2.NicState{ Actual: apiv2.SwitchPortStatus_SWITCH_PORT_STATUS_UP, }, + Membership: apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_INTERNAL, }, { Name: "Ethernet1", @@ -661,6 +712,7 @@ func Test_bootServiceServer_Register(t *testing.T) { State: &apiv2.NicState{ Actual: apiv2.SwitchPortStatus_SWITCH_PORT_STATUS_UP, }, + Membership: apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_INTERNAL, }, }, Os: &apiv2.SwitchOS{ @@ -883,6 +935,7 @@ func Test_bootServiceServer_InstallationSucceeded(t *testing.T) { State: &apiv2.NicState{ Actual: apiv2.SwitchPortStatus_SWITCH_PORT_STATUS_UP, }, + Membership: apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_UNMANAGED, // in a real setup, this would be internal but the endpoint doesn't touch the memberships }, { Name: "Ethernet1", @@ -891,6 +944,7 @@ func Test_bootServiceServer_InstallationSucceeded(t *testing.T) { State: &apiv2.NicState{ Actual: apiv2.SwitchPortStatus_SWITCH_PORT_STATUS_UP, }, + Membership: apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_UNMANAGED, }, }, Os: &apiv2.SwitchOS{ @@ -911,6 +965,7 @@ func Test_bootServiceServer_InstallationSucceeded(t *testing.T) { State: &apiv2.NicState{ Actual: apiv2.SwitchPortStatus_SWITCH_PORT_STATUS_UP, }, + Membership: apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_UNMANAGED, }, { Name: "Ethernet1", @@ -919,6 +974,7 @@ func Test_bootServiceServer_InstallationSucceeded(t *testing.T) { State: &apiv2.NicState{ Actual: apiv2.SwitchPortStatus_SWITCH_PORT_STATUS_UP, }, + Membership: apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_UNMANAGED, }, }, Os: &apiv2.SwitchOS{ @@ -982,6 +1038,7 @@ func Test_bootServiceServer_InstallationSucceeded(t *testing.T) { State: &apiv2.NicState{ Actual: apiv2.SwitchPortStatus_SWITCH_PORT_STATUS_UP, }, + Membership: apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_UNMANAGED, }, { Name: "Ethernet1", @@ -990,6 +1047,7 @@ func Test_bootServiceServer_InstallationSucceeded(t *testing.T) { State: &apiv2.NicState{ Actual: apiv2.SwitchPortStatus_SWITCH_PORT_STATUS_UP, }, + Membership: apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_UNMANAGED, }, }, Os: &apiv2.SwitchOS{ @@ -1010,6 +1068,7 @@ func Test_bootServiceServer_InstallationSucceeded(t *testing.T) { State: &apiv2.NicState{ Actual: apiv2.SwitchPortStatus_SWITCH_PORT_STATUS_UP, }, + Membership: apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_UNMANAGED, }, { Name: "Ethernet1", @@ -1018,6 +1077,7 @@ func Test_bootServiceServer_InstallationSucceeded(t *testing.T) { State: &apiv2.NicState{ Actual: apiv2.SwitchPortStatus_SWITCH_PORT_STATUS_UP, }, + Membership: apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_UNMANAGED, }, }, Os: &apiv2.SwitchOS{ diff --git a/pkg/service/infra/switch/switch-service.go b/pkg/service/infra/switch/switch-service.go index 94cf1165..97897545 100644 --- a/pkg/service/infra/switch/switch-service.go +++ b/pkg/service/infra/switch/switch-service.go @@ -12,6 +12,7 @@ import ( infrav2 "github.com/metal-stack/api/go/metalstack/infra/v2" "github.com/metal-stack/api/go/metalstack/infra/v2/infrav2connect" "github.com/metal-stack/metal-apiserver/pkg/repository" + "github.com/samber/lo" "google.golang.org/protobuf/testing/protocmp" "google.golang.org/protobuf/types/known/timestamppb" ) @@ -111,6 +112,21 @@ func (s *switchServiceServer) Heartbeat(ctx context.Context, rq *infrav2.SwitchS } } + for i, nic := range sw.Nics { + if nic.Membership != apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_UNSPECIFIED { + continue + } + _, connected := lo.Find(sw.MachineConnections, func(con *apiv2.MachineConnection) bool { + return con.Nic.Name == nic.Name + }) + if connected { + sw.Nics[i].Membership = apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_INTERNAL + } else { + sw.Nics[i].Membership = apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_EXTERNAL + } + updated = true + } + if updated { updateReq := &adminv2.SwitchServiceUpdateRequest{ Id: sw.Id, diff --git a/pkg/service/infra/switch/switch-service_test.go b/pkg/service/infra/switch/switch-service_test.go index d0709d77..a381d136 100644 --- a/pkg/service/infra/switch/switch-service_test.go +++ b/pkg/service/infra/switch/switch-service_test.go @@ -43,6 +43,15 @@ func Test_switchServiceServer_Register(t *testing.T) { Version: "v5.9", MetalCoreVersion: "v0.13.0", }, + Nics: []*apiv2.SwitchNic{ + { + Name: "Ethernet0", + Identifier: "Ethernet0", + State: &apiv2.NicState{ + Actual: apiv2.SwitchPortStatus_SWITCH_PORT_STATUS_UP, + }, + }, + }, }, }, want: func(e *test.Entities) *infrav2.SwitchServiceRegisterResponse { @@ -60,6 +69,17 @@ func Test_switchServiceServer_Register(t *testing.T) { Version: "v5.9", MetalCoreVersion: "v0.13.0", }, + Nics: []*apiv2.SwitchNic{ + { + Name: "Ethernet0", + Identifier: "Ethernet0", + State: &apiv2.NicState{ + Actual: apiv2.SwitchPortStatus_SWITCH_PORT_STATUS_UP, + }, + BgpFilter: &apiv2.BGPFilter{}, + Membership: apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_UNMANAGED, + }, + }, }, } }, @@ -79,6 +99,17 @@ func Test_switchServiceServer_Register(t *testing.T) { Version: "v5.9", MetalCoreVersion: "v0.13.0", }, + Nics: []*apiv2.SwitchNic{ + { + Name: "Ethernet0", + Identifier: "Ethernet0", + State: &apiv2.NicState{ + Actual: apiv2.SwitchPortStatus_SWITCH_PORT_STATUS_UP, + }, + BgpFilter: &apiv2.BGPFilter{}, + Membership: apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_UNMANAGED, + }, + }, } }, SwitchStatuses: func(switchStatuses map[string]*metal.SwitchStatus) { @@ -136,6 +167,7 @@ func Test_switchServiceServer_Register(t *testing.T) { State: &apiv2.NicState{ Actual: apiv2.SwitchPortStatus_SWITCH_PORT_STATUS_UP, }, + Membership: apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_INTERNAL, }, { Name: "Ethernet2", @@ -144,6 +176,7 @@ func Test_switchServiceServer_Register(t *testing.T) { State: &apiv2.NicState{ Actual: apiv2.SwitchPortStatus_SWITCH_PORT_STATUS_UP, }, + Membership: apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_UNMANAGED, }, } sw.Os.MetalCoreVersion = "v0.13.0" @@ -169,6 +202,7 @@ func Test_switchServiceServer_Register(t *testing.T) { State: &apiv2.NicState{ Actual: apiv2.SwitchPortStatus_SWITCH_PORT_STATUS_UP, }, + Membership: apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_INTERNAL, }, { Name: "Ethernet2", @@ -177,6 +211,7 @@ func Test_switchServiceServer_Register(t *testing.T) { State: &apiv2.NicState{ Actual: apiv2.SwitchPortStatus_SWITCH_PORT_STATUS_UP, }, + Membership: apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_UNMANAGED, }, } sw.Os.MetalCoreVersion = "v0.13.0" @@ -308,6 +343,7 @@ func Test_switchServiceServer_Register(t *testing.T) { State: &apiv2.NicState{ Actual: apiv2.SwitchPortStatus_SWITCH_PORT_STATUS_UP, }, + Membership: apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_INTERNAL, }, }, }, @@ -320,6 +356,7 @@ func Test_switchServiceServer_Register(t *testing.T) { State: &apiv2.NicState{ Actual: apiv2.SwitchPortStatus_SWITCH_PORT_STATUS_UP, }, + Membership: apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_INTERNAL, }, { Name: "Ethernet1", @@ -329,6 +366,7 @@ func Test_switchServiceServer_Register(t *testing.T) { State: &apiv2.NicState{ Actual: apiv2.SwitchPortStatus_SWITCH_PORT_STATUS_UP, }, + Membership: apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_UNMANAGED, }, }, Os: &apiv2.SwitchOS{ @@ -353,6 +391,7 @@ func Test_switchServiceServer_Register(t *testing.T) { State: &apiv2.NicState{ Actual: apiv2.SwitchPortStatus_SWITCH_PORT_STATUS_UP, }, + Membership: apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_INTERNAL, } sw.MachineConnections = []*apiv2.MachineConnection{ { @@ -361,15 +400,7 @@ func Test_switchServiceServer_Register(t *testing.T) { }, } sw.Nics = []*apiv2.SwitchNic{ - { - Name: "Ethernet0", - Identifier: "Ethernet0", - Mac: new("11:11:11:11:11:11"), - BgpFilter: &apiv2.BGPFilter{}, - State: &apiv2.NicState{ - Actual: apiv2.SwitchPortStatus_SWITCH_PORT_STATUS_UP, - }, - }, + nic1, { Name: "Ethernet1", Identifier: "Ethernet1", @@ -378,6 +409,7 @@ func Test_switchServiceServer_Register(t *testing.T) { State: &apiv2.NicState{ Actual: apiv2.SwitchPortStatus_SWITCH_PORT_STATUS_UP, }, + Membership: apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_UNMANAGED, }, } sw.Os = &apiv2.SwitchOS{ @@ -838,6 +870,7 @@ func Test_switchRepository_ConnectMachineWithSwitches(t *testing.T) { tests := []struct { name string m func() *apiv2.Machine + spec func() *sc.DatacenterSpec mods func() *test.Asserters wantErr error }{ @@ -1023,7 +1056,7 @@ func Test_switchRepository_ConnectMachineWithSwitches(t *testing.T) { wantErr: errorutil.FailedPrecondition("machine %s is connected to port swp1s1 on switch %s but not to the corresponding port Ethernet1 of switch %s", sc.Machine2, sc.P01Rack02Switch1, sc.P01Rack02Switch2), }, { - name: "machine is connected to different switches than before", + name: "can't connect machine to different rack than before", m: func() *apiv2.Machine { return &apiv2.Machine{ Uuid: sc.Machine1, @@ -1058,6 +1091,69 @@ func Test_switchRepository_ConnectMachineWithSwitches(t *testing.T) { }, wantErr: errorutil.FailedPrecondition(`machine wants to register on rack %q, but machine connections are present on the following switches [%s %s], likely the machine was moved in the data center but not deleted through the admin api`, sc.P01Rack02, sc.P01Rack01Switch1, sc.P01Rack01Switch2), }, + { + name: "can't connect machine to different switches than before, even within the same rack", + m: func() *apiv2.Machine { + return &apiv2.Machine{ + Uuid: sc.Machine2, + Partition: &apiv2.Partition{ + Id: sc.Partition1, + }, + Hardware: &apiv2.MachineHardware{ + Nics: []*apiv2.MachineNic{ + { + Name: "lan0", + Neighbors: []*apiv2.MachineNic{ + { + Name: "Ethernet0", + Identifier: "Ethernet0", + Hostname: sc.P01Rack02Switch1_1, + }, + }, + }, + { + Name: "lan1", + Neighbors: []*apiv2.MachineNic{ + { + Name: "Ethernet0", + Identifier: "Ethernet0", + Hostname: sc.P01Rack02Switch2, + }, + }, + }, + }, + }, + } + }, + spec: func() *sc.DatacenterSpec { + spec, err := sc.SwitchesWithMachinesDatacenter.DeepCopy() + require.NoError(t, err) + spec.Machines[1].Machine.Hardware.Nics = metal.Nics{ + { + Name: "lan0", + Neighbors: metal.Nics{ + { + Name: "Ethernet0", + Identifier: "Ethernet0", + Hostname: sc.P01Rack02Switch1, + }, + }, + }, + { + Name: "lan1", + Neighbors: metal.Nics{ + { + Name: "Ethernet0", + Identifier: "Ethernet0", + Hostname: sc.P01Rack02Switch2, + }, + }, + }, + } + return spec + }, + wantErr: errorutil.FailedPrecondition("cannot connect machine %q to different switches than it was previously connected to; current: %v, previous: %v; if you want to migrate machine connections from one switch to another call 'switch mirgate' first", sc.Machine2, []string{sc.P01Rack02Switch1_1, sc.P01Rack02Switch2}, []string{sc.P01Rack02Switch1, sc.P01Rack02Switch2}), + }, { name: "machine connections don't change", m: func() *apiv2.Machine { @@ -1132,11 +1228,13 @@ func Test_switchRepository_ConnectMachineWithSwitches(t *testing.T) { return &test.Asserters{ Switches: func(switches map[string]*apiv2.Switch) { sw := switches[sc.P01Rack01Switch1] + sw.Nics[1].Membership = apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_INTERNAL sw.MachineConnections = append(sw.MachineConnections, &apiv2.MachineConnection{ MachineId: sc.Machine8, Nic: sw.Nics[1], }) sw = switches[sc.P01Rack01Switch2] + sw.Nics[1].Membership = apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_INTERNAL sw.MachineConnections = append(sw.MachineConnections, &apiv2.MachineConnection{ MachineId: sc.Machine8, Nic: sw.Nics[1], @@ -1146,10 +1244,74 @@ func Test_switchRepository_ConnectMachineWithSwitches(t *testing.T) { }, wantErr: nil, }, + { + name: "connect machine to different ports on the same switches", + m: func() *apiv2.Machine { + return &apiv2.Machine{ + Uuid: sc.Machine1, + Partition: &apiv2.Partition{ + Id: sc.Partition1, + }, + Hardware: &apiv2.MachineHardware{ + Nics: []*apiv2.MachineNic{ + { + Name: "lan0", + Neighbors: []*apiv2.MachineNic{ + { + Name: "Ethernet1", + Identifier: "Ethernet1", + Hostname: sc.P01Rack01Switch1, + }, + }, + }, + { + Name: "lan1", + Neighbors: []*apiv2.MachineNic{ + { + Name: "Ethernet1", + Identifier: "Ethernet1", + Hostname: sc.P01Rack01Switch2, + }, + }, + }, + }, + }, + } + }, + mods: func() *test.Asserters { + return &test.Asserters{ + Switches: func(switches map[string]*apiv2.Switch) { + sw := switches[sc.P01Rack01Switch1] + sw.Nics[0].Membership = apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_UNMANAGED + sw.Nics[1].Membership = apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_INTERNAL + sw.MachineConnections = []*apiv2.MachineConnection{ + { + MachineId: sc.Machine1, + Nic: sw.Nics[1], + }, + } + sw = switches[sc.P01Rack01Switch2] + sw.Nics[0].Membership = apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_UNMANAGED + sw.Nics[1].Membership = apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_INTERNAL + sw.MachineConnections = []*apiv2.MachineConnection{ + { + MachineId: sc.Machine1, + Nic: sw.Nics[1], + }, + } + }, + } + }, + wantErr: nil, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - dc.Create(&sc.SwitchesWithMachinesDatacenter) + spec := &sc.SwitchesWithMachinesDatacenter + if tt.spec != nil { + spec = tt.spec() + } + dc.Create(spec) defer dc.Cleanup() var m *apiv2.Machine diff --git a/pkg/test/datacenter.go b/pkg/test/datacenter.go index 44cc0d71..f821a682 100644 --- a/pkg/test/datacenter.go +++ b/pkg/test/datacenter.go @@ -2,7 +2,6 @@ package test import ( "context" - "encoding/json" "fmt" "net/http" "net/http/httptest" @@ -129,27 +128,27 @@ func (dc *Datacenter) Snapshot() *Entities { } func (dc *Datacenter) GetTenants() map[string]*apiv2.Tenant { - return dc.entities.Tenants + return dc.Snapshot().Tenants } func (dc *Datacenter) GetProjects() map[string][]*apiv2.Project { - return dc.entities.Projects + return dc.Snapshot().Projects } func (dc *Datacenter) GetPartitions() map[string]*apiv2.Partition { - return dc.entities.Partitions + return dc.Snapshot().Partitions } func (dc *Datacenter) GetSizes() map[string]*apiv2.Size { - return dc.entities.Sizes + return dc.Snapshot().Sizes } func (dc *Datacenter) GetNetworks() map[string]*apiv2.Network { - return dc.entities.Networks + return dc.Snapshot().Networks } func (dc *Datacenter) GetNetworkByName(name string) *apiv2.Network { - for _, n := range dc.entities.Networks { + for _, n := range dc.Snapshot().Networks { if n.Name != nil && *n.Name == name { return n } @@ -158,27 +157,27 @@ func (dc *Datacenter) GetNetworkByName(name string) *apiv2.Network { } func (dc *Datacenter) GetIPs() map[string]*apiv2.IP { - return dc.entities.Ips + return dc.Snapshot().Ips } func (dc *Datacenter) GetImages() map[string]*apiv2.Image { - return dc.entities.Images + return dc.Snapshot().Images } func (dc *Datacenter) GetSwitches() map[string]*apiv2.Switch { - return dc.entities.Switches + return dc.Snapshot().Switches } func (dc *Datacenter) GetSwitchStatuses() map[string]*metal.SwitchStatus { - return dc.entities.SwitchStatuses + return dc.Snapshot().SwitchStatuses } func (dc *Datacenter) GetMachines() map[string]*apiv2.Machine { - return dc.entities.Machines + return dc.Snapshot().Machines } func (dc *Datacenter) GetFilesystemLayouts() map[string]*apiv2.FilesystemLayout { - return dc.entities.FilesystemLayouts + return dc.Snapshot().FilesystemLayouts } func (dc *Datacenter) Close() { @@ -505,56 +504,43 @@ func (e *Entities) deepCopy() (*Entities, error) { err error ) - if copied.Tenants, err = deepCopy(e.Tenants); err != nil { + if copied.Tenants, err = scenarios.DeepCopy(e.Tenants); err != nil { return nil, err } - if copied.Projects, err = deepCopy(e.Projects); err != nil { + if copied.Projects, err = scenarios.DeepCopy(e.Projects); err != nil { return nil, err } - if copied.Partitions, err = deepCopy(e.Partitions); err != nil { + if copied.Partitions, err = scenarios.DeepCopy(e.Partitions); err != nil { return nil, err } - if copied.Sizes, err = deepCopy(e.Sizes); err != nil { + if copied.Sizes, err = scenarios.DeepCopy(e.Sizes); err != nil { return nil, err } - if copied.FilesystemLayouts, err = deepCopy(e.FilesystemLayouts); err != nil { + if copied.FilesystemLayouts, err = scenarios.DeepCopy(e.FilesystemLayouts); err != nil { return nil, err } - if copied.Networks, err = deepCopy(e.Networks); err != nil { + if copied.Networks, err = scenarios.DeepCopy(e.Networks); err != nil { return nil, err } - if copied.Ips, err = deepCopy(e.Ips); err != nil { + if copied.Ips, err = scenarios.DeepCopy(e.Ips); err != nil { return nil, err } - if copied.Images, err = deepCopy(e.Images); err != nil { + if copied.Images, err = scenarios.DeepCopy(e.Images); err != nil { return nil, err } - if copied.Switches, err = deepCopy(e.Switches); err != nil { + if copied.Switches, err = scenarios.DeepCopy(e.Switches); err != nil { return nil, err } - if copied.SwitchStatuses, err = deepCopy(e.SwitchStatuses); err != nil { + if copied.SwitchStatuses, err = scenarios.DeepCopy(e.SwitchStatuses); err != nil { return nil, err } - if copied.Machines, err = deepCopy(e.Machines); err != nil { + if copied.Machines, err = scenarios.DeepCopy(e.Machines); err != nil { return nil, err } return copied, nil } -func deepCopy[T any](in T) (T, error) { - var out T - bytes, err := json.Marshal(in) - if err != nil { - return out, err - } - err = json.Unmarshal(bytes, &out) - if err != nil { - return out, err - } - return out, nil -} - func getCurrentEntities(ctx context.Context, store *testStore) (*Entities, error) { e := &Entities{} diff --git a/pkg/test/datacenter_test.go b/pkg/test/datacenter_test.go index dec0144e..6c1de44e 100644 --- a/pkg/test/datacenter_test.go +++ b/pkg/test/datacenter_test.go @@ -8,7 +8,6 @@ import ( "time" "github.com/google/go-cmp/cmp" - "github.com/google/go-cmp/cmp/cmpopts" "github.com/metal-stack/api/go/errorutil" adminv2 "github.com/metal-stack/api/go/metalstack/admin/v2" apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" @@ -731,55 +730,3 @@ func Test_entities_deepCopy(t *testing.T) { }) } } - -func Test_deepCopy(t *testing.T) { - type testStruct struct { - unexported string - StringValue string - StructValue *testStruct - } - tests := []struct { - name string - in testStruct - want testStruct - }{ - { - name: "copy all exported fields by value", - in: testStruct{ - unexported: "unexported", - StringValue: "some value", - StructValue: &testStruct{ - StringValue: "some other value", - StructValue: &testStruct{}, - }, - }, - want: testStruct{ - unexported: "", - StringValue: "some value", - StructValue: &testStruct{ - StringValue: "some other value", - StructValue: &testStruct{}, - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := deepCopy(tt.in) - require.NoError(t, err) - if diff := cmp.Diff(tt.want, got, cmpopts.IgnoreUnexported(testStruct{})); diff != "" { - t.Errorf("deepCopy() diff = %s", diff) - } - in := tt.in - if got == in { - t.Errorf("deepCopy() copied struct by reference") - } - if got.unexported == in.unexported { - t.Errorf("deepCopy() copied unexported value") - } - if got.StructValue == in.StructValue { - t.Errorf("deepCopy() copied struct value by reference") - } - }) - } -} diff --git a/pkg/test/rethinkdb.go b/pkg/test/rethinkdb.go index 5114e834..09cb791a 100644 --- a/pkg/test/rethinkdb.go +++ b/pkg/test/rethinkdb.go @@ -70,10 +70,7 @@ func StartRethink(t testing.TB, log *slog.Logger) (generic.Datastore, r.ConnectO MaxOpen: 2000, } - err := generic.Initialize(t.Context(), log, rethinkDbConnectOpts, generic.AsnPoolRange(uint(1), uint(100)), generic.VrfPoolRange(uint(1), uint(100)), generic.NewMutexOptCheckInterval(3*time.Second)) - require.NoError(t, err) - - ds, err := generic.New(log, rethinkDbConnectOpts) + ds, err := generic.Initialize(t.Context(), log, rethinkDbConnectOpts, generic.AsnPoolRange(uint(1), uint(100)), generic.VrfPoolRange(uint(1), uint(100)), generic.NewMutexOptCheckInterval(3*time.Second)) require.NoError(t, err) return ds, rethinkDbConnectOpts, rethinkDbCloser diff --git a/pkg/test/scenarios/funcs.go b/pkg/test/scenarios/funcs.go index 97b6236a..63181f78 100644 --- a/pkg/test/scenarios/funcs.go +++ b/pkg/test/scenarios/funcs.go @@ -9,44 +9,79 @@ import ( "github.com/samber/lo" ) -var ( - SwitchFunc = func(id, partition, rack string, ports []string, os *apiv2.SwitchOS, replaceMode apiv2.SwitchReplaceMode, machines ...string) *apiv2.Switch { - var ( - nics []*apiv2.SwitchNic - cons []*apiv2.MachineConnection - ) +func SwitchFunc(id, partition, rack string, ports []string, os *apiv2.SwitchOS, replaceMode apiv2.SwitchReplaceMode, machines ...string) *apiv2.Switch { + var ( + nics []*apiv2.SwitchNic + cons []*apiv2.MachineConnection + ) - for i, p := range ports { - nic := &apiv2.SwitchNic{ - Name: p, - Identifier: p, - State: &apiv2.NicState{ - Actual: apiv2.SwitchPortStatus_SWITCH_PORT_STATUS_UP, - }, - } - nics = append(nics, nic) - if i < len(machines) { - cons = append(cons, &apiv2.MachineConnection{ - MachineId: machines[i], - Nic: nic, - }) - } + for i, p := range ports { + nic := &apiv2.SwitchNic{ + Name: p, + Identifier: p, + State: &apiv2.NicState{ + Actual: apiv2.SwitchPortStatus_SWITCH_PORT_STATUS_UP, + }, + Membership: apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_UNMANAGED, + } + if i < len(machines) { + nic.Membership = apiv2.SwitchPortMembership_SWITCH_PORT_MEMBERSHIP_INTERNAL + cons = append(cons, &apiv2.MachineConnection{ + MachineId: machines[i], + Nic: nic, + }) } + nics = append(nics, nic) + } + + return &apiv2.Switch{ + Id: id, + Rack: new(rack), + Partition: partition, + Nics: nics, + Os: os, + ReplaceMode: replaceMode, + MachineConnections: cons, + } +} - return &apiv2.Switch{ - Id: id, - Rack: new(rack), - Partition: partition, - Nics: nics, - Os: os, - ReplaceMode: replaceMode, - MachineConnections: cons, +func MachineFunc(id, partition, size, project, image string, liveliness metal.MachineLiveliness, waiting bool) *MachineWithLiveliness { + machineNumber := lo.Substring(id, -1, 1) + m := &metal.Machine{ + ID: id, + PartitionID: partition, + SizeID: size, + IPMI: metal.IPMI{ // required for healthy machine state + Address: fmt.Sprintf("1.2.3.%s:623", machineNumber), + MacAddress: "aa:bb:0" + machineNumber, + LastUpdated: time.Now().Add(-1 * time.Minute), + Fru: metal.Fru{ + ProductSerial: "PS" + machineNumber, + }, + }, + State: metal.MachineState{ + Value: metal.AvailableState, + }, + Waiting: waiting, + } + if project != "" && image != "" { + m.Allocation = &metal.MachineAllocation{ + Project: project, + ImageID: image, } } + return &MachineWithLiveliness{ + Liveliness: liveliness, + Machine: m, + } +} + +func AllocatedMachineFunc(id, partition, size, project, image string, liveliness metal.MachineLiveliness, networks []*metal.MachineNetwork) *MachineWithLiveliness { + machineNumber := lo.Substring(id, -1, 1) - MachineFunc = func(id, partition, size, project, image string, liveliness metal.MachineLiveliness, waiting bool) *MachineWithLiveliness { - machineNumber := lo.Substring(id, -1, 1) - m := &metal.Machine{ + return &MachineWithLiveliness{ + Liveliness: liveliness, + Machine: &metal.Machine{ ID: id, PartitionID: partition, SizeID: size, @@ -61,49 +96,28 @@ var ( State: metal.MachineState{ Value: metal.AvailableState, }, - Waiting: waiting, - } - if project != "" && image != "" { - m.Allocation = &metal.MachineAllocation{ - Project: project, - ImageID: image, - } - } - return &MachineWithLiveliness{ - Liveliness: liveliness, - Machine: m, - } + Waiting: false, + Allocation: &metal.MachineAllocation{ + UUID: id, + Project: project, + ImageID: image, + Role: metal.RoleMachine, + MachineNetworks: networks, + }, + }, } +} - AllocatedMachineFunc = func(id, partition, size, project, image string, liveliness metal.MachineLiveliness, networks []*metal.MachineNetwork) *MachineWithLiveliness { - machineNumber := lo.Substring(id, -1, 1) - - return &MachineWithLiveliness{ - Liveliness: liveliness, - Machine: &metal.Machine{ - ID: id, - PartitionID: partition, - SizeID: size, - IPMI: metal.IPMI{ // required for healthy machine state - Address: fmt.Sprintf("1.2.3.%s:623", machineNumber), - MacAddress: "aa:bb:0" + machineNumber, - LastUpdated: time.Now().Add(-1 * time.Minute), - Fru: metal.Fru{ - ProductSerial: "PS" + machineNumber, - }, - }, - State: metal.MachineState{ - Value: metal.AvailableState, - }, - Waiting: false, - Allocation: &metal.MachineAllocation{ - UUID: id, - Project: project, - ImageID: image, - Role: metal.RoleMachine, - MachineNetworks: networks, - }, - }, +func AddNicsToVRF(sw *apiv2.Switch, vrf string, membership apiv2.SwitchPortMembership, nics ...string) error { + for _, nic := range nics { + switchNic, found := lo.Find(sw.Nics, func(n *apiv2.SwitchNic) bool { + return n.Name == nic + }) + if !found { + return fmt.Errorf("nic %q not found", nic) } + switchNic.Vrf = new(vrf) + switchNic.Membership = membership } -) + return nil +} diff --git a/pkg/test/scenarios/spec.go b/pkg/test/scenarios/spec.go index 0a0add00..936994f1 100644 --- a/pkg/test/scenarios/spec.go +++ b/pkg/test/scenarios/spec.go @@ -1,6 +1,7 @@ package scenarios import ( + "encoding/json" "testing" adminv2 "github.com/metal-stack/api/go/metalstack/admin/v2" @@ -35,6 +36,10 @@ const ( NetworkTenantSuperNamespaced = "tenant-super-namespaced" NetworkTenantSuperPartition1 = "tenant-super-partition-1" NetworkNameTenantPartition1 = "tenant-partition-1" + NetworkExternal = "external" + NetworkSuper = "super" + NetworkSuperNamespaced = "super-namespaced" + NetworkChildShared = "child-shared" P01Rack01 = "p01-rack01" P01Rack02 = "p01-rack02" @@ -119,3 +124,69 @@ type ( ReservedMachines []string // TODO } ) + +func (s *DatacenterSpec) DeepCopy() (*DatacenterSpec, error) { + var ( + copied = &DatacenterSpec{} + err error + ) + + copied.ProjectsPerTenant = s.ProjectsPerTenant + copied.MachineFns = s.MachineFns + copied.IpFns = s.IpFns + + if copied.Partitions, err = DeepCopy(s.Partitions); err != nil { + return nil, err + } + if copied.Tenants, err = DeepCopy(s.Tenants); err != nil { + return nil, err + } + if copied.Images, err = DeepCopy(s.Images); err != nil { + return nil, err + } + if copied.FilesystemLayouts, err = DeepCopy(s.FilesystemLayouts); err != nil { + return nil, err + } + if copied.Sizes, err = DeepCopy(s.Sizes); err != nil { + return nil, err + } + if copied.SizeReservations, err = DeepCopy(s.SizeReservations); err != nil { + return nil, err + } + if copied.SizeImageConstraints, err = DeepCopy(s.SizeImageConstraints); err != nil { + return nil, err + } + if copied.Networks, err = DeepCopy(s.Networks); err != nil { + return nil, err + } + if copied.IPs, err = DeepCopy(s.IPs); err != nil { + return nil, err + } + if copied.Switches, err = DeepCopy(s.Switches); err != nil { + return nil, err + } + if copied.SwitchStatuses, err = DeepCopy(s.SwitchStatuses); err != nil { + return nil, err + } + if copied.Machines, err = DeepCopy(s.Machines); err != nil { + return nil, err + } + if copied.ReservedMachines, err = DeepCopy(s.ReservedMachines); err != nil { + return nil, err + } + + return copied, nil +} + +func DeepCopy[T any](in T) (T, error) { + var out T + bytes, err := json.Marshal(in) + if err != nil { + return out, err + } + err = json.Unmarshal(bytes, &out) + if err != nil { + return out, err + } + return out, nil +} diff --git a/pkg/test/scenarios/spec_test.go b/pkg/test/scenarios/spec_test.go new file mode 100644 index 00000000..32a572d4 --- /dev/null +++ b/pkg/test/scenarios/spec_test.go @@ -0,0 +1,61 @@ +package scenarios + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/stretchr/testify/require" +) + +func Test_DeepCopy(t *testing.T) { + type testStruct struct { + unexported string + StringValue string + StructValue *testStruct + } + tests := []struct { + name string + in testStruct + want testStruct + }{ + { + name: "copy all exported fields by value", + in: testStruct{ + unexported: "unexported", + StringValue: "some value", + StructValue: &testStruct{ + StringValue: "some other value", + StructValue: &testStruct{}, + }, + }, + want: testStruct{ + unexported: "", + StringValue: "some value", + StructValue: &testStruct{ + StringValue: "some other value", + StructValue: &testStruct{}, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := DeepCopy(tt.in) + require.NoError(t, err) + if diff := cmp.Diff(tt.want, got, cmpopts.IgnoreUnexported(testStruct{})); diff != "" { + t.Errorf("deepCopy() diff = %s", diff) + } + in := tt.in + if got == in { + t.Errorf("deepCopy() copied struct by reference") + } + if got.unexported == in.unexported { + t.Errorf("deepCopy() copied unexported value") + } + if got.StructValue == in.StructValue { + t.Errorf("deepCopy() copied struct value by reference") + } + }) + } +} diff --git a/pkg/test/scenarios/switches-with-external-network-members.go b/pkg/test/scenarios/switches-with-external-network-members.go new file mode 100644 index 00000000..22a55651 --- /dev/null +++ b/pkg/test/scenarios/switches-with-external-network-members.go @@ -0,0 +1,57 @@ +package scenarios + +import ( + adminv2 "github.com/metal-stack/api/go/metalstack/admin/v2" + apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" + "github.com/metal-stack/metal-apiserver/pkg/db/metal" +) + +var ( + SwitchesWithExternalNetworkMembers = DatacenterSpec{ + Partitions: []string{Partition1, Partition2}, + Switches: []*apiv2.Switch{ + SwitchFunc(P01Rack01Switch1, Partition1, P01Rack01, []string{"Ethernet0", "Ethernet1"}, SwitchOSSonic2021, apiv2.SwitchReplaceMode_SWITCH_REPLACE_MODE_OPERATIONAL), + SwitchFunc(P01Rack01Switch2, Partition1, P01Rack01, []string{"Ethernet0", "Ethernet1", "Ethernet120"}, SwitchOSSonic2021, apiv2.SwitchReplaceMode_SWITCH_REPLACE_MODE_OPERATIONAL), + SwitchFunc(P01Rack02Switch1, Partition1, P01Rack02, []string{"Ethernet0", "Ethernet1"}, SwitchOSSonic2021, apiv2.SwitchReplaceMode_SWITCH_REPLACE_MODE_OPERATIONAL), + SwitchFunc(P01Rack02Switch2, Partition1, P01Rack02, []string{"Ethernet0", "Ethernet1"}, SwitchOSSonic2021, apiv2.SwitchReplaceMode_SWITCH_REPLACE_MODE_OPERATIONAL), + SwitchFunc(P02Rack01Switch1, Partition2, P02Rack01, []string{"Ethernet0", "Ethernet1"}, SwitchOSSonic2021, apiv2.SwitchReplaceMode_SWITCH_REPLACE_MODE_OPERATIONAL), + SwitchFunc(P02Rack01Switch2, Partition2, P02Rack01, []string{"Ethernet0", "Ethernet1"}, SwitchOSSonic2021, apiv2.SwitchReplaceMode_SWITCH_REPLACE_MODE_OPERATIONAL), + }, + Networks: []*adminv2.NetworkServiceCreateRequest{ + { + Id: new(NetworkExternal), + Vrf: new(uint32(100)), + Type: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, + Prefixes: []string{"10.1.0.0/16"}, + }, + { + Id: new(NetworkNameTenantPartition1), + Vrf: new(uint32(99)), + Type: apiv2.NetworkType_NETWORK_TYPE_EXTERNAL, + Prefixes: []string{"10.2.0.0/16"}, + Partition: new(Partition1), + }, + { + Id: new(NetworkUnderlayPartition1), + Type: apiv2.NetworkType_NETWORK_TYPE_UNDERLAY, + Prefixes: []string{"10.3.0.0/16"}, + Partition: new(Partition1), + }, + { + Id: new(NetworkSuper), + Type: apiv2.NetworkType_NETWORK_TYPE_SUPER, + DefaultChildPrefixLength: &apiv2.ChildPrefixLength{Ipv4: new(uint32(22))}, + Prefixes: []string{"10.4.0.0/16"}, + }, + { + Id: new(NetworkSuperNamespaced), + Type: apiv2.NetworkType_NETWORK_TYPE_SUPER_NAMESPACED, + DefaultChildPrefixLength: &apiv2.ChildPrefixLength{Ipv4: new(uint32(22))}, + Prefixes: []string{"10.5.0.0/16"}, + }, + }, + Machines: []*MachineWithLiveliness{ + MachineFunc(Machine1, Partition1, SizeC1Large, "", "", metal.MachineLivelinessAlive, false), + }, + } +)