"""Small, testable wrappers around required host-level operations.""" from __future__ import annotations import os import shlex import subprocess from collections.abc import Callable, Sequence from dataclasses import dataclass from pathlib import Path from .config import Settings from .errors import CommandError, UvmError @dataclass(frozen=True, slots=True) class CommandResult: args: tuple[str, ...] returncode: int stdout: str = "" stderr: str = "" class CommandRunner: """Execute host commands while keeping command construction testable.""" def __init__(self, emit: Callable[[str], None] | None = print) -> None: self._emit = emit def run( self, command: Sequence[str | Path], *, check: bool = True, capture: bool = False, input_text: str | None = None, sensitive: bool = False, timeout: float | None = None, ) -> CommandResult: args = tuple(str(part) for part in command) if self._emit is not None: self._emit(f"+ {shlex.join(args)}") try: completed = subprocess.run( args, check=False, text=True, input=input_text, stdout=subprocess.PIPE if capture else None, stderr=subprocess.PIPE if capture else None, timeout=timeout, ) except FileNotFoundError as error: raise CommandError(f"required command was not found: {args[0]}") from error except OSError as error: raise CommandError(f"could not run {args[0]}: {error}") from error except subprocess.TimeoutExpired as error: raise CommandError(f"command timed out: {shlex.join(args)}") from error result = CommandResult( args=args, returncode=completed.returncode, stdout=completed.stdout or "", stderr=completed.stderr or "", ) if check and result.returncode != 0: detail = "" if sensitive else result.stderr.strip() or result.stdout.strip() suffix = f": {detail}" if detail else "" raise CommandError( f"command failed ({result.returncode}): {shlex.join(args)}{suffix}" ) return result def require_root() -> None: if os.geteuid() != 0: raise UvmError("this command needs root. Run it with sudo.") def check_kvm() -> None: kvm = Path("/dev/kvm") if not kvm.exists(): raise UvmError("/dev/kvm does not exist. Enable hardware virtualization/KVM first.") if not os.access(kvm, os.R_OK | os.W_OK): raise UvmError("no read/write access to /dev/kvm. Run as root or grant KVM access.") def ensure_data_directories(settings: Settings) -> None: for path in (settings.base, settings.bin_dir, settings.images_dir, settings.vms_dir): path.mkdir(parents=True, exist_ok=True)