NetAuto Academy

Capstone

Canary Rollout and Validation Gates

Advanced16 min read

What you'll learn

  • Selecting a canary from a runtime-detected subset, not a static inventory tag
  • Applying the exact Git-evidenced configuration from Lesson 3, not a hardcoded string
  • Both real outcomes: a canary that passes and expands, and one that fails and rolls back
  • What this lesson does not re-teach from Production Automation
🎓 Teach This Lesson

Listen to a guided explanation of this lesson.

Spoken narration isn't supported in this browser.

Connecting the earlier stages to rollout

Not re-teaching Production Automation

select_canary(), run_stage(), evaluate_gate(), and rollback_stage() use the exact mechanics Production Automation already taught and verified -- nr.filter(), napalm_configure()/napalm_get(), a threshold gate, napalm_rollback. Nothing about how those individual pieces work is re-explained here. This lesson's job is connecting them to what Lessons 1-3 actually produced.

  1. Lesson 1's non-compliant devices + Lesson 3's approved config
  2. select_canary()one host from the detected subset, not a static role tag
  3. run_stage(canary, approved_config)apply, then validate against the Git-evidenced line
  4. evaluate_gate()
  5. PASS -> expand to the rest of the detected subsetsame approved_config
  6. FAIL -> abort -> rollback_stage(canary)
Quick checkConcept check
How does canary selection here differ from Production Automation's own capstone?

The code

capstone_lib.py (Lesson 4 portion)
from nornir_napalm.plugins.tasks import napalm_rollback


def select_canary(nr, affected_hostnames):
  affected_sorted = sorted(affected_hostnames)
  canary_host = affected_sorted[0]
  rest_hosts = affected_sorted[1:]
  canary_nr = nr.filter(filter_func=lambda h: h.name == canary_host)
  rest_nr = nr.filter(filter_func=lambda h: h.name in rest_hosts)
  return canary_nr, rest_nr


def _apply_and_validate(task, configuration):
  task.run(task=napalm_configure, configuration=configuration)
  task.run(task=napalm_get, getters=["get_config"])


def run_stage(nr_subset, configuration):
  agg = nr_subset.run(task=_apply_and_validate, configuration=configuration)
  stage_result = {}
  for hostname, multi_result in agg.items():
      if multi_result.failed:
          stage_result[hostname] = {"applied": False, "validated": False}
          continue
      applied = not multi_result[1].failed
      running_config = multi_result[2].result["get_config"]["running"]
      validated = configuration.strip() in running_config
      stage_result[hostname] = {"applied": applied, "validated": validated}
  return stage_result


def evaluate_gate(stage_result, threshold=1.0):
  total = len(stage_result)
  passed = sum(1 for r in stage_result.values() if r["applied"] and r["validated"])
  pass_rate = passed / total if total else 0.0
  return "PASS" if pass_rate >= threshold else "FAIL"


def rollback_stage(nr_subset):
  return nr_subset.run(task=napalm_rollback)

Real, verified code. run_stage() validates against the "configuration" parameter it was actually given -- Lesson 3's Git-evidenced artifact, passed in directly, not a separately hardcoded expectation.

Quick checkIdentify the correct code
In run_stage(), why does validated check `configuration.strip() in running_config` instead of checking against a separate constant like REQUIRED_NTP_LINE?

Scenario A — real, verified: canary passes, rollout expands

terminal output — real, verified run
canary: {'RTR1': {'applied': True, 'validated': True}}
decision: PASS
expanded: {
'RTR2': {'applied': True, 'validated': True},
'SW-EOS1': {'applied': True, 'validated': True}
}
rolled_back: False

Real, verified. RTR1 (canary) both applied and validated correctly; the gate decided PASS; the same approved configuration expanded to RTR2 and SW-EOS1 -- both applying and validating successfully.

Quick checkPredict output
In Scenario A, why does "expanded" contain RTR2 and SW-EOS1 but not RTR1?

Scenario B — real, verified: canary fails, aborts, rolls back

Not just the happy path

A separate fixture scenario represents the canary's commit succeeding but its post-change validation failing. No fixtures for the rollout stage exist for RTR2/SW-EOS1 in this scenario (they still have detection-stage fixtures, since detection touches the whole fleet) -- so if the code mistakenly tried to expand after a failed gate, it would fail loudly with a real error rather than silently succeeding.

terminal output — real, verified run
canary: {'RTR1': {'applied': True, 'validated': False}}
decision: FAIL
expanded: {}
rolled_back: True

Real, verified. RTR1's configuration was applied (committed) but validation found the required line still missing -- a genuine post-change verification failure, not a connection error. The gate correctly decided FAIL, expansion never ran, and rollback genuinely succeeded.

Quick checkScenario
In Scenario B, what specifically proves RTR2 and SW-EOS1 were never touched during rollout, rather than the report simply failing to mention them?

Key takeaways

  • select_canary() operates on Lesson 1's runtime-detected affected devices, not a static inventory role -- the one genuinely new wiring in this stage.
  • run_stage() validates against the exact configuration parameter it receives -- Lesson 3's Git-evidenced artifact, not a separately maintained constant.
  • Scenario A, real and verified: canary applies and validates, gate decides PASS, the same approved configuration expands to the rest of the affected devices.
  • Scenario B, real and verified: canary applies but fails validation, gate decides FAIL, expansion never runs, rollback genuinely succeeds -- with fixture absence proving the rest of the fleet was truly untouched.
  • None of napalm_configure(), napalm_rollback(), or nr.filter()'s own mechanics are re-explained here -- all assumed from Production Automation.

Done with this lesson?

Next lesson

Capstone: Source of Truth to Safe Network Change