Skip to content

feat(reboot): add --wait/--expect-up to verify a rebooted VC member - #164

Merged
shigechika merged 4 commits into
mainfrom
feat/reboot-member-wait
Sep 12, 2026
Merged

shigechika merged 4 commits into
mainfrom
feat/reboot-member-wait

Conversation

@shigechika

@shigechika shigechika commented Sep 11, 2026 •

Copy link
Copy Markdown
Owner

Closes #161.

Why

reboot --member N --now issued the reboot and returned. Verifying the member came back meant hand-polling show virtual-chassis status, show chassis fpc and show interfaces terse every 30 s for minutes — that wait is the actual gate before the next maintenance step.

What

  • --wait SEC (only with --member --now): the member's boot timestamp is read on the still-open connection before the reboot is issued; verification then reconnects until that timestamp is newer and the member is Prsnt with a role and its FPC slot is Online. The reboot RPC returns while the member is still up, so "healthy right now" is not evidence that anything happened — without the baseline (unreadable) recovery instead requires having observed the member go away, and the result says so.
  • --expect-up IFACE[,IFACE…]: also require those interfaces to be up/up. A member can be Prsnt while its PFE is not forwarding yet — precisely when traffic hashed to it is black-holed — so the single-homed ports belong in the condition.
  • Connection failures during the window mean "not yet", never a verdict: the whole VC can be unreachable while one member reboots if the management path transits its uplink (observed: ~7 minutes).
  • Exit code 10 (error=member_not_back / unreachable) when it does not come back in time. wait, after, fpc_state and interfaces are in the result dict and the --json row; the text output gains a confirmed: member N is back (FPC Online, K port(s) up) after Ns / M probe(s) line.
  • Scheduled (--at) reboots and --dry-run verify nothing. Guards: --wait requires --member --now, --expect-up requires --wait, --wait >= 0.

vc.py gains get_fpc_state(), get_interface_states(), get_member_boot_time(), boot_time_is_newer() and wait_for_member(), built on a new _poll_device() reconnect loop (probes bounded by the remaining window, RPCs bounded by the interval, time.sleep/monotonic through the module so tests patch them). wait_for_master() keeps its own loop for now — converting it would change its after/error semantics. XML shapes were read off a live two-member QFX5110 VC — note the terse reply wraps every text node in newlines, hence the strip().

Tests

581 passed (+22). vc.py 97 %.

  • readers: FPC slot state (Online/Present/Empty/unknown/RPC failure), interface states (up/up, up/down, unreported → None, empty request, RPC failure).
  • wait_for_member: comes back after an unreachable window (probe bounds, gather_facts=False, device closed each probe); Prsnt but FPC Present → not back; --expect-up gates success and names the offending port on timeout; member absent; never reachable.
  • cmd_reboot: confirmed → 0; --expect-up parsed (whitespace tolerated) and rendered; not back → 10; --wait 0, a failed reboot, a dry-run and a scheduled reboot verify nothing; CLI guard combinations.

🤖 Generated with Claude Code

https://claude.ai/code/session_011Kotm5Xha6X2hu3S2u2RAT

Closes #161.

`reboot --member N --now` issued the reboot and returned; verifying the
member came back meant hand-polling `show virtual-chassis status`,
`show chassis fpc` and `show interfaces terse` for minutes — which is
what the operator actually waits for before the next maintenance step.

- `--wait SEC` (only with `--member --now`): after issuing, reconnect
  until the member is Prsnt with a role *and* its FPC slot is Online.
  `--expect-up IFACE[,IFACE...]` additionally requires those ports to
  be up/up: a member can be Prsnt while its PFE is not forwarding yet,
  which is when traffic hashed to it is black-holed. Connection
  failures are "not yet" — the whole VC can be unreachable while one
  member reboots if the management path transits its uplink.
- Exit code 10 (`error=member_not_back`) when it does not come back;
  `wait`, `after`, `fpc_state` and `interfaces` land in the result and
  the --json row. Scheduled (`--at`) reboots and dry-runs verify
  nothing.
- vc.py gains `get_fpc_state()`, `get_interface_states()` (terse text
  nodes are newline-wrapped, hence the strip) and `wait_for_member()`;
  the reconnect loop is now shared with `wait_for_master()` via
  `_poll_device()` (bounded probes, sleep/monotonic via the module so
  tests can patch them).

XML shapes taken from a live two-member QFX5110 VC.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Kotm5Xha6X2hu3S2u2RAT
@github-actions github-actions Bot added the ai-review Admitted for review by pr-gate.yml label Sep 11, 2026
Comment thread junos_ops/vc.py
last_problem = None
while True:
result["attempts"] += 1
remaining = max(1, int(deadline - time.monotonic()))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

R1F1 (blocking)

A member reboot RPC can return after enqueueing the reboot but before the member leaves Prsnt or its FPC leaves Online. Because polling reconnects immediately and accepts that first healthy snapshot, the command can report confirmed and exit 0 just before the member actually goes down, allowing the next maintenance step to run during the reboot.

Advisory AI review — verify before acting. Round summary and ledger are in the ai-review comment.

Comment thread junos_ops/vc.py
logger.debug(f"get_interface_states: {type(e).__name__}: {e}")
return states
if rsp is None or isinstance(rsp, bool):
return states

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

R1F2 (advisory)

For --expect-up ge-0/0/40.0 or irb.0, Junos reports the requested name under a <logical-interface> element rather than <physical-interface>. This loop leaves the state as None, so an interface that is actually up/up causes polling to time out and return exit code 10.

Advisory AI review — verify before acting. Round summary and ledger are in the ai-review comment.

Comment thread junos_ops/vc.py
deadline = start + timeout
last_problem = None
while True:
result["attempts"] += 1

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

R1F3 (advisory)

If an unsuccessful probe finishes before the deadline, line 575 sleeps for the entire remaining interval and the loop then re-enters at the deadline. max(1, ...) starts one more connection attempt with a one-second probe budget, so an unreachable or not-yet-NETCONF-ready device can make --wait SEC exceed SEC rather than terminating at the requested deadline.

Advisory AI review — verify before acting. Round summary and ledger are in the ai-review comment.

codex review of #164 (critical): `request system reboot member N`
returns while the member is still up, so the first probe found the
pre-reboot member Prsnt/Online and reported success without a reboot
having happened.

- cmd_reboot reads the member's boot timestamp (and the VC master, for
  the localre/fpcN mapping) on the still-open connection *before*
  issuing, and wait_for_member now requires that timestamp to change.
  When it cannot be read, recovery requires having observed the member
  go away (connection failures count, via _poll_device's new
  on_unreachable callback) and the result carries a warning.
- get_interface_states matches logical interface names too, so
  --expect-up ae0.0 works, not only physical names.
- _poll_device caps dev.timeout as well as auto_probe with the
  remaining window: bounding only the connect left the RPCs able to
  run past --wait.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Kotm5Xha6X2hu3S2u2RAT
Comment thread junos_ops/vc.py Outdated
"fpc_state": last.get("fpc_state"),
"interfaces": last.get("interfaces"),
"booted": last.get("booted"),
"rebooted": bool(polled["ok"]),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

R2F1 (advisory)

If the boot timestamp changes but an expected interface remains down until timeout, the reboot was observed but this field returns False because polled["ok"] also includes FPC and interface readiness. Library callers therefore cannot distinguish “reboot occurred but readiness failed” from “reboot never occurred”; track reboot evidence independently from the final gate.

Advisory AI review — verify before acting. Round summary and ledger are in the ai-review comment.

…, tighten the fallback

codex review follow-up on #164:
- boot_time_is_newer() compares parsed instants (falling back to string
  inequality), so a reformatted or re-zoned rendering of the same boot
  is not read as a reboot, and only a later timestamp counts.
- the member's uptime block is resolved with the master seen in *this*
  probe: mastership can move while the member reboots, which changes
  whether it is localre or fpcN.
- a VC-status RPC failure no longer counts as 'the member went down'
  for the no-baseline fallback; only an unreachable device or a
  member-level not-ready state does.
- dev.timeout is capped by the probe interval rather than the whole
  window, so one slow RPC cannot consume the budget; --wait is
  documented as a between-probes budget.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Kotm5Xha6X2hu3S2u2RAT
Comment thread junos_ops/vc.py
if not booted or not baseline:
return False
fmt = "%Y-%m-%d %H:%M:%S"
try:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

R3F1 (advisory)

For baseline 2026-06-16 19:11:55 UTC and post-probe value 2026-06-17 04:11:55 JST, both timestamps represent the same instant, but slicing to 19 characters makes the latter appear newer. If the same boot is rendered in a different timezone, wait_for_member() can therefore falsely confirm that a reboot occurred.

Advisory AI review — verify before acting. Round summary and ledger are in the ai-review comment.

…recovery

/code-review findings on #164, plus regression tests:

- get_fpc_state() returns None both for "slot not reported" and for an
  RPC failure; treating that as "the member went down" let a transient
  error on the no-baseline path confirm a member that never rebooted.
  Only a known non-Online state counts as a transition now.
- get_member_boot_time() accepted a reply with no per-RE blocks for any
  member, but such a reply describes only the RE the session is on;
  rebooting member 1 from a session on member 0 would baseline against
  member 0 and always time out. It is accepted only when the master is
  unknown or is the member asked for.
- the verification-failure step was tagged "error", which the formatter
  prints above the reboot line; it is "verify_error" now.
- `rebooted` reported overall success rather than boot evidence, so a
  member that came back with a port still down said rebooted=false.
- docstring/CLAUDE.md still claimed the boot timestamp is never parsed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Kotm5Xha6X2hu3S2u2RAT
@github-actions

Copy link
Copy Markdown
Contributor

AI review (gpt-5.6-sol)

Verdict: no blocking findings — 2 advisory. Advisory findings do not need to be resolved before merge; open an issue for the ones worth keeping.

Scope: new commits since 85e947a (full PR diff supplied to the model as context).

No findings clear the reporting bar this round.

Still open from earlier rounds:

  • R1F3 [advisory] Polling starts an extra connection after its deadline
  • R3F1 [advisory] Boot-time comparison ignores timezone offsets
Findings ledger (all rounds)
  • R1F1 [fixed/blocking] — Initial healthy probe can falsely confirm recovery before reboot starts
  • R1F2 [fixed/advisory] — Logical interfaces requested by expect-up always remain unknown
  • R1F3 [open/advisory] — Polling starts an extra connection after its deadline
  • R2F1 [fixed/advisory] — rebooted incorrectly mirrors overall readiness
  • R3F1 [open/advisory] — Boot-time comparison ignores timezone offsets

Advisory per-push review (round 4) generated by ai-review — verify findings before acting.

@shigechika
shigechika merged commit 1bff3d0 into main Sep 12, 2026
9 checks passed
@shigechika
shigechika deleted the feat/reboot-member-wait branch September 12, 2026 00:06
@github-actions github-actions Bot removed the ai-review Admitted for review by pr-gate.yml label Sep 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

reboot --member: add --wait to verify the member (and its ports) came back

1 participant