#!/usr/bin/env bash shopt -s extglob # Upstream wrapper requires UUID to be used for configuration. # However, when declaratively describing a host, we may not know its UUID, and # shouldn't need to persist something that will differ between hosts built from # the same configuration template. # Thus, for using bees from NixOS, we have our own wrapper, which supports not # just UUID but any specification permitted by findmnt [[ $bees_debug ]] && { PS4=':${BASH_SOURCE##*/}:$LINENO+'; set -x; } usage() { cat >&2 <&2; exit 1; } allConfigNames=( blockdev fsSpec home idxSize idxSizeMB mntDir runDir status verbosity workDir ) # Alternate names for configuration values; "bees_" will always be prepended declare -A altConfigNames=( # from original bees wrapper [BEESHOME]=home [BEESSTATUS]=status [MNT_DIR]=mntDir [UUID]=uuid [WORK_DIR]=runDir [DB_SIZE]=idxSize ) # legacy bees config files can be arbitrary shell scripts, so we need to actually evaluate them sandboxedConfigFileEval() { bash_exe=$(type -P bash) || exit PATH=/var/empty ENV='' BASH_ENV='' AL128K="$((128*1024))" AL16M="$((16*1024*1024))" "$bash_exe" -r ${bees_debug+-x} \ -c 'eval "$(&2; for var; do [[ ${!var} ]] && printf "%q=%s\\0" "$var" "${!var}"; done' \ "${!altConfigNames[@]}" "${allConfigNames[@]}" \ <"$1" } readConfigFileIfExists() { local line [[ -s $1 ]] || return 1 while IFS= read -r -d '' line; do line=${line%%+([[:space:]])"#"*} [[ $line ]] || continue [[ $line = *=* ]] || { printf 'WARNING: Config file line not recognized: %q\n' "$line" >&2 continue } set_option "$line" done < <(sandboxedConfigFileEval "$1") } set_option() { local k v k="${1%%=*}" v="${1#*=}" [[ ${altConfigNames[$k]} ]] && k=${altConfigNames[$k]} printf -v "bees_$k" %s "$v" } uuid_re='^[[:xdigit:]]{8}-[[:xdigit:]]{4}-[[:xdigit:]]{4}-[[:xdigit:]]{4}-[[:xdigit:]]{12}$' # Shared code for setting configuration used by other operations. # # Reads from global associative array "opts" containing options passed in as # key=value pairs on the command line, looks for config-file overrides, and # sets individual global variables. _setup() { declare fstype bees_fsSpec=$1; shift # Look for file-based configuration, additional to honoring configuration on the command line bees_config_dir="${bees_config_dir:-/etc/bees}" if [[ $bees_fsSpec =~ $uuid_re ]]; then bees_uuid=$bees_fsSpec # If our spec looks like a bare UUID, and no config file exists in the new # format, fall back to legacy config file search mechanism (grep; ewww). if ! readConfigFileIfExists "$bees_config_dir/UUID=$bees_fsSpec.conf"; then # Legacy approach to finding a config file: Grep for a *.conf file # containing the UUID within its text. Permitting spaces around the "=" # appears to be a bug, but is retained for compatibility with the # original upstream script. allConfFiles=( "$bees_config_dir"/*.conf ) if (( ${#allConfFiles[@]} )); then # in read or readarray with -d '', the NUL terminating the empty string is used as delimiter character. readarray -d '' -t matchingConfFiles < <(grep -E -l -Z "^[^#]*UUID[[:space:]]*=[[:space:]]*" "${allConfFiles[@]}") else matchingConfFiles=( ) fi if (( ${#matchingConfFiles[@]} == 1 )); then # Exactly one configuration file exists in our target directory with a reference to the UUID given. bees_config_file=${matchingConfFiles[0]} readConfigFileIfExists "$bees_config_file" echo "NOTE: Please consider renaming $bees_config_file to $bees_config_dir/UUID=$bees_fsSpec" >&2 echo " ...and passing UUID=$bees_fsSpec on startup." >&2 elif (( ${#matchingConfFiles[@]} > 1 )); then # The legacy wrapper would silently use the first file and ignore # others, but... no. echo "ERROR: Passed a bare UUID, but multiple configuration files match it:" >&2 printf ' - %q\n' "${matchingConfFiles[@]}" >&2 die "Unable to continue." fi fi else # For a non-UUID fsSpec that is not a path, look only for a config file # exactly matching its text. # # (Passing a mount point as a fsSpec is only supported with the new # wrapper; all key=value pairs can be passed on the command line in this # mode, so config file support is not needed). [[ $bees_fsSpec = */* ]] || readConfigFileIfExists "$bees_config_dir/$bees_fsSpec.conf" fi [[ $bees_uuid ]] || { # if bees_uuid is not in our .conf file, look it up with findmnt for findmnt_mode in --kernel --mtab --fstab; do read -r bees_uuid fstype < <(findmnt "$findmnt_mode" -n -o uuid,fstype "$bees_fsSpec") ||: [[ $bees_uuid && $fstype ]] && break done [[ $bees_uuid ]] || die "Unable to identify a device matching $bees_fsSpec with findmnt" [[ $fstype = btrfs ]] || die "Device type is $fstype, not btrfs" } [[ $bees_uuid = */* ]] || readConfigFileIfExists "$bees_config_dir/UUID=$bees_uuid.conf" # Honor any values read from config files above; otherwise, set defaults. bees_workDir="${bees_workDir:-.beeshome}" bees_runDir="${bees_runDir:-/run/bees}" bees_mntDir="${bees_mntDir:-$bees_runDir/mnt/$bees_uuid}" bees_home="${bees_home:-$bees_mntDir/$bees_workDir}" bees_status="${bees_status:-${bees_runDir}/$bees_uuid.status}" bees_verbosity="${bees_verbosity:-6}" bees_idxSizeMB="${bees_idxSizeMB:-1024}" bees_idxSize=${bees_idxSize:-"$(( bees_idxSizeMB * 1024 * 1024 ))"} bees_blockdev=${bees_blockdev:-"/dev/disk/by-uuid/$bees_uuid"} [[ -b $bees_blockdev ]] || die "Block device $bees_blockdev missing" (( bees_idxSize % (16 * 1024 * 1024) == 0 )) || die "DB size must be divisible by 16MB" } do_run() { local db old_db_size _setup "$1"; shift mkdir -p -- "$bees_mntDir" || exit # subvol id 5 is reserved for the root subvolume of a btrfs filesystem. mountpoint -q "$bees_mntDir" || mount -osubvolid=5 -- "$bees_blockdev" "$bees_mntDir" || exit if [[ -d $bees_home ]]; then btrfs subvolume show "$bees_home" >/dev/null 2>&1 || die "$bees_home exists but is not a subvolume" else btrfs subvolume create "$bees_home" || exit sync # workaround for Zygo/bees#93 fi db=$bees_home/beeshash.dat touch -- "$db" old_db_size=$(stat -c %s -- "$db") new_db_size=$bees_idxSize if (( old_db_size != new_db_size )); then rm -f -- "$bees_home"/beescrawl."$bees_uuid".dat truncate -s "$new_db_size" -- "$db" || exit fi chmod 700 -- "$bees_home" # BEESSTATUS and BEESHOME are the only variables handled by the legacy # wrapper for which getenv() is called in C code. BEESSTATUS=$bees_status BEESHOME=$bees_home exec "${beesd_bin:-/lib/bees/bees}" \ --verbose "$bees_verbosity" \ "$@" "$bees_mntDir" || exit } do_cleanup() { _setup "$1"; shift mountpoint -q "$bees_mntDir" && umount -l -- "$bees_mntDir" || exit } (( $# >= 2 )) || usage declare -f "do_$1" >/dev/null 2>&1 || usage mode=$1; shift # must be a do_* function; currently "run" or "cleanup" declare -a args=( "$1" ); shift # pass first argument (config-name|fsSpec) through literally # parse other arguments as key=value pairs, or pass them through literally if they do not match that form. # similarly, any option after "--" will be passed through literally. while (( $# )); do if [[ $1 = *=* ]]; then set_option "$1" elif [[ $1 = -- ]]; then shift args+=( "$@" ) break else args+=( "$1" ) fi shift done "do_$mode" "${args[@]}"