Author: Kevin Schoon [me@kevinschoon.com]
Hash: 88fe11a226112c58230b719fa3de7caa6c2ac60f
Timestamp: Sun, 05 Mar 2023 00:03:00 +0000 (1 year ago)

+162 -0 +/-5 browse
init
1diff --git a/.gitignore b/.gitignore
2new file mode 100644
3index 0000000..4b0ab6f
4--- /dev/null
5+++ b/.gitignore
6 @@ -0,0 +1,3 @@
7+ pkg
8+ src
9+ *.tar.zst
10 diff --git a/PKGBUILD b/PKGBUILD
11new file mode 100644
12index 0000000..b41f081
13--- /dev/null
14+++ b/PKGBUILD
15 @@ -0,0 +1,18 @@
16+ pkgname=qemu-shim
17+ pkgver=0.0.2
18+ pkgrel=1
19+ pkgdesc="tiny qemu helper script"
20+ arch=(x86_64)
21+ license=('MIT')
22+ md5sums=()
23+ validpgpkeys=()
24+
25+ build() {
26+ cp ../qemu-shim .
27+ cp ../qemu@.service .
28+ }
29+
30+ package() {
31+ install -Dm755 "${pkgname}" -t "${pkgdir}"/usr/bin/
32+ install -Dm644 "qemu@.service" -t "${pkgdir}"/usr/lib/systemd/user/
33+ }
34 diff --git a/README.md b/README.md
35new file mode 100644
36index 0000000..b2a7e3c
37--- /dev/null
38+++ b/README.md
39 @@ -0,0 +1,13 @@
40+ # qemu-shim
41+
42+ A tiny script for running VMs with QEMU with graceful shutdown in a way that
43+ is compatible with systemd. This script makes a few assumptions about the
44+ arguments it is given:
45+
46+ * The args are a valid QEMU command
47+ * They contain the -name flag
48+ * They contain a -monitor flag with a unix socket specified. TCP/Telnet is not
49+ * yet supported by the script.
50+ * The guest OS supports ACPI shutdown signals
51+
52+ ## Example
53 diff --git a/qemu-shim b/qemu-shim
54new file mode 100755
55index 0000000..3620bf5
56--- /dev/null
57+++ b/qemu-shim
58 @@ -0,0 +1,116 @@
59+ #!/usr/bin/env python
60+ # A tiny script for running VMs with QEMU with graceful shutdown in a way that
61+ # is compatible with systemd. This script makes a few assumptions about the
62+ # arguments it is given:
63+ #
64+ # The args are a valid QEMU command
65+ # They contain the -name flag
66+ # They contain a -monitor flag with a unix socket specified. TCP/Telnet is not
67+ # yet supported by the script.
68+ # The guest OS supports ACPI shutdown signals
69+
70+ import os
71+ import re
72+ import signal
73+ import socket
74+ import subprocess
75+ import sys
76+ import time
77+
78+
79+ _shutdown_timeout = os.getenv("QEMU_SHIM_TIMEOUT")
80+ if _shutdown_timeout:
81+ _shutdown_timeout = int(_shutdown_timeout)
82+ else:
83+ _shutdown_timeout = 15
84+
85+
86+ def _read_all(client: socket.socket):
87+ while True:
88+ try:
89+ client.recv(1024)
90+ except TimeoutError:
91+ break
92+
93+ def _get_arg_by_name(args, name):
94+ for i, arg in enumerate(args):
95+ if arg == name:
96+ try:
97+ return args[i+1]
98+ except IndexError:
99+ pass
100+
101+ def _get_first_group(match):
102+ if match and len(match.groups()) > 0:
103+ return match.group(1)
104+
105+ def _get_name(args: list[str]):
106+ return _get_arg_by_name(args, "-name")
107+
108+ def _get_spice_path(args: list[str]):
109+ spice_arg = _get_arg_by_name(args, "-spice")
110+ if spice_arg:
111+ return _get_first_group(
112+ re.compile("^.*addr=([\w\/\-\.]*)").match(spice_arg))
113+
114+ def _get_monitor_path(name):
115+ monitor_arg = _get_arg_by_name(args, "-monitor")
116+ if monitor_arg:
117+ return _get_first_group(
118+ re.compile("^unix:([\w\/\-\.]*)").match(monitor_arg))
119+
120+ def run_vm(args):
121+
122+ name = _get_name(args)
123+ monitor_path = _get_monitor_path(args)
124+ spice_path = _get_spice_path(args)
125+
126+ if name is None:
127+ print("-name flag was not specified in qemu command")
128+ sys.exit(1)
129+ if monitor_path is None:
130+ print("-monitor flag was not specified in qemu command")
131+ sys.exit(1)
132+
133+ print(f"launching QEMU virtual machine {name}")
134+
135+ proc = subprocess.Popen(args=args, stdout=sys.stderr, stderr=sys.stderr,
136+ preexec_fn=os.setpgrp)
137+
138+ while True:
139+ try:
140+ os.stat(monitor_path)
141+ break
142+ except FileNotFoundError:
143+ time.sleep(1)
144+
145+ def cleanup(signum, frame):
146+ print(f"caught signal, shutting down via QMP @ {monitor_path}")
147+ with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as client:
148+ client.connect(monitor_path)
149+ client.setblocking(True)
150+ client.settimeout(1)
151+ _read_all(client)
152+ client.send(b"system_powerdown\n")
153+ _read_all(client)
154+ client.close()
155+
156+ print(f"waiting {_shutdown_timeout}s")
157+ time.sleep(_shutdown_timeout)
158+ print("timeout exceeded, signaling QEMU process")
159+ proc.send_signal(signal.SIGHUP)
160+
161+ signal.signal(signal.SIGINT, cleanup)
162+ signal.signal(signal.SIGTERM, cleanup)
163+
164+ retcode = proc.wait()
165+ if spice_path:
166+ os.remove(spice_path)
167+
168+ print(f"QEMU process ended with code {retcode}")
169+ sys.exit(retcode)
170+
171+
172+ if __name__ == "__main__":
173+ args = sys.argv[1:]
174+ run_vm(args)
175 diff --git a/qemu@.service b/qemu@.service
176new file mode 100644
177index 0000000..346893c
178--- /dev/null
179+++ b/qemu@.service
180 @@ -0,0 +1,12 @@
181+ [Unit]
182+ Description=QEMU Virtual Machine (qemu-shim)
183+
184+ [Service]
185+ KillMode=process
186+ Environment="QEMU_SHIM_TIMEOUT=10"
187+ ExecStart=/usr/bin/qemu-shim \
188+ qemu-system-x86_64 -name %N \
189+ -m 256M -nographic -monitor "unix:/tmp/test.sock,server,nowait"
190+
191+ [Install]
192+ WantedBy=multi-user.target