Commit
+162 -0 +/-5 browse
1 | diff --git a/.gitignore b/.gitignore |
2 | new file mode 100644 |
3 | index 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 |
11 | new file mode 100644 |
12 | index 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 |
35 | new file mode 100644 |
36 | index 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 |
54 | new file mode 100755 |
55 | index 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 |
176 | new file mode 100644 |
177 | index 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 |