Reliable Automation
Dry-Run Mode and Safe Change Workflows
What you'll learn
- What dry-run mode actually guarantees, and why that guarantee matters
- How to add a dry_run parameter to the pre-check/change/post-check pattern
- How to verify, by testing, that dry-run mode never touches the device
- Rollback thinking: what it means, and why network changes are harder to undo than software ones
- Why this module's capstone defaults to dry-run
Listen to a guided explanation of this lesson.
Spoken narration isn't supported in this browser.
Preview before you commit
Dry-run mode means running a script all the way through its decision-making — checking state, deciding whether a change is needed — while stopping short of actually making that change. Instead, it tells you what it would have done. This is one of the most common patterns in real production automation tooling (Ansible's --check mode is the same idea), and until now, nothing in this curriculum has had it.
What dry-run actually guarantees
Dry-run isn't "probably safe" — it's a hard guarantee that the change-making code path is never reached. The same script, run the same way, either can or cannot touch the device depending on one flag — and that flag should be impossible to miss.
Adding it to the pattern
- Pre-check
- Change needed?yes
- Print: would applynothing sent to the device
- Report: preview only
- Pre-check
- Change needed?yes
- send_config_set()real change applied
- Post-check + verify
def apply_ntp_config(connection, required_ntp_server, dry_run=True):
"""
Checks current NTP config, and only applies the required server if
it's missing. In dry-run mode, shows what would change without
actually changing anything.
"""
pre_check = connection.send_command("show run | include ntp server")
already_configured = required_ntp_server in pre_check
if already_configured:
return {"changed": False, "verified": True, "dry_run": dry_run}
if dry_run:
print(f" [DRY RUN] Would apply: ntp server {required_ntp_server}")
return {"changed": False, "verified": None, "dry_run": True}
connection.send_config_set([f"ntp server {required_ntp_server}"])
post_check = connection.send_command("show run | include ntp server")
verified = required_ntp_server in post_check
return {"changed": True, "verified": verified, "dry_run": False}The default is dry_run=True — a script that does nothing dangerous unless someone deliberately opts out of safety, not the other way around. Notice the dry-run branch returns before send_config_set() is ever reached.
Proving it never touches the device
conn = FakeConnection(already_has_ntp=False)
result = apply_ntp_config(conn, "10.0.0.5", dry_run=True)
print(result) # {'changed': False, 'verified': None, 'dry_run': True}
print(conn.config_calls) # [] -- send_config_set() was never called
print(conn.configured) # False -- the device's state never changedThis was actually run while writing this lesson. Three separate checks confirm dry-run kept its promise: the returned result, the empty call log, and the fake device's own state, all showing nothing happened.
Rollback thinking
Dry-run answers "what would this do?" A related but separate question is "if this goes wrong, how would I undo it?" Network configuration doesn't have a built-in "undo" the way editing a document does — reverting a change usually means knowing exactly what the configuration looked like before, and pushing that back deliberately. This module doesn't build a rollback system (that's a genuinely larger topic), but it's worth having the habit: before a real change, know what "before" looked like, and how you'd get back to it.
The pre-check is also your rollback reference
This is one more reason the pre-check step matters: the state it captures before a change is exactly the information you'd need to undo that change if something went wrong. A script that skips the pre-check doesn't just risk an unnecessary change — it also throws away the one piece of information rollback would need.
The safety checklist, generalized
The exact checklist from "Making Configuration Changes Safely" still applies, now with two more items:
- Is this a lab device you fully control, not production?
- Have you checked current state first (idempotency)?
- Have you previewed the change with dry-run before running for real?
- Do you have a way to verify the change worked afterward (post-check)?
- Do you know what 'before' looked like, in case you need to undo it?
Key takeaways
- Dry-run mode guarantees the change-making code path is never reached, while still running every check and decision leading up to it.
- Defaulting a function to dry_run=True means forgetting the parameter fails safe, not dangerous.
- Dry-run behavior is testable by confirming three things: the returned result, an empty log of change calls, and the fake device's unchanged state.
- Rollback thinking — knowing what 'before' looked like — is a separate concern from dry-run, and the pre-check step is exactly where that information comes from.
- This module's capstone, in Lesson 6, defaults to dry_run=True for exactly the reasons this lesson explains.