Skip to content

Configuration Versioning & Lifecycle

Every namespace’s dial plan lives in one YAML document, and that document is versioned like a source repository, not edited like a switch database. If you are coming from a legacy platform where translations tables are mutated live, row by row, this page is the mental-model shift; the day-to-day commands build on it.

The model: immutable versions, two pointers

Section titled “The model: immutable versions, two pointers”

Nothing ever edits a configuration in place. Every write — a full upload, a patch, an edited sub-tree — produces a new, immutable version, identified by a ULID (sortable by creation time). Old versions remain readable forever.

Two named pointers move across that history:

  • latest — the newest stored version: your drafting head.
  • running — the version calls are actually processed against. It advances only when someone applies a version.

The gap between them is the feature: you can stage a change, validate it, diff it, have a colleague review it, tag it — and only then promote it. A namespace whose latest is ahead of running is in the normal “change staged, not yet live” state.

In git terms: a version is a commit, latest is your branch head, apply is the deploy, and tags are tags. Two deliberate differences from git: history is linear (no branches — see concurrency below), and the deploy step is first-class in the API rather than an external CD pipeline, with its own permission (config:apply) so drafting and promoting can be held by different people (the editor / config-publisher split on the RBAC page).

Field Meaning
version The ULID — also the ETag everywhere this version appears
parent_version The version this one was derived from (provenance)
tag Optional human label, unique per namespace, reassignable (golden, pre-migration)
description Free-form operator notes
config_schema_version The configuration-schema revision it was written against

Two documents are stored per version: the YAML you authored, and the compiled effective configuration — your document after parent inheritance is merged in at store time. Apply renders the effective document, which is why a parent’s change reaches you only when you store (and apply) a new version. Both documents are retrievable for any version: the authored view is the default, and ?view=effective (or vsscli config get --effective) returns the merged one — the view to read when the switch behaves per a rule you never wrote.

Tags matter operationally because any API field or CLI argument that accepts a version accepts a tag (and the latest / running aliases). Keeping a golden tag on your last known-good version turns “roll back” into a one-liner.

Concurrency: declared bases and linear history

Section titled “Concurrency: declared bases and linear history”

Two people editing one document is the classic way to silently lose a change: A downloads, B downloads, A uploads, B uploads — A’s work vanishes. The API makes that impossible by requiring every write to declare the version it is based on:

  • A full upload (PUT) must carry the base in an If-Match header (the ETag from the version you fetched) or a ?base= parameter.
  • A patch (PATCH) names its source version in the URL.

If your declared base is no longer the current latest, the write is refused with 412 Precondition Failed — and the response tells you what latest has become (in the body’s current_latest field and the ETag header), so you can re-fetch, reconcile, and retry. Nothing is guessed and nothing is merged behind your back.

A consequence worth internalizing: because every accepted write is based on the latest it replaced, history is a single straight line. There are no branches to reconcile later; the 412 is where a would-have-been branch is stopped and turned into an explicit rebase by the losing writer.

For patch-shaped edits there is an opt-in shortcut: ?rebase=true on a PATCH replays your operations against the current latest instead of returning 412, and reports which version it rebased onto. This is a replay, not a merge — each operation is re-applied to the newer document, and an operation that no longer makes sense there (a path that vanished, a test op that no longer matches) still fails the patch. Use JSON Patch test operations when you need “apply only if the value is still what I saw” semantics; use plain 412-handling when a human should look before retrying. vsscli config push --rebase does the human version: on a 412 it shows you a diff against the new latest and asks.

  1. Dry-run validate — submit a document to the validate endpoint and get the full error list without storing anything. Two variants: one validates in your namespace’s context (inheritance merged in, so references to inherited entries resolve), one validates a standalone document with no context.
  2. Store-time validation — every write runs the same compiler; an invalid document is refused outright, so an invalid version cannot exist. Whatever latest points at is always applyable in the validation sense.
  3. Apply-time guards and render — apply re-checks two cheap guards (below), then converges the switch onto the version; a render failure rolls back (below).

Validation failures return a structured problem document whose validation_errors field lists every finding, not just the first — fix them in one pass.

POST …/config/{version}/apply promotes a version to running. Unless you pass force=true, two guards run first:

  • Descendancy — the version must be running itself or a descendant of it (reachable by walking parent_version back to running). This stops you from accidentally promoting a stale line of history — e.g. an old version fetched by a script racing a colleague’s newer apply. Applying an older version (a revert) trips this guard by design; that’s what force — or better, roll-forward (below) — is for.
  • Schema version — the version must have been stored under the same configuration-schema revision the server currently runs. After a platform upgrade that bumps the schema, old versions refuse to apply until re-stored (or forced) so they are never reinterpreted under changed rules.

Apply is a convergence, not a restart. The switch diffs the new effective configuration against what is currently rendered and touches only the deltas: unchanged trunks, devices, and registrations are left alone (a re-apply does not drop active calls or force re-registration), new resources are provisioned, and removed resources are torn down (including the voicemail-cleanup behavior governed by cleanup-mailboxes). Re-applying the currently running version is a supported repair operation — a no-op promote that re-renders the side state.

Apply is transactional at the namespace level. If rendering the new version fails partway (for example, a trunk IP turns out to be claimed by another namespace at the last moment):

  • the switch automatically re-renders the previous running version and reports the failure with the running version unchanged — you are not left half-migrated;
  • only if that rollback also fails do you get an explicit “namespace may be non-operational” error, which is your cue to re-apply a known-good version (or call support).

A version that failed to apply remains stored; nothing is deleted by a failed promotion.

Two ways to get back to a previous version, with a clear preference:

Roll forward (recommended). Fetch the old version’s YAML, push it as a new version (optionally tagging it revert-of-…), and apply that. History stays linear, the descendancy guard stays happy, provenance shows exactly what happened, and the next engineer’s latest is also the configuration that is actually running.

Force-apply the old version. apply with force=true skips the descendancy guard and promotes the old version directly. Faster in an incident, but it leaves running pointing behind latest — anyone who now edits latest is editing a document that does not match what’s live, and their eventual apply will silently re-deploy whatever was staged above your revert point. If you force-revert, follow up with a roll-forward push so latest and running agree again.

Two lifecycle rules from the inheritance model are worth restating here because they shape rollout ordering:

  • A child’s version compiles against its parent’s running config at the moment the child stores — so tree-wide changes deploy top-down: parent applies first, then each child re-stores and applies.
  • Your own store can be rejected by constraints you don’t author: the parent’s grant (permitted realms/edge IPs, linkage bindings) and the partition’s allow-lists are enforced at validate/store time.
Item Value
Version identifier ULID (time-sortable); doubles as the ETag
Version list page size 100 (cursor-paginated)
Document size limit 10 MB per configuration document
PUT precondition required — If-Match or ?base= (412 on staleness)
Patch format RFC 6902 JSON Patch, JSON or YAML encoded
Force / rebase toggles ?force=true / ?rebase=true (or X-Force / X-Rebase headers)
Tag unique per namespace, reassignable; valid anywhere a version is accepted