infrahub-server 1.3.6__py3-none-any.whl → 1.4.0b1__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.
Files changed (163) hide show
  1. infrahub/api/internal.py +5 -0
  2. infrahub/artifacts/tasks.py +17 -22
  3. infrahub/branch/merge_mutation_checker.py +38 -0
  4. infrahub/cli/__init__.py +2 -2
  5. infrahub/cli/context.py +7 -3
  6. infrahub/cli/db.py +5 -41
  7. infrahub/cli/upgrade.py +7 -29
  8. infrahub/computed_attribute/tasks.py +36 -46
  9. infrahub/config.py +53 -2
  10. infrahub/constants/environment.py +1 -0
  11. infrahub/core/attribute.py +9 -7
  12. infrahub/core/branch/tasks.py +43 -41
  13. infrahub/core/constants/__init__.py +20 -6
  14. infrahub/core/constants/infrahubkind.py +2 -0
  15. infrahub/core/diff/coordinator.py +3 -1
  16. infrahub/core/diff/repository/repository.py +0 -8
  17. infrahub/core/diff/tasks.py +11 -8
  18. infrahub/core/graph/__init__.py +1 -1
  19. infrahub/core/graph/index.py +1 -2
  20. infrahub/core/graph/schema.py +50 -29
  21. infrahub/core/initialization.py +62 -33
  22. infrahub/core/ipam/tasks.py +4 -3
  23. infrahub/core/manager.py +2 -2
  24. infrahub/core/merge.py +8 -10
  25. infrahub/core/migrations/graph/__init__.py +4 -0
  26. infrahub/core/migrations/graph/m035_drop_attr_value_index.py +45 -0
  27. infrahub/core/migrations/graph/m036_index_attr_vals.py +577 -0
  28. infrahub/core/migrations/query/attribute_add.py +27 -2
  29. infrahub/core/migrations/query/node_duplicate.py +3 -26
  30. infrahub/core/migrations/schema/tasks.py +6 -5
  31. infrahub/core/node/proposed_change.py +43 -0
  32. infrahub/core/protocols.py +12 -0
  33. infrahub/core/query/attribute.py +32 -14
  34. infrahub/core/query/diff.py +11 -0
  35. infrahub/core/query/ipam.py +13 -7
  36. infrahub/core/query/node.py +51 -23
  37. infrahub/core/query/resource_manager.py +3 -3
  38. infrahub/core/relationship/model.py +13 -13
  39. infrahub/core/schema/basenode_schema.py +8 -0
  40. infrahub/core/schema/definitions/core/__init__.py +10 -1
  41. infrahub/core/schema/definitions/core/ipam.py +28 -2
  42. infrahub/core/schema/definitions/core/propose_change.py +15 -0
  43. infrahub/core/schema/definitions/core/webhook.py +3 -0
  44. infrahub/core/schema/generic_schema.py +10 -0
  45. infrahub/core/schema/manager.py +10 -1
  46. infrahub/core/schema/node_schema.py +22 -17
  47. infrahub/core/schema/profile_schema.py +8 -0
  48. infrahub/core/schema/schema_branch.py +9 -5
  49. infrahub/core/schema/template_schema.py +8 -0
  50. infrahub/core/validators/checks_runner.py +5 -5
  51. infrahub/core/validators/tasks.py +6 -7
  52. infrahub/core/validators/uniqueness/checker.py +4 -2
  53. infrahub/core/validators/uniqueness/model.py +1 -0
  54. infrahub/core/validators/uniqueness/query.py +57 -7
  55. infrahub/database/__init__.py +2 -1
  56. infrahub/events/__init__.py +18 -0
  57. infrahub/events/constants.py +7 -0
  58. infrahub/events/generator.py +29 -2
  59. infrahub/events/proposed_change_action.py +181 -0
  60. infrahub/generators/tasks.py +24 -20
  61. infrahub/git/base.py +4 -7
  62. infrahub/git/integrator.py +21 -12
  63. infrahub/git/repository.py +15 -30
  64. infrahub/git/tasks.py +121 -106
  65. infrahub/graphql/field_extractor.py +69 -0
  66. infrahub/graphql/manager.py +15 -11
  67. infrahub/graphql/mutations/account.py +2 -2
  68. infrahub/graphql/mutations/action.py +8 -2
  69. infrahub/graphql/mutations/artifact_definition.py +4 -1
  70. infrahub/graphql/mutations/branch.py +10 -5
  71. infrahub/graphql/mutations/graphql_query.py +2 -1
  72. infrahub/graphql/mutations/main.py +14 -8
  73. infrahub/graphql/mutations/menu.py +2 -1
  74. infrahub/graphql/mutations/proposed_change.py +225 -8
  75. infrahub/graphql/mutations/relationship.py +6 -1
  76. infrahub/graphql/mutations/repository.py +2 -1
  77. infrahub/graphql/mutations/tasks.py +7 -9
  78. infrahub/graphql/mutations/webhook.py +4 -1
  79. infrahub/graphql/parser.py +15 -6
  80. infrahub/graphql/queries/__init__.py +10 -1
  81. infrahub/graphql/queries/account.py +3 -3
  82. infrahub/graphql/queries/branch.py +2 -2
  83. infrahub/graphql/queries/diff/tree.py +3 -3
  84. infrahub/graphql/queries/event.py +13 -3
  85. infrahub/graphql/queries/ipam.py +23 -1
  86. infrahub/graphql/queries/proposed_change.py +84 -0
  87. infrahub/graphql/queries/relationship.py +2 -2
  88. infrahub/graphql/queries/resource_manager.py +3 -3
  89. infrahub/graphql/queries/search.py +3 -2
  90. infrahub/graphql/queries/status.py +3 -2
  91. infrahub/graphql/queries/task.py +2 -2
  92. infrahub/graphql/resolvers/ipam.py +440 -0
  93. infrahub/graphql/resolvers/many_relationship.py +4 -3
  94. infrahub/graphql/resolvers/resolver.py +5 -5
  95. infrahub/graphql/resolvers/single_relationship.py +3 -2
  96. infrahub/graphql/schema.py +25 -5
  97. infrahub/graphql/types/__init__.py +2 -2
  98. infrahub/graphql/types/attribute.py +3 -3
  99. infrahub/graphql/types/event.py +60 -0
  100. infrahub/groups/tasks.py +6 -6
  101. infrahub/lock.py +3 -2
  102. infrahub/menu/generator.py +8 -0
  103. infrahub/message_bus/operations/__init__.py +9 -12
  104. infrahub/message_bus/operations/git/file.py +6 -5
  105. infrahub/message_bus/operations/git/repository.py +12 -20
  106. infrahub/message_bus/operations/refresh/registry.py +15 -9
  107. infrahub/message_bus/operations/send/echo.py +7 -4
  108. infrahub/message_bus/types.py +1 -0
  109. infrahub/permissions/globals.py +1 -4
  110. infrahub/permissions/manager.py +8 -5
  111. infrahub/pools/prefix.py +7 -5
  112. infrahub/prefect_server/app.py +31 -0
  113. infrahub/prefect_server/bootstrap.py +18 -0
  114. infrahub/proposed_change/action_checker.py +206 -0
  115. infrahub/proposed_change/approval_revoker.py +40 -0
  116. infrahub/proposed_change/branch_diff.py +3 -1
  117. infrahub/proposed_change/checker.py +45 -0
  118. infrahub/proposed_change/constants.py +32 -2
  119. infrahub/proposed_change/tasks.py +182 -150
  120. infrahub/server.py +29 -17
  121. infrahub/services/__init__.py +13 -28
  122. infrahub/services/adapters/cache/__init__.py +4 -0
  123. infrahub/services/adapters/cache/nats.py +2 -0
  124. infrahub/services/adapters/cache/redis.py +3 -0
  125. infrahub/services/adapters/message_bus/__init__.py +0 -2
  126. infrahub/services/adapters/message_bus/local.py +1 -2
  127. infrahub/services/adapters/message_bus/nats.py +6 -8
  128. infrahub/services/adapters/message_bus/rabbitmq.py +7 -9
  129. infrahub/services/adapters/workflow/__init__.py +1 -0
  130. infrahub/services/adapters/workflow/local.py +1 -8
  131. infrahub/services/component.py +2 -1
  132. infrahub/task_manager/event.py +52 -0
  133. infrahub/task_manager/models.py +9 -0
  134. infrahub/tasks/artifact.py +6 -7
  135. infrahub/tasks/check.py +4 -7
  136. infrahub/telemetry/tasks.py +15 -18
  137. infrahub/transformations/tasks.py +10 -6
  138. infrahub/trigger/tasks.py +4 -3
  139. infrahub/types.py +4 -0
  140. infrahub/validators/events.py +7 -7
  141. infrahub/validators/tasks.py +6 -7
  142. infrahub/webhook/models.py +45 -45
  143. infrahub/webhook/tasks.py +25 -24
  144. infrahub/workers/dependencies.py +143 -0
  145. infrahub/workers/infrahub_async.py +19 -43
  146. infrahub/workflows/catalogue.py +16 -2
  147. infrahub/workflows/initialization.py +5 -4
  148. infrahub/workflows/models.py +2 -0
  149. infrahub_sdk/client.py +6 -6
  150. infrahub_sdk/ctl/repository.py +51 -0
  151. infrahub_sdk/ctl/schema.py +9 -9
  152. infrahub_sdk/protocols.py +40 -6
  153. {infrahub_server-1.3.6.dist-info → infrahub_server-1.4.0b1.dist-info}/METADATA +6 -4
  154. {infrahub_server-1.3.6.dist-info → infrahub_server-1.4.0b1.dist-info}/RECORD +162 -149
  155. infrahub_testcontainers/container.py +17 -0
  156. infrahub_testcontainers/docker-compose-cluster.test.yml +56 -1
  157. infrahub_testcontainers/docker-compose.test.yml +56 -1
  158. infrahub_testcontainers/helpers.py +4 -1
  159. infrahub/cli/db_commands/check_inheritance.py +0 -284
  160. /infrahub/{cli/db_commands/__init__.py → py.typed} +0 -0
  161. {infrahub_server-1.3.6.dist-info → infrahub_server-1.4.0b1.dist-info}/LICENSE.txt +0 -0
  162. {infrahub_server-1.3.6.dist-info → infrahub_server-1.4.0b1.dist-info}/WHEEL +0 -0
  163. {infrahub_server-1.3.6.dist-info → infrahub_server-1.4.0b1.dist-info}/entry_points.txt +0 -0
@@ -10,17 +10,18 @@ from prefect.logging import get_run_logger
10
10
  from infrahub.core.branch import Branch # noqa: TC001
11
11
  from infrahub.core.migrations import MIGRATION_MAP
12
12
  from infrahub.core.path import SchemaPath # noqa: TC001
13
- from infrahub.services import InfrahubServices # noqa: TC001 needed for prefect flow
13
+ from infrahub.workers.dependencies import get_database
14
14
  from infrahub.workflows.utils import add_branch_tag
15
15
 
16
16
  from .models import SchemaApplyMigrationData, SchemaMigrationPathResponseData
17
17
 
18
18
  if TYPE_CHECKING:
19
19
  from infrahub.core.schema import MainSchemaTypes
20
+ from infrahub.database import InfrahubDatabase
20
21
 
21
22
 
22
23
  @flow(name="schema_apply_migrations", flow_run_name="Apply schema migrations", persist_result=True)
23
- async def schema_apply_migrations(message: SchemaApplyMigrationData, service: InfrahubServices) -> list[str]:
24
+ async def schema_apply_migrations(message: SchemaApplyMigrationData) -> list[str]:
24
25
  await add_branch_tag(branch_name=message.branch.name)
25
26
  log = get_run_logger()
26
27
 
@@ -55,7 +56,7 @@ async def schema_apply_migrations(message: SchemaApplyMigrationData, service: In
55
56
  new_node_schema=new_node_schema,
56
57
  previous_node_schema=previous_node_schema,
57
58
  schema_path=migration.path,
58
- service=service,
59
+ database=await get_database(),
59
60
  )
60
61
 
61
62
  async for _, result in batch.execute():
@@ -75,13 +76,13 @@ async def schema_path_migrate(
75
76
  branch: Branch,
76
77
  migration_name: str,
77
78
  schema_path: SchemaPath,
78
- service: InfrahubServices,
79
+ database: InfrahubDatabase,
79
80
  new_node_schema: MainSchemaTypes | None = None,
80
81
  previous_node_schema: MainSchemaTypes | None = None,
81
82
  ) -> SchemaMigrationPathResponseData:
82
83
  log = get_run_logger()
83
84
 
84
- async with service.database.start_session() as db:
85
+ async with database.start_session() as db:
85
86
  node_kind = None
86
87
  if new_node_schema:
87
88
  node_kind = new_node_schema.kind
@@ -0,0 +1,43 @@
1
+ from typing import cast
2
+
3
+ from infrahub.core.constants.infrahubkind import THREADCOMMENT
4
+ from infrahub.core.manager import NodeManager
5
+ from infrahub.core.node import Node
6
+ from infrahub.core.protocols import CoreProposedChange as CoreProposedChangeProtocol
7
+ from infrahub.database import InfrahubDatabase
8
+
9
+
10
+ class CoreProposedChange(Node):
11
+ async def to_graphql(
12
+ self,
13
+ db: InfrahubDatabase,
14
+ fields: dict | None = None,
15
+ related_node_ids: set | None = None,
16
+ filter_sensitive: bool = False,
17
+ permissions: dict | None = None,
18
+ include_properties: bool = True,
19
+ ) -> dict:
20
+ response = await super().to_graphql(
21
+ db,
22
+ fields=fields,
23
+ related_node_ids=related_node_ids,
24
+ filter_sensitive=filter_sensitive,
25
+ permissions=permissions,
26
+ include_properties=include_properties,
27
+ )
28
+
29
+ if fields:
30
+ if "total_comments" in fields:
31
+ total_comments = 0
32
+ proposed_change = cast(CoreProposedChangeProtocol, self)
33
+ change_comments = await proposed_change.comments.get_relationships(db=db)
34
+ total_comments += len(change_comments)
35
+
36
+ threads = await proposed_change.threads.get_peers(db=db)
37
+ thread_comments = await NodeManager.query(
38
+ db=db, schema=THREADCOMMENT, filters={"thread__ids": list(threads.keys())}
39
+ )
40
+ total_comments += len(thread_comments)
41
+ response["total_comments"] = {"value": total_comments}
42
+
43
+ return response
@@ -319,6 +319,7 @@ class CoreCheckDefinition(CoreTaskTarget):
319
319
 
320
320
 
321
321
  class CoreCustomWebhook(CoreWebhook, CoreTaskTarget):
322
+ shared_key: StringOptional
322
323
  transformation: RelationshipManager
323
324
 
324
325
 
@@ -480,7 +481,10 @@ class CoreProposedChange(CoreTaskTarget):
480
481
  source_branch: String
481
482
  destination_branch: String
482
483
  state: Enum
484
+ is_draft: Boolean
485
+ total_comments: IntegerOptional
483
486
  approved_by: RelationshipManager
487
+ rejected_by: RelationshipManager
484
488
  reviewers: RelationshipManager
485
489
  created_by: RelationshipManager
486
490
  comments: RelationshipManager
@@ -554,6 +558,14 @@ class InternalAccountToken(CoreNode):
554
558
  account: RelationshipManager
555
559
 
556
560
 
561
+ class InternalIPPrefixAvailable(BuiltinIPPrefix):
562
+ pass
563
+
564
+
565
+ class InternalIPRangeAvailable(BuiltinIPAddress):
566
+ last_address: IPHost
567
+
568
+
557
569
  class InternalRefreshToken(CoreNode):
558
570
  expiration: DateTime
559
571
  account: RelationshipManager
@@ -5,9 +5,16 @@ from typing import TYPE_CHECKING, Any
5
5
  from infrahub.core.constants import AttributeDBNodeType
6
6
  from infrahub.core.constants.relationship_label import RELATIONSHIP_TO_NODE_LABEL, RELATIONSHIP_TO_VALUE_LABEL
7
7
  from infrahub.core.constants.schema import FlagProperty, NodeProperty
8
+ from infrahub.core.graph.schema import (
9
+ GraphAttributeIPHostNode,
10
+ GraphAttributeIPNetworkNode,
11
+ GraphAttributeValueIndexedNode,
12
+ GraphAttributeValueNode,
13
+ )
8
14
  from infrahub.core.query import Query, QueryNode, QueryRel, QueryType
9
15
  from infrahub.core.timestamp import Timestamp
10
16
  from infrahub.core.utils import build_regex_attrs
17
+ from infrahub.types import is_large_attribute_type
11
18
 
12
19
  if TYPE_CHECKING:
13
20
  from infrahub.core.attribute import BaseAttribute
@@ -56,12 +63,14 @@ class AttributeUpdateValueQuery(AttributeQuery):
56
63
 
57
64
  prop_list = [f"{key}: ${key}" for key in content.keys()]
58
65
 
59
- labels = ["AttributeValue"]
66
+ labels = [GraphAttributeValueNode.get_default_label()]
60
67
  node_type = self.attr.get_db_node_type()
61
- if node_type == AttributeDBNodeType.IPHOST:
62
- labels.append("AttributeIPHost")
63
- elif node_type == AttributeDBNodeType.IPNETWORK:
64
- labels.append("AttributeIPNetwork")
68
+ if AttributeDBNodeType.INDEXED in node_type:
69
+ labels.append(GraphAttributeValueIndexedNode.get_default_label())
70
+ if AttributeDBNodeType.IPHOST in node_type:
71
+ labels.append(GraphAttributeIPHostNode.get_default_label())
72
+ if AttributeDBNodeType.IPNETWORK in node_type:
73
+ labels.append(GraphAttributeIPNetworkNode.get_default_label())
65
74
 
66
75
  query = """
67
76
  MATCH (a:Attribute { uuid: $attr_uuid })
@@ -198,6 +207,9 @@ async def default_attribute_query_filter(
198
207
  support_profiles: bool = False,
199
208
  ) -> tuple[list[QueryElement], dict[str, Any], list[str]]:
200
209
  """Generate Query String Snippet to filter the right node."""
210
+ attribute_value_label = GraphAttributeValueNode.get_default_label()
211
+ if attribute_kind and not is_large_attribute_type(attribute_kind):
212
+ attribute_value_label = GraphAttributeValueIndexedNode.get_default_label()
201
213
 
202
214
  query_filter: list[QueryElement] = []
203
215
  query_params: dict[str, Any] = {}
@@ -226,33 +238,35 @@ async def default_attribute_query_filter(
226
238
  query_filter.append(QueryRel(labels=[RELATIONSHIP_TO_VALUE_LABEL]))
227
239
 
228
240
  if filter_value is None:
229
- query_filter.append(QueryNode(name="av", labels=["AttributeValue"]))
241
+ query_filter.append(QueryNode(name="av", labels=[attribute_value_label]))
230
242
  else:
231
243
  if partial_match:
232
- query_filter.append(QueryNode(name="av", labels=["AttributeValue"]))
244
+ query_filter.append(QueryNode(name="av", labels=[attribute_value_label]))
233
245
  query_where.append(
234
246
  f"toLower(toString(av.{filter_name})) CONTAINS toLower(toString(${param_prefix}_{filter_name}))"
235
247
  )
236
248
  elif attribute_kind and attribute_kind == "List" and not isinstance(filter_value, list):
237
- query_filter.append(QueryNode(name="av", labels=["AttributeValue"]))
249
+ query_filter.append(QueryNode(name="av", labels=[attribute_value_label]))
238
250
  filter_value = build_regex_attrs(values=[filter_value])
239
251
  query_where.append(f"toString(av.{filter_name}) =~ ${param_prefix}_{filter_name}")
240
252
  elif filter_name == "isnull":
241
- query_filter.append(QueryNode(name="av", labels=["AttributeValue"]))
253
+ query_filter.append(QueryNode(name="av", labels=[attribute_value_label]))
242
254
  elif support_profiles:
243
- query_filter.append(QueryNode(name="av", labels=["AttributeValue"]))
255
+ query_filter.append(QueryNode(name="av", labels=[attribute_value_label]))
244
256
  query_where.append(f"(av.{filter_name} = ${param_prefix}_{filter_name} OR av.is_default)")
245
257
  else:
246
258
  query_filter.append(
247
259
  QueryNode(
248
- name="av", labels=["AttributeValue"], params={filter_name: f"${param_prefix}_{filter_name}"}
260
+ name="av",
261
+ labels=[attribute_value_label],
262
+ params={filter_name: f"${param_prefix}_{filter_name}"},
249
263
  )
250
264
  )
251
265
  query_params[f"{param_prefix}_{filter_name}"] = filter_value
252
266
 
253
267
  elif filter_name == "values" and isinstance(filter_value, list):
254
268
  query_filter.extend(
255
- (QueryRel(labels=[RELATIONSHIP_TO_VALUE_LABEL]), QueryNode(name="av", labels=["AttributeValue"]))
269
+ (QueryRel(labels=[RELATIONSHIP_TO_VALUE_LABEL]), QueryNode(name="av", labels=[attribute_value_label]))
256
270
  )
257
271
  if attribute_kind and attribute_kind == "List":
258
272
  query_params[f"{param_prefix}_{filter_name}"] = build_regex_attrs(values=filter_value)
@@ -267,10 +281,14 @@ async def default_attribute_query_filter(
267
281
  query_filter.append(QueryRel(labels=[RELATIONSHIP_TO_VALUE_LABEL]))
268
282
 
269
283
  if filter_value is None:
270
- query_filter.append(QueryNode(name="av", labels=["AttributeValue"]))
284
+ query_filter.append(QueryNode(name="av", labels=[GraphAttributeValueNode.get_default_label()]))
271
285
  else:
272
286
  query_filter.append(
273
- QueryNode(name="av", labels=["AttributeValue"], params={filter_name: f"${param_prefix}_{filter_name}"})
287
+ QueryNode(
288
+ name="av",
289
+ labels=[GraphAttributeValueNode.get_default_label()],
290
+ params={filter_name: f"${param_prefix}_{filter_name}"},
291
+ )
274
292
  )
275
293
  query_params[f"{param_prefix}_{filter_name}"] = filter_value
276
294
 
@@ -100,6 +100,17 @@ class DiffCountChanges(Query):
100
100
  return branch_count_map
101
101
 
102
102
 
103
+ async def get_num_changes_in_time_range_by_branch(
104
+ branch_names: list[str],
105
+ from_time: Timestamp,
106
+ to_time: Timestamp,
107
+ db: InfrahubDatabase,
108
+ ) -> dict[str, int]:
109
+ query = await DiffCountChanges.init(db=db, branch_names=branch_names, diff_from=from_time, diff_to=to_time)
110
+ await query.execute(db=db)
111
+ return query.get_num_changes_by_branch()
112
+
113
+
103
114
  class DiffCalculationQuery(DiffQuery):
104
115
  type = QueryType.READ
105
116
  insert_limit = False
@@ -5,6 +5,7 @@ from dataclasses import dataclass
5
5
  from typing import TYPE_CHECKING, Iterable
6
6
 
7
7
  from infrahub.core.constants import InfrahubKind
8
+ from infrahub.core.graph.schema import GraphAttributeIPHostNode, GraphAttributeIPNetworkNode
8
9
  from infrahub.core.ipam.constants import AllIPTypes, IPAddressType, IPNetworkType
9
10
  from infrahub.core.query import QueryType
10
11
  from infrahub.core.registry import registry
@@ -21,8 +22,8 @@ if TYPE_CHECKING:
21
22
  from infrahub.database import InfrahubDatabase
22
23
 
23
24
 
24
- PREFIX_ATTRIBUTE_LABEL = "AttributeIPNetwork"
25
- ADDRESS_ATTRIBUTE_LABEL = "AttributeIPHost"
25
+ PREFIX_ATTRIBUTE_LABEL = GraphAttributeIPNetworkNode.get_default_label()
26
+ ADDRESS_ATTRIBUTE_LABEL = GraphAttributeIPHostNode.get_default_label()
26
27
 
27
28
 
28
29
  @dataclass
@@ -280,14 +281,13 @@ class IPPrefixUtilization(Query):
280
281
  MATCH path = (
281
282
  (pfx)-[r_1:IS_RELATED]-(rl:Relationship)-[r_2:IS_RELATED]-(child:Node)
282
283
  -[r_attr:HAS_ATTRIBUTE]->(attr:Attribute)
283
- -[r_attr_val:HAS_VALUE]->(av:AttributeValue)
284
+ -[r_attr_val:HAS_VALUE]->(av:{PREFIX_ATTRIBUTE_LABEL}|{ADDRESS_ATTRIBUTE_LABEL})
284
285
  )
285
286
  WHERE %(id_func)s(r_1) = %(id_func)s(r_rel1)
286
287
  AND %(id_func)s(r_2) = %(id_func)s(r_rel2)
287
288
  AND ({rel_filter("r_attr")})
288
289
  AND ({rel_filter("r_attr_val")})
289
290
  AND attr.name IN ["prefix", "address"]
290
- AND any(l in labels(av) WHERE l in ["{PREFIX_ATTRIBUTE_LABEL}", "{ADDRESS_ATTRIBUTE_LABEL}"])
291
291
  WITH
292
292
  path,
293
293
  pfx,
@@ -478,6 +478,7 @@ class IPPrefixReconcileQuery(Query):
478
478
  -[pr2:IS_RELATED {status: "active"}]-(maybe_new_parent:%(ip_prefix_kind)s)
479
479
  -[har:HAS_ATTRIBUTE]->(:Attribute {name: "prefix"})
480
480
  -[hvr:HAS_VALUE]->(av:%(ip_prefix_attribute_kind)s)
481
+ USING INDEX av:%(ip_prefix_attribute_kind)s(binary_address)
481
482
  WHERE all(r IN relationships(parent_path) WHERE (%(branch_filter)s))
482
483
  AND av.version = $ip_version
483
484
  AND av.binary_address IN $possible_prefix_list
@@ -514,15 +515,16 @@ class IPPrefixReconcileQuery(Query):
514
515
  OPTIONAL MATCH child_path = (
515
516
  (ip_namespace)-[r1:IS_RELATED]
516
517
  -(ns_rel:Relationship)-[r2:IS_RELATED]
517
- -(maybe_new_child:Node)-[har:HAS_ATTRIBUTE]
518
+ -(maybe_new_child:%(ip_prefix_kind)s|%(ip_address_kind)s)-[har:HAS_ATTRIBUTE]
518
519
  ->(a:Attribute)-[hvr:HAS_VALUE]
519
- ->(av:AttributeValue)
520
+ ->(av:%(ip_prefix_attribute_kind)s|%(ip_address_attribute_kind)s)
520
521
  )
522
+ USING INDEX av:%(ip_prefix_attribute_kind)s(binary_address)
523
+ USING INDEX av:%(ip_address_attribute_kind)s(binary_address)
521
524
  WHERE $is_prefix // only prefix nodes can have children
522
525
  AND ns_rel.name IN ["ip_namespace__ip_prefix", "ip_namespace__ip_address"]
523
526
  AND any(child_kind IN [$ip_prefix_kind, $ip_address_kind] WHERE child_kind IN labels(maybe_new_child))
524
527
  AND a.name in ["prefix", "address"]
525
- AND any(attr_kind IN [$ip_prefix_attribute_kind, $ip_address_attribute_kind] WHERE attr_kind IN labels(av))
526
528
  AND (ip_node IS NULL OR maybe_new_child.uuid <> ip_node.uuid)
527
529
  AND (
528
530
  ($ip_prefix_kind IN labels(maybe_new_child) AND av.prefixlen > $prefixlen)
@@ -580,6 +582,10 @@ class IPPrefixReconcileQuery(Query):
580
582
  collect(new_child) as new_children
581
583
  """ % {
582
584
  "branch_filter": branch_filter,
585
+ "ip_prefix_kind": InfrahubKind.IPPREFIX,
586
+ "ip_address_kind": InfrahubKind.IPADDRESS,
587
+ "ip_prefix_attribute_kind": PREFIX_ATTRIBUTE_LABEL,
588
+ "ip_address_attribute_kind": ADDRESS_ATTRIBUTE_LABEL,
583
589
  }
584
590
  self.add_to_query(get_new_children_query)
585
591
  self.return_labels = ["ip_node", "current_parent", "current_children", "new_parent", "new_children"]
@@ -140,15 +140,19 @@ class NodeCreateAllQuery(NodeQuery):
140
140
  attributes: list[AttributeCreateData] = []
141
141
  attributes_iphost: list[AttributeCreateData] = []
142
142
  attributes_ipnetwork: list[AttributeCreateData] = []
143
+ attributes_indexed: list[AttributeCreateData] = []
143
144
 
144
145
  for attr_name in self.node._attributes:
145
146
  attr: BaseAttribute = getattr(self.node, attr_name)
146
147
  attr_data = attr.get_create_data()
148
+ node_type = attr.get_db_node_type()
147
149
 
148
- if attr_data.node_type == AttributeDBNodeType.IPHOST:
150
+ if AttributeDBNodeType.IPHOST in node_type:
149
151
  attributes_iphost.append(attr_data)
150
- elif attr_data.node_type == AttributeDBNodeType.IPNETWORK:
152
+ elif AttributeDBNodeType.IPNETWORK in node_type:
151
153
  attributes_ipnetwork.append(attr_data)
154
+ elif AttributeDBNodeType.INDEXED in node_type:
155
+ attributes_indexed.append(attr_data)
152
156
  else:
153
157
  attributes.append(attr_data)
154
158
 
@@ -157,20 +161,7 @@ class NodeCreateAllQuery(NodeQuery):
157
161
  relationships: list[RelationshipCreateData] = []
158
162
  for rel_name in self.node._relationships:
159
163
  rel_manager: RelationshipManager = getattr(self.node, rel_name)
160
- # Fetch all relationship peers through a single database call for performances.
161
- peers = await rel_manager.get_peers(db=db, branch_agnostic=self.branch_agnostic)
162
-
163
164
  for rel in rel_manager._relationships:
164
- try:
165
- rel.set_peer(value=peers[rel.get_peer_id()])
166
- except KeyError:
167
- pass
168
- except ValueError:
169
- # Relationship has not been initialized yet, it means the peer does not exist in db yet
170
- # typically because it will be allocated from a ressource pool. In that case, the peer
171
- # will be fetched using `rel.resolve` later.
172
- pass
173
-
174
165
  rel_create_data = await rel.get_create_data(db=db, at=at)
175
166
  if rel_create_data.peer_branch_level > deepest_branch_level or (
176
167
  deepest_branch_name == GLOBAL_BRANCH_NAME and rel_create_data.peer_branch == registry.default_branch
@@ -180,6 +171,7 @@ class NodeCreateAllQuery(NodeQuery):
180
171
  relationships.append(rel_create_data)
181
172
 
182
173
  self.params["attrs"] = [attr.model_dump() for attr in attributes]
174
+ self.params["attrs_indexed"] = [attr.model_dump() for attr in attributes_indexed]
183
175
  self.params["attrs_iphost"] = [attr.model_dump() for attr in attributes_iphost]
184
176
  self.params["attrs_ipnetwork"] = [attr.model_dump() for attr in attributes_ipnetwork]
185
177
  self.params["rels_bidir"] = [
@@ -222,24 +214,59 @@ class NodeCreateAllQuery(NodeQuery):
222
214
  "binary_address": "attr.content.binary_address",
223
215
  "version": "attr.content.version",
224
216
  "prefixlen": "attr.content.prefixlen",
225
- # "num_addresses": "attr.content.num_addresses",
226
217
  }
227
218
  ipnetwork_prop_list = [f"{key}: {value}" for key, value in ipnetwork_prop.items()]
228
219
 
229
- attrs_query = """
220
+ attrs_nonindexed_query = """
230
221
  WITH distinct n
231
222
  UNWIND $attrs AS attr
223
+ // Try to find a matching vertex
224
+ OPTIONAL MATCH (existing_av:AttributeValue {value: attr.content.value, is_default: attr.content.is_default})
225
+ WHERE NOT existing_av:AttributeValueIndexed
226
+ CALL (attr, existing_av) {
227
+ // If none found, create a new one
228
+ WITH existing_av
229
+ WHERE existing_av IS NULL
230
+ CREATE (:AttributeValue {value: attr.content.value, is_default: attr.content.is_default})
231
+ }
232
+ CALL (attr) {
233
+ MATCH (av:AttributeValue {value: attr.content.value, is_default: attr.content.is_default})
234
+ WHERE NOT av:AttributeValueIndexed
235
+ RETURN av
236
+ LIMIT 1
237
+ }
238
+ CALL (n, attr, av) {
239
+ CREATE (a:Attribute { uuid: attr.uuid, name: attr.name, branch_support: attr.branch_support })
240
+ CREATE (n)-[:HAS_ATTRIBUTE { branch: attr.branch, branch_level: attr.branch_level, status: attr.status, from: $at }]->(a)
241
+ CREATE (a)-[:HAS_VALUE { branch: attr.branch, branch_level: attr.branch_level, status: attr.status, from: $at }]->(av)
242
+ MERGE (ip:Boolean { value: attr.is_protected })
243
+ MERGE (iv:Boolean { value: attr.is_visible })
244
+ WITH a, ip, iv
245
+ LIMIT 1
246
+ CREATE (a)-[:IS_PROTECTED { branch: attr.branch, branch_level: attr.branch_level, status: attr.status, from: $at }]->(ip)
247
+ CREATE (a)-[:IS_VISIBLE { branch: attr.branch, branch_level: attr.branch_level, status: attr.status, from: $at }]->(iv)
248
+ FOREACH ( prop IN attr.source_prop |
249
+ MERGE (peer:Node { uuid: prop.peer_id })
250
+ CREATE (a)-[:HAS_SOURCE { branch: attr.branch, branch_level: attr.branch_level, status: attr.status, from: $at }]->(peer)
251
+ )
252
+ FOREACH ( prop IN attr.owner_prop |
253
+ MERGE (peer:Node { uuid: prop.peer_id })
254
+ CREATE (a)-[:HAS_OWNER { branch: attr.branch, branch_level: attr.branch_level, status: attr.status, from: $at }]->(peer)
255
+ )
256
+ }"""
257
+
258
+ attrs_indexed_query = """
259
+ WITH distinct n
260
+ UNWIND $attrs_indexed AS attr
232
261
  CALL (n, attr) {
233
262
  CREATE (a:Attribute { uuid: attr.uuid, name: attr.name, branch_support: attr.branch_support })
234
263
  CREATE (n)-[:HAS_ATTRIBUTE { branch: attr.branch, branch_level: attr.branch_level, status: attr.status, from: $at }]->(a)
235
- MERGE (av:AttributeValue { value: attr.content.value, is_default: attr.content.is_default })
264
+ MERGE (av:AttributeValue:AttributeValueIndexed { value: attr.content.value, is_default: attr.content.is_default })
236
265
  WITH av, a
237
266
  LIMIT 1
238
267
  CREATE (a)-[:HAS_VALUE { branch: attr.branch, branch_level: attr.branch_level, status: attr.status, from: $at }]->(av)
239
268
  MERGE (ip:Boolean { value: attr.is_protected })
240
269
  MERGE (iv:Boolean { value: attr.is_visible })
241
- WITH a, ip, iv
242
- LIMIT 1
243
270
  CREATE (a)-[:IS_PROTECTED { branch: attr.branch, branch_level: attr.branch_level, status: attr.status, from: $at }]->(ip)
244
271
  CREATE (a)-[:IS_VISIBLE { branch: attr.branch, branch_level: attr.branch_level, status: attr.status, from: $at }]->(iv)
245
272
  FOREACH ( prop IN attr.source_prop |
@@ -258,7 +285,7 @@ class NodeCreateAllQuery(NodeQuery):
258
285
  CALL (n, attr) {
259
286
  CREATE (a:Attribute { uuid: attr.uuid, name: attr.name, branch_support: attr.branch_support })
260
287
  CREATE (n)-[:HAS_ATTRIBUTE { branch: attr.branch, branch_level: attr.branch_level, status: attr.status, from: $at }]->(a)
261
- MERGE (av:AttributeValue:AttributeIPHost { %(iphost_prop)s })
288
+ MERGE (av:AttributeValue:AttributeValueIndexed:AttributeIPHost { %(iphost_prop)s })
262
289
  WITH attr, av, a
263
290
  LIMIT 1
264
291
  CREATE (a)-[:HAS_VALUE { branch: attr.branch, branch_level: attr.branch_level, status: attr.status, from: $at }]->(av)
@@ -285,7 +312,7 @@ class NodeCreateAllQuery(NodeQuery):
285
312
  CALL (n, attr) {
286
313
  CREATE (a:Attribute { uuid: attr.uuid, name: attr.name, branch_support: attr.branch_support })
287
314
  CREATE (n)-[:HAS_ATTRIBUTE { branch: attr.branch, branch_level: attr.branch_level, status: attr.status, from: $at }]->(a)
288
- MERGE (av:AttributeValue:AttributeIPNetwork { %(ipnetwork_prop)s })
315
+ MERGE (av:AttributeValue:AttributeValueIndexed:AttributeIPNetwork { %(ipnetwork_prop)s })
289
316
  WITH attr, av, a
290
317
  LIMIT 1
291
318
  CREATE (a)-[:HAS_VALUE { branch: attr.branch, branch_level: attr.branch_level, status: attr.status, from: $at }]->(av)
@@ -422,7 +449,8 @@ class NodeCreateAllQuery(NodeQuery):
422
449
  MATCH (root:Root)
423
450
  CREATE (n:Node:%(labels)s $node_prop )
424
451
  CREATE (n)-[r:IS_PART_OF $node_branch_prop ]->(root)
425
- {attrs_query if self.params["attrs"] else ""}
452
+ {attrs_nonindexed_query if self.params["attrs"] else ""}
453
+ {attrs_indexed_query if self.params["attrs_indexed"] else ""}
426
454
  {attrs_iphost_query if self.params["attrs_iphost"] else ""}
427
455
  {attrs_ipnetwork_query if self.params["attrs_ipnetwork"] else ""}
428
456
  {rels_bidir_query if self.params["rels_bidir"] else ""}
@@ -140,7 +140,7 @@ class NumberPoolGetAllocated(Query):
140
140
  self.params.update(branch_params)
141
141
 
142
142
  query = """
143
- MATCH (n:%(node)s)-[ha:HAS_ATTRIBUTE]-(a:Attribute {name: $node_attribute})-[hv:HAS_VALUE]-(av:AttributeValue)
143
+ MATCH (n:%(node)s)-[ha:HAS_ATTRIBUTE]-(a:Attribute {name: $node_attribute})-[hv:HAS_VALUE]-(av:AttributeValueIndexed)
144
144
  MATCH (a)-[hs:HAS_SOURCE]-(pool:%(number_pool_kind)s)
145
145
  WHERE
146
146
  pool.uuid = $pool_id
@@ -306,7 +306,7 @@ class NumberPoolGetUsed(Query):
306
306
  self.params["attribute_name"] = self.pool.node_attribute.value
307
307
 
308
308
  query = """
309
- MATCH (pool:%(number_pool)s { uuid: $pool_id })-[res:IS_RESERVED]->(av:AttributeValue)
309
+ MATCH (pool:%(number_pool)s { uuid: $pool_id })-[res:IS_RESERVED]->(av:AttributeValueIndexed)
310
310
  WHERE toInteger(av.value) >= $start_range and toInteger(av.value) <= $end_range
311
311
  CALL (pool, res, av) {
312
312
  MATCH (pool)-[res]->(av)<-[hv:HAS_VALUE]-(attr:Attribute)<-[ha:HAS_ATTRIBUTE]-(n:%(node)s)
@@ -371,7 +371,7 @@ class NumberPoolSetReserved(Query):
371
371
 
372
372
  query = """
373
373
  MATCH (pool:%(number_pool)s { uuid: $pool_id })
374
- MERGE (value:AttributeValue { value: $reserved, is_default: false })
374
+ MERGE (value:AttributeValue:AttributeValueIndexed { value: $reserved, is_default: false })
375
375
  WITH value, pool
376
376
  LIMIT 1
377
377
  CREATE (pool)-[rel:IS_RESERVED $rel_prop]->(value)
@@ -166,11 +166,11 @@ class Relationship(FlagPropertyMixin, NodePropertyMixin):
166
166
  return registry.get_global_branch()
167
167
  return self.branch
168
168
 
169
- def _process_data(self, data: dict | RelationshipPeerData | str) -> None:
169
+ async def _process_data(self, data: dict | RelationshipPeerData | str) -> None:
170
170
  self.data = data
171
171
 
172
172
  if isinstance(data, RelationshipPeerData):
173
- self.set_peer(value=str(data.peer_id))
173
+ await self.set_peer(value=str(data.peer_id))
174
174
 
175
175
  if not self.id and data.rel_node_id:
176
176
  self.id = data.rel_node_id
@@ -187,7 +187,7 @@ class Relationship(FlagPropertyMixin, NodePropertyMixin):
187
187
  elif isinstance(data, dict):
188
188
  for key, value in data.items():
189
189
  if key in ["peer", "id"]:
190
- self.set_peer(value=data.get(key, None))
190
+ await self.set_peer(value=data.get(key, None))
191
191
  elif key == "hfid" and self.peer_id is None:
192
192
  self.peer_hfid = value
193
193
  elif key.startswith(PREFIX_PROPERTY) and key.replace(PREFIX_PROPERTY, "") in self._flag_properties:
@@ -198,7 +198,7 @@ class Relationship(FlagPropertyMixin, NodePropertyMixin):
198
198
  self.from_pool = value
199
199
 
200
200
  else:
201
- self.set_peer(value=data)
201
+ await self.set_peer(value=data)
202
202
 
203
203
  async def new(
204
204
  self,
@@ -206,11 +206,11 @@ class Relationship(FlagPropertyMixin, NodePropertyMixin):
206
206
  data: dict | RelationshipPeerData | Any = None,
207
207
  **kwargs: Any, # noqa: ARG002
208
208
  ) -> Relationship:
209
- self._process_data(data=data)
209
+ await self._process_data(data=data)
210
210
 
211
211
  return self
212
212
 
213
- def load(
213
+ async def load(
214
214
  self,
215
215
  db: InfrahubDatabase, # noqa: ARG002
216
216
  id: UUID | None = None,
@@ -223,7 +223,7 @@ class Relationship(FlagPropertyMixin, NodePropertyMixin):
223
223
  self.id = id or self.id
224
224
  self.db_id = db_id or self.db_id
225
225
 
226
- self._process_data(data=data)
226
+ await self._process_data(data=data)
227
227
 
228
228
  if updated_at and hash(self) != hash_before:
229
229
  self.updated_at = Timestamp(updated_at)
@@ -252,7 +252,7 @@ class Relationship(FlagPropertyMixin, NodePropertyMixin):
252
252
  self._node_id = self._node.id
253
253
  return node
254
254
 
255
- def set_peer(self, value: str | Node) -> None:
255
+ async def set_peer(self, value: str | Node) -> None:
256
256
  if isinstance(value, str):
257
257
  self.peer_id = value
258
258
  else:
@@ -433,7 +433,7 @@ class Relationship(FlagPropertyMixin, NodePropertyMixin):
433
433
  db=db, id=self.peer_id, branch=self.branch, kind=self.schema.peer, fields={"display_label": None}
434
434
  )
435
435
  if peer:
436
- self.set_peer(value=peer)
436
+ await self.set_peer(value=peer)
437
437
 
438
438
  if not self.peer_id and self.peer_hfid:
439
439
  peer_schema = db.schema.get(name=self.schema.peer, branch=self.branch)
@@ -450,7 +450,7 @@ class Relationship(FlagPropertyMixin, NodePropertyMixin):
450
450
  fields={"display_label": None},
451
451
  raise_on_error=True,
452
452
  )
453
- self.set_peer(value=peer)
453
+ await self.set_peer(value=peer)
454
454
 
455
455
  if not self.peer_id and self.from_pool and "id" in self.from_pool:
456
456
  pool_id = str(self.from_pool.get("id"))
@@ -473,7 +473,7 @@ class Relationship(FlagPropertyMixin, NodePropertyMixin):
473
473
  data_from_pool["identifier"] = f"hfid={hfid_str} rel={self.name}"
474
474
 
475
475
  assigned_peer: Node = await pool.get_resource(db=db, branch=self.branch, at=at, **data_from_pool) # type: ignore[attr-defined]
476
- self.set_peer(value=assigned_peer)
476
+ await self.set_peer(value=assigned_peer)
477
477
  self.set_source(value=pool.id)
478
478
 
479
479
  async def save(self, db: InfrahubDatabase, at: Timestamp | None = None) -> Self:
@@ -962,7 +962,7 @@ class RelationshipManager:
962
962
 
963
963
  for peer_id in details.peer_ids_present_database_only:
964
964
  self._relationships.append(
965
- Relationship(
965
+ await Relationship(
966
966
  schema=self.schema,
967
967
  branch=self.branch,
968
968
  at=at or self.at,
@@ -1050,7 +1050,7 @@ class RelationshipManager:
1050
1050
  if isinstance(item, dict) and item.get("id", None) in previous_relationships:
1051
1051
  rel = previous_relationships[item["id"]]
1052
1052
  hash_before = hash(rel)
1053
- rel.load(data=item, db=db)
1053
+ await rel.load(data=item, db=db)
1054
1054
  if hash(rel) != hash_before:
1055
1055
  changed = True
1056
1056
  self._relationships.append(rel)
@@ -55,6 +55,14 @@ class BaseNodeSchema(GeneratedBaseNodeSchema):
55
55
  def is_profile_schema(self) -> bool:
56
56
  return False
57
57
 
58
+ @property
59
+ def is_ip_prefix(self) -> bool:
60
+ return False
61
+
62
+ @property
63
+ def is_ip_address(self) -> bool:
64
+ return False
65
+
58
66
  @property
59
67
  def kind(self) -> str:
60
68
  if self.namespace == "Attribute":
@@ -35,7 +35,14 @@ from .group import (
35
35
  core_repository_group,
36
36
  core_standard_group,
37
37
  )
38
- from .ipam import builtin_ip_address, builtin_ip_prefix, builtin_ipam, core_ipam_namespace
38
+ from .ipam import (
39
+ builtin_ip_address,
40
+ builtin_ip_prefix,
41
+ builtin_ipam,
42
+ core_ipam_namespace,
43
+ internal_ipam_ip_prefix_available,
44
+ internal_ipam_ip_range_available,
45
+ )
39
46
  from .lineage import lineage_owner, lineage_source
40
47
  from .menu import generic_menu_item, menu_item
41
48
  from .permission import (
@@ -172,6 +179,8 @@ core_models_mixed: dict[str, list] = {
172
179
  core_object_permission,
173
180
  core_account_role,
174
181
  core_account_group,
182
+ internal_ipam_ip_prefix_available,
183
+ internal_ipam_ip_range_available,
175
184
  ],
176
185
  }
177
186