#!/bin/sh # lbu - utility to create local backups. # Copyright (c) 2006-2010 Natanael Copa # May be distributed under GPL2 or MIT VERSION=@VERSION@ sysconfdir=@sysconfdir@ if [ ! -f ${libalpine:="./libalpine.sh"} ]; then libalpine=/usr/share/lbu/libalpine.sh if [ ! -f "$libalpine" ]; then libalpine=/lib/libalpine.sh fi fi . $libalpine || exit 1 EXCLUDE_LIST="$sysconfdir"/exclude INCLUDE_LIST="$sysconfdir"/include LBU_LIST="/etc/apk/protected_paths.d/lbu.list" DEFAULT_CIPHER="aes-256-cbc" LBU_CONF="$sysconfdir"/lbu.conf LBU_PREPACKAGE="$sysconfdir"/pre-package.d LBU_POSTPACKAGE="$sysconfdir"/post-package.d if [ -f "$LBU_CONF" ]; then . "$LBU_CONF" fi UMOUNT_LIST= usage() { cat <<-__EOF__ $PROGRAM $VERSION" usage: $PROGRAM [options] [args] Available subcommands: commit (ci) diff exclude (ex, delete) include (inc, add) list (ls) list-backup (lb) migrate_include_exclude package (pkg) revert status (stat, st) Common options: -h Show help for subcommand. -q Quiet mode. -v Verbose mode. __EOF__ exit 1 } cleanup() { local i for i in $REMOUNT_RO_LIST; do mount -o remount,ro $i done for i in $UMOUNT_LIST; do umount $i done } exit_clean() { cleanup exit 1 } # check if given dir is not a mounted mountpoint is_unmounted() { awk "\$2 == \"$1\" {exit 1}" /proc/mounts } mount_once() { if is_unmounted "$1"; then mount $1 && UMOUNT_LIST="$1 $UMOUNT_LIST" || return 1 fi } # check if given dir is read-only is_ro() { local tmpfile=$(mktemp -p "$1" 2>/dev/null) [ -z "$tmpfile" ] && return 0 rm -f "$tmpfile" return 1 } mount_once_rw() { mount_once "$1" || return 1 if is_ro "$1"; then REMOUNT_RO_LIST="$1 $REMOUNT_RO_LIST" mount -o remount,rw "$1" fi } # create backupfile backup_apkovl() { local outfile="$1" local d=$( date -u -r "$outfile" "+%Y%m%d%H%M%S" ) local backup=$(echo "$outfile" | sed "s/\.apkovl\.tar\.gz/.$d.tar.gz/") vecho "Creating backup $backup" if [ -z "$DRYRUN" ]; then mv "$outfile" "$backup" APKOVL_BACKUP="$backup" fi } restore_apkovl() { local outfile="$1" if [ -n "$DRYRUN" ] || [ -z "$APKOVL_BACKUP" ]; then return 0 fi mv "$APKOVL_BACKUP" "$outfile" } # verify we have openssl if we want to encrypt check_openssl() { [ -z "$ENCRYPTION" ] && return 0 OPENSSL=$(which openssl 2>/dev/null) || die "openssl was not found" $OPENSSL list-cipher-commands | grep "^$ENCRYPTION$" > /dev/null \ || die "Cipher $ENCRYPTION is not supported" } # grep and sed has issues with escaping '*' in lists so we rather do # our own filter functions list_has() { local line= [ -e "$LBU_LIST" ] || return 1 while read line; do [ "$line" = "$1" ] && return 0 done < "$LBU_LIST" return 1 } list_filter_out() { local line= while read line; do if [ "$line" != "$1" ]; then echo "$line" fi done < "$LBU_LIST" } # list_add(char prefix, char *listfile, char* file...) list_add() { local prefix="$1" shift mkdir -p "${LBU_LIST%/*}" while [ $# -gt 0 ] ; do filename="$(echo "$1" | sed -E 's:^/+::')" if list_has "${prefix}${filename}"; then vecho "$filename is already in $LBU_LIST." else vecho "Adding $filename to $LBU_LIST." echo "${prefix}${filename}" >> "$LBU_LIST" fi shift done } # list_delete(char prefix, char *listfile, char *file...) list_delete() { local prefix="$1" local tmp="$LBU_LIST.new" shift [ -f "$LBU_LIST" ] || return 1 while [ $# -gt 0 ] ; do filename="$(echo "$1" | sed -E 's:^/+::')" if list_has "${prefix}${filename}"; then vecho "Removing $filename from $LBU_LIST." list_filter_out "${prefix}${filename}" > "$tmp" \ && mv "$tmp" "$LBU_LIST" else vecho "$filename is not in $LBU_LIST" fi shift done } # unpack archive on LBU_MEDIA to given dir unpack_apkovl() { local f="$(hostname).apkovl.tar.gz" local dest="$1" local mnt="${LBU_BACKUPDIR:-/media/$LBU_MEDIA}" local count=0 mkdir -p "$dest" if [ -n "$LBU_MEDIA" ]; then mount_once "$mnt" fi if [ -n "$ENCRYPTION" ]; then f="$f.$ENCRYPTION" fi if [ ! -f "$mnt/$f" ]; then return 1 fi if [ -z "$ENCRYPTION" ]; then tar -C "$dest" -zxf "$mnt/$f" return fi check_openssl while [ $count -lt 3 ]; do $OPENSSL enc -d -$ENCRYPTION -in "$mnt/$f" | tar \ -C "$dest" -zx 2>/dev/null && return 0 count=$(( $count + 1 )) done cleanup die "Failed to unpack $mnt/$f" } # # lbu_include - add/remove files to include list # usage_include() { cat <<-__EOF__ $PROGRAM $VERSION Add filename(s) to include list ($sysconfdir/include) usage: $PROGRAM include|inc|add [-rv] ... $PROGRAM include|inc|add [-v] -l Options: -l List contents of include list. -r Remove specified file(s) from include list instead of adding. -v Verbose mode. __EOF__ exit 1 } cmd_include() { cmd_migrate_include_exclude if [ "$LIST" ] ; then [ $# -gt 0 ] && usage_include show_include return fi [ $# -lt 1 ] && usage_include if [ "$REMOVE" ] ; then list_delete + "$@" else list_add + "$@" list_delete - "$@" fi } show_include() { if [ -f "$LBU_LIST" ] ; then vecho "Include files:" grep -- '^+' "$LBU_LIST" | sed 's/^+//' fi } # # lbu_package - create a package # usage_package() { cat <<-__EOF__ $PROGRAM $VERSION Create backup package. usage: $PROGRAM package|pkg -v [|] Options: -v Verbose mode. If is a directory, a package named .apkovl.tar.gz will be created in the specified directory. If is specified, and is not a direcotry, a package with the specified name willbe created. If nor is not specified, a package named .apkovl.tar.gz will be created in current work directory. __EOF__ exit 1 } _gen_filelist() { apk audit --backup --quiet --recursive --check-permissions } cmd_package() { local pkg="$1" local rc=0 local owd="$PWD" local suff="apkovl.tar.gz" local tmpdir tmppkg cmd_migrate_include_exclude check_openssl init_tmpdir tmpdir if [ -d "$LBU_PREPACKAGE" ]; then run-parts "$LBU_PREPACKAGE" >&2 || return 1 fi [ -n "$ENCRYPTION" ] && suff="$suff.$ENCRYPTION" # find filename if [ -d "$pkg" ] ; then pkg="$pkg/$(hostname).$suff" elif [ -z "$pkg" ]; then pkg="$PWD/$(hostname).$suff" fi tmppkg="$tmpdir/$(basename $pkg)" local tar_create="tar -c --no-recursion -T -" cd "${ROOT:-/}" # remove old package.list if [ -f etc/lbu/packages.list ] && [ -f var/lib/apk/world ]; then echo "Note: Removing /etc/lbu/packages.list." >&2 echo " /var/lib/apk/world will be used." >&2 rm -f etc/lbu/packages.list fi # create tar archive if [ -n "$VERBOSE" ]; then echo "Archiving the following files:" >&2 # we dont want to mess the tar output with the # password prompt. Lets get the tar output first. _gen_filelist | $tar_create -v > /dev/null rc=$? fi if [ $rc -eq 0 ]; then if [ -z "$ENCRYPTION" ]; then _gen_filelist | $tar_create -z >"$tmppkg" rc=$? else set -- enc "-$ENCRYPTION" -salt [ -n "$PASSWORD" ] && set -- "$@" -pass pass:"$PASSWORD" _gen_filelist | $tar_create -z \ | $OPENSSL "$@" > "$tmppkg" rc=$? fi fi cd "$owd" # actually commit unless dryrun mode if [ $rc -eq 0 ]; then if [ -z "$DRYRUN" ]; then if [ "x$pkg" = "x-" ]; then cat "$tmppkg" elif [ -b "$pkg" ] || [ -c "$pkg" ]; then cat "$tmppkg" > "$pkg" else if cp "$tmppkg" "$pkg.new"; then mv "$pkg.new" "$pkg" rc=$? else rm -f "$pkg.new" rc=1 fi fi fi [ $rc -eq 0 ] && vecho "Created $pkg" fi if [ -d "$LBU_POSTPACKAGE" ]; then run-parts "$LBU_POSTPACKAGE" >&2 fi return $rc } # # lbu list - list files that would go to archive # usage_list() { cat <<-__EOF__ $PROGRAM $VERSION Lists files that would go to tar package. Same as: 'lbu package -v /dev/null' usage: $PROGRAM list|ls __EOF__ exit 1 } cmd_list() { cmd_migrate_include_exclude _gen_filelist } # # lbu_commit - commit config files to writeable media # usage_commit() { cat <<-__EOF__ $PROGRAM $VERSION Create a backup of config to writeable media. usage: $PROGRAM commit|ci [-nv] [] Options: -d Remove old apk overlay files. -e Protect configuration with a password. -n Don't commit, just show what would have been commited. -p Give encryption password on the command-line -v Verbose mode. The following values for is supported: floppy usb If is not specified, the environment variable LBU_BACKUPDIR or LBU_MEDIA will be used. If LBU_BACKUPDIR is set, nothing will be mounted. Password protection will use $DEFAULT_CIPHER encryption. Other ciphers can be used by setting the DEFAULT_CIPHER or ENCRYPTION environment variables. For possible ciphers, try: openssl -v The password used to encrypt the file, can either be specified with the -p option or using the PASSWORD environment variable. The environment variables can also be set in $LBU_CONF __EOF__ exit 1 } cmd_commit() { local media mnt statuslist tmplist local incl excl outfile ovls lines check_openssl # turn on verbose mode if dryrun [ -n "$DRYRUN" ] && VERBOSE="-v" mnt="$LBU_BACKUPDIR" if [ -z "$mnt" ]; then # find what media to use media="${1:-$LBU_MEDIA}" [ -z "$media" ] && usage_commit # mount media unles its already mounted mnt=/media/$media [ -d "$mnt" ] || usage mount_once_rw "$mnt" || die "failed to mount $mnt" fi # find the outfile outfile="$mnt/$(hostname).apkovl.tar.gz" if [ -n "$ENCRYPTION" ]; then outfile="$outfile.$ENCRYPTION" fi # remove old config files if [ -n "$DELETEOLDCONFIGS" ] ; then local rmfiles=$(ls "$mnt/"*.apkovl.tar.gz* 2>/dev/null) if [ -n "$rmfiles" ] ; then if [ -n "$VERBOSE" ]; then echo "Removing old apk overlay files:" >&2 echo "$rmfiles" echo "" >&2 fi [ -z "$DRYRUN" ] && rm "$mnt/"*.apkovl.tar.gz* fi else lines=$(ls -1 "$mnt"/*.apkovl.tar.gz* 2>/dev/null) if [ "$lines" = "$outfile" ]; then backup_apkovl "$outfile" elif [ -n "$lines" ]; then # More then one apkovl, this is a security concern cleanup eecho "The following apkovl file(s) were found:" eecho "$lines" eecho "" die "Please use -d to replace." fi fi # create package if ! cmd_package "$outfile"; then restore_apkovl "$outfile" cleanup die "Problems creating archive. aborting" fi # delete old backups if needed # poor mans 'head -n -N' done with awk. ls "$mnt"/$(hostname).[0-9][0-9][0-9][0-9]*[0-9].tar.gz 2>/dev/null \ | awk '{ a[++i] = $0; } END { print a[0]; while (i-- > '"${BACKUP_LIMIT:-0}"') { print a[++j] } }' | xargs rm 2>/dev/null # remove obsolete file. some older version of alpine needs this # to be able to upgrade if [ -z "$DRYRUN" ] && [ -f $mnt/packages.list ]; then echo "Note: Removing packages.list from $(basename $mnt)." echo " /var/lib/apk/world will be used." rm -f $mnt/packages.list fi # make sure data is written sync [ "$media" = "floppy" ] && sleep 1 # move current to commited. vecho "Successfully saved apk overlay files" } #--------------------------------------------------------------------------- # lbu_exclude - add remove file(s) from exclude list usage_exclude() { cat <<-__EOF__ $PROGRAM $VERSION Add filename(s) to exclude list ($sysconfdir/exclude) usage: $PROGRAM exclude|ex|delete [-rv] ... $PROGRAM exclude|ex|delete [-v] -l Options: -l List contents of exclude list. -r Remove specified file(s) from exclude list instead of adding. -v Verbose mode. __EOF__ exit 1 } cmd_exclude() { cmd_migrate_include_exclude if [ "$LIST" ] ; then [ $# -gt 0 ] && usage_exclude show_exclude return fi [ $# -lt 1 ] && usage_exclude if [ "$REMOVE" ] ; then list_delete - "$@" else list_delete + "$@" list_add - "$@" fi } show_exclude() { if [ -f "$LBU_LIST" ] ; then vecho "Exclude files:" grep -- '^-' "$LBU_LIST" | sed 's/^-//' fi } #--------------------------------------------------------------------------- # lbu_listbackup - Show old commits usage_listbackup() { cat <<-__EOF__ $PROGRAM $VERSION Show old commits. usage: $PROGRAM list-backup [] __EOF__ exit 1 } cmd_listbackup() { local media=${1:-"$LBU_MEDIA"} local mnt="${LBU_BACKUPDIR:-/media/$media}" [ -z "$media" ] && [ -z "$LBU_BACKUPDIR" ] && usage_listbackup if [ -n "$media" ]; then mount_once "$mnt" || die "failed to mount $mnt" fi ls -1 "$mnt"/*.[0-9][0-9]*[0-9][0-9].tar.gz* 2>/dev/null | sed 's:.*/::' } #--------------------------------------------------------------------------- # lbu_revert - revert to old config usage_revert() { cat <<-__EOF__ $PROGRAM $VERSION Revert to older commit. usage: $PROGRAM revert [] The revision should be one of the files listed by 'lbu list-backup'. __EOF__ } cmd_revert() { local media=${2:-"$LBU_MEDIA"} [ -z "$media" ] && usage_revert local mnt="/media/$media" local revertto="$mnt/$1" local current="$mnt/$(hostname).apkovl.tar.gz" if [ -n "$ENCRYPTION" ]; then current="$current.$ENCRYPTION" fi mount_once_rw "$mnt" || die "failed to mount $mnt" [ -f "$revertto" ] || die "file not found: $revertto" backup_apkovl "$current" vecho "Reverting to $1" [ -z "$DRYRUN" ] && mv "$revertto" "$current" } #--------------------------------------------------------------------------- # lbu_status - check what files have been changed since last save usage_status() { cat <<-__EOF__ $PROGRAM $VERSION Check what files have been changed since last commit. usage: $PROGRAM status|st [-av] Options: -a Compare all files, not just since last commit. -v Also show include and exclude lists. __EOF__ exit 1 } cmd_status() { if [ -n "$USE_DEFAULT" ]; then apk audit --backup return 0 fi LBU_MEDIA=${1:-"$LBU_MEDIA"} [ -z "$LBU_MEDIA" ] && [ -z "$LBU_BACKUPDIR" ] && usage_status local tmp init_tmpdir tmp mkdir -p "$tmp/a" "$tmp/b" # unpack last commited apkovl to tmpdir/a unpack_apkovl "$tmp/a" # generate new apkovl and extract to tmpdir/b local save_encryption="$ENCRYPTION" ENCRYPTION= cmd_package - | tar -C "$tmp/b" -zx ENCRYPTION="$save_encryption" # show files that exists in a but not in b as deleted local f ( cd "$tmp"/a && find ) | while read f; do f=${f#./} local b="$tmp/b/$f" if [ "$f" = "." ] || [ -e "$b" ] || [ -L "$b" ]; then continue fi echo "D $f" done # compare files in b with files in a ( cd "$tmp"/b && find ) | while read f; do f=${f#./} [ "$f" = "." ] && continue local a="$tmp/a/$f" local b="$tmp/b/$f" if [ ! -e "$a" ] && [ ! -L "$a" ]; then echo "A $f" elif [ -f "$a" ] && [ -f "$b" ] && [ "$b" -nt "$a" ] \ && ! cmp -s "$a" "$b"; then echo "U $f" fi done } #----------------------------------------------------------- # lbu_diff - run a diff against last commit usage_diff() { cat <<-__EOF__ $PROGRAM $VERSION Run a diff against last commit usage: $PROGRAM diff [] __EOF__ exit 1 } cmd_diff() { local diff_opts= LBU_MEDIA=${1:-"$LBU_MEDIA"} [ -z "$LBU_MEDIA" ] && [ -z "$LBU_BACKUPDIR" ] && usage_diff local tmp init_tmpdir tmp mkdir -p "$tmpdir/a" "$tmp/b" unpack_apkovl "$tmp/a" ENCRYPTION= cmd_package - | tar -C "$tmp/b" -zx if diff --help 2>&1 | grep -q -- --no-dereference; then diff_opts="--no-dereference" fi cd "$tmp" && diff -ruN $diff_opts a b } # migrate migrate_conf() { local pref="$1" conf="$2" line= echo "Note: Migrating $conf to $LBU_LIST" >&2 echo "# Automatically imported from $conf" >> "$LBU_LIST" while read line; do if [ "${line#'#'}" != "$line" ]; then # dont prefix comments echo "$line" >> "$LBU_LIST" || return 1 continue fi case "$line" in [a-zA-z0-9._/]*) line="$pref$line";; *) continue;; # skip files with weird names esac if ! list_has "$line"; then echo "$line" >> "$LBU_LIST" || return 1 fi done < "$conf" rm "$conf" } #----------------------------------------------------------- # lbu migrate_config - migrate include/exclude to protected_paths.d cmd_migrate_include_exclude() { if [ -e "$INCLUDE_LIST" ]; then migrate_conf + "$INCLUDE_LIST" fi if [ -e "$EXCLUDE_LIST" ]; then migrate_conf - "$EXCLUDE_LIST" fi } #----------------------------------------------------------- # Main cmd=$(echo "$PROGRAM" | cut -s -d_ -f2) PROGRAM=$(echo "$PROGRAM" | cut -d_ -f1) if [ -z "$cmd" ] ; then cmd="$1" [ -z "$cmd" ] && usage shift fi # check for valid sub command case "$cmd" in include|inc|add) SUBCMD="include";; commit|ci) SUBCMD="commit";; exclude|ex|delete) SUBCMD="exclude";; list|ls) SUBCMD="list";; package|pkg) SUBCMD="package";; status|stat|st) SUBCMD="status";; list-backup|lb) SUBCMD="listbackup";; revert) SUBCMD="revert";; diff) SUBCMD="diff";; migrate_include_exclude) SUBCMD="migrate_include_exclude";; *) usage;; esac # parse common args while getopts "adehlM:np:qrv" opt ; do case "$opt" in a) [ $SUBCMD = status ] || usage_$SUBCMD USE_DEFAULT="-a" ;; d) DELETEOLDCONFIGS="yes" ;; e) [ -z "$ENCRYPTION" ] && ENCRYPTION="$DEFAULT_CIPHER" ;; h) usage_$SUBCMD ;; l) LIST="-l" ;; n) [ $SUBCMD = commit ] || usage_$SUBCMD DRYRUN="-n" ;; p) PASSWORD="$OPTARG" ;; q) QUIET="$QUIET -q" ;; r) REMOVE="-r" ;; v) VERBOSE="$VERBOSE -v" ;; esac done shift $(expr $OPTIND - 1) trap exit_clean SIGINT SIGTERM cmd_$SUBCMD "$@" retcode=$? cleanup exit $retcode