dwipe 1.0.1__tar.gz → 1.0.3__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.3}/PKG-INFO +3 -1
- {dwipe-1.0.1 → dwipe-1.0.3}/README.md +2 -0
- {dwipe-1.0.1 → dwipe-1.0.3}/pyproject.toml +1 -1
- {dwipe-1.0.1 → dwipe-1.0.3}/src/dwipe/main.py +134 -29
- {dwipe-1.0.1 → dwipe-1.0.3/src/dwipe.egg-info}/PKG-INFO +3 -1
- {dwipe-1.0.1 → dwipe-1.0.3}/LICENSE +0 -0
- {dwipe-1.0.1 → dwipe-1.0.3}/setup.cfg +0 -0
- {dwipe-1.0.1 → dwipe-1.0.3}/src/dwipe/PowerWindow.py +0 -0
- {dwipe-1.0.1 → dwipe-1.0.3}/src/dwipe/__init__.py +0 -0
- {dwipe-1.0.1 → dwipe-1.0.3}/src/dwipe.egg-info/SOURCES.txt +0 -0
- {dwipe-1.0.1 → dwipe-1.0.3}/src/dwipe.egg-info/dependency_links.txt +0 -0
- {dwipe-1.0.1 → dwipe-1.0.3}/src/dwipe.egg-info/entry_points.txt +0 -0
- {dwipe-1.0.1 → dwipe-1.0.3}/src/dwipe.egg-info/requires.txt +0 -0
- {dwipe-1.0.1 → dwipe-1.0.3}/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.3
|
|
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,18 @@ 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
|
+
entry.state = state
|
|
311
|
+
|
|
219
312
|
return entry
|
|
220
313
|
|
|
221
314
|
# Run the `lsblk` command and get its output in JSON format with additional columns
|
|
@@ -228,6 +321,7 @@ class DeviceInfo:
|
|
|
228
321
|
# Parse each block device and its properties
|
|
229
322
|
for device in parsed_data['blockdevices']:
|
|
230
323
|
parent = eat_one(device)
|
|
324
|
+
parent.fstype = self.get_device_vendor_model(parent.name)
|
|
231
325
|
entries[parent.name] = parent
|
|
232
326
|
for child in device.get('children', []):
|
|
233
327
|
entry = eat_one(child)
|
|
@@ -359,7 +453,7 @@ class DeviceInfo:
|
|
|
359
453
|
def compute_field_widths(self, nss):
|
|
360
454
|
""" TBD """
|
|
361
455
|
|
|
362
|
-
wids = self.wids = SimpleNamespace(state=5, name=4, human=
|
|
456
|
+
wids = self.wids = SimpleNamespace(state=5, name=4, human=7, fstype=4, label=5)
|
|
363
457
|
for ns in nss.values():
|
|
364
458
|
wids.state = max(wids.state, len(ns.state))
|
|
365
459
|
# wids.fsuse = max(wids.fsuse, len(ns.fsuse))
|
|
@@ -383,18 +477,23 @@ class DeviceInfo:
|
|
|
383
477
|
emit += f'{sep}{"SIZE":_^{wids.human}}'
|
|
384
478
|
emit += f'{sep}{"TYPE":_^{wids.fstype}}'
|
|
385
479
|
emit += f'{sep}{"LABEL":_^{wids.label}}'
|
|
386
|
-
emit += f'{sep}MOUNTS'
|
|
480
|
+
emit += f'{sep}MOUNTS/STATUS'
|
|
387
481
|
return emit
|
|
388
482
|
|
|
389
483
|
def part_str(self, partition):
|
|
390
484
|
""" Convert partition to human value. """
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
485
|
+
# def print_str_or_dashes(name, width, chrs=' -'):
|
|
486
|
+
# if not name.strip(): # Create a string of '─' characters of the specified width
|
|
487
|
+
# result = f'{chrs}' * (width//2)
|
|
488
|
+
# result += ' ' * (width%2)
|
|
489
|
+
# else: # Format the name to be right-aligned within the specified width
|
|
490
|
+
# result = f'{name:>{width}}'
|
|
491
|
+
# return result
|
|
492
|
+
|
|
493
|
+
def print_str_or_dash(name, width, empty='-'):
|
|
494
|
+
if not name.strip(): # return
|
|
495
|
+
name = empty
|
|
496
|
+
return f'{name:^{width}}'
|
|
398
497
|
|
|
399
498
|
sep = ' '
|
|
400
499
|
ns = partition # shorthand
|
|
@@ -409,14 +508,17 @@ class DeviceInfo:
|
|
|
409
508
|
emit += f'{sep}{name_str:<{wids.name}}'
|
|
410
509
|
# emit += f'{sep}{ns.fsuse:^{wids.fsuse}}'
|
|
411
510
|
emit += f'{sep}{human(ns.size_bytes):>{wids.human}}'
|
|
412
|
-
emit += sep +
|
|
511
|
+
emit += sep + print_str_or_dash(ns.fstype, wids.fstype)
|
|
413
512
|
# emit += f'{sep}{ns.fstype:>{wids.fstype}}'
|
|
414
513
|
if ns.parent is None:
|
|
415
|
-
emit += sep + '■' + '─'*(wids.label-2) + '■'
|
|
514
|
+
emit += sep + '■' + '─'*(wids.label-2) + '■'
|
|
416
515
|
else:
|
|
417
|
-
emit += sep +
|
|
516
|
+
emit += sep + print_str_or_dash(ns.label, wids.label)
|
|
418
517
|
# emit += f' {ns.label:>{wids.label}}'
|
|
419
|
-
|
|
518
|
+
if ns.mounts:
|
|
519
|
+
emit += f'{sep}{",".join(ns.mounts)}'
|
|
520
|
+
else:
|
|
521
|
+
emit += f'{sep}{ns.marker}'
|
|
420
522
|
return emit
|
|
421
523
|
|
|
422
524
|
def merge_dev_infos(self, nss, prev_nss=None):
|
|
@@ -429,18 +531,21 @@ class DeviceInfo:
|
|
|
429
531
|
if new_ns:
|
|
430
532
|
if prev_ns.job:
|
|
431
533
|
new_ns.job = prev_ns.job
|
|
432
|
-
new_ns.
|
|
534
|
+
new_ns.dflt = prev_ns.dflt
|
|
535
|
+
|
|
433
536
|
if prev_ns.state == 'Lock':
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
new_ns.state =
|
|
537
|
+
new_ns.state = 'Lock'
|
|
538
|
+
elif new_ns.state not in ('s', 'W'):
|
|
539
|
+
new_ns.state = new_ns.dflt
|
|
540
|
+
if prev_ns.state not in ('s', 'W', 'Busy', 'Unlk'):
|
|
541
|
+
new_ns.state = prev_ns.state # re-infer these
|
|
437
542
|
elif prev_ns.job:
|
|
438
543
|
# unplugged device with job..
|
|
439
544
|
nss[name] = prev_ns # carry forward
|
|
440
545
|
prev_ns.job.do_abort = True
|
|
441
|
-
for name,
|
|
442
|
-
if name not in prev_nss:
|
|
443
|
-
|
|
546
|
+
for name, new_ns in nss.items():
|
|
547
|
+
if name not in prev_nss and new_ns.state not in ('s', 'W'):
|
|
548
|
+
new_ns.state = '^'
|
|
444
549
|
return nss
|
|
445
550
|
|
|
446
551
|
def assemble_partitions(self, prev_nss=None):
|
|
@@ -605,7 +710,7 @@ class DiskWipe:
|
|
|
605
710
|
for part in self.partitions.values():
|
|
606
711
|
stop_if_idle(part)
|
|
607
712
|
return None
|
|
608
|
-
|
|
713
|
+
|
|
609
714
|
if key == ord('l'):
|
|
610
715
|
part = self.partitions[self.pick_name]
|
|
611
716
|
self.set_state(part, 'Unlk' if part.state == 'Lock' else 'Lock')
|
|
@@ -739,7 +844,7 @@ class DiskWipe:
|
|
|
739
844
|
|
|
740
845
|
if partition.parent and self.partitions[partition.parent].state == 'Lock':
|
|
741
846
|
continue
|
|
742
|
-
|
|
847
|
+
|
|
743
848
|
if wanted(name) or partition.job:
|
|
744
849
|
partition.line = self.dev_info.part_str(partition)
|
|
745
850
|
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.3
|
|
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
|