infrahub-server 1.5.0b0__py3-none-any.whl → 1.5.0b2__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 (183) hide show
  1. infrahub/actions/tasks.py +8 -0
  2. infrahub/api/diff/diff.py +1 -1
  3. infrahub/api/internal.py +2 -0
  4. infrahub/api/oauth2.py +13 -19
  5. infrahub/api/oidc.py +15 -21
  6. infrahub/api/schema.py +24 -3
  7. infrahub/artifacts/models.py +2 -1
  8. infrahub/auth.py +137 -3
  9. infrahub/cli/__init__.py +2 -0
  10. infrahub/cli/db.py +103 -98
  11. infrahub/cli/db_commands/clean_duplicate_schema_fields.py +212 -0
  12. infrahub/cli/dev.py +118 -0
  13. infrahub/cli/tasks.py +46 -0
  14. infrahub/cli/upgrade.py +30 -3
  15. infrahub/computed_attribute/tasks.py +20 -8
  16. infrahub/core/attribute.py +13 -5
  17. infrahub/core/branch/enums.py +1 -1
  18. infrahub/core/branch/models.py +7 -3
  19. infrahub/core/branch/tasks.py +70 -8
  20. infrahub/core/changelog/models.py +4 -12
  21. infrahub/core/constants/__init__.py +3 -0
  22. infrahub/core/constants/infrahubkind.py +1 -0
  23. infrahub/core/diff/model/path.py +4 -0
  24. infrahub/core/diff/payload_builder.py +1 -1
  25. infrahub/core/diff/query/artifact.py +1 -0
  26. infrahub/core/diff/query/field_summary.py +1 -0
  27. infrahub/core/graph/__init__.py +1 -1
  28. infrahub/core/initialization.py +5 -2
  29. infrahub/core/ipam/utilization.py +1 -1
  30. infrahub/core/manager.py +6 -3
  31. infrahub/core/migrations/__init__.py +3 -0
  32. infrahub/core/migrations/exceptions.py +4 -0
  33. infrahub/core/migrations/graph/__init__.py +12 -11
  34. infrahub/core/migrations/graph/load_schema_branch.py +21 -0
  35. infrahub/core/migrations/graph/m013_convert_git_password_credential.py +1 -1
  36. infrahub/core/migrations/graph/m040_duplicated_attributes.py +81 -0
  37. infrahub/core/migrations/graph/m041_profile_attrs_in_db.py +145 -0
  38. infrahub/core/migrations/graph/m042_create_hfid_display_label_in_db.py +164 -0
  39. infrahub/core/migrations/graph/m043_backfill_hfid_display_label_in_db.py +866 -0
  40. infrahub/core/migrations/query/__init__.py +7 -8
  41. infrahub/core/migrations/query/attribute_add.py +8 -6
  42. infrahub/core/migrations/query/attribute_remove.py +134 -0
  43. infrahub/core/migrations/runner.py +54 -0
  44. infrahub/core/migrations/schema/attribute_kind_update.py +9 -3
  45. infrahub/core/migrations/schema/attribute_supports_profile.py +90 -0
  46. infrahub/core/migrations/schema/node_attribute_add.py +35 -4
  47. infrahub/core/migrations/schema/node_attribute_remove.py +13 -109
  48. infrahub/core/migrations/schema/node_kind_update.py +2 -1
  49. infrahub/core/migrations/schema/node_remove.py +2 -1
  50. infrahub/core/migrations/schema/placeholder_dummy.py +3 -2
  51. infrahub/core/migrations/shared.py +52 -19
  52. infrahub/core/node/__init__.py +158 -51
  53. infrahub/core/node/constraints/attribute_uniqueness.py +3 -1
  54. infrahub/core/node/create.py +46 -63
  55. infrahub/core/node/lock_utils.py +70 -44
  56. infrahub/core/node/node_property_attribute.py +230 -0
  57. infrahub/core/node/resource_manager/ip_address_pool.py +2 -1
  58. infrahub/core/node/resource_manager/ip_prefix_pool.py +2 -1
  59. infrahub/core/node/resource_manager/number_pool.py +2 -1
  60. infrahub/core/node/standard.py +1 -1
  61. infrahub/core/protocols.py +7 -1
  62. infrahub/core/query/attribute.py +55 -0
  63. infrahub/core/query/ipam.py +1 -0
  64. infrahub/core/query/node.py +23 -4
  65. infrahub/core/query/relationship.py +1 -0
  66. infrahub/core/registry.py +2 -2
  67. infrahub/core/relationship/constraints/count.py +1 -1
  68. infrahub/core/relationship/model.py +1 -1
  69. infrahub/core/schema/__init__.py +56 -0
  70. infrahub/core/schema/attribute_schema.py +4 -0
  71. infrahub/core/schema/basenode_schema.py +42 -2
  72. infrahub/core/schema/definitions/core/__init__.py +2 -0
  73. infrahub/core/schema/definitions/core/generator.py +2 -0
  74. infrahub/core/schema/definitions/core/group.py +16 -2
  75. infrahub/core/schema/definitions/internal.py +16 -3
  76. infrahub/core/schema/generated/attribute_schema.py +2 -2
  77. infrahub/core/schema/generated/base_node_schema.py +6 -1
  78. infrahub/core/schema/manager.py +22 -1
  79. infrahub/core/schema/node_schema.py +5 -2
  80. infrahub/core/schema/schema_branch.py +300 -8
  81. infrahub/core/schema/schema_branch_display.py +123 -0
  82. infrahub/core/schema/schema_branch_hfid.py +114 -0
  83. infrahub/core/validators/aggregated_checker.py +1 -1
  84. infrahub/core/validators/determiner.py +12 -1
  85. infrahub/core/validators/relationship/peer.py +1 -1
  86. infrahub/core/validators/tasks.py +1 -1
  87. infrahub/database/graph.py +21 -0
  88. infrahub/display_labels/__init__.py +0 -0
  89. infrahub/display_labels/gather.py +48 -0
  90. infrahub/display_labels/models.py +240 -0
  91. infrahub/display_labels/tasks.py +192 -0
  92. infrahub/display_labels/triggers.py +22 -0
  93. infrahub/events/branch_action.py +27 -1
  94. infrahub/events/group_action.py +1 -1
  95. infrahub/events/node_action.py +1 -1
  96. infrahub/generators/constants.py +7 -0
  97. infrahub/generators/models.py +7 -0
  98. infrahub/generators/tasks.py +34 -22
  99. infrahub/git/base.py +4 -1
  100. infrahub/git/integrator.py +23 -15
  101. infrahub/git/models.py +2 -1
  102. infrahub/git/repository.py +22 -5
  103. infrahub/git/tasks.py +66 -10
  104. infrahub/git/utils.py +123 -1
  105. infrahub/graphql/analyzer.py +1 -1
  106. infrahub/graphql/api/endpoints.py +14 -4
  107. infrahub/graphql/manager.py +4 -9
  108. infrahub/graphql/mutations/convert_object_type.py +11 -1
  109. infrahub/graphql/mutations/display_label.py +118 -0
  110. infrahub/graphql/mutations/generator.py +25 -7
  111. infrahub/graphql/mutations/hfid.py +125 -0
  112. infrahub/graphql/mutations/ipam.py +54 -35
  113. infrahub/graphql/mutations/main.py +27 -28
  114. infrahub/graphql/mutations/relationship.py +2 -2
  115. infrahub/graphql/mutations/resource_manager.py +2 -2
  116. infrahub/graphql/mutations/schema.py +5 -5
  117. infrahub/graphql/queries/resource_manager.py +1 -1
  118. infrahub/graphql/resolvers/resolver.py +2 -0
  119. infrahub/graphql/schema.py +4 -0
  120. infrahub/graphql/schema_sort.py +170 -0
  121. infrahub/graphql/types/branch.py +4 -1
  122. infrahub/graphql/types/enums.py +3 -0
  123. infrahub/groups/tasks.py +1 -1
  124. infrahub/hfid/__init__.py +0 -0
  125. infrahub/hfid/gather.py +48 -0
  126. infrahub/hfid/models.py +240 -0
  127. infrahub/hfid/tasks.py +191 -0
  128. infrahub/hfid/triggers.py +22 -0
  129. infrahub/lock.py +67 -16
  130. infrahub/message_bus/types.py +2 -1
  131. infrahub/middleware.py +26 -1
  132. infrahub/permissions/constants.py +2 -0
  133. infrahub/proposed_change/tasks.py +35 -17
  134. infrahub/server.py +21 -4
  135. infrahub/services/__init__.py +8 -5
  136. infrahub/services/adapters/http/__init__.py +5 -0
  137. infrahub/services/adapters/workflow/worker.py +14 -3
  138. infrahub/task_manager/event.py +5 -0
  139. infrahub/task_manager/models.py +7 -0
  140. infrahub/task_manager/task.py +73 -0
  141. infrahub/trigger/catalogue.py +4 -0
  142. infrahub/trigger/models.py +2 -0
  143. infrahub/trigger/setup.py +13 -4
  144. infrahub/trigger/tasks.py +6 -0
  145. infrahub/workers/dependencies.py +10 -1
  146. infrahub/workers/infrahub_async.py +10 -2
  147. infrahub/workflows/catalogue.py +80 -0
  148. infrahub/workflows/initialization.py +21 -0
  149. infrahub/workflows/utils.py +2 -1
  150. infrahub_sdk/checks.py +1 -1
  151. infrahub_sdk/client.py +13 -10
  152. infrahub_sdk/config.py +29 -2
  153. infrahub_sdk/ctl/cli_commands.py +2 -0
  154. infrahub_sdk/ctl/generator.py +4 -0
  155. infrahub_sdk/ctl/graphql.py +184 -0
  156. infrahub_sdk/ctl/schema.py +28 -9
  157. infrahub_sdk/generator.py +7 -1
  158. infrahub_sdk/graphql/__init__.py +12 -0
  159. infrahub_sdk/graphql/constants.py +1 -0
  160. infrahub_sdk/graphql/plugin.py +85 -0
  161. infrahub_sdk/graphql/query.py +77 -0
  162. infrahub_sdk/{graphql.py → graphql/renderers.py} +81 -73
  163. infrahub_sdk/graphql/utils.py +40 -0
  164. infrahub_sdk/protocols.py +14 -0
  165. infrahub_sdk/schema/__init__.py +70 -4
  166. infrahub_sdk/schema/repository.py +8 -0
  167. infrahub_sdk/spec/models.py +7 -0
  168. infrahub_sdk/spec/object.py +53 -44
  169. infrahub_sdk/spec/processors/__init__.py +0 -0
  170. infrahub_sdk/spec/processors/data_processor.py +10 -0
  171. infrahub_sdk/spec/processors/factory.py +34 -0
  172. infrahub_sdk/spec/processors/range_expand_processor.py +56 -0
  173. infrahub_sdk/spec/range_expansion.py +1 -1
  174. infrahub_sdk/transforms.py +1 -1
  175. {infrahub_server-1.5.0b0.dist-info → infrahub_server-1.5.0b2.dist-info}/METADATA +7 -4
  176. {infrahub_server-1.5.0b0.dist-info → infrahub_server-1.5.0b2.dist-info}/RECORD +182 -143
  177. infrahub_testcontainers/container.py +115 -3
  178. infrahub_testcontainers/docker-compose-cluster.test.yml +6 -1
  179. infrahub_testcontainers/docker-compose.test.yml +6 -1
  180. infrahub/core/migrations/graph/m040_profile_attrs_in_db.py +0 -166
  181. {infrahub_server-1.5.0b0.dist-info → infrahub_server-1.5.0b2.dist-info}/LICENSE.txt +0 -0
  182. {infrahub_server-1.5.0b0.dist-info → infrahub_server-1.5.0b2.dist-info}/WHEEL +0 -0
  183. {infrahub_server-1.5.0b0.dist-info → infrahub_server-1.5.0b2.dist-info}/entry_points.txt +0 -0
@@ -1,12 +1,15 @@
1
1
  import hashlib
2
- from typing import Any
2
+ from typing import TYPE_CHECKING
3
3
 
4
- from infrahub.core.branch import Branch
5
- from infrahub.core.constants.infrahubkind import GENERICGROUP, GRAPHQLQUERYGROUP
4
+ from infrahub.core.node import Node
6
5
  from infrahub.core.schema import GenericSchema
7
6
  from infrahub.core.schema.schema_branch import SchemaBranch
8
7
 
9
- KINDS_CONCURRENT_MUTATIONS_NOT_ALLOWED = [GENERICGROUP]
8
+ if TYPE_CHECKING:
9
+ from infrahub.core.relationship import RelationshipManager
10
+
11
+
12
+ RESOURCE_POOL_LOCK_NAMESPACE = "resource_pool"
10
13
 
11
14
 
12
15
  def _get_kinds_to_lock_on_object_mutation(kind: str, schema_branch: SchemaBranch) -> list[str]:
@@ -43,55 +46,78 @@ def _get_kinds_to_lock_on_object_mutation(kind: str, schema_branch: SchemaBranch
43
46
  return kinds
44
47
 
45
48
 
46
- def _should_kind_be_locked_on_any_branch(kind: str, schema_branch: SchemaBranch) -> bool:
47
- """
48
- Check whether kind or any kind generic is in KINDS_TO_LOCK_ON_ANY_BRANCH.
49
- """
50
-
51
- if kind in KINDS_CONCURRENT_MUTATIONS_NOT_ALLOWED:
52
- return True
53
-
54
- node_schema = schema_branch.get(name=kind, duplicate=False)
55
- if isinstance(node_schema, GenericSchema):
56
- return False
57
-
58
- for generic_kind in node_schema.inherit_from:
59
- if generic_kind in KINDS_CONCURRENT_MUTATIONS_NOT_ALLOWED:
60
- return True
61
- return False
62
-
63
-
64
49
  def _hash(value: str) -> str:
65
50
  # Do not use builtin `hash` for lock names as due to randomization results would differ between
66
51
  # different processes.
67
52
  return hashlib.sha256(value.encode()).hexdigest()
68
53
 
69
54
 
70
- def get_kind_lock_names_on_object_mutation(
71
- kind: str, branch: Branch, schema_branch: SchemaBranch, data: dict[str, Any]
72
- ) -> list[str]:
55
+ def get_lock_names_on_object_mutation(node: Node, schema_branch: SchemaBranch) -> list[str]:
73
56
  """
74
- Return objects kind for which we want to avoid concurrent mutation (create/update). Except for some specific kinds,
75
- concurrent mutations are only allowed on non-main branch as objects validations will be performed at least when merging in main branch.
57
+ Return lock names for object on which we want to avoid concurrent mutation (create/update).
58
+ Lock names include kind, some generic kinds, resource pool ids, and values of attributes of corresponding uniqueness constraints.
76
59
  """
77
60
 
78
- if not branch.is_default and not _should_kind_be_locked_on_any_branch(kind=kind, schema_branch=schema_branch):
79
- return []
80
-
81
- if kind == GRAPHQLQUERYGROUP:
82
- # Lock on name as well to improve performances
83
- try:
84
- name = data["name"].value
85
- return [build_object_lock_name(kind + "." + _hash(name))]
86
- except KeyError:
87
- # We might reach here if we are updating a CoreGraphQLQueryGroup without updating the name,
88
- # in which case we would not need to lock. This is not supposed to happen as current `update`
89
- # logic first fetches the node with its name.
90
- return []
91
-
92
- lock_kinds = _get_kinds_to_lock_on_object_mutation(kind, schema_branch)
93
- lock_names = [build_object_lock_name(kind) for kind in lock_kinds]
94
- return lock_names
61
+ lock_names: set[str] = set()
62
+
63
+ # Check if node is using resource manager allocation via attributes
64
+ for attr_name in node.get_schema().attribute_names:
65
+ attribute = getattr(node, attr_name, None)
66
+ if attribute is not None and getattr(attribute, "from_pool", None) and "id" in attribute.from_pool:
67
+ lock_names.add(f"{RESOURCE_POOL_LOCK_NAMESPACE}.{attribute.from_pool['id']}")
68
+
69
+ # Check if relationships allocate resources
70
+ for rel_name in node._relationships:
71
+ rel_manager: RelationshipManager = getattr(node, rel_name)
72
+ for rel in rel_manager._relationships:
73
+ if rel.from_pool and "id" in rel.from_pool:
74
+ lock_names.add(f"{RESOURCE_POOL_LOCK_NAMESPACE}.{rel.from_pool['id']}")
75
+
76
+ lock_kinds = _get_kinds_to_lock_on_object_mutation(node.get_kind(), schema_branch)
77
+ for kind in lock_kinds:
78
+ schema = schema_branch.get(name=kind, duplicate=False)
79
+ ucs = schema.uniqueness_constraints
80
+ if ucs is None:
81
+ continue
82
+
83
+ ucs_lock_names: list[str] = []
84
+ uc_attributes_names = set()
85
+
86
+ for uc in ucs:
87
+ uc_attributes_values = []
88
+ # Keep only attributes constraints
89
+ for field_path in uc:
90
+ # Some attributes may exist in different uniqueness constraints, we de-duplicate them
91
+ if field_path in uc_attributes_names:
92
+ continue
93
+
94
+ # Exclude relationships uniqueness constraints
95
+ schema_path = schema.parse_schema_path(path=field_path, schema=schema_branch)
96
+ if schema_path.related_schema is not None or schema_path.attribute_schema is None:
97
+ continue
98
+
99
+ uc_attributes_names.add(field_path)
100
+ attr = getattr(node, schema_path.attribute_schema.name, None)
101
+ if attr is None or attr.value is None:
102
+ # `attr.value` being None corresponds to optional unique attribute.
103
+ # `attr` being None is not supposed to happen.
104
+ value_hashed = _hash("")
105
+ else:
106
+ value_hashed = _hash(str(attr.value))
107
+
108
+ uc_attributes_values.append(value_hashed)
109
+
110
+ if uc_attributes_values:
111
+ uc_lock_name = ".".join(uc_attributes_values)
112
+ ucs_lock_names.append(uc_lock_name)
113
+
114
+ if not ucs_lock_names:
115
+ continue
116
+
117
+ partial_lock_name = kind + "." + ".".join(ucs_lock_names)
118
+ lock_names.add(build_object_lock_name(partial_lock_name))
119
+
120
+ return sorted(lock_names)
95
121
 
96
122
 
97
123
  def build_object_lock_name(name: str) -> str:
@@ -0,0 +1,230 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import abstractmethod
4
+ from enum import Enum
5
+ from typing import TYPE_CHECKING, Any, Generic, TypeVar
6
+
7
+ from infrahub_sdk.template import Jinja2Template
8
+
9
+ from infrahub.core.query.node import AttributeFromDB
10
+ from infrahub.core.schema import NodeSchema, ProfileSchema, TemplateSchema
11
+
12
+ from ..attribute import BaseAttribute, ListAttributeOptional, StringOptional
13
+
14
+ if TYPE_CHECKING:
15
+ from infrahub.core.node import Node
16
+ from infrahub.core.schema import NodeSchema, ProfileSchema, TemplateSchema
17
+ from infrahub.core.schema.attribute_schema import AttributeSchema
18
+ from infrahub.core.timestamp import Timestamp
19
+ from infrahub.database import InfrahubDatabase
20
+
21
+ T = TypeVar("T")
22
+
23
+
24
+ class NodePropertyAttribute(Generic[T]):
25
+ """A node property attribute is a construct that seats between a property and an attribute.
26
+
27
+ View it as a property, set at the node level but stored in the database as an attribute. It usually is something computed from other components of
28
+ a node, such as its attributes and its relationships.
29
+ """
30
+
31
+ def __init__(
32
+ self,
33
+ node_schema: NodeSchema | ProfileSchema | TemplateSchema,
34
+ template: T | None,
35
+ value: AttributeFromDB | T | None = None,
36
+ ) -> None:
37
+ self.node_schema = node_schema
38
+
39
+ self.node_attributes: list[str] = []
40
+ self.node_relationships: list[str] = []
41
+
42
+ self.template = template
43
+ self._value = value
44
+ self._manually_assigned = False
45
+
46
+ self.schema: AttributeSchema
47
+
48
+ self.analyze_variables()
49
+
50
+ def needs_update(self, fields: list[str] | None) -> bool:
51
+ """Tell if this node property attribute must be recomputed given a list of updated fields of a node."""
52
+ if self._manually_assigned or not fields:
53
+ return True
54
+ for field in fields:
55
+ if field in self.node_attributes or field in self.node_relationships:
56
+ return True
57
+
58
+ return False
59
+
60
+ @property
61
+ def attribute_value(self) -> AttributeFromDB | dict[str, T | None]:
62
+ if isinstance(self._value, AttributeFromDB):
63
+ return self._value
64
+ return {"value": self._value}
65
+
66
+ def set_value(self, value: T | None, manually_assigned: bool = False) -> None:
67
+ """Force the value of the node property attribute to the given one."""
68
+ if isinstance(self._value, AttributeFromDB):
69
+ self._value.value = value
70
+ else:
71
+ self._value = value
72
+
73
+ if manually_assigned:
74
+ self._manually_assigned = True
75
+
76
+ def get_value(self, node: Node, at: Timestamp) -> T | None:
77
+ if isinstance(self._value, AttributeFromDB):
78
+ attr = self.get_node_attribute(node=node, at=at)
79
+ return attr.value # type: ignore
80
+
81
+ return self._value
82
+
83
+ @abstractmethod
84
+ def analyze_variables(self) -> None: ...
85
+
86
+ @abstractmethod
87
+ async def compute(self, db: InfrahubDatabase, node: Node) -> None: ...
88
+
89
+ @abstractmethod
90
+ def get_node_attribute(self, node: Node, at: Timestamp) -> BaseAttribute: ...
91
+
92
+
93
+ class DisplayLabel(NodePropertyAttribute[str]):
94
+ def __init__(
95
+ self,
96
+ node_schema: NodeSchema | ProfileSchema | TemplateSchema,
97
+ template: str | None,
98
+ value: AttributeFromDB | str | None = None,
99
+ ) -> None:
100
+ super().__init__(node_schema=node_schema, template=template, value=value)
101
+
102
+ self.schema = node_schema.get_attribute(name="display_label")
103
+
104
+ @property
105
+ def is_jinja2_template(self) -> bool:
106
+ if self.template is None:
107
+ return False
108
+
109
+ return any(c in self.template for c in "{}")
110
+
111
+ def _analyze_plain_value(self) -> None:
112
+ if self.template is None or "__" not in self.template:
113
+ return
114
+
115
+ items = self.template.split("__", maxsplit=1)
116
+ if items[0] not in self.node_schema.attribute_names:
117
+ raise ValueError(f"{items[0]} is not an attribute of {self.node_schema.kind}")
118
+
119
+ self.node_attributes.append(items[0])
120
+
121
+ def _analyze_jinja2_value(self) -> None:
122
+ if self.template is None or not self.is_jinja2_template:
123
+ return
124
+
125
+ tpl = Jinja2Template(template=self.template)
126
+ for variable in tpl.get_variables():
127
+ items = variable.split("__", maxsplit=1)
128
+ if items[0] in self.node_schema.attribute_names:
129
+ self.node_attributes.append(items[0])
130
+ elif items[0] in self.node_schema.relationship_names:
131
+ self.node_relationships.append(items[0])
132
+ else:
133
+ raise ValueError(f"{items[0]} is neither an attribute or a relationship of {self.node_schema.kind}")
134
+
135
+ def analyze_variables(self) -> None:
136
+ """Look at variables used in the display label and record attributes and relationships required to compute it."""
137
+ if not self.is_jinja2_template:
138
+ self._analyze_plain_value()
139
+ else:
140
+ self._analyze_jinja2_value()
141
+
142
+ async def compute(self, db: InfrahubDatabase, node: Node) -> None:
143
+ """Update the display label value by recomputing it from the template."""
144
+ if self.template is None or self._manually_assigned:
145
+ return
146
+
147
+ if node.get_schema() != self.node_schema:
148
+ raise ValueError(
149
+ f"display_label for schema {self.node_schema.kind} cannot be rendered for node {node.get_schema().kind} {node.id}"
150
+ )
151
+
152
+ if not self.is_jinja2_template:
153
+ path_value = await node.get_path_value(db=db, path=self.template)
154
+ # Use .value for enum to keep compat with old style display label
155
+ self.set_value(value=str(path_value if not isinstance(path_value, Enum) else path_value.value))
156
+ return
157
+
158
+ jinja2_template = Jinja2Template(template=self.template)
159
+
160
+ variables: dict[str, Any] = {}
161
+ for variable in jinja2_template.get_variables():
162
+ variables[variable] = await node.get_path_value(db=db, path=variable)
163
+
164
+ self.set_value(value=await jinja2_template.render(variables=variables))
165
+
166
+ def get_node_attribute(self, node: Node, at: Timestamp) -> StringOptional:
167
+ """Return a node attribute that can be stored in the database for this display label and node."""
168
+ return StringOptional(
169
+ name="display_label",
170
+ schema=self.schema,
171
+ branch=node.get_branch(),
172
+ at=at,
173
+ node=node,
174
+ data=self.attribute_value,
175
+ )
176
+
177
+
178
+ class HumanFriendlyIdentifier(NodePropertyAttribute[list[str]]):
179
+ def __init__(
180
+ self,
181
+ node_schema: NodeSchema | ProfileSchema | TemplateSchema,
182
+ template: list[str] | None,
183
+ value: AttributeFromDB | list[str] | None = None,
184
+ ) -> None:
185
+ super().__init__(node_schema=node_schema, template=template, value=value)
186
+
187
+ self.schema = node_schema.get_attribute(name="human_friendly_id")
188
+
189
+ def _analyze_single_variable(self, value: str) -> None:
190
+ items = value.split("__", maxsplit=1)
191
+ if items[0] in self.node_schema.attribute_names:
192
+ self.node_attributes.append(items[0])
193
+ elif items[0] in self.node_schema.relationship_names:
194
+ self.node_relationships.append(items[0])
195
+ else:
196
+ raise ValueError(f"{items[0]} is neither an attribute or a relationship of {self.node_schema.kind}")
197
+
198
+ def analyze_variables(self) -> None:
199
+ """Look at variables used in the HFID and record attributes and relationships required to compute it."""
200
+ for item in self.template or []:
201
+ self._analyze_single_variable(value=item)
202
+
203
+ async def compute(self, db: InfrahubDatabase, node: Node) -> None:
204
+ """Update the HFID value by recomputing it from the template."""
205
+ if self.template is None or self._manually_assigned:
206
+ return
207
+
208
+ if node.get_schema() != self.node_schema:
209
+ raise ValueError(
210
+ f"human_friendly_id for schema {self.node_schema.kind} cannot be computed for node {node.get_schema().kind} {node.id}"
211
+ )
212
+
213
+ value: list[str] = []
214
+ for path in self.template:
215
+ path_value = await node.get_path_value(db=db, path=path)
216
+ # Use .value for enum to be consistent with display label
217
+ value.append(path_value if not isinstance(path_value, Enum) else path_value.value)
218
+
219
+ self.set_value(value=value)
220
+
221
+ def get_node_attribute(self, node: Node, at: Timestamp) -> ListAttributeOptional:
222
+ """Return a node attribute that can be stored in the database for this HFID and node."""
223
+ return ListAttributeOptional(
224
+ name="human_friendly_id",
225
+ schema=self.schema,
226
+ branch=node.get_branch(),
227
+ at=at,
228
+ node=node,
229
+ data=self.attribute_value,
230
+ )
@@ -15,6 +15,7 @@ from infrahub.exceptions import PoolExhaustedError, ValidationError
15
15
  from infrahub.pools.address import get_available
16
16
 
17
17
  from .. import Node
18
+ from ..lock_utils import RESOURCE_POOL_LOCK_NAMESPACE
18
19
 
19
20
  if TYPE_CHECKING:
20
21
  from infrahub.core.branch import Branch
@@ -34,7 +35,7 @@ class CoreIPAddressPool(Node):
34
35
  prefixlen: int | None = None,
35
36
  at: Timestamp | None = None,
36
37
  ) -> Node:
37
- async with lock.registry.get(name=self.get_id(), namespace="resource_pool"):
38
+ async with lock.registry.get(name=self.get_id(), namespace=RESOURCE_POOL_LOCK_NAMESPACE):
38
39
  # Check if there is already a resource allocated with this identifier
39
40
  # if not, pull all existing prefixes and allocated the next available
40
41
 
@@ -17,6 +17,7 @@ from infrahub.exceptions import ValidationError
17
17
  from infrahub.pools.prefix import get_next_available_prefix
18
18
 
19
19
  from .. import Node
20
+ from ..lock_utils import RESOURCE_POOL_LOCK_NAMESPACE
20
21
 
21
22
  if TYPE_CHECKING:
22
23
  from infrahub.core.branch import Branch
@@ -37,7 +38,7 @@ class CoreIPPrefixPool(Node):
37
38
  prefix_type: str | None = None,
38
39
  at: Timestamp | None = None,
39
40
  ) -> Node:
40
- async with lock.registry.get(name=self.get_id(), namespace="resource_pool"):
41
+ async with lock.registry.get(name=self.get_id(), namespace=RESOURCE_POOL_LOCK_NAMESPACE):
41
42
  # Check if there is already a resource allocated with this identifier
42
43
  # if not, pull all existing prefixes and allocated the next available
43
44
  if identifier:
@@ -9,6 +9,7 @@ from infrahub.core.schema.attribute_parameters import NumberAttributeParameters
9
9
  from infrahub.exceptions import PoolExhaustedError
10
10
 
11
11
  from .. import Node
12
+ from ..lock_utils import RESOURCE_POOL_LOCK_NAMESPACE
12
13
 
13
14
  if TYPE_CHECKING:
14
15
  from infrahub.core.branch import Branch
@@ -63,7 +64,7 @@ class CoreNumberPool(Node):
63
64
  identifier: str | None = None,
64
65
  at: Timestamp | None = None,
65
66
  ) -> int:
66
- async with lock.registry.get(name=self.get_id(), namespace="resource_pool"):
67
+ async with lock.registry.get(name=self.get_id(), namespace=RESOURCE_POOL_LOCK_NAMESPACE):
67
68
  # NOTE: ideally we should use the HFID as the identifier (if available)
68
69
  # one of the challenge with using the HFID is that it might change over time
69
70
  # so we need to ensure that the identifier is stable, or we need to handle the case where the identifier changes
@@ -111,7 +111,7 @@ class StandardNode(BaseModel):
111
111
  node = result.get("n")
112
112
 
113
113
  self.id = node.element_id
114
- self.uuid = node["uuid"]
114
+ self.uuid = UUID(node["uuid"])
115
115
 
116
116
  return True
117
117
 
@@ -4,7 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  from typing import TYPE_CHECKING
6
6
 
7
- from .protocols_base import CoreNode
7
+ from infrahub.core.protocols_base import CoreNode
8
8
 
9
9
  if TYPE_CHECKING:
10
10
  from enum import Enum
@@ -350,6 +350,10 @@ class CoreGeneratorAction(CoreAction):
350
350
  generator: RelationshipManager
351
351
 
352
352
 
353
+ class CoreGeneratorAwareGroup(CoreGroup):
354
+ pass
355
+
356
+
353
357
  class CoreGeneratorCheck(CoreCheck):
354
358
  instance: String
355
359
 
@@ -361,6 +365,8 @@ class CoreGeneratorDefinition(CoreTaskTarget):
361
365
  file_path: String
362
366
  class_name: String
363
367
  convert_query_response: BooleanOptional
368
+ execute_in_proposed_change: BooleanOptional
369
+ execute_after_merge: BooleanOptional
364
370
  query: RelationshipManager
365
371
  repository: RelationshipManager
366
372
  targets: RelationshipManager
@@ -184,6 +184,61 @@ class AttributeUpdateNodePropertyQuery(AttributeQuery):
184
184
  self.return_labels = ["a", "np", "r"]
185
185
 
186
186
 
187
+ class AttributeClearNodePropertyQuery(AttributeQuery):
188
+ name = "attribute_clear_node_property"
189
+ type: QueryType = QueryType.WRITE
190
+ insert_return: bool = False
191
+
192
+ def __init__(
193
+ self,
194
+ prop_name: str,
195
+ prop_id: str | None = None,
196
+ **kwargs: Any,
197
+ ):
198
+ self.prop_name = prop_name
199
+ self.prop_id = prop_id
200
+
201
+ super().__init__(**kwargs)
202
+
203
+ async def query_init(self, db: InfrahubDatabase, **kwargs: dict[str, Any]) -> None: # noqa: ARG002
204
+ at = self.at or self.attr.at
205
+
206
+ branch_filter, branch_params = self.branch.get_query_filter_path(at=at)
207
+ self.params.update(branch_params)
208
+ self.params["attr_uuid"] = self.attr.id
209
+ self.params["branch"] = self.branch.name
210
+ self.params["branch_level"] = self.branch.hierarchy_level
211
+ self.params["at"] = at.to_string()
212
+ self.params["prop_name"] = self.prop_name
213
+ self.params["prop_id"] = self.prop_id
214
+
215
+ rel_label = f"HAS_{self.prop_name.upper()}"
216
+ query = """
217
+ MATCH (a:Attribute { uuid: $attr_uuid })-[r:%(rel_label)s]->(np:Node { uuid: $prop_id })
218
+ WITH DISTINCT a, np
219
+ CALL (a, np) {
220
+ MATCH (a)-[r:%(rel_label)s]->(np)
221
+ WHERE %(branch_filter)s
222
+ ORDER BY r.branch_level DESC, r.from DESC, r.status ASC
223
+ LIMIT 1
224
+ RETURN r AS property_edge
225
+ }
226
+ WITH a, np, property_edge
227
+ WHERE property_edge.status = "active"
228
+ CALL (property_edge) {
229
+ WITH property_edge
230
+ WHERE property_edge.branch = $branch
231
+ SET property_edge.to = $at
232
+ }
233
+ CALL (a, np, property_edge) {
234
+ WITH property_edge
235
+ WHERE property_edge.branch_level < $branch_level
236
+ CREATE (a)-[r:%(rel_label)s { branch: $branch, branch_level: $branch_level, status: "deleted", from: $at }]->(np)
237
+ }
238
+ """ % {"branch_filter": branch_filter, "rel_label": rel_label}
239
+ self.add_to_query(query)
240
+
241
+
187
242
  class AttributeGetQuery(AttributeQuery):
188
243
  name = "attribute_get"
189
244
  type: QueryType = QueryType.READ
@@ -688,6 +688,7 @@ class IPPrefixReconcileQuery(Query):
688
688
  "ip_address_attribute_kind": ADDRESS_ATTRIBUTE_LABEL,
689
689
  }
690
690
  self.add_to_query(get_new_children_query)
691
+ self.order_by = ["ip_node.uuid"]
691
692
  self.return_labels = ["ip_node", "current_parent", "current_children", "new_parent", "new_children"]
692
693
 
693
694
  def _get_uuid_from_query(self, node_name: str) -> str | None:
@@ -142,9 +142,22 @@ class NodeCreateAllQuery(NodeQuery):
142
142
  attributes_ipnetwork: list[AttributeCreateData] = []
143
143
  attributes_indexed: list[AttributeCreateData] = []
144
144
 
145
+ if self.node.has_display_label():
146
+ attributes_indexed.append(
147
+ self.node._display_label.get_node_attribute(node=self.node, at=at).get_create_data(
148
+ node_schema=self.node.get_schema()
149
+ )
150
+ )
151
+ if self.node.has_human_friendly_id():
152
+ attributes_indexed.append(
153
+ self.node._human_friendly_id.get_node_attribute(node=self.node, at=at).get_create_data(
154
+ node_schema=self.node.get_schema()
155
+ )
156
+ )
157
+
145
158
  for attr_name in self.node._attributes:
146
159
  attr: BaseAttribute = getattr(self.node, attr_name)
147
- attr_data = attr.get_create_data()
160
+ attr_data = attr.get_create_data(node_schema=self.node.get_schema())
148
161
  node_type = attr.get_db_node_type()
149
162
 
150
163
  if AttributeDBNodeType.IPHOST in node_type:
@@ -233,11 +246,15 @@ class NodeCreateAllQuery(NodeQuery):
233
246
  ipnetwork_prop_list = [f"{key}: {value}" for key, value in ipnetwork_prop.items()]
234
247
 
235
248
  attrs_nonindexed_query = """
236
- WITH distinct n
249
+ WITH DISTINCT n
237
250
  UNWIND $attrs AS attr
238
251
  // Try to find a matching vertex
239
- OPTIONAL MATCH (existing_av:AttributeValue {value: attr.content.value, is_default: attr.content.is_default})
240
- WHERE NOT existing_av:AttributeValueIndexed
252
+ CALL (attr) {
253
+ OPTIONAL MATCH (existing_av:AttributeValue {value: attr.content.value, is_default: attr.content.is_default})
254
+ WHERE NOT existing_av:AttributeValueIndexed
255
+ RETURN existing_av
256
+ LIMIT 1
257
+ }
241
258
  CALL (attr, existing_av) {
242
259
  // If none found, create a new one
243
260
  WITH existing_av
@@ -902,6 +919,7 @@ class NodeListGetRelationshipsQuery(Query):
902
919
  RETURN DISTINCT n_uuid, rel_name, peer_uuid, direction
903
920
  """ % {"filters": rels_filter}
904
921
  self.add_to_query(query)
922
+ self.order_by = ["n_uuid", "rel_name", "peer_uuid", "direction"]
905
923
  self.return_labels = ["n_uuid", "rel_name", "peer_uuid", "direction"]
906
924
 
907
925
  def get_peers_group_by_node(self) -> GroupedPeerNodes:
@@ -971,6 +989,7 @@ class NodeListGetInfoQuery(Query):
971
989
  )
972
990
  self.params.update(branch_params)
973
991
  self.params["ids"] = self.ids
992
+ self.order_by = ["n.uuid"]
974
993
 
975
994
  query = """
976
995
  MATCH p = (root:Root)<-[:IS_PART_OF]-(n:Node)
@@ -1036,6 +1036,7 @@ class RelationshipCountPerNodeQuery(Query):
1036
1036
  """ % {"branch_filter": branch_filter, "path": path}
1037
1037
 
1038
1038
  self.add_to_query(query)
1039
+ self.order_by = ["peer_node.uuid"]
1039
1040
  self.return_labels = ["peer_node.uuid", "COUNT(peer_node.uuid) as nbr_peers"]
1040
1041
 
1041
1042
  async def get_count_per_peer(self) -> dict[str, int]:
infrahub/core/registry.py CHANGED
@@ -113,8 +113,8 @@ class Registry:
113
113
  return True
114
114
  return False
115
115
 
116
- def get_node_schema(self, name: str, branch: Branch | str | None = None) -> NodeSchema:
117
- return self.schema.get_node_schema(name=name, branch=branch)
116
+ def get_node_schema(self, name: str, branch: Branch | str | None = None, duplicate: bool = False) -> NodeSchema:
117
+ return self.schema.get_node_schema(name=name, branch=branch, duplicate=duplicate)
118
118
 
119
119
  def get_data_type(self, name: str) -> type[InfrahubDataType]:
120
120
  if name not in self.data_type:
@@ -40,7 +40,7 @@ class RelationshipCountConstraint(RelationshipManagerConstraintInterface):
40
40
  # peer_ids_present_database_only:
41
41
  # relationship to be deleted, need to check if the schema on the other side has a min_count defined
42
42
  # TODO see how to manage Generic node
43
- peer_schema = registry.schema.get(name=relm.schema.peer, branch=branch)
43
+ peer_schema = registry.schema.get(name=relm.schema.peer, branch=branch, duplicate=False)
44
44
  peer_rels = peer_schema.get_relationships_by_identifier(id=relm.schema.get_identifier())
45
45
  if not peer_rels:
46
46
  return
@@ -459,7 +459,7 @@ class Relationship(FlagPropertyMixin, NodePropertyMixin):
459
459
  self.set_peer(value=peer)
460
460
 
461
461
  if not self.peer_id and self.peer_hfid:
462
- peer_schema = db.schema.get(name=self.schema.peer, branch=self.branch)
462
+ peer_schema = db.schema.get(name=self.schema.peer, branch=self.branch, duplicate=False)
463
463
  kind = (
464
464
  self.data["kind"]
465
465
  if isinstance(self.data, dict) and "kind" in self.data and peer_schema.is_generic_schema