diff --git a/cmd/metal-api/internal/datastore/migrations/08_childprefixlength.go b/cmd/metal-api/internal/datastore/migrations/08_childprefixlength.go new file mode 100644 index 00000000..56103bf7 --- /dev/null +++ b/cmd/metal-api/internal/datastore/migrations/08_childprefixlength.go @@ -0,0 +1,90 @@ +package migrations + +import ( + "net/netip" + + r "gopkg.in/rethinkdb/rethinkdb-go.v6" + + "github.com/metal-stack/metal-api/cmd/metal-api/internal/datastore" + "github.com/metal-stack/metal-api/cmd/metal-api/internal/metal" +) + +func init() { + type tmpPartition struct { + PrivateNetworkPrefixLength uint8 `rethinkdb:"privatenetworkprefixlength"` + } + datastore.MustRegisterMigration(datastore.Migration{ + Name: "migrate partition.childprefixlength to tenant super network", + Version: 8, + 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 { + cursor, err := db.Table("partition").Get(old.PartitionID).Run(session) + if err != nil { + return err + } + if cursor.IsNil() { + _ = cursor.Close() + continue + } + var partition tmpPartition + err = cursor.One(&partition) + if err != nil { + _ = cursor.Close() + return err + } + err = cursor.Close() + if err != nil { + return err + } + new := old + + var ( + af metal.AddressFamily + defaultChildPrefixLength = metal.ChildPrefixLength{} + ) + // FIXME check all prefixes + parsed, err := netip.ParsePrefix(new.Prefixes[0].String()) + if err != nil { + return err + } + if parsed.Addr().Is4() { + af = metal.IPv4AddressFamily + defaultChildPrefixLength[af] = 22 + } + if parsed.Addr().Is6() { + af = metal.IPv6AddressFamily + defaultChildPrefixLength[af] = 64 + } + + if new.AddressFamilies == nil { + new.AddressFamilies = make(map[metal.AddressFamily]bool) + } + new.AddressFamilies[af] = true + + if new.PrivateSuper { + if new.DefaultChildPrefixLength == nil { + new.DefaultChildPrefixLength = make(map[metal.AddressFamily]uint8) + } + if partition.PrivateNetworkPrefixLength > 0 { + new.DefaultChildPrefixLength[af] = partition.PrivateNetworkPrefixLength + } else { + new.DefaultChildPrefixLength = defaultChildPrefixLength + } + + } + err = rs.UpdateNetwork(&old, &new) + if err != nil { + return err + } + } + + _, err = db.Table("partition").Replace(r.Row.Without("privatenetworkprefixlength")).RunWrite(session) + return err + }, + }) +} diff --git a/cmd/metal-api/internal/datastore/migrations_integration/migrate_integration_test.go b/cmd/metal-api/internal/datastore/migrations_integration/migrate_integration_test.go index 07be572c..640401a8 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 @@ -31,7 +31,7 @@ func Test_Migration(t *testing.T) { _ = container.Terminate(context.Background()) }() - log := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelError})) + log := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo})) rs := datastore.New(log, c.IP+":"+c.Port, c.DB, c.User, c.Password) rs.VRFPoolRangeMin = 10000 @@ -191,3 +191,134 @@ func Test_Migration(t *testing.T) { assert.Equal(t, ec.Events[0].Time.Unix(), lastEventTime.Unix()) assert.Equal(t, ec.Events[1].Time.Unix(), now.Unix()) } + +func Test_MigrationChildPrefixLength(t *testing.T) { + type tmpPartition struct { + ID string `rethinkdb:"id"` + PrivateNetworkPrefixLength uint8 `rethinkdb:"privatenetworkprefixlength"` + } + + container, c, err := test.StartRethink(t) + require.NoError(t, err) + defer func() { + _ = container.Terminate(context.Background()) + }() + + log := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug})) + + rs := datastore.New(log, c.IP+":"+c.Port, c.DB, c.User, c.Password) + // limit poolsize to speed up initialization + rs.VRFPoolRangeMin = 10000 + rs.VRFPoolRangeMax = 10010 + rs.ASNPoolRangeMin = 10000 + rs.ASNPoolRangeMax = 10010 + + err = rs.Connect() + require.NoError(t, err) + err = rs.Initialize() + require.NoError(t, err) + + var ( + p1 = &tmpPartition{ + ID: "p1", + PrivateNetworkPrefixLength: 22, + } + p2 = &tmpPartition{ + ID: "p2", + PrivateNetworkPrefixLength: 24, + } + p3 = &tmpPartition{ + ID: "p3", + } + n1 = &metal.Network{ + Base: metal.Base{ + ID: "n1", + }, + PartitionID: "p1", + Prefixes: metal.Prefixes{ + {IP: "10.0.0.0", Length: "8"}, + }, + PrivateSuper: true, + } + n2 = &metal.Network{ + Base: metal.Base{ + ID: "n2", + }, + Prefixes: metal.Prefixes{ + {IP: "2001::", Length: "64"}, + }, + PartitionID: "p2", + PrivateSuper: true, + } + n3 = &metal.Network{ + Base: metal.Base{ + ID: "n3", + }, + Prefixes: metal.Prefixes{ + {IP: "100.1.0.0", Length: "22"}, + }, + PartitionID: "p2", + PrivateSuper: false, + } + n4 = &metal.Network{ + Base: metal.Base{ + ID: "n4", + }, + Prefixes: metal.Prefixes{ + {IP: "100.1.0.0", Length: "22"}, + }, + PartitionID: "p3", + PrivateSuper: true, + } + ) + _, err = r.DB("metal").Table("partition").Insert(p1).RunWrite(rs.Session()) + require.NoError(t, err) + _, err = r.DB("metal").Table("partition").Insert(p2).RunWrite(rs.Session()) + require.NoError(t, err) + _, err = r.DB("metal").Table("partition").Insert(p3).RunWrite(rs.Session()) + require.NoError(t, err) + + err = rs.CreateNetwork(n1) + require.NoError(t, err) + err = rs.CreateNetwork(n2) + require.NoError(t, err) + err = rs.CreateNetwork(n3) + require.NoError(t, err) + err = rs.CreateNetwork(n4) + require.NoError(t, err) + + err = rs.Migrate(nil, false) + require.NoError(t, err) + + p, err := rs.FindPartition(p1.ID) + require.NoError(t, err) + require.NotNil(t, p) + p, err = rs.FindPartition(p2.ID) + require.NoError(t, err) + require.NotNil(t, p) + + n1fetched, err := rs.FindNetworkByID(n1.ID) + require.NoError(t, err) + require.NotNil(t, n1fetched) + require.Equal(t, p1.PrivateNetworkPrefixLength, n1fetched.DefaultChildPrefixLength[metal.IPv4AddressFamily], "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[metal.IPv6AddressFamily], "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.True(t, n3fetched.AddressFamilies[metal.IPv4AddressFamily]) + + n4fetched, err := rs.FindNetworkByID(n4.ID) + require.NoError(t, err) + require.NotNil(t, n4fetched) + require.NotNil(t, n4fetched.DefaultChildPrefixLength) + require.True(t, n4fetched.AddressFamilies[metal.IPv4AddressFamily]) + require.Equal(t, uint8(22), n4fetched.DefaultChildPrefixLength[metal.IPv4AddressFamily]) +} diff --git a/cmd/metal-api/internal/datastore/rethinkdb.go b/cmd/metal-api/internal/datastore/rethinkdb.go index ea21a0ff..06238a84 100644 --- a/cmd/metal-api/internal/datastore/rethinkdb.go +++ b/cmd/metal-api/internal/datastore/rethinkdb.go @@ -384,7 +384,7 @@ func (rs *RethinkStore) findEntity(query *r.Term, entity interface{}) error { } defer res.Close() if res.IsNil() { - return metal.NotFound("no %v with found", getEntityName(entity)) + return metal.NotFound("no %v found", getEntityName(entity)) } hasResult := res.Next(entity) diff --git a/cmd/metal-api/internal/ipam/ipam.go b/cmd/metal-api/internal/ipam/ipam.go index 3780ef8f..e65c0c59 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/pkg/healthstatus" @@ -51,10 +52,10 @@ func (i *ipam) AllocateChildPrefix(ctx context.Context, parentPrefix metal.Prefi Length: uint32(childLength), })) if err != nil { - return nil, fmt.Errorf("error creating new prefix in ipam: %w", err) + return nil, fmt.Errorf("error creating new prefix from:%s in ipam: %w", parentPrefix.String(), 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/machine.go b/cmd/metal-api/internal/metal/machine.go index 3226d64b..fbf5ba39 100644 --- a/cmd/metal-api/internal/metal/machine.go +++ b/cmd/metal-api/internal/metal/machine.go @@ -235,9 +235,6 @@ func (r IngressRule) Validate() error { if err := validateCIDRs(r.From); err != nil { return err } - if err := validateCIDRs(slices.Concat(r.From, r.To)); err != nil { - return err - } return nil } @@ -279,7 +276,7 @@ func validateCIDRs(cidrs []string) error { var newaf string if p.Addr().Is4() { newaf = "ipv4" - } else { + } else if p.Addr().Is6() { newaf = "ipv6" } if af != "" && af != newaf { diff --git a/cmd/metal-api/internal/metal/network.go b/cmd/metal-api/internal/metal/network.go index 117e0772..5a4f6e1c 100644 --- a/cmd/metal-api/internal/metal/network.go +++ b/cmd/metal-api/internal/metal/network.go @@ -172,17 +172,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 @@ -209,6 +209,7 @@ type Network struct { Base Prefixes Prefixes `rethinkdb:"prefixes" json:"prefixes"` DestinationPrefixes Prefixes `rethinkdb:"destinationprefixes" json:"destinationprefixes"` + 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"` @@ -218,7 +219,32 @@ type Network struct { Underlay bool `rethinkdb:"underlay" json:"underlay"` Shared bool `rethinkdb:"shared" json:"shared"` Labels map[string]string `rethinkdb:"labels" json:"labels"` - AdditionalAnnouncableCIDRs []string `rethinkdb:"additionalannouncablecidrs" json:"additionalannouncablecidrs" description:"list of cidrs which are added to the route maps per tenant private network, these are typically pod- and service cidrs, can only be set for private super networks"` + AddressFamilies AddressFamilies `rethinkdb:"addressfamily" json:"addressfamily"` + AdditionalAnnouncableCIDRs []string `rethinkdb:"additionalannouncablecidrs" json:"additionalannouncablecidrs" description:"list of cidrs which are added to the route maps per tenant private network, these are typically pod- and service cidrs, can only be set in a supernetwork"` +} + +type ChildPrefixLength map[AddressFamily]uint8 + +// AddressFamily identifies IPv4/IPv6 +type AddressFamily string +type AddressFamilies map[AddressFamily]bool + +const ( + // IPv4AddressFamily identifies IPv4 + IPv4AddressFamily = AddressFamily("IPv4") + // IPv6AddressFamily identifies IPv6 + IPv6AddressFamily = AddressFamily("IPv6") +) + +// ToAddressFamily will convert a string af to a AddressFamily +func ToAddressFamily(af string) AddressFamily { + switch af { + case "IPv4", "ipv4": + return IPv4AddressFamily + case "IPv6", "ipv6": + return IPv6AddressFamily + } + return IPv4AddressFamily } // Networks is a list of networks. @@ -229,10 +255,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 networks where the id is the index. diff --git a/cmd/metal-api/internal/metal/partition.go b/cmd/metal-api/internal/metal/partition.go index 2db7c2f8..09fb8795 100644 --- a/cmd/metal-api/internal/metal/partition.go +++ b/cmd/metal-api/internal/metal/partition.go @@ -3,10 +3,9 @@ package metal // A Partition represents a location. type Partition struct { Base - BootConfiguration BootConfiguration `rethinkdb:"bootconfig" json:"bootconfig"` - MgmtServiceAddress string `rethinkdb:"mgmtserviceaddr" json:"mgmtserviceaddr"` - PrivateNetworkPrefixLength uint8 `rethinkdb:"privatenetworkprefixlength" json:"privatenetworkprefixlength"` - Labels map[string]string `rethinkdb:"labels" json:"labels"` + BootConfiguration BootConfiguration `rethinkdb:"bootconfig" json:"bootconfig"` + MgmtServiceAddress string `rethinkdb:"mgmtserviceaddr" json:"mgmtserviceaddr"` + Labels map[string]string `rethinkdb:"labels" json:"labels"` } // BootConfiguration defines the metal-hammer initrd, kernel and commandline diff --git a/cmd/metal-api/internal/service/integration_test.go b/cmd/metal-api/internal/service/integration_test.go index d462d066..2a96e525 100644 --- a/cmd/metal-api/internal/service/integration_test.go +++ b/cmd/metal-api/internal/service/integration_test.go @@ -282,14 +282,18 @@ func createTestEnvironment(t *testing.T, log *slog.Logger, ds *datastore.Rethink PartitionID: &partition.ID, }, NetworkImmutable: v1.NetworkImmutable{ - Prefixes: []string{testPrivateSuperCidr}, - PrivateSuper: true, + Prefixes: []string{testPrivateSuperCidr}, + PrivateSuper: true, + 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) status = te.networkCreate(t, ncr, &createdNetwork) require.Equal(t, http.StatusCreated, status) require.NotNil(t, createdNetwork) require.Equal(t, *ncr.ID, createdNetwork.ID) + log.Info("created a network", "nw", createdNetwork) te.privateSuperNetwork = &createdNetwork diff --git a/cmd/metal-api/internal/service/ip-service.go b/cmd/metal-api/internal/service/ip-service.go index e7d020d1..ad8f0f12 100644 --- a/cmd/metal-api/internal/service/ip-service.go +++ b/cmd/metal-api/internal/service/ip-service.go @@ -283,6 +283,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 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)) @@ -318,7 +334,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 @@ -331,13 +347,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, @@ -434,8 +450,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 { var connectErr *connect.Error diff --git a/cmd/metal-api/internal/service/ip-service_test.go b/cmd/metal-api/internal/service/ip-service_test.go index c473ba5f..b4e3834e 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" @@ -52,13 +53,15 @@ func TestGetIPs(t *testing.T) { err = json.NewDecoder(resp.Body).Decode(&result) require.NoError(t, err) - require.Len(t, result, 3) + require.Len(t, result, 4) require.Equal(t, testdata.IP1.IPAddress, result[0].IPAddress) require.Equal(t, testdata.IP1.Name, *result[0].Name) require.Equal(t, testdata.IP2.IPAddress, result[1].IPAddress) require.Equal(t, testdata.IP2.Name, *result[1].Name) require.Equal(t, testdata.IP3.IPAddress, result[2].IPAddress) require.Equal(t, testdata.IP3.Name, *result[2].Name) + require.Equal(t, testdata.IP4.IPAddress, result[3].IPAddress) + require.Equal(t, testdata.IP4.Name, *result[3].Name) } func TestGetIP(t *testing.T) { @@ -85,6 +88,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) @@ -258,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 network:4"), + }, } for i := range tests { tt := tests[i] @@ -286,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 24570e34..770ede91 100644 --- a/cmd/metal-api/internal/service/machine-service.go +++ b/cmd/metal-api/internal/service/machine-service.go @@ -1406,6 +1406,14 @@ func findWaitingMachine(ctx context.Context, ds *datastore.RethinkStore, allocat // is enabled to clean up networks that were already created. func makeNetworks(ctx context.Context, ds *datastore.RethinkStore, ipamer ipam.IPAMer, allocationSpec *machineAllocationSpec, networks allocationNetworkMap, alloc *metal.MachineAllocation) error { for _, n := range networks { + if n == nil || n.network == nil { + continue + } + if len(n.network.AddressFamilies) == 0 { + n.network.AddressFamilies = metal.AddressFamilies{ + metal.IPv4AddressFamily: true, + } + } machineNetwork, err := makeMachineNetwork(ctx, ds, ipamer, allocationSpec, n) if err != nil { return err @@ -1482,10 +1490,12 @@ func gatherNetworksFromSpec(ds *datastore.RethinkStore, allocationSpec *machineA // - user specifies administrative networks, i.e. underlay or privatesuper networks // - user's private network is specified with noauto but no specific IPs are given: this would yield a machine with no ip address - specNetworks := make(map[string]*allocationNetwork) - var primaryPrivateNetwork *allocationNetwork - var privateNetworks []*allocationNetwork - var privateSharedNetworks []*allocationNetwork + var ( + specNetworks = make(map[string]*allocationNetwork) + primaryPrivateNetwork *allocationNetwork + privateNetworks []*allocationNetwork + privateSharedNetworks []*allocationNetwork + ) for _, networkSpec := range allocationSpec.Networks { auto := true @@ -1585,13 +1595,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) } } @@ -1622,25 +1632,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 := metal.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 ffde006e..6fa551df 100644 --- a/cmd/metal-api/internal/service/machine-service_allocation_test.go +++ b/cmd/metal-api/internal/service/machine-service_allocation_test.go @@ -392,7 +392,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 ff7deb16..3e12f476 100644 --- a/cmd/metal-api/internal/service/network-service.go +++ b/cmd/metal-api/internal/service/network-service.go @@ -10,6 +10,7 @@ import ( "slices" "strconv" + "connectrpc.com/connect" mdmv1 "github.com/metal-stack/masterdata-api/api/v1" mdm "github.com/metal-stack/masterdata-api/pkg/client" @@ -22,6 +23,7 @@ import ( v1 "github.com/metal-stack/metal-api/cmd/metal-api/internal/service/v1" "github.com/metal-stack/metal-lib/auditing" "github.com/metal-stack/metal-lib/httperrors" + "github.com/metal-stack/metal-lib/pkg/pointer" ) type networkResource struct { @@ -158,7 +160,11 @@ func (r *networkResource) findNetwork(request *restful.Request, response *restfu return } ctx := request.Request.Context() - usage := getNetworkUsage(ctx, nw, r.ipamer) + usage, err := r.getNetworkUsage(ctx, nw) + if err != nil { + r.sendError(request, response, defaultError(err)) + return + } r.send(request, response, http.StatusOK, v1.NewNetworkResponse(nw, usage)) } @@ -172,7 +178,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 := r.getNetworkUsage(ctx, &nws[i]) + if err != nil { + r.sendError(request, response, defaultError(err)) + return + } + result = append(result, v1.NewNetworkResponse(&nws[i], usage)) } @@ -202,13 +213,20 @@ 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 := r.getNetworkUsage(ctx, &nws[i]) + if err != nil { + r.sendError(request, response, defaultError(err)) + return + } + result = append(result, v1.NewNetworkResponse(&nws[i], usage)) } r.send(request, response, http.StatusOK, result) } +// TODO allow creation of networks with childprefixlength which are not privatesuper +// See https://github.com/metal-stack/metal-api/issues/16 func (r *networkResource) createNetwork(request *restful.Request, response *restful.Response) { var requestPayload v1.NetworkCreateRequest err := request.ReadEntity(&requestPayload) @@ -263,28 +281,15 @@ func (r *networkResource) createNetwork(request *restful.Request, response *rest return } - prefixes := metal.Prefixes{} - // all Prefixes must be valid - 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 - } - - prefixes = append(prefixes, *prefix) + var childPrefixLength = metal.ChildPrefixLength{} + for af, length := range requestPayload.DefaultChildPrefixLength { + childPrefixLength[metal.ToAddressFamily(string(af))] = length } - 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) + prefixes, destPrefixes, addressFamilies, err := validatePrefixesAndAddressFamilies(requestPayload.Prefixes, requestPayload.DestinationPrefixes, childPrefixLength, privateSuper) + if err != nil { + r.sendError(request, response, httperrors.BadRequest(err)) + return } allNws, err := r.ds.ListNetworks() @@ -328,16 +333,21 @@ 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{}) - 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))) + var nw metal.Network + err := r.ds.FindNetwork(&datastore.NetworkSearchQuery{ + PartitionID: &partition.ID, + PrivateSuper: pointer.Pointer(true), + }, &nw) + r.log.Info("createnetwork", "found", nw) + if err != nil && !metal.IsNotFound(err) { + r.sendError(request, response, defaultError(err)) + return + } + if nw.ID != "" { + r.sendError(request, response, httperrors.BadRequest(fmt.Errorf("partition with id %q already has a private super network", partition.ID))) return } } @@ -390,6 +400,7 @@ func (r *networkResource) createNetwork(request *restful.Request, response *rest }, Prefixes: prefixes, DestinationPrefixes: destPrefixes, + DefaultChildPrefixLength: childPrefixLength, PartitionID: partitionID, ProjectID: projectID, Nat: nat, @@ -397,6 +408,7 @@ func (r *networkResource) createNetwork(request *restful.Request, response *rest Underlay: underlay, Vrf: vrf, Labels: labels, + AddressFamilies: addressFamilies, AdditionalAnnouncableCIDRs: requestPayload.AdditionalAnnouncableCIDRs, } @@ -415,7 +427,11 @@ func (r *networkResource) createNetwork(request *restful.Request, response *rest return } - usage := getNetworkUsage(ctx, nw, r.ipamer) + usage, err := r.getNetworkUsage(ctx, nw) + if err != nil { + r.sendError(request, response, defaultError(err)) + return + } r.send(request, response, http.StatusCreated, v1.NewNetworkResponse(nw, usage)) } @@ -439,6 +455,80 @@ func validateAdditionalAnnouncableCIDRs(additionalCidrs []string, privateSuper b return nil } +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 int(length) <= 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 = metal.AddressFamilies{} + ) + for _, p := range prefixes { + 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[metal.IPv4AddressFamily] = true + } + if ipprefix.Addr().Is6() { + addressFamilies[metal.IPv6AddressFamily] = true + } + result = append(result, *prefix) + } + return result, addressFamilies, nil +} + +// 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) @@ -493,17 +583,9 @@ func (r *networkResource) allocateNetwork(request *restful.Request, response *re return } - var superNetwork metal.Network - boolTrue := true - err = r.ds.FindNetwork(&datastore.NetworkSearchQuery{PartitionID: &partition.ID, PrivateSuper: &boolTrue}, &superNetwork) - if err != nil { - r.sendError(request, response, defaultError(err)) - return - } - 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 @@ -512,6 +594,31 @@ func (r *networkResource) allocateNetwork(request *restful.Request, response *re destPrefixes = append(destPrefixes, *prefix) } + 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), + ParentNetworkID: requestPayload.ParentNetworkID, + }, &superNetwork) + if err != nil { + r.sendError(request, response, defaultError(err)) + return + } + + if superNetwork.ID == "" { + r.sendError(request, response, httperrors.BadRequest(fmt.Errorf("no supernetwork found"))) + return + } + if len(superNetwork.DefaultChildPrefixLength) == 0 { + r.sendError(request, response, httperrors.BadRequest(fmt.Errorf("supernetwork %s has no defaultchildprefixlength specified", superNetwork.ID))) + return + } + r.log.Info("network allocate", "supernetwork", superNetwork.ID) + nwSpec := &metal.Network{ Base: metal.Base{ Name: name, @@ -523,32 +630,64 @@ func (r *networkResource) allocateNetwork(request *restful.Request, response *re DestinationPrefixes: destPrefixes, Shared: shared, Nat: nat, + AddressFamilies: superNetwork.AddressFamilies, } + + // Allow configurable prefix length per AF + length := superNetwork.DefaultChildPrefixLength + if len(requestPayload.Length) > 0 { + 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, + } + nwSpec.AddressFamilies = metal.AddressFamilies{af: true} + } + + 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, partition.PrivateNetworkPrefixLength) + nw, err := r.createChildNetwork(ctx, nwSpec, &superNetwork, length) if err != nil { r.sendError(request, response, defaultError(err)) return } - usage := getNetworkUsage(ctx, nw, r.ipamer) + usage, err := r.getNetworkUsage(ctx, nw) + 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) { - vrf, err := acquireRandomVRF(ds) +func (r *networkResource) createChildNetwork(ctx context.Context, nwSpec *metal.Network, parent *metal.Network, childLengths metal.ChildPrefixLength) (*metal.Network, error) { + vrf, err := acquireRandomVRF(r.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 := r.createChildPrefix(ctx, parent.Prefixes, af, childLength) + 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 length:%d", parent.ID, af, childLength) + } + childPrefixes = append(childPrefixes, *childPrefix) } nw := &metal.Network{ @@ -556,7 +695,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, @@ -567,9 +706,10 @@ func createChildNetwork(ctx context.Context, ds *datastore.RethinkStore, ipamer Vrf: *vrf, ParentNetworkID: parent.ID, Labels: nwSpec.Labels, + AddressFamilies: nwSpec.AddressFamilies, } - err = ds.CreateNetwork(nw) + err = r.ds.CreateNetwork(nw) if err != nil { return nil, err } @@ -658,15 +798,18 @@ func (r *networkResource) updateNetwork(request *restful.Request, response *rest return } - var prefixesToBeRemoved metal.Prefixes - var prefixesToBeAdded metal.Prefixes + var ( + prefixesToBeRemoved metal.Prefixes + prefixesToBeAdded metal.Prefixes + ) if len(requestPayload.Prefixes) > 0 { - newNetwork.Prefixes, err = prefixesFromCidr(requestPayload.Prefixes) + prefixes, _, err := validatePrefixes(requestPayload.Prefixes) if err != nil { r.sendError(request, response, defaultError(err)) return } + newNetwork.Prefixes = prefixes prefixesToBeRemoved = oldNetwork.SubtractPrefixes(newNetwork.Prefixes...) @@ -686,6 +829,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 { @@ -704,13 +863,12 @@ func (r *networkResource) updateNetwork(request *restful.Request, response *rest } } - if len(requestPayload.DestinationPrefixes) > 0 { - newNetwork.DestinationPrefixes, err = prefixesFromCidr(requestPayload.DestinationPrefixes) - if err != nil { - r.sendError(request, response, defaultError(err)) - return - } + err = validateAdditionalAnnouncableCIDRs(requestPayload.AdditionalAnnouncableCIDRs, oldNetwork.PrivateSuper) + if err != nil { + r.sendError(request, response, defaultError(err)) + return } + newNetwork.AdditionalAnnouncableCIDRs = requestPayload.AdditionalAnnouncableCIDRs err = validateAdditionalAnnouncableCIDRs(requestPayload.AdditionalAnnouncableCIDRs, oldNetwork.PrivateSuper) if err != nil { @@ -732,23 +890,15 @@ func (r *networkResource) updateNetwork(request *restful.Request, response *rest return } - usage := getNetworkUsage(ctx, &newNetwork, r.ipamer) + usage, err := r.getNetworkUsage(ctx, &newNetwork) + if err != nil { + r.sendError(request, response, defaultError(err)) + return + } r.send(request, response, http.StatusOK, v1.NewNetworkResponse(&newNetwork, usage)) } -func prefixesFromCidr(PrefixesCidr []string) (metal.Prefixes, error) { - var prefixes metal.Prefixes - for _, prefixCidr := range PrefixesCidr { - Prefix, err := metal.NewPrefixFromCIDR(prefixCidr) - if err != nil { - return nil, err - } - prefixes = append(prefixes, *Prefix) - } - return prefixes, nil -} - func (r *networkResource) deleteNetwork(request *restful.Request, response *restful.Response) { id := request.PathParameter("id") @@ -808,33 +958,62 @@ 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 (r *networkResource) getNetworkUsage(ctx context.Context, nw *metal.Network) (*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 { - u, err := ipamer.PrefixUsage(ctx, prefix.String()) + pfx, err := netip.ParsePrefix(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 + } + key := metal.IPv4AddressFamily + if pfx.Addr().Is6() { + key = metal.IPv6AddressFamily + } + u, err := r.ipamer.PrefixUsage(ctx, prefix.String()) + if err != nil { + 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) { - var errors []error - var err error - var childPrefix *metal.Prefix +func (r *networkResource) createChildPrefix(ctx context.Context, parentPrefixes metal.Prefixes, af metal.AddressFamily, childLength uint8) (*metal.Prefix, error) { + var ( + errs []error + childPrefix *metal.Prefix + ) for _, parentPrefix := range parentPrefixes { - childPrefix, err = ipamer.AllocateChildPrefix(ctx, parentPrefix, childLength) + pfx, err := netip.ParsePrefix(parentPrefix.String()) + if err != nil { + return nil, fmt.Errorf("unable to parse prefix: %w", err) + } + if pfx.Addr().Is4() && af == metal.IPv6AddressFamily { + continue + } + if pfx.Addr().Is6() && af == metal.IPv4AddressFamily { + continue + } + childPrefix, err = r.ipamer.AllocateChildPrefix(ctx, parentPrefix, childLength) if err != nil { - errors = append(errors, err) + var connectErr *connect.Error + if errors.As(err, &connectErr) { + if connectErr.Code() == connect.CodeNotFound { + continue + } + } + errs = append(errs, err) continue } if childPrefix != nil { @@ -842,10 +1021,10 @@ func createChildPrefix(ctx context.Context, parentPrefixes metal.Prefixes, child } } if childPrefix == nil { - if len(errors) > 0 { - return nil, fmt.Errorf("cannot allocate free child prefix in ipam: %v", errors) + if len(errs) > 0 { + return nil, fmt.Errorf("cannot allocate free child prefix in ipam: %w", errors.Join(errs...)) } - return nil, fmt.Errorf("cannot allocate free child prefix in one of the given parent prefixes in ipam: %v", parentPrefixes) + return nil, fmt.Errorf("cannot allocate free child prefix in one of the given parent prefixes in ipam: %s", parentPrefixes.String()) } return childPrefix, nil diff --git a/cmd/metal-api/internal/service/network-service_test.go b/cmd/metal-api/internal/service/network-service_test.go index 4e54993a..f33d1875 100644 --- a/cmd/metal-api/internal/service/network-service_test.go +++ b/cmd/metal-api/internal/service/network-service_test.go @@ -9,26 +9,30 @@ import ( "net/http/httptest" "testing" - "github.com/metal-stack/metal-lib/httperrors" - r "gopkg.in/rethinkdb/rethinkdb-go.v6" - + 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" "github.com/metal-stack/metal-api/cmd/metal-api/internal/datastore" "github.com/metal-stack/metal-api/cmd/metal-api/internal/ipam" - "github.com/metal-stack/metal-api/cmd/metal-api/internal/metal" v1 "github.com/metal-stack/metal-api/cmd/metal-api/internal/service/v1" - - restful "github.com/emicklei/go-restful/v3" "github.com/metal-stack/metal-api/cmd/metal-api/internal/testdata" + "github.com/metal-stack/metal-lib/httperrors" + testifymock "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" + r "gopkg.in/rethinkdb/rethinkdb-go.v6" ) func TestGetNetworks(t *testing.T) { ds, mock := datastore.InitMockDB(t) + ipamer, err := testdata.InitMockIpamData(mock, false) + require.NoError(t, err) testdata.InitMockDBData(mock) log := slog.Default() - networkservice := NewNetwork(log, ds, ipam.InitTestIpam(t), nil) + networkservice := NewNetwork(log, ds, ipamer, nil) container := restful.NewContainer().Add(networkservice) req := httptest.NewRequest("GET", "/v1/network", nil) container = injectViewer(log, container, req) @@ -39,7 +43,7 @@ func TestGetNetworks(t *testing.T) { defer resp.Body.Close() require.Equal(t, http.StatusOK, resp.StatusCode, w.Body.String()) var result []v1.NetworkResponse - err := json.NewDecoder(resp.Body).Decode(&result) + err = json.NewDecoder(resp.Body).Decode(&result) require.NoError(t, err) require.Len(t, result, 4) @@ -53,10 +57,13 @@ func TestGetNetworks(t *testing.T) { func TestGetNetwork(t *testing.T) { ds, mock := datastore.InitMockDB(t) + ipamer, err := testdata.InitMockIpamData(mock, false) + require.NoError(t, err) + testdata.InitMockDBData(mock) log := slog.Default() - networkservice := NewNetwork(log, ds, ipam.InitTestIpam(t), nil) + networkservice := NewNetwork(log, ds, ipamer, nil) container := restful.NewContainer().Add(networkservice) req := httptest.NewRequest("GET", "/v1/network/1", nil) container = injectViewer(log, container, req) @@ -67,7 +74,7 @@ func TestGetNetwork(t *testing.T) { defer resp.Body.Close() require.Equal(t, http.StatusOK, resp.StatusCode, w.Body.String()) var result v1.NetworkResponse - err := json.NewDecoder(resp.Body).Decode(&result) + err = json.NewDecoder(resp.Body).Decode(&result) require.NoError(t, err) require.Equal(t, testdata.Nw1.ID, result.ID) @@ -191,10 +198,12 @@ func TestCreateNetwork(t *testing.T) { func TestUpdateNetwork(t *testing.T) { ds, mock := datastore.InitMockDB(t) + ipamer, err := testdata.InitMockIpamData(mock, false) + require.NoError(t, err) testdata.InitMockDBData(mock) log := slog.Default() - networkservice := NewNetwork(log, ds, ipam.InitTestIpam(t), nil) + networkservice := NewNetwork(log, ds, ipamer, nil) container := restful.NewContainer().Add(networkservice) newName := "new" @@ -226,11 +235,13 @@ func TestUpdateNetwork(t *testing.T) { func TestSearchNetwork(t *testing.T) { ds, mock := datastore.InitMockDB(t) + ipamer, err := testdata.InitMockIpamData(mock, false) + require.NoError(t, err) mock.On(r.DB("mockdb").Table("network").Filter(r.MockAnything())).Return([]interface{}{testdata.Nw1}, nil) testdata.InitMockDBData(mock) log := slog.Default() - networkService := NewNetwork(log, ds, ipam.InitTestIpam(t), nil) + networkService := NewNetwork(log, ds, ipamer, nil) container := restful.NewContainer().Add(networkService) requestJSON := fmt.Sprintf("{%q:%q}", "partitionid", "1") req := httptest.NewRequest("POST", "/v1/network/find", bytes.NewBufferString(requestJSON)) @@ -243,7 +254,7 @@ func TestSearchNetwork(t *testing.T) { defer resp.Body.Close() require.Equal(t, http.StatusOK, resp.StatusCode, w.Body.String()) var results []v1.NetworkResponse - err := json.NewDecoder(resp.Body).Decode(&results) + err = json.NewDecoder(resp.Body).Decode(&results) require.NoError(t, err) require.Len(t, results, 1) @@ -252,3 +263,348 @@ func TestSearchNetwork(t *testing.T) { require.Equal(t, testdata.Nw1.PartitionID, *result.PartitionID) require.Equal(t, testdata.Nw1.Name, *result.Name) } + +func Test_networkResource_createNetwork(t *testing.T) { + log := slog.Default() + tests := []struct { + name string + networkName string + networkID string + partitionID string + projectID string + prefixes []string + destinationPrefixes []string + vrf uint + childprefixlength metal.ChildPrefixLength + privateSuper bool + underlay bool + nat bool + expectedStatus int + expectedErrorMessage string + }{ + { + name: "simple IPv4", + networkName: testdata.Nw1.Name, + partitionID: testdata.Nw1.PartitionID, + projectID: testdata.Nw1.ProjectID, + prefixes: []string{"172.0.0.0/24"}, + destinationPrefixes: []string{"0.0.0.0/0"}, + vrf: uint(10000), + expectedStatus: http.StatusCreated, + }, + { + name: "privatesuper IPv4", + networkName: testdata.Nw1.Name, + partitionID: testdata.Nw1.PartitionID, + projectID: testdata.Nw1.ProjectID, + prefixes: []string{"172.0.0.0/24"}, + destinationPrefixes: []string{"0.0.0.0/0"}, + childprefixlength: metal.ChildPrefixLength{ + metal.IPv4AddressFamily: 22, + }, + privateSuper: true, + vrf: uint(10000), + expectedStatus: http.StatusBadRequest, + expectedErrorMessage: "given defaultchildprefixlength 22 is not greater than prefix length of:172.0.0.0/24", + }, + { + name: "privatesuper IPv4 without defaultchildprefixlength", + networkName: testdata.Nw1.Name, + partitionID: testdata.Nw1.PartitionID, + projectID: testdata.Nw1.ProjectID, + prefixes: []string{"172.0.0.0/24"}, + destinationPrefixes: []string{"0.0.0.0/0"}, + privateSuper: true, + vrf: uint(10001), + expectedStatus: http.StatusBadRequest, + expectedErrorMessage: "private super network must always contain a defaultchildprefixlength", + }, + { + 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{"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, + expectedErrorMessage: "given prefix 192.168.265.0/24 is not a valid ip with mask: netip.ParsePrefix(\"192.168.265.0/24\"): ParseAddr(\"192.168.265.0\"): IPv4 field has value >255", + }, + { + name: "broken IPv6", + networkName: testdata.Nw1.Name, + partitionID: testdata.Nw1.PartitionID, + projectID: testdata.Nw1.ProjectID, + prefixes: []string{"fdaa:::/50"}, + destinationPrefixes: []string{"::/0"}, + privateSuper: true, + vrf: uint(10000), + expectedStatus: http.StatusBadRequest, + expectedErrorMessage: "given prefix fdaa:::/50 is not a valid ip with mask: netip.ParsePrefix(\"fdaa:::/50\"): ParseAddr(\"fdaa:::\"): each colon-separated field must have at least one digit (at \":\")", + }, + { + name: "mixed prefix addressfamilies", + networkName: testdata.Nw1.Name, + partitionID: testdata.Nw1.PartitionID, + projectID: testdata.Nw1.ProjectID, + prefixes: []string{"172.0.0.0/24", "fdaa:bbcc::/50"}, + destinationPrefixes: []string{"0.0.0.0/0"}, + vrf: uint(10000), + expectedStatus: http.StatusCreated, + }, + { + name: "broken destinationprefix", + networkName: testdata.Nw1.Name, + partitionID: testdata.Nw1.PartitionID, + projectID: testdata.Nw1.ProjectID, + prefixes: []string{"172.0.0.0/24"}, + destinationPrefixes: []string{"0.0.0.0/33"}, + vrf: uint(10000), + expectedStatus: http.StatusBadRequest, + expectedErrorMessage: "given prefix 0.0.0.0/33 is not a valid ip with mask: netip.ParsePrefix(\"0.0.0.0/33\"): prefix length out of range", + }, + { + name: "broken childprefixlength", + networkName: testdata.Nw1.Name, + partitionID: testdata.Nw1.PartitionID, + projectID: testdata.Nw1.ProjectID, + prefixes: []string{"fdaa:bbcc::/50"}, + childprefixlength: metal.ChildPrefixLength{ + metal.IPv6AddressFamily: 50, + }, + privateSuper: true, + vrf: uint(10000), + expectedStatus: http.StatusBadRequest, + expectedErrorMessage: "given defaultchildprefixlength 50 is not greater than prefix length of:fdaa:bbcc::/50", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ds, mock := datastore.InitMockDB(t) + ipamer, err := testdata.InitMockIpamData(mock, false) + require.NoError(t, err) + testdata.InitMockDBData(mock) + + networkservice := NewNetwork(log, ds, ipamer, nil) + container := restful.NewContainer().Add(networkservice) + + createRequest := &v1.NetworkCreateRequest{ + Describable: v1.Describable{Name: &tt.networkName}, + NetworkBase: v1.NetworkBase{PartitionID: &tt.partitionID, ProjectID: &tt.projectID}, + NetworkImmutable: v1.NetworkImmutable{ + Prefixes: tt.prefixes, + DestinationPrefixes: tt.destinationPrefixes, + Vrf: &tt.vrf, Nat: tt.nat, PrivateSuper: tt.privateSuper, Underlay: tt.underlay, + }, + } + if tt.childprefixlength != nil { + createRequest.DefaultChildPrefixLength = tt.childprefixlength + } + js, _ := json.Marshal(createRequest) + body := bytes.NewBuffer(js) + req := httptest.NewRequest("PUT", "/v1/network", body) + req.Header.Add("Content-Type", "application/json") + container = injectAdmin(log, container, req) + w := httptest.NewRecorder() + container.ServeHTTP(w, req) + + resp := w.Result() + defer resp.Body.Close() + + require.Equal(t, tt.expectedStatus, resp.StatusCode, w.Body.String()) + if tt.expectedStatus > 300 { + var result httperrors.HTTPErrorResponse + err := json.NewDecoder(resp.Body).Decode(&result) + + require.NoError(t, err) + require.Equal(t, tt.expectedErrorMessage, result.Message) + } else { + var result v1.NetworkResponse + err = json.NewDecoder(resp.Body).Decode(&result) + require.NoError(t, err) + require.Equal(t, tt.networkName, *result.Name) + require.Equal(t, tt.partitionID, *result.PartitionID) + require.Equal(t, tt.projectID, *result.ProjectID) + require.Equal(t, tt.destinationPrefixes, result.DestinationPrefixes) + if tt.childprefixlength != nil { + require.Equal(t, tt.childprefixlength, result.DefaultChildPrefixLength) + } + } + }) + } +} + +func Test_networkResource_allocateNetwork(t *testing.T) { + log := slog.Default() + tests := []struct { + name string + networkName string + partitionID string + projectID string + childprefixlength metal.ChildPrefixLength + shared bool + expectedStatus int + expectedErrorMessage string + }{ + { + name: "simple ipv4, default childprefixlength", + networkName: "tenantv4", + partitionID: testdata.Partition2.ID, + projectID: "project-1", + 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", + 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 { + ds, mock := datastore.InitMockDB(t) + changes := []r.ChangeResponse{{OldValue: map[string]interface{}{"id": float64(42)}}} + mock.On(r.DB("mockdb").Table("integerpool").Limit(1).Delete(r. + DeleteOpts{ReturnChanges: true})).Return(r.WriteResponse{Changes: changes}, nil) + + ipamer, err := testdata.InitMockIpamData(mock, false) + require.NoError(t, err) + testdata.InitMockDBData(mock) + + psc := mdmv1mock.ProjectServiceClient{} + psc.On("Get", testifymock.Anything, &mdmv1.ProjectGetRequest{Id: "project-1"}).Return(&mdmv1.ProjectResponse{ + Project: &mdmv1.Project{ + Meta: &mdmv1.Meta{Id: tt.projectID}, + }, + }, nil, + ) + tsc := mdmv1mock.TenantServiceClient{} + + mdc := mdm.NewMock(&psc, &tsc, nil, nil) + + networkservice := NewNetwork(log, ds, ipamer, mdc) + container := restful.NewContainer().Add(networkservice) + + allocateRequest := &v1.NetworkAllocateRequest{ + Describable: v1.Describable{Name: &tt.networkName}, + NetworkBase: v1.NetworkBase{PartitionID: &tt.partitionID, ProjectID: &tt.projectID}, + Length: tt.childprefixlength, + } + + js, err := json.Marshal(allocateRequest) + require.NoError(t, err) + + body := bytes.NewBuffer(js) + req := httptest.NewRequest("POST", "/v1/network/allocate", body) + req.Header.Add("Content-Type", "application/json") + container = injectAdmin(log, container, req) + w := httptest.NewRecorder() + container.ServeHTTP(w, req) + + resp := w.Result() + defer resp.Body.Close() + require.Equal(t, tt.expectedStatus, resp.StatusCode, w.Body.String()) + if tt.expectedStatus > 300 { + var result httperrors.HTTPErrorResponse + err := json.NewDecoder(resp.Body).Decode(&result) + + require.NoError(t, err) + require.Equal(t, tt.expectedErrorMessage, result.Message) + } else { + var result v1.NetworkResponse + err = json.NewDecoder(resp.Body).Decode(&result) + + require.GreaterOrEqual(t, len(result.Prefixes), 1) + + 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) + } + } +} + +func Test_validatePrefixes(t *testing.T) { + tests := []struct { + name string + prefixes []string + wantPrefixes metal.Prefixes + 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: 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: metal.AddressFamilies{metal.IPv6AddressFamily: true}, + }, + { + name: "mixed af", + prefixes: []string{"10.0.0.0/8", "2001::/64"}, + 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, af, err := validatePrefixes(tt.prefixes) + if (err != nil) != tt.wantErr { + t.Errorf("validatePrefixes() error = %v, wantErr %v", err, tt.wantErr) + return + } + if diff := cmp.Diff(got, tt.wantPrefixes); diff != "" { + t.Errorf("validatePrefixes() diff=%s", diff) + } + if diff := cmp.Diff(af, tt.wantAF); diff != "" { + t.Errorf("validatePrefixes() diff=%s", diff) + } + }) + } +} diff --git a/cmd/metal-api/internal/service/partition-service.go b/cmd/metal-api/internal/service/partition-service.go index 5f5dfd7c..ce1f1d95 100644 --- a/cmd/metal-api/internal/service/partition-service.go +++ b/cmd/metal-api/internal/service/partition-service.go @@ -170,14 +170,6 @@ func (r *partitionResource) createPartition(request *restful.Request, response * if requestPayload.Labels != nil { labels = requestPayload.Labels } - prefixLength := uint8(22) - if requestPayload.PrivateNetworkPrefixLength != nil { - prefixLength = uint8(*requestPayload.PrivateNetworkPrefixLength) // nolint:gosec - if prefixLength < 16 || prefixLength > 30 { - r.sendError(request, response, httperrors.BadRequest(errors.New("private network prefix length is out of range"))) - return - } - } var imageURL string if requestPayload.PartitionBootConfiguration.ImageURL != nil { imageURL = *requestPayload.PartitionBootConfiguration.ImageURL @@ -211,9 +203,8 @@ func (r *partitionResource) createPartition(request *restful.Request, response * Name: name, Description: description, }, - Labels: labels, - MgmtServiceAddress: mgmtServiceAddress, - PrivateNetworkPrefixLength: prefixLength, + Labels: labels, + MgmtServiceAddress: mgmtServiceAddress, BootConfiguration: metal.BootConfiguration{ ImageURL: imageURL, KernelURL: kernelURL, diff --git a/cmd/metal-api/internal/service/v1/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 1c1aa1a2..c991320f 100644 --- a/cmd/metal-api/internal/service/v1/network.go +++ b/cmd/metal-api/internal/service/v1/network.go @@ -15,15 +15,17 @@ 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"` - 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"` - AdditionalAnnouncableCIDRs []string `json:"additionalAnnouncableCIDRs,omitempty" description:"list of cidrs which are added to the route maps per tenant private network, these are typically pod- and service cidrs, can only be set for private super networks"` + 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"` + 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"` + AdditionalAnnouncableCIDRs []string `json:"additionalAnnouncableCIDRs,omitempty" description:"list of cidrs which are added to the route maps per tenant private network, these are typically pod- and service cidrs, can only be set for private super networks"` } // NetworkUsage reports core metrics about available and used IPs or Prefixes in a Network. @@ -46,8 +48,11 @@ 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"` + 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"` } // NetworkFindRequest is used to find a Network with different criteria. @@ -70,7 +75,8 @@ type NetworkResponse struct { Common NetworkBase NetworkImmutable - Usage NetworkUsage `json:"usage" description:"usage of ips and prefixes in this network" readonly:"true"` + Usage NetworkUsage `json:"usage" description:"usage of IPv4 ips and prefixes in this network" readonly:"true"` + UsageV6 NetworkUsage `json:"usagev6" description:"usage of IPv6 ips and prefixes in this network" readonly:"true"` Timestamps } @@ -80,7 +86,12 @@ func NewNetworkResponse(network *metal.Network, usage *metal.NetworkUsage) *Netw return nil } - var parentNetworkID *string + var ( + parentNetworkID *string + usagev4 NetworkUsage + usagev6 NetworkUsage + ) + if network.ParentNetworkID != "" { parentNetworkID = &network.ParentNetworkID } @@ -89,6 +100,32 @@ func NewNetworkResponse(network *metal.Network, usage *metal.NetworkUsage) *Netw labels = make(map[string]string) } + // Existing tenant networks where not migrated and get AF created here + if len(network.AddressFamilies) == 0 { + network.AddressFamilies = metal.AddressFamilies{ + metal.IPv4AddressFamily: true, + } + } + + for af := range network.AddressFamilies { + if af == metal.IPv4AddressFamily { + usagev4 = NetworkUsage{ + AvailableIPs: usage.AvailableIPs[af], + UsedIPs: usage.UsedIPs[af], + AvailablePrefixes: usage.AvailablePrefixes[af], + UsedPrefixes: usage.UsedPrefixes[af], + } + } + if af == metal.IPv6AddressFamily { + usagev6 = NetworkUsage{ + AvailableIPs: usage.AvailableIPs[af], + UsedIPs: usage.UsedIPs[af], + AvailablePrefixes: usage.AvailablePrefixes[af], + UsedPrefixes: usage.UsedPrefixes[af], + } + } + } + return &NetworkResponse{ Common: Common{ Identifiable: Identifiable{ @@ -108,19 +145,17 @@ func NewNetworkResponse(network *metal.Network, usage *metal.NetworkUsage) *Netw NetworkImmutable: NetworkImmutable{ Prefixes: network.Prefixes.String(), DestinationPrefixes: network.DestinationPrefixes.String(), + DefaultChildPrefixLength: network.DefaultChildPrefixLength, Nat: network.Nat, PrivateSuper: network.PrivateSuper, Underlay: network.Underlay, Vrf: &network.Vrf, ParentNetworkID: parentNetworkID, + AddressFamilies: network.AddressFamilies, AdditionalAnnouncableCIDRs: network.AdditionalAnnouncableCIDRs, }, - Usage: NetworkUsage{ - AvailableIPs: usage.AvailableIPs, - UsedIPs: usage.UsedIPs, - AvailablePrefixes: usage.AvailablePrefixes, - UsedPrefixes: usage.UsedPrefixes, - }, + Usage: usagev4, + UsageV6: usagev6, Timestamps: Timestamps{ Created: network.Created, Changed: network.Changed, diff --git a/cmd/metal-api/internal/service/v1/partition.go b/cmd/metal-api/internal/service/v1/partition.go index f3304241..942049f7 100644 --- a/cmd/metal-api/internal/service/v1/partition.go +++ b/cmd/metal-api/internal/service/v1/partition.go @@ -5,9 +5,8 @@ import ( ) type PartitionBase struct { - MgmtServiceAddress *string `json:"mgmtserviceaddress" description:"the address to the management service of this partition" optional:"true"` - PrivateNetworkPrefixLength *int `json:"privatenetworkprefixlength" description:"the length of private networks for the machine's child networks in this partition, default 22" optional:"true" minimum:"16" maximum:"30"` - Labels map[string]string `json:"labels" description:"free labels that you associate with this partition" optional:"true"` + MgmtServiceAddress *string `json:"mgmtserviceaddress" description:"the address to the management service of this partition" optional:"true"` + Labels map[string]string `json:"labels" description:"free labels that you associate with this partition" optional:"true"` } type PartitionBootConfiguration struct { @@ -97,8 +96,6 @@ func NewPartitionResponse(p *metal.Partition) *PartitionResponse { return nil } - prefixLength := int(p.PrivateNetworkPrefixLength) - labels := map[string]string{} if p.Labels != nil { labels = p.Labels @@ -116,7 +113,6 @@ func NewPartitionResponse(p *metal.Partition) *PartitionResponse { }, PartitionBase: PartitionBase{ MgmtServiceAddress: &p.MgmtServiceAddress, - PrivateNetworkPrefixLength: &prefixLength, }, PartitionBootConfiguration: PartitionBootConfiguration{ ImageURL: &p.BootConfiguration.ImageURL, diff --git a/cmd/metal-api/internal/testdata/ipam.go b/cmd/metal-api/internal/testdata/ipam.go index 5b801f52..a06b786c 100644 --- a/cmd/metal-api/internal/testdata/ipam.go +++ b/cmd/metal-api/internal/testdata/ipam.go @@ -23,7 +23,7 @@ func InitMockIpamData(dbMock *r.Mock, withIP bool) (ipam.IPAMer, error) { return nil, fmt.Errorf("error creating ipam mock data: %w", err) } } - for _, prefix := range []metal.Prefix{prefix1, prefix2, prefix3} { + for _, prefix := range []metal.Prefix{prefix1, prefix2, prefix3, superPrefix, superPrefixV6} { err := ipamer.CreatePrefix(ctx, prefix) if err != nil { return nil, fmt.Errorf("error creating ipam mock data: %w", err) @@ -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 0f8a5b9a..6b0b77ad 100644 --- a/cmd/metal-api/internal/testdata/testdata.go +++ b/cmd/metal-api/internal/testdata/testdata.go @@ -273,10 +273,14 @@ var ( URL: "http://images.metal-stack.io/metal-os/master/ubuntu/20.04/20200730/img.tar.lz4", } // Networks - prefix1 = metal.Prefix{IP: "185.1.2.0", Length: "26"} - prefix2 = metal.Prefix{IP: "100.64.2.0", Length: "16"} - prefix3 = metal.Prefix{IP: "192.0.0.0", Length: "16"} - prefixIPAM = metal.Prefix{IP: "10.0.0.0", Length: "16"} + prefix1 = metal.Prefix{IP: "185.1.2.0", Length: "26"} + prefix2 = metal.Prefix{IP: "100.64.0.0", Length: "16"} + prefix3 = metal.Prefix{IP: "192.0.0.0", Length: "16"} + 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 = metal.ChildPrefixLength{metal.IPv4AddressFamily: 28} + cpl2 = metal.ChildPrefixLength{metal.IPv4AddressFamily: 22} prefixes1 = []metal.Prefix{prefix1, prefix2} prefixes2 = []metal.Prefix{prefix2} @@ -289,9 +293,11 @@ var ( Name: "Network 1", Description: "description 1", }, - PartitionID: Partition1.ID, - Prefixes: prefixes1, - PrivateSuper: true, + PartitionID: Partition1.ID, + Prefixes: prefixes1, + PrivateSuper: true, + DefaultChildPrefixLength: cpl1, + AddressFamilies: metal.AddressFamilies{metal.IPv4AddressFamily: true}, } Nw2 = metal.Network{ Base: metal.Base{ @@ -299,8 +305,11 @@ var ( Name: "Network 2", Description: "description 2", }, - Prefixes: prefixes2, - Underlay: true, + PartitionID: Partition1.ID, + Prefixes: prefixes2, + Underlay: true, + DefaultChildPrefixLength: cpl2, + AddressFamilies: metal.AddressFamilies{metal.IPv4AddressFamily: true}, } Nw3 = metal.Network{ Base: metal.Base{ @@ -311,6 +320,7 @@ var ( Prefixes: prefixes3, PartitionID: Partition1.ID, ParentNetworkID: Nw1.ID, + AddressFamilies: metal.AddressFamilies{metal.IPv4AddressFamily: true}, } Partition1PrivateSuperNetwork = metal.Network{ @@ -331,13 +341,45 @@ var ( Base: metal.Base{ ID: "super-tenant-network-2", }, - Prefixes: metal.Prefixes{{IP: "10.3.0.0", Length: "16"}}, - PartitionID: Partition2.ID, - ParentNetworkID: "", - ProjectID: "", - PrivateSuper: true, - Nat: false, - Underlay: false, + Prefixes: metal.Prefixes{superPrefix}, + PartitionID: Partition2.ID, + DefaultChildPrefixLength: metal.ChildPrefixLength{metal.IPv4AddressFamily: 22}, + AddressFamilies: metal.AddressFamilies{metal.IPv4AddressFamily: true}, + ParentNetworkID: "", + ProjectID: "", + PrivateSuper: true, + Nat: false, + Underlay: false, + } + + Partition2PrivateSuperNetworkV6 = metal.Network{ + Base: metal.Base{ + ID: "super-tenant-network-2-v6", + }, + Prefixes: metal.Prefixes{superPrefixV6}, + PartitionID: Partition2.ID, + 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, + Nat: false, + Underlay: false, } Partition1UnderlayNetwork = metal.Network{ @@ -450,7 +492,8 @@ var ( Name: "IPAM Network", Description: "description IPAM", }, - Prefixes: prefixesIPAM, + Prefixes: prefixesIPAM, + AddressFamilies: metal.AddressFamilies{metal.IPv4AddressFamily: true}, } // IPs @@ -458,24 +501,31 @@ 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", } + IP4 = metal.IP{ + IPAddress: "2001:0db8:85a3::1", + Name: "IPv6 4", + Description: "description 4", + Type: metal.Ephemeral, + ProjectID: "1", + } IPAMIP = metal.IP{ Name: "IPAM IP", Description: "description IPAM", @@ -509,7 +559,6 @@ var ( Name: "partition1", Description: "description 1", }, - PrivateNetworkPrefixLength: 22, } Partition2 = metal.Partition{ Base: metal.Base{ @@ -517,7 +566,6 @@ var ( Name: "partition2", Description: "description 2", }, - PrivateNetworkPrefixLength: 22, } Partition3 = metal.Partition{ Base: metal.Base{ @@ -525,9 +573,14 @@ var ( Name: "partition3", Description: "description 3", }, - PrivateNetworkPrefixLength: 22, } - + Partition4 = metal.Partition{ + Base: metal.Base{ + ID: "4", + Name: "partition4", + Description: "description 4", + }, + } // Switches Switch1 = metal.Switch{ Base: metal.Base{ @@ -713,7 +766,7 @@ var ( } // All IPs TestIPs = []metal.IP{ - IP1, IP2, IP3, + IP1, IP2, IP3, IP4, } // All Events @@ -803,6 +856,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) @@ -827,13 +882,25 @@ func InitMockDBData(mock *r.Mock) { mock.On(r.DB("mockdb").Table("network").Get("404")).Return(nil, errors.New("Test Error")) 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) })).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) })).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) })).Return(Partition2PrivateSuperNetwork, nil) + mock.On(r.DB("mockdb").Table("network").Filter( + 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) mock.On(r.DB("mockdb").Table("ip").Get("3.4.5.6")).Return(IP3, nil) mock.On(r.DB("mockdb").Table("ip").Get("8.8.8.8")).Return(nil, errors.New("Test Error")) mock.On(r.DB("mockdb").Table("ip").Get("9.9.9.9")).Return(nil, nil) + mock.On(r.DB("mockdb").Table("ip").Get("2001:0db8:85a3::1")).Return(IP4, nil) mock.On(r.DB("mockdb").Table("ip").Get(Partition1InternetIP.IPAddress)).Return(Partition1InternetIP, nil) mock.On(r.DB("mockdb").Table("ip").Get(Partition2InternetIP.IPAddress)).Return(Partition2InternetIP, nil) mock.On(r.DB("mockdb").Table("ip").Get(Partition1SpecificSharedIP.IPAddress)).Return(Partition1SpecificSharedIP, nil) diff --git a/go.mod b/go.mod index a889c763..a6bf4793 100644 --- a/go.mod +++ b/go.mod @@ -33,7 +33,7 @@ require ( go4.org/netipx v0.0.0-20231129151722-fdeea329fbba golang.org/x/crypto v0.28.0 golang.org/x/sync v0.8.0 - google.golang.org/grpc v1.67.0 + google.golang.org/grpc v1.67.1 google.golang.org/protobuf v1.34.2 gopkg.in/rethinkdb/rethinkdb-go.v6 v6.2.2 ) @@ -80,7 +80,7 @@ require ( github.com/go-openapi/runtime v0.28.0 // indirect github.com/go-openapi/strfmt v0.23.0 // indirect github.com/go-openapi/swag v0.23.0 // indirect - github.com/go-viper/mapstructure/v2 v2.1.0 // indirect + github.com/go-viper/mapstructure/v2 v2.2.1 // indirect github.com/goccy/go-json v0.10.3 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt/v4 v4.5.0 // indirect @@ -93,8 +93,8 @@ require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect - github.com/jackc/pgx/v5 v5.6.0 // indirect - github.com/jackc/puddle/v2 v2.2.1 // indirect + github.com/jackc/pgx/v5 v5.7.1 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect @@ -109,7 +109,7 @@ require ( github.com/lestrrat-go/jwx/v2 v2.1.1 // indirect github.com/lestrrat-go/option v1.0.1 // indirect github.com/lib/pq v1.10.9 // indirect - github.com/lufia/plan9stats v0.0.0-20240819163618-b1d8f4d146e7 // indirect + github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect @@ -138,7 +138,7 @@ require ( github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect github.com/prometheus/client_model v0.6.1 // indirect - github.com/prometheus/common v0.59.1 // indirect + github.com/prometheus/common v0.60.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect github.com/puzpuzpuz/xsync/v3 v3.4.0 // indirect github.com/redis/go-redis/v9 v9.6.1 // indirect @@ -157,7 +157,7 @@ require ( github.com/subosito/gotenv v1.6.0 // indirect github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a // indirect github.com/tklauser/go-sysconf v0.3.14 // indirect - github.com/tklauser/numcpus v0.8.0 // indirect + github.com/tklauser/numcpus v0.9.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.56.0 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect @@ -171,20 +171,21 @@ require ( go.mongodb.org/mongo-driver v1.17.1 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0 // indirect go.opentelemetry.io/otel v1.30.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.29.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.30.0 // indirect go.opentelemetry.io/otel/metric v1.30.0 // indirect + go.opentelemetry.io/otel/sdk v1.30.0 // indirect go.opentelemetry.io/otel/trace v1.30.0 go.opentelemetry.io/proto/otlp v1.3.1 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect go4.org/mem v0.0.0-20240501181205-ae6ca9944745 // indirect - golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 + golang.org/x/exp v0.0.0-20241004190924-225e2abe05e6 golang.org/x/net v0.30.0 // indirect golang.org/x/oauth2 v0.23.0 // indirect golang.org/x/sys v0.26.0 // indirect golang.org/x/text v0.19.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240924160255-9d4c2d233b61 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240924160255-9d4c2d233b61 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240930140551-af27646dc61f // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240930140551-af27646dc61f // indirect gopkg.in/cenkalti/backoff.v2 v2.2.1 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 9a331c5f..62bbb3b6 100644 --- a/go.sum +++ b/go.sum @@ -133,8 +133,8 @@ github.com/go-openapi/validate v0.24.0 h1:LdfDKwNbpB6Vn40xhTdNZAnfLECL81w+VX3Bum github.com/go-openapi/validate v0.24.0/go.mod h1:iyeX1sEufmv3nPbBdX3ieNviWnOZaJ1+zquzJEf2BAQ= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= -github.com/go-viper/mapstructure/v2 v2.1.0 h1:gHnMa2Y/pIxElCH2GlZZ1lZSsn6XMtufpGyP1XxdC/w= -github.com/go-viper/mapstructure/v2 v2.1.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= +github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= @@ -175,10 +175,10 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY= -github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw= -github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= -github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jackc/pgx/v5 v5.7.1 h1:x7SYsPBYDkHDksogeSmZZ5xzThcTgRz++I5E+ePFUcs= +github.com/jackc/pgx/v5 v5.7.1/go.mod h1:e7O26IywZZ+naJtWWos6i6fvWK+29etgITqrqHLfoZA= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= @@ -230,8 +230,8 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/looplab/fsm v1.0.2 h1:f0kdMzr4CRpXtaKKRUxwLYJ7PirTdwrtNumeLN+mDx8= github.com/looplab/fsm v1.0.2/go.mod h1:PmD3fFvQEIsjMEfvZdrCDZ6y8VwKTwWNjlpEr6IKPO4= -github.com/lufia/plan9stats v0.0.0-20240819163618-b1d8f4d146e7 h1:5RK988zAqB3/AN3opGfRpoQgAVqr6/A5+qRTi67VUZY= -github.com/lufia/plan9stats v0.0.0-20240819163618-b1d8f4d146e7/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k= +github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 h1:7UMa6KCCMjZEMDtTVdcGu0B1GmmC7QJKiCCjyTAWQy0= +github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= @@ -321,8 +321,8 @@ github.com/prometheus/client_golang v1.20.4 h1:Tgh3Yr67PaOv/uTqloMsCEdeuFTatm5zI github.com/prometheus/client_golang v1.20.4/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= -github.com/prometheus/common v0.59.1 h1:LXb1quJHWm1P6wq/U824uxYi4Sg0oGvNeUm1z5dJoX0= -github.com/prometheus/common v0.59.1/go.mod h1:GpWM7dewqmVYcd7SmRaiWVe9SSqjf0UrwnYnpEZNuT0= +github.com/prometheus/common v0.60.0 h1:+V9PAREWNvJMAuJ1x1BaWl9dewMW4YrHZQbx0sJNllA= +github.com/prometheus/common v0.60.0/go.mod h1:h0LYf1R1deLSKtD4Vdg8gy4RuOvENW2J/h19V5NADQw= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/puzpuzpuz/xsync/v3 v3.4.0 h1:DuVBAdXuGFHv8adVXjWWZ63pJq+NRXOWVXlKDBZ+mJ4= @@ -388,8 +388,8 @@ github.com/testcontainers/testcontainers-go v0.33.0 h1:zJS9PfXYT5O0ZFXM2xxXfk4J5 github.com/testcontainers/testcontainers-go v0.33.0/go.mod h1:W80YpTa8D5C3Yy16icheD01UTDu+LmXIA2Keo+jWtT8= github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU= github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY= -github.com/tklauser/numcpus v0.8.0 h1:Mx4Wwe/FjZLeQsK/6kt2EOepwwSl7SmJrK5bV/dXYgY= -github.com/tklauser/numcpus v0.8.0/go.mod h1:ZJZlAY+dmR4eut8epnzf0u/VwodKmryxR8txiloSqBE= +github.com/tklauser/numcpus v0.9.0 h1:lmyCHtANi8aRUgkckBgoDk1nHCux3n2cgkJLXdQGPDo= +github.com/tklauser/numcpus v0.9.0/go.mod h1:SN6Nq1O3VychhC1npsWostA+oW+VOQTxZrS604NSRyI= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.37.1-0.20220607072126-8a320890c08d/go.mod h1:t/G+3rLek+CyY9bnIE+YlMRddxVAAGjhxndDB4i4C0I= @@ -421,14 +421,14 @@ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0 h1:ZIg3ZT/ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0/go.mod h1:DQAwmETtZV00skUwgD6+0U89g80NKsJE3DCKeLLPQMI= go.opentelemetry.io/otel v1.30.0 h1:F2t8sK4qf1fAmY9ua4ohFS/K+FUuOPemHUIXHtktrts= go.opentelemetry.io/otel v1.30.0/go.mod h1:tFw4Br9b7fOS+uEao81PJjVMjW/5fvNCbpsDIXqP0pc= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.29.0 h1:dIIDULZJpgdiHz5tXrTgKIMLkus6jEFa7x5SOKcyR7E= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.29.0/go.mod h1:jlRVBe7+Z1wyxFSUs48L6OBQZ5JwH2Hg/Vbl+t9rAgI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.30.0 h1:lsInsfvhVIfOI6qHVyysXMNDnjO9Npvl7tlDPJFBVd4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.30.0/go.mod h1:KQsVNh4OjgjTG0G6EiNi1jVpnaeeKsKMRwbLN+f1+8M= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.29.0 h1:JAv0Jwtl01UFiyWZEMiJZBiTlv5A50zNs8lsthXqIio= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.29.0/go.mod h1:QNKLmUEAq2QUbPQUfvw4fmv0bgbK7UlOSFCnXyfvSNc= go.opentelemetry.io/otel/metric v1.30.0 h1:4xNulvn9gjzo4hjg+wzIKG7iNFEaBMX00Qd4QIZs7+w= go.opentelemetry.io/otel/metric v1.30.0/go.mod h1:aXTfST94tswhWEb+5QjlSqG+cZlmyXy/u8jFpor3WqQ= -go.opentelemetry.io/otel/sdk v1.29.0 h1:vkqKjk7gwhS8VaWb0POZKmIEDimRCMsopNYnriHyryo= -go.opentelemetry.io/otel/sdk v1.29.0/go.mod h1:pM8Dx5WKnvxLCb+8lG1PRNIDxu9g9b9g59Qr7hfAAok= +go.opentelemetry.io/otel/sdk v1.30.0 h1:cHdik6irO49R5IysVhdn8oaiR9m8XluDaJAs4DfOrYE= +go.opentelemetry.io/otel/sdk v1.30.0/go.mod h1:p14X4Ok8S+sygzblytT1nqG98QG2KYKv++HE0LY/mhg= go.opentelemetry.io/otel/trace v1.30.0 h1:7UBkkYzeg3C7kQX8VAidWh2biiQbtAKjyIML8dQ9wmc= go.opentelemetry.io/otel/trace v1.30.0/go.mod h1:5EyKqTzzmyqB9bwtCCq6pDLktPK6fmGf/Dph+8VI02o= go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= @@ -451,8 +451,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= -golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk= -golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY= +golang.org/x/exp v0.0.0-20241004190924-225e2abe05e6 h1:1wqE9dj9NpSm04INVsJhhEUzhuDVjbcyKH91sVyPATw= +golang.org/x/exp v0.0.0-20241004190924-225e2abe05e6/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= @@ -520,18 +520,18 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE= -golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg= +golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= +golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/genproto/googleapis/api v0.0.0-20240924160255-9d4c2d233b61 h1:pAjq8XSSzXoP9ya73v/w+9QEAAJNluLrpmMq5qFJQNY= -google.golang.org/genproto/googleapis/api v0.0.0-20240924160255-9d4c2d233b61/go.mod h1:O6rP0uBq4k0mdi/b4ZEMAZjkhYWhS815kCvaMha4VN8= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240924160255-9d4c2d233b61 h1:N9BgCIAUvn/M+p4NJccWPWb3BWh88+zyL0ll9HgbEeM= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240924160255-9d4c2d233b61/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= -google.golang.org/grpc v1.67.0 h1:IdH9y6PF5MPSdAntIcpjQ+tXO41pcQsfZV2RxtQgVcw= -google.golang.org/grpc v1.67.0/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= +google.golang.org/genproto/googleapis/api v0.0.0-20240930140551-af27646dc61f h1:jTm13A2itBi3La6yTGqn8bVSrc3ZZ1r8ENHlIXBfnRA= +google.golang.org/genproto/googleapis/api v0.0.0-20240930140551-af27646dc61f/go.mod h1:CLGoBuH1VHxAUXVPP8FfPwPEVJB6lz3URE5mY2SuayE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240930140551-af27646dc61f h1:cUMEy+8oS78BWIH9OWazBkzbr090Od9tWBNtZHkOhf0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240930140551-af27646dc61f/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= +google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= +google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= diff --git a/spec/metal-api.json b/spec/metal-api.json index d73f9e05..e8fe4f20 100644 --- a/spec/metal-api.json +++ b/spec/metal-api.json @@ -1487,6 +1487,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" @@ -1524,6 +1532,7 @@ } }, "required": [ + "addressfamily", "networkid", "projectid", "type" @@ -3562,6 +3571,14 @@ }, "v1.NetworkAllocateRequest": { "properties": { + "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": { "description": "a description for this entity", "type": "string" @@ -3580,6 +3597,13 @@ "description": "free labels that you associate with this network.", "type": "object" }, + "length": { + "additionalProperties": { + "type": "integer" + }, + "description": "the bitlen of the prefix to allocate, defaults to defaultchildprefixlength of super prefix", + "type": "object" + }, "name": { "description": "a readable name for this entity", "type": "string" @@ -3588,6 +3612,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" @@ -3600,7 +3628,12 @@ "description": "marks a network as shareable.", "type": "boolean" } - } + }, + "required": [ + "addressfamily", + "length", + "parentnetworkid" + ] }, "v1.NetworkBase": { "properties": { @@ -3634,6 +3667,20 @@ }, "type": "array" }, + "addressfamilies": { + "additionalProperties": { + "type": "boolean" + }, + "description": "the addressfamilies in this network, either IPv4 or IPv6 or both", + "type": "object" + }, + "defaultchildprefixlength": { + "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", "type": "string" @@ -3706,6 +3753,7 @@ } }, "required": [ + "addressfamilies", "destinationprefixes", "id", "nat", @@ -3774,6 +3822,20 @@ }, "type": "array" }, + "addressfamilies": { + "additionalProperties": { + "type": "boolean" + }, + "description": "the addressfamilies in this network, either IPv4 or IPv6 or both", + "type": "object" + }, + "defaultchildprefixlength": { + "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", "items": { @@ -3815,6 +3877,7 @@ } }, "required": [ + "addressfamilies", "destinationprefixes", "nat", "prefixes", @@ -3831,6 +3894,13 @@ }, "type": "array" }, + "addressfamilies": { + "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", "format": "date-time", @@ -3843,6 +3913,13 @@ "readOnly": true, "type": "string" }, + "defaultchildprefixlength": { + "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", "type": "string" @@ -3906,7 +3983,11 @@ }, "usage": { "$ref": "#/definitions/v1.NetworkUsage", - "description": "usage of ips and prefixes in this network" + "description": "usage of IPv4 ips and prefixes in this network" + }, + "usagev6": { + "$ref": "#/definitions/v1.NetworkUsage", + "description": "usage of IPv6 ips and prefixes in this network" }, "vrf": { "description": "the vrf this network is associated with", @@ -3919,13 +4000,15 @@ } }, "required": [ + "addressfamilies", "destinationprefixes", "id", "nat", "prefixes", "privatesuper", "underlay", - "usage" + "usage", + "usagev6" ] }, "v1.NetworkUpdateRequest": { @@ -4033,13 +4116,6 @@ "mgmtserviceaddress": { "description": "the address to the management service of this partition", "type": "string" - }, - "privatenetworkprefixlength": { - "description": "the length of private networks for the machine's child networks in this partition, default 22", - "format": "int32", - "maximum": 30, - "minimum": 16, - "type": "integer" } } }, @@ -4127,13 +4203,6 @@ "name": { "description": "a readable name for this entity", "type": "string" - }, - "privatenetworkprefixlength": { - "description": "the length of private networks for the machine's child networks in this partition, default 22", - "format": "int32", - "maximum": 30, - "minimum": 16, - "type": "integer" } }, "required": [ @@ -4181,13 +4250,6 @@ "name": { "description": "a readable name for this entity", "type": "string" - }, - "privatenetworkprefixlength": { - "description": "the length of private networks for the machine's child networks in this partition, default 22", - "format": "int32", - "maximum": 30, - "minimum": 16, - "type": "integer" } }, "required": [