Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions backend/infrahub/core/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
71 changes: 34 additions & 37 deletions backend/infrahub/core/schema/schema_branch.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down Expand Up @@ -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,
)
)

Expand Down Expand Up @@ -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",
Expand All @@ -1994,51 +1990,52 @@ 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:
continue

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)

Expand Down
12 changes: 12 additions & 0 deletions backend/tests/unit/core/schema_manager/test_manager_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
24 changes: 24 additions & 0 deletions backend/tests/unit/core/test_manager_node.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import copy

import pytest
from infrahub_sdk.uuidt import UUIDT

Expand All @@ -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):
Expand Down Expand Up @@ -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
Expand Down
58 changes: 58 additions & 0 deletions backend/tests/unit/graphql/test_mutation_create.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
1 change: 1 addition & 0 deletions changelog/+properhfiderrormessage.changed.md
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions changelog/+webhook.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Enable node select in the webhook form to quickly choose the node kind
1 change: 1 addition & 0 deletions changelog/6287.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add attributes and relationships to generic templates to ensure proper GraphQL schema generation
1 change: 1 addition & 0 deletions changelog/6301.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix node lookup by its HFID with a generic template kind
4 changes: 2 additions & 2 deletions docs/docs/guides/computed-attributes.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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).
:::
Expand Down
8 changes: 8 additions & 0 deletions docs/docs/guides/jinja2-transform.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
3 changes: 3 additions & 0 deletions frontend/app/src/config/constants.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -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 (
<div className="flex items-center gap-2">
{schema.label} <Badge>{schema.namespace}</Badge>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { AttributeType } from "@/entities/nodes/getObjectItemDisplayValue";
import { ColorCell } from "@/entities/nodes/object/ui/object-table/cells/color-cell";
import { DropdownCell } from "@/entities/nodes/object/ui/object-table/cells/dropdown-cell";
import { NodeKindCell } from "@/entities/nodes/object/ui/object-table/cells/node-kind-cell";
import { UrlCell } from "@/entities/nodes/object/ui/object-table/cells/url-cell";
import { ATTRIBUTE_KIND } from "@/entities/schema/constants";
import { AttributeKind, AttributeSchema } from "@/entities/schema/types";
Expand Down Expand Up @@ -38,6 +39,10 @@ export function TableAttributeCell({ attributeSchema, attributeData }: TableAttr
case ATTRIBUTE_KIND.IP_HOST:
case ATTRIBUTE_KIND.IP_NETWORK:
case ATTRIBUTE_KIND.TEXTAREA: {
if (attributeSchema.name === "node_kind") {
return <NodeKindCell kind={attributeData.value} />;
}

return <span className="truncate">{attributeData.value}</span>;
}
case ATTRIBUTE_KIND.URL: {
Expand Down
Loading
Loading