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