feat(a2): port leaf PS scripts to ci_orchestrator CLI
Implements Phase A2 of plans/implementation-plan-A-B.md: - commands/wait.py -> wait-ready (replaces Wait-VMReady.ps1) - commands/vm.py -> vm remove + vm cleanup (replaces Remove-BuildVM.ps1, Cleanup-OrphanedBuildVMs.ps1); cleanup accepts injected VmBackend (Phase C ESXi hook preserved) - commands/monitor.py -> monitor disk + monitor runner (replaces Watch-DiskSpace.ps1, Watch-RunnerHealth.ps1) - commands/report.py -> report job (replaces Get-CIJobSummary.ps1) Each PS script reduced to a 3-line shim that delegates to the Python CLI and preserves \0. Tests: 69 pytest cases across new test_commands_*.py modules; ruff clean, mypy --strict clean, coverage 74.5% (>=70 gate).
This commit is contained in:
+21
-102
@@ -1,122 +1,41 @@
|
||||
"""CLI entry point.
|
||||
|
||||
Phase A1 ships the ``wait-ready`` PoC sub-command. Subsequent steps add
|
||||
``vm``, ``build``, ``artifacts``, ``monitor``, ``report``, and ``job``.
|
||||
Phase A2 ships ``wait-ready``, ``vm`` (remove/cleanup), ``monitor``
|
||||
(disk/runner) and ``report`` (job). Subsequent phases add ``vm new``,
|
||||
``build``, ``artifacts``, ``job``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import time
|
||||
import time as time
|
||||
|
||||
import click
|
||||
|
||||
from ci_orchestrator import __version__
|
||||
from ci_orchestrator.backends import load_backend
|
||||
from ci_orchestrator.backends.errors import BackendError
|
||||
from ci_orchestrator.backends.protocol import VmHandle
|
||||
from ci_orchestrator.config import load_config
|
||||
from ci_orchestrator.credentials import KeyringCredentialStore
|
||||
from ci_orchestrator.transport.errors import TransportError
|
||||
from ci_orchestrator.transport.ssh import SshTransport
|
||||
from ci_orchestrator.transport.winrm import WinRmTransport
|
||||
from ci_orchestrator.commands.monitor import monitor
|
||||
from ci_orchestrator.commands.report import report
|
||||
from ci_orchestrator.commands.vm import vm
|
||||
|
||||
# Re-export so legacy A1 tests that patched ``cli_module.<name>`` keep working.
|
||||
from ci_orchestrator.commands.wait import ( # noqa: F401 (intentional re-exports)
|
||||
KeyringCredentialStore,
|
||||
SshTransport,
|
||||
WinRmTransport,
|
||||
load_backend,
|
||||
wait_ready,
|
||||
)
|
||||
|
||||
|
||||
@click.group()
|
||||
@click.version_option(__version__, prog_name="ci-orchestrator")
|
||||
def cli() -> None:
|
||||
"""Local CI/CD orchestrator (Phase A)."""
|
||||
"""Local CI/CD orchestrator."""
|
||||
|
||||
|
||||
@cli.command("wait-ready")
|
||||
@click.option("--vmx", required=True, help="Path to the guest VMX.")
|
||||
@click.option(
|
||||
"--guest-os",
|
||||
type=click.Choice(["windows", "linux"]),
|
||||
default="windows",
|
||||
show_default=True,
|
||||
)
|
||||
@click.option("--timeout", type=float, default=300.0, show_default=True)
|
||||
@click.option(
|
||||
"--poll-interval", type=float, default=5.0, show_default=True, help="Seconds between probes."
|
||||
)
|
||||
@click.option(
|
||||
"--credential-target",
|
||||
default=None,
|
||||
help="Override credential target name (defaults to config.guest_cred_target).",
|
||||
)
|
||||
@click.option("--ssh-user", default="ci_build", show_default=True)
|
||||
def wait_ready(
|
||||
vmx: str,
|
||||
guest_os: str,
|
||||
timeout: float,
|
||||
poll_interval: float,
|
||||
credential_target: str | None,
|
||||
ssh_user: str,
|
||||
) -> None:
|
||||
"""Poll a guest until WinRM (Windows) or SSH (Linux) is responsive."""
|
||||
config = load_config()
|
||||
backend = load_backend(config)
|
||||
handle = VmHandle(identifier=vmx)
|
||||
|
||||
deadline = time.monotonic() + timeout
|
||||
|
||||
# Step 1: wait until backend reports the VM as running.
|
||||
click.echo(f"[wait-ready] waiting for backend to report VM running: {vmx}")
|
||||
while time.monotonic() < deadline:
|
||||
try:
|
||||
if backend.is_running(handle):
|
||||
break
|
||||
except BackendError as exc:
|
||||
click.echo(f"[wait-ready] backend probe error: {exc}", err=True)
|
||||
time.sleep(poll_interval)
|
||||
else:
|
||||
click.echo("[wait-ready] timeout waiting for VM to be running.", err=True)
|
||||
sys.exit(2)
|
||||
|
||||
# Step 2: wait for an IP.
|
||||
ip: str | None = None
|
||||
while time.monotonic() < deadline:
|
||||
try:
|
||||
ip = backend.get_ip(handle)
|
||||
except BackendError:
|
||||
ip = None
|
||||
if ip:
|
||||
break
|
||||
time.sleep(poll_interval)
|
||||
if not ip:
|
||||
click.echo("[wait-ready] timeout waiting for guest IP.", err=True)
|
||||
sys.exit(3)
|
||||
click.echo(f"[wait-ready] guest IP: {ip}")
|
||||
|
||||
# Step 3: probe the transport layer.
|
||||
if guest_os == "windows":
|
||||
store = KeyringCredentialStore()
|
||||
target = credential_target or config.guest_cred_target
|
||||
cred = store.get(target)
|
||||
while time.monotonic() < deadline:
|
||||
try:
|
||||
with WinRmTransport(ip, cred.username, cred.password) as t:
|
||||
if t.is_ready():
|
||||
click.echo("[wait-ready] WinRM ready.")
|
||||
return
|
||||
except TransportError as exc:
|
||||
click.echo(f"[wait-ready] WinRM probe failed: {exc}", err=True)
|
||||
time.sleep(poll_interval)
|
||||
else:
|
||||
key_path = config.ssh_key_path
|
||||
while time.monotonic() < deadline:
|
||||
try:
|
||||
with SshTransport(ip, username=ssh_user, key_path=key_path) as t:
|
||||
if t.is_ready():
|
||||
click.echo("[wait-ready] SSH ready.")
|
||||
return
|
||||
except TransportError as exc:
|
||||
click.echo(f"[wait-ready] SSH probe failed: {exc}", err=True)
|
||||
time.sleep(poll_interval)
|
||||
|
||||
click.echo("[wait-ready] timeout waiting for transport to be ready.", err=True)
|
||||
sys.exit(4)
|
||||
cli.add_command(wait_ready)
|
||||
cli.add_command(vm)
|
||||
cli.add_command(monitor)
|
||||
cli.add_command(report)
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
|
||||
Reference in New Issue
Block a user