Stack Templates
EnterpriseGroup ordered Jinja2 services into one atomic NetStacks stack with shared, per-device, and API-sourced variables, confirmed-commit rollback, and MOP generation.
Overview
Config stacks are part of the NetStacks Controller (Enterprise tier). They are managed under the Controller's config routes and are not available in the standalone terminal client.
A stack (the ConfigStack record) groups multiple Jinja2 configuration templates — called services — into a single deployable unit. Instead of pushing NTP, SNMP, and Syslog templates one at a time, you define one stack that renders and pushes the whole baseline as an ordered operation.
A stack ties together three things:
- Services — an ordered list of
StackServiceentries. Each references an existing Jinja2 template bytemplate_id, has an integerorder, and may carry its ownvariablesandapi_variables. - Variable config — a
variable_configmap that classifies each variable'sscope(sharedvsper_device) and optionally binds it to an external API source. - Atomicity — the
atomicflag (defaulttrue) makes the stack all-or-nothing: if any device fails, succeeded devices are rolled back.
Use a stack whenever two or more related templates must deploy to the same devices and succeed or fail together. A single template push does not give you ordered execution or cross-device atomic rollback — a stack does.
The ConfigStack Model
A stack is stored as a ConfigStack. Knowing the real field names makes the API and JSON examples below unambiguous.
ConfigStack fields
id/org_id- UUIDs. The stack and the organization it belongs to.
name- Display name, for example
Monitoring Baseline. description- Optional free text describing what the stack deploys.
atomic- Boolean, defaults to
true. Controls all-or-nothing rollback (see below). services- JSON array of
StackServiceobjects (template_id,order,variables,api_variables). variable_config- JSON object mapping each variable name to its scope and optional API source.
deployment_procedure- Optional JSON describing pre-checks, post-checks, and rollback steps used when deploying with a generated MOP.
shared- Boolean. When set, the stack is marked shared (visible beyond its owning context).
created_by/created_at/updated_at- Ownership and timestamps.
A stack record does not store a device count, a shared_variables array, or a per_device_variables array. Variable scope lives entirely inside variable_config.
StackService fields
template_id- UUID (as a string) of an existing Jinja2 config template.
order- Integer. Services render and push in ascending order.
variables- Map of service-level default variable values (lowest precedence).
api_variables- Map of per-service API-sourced variables (each a
StackApiVariable).
A service does not have its own name field — it is identified by the template it references and its order.
Atomic Stacks & Rollback
The atomic flag (default true) is a core capability of stacks. It changes what happens when a deployment partially fails.
Atomic stacks (all-or-nothing)
When atomic is true and at least one target device fails, the deployer initiates a rollback for the devices that succeeded. The deployment log records a warning such as "Atomic stack: initiating rollback for succeeded devices". The mechanism depends on the device transport:
- NETCONF with confirmed commit — the deployer simply skips the confirm step, so the device auto-rolls-back when the confirmation timer expires.
- Other transports — succeeded devices are restored from the backup config captured before the push.
Confirmed-commit drift protection
For confirmed-commit devices, NetStacks runs post-push validation. If validation detects drift on an atomic stack, the deployer skips the confirm, logging "Skipping confirm: validation detected drift on atomic stack (auto-rollback will occur)". The device then auto-reverts on its commit timer — no manual rollback needed.
Non-atomic stacks
When atomic is false, devices are independent: successful devices keep their new config and the deployment is simply marked failed if any device failed. Use non-atomic only when partial application is acceptable.
The auto-rollback above is the atomic safety net during a deployment. You can also explicitly roll back a completed or failed deployment from backups via POST /api/config/deployments/{id}/rollback (only completed or failed deployments can be rolled back, and only devices that captured a backup config).
Variable Config
Variable scope and API bindings are stored in the stack's variable_config object — a map of variable name to a small config object. Each entry can include:
scope—"shared"(same value for every device, the default) or"per_device"(a distinct value per target device).resource_id,method,path,json_path,description— present only when the variable is sourced from an external API (see API Variables below).
{
"ntp_server_primary": { "scope": "shared" },
"snmp_community": { "scope": "shared" },
"syslog_server": { "scope": "shared" },
"hostname": { "scope": "per_device" },
"management_ip": { "scope": "per_device" }
}Variable precedence at render time
When a service renders for a device, values merge in this order (later wins): service defaults (StackService.variables) → shared values for the deployment → per-device overrides for that device. So a per-device override always beats a shared value, which beats a service default.
API Variables
An API variable auto-populates a value from an external HTTP service at deploy time. It does not carry a full URL. Instead it references a pre-configured API Resource (which supplies the base_url, auth, SSL verification, and timeout) and adds a relative path.
StackApiVariable fields
resource_id- UUID of a configured API Resource. The resource's
base_urland credentials are used. method- HTTP method. Defaults to
GET. path- Path appended to the resource
base_url. Supports Jinja2 device templating such as{{ device.name }}or{{ device.serial_number }}. json_path- JSONPath expression used to extract the value from the response body.
description- Optional human-readable note.
At resolve time the Controller builds the full URL as {base_url}/{rendered_path}, applies the API Resource's auth (bearer token, basic, or API-key header), and reads the value at json_path. The device context exposes fields including device.id, device.name, device.hostname, device.host, device.site, device.platform, device.manufacturer, device.model, device.device_type, device.serial_number, and device.asset_tag.
If a path contains {{ device, the variable is treated as device-specific even without an explicit "scope": "per_device" — the API is called once per target device.
Deployment Procedure & MOPs
A stack may carry an optional deployment_procedure JSON object. When you deploy an instance with a MOP (POST /api/config/instances/{id}/deploy-with-mop), NetStacks generates a Method of Procedure plan from this object. Without a deployment procedure, that endpoint returns an error.
The procedure is read for the following keys:
pre_checks— array of steps run before the config change.post_checks— array of steps run after the change.rollback_steps— array of steps appended for rollback.default_control_mode— default execution mode (overridable per request).on_post_check_failure— behavior on a failed post-check (defaults topause).pause_after_pre_checks— boolean, defaults totrue.pause_after_changes— boolean, defaults tofalse.default_ai_autonomy_level— integer, defaults to2.
The generator inserts a synthetic __config_deploy__ change step (the actual stack push) between the pre-checks and post-checks, then links a MOP plan and execution to the target devices.
Code Examples
Create a stack with three ordered services
The create payload (CreateConfigStack) takes name, description, atomic (defaults to true if omitted), and services:
{
"name": "Monitoring Baseline",
"description": "NTP + SNMP + Syslog baseline for all network devices",
"atomic": true,
"services": [
{
"template_id": "8f3a2b1c-0001-0000-0000-000000000001",
"order": 1,
"variables": { "ntp_server_secondary": "10.0.0.124" },
"api_variables": {}
},
{
"template_id": "8f3a2b1c-0002-0000-0000-000000000002",
"order": 2,
"variables": {},
"api_variables": {
"snmp_community": {
"resource_id": "c1d2e3f4-aaaa-bbbb-cccc-000000000001",
"method": "GET",
"path": "/v1/secret/data/network/snmp",
"json_path": "$.data.data.ro_community",
"description": "SNMP RO community from the secrets API resource"
}
}
},
{
"template_id": "8f3a2b1c-0003-0000-0000-000000000003",
"order": 3,
"variables": {},
"api_variables": {}
}
]
}Classify variable scope (PUT variable_config)
After creating the stack, update variable_config to mark each variable's scope. API-sourced variables also appear here so the resolve endpoint can find them:
{
"variable_config": {
"ntp_server_primary": { "scope": "shared" },
"ntp_server_secondary": { "scope": "shared" },
"syslog_server": { "scope": "shared" },
"hostname": { "scope": "per_device" },
"management_ip": { "scope": "per_device" },
"snmp_community": {
"scope": "shared",
"resource_id": "c1d2e3f4-aaaa-bbbb-cccc-000000000001",
"method": "GET",
"path": "/v1/secret/data/network/snmp",
"json_path": "$.data.data.ro_community"
}
}
}Per-device API variable using device templating
This variable fetches each device's management IP from a CMDB API Resource, keyed by serial number. Because path references device.serial_number, it is resolved once per device:
{
"management_ip": {
"scope": "per_device",
"resource_id": "c1d2e3f4-aaaa-bbbb-cccc-000000000002",
"method": "GET",
"path": "/api/v1/devices/{{ device.serial_number }}/mgmt-ip",
"json_path": "$.result.address",
"description": "Per-device mgmt IP from CMDB"
}
}Jinja2 template referenced by service order 1
! NTP Configuration for {{ hostname }}
ntp server {{ ntp_server_primary }} prefer
ntp server {{ ntp_server_secondary }}
ntp source {{ management_ip }}
ntp authenticate
ntp trusted-key 1
clock timezone UTCDeployment procedure for deploy-with-mop
{
"deployment_procedure": {
"default_control_mode": "auto_run",
"pause_after_pre_checks": true,
"pause_after_changes": false,
"on_post_check_failure": "pause",
"pre_checks": [
{ "command": "show ntp status", "description": "Capture pre-change NTP state" }
],
"post_checks": [
{ "command": "show ntp associations", "description": "Verify NTP peers are reachable" }
],
"rollback_steps": [
{ "command": "no ntp server", "description": "Remove NTP servers on rollback" }
]
}
}REST API
Stack routes live under the Controller config router, mounted at /api/config. All calls require an authenticated Controller session/token.
# List all stacks
curl -s -H "Authorization: Bearer $TOKEN" \
"https://controller.example.net/api/config/stacks" | jq .
# Create a stack
curl -s -X POST -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
--data @create-stack.json \
"https://controller.example.net/api/config/stacks" | jq .
# Get one stack by UUID
curl -s -H "Authorization: Bearer $TOKEN" \
"https://controller.example.net/api/config/stacks/$STACK_ID" | jq .
# Update variable_config / atomic / deployment_procedure / shared
curl -s -X PUT -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
--data @update-variable-config.json \
"https://controller.example.net/api/config/stacks/$STACK_ID" | jq .
# Delete a stack
curl -s -X DELETE -H "Authorization: Bearer $TOKEN" \
"https://controller.example.net/api/config/stacks/$STACK_ID"Related stack endpoints
# Dry-run render (no deploy) — returns render jobs
POST /api/config/stacks/{id}/render
# Resolve API variables against target devices
POST /api/config/stacks/{id}/resolve-variables
# Deploy a saved stack instance
POST /api/config/instances/{id}/deploy
# Deploy an instance and generate a MOP from deployment_procedure
POST /api/config/instances/{id}/deploy-with-mop
# Roll back a completed or failed deployment from backups
POST /api/config/deployments/{id}/rollbackQuestions & Answers
- Q: What is a stack template?
- A: A stack (the
ConfigStackrecord) groups multiple Jinja2 templates — services — into one deployable unit. It defines which templates deploy, in whatorder, how variables are scoped (variable_config), and whether the deployment isatomic. - Q: How is variable scope stored?
- A: In the stack's
variable_configmap. Each variable name maps to an object with ascopeof"shared"or"per_device". There are no separateshared_variables/per_device_variablesarrays, and there is nodevice_countfield on a stack. - Q: What makes a stack atomic?
- A: The
atomicboolean (defaulttrue). If any target device fails, succeeded devices are rolled back — for confirmed-commit (NETCONF) devices by skipping the confirm so the device auto-reverts on its timer, otherwise by restoring the pre-change backup. Confirmed-commit drift also triggers auto-rollback on atomic stacks. - Q: How do API variables work?
- A: Each API variable references an API Resource by
resource_id(which supplies thebase_urland auth) plus a relativepath,method, andjson_path. There is no fullendpointURL field. Thepathsupports{{ device.* }}templating, in which case the value is fetched per device. - Q: How do I deploy a stack with a MOP?
- A: Define a
deployment_procedureon the stack (pre-checks, post-checks, rollback steps), then callPOST /api/config/instances/{id}/deploy-with-mop. NetStacks generates a MOP plan with your checks around a synthetic__config_deploy__step. - Q: What is the correct API base path?
- A:
/api/config/stacks(list/create),/api/config/stacks/{id}(get/update/delete), plus the render, resolve-variables, instance deploy, and deployment rollback routes shown above. - Q: Does a service have its own name?
- A: No. A
StackServicehas onlytemplate_id,order,variables, andapi_variables. It is identified by the template it references and its order.
Troubleshooting
deploy-with-mop returns "Stack has no deployment procedure"
The MOP deploy path requires a deployment_procedure on the stack. Add one via PUT /api/config/stacks/{id} (see the example above), or use the plain POST /api/config/instances/{id}/deploy endpoint instead.
API variable fails to resolve
The resolve step calls each variable's API Resource. Verify:
- The
resource_idis a valid API Resource UUID in the same org. - The resource
base_urlis reachable from the Controller and itsverify_ssl/timeout_secssettings are appropriate. - The auth type and stored credentials (bearer / basic / API-key header) are valid.
- The
json_pathmatches the actual response body. - For per-device paths, that
{{ device.* }}fields used inpathare populated on each target device.
Rollback is rejected or does nothing
POST /api/config/deployments/{id}/rollback only accepts deployments in completed or failed status, and it only restores devices that captured a backup config during the push. If no backups exist, rollback reports that it is not possible.
Variable name collisions between services
If two templates in the same stack use the same variable name but expect different values, they receive the same value at render time. Rename one to be specific (for example ntp_server vs dns_server).
Conflicting target paths
If two services would write the same config path on the same device, the stack resolver reports a conflict. Give the templates distinct target_path values or split them into separate stacks.
Related Features
Learn more about related NetStacks features:
- Template Basics — create the Jinja2 templates that stack services reference by
template_id - Variables & Extraction — how variables are extracted from templates and what feeds
variable_config - Creating Stacks — build a stack and assign target devices
- Stack Instances — save target devices and variable values, then deploy
- Variable Overrides — per-device override precedence within a deployment
- Deployment History — review past deployments and trigger rollbacks
- Method of Procedures — the MOPs generated by deploy-with-mop from a stack's deployment procedure
- Stacks API — full REST reference for stacks and deployments