various improvements
This commit is contained in:
		
							
								
								
									
										16
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										16
									
								
								Makefile
									
									
									
									
									
								
							@@ -10,14 +10,17 @@ all: docs build-deb
 | 
				
			|||||||
requirements.txt:
 | 
					requirements.txt:
 | 
				
			||||||
	poetry export -f requirements.txt -o requirements.txt
 | 
						poetry export -f requirements.txt -o requirements.txt
 | 
				
			||||||
 | 
					
 | 
				
			||||||
build: format lint
 | 
					build: version format lint
 | 
				
			||||||
	awk '/^version/{print $$3}' pyproject.toml \
 | 
					 | 
				
			||||||
		| xargs -I {} sed "s/__version__ =.*/__version__ = '{}'/" -i $(SRCDIR)/__init__.py
 | 
					 | 
				
			||||||
	poetry build
 | 
						poetry build
 | 
				
			||||||
 | 
					
 | 
				
			||||||
build-deb: build
 | 
					build-deb: build
 | 
				
			||||||
	cd packaging && $(MAKE)
 | 
						cd packaging && $(MAKE)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					version:
 | 
				
			||||||
 | 
						VERSION=$$(awk '/^version/{print $$3}' pyproject.toml); \
 | 
				
			||||||
 | 
							sed "s/__version__ =.*/__version__ = $$VERSION/" -i $(SRCDIR)/__init__.py; \
 | 
				
			||||||
 | 
							sed "s/release =.*/release = $$VERSION/" -i $(DOCS_SRCDIR)/conf.py
 | 
				
			||||||
 | 
					
 | 
				
			||||||
format:
 | 
					format:
 | 
				
			||||||
	poetry run isort $(SRCDIR)
 | 
						poetry run isort $(SRCDIR)
 | 
				
			||||||
	poetry run ruff format $(SRCDIR)
 | 
						poetry run ruff format $(SRCDIR)
 | 
				
			||||||
@@ -32,7 +35,8 @@ docs-versions:
 | 
				
			|||||||
	poetry run sphinx-multiversion $(DOCS_SRCDIR) $(DOCS_BUILDDIR)
 | 
						poetry run sphinx-multiversion $(DOCS_SRCDIR) $(DOCS_BUILDDIR)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
serve-docs:
 | 
					serve-docs:
 | 
				
			||||||
	poetry run sphinx-autobuild $(DOCS_SRCDIR) $(DOCS_BUILDDIR)
 | 
						poetry run sphinx-autobuild $(DOCS_SRCDIR) $(DOCS_BUILDDIR) \
 | 
				
			||||||
 | 
							--pre-build 'make clean'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
clean:
 | 
					clean:
 | 
				
			||||||
	[ -d $(DISTDIR) ] && rm -rf $(DISTDIR) || true
 | 
						[ -d $(DISTDIR) ] && rm -rf $(DISTDIR) || true
 | 
				
			||||||
@@ -42,3 +46,7 @@ clean:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
test-build: build-deb
 | 
					test-build: build-deb
 | 
				
			||||||
	scp packaging/build/compute*.deb vm:~
 | 
						scp packaging/build/compute*.deb vm:~
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					upload-docs: docs-versions
 | 
				
			||||||
 | 
						ssh root@hitomi 'rm -rf /srv/http/nixhacks.net/hstack/*'
 | 
				
			||||||
 | 
						scp -r $(DOCS_DUILDDIR) root@hitomi:/srv/http/nixhacks.net/hstack/
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										20
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										20
									
								
								README.md
									
									
									
									
									
								
							@@ -9,23 +9,23 @@ Run `make serve-docs`. See [Development](#development) below.
 | 
				
			|||||||
## Roadmap
 | 
					## Roadmap
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- [x] Create instances
 | 
					- [x] Create instances
 | 
				
			||||||
- [ ] CDROM
 | 
					- [x] CDROM
 | 
				
			||||||
- [ ] cloud-init for provisioning instances
 | 
					- [ ] cloud-init for provisioning instances
 | 
				
			||||||
- [x] Instance power management
 | 
					- [x] Power management
 | 
				
			||||||
- [x] Instance pause and resume
 | 
					- [x] Pause and resume
 | 
				
			||||||
- [x] vCPU hotplug
 | 
					- [x] vCPU hotplug
 | 
				
			||||||
- [x] Memory hotplug
 | 
					- [x] Memory hotplug
 | 
				
			||||||
- [x] Hot disk resize [not tested]
 | 
					- [x] Hot disk resize [not tested]
 | 
				
			||||||
- [ ] CPU topology customization
 | 
					 | 
				
			||||||
- [x] CPU customization (emulation mode, model, vendor, features)
 | 
					- [x] CPU customization (emulation mode, model, vendor, features)
 | 
				
			||||||
 | 
					- [ ] CPU topology customization
 | 
				
			||||||
- [ ] BIOS/UEFI settings
 | 
					- [ ] BIOS/UEFI settings
 | 
				
			||||||
- [x] Device attaching
 | 
					- [x] Device attaching
 | 
				
			||||||
- [x] Device detaching
 | 
					- [x] Device detaching
 | 
				
			||||||
- [ ] GPU passthrough
 | 
					- [ ] GPU passthrough
 | 
				
			||||||
- [ ] CPU guarantied resource percent support
 | 
					- [ ] CPU guarantied resource percent support
 | 
				
			||||||
- [x] QEMU Guest Agent management
 | 
					- [x] QEMU Guest Agent management
 | 
				
			||||||
- [ ] Instance resources usage stats
 | 
					- [ ] Resource usage stats
 | 
				
			||||||
- [ ] SSH-keys management
 | 
					- [x] SSH-keys management
 | 
				
			||||||
- [x] Setting user passwords in guest
 | 
					- [x] Setting user passwords in guest
 | 
				
			||||||
- [x] QCOW2 disks support
 | 
					- [x] QCOW2 disks support
 | 
				
			||||||
- [ ] ZVOL support
 | 
					- [ ] ZVOL support
 | 
				
			||||||
@@ -35,10 +35,11 @@ Run `make serve-docs`. See [Development](#development) below.
 | 
				
			|||||||
- [ ] Idempotency
 | 
					- [ ] Idempotency
 | 
				
			||||||
- [ ] CLI [in progress]
 | 
					- [ ] CLI [in progress]
 | 
				
			||||||
- [ ] HTTP API
 | 
					- [ ] HTTP API
 | 
				
			||||||
- [ ] Instance migrations
 | 
					- [ ] Migrations
 | 
				
			||||||
- [ ] Instance snapshots
 | 
					- [ ] Snapshots
 | 
				
			||||||
- [ ] Instance backups
 | 
					- [ ] Backups
 | 
				
			||||||
- [ ] LXC
 | 
					- [ ] LXC
 | 
				
			||||||
 | 
					- [ ] Attaching CDROM from sources: block, (http|https|ftp|ftps|tftp)://
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Development
 | 
					## Development
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -78,6 +79,7 @@ After installation prepare environment, run following command to start libvirtd
 | 
				
			|||||||
    virsh pool-define-as $pool dir - - - - "/$pool"
 | 
					    virsh pool-define-as $pool dir - - - - "/$pool"
 | 
				
			||||||
    virsh pool-build $pool
 | 
					    virsh pool-build $pool
 | 
				
			||||||
    virsh pool-start $pool
 | 
					    virsh pool-start $pool
 | 
				
			||||||
 | 
					    virsh pool-autostart $pool
 | 
				
			||||||
done
 | 
					done
 | 
				
			||||||
```
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,17 +5,17 @@
 | 
				
			|||||||
# the Free Software Foundation, either version 3 of the License, or
 | 
					# the Free Software Foundation, either version 3 of the License, or
 | 
				
			||||||
# (at your option) any later version.
 | 
					# (at your option) any later version.
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
# Ansible is distributed in the hope that it will be useful,
 | 
					# Compute is distributed in the hope that it will be useful,
 | 
				
			||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
					# but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
				
			||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
					# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
				
			||||||
# GNU General Public License for more details.
 | 
					# GNU General Public License for more details.
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
# You should have received a copy of the GNU General Public License
 | 
					# You should have received a copy of the GNU General Public License
 | 
				
			||||||
# along with Ansible.  If not, see <http://www.gnu.org/licenses/>.
 | 
					# along with Compute.  If not, see <http://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"""Compute instances management library."""
 | 
					"""Compute instances management library."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
__version__ = '0.1.0-dev1'
 | 
					__version__ = '0.1.0-dev2'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from .instance import Instance, InstanceConfig, InstanceSchema
 | 
					from .instance import Instance, InstanceConfig, InstanceSchema
 | 
				
			||||||
from .session import Session
 | 
					from .session import Session
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,13 +5,13 @@
 | 
				
			|||||||
# the Free Software Foundation, either version 3 of the License, or
 | 
					# the Free Software Foundation, either version 3 of the License, or
 | 
				
			||||||
# (at your option) any later version.
 | 
					# (at your option) any later version.
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
# Ansible is distributed in the hope that it will be useful,
 | 
					# Compute is distributed in the hope that it will be useful,
 | 
				
			||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
					# but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
				
			||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
					# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
				
			||||||
# GNU General Public License for more details.
 | 
					# GNU General Public License for more details.
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
# You should have received a copy of the GNU General Public License
 | 
					# You should have received a copy of the GNU General Public License
 | 
				
			||||||
# along with Ansible.  If not, see <http://www.gnu.org/licenses/>.
 | 
					# along with Compute.  If not, see <http://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"""Command line interface for compute module."""
 | 
					"""Command line interface for compute module."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,25 +5,25 @@
 | 
				
			|||||||
# the Free Software Foundation, either version 3 of the License, or
 | 
					# the Free Software Foundation, either version 3 of the License, or
 | 
				
			||||||
# (at your option) any later version.
 | 
					# (at your option) any later version.
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
# Ansible is distributed in the hope that it will be useful,
 | 
					# Compute is distributed in the hope that it will be useful,
 | 
				
			||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
					# but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
				
			||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
					# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
				
			||||||
# GNU General Public License for more details.
 | 
					# GNU General Public License for more details.
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
# You should have received a copy of the GNU General Public License
 | 
					# You should have received a copy of the GNU General Public License
 | 
				
			||||||
# along with Ansible.  If not, see <http://www.gnu.org/licenses/>.
 | 
					# along with Compute.  If not, see <http://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"""Command line interface."""
 | 
					"""Command line interface."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import argparse
 | 
					import argparse
 | 
				
			||||||
import io
 | 
					import json
 | 
				
			||||||
import logging
 | 
					import logging
 | 
				
			||||||
import os
 | 
					import os
 | 
				
			||||||
 | 
					import re
 | 
				
			||||||
import shlex
 | 
					import shlex
 | 
				
			||||||
 | 
					import string
 | 
				
			||||||
import sys
 | 
					import sys
 | 
				
			||||||
from collections import UserDict
 | 
					import uuid
 | 
				
			||||||
from typing import Any
 | 
					 | 
				
			||||||
from uuid import uuid4
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
import libvirt
 | 
					import libvirt
 | 
				
			||||||
import yaml
 | 
					import yaml
 | 
				
			||||||
@@ -31,9 +31,10 @@ from pydantic import ValidationError
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
from compute import __version__
 | 
					from compute import __version__
 | 
				
			||||||
from compute.exceptions import ComputeError, GuestAgentTimeoutError
 | 
					from compute.exceptions import ComputeError, GuestAgentTimeoutError
 | 
				
			||||||
from compute.instance import GuestAgent
 | 
					from compute.instance import GuestAgent, Instance, InstanceSchema
 | 
				
			||||||
 | 
					from compute.instance.devices import DiskConfig, DiskDriver
 | 
				
			||||||
from compute.session import Session
 | 
					from compute.session import Session
 | 
				
			||||||
from compute.utils import ids
 | 
					from compute.utils import dictutil, ids
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
log = logging.getLogger(__name__)
 | 
					log = logging.getLogger(__name__)
 | 
				
			||||||
@@ -128,78 +129,77 @@ def _exec_guest_agent_command(
 | 
				
			|||||||
    sys.exit(output.exitcode)
 | 
					    sys.exit(output.exitcode)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class _NotPresent:
 | 
					def _init_instance(session: Session, args: argparse.Namespace) -> None:
 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
    Type for representing non-existent dictionary keys.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    See :class:`_FillableDict`.
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class _FillableDict(UserDict):
 | 
					 | 
				
			||||||
    """Use :method:`fill` to add key if not present."""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def __init__(self, data: dict):
 | 
					 | 
				
			||||||
        self.data = data
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def fill(self, key: str, value: Any) -> None:  # noqa: ANN401
 | 
					 | 
				
			||||||
        if self.data.get(key, _NotPresent) is _NotPresent:
 | 
					 | 
				
			||||||
            self.data[key] = value
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def _merge_dicts(a: dict, b: dict, path: list[str] | None = None) -> dict:
 | 
					 | 
				
			||||||
    """Merge `b` into `a`. Return modified `a`."""
 | 
					 | 
				
			||||||
    if path is None:
 | 
					 | 
				
			||||||
        path = []
 | 
					 | 
				
			||||||
    for key in b:
 | 
					 | 
				
			||||||
        if key in a:
 | 
					 | 
				
			||||||
            if isinstance(a[key], dict) and isinstance(b[key], dict):
 | 
					 | 
				
			||||||
                _merge_dicts(a[key], b[key], [path + str(key)])
 | 
					 | 
				
			||||||
            elif a[key] == b[key]:
 | 
					 | 
				
			||||||
                pass  # same leaf value
 | 
					 | 
				
			||||||
            else:
 | 
					 | 
				
			||||||
                a[key] = b[key]  # replace existing key's values
 | 
					 | 
				
			||||||
        else:
 | 
					 | 
				
			||||||
            a[key] = b[key]
 | 
					 | 
				
			||||||
    return a
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def _create_instance(session: Session, file: io.TextIOWrapper) -> None:
 | 
					 | 
				
			||||||
    try:
 | 
					    try:
 | 
				
			||||||
        data = _FillableDict(yaml.load(file.read(), Loader=yaml.SafeLoader))
 | 
					        data = yaml.load(args.file.read(), Loader=yaml.SafeLoader)
 | 
				
			||||||
        log.debug('Read from file: %s', data)
 | 
					        log.debug('Read from file: %s', data)
 | 
				
			||||||
    except yaml.YAMLError as e:
 | 
					    except yaml.YAMLError as e:
 | 
				
			||||||
        sys.exit(f'error: cannot parse YAML: {e}')
 | 
					        sys.exit(f'error: cannot parse YAML: {e}')
 | 
				
			||||||
 | 
					 | 
				
			||||||
    capabilities = session.get_capabilities()
 | 
					    capabilities = session.get_capabilities()
 | 
				
			||||||
    node_info = session.get_node_info()
 | 
					    node_info = session.get_node_info()
 | 
				
			||||||
 | 
					    base_instance_config = {
 | 
				
			||||||
    data.fill('name', uuid4().hex)
 | 
					        'name': str(uuid.uuid4()),
 | 
				
			||||||
    data.fill('title', None)
 | 
					        'title': None,
 | 
				
			||||||
    data.fill('description', None)
 | 
					        'description': None,
 | 
				
			||||||
    data.fill('arch', capabilities.arch)
 | 
					        'arch': capabilities.arch,
 | 
				
			||||||
    data.fill('machine', capabilities.machine)
 | 
					        'machine': capabilities.machine,
 | 
				
			||||||
    data.fill('emulator', capabilities.emulator)
 | 
					        'emulator': capabilities.emulator,
 | 
				
			||||||
    data.fill('max_vcpus', node_info.cpus)
 | 
					        'max_vcpus': node_info.cpus,
 | 
				
			||||||
    data.fill('max_memory', node_info.memory)
 | 
					        'max_memory': node_info.memory,
 | 
				
			||||||
    data.fill('cpu', {})
 | 
					        'cpu': {
 | 
				
			||||||
    cpu = {
 | 
					            'emulation_mode': 'host-passthrough',
 | 
				
			||||||
        'emulation_mode': 'host-passthrough',
 | 
					            'model': None,
 | 
				
			||||||
        'model': None,
 | 
					            'vendor': None,
 | 
				
			||||||
        'vendor': None,
 | 
					            'topology': None,
 | 
				
			||||||
        'topology': None,
 | 
					            'features': None,
 | 
				
			||||||
        'features': None,
 | 
					        },
 | 
				
			||||||
 | 
					        'network_interfaces': [
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                'source': 'default',
 | 
				
			||||||
 | 
					                'mac': ids.random_mac(),
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        'boot': {'order': ['cdrom', 'hd']},
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    data['cpu'] = _merge_dicts(data['cpu'], cpu)
 | 
					    data = dictutil.override(base_instance_config, data)
 | 
				
			||||||
    data.fill(
 | 
					    volumes = []
 | 
				
			||||||
        'network_interfaces',
 | 
					    for volume in data['volumes']:
 | 
				
			||||||
        [{'source': 'default', 'mac': ids.random_mac()}],
 | 
					        base_disk_config = {
 | 
				
			||||||
    )
 | 
					            'bus': 'virtio',
 | 
				
			||||||
    data.fill('boot', {'order': ['cdrom', 'hd']})
 | 
					            'is_readonly': False,
 | 
				
			||||||
 | 
					            'driver': {
 | 
				
			||||||
 | 
					                'name': 'qemu',
 | 
				
			||||||
 | 
					                'type': 'qcow2',
 | 
				
			||||||
 | 
					                'cache': 'writethrough',
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        base_cdrom_config = {
 | 
				
			||||||
 | 
					            'bus': 'ide',
 | 
				
			||||||
 | 
					            'target': 'hda',
 | 
				
			||||||
 | 
					            'is_readonly': True,
 | 
				
			||||||
 | 
					            'driver': {
 | 
				
			||||||
 | 
					                'name': 'qemu',
 | 
				
			||||||
 | 
					                'type': 'raw',
 | 
				
			||||||
 | 
					                'cache': 'writethrough',
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if volume.get('device') is None:
 | 
				
			||||||
 | 
					            volume['device'] = 'disk'
 | 
				
			||||||
 | 
					        if volume['device'] == 'disk':
 | 
				
			||||||
 | 
					            volumes.append(dictutil.override(base_disk_config, volume))
 | 
				
			||||||
 | 
					        if volume['device'] == 'cdrom':
 | 
				
			||||||
 | 
					            volumes.append(dictutil.override(base_cdrom_config, volume))
 | 
				
			||||||
 | 
					    data['volumes'] = volumes
 | 
				
			||||||
    try:
 | 
					    try:
 | 
				
			||||||
        log.debug('Input data: %s', data)
 | 
					        log.debug('Input data: %s', data)
 | 
				
			||||||
        session.create_instance(**data)
 | 
					        if args.test:
 | 
				
			||||||
 | 
					            _ = InstanceSchema(**data)
 | 
				
			||||||
 | 
					            print(json.dumps(dict(data), indent=4, sort_keys=True))
 | 
				
			||||||
 | 
					            sys.exit()
 | 
				
			||||||
 | 
					        instance = session.create_instance(**data)
 | 
				
			||||||
 | 
					        print(f'initialised: {instance.name}')
 | 
				
			||||||
 | 
					        if args.start:
 | 
				
			||||||
 | 
					            instance.start()
 | 
				
			||||||
    except ValidationError as e:
 | 
					    except ValidationError as e:
 | 
				
			||||||
        for error in e.errors():
 | 
					        for error in e.errors():
 | 
				
			||||||
            fields = '.'.join([str(lc) for lc in error['loc']])
 | 
					            fields = '.'.join([str(lc) for lc in error['loc']])
 | 
				
			||||||
@@ -223,11 +223,84 @@ def _shutdown_instance(session: Session, args: argparse.Namespace) -> None:
 | 
				
			|||||||
    instance.shutdown(method)
 | 
					    instance.shutdown(method)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def _confirm(message: str, *, default: bool | None = None) -> None:
 | 
				
			||||||
 | 
					    while True:
 | 
				
			||||||
 | 
					        match default:
 | 
				
			||||||
 | 
					            case True:
 | 
				
			||||||
 | 
					                prompt = 'default: yes'
 | 
				
			||||||
 | 
					            case False:
 | 
				
			||||||
 | 
					                prompt = 'default: no'
 | 
				
			||||||
 | 
					            case _:
 | 
				
			||||||
 | 
					                prompt = 'no default'
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            answer = input(f'{message} ({prompt}) ')
 | 
				
			||||||
 | 
					        except KeyboardInterrupt:
 | 
				
			||||||
 | 
					            sys.exit('aborted')
 | 
				
			||||||
 | 
					        if not answer and isinstance(default, bool):
 | 
				
			||||||
 | 
					            return default
 | 
				
			||||||
 | 
					        if re.match(r'^y(es)?$', answer, re.I):
 | 
				
			||||||
 | 
					            return True
 | 
				
			||||||
 | 
					        if re.match(r'^no?$', answer, re.I):
 | 
				
			||||||
 | 
					            return False
 | 
				
			||||||
 | 
					        print("Please respond 'yes' or 'no'")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def _delete_instance(session: Session, args: argparse.Namespace) -> None:
 | 
				
			||||||
 | 
					    if args.yes is True or _confirm(
 | 
				
			||||||
 | 
					        'this action is irreversible, continue?',
 | 
				
			||||||
 | 
					        default=False,
 | 
				
			||||||
 | 
					    ):
 | 
				
			||||||
 | 
					        instance = session.get_instance(args.instance)
 | 
				
			||||||
 | 
					        if args.save_volumes is False:
 | 
				
			||||||
 | 
					            instance.delete(with_volumes=True)
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            instance.delete()
 | 
				
			||||||
 | 
					    else:
 | 
				
			||||||
 | 
					        print('aborted')
 | 
				
			||||||
 | 
					        sys.exit()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def _get_disk_target(instance: Instance, prefix: str = 'hd') -> str:
 | 
				
			||||||
 | 
					    disks_live = instance.list_disks(persistent=False)
 | 
				
			||||||
 | 
					    disks_inactive = instance.list_disks(persistent=True)
 | 
				
			||||||
 | 
					    disks = [d for d in disks_inactive if d not in disks_live]
 | 
				
			||||||
 | 
					    devs = [d.target[-1] for d in disks if d.target.startswith(prefix)]
 | 
				
			||||||
 | 
					    return prefix + [x for x in string.ascii_lowercase if x not in devs][0]  # noqa: RUF015
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def _manage_cdrom(session: Session, args: argparse.Namespace) -> None:
 | 
				
			||||||
 | 
					    instance = session.get_instance(args.instance)
 | 
				
			||||||
 | 
					    if args.detach:
 | 
				
			||||||
 | 
					        for disk in instance.list_disks(persistent=True):
 | 
				
			||||||
 | 
					            if disk.device == 'cdrom' and disk.source == args.source:
 | 
				
			||||||
 | 
					                instance.detach_disk(disk.target, live=False)
 | 
				
			||||||
 | 
					                print(
 | 
				
			||||||
 | 
					                    f"disk '{disk.target}' detached, "
 | 
				
			||||||
 | 
					                    'perform power reset to apply changes'
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					        return
 | 
				
			||||||
 | 
					    target = _get_disk_target(instance, 'hd')
 | 
				
			||||||
 | 
					    cdrom = DiskConfig(
 | 
				
			||||||
 | 
					        type='file',
 | 
				
			||||||
 | 
					        device='cdrom',
 | 
				
			||||||
 | 
					        source=args.source,
 | 
				
			||||||
 | 
					        target=target,
 | 
				
			||||||
 | 
					        is_readonly=True,
 | 
				
			||||||
 | 
					        bus='ide',
 | 
				
			||||||
 | 
					        driver=DiskDriver('qemu', 'raw', 'writethrough'),
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    instance.attach_device(cdrom, live=False)
 | 
				
			||||||
 | 
					    print(
 | 
				
			||||||
 | 
					        f"CDROM attached as disk '{target}', "
 | 
				
			||||||
 | 
					        'perform power reset to apply changes'
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def main(session: Session, args: argparse.Namespace) -> None:
 | 
					def main(session: Session, args: argparse.Namespace) -> None:
 | 
				
			||||||
    """Perform actions."""
 | 
					    """Perform actions."""
 | 
				
			||||||
    match args.command:
 | 
					    match args.command:
 | 
				
			||||||
        case 'init':
 | 
					        case 'init':
 | 
				
			||||||
            _create_instance(session, args.file)
 | 
					            _init_instance(session, args)
 | 
				
			||||||
        case 'exec':
 | 
					        case 'exec':
 | 
				
			||||||
            _exec_guest_agent_command(session, args)
 | 
					            _exec_guest_agent_command(session, args)
 | 
				
			||||||
        case 'ls':
 | 
					        case 'ls':
 | 
				
			||||||
@@ -268,13 +341,17 @@ def main(session: Session, args: argparse.Namespace) -> None:
 | 
				
			|||||||
                args.password,
 | 
					                args.password,
 | 
				
			||||||
                encrypted=args.encrypted,
 | 
					                encrypted=args.encrypted,
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
					        case 'setcdrom':
 | 
				
			||||||
 | 
					            _manage_cdrom(session, args)
 | 
				
			||||||
 | 
					        case 'delete':
 | 
				
			||||||
 | 
					            _delete_instance(session, args)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def cli() -> None:  # noqa: PLR0915
 | 
					def get_parser() -> argparse.ArgumentParser:  # noqa: PLR0915
 | 
				
			||||||
    """Return command line arguments parser."""
 | 
					    """Return command line arguments parser."""
 | 
				
			||||||
    root = argparse.ArgumentParser(
 | 
					    root = argparse.ArgumentParser(
 | 
				
			||||||
        prog='compute',
 | 
					        prog='compute',
 | 
				
			||||||
        description='manage compute instances',
 | 
					        description='Manage compute instances.',
 | 
				
			||||||
        formatter_class=argparse.RawTextHelpFormatter,
 | 
					        formatter_class=argparse.RawTextHelpFormatter,
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    root.add_argument(
 | 
					    root.add_argument(
 | 
				
			||||||
@@ -317,12 +394,27 @@ def cli() -> None:  # noqa: PLR0915
 | 
				
			|||||||
        default='instance.yaml',
 | 
					        default='instance.yaml',
 | 
				
			||||||
        help='instance config [default: instance.yaml]',
 | 
					        help='instance config [default: instance.yaml]',
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					    init.add_argument(
 | 
				
			||||||
 | 
					        '-s',
 | 
				
			||||||
 | 
					        '--start',
 | 
				
			||||||
 | 
					        action='store_true',
 | 
				
			||||||
 | 
					        default=False,
 | 
				
			||||||
 | 
					        help='start instance after init',
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    init.add_argument(
 | 
				
			||||||
 | 
					        '-t',
 | 
				
			||||||
 | 
					        '--test',
 | 
				
			||||||
 | 
					        action='store_true',
 | 
				
			||||||
 | 
					        default=False,
 | 
				
			||||||
 | 
					        help='just print resulting instance config as JSON and exit',
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # exec subcommand
 | 
					    # exec subcommand
 | 
				
			||||||
    execute = subparsers.add_parser(
 | 
					    execute = subparsers.add_parser(
 | 
				
			||||||
        'exec',
 | 
					        'exec',
 | 
				
			||||||
        help='execute command in guest via guest agent',
 | 
					        help='execute command in guest via guest agent',
 | 
				
			||||||
        description=(
 | 
					        description=(
 | 
				
			||||||
 | 
					            'Execute command in guest via guest agent. '
 | 
				
			||||||
            'NOTE: any argument after instance name will be passed into '
 | 
					            'NOTE: any argument after instance name will be passed into '
 | 
				
			||||||
            'guest as shell command.'
 | 
					            'guest as shell command.'
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
@@ -463,27 +555,60 @@ def cli() -> None:  # noqa: PLR0915
 | 
				
			|||||||
        help='set it if password is already encrypted',
 | 
					        help='set it if password is already encrypted',
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # setcdrom subcommand
 | 
				
			||||||
 | 
					    setcdrom = subparsers.add_parser('setcdrom', help='manage CDROM devices')
 | 
				
			||||||
 | 
					    setcdrom.add_argument('instance')
 | 
				
			||||||
 | 
					    setcdrom.add_argument('source', help='source for CDROM')
 | 
				
			||||||
 | 
					    setcdrom.add_argument(
 | 
				
			||||||
 | 
					        '-d',
 | 
				
			||||||
 | 
					        '--detach',
 | 
				
			||||||
 | 
					        action='store_true',
 | 
				
			||||||
 | 
					        default=False,
 | 
				
			||||||
 | 
					        help='detach CDROM device',
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # delete subcommand
 | 
				
			||||||
 | 
					    delete = subparsers.add_parser(
 | 
				
			||||||
 | 
					        'delete',
 | 
				
			||||||
 | 
					        help='delete instance',
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    delete.add_argument('instance')
 | 
				
			||||||
 | 
					    delete.add_argument(
 | 
				
			||||||
 | 
					        '-y',
 | 
				
			||||||
 | 
					        '--yes',
 | 
				
			||||||
 | 
					        action='store_true',
 | 
				
			||||||
 | 
					        default=False,
 | 
				
			||||||
 | 
					        help='automatic yes to prompt',
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    delete.add_argument(
 | 
				
			||||||
 | 
					        '--save-volumes',
 | 
				
			||||||
 | 
					        action='store_true',
 | 
				
			||||||
 | 
					        default=False,
 | 
				
			||||||
 | 
					        help='do not delete local storage volumes',
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return root
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def cli() -> None:
 | 
				
			||||||
 | 
					    """Run arguments parser."""
 | 
				
			||||||
 | 
					    root = get_parser()
 | 
				
			||||||
    args = root.parse_args()
 | 
					    args = root.parse_args()
 | 
				
			||||||
    if args.command is None:
 | 
					    if args.command is None:
 | 
				
			||||||
        root.print_help()
 | 
					        root.print_help()
 | 
				
			||||||
        sys.exit()
 | 
					        sys.exit()
 | 
				
			||||||
 | 
					 | 
				
			||||||
    log_level = args.log_level or os.getenv('CMP_LOG')
 | 
					    log_level = args.log_level or os.getenv('CMP_LOG')
 | 
				
			||||||
 | 
					 | 
				
			||||||
    if isinstance(log_level, str) and log_level.lower() in log_levels:
 | 
					    if isinstance(log_level, str) and log_level.lower() in log_levels:
 | 
				
			||||||
        logging.basicConfig(
 | 
					        logging.basicConfig(
 | 
				
			||||||
            level=logging.getLevelNamesMapping()[log_level.upper()]
 | 
					            level=logging.getLevelNamesMapping()[log_level.upper()]
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					 | 
				
			||||||
    log.debug('CLI started with args: %s', args)
 | 
					    log.debug('CLI started with args: %s', args)
 | 
				
			||||||
 | 
					 | 
				
			||||||
    connect_uri = (
 | 
					    connect_uri = (
 | 
				
			||||||
        args.connect
 | 
					        args.connect
 | 
				
			||||||
        or os.getenv('CMP_LIBVIRT_URI')
 | 
					        or os.getenv('CMP_LIBVIRT_URI')
 | 
				
			||||||
        or os.getenv('LIBVIRT_DEFAULT_URI')
 | 
					        or os.getenv('LIBVIRT_DEFAULT_URI')
 | 
				
			||||||
        or 'qemu:///system'
 | 
					        or 'qemu:///system'
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					 | 
				
			||||||
    try:
 | 
					    try:
 | 
				
			||||||
        with Session(connect_uri) as session:
 | 
					        with Session(connect_uri) as session:
 | 
				
			||||||
            main(session, args)
 | 
					            main(session, args)
 | 
				
			||||||
@@ -493,8 +618,6 @@ def cli() -> None:  # noqa: PLR0915
 | 
				
			|||||||
        sys.exit()
 | 
					        sys.exit()
 | 
				
			||||||
    except SystemExit as e:
 | 
					    except SystemExit as e:
 | 
				
			||||||
        sys.exit(e)
 | 
					        sys.exit(e)
 | 
				
			||||||
    except Exception as e:  # noqa: BLE001
 | 
					 | 
				
			||||||
        sys.exit(f'unexpected error {type(e)}: {e}')
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
if __name__ == '__main__':
 | 
					if __name__ == '__main__':
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,13 +5,13 @@
 | 
				
			|||||||
# the Free Software Foundation, either version 3 of the License, or
 | 
					# the Free Software Foundation, either version 3 of the License, or
 | 
				
			||||||
# (at your option) any later version.
 | 
					# (at your option) any later version.
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
# Ansible is distributed in the hope that it will be useful,
 | 
					# Compute is distributed in the hope that it will be useful,
 | 
				
			||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
					# but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
				
			||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
					# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
				
			||||||
# GNU General Public License for more details.
 | 
					# GNU General Public License for more details.
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
# You should have received a copy of the GNU General Public License
 | 
					# You should have received a copy of the GNU General Public License
 | 
				
			||||||
# along with Ansible.  If not, see <http://www.gnu.org/licenses/>.
 | 
					# along with Compute.  If not, see <http://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"""Common symbols."""
 | 
					"""Common symbols."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,13 +5,13 @@
 | 
				
			|||||||
# the Free Software Foundation, either version 3 of the License, or
 | 
					# the Free Software Foundation, either version 3 of the License, or
 | 
				
			||||||
# (at your option) any later version.
 | 
					# (at your option) any later version.
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
# Ansible is distributed in the hope that it will be useful,
 | 
					# Compute is distributed in the hope that it will be useful,
 | 
				
			||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
					# but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
				
			||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
					# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
				
			||||||
# GNU General Public License for more details.
 | 
					# GNU General Public License for more details.
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
# You should have received a copy of the GNU General Public License
 | 
					# You should have received a copy of the GNU General Public License
 | 
				
			||||||
# along with Ansible.  If not, see <http://www.gnu.org/licenses/>.
 | 
					# along with Compute.  If not, see <http://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"""Exceptions."""
 | 
					"""Exceptions."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -80,9 +80,32 @@ class InstanceNotFoundError(InstanceError):
 | 
				
			|||||||
        super().__init__(f"compute instance '{msg}' not found")
 | 
					        super().__init__(f"compute instance '{msg}' not found")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class InvalidDeviceConfigError(ComputeError):
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Invalid device XML description.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :class:`DeviceCoonfig` instance cannot be created because
 | 
				
			||||||
 | 
					    device config in libvirt XML config is not valid.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, msg: str, xml: str):
 | 
				
			||||||
 | 
					        """Initialise InvalidDeviceConfigError."""
 | 
				
			||||||
 | 
					        self.msg = f'Invalid device XML config: {msg}'
 | 
				
			||||||
 | 
					        self.loc = f'    {xml}'
 | 
				
			||||||
 | 
					        super().__init__(f'{self.msg}\n:{self.loc}')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class InvalidDataUnitError(ValueError, ComputeError):
 | 
					class InvalidDataUnitError(ValueError, ComputeError):
 | 
				
			||||||
    """Data unit is not valid."""
 | 
					    """Data unit is not valid."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __init__(self, msg: str, units: list):
 | 
					    def __init__(self, msg: str, units: list):
 | 
				
			||||||
        """Initialise InvalidDataUnitError."""
 | 
					        """Initialise InvalidDataUnitError."""
 | 
				
			||||||
        super().__init__(f'{msg}, valid units are: {", ".join(units)}')
 | 
					        super().__init__(f'{msg}, valid units are: {", ".join(units)}')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class DictMergeConflictError(ComputeError):
 | 
				
			||||||
 | 
					    """Conflict when merging dicts."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, key: str):
 | 
				
			||||||
 | 
					        """Initialise DictMergeConflictError."""
 | 
				
			||||||
 | 
					        super().__init__(f'Conflicting key: {key}')
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,13 +5,13 @@
 | 
				
			|||||||
# the Free Software Foundation, either version 3 of the License, or
 | 
					# the Free Software Foundation, either version 3 of the License, or
 | 
				
			||||||
# (at your option) any later version.
 | 
					# (at your option) any later version.
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
# Ansible is distributed in the hope that it will be useful,
 | 
					# Compute is distributed in the hope that it will be useful,
 | 
				
			||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
					# but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
				
			||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
					# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
				
			||||||
# GNU General Public License for more details.
 | 
					# GNU General Public License for more details.
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
# You should have received a copy of the GNU General Public License
 | 
					# You should have received a copy of the GNU General Public License
 | 
				
			||||||
# along with Ansible.  If not, see <http://www.gnu.org/licenses/>.
 | 
					# along with Compute.  If not, see <http://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from .guest_agent import GuestAgent
 | 
					from .guest_agent import GuestAgent
 | 
				
			||||||
from .instance import Instance, InstanceConfig
 | 
					from .instance import Instance, InstanceConfig
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										128
									
								
								compute/instance/devices.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										128
									
								
								compute/instance/devices.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,128 @@
 | 
				
			|||||||
 | 
					# This file is part of Compute
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# Compute is free software: you can redistribute it and/or modify
 | 
				
			||||||
 | 
					# it under the terms of the GNU General Public License as published by
 | 
				
			||||||
 | 
					# the Free Software Foundation, either version 3 of the License, or
 | 
				
			||||||
 | 
					# (at your option) any later version.
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# Compute is distributed in the hope that it will be useful,
 | 
				
			||||||
 | 
					# but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
				
			||||||
 | 
					# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
				
			||||||
 | 
					# GNU General Public License for more details.
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# You should have received a copy of the GNU General Public License
 | 
				
			||||||
 | 
					# along with Compute.  If not, see <http://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# ruff: noqa: SIM211, UP007, A003
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""Virtual devices configs."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from dataclasses import dataclass, field
 | 
				
			||||||
 | 
					from pathlib import Path
 | 
				
			||||||
 | 
					from typing import Union
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from lxml import etree
 | 
				
			||||||
 | 
					from lxml.builder import E
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from compute.common import DeviceConfig
 | 
				
			||||||
 | 
					from compute.exceptions import InvalidDeviceConfigError
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@dataclass
 | 
				
			||||||
 | 
					class DiskDriver:
 | 
				
			||||||
 | 
					    """Disk driver description for libvirt."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    name: str
 | 
				
			||||||
 | 
					    type: str
 | 
				
			||||||
 | 
					    cache: str
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __call__(self):
 | 
				
			||||||
 | 
					        """Return self."""
 | 
				
			||||||
 | 
					        return self
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@dataclass
 | 
				
			||||||
 | 
					class DiskConfig(DeviceConfig):
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Disk config builder.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Generate XML config for attaching or detaching storage volumes
 | 
				
			||||||
 | 
					    to compute instances.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    type: str
 | 
				
			||||||
 | 
					    source: str | Path
 | 
				
			||||||
 | 
					    target: str
 | 
				
			||||||
 | 
					    is_readonly: bool = False
 | 
				
			||||||
 | 
					    device: str = 'disk'
 | 
				
			||||||
 | 
					    bus: str = 'virtio'
 | 
				
			||||||
 | 
					    driver: DiskDriver = field(
 | 
				
			||||||
 | 
					        default_factory=DiskDriver(
 | 
				
			||||||
 | 
					            name='qemu',
 | 
				
			||||||
 | 
					            type='qcow2',
 | 
				
			||||||
 | 
					            cache='writethrough',
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def to_xml(self) -> str:
 | 
				
			||||||
 | 
					        """Return XML config for libvirt."""
 | 
				
			||||||
 | 
					        xml = E.disk(type=self.type, device=self.device)
 | 
				
			||||||
 | 
					        xml.append(
 | 
				
			||||||
 | 
					            E.driver(
 | 
				
			||||||
 | 
					                name=self.driver.name,
 | 
				
			||||||
 | 
					                type=self.driver.type,
 | 
				
			||||||
 | 
					                cache=self.driver.cache,
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        if self.source and self.type == 'file':
 | 
				
			||||||
 | 
					            xml.append(E.source(file=str(self.source)))
 | 
				
			||||||
 | 
					        xml.append(E.target(dev=self.target, bus=self.bus))
 | 
				
			||||||
 | 
					        if self.is_readonly:
 | 
				
			||||||
 | 
					            xml.append(E.readonly())
 | 
				
			||||||
 | 
					        return etree.tostring(xml, encoding='unicode', pretty_print=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @classmethod
 | 
				
			||||||
 | 
					    def from_xml(cls, xml: Union[str, etree.Element]) -> 'DiskConfig':
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Create :class:`DiskConfig` instance from XML config.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param xml: Disk device XML configuration as :class:`str` or lxml
 | 
				
			||||||
 | 
					            :class:`etree.Element` object.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        if isinstance(xml, str):
 | 
				
			||||||
 | 
					            xml_str = xml
 | 
				
			||||||
 | 
					            xml = etree.fromstring(xml)
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            xml_str = etree.tostring(
 | 
				
			||||||
 | 
					                xml,
 | 
				
			||||||
 | 
					                encoding='unicode',
 | 
				
			||||||
 | 
					                pretty_print=True,
 | 
				
			||||||
 | 
					            ).strip()
 | 
				
			||||||
 | 
					        driver = xml.find('driver')
 | 
				
			||||||
 | 
					        disk_params = {
 | 
				
			||||||
 | 
					            'type': xml.get('type'),
 | 
				
			||||||
 | 
					            'device': xml.get('device'),
 | 
				
			||||||
 | 
					            'driver': DiskDriver(
 | 
				
			||||||
 | 
					                name=driver.get('name'),
 | 
				
			||||||
 | 
					                type=driver.get('type'),
 | 
				
			||||||
 | 
					                cache=driver.get('cache'),
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					            'source': xml.find('source').get('file'),
 | 
				
			||||||
 | 
					            'target': xml.find('target').get('dev'),
 | 
				
			||||||
 | 
					            'bus': xml.find('target').get('bus'),
 | 
				
			||||||
 | 
					            'is_readonly': False if xml.find('readonly') is None else True,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        for param in disk_params:
 | 
				
			||||||
 | 
					            if disk_params[param] is None:
 | 
				
			||||||
 | 
					                msg = f"missing XML tag '{param}'"
 | 
				
			||||||
 | 
					                raise InvalidDeviceConfigError(msg, xml_str)
 | 
				
			||||||
 | 
					            if param == 'driver':
 | 
				
			||||||
 | 
					                driver = disk_params[param]
 | 
				
			||||||
 | 
					                for driver_param in [driver.name, driver.type, driver.cache]:
 | 
				
			||||||
 | 
					                    if driver_param is None:
 | 
				
			||||||
 | 
					                        msg = (
 | 
				
			||||||
 | 
					                            "'driver' tag must have "
 | 
				
			||||||
 | 
					                            "'name', 'type' and 'cache' attributes"
 | 
				
			||||||
 | 
					                        )
 | 
				
			||||||
 | 
					                        raise InvalidDeviceConfigError(msg, xml_str)
 | 
				
			||||||
 | 
					        return cls(**disk_params)
 | 
				
			||||||
@@ -5,13 +5,13 @@
 | 
				
			|||||||
# the Free Software Foundation, either version 3 of the License, or
 | 
					# the Free Software Foundation, either version 3 of the License, or
 | 
				
			||||||
# (at your option) any later version.
 | 
					# (at your option) any later version.
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
# Ansible is distributed in the hope that it will be useful,
 | 
					# Compute is distributed in the hope that it will be useful,
 | 
				
			||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
					# but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
				
			||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
					# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
				
			||||||
# GNU General Public License for more details.
 | 
					# GNU General Public License for more details.
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
# You should have received a copy of the GNU General Public License
 | 
					# You should have received a copy of the GNU General Public License
 | 
				
			||||||
# along with Ansible.  If not, see <http://www.gnu.org/licenses/>.
 | 
					# along with Compute.  If not, see <http://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"""Interacting with the QEMU Guest Agent."""
 | 
					"""Interacting with the QEMU Guest Agent."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,19 +5,20 @@
 | 
				
			|||||||
# the Free Software Foundation, either version 3 of the License, or
 | 
					# the Free Software Foundation, either version 3 of the License, or
 | 
				
			||||||
# (at your option) any later version.
 | 
					# (at your option) any later version.
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
# Ansible is distributed in the hope that it will be useful,
 | 
					# Compute is distributed in the hope that it will be useful,
 | 
				
			||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
					# but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
				
			||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
					# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
				
			||||||
# GNU General Public License for more details.
 | 
					# GNU General Public License for more details.
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
# You should have received a copy of the GNU General Public License
 | 
					# You should have received a copy of the GNU General Public License
 | 
				
			||||||
# along with Ansible.  If not, see <http://www.gnu.org/licenses/>.
 | 
					# along with Compute.  If not, see <http://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"""Manage compute instances."""
 | 
					"""Manage compute instances."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
__all__ = ['Instance', 'InstanceConfig', 'InstanceInfo']
 | 
					__all__ = ['Instance', 'InstanceConfig', 'InstanceInfo']
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import logging
 | 
					import logging
 | 
				
			||||||
 | 
					import time
 | 
				
			||||||
from typing import NamedTuple
 | 
					from typing import NamedTuple
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import libvirt
 | 
					import libvirt
 | 
				
			||||||
@@ -29,9 +30,9 @@ from compute.exceptions import (
 | 
				
			|||||||
    GuestAgentCommandNotSupportedError,
 | 
					    GuestAgentCommandNotSupportedError,
 | 
				
			||||||
    InstanceError,
 | 
					    InstanceError,
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
from compute.storage import DiskConfig
 | 
					 | 
				
			||||||
from compute.utils import units
 | 
					from compute.utils import units
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from .devices import DiskConfig
 | 
				
			||||||
from .guest_agent import GuestAgent
 | 
					from .guest_agent import GuestAgent
 | 
				
			||||||
from .schemas import (
 | 
					from .schemas import (
 | 
				
			||||||
    CPUEmulationMode,
 | 
					    CPUEmulationMode,
 | 
				
			||||||
@@ -282,7 +283,7 @@ class Instance:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    def start(self) -> None:
 | 
					    def start(self) -> None:
 | 
				
			||||||
        """Start defined instance."""
 | 
					        """Start defined instance."""
 | 
				
			||||||
        log.info('Starting instnce=%s', self.name)
 | 
					        log.info("Starting instance '%s'", self.name)
 | 
				
			||||||
        if self.is_running():
 | 
					        if self.is_running():
 | 
				
			||||||
            log.warning(
 | 
					            log.warning(
 | 
				
			||||||
                'Already started, nothing to do instance=%s', self.name
 | 
					                'Already started, nothing to do instance=%s', self.name
 | 
				
			||||||
@@ -292,7 +293,7 @@ class Instance:
 | 
				
			|||||||
            self.domain.create()
 | 
					            self.domain.create()
 | 
				
			||||||
        except libvirt.libvirtError as e:
 | 
					        except libvirt.libvirtError as e:
 | 
				
			||||||
            raise InstanceError(
 | 
					            raise InstanceError(
 | 
				
			||||||
                f'Cannot start instance={self.name}: {e}'
 | 
					                f"Cannot start instance '{self.name}': {e}"
 | 
				
			||||||
            ) from e
 | 
					            ) from e
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def shutdown(self, method: str | None = None) -> None:
 | 
					    def shutdown(self, method: str | None = None) -> None:
 | 
				
			||||||
@@ -323,6 +324,7 @@ class Instance:
 | 
				
			|||||||
        :param method: Method used to shutdown instance
 | 
					        :param method: Method used to shutdown instance
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        if not self.is_running():
 | 
					        if not self.is_running():
 | 
				
			||||||
 | 
					            log.warning('Instance is not running, nothing to do')
 | 
				
			||||||
            return
 | 
					            return
 | 
				
			||||||
        methods = {
 | 
					        methods = {
 | 
				
			||||||
            'SOFT': libvirt.VIR_DOMAIN_SHUTDOWN_GUEST_AGENT,
 | 
					            'SOFT': libvirt.VIR_DOMAIN_SHUTDOWN_GUEST_AGENT,
 | 
				
			||||||
@@ -339,6 +341,7 @@ class Instance:
 | 
				
			|||||||
        method = method.upper()
 | 
					        method = method.upper()
 | 
				
			||||||
        if method not in methods:
 | 
					        if method not in methods:
 | 
				
			||||||
            raise ValueError(f"Unsupported shutdown method: '{method}'")
 | 
					            raise ValueError(f"Unsupported shutdown method: '{method}'")
 | 
				
			||||||
 | 
					        log.info("Performing instance shutdown with method '%s'", method)
 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
            if method in ['SOFT', 'NORMAL']:
 | 
					            if method in ['SOFT', 'NORMAL']:
 | 
				
			||||||
                self.domain.shutdownFlags(flags=methods[method])
 | 
					                self.domain.shutdownFlags(flags=methods[method])
 | 
				
			||||||
@@ -346,7 +349,7 @@ class Instance:
 | 
				
			|||||||
                self.domain.destroyFlags(flags=methods[method])
 | 
					                self.domain.destroyFlags(flags=methods[method])
 | 
				
			||||||
        except libvirt.libvirtError as e:
 | 
					        except libvirt.libvirtError as e:
 | 
				
			||||||
            raise InstanceError(
 | 
					            raise InstanceError(
 | 
				
			||||||
                f'Cannot shutdown instance={self.name} ' f'{method=}: {e}'
 | 
					                f"Cannot shutdown instance '{self.name}' with '{method=}': {e}"
 | 
				
			||||||
            ) from e
 | 
					            ) from e
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def reboot(self) -> None:
 | 
					    def reboot(self) -> None:
 | 
				
			||||||
@@ -375,7 +378,7 @@ class Instance:
 | 
				
			|||||||
            self.domain.reset()
 | 
					            self.domain.reset()
 | 
				
			||||||
        except libvirt.libvirtError as e:
 | 
					        except libvirt.libvirtError as e:
 | 
				
			||||||
            raise InstanceError(
 | 
					            raise InstanceError(
 | 
				
			||||||
                f'Cannot reset instance={self.name}: {e}'
 | 
					                f"Cannot reset instance '{self.name}': {e}"
 | 
				
			||||||
            ) from e
 | 
					            ) from e
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def power_reset(self) -> None:
 | 
					    def power_reset(self) -> None:
 | 
				
			||||||
@@ -389,7 +392,13 @@ class Instance:
 | 
				
			|||||||
        configuration change in libvirt and you need to restart the
 | 
					        configuration change in libvirt and you need to restart the
 | 
				
			||||||
        instance to apply the new configuration.
 | 
					        instance to apply the new configuration.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        self.shutdown(method='NORMAL')
 | 
					        log.debug("Performing power reset for instance '%s'", self.name)
 | 
				
			||||||
 | 
					        self.shutdown('NORMAL')
 | 
				
			||||||
 | 
					        time.sleep(3)
 | 
				
			||||||
 | 
					        # TODO @ge: do safe shutdown insted of this shit
 | 
				
			||||||
 | 
					        if self.is_running():
 | 
				
			||||||
 | 
					            self.shutdown('HARD')
 | 
				
			||||||
 | 
					        time.sleep(1)
 | 
				
			||||||
        self.start()
 | 
					        self.start()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def set_autostart(self, *, enabled: bool) -> None:
 | 
					    def set_autostart(self, *, enabled: bool) -> None:
 | 
				
			||||||
@@ -550,7 +559,9 @@ class Instance:
 | 
				
			|||||||
                return
 | 
					                return
 | 
				
			||||||
        self.domain.detachDeviceFlags(device.to_xml(), flags=flags)
 | 
					        self.domain.detachDeviceFlags(device.to_xml(), flags=flags)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_disk(self, name: str) -> DiskConfig | None:
 | 
					    def get_disk(
 | 
				
			||||||
 | 
					        self, name: str, *, persistent: bool = False
 | 
				
			||||||
 | 
					    ) -> DiskConfig | None:
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Return :class:`DiskConfig` by disk target name.
 | 
					        Return :class:`DiskConfig` by disk target name.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -558,20 +569,27 @@ class Instance:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        :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.
 | 
					            not match the name of the disk inside the guest OS.
 | 
				
			||||||
 | 
					        :param persistent: If True get only persistent volumes described
 | 
				
			||||||
 | 
					            in instance XML config.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        xml = etree.fromstring(self.dump_xml())
 | 
					        xml = etree.fromstring(self.dump_xml(inactive=persistent))
 | 
				
			||||||
        child = xml.xpath(f'/domain/devices/disk/target[@dev="{name}"]')
 | 
					        child = xml.xpath(f'/domain/devices/disk/target[@dev="{name}"]')
 | 
				
			||||||
        if len(child) == 0:
 | 
					        if len(child) == 0:
 | 
				
			||||||
            return None
 | 
					            return None
 | 
				
			||||||
        return DiskConfig.from_xml(child[0].getparent())
 | 
					        return DiskConfig.from_xml(child[0].getparent())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def list_disks(self) -> list[DiskConfig]:
 | 
					    def list_disks(self, *, persistent: bool = False) -> list[DiskConfig]:
 | 
				
			||||||
        """Return list of attached disk devices."""
 | 
					        """
 | 
				
			||||||
        xml = etree.fromstring(self.dump_xml())
 | 
					        Return list of attached disk devices.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param persistent: If True list only persistent volumes described
 | 
				
			||||||
 | 
					            in instance XML config.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        xml = etree.fromstring(self.dump_xml(inactive=persistent))
 | 
				
			||||||
        disks = xml.xpath('/domain/devices/disk')
 | 
					        disks = xml.xpath('/domain/devices/disk')
 | 
				
			||||||
        return [DiskConfig.from_xml(disk) for disk in disks]
 | 
					        return [DiskConfig.from_xml(disk) for disk in disks]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def detach_disk(self, name: str) -> None:
 | 
					    def detach_disk(self, name: str, *, live: bool = False) -> None:
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Detach disk device by target name.
 | 
					        Detach disk device by target name.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -580,15 +598,17 @@ class Instance:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        :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.
 | 
					            not match the name of the disk inside the guest OS.
 | 
				
			||||||
 | 
					        :param live: Affect a running instance. Not supported for CDROM
 | 
				
			||||||
 | 
					            devices.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        disk = self.get_disk(name)
 | 
					        disk = self.get_disk(name, persistent=live)
 | 
				
			||||||
        if disk is None:
 | 
					        if disk is None:
 | 
				
			||||||
            log.warning(
 | 
					            log.warning(
 | 
				
			||||||
                "Volume with target '%s' is already detached",
 | 
					                "Volume with target '%s' is already detached",
 | 
				
			||||||
                name,
 | 
					                name,
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
            return
 | 
					            return
 | 
				
			||||||
        self.detach_device(disk, live=True)
 | 
					        self.detach_device(disk, live=live)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def resize_disk(
 | 
					    def resize_disk(
 | 
				
			||||||
        self, name: str, capacity: int, unit: units.DataUnit
 | 
					        self, name: str, capacity: int, unit: units.DataUnit
 | 
				
			||||||
@@ -601,6 +621,7 @@ class Instance:
 | 
				
			|||||||
        :param capacity: New capacity.
 | 
					        :param capacity: New capacity.
 | 
				
			||||||
        :param unit: Capacity unit.
 | 
					        :param unit: Capacity unit.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
 | 
					        # TODO @ge: check actual size before making changes
 | 
				
			||||||
        self.domain.blockResize(
 | 
					        self.domain.blockResize(
 | 
				
			||||||
            name,
 | 
					            name,
 | 
				
			||||||
            units.to_bytes(capacity, unit=unit),
 | 
					            units.to_bytes(capacity, unit=unit),
 | 
				
			||||||
@@ -619,7 +640,7 @@ class Instance:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    def list_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.
 | 
					        Return list of authorized SSH keys in guest for specific user.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :param user: Username.
 | 
					        :param user: Username.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
@@ -655,7 +676,7 @@ class Instance:
 | 
				
			|||||||
        append: bool = False,
 | 
					        append: bool = False,
 | 
				
			||||||
    ) -> None:
 | 
					    ) -> None:
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Add SSH keys to guest for specific user.
 | 
					        Add authorized SSH keys to guest for specific user.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :param user: Username.
 | 
					        :param user: Username.
 | 
				
			||||||
        :param keys: List of authorized SSH keys.
 | 
					        :param keys: List of authorized SSH keys.
 | 
				
			||||||
@@ -666,7 +687,7 @@ class Instance:
 | 
				
			|||||||
        qemu_ga_commands = ['guest-ssh-add-authorized-keys']
 | 
					        qemu_ga_commands = ['guest-ssh-add-authorized-keys']
 | 
				
			||||||
        if remove and append:
 | 
					        if remove and append:
 | 
				
			||||||
            raise InstanceError(
 | 
					            raise InstanceError(
 | 
				
			||||||
                "'append' and 'remove' parameters is mutually exclusive"
 | 
					                "'append' and 'remove' parameters are mutually exclusive"
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
        if not self.is_running():
 | 
					        if not self.is_running():
 | 
				
			||||||
            raise InstanceError(
 | 
					            raise InstanceError(
 | 
				
			||||||
@@ -693,7 +714,7 @@ class Instance:
 | 
				
			|||||||
        """
 | 
					        """
 | 
				
			||||||
        Set new user password in guest OS.
 | 
					        Set new user password in guest OS.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        This action performs by guest agent inside the guest.
 | 
					        This action is performed by guest agent inside the guest.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :param user: Username.
 | 
					        :param user: Username.
 | 
				
			||||||
        :param password: Password.
 | 
					        :param password: Password.
 | 
				
			||||||
@@ -702,6 +723,7 @@ class Instance:
 | 
				
			|||||||
        """
 | 
					        """
 | 
				
			||||||
        self.guest_agent.raise_for_commands(['guest-set-user-password'])
 | 
					        self.guest_agent.raise_for_commands(['guest-set-user-password'])
 | 
				
			||||||
        flags = libvirt.VIR_DOMAIN_PASSWORD_ENCRYPTED if encrypted else 0
 | 
					        flags = libvirt.VIR_DOMAIN_PASSWORD_ENCRYPTED if encrypted else 0
 | 
				
			||||||
 | 
					        log.debug("Setting up password for user '%s'", user)
 | 
				
			||||||
        self.domain.setUserPassword(user, password, flags=flags)
 | 
					        self.domain.setUserPassword(user, password, flags=flags)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def dump_xml(self, *, inactive: bool = False) -> str:
 | 
					    def dump_xml(self, *, inactive: bool = False) -> str:
 | 
				
			||||||
@@ -709,11 +731,19 @@ class Instance:
 | 
				
			|||||||
        flags = libvirt.VIR_DOMAIN_XML_INACTIVE if inactive else 0
 | 
					        flags = libvirt.VIR_DOMAIN_XML_INACTIVE if inactive else 0
 | 
				
			||||||
        return self.domain.XMLDesc(flags)
 | 
					        return self.domain.XMLDesc(flags)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def delete(self) -> None:
 | 
					    def delete(self, *, with_volumes: bool = False) -> None:
 | 
				
			||||||
        """Delete instance with local disks."""
 | 
					        """
 | 
				
			||||||
 | 
					        Delete instance with local volumes.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param with_volumes: If True delete local volumes with instance.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
        self.shutdown(method='HARD')
 | 
					        self.shutdown(method='HARD')
 | 
				
			||||||
        for disk in self.list_disks():
 | 
					        disks = self.list_disks(persistent=True)
 | 
				
			||||||
            if disk.disk_type == 'file':
 | 
					        log.debug('Disks list: %s', disks)
 | 
				
			||||||
 | 
					        for disk in disks:
 | 
				
			||||||
 | 
					            if with_volumes and disk.type == 'file':
 | 
				
			||||||
                volume = self.connection.storageVolLookupByPath(disk.source)
 | 
					                volume = self.connection.storageVolLookupByPath(disk.source)
 | 
				
			||||||
 | 
					                log.debug('Delete volume: %s', volume.path())
 | 
				
			||||||
                volume.delete()
 | 
					                volume.delete()
 | 
				
			||||||
 | 
					        log.debug('Undefine instance')
 | 
				
			||||||
        self.domain.undefine()
 | 
					        self.domain.undefine()
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,13 +5,13 @@
 | 
				
			|||||||
# the Free Software Foundation, either version 3 of the License, or
 | 
					# the Free Software Foundation, either version 3 of the License, or
 | 
				
			||||||
# (at your option) any later version.
 | 
					# (at your option) any later version.
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
# Ansible is distributed in the hope that it will be useful,
 | 
					# Compute is distributed in the hope that it will be useful,
 | 
				
			||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
					# but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
				
			||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
					# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
				
			||||||
# GNU General Public License for more details.
 | 
					# GNU General Public License for more details.
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
# You should have received a copy of the GNU General Public License
 | 
					# You should have received a copy of the GNU General Public License
 | 
				
			||||||
# along with Ansible.  If not, see <http://www.gnu.org/licenses/>.
 | 
					# along with Compute.  If not, see <http://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"""Compute instance related objects schemas."""
 | 
					"""Compute instance related objects schemas."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -19,7 +19,8 @@ import re
 | 
				
			|||||||
from enum import StrEnum
 | 
					from enum import StrEnum
 | 
				
			||||||
from pathlib import Path
 | 
					from pathlib import Path
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from pydantic import validator
 | 
					from pydantic import ValidationError, validator
 | 
				
			||||||
 | 
					from pydantic.error_wrappers import ErrorWrapper
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from compute.common import EntityModel
 | 
					from compute.common import EntityModel
 | 
				
			||||||
from compute.utils.units import DataUnit
 | 
					from compute.utils.units import DataUnit
 | 
				
			||||||
@@ -73,15 +74,26 @@ class VolumeCapacitySchema(EntityModel):
 | 
				
			|||||||
    unit: DataUnit
 | 
					    unit: DataUnit
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class DiskDriverSchema(EntityModel):
 | 
				
			||||||
 | 
					    """Virtual disk driver model."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    name: str
 | 
				
			||||||
 | 
					    type: str  # noqa: A003
 | 
				
			||||||
 | 
					    cache: str = 'writethrough'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class VolumeSchema(EntityModel):
 | 
					class VolumeSchema(EntityModel):
 | 
				
			||||||
    """Storage volume model."""
 | 
					    """Storage volume model."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    type: VolumeType  # noqa: A003
 | 
					    type: VolumeType  # noqa: A003
 | 
				
			||||||
    target: str
 | 
					    target: str
 | 
				
			||||||
    capacity: VolumeCapacitySchema
 | 
					    driver: DiskDriverSchema
 | 
				
			||||||
 | 
					    capacity: VolumeCapacitySchema | None
 | 
				
			||||||
    source: str | None = None
 | 
					    source: str | None = None
 | 
				
			||||||
    is_readonly: bool = False
 | 
					    is_readonly: bool = False
 | 
				
			||||||
    is_system: bool = False
 | 
					    is_system: bool = False
 | 
				
			||||||
 | 
					    bus: str = 'virtio'
 | 
				
			||||||
 | 
					    device: str = 'disk'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class NetworkInterfaceSchema(EntityModel):
 | 
					class NetworkInterfaceSchema(EntityModel):
 | 
				
			||||||
@@ -118,10 +130,10 @@ class InstanceSchema(EntityModel):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    @validator('name')
 | 
					    @validator('name')
 | 
				
			||||||
    def _check_name(cls, value: str) -> str:  # noqa: N805
 | 
					    def _check_name(cls, value: str) -> str:  # noqa: N805
 | 
				
			||||||
        if not re.match(r'^[a-z0-9_]+$', value):
 | 
					        if not re.match(r'^[a-z0-9_-]+$', value):
 | 
				
			||||||
            msg = (
 | 
					            msg = (
 | 
				
			||||||
                'Name can contain only lowercase letters, numbers '
 | 
					                'Name can contain only lowercase letters, numbers, '
 | 
				
			||||||
                'and underscore.'
 | 
					                'minus sign and underscore.'
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
            raise ValueError(msg)
 | 
					            raise ValueError(msg)
 | 
				
			||||||
        return value
 | 
					        return value
 | 
				
			||||||
@@ -140,13 +152,22 @@ class InstanceSchema(EntityModel):
 | 
				
			|||||||
        if len([v for v in volumes if v.is_system is True]) != 1:
 | 
					        if len([v for v in volumes if v.is_system is True]) != 1:
 | 
				
			||||||
            msg = 'volumes list must contain one system volume'
 | 
					            msg = 'volumes list must contain one system volume'
 | 
				
			||||||
            raise ValueError(msg)
 | 
					            raise ValueError(msg)
 | 
				
			||||||
        vol_with_source = 0
 | 
					 | 
				
			||||||
        for vol in volumes:
 | 
					        for vol in volumes:
 | 
				
			||||||
 | 
					            if vol.source is None and vol.capacity is None:
 | 
				
			||||||
 | 
					                raise ValidationError(
 | 
				
			||||||
 | 
					                    [
 | 
				
			||||||
 | 
					                        ErrorWrapper(
 | 
				
			||||||
 | 
					                            Exception(
 | 
				
			||||||
 | 
					                                "capacity is required if 'source' is unset"
 | 
				
			||||||
 | 
					                            ),
 | 
				
			||||||
 | 
					                            loc='X.capacity',
 | 
				
			||||||
 | 
					                        )
 | 
				
			||||||
 | 
					                    ],
 | 
				
			||||||
 | 
					                    model=VolumeSchema,
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
            if vol.is_system is True and vol.is_readonly is True:
 | 
					            if vol.is_system is True and vol.is_readonly is True:
 | 
				
			||||||
                msg = 'volume marked as system cannot be readonly'
 | 
					                msg = 'volume marked as system cannot be readonly'
 | 
				
			||||||
                raise ValueError(msg)
 | 
					                raise ValueError(msg)
 | 
				
			||||||
            if vol.source is not None:
 | 
					 | 
				
			||||||
                vol_with_source += 1
 | 
					 | 
				
			||||||
        return volumes
 | 
					        return volumes
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @validator('network_interfaces')
 | 
					    @validator('network_interfaces')
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,13 +5,13 @@
 | 
				
			|||||||
# the Free Software Foundation, either version 3 of the License, or
 | 
					# the Free Software Foundation, either version 3 of the License, or
 | 
				
			||||||
# (at your option) any later version.
 | 
					# (at your option) any later version.
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
# Ansible is distributed in the hope that it will be useful,
 | 
					# Compute is distributed in the hope that it will be useful,
 | 
				
			||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
					# but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
				
			||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
					# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
				
			||||||
# GNU General Public License for more details.
 | 
					# GNU General Public License for more details.
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
# You should have received a copy of the GNU General Public License
 | 
					# You should have received a copy of the GNU General Public License
 | 
				
			||||||
# along with Ansible.  If not, see <http://www.gnu.org/licenses/>.
 | 
					# along with Compute.  If not, see <http://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"""Hypervisor session manager."""
 | 
					"""Hypervisor session manager."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -31,7 +31,8 @@ from .exceptions import (
 | 
				
			|||||||
    StoragePoolNotFoundError,
 | 
					    StoragePoolNotFoundError,
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
from .instance import Instance, InstanceConfig, InstanceSchema
 | 
					from .instance import Instance, InstanceConfig, InstanceSchema
 | 
				
			||||||
from .storage import DiskConfig, StoragePool, VolumeConfig
 | 
					from .instance.devices import DiskConfig, DiskDriver
 | 
				
			||||||
 | 
					from .storage import StoragePool, VolumeConfig
 | 
				
			||||||
from .utils import units
 | 
					from .utils import units
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -164,7 +165,7 @@ class Session(AbstractContextManager):
 | 
				
			|||||||
            cpu_vendor=caps.xpath(f'{hprefix}/vendor/text()')[0],
 | 
					            cpu_vendor=caps.xpath(f'{hprefix}/vendor/text()')[0],
 | 
				
			||||||
            cpu_model=caps.xpath(f'{hprefix}/model/text()')[0],
 | 
					            cpu_model=caps.xpath(f'{hprefix}/model/text()')[0],
 | 
				
			||||||
            cpu_features=self._cap_get_cpu_features(caps),
 | 
					            cpu_features=self._cap_get_cpu_features(caps),
 | 
				
			||||||
            usable_cpus=self._cap_get_cpus(caps),
 | 
					            usable_cpus=self._cap_get_usable_cpus(caps),
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def create_instance(self, **kwargs: Any) -> Instance:
 | 
					    def create_instance(self, **kwargs: Any) -> Instance:
 | 
				
			||||||
@@ -208,15 +209,15 @@ class Session(AbstractContextManager):
 | 
				
			|||||||
        config = InstanceConfig(data)
 | 
					        config = InstanceConfig(data)
 | 
				
			||||||
        log.info('Define XML...')
 | 
					        log.info('Define XML...')
 | 
				
			||||||
        log.info(config.to_xml())
 | 
					        log.info(config.to_xml())
 | 
				
			||||||
        self.connection.defineXML(config.to_xml())
 | 
					        try:
 | 
				
			||||||
 | 
					            self.connection.defineXML(config.to_xml())
 | 
				
			||||||
 | 
					        except libvirt.libvirtError as e:
 | 
				
			||||||
 | 
					            raise SessionError(f'Error defining instance: {e}') from e
 | 
				
			||||||
        log.info('Getting instance...')
 | 
					        log.info('Getting instance...')
 | 
				
			||||||
        instance = self.get_instance(config.name)
 | 
					        instance = self.get_instance(config.name)
 | 
				
			||||||
        log.info('Creating volumes...')
 | 
					        log.info('Start processing volumes...')
 | 
				
			||||||
        for volume in data.volumes:
 | 
					        for volume in data.volumes:
 | 
				
			||||||
            log.info('Creating volume=%s', volume)
 | 
					            log.info('Processing volume=%s', volume)
 | 
				
			||||||
            capacity = units.to_bytes(
 | 
					 | 
				
			||||||
                volume.capacity.value, volume.capacity.unit
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            log.info('Connecting to images pool...')
 | 
					            log.info('Connecting to images pool...')
 | 
				
			||||||
            images_pool = self.get_storage_pool(self.IMAGES_POOL)
 | 
					            images_pool = self.get_storage_pool(self.IMAGES_POOL)
 | 
				
			||||||
            log.info('Connecting to volumes pool...')
 | 
					            log.info('Connecting to volumes pool...')
 | 
				
			||||||
@@ -226,35 +227,48 @@ class Session(AbstractContextManager):
 | 
				
			|||||||
                vol_name = f'{uuid4()}.qcow2'
 | 
					                vol_name = f'{uuid4()}.qcow2'
 | 
				
			||||||
            else:
 | 
					            else:
 | 
				
			||||||
                vol_name = volume.source
 | 
					                vol_name = volume.source
 | 
				
			||||||
            vol_conf = VolumeConfig(
 | 
					            if volume.device == 'cdrom':
 | 
				
			||||||
                name=vol_name,
 | 
					                log.debug('Volume %s is CDROM device', vol_name)
 | 
				
			||||||
                path=str(volumes_pool.path.joinpath(vol_name)),
 | 
					 | 
				
			||||||
                capacity=capacity,
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            log.info('Volume configuration is:\n %s', vol_conf.to_xml())
 | 
					 | 
				
			||||||
            if volume.is_system is True and data.image:
 | 
					 | 
				
			||||||
                log.info(
 | 
					 | 
				
			||||||
                    "Volume is marked as 'system', start cloning image..."
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
                log.info('Get image %s', data.image)
 | 
					 | 
				
			||||||
                image = images_pool.get_volume(data.image)
 | 
					 | 
				
			||||||
                log.info('Cloning image into volumes pool...')
 | 
					 | 
				
			||||||
                vol = volumes_pool.clone_volume(image, vol_conf)
 | 
					 | 
				
			||||||
                log.info(
 | 
					 | 
				
			||||||
                    'Resize cloned volume to specified size: %s',
 | 
					 | 
				
			||||||
                    capacity,
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
                vol.resize(capacity, unit=units.DataUnit.BYTES)
 | 
					 | 
				
			||||||
            else:
 | 
					            else:
 | 
				
			||||||
                log.info('Create volume...')
 | 
					                capacity = units.to_bytes(
 | 
				
			||||||
                volumes_pool.create_volume(vol_conf)
 | 
					                    volume.capacity.value, volume.capacity.unit
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					                vol_conf = VolumeConfig(
 | 
				
			||||||
 | 
					                    name=vol_name,
 | 
				
			||||||
 | 
					                    path=str(volumes_pool.path.joinpath(vol_name)),
 | 
				
			||||||
 | 
					                    capacity=capacity,
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					                log.info('Volume configuration is:\n %s', vol_conf.to_xml())
 | 
				
			||||||
 | 
					                if volume.is_system is True and data.image:
 | 
				
			||||||
 | 
					                    log.info(
 | 
				
			||||||
 | 
					                        "Volume is marked as 'system', start cloning image..."
 | 
				
			||||||
 | 
					                    )
 | 
				
			||||||
 | 
					                    log.info('Get image %s', data.image)
 | 
				
			||||||
 | 
					                    image = images_pool.get_volume(data.image)
 | 
				
			||||||
 | 
					                    log.info('Cloning image into volumes pool...')
 | 
				
			||||||
 | 
					                    vol = volumes_pool.clone_volume(image, vol_conf)
 | 
				
			||||||
 | 
					                    log.info(
 | 
				
			||||||
 | 
					                        'Resize cloned volume to specified size: %s',
 | 
				
			||||||
 | 
					                        capacity,
 | 
				
			||||||
 | 
					                    )
 | 
				
			||||||
 | 
					                    vol.resize(capacity, unit=units.DataUnit.BYTES)
 | 
				
			||||||
 | 
					                else:
 | 
				
			||||||
 | 
					                    log.info('Create volume...')
 | 
				
			||||||
 | 
					                    volumes_pool.create_volume(vol_conf)
 | 
				
			||||||
            log.info('Attaching volume to instance...')
 | 
					            log.info('Attaching volume to instance...')
 | 
				
			||||||
            instance.attach_device(
 | 
					            instance.attach_device(
 | 
				
			||||||
                DiskConfig(
 | 
					                DiskConfig(
 | 
				
			||||||
                    disk_type=volume.type,
 | 
					                    type=volume.type,
 | 
				
			||||||
 | 
					                    device=volume.device,
 | 
				
			||||||
                    source=vol_conf.path,
 | 
					                    source=vol_conf.path,
 | 
				
			||||||
                    target=volume.target,
 | 
					                    target=volume.target,
 | 
				
			||||||
                    readonly=volume.is_readonly,
 | 
					                    is_readonly=volume.is_readonly,
 | 
				
			||||||
 | 
					                    bus=volume.bus,
 | 
				
			||||||
 | 
					                    driver=DiskDriver(
 | 
				
			||||||
 | 
					                        volume.driver.name,
 | 
				
			||||||
 | 
					                        volume.driver.type,
 | 
				
			||||||
 | 
					                        volume.driver.cache,
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
        return instance
 | 
					        return instance
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,13 +5,13 @@
 | 
				
			|||||||
# the Free Software Foundation, either version 3 of the License, or
 | 
					# the Free Software Foundation, either version 3 of the License, or
 | 
				
			||||||
# (at your option) any later version.
 | 
					# (at your option) any later version.
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
# Ansible is distributed in the hope that it will be useful,
 | 
					# Compute is distributed in the hope that it will be useful,
 | 
				
			||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
					# but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
				
			||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
					# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
				
			||||||
# GNU General Public License for more details.
 | 
					# GNU General Public License for more details.
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
# You should have received a copy of the GNU General Public License
 | 
					# You should have received a copy of the GNU General Public License
 | 
				
			||||||
# along with Ansible.  If not, see <http://www.gnu.org/licenses/>.
 | 
					# along with Compute.  If not, see <http://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from .pool import StoragePool
 | 
					from .pool import StoragePool
 | 
				
			||||||
from .volume import DiskConfig, Volume, VolumeConfig
 | 
					from .volume import Volume, VolumeConfig
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,13 +5,13 @@
 | 
				
			|||||||
# the Free Software Foundation, either version 3 of the License, or
 | 
					# the Free Software Foundation, either version 3 of the License, or
 | 
				
			||||||
# (at your option) any later version.
 | 
					# (at your option) any later version.
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
# Ansible is distributed in the hope that it will be useful,
 | 
					# Compute is distributed in the hope that it will be useful,
 | 
				
			||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
					# but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
				
			||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
					# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
				
			||||||
# GNU General Public License for more details.
 | 
					# GNU General Public License for more details.
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
# You should have received a copy of the GNU General Public License
 | 
					# You should have received a copy of the GNU General Public License
 | 
				
			||||||
# along with Ansible.  If not, see <http://www.gnu.org/licenses/>.
 | 
					# along with Compute.  If not, see <http://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"""Manage storage pools."""
 | 
					"""Manage storage pools."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,26 +5,25 @@
 | 
				
			|||||||
# the Free Software Foundation, either version 3 of the License, or
 | 
					# the Free Software Foundation, either version 3 of the License, or
 | 
				
			||||||
# (at your option) any later version.
 | 
					# (at your option) any later version.
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
# Ansible is distributed in the hope that it will be useful,
 | 
					# Compute is distributed in the hope that it will be useful,
 | 
				
			||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
					# but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
				
			||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
					# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
				
			||||||
# GNU General Public License for more details.
 | 
					# GNU General Public License for more details.
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
# You should have received a copy of the GNU General Public License
 | 
					# You should have received a copy of the GNU General Public License
 | 
				
			||||||
# along with Ansible.  If not, see <http://www.gnu.org/licenses/>.
 | 
					# along with Compute.  If not, see <http://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"""Manage storage volumes."""
 | 
					"""Manage storage volumes."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from dataclasses import dataclass
 | 
					from dataclasses import dataclass
 | 
				
			||||||
from pathlib import Path
 | 
					from pathlib import Path
 | 
				
			||||||
from time import time
 | 
					from time import time
 | 
				
			||||||
from typing import Union
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
import libvirt
 | 
					import libvirt
 | 
				
			||||||
from lxml import etree
 | 
					from lxml import etree
 | 
				
			||||||
from lxml.builder import E
 | 
					from lxml.builder import E
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from compute.common import DeviceConfig, EntityConfig
 | 
					from compute.common import EntityConfig
 | 
				
			||||||
from compute.utils import units
 | 
					from compute.utils import units
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -64,54 +63,6 @@ class VolumeConfig(EntityConfig):
 | 
				
			|||||||
        return etree.tostring(xml, encoding='unicode', pretty_print=True)
 | 
					        return etree.tostring(xml, encoding='unicode', pretty_print=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@dataclass
 | 
					 | 
				
			||||||
class DiskConfig(DeviceConfig):
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
    Disk XML config builder.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    Generate XML config for attaching or detaching storage volumes
 | 
					 | 
				
			||||||
    to compute instances.
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    disk_type: str
 | 
					 | 
				
			||||||
    source: str | Path
 | 
					 | 
				
			||||||
    target: str
 | 
					 | 
				
			||||||
    readonly: bool = False
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def to_xml(self) -> str:
 | 
					 | 
				
			||||||
        """Return XML config for libvirt."""
 | 
					 | 
				
			||||||
        xml = E.disk(type=self.disk_type, device='disk')
 | 
					 | 
				
			||||||
        xml.append(E.driver(name='qemu', type='qcow2', cache='writethrough'))
 | 
					 | 
				
			||||||
        if self.disk_type == 'file':
 | 
					 | 
				
			||||||
            xml.append(E.source(file=str(self.source)))
 | 
					 | 
				
			||||||
        xml.append(E.target(dev=self.target, bus='virtio'))
 | 
					 | 
				
			||||||
        if self.readonly:
 | 
					 | 
				
			||||||
            xml.append(E.readonly())
 | 
					 | 
				
			||||||
        return etree.tostring(xml, encoding='unicode', pretty_print=True)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    @classmethod
 | 
					 | 
				
			||||||
    def from_xml(cls, xml: Union[str, etree.Element]) -> 'DiskConfig':  # noqa: UP007
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        Return :class:`DiskConfig` instance using existing XML config.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        :param xml: Disk device XML configuration as :class:`str` or lxml
 | 
					 | 
				
			||||||
            :class:`etree.Element` object.
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        if isinstance(xml, str):
 | 
					 | 
				
			||||||
            xml = etree.fromstring(xml)
 | 
					 | 
				
			||||||
        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"Bad XML config: parameter '{param}' is not defined"
 | 
					 | 
				
			||||||
                raise ValueError(msg)
 | 
					 | 
				
			||||||
        return cls(**disk_params)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class Volume:
 | 
					class Volume:
 | 
				
			||||||
    """Storage volume manipulating class."""
 | 
					    """Storage volume manipulating class."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,13 +5,13 @@
 | 
				
			|||||||
# the Free Software Foundation, either version 3 of the License, or
 | 
					# the Free Software Foundation, either version 3 of the License, or
 | 
				
			||||||
# (at your option) any later version.
 | 
					# (at your option) any later version.
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
# Ansible is distributed in the hope that it will be useful,
 | 
					# Compute is distributed in the hope that it will be useful,
 | 
				
			||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
					# but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
				
			||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
					# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
				
			||||||
# GNU General Public License for more details.
 | 
					# GNU General Public License for more details.
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
# You should have received a copy of the GNU General Public License
 | 
					# You should have received a copy of the GNU General Public License
 | 
				
			||||||
# along with Ansible.  If not, see <http://www.gnu.org/licenses/>.
 | 
					# along with Compute.  If not, see <http://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"""Configuration loader."""
 | 
					"""Configuration loader."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										77
									
								
								compute/utils/dictutil.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								compute/utils/dictutil.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,77 @@
 | 
				
			|||||||
 | 
					# This file is part of Compute
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# Compute is free software: you can redistribute it and/or modify
 | 
				
			||||||
 | 
					# it under the terms of the GNU General Public License as published by
 | 
				
			||||||
 | 
					# the Free Software Foundation, either version 3 of the License, or
 | 
				
			||||||
 | 
					# (at your option) any later version.
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# Compute is distributed in the hope that it will be useful,
 | 
				
			||||||
 | 
					# but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
				
			||||||
 | 
					# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
				
			||||||
 | 
					# GNU General Public License for more details.
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# You should have received a copy of the GNU General Public License
 | 
				
			||||||
 | 
					# along with Compute.  If not, see <http://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""Dict tools."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from compute.exceptions import DictMergeConflictError
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def merge(a: dict, b: dict, path: list[str] | None = None) -> dict:
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Merge `b` into `a`. Return modified `a`.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :raise: :class:`DictMergeConflictError`
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    if path is None:
 | 
				
			||||||
 | 
					        path = []
 | 
				
			||||||
 | 
					    for key in b:
 | 
				
			||||||
 | 
					        if key in a:
 | 
				
			||||||
 | 
					            if isinstance(a[key], dict) and isinstance(b[key], dict):
 | 
				
			||||||
 | 
					                merge(a[key], b[key], [*path, str(key)])
 | 
				
			||||||
 | 
					            elif a[key] != b[key]:
 | 
				
			||||||
 | 
					                raise DictMergeConflictError('.'.join([*path, str(key)]))
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            a[key] = b[key]
 | 
				
			||||||
 | 
					    return a
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def override(a: dict, b: dict) -> dict:
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Override dict `a` by `b` values.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Keys that not exists in `a`, but exists in `b` will be
 | 
				
			||||||
 | 
					    appended to `a`.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .. code-block:: shell-session
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					       >>> from compute.utils import dictutil
 | 
				
			||||||
 | 
					       >>> default = {
 | 
				
			||||||
 | 
					       ...     'bus': 'virtio',
 | 
				
			||||||
 | 
					       ...     'driver': {'name': 'qemu', 'type': 'qcow2'}
 | 
				
			||||||
 | 
					       ... }
 | 
				
			||||||
 | 
					       >>> user = {
 | 
				
			||||||
 | 
					       ...     'bus': 'ide',
 | 
				
			||||||
 | 
					       ...     'target': 'vda',
 | 
				
			||||||
 | 
					       ...     'driver': {'type': 'raw'}
 | 
				
			||||||
 | 
					       ... }
 | 
				
			||||||
 | 
					       >>> dictutil.override(default, user)
 | 
				
			||||||
 | 
					       {'bus': 'ide', 'driver': {'name': 'qemu', 'type': 'raw'},
 | 
				
			||||||
 | 
					       'target': 'vda'}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    NOTE: merging dicts contained in lists is not supported.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param a: Dict to be overwritten.
 | 
				
			||||||
 | 
					    :param b: A dict whose values will be used to rewrite dict `a`.
 | 
				
			||||||
 | 
					    :return: Modified `a` dict.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    for key in b:
 | 
				
			||||||
 | 
					        if key in a:
 | 
				
			||||||
 | 
					            if isinstance(a[key], dict) and isinstance(b[key], dict):
 | 
				
			||||||
 | 
					                override(a[key], b[key])
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                a[key] = b[key]  # replace existing key's values
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            a[key] = b[key]
 | 
				
			||||||
 | 
					    return a
 | 
				
			||||||
@@ -5,13 +5,13 @@
 | 
				
			|||||||
# the Free Software Foundation, either version 3 of the License, or
 | 
					# the Free Software Foundation, either version 3 of the License, or
 | 
				
			||||||
# (at your option) any later version.
 | 
					# (at your option) any later version.
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
# Ansible is distributed in the hope that it will be useful,
 | 
					# Compute is distributed in the hope that it will be useful,
 | 
				
			||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
					# but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
				
			||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
					# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
				
			||||||
# GNU General Public License for more details.
 | 
					# GNU General Public License for more details.
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
# You should have received a copy of the GNU General Public License
 | 
					# You should have received a copy of the GNU General Public License
 | 
				
			||||||
# along with Ansible.  If not, see <http://www.gnu.org/licenses/>.
 | 
					# along with Compute.  If not, see <http://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"""Random identificators."""
 | 
					"""Random identificators."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,13 +5,13 @@
 | 
				
			|||||||
# the Free Software Foundation, either version 3 of the License, or
 | 
					# the Free Software Foundation, either version 3 of the License, or
 | 
				
			||||||
# (at your option) any later version.
 | 
					# (at your option) any later version.
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
# Ansible is distributed in the hope that it will be useful,
 | 
					# Compute is distributed in the hope that it will be useful,
 | 
				
			||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
					# but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
				
			||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
					# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
				
			||||||
# GNU General Public License for more details.
 | 
					# GNU General Public License for more details.
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
# You should have received a copy of the GNU General Public License
 | 
					# You should have received a copy of the GNU General Public License
 | 
				
			||||||
# along with Ansible.  If not, see <http://www.gnu.org/licenses/>.
 | 
					# along with Compute.  If not, see <http://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"""Tools for data units convertion."""
 | 
					"""Tools for data units convertion."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -21,7 +21,7 @@ from compute.exceptions import InvalidDataUnitError
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class DataUnit(StrEnum):
 | 
					class DataUnit(StrEnum):
 | 
				
			||||||
    """Data units enumerated."""
 | 
					    """Data units enumeration."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    BYTES = 'bytes'
 | 
					    BYTES = 'bytes'
 | 
				
			||||||
    KIB = 'KiB'
 | 
					    KIB = 'KiB'
 | 
				
			||||||
@@ -29,6 +29,13 @@ class DataUnit(StrEnum):
 | 
				
			|||||||
    GIB = 'GiB'
 | 
					    GIB = 'GiB'
 | 
				
			||||||
    TIB = 'TiB'
 | 
					    TIB = 'TiB'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @classmethod
 | 
				
			||||||
 | 
					    def _missing_(cls, name: str) -> 'DataUnit':
 | 
				
			||||||
 | 
					        for member in cls:
 | 
				
			||||||
 | 
					            if member.name.lower() == name.lower():
 | 
				
			||||||
 | 
					                return member
 | 
				
			||||||
 | 
					        return None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def to_bytes(value: int, unit: DataUnit = DataUnit.BYTES) -> int:
 | 
					def to_bytes(value: int, unit: DataUnit = DataUnit.BYTES) -> int:
 | 
				
			||||||
    """Convert value to bytes. See :class:`DataUnit`."""
 | 
					    """Convert value to bytes. See :class:`DataUnit`."""
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										1
									
								
								docs/source/_static/custom.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								docs/source/_static/custom.css
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					div.code-block-caption {background: #d0d0d0;}
 | 
				
			||||||
							
								
								
									
										8
									
								
								docs/source/cli/index.rst
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								docs/source/cli/index.rst
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,8 @@
 | 
				
			|||||||
 | 
					CLI
 | 
				
			||||||
 | 
					===
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. toctree::
 | 
				
			||||||
 | 
					    :maxdepth: 1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    usage
 | 
				
			||||||
 | 
					    reference
 | 
				
			||||||
							
								
								
									
										7
									
								
								docs/source/cli/reference.rst
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								docs/source/cli/reference.rst
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
				
			|||||||
 | 
					CLI Reference
 | 
				
			||||||
 | 
					=============
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. argparse::
 | 
				
			||||||
 | 
					   :module: compute.cli.control
 | 
				
			||||||
 | 
					   :func: get_parser
 | 
				
			||||||
 | 
					   :prog: compute
 | 
				
			||||||
							
								
								
									
										102
									
								
								docs/source/cli/usage.rst
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										102
									
								
								docs/source/cli/usage.rst
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,102 @@
 | 
				
			|||||||
 | 
					Usage
 | 
				
			||||||
 | 
					=====
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Creating compute instances
 | 
				
			||||||
 | 
					--------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					First place your image into images pool path.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Create :file:`inatance.yaml` config file with following content. Replace `debian_12.qcow2` with your actual image filename.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. code-block:: yaml
 | 
				
			||||||
 | 
					   :caption: Using prebuilt QCOW2 disk image
 | 
				
			||||||
 | 
					   :emphasize-lines: 4
 | 
				
			||||||
 | 
					   :linenos:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					   name: myinstance
 | 
				
			||||||
 | 
					   memory: 2048
 | 
				
			||||||
 | 
					   vcpus: 2
 | 
				
			||||||
 | 
					   image: debian_12.qcow2
 | 
				
			||||||
 | 
					   volumes:
 | 
				
			||||||
 | 
					     - type: file
 | 
				
			||||||
 | 
					       is_system: true
 | 
				
			||||||
 | 
					       target: vda
 | 
				
			||||||
 | 
					       capacity:
 | 
				
			||||||
 | 
					         value: 10
 | 
				
			||||||
 | 
					         unit: GiB
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Check out what configuration will be applied when ``init``::
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					   compute init -t
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Initialise instance with command::
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					   compute init
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Also you can use following syntax::
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  compute init yourfile.yaml
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Start instance::
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					   compute start myinstance
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Using ISO installation medium
 | 
				
			||||||
 | 
					`````````````````````````````
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Download ISO image and set it as source for ``cdrom`` device.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Note that the ``image`` parameter is not used here.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. code-block:: yaml
 | 
				
			||||||
 | 
					   :caption: Using ISO image
 | 
				
			||||||
 | 
					   :emphasize-lines: 11-13
 | 
				
			||||||
 | 
					   :linenos:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					   name: myinstance
 | 
				
			||||||
 | 
					   memory: 2048
 | 
				
			||||||
 | 
					   vcpus: 2
 | 
				
			||||||
 | 
					   volumes:
 | 
				
			||||||
 | 
					     - type: file
 | 
				
			||||||
 | 
					       is_system: true
 | 
				
			||||||
 | 
					       target: vda
 | 
				
			||||||
 | 
					       capacity:
 | 
				
			||||||
 | 
					         value: 10
 | 
				
			||||||
 | 
					         unit: GiB
 | 
				
			||||||
 | 
					     - type: file
 | 
				
			||||||
 | 
					       device: cdrom
 | 
				
			||||||
 | 
					       source: /images/debian-12.2.0-amd64-netinst.iso
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					::
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					   compute init
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Now edit instance XML configuration to add VNC-server listen address::
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					   virsh edit myinstance
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Add ``address`` attribute to start listen on all host network interfaces.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. code-block:: xml
 | 
				
			||||||
 | 
					   :caption: libvirt XML config fragment
 | 
				
			||||||
 | 
					   :emphasize-lines: 2
 | 
				
			||||||
 | 
					   :linenos:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					   <graphics type='vnc' port='-1' autoport='yes' listen='0.0.0.0'>
 | 
				
			||||||
 | 
					     <listen type='address' address='0.0.0.0'/>
 | 
				
			||||||
 | 
					   </graphics>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Also you can specify VNC server port. This is **5900** by default.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Start instance and connect to VNC via any VNC client such as `Remmina <https://remmina.org/>`_ or something else.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					::
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					   compute start myinstance
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Finish the OS installation over VNC and then do::
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					   compute setcdrom myinstance /images/debian-12.2.0-amd64-netinst.iso --detach
 | 
				
			||||||
 | 
					   compute powrst myinstance
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CDROM will be detached. ``powrst`` command will perform instance shutdown and start. Instance will booted from `vda` disk.
 | 
				
			||||||
@@ -1,4 +1,3 @@
 | 
				
			|||||||
# Add ../.. to path for autodoc Sphinx extension
 | 
					 | 
				
			||||||
import os
 | 
					import os
 | 
				
			||||||
import sys
 | 
					import sys
 | 
				
			||||||
sys.path.insert(0, os.path.abspath('../..'))
 | 
					sys.path.insert(0, os.path.abspath('../..'))
 | 
				
			||||||
@@ -7,16 +6,18 @@ sys.path.insert(0, os.path.abspath('../..'))
 | 
				
			|||||||
project = 'Compute'
 | 
					project = 'Compute'
 | 
				
			||||||
copyright = '2023, Compute Authors'
 | 
					copyright = '2023, Compute Authors'
 | 
				
			||||||
author = 'Compute Authors'
 | 
					author = 'Compute Authors'
 | 
				
			||||||
release = '0.1.0'
 | 
					release = '0.1.0-dev2'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Sphinx general settings
 | 
					# Sphinx general settings
 | 
				
			||||||
extensions = [
 | 
					extensions = [
 | 
				
			||||||
    'sphinx.ext.autodoc',
 | 
					    'sphinx.ext.autodoc',
 | 
				
			||||||
    'sphinx_multiversion',
 | 
					    'sphinx_multiversion',
 | 
				
			||||||
 | 
					    'sphinxarg.ext',
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
templates_path = ['_templates']
 | 
					templates_path = ['_templates']
 | 
				
			||||||
exclude_patterns = []
 | 
					exclude_patterns = []
 | 
				
			||||||
language = 'en'
 | 
					language = 'en'
 | 
				
			||||||
 | 
					#pygments_style = 'monokai'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# HTML output settings
 | 
					# HTML output settings
 | 
				
			||||||
html_theme = 'alabaster'
 | 
					html_theme = 'alabaster'
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,9 +3,14 @@ Compute
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
Compute instances management library.
 | 
					Compute instances management library.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.. toctree::
 | 
					Contents
 | 
				
			||||||
    :maxdepth: 1
 | 
					--------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. toctree::
 | 
				
			||||||
 | 
					    :maxdepth: 2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    installation
 | 
				
			||||||
 | 
					    cli/index
 | 
				
			||||||
    pyapi/index
 | 
					    pyapi/index
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Indices and tables
 | 
					Indices and tables
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										47
									
								
								docs/source/installation.rst
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								docs/source/installation.rst
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,47 @@
 | 
				
			|||||||
 | 
					Installation
 | 
				
			||||||
 | 
					============
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Install Debian 12 on your host system. If you want use virtual machine as host make sure that nested virtualization is enabled.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					1. Download or build ``compute`` DEB packages.
 | 
				
			||||||
 | 
					2. Install packages::
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      apt-get install ./compute*
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					3. Make sure that ``libvirtd`` and ``dnsmasq`` are enabled and running::
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      systemctl enable --now libvirtd.service
 | 
				
			||||||
 | 
					      systemctl enable --now dnsmasq.service
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					4. Prepare storage pools. You need storage pool for images and for instance volumes.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					   ::
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      for pool in images volumes; do
 | 
				
			||||||
 | 
					          virsh pool-define-as $pool dir - - - - "/$pool"
 | 
				
			||||||
 | 
					          virsh pool-build $pool
 | 
				
			||||||
 | 
					          virsh pool-start $pool
 | 
				
			||||||
 | 
					          virsh pool-autostart $pool
 | 
				
			||||||
 | 
					      done
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					5. Prepare env. Set environment variables in your `~/.profile`, `~/.bashrc` or global in `/etc/profile.d/compute` or `/etc/bash.bashrc`:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					   .. code-block:: sh
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      export CMP_IMAGES_POOL=images
 | 
				
			||||||
 | 
					      export CMP_VOLUMES_POOL=volumes
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					   Configuration file is yet not supported.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					   Make sure the variables are exported to the environment::
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      printenv | grep CMP_
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					   If the command didn't show anything source your rc files or relogin.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					6. Prepare network::
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      virsh net-start default
 | 
				
			||||||
 | 
					      virsh net-autostart default
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					7. Done. Now you can follow `CLI instructions <cli/index.html>`_
 | 
				
			||||||
@@ -1,38 +1,8 @@
 | 
				
			|||||||
Python API
 | 
					Python API
 | 
				
			||||||
==========
 | 
					==========
 | 
				
			||||||
 | 
					
 | 
				
			||||||
The API allows you to perform actions on instances programmatically.
 | 
					API Reference
 | 
				
			||||||
 | 
					-------------
 | 
				
			||||||
.. code-block:: python
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    import compute
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    with compute.Session() as session:
 | 
					 | 
				
			||||||
        instance = session.get_instance('myinstance')
 | 
					 | 
				
			||||||
        info = instance.get_info()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    print(info)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
:class:`Session` context manager provides an abstraction over :class:`libvirt.virConnect`
 | 
					 | 
				
			||||||
and returns objects of other classes of the present library.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Entity representation
 | 
					 | 
				
			||||||
---------------------
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Entities such as a compute-instance are represented as classes. These classes directly
 | 
					 | 
				
			||||||
call libvirt methods to perform operations on the hypervisor. An example class is
 | 
					 | 
				
			||||||
:class:`Volume`.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
The configuration files of various libvirt objects in `compute` are described by special
 | 
					 | 
				
			||||||
dataclasses. The dataclass stores object parameters in its properties and can return an
 | 
					 | 
				
			||||||
XML config for libvirt using the ``to_xml()`` method. For example :class:`VolumeConfig`.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
`Pydantic <https://docs.pydantic.dev/>`_ models are used to validate input data.
 | 
					 | 
				
			||||||
For example :class:`VolumeSchema`.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Modules documentation
 | 
					 | 
				
			||||||
---------------------
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
.. toctree::
 | 
					.. toctree::
 | 
				
			||||||
    :maxdepth: 4
 | 
					    :maxdepth: 4
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										5
									
								
								docs/source/pyapi/instance/devices.rst
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								docs/source/pyapi/instance/devices.rst
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
				
			|||||||
 | 
					``devices``
 | 
				
			||||||
 | 
					===========
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. automodule:: compute.instance.devices
 | 
				
			||||||
 | 
					   :members:
 | 
				
			||||||
@@ -7,4 +7,5 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    instance
 | 
					    instance
 | 
				
			||||||
    guest_agent
 | 
					    guest_agent
 | 
				
			||||||
 | 
					    devices
 | 
				
			||||||
    schemas
 | 
					    schemas
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -12,3 +12,10 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
.. automodule:: compute.utils.ids
 | 
					.. automodule:: compute.utils.ids
 | 
				
			||||||
   :members:
 | 
					   :members:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					``utils.dictutil``
 | 
				
			||||||
 | 
					------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. automodule:: compute.utils.dictutil
 | 
				
			||||||
 | 
					   :members:
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -14,6 +14,7 @@ RUN apt-get update; \
 | 
				
			|||||||
        python3-setuptools \
 | 
					        python3-setuptools \
 | 
				
			||||||
        python3-sphinx \
 | 
					        python3-sphinx \
 | 
				
			||||||
        python3-sphinx-multiversion \
 | 
					        python3-sphinx-multiversion \
 | 
				
			||||||
 | 
					        python3-sphinx-argparse \
 | 
				
			||||||
        python3-libvirt \
 | 
					        python3-libvirt \
 | 
				
			||||||
        python3-lxml \
 | 
					        python3-lxml \
 | 
				
			||||||
        python3-yaml \
 | 
					        python3-yaml \
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -18,8 +18,10 @@ _compute_root_cmd="
 | 
				
			|||||||
    status
 | 
					    status
 | 
				
			||||||
    setvcpus
 | 
					    setvcpus
 | 
				
			||||||
    setmem
 | 
					    setmem
 | 
				
			||||||
    setpass"
 | 
					    setpass
 | 
				
			||||||
_compute_init_opts=""
 | 
					    setcdrom
 | 
				
			||||||
 | 
					    delete"
 | 
				
			||||||
 | 
					_compute_init_opts="--test --start"
 | 
				
			||||||
_compute_exec_opts="--timeout --executable --env --no-join-args"
 | 
					_compute_exec_opts="--timeout --executable --env --no-join-args"
 | 
				
			||||||
_compute_ls_opts=""
 | 
					_compute_ls_opts=""
 | 
				
			||||||
_compute_start_opts=""
 | 
					_compute_start_opts=""
 | 
				
			||||||
@@ -33,6 +35,8 @@ _compute_status_opts=""
 | 
				
			|||||||
_compute_setvcpus_opts=""
 | 
					_compute_setvcpus_opts=""
 | 
				
			||||||
_compute_setmem_opts=""
 | 
					_compute_setmem_opts=""
 | 
				
			||||||
_compute_setpass_opts="--encrypted"
 | 
					_compute_setpass_opts="--encrypted"
 | 
				
			||||||
 | 
					_compute_setcdrom_opts="--detach"
 | 
				
			||||||
 | 
					_compute_delete_opts="--yes --save-volumes"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
_compute_complete_instances()
 | 
					_compute_complete_instances()
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
@@ -78,6 +82,8 @@ _compute_complete()
 | 
				
			|||||||
                setvcpus) _compute_compreply "$_compute_setvcpus_opts";;
 | 
					                setvcpus) _compute_compreply "$_compute_setvcpus_opts";;
 | 
				
			||||||
                setmem) _compute_compreply "$_compute_setmem_opts";;
 | 
					                setmem) _compute_compreply "$_compute_setmem_opts";;
 | 
				
			||||||
                setpass) _compute_compreply "$_compute_setpass_opts";;
 | 
					                setpass) _compute_compreply "$_compute_setpass_opts";;
 | 
				
			||||||
 | 
					                setcdrom) _compute_compreply "$_compute_setcdrom_opts";;
 | 
				
			||||||
 | 
					                delete) _compute_compreply "$_compute_delete_opts";;
 | 
				
			||||||
                *) COMPREPLY=()
 | 
					                *) COMPREPLY=()
 | 
				
			||||||
            esac
 | 
					            esac
 | 
				
			||||||
            ;;
 | 
					            ;;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -13,6 +13,7 @@ Build-Depends:
 | 
				
			|||||||
 python3-all,
 | 
					 python3-all,
 | 
				
			||||||
 python3-sphinx,
 | 
					 python3-sphinx,
 | 
				
			||||||
 python3-sphinx-multiversion,
 | 
					 python3-sphinx-multiversion,
 | 
				
			||||||
 | 
					 python3-sphinx-argparse,
 | 
				
			||||||
 python3-libvirt,
 | 
					 python3-libvirt,
 | 
				
			||||||
 python3-lxml,
 | 
					 python3-lxml,
 | 
				
			||||||
 python3-yaml,
 | 
					 python3-yaml,
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										20
									
								
								poetry.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										20
									
								
								poetry.lock
									
									
									
										generated
									
									
									
								
							@@ -696,6 +696,24 @@ docs = ["sphinxcontrib-websupport"]
 | 
				
			|||||||
lint = ["docutils-stubs", "flake8 (>=3.5.0)", "flake8-simplify", "isort", "mypy (>=0.990)", "ruff", "sphinx-lint", "types-requests"]
 | 
					lint = ["docutils-stubs", "flake8 (>=3.5.0)", "flake8-simplify", "isort", "mypy (>=0.990)", "ruff", "sphinx-lint", "types-requests"]
 | 
				
			||||||
test = ["cython (>=3.0)", "filelock", "html5lib", "pytest (>=4.6)", "setuptools (>=67.0)"]
 | 
					test = ["cython (>=3.0)", "filelock", "html5lib", "pytest (>=4.6)", "setuptools (>=67.0)"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "sphinx-argparse"
 | 
				
			||||||
 | 
					version = "0.4.0"
 | 
				
			||||||
 | 
					description = "A sphinx extension that automatically documents argparse commands and options"
 | 
				
			||||||
 | 
					category = "dev"
 | 
				
			||||||
 | 
					optional = false
 | 
				
			||||||
 | 
					python-versions = ">=3.7"
 | 
				
			||||||
 | 
					files = [
 | 
				
			||||||
 | 
					    {file = "sphinx_argparse-0.4.0-py3-none-any.whl", hash = "sha256:73bee01f7276fae2bf621ccfe4d167af7306e7288e3482005405d9f826f9b037"},
 | 
				
			||||||
 | 
					    {file = "sphinx_argparse-0.4.0.tar.gz", hash = "sha256:e0f34184eb56f12face774fbc87b880abdb9017a0998d1ec559b267e9697e449"},
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[package.dependencies]
 | 
				
			||||||
 | 
					sphinx = ">=1.2.0"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[package.extras]
 | 
				
			||||||
 | 
					markdown = ["CommonMark (>=0.5.6)"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "sphinx-autobuild"
 | 
					name = "sphinx-autobuild"
 | 
				
			||||||
version = "2021.3.14"
 | 
					version = "2021.3.14"
 | 
				
			||||||
@@ -895,4 +913,4 @@ zstd = ["zstandard (>=0.18.0)"]
 | 
				
			|||||||
[metadata]
 | 
					[metadata]
 | 
				
			||||||
lock-version = "2.0"
 | 
					lock-version = "2.0"
 | 
				
			||||||
python-versions = '^3.11'
 | 
					python-versions = '^3.11'
 | 
				
			||||||
content-hash = "e5c07eebe683b92360ec12cada14fc5ccbe4e4add52549bf978f580e551abfb0"
 | 
					content-hash = "cbded73a481e7c6d6e4c4d5de8e37ac5a53848ab774090a913f1846ef4d7421e"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -23,6 +23,7 @@ isort = '^5.12.0'
 | 
				
			|||||||
sphinx = '^7.2.6'
 | 
					sphinx = '^7.2.6'
 | 
				
			||||||
sphinx-autobuild = '^2021.3.14'
 | 
					sphinx-autobuild = '^2021.3.14'
 | 
				
			||||||
sphinx-multiversion = '^0.2.4'
 | 
					sphinx-multiversion = '^0.2.4'
 | 
				
			||||||
 | 
					sphinx-argparse = "^0.4.0"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[build-system]
 | 
					[build-system]
 | 
				
			||||||
requires = ['poetry-core']
 | 
					requires = ['poetry-core']
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user