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,519 @@
1
+ """
2
+ Schema Configuration Validators.
3
+
4
+ This module implements a decorator-based schema validation system for YAML configs.
5
+ Functions decorated with @config_validator automatically validate and normalize
6
+ configuration values during the parsing phase.
7
+
8
+ Key Features:
9
+ - Type coercion and value normalization (e.g., bytes to "4G")
10
+ - Cross-field validation (e.g., display/VGA compatibility)
11
+ - Extensible via @config_validator decorator
12
+ - Forward-compatible (unknown keys preserved)
13
+
14
+ Separation of Concerns:
15
+ - config.validators: Schema validation + value normalization (THIS MODULE)
16
+ - validation.ConfigValidator: Runtime health checks + pre-start validation
17
+
18
+ Use this module for:
19
+ - Validating config structure and types
20
+ - Normalizing values (e.g., converting bytes to "4G", splitting comma-separated tags)
21
+ - Cross-field validation (e.g., display/VGA compatibility)
22
+
23
+ For runtime health checks, see maqet.validation.ConfigValidator module.
24
+
25
+ Example:
26
+ @config_validator('memory')
27
+ def validate_memory(value: Any) -> str:
28
+ # Validation logic here
29
+ return normalized_value
30
+
31
+ @config_validator('cpu', required=True)
32
+ def validate_cpu_count(value: Any) -> int:
33
+ # Validation logic here
34
+ return normalized_value
35
+ """
36
+
37
+ import os
38
+ import re
39
+ import warnings
40
+ from functools import wraps
41
+ from typing import Any, Callable, Dict, List, Optional, Set, TypeVar, Union
42
+
43
+ # Global registry for config validators
44
+ _VALIDATOR_REGISTRY: Dict[str, "ConfigValidator"] = {}
45
+
46
+ T = TypeVar("T")
47
+
48
+
49
+ class ConfigValidationError(Exception):
50
+ """Configuration validation error."""
51
+
52
+
53
+ class ConfigValidator:
54
+ """Metadata for a configuration validator."""
55
+
56
+ def __init__(
57
+ self,
58
+ key: str,
59
+ func: Callable[[Any], Any],
60
+ required: bool = False,
61
+ description: Optional[str] = None,
62
+ ):
63
+ """Initialize a configuration validator.
64
+
65
+ Args:
66
+ key: Configuration key this validator handles
67
+ func: Validation function to apply
68
+ required: Whether this key is required in config
69
+ description: Description of the validator
70
+ """
71
+ self.key = key
72
+ self.func = func
73
+ self.required = required
74
+ self.description = description or func.__doc__
75
+
76
+ def validate(self, value: Any) -> Any:
77
+ """Run validation on a value."""
78
+ try:
79
+ return self.func(value)
80
+ except Exception as e:
81
+ raise ConfigValidationError(
82
+ f"Validation failed for '{self.key}': {e}"
83
+ )
84
+
85
+
86
+ def config_validator(
87
+ key: str, required: bool = False, description: Optional[str] = None
88
+ ) -> Callable[[Callable[[Any], T]], Callable[[Any], T]]:
89
+ """
90
+ Register a config validation function.
91
+
92
+ Args:
93
+ key: Configuration key this validator handles
94
+ required: Whether this key is required in config
95
+ description: Description of the validator
96
+
97
+ Returns:
98
+ Decorated function
99
+ """
100
+
101
+ def decorator(func: Callable[[Any], T]) -> Callable[[Any], T]:
102
+ validator = ConfigValidator(key, func, required, description)
103
+ _VALIDATOR_REGISTRY[key] = validator
104
+
105
+ @wraps(func)
106
+ def wrapper(value: Any) -> T:
107
+ return validator.validate(value)
108
+
109
+ return wrapper
110
+
111
+ return decorator
112
+
113
+
114
+ def get_validators() -> Dict[str, ConfigValidator]:
115
+ """Get all registered validators."""
116
+ return _VALIDATOR_REGISTRY.copy()
117
+
118
+
119
+ def get_required_keys() -> Set[str]:
120
+ """Get all required configuration keys."""
121
+ return {
122
+ key
123
+ for key, validator in _VALIDATOR_REGISTRY.items()
124
+ if validator.required
125
+ }
126
+
127
+
128
+ def validate_config_data(config_data: Dict[str, Any]) -> Dict[str, Any]:
129
+ """
130
+ Validate configuration data using registered validators.
131
+
132
+ Args:
133
+ config_data: Configuration dictionary
134
+
135
+ Returns:
136
+ Validated and normalized configuration
137
+
138
+ Raises:
139
+ ConfigValidationError: If validation fails
140
+ """
141
+ if not isinstance(config_data, dict):
142
+ raise ConfigValidationError("Configuration must be a dictionary")
143
+
144
+ validated_config = {}
145
+ required_keys = get_required_keys()
146
+
147
+ # Check for missing required keys
148
+ missing_required = required_keys - set(config_data.keys())
149
+ if missing_required:
150
+ raise ConfigValidationError(
151
+ f"Missing required configuration keys: {', '.join(missing_required)}"
152
+ )
153
+
154
+ # Check for API commands in config (configs should not contain API commands)
155
+ # Use hardcoded list of known API commands for reliability in tests
156
+ KNOWN_API_COMMANDS = {
157
+ "add", "start", "stop", "rm", "ls", "status", "info", "inspect",
158
+ "qmp", "snapshot", "config", "apply", "dump", "history", "import"
159
+ }
160
+
161
+ # Also try to get commands from registry if available
162
+ try:
163
+ from ..api.registry import API_REGISTRY
164
+ registry_commands = set(API_REGISTRY.get_all_cli_commands().keys())
165
+ # Combine hardcoded list with registry (handles both test and production cases)
166
+ api_commands = KNOWN_API_COMMANDS | registry_commands
167
+ except Exception:
168
+ # If registry access fails, use hardcoded list only
169
+ api_commands = KNOWN_API_COMMANDS
170
+
171
+ config_keys = set(config_data.keys())
172
+ api_commands_in_config = config_keys & api_commands
173
+ if api_commands_in_config:
174
+ raise ConfigValidationError(
175
+ f"Configuration cannot contain API commands: {', '.join(sorted(api_commands_in_config))}. "
176
+ f"Use CLI or Python API to execute commands."
177
+ )
178
+
179
+ # Validate each key that has a validator
180
+ for key, value in config_data.items():
181
+ if key in _VALIDATOR_REGISTRY:
182
+ # Key has a validator, run validation
183
+ validator = _VALIDATOR_REGISTRY[key]
184
+ try:
185
+ validated_config[key] = validator.validate(value)
186
+ except ConfigValidationError:
187
+ raise
188
+ except Exception as e:
189
+ raise ConfigValidationError(
190
+ f"Validation failed for '{key}': {e}"
191
+ )
192
+ else:
193
+ # No validator for this key, include as-is
194
+ # This allows forward compatibility with new config keys
195
+ validated_config[key] = value
196
+
197
+ # Run cross-field validation checks
198
+ validate_display_vga_compatibility(validated_config)
199
+
200
+ return validated_config
201
+
202
+
203
+ # Built-in validators for common configuration keys
204
+
205
+
206
+ @config_validator("binary", required=False)
207
+ def validate_binary(value: Any) -> str:
208
+ """
209
+ Validate QEMU binary path exists and is executable.
210
+
211
+ Note: Binary is not strictly required because machine.py provides a default
212
+ (/usr/bin/qemu-system-x86_64). However, if specified, it must be valid.
213
+ """
214
+ from pathlib import Path
215
+
216
+ if not isinstance(value, str):
217
+ raise ValueError("Binary path must be a string")
218
+ if not value.strip():
219
+ raise ValueError("Binary path cannot be empty")
220
+
221
+ binary_path = Path(value.strip())
222
+
223
+ # Check if file exists
224
+ if not binary_path.exists():
225
+ raise ValueError(
226
+ f"QEMU binary not found: {value}. "
227
+ f"Install QEMU or provide correct binary path."
228
+ )
229
+
230
+ # Check if it's a file (not a directory)
231
+ if not binary_path.is_file():
232
+ raise ValueError(f"QEMU binary path is not a file: {value}")
233
+
234
+ # Check if executable
235
+ if not os.access(binary_path, os.X_OK):
236
+ raise ValueError(
237
+ f"QEMU binary is not executable: {value}. "
238
+ f"Run: chmod +x {value}"
239
+ )
240
+
241
+ return value.strip()
242
+
243
+
244
+ @config_validator("memory")
245
+ def validate_memory(value: Any) -> str:
246
+ """Validate memory specification (e.g., '4G', '2048M')."""
247
+ if isinstance(value, int):
248
+ # Convert bytes to megabytes
249
+ return f"{value // (1024 * 1024)}M"
250
+
251
+ if not isinstance(value, str):
252
+ raise ValueError("Memory must be a string or integer")
253
+
254
+ memory = value.strip()
255
+
256
+ # Check for valid memory format
257
+ if re.match(r"^\d+[MGT]$", memory):
258
+ return memory
259
+ elif memory.isdigit():
260
+ # Assume bytes, convert to megabytes
261
+ return f"{int(memory) // (1024 * 1024)}M"
262
+ else:
263
+ raise ValueError(
264
+ "Memory must be in format like '4G', '2048M', or bytes as integer"
265
+ )
266
+
267
+
268
+ @config_validator("cpu")
269
+ def validate_cpu(value: Any) -> int:
270
+ """Validate CPU count and warn if exceeds system CPUs."""
271
+ try:
272
+ cpu_count = int(value)
273
+ if cpu_count < 1:
274
+ raise ValueError("CPU count must be at least 1")
275
+ if cpu_count > 64:
276
+ raise ValueError("CPU count cannot exceed 64")
277
+
278
+ # Warn if CPU count exceeds available system CPUs
279
+ try:
280
+ system_cpus = os.cpu_count() or 1
281
+ if cpu_count > system_cpus:
282
+ warnings.warn(
283
+ f"VM configured with {cpu_count} CPUs but system only has "
284
+ f"{system_cpus} CPUs. This may impact performance. "
285
+ f"Consider reducing CPU count to {system_cpus} or less.",
286
+ UserWarning
287
+ )
288
+ except Exception:
289
+ # Can't determine system CPU count - skip warning
290
+ pass
291
+
292
+ return cpu_count
293
+ except (ValueError, TypeError):
294
+ raise ValueError("CPU count must be a positive integer")
295
+
296
+
297
+ @config_validator("display")
298
+ def validate_display(value: Any) -> str:
299
+ """Validate display setting."""
300
+ if not isinstance(value, str):
301
+ raise ValueError("Display must be a string")
302
+
303
+ valid_displays = {"none", "gtk", "sdl", "vnc", "curses", "spice", "cocoa"}
304
+
305
+ # Also accept format like "gtk,gl=on" or "vnc=:1"
306
+ display_type = value.split(",")[0].split("=")[0]
307
+
308
+ if display_type not in valid_displays:
309
+ warnings.warn(
310
+ f"Display type '{display_type}' may not be supported by QEMU. "
311
+ f"Common types: {', '.join(sorted(valid_displays))}",
312
+ UserWarning
313
+ )
314
+
315
+ return value
316
+
317
+
318
+ @config_validator("storage")
319
+ def validate_storage(value: Any) -> List[Dict[str, Any]]:
320
+ """Validate storage devices configuration."""
321
+ if not isinstance(value, list):
322
+ raise ValueError("Storage must be a list of device configurations")
323
+
324
+ validated_storage = []
325
+ seen_names = set()
326
+
327
+ for i, device in enumerate(value):
328
+ if not isinstance(device, dict):
329
+ raise ValueError(f"Storage device {i} must be a dictionary")
330
+
331
+ # Auto-generate name if not provided (for backward compatibility)
332
+ validated_device = device.copy()
333
+ if "name" not in validated_device:
334
+ # Generate name from index: hdd0, hdd1, hdd2, etc.
335
+ validated_device["name"] = f"hdd{i}"
336
+
337
+ device_name = validated_device["name"]
338
+ if device_name in seen_names:
339
+ raise ValueError(f"Duplicate storage device name: {device_name}")
340
+ seen_names.add(device_name)
341
+
342
+ # Validate device type
343
+ device_type = validated_device.get("type", "qcow2")
344
+ valid_types = {"qcow2", "raw", "vmdk", "vdi", "vhd", "virtfs"}
345
+ if device_type not in valid_types:
346
+ raise ValueError(
347
+ f"Storage device {i} type must be one of {valid_types}"
348
+ )
349
+
350
+ validated_device["type"] = device_type
351
+ validated_storage.append(validated_device)
352
+
353
+ return validated_storage
354
+
355
+
356
+ @config_validator("network")
357
+ def validate_network(value: Any) -> List[Dict[str, Any]]:
358
+ """Validate network configuration."""
359
+ if not isinstance(value, list):
360
+ raise ValueError("Network must be a list of network configurations")
361
+
362
+ # Basic validation - can be expanded
363
+ return value
364
+
365
+
366
+ @config_validator("arguments")
367
+ def validate_arguments(value: Any) -> List[Dict[str, Any]]:
368
+ """Validate structured QEMU arguments."""
369
+ if not isinstance(value, list):
370
+ raise ValueError("Arguments must be a list of argument dictionaries")
371
+
372
+ return value
373
+
374
+
375
+ @config_validator("plain_arguments")
376
+ def validate_plain_arguments(value: Any) -> List[str]:
377
+ """Validate plain QEMU arguments."""
378
+ if isinstance(value, str):
379
+ # Split string into list
380
+ return value.split()
381
+ elif isinstance(value, list):
382
+ # Validate all items are strings
383
+ for i, arg in enumerate(value):
384
+ if not isinstance(arg, str):
385
+ raise ValueError(f"Plain argument {i} must be a string")
386
+ return value
387
+ else:
388
+ raise ValueError("Plain arguments must be a string or list of strings")
389
+
390
+
391
+ @config_validator("parameters")
392
+ def validate_parameters(value: Any) -> Dict[str, Any]:
393
+ """Validate user-defined parameters."""
394
+ if not isinstance(value, dict):
395
+ raise ValueError("Parameters must be a dictionary")
396
+
397
+ return value
398
+
399
+
400
+ @config_validator("description")
401
+ def validate_description(value: Any) -> str:
402
+ """Validate VM description."""
403
+ if not isinstance(value, str):
404
+ raise ValueError("Description must be a string")
405
+
406
+ return value.strip()
407
+
408
+
409
+ @config_validator("tags")
410
+ def validate_tags(value: Any) -> List[str]:
411
+ """Validate VM tags."""
412
+ if isinstance(value, str):
413
+ # Split comma-separated tags
414
+ return [tag.strip() for tag in value.split(",") if tag.strip()]
415
+ elif isinstance(value, list):
416
+ # Validate all items are strings
417
+ tags = []
418
+ for tag in value:
419
+ if not isinstance(tag, str):
420
+ raise ValueError("All tags must be strings")
421
+ tags.append(tag.strip())
422
+ return tags
423
+ else:
424
+ raise ValueError("Tags must be a string or list of strings")
425
+
426
+
427
+ @config_validator("vga")
428
+ def validate_vga(value: Any) -> str:
429
+ """Validate VGA device type."""
430
+ if not isinstance(value, str):
431
+ raise ValueError("VGA device type must be a string")
432
+
433
+ valid_vga = {
434
+ "std",
435
+ "cirrus",
436
+ "vmware",
437
+ "qxl",
438
+ "virtio",
439
+ "virtio-gpu-pci",
440
+ "none",
441
+ }
442
+
443
+ if value not in valid_vga:
444
+ warnings.warn(
445
+ f"VGA device type '{value}' may not be supported by QEMU. "
446
+ f"Common types: {', '.join(sorted(valid_vga))}",
447
+ UserWarning
448
+ )
449
+
450
+ return value
451
+
452
+
453
+ @config_validator("network")
454
+ def validate_network_config(value: Any) -> Dict[str, Any]:
455
+ """Validate network configuration."""
456
+ if isinstance(value, dict):
457
+ # Check network mode
458
+ mode = value.get("mode", "user")
459
+ valid_modes = {"user", "tap", "bridge", "none"}
460
+ if mode not in valid_modes:
461
+ warnings.warn(
462
+ f"Network mode '{mode}' may not be supported. "
463
+ f"Common modes: {', '.join(sorted(valid_modes))}",
464
+ UserWarning
465
+ )
466
+
467
+ # Validate MAC address format if provided
468
+ if "mac" in value:
469
+ mac = value["mac"]
470
+ if not re.match(r"^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$", mac):
471
+ raise ValueError(
472
+ f"Invalid MAC address format: '{mac}'. "
473
+ f"Use format like '52:54:00:12:34:56'"
474
+ )
475
+
476
+ # Warn about tap mode requiring privileges
477
+ if mode == "tap":
478
+ warnings.warn(
479
+ "Network mode 'tap' typically requires root privileges or "
480
+ "proper permissions on /dev/net/tun. Ensure you have the necessary access.",
481
+ UserWarning
482
+ )
483
+
484
+ return value
485
+ elif isinstance(value, list):
486
+ # List of network configurations - validate each
487
+ validated = []
488
+ for net_config in value:
489
+ validated.append(validate_network_config(net_config))
490
+ return validated
491
+ else:
492
+ raise ValueError("Network must be a dictionary or list of dictionaries")
493
+
494
+
495
+ def validate_display_vga_compatibility(config_data: Dict[str, Any]) -> None:
496
+ """
497
+ Validate display/VGA compatibility.
498
+
499
+ Warns about potentially inefficient configurations like headless display
500
+ with graphical VGA device.
501
+
502
+ Args:
503
+ config_data: Full configuration dictionary
504
+
505
+ Note: This is called by validate_config_data() after individual validators.
506
+ """
507
+ display = config_data.get("display", "")
508
+ vga = config_data.get("vga", "")
509
+
510
+ if (
511
+ display == "none"
512
+ and vga
513
+ and vga not in {"none", "virtio", "virtio-gpu-pci"}
514
+ ):
515
+ warnings.warn(
516
+ f"Display is 'none' but VGA is '{vga}'. Consider using vga=none "
517
+ f"for headless VMs to reduce resource usage.",
518
+ UserWarning
519
+ )