fix vpn host naming
[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 shopt -s nullglob
28
29 usage() {
30 cat <<EOF
31 Usage: ${0##*/} [OPTIONS] [SUBVOLUMES]
32
33 -h|--help Print help and exit.
34 -f|--force Use kill -9 to try fixing unmount errors
35 -v|--verbose Be more verbose
36
37
38 Note: In git this is not not executable because it's meant to be installed
39 using ./install-my-scripts
40
41 Note: Uses util-linux getopt option parsing: spaces between args and
42 options, short options can be combined, options before args.
43 EOF
44 exit $1
45 }
46
47 pre="mount-latest-subvol:${SSH_CLIENT:+ $HOSTNAME:}"
48
49 tu() {
50 while read -r line; do
51 file="$1"
52 grep -xFq "$line" "$file" || tee -a "$file"<<<"$line"
53 done
54 }
55 d() {
56 if $verbose; then
57 printf "$pre %s\n" "$*"
58 fi
59 }
60 m() {
61 if $verbose; then
62 printf "$pre %s\n" "$*"
63 fi
64 "$@"
65 }
66 x() {
67 printf "$pre %s\n" "$*"
68 "$@"
69 }
70
71 mnt() {
72 dir=$1
73 if ! mountpoint -q $dir; then
74 mkdir -p $dir
75 m mount $dir
76 fi
77 }
78 fstab() {
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
83 tu /etc/fstab <<<"$l"
84 done
85 }
86 pid-check() {
87 for p in ${pids}; do
88 for m in ${my_pids[@]}; do
89 if (( p == m )); then
90 echo "$0: error: pids to kill includes our pid or a parent. ps output:" >&2
91 ps -f -p $p
92 exit 1
93 fi
94 done
95 done
96 }
97 kill-dir() {
98 for sig; do
99 echo kill-dir $sig
100 found_pids=false
101 if pids=$(timeout 4 lsof -t $dir); then
102 found_pids=true
103 timeout 4 lsof -w $dir
104 pid-check
105 kill -$sig $pids
106 fi
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
110 pid-check
111 found_pids=true
112 fuser -$sig -mvk $dir
113 fi
114 sleep .5
115 if ! $found_pids; then
116 return 0
117 fi
118 done
119 return 1
120 }
121 umount-kill() {
122 dir=$1
123 if mountpoint -q $dir; then
124 if m umount -R $dir; then
125 unmounted+=($dir)
126 else
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
129 fi
130
131 if m umount -R $dir; then
132 unmounted+=($dir)
133 else
134 echo "$0: failed to umount $dir"
135 umount_ret=false
136 ret=1
137 fi
138 fi
139 fi
140 }
141
142 # duplicated in check-subvol
143 # Reassign $1 var from /dev/dm- to corresponding /dev/mapper/
144 mapper-dev() {
145 local mapdev
146 local -n devref=$1
147 if [[ $devref == /dev/dm-* ]]; then
148 for mapdev in /dev/mapper/*; do
149 if [[ $(readlink -f $mapdev) == "$devref" ]]; then
150 devref=$mapdev
151 break
152 fi
153 done
154 fi
155 }
156
157
158 ##### begin command line parsing ########
159
160 # you can remove this if you do not have options which can have args with spaces or empty.
161
162 verbose=true
163 force=false
164 temp=$(getopt -l help,force,verbose hfv "$@") || usage 1
165 eval set -- "$temp"
166 while true; do
167 case $1 in
168 -f|--force) force=true ;;
169 -v|--verbose) verbose=true ;;
170 -h|--help) usage ;;
171 --) shift; break ;;
172 *) echo "$0: unexpected args: $*" >&2 ; usage 1 ;;
173 esac
174 shift
175 done
176
177 if (( $# )); then
178 all_vols=( "$@" )
179 else
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
183 all_vols+=(ar)
184 fi
185 fi
186
187 ##### end command line parsing ########
188
189 ret=0
190
191 ##### begin setup fstab for subvols we care about ######
192
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
198 fi
199 done < <(cat /mnt/root/root2-crypttab)
200 fi
201 if [[ -e /mnt/root/root2-fstab ]]; then
202 tu /etc/fstab </mnt/root/root2-fstab
203 mnt /mnt/root2
204 mnt /mnt/boot2
205 fi
206
207 root_dev=$(awk '$2 == "/" {print $1}' /etc/mtab)
208 mapper-dev root_dev
209 o_dev=$(awk '$2 == "/mnt/o" {print $1}' /etc/mtab)
210 mapper-dev o_dev
211
212
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
217
218
219 if cryptsetup status $root_dev &>/dev/null; then
220 crypt_dev=$root_dev
221 else # if we are in a recovery boot, find the next best crypt device
222 mopts=,noauto
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
226 dev=/dev/mapper/$dev
227 if awk '{print $1}' /etc/mtab | grep -Fx $dev &>/dev/null; then
228 crypt_dev=$dev
229 break
230 fi
231 done
232 fi
233
234
235
236 # dont tax the cpus of old laptops
237 if (( $(nproc) > 2)); then
238 mopts+=,compress=zstd
239 fi
240
241 fstab <<EOF
242 $crypt_dev /a btrfs noatime,subvol=a$mopts 0 0
243 EOF
244
245
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]}
251 if [[ -e $f ]]; then
252 fstab <<EOF
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
256 EOF
257 fi
258
259 fa=(/mnt/root/btrbk/qr.*); f=${fa[0]}
260 if [[ -e $f ]]; then
261 fstab <<EOF
262 $crypt_dev /qr btrfs noatime,subvol=qr$mopts 0 0
263 EOF
264 fi
265
266 # not syncing ar at the moment
267 # fa=(/mnt/root/btrbk/ar.*); f=${fa[0]}
268 # if [[ -e $f ]]; then
269 # fstab <<EOF
270 # $crypt_dev /ar btrfs noatime,subvol=ar$mopts 0 0
271 # EOF
272 # fi
273
274
275 fa=(/mnt/o/btrbk/o.*); f=${fa[0]}
276 if [[ -e $f ]]; then
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.
280 fstab <<EOF
281 $o_dev /o btrfs noatime,subvol=o${mopts/,compress=zstd/},compress=zstd 0 0
282 EOF
283 fi
284 fstab <<EOF
285 /o/m /m none bind$mopts 0 0
286 /o/debbugs /debbugs none bind$mopts 0 0
287 EOF
288 fi
289
290
291 ##### end setup fstab for subvols we care about ######
292
293 ### begin get pids that this program depends on so we dont kill them
294 my_pids=($$ $PPID)
295 loop_limit=30
296 count=0
297 while [[ ${my_pids[-1]} != 1 && ${my_pids[-1]} != "${my_pids[-2]}" && $count -lt $loop_limit ]]; do
298 count=$((count + 1))
299 p=$(ps -p ${my_pids[-1]} -o ppid=)
300 if [[ $p == 0 || ! $p ]]; then
301 break
302 fi
303 my_pids+=($p)
304 done
305 ### end get pids that this program depends on so we dont kill them
306
307 for vol in ${all_vols[@]}; do
308 d=/$vol
309 if ! awk '$3 == "btrfs" {print $2}' /etc/fstab | grep -xF $d &>/dev/null; then
310 continue
311 fi
312
313
314 ##### begin building up list of bind mounts ######
315 binds=() # list of bind mounts
316 roots=($d)
317 while true; do
318 new_roots=()
319 for r in ${roots[@]}; do
320 # eg. when r=/q/p, for lines like
321 # /q/p /p none bind 0 0
322 # output /p
323 new_roots+=("$(sed -rn "s#^$r/\S+\s+(\S+)\s+none\s+(\S+,|)bind[[:space:],].*#\1#p" /etc/fstab)")
324 done
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[@]} )
329 done
330 ##### end building up list of bind mounts ######
331
332
333
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
338 mnt $d
339 did=$(stat -c%d $d)
340 for b in ${binds[@]}; do
341 if mountpoint -q $b; then
342 bid=$(stat -c%d $b)
343 if [[ $did != "$bid" ]]; then
344 umount-kill $b
345 fi
346 fi
347 mnt $b
348 done
349 continue
350 fi
351
352
353 ##### begin checking for loopback mounts ####
354 found_loop=false
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"
359 ret=1
360 found_loop=true
361 break
362 fi
363 done
364 if $found_loop; then
365 break
366 fi
367 done
368 if $found_loop; then
369 continue
370 fi
371 ##### end end checking loopback mounts ####
372
373
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 ||:
378 # fi
379 umount_ret=true
380 unmounted=()
381 for dir in $(echo $d ${binds[*]}\ |tac -s\ ); do
382 umount-kill $dir
383 done
384
385 # if we unmounted some but not all, restore them and move on
386 if ! $umount_ret; then
387 for dir in ${unmounted[@]}; do
388 mnt $dir
389 done
390 continue
391 fi
392
393 #### begin dealing with leaf vols ####
394
395 ### begin getting root_dir
396 ### this is duplicated in check-subvol-stale
397
398 dev=$(sed -rn "s,^\s*([^#]\S*)\s+$d\s.*,\1,p" /etc/fstab /etc/mtab|head -n1)
399 d dev=$dev
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
404 devx=/dev/$devx
405 mapper-dev devx
406 fi
407 d devx=$devx
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
410 d root_dir=$root_dir
411 break
412 fi
413 done
414 if [[ ! $root_dir ]]; then
415 echo "$0: error could not find root subvol mount for $dev" >&2
416 exit 1
417 fi
418 ### end getting root_dir
419
420 cd $root_dir
421 if [[ -e $vol ]]; then
422 if [[ $vol == qd ]]; then
423 m btrfs sub del qd
424 else
425 leaf=$vol.leaf.$(date +%Y-%m-%dT%H:%M:%S%z)
426 m mv $vol $leaf
427 m btrfs property set -ts $leaf ro true
428
429 ### begin check if leaf is different, delete it if not ###
430 parentid=$(btrfs sub show $leaf | awk '$1 == "Parent" && $2 == "UUID:" {print $3}')
431 bsubs=(btrbk/$vol.*)
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
436 bsub=${bsubs[i]}
437 break
438 fi
439 done
440 if [[ $bsub ]]; then
441 # in testing, same subvol is 136 bytes. allow some overhead. 32 happens sometimes under systemd.
442 # $ errno 32
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
450 fi
451 fi
452 ### end check if leaf is different, delete it if not ###
453
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
465 fi
466 count=$((count-1))
467 done
468 fi
469 ## end expire leaf vols ##
470 fi
471 #### end dealing with leaf vols ####
472
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
478 m mnt $dir
479 done
480
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 ||:
485 # fi
486
487 stale_dir=/nocow/btrfs-stale
488 rm -f $stale_dir/$d
489 done
490
491
492
493 for dir in /mnt/r7/amy/{root/root,boot/boot}_ubuntubionic /mnt/{root2/root,boot2/boot}_ubuntubionic; do
494 vol=${dir##*/}
495 root_dir=${dir%/*}
496 if [[ ! -d $root_dir ]]; then
497 # this only exists on host kd currently
498 continue
499 fi
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
504 continue
505 fi
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
509 fi
510 m btrfs sub del $dir
511 fi
512 m btrfs sub snapshot $fresh_snap $dir
513 rm -f /nocow/btrfs-stale/$vol
514 done
515
516 if (( ret >= 1 )); then
517 echo "$0: exit status $ret. see error above"
518 fi
519 exit $ret
520
521