Browse code

api,daemon: report IPAM status for network

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>

Cory Snider authored on 2025/09/09 05:43:29
Showing 24 changed files
... ...
@@ -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
 }
... ...
@@ -22,6 +22,8 @@ type IPAMConfig struct {
22 22
 	AuxAddress map[string]string `json:"AuxiliaryAddresses,omitempty"`
23 23
 }
24 24
 
25
+type SubnetStatuses = map[netip.Prefix]SubnetStatus
26
+
25 27
 type ipFamily string
26 28
 
27 29
 const (
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
+}
... ...
@@ -66,3 +66,10 @@ func (x Uint128) Fill16(a *[16]byte) {
66 66
 func (x Uint128) Uint64() uint64 {
67 67
 	return x.lo
68 68
 }
69
+
70
+func (x Uint128) Uint64Sat() uint64 {
71
+	if x.hi != 0 {
72
+		return ^uint64(0)
73
+	}
74
+	return x.lo
75
+}
... ...
@@ -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
 	}
... ...
@@ -215,4 +215,5 @@ type PluginDisableConfig struct {
215 215
 // NetworkListConfig stores the options available for listing networks
216 216
 type NetworkListConfig struct {
217 217
 	WithServices bool
218
+	WithStatus   bool
218 219
 }
... ...
@@ -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
 }
... ...
@@ -22,6 +22,8 @@ type IPAMConfig struct {
22 22
 	AuxAddress map[string]string `json:"AuxiliaryAddresses,omitempty"`
23 23
 }
24 24
 
25
+type SubnetStatuses = map[netip.Prefix]SubnetStatus
26
+
25 27
 type ipFamily string
26 28
 
27 29
 const (
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
+}