maqet 0.0.1.4__py3-none-any.whl → 0.0.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. maqet/__init__.py +50 -6
  2. maqet/__main__.py +96 -0
  3. maqet/__version__.py +3 -0
  4. maqet/api/__init__.py +35 -0
  5. maqet/api/decorators.py +184 -0
  6. maqet/api/metadata.py +147 -0
  7. maqet/api/registry.py +182 -0
  8. maqet/cli.py +71 -0
  9. maqet/config/__init__.py +26 -0
  10. maqet/config/merger.py +237 -0
  11. maqet/config/parser.py +198 -0
  12. maqet/config/validators.py +519 -0
  13. maqet/config_handlers.py +684 -0
  14. maqet/constants.py +200 -0
  15. maqet/exceptions.py +226 -0
  16. maqet/formatters.py +294 -0
  17. maqet/generators/__init__.py +12 -0
  18. maqet/generators/base_generator.py +101 -0
  19. maqet/generators/cli_generator.py +635 -0
  20. maqet/generators/python_generator.py +247 -0
  21. maqet/generators/rest_generator.py +58 -0
  22. maqet/handlers/__init__.py +12 -0
  23. maqet/handlers/base.py +108 -0
  24. maqet/handlers/init.py +147 -0
  25. maqet/handlers/stage.py +196 -0
  26. maqet/ipc/__init__.py +29 -0
  27. maqet/ipc/retry.py +265 -0
  28. maqet/ipc/runner_client.py +285 -0
  29. maqet/ipc/unix_socket_server.py +239 -0
  30. maqet/logger.py +160 -55
  31. maqet/machine.py +884 -0
  32. maqet/managers/__init__.py +7 -0
  33. maqet/managers/qmp_manager.py +333 -0
  34. maqet/managers/snapshot_coordinator.py +327 -0
  35. maqet/managers/vm_manager.py +683 -0
  36. maqet/maqet.py +1120 -0
  37. maqet/os_interactions.py +46 -0
  38. maqet/process_spawner.py +395 -0
  39. maqet/qemu_args.py +76 -0
  40. maqet/qmp/__init__.py +10 -0
  41. maqet/qmp/commands.py +92 -0
  42. maqet/qmp/keyboard.py +311 -0
  43. maqet/qmp/qmp.py +17 -0
  44. maqet/snapshot.py +473 -0
  45. maqet/state.py +958 -0
  46. maqet/storage.py +702 -162
  47. maqet/validation/__init__.py +9 -0
  48. maqet/validation/config_validator.py +170 -0
  49. maqet/vm_runner.py +523 -0
  50. maqet-0.0.5.dist-info/METADATA +237 -0
  51. maqet-0.0.5.dist-info/RECORD +55 -0
  52. {maqet-0.0.1.4.dist-info → maqet-0.0.5.dist-info}/WHEEL +1 -1
  53. maqet-0.0.5.dist-info/entry_points.txt +2 -0
  54. maqet-0.0.5.dist-info/licenses/LICENSE +21 -0
  55. {maqet-0.0.1.4.dist-info → maqet-0.0.5.dist-info}/top_level.txt +0 -1
  56. maqet/core.py +0 -411
  57. maqet/functions.py +0 -104
  58. maqet-0.0.1.4.dist-info/METADATA +0 -6
  59. maqet-0.0.1.4.dist-info/RECORD +0 -33
  60. qemu/machine/__init__.py +0 -36
  61. qemu/machine/console_socket.py +0 -142
  62. qemu/machine/machine.py +0 -954
  63. qemu/machine/py.typed +0 -0
  64. qemu/machine/qtest.py +0 -191
  65. qemu/qmp/__init__.py +0 -59
  66. qemu/qmp/error.py +0 -50
  67. qemu/qmp/events.py +0 -717
  68. qemu/qmp/legacy.py +0 -319
  69. qemu/qmp/message.py +0 -209
  70. qemu/qmp/models.py +0 -146
  71. qemu/qmp/protocol.py +0 -1057
  72. qemu/qmp/py.typed +0 -0
  73. qemu/qmp/qmp_client.py +0 -655
  74. qemu/qmp/qmp_shell.py +0 -618
  75. qemu/qmp/qmp_tui.py +0 -655
  76. qemu/qmp/util.py +0 -219
  77. qemu/utils/__init__.py +0 -162
  78. qemu/utils/accel.py +0 -84
  79. qemu/utils/py.typed +0 -0
  80. qemu/utils/qemu_ga_client.py +0 -323
  81. qemu/utils/qom.py +0 -273
  82. qemu/utils/qom_common.py +0 -175
  83. qemu/utils/qom_fuse.py +0 -207
maqet/api/registry.py ADDED
@@ -0,0 +1,182 @@
1
+ """
2
+ API Registry
3
+
4
+ Registry for tracking all decorated API methods and providing
5
+ lookup capabilities for CLI and Python API generation.
6
+
7
+ Supports both global registry (backward compatibility) and instance-based
8
+ registry (for parallel tests and multiple Maqet instances).
9
+ """
10
+
11
+ import inspect
12
+ from typing import Any, Dict, List, Optional
13
+
14
+ from .metadata import APIMethodMetadata
15
+
16
+
17
+ class APIRegistry:
18
+ """
19
+ Registry for API methods decorated with @api_method.
20
+
21
+ This registry enables generators to discover all available methods
22
+ and their metadata for creating CLI commands and Python APIs.
23
+
24
+ Can be used as:
25
+ - Global registry (API_REGISTRY) for backward compatibility
26
+ - Instance registry (one per Maqet instance) for isolated registries
27
+ """
28
+
29
+ def __init__(self):
30
+ """Initialize the API registry with empty collections."""
31
+ self._methods: Dict[str, APIMethodMetadata] = {}
32
+ self._cli_commands: Dict[str, str] = {} # cli_name -> full_name
33
+ self._categories: Dict[str, List[str]] = {} # category -> [full_names]
34
+
35
+ def register(self, metadata: APIMethodMetadata) -> None:
36
+ """
37
+ Register a new API method.
38
+
39
+ Args:
40
+ metadata: Method metadata to register
41
+ """
42
+ full_name = metadata.full_name
43
+ self._methods[full_name] = metadata
44
+
45
+ # Register CLI command mapping
46
+ if metadata.cli_name:
47
+ self._cli_commands[metadata.cli_name] = full_name
48
+
49
+ # Register category mapping
50
+ if metadata.category not in self._categories:
51
+ self._categories[metadata.category] = []
52
+ self._categories[metadata.category].append(full_name)
53
+
54
+ def get_method(self, full_name: str) -> Optional[APIMethodMetadata]:
55
+ """
56
+ Get method metadata by full name.
57
+
58
+ Args:
59
+ full_name: Full method name (e.g., 'Maqet.start')
60
+
61
+ Returns:
62
+ Method metadata or None if not found
63
+ """
64
+ return self._methods.get(full_name)
65
+
66
+ def get_by_cli_name(self, cli_name: str) -> Optional[APIMethodMetadata]:
67
+ """
68
+ Get method metadata by CLI command name.
69
+
70
+ Args:
71
+ cli_name: CLI command name (e.g., 'start')
72
+
73
+ Returns:
74
+ Method metadata or None if not found
75
+ """
76
+ full_name = self._cli_commands.get(cli_name)
77
+ return self._methods.get(full_name) if full_name else None
78
+
79
+ def get_by_category(self, category: str) -> List[APIMethodMetadata]:
80
+ """
81
+ Get all methods in a category.
82
+
83
+ Args:
84
+ category: Category name (e.g., 'vm', 'qmp')
85
+
86
+ Returns:
87
+ List of method metadata in the category
88
+ """
89
+ full_names = self._categories.get(category, [])
90
+ return [self._methods[name] for name in full_names]
91
+
92
+ def get_all_methods(self) -> List[APIMethodMetadata]:
93
+ """
94
+ Get all registered methods.
95
+
96
+ Returns:
97
+ List of all method metadata
98
+ """
99
+ return list(self._methods.values())
100
+
101
+ def get_all_methods_dict(self) -> Dict[str, APIMethodMetadata]:
102
+ """
103
+ Get all registered methods as a dictionary.
104
+
105
+ Returns:
106
+ Dictionary mapping method names to metadata
107
+ """
108
+ return self._methods.copy()
109
+
110
+ def get_all_cli_commands(self) -> Dict[str, APIMethodMetadata]:
111
+ """
112
+ Get all CLI commands and their metadata.
113
+
114
+ Returns:
115
+ Dictionary mapping CLI command names to metadata
116
+ """
117
+ return {
118
+ cli_name: self._methods[full_name]
119
+ for cli_name, full_name in self._cli_commands.items()
120
+ }
121
+
122
+ def get_categories(self) -> List[str]:
123
+ """
124
+ Get all available categories.
125
+
126
+ Returns:
127
+ List of category names
128
+ """
129
+ return list(self._categories.keys())
130
+
131
+ def clear(self) -> None:
132
+ """Clear all registered methods (mainly for testing)."""
133
+ self._methods.clear()
134
+ self._cli_commands.clear()
135
+ self._categories.clear()
136
+
137
+ def register_from_instance(self, instance: Any) -> None:
138
+ """
139
+ Register all @api_method decorated methods from an instance.
140
+
141
+ This enables instance-based registries where each Maqet instance
142
+ has its own registry, allowing for:
143
+ - Parallel test execution without registry pollution
144
+ - Multiple Maqet instances with different configurations
145
+ - Thread-safe operation
146
+
147
+ Args:
148
+ instance: Object instance to scan for @api_method decorated methods
149
+
150
+ Example:
151
+ registry = APIRegistry()
152
+ maqet = Maqet()
153
+ registry.register_from_instance(maqet)
154
+ """
155
+ # Get the class of the instance
156
+ cls = instance.__class__
157
+
158
+ # Scan for decorated methods
159
+ for name, method in inspect.getmembers(cls, predicate=inspect.isfunction):
160
+ if hasattr(method, "_api_metadata"):
161
+ metadata: APIMethodMetadata = method._api_metadata
162
+ # Clone metadata with bound method (instance-specific)
163
+ bound_metadata = APIMethodMetadata(
164
+ name=metadata.name,
165
+ function=getattr(instance, name), # Bound method
166
+ owner_class=metadata.owner_class,
167
+ cli_name=metadata.cli_name,
168
+ description=metadata.description,
169
+ signature=metadata.signature,
170
+ category=metadata.category,
171
+ requires_vm=metadata.requires_vm,
172
+ examples=metadata.examples,
173
+ aliases=metadata.aliases,
174
+ hidden=metadata.hidden,
175
+ parent=metadata.parent,
176
+ )
177
+ self.register(bound_metadata)
178
+
179
+
180
+ # Global registry instance (for backward compatibility)
181
+ # New code should prefer instance-based registries via register_from_instance()
182
+ API_REGISTRY = APIRegistry()
maqet/cli.py ADDED
@@ -0,0 +1,71 @@
1
+ import argparse
2
+ import os
3
+ from pathlib import Path
4
+
5
+ from benedict import benedict
6
+
7
+ from maqet.logger import LOG, configure_file_logging
8
+ from maqet.maqet import Maqet
9
+
10
+
11
+ def cli():
12
+ LOG.debug("Maqet CLI started")
13
+ parser = argparse.ArgumentParser(
14
+ prog="MAQET - m4x0n QEMU Tool",
15
+ description="Using YAML file for running QEMU VM and automate actions",
16
+ epilog="Still in development"
17
+ )
18
+
19
+ parser.add_argument(
20
+ "-f", "--file",
21
+ type=Path,
22
+ help="yaml with config" # TODO: Write more
23
+ )
24
+ parser.add_argument(
25
+ "stages",
26
+ nargs="*",
27
+ help="stages to run. If not stated - just start VM",
28
+ )
29
+ parser.add_argument(
30
+ "-v", "--verbose",
31
+ action='count',
32
+ help="increase verbose level",
33
+ default=0
34
+ )
35
+ parser.add_argument(
36
+ "-a", "--argument",
37
+ nargs="*",
38
+ help="Plain arguments to pass to qemu,"
39
+ "use with quotes: -a '-arg_a ... -arg_z' "
40
+ )
41
+
42
+
43
+ cli_args = parser.parse_args()
44
+
45
+ # Set console handler level based on verbosity, but leave file handler at DEBUG
46
+ console_handler = LOG.handlers[0]
47
+ console_handler.setLevel(50 - cli_args.verbose * 10 if cli_args.verbose < 5 else 10)
48
+
49
+ if cli_args.file is not None:
50
+ os.chdir(cli_args.file.parent)
51
+ raw_config = benedict.from_yaml(cli_args.file.name)
52
+ # NOTE: should rise Exception if yaml incorrect
53
+ else:
54
+ raw_config = benedict({})
55
+
56
+ if 'log_file' in raw_config:
57
+ configure_file_logging(raw_config['log_file'])
58
+
59
+ raw_config.plain_arguments = cli_args.argument
60
+
61
+ LOG.debug(f"CLI Arguments: {cli_args}")
62
+
63
+ maqet = Maqet(*cli_args.stages, **raw_config)
64
+ LOG.debug(f"Maqet initialized, stages to run: {cli_args.stages}")
65
+ maqet()
66
+ LOG.debug("Maqet finished")
67
+
68
+
69
+ def main():
70
+ """Entry point for the maqet command."""
71
+ cli()
@@ -0,0 +1,26 @@
1
+ """
2
+ MAQET Configuration Module
3
+
4
+ Dynamic configuration parsing and validation system using decorators.
5
+ """
6
+
7
+ from .merger import ConfigError, ConfigMerger
8
+ from .parser import ConfigParser
9
+ from .validators import (
10
+ ConfigValidationError,
11
+ config_validator,
12
+ get_required_keys,
13
+ get_validators,
14
+ validate_config_data,
15
+ )
16
+
17
+ __all__ = [
18
+ "ConfigError",
19
+ "ConfigMerger",
20
+ "ConfigParser",
21
+ "ConfigValidationError",
22
+ "config_validator",
23
+ "get_validators",
24
+ "get_required_keys",
25
+ "validate_config_data",
26
+ ]
maqet/config/merger.py ADDED
@@ -0,0 +1,237 @@
1
+ """
2
+ Configuration Merger
3
+
4
+ Handles deep-merging of multiple configuration files.
5
+ Provides utilities for loading and merging YAML configuration files
6
+ with deep merge support for complex nested structures.
7
+ """
8
+
9
+ from pathlib import Path
10
+ from typing import Any, Dict, List, Union
11
+
12
+ import yaml
13
+
14
+
15
+ class ConfigError(Exception):
16
+ """Configuration parsing errors"""
17
+
18
+
19
+ class ConfigMerger:
20
+ """
21
+ Handles deep-merging of multiple configuration files.
22
+
23
+ Provides utilities for loading and merging YAML configuration files
24
+ with deep merge support for complex nested structures.
25
+ """
26
+
27
+ @staticmethod
28
+ def _merge_arguments_list(
29
+ base_args: List[Any], override_args: List[Any]
30
+ ) -> List[Any]:
31
+ """
32
+ Merge two arguments lists with override behavior.
33
+
34
+ For arguments like [{foo: 0}, {bar: 10}] and [{bar: 20}, {baz: 30}],
35
+ later configs override earlier ones by key, producing:
36
+ [{foo: 0}, {bar: 20}, {baz: 30}]
37
+
38
+ Args:
39
+ base_args: Base arguments list
40
+ override_args: Override arguments list
41
+
42
+ Returns:
43
+ Merged arguments list with overrides applied
44
+ """
45
+ # Track arguments by their keys (for dict items)
46
+ # Use dict to maintain insertion order (Python 3.7+)
47
+ merged = {}
48
+
49
+ # Process base arguments first
50
+ for arg in base_args:
51
+ if isinstance(arg, dict):
52
+ # For dict items, use the key as identifier
53
+ for key in arg.keys():
54
+ merged[key] = arg
55
+ else:
56
+ # For non-dict items (strings), use the item itself as key
57
+ merged[str(arg)] = arg
58
+
59
+ # Process override arguments (later configs win)
60
+ for arg in override_args:
61
+ if isinstance(arg, dict):
62
+ # Override or add dict items by key
63
+ for key in arg.keys():
64
+ merged[key] = arg
65
+ else:
66
+ # Override or add non-dict items
67
+ merged[str(arg)] = arg
68
+
69
+ # Convert back to list, preserving order
70
+ return list(merged.values())
71
+
72
+ @staticmethod
73
+ def deep_merge(
74
+ base: Dict[str, Any], override: Dict[str, Any]
75
+ ) -> Dict[str, Any]:
76
+ """
77
+ Deep merge two dictionaries.
78
+
79
+ Args:
80
+ base: Base configuration dictionary
81
+ override: Override configuration dictionary
82
+
83
+ Returns:
84
+ Deep-merged configuration dictionary
85
+ """
86
+ result = base.copy()
87
+
88
+ for key, value in override.items():
89
+ if (
90
+ key in result
91
+ and isinstance(result[key], dict)
92
+ and isinstance(value, dict)
93
+ ):
94
+ # Recursively merge nested dictionaries
95
+ result[key] = ConfigMerger.deep_merge(result[key], value)
96
+ elif (
97
+ key in result
98
+ and isinstance(result[key], list)
99
+ and isinstance(value, list)
100
+ ):
101
+ # Special handling for 'arguments' list: merge dict items by key
102
+ if key == "arguments":
103
+ result[key] = ConfigMerger._merge_arguments_list(
104
+ result[key], value
105
+ )
106
+ else:
107
+ # For other lists (storage, etc.), concatenate
108
+ # This allows adding storage devices, network interfaces, etc.
109
+ result[key] = result[key] + value
110
+ else:
111
+ # Override scalar values and non-dict types
112
+ result[key] = value
113
+
114
+ return result
115
+
116
+ @staticmethod
117
+ def _resolve_relative_paths(
118
+ config_data: Dict[str, Any], config_dir: Path
119
+ ) -> Dict[str, Any]:
120
+ """
121
+ Resolve relative file paths in config to absolute paths.
122
+
123
+ Layer 3 of automatic path resolution: resolve paths relative to config file location.
124
+ This makes configs portable and allows relative paths like ./live.iso to work.
125
+
126
+ Args:
127
+ config_data: Configuration dictionary
128
+ config_dir: Directory containing the config file
129
+
130
+ Returns:
131
+ Configuration with resolved absolute paths
132
+ """
133
+ # Resolve storage device file paths
134
+ if "storage" in config_data and isinstance(
135
+ config_data["storage"], list
136
+ ):
137
+ for storage_item in config_data["storage"]:
138
+ if isinstance(storage_item, dict) and "file" in storage_item:
139
+ file_path = storage_item["file"]
140
+ if not Path(file_path).is_absolute():
141
+ # Resolve relative to config file directory
142
+ storage_item["file"] = str(
143
+ (config_dir / file_path).resolve()
144
+ )
145
+
146
+ # Resolve VirtFS path entries
147
+ if isinstance(storage_item, dict) and "path" in storage_item:
148
+ path_value = storage_item["path"]
149
+ if not Path(path_value).is_absolute():
150
+ storage_item["path"] = str(
151
+ (config_dir / path_value).resolve()
152
+ )
153
+
154
+ # Resolve file paths in arguments (drive file=./path,...)
155
+ if "arguments" in config_data and isinstance(
156
+ config_data["arguments"], list
157
+ ):
158
+ for arg_item in config_data["arguments"]:
159
+ if isinstance(arg_item, dict):
160
+ for key, value in arg_item.items():
161
+ if isinstance(value, str) and "file=" in value:
162
+ # Parse drive argument: file=./live.iso,media=cdrom
163
+ parts = value.split(",")
164
+ for i, part in enumerate(parts):
165
+ if part.startswith("file="):
166
+ file_path = part[
167
+ 5:
168
+ ] # Remove 'file=' prefix
169
+ if not Path(file_path).is_absolute():
170
+ abs_path = str(
171
+ (config_dir / file_path).resolve()
172
+ )
173
+ parts[i] = f"file={abs_path}"
174
+ arg_item[key] = ",".join(parts)
175
+
176
+ return config_data
177
+
178
+ @staticmethod
179
+ def load_and_merge_files(
180
+ config_files: Union[str, List[str]],
181
+ ) -> Dict[str, Any]:
182
+ """
183
+ Load and merge multiple configuration files.
184
+
185
+ Args:
186
+ config_files: Single config file path or list of config file paths
187
+
188
+ Returns:
189
+ Merged configuration data
190
+
191
+ Raises:
192
+ ConfigError: If any config file cannot be loaded or parsed
193
+ """
194
+ if isinstance(config_files, str):
195
+ config_files = [config_files]
196
+
197
+ if not config_files:
198
+ return {}
199
+
200
+ merged_config = {}
201
+
202
+ for config_file in config_files:
203
+ config_path = Path(config_file)
204
+ if not config_path.exists():
205
+ raise ConfigError(
206
+ f"Configuration file not found: {config_file}"
207
+ )
208
+
209
+ try:
210
+ with open(config_path) as f:
211
+ config_data = yaml.safe_load(f) or {}
212
+
213
+ if not isinstance(config_data, dict):
214
+ raise ConfigError(
215
+ f"Configuration in {config_file} must be a "
216
+ f"YAML dictionary"
217
+ )
218
+
219
+ # Layer 3: Resolve relative paths in config relative to config file location
220
+ config_dir = config_path.parent.resolve()
221
+ config_data = ConfigMerger._resolve_relative_paths(
222
+ config_data, config_dir
223
+ )
224
+
225
+ # Deep merge with previous configs
226
+ merged_config = ConfigMerger.deep_merge(
227
+ merged_config, config_data
228
+ )
229
+
230
+ except yaml.YAMLError as e:
231
+ raise ConfigError(f"Invalid YAML in {config_file}: {e}")
232
+ except Exception as e:
233
+ raise ConfigError(
234
+ f"Error loading configuration from {config_file}: {e}"
235
+ )
236
+
237
+ return merged_config