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