Skip to content

Commit

Permalink
Support DualStack Networks
Browse files Browse the repository at this point in the history
  • Loading branch information
majst01 committed Jul 23, 2024
1 parent 89a0857 commit 0c53831
Show file tree
Hide file tree
Showing 17 changed files with 568 additions and 411 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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])
}
31 changes: 12 additions & 19 deletions cmd/metal-api/internal/datastore/network.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down
32 changes: 26 additions & 6 deletions cmd/metal-api/internal/ipam/ipam.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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
}

Expand Down
21 changes: 12 additions & 9 deletions cmd/metal-api/internal/metal/network.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"`
Expand All @@ -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
Expand Down Expand Up @@ -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.
Expand Down
6 changes: 2 additions & 4 deletions cmd/metal-api/internal/service/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down
40 changes: 36 additions & 4 deletions cmd/metal-api/internal/service/ip-service.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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
Expand Down
32 changes: 32 additions & 0 deletions cmd/metal-api/internal/service/ip-service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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)
Expand Down
Loading

0 comments on commit 0c53831

Please sign in to comment.