YANG Version Support for IOS-XE Writers
Overview
IOS-XE YANG models change across firmware versions. The NAC (Network as Code) writers are authored against a baseline version (currently 17.18.2 on C9300-24P). Earlier versions (17.15, 17.16) diverge in several ways:
- Module prefixes required on augmented JSON body keys
- Different YANG list-key names (e.g.
ordering-seqvsseq) - Empty-leaf encoding (
[null]instead of booleantrue) - Different container shapes (flat ↔ nested, keyed list ↔ container)
- Different RESTCONF paths and envelope keys
Architecture
Future direction: ygot-typed bindings
The current architecture uses a hand-maintained runtime override table
(yang_version_override_table.go) to handle YANG model differences across
IOS-XE versions. This approach works well for a bounded set of managed
families but does not scale linearly as the number of families, versions,
and divergence types grows — each new combination requires a new table
entry and associated body-transform function.
In a future release, CVK anticipates a shift to
ygot-generated typed Go bindings
per IOS-XE release family. ygot would generate structs directly from the
Cisco YANG models and provide compile-time safety, automated serialisation,
and structural validation without manual ElementMap entries. The
validation boundary at
internal/drivers/iosxe/configdriver/validation is already designed as the
seam for this migration: ygot validators would plug in there and validate
the device-facing YANG payload, leaving the public NetAsCode intent model
unchanged.
What this means for operators and contributors today:
- The public IOSXEConfig.spec.source NetAsCode intent shape will remain
stable across this migration — you will not need to change your intent
YAML when the internal transport layer moves to ygot.
- New family contributions should continue using the override table pattern
described in this document until the ygot migration lands.
- Track progress and participate in design discussion on
GitHub Discussions.
Intent, Translation, Validation Boundary
CVK keeps the public IOSXEConfig.spec.source payload in the canonical
NetAsCode shape. That shape is intentionally release-stable: operators should
not have to change day-0 YAML just because IOS-XE moved a leaf or renamed an
augmented container between 17.18 and 26.01.
The release-specific boundary is therefore:
NetAsCode intent
-> family writer
-> version override / body transform
-> IOS-XE YANG JSON op
-> validation boundary
-> RESTCONF / NETCONF / gNMI transport
internal/drivers/iosxe/configdriver/validation owns the validation boundary.
It runs after writers emit transport.Op values and before the engine calls
Apply. The default StructuralValidator checks common op invariants
(path scope, JSON envelope shape, body presence) and hosts narrow release
profiles for known divergences. IOS-XE 26.01 currently validates that
ip_domain.name has been translated to
name-container.name-no-vrf before mutation.
This is the seam where generated ygot/ytypes validators belong once per-release config model packages are available. ygot should validate the device-facing YANG payload, not replace the public NetAsCode intent model.
Validation is deployment-policy controlled:
| Mode | Effect |
|---|---|
CONFIG_YANG_VALIDATION=disabled |
default; no validation gate |
CONFIG_YANG_VALIDATION=warn |
log validation failures and continue |
CONFIG_YANG_VALIDATION=strict |
fail the family before mutation |
The Helm value is config.yangValidationMode. The controller propagates it to
per-device VK pods so aggregator mode and per-pod mode use the same policy.
Stable NetAsCode Import Contract
For customer migrations from Terraform-based NetAsCode, CVK imports the
resolved NetAsCode IOS-XE model and records provenance in
IOSXEConfig.spec.modelSource. This keeps the public intent model stable while
the writer layer handles IOS-XE release differences.
spec:
modelSource:
format: netascode-iosxe
resolved: true
exporter: terraform-iosxe-nac-iosxe write_model_file
resolved: false is rejected by the resolver. If a future NetAsCode release
adds fields, CVK should first accept the canonical data model shape, then
either translate the field for the target YANG release or report it as an
unsupported family/field through migration tooling and validation. The CRD
should not fork by IOS-XE release.
Override Table (yang_version_override_table.go)
The declarative override table is the central registry of version-
conditional YANG behaviour. Each entry targets a (family, version_range)
and describes mutations to apply to the YANG wire representation.
┌──────────────────────────────────────┐
│ Override Table Entry │
│ │
│ Family: "route_map" │
│ MinVersion: [17, 0] │
│ MaxVersion: [17, 18] │
│ ElementMap: { old → new } │
│ NestedYANGInnerOverride: "..." │
│ YANGPathOverride: "..." │
│ EnvelopeKeyOverride: "..." │
│ EmptyLeaves: ["prefer"] │
│ BodyTransform: func(...) │
└──────────────────────────────────────┘
The table is resolved once at startup via ResolveForVersion(major, minor).
Writers query the resolved state at Diff/Apply time:
| Query Function | Purpose |
|---|---|
IsLegacyVersion(fam) |
Is the device on a pre-baseline version? |
ResolvedYANGPath(fam, default) |
Version-correct RESTCONF path |
ResolvedEnvelopeKey(fam, default) |
Version-correct JSON envelope |
ResolvedNestedYANGInner(fam, default) |
Version-correct inner list name |
ApplyOverrideToBody(body, override) |
Full transform chain |
Custom Version-Branched Writers
Three families have structural YANG differences too deep for the override table alone:
| Family | Mechanism |
|---|---|
bgp |
Keyed list (17.16) vs container (17.18+) |
prefix_list |
Flat compound-keyed (17.16) vs nested (17.18+) |
ip_community_list |
Deprecated groupings (17.16) vs community-list-entry (17.18+) |
These writers use IsLegacyVersion() to select their code path, and the
override table provides path/envelope selection. Transform logic is per-writer.
YAML 1.1 Boolean Key Fix
sigs.k8s.io/yaml (YAML 1.1) interprets bare no as boolean false,
which becomes map key "false" in map[string]any. This is fixed globally
by intent.FixYAML11BoolKeys(), which runs once on the fully-merged
configuration tree at the end of Resolver.Resolve().
Adding Support for a New IOS-XE Version
1. Identify divergences
Compare the YANG schemas between the new version and the baseline:
# Download YANG models from the device
curl -k https://<device>/restconf/.well-known/host-meta
# Or use Cisco's published YANG repo:
# https://github.com/YangModels/yang/tree/main/vendor/cisco/xe
Use pyang --tree-diff or yanglint to diff specific modules:
pyang -f tree --tree-path /native/ip/prefix-list \
Cisco-IOS-XE-native-17.16.yang \
Cisco-IOS-XE-native-17.18.yang
For RESTCONF, probe the device directly:
# GET the schema for a specific path
curl -k -u admin:pass \
'https://<device>/restconf/data/Cisco-IOS-XE-native:native/ip/prefix-list' \
-H 'Accept: application/yang-data+json'
2. Classify the divergence
| Type | Fix Mechanism |
|---|---|
| Module prefix needed on element key | ElementMap in override table |
| Different YANG path or envelope | YANGPathOverride / EnvelopeKeyOverride |
| Boolean → empty leaf encoding | EmptyLeaves in override table |
| Simple container shape change | BodyTransform function |
| Key field rename | KeyFieldOverride or ElementMap |
| Deep structural difference | Custom writer with IsLegacyVersion() |
3. Add the table entry
Add an entry to yang_version_override_table.go:
{
Family: "new_family",
MinVersion: [2]int{17, 0},
MaxVersion: [2]int{17, 20}, // exclusive upper bound
ElementMap: map[string]string{
"baseline-name": "Cisco-IOS-XE-module:version-name",
},
},
4. Write tests
- Add transform tests to
version_transforms_test.go - Add override resolution tests to
yang_version_overrides_test.go - Add structural validation coverage for any release-specific YANG payload
shape in
internal/drivers/iosxe/configdriver/validation - Run integration tests on a device of that version
5. Verify
# Unit tests
go test ./internal/drivers/iosxe/configdriver/writers/ -v
go test ./internal/drivers/iosxe/configdriver/validation -v
# Full build
go build ./...
# Integration tests (on lab device)
pytest tests/test_monolithic_router.py -v
Reference: CiscoDevNet/terraform-provider-iosxe
The Terraform provider for IOS-XE
is an excellent reference for correct YANG paths and data structures. It
was instrumental in discovering the 17.16 prefix-lists (plural) path
with the flat compound-keyed prefixes[name, no] list.
Reference: Known YAML 1.1 Boolean Keys
YAML 1.1 treats these bare tokens as booleans when used as map keys:
| Token | JSON key | Canonical name |
|---|---|---|
no |
"false" |
"no" |
yes |
"true" |
(not used in IOS-XE schema) |
on |
"true" |
(not used in IOS-XE schema) |
off |
"false" |
(not used in IOS-XE schema) |
The yaml11BoolKeyMap in intent/yaml11_fix.go maps only keys that
actually appear in the IOS-XE netascode schema. Add entries there if
future schema keys collide with YAML 1.1 booleans.