refactor(backend): add list_running() to VmBackend seam (W1)
operations.vms.list_live_vms needs a backend-agnostic running-VM list for the web dashboard; is_running now shares the single vmrun-list parse.
This commit is contained in:
@@ -79,3 +79,14 @@ class VmBackend(Protocol):
|
||||
getGuestIPAddress``) for this check — see ``AGENTS.md`` error #10.
|
||||
"""
|
||||
...
|
||||
|
||||
def list_running(self) -> list[VmHandle]:
|
||||
"""Return a handle for every VM the backend reports as running.
|
||||
|
||||
Powers the read-only inventory in the web UI and orphan
|
||||
reconciliation. Like :meth:`is_running`, implementations MUST NOT
|
||||
rely on guest tools (see ``AGENTS.md`` error #10) — a plain
|
||||
power-state listing is enough. Handle ``identifier`` is the
|
||||
backend-native reference (a VMX path on Workstation).
|
||||
"""
|
||||
...
|
||||
|
||||
@@ -140,22 +140,37 @@ class WorkstationVmrunBackend:
|
||||
lines = lines[1:]
|
||||
return lines
|
||||
|
||||
def list_running(self) -> list[VmHandle]:
|
||||
"""``vmrun list`` → one :class:`VmHandle` per running VMX path.
|
||||
|
||||
Skips the ``Total running VMs: N`` header and any ``Error:`` lines
|
||||
(``AGENTS.md`` error #9). Does NOT call ``getGuestIPAddress`` (error
|
||||
#10). The handle ``name`` is the VMX file stem. Returns ``[]`` when
|
||||
``vmrun list`` fails.
|
||||
"""
|
||||
result = self._run("list", check=False)
|
||||
if result.returncode != 0:
|
||||
return []
|
||||
handles: list[VmHandle] = []
|
||||
for raw in (result.stdout or "").splitlines():
|
||||
line = raw.strip()
|
||||
low = line.lower()
|
||||
if not line or low.startswith("total running") or low.startswith("error:"):
|
||||
continue
|
||||
handles.append(VmHandle(identifier=line, name=Path(line).stem))
|
||||
return handles
|
||||
|
||||
def is_running(self, handle: VmHandle) -> bool:
|
||||
"""``vmrun list`` lookup — does NOT call ``getGuestIPAddress``.
|
||||
|
||||
See ``AGENTS.md`` error #10: ``getGuestIPAddress`` blocks for 30-60s
|
||||
without VMware Tools. ``vmrun list`` returns immediately.
|
||||
without VMware Tools. ``vmrun list`` returns immediately. Shares the
|
||||
single parse in :meth:`list_running`.
|
||||
"""
|
||||
result = self._run("list", check=False)
|
||||
if result.returncode != 0:
|
||||
return False
|
||||
target = Path(handle.identifier).resolve()
|
||||
for line in (result.stdout or "").splitlines():
|
||||
line = line.strip()
|
||||
if not line or line.lower().startswith("total running"):
|
||||
continue
|
||||
for running in self.list_running():
|
||||
try:
|
||||
if Path(line).resolve() == target:
|
||||
if Path(running.identifier).resolve() == target:
|
||||
return True
|
||||
except OSError:
|
||||
continue
|
||||
|
||||
@@ -52,3 +52,4 @@ def test_protocol_stubs_are_callable() -> None:
|
||||
assert b.get_ip(h) is None # line 69
|
||||
assert b.list_snapshots(h) is None # line 73
|
||||
assert b.is_running(h) is None # line 81
|
||||
assert b.list_running() is None # list_running stub
|
||||
|
||||
@@ -164,6 +164,36 @@ def test_is_running_returns_false_when_absent(
|
||||
assert backend.is_running(VmHandle(str(vmx))) is False
|
||||
|
||||
|
||||
# ── list_running ─────────────────────────────────────────────────────────────
|
||||
|
||||
def test_list_running_parses_paths_and_skips_noise(
|
||||
monkeypatch: pytest.MonkeyPatch, fake_vmrun: str
|
||||
) -> None:
|
||||
"""Header and ``Error:`` lines are dropped; each VMX becomes a handle."""
|
||||
stdout = (
|
||||
"Total running VMs: 2\n"
|
||||
"/var/lib/ci/build-vms/Clone_a/Clone_a.vmx\n"
|
||||
"Error: The VMware Tools are not running\n"
|
||||
"/var/lib/ci/build-vms/Clone_b/Clone_b.vmx\n"
|
||||
)
|
||||
monkeypatch.setattr(subprocess, "run", lambda *a, **kw: _make_completed(0, stdout=stdout))
|
||||
backend = WorkstationVmrunBackend(vmrun_path=fake_vmrun)
|
||||
handles = backend.list_running()
|
||||
assert [h.identifier for h in handles] == [
|
||||
"/var/lib/ci/build-vms/Clone_a/Clone_a.vmx",
|
||||
"/var/lib/ci/build-vms/Clone_b/Clone_b.vmx",
|
||||
]
|
||||
assert [h.name for h in handles] == ["Clone_a", "Clone_b"]
|
||||
|
||||
|
||||
def test_list_running_returns_empty_on_failure(
|
||||
monkeypatch: pytest.MonkeyPatch, fake_vmrun: str
|
||||
) -> None:
|
||||
monkeypatch.setattr(subprocess, "run", lambda *a, **kw: _make_completed(1, stderr="boom"))
|
||||
backend = WorkstationVmrunBackend(vmrun_path=fake_vmrun)
|
||||
assert backend.list_running() == []
|
||||
|
||||
|
||||
# ── _resolve_vmrun(explicit=None) paths ───────────────────────────────────────
|
||||
|
||||
def test_resolve_vmrun_uses_which(monkeypatch: pytest.MonkeyPatch, fake_vmrun: str) -> None:
|
||||
|
||||
Reference in New Issue
Block a user