infrahub-server 1.7.0rc0__py3-none-any.whl → 1.7.2__py3-none-any.whl
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.
- infrahub/actions/gather.py +2 -2
- infrahub/api/query.py +3 -2
- infrahub/api/schema.py +5 -0
- infrahub/api/transformation.py +3 -3
- infrahub/cli/db.py +6 -2
- infrahub/computed_attribute/gather.py +2 -0
- infrahub/config.py +2 -2
- infrahub/core/attribute.py +21 -2
- infrahub/core/branch/models.py +11 -117
- infrahub/core/branch/tasks.py +7 -3
- infrahub/core/diff/merger/merger.py +5 -1
- infrahub/core/diff/model/path.py +43 -0
- infrahub/core/graph/__init__.py +1 -1
- infrahub/core/graph/index.py +2 -0
- infrahub/core/initialization.py +2 -1
- infrahub/core/ipam/resource_allocator.py +229 -0
- infrahub/core/migrations/graph/__init__.py +10 -0
- infrahub/core/migrations/graph/m014_remove_index_attr_value.py +3 -2
- infrahub/core/migrations/graph/m015_diff_format_update.py +3 -2
- infrahub/core/migrations/graph/m016_diff_delete_bug_fix.py +3 -2
- infrahub/core/migrations/graph/m017_add_core_profile.py +6 -4
- infrahub/core/migrations/graph/m018_uniqueness_nulls.py +3 -4
- infrahub/core/migrations/graph/m020_duplicate_edges.py +3 -3
- infrahub/core/migrations/graph/m025_uniqueness_nulls.py +3 -4
- infrahub/core/migrations/graph/m026_0000_prefix_fix.py +4 -5
- infrahub/core/migrations/graph/m028_delete_diffs.py +3 -2
- infrahub/core/migrations/graph/m029_duplicates_cleanup.py +3 -2
- infrahub/core/migrations/graph/m031_check_number_attributes.py +4 -3
- infrahub/core/migrations/graph/m032_cleanup_orphaned_branch_relationships.py +3 -2
- infrahub/core/migrations/graph/m034_find_orphaned_schema_fields.py +3 -2
- infrahub/core/migrations/graph/m035_orphan_relationships.py +3 -3
- infrahub/core/migrations/graph/m036_drop_attr_value_index.py +3 -2
- infrahub/core/migrations/graph/m037_index_attr_vals.py +3 -2
- infrahub/core/migrations/graph/m038_redo_0000_prefix_fix.py +4 -5
- infrahub/core/migrations/graph/m039_ipam_reconcile.py +3 -2
- infrahub/core/migrations/graph/m041_deleted_dup_edges.py +3 -2
- infrahub/core/migrations/graph/m042_profile_attrs_in_db.py +5 -4
- infrahub/core/migrations/graph/m043_create_hfid_display_label_in_db.py +12 -5
- infrahub/core/migrations/graph/m044_backfill_hfid_display_label_in_db.py +15 -4
- infrahub/core/migrations/graph/m045_backfill_hfid_display_label_in_db_profile_template.py +10 -4
- infrahub/core/migrations/graph/m046_fill_agnostic_hfid_display_labels.py +6 -5
- infrahub/core/migrations/graph/m047_backfill_or_null_display_label.py +19 -5
- infrahub/core/migrations/graph/m048_undelete_rel_props.py +6 -4
- infrahub/core/migrations/graph/m049_remove_is_visible_relationship.py +3 -3
- infrahub/core/migrations/graph/m050_backfill_vertex_metadata.py +3 -3
- infrahub/core/migrations/graph/m051_subtract_branched_from_microsecond.py +39 -0
- infrahub/core/migrations/graph/m052_fix_global_branch_level.py +51 -0
- infrahub/core/migrations/graph/m053_fix_branch_level_zero.py +61 -0
- infrahub/core/migrations/graph/m054_cleanup_orphaned_nodes.py +87 -0
- infrahub/core/migrations/graph/m055_remove_webhook_validate_certificates_default.py +86 -0
- infrahub/core/migrations/runner.py +6 -3
- infrahub/core/migrations/schema/attribute_kind_update.py +8 -11
- infrahub/core/migrations/schema/attribute_supports_profile.py +3 -8
- infrahub/core/migrations/schema/models.py +8 -0
- infrahub/core/migrations/schema/node_attribute_add.py +24 -29
- infrahub/core/migrations/schema/tasks.py +7 -1
- infrahub/core/migrations/shared.py +37 -30
- infrahub/core/node/__init__.py +2 -1
- infrahub/core/node/lock_utils.py +23 -2
- infrahub/core/node/resource_manager/ip_address_pool.py +5 -11
- infrahub/core/node/resource_manager/ip_prefix_pool.py +5 -21
- infrahub/core/node/resource_manager/number_pool.py +109 -39
- infrahub/core/query/__init__.py +7 -1
- infrahub/core/query/branch.py +18 -2
- infrahub/core/query/ipam.py +629 -40
- infrahub/core/query/node.py +128 -0
- infrahub/core/query/resource_manager.py +114 -1
- infrahub/core/relationship/model.py +9 -3
- infrahub/core/schema/attribute_parameters.py +28 -1
- infrahub/core/schema/attribute_schema.py +9 -2
- infrahub/core/schema/definitions/core/webhook.py +0 -1
- infrahub/core/schema/definitions/internal.py +7 -4
- infrahub/core/schema/manager.py +50 -38
- infrahub/core/validators/attribute/kind.py +5 -2
- infrahub/core/validators/determiner.py +4 -0
- infrahub/graphql/analyzer.py +3 -1
- infrahub/graphql/app.py +7 -10
- infrahub/graphql/execution.py +95 -0
- infrahub/graphql/manager.py +8 -2
- infrahub/graphql/mutations/proposed_change.py +15 -0
- infrahub/graphql/parser.py +10 -7
- infrahub/graphql/queries/ipam.py +20 -25
- infrahub/graphql/queries/search.py +29 -9
- infrahub/lock.py +7 -0
- infrahub/proposed_change/tasks.py +2 -0
- infrahub/services/adapters/cache/redis.py +7 -0
- infrahub/services/adapters/http/httpx.py +27 -0
- infrahub/trigger/catalogue.py +2 -0
- infrahub/trigger/models.py +73 -4
- infrahub/trigger/setup.py +1 -1
- infrahub/trigger/system.py +36 -0
- infrahub/webhook/models.py +4 -2
- infrahub/webhook/tasks.py +2 -2
- infrahub/workflows/initialization.py +2 -2
- infrahub_sdk/analyzer.py +2 -2
- infrahub_sdk/branch.py +12 -39
- infrahub_sdk/checks.py +4 -4
- infrahub_sdk/client.py +36 -0
- infrahub_sdk/ctl/cli_commands.py +2 -1
- infrahub_sdk/ctl/graphql.py +15 -4
- infrahub_sdk/ctl/utils.py +2 -2
- infrahub_sdk/enums.py +6 -0
- infrahub_sdk/graphql/renderers.py +21 -0
- infrahub_sdk/graphql/utils.py +85 -0
- infrahub_sdk/node/attribute.py +12 -2
- infrahub_sdk/node/constants.py +11 -0
- infrahub_sdk/node/metadata.py +69 -0
- infrahub_sdk/node/node.py +65 -14
- infrahub_sdk/node/property.py +3 -0
- infrahub_sdk/node/related_node.py +24 -1
- infrahub_sdk/node/relationship.py +10 -1
- infrahub_sdk/operation.py +2 -2
- infrahub_sdk/schema/repository.py +1 -2
- infrahub_sdk/transforms.py +2 -2
- infrahub_sdk/types.py +18 -2
- {infrahub_server-1.7.0rc0.dist-info → infrahub_server-1.7.2.dist-info}/METADATA +8 -8
- {infrahub_server-1.7.0rc0.dist-info → infrahub_server-1.7.2.dist-info}/RECORD +123 -114
- {infrahub_server-1.7.0rc0.dist-info → infrahub_server-1.7.2.dist-info}/entry_points.txt +0 -1
- infrahub_testcontainers/docker-compose-cluster.test.yml +16 -10
- infrahub_testcontainers/docker-compose.test.yml +11 -10
- infrahub_testcontainers/performance_test.py +1 -1
- infrahub/pools/address.py +0 -16
- {infrahub_server-1.7.0rc0.dist-info → infrahub_server-1.7.2.dist-info}/WHEEL +0 -0
- {infrahub_server-1.7.0rc0.dist-info → infrahub_server-1.7.2.dist-info}/licenses/LICENSE.txt +0 -0
infrahub/core/query/ipam.py
CHANGED
|
@@ -2,23 +2,19 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import ipaddress
|
|
4
4
|
from dataclasses import dataclass
|
|
5
|
-
from typing import TYPE_CHECKING
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
6
|
|
|
7
7
|
from infrahub.core.constants import InfrahubKind
|
|
8
8
|
from infrahub.core.graph.schema import GraphAttributeIPHostNode, GraphAttributeIPNetworkNode
|
|
9
9
|
from infrahub.core.ipam.constants import AllIPTypes, IPAddressType, IPNetworkType
|
|
10
|
-
from infrahub.core.query import QueryResult, QueryType
|
|
10
|
+
from infrahub.core.query import Query, QueryResult, QueryType
|
|
11
11
|
from infrahub.core.registry import registry
|
|
12
12
|
from infrahub.core.utils import convert_ip_to_binary_str
|
|
13
13
|
|
|
14
|
-
from . import Query
|
|
15
|
-
|
|
16
14
|
if TYPE_CHECKING:
|
|
17
15
|
from uuid import UUID
|
|
18
16
|
|
|
19
|
-
from infrahub.core.branch import Branch
|
|
20
17
|
from infrahub.core.node import Node
|
|
21
|
-
from infrahub.core.timestamp import Timestamp
|
|
22
18
|
from infrahub.database import InfrahubDatabase
|
|
23
19
|
|
|
24
20
|
|
|
@@ -38,6 +34,58 @@ class IPAddressData:
|
|
|
38
34
|
address: IPAddressType
|
|
39
35
|
|
|
40
36
|
|
|
37
|
+
@dataclass(frozen=True)
|
|
38
|
+
class IPPrefixFreeData:
|
|
39
|
+
free_start: int
|
|
40
|
+
|
|
41
|
+
@classmethod
|
|
42
|
+
def from_db(cls, result: QueryResult) -> IPPrefixFreeData:
|
|
43
|
+
return cls(
|
|
44
|
+
free_start=result.get_as_type("free_start", return_type=int),
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass(frozen=True)
|
|
49
|
+
class IPv6PrefixFreeData:
|
|
50
|
+
free_start_bin: str
|
|
51
|
+
|
|
52
|
+
@classmethod
|
|
53
|
+
def from_db(cls, result: QueryResult) -> IPv6PrefixFreeData:
|
|
54
|
+
return cls(
|
|
55
|
+
free_start_bin=result.get_as_type("free_start_bin", return_type=str),
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass(frozen=True)
|
|
60
|
+
class IPAddressFreeData:
|
|
61
|
+
free_addr: int
|
|
62
|
+
is_free: bool
|
|
63
|
+
is_last: bool
|
|
64
|
+
|
|
65
|
+
@classmethod
|
|
66
|
+
def from_db(cls, result: QueryResult) -> IPAddressFreeData:
|
|
67
|
+
return cls(
|
|
68
|
+
free_addr=result.get_as_type("free_addr", return_type=int),
|
|
69
|
+
is_free=result.get_as_type("is_free", return_type=bool),
|
|
70
|
+
is_last=result.get_as_type("is_last", return_type=bool),
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass(frozen=True)
|
|
75
|
+
class IPv6AddressFreeData:
|
|
76
|
+
free_addr_bin: str
|
|
77
|
+
is_free: bool
|
|
78
|
+
is_last: bool
|
|
79
|
+
|
|
80
|
+
@classmethod
|
|
81
|
+
def from_db(cls, result: QueryResult) -> IPv6AddressFreeData:
|
|
82
|
+
return cls(
|
|
83
|
+
free_addr_bin=result.get_as_type("free_addr_bin", return_type=str),
|
|
84
|
+
is_free=result.get_as_type("is_free", return_type=bool),
|
|
85
|
+
is_last=result.get_as_type("is_last", return_type=bool),
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
41
89
|
def _get_namespace_id(
|
|
42
90
|
namespace: Node | str | None = None,
|
|
43
91
|
) -> str:
|
|
@@ -84,11 +132,11 @@ class IPPrefixSubnetFetch(Query):
|
|
|
84
132
|
CALL (ns) {
|
|
85
133
|
MATCH (ns)-[r:IS_PART_OF]-(root:Root)
|
|
86
134
|
WHERE %(branch_filter)s
|
|
87
|
-
RETURN
|
|
135
|
+
RETURN r
|
|
88
136
|
ORDER BY r.branch_level DESC, r.from DESC
|
|
89
137
|
LIMIT 1
|
|
90
138
|
}
|
|
91
|
-
WITH ns,
|
|
139
|
+
WITH ns, r
|
|
92
140
|
WHERE r.status = "active"
|
|
93
141
|
WITH ns
|
|
94
142
|
// MATCH all prefixes that are IN SCOPE
|
|
@@ -137,6 +185,301 @@ class IPPrefixSubnetFetch(Query):
|
|
|
137
185
|
return subnets
|
|
138
186
|
|
|
139
187
|
|
|
188
|
+
class IPPrefixSubnetFetchFree(Query):
|
|
189
|
+
name = "ipprefix_subnet_fetch_free"
|
|
190
|
+
type = QueryType.READ
|
|
191
|
+
raise_error_if_empty = False
|
|
192
|
+
|
|
193
|
+
def __init__(
|
|
194
|
+
self,
|
|
195
|
+
obj: IPNetworkType,
|
|
196
|
+
target_prefixlen: int,
|
|
197
|
+
namespace: Node | str | None = None,
|
|
198
|
+
**kwargs,
|
|
199
|
+
) -> None:
|
|
200
|
+
self.obj = obj
|
|
201
|
+
self.target_prefixlen = target_prefixlen
|
|
202
|
+
self.namespace_id = _get_namespace_id(namespace)
|
|
203
|
+
|
|
204
|
+
super().__init__(**kwargs)
|
|
205
|
+
|
|
206
|
+
async def query_init(self, db: InfrahubDatabase, **kwargs) -> None: # noqa: ARG002
|
|
207
|
+
self.params["ns_id"] = self.namespace_id
|
|
208
|
+
|
|
209
|
+
prefix_bin = convert_ip_to_binary_str(self.obj)[: self.obj.prefixlen]
|
|
210
|
+
self.params["prefix_binary"] = prefix_bin
|
|
211
|
+
self.params["maxprefixlen"] = self.obj.prefixlen
|
|
212
|
+
self.params["ip_version"] = self.obj.version
|
|
213
|
+
self.params["parent_start"] = int(self.obj.network_address)
|
|
214
|
+
self.params["parent_end"] = int(self.obj.broadcast_address)
|
|
215
|
+
self.params["block_size"] = 1 << (32 - self.target_prefixlen)
|
|
216
|
+
|
|
217
|
+
branch_filter, branch_params = self.branch.get_query_filter_path(
|
|
218
|
+
at=self.at.to_string(), branch_agnostic=self.branch_agnostic
|
|
219
|
+
)
|
|
220
|
+
self.params.update(branch_params)
|
|
221
|
+
|
|
222
|
+
# ruff: noqa: E501
|
|
223
|
+
query = """
|
|
224
|
+
// First match on IPNAMESPACE
|
|
225
|
+
MATCH (ns:%(ns_label)s)
|
|
226
|
+
WHERE ns.uuid = $ns_id
|
|
227
|
+
CALL (ns) {
|
|
228
|
+
MATCH (ns)-[r:IS_PART_OF]-(root:Root)
|
|
229
|
+
WHERE %(branch_filter)s
|
|
230
|
+
RETURN r
|
|
231
|
+
ORDER BY r.branch_level DESC, r.from DESC
|
|
232
|
+
LIMIT 1
|
|
233
|
+
}
|
|
234
|
+
WITH ns, r
|
|
235
|
+
WHERE r.status = "active"
|
|
236
|
+
WITH ns
|
|
237
|
+
OPTIONAL MATCH path2 = (ns)-[:IS_RELATED]-(ns_rel:Relationship)-[:IS_RELATED]-(pfx:%(node_label)s)-[:HAS_ATTRIBUTE]-(an:Attribute {name: "prefix"})-[:HAS_VALUE]-(av:AttributeIPNetwork)
|
|
238
|
+
WHERE ns_rel.name = "ip_namespace__ip_prefix"
|
|
239
|
+
AND av.binary_address STARTS WITH $prefix_binary
|
|
240
|
+
AND av.prefixlen > $maxprefixlen
|
|
241
|
+
AND av.version = $ip_version
|
|
242
|
+
AND all(r IN relationships(path2) WHERE (%(branch_filter)s) AND r.status = "active")
|
|
243
|
+
WITH collect({binary: av.binary_address, prefixlen: av.prefixlen}) AS ranges_raw
|
|
244
|
+
// Convert binary strings to integer ranges for gap detection
|
|
245
|
+
// - start: Convert binary string to decimal by iterating through each bit
|
|
246
|
+
// and accumulating (dec * 2 + bit_value), which is binary-to-decimal conversion
|
|
247
|
+
// - end: start + (2^host_bits - 1), where host_bits = 32 - prefixlen
|
|
248
|
+
// This gives the broadcast address (last IP) of the prefix
|
|
249
|
+
WITH [r IN ranges_raw |
|
|
250
|
+
{
|
|
251
|
+
start: reduce(dec = 0, b IN split(r.binary, "") | dec * 2 + toInteger(b)),
|
|
252
|
+
end: reduce(dec = 0, b IN split(r.binary, "") | dec * 2 + toInteger(b)) + toInteger(2 ^ (32 - r.prefixlen)) - 1
|
|
253
|
+
}
|
|
254
|
+
] AS ranges
|
|
255
|
+
UNWIND CASE WHEN size(ranges) = 0 THEN [{start: null, end: null}] ELSE ranges END AS r
|
|
256
|
+
WITH r
|
|
257
|
+
ORDER BY r.start ASC
|
|
258
|
+
WITH collect(r) AS ranges_sorted_raw
|
|
259
|
+
WITH [r IN ranges_sorted_raw WHERE r.start IS NOT NULL] AS ranges_sorted
|
|
260
|
+
// Gap detection algorithm using reduce to scan through sorted ranges
|
|
261
|
+
// acc.cursor: current candidate position for a free block (always block-aligned)
|
|
262
|
+
// acc.found: set when a gap is found, stops further iteration
|
|
263
|
+
WITH reduce(acc = {cursor: $parent_start, found: null}, r IN ranges_sorted |
|
|
264
|
+
CASE
|
|
265
|
+
// Already found a gap, preserve result
|
|
266
|
+
WHEN acc.found IS NOT NULL THEN acc
|
|
267
|
+
// Range ends before cursor, skip (range already passed)
|
|
268
|
+
WHEN r.end < acc.cursor THEN acc
|
|
269
|
+
// Gap found: cursor + block_size - 1 < range start means there's room before this range
|
|
270
|
+
WHEN acc.cursor + $block_size - 1 < r.start THEN {cursor: acc.cursor, found: acc.cursor}
|
|
271
|
+
// No gap: advance cursor past this range using ceiling division
|
|
272
|
+
// Formula: ceil((r.end + 1) / block_size) * block_size
|
|
273
|
+
// This aligns the cursor to the next block boundary after the range
|
|
274
|
+
ELSE
|
|
275
|
+
{
|
|
276
|
+
cursor: CASE
|
|
277
|
+
WHEN toInteger((r.end + 1 + $block_size - 1) / $block_size) * $block_size > $parent_end + 1
|
|
278
|
+
THEN $parent_end + 1
|
|
279
|
+
ELSE toInteger((r.end + 1 + $block_size - 1) / $block_size) * $block_size
|
|
280
|
+
END,
|
|
281
|
+
found: null
|
|
282
|
+
}
|
|
283
|
+
END
|
|
284
|
+
) AS res
|
|
285
|
+
WITH CASE
|
|
286
|
+
WHEN res.found IS NOT NULL THEN res.found
|
|
287
|
+
WHEN res.cursor + $block_size - 1 <= $parent_end THEN res.cursor
|
|
288
|
+
ELSE NULL
|
|
289
|
+
END AS free_start
|
|
290
|
+
WHERE free_start IS NOT NULL
|
|
291
|
+
""" % {
|
|
292
|
+
"ns_label": InfrahubKind.IPNAMESPACE,
|
|
293
|
+
"node_label": InfrahubKind.IPPREFIX,
|
|
294
|
+
"branch_filter": branch_filter,
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
self.add_to_query(query)
|
|
298
|
+
self.return_labels = ["free_start"]
|
|
299
|
+
self.limit = 1
|
|
300
|
+
|
|
301
|
+
def get_prefix_data(self) -> IPPrefixFreeData | None:
|
|
302
|
+
result = self.get_result()
|
|
303
|
+
if not result:
|
|
304
|
+
return None
|
|
305
|
+
|
|
306
|
+
return IPPrefixFreeData.from_db(result=result)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
class IPv6PrefixSubnetFetchFree(Query):
|
|
310
|
+
"""Query to find the next free IPv6 prefix within a parent prefix.
|
|
311
|
+
|
|
312
|
+
This query uses binary string operations to handle IPv6's 128-bit address space,
|
|
313
|
+
as the integer values would overflow Neo4j's 64-bit integer type.
|
|
314
|
+
"""
|
|
315
|
+
|
|
316
|
+
name = "ipv6prefix_subnet_fetch_free"
|
|
317
|
+
type = QueryType.READ
|
|
318
|
+
raise_error_if_empty = False
|
|
319
|
+
|
|
320
|
+
def __init__(
|
|
321
|
+
self,
|
|
322
|
+
obj: IPNetworkType,
|
|
323
|
+
target_prefixlen: int,
|
|
324
|
+
namespace: Node | str | None = None,
|
|
325
|
+
**kwargs,
|
|
326
|
+
) -> None:
|
|
327
|
+
self.obj = obj
|
|
328
|
+
self.target_prefixlen = target_prefixlen
|
|
329
|
+
self.namespace_id = _get_namespace_id(namespace)
|
|
330
|
+
|
|
331
|
+
super().__init__(**kwargs)
|
|
332
|
+
|
|
333
|
+
async def query_init(self, db: InfrahubDatabase, **kwargs) -> None: # noqa: ARG002
|
|
334
|
+
self.params["ns_id"] = self.namespace_id
|
|
335
|
+
|
|
336
|
+
prefix_bin = convert_ip_to_binary_str(self.obj)[: self.obj.prefixlen]
|
|
337
|
+
self.params["prefix_binary"] = prefix_bin
|
|
338
|
+
self.params["maxprefixlen"] = self.obj.prefixlen
|
|
339
|
+
self.params["ip_version"] = self.obj.version
|
|
340
|
+
self.params["target_prefixlen"] = self.target_prefixlen
|
|
341
|
+
# Binary representation of parent network and broadcast addresses
|
|
342
|
+
self.params["parent_start_bin"] = convert_ip_to_binary_str(self.obj)
|
|
343
|
+
self.params["parent_end_bin"] = format(int(self.obj.broadcast_address), "0128b")
|
|
344
|
+
|
|
345
|
+
branch_filter, branch_params = self.branch.get_query_filter_path(
|
|
346
|
+
at=self.at.to_string(), branch_agnostic=self.branch_agnostic
|
|
347
|
+
)
|
|
348
|
+
self.params.update(branch_params)
|
|
349
|
+
|
|
350
|
+
# ruff: noqa: E501
|
|
351
|
+
query = """
|
|
352
|
+
// First match on IPNAMESPACE
|
|
353
|
+
MATCH (ns:%(ns_label)s)
|
|
354
|
+
WHERE ns.uuid = $ns_id
|
|
355
|
+
CALL (ns) {
|
|
356
|
+
MATCH (ns)-[r:IS_PART_OF]-(root:Root)
|
|
357
|
+
WHERE %(branch_filter)s
|
|
358
|
+
RETURN r
|
|
359
|
+
ORDER BY r.branch_level DESC, r.from DESC
|
|
360
|
+
LIMIT 1
|
|
361
|
+
}
|
|
362
|
+
WITH ns, r
|
|
363
|
+
WHERE r.status = "active"
|
|
364
|
+
WITH ns
|
|
365
|
+
OPTIONAL MATCH path2 = (ns)-[:IS_RELATED]-(ns_rel:Relationship)-[:IS_RELATED]-(pfx:%(node_label)s)-[:HAS_ATTRIBUTE]-(an:Attribute {name: "prefix"})-[:HAS_VALUE]-(av:AttributeIPNetwork)
|
|
366
|
+
WHERE ns_rel.name = "ip_namespace__ip_prefix"
|
|
367
|
+
AND av.binary_address STARTS WITH $prefix_binary
|
|
368
|
+
AND av.prefixlen > $maxprefixlen
|
|
369
|
+
AND av.version = $ip_version
|
|
370
|
+
AND all(r IN relationships(path2) WHERE (%(branch_filter)s) AND r.status = "active")
|
|
371
|
+
WITH collect({binary: av.binary_address, prefixlen: av.prefixlen}) AS ranges_raw
|
|
372
|
+
// Create ranges with start (block-aligned) and end_block (the block containing the end address)
|
|
373
|
+
WITH [r IN ranges_raw WHERE r.binary IS NOT NULL |
|
|
374
|
+
{
|
|
375
|
+
// Start block: first target_prefixlen bits of network address
|
|
376
|
+
start_block: left(r.binary, $target_prefixlen),
|
|
377
|
+
// End block: first target_prefixlen bits of broadcast address
|
|
378
|
+
// For a prefix, broadcast = network with all host bits set to 1
|
|
379
|
+
end_block: left(left(r.binary, r.prefixlen) + reduce(s = "", i IN range(1, 128 - r.prefixlen) | s + "1"), $target_prefixlen)
|
|
380
|
+
}
|
|
381
|
+
] AS ranges
|
|
382
|
+
UNWIND CASE WHEN size(ranges) = 0 THEN [{start_block: null, end_block: null}] ELSE ranges END AS r
|
|
383
|
+
WITH r
|
|
384
|
+
ORDER BY r.start_block ASC
|
|
385
|
+
WITH collect(r) AS ranges_sorted_raw
|
|
386
|
+
WITH [r IN ranges_sorted_raw WHERE r.start_block IS NOT NULL] AS ranges_sorted
|
|
387
|
+
// Find first available slot using binary string comparison
|
|
388
|
+
// We track cursor as the current candidate block (target_prefixlen bits)
|
|
389
|
+
WITH reduce(acc = {cursor: left($parent_start_bin, $target_prefixlen), found: null}, r IN ranges_sorted |
|
|
390
|
+
CASE
|
|
391
|
+
WHEN acc.found IS NOT NULL THEN acc
|
|
392
|
+
// Range ends before cursor, skip it
|
|
393
|
+
WHEN r.end_block < acc.cursor THEN acc
|
|
394
|
+
// Gap found: cursor is before this range starts
|
|
395
|
+
WHEN acc.cursor < r.start_block THEN {cursor: acc.cursor, found: acc.cursor}
|
|
396
|
+
// Cursor overlaps with range: advance cursor past this range
|
|
397
|
+
// Binary string increment: add 1 to end_block to get the next block
|
|
398
|
+
// Algorithm: process bits right-to-left with carry propagation
|
|
399
|
+
// - Start with carry=1 (we're adding 1)
|
|
400
|
+
// - For each bit: if carry=0, keep bit unchanged
|
|
401
|
+
// if carry=1 and bit="1", output "0" and carry remains 1
|
|
402
|
+
// if carry=1 and bit="0", output "1" and carry becomes 0
|
|
403
|
+
// Note: if all bits are "1", result will be all "0"s with carry=1 (overflow)
|
|
404
|
+
// We track the final carry and set cursor to null if overflow occurred
|
|
405
|
+
ELSE
|
|
406
|
+
{
|
|
407
|
+
cursor: CASE
|
|
408
|
+
// Check for overflow: if final carry is 1, return null
|
|
409
|
+
WHEN reduce(
|
|
410
|
+
inc = {bits: split(r.end_block, ""), carry: 1, result: []},
|
|
411
|
+
idx IN reverse(range(0, $target_prefixlen - 1)) |
|
|
412
|
+
{
|
|
413
|
+
bits: inc.bits,
|
|
414
|
+
carry: CASE
|
|
415
|
+
WHEN inc.carry = 0 THEN 0
|
|
416
|
+
WHEN inc.bits[idx] = "1" THEN 1
|
|
417
|
+
ELSE 0
|
|
418
|
+
END,
|
|
419
|
+
result: CASE
|
|
420
|
+
WHEN inc.carry = 0 THEN [inc.bits[idx]] + inc.result
|
|
421
|
+
WHEN inc.bits[idx] = "1" THEN ["0"] + inc.result
|
|
422
|
+
ELSE ["1"] + inc.result
|
|
423
|
+
END
|
|
424
|
+
}
|
|
425
|
+
).carry = 1 THEN null
|
|
426
|
+
ELSE reduce(s = "", c IN reduce(
|
|
427
|
+
inc = {bits: split(r.end_block, ""), carry: 1, result: []},
|
|
428
|
+
idx IN reverse(range(0, $target_prefixlen - 1)) |
|
|
429
|
+
{
|
|
430
|
+
bits: inc.bits,
|
|
431
|
+
carry: CASE
|
|
432
|
+
WHEN inc.carry = 0 THEN 0
|
|
433
|
+
WHEN inc.bits[idx] = "1" THEN 1
|
|
434
|
+
ELSE 0
|
|
435
|
+
END,
|
|
436
|
+
result: CASE
|
|
437
|
+
WHEN inc.carry = 0 THEN [inc.bits[idx]] + inc.result
|
|
438
|
+
WHEN inc.bits[idx] = "1" THEN ["0"] + inc.result
|
|
439
|
+
ELSE ["1"] + inc.result
|
|
440
|
+
END
|
|
441
|
+
}
|
|
442
|
+
).result | s + c)
|
|
443
|
+
END,
|
|
444
|
+
found: null
|
|
445
|
+
}
|
|
446
|
+
END
|
|
447
|
+
) AS res
|
|
448
|
+
// Handle overflow case
|
|
449
|
+
WITH
|
|
450
|
+
CASE
|
|
451
|
+
WHEN res.found IS NOT NULL THEN res.found
|
|
452
|
+
ELSE res.cursor
|
|
453
|
+
END AS cursor_str,
|
|
454
|
+
res.found AS found
|
|
455
|
+
// Check if we found a slot or if there's space after all ranges
|
|
456
|
+
WITH CASE
|
|
457
|
+
WHEN found IS NOT NULL THEN found
|
|
458
|
+
// Check cursor is valid (not overflowed) and within parent range
|
|
459
|
+
WHEN size(cursor_str) = $target_prefixlen AND cursor_str <= left($parent_end_bin, $target_prefixlen) THEN cursor_str
|
|
460
|
+
ELSE NULL
|
|
461
|
+
END AS free_start_partial
|
|
462
|
+
WHERE free_start_partial IS NOT NULL
|
|
463
|
+
// Pad the partial binary to full 128 bits
|
|
464
|
+
WITH free_start_partial + reduce(s = "", i IN range(1, 128 - $target_prefixlen) | s + "0") AS free_start_bin
|
|
465
|
+
""" % {
|
|
466
|
+
"ns_label": InfrahubKind.IPNAMESPACE,
|
|
467
|
+
"node_label": InfrahubKind.IPPREFIX,
|
|
468
|
+
"branch_filter": branch_filter,
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
self.add_to_query(query)
|
|
472
|
+
self.return_labels = ["free_start_bin"]
|
|
473
|
+
self.limit = 1
|
|
474
|
+
|
|
475
|
+
def get_prefix_data(self) -> IPv6PrefixFreeData | None:
|
|
476
|
+
result = self.get_result()
|
|
477
|
+
if not result:
|
|
478
|
+
return None
|
|
479
|
+
|
|
480
|
+
return IPv6PrefixFreeData.from_db(result=result)
|
|
481
|
+
|
|
482
|
+
|
|
140
483
|
class IPPrefixIPAddressFetch(Query):
|
|
141
484
|
name = "ipprefix_ipaddress_fetch"
|
|
142
485
|
type = QueryType.READ
|
|
@@ -173,11 +516,11 @@ class IPPrefixIPAddressFetch(Query):
|
|
|
173
516
|
CALL (ns) {
|
|
174
517
|
MATCH (ns)-[r:IS_PART_OF]-(root:Root)
|
|
175
518
|
WHERE %(branch_filter)s
|
|
176
|
-
RETURN
|
|
519
|
+
RETURN r
|
|
177
520
|
ORDER BY r.branch_level DESC, r.from DESC
|
|
178
521
|
LIMIT 1
|
|
179
522
|
}
|
|
180
|
-
WITH ns,
|
|
523
|
+
WITH ns, r
|
|
181
524
|
WHERE r.status = "active"
|
|
182
525
|
WITH ns
|
|
183
526
|
// MATCH all IPAddress that are IN SCOPE
|
|
@@ -210,36 +553,282 @@ class IPPrefixIPAddressFetch(Query):
|
|
|
210
553
|
return addresses
|
|
211
554
|
|
|
212
555
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
db: InfrahubDatabase,
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
556
|
+
class IPPrefixIPAddressFetchFree(Query):
|
|
557
|
+
name = "ipprefix_ipaddress_fetch_free"
|
|
558
|
+
type = QueryType.READ
|
|
559
|
+
|
|
560
|
+
def __init__(
|
|
561
|
+
self,
|
|
562
|
+
obj: IPNetworkType,
|
|
563
|
+
is_pool: bool,
|
|
564
|
+
namespace: Node | str | None = None,
|
|
565
|
+
**kwargs,
|
|
566
|
+
) -> None:
|
|
567
|
+
self.obj = obj
|
|
568
|
+
self.namespace_id = _get_namespace_id(namespace)
|
|
569
|
+
self.is_pool = is_pool
|
|
570
|
+
|
|
571
|
+
super().__init__(**kwargs)
|
|
572
|
+
|
|
573
|
+
async def query_init(self, db: InfrahubDatabase, **kwargs) -> None: # noqa: ARG002
|
|
574
|
+
self.params["ns_id"] = self.namespace_id
|
|
575
|
+
|
|
576
|
+
prefix_bin = convert_ip_to_binary_str(self.obj)[: self.obj.prefixlen]
|
|
577
|
+
self.params["prefix_binary"] = prefix_bin
|
|
578
|
+
self.params["start_range"] = int(self.obj.network_address)
|
|
579
|
+
self.params["end_range"] = int(self.obj.broadcast_address)
|
|
580
|
+
if not self.is_pool:
|
|
581
|
+
self.params["start_range"] += 1
|
|
582
|
+
self.params["end_range"] -= 1
|
|
583
|
+
|
|
584
|
+
self.params["maxprefixlen"] = self.obj.prefixlen
|
|
585
|
+
self.params["ip_version"] = self.obj.version
|
|
586
|
+
self.limit = 1 # Query only works at returning a single, free entry
|
|
587
|
+
|
|
588
|
+
branch_filter, branch_params = self.branch.get_query_filter_path(
|
|
589
|
+
at=self.at.to_string(), branch_agnostic=self.branch_agnostic
|
|
590
|
+
)
|
|
591
|
+
self.params.update(branch_params)
|
|
592
|
+
|
|
593
|
+
# ruff: noqa: E501
|
|
594
|
+
query = """
|
|
595
|
+
// First match on IPNAMESPACE
|
|
596
|
+
MATCH (ns:%(ns_label)s)
|
|
597
|
+
WHERE ns.uuid = $ns_id
|
|
598
|
+
CALL (ns) {
|
|
599
|
+
MATCH (ns)-[r:IS_PART_OF]-(root:Root)
|
|
600
|
+
WHERE %(branch_filter)s
|
|
601
|
+
RETURN r
|
|
602
|
+
ORDER BY r.branch_level DESC, r.from DESC
|
|
603
|
+
LIMIT 1
|
|
604
|
+
}
|
|
605
|
+
WITH ns, r
|
|
606
|
+
WHERE r.status = "active"
|
|
607
|
+
WITH ns
|
|
608
|
+
// MATCH all IPAddress that are IN SCOPE
|
|
609
|
+
MATCH path2 = (ns)-[:IS_RELATED]-(ns_rel:Relationship)-[:IS_RELATED]-(addr:%(node_label)s)-[:HAS_ATTRIBUTE]-(an:Attribute {name: "address"})-[:HAS_VALUE]-(av:AttributeIPHost)
|
|
610
|
+
WHERE ns_rel.name = "ip_namespace__ip_address"
|
|
611
|
+
AND av.binary_address STARTS WITH $prefix_binary
|
|
612
|
+
AND av.prefixlen >= $maxprefixlen
|
|
613
|
+
AND av.version = $ip_version
|
|
614
|
+
AND all(r IN relationships(path2) WHERE (%(branch_filter)s) and r.status = "active")
|
|
615
|
+
ORDER BY av.binary_address
|
|
616
|
+
// Gap detection algorithm: collect used addresses and find first available slot
|
|
617
|
+
// Each used_address is a single binary string representing an allocated IP
|
|
618
|
+
WITH DISTINCT av.binary_address AS used_address
|
|
619
|
+
// Convert binary string to integer for comparison
|
|
620
|
+
WITH [x IN split(used_address, "") | toInteger(x)] AS bits
|
|
621
|
+
// Build array: [start_range - 1] prepended to all used addresses as integers
|
|
622
|
+
// This creates a baseline for gap detection starting from the range beginning
|
|
623
|
+
WITH [$start_range - 1] + collect(reduce(dec = 0, b IN bits | dec * 2 + b)) AS nums
|
|
624
|
+
UNWIND range(0, size(nums) - 1) AS idx
|
|
625
|
+
CALL (nums, idx) {
|
|
626
|
+
// Compare expected sequential address with actual address at this position
|
|
627
|
+
// If they differ, we found a gap (is_free = true)
|
|
628
|
+
WITH nums[idx] AS curr, idx - 1 + $start_range AS expected
|
|
629
|
+
RETURN expected AS addr, expected <> curr AS is_free, idx = size(nums) - 1 AS is_last
|
|
630
|
+
}
|
|
631
|
+
WITH addr, is_free, is_last
|
|
632
|
+
WHERE is_free = true OR is_last = true
|
|
633
|
+
WITH addr AS free_addr, is_free, is_last
|
|
634
|
+
""" % {
|
|
635
|
+
"ns_label": InfrahubKind.IPNAMESPACE,
|
|
636
|
+
"node_label": InfrahubKind.IPADDRESS,
|
|
637
|
+
"branch_filter": branch_filter,
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
self.add_to_query(query)
|
|
641
|
+
self.return_labels = ["free_addr", "is_free", "is_last"]
|
|
642
|
+
self.order_by = ["free_addr"]
|
|
643
|
+
|
|
644
|
+
def get_address_data(self) -> IPAddressFreeData | None:
|
|
645
|
+
if not self.results:
|
|
646
|
+
return None
|
|
647
|
+
|
|
648
|
+
return IPAddressFreeData.from_db(result=self.results[0])
|
|
649
|
+
|
|
650
|
+
def get_address(self) -> IPAddressType | None:
|
|
651
|
+
"""Return the next free address fitting in the prefix."""
|
|
652
|
+
result_data = self.get_address_data()
|
|
653
|
+
if result_data is None:
|
|
654
|
+
return None
|
|
655
|
+
|
|
656
|
+
if result_data.is_free:
|
|
657
|
+
return ipaddress.ip_interface(result_data.free_addr)
|
|
658
|
+
# When is_last=True and is_free=False, we've reached the end of all allocated addresses
|
|
659
|
+
# without finding a gap. The next available address is one past the last allocated address,
|
|
660
|
+
# but only if it doesn't exceed the end of the valid range (end_range).
|
|
661
|
+
if result_data.is_last and result_data.free_addr < self.params["end_range"]:
|
|
662
|
+
return ipaddress.ip_interface(result_data.free_addr + 1)
|
|
663
|
+
|
|
664
|
+
return None
|
|
665
|
+
|
|
666
|
+
|
|
667
|
+
class IPv6PrefixIPAddressFetchFree(Query):
|
|
668
|
+
"""Query to find the next free IPv6 address within a prefix.
|
|
669
|
+
|
|
670
|
+
This query uses binary string operations to handle IPv6's 128-bit address space,
|
|
671
|
+
as the integer values would overflow Neo4j's 64-bit integer type.
|
|
672
|
+
"""
|
|
673
|
+
|
|
674
|
+
name = "ipv6prefix_ipaddress_fetch_free"
|
|
675
|
+
type = QueryType.READ
|
|
676
|
+
|
|
677
|
+
def __init__(
|
|
678
|
+
self,
|
|
679
|
+
obj: IPNetworkType,
|
|
680
|
+
is_pool: bool,
|
|
681
|
+
namespace: Node | str | None = None,
|
|
682
|
+
**kwargs,
|
|
683
|
+
) -> None:
|
|
684
|
+
self.obj = obj
|
|
685
|
+
self.namespace_id = _get_namespace_id(namespace)
|
|
686
|
+
self.is_pool = is_pool
|
|
687
|
+
|
|
688
|
+
super().__init__(**kwargs)
|
|
689
|
+
|
|
690
|
+
async def query_init(self, db: InfrahubDatabase, **kwargs) -> None: # noqa: ARG002
|
|
691
|
+
self.params["ns_id"] = self.namespace_id
|
|
692
|
+
|
|
693
|
+
prefix_bin = convert_ip_to_binary_str(self.obj)[: self.obj.prefixlen]
|
|
694
|
+
self.params["prefix_binary"] = prefix_bin
|
|
695
|
+
self.params["maxprefixlen"] = self.obj.prefixlen
|
|
696
|
+
self.params["ip_version"] = self.obj.version
|
|
697
|
+
|
|
698
|
+
# Binary representation of start and end of valid range
|
|
699
|
+
start_addr = self.obj.network_address
|
|
700
|
+
end_addr = self.obj.broadcast_address
|
|
701
|
+
if not self.is_pool:
|
|
702
|
+
start_addr += 1
|
|
703
|
+
end_addr -= 1
|
|
704
|
+
self.params["start_range_bin"] = format(int(start_addr), "0128b")
|
|
705
|
+
self.params["end_range_bin"] = format(int(end_addr), "0128b")
|
|
706
|
+
|
|
707
|
+
self.limit = 1 # Query only works at returning a single, free entry
|
|
708
|
+
|
|
709
|
+
branch_filter, branch_params = self.branch.get_query_filter_path(
|
|
710
|
+
at=self.at.to_string(), branch_agnostic=self.branch_agnostic
|
|
711
|
+
)
|
|
712
|
+
self.params.update(branch_params)
|
|
713
|
+
|
|
714
|
+
# ruff: noqa: E501
|
|
715
|
+
query = """
|
|
716
|
+
// First match on IPNAMESPACE
|
|
717
|
+
MATCH (ns:%(ns_label)s)
|
|
718
|
+
WHERE ns.uuid = $ns_id
|
|
719
|
+
CALL (ns) {
|
|
720
|
+
MATCH (ns)-[r:IS_PART_OF]-(root:Root)
|
|
721
|
+
WHERE %(branch_filter)s
|
|
722
|
+
RETURN r
|
|
723
|
+
ORDER BY r.branch_level DESC, r.from DESC
|
|
724
|
+
LIMIT 1
|
|
725
|
+
}
|
|
726
|
+
WITH ns, r
|
|
727
|
+
WHERE r.status = "active"
|
|
728
|
+
WITH ns
|
|
729
|
+
// MATCH all IPAddress that are IN SCOPE
|
|
730
|
+
OPTIONAL MATCH path2 = (ns)-[:IS_RELATED]-(ns_rel:Relationship)-[:IS_RELATED]-(addr:%(node_label)s)-[:HAS_ATTRIBUTE]-(an:Attribute {name: "address"})-[:HAS_VALUE]-(av:AttributeIPHost)
|
|
731
|
+
WHERE ns_rel.name = "ip_namespace__ip_address"
|
|
732
|
+
AND av.binary_address STARTS WITH $prefix_binary
|
|
733
|
+
AND av.prefixlen >= $maxprefixlen
|
|
734
|
+
AND av.version = $ip_version
|
|
735
|
+
AND all(r IN relationships(path2) WHERE (%(branch_filter)s) and r.status = "active")
|
|
736
|
+
WITH DISTINCT av.binary_address AS used_address
|
|
737
|
+
ORDER BY used_address
|
|
738
|
+
// Collect used addresses as binary strings, prepend a sentinel value before start_range
|
|
739
|
+
WITH collect(used_address) AS used_addrs_raw
|
|
740
|
+
WITH [addr IN used_addrs_raw WHERE addr IS NOT NULL AND addr >= $start_range_bin AND addr <= $end_range_bin] AS used_addrs
|
|
741
|
+
// Gap detection using binary string comparison
|
|
742
|
+
// We iterate through used addresses and find the first gap
|
|
743
|
+
WITH reduce(acc = {cursor: $start_range_bin, found: null, is_last: false}, addr IN used_addrs |
|
|
744
|
+
CASE
|
|
745
|
+
// Already found a gap, preserve result
|
|
746
|
+
WHEN acc.found IS NOT NULL THEN acc
|
|
747
|
+
// Gap found: cursor is less than this address
|
|
748
|
+
WHEN acc.cursor < addr THEN {cursor: acc.cursor, found: acc.cursor, is_last: false}
|
|
749
|
+
// No gap: advance cursor past this address using binary increment
|
|
750
|
+
ELSE
|
|
751
|
+
{
|
|
752
|
+
cursor: CASE
|
|
753
|
+
// Check for overflow: if final carry is 1, return a marker value
|
|
754
|
+
WHEN reduce(
|
|
755
|
+
inc = {bits: split(addr, ""), carry: 1, result: []},
|
|
756
|
+
idx IN reverse(range(0, 127)) |
|
|
757
|
+
{
|
|
758
|
+
bits: inc.bits,
|
|
759
|
+
carry: CASE
|
|
760
|
+
WHEN inc.carry = 0 THEN 0
|
|
761
|
+
WHEN inc.bits[idx] = "1" THEN 1
|
|
762
|
+
ELSE 0
|
|
763
|
+
END,
|
|
764
|
+
result: CASE
|
|
765
|
+
WHEN inc.carry = 0 THEN [inc.bits[idx]] + inc.result
|
|
766
|
+
WHEN inc.bits[idx] = "1" THEN ["0"] + inc.result
|
|
767
|
+
ELSE ["1"] + inc.result
|
|
768
|
+
END
|
|
769
|
+
}
|
|
770
|
+
).carry = 1 THEN null
|
|
771
|
+
ELSE reduce(s = "", c IN reduce(
|
|
772
|
+
inc = {bits: split(addr, ""), carry: 1, result: []},
|
|
773
|
+
idx IN reverse(range(0, 127)) |
|
|
774
|
+
{
|
|
775
|
+
bits: inc.bits,
|
|
776
|
+
carry: CASE
|
|
777
|
+
WHEN inc.carry = 0 THEN 0
|
|
778
|
+
WHEN inc.bits[idx] = "1" THEN 1
|
|
779
|
+
ELSE 0
|
|
780
|
+
END,
|
|
781
|
+
result: CASE
|
|
782
|
+
WHEN inc.carry = 0 THEN [inc.bits[idx]] + inc.result
|
|
783
|
+
WHEN inc.bits[idx] = "1" THEN ["0"] + inc.result
|
|
784
|
+
ELSE ["1"] + inc.result
|
|
785
|
+
END
|
|
786
|
+
}
|
|
787
|
+
).result | s + c)
|
|
788
|
+
END,
|
|
789
|
+
found: null,
|
|
790
|
+
is_last: false
|
|
791
|
+
}
|
|
792
|
+
END
|
|
793
|
+
) AS res
|
|
794
|
+
// Determine final result
|
|
795
|
+
WITH CASE
|
|
796
|
+
// Gap was found during iteration
|
|
797
|
+
WHEN res.found IS NOT NULL THEN {addr: res.found, is_free: true, is_last: false}
|
|
798
|
+
// No gap found, check if cursor is still valid and within range
|
|
799
|
+
WHEN res.cursor IS NOT NULL AND res.cursor <= $end_range_bin THEN {addr: res.cursor, is_free: true, is_last: true}
|
|
800
|
+
// No addresses available
|
|
801
|
+
ELSE {addr: null, is_free: false, is_last: true}
|
|
802
|
+
END AS result
|
|
803
|
+
WHERE result.addr IS NOT NULL
|
|
804
|
+
WITH result.addr AS free_addr_bin, result.is_free AS is_free, result.is_last AS is_last
|
|
805
|
+
""" % {
|
|
806
|
+
"ns_label": InfrahubKind.IPNAMESPACE,
|
|
807
|
+
"node_label": InfrahubKind.IPADDRESS,
|
|
808
|
+
"branch_filter": branch_filter,
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
self.add_to_query(query)
|
|
812
|
+
self.return_labels = ["free_addr_bin", "is_free", "is_last"]
|
|
813
|
+
|
|
814
|
+
def get_address_data(self) -> IPv6AddressFreeData | None:
|
|
815
|
+
if not self.results:
|
|
816
|
+
return None
|
|
817
|
+
|
|
818
|
+
return IPv6AddressFreeData.from_db(result=self.results[0])
|
|
819
|
+
|
|
820
|
+
def get_address(self) -> IPAddressType | None:
|
|
821
|
+
"""Return the next free IPv6 address fitting in the prefix."""
|
|
822
|
+
result_data = self.get_address_data()
|
|
823
|
+
if result_data is None:
|
|
824
|
+
return None
|
|
825
|
+
|
|
826
|
+
if result_data.is_free:
|
|
827
|
+
# Convert binary string to IPv6 address
|
|
828
|
+
addr_int = int(result_data.free_addr_bin, 2)
|
|
829
|
+
return ipaddress.ip_interface(ipaddress.IPv6Address(addr_int))
|
|
830
|
+
|
|
831
|
+
return None
|
|
243
832
|
|
|
244
833
|
|
|
245
834
|
@dataclass(frozen=True)
|