diff --git a/backend/infrahub/core/manager.py b/backend/infrahub/core/manager.py index d1e4cfb3b1..31fb943e4f 100644 --- a/backend/infrahub/core/manager.py +++ b/backend/infrahub/core/manager.py @@ -803,8 +803,21 @@ async def get_one_by_hfid( hfid_str = " :: ".join(hfid) - if not node_schema.human_friendly_id or len(node_schema.human_friendly_id) != len(hfid): - raise NodeNotFoundError(branch_name=branch.name, node_type=kind_str, identifier=hfid_str) + if not node_schema.human_friendly_id: + raise NodeNotFoundError( + branch_name=branch.name, + node_type=kind_str, + identifier=hfid_str, + message=f"Unable to lookup node by HFID, schema '{node_schema.kind}' does not have a HFID defined.", + ) + + if len(node_schema.human_friendly_id) != len(hfid): + raise NodeNotFoundError( + branch_name=branch.name, + node_type=kind_str, + identifier=hfid_str, + message=f"Unable to lookup node by HFID, schema '{node_schema.kind}' HFID does not contain the same number of elements as {hfid}", + ) filters = {} for key, item in zip(node_schema.human_friendly_id, hfid, strict=False): diff --git a/backend/infrahub/core/schema/schema_branch.py b/backend/infrahub/core/schema/schema_branch.py index 4d52858320..08b13fb723 100644 --- a/backend/infrahub/core/schema/schema_branch.py +++ b/backend/infrahub/core/schema/schema_branch.py @@ -1909,10 +1909,8 @@ def manage_object_template_relationships(self) -> None: self.set(name=node_name, schema=node_schema) - def add_relationships_to_template(self, node: NodeSchema) -> None: + def add_relationships_to_template(self, node: NodeSchema | GenericSchema) -> None: template_schema = self.get(name=self._get_object_template_kind(node_kind=node.kind), duplicate=False) - if template_schema.is_generic_schema: - return # Remove previous relationships to account for new ones template_schema.relationships = [ @@ -1954,6 +1952,7 @@ def add_relationships_to_template(self, node: NodeSchema) -> None: label=f"{relationship.name} template".title() if relationship.kind in [RelationshipKind.COMPONENT, RelationshipKind.PARENT] else relationship.name.title(), + inherited=relationship.inherited, ) ) @@ -1983,9 +1982,6 @@ def generate_object_template_from_node( need_template_kinds = [n.kind for n in need_templates] if node.is_generic_schema: - # When needing a template for a generic, we generate an empty shell mostly to make sure that schemas (including the GraphQL one) will - # look right. We don't really care about applying inheritance of fields as it was already processed and actual templates will have the - # correct attributes and relationships template = GenericSchema( name=node.kind, namespace="Template", @@ -1994,43 +1990,44 @@ def generate_object_template_from_node( generate_profile=False, branch=node.branch, include_in_menu=False, + display_labels=["template_name__value"], + human_friendly_id=["template_name__value"], + uniqueness_constraints=[["template_name__value"]], attributes=[template_name_attr], ) for used in node.used_by: if used in need_template_kinds: template.used_by.append(self._get_object_template_kind(node_kind=used)) + else: + template = TemplateSchema( + name=node.kind, + namespace="Template", + label=f"Object template {node.label}", + description=f"Object template for {node.kind}", + branch=node.branch, + include_in_menu=False, + display_labels=["template_name__value"], + human_friendly_id=["template_name__value"], + uniqueness_constraints=[["template_name__value"]], + inherit_from=[InfrahubKind.LINEAGESOURCE, InfrahubKind.NODE, core_template_schema.kind], + default_filter="template_name__value", + attributes=[template_name_attr], + relationships=[ + RelationshipSchema( + name="related_nodes", + identifier="node__objecttemplate", + peer=node.kind, + kind=RelationshipKind.TEMPLATE, + cardinality=RelationshipCardinality.MANY, + branch=BranchSupportType.AWARE, + ) + ], + ) - return template - - template = TemplateSchema( - name=node.kind, - namespace="Template", - label=f"Object template {node.label}", - description=f"Object template for {node.kind}", - branch=node.branch, - include_in_menu=False, - display_labels=["template_name__value"], - human_friendly_id=["template_name__value"], - uniqueness_constraints=[["template_name__value"]], - inherit_from=[InfrahubKind.LINEAGESOURCE, InfrahubKind.NODE, core_template_schema.kind], - default_filter="template_name__value", - attributes=[template_name_attr], - relationships=[ - RelationshipSchema( - name="related_nodes", - identifier="node__objecttemplate", - peer=node.kind, - kind=RelationshipKind.TEMPLATE, - cardinality=RelationshipCardinality.MANY, - branch=BranchSupportType.AWARE, - ) - ], - ) - - for inherited in node.inherit_from: - if inherited in need_template_kinds: - template.inherit_from.append(self._get_object_template_kind(node_kind=inherited)) + for inherited in node.inherit_from: + if inherited in need_template_kinds: + template.inherit_from.append(self._get_object_template_kind(node_kind=inherited)) for node_attr in node.attributes: if node_attr.unique or node_attr.read_only: @@ -2038,7 +2035,7 @@ def generate_object_template_from_node( attr = AttributeSchema( optional=node_attr.optional if is_autogenerated_subtemplate else True, - **node_attr.model_dump(exclude=["id", "unique", "optional", "read_only", "inherited"]), + **node_attr.model_dump(exclude=["id", "unique", "optional", "read_only"]), ) template.attributes.append(attr) diff --git a/backend/tests/unit/core/schema_manager/test_manager_schema.py b/backend/tests/unit/core/schema_manager/test_manager_schema.py index 88e27a8515..4a093080ba 100644 --- a/backend/tests/unit/core/schema_manager/test_manager_schema.py +++ b/backend/tests/unit/core/schema_manager/test_manager_schema.py @@ -3068,6 +3068,18 @@ async def test_manage_object_templates_with_component_relationships(): # Optional value in component template should match original's assert attr.optional == template_attr.optional + # Verify the generic by checking its attributes and relationships + test_interface_template = schema_branch.get(name=f"Template{TestKind.INTERFACE}", duplicate=False) + assert test_interface_template.is_generic_schema + test_interface = schema_branch.get(name=TestKind.INTERFACE, duplicate=False) + assert test_interface.is_generic_schema + for attr in test_interface.attributes: + template_attr = test_interface_template.get_attribute(name=attr.name) + assert attr.optional == template_attr.optional + for rel in test_interface.relationships: + template_rel = test_interface_template.get_relationship(name=rel.name) + assert template_rel.peer == f"Template{rel.peer}" + # Verify when a node is marked as absent ABSENT_VIRTUAL_INTERFACE = copy.deepcopy(DEVICE_SCHEMA) ABSENT_VIRTUAL_INTERFACE.get(name=TestKind.VIRTUAL_INTERFACE).state = HashableModelState.ABSENT diff --git a/backend/tests/unit/core/test_manager_node.py b/backend/tests/unit/core/test_manager_node.py index 307fc10687..8f403359d0 100644 --- a/backend/tests/unit/core/test_manager_node.py +++ b/backend/tests/unit/core/test_manager_node.py @@ -1,3 +1,5 @@ +import copy + import pytest from infrahub_sdk.uuidt import UUIDT @@ -13,6 +15,8 @@ from infrahub.core.timestamp import Timestamp from infrahub.database import InfrahubDatabase from infrahub.exceptions import NodeNotFoundError +from tests.constants import TestKind +from tests.helpers.schema import DEVICE_SCHEMA async def test_get_one_attribute(db: InfrahubDatabase, default_branch: Branch, criticality_schema): @@ -248,6 +252,26 @@ async def test_get_one_by_hfid( await NodeManager.get_one_by_hfid(db=db, hfid=["Not", "Dog"], kind=dog_schema.kind, raise_on_error=True) +async def test_get_by_hfid_with_invalid_hfid(db: InfrahubDatabase, branch: Branch): + schema = copy.deepcopy(DEVICE_SCHEMA) + # Change device schema to add a HFID + schema.nodes[0].human_friendly_id = ["name__value"] + schema.nodes[0].generate_template = False + + registry.schema.register_schema(schema=schema, branch=branch.name) + + device = await Node.init(db=db, schema=TestKind.DEVICE, branch=branch) + await device.new(db=db, name="device-01", manufacturer="Juniper", height=1, weight=6, airflow="Front to rear") + await device.save(db=db) + device_hfid = await device.get_hfid(db=db) + + with pytest.raises(NodeNotFoundError, match=r"does not have a HFID defined"): + await NodeManager.get_one_by_hfid(db=db, branch=branch, kind=TestKind.INTERFACE_HOLDER, hfid=device_hfid) + + with pytest.raises(NodeNotFoundError, match=r"HFID does not contain the same number of elements"): + await NodeManager.get_one_by_hfid(db=db, branch=branch, kind=TestKind.DEVICE, hfid=device_hfid + ["foo"]) + + async def test_get_many(db: InfrahubDatabase, default_branch: Branch, criticality_low, criticality_medium): nodes = await NodeManager.get_many(db=db, ids=[criticality_low.id, criticality_medium.id]) assert len(nodes) == 2 diff --git a/backend/tests/unit/graphql/test_mutation_create.py b/backend/tests/unit/graphql/test_mutation_create.py index f229013d87..0356d68972 100644 --- a/backend/tests/unit/graphql/test_mutation_create.py +++ b/backend/tests/unit/graphql/test_mutation_create.py @@ -1272,6 +1272,64 @@ async def test_create_without_object_template( assert not device_template_node +async def test_create_sub_object_template_by_hfid( + db: InfrahubDatabase, default_branch: Branch, register_core_models_schema: SchemaBranch, branch: Branch +): + registry.schema.register_schema(schema=DEVICE_SCHEMA, branch=branch.name) + + device_template = await Node.init(db=db, schema=f"Template{TestKind.DEVICE}", branch=branch) + await device_template.new( + db=db, template_name="MX204 Router", manufacturer="Juniper", height=1, weight=6, airflow="Front to rear" + ) + await device_template.save(db=db) + device_template_hfid = await device_template.get_hfid(db=db) + + template = await registry.manager.get_one_by_hfid( + db=db, branch=branch, kind=f"Template{TestKind.INTERFACE_HOLDER}", hfid=device_template_hfid + ) + assert device_template.id == template.id + + query = """ + mutation CreateTemplateInterfaceWithHFID($template_name: String!, $device_template_hfid: [String!], $name: String!, $phys_type: String!) { + TemplateTestingPhysicalInterfaceCreate( + data:{ + template_name: {value: $template_name} + device: {hfid: $device_template_hfid} + name: {value: $name} + phys_type: {value: $phys_type} + } + ) { + ok + object { + id + } + } + } + """ + + gql_params = await prepare_graphql_params(db=db, include_subscription=False, branch=branch) + result = await graphql( + schema=gql_params.schema, + source=query, + context_value=gql_params.context, + root_value=None, + variable_values={ + "template_name": "MX204 et-0/0/0", + "device_template_hfid": device_template_hfid, + "name": "et-0/0/0", + "phys_type": "QSFP28 (100GE)", + }, + ) + assert not result.errors + + node_id = result.data["TemplateTestingPhysicalInterfaceCreate"]["object"]["id"] + assert node_id + + interface_template = await registry.manager.get_one(db=db, branch=branch, id=node_id) + assert interface_template + assert (await interface_template.device.get_peer(db=db)).id == device_template.id + + # These tests have been moved at the end of the file to avoid colliding with other and breaking them diff --git a/changelog/+properhfiderrormessage.changed.md b/changelog/+properhfiderrormessage.changed.md new file mode 100644 index 0000000000..0f7fb7d6b6 --- /dev/null +++ b/changelog/+properhfiderrormessage.changed.md @@ -0,0 +1 @@ +Raise more accurate error when trying to lookup a node by HFID when the schema does not have a HFID or the number of elements does not match \ No newline at end of file diff --git a/changelog/+webhook.added.md b/changelog/+webhook.added.md new file mode 100644 index 0000000000..91a9f10a78 --- /dev/null +++ b/changelog/+webhook.added.md @@ -0,0 +1 @@ +Enable node select in the webhook form to quickly choose the node kind \ No newline at end of file diff --git a/changelog/6287.fixed.md b/changelog/6287.fixed.md new file mode 100644 index 0000000000..e8cd6d3aef --- /dev/null +++ b/changelog/6287.fixed.md @@ -0,0 +1 @@ +Add attributes and relationships to generic templates to ensure proper GraphQL schema generation \ No newline at end of file diff --git a/changelog/6301.fixed.md b/changelog/6301.fixed.md new file mode 100644 index 0000000000..638c378cf0 --- /dev/null +++ b/changelog/6301.fixed.md @@ -0,0 +1 @@ +Fix node lookup by its HFID with a generic template kind \ No newline at end of file diff --git a/docs/docs/guides/computed-attributes.mdx b/docs/docs/guides/computed-attributes.mdx index 569ffeb104..2102a42049 100644 --- a/docs/docs/guides/computed-attributes.mdx +++ b/docs/docs/guides/computed-attributes.mdx @@ -66,9 +66,9 @@ nodes: :::note -In your template, you can utilize most of the built-in filters provided by Jinja2! +In your template, you can utilize most of the **filters** provided by **Jinja2** and **Netutils**! -For more information, please consult the [Schema Reference](../reference). +For more information, please consult the [SDK Templating Reference]($(base_url)python-sdk/reference/templating). ::: diff --git a/docs/docs/guides/jinja2-transform.mdx b/docs/docs/guides/jinja2-transform.mdx index e04bda1272..736182f2a4 100644 --- a/docs/docs/guides/jinja2-transform.mdx +++ b/docs/docs/guides/jinja2-transform.mdx @@ -180,6 +180,14 @@ end {% endif %} ``` +:::note + +In your template, you can utilize most of the **filters** provided by **Jinja2** and **Netutils**! + +For more information, please consult the [SDK Templating Reference]($(base_url)python-sdk/reference/templating). + +::: + ## 4. Create a .infrahub.yml file In the .infrahub.yml file you define what transforms you have in your repository that you want to make available for Infrahub. diff --git a/frontend/app/src/config/constants.tsx b/frontend/app/src/config/constants.tsx index 3598eb062d..224e992b86 100644 --- a/frontend/app/src/config/constants.tsx +++ b/frontend/app/src/config/constants.tsx @@ -57,6 +57,9 @@ export const NUMBER_POOL_OBJECT = "CoreNumberPool"; export const TASK_OBJECT = "InfrahubTask"; +export const STANDARD_WEBHOOK_OBJECT = "CoreStandardWebhook"; +export const CUSTOM_WEBHOOK_OBJECT = "CoreCustomWebhook"; + export const MENU_EXCLUDELIST = [ "CoreChangeComment", "CoreChangeThread", diff --git a/frontend/app/src/entities/nodes/object/ui/object-table/cells/node-kind-cell.tsx b/frontend/app/src/entities/nodes/object/ui/object-table/cells/node-kind-cell.tsx new file mode 100644 index 0000000000..4734354b44 --- /dev/null +++ b/frontend/app/src/entities/nodes/object/ui/object-table/cells/node-kind-cell.tsx @@ -0,0 +1,14 @@ +import { useSchema } from "@/entities/schema/ui/hooks/useSchema"; +import { Badge } from "@/shared/components/ui/badge"; + +export function NodeKindCell({ kind }: { kind: string }) { + const { schema } = useSchema(kind); + + if (!schema) return "-"; + + return ( +