Skip to content

Instantly share code, notes, and snippets.

@Ropid
Last active May 8, 2026 20:15
Show Gist options
  • Select an option

  • Save Ropid/e48b6055c7b0948aaa734e8ef4ba5d37 to your computer and use it in GitHub Desktop.

Select an option

Save Ropid/e48b6055c7b0948aaa734e8ef4ba5d37 to your computer and use it in GitHub Desktop.
btrfs balance maintenance script
#!/bin/bash
### Configuration ##############################################################
services_to_stop=(
backup.timer
)
services_to_wait_for=(
backup.service
)
################################################################################
set -o pipefail
mapfile -t mountpoints < <(
findmnt --types btrfs --nofsroot --noheading --output SOURCE |
sort -u |
while IFS= read -r device; do
findmnt --source "$device" --output TARGET --noheading | head -1
done
)
usage() {
cat << EOF
Usage: ${0##*/} [scrub] [MOUNTPOINT [...]]
Runs a sequence of btrfs balance operations.
Filesystems to run on:
$(printf " * %s\n" "${mountpoints[@]}")
Services to stop or wait for:
$(printf " * %s\n" "${services_to_stop[@]}" "${services_to_wait_for[@]}")
Options:
scrub Run a btrfs scrub as well.
EOF
}
for x; do
if [[ $x = "scrub" ]]; then
scrub=1
elif [[ $x = /* ]]; then
mount_args=1
else
usage
exit 1
fi
done
[[ $UID -ne 0 ]] && exec sudo "$0" "$@"
stopped_services=()
restart_services() {
local unit
for unit in "${stopped_services[@]}"; do
if systemctl start "$unit"; then
echo "Started '$unit'."
else
echo "Failed to start '$unit'!"
fi
done
}
trap 'echo -e "\nInterrupted!"; exit 1' INT TERM
trap 'restart_services' EXIT
for unit in "${services_to_stop[@]}"; do
if systemctl status "$unit" &> /dev/null; then
systemctl stop "$unit"
echo "Stopped '$unit'."
stopped_services+=("$unit")
fi
done
for unit in "${services_to_wait_for[@]}"; do
if systemctl status "$unit" &> /dev/null; then
echo -n "Waiting for '$unit' to complete... "
sleep 1
while systemctl status "$unit" &> /dev/null; do
sleep 1
done
echo "done."
fi
done
die() {
cat << EOF
+----------------------+
| There were errors! |
+----------------------+
EOF
exit 1
}
msg() {
sed 's/^/ /'
}
for mount in "${mountpoints[@]}"; do
if (( mount_args )); then
skip_mount=1
for x; do
x="$(sed -r 's/\/{2,}/\//g; s/.\/$//' <<< "$x")"
if [[ $x = $mount ]]; then
skip_mount=0
fi
done
if (( skip_mount )); then
continue
fi
fi
back="----------------------------------------"
echo "== $mount ${back:${#mount}}"
if (( scrub )); then
echo "> Scrub..."
# progress bar when running in a terminal
if [[ -t 1 ]]; then
while sleep 1; do
btrfs scrub status "$mount" |
perl -n0777e '
sub quit { print " Cannot read scrub status!\e[K\r"; exit 1; }
my ($bytes, $progress) = /^bytes scrubbed:\s*(\S+)\s+.*?([\d.]+)%/im
or quit;
my ($timeleft) = /^time left:\s*(\S+)/im or quit;
my ($rate) = /^rate:\s*(.*)/im or quit;
$bytes =~ s{(\d+\.\d+)([GMKB])?}{
!$2 ? sprintf "%.1f ", $1 : sprintf "%.0f %s", $1, $2 }eg;
$rate =~ s{(\d+\.\d+)([MKB])?}{
!$2 ? sprintf "%.1f ", $1 : sprintf "%.0f %s", $1, $2 }eg;
my $width = 20;
my $bar = int($progress * $width / 100 + 0.5);
printf " ETA: %s [%s>%s] %s, %s\e[K\r",
$timeleft,
"=" x $bar,
" " x ($width - $bar),
$bytes,
$rate;
'
done &
status=$(btrfs scrub start -B "$mount" 2>&1)
error=$?
kill %
tput cr; tput el # clear line
msg <<< "$status"
(( error )) && die
else
btrfs scrub start -B "$mount" |& msg || die
fi
fi
#for x in 1 5 10 20 30; do
for x in 0 5; do
echo "> Metadata balancing, usage=$x..."
btrfs balance start -musage="$x" "$mount" |& msg || die
done
#for x in 1 5 10 20 30 40 50; do
for x in 0 5 10; do
echo "> Data balancing, usage=$x..."
btrfs balance start -dusage="$x" "$mount" |& msg || die
done
done
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment