dwipe 2.0.0__py3-none-any.whl → 2.0.1__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 +113 -12
- dwipe/DiskWipe.py +10 -0
- dwipe/PersistentState.py +2 -10
- dwipe/ToolManager.py +637 -0
- {dwipe-2.0.0.dist-info → dwipe-2.0.1.dist-info}/METADATA +5 -2
- dwipe-2.0.1.dist-info/RECORD +14 -0
- dwipe-2.0.0.dist-info/RECORD +0 -13
- {dwipe-2.0.0.dist-info → dwipe-2.0.1.dist-info}/WHEEL +0 -0
- {dwipe-2.0.0.dist-info → dwipe-2.0.1.dist-info}/entry_points.txt +0 -0
- {dwipe-2.0.0.dist-info → dwipe-2.0.1.dist-info}/licenses/LICENSE +0 -0
dwipe/DeviceInfo.py
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"""
|
|
2
2
|
DeviceInfo class for device discovery and information management
|
|
3
3
|
"""
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
4
6
|
import json
|
|
5
7
|
import subprocess
|
|
6
8
|
import time
|
|
@@ -36,6 +38,7 @@ class DeviceInfo:
|
|
|
36
38
|
dflt=dflt, # default run-time state
|
|
37
39
|
label='', # blkid
|
|
38
40
|
fstype='', # blkid
|
|
41
|
+
type='', # device type (disk, part)
|
|
39
42
|
model='', # /sys/class/block/{name}/device/vendor|model
|
|
40
43
|
size_bytes=size_bytes, # /sys/block/{name}/...
|
|
41
44
|
marker='', # persistent status
|
|
@@ -45,10 +48,59 @@ class DeviceInfo:
|
|
|
45
48
|
job=None, # if zap running
|
|
46
49
|
uuid='', # filesystem UUID or PARTUUID
|
|
47
50
|
serial='', # disk serial number (for whole disks)
|
|
51
|
+
port='', # port (for whole disks)
|
|
48
52
|
)
|
|
49
53
|
|
|
54
|
+
def _get_port_from_sysfs(self, device_name):
|
|
55
|
+
try:
|
|
56
|
+
sysfs_path = f'/sys/class/block/{device_name}'
|
|
57
|
+
if not os.path.exists(sysfs_path):
|
|
58
|
+
return ''
|
|
59
|
+
|
|
60
|
+
real_path = os.path.realpath(sysfs_path).lower()
|
|
61
|
+
|
|
62
|
+
# 1. USB - Format: USB:1-1.4
|
|
63
|
+
if '/usb' in real_path:
|
|
64
|
+
usb_match = re.search(r'/(\d+-\d+(?:\.\d+)*):', real_path)
|
|
65
|
+
if usb_match:
|
|
66
|
+
return f"USB:{usb_match.group(1)}"
|
|
67
|
+
|
|
68
|
+
# 2. SATA - Format: SATA:1
|
|
69
|
+
elif '/ata' in real_path:
|
|
70
|
+
ata_match = re.search(r'ata(\d+)', real_path)
|
|
71
|
+
if ata_match:
|
|
72
|
+
return f"SATA:{ata_match.group(1)}"
|
|
73
|
+
|
|
74
|
+
# 3. NVMe - Format: PCI:1b.0 (Stripped of 0000:00: noise)
|
|
75
|
+
elif '/nvme' in real_path:
|
|
76
|
+
# This regex ignores the 4-digit domain and the first 2-digit bus
|
|
77
|
+
pci_match = re.search(r'0000:[0-9a-f]{2}:([0-9a-f]{2}\.[0-9a-f])', real_path)
|
|
78
|
+
if pci_match:
|
|
79
|
+
return f"PCI:{pci_match.group(1)}"
|
|
80
|
+
return "NVMe"
|
|
81
|
+
|
|
82
|
+
# 4. MMC/eMMC - Format: MMC:0 or PCI:1a.0 (if PCI-attached)
|
|
83
|
+
elif '/mmc' in real_path:
|
|
84
|
+
# Try to extract mmc host number
|
|
85
|
+
mmc_match = re.search(r'/mmc_host/mmc(\d+)', real_path)
|
|
86
|
+
if mmc_match:
|
|
87
|
+
return f"MMC:{mmc_match.group(1)}"
|
|
88
|
+
# Fallback: try to get PCI address if available
|
|
89
|
+
pci_match = re.search(r'0000:[0-9a-f]{2}:([0-9a-f]{2}\.[0-9a-f])', real_path)
|
|
90
|
+
if pci_match:
|
|
91
|
+
return f"PCI:{pci_match.group(1)}"
|
|
92
|
+
return "MMC"
|
|
93
|
+
|
|
94
|
+
except Exception as e:
|
|
95
|
+
# Log exception to file for debugging
|
|
96
|
+
with open('/tmp/dwipe_port_debug.log', 'a', encoding='utf-8') as f:
|
|
97
|
+
import traceback
|
|
98
|
+
f.write(f"Exception in _get_port_from_sysfs({device_name}): {e}\n")
|
|
99
|
+
traceback.print_exc(file=f)
|
|
100
|
+
return ''
|
|
101
|
+
|
|
50
102
|
@staticmethod
|
|
51
|
-
def
|
|
103
|
+
def _get_device_vendor_model(device_name):
|
|
52
104
|
"""Gets the vendor and model for a given device from the /sys/class/block directory.
|
|
53
105
|
- Args: - device_name: The device name, such as 'sda', 'sdb', etc.
|
|
54
106
|
- Returns: A string containing the vendor and model information.
|
|
@@ -156,20 +208,51 @@ class DeviceInfo:
|
|
|
156
208
|
# Parse each block device and its properties
|
|
157
209
|
for device in parsed_data['blockdevices']:
|
|
158
210
|
parent = eat_one(device)
|
|
159
|
-
parent.fstype = self.get_device_vendor_model(parent.name)
|
|
160
211
|
entries[parent.name] = parent
|
|
161
212
|
for child in device.get('children', []):
|
|
162
213
|
entry = eat_one(child)
|
|
163
214
|
entries[entry.name] = entry
|
|
164
215
|
entry.parent = parent.name
|
|
165
216
|
parent.minors.append(entry.name)
|
|
166
|
-
if not parent.fstype:
|
|
167
|
-
parent.fstype = 'DISK'
|
|
168
217
|
self.disk_majors.add(entry.major)
|
|
169
218
|
if entry.mounts:
|
|
170
219
|
entry.state = 'Mnt'
|
|
171
220
|
parent.state = 'Mnt'
|
|
172
221
|
|
|
222
|
+
|
|
223
|
+
# Final pass: Identify disks, assign ports, and handle superfloppies
|
|
224
|
+
final_entries = {}
|
|
225
|
+
for name, entry in entries.items():
|
|
226
|
+
final_entries[name] = entry
|
|
227
|
+
|
|
228
|
+
# Only process top-level physical disks
|
|
229
|
+
if entry.parent is None:
|
|
230
|
+
# Hardware Info Gathering
|
|
231
|
+
entry.model = self._get_device_vendor_model(entry.name)
|
|
232
|
+
entry.port = self._get_port_from_sysfs(entry.name)
|
|
233
|
+
|
|
234
|
+
# The Split (Superfloppy Case)
|
|
235
|
+
# If it has children, the children already hold the data.
|
|
236
|
+
# If it has NO children but HAS data, we create the '----' child.
|
|
237
|
+
if not entry.minors and (entry.fstype or entry.label or entry.mounts):
|
|
238
|
+
v_key = f"{name}_data"
|
|
239
|
+
v_child = self._make_partition_namespace(entry.major, name, entry.size_bytes, dflt)
|
|
240
|
+
v_child.name = "----"
|
|
241
|
+
v_child.fstype = entry.fstype
|
|
242
|
+
v_child.label = entry.label
|
|
243
|
+
v_child.mounts = entry.mounts
|
|
244
|
+
v_child.parent = name
|
|
245
|
+
|
|
246
|
+
final_entries[v_key] = v_child
|
|
247
|
+
entry.minors.append(v_key)
|
|
248
|
+
|
|
249
|
+
# Clean the hardware row of data-specific strings
|
|
250
|
+
entry.fstype = entry.model if entry.model else 'DISK'
|
|
251
|
+
entry.label = ''
|
|
252
|
+
entry.mounts = []
|
|
253
|
+
|
|
254
|
+
entries = final_entries
|
|
255
|
+
|
|
173
256
|
if self.DB:
|
|
174
257
|
print('\n\nDB: --->>> after parse_lsblk:')
|
|
175
258
|
for entry in entries.values():
|
|
@@ -237,21 +320,38 @@ class DeviceInfo:
|
|
|
237
320
|
DeviceInfo.set_one_state(nss, ns)
|
|
238
321
|
|
|
239
322
|
def get_disk_partitions(self, nss):
|
|
240
|
-
"""Filter
|
|
241
|
-
Keeps: sd*, nvme*, vd*, hd*, mmcblk* (physical and USB drives)
|
|
242
|
-
Excludes: zram*, loop*, dm-*, sr*, ram*
|
|
243
|
-
"""
|
|
244
|
-
# Virtual/pseudo device prefixes to exclude
|
|
245
|
-
exclude_prefixes = ('zram', 'loop', 'dm-', 'sr', 'ram')
|
|
323
|
+
"""Filter to only wipeable physical storage using positive criteria.
|
|
246
324
|
|
|
325
|
+
Keeps devices that:
|
|
326
|
+
- Are type 'disk' or 'part' (from lsblk)
|
|
327
|
+
- Are writable (not read-only)
|
|
328
|
+
- Are real block devices (not virtual)
|
|
329
|
+
|
|
330
|
+
This automatically excludes:
|
|
331
|
+
- Virtual devices (zram, loop, dm-*, etc.)
|
|
332
|
+
- Read-only devices (CD-ROMs, eMMC boot partitions)
|
|
333
|
+
- Special partitions (boot loaders)
|
|
334
|
+
"""
|
|
247
335
|
ok_nss = {}
|
|
248
336
|
for name, ns in nss.items():
|
|
249
337
|
# Must be disk or partition type
|
|
250
338
|
if ns.type not in ('disk', 'part'):
|
|
251
339
|
continue
|
|
252
340
|
|
|
253
|
-
#
|
|
254
|
-
|
|
341
|
+
# Must be writable (excludes CD-ROMs, eMMC boot partitions, etc.)
|
|
342
|
+
ro_path = f'/sys/class/block/{name}/ro'
|
|
343
|
+
try:
|
|
344
|
+
with open(ro_path, 'r', encoding='utf-8') as f:
|
|
345
|
+
if f.read().strip() != '0':
|
|
346
|
+
continue # Skip read-only devices
|
|
347
|
+
except (FileNotFoundError, Exception):
|
|
348
|
+
# If we can't read ro flag, skip this device to be safe
|
|
349
|
+
continue
|
|
350
|
+
|
|
351
|
+
# Exclude common virtual device prefixes as a safety net
|
|
352
|
+
# (most should already be filtered by ro check or missing sysfs)
|
|
353
|
+
virtual_prefixes = ('zram', 'loop', 'dm-', 'ram')
|
|
354
|
+
if any(name.startswith(prefix) for prefix in virtual_prefixes):
|
|
255
355
|
continue
|
|
256
356
|
|
|
257
357
|
# Include this device
|
|
@@ -405,6 +505,7 @@ class DeviceInfo:
|
|
|
405
505
|
if new_ns:
|
|
406
506
|
if prev_ns.job:
|
|
407
507
|
new_ns.job = prev_ns.job
|
|
508
|
+
# Note: Do NOT preserve port - use fresh value from current scan
|
|
408
509
|
new_ns.dflt = prev_ns.dflt
|
|
409
510
|
# Preserve the "wiped this session" flag
|
|
410
511
|
if hasattr(prev_ns, 'wiped_this_session'):
|
dwipe/DiskWipe.py
CHANGED
|
@@ -187,6 +187,7 @@ class DiskWipe:
|
|
|
187
187
|
line += f' [P]ass={self.opts.passes}'
|
|
188
188
|
# Show verification percentage spinner with key
|
|
189
189
|
line += f' [V]pct={self.opts.verify_pct}%'
|
|
190
|
+
line += f' [p]ort'
|
|
190
191
|
line += ' '
|
|
191
192
|
if self.opts.dry_run:
|
|
192
193
|
line += ' DRY-RUN'
|
|
@@ -295,6 +296,7 @@ class DiskWipe:
|
|
|
295
296
|
spin = self.spin = OptionSpinner(stack=self.stack)
|
|
296
297
|
spin.default_obj = self.opts
|
|
297
298
|
spin.add_key('dense', 'D - dense/spaced view', vals=[False, True])
|
|
299
|
+
spin.add_key('port_serial', 'p - disk port info', vals=[False, True])
|
|
298
300
|
spin.add_key('slowdown_stop', 'L - stop if disk slows Nx', vals=[16, 64, 256, 0, 4])
|
|
299
301
|
spin.add_key('stall_timeout', 'T - stall timeout (sec)', vals=[60, 120, 300, 600, 0,])
|
|
300
302
|
spin.add_key('verify_pct', 'V - verification %', vals=[0, 2, 5, 10, 25, 50, 100])
|
|
@@ -372,6 +374,11 @@ class DiskWipeScreen(Screen):
|
|
|
372
374
|
class MainScreen(DiskWipeScreen):
|
|
373
375
|
"""Main device list screen"""
|
|
374
376
|
|
|
377
|
+
def _port_serial_line(self, port, serial):
|
|
378
|
+
wids = self.app.wids
|
|
379
|
+
wid = wids.state if wids else 5
|
|
380
|
+
sep = ' '
|
|
381
|
+
return f'{"":>{wid}}{sep}│ └────── {port:<12} {serial}'
|
|
375
382
|
|
|
376
383
|
def draw_screen(self):
|
|
377
384
|
"""Draw the main device list"""
|
|
@@ -612,6 +619,9 @@ class MainScreen(DiskWipeScreen):
|
|
|
612
619
|
ctx = Context(genre='disk' if partition.parent is None else 'partition',
|
|
613
620
|
partition=partition)
|
|
614
621
|
app.win.add_body(partition.line, attr=attr, context=ctx)
|
|
622
|
+
if partition.parent is None and app.opts.port_serial:
|
|
623
|
+
line = self._port_serial_line(partition.port, partition.serial)
|
|
624
|
+
app.win.add_body(line, attr=attr, context=Context(genre='DECOR'))
|
|
615
625
|
|
|
616
626
|
# Show inline confirmation prompt if this is the partition being confirmed
|
|
617
627
|
if app.confirmation.active and app.confirmation.partition_name == partition.name:
|
dwipe/PersistentState.py
CHANGED
|
@@ -25,12 +25,13 @@ class PersistentState:
|
|
|
25
25
|
self.state = {
|
|
26
26
|
'theme': 'default',
|
|
27
27
|
'wipe_mode': 'Zero', # 'Rand' or 'Zero' or +V
|
|
28
|
-
'passes': 1, # 1, 2, or 4 wipe
|
|
28
|
+
'passes': 1, # 1, 2, or 4 wipe pass
|
|
29
29
|
'confirmation': 'YES', # 'Y', 'y', 'YES', 'yes', 'device'
|
|
30
30
|
'verify_pct': 2, # 0, 2, 5, 10, 25, 50, 100
|
|
31
31
|
'dense': False, # True = compact view, False = blank lines between disks
|
|
32
32
|
'slowdown_stop': 16,
|
|
33
33
|
'stall_timeout': 60,
|
|
34
|
+
'port_serial': False,
|
|
34
35
|
'devices': {} # device_id -> {locked, last_seen, last_name, size_bytes}
|
|
35
36
|
}
|
|
36
37
|
self.dirty = False
|
|
@@ -61,15 +62,6 @@ class PersistentState:
|
|
|
61
62
|
try:
|
|
62
63
|
with open(self.config_path, 'r', encoding='utf-8') as f:
|
|
63
64
|
loaded = json.load(f)
|
|
64
|
-
# Migrate old mode values to new format
|
|
65
|
-
# if 'mode' in loaded:
|
|
66
|
-
# old_mode = loaded['mode']
|
|
67
|
-
# if old_mode in ('random', 'Random', 'RANDOM'):
|
|
68
|
-
# loaded['mode'] = 'Rand'
|
|
69
|
-
# self.dirty = True # Save migrated value
|
|
70
|
-
# elif old_mode in ('zero', 'zeros', 'Zero', 'ZERO'):
|
|
71
|
-
# loaded['mode'] = 'Zero'
|
|
72
|
-
# self.dirty = True # Save migrated value
|
|
73
65
|
self.state.update(loaded)
|
|
74
66
|
except (json.JSONDecodeError, IOError) as e:
|
|
75
67
|
print(f'Warning: Could not load state from {self.config_path}: {e}')
|
dwipe/ToolManager.py
ADDED
|
@@ -0,0 +1,637 @@
|
|
|
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())
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dwipe
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.1
|
|
4
4
|
Summary: A tool to wipe disks and partitions for Linux
|
|
5
5
|
Keywords: disk,partition,wipe,clean,scrub
|
|
6
6
|
Author-email: Joe Defen <joedef@google.com>
|
|
@@ -10,7 +10,7 @@ Classifier: Programming Language :: Python :: 3
|
|
|
10
10
|
Classifier: License :: OSI Approved :: MIT License
|
|
11
11
|
Classifier: Operating System :: POSIX :: Linux
|
|
12
12
|
License-File: LICENSE
|
|
13
|
-
Requires-Dist: console-window
|
|
13
|
+
Requires-Dist: console-window == 1.3.2
|
|
14
14
|
Project-URL: Bug Tracker, https://github.com/joedefen/dwipe/issues
|
|
15
15
|
Project-URL: Homepage, https://github.com/joedefen/dwipe
|
|
16
16
|
|
|
@@ -60,6 +60,9 @@ Project-URL: Homepage, https://github.com/joedefen/dwipe
|
|
|
60
60
|
* **Direct I/O to Disk** - Wiping is done with direct I/O which is fast and avoid polluting your page cache. Writer threads are given lower than normal I/O priority to play nice with other apps. This makes stopping jobs fast and certain.
|
|
61
61
|
* **Improved Handling of Bad Disks.** Now detects (sometimes corrects) write failures, slowdowns, excessive no progress, and reports/aborts hopeless or hopelessly slow wipes.
|
|
62
62
|
|
|
63
|
+
## **V2.x Features**
|
|
64
|
+
Features added since V2 deployed (may not be in latest demo):
|
|
65
|
+
* **Port and Serial number**. Press `p` to toggle whether port and serial number is show; it adds another line per disk and you may want to use it selectively.
|
|
63
66
|
## Requirements
|
|
64
67
|
- **Linux operating system** (uses `/dev/`, `/sys/`, `/proc/` interfaces)
|
|
65
68
|
- **Python 3.8 or higher**
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
dwipe/DeviceInfo.py,sha256=7ZgtLDT1z5QwMWVwEb9Q3-wonvuPyAWgl7Xl5o14XbY,25777
|
|
2
|
+
dwipe/DiskWipe.py,sha256=SVfcGDMZccGBocpS5U8JXI-Y2UzRix3jAzzaYPVRCDc,40161
|
|
3
|
+
dwipe/PersistentState.py,sha256=FZydQs_-VYiq-Gw0hvcWPeuWOU5VFgI-FvrUsbT92RQ,6339
|
|
4
|
+
dwipe/ToolManager.py,sha256=EpG_588Q76IzFfjrTM5LSnhTUC1CoBeCUdqtR0duliY,23371
|
|
5
|
+
dwipe/Utils.py,sha256=Cuq8Usamrq1DWUk8EtjTuD6lSLXYGY0x-pDcoLJBR8M,7714
|
|
6
|
+
dwipe/WipeJob.py,sha256=bjv_hVuH5DBDgY5-y3Mv11URhsBu1t1iu8lx76dmAhE,54486
|
|
7
|
+
dwipe/WipeJobFuture.py,sha256=urkrASHtqELsKQ5c7OMc_LxpgIiYAIOeb1ZixQYmp74,8746
|
|
8
|
+
dwipe/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
|
+
dwipe/main.py,sha256=Zyw9LFBGqWYxga1H9qTdQGHLyCsxZUg3tm_Ylj20FG4,1841
|
|
10
|
+
dwipe-2.0.1.dist-info/entry_points.txt,sha256=SZHFezmse2c-jxG-BJ0TXy_TZ8vVFf0lPJWs0cdxz6Y,41
|
|
11
|
+
dwipe-2.0.1.dist-info/licenses/LICENSE,sha256=qB9OdnyyF6WYHiEIXVm0rOSdcf8e2ctorrtWs6CC5lU,1062
|
|
12
|
+
dwipe-2.0.1.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
|
|
13
|
+
dwipe-2.0.1.dist-info/METADATA,sha256=kcykkG3kr9kyZ0T6nUN_dGCxbC3E2fh26HovbCnqGD4,21957
|
|
14
|
+
dwipe-2.0.1.dist-info/RECORD,,
|
dwipe-2.0.0.dist-info/RECORD
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
dwipe/DeviceInfo.py,sha256=MKoxZ5iDzKdJwIRWn2Sjpdl28tbqsW5_O3SRctpYXRE,21395
|
|
2
|
-
dwipe/DiskWipe.py,sha256=nmnK4gGxvzvKV5Z4i_Ef4eENyTt-Ld0G_mfk-0dPHzg,39611
|
|
3
|
-
dwipe/PersistentState.py,sha256=-2jcq5lOo6wFMUxCYR5Vxwgs4gk8KXasH-RVNSCymPY,6847
|
|
4
|
-
dwipe/Utils.py,sha256=Cuq8Usamrq1DWUk8EtjTuD6lSLXYGY0x-pDcoLJBR8M,7714
|
|
5
|
-
dwipe/WipeJob.py,sha256=bjv_hVuH5DBDgY5-y3Mv11URhsBu1t1iu8lx76dmAhE,54486
|
|
6
|
-
dwipe/WipeJobFuture.py,sha256=urkrASHtqELsKQ5c7OMc_LxpgIiYAIOeb1ZixQYmp74,8746
|
|
7
|
-
dwipe/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
-
dwipe/main.py,sha256=Zyw9LFBGqWYxga1H9qTdQGHLyCsxZUg3tm_Ylj20FG4,1841
|
|
9
|
-
dwipe-2.0.0.dist-info/entry_points.txt,sha256=SZHFezmse2c-jxG-BJ0TXy_TZ8vVFf0lPJWs0cdxz6Y,41
|
|
10
|
-
dwipe-2.0.0.dist-info/licenses/LICENSE,sha256=qB9OdnyyF6WYHiEIXVm0rOSdcf8e2ctorrtWs6CC5lU,1062
|
|
11
|
-
dwipe-2.0.0.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
|
|
12
|
-
dwipe-2.0.0.dist-info/METADATA,sha256=PIIIZRcbEsmmd2c3noJyGVACbQyqIgUZZiv3IW618ko,21713
|
|
13
|
-
dwipe-2.0.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|