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