dwipe 2.0.0__py3-none-any.whl → 2.0.2__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/DeviceInfo.py +291 -59
- dwipe/DiskWipe.py +497 -172
- dwipe/DrivePreChecker.py +90 -0
- dwipe/FirmwareWipeTask.py +370 -0
- dwipe/LsblkMonitor.py +124 -0
- dwipe/PersistentState.py +28 -18
- dwipe/Prereqs.py +84 -0
- dwipe/StructuredLogger.py +643 -0
- dwipe/ToolManager.py +618 -0
- dwipe/Utils.py +108 -0
- dwipe/VerifyTask.py +410 -0
- dwipe/WipeJob.py +613 -165
- dwipe/WipeTask.py +148 -0
- dwipe/WriteTask.py +402 -0
- dwipe/main.py +14 -9
- {dwipe-2.0.0.dist-info → dwipe-2.0.2.dist-info}/METADATA +69 -30
- dwipe-2.0.2.dist-info/RECORD +21 -0
- dwipe/WipeJobFuture.py +0 -245
- dwipe-2.0.0.dist-info/RECORD +0 -13
- {dwipe-2.0.0.dist-info → dwipe-2.0.2.dist-info}/WHEEL +0 -0
- {dwipe-2.0.0.dist-info → dwipe-2.0.2.dist-info}/entry_points.txt +0 -0
- {dwipe-2.0.0.dist-info → dwipe-2.0.2.dist-info}/licenses/LICENSE +0 -0
dwipe/ToolManager.py
ADDED
|
@@ -0,0 +1,618 @@
|
|
|
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
|
+
# ============================================================================
|
|
22
|
+
# Part 2: Drive Pre-Checks
|
|
23
|
+
# ============================================================================
|
|
24
|
+
|
|
25
|
+
class EraseStatus(Enum):
|
|
26
|
+
NOT_STARTED = "not_started"
|
|
27
|
+
STARTING = "starting"
|
|
28
|
+
IN_PROGRESS = "in_progress"
|
|
29
|
+
COMPLETE = "complete"
|
|
30
|
+
FAILED = "failed"
|
|
31
|
+
UNKNOWN = "unknown"
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class OLD-PreCheckResult:
|
|
35
|
+
compatible: bool = False
|
|
36
|
+
tool: Optional[str] = None
|
|
37
|
+
frozen: bool = False
|
|
38
|
+
locked: bool = False
|
|
39
|
+
enhanced_supported: bool = False
|
|
40
|
+
issues: List[str] = None
|
|
41
|
+
recommendation: Optional[str] = None
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class PreCheckResult:
|
|
45
|
+
# compatible: bool = False
|
|
46
|
+
# tool: Optional[str] = None
|
|
47
|
+
# frozen: bool = False
|
|
48
|
+
# locked: bool = False
|
|
49
|
+
# enhanced_supported: bool = False
|
|
50
|
+
issues: List[str] = None # list of "why not" ... any set, no wipe
|
|
51
|
+
# recommendation: Optional[str] = None
|
|
52
|
+
modes = {} # dict of descr/how 'Cropto': '--wipe=crypto'
|
|
53
|
+
|
|
54
|
+
def __post_init__(self):
|
|
55
|
+
if self.issues is None:
|
|
56
|
+
self.issues = []
|
|
57
|
+
|
|
58
|
+
class DrivePreChecker:
|
|
59
|
+
"""Pre-check drive before attempting secure erase"""
|
|
60
|
+
|
|
61
|
+
def __init__(self, timeout: int = 10):
|
|
62
|
+
self.timeout = timeout
|
|
63
|
+
|
|
64
|
+
def is_usb_attached(self, device: str) -> bool:
|
|
65
|
+
"""Check if device is USB-attached"""
|
|
66
|
+
dev_name = os.path.basename(device)
|
|
67
|
+
|
|
68
|
+
# Check via sysfs
|
|
69
|
+
sys_path = f'/sys/block/{dev_name}'
|
|
70
|
+
if os.path.exists(sys_path):
|
|
71
|
+
try:
|
|
72
|
+
# Check if in USB hierarchy
|
|
73
|
+
real_path = os.path.realpath(sys_path)
|
|
74
|
+
if 'usb' in real_path.lower():
|
|
75
|
+
return True
|
|
76
|
+
|
|
77
|
+
# Check via udev
|
|
78
|
+
udev_info = subprocess.run(
|
|
79
|
+
['udevadm', 'info', '-q', 'property', '-n', device],
|
|
80
|
+
capture_output=True,
|
|
81
|
+
text=True,
|
|
82
|
+
timeout=5
|
|
83
|
+
)
|
|
84
|
+
if udev_info.returncode == 0 and 'ID_BUS=usb' in udev_info.stdout:
|
|
85
|
+
return True
|
|
86
|
+
except:
|
|
87
|
+
pass
|
|
88
|
+
|
|
89
|
+
return False
|
|
90
|
+
|
|
91
|
+
def check_nvme_drive(self, device: str) -> PreCheckResult:
|
|
92
|
+
"""Probes NVMe and returns specific command flags for available wipe modes"""
|
|
93
|
+
result = PreCheckResult()
|
|
94
|
+
result.modes = {}
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
# Get controller capabilities in JSON for easy parsing
|
|
98
|
+
id_ctrl = subprocess.run(
|
|
99
|
+
['nvme', 'id-ctrl', device, '-o', 'json'],
|
|
100
|
+
capture_output=True, text=True, timeout=self.timeout
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
if id_ctrl.returncode != 0:
|
|
104
|
+
result.issues.append("NVMe controller unresponsive")
|
|
105
|
+
return result
|
|
106
|
+
|
|
107
|
+
import json
|
|
108
|
+
data = json.loads(id_ctrl.stdout)
|
|
109
|
+
|
|
110
|
+
# 1. Check for Sanitize Capabilities (The most modern/safe method)
|
|
111
|
+
# Bit 1: Block, Bit 2: Crypto, Bit 3: Overwrite
|
|
112
|
+
sanicap = data.get('sanicap', 0)
|
|
113
|
+
if sanicap > 0:
|
|
114
|
+
# We use OrderedDict or similar to put the 'best' options first
|
|
115
|
+
if sanicap & 0x04: # Crypto Erase
|
|
116
|
+
result.modes['Sanitize-Crypto'] = 'sanitize --action=0x04'
|
|
117
|
+
if sanicap & 0x02: # Block Erase (Physical)
|
|
118
|
+
result.modes['Sanitize-Block'] = 'sanitize --action=0x02'
|
|
119
|
+
if sanicap & 0x08: # Overwrite
|
|
120
|
+
result.modes['Sanitize-Overwrite'] = 'sanitize --action=0x03'
|
|
121
|
+
|
|
122
|
+
# 2. Check for Legacy Format Capabilities
|
|
123
|
+
# Bit 1: Crypto, Bit 2: User Data Erase
|
|
124
|
+
fna = data.get('fna', 0)
|
|
125
|
+
if 'Format NVM' in id_ctrl.stdout:
|
|
126
|
+
# Check if Crypto Erase is supported via Format
|
|
127
|
+
if (fna >> 2) & 0x1:
|
|
128
|
+
result.modes['Format-Crypto'] = 'format --ses=2'
|
|
129
|
+
# Standard User Data Erase
|
|
130
|
+
result.modes['Format-Erase'] = 'format --ses=1'
|
|
131
|
+
|
|
132
|
+
# Final Validation
|
|
133
|
+
if not result.modes:
|
|
134
|
+
result.issues.append("No HW wipe modes (Sanitize/Format) supported")
|
|
135
|
+
|
|
136
|
+
except Exception as e:
|
|
137
|
+
result.issues.append(f"Probe Error: {str(e)}")
|
|
138
|
+
|
|
139
|
+
return result
|
|
140
|
+
|
|
141
|
+
def check_ata_drive(self, device: str) -> PreCheckResult:
|
|
142
|
+
"""Probes SATA/ATA and returns hdparm flags or specific blocking reasons
|
|
143
|
+
+ Why the "NULL" password? In the modes dictionary above, we use NULL.
|
|
144
|
+
- To perform an ATA Secure Erase, you have to set a temporary password first,
|
|
145
|
+
then immediately issue the erase command with that same password.
|
|
146
|
+
- Most tools (and hdparm itself) use NULL or a simple string like p
|
|
147
|
+
as a throwaway.
|
|
148
|
+
- Note: If the dwipe app crashes after setting the password but before the
|
|
149
|
+
erase finishes, the drive will stay locked. On the next run, your enabled
|
|
150
|
+
check (Step 3) will catch this.
|
|
151
|
+
+ Handling "Frozen" in the UI
|
|
152
|
+
-the "Frozen" issue is the one that will frustrate users most.
|
|
153
|
+
-The "Short Crisp Reason": Drive is FROZEN.
|
|
154
|
+
- The Fix: To unfreeze, try suspending (sleeping) and waking the computer,
|
|
155
|
+
or re-plugging the drive's power cable."
|
|
156
|
+
|
|
157
|
+
+ Now, dwipe builds that list:
|
|
158
|
+
It calls can_use_hardware_erase().
|
|
159
|
+
It looks at result.issues. If empty, the [f]:irmW key is active.
|
|
160
|
+
"""
|
|
161
|
+
result = PreCheckResult()
|
|
162
|
+
result.modes = {}
|
|
163
|
+
|
|
164
|
+
try:
|
|
165
|
+
# Get drive info via hdparm
|
|
166
|
+
info = subprocess.run(
|
|
167
|
+
['hdparm', '-I', device],
|
|
168
|
+
capture_output=True, text=True, timeout=self.timeout
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
if info.returncode != 0:
|
|
172
|
+
result.issues.append("Drive not responsive to hdparm")
|
|
173
|
+
return result
|
|
174
|
+
|
|
175
|
+
out = info.stdout.lower()
|
|
176
|
+
|
|
177
|
+
# 1. Check if the drive even supports Security Erase
|
|
178
|
+
if "security erase unit" not in out:
|
|
179
|
+
result.issues.append("Drive does not support ATA Security Erase")
|
|
180
|
+
return result
|
|
181
|
+
|
|
182
|
+
# 2. Check for "Frozen" state (The most common blocker)
|
|
183
|
+
# A frozen drive rejects security commands until a power cycle.
|
|
184
|
+
if "frozen" in out and "not frozen" not in out:
|
|
185
|
+
result.issues.append("Drive is FROZEN (BIOS/OS lock)")
|
|
186
|
+
# You might want to keep this in issues so user can't select it,
|
|
187
|
+
# or move it to a 'warning' if you want to allow them to try anyway.
|
|
188
|
+
|
|
189
|
+
# 3. Check if security is already "Enabled" (Drive is locked)
|
|
190
|
+
if "enabled" in out and "not enabled" not in out:
|
|
191
|
+
# If it's already locked, we can't wipe without the existing password.
|
|
192
|
+
result.issues.append("Security is ENABLED (Drive is password locked)")
|
|
193
|
+
|
|
194
|
+
# 4. Populate Modes if no fatal issues
|
|
195
|
+
if not result.issues:
|
|
196
|
+
# Enhanced Erase: Usually writes a pattern or destroys encryption keys
|
|
197
|
+
if "enhanced erase" in out:
|
|
198
|
+
result.modes['ATA-Enhanced'] = '--user-master u --security-erase-enhanced NULL'
|
|
199
|
+
|
|
200
|
+
# Normal Erase: Usually writes zeros to the whole platter
|
|
201
|
+
result.modes['ATA-Normal'] = '--user-master u --security-erase NULL'
|
|
202
|
+
|
|
203
|
+
except Exception as e:
|
|
204
|
+
result.issues.append(f"ATA Probe Error: {str(e)}")
|
|
205
|
+
return result
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
# ============================================================================
|
|
209
|
+
# Part 3: Drive Eraser with Monitoring
|
|
210
|
+
# ============================================================================
|
|
211
|
+
|
|
212
|
+
class DriveEraser:
|
|
213
|
+
"""Execute and monitor hardware secure erase"""
|
|
214
|
+
|
|
215
|
+
def __init__(self, progress_callback: Optional[Callable] = None):
|
|
216
|
+
self.status = EraseStatus.NOT_STARTED
|
|
217
|
+
self.start_time = None
|
|
218
|
+
self.progress_callback = progress_callback
|
|
219
|
+
self.monitor_thread = None
|
|
220
|
+
self.current_process = None
|
|
221
|
+
|
|
222
|
+
def run_firmware_wipe(self):
|
|
223
|
+
"""The thread target for firmware wipes"""
|
|
224
|
+
self.start_mono = time.monotonic()
|
|
225
|
+
|
|
226
|
+
# 1. Start the process (non-blocking)
|
|
227
|
+
# self.opts.hw_cmd might be: "nvme sanitize --action=0x02"
|
|
228
|
+
full_cmd = f"{self.opts.tool} {self.opts.hw_cmd} {self.device_path}"
|
|
229
|
+
self.process = subprocess.Popen(full_cmd.split(), stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
230
|
+
|
|
231
|
+
# 2. Polling Loop
|
|
232
|
+
while not self._abort_requested:
|
|
233
|
+
if self.process.poll() is not None:
|
|
234
|
+
break
|
|
235
|
+
|
|
236
|
+
# Optional: For NVMe, you can poll 'nvme sanitize-log' here
|
|
237
|
+
# to get actual 0-100% progress and update self.total_written
|
|
238
|
+
|
|
239
|
+
time.sleep(1)
|
|
240
|
+
|
|
241
|
+
# 3. Finalize
|
|
242
|
+
self.finish_mono = time.monotonic()
|
|
243
|
+
if self.process.returncode == 0:
|
|
244
|
+
self.total_written = self.total_size # Mark as done for the UI
|
|
245
|
+
|
|
246
|
+
def abort(self):
|
|
247
|
+
self._abort_requested = True
|
|
248
|
+
if self.process and self.process.poll() is None:
|
|
249
|
+
self.process.terminate() # Try nice first
|
|
250
|
+
time.sleep(0.5)
|
|
251
|
+
self.process.kill() # Then hammer it
|
|
252
|
+
|
|
253
|
+
"""
|
|
254
|
+
# Inside get_summary_dict...
|
|
255
|
+
is_hw = getattr(self.opts, 'is_hardware', False)
|
|
256
|
+
|
|
257
|
+
if is_hw:
|
|
258
|
+
# Rate and written bytes don't follow standard rules
|
|
259
|
+
wipe_step["rate"] = "Hardware"
|
|
260
|
+
wipe_step["status"] = "Sanitizing..." if not self.done else "Complete"
|
|
261
|
+
|
|
262
|
+
"""
|
|
263
|
+
|
|
264
|
+
def start_nvme_erase(self, device: str) -> bool:
|
|
265
|
+
"""Start NVMe secure erase (non-blocking)"""
|
|
266
|
+
try:
|
|
267
|
+
self.current_process = subprocess.Popen(
|
|
268
|
+
['nvme', 'format', device, '--ses=1'],
|
|
269
|
+
stdout=subprocess.PIPE,
|
|
270
|
+
stderr=subprocess.PIPE,
|
|
271
|
+
text=True
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
self.status = EraseStatus.STARTING
|
|
275
|
+
self.start_time = time.time()
|
|
276
|
+
self._start_monitoring(device, 'nvme')
|
|
277
|
+
return True
|
|
278
|
+
|
|
279
|
+
except Exception as e:
|
|
280
|
+
print(f"Failed to start NVMe erase: {e}")
|
|
281
|
+
self.status = EraseStatus.FAILED
|
|
282
|
+
return False
|
|
283
|
+
|
|
284
|
+
def start_ata_erase(self, device: str, enhanced: bool = True) -> bool:
|
|
285
|
+
"""Start ATA secure erase (non-blocking)"""
|
|
286
|
+
try:
|
|
287
|
+
# Build command
|
|
288
|
+
cmd = ['hdparm', '--user-master', 'u']
|
|
289
|
+
if enhanced:
|
|
290
|
+
cmd.extend(['--security-erase-enhanced', 'NULL'])
|
|
291
|
+
else:
|
|
292
|
+
cmd.extend(['--security-erase', 'NULL'])
|
|
293
|
+
cmd.append(device)
|
|
294
|
+
|
|
295
|
+
self.current_process = subprocess.Popen(
|
|
296
|
+
cmd,
|
|
297
|
+
stdout=subprocess.PIPE,
|
|
298
|
+
stderr=subprocess.PIPE,
|
|
299
|
+
text=True
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
self.status = EraseStatus.STARTING
|
|
303
|
+
self.start_time = time.time()
|
|
304
|
+
self._start_monitoring(device, 'ata')
|
|
305
|
+
return True
|
|
306
|
+
|
|
307
|
+
except Exception as e:
|
|
308
|
+
print(f"Failed to start ATA erase: {e}")
|
|
309
|
+
self.status = EraseStatus.FAILED
|
|
310
|
+
return False
|
|
311
|
+
|
|
312
|
+
def _start_monitoring(self, device: str, drive_type: str):
|
|
313
|
+
"""Start background monitoring thread"""
|
|
314
|
+
def monitor():
|
|
315
|
+
time.sleep(3) # Let command start
|
|
316
|
+
self.status = EraseStatus.IN_PROGRESS
|
|
317
|
+
|
|
318
|
+
check_interval = 5
|
|
319
|
+
max_checks = 7200 # 10 hours max
|
|
320
|
+
|
|
321
|
+
for _ in range(max_checks):
|
|
322
|
+
# Check if process completed
|
|
323
|
+
if self.current_process and self.current_process.poll() is not None:
|
|
324
|
+
if self.current_process.returncode == 0:
|
|
325
|
+
self.status = EraseStatus.COMPLETE
|
|
326
|
+
else:
|
|
327
|
+
self.status = EraseStatus.FAILED
|
|
328
|
+
break
|
|
329
|
+
|
|
330
|
+
# Update progress callback
|
|
331
|
+
if self.progress_callback:
|
|
332
|
+
elapsed = time.time() - self.start_time
|
|
333
|
+
progress = self._estimate_progress(elapsed, drive_type)
|
|
334
|
+
self.progress_callback(progress, elapsed, self.status)
|
|
335
|
+
|
|
336
|
+
time.sleep(check_interval)
|
|
337
|
+
else:
|
|
338
|
+
self.status = EraseStatus.FAILED
|
|
339
|
+
|
|
340
|
+
self.monitor_thread = threading.Thread(target=monitor, daemon=True)
|
|
341
|
+
self.monitor_thread.start()
|
|
342
|
+
|
|
343
|
+
def _estimate_progress(self, elapsed_seconds: float, drive_type: str) -> float:
|
|
344
|
+
"""Estimate fake progress based on typical times"""
|
|
345
|
+
if drive_type == 'nvme':
|
|
346
|
+
progress = min(1.0, elapsed_seconds / 30)
|
|
347
|
+
elif drive_type == 'ata':
|
|
348
|
+
# Very rough estimate - would need drive size for better guess
|
|
349
|
+
progress = min(1.0, elapsed_seconds / 3600)
|
|
350
|
+
else:
|
|
351
|
+
progress = 0.0
|
|
352
|
+
|
|
353
|
+
return progress * 100
|
|
354
|
+
|
|
355
|
+
def get_status(self) -> Dict:
|
|
356
|
+
"""Get current status info"""
|
|
357
|
+
elapsed = time.time() - self.start_time if self.start_time else 0
|
|
358
|
+
|
|
359
|
+
return {
|
|
360
|
+
'status': self.status.value,
|
|
361
|
+
'elapsed_seconds': elapsed,
|
|
362
|
+
'monitor_alive': self.monitor_thread and self.monitor_thread.is_alive(),
|
|
363
|
+
'process_active': self.current_process and self.current_process.poll() is None
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
def wait_for_completion(self, timeout: Optional[float] = None) -> bool:
|
|
367
|
+
"""Wait for erase to complete"""
|
|
368
|
+
if not self.current_process:
|
|
369
|
+
return False
|
|
370
|
+
|
|
371
|
+
try:
|
|
372
|
+
return_code = self.current_process.wait(timeout=timeout)
|
|
373
|
+
return return_code == 0
|
|
374
|
+
except subprocess.TimeoutExpired:
|
|
375
|
+
return False
|
|
376
|
+
|
|
377
|
+
# ============================================================================
|
|
378
|
+
# Part 4: Main Wipe Controller (Integration Point)
|
|
379
|
+
# ============================================================================
|
|
380
|
+
|
|
381
|
+
class HardwareWipeController:
|
|
382
|
+
"""
|
|
383
|
+
Main controller for hardware wiping.
|
|
384
|
+
This is what you'd integrate into dwipe.
|
|
385
|
+
"""
|
|
386
|
+
|
|
387
|
+
def __init__(self, auto_install_tools: bool = False, verbose: bool = False):
|
|
388
|
+
self.tool_mgr = ToolManager(auto_install=auto_install_tools, verbose=verbose)
|
|
389
|
+
self.pre_checker = DrivePreChecker(timeout=15)
|
|
390
|
+
self.eraser = None
|
|
391
|
+
self.verbose = verbose
|
|
392
|
+
|
|
393
|
+
def _log(self, message: str):
|
|
394
|
+
if self.verbose:
|
|
395
|
+
print(f"[HardwareWipe] {message}")
|
|
396
|
+
|
|
397
|
+
def prepare(self) -> bool:
|
|
398
|
+
"""Ensure required tools are available"""
|
|
399
|
+
if not self.tool_mgr.ensure_tool('hdparm', critical=True):
|
|
400
|
+
return False
|
|
401
|
+
if not self.tool_mgr.ensure_tool('nvme', critical=True):
|
|
402
|
+
return False
|
|
403
|
+
return True
|
|
404
|
+
|
|
405
|
+
def pre_check(self, device: str) -> PreCheckResult:
|
|
406
|
+
"""Perform comprehensive pre-check"""
|
|
407
|
+
self._log(f"Pre-checking {device}...")
|
|
408
|
+
result = self.pre_checker.can_use_hardware_erase(device)
|
|
409
|
+
|
|
410
|
+
if self.verbose:
|
|
411
|
+
print(f"Pre-check for {device}:")
|
|
412
|
+
print(f" Compatible: {result.compatible}")
|
|
413
|
+
print(f" Tool: {result.tool}")
|
|
414
|
+
if result.issues:
|
|
415
|
+
print(f" Issues: {', '.join(result.issues)}")
|
|
416
|
+
if result.recommendation:
|
|
417
|
+
print(f" Recommendation: {result.recommendation}")
|
|
418
|
+
|
|
419
|
+
return result
|
|
420
|
+
|
|
421
|
+
def wipe(self, device: str, fallback_callback: Optional[Callable] = None) -> bool:
|
|
422
|
+
"""
|
|
423
|
+
Execute hardware wipe with automatic fallback.
|
|
424
|
+
|
|
425
|
+
Args:
|
|
426
|
+
device: Device path (/dev/sda, /dev/nvme0n1, etc.)
|
|
427
|
+
fallback_callback: Function to call if hardware wipe fails
|
|
428
|
+
Should accept device path and return bool
|
|
429
|
+
|
|
430
|
+
Returns:
|
|
431
|
+
True if wipe succeeded (hardware or software), False otherwise
|
|
432
|
+
"""
|
|
433
|
+
if not self.prepare():
|
|
434
|
+
print("Required tools not available")
|
|
435
|
+
return False
|
|
436
|
+
|
|
437
|
+
# Step 1: Pre-check
|
|
438
|
+
pre_check = self.pre_check(device)
|
|
439
|
+
|
|
440
|
+
if not pre_check.compatible:
|
|
441
|
+
print(f"Hardware erase not compatible for {device}:")
|
|
442
|
+
for issue in pre_check.issues:
|
|
443
|
+
print(f" - {issue}")
|
|
444
|
+
|
|
445
|
+
if fallback_callback:
|
|
446
|
+
self._log("Falling back to software wipe...")
|
|
447
|
+
return fallback_callback(device)
|
|
448
|
+
return False
|
|
449
|
+
|
|
450
|
+
# Step 2: Show user what to expect
|
|
451
|
+
tool_name = pre_check.tool
|
|
452
|
+
print(f"Using {tool_name} for hardware secure erase...")
|
|
453
|
+
print("Note: Drive erases in firmware - tool will exit immediately.")
|
|
454
|
+
|
|
455
|
+
if tool_name == 'nvme':
|
|
456
|
+
print("Expected time: 2-10 seconds")
|
|
457
|
+
elif tool_name == 'hdparm' and pre_check.enhanced_supported:
|
|
458
|
+
print("Expected time: 10-60 seconds (enhanced erase)")
|
|
459
|
+
elif tool_name == 'hdparm':
|
|
460
|
+
print("Expected time: 1-3 hours per TB (normal erase)")
|
|
461
|
+
|
|
462
|
+
# Step 3: Start erase
|
|
463
|
+
self.eraser = DriveEraser(progress_callback=self._progress_update)
|
|
464
|
+
|
|
465
|
+
try:
|
|
466
|
+
if tool_name == 'nvme':
|
|
467
|
+
success = self.eraser.start_nvme_erase(device)
|
|
468
|
+
else: # hdparm
|
|
469
|
+
enhanced = pre_check.enhanced_supported
|
|
470
|
+
success = self.eraser.start_ata_erase(device, enhanced)
|
|
471
|
+
|
|
472
|
+
if not success:
|
|
473
|
+
raise RuntimeError("Failed to start erase")
|
|
474
|
+
|
|
475
|
+
# Step 4: Monitor with timeout
|
|
476
|
+
timeout = self._get_timeout(tool_name, device)
|
|
477
|
+
print(f"Waiting up to {timeout//60} minutes for completion...")
|
|
478
|
+
|
|
479
|
+
# Simple spinner while waiting
|
|
480
|
+
spinner = ['|', '/', '-', '\\']
|
|
481
|
+
i = 0
|
|
482
|
+
|
|
483
|
+
while True:
|
|
484
|
+
status = self.eraser.get_status()
|
|
485
|
+
|
|
486
|
+
if status['status'] == EraseStatus.COMPLETE.value:
|
|
487
|
+
print(f"\nHardware secure erase completed successfully!")
|
|
488
|
+
return True
|
|
489
|
+
|
|
490
|
+
elif status['status'] == EraseStatus.FAILED.value:
|
|
491
|
+
print(f"\nHardware secure erase failed")
|
|
492
|
+
break
|
|
493
|
+
|
|
494
|
+
# Show spinner and elapsed time
|
|
495
|
+
elapsed = status['elapsed_seconds']
|
|
496
|
+
print(f"\r{spinner[i % 4]} Erasing... {int(elapsed)}s elapsed", end='')
|
|
497
|
+
i += 1
|
|
498
|
+
|
|
499
|
+
# Check timeout
|
|
500
|
+
if elapsed > timeout:
|
|
501
|
+
print(f"\nTimeout after {timeout} seconds")
|
|
502
|
+
break
|
|
503
|
+
|
|
504
|
+
time.sleep(0.5)
|
|
505
|
+
|
|
506
|
+
# If we get here, hardware failed
|
|
507
|
+
if fallback_callback:
|
|
508
|
+
print("Falling back to software wipe...")
|
|
509
|
+
return fallback_callback(device)
|
|
510
|
+
|
|
511
|
+
return False
|
|
512
|
+
|
|
513
|
+
except Exception as e:
|
|
514
|
+
print(f"Error during hardware erase: {e}")
|
|
515
|
+
if fallback_callback:
|
|
516
|
+
return fallback_callback(device)
|
|
517
|
+
return False
|
|
518
|
+
|
|
519
|
+
def _progress_update(self, progress: float, elapsed: float, status: EraseStatus):
|
|
520
|
+
"""Callback for progress updates"""
|
|
521
|
+
if self.verbose:
|
|
522
|
+
print(f"[Progress] {progress:.1f}% - {elapsed:.0f}s - {status.value}")
|
|
523
|
+
|
|
524
|
+
def _get_timeout(self, tool: str, device: str) -> int:
|
|
525
|
+
"""Get appropriate timeout based on drive type"""
|
|
526
|
+
if tool == 'nvme':
|
|
527
|
+
return 30 # 30 seconds for NVMe
|
|
528
|
+
elif tool == 'hdparm':
|
|
529
|
+
# Try to get drive size for better timeout
|
|
530
|
+
try:
|
|
531
|
+
size_gb = self._get_drive_size_gb(device)
|
|
532
|
+
# 2 hours per TB, minimum 30 minutes
|
|
533
|
+
hours = max(0.5, (size_gb / 1024) * 2)
|
|
534
|
+
return int(hours * 3600)
|
|
535
|
+
except:
|
|
536
|
+
return 7200 # 2 hours default
|
|
537
|
+
return 3600 # 1 hour default
|
|
538
|
+
|
|
539
|
+
def _get_drive_size_gb(self, device: str) -> float:
|
|
540
|
+
"""Get drive size in GB"""
|
|
541
|
+
try:
|
|
542
|
+
# Use blockdev to get size
|
|
543
|
+
result = subprocess.run(
|
|
544
|
+
['blockdev', '--getsize64', device],
|
|
545
|
+
capture_output=True,
|
|
546
|
+
text=True,
|
|
547
|
+
timeout=5
|
|
548
|
+
)
|
|
549
|
+
if result.returncode == 0:
|
|
550
|
+
size_bytes = int(result.stdout.strip())
|
|
551
|
+
return size_bytes / (1024**3) # Convert to GB
|
|
552
|
+
except:
|
|
553
|
+
pass
|
|
554
|
+
return 500 # Default guess
|
|
555
|
+
|
|
556
|
+
def apply_marker(self):
|
|
557
|
+
# 1. Force the OS to realize the partitions are gone
|
|
558
|
+
subprocess.run(['blockdev', '--rereadpt', self.device_path])
|
|
559
|
+
time.sleep(1) # Give the kernel a breath
|
|
560
|
+
|
|
561
|
+
try:
|
|
562
|
+
with open(self.device_path, 'wb') as f:
|
|
563
|
+
# Clear first 16K
|
|
564
|
+
f.write(b'\x00' * 16384)
|
|
565
|
+
# Seek to 15K
|
|
566
|
+
f.seek(15360)
|
|
567
|
+
f.write(self.generate_json_marker())
|
|
568
|
+
f.flush()
|
|
569
|
+
os.fsync(f.fileno())
|
|
570
|
+
except OSError as e:
|
|
571
|
+
# If this happens, the drive is likely still 'settling' its FTL
|
|
572
|
+
return "RETRY_NEEDED"
|
|
573
|
+
|
|
574
|
+
# ============================================================================
|
|
575
|
+
# Part 5: Example Usage & Integration Helper
|
|
576
|
+
# ============================================================================
|
|
577
|
+
|
|
578
|
+
def example_software_wipe(device: str) -> bool:
|
|
579
|
+
"""Example fallback function for software wipe"""
|
|
580
|
+
print(f"[Software] Would wipe {device} with dd/scrub/etc.")
|
|
581
|
+
# Implement your existing software wipe here
|
|
582
|
+
return True
|
|
583
|
+
|
|
584
|
+
def main():
|
|
585
|
+
"""Example standalone usage"""
|
|
586
|
+
import argparse
|
|
587
|
+
|
|
588
|
+
parser = argparse.ArgumentParser(description='Hardware Secure Erase Test')
|
|
589
|
+
parser.add_argument('device', help='Device to wipe (e.g., /dev/sda)')
|
|
590
|
+
parser.add_argument('--auto-install', action='store_true',
|
|
591
|
+
help='Automatically install missing tools')
|
|
592
|
+
parser.add_argument('--verbose', '-v', action='store_true',
|
|
593
|
+
help='Verbose output')
|
|
594
|
+
parser.add_argument('--no-fallback', action='store_true',
|
|
595
|
+
help='Don\'t fall back to software wipe')
|
|
596
|
+
args = parser.parse_args()
|
|
597
|
+
|
|
598
|
+
# Create controller
|
|
599
|
+
controller = HardwareWipeController(
|
|
600
|
+
auto_install_tools=args.auto_install,
|
|
601
|
+
verbose=args.verbose
|
|
602
|
+
)
|
|
603
|
+
|
|
604
|
+
# Define fallback
|
|
605
|
+
fallback = None if args.no_fallback else example_software_wipe
|
|
606
|
+
|
|
607
|
+
# Execute wipe
|
|
608
|
+
success = controller.wipe(args.device, fallback_callback=fallback)
|
|
609
|
+
|
|
610
|
+
if success:
|
|
611
|
+
print(f"\n✓ Wipe completed successfully")
|
|
612
|
+
return 0
|
|
613
|
+
else:
|
|
614
|
+
print(f"\n✗ Wipe failed")
|
|
615
|
+
return 1
|
|
616
|
+
|
|
617
|
+
if __name__ == '__main__':
|
|
618
|
+
sys.exit(main())
|