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,7 @@
1
+ """Manager classes for Maqet components."""
2
+
3
+ from .qmp_manager import QMPManager
4
+ from .snapshot_coordinator import SnapshotCoordinator
5
+ from .vm_manager import VMManager
6
+
7
+ __all__ = ["VMManager", "QMPManager", "SnapshotCoordinator"]
@@ -0,0 +1,333 @@
1
+ """
2
+ QMP Manager
3
+
4
+ Manages QMP (QEMU Machine Protocol) operations for VMs.
5
+ All QMP commands are sent via IPC to VM runner processes.
6
+
7
+ Responsibilities:
8
+ - Execute arbitrary QMP commands
9
+ - Send keyboard input (keys, typing)
10
+ - Take screenshots
11
+ - Pause/resume VM execution
12
+ - Hot-plug/unplug devices
13
+ """
14
+
15
+ from pathlib import Path
16
+ from typing import Any, Dict, List, Optional
17
+
18
+ from ..constants import QMP as QPMConstants
19
+ from ..constants import Timeouts
20
+ from ..exceptions import (
21
+ IPCError,
22
+ QMPCommandError,
23
+ QMPError,
24
+ VMNotFoundError,
25
+ VMNotRunningError,
26
+ )
27
+ from ..ipc.runner_client import RunnerClient
28
+ from ..logger import LOG
29
+ from ..qmp import KeyboardEmulator
30
+ from ..state import StateManager, VMInstance
31
+
32
+ # Legacy exception aliases (backward compatibility)
33
+ QMPManagerError = QMPError
34
+ RunnerClientError = IPCError
35
+
36
+
37
+ class QMPManager:
38
+ """
39
+ Manages QMP (QEMU Machine Protocol) operations.
40
+
41
+ All QMP commands are sent to VM runner processes via IPC (Inter-Process
42
+ Communication). The VM runner handles QMP interaction with QEMU.
43
+
44
+ This enables QMP to work regardless of whether the VM was started from
45
+ CLI or Python API, fixing the previous limitation where QMP only worked
46
+ in Python API mode.
47
+ """
48
+
49
+ def __init__(self, state_manager: StateManager):
50
+ """
51
+ Initialize QMP manager.
52
+
53
+ Args:
54
+ state_manager: State management instance for VM database access
55
+ """
56
+ self.state_manager = state_manager
57
+ LOG.debug("QMPManager initialized")
58
+
59
+ def execute_qmp(self, vm_id: str, command: str, **kwargs) -> Dict[str, Any]:
60
+ """
61
+ Execute arbitrary QMP command on VM via IPC.
62
+
63
+ Args:
64
+ vm_id: VM identifier (name or ID)
65
+ command: QMP command to execute (e.g., "query-status", "system_powerdown")
66
+ **kwargs: Command parameters
67
+
68
+ Returns:
69
+ QMP command result dictionary
70
+
71
+ Raises:
72
+ QMPManagerError: If VM not found, not running, or command fails
73
+
74
+ Example:
75
+ result = qmp_manager.execute_qmp("myvm", "query-status")
76
+ result = qmp_manager.execute_qmp("myvm", "screendump", filename="screen.ppm")
77
+ """
78
+ try:
79
+ # Get VM from database
80
+ vm = self.state_manager.get_vm(vm_id)
81
+ if not vm:
82
+ raise QMPManagerError(f"VM '{vm_id}' not found")
83
+
84
+ # Check VM is running
85
+ if vm.status != "running":
86
+ raise QMPManagerError(
87
+ f"VM '{vm_id}' is not running (status: {vm.status})"
88
+ )
89
+
90
+ # Verify runner process is alive
91
+ if not vm.runner_pid:
92
+ raise QMPManagerError(
93
+ f"VM '{vm_id}' has no runner process (state corrupted)"
94
+ )
95
+
96
+ # Create IPC client and send QMP command
97
+ client = RunnerClient(vm.id, self.state_manager)
98
+
99
+ try:
100
+ result = client.send_command("qmp", command, **kwargs)
101
+ LOG.debug(f"QMP command '{command}' executed successfully on {vm_id}")
102
+ return result
103
+
104
+ except RunnerClientError as e:
105
+ raise QMPManagerError(f"Failed to communicate with VM runner: {e}")
106
+
107
+ except QMPManagerError:
108
+ raise
109
+ except Exception as e:
110
+ raise QMPManagerError(f"QMP command failed on VM '{vm_id}': {e}")
111
+
112
+ def send_keys(
113
+ self, vm_id: str, *keys: str, hold_time: int = 100
114
+ ) -> Dict[str, Any]:
115
+ """
116
+ Send key combination to VM via QMP.
117
+
118
+ Uses KeyboardEmulator to translate key names into QMP send-key command.
119
+
120
+ Args:
121
+ vm_id: VM identifier (name or ID)
122
+ *keys: Key names to press (e.g., 'ctrl', 'alt', 'f2')
123
+ hold_time: How long to hold keys in milliseconds (default: 100)
124
+
125
+ Returns:
126
+ QMP command result dictionary
127
+
128
+ Raises:
129
+ QMPManagerError: If VM not found, not running, or command fails
130
+
131
+ Example:
132
+ qmp_manager.send_keys("myvm", "ctrl", "alt", "f2")
133
+ qmp_manager.send_keys("myvm", "ret", hold_time=200)
134
+ """
135
+ try:
136
+ # Generate QMP command from key names
137
+ qmp_cmd = KeyboardEmulator.press_keys(*keys, hold_time=hold_time)
138
+
139
+ # Execute QMP command via IPC
140
+ result = self.execute_qmp(
141
+ vm_id, qmp_cmd["command"], **qmp_cmd["arguments"]
142
+ )
143
+
144
+ LOG.debug(f"Sent keys {keys} to VM {vm_id}")
145
+ return result
146
+
147
+ except Exception as e:
148
+ raise QMPManagerError(f"Failed to send keys to VM '{vm_id}': {e}")
149
+
150
+ def type_text(
151
+ self, vm_id: str, text: str, hold_time: int = 100
152
+ ) -> List[Dict[str, Any]]:
153
+ """
154
+ Type text string to VM via QMP.
155
+
156
+ Sends each character as a separate QMP send-key command.
157
+
158
+ Args:
159
+ vm_id: VM identifier (name or ID)
160
+ text: Text to type
161
+ hold_time: How long to hold each key in milliseconds (default: 100)
162
+
163
+ Returns:
164
+ List of QMP command results (one per character)
165
+
166
+ Raises:
167
+ QMPManagerError: If VM not found, not running, or command fails
168
+
169
+ Example:
170
+ qmp_manager.type_text("myvm", "hello world")
171
+ qmp_manager.type_text("myvm", "slow typing", hold_time=50)
172
+ """
173
+ try:
174
+ # Generate QMP commands for each character
175
+ qmp_commands = KeyboardEmulator.type_string(text, hold_time=hold_time)
176
+
177
+ # Execute each command via IPC
178
+ results = []
179
+ for cmd in qmp_commands:
180
+ result = self.execute_qmp(
181
+ vm_id, cmd["command"], **cmd["arguments"]
182
+ )
183
+ results.append(result)
184
+
185
+ LOG.debug(f"Typed {len(text)} characters to VM {vm_id}")
186
+ return results
187
+
188
+ except Exception as e:
189
+ raise QMPManagerError(f"Failed to type text to VM '{vm_id}': {e}")
190
+
191
+ def take_screenshot(self, vm_id: str, filename: str) -> Dict[str, Any]:
192
+ """
193
+ Take screenshot of VM screen.
194
+
195
+ Saves screenshot to specified file in PPM format (QEMU default).
196
+
197
+ Args:
198
+ vm_id: VM identifier (name or ID)
199
+ filename: Output filename for screenshot (e.g., "screenshot.ppm")
200
+
201
+ Returns:
202
+ QMP command result dictionary
203
+
204
+ Raises:
205
+ QMPManagerError: If VM not found, not running, or command fails
206
+
207
+ Example:
208
+ qmp_manager.take_screenshot("myvm", "/tmp/screenshot.ppm")
209
+ """
210
+ try:
211
+ result = self.execute_qmp(vm_id, "screendump", filename=filename)
212
+ LOG.info(f"Screenshot saved to {filename} for VM {vm_id}")
213
+ return result
214
+
215
+ except Exception as e:
216
+ raise QMPManagerError(
217
+ f"Failed to take screenshot of VM '{vm_id}': {e}"
218
+ )
219
+
220
+ def pause(self, vm_id: str) -> Dict[str, Any]:
221
+ """
222
+ Pause VM execution via QMP.
223
+
224
+ Suspends VM execution (freezes guest). VM can be resumed later.
225
+
226
+ Args:
227
+ vm_id: VM identifier (name or ID)
228
+
229
+ Returns:
230
+ QMP command result dictionary
231
+
232
+ Raises:
233
+ QMPManagerError: If VM not found, not running, or command fails
234
+
235
+ Example:
236
+ qmp_manager.pause("myvm")
237
+ """
238
+ try:
239
+ result = self.execute_qmp(vm_id, "stop")
240
+ LOG.info(f"VM {vm_id} paused")
241
+ return result
242
+
243
+ except Exception as e:
244
+ raise QMPManagerError(f"Failed to pause VM '{vm_id}': {e}")
245
+
246
+ def resume(self, vm_id: str) -> Dict[str, Any]:
247
+ """
248
+ Resume VM execution via QMP.
249
+
250
+ Resumes a previously paused VM.
251
+
252
+ Args:
253
+ vm_id: VM identifier (name or ID)
254
+
255
+ Returns:
256
+ QMP command result dictionary
257
+
258
+ Raises:
259
+ QMPManagerError: If VM not found, not running, or command fails
260
+
261
+ Example:
262
+ qmp_manager.resume("myvm")
263
+ """
264
+ try:
265
+ result = self.execute_qmp(vm_id, "cont")
266
+ LOG.info(f"VM {vm_id} resumed")
267
+ return result
268
+
269
+ except Exception as e:
270
+ raise QMPManagerError(f"Failed to resume VM '{vm_id}': {e}")
271
+
272
+ def device_add(
273
+ self, vm_id: str, driver: str, device_id: str, **kwargs
274
+ ) -> Dict[str, Any]:
275
+ """
276
+ Hot-plug device to VM via QMP.
277
+
278
+ Adds a device to running VM without restart.
279
+
280
+ Args:
281
+ vm_id: VM identifier (name or ID)
282
+ driver: Device driver name (e.g., 'usb-storage', 'e1000', 'virtio-net-pci')
283
+ device_id: Unique device identifier
284
+ **kwargs: Additional device properties (e.g., drive="usb-drive", netdev="user1")
285
+
286
+ Returns:
287
+ QMP command result dictionary
288
+
289
+ Raises:
290
+ QMPManagerError: If VM not found, not running, or command fails
291
+
292
+ Example:
293
+ qmp_manager.device_add("myvm", "usb-storage", "usb1", drive="usb-drive")
294
+ qmp_manager.device_add("myvm", "e1000", "net1", netdev="user1")
295
+ """
296
+ try:
297
+ result = self.execute_qmp(
298
+ vm_id, "device_add", driver=driver, id=device_id, **kwargs
299
+ )
300
+ LOG.info(f"Device {device_id} (driver={driver}) added to VM {vm_id}")
301
+ return result
302
+
303
+ except Exception as e:
304
+ raise QMPManagerError(f"Failed to add device to VM '{vm_id}': {e}")
305
+
306
+ def device_del(self, vm_id: str, device_id: str) -> Dict[str, Any]:
307
+ """
308
+ Hot-unplug device from VM via QMP.
309
+
310
+ Removes a device from running VM without restart.
311
+
312
+ Args:
313
+ vm_id: VM identifier (name or ID)
314
+ device_id: Device identifier to remove
315
+
316
+ Returns:
317
+ QMP command result dictionary
318
+
319
+ Raises:
320
+ QMPManagerError: If VM not found, not running, or command fails
321
+
322
+ Example:
323
+ qmp_manager.device_del("myvm", "usb1")
324
+ """
325
+ try:
326
+ result = self.execute_qmp(vm_id, "device_del", id=device_id)
327
+ LOG.info(f"Device {device_id} removed from VM {vm_id}")
328
+ return result
329
+
330
+ except Exception as e:
331
+ raise QMPManagerError(
332
+ f"Failed to remove device from VM '{vm_id}': {e}"
333
+ )
@@ -0,0 +1,327 @@
1
+ """
2
+ Snapshot Coordinator
3
+
4
+ Coordinates snapshot operations across VM storage devices.
5
+ This manager handles snapshot lifecycle: create, load, list, and delete operations.
6
+ """
7
+
8
+ from pathlib import Path
9
+ from typing import Any, Dict, List, Optional, Union
10
+
11
+ from ..constants import Defaults
12
+ from ..exceptions import (
13
+ SnapshotCreationError,
14
+ SnapshotDeleteError,
15
+ SnapshotError,
16
+ SnapshotLoadError,
17
+ SnapshotNotFoundError,
18
+ StorageDeviceNotFoundError,
19
+ VMNotFoundError,
20
+ )
21
+ from ..logger import LOG
22
+ from ..snapshot import SnapshotManager
23
+ from ..state import StateManager, VMInstance
24
+ from ..storage import StorageManager
25
+
26
+ # Legacy exception alias (backward compatibility)
27
+ SnapshotCoordinatorError = SnapshotError
28
+
29
+
30
+ class SnapshotCoordinator:
31
+ """
32
+ Coordinates snapshot operations across VM storage devices.
33
+
34
+ Responsibilities:
35
+ - Create snapshots on QCOW2 storage devices
36
+ - Restore/load snapshots
37
+ - List available snapshots for VM drives
38
+ - Route snapshot commands to appropriate operations
39
+ - Integrate with storage management system
40
+
41
+ This coordinator acts as a facade between the Maqet API and the
42
+ underlying SnapshotManager implementation. It handles VM lookup,
43
+ storage configuration, and error translation.
44
+ """
45
+
46
+ def __init__(self, state_manager: StateManager):
47
+ """
48
+ Initialize snapshot coordinator.
49
+
50
+ Args:
51
+ state_manager: State management instance for VM lookups
52
+ """
53
+ self.state_manager = state_manager
54
+ LOG.debug("SnapshotCoordinator initialized")
55
+
56
+ def snapshot(
57
+ self,
58
+ vm_id: str,
59
+ action: str,
60
+ drive: str,
61
+ name: Optional[str] = None,
62
+ overwrite: bool = False,
63
+ ) -> Union[Dict[str, Any], List[str]]:
64
+ """
65
+ Main snapshot command router.
66
+
67
+ Routes snapshot operations to appropriate handler based on action.
68
+ This is the primary entry point for all snapshot operations.
69
+
70
+ Args:
71
+ vm_id: VM identifier (name or ID)
72
+ action: Snapshot action ('create', 'load', 'list')
73
+ drive: Storage drive name
74
+ name: Snapshot name (required for create/load)
75
+ overwrite: Overwrite existing snapshot (create only)
76
+
77
+ Returns:
78
+ Operation result dictionary or list of snapshots
79
+
80
+ Raises:
81
+ SnapshotCoordinatorError: If VM not found or operation fails
82
+ """
83
+ try:
84
+ vm = self.state_manager.get_vm(vm_id)
85
+ if not vm:
86
+ raise SnapshotCoordinatorError(f"VM '{vm_id}' not found")
87
+
88
+ # Create snapshot manager for this VM
89
+ storage_manager = StorageManager(vm_id)
90
+ storage_configs = vm.config_data.get("storage", [])
91
+ if storage_configs:
92
+ storage_manager.add_storage_from_config(storage_configs)
93
+ snapshot_mgr = SnapshotManager(vm_id, storage_manager)
94
+
95
+ # Route to appropriate action
96
+ if action == "create":
97
+ if not name:
98
+ raise SnapshotCoordinatorError(
99
+ "Snapshot name required for create action"
100
+ )
101
+ return snapshot_mgr.create(drive, name, overwrite=overwrite)
102
+
103
+ elif action == "load":
104
+ if not name:
105
+ raise SnapshotCoordinatorError(
106
+ "Snapshot name required for load action"
107
+ )
108
+ return snapshot_mgr.load(drive, name)
109
+
110
+ elif action == "list":
111
+ return snapshot_mgr.list(drive)
112
+
113
+ else:
114
+ raise SnapshotCoordinatorError(
115
+ f"Invalid action '{action}'. "
116
+ f"Available actions: create, load, list"
117
+ )
118
+
119
+ except SnapshotError as e:
120
+ raise SnapshotCoordinatorError(f"Snapshot operation failed: {e}")
121
+ except Exception as e:
122
+ raise SnapshotCoordinatorError(
123
+ f"Failed to manage snapshots for VM '{vm_id}': {e}"
124
+ )
125
+
126
+ def create(
127
+ self, vm_id: str, drive: str, name: str, overwrite: bool = False
128
+ ) -> Dict[str, Any]:
129
+ """
130
+ Create snapshot on storage device.
131
+
132
+ Args:
133
+ vm_id: VM identifier (name or ID)
134
+ drive: Storage drive name
135
+ name: Snapshot name
136
+ overwrite: Overwrite existing snapshot
137
+
138
+ Returns:
139
+ Operation result dictionary
140
+
141
+ Raises:
142
+ SnapshotCoordinatorError: If VM not found or operation fails
143
+ """
144
+ try:
145
+ vm = self.state_manager.get_vm(vm_id)
146
+ if not vm:
147
+ raise SnapshotCoordinatorError(f"VM '{vm_id}' not found")
148
+
149
+ # Create snapshot manager
150
+ storage_manager = StorageManager(vm_id)
151
+ storage_configs = vm.config_data.get("storage", [])
152
+ if storage_configs:
153
+ storage_manager.add_storage_from_config(storage_configs)
154
+ snapshot_mgr = SnapshotManager(vm_id, storage_manager)
155
+
156
+ # Create snapshot
157
+ result = snapshot_mgr.create(drive, name, overwrite=overwrite)
158
+ LOG.info(
159
+ f"Created snapshot '{name}' on drive '{drive}' for VM '{vm_id}'"
160
+ )
161
+ return result
162
+
163
+ except SnapshotError as e:
164
+ raise SnapshotCoordinatorError(str(e))
165
+ except Exception as e:
166
+ raise SnapshotCoordinatorError(
167
+ f"Failed to create snapshot '{name}' on drive '{drive}' "
168
+ f"for VM '{vm_id}': {e}"
169
+ )
170
+
171
+ def load(self, vm_id: str, drive: str, name: str) -> Dict[str, Any]:
172
+ """
173
+ Restore/load snapshot on storage device.
174
+
175
+ Args:
176
+ vm_id: VM identifier (name or ID)
177
+ drive: Storage drive name
178
+ name: Snapshot name
179
+
180
+ Returns:
181
+ Operation result dictionary
182
+
183
+ Raises:
184
+ SnapshotCoordinatorError: If VM not found or operation fails
185
+ """
186
+ try:
187
+ vm = self.state_manager.get_vm(vm_id)
188
+ if not vm:
189
+ raise SnapshotCoordinatorError(f"VM '{vm_id}' not found")
190
+
191
+ # Create snapshot manager
192
+ storage_manager = StorageManager(vm_id)
193
+ storage_configs = vm.config_data.get("storage", [])
194
+ if storage_configs:
195
+ storage_manager.add_storage_from_config(storage_configs)
196
+ snapshot_mgr = SnapshotManager(vm_id, storage_manager)
197
+
198
+ # Load snapshot
199
+ result = snapshot_mgr.load(drive, name)
200
+ LOG.info(
201
+ f"Loaded snapshot '{name}' on drive '{drive}' for VM '{vm_id}'"
202
+ )
203
+ return result
204
+
205
+ except SnapshotError as e:
206
+ raise SnapshotCoordinatorError(str(e))
207
+ except Exception as e:
208
+ raise SnapshotCoordinatorError(
209
+ f"Failed to load snapshot '{name}' on drive '{drive}' "
210
+ f"for VM '{vm_id}': {e}"
211
+ )
212
+
213
+ def list(self, vm_id: str, drive: str) -> List[str]:
214
+ """
215
+ List snapshots for VM storage device.
216
+
217
+ Args:
218
+ vm_id: VM identifier (name or ID)
219
+ drive: Storage drive name
220
+
221
+ Returns:
222
+ List of snapshot names
223
+
224
+ Raises:
225
+ SnapshotCoordinatorError: If VM not found or operation fails
226
+ """
227
+ try:
228
+ vm = self.state_manager.get_vm(vm_id)
229
+ if not vm:
230
+ raise SnapshotCoordinatorError(f"VM '{vm_id}' not found")
231
+
232
+ # Create snapshot manager
233
+ storage_manager = StorageManager(vm_id)
234
+ storage_configs = vm.config_data.get("storage", [])
235
+ if storage_configs:
236
+ storage_manager.add_storage_from_config(storage_configs)
237
+ snapshot_mgr = SnapshotManager(vm_id, storage_manager)
238
+
239
+ # List snapshots
240
+ snapshots = snapshot_mgr.list(drive)
241
+ LOG.debug(
242
+ f"Listed {len(snapshots)} snapshot(s) on drive '{drive}' "
243
+ f"for VM '{vm_id}'"
244
+ )
245
+ return snapshots
246
+
247
+ except SnapshotError as e:
248
+ raise SnapshotCoordinatorError(str(e))
249
+ except Exception as e:
250
+ raise SnapshotCoordinatorError(
251
+ f"Failed to list snapshots on drive '{drive}' "
252
+ f"for VM '{vm_id}': {e}"
253
+ )
254
+
255
+ def get_snapshot_capable_drives(self, vm_id: str) -> List[str]:
256
+ """
257
+ Get list of drives that support snapshots for a VM.
258
+
259
+ Args:
260
+ vm_id: VM identifier (name or ID)
261
+
262
+ Returns:
263
+ List of drive names that support snapshots (QCOW2 only)
264
+
265
+ Raises:
266
+ SnapshotCoordinatorError: If VM not found
267
+ """
268
+ try:
269
+ vm = self.state_manager.get_vm(vm_id)
270
+ if not vm:
271
+ raise SnapshotCoordinatorError(f"VM '{vm_id}' not found")
272
+
273
+ # Create snapshot manager
274
+ storage_manager = StorageManager(vm_id)
275
+ storage_configs = vm.config_data.get("storage", [])
276
+ if storage_configs:
277
+ storage_manager.add_storage_from_config(storage_configs)
278
+ snapshot_mgr = SnapshotManager(vm_id, storage_manager)
279
+
280
+ # Get snapshot-capable drives
281
+ drives = snapshot_mgr.list_snapshot_capable_drives()
282
+ LOG.debug(
283
+ f"Found {len(drives)} snapshot-capable drive(s) for VM '{vm_id}'"
284
+ )
285
+ return drives
286
+
287
+ except Exception as e:
288
+ raise SnapshotCoordinatorError(
289
+ f"Failed to get snapshot-capable drives for VM '{vm_id}': {e}"
290
+ )
291
+
292
+ def get_drive_info(self, vm_id: str, drive: str) -> Dict[str, Any]:
293
+ """
294
+ Get detailed information about a storage drive including snapshots.
295
+
296
+ Args:
297
+ vm_id: VM identifier (name or ID)
298
+ drive: Storage drive name
299
+
300
+ Returns:
301
+ Dictionary with drive information and snapshot list
302
+
303
+ Raises:
304
+ SnapshotCoordinatorError: If VM or drive not found
305
+ """
306
+ try:
307
+ vm = self.state_manager.get_vm(vm_id)
308
+ if not vm:
309
+ raise SnapshotCoordinatorError(f"VM '{vm_id}' not found")
310
+
311
+ # Create snapshot manager
312
+ storage_manager = StorageManager(vm_id)
313
+ storage_configs = vm.config_data.get("storage", [])
314
+ if storage_configs:
315
+ storage_manager.add_storage_from_config(storage_configs)
316
+ snapshot_mgr = SnapshotManager(vm_id, storage_manager)
317
+
318
+ # Get drive info with snapshots
319
+ info = snapshot_mgr.get_drive_info(drive)
320
+ return info
321
+
322
+ except SnapshotError as e:
323
+ raise SnapshotCoordinatorError(str(e))
324
+ except Exception as e:
325
+ raise SnapshotCoordinatorError(
326
+ f"Failed to get drive info for '{drive}' on VM '{vm_id}': {e}"
327
+ )