On API v1.52 and newer, the GET /networks/{id} endpoint returns
statistics about the IPAM state for the subnets assigned to the network.
Signed-off-by: Cory Snider <csnider@mirantis.com>
| ... | ... |
@@ -2574,6 +2574,21 @@ definitions: |
| 2574 | 2574 |
type: ServiceInfo |
| 2575 | 2575 |
hints: |
| 2576 | 2576 |
nullable: false |
| 2577 |
+ Status: |
|
| 2578 |
+ description: > |
|
| 2579 |
+ provides runtime information about the network |
|
| 2580 |
+ such as the number of allocated IPs. |
|
| 2581 |
+ $ref: "#/definitions/NetworkStatus" |
|
| 2582 |
+ |
|
| 2583 |
+ NetworkStatus: |
|
| 2584 |
+ description: > |
|
| 2585 |
+ provides runtime information about the network |
|
| 2586 |
+ such as the number of allocated IPs. |
|
| 2587 |
+ type: "object" |
|
| 2588 |
+ x-go-name: Status |
|
| 2589 |
+ properties: |
|
| 2590 |
+ IPAM: |
|
| 2591 |
+ $ref: "#/definitions/IPAMStatus" |
|
| 2577 | 2592 |
|
| 2578 | 2593 |
ServiceInfo: |
| 2579 | 2594 |
x-nullable: false |
| ... | ... |
@@ -2685,6 +2700,46 @@ definitions: |
| 2685 | 2685 |
additionalProperties: |
| 2686 | 2686 |
type: "string" |
| 2687 | 2687 |
|
| 2688 |
+ IPAMStatus: |
|
| 2689 |
+ type: "object" |
|
| 2690 |
+ x-nullable: false |
|
| 2691 |
+ x-omitempty: false |
|
| 2692 |
+ properties: |
|
| 2693 |
+ Subnets: |
|
| 2694 |
+ type: "object" |
|
| 2695 |
+ additionalProperties: |
|
| 2696 |
+ $ref: "#/definitions/SubnetStatus" |
|
| 2697 |
+ example: |
|
| 2698 |
+ "172.16.0.0/16": |
|
| 2699 |
+ IPsInUse: 3 |
|
| 2700 |
+ DynamicIPsAvailable: 65533 |
|
| 2701 |
+ "2001:db8:abcd:0012::0/96": |
|
| 2702 |
+ IPsInUse: 5 |
|
| 2703 |
+ DynamicIPsAvailable: 4294967291 |
|
| 2704 |
+ x-go-type: |
|
| 2705 |
+ type: SubnetStatuses |
|
| 2706 |
+ kind: map |
|
| 2707 |
+ |
|
| 2708 |
+ SubnetStatus: |
|
| 2709 |
+ type: "object" |
|
| 2710 |
+ x-nullable: false |
|
| 2711 |
+ x-omitempty: false |
|
| 2712 |
+ properties: |
|
| 2713 |
+ IPsInUse: |
|
| 2714 |
+ description: > |
|
| 2715 |
+ Number of IP addresses in the subnet that are in use or reserved and |
|
| 2716 |
+ are therefore unavailable for allocation, saturating at 2<sup>64</sup> - 1. |
|
| 2717 |
+ type: integer |
|
| 2718 |
+ format: uint64 |
|
| 2719 |
+ x-omitempty: false |
|
| 2720 |
+ DynamicIPsAvailable: |
|
| 2721 |
+ description: > |
|
| 2722 |
+ Number of IP addresses within the network's IPRange for the subnet |
|
| 2723 |
+ that are available for allocation, saturating at 2<sup>64</sup> - 1. |
|
| 2724 |
+ type: integer |
|
| 2725 |
+ format: uint64 |
|
| 2726 |
+ x-omitempty: false |
|
| 2727 |
+ |
|
| 2688 | 2728 |
EndpointResource: |
| 2689 | 2729 |
type: "object" |
| 2690 | 2730 |
description: > |
| ... | ... |
@@ -20,4 +20,8 @@ type Inspect struct {
|
| 20 | 20 |
// swarm scope networks, and omitted for local scope networks. |
| 21 | 21 |
// |
| 22 | 22 |
Services map[string]ServiceInfo `json:"Services,omitempty"` |
| 23 |
+ |
|
| 24 |
+ // provides runtime information about the network such as the number of allocated IPs. |
|
| 25 |
+ // |
|
| 26 |
+ Status *Status `json:"Status,omitempty"` |
|
| 23 | 27 |
} |
| 28 | 30 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,16 @@ |
| 0 |
+// Code generated by go-swagger; DO NOT EDIT. |
|
| 1 |
+ |
|
| 2 |
+package network |
|
| 3 |
+ |
|
| 4 |
+// This file was generated by the swagger tool. |
|
| 5 |
+// Editing this file might prove futile when you re-run the swagger generate command |
|
| 6 |
+ |
|
| 7 |
+// IPAMStatus IPAM status |
|
| 8 |
+// |
|
| 9 |
+// swagger:model IPAMStatus |
|
| 10 |
+type IPAMStatus struct {
|
|
| 11 |
+ |
|
| 12 |
+ // subnets |
|
| 13 |
+ // Example: {"172.16.0.0/16":{"DynamicIPsAvailable":65533,"IPsInUse":3},"2001:db8:abcd:0012::0/96":{"DynamicIPsAvailable":4294967291,"IPsInUse":5}}
|
|
| 14 |
+ Subnets SubnetStatuses `json:"Subnets,omitempty"` |
|
| 15 |
+} |
| 0 | 16 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,15 @@ |
| 0 |
+// Code generated by go-swagger; DO NOT EDIT. |
|
| 1 |
+ |
|
| 2 |
+package network |
|
| 3 |
+ |
|
| 4 |
+// This file was generated by the swagger tool. |
|
| 5 |
+// Editing this file might prove futile when you re-run the swagger generate command |
|
| 6 |
+ |
|
| 7 |
+// Status provides runtime information about the network such as the number of allocated IPs. |
|
| 8 |
+// |
|
| 9 |
+// swagger:model Status |
|
| 10 |
+type Status struct {
|
|
| 11 |
+ |
|
| 12 |
+ // IPAM |
|
| 13 |
+ IPAM IPAMStatus `json:"IPAM"` |
|
| 14 |
+} |
| 0 | 15 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,20 @@ |
| 0 |
+// Code generated by go-swagger; DO NOT EDIT. |
|
| 1 |
+ |
|
| 2 |
+package network |
|
| 3 |
+ |
|
| 4 |
+// This file was generated by the swagger tool. |
|
| 5 |
+// Editing this file might prove futile when you re-run the swagger generate command |
|
| 6 |
+ |
|
| 7 |
+// SubnetStatus subnet status |
|
| 8 |
+// |
|
| 9 |
+// swagger:model SubnetStatus |
|
| 10 |
+type SubnetStatus struct {
|
|
| 11 |
+ |
|
| 12 |
+ // Number of IP addresses in the subnet that are in use or reserved and are therefore unavailable for allocation, saturating at 2<sup>64</sup> - 1. |
|
| 13 |
+ // |
|
| 14 |
+ IPsInUse uint64 `json:"IPsInUse"` |
|
| 15 |
+ |
|
| 16 |
+ // Number of IP addresses within the network's IPRange for the subnet that are available for allocation, saturating at 2<sup>64</sup> - 1. |
|
| 17 |
+ // |
|
| 18 |
+ DynamicIPsAvailable uint64 `json:"DynamicIPsAvailable"` |
|
| 19 |
+} |
| ... | ... |
@@ -5,6 +5,7 @@ import ( |
| 5 | 5 |
"net" |
| 6 | 6 |
"net/netip" |
| 7 | 7 |
|
| 8 |
+ "github.com/moby/moby/api/types/network" |
|
| 8 | 9 |
"github.com/moby/moby/v2/daemon/libnetwork/types" |
| 9 | 10 |
) |
| 10 | 11 |
|
| ... | ... |
@@ -57,6 +58,12 @@ type Ipam interface {
|
| 57 | 57 |
IsBuiltIn() bool |
| 58 | 58 |
} |
| 59 | 59 |
|
| 60 |
+type PoolStatuser interface {
|
|
| 61 |
+ Ipam |
|
| 62 |
+ // Status returns the operational status of the specified IPAM pool. |
|
| 63 |
+ PoolStatus(poolID string) (network.SubnetStatus, error) |
|
| 64 |
+} |
|
| 65 |
+ |
|
| 60 | 66 |
type PoolRequest struct {
|
| 61 | 67 |
// AddressSpace is a mandatory field which denotes which block of pools |
| 62 | 68 |
// should be used to make the allocation. This value is opaque, and only |
| ... | ... |
@@ -7,7 +7,9 @@ import ( |
| 7 | 7 |
"sync" |
| 8 | 8 |
|
| 9 | 9 |
"github.com/containerd/log" |
| 10 |
+ "github.com/moby/moby/api/types/network" |
|
| 10 | 11 |
"github.com/moby/moby/v2/daemon/libnetwork/internal/netiputil" |
| 12 |
+ "github.com/moby/moby/v2/daemon/libnetwork/internal/uint128" |
|
| 11 | 13 |
"github.com/moby/moby/v2/daemon/libnetwork/ipamapi" |
| 12 | 14 |
"github.com/moby/moby/v2/daemon/libnetwork/ipamutils" |
| 13 | 15 |
"github.com/moby/moby/v2/daemon/libnetwork/ipbits" |
| ... | ... |
@@ -369,3 +371,24 @@ func (aSpace *addrSpace) releaseAddress(nw, sub netip.Prefix, address netip.Addr |
| 369 | 369 |
|
| 370 | 370 |
return p.addrs.Remove(address) |
| 371 | 371 |
} |
| 372 |
+ |
|
| 373 |
+func (aSpace *addrSpace) allocationStatus(nw, ipr netip.Prefix) (network.SubnetStatus, error) {
|
|
| 374 |
+ aSpace.mu.Lock() |
|
| 375 |
+ defer aSpace.mu.Unlock() |
|
| 376 |
+ |
|
| 377 |
+ if ipr == (netip.Prefix{}) {
|
|
| 378 |
+ ipr = nw |
|
| 379 |
+ } |
|
| 380 |
+ p, ok := aSpace.subnets[nw] |
|
| 381 |
+ if !ok {
|
|
| 382 |
+ return network.SubnetStatus{}, types.NotFoundErrorf("cannot find address pool for %v", nw)
|
|
| 383 |
+ } |
|
| 384 |
+ |
|
| 385 |
+ iprcap := uint128.From(0, 1).Lsh(uint(ipr.Addr().BitLen() - ipr.Bits())) |
|
| 386 |
+ ipralloc := uint128.From(p.addrs.AddrsInPrefix(ipr)) |
|
| 387 |
+ |
|
| 388 |
+ return network.SubnetStatus{
|
|
| 389 |
+ IPsInUse: uint128.From(p.addrs.Len()).Uint64Sat(), |
|
| 390 |
+ DynamicIPsAvailable: iprcap.Sub(ipralloc).Uint64Sat(), |
|
| 391 |
+ }, nil |
|
| 392 |
+} |
| ... | ... |
@@ -8,6 +8,7 @@ import ( |
| 8 | 8 |
"net/netip" |
| 9 | 9 |
|
| 10 | 10 |
"github.com/containerd/log" |
| 11 |
+ "github.com/moby/moby/api/types/network" |
|
| 11 | 12 |
"github.com/moby/moby/v2/daemon/libnetwork/internal/addrset" |
| 12 | 13 |
"github.com/moby/moby/v2/daemon/libnetwork/internal/netiputil" |
| 13 | 14 |
"github.com/moby/moby/v2/daemon/libnetwork/ipamapi" |
| ... | ... |
@@ -23,6 +24,8 @@ const ( |
| 23 | 23 |
globalAddressSpace = "GlobalDefault" |
| 24 | 24 |
) |
| 25 | 25 |
|
| 26 |
+var _ ipamapi.PoolStatuser = &Allocator{}
|
|
| 27 |
+ |
|
| 26 | 28 |
// Register registers the default ipam driver with libnetwork. It takes |
| 27 | 29 |
// two optional address pools respectively containing the list of user-defined |
| 28 | 30 |
// address pools for 'local' and 'global' address spaces. |
| ... | ... |
@@ -312,6 +315,21 @@ func getAddress(base netip.Prefix, addrSet *addrset.AddrSet, prefAddress netip.A |
| 312 | 312 |
return addr, nil |
| 313 | 313 |
} |
| 314 | 314 |
|
| 315 |
+// PoolStatus returns the operational status of the specified IPAM pool. |
|
| 316 |
+func (a *Allocator) PoolStatus(poolID string) (network.SubnetStatus, error) {
|
|
| 317 |
+ k, err := PoolIDFromString(poolID) |
|
| 318 |
+ if err != nil {
|
|
| 319 |
+ return network.SubnetStatus{}, types.InvalidParameterErrorf("invalid pool id: %s", poolID)
|
|
| 320 |
+ } |
|
| 321 |
+ |
|
| 322 |
+ aSpace, err := a.getAddrSpace(k.AddressSpace, k.Is6()) |
|
| 323 |
+ if err != nil {
|
|
| 324 |
+ return network.SubnetStatus{}, err
|
|
| 325 |
+ } |
|
| 326 |
+ |
|
| 327 |
+ return aSpace.allocationStatus(k.Subnet, k.ChildSubnet) |
|
| 328 |
+} |
|
| 329 |
+ |
|
| 315 | 330 |
// IsBuiltIn returns true for builtin drivers |
| 316 | 331 |
func (a *Allocator) IsBuiltIn() bool {
|
| 317 | 332 |
return true |
| ... | ... |
@@ -9,12 +9,14 @@ import ( |
| 9 | 9 |
"net" |
| 10 | 10 |
"net/netip" |
| 11 | 11 |
"runtime" |
| 12 |
+ "slices" |
|
| 12 | 13 |
"strings" |
| 13 | 14 |
"sync" |
| 14 | 15 |
"time" |
| 15 | 16 |
|
| 16 | 17 |
cerrdefs "github.com/containerd/errdefs" |
| 17 | 18 |
"github.com/containerd/log" |
| 19 |
+ "github.com/moby/moby/api/types/network" |
|
| 18 | 20 |
"github.com/moby/moby/v2/daemon/internal/sliceutil" |
| 19 | 21 |
"github.com/moby/moby/v2/daemon/internal/stringid" |
| 20 | 22 |
"github.com/moby/moby/v2/daemon/libnetwork/datastore" |
| ... | ... |
@@ -30,6 +32,7 @@ import ( |
| 30 | 30 |
"github.com/moby/moby/v2/daemon/libnetwork/scope" |
| 31 | 31 |
"github.com/moby/moby/v2/daemon/libnetwork/types" |
| 32 | 32 |
"github.com/moby/moby/v2/errdefs" |
| 33 |
+ "github.com/moby/moby/v2/internal/iterutil" |
|
| 33 | 34 |
"go.opentelemetry.io/otel" |
| 34 | 35 |
"go.opentelemetry.io/otel/attribute" |
| 35 | 36 |
"go.opentelemetry.io/otel/trace" |
| ... | ... |
@@ -2171,3 +2174,37 @@ func (n *Network) deleteLoadBalancerSandbox() error {
|
| 2171 | 2171 |
} |
| 2172 | 2172 |
return nil |
| 2173 | 2173 |
} |
| 2174 |
+ |
|
| 2175 |
+func (n *Network) IPAMStatus(ctx context.Context) (network.IPAMStatus, error) {
|
|
| 2176 |
+ status := network.IPAMStatus{
|
|
| 2177 |
+ Subnets: make(map[netip.Prefix]network.SubnetStatus), |
|
| 2178 |
+ } |
|
| 2179 |
+ |
|
| 2180 |
+ if n.hasSpecialDriver() {
|
|
| 2181 |
+ // Special drivers do not assign addresses from IPAM |
|
| 2182 |
+ return status, nil |
|
| 2183 |
+ } |
|
| 2184 |
+ |
|
| 2185 |
+ ipamdriver, _, err := n.getController().getIPAMDriver(n.ipamType) |
|
| 2186 |
+ if err != nil {
|
|
| 2187 |
+ return status, err |
|
| 2188 |
+ } |
|
| 2189 |
+ ipam, ok := ipamdriver.(ipamapi.PoolStatuser) |
|
| 2190 |
+ if !ok {
|
|
| 2191 |
+ return status, nil |
|
| 2192 |
+ } |
|
| 2193 |
+ |
|
| 2194 |
+ var errs []error |
|
| 2195 |
+ info4, info6 := n.IpamInfo() |
|
| 2196 |
+ for info := range iterutil.Chain(slices.Values(info4), slices.Values(info6)) {
|
|
| 2197 |
+ pstat, err := ipam.PoolStatus(info.PoolID) |
|
| 2198 |
+ if err != nil {
|
|
| 2199 |
+ errs = append(errs, fmt.Errorf("failed to retrieve pool %s status: %w", info.PoolID, err))
|
|
| 2200 |
+ continue |
|
| 2201 |
+ } |
|
| 2202 |
+ prefix, _ := netiputil.ToPrefix(info.Pool) |
|
| 2203 |
+ status.Subnets[prefix] = pstat |
|
| 2204 |
+ } |
|
| 2205 |
+ |
|
| 2206 |
+ return status, errors.Join(errs...) |
|
| 2207 |
+} |
| ... | ... |
@@ -602,6 +602,19 @@ func (daemon *Daemon) GetNetworks(filter network.Filter, config backend.NetworkL |
| 602 | 602 |
if config.WithServices {
|
| 603 | 603 |
nr.Services = buildServiceAttachments(n) |
| 604 | 604 |
} |
| 605 |
+ if config.WithStatus {
|
|
| 606 |
+ ipam, err := n.IPAMStatus(context.TODO()) |
|
| 607 |
+ if err != nil {
|
|
| 608 |
+ log.G(context.TODO()).WithFields(log.Fields{
|
|
| 609 |
+ "network": n.Name(), |
|
| 610 |
+ "id": n.ID(), |
|
| 611 |
+ "error": err, |
|
| 612 |
+ }).Warning("Error encountered while gathering IPAM status for network")
|
|
| 613 |
+ } |
|
| 614 |
+ nr.Status = &networktypes.Status{
|
|
| 615 |
+ IPAM: ipam, |
|
| 616 |
+ } |
|
| 617 |
+ } |
|
| 605 | 618 |
networks = append(networks, nr) |
| 606 | 619 |
} |
| 607 | 620 |
} |
| ... | ... |
@@ -146,7 +146,10 @@ func (n *networkRouter) getNetwork(ctx context.Context, w http.ResponseWriter, r |
| 146 | 146 |
} |
| 147 | 147 |
filter.IDAlsoMatchesName = true |
| 148 | 148 |
|
| 149 |
- networks, _ := n.backend.GetNetworks(filter, backend.NetworkListConfig{WithServices: verbose})
|
|
| 149 |
+ networks, _ := n.backend.GetNetworks(filter, backend.NetworkListConfig{
|
|
| 150 |
+ WithServices: verbose, |
|
| 151 |
+ WithStatus: versions.GreaterThanOrEqualTo(httputils.VersionFromContext(ctx), "1.52"), |
|
| 152 |
+ }) |
|
| 150 | 153 |
for _, nw := range networks {
|
| 151 | 154 |
if nw.ID == term {
|
| 152 | 155 |
return httputils.WriteJSON(w, http.StatusOK, nw) |
| ... | ... |
@@ -60,15 +60,18 @@ EOT |
| 60 | 60 |
# TODO: Restore when go-swagger is updated |
| 61 | 61 |
# See https://github.com/moby/moby/pull/47526#discussion_r1551800022 |
| 62 | 62 |
|
| 63 |
-generate_model types/network --keep-spec-order <<- 'EOT' |
|
| 63 |
+generate_model types/network --keep-spec-order --additional-initialism=IPAM <<- 'EOT' |
|
| 64 | 64 |
ConfigReference |
| 65 | 65 |
EndpointResource |
| 66 |
+ IPAMStatus |
|
| 66 | 67 |
Network |
| 67 | 68 |
NetworkCreateResponse |
| 68 | 69 |
NetworkInspect |
| 70 |
+ NetworkStatus |
|
| 69 | 71 |
NetworkSummary |
| 70 | 72 |
NetworkTaskInfo |
| 71 | 73 |
PeerInfo |
| 74 |
+ SubnetStatus |
|
| 72 | 75 |
EOT |
| 73 | 76 |
|
| 74 | 77 |
generate_model types/plugin <<- 'EOT' |
| ... | ... |
@@ -137,16 +137,20 @@ func WithIPAM(subnet, gateway string) func(*client.NetworkCreateOptions) {
|
| 137 | 137 |
|
| 138 | 138 |
// WithIPAMRange adds an IPAM with the specified Subnet, IPRange and Gateway to the network |
| 139 | 139 |
func WithIPAMRange(subnet, iprange, gateway string) func(*client.NetworkCreateOptions) {
|
| 140 |
+ return WithIPAMConfig(network.IPAMConfig{
|
|
| 141 |
+ Subnet: subnet, |
|
| 142 |
+ IPRange: iprange, |
|
| 143 |
+ Gateway: gateway, |
|
| 144 |
+ AuxAddress: map[string]string{},
|
|
| 145 |
+ }) |
|
| 146 |
+} |
|
| 147 |
+ |
|
| 148 |
+// WithIPAMConfig adds the provided IPAM configurations to the network |
|
| 149 |
+func WithIPAMConfig(configs ...network.IPAMConfig) func(*client.NetworkCreateOptions) {
|
|
| 140 | 150 |
return func(n *client.NetworkCreateOptions) {
|
| 141 | 151 |
if n.IPAM == nil {
|
| 142 | 152 |
n.IPAM = &network.IPAM{}
|
| 143 | 153 |
} |
| 144 |
- |
|
| 145 |
- n.IPAM.Config = append(n.IPAM.Config, network.IPAMConfig{
|
|
| 146 |
- Subnet: subnet, |
|
| 147 |
- IPRange: iprange, |
|
| 148 |
- Gateway: gateway, |
|
| 149 |
- AuxAddress: map[string]string{},
|
|
| 150 |
- }) |
|
| 154 |
+ n.IPAM.Config = append(n.IPAM.Config, configs...) |
|
| 151 | 155 |
} |
| 152 | 156 |
} |
| ... | ... |
@@ -3,6 +3,7 @@ package bridge |
| 3 | 3 |
import ( |
| 4 | 4 |
"context" |
| 5 | 5 |
"fmt" |
| 6 |
+ "math" |
|
| 6 | 7 |
"net" |
| 7 | 8 |
"net/netip" |
| 8 | 9 |
"strings" |
| ... | ... |
@@ -1064,3 +1065,154 @@ func TestPortBindingBackfillingForOlderContainers(t *testing.T) {
|
| 1064 | 1064 |
}} |
| 1065 | 1065 |
assert.DeepEqual(t, expMappings, inspect.HostConfig.PortBindings) |
| 1066 | 1066 |
} |
| 1067 |
+ |
|
| 1068 |
+func TestBridgeIPAMStatus(t *testing.T) {
|
|
| 1069 |
+ ctx := testutil.StartSpan(baseContext, t) |
|
| 1070 |
+ d := daemon.New(t) |
|
| 1071 |
+ d.StartWithBusybox(ctx, t) |
|
| 1072 |
+ defer d.Stop(t) |
|
| 1073 |
+ |
|
| 1074 |
+ c := d.NewClientT(t, client.WithVersion("1.52"))
|
|
| 1075 |
+ |
|
| 1076 |
+ checkSubnets := func( |
|
| 1077 |
+ netName string, want networktypes.SubnetStatuses) bool {
|
|
| 1078 |
+ t.Helper() |
|
| 1079 |
+ nw, err := c.NetworkInspect(ctx, netName, client.NetworkInspectOptions{})
|
|
| 1080 |
+ if assert.Check(t, err) && assert.Check(t, nw.Status != nil) {
|
|
| 1081 |
+ return assert.Check(t, is.DeepEqual(want, nw.Status.IPAM.Subnets)) |
|
| 1082 |
+ } |
|
| 1083 |
+ return false |
|
| 1084 |
+ } |
|
| 1085 |
+ |
|
| 1086 |
+ t.Run("DualStack", func(t *testing.T) {
|
|
| 1087 |
+ const ( |
|
| 1088 |
+ netName = "testipambridge" |
|
| 1089 |
+ |
|
| 1090 |
+ ipv4gw = "192.168.0.1" |
|
| 1091 |
+ ipv4Range = "192.168.0.64/31" |
|
| 1092 |
+ prefIPv4OutOfRange = "192.168.0.129" |
|
| 1093 |
+ auxIPv4FromRange = "192.168.0.65" |
|
| 1094 |
+ auxIPv4OutOfRange = "192.168.0.128" |
|
| 1095 |
+ |
|
| 1096 |
+ ipv6gw = "2001:db8:abcd::1" |
|
| 1097 |
+ ipv6Range = "2001:db8:abcd::/120" |
|
| 1098 |
+ prefIPv6OutOfRange = "2001:db8:abcd::9000" |
|
| 1099 |
+ auxIPv6FromRange = "2001:db8:abcd::2a" |
|
| 1100 |
+ auxIPv6OutOfRange = "2001:db8:abcd::8000" |
|
| 1101 |
+ ) |
|
| 1102 |
+ var ( |
|
| 1103 |
+ cidrv4 = netip.MustParsePrefix("192.168.0.0/24")
|
|
| 1104 |
+ cidrv6 = netip.MustParsePrefix("2001:db8:abcd::/64")
|
|
| 1105 |
+ ) |
|
| 1106 |
+ |
|
| 1107 |
+ network.CreateNoError(ctx, t, c, netName, |
|
| 1108 |
+ network.WithIPv4(true), |
|
| 1109 |
+ network.WithIPAMConfig(networktypes.IPAMConfig{
|
|
| 1110 |
+ Subnet: cidrv4.String(), |
|
| 1111 |
+ IPRange: ipv4Range, |
|
| 1112 |
+ Gateway: ipv4gw, |
|
| 1113 |
+ AuxAddress: map[string]string{
|
|
| 1114 |
+ "reserved": auxIPv4FromRange, |
|
| 1115 |
+ "reserved_1": auxIPv4OutOfRange, |
|
| 1116 |
+ }}), |
|
| 1117 |
+ network.WithIPv6(), |
|
| 1118 |
+ network.WithIPAMConfig(networktypes.IPAMConfig{
|
|
| 1119 |
+ Subnet: cidrv6.String(), |
|
| 1120 |
+ IPRange: ipv6Range, |
|
| 1121 |
+ Gateway: ipv6gw, |
|
| 1122 |
+ AuxAddress: map[string]string{
|
|
| 1123 |
+ "reserved1": auxIPv6FromRange, |
|
| 1124 |
+ "reserved2": auxIPv6OutOfRange, |
|
| 1125 |
+ }, |
|
| 1126 |
+ }), |
|
| 1127 |
+ ) |
|
| 1128 |
+ defer c.NetworkRemove(ctx, netName) |
|
| 1129 |
+ |
|
| 1130 |
+ checkSubnets(netName, map[netip.Prefix]networktypes.SubnetStatus{
|
|
| 1131 |
+ cidrv4: {
|
|
| 1132 |
+ // 1 subnet + 1 gateway + 1 broadcast + 2 aux addresses |
|
| 1133 |
+ IPsInUse: 5, |
|
| 1134 |
+ // IPv4 /31 IPRange (2 addresses) - aux in-range |
|
| 1135 |
+ DynamicIPsAvailable: 1, |
|
| 1136 |
+ }, |
|
| 1137 |
+ cidrv6: {
|
|
| 1138 |
+ IPsInUse: 4, // 1 gateway + 1 anycast + 2 aux addresses |
|
| 1139 |
+ DynamicIPsAvailable: 253, // IPv6 /120 IPRange (256 addresses) - 1 router-anycast - 1 gateway - 1 aux in-range |
|
| 1140 |
+ }, |
|
| 1141 |
+ }) |
|
| 1142 |
+ |
|
| 1143 |
+ func() {
|
|
| 1144 |
+ // From IPRange pool: both counters should be changed by 1 |
|
| 1145 |
+ id := ctr.Run(ctx, t, c, ctr.WithNetworkMode(netName)) |
|
| 1146 |
+ defer c.ContainerRemove(ctx, id, client.ContainerRemoveOptions{Force: true})
|
|
| 1147 |
+ |
|
| 1148 |
+ checkSubnets(netName, map[netip.Prefix]networktypes.SubnetStatus{
|
|
| 1149 |
+ cidrv4: {
|
|
| 1150 |
+ IPsInUse: 6, |
|
| 1151 |
+ DynamicIPsAvailable: 0, |
|
| 1152 |
+ }, |
|
| 1153 |
+ cidrv6: {
|
|
| 1154 |
+ IPsInUse: 5, |
|
| 1155 |
+ DynamicIPsAvailable: 252, |
|
| 1156 |
+ }, |
|
| 1157 |
+ }) |
|
| 1158 |
+ |
|
| 1159 |
+ // Out of IPRange pools: subnet counter should be changed by 1 |
|
| 1160 |
+ id = ctr.Run(ctx, t, c, |
|
| 1161 |
+ ctr.WithNetworkMode(netName), |
|
| 1162 |
+ ctr.WithIPv4(netName, prefIPv4OutOfRange), |
|
| 1163 |
+ ctr.WithIPv6(netName, prefIPv6OutOfRange), |
|
| 1164 |
+ ) |
|
| 1165 |
+ defer c.ContainerRemove(ctx, id, client.ContainerRemoveOptions{Force: true})
|
|
| 1166 |
+ |
|
| 1167 |
+ checkSubnets(netName, map[netip.Prefix]networktypes.SubnetStatus{
|
|
| 1168 |
+ cidrv4: {
|
|
| 1169 |
+ IPsInUse: 7, |
|
| 1170 |
+ DynamicIPsAvailable: 0, // unchanged |
|
| 1171 |
+ }, |
|
| 1172 |
+ cidrv6: {
|
|
| 1173 |
+ IPsInUse: 6, |
|
| 1174 |
+ DynamicIPsAvailable: 252, // unchanged |
|
| 1175 |
+ }, |
|
| 1176 |
+ }) |
|
| 1177 |
+ }() |
|
| 1178 |
+ |
|
| 1179 |
+ // Counters should decrease after container removal |
|
| 1180 |
+ checkSubnets(netName, map[netip.Prefix]networktypes.SubnetStatus{
|
|
| 1181 |
+ cidrv4: {
|
|
| 1182 |
+ IPsInUse: 5, |
|
| 1183 |
+ DynamicIPsAvailable: 1, |
|
| 1184 |
+ }, |
|
| 1185 |
+ cidrv6: {
|
|
| 1186 |
+ IPsInUse: 4, |
|
| 1187 |
+ DynamicIPsAvailable: 253, |
|
| 1188 |
+ }, |
|
| 1189 |
+ }) |
|
| 1190 |
+ |
|
| 1191 |
+ oldc := d.NewClientT(t, client.WithVersion("1.51"))
|
|
| 1192 |
+ nw, err := oldc.NetworkInspect(ctx, netName, client.NetworkInspectOptions{})
|
|
| 1193 |
+ if assert.Check(t, err) {
|
|
| 1194 |
+ assert.Check(t, nw.Status == nil, "expected nil Status with API version 1.51") |
|
| 1195 |
+ } |
|
| 1196 |
+ }) |
|
| 1197 |
+ |
|
| 1198 |
+ t.Run("IPv6", func(t *testing.T) {
|
|
| 1199 |
+ const netName = "testipambridgev6" |
|
| 1200 |
+ cidr := netip.MustParsePrefix("2001:db8:abcd::/56")
|
|
| 1201 |
+ network.CreateNoError(ctx, t, c, netName, |
|
| 1202 |
+ network.WithIPv4(false), |
|
| 1203 |
+ network.WithIPv6(), |
|
| 1204 |
+ network.WithIPAMConfig(networktypes.IPAMConfig{
|
|
| 1205 |
+ Subnet: cidr.String(), |
|
| 1206 |
+ }), |
|
| 1207 |
+ ) |
|
| 1208 |
+ defer c.NetworkRemove(ctx, netName) |
|
| 1209 |
+ |
|
| 1210 |
+ checkSubnets(netName, map[netip.Prefix]networktypes.SubnetStatus{
|
|
| 1211 |
+ cidr: {
|
|
| 1212 |
+ IPsInUse: 2, |
|
| 1213 |
+ DynamicIPsAvailable: math.MaxUint64, |
|
| 1214 |
+ }, |
|
| 1215 |
+ }) |
|
| 1216 |
+ }) |
|
| 1217 |
+} |
| ... | ... |
@@ -5,10 +5,12 @@ package ipvlan |
| 5 | 5 |
import ( |
| 6 | 6 |
"context" |
| 7 | 7 |
"fmt" |
| 8 |
+ "net/netip" |
|
| 8 | 9 |
"strings" |
| 9 | 10 |
"testing" |
| 10 | 11 |
"time" |
| 11 | 12 |
|
| 13 |
+ "github.com/google/go-cmp/cmp/cmpopts" |
|
| 12 | 14 |
"github.com/moby/moby/api/types/network" |
| 13 | 15 |
dclient "github.com/moby/moby/client" |
| 14 | 16 |
"github.com/moby/moby/v2/daemon/libnetwork/netlabel" |
| ... | ... |
@@ -484,6 +486,11 @@ func TestIpvlanIPAM(t *testing.T) {
|
| 484 | 484 |
}, |
| 485 | 485 |
} |
| 486 | 486 |
|
| 487 |
+ var ( |
|
| 488 |
+ subnetv4 = netip.MustParsePrefix("10.42.42.0/24")
|
|
| 489 |
+ subnetv6 = netip.MustParsePrefix("2001:db8:abcd::/64")
|
|
| 490 |
+ ) |
|
| 491 |
+ |
|
| 487 | 492 |
for _, tc := range tests {
|
| 488 | 493 |
t.Run(tc.name, func(t *testing.T) {
|
| 489 | 494 |
ctx := testutil.StartSpan(ctx, t) |
| ... | ... |
@@ -492,6 +499,15 @@ func TestIpvlanIPAM(t *testing.T) {
|
| 492 | 492 |
netOpts := []func(*dclient.NetworkCreateOptions){
|
| 493 | 493 |
net.WithIPvlan("", "l3"),
|
| 494 | 494 |
net.WithIPv4(tc.enableIPv4), |
| 495 |
+ net.WithIPAMConfig( |
|
| 496 |
+ network.IPAMConfig{
|
|
| 497 |
+ Subnet: subnetv4.String(), |
|
| 498 |
+ }, |
|
| 499 |
+ network.IPAMConfig{
|
|
| 500 |
+ Subnet: subnetv6.String(), |
|
| 501 |
+ IPRange: "2001:db8:abcd::100/120", |
|
| 502 |
+ }, |
|
| 503 |
+ ), |
|
| 495 | 504 |
} |
| 496 | 505 |
if tc.enableIPv6 {
|
| 497 | 506 |
netOpts = append(netOpts, net.WithIPv6()) |
| ... | ... |
@@ -509,10 +525,15 @@ func TestIpvlanIPAM(t *testing.T) {
|
| 509 | 509 |
assert.Check(t, is.Contains(loRes.Combined(), " inet ")) |
| 510 | 510 |
assert.Check(t, is.Contains(loRes.Combined(), " inet6 ")) |
| 511 | 511 |
|
| 512 |
+ wantSubnetStatus := make(map[netip.Prefix]network.SubnetStatus) |
|
| 512 | 513 |
eth0Res := container.ExecT(ctx, t, c, id, []string{"ip", "a", "show", "dev", "eth0"})
|
| 513 | 514 |
if tc.enableIPv4 || tc.expIPv4 {
|
| 514 | 515 |
assert.Check(t, is.Contains(eth0Res.Combined(), " inet "), |
| 515 | 516 |
"Expected IPv4 in: %s", eth0Res.Combined()) |
| 517 |
+ wantSubnetStatus[subnetv4] = network.SubnetStatus{
|
|
| 518 |
+ IPsInUse: 3, // network, broadcast, container |
|
| 519 |
+ DynamicIPsAvailable: 253, |
|
| 520 |
+ } |
|
| 516 | 521 |
} else {
|
| 517 | 522 |
assert.Check(t, !strings.Contains(eth0Res.Combined(), " inet "), |
| 518 | 523 |
"Expected no IPv4 in: %s", eth0Res.Combined()) |
| ... | ... |
@@ -520,6 +541,10 @@ func TestIpvlanIPAM(t *testing.T) {
|
| 520 | 520 |
if tc.enableIPv6 {
|
| 521 | 521 |
assert.Check(t, is.Contains(eth0Res.Combined(), " inet6 "), |
| 522 | 522 |
"Expected IPv6 in: %s", eth0Res.Combined()) |
| 523 |
+ wantSubnetStatus[subnetv6] = network.SubnetStatus{
|
|
| 524 |
+ IPsInUse: 2, // subnet-router anycast, container |
|
| 525 |
+ DynamicIPsAvailable: 255, |
|
| 526 |
+ } |
|
| 523 | 527 |
} else {
|
| 524 | 528 |
assert.Check(t, !strings.Contains(eth0Res.Combined(), " inet6 "), |
| 525 | 529 |
"Expected no IPv6 in: %s", eth0Res.Combined()) |
| ... | ... |
@@ -531,10 +556,188 @@ func TestIpvlanIPAM(t *testing.T) {
|
| 531 | 531 |
expDisableIPv6 = "0" |
| 532 | 532 |
} |
| 533 | 533 |
assert.Check(t, is.Equal(strings.TrimSpace(sysctlRes.Combined()), expDisableIPv6)) |
| 534 |
+ |
|
| 535 |
+ cc := d.NewClientT(t, dclient.WithVersion("1.52"))
|
|
| 536 |
+ inspect, err := cc.NetworkInspect(ctx, netName, dclient.NetworkInspectOptions{})
|
|
| 537 |
+ if assert.Check(t, err) && assert.Check(t, inspect.Status != nil) {
|
|
| 538 |
+ assert.Check(t, is.DeepEqual(wantSubnetStatus, inspect.Status.IPAM.Subnets, cmpopts.EquateEmpty())) |
|
| 539 |
+ } |
|
| 540 |
+ cc.Close() |
|
| 541 |
+ cc = d.NewClientT(t, dclient.WithVersion("1.51"))
|
|
| 542 |
+ inspect, err = cc.NetworkInspect(ctx, netName, dclient.NetworkInspectOptions{})
|
|
| 543 |
+ assert.Check(t, err) |
|
| 544 |
+ assert.Check(t, inspect.Status == nil) |
|
| 545 |
+ cc.Close() |
|
| 534 | 546 |
}) |
| 535 | 547 |
} |
| 536 | 548 |
} |
| 537 | 549 |
|
| 550 |
+// IPVLAN networks are allowed to be assigned IPAM subnets that overlap with |
|
| 551 |
+// other IPVLAN networks' IPAM subnets. But no two IPVLAN endpoints may be |
|
| 552 |
+// assigned the same address, even when the endpoints are attached to different |
|
| 553 |
+// networks. The assignment of an address to an endpoint on one network may |
|
| 554 |
+// therefore reduce the number of available addresses to assign to other |
|
| 555 |
+// networks' endpoints. |
|
| 556 |
+func TestIpvlanIPAMOverlap(t *testing.T) {
|
|
| 557 |
+ skip.If(t, testEnv.IsRemoteDaemon) |
|
| 558 |
+ skip.If(t, testEnv.IsRootless, "rootless mode has different view of network") |
|
| 559 |
+ |
|
| 560 |
+ ctx := testutil.StartSpan(baseContext, t) |
|
| 561 |
+ d := daemon.New(t) |
|
| 562 |
+ d.StartWithBusybox(ctx, t) |
|
| 563 |
+ defer d.Stop(t) |
|
| 564 |
+ |
|
| 565 |
+ c := d.NewClientT(t) |
|
| 566 |
+ |
|
| 567 |
+ checkNetworkIPAMState := func(networkID string, want map[netip.Prefix]network.SubnetStatus) bool {
|
|
| 568 |
+ t.Helper() |
|
| 569 |
+ nw, err := c.NetworkInspect(ctx, networkID, dclient.NetworkInspectOptions{})
|
|
| 570 |
+ if assert.Check(t, err) && assert.Check(t, nw.Status != nil) {
|
|
| 571 |
+ return assert.Check(t, is.DeepEqual(want, nw.Status.IPAM.Subnets, cmpopts.EquateEmpty())) |
|
| 572 |
+ } |
|
| 573 |
+ return false |
|
| 574 |
+ } |
|
| 575 |
+ |
|
| 576 |
+ // Create three networks with joined and overlapped IPAM ranges |
|
| 577 |
+ // and verify that the IPAM state is correct |
|
| 578 |
+ |
|
| 579 |
+ const ( |
|
| 580 |
+ netName1 = "ipvlannet1" |
|
| 581 |
+ netName2 = "ipvlannet2" |
|
| 582 |
+ netName3 = "ipvlannet3" |
|
| 583 |
+ ) |
|
| 584 |
+ cidrv4 := netip.MustParsePrefix("192.168.0.0/24")
|
|
| 585 |
+ cidrv6 := netip.MustParsePrefix("2001:db8:abcd::/64")
|
|
| 586 |
+ |
|
| 587 |
+ net.CreateNoError(ctx, t, c, netName1, |
|
| 588 |
+ net.WithIPvlan("", "l3"),
|
|
| 589 |
+ net.WithIPv6(), |
|
| 590 |
+ net.WithIPAMConfig( |
|
| 591 |
+ network.IPAMConfig{
|
|
| 592 |
+ Subnet: cidrv4.String(), |
|
| 593 |
+ IPRange: "192.168.0.0/25", |
|
| 594 |
+ Gateway: "192.168.0.1", |
|
| 595 |
+ AuxAddress: map[string]string{
|
|
| 596 |
+ "reserved": "192.168.0.100", |
|
| 597 |
+ }, |
|
| 598 |
+ }, |
|
| 599 |
+ network.IPAMConfig{
|
|
| 600 |
+ Subnet: cidrv6.String(), |
|
| 601 |
+ IPRange: "2001:db8:abcd::/124", |
|
| 602 |
+ }, |
|
| 603 |
+ ), |
|
| 604 |
+ ) |
|
| 605 |
+ defer c.NetworkRemove(ctx, netName1) |
|
| 606 |
+ assert.Check(t, n.IsNetworkAvailable(ctx, c, netName1)) |
|
| 607 |
+ |
|
| 608 |
+ checkNetworkIPAMState(netName1, map[netip.Prefix]network.SubnetStatus{
|
|
| 609 |
+ cidrv4: {
|
|
| 610 |
+ IPsInUse: 4, |
|
| 611 |
+ DynamicIPsAvailable: 125, |
|
| 612 |
+ }, |
|
| 613 |
+ cidrv6: {
|
|
| 614 |
+ IPsInUse: 1, |
|
| 615 |
+ DynamicIPsAvailable: 15, |
|
| 616 |
+ }, |
|
| 617 |
+ }) |
|
| 618 |
+ |
|
| 619 |
+ net.CreateNoError(ctx, t, c, netName2, |
|
| 620 |
+ net.WithIPvlan("", "l3"),
|
|
| 621 |
+ net.WithIPv6(), |
|
| 622 |
+ net.WithIPAMConfig( |
|
| 623 |
+ network.IPAMConfig{
|
|
| 624 |
+ Subnet: cidrv4.String(), |
|
| 625 |
+ IPRange: "192.168.0.0/24", |
|
| 626 |
+ }, |
|
| 627 |
+ network.IPAMConfig{
|
|
| 628 |
+ Subnet: cidrv6.String(), |
|
| 629 |
+ IPRange: "2001:db8:abcd::/120", |
|
| 630 |
+ }, |
|
| 631 |
+ ), |
|
| 632 |
+ ) |
|
| 633 |
+ |
|
| 634 |
+ defer c.NetworkRemove(ctx, netName2) |
|
| 635 |
+ assert.Check(t, n.IsNetworkAvailable(ctx, c, netName2)) |
|
| 636 |
+ |
|
| 637 |
+ checkNetworkIPAMState(netName2, map[netip.Prefix]network.SubnetStatus{
|
|
| 638 |
+ cidrv4: {
|
|
| 639 |
+ IPsInUse: 4, |
|
| 640 |
+ DynamicIPsAvailable: 252, |
|
| 641 |
+ }, |
|
| 642 |
+ cidrv6: {
|
|
| 643 |
+ IPsInUse: 1, |
|
| 644 |
+ DynamicIPsAvailable: 255, |
|
| 645 |
+ }, |
|
| 646 |
+ }) |
|
| 647 |
+ |
|
| 648 |
+ net.CreateNoError(ctx, t, c, netName3, |
|
| 649 |
+ net.WithIPvlan("", "l3"),
|
|
| 650 |
+ net.WithIPv6(), |
|
| 651 |
+ net.WithIPAMConfig( |
|
| 652 |
+ network.IPAMConfig{
|
|
| 653 |
+ Subnet: cidrv4.String(), |
|
| 654 |
+ IPRange: "192.168.0.128/25", |
|
| 655 |
+ }, |
|
| 656 |
+ network.IPAMConfig{
|
|
| 657 |
+ Subnet: cidrv6.String(), |
|
| 658 |
+ IPRange: "2001:db8:abcd::80/124", |
|
| 659 |
+ }, |
|
| 660 |
+ ), |
|
| 661 |
+ ) |
|
| 662 |
+ |
|
| 663 |
+ defer c.NetworkRemove(ctx, netName3) |
|
| 664 |
+ assert.Check(t, n.IsNetworkAvailable(ctx, c, netName3)) |
|
| 665 |
+ |
|
| 666 |
+ checkNetworkIPAMState(netName3, map[netip.Prefix]network.SubnetStatus{
|
|
| 667 |
+ cidrv4: {
|
|
| 668 |
+ IPsInUse: 4, |
|
| 669 |
+ DynamicIPsAvailable: 127, |
|
| 670 |
+ }, |
|
| 671 |
+ cidrv6: {
|
|
| 672 |
+ IPsInUse: 1, |
|
| 673 |
+ DynamicIPsAvailable: 16, |
|
| 674 |
+ }, |
|
| 675 |
+ }) |
|
| 676 |
+ |
|
| 677 |
+ // Create a container on one of the networks |
|
| 678 |
+ id := container.Run(ctx, t, c, container.WithNetworkMode(netName1)) |
|
| 679 |
+ defer c.ContainerRemove(ctx, id, dclient.ContainerRemoveOptions{Force: true})
|
|
| 680 |
+ |
|
| 681 |
+ // Verify that the IPAM status of all three networks are affected. |
|
| 682 |
+ checkNetworkIPAMState(netName1, map[netip.Prefix]network.SubnetStatus{
|
|
| 683 |
+ cidrv4: {
|
|
| 684 |
+ IPsInUse: 5, |
|
| 685 |
+ DynamicIPsAvailable: 124, |
|
| 686 |
+ }, |
|
| 687 |
+ cidrv6: {
|
|
| 688 |
+ IPsInUse: 2, |
|
| 689 |
+ DynamicIPsAvailable: 14, |
|
| 690 |
+ }, |
|
| 691 |
+ }) |
|
| 692 |
+ |
|
| 693 |
+ checkNetworkIPAMState(netName2, map[netip.Prefix]network.SubnetStatus{
|
|
| 694 |
+ cidrv4: {
|
|
| 695 |
+ IPsInUse: 5, |
|
| 696 |
+ DynamicIPsAvailable: 251, |
|
| 697 |
+ }, |
|
| 698 |
+ cidrv6: {
|
|
| 699 |
+ IPsInUse: 2, |
|
| 700 |
+ DynamicIPsAvailable: 254, |
|
| 701 |
+ }, |
|
| 702 |
+ }) |
|
| 703 |
+ |
|
| 704 |
+ checkNetworkIPAMState(netName3, map[netip.Prefix]network.SubnetStatus{
|
|
| 705 |
+ cidrv4: {
|
|
| 706 |
+ IPsInUse: 5, |
|
| 707 |
+ DynamicIPsAvailable: 127, |
|
| 708 |
+ }, |
|
| 709 |
+ cidrv6: {
|
|
| 710 |
+ IPsInUse: 2, |
|
| 711 |
+ DynamicIPsAvailable: 16, |
|
| 712 |
+ }, |
|
| 713 |
+ }) |
|
| 714 |
+} |
|
| 715 |
+ |
|
| 538 | 716 |
// TestIPVlanDNS checks whether DNS is forwarded, for combinations of l2/l3 mode, |
| 539 | 717 |
// with/without a parent interface, and with '--internal'. Note that, there's no |
| 540 | 718 |
// attempt here to give the ipvlan network external connectivity - when this test |
| ... | ... |
@@ -4,10 +4,12 @@ package macvlan |
| 4 | 4 |
|
| 5 | 5 |
import ( |
| 6 | 6 |
"context" |
| 7 |
+ "net/netip" |
|
| 7 | 8 |
"strings" |
| 8 | 9 |
"testing" |
| 9 | 10 |
"time" |
| 10 | 11 |
|
| 12 |
+ "github.com/google/go-cmp/cmp/cmpopts" |
|
| 11 | 13 |
"github.com/moby/moby/api/types/network" |
| 12 | 14 |
"github.com/moby/moby/client" |
| 13 | 15 |
"github.com/moby/moby/v2/daemon/libnetwork/netlabel" |
| ... | ... |
@@ -480,6 +482,10 @@ func TestMacvlanIPAM(t *testing.T) {
|
| 480 | 480 |
expIPv4: true, |
| 481 | 481 |
}, |
| 482 | 482 |
} |
| 483 |
+ var ( |
|
| 484 |
+ subnetv4 = netip.MustParsePrefix("10.66.77.0/24")
|
|
| 485 |
+ subnetv6 = netip.MustParsePrefix("2001:db8:abcd::/64")
|
|
| 486 |
+ ) |
|
| 483 | 487 |
|
| 484 | 488 |
for _, tc := range testcases {
|
| 485 | 489 |
t.Run(tc.name, func(t *testing.T) {
|
| ... | ... |
@@ -490,6 +496,21 @@ func TestMacvlanIPAM(t *testing.T) {
|
| 490 | 490 |
net.WithMacvlan(""),
|
| 491 | 491 |
net.WithOption("macvlan_mode", "bridge"),
|
| 492 | 492 |
net.WithIPv4(tc.enableIPv4), |
| 493 |
+ net.WithIPAMConfig( |
|
| 494 |
+ network.IPAMConfig{
|
|
| 495 |
+ Subnet: subnetv4.String(), |
|
| 496 |
+ IPRange: "10.66.77.64/30", |
|
| 497 |
+ Gateway: "10.66.77.1", |
|
| 498 |
+ AuxAddress: map[string]string{
|
|
| 499 |
+ "inrange": "10.66.77.65", |
|
| 500 |
+ "outofrange": "10.66.77.128", |
|
| 501 |
+ }, |
|
| 502 |
+ }, |
|
| 503 |
+ network.IPAMConfig{
|
|
| 504 |
+ Subnet: subnetv6.String(), |
|
| 505 |
+ IPRange: "2001:db8:abcd::/120", |
|
| 506 |
+ }, |
|
| 507 |
+ ), |
|
| 493 | 508 |
} |
| 494 | 509 |
if tc.enableIPv6 {
|
| 495 | 510 |
netOpts = append(netOpts, net.WithIPv6()) |
| ... | ... |
@@ -507,10 +528,15 @@ func TestMacvlanIPAM(t *testing.T) {
|
| 507 | 507 |
assert.Check(t, is.Contains(loRes.Combined(), " inet ")) |
| 508 | 508 |
assert.Check(t, is.Contains(loRes.Combined(), " inet6 ")) |
| 509 | 509 |
|
| 510 |
+ wantSubnetStatus := make(map[netip.Prefix]network.SubnetStatus) |
|
| 510 | 511 |
eth0Res := container.ExecT(ctx, t, c, id, []string{"ip", "a", "show", "dev", "eth0"})
|
| 511 | 512 |
if tc.enableIPv4 || tc.expIPv4 {
|
| 512 | 513 |
assert.Check(t, is.Contains(eth0Res.Combined(), " inet "), |
| 513 | 514 |
"Expected IPv4 in: %s", eth0Res.Combined()) |
| 515 |
+ wantSubnetStatus[subnetv4] = network.SubnetStatus{
|
|
| 516 |
+ IPsInUse: 6, // network, gateway, 2x aux, broadcast, container |
|
| 517 |
+ DynamicIPsAvailable: 2, // container, aux "inrange" |
|
| 518 |
+ } |
|
| 514 | 519 |
} else {
|
| 515 | 520 |
assert.Check(t, !strings.Contains(eth0Res.Combined(), " inet "), |
| 516 | 521 |
"Expected no IPv4 in: %s", eth0Res.Combined()) |
| ... | ... |
@@ -518,6 +544,10 @@ func TestMacvlanIPAM(t *testing.T) {
|
| 518 | 518 |
if tc.enableIPv6 {
|
| 519 | 519 |
assert.Check(t, is.Contains(eth0Res.Combined(), " inet6 "), |
| 520 | 520 |
"Expected IPv6 in: %s", eth0Res.Combined()) |
| 521 |
+ wantSubnetStatus[subnetv6] = network.SubnetStatus{
|
|
| 522 |
+ IPsInUse: 2, // subnet-router anycast, container |
|
| 523 |
+ DynamicIPsAvailable: 254, |
|
| 524 |
+ } |
|
| 521 | 525 |
} else {
|
| 522 | 526 |
assert.Check(t, !strings.Contains(eth0Res.Combined(), " inet6 "), |
| 523 | 527 |
"Expected no IPv6 in: %s", eth0Res.Combined()) |
| ... | ... |
@@ -529,10 +559,188 @@ func TestMacvlanIPAM(t *testing.T) {
|
| 529 | 529 |
expDisableIPv6 = "0" |
| 530 | 530 |
} |
| 531 | 531 |
assert.Check(t, is.Equal(strings.TrimSpace(sysctlRes.Combined()), expDisableIPv6)) |
| 532 |
+ |
|
| 533 |
+ cc := d.NewClientT(t, client.WithVersion("1.52"))
|
|
| 534 |
+ inspect, err := cc.NetworkInspect(ctx, netName, client.NetworkInspectOptions{})
|
|
| 535 |
+ if assert.Check(t, err) && assert.Check(t, inspect.Status != nil) {
|
|
| 536 |
+ assert.Check(t, is.DeepEqual(wantSubnetStatus, inspect.Status.IPAM.Subnets, cmpopts.EquateEmpty())) |
|
| 537 |
+ } |
|
| 538 |
+ cc.Close() |
|
| 539 |
+ cc = d.NewClientT(t, client.WithVersion("1.51"))
|
|
| 540 |
+ inspect, err = cc.NetworkInspect(ctx, netName, client.NetworkInspectOptions{})
|
|
| 541 |
+ assert.Check(t, err) |
|
| 542 |
+ assert.Check(t, inspect.Status == nil) |
|
| 543 |
+ cc.Close() |
|
| 532 | 544 |
}) |
| 533 | 545 |
} |
| 534 | 546 |
} |
| 535 | 547 |
|
| 548 |
+// MACVLAN networks are allowed to be assigned IPAM subnets that overlap with |
|
| 549 |
+// other MACVLAN networks' IPAM subnets. But no two MACVLAN endpoints may be |
|
| 550 |
+// assigned the same address, even when the endpoints are attached to different |
|
| 551 |
+// networks. The assignment of an address to an endpoint on one network may |
|
| 552 |
+// therefore reduce the number of available addresses to assign to other |
|
| 553 |
+// networks' endpoints. |
|
| 554 |
+func TestMacvlanIPAMOverlap(t *testing.T) {
|
|
| 555 |
+ skip.If(t, testEnv.IsRemoteDaemon) |
|
| 556 |
+ skip.If(t, testEnv.IsRootless, "rootless mode has different view of network") |
|
| 557 |
+ |
|
| 558 |
+ ctx := testutil.StartSpan(baseContext, t) |
|
| 559 |
+ d := daemon.New(t) |
|
| 560 |
+ d.StartWithBusybox(ctx, t) |
|
| 561 |
+ defer d.Stop(t) |
|
| 562 |
+ |
|
| 563 |
+ c := d.NewClientT(t) |
|
| 564 |
+ |
|
| 565 |
+ checkNetworkIPAMState := func(networkID string, want map[netip.Prefix]network.SubnetStatus) bool {
|
|
| 566 |
+ t.Helper() |
|
| 567 |
+ nw, err := c.NetworkInspect(ctx, networkID, client.NetworkInspectOptions{})
|
|
| 568 |
+ if assert.Check(t, err) && assert.Check(t, nw.Status != nil) {
|
|
| 569 |
+ return assert.Check(t, is.DeepEqual(want, nw.Status.IPAM.Subnets, cmpopts.EquateEmpty())) |
|
| 570 |
+ } |
|
| 571 |
+ return false |
|
| 572 |
+ } |
|
| 573 |
+ |
|
| 574 |
+ // Create three networks with joined and overlapped IPAM ranges |
|
| 575 |
+ // and verify that the IPAM state is correct |
|
| 576 |
+ |
|
| 577 |
+ const ( |
|
| 578 |
+ netName1 = "macvlannet1" |
|
| 579 |
+ netName2 = "macvlannet2" |
|
| 580 |
+ netName3 = "macvlannet3" |
|
| 581 |
+ ) |
|
| 582 |
+ cidrv4 := netip.MustParsePrefix("192.168.0.0/24")
|
|
| 583 |
+ cidrv6 := netip.MustParsePrefix("2001:db8:abcd::/64")
|
|
| 584 |
+ |
|
| 585 |
+ net.CreateNoError(ctx, t, c, netName1, |
|
| 586 |
+ net.WithMacvlan(""),
|
|
| 587 |
+ net.WithIPv6(), |
|
| 588 |
+ net.WithIPAMConfig( |
|
| 589 |
+ network.IPAMConfig{
|
|
| 590 |
+ Subnet: cidrv4.String(), |
|
| 591 |
+ IPRange: "192.168.0.0/25", |
|
| 592 |
+ Gateway: "192.168.0.1", |
|
| 593 |
+ AuxAddress: map[string]string{
|
|
| 594 |
+ "reserved": "192.168.0.100", |
|
| 595 |
+ }, |
|
| 596 |
+ }, |
|
| 597 |
+ network.IPAMConfig{
|
|
| 598 |
+ Subnet: cidrv6.String(), |
|
| 599 |
+ IPRange: "2001:db8:abcd::/124", |
|
| 600 |
+ }, |
|
| 601 |
+ ), |
|
| 602 |
+ ) |
|
| 603 |
+ defer c.NetworkRemove(ctx, netName1) |
|
| 604 |
+ assert.Check(t, n.IsNetworkAvailable(ctx, c, netName1)) |
|
| 605 |
+ |
|
| 606 |
+ checkNetworkIPAMState(netName1, map[netip.Prefix]network.SubnetStatus{
|
|
| 607 |
+ cidrv4: {
|
|
| 608 |
+ IPsInUse: 4, |
|
| 609 |
+ DynamicIPsAvailable: 125, |
|
| 610 |
+ }, |
|
| 611 |
+ cidrv6: {
|
|
| 612 |
+ IPsInUse: 1, |
|
| 613 |
+ DynamicIPsAvailable: 15, |
|
| 614 |
+ }, |
|
| 615 |
+ }) |
|
| 616 |
+ |
|
| 617 |
+ net.CreateNoError(ctx, t, c, netName2, |
|
| 618 |
+ net.WithMacvlan(""),
|
|
| 619 |
+ net.WithIPv6(), |
|
| 620 |
+ net.WithIPAMConfig( |
|
| 621 |
+ network.IPAMConfig{
|
|
| 622 |
+ Subnet: cidrv4.String(), |
|
| 623 |
+ IPRange: "192.168.0.0/24", |
|
| 624 |
+ }, |
|
| 625 |
+ network.IPAMConfig{
|
|
| 626 |
+ Subnet: cidrv6.String(), |
|
| 627 |
+ IPRange: "2001:db8:abcd::/120", |
|
| 628 |
+ }, |
|
| 629 |
+ ), |
|
| 630 |
+ ) |
|
| 631 |
+ |
|
| 632 |
+ defer c.NetworkRemove(ctx, netName2) |
|
| 633 |
+ assert.Check(t, n.IsNetworkAvailable(ctx, c, netName2)) |
|
| 634 |
+ |
|
| 635 |
+ checkNetworkIPAMState(netName2, map[netip.Prefix]network.SubnetStatus{
|
|
| 636 |
+ cidrv4: {
|
|
| 637 |
+ IPsInUse: 4, |
|
| 638 |
+ DynamicIPsAvailable: 252, |
|
| 639 |
+ }, |
|
| 640 |
+ cidrv6: {
|
|
| 641 |
+ IPsInUse: 1, |
|
| 642 |
+ DynamicIPsAvailable: 255, |
|
| 643 |
+ }, |
|
| 644 |
+ }) |
|
| 645 |
+ |
|
| 646 |
+ net.CreateNoError(ctx, t, c, netName3, |
|
| 647 |
+ net.WithMacvlan(""),
|
|
| 648 |
+ net.WithIPv6(), |
|
| 649 |
+ net.WithIPAMConfig( |
|
| 650 |
+ network.IPAMConfig{
|
|
| 651 |
+ Subnet: cidrv4.String(), |
|
| 652 |
+ IPRange: "192.168.0.128/25", |
|
| 653 |
+ }, |
|
| 654 |
+ network.IPAMConfig{
|
|
| 655 |
+ Subnet: cidrv6.String(), |
|
| 656 |
+ IPRange: "2001:db8:abcd::80/124", |
|
| 657 |
+ }, |
|
| 658 |
+ ), |
|
| 659 |
+ ) |
|
| 660 |
+ |
|
| 661 |
+ defer c.NetworkRemove(ctx, netName3) |
|
| 662 |
+ assert.Check(t, n.IsNetworkAvailable(ctx, c, netName3)) |
|
| 663 |
+ |
|
| 664 |
+ checkNetworkIPAMState(netName3, map[netip.Prefix]network.SubnetStatus{
|
|
| 665 |
+ cidrv4: {
|
|
| 666 |
+ IPsInUse: 4, |
|
| 667 |
+ DynamicIPsAvailable: 127, |
|
| 668 |
+ }, |
|
| 669 |
+ cidrv6: {
|
|
| 670 |
+ IPsInUse: 1, |
|
| 671 |
+ DynamicIPsAvailable: 16, |
|
| 672 |
+ }, |
|
| 673 |
+ }) |
|
| 674 |
+ |
|
| 675 |
+ // Create a container on one of the networks |
|
| 676 |
+ id := container.Run(ctx, t, c, container.WithNetworkMode(netName1)) |
|
| 677 |
+ defer c.ContainerRemove(ctx, id, client.ContainerRemoveOptions{Force: true})
|
|
| 678 |
+ |
|
| 679 |
+ // Verify that the IPAM status of all three networks are affected. |
|
| 680 |
+ checkNetworkIPAMState(netName1, map[netip.Prefix]network.SubnetStatus{
|
|
| 681 |
+ cidrv4: {
|
|
| 682 |
+ IPsInUse: 5, |
|
| 683 |
+ DynamicIPsAvailable: 124, |
|
| 684 |
+ }, |
|
| 685 |
+ cidrv6: {
|
|
| 686 |
+ IPsInUse: 2, |
|
| 687 |
+ DynamicIPsAvailable: 14, |
|
| 688 |
+ }, |
|
| 689 |
+ }) |
|
| 690 |
+ |
|
| 691 |
+ checkNetworkIPAMState(netName2, map[netip.Prefix]network.SubnetStatus{
|
|
| 692 |
+ cidrv4: {
|
|
| 693 |
+ IPsInUse: 5, |
|
| 694 |
+ DynamicIPsAvailable: 251, |
|
| 695 |
+ }, |
|
| 696 |
+ cidrv6: {
|
|
| 697 |
+ IPsInUse: 2, |
|
| 698 |
+ DynamicIPsAvailable: 254, |
|
| 699 |
+ }, |
|
| 700 |
+ }) |
|
| 701 |
+ |
|
| 702 |
+ checkNetworkIPAMState(netName3, map[netip.Prefix]network.SubnetStatus{
|
|
| 703 |
+ cidrv4: {
|
|
| 704 |
+ IPsInUse: 5, |
|
| 705 |
+ DynamicIPsAvailable: 127, |
|
| 706 |
+ }, |
|
| 707 |
+ cidrv6: {
|
|
| 708 |
+ IPsInUse: 2, |
|
| 709 |
+ DynamicIPsAvailable: 16, |
|
| 710 |
+ }, |
|
| 711 |
+ }) |
|
| 712 |
+} |
|
| 713 |
+ |
|
| 536 | 714 |
// TestMACVlanDNS checks whether DNS is forwarded, with/without a parent |
| 537 | 715 |
// interface, and with '--internal'. Note that there's no attempt here to give |
| 538 | 716 |
// the macvlan network external connectivity - when this test supplies a parent |
| ... | ... |
@@ -20,4 +20,8 @@ type Inspect struct {
|
| 20 | 20 |
// swarm scope networks, and omitted for local scope networks. |
| 21 | 21 |
// |
| 22 | 22 |
Services map[string]ServiceInfo `json:"Services,omitempty"` |
| 23 |
+ |
|
| 24 |
+ // provides runtime information about the network such as the number of allocated IPs. |
|
| 25 |
+ // |
|
| 26 |
+ Status *Status `json:"Status,omitempty"` |
|
| 23 | 27 |
} |
| 28 | 30 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,16 @@ |
| 0 |
+// Code generated by go-swagger; DO NOT EDIT. |
|
| 1 |
+ |
|
| 2 |
+package network |
|
| 3 |
+ |
|
| 4 |
+// This file was generated by the swagger tool. |
|
| 5 |
+// Editing this file might prove futile when you re-run the swagger generate command |
|
| 6 |
+ |
|
| 7 |
+// IPAMStatus IPAM status |
|
| 8 |
+// |
|
| 9 |
+// swagger:model IPAMStatus |
|
| 10 |
+type IPAMStatus struct {
|
|
| 11 |
+ |
|
| 12 |
+ // subnets |
|
| 13 |
+ // Example: {"172.16.0.0/16":{"DynamicIPsAvailable":65533,"IPsInUse":3},"2001:db8:abcd:0012::0/96":{"DynamicIPsAvailable":4294967291,"IPsInUse":5}}
|
|
| 14 |
+ Subnets SubnetStatuses `json:"Subnets,omitempty"` |
|
| 15 |
+} |
| 0 | 16 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,15 @@ |
| 0 |
+// Code generated by go-swagger; DO NOT EDIT. |
|
| 1 |
+ |
|
| 2 |
+package network |
|
| 3 |
+ |
|
| 4 |
+// This file was generated by the swagger tool. |
|
| 5 |
+// Editing this file might prove futile when you re-run the swagger generate command |
|
| 6 |
+ |
|
| 7 |
+// Status provides runtime information about the network such as the number of allocated IPs. |
|
| 8 |
+// |
|
| 9 |
+// swagger:model Status |
|
| 10 |
+type Status struct {
|
|
| 11 |
+ |
|
| 12 |
+ // IPAM |
|
| 13 |
+ IPAM IPAMStatus `json:"IPAM"` |
|
| 14 |
+} |
| 0 | 15 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,20 @@ |
| 0 |
+// Code generated by go-swagger; DO NOT EDIT. |
|
| 1 |
+ |
|
| 2 |
+package network |
|
| 3 |
+ |
|
| 4 |
+// This file was generated by the swagger tool. |
|
| 5 |
+// Editing this file might prove futile when you re-run the swagger generate command |
|
| 6 |
+ |
|
| 7 |
+// SubnetStatus subnet status |
|
| 8 |
+// |
|
| 9 |
+// swagger:model SubnetStatus |
|
| 10 |
+type SubnetStatus struct {
|
|
| 11 |
+ |
|
| 12 |
+ // Number of IP addresses in the subnet that are in use or reserved and are therefore unavailable for allocation, saturating at 2<sup>64</sup> - 1. |
|
| 13 |
+ // |
|
| 14 |
+ IPsInUse uint64 `json:"IPsInUse"` |
|
| 15 |
+ |
|
| 16 |
+ // Number of IP addresses within the network's IPRange for the subnet that are available for allocation, saturating at 2<sup>64</sup> - 1. |
|
| 17 |
+ // |
|
| 18 |
+ DynamicIPsAvailable uint64 `json:"DynamicIPsAvailable"` |
|
| 19 |
+} |