NetStacksNetStacks

Rendering & Preview

TeamsEnterprise

How to preview rendered template output, validate Jinja2 syntax, diff against running configs, and use dry-run mode before deploying.

Overview

Rendering is the process of combining a Jinja2 template with variable values to produce the final device configuration text. NetStacks provides two endpoints that let you validate and preview templates before deploying to production devices:

  • Validate (POST /plugins/stacks/admin/templates/validate) — Check Jinja2 syntax without requiring variable values. This catches unclosed tags, invalid filter names, and structural errors
  • Render (POST /plugins/stacks/admin/templates/render) — Combine the template with a set of variable values and return the rendered configuration text

Beyond preview, the deployment system supports a dry_run mode that renders the template, generates the device commands, and computes a diff against the running config — all without pushing any changes to the device. This gives you complete visibility into what will change before you commit.

How It Works

The Rendering Pipeline

When you render a template, NetStacks follows this sequence:

  1. Load template source — The raw Jinja2 text is retrieved from the database using the template_id
  2. Merge variables — Shared variables, per-device variables, and any overrides are combined into a single context dictionary. Per-device overrides take highest priority
  3. Render in sandbox — The Jinja2 engine processes the template in a sandboxed environment, substituting variables, evaluating conditionals, and expanding loops
  4. Return output — The rendered string is returned in a RenderTemplateResponse with a single rendered field containing the final configuration text

Validation (Syntax Check)

The validation endpoint parses the template source without rendering it. This means it does not need variable values — it only checks that the Jinja2 syntax is structurally correct. Use this to catch errors early, before you have variable values ready.

Dry-Run Deployment

The deploy action supports a dry_run: true flag. In dry-run mode, NetStacks:

  1. Renders the template with the provided variables
  2. Generates the device commands that would be pushed
  3. Computes a diff_summary comparing the rendered config against the device's current running config
  4. Returns all of this without making any changes to the device
Always Dry-Run First

For production deployments, run with dry_run: true first to review the exact commands and diff. Then deploy with dry_run: false once you have confirmed the changes are correct.

Step-by-Step Guide

Step 1: Select a Template

Open the template you want to preview in the template editor. You can also render any template by its ID through the API.

Step 2: Provide Variable Values

Enter values for each variable. In the UI, the variable panel shows all extracted variables with input fields. Through the API, pass a variables object in the render request body.

Step 3: Preview the Rendered Output

Click Preview (or call the render endpoint). The rendered configuration text appears in the output panel. Review it line by line to confirm the output matches your expectations.

Step 4: Compare Against Running Config

When deploying through a stack, use dry-run mode to see a diff between the rendered template and the device's current running configuration. The diff highlights lines that will be added, removed, or changed.

Step 5: Validate Syntax

Before saving a template, use the validation endpoint to check for syntax errors. Validation runs independently of variable values, so you can check syntax even before you know what values you will use.

Validation vs Rendering

Validation checks syntax only (are tags balanced? are filter names valid?). Rendering goes further by substituting actual values and producing output. A template can pass validation but still fail rendering if required variables are missing.

Step 6: Deploy with Confidence

After confirming the preview and dry-run results, deploy with dry_run: false. The rendered configuration is pushed to the target device.

Code Examples

BGP Neighbor Template

bgp-neighbor.j2jinja2
{# BGP Neighbor Configuration - Cisco IOS #}
router bgp {{ bgp_asn }}
{% for neighbor in bgp_neighbors %}
 neighbor {{ neighbor.ip }} remote-as {{ neighbor.remote_asn }}
 neighbor {{ neighbor.ip }} description {{ neighbor.description | default('BGP Peer') }}
{% if neighbor.route_map_in is defined %}
 neighbor {{ neighbor.ip }} route-map {{ neighbor.route_map_in }} in
{% endif %}
{% if neighbor.route_map_out is defined %}
 neighbor {{ neighbor.ip }} route-map {{ neighbor.route_map_out }} out
{% endif %}
{% endfor %}

Variable Values

bgp-variables.jsonjson
{
  "bgp_asn": "65001",
  "bgp_neighbors": [
    {
      "ip": "10.0.0.2",
      "remote_asn": "65002",
      "description": "Transit - Provider-A",
      "route_map_in": "RM-PROVIDER-A-IN",
      "route_map_out": "RM-PROVIDER-A-OUT"
    },
    {
      "ip": "10.0.0.6",
      "remote_asn": "65003",
      "description": "Peering - IX-East"
    }
  ]
}

Rendered Output

bgp-rendered.txttext
router bgp 65001
 neighbor 10.0.0.2 remote-as 65002
 neighbor 10.0.0.2 description Transit - Provider-A
 neighbor 10.0.0.2 route-map RM-PROVIDER-A-IN in
 neighbor 10.0.0.2 route-map RM-PROVIDER-A-OUT out
 neighbor 10.0.0.6 remote-as 65003
 neighbor 10.0.0.6 description Peering - IX-East

Validate Template Syntax

validate-template.shbash
POST /plugins/stacks/admin/templates/validate
Content-Type: application/json
Authorization: Bearer <token>

{
  "template_source": "router bgp {{ bgp_asn }}\n{% for neighbor in bgp_neighbors %}\n neighbor {{ neighbor.ip }} remote-as {{ neighbor.remote_asn }}\n{% endfor %}"
}

# Response (success):
{
  "valid": true
}

Render Template via API

render-template.shbash
POST /plugins/stacks/admin/templates/render
Content-Type: application/json
Authorization: Bearer <token>

{
  "template_id": "tmpl_8f3a2b1c",
  "variables": {
    "bgp_asn": "65001",
    "bgp_neighbors": [
      { "ip": "10.0.0.2", "remote_asn": "65002", "description": "Transit" }
    ]
  }
}

# Response:
{
  "rendered": "router bgp 65001\n neighbor 10.0.0.2 remote-as 65002\n neighbor 10.0.0.2 description Transit"
}

Dry-Run Deployment

dry-run-deploy.shbash
POST /plugins/stacks/admin/actions/deploy
Content-Type: application/json
Authorization: Bearer <token>

{
  "stack_instance_id": "si_abc123",
  "device_id": "dev_xyz789",
  "credential_id": "cred_456",
  "template_id": "tmpl_8f3a2b1c",
  "variables": { "bgp_asn": "65001", "bgp_neighbors": [] },
  "platform": "cisco_ios",
  "dry_run": true
}

# Response:
{
  "success": true,
  "commands": [
    "router bgp 65001"
  ],
  "pushed": false,
  "push_output": null,
  "error": null,
  "diff_summary": "+ router bgp 65001"
}

Questions & Answers

Q: How do I preview a template before deploying?
A: Use the render endpoint (POST /plugins/stacks/admin/templates/render) with the template ID and a set of variable values. The response contains the rendered field with the complete configuration text. In the UI, fill in variable values and click Preview.
Q: Can I render a template without deploying to any device?
A: Yes. The render endpoint only produces text output — it does not connect to any device. For deployment preview with a diff against the running config, use the deploy action with dry_run: true.
Q: What is dry-run mode?
A: Dry-run mode (dry_run: true on the deploy action) renders the template, generates the device commands, and computes a diff against the running configuration — all without pushing any changes to the device. The response includes the command list and a diff_summary showing what would change.
Q: How does the diff preview work?
A: During a dry-run deployment, NetStacks connects to the target device, reads its current running configuration, renders the template, and computes the difference. The diff_summary in the response shows added lines (prefixed with +) and removed lines (prefixed with -).
Q: Can I validate template syntax without providing variable values?
A: Yes. The validation endpoint (POST /plugins/stacks/admin/templates/validate) only checks Jinja2 syntax structure. It verifies that tags are balanced, filter names are valid, and the template parses correctly. It does not need variable values because it does not render the template.
Q: What errors does validation catch?
A: Validation catches structural Jinja2 errors: unclosed tags (missing endif, endfor, endmacro), unknown filter names, invalid syntax inside expressions, and malformed block structures. It does not catch runtime errors like undefined variables, which only appear during rendering.

Troubleshooting

UndefinedError during rendering

The template references a variable that was not provided in the render request. Check the variables object in your request against the template's extracted variable list. Either add the missing variable or use the default filter in the template.

Template validates successfully but renders incorrectly

Validation only checks syntax, not logic. If the rendered output is wrong, review your variable values:

  • Are list variables actually arrays? A string where a list is expected will not loop correctly
  • Are object properties spelled correctly? {{ vlan.id }} will fail if the object has ID instead of id
  • Are boolean values actual booleans? The string "true" is truthy in Jinja2, but it is not the same as the boolean true

Diff shows unexpected changes

If the dry-run diff shows more changes than expected, the issue is usually whitespace or ordering differences between the rendered template and the running config. Jinja2 templates may produce trailing spaces or extra blank lines that differ from the device's formatted output. Use whitespace control ({%- -%}) to trim unwanted whitespace.

Render endpoint returns empty output

An empty rendered string usually means all content was inside conditional blocks that evaluated to false. Check that your boolean variables have the correct values and that {% if %} conditions match what you intend.

Related pages for the rendering and deployment workflow: