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/config_handlers.py
ADDED
@@ -0,0 +1,684 @@
|
|
1
|
+
"""
|
2
|
+
Configuration Handlers for Machine Setup
|
3
|
+
|
4
|
+
Provides an extensible handler-based system for processing VM configuration.
|
5
|
+
Each configuration key can have a dedicated handler method.
|
6
|
+
|
7
|
+
Supports both global registry (backward compatibility) and instance-based
|
8
|
+
registry (for parallel tests and multiple Machine instances).
|
9
|
+
"""
|
10
|
+
|
11
|
+
import inspect
|
12
|
+
from typing import Any, Callable, Dict, List, Optional, Type
|
13
|
+
|
14
|
+
from .logger import LOG
|
15
|
+
|
16
|
+
|
17
|
+
class ConfigHandlerRegistry:
|
18
|
+
"""Registry for configuration handlers."""
|
19
|
+
|
20
|
+
def __init__(self):
|
21
|
+
"""Initialize the handler registry."""
|
22
|
+
self._handlers: Dict[str, Callable] = {}
|
23
|
+
|
24
|
+
def register_handler(self, config_key: str, handler: Callable):
|
25
|
+
"""
|
26
|
+
Register a configuration handler.
|
27
|
+
|
28
|
+
Args:
|
29
|
+
config_key: Configuration key to handle
|
30
|
+
handler: Handler function
|
31
|
+
"""
|
32
|
+
self._handlers[config_key] = handler
|
33
|
+
LOG.debug(f"Registered config handler for '{config_key}'")
|
34
|
+
|
35
|
+
def get_handler(self, config_key: str) -> Optional[Callable]:
|
36
|
+
"""
|
37
|
+
Get handler for configuration key.
|
38
|
+
|
39
|
+
Args:
|
40
|
+
config_key: Configuration key
|
41
|
+
|
42
|
+
Returns:
|
43
|
+
Handler function or None if not found
|
44
|
+
"""
|
45
|
+
return self._handlers.get(config_key)
|
46
|
+
|
47
|
+
def get_registered_keys(self) -> List[str]:
|
48
|
+
"""Get list of all registered configuration keys."""
|
49
|
+
return list(self._handlers.keys())
|
50
|
+
|
51
|
+
def register_from_instance(self, instance: Any) -> None:
|
52
|
+
"""
|
53
|
+
Register all @config_handler decorated methods from an instance.
|
54
|
+
|
55
|
+
This enables instance-based registries where each ConfigurableMachine
|
56
|
+
has its own registry, allowing for:
|
57
|
+
- Parallel test execution without registry pollution
|
58
|
+
- Multiple Machine instances with different handlers
|
59
|
+
- Thread-safe operation
|
60
|
+
|
61
|
+
Args:
|
62
|
+
instance: Object instance to scan for @config_handler decorated methods
|
63
|
+
|
64
|
+
Example:
|
65
|
+
registry = ConfigHandlerRegistry()
|
66
|
+
machine = ConfigurableMachine()
|
67
|
+
registry.register_from_instance(machine)
|
68
|
+
"""
|
69
|
+
# Get the class of the instance
|
70
|
+
cls = instance.__class__
|
71
|
+
|
72
|
+
# Scan for decorated methods
|
73
|
+
for name, method in inspect.getmembers(cls, predicate=inspect.isfunction):
|
74
|
+
if hasattr(method, "_config_handler_key"):
|
75
|
+
key = method._config_handler_key
|
76
|
+
# Bind method to instance
|
77
|
+
bound_method = getattr(instance, name)
|
78
|
+
self.register_handler(key, bound_method)
|
79
|
+
|
80
|
+
|
81
|
+
# Global registry instance (for backward compatibility)
|
82
|
+
# New code should prefer instance-based registries via register_from_instance()
|
83
|
+
# TODO(architect, 2025-10-10): [ARCH] Global mutable state - tests interfere with each other
|
84
|
+
# Context: Module-level singleton makes parallel test execution unreliable and prevents
|
85
|
+
# multiple Maqet instances with different configs. Issue #5 in ARCHITECTURAL_REVIEW.md.
|
86
|
+
#
|
87
|
+
# Recommendation: Make registry instance-based (Maqet owns registry) or use thread-local
|
88
|
+
# context managers.
|
89
|
+
#
|
90
|
+
# Effort: Medium (4-6 days, requires refactoring decorators)
|
91
|
+
# Priority: High (should fix for 1.0)
|
92
|
+
# See: ARCHITECTURAL_REVIEW.md Issue #5
|
93
|
+
_config_registry = ConfigHandlerRegistry()
|
94
|
+
|
95
|
+
|
96
|
+
def config_handler(config_key: str):
|
97
|
+
"""
|
98
|
+
Decorator to register configuration handlers.
|
99
|
+
|
100
|
+
The decorator stores metadata on the function for later registration.
|
101
|
+
Supports both global registration (backward compatibility) and
|
102
|
+
instance registration (preferred for new code).
|
103
|
+
|
104
|
+
Args:
|
105
|
+
config_key: Configuration key this handler processes
|
106
|
+
|
107
|
+
Example:
|
108
|
+
@config_handler("memory")
|
109
|
+
def handle_memory(self, value):
|
110
|
+
self._qemu_machine.add_args("-m", value)
|
111
|
+
"""
|
112
|
+
|
113
|
+
def decorator(func: Callable):
|
114
|
+
# Store metadata on function for instance registration
|
115
|
+
func._config_handler_key = config_key
|
116
|
+
|
117
|
+
# Also register globally for backward compatibility
|
118
|
+
_config_registry.register_handler(config_key, func)
|
119
|
+
|
120
|
+
return func
|
121
|
+
|
122
|
+
return decorator
|
123
|
+
|
124
|
+
|
125
|
+
class ConfigurableMachine:
|
126
|
+
"""
|
127
|
+
Mixin class providing extensible configuration handling.
|
128
|
+
|
129
|
+
Classes that inherit from this can use @config_handler decorators
|
130
|
+
to define how different configuration keys are processed.
|
131
|
+
|
132
|
+
Now supports instance-based config handler registries for:
|
133
|
+
- Parallel test execution without registry pollution
|
134
|
+
- Multiple Machine instances with different handlers
|
135
|
+
- Thread-safe operation
|
136
|
+
|
137
|
+
# NOTE: Good - decorator-based handler registration (@config_handler) is
|
138
|
+
# clean
|
139
|
+
# and allows adding new config keys without modifying core code.
|
140
|
+
# Very maintainable and extensible pattern.
|
141
|
+
# NOTE: Validates handlers exist for keys and fails fast on critical key
|
142
|
+
# errors.
|
143
|
+
# Warnings are issued for unhandled keys to help identify typos/deprecated
|
144
|
+
# keys.
|
145
|
+
"""
|
146
|
+
|
147
|
+
# Critical configuration keys that must be processed successfully
|
148
|
+
# Errors in these keys will cause VM creation to fail immediately
|
149
|
+
CRITICAL_CONFIG_KEYS = {
|
150
|
+
"binary", # QEMU binary path - VM cannot start without it
|
151
|
+
"storage", # Storage configuration - must succeed to avoid data loss
|
152
|
+
"arguments", # Core QEMU arguments - failures could break VM
|
153
|
+
}
|
154
|
+
|
155
|
+
def __init__(self, *args, **kwargs):
|
156
|
+
"""
|
157
|
+
Initialize ConfigurableMachine with instance-specific config registry.
|
158
|
+
|
159
|
+
Creates an isolated config handler registry for this instance,
|
160
|
+
preventing cross-contamination between instances and enabling
|
161
|
+
parallel test execution.
|
162
|
+
"""
|
163
|
+
# Call parent __init__ if it exists (for multiple inheritance)
|
164
|
+
super().__init__(*args, **kwargs)
|
165
|
+
|
166
|
+
# Create instance-specific config handler registry
|
167
|
+
self._config_handler_registry = ConfigHandlerRegistry()
|
168
|
+
self._config_handler_registry.register_from_instance(self)
|
169
|
+
|
170
|
+
def process_configuration(self, config_data: Dict[str, Any]):
|
171
|
+
"""
|
172
|
+
Process configuration data using registered handlers.
|
173
|
+
|
174
|
+
Uses instance-specific registry (falls back to global for compatibility).
|
175
|
+
|
176
|
+
Args:
|
177
|
+
config_data: Configuration dictionary
|
178
|
+
|
179
|
+
Validates that handlers exist for all keys and fails fast on errors
|
180
|
+
for critical keys. Warns about unhandled keys that might be typos.
|
181
|
+
"""
|
182
|
+
processed_keys = []
|
183
|
+
|
184
|
+
# Use instance-specific registry (falls back to global if not available)
|
185
|
+
registry = getattr(self, '_config_handler_registry', _config_registry)
|
186
|
+
|
187
|
+
# Identify unhandled keys to warn about potential typos or deprecated
|
188
|
+
# keys
|
189
|
+
all_keys = set(config_data.keys())
|
190
|
+
registered_keys = set(registry.get_registered_keys())
|
191
|
+
unhandled_keys = all_keys - registered_keys
|
192
|
+
|
193
|
+
# Warn about unhandled keys (potential typos/deprecated keys)
|
194
|
+
if unhandled_keys:
|
195
|
+
LOG.warning(
|
196
|
+
f"Configuration contains unhandled keys (potential typos or deprecated): "
|
197
|
+
f"{', '.join(sorted(unhandled_keys))}. "
|
198
|
+
f"Valid keys: {', '.join(sorted(registered_keys))}"
|
199
|
+
)
|
200
|
+
|
201
|
+
# Iterate over a list of items to allow handlers to modify config_data
|
202
|
+
for config_key, config_value in list(config_data.items()):
|
203
|
+
handler = registry.get_handler(config_key)
|
204
|
+
if handler:
|
205
|
+
try:
|
206
|
+
# Check if handler is bound method or unbound function
|
207
|
+
# Instance registry: bound methods (already have self)
|
208
|
+
# Global registry: unbound functions (need self passed)
|
209
|
+
if hasattr(handler, '__self__'):
|
210
|
+
# Bound method - call directly
|
211
|
+
handler(config_value)
|
212
|
+
else:
|
213
|
+
# Unbound function - pass self explicitly
|
214
|
+
handler(self, config_value)
|
215
|
+
processed_keys.append(config_key)
|
216
|
+
LOG.debug(
|
217
|
+
f"Processed config key '{config_key}' with handler"
|
218
|
+
)
|
219
|
+
except Exception as e:
|
220
|
+
# Fail fast for critical configuration keys
|
221
|
+
if config_key in self.CRITICAL_CONFIG_KEYS:
|
222
|
+
LOG.error(
|
223
|
+
f"Critical config key '{
|
224
|
+
config_key}' failed to process: {e}. "
|
225
|
+
f"VM creation cannot continue with invalid {
|
226
|
+
config_key} configuration."
|
227
|
+
)
|
228
|
+
# Re-raise exception to stop VM creation immediately
|
229
|
+
raise
|
230
|
+
else:
|
231
|
+
# Non-critical keys can fail with warnings
|
232
|
+
LOG.warning(
|
233
|
+
f"Error processing config key '{config_key}': {e}"
|
234
|
+
)
|
235
|
+
else:
|
236
|
+
LOG.debug(
|
237
|
+
f"No handler for config key '{config_key}', ignoring"
|
238
|
+
)
|
239
|
+
|
240
|
+
LOG.debug(f"Processed configuration keys: {processed_keys}")
|
241
|
+
return processed_keys
|
242
|
+
|
243
|
+
def get_unhandled_keys(self, config_data: Dict[str, Any]) -> List[str]:
|
244
|
+
"""
|
245
|
+
Get list of configuration keys that have no registered handlers.
|
246
|
+
|
247
|
+
Uses instance-specific registry (falls back to global for compatibility).
|
248
|
+
|
249
|
+
Useful for identifying typos or deprecated keys in configurations.
|
250
|
+
|
251
|
+
Args:
|
252
|
+
config_data: Configuration dictionary
|
253
|
+
|
254
|
+
Returns:
|
255
|
+
List of unhandled configuration keys (sorted)
|
256
|
+
"""
|
257
|
+
# Use instance-specific registry (falls back to global if not available)
|
258
|
+
registry = getattr(self, '_config_handler_registry', _config_registry)
|
259
|
+
|
260
|
+
all_keys = set(config_data.keys())
|
261
|
+
registered_keys = set(registry.get_registered_keys())
|
262
|
+
unhandled_keys = all_keys - registered_keys
|
263
|
+
return sorted(list(unhandled_keys))
|
264
|
+
|
265
|
+
@config_handler("binary")
|
266
|
+
def handle_binary(self, binary: str):
|
267
|
+
"""
|
268
|
+
Handle QEMU binary path configuration.
|
269
|
+
|
270
|
+
The binary path is already stored in config_data and used by
|
271
|
+
_create_qemu_machine(). This handler just registers 'binary' as
|
272
|
+
a valid configuration key to prevent warnings.
|
273
|
+
"""
|
274
|
+
# Binary path is used directly from config_data in machine.py
|
275
|
+
# No additional processing needed here
|
276
|
+
pass
|
277
|
+
|
278
|
+
@config_handler("memory")
|
279
|
+
def handle_memory(self, memory: str):
|
280
|
+
"""Handle memory configuration."""
|
281
|
+
if hasattr(self, "_qemu_machine") and self._qemu_machine:
|
282
|
+
self._qemu_machine.add_args("-m", memory)
|
283
|
+
|
284
|
+
@config_handler("cpu")
|
285
|
+
def handle_cpu(self, cpu: int):
|
286
|
+
"""Handle CPU configuration."""
|
287
|
+
if hasattr(self, "_qemu_machine") and self._qemu_machine:
|
288
|
+
self._qemu_machine.add_args("-smp", str(cpu))
|
289
|
+
|
290
|
+
@config_handler("display")
|
291
|
+
def handle_display(self, display: str):
|
292
|
+
"""Handle display configuration."""
|
293
|
+
if hasattr(self, "_qemu_machine") and self._qemu_machine:
|
294
|
+
self._qemu_machine.add_args("-display", display)
|
295
|
+
|
296
|
+
@config_handler("vga")
|
297
|
+
def handle_vga(self, vga: str):
|
298
|
+
"""Handle VGA configuration."""
|
299
|
+
if hasattr(self, "_qemu_machine") and self._qemu_machine:
|
300
|
+
self._qemu_machine.add_args("-vga", vga)
|
301
|
+
|
302
|
+
@config_handler("args")
|
303
|
+
def handle_args(self, args: List[str]):
|
304
|
+
"""Handle additional QEMU arguments (simple list format)."""
|
305
|
+
if hasattr(self, "_qemu_machine") and self._qemu_machine and args:
|
306
|
+
self._qemu_machine.add_args(*args)
|
307
|
+
|
308
|
+
@staticmethod
|
309
|
+
def _format_nested_value(value, stack=None):
|
310
|
+
"""
|
311
|
+
Recursively format nested dicts/lists as QEMU suboptions.
|
312
|
+
|
313
|
+
# TODO(architect, 2025-10-10): [REFACTOR] Method too complex (101 lines, high cyclomatic complexity)
|
314
|
+
# Context: Recursive method with complex nested conditionals. Hard to understand and test.
|
315
|
+
# Issue #13 in ARCHITECTURAL_REVIEW.md.
|
316
|
+
#
|
317
|
+
# Recommendation: Break into smaller methods:
|
318
|
+
# - _is_empty_value(), _format_empty_value()
|
319
|
+
# - _is_primitive(), _format_primitive()
|
320
|
+
# - _format_list(), _format_dict()
|
321
|
+
#
|
322
|
+
# Effort: Small (2-3 hours)
|
323
|
+
# Priority: Medium
|
324
|
+
# See: ARCHITECTURAL_REVIEW.md Issue #13
|
325
|
+
|
326
|
+
Supports WYSIWYG (What You See Is What You Get) argument parsing
|
327
|
+
with no implicit special keys. Handles arbitrary nesting levels
|
328
|
+
with dot notation.
|
329
|
+
|
330
|
+
Args:
|
331
|
+
value: Value to format (dict, list, or primitive)
|
332
|
+
stack: Current nesting path for dot notation (e.g., ['device', 'net'])
|
333
|
+
|
334
|
+
Returns:
|
335
|
+
Formatted string suitable for QEMU arguments
|
336
|
+
|
337
|
+
Format Examples:
|
338
|
+
Format 1 (key only):
|
339
|
+
value: 'foo' -> 'foo'
|
340
|
+
|
341
|
+
Format 2 (key-value):
|
342
|
+
value: {'a': 1, 'b': 2} -> 'a=1,b=2'
|
343
|
+
|
344
|
+
Format 3 (nested key-values):
|
345
|
+
value: {'bar': 42, 'baz': 42} -> 'bar=42,baz=42'
|
346
|
+
|
347
|
+
Format 4 (value and key-values - handled by caller):
|
348
|
+
{display: 'gtk', 'zoom-to-fit': 'on'} -> 'gtk,zoom-to-fit=on'
|
349
|
+
This is handled in handle_arguments() for multi-key dicts
|
350
|
+
|
351
|
+
Format 5 (deep nesting):
|
352
|
+
{'bar': {'baz': {'spam': 1}}} -> 'bar.baz.spam=1'
|
353
|
+
|
354
|
+
Empty nested values (key without value):
|
355
|
+
{'gtk': None} -> 'gtk'
|
356
|
+
{'gtk': {}} -> 'gtk'
|
357
|
+
"""
|
358
|
+
if stack is None:
|
359
|
+
stack = []
|
360
|
+
|
361
|
+
# Base case: None or empty dict means this is a key without value
|
362
|
+
# Example: {gtk: None} or {gtk: {}} -> just 'gtk'
|
363
|
+
if value is None or (isinstance(value, dict) and not value):
|
364
|
+
if stack:
|
365
|
+
return ".".join(stack)
|
366
|
+
return ""
|
367
|
+
|
368
|
+
# Base case: primitive value (string, int, bool)
|
369
|
+
if not isinstance(value, (list, dict)):
|
370
|
+
if stack:
|
371
|
+
# We have a nesting path, format as: stack.path=value
|
372
|
+
return ".".join(stack) + f"={value}"
|
373
|
+
else:
|
374
|
+
# No nesting, return value as-is
|
375
|
+
return str(value)
|
376
|
+
|
377
|
+
# Recursive case: collections
|
378
|
+
options = []
|
379
|
+
|
380
|
+
if isinstance(value, list):
|
381
|
+
# Lists: each item becomes a separate option
|
382
|
+
for item in value:
|
383
|
+
if isinstance(item, (list, dict)):
|
384
|
+
# Nested structure within list
|
385
|
+
formatted = ConfigurableMachine._format_nested_value(
|
386
|
+
item, stack
|
387
|
+
)
|
388
|
+
if formatted:
|
389
|
+
options.append(formatted)
|
390
|
+
else:
|
391
|
+
# Simple value in list
|
392
|
+
if stack:
|
393
|
+
options.append(".".join(stack) + f".{item}")
|
394
|
+
else:
|
395
|
+
options.append(str(item))
|
396
|
+
|
397
|
+
elif isinstance(value, dict):
|
398
|
+
# Dicts: process each key-value pair
|
399
|
+
# Order is preserved (Python 3.7+ guarantees dict order)
|
400
|
+
for k, v in value.items():
|
401
|
+
if v is None or (isinstance(v, dict) and not v):
|
402
|
+
# Empty value: key without assignment
|
403
|
+
# Example: {gtk: None} -> 'gtk' not 'gtk='
|
404
|
+
if stack:
|
405
|
+
options.append(".".join(stack + [k]))
|
406
|
+
else:
|
407
|
+
options.append(k)
|
408
|
+
elif isinstance(v, (list, dict)):
|
409
|
+
# Nested structure: recurse with extended stack
|
410
|
+
formatted = ConfigurableMachine._format_nested_value(
|
411
|
+
v, stack + [k]
|
412
|
+
)
|
413
|
+
if formatted:
|
414
|
+
options.append(formatted)
|
415
|
+
else:
|
416
|
+
# Simple value: format as key=value or stack.key=value
|
417
|
+
if stack:
|
418
|
+
options.append(".".join(stack) + f".{k}={v}")
|
419
|
+
else:
|
420
|
+
options.append(f"{k}={v}")
|
421
|
+
|
422
|
+
return ",".join(options)
|
423
|
+
|
424
|
+
@config_handler("arguments")
|
425
|
+
def handle_arguments(self, arguments: List):
|
426
|
+
"""
|
427
|
+
Handle structured QEMU arguments from config.
|
428
|
+
|
429
|
+
Args:
|
430
|
+
arguments: List of dicts or strings representing QEMU arguments
|
431
|
+
Examples:
|
432
|
+
- {smp: 2} -> -smp 2
|
433
|
+
- {m: "2G"} -> -m 2G
|
434
|
+
- {enable-kvm: null} -> -enable-kvm
|
435
|
+
- "enable-kvm" -> -enable-kvm
|
436
|
+
"""
|
437
|
+
if not hasattr(self, "_qemu_machine") or not self._qemu_machine:
|
438
|
+
return
|
439
|
+
|
440
|
+
if not arguments:
|
441
|
+
return
|
442
|
+
|
443
|
+
# NOTE: Argument deduplication removed - broke multiple device/drive args
|
444
|
+
# Previously implemented deduplication that removed ALL duplicate keys,
|
445
|
+
# breaking multiple -device, -drive, -netdev arguments which QEMU requires.
|
446
|
+
# TODO: Implement whitelist-based deduplication (display, vga, memory only)
|
447
|
+
|
448
|
+
# Collect keys to mark in config_data (done after iteration to avoid
|
449
|
+
# RuntimeError)
|
450
|
+
config_updates = {}
|
451
|
+
|
452
|
+
# Process arguments
|
453
|
+
for arg_item in arguments:
|
454
|
+
if isinstance(arg_item, dict):
|
455
|
+
# Check if this is a multi-key dict (Format 4)
|
456
|
+
# Example: {display: gtk, zoom-to-fit: on} → -display gtk,zoom-to-fit=on
|
457
|
+
if len(arg_item) > 1:
|
458
|
+
# Multi-key dict: first key is arg name, its value is positional,
|
459
|
+
# remaining keys are suboptions
|
460
|
+
items = list(arg_item.items())
|
461
|
+
first_key, first_value = items[0]
|
462
|
+
arg_name = f"-{first_key}"
|
463
|
+
|
464
|
+
# Build argument value: positional_value,key1=val1,key2=val2
|
465
|
+
parts = []
|
466
|
+
|
467
|
+
# Add first value as positional (if not None)
|
468
|
+
if first_value is not None:
|
469
|
+
if isinstance(first_value, (dict, list)):
|
470
|
+
parts.append(
|
471
|
+
self._format_nested_value(first_value)
|
472
|
+
)
|
473
|
+
else:
|
474
|
+
parts.append(str(first_value))
|
475
|
+
|
476
|
+
# Add remaining key=value pairs as suboptions
|
477
|
+
for key, value in items[1:]:
|
478
|
+
if value is None or (isinstance(value, dict) and not value):
|
479
|
+
# Empty value: add key without assignment
|
480
|
+
parts.append(key)
|
481
|
+
elif isinstance(value, (dict, list)):
|
482
|
+
# Nested value: format recursively with stack context
|
483
|
+
# This ensures lists use dot notation: key.item1,key.item2
|
484
|
+
formatted = self._format_nested_value(value, stack=[key])
|
485
|
+
if formatted:
|
486
|
+
parts.append(formatted)
|
487
|
+
else:
|
488
|
+
parts.append(f"{key}={value}")
|
489
|
+
|
490
|
+
# Add the complete argument
|
491
|
+
if parts:
|
492
|
+
self._qemu_machine.add_args(arg_name, ",".join(parts))
|
493
|
+
else:
|
494
|
+
self._qemu_machine.add_args(arg_name)
|
495
|
+
|
496
|
+
# Collect config updates to prevent defaults (from first key only)
|
497
|
+
if first_key == "m":
|
498
|
+
config_updates["memory"] = str(first_value)
|
499
|
+
elif first_key == "smp":
|
500
|
+
config_updates["cpu"] = first_value
|
501
|
+
|
502
|
+
else:
|
503
|
+
# Single-key dict format: {key: value} or {key: null}
|
504
|
+
for key, value in arg_item.items():
|
505
|
+
# Convert key to QEMU argument format (add - prefix)
|
506
|
+
arg_name = f"-{key}"
|
507
|
+
|
508
|
+
if value is None:
|
509
|
+
# Format 1: Flag argument (e.g., {enable-kvm: null} → -enable-kvm)
|
510
|
+
self._qemu_machine.add_args(arg_name)
|
511
|
+
elif isinstance(value, dict):
|
512
|
+
# Check for Format 4 alternative syntax:
|
513
|
+
# {display: {gtk: null, zoom-to-fit: on}} should behave like
|
514
|
+
# {display: gtk, zoom-to-fit: on}
|
515
|
+
# i.e., first key with empty value becomes positional
|
516
|
+
if len(value) > 1:
|
517
|
+
items = list(value.items())
|
518
|
+
first_nested_key, first_nested_value = items[0]
|
519
|
+
|
520
|
+
if first_nested_value is None or (isinstance(first_nested_value, dict) and not first_nested_value):
|
521
|
+
# First nested key has empty value: treat as positional
|
522
|
+
# Example: {display: {gtk: null, zoom-to-fit: on}} → -display gtk,zoom-to-fit=on
|
523
|
+
parts = [first_nested_key]
|
524
|
+
|
525
|
+
# Add remaining key=value pairs
|
526
|
+
for nested_key, nested_value in items[1:]:
|
527
|
+
if nested_value is None or (isinstance(nested_value, dict) and not nested_value):
|
528
|
+
parts.append(nested_key)
|
529
|
+
elif isinstance(nested_value, (dict, list)):
|
530
|
+
# Use stack context for dot notation
|
531
|
+
formatted = self._format_nested_value(nested_value, stack=[nested_key])
|
532
|
+
if formatted:
|
533
|
+
parts.append(formatted)
|
534
|
+
else:
|
535
|
+
parts.append(f"{nested_key}={nested_value}")
|
536
|
+
|
537
|
+
self._qemu_machine.add_args(arg_name, ",".join(parts))
|
538
|
+
else:
|
539
|
+
# Regular nested dict: Format 3 or Format 5
|
540
|
+
# Example: {foo: {bar: 42, baz: 42}} → -foo bar=42,baz=42
|
541
|
+
formatted = self._format_nested_value(value)
|
542
|
+
if formatted:
|
543
|
+
self._qemu_machine.add_args(arg_name, formatted)
|
544
|
+
else:
|
545
|
+
self._qemu_machine.add_args(arg_name)
|
546
|
+
else:
|
547
|
+
# Single-key nested dict: Format 3 or Format 5
|
548
|
+
# Example: {display: {gtk: null}} → -display gtk
|
549
|
+
formatted = self._format_nested_value(value)
|
550
|
+
if formatted:
|
551
|
+
self._qemu_machine.add_args(arg_name, formatted)
|
552
|
+
else:
|
553
|
+
self._qemu_machine.add_args(arg_name)
|
554
|
+
elif isinstance(value, list):
|
555
|
+
# List: format as comma-separated values
|
556
|
+
# Example: {arg: [foo, bar]} → -arg foo,bar
|
557
|
+
formatted = self._format_nested_value(value)
|
558
|
+
if formatted:
|
559
|
+
self._qemu_machine.add_args(arg_name, formatted)
|
560
|
+
else:
|
561
|
+
self._qemu_machine.add_args(arg_name)
|
562
|
+
else:
|
563
|
+
# Format 2: Simple value (string/int)
|
564
|
+
# Example: {smp: 2} → -smp 2
|
565
|
+
self._qemu_machine.add_args(arg_name, str(value))
|
566
|
+
|
567
|
+
# Collect config updates to prevent defaults
|
568
|
+
if key == "m":
|
569
|
+
config_updates["memory"] = str(value)
|
570
|
+
elif key == "smp":
|
571
|
+
config_updates["cpu"] = value
|
572
|
+
|
573
|
+
elif isinstance(arg_item, str):
|
574
|
+
# String format: "enable-kvm" -> -enable-kvm
|
575
|
+
arg_name = (
|
576
|
+
f"-{arg_item}"
|
577
|
+
if not arg_item.startswith("-")
|
578
|
+
else arg_item
|
579
|
+
)
|
580
|
+
self._qemu_machine.add_args(arg_name)
|
581
|
+
else:
|
582
|
+
LOG.warning(
|
583
|
+
f"Invalid argument format: {arg_item}. "
|
584
|
+
"Expected dict or string."
|
585
|
+
)
|
586
|
+
|
587
|
+
# Apply config updates after iteration to prevent RuntimeError
|
588
|
+
if hasattr(self, "config_data") and config_updates:
|
589
|
+
self.config_data.update(config_updates)
|
590
|
+
|
591
|
+
@config_handler("storage")
|
592
|
+
def handle_storage(self, storage_config: List[Dict[str, Any]]):
|
593
|
+
"""Handle storage configuration using storage manager."""
|
594
|
+
# This will be handled by the storage manager integration
|
595
|
+
if hasattr(self, "_add_storage_devices"):
|
596
|
+
self._add_storage_devices()
|
597
|
+
|
598
|
+
def apply_default_configuration(self):
|
599
|
+
"""
|
600
|
+
Apply minimal required configuration for maqet functionality.
|
601
|
+
|
602
|
+
Philosophy: Provide mechanism, not policy (Unix philosophy).
|
603
|
+
|
604
|
+
QEMU has perfectly good defaults. Maqet only adds arguments required
|
605
|
+
for maqet itself to function (QMP socket, console setup).
|
606
|
+
|
607
|
+
All other configuration comes from:
|
608
|
+
1. User config (explicit)
|
609
|
+
2. QEMU's own defaults (implicit)
|
610
|
+
|
611
|
+
No opinionated defaults for memory, CPU, network, display, etc.
|
612
|
+
Users configure these explicitly if they want something different
|
613
|
+
from QEMU's defaults.
|
614
|
+
|
615
|
+
QEMU defaults (when no arguments provided):
|
616
|
+
- Memory: Architecture-specific default (typically 128MB)
|
617
|
+
- CPU: 1 core
|
618
|
+
- Display: GTK/SDL if available, else none
|
619
|
+
- Network: None (no network by default)
|
620
|
+
- VGA: Default VGA for architecture
|
621
|
+
"""
|
622
|
+
# NOTE: QMP socket and console setup are handled by
|
623
|
+
# MaqetQEMUMachine._base_args - these are required for maqet to
|
624
|
+
# communicate with QEMU and are not "policy" defaults.
|
625
|
+
pass
|
626
|
+
|
627
|
+
|
628
|
+
def register_config_handler(config_key: str, handler: Callable):
|
629
|
+
"""
|
630
|
+
Register a configuration handler programmatically.
|
631
|
+
|
632
|
+
Args:
|
633
|
+
config_key: Configuration key to handle
|
634
|
+
handler: Handler function
|
635
|
+
|
636
|
+
This allows external code to register handlers without using decorators.
|
637
|
+
"""
|
638
|
+
_config_registry.register_handler(config_key, handler)
|
639
|
+
|
640
|
+
|
641
|
+
def get_registered_config_keys() -> List[str]:
|
642
|
+
"""Get list of all registered configuration keys."""
|
643
|
+
return _config_registry.get_registered_keys()
|
644
|
+
|
645
|
+
|
646
|
+
def get_registered_handlers() -> List[str]:
|
647
|
+
"""
|
648
|
+
Get list of all registered config handler names.
|
649
|
+
|
650
|
+
Returns:
|
651
|
+
List of registered configuration handler names (sorted)
|
652
|
+
|
653
|
+
Example:
|
654
|
+
>>> handlers = get_registered_handlers()
|
655
|
+
>>> print(handlers)
|
656
|
+
['arguments', 'binary', 'cpu', 'display', 'memory', 'storage', 'vga']
|
657
|
+
"""
|
658
|
+
return sorted(_config_registry.get_registered_keys())
|
659
|
+
|
660
|
+
|
661
|
+
def validate_critical_handlers() -> None:
|
662
|
+
"""
|
663
|
+
Ensure critical configuration handlers are registered.
|
664
|
+
|
665
|
+
Checks that required handlers exist at startup to fail fast
|
666
|
+
if core functionality is missing.
|
667
|
+
|
668
|
+
Raises:
|
669
|
+
RuntimeError: If any critical handlers are missing
|
670
|
+
|
671
|
+
Example:
|
672
|
+
>>> validate_critical_handlers() # Succeeds if all critical handlers exist
|
673
|
+
>>> # Raises RuntimeError if 'binary' handler is missing
|
674
|
+
"""
|
675
|
+
critical = ['binary', 'storage', 'arguments']
|
676
|
+
registered = set(_config_registry.get_registered_keys())
|
677
|
+
missing = [h for h in critical if h not in registered]
|
678
|
+
|
679
|
+
if missing:
|
680
|
+
raise RuntimeError(
|
681
|
+
f"Missing critical config handlers: {', '.join(missing)}. "
|
682
|
+
f"This indicates a code registration issue. "
|
683
|
+
f"Registered handlers: {', '.join(sorted(registered))}"
|
684
|
+
)
|