bd31a3f2f3
- pyproject.toml (hatchling) with ruff/mypy/pytest dev deps - src/ci_orchestrator/: config, credentials, backends (Protocol + WorkstationVmrunBackend), transport (WinRM via pypsrp, SSH via paramiko) - CLI entry point with PoC 'wait-ready' subcommand (Windows + Linux guests) - tests/python/: 35 unit tests, 83% coverage, all backends/transport mocked - gitea/workflows/lint.yml: new 'python' job (ruff + mypy --strict + pytest --cov-fail-under=70) - config.example.toml + README setup section + .gitignore Python entries Neutral VmBackend Protocol prepared for Phase C ESXi extension. See plans/implementation-plan-A-B.md step A1.
82 lines
2.4 KiB
Python
82 lines
2.4 KiB
Python
"""Neutral VM backend Protocol.
|
|
|
|
Phase C hook: keep names hypervisor-agnostic. ``clone_linked``, ``start``,
|
|
``stop``, ``delete``, ``get_ip``, ``list_snapshots`` map cleanly onto both
|
|
``vmrun`` and pyVmomi (vSphere). Identifiers are opaque strings (a VMX path
|
|
on Workstation, a managed object reference on ESXi).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from enum import StrEnum
|
|
from typing import Protocol
|
|
|
|
|
|
class VmState(StrEnum):
|
|
"""Coarse VM power state, normalised across backends."""
|
|
|
|
POWERED_OFF = "powered_off"
|
|
POWERED_ON = "powered_on"
|
|
SUSPENDED = "suspended"
|
|
UNKNOWN = "unknown"
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class VmHandle:
|
|
"""Opaque reference to a VM managed by a backend.
|
|
|
|
``identifier`` is backend-specific (a VMX path for Workstation, a
|
|
managed object reference for vSphere). Callers MUST treat it as opaque.
|
|
"""
|
|
|
|
identifier: str
|
|
name: str | None = None
|
|
|
|
|
|
class VmBackend(Protocol):
|
|
"""Hypervisor-agnostic VM operations.
|
|
|
|
All methods raise :class:`~ci_orchestrator.backends.errors.BackendError`
|
|
on failure. Implementations are free to add backend-specific kwargs but
|
|
MUST honour the documented signature for the listed parameters.
|
|
"""
|
|
|
|
def clone_linked(
|
|
self,
|
|
template: str,
|
|
snapshot: str,
|
|
name: str,
|
|
destination: str | None = None,
|
|
) -> VmHandle:
|
|
"""Create a linked clone of ``template`` at snapshot ``snapshot``."""
|
|
...
|
|
|
|
def start(self, handle: VmHandle, headless: bool = True) -> None:
|
|
"""Power on the VM."""
|
|
...
|
|
|
|
def stop(self, handle: VmHandle, hard: bool = False) -> None:
|
|
"""Power off the VM. ``hard=True`` requests a forced power-off."""
|
|
...
|
|
|
|
def delete(self, handle: VmHandle) -> None:
|
|
"""Remove the VM from inventory and delete its files."""
|
|
...
|
|
|
|
def get_ip(self, handle: VmHandle, timeout: float = 0.0) -> str | None:
|
|
"""Return the guest's primary IP, or ``None`` if not yet known."""
|
|
...
|
|
|
|
def list_snapshots(self, handle: VmHandle) -> list[str]:
|
|
"""Return snapshot names defined on ``handle``."""
|
|
...
|
|
|
|
def is_running(self, handle: VmHandle) -> bool:
|
|
"""Return ``True`` if the VM appears in the running-VM list.
|
|
|
|
Implementations MUST NOT rely on guest tools (e.g. ``vmrun
|
|
getGuestIPAddress``) for this check — see ``AGENTS.md`` error #10.
|
|
"""
|
|
...
|