3 # On Ian's computers, mount received subvolumes after btrbk.
4 # Copyright (C) 2024 Ian Kelling
6 # This program is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or
9 # (at your option) any later version.
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19 # SPDX-License-Identifier: GPL-3.0-or-later
21 this_file
="$(readlink -f -- "${BASH_SOURCE[0]}")"
24 [[ $EUID == 0 ]] ||
exec sudo
-E "$this_file" "$@"
26 set -e; .
/usr
/local
/lib
/bash-bear
; set +e
31 Usage: ${0##*/} [OPTIONS] [SUBVOLUMES]
33 -h|--help Print help and exit.
34 -f|--force Use kill -9 to try fixing unmount errors
35 -v|--verbose Be more verbose
38 Note: In git this is not not executable because it's meant to be installed
39 using ./install-my-scripts
41 Note: Uses util-linux getopt option parsing: spaces between args and
42 options, short options can be combined, options before args.
47 pre
="mount-latest-subvol:${SSH_CLIENT:+ $HOSTNAME:}"
50 while read -r line
; do
52 grep -xFq "$line" "$file" ||
tee -a "$file"<<<"$line"
57 printf "$pre %s\n" "$*"
62 printf "$pre %s\n" "$*"
67 printf "$pre %s\n" "$*"
73 if ! mountpoint
-q $dir; then
79 while read -r start mpoint end
; do
80 l
="$start $mpoint $end"
81 # kill off any lines that duplicate the mount point.
82 sed --follow-symlinks -ri "\%$l%b;\%^\s*\S+\s+$mpoint\s%d" /etc
/fstab
88 for m
in ${my_pids[@]}; do
90 echo "$0: error: pids to kill includes our pid or a parent. ps output:" >&2
101 if pids
=$
(timeout
4 lsof
-t $dir); then
103 timeout
4 lsof
-w $dir
107 # fuser will find open sockets that lsof won't, for example from gpg-agent.
108 # note: -v shows kernel processes, which then doesn't return true when we want
109 if pids
=$
(timeout
4 fuser
-m $dir 2>/dev
/null
); then
112 fuser
-$sig -mvk $dir
115 if ! $found_pids; then
123 if mountpoint
-q $dir; then
124 if m umount
-R $dir; then
127 if ! kill-dir TERM TERM TERM INT INT HUP HUP TERM TERM TERM INT INT HUP HUP
; then
128 if $force; then kill-dir KILL
; fi
131 if m umount
-R $dir; then
134 echo "$0: failed to umount $dir"
142 # duplicated in check-subvol
143 # Reassign $1 var from /dev/dm- to corresponding /dev/mapper/
147 if [[ $devref == /dev
/dm-
* ]]; then
148 for mapdev
in /dev
/mapper
/*; do
149 if [[ $
(readlink
-f $mapdev) == "$devref" ]]; then
158 ##### begin command line parsing ########
160 # you can remove this if you do not have options which can have args with spaces or empty.
164 temp
=$
(getopt
-l help,force
,verbose hfv
"$@") || usage
1
168 -f|
--force) force
=true
;;
169 -v|
--verbose) verbose
=true
;;
172 *) echo "$0: unexpected args: $*" >&2 ; usage
1 ;;
180 all_vols
=(q a o i qd qr
)
181 ar_snaps
=(/mnt
/root
/btrbk
/ar.
*)
182 if [[ -e /mnt
/root
/ar ]] ||
(( ${#ar_snaps[@]} > 0 )); then
187 ##### end command line parsing ########
191 ##### begin setup fstab for subvols we care about ######
193 if [[ -e /mnt
/root
/root2-crypttab
]]; then
194 tu
/etc
/crypttab
</mnt
/root
/root2-crypttab
195 while read -r mapper_dev _
; do
196 if [[ ! -e /dev
/mapper
/$mapper_dev ]]; then
197 m cryptdisks_start
$mapper_dev
199 done < <(cat /mnt
/root
/root2-crypttab
)
201 if [[ -e /mnt
/root
/root2-fstab
]]; then
202 tu
/etc
/fstab
</mnt
/root
/root2-fstab
207 root_dev
=$
(awk '$2 == "/" {print $1}' /etc
/mtab
)
209 o_dev
=$
(awk '$2 == "/mnt/o" {print $1}' /etc
/mtab
)
213 # root2_dev=$(awk '$2 == "/mnt/root2" {print $1}' /etc/mtab)
214 # mapper-dev root2_dev
215 # # dont bother with the above for crypt2_dev
216 # crypt2_dev=$root2_dev
219 if cryptsetup status
$root_dev &>/dev
/null
; then
221 else # if we are in a recovery boot, find the next best crypt device
223 # todo: I think I had an idea to not setup /o in this case,
224 # but never finished implementing it
225 for dev
in $
(dmsetup
ls --target crypt |
awk '{print $1}'); do
227 if awk '{print $1}' /etc
/mtab |
grep -Fx $dev &>/dev
/null
; then
236 # dont tax the cpus of old laptops
237 if (( $
(nproc
) > 2)); then
238 mopts
+=,compress=zstd
242 $crypt_dev /a btrfs noatime,subvol=a$mopts 0 0
246 # ssh and probably some other things care about parent directory
247 # ownership, and ssh doesn\'t allow any group writable parent
248 # directories, so we are forced to use a directory structure similar
249 # to home directories
250 fa
=(/mnt
/root
/btrbk
/q.
*); f
=${fa[0]}
253 $crypt_dev /q btrfs noatime,subvol=q$mopts 0 0
254 $crypt_dev /qd btrfs noatime,subvol=qd$mopts 0 0
255 /q/p /p none bind$mopts 0 0
259 fa
=(/mnt
/root
/btrbk
/qr.
*); f
=${fa[0]}
262 $crypt_dev /qr btrfs noatime,subvol=qr$mopts 0 0
266 # not syncing ar at the moment
267 # fa=(/mnt/root/btrbk/ar.*); f=${fa[0]}
268 # if [[ -e $f ]]; then
270 # $crypt_dev /ar btrfs noatime,subvol=ar$mopts 0 0
275 fa
=(/mnt
/o
/btrbk
/o.
*); f
=${fa[0]}
277 if [[ $o_dev != "$root_dev" ]]; then
278 # ,compress=zstd regardless of mopts since these are all text files
279 # and it cuts disk use by about half.
281 $o_dev /o btrfs noatime,subvol=o${mopts/,compress=zstd/},compress=zstd 0 0
285 /o/m /m none bind$mopts 0 0
286 /o/debbugs /debbugs none bind$mopts 0 0
291 ##### end setup fstab for subvols we care about ######
293 ### begin get pids that this program depends on so we dont kill them
297 while [[ ${my_pids[-1]} != 1 && ${my_pids[-1]} != "${my_pids[-2]}" && $count -lt $loop_limit ]]; do
299 p=$(ps -p ${my_pids[-1]} -o ppid=)
300 if [[ $p == 0 || ! $p ]]; then
305 ### end get pids that this program depends on so we dont kill them
307 for vol in ${all_vols[@]}; do
309 if ! awk '$3 == "btrfs
" {print $2}' /etc/fstab | grep -xF $d &>/dev/null; then
314 ##### begin building up list of bind mounts ######
315 binds=() # list of bind mounts
319 for r in ${roots[@]}; do
320 # eg. when r=/q/p, for lines like
321 # /q/p /p none bind 0 0
323 new_roots+=("$
(sed -rn "s#^$r/\S+\s+(\S+)\s+none\s+(\S+,|)bind[[:space:],].*#\1#p" /etc
/fstab
)")
325 (( ${#new_roots} )) || break
326 binds+=(${new_roots[@]})
327 # roots is used to recursively find binds of binds if they exist.
328 roots=( ${new_roots[@]} )
330 ##### end building up list of bind mounts ######
334 # if latest is already mounted, make sure binds are mounted and move on
335 m check-subvol-stale $d
336 # populated by check-subvol-stale if stale
337 if ! fresh_snap=$(cat /nocow/btrfs-stale/$vol 2>/dev/null); then
340 for b in ${binds[@]}; do
341 if mountpoint -q $b; then
343 if [[ $did != "$bid" ]]; then
353 ##### begin checking for loopback mounts ####
355 for l in $(losetup -ln|awk '{print $6}'); do
356 for dir in $d ${binds[@]}; do
357 if [[ $l == $dir* ]]; then
358 echo "$0: found loopback mount
$l. giving up on unmounting
$dir"
371 ##### end end checking loopback mounts ####
374 ## not using arbtt at the moment
375 # if [[ $vol == q ]]; then
376 # ## allow to fail, user might not be logged in
377 # x sudo -u $(id -nu 1000) XDG_RUNTIME_DIR=/run/user/1000 systemctl --user stop arbtt ||:
381 for dir in $(echo $d ${binds[*]}\ |tac -s\ ); do
385 # if we unmounted some but not all, restore them and move on
386 if ! $umount_ret; then
387 for dir in ${unmounted[@]}; do
393 #### begin dealing with leaf vols ####
395 ### begin getting root_dir
396 ### this is duplicated in check-subvol-stale
398 dev=$(sed -rn "s
,^\s
*([^
#]\S*)\s+$d\s.*,\1,p" /etc/fstab /etc/mtab|head -n1)
400 # note, we need $dev because $d might not be mounted, and we do this loop
401 # because the device in fstab for the rootfs can be different.
402 for devx
in $
(btrfs fil show
$dev|
sed -rn 's#.*path (\S+)$#\1#p'); do
403 if [[ $devx == dm-
* ]]; then
408 root_dir
=$
(sed -rn "s,^\s*$devx\s+(\S+).*\bsubvolid=[05]\b.*,\1,p" /etc
/mtab
/etc
/fstab|
head -n1)
409 if [[ $root_dir ]]; then
414 if [[ ! $root_dir ]]; then
415 echo "$0: error could not find root subvol mount for $dev" >&2
418 ### end getting root_dir
421 if [[ -e $vol ]]; then
422 if [[ $vol == qd
]]; then
425 leaf
=$vol.leaf.$
(date +%Y-
%m-
%dT
%H
:%M
:%S
%z
)
427 m btrfs property
set -ts $leaf ro true
429 ### begin check if leaf is different, delete it if not ###
430 parentid
=$
(btrfs sub show
$leaf |
awk '$1 == "Parent" && $2 == "UUID:" {print $3}')
432 bsub
= # base subvolume
433 # go in reverse order as its more likely to be at the end
434 for ((i
=${#bsubs[@]}-1; i
>=0; i--
)); do
435 if [[ $parentid == $
(btrfs sub show
${bsubs[i]} |
awk '$1 == "UUID:" {print $2}') ]]; then
441 # in testing, same subvol is 136 bytes. allow some overhead. 32 happens sometimes under systemd.
443 # EPIPE 32 Broken pipe
444 lines
=$
(btrfs send
--no-data -p $bsub $leaf | btrfs receive
--dump |
head -n 100 |
wc -l ||
[[ $?
== 141 ||
${PIPESTATUS[0]} == 32 ]])
445 if [[ $lines == 0 ]]; then
446 # example output of no differences:
447 # snapshot ./qrtest uuid=c41ff6b7-0527-f34d-95ac-190eecf54ff5 transid=2239 parent_uuid=64949e1b-4a3e-3945-9a8e-cd7b7c15d7d6 parent_transid=2239
448 echo suspected identical
: $bsub $leaf
449 x btrfs sub del
$leaf
452 ### end check if leaf is different, delete it if not ###
454 ## begin expire leaf vols ##
455 leaf_vols
=($vol.leaf.
*)
456 count
=${#leaf_vols[@]}
457 leaf_limit_time
=$
(( EPOCHSECONDS
- 60*60*24*60 )) # 60 days
458 leaf_new_limit_time
=$
(( EPOCHSECONDS
- 60*60*24 * 5 )) # 5 days this
459 # goes backwards from oldest. leaf_new_limit_time is a safety
460 # measure to ensure we don't delete very recent leafs.
461 for leaf
in ${leaf_vols[@]}; do
462 leaf_time
=$
(date -d ${leaf#"$vol".leaf.} +%s
)
463 if (( leaf_limit_time
> leaf_time ||
( leaf_new_limit_time
> leaf_time
&& count
> 30 ) )); then
464 x btrfs sub del
$leaf
469 ## end expire leaf vols ##
471 #### end dealing with leaf vols ####
473 # Note, we make a few assumptions in this script, like
474 # $d was not a different subvol id than $vol, and
475 # things otherwise didn't get mounted very strangely.
476 m btrfs sub snapshot
$fresh_snap $vol
477 for dir
in $d ${binds[@]}; do
481 ## arbtt disabled for now
482 # if [[ $vol == q ]]; then
483 # # maybe this will fail if X is not running
484 # x sudo -u $(id -nu 1000) XDG_RUNTIME_DIR=/run/user/1000 systemctl --user start arbtt ||:
487 stale_dir
=/nocow
/btrfs-stale
493 for dir
in /mnt
/r
7/amy
/{root
/root
,boot
/boot
}_ubuntubionic
/mnt
/{root
2/root
,boot
2/boot
}_ubuntubionic
; do
496 if [[ ! -d $root_dir ]]; then
497 # this only exists on host kd currently
500 # if latest is already mounted, make sure binds are mounted and move on
501 m check-subvol-stale
-p $dir
502 # populated by check-subvol-stale if stale
503 if ! fresh_snap
=$
(cat /nocow
/btrfs-stale
/$vol 2>/dev
/null
); then
506 if [[ -d $dir ]]; then
507 if ! kill-dir TERM TERM TERM INT INT HUP HUP TERM TERM TERM INT INT HUP HUP
; then
508 if $force; then kill-dir KILL
; fi
512 m btrfs sub snapshot
$fresh_snap $dir
513 rm -f /nocow
/btrfs-stale
/$vol
516 if (( ret
>= 1 )); then
517 echo "$0: exit status $ret. see error above"