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,635 @@
|
|
1
|
+
"""
|
2
|
+
CLI Generator
|
3
|
+
|
4
|
+
Automatically generates argparse CLI commands from @api_method decorated methods.
|
5
|
+
|
6
|
+
This module implements the CLI interface generation component of MAQET's unified API system.
|
7
|
+
It takes method metadata and creates a complete command-line interface with proper argument
|
8
|
+
parsing, help text, and command routing.
|
9
|
+
|
10
|
+
The CLIGenerator converts Python method signatures into argparse subcommands, handling:
|
11
|
+
- Required and optional parameters
|
12
|
+
- Type conversion (str, int, bool, list)
|
13
|
+
- Default values and help text
|
14
|
+
- Command examples and documentation
|
15
|
+
- Error handling and validation
|
16
|
+
|
17
|
+
Example:
|
18
|
+
@api_method(cli_name="start", description="Start VM")
|
19
|
+
def start(self, vm_id: str, detach: bool = False):
|
20
|
+
pass
|
21
|
+
|
22
|
+
Becomes CLI command:
|
23
|
+
$ maqet start myvm --detach
|
24
|
+
"""
|
25
|
+
|
26
|
+
import argparse
|
27
|
+
import inspect
|
28
|
+
import logging
|
29
|
+
import os
|
30
|
+
import sys
|
31
|
+
from typing import Any, Dict, List, Optional, Union, get_args, get_origin
|
32
|
+
|
33
|
+
from ..api import APIMethodMetadata, APIRegistry
|
34
|
+
from ..logger import configure_file_logging, set_verbosity
|
35
|
+
from .base_generator import BaseGenerator
|
36
|
+
|
37
|
+
LOG = logging.getLogger(__name__)
|
38
|
+
|
39
|
+
|
40
|
+
class CLIGenerator(BaseGenerator):
|
41
|
+
"""
|
42
|
+
Generates CLI commands from @api_method decorated methods.
|
43
|
+
|
44
|
+
This generator creates an argparse-based CLI that automatically
|
45
|
+
maps CLI arguments to method parameters and executes the appropriate
|
46
|
+
method on the Maqet instance.
|
47
|
+
|
48
|
+
Key Features:
|
49
|
+
- Automatic subcommand generation from method metadata
|
50
|
+
- Type-aware argument parsing (bool flags, optional args, etc.)
|
51
|
+
- Built-in help generation with examples and descriptions
|
52
|
+
- Global options support (--verbose, --config-dir, etc.)
|
53
|
+
- Proper error handling and user feedback
|
54
|
+
|
55
|
+
Usage:
|
56
|
+
generator = CLIGenerator(maqet_instance, API_REGISTRY)
|
57
|
+
result = generator.run(sys.argv[1:])
|
58
|
+
|
59
|
+
The generator automatically creates CLI commands like:
|
60
|
+
- maqet add config.yaml --name myvm
|
61
|
+
- maqet start myvm --detach
|
62
|
+
- maqet qmp myvm system_powerdown
|
63
|
+
"""
|
64
|
+
|
65
|
+
def __init__(self, maqet_instance: Any, registry: APIRegistry):
|
66
|
+
"""
|
67
|
+
Initialize CLI generator.
|
68
|
+
|
69
|
+
Args:
|
70
|
+
maqet_instance: Instance of Maqet class
|
71
|
+
registry: API registry containing method metadata
|
72
|
+
"""
|
73
|
+
super().__init__(maqet_instance, registry)
|
74
|
+
self.parser: Optional[argparse.ArgumentParser] = None
|
75
|
+
|
76
|
+
def generate(self) -> argparse.ArgumentParser:
|
77
|
+
"""
|
78
|
+
Generate argparse CLI from registered methods.
|
79
|
+
|
80
|
+
Returns:
|
81
|
+
Configured ArgumentParser
|
82
|
+
"""
|
83
|
+
# Create parent parser with global options for subparsers
|
84
|
+
# This allows global options AFTER subcommands (e.g., maqet ls -v)
|
85
|
+
self.global_parent = argparse.ArgumentParser(add_help=False)
|
86
|
+
self._add_global_options_to_parent()
|
87
|
+
|
88
|
+
# Create main parser with global options at top level
|
89
|
+
# This allows global options BEFORE subcommands (e.g., maqet -v ls)
|
90
|
+
self.parser = argparse.ArgumentParser(
|
91
|
+
prog="maqet",
|
92
|
+
description="MAQET - M4x0n's QEMU Tool for VM management",
|
93
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
94
|
+
)
|
95
|
+
|
96
|
+
# Add global options directly to main parser (for before subcommand)
|
97
|
+
self._add_global_options_to_main()
|
98
|
+
|
99
|
+
# Add subcommands
|
100
|
+
subparsers = self.parser.add_subparsers(
|
101
|
+
dest="command", help="Available commands", metavar="COMMAND"
|
102
|
+
)
|
103
|
+
|
104
|
+
# Group methods by parent (None for top-level, string for nested)
|
105
|
+
parent_groups = {}
|
106
|
+
for category in self.registry.get_categories():
|
107
|
+
methods = self.registry.get_by_category(category)
|
108
|
+
for metadata in methods:
|
109
|
+
if not metadata.hidden:
|
110
|
+
parent = metadata.parent
|
111
|
+
if parent not in parent_groups:
|
112
|
+
parent_groups[parent] = []
|
113
|
+
parent_groups[parent].append(metadata)
|
114
|
+
|
115
|
+
# Add top-level commands (parent=None)
|
116
|
+
if None in parent_groups:
|
117
|
+
for metadata in parent_groups[None]:
|
118
|
+
self._add_subcommand(subparsers, metadata)
|
119
|
+
|
120
|
+
# Add parent commands with nested subcommands
|
121
|
+
for parent, child_methods in parent_groups.items():
|
122
|
+
if parent is not None:
|
123
|
+
self._add_parent_command(subparsers, parent, child_methods)
|
124
|
+
|
125
|
+
return self.parser
|
126
|
+
|
127
|
+
def run(self, args: Optional[List[str]] = None) -> Any:
|
128
|
+
"""
|
129
|
+
Parse arguments and execute the appropriate method.
|
130
|
+
|
131
|
+
Args:
|
132
|
+
args: Command line arguments (defaults to sys.argv[1:])
|
133
|
+
|
134
|
+
Returns:
|
135
|
+
Result of method execution
|
136
|
+
"""
|
137
|
+
if self.parser is None:
|
138
|
+
self.generate()
|
139
|
+
|
140
|
+
parsed_args = self.parser.parse_args(args)
|
141
|
+
|
142
|
+
# Configure logging based on verbosity flags
|
143
|
+
self._configure_logging(parsed_args)
|
144
|
+
|
145
|
+
if not hasattr(parsed_args, "command") or parsed_args.command is None:
|
146
|
+
self.parser.print_help()
|
147
|
+
sys.exit(1)
|
148
|
+
|
149
|
+
# Determine which command to execute (handle nested subcommands)
|
150
|
+
command_name = parsed_args.command
|
151
|
+
|
152
|
+
# Check if this is a nested subcommand
|
153
|
+
if hasattr(parsed_args, "subcommand") and parsed_args.subcommand:
|
154
|
+
# This is a nested command (e.g., maqet qmp pause)
|
155
|
+
metadata = self.registry.get_by_cli_name(parsed_args.subcommand)
|
156
|
+
if not metadata:
|
157
|
+
print(
|
158
|
+
f"Error: Unknown subcommand '{parsed_args.subcommand}'",
|
159
|
+
file=sys.stderr,
|
160
|
+
)
|
161
|
+
sys.exit(1)
|
162
|
+
else:
|
163
|
+
# This is a top-level command (e.g., maqet start)
|
164
|
+
metadata = self.registry.get_by_cli_name(command_name)
|
165
|
+
if not metadata:
|
166
|
+
print(
|
167
|
+
f"Error: Unknown command '{command_name}'",
|
168
|
+
file=sys.stderr,
|
169
|
+
)
|
170
|
+
sys.exit(1)
|
171
|
+
|
172
|
+
# Execute method
|
173
|
+
try:
|
174
|
+
result = self._execute_method(metadata, parsed_args)
|
175
|
+
return result
|
176
|
+
except Exception as e:
|
177
|
+
cmd_display = (
|
178
|
+
f"{command_name} {parsed_args.subcommand}"
|
179
|
+
if hasattr(parsed_args, "subcommand") and parsed_args.subcommand
|
180
|
+
else command_name
|
181
|
+
)
|
182
|
+
print(
|
183
|
+
f"Error executing {cmd_display}: {e}", file=sys.stderr
|
184
|
+
)
|
185
|
+
sys.exit(1)
|
186
|
+
|
187
|
+
def _add_global_options_to_parent(self) -> None:
|
188
|
+
"""Add global CLI options to parent parser (for subparsers)."""
|
189
|
+
# TODO(architect, 2025-10-10): [REFACTOR] Duplicate code - exact same as _add_global_options_to_main
|
190
|
+
# Context: Global options added twice (to main parser and parent parser), exact same code
|
191
|
+
# duplicated. Issue #10 in ARCHITECTURAL_REVIEW.md.
|
192
|
+
#
|
193
|
+
# Recommendation: Extract to common method:
|
194
|
+
# def _add_global_options(self, parser):
|
195
|
+
# parser.add_argument("-v", "--verbose", ...)
|
196
|
+
# Then call from both methods.
|
197
|
+
#
|
198
|
+
# Effort: Small (<1 hour)
|
199
|
+
# Priority: Medium
|
200
|
+
# See: ARCHITECTURAL_REVIEW.md Issue #10
|
201
|
+
self.global_parent.add_argument(
|
202
|
+
"-v",
|
203
|
+
"--verbose",
|
204
|
+
action="count",
|
205
|
+
default=0,
|
206
|
+
help="Increase verbosity (-v, -vv, -vvv)",
|
207
|
+
)
|
208
|
+
self.global_parent.add_argument(
|
209
|
+
"-q",
|
210
|
+
"--quiet",
|
211
|
+
action="store_true",
|
212
|
+
help="Suppress non-error output",
|
213
|
+
)
|
214
|
+
self.global_parent.add_argument(
|
215
|
+
"--config-dir", help="Override config directory path"
|
216
|
+
)
|
217
|
+
self.global_parent.add_argument(
|
218
|
+
"--data-dir", help="Override data directory path"
|
219
|
+
)
|
220
|
+
self.global_parent.add_argument(
|
221
|
+
"--runtime-dir", help="Override runtime directory path"
|
222
|
+
)
|
223
|
+
self.global_parent.add_argument(
|
224
|
+
"--log-file", help="Enable file logging to specified path"
|
225
|
+
)
|
226
|
+
|
227
|
+
def _add_global_options_to_main(self) -> None:
|
228
|
+
"""Add global CLI options to main parser (for before subcommand)."""
|
229
|
+
self.parser.add_argument(
|
230
|
+
"-v",
|
231
|
+
"--verbose",
|
232
|
+
action="count",
|
233
|
+
default=0,
|
234
|
+
help="Increase verbosity (-v, -vv, -vvv)",
|
235
|
+
)
|
236
|
+
self.parser.add_argument(
|
237
|
+
"-q",
|
238
|
+
"--quiet",
|
239
|
+
action="store_true",
|
240
|
+
help="Suppress non-error output",
|
241
|
+
)
|
242
|
+
self.parser.add_argument(
|
243
|
+
"--config-dir", help="Override config directory path"
|
244
|
+
)
|
245
|
+
self.parser.add_argument(
|
246
|
+
"--data-dir", help="Override data directory path"
|
247
|
+
)
|
248
|
+
self.parser.add_argument(
|
249
|
+
"--runtime-dir", help="Override runtime directory path"
|
250
|
+
)
|
251
|
+
self.parser.add_argument(
|
252
|
+
"--log-file", help="Enable file logging to specified path"
|
253
|
+
)
|
254
|
+
|
255
|
+
def _configure_logging(self, args: argparse.Namespace) -> None:
|
256
|
+
"""
|
257
|
+
Configure logging based on CLI arguments.
|
258
|
+
|
259
|
+
Args:
|
260
|
+
args: Parsed command line arguments
|
261
|
+
"""
|
262
|
+
# Handle verbosity levels
|
263
|
+
# Check both verbose and count attribute (for compatibility)
|
264
|
+
verbosity = max(
|
265
|
+
getattr(args, "verbose", 0) or 0,
|
266
|
+
getattr(args, "count", 0) or 0
|
267
|
+
)
|
268
|
+
quiet = getattr(args, "quiet", False)
|
269
|
+
|
270
|
+
if quiet:
|
271
|
+
# Set to ERROR level for quiet mode
|
272
|
+
set_verbosity(0)
|
273
|
+
else:
|
274
|
+
# Set verbosity based on -v flags
|
275
|
+
# Verbosity mapping:
|
276
|
+
# 0 = INFO (default)
|
277
|
+
# 1 (-v) = DEBUG
|
278
|
+
# 2+ (-vv, -vvv) = DEBUG
|
279
|
+
# Logger expects: 0=ERROR, 1=WARNING, 2=INFO, 3=DEBUG
|
280
|
+
if verbosity == 0:
|
281
|
+
set_verbosity(2) # INFO level (default)
|
282
|
+
else:
|
283
|
+
set_verbosity(3) # DEBUG level for any -v flag
|
284
|
+
|
285
|
+
# Configure file logging if requested
|
286
|
+
# Try both log_file and log-file (argparse converts - to _)
|
287
|
+
log_file = getattr(args, "log_file", None) or getattr(args, "log-file", None)
|
288
|
+
if log_file:
|
289
|
+
from pathlib import Path
|
290
|
+
|
291
|
+
configure_file_logging(Path(log_file))
|
292
|
+
|
293
|
+
def _add_subcommand(
|
294
|
+
self,
|
295
|
+
subparsers: argparse._SubParsersAction,
|
296
|
+
metadata: APIMethodMetadata,
|
297
|
+
) -> None:
|
298
|
+
"""
|
299
|
+
Add a subcommand for a method.
|
300
|
+
|
301
|
+
Args:
|
302
|
+
subparsers: Argparse subparsers object
|
303
|
+
metadata: Method metadata
|
304
|
+
"""
|
305
|
+
# Create subparser with global parent to inherit global options
|
306
|
+
sub = subparsers.add_parser(
|
307
|
+
metadata.cli_name,
|
308
|
+
help=metadata.description,
|
309
|
+
description=metadata.description,
|
310
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
311
|
+
parents=[self.global_parent],
|
312
|
+
)
|
313
|
+
|
314
|
+
# Add aliases if specified
|
315
|
+
for alias in metadata.aliases:
|
316
|
+
subparsers._name_parser_map[alias] = sub
|
317
|
+
|
318
|
+
# Add method parameters as arguments (excluding **kwargs)
|
319
|
+
for param_name, param in metadata.parameters.items():
|
320
|
+
if param.kind != inspect.Parameter.VAR_KEYWORD: # Skip **kwargs
|
321
|
+
self._add_parameter_argument(sub, metadata, param_name, param)
|
322
|
+
|
323
|
+
# Special handling for apply command: add common config parameters
|
324
|
+
if metadata.cli_name == "apply":
|
325
|
+
self._add_apply_config_parameters(sub)
|
326
|
+
|
327
|
+
# Add examples to help if available
|
328
|
+
if metadata.examples:
|
329
|
+
examples_text = "\nExamples:\n" + "\n".join(
|
330
|
+
f" {example}" for example in metadata.examples
|
331
|
+
)
|
332
|
+
sub.epilog = examples_text
|
333
|
+
|
334
|
+
def _add_parent_command(
|
335
|
+
self,
|
336
|
+
subparsers: argparse._SubParsersAction,
|
337
|
+
parent_name: str,
|
338
|
+
child_methods: List[APIMethodMetadata],
|
339
|
+
) -> None:
|
340
|
+
"""
|
341
|
+
Add a parent command with nested subcommands.
|
342
|
+
|
343
|
+
Args:
|
344
|
+
subparsers: Argparse subparsers object
|
345
|
+
parent_name: Name of the parent command (e.g., 'qmp')
|
346
|
+
child_methods: List of child method metadata
|
347
|
+
"""
|
348
|
+
# Create parent subparser with global parent to inherit global options
|
349
|
+
parent_parser = subparsers.add_parser(
|
350
|
+
parent_name,
|
351
|
+
help=f"{parent_name.upper()} subcommands",
|
352
|
+
description=f"{parent_name.upper()} subcommands",
|
353
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
354
|
+
parents=[self.global_parent],
|
355
|
+
)
|
356
|
+
|
357
|
+
# Add nested subparsers for child commands
|
358
|
+
child_subparsers = parent_parser.add_subparsers(
|
359
|
+
dest="subcommand",
|
360
|
+
help="Available subcommands",
|
361
|
+
metavar="SUBCOMMAND",
|
362
|
+
)
|
363
|
+
|
364
|
+
# Add each child method as a nested subcommand
|
365
|
+
for metadata in child_methods:
|
366
|
+
self._add_subcommand(child_subparsers, metadata)
|
367
|
+
|
368
|
+
def _add_parameter_argument(
|
369
|
+
self,
|
370
|
+
parser: argparse.ArgumentParser,
|
371
|
+
metadata: APIMethodMetadata,
|
372
|
+
param_name: str,
|
373
|
+
param: inspect.Parameter,
|
374
|
+
) -> None:
|
375
|
+
"""
|
376
|
+
Add an argument for a method parameter.
|
377
|
+
|
378
|
+
Args:
|
379
|
+
parser: Argument parser
|
380
|
+
metadata: Method metadata for context
|
381
|
+
param_name: Parameter name
|
382
|
+
param: Parameter metadata
|
383
|
+
"""
|
384
|
+
# Determine argument properties
|
385
|
+
is_required = param.default == inspect.Parameter.empty
|
386
|
+
arg_name = param_name.replace("_", "-")
|
387
|
+
|
388
|
+
# Check for Union[str, List[str]] type for multiple files support
|
389
|
+
is_multiple_files = self._is_multiple_files_param(param)
|
390
|
+
|
391
|
+
# Special handling for vm_id parameters - make them positional for
|
392
|
+
# better UX
|
393
|
+
is_vm_id_param = param_name == "vm_id"
|
394
|
+
|
395
|
+
# For rm command, vm_id should be optional positional since it has
|
396
|
+
# --all alternative
|
397
|
+
is_rm_command = metadata.cli_name == "rm"
|
398
|
+
|
399
|
+
# Special handling for VAR_POSITIONAL parameters (*args)
|
400
|
+
is_var_positional = param.kind == inspect.Parameter.VAR_POSITIONAL
|
401
|
+
|
402
|
+
if param.annotation == bool:
|
403
|
+
# Boolean flags
|
404
|
+
if is_required:
|
405
|
+
# Required boolean (rare case)
|
406
|
+
parser.add_argument(
|
407
|
+
f"--{arg_name}",
|
408
|
+
action="store_true",
|
409
|
+
required=True,
|
410
|
+
help=f"{param_name} (required boolean flag)",
|
411
|
+
)
|
412
|
+
else:
|
413
|
+
# Optional boolean flag
|
414
|
+
default_value = (
|
415
|
+
param.default
|
416
|
+
if param.default != inspect.Parameter.empty
|
417
|
+
else False
|
418
|
+
)
|
419
|
+
parser.add_argument(
|
420
|
+
f"--{arg_name}",
|
421
|
+
action=(
|
422
|
+
"store_true" if not default_value else "store_false"
|
423
|
+
),
|
424
|
+
default=default_value,
|
425
|
+
help=f"{param_name} (default: {default_value})",
|
426
|
+
)
|
427
|
+
elif is_multiple_files:
|
428
|
+
# Multiple files parameter (Union[str, List[str]])
|
429
|
+
if is_required:
|
430
|
+
parser.add_argument(
|
431
|
+
param_name,
|
432
|
+
nargs="+",
|
433
|
+
help=f"{param_name} (one or more files)",
|
434
|
+
)
|
435
|
+
else:
|
436
|
+
parser.add_argument(
|
437
|
+
f"--{arg_name}",
|
438
|
+
nargs="*",
|
439
|
+
default=param.default,
|
440
|
+
help=f"{param_name} (one or more files, default: {
|
441
|
+
param.default})",
|
442
|
+
)
|
443
|
+
elif is_vm_id_param:
|
444
|
+
# Special handling for vm_id parameters - make them positional for
|
445
|
+
# better UX
|
446
|
+
if is_rm_command:
|
447
|
+
# For rm command, vm_id is optional positional since --all is
|
448
|
+
# alternative
|
449
|
+
parser.add_argument(
|
450
|
+
param_name,
|
451
|
+
nargs="?",
|
452
|
+
default=param.default,
|
453
|
+
help=f"{
|
454
|
+
param_name} (VM name or ID, optional when using --all)",
|
455
|
+
)
|
456
|
+
elif is_required:
|
457
|
+
# Regular required vm_id (for start, stop, status, etc.)
|
458
|
+
parser.add_argument(
|
459
|
+
param_name, help=f"{param_name} (VM name or ID, required)"
|
460
|
+
)
|
461
|
+
else:
|
462
|
+
# Optional vm_id but still positional
|
463
|
+
parser.add_argument(
|
464
|
+
param_name,
|
465
|
+
nargs="?",
|
466
|
+
default=param.default,
|
467
|
+
help=f"{param_name} (VM name or ID, optional)",
|
468
|
+
)
|
469
|
+
elif is_var_positional:
|
470
|
+
# VAR_POSITIONAL parameters (*args) - use nargs='*' for zero or
|
471
|
+
# more
|
472
|
+
parser.add_argument(
|
473
|
+
param_name,
|
474
|
+
nargs="*",
|
475
|
+
help=f"{param_name} (zero or more values)",
|
476
|
+
)
|
477
|
+
elif is_required:
|
478
|
+
# Required positional argument
|
479
|
+
parser.add_argument(param_name, help=f"{param_name} (required)")
|
480
|
+
else:
|
481
|
+
# Optional argument with default
|
482
|
+
parser.add_argument(
|
483
|
+
f"--{arg_name}",
|
484
|
+
default=param.default,
|
485
|
+
help=f"{param_name} (default: {param.default})",
|
486
|
+
)
|
487
|
+
|
488
|
+
def _is_multiple_files_param(self, param: inspect.Parameter) -> bool:
|
489
|
+
"""
|
490
|
+
Check if parameter accepts multiple files (Union[str, List[str]]).
|
491
|
+
|
492
|
+
Args:
|
493
|
+
param: Parameter to check
|
494
|
+
|
495
|
+
Returns:
|
496
|
+
True if parameter accepts multiple files
|
497
|
+
"""
|
498
|
+
if param.annotation == inspect.Parameter.empty:
|
499
|
+
return False
|
500
|
+
|
501
|
+
# Check for Union[str, List[str]] or similar patterns
|
502
|
+
origin = get_origin(param.annotation)
|
503
|
+
if origin is Union:
|
504
|
+
args = get_args(param.annotation)
|
505
|
+
# Check for Union[str, List[str]] pattern
|
506
|
+
has_str = str in args
|
507
|
+
has_list_str = any(
|
508
|
+
get_origin(arg) is list and get_args(arg) == (str,)
|
509
|
+
for arg in args
|
510
|
+
)
|
511
|
+
return has_str and has_list_str
|
512
|
+
|
513
|
+
return False
|
514
|
+
|
515
|
+
def _execute_method(
|
516
|
+
self, metadata: APIMethodMetadata, args: argparse.Namespace
|
517
|
+
) -> Any:
|
518
|
+
"""
|
519
|
+
Execute a method with parsed arguments.
|
520
|
+
|
521
|
+
Args:
|
522
|
+
metadata: Method metadata
|
523
|
+
args: Parsed command line arguments
|
524
|
+
|
525
|
+
Returns:
|
526
|
+
Method execution result
|
527
|
+
"""
|
528
|
+
# Extract method parameters from parsed args
|
529
|
+
method_kwargs = {}
|
530
|
+
method_args = []
|
531
|
+
|
532
|
+
# Check if method has VAR_POSITIONAL (*args) parameter
|
533
|
+
has_var_positional = any(
|
534
|
+
param.kind == inspect.Parameter.VAR_POSITIONAL
|
535
|
+
for param in metadata.parameters.values()
|
536
|
+
)
|
537
|
+
|
538
|
+
# Collect positional parameters that come before VAR_POSITIONAL
|
539
|
+
positional_params = []
|
540
|
+
for param_name, param in metadata.parameters.items():
|
541
|
+
if param.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD:
|
542
|
+
positional_params.append((param_name, param))
|
543
|
+
elif param.kind == inspect.Parameter.VAR_POSITIONAL:
|
544
|
+
break # Stop when we hit VAR_POSITIONAL
|
545
|
+
|
546
|
+
for param_name, param in metadata.parameters.items():
|
547
|
+
# Skip **kwargs parameters
|
548
|
+
if param.kind == inspect.Parameter.VAR_KEYWORD:
|
549
|
+
continue
|
550
|
+
|
551
|
+
# Convert parameter names (dashes to underscores)
|
552
|
+
arg_name = param_name.replace("_", "-")
|
553
|
+
|
554
|
+
# Get value from args
|
555
|
+
if hasattr(args, param_name):
|
556
|
+
value = getattr(args, param_name)
|
557
|
+
elif hasattr(args, arg_name):
|
558
|
+
value = getattr(args, arg_name.replace("-", "_"))
|
559
|
+
else:
|
560
|
+
continue
|
561
|
+
|
562
|
+
if value is not None:
|
563
|
+
# Handle VAR_POSITIONAL parameters (*args) specially
|
564
|
+
if param.kind == inspect.Parameter.VAR_POSITIONAL:
|
565
|
+
# For *args parameters, extend the args list
|
566
|
+
if isinstance(value, list):
|
567
|
+
method_args.extend(value)
|
568
|
+
else:
|
569
|
+
method_args.append(value)
|
570
|
+
elif (
|
571
|
+
param.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD
|
572
|
+
and has_var_positional
|
573
|
+
):
|
574
|
+
# When method has VAR_POSITIONAL, regular positional params
|
575
|
+
# must go in method_args to avoid "multiple values" error
|
576
|
+
method_args.insert(0, value)
|
577
|
+
else:
|
578
|
+
# Everything else goes in kwargs
|
579
|
+
method_kwargs[param_name] = value
|
580
|
+
|
581
|
+
# Special handling for apply command: collect config parameters as
|
582
|
+
# kwargs
|
583
|
+
if metadata.cli_name == "apply":
|
584
|
+
config_params = {}
|
585
|
+
for param_name in ["memory", "cpu", "binary", "enable_kvm"]:
|
586
|
+
arg_name = param_name.replace("_", "-")
|
587
|
+
if hasattr(args, arg_name.replace("-", "_")):
|
588
|
+
value = getattr(args, arg_name.replace("-", "_"))
|
589
|
+
if value is not None:
|
590
|
+
config_params[param_name] = value
|
591
|
+
|
592
|
+
# Add config parameters to method kwargs
|
593
|
+
method_kwargs.update(config_params)
|
594
|
+
|
595
|
+
# Resolve config file paths to absolute paths for consistent handling
|
596
|
+
if "config" in method_kwargs and method_kwargs["config"] is not None:
|
597
|
+
|
598
|
+
config = method_kwargs["config"]
|
599
|
+
if isinstance(config, str):
|
600
|
+
# Single config file - resolve to absolute path
|
601
|
+
method_kwargs["config"] = os.path.abspath(config)
|
602
|
+
elif isinstance(config, list):
|
603
|
+
# Multiple config files - resolve each to absolute path
|
604
|
+
method_kwargs["config"] = [os.path.abspath(c) for c in config]
|
605
|
+
|
606
|
+
# Execute method directly
|
607
|
+
method = getattr(self.maqet_instance, metadata.name)
|
608
|
+
|
609
|
+
# Execute method with both positional and keyword arguments
|
610
|
+
return method(*method_args, **method_kwargs)
|
611
|
+
|
612
|
+
def _add_apply_config_parameters(
|
613
|
+
self, parser: argparse.ArgumentParser
|
614
|
+
) -> None:
|
615
|
+
"""
|
616
|
+
Add common configuration parameters for the apply command.
|
617
|
+
|
618
|
+
Args:
|
619
|
+
parser: Argument parser for the apply subcommand
|
620
|
+
"""
|
621
|
+
# Memory configuration
|
622
|
+
parser.add_argument(
|
623
|
+
"--memory", type=str, help="VM memory size (e.g., 2G, 4096M)"
|
624
|
+
)
|
625
|
+
|
626
|
+
# CPU configuration
|
627
|
+
parser.add_argument("--cpu", type=int, help="Number of CPU cores")
|
628
|
+
|
629
|
+
# Binary path
|
630
|
+
parser.add_argument("--binary", type=str, help="Path to QEMU binary")
|
631
|
+
|
632
|
+
# KVM enablement
|
633
|
+
parser.add_argument(
|
634
|
+
"--enable-kvm", action="store_true", help="Enable KVM acceleration"
|
635
|
+
)
|