various improvements
This commit is contained in:
		@@ -1,26 +0,0 @@
 | 
			
		||||
import argparse
 | 
			
		||||
 | 
			
		||||
from compute import Session
 | 
			
		||||
from compute.utils import identifiers
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _create_instance(session: Session, args: argparse.Namespace) -> None:
 | 
			
		||||
    """
 | 
			
		||||
    Умолчания (достать информацию из либвирта):
 | 
			
		||||
    - arch
 | 
			
		||||
    - machine
 | 
			
		||||
    - emulator
 | 
			
		||||
    - CPU
 | 
			
		||||
        - cpu_vendor
 | 
			
		||||
        - cpu_model
 | 
			
		||||
        - фичи
 | 
			
		||||
    - max_memory
 | 
			
		||||
    - max_vcpus
 | 
			
		||||
 | 
			
		||||
    (сегнерировать):
 | 
			
		||||
    - MAC адрес
 | 
			
		||||
    - boot_order = ('cdrom', 'hd')
 | 
			
		||||
    - title = ''
 | 
			
		||||
    - name = uuid.uuid4().hex
 | 
			
		||||
    """
 | 
			
		||||
    print(args)
 | 
			
		||||
@@ -1,12 +1,18 @@
 | 
			
		||||
"""Command line interface."""
 | 
			
		||||
 | 
			
		||||
import argparse
 | 
			
		||||
import io
 | 
			
		||||
import logging
 | 
			
		||||
import os
 | 
			
		||||
import shlex
 | 
			
		||||
import sys
 | 
			
		||||
from collections import UserDict
 | 
			
		||||
from typing import Any
 | 
			
		||||
from uuid import uuid4
 | 
			
		||||
 | 
			
		||||
import libvirt
 | 
			
		||||
import yaml
 | 
			
		||||
from pydantic import ValidationError
 | 
			
		||||
 | 
			
		||||
from compute import __version__
 | 
			
		||||
from compute.exceptions import (
 | 
			
		||||
@@ -15,8 +21,7 @@ from compute.exceptions import (
 | 
			
		||||
)
 | 
			
		||||
from compute.instance import GuestAgent
 | 
			
		||||
from compute.session import Session
 | 
			
		||||
 | 
			
		||||
from ._create import _create_instance
 | 
			
		||||
from compute.utils import ids
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
log = logging.getLogger(__name__)
 | 
			
		||||
@@ -37,41 +42,41 @@ class Table:
 | 
			
		||||
        """Initialise Table."""
 | 
			
		||||
        self.whitespace = whitespace or '\t'
 | 
			
		||||
        self.header = []
 | 
			
		||||
        self._rows = []
 | 
			
		||||
        self._table = ''
 | 
			
		||||
        self.rows = []
 | 
			
		||||
        self.table = ''
 | 
			
		||||
 | 
			
		||||
    def row(self, row: list) -> None:
 | 
			
		||||
    def add_row(self, row: list) -> None:
 | 
			
		||||
        """Add table row."""
 | 
			
		||||
        self._rows.append([str(col) for col in row])
 | 
			
		||||
        self.rows.append([str(col) for col in row])
 | 
			
		||||
 | 
			
		||||
    def rows(self, rows: list[list]) -> None:
 | 
			
		||||
    def add_rows(self, rows: list[list]) -> None:
 | 
			
		||||
        """Add multiple rows."""
 | 
			
		||||
        for row in rows:
 | 
			
		||||
            self.row(row)
 | 
			
		||||
            self.add_row(row)
 | 
			
		||||
 | 
			
		||||
    def __str__(self) -> str:
 | 
			
		||||
        """Build table and return."""
 | 
			
		||||
        widths = [max(map(len, col)) for col in zip(*self._rows, strict=True)]
 | 
			
		||||
        self._rows.insert(0, [str(h).upper() for h in self.header])
 | 
			
		||||
        for row in self._rows:
 | 
			
		||||
            self._table += self.whitespace.join(
 | 
			
		||||
        widths = [max(map(len, col)) for col in zip(*self.rows, strict=True)]
 | 
			
		||||
        self.rows.insert(0, [str(h).upper() for h in self.header])
 | 
			
		||||
        for row in self.rows:
 | 
			
		||||
            self.table += self.whitespace.join(
 | 
			
		||||
                (
 | 
			
		||||
                    val.ljust(width)
 | 
			
		||||
                    for val, width in zip(row, widths, strict=True)
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
            self._table += '\n'
 | 
			
		||||
        return self._table.strip()
 | 
			
		||||
            self.table += '\n'
 | 
			
		||||
        return self.table.strip()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _list_instances(session: Session) -> None:
 | 
			
		||||
    table = Table()
 | 
			
		||||
    table.header = ['NAME', 'STATE']
 | 
			
		||||
    for instance in session.list_instances():
 | 
			
		||||
        table.row(
 | 
			
		||||
        table.add_row(
 | 
			
		||||
            [
 | 
			
		||||
                instance.name,
 | 
			
		||||
                instance.status,
 | 
			
		||||
                instance.get_status(),
 | 
			
		||||
            ]
 | 
			
		||||
        )
 | 
			
		||||
    print(table)
 | 
			
		||||
@@ -113,11 +118,93 @@ def _exec_guest_agent_command(
 | 
			
		||||
    sys.exit(output.exitcode)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class _NotPresent:
 | 
			
		||||
    """
 | 
			
		||||
    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:
 | 
			
		||||
        data = _FillableDict(yaml.load(file.read(), Loader=yaml.SafeLoader))
 | 
			
		||||
        log.debug('Read from file: %s', data)
 | 
			
		||||
    except yaml.YAMLError as e:
 | 
			
		||||
        sys.exit(f'error: cannot parse YAML: {e}')
 | 
			
		||||
 | 
			
		||||
    capabilities = session.get_capabilities()
 | 
			
		||||
    node_info = session.get_node_info()
 | 
			
		||||
 | 
			
		||||
    data.fill('name', uuid4().hex)
 | 
			
		||||
    data.fill('title', None)
 | 
			
		||||
    data.fill('description', None)
 | 
			
		||||
    data.fill('arch', capabilities.arch)
 | 
			
		||||
    data.fill('machine', capabilities.machine)
 | 
			
		||||
    data.fill('emulator', capabilities.emulator)
 | 
			
		||||
    data.fill('max_vcpus', node_info.cpus)
 | 
			
		||||
    data.fill('max_memory', node_info.memory)
 | 
			
		||||
    data.fill('cpu', {})
 | 
			
		||||
    cpu = {
 | 
			
		||||
        'emulation_mode': 'host-passthrough',
 | 
			
		||||
        'model': None,
 | 
			
		||||
        'vendor': None,
 | 
			
		||||
        'topology': None,
 | 
			
		||||
        'features': None,
 | 
			
		||||
    }
 | 
			
		||||
    data['cpu'] = _merge_dicts(data['cpu'], cpu)
 | 
			
		||||
    data.fill(
 | 
			
		||||
        'network_interfaces',
 | 
			
		||||
        [{'source': 'default', 'mac': ids.random_mac()}],
 | 
			
		||||
    )
 | 
			
		||||
    data.fill('boot', {'order': ['cdrom', 'hd']})
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        log.debug('Input data: %s', data)
 | 
			
		||||
        session.create_instance(**data)
 | 
			
		||||
    except ValidationError as e:
 | 
			
		||||
        for error in e.errors():
 | 
			
		||||
            fields = '.'.join([str(lc) for lc in error['loc']])
 | 
			
		||||
            print(
 | 
			
		||||
                f"validation error: {fields}: {error['msg']}",
 | 
			
		||||
                file=sys.stderr,
 | 
			
		||||
            )
 | 
			
		||||
        sys.exit()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def main(session: Session, args: argparse.Namespace) -> None:
 | 
			
		||||
    """Perform actions."""
 | 
			
		||||
    match args.command:
 | 
			
		||||
        case 'create':
 | 
			
		||||
            _create_instance(session, args)
 | 
			
		||||
            _create_instance(session, args.file)
 | 
			
		||||
        case 'exec':
 | 
			
		||||
            _exec_guest_agent_command(session, args)
 | 
			
		||||
        case 'ls':
 | 
			
		||||
@@ -179,30 +266,13 @@ def cli() -> None:  # noqa: PLR0915
 | 
			
		||||
    subparsers = root.add_subparsers(dest='command', metavar='COMMAND')
 | 
			
		||||
 | 
			
		||||
    # create command
 | 
			
		||||
    create = subparsers.add_parser('create', help='create compute instance')
 | 
			
		||||
    create.add_argument('image', nargs='?')
 | 
			
		||||
    create.add_argument('--name', help='instance name, used as ID')
 | 
			
		||||
    create.add_argument('--title', help='human-understandable instance title')
 | 
			
		||||
    create.add_argument('--desc', default='', help='instance description')
 | 
			
		||||
    create.add_argument('--memory', type=int, help='memory in MiB')
 | 
			
		||||
    create.add_argument('--max-memory', type=int, help='max memory in MiB')
 | 
			
		||||
    create.add_argument('--vcpus', type=int)
 | 
			
		||||
    create.add_argument('--max-vcpus', type=int)
 | 
			
		||||
    create.add_argument('--cpu-vendor')
 | 
			
		||||
    create.add_argument('--cpu-model')
 | 
			
		||||
    create.add_argument(
 | 
			
		||||
        '--cpu-emulation-mode',
 | 
			
		||||
        choices=['host-passthrough', 'host-model', 'custom'],
 | 
			
		||||
        default='host-passthrough',
 | 
			
		||||
    create = subparsers.add_parser(
 | 
			
		||||
        'create', help='create new instance from YAML config file'
 | 
			
		||||
    )
 | 
			
		||||
    create.add_argument(
 | 
			
		||||
        'file',
 | 
			
		||||
        type=argparse.FileType('r', encoding='UTF-8'),
 | 
			
		||||
    )
 | 
			
		||||
    create.add_argument('--cpu-features')
 | 
			
		||||
    create.add_argument('--cpu-topology')
 | 
			
		||||
    create.add_argument('--mahine')
 | 
			
		||||
    create.add_argument('--emulator')
 | 
			
		||||
    create.add_argument('--arch')
 | 
			
		||||
    create.add_argument('--boot-order')
 | 
			
		||||
    create.add_argument('--volume')
 | 
			
		||||
    create.add_argument('-f', '--file', help='create instance from YAML')
 | 
			
		||||
 | 
			
		||||
    # exec subcommand
 | 
			
		||||
    execute = subparsers.add_parser(
 | 
			
		||||
@@ -303,14 +373,17 @@ def cli() -> None:  # noqa: PLR0915
 | 
			
		||||
    if log_level in log_levels:
 | 
			
		||||
        logging.basicConfig(level=log_levels[log_level])
 | 
			
		||||
 | 
			
		||||
    log.debug('CLI started with args: %s', args)
 | 
			
		||||
    # Perform actions
 | 
			
		||||
    try:
 | 
			
		||||
        with Session(args.connect) as session:
 | 
			
		||||
            main(session, args)
 | 
			
		||||
    except ComputeServiceError as e:
 | 
			
		||||
        sys.exit(f'error: {e}')
 | 
			
		||||
    except (KeyboardInterrupt, SystemExit):
 | 
			
		||||
    except KeyboardInterrupt:
 | 
			
		||||
        sys.exit()
 | 
			
		||||
    except SystemExit as e:
 | 
			
		||||
        sys.exit(e)
 | 
			
		||||
    except Exception as e:  # noqa: BLE001
 | 
			
		||||
        sys.exit(f'unexpected error {type(e)}: {e}')
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user