feat(a3): port build pipeline (vm new, build run, artifacts collect)
Implements Phase A3 of plans/implementation-plan-A-B.md:
- commands/vm.py: vm new (replaces New-BuildVM.ps1)
- commands/build.py: build run (replaces Invoke-RemoteBuild.ps1) with WinRM/SSH dispatch and stdout streaming for act_runner
- commands/artifacts.py: artifacts collect (replaces Get-BuildArtifacts.ps1) using transport.fetch()
- 3 PS scripts reduced to shims preserving \0
- Pester tests/{New-BuildVM,Wait-VMReady,Remove-BuildVM}.Tests.ps1 removed; equivalent cases covered in pytest
- Phase C hook: build/artifacts accept opaque VmHandle/vmx; no Windows path assumptions
Tests: 91 pytest, ruff clean, mypy --strict clean, coverage 78.27% (>=70 gate).
Master checklist + step A3 attivita + 'definizione di fatto' updated; A3-closeout.md added.
This commit is contained in:
@@ -354,4 +354,116 @@ def vm_cleanup(
|
||||
)
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────── new
|
||||
|
||||
|
||||
@vm.command("new")
|
||||
@click.option(
|
||||
"--template",
|
||||
"--template-path",
|
||||
"template",
|
||||
required=True,
|
||||
help="Identifier of the template VM (Workstation: VMX path; ESXi: managed-object ref).",
|
||||
)
|
||||
@click.option(
|
||||
"--snapshot",
|
||||
"--snapshot-name",
|
||||
"snapshot",
|
||||
default="BaseClean",
|
||||
show_default=True,
|
||||
help="Snapshot name on the template to clone from.",
|
||||
)
|
||||
@click.option(
|
||||
"--clone-base-dir",
|
||||
"clone_base_dir",
|
||||
required=True,
|
||||
help="Directory under which the new clone folder will be created.",
|
||||
)
|
||||
@click.option(
|
||||
"--job-id",
|
||||
"job_id",
|
||||
required=True,
|
||||
help="Unique job identifier (used to name the clone folder).",
|
||||
)
|
||||
@click.option(
|
||||
"--vmrun-path",
|
||||
"vmrun_path",
|
||||
default=None,
|
||||
help="Override vmrun binary path.",
|
||||
)
|
||||
@click.option(
|
||||
"--guest-os",
|
||||
type=click.Choice(["windows", "linux"], case_sensitive=False),
|
||||
default="windows",
|
||||
show_default=True,
|
||||
help="Informational; reserved for backend hints in Phase C.",
|
||||
)
|
||||
def vm_new(
|
||||
template: str,
|
||||
snapshot: str,
|
||||
clone_base_dir: str,
|
||||
job_id: str,
|
||||
vmrun_path: str | None,
|
||||
guest_os: str,
|
||||
) -> None:
|
||||
"""Create a linked clone of the template VM for an ephemeral CI build.
|
||||
|
||||
Mirrors ``scripts/New-BuildVM.ps1``. Prints the clone identifier
|
||||
(Workstation: VMX path) on stdout on success.
|
||||
|
||||
Phase C hook: ``--template`` is treated as an opaque string; the backend
|
||||
is responsible for interpretation. Workstation builds the destination
|
||||
path under ``--clone-base-dir`` from ``--job-id`` + timestamp.
|
||||
"""
|
||||
del guest_os # currently informational only
|
||||
# Verify the template identifier looks plausible: for Workstation it is
|
||||
# a VMX path. For other backends in Phase C this validation will move
|
||||
# into the backend itself.
|
||||
template_path = Path(template)
|
||||
if not template_path.is_file():
|
||||
raise click.ClickException(f"Template VMX not found: {template}")
|
||||
|
||||
base = Path(clone_base_dir)
|
||||
base.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
timestamp = datetime.now(tz=UTC).strftime("%Y%m%d_%H%M%S")
|
||||
clone_name = f"Clone_{job_id}_{timestamp}"
|
||||
clone_dir = base / clone_name
|
||||
clone_vmx = clone_dir / f"{clone_name}.vmx"
|
||||
|
||||
click.echo("[vm new] creating linked clone...")
|
||||
click.echo(f" template : {template}")
|
||||
click.echo(f" snapshot : {snapshot}")
|
||||
click.echo(f" clone vmx: {clone_vmx}")
|
||||
|
||||
try:
|
||||
backend = _make_backend(vmrun_path)
|
||||
except BackendNotAvailable as exc:
|
||||
raise click.ClickException(f"vmrun unavailable: {exc}") from exc
|
||||
|
||||
start = time.monotonic()
|
||||
try:
|
||||
handle = backend.clone_linked(
|
||||
template=str(template_path),
|
||||
snapshot=snapshot,
|
||||
name=clone_name,
|
||||
destination=str(clone_vmx),
|
||||
)
|
||||
except BackendError as exc:
|
||||
# Clean up partial clone directory if vmrun left one behind.
|
||||
if clone_dir.exists():
|
||||
_try_remove_dir(clone_dir)
|
||||
raise click.ClickException(f"clone failed: {exc}") from exc
|
||||
|
||||
if not clone_vmx.is_file():
|
||||
raise click.ClickException(
|
||||
f"backend reported success but clone VMX is missing: {clone_vmx}"
|
||||
)
|
||||
|
||||
elapsed = time.monotonic() - start
|
||||
click.echo(f"[vm new] clone created in {elapsed:.1f}s")
|
||||
# Final stdout line: the identifier itself, so PS callers can capture it.
|
||||
click.echo(handle.identifier)
|
||||
|
||||
|
||||
__all__ = ["cleanup_orphans", "vm"]
|
||||
|
||||
Reference in New Issue
Block a user