NetStacksNetStacks

Variable Overrides

Enterprise

Customize stack variable values per device using NetStacks' per-service shared/override precedence chain to tailor each deployment.

Overview

Controller-only feature

Stacks, stack instances, and per-device overrides are part of the NetStacks Controller (Enterprise tier). The endpoints on this page live under the Controller config API at /api/config.

When deploying a stack to multiple devices, most variable values are the same across all targets — the NTP server, SNMP community, and syslog destination are typically identical for every device in a site. But some values must be unique per device: hostnames, management IPs, interface descriptions, and physical locations.

Device overrides let you customize specific variable values for individual target devices without changing the shared values. On a stack instance, overrides are stored in the device_overrides field. At deploy time the Controller merges that field together with variable_values (shared values) and target_devices into a per-service render context, then runs the Jinja2 engine once per device.

When to use overrides vs shared values

Put a value in shared values when it is the same for every device the service targets (for example ntp_server). Put a value in a device override when one or a few devices need a different value for an otherwise-shared variable, or for naturally per-device values like hostname and management_ip.

How It Works

Variable Precedence Chain

When the Controller renders a template for a specific device, it builds a merged variable context. The merge applies values in increasing priority, so later layers win on key collisions:

  1. Service defaults (lowest) — values shared by every device in a service, stored as that service's shared values. (Internally these seed the merge before shared values are overlaid.)
  2. Shared values — the instance's variable_values for that service. Applies to all devices in the service unless overridden.
  3. Device override (highest) — a value in device_overrides for that specific device. If present, it wins over the shared value.

Keys that are not supplied by any of those layers fall back to whatever the Jinja2 template itself provides — typically the default() filter, for example {{ timezone | default('UTC') }}. If a key is referenced with no value and no default, rendering fails.

Resolution is per service, not flat

Precedence is resolved per service within the stack, not from a single flat instance-wide map. Each service has its own device list, shared values, and overrides. A variable named vlan_id in service 0 is independent of vlan_id in service 1.

Merge Behavior at Render Time

For each service in stack order, the resolver iterates the service's target devices. For each device it merges service defaults, then shared values, then that device's override map, and passes the result to the Jinja2 engine. The output is one rendered config per (service, device) pair.

Per-Service Structure

A stack instance stores three JSON fields, and the deploy/render pipeline also accepts a single combined per-service object. Both shapes are valid — the Controller detects which one you sent.

Three-field shape (instance storage)

Each field is a JSON object keyed by service index (a string:"0", "1", …). Within a service:

  • target_devices["0"] — array of device UUID strings the service targets.
  • variable_values["0"] — shared values for that service (a flat variable-to-value map).
  • device_overrides["0"] — a map keyed by device UUID, each entry a variable-to-value map applied only to that device.
Override keys are device UUIDs, not service indexes

Inside device_overrides["0"] the inner keys are the device UUIDs from target_devices["0"], exactly as they appear there. A UUID that is not in the service's device list is stored but never matched, so its override has no effect.

Combined per-service shape (deploy/render payload)

When you preview or deploy, the resolver expects one object keyed by service index, each holding devices, shared_vars, and device_vars. This is the merge of the three instance fields:

per-service-config.jsonjson
{
  "0": {
    "devices": ["7b3c...001", "7b3c...002"],
    "shared_vars": { "ntp_server": "10.0.0.10" },
    "device_vars": {
      "7b3c...001": { "hostname": "access-sw-01.nyc" },
      "7b3c...002": { "hostname": "access-sw-02.nyc" }
    }
  }
}

Step-by-Step Guide

Walk through configuring per-device overrides for a three-switch service where each switch needs a unique hostname, management IP, and rack location while sharing NTP, SNMP, and syslog settings.

Step 1: Set shared values for the service

On the stack instance, set the values that are identical for every device in service 0. These go in variable_values["0"]:

  • ntp_server: 10.0.0.10
  • snmp_community: netstack-ro
  • syslog_server: 10.0.0.20
  • timezone: America/New_York

Step 2: Add per-device overrides

In device_overrides["0"], add one entry per device UUID with only the variables that differ:

  • access-sw-01: hostname = access-sw-01.nyc, management_ip = 10.1.1.1, snmp_location = NYC-Floor3-Rack1
  • access-sw-02: hostname = access-sw-02.nyc, management_ip = 10.1.1.2, snmp_location = NYC-Floor3-Rack4
  • access-sw-03: hostname = access-sw-03.nyc, management_ip = 10.1.1.3, snmp_location = NYC-Floor4-Rack2
Override only what differs

You do not need to repeat shared values in device overrides. Only specify variables whose values differ. If all switches use the same NTP server, leave ntp_server out of the overrides — it resolves from the shared values.

Step 3: Preview rendered output (dry run)

Before deploying, call the render endpoint to preview the rendered configuration for each device. It returns one job per (service, device) without touching any device. See Code Examples for the exact request.

Step 4: Deploy

Deploy the instance with POST /api/config/instances/:id/deploy. The Controller merges the three fields, renders per device, and creates a deployment.

Step 5: Change an override later

To change an override after deployment, PUT the instance with the updated device_overrides, then deploy the instance again. Updating the instance only changes stored values; the new config is not pushed until you redeploy.

Code Examples

Instance fields: shared values + overrides

Three access switches in service 0 share NTP/SNMP/syslog settings with per-device hostnames, IPs, and rack locations. Note the per-service indexing and the device-UUID keys inside device_overrides:

instance-overrides.jsonjson
{
  "target_devices": {
    "0": ["7b3c...001", "7b3c...002", "7b3c...003"]
  },
  "variable_values": {
    "0": {
      "ntp_server": "10.0.0.10",
      "snmp_community": "netstack-ro",
      "syslog_server": "10.0.0.20",
      "timezone": "America/New_York"
    }
  },
  "device_overrides": {
    "0": {
      "7b3c...001": {
        "hostname": "access-sw-01.nyc",
        "management_ip": "10.1.1.1",
        "snmp_location": "NYC-Floor3-Rack1"
      },
      "7b3c...002": {
        "hostname": "access-sw-02.nyc",
        "management_ip": "10.1.1.2",
        "snmp_location": "NYC-Floor3-Rack4"
      },
      "7b3c...003": {
        "hostname": "access-sw-03.nyc",
        "management_ip": "10.1.1.3",
        "snmp_location": "NYC-Floor4-Rack2"
      }
    }
  }
}

Preview rendered output (dry run)

Post a per-service config to the stack render endpoint to preview output without deploying. The response contains a jobs array, one entry per (service, device), each with the device's rendered_config:

render-preview.shbash
curl -s -X POST \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  "https://controller.example.net/api/config/stacks/$STACK_ID/render" \
  -d '{
    "variable_values": {
      "0": {
        "devices": ["7b3c...001", "7b3c...002"],
        "shared_vars": { "ntp_server": "10.0.0.10" },
        "device_vars": {
          "7b3c...001": { "hostname": "access-sw-01.nyc", "snmp_location": "NYC-Floor3-Rack1" },
          "7b3c...002": { "hostname": "access-sw-02.nyc", "snmp_location": "NYC-Floor3-Rack4" }
        }
      }
    }
  }'
render-response.jsonjson
{
  "jobs": [
    {
      "device_id": "7b3c...001",
      "device_name": "access-sw-01",
      "template_name": "snmp-baseline",
      "service_order": 0,
      "target_path": "running-config",
      "operation": "merge",
      "config_format": "cli",
      "rendered_config": "hostname access-sw-01.nyc\nsnmp-server community netstack-ro RO\nsnmp-server location NYC-Floor3-Rack1\nntp server 10.0.0.10"
    },
    {
      "device_id": "7b3c...002",
      "device_name": "access-sw-02",
      "template_name": "snmp-baseline",
      "service_order": 0,
      "rendered_config": "hostname access-sw-02.nyc\nsnmp-server community netstack-ro RO\nsnmp-server location NYC-Floor3-Rack4\nntp server 10.0.0.10"
    }
  ]
}

Change an override, then redeploy

Update the instance's device_overrides with PUT /api/config/instances/:id, then deploy the instance. (The PUT body only needs the fields you are changing.)

update-and-deploy.shbash
# 1. Update the stored overrides for one device
curl -s -X PUT \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  "https://controller.example.net/api/config/instances/$INSTANCE_ID" \
  -d '{
    "device_overrides": {
      "0": {
        "7b3c...001": {
          "hostname": "access-sw-01.nyc",
          "management_ip": "10.1.1.1",
          "snmp_location": "NYC-Floor3-Rack2-Moved"
        }
      }
    }
  }'

# 2. Redeploy the instance to push the change
curl -s -X POST \
  -H "Authorization: Bearer $TOKEN" \
  "https://controller.example.net/api/config/instances/$INSTANCE_ID/deploy"
Rolling back

If a redeploy produces the wrong result, roll the deployment back with POST /api/config/deployments/:id/rollback using the deployment ID returned by the deploy call.

Questions & Answers

Q: What are variable overrides in NetStacks?
A: Variable overrides are per-device customizations of variable values on a stack instance. They let you set different values for specific devices while keeping shared values for everyone else. They are stored in the instance's device_overrides field, organized per service index and keyed by device UUID.
Q: What is the precedence order for variable resolution?
A: Per service, values merge in increasing priority: service defaults, then shared values (variable_values), then the device override (device_overrides) — the device override wins. Keys absent from all three fall back to the Jinja2 template's own value, typically a default() filter.
Q: Is the override map keyed by service or by device?
A: Both. The outer key is the service index ("0", "1", …). Inside each service, the keys are the device UUIDs of that service's targets, and each value is a variable-to-value map for that device.
Q: How do I change an override after deployment?
A: Send PUT /api/config/instances/:id with the updated device_overrides, then redeploy with POST /api/config/instances/:id/deploy. Updating the instance only changes stored values; redeploying pushes them. There is no separate "modify deployment" endpoint.
Q: How do I preview what a device will get before deploying?
A: Call POST /api/config/stacks/:id/render with a per-service variable_values object. The response's jobs array gives the resolved rendered_config for each device without touching any device.
Q: What happens if an override references a non-existent variable?
A: It is stored but has no effect on output. Jinja2 only uses variables that appear in template expressions, so an unreferenced key is silently ignored. No error is raised.
Q: Can I remove an override?
A: Yes. PUT the instance with the variable omitted from the device's override map, then redeploy. The variable falls back to the shared value or the template default.

Troubleshooting

Override not applied to a device

Confirm the device UUID used as a key inside device_overrides["<service>"] exactly matches a UUID in that service's target_devices list, and that you used the correct service index. UUIDs are case-sensitive. A key that is not in the service's device list is stored but never matched. Use the render preview to confirm.

Override placed under the wrong service

Overrides are scoped per service. If you put a device's override under service 0 but the template that uses the variable runs in service 1, the override will not appear in service 1's output. Place each override under the same service index as the template that consumes it.

Unexpected rendered value

Trace the precedence chain: a device override beats a shared value, which beats a service default, which beats the template's default(). Run POST /api/config/stacks/:id/render and inspect each job's rendered_config to see which layer supplied the value.

Typo in an override variable name

A misspelled variable name (for example hostnme instead of hostname) is not matched by Jinja2, so the override has no effect and the correctly-spelled variable uses its shared value or template default. Double-check names against the stack's extracted variables.

Override change did not reach the device

Updating an instance with PUT only changes stored values. Redeploy with POST /api/config/instances/:id/deploy to push the change.

Path collision error

If two services in the stack target the same path on the same device, resolution fails with a path-collision error before any override is applied. Adjust the services' target paths so each (device, path) pair is rendered by only one service.

Learn more about related NetStacks features:

  • Variables & Extraction — How Jinja2 variables are extracted, and how the default() filter provides fallbacks
  • Creating Stacks — Build stacks with services, target devices, and variable values
  • Stack Instances — Deploy, monitor, and manage live stack deployments
  • Rendering & Preview — How Jinja2 templates render with a merged variable context
  • Stack Templates — Define the services that make up a stack blueprint
  • Stacks API — Full reference for stack, instance, deploy, render, and rollback endpoints