various improvements

This commit is contained in:
ge
2023-11-23 02:34:02 +03:00
parent b9d089dd78
commit 05f90b14f2
200 changed files with 15968 additions and 84 deletions

View File

@ -1,6 +1,21 @@
# 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.
#
# Ansible 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 Ansible. If not, see <http://www.gnu.org/licenses/>.
"""Compute instances management library."""
__version__ = '0.1.0'
__version__ = '0.1.0-dev1'
from .instance import Instance, InstanceConfig, InstanceSchema
from .session import Session

View File

@ -1,6 +1,21 @@
# 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.
#
# Ansible 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 Ansible. If not, see <http://www.gnu.org/licenses/>.
"""Command line interface for compute module."""
from compute.cli import control
from compute.cli import main
control.cli()
main.cli()

View File

@ -1,3 +1,18 @@
# 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.
#
# Ansible 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 Ansible. If not, see <http://www.gnu.org/licenses/>.
"""Command line interface."""
import argparse
@ -15,10 +30,7 @@ import yaml
from pydantic import ValidationError
from compute import __version__
from compute.exceptions import (
ComputeServiceError,
GuestAgentTimeoutExceededError,
)
from compute.exceptions import ComputeError, GuestAgentTimeoutExceededError
from compute.instance import GuestAgent
from compute.session import Session
from compute.utils import ids
@ -198,10 +210,23 @@ def _create_instance(session: Session, file: io.TextIOWrapper) -> None:
sys.exit()
def _shutdown_instance(session: Session, args: argparse.Namespace) -> None:
instance = session.get_instance(args.instance)
if args.soft:
method = 'SOFT'
elif args.hard:
method = 'HARD'
elif args.unsafe:
method = 'UNSAFE'
else:
method = 'NORMAL'
instance.shutdown(method)
def main(session: Session, args: argparse.Namespace) -> None:
"""Perform actions."""
match args.command:
case 'create':
case 'init':
_create_instance(session, args.file)
case 'exec':
_exec_guest_agent_command(session, args)
@ -211,14 +236,16 @@ def main(session: Session, args: argparse.Namespace) -> None:
instance = session.get_instance(args.instance)
instance.start()
case 'shutdown':
instance = session.get_instance(args.instance)
instance.shutdown(args.method)
_shutdown_instance(session, args)
case 'reboot':
instance = session.get_instance(args.instance)
instance.reboot()
case 'reset':
instance = session.get_instance(args.instance)
instance.reset()
case 'powrst':
instance = session.get_instance(args.instance)
instance.power_reset()
case 'pause':
instance = session.get_instance(args.instance)
instance.pause()
@ -234,7 +261,7 @@ def main(session: Session, args: argparse.Namespace) -> None:
case 'setmem':
instance = session.get_instance(args.instance)
instance.set_memory(args.memory, live=True)
case 'setpasswd':
case 'setpass':
instance = session.get_instance(args.instance)
instance.set_user_password(
args.username,
@ -261,7 +288,6 @@ def cli() -> None: # noqa: PLR0915
'-c',
'--connect',
metavar='URI',
default='qemu:///system',
help='libvirt connection URI',
)
root.add_argument(
@ -270,7 +296,7 @@ def cli() -> None: # noqa: PLR0915
type=str.lower,
metavar='LEVEL',
choices=log_levels,
help='log level [envvar: CMP_LOG]',
help='log level',
)
root.add_argument(
'-V',
@ -280,13 +306,16 @@ def cli() -> None: # noqa: PLR0915
)
subparsers = root.add_subparsers(dest='command', metavar='COMMAND')
# create command
create = subparsers.add_parser(
'create', help='create new instance from YAML config file'
# init command
init = subparsers.add_parser(
'init', help='initialise instance using YAML config file'
)
create.add_argument(
init.add_argument(
'file',
type=argparse.FileType('r', encoding='UTF-8'),
nargs='?',
default='instance.yaml',
help='instance config [default: instance.yaml]',
)
# exec subcommand
@ -307,14 +336,14 @@ def cli() -> None: # noqa: PLR0915
default=60,
help=(
'waiting time in seconds for a command to be executed '
'in guest, 60 sec by default'
'in guest [default: 60]'
),
)
execute.add_argument(
'-x',
'--executable',
default='/bin/sh',
help='path to executable in guest, /bin/sh by default',
help='path to executable in guest [default: /bin/sh]',
)
execute.add_argument(
'-e',
@ -352,12 +381,36 @@ def cli() -> None: # noqa: PLR0915
# shutdown subcommand
shutdown = subparsers.add_parser('shutdown', help='shutdown instance')
shutdown.add_argument('instance')
shutdown.add_argument(
'-m',
'--method',
choices=['soft', 'normal', 'hard', 'unsafe'],
default='normal',
help='use shutdown method',
shutdown_opts = shutdown.add_mutually_exclusive_group()
shutdown_opts.add_argument(
'-s',
'--soft',
action='store_true',
help='normal guest OS shutdown, guest agent is used',
)
shutdown_opts.add_argument(
'-n',
'--normal',
action='store_true',
help='shutdown with hypervisor selected method [default]',
)
shutdown_opts.add_argument(
'-H',
'--hard',
action='store_true',
help=(
"gracefully destroy instance, it's like long "
'pressing the power button'
),
)
shutdown_opts.add_argument(
'-u',
'--unsafe',
action='store_true',
help=(
'destroy instance, this is similar to a power outage '
'and may result in data loss or corruption'
),
)
# reboot subcommand
@ -368,6 +421,10 @@ def cli() -> None: # noqa: PLR0915
reset = subparsers.add_parser('reset', help='reset instance')
reset.add_argument('instance')
# powrst subcommand
powrst = subparsers.add_parser('powrst', help='power reset instance')
powrst.add_argument('instance')
# pause subcommand
pause = subparsers.add_parser('pause', help='pause instance')
pause.add_argument('instance')
@ -390,15 +447,15 @@ def cli() -> None: # noqa: PLR0915
setmem.add_argument('instance')
setmem.add_argument('memory', type=int, help='memory in MiB')
# setpasswd subcommand
setpasswd = subparsers.add_parser(
'setpasswd',
# setpass subcommand
setpass = subparsers.add_parser(
'setpass',
help='set user password in guest',
)
setpasswd.add_argument('instance')
setpasswd.add_argument('username')
setpasswd.add_argument('password')
setpasswd.add_argument(
setpass.add_argument('instance')
setpass.add_argument('username')
setpass.add_argument('password')
setpass.add_argument(
'-e',
'--encrypted',
action='store_true',
@ -419,10 +476,18 @@ def cli() -> None: # noqa: PLR0915
)
log.debug('CLI started with args: %s', args)
connect_uri = (
args.connect
or os.getenv('CMP_LIBVIRT_URI')
or os.getenv('LIBVIRT_DEFAULT_URI')
or 'qemu:///system'
)
try:
with Session(args.connect) as session:
with Session(connect_uri) as session:
main(session, args)
except ComputeServiceError as e:
except ComputeError as e:
sys.exit(f'error: {e}')
except KeyboardInterrupt:
sys.exit()

30
compute/common.py Normal file
View File

@ -0,0 +1,30 @@
# 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.
#
# Ansible 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 Ansible. If not, see <http://www.gnu.org/licenses/>.
"""Common symbols."""
from abc import ABC, abstractmethod
class EntityConfig(ABC):
"""An abstract entity XML config builder class."""
@abstractmethod
def to_xml(self) -> str:
"""Return device XML config."""
raise NotImplementedError
DeviceConfig = EntityConfig

View File

@ -1,19 +1,34 @@
# 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.
#
# Ansible 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 Ansible. If not, see <http://www.gnu.org/licenses/>.
"""Exceptions."""
class ComputeServiceError(Exception):
"""Basic exception class for Compute."""
class ComputeError(Exception):
"""Basic exception class."""
class ConfigLoaderError(ComputeServiceError):
class ConfigLoaderError(ComputeError):
"""Something went wrong when loading configuration."""
class SessionError(ComputeServiceError):
class SessionError(ComputeError):
"""Something went wrong while connecting to libvirtd."""
class GuestAgentError(ComputeServiceError):
class GuestAgentError(ComputeError):
"""Something went wring when QEMU Guest Agent call."""
@ -33,7 +48,7 @@ class GuestAgentCommandNotSupportedError(GuestAgentError):
"""Guest agent command is not supported or blacklisted on guest."""
class StoragePoolError(ComputeServiceError):
class StoragePoolError(ComputeError):
"""Something went wrong when operating with storage pool."""
@ -53,7 +68,7 @@ class VolumeNotFoundError(StoragePoolError):
super().__init__(f"storage volume '{msg}' not found")
class InstanceError(ComputeServiceError):
class InstanceError(ComputeError):
"""Something went wrong while interacting with the domain."""

View File

@ -1,3 +1,18 @@
# 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.
#
# Ansible 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 Ansible. If not, see <http://www.gnu.org/licenses/>.
from .guest_agent import GuestAgent
from .instance import Instance, InstanceConfig
from .schemas import InstanceSchema

View File

@ -1,3 +1,18 @@
# 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.
#
# Ansible 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 Ansible. If not, see <http://www.gnu.org/licenses/>.
"""Interacting with the QEMU Guest Agent."""
import json

View File

@ -1,3 +1,18 @@
# 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.
#
# Ansible 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 Ansible. If not, see <http://www.gnu.org/licenses/>.
"""Manage compute instances."""
__all__ = ['Instance', 'InstanceConfig', 'InstanceInfo']
@ -9,6 +24,7 @@ import libvirt
from lxml import etree
from lxml.builder import E
from compute.common import DeviceConfig, EntityConfig
from compute.exceptions import (
GuestAgentCommandNotSupportedError,
InstanceError,
@ -28,8 +44,8 @@ from .schemas import (
log = logging.getLogger(__name__)
class InstanceConfig:
"""Compute instance config builder."""
class InstanceConfig(EntityConfig):
"""Compute instance XML config builder."""
def __init__(self, schema: InstanceSchema):
"""
@ -181,10 +197,6 @@ class InstanceInfo(NamedTuple):
cputime: int
class DeviceConfig:
"""Abstract device config class."""
class Instance:
"""Manage compute instances."""
@ -492,7 +504,7 @@ class Instance:
return child[0].getparent() if child else None
def attach_device(
self, device: 'DeviceConfig', *, live: bool = False
self, device: DeviceConfig, *, live: bool = False
) -> None:
"""
Attach device to compute instance.
@ -517,7 +529,7 @@ class Instance:
self.domain.attachDeviceFlags(device.to_xml(), flags=flags)
def detach_device(
self, device: 'DeviceConfig', *, live: bool = False
self, device: DeviceConfig, *, live: bool = False
) -> None:
"""
Dettach device from compute instance.
@ -545,8 +557,8 @@ class Instance:
"""
Detach disk device by target name.
There is no ``attach_disk()`` method. Use :method:`attach_device`
with :class:`DiskConfig` as parameter.
There is no ``attach_disk()`` method. Use :func:`attach_device`
with :class:`DiskConfig` as argument.
:param name: Disk name e.g. 'vda', 'sda', etc. This name may
not match the name of the disk inside the guest OS.
@ -574,14 +586,14 @@ class Instance:
raise InstanceError(msg)
self.detach_device(DiskConfig(**disk_params), live=True)
def resize_volume(
def resize_disk(
self, name: str, capacity: int, unit: units.DataUnit
) -> None:
"""
Resize attached block device.
:param name: Disk device name e.g. `vda`, `sda`, etc.
:param capacity: New volume capacity.
:param capacity: New capacity.
:param unit: Capacity unit.
"""
self.domain.blockResize(
@ -590,6 +602,10 @@ class Instance:
flags=libvirt.VIR_DOMAIN_BLOCK_RESIZE_BYTES,
)
def get_disks(self) -> list[DiskConfig]:
"""Return list of attached disks."""
raise NotImplementedError
def pause(self) -> None:
"""Pause instance."""
if not self.is_running():
@ -600,9 +616,9 @@ class Instance:
"""Resume paused instance."""
self.domain.resume()
def list_ssh_keys(self, user: str) -> list[str]:
def get_ssh_keys(self, user: str) -> list[str]:
"""
Get list of SSH keys on guest for specific user.
Return list of SSH keys on guest for specific user.
:param user: Username.
"""
@ -617,7 +633,7 @@ class Instance:
"""
raise NotImplementedError
def remove_ssh_keys(self, user: str, ssh_keys: list[str]) -> None:
def delete_ssh_keys(self, user: str, ssh_keys: list[str]) -> None:
"""
Remove SSH keys from guest for specific user.
@ -632,7 +648,7 @@ class Instance:
"""
Set new user password in guest OS.
This action performs by guest agent inside guest.
This action performs by guest agent inside the guest.
:param user: Username.
:param password: Password.
@ -653,6 +669,7 @@ class Instance:
return self.domain.XMLDesc(flags)
def delete(self) -> None:
"""Undefine instance and delete local volumes."""
"""Undefine instance."""
# TODO @ge: delete local disks
self.shutdown(method='HARD')
self.domain.undefine()

View File

@ -1,3 +1,18 @@
# 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.
#
# Ansible 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 Ansible. If not, see <http://www.gnu.org/licenses/>.
"""Compute instance related objects schemas."""
import re

View File

@ -1,3 +1,18 @@
# 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.
#
# Ansible 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 Ansible. If not, see <http://www.gnu.org/licenses/>.
"""Hypervisor session manager."""
import logging

View File

@ -1,2 +1,17 @@
# 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.
#
# Ansible 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 Ansible. If not, see <http://www.gnu.org/licenses/>.
from .pool import StoragePool
from .volume import DiskConfig, Volume, VolumeConfig

View File

@ -1,3 +1,18 @@
# 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.
#
# Ansible 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 Ansible. If not, see <http://www.gnu.org/licenses/>.
"""Manage storage pools."""
import logging

View File

@ -1,3 +1,18 @@
# 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.
#
# Ansible 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 Ansible. If not, see <http://www.gnu.org/licenses/>.
"""Manage storage volumes."""
from dataclasses import dataclass
@ -8,13 +23,14 @@ import libvirt
from lxml import etree
from lxml.builder import E
from compute.common import DeviceConfig, EntityConfig
from compute.utils import units
@dataclass
class VolumeConfig:
class VolumeConfig(EntityConfig):
"""
Storage volume config builder.
Storage volume XML config builder.
Generate XML config for creating a volume in a libvirt
storage pool.
@ -48,9 +64,9 @@ class VolumeConfig:
@dataclass
class DiskConfig:
class DiskConfig(DeviceConfig):
"""
Disk config builder.
Disk XML config builder.
Generate XML config for attaching or detaching storage volumes
to compute instances.

View File

@ -1,3 +1,18 @@
# 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.
#
# Ansible 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 Ansible. If not, see <http://www.gnu.org/licenses/>.
"""Configuration loader."""
import tomllib

View File

@ -1,3 +1,18 @@
# 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.
#
# Ansible 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 Ansible. If not, see <http://www.gnu.org/licenses/>.
"""Random identificators."""
# ruff: noqa: S311, C417

View File

@ -1,3 +1,18 @@
# 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.
#
# Ansible 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 Ansible. If not, see <http://www.gnu.org/licenses/>.
"""Tools for data units convertion."""
from enum import StrEnum