diff --git a/cmd/metal-api/internal/datastore/migrations/06_childprefixlength.go b/cmd/metal-api/internal/datastore/migrations/06_childprefixlength.go index 7d08220f..b45ab386 100644 --- a/cmd/metal-api/internal/datastore/migrations/06_childprefixlength.go +++ b/cmd/metal-api/internal/datastore/migrations/06_childprefixlength.go @@ -4,6 +4,7 @@ import ( r "gopkg.in/rethinkdb/rethinkdb-go.v6" "github.com/metal-stack/metal-api/cmd/metal-api/internal/datastore" + "github.com/metal-stack/metal-api/cmd/metal-api/internal/metal" ) func init() { @@ -20,10 +21,6 @@ func init() { } for _, old := range nws { - if !old.PrivateSuper { - continue - } - cursor, err := db.Table("partition").Get(old.PartitionID).Run(session) if err != nil { return err @@ -36,7 +33,17 @@ func init() { // TODO: does not work somehow new := old - new.ChildPrefixLength = &partition.PrivateNetworkPrefixLength + + af, err := metal.GetAddressFamily(new.Prefixes) + if err != nil { + return err + } + if af != nil { + new.AddressFamily = *af + } + if new.PrivateSuper { + new.DefaultChildPrefixLength = &partition.PrivateNetworkPrefixLength + } err = rs.UpdateNetwork(&old, &new) if err != nil { return err diff --git a/cmd/metal-api/internal/datastore/migrations_integration/migrate_integration_test.go b/cmd/metal-api/internal/datastore/migrations_integration/migrate_integration_test.go index 48820f29..53d54cd7 100644 --- a/cmd/metal-api/internal/datastore/migrations_integration/migrate_integration_test.go +++ b/cmd/metal-api/internal/datastore/migrations_integration/migrate_integration_test.go @@ -167,13 +167,19 @@ func Test_MigrationChildPrefixLength(t *testing.T) { Base: metal.Base{ ID: "n1", }, - PartitionID: "p1", + PartitionID: "p1", + Prefixes: metal.Prefixes{ + {IP: "10.0.0.0", Length: "8"}, + }, PrivateSuper: true, } n2 = &metal.Network{ Base: metal.Base{ ID: "n2", }, + Prefixes: metal.Prefixes{ + {IP: "2001::", Length: "64"}, + }, PartitionID: "p2", PrivateSuper: true, } @@ -181,6 +187,9 @@ func Test_MigrationChildPrefixLength(t *testing.T) { Base: metal.Base{ ID: "n3", }, + Prefixes: metal.Prefixes{ + {IP: "100.1.0.0", Length: "22"}, + }, PartitionID: "p2", PrivateSuper: false, } @@ -210,15 +219,18 @@ func Test_MigrationChildPrefixLength(t *testing.T) { n1fetched, err := rs.FindNetworkByID(n1.ID) require.NoError(t, err) require.NotNil(t, n1fetched) - require.Equal(t, p1.PrivateNetworkPrefixLength, *n1fetched.ChildPrefixLength, fmt.Sprintf("childprefixlength:%d", *n1fetched.ChildPrefixLength)) + require.Equal(t, p1.PrivateNetworkPrefixLength, *n1fetched.DefaultChildPrefixLength, fmt.Sprintf("childprefixlength:%d", *n1fetched.DefaultChildPrefixLength)) + require.Equal(t, metal.IPv4AddressFamily, n1fetched.AddressFamily) n2fetched, err := rs.FindNetworkByID(n2.ID) require.NoError(t, err) require.NotNil(t, n2fetched) - require.Equal(t, p2.PrivateNetworkPrefixLength, *n2fetched.ChildPrefixLength, fmt.Sprintf("childprefixlength:%d", *n2fetched.ChildPrefixLength)) + require.Equal(t, p2.PrivateNetworkPrefixLength, *n2fetched.DefaultChildPrefixLength, fmt.Sprintf("childprefixlength:%d", *n2fetched.DefaultChildPrefixLength)) + require.Equal(t, metal.IPv6AddressFamily, n2fetched.AddressFamily) n3fetched, err := rs.FindNetworkByID(n3.ID) require.NoError(t, err) require.NotNil(t, n3fetched) - require.Nil(t, n3fetched.ChildPrefixLength) + require.Nil(t, n3fetched.DefaultChildPrefixLength) + require.Equal(t, metal.IPv4AddressFamily, n3fetched.AddressFamily) } diff --git a/cmd/metal-api/internal/datastore/network.go b/cmd/metal-api/internal/datastore/network.go index 1e9990e7..4e949adb 100644 --- a/cmd/metal-api/internal/datastore/network.go +++ b/cmd/metal-api/internal/datastore/network.go @@ -12,18 +12,19 @@ import ( // NetworkSearchQuery can be used to search networks. type NetworkSearchQuery struct { - ID *string `json:"id" optional:"true"` - Name *string `json:"name" optional:"true"` - PartitionID *string `json:"partitionid" optional:"true"` - ProjectID *string `json:"projectid" optional:"true"` - Prefixes []string `json:"prefixes" optional:"true"` - DestinationPrefixes []string `json:"destinationprefixes" optional:"true"` - Nat *bool `json:"nat" optional:"true"` - PrivateSuper *bool `json:"privatesuper" optional:"true"` - Underlay *bool `json:"underlay" optional:"true"` - Vrf *int64 `json:"vrf" optional:"true"` - ParentNetworkID *string `json:"parentnetworkid" optional:"true"` - Labels map[string]string `json:"labels" optional:"true"` + ID *string `json:"id" optional:"true"` + Name *string `json:"name" optional:"true"` + PartitionID *string `json:"partitionid" optional:"true"` + ProjectID *string `json:"projectid" optional:"true"` + Prefixes []string `json:"prefixes" optional:"true"` + DestinationPrefixes []string `json:"destinationprefixes" optional:"true"` + Nat *bool `json:"nat" optional:"true"` + PrivateSuper *bool `json:"privatesuper" optional:"true"` + Underlay *bool `json:"underlay" optional:"true"` + Vrf *int64 `json:"vrf" optional:"true"` + ParentNetworkID *string `json:"parentnetworkid" optional:"true"` + Labels map[string]string `json:"labels" optional:"true"` + AddressFamily *metal.AddressFamily `json:"addressfamily" optional:"true"` } func (p *NetworkSearchQuery) Validate() error { @@ -104,6 +105,12 @@ func (p *NetworkSearchQuery) generateTerm(rs *RethinkStore) (*r.Term, error) { }) } + if p.AddressFamily != nil { + q = q.Filter(func(row r.Term) r.Term { + return row.Field("addressfamily").Eq(string(*p.AddressFamily)) + }) + } + for k, v := range p.Labels { k := k v := v diff --git a/cmd/metal-api/internal/datastore/rethinkdb.go b/cmd/metal-api/internal/datastore/rethinkdb.go index 6564278a..8167ede1 100644 --- a/cmd/metal-api/internal/datastore/rethinkdb.go +++ b/cmd/metal-api/internal/datastore/rethinkdb.go @@ -378,7 +378,7 @@ func (rs *RethinkStore) findEntity(query *r.Term, entity interface{}) error { } defer res.Close() if res.IsNil() { - return metal.NotFound("no %v with found", getEntityName(entity)) + return metal.NotFound("no %v found", getEntityName(entity)) } hasResult := res.Next(entity) diff --git a/cmd/metal-api/internal/datastore/size.go b/cmd/metal-api/internal/datastore/size.go index b3bbfb42..45f99766 100644 --- a/cmd/metal-api/internal/datastore/size.go +++ b/cmd/metal-api/internal/datastore/size.go @@ -4,8 +4,65 @@ import ( "errors" "github.com/metal-stack/metal-api/cmd/metal-api/internal/metal" + r "gopkg.in/rethinkdb/rethinkdb-go.v6" ) +// SizeSearchQuery can be used to search sizes. +type SizeSearchQuery struct { + ID *string `json:"id" optional:"true"` + Name *string `json:"name" optional:"true"` + Labels map[string]string `json:"labels" optional:"true"` + Reservation Reservation `json:"reservation" optional:"true"` +} + +type Reservation struct { + Partition *string `json:"partition" optional:"true"` + Project *string `json:"project" optional:"true"` +} + +// GenerateTerm generates the project search query term. +func (s *SizeSearchQuery) generateTerm(rs *RethinkStore) *r.Term { + q := *rs.sizeTable() + + if s.ID != nil { + q = q.Filter(func(row r.Term) r.Term { + return row.Field("id").Eq(*s.ID) + }) + } + + if s.Name != nil { + q = q.Filter(func(row r.Term) r.Term { + return row.Field("name").Eq(*s.Name) + }) + } + + for k, v := range s.Labels { + k := k + v := v + q = q.Filter(func(row r.Term) r.Term { + return row.Field("labels").Field(k).Eq(v) + }) + } + + if s.Reservation.Project != nil { + q = q.Filter(func(row r.Term) r.Term { + return row.Field("reservations").Contains(func(p r.Term) r.Term { + return p.Field("projectid").Eq(r.Expr(*s.Reservation.Project)) + }) + }) + } + + if s.Reservation.Partition != nil { + q = q.Filter(func(row r.Term) r.Term { + return row.Field("reservations").Contains(func(p r.Term) r.Term { + return p.Field("partitionids").Contains(r.Expr(*s.Reservation.Partition)) + }) + }) + } + + return &q +} + // FindSize return a size for a given id. func (rs *RethinkStore) FindSize(id string) (*metal.Size, error) { var s metal.Size @@ -16,6 +73,11 @@ func (rs *RethinkStore) FindSize(id string) (*metal.Size, error) { return &s, nil } +// SearchSizes returns the result of the sizes search request query. +func (rs *RethinkStore) SearchSizes(q *SizeSearchQuery, sizes *metal.Sizes) error { + return rs.searchEntities(q.generateTerm(rs), sizes) +} + // ListSizes returns all sizes. func (rs *RethinkStore) ListSizes() (metal.Sizes, error) { szs := make(metal.Sizes, 0) diff --git a/cmd/metal-api/internal/datastore/size_integration_test.go b/cmd/metal-api/internal/datastore/size_integration_test.go new file mode 100644 index 00000000..c774a92f --- /dev/null +++ b/cmd/metal-api/internal/datastore/size_integration_test.go @@ -0,0 +1,315 @@ +//go:build integration +// +build integration + +package datastore + +import ( + "testing" + + "github.com/metal-stack/metal-api/cmd/metal-api/internal/metal" + "github.com/metal-stack/metal-lib/pkg/pointer" + "github.com/stretchr/testify/require" +) + +type sizeTestable struct{} + +func (_ *sizeTestable) wipe() error { + _, err := sharedDS.sizeTable().Delete().RunWrite(sharedDS.session) + return err +} + +func (_ *sizeTestable) create(s *metal.Size) error { // nolint:unused + return sharedDS.CreateSize(s) +} + +func (_ *sizeTestable) delete(id string) error { // nolint:unused + return sharedDS.DeleteSize(&metal.Size{Base: metal.Base{ID: id}}) +} + +func (_ *sizeTestable) update(old *metal.Size, mutateFn func(s *metal.Size)) error { // nolint:unused + mod := *old + if mutateFn != nil { + mutateFn(&mod) + } + + return sharedDS.UpdateSize(old, &mod) +} + +func (_ *sizeTestable) find(id string) (*metal.Size, error) { // nolint:unused + return sharedDS.FindSize(id) +} + +func (_ *sizeTestable) list() ([]*metal.Size, error) { // nolint:unused + res, err := sharedDS.ListSizes() + if err != nil { + return nil, err + } + + return derefSlice(res), nil +} + +func (_ *sizeTestable) search(q *SizeSearchQuery) ([]*metal.Size, error) { // nolint:unused + var res metal.Sizes + err := sharedDS.SearchSizes(q, &res) + if err != nil { + return nil, err + } + + return derefSlice(res), nil +} + +func (_ *sizeTestable) defaultBody(s *metal.Size) *metal.Size { + if s.Constraints == nil { + s.Constraints = []metal.Constraint{} + } + if s.Reservations == nil { + s.Reservations = metal.Reservations{} + } + for i := range s.Reservations { + if s.Reservations[i].PartitionIDs == nil { + s.Reservations[i].PartitionIDs = []string{} + } + } + return s +} + +func TestRethinkStore_FindSize(t *testing.T) { + tt := &sizeTestable{} + defer func() { + require.NoError(t, tt.wipe()) + }() + + tests := []findTest[*metal.Size, *SizeSearchQuery]{ + { + name: "find", + id: "2", + + mock: []*metal.Size{ + {Base: metal.Base{ID: "1"}}, + {Base: metal.Base{ID: "2"}}, + {Base: metal.Base{ID: "3"}}, + }, + want: tt.defaultBody(&metal.Size{Base: metal.Base{ID: "2"}}), + wantErr: nil, + }, + { + name: "not found", + id: "4", + want: nil, + wantErr: metal.NotFound(`no size with id "4" found`), + }, + } + for i := range tests { + tests[i].run(t, tt) + } +} + +func TestRethinkStore_SearchSizes(t *testing.T) { + tt := &sizeTestable{} + defer func() { + require.NoError(t, tt.wipe()) + }() + + tests := []searchTest[*metal.Size, *SizeSearchQuery]{ + { + name: "empty result", + q: &SizeSearchQuery{ + ID: pointer.Pointer("2"), + }, + mock: []*metal.Size{ + {Base: metal.Base{ID: "1"}}, + }, + want: nil, + wantErr: nil, + }, + { + name: "search by id", + q: &SizeSearchQuery{ + ID: pointer.Pointer("2"), + }, + mock: []*metal.Size{ + {Base: metal.Base{ID: "1"}}, + {Base: metal.Base{ID: "2"}}, + {Base: metal.Base{ID: "3"}}, + }, + want: []*metal.Size{ + tt.defaultBody(&metal.Size{Base: metal.Base{ID: "2"}}), + }, + wantErr: nil, + }, + { + name: "search by name", + q: &SizeSearchQuery{ + Name: pointer.Pointer("b"), + }, + mock: []*metal.Size{ + {Base: metal.Base{ID: "1", Name: "a"}}, + {Base: metal.Base{ID: "2", Name: "b"}}, + {Base: metal.Base{ID: "3", Name: "c"}}, + }, + want: []*metal.Size{ + tt.defaultBody(&metal.Size{Base: metal.Base{ID: "2", Name: "b"}}), + }, + wantErr: nil, + }, + { + name: "search reservation project", + q: &SizeSearchQuery{ + Reservation: Reservation{ + Project: pointer.Pointer("2"), + }, + }, + mock: []*metal.Size{ + {Base: metal.Base{ID: "1"}, Reservations: metal.Reservations{{ProjectID: "1"}}}, + {Base: metal.Base{ID: "2"}, Reservations: metal.Reservations{{ProjectID: "2"}}}, + {Base: metal.Base{ID: "3"}, Reservations: metal.Reservations{{ProjectID: "3"}}}, + }, + want: []*metal.Size{ + tt.defaultBody(&metal.Size{Base: metal.Base{ID: "2"}, Reservations: metal.Reservations{{ProjectID: "2"}}}), + }, + wantErr: nil, + }, + { + name: "search reservation partition", + q: &SizeSearchQuery{ + Reservation: Reservation{ + Partition: pointer.Pointer("p1"), + }, + }, + mock: []*metal.Size{ + {Base: metal.Base{ID: "1"}, Reservations: metal.Reservations{{PartitionIDs: []string{"p1"}}}}, + {Base: metal.Base{ID: "2"}, Reservations: metal.Reservations{{PartitionIDs: []string{"p1", "p2"}}}}, + {Base: metal.Base{ID: "3"}, Reservations: metal.Reservations{{PartitionIDs: []string{"p3"}}}}, + }, + want: []*metal.Size{ + tt.defaultBody(&metal.Size{Base: metal.Base{ID: "1"}, Reservations: metal.Reservations{{PartitionIDs: []string{"p1"}}}}), + tt.defaultBody(&metal.Size{Base: metal.Base{ID: "2"}, Reservations: metal.Reservations{{PartitionIDs: []string{"p1", "p2"}}}}), + }, + wantErr: nil, + }, + } + + for i := range tests { + tests[i].run(t, tt) + } +} + +func TestRethinkStore_ListSizes(t *testing.T) { + tt := &sizeTestable{} + defer func() { + require.NoError(t, tt.wipe()) + }() + + tests := []listTest[*metal.Size, *SizeSearchQuery]{ + { + name: "list", + mock: []*metal.Size{ + {Base: metal.Base{ID: "1"}}, + {Base: metal.Base{ID: "2"}}, + {Base: metal.Base{ID: "3"}}, + }, + want: []*metal.Size{ + tt.defaultBody(&metal.Size{Base: metal.Base{ID: "1"}}), + tt.defaultBody(&metal.Size{Base: metal.Base{ID: "2"}}), + tt.defaultBody(&metal.Size{Base: metal.Base{ID: "3"}}), + }, + }, + } + for i := range tests { + tests[i].run(t, tt) + } +} + +func TestRethinkStore_CreateSize(t *testing.T) { + tt := &sizeTestable{} + defer func() { + require.NoError(t, tt.wipe()) + }() + + tests := []createTest[*metal.Size, *SizeSearchQuery]{ + { + name: "create", + want: tt.defaultBody(&metal.Size{Base: metal.Base{ID: "1"}}), + wantErr: nil, + }, + { + name: "already exists", + mock: []*metal.Size{ + {Base: metal.Base{ID: "1"}}, + }, + want: tt.defaultBody(&metal.Size{Base: metal.Base{ID: "1"}}), + wantErr: metal.Conflict(`cannot create size in database, entity already exists: 1`), + }, + } + for i := range tests { + tests[i].run(t, tt) + } +} + +func TestRethinkStore_DeleteSize(t *testing.T) { + tt := &sizeTestable{} + defer func() { + require.NoError(t, tt.wipe()) + }() + + tests := []deleteTest[*metal.Size, *SizeSearchQuery]{ + { + name: "delete", + id: "2", + mock: []*metal.Size{ + {Base: metal.Base{ID: "1"}}, + {Base: metal.Base{ID: "2"}}, + {Base: metal.Base{ID: "3"}}, + }, + want: []*metal.Size{ + tt.defaultBody(&metal.Size{Base: metal.Base{ID: "1"}}), + tt.defaultBody(&metal.Size{Base: metal.Base{ID: "3"}}), + }, + }, + { + name: "not exists results in noop", + id: "abc", + mock: []*metal.Size{ + {Base: metal.Base{ID: "1"}}, + {Base: metal.Base{ID: "2"}}, + {Base: metal.Base{ID: "3"}}, + }, + want: []*metal.Size{ + tt.defaultBody(&metal.Size{Base: metal.Base{ID: "1"}}), + tt.defaultBody(&metal.Size{Base: metal.Base{ID: "2"}}), + tt.defaultBody(&metal.Size{Base: metal.Base{ID: "3"}}), + }, + }, + } + for i := range tests { + tests[i].run(t, tt) + } +} + +func TestRethinkStore_UpdateSize(t *testing.T) { + tt := &sizeTestable{} + defer func() { + require.NoError(t, tt.wipe()) + }() + + tests := []updateTest[*metal.Size, *SizeSearchQuery]{ + { + name: "update", + mock: []*metal.Size{ + {Base: metal.Base{ID: "1"}}, + {Base: metal.Base{ID: "2"}}, + {Base: metal.Base{ID: "3"}}, + }, + mutateFn: func(s *metal.Size) { + s.Labels = map[string]string{"a": "b"} + }, + want: tt.defaultBody(&metal.Size{ + Base: metal.Base{ID: "1"}, + Labels: map[string]string{"a": "b"}, + }), + }, + } + for i := range tests { + tests[i].run(t, tt) + } +} diff --git a/cmd/metal-api/internal/datastore/size_test.go b/cmd/metal-api/internal/datastore/size_test.go index 5b7cfd9e..2b9a6438 100644 --- a/cmd/metal-api/internal/datastore/size_test.go +++ b/cmd/metal-api/internal/datastore/size_test.go @@ -4,176 +4,10 @@ import ( "reflect" "testing" - "github.com/google/go-cmp/cmp" "github.com/metal-stack/metal-api/cmd/metal-api/internal/metal" "github.com/metal-stack/metal-api/cmd/metal-api/internal/testdata" ) -func TestRethinkStore_FindSize(t *testing.T) { - ds, mock := InitMockDB(t) - testdata.InitMockDBData(mock) - - tests := []struct { - name string - rs *RethinkStore - id string - want *metal.Size - wantErr bool - }{ - { - name: "TestRethinkStore_FindSize Test 1", - rs: ds, - id: "1", - want: &testdata.Sz1, - wantErr: false, - }, - } - - for i := range tests { - tt := tests[i] - t.Run(tt.name, func(t *testing.T) { - got, err := tt.rs.FindSize(tt.id) - if (err != nil) != tt.wantErr { - t.Errorf("RethinkStore.FindSize() error = %v, wantErr %v", err, tt.wantErr) - return - } - if diff := cmp.Diff(got, tt.want); diff != "" { - t.Errorf("RethinkStore.FindSize() mismatch (-want +got):\n%s", diff) - } - }) - } -} - -func TestRethinkStore_ListSizes(t *testing.T) { - ds, mock := InitMockDB(t) - testdata.InitMockDBData(mock) - - tests := []struct { - name string - rs *RethinkStore - want metal.Sizes - wantErr bool - }{ - { - name: "TestRethinkStore_ListSizes Test 1", - rs: ds, - want: testdata.TestSizes, - wantErr: false, - }, - } - for i := range tests { - tt := tests[i] - t.Run(tt.name, func(t *testing.T) { - got, err := tt.rs.ListSizes() - if (err != nil) != tt.wantErr { - t.Errorf("RethinkStore.ListSizes() error = %v, wantErr %v", err, tt.wantErr) - return - } - if diff := cmp.Diff(got, tt.want); diff != "" { - t.Errorf("RethinkStore.ListSizes() mismatch (-want +got):\n%s", diff) - } - }) - } -} - -func TestRethinkStore_CreateSize(t *testing.T) { - ds, mock := InitMockDB(t) - testdata.InitMockDBData(mock) - - tests := []struct { - name string - rs *RethinkStore - size *metal.Size - wantErr bool - }{ - { - name: "TestRethinkStore_CreateSize Test 1", - rs: ds, - size: &testdata.Sz1, - wantErr: false, - }, - } - for i := range tests { - tt := tests[i] - t.Run(tt.name, func(t *testing.T) { - if err := tt.rs.CreateSize(tt.size); (err != nil) != tt.wantErr { - t.Errorf("RethinkStore.CreateSize() error = %v, wantErr %v", err, tt.wantErr) - } - }) - } -} - -func TestRethinkStore_DeleteSize(t *testing.T) { - ds, mock := InitMockDB(t) - testdata.InitMockDBData(mock) - - tests := []struct { - name string - rs *RethinkStore - size *metal.Size - wantErr bool - }{ - { - name: "TestRethinkStore_DeleteSize Test 1", - rs: ds, - size: &testdata.Sz1, - wantErr: false, - }, - { - name: "TestRethinkStore_DeleteSize Test 2", - rs: ds, - size: &testdata.Sz2, - wantErr: false, - }, - } - for i := range tests { - tt := tests[i] - t.Run(tt.name, func(t *testing.T) { - err := tt.rs.DeleteSize(tt.size) - if (err != nil) != tt.wantErr { - t.Errorf("RethinkStore.DeleteSize() error = %v, wantErr %v", err, tt.wantErr) - return - } - }) - } -} - -func TestRethinkStore_UpdateSize(t *testing.T) { - ds, mock := InitMockDB(t) - testdata.InitMockDBData(mock) - - tests := []struct { - name string - rs *RethinkStore - oldSize *metal.Size - newSize *metal.Size - wantErr bool - }{ - { - name: "TestRethinkStore_UpdateSize Test 1", - rs: ds, - oldSize: &testdata.Sz1, - newSize: &testdata.Sz2, - wantErr: false, - }, - { - name: "TestRethinkStore_UpdateSize Test 2", - rs: ds, - oldSize: &testdata.Sz2, - newSize: &testdata.Sz1, - wantErr: false, - }, - } - for i := range tests { - tt := tests[i] - t.Run(tt.name, func(t *testing.T) { - if err := tt.rs.UpdateSize(tt.oldSize, tt.newSize); (err != nil) != tt.wantErr { - t.Errorf("RethinkStore.UpdateSize() error = %v, wantErr %v", err, tt.wantErr) - } - }) - } -} - func TestRethinkStore_FromHardware(t *testing.T) { ds, mock := InitMockDB(t) testdata.InitMockDBData(mock) diff --git a/cmd/metal-api/internal/metal/network.go b/cmd/metal-api/internal/metal/network.go index be55ee97..8f54b00d 100644 --- a/cmd/metal-api/internal/metal/network.go +++ b/cmd/metal-api/internal/metal/network.go @@ -1,10 +1,12 @@ package metal import ( + "fmt" "net" "net/netip" "strconv" + "github.com/metal-stack/metal-lib/pkg/pointer" "github.com/samber/lo" ) @@ -207,18 +209,40 @@ func (p *Prefix) equals(other *Prefix) bool { // TODO specify rethinkdb restrictions. type Network struct { Base - Prefixes Prefixes `rethinkdb:"prefixes" json:"prefixes"` - DestinationPrefixes Prefixes `rethinkdb:"destinationprefixes" json:"destinationprefixes"` - ChildPrefixLength *uint8 `rethinkdb:"childprefixlength" json:"childprefixlength" description:"if privatesuper, this defines the bitlen of child prefixes if not nil"` - PartitionID string `rethinkdb:"partitionid" json:"partitionid"` - ProjectID string `rethinkdb:"projectid" json:"projectid"` - ParentNetworkID string `rethinkdb:"parentnetworkid" json:"parentnetworkid"` - Vrf uint `rethinkdb:"vrf" json:"vrf"` - PrivateSuper bool `rethinkdb:"privatesuper" json:"privatesuper"` - Nat bool `rethinkdb:"nat" json:"nat"` - Underlay bool `rethinkdb:"underlay" json:"underlay"` - Shared bool `rethinkdb:"shared" json:"shared"` - Labels map[string]string `rethinkdb:"labels" json:"labels"` + Prefixes Prefixes `rethinkdb:"prefixes" json:"prefixes"` + DestinationPrefixes Prefixes `rethinkdb:"destinationprefixes" json:"destinationprefixes"` + DefaultChildPrefixLength *uint8 `rethinkdb:"defaultchildprefixlength" json:"childprefixlength" description:"if privatesuper, this defines the bitlen of child prefixes if not nil"` + PartitionID string `rethinkdb:"partitionid" json:"partitionid"` + ProjectID string `rethinkdb:"projectid" json:"projectid"` + ParentNetworkID string `rethinkdb:"parentnetworkid" json:"parentnetworkid"` + Vrf uint `rethinkdb:"vrf" json:"vrf"` + PrivateSuper bool `rethinkdb:"privatesuper" json:"privatesuper"` + Nat bool `rethinkdb:"nat" json:"nat"` + Underlay bool `rethinkdb:"underlay" json:"underlay"` + Shared bool `rethinkdb:"shared" json:"shared"` + Labels map[string]string `rethinkdb:"labels" json:"labels"` + AddressFamily AddressFamily `rethinkdb:"addressfamily" json:"addressfamily"` +} + +// AddressFamily identifies IPv4/IPv6 +type AddressFamily string + +const ( + // IPv4AddressFamily identifies IPv4 + IPv4AddressFamily = AddressFamily("IPv4") + // IPv6AddressFamily identifies IPv6 + IPv6AddressFamily = AddressFamily("IPv6") +) + +// ToAddressFamily will convert a string af to a AddressFamily +func ToAddressFamily(af string) AddressFamily { + switch af { + case "IPv4", "ipv4": + return IPv4AddressFamily + case "IPv6", "ipv6": + return IPv6AddressFamily + } + return IPv4AddressFamily } // Networks is a list of networks. @@ -324,3 +348,21 @@ func (nics Nics) ByIdentifier() map[string]*Nic { return res } + +func GetAddressFamily(prefixes Prefixes) (*AddressFamily, error) { + if len(prefixes) == 0 { + return nil, nil + } + + parsed, err := netip.ParsePrefix(prefixes[0].String()) + if err != nil { + return nil, err + } + if parsed.Addr().Is4() { + return pointer.Pointer(IPv4AddressFamily), nil + } + if parsed.Addr().Is6() { + return pointer.Pointer(IPv6AddressFamily), nil + } + return nil, fmt.Errorf("unable to detect addressfamily from prefixes:%v", prefixes) +} diff --git a/cmd/metal-api/internal/metal/network_test.go b/cmd/metal-api/internal/metal/network_test.go index a7dcfddf..a62ad927 100644 --- a/cmd/metal-api/internal/metal/network_test.go +++ b/cmd/metal-api/internal/metal/network_test.go @@ -4,6 +4,8 @@ import ( "fmt" "reflect" "testing" + + "github.com/metal-stack/metal-lib/pkg/pointer" ) func TestNics_ByIdentifier(t *testing.T) { @@ -335,3 +337,47 @@ func TestNicState_SetState(t *testing.T) { }) } } + +func Test_getAddressFamily(t *testing.T) { + tests := []struct { + name string + prefixes Prefixes + want *AddressFamily + wantErr bool + }{ + { + name: "ipv4", + prefixes: Prefixes{{IP: "10.0.0.0", Length: "8"}}, + want: pointer.Pointer(IPv4AddressFamily), + }, + { + name: "ipv6", + prefixes: Prefixes{{IP: "2001::", Length: "64"}}, + want: pointer.Pointer(IPv6AddressFamily), + }, + { + name: "empty prefixes", + prefixes: Prefixes{}, + want: nil, + wantErr: false, + }, + { + name: "malformed ipv4", + prefixes: Prefixes{{IP: "10.0.0.0.0", Length: "6"}}, + want: nil, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := GetAddressFamily(tt.prefixes) + if (err != nil) != tt.wantErr { + t.Errorf("getAddressFamily() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("getAddressFamily() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/cmd/metal-api/internal/service/integration_test.go b/cmd/metal-api/internal/service/integration_test.go index eb10d04d..315030e2 100644 --- a/cmd/metal-api/internal/service/integration_test.go +++ b/cmd/metal-api/internal/service/integration_test.go @@ -295,15 +295,18 @@ func createTestEnvironment(t *testing.T) testEnv { PartitionID: &partition.ID, }, NetworkImmutable: v1.NetworkImmutable{ - Prefixes: []string{testPrivateSuperCidr}, - PrivateSuper: true, - ChildPrefixLength: pointer.Pointer(uint8(22)), + Prefixes: []string{testPrivateSuperCidr}, + PrivateSuper: true, + DefaultChildPrefixLength: pointer.Pointer(uint8(22)), + AddressFamily: v1.IPv4AddressFamily, }, } + log.Info("try to create a network", "request", ncr) status = te.networkCreate(t, ncr, &createdNetwork) require.Equal(t, http.StatusCreated, status) require.NotNil(t, createdNetwork) require.Equal(t, *ncr.ID, createdNetwork.ID) + log.Info("created a network", "nw", createdNetwork) te.privateSuperNetwork = &createdNetwork @@ -320,6 +323,7 @@ func createTestEnvironment(t *testing.T) testEnv { ProjectID: &projectID, PartitionID: &partition.ID, }, + AddressFamily: pointer.Pointer("ipv4"), } status = te.networkAcquire(t, nar, &acquiredPrivateNetwork) require.Equal(t, http.StatusCreated, status) diff --git a/cmd/metal-api/internal/service/network-service.go b/cmd/metal-api/internal/service/network-service.go index fd8e3794..8e0c4b41 100644 --- a/cmd/metal-api/internal/service/network-service.go +++ b/cmd/metal-api/internal/service/network-service.go @@ -23,11 +23,6 @@ import ( "github.com/metal-stack/metal-lib/pkg/pointer" ) -const ( - ipv4DefaultChildPrefixLength = uint8(22) - ipv6DefaultChildPrefixLength = uint8(64) -) - type networkResource struct { webResource ipamer ipam.IPAMer @@ -213,6 +208,7 @@ func (r *networkResource) findNetworks(request *restful.Request, response *restf } // TODO allow creation of networks with childprefixlength which are not privatesuper +// See https://github.com/metal-stack/metal-api/issues/16 func (r *networkResource) createNetwork(request *restful.Request, response *restful.Response) { var requestPayload v1.NetworkCreateRequest err := request.ReadEntity(&requestPayload) @@ -280,15 +276,9 @@ func (r *networkResource) createNetwork(request *restful.Request, response *rest return } - if privateSuper && requestPayload.ChildPrefixLength == nil { - var childprefixlength *uint8 - if addressFamily == v1.IPv4AddressFamily { - childprefixlength = pointer.Pointer(ipv4DefaultChildPrefixLength) - } - if addressFamily == v1.IPv6AddressFamily { - childprefixlength = pointer.Pointer(ipv6DefaultChildPrefixLength) - } - r.log.Info("createnetwork childprefixlength not set for private super network, using default", "addressfamily", addressFamily, "childprefixlength", childprefixlength) + if privateSuper && requestPayload.DefaultChildPrefixLength == nil { + r.sendError(request, response, httperrors.BadRequest(fmt.Errorf("private super network must always contain a defaultchildprefixlength"))) + return } destPrefixes := metal.Prefixes{} @@ -346,35 +336,20 @@ func (r *networkResource) createNetwork(request *restful.Request, response *rest // We should support two private super per partition, one per addressfamily // the network allocate request must be configurable with addressfamily if privateSuper { - boolTrue := true var nw metal.Network - err := r.ds.FindNetwork(&datastore.NetworkSearchQuery{PartitionID: &partition.ID, PrivateSuper: &boolTrue}, &nw) - if err != nil { - if !metal.IsNotFound(err) { - r.sendError(request, response, defaultError(err)) - return - } - } else { - if len(nw.Prefixes) != 0 { - existingsuper := nw.Prefixes[0].String() - - ipprefxexistingsuper, err := netip.ParsePrefix(existingsuper) - if err != nil { - r.sendError(request, response, httperrors.BadRequest(fmt.Errorf("given prefix %v is not valid: %w", existingsuper, err))) - return - - } - newsuper := prefixes[0].String() - ipprefixnew, err := netip.ParsePrefix(newsuper) - if err != nil { - r.sendError(request, response, httperrors.BadRequest(fmt.Errorf("given prefix %v is not valid: %w", newsuper, err))) - return - } - if (ipprefixnew.Addr().Is4() && ipprefxexistingsuper.Addr().Is4()) || (ipprefixnew.Addr().Is6() && ipprefxexistingsuper.Addr().Is6()) { - r.sendError(request, response, httperrors.BadRequest(fmt.Errorf("partition with id %q already has a private super network for this addressfamily", partition.ID))) - return - } - } + err := r.ds.FindNetwork(&datastore.NetworkSearchQuery{ + PartitionID: &partition.ID, + PrivateSuper: pointer.Pointer(true), + AddressFamily: pointer.Pointer(metal.ToAddressFamily(string(*addressFamily))), + }, &nw) + r.log.Info("createnetwork", "found", nw) + if err != nil && !metal.IsNotFound(err) { + r.sendError(request, response, defaultError(err)) + return + } + if nw.ID != "" { + r.sendError(request, response, httperrors.BadRequest(fmt.Errorf("partition with id %q already has a private super network for addressfamily:%s", partition.ID, *addressFamily))) + return } } if underlay { @@ -427,23 +402,24 @@ func (r *networkResource) createNetwork(request *restful.Request, response *rest Underlay: underlay, Vrf: vrf, Labels: labels, + AddressFamily: metal.AddressFamily(*addressFamily), } // check if childprefixlength is set and matches addressfamily - if requestPayload.ChildPrefixLength != nil && privateSuper { - cpl := *requestPayload.ChildPrefixLength + if requestPayload.DefaultChildPrefixLength != nil && privateSuper { + dcpl := *requestPayload.DefaultChildPrefixLength for _, p := range prefixes { ipprefix, err := netip.ParsePrefix(p.String()) if err != nil { r.sendError(request, response, httperrors.BadRequest(fmt.Errorf("given prefix %v is not a valid ip with mask: %w", p, err))) return } - if cpl <= uint8(ipprefix.Bits()) { - r.sendError(request, response, httperrors.BadRequest(fmt.Errorf("given childprefixlength %d is not greater than prefix length of:%s", cpl, p.String()))) + if dcpl <= uint8(ipprefix.Bits()) { + r.sendError(request, response, httperrors.BadRequest(fmt.Errorf("given defaultchildprefixlength %d is not greater than prefix length of:%s", dcpl, p.String()))) return } } - nw.ChildPrefixLength = requestPayload.ChildPrefixLength + nw.DefaultChildPrefixLength = requestPayload.DefaultChildPrefixLength } ctx := request.Request.Context() @@ -466,25 +442,7 @@ func (r *networkResource) createNetwork(request *restful.Request, response *rest r.send(request, response, http.StatusCreated, v1.NewNetworkResponse(nw, usage)) } -func getAddressFamily(prefixes metal.Prefixes) (*v1.AddressFamily, error) { - if len(prefixes) == 0 { - return nil, nil - } - - parsed, err := netip.ParsePrefix(prefixes[0].String()) - if err != nil { - return nil, err - } - if parsed.Addr().Is4() { - return pointer.Pointer(v1.IPv4AddressFamily), nil - } - if parsed.Addr().Is6() { - return pointer.Pointer(v1.IPv6AddressFamily), nil - } - return nil, fmt.Errorf("unable to detect addressfamily from prefixes:%v", prefixes.String()) -} - -func validatePrefixes(prefixes []string) (metal.Prefixes, v1.AddressFamily, error) { +func validatePrefixes(prefixes []string) (metal.Prefixes, *v1.AddressFamily, error) { var ( result metal.Prefixes addressFamilies = make(map[string]bool) @@ -493,11 +451,11 @@ func validatePrefixes(prefixes []string) (metal.Prefixes, v1.AddressFamily, erro for _, p := range prefixes { prefix, err := metal.NewPrefixFromCIDR(p) if err != nil { - return nil, v1.IPv4AddressFamily, fmt.Errorf("given prefix %v is not a valid ip with mask: %w", p, err) + return nil, nil, fmt.Errorf("given prefix %v is not a valid ip with mask: %w", p, err) } ipprefix, err := netip.ParsePrefix(p) if err != nil { - return nil, v1.IPv4AddressFamily, fmt.Errorf("given prefix %v is not a valid ip with mask: %w", p, err) + return nil, nil, fmt.Errorf("given prefix %v is not a valid ip with mask: %w", p, err) } if ipprefix.Addr().Is4() { addressFamilies["ipv4"] = true @@ -510,9 +468,9 @@ func validatePrefixes(prefixes []string) (metal.Prefixes, v1.AddressFamily, erro result = append(result, *prefix) } if len(addressFamilies) > 1 { - return nil, v1.IPv4AddressFamily, fmt.Errorf("given prefixes have different addressfamilies") + return nil, nil, fmt.Errorf("given prefixes have different addressfamilies") } - return result, addressFamily, nil + return result, &addressFamily, nil } // TODO add possibility to allocate from a non super network if given in the AllocateRequest and super has childprefixlength @@ -524,7 +482,6 @@ func (r *networkResource) allocateNetwork(request *restful.Request, response *re return } - r.log.Info("allocateNetwork", "request", requestPayload) var name string if requestPayload.Name != nil { name = *requestPayload.Name @@ -587,38 +544,29 @@ func (r *networkResource) allocateNetwork(request *restful.Request, response *re addressFamily = v1.ToAddressFamily(*requestPayload.AddressFamily) } - r.log.Info("network allocate", "family", addressFamily) + r.log.Info("network allocate", "family", addressFamily, "partition", partition.ID) var ( - superNetwork metal.Network - superNetworks metal.Networks - superNetworkFound bool + superNetwork metal.Network ) - err = r.ds.SearchNetworks(&datastore.NetworkSearchQuery{PartitionID: &partition.ID, PrivateSuper: pointer.Pointer(true)}, &superNetworks) + err = r.ds.FindNetwork(&datastore.NetworkSearchQuery{ + PartitionID: &partition.ID, + PrivateSuper: pointer.Pointer(true), + AddressFamily: pointer.Pointer(metal.ToAddressFamily(string(addressFamily))), + }, &superNetwork) if err != nil { r.sendError(request, response, defaultError(err)) return } - for _, snw := range superNetworks { - ipprefix, err := netip.ParsePrefix(snw.Prefixes[0].String()) - if err != nil { - r.sendError(request, response, httperrors.BadRequest(err)) - return - } - if addressFamily == v1.IPv4AddressFamily && ipprefix.Addr().Is4() { - superNetwork = snw - superNetworkFound = true - } - if addressFamily == v1.IPv6AddressFamily && ipprefix.Addr().Is6() { - superNetwork = snw - superNetworkFound = true - } - } - if !superNetworkFound { + if superNetwork.ID == "" { r.sendError(request, response, httperrors.BadRequest(fmt.Errorf("no supernetwork for addressfamily:%s found", addressFamily))) return } + if superNetwork.DefaultChildPrefixLength == nil { + r.sendError(request, response, httperrors.BadRequest(fmt.Errorf("supernetwork %s has no defaultchildprefixlength specified", superNetwork.ID))) + return + } r.log.Info("network allocate", "supernetwork", superNetwork.ID) nwSpec := &metal.Network{ @@ -632,18 +580,16 @@ func (r *networkResource) allocateNetwork(request *restful.Request, response *re DestinationPrefixes: destPrefixes, Shared: shared, Nat: nat, - } - if superNetwork.ChildPrefixLength == nil { - r.sendError(request, response, httperrors.BadRequest(fmt.Errorf("supernetwork %s has no childprefixlength specified", superNetwork.ID))) - return + AddressFamily: metal.AddressFamily(addressFamily), } // Allow configurable prefix length - length := *superNetwork.ChildPrefixLength + length := *superNetwork.DefaultChildPrefixLength if requestPayload.Length != nil { + // requestPayload.Length must be smaller than defaultchildprefixlength, but is checked in go-ipam length = *requestPayload.Length } - r.log.Info("network allocate", "supernetwork", superNetwork.Name, "length", length) + r.log.Info("network allocate", "supernetwork", superNetwork.ID, "defaultchildprefixlength", *superNetwork.DefaultChildPrefixLength, "length", length) ctx := request.Request.Context() nw, err := createChildNetwork(ctx, r.ds, r.ipamer, nwSpec, &superNetwork, length) @@ -747,12 +693,6 @@ func (r *networkResource) updateNetwork(request *restful.Request, response *rest return } - addressFamily, err := getAddressFamily(oldNetwork.Prefixes) - if err != nil { - r.sendError(request, response, defaultError(err)) - return - } - newNetwork := *oldNetwork if requestPayload.Name != nil { @@ -786,8 +726,8 @@ func (r *networkResource) updateNetwork(request *restful.Request, response *rest return } - if af != *addressFamily { - r.sendError(request, response, httperrors.BadRequest(fmt.Errorf("new prefixes have different addressfamily %q then existing prefixes %q", af, *addressFamily))) + if *af != v1.FromAddressFamily(oldNetwork.AddressFamily) { + r.sendError(request, response, httperrors.BadRequest(fmt.Errorf("new prefixes have different addressfamily %q then existing prefixes %q", *af, oldNetwork.AddressFamily))) return } @@ -837,8 +777,8 @@ func (r *networkResource) updateNetwork(request *restful.Request, response *rest return } - if af != *addressFamily { - r.sendError(request, response, httperrors.BadRequest(fmt.Errorf("new destination prefixes have different addressfamily %q then existing destination prefixes %q", af, *addressFamily))) + if *af != v1.FromAddressFamily(oldNetwork.AddressFamily) { + r.sendError(request, response, httperrors.BadRequest(fmt.Errorf("new destination prefixes have different addressfamily %q then existing destination prefixes %q", *af, oldNetwork.AddressFamily))) return } diff --git a/cmd/metal-api/internal/service/network-service_test.go b/cmd/metal-api/internal/service/network-service_test.go index 9a7d8238..ed400b38 100644 --- a/cmd/metal-api/internal/service/network-service_test.go +++ b/cmd/metal-api/internal/service/network-service_test.go @@ -292,10 +292,23 @@ func Test_networkResource_createNetwork(t *testing.T) { projectID: testdata.Nw1.ProjectID, prefixes: []string{"172.0.0.0/24"}, destinationPrefixes: []string{"0.0.0.0/0"}, + childprefixlength: pointer.Pointer(uint8(22)), privateSuper: true, vrf: uint(10000), expectedStatus: http.StatusBadRequest, - expectedErrorMessage: "partition with id \"1\" already has a private super network for this addressfamily", + expectedErrorMessage: "partition with id \"1\" already has a private super network for addressfamily:IPv4", + }, + { + name: "privatesuper IPv4 without defaultchildprefixlength", + networkName: testdata.Nw1.Name, + partitionID: testdata.Nw1.PartitionID, + projectID: testdata.Nw1.ProjectID, + prefixes: []string{"172.0.0.0/24"}, + destinationPrefixes: []string{"0.0.0.0/0"}, + privateSuper: true, + vrf: uint(10001), + expectedStatus: http.StatusBadRequest, + expectedErrorMessage: "private super network must always contain a defaultchildprefixlength", }, { name: "privatesuper IPv6", @@ -304,6 +317,7 @@ func Test_networkResource_createNetwork(t *testing.T) { projectID: testdata.Nw1.ProjectID, prefixes: []string{"fdaa:bbcc::/50"}, destinationPrefixes: []string{"::/0"}, + childprefixlength: pointer.Pointer(uint8(64)), privateSuper: true, vrf: uint(10000), expectedStatus: http.StatusCreated, @@ -315,6 +329,7 @@ func Test_networkResource_createNetwork(t *testing.T) { projectID: testdata.Nw1.ProjectID, prefixes: []string{"192.168.265.0/24"}, destinationPrefixes: []string{"0.0.0.0/0"}, + childprefixlength: pointer.Pointer(uint8(64)), privateSuper: true, vrf: uint(10000), expectedStatus: http.StatusBadRequest, @@ -364,7 +379,7 @@ func Test_networkResource_createNetwork(t *testing.T) { privateSuper: true, vrf: uint(10000), expectedStatus: http.StatusBadRequest, - expectedErrorMessage: "given childprefixlength 50 is not greater than prefix length of:fdaa:bbcc::/50", + expectedErrorMessage: "given defaultchildprefixlength 50 is not greater than prefix length of:fdaa:bbcc::/50", }, } for _, tt := range tests { @@ -387,7 +402,7 @@ func Test_networkResource_createNetwork(t *testing.T) { }, } if tt.childprefixlength != nil { - createRequest.ChildPrefixLength = tt.childprefixlength + createRequest.DefaultChildPrefixLength = tt.childprefixlength } js, _ := json.Marshal(createRequest) body := bytes.NewBuffer(js) @@ -416,7 +431,7 @@ func Test_networkResource_createNetwork(t *testing.T) { require.Equal(t, tt.projectID, *result.ProjectID) require.Equal(t, tt.destinationPrefixes, result.DestinationPrefixes) if tt.childprefixlength != nil { - require.Equal(t, tt.childprefixlength, result.ChildPrefixLength) + require.Equal(t, tt.childprefixlength, result.DefaultChildPrefixLength) } } }) @@ -439,45 +454,44 @@ func Test_networkResource_allocateNetwork(t *testing.T) { { name: "simple ipv4, default childprefixlength", networkName: "tenantv4", - partitionID: testdata.Partition1.ID, + partitionID: testdata.Partition2.ID, projectID: "project-1", expectedStatus: http.StatusCreated, }, { name: "simple ipv4, specific childprefixlength", networkName: "tenantv4.2", - partitionID: testdata.Partition1.ID, + partitionID: testdata.Partition2.ID, projectID: "project-1", childprefixlength: pointer.Pointer(uint8(29)), expectedStatus: http.StatusCreated, }, { - name: "ipv6 without ipv6 super", - networkName: "tenantv6", - partitionID: testdata.Partition1.ID, - projectID: "project-1", - addressFamily: pointer.Pointer("ipv6"), - expectedStatus: http.StatusBadRequest, - expectedErrorMessage: "no supernetwork for addressfamily:IPv6 found", + name: "ipv6 default childprefixlength", + networkName: "tenantv6", + partitionID: testdata.Partition2.ID, + projectID: "project-1", + addressFamily: pointer.Pointer("ipv6"), + expectedStatus: http.StatusCreated, + }, + { + name: "simple ipv6, specific childprefixlength", + networkName: "tenantv6.2", + partitionID: testdata.Partition2.ID, + projectID: "project-1", + addressFamily: pointer.Pointer("ipv6"), + childprefixlength: pointer.Pointer(uint8(58)), + expectedStatus: http.StatusCreated, }, } for _, tt := range tests { ds, mock := datastore.InitMockDB(t) - - supernetwork := testdata.Nw1 - ipamer, err := testdata.InitMockIpamData(mock, false) - require.NoError(t, err) - mock.On(r.DB("mockdb").Table("network").Filter(r.MockAnything()).Filter(r.MockAnything())).Return(metal.Networks{supernetwork}, nil) changes := []r.ChangeResponse{{OldValue: map[string]interface{}{"id": float64(42)}}} mock.On(r.DB("mockdb").Table("integerpool").Limit(1).Delete(r. DeleteOpts{ReturnChanges: true})).Return(r.WriteResponse{Changes: changes}, nil) - mock.On(r.DB("mockdb").Table("partition").Get(r.MockAnything())).Return( - metal.Partition{ - Base: metal.Base{ID: tt.partitionID}, - }, - nil, - ) + ipamer, err := testdata.InitMockIpamData(mock, false) + require.NoError(t, err) testdata.InitMockDBData(mock) psc := mdmv1mock.ProjectServiceClient{} @@ -535,65 +549,56 @@ func Test_networkResource_allocateNetwork(t *testing.T) { if resultFirstPrefix.Addr().Is6() { af = "ipv6" } - expectedLength := *supernetwork.ChildPrefixLength - if tt.childprefixlength != nil { - expectedLength = *tt.childprefixlength - } + require.NoError(t, err) require.Equal(t, tt.networkName, *result.Name) require.Equal(t, tt.partitionID, *result.PartitionID) require.Equal(t, tt.projectID, *result.ProjectID) require.Equal(t, requestAF, af) - require.Equal(t, int(expectedLength), resultFirstPrefix.Bits()) } } } -func Test_getAddressFamily(t *testing.T) { +func Test_validatePrefixes(t *testing.T) { tests := []struct { - name string - prefixes metal.Prefixes - want *v1.AddressFamily - wantErr bool + name string + prefixes []string + wantPrefixes metal.Prefixes + wantAF *v1.AddressFamily + wantErr bool }{ { - name: "ipv4", - prefixes: metal.Prefixes{ - metal.Prefix{IP: "10.0.0.0", Length: "8"}, - }, - want: pointer.Pointer(v1.IPv4AddressFamily), - }, - { - name: "ipv6", - prefixes: metal.Prefixes{ - metal.Prefix{IP: "2001::", Length: "64"}, - }, - want: pointer.Pointer(v1.IPv6AddressFamily), + name: "simple all ipv4", + prefixes: []string{"10.0.0.0/8", "11.0.0.0/24"}, + wantPrefixes: metal.Prefixes{{IP: "10.0.0.0", Length: "8"}, {IP: "11.0.0.0", Length: "24"}}, + wantAF: pointer.Pointer(v1.IPv4AddressFamily), }, { - name: "empty prefixes", - prefixes: metal.Prefixes{}, - want: nil, - wantErr: false, + name: "simple all ipv6", + prefixes: []string{"2001::/64", "fbaa::/48"}, + wantPrefixes: metal.Prefixes{{IP: "2001::", Length: "64"}, {IP: "fbaa::", Length: "48"}}, + wantAF: pointer.Pointer(v1.IPv6AddressFamily), }, { - name: "malformed ipv4", - prefixes: metal.Prefixes{ - metal.Prefix{IP: "10.0.0.0.0", Length: "6"}, - }, - want: nil, - wantErr: true, + name: "mixed af", + prefixes: []string{"10.0.0.0/8", "2001::/64"}, + wantPrefixes: nil, + wantAF: nil, + wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := getAddressFamily(tt.prefixes) + got, got1, err := validatePrefixes(tt.prefixes) if (err != nil) != tt.wantErr { - t.Errorf("getAddressFamily() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("validatePrefixes() error = %v, wantErr %v", err, tt.wantErr) return } - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("getAddressFamily() = %v, want %v", got, tt.want) + if !reflect.DeepEqual(got, tt.wantPrefixes) { + t.Errorf("validatePrefixes() got = %v, want %v", got, tt.wantPrefixes) + } + if !reflect.DeepEqual(got1, tt.wantAF) { + t.Errorf("validatePrefixes() got1 = %v, want %v", got1, tt.wantAF) } }) } diff --git a/cmd/metal-api/internal/service/size-service.go b/cmd/metal-api/internal/service/size-service.go index 91050a1a..2be4d54a 100644 --- a/cmd/metal-api/internal/service/size-service.go +++ b/cmd/metal-api/internal/service/size-service.go @@ -12,7 +12,7 @@ import ( "github.com/metal-stack/metal-api/cmd/metal-api/internal/metal" v1 "github.com/metal-stack/metal-api/cmd/metal-api/internal/service/v1" "github.com/metal-stack/metal-lib/auditing" - "github.com/metal-stack/metal-lib/pkg/pointer" + "google.golang.org/protobuf/types/known/wrapperspb" restfulspec "github.com/emicklei/go-restful-openapi/v2" restful "github.com/emicklei/go-restful/v3" @@ -70,7 +70,7 @@ func (r *sizeResource) webService() *restful.WebService { Doc("get all size reservations"). Metadata(restfulspec.KeyOpenAPITags, tags). Metadata(auditing.Exclude, true). - Reads(v1.EmptyBody{}). + Reads(v1.SizeReservationListRequest{}). Writes([]v1.SizeReservationResponse{}). Returns(http.StatusOK, "OK", []v1.SizeReservationResponse{}). DefaultReturns("Error", httperrors.HTTPErrorResponse{})) @@ -439,19 +439,45 @@ func (r *sizeResource) updateSize(request *restful.Request, response *restful.Re } func (r *sizeResource) listSizeReservations(request *restful.Request, response *restful.Response) { - ss, err := r.ds.ListSizes() + var requestPayload v1.SizeReservationListRequest + err := request.ReadEntity(&requestPayload) + if err != nil { + r.sendError(request, response, httperrors.BadRequest(err)) + return + } + + ss := metal.Sizes{} + err = r.ds.SearchSizes(&datastore.SizeSearchQuery{ + ID: requestPayload.SizeID, + Reservation: datastore.Reservation{ + Partition: requestPayload.PartitionID, + Project: requestPayload.ProjectID, + }, + }, &ss) if err != nil { r.sendError(request, response, defaultError(err)) return } - projects, err := r.mdc.Project().Find(request.Request.Context(), &mdmv1.ProjectFindRequest{}) + pfr := &mdmv1.ProjectFindRequest{} + + if requestPayload.ProjectID != nil { + pfr.Id = wrapperspb.String(*requestPayload.ProjectID) + } + if requestPayload.Tenant != nil { + pfr.TenantId = wrapperspb.String(*requestPayload.Tenant) + } + + projects, err := r.mdc.Project().Find(request.Request.Context(), pfr) if err != nil { r.sendError(request, response, defaultError(err)) return } - ms, err := r.ds.ListMachines() + ms := metal.Machines{} + err = r.ds.SearchMachines(&datastore.MachineSearchQuery{ + PartitionID: requestPayload.PartitionID, + }, &ms) if err != nil { r.sendError(request, response, defaultError(err)) return @@ -469,8 +495,12 @@ func (r *sizeResource) listSizeReservations(request *restful.Request, response * for _, reservation := range size.Reservations { reservation := reservation + project, ok := projectsByID[reservation.ProjectID] + if !ok { + continue + } + for _, partitionID := range reservation.PartitionIDs { - project := pointer.SafeDeref(projectsByID[reservation.ProjectID]) allocations := len(machinesByProjectID[reservation.ProjectID].WithPartition(partitionID).WithSize(size.ID)) result = append(result, &v1.SizeReservationResponse{ diff --git a/cmd/metal-api/internal/service/size-service_test.go b/cmd/metal-api/internal/service/size-service_test.go index 2a61238b..e5136780 100644 --- a/cmd/metal-api/internal/service/size-service_test.go +++ b/cmd/metal-api/internal/service/size-service_test.go @@ -18,10 +18,12 @@ import ( v1 "github.com/metal-stack/metal-api/cmd/metal-api/internal/service/v1" "github.com/metal-stack/metal-api/cmd/metal-api/internal/testdata" "github.com/metal-stack/metal-lib/httperrors" + "github.com/metal-stack/metal-lib/pkg/pointer" "github.com/stretchr/testify/assert" testifymock "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" - "gopkg.in/rethinkdb/rethinkdb-go.v6" + "google.golang.org/protobuf/types/known/wrapperspb" + r "gopkg.in/rethinkdb/rethinkdb-go.v6" ) func TestGetSizes(t *testing.T) { @@ -79,13 +81,13 @@ func TestGetSize(t *testing.T) { func TestSuggest(t *testing.T) { tests := []struct { name string - mockFn func(mock *rethinkdb.Mock) + mockFn func(mock *r.Mock) want []metal.Constraint }{ { name: "size", - mockFn: func(mock *rethinkdb.Mock) { - mock.On(rethinkdb.DB("mockdb").Table("machine").Get("1")).Return(&metal.Machine{ + mockFn: func(mock *r.Mock) { + mock.On(r.DB("mockdb").Table("machine").Get("1")).Return(&metal.Machine{ Hardware: metal.MachineHardware{ MetalCPUs: []metal.MetalCPU{ { @@ -341,46 +343,95 @@ func TestUpdateSize(t *testing.T) { } func TestListSizeReservations(t *testing.T) { - ds, mock := datastore.InitMockDB(t) - testdata.InitMockDBData(mock) - log := slog.Default() - - psc := &mdmv1mock.ProjectServiceClient{} - psc.On("Find", testifymock.Anything, &mdmv1.ProjectFindRequest{}).Return(&mdmv1.ProjectListResponse{Projects: []*mdmv1.Project{ - {Meta: &mdmv1.Meta{Id: "p1"}}, - }}, nil) - mdc := mdm.NewMock(psc, &mdmv1mock.TenantServiceClient{}, nil, nil) - - sizeservice := NewSize(log, ds, mdc) - container := restful.NewContainer().Add(sizeservice) - - req := httptest.NewRequest("POST", "/v1/size/reservations", nil) - req.Header.Add("Content-Type", "application/json") - container = injectAdmin(log, container, req) - w := httptest.NewRecorder() - container.ServeHTTP(w, req) - - resp := w.Result() - defer resp.Body.Close() - require.Equal(t, http.StatusOK, resp.StatusCode, w.Body.String()) - var result []*v1.SizeReservationResponse - err := json.NewDecoder(resp.Body).Decode(&result) - require.NoError(t, err) - - want := []*v1.SizeReservationResponse{ + tests := []struct { + name string + req *v1.SizeReservationListRequest + dbMockFn func(mock *r.Mock) + projectMockFn func(mock *testifymock.Mock) + want []*v1.SizeReservationResponse + }{ { - SizeID: testdata.Sz1.ID, - PartitionID: "1", - ProjectID: "p1", - Reservations: 3, - UsedReservations: 1, - ProjectAllocations: 1, + name: "list reservations", + req: &v1.SizeReservationListRequest{ + SizeID: pointer.Pointer("1"), + Tenant: pointer.Pointer("t1"), + ProjectID: pointer.Pointer("p1"), + PartitionID: pointer.Pointer("a"), + }, + dbMockFn: func(mock *r.Mock) { + mock.On(r.DB("mockdb").Table("size").Filter(r.MockAnything()).Filter(r.MockAnything()).Filter(r.MockAnything())).Return(metal.Sizes{ + { + Base: metal.Base{ + ID: "1", + }, + Reservations: metal.Reservations{ + { + Amount: 3, + PartitionIDs: []string{"a"}, + ProjectID: "p1", + }, + }, + }, + }, nil) + mock.On(r.DB("mockdb").Table("machine").Filter(r.MockAnything())).Return(metal.Machines{ + { + Base: metal.Base{ + ID: "1", + }, + SizeID: "1", + PartitionID: "a", + Allocation: &metal.MachineAllocation{ + Project: "p1", + }, + }, + }, nil) + }, + projectMockFn: func(mock *testifymock.Mock) { + mock.On("Find", testifymock.Anything, &mdmv1.ProjectFindRequest{ + Id: wrapperspb.String("p1"), + TenantId: wrapperspb.String("t1"), + }).Return(&mdmv1.ProjectListResponse{Projects: []*mdmv1.Project{ + {Meta: &mdmv1.Meta{Id: "p1"}, TenantId: "t1"}, + }}, nil) + }, + want: []*v1.SizeReservationResponse{ + { + SizeID: "1", + PartitionID: "a", + Tenant: "t1", + ProjectID: "p1", + Reservations: 3, + UsedReservations: 1, + ProjectAllocations: 1, + }, + }, }, } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var ( + projectMock = mdmv1mock.NewProjectServiceClient(t) + m = mdm.NewMock(projectMock, nil, nil, nil) + ds, dbMock = datastore.InitMockDB(t) + ws = NewSize(slog.Default(), ds, m) + ) + + if tt.dbMockFn != nil { + tt.dbMockFn(dbMock) + } + if tt.projectMockFn != nil { + tt.projectMockFn(&projectMock.Mock) + } - if diff := cmp.Diff(result, want); diff != "" { - t.Errorf("diff (-want +got):\n%s", diff) + code, got := genericWebRequest[[]*v1.SizeReservationResponse](t, ws, testViewUser, tt.req, "POST", "/v1/size/reservations") + assert.Equal(t, http.StatusOK, code) + + if diff := cmp.Diff(tt.want, got); diff != "" { + t.Errorf("diff (-want +got):\n%s", diff) + } + }) } + } func Test_longestCommonPrefix(t *testing.T) { diff --git a/cmd/metal-api/internal/service/switch-service.go b/cmd/metal-api/internal/service/switch-service.go index 092096c1..4e9cd57b 100644 --- a/cmd/metal-api/internal/service/switch-service.go +++ b/cmd/metal-api/internal/service/switch-service.go @@ -5,6 +5,7 @@ import ( "fmt" "log/slog" "net/http" + "net/netip" "sort" "strconv" "strings" @@ -776,20 +777,27 @@ func makeSwitchResponse(s *metal.Switch, ds *datastore.RethinkStore) (*v1.Switch return nil, err } - nics := makeSwitchNics(s, ips, machines) + nics, err := makeSwitchNics(s, ips, machines) + if err != nil { + return nil, err + } cons := makeSwitchCons(s) return v1.NewSwitchResponse(s, ss, p, nics, cons), nil } -func makeBGPFilterFirewall(m metal.Machine) v1.BGPFilter { +func makeBGPFilterFirewall(m metal.Machine) (v1.BGPFilter, error) { vnis := []string{} cidrs := []string{} for _, net := range m.Allocation.MachineNetworks { if net.Underlay { for _, ip := range net.IPs { - cidrs = append(cidrs, fmt.Sprintf("%s/32", ip)) + ipwithMask, err := ipWithMask(ip) + if err != nil { + return v1.BGPFilter{}, err + } + cidrs = append(cidrs, ipwithMask) } } else { vnis = append(vnis, fmt.Sprintf("%d", net.Vrf)) @@ -797,10 +805,10 @@ func makeBGPFilterFirewall(m metal.Machine) v1.BGPFilter { } } - return v1.NewBGPFilter(vnis, cidrs) + return v1.NewBGPFilter(vnis, cidrs), nil } -func makeBGPFilterMachine(m metal.Machine, ips metal.IPsMap) v1.BGPFilter { +func makeBGPFilterMachine(m metal.Machine, ips metal.IPsMap) (v1.BGPFilter, error) { vnis := []string{} cidrs := []string{} @@ -828,29 +836,44 @@ func makeBGPFilterMachine(m metal.Machine, ips metal.IPsMap) v1.BGPFilter { continue } // Allow all other ip addresses allocated for the project. - cidrs = append(cidrs, fmt.Sprintf("%s/32", i.IPAddress)) + ipwithMask, err := ipWithMask(i.IPAddress) + if err != nil { + return v1.BGPFilter{}, err + } + cidrs = append(cidrs, ipwithMask) } - return v1.NewBGPFilter(vnis, cidrs) + return v1.NewBGPFilter(vnis, cidrs), nil } -func makeBGPFilter(m metal.Machine, vrf string, ips metal.IPsMap) v1.BGPFilter { - var filter v1.BGPFilter +func ipWithMask(ip string) (string, error) { + parsed, err := netip.ParseAddr(ip) + if err != nil { + return "", err + } + return fmt.Sprintf("%s/%d", ip, parsed.BitLen()), nil +} + +func makeBGPFilter(m metal.Machine, vrf string, ips metal.IPsMap) (v1.BGPFilter, error) { + var ( + filter v1.BGPFilter + err error + ) if m.IsFirewall() { // vrf "default" means: the firewall was successfully allocated and the switch port configured // otherwise the port is still not configured yet (pxe-setup) and a BGPFilter would break the install routine if vrf == "default" { - filter = makeBGPFilterFirewall(m) + filter, err = makeBGPFilterFirewall(m) } } else { - filter = makeBGPFilterMachine(m, ips) + filter, err = makeBGPFilterMachine(m, ips) } - return filter + return filter, err } -func makeSwitchNics(s *metal.Switch, ips metal.IPsMap, machines metal.Machines) v1.SwitchNics { +func makeSwitchNics(s *metal.Switch, ips metal.IPsMap, machines metal.Machines) (v1.SwitchNics, error) { machinesByID := map[string]*metal.Machine{} for i, m := range machines { machinesByID[m.ID] = &machines[i] @@ -871,7 +894,10 @@ func makeSwitchNics(s *metal.Switch, ips metal.IPsMap, machines metal.Machines) m := machinesBySwp[n.Name] var filter *v1.BGPFilter if m != nil && m.Allocation != nil { - f := makeBGPFilter(*m, n.Vrf, ips) + f, err := makeBGPFilter(*m, n.Vrf, ips) + if err != nil { + return nil, err + } filter = &f } nic := v1.SwitchNic{ @@ -896,7 +922,7 @@ func makeSwitchNics(s *metal.Switch, ips metal.IPsMap, machines metal.Machines) return nics[i].Name < nics[j].Name }) - return nics + return nics, nil } func makeSwitchCons(s *metal.Switch) []v1.SwitchConnection { @@ -990,7 +1016,10 @@ func makeSwitchResponseList(ss metal.Switches, ds *datastore.RethinkStore) ([]*v p = &partitionEntity } - nics := makeSwitchNics(&sw, ips, m) + nics, err := makeSwitchNics(&sw, ips, m) + if err != nil { + return nil, err + } cons := makeSwitchCons(&sw) ss, err := ds.GetSwitchStatus(sw.ID) if err != nil && !metal.IsNotFound(err) { diff --git a/cmd/metal-api/internal/service/switch-service_test.go b/cmd/metal-api/internal/service/switch-service_test.go index 46bbd031..51be9077 100644 --- a/cmd/metal-api/internal/service/switch-service_test.go +++ b/cmd/metal-api/internal/service/switch-service_test.go @@ -15,7 +15,6 @@ import ( "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "gopkg.in/rethinkdb/rethinkdb-go.v6" r "gopkg.in/rethinkdb/rethinkdb-go.v6" "github.com/metal-stack/metal-api/cmd/metal-api/internal/datastore" @@ -351,11 +350,20 @@ func TestMakeBGPFilterFirewall(t *testing.T) { IPs: []string{"212.89.42.1", "212.89.42.2"}, Vrf: 104009, }, + { + IPs: []string{"2001::", "fe80::"}, + Vrf: 104011, + }, + { + IPs: []string{"2002::", "fe81::"}, + Underlay: true, + Vrf: 104012, + }, }, }, }, }, - want: v1.NewBGPFilter([]string{"104009", "104010"}, []string{"10.0.0.1/32", "10.0.0.2/32"}), + want: v1.NewBGPFilter([]string{"104009", "104010", "104011"}, []string{"10.0.0.1/32", "10.0.0.2/32", "2002::/128", "fe81::/128"}), }, { name: "no underlay firewall networks", @@ -395,7 +403,7 @@ func TestMakeBGPFilterFirewall(t *testing.T) { for i := range tests { tt := tests[i] t.Run(tt.name, func(t *testing.T) { - got := makeBGPFilterFirewall(tt.args.machine) + got, _ := makeBGPFilterFirewall(tt.args.machine) if !reflect.DeepEqual(got, tt.want) { t.Errorf("makeBGPFilterFirewall() = %v, want %v", got, tt.want) } @@ -429,6 +437,9 @@ func TestMakeBGPFilterMachine(t *testing.T) { metal.IP{ IPAddress: "10.1.0.1", }, + metal.IP{ + IPAddress: "2001::1", + }, }}, machine: metal.Machine{ Allocation: &metal.MachineAllocation{ @@ -449,11 +460,15 @@ func TestMakeBGPFilterMachine(t *testing.T) { IPs: []string{"212.89.42.2", "212.89.42.1"}, Vrf: 104009, }, + { + IPs: []string{"2001::"}, + Vrf: 104010, + }, }, }, }, }, - want: v1.NewBGPFilter([]string{}, []string{"10.1.0.0/22", "10.2.0.0/22", "100.127.1.1/32", "212.89.42.1/32", "212.89.42.2/32"}), + want: v1.NewBGPFilter([]string{}, []string{"10.1.0.0/22", "10.2.0.0/22", "100.127.1.1/32", "2001::1/128", "212.89.42.1/32", "212.89.42.2/32"}), }, { name: "allow only allocated ips", @@ -481,7 +496,7 @@ func TestMakeBGPFilterMachine(t *testing.T) { for i := range tests { tt := tests[i] t.Run(tt.name, func(t *testing.T) { - got := makeBGPFilterMachine(tt.args.machine, tt.args.ipsMap) + got, _ := makeBGPFilterMachine(tt.args.machine, tt.args.ipsMap) if !reflect.DeepEqual(got, tt.want) { t.Errorf("makeBGPFilterMachine() = %v, want %v", got, tt.want) } @@ -588,7 +603,7 @@ func TestMakeSwitchNics(t *testing.T) { for i := range tests { tt := tests[i] t.Run(tt.name, func(t *testing.T) { - got := makeSwitchNics(tt.args.s, tt.args.ips, tt.args.machines) + got, _ := makeSwitchNics(tt.args.s, tt.args.ips, tt.args.machines) if !reflect.DeepEqual(got, tt.want) { t.Errorf("makeSwitchNics() = %v, want %v", got, tt.want) } @@ -1425,7 +1440,7 @@ func TestToggleSwitchNicWithoutMachine(t *testing.T) { func Test_SwitchDelete(t *testing.T) { tests := []struct { name string - mockFn func(mock *rethinkdb.Mock) + mockFn func(mock *r.Mock) want *v1.SwitchResponse wantErr error wantStatus int @@ -1433,15 +1448,15 @@ func Test_SwitchDelete(t *testing.T) { }{ { name: "delete switch", - mockFn: func(mock *rethinkdb.Mock) { - mock.On(rethinkdb.DB("mockdb").Table("switch").Get("switch-1")).Return(&metal.Switch{ + mockFn: func(mock *r.Mock) { + mock.On(r.DB("mockdb").Table("switch").Get("switch-1")).Return(&metal.Switch{ Base: metal.Base{ ID: "switch-1", }, }, nil) - mock.On(rethinkdb.DB("mockdb").Table("switch").Get("switch-1").Delete()).Return(testdata.EmptyResult, nil) - mock.On(rethinkdb.DB("mockdb").Table("switchstatus").Get("switch-1")).Return(nil, nil) - mock.On(rethinkdb.DB("mockdb").Table("ip")).Return(nil, nil) + mock.On(r.DB("mockdb").Table("switch").Get("switch-1").Delete()).Return(testdata.EmptyResult, nil) + mock.On(r.DB("mockdb").Table("switchstatus").Get("switch-1")).Return(nil, nil) + mock.On(r.DB("mockdb").Table("ip")).Return(nil, nil) }, want: &v1.SwitchResponse{ Common: v1.Common{ @@ -1460,8 +1475,8 @@ func Test_SwitchDelete(t *testing.T) { }, { name: "delete switch does not work due to machine connections", - mockFn: func(mock *rethinkdb.Mock) { - mock.On(rethinkdb.DB("mockdb").Table("switch").Get("switch-1")).Return(&metal.Switch{ + mockFn: func(mock *r.Mock) { + mock.On(r.DB("mockdb").Table("switch").Get("switch-1")).Return(&metal.Switch{ Base: metal.Base{ ID: "switch-1", }, @@ -1469,7 +1484,7 @@ func Test_SwitchDelete(t *testing.T) { "port-a": metal.Connections{}, }, }, nil) - mock.On(rethinkdb.DB("mockdb").Table("switch").Get("switch-1").Delete()).Return(testdata.EmptyResult, nil) + mock.On(r.DB("mockdb").Table("switch").Get("switch-1").Delete()).Return(testdata.EmptyResult, nil) }, wantErr: &httperrors.HTTPErrorResponse{ StatusCode: http.StatusBadRequest, @@ -1479,8 +1494,8 @@ func Test_SwitchDelete(t *testing.T) { }, { name: "delete switch with force", - mockFn: func(mock *rethinkdb.Mock) { - mock.On(rethinkdb.DB("mockdb").Table("switch").Get("switch-1")).Return(&metal.Switch{ + mockFn: func(mock *r.Mock) { + mock.On(r.DB("mockdb").Table("switch").Get("switch-1")).Return(&metal.Switch{ Base: metal.Base{ ID: "switch-1", }, @@ -1488,9 +1503,9 @@ func Test_SwitchDelete(t *testing.T) { "port-a": metal.Connections{}, }, }, nil) - mock.On(rethinkdb.DB("mockdb").Table("switch").Get("switch-1").Delete()).Return(testdata.EmptyResult, nil) - mock.On(rethinkdb.DB("mockdb").Table("switchstatus").Get("switch-1")).Return(nil, nil) - mock.On(rethinkdb.DB("mockdb").Table("ip")).Return(nil, nil) + mock.On(r.DB("mockdb").Table("switch").Get("switch-1").Delete()).Return(testdata.EmptyResult, nil) + mock.On(r.DB("mockdb").Table("switchstatus").Get("switch-1")).Return(nil, nil) + mock.On(r.DB("mockdb").Table("ip")).Return(nil, nil) }, force: true, want: &v1.SwitchResponse{ diff --git a/cmd/metal-api/internal/service/v1/network.go b/cmd/metal-api/internal/service/v1/network.go index c1362d62..c6ebb28b 100644 --- a/cmd/metal-api/internal/service/v1/network.go +++ b/cmd/metal-api/internal/service/v1/network.go @@ -15,15 +15,16 @@ type NetworkBase struct { // NetworkImmutable defines the properties which are immutable in the Network. type NetworkImmutable struct { - Prefixes []string `json:"prefixes" modelDescription:"a network which contains prefixes from which IP addresses can be allocated" description:"the prefixes of this network"` - DestinationPrefixes []string `json:"destinationprefixes" modelDescription:"prefixes that are reachable within this network" description:"the destination prefixes of this network"` - ChildPrefixLength *uint8 `json:"childprefixlength" description:"if privatesuper, this defines the bitlen of child prefixes if not nil" optional:"true"` - Nat bool `json:"nat" description:"if set to true, packets leaving this network get masqueraded behind interface ip"` - PrivateSuper bool `json:"privatesuper" description:"if set to true, this network will serve as a partition's super network for the internal machine networks,there can only be one privatesuper network per partition"` - Underlay bool `json:"underlay" description:"if set to true, this network can be used for underlay communication"` - Vrf *uint `json:"vrf" description:"the vrf this network is associated with" optional:"true"` - VrfShared *bool `json:"vrfshared" description:"if set to true, given vrf can be used by multiple networks, which is sometimes useful for network partitioning (default: false)" optional:"true"` - ParentNetworkID *string `json:"parentnetworkid" description:"the id of the parent network" optional:"true"` + Prefixes []string `json:"prefixes" modelDescription:"a network which contains prefixes from which IP addresses can be allocated" description:"the prefixes of this network"` + DestinationPrefixes []string `json:"destinationprefixes" modelDescription:"prefixes that are reachable within this network" description:"the destination prefixes of this network"` + DefaultChildPrefixLength *uint8 `json:"defaultchildprefixlength" description:"if privatesuper, this defines the bitlen of child prefixes if not nil" optional:"true"` + Nat bool `json:"nat" description:"if set to true, packets leaving this network get masqueraded behind interface ip"` + PrivateSuper bool `json:"privatesuper" description:"if set to true, this network will serve as a partition's super network for the internal machine networks,there can only be one privatesuper network per partition"` + Underlay bool `json:"underlay" description:"if set to true, this network can be used for underlay communication"` + Vrf *uint `json:"vrf" description:"the vrf this network is associated with" optional:"true"` + VrfShared *bool `json:"vrfshared" description:"if set to true, given vrf can be used by multiple networks, which is sometimes useful for network partitioning (default: false)" optional:"true"` + ParentNetworkID *string `json:"parentnetworkid" description:"the id of the parent network" optional:"true"` + AddressFamily AddressFamily `json:"addressfamily" description:"the addressfamily either IPv4 or IPv6 of this network" enum:"IPv4|IPv6"` } // NetworkUsage reports core metrics about available and used IPs or Prefixes in a Network. @@ -49,7 +50,7 @@ type NetworkAllocateRequest struct { DestinationPrefixes []string `json:"destinationprefixes" description:"the destination prefixes of this network" optional:"true"` Nat *bool `json:"nat" description:"if set to true, packets leaving this network get masqueraded behind interface ip" optional:"true"` AddressFamily *string `json:"address_family" description:"can be ipv4 or ipv6, defaults to ipv4"` - Length *uint8 `json:"length" description:"the bitlen of the prefix to allocate, defaults to childprefixlength of super prefix"` + Length *uint8 `json:"length" description:"the bitlen of the prefix to allocate, defaults to defaultchildprefixlength of super prefix"` } // AddressFamily identifies IPv4/IPv6 @@ -73,6 +74,17 @@ func ToAddressFamily(af string) AddressFamily { return IPv4AddressFamily } +// FromAddressFamily will convert from a metal.AddressFamily to a AddressFamily +func FromAddressFamily(af metal.AddressFamily) AddressFamily { + switch af { + case metal.IPv4AddressFamily: + return IPv4AddressFamily + case metal.IPv6AddressFamily: + return IPv6AddressFamily + } + return IPv4AddressFamily +} + // NetworkFindRequest is used to find a Network with different criteria. type NetworkFindRequest struct { datastore.NetworkSearchQuery @@ -128,14 +140,15 @@ func NewNetworkResponse(network *metal.Network, usage *metal.NetworkUsage) *Netw Shared: &network.Shared, }, NetworkImmutable: NetworkImmutable{ - Prefixes: network.Prefixes.String(), - DestinationPrefixes: network.DestinationPrefixes.String(), - ChildPrefixLength: network.ChildPrefixLength, - Nat: network.Nat, - PrivateSuper: network.PrivateSuper, - Underlay: network.Underlay, - Vrf: &network.Vrf, - ParentNetworkID: parentNetworkID, + Prefixes: network.Prefixes.String(), + DestinationPrefixes: network.DestinationPrefixes.String(), + DefaultChildPrefixLength: network.DefaultChildPrefixLength, + Nat: network.Nat, + PrivateSuper: network.PrivateSuper, + Underlay: network.Underlay, + Vrf: &network.Vrf, + ParentNetworkID: parentNetworkID, + AddressFamily: ToAddressFamily(string(network.AddressFamily)), }, Usage: NetworkUsage{ AvailableIPs: usage.AvailableIPs, diff --git a/cmd/metal-api/internal/service/v1/size.go b/cmd/metal-api/internal/service/v1/size.go index b8a383fd..d37eed0c 100644 --- a/cmd/metal-api/internal/service/v1/size.go +++ b/cmd/metal-api/internal/service/v1/size.go @@ -16,7 +16,7 @@ type SizeReservation struct { Description string `json:"description,omitempty" description:"a description for this reservation"` ProjectID string `json:"projectid" description:"the project for which this size reservation is considered"` PartitionIDs []string `json:"partitionids" description:"the partitions in which this size reservation is considered, the amount is valid for every partition"` - Labels map[string]string `json:"labels" description:"free labels associated with this size reservation."` + Labels map[string]string `json:"labels,omitempty" description:"free labels associated with this size reservation."` } type SizeCreateRequest struct { @@ -50,7 +50,14 @@ type SizeReservationResponse struct { Reservations int `json:"reservations" description:"the amount of reservations of this size reservation"` UsedReservations int `json:"usedreservations" description:"the used amount of reservations of this size reservation"` ProjectAllocations int `json:"projectallocations" description:"the amount of allocations of this project referenced by this size reservation"` - Labels map[string]string `json:"labels" description:"free labels associated with this size reservation."` + Labels map[string]string `json:"labels,omitempty" description:"free labels associated with this size reservation."` +} + +type SizeReservationListRequest struct { + SizeID *string `json:"sizeid,omitempty" description:"the size id of this size reservation"` + Tenant *string `json:"tenant,omitempty" description:"the tenant of this size reservation"` + ProjectID *string `json:"projectid,omitempty" description:"the project id of this size reservation"` + PartitionID *string `json:"partitionid,omitempty" description:"the partition id of this size reservation"` } type SizeSuggestRequest struct { diff --git a/cmd/metal-api/internal/testdata/ipam.go b/cmd/metal-api/internal/testdata/ipam.go index 5b801f52..ca782319 100644 --- a/cmd/metal-api/internal/testdata/ipam.go +++ b/cmd/metal-api/internal/testdata/ipam.go @@ -23,7 +23,7 @@ func InitMockIpamData(dbMock *r.Mock, withIP bool) (ipam.IPAMer, error) { return nil, fmt.Errorf("error creating ipam mock data: %w", err) } } - for _, prefix := range []metal.Prefix{prefix1, prefix2, prefix3} { + for _, prefix := range []metal.Prefix{prefix1, prefix2, prefix3, superPrefix, superPrefixV6} { err := ipamer.CreatePrefix(ctx, prefix) if err != nil { return nil, fmt.Errorf("error creating ipam mock data: %w", err) diff --git a/cmd/metal-api/internal/testdata/testdata.go b/cmd/metal-api/internal/testdata/testdata.go index 6548e05e..f3ea6c97 100644 --- a/cmd/metal-api/internal/testdata/testdata.go +++ b/cmd/metal-api/internal/testdata/testdata.go @@ -4,6 +4,7 @@ import ( "errors" "time" + "github.com/metal-stack/metal-lib/pkg/pointer" "github.com/metal-stack/metal-lib/pkg/tag" "github.com/metal-stack/metal-api/cmd/metal-api/internal/metal" @@ -279,12 +280,14 @@ var ( URL: "http://images.metal-stack.io/metal-os/master/ubuntu/20.04/20200730/img.tar.lz4", } // Networks - prefix1 = metal.Prefix{IP: "185.1.2.0", Length: "26"} - prefix2 = metal.Prefix{IP: "100.64.0.0", Length: "16"} - prefix3 = metal.Prefix{IP: "192.0.0.0", Length: "16"} - prefixIPAM = metal.Prefix{IP: "10.0.0.0", Length: "16"} - cpl1 = uint8(28) - cpl2 = uint8(22) + prefix1 = metal.Prefix{IP: "185.1.2.0", Length: "26"} + prefix2 = metal.Prefix{IP: "100.64.0.0", Length: "16"} + prefix3 = metal.Prefix{IP: "192.0.0.0", Length: "16"} + prefixIPAM = metal.Prefix{IP: "10.0.0.0", Length: "16"} + superPrefix = metal.Prefix{IP: "10.1.0.0", Length: "16"} + superPrefixV6 = metal.Prefix{IP: "2001::", Length: "48"} + cpl1 = uint8(28) + cpl2 = uint8(22) prefixes1 = []metal.Prefix{prefix1, prefix2} prefixes2 = []metal.Prefix{prefix2} @@ -297,10 +300,11 @@ var ( Name: "Network 1", Description: "description 1", }, - PartitionID: Partition1.ID, - Prefixes: prefixes1, - PrivateSuper: true, - ChildPrefixLength: &cpl1, + PartitionID: Partition1.ID, + Prefixes: prefixes1, + PrivateSuper: true, + DefaultChildPrefixLength: &cpl1, + AddressFamily: metal.IPv4AddressFamily, } Nw2 = metal.Network{ Base: metal.Base{ @@ -308,10 +312,11 @@ var ( Name: "Network 2", Description: "description 2", }, - PartitionID: Partition1.ID, - Prefixes: prefixes2, - Underlay: true, - ChildPrefixLength: &cpl2, + PartitionID: Partition1.ID, + Prefixes: prefixes2, + Underlay: true, + DefaultChildPrefixLength: &cpl2, + AddressFamily: metal.IPv4AddressFamily, } Nw3 = metal.Network{ Base: metal.Base{ @@ -322,32 +327,51 @@ var ( Prefixes: prefixes3, PartitionID: Partition1.ID, ParentNetworkID: Nw1.ID, + AddressFamily: metal.IPv4AddressFamily, } Partition1PrivateSuperNetwork = metal.Network{ Base: metal.Base{ ID: "super-tenant-network-1", }, - Prefixes: metal.Prefixes{{IP: "10.0.0.0", Length: "16"}}, - PartitionID: Partition1.ID, - ParentNetworkID: "", - ProjectID: "", - PrivateSuper: true, - Nat: false, - Underlay: false, + Prefixes: metal.Prefixes{superPrefix}, + PartitionID: Partition1.ID, + DefaultChildPrefixLength: pointer.Pointer(uint8(22)), + ParentNetworkID: "", + ProjectID: "", + PrivateSuper: true, + Nat: false, + Underlay: false, } Partition2PrivateSuperNetwork = metal.Network{ Base: metal.Base{ ID: "super-tenant-network-2", }, - Prefixes: metal.Prefixes{{IP: "10.3.0.0", Length: "16"}}, - PartitionID: Partition2.ID, - ParentNetworkID: "", - ProjectID: "", - PrivateSuper: true, - Nat: false, - Underlay: false, + Prefixes: metal.Prefixes{superPrefix}, + PartitionID: Partition2.ID, + DefaultChildPrefixLength: pointer.Pointer(uint8(22)), + AddressFamily: metal.IPv4AddressFamily, + ParentNetworkID: "", + ProjectID: "", + PrivateSuper: true, + Nat: false, + Underlay: false, + } + + Partition2PrivateSuperNetworkV6 = metal.Network{ + Base: metal.Base{ + ID: "super-tenant-network-2-v6", + }, + Prefixes: metal.Prefixes{superPrefixV6}, + PartitionID: Partition2.ID, + DefaultChildPrefixLength: pointer.Pointer(uint8(64)), + AddressFamily: metal.IPv6AddressFamily, + ParentNetworkID: "", + ProjectID: "", + PrivateSuper: true, + Nat: false, + Underlay: false, } Partition1UnderlayNetwork = metal.Network{ @@ -841,7 +865,22 @@ func InitMockDBData(mock *r.Mock) { mock.On(r.DB("mockdb").Table("network").Get("404")).Return(nil, errors.New("Test Error")) mock.On(r.DB("mockdb").Table("network").Get("999")).Return(nil, nil) - mock.On(r.DB("mockdb").Table("network").Filter(func(var_3 r.Term) r.Term { return var_3.Field("partitionid").Eq("1") }).Filter(func(var_4 r.Term) r.Term { return var_4.Field("privatesuper").Eq(true) })).Return(Nw3, nil) + mock.On(r.DB("mockdb").Table("network").Filter( + func(var_3 r.Term) r.Term { return var_3.Field("partitionid").Eq("1") }).Filter( + func(var_4 r.Term) r.Term { return var_4.Field("privatesuper").Eq(true) }).Filter( + func(var_5 r.Term) r.Term { return var_5.Field("addressfamily").Eq("IPv4") })).Return(Nw3, nil) + mock.On(r.DB("mockdb").Table("network").Filter( + func(var_3 r.Term) r.Term { return var_3.Field("partitionid").Eq("1") }).Filter( + func(var_4 r.Term) r.Term { return var_4.Field("privatesuper").Eq(true) }).Filter( + func(var_5 r.Term) r.Term { return var_5.Field("addressfamily").Eq("IPv6") })).Return(nil, metal.NotFound("network not found")) + mock.On(r.DB("mockdb").Table("network").Filter( + func(var_3 r.Term) r.Term { return var_3.Field("partitionid").Eq("2") }).Filter( + func(var_4 r.Term) r.Term { return var_4.Field("privatesuper").Eq(true) }).Filter( + func(var_5 r.Term) r.Term { return var_5.Field("addressfamily").Eq("IPv4") })).Return(Partition2PrivateSuperNetwork, nil) + mock.On(r.DB("mockdb").Table("network").Filter( + func(var_3 r.Term) r.Term { return var_3.Field("partitionid").Eq("2") }).Filter( + func(var_4 r.Term) r.Term { return var_4.Field("privatesuper").Eq(true) }).Filter( + func(var_5 r.Term) r.Term { return var_5.Field("addressfamily").Eq("IPv6") })).Return(Partition2PrivateSuperNetworkV6, nil) mock.On(r.DB("mockdb").Table("ip").Get("1.2.3.4")).Return(IP1, nil) mock.On(r.DB("mockdb").Table("ip").Get("2.3.4.5")).Return(IP2, nil) diff --git a/spec/metal-api.json b/spec/metal-api.json index 5efe1b75..caa8a9b3 100644 --- a/spec/metal-api.json +++ b/spec/metal-api.json @@ -255,6 +255,9 @@ }, "datastore.NetworkSearchQuery": { "properties": { + "addressfamily": { + "type": "string" + }, "destinationprefixes": { "items": { "type": "string" @@ -3591,7 +3594,7 @@ "type": "object" }, "length": { - "description": "the bitlen of the prefix to allocate, defaults to childprefixlength of super prefix", + "description": "the bitlen of the prefix to allocate, defaults to defaultchildprefixlength of super prefix", "format": "byte", "type": "integer" }, @@ -3646,7 +3649,15 @@ }, "v1.NetworkCreateRequest": { "properties": { - "childprefixlength": { + "addressfamily": { + "description": "the addressfamily either IPv4 or IPv6 of this network", + "enum": [ + "IPv4", + "IPv6" + ], + "type": "string" + }, + "defaultchildprefixlength": { "description": "if privatesuper, this defines the bitlen of child prefixes if not nil", "format": "byte", "type": "integer" @@ -3723,6 +3734,7 @@ } }, "required": [ + "addressfamily", "destinationprefixes", "id", "nat", @@ -3733,6 +3745,9 @@ }, "v1.NetworkFindRequest": { "properties": { + "addressfamily": { + "type": "string" + }, "destinationprefixes": { "items": { "type": "string" @@ -3784,7 +3799,15 @@ "v1.NetworkImmutable": { "description": "a network which contains prefixes from which IP addresses can be allocated\nprefixes that are reachable within this network", "properties": { - "childprefixlength": { + "addressfamily": { + "description": "the addressfamily either IPv4 or IPv6 of this network", + "enum": [ + "IPv4", + "IPv6" + ], + "type": "string" + }, + "defaultchildprefixlength": { "description": "if privatesuper, this defines the bitlen of child prefixes if not nil", "format": "byte", "type": "integer" @@ -3830,6 +3853,7 @@ } }, "required": [ + "addressfamily", "destinationprefixes", "nat", "prefixes", @@ -3839,23 +3863,31 @@ }, "v1.NetworkResponse": { "properties": { + "addressfamily": { + "description": "the addressfamily either IPv4 or IPv6 of this network", + "enum": [ + "IPv4", + "IPv6" + ], + "type": "string" + }, "changed": { "description": "the last changed timestamp of this entity", "format": "date-time", "readOnly": true, "type": "string" }, - "childprefixlength": { - "description": "if privatesuper, this defines the bitlen of child prefixes if not nil", - "format": "byte", - "type": "integer" - }, "created": { "description": "the creation time of this entity", "format": "date-time", "readOnly": true, "type": "string" }, + "defaultchildprefixlength": { + "description": "if privatesuper, this defines the bitlen of child prefixes if not nil", + "format": "byte", + "type": "integer" + }, "description": { "description": "a description for this entity", "type": "string" @@ -3932,6 +3964,7 @@ } }, "required": [ + "addressfamily", "destinationprefixes", "id", "nat", @@ -4668,11 +4701,30 @@ }, "required": [ "amount", - "labels", "partitionids", "projectid" ] }, + "v1.SizeReservationListRequest": { + "properties": { + "partitionid": { + "description": "the partition id of this size reservation", + "type": "string" + }, + "projectid": { + "description": "the project id of this size reservation", + "type": "string" + }, + "sizeid": { + "description": "the size id of this size reservation", + "type": "string" + }, + "tenant": { + "description": "the tenant of this size reservation", + "type": "string" + } + } + }, "v1.SizeReservationResponse": { "properties": { "labels": { @@ -4719,7 +4771,6 @@ } }, "required": [ - "labels", "partitionid", "projectallocations", "projectid", @@ -9053,7 +9104,7 @@ "name": "body", "required": true, "schema": { - "$ref": "#/definitions/v1.EmptyBody" + "$ref": "#/definitions/v1.SizeReservationListRequest" } } ],