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..11526c80 --- /dev/null +++ b/cmd/metal-api/internal/datastore/migrations/06_childprefixlength.go @@ -0,0 +1,53 @@ +package migrations + +import ( + r "gopkg.in/rethinkdb/rethinkdb-go.v6" + + "github.com/metal-stack/metal-api/cmd/metal-api/internal/datastore" +) + +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 { + if !old.PrivateSuper { + continue + } + + 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 + } + + new := old + new.ChildPrefixLength = &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/metal/network.go b/cmd/metal-api/internal/metal/network.go index 9f2477d7..be55ee97 100644 --- a/cmd/metal-api/internal/metal/network.go +++ b/cmd/metal-api/internal/metal/network.go @@ -209,6 +209,7 @@ 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"` 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..eb10d04d 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,8 +295,9 @@ func createTestEnvironment(t *testing.T) testEnv { PartitionID: &partition.ID, }, NetworkImmutable: v1.NetworkImmutable{ - Prefixes: []string{testPrivateSuperCidr}, - PrivateSuper: true, + Prefixes: []string{testPrivateSuperCidr}, + PrivateSuper: true, + ChildPrefixLength: pointer.Pointer(uint8(22)), }, } status = te.networkCreate(t, ncr, &createdNetwork) 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..a1a31357 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" @@ -205,6 +206,7 @@ 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 func (r *networkResource) createNetwork(request *restful.Request, response *restful.Response) { var requestPayload v1.NetworkCreateRequest err := request.ReadEntity(&requestPayload) @@ -260,7 +262,8 @@ func (r *networkResource) createNetwork(request *restful.Request, response *rest } prefixes := metal.Prefixes{} - // all Prefixes must be valid + addressFamilies := make(map[string]bool) + // all Prefixes must be valid and from the same addressfamily for i := range requestPayload.Prefixes { p := requestPayload.Prefixes[i] prefix, err := metal.NewPrefixFromCIDR(p) @@ -268,10 +271,25 @@ func (r *networkResource) createNetwork(request *restful.Request, response *rest r.sendError(request, response, httperrors.BadRequest(fmt.Errorf("given prefix %v is not a valid ip with mask: %w", p, err))) return } - + ipprefix, err := netip.ParsePrefix(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 + } + if ipprefix.Addr().Is4() { + addressFamilies["ipv4"] = true + } + if ipprefix.Addr().Is6() { + addressFamilies["ipv6"] = true + } prefixes = append(prefixes, *prefix) } + if len(addressFamilies) > 1 { + r.sendError(request, response, httperrors.BadRequest(fmt.Errorf("given prefixes have different addressfamilies"))) + return + } + destPrefixes := metal.Prefixes{} for i := range requestPayload.DestinationPrefixes { p := requestPayload.DestinationPrefixes[i] @@ -324,17 +342,38 @@ 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{}) + 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 { - r.sendError(request, response, defaultError(fmt.Errorf("partition with id %q already has a private super network", partition.ID))) - return + 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 + } + } } } if underlay { @@ -389,6 +428,23 @@ func (r *networkResource) createNetwork(request *restful.Request, response *rest Labels: labels, } + // check if childprefixlength is set and matches addressfamily + if requestPayload.ChildPrefixLength != nil && privateSuper { + cpl := *requestPayload.ChildPrefixLength + 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()))) + return + } + } + nw.ChildPrefixLength = requestPayload.ChildPrefixLength + } + ctx := request.Request.Context() for _, p := range nw.Prefixes { err := r.ipamer.CreatePrefix(ctx, p) @@ -409,6 +465,7 @@ func (r *networkResource) createNetwork(request *restful.Request, response *rest r.send(request, response, http.StatusCreated, v1.NewNetworkResponse(nw, usage)) } +// 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,9 +520,9 @@ func (r *networkResource) allocateNetwork(request *restful.Request, response *re return } - var superNetwork metal.Network + var superNetworks metal.Networks boolTrue := true - err = r.ds.FindNetwork(&datastore.NetworkSearchQuery{PartitionID: &partition.ID, PrivateSuper: &boolTrue}, &superNetwork) + err = r.ds.SearchNetworks(&datastore.NetworkSearchQuery{PartitionID: &partition.ID, PrivateSuper: &boolTrue}, &superNetworks) if err != nil { r.sendError(request, response, defaultError(err)) return @@ -482,6 +539,37 @@ 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) + var ( + superNetwork metal.Network + superNetworkFound bool + ) + 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 { + r.sendError(request, response, httperrors.BadRequest(fmt.Errorf("no supernetwork for addressfamily:%s found", addressFamily))) + return + } + r.log.Info("network allocate", "supernetwork", superNetwork.ID) + nwSpec := &metal.Network{ Base: metal.Base{ Name: name, @@ -494,8 +582,19 @@ func (r *networkResource) allocateNetwork(request *restful.Request, response *re 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 + } + + // Allow configurable prefix length + length := *superNetwork.ChildPrefixLength + if requestPayload.Length != nil { + length = *requestPayload.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 diff --git a/cmd/metal-api/internal/service/network-service_test.go b/cmd/metal-api/internal/service/network-service_test.go index 4e54993a..c57ee945 100644 --- a/cmd/metal-api/internal/service/network-service_test.go +++ b/cmd/metal-api/internal/service/network-service_test.go @@ -10,6 +10,7 @@ import ( "testing" "github.com/metal-stack/metal-lib/httperrors" + "github.com/metal-stack/metal-lib/pkg/pointer" r "gopkg.in/rethinkdb/rethinkdb-go.v6" "github.com/metal-stack/metal-api/cmd/metal-api/internal/datastore" @@ -252,3 +253,266 @@ 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"}, + privateSuper: true, + vrf: uint(10000), + expectedStatus: http.StatusBadRequest, + expectedErrorMessage: "partition with id \"1\" already has a private super network for this addressfamily", + }, + { + name: "privatesuper IPv6", + networkName: testdata.Nw1.Name, + partitionID: testdata.Nw1.PartitionID, + projectID: testdata.Nw1.ProjectID, + prefixes: []string{"fdaa:bbcc::/50"}, + destinationPrefixes: []string{"::/0"}, + 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"}, + 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 childprefixlength 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.ChildPrefixLength = 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.ChildPrefixLength) + } + } + }) + } +} + +// 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", +// networkName: "tenantv4", +// partitionID: "1", +// projectID: "project-1", +// expectedStatus: http.StatusCreated, +// }, +// { +// name: "ipv6 without ipv6 super", +// networkName: "tenantv6", +// partitionID: "1", +// projectID: "project-1", +// addressFamily: pointer.Pointer("ipv6"), +// expectedStatus: http.StatusUnprocessableEntity, +// expectedErrorMessage: "no supernetwork for addressfamily:IPv6 found", +// }, +// } +// for _, tt := range tests { +// ds, mock := datastore.InitMockDB(t) + +// ipamer, err := testdata.InitMockIpamData(mock, false) +// require.Nil(t, err) +// mock.On(r.DB("mockdb").Table("network").Filter(r.MockAnything()).Filter(r.MockAnything())).Return(metal.Networks{testdata.Nw1, testdata.Nw2}, 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, +// ) +// testdata.InitMockDBData(mock) + +// psc := mdmock.ProjectServiceClient{} +// psc.On("Get", context.Background(), &mdmv1.ProjectGetRequest{Id: "project-1"}).Return(&mdmv1.ProjectResponse{ +// Project: &mdmv1.Project{ +// Meta: &mdmv1.Meta{Id: tt.projectID}, +// }, +// }, nil, +// ) +// tsc := mdmock.TenantServiceClient{} + +// mdc := mdm.NewMock(&psc, &tsc) + +// 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, _ := json.Marshal(allocateRequest) +// 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() +// 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.Nil(t, err) +// require.Equal(t, tt.expectedErrorMessage, result.Message) +// } else { +// var result v1.NetworkResponse +// err = json.NewDecoder(resp.Body).Decode(&result) +// require.Nil(t, err) +// require.Equal(t, tt.networkName, *result.Name) +// require.Equal(t, tt.partitionID, *result.PartitionID) +// require.Equal(t, tt.projectID, *result.ProjectID) +// // TODO check af and length +// } +// } +// } 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/v1/network.go b/cmd/metal-api/internal/service/v1/network.go index d1bb9cc7..eb5e529a 100644 --- a/cmd/metal-api/internal/service/v1/network.go +++ b/cmd/metal-api/internal/service/v1/network.go @@ -17,6 +17,7 @@ type NetworkBase struct { 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"` @@ -47,6 +48,29 @@ 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" optional:"true"` + Length *uint8 `json:"length" description:"the bitlen of the prefix to allocate, defaults to childprefixlength of super prefix" optional:"true"` +} + +// 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 } // NetworkFindRequest is used to find a Network with different criteria. @@ -106,6 +130,7 @@ func NewNetworkResponse(network *metal.Network, usage *metal.NetworkUsage) *Netw NetworkImmutable: NetworkImmutable{ Prefixes: network.Prefixes.String(), DestinationPrefixes: network.DestinationPrefixes.String(), + ChildPrefixLength: network.ChildPrefixLength, Nat: network.Nat, PrivateSuper: network.PrivateSuper, Underlay: network.Underlay, 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/testdata.go b/cmd/metal-api/internal/testdata/testdata.go index 273f5bfb..5722e137 100644 --- a/cmd/metal-api/internal/testdata/testdata.go +++ b/cmd/metal-api/internal/testdata/testdata.go @@ -283,6 +283,8 @@ var ( 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"} + cpl1 = uint8(28) + cpl2 = uint8(22) prefixes1 = []metal.Prefix{prefix1, prefix2} prefixes2 = []metal.Prefix{prefix2} @@ -295,9 +297,10 @@ var ( Name: "Network 1", Description: "description 1", }, - PartitionID: Partition1.ID, - Prefixes: prefixes1, - PrivateSuper: true, + PartitionID: Partition1.ID, + Prefixes: prefixes1, + PrivateSuper: true, + ChildPrefixLength: &cpl1, } Nw2 = metal.Network{ Base: metal.Base{ @@ -305,8 +308,9 @@ var ( Name: "Network 2", Description: "description 2", }, - Prefixes: prefixes2, - Underlay: true, + Prefixes: prefixes2, + Underlay: true, + ChildPrefixLength: &cpl2, } Nw3 = metal.Network{ Base: metal.Base{ @@ -481,6 +485,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 +525,6 @@ var ( Name: "partition1", Description: "description 1", }, - PrivateNetworkPrefixLength: 22, } Partition2 = metal.Partition{ Base: metal.Base{ @@ -522,7 +532,6 @@ var ( Name: "partition2", Description: "description 2", }, - PrivateNetworkPrefixLength: 22, } Partition3 = metal.Partition{ Base: metal.Base{ @@ -530,7 +539,6 @@ var ( Name: "partition3", Description: "description 3", }, - PrivateNetworkPrefixLength: 22, } // Switches @@ -718,7 +726,7 @@ var ( } // All IPs TestIPs = []metal.IP{ - IP1, IP2, IP3, + IP1, IP2, IP3, IP4, } // All Events @@ -839,6 +847,7 @@ func InitMockDBData(mock *r.Mock) { 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 867f4fba..23f98a60 100644 --- a/spec/metal-api.json +++ b/spec/metal-api.json @@ -3565,6 +3565,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" @@ -3583,6 +3587,11 @@ "description": "free labels that you associate with this network.", "type": "object" }, + "length": { + "description": "the bitlen of the prefix to allocate, defaults to childprefixlength of super prefix", + "format": "byte", + "type": "integer" + }, "name": { "description": "a readable name for this entity", "type": "string" @@ -3630,6 +3639,11 @@ }, "v1.NetworkCreateRequest": { "properties": { + "childprefixlength": { + "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" @@ -3763,6 +3777,11 @@ "v1.NetworkImmutable": { "description": "a network which contains prefixes from which IP addresses can be allocated\nprefixes that are reachable within this network", "properties": { + "childprefixlength": { + "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": { @@ -3819,6 +3838,11 @@ "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", @@ -4008,13 +4032,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" } } }, @@ -4102,13 +4119,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": [ @@ -4156,13 +4166,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": [