NetAuto Academy

Reliable Automation

Pre-Check, Change, Post-Check

Intermediate13 min read

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
🎓 Teach This Lesson

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

  1. Desired statewhat you want to be true
  2. Pre-checkread current state
  3. Decidealready matches? skip. doesn't? continue
  4. Changeapply, only if needed
  5. Post-checkindependently re-read state
  6. Comparedoes actual state now match desired state?
apply_ntp_config.py
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.

Quick checkConcept check
Why does apply_ntp_config() call send_command() a second time after send_config_set(), instead of just assuming the push worked?

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:

test_apply_ntp.py
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.

Quick checkScenario
Using the FakeConnection above, what does conn1.config_calls equal after calling apply_ntp_config(conn1, "10.0.0.5") on an already-configured device?
Quick checkIdentify the correct code
Which line correctly represents an independent post-check, rather than reusing the pre-check's result?
Quick checkMyth check
If send_config_set() runs without raising an exception, is that sufficient proof the configuration change actually worked?

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.