maqet 0.0.1.3__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.
Files changed (83) hide show
  1. maqet/__init__.py +50 -6
  2. maqet/__main__.py +96 -0
  3. maqet/__version__.py +3 -0
  4. maqet/api/__init__.py +35 -0
  5. maqet/api/decorators.py +184 -0
  6. maqet/api/metadata.py +147 -0
  7. maqet/api/registry.py +182 -0
  8. maqet/cli.py +71 -0
  9. maqet/config/__init__.py +26 -0
  10. maqet/config/merger.py +237 -0
  11. maqet/config/parser.py +198 -0
  12. maqet/config/validators.py +519 -0
  13. maqet/config_handlers.py +684 -0
  14. maqet/constants.py +200 -0
  15. maqet/exceptions.py +226 -0
  16. maqet/formatters.py +294 -0
  17. maqet/generators/__init__.py +12 -0
  18. maqet/generators/base_generator.py +101 -0
  19. maqet/generators/cli_generator.py +635 -0
  20. maqet/generators/python_generator.py +247 -0
  21. maqet/generators/rest_generator.py +58 -0
  22. maqet/handlers/__init__.py +12 -0
  23. maqet/handlers/base.py +108 -0
  24. maqet/handlers/init.py +147 -0
  25. maqet/handlers/stage.py +196 -0
  26. maqet/ipc/__init__.py +29 -0
  27. maqet/ipc/retry.py +265 -0
  28. maqet/ipc/runner_client.py +285 -0
  29. maqet/ipc/unix_socket_server.py +239 -0
  30. maqet/logger.py +160 -55
  31. maqet/machine.py +884 -0
  32. maqet/managers/__init__.py +7 -0
  33. maqet/managers/qmp_manager.py +333 -0
  34. maqet/managers/snapshot_coordinator.py +327 -0
  35. maqet/managers/vm_manager.py +683 -0
  36. maqet/maqet.py +1120 -0
  37. maqet/os_interactions.py +46 -0
  38. maqet/process_spawner.py +395 -0
  39. maqet/qemu_args.py +76 -0
  40. maqet/qmp/__init__.py +10 -0
  41. maqet/qmp/commands.py +92 -0
  42. maqet/qmp/keyboard.py +311 -0
  43. maqet/qmp/qmp.py +17 -0
  44. maqet/snapshot.py +473 -0
  45. maqet/state.py +958 -0
  46. maqet/storage.py +702 -162
  47. maqet/validation/__init__.py +9 -0
  48. maqet/validation/config_validator.py +170 -0
  49. maqet/vm_runner.py +523 -0
  50. maqet-0.0.5.dist-info/METADATA +237 -0
  51. maqet-0.0.5.dist-info/RECORD +55 -0
  52. {maqet-0.0.1.3.dist-info → maqet-0.0.5.dist-info}/WHEEL +1 -1
  53. maqet-0.0.5.dist-info/entry_points.txt +2 -0
  54. maqet-0.0.5.dist-info/licenses/LICENSE +21 -0
  55. {maqet-0.0.1.3.dist-info → maqet-0.0.5.dist-info}/top_level.txt +0 -1
  56. maqet/core.py +0 -395
  57. maqet/functions.py +0 -104
  58. maqet-0.0.1.3.dist-info/METADATA +0 -104
  59. maqet-0.0.1.3.dist-info/RECORD +0 -33
  60. qemu/machine/__init__.py +0 -36
  61. qemu/machine/console_socket.py +0 -142
  62. qemu/machine/machine.py +0 -954
  63. qemu/machine/py.typed +0 -0
  64. qemu/machine/qtest.py +0 -191
  65. qemu/qmp/__init__.py +0 -59
  66. qemu/qmp/error.py +0 -50
  67. qemu/qmp/events.py +0 -717
  68. qemu/qmp/legacy.py +0 -319
  69. qemu/qmp/message.py +0 -209
  70. qemu/qmp/models.py +0 -146
  71. qemu/qmp/protocol.py +0 -1057
  72. qemu/qmp/py.typed +0 -0
  73. qemu/qmp/qmp_client.py +0 -655
  74. qemu/qmp/qmp_shell.py +0 -618
  75. qemu/qmp/qmp_tui.py +0 -655
  76. qemu/qmp/util.py +0 -219
  77. qemu/utils/__init__.py +0 -162
  78. qemu/utils/accel.py +0 -84
  79. qemu/utils/py.typed +0 -0
  80. qemu/utils/qemu_ga_client.py +0 -323
  81. qemu/utils/qom.py +0 -273
  82. qemu/utils/qom_common.py +0 -175
  83. qemu/utils/qom_fuse.py +0 -207
@@ -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
@@ -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
@@ -0,0 +1,10 @@
1
+ """
2
+ QMP Module
3
+
4
+ Contains QMP-related functionality for MAQET.
5
+ """
6
+
7
+ from .commands import QMPCommands
8
+ from .keyboard import KeyboardEmulator
9
+
10
+ __all__ = ["KeyboardEmulator", "QMPCommands"]
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")