various fixes
[distro-setup] / mount-latest-subvol
1 #!/bin/bash
2
3 # On Ian's computers, mount received subvolumes after btrbk.
4 # Copyright (C) 2024 Ian Kelling
5
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.
10
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.
15
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/>.
18
19 # SPDX-License-Identifier: GPL-3.0-or-later
20
21 this_file="$(readlink -f -- "${BASH_SOURCE[0]}")"
22 readonly this_file
23 cd /
24 [[ $EUID == 0 ]] || exec sudo -E "$this_file" "$@"
25
26 set -e; . /usr/local/lib/bash-bear; set +e
27
28 usage() {
29 cat <<EOF
30 Usage: ${0##*/} [OPTIONS] [SUBVOLUMES]
31
32 -h|--help Print help and exit.
33 -f|--force Use kill -9 to try fixing unmount errors
34 -v|--verbose Be more verbose
35
36
37 Note: In git this is not not executable because it's meant to be installed
38 using ./install-my-scripts
39
40 Note: Uses util-linux getopt option parsing: spaces between args and
41 options, short options can be combined, options before args.
42 EOF
43 exit $1
44 }
45
46 pre="mount-latest-subvol:${SSH_CLIENT:+ $HOSTNAME:}"
47
48 tu() {
49 while read -r line; do
50 file="$1"
51 grep -xFq "$line" "$file" || tee -a "$file"<<<"$line"
52 done
53 }
54 d() {
55 if $verbose; then
56 printf "$pre %s\n" "$*"
57 fi
58 }
59 m() {
60 if $verbose; then
61 printf "$pre %s\n" "$*"
62 fi
63 "$@"
64 }
65 x() {
66 printf "$pre %s\n" "$*"
67 "$@"
68 }
69
70 mnt() {
71 dir=$1
72 if ! mountpoint -q $dir; then
73 mkdir -p $dir
74 m mount $dir
75 fi
76 }
77 fstab() {
78 while read -r start mpoint end; do
79 l="$start $mpoint $end"
80 # kill off any lines that duplicate the mount point.
81 sed --follow-symlinks -ri "\%$l%b;\%^\s*\S+\s+$mpoint\s%d" /etc/fstab
82 tu /etc/fstab <<<"$l"
83 done
84 }
85 pid-check() {
86 for p in ${pids}; do
87 for m in ${my_pids[@]}; do
88 if (( p == m )); then
89 echo "$0: error: pids to kill includes our pid or a parent. ps output:" >&2
90 ps -f -p $p
91 exit 1
92 fi
93 done
94 done
95 }
96 kill-dir() {
97 for sig; do
98 echo kill-dir $sig
99 found_pids=false
100 if pids=$(timeout 4 lsof -t $dir); then
101 found_pids=true
102 timeout 4 lsof -w $dir
103 pid-check
104 kill -$sig $pids
105 fi
106 # fuser will find open sockets that lsof won't, for example from gpg-agent.
107 # note: -v shows kernel processes, which then doesn't return true when we want
108 if pids=$(timeout 4 fuser -m $dir 2>/dev/null); then
109 pid-check
110 found_pids=true
111 fuser -$sig -mvk $dir
112 fi
113 sleep .5
114 if ! $found_pids; then
115 return 0
116 fi
117 done
118 return 1
119 }
120 umount-kill() {
121 dir=$1
122 if mountpoint -q $dir; then
123 if m umount -R $dir; then
124 unmounted+=($dir)
125 else
126 if ! kill-dir TERM TERM TERM INT INT HUP HUP TERM TERM TERM INT INT HUP HUP; then
127 if $force; then kill-dir KILL; fi
128 fi
129
130 if m umount -R $dir; then
131 unmounted+=($dir)
132 else
133 echo "$0: failed to umount $dir"
134 umount_ret=false
135 ret=1
136 fi
137 fi
138 fi
139 }
140
141 # duplicated in check-subvol
142 # Reassign $1 var from /dev/dm- to corresponding /dev/mapper/
143 mapper-dev() {
144 local mapdev
145 local -n devref=$1
146 if [[ $devref == /dev/dm-* ]]; then
147 for mapdev in /dev/mapper/*; do
148 if [[ $(readlink -f $mapdev) == "$devref" ]]; then
149 devref=$mapdev
150 break
151 fi
152 done
153 fi
154 }
155
156
157 ##### begin command line parsing ########
158
159 # you can remove this if you do not have options which can have args with spaces or empty.
160
161 verbose=true
162 force=false
163 temp=$(getopt -l help,force,verbose hfv "$@") || usage 1
164 eval set -- "$temp"
165 while true; do
166 case $1 in
167 -f|--force) force=true ;;
168 -v|--verbose) verbose=true ;;
169 -h|--help) usage ;;
170 --) shift; break ;;
171 *) echo "$0: unexpected args: $*" >&2 ; usage 1 ;;
172 esac
173 shift
174 done
175
176 if (( $# )); then
177 all_vols=( "$@" )
178 else
179 all_vols=(q a o i ar qd qr)
180 fi
181
182 ##### end command line parsing ########
183
184 ret=0
185
186 ##### begin setup fstab for subvols we care about ######
187
188 if [[ -e /mnt/root/root2-crypttab ]]; then
189 tu /etc/crypttab </mnt/root/root2-crypttab
190 while read -r mapper_dev _; do
191 if [[ ! -e /dev/mapper/$mapper_dev ]]; then
192 m cryptdisks_start $mapper_dev
193 fi
194 done < <(cat /mnt/root/root2-crypttab)
195 fi
196 if [[ -e /mnt/root/root2-fstab ]]; then
197 tu /etc/fstab </mnt/root/root2-fstab
198 mnt /mnt/root2
199 mnt /mnt/boot2
200 fi
201
202 root_dev=$(awk '$2 == "/" {print $1}' /etc/mtab)
203 mapper-dev root_dev
204 o_dev=$(awk '$2 == "/mnt/o" {print $1}' /etc/mtab)
205 mapper-dev o_dev
206
207
208 # root2_dev=$(awk '$2 == "/mnt/root2" {print $1}' /etc/mtab)
209 # mapper-dev root2_dev
210 # # dont bother with the above for crypt2_dev
211 # crypt2_dev=$root2_dev
212
213
214 if cryptsetup status $root_dev &>/dev/null; then
215 crypt_dev=$root_dev
216 else # if we are in a recovery boot, find the next best crypt device
217 mopts=,noauto
218 # todo: I think I had an idea to not setup /o in this case,
219 # but never finished implementing it
220 for dev in $(dmsetup ls --target crypt | awk '{print $1}'); do
221 dev=/dev/mapper/$dev
222 if awk '{print $1}' /etc/mtab | grep -Fx $dev &>/dev/null; then
223 crypt_dev=$dev
224 break
225 fi
226 done
227 fi
228
229
230
231 # dont tax the cpus of old laptops
232 if (( $(nproc) > 2)); then
233 mopts+=,compress=zstd
234 fi
235
236 fstab <<EOF
237 $crypt_dev /a btrfs noatime,subvol=a$mopts 0 0
238 EOF
239
240 shopt -s nullglob
241
242 # ssh and probably some other things care about parent directory
243 # ownership, and ssh doesn\'t allow any group writable parent
244 # directories, so we are forced to use a directory structure similar
245 # to home directories
246 fa=(/mnt/root/btrbk/q.*); f=${fa[0]}
247 if [[ -e $f ]]; then
248 fstab <<EOF
249 $crypt_dev /q btrfs noatime,subvol=q$mopts 0 0
250 $crypt_dev /qd btrfs noatime,subvol=qd$mopts 0 0
251 /q/p /p none bind$mopts 0 0
252 EOF
253 fi
254
255 fa=(/mnt/root/btrbk/qr.*); f=${fa[0]}
256 if [[ -e $f ]]; then
257 fstab <<EOF
258 $crypt_dev /qr btrfs noatime,subvol=qr$mopts 0 0
259 EOF
260 fi
261
262 # not syncing ar at the moment
263 # fa=(/mnt/root/btrbk/ar.*); f=${fa[0]}
264 # if [[ -e $f ]]; then
265 # fstab <<EOF
266 # $crypt_dev /ar btrfs noatime,subvol=ar,uid=1000$mopts 0 0
267 # EOF
268 # fi
269
270
271 fa=(/mnt/o/btrbk/o.*); f=${fa[0]}
272 if [[ -e $f ]]; then
273 if [[ $o_dev != "$root_dev" ]]; then
274 # ,compress=zstd regardless of mopts since these are all text files
275 # and it cuts disk use by about half.
276 fstab <<EOF
277 $o_dev /o btrfs noatime,subvol=o${mopts/,compress=zstd/},compress=zstd 0 0
278 EOF
279 fi
280 fstab <<EOF
281 /o/m /m none bind$mopts 0 0
282 /o/debbugs /debbugs none bind$mopts 0 0
283 EOF
284 fi
285
286
287 ##### end setup fstab for subvols we care about ######
288
289 ### begin get pids that this program depends on so we dont kill them
290 my_pids=($$ $PPID)
291 loop_limit=30
292 count=0
293 while [[ ${my_pids[-1]} != 1 && ${my_pids[-1]} != "${my_pids[-2]}" && $count -lt $loop_limit ]]; do
294 count=$((count + 1))
295 p=$(ps -p ${my_pids[-1]} -o ppid=)
296 if [[ $p == 0 || ! $p ]]; then
297 break
298 fi
299 my_pids+=($p)
300 done
301 ### end get pids that this program depends on so we dont kill them
302
303 for vol in ${all_vols[@]}; do
304 d=/$vol
305 if ! awk '$3 == "btrfs" {print $2}' /etc/fstab | grep -xF $d &>/dev/null; then
306 continue
307 fi
308
309
310 ##### begin building up list of bind mounts ######
311 binds=() # list of bind mounts
312 roots=($d)
313 while true; do
314 new_roots=()
315 for r in ${roots[@]}; do
316 # eg. when r=/q/p, for lines like
317 # /q/p /p none bind 0 0
318 # output /p
319 new_roots+=("$(sed -rn "s#^$r/\S+\s+(\S+)\s+none\s+(\S+,|)bind[[:space:],].*#\1#p" /etc/fstab)")
320 done
321 (( ${#new_roots} )) || break
322 binds+=(${new_roots[@]})
323 # roots is used to recursively find binds of binds if they exist.
324 roots=( ${new_roots[@]} )
325 done
326 ##### end building up list of bind mounts ######
327
328
329
330 # if latest is already mounted, make sure binds are mounted and move on
331 m check-subvol-stale $d
332 # populated by check-subvol-stale if stale
333 if ! fresh_snap=$(cat /nocow/btrfs-stale/$vol 2>/dev/null); then
334 mnt $d
335 did=$(stat -c%d $d)
336 for b in ${binds[@]}; do
337 if mountpoint -q $b; then
338 bid=$(stat -c%d $b)
339 if [[ $did != "$bid" ]]; then
340 umount-kill $b
341 fi
342 fi
343 mnt $b
344 done
345 continue
346 fi
347
348
349 ##### begin checking for loopback mounts ####
350 found_loop=false
351 for l in $(losetup -ln|awk '{print $6}'); do
352 for dir in $d ${binds[@]}; do
353 if [[ $l == $dir* ]]; then
354 echo "$0: found loopback mount $l. giving up on unmounting $dir"
355 ret=1
356 found_loop=true
357 break
358 fi
359 done
360 if $found_loop; then
361 break
362 fi
363 done
364 if $found_loop; then
365 continue
366 fi
367 ##### end end checking loopback mounts ####
368
369
370 ## not using arbtt at the moment
371 # if [[ $vol == q ]]; then
372 # ## allow to fail, user might not be logged in
373 # x sudo -u $(id -nu 1000) XDG_RUNTIME_DIR=/run/user/1000 systemctl --user stop arbtt ||:
374 # fi
375 umount_ret=true
376 unmounted=()
377 for dir in $(echo $d ${binds[*]}\ |tac -s\ ); do
378 umount-kill $dir
379 done
380
381 # if we unmounted some but not all, restore them and move on
382 if ! $umount_ret; then
383 for dir in ${unmounted[@]}; do
384 mnt $dir
385 done
386 continue
387 fi
388
389 #### begin dealing with leaf vols ####
390
391 ### begin getting root_dir
392 ### this is duplicated in check-subvol-stale
393
394 dev=$(sed -rn "s,^\s*([^#]\S*)\s+$d\s.*,\1,p" /etc/fstab /etc/mtab|head -n1)
395 d dev=$dev
396 # note, we need $dev because $d might not be mounted, and we do this loop
397 # because the device in fstab for the rootfs can be different.
398 for devx in $(btrfs fil show $dev| sed -rn 's#.*path (\S+)$#\1#p'); do
399 if [[ $devx == dm-* ]]; then
400 devx=/dev/$devx
401 mapper-dev devx
402 fi
403 d devx=$devx
404 root_dir=$(sed -rn "s,^\s*$devx\s+(\S+).*\bsubvolid=[05]\b.*,\1,p" /etc/mtab /etc/fstab|head -n1)
405 if [[ $root_dir ]]; then
406 d root_dir=$root_dir
407 break
408 fi
409 done
410 if [[ ! $root_dir ]]; then
411 echo "$0: error could not find root subvol mount for $dev" >&2
412 exit 1
413 fi
414 ### end getting root_dir
415
416 cd $root_dir
417 if [[ -e $vol ]]; then
418 if [[ $vol == qd ]]; then
419 m btrfs sub del qd
420 else
421 leaf=$vol.leaf.$(date +%Y-%m-%dT%H:%M:%S%z)
422 m mv $vol $leaf
423 m btrfs property set -ts $leaf ro true
424
425 ### begin check if leaf is different, delete it if not ###
426 parentid=$(btrfs sub show $leaf | awk '$1 == "Parent" && $2 == "UUID:" {print $3}')
427 bsubs=(btrbk/$vol.*)
428 bsub= # base subvolume
429 # go in reverse order as its more likely to be at the end
430 for ((i=${#bsubs[@]}-1; i>=0; i--)); do
431 if [[ $parentid == $(btrfs sub show ${bsubs[i]} | awk '$1 == "UUID:" {print $2}') ]]; then
432 bsub=${bsubs[i]}
433 break
434 fi
435 done
436 if [[ $bsub ]]; then
437 # in testing, same subvol is 136 bytes. allow some overhead. 32 happens sometimes under systemd.
438 # $ errno 32
439 # EPIPE 32 Broken pipe
440 lines=$(btrfs send --no-data -p $bsub $leaf | btrfs receive --dump | head -n 100 | wc -l || [[ $? == 141 || ${PIPESTATUS[0]} == 32 ]])
441 if [[ $lines == 0 ]]; then
442 # example output of no differences:
443 # snapshot ./qrtest uuid=c41ff6b7-0527-f34d-95ac-190eecf54ff5 transid=2239 parent_uuid=64949e1b-4a3e-3945-9a8e-cd7b7c15d7d6 parent_transid=2239
444 echo suspected identical: $bsub $leaf
445 x btrfs sub del $leaf
446 fi
447 fi
448 ### end check if leaf is different, delete it if not ###
449
450 ## begin expire leaf vols ##
451 leaf_vols=($vol.leaf.*)
452 count=${#leaf_vols[@]}
453 leaf_limit_time=$(( EPOCHSECONDS - 60*60*24*60 )) # 60 days
454 leaf_new_limit_time=$(( EPOCHSECONDS - 60*60*24 * 5 )) # 5 days this
455 # goes backwards from oldest. leaf_new_limit_time is a safety
456 # measure to ensure we don't delete very recent leafs.
457 for leaf in ${leaf_vols[@]}; do
458 leaf_time=$(date -d ${leaf#"$vol".leaf.} +%s)
459 if (( leaf_limit_time > leaf_time || ( leaf_new_limit_time > leaf_time && count > 30 ) )); then
460 x btrfs sub del $leaf
461 fi
462 count=$((count-1))
463 done
464 fi
465 ## end expire leaf vols ##
466 fi
467 #### end dealing with leaf vols ####
468
469 # Note, we make a few assumptions in this script, like
470 # $d was not a different subvol id than $vol, and
471 # things otherwise didn't get mounted very strangely.
472 m btrfs sub snapshot $fresh_snap $vol
473 for dir in $d ${binds[@]}; do
474 m mnt $dir
475 done
476
477 ## arbtt disabled for now
478 # if [[ $vol == q ]]; then
479 # # maybe this will fail if X is not running
480 # x sudo -u $(id -nu 1000) XDG_RUNTIME_DIR=/run/user/1000 systemctl --user start arbtt ||:
481 # fi
482
483 stale_dir=/nocow/btrfs-stale
484 rm -f $stale_dir/$d
485 done
486
487
488
489 for dir in /mnt/r7/amy/{root/root,boot/boot}_ubuntubionic /mnt/{root2/root,boot2/boot}_ubuntubionic; do
490 vol=${dir##*/}
491 root_dir=${dir%/*}
492 if [[ ! -d $root_dir ]]; then
493 # this only exists on host kd currently
494 continue
495 fi
496 # if latest is already mounted, make sure binds are mounted and move on
497 m check-subvol-stale -p $dir
498 # populated by check-subvol-stale if stale
499 if ! fresh_snap=$(cat /nocow/btrfs-stale/$vol 2>/dev/null); then
500 continue
501 fi
502 if [[ -d $dir ]]; then
503 if ! kill-dir TERM TERM TERM INT INT HUP HUP TERM TERM TERM INT INT HUP HUP; then
504 if $force; then kill-dir KILL; fi
505 fi
506 m btrfs sub del $dir
507 fi
508 m btrfs sub snapshot $fresh_snap $dir
509 rm -f /nocow/btrfs-stale/$vol
510 done
511
512 if (( ret >= 1 )); then
513 echo "$0: exit status $ret. see error above"
514 fi
515 exit $ret
516
517