various 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 script=$(readlink -f -- "$BASH_SOURCE")
17 cd /
18 [[ $EUID == 0 ]] || exec sudo -E "$script" "$@"
19
20
21 usage() {
22 cat <<EOF
23 Usage: ${0##*/} [OPTIONS]
24
25 -h|--help Print help and exit.
26
27
28 Note, at source location, intentionally not executable, run and read
29 install-my-scripts.
30
31 Note: Uses util-linux getopt option parsing: spaces between args and
32 options, short options can be combined, options before args.
33 EOF
34 exit $1
35 }
36
37
38 err-bash-trace() {
39 local -i argc_index=0 frame i start=${1:-0} max_indent=8 indent
40 local source
41 local extdebug=false
42 if [[ $(shopt -p extdebug) == *-s* ]]; then
43 extdebug=true
44 fi
45 for ((frame=0; frame < ${#FUNCNAME[@]}-1; frame++)); do
46 argc=${BASH_ARGC[frame]}
47 argc_index+=$argc
48 ((frame < start)) && continue
49 if (( ${#BASH_SOURCE[@]} > 1 )); then
50 source="${BASH_SOURCE[frame+1]}:${BASH_LINENO[frame]}:"
51 fi
52 indent=$((frame-start + 1))
53 indent=$((indent < max_indent ? indent : max_indent))
54 printf "%${indent}s↳%sin \`%s" '' "$source" "${FUNCNAME[frame]}"
55 if $extdebug; then
56 for ((i=argc_index-1; i >= argc_index-argc; i--)); do
57 printf " %s" "${BASH_ARGV[i]}"
58 done
59 fi
60 echo \'
61 done
62 return 0
63 }
64 err-catch() {
65 set -E; shopt -s extdebug
66 _err-trap() {
67 err=$?
68 exec >&2
69 set +x
70 echo "${BASH_SOURCE[1]}:${BASH_LINENO[0]}: \`$BASH_COMMAND' returned $err"
71 err-bash-trace 2
72 set -e # err trap does not work within an error trap
73 "${_errcatch_cleanup[@]:-:}" # note :-: is to be compatible with set -u
74 echo "$0: exiting with code $err"
75 exit $err
76 }
77 trap _err-trap ERR
78 set -o pipefail
79 }
80 err-catch
81
82 tu() {
83 while read -r line; do
84 file="$1"
85 grep -xFq "$line" "$file" || tee -a "$file"<<<"$line"
86 done
87 }
88 m() {
89 if $verbose; then
90 printf "%s\n" "$*"
91 fi
92 "$@"
93 }
94 x() {
95 printf "%s\n" "$*"
96 "$@"
97 }
98
99 mnt() {
100 dir=$1
101 if ! mountpoint -q $dir; then
102 mkdir -p $dir
103 m mount $dir
104 fi
105 }
106 fstab() {
107 while read -r start mpoint end; do
108 l="$start $mpoint $end"
109 # kill off any lines that duplicate the mount point.
110 sed --follow-symlinks -ri "\%$l%b;\%^\s*\S+\s+$mpoint\s%d" /etc/fstab
111 tu /etc/fstab <<<"$l"
112 done
113 }
114 pid-check() {
115 for p in ${pids}; do
116 for m in ${my_pids[@]}; do
117 if (( p == m )); then
118 echo "$0: error: pids to kill includes our pid or a parent. ps output:" >&2
119 ps -f -p $p
120 exit 1
121 fi
122 done
123 done
124 }
125 kill-dir() {
126 for sig; do
127 echo kill-dir $sig
128 found_pids=false
129 if pids=$(timeout 4 lsof -t $dir); then
130 found_pids=true
131 timeout 4 lsof -w $dir
132 pid-check
133 kill -$sig $pids
134 fi
135 # fuser will find open sockets that lsof won't, for example from gpg-agent.
136 # note: -v shows kernel processes, which then doesn't return true when we want
137 if pids=$(timeout 4 fuser -m $dir 2>/dev/null); then
138 pid-check
139 found_pids=true
140 fuser -$sig -mvk $dir
141 fi
142 sleep .5
143 if ! $found_pids; then
144 return 0
145 fi
146 done
147 return 1
148 }
149
150 ##### begin command line parsing ########
151
152 # you can remove this if you do not have options which can have args with spaces or empty.
153
154 verbose=false
155 force=false
156 temp=$(getopt -l help,force,verbose hfv "$@") || usage 1
157 eval set -- "$temp"
158 while true; do
159 case $1 in
160 -f|--force) force=true ;;
161 -v|--verbose) verbose=true ;;
162 -h|--help) usage ;;
163 --) shift; break ;;
164 *) echo "$0: unexpected args: $*" >&2 ; usage 1 ;;
165 esac
166 shift
167 done
168
169 ##### end command line parsing ########
170
171 ret=0
172
173 ##### begin setup fstab for subvols we care about ######
174 root_dev=$(awk '$2 == "/" {print $1}' /etc/mtab)
175 if [[ $root_dev == /dev/dm-* ]]; then
176 for d in /dev/mapper/*; do
177 if [[ $(readlink -f $d) == "$root_dev" ]]; then
178 root_dev=$d
179 break
180 fi
181 done
182 fi
183
184 if cryptsetup status $root_dev &>/dev/null; then
185 crypt_dev=$root_dev
186 else # if we are in a recovery boot, find the next best crypt device
187 noauto=,noauto
188 for dev in $(dmsetup ls --target crypt | awk '{print $1}'); do
189 dev=/dev/mapper/$dev
190 if awk '{print $1}' /etc/mtab | grep -Fx $dev &>/dev/null; then
191 crypt_dev=$dev
192 break
193 fi
194 done
195 fi
196
197
198 fstab <<EOF
199 $crypt_dev /a btrfs noatime,subvol=a$noauto 0 0
200 EOF
201
202 shopt -s nullglob
203
204 # ssh and probably some other things care about parent directory
205 # ownership, and ssh doesn\'t allow any group writable parent
206 # directories, so we are forced to use a directory structure similar
207 # to home directories
208 f=(/mnt/root/btrbk/q.*); f=${f[0]}
209 if [[ -e $f ]]; then
210 fstab <<EOF
211 $crypt_dev /q btrfs noatime,subvol=q,gid=1000$noauto 0 0
212 /q/p /p none bind$noauto 0 0
213 EOF
214 fi
215
216 f=(/mnt/root/btrbk/o.*); f=${f[0]}
217 if [[ -e $f ]]; then
218 fstab <<EOF
219 $crypt_dev /o btrfs noatime,subvol=o$noauto 0 0
220 /o/m /m none bind$noauto 0 0
221 EOF
222 fi
223
224 if [[ $HOSTNAME == frodo ]]; then
225 fstab <<EOF
226 $crypt_dev /i btrfs noatime,subvol=i$noauto 0 0
227 EOF
228 fi
229 ##### end setup fstab for subvols we care about ######
230
231 # get pids that this program depends on so we dont kill them
232 my_pids=($$ $PPID)
233 loop_limit=30
234 count=0
235 while [[ ${my_pids[-1]} != 1 && ${my_pids[-1]} != ${my_pids[-2]} && $count -lt $loop_limit ]]; do
236 count=$((count + 1))
237 p=$(ps -p ${my_pids[-1]} -o ppid=)
238 if [[ $p == 0 || ! $p ]]; then
239 break
240 fi
241 my_pids+=($p)
242 done
243
244
245 for vol in q a o i; do
246 d=/$vol
247 if ! awk '{print $2}' /etc/fstab | grep -xF $d &>/dev/null; then
248 continue
249 fi
250
251
252 ##### begin building up list of bind mounts ######
253 binds=() # list of bind mounts
254 roots=($d) # list of bind mounts, plus the original mount
255 while true; do
256 new_roots=()
257 for r in ${roots[@]}; do
258 # eg. when r=/q/p, for lines like
259 # /q/p /p none bind 0 0
260 # output /p
261 new_roots+=($(sed -rn "s#^$r/\S+\s+(\S+)\s+none\s+bind\s.*#\1#p" /etc/fstab))
262 done
263 (( ${#new_roots} )) || break
264 binds+=(${new_roots[@]})
265 roots=( ${new_roots[@]} )
266 done
267 ##### end building up list of bind mounts ######
268
269
270 # if latest is already mounted, make sure binds are mounted and move on
271 m check-subvol-stale $d
272 # populated by check-subvol-stale if stale
273 if ! fresh_snap=$(cat /nocow/btrfs-stale/$vol 2>/dev/null); then
274 mnt $d
275 for b in ${binds[@]}; do
276 mnt $b
277 done
278 continue
279 fi
280
281 umount_ret=true
282 unmounted=()
283 for dir in $(echo $d ${binds[*]}\ |tac -s\ ); do
284 if mountpoint -q $dir; then
285 if m umount -R $dir; then
286 unmounted+=($dir)
287 else
288 if ! kill-dir TERM TERM TERM INT INT HUP HUP; then
289 if $force; then kill-dir KILL; fi
290 fi
291
292 if m umount -R $dir; then
293 unmounted+=($dir)
294 else
295 echo "$0: failed to umount $dir"
296 umount_ret=false
297 ret=1
298 continue
299 fi
300 fi
301 fi
302 done
303
304 # if we unmounted some but not all, restore them and move on
305 if ! $umount_ret; then
306 for dir in ${unmounted[@]}; do
307 mnt $dir
308 done
309 continue
310 fi
311
312 #### begin dealing with leaf vols ####
313 # todo: decipher /mnt/root, like we do in check-subvol-stale
314 cd /mnt/root
315 if [[ -e $vol ]]; then
316 leaf=$vol.leaf.$(date +%Y-%m-%dT%H:%M:%S%z)
317 m mv $vol $leaf
318 m btrfs property set -ts $leaf ro true
319
320 ### begin check if leaf is different, delete it if not ###
321 if [[ -e /a/opt/btrfs-snapshots-diff/btrfs-snapshots-diff.py ]]; then
322 source /a/bin/distro-functions/src/package-manager-abstractions
323 pi python-jmespath # dependency
324 parentid=$(btrfs sub show $leaf | awk '$1 == "Parent" && $2 == "UUID:" {print $3}')
325 bsubs=(/mnt/root/btrbk/$vol.*)
326 bsub=
327 # go in reverse order as its more likely to be at the end
328 for ((i=${#bsubs[@]}-1; i>=0; i--)); do
329 if [[ $parentid == $(btrfs sub show ${bsubs[i]} | awk '$1 == "UUID:" {print $2}') ]]; then
330 bsub=${bsubs[i]}
331 break
332 fi
333 done
334 if [[ $bsub ]]; then
335 tmp=$(mktemp)
336 # in testing, same subvol is 136 bytes. allow some overhead
337 btrfs send --no-data -p $bsub $leaf | head -c 1000 > $tmp || [[ $? == 141 ]]
338 if (( $(stat -c%s $tmp) < 1000)); then
339 # example output for an empty diff:
340 # Found a valid Btrfs stream header, version 1
341 # o.leaf.2019-05-15T14:00:50-0400;snapshot: uuid=ba045ea30737dd449003f1ee40ec12d0, ctrasid=109533, clone_uuid=3c7e3544e486834aa71d89e5b8f30056, clone_ctransid=109533
342 lines=$(/a/opt/btrfs-snapshots-diff/btrfs-snapshots-diff.py -s -f $tmp | \
343 grep -vxF "Found a valid Btrfs stream header, version 1" | \
344 grep -cv "^[^;]*;snapshot: ") ||:
345 if [[ $lines == 0 ]]; then
346 x btrfs sub del $leaf
347 fi
348 fi
349 fi
350 fi
351 ### end check if leaf is different, delete it if not ###
352
353 ## begin expire leaf vols ##
354 leaf_vols=($vol.leaf.*)
355 count=1
356 for leaf in ${leaf_vols[@]}; do
357 leaf_secs=$(date -d ${leaf#$vol.leaf.} +%s)
358 if (( $(date +%s) - 60*60*24*60 > leaf_secs || count > 200 )); then # 60 days
359 x btrfs sub del $leaf
360 fi
361 count=$((count+1))
362 done
363 ## end expire leaf vols ##
364 fi
365 #### end dealing with leaf vols ####
366
367 # Note, we make a few assumptions in this script, like
368 # $d was not a different subvol id than $vol, and
369 # things otherwise didn't get mounted very strangely.
370 m btrfs sub snapshot $fresh_snap $vol
371 for dir in $d ${binds[@]}; do
372 m mnt $dir
373 done
374 stale_dir=/nocow/btrfs-stale
375 rm -f $stale_dir/$d
376
377 done
378
379 ### disabled
380 if [[ $HOSTNAME == kdxxxxxxxxx ]]; then
381 # partitioned it with fai partitioner outside of fai,
382 # because it\'s worth it to have 1% space reserved for boot and
383 # swap partitions in case I ever want to boot off those drives.
384 # as root:
385 # . /a/bin/fai/fai-wrapper
386 # eval-fai-classfile /a/bin/fai/fai/config/class/51-multi-boot
387 # fai-setclass ROTATIONAL
388 # export LUKS_DIR=/q/root/luks/
389 # # because the partition nums existed already
390 # fai-setclass REPARTITION
391 # /a/bin/fai/fai/config/hooks/partition.DEFAULT
392
393 devs=(
394 ata-TOSHIBA_MD04ACA500_84REK6NTFS9A-part1
395 ata-TOSHIBA_MD04ACA500_84R2K773FS9A-part1
396 ata-TOSHIBA_MD04ACA500_8471K430FS9A-part1
397 ata-TOSHIBA_MD04ACA500_8481K493FS9A-part1
398 )
399 first=true
400 for dev in ${devs[@]}; do
401 if $first; then
402 first=false
403 tu /etc/fstab <<EOF
404 /dev/mapper/crypt_dev_$dev /i btrfs noatime,subvol=i,noauto 0 0
405 /dev/mapper/crypt_dev_$dev /mnt/iroot btrfs noatime,subvolid=0,noauto 0 0
406 EOF
407 fi
408 tu /etc/crypttab <<EOF
409 crypt_dev_$dev /dev/disk/by-id/$dev /q/root/luks/host-kd discard,luks
410 EOF
411 if [[ ! -e /dev/mapper/crypt_dev_$dev ]]; then
412 cryptdisks_start crypt_dev_$dev
413 fi
414 done
415 # note, could do an else here and have some kind of mount for /i
416 # on other hosts.
417 fi
418
419 exit $ret