Two failures from one strict expect, both hit while building the sim ISPs. A run that dies mid-config leaves the console parked in configuration mode. The next run then waits for the operational `$ ` prompt against a perfectly healthy VM and hangs until timeout, with nothing in the output to say why -- the box was sitting at `vyos@isp-dhcp#` the whole time. Login now accepts `# ` as well and discards the stale candidate rather than committing something nobody has seen. The same mistake at the exit step: insisting on `$ ` after `save` hung, AND left the console in config mode, which is what created the first failure for the following run. Now accepts either prompt. Known-bad, not fixed: the tool reports "committed and saved" when the set commands have not applied. Verified against the clone -- prompt showed the host-name change had landed while `grep -c dhcp-server` returned 0. The failure detection only inspects c.before for a few strings and evidently misses the real failure mode, so success is being reported without evidence. That needs fixing before this tool is trusted for anything; it is currently only safe to use with an independent check afterwards, which is how the gap was found. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DMVzWZgiKW2wquf5z8S1yH
110 lines
3.6 KiB
Python
Executable File
110 lines
3.6 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Apply VyOS config to a labsim VM over its serial console.
|
|
|
|
Needed because a freshly installed VyOS comes up holding the same addresses as
|
|
its peer, so there is a window where it cannot safely be reached over the
|
|
network at all. The console does not care.
|
|
|
|
./console-apply.py --vm labsim-vyos2 --config r2.conf
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import sys
|
|
import time
|
|
|
|
import pexpect
|
|
|
|
|
|
def main() -> int:
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("--vm", required=True)
|
|
ap.add_argument("--config", required=True)
|
|
ap.add_argument("--user", default="vyos")
|
|
ap.add_argument("--password", default="vyos")
|
|
args = ap.parse_args()
|
|
|
|
cmds = [l.rstrip() for l in open(args.config)
|
|
if l.strip() and not l.lstrip().startswith("#")]
|
|
print(f"{len(cmds)} commands to apply to {args.vm}", file=sys.stderr)
|
|
|
|
c = pexpect.spawn(f"virsh --connect qemu:///system console {args.vm}",
|
|
timeout=90, encoding="utf-8")
|
|
c.logfile_read = None
|
|
c.sendline("")
|
|
time.sleep(2)
|
|
c.sendline("")
|
|
|
|
# Log in. A freshly booted box may still be starting services, so allow a
|
|
# generous window and re-prod the console rather than failing on the first
|
|
# miss.
|
|
#
|
|
# `# ` matters as much as `$ `: a previous run that died mid-config leaves
|
|
# the console sitting in configuration mode, and waiting only for the
|
|
# operational prompt then hangs forever against a perfectly healthy VM.
|
|
in_config = False
|
|
for _ in range(40):
|
|
i = c.expect([r"login:", r"\$ ", r"# ", pexpect.TIMEOUT], timeout=15)
|
|
if i == 0:
|
|
c.sendline(args.user)
|
|
c.expect("Password:", timeout=30)
|
|
c.sendline(args.password)
|
|
c.expect([r"\$ ", r"# "], timeout=60)
|
|
break
|
|
if i == 1:
|
|
break
|
|
if i == 2:
|
|
in_config = True
|
|
break
|
|
c.sendline("")
|
|
else:
|
|
print("never reached a prompt", file=sys.stderr)
|
|
return 1
|
|
|
|
if in_config:
|
|
# Drop whatever the previous run left half-built rather than committing
|
|
# a candidate nobody has seen.
|
|
print("console was left in config mode; discarding stale candidate",
|
|
file=sys.stderr)
|
|
c.sendline("discard")
|
|
c.expect(r"# ", timeout=60)
|
|
else:
|
|
c.sendline("configure")
|
|
c.expect(r"# ", timeout=60)
|
|
|
|
for cmd in cmds:
|
|
c.sendline(cmd)
|
|
c.expect(r"# ", timeout=60)
|
|
out = c.before or ""
|
|
if "Set failed" in out or "not valid" in out or "Invalid" in out:
|
|
print(f"FAILED: {cmd}\n {out.strip()[:200]}", file=sys.stderr)
|
|
|
|
print("committing...", file=sys.stderr)
|
|
c.sendline("commit")
|
|
c.expect(r"# ", timeout=300)
|
|
commit_out = c.before or ""
|
|
c.sendline("save")
|
|
c.expect(r"# ", timeout=120)
|
|
# Accept either prompt on the way out. Insisting on `$ ` here hangs against
|
|
# a healthy box -- and worse, leaves the console parked in config mode, so
|
|
# the NEXT run finds a `# ` it was not expecting either. One strict expect
|
|
# turned into two failures.
|
|
c.sendline("exit")
|
|
c.expect([r"\$ ", r"# ", pexpect.TIMEOUT], timeout=60)
|
|
c.sendline("exit")
|
|
c.close(force=True)
|
|
|
|
bad = [l for l in commit_out.splitlines()
|
|
if "failed" in l.lower() or "error" in l.lower()]
|
|
if bad:
|
|
print("commit reported:", file=sys.stderr)
|
|
for l in bad[:10]:
|
|
print(f" {l.strip()}", file=sys.stderr)
|
|
return 1
|
|
print("committed and saved", file=sys.stderr)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|