various updates
This commit is contained in:
		
							
								
								
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -2,5 +2,8 @@ __pycache__/
 | 
				
			|||||||
*.pyc
 | 
					*.pyc
 | 
				
			||||||
*~
 | 
					*~
 | 
				
			||||||
domain.xml
 | 
					domain.xml
 | 
				
			||||||
 | 
					domgen.py
 | 
				
			||||||
na
 | 
					na
 | 
				
			||||||
dist/
 | 
					dist/
 | 
				
			||||||
 | 
					P@ssw0rd
 | 
				
			||||||
 | 
					*.todo
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										9
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										9
									
								
								Makefile
									
									
									
									
									
								
							@@ -1,3 +1,5 @@
 | 
				
			|||||||
 | 
					SRC = na/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
all: build
 | 
					all: build
 | 
				
			||||||
 | 
					
 | 
				
			||||||
build:
 | 
					build:
 | 
				
			||||||
@@ -6,3 +8,10 @@ build:
 | 
				
			|||||||
clean:
 | 
					clean:
 | 
				
			||||||
	[ -d dist/ ] && rm -rf dist/ || true
 | 
						[ -d dist/ ] && rm -rf dist/ || true
 | 
				
			||||||
	find . -type d -name __pycache__ -exec rm -rf {} \; > /dev/null 2>&1 || true
 | 
						find . -type d -name __pycache__ -exec rm -rf {} \; > /dev/null 2>&1 || true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					format:
 | 
				
			||||||
 | 
						isort --lai 2 $(SRC)
 | 
				
			||||||
 | 
						autopep8 -riva --experimental --ignore e255 $(SRC)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					lint:
 | 
				
			||||||
 | 
						pylint $(SRC)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -123,6 +123,8 @@ print(domain_xml.to_string())
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
- [ ] Установка ВМ
 | 
					- [ ] Установка ВМ
 | 
				
			||||||
    - [x] Конструктор XML (базовый)
 | 
					    - [x] Конструктор XML (базовый)
 | 
				
			||||||
 | 
					    - [ ] Метод создания дисков
 | 
				
			||||||
 | 
					    - [ ] Дефайн, запуск и автостарт ВМ
 | 
				
			||||||
- [ ] Управление дисками
 | 
					- [ ] Управление дисками
 | 
				
			||||||
- [ ] Удаление ВМ
 | 
					- [ ] Удаление ВМ
 | 
				
			||||||
- [ ] Изменение CPU
 | 
					- [ ] Изменение CPU
 | 
				
			||||||
@@ -130,12 +132,12 @@ print(domain_xml.to_string())
 | 
				
			|||||||
- [ ] Миграция ВМ между нодами
 | 
					- [ ] Миграция ВМ между нодами
 | 
				
			||||||
- [x] Работа с qemu-ga
 | 
					- [x] Работа с qemu-ga
 | 
				
			||||||
- [x] Управление питанием
 | 
					- [x] Управление питанием
 | 
				
			||||||
- [ ] Вкл/выкл автостарт ВМ
 | 
					- [x] Вкл/выкл автостарт ВМ
 | 
				
			||||||
- [ ] Статистика потребления ресурсов
 | 
					- [ ] Статистика потребления ресурсов
 | 
				
			||||||
- [ ] Получение инфомрации из/о ВМ
 | 
					- [ ] Получение инфомрации из/о ВМ
 | 
				
			||||||
- [ ] SSH-ключи
 | 
					- [ ] SSH-ключи
 | 
				
			||||||
- [ ] Сеть
 | 
					- [ ] Сеть
 | 
				
			||||||
- [ ] ???
 | 
					- [ ] Создание снапшотов
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Заметки
 | 
					# Заметки
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,3 +1,3 @@
 | 
				
			|||||||
from .main import LibvirtSession
 | 
					 | 
				
			||||||
from .config import ConfigLoader
 | 
					from .config import ConfigLoader
 | 
				
			||||||
 | 
					from .session import LibvirtSession
 | 
				
			||||||
from .vm import *
 | 
					from .vm import *
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -13,14 +13,14 @@ Options:
 | 
				
			|||||||
    -9, --sigkill        Send SIGKILL to QEMU process. Not affects without --force
 | 
					    -9, --sigkill        Send SIGKILL to QEMU process. Not affects without --force
 | 
				
			||||||
"""
 | 
					"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import sys
 | 
					 | 
				
			||||||
import pathlib
 | 
					 | 
				
			||||||
import logging
 | 
					import logging
 | 
				
			||||||
 | 
					import pathlib
 | 
				
			||||||
 | 
					import sys
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import libvirt
 | 
					import libvirt
 | 
				
			||||||
from docopt import docopt
 | 
					from docopt import docopt
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from ..main import LibvirtSession
 | 
					from ..session import LibvirtSession
 | 
				
			||||||
from ..vm import VirtualMachine, VMError, VMNotFound
 | 
					from ..vm import VirtualMachine, VMError, VMNotFound
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -10,13 +10,13 @@ Options:
 | 
				
			|||||||
    -t, --timeout <sec>  QEMU timeout in seconds to stop polling command status [default: 60]
 | 
					    -t, --timeout <sec>  QEMU timeout in seconds to stop polling command status [default: 60]
 | 
				
			||||||
"""
 | 
					"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import sys
 | 
					 | 
				
			||||||
import pathlib
 | 
					 | 
				
			||||||
import logging
 | 
					import logging
 | 
				
			||||||
 | 
					import pathlib
 | 
				
			||||||
 | 
					import sys
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from docopt import docopt
 | 
					from docopt import docopt
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from ..main import LibvirtSession
 | 
					from ..session import LibvirtSession
 | 
				
			||||||
from ..vm import QemuAgent, QemuAgentError, VMNotFound
 | 
					from ..vm import QemuAgent, QemuAgentError, VMNotFound
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -30,6 +30,8 @@ class Color:
 | 
				
			|||||||
    YELLOW = '\033[33m'
 | 
					    YELLOW = '\033[33m'
 | 
				
			||||||
    NONE = '\033[0m'
 | 
					    NONE = '\033[0m'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# TODO: Add STDIN support e.g.: cat something.sh | na-vmexec vmname bash
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def cli():
 | 
					def cli():
 | 
				
			||||||
    args = docopt(__doc__)
 | 
					    args = docopt(__doc__)
 | 
				
			||||||
@@ -50,44 +52,28 @@ def cli():
 | 
				
			|||||||
        try:
 | 
					        try:
 | 
				
			||||||
            ga = QemuAgent(session, machine)
 | 
					            ga = QemuAgent(session, machine)
 | 
				
			||||||
            exited, exitcode, stdout, stderr = ga.shellexec(
 | 
					            exited, exitcode, stdout, stderr = ga.shellexec(
 | 
				
			||||||
                cmd,
 | 
					                cmd, executable=shell, capture_output=True, decode_output=True,
 | 
				
			||||||
                executable=shell,
 | 
					                timeout=int(args['--timeout']))
 | 
				
			||||||
                capture_output=True,
 | 
					 | 
				
			||||||
                decode_output=True,
 | 
					 | 
				
			||||||
                timeout=int(args['--timeout']),
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
        except QemuAgentError as qemuerr:
 | 
					        except QemuAgentError as qemuerr:
 | 
				
			||||||
            errmsg = f'{Color.RED}{err}{Color.NONE}'
 | 
					            errmsg = f'{Color.RED}{qemuerr}{Color.NONE}'
 | 
				
			||||||
            if str(err).startswith('Polling command pid='):
 | 
					            if str(qemuerr).startswith('Polling command pid='):
 | 
				
			||||||
                errmsg = (
 | 
					                errmsg = (errmsg + Color.YELLOW +
 | 
				
			||||||
                    errmsg + Color.YELLOW
 | 
					                          '\n[NOTE: command may still running]' + Color.NONE)
 | 
				
			||||||
                    + '\n[NOTE: command may still running]'
 | 
					 | 
				
			||||||
                    + Color.NONE
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
            sys.exit(errmsg)
 | 
					            sys.exit(errmsg)
 | 
				
			||||||
        except VMNotFound as err:
 | 
					        except VMNotFound as err:
 | 
				
			||||||
            sys.exit(
 | 
					            sys.exit(f'{Color.RED}VM {machine} not found{Color.NONE}')
 | 
				
			||||||
                f'{Color.RED}VM {machine} not found.{Color.NONE}'
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if not exited:
 | 
					    if not exited:
 | 
				
			||||||
        print(
 | 
					        print(Color.YELLOW + '[NOTE: command may still running]' + Color.NONE,
 | 
				
			||||||
            Color.YELLOW
 | 
					              file=sys.stderr)
 | 
				
			||||||
            +'[NOTE: command may still running]'
 | 
					 | 
				
			||||||
            + Color.NONE,
 | 
					 | 
				
			||||||
            file=sys.stderr
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
    else:
 | 
					    else:
 | 
				
			||||||
        if exitcode == 0:
 | 
					        if exitcode == 0:
 | 
				
			||||||
            exitcolor = Color.GREEN
 | 
					            exitcolor = Color.GREEN
 | 
				
			||||||
        else:
 | 
					        else:
 | 
				
			||||||
            exitcolor = Color.RED
 | 
					            exitcolor = Color.RED
 | 
				
			||||||
        print(
 | 
					        print(exitcolor + f'[command exited with exit code {exitcode}]' +
 | 
				
			||||||
            exitcolor
 | 
					              Color.NONE,
 | 
				
			||||||
            + f'[command exited with exit code {exitcode}]'
 | 
					              file=sys.stderr)
 | 
				
			||||||
            + Color.NONE,
 | 
					 | 
				
			||||||
            file=sys.stderr
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if stderr:
 | 
					    if stderr:
 | 
				
			||||||
        print(Color.RED + stderr.strip() + Color.NONE, file=sys.stderr)
 | 
					        print(Color.RED + stderr.strip() + Color.NONE, file=sys.stderr)
 | 
				
			||||||
@@ -95,5 +81,6 @@ def cli():
 | 
				
			|||||||
        print(Color.GREEN + stdout.strip() + Color.NONE, file=sys.stdout)
 | 
					        print(Color.GREEN + stdout.strip() + Color.NONE, file=sys.stdout)
 | 
				
			||||||
    sys.exit(exitcode)
 | 
					    sys.exit(exitcode)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
if __name__ == '__main__':
 | 
					if __name__ == '__main__':
 | 
				
			||||||
    cli()
 | 
					    cli()
 | 
				
			||||||
@@ -1,7 +1,7 @@
 | 
				
			|||||||
import os
 | 
					import os
 | 
				
			||||||
import tomllib
 | 
					import tomllib
 | 
				
			||||||
from pathlib import Path
 | 
					 | 
				
			||||||
from collections import UserDict
 | 
					from collections import UserDict
 | 
				
			||||||
 | 
					from pathlib import Path
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
NODEAGENT_CONFIG_FILE = os.getenv('NODEAGENT_CONFIG_FILE')
 | 
					NODEAGENT_CONFIG_FILE = os.getenv('NODEAGENT_CONFIG_FILE')
 | 
				
			||||||
@@ -9,15 +9,16 @@ NODEAGENT_DEFAULT_CONFIG_FILE = '/etc/node-agent/config.toml'
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ConfigLoadError(Exception):
 | 
					class ConfigLoadError(Exception):
 | 
				
			||||||
    """Bad config file syntax, unreachable file or bad data."""
 | 
					    """Bad config file syntax, unreachable file or bad config schema."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ConfigLoader(UserDict):
 | 
					class ConfigLoader(UserDict):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __init__(self, file: Path | None = None):
 | 
					    def __init__(self, file: Path | None = None):
 | 
				
			||||||
        if file is None:
 | 
					        if file is None:
 | 
				
			||||||
            file = NODEAGENT_CONFIG_FILE or NODEAGENT_DEFAULT_CONFIG_FILE
 | 
					            file = NODEAGENT_CONFIG_FILE or NODEAGENT_DEFAULT_CONFIG_FILE
 | 
				
			||||||
        self.file = Path(file)
 | 
					        self.file = Path(file)
 | 
				
			||||||
        self.data = self._load()
 | 
					        super().__init__(self._load())
 | 
				
			||||||
        # todo: load deafult configuration
 | 
					        # todo: load deafult configuration
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def _load(self):
 | 
					    def _load(self):
 | 
				
			||||||
@@ -26,6 +27,12 @@ class ConfigLoader(UserDict):
 | 
				
			|||||||
                return tomllib.load(config)
 | 
					                return tomllib.load(config)
 | 
				
			||||||
                # todo: config schema validation
 | 
					                # todo: config schema validation
 | 
				
			||||||
        except tomllib.TOMLDecodeError as tomlerr:
 | 
					        except tomllib.TOMLDecodeError as tomlerr:
 | 
				
			||||||
            raise ConfigLoadError(f'Bad TOML syntax in config file: {self.file}: {tomlerr}') from tomlerr
 | 
					            raise ConfigLoadError(
 | 
				
			||||||
 | 
					                f'Bad TOML syntax in config file: {self.file}: {tomlerr}'
 | 
				
			||||||
 | 
					            ) from tomlerr
 | 
				
			||||||
        except (OSError, ValueError) as readerr:
 | 
					        except (OSError, ValueError) as readerr:
 | 
				
			||||||
            raise ConfigLoadError(f'Cannot read config file: {self.file}: {readerr}') from readerr
 | 
					            raise ConfigLoadError(
 | 
				
			||||||
 | 
					                f'Cannot read config file: {self.file}: {readerr}') from readerr
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def reload(self):
 | 
				
			||||||
 | 
					        self.data = self._load()
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,5 @@
 | 
				
			|||||||
from pathlib import Path
 | 
					 | 
				
			||||||
from contextlib import AbstractContextManager
 | 
					from contextlib import AbstractContextManager
 | 
				
			||||||
 | 
					from pathlib import Path
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import libvirt
 | 
					import libvirt
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -7,10 +7,11 @@ from .config import ConfigLoader
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class LibvirtSessionError(Exception):
 | 
					class LibvirtSessionError(Exception):
 | 
				
			||||||
    """Something went wrong while connecting to libvirt."""
 | 
					    """Something went wrong while connecting to libvirtd."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class LibvirtSession(AbstractContextManager):
 | 
					class LibvirtSession(AbstractContextManager):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __init__(self, config: Path | None = None):
 | 
					    def __init__(self, config: Path | None = None):
 | 
				
			||||||
        self.config = ConfigLoader(config)
 | 
					        self.config = ConfigLoader(config)
 | 
				
			||||||
        self.session = self._connect(self.config['libvirt']['uri'])
 | 
					        self.session = self._connect(self.config['libvirt']['uri'])
 | 
				
			||||||
@@ -26,8 +27,7 @@ class LibvirtSession(AbstractContextManager):
 | 
				
			|||||||
            return libvirt.open(connection_uri)
 | 
					            return libvirt.open(connection_uri)
 | 
				
			||||||
        except libvirt.libvirtError as err:
 | 
					        except libvirt.libvirtError as err:
 | 
				
			||||||
            raise LibvirtSessionError(
 | 
					            raise LibvirtSessionError(
 | 
				
			||||||
                f'Failed to open connection to the hypervisor: {err}'
 | 
					                f'Failed to open connection to the hypervisor: {err}') from err
 | 
				
			||||||
            ) from err
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def close(self) -> None:
 | 
					    def close(self) -> None:
 | 
				
			||||||
        self.session.close()
 | 
					        self.session.close()
 | 
				
			||||||
							
								
								
									
										1
									
								
								node_agent/utils/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								node_agent/utils/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					from .mac import *
 | 
				
			||||||
							
								
								
									
										16
									
								
								node_agent/utils/mac.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								node_agent/utils/mac.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,16 @@
 | 
				
			|||||||
 | 
					import random
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def random_mac() -> str:
 | 
				
			||||||
 | 
					    """Retrun random MAC address."""
 | 
				
			||||||
 | 
					    mac = [0x00, 0x16, 0x3e,
 | 
				
			||||||
 | 
					           random.randint(0x00, 0x7f),
 | 
				
			||||||
 | 
					           random.randint(0x00, 0xff),
 | 
				
			||||||
 | 
					           random.randint(0x00, 0xff)]
 | 
				
			||||||
 | 
					    return ':'.join(map(lambda x: "%02x" % x, mac))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def unique_mac() -> str:
 | 
				
			||||||
 | 
					    """Return non-conflicting MAC address."""
 | 
				
			||||||
 | 
					    # todo: see virtinst.DeviceInterface.generate_mac
 | 
				
			||||||
 | 
					    raise NotImplementedError()
 | 
				
			||||||
@@ -1,7 +1,20 @@
 | 
				
			|||||||
from pathlib import Path
 | 
					from pathlib import Path
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from lxml.etree import Element, SubElement, QName, tostring
 | 
					 | 
				
			||||||
from lxml.builder import E
 | 
					from lxml.builder import E
 | 
				
			||||||
 | 
					from lxml.etree import Element, QName, SubElement, tostring
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from .mac import random_mac
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					XPATH_DOMAIN_NAME = '/domain/name'
 | 
				
			||||||
 | 
					XPATH_DOMAIN_TITLE = '/domain/title'
 | 
				
			||||||
 | 
					XPATH_DOMAIN_DESCRIPTION = '/domain/description'
 | 
				
			||||||
 | 
					XPATH_DOMAIN_METADATA = '/domain/metadata'
 | 
				
			||||||
 | 
					XPATH_DOMAIN_MEMORY = '/domain/memory'
 | 
				
			||||||
 | 
					XPATH_DOMAIN_CURRENT_MEMORY = '/domain/currentMemory'
 | 
				
			||||||
 | 
					XPATH_DOMAIN_VCPU = '/domain/vcpu'
 | 
				
			||||||
 | 
					XPATH_DOMAIN_OS = '/domian/os'
 | 
				
			||||||
 | 
					XPATH_DOMAIN_CPU = '/domain/cpu'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class XMLConstructor:
 | 
					class XMLConstructor:
 | 
				
			||||||
@@ -21,17 +34,16 @@ class XMLConstructor:
 | 
				
			|||||||
    def domain_xml(self):
 | 
					    def domain_xml(self):
 | 
				
			||||||
        return self.xml
 | 
					        return self.xml
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def gen_domain_xml(
 | 
					    def gen_domain_xml(self,
 | 
				
			||||||
        self,
 | 
					                       name: str,
 | 
				
			||||||
        name: str,
 | 
					                       title: str,
 | 
				
			||||||
        title: str,
 | 
					                       vcpus: int,
 | 
				
			||||||
        vcpus: int,
 | 
					                       vcpu_vendor: str,
 | 
				
			||||||
        cpu_vendor: str,
 | 
					                       vcpu_model: str,
 | 
				
			||||||
        cpu_model: str,
 | 
					                       memory: int,
 | 
				
			||||||
        memory: int,
 | 
					                       volume: Path,
 | 
				
			||||||
        volume: Path,
 | 
					                       vcpu_features: dict | None = None,
 | 
				
			||||||
        desc: str = ""
 | 
					                       desc: str = "") -> None:
 | 
				
			||||||
    ) -> None:
 | 
					 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Generate default domain XML configuration for virtual machines.
 | 
					        Generate default domain XML configuration for virtual machines.
 | 
				
			||||||
        See https://lxml.de/tutorial.html#the-e-factory for details.
 | 
					        See https://lxml.de/tutorial.html#the-e-factory for details.
 | 
				
			||||||
@@ -54,9 +66,10 @@ class XMLConstructor:
 | 
				
			|||||||
                E.apic(),
 | 
					                E.apic(),
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
            E.cpu(
 | 
					            E.cpu(
 | 
				
			||||||
                E.vendor(cpu_vendor),
 | 
					                E.vendor(vcpu_vendor),
 | 
				
			||||||
                E.model(cpu_model, fallback='forbid'),
 | 
					                E.model(vcpu_model, fallback='forbid'),
 | 
				
			||||||
                E.topology(sockets='1', dies='1', cores=str(vcpus), threads='1'),
 | 
					                E.topology(sockets='1', dies='1', cores=str(vcpus),
 | 
				
			||||||
 | 
					                           threads='1'),
 | 
				
			||||||
                mode='custom',
 | 
					                mode='custom',
 | 
				
			||||||
                match='exact',
 | 
					                match='exact',
 | 
				
			||||||
                check='partial',
 | 
					                check='partial',
 | 
				
			||||||
@@ -77,25 +90,35 @@ class XMLConstructor:
 | 
				
			|||||||
                    type='file',
 | 
					                    type='file',
 | 
				
			||||||
                    device='disk',
 | 
					                    device='disk',
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
 | 
					                E.interface(
 | 
				
			||||||
 | 
					                    E.source(network='default'),
 | 
				
			||||||
 | 
					                    E.mac(address=random_mac()),
 | 
				
			||||||
 | 
					                    type='network',
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					                E.graphics(
 | 
				
			||||||
 | 
					                    E.listen(type='address'),
 | 
				
			||||||
 | 
					                    type='vnc', port='-1', autoport='yes'
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					                E.video(
 | 
				
			||||||
 | 
					                    E.model(type='vga', vram='16384', heads='1', primary='yes'),
 | 
				
			||||||
 | 
					                    E.address(type='pci', domain='0x0000', bus='0x00',
 | 
				
			||||||
 | 
					                              slot='0x02', function='0x0'),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
            type='kvm',
 | 
					            type='kvm',
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def gen_volume_xml(
 | 
					    def gen_volume_xml(self,
 | 
				
			||||||
        self,
 | 
					                       device_name: str,
 | 
				
			||||||
        device_name: str,
 | 
					                       file: Path,
 | 
				
			||||||
        file: Path,
 | 
					                       bus: str = 'virtio',
 | 
				
			||||||
        bus: str = 'virtio',
 | 
					                       cache: str = 'writethrough',
 | 
				
			||||||
        cache: str = 'writethrough',
 | 
					                       disktype: str = 'file'):
 | 
				
			||||||
        disktype: str = 'file',
 | 
					        return E.disk(E.driver(name='qemu', type='qcow2', cache=cache),
 | 
				
			||||||
    ):
 | 
					                      E.source(file=file),
 | 
				
			||||||
        return E.disk(
 | 
					                      E.target(dev=device_name, bus=bus),
 | 
				
			||||||
            E.driver(name='qemu', type='qcow2', cache=cache),
 | 
					                      type=disktype,
 | 
				
			||||||
            E.source(file=file),
 | 
					                      device='disk')
 | 
				
			||||||
            E.target(dev=device_name, bus=bus),
 | 
					 | 
				
			||||||
            type=disktype,
 | 
					 | 
				
			||||||
            device='disk'
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def add_volume(self):
 | 
					    def add_volume(self):
 | 
				
			||||||
        raise NotImplementedError()
 | 
					        raise NotImplementedError()
 | 
				
			||||||
@@ -111,21 +134,18 @@ class XMLConstructor:
 | 
				
			|||||||
                data,
 | 
					                data,
 | 
				
			||||||
                namespace=namespace,
 | 
					                namespace=namespace,
 | 
				
			||||||
                nsprefix=nsprefix,
 | 
					                nsprefix=nsprefix,
 | 
				
			||||||
            )
 | 
					            ))
 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        self.xml.replace(metadata_old, metadata)
 | 
					        self.xml.replace(metadata_old, metadata)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def remove_meta(self, namespace: str):
 | 
					    def remove_meta(self, namespace: str):
 | 
				
			||||||
        """Remove metadata by namespace."""
 | 
					        """Remove metadata by namespace."""
 | 
				
			||||||
        raise NotImplementedError()
 | 
					        raise NotImplementedError()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def construct_xml(
 | 
					    def construct_xml(self,
 | 
				
			||||||
        self,
 | 
					                      tag: dict,
 | 
				
			||||||
        tag: dict,
 | 
					                      namespace: str | None = None,
 | 
				
			||||||
        namespace: str | None = None,
 | 
					                      nsprefix: str | None = None,
 | 
				
			||||||
        nsprefix: str | None = None,
 | 
					                      root: Element = None) -> Element:
 | 
				
			||||||
        root: Element = None,
 | 
					 | 
				
			||||||
    ) -> Element:
 | 
					 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Shortly this recursive function transforms dictonary to XML.
 | 
					        Shortly this recursive function transforms dictonary to XML.
 | 
				
			||||||
        Return etree.Element built from dict with following structure::
 | 
					        Return etree.Element built from dict with following structure::
 | 
				
			||||||
@@ -148,18 +168,13 @@ class XMLConstructor:
 | 
				
			|||||||
        # Create element
 | 
					        # Create element
 | 
				
			||||||
        if root is None:
 | 
					        if root is None:
 | 
				
			||||||
            if use_ns:
 | 
					            if use_ns:
 | 
				
			||||||
                element = Element(
 | 
					                element = Element(QName(namespace, tag['name']),
 | 
				
			||||||
                    QName(namespace, tag['name']),
 | 
					                                  nsmap={nsprefix: namespace})
 | 
				
			||||||
                    nsmap={nsprefix: namespace},
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
            else:
 | 
					            else:
 | 
				
			||||||
                element = Element(tag['name'])
 | 
					                element = Element(tag['name'])
 | 
				
			||||||
        else:
 | 
					        else:
 | 
				
			||||||
            if use_ns:
 | 
					            if use_ns:
 | 
				
			||||||
                element = SubElement(
 | 
					                element = SubElement(root, QName(namespace, tag['name']))
 | 
				
			||||||
                    root,
 | 
					 | 
				
			||||||
                    QName(namespace, tag['name']),
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
            else:
 | 
					            else:
 | 
				
			||||||
                element = SubElement(root, tag['name'])
 | 
					                element = SubElement(root, tag['name'])
 | 
				
			||||||
        # Fill up element with content
 | 
					        # Fill up element with content
 | 
				
			||||||
@@ -171,16 +186,12 @@ class XMLConstructor:
 | 
				
			|||||||
        if 'children' in tag.keys():
 | 
					        if 'children' in tag.keys():
 | 
				
			||||||
            for child in tag['children']:
 | 
					            for child in tag['children']:
 | 
				
			||||||
                element.append(
 | 
					                element.append(
 | 
				
			||||||
                    self.construct_xml(
 | 
					                    self.construct_xml(child,
 | 
				
			||||||
                        child,
 | 
					                                       namespace=namespace,
 | 
				
			||||||
                        namespace=namespace,
 | 
					                                       nsprefix=nsprefix,
 | 
				
			||||||
                        nsprefix=nsprefix,
 | 
					                                       root=element))
 | 
				
			||||||
                        root=element,
 | 
					 | 
				
			||||||
                    )
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
        return element
 | 
					        return element
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def to_string(self):
 | 
					    def to_string(self):
 | 
				
			||||||
        return tostring(
 | 
					        return (tostring(self.xml, pretty_print=True,
 | 
				
			||||||
            self.xml, pretty_print=True, encoding='utf-8'
 | 
					                         encoding='utf-8').decode().strip())
 | 
				
			||||||
        ).decode().strip()
 | 
					 | 
				
			||||||
@@ -1,3 +1,3 @@
 | 
				
			|||||||
from .main import VirtualMachine
 | 
					 | 
				
			||||||
from .ga import QemuAgent, QemuAgentError
 | 
					 | 
				
			||||||
from .exceptions import *
 | 
					from .exceptions import *
 | 
				
			||||||
 | 
					from .ga import QemuAgent
 | 
				
			||||||
 | 
					from .main import VirtualMachine
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,11 +1,11 @@
 | 
				
			|||||||
import libvirt
 | 
					import libvirt
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from ..main import LibvirtSession
 | 
					from .exceptions import VMError, VMNotFound
 | 
				
			||||||
from .exceptions import VMNotFound
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class VirtualMachineBase:
 | 
					class VirtualMachineBase:
 | 
				
			||||||
    def __init__(self, session: LibvirtSession, name: str):
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, session: 'LibvirtSession', name: str):
 | 
				
			||||||
        self.domname = name
 | 
					        self.domname = name
 | 
				
			||||||
        self.session = session.session  # virConnect object
 | 
					        self.session = session.session  # virConnect object
 | 
				
			||||||
        self.config = session.config  # ConfigLoader object
 | 
					        self.config = session.config  # ConfigLoader object
 | 
				
			||||||
@@ -19,4 +19,4 @@ class VirtualMachineBase:
 | 
				
			|||||||
                return domain
 | 
					                return domain
 | 
				
			||||||
            raise VMNotFound(name)
 | 
					            raise VMNotFound(name)
 | 
				
			||||||
        except libvirt.libvirtError as err:
 | 
					        except libvirt.libvirtError as err:
 | 
				
			||||||
            raise VMNotFound(err) from err
 | 
					            raise VMError(err) from err
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,6 +7,7 @@ class VMError(Exception):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class VMNotFound(Exception):
 | 
					class VMNotFound(Exception):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __init__(self, domain, message='VM not found: {domain}'):
 | 
					    def __init__(self, domain, message='VM not found: {domain}'):
 | 
				
			||||||
        self.domain = domain
 | 
					        self.domain = domain
 | 
				
			||||||
        self.message = message.format(domain=domain)
 | 
					        self.message = message.format(domain=domain)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,19 +1,17 @@
 | 
				
			|||||||
import json
 | 
					import json
 | 
				
			||||||
import logging
 | 
					import logging
 | 
				
			||||||
from time import time, sleep
 | 
					from base64 import b64decode, standard_b64encode
 | 
				
			||||||
from base64 import standard_b64encode, b64decode
 | 
					from time import sleep, time
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import libvirt
 | 
					import libvirt
 | 
				
			||||||
import libvirt_qemu
 | 
					import libvirt_qemu
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from ..main import LibvirtSession
 | 
					 | 
				
			||||||
from .base import VirtualMachineBase
 | 
					from .base import VirtualMachineBase
 | 
				
			||||||
from .exceptions import QemuAgentError
 | 
					from .exceptions import QemuAgentError
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
logger = logging.getLogger(__name__)
 | 
					logger = logging.getLogger(__name__)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
QEMU_TIMEOUT = 60  # seconds
 | 
					QEMU_TIMEOUT = 60  # seconds
 | 
				
			||||||
POLL_INTERVAL = 0.3  # also seconds
 | 
					POLL_INTERVAL = 0.3  # also seconds
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -28,39 +26,30 @@ class QemuAgent(VirtualMachineBase):
 | 
				
			|||||||
    shellexec()
 | 
					    shellexec()
 | 
				
			||||||
        High-level method for executing shell commands on guest. Command
 | 
					        High-level method for executing shell commands on guest. Command
 | 
				
			||||||
        must be passed as string. Wraps execute() method.
 | 
					        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,
 | 
					    def __init__(self,
 | 
				
			||||||
        session: LibvirtSession,
 | 
					                 session: 'LibvirtSession',
 | 
				
			||||||
        name: str,
 | 
					                 name: str,
 | 
				
			||||||
        timeout: int | None = None,
 | 
					                 timeout: int | None = None,
 | 
				
			||||||
        flags: int | None = None
 | 
					                 flags: int | None = None):
 | 
				
			||||||
    ):
 | 
					 | 
				
			||||||
        super().__init__(session, name)
 | 
					        super().__init__(session, name)
 | 
				
			||||||
        self.timeout = timeout or QEMU_TIMEOUT  # timeout for guest agent
 | 
					        self.timeout = timeout or QEMU_TIMEOUT  # timeout for guest agent
 | 
				
			||||||
        self.flags = flags or libvirt_qemu.VIR_DOMAIN_QEMU_MONITOR_COMMAND_DEFAULT
 | 
					        self.flags = flags or libvirt_qemu.VIR_DOMAIN_QEMU_MONITOR_COMMAND_DEFAULT
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def execute(
 | 
					    def execute(self,
 | 
				
			||||||
        self,
 | 
					                command: dict,
 | 
				
			||||||
        command: dict,
 | 
					                stdin: str | None = None,
 | 
				
			||||||
        stdin: str | None = None,
 | 
					                capture_output: bool = False,
 | 
				
			||||||
        capture_output: bool = False,
 | 
					                decode_output: bool = False,
 | 
				
			||||||
        decode_output: bool = False,
 | 
					                wait: bool = True,
 | 
				
			||||||
        wait: bool = True,
 | 
					                timeout: int = QEMU_TIMEOUT
 | 
				
			||||||
        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.
 | 
					        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:
 | 
					        Return values:
 | 
				
			||||||
            tuple(
 | 
					            tuple(
 | 
				
			||||||
                exited: bool | None,
 | 
					                exited: bool | None,
 | 
				
			||||||
@@ -68,15 +57,15 @@ class QemuAgent(VirtualMachineBase):
 | 
				
			|||||||
                stdout: str | None,
 | 
					                stdout: str | None,
 | 
				
			||||||
                stderr: 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
 | 
					        # todo command dict schema validation
 | 
				
			||||||
        if capture_output:
 | 
					        if capture_output:
 | 
				
			||||||
            command['arguments']['capture-output'] = True
 | 
					            command['arguments']['capture-output'] = True
 | 
				
			||||||
        if isinstance(stdin, str):
 | 
					        if isinstance(stdin, str):
 | 
				
			||||||
            command['arguments']['input-data'] = standard_b64encode(
 | 
					            command['arguments']['input-data'] = standard_b64encode(
 | 
				
			||||||
                stdin.encode('utf-8')
 | 
					                stdin.encode('utf-8')).decode('utf-8')
 | 
				
			||||||
            ).decode('utf-8')
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Execute command on guest
 | 
					        # Execute command on guest
 | 
				
			||||||
        cmd_out = self._execute(command)
 | 
					        cmd_out = self._execute(command)
 | 
				
			||||||
@@ -91,19 +80,18 @@ class QemuAgent(VirtualMachineBase):
 | 
				
			|||||||
            )
 | 
					            )
 | 
				
			||||||
        return None, None, None, None
 | 
					        return None, None, None, None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def shellexec(
 | 
					    def shellexec(self,
 | 
				
			||||||
        self,
 | 
					                  command: str,
 | 
				
			||||||
        command: str,
 | 
					                  stdin: str | None = None,
 | 
				
			||||||
        stdin: str | None = None,
 | 
					                  executable: str = '/bin/sh',
 | 
				
			||||||
        executable: str = '/bin/sh',
 | 
					                  capture_output: bool = False,
 | 
				
			||||||
        capture_output: bool = False,
 | 
					                  decode_output: bool = False,
 | 
				
			||||||
        decode_output: bool = False,
 | 
					                  wait: bool = True,
 | 
				
			||||||
        wait: bool = True,
 | 
					                  timeout: int = QEMU_TIMEOUT
 | 
				
			||||||
        timeout: int = QEMU_TIMEOUT,
 | 
					                  ) -> tuple[bool | None, int | None, str | None, str | None]:
 | 
				
			||||||
    ):
 | 
					 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Execute command on guest with selected shell. /bin/sh by default.
 | 
					        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 = {
 | 
					        cmd = {
 | 
				
			||||||
            'execute': 'guest-exec',
 | 
					            'execute': 'guest-exec',
 | 
				
			||||||
@@ -121,7 +109,6 @@ class QemuAgent(VirtualMachineBase):
 | 
				
			|||||||
            timeout=timeout,
 | 
					            timeout=timeout,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
    def _execute(self, command: dict):
 | 
					    def _execute(self, command: dict):
 | 
				
			||||||
        logging.debug('Execute command: vm=%s cmd=%s', self.domname, command)
 | 
					        logging.debug('Execute command: vm=%s cmd=%s', self.domname, command)
 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
@@ -135,19 +122,10 @@ class QemuAgent(VirtualMachineBase):
 | 
				
			|||||||
            raise QemuAgentError(err) from err
 | 
					            raise QemuAgentError(err) from err
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def _get_cmd_result(
 | 
					    def _get_cmd_result(
 | 
				
			||||||
        self,
 | 
					            self, pid: int, decode_output: bool = False, wait: bool = True,
 | 
				
			||||||
        pid: int,
 | 
					            timeout: int = QEMU_TIMEOUT):
 | 
				
			||||||
        decode_output: bool = False,
 | 
					 | 
				
			||||||
        wait: bool = True,
 | 
					 | 
				
			||||||
        timeout: int = QEMU_TIMEOUT,
 | 
					 | 
				
			||||||
    ):
 | 
					 | 
				
			||||||
        """Get executed command result. See GuestAgent.execute() for info."""
 | 
					        """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:
 | 
					        if not wait:
 | 
				
			||||||
            output = json.loads(self._execute(cmd))
 | 
					            output = json.loads(self._execute(cmd))
 | 
				
			||||||
@@ -165,28 +143,23 @@ class QemuAgent(VirtualMachineBase):
 | 
				
			|||||||
                raise QemuAgentError(
 | 
					                raise QemuAgentError(
 | 
				
			||||||
                    f'Polling command pid={pid} took longer than {timeout} seconds.'
 | 
					                    f'Polling command pid={pid} took longer than {timeout} seconds.'
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
        logger.debug(
 | 
					        logger.debug('Polling command pid=%s finished, time taken: %s seconds',
 | 
				
			||||||
            'Polling command pid=%s finished, time taken: %s seconds',
 | 
					                     pid, int(time() - start_time))
 | 
				
			||||||
            pid, int(time()-start_time)
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        return self._return_tuple(output, decode=decode_output)
 | 
					        return self._return_tuple(output, decode=decode_output)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def _return_tuple(self, cmd_output: dict, decode: bool = False):
 | 
					    def _return_tuple(self, output: dict, decode: bool = False):
 | 
				
			||||||
        exited = cmd_output['return']['exited']
 | 
					        output = output['return']
 | 
				
			||||||
        exitcode = cmd_output['return']['exitcode']
 | 
					        exited = output['exited']
 | 
				
			||||||
 | 
					        exitcode = output['exitcode']
 | 
				
			||||||
 | 
					        stdout = stderr = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        try:
 | 
					        if 'out-data' in output.keys():
 | 
				
			||||||
            stdout = cmd_output['return']['out-data']
 | 
					            stdout = output['out-data']
 | 
				
			||||||
            if decode and stdout:
 | 
					        if 'err-data' in output.keys():
 | 
				
			||||||
                stdout = b64decode(stdout).decode('utf-8')
 | 
					            stderr = output['err-data']
 | 
				
			||||||
        except KeyError:
 | 
					 | 
				
			||||||
            stdout = None
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        try:
 | 
					        if decode:
 | 
				
			||||||
            stderr = cmd_output['return']['err-data']
 | 
					            stdout = b64decode(stdout).decode('utf-8') if stdout else None
 | 
				
			||||||
            if decode and stderr:
 | 
					            stderr = b64decode(stderr).decode('utf-8') if stderr else None
 | 
				
			||||||
                stderr = b64decode(stderr).decode('utf-8')
 | 
					 | 
				
			||||||
        except KeyError:
 | 
					 | 
				
			||||||
            stderr = None
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return exited, exitcode, stdout, stderr
 | 
					        return exited, exitcode, stdout, stderr
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -26,24 +26,19 @@ class VirtualMachine(VirtualMachineBase):
 | 
				
			|||||||
            # https://libvirt.org/html/libvirt-libvirt-domain.html#virDomainGetState
 | 
					            # https://libvirt.org/html/libvirt-libvirt-domain.html#virDomainGetState
 | 
				
			||||||
            state = self.domain.state()[0]
 | 
					            state = self.domain.state()[0]
 | 
				
			||||||
        except libvirt.libvirtError as err:
 | 
					        except libvirt.libvirtError as err:
 | 
				
			||||||
            raise VMError(f'Cannot fetch VM status vm={self.domname}: {err}') from err
 | 
					            raise VMError(
 | 
				
			||||||
        match state:
 | 
					                f'Cannot fetch VM status vm={self.domname}: {err}') from err
 | 
				
			||||||
            case libvirt.VIR_DOMAIN_NOSTATE:
 | 
					        STATES = {
 | 
				
			||||||
                return 'nostate'
 | 
					            libvirt.VIR_DOMAIN_NOSTATE: 'nostate',
 | 
				
			||||||
            case libvirt.VIR_DOMAIN_RUNNING:
 | 
					            libvirt.VIR_DOMAIN_RUNNING: 'running',
 | 
				
			||||||
                return 'running'
 | 
					            libvirt.VIR_DOMAIN_BLOCKED: 'blocked',
 | 
				
			||||||
            case libvirt.VIR_DOMAIN_BLOCKED:
 | 
					            libvirt.VIR_DOMAIN_PAUSED: 'paused',
 | 
				
			||||||
                return 'blocked'
 | 
					            libvirt.VIR_DOMAIN_SHUTDOWN: 'shutdown',
 | 
				
			||||||
            case libvirt.VIR_DOMAIN_PAUSED:
 | 
					            libvirt.VIR_DOMAIN_SHUTOFF: 'shutoff',
 | 
				
			||||||
                return 'paused'
 | 
					            libvirt.VIR_DOMAIN_CRASHED: 'crashed',
 | 
				
			||||||
            case libvirt.VIR_DOMAIN_SHUTDOWN:
 | 
					            libvirt.VIR_DOMAIN_PMSUSPENDED: 'pmsuspended',
 | 
				
			||||||
                return 'shutdown'
 | 
					        }
 | 
				
			||||||
            case libvirt.VIR_DOMAIN_SHUTOFF:
 | 
					        return STATES.get(state)
 | 
				
			||||||
                return 'shutoff'
 | 
					 | 
				
			||||||
            case libvirt.VIR_DOMAIN_CRASHED:
 | 
					 | 
				
			||||||
                return 'crashed'
 | 
					 | 
				
			||||||
            case libvirt.VIR_DOMAIN_PMSUSPENDED:
 | 
					 | 
				
			||||||
                return 'pmsuspended'
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @property
 | 
					    @property
 | 
				
			||||||
    def is_running(self) -> bool:
 | 
					    def is_running(self) -> bool:
 | 
				
			||||||
@@ -61,42 +56,53 @@ class VirtualMachine(VirtualMachineBase):
 | 
				
			|||||||
                return True
 | 
					                return True
 | 
				
			||||||
            return False
 | 
					            return False
 | 
				
			||||||
        except libvirt.libvirtError as err:
 | 
					        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:
 | 
					    def start(self) -> None:
 | 
				
			||||||
        """Start defined VM."""
 | 
					        """Start defined VM."""
 | 
				
			||||||
        logger.info('Starting VM: vm=%s', self.domname)
 | 
					        logger.info('Starting VM: vm=%s', self.domname)
 | 
				
			||||||
        if self.is_running:
 | 
					        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
 | 
					            return
 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
            self.domain.create()
 | 
					            self.domain.create()
 | 
				
			||||||
        except libvirt.libvirtError as err:
 | 
					        except libvirt.libvirtError as err:
 | 
				
			||||||
            raise VMError(f'Cannot start vm={self.domname}: {err}') from 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.
 | 
					        Send signal to guest OS to shutdown. Supports several modes:
 | 
				
			||||||
        Use `force=True` for graceful VM destroy. Add `sigkill=True`
 | 
					        * GUEST_AGENT - use guest agent
 | 
				
			||||||
        to hard shutdown (may corrupt guest data!).
 | 
					        * 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:
 | 
					        MODES = {
 | 
				
			||||||
            flags = libvirt.VIR_DOMAIN_DESTROY_DEFAULT
 | 
					            'GUEST_AGENT': libvirt.VIR_DOMAIN_SHUTDOWN_GUEST_AGENT,
 | 
				
			||||||
        else:
 | 
					            'NORMAL': libvirt.VIR_DOMAIN_SHUTDOWN_DEFAULT,
 | 
				
			||||||
            flags = libvirt.VIR_DOMAIN_DESTROY_GRACEFUL
 | 
					            '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:
 | 
					        try:
 | 
				
			||||||
            if force:
 | 
					            if mode in ['GUEST_AGENT', 'NORMAL']:
 | 
				
			||||||
                self.domain.destroyFlags(flags=flags)
 | 
					                self.domain.shutdownFlags(flags=MODES.get(mode))
 | 
				
			||||||
            else:
 | 
					            elif mode in ['SIGTERM', 'SIGKILL']:
 | 
				
			||||||
                # Normal VM shutdown via ACPI signal, OS may ignore this.
 | 
					                self.domain.destroyFlags(flags=MODES.get(mode))
 | 
				
			||||||
                self.domain.shutdown()
 | 
					 | 
				
			||||||
        except libvirt.libvirtError as err:
 | 
					        except libvirt.libvirtError as err:
 | 
				
			||||||
            raise VMError(
 | 
					            raise VMError(f'Cannot shutdown vm={self.domname} with '
 | 
				
			||||||
                f'Cannot shutdown vm={self.domname} '
 | 
					                          f'mode={mode}: {err}') from err
 | 
				
			||||||
                f'force={force} sigkill={sigkill}: {err}'
 | 
					 | 
				
			||||||
            ) from err
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def reset(self):
 | 
					    def reset(self) -> None:
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Copypaste from libvirt doc:
 | 
					        Copypaste from libvirt doc:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -119,35 +125,33 @@ class VirtualMachine(VirtualMachineBase):
 | 
				
			|||||||
        except libvirt.libvirtError as err:
 | 
					        except libvirt.libvirtError as err:
 | 
				
			||||||
            raise VMError(f'Cannot reboot vm={self.domname}: {err}') from 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.
 | 
					        Configure VM to be automatically started when the host machine boots.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        if enabled:
 | 
					        if enable:
 | 
				
			||||||
            autostart_flag = 1
 | 
					            autostart_flag = 1
 | 
				
			||||||
        else:
 | 
					        else:
 | 
				
			||||||
            autostart_flag = 0
 | 
					            autostart_flag = 0
 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
            self.domain.setAutostart(autostart_flag)
 | 
					            self.domain.setAutostart(autostart_flag)
 | 
				
			||||||
        except libvirt.libvirtError as err:
 | 
					        except libvirt.libvirtError as err:
 | 
				
			||||||
            raise VMError(
 | 
					            raise VMError(f'Cannot set autostart vm={self.domname} '
 | 
				
			||||||
                f'Cannot set autostart vm={self.domname} '
 | 
					                          f'autostart={autostart_flag}: {err}') from err
 | 
				
			||||||
                f'autostart={autostart_flag}: {err}'
 | 
					 | 
				
			||||||
            ) from err
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def vcpu_set(self, count: int):
 | 
					    def set_vcpus(self, count: int):
 | 
				
			||||||
        pass
 | 
					        pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def vram_set(self, count: int):
 | 
					    def set_ram(self, count: int):
 | 
				
			||||||
        pass
 | 
					        pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def ssh_keys_list(self, user: str):
 | 
					    def list_ssh_keys(self, user: str):
 | 
				
			||||||
        pass
 | 
					        pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def ssh_keys_add(self, user: str):
 | 
					    def set_ssh_keys(self, user: str):
 | 
				
			||||||
        pass
 | 
					        pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def ssh_keys_remove(self, user: str):
 | 
					    def remove_ssh_keys(self, user: str):
 | 
				
			||||||
        pass
 | 
					        pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def set_user_password(self, user: str):
 | 
					    def set_user_password(self, user: str):
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -12,9 +12,22 @@ lxml = "^4.9.2"  # 4.9.2 on Debian 12
 | 
				
			|||||||
docopt = "^0.6.2"  # 0.6.2 on Debian 12
 | 
					docopt = "^0.6.2"  # 0.6.2 on Debian 12
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[tool.poetry.scripts]
 | 
					[tool.poetry.scripts]
 | 
				
			||||||
na-vmctl = "node_agent.utils.vmctl:cli"
 | 
					na-vmctl = "node_agent.cli.vmctl:cli"
 | 
				
			||||||
na-vmexec = "node_agent.utils.vmexec:cli"
 | 
					na-vmexec = "node_agent.cli.vmexec:cli"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[build-system]
 | 
					[build-system]
 | 
				
			||||||
requires = ["poetry-core"]
 | 
					requires = ["poetry-core"]
 | 
				
			||||||
build-backend = "poetry.core.masonry.api"
 | 
					build-backend = "poetry.core.masonry.api"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[tool.yapf]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[tool.pylint."MESSAGES CONTROL"]
 | 
				
			||||||
 | 
					disable = [
 | 
				
			||||||
 | 
					    "invalid-name",
 | 
				
			||||||
 | 
					    "missing-module-docstring",
 | 
				
			||||||
 | 
					    "missing-class-docstring",
 | 
				
			||||||
 | 
					    "missing-function-docstring",
 | 
				
			||||||
 | 
					    "import-error",
 | 
				
			||||||
 | 
					    "too-many-arguments",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user