infrahub-server 1.3.2__py3-none-any.whl → 1.3.4__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/api/schema.py +2 -2
- infrahub/cli/db.py +194 -13
- infrahub/core/branch/enums.py +8 -0
- infrahub/core/branch/models.py +28 -5
- infrahub/core/branch/tasks.py +5 -7
- infrahub/core/convert_object_type/conversion.py +10 -0
- infrahub/core/diff/coordinator.py +32 -34
- infrahub/core/diff/diff_locker.py +26 -0
- infrahub/core/diff/enricher/hierarchy.py +7 -3
- infrahub/core/diff/query_parser.py +7 -3
- infrahub/core/graph/__init__.py +1 -1
- infrahub/core/initialization.py +4 -3
- infrahub/core/merge.py +31 -16
- infrahub/core/migrations/graph/__init__.py +26 -0
- infrahub/core/migrations/graph/m012_convert_account_generic.py +4 -3
- infrahub/core/migrations/graph/m013_convert_git_password_credential.py +4 -3
- infrahub/core/migrations/graph/m032_cleanup_orphaned_branch_relationships.py +105 -0
- infrahub/core/migrations/graph/m033_deduplicate_relationship_vertices.py +97 -0
- infrahub/core/migrations/graph/m034_find_orphaned_schema_fields.py +84 -0
- infrahub/core/migrations/schema/node_attribute_add.py +55 -2
- infrahub/core/migrations/shared.py +37 -9
- infrahub/core/node/__init__.py +44 -21
- infrahub/core/node/resource_manager/ip_address_pool.py +5 -3
- infrahub/core/node/resource_manager/ip_prefix_pool.py +7 -4
- infrahub/core/node/resource_manager/number_pool.py +62 -22
- infrahub/core/node/standard.py +4 -0
- infrahub/core/query/branch.py +25 -56
- infrahub/core/query/node.py +78 -24
- infrahub/core/query/relationship.py +11 -8
- infrahub/core/query/resource_manager.py +117 -20
- infrahub/core/relationship/model.py +10 -5
- infrahub/core/schema/__init__.py +5 -0
- infrahub/core/schema/attribute_parameters.py +6 -0
- infrahub/core/schema/attribute_schema.py +6 -0
- infrahub/core/schema/manager.py +5 -11
- infrahub/core/schema/relationship_schema.py +6 -0
- infrahub/core/schema/schema_branch.py +50 -11
- infrahub/core/validators/node/attribute.py +15 -0
- infrahub/core/validators/tasks.py +12 -4
- infrahub/dependencies/builder/diff/coordinator.py +3 -0
- infrahub/dependencies/builder/diff/locker.py +8 -0
- infrahub/graphql/mutations/main.py +7 -2
- infrahub/graphql/mutations/tasks.py +2 -0
- infrahub/graphql/queries/resource_manager.py +4 -4
- infrahub/tasks/registry.py +63 -35
- infrahub_sdk/client.py +7 -8
- infrahub_sdk/ctl/utils.py +3 -0
- infrahub_sdk/node/node.py +6 -6
- infrahub_sdk/node/relationship.py +43 -2
- infrahub_sdk/yaml.py +13 -7
- infrahub_server-1.3.4.dist-info/LICENSE.txt +201 -0
- {infrahub_server-1.3.2.dist-info → infrahub_server-1.3.4.dist-info}/METADATA +3 -3
- {infrahub_server-1.3.2.dist-info → infrahub_server-1.3.4.dist-info}/RECORD +58 -52
- infrahub_testcontainers/container.py +1 -1
- infrahub_testcontainers/docker-compose-cluster.test.yml +3 -0
- infrahub_testcontainers/docker-compose.test.yml +1 -0
- infrahub_server-1.3.2.dist-info/LICENSE.txt +0 -661
- {infrahub_server-1.3.2.dist-info → infrahub_server-1.3.4.dist-info}/WHEEL +0 -0
- {infrahub_server-1.3.2.dist-info → infrahub_server-1.3.4.dist-info}/entry_points.txt +0 -0
infrahub/core/query/node.py
CHANGED
|
@@ -10,6 +10,7 @@ from typing import TYPE_CHECKING, Any, AsyncIterator, Generator
|
|
|
10
10
|
from infrahub import config
|
|
11
11
|
from infrahub.core import registry
|
|
12
12
|
from infrahub.core.constants import (
|
|
13
|
+
GLOBAL_BRANCH_NAME,
|
|
13
14
|
AttributeDBNodeType,
|
|
14
15
|
RelationshipDirection,
|
|
15
16
|
RelationshipHierarchyDirection,
|
|
@@ -128,7 +129,7 @@ class NodeCreateAllQuery(NodeQuery):
|
|
|
128
129
|
|
|
129
130
|
raise_error_if_empty: bool = True
|
|
130
131
|
|
|
131
|
-
async def query_init(self, db: InfrahubDatabase, **kwargs) -> None: # noqa: ARG002
|
|
132
|
+
async def query_init(self, db: InfrahubDatabase, **kwargs) -> None: # noqa: ARG002, PLR0915
|
|
132
133
|
at = self.at or self.node._at
|
|
133
134
|
self.params["uuid"] = self.node.id
|
|
134
135
|
self.params["branch"] = self.branch.name
|
|
@@ -151,11 +152,19 @@ class NodeCreateAllQuery(NodeQuery):
|
|
|
151
152
|
else:
|
|
152
153
|
attributes.append(attr_data)
|
|
153
154
|
|
|
155
|
+
deepest_branch_name = self.branch.name
|
|
156
|
+
deepest_branch_level = self.branch.hierarchy_level
|
|
154
157
|
relationships: list[RelationshipCreateData] = []
|
|
155
158
|
for rel_name in self.node._relationships:
|
|
156
159
|
rel_manager: RelationshipManager = getattr(self.node, rel_name)
|
|
157
160
|
for rel in rel_manager._relationships:
|
|
158
|
-
|
|
161
|
+
rel_create_data = await rel.get_create_data(db=db, at=at)
|
|
162
|
+
if rel_create_data.peer_branch_level > deepest_branch_level or (
|
|
163
|
+
deepest_branch_name == GLOBAL_BRANCH_NAME and rel_create_data.peer_branch == registry.default_branch
|
|
164
|
+
):
|
|
165
|
+
deepest_branch_name = rel_create_data.peer_branch
|
|
166
|
+
deepest_branch_level = rel_create_data.peer_branch_level
|
|
167
|
+
relationships.append(rel_create_data)
|
|
159
168
|
|
|
160
169
|
self.params["attrs"] = [attr.model_dump() for attr in attributes]
|
|
161
170
|
self.params["attrs_iphost"] = [attr.model_dump() for attr in attributes_iphost]
|
|
@@ -216,6 +225,8 @@ class NodeCreateAllQuery(NodeQuery):
|
|
|
216
225
|
CREATE (a)-[:HAS_VALUE { branch: attr.branch, branch_level: attr.branch_level, status: attr.status, from: $at }]->(av)
|
|
217
226
|
MERGE (ip:Boolean { value: attr.is_protected })
|
|
218
227
|
MERGE (iv:Boolean { value: attr.is_visible })
|
|
228
|
+
WITH a, ip, iv
|
|
229
|
+
LIMIT 1
|
|
219
230
|
CREATE (a)-[:IS_PROTECTED { branch: attr.branch, branch_level: attr.branch_level, status: attr.status, from: $at }]->(ip)
|
|
220
231
|
CREATE (a)-[:IS_VISIBLE { branch: attr.branch, branch_level: attr.branch_level, status: attr.status, from: $at }]->(iv)
|
|
221
232
|
FOREACH ( prop IN attr.source_prop |
|
|
@@ -230,9 +241,8 @@ class NodeCreateAllQuery(NodeQuery):
|
|
|
230
241
|
|
|
231
242
|
attrs_iphost_query = """
|
|
232
243
|
WITH distinct n
|
|
233
|
-
UNWIND $attrs_iphost AS
|
|
234
|
-
CALL (n,
|
|
235
|
-
WITH n, attr_iphost AS attr
|
|
244
|
+
UNWIND $attrs_iphost AS attr
|
|
245
|
+
CALL (n, attr) {
|
|
236
246
|
CREATE (a:Attribute { uuid: attr.uuid, name: attr.name, branch_support: attr.branch_support })
|
|
237
247
|
CREATE (n)-[:HAS_ATTRIBUTE { branch: attr.branch, branch_level: attr.branch_level, status: attr.status, from: $at }]->(a)
|
|
238
248
|
MERGE (av:AttributeValue:AttributeIPHost { %(iphost_prop)s })
|
|
@@ -241,6 +251,8 @@ class NodeCreateAllQuery(NodeQuery):
|
|
|
241
251
|
CREATE (a)-[:HAS_VALUE { branch: attr.branch, branch_level: attr.branch_level, status: attr.status, from: $at }]->(av)
|
|
242
252
|
MERGE (ip:Boolean { value: attr.is_protected })
|
|
243
253
|
MERGE (iv:Boolean { value: attr.is_visible })
|
|
254
|
+
WITH a, ip, iv
|
|
255
|
+
LIMIT 1
|
|
244
256
|
CREATE (a)-[:IS_PROTECTED { branch: attr.branch, branch_level: attr.branch_level, status: attr.status, from: $at }]->(ip)
|
|
245
257
|
CREATE (a)-[:IS_VISIBLE { branch: attr.branch, branch_level: attr.branch_level, status: attr.status, from: $at }]->(iv)
|
|
246
258
|
FOREACH ( prop IN attr.source_prop |
|
|
@@ -256,9 +268,8 @@ class NodeCreateAllQuery(NodeQuery):
|
|
|
256
268
|
|
|
257
269
|
attrs_ipnetwork_query = """
|
|
258
270
|
WITH distinct n
|
|
259
|
-
UNWIND $attrs_ipnetwork AS
|
|
260
|
-
CALL (n,
|
|
261
|
-
WITH n, attr_ipnetwork AS attr
|
|
271
|
+
UNWIND $attrs_ipnetwork AS attr
|
|
272
|
+
CALL (n, attr) {
|
|
262
273
|
CREATE (a:Attribute { uuid: attr.uuid, name: attr.name, branch_support: attr.branch_support })
|
|
263
274
|
CREATE (n)-[:HAS_ATTRIBUTE { branch: attr.branch, branch_level: attr.branch_level, status: attr.status, from: $at }]->(a)
|
|
264
275
|
MERGE (av:AttributeValue:AttributeIPNetwork { %(ipnetwork_prop)s })
|
|
@@ -267,6 +278,8 @@ class NodeCreateAllQuery(NodeQuery):
|
|
|
267
278
|
CREATE (a)-[:HAS_VALUE { branch: attr.branch, branch_level: attr.branch_level, status: attr.status, from: $at }]->(av)
|
|
268
279
|
MERGE (ip:Boolean { value: attr.is_protected })
|
|
269
280
|
MERGE (iv:Boolean { value: attr.is_visible })
|
|
281
|
+
WITH a, ip, iv
|
|
282
|
+
LIMIT 1
|
|
270
283
|
CREATE (a)-[:IS_PROTECTED { branch: attr.branch, branch_level: attr.branch_level, status: attr.status, from: $at }]->(ip)
|
|
271
284
|
CREATE (a)-[:IS_VISIBLE { branch: attr.branch, branch_level: attr.branch_level, status: attr.status, from: $at }]->(iv)
|
|
272
285
|
FOREACH ( prop IN attr.source_prop |
|
|
@@ -280,16 +293,55 @@ class NodeCreateAllQuery(NodeQuery):
|
|
|
280
293
|
}
|
|
281
294
|
""" % {"ipnetwork_prop": ", ".join(ipnetwork_prop_list)}
|
|
282
295
|
|
|
296
|
+
deepest_branch = await registry.get_branch(db=db, branch=deepest_branch_name)
|
|
297
|
+
branch_filter, branch_params = deepest_branch.get_query_filter_path(at=self.at)
|
|
298
|
+
self.params.update(branch_params)
|
|
299
|
+
self.params["global_branch_name"] = GLOBAL_BRANCH_NAME
|
|
300
|
+
self.params["default_branch_name"] = registry.default_branch
|
|
301
|
+
|
|
302
|
+
dest_node_subquery = """
|
|
303
|
+
CALL (rel) {
|
|
304
|
+
MATCH (dest_node:Node { uuid: rel.destination_id })-[r:IS_PART_OF]->(root:Root)
|
|
305
|
+
WHERE (
|
|
306
|
+
// if the relationship is on a branch, use the regular filter
|
|
307
|
+
(rel.peer_branch_level = 2 AND %(branch_filter)s)
|
|
308
|
+
// simplified filter for the global branch
|
|
309
|
+
OR (
|
|
310
|
+
rel.peer_branch_level = 1
|
|
311
|
+
AND rel.peer_branch = $global_branch_name
|
|
312
|
+
AND r.branch = $global_branch_name
|
|
313
|
+
AND r.from <= $at AND (r.to IS NULL or r.to > $at)
|
|
314
|
+
)
|
|
315
|
+
// simplified filter for the default branch
|
|
316
|
+
OR (
|
|
317
|
+
rel.peer_branch_level = 1 AND
|
|
318
|
+
rel.peer_branch = $default_branch_name AND
|
|
319
|
+
r.branch IN [$default_branch_name, $global_branch_name]
|
|
320
|
+
AND r.from <= $at AND (r.to IS NULL or r.to > $at)
|
|
321
|
+
)
|
|
322
|
+
)
|
|
323
|
+
// r.status is a tie-breaker when there are nodes with the same UUID added/deleted at the same time
|
|
324
|
+
ORDER BY r.branch_level DESC, r.from DESC, r.status ASC
|
|
325
|
+
WITH dest_node, r
|
|
326
|
+
LIMIT 1
|
|
327
|
+
WITH dest_node, r
|
|
328
|
+
WHERE r.status = "active"
|
|
329
|
+
RETURN dest_node
|
|
330
|
+
}
|
|
331
|
+
""" % {"branch_filter": branch_filter}
|
|
332
|
+
|
|
283
333
|
rels_bidir_query = """
|
|
284
334
|
WITH distinct n
|
|
285
335
|
UNWIND $rels_bidir AS rel
|
|
286
|
-
|
|
287
|
-
|
|
336
|
+
%(dest_node_subquery)s
|
|
337
|
+
CALL (n, rel, dest_node) {
|
|
288
338
|
CREATE (rl:Relationship { uuid: rel.uuid, name: rel.name, branch_support: rel.branch_support })
|
|
289
339
|
CREATE (n)-[:IS_RELATED %(rel_prop)s ]->(rl)
|
|
290
|
-
CREATE (
|
|
340
|
+
CREATE (dest_node)-[:IS_RELATED %(rel_prop)s ]->(rl)
|
|
291
341
|
MERGE (ip:Boolean { value: rel.is_protected })
|
|
292
342
|
MERGE (iv:Boolean { value: rel.is_visible })
|
|
343
|
+
WITH rl, ip, iv
|
|
344
|
+
LIMIT 1
|
|
293
345
|
CREATE (rl)-[:IS_PROTECTED { branch: rel.branch, branch_level: rel.branch_level, status: rel.status, from: $at }]->(ip)
|
|
294
346
|
CREATE (rl)-[:IS_VISIBLE { branch: rel.branch, branch_level: rel.branch_level, status: rel.status, from: $at }]->(iv)
|
|
295
347
|
FOREACH ( prop IN rel.source_prop |
|
|
@@ -301,19 +353,20 @@ class NodeCreateAllQuery(NodeQuery):
|
|
|
301
353
|
CREATE (rl)-[:HAS_OWNER { branch: rel.branch, branch_level: rel.branch_level, status: rel.status, from: $at }]->(peer)
|
|
302
354
|
)
|
|
303
355
|
}
|
|
304
|
-
""" % {"rel_prop": rel_prop_str}
|
|
356
|
+
""" % {"rel_prop": rel_prop_str, "dest_node_subquery": dest_node_subquery}
|
|
305
357
|
|
|
306
358
|
rels_out_query = """
|
|
307
359
|
WITH distinct n
|
|
308
|
-
UNWIND $rels_out AS
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
MERGE (d:Node { uuid: rel.destination_id })
|
|
360
|
+
UNWIND $rels_out AS rel
|
|
361
|
+
%(dest_node_subquery)s
|
|
362
|
+
CALL (n, rel, dest_node) {
|
|
312
363
|
CREATE (rl:Relationship { uuid: rel.uuid, name: rel.name, branch_support: rel.branch_support })
|
|
313
364
|
CREATE (n)-[:IS_RELATED %(rel_prop)s ]->(rl)
|
|
314
|
-
CREATE (
|
|
365
|
+
CREATE (dest_node)<-[:IS_RELATED %(rel_prop)s ]-(rl)
|
|
315
366
|
MERGE (ip:Boolean { value: rel.is_protected })
|
|
316
367
|
MERGE (iv:Boolean { value: rel.is_visible })
|
|
368
|
+
WITH rl, ip, iv
|
|
369
|
+
LIMIT 1
|
|
317
370
|
CREATE (rl)-[:IS_PROTECTED { branch: rel.branch, branch_level: rel.branch_level, status: rel.status, from: $at }]->(ip)
|
|
318
371
|
CREATE (rl)-[:IS_VISIBLE { branch: rel.branch, branch_level: rel.branch_level, status: rel.status, from: $at }]->(iv)
|
|
319
372
|
FOREACH ( prop IN rel.source_prop |
|
|
@@ -325,19 +378,20 @@ class NodeCreateAllQuery(NodeQuery):
|
|
|
325
378
|
CREATE (rl)-[:HAS_OWNER { branch: rel.branch, branch_level: rel.branch_level, status: rel.status, from: $at }]->(peer)
|
|
326
379
|
)
|
|
327
380
|
}
|
|
328
|
-
""" % {"rel_prop": rel_prop_str}
|
|
381
|
+
""" % {"rel_prop": rel_prop_str, "dest_node_subquery": dest_node_subquery}
|
|
329
382
|
|
|
330
383
|
rels_in_query = """
|
|
331
384
|
WITH distinct n
|
|
332
|
-
UNWIND $rels_in AS
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
MERGE (d:Node { uuid: rel.destination_id })
|
|
385
|
+
UNWIND $rels_in AS rel
|
|
386
|
+
%(dest_node_subquery)s
|
|
387
|
+
CALL (n, rel, dest_node) {
|
|
336
388
|
CREATE (rl:Relationship { uuid: rel.uuid, name: rel.name, branch_support: rel.branch_support })
|
|
337
389
|
CREATE (n)<-[:IS_RELATED %(rel_prop)s ]-(rl)
|
|
338
|
-
CREATE (
|
|
390
|
+
CREATE (dest_node)-[:IS_RELATED %(rel_prop)s ]->(rl)
|
|
339
391
|
MERGE (ip:Boolean { value: rel.is_protected })
|
|
340
392
|
MERGE (iv:Boolean { value: rel.is_visible })
|
|
393
|
+
WITH rl, ip, iv
|
|
394
|
+
LIMIT 1
|
|
341
395
|
CREATE (rl)-[:IS_PROTECTED { branch: rel.branch, branch_level: rel.branch_level, status: rel.status, from: $at }]->(ip)
|
|
342
396
|
CREATE (rl)-[:IS_VISIBLE { branch: rel.branch, branch_level: rel.branch_level, status: rel.status, from: $at }]->(iv)
|
|
343
397
|
FOREACH ( prop IN rel.source_prop |
|
|
@@ -349,7 +403,7 @@ class NodeCreateAllQuery(NodeQuery):
|
|
|
349
403
|
CREATE (rl)-[:HAS_OWNER { branch: rel.branch, branch_level: rel.branch_level, status: rel.status, from: $at }]->(peer)
|
|
350
404
|
)
|
|
351
405
|
}
|
|
352
|
-
""" % {"rel_prop": rel_prop_str}
|
|
406
|
+
""" % {"rel_prop": rel_prop_str, "dest_node_subquery": dest_node_subquery}
|
|
353
407
|
|
|
354
408
|
query = f"""
|
|
355
409
|
MATCH (root:Root)
|
|
@@ -206,16 +206,18 @@ class RelationshipQuery(Query):
|
|
|
206
206
|
self.params["source_id"] = self.source_id or self.source.get_id()
|
|
207
207
|
if source_branch.is_global or source_branch.is_default:
|
|
208
208
|
source_query_match = """
|
|
209
|
-
MATCH (s:Node { uuid: $source_id })
|
|
209
|
+
MATCH (s:Node { uuid: $source_id })-[source_e:IS_PART_OF {branch: $source_branch, status: "active"}]->(:Root)
|
|
210
|
+
WHERE source_e.from <= $at AND (source_e.to IS NULL OR source_e.to > $at)
|
|
210
211
|
OPTIONAL MATCH (s)-[delete_edge:IS_PART_OF {status: "deleted", branch: $source_branch}]->(:Root)
|
|
211
212
|
WHERE delete_edge.from <= $at
|
|
212
213
|
WITH *, s WHERE delete_edge IS NULL
|
|
213
214
|
"""
|
|
214
215
|
self.params["source_branch"] = source_branch.name
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
216
|
+
else:
|
|
217
|
+
source_filter, source_filter_params = source_branch.get_query_filter_path(
|
|
218
|
+
at=self.at, variable_name="r", params_prefix="src_"
|
|
219
|
+
)
|
|
220
|
+
source_query_match = """
|
|
219
221
|
MATCH (s:Node { uuid: $source_id })
|
|
220
222
|
CALL (s) {
|
|
221
223
|
MATCH (s)-[r:IS_PART_OF]->(:Root)
|
|
@@ -225,15 +227,16 @@ class RelationshipQuery(Query):
|
|
|
225
227
|
LIMIT 1
|
|
226
228
|
}
|
|
227
229
|
WITH *, s WHERE s_is_active = TRUE
|
|
228
|
-
|
|
229
|
-
|
|
230
|
+
""" % {"source_filter": source_filter}
|
|
231
|
+
self.params.update(source_filter_params)
|
|
230
232
|
self.add_to_query(source_query_match)
|
|
231
233
|
|
|
232
234
|
def add_dest_match_to_query(self, destination_branch: Branch, destination_id: str) -> None:
|
|
233
235
|
self.params["destination_id"] = destination_id
|
|
234
236
|
if destination_branch.is_global or destination_branch.is_default:
|
|
235
237
|
destination_query_match = """
|
|
236
|
-
MATCH (d:Node { uuid: $destination_id })
|
|
238
|
+
MATCH (d:Node { uuid: $destination_id })-[dest_e:IS_PART_OF {branch: $destination_branch, status: "active"}]->(:Root)
|
|
239
|
+
WHERE dest_e.from <= $at AND (dest_e.to IS NULL OR dest_e.to > $at)
|
|
237
240
|
OPTIONAL MATCH (d)-[delete_edge:IS_PART_OF {status: "deleted", branch: $destination_branch}]->(:Root)
|
|
238
241
|
WHERE delete_edge.from <= $at
|
|
239
242
|
WITH *, d WHERE delete_edge IS NULL
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from typing import TYPE_CHECKING, Any
|
|
3
|
+
from typing import TYPE_CHECKING, Any, Generator
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, ConfigDict
|
|
4
6
|
|
|
5
7
|
from infrahub.core import registry
|
|
6
8
|
from infrahub.core.constants import InfrahubKind, RelationshipStatus
|
|
@@ -11,6 +13,13 @@ if TYPE_CHECKING:
|
|
|
11
13
|
from infrahub.database import InfrahubDatabase
|
|
12
14
|
|
|
13
15
|
|
|
16
|
+
class NumberPoolIdentifierData(BaseModel):
|
|
17
|
+
model_config = ConfigDict(frozen=True)
|
|
18
|
+
|
|
19
|
+
value: int
|
|
20
|
+
identifier: str
|
|
21
|
+
|
|
22
|
+
|
|
14
23
|
class IPAddressPoolGetIdentifiers(Query):
|
|
15
24
|
name = "ipaddresspool_get_identifiers"
|
|
16
25
|
type = QueryType.READ
|
|
@@ -158,7 +167,7 @@ class NumberPoolGetReserved(Query):
|
|
|
158
167
|
def __init__(
|
|
159
168
|
self,
|
|
160
169
|
pool_id: str,
|
|
161
|
-
identifier: str,
|
|
170
|
+
identifier: str | None = None,
|
|
162
171
|
**kwargs: dict[str, Any],
|
|
163
172
|
) -> None:
|
|
164
173
|
self.pool_id = pool_id
|
|
@@ -176,26 +185,104 @@ class NumberPoolGetReserved(Query):
|
|
|
176
185
|
|
|
177
186
|
self.params.update(branch_params)
|
|
178
187
|
|
|
188
|
+
# If identifier is not provided, we return all reservations for the pool
|
|
189
|
+
identifier_filter = ""
|
|
190
|
+
if self.identifier:
|
|
191
|
+
identifier_filter = "r.identifier = $identifier AND "
|
|
192
|
+
self.params["identifier"] = self.identifier
|
|
193
|
+
|
|
179
194
|
query = """
|
|
180
195
|
MATCH (pool:%(number_pool)s { uuid: $pool_id })-[r:IS_RESERVED]->(reservation:AttributeValue)
|
|
181
196
|
WHERE
|
|
182
|
-
|
|
183
|
-
AND
|
|
197
|
+
%(identifier_filter)s
|
|
184
198
|
%(branch_filter)s
|
|
185
|
-
""" % {
|
|
199
|
+
""" % {
|
|
200
|
+
"branch_filter": branch_filter,
|
|
201
|
+
"number_pool": InfrahubKind.NUMBERPOOL,
|
|
202
|
+
"identifier_filter": identifier_filter,
|
|
203
|
+
}
|
|
186
204
|
self.add_to_query(query)
|
|
187
|
-
self.return_labels = ["reservation.value"]
|
|
205
|
+
self.return_labels = ["reservation.value AS value", "r.identifier AS identifier"]
|
|
188
206
|
|
|
189
207
|
def get_reservation(self) -> int | None:
|
|
190
208
|
result = self.get_result()
|
|
191
209
|
if result:
|
|
192
|
-
return result.get_as_optional_type("
|
|
210
|
+
return result.get_as_optional_type("value", return_type=int)
|
|
193
211
|
return None
|
|
194
212
|
|
|
213
|
+
def get_reservations(self) -> Generator[NumberPoolIdentifierData]:
|
|
214
|
+
for result in self.results:
|
|
215
|
+
yield NumberPoolIdentifierData.model_construct(
|
|
216
|
+
value=result.get_as_type("value", return_type=int),
|
|
217
|
+
identifier=result.get_as_type("identifier", return_type=str),
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
class PoolChangeReserved(Query):
|
|
222
|
+
"""Change the identifier on all pools.
|
|
223
|
+
This is useful when a node is being converted to a different type and its ID has changed
|
|
224
|
+
"""
|
|
225
|
+
|
|
226
|
+
name = "pool_change_reserved"
|
|
227
|
+
type = QueryType.WRITE
|
|
228
|
+
|
|
229
|
+
def __init__(
|
|
230
|
+
self,
|
|
231
|
+
existing_identifier: str,
|
|
232
|
+
new_identifier: str,
|
|
233
|
+
**kwargs: dict[str, Any],
|
|
234
|
+
) -> None:
|
|
235
|
+
self.existing_identifier = existing_identifier
|
|
236
|
+
self.new_identifier = new_identifier
|
|
237
|
+
|
|
238
|
+
super().__init__(**kwargs) # type: ignore[arg-type]
|
|
239
|
+
|
|
240
|
+
async def query_init(self, db: InfrahubDatabase, **kwargs: dict[str, Any]) -> None: # noqa: ARG002
|
|
241
|
+
self.params["new_identifier"] = self.new_identifier
|
|
242
|
+
self.params["existing_identifier"] = self.existing_identifier
|
|
243
|
+
self.params["at"] = self.at.to_string()
|
|
244
|
+
|
|
245
|
+
branch_filter, branch_params = self.branch.get_query_filter_path(
|
|
246
|
+
at=self.at.to_string(), branch_agnostic=self.branch_agnostic
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
self.params.update(branch_params)
|
|
250
|
+
|
|
251
|
+
global_branch = registry.get_global_branch()
|
|
252
|
+
self.params["rel_prop"] = {
|
|
253
|
+
"branch": global_branch.name,
|
|
254
|
+
"branch_level": global_branch.hierarchy_level,
|
|
255
|
+
"status": RelationshipStatus.ACTIVE.value,
|
|
256
|
+
"from": self.at.to_string(),
|
|
257
|
+
"identifier": self.new_identifier,
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
query = """
|
|
261
|
+
MATCH (pool:Node)-[r:IS_RESERVED]->(resource)
|
|
262
|
+
WHERE
|
|
263
|
+
r.identifier = $existing_identifier
|
|
264
|
+
AND
|
|
265
|
+
%(branch_filter)s
|
|
266
|
+
SET r.to = $at
|
|
267
|
+
CREATE (pool)-[new_rel:IS_RESERVED $rel_prop]->(resource)
|
|
268
|
+
""" % {"branch_filter": branch_filter}
|
|
269
|
+
self.add_to_query(query)
|
|
270
|
+
self.return_labels = ["pool.uuid AS pool_id", "r", "new_rel"]
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
"""
|
|
274
|
+
Important!: The relationship IS_RESERVED for Number is not being cleaned up when the node or the branch is deleted
|
|
275
|
+
I think this is something we should address in the future.
|
|
276
|
+
It works for now because the query has been updated to match the identifier in IS_RESERVED with the UUID of the related node
|
|
277
|
+
But in the future, if we need to use an identifier that is not the UUID, we will need to clean up the relationships
|
|
278
|
+
This will be especially important as we want to support upsert with NumberPool
|
|
279
|
+
"""
|
|
280
|
+
|
|
195
281
|
|
|
196
282
|
class NumberPoolGetUsed(Query):
|
|
197
283
|
name = "number_pool_get_used"
|
|
198
284
|
type = QueryType.READ
|
|
285
|
+
return_model = NumberPoolIdentifierData
|
|
199
286
|
|
|
200
287
|
def __init__(
|
|
201
288
|
self,
|
|
@@ -219,26 +306,36 @@ class NumberPoolGetUsed(Query):
|
|
|
219
306
|
self.params["attribute_name"] = self.pool.node_attribute.value
|
|
220
307
|
|
|
221
308
|
query = """
|
|
222
|
-
MATCH (pool:%(number_pool)s { uuid: $pool_id })
|
|
223
|
-
|
|
224
|
-
|
|
309
|
+
MATCH (pool:%(number_pool)s { uuid: $pool_id })-[res:IS_RESERVED]->(av:AttributeValue)
|
|
310
|
+
WHERE toInteger(av.value) >= $start_range and toInteger(av.value) <= $end_range
|
|
311
|
+
CALL (pool, res, av) {
|
|
312
|
+
MATCH (pool)-[res]->(av)<-[hv:HAS_VALUE]-(attr:Attribute)<-[ha:HAS_ATTRIBUTE]-(n:%(node)s)
|
|
225
313
|
WHERE
|
|
226
|
-
|
|
227
|
-
AND
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
314
|
+
n.uuid = res.identifier AND
|
|
315
|
+
attr.name = $attribute_name AND
|
|
316
|
+
all(r in [res, hv, ha] WHERE (%(branch_filter)s))
|
|
317
|
+
ORDER BY res.branch_level DESC, hv.branch_level DESC, ha.branch_level DESC, res.from DESC, hv.from DESC, ha.from DESC
|
|
318
|
+
RETURN (res.status = "active" AND hv.status = "active" AND ha.status = "active") AS is_active
|
|
319
|
+
LIMIT 1
|
|
232
320
|
}
|
|
233
|
-
WITH av, is_active
|
|
234
|
-
WHERE is_active =
|
|
321
|
+
WITH av, res, is_active
|
|
322
|
+
WHERE is_active = True
|
|
235
323
|
""" % {
|
|
236
324
|
"branch_filter": branch_filter,
|
|
237
325
|
"number_pool": InfrahubKind.NUMBERPOOL,
|
|
326
|
+
"node": self.pool.node.value,
|
|
238
327
|
}
|
|
328
|
+
|
|
239
329
|
self.add_to_query(query)
|
|
240
|
-
self.return_labels = ["av.value"]
|
|
241
|
-
self.order_by = ["
|
|
330
|
+
self.return_labels = ["DISTINCT(av.value) as value", "res.identifier as identifier"]
|
|
331
|
+
self.order_by = ["value"]
|
|
332
|
+
|
|
333
|
+
def iter_results(self) -> Generator[NumberPoolIdentifierData]:
|
|
334
|
+
for result in self.results:
|
|
335
|
+
yield self.return_model.model_construct(
|
|
336
|
+
value=result.get_as_type("value", return_type=int),
|
|
337
|
+
identifier=result.get_as_type("identifier", return_type=str),
|
|
338
|
+
)
|
|
242
339
|
|
|
243
340
|
|
|
244
341
|
class NumberPoolSetReserved(Query):
|
|
@@ -63,9 +63,11 @@ class RelationshipCreateData(BaseModel):
|
|
|
63
63
|
uuid: str
|
|
64
64
|
name: str
|
|
65
65
|
destination_id: str
|
|
66
|
-
branch: str
|
|
66
|
+
branch: str
|
|
67
67
|
branch_level: int
|
|
68
68
|
branch_support: str | None = None
|
|
69
|
+
peer_branch: str
|
|
70
|
+
peer_branch_level: int
|
|
69
71
|
direction: str
|
|
70
72
|
status: str
|
|
71
73
|
is_protected: bool
|
|
@@ -420,7 +422,7 @@ class Relationship(FlagPropertyMixin, NodePropertyMixin):
|
|
|
420
422
|
)
|
|
421
423
|
await delete_query.execute(db=db)
|
|
422
424
|
|
|
423
|
-
async def resolve(self, db: InfrahubDatabase) -> None:
|
|
425
|
+
async def resolve(self, db: InfrahubDatabase, at: Timestamp | None = None) -> None:
|
|
424
426
|
"""Resolve the peer of the relationship."""
|
|
425
427
|
|
|
426
428
|
if self._peer is not None:
|
|
@@ -470,7 +472,7 @@ class Relationship(FlagPropertyMixin, NodePropertyMixin):
|
|
|
470
472
|
if hfid_str:
|
|
471
473
|
data_from_pool["identifier"] = f"hfid={hfid_str} rel={self.name}"
|
|
472
474
|
|
|
473
|
-
assigned_peer: Node = await pool.get_resource(db=db, branch=self.branch, **data_from_pool) # type: ignore[attr-defined]
|
|
475
|
+
assigned_peer: Node = await pool.get_resource(db=db, branch=self.branch, at=at, **data_from_pool) # type: ignore[attr-defined]
|
|
474
476
|
await self.set_peer(value=assigned_peer)
|
|
475
477
|
self.set_source(value=pool.id)
|
|
476
478
|
|
|
@@ -526,16 +528,19 @@ class Relationship(FlagPropertyMixin, NodePropertyMixin):
|
|
|
526
528
|
|
|
527
529
|
return response
|
|
528
530
|
|
|
529
|
-
async def get_create_data(self, db: InfrahubDatabase) -> RelationshipCreateData:
|
|
531
|
+
async def get_create_data(self, db: InfrahubDatabase, at: Timestamp | None = None) -> RelationshipCreateData:
|
|
530
532
|
branch = self.get_branch_based_on_support_type()
|
|
531
533
|
|
|
532
|
-
await self.resolve(db=db)
|
|
534
|
+
await self.resolve(db=db, at=at)
|
|
533
535
|
|
|
534
536
|
peer = await self.get_peer(db=db)
|
|
537
|
+
peer_branch = peer.get_branch()
|
|
535
538
|
data = RelationshipCreateData(
|
|
536
539
|
uuid=str(UUIDT()),
|
|
537
540
|
name=self.schema.get_identifier(),
|
|
538
541
|
branch=branch.name,
|
|
542
|
+
peer_branch=peer_branch.name,
|
|
543
|
+
peer_branch_level=peer_branch.hierarchy_level,
|
|
539
544
|
destination_id=peer.id,
|
|
540
545
|
status="active",
|
|
541
546
|
direction=self.schema.direction.value,
|
infrahub/core/schema/__init__.py
CHANGED
|
@@ -46,6 +46,7 @@ class SchemaExtension(HashableModel):
|
|
|
46
46
|
|
|
47
47
|
class SchemaRoot(BaseModel):
|
|
48
48
|
model_config = ConfigDict(extra="forbid")
|
|
49
|
+
|
|
49
50
|
version: str | None = Field(default=None)
|
|
50
51
|
generics: list[GenericSchema] = Field(default_factory=list)
|
|
51
52
|
nodes: list[NodeSchema] = Field(default_factory=list)
|
|
@@ -93,6 +94,10 @@ class SchemaRoot(BaseModel):
|
|
|
93
94
|
"""Return a new `SchemaRoot` after merging `self` with `schema`."""
|
|
94
95
|
return SchemaRoot.model_validate(deep_merge_dict(dicta=self.model_dump(), dictb=schema.model_dump()))
|
|
95
96
|
|
|
97
|
+
def duplicate(self) -> SchemaRoot:
|
|
98
|
+
"""Return a duplicate of the current schema."""
|
|
99
|
+
return SchemaRoot.model_validate(self.model_dump())
|
|
100
|
+
|
|
96
101
|
|
|
97
102
|
internal_schema = internal.to_dict()
|
|
98
103
|
|
|
@@ -165,3 +165,9 @@ class NumberPoolParameters(AttributeParameters):
|
|
|
165
165
|
if self.start_range > self.end_range:
|
|
166
166
|
raise ValueError("`start_range` can't be less than `end_range`")
|
|
167
167
|
return self
|
|
168
|
+
|
|
169
|
+
def get_pool_size(self) -> int:
|
|
170
|
+
"""
|
|
171
|
+
Returns the size of the pool based on the defined ranges.
|
|
172
|
+
"""
|
|
173
|
+
return self.end_range - self.start_range + 1
|
|
@@ -10,6 +10,7 @@ from infrahub import config
|
|
|
10
10
|
from infrahub.core.constants.schema import UpdateSupport
|
|
11
11
|
from infrahub.core.enums import generate_python_enum
|
|
12
12
|
from infrahub.core.query.attribute import default_attribute_query_filter
|
|
13
|
+
from infrahub.exceptions import InitializationError
|
|
13
14
|
from infrahub.types import ATTRIBUTE_KIND_LABELS, ATTRIBUTE_TYPES
|
|
14
15
|
|
|
15
16
|
from .attribute_parameters import (
|
|
@@ -67,6 +68,11 @@ class AttributeSchema(GeneratedAttributeSchema):
|
|
|
67
68
|
def is_deprecated(self) -> bool:
|
|
68
69
|
return bool(self.deprecation)
|
|
69
70
|
|
|
71
|
+
def get_id(self) -> str:
|
|
72
|
+
if self.id is None:
|
|
73
|
+
raise InitializationError("The attribute schema has not been saved yet and doesn't have an id")
|
|
74
|
+
return self.id
|
|
75
|
+
|
|
70
76
|
def to_dict(self) -> dict:
|
|
71
77
|
data = self.model_dump(exclude_unset=True, exclude_none=True)
|
|
72
78
|
for field_name, value in data.items():
|
infrahub/core/schema/manager.py
CHANGED
|
@@ -535,7 +535,7 @@ class SchemaManager(NodeManager):
|
|
|
535
535
|
"""Delete the node with its attributes and relationships."""
|
|
536
536
|
branch = await registry.get_branch(branch=branch, db=db)
|
|
537
537
|
|
|
538
|
-
obj = await self.get_one(id=node.get_id(), branch=branch, db=db)
|
|
538
|
+
obj = await self.get_one(id=node.get_id(), branch=branch, db=db, prefetch_relationships=True)
|
|
539
539
|
if not obj:
|
|
540
540
|
raise SchemaNotFoundError(
|
|
541
541
|
branch_name=branch.name,
|
|
@@ -544,16 +544,10 @@ class SchemaManager(NodeManager):
|
|
|
544
544
|
)
|
|
545
545
|
|
|
546
546
|
# First delete the attributes and the relationships
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
include_owner=True,
|
|
552
|
-
include_source=True,
|
|
553
|
-
)
|
|
554
|
-
|
|
555
|
-
for item in items.values():
|
|
556
|
-
await item.delete(db=db)
|
|
547
|
+
for attr_schema_node in (await obj.attributes.get_peers(db=db)).values():
|
|
548
|
+
await attr_schema_node.delete(db=db)
|
|
549
|
+
for rel_schema_node in (await obj.relationships.get_peers(db=db)).values():
|
|
550
|
+
await rel_schema_node.delete(db=db)
|
|
557
551
|
|
|
558
552
|
await obj.delete(db=db)
|
|
559
553
|
|
|
@@ -9,6 +9,7 @@ from infrahub import config
|
|
|
9
9
|
from infrahub.core.constants import RelationshipDirection
|
|
10
10
|
from infrahub.core.query import QueryNode, QueryRel, QueryRelDirection
|
|
11
11
|
from infrahub.core.relationship import Relationship
|
|
12
|
+
from infrahub.exceptions import InitializationError
|
|
12
13
|
|
|
13
14
|
from .generated.relationship_schema import GeneratedRelationshipSchema
|
|
14
15
|
|
|
@@ -57,6 +58,11 @@ class RelationshipSchema(GeneratedRelationshipSchema):
|
|
|
57
58
|
raise ValueError("RelationshipSchema is not initialized")
|
|
58
59
|
return self.identifier
|
|
59
60
|
|
|
61
|
+
def get_id(self) -> str:
|
|
62
|
+
if not self.id:
|
|
63
|
+
raise InitializationError("The relationship schema has not been saved yet and doesn't have an id")
|
|
64
|
+
return self.id
|
|
65
|
+
|
|
60
66
|
def get_query_arrows(self) -> QueryArrows:
|
|
61
67
|
"""Return (in 4 parts) the 2 arrows for the relationship R1 and R2 based on the direction of the relationship."""
|
|
62
68
|
|