vms/run: better defaults for run api

This commit is contained in:
Jörg Thalheim
2024-10-08 19:11:35 +02:00
committed by Mic92
parent 112d7bf2be
commit 9e5de5c8f0
4 changed files with 54 additions and 47 deletions

View File

@@ -1,11 +1,19 @@
import base64 import base64
import time import time
import types import types
from dataclasses import dataclass
from clan_cli.errors import ClanError from clan_cli.errors import ClanError
from clan_cli.qemu.qmp import QEMUMonitorProtocol from clan_cli.qemu.qmp import QEMUMonitorProtocol
@dataclass
class VmCommandResult:
returncode: int
stdout: str | None
stderr: str | None
# qga is almost like qmp, but not quite, because: # qga is almost like qmp, but not quite, because:
# - server doesn't send initial message # - server doesn't send initial message
# - no need to initialize by asking for capabilities # - no need to initialize by asking for capabilities
@@ -35,10 +43,17 @@ class QgaSession:
if result_pid is None: if result_pid is None:
msg = "Could not get PID from QGA" msg = "Could not get PID from QGA"
raise ClanError(msg) raise ClanError(msg)
try:
return result_pid["return"]["pid"] return result_pid["return"]["pid"]
except KeyError as e:
if "error" in result_pid:
msg = f"Could not run command: {result_pid['error']['desc']}"
raise ClanError(msg) from e
msg = f"PID could not be found: {result_pid}"
raise ClanError(msg) from e
# run, wait for result, return exitcode and output # run, wait for result, return exitcode and output
def run(self, cmd: list[str], check: bool = False) -> tuple[int, str, str]: def run(self, cmd: list[str], check: bool = True) -> VmCommandResult:
pid = self.run_nonblocking(cmd) pid = self.run_nonblocking(cmd)
# loop until exited=true # loop until exited=true
while True: while True:
@@ -54,17 +69,14 @@ class QgaSession:
time.sleep(0.1) time.sleep(0.1)
exitcode = result["return"]["exitcode"] exitcode = result["return"]["exitcode"]
stdout = ( err_data = result["return"].get("err-data")
"" stdout = None
if "out-data" not in result["return"] stderr = None
else base64.b64decode(result["return"]["out-data"]).decode("utf-8") if out_data := result["return"].get("out-data"):
) stdout = base64.b64decode(out_data).decode("utf-8")
stderr = ( if err_data is not None:
"" stderr = base64.b64decode(err_data).decode("utf-8")
if "err-data" not in result["return"]
else base64.b64decode(result["return"]["err-data"]).decode("utf-8")
)
if check and exitcode != 0: if check and exitcode != 0:
msg = f"Command on guest failed\nCommand: {cmd}\nExitcode {exitcode}\nStdout: {stdout}\nStderr: {stderr}" msg = f"Command on guest failed\nCommand: {cmd}\nExitcode {exitcode}\nStdout: {stdout}\nStderr: {stderr}"
raise ClanError(msg) raise ClanError(msg)
return exitcode, stdout, stderr return VmCommandResult(exitcode, stdout, stderr)

View File

@@ -5,10 +5,10 @@ import logging
import os import os
import subprocess import subprocess
import time import time
from dataclasses import dataclass
from collections.abc import Iterator from collections.abc import Iterator
from concurrent.futures import ThreadPoolExecutor from concurrent.futures import ThreadPoolExecutor
from contextlib import ExitStack, contextmanager from contextlib import ExitStack, contextmanager
from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from tempfile import TemporaryDirectory from tempfile import TemporaryDirectory
@@ -324,8 +324,7 @@ def run_vm(
runtime_config: RuntimeConfig, runtime_config: RuntimeConfig,
) -> CmdOut: ) -> CmdOut:
stdin = None stdin = None
# if command is not None: if runtime_config.command is not None:
# stdin = subprocess.DEVNULL
stdin = subprocess.DEVNULL stdin = subprocess.DEVNULL
with ( with (
spawn_vm( spawn_vm(

View File

@@ -107,23 +107,21 @@ def test_vm_deployment(
qga_m1 = stack.enter_context(vm1.qga_connect()) qga_m1 = stack.enter_context(vm1.qga_connect())
qga_m2 = stack.enter_context(vm2.qga_connect()) qga_m2 = stack.enter_context(vm2.qga_connect())
# check my_secret is deployed # check my_secret is deployed
_, out, _ = qga_m1.run( result = qga_m1.run(["cat", "/run/secrets/vars/m1_generator/my_secret"])
["cat", "/run/secrets/vars/m1_generator/my_secret"], check=True assert result.stdout == "hello\n"
)
assert out == "hello\n"
# check shared_secret is deployed on m1 # check shared_secret is deployed on m1
_, out, _ = qga_m1.run( result = qga_m1.run(
["cat", "/run/secrets/vars/my_shared_generator/shared_secret"], check=True ["cat", "/run/secrets/vars/my_shared_generator/shared_secret"]
) )
assert out == "hello\n" assert result.stdout == "hello\n"
# check shared_secret is deployed on m2 # check shared_secret is deployed on m2
_, out, _ = qga_m2.run( result = qga_m2.run(
["cat", "/run/secrets/vars/my_shared_generator/shared_secret"], check=True ["cat", "/run/secrets/vars/my_shared_generator/shared_secret"]
) )
assert out == "hello\n" assert result.stdout == "hello\n"
# check no_deploy_secret is not deployed # check no_deploy_secret is not deployed
returncode, out, _ = qga_m1.run( result = qga_m1.run(
["test", "-e", "/run/secrets/vars/my_shared_generator/no_deploy_secret"], ["test", "-e", "/run/secrets/vars/my_shared_generator/no_deploy_secret"],
check=False, check=False,
) )
assert returncode != 0 assert result.returncode != 0

View File

@@ -90,38 +90,36 @@ def test_vm_persistence(
with spawn_vm(vm_config) as vm, vm.qga_connect() as qga: with spawn_vm(vm_config) as vm, vm.qga_connect() as qga:
# create state via qmp command instead of systemd service # create state via qmp command instead of systemd service
qga.run(["sh", "-c", "echo 'dream2nix' > /var/my-state/root"], check=True) qga.run(["/bin/sh", "-c", "echo 'dream2nix' > /var/my-state/root"])
qga.run(["sh", "-c", "echo 'dream2nix' > /var/my-state/test"], check=True) qga.run(["/bin/sh", "-c", "echo 'dream2nix' > /var/my-state/test"])
qga.run(["sh", "-c", "chown test /var/my-state/test"], check=True) qga.run(["/bin/sh", "-c", "chown test /var/my-state/test"])
qga.run(["sh", "-c", "chown test /var/user-state"], check=True) qga.run(["/bin/sh", "-c", "chown test /var/user-state"])
qga.run(["sh", "-c", "touch /var/my-state/rebooting"], check=True) qga.run_nonblocking(["shutdown", "-h", "now"])
## start vm again ## start vm again
with spawn_vm(vm_config) as vm, vm.qga_connect() as qga: with spawn_vm(vm_config) as vm, vm.qga_connect() as qga:
# check state exists # check state exists
qga.run(["cat", "/var/my-state/test"], check=True) qga.run(["cat", "/var/my-state/test"])
# ensure root file is owned by root # ensure root file is owned by root
qga.run(["stat", "-c", "%U", "/var/my-state/root"], check=True) qga.run(["stat", "-c", "%U", "/var/my-state/root"])
# ensure test file is owned by test # ensure test file is owned by test
qga.run(["stat", "-c", "%U", "/var/my-state/test"], check=True) qga.run(["stat", "-c", "%U", "/var/my-state/test"])
# ensure /var/user-state is owned by test # ensure /var/user-state is owned by test
qga.run(["stat", "-c", "%U", "/var/user-state"], check=True) qga.run(["stat", "-c", "%U", "/var/user-state"])
# ensure that the file created by the service is still there and has the expected content # ensure that the file created by the service is still there and has the expected content
exitcode, out, err = qga.run(["cat", "/var/my-state/test"]) result = qga.run(["cat", "/var/my-state/test"])
assert exitcode == 0, err assert result.stdout == "dream2nix\n", result.stdout
assert out == "dream2nix\n", out
# check for errors # check for errors
exitcode, out, err = qga.run(["cat", "/var/my-state/error"]) result = qga.run(["cat", "/var/my-state/error"], check=False)
assert exitcode == 1, out assert result.returncode == 1, result.stdout
# check all systemd services are OK, or print details # check all systemd services are OK, or print details
exitcode, out, err = qga.run( result = qga.run(
[ [
"sh", "/bin/sh",
"-c", "-c",
"systemctl --failed | tee /tmp/log | grep -q '0 loaded units listed' || ( cat /tmp/log && false )", "systemctl --failed | tee /tmp/log | grep -q '0 loaded units listed' || ( cat /tmp/log && false )",
] ],
) )
assert exitcode == 0, out