dwipe 1.0.1__tar.gz → 1.0.2__tar.gz
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-1.0.1/src/dwipe.egg-info → dwipe-1.0.2}/PKG-INFO +3 -1
- {dwipe-1.0.1 → dwipe-1.0.2}/README.md +2 -0
- {dwipe-1.0.1 → dwipe-1.0.2}/pyproject.toml +1 -1
- {dwipe-1.0.1 → dwipe-1.0.2}/src/dwipe/main.py +125 -22
- {dwipe-1.0.1 → dwipe-1.0.2/src/dwipe.egg-info}/PKG-INFO +3 -1
- {dwipe-1.0.1 → dwipe-1.0.2}/LICENSE +0 -0
- {dwipe-1.0.1 → dwipe-1.0.2}/setup.cfg +0 -0
- {dwipe-1.0.1 → dwipe-1.0.2}/src/dwipe/PowerWindow.py +0 -0
- {dwipe-1.0.1 → dwipe-1.0.2}/src/dwipe/__init__.py +0 -0
- {dwipe-1.0.1 → dwipe-1.0.2}/src/dwipe.egg-info/SOURCES.txt +0 -0
- {dwipe-1.0.1 → dwipe-1.0.2}/src/dwipe.egg-info/dependency_links.txt +0 -0
- {dwipe-1.0.1 → dwipe-1.0.2}/src/dwipe.egg-info/entry_points.txt +0 -0
- {dwipe-1.0.1 → dwipe-1.0.2}/src/dwipe.egg-info/requires.txt +0 -0
- {dwipe-1.0.1 → dwipe-1.0.2}/src/dwipe.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: dwipe
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.2
|
|
4
4
|
Summary: A tool to wipe disks and partitions for Linux
|
|
5
5
|
Author-email: Joe Defen <joedef@google.com>
|
|
6
6
|
License: MIT
|
|
@@ -63,6 +63,8 @@ The top line shows the "Mode" which is Random or Zeros. For some disks, zeroing
|
|
|
63
63
|
|
|
64
64
|
The write rate and estimating remaining times are shown when wiping a device. Due to write queueing, the initial rates may be inflated, final rates are deflated, and the times are optimistic.
|
|
65
65
|
|
|
66
|
+
The 'W' (Wiped) and 's' (partly wiped) states are disk persistent. For those states, more information is provided about the wipe including when and percent complete.
|
|
67
|
+
|
|
66
68
|
|
|
67
69
|
### The Help Screen
|
|
68
70
|
When **?** is typed, the help screen looks like:
|
|
@@ -45,6 +45,8 @@ The top line shows the "Mode" which is Random or Zeros. For some disks, zeroing
|
|
|
45
45
|
|
|
46
46
|
The write rate and estimating remaining times are shown when wiping a device. Due to write queueing, the initial rates may be inflated, final rates are deflated, and the times are optimistic.
|
|
47
47
|
|
|
48
|
+
The 'W' (Wiped) and 's' (partly wiped) states are disk persistent. For those states, more information is provided about the wipe including when and percent complete.
|
|
49
|
+
|
|
48
50
|
|
|
49
51
|
### The Help Screen
|
|
50
52
|
When **?** is typed, the help screen looks like:
|
|
@@ -15,13 +15,13 @@ import re
|
|
|
15
15
|
import json
|
|
16
16
|
import subprocess
|
|
17
17
|
import time
|
|
18
|
+
import datetime
|
|
18
19
|
import threading
|
|
19
20
|
import random
|
|
20
21
|
import shutil
|
|
21
22
|
import traceback
|
|
22
23
|
import curses as cs
|
|
23
24
|
from types import SimpleNamespace
|
|
24
|
-
from typing import Tuple, List
|
|
25
25
|
from dwipe.PowerWindow import Window, OptionSpinner
|
|
26
26
|
|
|
27
27
|
def human(number):
|
|
@@ -30,9 +30,9 @@ def human(number):
|
|
|
30
30
|
number = float(number)
|
|
31
31
|
while suffixes:
|
|
32
32
|
suffix = suffixes.pop(0)
|
|
33
|
-
number /=
|
|
34
|
-
if number <
|
|
35
|
-
return f'{number:.1f}{suffix}'
|
|
33
|
+
number /= 1000 # decimal
|
|
34
|
+
if number < 999.95 or not suffixes:
|
|
35
|
+
return f'{number:.1f}{suffix}B' # decimal
|
|
36
36
|
return None
|
|
37
37
|
##############################################################################
|
|
38
38
|
def ago_str(delta_secs, signed=False):
|
|
@@ -62,6 +62,7 @@ class WipeJob:
|
|
|
62
62
|
# Generate a 1MB buffer of random data
|
|
63
63
|
BUFFER_SIZE = 1 * 1024 * 1024 # 1MB
|
|
64
64
|
WRITE_SIZE = 16 * 1024 # 16KB
|
|
65
|
+
STATE_OFFSET = 15 * 1024 # where json is written
|
|
65
66
|
buffer = bytearray(os.urandom(BUFFER_SIZE))
|
|
66
67
|
zero_buffer = bytes(WRITE_SIZE)
|
|
67
68
|
|
|
@@ -123,6 +124,62 @@ class WipeJob:
|
|
|
123
124
|
|
|
124
125
|
return ago_str(int(round(elapsed_time))), pct_str, rate_str, when_str
|
|
125
126
|
|
|
127
|
+
def prep_marker_buffer(self, is_random):
|
|
128
|
+
""" Get the 1st 16KB to write:
|
|
129
|
+
- 15K zeros
|
|
130
|
+
- JSON status + zero fill to 1KB
|
|
131
|
+
"""
|
|
132
|
+
data = { "unixtime": int(time.time()),
|
|
133
|
+
"scrubbed_bytes": self.total_written,
|
|
134
|
+
"size_bytes": self.total_size,
|
|
135
|
+
"mode": 'Rand' if is_random else 'Zero'
|
|
136
|
+
}
|
|
137
|
+
json_data = json.dumps(data).encode('utf-8')
|
|
138
|
+
buffer = bytearray(self.BUFFER_SIZE)
|
|
139
|
+
buffer[:self.STATE_OFFSET] = b'\x00' * self.STATE_OFFSET
|
|
140
|
+
buffer[self.STATE_OFFSET:self.STATE_OFFSET+len(json_data)] = json_data
|
|
141
|
+
remaining_size = self.BUFFER_SIZE - (self.STATE_OFFSET+len(json_data))
|
|
142
|
+
buffer[self.STATE_OFFSET+len(json_data):] = b'\x00' * remaining_size
|
|
143
|
+
return buffer
|
|
144
|
+
|
|
145
|
+
@staticmethod
|
|
146
|
+
def read_marker_buffer(device_name):
|
|
147
|
+
""" Open the device and read the first 16 KB """
|
|
148
|
+
try:
|
|
149
|
+
with open(f'/dev/{device_name}', 'rb') as device:
|
|
150
|
+
device.seek(0)
|
|
151
|
+
buffer = device.read(WipeJob.BUFFER_SIZE)
|
|
152
|
+
except Exception:
|
|
153
|
+
return None # cannot find info
|
|
154
|
+
|
|
155
|
+
if buffer[:WipeJob.STATE_OFFSET] != b'\x00' * (WipeJob.STATE_OFFSET):
|
|
156
|
+
return None # First 15 KB are not zeros
|
|
157
|
+
|
|
158
|
+
# Extract JSON data from the next 1 KB Strip trailing zeros
|
|
159
|
+
json_data_bytes = buffer[WipeJob.STATE_OFFSET:WipeJob.BUFFER_SIZE].rstrip(b'\x00')
|
|
160
|
+
|
|
161
|
+
if not json_data_bytes:
|
|
162
|
+
return None # No JSON data found
|
|
163
|
+
|
|
164
|
+
# Deserialize the JSON data
|
|
165
|
+
try:
|
|
166
|
+
data = json.loads(json_data_bytes.decode('utf-8'))
|
|
167
|
+
except (json.JSONDecodeError, Exception):
|
|
168
|
+
return None # Invalid JSON data!
|
|
169
|
+
|
|
170
|
+
rv = {}
|
|
171
|
+
for key, value in data.items():
|
|
172
|
+
if key in ('unixtime', 'scrubbed_bytes', 'size_bytes') and isinstance(value, int):
|
|
173
|
+
rv[key] = value
|
|
174
|
+
elif key in ('mode', ) and isinstance(value, str):
|
|
175
|
+
rv[key] = value
|
|
176
|
+
else:
|
|
177
|
+
return None # bogus data
|
|
178
|
+
if len(rv) != 4:
|
|
179
|
+
return None # bogus data
|
|
180
|
+
return SimpleNamespace(**rv)
|
|
181
|
+
|
|
182
|
+
|
|
126
183
|
def write_partition(self):
|
|
127
184
|
"""Writes random chunks to a device and updates the progress status."""
|
|
128
185
|
self.total_written = 0 # Track total bytes written
|
|
@@ -155,11 +212,11 @@ class WipeJob:
|
|
|
155
212
|
if self.opts.dry_run and self.total_written >= self.total_size:
|
|
156
213
|
break
|
|
157
214
|
# clear the beginning of device whether aborted or not
|
|
158
|
-
# if we have started writing
|
|
215
|
+
# if we have started writing + status in JSON
|
|
159
216
|
if not self.opts.dry_run and self.total_written > 0 and is_random:
|
|
160
217
|
device.seek(0)
|
|
161
|
-
chunk = memoryview(WipeJob.zero_buffer)
|
|
162
|
-
bytes_written = device.write(
|
|
218
|
+
# chunk = memoryview(WipeJob.zero_buffer)
|
|
219
|
+
bytes_written = device.write(self.prep_marker_buffer(is_random))
|
|
163
220
|
self.done = True
|
|
164
221
|
|
|
165
222
|
class DeviceInfo:
|
|
@@ -183,13 +240,37 @@ class DeviceInfo:
|
|
|
183
240
|
dflt=dflt, # default run-time state
|
|
184
241
|
label='', # blkid
|
|
185
242
|
fstype='', # blkid
|
|
243
|
+
model='', # /sys/class/block/{name}/device/vendor|model
|
|
186
244
|
# fsuse='-',
|
|
187
245
|
size_bytes=size_bytes, # /sys/block/{name}/...
|
|
246
|
+
marker='', # persistent status
|
|
188
247
|
mounts=[], # /proc/mounts
|
|
189
248
|
minors=[],
|
|
190
249
|
job=None, # if zap running
|
|
191
250
|
)
|
|
192
251
|
|
|
252
|
+
|
|
253
|
+
@staticmethod
|
|
254
|
+
def get_device_vendor_model(device_name):
|
|
255
|
+
""" Gets the vendor and model for a given device from the /sys/class/block directory.
|
|
256
|
+
- Args: - device_name: The device name, such as 'sda', 'sdb', etc.
|
|
257
|
+
- - Returns: A string containing the vendor and model information.
|
|
258
|
+
"""
|
|
259
|
+
def get_str(device_name, suffix):
|
|
260
|
+
try:
|
|
261
|
+
rv = ''
|
|
262
|
+
fullpath = f'/sys/class/block/{device_name}/device/{suffix}'
|
|
263
|
+
with open(fullpath, 'r', encoding='utf-8') as f: # Read information
|
|
264
|
+
rv = f.read().strip()
|
|
265
|
+
except (FileNotFoundError, Exception):
|
|
266
|
+
# print(f"Error reading {info} for {device_name} : {e}")
|
|
267
|
+
pass
|
|
268
|
+
return rv
|
|
269
|
+
|
|
270
|
+
# rv = f'{get_str(device_name, "vendor")}' #vendor seems useless/confusing
|
|
271
|
+
rv = f'{get_str(device_name, "model")}'
|
|
272
|
+
return rv.strip()
|
|
273
|
+
|
|
193
274
|
def parse_lsblk(self, dflt):
|
|
194
275
|
""" Parse ls_blk for all the goodies we need """
|
|
195
276
|
def eat_one(device):
|
|
@@ -216,6 +297,17 @@ class DeviceInfo:
|
|
|
216
297
|
while len(mounts) >= 1 and mounts[0] is None:
|
|
217
298
|
del mounts[0]
|
|
218
299
|
entry.mounts = mounts
|
|
300
|
+
if not mounts:
|
|
301
|
+
marker = WipeJob.read_marker_buffer(entry.name)
|
|
302
|
+
now = int(round(time.time()))
|
|
303
|
+
if (marker and marker.size_bytes == entry.size_bytes
|
|
304
|
+
and 0 <= marker.scrubbed_bytes <= entry.size_bytes
|
|
305
|
+
and marker.unixtime < now):
|
|
306
|
+
pct = int(round((marker.scrubbed_bytes/marker.size_bytes)*100))
|
|
307
|
+
state = 'W' if pct >= 100 else 's'
|
|
308
|
+
dt = datetime.datetime.fromtimestamp(marker.unixtime)
|
|
309
|
+
entry.marker = f'{state} {pct}% {marker.mode} {dt.strftime('%Y/%m/%d %H:%M')}'
|
|
310
|
+
|
|
219
311
|
return entry
|
|
220
312
|
|
|
221
313
|
# Run the `lsblk` command and get its output in JSON format with additional columns
|
|
@@ -228,6 +320,7 @@ class DeviceInfo:
|
|
|
228
320
|
# Parse each block device and its properties
|
|
229
321
|
for device in parsed_data['blockdevices']:
|
|
230
322
|
parent = eat_one(device)
|
|
323
|
+
parent.fstype = self.get_device_vendor_model(parent.name)
|
|
231
324
|
entries[parent.name] = parent
|
|
232
325
|
for child in device.get('children', []):
|
|
233
326
|
entry = eat_one(child)
|
|
@@ -240,6 +333,8 @@ class DeviceInfo:
|
|
|
240
333
|
if entry.mounts:
|
|
241
334
|
entry.state = 'Mnt'
|
|
242
335
|
parent.state = 'Mnt'
|
|
336
|
+
elif entry.marker:
|
|
337
|
+
entry.state = entry.marker[0]
|
|
243
338
|
|
|
244
339
|
if self.DB:
|
|
245
340
|
print('\n\nDB: --->>> after parse_lsblk:')
|
|
@@ -359,7 +454,7 @@ class DeviceInfo:
|
|
|
359
454
|
def compute_field_widths(self, nss):
|
|
360
455
|
""" TBD """
|
|
361
456
|
|
|
362
|
-
wids = self.wids = SimpleNamespace(state=5, name=4, human=
|
|
457
|
+
wids = self.wids = SimpleNamespace(state=5, name=4, human=7, fstype=4, label=5)
|
|
363
458
|
for ns in nss.values():
|
|
364
459
|
wids.state = max(wids.state, len(ns.state))
|
|
365
460
|
# wids.fsuse = max(wids.fsuse, len(ns.fsuse))
|
|
@@ -383,18 +478,23 @@ class DeviceInfo:
|
|
|
383
478
|
emit += f'{sep}{"SIZE":_^{wids.human}}'
|
|
384
479
|
emit += f'{sep}{"TYPE":_^{wids.fstype}}'
|
|
385
480
|
emit += f'{sep}{"LABEL":_^{wids.label}}'
|
|
386
|
-
emit += f'{sep}MOUNTS'
|
|
481
|
+
emit += f'{sep}MOUNTS/STATUS'
|
|
387
482
|
return emit
|
|
388
483
|
|
|
389
484
|
def part_str(self, partition):
|
|
390
485
|
""" Convert partition to human value. """
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
486
|
+
# def print_str_or_dashes(name, width, chrs=' -'):
|
|
487
|
+
# if not name.strip(): # Create a string of '─' characters of the specified width
|
|
488
|
+
# result = f'{chrs}' * (width//2)
|
|
489
|
+
# result += ' ' * (width%2)
|
|
490
|
+
# else: # Format the name to be right-aligned within the specified width
|
|
491
|
+
# result = f'{name:>{width}}'
|
|
492
|
+
# return result
|
|
493
|
+
|
|
494
|
+
def print_str_or_dash(name, width, empty='-'):
|
|
495
|
+
if not name.strip(): # return
|
|
496
|
+
name = empty
|
|
497
|
+
return f'{name:^{width}}'
|
|
398
498
|
|
|
399
499
|
sep = ' '
|
|
400
500
|
ns = partition # shorthand
|
|
@@ -409,14 +509,17 @@ class DeviceInfo:
|
|
|
409
509
|
emit += f'{sep}{name_str:<{wids.name}}'
|
|
410
510
|
# emit += f'{sep}{ns.fsuse:^{wids.fsuse}}'
|
|
411
511
|
emit += f'{sep}{human(ns.size_bytes):>{wids.human}}'
|
|
412
|
-
emit += sep +
|
|
512
|
+
emit += sep + print_str_or_dash(ns.fstype, wids.fstype)
|
|
413
513
|
# emit += f'{sep}{ns.fstype:>{wids.fstype}}'
|
|
414
514
|
if ns.parent is None:
|
|
415
|
-
emit += sep + '■' + '─'*(wids.label-2) + '■'
|
|
515
|
+
emit += sep + '■' + '─'*(wids.label-2) + '■'
|
|
416
516
|
else:
|
|
417
|
-
emit += sep +
|
|
517
|
+
emit += sep + print_str_or_dash(ns.label, wids.label)
|
|
418
518
|
# emit += f' {ns.label:>{wids.label}}'
|
|
419
|
-
|
|
519
|
+
if ns.mounts:
|
|
520
|
+
emit += f'{sep}{",".join(ns.mounts)}'
|
|
521
|
+
else:
|
|
522
|
+
emit += f'{sep}{ns.marker}'
|
|
420
523
|
return emit
|
|
421
524
|
|
|
422
525
|
def merge_dev_infos(self, nss, prev_nss=None):
|
|
@@ -605,7 +708,7 @@ class DiskWipe:
|
|
|
605
708
|
for part in self.partitions.values():
|
|
606
709
|
stop_if_idle(part)
|
|
607
710
|
return None
|
|
608
|
-
|
|
711
|
+
|
|
609
712
|
if key == ord('l'):
|
|
610
713
|
part = self.partitions[self.pick_name]
|
|
611
714
|
self.set_state(part, 'Unlk' if part.state == 'Lock' else 'Lock')
|
|
@@ -739,7 +842,7 @@ class DiskWipe:
|
|
|
739
842
|
|
|
740
843
|
if partition.parent and self.partitions[partition.parent].state == 'Lock':
|
|
741
844
|
continue
|
|
742
|
-
|
|
845
|
+
|
|
743
846
|
if wanted(name) or partition.job:
|
|
744
847
|
partition.line = self.dev_info.part_str(partition)
|
|
745
848
|
self.win.add_body(partition.line)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: dwipe
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.2
|
|
4
4
|
Summary: A tool to wipe disks and partitions for Linux
|
|
5
5
|
Author-email: Joe Defen <joedef@google.com>
|
|
6
6
|
License: MIT
|
|
@@ -63,6 +63,8 @@ The top line shows the "Mode" which is Random or Zeros. For some disks, zeroing
|
|
|
63
63
|
|
|
64
64
|
The write rate and estimating remaining times are shown when wiping a device. Due to write queueing, the initial rates may be inflated, final rates are deflated, and the times are optimistic.
|
|
65
65
|
|
|
66
|
+
The 'W' (Wiped) and 's' (partly wiped) states are disk persistent. For those states, more information is provided about the wipe including when and percent complete.
|
|
67
|
+
|
|
66
68
|
|
|
67
69
|
### The Help Screen
|
|
68
70
|
When **?** is typed, the help screen looks like:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|