various improvemets
This commit is contained in:
		@@ -27,7 +27,7 @@ import libvirt_qemu
 | 
			
		||||
from compute.exceptions import (
 | 
			
		||||
    GuestAgentCommandNotSupportedError,
 | 
			
		||||
    GuestAgentError,
 | 
			
		||||
    GuestAgentTimeoutExceededError,
 | 
			
		||||
    GuestAgentTimeoutError,
 | 
			
		||||
    GuestAgentUnavailableError,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@@ -199,7 +199,7 @@ class GuestAgent:
 | 
			
		||||
            sleep(poll_interval)
 | 
			
		||||
            now = time()
 | 
			
		||||
            if now - start_time > self.timeout:
 | 
			
		||||
                raise GuestAgentTimeoutExceededError(self.timeout)
 | 
			
		||||
                raise GuestAgentTimeoutError(self.timeout)
 | 
			
		||||
        log.debug(
 | 
			
		||||
            'Polling command pid=%s finished, time taken: %s seconds',
 | 
			
		||||
            pid,
 | 
			
		||||
 
 | 
			
		||||
@@ -322,6 +322,8 @@ class Instance:
 | 
			
		||||
 | 
			
		||||
        :param method: Method used to shutdown instance
 | 
			
		||||
        """
 | 
			
		||||
        if not self.is_running():
 | 
			
		||||
            return
 | 
			
		||||
        methods = {
 | 
			
		||||
            'SOFT': libvirt.VIR_DOMAIN_SHUTDOWN_GUEST_AGENT,
 | 
			
		||||
            'NORMAL': libvirt.VIR_DOMAIN_SHUTDOWN_DEFAULT,
 | 
			
		||||
@@ -498,11 +500,6 @@ class Instance:
 | 
			
		||||
            msg = f'Cannot set memory for instance={self.name} {memory=}: {e}'
 | 
			
		||||
            raise InstanceError(msg) from e
 | 
			
		||||
 | 
			
		||||
    def _get_disk_by_target(self, target: str) -> etree.Element:
 | 
			
		||||
        xml = etree.fromstring(self.dump_xml())  # noqa: S320
 | 
			
		||||
        child = xml.xpath(f'/domain/devices/disk/target[@dev="{target}"]')
 | 
			
		||||
        return child[0].getparent() if child else None
 | 
			
		||||
 | 
			
		||||
    def attach_device(
 | 
			
		||||
        self, device: DeviceConfig, *, live: bool = False
 | 
			
		||||
    ) -> None:
 | 
			
		||||
@@ -520,7 +517,7 @@ class Instance:
 | 
			
		||||
        else:
 | 
			
		||||
            flags = libvirt.VIR_DOMAIN_AFFECT_CONFIG
 | 
			
		||||
        if isinstance(device, DiskConfig):  # noqa: SIM102
 | 
			
		||||
            if self._get_disk_by_target(device.target):
 | 
			
		||||
            if self.get_disk(device.target):
 | 
			
		||||
                log.warning(
 | 
			
		||||
                    "Volume with target '%s' is already attached",
 | 
			
		||||
                    device.target,
 | 
			
		||||
@@ -545,7 +542,7 @@ class Instance:
 | 
			
		||||
        else:
 | 
			
		||||
            flags = libvirt.VIR_DOMAIN_AFFECT_CONFIG
 | 
			
		||||
        if isinstance(device, DiskConfig):  # noqa: SIM102
 | 
			
		||||
            if self._get_disk_by_target(device.target) is None:
 | 
			
		||||
            if self.get_disk(device.target) is None:
 | 
			
		||||
                log.warning(
 | 
			
		||||
                    "Volume with target '%s' is already detached",
 | 
			
		||||
                    device.target,
 | 
			
		||||
@@ -553,6 +550,27 @@ class Instance:
 | 
			
		||||
                return
 | 
			
		||||
        self.domain.detachDeviceFlags(device.to_xml(), flags=flags)
 | 
			
		||||
 | 
			
		||||
    def get_disk(self, name: str) -> DiskConfig | None:
 | 
			
		||||
        """
 | 
			
		||||
        Return :class:`DiskConfig` by disk target name.
 | 
			
		||||
 | 
			
		||||
        Return None if disk with specified target not found.
 | 
			
		||||
 | 
			
		||||
        :param name: Disk name e.g. `vda`, `sda`, etc. This name may
 | 
			
		||||
            not match the name of the disk inside the guest OS.
 | 
			
		||||
        """
 | 
			
		||||
        xml = etree.fromstring(self.dump_xml())
 | 
			
		||||
        child = xml.xpath(f'/domain/devices/disk/target[@dev="{name}"]')
 | 
			
		||||
        if len(child) == 0:
 | 
			
		||||
            return None
 | 
			
		||||
        return DiskConfig.from_xml(child[0].getparent())
 | 
			
		||||
 | 
			
		||||
    def list_disks(self) -> list[DiskConfig]:
 | 
			
		||||
        """Return list of attached disk devices."""
 | 
			
		||||
        xml = etree.fromstring(self.dump_xml())
 | 
			
		||||
        disks = xml.xpath('/domain/devices/disk')
 | 
			
		||||
        return [DiskConfig.from_xml(disk) for disk in disks]
 | 
			
		||||
 | 
			
		||||
    def detach_disk(self, name: str) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        Detach disk device by target name.
 | 
			
		||||
@@ -560,31 +578,17 @@ class Instance:
 | 
			
		||||
        There is no ``attach_disk()`` method. Use :func:`attach_device`
 | 
			
		||||
        with :class:`DiskConfig` as argument.
 | 
			
		||||
 | 
			
		||||
        :param name: Disk name e.g. 'vda', 'sda', etc. This name may
 | 
			
		||||
        :param name: Disk name e.g. `vda`, `sda`, etc. This name may
 | 
			
		||||
            not match the name of the disk inside the guest OS.
 | 
			
		||||
        """
 | 
			
		||||
        xml = self._get_disk_by_target(name)
 | 
			
		||||
        if xml is None:
 | 
			
		||||
        disk = self.get_disk(name)
 | 
			
		||||
        if disk is None:
 | 
			
		||||
            log.warning(
 | 
			
		||||
                "Volume with target '%s' is already detached",
 | 
			
		||||
                name,
 | 
			
		||||
            )
 | 
			
		||||
            return
 | 
			
		||||
        disk_params = {
 | 
			
		||||
            'disk_type': xml.get('type'),
 | 
			
		||||
            'source': xml.find('source').get('file'),
 | 
			
		||||
            'target': xml.find('target').get('dev'),
 | 
			
		||||
            'readonly': False if xml.find('readonly') is None else True,  # noqa: SIM211
 | 
			
		||||
        }
 | 
			
		||||
        for param in disk_params:
 | 
			
		||||
            if disk_params[param] is None:
 | 
			
		||||
                msg = (
 | 
			
		||||
                    f"Cannot detach volume with target '{name}': "
 | 
			
		||||
                    f"parameter '{param}' is not defined in libvirt XML "
 | 
			
		||||
                    'config on host.'
 | 
			
		||||
                )
 | 
			
		||||
                raise InstanceError(msg)
 | 
			
		||||
        self.detach_device(DiskConfig(**disk_params), live=True)
 | 
			
		||||
        self.detach_device(disk, live=True)
 | 
			
		||||
 | 
			
		||||
    def resize_disk(
 | 
			
		||||
        self, name: str, capacity: int, unit: units.DataUnit
 | 
			
		||||
@@ -592,7 +596,8 @@ class Instance:
 | 
			
		||||
        """
 | 
			
		||||
        Resize attached block device.
 | 
			
		||||
 | 
			
		||||
        :param name: Disk device name e.g. `vda`, `sda`, etc.
 | 
			
		||||
        :param name: Disk name e.g. `vda`, `sda`, etc. This name may
 | 
			
		||||
            not match the name of the disk inside the guest OS.
 | 
			
		||||
        :param capacity: New capacity.
 | 
			
		||||
        :param unit: Capacity unit.
 | 
			
		||||
        """
 | 
			
		||||
@@ -602,10 +607,6 @@ class Instance:
 | 
			
		||||
            flags=libvirt.VIR_DOMAIN_BLOCK_RESIZE_BYTES,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def get_disks(self) -> list[DiskConfig]:
 | 
			
		||||
        """Return list of attached disks."""
 | 
			
		||||
        raise NotImplementedError
 | 
			
		||||
 | 
			
		||||
    def pause(self) -> None:
 | 
			
		||||
        """Pause instance."""
 | 
			
		||||
        if not self.is_running():
 | 
			
		||||
@@ -616,31 +617,75 @@ class Instance:
 | 
			
		||||
        """Resume paused instance."""
 | 
			
		||||
        self.domain.resume()
 | 
			
		||||
 | 
			
		||||
    def get_ssh_keys(self, user: str) -> list[str]:
 | 
			
		||||
    def list_ssh_keys(self, user: str) -> list[str]:
 | 
			
		||||
        """
 | 
			
		||||
        Return list of SSH keys on guest for specific user.
 | 
			
		||||
 | 
			
		||||
        :param user: Username.
 | 
			
		||||
        """
 | 
			
		||||
        raise NotImplementedError
 | 
			
		||||
        self.guest_agent.raise_for_commands(['guest-ssh-get-authorized-keys'])
 | 
			
		||||
        exc = self.guest_agent.guest_exec(
 | 
			
		||||
            path='/bin/sh',
 | 
			
		||||
            args=[
 | 
			
		||||
                '-c',
 | 
			
		||||
                (
 | 
			
		||||
                    'su -c "'
 | 
			
		||||
                    'if ! [ -f ~/.ssh/authorized_keys ]; then '
 | 
			
		||||
                    'mkdir -p ~/.ssh && touch ~/.ssh/authorized_keys; '
 | 
			
		||||
                    'fi" '
 | 
			
		||||
                    f'{user}'
 | 
			
		||||
                ),
 | 
			
		||||
            ],
 | 
			
		||||
            capture_output=True,
 | 
			
		||||
            decode_output=True,
 | 
			
		||||
            poll=True,
 | 
			
		||||
        )
 | 
			
		||||
        log.debug(exc)
 | 
			
		||||
        try:
 | 
			
		||||
            return self.domain.authorizedSSHKeysGet(user)
 | 
			
		||||
        except libvirt.libvirtError as e:
 | 
			
		||||
            raise InstanceError(e) from e
 | 
			
		||||
 | 
			
		||||
    def set_ssh_keys(self, user: str, ssh_keys: list[str]) -> None:
 | 
			
		||||
    def set_ssh_keys(
 | 
			
		||||
        self,
 | 
			
		||||
        user: str,
 | 
			
		||||
        keys: list[str],
 | 
			
		||||
        *,
 | 
			
		||||
        remove: bool = False,
 | 
			
		||||
        append: bool = False,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        Add SSH keys to guest for specific user.
 | 
			
		||||
 | 
			
		||||
        :param user: Username.
 | 
			
		||||
        :param ssh_keys: List of public SSH keys.
 | 
			
		||||
        :param keys: List of authorized SSH keys.
 | 
			
		||||
        :param append: Append keys to authorized SSH keys instead of
 | 
			
		||||
            overriding authorized_keys file.
 | 
			
		||||
        :param remove: Remove authorized keys listed in `keys` parameter.
 | 
			
		||||
        """
 | 
			
		||||
        raise NotImplementedError
 | 
			
		||||
 | 
			
		||||
    def delete_ssh_keys(self, user: str, ssh_keys: list[str]) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        Remove SSH keys from guest for specific user.
 | 
			
		||||
 | 
			
		||||
        :param user: Username.
 | 
			
		||||
        :param ssh_keys: List of public SSH keys.
 | 
			
		||||
        """
 | 
			
		||||
        raise NotImplementedError
 | 
			
		||||
        qemu_ga_commands = ['guest-ssh-add-authorized-keys']
 | 
			
		||||
        if remove and append:
 | 
			
		||||
            raise InstanceError(
 | 
			
		||||
                "'append' and 'remove' parameters is mutually exclusive"
 | 
			
		||||
            )
 | 
			
		||||
        if not self.is_running():
 | 
			
		||||
            raise InstanceError(
 | 
			
		||||
                'Cannot add authorized SSH keys to inactive instance'
 | 
			
		||||
            )
 | 
			
		||||
        if append:
 | 
			
		||||
            flags = libvirt.VIR_DOMAIN_AUTHORIZED_SSH_KEYS_SET_APPEND
 | 
			
		||||
        elif remove:
 | 
			
		||||
            flags = libvirt.VIR_DOMAIN_AUTHORIZED_SSH_KEYS_SET_REMOVE
 | 
			
		||||
            qemu_ga_commands = ['guest-ssh-remove-authorized-keys']
 | 
			
		||||
        else:
 | 
			
		||||
            flags = 0
 | 
			
		||||
        if keys.sort() == self.list_ssh_keys().sort():
 | 
			
		||||
            return
 | 
			
		||||
        self.guest_agent.raise_for_commands(qemu_ga_commands)
 | 
			
		||||
        try:
 | 
			
		||||
            self.domain.authorizedSSHKeysSet(user, keys, flags=flags)
 | 
			
		||||
        except libvirt.libvirtError as e:
 | 
			
		||||
            raise InstanceError(e) from e
 | 
			
		||||
 | 
			
		||||
    def set_user_password(
 | 
			
		||||
        self, user: str, password: str, *, encrypted: bool = False
 | 
			
		||||
@@ -655,10 +700,6 @@ class Instance:
 | 
			
		||||
        :param encrypted: Set it to True if password is already encrypted.
 | 
			
		||||
            Right encryption method depends on guest OS.
 | 
			
		||||
        """
 | 
			
		||||
        if not self.guest_agent.is_available():
 | 
			
		||||
            raise InstanceError(
 | 
			
		||||
                'Cannot change password: guest agent is unavailable'
 | 
			
		||||
            )
 | 
			
		||||
        self.guest_agent.raise_for_commands(['guest-set-user-password'])
 | 
			
		||||
        flags = libvirt.VIR_DOMAIN_PASSWORD_ENCRYPTED if encrypted else 0
 | 
			
		||||
        self.domain.setUserPassword(user, password, flags=flags)
 | 
			
		||||
@@ -669,7 +710,10 @@ class Instance:
 | 
			
		||||
        return self.domain.XMLDesc(flags)
 | 
			
		||||
 | 
			
		||||
    def delete(self) -> None:
 | 
			
		||||
        """Undefine instance."""
 | 
			
		||||
        # TODO @ge: delete local disks
 | 
			
		||||
        """Delete instance with local disks."""
 | 
			
		||||
        self.shutdown(method='HARD')
 | 
			
		||||
        for disk in self.list_disks():
 | 
			
		||||
            if disk.disk_type == 'file':
 | 
			
		||||
                volume = self.connection.storageVolLookupByPath(disk.source)
 | 
			
		||||
                volume.delete()
 | 
			
		||||
        self.domain.undefine()
 | 
			
		||||
 
 | 
			
		||||
@@ -19,20 +19,12 @@ import re
 | 
			
		||||
from enum import StrEnum
 | 
			
		||||
from pathlib import Path
 | 
			
		||||
 | 
			
		||||
from pydantic import BaseModel, Extra, validator
 | 
			
		||||
from pydantic import validator
 | 
			
		||||
 | 
			
		||||
from compute.common import EntityModel
 | 
			
		||||
from compute.utils.units import DataUnit
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class EntityModel(BaseModel):
 | 
			
		||||
    """Basic entity model."""
 | 
			
		||||
 | 
			
		||||
    class Config:
 | 
			
		||||
        """Do not allow extra fields."""
 | 
			
		||||
 | 
			
		||||
        extra = Extra.forbid
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CPUEmulationMode(StrEnum):
 | 
			
		||||
    """CPU emulation mode enumerated."""
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user