Add safe ISO download and explicit run/open commands
This commit is contained in:
+123
@@ -0,0 +1,123 @@
|
||||
"""Explicit start-if-stopped and new-ISO launch, preserving runtime authority."""
|
||||
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from . import media, session
|
||||
from .profiles import Profile, load_defaults, validate_name
|
||||
from .runtime import Runtime, RuntimeError
|
||||
|
||||
|
||||
HOST_RESERVE_MIB = 768
|
||||
|
||||
|
||||
def available_memory_mib() -> int:
|
||||
"""Linux MemAvailable accounts for reclaimable host caches."""
|
||||
try:
|
||||
for line in Path('/proc/meminfo').read_text().splitlines():
|
||||
if line.startswith('MemAvailable:'):
|
||||
return int(line.split()[1]) // 1024
|
||||
except (OSError, ValueError, IndexError) as exc:
|
||||
raise RuntimeError('Cannot estimate available RAM from /proc/meminfo') from exc
|
||||
raise RuntimeError('Cannot estimate available RAM: MemAvailable is missing')
|
||||
|
||||
|
||||
def _memory_preflight(memory: int) -> None:
|
||||
available = available_memory_mib()
|
||||
if memory + HOST_RESERVE_MIB > available:
|
||||
raise RuntimeError(f'Not enough available RAM: VM needs {memory} MiB plus a {HOST_RESERVE_MIB} MiB host reserve; '
|
||||
f'{available} MiB available. Choose less --memory or stop another VM')
|
||||
|
||||
|
||||
def session_manifest(runtime: Runtime, manifest: Path | None = None) -> Path | None:
|
||||
"""Use an inherited session only when it belongs to these runtime roots."""
|
||||
explicit = manifest is not None
|
||||
inherited = os.environ.get('HYDRA_SESSION_MANIFEST')
|
||||
if not explicit and not inherited:
|
||||
return None
|
||||
path = Path(manifest if explicit else inherited).expanduser().absolute()
|
||||
try:
|
||||
data = json.loads(path.read_text(encoding='utf-8'))
|
||||
if (not isinstance(data, dict) or
|
||||
any(not isinstance(data.get(key), str) or not Path(data[key]).is_absolute()
|
||||
or Path(data[key]) != getattr(runtime, key) for key in ('config', 'state', 'runtime')) or
|
||||
not isinstance(data.get('names'), list) or
|
||||
any(not isinstance(name, str) for name in data['names']) or
|
||||
not isinstance(data.get('terminal'), str) or not Path(data['terminal']).is_absolute() or
|
||||
not isinstance(data.get('directory'), str) or Path(data['directory']) != path.parent):
|
||||
raise ValueError('Invalid session manifest or different runtime roots')
|
||||
for name in data['names']:
|
||||
validate_name(name)
|
||||
return path
|
||||
except (OSError, ValueError) as exc:
|
||||
if explicit:
|
||||
raise session.SessionError('Invalid session manifest; start a fresh Hydra OS session') from exc
|
||||
return None
|
||||
|
||||
|
||||
def _view_preflight(runtime: Runtime, *, no_view: bool, manifest: Path | None) -> Path | None:
|
||||
if no_view:
|
||||
return None
|
||||
selected = session_manifest(runtime, manifest)
|
||||
session._display()
|
||||
session._viewer()
|
||||
return selected
|
||||
|
||||
|
||||
def _show(runtime: Runtime, name: str, *, no_view: bool, windowed: bool,
|
||||
manifest: Path | None) -> int:
|
||||
if no_view:
|
||||
return 0
|
||||
if manifest is not None:
|
||||
return session.request_refresh(manifest, selected=name)
|
||||
return session.view(runtime, name, windowed=windowed)
|
||||
|
||||
|
||||
def open_vm(runtime: Runtime, name: str, *, windowed=False, no_view=False,
|
||||
manifest: Path | None = None) -> int:
|
||||
profile = runtime.profile(name)
|
||||
current = runtime.status(name)
|
||||
status = current['status']
|
||||
if status == 'unknown':
|
||||
raise RuntimeError(f'Cannot open {name}: unknown VM identity; {current.get("error", "inspect its QMP identity before retrying")}')
|
||||
manifest = _view_preflight(runtime, no_view=no_view, manifest=manifest)
|
||||
if status == 'stopped':
|
||||
_memory_preflight(profile.memory)
|
||||
runtime.start(name)
|
||||
# Other verified QEMU states (for example paused) are viewable without a
|
||||
# new process. Runtime.start owns the lock and final identity recheck.
|
||||
return _show(runtime, name, no_view=no_view, windowed=windowed, manifest=manifest)
|
||||
|
||||
|
||||
def run_source(runtime: Runtime, source: str | Path, *, name: str, memory=4096,
|
||||
cpus=2, disk_size='40G', sha256: str | None = None, windowed=False,
|
||||
no_view=False, manifest: Path | None = None, defaults: Path | None = None,
|
||||
progress=None) -> int:
|
||||
validate_name(name)
|
||||
profile_path = runtime.config / f'{name}.toml'
|
||||
if profile_path.exists() or profile_path.is_symlink():
|
||||
raise RuntimeError(f'VM {name} already exists; use hydra open {name}')
|
||||
paths = runtime.paths(name)
|
||||
if paths.disk.parent.exists() or paths.disk.parent.is_symlink():
|
||||
raise RuntimeError(f'Existing VM state/disk for {name}; choose a new name and preserve the old disk')
|
||||
if runtime.status(name)['status'] != 'stopped':
|
||||
raise RuntimeError(f'Cannot create {name}: existing or unknown VM identity')
|
||||
preferences = load_defaults(defaults) if defaults is not None else {}
|
||||
# Validate requested allocations before downloading or creating anything.
|
||||
Profile(name, memory=memory, cpus=cpus, disk_size=disk_size, **preferences)
|
||||
manifest = _view_preflight(runtime, no_view=no_view, manifest=manifest)
|
||||
_memory_preflight(memory)
|
||||
source = str(source)
|
||||
if '://' in source:
|
||||
iso = media.download_iso(source, sha256=sha256, progress=progress)
|
||||
else:
|
||||
iso = media.validate_iso(Path(source).expanduser().resolve(), sha256=sha256)
|
||||
# Recheck after a potentially lengthy transfer. Profile publication itself
|
||||
# is exclusive ('x') and Runtime never replaces an existing disk.
|
||||
if paths.disk.parent.exists() or paths.disk.parent.is_symlink():
|
||||
raise RuntimeError(f'Existing VM state/disk for {name}; choose a new name')
|
||||
_memory_preflight(memory)
|
||||
runtime.add(Profile(name, iso, memory, cpus, disk_size, **preferences))
|
||||
runtime.start(name)
|
||||
return _show(runtime, name, no_view=no_view, windowed=windowed, manifest=manifest)
|
||||
Reference in New Issue
Block a user