diff --git a/cmd/metal-api/internal/datastore/migrations/06_childprefixlength.go b/cmd/metal-api/internal/datastore/migrations/06_childprefixlength.go new file mode 100644 index 00000000..b45ab386 --- /dev/null +++ b/cmd/metal-api/internal/datastore/migrations/06_childprefixlength.go @@ -0,0 +1,61 @@ +package migrations + +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() { + type tmpPartition struct { + PrivateNetworkPrefixLength uint8 `rethinkdb:"privatenetworkprefixlength"` + } + datastore.MustRegisterMigration(datastore.Migration{ + Name: "migrate partition.childprefixlength to tenant super network", + Version: 6, + Up: func(db *r.Term, session r.QueryExecutor, rs *datastore.RethinkStore) error { + nws, err := rs.ListNetworks() + if err != nil { + return err + } + + for _, old := range nws { + cursor, err := db.Table("partition").Get(old.PartitionID).Run(session) + if err != nil { + return err + } + var partition tmpPartition + err = cursor.One(&partition) + if err != nil { + return err + } + + // TODO: does not work somehow + new := old + + 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 + } + err = cursor.Close() + if err != nil { + return err + } + } + + _, err = db.Table("partition").Replace(r.Row.Without("privatenetworkprefixlength")).RunWrite(session) + 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 c42b553a..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 @@ -5,6 +5,7 @@ package migrations_integration import ( "context" + "fmt" "log/slog" "os" "time" @@ -15,6 +16,7 @@ import ( _ "github.com/metal-stack/metal-api/cmd/metal-api/internal/datastore/migrations" "github.com/metal-stack/metal-api/cmd/metal-api/internal/metal" "github.com/metal-stack/metal-api/test" + r "gopkg.in/rethinkdb/rethinkdb-go.v6" "testing" @@ -22,7 +24,7 @@ import ( "github.com/stretchr/testify/require" ) -func Test_Migration(t *testing.T) { +func Test_MigrationProvisioningEventContainer(t *testing.T) { container, c, err := test.StartRethink(t) require.NoError(t, err) defer func() { @@ -125,3 +127,110 @@ func Test_Migration(t *testing.T) { assert.Equal(t, ec.Events[0].Time.Unix(), lastEventTime.Unix()) assert.Equal(t, ec.Events[1].Time.Unix(), now.Unix()) } + +func Test_MigrationChildPrefixLength(t *testing.T) { + type tmpPartition struct { + ID string `rethinkdb:"id"` + PrivateNetworkPrefixLength uint8 `rethinkdb:"privatenetworkprefixlength"` + } + + container, c, err := test.StartRethink(t) + require.NoError(t, err) + defer func() { + _ = container.Terminate(context.Background()) + }() + + log := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug})) + + rs := datastore.New(log, c.IP+":"+c.Port, c.DB, c.User, c.Password) + // limit poolsize to speed up initialization + rs.VRFPoolRangeMin = 10000 + rs.VRFPoolRangeMax = 10010 + rs.ASNPoolRangeMin = 10000 + rs.ASNPoolRangeMax = 10010 + + err = rs.Connect() + require.NoError(t, err) + err = rs.Initialize() + require.NoError(t, err) + + var ( + p1 = &tmpPartition{ + ID: "p1", + PrivateNetworkPrefixLength: 22, + } + p2 = &tmpPartition{ + ID: "p2", + PrivateNetworkPrefixLength: 24, + } + n1 = &metal.Network{ + Base: metal.Base{ + ID: "n1", + }, + 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, + } + n3 = &metal.Network{ + Base: metal.Base{ + ID: "n3", + }, + Prefixes: metal.Prefixes{ + {IP: "100.1.0.0", Length: "22"}, + }, + PartitionID: "p2", + PrivateSuper: false, + } + ) + _, err = r.DB("metal").Table("partition").Insert(p1).RunWrite(rs.Session()) + require.NoError(t, err) + _, err = r.DB("metal").Table("partition").Insert(p2).RunWrite(rs.Session()) + require.NoError(t, err) + + err = rs.CreateNetwork(n1) + require.NoError(t, err) + err = rs.CreateNetwork(n2) + require.NoError(t, err) + err = rs.CreateNetwork(n3) + require.NoError(t, err) + + err = rs.Migrate(nil, false) + require.NoError(t, err) + + p, err := rs.FindPartition(p1.ID) + require.NoError(t, err) + require.NotNil(t, p) + p, err = rs.FindPartition(p2.ID) + require.NoError(t, err) + require.NotNil(t, p) + + n1fetched, err := rs.FindNetworkByID(n1.ID) + require.NoError(t, err) + require.NotNil(t, n1fetched) + 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.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.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 38cbc07c..8167ede1 100644 --- a/cmd/metal-api/internal/datastore/rethinkdb.go +++ b/cmd/metal-api/internal/datastore/rethinkdb.go @@ -78,6 +78,11 @@ func New(log *slog.Logger, dbhost string, dbname string, dbuser string, dbpass s } } +// Session exported for migration unit test +func (rs *RethinkStore) Session() r.QueryExecutor { + return rs.session +} + func multi(session r.QueryExecutor, tt ...r.Term) error { for _, t := range tt { if err := t.Exec(session); err != nil { @@ -373,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/metal/network.go b/cmd/metal-api/internal/metal/network.go index 9f2477d7..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,17 +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"` - 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. @@ -323,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/metal/partition.go b/cmd/metal-api/internal/metal/partition.go index 2db7c2f8..09fb8795 100644 --- a/cmd/metal-api/internal/metal/partition.go +++ b/cmd/metal-api/internal/metal/partition.go @@ -3,10 +3,9 @@ package metal // A Partition represents a location. type Partition struct { Base - BootConfiguration BootConfiguration `rethinkdb:"bootconfig" json:"bootconfig"` - MgmtServiceAddress string `rethinkdb:"mgmtserviceaddr" json:"mgmtserviceaddr"` - PrivateNetworkPrefixLength uint8 `rethinkdb:"privatenetworkprefixlength" json:"privatenetworkprefixlength"` - Labels map[string]string `rethinkdb:"labels" json:"labels"` + BootConfiguration BootConfiguration `rethinkdb:"bootconfig" json:"bootconfig"` + MgmtServiceAddress string `rethinkdb:"mgmtserviceaddr" json:"mgmtserviceaddr"` + Labels map[string]string `rethinkdb:"labels" json:"labels"` } // BootConfiguration defines the metal-hammer initrd, kernel and commandline diff --git a/cmd/metal-api/internal/service/integration_test.go b/cmd/metal-api/internal/service/integration_test.go index 10a1f16c..315030e2 100644 --- a/cmd/metal-api/internal/service/integration_test.go +++ b/cmd/metal-api/internal/service/integration_test.go @@ -23,6 +23,7 @@ import ( metalgrpc "github.com/metal-stack/metal-api/cmd/metal-api/internal/grpc" "github.com/metal-stack/metal-api/test" "github.com/metal-stack/metal-lib/bus" + "github.com/metal-stack/metal-lib/pkg/pointer" "github.com/metal-stack/security" mdmv1 "github.com/metal-stack/masterdata-api/api/v1" @@ -294,14 +295,18 @@ func createTestEnvironment(t *testing.T) testEnv { PartitionID: &partition.ID, }, NetworkImmutable: v1.NetworkImmutable{ - Prefixes: []string{testPrivateSuperCidr}, - PrivateSuper: true, + 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 @@ -318,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/ip-service_test.go b/cmd/metal-api/internal/service/ip-service_test.go index c473ba5f..72889fd5 100644 --- a/cmd/metal-api/internal/service/ip-service_test.go +++ b/cmd/metal-api/internal/service/ip-service_test.go @@ -52,13 +52,15 @@ func TestGetIPs(t *testing.T) { err = json.NewDecoder(resp.Body).Decode(&result) require.NoError(t, err) - require.Len(t, result, 3) + require.Len(t, result, 4) require.Equal(t, testdata.IP1.IPAddress, result[0].IPAddress) require.Equal(t, testdata.IP1.Name, *result[0].Name) require.Equal(t, testdata.IP2.IPAddress, result[1].IPAddress) require.Equal(t, testdata.IP2.Name, *result[1].Name) require.Equal(t, testdata.IP3.IPAddress, result[2].IPAddress) require.Equal(t, testdata.IP3.Name, *result[2].Name) + require.Equal(t, testdata.IP4.IPAddress, result[3].IPAddress) + require.Equal(t, testdata.IP4.Name, *result[3].Name) } func TestGetIP(t *testing.T) { @@ -85,6 +87,31 @@ func TestGetIP(t *testing.T) { require.Equal(t, testdata.IP1.Name, *result.Name) } +func TestGetIPv6(t *testing.T) { + ds, mock := datastore.InitMockDB(t) + testdata.InitMockDBData(mock) + + logger := slog.Default() + ipservice, err := NewIP(logger, ds, bus.DirectEndpoints(), ipam.InitTestIpam(t), nil) + require.NoError(t, err) + container := restful.NewContainer().Add(ipservice) + req := httptest.NewRequest("GET", "/v1/ip/2001:0db8:85a3::1", nil) + container = injectViewer(logger, 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.IPResponse + err = json.NewDecoder(resp.Body).Decode(&result) + + require.NoError(t, err) + require.Equal(t, testdata.IP4.IPAddress, result.IPAddress) + require.Equal(t, testdata.IP4.Name, *result.Name) +} + func TestGetIPNotFound(t *testing.T) { ds, mock := datastore.InitMockDB(t) testdata.InitMockDBData(mock) diff --git a/cmd/metal-api/internal/service/network-service.go b/cmd/metal-api/internal/service/network-service.go index bd4db1d6..8e0c4b41 100644 --- a/cmd/metal-api/internal/service/network-service.go +++ b/cmd/metal-api/internal/service/network-service.go @@ -6,6 +6,7 @@ import ( "fmt" "log/slog" "net/http" + "net/netip" mdmv1 "github.com/metal-stack/masterdata-api/api/v1" mdm "github.com/metal-stack/masterdata-api/pkg/client" @@ -19,6 +20,7 @@ import ( 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/httperrors" + "github.com/metal-stack/metal-lib/pkg/pointer" ) type networkResource struct { @@ -205,6 +207,8 @@ func (r *networkResource) findNetworks(request *restful.Request, response *restf r.send(request, response, http.StatusOK, result) } +// 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) @@ -259,17 +263,22 @@ func (r *networkResource) createNetwork(request *restful.Request, response *rest return } - prefixes := metal.Prefixes{} - // all Prefixes must be valid - for i := range requestPayload.Prefixes { - p := requestPayload.Prefixes[i] - prefix, err := metal.NewPrefixFromCIDR(p) - 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 - } + // all Prefixes must be valid and from the same addressfamily + prefixes, addressFamily, err := validatePrefixes(requestPayload.Prefixes) + if err != nil { + r.sendError(request, response, httperrors.BadRequest(err)) + return + } + // all DestinationPrefixes must be valid and from the same addressfamily + _, _, err = validatePrefixes(requestPayload.DestinationPrefixes) + if err != nil { + r.sendError(request, response, httperrors.BadRequest(err)) + return + } - prefixes = append(prefixes, *prefix) + 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{} @@ -324,16 +333,22 @@ func (r *networkResource) createNetwork(request *restful.Request, response *rest return } + // We should support two private super per partition, one per addressfamily + // the network allocate request must be configurable with addressfamily if privateSuper { - boolTrue := true - err := r.ds.FindNetwork(&datastore.NetworkSearchQuery{PartitionID: &partition.ID, PrivateSuper: &boolTrue}, &metal.Network{}) - if err != nil { - if !metal.IsNotFound(err) { - r.sendError(request, response, defaultError(err)) - return - } - } else { - r.sendError(request, response, defaultError(fmt.Errorf("partition with id %q already has a private super network", partition.ID))) + var nw metal.Network + 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 } } @@ -387,6 +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.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 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.DefaultChildPrefixLength = requestPayload.DefaultChildPrefixLength } ctx := request.Request.Context() @@ -409,6 +442,38 @@ func (r *networkResource) createNetwork(request *restful.Request, response *rest r.send(request, response, http.StatusCreated, v1.NewNetworkResponse(nw, usage)) } +func validatePrefixes(prefixes []string) (metal.Prefixes, *v1.AddressFamily, error) { + var ( + result metal.Prefixes + addressFamilies = make(map[string]bool) + addressFamily v1.AddressFamily + ) + for _, p := range prefixes { + prefix, err := metal.NewPrefixFromCIDR(p) + if err != nil { + 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, nil, fmt.Errorf("given prefix %v is not a valid ip with mask: %w", p, err) + } + if ipprefix.Addr().Is4() { + addressFamilies["ipv4"] = true + addressFamily = v1.IPv4AddressFamily + } + if ipprefix.Addr().Is6() { + addressFamilies["ipv6"] = true + addressFamily = v1.IPv6AddressFamily + } + result = append(result, *prefix) + } + if len(addressFamilies) > 1 { + return nil, nil, fmt.Errorf("given prefixes have different addressfamilies") + } + return result, &addressFamily, nil +} + +// TODO add possibility to allocate from a non super network if given in the AllocateRequest and super has childprefixlength func (r *networkResource) allocateNetwork(request *restful.Request, response *restful.Response) { var requestPayload v1.NetworkAllocateRequest err := request.ReadEntity(&requestPayload) @@ -463,14 +528,6 @@ func (r *networkResource) allocateNetwork(request *restful.Request, response *re return } - var superNetwork metal.Network - boolTrue := true - err = r.ds.FindNetwork(&datastore.NetworkSearchQuery{PartitionID: &partition.ID, PrivateSuper: &boolTrue}, &superNetwork) - if err != nil { - r.sendError(request, response, defaultError(err)) - return - } - destPrefixes := metal.Prefixes{} for _, p := range requestPayload.DestinationPrefixes { prefix, err := metal.NewPrefixFromCIDR(p) @@ -482,6 +539,36 @@ func (r *networkResource) allocateNetwork(request *restful.Request, response *re destPrefixes = append(destPrefixes, *prefix) } + addressFamily := v1.IPv4AddressFamily + if requestPayload.AddressFamily != nil { + addressFamily = v1.ToAddressFamily(*requestPayload.AddressFamily) + } + + r.log.Info("network allocate", "family", addressFamily, "partition", partition.ID) + var ( + superNetwork metal.Network + ) + + 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 + } + + 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{ Base: metal.Base{ Name: name, @@ -493,9 +580,19 @@ func (r *networkResource) allocateNetwork(request *restful.Request, response *re DestinationPrefixes: destPrefixes, Shared: shared, Nat: nat, + AddressFamily: metal.AddressFamily(addressFamily), + } + + // Allow configurable prefix length + 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.ID, "defaultchildprefixlength", *superNetwork.DefaultChildPrefixLength, "length", length) + ctx := request.Request.Context() - nw, err := createChildNetwork(ctx, r.ds, r.ipamer, nwSpec, &superNetwork, partition.PrivateNetworkPrefixLength) + nw, err := createChildNetwork(ctx, r.ds, r.ipamer, nwSpec, &superNetwork, length) if err != nil { r.sendError(request, response, defaultError(err)) return @@ -616,16 +713,26 @@ func (r *networkResource) updateNetwork(request *restful.Request, response *rest return } - var prefixesToBeRemoved metal.Prefixes - var prefixesToBeAdded metal.Prefixes + var ( + prefixesToBeRemoved metal.Prefixes + prefixesToBeAdded metal.Prefixes + ) if len(requestPayload.Prefixes) > 0 { - newNetwork.Prefixes, err = prefixesFromCidr(requestPayload.Prefixes) + // all Prefixes must be valid and from the same addressfamily + prefixes, af, err := validatePrefixes(requestPayload.Prefixes) if err != nil { - r.sendError(request, response, defaultError(err)) + r.sendError(request, response, httperrors.BadRequest(err)) return } + 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 + } + + newNetwork.Prefixes = prefixes + prefixesToBeRemoved = oldNetwork.SubtractPrefixes(newNetwork.Prefixes...) // now validate if there are ips which have a prefix to be removed as a parent @@ -663,11 +770,19 @@ func (r *networkResource) updateNetwork(request *restful.Request, response *rest } if len(requestPayload.DestinationPrefixes) > 0 { - newNetwork.DestinationPrefixes, err = prefixesFromCidr(requestPayload.DestinationPrefixes) + // all Prefixes must be valid and from the same addressfamily + prefixes, af, err := validatePrefixes(requestPayload.Prefixes) if err != nil { - r.sendError(request, response, defaultError(err)) + r.sendError(request, response, httperrors.BadRequest(err)) return } + + 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 + } + + newNetwork.DestinationPrefixes = prefixes } err = r.ds.UpdateNetwork(oldNetwork, &newNetwork) @@ -681,18 +796,6 @@ func (r *networkResource) updateNetwork(request *restful.Request, response *rest r.send(request, response, http.StatusOK, v1.NewNetworkResponse(&newNetwork, usage)) } -func prefixesFromCidr(PrefixesCidr []string) (metal.Prefixes, error) { - var prefixes metal.Prefixes - for _, prefixCidr := range PrefixesCidr { - Prefix, err := metal.NewPrefixFromCIDR(prefixCidr) - if err != nil { - return nil, err - } - prefixes = append(prefixes, *Prefix) - } - return prefixes, nil -} - func (r *networkResource) deleteNetwork(request *restful.Request, response *restful.Response) { id := request.PathParameter("id") diff --git a/cmd/metal-api/internal/service/network-service_test.go b/cmd/metal-api/internal/service/network-service_test.go index 4e54993a..ed400b38 100644 --- a/cmd/metal-api/internal/service/network-service_test.go +++ b/cmd/metal-api/internal/service/network-service_test.go @@ -7,20 +7,24 @@ import ( "log/slog" "net/http" "net/http/httptest" + "net/netip" + "reflect" "testing" - "github.com/metal-stack/metal-lib/httperrors" - r "gopkg.in/rethinkdb/rethinkdb-go.v6" - + restful "github.com/emicklei/go-restful/v3" + mdmv1 "github.com/metal-stack/masterdata-api/api/v1" + mdmv1mock "github.com/metal-stack/masterdata-api/api/v1/mocks" + mdm "github.com/metal-stack/masterdata-api/pkg/client" "github.com/metal-stack/metal-api/cmd/metal-api/internal/datastore" "github.com/metal-stack/metal-api/cmd/metal-api/internal/ipam" - "github.com/metal-stack/metal-api/cmd/metal-api/internal/metal" v1 "github.com/metal-stack/metal-api/cmd/metal-api/internal/service/v1" - - restful "github.com/emicklei/go-restful/v3" "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" + testifymock "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" + r "gopkg.in/rethinkdb/rethinkdb-go.v6" ) func TestGetNetworks(t *testing.T) { @@ -252,3 +256,350 @@ func TestSearchNetwork(t *testing.T) { require.Equal(t, testdata.Nw1.PartitionID, *result.PartitionID) require.Equal(t, testdata.Nw1.Name, *result.Name) } + +func Test_networkResource_createNetwork(t *testing.T) { + log := slog.Default() + tests := []struct { + name string + networkName string + networkID string + partitionID string + projectID string + prefixes []string + destinationPrefixes []string + vrf uint + childprefixlength *uint8 + privateSuper bool + underlay bool + nat bool + expectedStatus int + expectedErrorMessage string + }{ + { + name: "simple IPv4", + 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"}, + vrf: uint(10000), + expectedStatus: http.StatusCreated, + }, + { + name: "privatesuper IPv4", + 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"}, + 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 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", + networkName: testdata.Nw1.Name, + partitionID: testdata.Nw1.PartitionID, + 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, + }, + { + name: "broken IPv4", + networkName: testdata.Nw1.Name, + partitionID: testdata.Nw1.PartitionID, + 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, + expectedErrorMessage: "given prefix 192.168.265.0/24 is not a valid ip with mask: netip.ParsePrefix(\"192.168.265.0/24\"): ParseAddr(\"192.168.265.0\"): IPv4 field has value >255", + }, + { + name: "broken IPv6", + networkName: testdata.Nw1.Name, + partitionID: testdata.Nw1.PartitionID, + projectID: testdata.Nw1.ProjectID, + prefixes: []string{"fdaa:::/50"}, + destinationPrefixes: []string{"::/0"}, + privateSuper: true, + vrf: uint(10000), + expectedStatus: http.StatusBadRequest, + expectedErrorMessage: "given prefix fdaa:::/50 is not a valid ip with mask: netip.ParsePrefix(\"fdaa:::/50\"): ParseAddr(\"fdaa:::\"): each colon-separated field must have at least one digit (at \":\")", + }, + { + name: "mixed prefix addressfamilies", + networkName: testdata.Nw1.Name, + partitionID: testdata.Nw1.PartitionID, + projectID: testdata.Nw1.ProjectID, + prefixes: []string{"172.0.0.0/24", "fdaa:bbcc::/50"}, + destinationPrefixes: []string{"0.0.0.0/0"}, + vrf: uint(10000), + expectedStatus: http.StatusBadRequest, + expectedErrorMessage: "given prefixes have different addressfamilies", + }, + { + name: "broken destinationprefix", + 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/33"}, + vrf: uint(10000), + expectedStatus: http.StatusBadRequest, + expectedErrorMessage: "given prefix 0.0.0.0/33 is not a valid ip with mask: netip.ParsePrefix(\"0.0.0.0/33\"): prefix length out of range", + }, + { + name: "broken childprefixlength", + networkName: testdata.Nw1.Name, + partitionID: testdata.Nw1.PartitionID, + projectID: testdata.Nw1.ProjectID, + prefixes: []string{"fdaa:bbcc::/50"}, + childprefixlength: pointer.Pointer(uint8(50)), + privateSuper: true, + vrf: uint(10000), + expectedStatus: http.StatusBadRequest, + expectedErrorMessage: "given defaultchildprefixlength 50 is not greater than prefix length of:fdaa:bbcc::/50", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ds, mock := datastore.InitMockDB(t) + ipamer, err := testdata.InitMockIpamData(mock, false) + require.NoError(t, err) + testdata.InitMockDBData(mock) + + networkservice := NewNetwork(log, ds, ipamer, nil) + container := restful.NewContainer().Add(networkservice) + + createRequest := &v1.NetworkCreateRequest{ + Describable: v1.Describable{Name: &tt.networkName}, + NetworkBase: v1.NetworkBase{PartitionID: &tt.partitionID, ProjectID: &tt.projectID}, + NetworkImmutable: v1.NetworkImmutable{ + Prefixes: tt.prefixes, + DestinationPrefixes: tt.destinationPrefixes, + Vrf: &tt.vrf, Nat: tt.nat, PrivateSuper: tt.privateSuper, Underlay: tt.underlay, + }, + } + if tt.childprefixlength != nil { + createRequest.DefaultChildPrefixLength = tt.childprefixlength + } + js, _ := json.Marshal(createRequest) + body := bytes.NewBuffer(js) + req := httptest.NewRequest("PUT", "/v1/network", body) + 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, tt.expectedStatus, resp.StatusCode, w.Body.String()) + if tt.expectedStatus > 300 { + var result httperrors.HTTPErrorResponse + err := json.NewDecoder(resp.Body).Decode(&result) + + require.NoError(t, err) + require.Equal(t, tt.expectedErrorMessage, result.Message) + } else { + var result v1.NetworkResponse + err = json.NewDecoder(resp.Body).Decode(&result) + 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, tt.destinationPrefixes, result.DestinationPrefixes) + if tt.childprefixlength != nil { + require.Equal(t, tt.childprefixlength, result.DefaultChildPrefixLength) + } + } + }) + } +} + +func Test_networkResource_allocateNetwork(t *testing.T) { + log := slog.Default() + tests := []struct { + name string + networkName string + partitionID string + projectID string + childprefixlength *uint8 + addressFamily *string + shared bool + expectedStatus int + expectedErrorMessage string + }{ + { + name: "simple ipv4, default childprefixlength", + networkName: "tenantv4", + partitionID: testdata.Partition2.ID, + projectID: "project-1", + expectedStatus: http.StatusCreated, + }, + { + name: "simple ipv4, specific childprefixlength", + networkName: "tenantv4.2", + partitionID: testdata.Partition2.ID, + projectID: "project-1", + childprefixlength: pointer.Pointer(uint8(29)), + expectedStatus: http.StatusCreated, + }, + { + 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) + 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) + + ipamer, err := testdata.InitMockIpamData(mock, false) + require.NoError(t, err) + testdata.InitMockDBData(mock) + + psc := mdmv1mock.ProjectServiceClient{} + psc.On("Get", testifymock.Anything, &mdmv1.ProjectGetRequest{Id: "project-1"}).Return(&mdmv1.ProjectResponse{ + Project: &mdmv1.Project{ + Meta: &mdmv1.Meta{Id: tt.projectID}, + }, + }, nil, + ) + tsc := mdmv1mock.TenantServiceClient{} + + mdc := mdm.NewMock(&psc, &tsc, nil, nil) + + networkservice := NewNetwork(log, ds, ipamer, mdc) + container := restful.NewContainer().Add(networkservice) + + allocateRequest := &v1.NetworkAllocateRequest{ + Describable: v1.Describable{Name: &tt.networkName}, + NetworkBase: v1.NetworkBase{PartitionID: &tt.partitionID, ProjectID: &tt.projectID}, + AddressFamily: tt.addressFamily, + Length: tt.childprefixlength, + } + + js, err := json.Marshal(allocateRequest) + require.NoError(t, err) + + body := bytes.NewBuffer(js) + req := httptest.NewRequest("POST", "/v1/network/allocate", body) + 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, tt.expectedStatus, resp.StatusCode, w.Body.String()) + if tt.expectedStatus > 300 { + var result httperrors.HTTPErrorResponse + err := json.NewDecoder(resp.Body).Decode(&result) + + require.NoError(t, err) + require.Equal(t, tt.expectedErrorMessage, result.Message) + } else { + var result v1.NetworkResponse + err = json.NewDecoder(resp.Body).Decode(&result) + + requestAF := "ipv4" + if tt.addressFamily != nil { + requestAF = "ipv6" + } + + require.GreaterOrEqual(t, len(result.Prefixes), 1) + resultFirstPrefix := netip.MustParsePrefix(result.Prefixes[0]) + af := "ipv4" + if resultFirstPrefix.Addr().Is6() { + af = "ipv6" + } + + 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) + } + } +} + +func Test_validatePrefixes(t *testing.T) { + tests := []struct { + name string + prefixes []string + wantPrefixes metal.Prefixes + wantAF *v1.AddressFamily + wantErr bool + }{ + { + 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: "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: "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, got1, err := validatePrefixes(tt.prefixes) + if (err != nil) != tt.wantErr { + t.Errorf("validatePrefixes() error = %v, wantErr %v", err, tt.wantErr) + return + } + 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/partition-service.go b/cmd/metal-api/internal/service/partition-service.go index 7d11615d..efc55ac9 100644 --- a/cmd/metal-api/internal/service/partition-service.go +++ b/cmd/metal-api/internal/service/partition-service.go @@ -170,14 +170,7 @@ func (r *partitionResource) createPartition(request *restful.Request, response * if requestPayload.Labels != nil { labels = requestPayload.Labels } - prefixLength := uint8(22) - if requestPayload.PrivateNetworkPrefixLength != nil { - prefixLength = uint8(*requestPayload.PrivateNetworkPrefixLength) - if prefixLength < 16 || prefixLength > 30 { - r.sendError(request, response, httperrors.BadRequest(errors.New("private network prefix length is out of range"))) - return - } - } + var imageURL string if requestPayload.PartitionBootConfiguration.ImageURL != nil { imageURL = *requestPayload.PartitionBootConfiguration.ImageURL @@ -211,9 +204,8 @@ func (r *partitionResource) createPartition(request *restful.Request, response * Name: name, Description: description, }, - Labels: labels, - MgmtServiceAddress: mgmtServiceAddress, - PrivateNetworkPrefixLength: prefixLength, + Labels: labels, + MgmtServiceAddress: mgmtServiceAddress, BootConfiguration: metal.BootConfiguration{ ImageURL: imageURL, KernelURL: kernelURL, 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 d1bb9cc7..c6ebb28b 100644 --- a/cmd/metal-api/internal/service/v1/network.go +++ b/cmd/metal-api/internal/service/v1/network.go @@ -15,14 +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"` - 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. @@ -47,6 +49,40 @@ type NetworkAllocateRequest struct { NetworkBase 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 defaultchildprefixlength of super prefix"` +} + +// 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 +} + +// 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. @@ -104,13 +140,15 @@ func NewNetworkResponse(network *metal.Network, usage *metal.NetworkUsage) *Netw Shared: &network.Shared, }, NetworkImmutable: NetworkImmutable{ - Prefixes: network.Prefixes.String(), - DestinationPrefixes: network.DestinationPrefixes.String(), - 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/partition.go b/cmd/metal-api/internal/service/v1/partition.go index beed5d2f..e4843779 100644 --- a/cmd/metal-api/internal/service/v1/partition.go +++ b/cmd/metal-api/internal/service/v1/partition.go @@ -5,9 +5,8 @@ import ( ) type PartitionBase struct { - MgmtServiceAddress *string `json:"mgmtserviceaddress" description:"the address to the management service of this partition" optional:"true"` - PrivateNetworkPrefixLength *int `json:"privatenetworkprefixlength" description:"the length of private networks for the machine's child networks in this partition, default 22" optional:"true" minimum:"16" maximum:"30"` - Labels map[string]string `json:"labels" description:"free labels that you associate with this partition" optional:"true"` + MgmtServiceAddress *string `json:"mgmtserviceaddress" description:"the address to the management service of this partition" optional:"true"` + Labels map[string]string `json:"labels" description:"free labels that you associate with this partition" optional:"true"` } type PartitionBootConfiguration struct { @@ -67,8 +66,6 @@ func NewPartitionResponse(p *metal.Partition) *PartitionResponse { return nil } - prefixLength := int(p.PrivateNetworkPrefixLength) - labels := map[string]string{} if p.Labels != nil { labels = p.Labels @@ -86,7 +83,6 @@ func NewPartitionResponse(p *metal.Partition) *PartitionResponse { }, PartitionBase: PartitionBase{ MgmtServiceAddress: &p.MgmtServiceAddress, - PrivateNetworkPrefixLength: &prefixLength, }, PartitionBootConfiguration: PartitionBootConfiguration{ ImageURL: &p.BootConfiguration.ImageURL, 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 273f5bfb..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,10 +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.2.0", Length: "16"} - prefix3 = metal.Prefix{IP: "192.0.0.0", Length: "16"} - prefixIPAM = metal.Prefix{IP: "10.0.0.0", Length: "16"} + 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} @@ -295,9 +300,11 @@ var ( Name: "Network 1", Description: "description 1", }, - PartitionID: Partition1.ID, - Prefixes: prefixes1, - PrivateSuper: true, + PartitionID: Partition1.ID, + Prefixes: prefixes1, + PrivateSuper: true, + DefaultChildPrefixLength: &cpl1, + AddressFamily: metal.IPv4AddressFamily, } Nw2 = metal.Network{ Base: metal.Base{ @@ -305,8 +312,11 @@ var ( Name: "Network 2", Description: "description 2", }, - Prefixes: prefixes2, - Underlay: true, + PartitionID: Partition1.ID, + Prefixes: prefixes2, + Underlay: true, + DefaultChildPrefixLength: &cpl2, + AddressFamily: metal.IPv4AddressFamily, } Nw3 = metal.Network{ Base: metal.Base{ @@ -317,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{ @@ -481,6 +510,13 @@ var ( Tags: []string{tag.MachineID}, ProjectID: "1", } + IP4 = metal.IP{ + IPAddress: "2001:0db8:85a3::1", + Name: "IPv6 4", + Description: "description 4", + Type: "ephemeral", + ProjectID: "1", + } IPAMIP = metal.IP{ Name: "IPAM IP", Description: "description IPAM", @@ -514,7 +550,6 @@ var ( Name: "partition1", Description: "description 1", }, - PrivateNetworkPrefixLength: 22, } Partition2 = metal.Partition{ Base: metal.Base{ @@ -522,7 +557,6 @@ var ( Name: "partition2", Description: "description 2", }, - PrivateNetworkPrefixLength: 22, } Partition3 = metal.Partition{ Base: metal.Base{ @@ -530,7 +564,6 @@ var ( Name: "partition3", Description: "description 3", }, - PrivateNetworkPrefixLength: 22, } // Switches @@ -718,7 +751,7 @@ var ( } // All IPs TestIPs = []metal.IP{ - IP1, IP2, IP3, + IP1, IP2, IP3, IP4, } // All Events @@ -832,13 +865,29 @@ 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) mock.On(r.DB("mockdb").Table("ip").Get("3.4.5.6")).Return(IP3, nil) mock.On(r.DB("mockdb").Table("ip").Get("8.8.8.8")).Return(nil, errors.New("Test Error")) mock.On(r.DB("mockdb").Table("ip").Get("9.9.9.9")).Return(nil, nil) + mock.On(r.DB("mockdb").Table("ip").Get("2001:0db8:85a3::1")).Return(IP4, nil) mock.On(r.DB("mockdb").Table("ip").Get(Partition1InternetIP.IPAddress)).Return(Partition1InternetIP, nil) mock.On(r.DB("mockdb").Table("ip").Get(Partition2InternetIP.IPAddress)).Return(Partition2InternetIP, nil) mock.On(r.DB("mockdb").Table("ip").Get(Partition1SpecificSharedIP.IPAddress)).Return(Partition1SpecificSharedIP, nil) diff --git a/spec/metal-api.json b/spec/metal-api.json index 163f5f1c..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" @@ -3568,6 +3571,10 @@ }, "v1.NetworkAllocateRequest": { "properties": { + "address_family": { + "description": "can be ipv4 or ipv6, defaults to ipv4", + "type": "string" + }, "description": { "description": "a description for this entity", "type": "string" @@ -3586,6 +3593,11 @@ "description": "free labels that you associate with this network.", "type": "object" }, + "length": { + "description": "the bitlen of the prefix to allocate, defaults to defaultchildprefixlength of super prefix", + "format": "byte", + "type": "integer" + }, "name": { "description": "a readable name for this entity", "type": "string" @@ -3606,7 +3618,11 @@ "description": "marks a network as shareable.", "type": "boolean" } - } + }, + "required": [ + "address_family", + "length" + ] }, "v1.NetworkBase": { "properties": { @@ -3633,6 +3649,19 @@ }, "v1.NetworkCreateRequest": { "properties": { + "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" + }, "description": { "description": "a description for this entity", "type": "string" @@ -3705,6 +3734,7 @@ } }, "required": [ + "addressfamily", "destinationprefixes", "id", "nat", @@ -3715,6 +3745,9 @@ }, "v1.NetworkFindRequest": { "properties": { + "addressfamily": { + "type": "string" + }, "destinationprefixes": { "items": { "type": "string" @@ -3766,6 +3799,19 @@ "v1.NetworkImmutable": { "description": "a network which contains prefixes from which IP addresses can be allocated\nprefixes that are reachable within this network", "properties": { + "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" + }, "destinationprefixes": { "description": "the destination prefixes of this network", "items": { @@ -3807,6 +3853,7 @@ } }, "required": [ + "addressfamily", "destinationprefixes", "nat", "prefixes", @@ -3816,6 +3863,14 @@ }, "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", @@ -3828,6 +3883,11 @@ "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" @@ -3904,6 +3964,7 @@ } }, "required": [ + "addressfamily", "destinationprefixes", "id", "nat", @@ -4011,13 +4072,6 @@ "mgmtserviceaddress": { "description": "the address to the management service of this partition", "type": "string" - }, - "privatenetworkprefixlength": { - "description": "the length of private networks for the machine's child networks in this partition, default 22", - "format": "int32", - "maximum": 30, - "minimum": 16, - "type": "integer" } } }, @@ -4105,13 +4159,6 @@ "name": { "description": "a readable name for this entity", "type": "string" - }, - "privatenetworkprefixlength": { - "description": "the length of private networks for the machine's child networks in this partition, default 22", - "format": "int32", - "maximum": 30, - "minimum": 16, - "type": "integer" } }, "required": [ @@ -4159,13 +4206,6 @@ "name": { "description": "a readable name for this entity", "type": "string" - }, - "privatenetworkprefixlength": { - "description": "the length of private networks for the machine's child networks in this partition, default 22", - "format": "int32", - "maximum": 30, - "minimum": 16, - "type": "integer" } }, "required": [