aetherscale

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

commit 57a97efaa00b03312acd388f105b9d882e7a70f0
parent a847be3343a32e8886de2cae2e7f4c48282d08c0
Author: Stefan Koch <programming@stefan-koch.name>
Date:   Mon,  7 Dec 2020 22:17:52 +0100

implement graceful shutdown through qemu monitor

Diffstat:
Maetherscale/client.py | 14+++++++++++++-
Maetherscale/computing.py | 28++++++++++++++++++++++++----
Aaetherscale/qemu.py | 28++++++++++++++++++++++++++++
3 files changed, 65 insertions(+), 5 deletions(-)

diff --git a/aetherscale/client.py b/aetherscale/client.py @@ -75,6 +75,9 @@ def main(): 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) + stop_vm_parser.add_argument( + '--kill', dest='kill', action='store_true', default=False, + help='Kill the VM immediately, no graceful shutdown') 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) @@ -95,7 +98,16 @@ def main(): 'image': args.image, } } - elif args.subparser_name in ['start-vm', 'stop-vm', 'delete-vm']: + elif args.subparser_name == 'stop-vm': + response_expected = True + data = { + 'command': args.subparser_name, + 'options': { + 'vm-id': args.vm_id, + 'kill': args.kill, + } + } + elif args.subparser_name in ['start-vm', 'delete-vm']: response_expected = True data = { 'command': args.subparser_name, diff --git a/aetherscale/computing.py b/aetherscale/computing.py @@ -16,6 +16,7 @@ from typing import List, Optional, Dict, Any, Callable from . import interfaces from . import execution +from . import qemu # TODO: Since this is not a command line interface file anymore, switch to # logging from print @@ -43,6 +44,10 @@ def user_image_path(vm_id: str) -> Path: return USER_IMAGE_FOLDER / f'{vm_id}.qcow2' +def qemu_socket_monitor(vm_id: str) -> Path: + return Path(f'/tmp/aetherscale-qmp-{vm_id}.sock') + + 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(): @@ -137,22 +142,31 @@ def stop_vm(options: Dict[str, Any]) -> Dict[str, str]: except KeyError: raise ValueError('VM ID not specified') + kill_flag = bool(options.get('kill', False)) + stop_status = 'killed' if kill_flag else 'stopped' + 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', + 'status': stop_status, '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) + + if kill_flag: + execution.stop_systemd_unit(unit_name) + else: + qemu_socket = qemu_socket_monitor(vm_id) + qm = qemu.QemuMonitor(qemu_socket) + qm.execute('system_powerdown') response = { - 'status': 'killed', + 'status': stop_status, 'vm-id': vm_id, } @@ -165,6 +179,8 @@ def delete_vm(options: Dict[str, Any]) -> Dict[str, str]: except KeyError: raise ValueError('VM ID not specified') + # force kill stop when a VM is deleted + options['kill'] = True stop_vm(options) unit_name = systemd_unit_name_for_vm(vm_id) @@ -209,10 +225,14 @@ def create_qemu_systemd_unit( name_quoted = shlex.quote( f'qemu-vm-{qemu_config.vm_id},process=vm-{qemu_config.vm_id}') + qemu_monitor_path = qemu_socket_monitor(qemu_config.vm_id) + socket_quoted = shlex.quote(f'unix:{qemu_monitor_path},server,nowait') + command = f'qemu-system-x86_64 -m 4096 -accel kvm -hda {hda_quoted} ' \ f'-device {device_quoted} -netdev {netdev_quoted} ' \ f'-name {name_quoted} ' \ - '-nographic' + '-nographic ' \ + f'-qmp {socket_quoted}' with tempfile.NamedTemporaryFile(mode='w+t', delete=False) as f: f.write('[Unit]\n') diff --git a/aetherscale/qemu.py b/aetherscale/qemu.py @@ -0,0 +1,28 @@ +import json +from pathlib import Path +import socket +from typing import Any + + +class QemuMonitor: + def __init__(self, socket_file: Path): + # TODO: It's not really nice that we use the file object + # to read lines and the socket to write + self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + self.sock.connect(str(socket_file)) + self.f = self.sock.makefile('rw') + + # Initialize connection immediately + self._initialize() + + def execute(self, command: str) -> Any: + json_line = json.dumps({'execute': command}) + '\n' + self.sock.sendall(json_line.encode('utf-8')) + return json.loads(self.f.readline()) + + def _initialize(self): + # Read the capabilities + self.f.readline() + + # Acknowledge the QMP capability negotiation + self.execute('qmp_capabilities')