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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dwipe
3
- Version: 1.0.1
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:
@@ -47,7 +47,7 @@ build-backend = "setuptools.build_meta"
47
47
 
48
48
  [project]
49
49
  name = "dwipe"
50
- version = "1.0.1"
50
+ version = "1.0.3"
51
51
  description = "A tool to wipe disks and partitions for Linux"
52
52
  authors = [
53
53
  { name = "Joe Defen", email = "joedef@google.com" }
@@ -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 /= 1024
34
- if number < 99.95 or not suffixes:
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(chunk)
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=6, fstype=4, label=5)
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
- def print_str_or_dashes(name, width, chrs=' -'):
392
- if not name.strip(): # Create a string of '─' characters of the specified width
393
- result = f'{chrs}' * (width//2)
394
- result += ' ' * (width%2)
395
- else: # Format the name to be right-aligned within the specified width
396
- result = f'{name:>{width}}'
397
- return result
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 + print_str_or_dashes(ns.fstype, wids.fstype)
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 + print_str_or_dashes(ns.label, wids.label, chrs=' -')
516
+ emit += sep + print_str_or_dash(ns.label, wids.label)
418
517
  # emit += f' {ns.label:>{wids.label}}'
419
- emit += f'{sep}{",".join(ns.mounts)}'
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.state = new_ns.dflt = prev_ns.dflt
534
+ new_ns.dflt = prev_ns.dflt
535
+
433
536
  if prev_ns.state == 'Lock':
434
- pass
435
- if prev_ns.state not in ('Busy', 'Unlk'): # re-infer these
436
- new_ns.state = prev_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, prev_ns in nss.items():
442
- if name not in prev_nss:
443
- prev_ns.state = '^'
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.1
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