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,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
+ )