clan_cli: Made qmp implementation lazy
This commit is contained in:
parent
ef6d7cee1a
commit
87dbc99cab
@ -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.
|
||||||
|
@ -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"]
|
||||||
|
|
||||||
|
@ -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")
|
||||||
|
@ -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 {
|
||||||
|
@ -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(
|
||||||
|
@ -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()
|
||||||
|
Loading…
Reference in New Issue
Block a user