Skip to content

feat: Cisco IOS-XR gNMI support via module-anchored SetRequest encoding#442

Open
steiler wants to merge 3 commits into
mainfrom
ciscoiosxrd2
Open

feat: Cisco IOS-XR gNMI support via module-anchored SetRequest encoding#442
steiler wants to merge 3 commits into
mainfrom
ciscoiosxrd2

Conversation

@steiler
Copy link
Copy Markdown
Collaborator

@steiler steiler commented Jun 1, 2026

Adds first-class support for Cisco IOS-XR / XRd devices as gNMI targets by introducing a materialization layer that owns all NOS-specific encoding, keeping the gNMI transport driver a thin wire-execution layer.

Companion proto change: sdcio/sdc-protos#120 (adds device_profile field)

┌─────────────────────────────────────────────────────────────────────────┐
│  BEFORE                                                                 │
│                                                                         │
│  applyIntent                                                            │
│      │                                                                  │
│      ▼                                                                  │
│  TargetSource adapter  ◄── api.Entry wrapped here                       │
│      │                                                                  │
│      ▼                                                                  │
│  Target.Set(TargetSource)                                               │
│      │                                                                  │
│      ▼                                                                  │
│  gNMI driver  ── serializes internally ──► SetRequest                   │
│                                             └─ Update{origin:"", /}     │
│                                                  └─ entire JSON blob    │
│                                                                         │
│  (IOS-XR rejects: unknown native YANG keys in OpenConfig context)       │
└─────────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────────┐
│  AFTER                                                                  │
│                                                                         │
│  applyIntent                                                            │
│      │  api.Entry + replace flag                                        │
│      ▼                                                                  │
│  materialize.BuildPlan(entry, device-profile)                           │
│      │                                                                  │
│      ├─[generic / proto]──────────────► GnmiSetPlan (single update)     │
│      │                                                                  │
│      └─[cisco-ios-xr + json/json_ietf]                                  │
│              │                                                          │
│              ▼                                                          │
│         permodule encoder                                               │
│         group children by ModuleName                                    │
│              │                                                          │
│              ▼                                                          │
│         GnmiSetPlan                                                     │
│           ├─ Update{origin:"Cisco-IOS-XR-ip-static-cfg", /router}       │
│           ├─ Update{origin:"Cisco-IOS-XR-ifmgr-cfg",     /interfaces}   │
│           └─ ...one per module...                                       │
│                                                                         │
│      ▼                                                                  │
│  Target.Set(SouthboundSetPlan)   ◄── typed plan, no NOS logic here      │
│      │                                                                  │
│      ▼                                                                  │
│  gNMI driver (transport only) ──────► single SetRequest                 │
│                                        └─ all module Updates atomic     │
└─────────────────────────────────────────────────────────────────────────┘

Core additions:

  • New pkg/datastore/target/materialize package: translates api.Entry + device-profile into a typed SouthboundSetPlan before any transport call
  • New pkg/datastore/target/gnmi/permodule encoder: groups config tree children by YANG module name, emits one Update per module with Path.origin set to the full module name (e.g. Cisco-IOS-XR-ip-static-cfg) — all updates in a single SetRequest to preserve gNMI atomicity
  • New pkg/datastore/target/types discriminated plan type (GnmiSetPlan / NetconfSetPlan) replacing the TargetSource abstraction on the Set path
  • device-profile: cisco-ios-xr config field on SBI with closed-set validation: unknown profiles fail at load time; cisco-ios-xr + netconf is rejected; cisco-ios-xr + proto is accepted without IOS-XR shaping

Transport/driver changes:

  • gNMI driver (gnmi.go) simplified to pure transport: receives a pre-built GnmiSetPlan and executes it, with no knowledge of device-profile or module grouping
  • Replace transactions encoded as per-module delete + per-module update pairs in one SetRequest (gNMI replace field not used, consistent with existing behavior)
  • Delete paths carry origin resolved from schema metadata or schema-client lookup for choice-case entries

Removals / cleanup:

  • TargetSource interface and its adapters retired from the Set path (targetsource.go, target_source_replace.go, entryoutputadapter.go deleted)
  • applyIntent now passes raw api.Entry + replace flag to materialize instead of wrapping in a TargetSource adapter

Test coverage added:

  • permodule encoder: multi-module tree assertions (update count, origin values, path elements, JSON body shape, replace delete emission)
  • materialize: BuildPlan assertions for both generic and IOS-XR profiles
  • config: validation rejection of unknown profiles and netconf+ios-xr
  • noop and gNMI get path: adapted to new plan-based interface

Adds first-class support for Cisco IOS-XR / XRd devices as gNMI targets
by introducing a materialization layer that owns all NOS-specific encoding,
keeping the gNMI transport driver a thin wire-execution layer.

Companion proto change: sdcio/sdc-protos#120 (adds `device_profile` field)

┌─────────────────────────────────────────────────────────────────────────┐
│  BEFORE                                                                 │
│                                                                         │
│  applyIntent                                                            │
│      │                                                                  │
│      ▼                                                                  │
│  TargetSource adapter  ◄── api.Entry wrapped here                      │
│      │                                                                  │
│      ▼                                                                  │
│  Target.Set(TargetSource)                                               │
│      │                                                                  │
│      ▼                                                                  │
│  gNMI driver  ── serializes internally ──► SetRequest                  │
│                                             └─ Update{origin:"", /}    │
│                                                  └─ entire JSON blob    │
│                                                                         │
│  (IOS-XR rejects: unknown native YANG keys in OpenConfig context)      │
└─────────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────────┐
│  AFTER                                                                  │
│                                                                         │
│  applyIntent                                                            │
│      │  api.Entry + replace flag                                        │
│      ▼                                                                  │
│  materialize.BuildPlan(entry, device-profile)                           │
│      │                                                                  │
│      ├─[generic / proto]──────────────► GnmiSetPlan (single update)    │
│      │                                                                  │
│      └─[cisco-ios-xr + json/json_ietf]                                 │
│              │                                                          │
│              ▼                                                          │
│         permodule encoder                                               │
│         group children by ModuleName                                    │
│              │                                                          │
│              ▼                                                          │
│         GnmiSetPlan                                                     │
│           ├─ Update{origin:"Cisco-IOS-XR-ip-static-cfg", /router}      │
│           ├─ Update{origin:"Cisco-IOS-XR-ifmgr-cfg",     /interfaces}  │
│           └─ ...one per module...                                       │
│                                                                         │
│      ▼                                                                  │
│  Target.Set(SouthboundSetPlan)   ◄── typed plan, no NOS logic here     │
│      │                                                                  │
│      ▼                                                                  │
│  gNMI driver (transport only) ──────► single SetRequest                │
│                                        └─ all module Updates atomic     │
└─────────────────────────────────────────────────────────────────────────┘

Core additions:
- New `pkg/datastore/target/materialize` package: translates `api.Entry` +
  device-profile into a typed `SouthboundSetPlan` before any transport call
- New `pkg/datastore/target/gnmi/permodule` encoder: groups config tree
  children by YANG module name, emits one `Update` per module with
  `Path.origin` set to the full module name (e.g. `Cisco-IOS-XR-ip-static-cfg`)
  — all updates in a single `SetRequest` to preserve gNMI atomicity
- New `pkg/datastore/target/types` discriminated plan type (`GnmiSetPlan` /
  `NetconfSetPlan`) replacing the `TargetSource` abstraction on the Set path
- `device-profile: cisco-ios-xr` config field on SBI with closed-set
  validation: unknown profiles fail at load time; `cisco-ios-xr` + netconf
  is rejected; `cisco-ios-xr` + proto is accepted without IOS-XR shaping

Transport/driver changes:
- gNMI driver (`gnmi.go`) simplified to pure transport: receives a pre-built
  `GnmiSetPlan` and executes it, with no knowledge of device-profile or
  module grouping
- Replace transactions encoded as per-module delete + per-module update pairs
  in one `SetRequest` (gNMI `replace` field not used, consistent with
  existing behavior)
- Delete paths carry `origin` resolved from schema metadata or schema-client
  lookup for choice-case entries

Removals / cleanup:
- `TargetSource` interface and its adapters retired from the Set path
  (`targetsource.go`, `target_source_replace.go`, `entryoutputadapter.go`
  deleted)
- `applyIntent` now passes raw `api.Entry` + replace flag to materialize
  instead of wrapping in a `TargetSource` adapter

Test coverage added:
- `permodule` encoder: multi-module tree assertions (update count, origin
  values, path elements, JSON body shape, replace delete emission)
- `materialize`: `BuildPlan` assertions for both generic and IOS-XR profiles
- `config`: validation rejection of unknown profiles and netconf+ios-xr
- `noop` and gNMI `get` path: adapted to new plan-based interface
@steiler steiler requested a review from a team as a code owner June 1, 2026 10:24
steiler and others added 2 commits June 2, 2026 14:24
Update comment for DeviceProfile to clarify usage.
ops.ToJson/ToJsonIETF returns nil when no leaves are new or updated.
Without a nil guard, json.Marshal(nil) produces "null", and the
resulting gNMI SetRequest carries {path:{}, val:{jsonVal:"null"}}.
SROS rejects this with GMI #2052 ("Cannot set JSON value, because
last element is not leaf") because null is not valid JSON for a
container node.

Add the nil check that existed in the old gnmi.go Set switch
(pre-materialize refactor) so a no-op reconcile never sends a
spurious null update to the device.

Co-authored-by: Cursor <cursoragent@cursor.com>
@codecov
Copy link
Copy Markdown

codecov Bot commented Jun 2, 2026

@steiler
Copy link
Copy Markdown
Collaborator Author

steiler commented Jun 3, 2026

@giacoliva if I provide proper instructions to you how to test this, whould you be able to do so?
Thanks already in advance.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant