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:
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()