Build a Jinja2 Transformation
By the end of this tutorial you will have built a working Jinja2 Transformation end-to-end: loaded a small network-device schema, created a few sample devices, written a GraphQL query that filters by device name, written a Jinja template that renders a configuration snippet from the result, registered it in .infrahub.yml, tested it locally with infrahubctl render, added the repository to Infrahub, and called the render API. You'll leave with a device_config_transform you can call against any device by name.
For conceptual background, see Transformations. For a recipe-form how-to without the running example, see Write a Jinja2 transformation.
Within Infrahub a Transformation is defined in an external repository. However, during development and troubleshooting it is easiest to start from your local computer and run the render using infrahubctl render.
The tutorial follows these steps:
- Identify the relevant data you want to extract from the database using a GraphQL query, that can take an input parameter to filter the data
- Write a Jinja2 file that uses the GraphQL query to read information from the system and render the data into a new format
- Create an entry for the Jinja2 Transformation within an
.infrahub.ymlfile - Create a Git repository
- Test the Transformation rendering with
infrahubctl - Add the repository to Infrahub as an external repository
- Validate that the Transformation works using the render API endpoint
1. Loading a schema​
This tutorial uses a very simplistic network device model. The rendered template won't be very useful on its own — the goal is to show how Jinja rendering works. Once you've mastered the basics you'll be ready to create more advanced templates.
---
version: "1.0"
nodes:
- name: Device
namespace: Network
display_label: "{{ name__value }}"
attributes:
- name: name
kind: Text
label: Name
optional: false
unique: true
- name: description
kind: Text
label: Description
optional: true
Store the schema as a YAML file on your local disk, and load the schema into Infrahub using the following command
infrahubctl schema load /path/to/schema.yml
More information on loading schema files into Infrahub can be found in the schema import guide.
2. Creating a query to collect the desired data​
As the first step we need some data in the database to actually query.
Create three devices, called "switch1", "switch2", "switch3", either using the frontend or by submitting three GraphQL mutations as per below (swapping out the name each time).
mutation CreateDevice {
NetworkDeviceCreate(
data: {name: {value: "switch1"}, description: {value: "This is device switch1"}}
) {
ok
object {
id
}
}
}
The next step is to create a query that returns the data we created above. The rest of this tutorial assumes that the following query will return a response similar to the response below the query.
query DeviceQuery {
NetworkDevice {
edges {
node {
name {
value
}
description {
value
}
}
}
}
}
Response to the query:
{
"data": {
"NetworkDevice": {
"edges": [
{
"node": {
"name": {
"value": "switch1"
},
"description": {
"value": "This is device switch1"
}
}
},
{
"node": {
"name": {
"value": "switch2"
},
"description": {
"value": "This is device switch2"
}
}
},
{
"node": {
"name": {
"value": "switch3"
},
"description": {
"value": "This is device switch3"
}
}
}
]
}
}
}
While it's possible to create a Transformation that targets all of these devices — for example to create a report — the goal here is to focus on one device at a time. Modify the query above to take an input parameter so that we can filter the result.
For proper artifact detection, your query must target a unique node using a unique attribute or ID. This ensures Infrahub only regenerates the necessary artifacts instead of regenerating all artifacts unnecessarily.
Requirements for a valid single-target query:
- Must filter on a unique identifier (ID or unique attribute like
name) - Must use a required variable, for example,
$name: String! - Must use exact match filters, for example,
name__value: $name, not list filters, for example,name__values: $name
Valid example:
query BuiltinTag($name: String!) {
BuiltinTag(name__value: $name) {
edges { node { id } }
}
}
Invalid examples (will cause excessive artifact generation):
No filter at all:
query BuiltinTag {
BuiltinTag {
edges { node { id } }
}
}
Filtering on a non-unique attribute:
query BuiltinTag($description: String!) {
BuiltinTag(description__value: $description) {
edges { node { id } }
}
}
To learn more about single-target queries and why they are important, see the GraphQL topic.
Create a local directory on your computer.
mkdir device_config_render
Then save the below query as a text file named device_config.gql.
query DeviceQuery($name: String!) {
NetworkDevice(name__value: $name) {
edges {
node {
name {
value
}
description {
value
}
}
}
}
}
The query requires an input parameter called $name that will refer to the name of each device. When we want to query for device switch1, the input variables to the query would look like this:
{
"name": "switch1"
}
3. Create the Jinja template​
The next step is to create the actual Jinja Template file. Create a file called device_config.j2.
{% if data.NetworkDevice.edges and data.NetworkDevice.edges is iterable %}
{% for device in data["NetworkDevice"]["edges"] %}
{% set device_name = device.node.name.value %}
{% set device_description = device.node.description.value %}
hostname {{ device_name }}
description "{{ device_description }}"
end
{% endfor %}
{% endif %}
In your template, you can utilize most of the filters provided by Jinja2 and Netutils!
For more information, see the SDK Templating Reference.