2023-07-22 23:59:49 +03:00
|
|
|
import logging
|
|
|
|
|
|
|
|
import libvirt
|
|
|
|
|
2023-09-02 00:52:28 +03:00
|
|
|
from ..exceptions import VMError
|
2023-08-31 20:37:41 +03:00
|
|
|
from ..volume import VolumeInfo
|
2023-07-29 14:29:37 +03:00
|
|
|
from .base import VirtualMachineBase
|
2023-07-22 23:59:49 +03:00
|
|
|
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
2023-07-29 14:29:37 +03:00
|
|
|
class VirtualMachine(VirtualMachineBase):
|
2023-07-22 23:59:49 +03:00
|
|
|
|
|
|
|
@property
|
|
|
|
def name(self):
|
2023-08-27 23:42:56 +03:00
|
|
|
return self.domain_name
|
2023-07-22 23:59:49 +03:00
|
|
|
|
|
|
|
@property
|
|
|
|
def status(self) -> str:
|
|
|
|
"""
|
|
|
|
Return VM state: 'running', 'shutoff', etc. Reference:
|
|
|
|
https://libvirt.org/html/libvirt-libvirt-domain.html#virDomainState
|
|
|
|
"""
|
2023-07-28 01:01:32 +03:00
|
|
|
try:
|
|
|
|
# libvirt returns list [state: int, reason: int]
|
|
|
|
# https://libvirt.org/html/libvirt-libvirt-domain.html#virDomainGetState
|
|
|
|
state = self.domain.state()[0]
|
|
|
|
except libvirt.libvirtError as err:
|
2023-08-24 22:36:12 +03:00
|
|
|
raise VMError(
|
2023-08-27 23:42:56 +03:00
|
|
|
f'Cannot fetch VM status vm={self.domain_name}: {err}') from err
|
2023-08-24 22:36:12 +03:00
|
|
|
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)
|
2023-07-22 23:59:49 +03:00
|
|
|
|
|
|
|
@property
|
|
|
|
def is_running(self) -> bool:
|
|
|
|
"""Return True if VM is running, else return False."""
|
|
|
|
if self.domain.isActive() != 1:
|
|
|
|
# inactive (0) or error (-1)
|
|
|
|
return False
|
|
|
|
return True
|
|
|
|
|
2023-07-28 01:01:32 +03:00
|
|
|
@property
|
|
|
|
def is_autostart(self) -> bool:
|
|
|
|
"""Return True if VM autostart is enabled, else return False."""
|
|
|
|
try:
|
|
|
|
if self.domain.autostart() == 1:
|
|
|
|
return True
|
|
|
|
return False
|
|
|
|
except libvirt.libvirtError as err:
|
2023-08-24 22:36:12 +03:00
|
|
|
raise VMError(
|
2023-08-27 23:42:56 +03:00
|
|
|
f'Cannot get autostart status vm={self.domain_name}: {err}'
|
2023-08-24 22:36:12 +03:00
|
|
|
) from err
|
2023-07-28 01:01:32 +03:00
|
|
|
|
2023-07-22 23:59:49 +03:00
|
|
|
def start(self) -> None:
|
|
|
|
"""Start defined VM."""
|
2023-08-27 23:42:56 +03:00
|
|
|
logger.info('Starting VM: vm=%s', self.domain_name)
|
2023-07-22 23:59:49 +03:00
|
|
|
if self.is_running:
|
2023-08-27 23:42:56 +03:00
|
|
|
logger.warning('VM vm=%s is already started, nothing to do',
|
2023-08-31 20:37:41 +03:00
|
|
|
self.domain_name)
|
2023-07-22 23:59:49 +03:00
|
|
|
return
|
|
|
|
try:
|
2023-07-29 15:35:36 +03:00
|
|
|
self.domain.create()
|
2023-07-22 23:59:49 +03:00
|
|
|
except libvirt.libvirtError as err:
|
2023-08-27 23:42:56 +03:00
|
|
|
raise VMError(
|
|
|
|
f'Cannot start vm={self.domain_name}: {err}') from err
|
2023-07-22 23:59:49 +03:00
|
|
|
|
2023-08-31 20:37:41 +03:00
|
|
|
def shutdown(self, method: str | None = None) -> None:
|
2023-07-22 23:59:49 +03:00
|
|
|
"""
|
2023-08-24 22:36:12 +03:00
|
|
|
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
|
2023-08-27 23:42:56 +03:00
|
|
|
* SIGKILL - send SIGKILL to QEMU process. May corrupt guest data!
|
2023-08-24 22:36:12 +03:00
|
|
|
If mode is not passed use 'NORMAL' mode.
|
2023-07-22 23:59:49 +03:00
|
|
|
"""
|
2023-08-31 20:37:41 +03:00
|
|
|
METHODS = {
|
2023-08-24 22:36:12 +03:00
|
|
|
'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
|
|
|
|
}
|
2023-08-31 20:37:41 +03:00
|
|
|
if method is None:
|
|
|
|
method = 'NORMAL'
|
|
|
|
if not isinstance(method, str):
|
|
|
|
raise ValueError(f"Mode must be a 'str', not {type(method)}")
|
|
|
|
if method.upper() not in METHODS:
|
|
|
|
raise ValueError(f"Unsupported mode: '{method}'")
|
2023-07-28 01:01:32 +03:00
|
|
|
try:
|
2023-08-31 20:37:41 +03:00
|
|
|
if method in ['GUEST_AGENT', 'NORMAL']:
|
|
|
|
self.domain.shutdownFlags(flags=METHODS.get(method))
|
|
|
|
elif method in ['SIGTERM', 'SIGKILL']:
|
|
|
|
self.domain.destroyFlags(flags=METHODS.get(method))
|
2023-07-28 01:01:32 +03:00
|
|
|
except libvirt.libvirtError as err:
|
2023-08-27 23:42:56 +03:00
|
|
|
raise VMError(f'Cannot shutdown vm={self.domain_name} with '
|
2023-08-31 20:37:41 +03:00
|
|
|
f'method={method}: {err}') from err
|
2023-07-22 23:59:49 +03:00
|
|
|
|
2023-08-24 22:36:12 +03:00
|
|
|
def reset(self) -> None:
|
2023-07-22 23:59:49 +03:00
|
|
|
"""
|
2023-07-28 01:01:32 +03:00
|
|
|
Copypaste from libvirt doc:
|
2023-07-22 23:59:49 +03:00
|
|
|
|
2023-07-28 01:01:32 +03:00
|
|
|
Reset a domain immediately without any guest OS shutdown.
|
|
|
|
Reset emulates the power reset button on a machine, where all
|
|
|
|
hardware sees the RST line set and reinitializes internal state.
|
2023-07-22 23:59:49 +03:00
|
|
|
|
2023-07-28 01:01:32 +03:00
|
|
|
Note that there is a risk of data loss caused by reset without any
|
|
|
|
guest OS shutdown.
|
2023-07-22 23:59:49 +03:00
|
|
|
"""
|
2023-07-28 01:01:32 +03:00
|
|
|
try:
|
2023-07-29 14:29:37 +03:00
|
|
|
self.domain.reset()
|
2023-07-28 01:01:32 +03:00
|
|
|
except libvirt.libvirtError as err:
|
2023-08-27 23:42:56 +03:00
|
|
|
raise VMError(
|
|
|
|
f'Cannot reset vm={self.domain_name}: {err}') from err
|
2023-07-22 23:59:49 +03:00
|
|
|
|
|
|
|
def reboot(self) -> None:
|
|
|
|
"""Send ACPI signal to guest OS to reboot. OS may ignore this."""
|
2023-07-28 01:01:32 +03:00
|
|
|
try:
|
|
|
|
self.domain.reboot()
|
|
|
|
except libvirt.libvirtError as err:
|
2023-08-27 23:42:56 +03:00
|
|
|
raise VMError(
|
|
|
|
f'Cannot reboot vm={self.domain_name}: {err}') from err
|
2023-07-28 01:01:32 +03:00
|
|
|
|
2023-08-27 23:42:56 +03:00
|
|
|
def set_autostart(self, enable: bool) -> None:
|
2023-07-28 01:01:32 +03:00
|
|
|
"""
|
|
|
|
Configure VM to be automatically started when the host machine boots.
|
|
|
|
"""
|
2023-08-24 22:36:12 +03:00
|
|
|
if enable:
|
2023-07-28 01:01:32 +03:00
|
|
|
autostart_flag = 1
|
|
|
|
else:
|
|
|
|
autostart_flag = 0
|
|
|
|
try:
|
|
|
|
self.domain.setAutostart(autostart_flag)
|
|
|
|
except libvirt.libvirtError as err:
|
2023-08-27 23:42:56 +03:00
|
|
|
raise VMError(f'Cannot set autostart vm={self.domain_name} '
|
2023-08-24 22:36:12 +03:00
|
|
|
f'autostart={autostart_flag}: {err}') from err
|
2023-07-22 23:59:49 +03:00
|
|
|
|
2023-08-27 23:42:56 +03:00
|
|
|
def set_vcpus(self, nvcpus: int, hotplug: bool = False):
|
|
|
|
"""
|
|
|
|
Set vCPUs for VM. If `hotplug` is True set vCPUs on running VM.
|
|
|
|
If VM is not running set `hotplug` to False. If `hotplug` is True
|
|
|
|
and VM is not currently running vCPUs will set in config and will
|
|
|
|
applied when machine boot.
|
|
|
|
|
|
|
|
NB: Note that if this call is executed before the guest has
|
|
|
|
finished booting, the guest may fail to process the change.
|
|
|
|
"""
|
|
|
|
if nvcpus == 0:
|
|
|
|
raise VMError(f'Cannot set zero vCPUs vm={self.domain_name}')
|
|
|
|
if hotplug and self.domain_info['state'] == libvirt.VIR_DOMAIN_RUNNING:
|
2023-09-23 21:24:56 +03:00
|
|
|
flags = (libvirt.VIR_DOMAIN_AFFECT_LIVE |
|
2023-08-27 23:42:56 +03:00
|
|
|
libvirt.VIR_DOMAIN_AFFECT_CONFIG)
|
|
|
|
else:
|
|
|
|
flags = libvirt.VIR_DOMAIN_AFFECT_CONFIG
|
|
|
|
try:
|
|
|
|
self.domain.setVcpusFlags(nvcpus, flags=flags)
|
|
|
|
except libvirt.libvirtError as err:
|
|
|
|
raise VMError(
|
|
|
|
f'Cannot set vCPUs for vm={self.domain_name}: {err}') from err
|
|
|
|
|
|
|
|
def set_memory(self, memory: int, hotplug: bool = False):
|
|
|
|
"""
|
|
|
|
Set momory for VM. `memory` must be passed in mebibytes. Internally
|
|
|
|
converted to kibibytes. If `hotplug` is True set memory for running
|
|
|
|
VM, else set memory in config and will applied when machine boot.
|
|
|
|
If `hotplug` is True and machine is not currently running set memory
|
|
|
|
in config.
|
|
|
|
"""
|
|
|
|
if memory == 0:
|
|
|
|
raise VMError(f'Cannot set zero memory vm={self.domain_name}')
|
|
|
|
if hotplug and self.domain_info['state'] == libvirt.VIR_DOMAIN_RUNNING:
|
2023-09-23 21:24:56 +03:00
|
|
|
flags = (libvirt.VIR_DOMAIN_AFFECT_LIVE |
|
2023-08-27 23:42:56 +03:00
|
|
|
libvirt.VIR_DOMAIN_AFFECT_CONFIG)
|
|
|
|
else:
|
|
|
|
flags = libvirt.VIR_DOMAIN_AFFECT_CONFIG
|
|
|
|
try:
|
2023-09-23 21:24:56 +03:00
|
|
|
self.domain.setMemoryFlags(memory * 1024,
|
|
|
|
libvirt.VIR_DOMAIN_MEM_MAXIMUM)
|
|
|
|
self.domain.setMemoryFlags(memory * 1024, flags=flags)
|
2023-08-27 23:42:56 +03:00
|
|
|
except libvirt.libvirtError as err:
|
|
|
|
raise VMError(
|
2023-09-23 21:24:56 +03:00
|
|
|
f'Cannot set memory for vm={self.domain_name} {memory=}: {err}') from err
|
2023-08-27 23:42:56 +03:00
|
|
|
|
2023-09-23 21:24:56 +03:00
|
|
|
def attach_device(self, device_info: 'DeviceInfo', hotplug: bool = False):
|
2023-08-31 20:37:41 +03:00
|
|
|
if hotplug and self.domain_info['state'] == libvirt.VIR_DOMAIN_RUNNING:
|
2023-09-23 21:24:56 +03:00
|
|
|
flags = (libvirt.VIR_DOMAIN_AFFECT_LIVE |
|
2023-08-31 20:37:41 +03:00
|
|
|
libvirt.VIR_DOMAIN_AFFECT_CONFIG)
|
|
|
|
else:
|
|
|
|
flags = libvirt.VIR_DOMAIN_AFFECT_CONFIG
|
2023-09-23 21:24:56 +03:00
|
|
|
self.domain.attachDeviceFlags(device_info.to_xml(), flags=flags)
|
2023-07-22 23:59:49 +03:00
|
|
|
|
2023-09-23 21:24:56 +03:00
|
|
|
def detach_device(self, device_info: 'DeviceInfo', hotplug: bool = False):
|
2023-08-31 20:37:41 +03:00
|
|
|
if hotplug and self.domain_info['state'] == libvirt.VIR_DOMAIN_RUNNING:
|
2023-09-23 21:24:56 +03:00
|
|
|
flags = (libvirt.VIR_DOMAIN_AFFECT_LIVE |
|
2023-08-31 20:37:41 +03:00
|
|
|
libvirt.VIR_DOMAIN_AFFECT_CONFIG)
|
|
|
|
else:
|
|
|
|
flags = libvirt.VIR_DOMAIN_AFFECT_CONFIG
|
2023-09-23 21:24:56 +03:00
|
|
|
self.domain.detachDeviceFlags(device_info.to_xml(), flags=flags)
|
2023-08-31 20:37:41 +03:00
|
|
|
|
|
|
|
def resize_volume(self, vol_info: VolumeInfo, online: bool = False):
|
|
|
|
# Этот метод должен принимать описание волюма и в зависимости от
|
|
|
|
# флага online вызывать virStorageVolResize или virDomainBlockResize
|
|
|
|
# https://libvirt.org/html/libvirt-libvirt-domain.html#virDomainBlockResize
|
2023-07-22 23:59:49 +03:00
|
|
|
pass
|
|
|
|
|
2023-08-24 22:36:12 +03:00
|
|
|
def list_ssh_keys(self, user: str):
|
2023-07-22 23:59:49 +03:00
|
|
|
pass
|
|
|
|
|
2023-08-24 22:36:12 +03:00
|
|
|
def set_ssh_keys(self, user: str):
|
2023-07-22 23:59:49 +03:00
|
|
|
pass
|
|
|
|
|
2023-08-24 22:36:12 +03:00
|
|
|
def remove_ssh_keys(self, user: str):
|
2023-07-22 23:59:49 +03:00
|
|
|
pass
|
|
|
|
|
2023-09-23 21:24:56 +03:00
|
|
|
def set_user_password(self, user: str, password: str) -> None:
|
2023-08-31 20:37:41 +03:00
|
|
|
self.domain.setUserPassword(user, password)
|
2023-08-27 23:42:56 +03:00
|
|
|
|
|
|
|
def dump_xml(self) -> str:
|
|
|
|
return self.domain.XMLDesc()
|
|
|
|
|
2023-09-23 21:24:56 +03:00
|
|
|
def delete(self, delete_volumes: bool = False) -> None:
|
2023-08-27 23:42:56 +03:00
|
|
|
"""Undefine VM."""
|
|
|
|
self.shutdown(method='SIGTERM')
|
|
|
|
self.domain.undefine()
|
2023-09-23 21:24:56 +03:00
|
|
|
# todo: delete local volumes
|