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.
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.4.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.4.dist-info → maqet-0.0.5.dist-info}/top_level.txt +0 -1
  56. maqet/core.py +0 -411
  57. maqet/functions.py +0 -104
  58. maqet-0.0.1.4.dist-info/METADATA +0 -6
  59. maqet-0.0.1.4.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,285 @@
1
+ """
2
+ Runner Client
3
+
4
+ Client for communicating with VM runner processes via Unix sockets.
5
+ Used by CLI to send commands to running VM runner processes.
6
+
7
+ Architecture:
8
+ - Each VM has unique socket path
9
+ - Client connects, sends request, receives response, disconnects
10
+ - Synchronous and async interfaces
11
+ - Error handling for connection issues
12
+ """
13
+
14
+ import asyncio
15
+ import json
16
+ import os
17
+ import socket
18
+ from pathlib import Path
19
+ from typing import Any, Dict, Optional
20
+
21
+ from ..constants import Intervals, Retries, Timeouts
22
+ from ..logger import LOG
23
+ from .retry import async_retry_with_backoff, CircuitBreaker
24
+
25
+ # Optional dependency - imported inline with fallback
26
+ try:
27
+ import psutil
28
+
29
+ PSUTIL_AVAILABLE = True
30
+ except ImportError:
31
+ PSUTIL_AVAILABLE = False
32
+
33
+
34
+ class RunnerClientError(Exception):
35
+ """Runner client communication errors."""
36
+
37
+
38
+ class RunnerClient:
39
+ """
40
+ Client for communicating with a VM runner process.
41
+
42
+ Used by CLI to send commands to running VM runners via Unix sockets.
43
+ Provides both synchronous and asynchronous interfaces.
44
+
45
+ Example:
46
+ client = RunnerClient("vm1", state_manager)
47
+ result = client.send_command("qmp", "query-status")
48
+ """
49
+
50
+ def __init__(self, vm_id: str, state_manager):
51
+ """
52
+ Initialize runner client.
53
+
54
+ Args:
55
+ vm_id: VM identifier
56
+ state_manager: StateManager instance for DB access
57
+ """
58
+ self.vm_id = vm_id
59
+ self.state_manager = state_manager
60
+ self.socket_path = self._get_socket_path()
61
+ self._circuit_breaker = CircuitBreaker(
62
+ failure_threshold=5, timeout=60.0
63
+ )
64
+
65
+ LOG.debug(f"RunnerClient initialized for {vm_id}")
66
+
67
+ def is_runner_running(self) -> bool:
68
+ """
69
+ Check if VM runner process is running.
70
+
71
+ Uses psutil if available for accurate check, otherwise
72
+ falls back to checking if socket exists.
73
+
74
+ Returns:
75
+ True if runner is running, False otherwise
76
+ """
77
+ # Get VM from database
78
+ vm = self.state_manager.get_vm(self.vm_id)
79
+ if not vm or not vm.runner_pid:
80
+ return False
81
+
82
+ # Check if process exists (if psutil available)
83
+ if PSUTIL_AVAILABLE:
84
+ return psutil.pid_exists(vm.runner_pid)
85
+ else:
86
+ # Fallback: check if socket exists
87
+ return self.socket_path.exists()
88
+
89
+ @async_retry_with_backoff(
90
+ max_attempts=Retries.IPC_MAX_RETRIES,
91
+ backoff_base=Intervals.IPC_BACKOFF_BASE,
92
+ exceptions=(ConnectionRefusedError, FileNotFoundError, OSError),
93
+ )
94
+ async def _connect_to_socket(self):
95
+ """
96
+ Connect to Unix socket with retry logic.
97
+
98
+ Internal method that handles connection with automatic retry
99
+ on transient failures.
100
+
101
+ Returns:
102
+ Tuple of (reader, writer) for socket communication
103
+
104
+ Raises:
105
+ ConnectionRefusedError: If connection refused
106
+ FileNotFoundError: If socket doesn't exist
107
+ OSError: On other connection errors
108
+ """
109
+ return await asyncio.wait_for(
110
+ asyncio.open_unix_connection(str(self.socket_path)),
111
+ timeout=Timeouts.IPC_CONNECT,
112
+ )
113
+
114
+ async def send_command_async(
115
+ self, method: str, *args, **kwargs
116
+ ) -> Dict[str, Any]:
117
+ """
118
+ Send command to VM runner asynchronously.
119
+
120
+ Args:
121
+ method: Method name (e.g., "qmp", "stop", "status", "ping")
122
+ *args: Method arguments
123
+ **kwargs: Method keyword arguments
124
+
125
+ Returns:
126
+ Result dictionary from runner
127
+
128
+ Raises:
129
+ RunnerClientError: If runner not running or communication fails
130
+ """
131
+ # Check circuit breaker
132
+ if self._circuit_breaker.is_open():
133
+ raise RunnerClientError(
134
+ f"Circuit breaker open for {self.vm_id}. "
135
+ f"Too many consecutive failures. Try again later."
136
+ )
137
+
138
+ # Check runner running
139
+ if not self.is_runner_running():
140
+ raise RunnerClientError(
141
+ f"VM runner for {self.vm_id} is not running. "
142
+ f"Start VM first with: maqet start {self.vm_id}"
143
+ )
144
+
145
+ # Check socket exists
146
+ if not self.socket_path.exists():
147
+ raise RunnerClientError(
148
+ f"Socket not found: {self.socket_path}. "
149
+ f"VM runner may have crashed."
150
+ )
151
+
152
+ # Build request
153
+ request = {"method": method, "args": list(args), "kwargs": kwargs}
154
+
155
+ LOG.debug(f"Sending IPC request: method={method}")
156
+
157
+ try:
158
+ # Connect to Unix socket with retry logic
159
+ reader, writer = await self._connect_to_socket()
160
+
161
+ try:
162
+ # Send request
163
+ request_data = json.dumps(request).encode("utf-8")
164
+ writer.write(request_data)
165
+ await writer.drain()
166
+
167
+ # Receive response (up to 1MB)
168
+ response_data = await asyncio.wait_for(
169
+ reader.read(1024 * 1024), timeout=Timeouts.IPC_COMMAND
170
+ )
171
+ if not response_data:
172
+ raise RunnerClientError("Empty response from runner")
173
+
174
+ # Parse response
175
+ try:
176
+ response = json.loads(response_data.decode("utf-8"))
177
+ except json.JSONDecodeError as e:
178
+ raise RunnerClientError(f"Invalid JSON response: {e}")
179
+
180
+ # Check response status
181
+ if response.get("status") == "error":
182
+ raise RunnerClientError(response.get("error", "Unknown error"))
183
+
184
+ LOG.debug(f"IPC response received: status={response.get('status')}")
185
+
186
+ # Record success for circuit breaker
187
+ self._circuit_breaker.record_success()
188
+
189
+ return response.get("result", {})
190
+
191
+ finally:
192
+ # Always close connection
193
+ try:
194
+ writer.close()
195
+ await writer.wait_closed()
196
+ except Exception as e:
197
+ LOG.debug(f"Error closing connection: {e}")
198
+
199
+ except ConnectionRefusedError as e:
200
+ self._circuit_breaker.record_failure()
201
+ raise RunnerClientError(
202
+ f"Connection refused. VM runner for {self.vm_id} "
203
+ f"may have crashed or stopped. Error: {e}"
204
+ )
205
+ except FileNotFoundError as e:
206
+ self._circuit_breaker.record_failure()
207
+ raise RunnerClientError(
208
+ f"Socket not found: {self.socket_path}. "
209
+ f"VM runner may have exited. Error: {e}"
210
+ )
211
+ except asyncio.TimeoutError as e:
212
+ self._circuit_breaker.record_failure()
213
+ raise RunnerClientError(
214
+ f"IPC operation timed out for {self.vm_id}. "
215
+ f"Runner may be unresponsive. Error: {e}"
216
+ )
217
+ except OSError as e:
218
+ self._circuit_breaker.record_failure()
219
+ raise RunnerClientError(f"Communication error: {e}")
220
+ except RunnerClientError:
221
+ # Already a RunnerClientError, don't wrap again
222
+ self._circuit_breaker.record_failure()
223
+ raise
224
+ except Exception as e:
225
+ self._circuit_breaker.record_failure()
226
+ raise RunnerClientError(f"Unexpected error: {e}")
227
+
228
+ def send_command(self, method: str, *args, **kwargs) -> Dict[str, Any]:
229
+ """
230
+ Send command to VM runner synchronously.
231
+
232
+ Convenience wrapper around send_command_async() for synchronous usage.
233
+
234
+ Args:
235
+ method: Method name (e.g., "qmp", "stop", "status", "ping")
236
+ *args: Method arguments
237
+ **kwargs: Method keyword arguments
238
+
239
+ Returns:
240
+ Result dictionary from runner
241
+
242
+ Raises:
243
+ RunnerClientError: If runner not running or communication fails
244
+ """
245
+ return asyncio.run(self.send_command_async(method, *args, **kwargs))
246
+
247
+ def ping(self) -> bool:
248
+ """
249
+ Ping VM runner to check if it's responsive.
250
+
251
+ Returns:
252
+ True if runner responds to ping, False otherwise
253
+ """
254
+ try:
255
+ result = self.send_command("ping")
256
+ return result == "pong"
257
+ except RunnerClientError:
258
+ return False
259
+
260
+ def _get_socket_path(self) -> Path:
261
+ """
262
+ Get Unix socket path for this VM.
263
+
264
+ Socket location: XDG_RUNTIME_DIR/maqet/sockets/{vm_id}.sock
265
+ Falls back to /tmp/maqet-{uid}/sockets/ if XDG_RUNTIME_DIR not available.
266
+
267
+ Returns:
268
+ Path to Unix socket
269
+ """
270
+ # Get runtime directory (prefer XDG_RUNTIME_DIR)
271
+ runtime_dir_base = os.environ.get(
272
+ "XDG_RUNTIME_DIR", f"/run/user/{os.getuid()}"
273
+ )
274
+ if not Path(runtime_dir_base).exists():
275
+ # Fallback to /tmp
276
+ runtime_dir_base = f"/tmp/maqet-{os.getuid()}"
277
+
278
+ runtime_dir = Path(runtime_dir_base) / "maqet"
279
+ socket_dir = runtime_dir / "sockets"
280
+
281
+ return socket_dir / f"{self.vm_id}.sock"
282
+
283
+ def __repr__(self) -> str:
284
+ """String representation for debugging."""
285
+ return f"RunnerClient(vm_id={self.vm_id}, socket={self.socket_path})"
@@ -0,0 +1,239 @@
1
+ """
2
+ Unix Socket IPC Server
3
+
4
+ Simple Unix domain socket server for IPC between CLI and VM runner.
5
+ Uses JSON-RPC style protocol for request/response communication.
6
+
7
+ Architecture:
8
+ - Each VM runner process has its own Unix socket
9
+ - Non-blocking async I/O using asyncio
10
+ - Simple request/response pattern
11
+ - Socket cleanup on server stop
12
+ """
13
+
14
+ import asyncio
15
+ import json
16
+ from pathlib import Path
17
+ from typing import Any, Callable, Dict, Optional
18
+
19
+ from ..constants import Timeouts
20
+ from ..logger import LOG
21
+
22
+
23
+ class UnixSocketIPCServerError(Exception):
24
+ """Unix socket IPC server errors."""
25
+
26
+
27
+ class UnixSocketIPCServer:
28
+ """
29
+ Unix domain socket server for IPC between CLI and VM runner.
30
+
31
+ Protocol: JSON-RPC style
32
+ - Client sends: {"method": "qmp", "args": [...], "kwargs": {...}}
33
+ - Server responds: {"status": "success", "result": ...} or
34
+ {"status": "error", "error": "..."}
35
+
36
+ Socket lifecycle:
37
+ 1. Server starts, binds to socket path
38
+ 2. Accepts connections from CLI clients
39
+ 3. Reads JSON request, calls handler
40
+ 4. Writes JSON response
41
+ 5. Closes connection
42
+ """
43
+
44
+ def __init__(
45
+ self, socket_path: Path, handler: Callable[[Dict[str, Any]], Dict[str, Any]]
46
+ ):
47
+ """
48
+ Initialize Unix socket server.
49
+
50
+ Args:
51
+ socket_path: Path to Unix socket file
52
+ handler: Async function to handle requests
53
+ Takes request dict, returns response dict
54
+ """
55
+ self.socket_path = Path(socket_path)
56
+ self.handler = handler
57
+ self.server: Optional[asyncio.Server] = None
58
+ self._running = False
59
+ self._loop: Optional[asyncio.AbstractEventLoop] = None
60
+
61
+ LOG.debug(f"UnixSocketIPCServer initialized for {socket_path}")
62
+
63
+ async def start(self) -> None:
64
+ """
65
+ Start listening on Unix socket.
66
+
67
+ Process:
68
+ 1. Remove existing socket if present (stale socket handling)
69
+ 2. Create Unix socket server
70
+ 3. Start accepting connections
71
+ 4. Keep server running until stop() called
72
+
73
+ Raises:
74
+ UnixSocketIPCServerError: If socket already in use or bind fails
75
+ """
76
+ # Store event loop for cross-thread communication
77
+ self._loop = asyncio.get_running_loop()
78
+
79
+ # Remove existing socket if present
80
+ if self.socket_path.exists():
81
+ # Try to connect to check if someone is using it
82
+ try:
83
+ reader, writer = await asyncio.wait_for(
84
+ asyncio.open_unix_connection(str(self.socket_path)),
85
+ timeout=Timeouts.IPC_HEALTH_CHECK,
86
+ )
87
+ writer.close()
88
+ await writer.wait_closed()
89
+ # Someone is using it
90
+ raise UnixSocketIPCServerError(
91
+ f"Socket already in use: {self.socket_path}"
92
+ )
93
+ except (ConnectionRefusedError, FileNotFoundError, asyncio.TimeoutError):
94
+ # Stale socket, remove it
95
+ LOG.debug(f"Removing stale socket {self.socket_path}")
96
+ self.socket_path.unlink()
97
+
98
+ # Create Unix socket server
99
+ try:
100
+ self.server = await asyncio.start_unix_server(
101
+ self._handle_client, path=str(self.socket_path)
102
+ )
103
+ self._running = True
104
+ LOG.info(f"IPC server listening on {self.socket_path}")
105
+
106
+ # Keep server running
107
+ async with self.server:
108
+ await self.server.serve_forever()
109
+
110
+ except asyncio.CancelledError:
111
+ # Server was stopped via stop() - this is normal
112
+ LOG.debug("IPC server cancelled (normal shutdown)")
113
+ except Exception as e:
114
+ raise UnixSocketIPCServerError(f"Failed to start IPC server: {e}")
115
+
116
+ async def _handle_client(
117
+ self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter
118
+ ) -> None:
119
+ """
120
+ Handle single client connection.
121
+
122
+ Process:
123
+ 1. Read JSON request from client
124
+ 2. Parse and validate request
125
+ 3. Call handler function
126
+ 4. Write JSON response to client
127
+ 5. Close connection
128
+
129
+ Args:
130
+ reader: Async stream reader
131
+ writer: Async stream writer
132
+ """
133
+ try:
134
+ # Read request (up to 1MB)
135
+ data = await reader.read(1024 * 1024)
136
+ if not data:
137
+ return
138
+
139
+ # Parse JSON request
140
+ try:
141
+ request = json.loads(data.decode("utf-8"))
142
+ except json.JSONDecodeError as e:
143
+ response = {"status": "error", "error": f"Invalid JSON: {e}"}
144
+ writer.write(json.dumps(response).encode("utf-8"))
145
+ await writer.drain()
146
+ return
147
+
148
+ LOG.debug(f"IPC request: {request.get('method', 'unknown')}")
149
+
150
+ # Call handler
151
+ try:
152
+ response = await self.handler(request)
153
+ except Exception as e:
154
+ LOG.error(f"Handler error: {e}")
155
+ response = {"status": "error", "error": str(e)}
156
+
157
+ # Write response
158
+ response_data = json.dumps(response).encode("utf-8")
159
+ writer.write(response_data)
160
+ await writer.drain()
161
+
162
+ LOG.debug(f"IPC response: {response.get('status', 'unknown')}")
163
+
164
+ except Exception as e:
165
+ LOG.error(f"Error handling client: {e}")
166
+
167
+ finally:
168
+ # Close connection
169
+ try:
170
+ writer.close()
171
+ await writer.wait_closed()
172
+ except Exception as e:
173
+ LOG.debug(f"Error closing connection: {e}")
174
+
175
+ async def stop(self) -> None:
176
+ """
177
+ Stop server and cleanup socket (async version).
178
+
179
+ Process:
180
+ 1. Close server (stop accepting connections)
181
+ 2. Remove socket file
182
+
183
+ Note: This should be called from within the same event loop as start().
184
+ For cross-thread stopping, use stop_sync() instead.
185
+ """
186
+ LOG.debug("Stopping IPC server")
187
+ self._running = False
188
+
189
+ # Close server
190
+ if self.server:
191
+ self.server.close()
192
+ await self.server.wait_closed()
193
+
194
+ # Remove socket file
195
+ if self.socket_path.exists():
196
+ try:
197
+ self.socket_path.unlink()
198
+ LOG.debug(f"Removed socket {self.socket_path}")
199
+ except Exception as e:
200
+ LOG.warning(f"Failed to remove socket: {e}")
201
+
202
+ def stop_sync(self) -> None:
203
+ """
204
+ Stop server from another thread (synchronous).
205
+
206
+ This method is thread-safe and can be called from the main thread
207
+ to stop the IPC server running in a background thread.
208
+
209
+ Process:
210
+ 1. Mark server as stopped
211
+ 2. Close server socket (cancels serve_forever)
212
+ 3. Remove socket file from filesystem
213
+ """
214
+ LOG.debug("Stopping IPC server (sync)")
215
+ self._running = False
216
+
217
+ # Close server socket from any thread
218
+ # This will cause serve_forever() to raise CancelledError
219
+ if self.server:
220
+ # Call server.close() in a thread-safe way
221
+ if self._loop and self._loop.is_running():
222
+ # Schedule close in the IPC server's event loop
223
+ self._loop.call_soon_threadsafe(self.server.close)
224
+ else:
225
+ # Event loop not running, close directly
226
+ self.server.close()
227
+
228
+ # Remove socket file (filesystem operation, thread-safe)
229
+ if self.socket_path.exists():
230
+ try:
231
+ self.socket_path.unlink()
232
+ LOG.debug(f"Removed socket {self.socket_path}")
233
+ except Exception as e:
234
+ LOG.warning(f"Failed to remove socket: {e}")
235
+
236
+ @property
237
+ def is_running(self) -> bool:
238
+ """Check if server is running."""
239
+ return self._running