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. storage changes are made.
Offline nodes, inactive storage, and failed reads are reported as incomplete Offline nodes, inactive storage, and failed reads are reported as incomplete
coverage; missing guest configurations prevent orphan classification. Changes coverage; missing guest configurations prevent orphan classification.
to guest configurations during the scan require a rerun. Scan when migration, 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. 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 Exit status is 0 for a completed scan, 2 for incomplete coverage or changed
configurations, and 1 for a fatal scan/export error. configurations, and 1 for a fatal scan/export error.
+1 -1
View File
@@ -3,7 +3,7 @@
action="${1:-}" action="${1:-}"
FOLDER='/opt/idssys/ta-proxmenu' FOLDER='/opt/idssys/ta-proxmenu'
VERS='2026.9.22-8' VERS='2026.9.22-10'
noupdate=' ' noupdate=' '
+6 -7
View File
@@ -182,6 +182,8 @@ def render(rows, warnings, coverage):
for entry in coverage: for entry in coverage:
print(' ' + entry) print(' ' + entry)
for warning in warnings: for warning in warnings:
if warning.endswith(': inactive or disabled; not scanned'):
continue
print('WARNING: ' + warning) print('WARNING: ' + warning)
group = None group = None
for row in sorted(rows, key=lambda r: (r['node'], r['storage'], r['volume'])): 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.') 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. # Exclusive creation avoids overwriting existing files, including symlink targets.
with open(path, 'x', newline='') as handle: 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() writer.writeheader()
for row in rows: for row in rows:
writer.writerow(dict(row, disk_size=format_size(row['size_bytes'], row.get('size_is_approximate', False)), 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)})
def main(): def main():
@@ -220,7 +219,7 @@ def main():
if args.interactive and sys.stdin.isatty() and not destination: if args.interactive and sys.stdin.isatty() and not destination:
destination = input('\nCSV export path (Enter to skip): ').strip() destination = input('\nCSV export path (Enter to skip): ').strip()
if destination: if destination:
export_csv(destination, rows, warnings) export_csv(destination, rows)
print('CSV saved: ' + destination) print('CSV saved: ' + destination)
return 2 if warnings else 0 return 2 if warnings else 0
except (OSError, RuntimeError, ValueError, subprocess.TimeoutExpired) as exc: 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.assertEqual(len(coverage), 3)
self.assertNotIn('/nodes/b/storage/shared/content', calls) 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' path = self.root / 'report.csv'
disks.export_csv(path, [], ['node offline']) disks.export_csv(path, [])
self.assertIn('node offline', path.read_text()) self.assertNotIn('coverage_warnings', path.read_text())
self.assertEqual(len(path.read_text().splitlines()), 1)
with self.assertRaises(FileExistsError): with self.assertRaises(FileExistsError):
disks.export_csv(path, [], []) disks.export_csv(path, [])
def test_disk_sizes_in_display_and_csv(self): def test_disk_sizes_in_display_and_csv(self):
for index, (size, expected) in enumerate([ for index, (size, expected) in enumerate([
@@ -103,7 +104,7 @@ class ReportTests(unittest.TestCase):
disks.render([row], [], []) disks.render([row], [], [])
self.assertIn('Disk size: ' + expected, output.getvalue()) self.assertIn('Disk size: ' + expected, output.getvalue())
path = self.root / f'sizes-{index}.csv' path = self.root / f'sizes-{index}.csv'
disks.export_csv(path, [row], []) disks.export_csv(path, [row])
with path.open(newline='') as handle: with path.open(newline='') as handle:
exported = next(csv.DictReader(handle)) exported = next(csv.DictReader(handle))
self.assertEqual(exported['disk_size'], expected) self.assertEqual(exported['disk_size'], expected)
@@ -135,7 +136,7 @@ class ReportTests(unittest.TestCase):
disks.render(rows, warnings, coverage) disks.render(rows, warnings, coverage)
self.assertIn('Disk size: ~25.91 GiB', output.getvalue()) self.assertIn('Disk size: ~25.91 GiB', output.getvalue())
path = self.root / 'approximate.csv' path = self.root / 'approximate.csv'
disks.export_csv(path, rows, warnings) disks.export_csv(path, rows)
with path.open(newline='') as handle: with path.open(newline='') as handle:
exported = list(csv.DictReader(handle)) exported = list(csv.DictReader(handle))
self.assertEqual([row['disk_size'] for row in exported], self.assertEqual([row['disk_size'] for row in exported],