orphan disk search update
This commit is contained in:
@@ -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
@@ -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
@@ -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:
|
||||||
|
|||||||
@@ -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],
|
||||||
|
|||||||
Reference in New Issue
Block a user