Reliable Automation
Pre-Check, Change, Post-Check
What you'll learn
- Why a command running without error still isn't proof a change worked
- The pre-check, change, post-check pattern as a reusable shape, not a one-off example
- How desired state, actual state, and a decision fit into that same shape
- How to test this logic against a fake connection, with no real device
- Why the post-check has to be an independent re-read, not a reused result
Listen to a guided explanation of this lesson.
Spoken narration isn't supported in this browser.
The command ran. Did the change actually happen?
"Making Configuration Changes Safely" already showed one idempotent example: check whether NTP is already configured before pushing it. This lesson takes that one step further — the piece that example didn't have. After pushing a change, how do you know it actually took effect, rather than just that send_config_set() returned without an error?
A command succeeding and a change working are different claims
This is the same lesson "Configuration vs State" taught two modules ago, now applied to the moment of making a change: send_config_set() not raising an exception means the device accepted the commands. It doesn't mean the device's actual, running state now matches what you wanted. Only checking again — independently — proves that.
The pattern, generalized
- Desired statewhat you want to be true
- Pre-checkread current state
- Decidealready matches? skip. doesn't? continue
- Changeapply, only if needed
- Post-checkindependently re-read state
- Comparedoes actual state now match desired state?
def apply_ntp_config(connection, required_ntp_server):
"""
Checks current NTP config, applies the required server only if it's
missing, then independently re-checks to confirm the change
actually took effect -- not just that the command ran without error.
"""
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}
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}Notice send_command() is called twice — once before, once after — and post_check is a completely fresh read from the device, not something reused from before the change. That's what makes verified mean something.
Testing it without a device
A fake connection object with the same two methods (send_command, send_config_set) is enough to exercise every branch of this logic for real:
class FakeConnection:
def __init__(self, already_has_ntp):
self.configured = already_has_ntp
self.config_calls = []
def send_command(self, cmd):
return "ntp server 10.0.0.5" if self.configured else ""
def send_config_set(self, lines):
self.config_calls.append(lines)
self.configured = True
return "ok"
# Already configured -- no change should happen
conn1 = FakeConnection(already_has_ntp=True)
print(apply_ntp_config(conn1, "10.0.0.5")) # {'changed': False, 'verified': True}
print(conn1.config_calls) # [] -- nothing was pushed
# Needs configuring -- change happens and gets verified
conn2 = FakeConnection(already_has_ntp=False)
print(apply_ntp_config(conn2, "10.0.0.5")) # {'changed': True, 'verified': True}Both branches were actually run while writing this lesson, with these exact results. FakeConnection.configured flips to True the moment send_config_set() is called, which is what makes the post-check's send_command() correctly return the new value — a genuine simulation of a device's state actually changing.
Key takeaways
- A command succeeding proves it was accepted — not that the device's actual state now matches what you intended.
- The reusable pattern: pre-check (read current state), decide (already correct? skip), change (only if needed), post-check (independently re-read), compare.
- A post-check has to be a fresh observation — reusing the pre-check's result or just assuming success defeats its purpose entirely.
- This logic is fully testable with a fake connection object that tracks its own state, no real device required.
- This same function grows a dry_run option in the next lesson, and folds into the full capstone in Lesson 6.