various updates

This commit is contained in:
ge
2023-08-24 22:36:12 +03:00
parent a0344b703f
commit 91478b8122
18 changed files with 261 additions and 234 deletions

View File

@ -1,3 +1,3 @@
from .main import VirtualMachine
from .ga import QemuAgent, QemuAgentError
from .exceptions import *
from .ga import QemuAgent
from .main import VirtualMachine

View File

@ -1,11 +1,11 @@
import libvirt
from ..main import LibvirtSession
from .exceptions import VMNotFound
from .exceptions import VMError, VMNotFound
class VirtualMachineBase:
def __init__(self, session: LibvirtSession, name: str):
def __init__(self, session: 'LibvirtSession', name: str):
self.domname = name
self.session = session.session # virConnect object
self.config = session.config # ConfigLoader object
@ -19,4 +19,4 @@ class VirtualMachineBase:
return domain
raise VMNotFound(name)
except libvirt.libvirtError as err:
raise VMNotFound(err) from err
raise VMError(err) from err

View File

@ -7,6 +7,7 @@ class VMError(Exception):
class VMNotFound(Exception):
def __init__(self, domain, message='VM not found: {domain}'):
self.domain = domain
self.message = message.format(domain=domain)

View File

@ -1,19 +1,17 @@
import json
import logging
from time import time, sleep
from base64 import standard_b64encode, b64decode
from base64 import b64decode, standard_b64encode
from time import sleep, time
import libvirt
import libvirt_qemu
from ..main import LibvirtSession
from .base import VirtualMachineBase
from .exceptions import QemuAgentError
logger = logging.getLogger(__name__)
QEMU_TIMEOUT = 60 # seconds
POLL_INTERVAL = 0.3 # also seconds
@ -28,39 +26,30 @@ class QemuAgent(VirtualMachineBase):
shellexec()
High-level method for executing shell commands on guest. Command
must be passed as string. Wraps execute() method.
_execute()
Just executes QEMU command. Wraps libvirt_qemu.qemuAgentCommand()
_get_cmd_result()
Intended for long-time commands. This function loops and every
POLL_INTERVAL calls 'guest-exec-status' for specified guest PID.
Polling ends if command exited or on timeout.
_return_tuple()
This method transforms JSON command output to tuple and decode
base64 encoded strings optionally.
"""
def __init__(self,
session: LibvirtSession,
name: str,
timeout: int | None = None,
flags: int | None = None
):
session: 'LibvirtSession',
name: str,
timeout: int | None = None,
flags: int | None = None):
super().__init__(session, name)
self.timeout = timeout or QEMU_TIMEOUT # timeout for guest agent
self.flags = flags or libvirt_qemu.VIR_DOMAIN_QEMU_MONITOR_COMMAND_DEFAULT
def execute(
self,
command: dict,
stdin: str | None = None,
capture_output: bool = False,
decode_output: bool = False,
wait: bool = True,
timeout: int = QEMU_TIMEOUT,
):
def execute(self,
command: dict,
stdin: str | None = None,
capture_output: bool = False,
decode_output: bool = False,
wait: bool = True,
timeout: int = QEMU_TIMEOUT
) -> tuple[bool | None, int | None, str | None, str | None]:
"""
Execute command on guest and return output if capture_output is True.
Execute command on guest and return output if `capture_output` is True.
See https://wiki.qemu.org/Documentation/QMP for QEMU commands reference.
If `wait` is True poll guest command output with POLL_INTERVAL. Raise
QemuAgentError on `timeout` reached (in seconds).
Return values:
tuple(
exited: bool | None,
@ -68,15 +57,15 @@ class QemuAgent(VirtualMachineBase):
stdout: str | None,
stderr: str | None
)
stdout and stderr are base64 encoded strings or None.
stdout and stderr are base64 encoded strings or None. stderr and stdout
will be decoded if `decode_output` is True.
"""
# todo command dict schema validation
if capture_output:
command['arguments']['capture-output'] = True
if isinstance(stdin, str):
command['arguments']['input-data'] = standard_b64encode(
stdin.encode('utf-8')
).decode('utf-8')
stdin.encode('utf-8')).decode('utf-8')
# Execute command on guest
cmd_out = self._execute(command)
@ -91,19 +80,18 @@ class QemuAgent(VirtualMachineBase):
)
return None, None, None, None
def shellexec(
self,
command: str,
stdin: str | None = None,
executable: str = '/bin/sh',
capture_output: bool = False,
decode_output: bool = False,
wait: bool = True,
timeout: int = QEMU_TIMEOUT,
):
def shellexec(self,
command: str,
stdin: str | None = None,
executable: str = '/bin/sh',
capture_output: bool = False,
decode_output: bool = False,
wait: bool = True,
timeout: int = QEMU_TIMEOUT
) -> tuple[bool | None, int | None, str | None, str | None]:
"""
Execute command on guest with selected shell. /bin/sh by default.
Otherwise of execute() this function brings command as string.
Otherwise of execute() this function brings shell command as string.
"""
cmd = {
'execute': 'guest-exec',
@ -121,7 +109,6 @@ class QemuAgent(VirtualMachineBase):
timeout=timeout,
)
def _execute(self, command: dict):
logging.debug('Execute command: vm=%s cmd=%s', self.domname, command)
try:
@ -135,19 +122,10 @@ class QemuAgent(VirtualMachineBase):
raise QemuAgentError(err) from err
def _get_cmd_result(
self,
pid: int,
decode_output: bool = False,
wait: bool = True,
timeout: int = QEMU_TIMEOUT,
):
self, pid: int, decode_output: bool = False, wait: bool = True,
timeout: int = QEMU_TIMEOUT):
"""Get executed command result. See GuestAgent.execute() for info."""
exited = exitcode = stdout = stderr = None
cmd = {
'execute': 'guest-exec-status',
'arguments': {'pid': pid},
}
cmd = {'execute': 'guest-exec-status', 'arguments': {'pid': pid}}
if not wait:
output = json.loads(self._execute(cmd))
@ -165,28 +143,23 @@ class QemuAgent(VirtualMachineBase):
raise QemuAgentError(
f'Polling command pid={pid} took longer than {timeout} seconds.'
)
logger.debug(
'Polling command pid=%s finished, time taken: %s seconds',
pid, int(time()-start_time)
)
logger.debug('Polling command pid=%s finished, time taken: %s seconds',
pid, int(time() - start_time))
return self._return_tuple(output, decode=decode_output)
def _return_tuple(self, cmd_output: dict, decode: bool = False):
exited = cmd_output['return']['exited']
exitcode = cmd_output['return']['exitcode']
def _return_tuple(self, output: dict, decode: bool = False):
output = output['return']
exited = output['exited']
exitcode = output['exitcode']
stdout = stderr = None
try:
stdout = cmd_output['return']['out-data']
if decode and stdout:
stdout = b64decode(stdout).decode('utf-8')
except KeyError:
stdout = None
if 'out-data' in output.keys():
stdout = output['out-data']
if 'err-data' in output.keys():
stderr = output['err-data']
try:
stderr = cmd_output['return']['err-data']
if decode and stderr:
stderr = b64decode(stderr).decode('utf-8')
except KeyError:
stderr = None
if decode:
stdout = b64decode(stdout).decode('utf-8') if stdout else None
stderr = b64decode(stderr).decode('utf-8') if stderr else None
return exited, exitcode, stdout, stderr

View File

@ -26,24 +26,19 @@ class VirtualMachine(VirtualMachineBase):
# https://libvirt.org/html/libvirt-libvirt-domain.html#virDomainGetState
state = self.domain.state()[0]
except libvirt.libvirtError as err:
raise VMError(f'Cannot fetch VM status vm={self.domname}: {err}') from err
match state:
case libvirt.VIR_DOMAIN_NOSTATE:
return 'nostate'
case libvirt.VIR_DOMAIN_RUNNING:
return 'running'
case libvirt.VIR_DOMAIN_BLOCKED:
return 'blocked'
case libvirt.VIR_DOMAIN_PAUSED:
return 'paused'
case libvirt.VIR_DOMAIN_SHUTDOWN:
return 'shutdown'
case libvirt.VIR_DOMAIN_SHUTOFF:
return 'shutoff'
case libvirt.VIR_DOMAIN_CRASHED:
return 'crashed'
case libvirt.VIR_DOMAIN_PMSUSPENDED:
return 'pmsuspended'
raise VMError(
f'Cannot fetch VM status vm={self.domname}: {err}') from err
STATES = {
libvirt.VIR_DOMAIN_NOSTATE: 'nostate',
libvirt.VIR_DOMAIN_RUNNING: 'running',
libvirt.VIR_DOMAIN_BLOCKED: 'blocked',
libvirt.VIR_DOMAIN_PAUSED: 'paused',
libvirt.VIR_DOMAIN_SHUTDOWN: 'shutdown',
libvirt.VIR_DOMAIN_SHUTOFF: 'shutoff',
libvirt.VIR_DOMAIN_CRASHED: 'crashed',
libvirt.VIR_DOMAIN_PMSUSPENDED: 'pmsuspended',
}
return STATES.get(state)
@property
def is_running(self) -> bool:
@ -61,42 +56,53 @@ class VirtualMachine(VirtualMachineBase):
return True
return False
except libvirt.libvirtError as err:
raise VMError(f'Cannot get autostart status vm={self.domname}: {err}') from err
raise VMError(
f'Cannot get autostart status vm={self.domname}: {err}'
) from err
def start(self) -> None:
"""Start defined VM."""
logger.info('Starting VM: vm=%s', self.domname)
if self.is_running:
logger.debug('VM vm=%s is already started, nothing to do', self.domname)
logger.debug('VM vm=%s is already started, nothing to do',
self.domname)
return
try:
self.domain.create()
except libvirt.libvirtError as err:
raise VMError(f'Cannot start vm={self.domname}: {err}') from err
def shutdown(self, force=False, sigkill=False) -> None:
def shutdown(self, mode: str | None = None) -> None:
"""
Send ACPI signal to guest OS to shutdown. OS may ignore this.
Use `force=True` for graceful VM destroy. Add `sigkill=True`
to hard shutdown (may corrupt guest data!).
Send signal to guest OS to shutdown. Supports several modes:
* GUEST_AGENT - use guest agent
* NORMAL - use method choosen by hypervisor to shutdown machine
* SIGTERM - send SIGTERM to QEMU process, destroy machine gracefully
* SIGKILL - send SIGKILL, this option may corrupt guest data!
If mode is not passed use 'NORMAL' mode.
"""
if sigkill:
flags = libvirt.VIR_DOMAIN_DESTROY_DEFAULT
else:
flags = libvirt.VIR_DOMAIN_DESTROY_GRACEFUL
MODES = {
'GUEST_AGENT': libvirt.VIR_DOMAIN_SHUTDOWN_GUEST_AGENT,
'NORMAL': libvirt.VIR_DOMAIN_SHUTDOWN_DEFAULT,
'SIGTERM': libvirt.VIR_DOMAIN_DESTROY_GRACEFUL,
'SIGKILL': libvirt.VIR_DOMAIN_DESTROY_DEFAULT
}
if mode is None:
mode = 'NORMAL'
if not isinstance(mode, str):
raise ValueError(f'Mode must be a string, not {type(mode)}')
if mode.upper() not in MODES:
raise ValueError(f"Unsupported mode: '{mode}'")
try:
if force:
self.domain.destroyFlags(flags=flags)
else:
# Normal VM shutdown via ACPI signal, OS may ignore this.
self.domain.shutdown()
if mode in ['GUEST_AGENT', 'NORMAL']:
self.domain.shutdownFlags(flags=MODES.get(mode))
elif mode in ['SIGTERM', 'SIGKILL']:
self.domain.destroyFlags(flags=MODES.get(mode))
except libvirt.libvirtError as err:
raise VMError(
f'Cannot shutdown vm={self.domname} '
f'force={force} sigkill={sigkill}: {err}'
) from err
raise VMError(f'Cannot shutdown vm={self.domname} with '
f'mode={mode}: {err}') from err
def reset(self):
def reset(self) -> None:
"""
Copypaste from libvirt doc:
@ -119,35 +125,33 @@ class VirtualMachine(VirtualMachineBase):
except libvirt.libvirtError as err:
raise VMError(f'Cannot reboot vm={self.domname}: {err}') from err
def autostart(self, enabled: bool) -> None:
def autostart(self, enable: bool) -> None:
"""
Configure VM to be automatically started when the host machine boots.
"""
if enabled:
if enable:
autostart_flag = 1
else:
autostart_flag = 0
try:
self.domain.setAutostart(autostart_flag)
except libvirt.libvirtError as err:
raise VMError(
f'Cannot set autostart vm={self.domname} '
f'autostart={autostart_flag}: {err}'
) from err
raise VMError(f'Cannot set autostart vm={self.domname} '
f'autostart={autostart_flag}: {err}') from err
def vcpu_set(self, count: int):
def set_vcpus(self, count: int):
pass
def vram_set(self, count: int):
def set_ram(self, count: int):
pass
def ssh_keys_list(self, user: str):
def list_ssh_keys(self, user: str):
pass
def ssh_keys_add(self, user: str):
def set_ssh_keys(self, user: str):
pass
def ssh_keys_remove(self, user: str):
def remove_ssh_keys(self, user: str):
pass
def set_user_password(self, user: str):