Skip to content

Commit f648c0c

Browse files
committed
feat: add --silent mode and --no-csv option; bump version to 2.11.0
1 parent 0894777 commit f648c0c

3 files changed

Lines changed: 92 additions & 34 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# 📝 Changelog
22

3+
- 2.11.0
4+
- Disallow combining `--silent` and `--no-csv` to avoid fully silent runs without CSV output.
5+
- Keep documentation in sync with the new CLI constraint.
6+
- Add `--silent` to suppress all output (useful for cron jobs) and avoid interactive mode in silent runs.
7+
- Add `--no-csv` to disable CSV report generation for `--all-guests`.
8+
- Document the new CLI options.
39
- 2.10.1
410
- Always quote only the `last_comment` CSV field to avoid delimiter-related parsing issues (semicolon-separated).
511
- 2.10.0

README.md

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ It calculates the **real disk space usage** of a specific **namespace**, **VM**,
99

1010
This allows accurate insights into space consumption per tenant or object — useful for chargeback, reporting, and storage optimization.
1111

12-
**Current version:** 2.10.1 (`./pbs_chunk_checker.py --version`)
12+
**Current version:** 2.11.0 (`./pbs_chunk_checker.py --version`)
1313

1414
See full changes in `CHANGELOG.md`.
1515

@@ -68,6 +68,9 @@ Examples:
6868
./pbs_chunk_checker.py --datastore MyDatastore --searchpath /ns/MyNamespace --all-guests
6969
```
7070

71+
Tip:
72+
Use `--silent` to suppress all output when running the script from cron jobs. Note that `--silent` and `--no-csv` cannot be combined.
73+
7174
### Interactive mode
7275
Run without parameters to open a menu for selecting the datastore and the search path:
7376

@@ -135,19 +138,21 @@ Notes:
135138
|--------|-------------|-------------|---------|
136139
| `--datastore` | Required (script mode) | PBS datastore name that contains the object you want to analyse ||
137140
| `--searchpath` | Required (script mode) | Object path inside the datastore (e.g. `/ns/MyNamespace` or `/ns/MyNamespace/vm/100`) ||
138-
| `--all-guests` | Optional | Scan the entire datastore (or only the namespace given via `--searchpath`), print a per-guest size summary, and write a CSV report (requires `--datastore`) ||
141+
| `--all-guests` | Optional | Scan the entire datastore (or only the namespace given via `--searchpath`), print a per-guest size summary, and write a CSV report unless `--no-csv` is set (requires `--datastore`) ||
139142
| `--threads` | Optional | Degree of parallelism for parsing index files and statting chunks | `2 × CPU cores (max 32)` |
140143
| `--no-emoji` | Optional | Replace emoji icons in CLI output with ASCII labels | Emoji output |
144+
| `--silent` | Optional | Suppress all output (useful for cron jobs; cannot be combined with `--no-csv`) | Disabled |
141145
| `--show-comments` | Optional | Show a short guest label derived from the latest snapshot comment next to each VM/CT in per-guest summaries and interactive path selection | Disabled |
142-
| `--csv-dir` | Optional | Directory where the CSV report for `--all-guests` is written | Current working directory |
146+
| `--no-csv` | Optional | Disable writing the CSV report for `--all-guests` (cannot be combined with `--silent`) | Enabled |
147+
| `--csv-dir` | Optional | Directory where the CSV report for `--all-guests` is written (ignored with `--no-csv`) | Current working directory |
143148
| `--version` | Optional | Show the script version and exit ||
144149
| `--update` | Optional | Check for new releases and offer self-update, then exit ||
145150

146151
---
147152

148153
### CSV output for full datastore scans
149154

150-
When running with `--all-guests`, the script writes a CSV report **after** the scan finishes.
155+
When running with `--all-guests`, the script writes a CSV report **after** the scan finishes unless `--no-csv` is set.
151156

152157
- File name: ISO 8601 timestamp, e.g. `2025-07-01T12:34:56.csv`
153158
- Output directory: current working directory by default, or via `--csv-dir` / the Options overlay

pbs_chunk_checker.py

Lines changed: 77 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
- Repository: https://github.com/VoltKraft/PBS_Chunk_Checker
1919
"""
2020

21-
__version__ = "2.10.1"
21+
__version__ = "2.11.0"
2222

2323
import argparse
2424
import concurrent.futures as futures
@@ -46,6 +46,7 @@
4646
Optional,
4747
Sequence,
4848
Set,
49+
TextIO,
4950
Tuple,
5051
)
5152
import urllib.error
@@ -135,6 +136,8 @@ def _format_command(cmd: object) -> str:
135136

136137
ICONS: Dict[str, str] = EMOJI_ICONS.copy()
137138
_EMOJI_ENABLED = True
139+
_SILENT = False
140+
_SILENT_STREAM: Optional[TextIO] = None
138141

139142

140143
def _set_emoji_mode(enabled: bool) -> None:
@@ -148,8 +151,24 @@ def _set_emoji_mode(enabled: bool) -> None:
148151
def clear_console() -> None:
149152
"""Clear the terminal similar to the POSIX 'clear' command."""
150153
# ANSI full reset works for Linux terminal emulators
154+
if _SILENT:
155+
return
151156
print("\033c", end="", flush=True)
152157

158+
159+
def _enable_silent_output() -> None:
160+
"""Suppress stdout/stderr output (best-effort)."""
161+
global _SILENT, _SILENT_STREAM
162+
if _SILENT:
163+
return
164+
_SILENT = True
165+
try:
166+
_SILENT_STREAM = open(os.devnull, "w")
167+
sys.stdout = _SILENT_STREAM # type: ignore[assignment]
168+
sys.stderr = _SILENT_STREAM # type: ignore[assignment]
169+
except Exception:
170+
pass
171+
153172
# =============================================================================
154173
# Update check and self-update (GitHub Releases)
155174
# =============================================================================
@@ -2256,7 +2275,7 @@ def run_full_datastore_scan(
22562275
scan_root: Optional[Path] = None,
22572276
require_confirmation: bool = True,
22582277
) -> int:
2259-
"""Analyze every VM/CT under the datastore, print a size overview, and emit CSV."""
2278+
"""Analyze every VM/CT under the datastore, print a size overview, and emit CSV when enabled."""
22602279
root = scan_root if scan_root is not None else datastore_root
22612280
guests = discover_guest_paths(root)
22622281
guests = sorted(guests, key=lambda p: str(p).lower())
@@ -2266,22 +2285,24 @@ def run_full_datastore_scan(
22662285
print(f"{ICONS['info']} No VM/CT directories found in this datastore.")
22672286
return 0
22682287

2269-
csv_dir = _resolve_csv_dir(getattr(args, "csv_dir", None))
2270-
if not csv_dir.exists():
2271-
sys.stderr.write(
2272-
f"{ICONS['error']} Error: CSV output path does not exist → {csv_dir}\n"
2273-
)
2274-
return 1
2275-
if not csv_dir.is_dir():
2276-
sys.stderr.write(
2277-
f"{ICONS['error']} Error: CSV output path is not a directory → {csv_dir}\n"
2278-
)
2279-
return 1
2280-
if not os.access(csv_dir, os.W_OK):
2281-
sys.stderr.write(
2282-
f"{ICONS['error']} Error: CSV output path is not writable → {csv_dir}\n"
2283-
)
2284-
return 1
2288+
csv_dir: Optional[Path] = None
2289+
if getattr(args, "csv_report", True):
2290+
csv_dir = _resolve_csv_dir(getattr(args, "csv_dir", None))
2291+
if not csv_dir.exists():
2292+
sys.stderr.write(
2293+
f"{ICONS['error']} Error: CSV output path does not exist → {csv_dir}\n"
2294+
)
2295+
return 1
2296+
if not csv_dir.is_dir():
2297+
sys.stderr.write(
2298+
f"{ICONS['error']} Error: CSV output path is not a directory → {csv_dir}\n"
2299+
)
2300+
return 1
2301+
if not os.access(csv_dir, os.W_OK):
2302+
sys.stderr.write(
2303+
f"{ICONS['error']} Error: CSV output path is not writable → {csv_dir}\n"
2304+
)
2305+
return 1
22852306

22862307
if require_confirmation:
22872308
if not confirm_full_datastore_scan():
@@ -2298,7 +2319,7 @@ def run_full_datastore_scan(
22982319

22992320
overall_start = time.time()
23002321
results: List[Tuple[str, UsageResult]] = []
2301-
csv_rows: List[Tuple[str, str, int]] = []
2322+
csv_rows: Optional[List[Tuple[str, str, int]]] = [] if csv_dir is not None else None
23022323
total_guests = len(guests)
23032324

23042325
for idx, guest_path in enumerate(guests, 1):
@@ -2323,7 +2344,8 @@ def run_full_datastore_scan(
23232344
simplified = _simplify_guest_comment(raw_comment)
23242345
if simplified:
23252346
summary_label = f"{label} ({simplified})"
2326-
csv_rows.append((label, raw_comment or "", result.unique_bytes))
2347+
if csv_rows is not None:
2348+
csv_rows.append((label, raw_comment or "", result.unique_bytes))
23272349
results.append((summary_label, result))
23282350
if result.index_files == 0:
23292351
print(f"{ICONS['info']} No index files (*.fidx/*.didx) found for {label}.")
@@ -2338,14 +2360,15 @@ def run_full_datastore_scan(
23382360
print_full_datastore_summary(results)
23392361
total_elapsed = format_elapsed(time.time() - overall_start)
23402362
print(f"{ICONS['timer']} Full datastore scan duration: {total_elapsed}")
2341-
try:
2342-
csv_path = _write_full_scan_csv(csv_rows, csv_dir)
2343-
except Exception as exc:
2344-
sys.stderr.write(
2345-
f"{ICONS['error']} Error: failed to write CSV report: {exc}\n"
2346-
)
2347-
return 1
2348-
print(f"{ICONS['save']} CSV report saved to: {csv_path}")
2363+
if csv_dir is not None and csv_rows is not None:
2364+
try:
2365+
csv_path = _write_full_scan_csv(csv_rows, csv_dir)
2366+
except Exception as exc:
2367+
sys.stderr.write(
2368+
f"{ICONS['error']} Error: failed to write CSV report: {exc}\n"
2369+
)
2370+
return 1
2371+
print(f"{ICONS['save']} CSV report saved to: {csv_path}")
23492372
return 0
23502373

23512374

@@ -2537,7 +2560,8 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
25372560
action="store_true",
25382561
help=(
25392562
"Analyze the entire datastore and show per-guest usage "
2540-
"(includes nested namespaces) and write a CSV report."
2563+
"(includes nested namespaces) and write a CSV report "
2564+
"(unless --no-csv is set)."
25412565
),
25422566
)
25432567
default_threads = min(32, (os.cpu_count() or 4) * 2)
@@ -2564,6 +2588,11 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
25642588
action="store_true",
25652589
help="Disable emoji characters in console output.",
25662590
)
2591+
parser.add_argument(
2592+
"--silent",
2593+
action="store_true",
2594+
help="Suppress all output (useful for cron jobs; cannot be combined with --no-csv).",
2595+
)
25672596
parser.add_argument(
25682597
"--show-comments",
25692598
action="store_true",
@@ -2573,23 +2602,41 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
25732602
"path selection (best-effort)."
25742603
),
25752604
)
2605+
parser.add_argument(
2606+
"--no-csv",
2607+
dest="csv_report",
2608+
action="store_false",
2609+
default=True,
2610+
help="Disable writing the CSV report for --all-guests (cannot be combined with --silent).",
2611+
)
25762612
parser.add_argument(
25772613
"--csv-dir",
25782614
dest="csv_dir",
25792615
default=os.getcwd(),
25802616
help=(
25812617
"Directory to store the CSV report produced by --all-guests "
2582-
"(defaults to the current working directory)."
2618+
"(defaults to the current working directory; ignored with --no-csv)."
25832619
),
25842620
)
25852621
args = parser.parse_args(argv)
25862622

2623+
if args.silent and not args.csv_report:
2624+
parser.error("--silent cannot be combined with --no-csv.")
2625+
25872626
_set_emoji_mode(not args.no_emoji)
25882627

25892628
if args.update:
2629+
if args.silent:
2630+
return 0
25902631
_text_show_version(pause_after=False, clear_screen=False)
25912632
return 0
25922633

2634+
if args.silent and not args.datastore and not args.searchpath and not args.all_guests:
2635+
return 2
2636+
2637+
if args.silent:
2638+
_enable_silent_output()
2639+
25932640
clear_console()
25942641

25952642
ensure_required_tools()

0 commit comments

Comments
 (0)