orphan disk search update

This commit is contained in:
David Schroeder
2026-09-22 22:28:41 -05:00
parent 3ecc338466
commit e34f9d832e
4 changed files with 19 additions and 17 deletions
+5 -3
View File
@@ -87,10 +87,12 @@ aliases, and backend-specific backing dependencies require review. No guest or
storage changes are made.
Offline nodes, inactive storage, and failed reads are reported as incomplete
coverage; missing guest configurations prevent orphan classification. Changes
to guest configurations during the scan require a rerun. Scan when migration,
coverage; missing guest configurations prevent orphan classification.
Inactive/disabled storage warning lines are hidden in the terminal display.
Changes to guest configurations during the scan require a rerun. Scan when migration,
replication, and disk operations are idle because the inventory is not atomic.
CSV export includes coverage warnings and refuses to overwrite an existing file.
CSV export contains disk rows only, omits coverage warnings, and refuses to
overwrite an existing file.
Exit status is 0 for a completed scan, 2 for incomplete coverage or changed
configurations, and 1 for a fatal scan/export error.
+1 -1
View File
@@ -3,7 +3,7 @@
action="${1:-}"
FOLDER='/opt/idssys/ta-proxmenu'
VERS='2026.9.22-8'
VERS='2026.9.22-10'
noupdate=' '
+6 -7
View File
@@ -182,6 +182,8 @@ def render(rows, warnings, coverage):
for entry in coverage:
print(' ' + entry)
for warning in warnings:
if warning.endswith(': inactive or disabled; not scanned'):
continue
print('WARNING: ' + warning)
group = None
for row in sorted(rows, key=lambda r: (r['node'], r['storage'], r['volume'])):
@@ -195,16 +197,13 @@ def render(rows, warnings, coverage):
print('No unreferenced or unused volumes found in the successfully scanned storage.')
def export_csv(path, rows, warnings):
def export_csv(path, rows):
# Exclusive creation avoids overwriting existing files, including symlink targets.
with open(path, 'x', newline='') as handle:
writer = csv.DictWriter(handle, fieldnames=FIELDS + ['disk_size', 'coverage_warnings'])
writer = csv.DictWriter(handle, fieldnames=FIELDS + ['disk_size'])
writer.writeheader()
for row in rows:
writer.writerow(dict(row, disk_size=format_size(row['size_bytes'], row.get('size_is_approximate', False)),
coverage_warnings='; '.join(warnings)))
if warnings and not rows:
writer.writerow({'status': 'Incomplete scan', 'coverage_warnings': '; '.join(warnings)})
writer.writerow(dict(row, disk_size=format_size(row['size_bytes'], row.get('size_is_approximate', False))))
def main():
@@ -220,7 +219,7 @@ def main():
if args.interactive and sys.stdin.isatty() and not destination:
destination = input('\nCSV export path (Enter to skip): ').strip()
if destination:
export_csv(destination, rows, warnings)
export_csv(destination, rows)
print('CSV saved: ' + destination)
return 2 if warnings else 0
except (OSError, RuntimeError, ValueError, subprocess.TimeoutExpired) as exc:
+7 -6
View File
@@ -82,12 +82,13 @@ class ReportTests(unittest.TestCase):
self.assertEqual(len(coverage), 3)
self.assertNotIn('/nodes/b/storage/shared/content', calls)
def test_csv_preserves_warnings_and_refuses_overwrite(self):
def test_csv_omits_warnings_and_refuses_overwrite(self):
path = self.root / 'report.csv'
disks.export_csv(path, [], ['node offline'])
self.assertIn('node offline', path.read_text())
disks.export_csv(path, [])
self.assertNotIn('coverage_warnings', path.read_text())
self.assertEqual(len(path.read_text().splitlines()), 1)
with self.assertRaises(FileExistsError):
disks.export_csv(path, [], [])
disks.export_csv(path, [])
def test_disk_sizes_in_display_and_csv(self):
for index, (size, expected) in enumerate([
@@ -103,7 +104,7 @@ class ReportTests(unittest.TestCase):
disks.render([row], [], [])
self.assertIn('Disk size: ' + expected, output.getvalue())
path = self.root / f'sizes-{index}.csv'
disks.export_csv(path, [row], [])
disks.export_csv(path, [row])
with path.open(newline='') as handle:
exported = next(csv.DictReader(handle))
self.assertEqual(exported['disk_size'], expected)
@@ -135,7 +136,7 @@ class ReportTests(unittest.TestCase):
disks.render(rows, warnings, coverage)
self.assertIn('Disk size: ~25.91 GiB', output.getvalue())
path = self.root / 'approximate.csv'
disks.export_csv(path, rows, warnings)
disks.export_csv(path, rows)
with path.open(newline='') as handle:
exported = list(csv.DictReader(handle))
self.assertEqual([row['disk_size'] for row in exported],