various improvements
This commit is contained in:
		
							
								
								
									
										0
									
								
								compute/cli/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								compute/cli/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										26
									
								
								compute/cli/_create.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								compute/cli/_create.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,26 @@
 | 
			
		||||
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)
 | 
			
		||||
							
								
								
									
										319
									
								
								compute/cli/control.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										319
									
								
								compute/cli/control.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,319 @@
 | 
			
		||||
"""Command line interface."""
 | 
			
		||||
 | 
			
		||||
import argparse
 | 
			
		||||
import logging
 | 
			
		||||
import os
 | 
			
		||||
import shlex
 | 
			
		||||
import sys
 | 
			
		||||
 | 
			
		||||
import libvirt
 | 
			
		||||
 | 
			
		||||
from compute import __version__
 | 
			
		||||
from compute.exceptions import (
 | 
			
		||||
    ComputeServiceError,
 | 
			
		||||
    GuestAgentTimeoutExceededError,
 | 
			
		||||
)
 | 
			
		||||
from compute.instance import GuestAgent
 | 
			
		||||
from compute.session import Session
 | 
			
		||||
 | 
			
		||||
from ._create import _create_instance
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
log = logging.getLogger(__name__)
 | 
			
		||||
log_levels = logging.getLevelNamesMapping()
 | 
			
		||||
 | 
			
		||||
env_log_level = os.getenv('CMP_LOG')
 | 
			
		||||
 | 
			
		||||
libvirt.registerErrorHandler(
 | 
			
		||||
    lambda userdata, err: None,  # noqa: ARG005
 | 
			
		||||
    ctx=None,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Table:
 | 
			
		||||
    """Minimalistic text table constructor."""
 | 
			
		||||
 | 
			
		||||
    def __init__(self, whitespace: str | None = None):
 | 
			
		||||
        """Initialise Table."""
 | 
			
		||||
        self.whitespace = whitespace or '\t'
 | 
			
		||||
        self.header = []
 | 
			
		||||
        self._rows = []
 | 
			
		||||
        self._table = ''
 | 
			
		||||
 | 
			
		||||
    def row(self, row: list) -> None:
 | 
			
		||||
        """Add table row."""
 | 
			
		||||
        self._rows.append([str(col) for col in row])
 | 
			
		||||
 | 
			
		||||
    def rows(self, rows: list[list]) -> None:
 | 
			
		||||
        """Add multiple rows."""
 | 
			
		||||
        for row in rows:
 | 
			
		||||
            self.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(
 | 
			
		||||
                (
 | 
			
		||||
                    val.ljust(width)
 | 
			
		||||
                    for val, width in zip(row, widths, strict=True)
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
            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(
 | 
			
		||||
            [
 | 
			
		||||
                instance.name,
 | 
			
		||||
                instance.status,
 | 
			
		||||
            ]
 | 
			
		||||
        )
 | 
			
		||||
    print(table)
 | 
			
		||||
    sys.exit()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _exec_guest_agent_command(
 | 
			
		||||
    session: Session, args: argparse.Namespace
 | 
			
		||||
) -> None:
 | 
			
		||||
    instance = session.get_instance(args.instance)
 | 
			
		||||
    ga = GuestAgent(instance.domain, timeout=args.timeout)
 | 
			
		||||
    arguments = args.arguments.copy()
 | 
			
		||||
    if len(arguments) > 1:
 | 
			
		||||
        arguments = [shlex.join(arguments)]
 | 
			
		||||
    if not args.no_cmd_string:
 | 
			
		||||
        arguments.insert(0, '-c')
 | 
			
		||||
    stdin = None
 | 
			
		||||
    if not sys.stdin.isatty():
 | 
			
		||||
        stdin = sys.stdin.read()
 | 
			
		||||
    try:
 | 
			
		||||
        output = ga.guest_exec(
 | 
			
		||||
            path=args.shell,
 | 
			
		||||
            args=arguments,
 | 
			
		||||
            env=args.env,
 | 
			
		||||
            stdin=stdin,
 | 
			
		||||
            capture_output=True,
 | 
			
		||||
            decode_output=True,
 | 
			
		||||
            poll=True,
 | 
			
		||||
        )
 | 
			
		||||
    except GuestAgentTimeoutExceededError as e:
 | 
			
		||||
        sys.exit(
 | 
			
		||||
            f'{e}. NOTE: command may still running in guest, '
 | 
			
		||||
            f'PID={ga.last_pid}'
 | 
			
		||||
        )
 | 
			
		||||
    if output.stderr:
 | 
			
		||||
        print(output.stderr.strip(), file=sys.stderr)
 | 
			
		||||
    if output.stdout:
 | 
			
		||||
        print(output.stdout.strip(), file=sys.stdout)
 | 
			
		||||
    sys.exit(output.exitcode)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def main(session: Session, args: argparse.Namespace) -> None:
 | 
			
		||||
    """Perform actions."""
 | 
			
		||||
    match args.command:
 | 
			
		||||
        case 'create':
 | 
			
		||||
            _create_instance(session, args)
 | 
			
		||||
        case 'exec':
 | 
			
		||||
            _exec_guest_agent_command(session, args)
 | 
			
		||||
        case 'ls':
 | 
			
		||||
            _list_instances(session)
 | 
			
		||||
        case 'start':
 | 
			
		||||
            instance = session.get_instance(args.instance)
 | 
			
		||||
            instance.start()
 | 
			
		||||
        case 'shutdown':
 | 
			
		||||
            instance = session.get_instance(args.instance)
 | 
			
		||||
            instance.shutdown(args.method)
 | 
			
		||||
        case 'reboot':
 | 
			
		||||
            instance = session.get_instance(args.instance)
 | 
			
		||||
            instance.reboot()
 | 
			
		||||
        case 'reset':
 | 
			
		||||
            instance = session.get_instance(args.instance)
 | 
			
		||||
            instance.reset()
 | 
			
		||||
        case 'status':
 | 
			
		||||
            instance = session.get_instance(args.instance)
 | 
			
		||||
            print(instance.status)
 | 
			
		||||
        case 'setvcpus':
 | 
			
		||||
            instance = session.get_instance(args.instance)
 | 
			
		||||
            instance.set_vcpus(args.nvcpus, live=True)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def cli() -> None:  # noqa: PLR0915
 | 
			
		||||
    """Parse command line arguments."""
 | 
			
		||||
    root = argparse.ArgumentParser(
 | 
			
		||||
        prog='compute',
 | 
			
		||||
        description='manage compute instances and storage volumes.',
 | 
			
		||||
        formatter_class=argparse.RawTextHelpFormatter,
 | 
			
		||||
    )
 | 
			
		||||
    root.add_argument(
 | 
			
		||||
        '-v',
 | 
			
		||||
        '--verbose',
 | 
			
		||||
        action='store_true',
 | 
			
		||||
        default=False,
 | 
			
		||||
        help='enable verbose mode',
 | 
			
		||||
    )
 | 
			
		||||
    root.add_argument(
 | 
			
		||||
        '-c',
 | 
			
		||||
        '--connect',
 | 
			
		||||
        metavar='URI',
 | 
			
		||||
        default='qemu:///system',
 | 
			
		||||
        help='libvirt connection URI',
 | 
			
		||||
    )
 | 
			
		||||
    root.add_argument(
 | 
			
		||||
        '-l',
 | 
			
		||||
        '--log-level',
 | 
			
		||||
        metavar='LEVEL',
 | 
			
		||||
        choices=log_levels,
 | 
			
		||||
        help='log level [envvar: CMP_LOG]',
 | 
			
		||||
    )
 | 
			
		||||
    root.add_argument(
 | 
			
		||||
        '-V',
 | 
			
		||||
        '--version',
 | 
			
		||||
        action='version',
 | 
			
		||||
        version=__version__,
 | 
			
		||||
    )
 | 
			
		||||
    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.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(
 | 
			
		||||
        'exec',
 | 
			
		||||
        help='execute command in guest via guest agent',
 | 
			
		||||
        description=(
 | 
			
		||||
            'NOTE: any argument after instance name will be passed into '
 | 
			
		||||
            'guest as shell command.'
 | 
			
		||||
        ),
 | 
			
		||||
    )
 | 
			
		||||
    execute.add_argument('instance')
 | 
			
		||||
    execute.add_argument('arguments', nargs=argparse.REMAINDER)
 | 
			
		||||
    execute.add_argument(
 | 
			
		||||
        '-t',
 | 
			
		||||
        '--timeout',
 | 
			
		||||
        type=int,
 | 
			
		||||
        default=60,
 | 
			
		||||
        help=(
 | 
			
		||||
            'waiting time in seconds for a command to be executed '
 | 
			
		||||
            'in guest, 60 sec by default'
 | 
			
		||||
        ),
 | 
			
		||||
    )
 | 
			
		||||
    execute.add_argument(
 | 
			
		||||
        '-s',
 | 
			
		||||
        '--shell',
 | 
			
		||||
        default='/bin/sh',
 | 
			
		||||
        help='path to executable in guest, /bin/sh by default',
 | 
			
		||||
    )
 | 
			
		||||
    execute.add_argument(
 | 
			
		||||
        '-e',
 | 
			
		||||
        '--env',
 | 
			
		||||
        type=str,
 | 
			
		||||
        nargs='?',
 | 
			
		||||
        action='append',
 | 
			
		||||
        help='environment variables to pass to executable in guest',
 | 
			
		||||
    )
 | 
			
		||||
    execute.add_argument(
 | 
			
		||||
        '-n',
 | 
			
		||||
        '--no-cmd-string',
 | 
			
		||||
        action='store_true',
 | 
			
		||||
        default=False,
 | 
			
		||||
        help=(
 | 
			
		||||
            "do not append '-c' option to arguments list, suitable "
 | 
			
		||||
            'for non-shell executables and other specific cases.'
 | 
			
		||||
        ),
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    # ls subcommand
 | 
			
		||||
    listall = subparsers.add_parser('ls', help='list instances')
 | 
			
		||||
    listall.add_argument(
 | 
			
		||||
        '-a',
 | 
			
		||||
        '--all',
 | 
			
		||||
        action='store_true',
 | 
			
		||||
        default=False,
 | 
			
		||||
        help='list all instances including inactive',
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    # start subcommand
 | 
			
		||||
    start = subparsers.add_parser('start', help='start instance')
 | 
			
		||||
    start.add_argument('instance')
 | 
			
		||||
 | 
			
		||||
    # 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',
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    # reboot subcommand
 | 
			
		||||
    reboot = subparsers.add_parser('reboot', help='reboot instance')
 | 
			
		||||
    reboot.add_argument('instance')
 | 
			
		||||
 | 
			
		||||
    # reset subcommand
 | 
			
		||||
    reset = subparsers.add_parser('reset', help='reset instance')
 | 
			
		||||
    reset.add_argument('instance')
 | 
			
		||||
 | 
			
		||||
    # status subcommand
 | 
			
		||||
    status = subparsers.add_parser('status', help='display instance status')
 | 
			
		||||
    status.add_argument('instance')
 | 
			
		||||
 | 
			
		||||
    # setvcpus subcommand
 | 
			
		||||
    setvcpus = subparsers.add_parser('setvcpus', help='set vCPU number')
 | 
			
		||||
    setvcpus.add_argument('instance')
 | 
			
		||||
    setvcpus.add_argument('nvcpus', type=int)
 | 
			
		||||
 | 
			
		||||
    # Run parser
 | 
			
		||||
    args = root.parse_args()
 | 
			
		||||
    if args.command is None:
 | 
			
		||||
        root.print_help()
 | 
			
		||||
        sys.exit()
 | 
			
		||||
 | 
			
		||||
    # Set logging level
 | 
			
		||||
    log_level = args.log_level or env_log_level
 | 
			
		||||
    if log_level in log_levels:
 | 
			
		||||
        logging.basicConfig(level=log_levels[log_level])
 | 
			
		||||
 | 
			
		||||
    # Perform actions
 | 
			
		||||
    try:
 | 
			
		||||
        with Session(args.connect) as session:
 | 
			
		||||
            main(session, args)
 | 
			
		||||
    except ComputeServiceError as e:
 | 
			
		||||
        sys.exit(f'error: {e}')
 | 
			
		||||
    except (KeyboardInterrupt, SystemExit):
 | 
			
		||||
        sys.exit()
 | 
			
		||||
    except Exception as e:  # noqa: BLE001
 | 
			
		||||
        sys.exit(f'unexpected error {type(e)}: {e}')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
if __name__ == '__main__':
 | 
			
		||||
    cli()
 | 
			
		||||
		Reference in New Issue
	
	Block a user