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.
- infrahub/actions/tasks.py +8 -0
- infrahub/api/diff/diff.py +1 -1
- infrahub/api/internal.py +2 -0
- infrahub/api/oauth2.py +13 -19
- infrahub/api/oidc.py +15 -21
- infrahub/api/schema.py +24 -3
- infrahub/artifacts/models.py +2 -1
- infrahub/auth.py +137 -3
- infrahub/cli/__init__.py +2 -0
- infrahub/cli/db.py +103 -98
- infrahub/cli/db_commands/clean_duplicate_schema_fields.py +212 -0
- infrahub/cli/dev.py +118 -0
- infrahub/cli/tasks.py +46 -0
- infrahub/cli/upgrade.py +30 -3
- infrahub/computed_attribute/tasks.py +20 -8
- infrahub/core/attribute.py +13 -5
- infrahub/core/branch/enums.py +1 -1
- infrahub/core/branch/models.py +7 -3
- infrahub/core/branch/tasks.py +70 -8
- infrahub/core/changelog/models.py +4 -12
- infrahub/core/constants/__init__.py +3 -0
- infrahub/core/constants/infrahubkind.py +1 -0
- infrahub/core/diff/model/path.py +4 -0
- infrahub/core/diff/payload_builder.py +1 -1
- infrahub/core/diff/query/artifact.py +1 -0
- infrahub/core/diff/query/field_summary.py +1 -0
- infrahub/core/graph/__init__.py +1 -1
- infrahub/core/initialization.py +5 -2
- infrahub/core/ipam/utilization.py +1 -1
- infrahub/core/manager.py +6 -3
- infrahub/core/migrations/__init__.py +3 -0
- infrahub/core/migrations/exceptions.py +4 -0
- infrahub/core/migrations/graph/__init__.py +12 -11
- infrahub/core/migrations/graph/load_schema_branch.py +21 -0
- infrahub/core/migrations/graph/m013_convert_git_password_credential.py +1 -1
- infrahub/core/migrations/graph/m040_duplicated_attributes.py +81 -0
- infrahub/core/migrations/graph/m041_profile_attrs_in_db.py +145 -0
- infrahub/core/migrations/graph/m042_create_hfid_display_label_in_db.py +164 -0
- infrahub/core/migrations/graph/m043_backfill_hfid_display_label_in_db.py +866 -0
- infrahub/core/migrations/query/__init__.py +7 -8
- infrahub/core/migrations/query/attribute_add.py +8 -6
- infrahub/core/migrations/query/attribute_remove.py +134 -0
- infrahub/core/migrations/runner.py +54 -0
- infrahub/core/migrations/schema/attribute_kind_update.py +9 -3
- infrahub/core/migrations/schema/attribute_supports_profile.py +90 -0
- infrahub/core/migrations/schema/node_attribute_add.py +35 -4
- infrahub/core/migrations/schema/node_attribute_remove.py +13 -109
- infrahub/core/migrations/schema/node_kind_update.py +2 -1
- infrahub/core/migrations/schema/node_remove.py +2 -1
- infrahub/core/migrations/schema/placeholder_dummy.py +3 -2
- infrahub/core/migrations/shared.py +52 -19
- infrahub/core/node/__init__.py +158 -51
- infrahub/core/node/constraints/attribute_uniqueness.py +3 -1
- infrahub/core/node/create.py +46 -63
- infrahub/core/node/lock_utils.py +70 -44
- infrahub/core/node/node_property_attribute.py +230 -0
- infrahub/core/node/resource_manager/ip_address_pool.py +2 -1
- infrahub/core/node/resource_manager/ip_prefix_pool.py +2 -1
- infrahub/core/node/resource_manager/number_pool.py +2 -1
- infrahub/core/node/standard.py +1 -1
- infrahub/core/protocols.py +7 -1
- infrahub/core/query/attribute.py +55 -0
- infrahub/core/query/ipam.py +1 -0
- infrahub/core/query/node.py +23 -4
- infrahub/core/query/relationship.py +1 -0
- infrahub/core/registry.py +2 -2
- infrahub/core/relationship/constraints/count.py +1 -1
- infrahub/core/relationship/model.py +1 -1
- infrahub/core/schema/__init__.py +56 -0
- infrahub/core/schema/attribute_schema.py +4 -0
- infrahub/core/schema/basenode_schema.py +42 -2
- infrahub/core/schema/definitions/core/__init__.py +2 -0
- infrahub/core/schema/definitions/core/generator.py +2 -0
- infrahub/core/schema/definitions/core/group.py +16 -2
- infrahub/core/schema/definitions/internal.py +16 -3
- infrahub/core/schema/generated/attribute_schema.py +2 -2
- infrahub/core/schema/generated/base_node_schema.py +6 -1
- infrahub/core/schema/manager.py +22 -1
- infrahub/core/schema/node_schema.py +5 -2
- infrahub/core/schema/schema_branch.py +300 -8
- infrahub/core/schema/schema_branch_display.py +123 -0
- infrahub/core/schema/schema_branch_hfid.py +114 -0
- infrahub/core/validators/aggregated_checker.py +1 -1
- infrahub/core/validators/determiner.py +12 -1
- infrahub/core/validators/relationship/peer.py +1 -1
- infrahub/core/validators/tasks.py +1 -1
- infrahub/database/graph.py +21 -0
- infrahub/display_labels/__init__.py +0 -0
- infrahub/display_labels/gather.py +48 -0
- infrahub/display_labels/models.py +240 -0
- infrahub/display_labels/tasks.py +192 -0
- infrahub/display_labels/triggers.py +22 -0
- infrahub/events/branch_action.py +27 -1
- infrahub/events/group_action.py +1 -1
- infrahub/events/node_action.py +1 -1
- infrahub/generators/constants.py +7 -0
- infrahub/generators/models.py +7 -0
- infrahub/generators/tasks.py +34 -22
- infrahub/git/base.py +4 -1
- infrahub/git/integrator.py +23 -15
- infrahub/git/models.py +2 -1
- infrahub/git/repository.py +22 -5
- infrahub/git/tasks.py +66 -10
- infrahub/git/utils.py +123 -1
- infrahub/graphql/analyzer.py +1 -1
- infrahub/graphql/api/endpoints.py +14 -4
- infrahub/graphql/manager.py +4 -9
- infrahub/graphql/mutations/convert_object_type.py +11 -1
- infrahub/graphql/mutations/display_label.py +118 -0
- infrahub/graphql/mutations/generator.py +25 -7
- infrahub/graphql/mutations/hfid.py +125 -0
- infrahub/graphql/mutations/ipam.py +54 -35
- infrahub/graphql/mutations/main.py +27 -28
- infrahub/graphql/mutations/relationship.py +2 -2
- infrahub/graphql/mutations/resource_manager.py +2 -2
- infrahub/graphql/mutations/schema.py +5 -5
- infrahub/graphql/queries/resource_manager.py +1 -1
- infrahub/graphql/resolvers/resolver.py +2 -0
- infrahub/graphql/schema.py +4 -0
- infrahub/graphql/schema_sort.py +170 -0
- infrahub/graphql/types/branch.py +4 -1
- infrahub/graphql/types/enums.py +3 -0
- infrahub/groups/tasks.py +1 -1
- infrahub/hfid/__init__.py +0 -0
- infrahub/hfid/gather.py +48 -0
- infrahub/hfid/models.py +240 -0
- infrahub/hfid/tasks.py +191 -0
- infrahub/hfid/triggers.py +22 -0
- infrahub/lock.py +67 -16
- infrahub/message_bus/types.py +2 -1
- infrahub/middleware.py +26 -1
- infrahub/permissions/constants.py +2 -0
- infrahub/proposed_change/tasks.py +35 -17
- infrahub/server.py +21 -4
- infrahub/services/__init__.py +8 -5
- infrahub/services/adapters/http/__init__.py +5 -0
- infrahub/services/adapters/workflow/worker.py +14 -3
- infrahub/task_manager/event.py +5 -0
- infrahub/task_manager/models.py +7 -0
- infrahub/task_manager/task.py +73 -0
- infrahub/trigger/catalogue.py +4 -0
- infrahub/trigger/models.py +2 -0
- infrahub/trigger/setup.py +13 -4
- infrahub/trigger/tasks.py +6 -0
- infrahub/workers/dependencies.py +10 -1
- infrahub/workers/infrahub_async.py +10 -2
- infrahub/workflows/catalogue.py +80 -0
- infrahub/workflows/initialization.py +21 -0
- infrahub/workflows/utils.py +2 -1
- infrahub_sdk/checks.py +1 -1
- infrahub_sdk/client.py +13 -10
- infrahub_sdk/config.py +29 -2
- infrahub_sdk/ctl/cli_commands.py +2 -0
- infrahub_sdk/ctl/generator.py +4 -0
- infrahub_sdk/ctl/graphql.py +184 -0
- infrahub_sdk/ctl/schema.py +28 -9
- infrahub_sdk/generator.py +7 -1
- infrahub_sdk/graphql/__init__.py +12 -0
- infrahub_sdk/graphql/constants.py +1 -0
- infrahub_sdk/graphql/plugin.py +85 -0
- infrahub_sdk/graphql/query.py +77 -0
- infrahub_sdk/{graphql.py → graphql/renderers.py} +81 -73
- infrahub_sdk/graphql/utils.py +40 -0
- infrahub_sdk/protocols.py +14 -0
- infrahub_sdk/schema/__init__.py +70 -4
- infrahub_sdk/schema/repository.py +8 -0
- infrahub_sdk/spec/models.py +7 -0
- infrahub_sdk/spec/object.py +53 -44
- infrahub_sdk/spec/processors/__init__.py +0 -0
- infrahub_sdk/spec/processors/data_processor.py +10 -0
- infrahub_sdk/spec/processors/factory.py +34 -0
- infrahub_sdk/spec/processors/range_expand_processor.py +56 -0
- infrahub_sdk/spec/range_expansion.py +1 -1
- infrahub_sdk/transforms.py +1 -1
- {infrahub_server-1.5.0b0.dist-info → infrahub_server-1.5.0b2.dist-info}/METADATA +7 -4
- {infrahub_server-1.5.0b0.dist-info → infrahub_server-1.5.0b2.dist-info}/RECORD +182 -143
- infrahub_testcontainers/container.py +115 -3
- infrahub_testcontainers/docker-compose-cluster.test.yml +6 -1
- infrahub_testcontainers/docker-compose.test.yml +6 -1
- infrahub/core/migrations/graph/m040_profile_attrs_in_db.py +0 -166
- {infrahub_server-1.5.0b0.dist-info → infrahub_server-1.5.0b2.dist-info}/LICENSE.txt +0 -0
- {infrahub_server-1.5.0b0.dist-info → infrahub_server-1.5.0b2.dist-info}/WHEEL +0 -0
- {infrahub_server-1.5.0b0.dist-info → infrahub_server-1.5.0b2.dist-info}/entry_points.txt +0 -0
infrahub/core/node/lock_utils.py
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import hashlib
|
|
2
|
-
from typing import
|
|
2
|
+
from typing import TYPE_CHECKING
|
|
3
3
|
|
|
4
|
-
from infrahub.core.
|
|
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
|
-
|
|
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
|
|
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
|
|
75
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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=
|
|
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=
|
|
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=
|
|
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
|
infrahub/core/node/standard.py
CHANGED
infrahub/core/protocols.py
CHANGED
|
@@ -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
|
infrahub/core/query/attribute.py
CHANGED
|
@@ -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
|
infrahub/core/query/ipam.py
CHANGED
|
@@ -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:
|
infrahub/core/query/node.py
CHANGED
|
@@ -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
|
|
249
|
+
WITH DISTINCT n
|
|
237
250
|
UNWIND $attrs AS attr
|
|
238
251
|
// Try to find a matching vertex
|
|
239
|
-
|
|
240
|
-
|
|
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
|