"Why does
df -hsay I have 2.6TB free but BTRFS thinks it's 3.5TB?"Because
dfis wrong — and nobody else fixes this.
ASG (Aldertech Storage Governor) is a lightweight, zero-dependency Python tool that provides accurate health monitoring, capacity planning, and intelligent scrub scheduling for BTRFS RAID1 pools — especially those with mismatched drive sizes.
If you've ever mixed a 4TB drive with two 1TB drives and wondered why your free space numbers don't add up, ASG is for you.
BTRFS RAID1 with mismatched drives is powerful but poorly served by standard tools:
df -his mathematically wrong. It doesn't account for RAID1 pairing constraints on mismatched drives.btrfs scrubis fire-and-forget. The standard approach (btrfs scrub start -Bd) blocks indefinitely with zero awareness of system load — your media server stutters while the scrub runs.- Metadata concentration is invisible. BTRFS may silently place all metadata chunks on your two smallest drives. If either fails, the entire pool is unrecoverable. No standard tool warns you about this.
- No capacity prediction. Without historical tracking, you won't know your pool is filling up until it's too late.
ASG provides five integrated governance modules:
- Real Capacity Engine — Calculates true RAID1 free space using the formula
min(sum/2, sum - largest), tracks historical usage, and predicts days-to-full. - Heuristic Scrub Controller — Runs scrubs in the background, monitoring
/proc/loadavgand/proc/diskstats. Automatically pauses when your system is busy and resumes when idle. - Integrity Monitor — Polls
btrfs device statsfor all drives, detects errors, and sends alerts with deduplication. - Metadata Concentration Watchdog — Warns if metadata/system chunks are dangerously concentrated on a subset of drives.
- Snapshot Bridge — Creates atomic read-only snapshots before your backup software runs, with automatic cleanup.
- Zero dependencies — Python standard library only (PyYAML optional for config)
- Accurate RAID1 maths for 2, 3, 4, or more mismatched drives
- Load-aware scrub throttling using
/proc/loadavgand/proc/diskstats - Metadata concentration alerts — catches the silent killer
- Days-to-full prediction from historical usage data
- Pre-backup atomic snapshots with retention management
- Pool UUID validation before every operation (prevents wrong-pool disasters)
--dry-runon all filesystem-modifying commands- Notifications — Discord, NTFY, and Gotify (all optional)
- Config-driven — single YAML file, sensible defaults
| Requirement | Minimum | Notes |
|---|---|---|
| Linux kernel | 4.0+ | Any kernel with BTRFS support |
| btrfs-progs | 5.0+ | sudo apt install btrfs-progs |
| Python | 3.9+ | |
| BTRFS profile | RAID1 | RAID1C3/RAID1C4 should also work |
| sudo | Required | BTRFS commands require root |
| PyYAML | Optional | For config file; falls back to defaults without it |
pip3 install aldertech-asg# 1. Clone the repository
git clone https://github.com/jaldertech/asg.git
cd asg
# 2. Run the installer (requires root)
sudo bash setup.sh
# 3. Edit your config
sudo nano /etc/asg/config.yaml
# 4. Test it
asg statusgit clone https://github.com/jaldertech/asg.git
cd asg
python3 -m asg statusCopy config.yaml to /etc/asg/config.yaml and edit it:
pool:
mount: "/mnt/my_pool"
# uuid: "optional-pin-for-safety"
scrub:
load_threshold: 3.0 # Pause scrub above this 1-min load average
io_threshold_percent: 40.0 # Pause if any drive exceeds this I/O %
capacity:
chunk_fullness_warn_percent: 90.0
free_space_warn_percent: 10.0
metadata_min_device_count: 3 # Warn if metadata on fewer devices
snapshots:
directory: ".snapshots" # Relative to pool mount
retention_days: 3
# Optional — remove if you don't want notifications
notifications:
discord:
webhook_url: "https://discord.com/api/webhooks/..."If PyYAML is not installed, the tool runs with built-in defaults (mount point: /mnt/media_pool).
# Full status overview (integrity + capacity + snapshots)
asg status
# Real capacity report with days-to-full prediction
asg capacity
# Run a load-aware scrub (pauses during high I/O)
asg scrub --dry-run # Preview first
asg scrub # Run for real
# Check all drives for errors
asg check
# Pre-backup snapshot (cleanup old + create new)
asg snapshot --dry-run
asg snapshot
# Clean up expired snapshots only
asg cleanup
# Use a custom config path
asg --config /path/to/config.yaml statusASG Capacity Report — /mnt/media_pool
Timestamp: 2026-03-07 19:45:57
=================================================================
Total Raw Capacity: 7454.1 GiB
Largest Drive: 3727.4 GiB
RAID1 Usable Ceiling: 3726.7 GiB
Data Used: 100.5 GiB
Real Free Space: 3625.8 GiB
Utilisation: 2.7%
Days to Full: ~342
Per-Device Allocation:
Device Data Meta System Unalloc Total
---------------------------------------------------------------
/dev/sdc 103.0G 1.000G 0.031G 1761.3G 1863.7G
/dev/sdd 30.0G 1.000G 0.008G 900.5G 931.5G
/dev/sde 25.0G 1.000G 0.008G 905.5G 931.5G
/dev/sdf 50.0G 1.000G 0.031G 3676.2G 3727.4G
WARNINGS:
! Data chunks are 96.6% full — new chunk allocation imminent.
=================================================================
Add to your crontab (crontab -e):
# Daily capacity report at 00:20
20 0 * * * asg capacity >> ~/asg.log 2>&1
# Weekly throttled scrub on Sunday at 02:00
0 2 * * 0 asg scrub >> ~/asg.log 2>&1
# Daily pre-backup snapshot at 02:00
0 2 * * * asg snapshot >> ~/asg.log 2>&1Standard df divides total raw space by the data ratio (2.0 for RAID1), giving sum / 2. But this ignores the pairing constraint: each RAID1 chunk must fit on two different drives.
For mismatched drives, the real limit is:
usable = min(sum_of_all_drives / 2, sum_of_all_drives - largest_drive)
Example: 4TB + 2TB + 1TB + 1TB = 8TB raw
| Calculation | Result | Why |
|---|---|---|
sum / 2 |
4 TB | Standard RAID1 formula |
sum - largest |
4 TB | The 4TB drive can only mirror 4TB of data across the other 3 drives |
| Usable ceiling | 4 TB | min(4, 4) — balanced in this case |
But with 8TB + 1TB + 1TB = 10TB raw:
| Calculation | Result | Why |
|---|---|---|
sum / 2 |
5 TB | Looks generous |
sum - largest |
2 TB | The 8TB drive can only mirror 2TB across the others |
| Usable ceiling | 2 TB | 6TB of the 8TB drive is wasted |
This tool calculates the correct number automatically.
| Hardware | OS | Pool Layout | Status |
|---|---|---|---|
| Raspberry Pi 5 (16GB) | Raspberry Pi OS Bookworm | 4TB + 2TB + 1TB + 1TB RAID1 | Verified |
ASG uses generic Linux interfaces (/proc, btrfs-progs). It should work on any Linux system with BTRFS. If you test on other hardware, please open a PR to add it to this table.
All backends are optional and independent. Configure any combination in config.yaml:
| Backend | Config Key | Notes |
|---|---|---|
| Discord | notifications.discord.webhook_url |
Standard Discord webhook |
| NTFY | notifications.ntfy.url |
Full topic URL |
| Gotify | notifications.gotify.url + .token |
Base URL + app token |
ASG runs btrfs commands via sudo. It requires that the executing user has passwordless sudo for btrfs (standard on Raspberry Pi OS) or is run as root.
The tool:
- Never writes to arbitrary paths (only state files in a configured directory)
- Validates the pool UUID before every operation
- Supports
--dry-runfor all destructive commands - Has zero network dependencies (notifications are opt-in)
- ADRG — Aldertech Dynamic Resource Governor: Automatic CPU/RAM management for high-density Docker stacks.
MIT — see LICENCE file.
Built by Aldertech
