diff --git a/cmd/metal-api/internal/datastore/migrations/06_childprefixlength.go b/cmd/metal-api/internal/datastore/migrations/06_childprefixlength.go index b45ab386..f7d56961 100644 --- a/cmd/metal-api/internal/datastore/migrations/06_childprefixlength.go +++ b/cmd/metal-api/internal/datastore/migrations/06_childprefixlength.go @@ -39,10 +39,16 @@ func init() { return err } if af != nil { - new.AddressFamily = *af + if new.AddressFamilies == nil { + new.AddressFamilies = make(map[metal.AddressFamily]bool) + } + new.AddressFamilies[*af] = true } if new.PrivateSuper { - new.DefaultChildPrefixLength = &partition.PrivateNetworkPrefixLength + if new.DefaultChildPrefixLength == nil { + new.DefaultChildPrefixLength = make(map[metal.AddressFamily]uint8) + } + new.DefaultChildPrefixLength[*af] = partition.PrivateNetworkPrefixLength } err = rs.UpdateNetwork(&old, &new) if err != nil { 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 53d54cd7..7114268e 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 @@ -219,18 +219,18 @@ func Test_MigrationChildPrefixLength(t *testing.T) { n1fetched, err := rs.FindNetworkByID(n1.ID) require.NoError(t, err) require.NotNil(t, n1fetched) - require.Equal(t, p1.PrivateNetworkPrefixLength, *n1fetched.DefaultChildPrefixLength, fmt.Sprintf("childprefixlength:%d", *n1fetched.DefaultChildPrefixLength)) - require.Equal(t, metal.IPv4AddressFamily, n1fetched.AddressFamily) + require.Equal(t, p1.PrivateNetworkPrefixLength, n1fetched.DefaultChildPrefixLength[metal.IPv4AddressFamily], fmt.Sprintf("childprefixlength:%v", n1fetched.DefaultChildPrefixLength)) + require.True(t, n1fetched.AddressFamilies[metal.IPv4AddressFamily]) 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) + require.Equal(t, p2.PrivateNetworkPrefixLength, n2fetched.DefaultChildPrefixLength[metal.IPv6AddressFamily], fmt.Sprintf("childprefixlength:%v", n2fetched.DefaultChildPrefixLength)) + require.True(t, n2fetched.AddressFamilies[metal.IPv6AddressFamily]) 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) + require.True(t, n3fetched.AddressFamilies[metal.IPv4AddressFamily]) } diff --git a/cmd/metal-api/internal/datastore/network.go b/cmd/metal-api/internal/datastore/network.go index 4e949adb..1e9990e7 100644 --- a/cmd/metal-api/internal/datastore/network.go +++ b/cmd/metal-api/internal/datastore/network.go @@ -12,19 +12,18 @@ 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"` - AddressFamily *metal.AddressFamily `json:"addressfamily" 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"` } func (p *NetworkSearchQuery) Validate() error { @@ -105,12 +104,6 @@ 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/ipam/ipam.go b/cmd/metal-api/internal/ipam/ipam.go index df475ae8..f820187b 100644 --- a/cmd/metal-api/internal/ipam/ipam.go +++ b/cmd/metal-api/internal/ipam/ipam.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "net/netip" "github.com/metal-stack/metal-api/cmd/metal-api/internal/metal" "github.com/metal-stack/metal-lib/rest" @@ -54,7 +55,7 @@ func (i *ipam) AllocateChildPrefix(ctx context.Context, parentPrefix metal.Prefi return nil, fmt.Errorf("error creating new prefix in ipam: %w", err) } - prefix, err := metal.NewPrefixFromCIDR(ipamPrefix.Msg.Prefix.Cidr) + prefix, _, err := metal.NewPrefixFromCIDR(ipamPrefix.Msg.Prefix.Cidr) if err != nil { return nil, fmt.Errorf("error creating prefix from ipam prefix: %w", err) } @@ -154,14 +155,33 @@ func (i *ipam) PrefixUsage(ctx context.Context, cidr string) (*metal.NetworkUsag if err != nil { return nil, fmt.Errorf("prefix usage for cidr:%s not found %w", cidr, err) } - + pfx, err := netip.ParsePrefix(cidr) + if err != nil { + return nil, err + } + af := metal.IPv4AddressFamily + if pfx.Addr().Is6() { + af = metal.IPv6AddressFamily + } + availableIPs := map[metal.AddressFamily]uint64{ + af: usage.Msg.AvailableIps, + } + usedIPs := map[metal.AddressFamily]uint64{ + af: usage.Msg.AcquiredIps, + } + availablePrefixes := map[metal.AddressFamily]uint64{ + af: usage.Msg.AvailableSmallestPrefixes, + } + usedPrefixes := map[metal.AddressFamily]uint64{ + af: usage.Msg.AcquiredPrefixes, + } return &metal.NetworkUsage{ - AvailableIPs: usage.Msg.AvailableIps, - UsedIPs: usage.Msg.AcquiredIps, + AvailableIPs: availableIPs, + UsedIPs: usedIPs, // FIXME add usage.AvailablePrefixList as already done here // https://github.com/metal-stack/metal-api/pull/152/files#diff-fe05f7f1480be933b5c482b74af28c8b9ca7ef2591f8341eb6e6663cbaeda7baR828 - AvailablePrefixes: usage.Msg.AvailableSmallestPrefixes, - UsedPrefixes: usage.Msg.AcquiredPrefixes, + AvailablePrefixes: availablePrefixes, + UsedPrefixes: usedPrefixes, }, nil } diff --git a/cmd/metal-api/internal/metal/network.go b/cmd/metal-api/internal/metal/network.go index 8f54b00d..f1b2e53b 100644 --- a/cmd/metal-api/internal/metal/network.go +++ b/cmd/metal-api/internal/metal/network.go @@ -174,17 +174,17 @@ type Prefix struct { type Prefixes []Prefix // NewPrefixFromCIDR returns a new prefix from a given cidr. -func NewPrefixFromCIDR(cidr string) (*Prefix, error) { +func NewPrefixFromCIDR(cidr string) (*Prefix, *netip.Prefix, error) { prefix, err := netip.ParsePrefix(cidr) if err != nil { - return nil, err + return nil, nil, err } ip := prefix.Addr().String() length := strconv.Itoa(prefix.Bits()) return &Prefix{ IP: ip, Length: length, - }, nil + }, &prefix, nil } // String implements the Stringer interface @@ -211,7 +211,7 @@ type Network struct { Base 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"` + DefaultChildPrefixLength ChildPrefixLength `rethinkdb:"defaultchildprefixlength" json:"childprefixlength" description:"if privatesuper, this defines the bitlen of child prefixes per addressfamily if not nil"` PartitionID string `rethinkdb:"partitionid" json:"partitionid"` ProjectID string `rethinkdb:"projectid" json:"projectid"` ParentNetworkID string `rethinkdb:"parentnetworkid" json:"parentnetworkid"` @@ -221,11 +221,14 @@ type Network struct { 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"` + AddressFamilies AddressFamilies `rethinkdb:"addressfamily" json:"addressfamily"` } +type ChildPrefixLength map[AddressFamily]uint8 + // AddressFamily identifies IPv4/IPv6 type AddressFamily string +type AddressFamilies map[AddressFamily]bool const ( // IPv4AddressFamily identifies IPv4 @@ -253,10 +256,10 @@ type NetworkMap map[string]Network // NetworkUsage contains usage information of a network type NetworkUsage struct { - AvailableIPs uint64 `json:"available_ips" description:"the total available IPs" readonly:"true"` - UsedIPs uint64 `json:"used_ips" description:"the total used IPs" readonly:"true"` - AvailablePrefixes uint64 `json:"available_prefixes" description:"the total available 2 bit Prefixes" readonly:"true"` - UsedPrefixes uint64 `json:"used_prefixes" description:"the total used Prefixes" readonly:"true"` + AvailableIPs map[AddressFamily]uint64 `json:"available_ips" description:"the total available IPs" readonly:"true"` + UsedIPs map[AddressFamily]uint64 `json:"used_ips" description:"the total used IPs" readonly:"true"` + AvailablePrefixes map[AddressFamily]uint64 `json:"available_prefixes" description:"the total available 2 bit Prefixes" readonly:"true"` + UsedPrefixes map[AddressFamily]uint64 `json:"used_prefixes" description:"the total used Prefixes" readonly:"true"` } // ByID creates an indexed map of partitions where the id is the index. diff --git a/cmd/metal-api/internal/service/integration_test.go b/cmd/metal-api/internal/service/integration_test.go index 315030e2..1d2f0e5d 100644 --- a/cmd/metal-api/internal/service/integration_test.go +++ b/cmd/metal-api/internal/service/integration_test.go @@ -23,7 +23,6 @@ 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" @@ -297,8 +296,8 @@ func createTestEnvironment(t *testing.T) testEnv { NetworkImmutable: v1.NetworkImmutable{ Prefixes: []string{testPrivateSuperCidr}, PrivateSuper: true, - DefaultChildPrefixLength: pointer.Pointer(uint8(22)), - AddressFamily: v1.IPv4AddressFamily, + DefaultChildPrefixLength: map[metal.AddressFamily]uint8{metal.IPv4AddressFamily: 22}, + AddressFamilies: map[metal.AddressFamily]bool{metal.IPv4AddressFamily: true}, }, } log.Info("try to create a network", "request", ncr) @@ -323,7 +322,6 @@ 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.go b/cmd/metal-api/internal/service/ip-service.go index 75458b98..a0891385 100644 --- a/cmd/metal-api/internal/service/ip-service.go +++ b/cmd/metal-api/internal/service/ip-service.go @@ -285,6 +285,22 @@ func (r *ipResource) allocateIP(request *restful.Request, response *restful.Resp return } + if requestPayload.AddressFamily != nil { + ok := nw.AddressFamilies[metal.ToAddressFamily(string(*requestPayload.AddressFamily))] + if !ok { + r.sendError(request, response, httperrors.BadRequest( + fmt.Errorf("there is no prefix for the given addressfamily:%s present in this network:%s", string(*requestPayload.AddressFamily), requestPayload.NetworkID)), + ) + return + } + if specificIP != "" { + r.sendError(request, response, httperrors.BadRequest( + fmt.Errorf("it is not possible to specify specificIP and addressfamily"), + )) + return + } + } + p, err := r.mdc.Project().Get(request.Request.Context(), &mdmv1.ProjectGetRequest{Id: requestPayload.ProjectID}) if err != nil { r.sendError(request, response, defaultError(err)) @@ -320,7 +336,7 @@ func (r *ipResource) allocateIP(request *restful.Request, response *restful.Resp ctx := request.Request.Context() if specificIP == "" { - ipAddress, ipParentCidr, err = allocateRandomIP(ctx, nw, r.ipamer) + ipAddress, ipParentCidr, err = allocateRandomIP(ctx, nw, r.ipamer, requestPayload.AddressFamily) if err != nil { r.sendError(request, response, defaultError(err)) return @@ -333,13 +349,13 @@ func (r *ipResource) allocateIP(request *restful.Request, response *restful.Resp } } - r.logger(request).Debug("allocated ip in ipam", "ip", ipAddress, "network", nw.ID) - ipType := metal.Ephemeral if requestPayload.Type == metal.Static { ipType = metal.Static } + r.logger(request).Info("allocated ip in ipam", "ip", ipAddress, "network", nw.ID, "type", ipType) + ip := &metal.IP{ IPAddress: ipAddress, ParentPrefixCidr: ipParentCidr, @@ -436,8 +452,24 @@ func allocateSpecificIP(ctx context.Context, parent *metal.Network, specificIP s return "", "", fmt.Errorf("specific ip not contained in any of the defined prefixes") } -func allocateRandomIP(ctx context.Context, parent *metal.Network, ipamer ipam.IPAMer) (ipAddress, parentPrefixCidr string, err error) { +func allocateRandomIP(ctx context.Context, parent *metal.Network, ipamer ipam.IPAMer, af *metal.AddressFamily) (ipAddress, parentPrefixCidr string, err error) { + var addressfamily = metal.IPv4AddressFamily + if af != nil { + addressfamily = *af + } + for _, prefix := range parent.Prefixes { + pfx, err := netip.ParsePrefix(prefix.String()) + if err != nil { + return "", "", fmt.Errorf("unable to parse prefix: %w", err) + } + if pfx.Addr().Is4() && addressfamily == metal.IPv6AddressFamily { + continue + } + if pfx.Addr().Is6() && addressfamily == metal.IPv4AddressFamily { + continue + } + ipAddress, err = ipamer.AllocateIP(ctx, prefix) if err != nil && errors.Is(err, goipam.ErrNoIPAvailable) { continue diff --git a/cmd/metal-api/internal/service/ip-service_test.go b/cmd/metal-api/internal/service/ip-service_test.go index 72889fd5..a8af1a91 100644 --- a/cmd/metal-api/internal/service/ip-service_test.go +++ b/cmd/metal-api/internal/service/ip-service_test.go @@ -10,6 +10,7 @@ import ( "testing" "github.com/metal-stack/metal-lib/bus" + "github.com/metal-stack/metal-lib/pkg/pointer" "github.com/metal-stack/metal-lib/pkg/tag" mdmv1 "github.com/metal-stack/masterdata-api/api/v1" @@ -285,6 +286,35 @@ func TestAllocateIP(t *testing.T) { wantedStatus: http.StatusUnprocessableEntity, wantErr: errors.New("specific ip not contained in any of the defined prefixes"), }, + { + name: "allocate a IPv4 address", + allocateRequest: v1.IPAllocateRequest{ + Describable: v1.Describable{}, + IPBase: v1.IPBase{ + ProjectID: "123", + NetworkID: testdata.NwIPAM.ID, + Type: metal.Ephemeral, + }, + AddressFamily: pointer.Pointer(metal.IPv4AddressFamily), + }, + wantedIP: "10.0.0.3", + wantedType: metal.Ephemeral, + wantedStatus: http.StatusCreated, + }, + { + name: "allocate a IPv6 address", + allocateRequest: v1.IPAllocateRequest{ + Describable: v1.Describable{}, + IPBase: v1.IPBase{ + ProjectID: "123", + NetworkID: testdata.NwIPAM.ID, + Type: metal.Ephemeral, + }, + AddressFamily: pointer.Pointer(metal.IPv6AddressFamily), + }, + wantedStatus: http.StatusBadRequest, + wantErr: errors.New("there is no prefix for the given addressfamily:IPv6 present in this network:4"), + }, } for i := range tests { tt := tests[i] @@ -313,6 +343,8 @@ func TestAllocateIP(t *testing.T) { err = json.NewDecoder(resp.Body).Decode(&result) require.NoError(t, err) + require.NotNil(t, result.IPAddress) + require.NotNil(t, result.AllocationUUID) require.Equal(t, tt.wantedType, result.Type) require.Equal(t, tt.wantedIP, result.IPAddress) require.Equal(t, tt.name, *result.Name) diff --git a/cmd/metal-api/internal/service/machine-service.go b/cmd/metal-api/internal/service/machine-service.go index a9511218..06c5269b 100644 --- a/cmd/metal-api/internal/service/machine-service.go +++ b/cmd/metal-api/internal/service/machine-service.go @@ -1575,13 +1575,13 @@ func gatherNetworksFromSpec(ds *datastore.RethinkStore, allocationSpec *machineA network.ips = append(network.ips, *ip) } - for _, pn := range privateNetworks { - if pn.network.PartitionID != partitionPrivateSuperNetwork.PartitionID { - return nil, fmt.Errorf("private network %q must be located in the partition where the machine is going to be placed", pn.network.ID) + for _, privateNetwork := range privateNetworks { + if privateNetwork.network.PartitionID != partitionPrivateSuperNetwork.PartitionID { + return nil, fmt.Errorf("private network %q must be located in the partition where the machine is going to be placed", privateNetwork.network.ID) } - if !pn.auto && len(pn.ips) == 0 { - return nil, fmt.Errorf("the private network %q has no auto ip acquisition, but no suitable IPs were provided, which would lead into a machine having no ip address", pn.network.ID) + if !privateNetwork.auto && len(privateNetwork.ips) == 0 { + return nil, fmt.Errorf("the private network %q has no auto ip acquisition, but no suitable IPs were provided, which would lead into a machine having no ip address", privateNetwork.network.ID) } } @@ -1612,25 +1612,29 @@ func gatherUnderlayNetwork(ds *datastore.RethinkStore, partition *metal.Partitio func makeMachineNetwork(ctx context.Context, ds *datastore.RethinkStore, ipamer ipam.IPAMer, allocationSpec *machineAllocationSpec, n *allocationNetwork) (*metal.MachineNetwork, error) { if n.auto { - ipAddress, ipParentCidr, err := allocateRandomIP(ctx, n.network, ipamer) - if err != nil { - return nil, fmt.Errorf("unable to allocate an ip in network: %s %w", n.network.ID, err) - } - ip := &metal.IP{ - IPAddress: ipAddress, - ParentPrefixCidr: ipParentCidr, - Name: allocationSpec.Name, - Description: "autoassigned", - NetworkID: n.network.ID, - Type: metal.Ephemeral, - ProjectID: allocationSpec.ProjectID, - } - ip.AddMachineId(allocationSpec.UUID) - err = ds.CreateIP(ip) - if err != nil { - return nil, err + + for af := range n.network.AddressFamilies { + addressFamily := v1.ToAddressFamily(string(af)) + ipAddress, ipParentCidr, err := allocateRandomIP(ctx, n.network, ipamer, &addressFamily) + if err != nil { + return nil, fmt.Errorf("unable to allocate an ip in network: %s %w", n.network.ID, err) + } + ip := &metal.IP{ + IPAddress: ipAddress, + ParentPrefixCidr: ipParentCidr, + Name: allocationSpec.Name, + Description: "autoassigned", + NetworkID: n.network.ID, + Type: metal.Ephemeral, + ProjectID: allocationSpec.ProjectID, + } + ip.AddMachineId(allocationSpec.UUID) + err = ds.CreateIP(ip) + if err != nil { + return nil, err + } + n.ips = append(n.ips, *ip) } - n.ips = append(n.ips, *ip) } // from the makeNetworks call, a lot of ips might be set in this network diff --git a/cmd/metal-api/internal/service/machine-service_allocation_test.go b/cmd/metal-api/internal/service/machine-service_allocation_test.go index 1cb50dce..bc42120e 100644 --- a/cmd/metal-api/internal/service/machine-service_allocation_test.go +++ b/cmd/metal-api/internal/service/machine-service_allocation_test.go @@ -380,7 +380,7 @@ func createTestdata(machineCount int, rs *datastore.RethinkStore, ipamer ipam.IP private, err := ipamer.AllocateChildPrefix(ctx, tenantSuper, 22) require.NoError(t, err) require.NotNil(t, private) - privateNetwork, err := metal.NewPrefixFromCIDR(private.String()) + privateNetwork, _, err := metal.NewPrefixFromCIDR(private.String()) require.NoError(t, err) require.NotNil(t, privateNetwork) diff --git a/cmd/metal-api/internal/service/network-service.go b/cmd/metal-api/internal/service/network-service.go index 8e0c4b41..49e95bd4 100644 --- a/cmd/metal-api/internal/service/network-service.go +++ b/cmd/metal-api/internal/service/network-service.go @@ -156,7 +156,11 @@ func (r *networkResource) findNetwork(request *restful.Request, response *restfu return } ctx := request.Request.Context() - usage := getNetworkUsage(ctx, nw, r.ipamer) + usage, err := getNetworkUsage(ctx, nw, r.ipamer) + if err != nil { + r.sendError(request, response, defaultError(err)) + return + } r.send(request, response, http.StatusOK, v1.NewNetworkResponse(nw, usage)) } @@ -170,7 +174,12 @@ func (r *networkResource) listNetworks(request *restful.Request, response *restf ctx := request.Request.Context() var result []*v1.NetworkResponse for i := range nws { - usage := getNetworkUsage(ctx, &nws[i], r.ipamer) + usage, err := getNetworkUsage(ctx, &nws[i], r.ipamer) + if err != nil { + r.sendError(request, response, defaultError(err)) + return + } + result = append(result, v1.NewNetworkResponse(&nws[i], usage)) } @@ -200,7 +209,12 @@ func (r *networkResource) findNetworks(request *restful.Request, response *restf ctx := request.Request.Context() result := []*v1.NetworkResponse{} for i := range nws { - usage := getNetworkUsage(ctx, &nws[i], r.ipamer) + usage, err := getNetworkUsage(ctx, &nws[i], r.ipamer) + if err != nil { + r.sendError(request, response, defaultError(err)) + return + } + result = append(result, v1.NewNetworkResponse(&nws[i], usage)) } @@ -263,35 +277,17 @@ func (r *networkResource) createNetwork(request *restful.Request, response *rest 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 + var childPrefixLength = metal.ChildPrefixLength{} + for af, length := range requestPayload.DefaultChildPrefixLength { + childPrefixLength[metal.ToAddressFamily(string(af))] = length } - // all DestinationPrefixes must be valid and from the same addressfamily - _, _, err = validatePrefixes(requestPayload.DestinationPrefixes) + + prefixes, destPrefixes, addressFamilies, err := validatePrefixesAndAddressFamilies(requestPayload.Prefixes, requestPayload.DestinationPrefixes, childPrefixLength, privateSuper) if err != nil { r.sendError(request, response, httperrors.BadRequest(err)) return } - 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{} - for i := range requestPayload.DestinationPrefixes { - p := requestPayload.DestinationPrefixes[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 - } - destPrefixes = append(destPrefixes, *prefix) - } - allNws, err := r.ds.ListNetworks() if err != nil { r.sendError(request, response, defaultError(err)) @@ -338,9 +334,8 @@ func (r *networkResource) createNetwork(request *restful.Request, response *rest if privateSuper { var nw metal.Network err := r.ds.FindNetwork(&datastore.NetworkSearchQuery{ - PartitionID: &partition.ID, - PrivateSuper: pointer.Pointer(true), - AddressFamily: pointer.Pointer(metal.ToAddressFamily(string(*addressFamily))), + PartitionID: &partition.ID, + PrivateSuper: pointer.Pointer(true), }, &nw) r.log.Info("createnetwork", "found", nw) if err != nil && !metal.IsNotFound(err) { @@ -348,7 +343,7 @@ func (r *networkResource) createNetwork(request *restful.Request, response *rest 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))) + r.sendError(request, response, httperrors.BadRequest(fmt.Errorf("partition with id %q already has a private super network", partition.ID))) return } } @@ -393,33 +388,17 @@ func (r *networkResource) createNetwork(request *restful.Request, response *rest Name: name, Description: description, }, - Prefixes: prefixes, - DestinationPrefixes: destPrefixes, - PartitionID: partitionID, - ProjectID: projectID, - Nat: nat, - PrivateSuper: privateSuper, - 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 + Prefixes: prefixes, + DestinationPrefixes: destPrefixes, + DefaultChildPrefixLength: childPrefixLength, + PartitionID: partitionID, + ProjectID: projectID, + Nat: nat, + PrivateSuper: privateSuper, + Underlay: underlay, + Vrf: vrf, + Labels: labels, + AddressFamilies: addressFamilies, } ctx := request.Request.Context() @@ -437,40 +416,86 @@ func (r *networkResource) createNetwork(request *restful.Request, response *rest return } - usage := getNetworkUsage(ctx, nw, r.ipamer) + usage, err := getNetworkUsage(ctx, nw, r.ipamer) + if err != nil { + r.sendError(request, response, defaultError(err)) + return + } r.send(request, response, http.StatusCreated, v1.NewNetworkResponse(nw, usage)) } -func validatePrefixes(prefixes []string) (metal.Prefixes, *v1.AddressFamily, error) { +func validatePrefixesAndAddressFamilies(prefixes, destinationPrefixes []string, defaultChildPrefixLength metal.ChildPrefixLength, privateSuper bool) (metal.Prefixes, metal.Prefixes, metal.AddressFamilies, error) { + + pfxs, addressFamilies, err := validatePrefixes(prefixes) + if err != nil { + return nil, nil, nil, err + } + // all DestinationPrefixes must be valid and from the same addressfamily + destPfxs, destinationAddressFamilies, err := validatePrefixes(destinationPrefixes) + if err != nil { + return nil, nil, nil, err + } + if len(destinationAddressFamilies) > len(addressFamilies) { + return nil, nil, nil, fmt.Errorf("destination prefixes have more addressfamilies then prefixes") + + } + + if !privateSuper { + return pfxs, destPfxs, addressFamilies, nil + } + + if len(defaultChildPrefixLength) == 0 { + return nil, nil, nil, fmt.Errorf("private super network must always contain a defaultchildprefixlength") + } + + for af, length := range defaultChildPrefixLength { + fmt.Printf("af %s length:%d addressfamilies:%v", af, length, addressFamilies) + ok := addressFamilies[af] + if !ok { + return nil, nil, nil, fmt.Errorf("private super network must always contain a defaultchildprefixlength per addressfamily:%s", af) + } + + // check if childprefixlength is set and matches addressfamily + for _, p := range pfxs { + ipprefix, err := netip.ParsePrefix(p.String()) + if err != nil { + return nil, nil, nil, fmt.Errorf("given prefix %v is not a valid ip with mask: %w", p, err) + } + if ipprefix.Addr().Is4() && af == metal.IPv6AddressFamily { + continue + } + if ipprefix.Addr().Is6() && af == metal.IPv4AddressFamily { + continue + } + if length <= uint8(ipprefix.Bits()) { + return nil, nil, nil, fmt.Errorf("given defaultchildprefixlength %d is not greater than prefix length of:%s", length, p.String()) + } + } + } + + return pfxs, destPfxs, addressFamilies, nil +} + +func validatePrefixes(prefixes []string) (metal.Prefixes, metal.AddressFamilies, error) { var ( result metal.Prefixes - addressFamilies = make(map[string]bool) - addressFamily v1.AddressFamily + addressFamilies = metal.AddressFamilies{} ) 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) + prefix, ipprefix, 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) } if ipprefix.Addr().Is4() { - addressFamilies["ipv4"] = true - addressFamily = v1.IPv4AddressFamily + addressFamilies[metal.IPv4AddressFamily] = true } if ipprefix.Addr().Is6() { - addressFamilies["ipv6"] = true - addressFamily = v1.IPv6AddressFamily + addressFamilies[metal.IPv6AddressFamily] = true } result = append(result, *prefix) } - if len(addressFamilies) > 1 { - return nil, nil, fmt.Errorf("given prefixes have different addressfamilies") - } - return result, &addressFamily, nil + return result, addressFamilies, nil } // TODO add possibility to allocate from a non super network if given in the AllocateRequest and super has childprefixlength @@ -530,7 +555,7 @@ func (r *networkResource) allocateNetwork(request *restful.Request, response *re destPrefixes := metal.Prefixes{} for _, p := range requestPayload.DestinationPrefixes { - prefix, err := metal.NewPrefixFromCIDR(p) + 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 @@ -539,20 +564,15 @@ 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) + r.log.Info("network allocate", "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))), + PartitionID: &partition.ID, + PrivateSuper: pointer.Pointer(true), + ParentNetworkID: requestPayload.ParentNetworkID, }, &superNetwork) if err != nil { r.sendError(request, response, defaultError(err)) @@ -560,7 +580,7 @@ func (r *networkResource) allocateNetwork(request *restful.Request, response *re } if superNetwork.ID == "" { - r.sendError(request, response, httperrors.BadRequest(fmt.Errorf("no supernetwork for addressfamily:%s found", addressFamily))) + r.sendError(request, response, httperrors.BadRequest(fmt.Errorf("no supernetwork found"))) return } if superNetwork.DefaultChildPrefixLength == nil { @@ -580,16 +600,30 @@ func (r *networkResource) allocateNetwork(request *restful.Request, response *re DestinationPrefixes: destPrefixes, Shared: shared, Nat: nat, - AddressFamily: metal.AddressFamily(addressFamily), + AddressFamilies: superNetwork.AddressFamilies, } - // Allow configurable prefix length - length := *superNetwork.DefaultChildPrefixLength + // Allow configurable prefix length per AF + length := superNetwork.DefaultChildPrefixLength if requestPayload.Length != nil { - // requestPayload.Length must be smaller than defaultchildprefixlength, but is checked in go-ipam - length = *requestPayload.Length + for af, l := range requestPayload.Length { + length[metal.ToAddressFamily(string(af))] = l + } + } + + if requestPayload.AddressFamily != nil { + af := metal.ToAddressFamily(string(*requestPayload.AddressFamily)) + bits, ok := length[af] + if !ok { + r.sendError(request, response, httperrors.BadRequest(fmt.Errorf("addressfamiliy %s specified, but no childprefixlength for this addressfamily", *requestPayload.AddressFamily))) + return + } + length = metal.ChildPrefixLength{ + af: bits, + } } - r.log.Info("network allocate", "supernetwork", superNetwork.ID, "defaultchildprefixlength", *superNetwork.DefaultChildPrefixLength, "length", length) + + r.log.Info("network allocate", "supernetwork", superNetwork.ID, "defaultchildprefixlength", superNetwork.DefaultChildPrefixLength, "length", length) ctx := request.Request.Context() nw, err := createChildNetwork(ctx, r.ds, r.ipamer, nwSpec, &superNetwork, length) @@ -598,24 +632,31 @@ func (r *networkResource) allocateNetwork(request *restful.Request, response *re return } - usage := getNetworkUsage(ctx, nw, r.ipamer) + usage, err := getNetworkUsage(ctx, nw, r.ipamer) + if err != nil { + r.sendError(request, response, defaultError(err)) + return + } r.send(request, response, http.StatusCreated, v1.NewNetworkResponse(nw, usage)) } -func createChildNetwork(ctx context.Context, ds *datastore.RethinkStore, ipamer ipam.IPAMer, nwSpec *metal.Network, parent *metal.Network, childLength uint8) (*metal.Network, error) { +func createChildNetwork(ctx context.Context, ds *datastore.RethinkStore, ipamer ipam.IPAMer, nwSpec *metal.Network, parent *metal.Network, childLengths metal.ChildPrefixLength) (*metal.Network, error) { vrf, err := acquireRandomVRF(ds) if err != nil { return nil, fmt.Errorf("could not acquire a vrf: %w", err) } - childPrefix, err := createChildPrefix(ctx, parent.Prefixes, childLength, ipamer) - if err != nil { - return nil, err - } - - if childPrefix == nil { - return nil, fmt.Errorf("could not allocate child prefix in parent network: %s", parent.ID) + var childPrefixes = metal.Prefixes{} + for af, childLength := range childLengths { + childPrefix, err := createChildPrefix(ctx, parent.Prefixes, childLength, ipamer) + if err != nil { + return nil, err + } + if childPrefix == nil { + return nil, fmt.Errorf("could not allocate child prefix in parent network: %s for addressfamily: %s", parent.ID, af) + } + childPrefixes = append(childPrefixes, *childPrefix) } nw := &metal.Network{ @@ -623,7 +664,7 @@ func createChildNetwork(ctx context.Context, ds *datastore.RethinkStore, ipamer Name: nwSpec.Name, Description: nwSpec.Description, }, - Prefixes: metal.Prefixes{*childPrefix}, + Prefixes: childPrefixes, DestinationPrefixes: nwSpec.DestinationPrefixes, PartitionID: parent.PartitionID, ProjectID: nwSpec.ProjectID, @@ -634,6 +675,7 @@ func createChildNetwork(ctx context.Context, ds *datastore.RethinkStore, ipamer Vrf: *vrf, ParentNetworkID: parent.ID, Labels: nwSpec.Labels, + AddressFamilies: nwSpec.AddressFamilies, } err = ds.CreateNetwork(nw) @@ -719,18 +761,11 @@ func (r *networkResource) updateNetwork(request *restful.Request, response *rest ) if len(requestPayload.Prefixes) > 0 { - // all Prefixes must be valid and from the same addressfamily - prefixes, af, err := validatePrefixes(requestPayload.Prefixes) + prefixes, _, err := validatePrefixes(requestPayload.Prefixes) if err != nil { - 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))) + r.sendError(request, response, defaultError(err)) return } - newNetwork.Prefixes = prefixes prefixesToBeRemoved = oldNetwork.SubtractPrefixes(newNetwork.Prefixes...) @@ -751,6 +786,22 @@ func (r *networkResource) updateNetwork(request *restful.Request, response *rest prefixesToBeAdded = newNetwork.SubtractPrefixes(oldNetwork.Prefixes...) } + if len(requestPayload.DestinationPrefixes) > 0 { + destPrefixes, _, err := validatePrefixes(requestPayload.DestinationPrefixes) + if err != nil { + r.sendError(request, response, defaultError(err)) + return + } + newNetwork.DestinationPrefixes = destPrefixes + } + + _, _, addressFamilies, err := validatePrefixesAndAddressFamilies(newNetwork.Prefixes.String(), newNetwork.DestinationPrefixes.String(), oldNetwork.DefaultChildPrefixLength, oldNetwork.PrivateSuper) + if err != nil { + r.sendError(request, response, httperrors.BadRequest(err)) + return + } + newNetwork.AddressFamilies = addressFamilies + ctx := request.Request.Context() for _, p := range prefixesToBeRemoved { @@ -769,29 +820,17 @@ func (r *networkResource) updateNetwork(request *restful.Request, response *rest } } - if len(requestPayload.DestinationPrefixes) > 0 { - // all Prefixes must be valid and from the same addressfamily - prefixes, af, err := validatePrefixes(requestPayload.Prefixes) - if err != nil { - 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) if err != nil { r.sendError(request, response, defaultError(err)) return } - usage := getNetworkUsage(ctx, &newNetwork, r.ipamer) + usage, err := getNetworkUsage(ctx, &newNetwork, r.ipamer) + if err != nil { + r.sendError(request, response, defaultError(err)) + return + } r.send(request, response, http.StatusOK, v1.NewNetworkResponse(&newNetwork, usage)) } @@ -855,23 +894,35 @@ func (r *networkResource) deleteNetwork(request *restful.Request, response *rest r.send(request, response, http.StatusOK, v1.NewNetworkResponse(nw, &metal.NetworkUsage{})) } -func getNetworkUsage(ctx context.Context, nw *metal.Network, ipamer ipam.IPAMer) *metal.NetworkUsage { - usage := &metal.NetworkUsage{} +func getNetworkUsage(ctx context.Context, nw *metal.Network, ipamer ipam.IPAMer) (*metal.NetworkUsage, error) { + usage := &metal.NetworkUsage{ + AvailableIPs: make(map[metal.AddressFamily]uint64), + UsedIPs: make(map[metal.AddressFamily]uint64), + AvailablePrefixes: make(map[metal.AddressFamily]uint64), + UsedPrefixes: make(map[metal.AddressFamily]uint64), + } if nw == nil { - return usage + return usage, nil } for _, prefix := range nw.Prefixes { + pfx, err := netip.ParsePrefix(prefix.String()) + if err != nil { + return nil, err + } + key := metal.IPv4AddressFamily + if pfx.Addr().Is6() { + key = metal.IPv6AddressFamily + } u, err := ipamer.PrefixUsage(ctx, prefix.String()) if err != nil { - // FIXME: the error should not be swallowed here as this can return wrong usage information to the clients - continue + return nil, err } - usage.AvailableIPs = usage.AvailableIPs + u.AvailableIPs - usage.UsedIPs = usage.UsedIPs + u.UsedIPs - usage.AvailablePrefixes = usage.AvailablePrefixes + u.AvailablePrefixes - usage.UsedPrefixes = usage.UsedPrefixes + u.UsedPrefixes + usage.AvailableIPs[key] += u.AvailableIPs[key] + usage.UsedIPs[key] += u.UsedIPs[key] + usage.AvailablePrefixes[key] += u.AvailablePrefixes[key] + usage.UsedPrefixes[key] += u.UsedPrefixes[key] } - return usage + return usage, nil } func createChildPrefix(ctx context.Context, parentPrefixes metal.Prefixes, childLength uint8, ipamer ipam.IPAMer) (*metal.Prefix, error) { diff --git a/cmd/metal-api/internal/service/network-service_test.go b/cmd/metal-api/internal/service/network-service_test.go index ed400b38..2be66e90 100644 --- a/cmd/metal-api/internal/service/network-service_test.go +++ b/cmd/metal-api/internal/service/network-service_test.go @@ -7,11 +7,10 @@ import ( "log/slog" "net/http" "net/http/httptest" - "net/netip" - "reflect" "testing" restful "github.com/emicklei/go-restful/v3" + "github.com/google/go-cmp/cmp" 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" @@ -21,7 +20,6 @@ import ( v1 "github.com/metal-stack/metal-api/cmd/metal-api/internal/service/v1" "github.com/metal-stack/metal-api/cmd/metal-api/internal/testdata" "github.com/metal-stack/metal-lib/httperrors" - "github.com/metal-stack/metal-lib/pkg/pointer" testifymock "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" r "gopkg.in/rethinkdb/rethinkdb-go.v6" @@ -268,7 +266,7 @@ func Test_networkResource_createNetwork(t *testing.T) { prefixes []string destinationPrefixes []string vrf uint - childprefixlength *uint8 + childprefixlength metal.ChildPrefixLength privateSuper bool underlay bool nat bool @@ -286,17 +284,19 @@ func Test_networkResource_createNetwork(t *testing.T) { 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)), + 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: metal.ChildPrefixLength{ + metal.IPv4AddressFamily: 22, + }, privateSuper: true, vrf: uint(10000), expectedStatus: http.StatusBadRequest, - expectedErrorMessage: "partition with id \"1\" already has a private super network for addressfamily:IPv4", + expectedErrorMessage: "given defaultchildprefixlength 22 is not greater than prefix length of:172.0.0.0/24", }, { name: "privatesuper IPv4 without defaultchildprefixlength", @@ -311,25 +311,30 @@ func Test_networkResource_createNetwork(t *testing.T) { expectedErrorMessage: "private super network must always contain a defaultchildprefixlength", }, { - name: "privatesuper IPv6", + name: "privatesuper Mixed", + networkName: "privatesuper mixed", + partitionID: "3", + projectID: "", + prefixes: []string{"fdaa:bbcc::/50", "172.0.0.0/16"}, + destinationPrefixes: []string{"::/0", "0.0.0.0/0"}, + childprefixlength: metal.ChildPrefixLength{ + metal.IPv4AddressFamily: 22, + metal.IPv6AddressFamily: 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{"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)), + prefixes: []string{"192.168.265.0/24"}, + destinationPrefixes: []string{"0.0.0.0/0"}, + childprefixlength: metal.ChildPrefixLength{ + metal.IPv6AddressFamily: 64, + }, privateSuper: true, vrf: uint(10000), expectedStatus: http.StatusBadRequest, @@ -348,15 +353,14 @@ func Test_networkResource_createNetwork(t *testing.T) { 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: "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.StatusCreated, }, { name: "broken destinationprefix", @@ -370,12 +374,14 @@ func Test_networkResource_createNetwork(t *testing.T) { 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)), + name: "broken childprefixlength", + networkName: testdata.Nw1.Name, + partitionID: testdata.Nw1.PartitionID, + projectID: testdata.Nw1.ProjectID, + prefixes: []string{"fdaa:bbcc::/50"}, + childprefixlength: metal.ChildPrefixLength{ + metal.IPv6AddressFamily: 50, + }, privateSuper: true, vrf: uint(10000), expectedStatus: http.StatusBadRequest, @@ -445,8 +451,7 @@ func Test_networkResource_allocateNetwork(t *testing.T) { networkName string partitionID string projectID string - childprefixlength *uint8 - addressFamily *string + childprefixlength metal.ChildPrefixLength shared bool expectedStatus int expectedErrorMessage string @@ -459,29 +464,32 @@ func Test_networkResource_allocateNetwork(t *testing.T) { 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: "simple ipv4, specific childprefixlength", + networkName: "tenantv4.2", + partitionID: testdata.Partition2.ID, + projectID: "project-1", + childprefixlength: metal.ChildPrefixLength{ + metal.IPv4AddressFamily: 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, + name: "mixed, specific childprefixlength", + networkName: "tenantv6.2", + partitionID: "4", + projectID: "project-1", + childprefixlength: metal.ChildPrefixLength{ + metal.IPv4AddressFamily: 22, + metal.IPv6AddressFamily: 58, + }, + expectedStatus: http.StatusCreated, }, } for _, tt := range tests { @@ -509,10 +517,9 @@ func Test_networkResource_allocateNetwork(t *testing.T) { 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, + Describable: v1.Describable{Name: &tt.networkName}, + NetworkBase: v1.NetworkBase{PartitionID: &tt.partitionID, ProjectID: &tt.projectID}, + Length: tt.childprefixlength, } js, err := json.Marshal(allocateRequest) @@ -538,23 +545,12 @@ func Test_networkResource_allocateNetwork(t *testing.T) { 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) } } } @@ -564,41 +560,41 @@ func Test_validatePrefixes(t *testing.T) { name string prefixes []string wantPrefixes metal.Prefixes - wantAF *v1.AddressFamily + wantAF metal.AddressFamilies 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), + wantAF: metal.AddressFamilies{metal.IPv4AddressFamily: true}, }, { 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), + wantAF: metal.AddressFamilies{metal.IPv6AddressFamily: true}, }, { name: "mixed af", prefixes: []string{"10.0.0.0/8", "2001::/64"}, - wantPrefixes: nil, - wantAF: nil, - wantErr: true, + wantPrefixes: metal.Prefixes{{IP: "10.0.0.0", Length: "8"}, {IP: "2001::", Length: "64"}}, + wantAF: metal.AddressFamilies{metal.IPv4AddressFamily: true, metal.IPv6AddressFamily: true}, + wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, got1, err := validatePrefixes(tt.prefixes) + got, af, 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 diff := cmp.Diff(got, tt.wantPrefixes); diff != "" { + t.Errorf("validatePrefixes() diff=%s", diff) } - if !reflect.DeepEqual(got1, tt.wantAF) { - t.Errorf("validatePrefixes() got1 = %v, want %v", got1, tt.wantAF) + if diff := cmp.Diff(af, tt.wantAF); diff != "" { + t.Errorf("validatePrefixes() diff=%s", diff) } }) } diff --git a/cmd/metal-api/internal/service/v1/ip.go b/cmd/metal-api/internal/service/v1/ip.go index a0929267..13b8a011 100644 --- a/cmd/metal-api/internal/service/v1/ip.go +++ b/cmd/metal-api/internal/service/v1/ip.go @@ -20,7 +20,8 @@ type IPIdentifiable struct { type IPAllocateRequest struct { Describable IPBase - MachineID *string `json:"machineid" description:"the machine id this ip should be associated with" optional:"true"` + MachineID *string `json:"machineid" description:"the machine id this ip should be associated with" optional:"true"` + AddressFamily *metal.AddressFamily `json:"addressfamily" description:"the addressfamily to allocate a ip address from the given network, defaults to IPv4" enum:"IPv4|IPv6"` } type IPUpdateRequest struct { diff --git a/cmd/metal-api/internal/service/v1/network.go b/cmd/metal-api/internal/service/v1/network.go index c6ebb28b..5c979ce0 100644 --- a/cmd/metal-api/internal/service/v1/network.go +++ b/cmd/metal-api/internal/service/v1/network.go @@ -15,24 +15,25 @@ 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"` - 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"` + 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 metal.ChildPrefixLength `json:"defaultchildprefixlength" description:"if privatesuper, this defines the bitlen of child prefixes per addressfamily if not nil" optional:"true"` + // FIXME add IPv6Nat + 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"` + AddressFamilies metal.AddressFamilies `json:"addressfamilies" description:"the addressfamilies in this network, either IPv4 or IPv6 or both"` } // NetworkUsage reports core metrics about available and used IPs or Prefixes in a Network. type NetworkUsage struct { - AvailableIPs uint64 `json:"available_ips" description:"the total available IPs" readonly:"true"` - UsedIPs uint64 `json:"used_ips" description:"the total used IPs" readonly:"true"` - AvailablePrefixes uint64 `json:"available_prefixes" description:"the total available 2 bit Prefixes" readonly:"true"` - UsedPrefixes uint64 `json:"used_prefixes" description:"the total used Prefixes" readonly:"true"` + AvailableIPs map[metal.AddressFamily]uint64 `json:"available_ips" description:"the total available IPs" readonly:"true"` + UsedIPs map[metal.AddressFamily]uint64 `json:"used_ips" description:"the total used IPs" readonly:"true"` + AvailablePrefixes map[metal.AddressFamily]uint64 `json:"available_prefixes" description:"the total available 2 bit Prefixes" readonly:"true"` + UsedPrefixes map[metal.AddressFamily]uint64 `json:"used_prefixes" description:"the total used Prefixes" readonly:"true"` } // NetworkCreateRequest is used to create a new Network. @@ -47,42 +48,22 @@ type NetworkCreateRequest struct { type NetworkAllocateRequest struct { Describable 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"` + 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"` + Length metal.ChildPrefixLength `json:"length" description:"the bitlen of the prefix to allocate, defaults to defaultchildprefixlength of super prefix"` + ParentNetworkID *string `json:"parentnetworkid" description:"the parent network from which this network should be allocated"` + AddressFamily *metal.AddressFamily `json:"addressfamily" description:"the addressfamily to allocate a child network defaults. If not specified, the child network inherits the addressfamilies from the parent." enum:"IPv4|IPv6"` } -// 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 { +func ToAddressFamily(af string) metal.AddressFamily { switch af { case "IPv4", "ipv4": - return IPv4AddressFamily + return metal.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 metal.IPv6AddressFamily } - return IPv4AddressFamily + return metal.IPv4AddressFamily } // NetworkFindRequest is used to find a Network with different criteria. @@ -114,7 +95,10 @@ func NewNetworkResponse(network *metal.Network, usage *metal.NetworkUsage) *Netw return nil } - var parentNetworkID *string + var ( + parentNetworkID *string + ) + if network.ParentNetworkID != "" { parentNetworkID = &network.ParentNetworkID } @@ -148,7 +132,7 @@ func NewNetworkResponse(network *metal.Network, usage *metal.NetworkUsage) *Netw Underlay: network.Underlay, Vrf: &network.Vrf, ParentNetworkID: parentNetworkID, - AddressFamily: ToAddressFamily(string(network.AddressFamily)), + AddressFamilies: network.AddressFamilies, }, Usage: NetworkUsage{ AvailableIPs: usage.AvailableIPs, diff --git a/cmd/metal-api/internal/testdata/ipam.go b/cmd/metal-api/internal/testdata/ipam.go index ca782319..a06b786c 100644 --- a/cmd/metal-api/internal/testdata/ipam.go +++ b/cmd/metal-api/internal/testdata/ipam.go @@ -36,7 +36,8 @@ func InitMockIpamData(dbMock *r.Mock, withIP bool) (ipam.IPAMer, error) { Name: "IPAM Network", Description: "description IPAM", }, - Prefixes: prefixesIPAM, + Prefixes: prefixesIPAM, + AddressFamilies: metal.AddressFamilies{metal.IPv4AddressFamily: true}, } // now, let's get an ip from the IPAM for IPAMIP diff --git a/cmd/metal-api/internal/testdata/testdata.go b/cmd/metal-api/internal/testdata/testdata.go index f3ea6c97..b96d76c6 100644 --- a/cmd/metal-api/internal/testdata/testdata.go +++ b/cmd/metal-api/internal/testdata/testdata.go @@ -4,7 +4,6 @@ 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" @@ -286,8 +285,8 @@ var ( 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) + cpl1 = metal.ChildPrefixLength{metal.IPv4AddressFamily: 28} + cpl2 = metal.ChildPrefixLength{metal.IPv4AddressFamily: 22} prefixes1 = []metal.Prefix{prefix1, prefix2} prefixes2 = []metal.Prefix{prefix2} @@ -303,8 +302,8 @@ var ( PartitionID: Partition1.ID, Prefixes: prefixes1, PrivateSuper: true, - DefaultChildPrefixLength: &cpl1, - AddressFamily: metal.IPv4AddressFamily, + DefaultChildPrefixLength: cpl1, + AddressFamilies: metal.AddressFamilies{metal.IPv4AddressFamily: true}, } Nw2 = metal.Network{ Base: metal.Base{ @@ -315,8 +314,8 @@ var ( PartitionID: Partition1.ID, Prefixes: prefixes2, Underlay: true, - DefaultChildPrefixLength: &cpl2, - AddressFamily: metal.IPv4AddressFamily, + DefaultChildPrefixLength: cpl2, + AddressFamilies: metal.AddressFamilies{metal.IPv4AddressFamily: true}, } Nw3 = metal.Network{ Base: metal.Base{ @@ -327,7 +326,7 @@ var ( Prefixes: prefixes3, PartitionID: Partition1.ID, ParentNetworkID: Nw1.ID, - AddressFamily: metal.IPv4AddressFamily, + AddressFamilies: metal.AddressFamilies{metal.IPv4AddressFamily: true}, } Partition1PrivateSuperNetwork = metal.Network{ @@ -336,7 +335,7 @@ var ( }, Prefixes: metal.Prefixes{superPrefix}, PartitionID: Partition1.ID, - DefaultChildPrefixLength: pointer.Pointer(uint8(22)), + DefaultChildPrefixLength: metal.ChildPrefixLength{metal.IPv4AddressFamily: 22}, ParentNetworkID: "", ProjectID: "", PrivateSuper: true, @@ -350,8 +349,8 @@ var ( }, Prefixes: metal.Prefixes{superPrefix}, PartitionID: Partition2.ID, - DefaultChildPrefixLength: pointer.Pointer(uint8(22)), - AddressFamily: metal.IPv4AddressFamily, + DefaultChildPrefixLength: metal.ChildPrefixLength{metal.IPv4AddressFamily: 22}, + AddressFamilies: metal.AddressFamilies{metal.IPv4AddressFamily: true}, ParentNetworkID: "", ProjectID: "", PrivateSuper: true, @@ -365,8 +364,23 @@ var ( }, Prefixes: metal.Prefixes{superPrefixV6}, PartitionID: Partition2.ID, - DefaultChildPrefixLength: pointer.Pointer(uint8(64)), - AddressFamily: metal.IPv6AddressFamily, + DefaultChildPrefixLength: metal.ChildPrefixLength{metal.IPv6AddressFamily: 64}, + AddressFamilies: metal.AddressFamilies{metal.IPv6AddressFamily: true}, + ParentNetworkID: "", + ProjectID: "", + PrivateSuper: true, + Nat: false, + Underlay: false, + } + + Partition4PrivateSuperNetworkMixed = metal.Network{ + Base: metal.Base{ + ID: "super-tenant-network-2-mixed", + }, + Prefixes: metal.Prefixes{superPrefix, superPrefixV6}, + PartitionID: "4", + DefaultChildPrefixLength: metal.ChildPrefixLength{metal.IPv4AddressFamily: 22, metal.IPv6AddressFamily: 64}, + AddressFamilies: metal.AddressFamilies{metal.IPv4AddressFamily: true, metal.IPv6AddressFamily: true}, ParentNetworkID: "", ProjectID: "", PrivateSuper: true, @@ -484,7 +498,8 @@ var ( Name: "IPAM Network", Description: "description IPAM", }, - Prefixes: prefixesIPAM, + Prefixes: prefixesIPAM, + AddressFamilies: metal.AddressFamilies{metal.IPv4AddressFamily: true}, } // IPs @@ -492,21 +507,21 @@ var ( IPAddress: "1.2.3.4", Name: "Image 1", Description: "description 1", - Type: "ephemeral", + Type: metal.Ephemeral, ProjectID: "1", } IP2 = metal.IP{ IPAddress: "2.3.4.5", Name: "Image 2", Description: "description 2", - Type: "static", + Type: metal.Static, ProjectID: "1", } IP3 = metal.IP{ IPAddress: "3.4.5.6", Name: "Image 3", Description: "description 3", - Type: "static", + Type: metal.Static, Tags: []string{tag.MachineID}, ProjectID: "1", } @@ -514,7 +529,7 @@ var ( IPAddress: "2001:0db8:85a3::1", Name: "IPv6 4", Description: "description 4", - Type: "ephemeral", + Type: metal.Ephemeral, ProjectID: "1", } IPAMIP = metal.IP{ @@ -565,7 +580,13 @@ var ( Description: "description 3", }, } - + Partition4 = metal.Partition{ + Base: metal.Base{ + ID: "4", + Name: "partition4", + Description: "description 4", + }, + } // Switches Switch1 = metal.Switch{ Base: metal.Base{ @@ -841,6 +862,8 @@ func InitMockDBData(mock *r.Mock) { mock.On(r.DB("mockdb").Table("partition").Get("1")).Return(Partition1, nil) mock.On(r.DB("mockdb").Table("partition").Get("2")).Return(Partition2, nil) mock.On(r.DB("mockdb").Table("partition").Get("3")).Return(Partition3, nil) + mock.On(r.DB("mockdb").Table("partition").Get("4")).Return(Partition4, nil) + mock.On(r.DB("mockdb").Table("partition").Get("404")).Return(nil, errors.New("Test Error")) mock.On(r.DB("mockdb").Table("partition").Get("999")).Return(nil, nil) mock.On(r.DB("mockdb").Table("image").Get("image-1")).Return(Img1, nil) @@ -867,20 +890,16 @@ func InitMockDBData(mock *r.Mock) { 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) }).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")) + 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("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) + func(var_4 r.Term) r.Term { return var_4.Field("privatesuper").Eq(true) })).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) + func(var_3 r.Term) r.Term { return var_3.Field("partitionid").Eq("3") }).Filter( + func(var_4 r.Term) r.Term { return var_4.Field("privatesuper").Eq(true) })).Return(nil, nil) + mock.On(r.DB("mockdb").Table("network").Filter( + func(var_3 r.Term) r.Term { return var_3.Field("partitionid").Eq("4") }).Filter( + func(var_4 r.Term) r.Term { return var_4.Field("privatesuper").Eq(true) })).Return(Partition4PrivateSuperNetworkMixed, nil) mock.On(r.DB("mockdb").Table("ip").Get("1.2.3.4")).Return(IP1, nil) mock.On(r.DB("mockdb").Table("ip").Get("2.3.4.5")).Return(IP2, nil) diff --git a/spec/metal-api.json b/spec/metal-api.json index caa8a9b3..2ebf1dbf 100644 --- a/spec/metal-api.json +++ b/spec/metal-api.json @@ -255,9 +255,6 @@ }, "datastore.NetworkSearchQuery": { "properties": { - "addressfamily": { - "type": "string" - }, "destinationprefixes": { "items": { "type": "string" @@ -1510,6 +1507,14 @@ }, "v1.IPAllocateRequest": { "properties": { + "addressfamily": { + "description": "the addressfamily to allocate a ip address from the given network, defaults to IPv4", + "enum": [ + "IPv4", + "IPv6" + ], + "type": "string" + }, "description": { "description": "a description for this entity", "type": "string" @@ -1547,6 +1552,7 @@ } }, "required": [ + "addressfamily", "networkid", "projectid", "type" @@ -3571,8 +3577,12 @@ }, "v1.NetworkAllocateRequest": { "properties": { - "address_family": { - "description": "can be ipv4 or ipv6, defaults to ipv4", + "addressfamily": { + "description": "the addressfamily to allocate a child network defaults. If not specified, the child network inherits the addressfamilies from the parent.", + "enum": [ + "IPv4", + "IPv6" + ], "type": "string" }, "description": { @@ -3594,9 +3604,11 @@ "type": "object" }, "length": { + "additionalProperties": { + "type": "integer" + }, "description": "the bitlen of the prefix to allocate, defaults to defaultchildprefixlength of super prefix", - "format": "byte", - "type": "integer" + "type": "object" }, "name": { "description": "a readable name for this entity", @@ -3606,6 +3618,10 @@ "description": "if set to true, packets leaving this network get masqueraded behind interface ip", "type": "boolean" }, + "parentnetworkid": { + "description": "the parent network from which this network should be allocated", + "type": "string" + }, "partitionid": { "description": "the partition this network belongs to", "type": "string" @@ -3620,8 +3636,9 @@ } }, "required": [ - "address_family", - "length" + "addressfamily", + "length", + "parentnetworkid" ] }, "v1.NetworkBase": { @@ -3650,17 +3667,18 @@ "v1.NetworkCreateRequest": { "properties": { "addressfamily": { - "description": "the addressfamily either IPv4 or IPv6 of this network", - "enum": [ - "IPv4", - "IPv6" - ], - "type": "string" + "additionalProperties": { + "type": "boolean" + }, + "description": "the addressfamilies in this network, either IPv4 or IPv6 or both", + "type": "object" }, "defaultchildprefixlength": { - "description": "if privatesuper, this defines the bitlen of child prefixes if not nil", - "format": "byte", - "type": "integer" + "additionalProperties": { + "type": "integer" + }, + "description": "if privatesuper, this defines the bitlen of child prefixes per addressfamily if not nil", + "type": "object" }, "description": { "description": "a description for this entity", @@ -3745,9 +3763,6 @@ }, "v1.NetworkFindRequest": { "properties": { - "addressfamily": { - "type": "string" - }, "destinationprefixes": { "items": { "type": "string" @@ -3800,17 +3815,18 @@ "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" + "additionalProperties": { + "type": "boolean" + }, + "description": "the addressfamilies in this network, either IPv4 or IPv6 or both", + "type": "object" }, "defaultchildprefixlength": { - "description": "if privatesuper, this defines the bitlen of child prefixes if not nil", - "format": "byte", - "type": "integer" + "additionalProperties": { + "type": "integer" + }, + "description": "if privatesuper, this defines the bitlen of child prefixes per addressfamily if not nil", + "type": "object" }, "destinationprefixes": { "description": "the destination prefixes of this network", @@ -3864,12 +3880,11 @@ "v1.NetworkResponse": { "properties": { "addressfamily": { - "description": "the addressfamily either IPv4 or IPv6 of this network", - "enum": [ - "IPv4", - "IPv6" - ], - "type": "string" + "additionalProperties": { + "type": "boolean" + }, + "description": "the addressfamilies in this network, either IPv4 or IPv6 or both", + "type": "object" }, "changed": { "description": "the last changed timestamp of this entity", @@ -3884,9 +3899,11 @@ "type": "string" }, "defaultchildprefixlength": { - "description": "if privatesuper, this defines the bitlen of child prefixes if not nil", - "format": "byte", - "type": "integer" + "additionalProperties": { + "type": "integer" + }, + "description": "if privatesuper, this defines the bitlen of child prefixes per addressfamily if not nil", + "type": "object" }, "description": { "description": "a description for this entity",