#!/bin/sh # lbu - utility to create local backups. # Copyright (c) 2006 Natanael Copa # May be distributed under GPL2 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 DEFAULT_CIPHER="aes-256-cbc" LBU_CONF="$sysconfdir"/lbu.conf if [ -f "$LBU_CONF" ]; then . "$LBU_CONF" fi UMOUNT_LIST= usage() { echo "$PROGRAM $VERSION" echo "usage: $PROGRAM [options] [args] Available subcommands: commit (ci) exclude (ex, delete) include (inc, add) list (ls) package (pkg) status (stat, st) list-backup (lb) revert Common options: -h Show help for subcommand. -q Quiet mode. -v Verbose mode. " exit 1 } cleanup() { local i for i in $UMOUNT_LIST; do umount $i done } exit_clean() { cleanup exit 1 } mount_once() { if ! grep $1 /proc/mounts >/dev/null; then mount $1 && UMOUNT_LIST="$1 $UMOUNT_LIST" 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" fi } # 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" } # list_add(char *listfile, char* file...) list_add() { local list="$1" shift mkdir -p `dirname "$list"` while [ $# -gt 0 ] ; do filename=`echo "$1" | sed 's:^/\+::'` if grep "^$filename$" "$list" >/dev/null 2>&1 ; then vecho "$filename is already in $list." else vecho "Adding $filename to $list." echo "$filename" >> "$list" fi shift done } # list_delete(char *listfile, char *file...) list_delete() { local list="$1" local tmp="$list.old" shift [ -f "$list" ] || return 1 while [ $# -gt 0 ] ; do filename=`echo "$1" | sed 's:^/\+::'` mv "$list" "$tmp" vecho "Removing $filename from list." grep -v "^$filename$" "$tmp" > "$list" rm "$tmp" shift done } # unpack archive on LBU_MEDIA to given dir unpack_apkovl() { local f="$(hostname).apkovl.tar.gz" local dest="$1" local mnt="/media/$LBU_MEDIA" local count=0 mkdir -p "$dest" mount_once "$mnt" if [ ! -f "$mnt/$f" ]; then return 1 fi if [ -z "$ENCRYPTION" ]; then tar -C "$dest" -zxf "$mnt/$f" return fi f="$f.$ENCRYPTION" 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() { echo "$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. " exit 1 } cmd_include() { if [ "$LIST" ] ; then [ $# -gt 0 ] && usage_include show_include return fi [ $# -lt 1 ] && usage_include if [ "$REMOVE" ] ; then list_delete "$INCLUDE_LIST" "$@" else list_add "$INCLUDE_LIST" "$@" list_delete "$EXCLUDE_LIST" "$@" fi } show_include() { if [ -f "$INCLUDE_LIST" ] ; then vecho "Include files:" cat "$INCLUDE_LIST" fi } # # lbu_package - create a package # usage_package() { echo "$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. " exit 1 } cmd_package() { local pkg="$1" local rc=0 local owd="$PWD" local suff="apkovl.tar.gz" local tmpdir tmppkg check_openssl init_tmpdir tmpdir [ -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)" 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." echo " /var/lib/apk/world will be used." rm -f etc/lbu/packages.list fi currentlist=$(apk audit --backup -q) if [ -f var/lib/apk/world ]; then currentlist="$currentlist var/lib/apk/world" fi # create tar archive [ -f "$EXCLUDE_LIST" ] && excl="-X $EXCLUDE_LIST" [ -f "$INCLUDE_LIST" ] && incl="-T $INCLUDE_LIST" 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. tar $excl $incl -c -v $currentlist > /dev/null rc=$? fi if [ $rc -eq 0 ]; then if [ -z "$ENCRYPTION" ]; then tar $excl $incl -c $currentlist | gzip -c >"$tmppkg" rc=$? else set -- enc "-$ENCRYPTION" -salt [ -n "$PASSWORD" ] && set -- "$@" -pass pass:"$PASSWORD" tar $excl $incl -c $currentlist | gzip -c \ | $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" else cp "$tmppkg" "$pkg" fi fi vecho "Created $pkg" fi return $rc } # # lbu list - list files that would go to archive # usage_list() { echo "$PROGRAM $VERSION Lists files that would go to tar package. Same as: 'lbu package -v /dev/null' usage: $PROGRAM list|ls " exit 1 } cmd_list() { VERBOSE="-v" cmd_package /dev/null } # # lbu_commit - commit config files to writeable media # usage_commit() { echo "$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_MEDIA will be used. 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 varialbes can also be set in $LBU_CONF " exit 1 } cmd_commit() { local media mnt statuslist tmplist currentlist local incl excl outfile ovls lines check_openssl # turn on verbose mode if dryrun [ -n "$DRYRUN" ] && VERBOSE="-v" # 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 "$mnt" || die "failed to mount $mnt" # 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 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() { echo "$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. " exit 1 } cmd_exclude() { if [ "$LIST" ] ; then [ $# -gt 0 ] && usage_exclude show_exclude return fi [ $# -lt 1 ] && usage_exclude if [ "$REMOVE" ] ; then list_delete "$EXCLUDE_LIST" "$@" else list_delete "$INCLUDE_LIST" "$@" list_add "$EXCLUDE_LIST" "$@" fi } show_exclude() { if [ -f "$EXCLUDE_LIST" ] ; then vecho "Exclude files:" cat "$EXCLUDE_LIST" fi } #--------------------------------------------------------------------------- # lbu_listbackup - Show old commits usage_listbackup() { cat <] EOF exit 1 } cmd_listbackup() { local media=${1:-"$LBU_MEDIA"} local mnt="/media/$media" [ -z "$media" ] && usage_listbackup mount_once "$mnt" || die "failed to mount $mnt" 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 < [] 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 "$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() { echo "$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. " exit 1 } cmd_status() { if [ -n "$USE_DEFAULT" ]; then apk audit --backup return 0 fi LBU_MEDIA=${1:-"$LBU_MEDIA"} [ -z "$LBU_MEDIA" ] && usage_status local tmp init_tmpdir tmp mkdir -p "$tmpdir/a" "$tmp/b" # unpack last commited apkovl to tmpdir/a unpack_apkovl "$tmp/a" # generate new apkovl and extract to tmpdir/b cmd_package - | tar -C "$tmp/b" -zx # show files that exists in a but not in b as deleted local f ( cd "$tmp"/a && find ) | while read f; do f=${f#./} [ "$f" = "." ] && continue [ -e "$tmp/b/$f" ] || 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" ]; 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() { echo "$PROGRAM $VERSION Run a diff against last commit usage: $PROGRAM diff [] " exit 1 } cmd_diff() { LBU_MEDIA=${1:-"$LBU_MEDIA"} [ -z "$LBU_MEDIA" ] && usage_diff local tmp init_tmpdir tmp mkdir -p "$tmpdir/a" "$tmp/b" unpack_apkovl "$tmp/a" cmd_package - | tar -C "$tmp/b" -zx cd "$tmp" && diff -ruN a b } #----------------------------------------------------------- # 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";; *) 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