NetStacksNetStacks

Stack Templates

Enterprise

Group ordered Jinja2 services into one atomic NetStacks stack with shared, per-device, and API-sourced variables, confirmed-commit rollback, and MOP generation.

Overview

Controller-only feature

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 StackService entries. Each references an existing Jinja2 template by template_id, has an integer order, and may carry its own variables and api_variables.
  • Variable config — a variable_config map that classifies each variable's scope (shared vs per_device) and optionally binds it to an external API source.
  • Atomicity — the atomic flag (default true) makes the stack all-or-nothing: if any device fails, succeeded devices are rolled back.
When to use a stack

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 StackService objects (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.
No device_count field

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.

Manual rollback is separate

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).
variable_config.jsonjson
{
  "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_url and 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.

Path templating implies per-device

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 to pause).
  • pause_after_pre_checks — boolean, defaults to true.
  • pause_after_changes — boolean, defaults to false.
  • default_ai_autonomy_level — integer, defaults to 2.

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:

create-stack.jsonjson
{
  "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:

update-variable-config.jsonjson
{
  "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:

per-device-api-variable.jsonjson
{
  "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-config.j2jinja2
! 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 UTC

Deployment procedure for deploy-with-mop

deployment-procedure.jsonjson
{
  "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.

stacks-api.shbash
# 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

stack-endpoints.txthttp
# 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}/rollback

Questions & Answers

Q: What is a stack template?
A: A stack (the ConfigStack record) groups multiple Jinja2 templates — services — into one deployable unit. It defines which templates deploy, in what order, how variables are scoped (variable_config), and whether the deployment is atomic.
Q: How is variable scope stored?
A: In the stack's variable_config map. Each variable name maps to an object with a scope of "shared" or "per_device". There are no separate shared_variables / per_device_variables arrays, and there is no device_count field on a stack.
Q: What makes a stack atomic?
A: The atomic boolean (default true). 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 the base_url and auth) plus a relative path, method, and json_path. There is no full endpoint URL field. The path supports {{ 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_procedure on the stack (pre-checks, post-checks, rollback steps), then call POST /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 StackService has only template_id, order, variables, and api_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_id is a valid API Resource UUID in the same org.
  • The resource base_url is reachable from the Controller and its verify_ssl / timeout_secs settings are appropriate.
  • The auth type and stored credentials (bearer / basic / API-key header) are valid.
  • The json_path matches the actual response body.
  • For per-device paths, that {{ device.* }} fields used in path are 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.

Learn more about related NetStacks features: