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