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