infrahub-server 1.3.5__py3-none-any.whl → 1.4.0b0__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 (158) 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 -16
  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/merge.py +8 -10
  24. infrahub/core/migrations/graph/__init__.py +2 -0
  25. infrahub/core/migrations/graph/m035_drop_attr_value_index.py +45 -0
  26. infrahub/core/migrations/query/attribute_add.py +27 -2
  27. infrahub/core/migrations/schema/tasks.py +6 -5
  28. infrahub/core/node/proposed_change.py +43 -0
  29. infrahub/core/protocols.py +12 -0
  30. infrahub/core/query/attribute.py +32 -14
  31. infrahub/core/query/diff.py +11 -0
  32. infrahub/core/query/ipam.py +13 -7
  33. infrahub/core/query/node.py +51 -10
  34. infrahub/core/query/resource_manager.py +3 -3
  35. infrahub/core/schema/basenode_schema.py +8 -0
  36. infrahub/core/schema/definitions/core/__init__.py +10 -1
  37. infrahub/core/schema/definitions/core/ipam.py +28 -2
  38. infrahub/core/schema/definitions/core/propose_change.py +15 -0
  39. infrahub/core/schema/definitions/core/webhook.py +3 -0
  40. infrahub/core/schema/generic_schema.py +10 -0
  41. infrahub/core/schema/manager.py +10 -1
  42. infrahub/core/schema/node_schema.py +22 -17
  43. infrahub/core/schema/profile_schema.py +8 -0
  44. infrahub/core/schema/schema_branch.py +9 -5
  45. infrahub/core/schema/template_schema.py +8 -0
  46. infrahub/core/validators/checks_runner.py +5 -5
  47. infrahub/core/validators/tasks.py +6 -7
  48. infrahub/core/validators/uniqueness/checker.py +4 -2
  49. infrahub/core/validators/uniqueness/model.py +1 -0
  50. infrahub/core/validators/uniqueness/query.py +57 -7
  51. infrahub/database/__init__.py +2 -1
  52. infrahub/events/__init__.py +18 -0
  53. infrahub/events/constants.py +7 -0
  54. infrahub/events/generator.py +29 -2
  55. infrahub/events/proposed_change_action.py +181 -0
  56. infrahub/generators/tasks.py +24 -20
  57. infrahub/git/base.py +4 -7
  58. infrahub/git/integrator.py +21 -12
  59. infrahub/git/repository.py +15 -30
  60. infrahub/git/tasks.py +121 -106
  61. infrahub/graphql/field_extractor.py +69 -0
  62. infrahub/graphql/manager.py +15 -11
  63. infrahub/graphql/mutations/account.py +2 -2
  64. infrahub/graphql/mutations/action.py +8 -2
  65. infrahub/graphql/mutations/artifact_definition.py +4 -1
  66. infrahub/graphql/mutations/branch.py +10 -5
  67. infrahub/graphql/mutations/graphql_query.py +2 -1
  68. infrahub/graphql/mutations/main.py +14 -8
  69. infrahub/graphql/mutations/menu.py +2 -1
  70. infrahub/graphql/mutations/proposed_change.py +225 -8
  71. infrahub/graphql/mutations/relationship.py +5 -0
  72. infrahub/graphql/mutations/repository.py +2 -1
  73. infrahub/graphql/mutations/tasks.py +7 -9
  74. infrahub/graphql/mutations/webhook.py +4 -1
  75. infrahub/graphql/parser.py +15 -6
  76. infrahub/graphql/queries/__init__.py +10 -1
  77. infrahub/graphql/queries/account.py +3 -3
  78. infrahub/graphql/queries/branch.py +2 -2
  79. infrahub/graphql/queries/diff/tree.py +3 -3
  80. infrahub/graphql/queries/event.py +13 -3
  81. infrahub/graphql/queries/ipam.py +23 -1
  82. infrahub/graphql/queries/proposed_change.py +84 -0
  83. infrahub/graphql/queries/relationship.py +2 -2
  84. infrahub/graphql/queries/resource_manager.py +3 -3
  85. infrahub/graphql/queries/search.py +3 -2
  86. infrahub/graphql/queries/status.py +3 -2
  87. infrahub/graphql/queries/task.py +2 -2
  88. infrahub/graphql/resolvers/ipam.py +440 -0
  89. infrahub/graphql/resolvers/many_relationship.py +4 -3
  90. infrahub/graphql/resolvers/resolver.py +5 -5
  91. infrahub/graphql/resolvers/single_relationship.py +3 -2
  92. infrahub/graphql/schema.py +25 -5
  93. infrahub/graphql/types/__init__.py +2 -2
  94. infrahub/graphql/types/attribute.py +3 -3
  95. infrahub/graphql/types/event.py +60 -0
  96. infrahub/groups/tasks.py +6 -6
  97. infrahub/lock.py +3 -2
  98. infrahub/menu/generator.py +8 -0
  99. infrahub/message_bus/operations/__init__.py +9 -12
  100. infrahub/message_bus/operations/git/file.py +6 -5
  101. infrahub/message_bus/operations/git/repository.py +12 -20
  102. infrahub/message_bus/operations/refresh/registry.py +15 -9
  103. infrahub/message_bus/operations/send/echo.py +7 -4
  104. infrahub/message_bus/types.py +1 -0
  105. infrahub/permissions/globals.py +1 -4
  106. infrahub/permissions/manager.py +8 -5
  107. infrahub/pools/prefix.py +7 -5
  108. infrahub/prefect_server/app.py +31 -0
  109. infrahub/prefect_server/bootstrap.py +18 -0
  110. infrahub/proposed_change/action_checker.py +206 -0
  111. infrahub/proposed_change/approval_revoker.py +40 -0
  112. infrahub/proposed_change/branch_diff.py +3 -1
  113. infrahub/proposed_change/checker.py +45 -0
  114. infrahub/proposed_change/constants.py +32 -2
  115. infrahub/proposed_change/tasks.py +182 -150
  116. infrahub/py.typed +0 -0
  117. infrahub/server.py +29 -17
  118. infrahub/services/__init__.py +13 -28
  119. infrahub/services/adapters/cache/__init__.py +4 -0
  120. infrahub/services/adapters/cache/nats.py +2 -0
  121. infrahub/services/adapters/cache/redis.py +3 -0
  122. infrahub/services/adapters/message_bus/__init__.py +0 -2
  123. infrahub/services/adapters/message_bus/local.py +1 -2
  124. infrahub/services/adapters/message_bus/nats.py +6 -8
  125. infrahub/services/adapters/message_bus/rabbitmq.py +7 -9
  126. infrahub/services/adapters/workflow/__init__.py +1 -0
  127. infrahub/services/adapters/workflow/local.py +1 -8
  128. infrahub/services/component.py +2 -1
  129. infrahub/task_manager/event.py +52 -0
  130. infrahub/task_manager/models.py +9 -0
  131. infrahub/tasks/artifact.py +6 -7
  132. infrahub/tasks/check.py +4 -7
  133. infrahub/telemetry/tasks.py +15 -18
  134. infrahub/transformations/tasks.py +10 -6
  135. infrahub/trigger/tasks.py +4 -3
  136. infrahub/types.py +4 -0
  137. infrahub/validators/events.py +7 -7
  138. infrahub/validators/tasks.py +6 -7
  139. infrahub/webhook/models.py +45 -45
  140. infrahub/webhook/tasks.py +25 -24
  141. infrahub/workers/dependencies.py +143 -0
  142. infrahub/workers/infrahub_async.py +19 -43
  143. infrahub/workflows/catalogue.py +16 -2
  144. infrahub/workflows/initialization.py +5 -4
  145. infrahub/workflows/models.py +2 -0
  146. infrahub_sdk/client.py +6 -6
  147. infrahub_sdk/ctl/repository.py +51 -0
  148. infrahub_sdk/ctl/schema.py +9 -9
  149. infrahub_sdk/protocols.py +40 -6
  150. {infrahub_server-1.3.5.dist-info → infrahub_server-1.4.0b0.dist-info}/METADATA +5 -4
  151. {infrahub_server-1.3.5.dist-info → infrahub_server-1.4.0b0.dist-info}/RECORD +158 -144
  152. infrahub_testcontainers/container.py +17 -0
  153. infrahub_testcontainers/docker-compose-cluster.test.yml +56 -1
  154. infrahub_testcontainers/docker-compose.test.yml +56 -1
  155. infrahub_testcontainers/helpers.py +4 -1
  156. {infrahub_server-1.3.5.dist-info → infrahub_server-1.4.0b0.dist-info}/LICENSE.txt +0 -0
  157. {infrahub_server-1.3.5.dist-info → infrahub_server-1.4.0b0.dist-info}/WHEEL +0 -0
  158. {infrahub_server-1.3.5.dist-info → infrahub_server-1.4.0b0.dist-info}/entry_points.txt +0 -0
@@ -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
 
@@ -167,6 +171,7 @@ class NodeCreateAllQuery(NodeQuery):
167
171
  relationships.append(rel_create_data)
168
172
 
169
173
  self.params["attrs"] = [attr.model_dump() for attr in attributes]
174
+ self.params["attrs_indexed"] = [attr.model_dump() for attr in attributes_indexed]
170
175
  self.params["attrs_iphost"] = [attr.model_dump() for attr in attributes_iphost]
171
176
  self.params["attrs_ipnetwork"] = [attr.model_dump() for attr in attributes_ipnetwork]
172
177
  self.params["rels_bidir"] = [
@@ -209,24 +214,59 @@ class NodeCreateAllQuery(NodeQuery):
209
214
  "binary_address": "attr.content.binary_address",
210
215
  "version": "attr.content.version",
211
216
  "prefixlen": "attr.content.prefixlen",
212
- # "num_addresses": "attr.content.num_addresses",
213
217
  }
214
218
  ipnetwork_prop_list = [f"{key}: {value}" for key, value in ipnetwork_prop.items()]
215
219
 
216
- attrs_query = """
220
+ attrs_nonindexed_query = """
217
221
  WITH distinct n
218
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
219
261
  CALL (n, attr) {
220
262
  CREATE (a:Attribute { uuid: attr.uuid, name: attr.name, branch_support: attr.branch_support })
221
263
  CREATE (n)-[:HAS_ATTRIBUTE { branch: attr.branch, branch_level: attr.branch_level, status: attr.status, from: $at }]->(a)
222
- 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 })
223
265
  WITH av, a
224
266
  LIMIT 1
225
267
  CREATE (a)-[:HAS_VALUE { branch: attr.branch, branch_level: attr.branch_level, status: attr.status, from: $at }]->(av)
226
268
  MERGE (ip:Boolean { value: attr.is_protected })
227
269
  MERGE (iv:Boolean { value: attr.is_visible })
228
- WITH a, ip, iv
229
- LIMIT 1
230
270
  CREATE (a)-[:IS_PROTECTED { branch: attr.branch, branch_level: attr.branch_level, status: attr.status, from: $at }]->(ip)
231
271
  CREATE (a)-[:IS_VISIBLE { branch: attr.branch, branch_level: attr.branch_level, status: attr.status, from: $at }]->(iv)
232
272
  FOREACH ( prop IN attr.source_prop |
@@ -245,7 +285,7 @@ class NodeCreateAllQuery(NodeQuery):
245
285
  CALL (n, attr) {
246
286
  CREATE (a:Attribute { uuid: attr.uuid, name: attr.name, branch_support: attr.branch_support })
247
287
  CREATE (n)-[:HAS_ATTRIBUTE { branch: attr.branch, branch_level: attr.branch_level, status: attr.status, from: $at }]->(a)
248
- MERGE (av:AttributeValue:AttributeIPHost { %(iphost_prop)s })
288
+ MERGE (av:AttributeValue:AttributeValueIndexed:AttributeIPHost { %(iphost_prop)s })
249
289
  WITH attr, av, a
250
290
  LIMIT 1
251
291
  CREATE (a)-[:HAS_VALUE { branch: attr.branch, branch_level: attr.branch_level, status: attr.status, from: $at }]->(av)
@@ -272,7 +312,7 @@ class NodeCreateAllQuery(NodeQuery):
272
312
  CALL (n, attr) {
273
313
  CREATE (a:Attribute { uuid: attr.uuid, name: attr.name, branch_support: attr.branch_support })
274
314
  CREATE (n)-[:HAS_ATTRIBUTE { branch: attr.branch, branch_level: attr.branch_level, status: attr.status, from: $at }]->(a)
275
- MERGE (av:AttributeValue:AttributeIPNetwork { %(ipnetwork_prop)s })
315
+ MERGE (av:AttributeValue:AttributeValueIndexed:AttributeIPNetwork { %(ipnetwork_prop)s })
276
316
  WITH attr, av, a
277
317
  LIMIT 1
278
318
  CREATE (a)-[:HAS_VALUE { branch: attr.branch, branch_level: attr.branch_level, status: attr.status, from: $at }]->(av)
@@ -409,7 +449,8 @@ class NodeCreateAllQuery(NodeQuery):
409
449
  MATCH (root:Root)
410
450
  CREATE (n:Node:%(labels)s $node_prop )
411
451
  CREATE (n)-[r:IS_PART_OF $node_branch_prop ]->(root)
412
- {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 ""}
413
454
  {attrs_iphost_query if self.params["attrs_iphost"] else ""}
414
455
  {attrs_ipnetwork_query if self.params["attrs_ipnetwork"] else ""}
415
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)
@@ -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
 
@@ -59,7 +59,7 @@ builtin_ip_prefix = GenericSchema(
59
59
  name="IPPrefix",
60
60
  label="IP Prefix",
61
61
  namespace="Builtin",
62
- description="IPv6 or IPv4 prefix also referred as network",
62
+ description="IPv4 or IPv6 prefix also referred as network",
63
63
  include_in_menu=False,
64
64
  default_filter="prefix__value",
65
65
  order_by=["prefix__version", "prefix__binary_address", "prefix__prefixlen"],
@@ -142,7 +142,7 @@ builtin_ip_address = GenericSchema(
142
142
  name="IPAddress",
143
143
  label="IP Address",
144
144
  namespace="Builtin",
145
- description="IPv6 or IPv4 address",
145
+ description="IPv4 or IPv6 address",
146
146
  include_in_menu=False,
147
147
  default_filter="address__value",
148
148
  order_by=["address__version", "address__binary_address"],
@@ -176,6 +176,32 @@ builtin_ip_address = GenericSchema(
176
176
  ],
177
177
  )
178
178
 
179
+ internal_ipam_ip_range_available = NodeSchema(
180
+ name="IPRangeAvailable",
181
+ label="Available IP Range",
182
+ namespace="Internal",
183
+ description="Range of IPv4 or IPv6 addresses which has not been allocated yet",
184
+ include_in_menu=False,
185
+ display_labels=["address__value", "last_address__value"],
186
+ branch=BranchSupportType.AWARE,
187
+ inherit_from=[InfrahubKind.IPADDRESS],
188
+ generate_profile=False,
189
+ attributes=[Attr(name="last_address", kind="IPHost", branch=BranchSupportType.AWARE, order_weight=2000)],
190
+ )
191
+
192
+ internal_ipam_ip_prefix_available = NodeSchema(
193
+ name="IPPrefixAvailable",
194
+ label="Available IP Prefix",
195
+ namespace="Internal",
196
+ description="IPv4 or IPv6 prefix also referred as network which has not been allocated yet",
197
+ include_in_menu=False,
198
+ display_labels=["prefix__value"],
199
+ branch=BranchSupportType.AWARE,
200
+ inherit_from=[InfrahubKind.IPPREFIX],
201
+ generate_profile=False,
202
+ )
203
+
204
+
179
205
  core_ipam_namespace = NodeSchema(
180
206
  name="Namespace",
181
207
  namespace="Ipam",
@@ -38,6 +38,9 @@ core_proposed_change = NodeSchema(
38
38
  default_value=ProposedChangeState.OPEN.value,
39
39
  optional=True,
40
40
  ),
41
+ Attr(name="is_draft", kind="Boolean", optional=False, default_value=False),
42
+ # Ideally we should support some "runtime-attribute" that could not even be stored in the database.
43
+ Attr(name="total_comments", kind="Number", optional=True, read_only=True),
41
44
  ],
42
45
  relationships=[
43
46
  Rel(
@@ -48,6 +51,17 @@ core_proposed_change = NodeSchema(
48
51
  kind=RelKind.ATTRIBUTE,
49
52
  branch=BranchSupportType.AGNOSTIC,
50
53
  identifier="coreaccount__proposedchange_approved_by",
54
+ read_only=True,
55
+ ),
56
+ Rel(
57
+ name="rejected_by",
58
+ peer=InfrahubKind.GENERICACCOUNT,
59
+ optional=True,
60
+ cardinality=Cardinality.MANY,
61
+ kind=RelKind.ATTRIBUTE,
62
+ branch=BranchSupportType.AGNOSTIC,
63
+ identifier="coreaccount__proposedchange_rejected_by",
64
+ read_only=True,
51
65
  ),
52
66
  Rel(
53
67
  name="reviewers",
@@ -66,6 +80,7 @@ core_proposed_change = NodeSchema(
66
80
  kind=RelKind.ATTRIBUTE,
67
81
  branch=BranchSupportType.AGNOSTIC,
68
82
  identifier="coreaccount__proposedchange_created_by",
83
+ read_only=True,
69
84
  ),
70
85
  Rel(
71
86
  name="comments",
@@ -120,6 +120,9 @@ core_custom_webhook = NodeSchema(
120
120
  branch=BranchSupportType.AGNOSTIC,
121
121
  generate_profile=False,
122
122
  inherit_from=[InfrahubKind.WEBHOOK, InfrahubKind.TASKTARGET],
123
+ attributes=[
124
+ Attr(name="shared_key", kind="Password", unique=False, optional=True, order_weight=4000),
125
+ ],
123
126
  relationships=[
124
127
  Rel(
125
128
  name="transformation",
@@ -2,6 +2,8 @@ from __future__ import annotations
2
2
 
3
3
  from typing import TYPE_CHECKING
4
4
 
5
+ from infrahub.core.constants import InfrahubKind
6
+
5
7
  from .generated.genericnode_schema import GeneratedGenericSchema
6
8
 
7
9
  if TYPE_CHECKING:
@@ -28,6 +30,14 @@ class GenericSchema(GeneratedGenericSchema):
28
30
  def is_template_schema(self) -> bool:
29
31
  return False
30
32
 
33
+ @property
34
+ def is_ip_prefix(self) -> bool:
35
+ return self.kind == InfrahubKind.IPPREFIX
36
+
37
+ @property
38
+ def is_ip_address(self) -> bool:
39
+ return self.kind == InfrahubKind.IPADDRESS
40
+
31
41
  def get_hierarchy_schema(self, db: InfrahubDatabase, branch: Branch | str | None = None) -> GenericSchema: # noqa: ARG002
32
42
  if self.hierarchical:
33
43
  return self
@@ -93,6 +93,15 @@ class SchemaManager(NodeManager):
93
93
 
94
94
  raise ValueError("The selected node is not of type NodeSchema")
95
95
 
96
+ def get_generic_schema(
97
+ self, name: str, branch: Branch | str | None = None, duplicate: bool = True
98
+ ) -> GenericSchema:
99
+ schema = self.get(name=name, branch=branch, duplicate=duplicate)
100
+ if isinstance(schema, GenericSchema):
101
+ return schema
102
+
103
+ raise ValueError("The selected node is not of type GenericSchema")
104
+
96
105
  def get_profile_schema(
97
106
  self, name: str, branch: Branch | str | None = None, duplicate: bool = True
98
107
  ) -> ProfileSchema:
@@ -122,7 +131,7 @@ class SchemaManager(NodeManager):
122
131
 
123
132
  return self._branches[branch_name].get_all(duplicate=duplicate)
124
133
 
125
- async def get_full_safe(self, branch: Branch | str | None = None) -> dict[str, NodeSchema | GenericSchema]:
134
+ async def get_full_safe(self, branch: Branch | str | None = None) -> dict[str, MainSchemaTypes]:
126
135
  await lock.registry.local_schema_wait()
127
136
 
128
137
  return self.get_full(branch=branch)
@@ -2,7 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  from typing import TYPE_CHECKING
4
4
 
5
- from infrahub.core.constants import AllowOverrideType, InfrahubKind
5
+ from infrahub.core.constants import AllowOverrideType, InfrahubKind, RelationshipKind
6
6
 
7
7
  from .generated.node_schema import GeneratedNodeSchema
8
8
  from .generic_schema import GenericSchema
@@ -29,6 +29,16 @@ class NodeSchema(GeneratedNodeSchema):
29
29
  def is_template_schema(self) -> bool:
30
30
  return False
31
31
 
32
+ @property
33
+ def is_ip_prefix(self) -> bool:
34
+ """Return whether a node is a derivative of built-in IP prefixes."""
35
+ return InfrahubKind.IPPREFIX in self.inherit_from
36
+
37
+ @property
38
+ def is_ip_address(self) -> bool:
39
+ """Return whether a node is a derivative of built-in IP addreses."""
40
+ return InfrahubKind.IPADDRESS in self.inherit_from
41
+
32
42
  def validate_inheritance(self, interface: GenericSchema) -> None:
33
43
  """Perform checks specific to inheritance from Generics.
34
44
 
@@ -60,14 +70,17 @@ class NodeSchema(GeneratedNodeSchema):
60
70
  )
61
71
 
62
72
  for relationship in self.relationships:
63
- if (
64
- relationship.name in interface.relationship_names
65
- and not relationship.inherited
66
- and interface.get_relationship(relationship.name).allow_override == AllowOverrideType.NONE
67
- ):
68
- raise ValueError(
69
- f"{self.kind}'s relationship {relationship.name} inherited from {interface.kind} cannot be overriden"
70
- )
73
+ if relationship.name in interface.relationship_names and not relationship.inherited:
74
+ interface_relationship = interface.get_relationship(relationship.name)
75
+ if interface_relationship.allow_override == AllowOverrideType.NONE:
76
+ raise ValueError(
77
+ f"{self.kind}'s relationship {relationship.name} inherited from {interface.kind} cannot be overriden"
78
+ )
79
+ if relationship.kind != RelationshipKind.HIERARCHY and relationship.peer != interface_relationship.peer:
80
+ raise ValueError(
81
+ f"{self.kind}'s relationship {relationship.name} inherited from {interface.kind} must have the same peer "
82
+ f"({interface_relationship.peer} != {relationship.peer})"
83
+ )
71
84
 
72
85
  def inherit_from_interface(self, interface: GenericSchema) -> None:
73
86
  existing_inherited_attributes: dict[str, int] = {
@@ -137,11 +150,3 @@ class NodeSchema(GeneratedNodeSchema):
137
150
  if self.namespace not in ["Schema", "Internal"] and InfrahubKind.GENERICGROUP not in self.inherit_from:
138
151
  labels.append(InfrahubKind.NODE)
139
152
  return labels
140
-
141
- def is_ip_prefix(self) -> bool:
142
- """Return whether a node is a derivative of built-in IP prefixes."""
143
- return InfrahubKind.IPPREFIX in self.inherit_from
144
-
145
- def is_ip_address(self) -> bool:
146
- """Return whether a node is a derivative of built-in IP addreses."""
147
- return InfrahubKind.IPADDRESS in self.inherit_from
@@ -28,6 +28,14 @@ class ProfileSchema(BaseNodeSchema):
28
28
  def is_template_schema(self) -> bool:
29
29
  return False
30
30
 
31
+ @property
32
+ def is_ip_prefix(self) -> bool:
33
+ return False
34
+
35
+ @property
36
+ def is_ip_address(self) -> bool:
37
+ return False
38
+
31
39
  def get_labels(self) -> list[str]:
32
40
  """Return the labels for this object, composed of the kind
33
41
  and the list of Generic this object is inheriting from."""