dwipe 2.0.1__py3-none-any.whl → 2.0.2__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 +183 -52
- dwipe/DiskWipe.py +495 -180
- dwipe/DrivePreChecker.py +90 -0
- dwipe/FirmwareWipeTask.py +370 -0
- dwipe/LsblkMonitor.py +124 -0
- dwipe/PersistentState.py +26 -8
- dwipe/Prereqs.py +84 -0
- dwipe/StructuredLogger.py +643 -0
- dwipe/ToolManager.py +235 -254
- dwipe/Utils.py +108 -0
- dwipe/VerifyTask.py +410 -0
- dwipe/WipeJob.py +613 -165
- dwipe/WipeTask.py +148 -0
- dwipe/WriteTask.py +402 -0
- dwipe/main.py +14 -9
- {dwipe-2.0.1.dist-info → dwipe-2.0.2.dist-info}/METADATA +69 -33
- dwipe-2.0.2.dist-info/RECORD +21 -0
- dwipe/WipeJobFuture.py +0 -245
- dwipe-2.0.1.dist-info/RECORD +0 -14
- {dwipe-2.0.1.dist-info → dwipe-2.0.2.dist-info}/WHEEL +0 -0
- {dwipe-2.0.1.dist-info → dwipe-2.0.2.dist-info}/entry_points.txt +0 -0
- {dwipe-2.0.1.dist-info → dwipe-2.0.2.dist-info}/licenses/LICENSE +0 -0
dwipe/Utils.py
CHANGED
|
@@ -5,11 +5,25 @@ import os
|
|
|
5
5
|
import sys
|
|
6
6
|
import datetime
|
|
7
7
|
from pathlib import Path
|
|
8
|
+
from .StructuredLogger import StructuredLogger
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
class Utils:
|
|
11
12
|
"""Utility functions encapsulated as a family"""
|
|
12
13
|
|
|
14
|
+
# Singleton logger instance
|
|
15
|
+
_logger = None
|
|
16
|
+
|
|
17
|
+
@staticmethod
|
|
18
|
+
def get_logger():
|
|
19
|
+
"""Get or create the singleton StructuredLogger instance"""
|
|
20
|
+
if Utils._logger is None:
|
|
21
|
+
Utils._logger = StructuredLogger(
|
|
22
|
+
app_name='dwipe',
|
|
23
|
+
log_dir=Utils.get_config_dir()
|
|
24
|
+
)
|
|
25
|
+
return Utils._logger
|
|
26
|
+
|
|
13
27
|
@staticmethod
|
|
14
28
|
def human(number):
|
|
15
29
|
"""Return a concise number description."""
|
|
@@ -129,6 +143,100 @@ class Utils:
|
|
|
129
143
|
except Exception:
|
|
130
144
|
pass # Don't fail if log trimming fails
|
|
131
145
|
|
|
146
|
+
@staticmethod
|
|
147
|
+
def get_device_dict(partitions, partition):
|
|
148
|
+
"""Extract device information from partition namespace as dict
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
partition: Partition namespace object with device attributes
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
dict: Device information for structured logging
|
|
155
|
+
"""
|
|
156
|
+
if partition.name in partitions:
|
|
157
|
+
disk = partitions[partition.name]
|
|
158
|
+
while disk.parent:
|
|
159
|
+
disk = partitions[disk.parent]
|
|
160
|
+
else:
|
|
161
|
+
disk = partition
|
|
162
|
+
device_dict = {
|
|
163
|
+
"name": partition.name,
|
|
164
|
+
"path": f"/dev/{partition.name}",
|
|
165
|
+
"size": Utils.human(partition.size_bytes),
|
|
166
|
+
}
|
|
167
|
+
device_dict["uuid"] = partition.uuid
|
|
168
|
+
if partition.type:
|
|
169
|
+
device_dict["type"] = partition.type
|
|
170
|
+
if partition.fstype:
|
|
171
|
+
device_dict["fstype"] = partition.fstype
|
|
172
|
+
if partition.label:
|
|
173
|
+
device_dict["label"] = partition.label
|
|
174
|
+
|
|
175
|
+
device_dict["model"] = disk.model
|
|
176
|
+
device_dict["serial"] = disk.serial
|
|
177
|
+
device_dict["port"] = disk.port
|
|
178
|
+
|
|
179
|
+
return device_dict
|
|
180
|
+
|
|
181
|
+
@staticmethod
|
|
182
|
+
def log_wipe_structured(partitions, partition, job, mode=None):
|
|
183
|
+
"""Log a wipe or verify operation using structured logging
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
partition: Partition namespace object with device info
|
|
187
|
+
job: WipeJob object with job statistics
|
|
188
|
+
mode: Optional mode override (defaults to job.opts.wipe_mode)
|
|
189
|
+
"""
|
|
190
|
+
logger = Utils.get_logger()
|
|
191
|
+
|
|
192
|
+
# Determine log level based on result
|
|
193
|
+
is_verify_only = getattr(job, 'is_verify_only', False)
|
|
194
|
+
is_stopped = job.do_abort
|
|
195
|
+
|
|
196
|
+
if is_verify_only:
|
|
197
|
+
level = "VERIFY_STOPPED" if is_stopped else "VERIFY_COMPLETE"
|
|
198
|
+
else:
|
|
199
|
+
level = "WIPE_STOPPED" if is_stopped else "WIPE_COMPLETE"
|
|
200
|
+
|
|
201
|
+
# Get the three sections
|
|
202
|
+
plan = job.get_plan_dict(mode)
|
|
203
|
+
device = Utils.get_device_dict(partitions, partition)
|
|
204
|
+
summary = job.get_summary_dict()
|
|
205
|
+
|
|
206
|
+
# Create summary message
|
|
207
|
+
result_str = summary['result']
|
|
208
|
+
size_str = device['size']
|
|
209
|
+
time_str = summary['total_elapsed']
|
|
210
|
+
# Get rate from first step (wipe step)
|
|
211
|
+
rate_str = summary['steps'][0]['rate'] if summary['steps'] else 'N/A'
|
|
212
|
+
|
|
213
|
+
# Build base message
|
|
214
|
+
operation = plan['operation'].capitalize()
|
|
215
|
+
message = f"{operation} {result_str}: {device['name']} {size_str}"
|
|
216
|
+
|
|
217
|
+
# Add percentage if stopped
|
|
218
|
+
if result_str == 'stopped' and summary.get('pct_complete', 0) > 0:
|
|
219
|
+
message += f" ({summary['pct_complete']:.0f}%)"
|
|
220
|
+
|
|
221
|
+
# Add timing and rate
|
|
222
|
+
message += f" in {time_str} @ {rate_str}"
|
|
223
|
+
|
|
224
|
+
# Add error reason if present
|
|
225
|
+
abort_reason = summary.get('abort_reason')
|
|
226
|
+
if abort_reason:
|
|
227
|
+
message += f" [Error: {abort_reason}]"
|
|
228
|
+
|
|
229
|
+
# Log the structured event
|
|
230
|
+
logger.put(
|
|
231
|
+
level,
|
|
232
|
+
message,
|
|
233
|
+
data={
|
|
234
|
+
"plan": plan,
|
|
235
|
+
"device": device,
|
|
236
|
+
"summary": summary
|
|
237
|
+
}
|
|
238
|
+
)
|
|
239
|
+
|
|
132
240
|
@staticmethod
|
|
133
241
|
def log_wipe(device_name, size_bytes, mode, result, elapsed_time=None, uuid=None, label=None, fstype=None, pct=None, verify_result=None):
|
|
134
242
|
"""Log a wipe or verify operation to ~/.config/dwipe/log.txt
|
dwipe/VerifyTask.py
ADDED
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
"""
|
|
2
|
+
VerifyTask - Abstract base class and implementations for verification operations
|
|
3
|
+
|
|
4
|
+
Includes:
|
|
5
|
+
- VerifyTask: Abstract base class with verification logic and statistical analysis
|
|
6
|
+
- VerifyZeroTask: Concrete class for verifying zeros (fast memcmp)
|
|
7
|
+
- VerifyRandTask: Concrete class for verifying random data (statistical)
|
|
8
|
+
"""
|
|
9
|
+
# pylint: disable=broad-exception-raised,broad-exception-caught
|
|
10
|
+
import os
|
|
11
|
+
import time
|
|
12
|
+
import random
|
|
13
|
+
import traceback
|
|
14
|
+
from types import SimpleNamespace
|
|
15
|
+
|
|
16
|
+
from .WipeTask import WipeTask
|
|
17
|
+
from .Utils import Utils
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class VerifyTask(WipeTask):
|
|
21
|
+
"""Abstract base class for verify operations (VerifyZeroTask, VerifyRandTask)
|
|
22
|
+
|
|
23
|
+
Implements verification logic with:
|
|
24
|
+
- Section-by-section analysis of disk content
|
|
25
|
+
- Fast-fail for zero verification (memcmp)
|
|
26
|
+
- Statistical analysis for random pattern verification
|
|
27
|
+
- Progress tracking
|
|
28
|
+
|
|
29
|
+
Subclasses must set:
|
|
30
|
+
- expected_pattern: "zeroed" or "random"
|
|
31
|
+
- fast_fail: True for zero (fast memcmp), False for random (statistical)
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self, device_path, total_size, opts=None, verify_pct=2, expected_pattern=None):
|
|
35
|
+
"""Initialize verify task
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
device_path: Path to device (e.g., '/dev/sda1')
|
|
39
|
+
total_size: Total size in bytes
|
|
40
|
+
opts: Options namespace
|
|
41
|
+
verify_pct: Percentage of disk to verify (e.g., 2 for 2%)
|
|
42
|
+
expected_pattern: "zeroed", "random", or None (auto-detect)
|
|
43
|
+
"""
|
|
44
|
+
super().__init__(device_path, total_size, opts)
|
|
45
|
+
|
|
46
|
+
# Verify-specific attributes
|
|
47
|
+
self.verify_pct = verify_pct
|
|
48
|
+
self.expected_pattern = expected_pattern
|
|
49
|
+
self.verify_result = None # "zeroed", "random", "not-wiped", "mixed", "error"
|
|
50
|
+
self.section_results = [] # Section-by-section results
|
|
51
|
+
self.verify_progress = 0 # Bytes verified (for total_written tracking)
|
|
52
|
+
|
|
53
|
+
# Fast-fail flag (set by subclasses)
|
|
54
|
+
self.fast_fail = False
|
|
55
|
+
|
|
56
|
+
def run_task(self):
|
|
57
|
+
"""Execute verification operation (blocking, runs in thread)"""
|
|
58
|
+
try:
|
|
59
|
+
if self.verify_pct == 0:
|
|
60
|
+
self.verify_result = "skipped"
|
|
61
|
+
self.done = True
|
|
62
|
+
return
|
|
63
|
+
|
|
64
|
+
# Fast-fail for zeros (VerifyZeroTask)
|
|
65
|
+
fast_fail_zeros = self.fast_fail and self.expected_pattern == "zeroed"
|
|
66
|
+
|
|
67
|
+
# For unmarked disks: track if ALL bytes are zero
|
|
68
|
+
all_zeros = (self.expected_pattern is None)
|
|
69
|
+
|
|
70
|
+
# Open with regular buffered I/O
|
|
71
|
+
fd = os.open(self.device_path, os.O_RDONLY)
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
read_chunk_size = 64 * 1024 # 64KB chunks
|
|
75
|
+
SAMPLE_STEP = 23 # Sample every 23rd byte (~4% of data) - prime for even distribution
|
|
76
|
+
|
|
77
|
+
# Skip marker area
|
|
78
|
+
marker_skip = WipeTask.BUFFER_SIZE
|
|
79
|
+
usable_size = self.total_size - marker_skip
|
|
80
|
+
|
|
81
|
+
# Divide disk into 100 sections for sampling
|
|
82
|
+
num_sections = 100
|
|
83
|
+
section_size = usable_size // num_sections
|
|
84
|
+
|
|
85
|
+
# Pre-allocated zero pattern for fast comparison
|
|
86
|
+
ZERO_PATTERN_64K = b'\x00' * (64 * 1024)
|
|
87
|
+
|
|
88
|
+
# Track if any section failed
|
|
89
|
+
overall_failed = False
|
|
90
|
+
failure_reason = ""
|
|
91
|
+
|
|
92
|
+
for section_idx in range(num_sections):
|
|
93
|
+
if self.do_abort or overall_failed:
|
|
94
|
+
break
|
|
95
|
+
|
|
96
|
+
# Reset analysis for THIS SECTION
|
|
97
|
+
section_byte_counts = [0] * 256
|
|
98
|
+
section_samples = 0
|
|
99
|
+
section_found_nonzero = False
|
|
100
|
+
|
|
101
|
+
# Calculate bytes to verify in this section
|
|
102
|
+
bytes_in_section = min(section_size, usable_size - section_idx * section_size)
|
|
103
|
+
bytes_to_verify = int(bytes_in_section * self.verify_pct / 100)
|
|
104
|
+
|
|
105
|
+
if bytes_to_verify == 0:
|
|
106
|
+
self.section_results.append((section_idx, "skipped", {}))
|
|
107
|
+
continue
|
|
108
|
+
|
|
109
|
+
# Random offset within section
|
|
110
|
+
if bytes_to_verify < bytes_in_section:
|
|
111
|
+
offset_in_section = random.randint(0, bytes_in_section - bytes_to_verify)
|
|
112
|
+
else:
|
|
113
|
+
offset_in_section = 0
|
|
114
|
+
|
|
115
|
+
read_pos = marker_skip + (section_idx * section_size) + offset_in_section
|
|
116
|
+
verified_in_section = 0
|
|
117
|
+
|
|
118
|
+
# Seek to position in this section
|
|
119
|
+
os.lseek(fd, read_pos, os.SEEK_SET)
|
|
120
|
+
|
|
121
|
+
# Read and analyze THIS SECTION
|
|
122
|
+
while verified_in_section < bytes_to_verify:
|
|
123
|
+
if self.do_abort:
|
|
124
|
+
break
|
|
125
|
+
|
|
126
|
+
chunk_size = min(read_chunk_size, bytes_to_verify - verified_in_section)
|
|
127
|
+
|
|
128
|
+
data = os.read(fd, chunk_size)
|
|
129
|
+
if not data:
|
|
130
|
+
break
|
|
131
|
+
|
|
132
|
+
# --------------------------------------------------
|
|
133
|
+
# SECTION ANALYSIS
|
|
134
|
+
# --------------------------------------------------
|
|
135
|
+
|
|
136
|
+
# FAST zero check for zeroed pattern
|
|
137
|
+
if fast_fail_zeros:
|
|
138
|
+
# Ultra-fast: compare against pre-allocated zero pattern
|
|
139
|
+
if memoryview(data) != ZERO_PATTERN_64K[:len(data)]:
|
|
140
|
+
failed_offset = read_pos + verified_in_section
|
|
141
|
+
overall_failed = True
|
|
142
|
+
failure_reason = f"non-zero at {Utils.human(failed_offset)}"
|
|
143
|
+
break
|
|
144
|
+
|
|
145
|
+
# FAST check for unmarked disks (looking for all zeros)
|
|
146
|
+
if all_zeros and not section_found_nonzero:
|
|
147
|
+
# Fast check: use bytes.count() which is C-optimized
|
|
148
|
+
if data.count(0) != len(data):
|
|
149
|
+
section_found_nonzero = True
|
|
150
|
+
|
|
151
|
+
# RANDOM pattern analysis (always collect data for analysis)
|
|
152
|
+
# Use memoryview for fast slicing
|
|
153
|
+
mv = memoryview(data)
|
|
154
|
+
data_len = len(data)
|
|
155
|
+
|
|
156
|
+
# Sample every SAMPLE_STEP-th byte
|
|
157
|
+
for i in range(0, data_len, SAMPLE_STEP):
|
|
158
|
+
section_byte_counts[mv[i]] += 1
|
|
159
|
+
section_samples += 1
|
|
160
|
+
|
|
161
|
+
# --------------------------------------------------
|
|
162
|
+
# END SECTION ANALYSIS
|
|
163
|
+
# --------------------------------------------------
|
|
164
|
+
|
|
165
|
+
verified_in_section += len(data)
|
|
166
|
+
self.verify_progress += len(data) # Track actual bytes read for progress
|
|
167
|
+
self.total_written = self.verify_progress # Update for get_status()
|
|
168
|
+
|
|
169
|
+
# After reading section, analyze it
|
|
170
|
+
if overall_failed:
|
|
171
|
+
break
|
|
172
|
+
|
|
173
|
+
# Determine section result
|
|
174
|
+
if fast_fail_zeros:
|
|
175
|
+
# Already passed zero check if we got here
|
|
176
|
+
section_result = "zeroed"
|
|
177
|
+
section_stats = {}
|
|
178
|
+
|
|
179
|
+
elif all_zeros:
|
|
180
|
+
if not section_found_nonzero:
|
|
181
|
+
section_result = "zeroed"
|
|
182
|
+
section_stats = {}
|
|
183
|
+
else:
|
|
184
|
+
# Need to check if it's random
|
|
185
|
+
section_result, section_stats = self._analyze_section_randomness(
|
|
186
|
+
section_byte_counts, section_samples
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
else: # Expected random
|
|
190
|
+
section_result, section_stats = self._analyze_section_randomness(
|
|
191
|
+
section_byte_counts, section_samples
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
# Store section result
|
|
195
|
+
self.section_results.append((section_idx, section_result, section_stats))
|
|
196
|
+
|
|
197
|
+
# Check if section failed
|
|
198
|
+
if (self.expected_pattern == "random" and section_result != "random") or \
|
|
199
|
+
(self.expected_pattern == "zeroed" and section_result != "zeroed") or \
|
|
200
|
+
(self.expected_pattern is None and section_result == "not-wiped"):
|
|
201
|
+
|
|
202
|
+
overall_failed = True
|
|
203
|
+
failure_reason = f"section {section_idx}: {section_result}"
|
|
204
|
+
break
|
|
205
|
+
|
|
206
|
+
finally:
|
|
207
|
+
# Close file descriptor
|
|
208
|
+
if fd is not None:
|
|
209
|
+
os.close(fd)
|
|
210
|
+
|
|
211
|
+
# Determine overall result
|
|
212
|
+
if overall_failed:
|
|
213
|
+
if self.expected_pattern == "zeroed":
|
|
214
|
+
self.verify_result = f"not-wiped ({failure_reason})"
|
|
215
|
+
elif self.expected_pattern == "random":
|
|
216
|
+
self.verify_result = f"not-wiped ({failure_reason})"
|
|
217
|
+
else: # unmarked
|
|
218
|
+
# Count section results
|
|
219
|
+
zeroed_sections = sum(1 for _, result, _ in self.section_results if result == "zeroed")
|
|
220
|
+
random_sections = sum(1 for _, result, _ in self.section_results if result == "random")
|
|
221
|
+
total_checked = len([r for _, r, _ in self.section_results if r != "skipped"])
|
|
222
|
+
|
|
223
|
+
if zeroed_sections == total_checked:
|
|
224
|
+
self.verify_result = "zeroed"
|
|
225
|
+
self.expected_pattern = "zeroed"
|
|
226
|
+
elif random_sections == total_checked:
|
|
227
|
+
self.verify_result = "random"
|
|
228
|
+
self.expected_pattern = "random"
|
|
229
|
+
else:
|
|
230
|
+
self.verify_result = f"mixed ({failure_reason})"
|
|
231
|
+
else:
|
|
232
|
+
# All sections passed
|
|
233
|
+
if self.expected_pattern == "zeroed":
|
|
234
|
+
self.verify_result = "zeroed"
|
|
235
|
+
elif self.expected_pattern == "random":
|
|
236
|
+
self.verify_result = "random"
|
|
237
|
+
else: # unmarked
|
|
238
|
+
# Determine from section consensus
|
|
239
|
+
zeroed_sections = sum(1 for _, result, _ in self.section_results if result == "zeroed")
|
|
240
|
+
random_sections = sum(1 for _, result, _ in self.section_results if result == "random")
|
|
241
|
+
|
|
242
|
+
if zeroed_sections > random_sections:
|
|
243
|
+
self.verify_result = "zeroed"
|
|
244
|
+
self.expected_pattern = "zeroed"
|
|
245
|
+
else:
|
|
246
|
+
self.verify_result = "random"
|
|
247
|
+
self.expected_pattern = "random"
|
|
248
|
+
|
|
249
|
+
self.done = True
|
|
250
|
+
except Exception:
|
|
251
|
+
self.exception = traceback.format_exc()
|
|
252
|
+
self.verify_result = "error"
|
|
253
|
+
self.done = True
|
|
254
|
+
|
|
255
|
+
def _analyze_section_randomness(self, byte_counts, total_samples):
|
|
256
|
+
"""Analyze if a section appears random"""
|
|
257
|
+
if total_samples < 100:
|
|
258
|
+
return "insufficient-data", {"samples": total_samples}
|
|
259
|
+
|
|
260
|
+
# Calculate statistics
|
|
261
|
+
max_count = max(byte_counts)
|
|
262
|
+
max_freq = max_count / total_samples
|
|
263
|
+
|
|
264
|
+
# Count unique bytes seen
|
|
265
|
+
unique_bytes = sum(1 for count in byte_counts if count > 0)
|
|
266
|
+
|
|
267
|
+
# Count completely unused bytes
|
|
268
|
+
unused_bytes = sum(1 for count in byte_counts if count == 0)
|
|
269
|
+
|
|
270
|
+
# Calculate expected frequency and variance
|
|
271
|
+
expected = total_samples / 256
|
|
272
|
+
if expected > 0:
|
|
273
|
+
# Coefficient of variation (measure of dispersion)
|
|
274
|
+
variance = sum((count - expected) ** 2 for count in byte_counts) / 256
|
|
275
|
+
std_dev = variance ** 0.5
|
|
276
|
+
cv = std_dev / expected
|
|
277
|
+
else:
|
|
278
|
+
cv = float('inf')
|
|
279
|
+
|
|
280
|
+
# Decision logic for "random"
|
|
281
|
+
# Good random data should:
|
|
282
|
+
# 1. Use most byte values (>200 unique)
|
|
283
|
+
# 2. No single byte dominates (<2% frequency)
|
|
284
|
+
# 3. Relatively even distribution (CV < 2.0)
|
|
285
|
+
# 4. Not too many zeros (if it's supposed to be random, not zeroed)
|
|
286
|
+
|
|
287
|
+
is_random = (unique_bytes > 200 and # >78% of bytes used
|
|
288
|
+
max_freq < 0.02 and # No byte > 2%
|
|
289
|
+
cv < 2.0 and # Not too lumpy
|
|
290
|
+
byte_counts[0] / total_samples < 0.5) # Not mostly zeros
|
|
291
|
+
|
|
292
|
+
stats = {
|
|
293
|
+
"samples": total_samples,
|
|
294
|
+
"max_freq": max_freq,
|
|
295
|
+
"unique_bytes": unique_bytes,
|
|
296
|
+
"unused_bytes": unused_bytes,
|
|
297
|
+
"cv": cv,
|
|
298
|
+
"zero_freq": byte_counts[0] / total_samples if total_samples > 0 else 0
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if is_random:
|
|
302
|
+
return "random", stats
|
|
303
|
+
else:
|
|
304
|
+
# Check if it's zeros
|
|
305
|
+
if byte_counts[0] / total_samples > 0.95:
|
|
306
|
+
return "zeroed", stats
|
|
307
|
+
else:
|
|
308
|
+
return "not-wiped", stats
|
|
309
|
+
|
|
310
|
+
def get_status(self):
|
|
311
|
+
"""Get current progress status (thread-safe, called from main thread)
|
|
312
|
+
|
|
313
|
+
Returns verification percentage with 'v' prefix (e.g., "v45%")
|
|
314
|
+
"""
|
|
315
|
+
mono = time.monotonic()
|
|
316
|
+
elapsed_time = mono - self.start_mono
|
|
317
|
+
|
|
318
|
+
# Calculate total bytes to verify (verify_pct% of total_size)
|
|
319
|
+
if self.verify_pct > 0:
|
|
320
|
+
total_to_verify = self.total_size * self.verify_pct / 100
|
|
321
|
+
else:
|
|
322
|
+
total_to_verify = self.total_size
|
|
323
|
+
|
|
324
|
+
# Calculate verification percentage (0-100)
|
|
325
|
+
pct = int((self.verify_progress / total_to_verify) * 100) if total_to_verify > 0 else 0
|
|
326
|
+
pct_str = f'v{pct}%'
|
|
327
|
+
|
|
328
|
+
if self.do_abort:
|
|
329
|
+
pct_str = 'STOP'
|
|
330
|
+
|
|
331
|
+
# Track verification progress for rate calculation
|
|
332
|
+
self.wr_hists.append(SimpleNamespace(mono=mono, written=self.verify_progress))
|
|
333
|
+
floor = mono - 30
|
|
334
|
+
while len(self.wr_hists) >= 3 and self.wr_hists[1].mono >= floor:
|
|
335
|
+
del self.wr_hists[0]
|
|
336
|
+
|
|
337
|
+
delta_mono = mono - self.wr_hists[0].mono
|
|
338
|
+
physical_rate = (self.verify_progress - self.wr_hists[0].written) / delta_mono if delta_mono > 1.0 else 0
|
|
339
|
+
# Scale rate to show "effective" verification rate (as if verifying 100% of disk)
|
|
340
|
+
effective_rate = physical_rate * (100 / self.verify_pct) if self.verify_pct > 0 else physical_rate
|
|
341
|
+
rate_str = f'{Utils.human(int(round(effective_rate, 0)))}/s'
|
|
342
|
+
|
|
343
|
+
if physical_rate > 0:
|
|
344
|
+
remaining = total_to_verify - self.verify_progress
|
|
345
|
+
when = int(round(remaining / physical_rate))
|
|
346
|
+
when_str = Utils.ago_str(when)
|
|
347
|
+
else:
|
|
348
|
+
when_str = '0'
|
|
349
|
+
|
|
350
|
+
return Utils.ago_str(int(round(elapsed_time))), pct_str, rate_str, when_str
|
|
351
|
+
|
|
352
|
+
def get_summary_dict(self):
|
|
353
|
+
"""Get final summary for this verify task
|
|
354
|
+
|
|
355
|
+
Returns:
|
|
356
|
+
dict: Summary with step name, elapsed, rate, bytes checked, result
|
|
357
|
+
"""
|
|
358
|
+
mono = time.monotonic()
|
|
359
|
+
elapsed = mono - self.start_mono
|
|
360
|
+
rate_bps = self.verify_progress / elapsed if elapsed > 0 else 0
|
|
361
|
+
|
|
362
|
+
# Determine mode from expected pattern
|
|
363
|
+
mode = "Rand" if self.expected_pattern == "random" else "Zero"
|
|
364
|
+
|
|
365
|
+
# Build verify label
|
|
366
|
+
verify_label = f"verify {mode}"
|
|
367
|
+
if self.verify_pct > 0 and self.verify_pct < 100:
|
|
368
|
+
verify_label += f" ({self.verify_pct}% sample)"
|
|
369
|
+
|
|
370
|
+
# Extract verify detail if present
|
|
371
|
+
verify_detail = None
|
|
372
|
+
if self.verify_result and '(' in str(self.verify_result):
|
|
373
|
+
verify_detail = str(self.verify_result).split('(')[1].rstrip(')')
|
|
374
|
+
|
|
375
|
+
result = {
|
|
376
|
+
"step": verify_label,
|
|
377
|
+
"elapsed": Utils.ago_str(int(elapsed)),
|
|
378
|
+
"rate": f"{Utils.human(int(rate_bps))}/s",
|
|
379
|
+
"bytes_checked": self.verify_progress,
|
|
380
|
+
"result": self.verify_result,
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if verify_detail:
|
|
384
|
+
result["verify_detail"] = verify_detail
|
|
385
|
+
|
|
386
|
+
return result
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
class VerifyZeroTask(VerifyTask):
|
|
390
|
+
"""Verify disk contains zeros"""
|
|
391
|
+
|
|
392
|
+
def __init__(self, device_path, total_size, opts=None, verify_pct=2):
|
|
393
|
+
super().__init__(device_path, total_size, opts, verify_pct, expected_pattern="zeroed")
|
|
394
|
+
self.fast_fail = True # Use fast memcmp verification
|
|
395
|
+
|
|
396
|
+
def get_display_name(self):
|
|
397
|
+
"""Get display name for zero verification"""
|
|
398
|
+
return "Verify"
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
class VerifyRandTask(VerifyTask):
|
|
402
|
+
"""Verify disk contains random pattern"""
|
|
403
|
+
|
|
404
|
+
def __init__(self, device_path, total_size, opts=None, verify_pct=2):
|
|
405
|
+
super().__init__(device_path, total_size, opts, verify_pct, expected_pattern="random")
|
|
406
|
+
self.fast_fail = False # Use statistical analysis
|
|
407
|
+
|
|
408
|
+
def get_display_name(self):
|
|
409
|
+
"""Get display name for random verification"""
|
|
410
|
+
return "Verify"
|