d2e41f33f7e4f65388019d28b068d8860ebad8aa
[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 script=$(readlink -f -- "$BASH_SOURCE")
17 cd /
18 [[ $EUID == 0 ]] || exec sudo -E "$script" "$@"
19
20 source /usr/local/lib/err
21
22 usage() {
23 cat <<EOF
24 Usage: ${0##*/} [OPTIONS]
25
26 -h|--help Print help and exit.
27 -f|--force Use kill -9 to try fixing unmount errors
28 -v|--verbose Be more verbose
29
30
31 Note, at source location, intentionally not executable, run and read
32 install-my-scripts.
33
34 Note: Uses util-linux getopt option parsing: spaces between args and
35 options, short options can be combined, options before args.
36 EOF
37 exit $1
38 }
39
40 all_vols=(q a o i ar qr)
41
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 "%s\n" "$*"
52 fi
53 }
54 m() {
55 if $verbose; then
56 printf "%s\n" "$*"
57 fi
58 "$@"
59 }
60 x() {
61 printf "%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 ##### end command line parsing ########
172
173 ret=0
174
175 ##### begin setup fstab for subvols we care about ######
176
177 if [[ -e /mnt/root/root2-crypttab ]]; then
178 tu /etc/crypttab </mnt/root/root2-crypttab
179 while read -r mapper_dev _; do
180 if [[ ! -e /dev/mapper/$mapper_dev ]]; then
181 m cryptdisks_start $mapper_dev
182 fi
183 done < <(cat /mnt/root/root2-crypttab)
184 fi
185 if [[ -e /mnt/root/root2-fstab ]]; then
186 tu /etc/fstab </mnt/root/root2-fstab
187 mnt /mnt/root2
188 mnt /mnt/boot2
189 fi
190
191 do_o=true
192 root_dev=$(awk '$2 == "/" {print $1}' /etc/mtab)
193 mapper-dev root_dev
194 o_dev=$(awk '$2 == "/mnt/o" {print $1}' /etc/mtab)
195 mapper-dev o_dev
196
197 if [[ $o_dev == "$root_dev" ]]; then
198 do_o=false
199 fi
200
201 # root2_dev=$(awk '$2 == "/mnt/root2" {print $1}' /etc/mtab)
202 # mapper-dev root2_dev
203 # # dont bother with the above for crypt2_dev
204 # crypt2_dev=$root2_dev
205
206
207 if cryptsetup status $root_dev &>/dev/null; then
208 crypt_dev=$root_dev
209 else # if we are in a recovery boot, find the next best crypt device
210 mopts=,noauto
211 do_o=false
212 for dev in $(dmsetup ls --target crypt | awk '{print $1}'); do
213 dev=/dev/mapper/$dev
214 if awk '{print $1}' /etc/mtab | grep -Fx $dev &>/dev/null; then
215 crypt_dev=$dev
216 break
217 fi
218 done
219 fi
220
221
222
223 # dont tax the cpus of old laptops
224 if ((`nproc` > 2)); then
225 mopts+=,compress=zstd
226 fi
227
228 fstab <<EOF
229 $crypt_dev /a btrfs noatime,subvol=a$mopts 0 0
230 EOF
231
232 shopt -s nullglob
233
234 # ssh and probably some other things care about parent directory
235 # ownership, and ssh doesn\'t allow any group writable parent
236 # directories, so we are forced to use a directory structure similar
237 # to home directories
238 f=(/mnt/root/btrbk/q.*); f=${f[0]}
239 if [[ -e $f ]]; then
240 fstab <<EOF
241 $crypt_dev /q btrfs noatime,subvol=q,gid=1000$mopts 0 0
242 /q/p /p none bind$mopts 0 0
243 EOF
244 fi
245
246 f=(/mnt/o/btrbk/o.*); f=${f[0]}
247 if [[ -e $f ]]; then
248 fstab <<EOF
249 $o_dev /o btrfs noatime,subvol=o$mopts 0 0
250 /o/m /m none bind$mopts 0 0
251 EOF
252 else
253 do_o=false
254 fi
255
256 if [[ $HOSTNAME == frodo ]]; then
257 fstab <<EOF
258 $crypt_dev /i btrfs noatime,subvol=i$mopts 0 0
259 EOF
260 fi
261
262
263
264 ##### end setup fstab for subvols we care about ######
265
266 ### begin get pids that this program depends on so we dont kill them
267 my_pids=($$ $PPID)
268 loop_limit=30
269 count=0
270 while [[ ${my_pids[-1]} != 1 && ${my_pids[-1]} != ${my_pids[-2]} && $count -lt $loop_limit ]]; do
271 count=$((count + 1))
272 p=$(ps -p ${my_pids[-1]} -o ppid=)
273 if [[ $p == 0 || ! $p ]]; then
274 break
275 fi
276 my_pids+=($p)
277 done
278 ### end get pids that this program depends on so we dont kill them
279
280 for vol in ${all_vols[@]}; do
281 d=/$vol
282 if ! awk '{print $2}' /etc/fstab | grep -xF $d &>/dev/null; then
283 continue
284 fi
285
286
287 ##### begin building up list of bind mounts ######
288 binds=() # list of bind mounts
289 roots=($d)
290 while true; do
291 new_roots=()
292 for r in ${roots[@]}; do
293 # eg. when r=/q/p, for lines like
294 # /q/p /p none bind 0 0
295 # output /p
296 new_roots+=($(sed -rn "s#^$r/\S+\s+(\S+)\s+none\s+(\S+,|)bind[[:space:],].*#\1#p" /etc/fstab))
297 done
298 (( ${#new_roots} )) || break
299 binds+=(${new_roots[@]})
300 # roots is used to recursively find binds of binds if they exist.
301 roots=( ${new_roots[@]} )
302 done
303 ##### end building up list of bind mounts ######
304
305
306 # if latest is already mounted, make sure binds are mounted and move on
307 m check-subvol-stale $d
308 # populated by check-subvol-stale if stale
309 if ! fresh_snap=$(cat /nocow/btrfs-stale/$vol 2>/dev/null); then
310 mnt $d
311 did=$(stat -c%d $d)
312 for b in ${binds[@]}; do
313 if mountpoint -q $b; then
314 bid=$(stat -c%d $b)
315 if [[ $did != $bid ]]; then
316 umount-kill $b
317 fi
318 fi
319 mnt $b
320 done
321 continue
322 fi
323
324 ## not using arbtt at the moment
325 # if [[ $vol == q ]]; then
326 # ## allow to fail, user might not be logged in
327 # x sudo -u $(id -nu 1000) XDG_RUNTIME_DIR=/run/user/1000 systemctl --user stop arbtt ||:
328 # fi
329 umount_ret=true
330 unmounted=()
331 for dir in $(echo $d ${binds[*]}\ |tac -s\ ); do
332 umount-kill $dir
333 done
334
335 # if we unmounted some but not all, restore them and move on
336 if ! $umount_ret; then
337 for dir in ${unmounted[@]}; do
338 mnt $dir
339 done
340 continue
341 fi
342
343 #### begin dealing with leaf vols ####
344
345 ### begin getting root_dir
346 ### this is duplicated in check-subvol-stale
347
348 dev=$(sed -rn "s,^\s*([^#]\S*)\s+$d\s.*,\1,p" /etc/fstab /etc/mtab|head -n1)
349 d dev=$dev
350 # note, we need $dev because $d might not be mounted, and we do this loop
351 # because the device in fstab for the rootfs can be different.
352 for devx in $(btrfs fil show $dev| sed -rn 's#.*path (\S+)$#\1#p'); do
353 if [[ $devx == dm-* ]]; then
354 devx=/dev/$devx
355 mapper-dev devx
356 fi
357 d devx=$devx
358 root_dir=$(sed -rn "s,^\s*$devx\s+(\S+).*\bsubvolid=[05]\b.*,\1,p" /etc/mtab /etc/fstab|head -n1)
359 if [[ $root_dir ]]; then
360 d root_dir=$root_dir
361 break
362 fi
363 done
364 if [[ ! $root_dir ]]; then
365 echo "$0: error could not find root subvol mount for $dev" >&2
366 exit 1
367 fi
368 ### end getting root_dir
369
370 cd $root_dir
371 if [[ -e $vol ]]; then
372 leaf=$vol.leaf.$(date +%Y-%m-%dT%H:%M:%S%z)
373 m mv $vol $leaf
374 m btrfs property set -ts $leaf ro true
375
376 ### begin check if leaf is different, delete it if not ###
377 if [[ -e /a/opt/btrfs-snapshots-diff/btrfs-snapshots-diff.py ]]; then
378 source /a/bin/distro-functions/src/package-manager-abstractions
379 #pi python-jmespath # dependency of btrfs-snapshots-diff
380 # todo: need python3 port of btrfs-snapshots-diff, py2 no exist on nabia
381 parentid=$(btrfs sub show $leaf | awk '$1 == "Parent" && $2 == "UUID:" {print $3}')
382 bsubs=(btrbk/$vol.*)
383 bsub=
384 # go in reverse order as its more likely to be at the end
385 for ((i=${#bsubs[@]}-1; i>=0; i--)); do
386 if [[ $parentid == $(btrfs sub show ${bsubs[i]} | awk '$1 == "UUID:" {print $2}') ]]; then
387 bsub=${bsubs[i]}
388 break
389 fi
390 done
391 if [[ $bsub ]]; then
392 tmp=$(mktemp)
393 # in testing, same subvol is 136 bytes. allow some overhead. 32 happens sometimes under systemd.
394 # $ errno 32
395 # EPIPE 32 Broken pipe
396 btrfs send --no-data -p $bsub $leaf | head -c 1000 > $tmp || [[ $? == 141 || ${PIPESTATUS[0]} == 32 ]]
397 if (( $(stat -c%s $tmp) < 1000)); then
398 # example output for an empty diff:
399 # Found a valid Btrfs stream header, version 1
400 # o.leaf.2019-05-15T14:00:50-0400;snapshot: uuid=ba045ea30737dd449003f1ee40ec12d0, ctrasid=109533, clone_uuid=3c7e3544e486834aa71d89e5b8f30056, clone_ctransid=109533
401 lines=$(/a/opt/btrfs-snapshots-diff/btrfs-snapshots-diff.py -s -f $tmp | \
402 grep -vxF "Found a valid Btrfs stream header, version 1" | \
403 grep -cv "^[^;]*;snapshot: ") ||:
404 if [[ $lines == 0 ]]; then
405 # rotate in case we find a bug, weve got 2 old ones
406 tmpleaf=($vol.tmpleaf2.*)
407 if (( ${#tmpleaf[@]} )); then
408 x btrfs sub del ${tmpleaf[@]}
409 fi
410 tmpleaf=($vol.tmpleaf1.*)
411 if (( ${#tmpleaf[@]} )); then
412 x mv ${tmpleaf[0]} $vol.tmpleaf2.${tmpleaf[0]#$vol.tmpleaf1.}
413 fi
414 echo suspected identical: $bsub $leaf
415 x mv $leaf $vol.tmpleaf1.${leaf#$vol.leaf.}
416 fi
417 fi
418 fi
419 fi
420 ### end check if leaf is different, delete it if not ###
421
422 ## begin expire leaf vols ##
423 leaf_vols=($vol.leaf.*)
424 count=${#leaf_vols[@]}
425 leaf_limit_time=$(( EPOCHSECONDS - 60*60*24*60 )) # 60 days
426 leaf_new_limit_time=$(( EPOCHSECONDS - 60*60*24 )) # 1 day
427 # this goes backwards from oldest. leaf_new_limit_time is just in case
428 # the order gets screwed up or something.
429 for leaf in ${leaf_vols[@]}; do
430 leaf_time=$(date -d ${leaf#$vol.leaf.} +%s)
431 if (( leaf_limit_time > leaf_time || ( leaf_new_limit_time > leaf_time && count > 15 ) )); then
432 x btrfs sub del $leaf
433 fi
434 count=$((count-1))
435 done
436 ## end expire leaf vols ##
437 fi
438 #### end dealing with leaf vols ####
439
440 # Note, we make a few assumptions in this script, like
441 # $d was not a different subvol id than $vol, and
442 # things otherwise didn't get mounted very strangely.
443 m btrfs sub snapshot $fresh_snap $vol
444 for dir in $d ${binds[@]}; do
445 m mnt $dir
446 done
447
448 ## arbtt disabled for now
449 # if [[ $vol == q ]]; then
450 # # maybe this will fail if X is not running
451 # x sudo -u $(id -nu 1000) XDG_RUNTIME_DIR=/run/user/1000 systemctl --user start arbtt ||:
452 # fi
453
454 stale_dir=/nocow/btrfs-stale
455 rm -f $stale_dir/$d
456 done
457
458
459
460 for dir in /mnt/r7/amy/{root/root,boot/boot}_ubuntubionic /mnt/{root2/root,boot2/boot}_ubuntubionic; do
461 vol=${dir##*/}
462 root_dir=${dir%/*}
463 if [[ ! -d $root_dir ]]; then
464 # this only exists on host kd currently
465 continue
466 fi
467 # if latest is already mounted, make sure binds are mounted and move on
468 m check-subvol-stale -p $dir
469 # populated by check-subvol-stale if stale
470 if ! fresh_snap=$(cat /nocow/btrfs-stale/$vol 2>/dev/null); then
471 continue
472 fi
473 if [[ -d $dir ]]; then
474 if ! kill-dir TERM TERM TERM INT INT HUP HUP TERM TERM TERM INT INT HUP HUP; then
475 if $force; then kill-dir KILL; fi
476 fi
477 m btrfs sub del $dir
478 fi
479 m btrfs sub snapshot $fresh_snap $dir
480 rm -f /nocow/btrfs-stale/$vol
481 done
482
483 exit $ret
484
485