Skip to content

Commit

Permalink
IPv6 Support
Browse files Browse the repository at this point in the history
  • Loading branch information
majst01 committed Jul 4, 2024
1 parent 1bf4e40 commit 0b8af54
Show file tree
Hide file tree
Showing 11 changed files with 527 additions and 60 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package migrations

import (
r "gopkg.in/rethinkdb/rethinkdb-go.v6"

"github.com/metal-stack/metal-api/cmd/metal-api/internal/datastore"
)

func init() {
type tmpPartition struct {
PrivateNetworkPrefixLength uint8 `rethinkdb:"privatenetworkprefixlength"`
}
datastore.MustRegisterMigration(datastore.Migration{
Name: "migrate partition.childprefixlength to tenant super network",
Version: 6,
Up: func(db *r.Term, session r.QueryExecutor, rs *datastore.RethinkStore) error {
nws, err := rs.ListNetworks()
if err != nil {
return err
}

for _, old := range nws {
if !old.PrivateSuper {
continue
}

cursor, err := db.Table("partition").Get(old.PartitionID).Run(session)
if err != nil {
return err
}
var partition tmpPartition
err = cursor.One(&partition)
if err != nil {
return err
}

new := old
new.ChildPrefixLength = &partition.PrivateNetworkPrefixLength
err = rs.UpdateNetwork(&old, &new)
if err != nil {
return err
}
err = cursor.Close()
if err != nil {
return err
}
}

_, err = db.Table("partition").Replace(r.Row.Without("privatenetworkprefixlength")).RunWrite(session)
return err
},
})
}
1 change: 1 addition & 0 deletions cmd/metal-api/internal/metal/network.go
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ type Network struct {
Base
Prefixes Prefixes `rethinkdb:"prefixes" json:"prefixes"`
DestinationPrefixes Prefixes `rethinkdb:"destinationprefixes" json:"destinationprefixes"`
ChildPrefixLength *uint8 `rethinkdb:"childprefixlength" json:"childprefixlength" description:"if privatesuper, this defines the bitlen of child prefixes if not nil"`
PartitionID string `rethinkdb:"partitionid" json:"partitionid"`
ProjectID string `rethinkdb:"projectid" json:"projectid"`
ParentNetworkID string `rethinkdb:"parentnetworkid" json:"parentnetworkid"`
Expand Down
7 changes: 3 additions & 4 deletions cmd/metal-api/internal/metal/partition.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,9 @@ package metal
// A Partition represents a location.
type Partition struct {
Base
BootConfiguration BootConfiguration `rethinkdb:"bootconfig" json:"bootconfig"`
MgmtServiceAddress string `rethinkdb:"mgmtserviceaddr" json:"mgmtserviceaddr"`
PrivateNetworkPrefixLength uint8 `rethinkdb:"privatenetworkprefixlength" json:"privatenetworkprefixlength"`
Labels map[string]string `rethinkdb:"labels" json:"labels"`
BootConfiguration BootConfiguration `rethinkdb:"bootconfig" json:"bootconfig"`
MgmtServiceAddress string `rethinkdb:"mgmtserviceaddr" json:"mgmtserviceaddr"`
Labels map[string]string `rethinkdb:"labels" json:"labels"`
}

// BootConfiguration defines the metal-hammer initrd, kernel and commandline
Expand Down
29 changes: 28 additions & 1 deletion cmd/metal-api/internal/service/ip-service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,15 @@ func TestGetIPs(t *testing.T) {
err = json.NewDecoder(resp.Body).Decode(&result)

require.NoError(t, err)
require.Len(t, result, 3)
require.Len(t, result, 4)
require.Equal(t, testdata.IP1.IPAddress, result[0].IPAddress)
require.Equal(t, testdata.IP1.Name, *result[0].Name)
require.Equal(t, testdata.IP2.IPAddress, result[1].IPAddress)
require.Equal(t, testdata.IP2.Name, *result[1].Name)
require.Equal(t, testdata.IP3.IPAddress, result[2].IPAddress)
require.Equal(t, testdata.IP3.Name, *result[2].Name)
require.Equal(t, testdata.IP4.IPAddress, result[3].IPAddress)
require.Equal(t, testdata.IP4.Name, *result[3].Name)
}

func TestGetIP(t *testing.T) {
Expand All @@ -85,6 +87,31 @@ func TestGetIP(t *testing.T) {
require.Equal(t, testdata.IP1.Name, *result.Name)
}

func TestGetIPv6(t *testing.T) {
ds, mock := datastore.InitMockDB(t)
testdata.InitMockDBData(mock)

logger := slog.Default()
ipservice, err := NewIP(logger, ds, bus.DirectEndpoints(), ipam.InitTestIpam(t), nil)
require.NoError(t, err)
container := restful.NewContainer().Add(ipservice)
req := httptest.NewRequest("GET", "/v1/ip/2001:0db8:85a3::1", nil)
container = injectViewer(logger, container, req)
w := httptest.NewRecorder()
container.ServeHTTP(w, req)

resp := w.Result()
defer resp.Body.Close()

require.Equal(t, http.StatusOK, resp.StatusCode, w.Body.String())
var result v1.IPResponse
err = json.NewDecoder(resp.Body).Decode(&result)

require.NoError(t, err)
require.Equal(t, testdata.IP4.IPAddress, result.IPAddress)
require.Equal(t, testdata.IP4.Name, *result.Name)
}

func TestGetIPNotFound(t *testing.T) {
ds, mock := datastore.InitMockDB(t)
testdata.InitMockDBData(mock)
Expand Down
114 changes: 106 additions & 8 deletions cmd/metal-api/internal/service/network-service.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"
"log/slog"
"net/http"
"net/netip"

mdmv1 "github.com/metal-stack/masterdata-api/api/v1"
mdm "github.com/metal-stack/masterdata-api/pkg/client"
Expand Down Expand Up @@ -205,6 +206,7 @@ func (r *networkResource) findNetworks(request *restful.Request, response *restf
r.send(request, response, http.StatusOK, result)
}

// TODO allow creation of networks with childprefixlength which are not privatesuper
func (r *networkResource) createNetwork(request *restful.Request, response *restful.Response) {
var requestPayload v1.NetworkCreateRequest
err := request.ReadEntity(&requestPayload)
Expand Down Expand Up @@ -260,18 +262,34 @@ func (r *networkResource) createNetwork(request *restful.Request, response *rest
}

prefixes := metal.Prefixes{}
// all Prefixes must be valid
addressFamilies := make(map[string]bool)
// all Prefixes must be valid and from the same addressfamily
for i := range requestPayload.Prefixes {
p := requestPayload.Prefixes[i]
prefix, err := metal.NewPrefixFromCIDR(p)
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
}

ipprefix, err := netip.ParsePrefix(p)
if err != nil {
r.sendError(request, response, httperrors.BadRequest(fmt.Errorf("given prefix %v is not a valid ip with mask: %w", p, err)))
return
}
if ipprefix.Addr().Is4() {
addressFamilies["ipv4"] = true
}
if ipprefix.Addr().Is6() {
addressFamilies["ipv6"] = true
}
prefixes = append(prefixes, *prefix)
}

if len(addressFamilies) > 1 {
r.sendError(request, response, httperrors.BadRequest(fmt.Errorf("given prefixes have different addressfamilies")))
return
}

destPrefixes := metal.Prefixes{}
for i := range requestPayload.DestinationPrefixes {
p := requestPayload.DestinationPrefixes[i]
Expand Down Expand Up @@ -324,17 +342,38 @@ func (r *networkResource) createNetwork(request *restful.Request, response *rest
return
}

// We should support two private super per partition, one per addressfamily
// the network allocate request must be configurable with addressfamily
if privateSuper {
boolTrue := true
err := r.ds.FindNetwork(&datastore.NetworkSearchQuery{PartitionID: &partition.ID, PrivateSuper: &boolTrue}, &metal.Network{})
var nw metal.Network
err := r.ds.FindNetwork(&datastore.NetworkSearchQuery{PartitionID: &partition.ID, PrivateSuper: &boolTrue}, &nw)
if err != nil {
if !metal.IsNotFound(err) {
r.sendError(request, response, defaultError(err))
return
}
} else {
r.sendError(request, response, defaultError(fmt.Errorf("partition with id %q already has a private super network", partition.ID)))
return
if len(nw.Prefixes) != 0 {
existingsuper := nw.Prefixes[0].String()

ipprefxexistingsuper, err := netip.ParsePrefix(existingsuper)
if err != nil {
r.sendError(request, response, httperrors.BadRequest(fmt.Errorf("given prefix %v is not valid: %w", existingsuper, err)))
return

}
newsuper := prefixes[0].String()
ipprefixnew, err := netip.ParsePrefix(newsuper)
if err != nil {
r.sendError(request, response, httperrors.BadRequest(fmt.Errorf("given prefix %v is not valid: %w", newsuper, err)))
return
}
if (ipprefixnew.Addr().Is4() && ipprefxexistingsuper.Addr().Is4()) || (ipprefixnew.Addr().Is6() && ipprefxexistingsuper.Addr().Is6()) {
r.sendError(request, response, httperrors.BadRequest(fmt.Errorf("partition with id %q already has a private super network for this addressfamily", partition.ID)))
return
}
}
}
}
if underlay {
Expand Down Expand Up @@ -389,6 +428,23 @@ func (r *networkResource) createNetwork(request *restful.Request, response *rest
Labels: labels,
}

// check if childprefixlength is set and matches addressfamily
if requestPayload.ChildPrefixLength != nil && privateSuper {
cpl := *requestPayload.ChildPrefixLength
for _, p := range prefixes {
ipprefix, err := netip.ParsePrefix(p.String())
if err != nil {
r.sendError(request, response, httperrors.BadRequest(fmt.Errorf("given prefix %v is not a valid ip with mask: %w", p, err)))
return
}
if cpl <= uint8(ipprefix.Bits()) {
r.sendError(request, response, httperrors.BadRequest(fmt.Errorf("given childprefixlength %d is not greater than prefix length of:%s", cpl, p.String())))
return
}
}
nw.ChildPrefixLength = requestPayload.ChildPrefixLength
}

ctx := request.Request.Context()
for _, p := range nw.Prefixes {
err := r.ipamer.CreatePrefix(ctx, p)
Expand All @@ -409,6 +465,7 @@ func (r *networkResource) createNetwork(request *restful.Request, response *rest
r.send(request, response, http.StatusCreated, v1.NewNetworkResponse(nw, usage))
}

// TODO add possibility to allocate from a non super network if given in the AllocateRequest and super has childprefixlength
func (r *networkResource) allocateNetwork(request *restful.Request, response *restful.Response) {
var requestPayload v1.NetworkAllocateRequest
err := request.ReadEntity(&requestPayload)
Expand Down Expand Up @@ -463,9 +520,9 @@ func (r *networkResource) allocateNetwork(request *restful.Request, response *re
return
}

var superNetwork metal.Network
var superNetworks metal.Networks
boolTrue := true
err = r.ds.FindNetwork(&datastore.NetworkSearchQuery{PartitionID: &partition.ID, PrivateSuper: &boolTrue}, &superNetwork)
err = r.ds.SearchNetworks(&datastore.NetworkSearchQuery{PartitionID: &partition.ID, PrivateSuper: &boolTrue}, &superNetworks)
if err != nil {
r.sendError(request, response, defaultError(err))
return
Expand All @@ -482,6 +539,37 @@ func (r *networkResource) allocateNetwork(request *restful.Request, response *re
destPrefixes = append(destPrefixes, *prefix)
}

addressFamily := v1.IPv4AddressFamily
if requestPayload.AddressFamily != nil {
addressFamily = v1.ToAddressFamily(*requestPayload.AddressFamily)
}

r.log.Info("network allocate", "family", addressFamily)
var (
superNetwork metal.Network
superNetworkFound bool
)
for _, snw := range superNetworks {
ipprefix, err := netip.ParsePrefix(snw.Prefixes[0].String())
if err != nil {
r.sendError(request, response, httperrors.BadRequest(err))
return
}
if addressFamily == v1.IPv4AddressFamily && ipprefix.Addr().Is4() {
superNetwork = snw
superNetworkFound = true
}
if addressFamily == v1.IPv6AddressFamily && ipprefix.Addr().Is6() {
superNetwork = snw
superNetworkFound = true
}
}
if !superNetworkFound {
r.sendError(request, response, httperrors.BadRequest(fmt.Errorf("no supernetwork for addressfamily:%s found", addressFamily)))
return
}
r.log.Info("network allocate", "supernetwork", superNetwork.ID)

nwSpec := &metal.Network{
Base: metal.Base{
Name: name,
Expand All @@ -494,8 +582,18 @@ func (r *networkResource) allocateNetwork(request *restful.Request, response *re
Shared: shared,
Nat: nat,
}
if superNetwork.ChildPrefixLength == nil {
r.sendError(request, response, httperrors.BadRequest(fmt.Errorf("supernetwork %s has no childprefixlength specified", superNetwork.ID)))
}

// Allow configurable prefix length
length := *superNetwork.ChildPrefixLength
if requestPayload.Length != nil {
length = *requestPayload.Length
}

ctx := request.Request.Context()
nw, err := createChildNetwork(ctx, r.ds, r.ipamer, nwSpec, &superNetwork, partition.PrivateNetworkPrefixLength)
nw, err := createChildNetwork(ctx, r.ds, r.ipamer, nwSpec, &superNetwork, length)
if err != nil {
r.sendError(request, response, defaultError(err))
return
Expand Down
Loading

0 comments on commit 0b8af54

Please sign in to comment.