ipaddons 0.3.2__tar.gz → 0.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ipaddons
3
- Version: 0.3.2
3
+ Version: 0.4.0
4
4
  Summary: Additional tools for the ipaddress standard library module
5
5
  Project-URL: documentation, https://ipaddons.readthedocs.io/
6
6
  Project-URL: homepage, https://gitlab.com/sebastianw/ipaddons/
@@ -31,6 +31,7 @@ urls.repository = "https://gitlab.com/sebastianw/ipaddons/"
31
31
 
32
32
  [dependency-groups]
33
33
  dev = [
34
+ "coverage>=7.11.3",
34
35
  "mypy>=1.18.2",
35
36
  "pytest>=8.4.2",
36
37
  ]
@@ -68,7 +69,6 @@ lint.extend-select = [
68
69
  "BLE", # flake8-blind-except Don't catch base exceptions
69
70
  "C4", # flake8-comprehensions Unneccessary comprehension code
70
71
  "D", # pydocstyle
71
- "DOC", # pydoclint
72
72
  "DTZ", # flake8-datetimez Datetime mistakes
73
73
  "E", # pycodestyle errors
74
74
  "EM", # flake8-errmsg Bad message strings in exception handling
@@ -103,6 +103,8 @@ lint.extend-select = [
103
103
  ]
104
104
  lint.ignore = [
105
105
  "D100", # Public module documentation missing
106
+ "D203", # Blank line before doc-string
107
+ "D212", # new-line after doc-string begins
106
108
  "E501", # Line too long
107
109
  "G004", # Logging statement uses f-string
108
110
  "S101", # Use of assert detected
@@ -110,6 +112,12 @@ lint.ignore = [
110
112
  "S603", # Subprocess call
111
113
  "SIM105", # Use contextlib.suppress({exception}) instead of try-except-pass -> slower code
112
114
  ]
115
+ lint.per-file-ignores."tests/**/*.py" = [
116
+ "D",
117
+ "DOC",
118
+ "I002",
119
+ "INP",
120
+ ]
113
121
  lint.flake8-comprehensions.allow-dict-calls-with-keyword-arguments = true
114
122
  lint.flake8-import-conventions.aliases.datetime = "dt"
115
123
  lint.flake8-import-conventions.banned-from = [ "datetime" ]
@@ -117,13 +125,6 @@ lint.isort.required-imports = [ "from __future__ import annotations" ]
117
125
  # Enable "FA" rule fixes
118
126
  lint.future-annotations = true
119
127
 
120
- per-file-ignores."tests/**/*.py" = [
121
- "D",
122
- "DOC",
123
- "I002",
124
- "INP",
125
- ]
126
-
127
128
  [tool.pyproject-fmt]
128
129
  max_supported_python = "3.14"
129
130
  column_width = 120
@@ -1,6 +1,8 @@
1
+ """Various addons for the :doc:`ipaddress <python:library/ipaddress>` standard library module."""
2
+
1
3
  from __future__ import annotations
2
4
 
3
- from ._version import __version__, __version_tuple__
5
+ from ._version import __version__, __version_tuple__ # noqa: F401
4
6
  from .tools import IPv4Allocation, IPv6Allocation, ip_allocation
5
7
 
6
8
  __all__ = ["IPv4Allocation", "IPv6Allocation", "ip_allocation"]
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '0.3.2'
32
- __version_tuple__ = version_tuple = (0, 3, 2)
31
+ __version__ = version = '0.4.0'
32
+ __version_tuple__ = version_tuple = (0, 4, 0)
33
33
 
34
34
  __commit_id__ = commit_id = None
@@ -18,11 +18,14 @@ if TYPE_CHECKING:
18
18
 
19
19
 
20
20
  class IPAllocationError(Exception):
21
- pass
21
+ """General Error in an IPAllocation object."""
22
22
 
23
23
 
24
- def ip_allocation(supernet: UnSpecNet, used_networks: UnspecNetSeq | None = None) -> IPv4Allocation | IPv6Allocation:
25
- """Create an IP allocation.
24
+ def ip_allocation(
25
+ supernet: UnSpecNet, used_networks: UnspecNetSeq | None = None, ignore_invalid_subnets: bool = True
26
+ ) -> IPv4Allocation | IPv6Allocation:
27
+ """
28
+ Create an IP allocation.
26
29
 
27
30
  Creates and returns an IP allocation object.
28
31
 
@@ -32,16 +35,18 @@ def ip_allocation(supernet: UnSpecNet, used_networks: UnspecNetSeq | None = None
32
35
  either as a string or an :doc:`ipaddress <python:library/ipaddress>` network object.
33
36
  :return: An :class:`IPv4Allocation` or :class:`IPv6Allocation` instance, depending on the passed
34
37
  :py:attr:`supernet` argument. Same principle as :py:func:`python:ipaddress.ip_network`.
38
+ :param ignore_invalid_subnets: If set to False, used subnets that do not overlap with the supernet will raise
39
+ a ValueError.
35
40
  """
36
41
  if used_networks is None:
37
42
  used_networks = []
38
43
  supernet = ipaddress.ip_network(supernet) if isinstance(supernet, str) else supernet
39
44
  if supernet.version == 4:
40
45
  assert isinstance(supernet, ipaddress.IPv4Network)
41
- return IPv4Allocation(supernet, used_networks)
46
+ return IPv4Allocation(supernet, used_networks, ignore_invalid_subnets)
42
47
  if supernet.version == 6:
43
48
  assert isinstance(supernet, ipaddress.IPv6Network)
44
- return IPv6Allocation(supernet, used_networks)
49
+ return IPv6Allocation(supernet, used_networks, ignore_invalid_subnets)
45
50
  msg = f"IP Version of Supernet is unknown: {supernet.version}"
46
51
  raise ValueError(msg)
47
52
 
@@ -51,7 +56,8 @@ def _ceildiv(a: int, b: int) -> int:
51
56
 
52
57
 
53
58
  def range_overlap(a: NetworkRange, b: NetworkRange) -> bool:
54
- """Network Range Overlap Check.
59
+ """
60
+ Network Range Overlap Check.
55
61
 
56
62
  :param a: a network range
57
63
  :param b: a network range
@@ -61,7 +67,8 @@ def range_overlap(a: NetworkRange, b: NetworkRange) -> bool:
61
67
 
62
68
 
63
69
  def range_overlaps(a: NetworkRange, b: list[NetworkRange]) -> NetworkRange | None:
64
- """Overlap between a network range and a list of other network ranges.
70
+ """
71
+ Overlap between a network range and a list of other network ranges.
65
72
 
66
73
  :param a: Network range to check against.
67
74
  :param b: List of network ranges that could overlap.
@@ -74,7 +81,8 @@ def range_overlaps(a: NetworkRange, b: list[NetworkRange]) -> NetworkRange | Non
74
81
 
75
82
 
76
83
  def netrange(network: IPNetwork) -> NetworkRange:
77
- """Create a network range from an :doc:python:`ipaddress <library/ipaddress>` object.
84
+ """
85
+ Create a network range from an :doc:python:`ipaddress <library/ipaddress>` object.
78
86
 
79
87
  A network range is the integer presentation of the first and last IP address of the network as a tuple.
80
88
  :param network: An :doc:`ipaddress <python:library/ipaddress>` network object.
@@ -84,30 +92,45 @@ def netrange(network: IPNetwork) -> NetworkRange:
84
92
 
85
93
 
86
94
  def free_ranges(superrange: NetworkRange, used_ranges: list[NetworkRange]) -> Generator[NetworkRange, None, None]:
87
- """Get free ranges from a supernet with provided used ranges.
95
+ """
96
+ Get free ranges from a supernet with provided used ranges.
88
97
 
89
98
  :param superrange: Covering range from where free subranges should be returned.
90
99
  :param used_ranges: List of used network ranges.
91
100
  :return: An interator for free ranges.
92
101
  """
93
- used_ranges = sorted(used_ranges)
94
- for range in used_ranges:
95
- if superrange[0] >= superrange[1]:
96
- # Last range either ends at or shoots over the supernet range
102
+ remaining_superrange = superrange
103
+ # Make values unique and sort them. This is crucial when interating over them.
104
+ used_ranges = list(dict.fromkeys(used_ranges))
105
+ used_ranges = sorted(used_ranges, key=lambda x: (x[0], -x[1]))
106
+ for used_range in used_ranges:
107
+ if not range_overlap(superrange, used_range):
108
+ msg = f"{used_range} does not overlap with {remaining_superrange}"
109
+ raise ValueError(msg)
110
+ if used_range[0] <= remaining_superrange[0] and used_range[1] >= remaining_superrange[1]:
111
+ # Subrange is as large or larger than the remaining superrange so we have no more free ranges.
97
112
  return
98
- if superrange[0] == range[0]:
99
- # used range is at the beginning, shrink superrange
100
- superrange = range[1] + 1, superrange[1]
113
+ if used_range[1] <= remaining_superrange[0] or used_range[0] >= remaining_superrange[1]:
114
+ # Subrange ends below or begins above remaining superrange, ignore.
101
115
  continue
102
- free_range = superrange[0], range[0] - 1
103
- superrange = range[1] + 1, superrange[1]
116
+ if remaining_superrange[0] == used_range[0]:
117
+ # Used range is at the beginning of remaining superrange, shrink it.
118
+ remaining_superrange = used_range[1] + 1, remaining_superrange[1]
119
+ continue
120
+ # There is a free range between the beginning of the remaining superrange and the current used range.
121
+ free_range = remaining_superrange[0], used_range[0] - 1
122
+ # Adjust remaining free superrange to begin after current used range.
123
+ remaining_superrange = used_range[1] + 1, remaining_superrange[1]
124
+ assert free_range[0] < free_range[1]
104
125
  yield free_range
105
126
  # The rest of the superrange is free when there are no more used ranges left
106
- yield superrange
127
+ assert remaining_superrange[0] < remaining_superrange[1]
128
+ yield remaining_superrange
107
129
 
108
130
 
109
131
  def merge_ranges(ranges: list[NetworkRange]) -> list[NetworkRange]:
110
- """Merge network ranges that overlap.
132
+ """
133
+ Merge network ranges that overlap.
111
134
 
112
135
  :param ranges: A list of network ranges.
113
136
  :return: A list of network ranges with overlapping ranges merged into one.
@@ -132,7 +155,8 @@ def merge_ranges(ranges: list[NetworkRange]) -> list[NetworkRange]:
132
155
  def net_size_iterator(
133
156
  supernet: IPNetwork, cidr: int, used_ranges: list[NetworkRange] | None = None
134
157
  ) -> Generator[NetworkRange, None, None]:
135
- """Iterate over an ipaddr network object and return subnet network ranges of the specified cidr size.
158
+ """
159
+ Iterate over an ipaddr network object and return subnet network ranges of the specified cidr size.
136
160
 
137
161
  :param supernet: An ipaddr network object.
138
162
  :param cidr: CIDR size of the subnets.
@@ -154,7 +178,8 @@ def net_size_iterator(
154
178
 
155
179
 
156
180
  class _BaseIPAllocation(ABC, Generic[_N]):
157
- """An IP allocation.
181
+ """
182
+ An IP allocation.
158
183
 
159
184
  The object represents a supernet (the allocation) with an optional list of used (assigned) subnets. You can also
160
185
  request an iterator for free subnets of a specific size.
@@ -163,14 +188,15 @@ class _BaseIPAllocation(ABC, Generic[_N]):
163
188
  network object.
164
189
  :param used_subnets: A list of subnets of the :py:attr:`supernet` that are already in use in this allocation,
165
190
  either as string or an :doc:`ipaddress <python:library/ipaddress>` network object.
191
+ :param ignore_invalid_subnets: If set to False, used subnets that do not overlap with the supernet will raise
192
+ a ValueError.
166
193
  """
167
194
 
168
- _version: int | None = None
169
- _max_prefixlen: int = 0
170
-
171
- def __init__(self, supernet: UnSpecNet, used_subnets: UnspecNetSeq | None = None):
195
+ def __init__(
196
+ self, supernet: UnSpecNet, used_subnets: UnspecNetSeq | None = None, ignore_invalid_subnets: bool = True
197
+ ):
172
198
  self.lock = RLock()
173
- self.merge_ranges = merge_ranges
199
+ self.ignore_invalid_subnets = ignore_invalid_subnets
174
200
  self.supernet: _N = self._normalize_network(supernet)
175
201
  if used_subnets is None:
176
202
  used_subnets = []
@@ -186,6 +212,16 @@ class _BaseIPAllocation(ABC, Generic[_N]):
186
212
  def _network_class(self) -> type[_N]:
187
213
  raise NotImplementedError
188
214
 
215
+ @property
216
+ @abstractmethod
217
+ def _version(self) -> int:
218
+ raise NotImplementedError
219
+
220
+ @property
221
+ @abstractmethod
222
+ def _max_prefixlen(self) -> int:
223
+ raise NotImplementedError
224
+
189
225
  def _normalize_network(self, _network: UnSpecNet) -> _N:
190
226
  network = ipaddress.ip_network(_network) if isinstance(_network, str) else _network
191
227
  if not self._version_compatible(network):
@@ -199,9 +235,18 @@ class _BaseIPAllocation(ABC, Generic[_N]):
199
235
  def _version_compatible(self, network: IPNetwork) -> TypeGuard[_N]:
200
236
  return self._version == network.version
201
237
 
238
+ def _check_subnet(self, subnet: _N) -> bool:
239
+ if not range_overlap(netrange(self.supernet), netrange(subnet)):
240
+ if not self.ignore_invalid_subnets:
241
+ msg = f"Subnet {subnet} does not overlap with {self.supernet}"
242
+ raise ValueError(msg)
243
+ return False
244
+ return True
245
+
202
246
  @property
203
- def used_subnets(self) -> Generator[IPNetwork, None, None]:
204
- """Used subnets in this allocation.
247
+ def used_subnets(self) -> Generator[_N, None, None]:
248
+ """
249
+ Used subnets in this allocation.
205
250
 
206
251
  :getter: An iterator for used networks in this allocation.
207
252
  :setter: Sets the list of used networks.
@@ -212,17 +257,23 @@ class _BaseIPAllocation(ABC, Generic[_N]):
212
257
  @used_subnets.setter
213
258
  def used_subnets(self, subnets: UnspecNetSeq) -> None:
214
259
  with self.lock:
215
- self._used_subnets = self._normalize_networks(subnets)
216
- self._update_used_subnet_ranges()
260
+ self._used_subnets = []
261
+ self.add_used_subnets(subnets)
217
262
 
218
263
  def add_used_subnets(self, subnets: UnspecNetSeq) -> None:
219
- """Add additional subnets to the list of used networks in this allocation.
264
+ """
265
+ Add additional subnets to the list of used networks in this allocation.
220
266
 
221
267
  :param subnets: A list of networks that are added to the in-use networks of this allocation, either as a string
222
268
  or :doc:`ipaddress <python:library/ipaddress>` network objects.
223
269
  """
224
270
  with self.lock:
225
- self._used_subnets += self._normalize_networks(subnets)
271
+ for _subnet in subnets:
272
+ subnet = self._normalize_network(_subnet)
273
+ if not self._check_subnet(subnet):
274
+ continue
275
+ self._used_subnets.append(subnet)
276
+ self._used_subnets.sort()
226
277
  self._update_used_subnet_ranges()
227
278
 
228
279
  def _update_used_subnet_ranges(self, merge: bool = True) -> None:
@@ -233,14 +284,15 @@ class _BaseIPAllocation(ABC, Generic[_N]):
233
284
  self._used_network_ranges = ranges
234
285
 
235
286
  def get_free_subnets(self, prefixlen: int) -> Generator[_N, None, None]:
236
- """List free subnets of a specific size.
287
+ """
288
+ List free subnets of a specific size.
237
289
 
238
290
  :param prefixlen: Size of the free subnets returned in CIDR notation.
239
291
  :return: An iterator for free subnets of :py:attr:`prefixlen` size.
240
292
  """
241
293
  for n in net_size_iterator(self.supernet, prefixlen, self._used_network_ranges):
242
294
  if range_overlaps(n, self._used_network_ranges):
243
- msg = "This should never happen!"
295
+ msg = "This should never happen! A free range cannot overlap with a used range."
244
296
  raise IPAllocationError(msg)
245
297
  yield self._network_class((n[0], prefixlen))
246
298
 
@@ -0,0 +1,374 @@
1
+ from __future__ import annotations
2
+
3
+ import ipaddress
4
+ from typing import Any, NamedTuple
5
+
6
+ import pytest
7
+
8
+ from ipaddons import IPv4Allocation, IPv6Allocation, ip_allocation, tools
9
+
10
+
11
+ class Net(NamedTuple):
12
+ str: str
13
+ ipa: ipaddress.IPv4Network | ipaddress.IPv6Network
14
+ range: tuple[int, int]
15
+
16
+
17
+ def make_nets(input_str: str | list[str]) -> Net | list[Net]:
18
+ if isinstance(input_str, str):
19
+ single = True
20
+ input_str = [input_str]
21
+ else:
22
+ single = False
23
+
24
+ ret = []
25
+ for input_net in input_str:
26
+ net = ipaddress.ip_network(input_net, strict=True)
27
+ ret.append(Net(str(net), net, (int(net[0]), int(net[-1]))))
28
+
29
+ if single:
30
+ return ret[0]
31
+ return ret
32
+
33
+
34
+ def net_attr(nets: Net | list[Net], attribute: str) -> Any | list[Any]:
35
+ if isinstance(nets, Net):
36
+ return getattr(nets, attribute)
37
+ return [getattr(n, attribute) for n in nets]
38
+
39
+
40
+ v4_supernet = make_nets("10.0.0.0/8")
41
+
42
+ v4_subnets = make_nets(
43
+ [
44
+ "10.0.0.0/16",
45
+ "10.1.1.0/24",
46
+ "10.1.1.16/29",
47
+ "10.1.2.0/25",
48
+ "10.1.2.0/24",
49
+ "10.1.3.0/25",
50
+ "10.1.3.64/26",
51
+ "10.1.3.128/26",
52
+ ]
53
+ )
54
+
55
+ v6_supernet = make_nets("fc00::/32")
56
+
57
+ v6_subnets = make_nets(
58
+ [
59
+ "fc00::/48",
60
+ "fc00:0:a::/56",
61
+ "fc00:0:b::/56",
62
+ "fc00:0:b:100::/56",
63
+ "fc00:0:b:200::/64",
64
+ "fc00:0:b:100::/56",
65
+ ]
66
+ )
67
+
68
+
69
+ mergeable_subnets = [
70
+ [
71
+ (int(ipaddress.IPv4Network(n)[0]), int(ipaddress.IPv4Network(n)[-1]))
72
+ for n in ["192.168.0.0/24", "192.168.0.0/25", "192.168.0.128/25", "192.168.0.64/29"]
73
+ ],
74
+ [
75
+ (int(ipaddress.IPv6Network(n)[0]), int(ipaddress.IPv6Network(n)[-1]))
76
+ for n in ["fc01::/32", "fc01::/64", "fc01:0:ffff::/48", "fc01:0:cafe::/64"]
77
+ ],
78
+ ]
79
+
80
+
81
+ @pytest.mark.parametrize(
82
+ ("supernet", "subnets", "allocation_class", "network_class"),
83
+ [
84
+ (net_attr(v4_supernet, "ipa"), net_attr(v4_subnets, "ipa"), IPv4Allocation, ipaddress.IPv4Network),
85
+ (net_attr(v4_supernet, "str"), net_attr(v4_subnets, "str"), IPv4Allocation, ipaddress.IPv4Network),
86
+ (net_attr(v6_supernet, "ipa"), net_attr(v6_subnets, "ipa"), IPv6Allocation, ipaddress.IPv6Network),
87
+ (net_attr(v6_supernet, "str"), net_attr(v6_subnets, "str"), IPv6Allocation, ipaddress.IPv6Network),
88
+ (net_attr(v4_supernet, "ipa"), net_attr(v4_subnets[:1], "ipa"), IPv4Allocation, ipaddress.IPv4Network),
89
+ (net_attr(v6_supernet, "ipa"), net_attr(v6_subnets[:1], "ipa"), IPv6Allocation, ipaddress.IPv6Network),
90
+ (net_attr(v4_supernet, "ipa"), [], IPv4Allocation, ipaddress.IPv4Network),
91
+ (net_attr(v6_supernet, "ipa"), [], IPv6Allocation, ipaddress.IPv6Network),
92
+ ],
93
+ )
94
+ def test_allocation_classes(supernet, subnets, allocation_class, network_class):
95
+ a = ip_allocation(supernet, used_networks=subnets)
96
+ assert isinstance(a, allocation_class)
97
+ assert all(isinstance(n, network_class) for n in a.used_subnets)
98
+
99
+
100
+ def test_base_abstract_methods():
101
+ class TestAllocation(tools._BaseIPAllocation):
102
+ pass
103
+
104
+ with pytest.raises(
105
+ TypeError,
106
+ match=r"Can't instantiate abstract class TestAllocation without an implementation for abstract methods '_max_prefixlen', '_network_class', '_version'",
107
+ ):
108
+ TestAllocation()
109
+
110
+
111
+ @pytest.mark.parametrize(
112
+ ("supernet", "used_nets", "cidr", "first_free", "last_free", "num_subnets"),
113
+ [
114
+ ("10.0.0.0/8", ["10.0.0.0/16", "10.1.0.0/24"], 16, "10.2.0.0/16", "10.255.0.0/16", 254),
115
+ ("2001:db8::/32", ["2001:db8::/48", "2001:db8:1::/120"], 48, "2001:db8:2::/48", "2001:db8:ffff::/48", 65534),
116
+ ("10.0.0.0/8", ["10.5.0.0/16", "10.193.0.0/24"], 16, "10.0.0.0/16", "10.255.0.0/16", 254),
117
+ ("2001:db8::/32", ["2001:db8:4::/48", "2001:db8:fca::/120"], 48, "2001:db8::/48", "2001:db8:ffff::/48", 65534),
118
+ ("10.0.0.0/8", ["10.5.0.0/16", "10.255.0.0/24"], 16, "10.0.0.0/16", "10.254.0.0/16", 254),
119
+ ("2001:db8::/32", ["2001:db8:4::/48", "2001:db8:ffff::/120"], 48, "2001:db8::/48", "2001:db8:fffe::/48", 65534),
120
+ ],
121
+ )
122
+ def test_free_networks(supernet, used_nets, cidr, first_free, last_free, num_subnets):
123
+ allocation = ip_allocation(supernet, used_nets)
124
+ free_nets = list(allocation.get_free_subnets(cidr))
125
+ assert first_free == str(free_nets[0])
126
+ assert last_free == str(free_nets[-1])
127
+ assert num_subnets == len(free_nets)
128
+
129
+
130
+ @pytest.mark.parametrize(
131
+ ("subnet", "subnet_range"),
132
+ zip(
133
+ net_attr(v4_subnets, "ipa") + net_attr(v6_subnets, "ipa"),
134
+ net_attr(v4_subnets, "range") + net_attr(v6_subnets, "range"),
135
+ strict=True,
136
+ ),
137
+ )
138
+ def test_netrange(subnet, subnet_range):
139
+ assert tools.netrange(subnet) == subnet_range
140
+
141
+
142
+ @pytest.mark.parametrize(
143
+ ("supernet", "subnets", "subnet_ranges"),
144
+ [
145
+ (net_attr(v4_supernet, "ipa"), net_attr(v4_subnets, "ipa"), net_attr(v4_subnets, "range")),
146
+ (net_attr(v6_supernet, "ipa"), net_attr(v6_subnets, "ipa"), net_attr(v6_subnets, "range")),
147
+ ],
148
+ )
149
+ def test_allocation_ranges(supernet, subnets, subnet_ranges):
150
+ a = ip_allocation(supernet, used_networks=subnets, ignore_invalid_subnets=False)
151
+ a._update_used_subnet_ranges(merge=False)
152
+ assert sorted(a._used_network_ranges) == sorted(subnet_ranges)
153
+
154
+
155
+ @pytest.mark.parametrize(
156
+ ("supernet_range", "subnet_ranges"),
157
+ [
158
+ (net_attr(v4_supernet, "range"), net_attr(v4_subnets, "range")),
159
+ (net_attr(v6_supernet, "range"), net_attr(v6_subnets, "range")),
160
+ ],
161
+ )
162
+ def test_range_overlaps(supernet_range, subnet_ranges):
163
+ assert tools.range_overlaps(supernet_range, subnet_ranges) == subnet_ranges[0]
164
+
165
+
166
+ @pytest.mark.parametrize(
167
+ ("supernet_range", "subnet_ranges"),
168
+ [
169
+ (net_attr(v4_supernet, "range"), net_attr(v6_subnets, "range")),
170
+ (net_attr(v6_supernet, "range"), net_attr(v4_subnets, "range")),
171
+ ],
172
+ )
173
+ def test_range_no_overlaps(supernet_range, subnet_ranges):
174
+ assert tools.range_overlaps(supernet_range, subnet_ranges) is None
175
+
176
+
177
+ @pytest.mark.parametrize(
178
+ ("supernet", "subnets"),
179
+ [
180
+ (net_attr(v4_supernet, "ipa"), net_attr(v4_subnets, "ipa")),
181
+ (net_attr(v6_supernet, "ipa"), net_attr(v6_subnets, "ipa")),
182
+ ],
183
+ )
184
+ def test_add_used_subnets(supernet, subnets):
185
+ a = ip_allocation(supernet, ignore_invalid_subnets=False)
186
+ a.add_used_subnets([subnets[0]])
187
+ a.add_used_subnets(subnets[1:])
188
+ assert sorted(a.used_subnets) == sorted(subnets)
189
+
190
+
191
+ @pytest.mark.parametrize(
192
+ "subnets",
193
+ mergeable_subnets,
194
+ )
195
+ def test_merge(subnets):
196
+ covering_prefix = subnets[0]
197
+ assert tools.merge_ranges(subnets) == [covering_prefix]
198
+
199
+
200
+ @pytest.mark.parametrize(
201
+ "subnets",
202
+ mergeable_subnets,
203
+ )
204
+ def test_merge_one(subnets):
205
+ assert subnets[:1] == tools.merge_ranges(subnets[:1])
206
+
207
+
208
+ def test_merge_empty():
209
+ assert tools.merge_ranges([]) == []
210
+
211
+
212
+ @pytest.mark.parametrize(
213
+ ("supernet", "used_nets", "cidr", "first_free"),
214
+ [
215
+ ("2001:db8::/32", ["2001:db8::/48", "2001:db8:1::/120"], 64, "2001:db8:1:1::/64"),
216
+ ("10.0.0.0/8", ["10.0.0.0/16", "10.1.0.0/24"], 16, "10.2.0.0/16"),
217
+ ],
218
+ )
219
+ def test_netsize_iterator(supernet, used_nets, cidr, first_free):
220
+ supernet = ipaddress.ip_network(supernet)
221
+ used_nets = [(int(ipaddress.ip_network(n)[0]), int(ipaddress.ip_network(n)[-1])) for n in used_nets]
222
+ i = tools.net_size_iterator(supernet, cidr, used_nets)
223
+ first_ip, last_ip = next(i)
224
+ net = next(ipaddress.summarize_address_range(ipaddress.ip_address(first_ip), ipaddress.ip_address(last_ip)))
225
+ assert first_free == str(net)
226
+
227
+
228
+ @pytest.mark.parametrize(
229
+ ("supernet", "used_nets", "cidr", "last_free"),
230
+ [
231
+ ("10.0.0.0/24", ["10.0.0.0/29", "10.0.0.96/30"], 30, "10.0.0.252/30"),
232
+ ("2001:db8::/32", ["2001:db8::/48", "2001:db8:1::/120"], 48, "2001:db8:ffff::/48"),
233
+ ],
234
+ )
235
+ def test_netsize_iterator_last(supernet, used_nets, cidr, last_free):
236
+ supernet = ipaddress.ip_network(supernet)
237
+ used_nets = [(int(ipaddress.ip_network(n)[0]), int(ipaddress.ip_network(n)[-1])) for n in used_nets]
238
+ i = tools.net_size_iterator(supernet, cidr, used_nets)
239
+ first_ip, last_ip = list(i)[-1]
240
+ net = next(ipaddress.summarize_address_range(ipaddress.ip_address(first_ip), ipaddress.ip_address(last_ip)))
241
+ assert last_free == str(net)
242
+
243
+
244
+ @pytest.mark.parametrize(
245
+ ("supernet", "subnets", "used_networks"),
246
+ [
247
+ ("10.0.0.0/24", ["192.168.1.0/30", "10.0.0.0/25"], ["10.0.0.0/25"]),
248
+ ("fc00::/32", ["fd00::/64", "fc00::/64"], ["fc00::/64"]),
249
+ ],
250
+ )
251
+ def test_outside_networks(supernet, subnets, used_networks):
252
+ allocation = ip_allocation(supernet, subnets)
253
+ assert sorted([str(s) for s in allocation.used_subnets]) == sorted(used_networks)
254
+
255
+
256
+ @pytest.mark.parametrize(
257
+ ("supernet", "subnets", "used_networks"),
258
+ [
259
+ ("10.0.0.0/24", ["192.168.1.0/30", "10.0.0.0/25"], ["10.0.0.0/25"]),
260
+ ("fc00::/32", ["fd00::/64", "fc00::/64"], ["fc00::/64"]),
261
+ ],
262
+ )
263
+ def test_outside_networks_added(supernet, subnets, used_networks):
264
+ allocation = ip_allocation(supernet)
265
+ allocation.add_used_subnets(subnets)
266
+ assert sorted([str(s) for s in allocation.used_subnets]) == sorted(used_networks)
267
+
268
+
269
+ @pytest.mark.parametrize(
270
+ ("supernet", "subnets"),
271
+ [
272
+ ("10.0.0.0/24", ["192.168.1.0/30", "10.0.0.0/25"]),
273
+ ("fc00::/32", ["fd00::/64", "fc00::/64"]),
274
+ ],
275
+ )
276
+ def test_outside_networks_exception(supernet, subnets):
277
+ with pytest.raises(ValueError, match=r"Subnet .*? does not overlap with .*?"):
278
+ _ = ip_allocation(supernet, subnets, ignore_invalid_subnets=False)
279
+
280
+
281
+ @pytest.mark.parametrize(
282
+ ("supernet", "subnets"),
283
+ [
284
+ ("10.0.0.0/24", ["192.168.1.0/30", "10.0.0.0/25"]),
285
+ ("fc00::/32", ["fd00::/64", "fc00::/64"]),
286
+ ],
287
+ )
288
+ def test_outside_networks_added_exception(supernet, subnets):
289
+ allocation = ip_allocation(supernet, ignore_invalid_subnets=False)
290
+ with pytest.raises(ValueError, match=r" does not overlap with "):
291
+ allocation.add_used_subnets(subnets)
292
+
293
+
294
+ @pytest.mark.parametrize(
295
+ ("supernet_range", "subnet_ranges"),
296
+ [
297
+ (net_attr(v4_supernet, "range"), net_attr(v6_subnets, "range")),
298
+ (net_attr(v6_supernet, "range"), net_attr(v4_subnets, "range")),
299
+ ],
300
+ )
301
+ def test_free_ranges_mismatch(supernet_range, subnet_ranges):
302
+ range_iterator = tools.free_ranges(supernet_range, subnet_ranges)
303
+ with pytest.raises(ValueError, match=r" does not overlap with "):
304
+ next(range_iterator)
305
+
306
+
307
+ @pytest.mark.parametrize(
308
+ ("supernet", "subnets", "free_size"),
309
+ [
310
+ (make_nets("10.0.0.0/16"), make_nets(["10.0.0.0/8"]), 29),
311
+ (make_nets("fc00::/32"), make_nets(["fc00::/16"]), 64),
312
+ (make_nets("10.0.0.0/16"), make_nets(["10.0.0.0/17", "10.0.128.0/17"]), 29),
313
+ (make_nets("fc00::/32"), make_nets(["fc00::/33", "fc00:0:8000::/33"]), 64),
314
+ (make_nets("10.0.0.0/16"), make_nets(["10.0.0.0/17", "10.0.0.0/8"]), 29),
315
+ (make_nets("fc00::/32"), make_nets(["fc00::/33", "fc00::/16"]), 64),
316
+ (make_nets("10.0.0.0/16"), make_nets(["10.0.0.0/17", "10.0.0.0/17", "10.0.128.0/17"]), 29),
317
+ (make_nets("fc00::/32"), make_nets(["fc00::/33", "fc00::/33", "fc00:0:8000::/33"]), 64),
318
+ (make_nets("10.0.0.0/16"), make_nets(["10.0.0.0/17", "10.0.128.0/17", "10.0.128.0/17"]), 29),
319
+ (make_nets("fc00::/32"), make_nets(["fc00::/33", "fc00:0:8000::/33", "fc00:0:8000::/33"]), 64),
320
+ (make_nets("10.0.0.0/16"), make_nets(["10.0.0.0/17", "10.0.0.0/24", "10.0.128.0/17"]), 29),
321
+ (make_nets("fc00::/32"), make_nets(["fc00::/33", "fc00::/64", "fc00:0:8000::/33"]), 64),
322
+ ],
323
+ )
324
+ class TestNoFreeRanges:
325
+ def test_no_free_ranges(self, supernet, subnets, free_size):
326
+ range_iterator = tools.free_ranges(supernet.range, net_attr(subnets, "range"))
327
+ with pytest.raises(StopIteration):
328
+ next(range_iterator)
329
+
330
+ def test_no_free_subnets(self, supernet, subnets, free_size):
331
+ allocation = ip_allocation(supernet.ipa, net_attr(subnets, "ipa"))
332
+ with pytest.raises(StopIteration):
333
+ next(allocation.get_free_subnets(free_size))
334
+
335
+
336
+ @pytest.mark.parametrize(
337
+ ("supernet", "subnets"),
338
+ [
339
+ (net_attr(v4_supernet, "str"), net_attr(v6_subnets, "str")),
340
+ (net_attr(v6_supernet, "str"), net_attr(v4_subnets, "str")),
341
+ ],
342
+ )
343
+ def test_version_mismatch(supernet, subnets):
344
+ with pytest.raises(ValueError, match=r"IP Version missmatch"):
345
+ ip_allocation(supernet, subnets)
346
+
347
+ a = ip_allocation(supernet)
348
+ with pytest.raises(ValueError, match=r"IP Version missmatch"):
349
+ a._normalize_networks(subnets)
350
+
351
+
352
+ @pytest.mark.parametrize(
353
+ ("networks", "network_class"),
354
+ [
355
+ ([net_attr(v4_supernet, "str"), *net_attr(v4_subnets, "str")], ipaddress.IPv4Network),
356
+ ([net_attr(v6_supernet, "str"), *net_attr(v6_subnets, "str")], ipaddress.IPv6Network),
357
+ ],
358
+ )
359
+ def test_network_normalize(networks, network_class):
360
+ a = ip_allocation(networks[0])
361
+ assert all(isinstance(n, network_class) for n in a._normalize_networks(networks))
362
+ assert all(str(n) == networks[i] for i, n in enumerate(a._normalize_networks(networks)))
363
+
364
+
365
+ @pytest.mark.parametrize(
366
+ ("supernet", "subnets", "new_subnets"),
367
+ [
368
+ ("10.0.0.0/8", ["10.0.0.0/24", "10.1.0.0/24"], ["10.255.0.0/16", "10.254.30.0/24"]),
369
+ ],
370
+ )
371
+ def test_used_subnet_setter(supernet, subnets, new_subnets):
372
+ allocation = ip_allocation(supernet, subnets)
373
+ allocation.used_subnets = new_subnets
374
+ assert sorted([str(n) for n in allocation.used_subnets]) == sorted(new_subnets)
@@ -1,142 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import ipaddress
4
-
5
- import pytest
6
-
7
- from ipaddons import IPv4Allocation, IPv6Allocation, ip_allocation, tools
8
-
9
- v4_supernet_string = "10.0.0.0/8"
10
- v4_supernet = ipaddress.IPv4Network(v4_supernet_string)
11
-
12
- v4_subnet_strings = [
13
- "10.0.0.0/16",
14
- "10.1.1.0/24",
15
- "10.1.1.16/29",
16
- "10.1.2.0/25",
17
- "10.1.2.0/24",
18
- "10.1.3.0/25",
19
- "10.1.3.64/26",
20
- "10.1.3.128/26",
21
- ]
22
- v4_subnets = [ipaddress.IPv4Network(n) for n in v4_subnet_strings]
23
- v4_subnet_ranges = [(int(n[0]), int(n[-1])) for n in v4_subnets]
24
-
25
- v6_supernet_string = "fc00::/32"
26
- v6_supernet = ipaddress.IPv6Network(v6_supernet_string)
27
-
28
-
29
- v6_subnet_strings = [
30
- "fc00::/48",
31
- "fc00:a::/56",
32
- "fc00:b::/56",
33
- "fc00:b:0:100::/56",
34
- "fc00:b:0:200::/64",
35
- "fc00:b:0:100::/56",
36
- "fc00:c::/44",
37
- ]
38
- v6_subnets = [ipaddress.IPv6Network(n) for n in v6_subnet_strings]
39
- v6_subnet_ranges = [(int(n[0]), int(n[-1])) for n in v6_subnets]
40
-
41
- mergeable_subnets = [
42
- [
43
- (int(ipaddress.IPv4Network(n)[0]), int(ipaddress.IPv4Network(n)[-1]))
44
- for n in ["192.168.0.0/24", "192.168.0.0/25", "192.168.0.128/25", "192.168.0.64/29"]
45
- ],
46
- [
47
- (int(ipaddress.IPv6Network(n)[0]), int(ipaddress.IPv6Network(n)[-1]))
48
- for n in ["fc01::/32", "fc01::/64", "fc01:0:ffff::/48", "fc01:0:cafe::/64"]
49
- ],
50
- ]
51
-
52
-
53
- @pytest.mark.parametrize(
54
- ("supernet", "subnets", "allocation_class", "network_class"),
55
- [
56
- (v4_supernet, v4_subnets, IPv4Allocation, ipaddress.IPv4Network),
57
- (v4_supernet_string, v4_subnet_strings, IPv4Allocation, ipaddress.IPv4Network),
58
- (v6_supernet, v6_subnets, IPv6Allocation, ipaddress.IPv6Network),
59
- (v6_supernet_string, v6_subnet_strings, IPv6Allocation, ipaddress.IPv6Network),
60
- (v4_supernet, v4_subnets[:1], IPv4Allocation, ipaddress.IPv4Network),
61
- (v6_supernet, v6_subnets[:1], IPv6Allocation, ipaddress.IPv6Network),
62
- (v4_supernet, [], IPv4Allocation, ipaddress.IPv4Network),
63
- (v6_supernet, [], IPv6Allocation, ipaddress.IPv6Network),
64
- ],
65
- )
66
- def test_allocation_classes(supernet, subnets, allocation_class, network_class):
67
- a = ip_allocation(supernet, used_networks=subnets)
68
- assert isinstance(a, allocation_class)
69
- assert all(isinstance(n, network_class) for n in a.used_subnets)
70
-
71
-
72
- @pytest.mark.parametrize(
73
- ("subnet", "subnet_range"),
74
- zip(v4_subnets + v6_subnets, v4_subnet_ranges + v6_subnet_ranges, strict=True),
75
- )
76
- def test_netrange(subnet, subnet_range):
77
- assert tools.netrange(subnet) == subnet_range
78
-
79
-
80
- @pytest.mark.parametrize(
81
- ("supernet", "subnets", "subnet_ranges"),
82
- [
83
- (v4_supernet, v4_subnets, v4_subnet_ranges),
84
- (v6_supernet, v6_subnets, v6_subnet_ranges),
85
- ],
86
- )
87
- def test_allocation_ranges(supernet, subnets, subnet_ranges):
88
- a = ip_allocation(supernet, used_networks=subnets)
89
- a._update_used_subnet_ranges(merge=False)
90
- assert sorted(a._used_network_ranges) == sorted(subnet_ranges)
91
-
92
-
93
- @pytest.mark.parametrize(
94
- "subnets",
95
- mergeable_subnets,
96
- )
97
- def test_merge(subnets):
98
- covering_prefix = subnets[0]
99
- assert tools.merge_ranges(subnets) == [covering_prefix]
100
-
101
-
102
- @pytest.mark.parametrize(
103
- "subnets",
104
- mergeable_subnets,
105
- )
106
- def test_merge_one(subnets):
107
- assert subnets[:1] == tools.merge_ranges(subnets[:1])
108
-
109
-
110
- def test_merge_empty():
111
- assert tools.merge_ranges([]) == []
112
-
113
-
114
- @pytest.mark.parametrize(
115
- ("supernet", "used_nets", "cidr", "first_free"),
116
- [
117
- ("2001:db8::/32", ["2001:db8::/48", "2001:db8:1::/120"], 64, "2001:db8:1:1::/64"),
118
- ("10.0.0.0/8", ["10.0.0.0/16", "10.1.0.0/24"], 16, "10.2.0.0/16"),
119
- ],
120
- )
121
- def test_netsize_iterator(supernet, used_nets, cidr, first_free):
122
- supernet = ipaddress.ip_network(supernet)
123
- used_nets = [(int(ipaddress.ip_network(n)[0]), int(ipaddress.ip_network(n)[-1])) for n in used_nets]
124
- i = tools.net_size_iterator(supernet, cidr, used_nets)
125
- first_ip, last_ip = next(i)
126
- net = next(ipaddress.summarize_address_range(ipaddress.ip_address(first_ip), ipaddress.ip_address(last_ip)))
127
- assert first_free == str(net)
128
-
129
-
130
- @pytest.mark.parametrize(
131
- ("supernet", "used_nets", "cidr", "last_free"),
132
- [
133
- ("10.0.0.0/24", ["10.0.0.0/29", "10.0.0.96/30"], 30, "10.0.0.252/30"),
134
- ],
135
- )
136
- def test_netsize_iterator_last(supernet, used_nets, cidr, last_free):
137
- supernet = ipaddress.ip_network(supernet)
138
- used_nets = [(int(ipaddress.ip_network(n)[0]), int(ipaddress.ip_network(n)[-1])) for n in used_nets]
139
- i = tools.net_size_iterator(supernet, cidr, used_nets)
140
- first_ip, last_ip = list(i)[-1]
141
- net = next(ipaddress.summarize_address_range(ipaddress.ip_address(first_ip), ipaddress.ip_address(last_ip)))
142
- assert last_free == str(net)
File without changes
File without changes
File without changes
File without changes