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