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
maqet/maqet.py
ADDED
@@ -0,0 +1,1120 @@
|
|
1
|
+
"""
|
2
|
+
MAQET Core
|
3
|
+
|
4
|
+
Main MAQET class implementing unified API for VM management.
|
5
|
+
All methods are decorated with @api_method to enable automatic CLI
|
6
|
+
and Python API generation.
|
7
|
+
"""
|
8
|
+
|
9
|
+
import sys
|
10
|
+
import os
|
11
|
+
from pathlib import Path
|
12
|
+
from typing import Any, Dict, List, Optional, Union
|
13
|
+
|
14
|
+
from .api import (
|
15
|
+
API_REGISTRY,
|
16
|
+
APIRegistry,
|
17
|
+
AutoRegisterAPI,
|
18
|
+
api_method,
|
19
|
+
)
|
20
|
+
from .config import ConfigMerger, ConfigParser
|
21
|
+
from .constants import Timeouts
|
22
|
+
from .exceptions import MaqetError, QMPError, SnapshotError, VMLifecycleError
|
23
|
+
from .generators import CLIGenerator, PythonAPIGenerator
|
24
|
+
from .logger import LOG
|
25
|
+
from .machine import Machine
|
26
|
+
from .managers import QMPManager, SnapshotCoordinator, VMManager
|
27
|
+
from .snapshot import SnapshotManager
|
28
|
+
from .state import StateManager, VMInstance
|
29
|
+
from .storage import StorageManager
|
30
|
+
|
31
|
+
# Legacy exception aliases (backward compatibility)
|
32
|
+
from .exceptions import ConfigError
|
33
|
+
from .exceptions import QMPManagerError
|
34
|
+
from .exceptions import SnapshotCoordinatorError
|
35
|
+
from .exceptions import StateManagerError
|
36
|
+
from .exceptions import VMManagerError
|
37
|
+
|
38
|
+
|
39
|
+
class Maqet(AutoRegisterAPI):
|
40
|
+
"""
|
41
|
+
MAQET - M4x0n's QEMU Tool
|
42
|
+
|
43
|
+
Unified VM management system that provides CLI commands, Python API,
|
44
|
+
and configuration-based
|
45
|
+
VM orchestration through a single decorated method interface.
|
46
|
+
|
47
|
+
This class implements your vision of "write once, generate everywhere"
|
48
|
+
- each @api_method
|
49
|
+
decorated method automatically becomes available as:
|
50
|
+
- CLI command (via maqet <command>)
|
51
|
+
- Python API method (via maqet.method())
|
52
|
+
- Configuration file key (via YAML parsing)
|
53
|
+
|
54
|
+
# TODO(architect, 2025-10-10): [ARCH] CRITICAL - God Object Pattern (1496 lines)
|
55
|
+
# Context: This class violates Single Responsibility Principle. It handles:
|
56
|
+
# - VM lifecycle (add, start, stop, rm, ls)
|
57
|
+
# - QMP operations (qmp, keys, type, screendump, pause, resume, device-add/del)
|
58
|
+
# - Storage/snapshots (snapshot create/load/list)
|
59
|
+
# - State coordination (StateManager integration)
|
60
|
+
# - Config parsing (ConfigParser integration)
|
61
|
+
# - CLI/API generation coordination
|
62
|
+
# This is Issue #2 in ARCHITECTURAL_REVIEW.md.
|
63
|
+
#
|
64
|
+
# Impact: Maintainability, testing difficulty, tight coupling, hard to extend
|
65
|
+
#
|
66
|
+
# Recommendation: Refactor into sub-managers:
|
67
|
+
# - VMManager: start, stop, lifecycle
|
68
|
+
# - QMPManager: all QMP operations
|
69
|
+
# - SnapshotCoordinator: snapshot operations
|
70
|
+
# - ConfigManager: configuration handling
|
71
|
+
# Maqet becomes facade delegating to managers.
|
72
|
+
#
|
73
|
+
# Effort: Large (1-2 weeks), but can be done incrementally
|
74
|
+
# Priority: Critical for 1.0 release
|
75
|
+
# See: ARCHITECTURAL_REVIEW.md Issue #2 for detailed refactoring plan
|
76
|
+
|
77
|
+
# ARCHITECTURAL DESIGN: In-memory Machine Instances
|
78
|
+
# ================================================
|
79
|
+
# Design Choice: Machine instances stored in memory dict (_machines)
|
80
|
+
# - Simple, fast, no serialization overhead
|
81
|
+
# - Perfect for Python API usage (long-running scripts)
|
82
|
+
# - Trade-off: Lost between CLI invocations
|
83
|
+
#
|
84
|
+
# Implications:
|
85
|
+
# - CLI Mode: Each command runs in fresh process
|
86
|
+
# * VM state persisted in SQLite (~/.local/share/maqet/instances.db)
|
87
|
+
# * Machine objects recreated on each CLI call
|
88
|
+
# * QMP connections NOT maintained across CLI calls
|
89
|
+
# - Python API Mode: Single process, instances persist
|
90
|
+
# * maqet = Maqet(); maqet.start("vm1"); maqet.qmp("vm1", "query-status")
|
91
|
+
# * QMP works seamlessly within same process
|
92
|
+
#
|
93
|
+
# When to use each mode:
|
94
|
+
# - CLI Mode: Simple VM management (start, stop, status, info, inspect)
|
95
|
+
# - Python API: Automation scripts, CI/CD pipelines, persistent QMP
|
96
|
+
#
|
97
|
+
# QMP commands work in Python API mode where Machine instances persist
|
98
|
+
# across method calls. For CLI workflows requiring QMP, use Python API.
|
99
|
+
"""
|
100
|
+
|
101
|
+
def __init__(
|
102
|
+
self, data_dir: Optional[str] = None, register_signals: bool = True
|
103
|
+
):
|
104
|
+
"""
|
105
|
+
Initialize MAQET instance.
|
106
|
+
|
107
|
+
Args:
|
108
|
+
data_dir: Override default XDG data directory
|
109
|
+
register_signals: Register signal handlers for graceful shutdown (default: True)
|
110
|
+
"""
|
111
|
+
self.state_manager = StateManager(data_dir)
|
112
|
+
self.config_parser = ConfigParser(self)
|
113
|
+
self._machines: Dict[str, Machine] = {}
|
114
|
+
self._signal_handlers_registered = False
|
115
|
+
|
116
|
+
# Initialize managers
|
117
|
+
self.vm_manager = VMManager(self.state_manager, self.config_parser)
|
118
|
+
self.qmp_manager = QMPManager(self.state_manager)
|
119
|
+
self.snapshot_coordinator = SnapshotCoordinator(self.state_manager)
|
120
|
+
|
121
|
+
# Create instance-specific API registry
|
122
|
+
# This allows parallel test execution and multiple Maqet instances
|
123
|
+
# with isolated registries (no cross-contamination)
|
124
|
+
self._api_registry = APIRegistry()
|
125
|
+
self._api_registry.register_from_instance(self)
|
126
|
+
|
127
|
+
if register_signals:
|
128
|
+
self._register_signal_handlers()
|
129
|
+
|
130
|
+
# Clean up any stale runner processes via VM manager
|
131
|
+
self.vm_manager.cleanup_dead_processes()
|
132
|
+
|
133
|
+
def __enter__(self):
|
134
|
+
"""Context manager entry."""
|
135
|
+
return self
|
136
|
+
|
137
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
138
|
+
"""Context manager exit - cleanup resources."""
|
139
|
+
self.cleanup()
|
140
|
+
return False # Don't suppress exceptions
|
141
|
+
|
142
|
+
def _register_signal_handlers(self) -> None:
|
143
|
+
"""Register signal handlers for graceful shutdown."""
|
144
|
+
import signal
|
145
|
+
|
146
|
+
def signal_handler(signum, frame):
|
147
|
+
LOG.info(
|
148
|
+
f"Received signal {signum}, initiating graceful shutdown..."
|
149
|
+
)
|
150
|
+
self.cleanup()
|
151
|
+
sys.exit(0)
|
152
|
+
|
153
|
+
# Register handlers for SIGINT (Ctrl+C) and SIGTERM
|
154
|
+
signal.signal(signal.SIGINT, signal_handler)
|
155
|
+
signal.signal(signal.SIGTERM, signal_handler)
|
156
|
+
self._signal_handlers_registered = True
|
157
|
+
LOG.debug("Signal handlers registered for graceful shutdown")
|
158
|
+
|
159
|
+
def get_api_registry(self) -> APIRegistry:
|
160
|
+
"""
|
161
|
+
Get the instance-specific API registry.
|
162
|
+
|
163
|
+
Returns:
|
164
|
+
APIRegistry instance for this Maqet instance
|
165
|
+
|
166
|
+
Example:
|
167
|
+
maqet = Maqet()
|
168
|
+
registry = maqet.get_api_registry()
|
169
|
+
methods = registry.get_all_methods()
|
170
|
+
"""
|
171
|
+
return self._api_registry
|
172
|
+
|
173
|
+
def cleanup(self) -> None:
|
174
|
+
"""Clean up all resources (stop running VMs, close connections).
|
175
|
+
|
176
|
+
Uses ThreadPoolExecutor for parallel VM shutdown to reduce total cleanup time.
|
177
|
+
Global timeout of 60 seconds prevents cleanup from blocking indefinitely.
|
178
|
+
"""
|
179
|
+
from concurrent.futures import ThreadPoolExecutor
|
180
|
+
from concurrent.futures import TimeoutError as FuturesTimeoutError
|
181
|
+
|
182
|
+
LOG.debug("Cleaning up MAQET resources...")
|
183
|
+
|
184
|
+
# Stop all running VMs
|
185
|
+
running_vms = [
|
186
|
+
vm_id
|
187
|
+
for vm_id, machine in self._machines.items()
|
188
|
+
if machine._qemu_machine and machine._qemu_machine.is_running()
|
189
|
+
]
|
190
|
+
|
191
|
+
if running_vms:
|
192
|
+
LOG.info(
|
193
|
+
f"Stopping {len(running_vms)} running VM(s) in parallel..."
|
194
|
+
)
|
195
|
+
|
196
|
+
def stop_vm(vm_id: str) -> tuple[str, bool, str]:
|
197
|
+
"""Stop a single VM. Returns (vm_id, success, error_msg)."""
|
198
|
+
try:
|
199
|
+
LOG.debug(f"Stopping VM {vm_id}")
|
200
|
+
self.stop(vm_id, force=True)
|
201
|
+
return (vm_id, True, "")
|
202
|
+
except Exception as e:
|
203
|
+
return (vm_id, False, str(e))
|
204
|
+
|
205
|
+
# Use ThreadPoolExecutor for parallel shutdown
|
206
|
+
# Max 10 threads to avoid overwhelming system
|
207
|
+
max_workers = min(10, len(running_vms))
|
208
|
+
|
209
|
+
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
210
|
+
# Submit all stop tasks
|
211
|
+
futures = {
|
212
|
+
executor.submit(stop_vm, vm_id): vm_id
|
213
|
+
for vm_id in running_vms
|
214
|
+
}
|
215
|
+
|
216
|
+
try:
|
217
|
+
# Wait for all tasks with global timeout
|
218
|
+
for future in futures:
|
219
|
+
try:
|
220
|
+
# Per-VM timeout
|
221
|
+
vm_id, success, error = future.result(
|
222
|
+
timeout=Timeouts.CLEANUP_VM_STOP
|
223
|
+
)
|
224
|
+
if not success:
|
225
|
+
LOG.warning(
|
226
|
+
f"Failed to stop VM {
|
227
|
+
vm_id} during cleanup: {error}"
|
228
|
+
)
|
229
|
+
except FuturesTimeoutError:
|
230
|
+
vm_id = futures[future]
|
231
|
+
LOG.warning(
|
232
|
+
f"Timeout stopping VM {vm_id} during cleanup"
|
233
|
+
)
|
234
|
+
except Exception as e:
|
235
|
+
vm_id = futures[future]
|
236
|
+
LOG.warning(
|
237
|
+
f"Unexpected error stopping VM {vm_id}: {e}"
|
238
|
+
)
|
239
|
+
|
240
|
+
except Exception as e:
|
241
|
+
LOG.error(f"Error during parallel VM cleanup: {e}")
|
242
|
+
|
243
|
+
# Clear machine cache
|
244
|
+
self._machines.clear()
|
245
|
+
LOG.debug("MAQET cleanup completed")
|
246
|
+
|
247
|
+
@api_method(
|
248
|
+
cli_name="add",
|
249
|
+
description="Create a new VM from configuration",
|
250
|
+
category="vm",
|
251
|
+
examples=[
|
252
|
+
"maqet add config.yaml",
|
253
|
+
"maqet add config.yaml --name myvm",
|
254
|
+
"maqet add --name testvm --memory 4G --cpu 2",
|
255
|
+
"maqet add base.yaml custom.yaml --name myvm",
|
256
|
+
"maqet add base.yaml --memory 8G",
|
257
|
+
"maqet add --name empty-vm --empty",
|
258
|
+
],
|
259
|
+
)
|
260
|
+
def add(
|
261
|
+
self,
|
262
|
+
config: Optional[Union[str, List[str]]] = None,
|
263
|
+
name: Optional[str] = None,
|
264
|
+
empty: bool = False,
|
265
|
+
**kwargs,
|
266
|
+
) -> str:
|
267
|
+
"""
|
268
|
+
Create a new VM from configuration file(s) or parameters.
|
269
|
+
|
270
|
+
Delegates to VMManager for actual VM creation logic.
|
271
|
+
|
272
|
+
Args:
|
273
|
+
config: Path to YAML configuration file, or list of config
|
274
|
+
files for deep-merge
|
275
|
+
name: VM name (auto-generated if not provided)
|
276
|
+
empty: Create empty VM without any configuration (won't be
|
277
|
+
startable until configured)
|
278
|
+
**kwargs: Additional VM configuration parameters
|
279
|
+
|
280
|
+
Returns:
|
281
|
+
VM instance ID
|
282
|
+
|
283
|
+
Raises:
|
284
|
+
MaqetError: If VM creation fails
|
285
|
+
|
286
|
+
Examples:
|
287
|
+
Single config: add(config="vm.yaml", name="myvm")
|
288
|
+
Multiple configs: add(
|
289
|
+
config=["base.yaml", "custom.yaml"], name="myvm"
|
290
|
+
)
|
291
|
+
Config + params: add(config="base.yaml", memory="8G", cpu=4)
|
292
|
+
Empty VM: add(name="placeholder-vm", empty=True)
|
293
|
+
"""
|
294
|
+
try:
|
295
|
+
return self.vm_manager.add(config, name, empty, **kwargs)
|
296
|
+
except VMManagerError as e:
|
297
|
+
raise MaqetError(str(e))
|
298
|
+
|
299
|
+
@api_method(
|
300
|
+
cli_name="start",
|
301
|
+
description="Start a virtual machine",
|
302
|
+
category="vm",
|
303
|
+
requires_vm=True,
|
304
|
+
examples=["maqet start myvm"],
|
305
|
+
)
|
306
|
+
def start(self, vm_id: str) -> VMInstance:
|
307
|
+
"""
|
308
|
+
Start a virtual machine by spawning a detached VM runner process.
|
309
|
+
|
310
|
+
Delegates to VMManager for actual VM start logic.
|
311
|
+
|
312
|
+
Args:
|
313
|
+
vm_id: VM identifier (name or ID)
|
314
|
+
|
315
|
+
Returns:
|
316
|
+
VM instance information
|
317
|
+
|
318
|
+
Raises:
|
319
|
+
MaqetError: If VM start fails
|
320
|
+
"""
|
321
|
+
try:
|
322
|
+
return self.vm_manager.start(vm_id)
|
323
|
+
except VMManagerError as e:
|
324
|
+
raise MaqetError(str(e))
|
325
|
+
|
326
|
+
@api_method(
|
327
|
+
cli_name="stop",
|
328
|
+
description="Stop a virtual machine",
|
329
|
+
category="vm",
|
330
|
+
requires_vm=True,
|
331
|
+
examples=["maqet stop myvm", "maqet stop myvm --force"],
|
332
|
+
)
|
333
|
+
def stop(
|
334
|
+
self, vm_id: str, force: bool = False, timeout: int = Timeouts.VM_STOP
|
335
|
+
) -> VMInstance:
|
336
|
+
"""
|
337
|
+
Stop a VM by sending stop command to VM runner or killing runner process.
|
338
|
+
|
339
|
+
Delegates to VMManager for actual VM stop logic.
|
340
|
+
|
341
|
+
Args:
|
342
|
+
vm_id: VM identifier (name or ID)
|
343
|
+
force: If True, kill runner immediately (SIGKILL).
|
344
|
+
If False, graceful shutdown (SIGTERM)
|
345
|
+
timeout: Timeout for graceful shutdown
|
346
|
+
|
347
|
+
Returns:
|
348
|
+
VM instance information
|
349
|
+
|
350
|
+
Raises:
|
351
|
+
MaqetError: If VM stop fails
|
352
|
+
"""
|
353
|
+
try:
|
354
|
+
return self.vm_manager.stop(vm_id, force, timeout)
|
355
|
+
except VMManagerError as e:
|
356
|
+
raise MaqetError(str(e))
|
357
|
+
|
358
|
+
@api_method(
|
359
|
+
cli_name="rm",
|
360
|
+
description="Remove a virtual machine",
|
361
|
+
category="vm",
|
362
|
+
requires_vm=False,
|
363
|
+
examples=[
|
364
|
+
"maqet rm myvm",
|
365
|
+
"maqet rm myvm --force",
|
366
|
+
"maqet rm --all",
|
367
|
+
"maqet rm --all --force",
|
368
|
+
],
|
369
|
+
)
|
370
|
+
def rm(
|
371
|
+
self,
|
372
|
+
vm_id: Optional[str] = None,
|
373
|
+
force: bool = False,
|
374
|
+
all: bool = False,
|
375
|
+
clean_storage: bool = False,
|
376
|
+
) -> bool:
|
377
|
+
"""
|
378
|
+
Remove a virtual machine completely.
|
379
|
+
|
380
|
+
Delegates to VMManager for actual VM removal logic.
|
381
|
+
|
382
|
+
Args:
|
383
|
+
vm_id: VM identifier (name or ID)
|
384
|
+
force: Force removal even if VM is running
|
385
|
+
all: Remove all virtual machines
|
386
|
+
clean_storage: Also delete associated storage files
|
387
|
+
|
388
|
+
Returns:
|
389
|
+
True if removed successfully
|
390
|
+
|
391
|
+
Raises:
|
392
|
+
MaqetError: If VM removal fails
|
393
|
+
"""
|
394
|
+
try:
|
395
|
+
result = self.vm_manager.remove(vm_id, force, all, clean_storage)
|
396
|
+
# Clean up machine instances that were removed
|
397
|
+
if all:
|
398
|
+
self._machines.clear()
|
399
|
+
elif vm_id:
|
400
|
+
vm = self.state_manager.get_vm(vm_id)
|
401
|
+
if vm:
|
402
|
+
self._machines.pop(vm.id, None)
|
403
|
+
return result
|
404
|
+
except VMManagerError as e:
|
405
|
+
raise MaqetError(str(e))
|
406
|
+
|
407
|
+
@api_method(
|
408
|
+
cli_name="ls",
|
409
|
+
description="List virtual machines in table format",
|
410
|
+
category="vm",
|
411
|
+
examples=["maqet ls", "maqet ls --status running"],
|
412
|
+
)
|
413
|
+
def ls(self, status: Optional[str] = None) -> str:
|
414
|
+
"""
|
415
|
+
List virtual machines in readable table format.
|
416
|
+
|
417
|
+
Delegates to VMManager for VM list retrieval.
|
418
|
+
|
419
|
+
Args:
|
420
|
+
status: Filter by status ('running', 'stopped', 'created',
|
421
|
+
'failed')
|
422
|
+
|
423
|
+
Returns:
|
424
|
+
Formatted table string
|
425
|
+
"""
|
426
|
+
vms = self.vm_manager.list_vms(status)
|
427
|
+
|
428
|
+
if not vms:
|
429
|
+
return "No virtual machines found."
|
430
|
+
|
431
|
+
# Create table header
|
432
|
+
header = f"{'NAME':<20} {'STATUS':<10} {'PID':<8}"
|
433
|
+
separator = "-" * 40
|
434
|
+
|
435
|
+
# Build table rows
|
436
|
+
rows = [header, separator]
|
437
|
+
for vm in vms:
|
438
|
+
pid_str = str(vm.pid) if vm.pid else "-"
|
439
|
+
row = f"{vm.name:<20} {vm.status:<10} {pid_str:<8}"
|
440
|
+
rows.append(row)
|
441
|
+
|
442
|
+
return "\n".join(rows)
|
443
|
+
|
444
|
+
@api_method(
|
445
|
+
cli_name="status",
|
446
|
+
description="Show comprehensive VM status information",
|
447
|
+
category="vm",
|
448
|
+
requires_vm=True,
|
449
|
+
examples=["maqet status myvm", "maqet status myvm --detailed"],
|
450
|
+
)
|
451
|
+
def status(self, vm_id: str, detailed: bool = False) -> Dict[str, Any]:
|
452
|
+
"""
|
453
|
+
Get basic status information for a VM.
|
454
|
+
|
455
|
+
Args:
|
456
|
+
vm_id: VM identifier (name or ID)
|
457
|
+
detailed: (DEPRECATED) Use 'maqet inspect' instead for detailed information
|
458
|
+
|
459
|
+
Returns:
|
460
|
+
Dictionary with basic VM status information
|
461
|
+
|
462
|
+
Raises:
|
463
|
+
MaqetError: If VM not found
|
464
|
+
"""
|
465
|
+
# Handle deprecated detailed flag
|
466
|
+
if detailed:
|
467
|
+
LOG.warning(
|
468
|
+
"The --detailed flag for 'status' command is deprecated. "
|
469
|
+
"Use 'maqet inspect %s' for detailed VM inspection instead.",
|
470
|
+
vm_id
|
471
|
+
)
|
472
|
+
# Redirect to inspect method for backward compatibility
|
473
|
+
return self.inspect(vm_id)
|
474
|
+
|
475
|
+
try:
|
476
|
+
vm = self.state_manager.get_vm(vm_id)
|
477
|
+
if not vm:
|
478
|
+
raise MaqetError(f"VM '{vm_id}' not found")
|
479
|
+
|
480
|
+
# Check if process is actually running and update status
|
481
|
+
is_actually_running = self._check_process_alive(vm_id, vm)
|
482
|
+
|
483
|
+
# Check if VM is empty/unconfigured
|
484
|
+
is_empty_vm = not vm.config_data or not vm.config_data.get(
|
485
|
+
"binary"
|
486
|
+
)
|
487
|
+
|
488
|
+
# Build simplified status response (no configuration or detailed info)
|
489
|
+
status_info = {
|
490
|
+
"name": vm.name,
|
491
|
+
"status": vm.status,
|
492
|
+
"is_running": is_actually_running,
|
493
|
+
"is_empty": is_empty_vm,
|
494
|
+
"pid": vm.pid,
|
495
|
+
"socket_path": vm.socket_path,
|
496
|
+
}
|
497
|
+
|
498
|
+
# Add QMP socket info if socket exists
|
499
|
+
if vm.socket_path:
|
500
|
+
status_info["qmp_socket"] = {
|
501
|
+
"path": vm.socket_path,
|
502
|
+
"exists": os.path.exists(vm.socket_path),
|
503
|
+
}
|
504
|
+
|
505
|
+
return status_info
|
506
|
+
|
507
|
+
except Exception as e:
|
508
|
+
raise MaqetError(f"Failed to get status for VM '{vm_id}': {e}")
|
509
|
+
|
510
|
+
@api_method(
|
511
|
+
cli_name="info",
|
512
|
+
description="Show VM configuration details",
|
513
|
+
category="vm",
|
514
|
+
requires_vm=True,
|
515
|
+
examples=["maqet info myvm"],
|
516
|
+
)
|
517
|
+
def info(self, vm_id: str) -> Dict[str, Any]:
|
518
|
+
"""
|
519
|
+
Get VM configuration details.
|
520
|
+
|
521
|
+
This method provides configuration information about a VM,
|
522
|
+
including binary, memory, CPU, display settings, and storage devices.
|
523
|
+
It's a focused view of the VM's configuration without runtime details.
|
524
|
+
|
525
|
+
Args:
|
526
|
+
vm_id: VM identifier (name or ID)
|
527
|
+
|
528
|
+
Returns:
|
529
|
+
Dictionary with VM configuration details
|
530
|
+
|
531
|
+
Raises:
|
532
|
+
MaqetError: If VM not found
|
533
|
+
"""
|
534
|
+
try:
|
535
|
+
vm = self.state_manager.get_vm(vm_id)
|
536
|
+
if not vm:
|
537
|
+
raise MaqetError(f"VM '{vm_id}' not found")
|
538
|
+
|
539
|
+
# Build info response with configuration details
|
540
|
+
info_data = {
|
541
|
+
"vm_id": vm.id,
|
542
|
+
"name": vm.name,
|
543
|
+
"config_path": vm.config_path,
|
544
|
+
"config_data": vm.config_data,
|
545
|
+
"configuration": self._get_config_summary(vm.config_data),
|
546
|
+
}
|
547
|
+
|
548
|
+
return info_data
|
549
|
+
|
550
|
+
except Exception as e:
|
551
|
+
raise MaqetError(f"Failed to get info for VM '{vm_id}': {e}")
|
552
|
+
|
553
|
+
@api_method(
|
554
|
+
cli_name="inspect",
|
555
|
+
description="Inspect VM with detailed process and resource information",
|
556
|
+
category="vm",
|
557
|
+
requires_vm=True,
|
558
|
+
examples=["maqet inspect myvm"],
|
559
|
+
)
|
560
|
+
def inspect(self, vm_id: str) -> Dict[str, Any]:
|
561
|
+
"""
|
562
|
+
Get detailed inspection information for a VM.
|
563
|
+
|
564
|
+
This method provides comprehensive information including VM status,
|
565
|
+
configuration, process details (if running), QMP socket status,
|
566
|
+
and snapshot information. It's the most detailed view of a VM.
|
567
|
+
|
568
|
+
Args:
|
569
|
+
vm_id: VM identifier (name or ID)
|
570
|
+
|
571
|
+
Returns:
|
572
|
+
Dictionary with comprehensive VM inspection data
|
573
|
+
|
574
|
+
Raises:
|
575
|
+
MaqetError: If VM not found
|
576
|
+
"""
|
577
|
+
try:
|
578
|
+
vm = self.state_manager.get_vm(vm_id)
|
579
|
+
if not vm:
|
580
|
+
raise MaqetError(f"VM '{vm_id}' not found")
|
581
|
+
|
582
|
+
# Check if process is actually running
|
583
|
+
is_actually_running = self._check_process_alive(vm_id, vm)
|
584
|
+
|
585
|
+
# Build comprehensive inspection response
|
586
|
+
inspect_data = {
|
587
|
+
"vm_id": vm.id,
|
588
|
+
"name": vm.name,
|
589
|
+
"status": vm.status,
|
590
|
+
"is_running": is_actually_running,
|
591
|
+
"pid": vm.pid,
|
592
|
+
"socket_path": vm.socket_path,
|
593
|
+
"config_path": vm.config_path,
|
594
|
+
"created_at": (
|
595
|
+
vm.created_at.isoformat() if vm.created_at else None
|
596
|
+
),
|
597
|
+
"updated_at": (
|
598
|
+
vm.updated_at.isoformat() if vm.updated_at else None
|
599
|
+
),
|
600
|
+
"configuration": self._get_config_summary(vm.config_data),
|
601
|
+
}
|
602
|
+
|
603
|
+
# Add process details if running
|
604
|
+
if is_actually_running and vm.pid:
|
605
|
+
process_info = self._get_process_info(vm.pid)
|
606
|
+
if process_info:
|
607
|
+
inspect_data["process"] = process_info
|
608
|
+
|
609
|
+
# Add QMP socket status
|
610
|
+
if vm.socket_path:
|
611
|
+
inspect_data["qmp_socket"] = {
|
612
|
+
"path": vm.socket_path,
|
613
|
+
"exists": os.path.exists(vm.socket_path),
|
614
|
+
}
|
615
|
+
|
616
|
+
return inspect_data
|
617
|
+
|
618
|
+
except Exception as e:
|
619
|
+
raise MaqetError(f"Failed to inspect VM '{vm_id}': {e}")
|
620
|
+
|
621
|
+
def _check_process_alive(self, vm_id: str, vm: VMInstance) -> bool:
|
622
|
+
"""
|
623
|
+
Check if VM process is actually alive and update status if needed.
|
624
|
+
|
625
|
+
Args:
|
626
|
+
vm_id: VM identifier
|
627
|
+
vm: VM instance
|
628
|
+
|
629
|
+
Returns:
|
630
|
+
True if process is alive, False otherwise
|
631
|
+
"""
|
632
|
+
if vm.status != "running" or not vm.pid:
|
633
|
+
return False
|
634
|
+
|
635
|
+
try:
|
636
|
+
# Send signal 0 to check if process exists
|
637
|
+
os.kill(vm.pid, 0)
|
638
|
+
return True
|
639
|
+
except OSError:
|
640
|
+
# Process doesn't exist, update status
|
641
|
+
self.state_manager.update_vm_status(
|
642
|
+
vm.id, "stopped", pid=None, socket_path=None
|
643
|
+
)
|
644
|
+
return False
|
645
|
+
|
646
|
+
def _get_config_summary(self, config_data: Dict[str, Any]) -> Dict[str, Any]:
|
647
|
+
"""
|
648
|
+
Extract configuration summary from config data.
|
649
|
+
|
650
|
+
Args:
|
651
|
+
config_data: VM configuration dictionary
|
652
|
+
|
653
|
+
Returns:
|
654
|
+
Dictionary with configuration summary
|
655
|
+
"""
|
656
|
+
summary = {
|
657
|
+
"binary": config_data.get("binary"),
|
658
|
+
"memory": config_data.get("memory"),
|
659
|
+
"cpu": config_data.get("cpu"),
|
660
|
+
"display": config_data.get("display"),
|
661
|
+
}
|
662
|
+
|
663
|
+
# Count and list storage devices
|
664
|
+
storage_devices = config_data.get("storage", [])
|
665
|
+
if isinstance(storage_devices, list):
|
666
|
+
summary["storage_count"] = len(storage_devices)
|
667
|
+
summary["storage_devices"] = [
|
668
|
+
{
|
669
|
+
"name": dev.get("name", "unnamed"),
|
670
|
+
"type": dev.get("type", "unknown"),
|
671
|
+
"size": dev.get("size"),
|
672
|
+
}
|
673
|
+
for dev in storage_devices
|
674
|
+
]
|
675
|
+
else:
|
676
|
+
summary["storage_count"] = 0
|
677
|
+
summary["storage_devices"] = []
|
678
|
+
|
679
|
+
return summary
|
680
|
+
|
681
|
+
def _get_process_info(self, pid: int) -> Optional[Dict[str, Any]]:
|
682
|
+
"""
|
683
|
+
Get detailed process information using psutil if available.
|
684
|
+
|
685
|
+
Args:
|
686
|
+
pid: Process ID
|
687
|
+
|
688
|
+
Returns:
|
689
|
+
Dictionary with process information or None if psutil not available
|
690
|
+
"""
|
691
|
+
try:
|
692
|
+
import psutil
|
693
|
+
|
694
|
+
try:
|
695
|
+
proc = psutil.Process(pid)
|
696
|
+
return {
|
697
|
+
"cpu_percent": proc.cpu_percent(),
|
698
|
+
"memory_info": proc.memory_info()._asdict(),
|
699
|
+
"create_time": proc.create_time(),
|
700
|
+
"cmdline": proc.cmdline(),
|
701
|
+
"status": proc.status(),
|
702
|
+
}
|
703
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
704
|
+
return None
|
705
|
+
except ImportError:
|
706
|
+
# psutil not available
|
707
|
+
return {"note": "Install psutil for detailed process information"}
|
708
|
+
|
709
|
+
@api_method(
|
710
|
+
cli_name="qmp",
|
711
|
+
description="Execute QMP command on VM",
|
712
|
+
category="qmp",
|
713
|
+
requires_vm=True,
|
714
|
+
hidden=True,
|
715
|
+
examples=[
|
716
|
+
"maqet qmp myvm system_powerdown",
|
717
|
+
"maqet qmp myvm screendump --filename screenshot.ppm",
|
718
|
+
],
|
719
|
+
)
|
720
|
+
def qmp(self, vm_id: str, command: str, **kwargs) -> Dict[str, Any]:
|
721
|
+
"""
|
722
|
+
Execute QMP command (delegates to QMPManager).
|
723
|
+
|
724
|
+
Args:
|
725
|
+
vm_id: VM identifier (name or ID)
|
726
|
+
command: QMP command to execute
|
727
|
+
**kwargs: Command parameters
|
728
|
+
|
729
|
+
Returns:
|
730
|
+
QMP command result
|
731
|
+
|
732
|
+
Raises:
|
733
|
+
MaqetError: If VM not found or command fails
|
734
|
+
"""
|
735
|
+
try:
|
736
|
+
return self.qmp_manager.execute_qmp(vm_id, command, **kwargs)
|
737
|
+
except QMPManagerError as e:
|
738
|
+
raise MaqetError(str(e))
|
739
|
+
|
740
|
+
@api_method(
|
741
|
+
cli_name="keys",
|
742
|
+
description="Send key combination to VM via QMP",
|
743
|
+
category="qmp",
|
744
|
+
requires_vm=True,
|
745
|
+
parent="qmp",
|
746
|
+
examples=[
|
747
|
+
"maqet qmp keys myvm ctrl alt f2",
|
748
|
+
"maqet qmp keys myvm --hold-time 200 ctrl c",
|
749
|
+
],
|
750
|
+
)
|
751
|
+
def qmp_key(
|
752
|
+
self, vm_id: str, *keys: str, hold_time: int = 100
|
753
|
+
) -> Dict[str, Any]:
|
754
|
+
"""
|
755
|
+
Send key combination to VM (delegates to QMPManager).
|
756
|
+
|
757
|
+
Args:
|
758
|
+
vm_id: VM identifier (name or ID)
|
759
|
+
*keys: Key names to press (e.g., 'ctrl', 'alt', 'f2')
|
760
|
+
hold_time: How long to hold keys in milliseconds
|
761
|
+
|
762
|
+
Returns:
|
763
|
+
QMP command result
|
764
|
+
|
765
|
+
Raises:
|
766
|
+
MaqetError: If VM not found or command fails
|
767
|
+
"""
|
768
|
+
try:
|
769
|
+
return self.qmp_manager.send_keys(vm_id, *keys, hold_time=hold_time)
|
770
|
+
except QMPManagerError as e:
|
771
|
+
raise MaqetError(str(e))
|
772
|
+
|
773
|
+
@api_method(
|
774
|
+
cli_name="type",
|
775
|
+
description="Type text string to VM via QMP",
|
776
|
+
category="qmp",
|
777
|
+
requires_vm=True,
|
778
|
+
parent="qmp",
|
779
|
+
examples=[
|
780
|
+
"maqet qmp type myvm 'hello world'",
|
781
|
+
"maqet qmp type myvm --hold-time 50 'slow typing'",
|
782
|
+
],
|
783
|
+
)
|
784
|
+
def qmp_type(
|
785
|
+
self, vm_id: str, text: str, hold_time: int = 100
|
786
|
+
) -> List[Dict[str, Any]]:
|
787
|
+
"""
|
788
|
+
Type text string to VM (delegates to QMPManager).
|
789
|
+
|
790
|
+
Args:
|
791
|
+
vm_id: VM identifier (name or ID)
|
792
|
+
text: Text to type
|
793
|
+
hold_time: How long to hold each key in milliseconds
|
794
|
+
|
795
|
+
Returns:
|
796
|
+
List of QMP command results
|
797
|
+
|
798
|
+
Raises:
|
799
|
+
MaqetError: If VM not found or command fails
|
800
|
+
"""
|
801
|
+
try:
|
802
|
+
return self.qmp_manager.type_text(vm_id, text, hold_time=hold_time)
|
803
|
+
except QMPManagerError as e:
|
804
|
+
raise MaqetError(str(e))
|
805
|
+
|
806
|
+
@api_method(
|
807
|
+
cli_name="screendump",
|
808
|
+
description="Take screenshot of VM screen",
|
809
|
+
category="qmp",
|
810
|
+
requires_vm=True,
|
811
|
+
parent="qmp",
|
812
|
+
examples=[
|
813
|
+
"maqet qmp screendump myvm screenshot.ppm",
|
814
|
+
"maqet qmp screendump myvm /tmp/vm_screen.ppm",
|
815
|
+
],
|
816
|
+
)
|
817
|
+
def screendump(self, vm_id: str, filename: str) -> Dict[str, Any]:
|
818
|
+
"""
|
819
|
+
Take screenshot of VM screen (delegates to QMPManager).
|
820
|
+
|
821
|
+
Args:
|
822
|
+
vm_id: VM identifier (name or ID)
|
823
|
+
filename: Output filename for screenshot
|
824
|
+
|
825
|
+
Returns:
|
826
|
+
QMP command result
|
827
|
+
|
828
|
+
Raises:
|
829
|
+
MaqetError: If VM not found or command fails
|
830
|
+
"""
|
831
|
+
try:
|
832
|
+
return self.qmp_manager.take_screenshot(vm_id, filename)
|
833
|
+
except QMPManagerError as e:
|
834
|
+
raise MaqetError(str(e))
|
835
|
+
|
836
|
+
@api_method(
|
837
|
+
cli_name="pause",
|
838
|
+
description="Pause VM execution via QMP",
|
839
|
+
category="qmp",
|
840
|
+
requires_vm=True,
|
841
|
+
parent="qmp",
|
842
|
+
examples=["maqet qmp pause myvm"],
|
843
|
+
)
|
844
|
+
def qmp_stop(self, vm_id: str) -> Dict[str, Any]:
|
845
|
+
"""
|
846
|
+
Pause VM execution via QMP (delegates to QMPManager).
|
847
|
+
|
848
|
+
Args:
|
849
|
+
vm_id: VM identifier (name or ID)
|
850
|
+
|
851
|
+
Returns:
|
852
|
+
QMP command result
|
853
|
+
|
854
|
+
Raises:
|
855
|
+
MaqetError: If VM not found or command fails
|
856
|
+
"""
|
857
|
+
try:
|
858
|
+
return self.qmp_manager.pause(vm_id)
|
859
|
+
except QMPManagerError as e:
|
860
|
+
raise MaqetError(str(e))
|
861
|
+
|
862
|
+
@api_method(
|
863
|
+
cli_name="resume",
|
864
|
+
description="Resume VM execution via QMP",
|
865
|
+
category="qmp",
|
866
|
+
requires_vm=True,
|
867
|
+
parent="qmp",
|
868
|
+
examples=["maqet qmp resume myvm"],
|
869
|
+
)
|
870
|
+
def qmp_cont(self, vm_id: str) -> Dict[str, Any]:
|
871
|
+
"""
|
872
|
+
Resume VM execution via QMP (delegates to QMPManager).
|
873
|
+
|
874
|
+
Args:
|
875
|
+
vm_id: VM identifier (name or ID)
|
876
|
+
|
877
|
+
Returns:
|
878
|
+
QMP command result
|
879
|
+
|
880
|
+
Raises:
|
881
|
+
MaqetError: If VM not found or command fails
|
882
|
+
"""
|
883
|
+
try:
|
884
|
+
return self.qmp_manager.resume(vm_id)
|
885
|
+
except QMPManagerError as e:
|
886
|
+
raise MaqetError(str(e))
|
887
|
+
|
888
|
+
@api_method(
|
889
|
+
cli_name="device-add",
|
890
|
+
description="Hot-plug device to VM via QMP",
|
891
|
+
category="qmp",
|
892
|
+
requires_vm=True,
|
893
|
+
parent="qmp",
|
894
|
+
examples=[
|
895
|
+
"maqet qmp device-add myvm usb-storage --device-id usb1 "
|
896
|
+
"--drive usb-drive",
|
897
|
+
"maqet qmp device-add myvm e1000 --device-id net1 --netdev user1",
|
898
|
+
],
|
899
|
+
)
|
900
|
+
def device_add(
|
901
|
+
self, vm_id: str, driver: str, device_id: str, **kwargs
|
902
|
+
) -> Dict[str, Any]:
|
903
|
+
"""
|
904
|
+
Hot-plug device to VM via QMP (delegates to QMPManager).
|
905
|
+
|
906
|
+
Args:
|
907
|
+
vm_id: VM identifier (name or ID)
|
908
|
+
driver: Device driver name (e.g., 'usb-storage', 'e1000')
|
909
|
+
device_id: Unique device identifier
|
910
|
+
**kwargs: Additional device properties
|
911
|
+
|
912
|
+
Returns:
|
913
|
+
QMP command result
|
914
|
+
|
915
|
+
Raises:
|
916
|
+
MaqetError: If VM not found or command fails
|
917
|
+
"""
|
918
|
+
try:
|
919
|
+
return self.qmp_manager.device_add(vm_id, driver, device_id, **kwargs)
|
920
|
+
except QMPManagerError as e:
|
921
|
+
raise MaqetError(str(e))
|
922
|
+
|
923
|
+
@api_method(
|
924
|
+
cli_name="device-del",
|
925
|
+
description="Hot-unplug device from VM via QMP",
|
926
|
+
category="qmp",
|
927
|
+
requires_vm=True,
|
928
|
+
parent="qmp",
|
929
|
+
examples=["maqet qmp device-del myvm usb1"],
|
930
|
+
)
|
931
|
+
def device_del(self, vm_id: str, device_id: str) -> Dict[str, Any]:
|
932
|
+
"""
|
933
|
+
Hot-unplug device from VM via QMP (delegates to QMPManager).
|
934
|
+
|
935
|
+
Args:
|
936
|
+
vm_id: VM identifier (name or ID)
|
937
|
+
device_id: Device identifier to remove
|
938
|
+
|
939
|
+
Returns:
|
940
|
+
QMP command result
|
941
|
+
|
942
|
+
Raises:
|
943
|
+
MaqetError: If VM not found or command fails
|
944
|
+
"""
|
945
|
+
try:
|
946
|
+
return self.qmp_manager.device_del(vm_id, device_id)
|
947
|
+
except QMPManagerError as e:
|
948
|
+
raise MaqetError(str(e))
|
949
|
+
|
950
|
+
@api_method(
|
951
|
+
cli_name="snapshot",
|
952
|
+
description="Manage VM storage snapshots",
|
953
|
+
category="storage",
|
954
|
+
requires_vm=True,
|
955
|
+
examples=[
|
956
|
+
"maqet snapshot myvm create ssd backup_name",
|
957
|
+
"maqet snapshot myvm load ssd backup_name",
|
958
|
+
"maqet snapshot myvm list ssd",
|
959
|
+
"maqet snapshot myvm create ssd backup_name --overwrite",
|
960
|
+
],
|
961
|
+
)
|
962
|
+
def snapshot(
|
963
|
+
self,
|
964
|
+
vm_id: str,
|
965
|
+
action: str,
|
966
|
+
drive: str,
|
967
|
+
name: Optional[str] = None,
|
968
|
+
overwrite: bool = False,
|
969
|
+
) -> Union[Dict[str, Any], List[str]]:
|
970
|
+
"""
|
971
|
+
Manage VM storage snapshots (delegates to SnapshotCoordinator).
|
972
|
+
|
973
|
+
Args:
|
974
|
+
vm_id: VM identifier (name or ID)
|
975
|
+
action: Snapshot action ('create', 'load', 'list')
|
976
|
+
drive: Storage drive name
|
977
|
+
name: Snapshot name (required for create/load)
|
978
|
+
overwrite: Overwrite existing snapshot (create only)
|
979
|
+
|
980
|
+
Returns:
|
981
|
+
Operation result dictionary or list of snapshots
|
982
|
+
|
983
|
+
Raises:
|
984
|
+
MaqetError: If VM not found or snapshot operation fails
|
985
|
+
"""
|
986
|
+
try:
|
987
|
+
return self.snapshot_coordinator.snapshot(
|
988
|
+
vm_id, action, drive, name, overwrite
|
989
|
+
)
|
990
|
+
except SnapshotCoordinatorError as e:
|
991
|
+
raise MaqetError(str(e))
|
992
|
+
except SnapshotError as e:
|
993
|
+
raise MaqetError(f"Snapshot operation failed: {e}")
|
994
|
+
|
995
|
+
@api_method(
|
996
|
+
cli_name="apply",
|
997
|
+
description="Apply configuration to existing VM",
|
998
|
+
category="vm",
|
999
|
+
requires_vm=True,
|
1000
|
+
examples=[
|
1001
|
+
"maqet apply myvm config.yaml",
|
1002
|
+
"maqet apply myvm --memory 8G --cpu 4",
|
1003
|
+
],
|
1004
|
+
)
|
1005
|
+
def apply(
|
1006
|
+
self,
|
1007
|
+
vm_id: str,
|
1008
|
+
config: Optional[Union[str, List[str]]] = None,
|
1009
|
+
**kwargs,
|
1010
|
+
) -> VMInstance:
|
1011
|
+
"""
|
1012
|
+
Apply configuration to existing VM, or create it if it doesn't exist.
|
1013
|
+
|
1014
|
+
Args:
|
1015
|
+
vm_id: VM identifier (name or ID)
|
1016
|
+
config: Path to configuration file, or list of config
|
1017
|
+
files for deep-merge
|
1018
|
+
**kwargs: Configuration parameters to update
|
1019
|
+
|
1020
|
+
Returns:
|
1021
|
+
VM instance (created or updated)
|
1022
|
+
|
1023
|
+
Raises:
|
1024
|
+
MaqetError: If configuration is invalid or operation fails
|
1025
|
+
"""
|
1026
|
+
try:
|
1027
|
+
# Get VM from database
|
1028
|
+
vm = self.state_manager.get_vm(vm_id)
|
1029
|
+
|
1030
|
+
if not vm:
|
1031
|
+
# VM doesn't exist, create it using add functionality
|
1032
|
+
LOG.info(f"VM '{vm_id}' not found, creating new VM")
|
1033
|
+
new_vm_id = self.add(config=config, name=vm_id, **kwargs)
|
1034
|
+
return self.state_manager.get_vm(new_vm_id)
|
1035
|
+
|
1036
|
+
# VM exists, update its configuration
|
1037
|
+
# Load and merge new configuration files
|
1038
|
+
if config:
|
1039
|
+
new_config = ConfigMerger.load_and_merge_files(config)
|
1040
|
+
else:
|
1041
|
+
new_config = {}
|
1042
|
+
|
1043
|
+
# Merge kwargs with new config (kwargs take precedence)
|
1044
|
+
if kwargs:
|
1045
|
+
new_config = ConfigMerger.deep_merge(new_config, kwargs)
|
1046
|
+
|
1047
|
+
# Remove name from new_config as it's not QEMU configuration
|
1048
|
+
# (VM already exists with its name)
|
1049
|
+
if "name" in new_config:
|
1050
|
+
new_config = {
|
1051
|
+
k: v for k, v in new_config.items() if k != "name"
|
1052
|
+
}
|
1053
|
+
|
1054
|
+
# Merge with existing configuration (existing config provides base)
|
1055
|
+
final_config = ConfigMerger.deep_merge(
|
1056
|
+
dict(vm.config_data), new_config
|
1057
|
+
)
|
1058
|
+
|
1059
|
+
# Validate the merged configuration
|
1060
|
+
final_config = self.config_parser.validate_config(final_config)
|
1061
|
+
|
1062
|
+
# Update VM configuration in database
|
1063
|
+
# This is a simplified approach - in reality we'd update
|
1064
|
+
# the existing record
|
1065
|
+
self.state_manager.update_vm_config(vm.id, final_config)
|
1066
|
+
|
1067
|
+
return self.state_manager.get_vm(vm_id)
|
1068
|
+
|
1069
|
+
except Exception as e:
|
1070
|
+
raise MaqetError(
|
1071
|
+
f"Failed to apply configuration to VM '{vm_id}': {e}"
|
1072
|
+
)
|
1073
|
+
|
1074
|
+
def cli(self, args: Optional[List[str]] = None) -> Any:
|
1075
|
+
"""
|
1076
|
+
Run CLI interface using CLIGenerator.
|
1077
|
+
|
1078
|
+
Uses instance-specific API registry for isolated command generation.
|
1079
|
+
|
1080
|
+
Args:
|
1081
|
+
args: Command line arguments (defaults to sys.argv[1:])
|
1082
|
+
|
1083
|
+
Returns:
|
1084
|
+
Result of CLI command execution
|
1085
|
+
"""
|
1086
|
+
# Use instance-specific registry (falls back to global if not available)
|
1087
|
+
registry = getattr(self, '_api_registry', API_REGISTRY)
|
1088
|
+
generator = CLIGenerator(self, registry)
|
1089
|
+
return generator.run(args)
|
1090
|
+
|
1091
|
+
def __call__(self, method_name: str, **kwargs) -> Any:
|
1092
|
+
"""
|
1093
|
+
Direct Python API access.
|
1094
|
+
|
1095
|
+
Args:
|
1096
|
+
method_name: Method to execute
|
1097
|
+
**kwargs: Method parameters
|
1098
|
+
|
1099
|
+
Returns:
|
1100
|
+
Method execution result
|
1101
|
+
"""
|
1102
|
+
generator = PythonAPIGenerator(self, API_REGISTRY)
|
1103
|
+
return generator.execute_method(method_name, **kwargs)
|
1104
|
+
|
1105
|
+
def python_api(self):
|
1106
|
+
"""
|
1107
|
+
Get Python API interface.
|
1108
|
+
|
1109
|
+
Returns:
|
1110
|
+
PythonAPIInterface for direct method access
|
1111
|
+
"""
|
1112
|
+
generator = PythonAPIGenerator(self, API_REGISTRY)
|
1113
|
+
return generator.generate()
|
1114
|
+
|
1115
|
+
|
1116
|
+
# NOTE: API methods are automatically registered via AutoRegisterAPI
|
1117
|
+
# inheritance
|
1118
|
+
# No manual register_class_methods() call needed!
|
1119
|
+
|
1120
|
+
# NOTE: Line length compliance is now enforced by black formatter
|