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/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())