maqet 0.0.1.4__py3-none-any.whl → 0.0.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- maqet/__init__.py +50 -6
- maqet/__main__.py +96 -0
- maqet/__version__.py +3 -0
- maqet/api/__init__.py +35 -0
- maqet/api/decorators.py +184 -0
- maqet/api/metadata.py +147 -0
- maqet/api/registry.py +182 -0
- maqet/cli.py +71 -0
- maqet/config/__init__.py +26 -0
- maqet/config/merger.py +237 -0
- maqet/config/parser.py +198 -0
- maqet/config/validators.py +519 -0
- maqet/config_handlers.py +684 -0
- maqet/constants.py +200 -0
- maqet/exceptions.py +226 -0
- maqet/formatters.py +294 -0
- maqet/generators/__init__.py +12 -0
- maqet/generators/base_generator.py +101 -0
- maqet/generators/cli_generator.py +635 -0
- maqet/generators/python_generator.py +247 -0
- maqet/generators/rest_generator.py +58 -0
- maqet/handlers/__init__.py +12 -0
- maqet/handlers/base.py +108 -0
- maqet/handlers/init.py +147 -0
- maqet/handlers/stage.py +196 -0
- maqet/ipc/__init__.py +29 -0
- maqet/ipc/retry.py +265 -0
- maqet/ipc/runner_client.py +285 -0
- maqet/ipc/unix_socket_server.py +239 -0
- maqet/logger.py +160 -55
- maqet/machine.py +884 -0
- maqet/managers/__init__.py +7 -0
- maqet/managers/qmp_manager.py +333 -0
- maqet/managers/snapshot_coordinator.py +327 -0
- maqet/managers/vm_manager.py +683 -0
- maqet/maqet.py +1120 -0
- maqet/os_interactions.py +46 -0
- maqet/process_spawner.py +395 -0
- maqet/qemu_args.py +76 -0
- maqet/qmp/__init__.py +10 -0
- maqet/qmp/commands.py +92 -0
- maqet/qmp/keyboard.py +311 -0
- maqet/qmp/qmp.py +17 -0
- maqet/snapshot.py +473 -0
- maqet/state.py +958 -0
- maqet/storage.py +702 -162
- maqet/validation/__init__.py +9 -0
- maqet/validation/config_validator.py +170 -0
- maqet/vm_runner.py +523 -0
- maqet-0.0.5.dist-info/METADATA +237 -0
- maqet-0.0.5.dist-info/RECORD +55 -0
- {maqet-0.0.1.4.dist-info → maqet-0.0.5.dist-info}/WHEEL +1 -1
- maqet-0.0.5.dist-info/entry_points.txt +2 -0
- maqet-0.0.5.dist-info/licenses/LICENSE +21 -0
- {maqet-0.0.1.4.dist-info → maqet-0.0.5.dist-info}/top_level.txt +0 -1
- maqet/core.py +0 -411
- maqet/functions.py +0 -104
- maqet-0.0.1.4.dist-info/METADATA +0 -6
- maqet-0.0.1.4.dist-info/RECORD +0 -33
- qemu/machine/__init__.py +0 -36
- qemu/machine/console_socket.py +0 -142
- qemu/machine/machine.py +0 -954
- qemu/machine/py.typed +0 -0
- qemu/machine/qtest.py +0 -191
- qemu/qmp/__init__.py +0 -59
- qemu/qmp/error.py +0 -50
- qemu/qmp/events.py +0 -717
- qemu/qmp/legacy.py +0 -319
- qemu/qmp/message.py +0 -209
- qemu/qmp/models.py +0 -146
- qemu/qmp/protocol.py +0 -1057
- qemu/qmp/py.typed +0 -0
- qemu/qmp/qmp_client.py +0 -655
- qemu/qmp/qmp_shell.py +0 -618
- qemu/qmp/qmp_tui.py +0 -655
- qemu/qmp/util.py +0 -219
- qemu/utils/__init__.py +0 -162
- qemu/utils/accel.py +0 -84
- qemu/utils/py.typed +0 -0
- qemu/utils/qemu_ga_client.py +0 -323
- qemu/utils/qom.py +0 -273
- qemu/utils/qom_common.py +0 -175
- qemu/utils/qom_fuse.py +0 -207
maqet/os_interactions.py
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
import subprocess
|
2
|
+
import sys
|
3
|
+
import os
|
4
|
+
import signal
|
5
|
+
|
6
|
+
from benedict import benedict
|
7
|
+
|
8
|
+
from maqet.logger import LEVELS, LOG
|
9
|
+
|
10
|
+
|
11
|
+
|
12
|
+
def shell_command(command: str, verbose: bool = True) -> benedict:
|
13
|
+
"""
|
14
|
+
Run shell command and return dictionary of stdout, stderr, returncode
|
15
|
+
Return is benedict object, members can be accessed as fields
|
16
|
+
"""
|
17
|
+
command = " ".join(command.split())
|
18
|
+
|
19
|
+
proc = subprocess.Popen(command, shell=True,
|
20
|
+
stdout=subprocess.PIPE,
|
21
|
+
stderr=subprocess.PIPE,
|
22
|
+
preexec_fn=os.setsid)
|
23
|
+
try:
|
24
|
+
out = proc.communicate()
|
25
|
+
except KeyboardInterrupt:
|
26
|
+
os.killpg(os.getpgid(proc.pid), signal.SIGTERM)
|
27
|
+
proc.wait()
|
28
|
+
raise
|
29
|
+
|
30
|
+
output = benedict({
|
31
|
+
"stdout": out[0].decode('ascii').strip("\n"),
|
32
|
+
"stderr": out[1].decode('ascii').strip("\n"),
|
33
|
+
"rc": proc.returncode
|
34
|
+
})
|
35
|
+
|
36
|
+
message = f"command `{command}` returned {output}"
|
37
|
+
|
38
|
+
if verbose:
|
39
|
+
level = LEVELS['DEBUG']
|
40
|
+
if output.stderr != '':
|
41
|
+
level = LEVELS['WARNING']
|
42
|
+
if output.rc != 0:
|
43
|
+
level = LEVELS['ERROR']
|
44
|
+
|
45
|
+
LOG.log(level, message)
|
46
|
+
return output
|
maqet/process_spawner.py
ADDED
@@ -0,0 +1,395 @@
|
|
1
|
+
"""
|
2
|
+
Process Spawner
|
3
|
+
|
4
|
+
Utilities for spawning and managing detached VM runner processes.
|
5
|
+
Used by CLI to create independent VM runner processes that survive parent exit.
|
6
|
+
|
7
|
+
Architecture:
|
8
|
+
- Each VM gets its own persistent Python process (VM runner)
|
9
|
+
- Spawned processes are detached (new session, own process group)
|
10
|
+
- Processes redirect stdout/stderr to log files
|
11
|
+
- Socket-based readiness checking with timeout
|
12
|
+
- Health checks using psutil or fallback methods
|
13
|
+
|
14
|
+
Key Functions:
|
15
|
+
- spawn_vm_runner(): Create detached VM runner process
|
16
|
+
- wait_for_vm_ready(): Wait for VM runner to be ready (socket available)
|
17
|
+
- get_socket_path(): Get Unix socket path for VM
|
18
|
+
- get_log_path(): Get log file path for VM runner
|
19
|
+
- is_runner_alive(): Check if runner process is alive
|
20
|
+
"""
|
21
|
+
|
22
|
+
import os
|
23
|
+
import subprocess
|
24
|
+
import sys
|
25
|
+
import time
|
26
|
+
from pathlib import Path
|
27
|
+
from typing import Optional
|
28
|
+
|
29
|
+
from .constants import Intervals, Timeouts
|
30
|
+
from .exceptions import ProcessNotFoundError, RunnerSpawnError
|
31
|
+
from .logger import LOG
|
32
|
+
|
33
|
+
# Optional dependency - imported inline with fallback
|
34
|
+
try:
|
35
|
+
import psutil
|
36
|
+
|
37
|
+
PSUTIL_AVAILABLE = True
|
38
|
+
except ImportError:
|
39
|
+
PSUTIL_AVAILABLE = False
|
40
|
+
|
41
|
+
# Legacy exception alias (backward compatibility)
|
42
|
+
ProcessSpawnerError = RunnerSpawnError
|
43
|
+
|
44
|
+
|
45
|
+
def spawn_vm_runner(
|
46
|
+
vm_id: str,
|
47
|
+
db_path: Optional[Path] = None,
|
48
|
+
timeout: int = Timeouts.PROCESS_SPAWN
|
49
|
+
) -> int:
|
50
|
+
"""
|
51
|
+
Spawn a detached VM runner process.
|
52
|
+
|
53
|
+
The spawned process:
|
54
|
+
- Runs in background (detached from CLI)
|
55
|
+
- Has its own process group (start_new_session=True)
|
56
|
+
- Redirects stdout/stderr to log files
|
57
|
+
- Survives parent CLI process exit
|
58
|
+
- Returns immediately (non-blocking)
|
59
|
+
|
60
|
+
Args:
|
61
|
+
vm_id: VM identifier
|
62
|
+
db_path: Optional path to database (for testing)
|
63
|
+
timeout: Maximum time to wait for process to start (seconds)
|
64
|
+
|
65
|
+
Returns:
|
66
|
+
runner_pid: PID of spawned runner process
|
67
|
+
|
68
|
+
Raises:
|
69
|
+
ProcessSpawnerError: If spawn fails or process dies immediately
|
70
|
+
|
71
|
+
Example:
|
72
|
+
runner_pid = spawn_vm_runner("vm1")
|
73
|
+
# Returns: PID of spawned runner process
|
74
|
+
"""
|
75
|
+
# Get Python interpreter path (same interpreter as CLI)
|
76
|
+
python_exe = sys.executable
|
77
|
+
|
78
|
+
# Build command: python3 -m maqet.vm_runner <vm_id> [db_path]
|
79
|
+
cmd = [python_exe, "-m", "maqet.vm_runner", vm_id]
|
80
|
+
if db_path:
|
81
|
+
cmd.append(str(db_path))
|
82
|
+
|
83
|
+
# Get log file path for stdout/stderr
|
84
|
+
log_path = get_log_path(vm_id)
|
85
|
+
|
86
|
+
LOG.debug(f"Spawning VM runner for {vm_id}: {' '.join(cmd)}")
|
87
|
+
LOG.debug(f"Log output: {log_path}")
|
88
|
+
|
89
|
+
try:
|
90
|
+
# Open log file for output
|
91
|
+
log_file = open(log_path, "w")
|
92
|
+
|
93
|
+
# Spawn detached process
|
94
|
+
process = subprocess.Popen(
|
95
|
+
cmd,
|
96
|
+
start_new_session=True, # Detach from parent process group
|
97
|
+
stdin=subprocess.DEVNULL, # No stdin (non-interactive)
|
98
|
+
stdout=log_file, # Redirect stdout to log
|
99
|
+
stderr=subprocess.STDOUT, # Redirect stderr to stdout (combined log)
|
100
|
+
close_fds=True, # Close all file descriptors
|
101
|
+
)
|
102
|
+
|
103
|
+
runner_pid = process.pid
|
104
|
+
LOG.info(f"VM runner spawned: PID {runner_pid}")
|
105
|
+
|
106
|
+
# Wait briefly to ensure process started successfully
|
107
|
+
time.sleep(Intervals.PROCESS_STARTUP_WAIT)
|
108
|
+
|
109
|
+
# Check if process still alive (didn't crash immediately)
|
110
|
+
if process.poll() is not None:
|
111
|
+
# Process exited immediately - read log for error
|
112
|
+
log_file.close()
|
113
|
+
try:
|
114
|
+
with open(log_path, "r") as f:
|
115
|
+
error_output = f.read().strip()
|
116
|
+
except Exception:
|
117
|
+
error_output = "(could not read log)"
|
118
|
+
|
119
|
+
raise RunnerSpawnError(
|
120
|
+
f"VM runner '{vm_id}' failed to start (exit code {process.poll()}). "
|
121
|
+
f"Error: {error_output}. Check log at: {log_path}"
|
122
|
+
)
|
123
|
+
|
124
|
+
# Close log file handle (process has its own handle now)
|
125
|
+
log_file.close()
|
126
|
+
|
127
|
+
return runner_pid
|
128
|
+
|
129
|
+
except RunnerSpawnError:
|
130
|
+
# Re-raise RunnerSpawnError as-is (don't wrap it again)
|
131
|
+
raise
|
132
|
+
except FileNotFoundError:
|
133
|
+
raise RunnerSpawnError(
|
134
|
+
f"Python interpreter not found: {python_exe}. "
|
135
|
+
f"This should never happen - check sys.executable."
|
136
|
+
)
|
137
|
+
except PermissionError as e:
|
138
|
+
raise RunnerSpawnError(
|
139
|
+
f"Permission denied when spawning VM runner '{vm_id}': {e}. "
|
140
|
+
f"Check file permissions for log directory."
|
141
|
+
)
|
142
|
+
except Exception as e:
|
143
|
+
raise RunnerSpawnError(
|
144
|
+
f"Failed to spawn VM runner '{vm_id}': {e}"
|
145
|
+
)
|
146
|
+
|
147
|
+
|
148
|
+
def wait_for_vm_ready(
|
149
|
+
vm_id: str,
|
150
|
+
socket_path: Optional[Path] = None,
|
151
|
+
timeout: int = Timeouts.VM_START
|
152
|
+
) -> bool:
|
153
|
+
"""
|
154
|
+
Wait for VM runner to be ready (socket available and connectable).
|
155
|
+
|
156
|
+
Polls for socket existence and connectivity with exponential backoff.
|
157
|
+
Uses direct socket connection to verify socket is functional.
|
158
|
+
|
159
|
+
Args:
|
160
|
+
vm_id: VM identifier
|
161
|
+
socket_path: Optional socket path (auto-detected if not provided)
|
162
|
+
timeout: Maximum wait time in seconds
|
163
|
+
|
164
|
+
Returns:
|
165
|
+
True if ready within timeout, False if timeout
|
166
|
+
|
167
|
+
Implementation Notes:
|
168
|
+
- Polls every 0.1s initially, increasing to 0.5s
|
169
|
+
- Checks socket exists AND is connectable
|
170
|
+
- Uses direct socket connection for connectivity check
|
171
|
+
- Logs progress at DEBUG level
|
172
|
+
|
173
|
+
Example:
|
174
|
+
socket_path = get_socket_path("vm1")
|
175
|
+
ready = wait_for_vm_ready("vm1", socket_path, timeout=10)
|
176
|
+
# Returns: True if ready, False if timeout
|
177
|
+
"""
|
178
|
+
if socket_path is None:
|
179
|
+
socket_path = get_socket_path(vm_id)
|
180
|
+
|
181
|
+
start_time = time.time()
|
182
|
+
poll_interval = 0.1 # Start with 100ms
|
183
|
+
max_poll_interval = 0.5 # Cap at 500ms
|
184
|
+
|
185
|
+
LOG.debug(f"Waiting for VM runner to be ready: {socket_path}")
|
186
|
+
|
187
|
+
while time.time() - start_time < timeout:
|
188
|
+
# Check if socket exists
|
189
|
+
if socket_path.exists():
|
190
|
+
# Socket exists, try to connect directly
|
191
|
+
try:
|
192
|
+
import socket as sock_module
|
193
|
+
import json
|
194
|
+
|
195
|
+
client_socket = sock_module.socket(sock_module.AF_UNIX, sock_module.SOCK_STREAM)
|
196
|
+
client_socket.settimeout(2.0)
|
197
|
+
client_socket.connect(str(socket_path))
|
198
|
+
|
199
|
+
# Send ping request
|
200
|
+
request = json.dumps({"method": "ping", "args": [], "kwargs": {}})
|
201
|
+
client_socket.sendall(request.encode("utf-8"))
|
202
|
+
|
203
|
+
# Receive response
|
204
|
+
response_data = client_socket.recv(1024)
|
205
|
+
response = json.loads(response_data.decode("utf-8"))
|
206
|
+
|
207
|
+
client_socket.close()
|
208
|
+
|
209
|
+
if response.get("status") == "success" and response.get("result") == "pong":
|
210
|
+
LOG.debug("VM runner ready (socket connectable)")
|
211
|
+
return True
|
212
|
+
else:
|
213
|
+
LOG.debug("Socket exists but ping failed, retrying...")
|
214
|
+
|
215
|
+
except (ConnectionRefusedError, FileNotFoundError, sock_module.timeout) as e:
|
216
|
+
LOG.debug(f"Socket exists but not ready yet: {type(e).__name__}")
|
217
|
+
except Exception as e:
|
218
|
+
LOG.debug(f"Socket check error: {e}")
|
219
|
+
|
220
|
+
# Sleep with exponential backoff
|
221
|
+
time.sleep(poll_interval)
|
222
|
+
poll_interval = min(poll_interval * 1.2, max_poll_interval)
|
223
|
+
|
224
|
+
# Log progress every 5 seconds
|
225
|
+
elapsed = time.time() - start_time
|
226
|
+
if int(elapsed) % 5 == 0 and elapsed > 0:
|
227
|
+
LOG.debug(f"Still waiting for VM runner... ({int(elapsed)}s elapsed)")
|
228
|
+
|
229
|
+
# Timeout
|
230
|
+
LOG.warning(f"Timeout waiting for VM runner after {timeout}s")
|
231
|
+
return False
|
232
|
+
|
233
|
+
|
234
|
+
def get_socket_path(vm_id: str) -> Path:
|
235
|
+
"""
|
236
|
+
Get Unix socket path for VM runner.
|
237
|
+
|
238
|
+
Socket location: XDG_RUNTIME_DIR/maqet/sockets/{vm_id}.sock
|
239
|
+
Falls back to /tmp/maqet-{uid}/sockets/ if XDG_RUNTIME_DIR not available.
|
240
|
+
|
241
|
+
This MUST match the socket path used in vm_runner.py!
|
242
|
+
|
243
|
+
Args:
|
244
|
+
vm_id: VM identifier
|
245
|
+
|
246
|
+
Returns:
|
247
|
+
Path to Unix socket
|
248
|
+
|
249
|
+
Example:
|
250
|
+
socket_path = get_socket_path("vm1")
|
251
|
+
# Returns: /run/user/1000/maqet/sockets/vm1.sock
|
252
|
+
"""
|
253
|
+
# Get runtime directory (prefer XDG_RUNTIME_DIR)
|
254
|
+
runtime_dir_base = os.environ.get("XDG_RUNTIME_DIR", f"/run/user/{os.getuid()}")
|
255
|
+
|
256
|
+
if not Path(runtime_dir_base).exists():
|
257
|
+
# Fallback to /tmp (already includes maqet-{uid})
|
258
|
+
socket_dir = Path(f"/tmp/maqet-{os.getuid()}") / "sockets"
|
259
|
+
else:
|
260
|
+
# XDG_RUNTIME_DIR exists (e.g., /run/user/1000)
|
261
|
+
socket_dir = Path(runtime_dir_base) / "maqet" / "sockets"
|
262
|
+
|
263
|
+
return socket_dir / f"{vm_id}.sock"
|
264
|
+
|
265
|
+
|
266
|
+
def get_log_path(vm_id: str) -> Path:
|
267
|
+
"""
|
268
|
+
Get log file path for VM runner.
|
269
|
+
|
270
|
+
Log location: ~/.local/share/maqet/logs/vm_{vm_id}.log
|
271
|
+
Creates parent directory if it doesn't exist.
|
272
|
+
|
273
|
+
Args:
|
274
|
+
vm_id: VM identifier
|
275
|
+
|
276
|
+
Returns:
|
277
|
+
Path to log file
|
278
|
+
|
279
|
+
Example:
|
280
|
+
log_path = get_log_path("vm1")
|
281
|
+
# Returns: /home/user/.local/share/maqet/logs/vm_vm1.log
|
282
|
+
"""
|
283
|
+
# Get XDG data directory
|
284
|
+
xdg_data_home = os.environ.get(
|
285
|
+
"XDG_DATA_HOME", os.path.expanduser("~/.local/share")
|
286
|
+
)
|
287
|
+
data_dir = Path(xdg_data_home) / "maqet"
|
288
|
+
log_dir = data_dir / "logs"
|
289
|
+
|
290
|
+
# Create directory if it doesn't exist
|
291
|
+
log_dir.mkdir(parents=True, exist_ok=True)
|
292
|
+
|
293
|
+
return log_dir / f"vm_{vm_id}.log"
|
294
|
+
|
295
|
+
|
296
|
+
def is_runner_alive(runner_pid: int) -> bool:
|
297
|
+
"""
|
298
|
+
Check if runner process is alive.
|
299
|
+
|
300
|
+
Uses psutil if available for accurate check, otherwise falls back
|
301
|
+
to os.kill(pid, 0) which only checks if process exists.
|
302
|
+
|
303
|
+
Args:
|
304
|
+
runner_pid: PID of runner process
|
305
|
+
|
306
|
+
Returns:
|
307
|
+
True if process exists and belongs to current user, False otherwise
|
308
|
+
|
309
|
+
Example:
|
310
|
+
alive = is_runner_alive(12345)
|
311
|
+
# Returns: True if process exists, False otherwise
|
312
|
+
"""
|
313
|
+
if runner_pid is None or runner_pid <= 0:
|
314
|
+
return False
|
315
|
+
|
316
|
+
if PSUTIL_AVAILABLE:
|
317
|
+
# Use psutil for accurate check
|
318
|
+
try:
|
319
|
+
process = psutil.Process(runner_pid)
|
320
|
+
# Check if process exists and is not a zombie
|
321
|
+
return process.is_running() and process.status() != psutil.STATUS_ZOMBIE
|
322
|
+
except psutil.NoSuchProcess:
|
323
|
+
return False
|
324
|
+
except psutil.AccessDenied:
|
325
|
+
# Process exists but we can't access it (different user)
|
326
|
+
return False
|
327
|
+
|
328
|
+
else:
|
329
|
+
# Fallback: Check /proc/{pid}/stat for zombie status
|
330
|
+
try:
|
331
|
+
# First check if process exists
|
332
|
+
os.kill(runner_pid, 0)
|
333
|
+
|
334
|
+
# Read process status from /proc
|
335
|
+
try:
|
336
|
+
with open(f"/proc/{runner_pid}/stat", "r") as f:
|
337
|
+
stat = f.read()
|
338
|
+
# Process state is the 3rd field (after PID and command name in parens)
|
339
|
+
# States: R (running), S (sleeping), D (disk sleep), Z (zombie), T (stopped)
|
340
|
+
# Extract state: it's the character after the closing paren and space
|
341
|
+
state_start = stat.rfind(")") + 2
|
342
|
+
state = stat[state_start] if state_start < len(stat) else "?"
|
343
|
+
# Zombie state is 'Z'
|
344
|
+
return state != "Z"
|
345
|
+
except (FileNotFoundError, IOError):
|
346
|
+
# /proc not available or process gone - assume not alive
|
347
|
+
return False
|
348
|
+
|
349
|
+
except ProcessLookupError:
|
350
|
+
# Process doesn't exist
|
351
|
+
return False
|
352
|
+
except PermissionError:
|
353
|
+
# Process exists but different user (shouldn't happen for our processes)
|
354
|
+
return False
|
355
|
+
except Exception:
|
356
|
+
return False
|
357
|
+
|
358
|
+
|
359
|
+
def kill_runner(runner_pid: int, force: bool = False) -> bool:
|
360
|
+
"""
|
361
|
+
Kill VM runner process.
|
362
|
+
|
363
|
+
Args:
|
364
|
+
runner_pid: PID of runner process
|
365
|
+
force: If True, use SIGKILL. If False, use SIGTERM (graceful)
|
366
|
+
|
367
|
+
Returns:
|
368
|
+
True if process was killed, False if process not found
|
369
|
+
|
370
|
+
Example:
|
371
|
+
# Graceful shutdown
|
372
|
+
kill_runner(12345, force=False)
|
373
|
+
|
374
|
+
# Force kill
|
375
|
+
kill_runner(12345, force=True)
|
376
|
+
"""
|
377
|
+
if not is_runner_alive(runner_pid):
|
378
|
+
return False
|
379
|
+
|
380
|
+
try:
|
381
|
+
if force:
|
382
|
+
LOG.debug(f"Force killing runner process {runner_pid} (SIGKILL)")
|
383
|
+
os.kill(runner_pid, 9) # SIGKILL
|
384
|
+
else:
|
385
|
+
LOG.debug(f"Gracefully stopping runner process {runner_pid} (SIGTERM)")
|
386
|
+
os.kill(runner_pid, 15) # SIGTERM
|
387
|
+
return True
|
388
|
+
except ProcessLookupError:
|
389
|
+
return False
|
390
|
+
except PermissionError:
|
391
|
+
LOG.error(f"Permission denied when killing process {runner_pid}")
|
392
|
+
return False
|
393
|
+
except Exception as e:
|
394
|
+
LOG.error(f"Failed to kill process {runner_pid}: {e}")
|
395
|
+
return False
|
maqet/qemu_args.py
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
from typing import List
|
2
|
+
|
3
|
+
from maqet.logger import LOG
|
4
|
+
|
5
|
+
|
6
|
+
class Arguments:
|
7
|
+
@staticmethod
|
8
|
+
def split_args(*args: str) -> list[str]:
|
9
|
+
"""
|
10
|
+
Split args by spaces and return as list
|
11
|
+
"""
|
12
|
+
return [
|
13
|
+
a_splitted
|
14
|
+
for a in args
|
15
|
+
for a_splitted in a.split(' ')
|
16
|
+
]
|
17
|
+
|
18
|
+
@staticmethod
|
19
|
+
def parse_options(arg, stack=[], key_only=False):
|
20
|
+
if not isinstance(arg, (list, dict)):
|
21
|
+
if len(stack) > 0:
|
22
|
+
if key_only:
|
23
|
+
return '.'.join(stack) + f'.{arg}'
|
24
|
+
else:
|
25
|
+
return '.'.join(stack) + f'={arg}'
|
26
|
+
else:
|
27
|
+
return arg
|
28
|
+
|
29
|
+
options = []
|
30
|
+
if isinstance(arg, list):
|
31
|
+
for v in arg:
|
32
|
+
if isinstance(arg, (list, dict)):
|
33
|
+
options.append(Arguments.parse_options(
|
34
|
+
v, stack, key_only=True))
|
35
|
+
else:
|
36
|
+
options.append('.'.join(stack) + f'.{v}')
|
37
|
+
|
38
|
+
elif isinstance(arg, dict):
|
39
|
+
for k, v in arg.items():
|
40
|
+
if isinstance(arg, (list, dict)):
|
41
|
+
options.append(Arguments.parse_options(
|
42
|
+
v, stack+[k], key_only=False))
|
43
|
+
else:
|
44
|
+
option = '.'.join(stack) + f'={v}'
|
45
|
+
options.append(option)
|
46
|
+
return ','.join(options)
|
47
|
+
|
48
|
+
@staticmethod
|
49
|
+
def parse_args(*args) -> List[str]:
|
50
|
+
final_args = []
|
51
|
+
|
52
|
+
LOG.debug(f'Parsing {args}')
|
53
|
+
|
54
|
+
for arg in args:
|
55
|
+
if type(arg) is str:
|
56
|
+
argument = f'-{arg}'
|
57
|
+
else:
|
58
|
+
if isinstance(arg, dict):
|
59
|
+
al = list(arg.items())
|
60
|
+
else:
|
61
|
+
al = list(args)
|
62
|
+
if len(al) == 1:
|
63
|
+
argument = (f"-{al[0][0]}"
|
64
|
+
f" {Arguments.parse_options(al[0][1])}")
|
65
|
+
else:
|
66
|
+
arg = al[0][0]
|
67
|
+
subarg = dict(al[1:])
|
68
|
+
argument = (f"-{al[0][0]} "
|
69
|
+
f"{al[0][1]},"
|
70
|
+
f"{Arguments.parse_options(subarg)}")
|
71
|
+
final_args.append(argument)
|
72
|
+
|
73
|
+
return Arguments.split_args(*final_args)
|
74
|
+
|
75
|
+
def __call__(args: list):
|
76
|
+
return Arguments.parse_args(args)
|
maqet/qmp/__init__.py
ADDED
maqet/qmp/commands.py
ADDED
@@ -0,0 +1,92 @@
|
|
1
|
+
"""
|
2
|
+
QMP Command Wrappers
|
3
|
+
|
4
|
+
Pre-built QMP command implementations that can be reused across the codebase.
|
5
|
+
These provide a clean abstraction layer over raw QMP commands.
|
6
|
+
"""
|
7
|
+
|
8
|
+
from typing import Any, Dict
|
9
|
+
|
10
|
+
|
11
|
+
class QMPCommands:
|
12
|
+
"""Collection of pre-built QMP command wrappers."""
|
13
|
+
|
14
|
+
@staticmethod
|
15
|
+
def screendump(machine, filename: str) -> Dict[str, Any]:
|
16
|
+
"""
|
17
|
+
Take a screenshot of the VM display.
|
18
|
+
|
19
|
+
Args:
|
20
|
+
machine: Machine instance with qmp_command method
|
21
|
+
filename: Path to save screenshot (PPM format)
|
22
|
+
|
23
|
+
Returns:
|
24
|
+
QMP command result
|
25
|
+
"""
|
26
|
+
return machine.qmp_command("screendump", filename=filename)
|
27
|
+
|
28
|
+
@staticmethod
|
29
|
+
def pause(machine) -> Dict[str, Any]:
|
30
|
+
"""
|
31
|
+
Pause VM execution.
|
32
|
+
|
33
|
+
Args:
|
34
|
+
machine: Machine instance with qmp_command method
|
35
|
+
|
36
|
+
Returns:
|
37
|
+
QMP command result
|
38
|
+
"""
|
39
|
+
return machine.qmp_command("stop")
|
40
|
+
|
41
|
+
@staticmethod
|
42
|
+
def resume(machine) -> Dict[str, Any]:
|
43
|
+
"""
|
44
|
+
Resume VM execution.
|
45
|
+
|
46
|
+
Args:
|
47
|
+
machine: Machine instance with qmp_command method
|
48
|
+
|
49
|
+
Returns:
|
50
|
+
QMP command result
|
51
|
+
"""
|
52
|
+
return machine.qmp_command("cont")
|
53
|
+
|
54
|
+
@staticmethod
|
55
|
+
def device_del(machine, device_id: str) -> Dict[str, Any]:
|
56
|
+
"""
|
57
|
+
Remove a hotplugged device.
|
58
|
+
|
59
|
+
Args:
|
60
|
+
machine: Machine instance with qmp_command method
|
61
|
+
device_id: Device identifier to remove
|
62
|
+
|
63
|
+
Returns:
|
64
|
+
QMP command result
|
65
|
+
"""
|
66
|
+
return machine.qmp_command("device_del", id=device_id)
|
67
|
+
|
68
|
+
@staticmethod
|
69
|
+
def system_powerdown(machine) -> Dict[str, Any]:
|
70
|
+
"""
|
71
|
+
Send ACPI shutdown signal to VM.
|
72
|
+
|
73
|
+
Args:
|
74
|
+
machine: Machine instance with qmp_command method
|
75
|
+
|
76
|
+
Returns:
|
77
|
+
QMP command result
|
78
|
+
"""
|
79
|
+
return machine.qmp_command("system_powerdown")
|
80
|
+
|
81
|
+
@staticmethod
|
82
|
+
def system_reset(machine) -> Dict[str, Any]:
|
83
|
+
"""
|
84
|
+
Reset the VM.
|
85
|
+
|
86
|
+
Args:
|
87
|
+
machine: Machine instance with qmp_command method
|
88
|
+
|
89
|
+
Returns:
|
90
|
+
QMP command result
|
91
|
+
"""
|
92
|
+
return machine.qmp_command("system_reset")
|