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/snapshot.py
ADDED
@@ -0,0 +1,473 @@
|
|
1
|
+
"""
|
2
|
+
Integrated Snapshot Manager
|
3
|
+
|
4
|
+
Manages QCOW2 snapshots for VM storage drives using the unified storage system.
|
5
|
+
Provides create, load, and list operations for VM storage snapshots.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import fcntl
|
9
|
+
import shutil
|
10
|
+
import subprocess
|
11
|
+
import time
|
12
|
+
from pathlib import Path
|
13
|
+
from typing import Any, Dict, List, Optional
|
14
|
+
|
15
|
+
from .constants import Defaults, Retries, Timeouts
|
16
|
+
from .exceptions import (
|
17
|
+
SnapshotCreationError,
|
18
|
+
SnapshotDeleteError,
|
19
|
+
SnapshotError,
|
20
|
+
SnapshotLoadError,
|
21
|
+
SnapshotNotFoundError,
|
22
|
+
StorageDeviceNotFoundError,
|
23
|
+
StorageError,
|
24
|
+
)
|
25
|
+
from .logger import LOG
|
26
|
+
from .storage import BaseStorageDevice, StorageManager
|
27
|
+
|
28
|
+
|
29
|
+
class SnapshotManager:
|
30
|
+
"""
|
31
|
+
Manages QCOW2 snapshots for VM storage drives.
|
32
|
+
|
33
|
+
Integrates with the unified storage management system to provide
|
34
|
+
snapshot operations on supported storage devices.
|
35
|
+
|
36
|
+
# NOTE: Good - cleanly integrates with StorageManager abstraction instead
|
37
|
+
# of directly manipulating storage files. Respects storage device
|
38
|
+
# capabilities.
|
39
|
+
#
|
40
|
+
# TODO(architect, 2025-10-10): [PERF] Snapshot operations are synchronous and block
|
41
|
+
# Context: All snapshot operations (create/load/list) use subprocess.run() which blocks
|
42
|
+
# until qemu-img completes. For large disks (100GB+), this can take minutes with no
|
43
|
+
# progress indication. Issue #7 in ARCHITECTURAL_REVIEW.md.
|
44
|
+
#
|
45
|
+
# Impact: CLI freezes, no way to cancel, users don't know if stuck or progressing
|
46
|
+
#
|
47
|
+
# Recommendation (Option 1 - Async): Use asyncio.create_subprocess_exec() for non-blocking
|
48
|
+
# Recommendation (Option 2 - Progress): Add progress callback and timeout warnings
|
49
|
+
#
|
50
|
+
# Effort: Medium (3-5 days for async, 1-2 days for progress reporting)
|
51
|
+
# Priority: High but defer to 1.1 (not critical until working with large disks)
|
52
|
+
# See: ARCHITECTURAL_REVIEW.md Issue #7
|
53
|
+
"""
|
54
|
+
|
55
|
+
def __init__(self, vm_id: str, storage_manager: StorageManager):
|
56
|
+
"""
|
57
|
+
Initialize snapshot manager for a VM.
|
58
|
+
|
59
|
+
Args:
|
60
|
+
vm_id: VM identifier
|
61
|
+
storage_manager: Unified storage manager instance
|
62
|
+
"""
|
63
|
+
self.vm_id = vm_id
|
64
|
+
self.storage_manager = storage_manager
|
65
|
+
|
66
|
+
def create(
|
67
|
+
self, drive_name: str, snapshot_name: str, overwrite: bool = False
|
68
|
+
) -> Dict[str, Any]:
|
69
|
+
"""
|
70
|
+
Create QCOW2 snapshot on specified drive.
|
71
|
+
|
72
|
+
Args:
|
73
|
+
drive_name: Name of storage drive
|
74
|
+
snapshot_name: Name for the snapshot
|
75
|
+
overwrite: Whether to overwrite existing snapshot
|
76
|
+
|
77
|
+
Returns:
|
78
|
+
Result dictionary with operation status
|
79
|
+
|
80
|
+
Raises:
|
81
|
+
SnapshotError: If drive not found or operation fails
|
82
|
+
"""
|
83
|
+
try:
|
84
|
+
device = self._get_snapshot_capable_device(drive_name)
|
85
|
+
drive_path = self._get_device_file_path(device)
|
86
|
+
|
87
|
+
# Check if snapshot already exists
|
88
|
+
existing_snapshots = self._list_snapshots(drive_path)
|
89
|
+
if snapshot_name in existing_snapshots:
|
90
|
+
if overwrite:
|
91
|
+
# Delete existing snapshot first
|
92
|
+
self._run_qemu_img(
|
93
|
+
["snapshot", str(drive_path), "-d", snapshot_name]
|
94
|
+
)
|
95
|
+
else:
|
96
|
+
raise SnapshotCreationError(
|
97
|
+
f"Snapshot '{snapshot_name}' already exists on drive '{drive_name}'. "
|
98
|
+
f"Use --overwrite flag to replace it."
|
99
|
+
)
|
100
|
+
|
101
|
+
# Create snapshot
|
102
|
+
self._run_qemu_img(
|
103
|
+
["snapshot", str(drive_path), "-c", snapshot_name]
|
104
|
+
)
|
105
|
+
|
106
|
+
return {
|
107
|
+
"status": "success",
|
108
|
+
"operation": "create",
|
109
|
+
"vm_id": self.vm_id,
|
110
|
+
"drive": drive_name,
|
111
|
+
"snapshot": snapshot_name,
|
112
|
+
"overwrite": overwrite,
|
113
|
+
}
|
114
|
+
|
115
|
+
except SnapshotCreationError:
|
116
|
+
# Re-raise specific errors
|
117
|
+
raise
|
118
|
+
except Exception as e:
|
119
|
+
raise SnapshotCreationError(
|
120
|
+
f"Failed to create snapshot '{snapshot_name}' on drive '{drive_name}': {e}"
|
121
|
+
)
|
122
|
+
|
123
|
+
def load(self, drive_name: str, snapshot_name: str) -> Dict[str, Any]:
|
124
|
+
"""
|
125
|
+
Load/revert to QCOW2 snapshot on specified drive.
|
126
|
+
|
127
|
+
Args:
|
128
|
+
drive_name: Name of storage drive
|
129
|
+
snapshot_name: Name of snapshot to load
|
130
|
+
|
131
|
+
Returns:
|
132
|
+
Result dictionary with operation status
|
133
|
+
|
134
|
+
Raises:
|
135
|
+
SnapshotError: If drive not found or operation fails
|
136
|
+
"""
|
137
|
+
try:
|
138
|
+
device = self._get_snapshot_capable_device(drive_name)
|
139
|
+
drive_path = self._get_device_file_path(device)
|
140
|
+
|
141
|
+
# Check if snapshot exists
|
142
|
+
existing_snapshots = self._list_snapshots(drive_path)
|
143
|
+
if snapshot_name not in existing_snapshots:
|
144
|
+
available = ", ".join(existing_snapshots) if existing_snapshots else "none"
|
145
|
+
raise SnapshotNotFoundError(
|
146
|
+
f"Snapshot '{snapshot_name}' not found on drive '{drive_name}'. "
|
147
|
+
f"Available snapshots: {available}"
|
148
|
+
)
|
149
|
+
|
150
|
+
# Apply/revert to snapshot
|
151
|
+
self._run_qemu_img(
|
152
|
+
["snapshot", str(drive_path), "-a", snapshot_name]
|
153
|
+
)
|
154
|
+
|
155
|
+
return {
|
156
|
+
"status": "success",
|
157
|
+
"operation": "load",
|
158
|
+
"vm_id": self.vm_id,
|
159
|
+
"drive": drive_name,
|
160
|
+
"snapshot": snapshot_name,
|
161
|
+
}
|
162
|
+
|
163
|
+
except SnapshotNotFoundError:
|
164
|
+
# Re-raise specific errors
|
165
|
+
raise
|
166
|
+
except Exception as e:
|
167
|
+
raise SnapshotLoadError(
|
168
|
+
f"Failed to load snapshot '{snapshot_name}' on drive '{drive_name}': {e}"
|
169
|
+
)
|
170
|
+
|
171
|
+
def list(self, drive_name: str) -> List[str]:
|
172
|
+
"""
|
173
|
+
List all available snapshots for specified drive.
|
174
|
+
|
175
|
+
Args:
|
176
|
+
drive_name: Name of storage drive
|
177
|
+
|
178
|
+
Returns:
|
179
|
+
List of snapshot names
|
180
|
+
|
181
|
+
Raises:
|
182
|
+
SnapshotError: If drive not found or operation fails
|
183
|
+
"""
|
184
|
+
try:
|
185
|
+
device = self._get_snapshot_capable_device(drive_name)
|
186
|
+
drive_path = self._get_device_file_path(device)
|
187
|
+
return self._list_snapshots(drive_path)
|
188
|
+
|
189
|
+
except Exception as e:
|
190
|
+
raise SnapshotError(
|
191
|
+
f"Failed to list snapshots for drive '{drive_name}': {e}"
|
192
|
+
)
|
193
|
+
|
194
|
+
def _get_snapshot_capable_device(
|
195
|
+
self, drive_name: str
|
196
|
+
) -> BaseStorageDevice:
|
197
|
+
"""
|
198
|
+
Get storage device by name and verify it supports snapshots.
|
199
|
+
|
200
|
+
Args:
|
201
|
+
drive_name: Name of the drive
|
202
|
+
|
203
|
+
Returns:
|
204
|
+
Storage device that supports snapshots
|
205
|
+
|
206
|
+
Raises:
|
207
|
+
SnapshotError: If drive not found or doesn't support snapshots
|
208
|
+
"""
|
209
|
+
device = self.storage_manager.get_device_by_name(drive_name)
|
210
|
+
if not device:
|
211
|
+
available_devices = [d.name for d in self.storage_manager.devices]
|
212
|
+
raise SnapshotError(
|
213
|
+
f"Drive '{drive_name}' not found in VM '{self.vm_id}'. "
|
214
|
+
f"Available drives: {available_devices}"
|
215
|
+
)
|
216
|
+
|
217
|
+
if not device.supports_snapshots():
|
218
|
+
raise SnapshotError(
|
219
|
+
f"Drive '{drive_name}' (type: {device.get_type()}) "
|
220
|
+
f"does not support snapshots. Only QCOW2 drives support snapshots."
|
221
|
+
)
|
222
|
+
|
223
|
+
return device
|
224
|
+
|
225
|
+
def _get_device_file_path(self, device: BaseStorageDevice) -> Path:
|
226
|
+
"""
|
227
|
+
Get file path from storage device.
|
228
|
+
|
229
|
+
Args:
|
230
|
+
device: Storage device
|
231
|
+
|
232
|
+
Returns:
|
233
|
+
Path to the storage file
|
234
|
+
|
235
|
+
Raises:
|
236
|
+
SnapshotError: If device doesn't have a file path or file doesn't exist
|
237
|
+
"""
|
238
|
+
if not hasattr(device, "file_path"):
|
239
|
+
raise SnapshotError(
|
240
|
+
f"Storage device '{device.name}' doesn't have a file path"
|
241
|
+
)
|
242
|
+
|
243
|
+
file_path = device.file_path
|
244
|
+
if not file_path.exists():
|
245
|
+
raise SnapshotError(f"Storage file '{file_path}' does not exist")
|
246
|
+
|
247
|
+
return file_path
|
248
|
+
|
249
|
+
def _run_qemu_img(
|
250
|
+
self,
|
251
|
+
args: List[str],
|
252
|
+
timeout: int = 300,
|
253
|
+
max_retries: int = 3,
|
254
|
+
) -> str:
|
255
|
+
"""
|
256
|
+
Run qemu-img command with given arguments.
|
257
|
+
|
258
|
+
Args:
|
259
|
+
args: List of command arguments (e.g., ["snapshot", "/path/to/file", "-c", "name"])
|
260
|
+
timeout: Command timeout in seconds (default 5 minutes)
|
261
|
+
max_retries: Maximum number of retry attempts for transient failures
|
262
|
+
|
263
|
+
Returns:
|
264
|
+
Command output
|
265
|
+
|
266
|
+
Raises:
|
267
|
+
SnapshotError: If command fails after all retries
|
268
|
+
"""
|
269
|
+
# Verify qemu-img binary exists
|
270
|
+
qemu_img_path = shutil.which("qemu-img")
|
271
|
+
if not qemu_img_path:
|
272
|
+
raise SnapshotError(
|
273
|
+
"qemu-img binary not found in PATH. "
|
274
|
+
"Please install QEMU tools."
|
275
|
+
)
|
276
|
+
|
277
|
+
# Build secure command list
|
278
|
+
command = [qemu_img_path] + args
|
279
|
+
|
280
|
+
# Extract file path from args for locking (usually args[1])
|
281
|
+
# Example: ["snapshot", "/path/to/file.qcow2", "-c", "snap1"]
|
282
|
+
lock_file = None
|
283
|
+
lock_file_path = None
|
284
|
+
if len(args) >= 2 and Path(args[1]).exists():
|
285
|
+
file_path = Path(args[1])
|
286
|
+
lock_file_path = (
|
287
|
+
file_path.parent / f".{file_path.name}.snapshot.lock"
|
288
|
+
)
|
289
|
+
|
290
|
+
# Retry loop for transient failures
|
291
|
+
last_error = None
|
292
|
+
for attempt in range(max_retries):
|
293
|
+
process = None
|
294
|
+
try:
|
295
|
+
# Acquire file lock to prevent concurrent snapshot operations
|
296
|
+
if lock_file_path:
|
297
|
+
lock_file = open(lock_file_path, "w")
|
298
|
+
try:
|
299
|
+
fcntl.flock(
|
300
|
+
lock_file.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB
|
301
|
+
)
|
302
|
+
except BlockingIOError:
|
303
|
+
raise SnapshotError(
|
304
|
+
f"Another snapshot operation is in progress on {args[1]}. "
|
305
|
+
f"Please wait for it to complete."
|
306
|
+
)
|
307
|
+
|
308
|
+
LOG.debug(f"Running qemu-img command: {' '.join(command)}")
|
309
|
+
|
310
|
+
# Use Popen for better process control (kill on timeout)
|
311
|
+
process = subprocess.Popen(
|
312
|
+
command,
|
313
|
+
stdout=subprocess.PIPE,
|
314
|
+
stderr=subprocess.PIPE,
|
315
|
+
text=True,
|
316
|
+
)
|
317
|
+
|
318
|
+
try:
|
319
|
+
stdout, stderr = process.communicate(timeout=timeout)
|
320
|
+
except subprocess.TimeoutExpired:
|
321
|
+
# Kill the process on timeout to prevent resource leaks
|
322
|
+
LOG.warning(
|
323
|
+
f"qemu-img command timed out after {timeout}s, killing process"
|
324
|
+
)
|
325
|
+
process.kill()
|
326
|
+
# Wait for process to actually die and collect zombie
|
327
|
+
try:
|
328
|
+
process.wait(timeout=Timeouts.PROCESS_KILL)
|
329
|
+
except subprocess.TimeoutExpired:
|
330
|
+
# Force kill if still alive
|
331
|
+
process.terminate()
|
332
|
+
process.wait(timeout=Timeouts.PROCESS_WAIT_AFTER_KILL)
|
333
|
+
|
334
|
+
raise SnapshotError(
|
335
|
+
f"qemu-img command timed out after {timeout} seconds and was killed"
|
336
|
+
)
|
337
|
+
|
338
|
+
# Check exit code
|
339
|
+
if process.returncode != 0:
|
340
|
+
raise subprocess.CalledProcessError(
|
341
|
+
process.returncode, command, stdout, stderr
|
342
|
+
)
|
343
|
+
|
344
|
+
return stdout.strip()
|
345
|
+
|
346
|
+
except subprocess.CalledProcessError as e:
|
347
|
+
last_error = e
|
348
|
+
# Check if error is potentially transient
|
349
|
+
if self._is_transient_error(e.stderr):
|
350
|
+
if attempt < max_retries - 1:
|
351
|
+
wait_time = 2**attempt # Exponential backoff
|
352
|
+
LOG.warning(
|
353
|
+
f"qemu-img command failed (attempt {attempt + 1}/{max_retries}), "
|
354
|
+
f"retrying in {wait_time}s: {e.stderr}"
|
355
|
+
)
|
356
|
+
time.sleep(wait_time)
|
357
|
+
continue
|
358
|
+
# Non-transient error or final retry
|
359
|
+
raise SnapshotError(
|
360
|
+
f"qemu-img command failed: {e.stderr.strip()}"
|
361
|
+
)
|
362
|
+
|
363
|
+
finally:
|
364
|
+
# Release lock and clean up
|
365
|
+
if lock_file:
|
366
|
+
try:
|
367
|
+
fcntl.flock(lock_file.fileno(), fcntl.LOCK_UN)
|
368
|
+
lock_file.close()
|
369
|
+
if lock_file_path and lock_file_path.exists():
|
370
|
+
lock_file_path.unlink()
|
371
|
+
except Exception:
|
372
|
+
pass # Best effort cleanup
|
373
|
+
|
374
|
+
# Should not reach here, but handle gracefully
|
375
|
+
if last_error:
|
376
|
+
raise SnapshotError(
|
377
|
+
f"qemu-img command failed after {max_retries} attempts: "
|
378
|
+
f"{last_error.stderr.strip()}"
|
379
|
+
)
|
380
|
+
raise SnapshotError("qemu-img command failed for unknown reason")
|
381
|
+
|
382
|
+
def _is_transient_error(self, stderr: str) -> bool:
|
383
|
+
"""
|
384
|
+
Check if error message indicates a transient failure.
|
385
|
+
|
386
|
+
Args:
|
387
|
+
stderr: Error output from qemu-img
|
388
|
+
|
389
|
+
Returns:
|
390
|
+
True if error appears transient and worth retrying
|
391
|
+
"""
|
392
|
+
transient_indicators = [
|
393
|
+
"resource temporarily unavailable",
|
394
|
+
"device or resource busy",
|
395
|
+
"try again",
|
396
|
+
"temporary failure",
|
397
|
+
"connection timed out",
|
398
|
+
]
|
399
|
+
stderr_lower = stderr.lower()
|
400
|
+
return any(
|
401
|
+
indicator in stderr_lower for indicator in transient_indicators
|
402
|
+
)
|
403
|
+
|
404
|
+
def _list_snapshots(self, drive_path: Path) -> List[str]:
|
405
|
+
"""
|
406
|
+
List snapshots for a QCOW2 drive.
|
407
|
+
|
408
|
+
Args:
|
409
|
+
drive_path: Path to the drive file
|
410
|
+
|
411
|
+
Returns:
|
412
|
+
List of snapshot names
|
413
|
+
"""
|
414
|
+
try:
|
415
|
+
output = self._run_qemu_img(["snapshot", str(drive_path), "-l"])
|
416
|
+
|
417
|
+
# Parse qemu-img snapshot output
|
418
|
+
# Format: "Snapshot list:\nID TAG VM SIZE DATE VM CLOCK\n1 snap1 0
|
419
|
+
# B 2024-01-01 12:00:00 00:00:00.000\n"
|
420
|
+
lines = output.split("\n")
|
421
|
+
snapshots = []
|
422
|
+
|
423
|
+
for line in lines[2:]: # Skip header lines
|
424
|
+
if line.strip():
|
425
|
+
parts = line.split()
|
426
|
+
if len(parts) >= 2:
|
427
|
+
snapshot_name = parts[1] # TAG column
|
428
|
+
snapshots.append(snapshot_name)
|
429
|
+
|
430
|
+
return snapshots
|
431
|
+
except SnapshotError:
|
432
|
+
# If snapshot listing fails, assume no snapshots exist
|
433
|
+
return []
|
434
|
+
|
435
|
+
def get_drive_info(self, drive_name: str) -> Dict[str, Any]:
|
436
|
+
"""
|
437
|
+
Get information about a storage drive including snapshot data.
|
438
|
+
|
439
|
+
Args:
|
440
|
+
drive_name: Name of the drive
|
441
|
+
|
442
|
+
Returns:
|
443
|
+
Dictionary with drive information including snapshots
|
444
|
+
|
445
|
+
Raises:
|
446
|
+
SnapshotError: If drive not found
|
447
|
+
"""
|
448
|
+
device = self._get_snapshot_capable_device(drive_name)
|
449
|
+
drive_path = self._get_device_file_path(device)
|
450
|
+
snapshots = self._list_snapshots(drive_path)
|
451
|
+
|
452
|
+
# Get base device info and add snapshot information
|
453
|
+
info = device.get_info()
|
454
|
+
info.update(
|
455
|
+
{
|
456
|
+
"snapshots": snapshots,
|
457
|
+
"snapshot_count": len(snapshots),
|
458
|
+
}
|
459
|
+
)
|
460
|
+
|
461
|
+
return info
|
462
|
+
|
463
|
+
def list_snapshot_capable_drives(self) -> List[str]:
|
464
|
+
"""
|
465
|
+
Get list of drives that support snapshots.
|
466
|
+
|
467
|
+
Returns:
|
468
|
+
List of drive names that support snapshots
|
469
|
+
"""
|
470
|
+
return [
|
471
|
+
device.name
|
472
|
+
for device in self.storage_manager.get_snapshot_capable_devices()
|
473
|
+
]
|