X-Git-Url: https://iankelling.org/git/?p=distro-setup;a=blobdiff_plain;f=mount-latest-subvol;h=f9bc2def096c8710e53e812fac815216d2592ce5;hp=6a714d1a791ff8a67d4901f9654b5a049f93ccfd;hb=79b274fcd8bfa556133ab13270e84b40aebe8468;hpb=7002af6b587b8d66d1f3b58ae77f09fa3d794d15 diff --git a/mount-latest-subvol b/mount-latest-subvol index 6a714d1..f9bc2de 100644 --- a/mount-latest-subvol +++ b/mount-latest-subvol @@ -1,163 +1,376 @@ #!/bin/bash +# Copyright (C) 2016 Ian Kelling +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. -[[ $EUID == 0 ]] || exec sudo -E "$BASH_SOURCE" "$@" - -errcatch() { - set -E; shopt -s extdebug - _err-trap() { - err=$? - exec >&2 - set +x - echo "${BASH_SOURCE[1]}:${BASH_LINENO[0]}:in \`$BASH_COMMAND' returned $err" - bash-trace 2 - echo "$0: exiting with code $err" - exit $err - } - trap _err-trap ERR - set -o pipefail +script=$(readlink -f -- "$BASH_SOURCE") +cd / +[[ $EUID == 0 ]] || exec sudo -E "$script" "$@" + +source /usr/local/lib/err + +usage() { + cat < 1 )); then - source="${BASH_SOURCE[frame+1]}:${BASH_LINENO[frame]}:" - fi - indent=$((frame-start+1)) - indent=$((indent < max_indent ? indent : max_indent)) - printf "%${indent}s↳%sin \`%s" '' "$source" "${FUNCNAME[frame]}" - if $extdebug; then - for ((i=argc_index-1; i >= argc_index-argc; i--)); do - printf " %s" "${BASH_ARGV[i]}" - done - fi - echo \' + +tu() { + while read -r line; do + file="$1" + grep -xFq "$line" "$file" || tee -a "$file"<<<"$line" + done +} +m() { + if $verbose; then + printf "%s\n" "$*" + fi + "$@" +} +x() { + printf "%s\n" "$*" + "$@" +} + +mnt() { + dir=$1 + if ! mountpoint -q $dir; then + mkdir -p $dir + m mount $dir + fi +} +fstab() { + while read -r start mpoint end; do + l="$start $mpoint $end" + # kill off any lines that duplicate the mount point. + sed --follow-symlinks -ri "\%$l%b;\%^\s*\S+\s+$mpoint\s%d" /etc/fstab + tu /etc/fstab <<<"$l" + done +} +pid-check() { + for p in ${pids}; do + for m in ${my_pids[@]}; do + if (( p == m )); then + echo "$0: error: pids to kill includes our pid or a parent. ps output:" >&2 + ps -f -p $p + exit 1 + fi done + done +} +kill-dir() { + for sig; do + echo kill-dir $sig + found_pids=false + if pids=$(timeout 4 lsof -t $dir); then + found_pids=true + timeout 4 lsof -w $dir + pid-check + kill -$sig $pids + fi + # fuser will find open sockets that lsof won't, for example from gpg-agent. + # note: -v shows kernel processes, which then doesn't return true when we want + if pids=$(timeout 4 fuser -m $dir 2>/dev/null); then + pid-check + found_pids=true + fuser -$sig -mvk $dir + fi + sleep .5 + if ! $found_pids; then + return 0 + fi + done + return 1 } -errcatch +##### begin command line parsing ######## + +# you can remove this if you do not have options which can have args with spaces or empty. + +verbose=false +force=false +temp=$(getopt -l help,force,verbose hfv "$@") || usage 1 +eval set -- "$temp" +while true; do + case $1 in + -f|--force) force=true ;; + -v|--verbose) verbose=true ;; + -h|--help) usage ;; + --) shift; break ;; + *) echo "$0: unexpected args: $*" >&2 ; usage 1 ;; + esac + shift +done -e() { printf "%s\n" "$*"; "$@"; } +##### end command line parsing ######## ret=0 -first_root_crypt=$(awk '$2 == "/" {print $1}' /etc/mtab) -tu /etc/fstab </dev/null; then + crypt_dev=$root_dev +else # if we are in a recovery boot, find the next best crypt device + noauto=,noauto + for dev in $(dmsetup ls --target crypt | awk '{print $1}'); do + dev=/dev/mapper/$dev + if awk '{print $1}' /etc/mtab | grep -Fx $dev &>/dev/null; then + crypt_dev=$dev + break + fi + done +fi + + +fstab </dev/null; then - continue - fi +for vol in q a o i; do + d=/$vol + if ! awk '{print $2}' /etc/fstab | grep -xF $d &>/dev/null; then + continue + fi - binds=() - roots=($d) - while true; do - new_roots=() - for r in ${roots[@]}; do - # /q/a /a none bind 0 0 - new_roots+=($(sed -rn "s#^$r/\S+\s+(\S+)\s+none\s+bind\s.*#\1#" /etc/fstab)) - done - (( ${#new_roots} )) || break - binds+=(${new_roots[@]}) - roots=( ${new_roots[@]} ) + ##### begin building up list of bind mounts ###### + binds=() # list of bind mounts + roots=($d) # list of bind mounts, plus the original mount + while true; do + new_roots=() + for r in ${roots[@]}; do + # eg. when r=/q/p, for lines like + # /q/p /p none bind 0 0 + # output /p + new_roots+=($(sed -rn "s#^$r/\S+\s+(\S+)\s+none\s+bind\s.*#\1#p" /etc/fstab)) done + (( ${#new_roots} )) || break + binds+=(${new_roots[@]}) + roots=( ${new_roots[@]} ) + done + ##### end building up list of bind mounts ###### - if e check-subvol-stale $d; then - for b in ${binds[@]}; do - mount $b - done - continue - fi - last_snap=$(/dev/null); then + mnt $d + for b in ${binds[@]}; do + mnt $b + done + continue + fi - umount_ret=true - unmounted=() - for dir in $(echo $d ${binds[*]}\ |tac -s\ ); do - if mountpoint $dir; then - if e umount -R $dir; then - unmounted+=($dir) - else - umount_ret=false - echo "$0: failed to umount $dir" - break - fi + umount_ret=true + unmounted=() + for dir in $(echo $d ${binds[*]}\ |tac -s\ ); do + if mountpoint -q $dir; then + if m umount -R $dir; then + unmounted+=($dir) + else + if ! kill-dir TERM TERM TERM INT INT HUP HUP; then + if $force; then kill-dir KILL; fi fi - done - if ! $umount_ret; then - for dir in ${unmounted[@]}; do - mount $dir - done - ret=1 - continue + if m umount -R $dir; then + unmounted+=($dir) + else + echo "$0: failed to umount $dir" + umount_ret=false + ret=1 + continue + fi + fi fi + done + + # if we unmounted some but not all, restore them and move on + if ! $umount_ret; then + for dir in ${unmounted[@]}; do + mnt $dir + done + continue + fi + + #### begin dealing with leaf vols #### + # todo: decipher /mnt/root, like we do in check-subvol-stale + cd /mnt/root + if [[ -e $vol ]]; then + leaf=$vol.leaf.$(date +%Y-%m-%dT%H:%M:%S%z) + m mv $vol $leaf + m btrfs property set -ts $leaf ro true - cd /mnt/root - if [[ -e $vol ]]; then - e btrfs sub del $vol + ### begin check if leaf is different, delete it if not ### + if [[ -e /a/opt/btrfs-snapshots-diff/btrfs-snapshots-diff.py ]]; then + source /a/bin/distro-functions/src/package-manager-abstractions + pi python-jmespath # dependency + parentid=$(btrfs sub show $leaf | awk '$1 == "Parent" && $2 == "UUID:" {print $3}') + bsubs=(/mnt/root/btrbk/$vol.*) + bsub= + # go in reverse order as its more likely to be at the end + for ((i=${#bsubs[@]}-1; i>=0; i--)); do + if [[ $parentid == $(btrfs sub show ${bsubs[i]} | awk '$1 == "UUID:" {print $2}') ]]; then + bsub=${bsubs[i]} + break + fi + done + if [[ $bsub ]]; then + tmp=$(mktemp) + # in testing, same subvol is 136 bytes. allow some overhead + btrfs send --no-data -p $bsub $leaf | head -c 1000 > $tmp || [[ $? == 141 ]] + if (( $(stat -c%s $tmp) < 1000)); then + # example output for an empty diff: + # Found a valid Btrfs stream header, version 1 + # o.leaf.2019-05-15T14:00:50-0400;snapshot: uuid=ba045ea30737dd449003f1ee40ec12d0, ctrasid=109533, clone_uuid=3c7e3544e486834aa71d89e5b8f30056, clone_ctransid=109533 + lines=$(/a/opt/btrfs-snapshots-diff/btrfs-snapshots-diff.py -s -f $tmp | \ + grep -vxF "Found a valid Btrfs stream header, version 1" | \ + grep -cv "^[^;]*;snapshot: ") ||: + if [[ $lines == 0 ]]; then + x btrfs sub del $leaf + fi + fi + fi fi - # Note, we make a few assumptions in this script, like - # $d was not a different subvol id than $vol, and - # things otherwise didn't get mounted very strangely. - e btrfs sub snapshot btrbk/$last_snap $vol - for dir in $d ${binds[@]}; do - e mount $dir + ### end check if leaf is different, delete it if not ### + + ## begin expire leaf vols ## + leaf_vols=($vol.leaf.*) + count=1 + for leaf in ${leaf_vols[@]}; do + leaf_secs=$(date -d ${leaf#$vol.leaf.} +%s) + if (( $(date +%s) - 60*60*24*60 > leaf_secs || count > 200 )); then # 60 days + x btrfs sub del $leaf + fi + count=$((count+1)) done + ## end expire leaf vols ## + fi + #### end dealing with leaf vols #### + + # Note, we make a few assumptions in this script, like + # $d was not a different subvol id than $vol, and + # things otherwise didn't get mounted very strangely. + m btrfs sub snapshot $fresh_snap $vol + for dir in $d ${binds[@]}; do + m mnt $dir + done + stale_dir=/nocow/btrfs-stale + rm -f $stale_dir/$d + done + +### disabled +if [[ $HOSTNAME == kdxxxxxxxxx ]]; then + # partitioned it with fai partitioner outside of fai, + # because it\'s worth it to have 1% space reserved for boot and + # swap partitions in case I ever want to boot off those drives. + # as root: + # . /a/bin/fai/fai-wrapper + # eval-fai-classfile /a/bin/fai/fai/config/class/51-multi-boot + # fai-setclass ROTATIONAL + # export LUKS_DIR=/q/root/luks/ + # # because the partition nums existed already + # fai-setclass REPARTITION + # /a/bin/fai/fai/config/hooks/partition.DEFAULT + + devs=( + ata-TOSHIBA_MD04ACA500_84REK6NTFS9A-part1 + ata-TOSHIBA_MD04ACA500_84R2K773FS9A-part1 + ata-TOSHIBA_MD04ACA500_8471K430FS9A-part1 + ata-TOSHIBA_MD04ACA500_8481K493FS9A-part1 + ) + first=true + for dev in ${devs[@]}; do + if $first; then + first=false + tu /etc/fstab <