aetherscale

[unmaintained] code for a cloud provider tutorial
Log | Files | Refs | README | LICENSE

commit a847be3343a32e8886de2cae2e7f4c48282d08c0
parent 28c32b9a1e483048159a9b9015b68abf0132de8c
Author: Stefan Koch <programming@stefan-koch.name>
Date:   Sun,  6 Dec 2020 12:22:37 +0100

distinguish between creation, start, stop and deletion of VM

Diffstat:
Maetherscale/client.py | 28++++++++++++++++++----------
Maetherscale/computing.py | 219++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------
Maetherscale/execution.py | 4++++
3 files changed, 177 insertions(+), 74 deletions(-)

diff --git a/aetherscale/client.py b/aetherscale/client.py @@ -3,6 +3,7 @@ import argparse import json import pika +import pika.exceptions import sys @@ -64,13 +65,21 @@ def main(): parser = argparse.ArgumentParser( description='Manage aetherscale instances') subparsers = parser.add_subparsers(dest='subparser_name') - create_vm_parser = subparsers.add_parser('start-vm') - create_vm_parser.add_argument( - '--image', help='Name of the image to start', required=True) - create_vm_parser = subparsers.add_parser('stop-vm') + + create_vm_parser = subparsers.add_parser('create-vm') create_vm_parser.add_argument( + '--image', help='Name of the image to create a VM from', required=True) + start_vm_parser = subparsers.add_parser('start-vm') + start_vm_parser.add_argument( + '--vm-id', dest='vm_id', help='ID of the VM to start', required=True) + stop_vm_parser = subparsers.add_parser('stop-vm') + stop_vm_parser.add_argument( '--vm-id', dest='vm_id', help='ID of the VM to stop', required=True) + delete_vm_parser = subparsers.add_parser('delete-vm') + delete_vm_parser.add_argument( + '--vm-id', dest='vm_id', help='ID of the VM to delete', required=True) subparsers.add_parser('list-vms') + args = parser.parse_args() if args.subparser_name == 'list-vms': @@ -78,25 +87,24 @@ def main(): data = { 'command': 'list-vms', } - elif args.subparser_name == 'start-vm': + elif args.subparser_name == 'create-vm': response_expected = True data = { - 'command': 'start-vm', + 'command': 'create-vm', 'options': { 'image': args.image, } } - elif args.subparser_name == 'stop-vm': + elif args.subparser_name in ['start-vm', 'stop-vm', 'delete-vm']: response_expected = True data = { - 'command': 'stop-vm', + 'command': args.subparser_name, 'options': { - 'kill': True, 'vm-id': args.vm_id, } } else: - print('Command does not exist', file=sys.stderr) + parser.print_usage() sys.exit(1) try: diff --git a/aetherscale/computing.py b/aetherscale/computing.py @@ -12,7 +12,7 @@ import subprocess import sys import tempfile import time -from typing import List, Optional, IO +from typing import List, Optional, Dict, Any, Callable from . import interfaces from . import execution @@ -39,12 +39,16 @@ class QemuException(Exception): pass +def user_image_path(vm_id: str) -> Path: + return USER_IMAGE_FOLDER / f'{vm_id}.qcow2' + + def create_user_image(vm_id: str, image_name: str) -> Path: base_image = BASE_IMAGE_FOLDER / f'{image_name}.qcow2' if not base_image.is_file(): raise IOError(f'Image "{image_name}" does not exist') - user_image = USER_IMAGE_FOLDER / f'{vm_id}.qcow2' + user_image = user_image_path(vm_id) create_img_result = subprocess.run([ 'qemu-img', 'create', '-f', 'qcow2', @@ -55,7 +59,7 @@ def create_user_image(vm_id: str, image_name: str) -> Path: return user_image -def list_vms() -> List[str]: +def list_vms(_: Dict[str, Any]) -> List[str]: vms = [] for proc in psutil.process_iter(['pid', 'name']): @@ -65,6 +69,116 @@ def list_vms() -> List[str]: return vms +def create_vm(options: Dict[str, Any]) -> Dict[str, str]: + vm_id = ''.join( + random.choice(string.ascii_lowercase) for _ in range(8)) + logging.info(f'Starting VM "{vm_id}"') + + try: + image_name = os.path.basename(options['image']) + except KeyError: + raise ValueError('Image not specified') + + try: + user_image = create_user_image(vm_id, image_name) + except (OSError, QemuException): + raise + + mac_addr = interfaces.create_mac_address() + logging.debug(f'Assigning MAC address "{mac_addr}" to VM "{vm_id}"') + + qemu_config = QemuStartupConfig( + vm_id=vm_id, + hda_image=user_image, + mac_addr=mac_addr, + vde_folder=Path(VDE_FOLDER)) + unit_name = systemd_unit_name_for_vm(vm_id) + create_qemu_systemd_unit(unit_name, qemu_config) + execution.start_systemd_unit(unit_name) + + logging.info(f'Started VM "{vm_id}"') + return { + 'status': 'starting', + 'vm-id': vm_id, + } + + +def start_vm(options: Dict[str, Any]) -> Dict[str, str]: + try: + vm_id = options['vm-id'] + except KeyError: + raise ValueError('VM ID not specified') + + unit_name = systemd_unit_name_for_vm(vm_id) + + if not execution.systemd_unit_exists(unit_name): + raise RuntimeError('VM does not exist') + elif execution.systemctl_is_running(unit_name): + response = { + 'status': 'starting', + 'vm-id': vm_id, + 'hint': f'VM "{vm_id}" was already started', + } + else: + execution.start_systemd_unit(unit_name) + execution.enable_systemd_unit(unit_name) + + response = { + 'status': 'starting', + 'vm-id': vm_id, + } + + return response + + +def stop_vm(options: Dict[str, Any]) -> Dict[str, str]: + try: + vm_id = options['vm-id'] + except KeyError: + raise ValueError('VM ID not specified') + + unit_name = systemd_unit_name_for_vm(vm_id) + + if not execution.systemd_unit_exists(unit_name): + raise RuntimeError('VM does not exist') + elif not execution.systemctl_is_running(unit_name): + response = { + 'status': 'killed', + 'vm-id': vm_id, + 'hint': f'VM "{vm_id}" was not running', + } + else: + execution.disable_systemd_unit(unit_name) + execution.stop_systemd_unit(unit_name) + + response = { + 'status': 'killed', + 'vm-id': vm_id, + } + + return response + + +def delete_vm(options: Dict[str, Any]) -> Dict[str, str]: + try: + vm_id = options['vm-id'] + except KeyError: + raise ValueError('VM ID not specified') + + stop_vm(options) + + unit_name = systemd_unit_name_for_vm(vm_id) + user_image = user_image_path(vm_id) + + execution.delete_systemd_unit(unit_name) + user_image.unlink() + + return { + 'status': 'deleted', + 'vm-id': vm_id, + } + + def get_process_for_vm(vm_id: str) -> Optional[psutil.Process]: for proc in psutil.process_iter(['name']): if proc.name() == vm_id: @@ -115,84 +229,61 @@ def create_qemu_systemd_unit( def callback(ch, method, properties, body): + command_fn: Dict[str, Callable[[Dict[str, Any]], Dict[str, Any]]] = { + 'list-vms': list_vms, + 'create-vm': create_vm, + 'start-vm': start_vm, + 'stop-vm': stop_vm, + 'delete-vm': delete_vm, + } + message = body.decode('utf-8') - print('Received message: ' + message) + logging.debug('Received message: ' + message) data = json.loads(message) - response = None - if 'command' not in data: + try: + command = data['command'] + except KeyError: + logging.error('No "command" specified in message') return - elif data['command'] == 'list-vms': - response = list_vms() - elif data['command'] == 'stop-vm': - try: - vm_id = data['options']['vm-id'] - except KeyError: - print('VM ID not specified', file=sys.stderr) - return - - unit_name = systemd_unit_name_for_vm(vm_id) - is_running = execution.systemctl_is_running(unit_name) - - if is_running: - execution.disable_systemd_unit(unit_name) - execution.stop_systemd_unit(unit_name) - - response = { - 'status': 'killed', - 'vm-id': vm_id, - } - else: - response = { + + try: + fn = command_fn[command] + except KeyError: + logging.error(f'Invalid command "{command}" specified') + return + + options = data.get('options', {}) + try: + response = fn(options) + # if a function wants to return a response + # set its execution status to success + resp_message = { + 'execution-info': { + 'status': 'success' + }, + 'response': response, + } + except Exception as e: + resp_message = { + 'execution-info': { 'status': 'error', - 'reason': f'VM "{vm_id}" does not exist', + # TODO: Only ouput message if it is an exception generated by us + 'reason': str(e), } - elif data['command'] == 'start-vm': - vm_id = ''.join( - random.choice(string.ascii_lowercase) for i in range(8)) - print(f'Starting VM "{vm_id}"') - - try: - image_name = os.path.basename(data['options']['image']) - except KeyError: - print('Image not specified', file=sys.stderr) - return - - try: - user_image = create_user_image(vm_id, image_name) - except (OSError, QemuException) as e: - print(str(e), file=sys.stderr) - return - - mac_addr = interfaces.create_mac_address() - print(f'Assigning MAC address "{mac_addr}" to VM "{vm_id}"') - - qemu_config = QemuStartupConfig( - vm_id=vm_id, - hda_image=user_image, - mac_addr=mac_addr, - vde_folder=Path(VDE_FOLDER)) - unit_name = systemd_unit_name_for_vm(vm_id) - create_qemu_systemd_unit(unit_name, qemu_config) - execution.start_systemd_unit(unit_name) - - print(f'Started VM "{vm_id}"') - response = { - 'status': 'starting', - 'vm-id': vm_id, } ch.basic_ack(delivery_tag=method.delivery_tag) - if response is not None and properties.reply_to: + if properties.reply_to: ch.basic_publish( exchange='', routing_key=properties.reply_to, properties=pika.BasicProperties( correlation_id=properties.correlation_id ), - body=json.dumps(response)) + body=json.dumps(resp_message)) def run(): diff --git a/aetherscale/execution.py b/aetherscale/execution.py @@ -66,3 +66,7 @@ def systemctl_is_running(unit_name: str) -> bool: result = subprocess.run([ 'systemctl', '--user', 'is-active', '--quiet', unit_name]) return result.returncode == 0 + + +def systemd_unit_exists(unit_name: str) -> bool: + return systemd_unit_path(unit_name).is_file()