dwipe 2.0.1__py3-none-any.whl → 3.0.0__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.
- dwipe/DeviceChangeMonitor.py +244 -0
- dwipe/DeviceInfo.py +703 -177
- dwipe/DeviceWorker.py +566 -0
- dwipe/DiskWipe.py +953 -214
- dwipe/DrivePreChecker.py +203 -0
- dwipe/FirmwareWipeTask.py +865 -0
- dwipe/NvmeTool.py +225 -0
- dwipe/PersistentState.py +45 -16
- dwipe/Prereqs.py +84 -0
- dwipe/SataTool.py +499 -0
- dwipe/StructuredLogger.py +644 -0
- dwipe/Tunables.py +62 -0
- dwipe/Utils.py +298 -3
- dwipe/VerifyTask.py +412 -0
- dwipe/WipeJob.py +631 -171
- dwipe/WipeTask.py +150 -0
- dwipe/WriteTask.py +402 -0
- dwipe/main.py +34 -9
- dwipe-3.0.0.dist-info/METADATA +566 -0
- dwipe-3.0.0.dist-info/RECORD +24 -0
- dwipe/ToolManager.py +0 -637
- dwipe/WipeJobFuture.py +0 -245
- dwipe-2.0.1.dist-info/METADATA +0 -410
- dwipe-2.0.1.dist-info/RECORD +0 -14
- {dwipe-2.0.1.dist-info → dwipe-3.0.0.dist-info}/WHEEL +0 -0
- {dwipe-2.0.1.dist-info → dwipe-3.0.0.dist-info}/entry_points.txt +0 -0
- {dwipe-2.0.1.dist-info → dwipe-3.0.0.dist-info}/licenses/LICENSE +0 -0
dwipe/ToolManager.py
DELETED
|
@@ -1,637 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
Hardware Secure Erase Module for dwipe
|
|
4
|
-
Provides pre-checks, execution, monitoring, and fallback for hardware-level wipes
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
import subprocess
|
|
8
|
-
import shutil
|
|
9
|
-
import os
|
|
10
|
-
import time
|
|
11
|
-
import threading
|
|
12
|
-
import sys
|
|
13
|
-
from typing import Dict, List, Optional, Tuple, Callable
|
|
14
|
-
from enum import Enum
|
|
15
|
-
from dataclasses import dataclass
|
|
16
|
-
|
|
17
|
-
# ============================================================================
|
|
18
|
-
# Part 1: Tool Manager (Dependency Management)
|
|
19
|
-
# ============================================================================
|
|
20
|
-
|
|
21
|
-
class ToolManager:
|
|
22
|
-
"""Manages tool dependencies (hdparm, nvme-cli)"""
|
|
23
|
-
|
|
24
|
-
TOOL_PACKAGES = {
|
|
25
|
-
'hdparm': {
|
|
26
|
-
'apt': ['hdparm'],
|
|
27
|
-
'dnf': ['hdparm'],
|
|
28
|
-
'yum': ['hdparm'],
|
|
29
|
-
'pacman': ['hdparm'],
|
|
30
|
-
'zypper': ['hdparm'],
|
|
31
|
-
'apk': ['hdparm'],
|
|
32
|
-
'brew': ['hdparm'],
|
|
33
|
-
},
|
|
34
|
-
'nvme': {
|
|
35
|
-
'apt': ['nvme-cli'],
|
|
36
|
-
'dnf': ['nvme-cli'],
|
|
37
|
-
'yum': ['nvme-cli'],
|
|
38
|
-
'pacman': ['nvme-cli'],
|
|
39
|
-
'zypper': ['nvme-cli'],
|
|
40
|
-
'apk': ['nvme-cli'],
|
|
41
|
-
'brew': ['nvme-cli'],
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
def __init__(self, auto_install: bool = False, verbose: bool = False):
|
|
46
|
-
self.auto_install = auto_install
|
|
47
|
-
self.verbose = verbose
|
|
48
|
-
self.package_manager = self._detect_package_manager()
|
|
49
|
-
|
|
50
|
-
def _detect_package_manager(self) -> Optional[str]:
|
|
51
|
-
package_managers = {
|
|
52
|
-
'apt': ['apt-get', 'apt'],
|
|
53
|
-
'dnf': ['dnf'],
|
|
54
|
-
'yum': ['yum'],
|
|
55
|
-
'pacman': ['pacman'],
|
|
56
|
-
'zypper': ['zypper'],
|
|
57
|
-
'apk': ['apk'],
|
|
58
|
-
'brew': ['brew'],
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
for pm, binaries in package_managers.items():
|
|
62
|
-
for binary in binaries:
|
|
63
|
-
if shutil.which(binary):
|
|
64
|
-
return pm
|
|
65
|
-
return None
|
|
66
|
-
|
|
67
|
-
def tool_available(self, tool_name: str) -> bool:
|
|
68
|
-
return shutil.which(tool_name) is not None
|
|
69
|
-
|
|
70
|
-
def ensure_tool(self, tool_name: str, critical: bool = True) -> bool:
|
|
71
|
-
if self.tool_available(tool_name):
|
|
72
|
-
return True
|
|
73
|
-
|
|
74
|
-
if self.auto_install and self._install_tool(tool_name):
|
|
75
|
-
return True
|
|
76
|
-
|
|
77
|
-
if critical:
|
|
78
|
-
print(f"ERROR: Required tool '{tool_name}' not found")
|
|
79
|
-
packages = self.TOOL_PACKAGES.get(tool_name, {}).get(self.package_manager, [])
|
|
80
|
-
if packages:
|
|
81
|
-
print(f"Install with: sudo {self.package_manager} install {packages[0]}")
|
|
82
|
-
return False
|
|
83
|
-
|
|
84
|
-
def _install_tool(self, tool_name: str) -> bool:
|
|
85
|
-
"""Install tool using package manager"""
|
|
86
|
-
# Simplified - use the installation logic from earlier if needed
|
|
87
|
-
return False # Placeholder
|
|
88
|
-
|
|
89
|
-
def get_tool_path(self, tool_name: str) -> Optional[str]:
|
|
90
|
-
return shutil.which(tool_name)
|
|
91
|
-
|
|
92
|
-
# ============================================================================
|
|
93
|
-
# Part 2: Drive Pre-Checks
|
|
94
|
-
# ============================================================================
|
|
95
|
-
|
|
96
|
-
class EraseStatus(Enum):
|
|
97
|
-
NOT_STARTED = "not_started"
|
|
98
|
-
STARTING = "starting"
|
|
99
|
-
IN_PROGRESS = "in_progress"
|
|
100
|
-
COMPLETE = "complete"
|
|
101
|
-
FAILED = "failed"
|
|
102
|
-
UNKNOWN = "unknown"
|
|
103
|
-
|
|
104
|
-
@dataclass
|
|
105
|
-
class PreCheckResult:
|
|
106
|
-
compatible: bool = False
|
|
107
|
-
tool: Optional[str] = None
|
|
108
|
-
frozen: bool = False
|
|
109
|
-
locked: bool = False
|
|
110
|
-
enhanced_supported: bool = False
|
|
111
|
-
issues: List[str] = None
|
|
112
|
-
recommendation: Optional[str] = None
|
|
113
|
-
|
|
114
|
-
def __post_init__(self):
|
|
115
|
-
if self.issues is None:
|
|
116
|
-
self.issues = []
|
|
117
|
-
|
|
118
|
-
class DrivePreChecker:
|
|
119
|
-
"""Pre-check drive before attempting secure erase"""
|
|
120
|
-
|
|
121
|
-
def __init__(self, timeout: int = 10):
|
|
122
|
-
self.timeout = timeout
|
|
123
|
-
|
|
124
|
-
def is_usb_attached(self, device: str) -> bool:
|
|
125
|
-
"""Check if device is USB-attached"""
|
|
126
|
-
dev_name = os.path.basename(device)
|
|
127
|
-
|
|
128
|
-
# Check via sysfs
|
|
129
|
-
sys_path = f'/sys/block/{dev_name}'
|
|
130
|
-
if os.path.exists(sys_path):
|
|
131
|
-
try:
|
|
132
|
-
# Check if in USB hierarchy
|
|
133
|
-
real_path = os.path.realpath(sys_path)
|
|
134
|
-
if 'usb' in real_path.lower():
|
|
135
|
-
return True
|
|
136
|
-
|
|
137
|
-
# Check via udev
|
|
138
|
-
udev_info = subprocess.run(
|
|
139
|
-
['udevadm', 'info', '-q', 'property', '-n', device],
|
|
140
|
-
capture_output=True,
|
|
141
|
-
text=True,
|
|
142
|
-
timeout=5
|
|
143
|
-
)
|
|
144
|
-
if udev_info.returncode == 0 and 'ID_BUS=usb' in udev_info.stdout:
|
|
145
|
-
return True
|
|
146
|
-
except:
|
|
147
|
-
pass
|
|
148
|
-
|
|
149
|
-
return False
|
|
150
|
-
|
|
151
|
-
def check_nvme_drive(self, device: str) -> PreCheckResult:
|
|
152
|
-
"""Check if NVMe secure erase will likely work"""
|
|
153
|
-
result = PreCheckResult(tool='nvme')
|
|
154
|
-
|
|
155
|
-
try:
|
|
156
|
-
# Check if device exists
|
|
157
|
-
if not os.path.exists(device):
|
|
158
|
-
result.issues.append(f"Device {device} does not exist")
|
|
159
|
-
return result
|
|
160
|
-
|
|
161
|
-
# Check USB attachment
|
|
162
|
-
if self.is_usb_attached(device):
|
|
163
|
-
result.issues.append("NVMe is USB-attached - hardware erase unreliable")
|
|
164
|
-
result.recommendation = "Use software wipe"
|
|
165
|
-
return result
|
|
166
|
-
|
|
167
|
-
# Check if NVMe device responds
|
|
168
|
-
id_ctrl = subprocess.run(
|
|
169
|
-
['nvme', 'id-ctrl', device],
|
|
170
|
-
capture_output=True,
|
|
171
|
-
text=True,
|
|
172
|
-
timeout=self.timeout
|
|
173
|
-
)
|
|
174
|
-
|
|
175
|
-
if id_ctrl.returncode != 0:
|
|
176
|
-
result.issues.append(f"Not an NVMe device: {id_ctrl.stderr}")
|
|
177
|
-
return result
|
|
178
|
-
|
|
179
|
-
# Check format support
|
|
180
|
-
if 'Format NVM' not in id_ctrl.stdout:
|
|
181
|
-
result.issues.append("Drive doesn't support Format NVM command")
|
|
182
|
-
|
|
183
|
-
# Check for write protection
|
|
184
|
-
id_ns = subprocess.run(
|
|
185
|
-
['nvme', 'id-ns', device],
|
|
186
|
-
capture_output=True,
|
|
187
|
-
text=True,
|
|
188
|
-
timeout=self.timeout
|
|
189
|
-
)
|
|
190
|
-
|
|
191
|
-
if id_ns.returncode == 0 and 'Write Protected' in id_ns.stdout:
|
|
192
|
-
result.issues.append("Namespace is write protected")
|
|
193
|
-
|
|
194
|
-
result.compatible = len(result.issues) == 0
|
|
195
|
-
result.recommendation = "Proceed with hardware erase" if result.compatible else "Use software wipe"
|
|
196
|
-
|
|
197
|
-
except subprocess.TimeoutExpired:
|
|
198
|
-
result.issues.append(f"Command timed out after {self.timeout}s")
|
|
199
|
-
except Exception as e:
|
|
200
|
-
result.issues.append(f"Unexpected error: {e}")
|
|
201
|
-
|
|
202
|
-
return result
|
|
203
|
-
|
|
204
|
-
def check_ata_drive(self, device: str) -> PreCheckResult:
|
|
205
|
-
"""Check if ATA secure erase will likely work"""
|
|
206
|
-
result = PreCheckResult(tool='hdparm')
|
|
207
|
-
|
|
208
|
-
try:
|
|
209
|
-
if not os.path.exists(device):
|
|
210
|
-
result.issues.append(f"Device {device} does not exist")
|
|
211
|
-
return result
|
|
212
|
-
|
|
213
|
-
# Check USB attachment
|
|
214
|
-
if self.is_usb_attached(device):
|
|
215
|
-
result.issues.append("Drive is USB-attached - hardware erase unreliable")
|
|
216
|
-
result.recommendation = "Use software wipe"
|
|
217
|
-
return result
|
|
218
|
-
|
|
219
|
-
# Get drive info
|
|
220
|
-
info = subprocess.run(
|
|
221
|
-
['hdparm', '-I', device],
|
|
222
|
-
capture_output=True,
|
|
223
|
-
text=True,
|
|
224
|
-
timeout=self.timeout
|
|
225
|
-
)
|
|
226
|
-
|
|
227
|
-
if info.returncode != 0:
|
|
228
|
-
result.issues.append(f"Drive not responsive: {info.stderr}")
|
|
229
|
-
return result
|
|
230
|
-
|
|
231
|
-
output = info.stdout
|
|
232
|
-
|
|
233
|
-
# Check if frozen
|
|
234
|
-
if 'frozen' in output.lower():
|
|
235
|
-
result.frozen = True
|
|
236
|
-
result.issues.append("Drive is FROZEN - will hang on erase")
|
|
237
|
-
|
|
238
|
-
# Check if locked/enabled
|
|
239
|
-
if 'enabled' in output and 'not' not in output:
|
|
240
|
-
result.locked = True
|
|
241
|
-
result.issues.append("Security is ENABLED - needs password")
|
|
242
|
-
|
|
243
|
-
# Check enhanced erase support
|
|
244
|
-
if 'supported: enhanced erase' in output:
|
|
245
|
-
result.enhanced_supported = True
|
|
246
|
-
|
|
247
|
-
# Check ATA device and erase support
|
|
248
|
-
if 'ATA' not in output and 'SATA' not in output:
|
|
249
|
-
result.issues.append("Not an ATA/SATA device")
|
|
250
|
-
|
|
251
|
-
if 'SECURITY ERASE UNIT' not in output:
|
|
252
|
-
result.issues.append("Drive doesn't support SECURITY ERASE UNIT")
|
|
253
|
-
|
|
254
|
-
result.compatible = len(result.issues) == 0
|
|
255
|
-
|
|
256
|
-
if result.compatible:
|
|
257
|
-
result.recommendation = "Proceed with hardware erase"
|
|
258
|
-
elif result.frozen:
|
|
259
|
-
result.recommendation = "Thaw drive first or use software wipe"
|
|
260
|
-
elif result.locked:
|
|
261
|
-
result.recommendation = "Disable security first or use software wipe"
|
|
262
|
-
else:
|
|
263
|
-
result.recommendation = "Use software wipe"
|
|
264
|
-
|
|
265
|
-
except subprocess.TimeoutExpired:
|
|
266
|
-
result.issues.append(f"Command timed out after {self.timeout}s")
|
|
267
|
-
except Exception as e:
|
|
268
|
-
result.issues.append(f"Unexpected error: {e}")
|
|
269
|
-
|
|
270
|
-
return result
|
|
271
|
-
|
|
272
|
-
def can_use_hardware_erase(self, device: str) -> PreCheckResult:
|
|
273
|
-
"""
|
|
274
|
-
Determine if hardware erase will work.
|
|
275
|
-
Returns comprehensive pre-check result.
|
|
276
|
-
"""
|
|
277
|
-
if not os.path.exists(device):
|
|
278
|
-
return PreCheckResult(issues=[f"Device {device} does not exist"])
|
|
279
|
-
|
|
280
|
-
if 'nvme' in device:
|
|
281
|
-
return self.check_nvme_drive(device)
|
|
282
|
-
elif device.startswith('/dev/sd'):
|
|
283
|
-
return self.check_ata_drive(device)
|
|
284
|
-
else:
|
|
285
|
-
return PreCheckResult(issues=[f"Unsupported device type: {device}"])
|
|
286
|
-
|
|
287
|
-
# ============================================================================
|
|
288
|
-
# Part 3: Drive Eraser with Monitoring
|
|
289
|
-
# ============================================================================
|
|
290
|
-
|
|
291
|
-
class DriveEraser:
|
|
292
|
-
"""Execute and monitor hardware secure erase"""
|
|
293
|
-
|
|
294
|
-
def __init__(self, progress_callback: Optional[Callable] = None):
|
|
295
|
-
self.status = EraseStatus.NOT_STARTED
|
|
296
|
-
self.start_time = None
|
|
297
|
-
self.progress_callback = progress_callback
|
|
298
|
-
self.monitor_thread = None
|
|
299
|
-
self.current_process = None
|
|
300
|
-
|
|
301
|
-
def start_nvme_erase(self, device: str) -> bool:
|
|
302
|
-
"""Start NVMe secure erase (non-blocking)"""
|
|
303
|
-
try:
|
|
304
|
-
self.current_process = subprocess.Popen(
|
|
305
|
-
['nvme', 'format', device, '--ses=1'],
|
|
306
|
-
stdout=subprocess.PIPE,
|
|
307
|
-
stderr=subprocess.PIPE,
|
|
308
|
-
text=True
|
|
309
|
-
)
|
|
310
|
-
|
|
311
|
-
self.status = EraseStatus.STARTING
|
|
312
|
-
self.start_time = time.time()
|
|
313
|
-
self._start_monitoring(device, 'nvme')
|
|
314
|
-
return True
|
|
315
|
-
|
|
316
|
-
except Exception as e:
|
|
317
|
-
print(f"Failed to start NVMe erase: {e}")
|
|
318
|
-
self.status = EraseStatus.FAILED
|
|
319
|
-
return False
|
|
320
|
-
|
|
321
|
-
def start_ata_erase(self, device: str, enhanced: bool = True) -> bool:
|
|
322
|
-
"""Start ATA secure erase (non-blocking)"""
|
|
323
|
-
try:
|
|
324
|
-
# Build command
|
|
325
|
-
cmd = ['hdparm', '--user-master', 'u']
|
|
326
|
-
if enhanced:
|
|
327
|
-
cmd.extend(['--security-erase-enhanced', 'NULL'])
|
|
328
|
-
else:
|
|
329
|
-
cmd.extend(['--security-erase', 'NULL'])
|
|
330
|
-
cmd.append(device)
|
|
331
|
-
|
|
332
|
-
self.current_process = subprocess.Popen(
|
|
333
|
-
cmd,
|
|
334
|
-
stdout=subprocess.PIPE,
|
|
335
|
-
stderr=subprocess.PIPE,
|
|
336
|
-
text=True
|
|
337
|
-
)
|
|
338
|
-
|
|
339
|
-
self.status = EraseStatus.STARTING
|
|
340
|
-
self.start_time = time.time()
|
|
341
|
-
self._start_monitoring(device, 'ata')
|
|
342
|
-
return True
|
|
343
|
-
|
|
344
|
-
except Exception as e:
|
|
345
|
-
print(f"Failed to start ATA erase: {e}")
|
|
346
|
-
self.status = EraseStatus.FAILED
|
|
347
|
-
return False
|
|
348
|
-
|
|
349
|
-
def _start_monitoring(self, device: str, drive_type: str):
|
|
350
|
-
"""Start background monitoring thread"""
|
|
351
|
-
def monitor():
|
|
352
|
-
time.sleep(3) # Let command start
|
|
353
|
-
self.status = EraseStatus.IN_PROGRESS
|
|
354
|
-
|
|
355
|
-
check_interval = 5
|
|
356
|
-
max_checks = 7200 # 10 hours max
|
|
357
|
-
|
|
358
|
-
for _ in range(max_checks):
|
|
359
|
-
# Check if process completed
|
|
360
|
-
if self.current_process and self.current_process.poll() is not None:
|
|
361
|
-
if self.current_process.returncode == 0:
|
|
362
|
-
self.status = EraseStatus.COMPLETE
|
|
363
|
-
else:
|
|
364
|
-
self.status = EraseStatus.FAILED
|
|
365
|
-
break
|
|
366
|
-
|
|
367
|
-
# Update progress callback
|
|
368
|
-
if self.progress_callback:
|
|
369
|
-
elapsed = time.time() - self.start_time
|
|
370
|
-
progress = self._estimate_progress(elapsed, drive_type)
|
|
371
|
-
self.progress_callback(progress, elapsed, self.status)
|
|
372
|
-
|
|
373
|
-
time.sleep(check_interval)
|
|
374
|
-
else:
|
|
375
|
-
self.status = EraseStatus.FAILED
|
|
376
|
-
|
|
377
|
-
self.monitor_thread = threading.Thread(target=monitor, daemon=True)
|
|
378
|
-
self.monitor_thread.start()
|
|
379
|
-
|
|
380
|
-
def _estimate_progress(self, elapsed_seconds: float, drive_type: str) -> float:
|
|
381
|
-
"""Estimate fake progress based on typical times"""
|
|
382
|
-
if drive_type == 'nvme':
|
|
383
|
-
progress = min(1.0, elapsed_seconds / 30)
|
|
384
|
-
elif drive_type == 'ata':
|
|
385
|
-
# Very rough estimate - would need drive size for better guess
|
|
386
|
-
progress = min(1.0, elapsed_seconds / 3600)
|
|
387
|
-
else:
|
|
388
|
-
progress = 0.0
|
|
389
|
-
|
|
390
|
-
return progress * 100
|
|
391
|
-
|
|
392
|
-
def get_status(self) -> Dict:
|
|
393
|
-
"""Get current status info"""
|
|
394
|
-
elapsed = time.time() - self.start_time if self.start_time else 0
|
|
395
|
-
|
|
396
|
-
return {
|
|
397
|
-
'status': self.status.value,
|
|
398
|
-
'elapsed_seconds': elapsed,
|
|
399
|
-
'monitor_alive': self.monitor_thread and self.monitor_thread.is_alive(),
|
|
400
|
-
'process_active': self.current_process and self.current_process.poll() is None
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
def wait_for_completion(self, timeout: Optional[float] = None) -> bool:
|
|
404
|
-
"""Wait for erase to complete"""
|
|
405
|
-
if not self.current_process:
|
|
406
|
-
return False
|
|
407
|
-
|
|
408
|
-
try:
|
|
409
|
-
return_code = self.current_process.wait(timeout=timeout)
|
|
410
|
-
return return_code == 0
|
|
411
|
-
except subprocess.TimeoutExpired:
|
|
412
|
-
return False
|
|
413
|
-
|
|
414
|
-
# ============================================================================
|
|
415
|
-
# Part 4: Main Wipe Controller (Integration Point)
|
|
416
|
-
# ============================================================================
|
|
417
|
-
|
|
418
|
-
class HardwareWipeController:
|
|
419
|
-
"""
|
|
420
|
-
Main controller for hardware wiping.
|
|
421
|
-
This is what you'd integrate into dwipe.
|
|
422
|
-
"""
|
|
423
|
-
|
|
424
|
-
def __init__(self, auto_install_tools: bool = False, verbose: bool = False):
|
|
425
|
-
self.tool_mgr = ToolManager(auto_install=auto_install_tools, verbose=verbose)
|
|
426
|
-
self.pre_checker = DrivePreChecker(timeout=15)
|
|
427
|
-
self.eraser = None
|
|
428
|
-
self.verbose = verbose
|
|
429
|
-
|
|
430
|
-
def _log(self, message: str):
|
|
431
|
-
if self.verbose:
|
|
432
|
-
print(f"[HardwareWipe] {message}")
|
|
433
|
-
|
|
434
|
-
def prepare(self) -> bool:
|
|
435
|
-
"""Ensure required tools are available"""
|
|
436
|
-
if not self.tool_mgr.ensure_tool('hdparm', critical=True):
|
|
437
|
-
return False
|
|
438
|
-
if not self.tool_mgr.ensure_tool('nvme', critical=True):
|
|
439
|
-
return False
|
|
440
|
-
return True
|
|
441
|
-
|
|
442
|
-
def pre_check(self, device: str) -> PreCheckResult:
|
|
443
|
-
"""Perform comprehensive pre-check"""
|
|
444
|
-
self._log(f"Pre-checking {device}...")
|
|
445
|
-
result = self.pre_checker.can_use_hardware_erase(device)
|
|
446
|
-
|
|
447
|
-
if self.verbose:
|
|
448
|
-
print(f"Pre-check for {device}:")
|
|
449
|
-
print(f" Compatible: {result.compatible}")
|
|
450
|
-
print(f" Tool: {result.tool}")
|
|
451
|
-
if result.issues:
|
|
452
|
-
print(f" Issues: {', '.join(result.issues)}")
|
|
453
|
-
if result.recommendation:
|
|
454
|
-
print(f" Recommendation: {result.recommendation}")
|
|
455
|
-
|
|
456
|
-
return result
|
|
457
|
-
|
|
458
|
-
def wipe(self, device: str, fallback_callback: Optional[Callable] = None) -> bool:
|
|
459
|
-
"""
|
|
460
|
-
Execute hardware wipe with automatic fallback.
|
|
461
|
-
|
|
462
|
-
Args:
|
|
463
|
-
device: Device path (/dev/sda, /dev/nvme0n1, etc.)
|
|
464
|
-
fallback_callback: Function to call if hardware wipe fails
|
|
465
|
-
Should accept device path and return bool
|
|
466
|
-
|
|
467
|
-
Returns:
|
|
468
|
-
True if wipe succeeded (hardware or software), False otherwise
|
|
469
|
-
"""
|
|
470
|
-
if not self.prepare():
|
|
471
|
-
print("Required tools not available")
|
|
472
|
-
return False
|
|
473
|
-
|
|
474
|
-
# Step 1: Pre-check
|
|
475
|
-
pre_check = self.pre_check(device)
|
|
476
|
-
|
|
477
|
-
if not pre_check.compatible:
|
|
478
|
-
print(f"Hardware erase not compatible for {device}:")
|
|
479
|
-
for issue in pre_check.issues:
|
|
480
|
-
print(f" - {issue}")
|
|
481
|
-
|
|
482
|
-
if fallback_callback:
|
|
483
|
-
self._log("Falling back to software wipe...")
|
|
484
|
-
return fallback_callback(device)
|
|
485
|
-
return False
|
|
486
|
-
|
|
487
|
-
# Step 2: Show user what to expect
|
|
488
|
-
tool_name = pre_check.tool
|
|
489
|
-
print(f"Using {tool_name} for hardware secure erase...")
|
|
490
|
-
print("Note: Drive erases in firmware - tool will exit immediately.")
|
|
491
|
-
|
|
492
|
-
if tool_name == 'nvme':
|
|
493
|
-
print("Expected time: 2-10 seconds")
|
|
494
|
-
elif tool_name == 'hdparm' and pre_check.enhanced_supported:
|
|
495
|
-
print("Expected time: 10-60 seconds (enhanced erase)")
|
|
496
|
-
elif tool_name == 'hdparm':
|
|
497
|
-
print("Expected time: 1-3 hours per TB (normal erase)")
|
|
498
|
-
|
|
499
|
-
# Step 3: Start erase
|
|
500
|
-
self.eraser = DriveEraser(progress_callback=self._progress_update)
|
|
501
|
-
|
|
502
|
-
try:
|
|
503
|
-
if tool_name == 'nvme':
|
|
504
|
-
success = self.eraser.start_nvme_erase(device)
|
|
505
|
-
else: # hdparm
|
|
506
|
-
enhanced = pre_check.enhanced_supported
|
|
507
|
-
success = self.eraser.start_ata_erase(device, enhanced)
|
|
508
|
-
|
|
509
|
-
if not success:
|
|
510
|
-
raise RuntimeError("Failed to start erase")
|
|
511
|
-
|
|
512
|
-
# Step 4: Monitor with timeout
|
|
513
|
-
timeout = self._get_timeout(tool_name, device)
|
|
514
|
-
print(f"Waiting up to {timeout//60} minutes for completion...")
|
|
515
|
-
|
|
516
|
-
# Simple spinner while waiting
|
|
517
|
-
spinner = ['|', '/', '-', '\\']
|
|
518
|
-
i = 0
|
|
519
|
-
|
|
520
|
-
while True:
|
|
521
|
-
status = self.eraser.get_status()
|
|
522
|
-
|
|
523
|
-
if status['status'] == EraseStatus.COMPLETE.value:
|
|
524
|
-
print(f"\nHardware secure erase completed successfully!")
|
|
525
|
-
return True
|
|
526
|
-
|
|
527
|
-
elif status['status'] == EraseStatus.FAILED.value:
|
|
528
|
-
print(f"\nHardware secure erase failed")
|
|
529
|
-
break
|
|
530
|
-
|
|
531
|
-
# Show spinner and elapsed time
|
|
532
|
-
elapsed = status['elapsed_seconds']
|
|
533
|
-
print(f"\r{spinner[i % 4]} Erasing... {int(elapsed)}s elapsed", end='')
|
|
534
|
-
i += 1
|
|
535
|
-
|
|
536
|
-
# Check timeout
|
|
537
|
-
if elapsed > timeout:
|
|
538
|
-
print(f"\nTimeout after {timeout} seconds")
|
|
539
|
-
break
|
|
540
|
-
|
|
541
|
-
time.sleep(0.5)
|
|
542
|
-
|
|
543
|
-
# If we get here, hardware failed
|
|
544
|
-
if fallback_callback:
|
|
545
|
-
print("Falling back to software wipe...")
|
|
546
|
-
return fallback_callback(device)
|
|
547
|
-
|
|
548
|
-
return False
|
|
549
|
-
|
|
550
|
-
except Exception as e:
|
|
551
|
-
print(f"Error during hardware erase: {e}")
|
|
552
|
-
if fallback_callback:
|
|
553
|
-
return fallback_callback(device)
|
|
554
|
-
return False
|
|
555
|
-
|
|
556
|
-
def _progress_update(self, progress: float, elapsed: float, status: EraseStatus):
|
|
557
|
-
"""Callback for progress updates"""
|
|
558
|
-
if self.verbose:
|
|
559
|
-
print(f"[Progress] {progress:.1f}% - {elapsed:.0f}s - {status.value}")
|
|
560
|
-
|
|
561
|
-
def _get_timeout(self, tool: str, device: str) -> int:
|
|
562
|
-
"""Get appropriate timeout based on drive type"""
|
|
563
|
-
if tool == 'nvme':
|
|
564
|
-
return 30 # 30 seconds for NVMe
|
|
565
|
-
elif tool == 'hdparm':
|
|
566
|
-
# Try to get drive size for better timeout
|
|
567
|
-
try:
|
|
568
|
-
size_gb = self._get_drive_size_gb(device)
|
|
569
|
-
# 2 hours per TB, minimum 30 minutes
|
|
570
|
-
hours = max(0.5, (size_gb / 1024) * 2)
|
|
571
|
-
return int(hours * 3600)
|
|
572
|
-
except:
|
|
573
|
-
return 7200 # 2 hours default
|
|
574
|
-
return 3600 # 1 hour default
|
|
575
|
-
|
|
576
|
-
def _get_drive_size_gb(self, device: str) -> float:
|
|
577
|
-
"""Get drive size in GB"""
|
|
578
|
-
try:
|
|
579
|
-
# Use blockdev to get size
|
|
580
|
-
result = subprocess.run(
|
|
581
|
-
['blockdev', '--getsize64', device],
|
|
582
|
-
capture_output=True,
|
|
583
|
-
text=True,
|
|
584
|
-
timeout=5
|
|
585
|
-
)
|
|
586
|
-
if result.returncode == 0:
|
|
587
|
-
size_bytes = int(result.stdout.strip())
|
|
588
|
-
return size_bytes / (1024**3) # Convert to GB
|
|
589
|
-
except:
|
|
590
|
-
pass
|
|
591
|
-
return 500 # Default guess
|
|
592
|
-
|
|
593
|
-
# ============================================================================
|
|
594
|
-
# Part 5: Example Usage & Integration Helper
|
|
595
|
-
# ============================================================================
|
|
596
|
-
|
|
597
|
-
def example_software_wipe(device: str) -> bool:
|
|
598
|
-
"""Example fallback function for software wipe"""
|
|
599
|
-
print(f"[Software] Would wipe {device} with dd/scrub/etc.")
|
|
600
|
-
# Implement your existing software wipe here
|
|
601
|
-
return True
|
|
602
|
-
|
|
603
|
-
def main():
|
|
604
|
-
"""Example standalone usage"""
|
|
605
|
-
import argparse
|
|
606
|
-
|
|
607
|
-
parser = argparse.ArgumentParser(description='Hardware Secure Erase Test')
|
|
608
|
-
parser.add_argument('device', help='Device to wipe (e.g., /dev/sda)')
|
|
609
|
-
parser.add_argument('--auto-install', action='store_true',
|
|
610
|
-
help='Automatically install missing tools')
|
|
611
|
-
parser.add_argument('--verbose', '-v', action='store_true',
|
|
612
|
-
help='Verbose output')
|
|
613
|
-
parser.add_argument('--no-fallback', action='store_true',
|
|
614
|
-
help='Don\'t fall back to software wipe')
|
|
615
|
-
args = parser.parse_args()
|
|
616
|
-
|
|
617
|
-
# Create controller
|
|
618
|
-
controller = HardwareWipeController(
|
|
619
|
-
auto_install_tools=args.auto_install,
|
|
620
|
-
verbose=args.verbose
|
|
621
|
-
)
|
|
622
|
-
|
|
623
|
-
# Define fallback
|
|
624
|
-
fallback = None if args.no_fallback else example_software_wipe
|
|
625
|
-
|
|
626
|
-
# Execute wipe
|
|
627
|
-
success = controller.wipe(args.device, fallback_callback=fallback)
|
|
628
|
-
|
|
629
|
-
if success:
|
|
630
|
-
print(f"\n✓ Wipe completed successfully")
|
|
631
|
-
return 0
|
|
632
|
-
else:
|
|
633
|
-
print(f"\n✗ Wipe failed")
|
|
634
|
-
return 1
|
|
635
|
-
|
|
636
|
-
if __name__ == '__main__':
|
|
637
|
-
sys.exit(main())
|