feat: support installed guests and distribution display defaults

This commit is contained in:
Codex
2026-10-13 04:27:06 +03:00
parent 533ae511b8
commit 48ee598e9f
8 changed files with 299 additions and 24 deletions
+42 -8
View File
@@ -11,7 +11,7 @@ import subprocess
import tempfile
import time
import uuid
from dataclasses import dataclass
from dataclasses import dataclass, replace
from pathlib import Path
from .profiles import Profile, load_profile, save_profile, validate_name
@@ -64,15 +64,21 @@ def build_qemu_argv(profile: Profile, paths: VMPaths) -> list[str]:
'-mon', 'chardev=qmp,mode=control']
if accel == 'kvm':
argv += ['-cpu', 'host']
for options in (
blockdevs = [
{'driver': 'file', 'filename': str(paths.disk), 'node-name': 'disk-file'},
{'driver': 'qcow2', 'file': 'disk-file', 'node-name': 'disk'},
]
if profile.iso is not None:
blockdevs += [
{'driver': 'file', 'filename': str(profile.iso), 'node-name': 'iso-file', 'read-only': True},
{'driver': 'raw', 'file': 'iso-file', 'node-name': 'iso', 'read-only': True},
):
]
for options in blockdevs:
argv += ['-blockdev', json.dumps(options)]
argv += ['-device', 'virtio-blk-pci,drive=disk', '-device', 'ide-cd,drive=iso',
'-boot', 'order=dc', '-device', 'virtio-net-pci,netdev=net',
argv += ['-device', 'virtio-blk-pci,drive=disk']
if profile.iso is not None:
argv += ['-device', 'ide-cd,drive=iso']
argv += ['-boot', 'order=dc' if profile.iso is not None else 'order=c', '-device', 'virtio-net-pci,netdev=net',
'-netdev', 'user,id=net' if profile.network == 'user'
else f'bridge,id=net,br={profile.bridge}',
'-device', 'qemu-xhci,id=usb', '-device', 'usb-tablet', '-device', 'usb-kbd']
@@ -83,12 +89,15 @@ def build_qemu_argv(profile: Profile, paths: VMPaths) -> list[str]:
'-device', f'usb-redir,chardev=usbredir{channel}']
spice = (f'unix=on,addr={_qemu_value(paths.spice)},disable-ticketing=on,'
'disable-copy-paste=on,disable-agent-file-xfer=on')
mode = f',xres={profile.width},yres={profile.height}'
if profile.refresh_millihz:
mode += f',refresh_rate={profile.refresh_millihz}'
render = _intel_render_node() if profile.graphics == 'auto' else None
if render:
spice += f',gl=on,rendernode={_qemu_value(render)}'
argv += ['-display', f'egl-headless,rendernode={_qemu_value(render)}', '-device', 'virtio-vga-gl']
argv += ['-display', f'egl-headless,rendernode={_qemu_value(render)}', '-device', 'virtio-vga-gl' + mode]
else:
argv += ['-display', 'none', '-device', 'virtio-vga']
argv += ['-display', 'none', '-device', 'virtio-vga' + mode]
return argv + ['-spice', spice]
@@ -125,9 +134,33 @@ class Runtime:
_private_dir(directory)
def add(self, profile: Profile) -> None:
self._validate_media(profile.iso)
_private_dir(self.config)
save_profile(self.config / f'{profile.name}.toml', profile)
@staticmethod
def _validate_media(iso: Path | None) -> None:
if iso is not None and not iso.is_file():
raise RuntimeError(f'ISO does not exist or is not a file: {iso}; attach valid media or detach it while stopped')
def media(self, name: str, iso: Path | None) -> dict:
paths = self.paths(name)
with self._locked(paths):
current = self.status(name)
if current['status'] != 'stopped':
raise RuntimeError(f'Cannot change media for {name}: {current["status"]}')
profile = replace(self.profile(name), iso=iso)
self._validate_media(profile.iso)
destination = self.config / f'{name}.toml'
if destination.is_symlink():
raise RuntimeError('Refusing to replace a symlink profile')
with tempfile.TemporaryDirectory(prefix=f'.{name}-', dir=self.config) as tmp:
temp = Path(tmp) / f'{name}.toml'
save_profile(temp, profile)
temp.chmod(0o600)
os.replace(temp, destination)
return {'name': name, 'iso': str(profile.iso) if profile.iso else None, 'status': 'media-updated'}
def profiles(self) -> list[Profile]:
if not self.config.exists():
return []
@@ -234,12 +267,13 @@ class Runtime:
temp.unlink(missing_ok=True)
def start(self, name: str) -> dict:
profile = self.profile(name)
paths = self.paths(name)
with self._locked(paths):
profile = self.profile(name)
current = self.status(name)
if current['status'] != 'stopped':
raise RuntimeError(f'Cannot start {name}: {current["status"]}')
self._validate_media(profile.iso)
for path in (paths.qmp, paths.spice):
if self._listening(path):
raise RuntimeError(f'Runtime socket is already in use: {path}')