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

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
1 parent 89a0857 commit 0c53831
Showing 17 changed files with 568 additions and 411 deletions.
Original file line number Diff line number Diff line change
@@ -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 {
Original file line number Diff line number Diff line change
@@ -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
@@ -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
32 changes: 26 additions & 6 deletions cmd/metal-api/internal/ipam/ipam.go
Original file line number Diff line number Diff line change
@@ -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
}

21 changes: 12 additions & 9 deletions cmd/metal-api/internal/metal/network.go
Original file line number Diff line number Diff line change
@@ -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.
6 changes: 2 additions & 4 deletions cmd/metal-api/internal/service/integration_test.go
Original file line number Diff line number Diff line change
@@ -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)
40 changes: 36 additions & 4 deletions cmd/metal-api/internal/service/ip-service.go
Original file line number Diff line number Diff line change
@@ -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
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
@@ -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)
Loading

0 comments on commit 0c53831

Please sign in to comment.