Skip to content

The driver framework

A driver is a vendor personality. It owns the full interactive loop for one device: reading input, parsing commands, and writing responses. rcfg-sim ships two — cisco_ios and ciena_tl1 — and adding more is deliberately a one-file job.

Every driver implements four methods:

type Driver interface {
Name() string // manifest `template` id, e.g. "cisco_ios"
Commands() []string // metric label values this driver can emit
RequiresSSHAuth() bool // does it authenticate at the SSH transport?
Serve(ctx *sessionCtx) // the full interactive loop
}
  • Name() is the id used in the manifest template column and the registry key.
  • Commands() is the closed set of command labels the driver may emit on rcfgsim_command_duration_seconds. The server unions these across all drivers and pre-registers them, so the metric’s cardinality is assembled from drivers rather than hardcoded — and stays bounded.
  • RequiresSSHAuth() is consulted only under --ssh-auth=driver.
  • Serve() runs the session.

At session start the server looks up the device’s manifest template value in the driver registry (driverFor). Unknown or empty values fall back to cisco_ios, so older manifests keep working unchanged.

new SSH session
manifest row ── template column ──▶ driver registry
┌───────────────────┼───────────────────┐
cisco_ios ciena_tl1 unknown / empty
│ │ │
▼ ▼ ▼
ciscoIOS.Serve cienaTL1.Serve ciscoIOS.Serve (fallback)

Drivers don’t reimplement timing, faults, or metrics. They route every response through two shared helpers on the session context:

  • applyResponseDelay() — the single place a response sleep happens (base jitter plus the slow_response fault multiplier). See timing.
  • emit() — writes the response, applies disconnect_mid / malformed faults to streamed config bytes, preserves the zero-copy path for the un-faulted case, and records the command-duration metric.

This is what guarantees that fault and metric behaviour is identical across vendors — a driver author gets all of it for free.

Each driver registers itself from an init() in its own file (driver_<vendor>.go), so nothing else in the hot path needs to know it exists:

func init() { registerDriver(ciscoIOS{}) }

Want to add your own? See Writing a new driver.