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