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
@@ -0,0 +1,247 @@
1
+ """
2
+ Python API Generator
3
+
4
+ Provides clean Python API access to decorated methods.
5
+ """
6
+
7
+ import inspect
8
+ from typing import Any, Dict, Optional
9
+
10
+ from ..api import APIMethodMetadata, APIRegistry
11
+ from .base_generator import BaseGenerator
12
+
13
+
14
+ class PythonAPIGenerator(BaseGenerator):
15
+ """
16
+ Provides programmatic access to @api_method decorated methods.
17
+
18
+ This generator enables clean Python API usage where methods can be
19
+ called directly with proper validation and error handling.
20
+ """
21
+
22
+ def __init__(self, maqet_instance: Any, registry: APIRegistry):
23
+ """
24
+ Initialize Python API generator.
25
+
26
+ Args:
27
+ maqet_instance: Instance of Maqet class
28
+ registry: API registry containing method metadata
29
+ """
30
+ super().__init__(maqet_instance, registry)
31
+
32
+ def generate(self) -> "PythonAPIInterface":
33
+ """
34
+ Generate Python API interface.
35
+
36
+ Returns:
37
+ Python API interface object
38
+ """
39
+ return PythonAPIInterface(self.maqet_instance, self.registry)
40
+
41
+ def execute_method(self, method_name: str, *args, **kwargs) -> Any:
42
+ """
43
+ Execute a method by name with validation.
44
+
45
+ Args:
46
+ method_name: Name of method to execute
47
+ *args: Positional arguments
48
+ **kwargs: Keyword arguments
49
+
50
+ Returns:
51
+ Method execution result
52
+
53
+ Raises:
54
+ ValueError: If method not found or invalid parameters
55
+ TypeError: If parameter types are incorrect
56
+ """
57
+ # Get method metadata
58
+ metadata = self.get_method_by_name(method_name)
59
+ if not metadata:
60
+ raise ValueError(f"Method '{method_name}' not found")
61
+
62
+ # Convert positional args to kwargs for validation
63
+ combined_kwargs = self._combine_args_kwargs(metadata, args, kwargs)
64
+
65
+ # Validate parameters
66
+ self._validate_parameters(metadata, combined_kwargs)
67
+
68
+ # Get the actual method from the instance
69
+ method = getattr(self.maqet_instance, method_name)
70
+
71
+ # Execute method with original args and kwargs
72
+ return method(*args, **kwargs)
73
+
74
+ def _validate_parameters(
75
+ self, metadata: APIMethodMetadata, kwargs: Dict[str, Any]
76
+ ) -> None:
77
+ """
78
+ Validate method parameters.
79
+
80
+ Args:
81
+ metadata: Method metadata
82
+ kwargs: Parameters provided by user
83
+
84
+ Raises:
85
+ ValueError: If required parameters missing or unknown parameters provided
86
+ """
87
+ # Check for missing required parameters
88
+ missing = self.validate_required_parameters(metadata, kwargs)
89
+ if missing:
90
+ raise ValueError(
91
+ f"Missing required parameters: {', '.join(missing)}"
92
+ )
93
+
94
+ # Check for unknown parameters only if there's no **kwargs parameter
95
+ has_var_keyword = any(
96
+ param.kind == inspect.Parameter.VAR_KEYWORD
97
+ for param in metadata.parameters.values()
98
+ )
99
+
100
+ if not has_var_keyword:
101
+ valid_params = set(metadata.parameters.keys())
102
+ provided_params = set(kwargs.keys())
103
+ unknown = provided_params - valid_params
104
+ if unknown:
105
+ raise ValueError(f"Unknown parameters: {', '.join(unknown)}")
106
+
107
+ # Type validation could be added here if needed
108
+
109
+ def _combine_args_kwargs(
110
+ self, metadata: APIMethodMetadata, args: tuple, kwargs: Dict[str, Any]
111
+ ) -> Dict[str, Any]:
112
+ """
113
+ Combine positional and keyword arguments into a single kwargs dict for validation.
114
+
115
+ Args:
116
+ metadata: Method metadata
117
+ args: Positional arguments
118
+ kwargs: Keyword arguments
119
+
120
+ Returns:
121
+ Combined arguments as kwargs dict
122
+
123
+ Raises:
124
+ ValueError: If too many positional arguments provided
125
+ """
126
+ combined = kwargs.copy()
127
+
128
+ # Get parameter names in order (excluding 'self')
129
+ param_names = list(metadata.parameters.keys())
130
+
131
+ # Map positional args to parameter names
132
+ if len(args) > len(param_names):
133
+ raise ValueError(
134
+ f"Too many positional arguments: expected {len(param_names)}, got {len(args)}"
135
+ )
136
+
137
+ for i, arg_value in enumerate(args):
138
+ param_name = param_names[i]
139
+ if param_name in combined:
140
+ raise ValueError(
141
+ f"Parameter '{param_name}' specified both positionally and as keyword argument"
142
+ )
143
+ combined[param_name] = arg_value
144
+
145
+ return combined
146
+
147
+
148
+ class PythonAPIInterface:
149
+ """
150
+ Dynamic Python API interface that provides direct method access.
151
+
152
+ This class dynamically creates methods based on registered API methods,
153
+ allowing for clean Python usage like:
154
+
155
+ api = PythonAPIInterface(maqet_instance, registry)
156
+ api.start("myvm", detach=True)
157
+ api.stop("myvm")
158
+ """
159
+
160
+ def __init__(self, maqet_instance: Any, registry: APIRegistry):
161
+ """
162
+ Initialize Python API interface.
163
+
164
+ Args:
165
+ maqet_instance: Instance of Maqet class
166
+ registry: API registry containing method metadata
167
+ """
168
+ self.maqet_instance = maqet_instance
169
+ self.registry = registry
170
+ self.generator = PythonAPIGenerator(maqet_instance, registry)
171
+
172
+ def __getattr__(self, name: str) -> Any:
173
+ """
174
+ Dynamically provide access to API methods.
175
+
176
+ Args:
177
+ name: Method name
178
+
179
+ Returns:
180
+ Callable method
181
+
182
+ Raises:
183
+ AttributeError: If method not found
184
+ """
185
+ # Check if this is a registered API method
186
+ metadata = self.generator.get_method_by_name(name)
187
+ if metadata:
188
+ return lambda *args, **kwargs: self.generator.execute_method(
189
+ name, *args, **kwargs
190
+ )
191
+
192
+ # Check if it's a direct attribute on the instance
193
+ if hasattr(self.maqet_instance, name):
194
+ return getattr(self.maqet_instance, name)
195
+
196
+ raise AttributeError(
197
+ f"'{self.__class__.__name__}' object has no attribute '{name}'"
198
+ )
199
+
200
+ def list_methods(self) -> Dict[str, str]:
201
+ """
202
+ List all available API methods.
203
+
204
+ Returns:
205
+ Dictionary mapping method names to descriptions
206
+ """
207
+ methods = {}
208
+ for metadata in self.registry.get_all_methods():
209
+ if metadata.owner_class == self.maqet_instance.__class__.__name__:
210
+ methods[metadata.name] = metadata.description
211
+ return methods
212
+
213
+ def get_method_help(self, method_name: str) -> Optional[str]:
214
+ """
215
+ Get help text for a method.
216
+
217
+ Args:
218
+ method_name: Method name
219
+
220
+ Returns:
221
+ Help text or None if method not found
222
+ """
223
+ metadata = self.generator.get_method_by_name(method_name)
224
+ if not metadata:
225
+ return None
226
+
227
+ help_text = f"{metadata.name}: {metadata.description}\n\n"
228
+
229
+ # Add parameters
230
+ if metadata.parameters:
231
+ help_text += "Parameters:\n"
232
+ for param_name, param in metadata.parameters.items():
233
+ required = param.default == param.empty
234
+ default_text = (
235
+ f" (default: {param.default})"
236
+ if not required
237
+ else " (required)"
238
+ )
239
+ help_text += f" {param_name}{default_text}\n"
240
+
241
+ # Add examples
242
+ if metadata.examples:
243
+ help_text += "\nExamples:\n"
244
+ for example in metadata.examples:
245
+ help_text += f" {example}\n"
246
+
247
+ return help_text.strip()
@@ -0,0 +1,58 @@
1
+ """
2
+ REST API Generator (Placeholder)
3
+
4
+ This is a placeholder showing how the API system can be extended with new generators.
5
+ A REST API generator could be implemented here using FastAPI, Flask, or similar.
6
+
7
+ Example implementation would:
8
+ 1. Inherit from BaseGenerator
9
+ 2. Read method metadata from API_REGISTRY
10
+ 3. Generate REST routes with proper HTTP methods:
11
+ - GET /api/vms/{vm_id}/status -> status(vm_id)
12
+ - POST /api/vms -> add(config, name)
13
+ - DELETE /api/vms/{vm_id} -> rm(vm_id)
14
+ 4. Handle request/response serialization
15
+ 5. Add authentication/authorization if needed
16
+
17
+ Usage:
18
+ from maqet.generators import RestAPIGenerator
19
+ generator = RestAPIGenerator(maqet_instance, API_REGISTRY)
20
+ app = generator.generate() # Returns FastAPI/Flask app
21
+ """
22
+
23
+ from ..api import APIRegistry
24
+ from .base_generator import BaseGenerator
25
+
26
+
27
+ class RestAPIGenerator(BaseGenerator):
28
+ """
29
+ Placeholder REST API generator demonstrating extensibility.
30
+
31
+ A real implementation would generate REST endpoints from @api_method
32
+ decorated methods, handling routing, serialization, and validation.
33
+ """
34
+
35
+ def generate(self):
36
+ """
37
+ Generate REST API routes.
38
+
39
+ Returns:
40
+ Web application instance (FastAPI, Flask, etc.)
41
+ """
42
+ raise NotImplementedError(
43
+ "REST API generation not yet implemented. "
44
+ "This is a placeholder demonstrating the extensible architecture."
45
+ )
46
+
47
+ def run(self, host: str = "0.0.0.0", port: int = 8000):
48
+ """
49
+ Run the REST API server.
50
+
51
+ Args:
52
+ host: Server host
53
+ port: Server port
54
+ """
55
+ raise NotImplementedError(
56
+ "REST API server not yet implemented. "
57
+ "This would start a web server with generated routes."
58
+ )
@@ -0,0 +1,12 @@
1
+ """
2
+ MAQET handlers package.
3
+
4
+ Contains base handler classes and specific handlers for configuration
5
+ initialization and stage execution.
6
+ """
7
+
8
+ from .base import Handler
9
+ from .init import InitHandler
10
+ from .stage import StageHandler
11
+
12
+ __all__ = ["Handler", "InitHandler", "StageHandler"]
maqet/handlers/base.py ADDED
@@ -0,0 +1,108 @@
1
+ import inspect
2
+ from abc import ABC
3
+ from typing import Callable
4
+
5
+ from benedict import benedict
6
+
7
+ from maqet.logger import LOG
8
+
9
+
10
+ class HandlerError(Exception):
11
+ """
12
+ Handler error
13
+ """
14
+
15
+
16
+ class Handler(ABC):
17
+ """
18
+ Interface for Maqet state processors
19
+ """
20
+ __METHODS = {}
21
+
22
+ @classmethod
23
+ def method(self, function: Callable, **kwargs):
24
+ """
25
+ Decorator to add method to handler methods
26
+ """
27
+ name = kwargs.get('name', function.__name__)
28
+ handler_name = self.__name__
29
+ if handler_name not in self.__METHODS:
30
+ self.__METHODS[handler_name] = {}
31
+
32
+ self.__METHODS[handler_name][name] = function
33
+
34
+ # TODO: add signature check:
35
+ # method(state: dict, *args, **kwargs)
36
+
37
+ def stub(*args, **kwargs):
38
+ raise HandlerError("Handler method called outside of handler")
39
+
40
+ return stub
41
+
42
+ def __init__(self, state: dict,
43
+ argument: list | dict | str,
44
+ *args, **kwargs):
45
+
46
+ self.state = benedict(state)
47
+ self.error_fatal = kwargs.get('error_fatal', False)
48
+
49
+ self.__execute(argument)
50
+
51
+ def __execute(self, argument: list | dict | str):
52
+ if isinstance(argument, list):
53
+ LOG.debug(f"Argument {argument} - splitting into subarguments")
54
+ for subargument in argument:
55
+ self.__execute(subargument)
56
+ elif isinstance(argument, dict):
57
+ LOG.debug(f"Argument {argument} - running by key-value")
58
+ for method_name, subargument in argument.items():
59
+ self.__call_method(method_name, subargument)
60
+ elif isinstance(argument, str):
61
+ LOG.debug(f"Argument {argument} - running without argument")
62
+ self.__call_method(argument, None)
63
+ else:
64
+ self.__fail("Type check error"
65
+ f" {argument} is not list | dict | str")
66
+
67
+ @classmethod
68
+ def method_exists(self, method_name: str) -> bool:
69
+ if method_name not in self.__METHODS[self.__name__]:
70
+ LOG.debug(f"{self.__name__}::{method_name} not exists")
71
+ return False
72
+ LOG.debug(f"{self.__name__}::{method_name} exists")
73
+ return True
74
+
75
+ @classmethod
76
+ def get_methods(self) -> list:
77
+ return self.__METHODS[self.__name__].keys()
78
+
79
+ def __call_method(self,
80
+ method_name: str,
81
+ argument: list | dict | str = None):
82
+
83
+ if not self.method_exists(method_name):
84
+ self.__fail(f"Method '{method_name}' not available"
85
+ f" in {self.__class__.__name__}")
86
+ method = self.__METHODS[self.__class__.__name__].get(method_name)
87
+
88
+ LOG.debug(f"Inspecting signature for {method_name}: {inspect.signature(method)}")
89
+ LOG.debug(f"{self.__class__.__name__}::"
90
+ f"{method.__name__}({str(argument)})")
91
+ try:
92
+ if isinstance(argument, list):
93
+ method(self.state, *argument)
94
+ elif isinstance(argument, dict):
95
+ method(self.state, **argument)
96
+ elif argument is None:
97
+ method(self.state)
98
+ else:
99
+ method(self.state, argument)
100
+ except Exception as exc:
101
+ msg = f"{method_name}({argument}) execution error\n{exc}\n"
102
+ self.__fail(msg)
103
+
104
+ def __fail(self, msg: str):
105
+ if self.error_fatal:
106
+ raise HandlerError(msg)
107
+ else:
108
+ LOG.error(msg)
maqet/handlers/init.py ADDED
@@ -0,0 +1,147 @@
1
+ from benedict import benedict
2
+
3
+ from maqet.handlers.base import Handler
4
+ # from maqet.handlers.stage import PipelineHandler as run_pipeline
5
+ from maqet.handlers.stage import StageHandler
6
+ from maqet.logger import LOG
7
+ from maqet.qemu_args import Arguments
8
+ # Legacy import - Drive class no longer exists in current storage.py
9
+ # from maqet.storage import Drive
10
+
11
+
12
+ class InitHandler(Handler):
13
+ """
14
+ Handles full config of maqet (basically yaml)
15
+ """
16
+
17
+
18
+ @InitHandler.method
19
+ def binary(state, binary: str):
20
+ LOG.debug(f'Setting binary: {binary}')
21
+ state.binary = binary
22
+
23
+
24
+ @InitHandler.method
25
+ def arguments(state, *args):
26
+ LOG.debug(f'Setting arguments: {args}')
27
+ state['const_args'] = Arguments.parse_args(*args)
28
+
29
+
30
+ @InitHandler.method
31
+ def plain_arguments(state, *args):
32
+ LOG.debug(f'Setting plain arguments: {args}')
33
+ if 'const_args' not in state:
34
+ state['const_args'] = []
35
+ state['const_args'] += Arguments.split_args(*args)
36
+
37
+
38
+ @InitHandler.method
39
+ def storage(state, *args):
40
+ if 'storage' not in state:
41
+ state.storage = {}
42
+
43
+ if 'const_args' not in state:
44
+ state.const_args = []
45
+
46
+ for drive in args:
47
+ n = 0
48
+ name = drive.get('name', f"drive{n}")
49
+ while name in state.storage:
50
+ n += 1
51
+ name = f"drive{n}"
52
+
53
+ # Legacy code - Drive class no longer exists
54
+ # state.storage[name] = Drive(**drive)
55
+ state.storage[name] = drive # Store config dict for now
56
+
57
+ # Legacy code - commented out as Drive class no longer exists
58
+ # for name, drive in state.storage.items():
59
+ # state.const_args += drive()
60
+
61
+
62
+ @InitHandler.method
63
+ def parameters(state, **kwargs):
64
+ LOG.debug(f'Setting parameters: {kwargs}')
65
+ if len(kwargs) == 0:
66
+ return
67
+ state.parameters = kwargs
68
+
69
+
70
+ @InitHandler.method
71
+ def serial(state, *args):
72
+ LOG.debug(f'Setting serial: {args}')
73
+ state.serial = args
74
+
75
+
76
+ @InitHandler.method
77
+ def pipeline(state, **kwargs):
78
+ default_stage = {'idle': {'tasks': [
79
+ 'launch',
80
+ {'wait_for_input': {'prompt': 'Press ENTER to finish'}},
81
+ ]}}
82
+
83
+ state.pipeline = []
84
+ state.procedures = []
85
+
86
+ stages = kwargs.get('stages', {})
87
+
88
+ if len(state._stages_to_run) == 0:
89
+ state.pipeline.append(default_stage)
90
+ LOG.info("Stage idle (default) added to current pipeline")
91
+ return
92
+
93
+ # pre_pipeline_tasks = kwargs.get('pre_pipeline_tasks', [])
94
+ # post_pipeline_tasks = kwargs.get('post_pipeline_tasks', [])
95
+ # pre_stage_tasks = kwargs.get('pre_stage_tasks', [])
96
+ # post_stage_tasks = kwargs.get('post_stage_tasks', [])
97
+
98
+ procedures = kwargs.get('procedures', {})
99
+ state.procedures.append(procedures)
100
+ # data._stages_to_run += ['_pre_pipeline_tasks', '_post_pipeline_tasks']
101
+
102
+ for name, stage in stages.items():
103
+ current_stage = stage
104
+ current_tasks = []
105
+
106
+ # stage.tasks = pre_stage_tasks + stage.tasks + post_stage_tasks
107
+
108
+ if name not in state._stages_to_run:
109
+ continue
110
+ if 'tasks' not in stage or len(stage['tasks']) == 0:
111
+ LOG.warn(f'Stage {name} incorrect, no tasks found. Skipping')
112
+ continue
113
+
114
+ # TODO: procedure that uses another procedure
115
+ for task in stage['tasks']:
116
+ if isinstance(task, dict):
117
+ task_name = next(iter(task))
118
+ if task_name == 'procedure':
119
+ if task['procedure'] in procedures:
120
+ current_tasks += procedures[task['procedure']]
121
+ LOG.debug(f"Procedure {task['procedure']}"
122
+ " added to stage")
123
+ else:
124
+ raise Exception(f"Procedure {task['procedure']}"
125
+ " not found in procedures")
126
+ else:
127
+ current_tasks.append(task)
128
+ LOG.debug(f"Task {task_name} added to stage")
129
+ else:
130
+ current_tasks.append(task)
131
+ LOG.debug(f"Task {task} added to stage")
132
+
133
+ for task in current_tasks:
134
+ if isinstance(task, dict):
135
+ task = next(iter(task))
136
+ if not StageHandler.method_exists(task):
137
+ LOG.error(f"Task {task} not validated")
138
+ raise Exception(f"Task {task} is invalid")
139
+ LOG.debug(f"Task {task} validated")
140
+
141
+ current_stage['tasks'] = current_tasks
142
+ current_stage['arguments'] = Arguments.parse_args(
143
+ *stage.get('arguments', [])
144
+ ) + stage.get('plain_arguments', [])
145
+
146
+ state.pipeline.append({name: current_stage})
147
+ LOG.info(f"Stage {name} added to current pipeline")