clan_cli: Made qmp implementation lazy

This commit is contained in:
Luis Hebendanz 2024-02-13 15:15:43 +07:00
parent ef6d7cee1a
commit 87dbc99cab
6 changed files with 110 additions and 51 deletions

View File

@ -1,11 +1,11 @@
import json import json
import logging import logging
from collections.abc import Generator
from contextlib import contextmanager
from os import path from os import path
from pathlib import Path from pathlib import Path
from tempfile import NamedTemporaryFile from tempfile import NamedTemporaryFile, TemporaryDirectory
from time import sleep
from clan_cli.dirs import vm_state_dir
from qemu.qmp import QEMUMonitorProtocol from qemu.qmp import QEMUMonitorProtocol
from ..cmd import run from ..cmd import run
@ -16,6 +16,39 @@ from ..ssh import Host, parse_deployment_address
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
class VMAttr:
def __init__(self, machine_name: str) -> None:
self.temp_dir = TemporaryDirectory(prefix="clan_vm-", suffix=f"-{machine_name}")
self._qmp_socket: Path = Path(self.temp_dir.name) / "qmp.sock"
self._qga_socket: Path = Path(self.temp_dir.name) / "qga.sock"
self._qmp: QEMUMonitorProtocol | None = None
@contextmanager
def qmp(self) -> Generator[QEMUMonitorProtocol, None, None]:
if self._qmp is None:
log.debug(f"qmp_socket: {self._qmp_socket}")
self._qmp = QEMUMonitorProtocol(path.realpath(self._qmp_socket))
self._qmp.connect()
try:
yield self._qmp
finally:
self._qmp.close()
@property
def qmp_socket(self) -> Path:
if self._qmp is None:
log.debug(f"qmp_socket: {self._qmp_socket}")
self._qmp = QEMUMonitorProtocol(path.realpath(self._qmp_socket))
return self._qmp_socket
@property
def qga_socket(self) -> Path:
if self._qmp is None:
log.debug(f"qmp_socket: {self.qga_socket}")
self._qmp = QEMUMonitorProtocol(path.realpath(self._qmp_socket))
return self._qga_socket
class Machine: class Machine:
def __init__( def __init__(
self, self,
@ -36,14 +69,8 @@ class Machine:
self.build_cache: dict[str, Path] = {} self.build_cache: dict[str, Path] = {}
self._deployment_info: None | dict[str, str] = deployment_info self._deployment_info: None | dict[str, str] = deployment_info
state_dir = vm_state_dir(flake_url=str(self.flake), vm_name=self.name)
self.qmp_socket: Path = state_dir / "qmp.sock" self.vm: VMAttr = VMAttr(name)
self.qga_socket: Path = state_dir / "qga.sock"
log.debug(f"qmp_socket: {self.qmp_socket}")
self._qmp = QEMUMonitorProtocol(path.realpath(self.qmp_socket))
self._qmp_connected = False
def __str__(self) -> str: def __str__(self) -> str:
return f"Machine(name={self.name}, flake={self.flake})" return f"Machine(name={self.name}, flake={self.flake})"
@ -60,28 +87,6 @@ class Machine:
) )
return self._deployment_info return self._deployment_info
def qmp_connect(self) -> None:
if not self._qmp_connected:
tries = 100
for num in range(tries):
try:
# the socket file link might be outdated, therefore re-init the qmp object
self._qmp = QEMUMonitorProtocol(path.realpath(self.qmp_socket))
self._qmp.connect()
self._qmp_connected = True
log.debug("QMP Connected")
return
except FileNotFoundError:
if num < 99:
sleep(0.1)
continue
else:
raise
def qmp_command(self, command: str) -> dict:
self.qmp_connect()
return self._qmp.command(command)
@property @property
def target_host_address(self) -> str: def target_host_address(self) -> str:
# deploymentAddress is deprecated. # deploymentAddress is deprecated.

View File

@ -146,7 +146,9 @@ def qemu_command(
# TODO move this to the Machines class # TODO move this to the Machines class
def build_vm(machine: Machine, vm: VmConfig, nix_options: list[str]) -> dict[str, str]: def build_vm(
machine: Machine, vm: VmConfig, nix_options: list[str] = []
) -> dict[str, str]:
config = nix_config() config = nix_config()
system = config["system"] system = config["system"]

View File

@ -1,7 +1,10 @@
import os import os
import sys import sys
import tempfile
import threading import threading
import traceback import traceback
from collections.abc import Generator
from contextlib import contextmanager
from pathlib import Path from pathlib import Path
from time import sleep from time import sleep
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
@ -21,6 +24,34 @@ if TYPE_CHECKING:
no_kvm = not os.path.exists("/dev/kvm") no_kvm = not os.path.exists("/dev/kvm")
@contextmanager
def monkeypatch_tempdir_with_custom_path(
*, monkeypatch: pytest.MonkeyPatch, custom_path: str, prefix_condition: str
) -> Generator[None, None, None]:
# Custom wrapper function that checks the prefix and either modifies the behavior or falls back to the original
class CustomTemporaryDirectory(tempfile.TemporaryDirectory):
def __init__(
self,
suffix: str | None = None,
prefix: str | None = None,
dir: str | None = None, # noqa: A002
) -> None:
if prefix == prefix_condition:
self.name = custom_path # Use the custom path
self._finalizer = None # Prevent cleanup attempts on the custom path by the original finalizer
else:
super().__init__(suffix=suffix, prefix=prefix, dir=dir)
# Use ExitStack to ensure unpatching
try:
# Patch the TemporaryDirectory with our custom class
monkeypatch.setattr(tempfile, "TemporaryDirectory", CustomTemporaryDirectory)
yield # This allows the code within the 'with' block of this context manager to run
finally:
# Unpatch the TemporaryDirectory
monkeypatch.undo()
def run_vm_in_thread(machine_name: str) -> None: def run_vm_in_thread(machine_name: str) -> None:
# runs machine and prints exceptions # runs machine and prints exceptions
def run() -> None: def run() -> None:
@ -125,22 +156,26 @@ def test_vm_qmp(
# 'clan vms run' must be executed from within the flake # 'clan vms run' must be executed from within the flake
monkeypatch.chdir(flake.path) monkeypatch.chdir(flake.path)
# the state dir is a point of reference for qemu interactions as it links to the qga/qmp sockets with monkeypatch_tempdir_with_custom_path(
state_dir = vm_state_dir(str(flake.path), "my_machine") monkeypatch=monkeypatch,
custom_path=str(temporary_home / "vm-tmp"),
prefix_condition="clan_vm-",
):
# the state dir is a point of reference for qemu interactions as it links to the qga/qmp sockets
state_dir = vm_state_dir(str(flake.path), "my_machine")
# start the VM
run_vm_in_thread("my_machine")
# start the VM # connect with qmp
run_vm_in_thread("my_machine") qmp = qmp_connect(state_dir)
# connect with qmp # verify that issuing a command works
qmp = qmp_connect(state_dir) # result = qmp.cmd_obj({"execute": "query-status"})
result = qmp.command("query-status")
assert result["status"] == "running", result
# verify that issuing a command works # shutdown machine (prevent zombie qemu processes)
# result = qmp.cmd_obj({"execute": "query-status"}) qmp.command("system_powerdown")
result = qmp.command("query-status")
assert result["status"] == "running", result
# shutdown machine (prevent zombie qemu processes)
qmp.command("system_powerdown")
@pytest.mark.skipif(no_kvm, reason="Requires KVM") @pytest.mark.skipif(no_kvm, reason="Requires KVM")

View File

@ -12,8 +12,8 @@ avatar {
} }
.trust { .trust {
padding-top: 25px; padding-top: 25px;
padding-bottom: 25px; padding-bottom: 25px;
} }
.join-list { .join-list {
@ -22,11 +22,16 @@ avatar {
} }
.progress-bar {
margin-right: 25px;
min-width: 200px;
}
.group-list { .group-list {
background-color: inherit; background-color: inherit;
} }
.group-list > .activatable:hover { .group-list > .activatable:hover {
background-color: unset; background-color: unset;
} }
.group-list > row { .group-list > row {

View File

@ -115,6 +115,8 @@ class VM(GObject.Object):
self._logs_id: int = 0 self._logs_id: int = 0
self._log_file: IO[str] | None = None self._log_file: IO[str] | None = None
self.progress_bar: Gtk.ProgressBar = Gtk.ProgressBar() self.progress_bar: Gtk.ProgressBar = Gtk.ProgressBar()
self.progress_bar.hide()
self.progress_bar.set_hexpand(True) # Horizontally expand
self.prog_bar_id: int = 0 self.prog_bar_id: int = 0
self.log_dir = tempfile.TemporaryDirectory( self.log_dir = tempfile.TemporaryDirectory(
prefix="clan_vm-", suffix=f"-{self.data.flake.flake_attr}" prefix="clan_vm-", suffix=f"-{self.data.flake.flake_attr}"
@ -144,10 +146,12 @@ class VM(GObject.Object):
def build_vm(self, vm: "VM", _vm: "VM", building: bool) -> None: def build_vm(self, vm: "VM", _vm: "VM", building: bool) -> None:
if building: if building:
log.info("Building VM") log.info("Building VM")
self.progress_bar.show()
self.prog_bar_id = GLib.timeout_add(100, self._pulse_progress_bar) self.prog_bar_id = GLib.timeout_add(100, self._pulse_progress_bar)
if self.prog_bar_id == 0: if self.prog_bar_id == 0:
raise ClanError("Couldn't spawn a progess bar task") raise ClanError("Couldn't spawn a progess bar task")
else: else:
self.progress_bar.hide()
if not GLib.Source.remove(self.prog_bar_id): if not GLib.Source.remove(self.prog_bar_id):
log.error("Failed to remove progress bar task") log.error("Failed to remove progress bar task")
log.info("VM built") log.info("VM built")
@ -157,7 +161,14 @@ class VM(GObject.Object):
vm = vms.run.inspect_vm(self.machine) vm = vms.run.inspect_vm(self.machine)
GLib.idle_add(self.emit, "build_vm", self, True) GLib.idle_add(self.emit, "build_vm", self, True)
vms.run.build_vm(self.machine, vm, []) self.process = spawn(
on_except=None,
log_dir=Path(str(self.log_dir.name)),
func=vms.run.build_vm,
machine=self.machine,
vm=vm,
)
self.process.proc.join()
GLib.idle_add(self.emit, "build_vm", self, False) GLib.idle_add(self.emit, "build_vm", self, False)
self.process = spawn( self.process = spawn(

View File

@ -164,7 +164,8 @@ class ClanList(Gtk.Box):
box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5) box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5)
box.set_valign(Gtk.Align.CENTER) box.set_valign(Gtk.Align.CENTER)
box.append(vm.progress_bar) box.append(vm.progress_bar)
row.add_suffix(box) box.set_homogeneous(False)
row.add_suffix(box) # This allows children to have different sizes
# ==== Action buttons ==== # ==== Action buttons ====
switch = Gtk.Switch() switch = Gtk.Switch()