Hello community, here is the log from the commit of package duply for openSUSE:Factory checked in at 2015-10-02 09:23:36 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/duply (Old) and /work/SRC/openSUSE:Factory/.duply.new (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "duply" Changes: -------- --- /work/SRC/openSUSE:Factory/duply/duply.changes 2014-07-12 17:14:50.000000000 +0200 +++ /work/SRC/openSUSE:Factory/.duply.new/duply.changes 2015-10-02 09:23:37.000000000 +0200 @@ -1,0 +2,47 @@ +Wed Sep 23 16:20:59 UTC 2015 - malcolmle...@opensuse.org + +- Update to version 1.10.1: + + Bugfix 86: Duply+Swift outputs warning. + + Bugfix 87: Swift fails without BACKEND_URL. +- Changes from 1.10: + + Featreq 36: busybox issues - fix awk, grep version detection, + fix grep failure because --color=never switch is unsupported. + + Bugfix 81: --exclude-globbing-filelist is deprecated since + 0.7.03. + + Implemented base-/dirname as bash functions. + + Featreq 31 " Support for duplicity Azure backend " - ignored a + contributed patch by Scott McKenzie and instead opted for + removing almost all code that deals with special env vars + required by backends. Adding and modifying these results in too + much overhead so I dropped this feature. The future alternative + for users is to consult the duplicity manpage and add the + needed export definitions to the conf file. Appended a + commented example to the template conf below the auth section. +- Changes from 1.9.2: + + Bugfix: exporting keys with gpg2.1 works now. + + Documented GPG_OPTS needed for gpg2.1 to conf template. + + Bugfix 82: GREP_OPTIONS=--color=always disrupted time + calculation. + + Added GPG conf var (see conf template for details). + + Added grep version output as it is an integral needed binary. + + Added PYTHONPATH printout in version output. +- Changes from 1.9.1: + + Export CMD_ERR now for scripts to detect if CMD_PREV + failed/succeeded. + + Bugfix: CMD_PREV contained command even if it was skipped. +- Changes from 1.9.0: + + Bugfix: env vars were not exported when external script was + executable. + + Rework GPG_KEY handling, allow virtually anything now (uid, + keyid etc.) see gpg manpage, section "How to specify a user ID" + let gpg complain when the delivered values are invalid for + whatever reason. + + Started to rework tmp space checking, exposed folder & writable + check. TODO: reimplement enough file space available checking. +- Changes from 1.8.0: + + Add command verifyPath to expose 'verify --file-to-restore' + action. + + Add time parameter support to verify command. + + Add section time formats to usage output. + +------------------------------------------------------------------- Old: ---- duply_1.7.4.tgz New: ---- duply_1.10.1.tgz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ duply.spec ++++++ --- /var/tmp/diff_new_pack.Ip8e7u/_old 2015-10-02 09:23:37.000000000 +0200 +++ /var/tmp/diff_new_pack.Ip8e7u/_new 2015-10-02 09:23:37.000000000 +0200 @@ -1,8 +1,8 @@ # # spec file for package duply # -# Copyright (c) 2014 SUSE LINUX Products GmbH, Nuernberg, Germany. -# Copyright (c) 2011-2013 Malcolm J Lewis <malcolmle...@opensuse.org> +# Copyright (c) 2015 SUSE LINUX Products GmbH, Nuernberg, Germany. +# Copyright (c) 2011-2015 Malcolm J Lewis <malcolmle...@opensuse.org> # # All modifications and additions to the file contributed by third parties # remain the property of their copyright owners, unless otherwise agreed @@ -18,14 +18,16 @@ Name: duply -Version: 1.7.4 +Version: 1.10.1 Release: 0 Summary: A frontend for the mighty duplicity magic License: GPL-2.0 Group: Productivity/Archiving/Compression Url: http://duply.net/ -Source0: http://downloads.sourceforge.net/ftplicity/duply%20%28simple%20duplicity%29/1.7.x/%{name}_%{version}.tgz +Source0: http://downloads.sourceforge.net/ftplicity/duply%20%28simple%20duplicity%29/1.10.x/%{name}_%{version}.tgz +# MANUAL BEGIN Requires: duplicity +# MANUAL END BuildArch: noarch BuildRoot: %{_tmppath}/%{name}-%{version}-build ++++++ duply_1.7.4.tgz -> duply_1.10.1.tgz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/duply_1.7.4/INSTALL.txt new/duply_1.10.1/INSTALL.txt --- old/duply_1.7.4/INSTALL.txt 2014-06-24 20:55:59.000000000 +0200 +++ new/duply_1.10.1/INSTALL.txt 2015-08-19 16:57:44.000000000 +0200 @@ -38,12 +38,12 @@ # install into PREFIX PREFIX=~/_apps/duplicity-0.6.07 python setup.py install --prefix="$PREFIX" --install-lib="$PREFIX" -# OBSOLETE: since 0.6.17 the next step is not required anymore +# NOTE: 0.6.17 to 25 do not need the next step, 0.6.25 and newer do again # patch executable to find libs in PREFIX -cat $PREFIX/bin/duplicity | \ -awk '1;/import getpass, gzip, os, sys, time, types/{print "sys.path.insert(1,sys.path[0] + \47/../\47)"}' > \ -$PREFIX/bin/duplicity_mod && chmod 755 $PREFIX/bin/duplicity_mod && \ -mv $PREFIX/bin/duplicity_mod $PREFIX/bin/duplicity +awk '1;/import.*[ ,]+sys/{print "sys.path.insert(1,sys.path[0] + \47/../\47)"}' \ +"$PREFIX/bin/duplicity" > "$PREFIX/bin/duplicity_mod" && \ +chmod 755 "$PREFIX/bin/duplicity_mod" && \ +mv "$PREFIX/bin/duplicity_mod" "$PREFIX/bin/duplicity" If this works flawlessly than you will find the duplicity executable under $PREFIX/bin/duplicity @@ -78,4 +78,4 @@ UNINSTALL DUPLICITY python setup.py install --record files.txt -cat files.txt | xargs rm -rf \ No newline at end of file +cat files.txt | xargs rm -rf diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/duply_1.7.4/duply new/duply_1.10.1/duply --- old/duply_1.7.4/duply 2014-06-24 20:55:59.000000000 +0200 +++ new/duply_1.10.1/duply 2015-08-19 16:57:44.000000000 +0200 @@ -9,7 +9,7 @@ # changed from ftplicity to duply. # # See http://duply.net or http://ftplicity.sourceforge.net/ for more info. # # (c) 2006 Christiane Ruetten, Heise Zeitschriften Verlag, Germany # -# (c) 2008-2014 Edgar Soldin (changes since version 1.3) # +# (c) 2008-2015 Edgar Soldin (changes since version 1.3) # ############################################################################### # LICENSE: # # This program is licensed under GPLv2. # @@ -34,6 +34,50 @@ # # # CHANGELOG: +# 1.10.1 (19.8.2015) +# - bugfix 86: Duply+Swift outputs warning +# - bugfix 87: Swift fails without BACKEND_URL +# +# 1.10 (31.7.2015) +# - featreq 36: busybox issues - fix awk, grep version detection, +# fix grep failure because --color=never switch is unsupported +# (thx Thomas Harning Jr. for reporting and helping to debug/fix it) +# - bugfix 81: --exclude-globbing-filelist is deprecated since 0.7.03 +# (thx Joachim Wiedorn, also for maintaining the debian package) +# - implemented base-/dirname as bash functions +# - featreq 31 " Support for duplicity Azure backend " - ignored a +# contributed patch by Scott McKenzie and instead opted for removing almost +# all code that deals with special env vars required by backends. +# adding and modifying these results in too much overhead so i dropped this +# feature. the future alternative for users is to consult the duplicity +# manpage and add the needed export definitions to the conf file. +# appended a commented example to the template conf below the auth section. +# +# 1.9.2 (21.6.2015) +# - bugfix: exporting keys with gpg2.1 works now (thx Philip Jocks) +# - documented GPG_OPTS needed for gpg2.1 to conf template (thx Troy Engel) +# - bugfix 82: GREP_OPTIONS=--color=always disrupted time calculation +# - added GPG conf var (see conf template for details) +# - added grep version output as it is an integral needed binary +# - added PYTHONPATH printout in version output +# +# 1.9.1 (13.10.2014) +# - export CMD_ERR now for scripts to detect if CMD_PREV failed/succeeded +# - bugfix: CMD_PREV contained command even if it was skipped +# +# 1.9.0 (24.8.2014) +# - bugfix: env vars were not exported when external script was executable +# - rework GPG_KEY handling, allow virtually anything now (uid, keyid etc.) +# see gpg manpage, section "How to specify a user ID" +# let gpg complain when the delivered values are invalid for whatever reason +# - started to rework tmp space checking, exposed folder & writable check +# TODO: reimplement enough file space available checking +# +# 1.8.0 (13.7.2014) +# - add command verifyPath to expose 'verify --file-to-restore' action +# - add time parameter support to verify command +# - add section time formats to usage output +# # 1.7.4 (24.6.2014) # - remove ubuntu one support, service is discontinued # - featreq 31: add authenticated swift (contributed by Justus Seifert) @@ -343,13 +387,35 @@ # 1.0 - first release ############################################################################### +# utility functions overriding binaries + +# wrap grep to override possible env set GREP_OPTIONS=--color=always +function grep { + command env -u GREP_OPTIONS grep "$@" +} + +# implement basename in plain bash +function basename { + echo "${1##*/}" +} + +# implement dirname in plain bash +function dirname { + echo ${1%/*} +} + +# a lookup function for executables working with names or file paths +function lookup { + local bin="$1" + ( [ "${bin##*/}" == "$bin" ] && hash "$bin" 2>/dev/null ) || [ -x "$bin" ] +} # important definitions ####################################################### ME_LONG="$0" ME="$(basename $0)" ME_NAME="${ME%%.*}" -ME_VERSION="1.7.4" +ME_VERSION="1.10.1" ME_WEBSITE="http://duply.net" # default config values @@ -357,11 +423,13 @@ DEFAULT_TARGET='scheme://user[:password]@host[:port]/[/]path' DEFAULT_TARGET_USER='_backend_username_' DEFAULT_TARGET_PASS='_backend_password_' +DEFAULT_GPG='gpg' DEFAULT_GPG_KEY='_KEY_ID_' DEFAULT_GPG_PW='_GPG_PASSWORD_' # function definitions ########################## -function set_config { # sets config vars + +function set_config { # sets global config vars local CONFHOME_COMPAT="$HOME/.ftplicity" local CONFHOME="$HOME/.duply" local CONFHOME_ETC_COMPAT="/etc/ftplicity" @@ -405,7 +473,7 @@ function version_info { # print version information cat <<END - $ME version $ME_VERSION + $ME_NAME version $ME_VERSION ($ME_WEBSITE) END } @@ -419,14 +487,18 @@ } function using_info { - duplicity_version_get + lookup duplicity && duplicity_version_get + local NOTFOUND="MISSING" # freebsd awk (--version only), debian mawk (-W version only), deliver '' so awk does not wait for input - AWK_VERSION=$((awk --version '' 2>/dev/null || awk -W version '' 2>/dev/null) | awk '/.+/{sub(/^[Aa][Ww][Kk][ \t]*/,"",$0);print $0;exit}') - PYTHON_VERSION=$(python -V 2>&1| awk '{print tolower($0);exit}') - GPG_INFO=`gpg --version 2>/dev/null| awk '/^gpg/{v=$1" "$3};/^Home/{print v" ("$0")"}'` - BASH_VERSION=$(bash --version | awk '/^GNU bash, version/{sub(/GNU bash, version[ ]+/,"",$0);print $0}') - echo -e "Using installed duplicity version ${DUPL_VERSION:-(not found)}${PYTHON_VERSION+, $PYTHON_VERSION}\ -${GPG_INFO:+, $GPG_INFO}${AWK_VERSION:+, awk '${AWK_VERSION}'}${BASH_VERSION:+, bash '${BASH_VERSION}'}." + local AWK_VERSION=$( lookup awk && (awk --version 2>/dev/null || awk -W version 2>&1) | awk 'NR<=2&&tolower($0)~/(busybox|awk)/{success=1;print;exit} END{if(success<1) print "unknown"}' || echo "$NOTFOUND" ) + local GREP_VERSION=$( lookup grep && grep --version 2>&1 | awk 'NR<=2&&tolower($0)~/(busybox|grep.*[0-9]+\.[0-9]+)/{success=1;print;exit} END{if(success<1) print "unknown"}' || echo "$NOTFOUND" ) + local PYTHON_VERSION=$(lookup python && python -V 2>&1| awk '{print tolower($0);exit}' || echo "python $NOTFOUND" ) + local GPG_INFO=$(gpg_avail && gpg --version 2>&1| awk 'NR==1{v=$1" "$3};/^Home:/{print v" ("$0")"}' || echo "gpg $NOTFOUND") + local BASH_VERSION=$(bash --version | awk 'NR==1{IGNORECASE=1;sub(/GNU bash, version[ ]+/,"",$0);print $0}') + echo -e "Using installed duplicity version ${DUPL_VERSION:-$NOTFOUND}\ +${PYTHON_VERSION+, $PYTHON_VERSION${PYTHONPATH:+ 'PYTHONPATH=$PYTHONPATH'}}\ +${GPG_INFO:+, $GPG_INFO}${AWK_VERSION:+, awk '${AWK_VERSION}'}${GREP_VERSION:+, grep '${GREP_VERSION}'}\ +${BASH_VERSION:+, bash '${BASH_VERSION}'}." } function usage_info { # print usage information @@ -469,10 +541,10 @@ to '~/.${ME_NAME}/<profile>' (~ expands to environment variable \$HOME). Superuser root can place profiles under '/etc/${ME_NAME}'. Simply create - the folder manually before running $ME as superuser. + the folder manually before running $ME_NAME as superuser. Note: - Already existing profiles in root's profile folder will cease to work - unless there are moved to the new location manually. + Already existing profiles in root's home folder will cease to work + unless they are moved to the new location manually. example 1: $ME humbug backup @@ -495,7 +567,7 @@ the next command will only be executed if the previous failed example: - 'pre_and_bkp_or_verify_post' translates to 'pre+bkp-verify_post + 'pre+bkp-verify_post' translates to 'pre_and_bkp_or_verify_post' COMMANDS: usage get usage help text @@ -512,7 +584,11 @@ list [<age>] list all files in backup (as it was at <age>, default: now) status prints backup sets and chains currently in repository - verify list files changed since latest backup + verify [<age>] [--compare-data] + list files changed, since age if given + verifyPath <rel_path_in_bkp> <local_path> [<age>] [--compare-data] + list changes of a file or folder path in backup compared to a + local path, since age if given restore <target_path> [<age>] restore the complete backup to <target_path> [as it was at <age>] fetch <src_path> <target_path> [<age>] @@ -538,7 +614,7 @@ txt2man feature for package maintainers - create a manpage based on the usage output. download txt2man from http://mvertes.free.fr/, put it in the PATH and run '$ME txt2man' to create a man page. - version show version information of $ME and needed programs + version show version information of $ME_NAME and needed programs OPTIONS: --force passed to duplicity (see commands: purge, purge-full, cleanup) @@ -546,26 +622,35 @@ --disable-encryption disable encryption, overrides profile settings +TIME FORMATS: + For all time related parameters like age, max_age etc. + Refer to the duplicity manpage for all available formats. Here some examples: + 2002-01-25T07:00:00+02:00 (full date time format string) + 2002/3/5 (date string YYYY/MM/DD) + 12D (interval, 12 days ago) + 1h78m (interval, 1 hour 78 minutes ago) + PRE/POST SCRIPTS: - All internal duply variables will be readable in the scripts. - Some of interest might be + Useful internal duply variables will be readable in the scripts. + Some of interest may be CONFDIR, SOURCE, TARGET_URL_<PROT|HOSTPATH|USER|PASS>, - GPG_<KEYS_ENC|KEY_SIGN|PW>, CMD_<PREV|NEXT> + GPG_<KEYS_ENC|KEY_SIGN|PW>, CMD_<PREV|NEXT>, CMD_ERR The CMD_* variables were introduced to allow different actions according to the command the scripts were attached to e.g. 'pre_bkp_post_pre_verify_post' will call the pre script two times, with CMD_NEXT variable set to 'bkp' on the first and to 'verify' on the second run. + CMD_ERR holds the exit code of the CMD_PREV . EXAMPLES: create profile 'humbug': - $ME humbug create (now edit the resulting conf file) + $ME humbug create (don't forget to edit this new conf file) backup 'humbug' now: $ME humbug backup list available backup sets of profile 'humbug': $ME humbug status - list and delete obsolete backup archives of 'humbug': + list and delete outdated backups of 'humbug': $ME humbug purge --force restore latest backup of 'humbug' to /mnt/restore: $ME humbug restore /mnt/restore @@ -574,6 +659,8 @@ (see "duplicity manpage", section TIME FORMATS) a one line batch job on 'humbug' for cron execution: $ME humbug backup_verify_purge --force + batch job to run a full backup with pre/post scripts: + $ME humbug pre_full_post FILES: in profile folder '~/.${ME_NAME}/<profile>' or '/etc/${ME_NAME}' @@ -609,20 +696,24 @@ cat <<EOF >"$CONF" # gpg encryption settings, simple settings: # GPG_KEY='disabled' - disables encryption alltogether -# GPG_KEY='<key1>[,<key2>]'; GPG_PW='pass' - encrypt with keys, sign -# with key1 if secret key available and use GPG_PW for sign & decrypt +# GPG_KEY='<key1>[,<key2>]'; GPG_PW='pass' - encrypt with keys, +# sign if secret key of key1 is available use GPG_PW for sign & decrypt +# Note: you can specify keys via all methods described in gpg manpage, +# section "How to specify a user ID", escape commas (,) via backslash (\) +# e.g. 'Mueller, Horst', 'Bernd' -> 'Mueller\, Horst, Bernd' +# as they are used to separate the entries # GPG_PW='passphrase' - symmetric encryption using passphrase only GPG_KEY='${DEFAULT_GPG_KEY}' GPG_PW='${DEFAULT_GPG_PW}' # gpg encryption settings in detail (extended settings) # the above settings translate to the following more specific settings -# GPG_KEYS_ENC='<keyid1>,[<keyid2>,...]' - list of pubkeys to encrypt to +# GPG_KEYS_ENC='<keyid1>[,<keyid2>,...]' - list of pubkeys to encrypt to # GPG_KEY_SIGN='<keyid1>|disabled' - a secret key for signing # GPG_PW='<passphrase>' - needed for signing, decryption and symmetric # encryption. If you want to deliver different passphrases for e.g. # several keys or symmetric encryption plus key signing you can use -# gpg-agent. Add '--use-agent' to the duplicity parameters below. -# also see "A NOTE ON SYMMETRIC ENCRYPTION AND SIGNING" in duplicity manpage +# gpg-agent. Simply make sure that GPG_AGENT_INFO is set in environment. +# also see "A NOTE ON SYMMETRIC ENCRYPTION AND SIGNING" in duplicity manpage # notes on en/decryption # private key and passphrase will only be needed for decryption or signing. # decryption happens on restore and incrementals (compare archdir contents). @@ -634,60 +725,50 @@ # NOTE: available since duplicity 0.6.14, translates to SIGN_PASSPHRASE #GPG_PW_SIGN='<signpass>' +# uncomment and set a file path or name force duply to use this gpg executable +# available in duplicity 0.7.04 and above (currently unreleased 06/2015) +#GPG='/usr/local/gpg-2.1/bin/gpg' + # gpg options passed from duplicity to gpg process (default='') # e.g. "--trust-model pgp|classic|direct|always" # or "--compress-algo=bzip2 --bzip2-compress-level=9" # or "--personal-cipher-preferences AES256,AES192,AES..." # or "--homedir ~/.duply" - keep keyring and gpg settings duply specific +# or "--pinentry-mode loopback" - needed for GPG 2.1+ _and_ +# also enable allow-loopback-pinentry in your .gnupg/gpg-agent.conf #GPG_OPTS='' # disable preliminary tests with the following setting #GPG_TEST='disabled' -# credentials & server address of the backup target (URL-Format) -# syntax is -# scheme://[user:password@]host[:port]/[/]path -# for details see duplicity manpage, section URL Format -# http://duplicity.nongnu.org/duplicity.1.html#sect8 -# probably one out of -# # for cloudfiles backend user id is CLOUDFILES_USERNAME, password is -# # CLOUDFILES_APIKEY, you might need to set CLOUDFILES_AUTHURL manually -# cf+http://[user:password@]container_name -# dpbx:///some_dir -# file://[relative|/absolute]/local/path -# ftp[s]://user[:password]@other.host[:port]/some_dir -# gdocs://user[:password]@other.host/some_dir -# # for the google cloud storage (since duplicity 0.6.22) -# # user/password are GS_ACCESS_KEY_ID/GS_SECRET_ACCESS_KEY -# gs://bucket[/prefix] -# hsi://user[:password]@other.host/some_dir -# imap[s]://user[:password]@host.com[/from_address_prefix] -# mega://user[:password]@mega.co.nz/some_dir -# rsync://user[:password]@host.com[:port]::[/]module/some_dir -# # rsync over ssh (only keyauth) -# rsync://u...@host.com[:port]/[relative|/absolute]_path -# # for the s3 user/password are AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY -# s3://[user:password@]host/bucket_name[/prefix] -# s3+http://[user:password@]bucket_name[/prefix] -# # scp and sftp are aliases for the ssh backend -# ssh://user[:password]@other.host[:port]/[/]some_dir -# # for authenticated swift define TARGET_USER or SWIFT_USERNAME, -# # TARGET_PASS or SWIFT_PASSWORD, SWIFT_AUTHURL (mandatory, the path to -# # your identity service, omitting leads to an error with swift), -# # optionally SWIFT_AUTHVERSION (which defaults to "1") -# swift://container_name -# tahoe://alias/directory -# webdav[s]://user[:password]@other.host/some_dir -# ATTENTION: characters other than A-Za-z0-9.-_.~ in the URL have -# to be replaced by their url encoded pendants, see -# http://en.wikipedia.org/wiki/Url_encoding -# if you define the credentials as TARGET_USER, TARGET_PASS below -# duply will try to url_encode them for you if the need arises +# backend, credentials & location of the backup target (URL-Format) +# generic syntax is +# scheme://[user[:password]@]host[:port]/[/]path +# eg. +# sftp://bob:sec...@backupserver.com//home/bob/dupbkp +# for details and available backends see duplicity manpage, section URL Format +# http://duplicity.nongnu.org/duplicity.1.html#sect7 +# NOTE: +# some backends (eg. cloudfiles) need additional env vars to be set to +# work properly, when in doubt consult the man page mentioned above. +# ATTENTION: +# characters other than A-Za-z0-9.-_.~ in the URL have to be +# replaced by their url encoded pendants, see +# http://en.wikipedia.org/wiki/Url_encoding +# if you define the credentials as TARGET_USER, TARGET_PASS below $ME +# will try to url_encode them for you if the need arises. TARGET='${DEFAULT_TARGET}' # optionally the username/password can be defined as extra variables # setting them here _and_ in TARGET results in an error #TARGET_USER='${DEFAULT_TARGET_USER}' #TARGET_PASS='${DEFAULT_TARGET_PASS}' +# alternatively you might export the auth env vars for your backend here +# when in doubt consult (if existing) the NOTE section of your backend on +# http://duplicity.nongnu.org/duplicity.1.html for details +# eg. for cloud files backend it might look like this (uncomment for use!) +#export CLOUDFILES_USERNAME='someuser' +#export CLOUDFILES_APIKEY='somekey' +#export CLOUDFILES_AUTHURL ='someurl' # base directory to backup SOURCE='${DEFAULT_SOURCE}' @@ -865,15 +946,14 @@ " } -# TODO function error_gpg_key { - local KEY_ID=$1 - local KIND=$2 + local KEY_ID="$1" + local KIND="$2" error_gpg "${KIND} gpg key '${KEY_ID}' cannot be found." \ "Doublecheck if the above key is listed by 'gpg --list-keys' or available - as gpg key file '$(basename "$(gpg_keyfile ${KEY_ID})")' in the profile folder. - If not you can put it there and $ME will autoimport it on the next run. - Alternatively import it manually as the user you plan to run $ME with." + as gpg key file '$(basename "$(gpg_keyfile "${KEY_ID}")")' in the profile folder. + If not you can put it there and $ME_NAME will autoimport it on the next run. + Alternatively import it manually as the user you plan to run $ME_NAME with." } function error_gpg_test { @@ -884,10 +964,10 @@ Hint${hint:+s}: ${hint}This error means that gpg is probably misconfigured or not working correctly. The error message above should help to solve the problem. - However, if for some reason $ME should misinterpret the situation you + However, if for some reason $ME_NAME should misinterpret the situation you can define GPG_TEST='disabled' in the conf file to bypass the test. Please do not forget to report the bug in order to resolve the problem - in future versions of $ME. + in future versions of $ME_NAME. " } @@ -904,7 +984,7 @@ function duplicity_version_get { var_isset DUPL_VERSION && return DUPL_VERSION=`duplicity --version 2>&1 | awk '/^duplicity /{print $2; exit;}'` - #DUPL_VERSION='0.6.08b' #,0.4.4.RC4,0.6.08b + #DUPL_VERSION='0.7.03' #'0.6.08b' #,0.4.4.RC4,0.6.08b DUPL_VERSION_VALUE=0 DUPL_VERSION_AWK=$(awk -v v="$DUPL_VERSION" 'BEGIN{ if (match(v,/[^\.0-9]+[0-9]*$/)){ @@ -924,7 +1004,7 @@ if [ $DUPL_VERSION_VALUE -eq 0 ]; then inform "duplicity version check failed (please report, this is a bug)" elif [ $DUPL_VERSION_VALUE -le 404 ] && [ ${DUPL_VERSION_RC:-4} -lt 4 ]; then - error "The installed version $DUPL_VERSION is incompatible with $ME v$ME_VERSION. + error "The installed version $DUPL_VERSION is incompatible with $ME_NAME v$ME_VERSION. You should upgrade your version of duplicity to at least v0.4.4RC4 or use the older ftplicity version 1.1.1 from $ME_WEBSITE." fi @@ -962,14 +1042,14 @@ function run_cmd { # run or print escaped cmd string - CMD_ERR=0 + local CMD_ERR=0 if [ -n "$PREVIEW" ]; then CMD_OUT=$( echo "$@ 2>&1" ) - CMD_MSG="-- Run cmd '$CMD_MSG' --\n$CMD_OUT" + CMD_MSG="-- Run cmd -- $CMD_MSG --\n$CMD_OUT" elif [ -n "$CMD_DISABLED" ]; then CMD_MSG="$CMD_MSG (DISABLED) - $CMD_DISABLED" else - CMD_OUT=` eval $@ 2>&1 ` + CMD_OUT=` eval "$@" 2>&1 ` CMD_ERR=$? if [ "$CMD_ERR" = "0" ]; then CMD_MSG="$CMD_MSG (OK)" @@ -1007,8 +1087,8 @@ if gpg_disabled; then local DUPL_PARAM_ENC='--no-encryption' else - local DUPL_PARAM_ENC=$(gpg_prefix_keyset ' --encrypt-key ' 'GPG_KEYS_ENC') - gpg_signing && local DUPL_PARAM_SIGN=$(gpg_prefix_keyset ' --sign-key ' 'GPG_KEY_SIGN') + local DUPL_PARAM_ENC=$(gpg_prefix_keyset '--encrypt-key' "${GPG_KEYS_ENC_ARRAY[@]}") + gpg_signing && local DUPL_PARAM_SIGN=$(gpg_prefix_keyset '--sign-key' "$GPG_KEY_SIGN") # interpret password settings var_isset 'GPG_PW' && DUPL_ARG_ENC="PASSPHRASE=$(qw "${GPG_PW}")" var_isset 'GPG_PW_SIGN' && DUPL_ARG_ENC="${DUPL_ARG_ENC} SIGN_PASSPHRASE=$(qw "${GPG_PW_SIGN}")" @@ -1033,17 +1113,18 @@ ${DUPL_ARG_ENC}" } -# filter the DUPL_PARAMS var from conf + +# function to filter the DUPL_PARAMS var from user conf function duplicity_params_conf { - # reuse cmd var from main loop - ## in/exclude parameters are currently not supported on restores - if [ "$cmd" = "fetch" ] || [ "$cmd" = "restore" ]; then - # filter exclude params from fetch/restore - echo "$DUPL_PARAMS" | awk '{gsub(/--(ex|in)clude[a-z-]*(([ \t]+|=)[^-][^ \t]+)?/,"");print}' - return - fi - - echo "$DUPL_PARAMS" + # reuse cmd var from main loop + ## in/exclude parameters are currently not supported on restores + if [ "$cmd" = "fetch" ] || [ "$cmd" = "restore" ] || [ "$cmd" = "status" ]; then + # filter exclude params from fetch/restore + echo "$DUPL_PARAMS" | awk '{gsub(/--(ex|in)clude[a-z-]*(([ \t]+|=)[^-][^ \t]+)?/,"");print}' + return + fi + + echo "$DUPL_PARAMS" } function duplify { # the actual wrapper function @@ -1069,7 +1150,7 @@ var_isset 'PREVIEW' && local RUN=echo || local RUN=eval $RUN ${DUPL_VARS_GLOBAL} ${BACKEND_PARAMS} \ ${DUPL_PRECMD} duplicity $DUPL_CMD $DUPL_PARAMS_GLOBAL $(duplicity_params_conf)\ - $GPG_USEAGENT $DUPL_CMD_PARAMS ${PREVIEW:+} + $GPG_USEAGENT $(gpg_custom_binary) $DUPL_CMD_PARAMS ${PREVIEW:+} local ERR=$? return $ERR @@ -1136,12 +1217,12 @@ } function var_isset { - if [ -z "$1" ]; then - echo "ERROR: function var_isset needs a string as parameter" - elif eval "[ \"\${$1}\" == 'not_set' ]" || eval "[ \"\${$1-not_set}\" != 'not_set' ]"; then - return 0 - fi - return 1 + if [ -z "$1" ]; then + echo "ERROR: function var_isset needs a string as parameter" + elif eval "[ \"\${$1}\" == 'not_set' ]" || eval "[ \"\${$1-not_set}\" != 'not_set' ]"; then + return 0 + fi + return 1 } function url_encode { @@ -1167,17 +1248,53 @@ echo "$@"|awk '$0=tolower($0)' } +function isnumber { + case "$*" in + ''|*[!0-9]*) return 1;; + *) return 0;; + esac +} + +#function tmp_space { +# +# if ! isnumber $VOLSIZE; then +# inform "failed to determine free space (please report, this is a bug)" +# return +# fi +# +# get free temp space +# TEMP_FREE="$(df -P -k "$TEMP_DIR" 2>/dev/null | awk 'END{pos=(NF-2);if(pos>0) print $pos;}')" +# # check for free space or FAIL +# if [ $((${TEMP_FREE:-0}-${VOLSIZE:-0}*1024)) -lt 0-lt 0 ]; then +# error "Temporary file space '$TEMP_DIR' free space is smaller ($((TEMP_FREE/1024))MB) +#than one duplicity volume (${VOLSIZE}MB). +# +# Hint: Free space or change TEMP_DIR setting." +#fi +# +#} + function gpg_disabled { - echo "${GPG_KEY}${GPG_KEYS_ENC}" | grep -iq 'disabled' + echo "${GPG_KEY}" | grep -iq -e '^disabled$' +} + +# usage: join SEPARATOR "entry1" "entry2" +function join { + local SEP="$1" ENTRY OUT; shift; + for ENTRY in "$@"; do + ENTRY=${ENTRY//$SEP/\\$SEP} + [ -z "$OUT" ] && OUT=$ENTRY || OUT="$OUT$SEP$ENTRY" + done + echo $OUT } function gpg_signing { - return $(echo ${GPG_KEY_SIGN} | grep -ic 'disabled') + echo ${GPG_KEY_SIGN} | grep -v -q -e '^disabled$' } # parameter key id, key_type function gpg_keyfile { - local GPG_KEY="$1" TYPE="$2" + local GPG_KEY=$(gpg_key_legalize $1) TYPE="$2" local KEYFILE="${KEYFILE//.asc/${GPG_KEY:+.$GPG_KEY}.asc}" echo "${KEYFILE//.asc/${TYPE:+.$(tolower $TYPE)}.asc}" } @@ -1187,8 +1304,8 @@ local i FILE FOUND=0 KEY_ID="$1" KEY_TYPE="$2" KEY_FP="" ERR=0 # create a list of legacy key file names and current naming scheme # we always import pub and sec if they are avail in conf folder - local KEYFILES=( "$CONFDIR/gpgkey" "$(gpg_keyfile $KEY_ID)" \ - "$(gpg_keyfile $KEY_ID PUB)" "$(gpg_keyfile $KEY_ID SEC)") + local KEYFILES=( "$CONFDIR/gpgkey" $(gpg_keyfile "$KEY_ID") \ + $(gpg_keyfile "$KEY_ID" PUB) $(gpg_keyfile "$KEY_ID" SEC)) # Try autoimport from existing old gpgkey files # and new gpgkey.XXX.asc files (since v1.4.2) @@ -1199,8 +1316,8 @@ FOUND=1 CMD_MSG="Import keyfile '$FILE' to keyring" - run_cmd "$GPG" $GPG_OPTS --batch --import "$FILE" - if [ "$CMD_ERR" != "0" ]; then + run_cmd gpg $GPG_OPTS --batch --import "$FILE" + if [ "$?" != "0" ]; then warning "Import failed.${CMD_OUT:+\n$CMD_OUT}" ERR=1 # continue with next @@ -1214,43 +1331,44 @@ fi # try to set trust automagically - CMD_MSG="Autoset trust of key '$KEY_ID'to ultimate" - run_cmd echo $(gpg_fingerprint $KEY_ID):6: \| "$GPG" $GPG_OPTS --import-ownertrust --batch --logger-fd 1 - if [ "$CMD_ERR" = "0" ] && [ -z "$PREVIEW" ]; then + CMD_MSG="Autoset trust of key '$KEY_ID' to ultimate" + run_cmd echo $(gpg_fingerprint "$KEY_ID"):6: \| gpg $GPG_OPTS --import-ownertrust --batch --logger-fd 1 + if [ "$?" = "0" ] && [ -z "$PREVIEW" ]; then # success on all levels, we're done return $ERR fi # failover: user has to set trust manually - echo -e "For $ME to work you have to set the trust level + echo -e "For $ME_NAME to work you have to set the trust level with the command \"trust\" to \"ultimate\" (5) now. Exit the edit mode of gpg with \"quit\"." CMD_MSG="Running gpg to manually edit key '$KEY_ID'" - run_cmd sleep 5\; "$GPG" $GPG_OPTS --edit-key $KEY_ID + run_cmd sleep 5\; gpg $GPG_OPTS --edit-key "$KEY_ID" return $ERR } -# check for 8 digits and using 0x00.. here because gpg uses substring matching by default # see 'How to specify a user ID' on gpg manpage function gpg_fingerprint { - [ ${#1} -eq 8 ] \ - && local PRINT=$("$GPG" $GPG_OPTS --fingerprint 0x"$1" 2>&1|awk -F= 'NR==2{gsub(/ /,"",$2);$2=toupper($2); if ( $2 ~ /^[A-F0-9]+$/ && length($2) == 40 ) print $2; else exit 1}') \ + local PRINT=$(gpg $GPG_OPTS --fingerprint "$1" 2>&1|awk -F= 'NR==2{gsub(/ /,"",$2);$2=toupper($2); if ( $2 ~ /^[A-F0-9]+$/ && length($2) == 40 ) print $2; else exit 1}') \ && [ -n "$PRINT" ] && echo $PRINT && return 0 return 1 } function gpg_export_if_needed { - local SUCCESS FILE KEY_TYPE KEY_LIST=$1 + local SUCCESS FILE KEY_TYPE local TMPFILE="$TEMP_DIR/${ME_NAME}.$$.$(date_fix %s).gpgexp" - for KEY_ID in $KEY_LIST; do + for KEY_ID in "$@"; do # check if already exported, do it if not for KEY_TYPE in PUB SEC; do - FILE="$(gpg_keyfile $KEY_ID $KEY_TYPE)" - if [ ! -f "$FILE" ] && eval gpg_$(tolower $KEY_TYPE)_avail $KEY_ID; then + FILE="$(gpg_keyfile "$KEY_ID" $KEY_TYPE)" + if [ ! -f "$FILE" ] && eval gpg_$(tolower $KEY_TYPE)_avail \"$KEY_ID\"; then # exporting - CMD_MSG="Export $KEY_TYPE key $KEY_ID" - run_cmd $GPG $GPG_OPTS --armor --export"$(test "SEC" = "$KEY_TYPE" && echo -secret-keys)"" $KEY_ID >> \"$TMPFILE\"" + CMD_MSG="Export $KEY_TYPE key '$KEY_ID'" + # gpg2.1 insists on passphrase here, gpg2.0- happily exports w/o it + # we pipe an empty string when GPG_PW is not set to avoid gpg silently waiting for input + run_cmd echo $(qw $GPG_PW) \| gpg $GPG_OPTS --passphrase-fd 0 --armor --export"$(test "SEC" = "$KEY_TYPE" && echo -secret-keys)"" $(qw $KEY_ID) >> \"$TMPFILE\"" + CMD_ERR=$? if [ "$CMD_ERR" = "0" ]; then CMD_MSG="Write file '"$(basename "$FILE")"'" @@ -1269,22 +1387,31 @@ done done - [ -n "$SUCCESS" ] && inform "$ME exported new keys to your profile. + [ -n "$SUCCESS" ] && inform "$ME_NAME exported new keys to your profile. You should backup your changed profile folder now and store it in a safe place." } +# replace all non-alnum chars with underscore (for file operations) +function gpg_key_legalize { + echo $* | awk '{gsub(/[^a-zA-Z0-9]/,"_",$0); print}' +} + function gpg_key_cache { local RES + local MODE=$1 + shift local PREFIX="GPG_KEY" - local CACHE="PREFIX_$1_$2" - if [ "$1" = "RESET" ]; then - eval unset PREFIX_PUB_$2 PREFIX_SEC_$2 + local SUFFIX=$(gpg_key_legalize "$@") + local KEYID="$*" + local CACHE="${PREFIX}_${MODE}_${SUFFIX}" + if [ "$MODE" = "RESET" ]; then + eval unset ${PREFIX}_PUB_$SUFFIX ${PREFIX}_SEC_$SUFFIX return 255 elif ! var_isset "$CACHE"; then - if [ "$1" = "PUB" ]; then - RES=$("$GPG" $GPG_OPTS --list-key "$2" > /dev/null 2>&1; echo -n $?) - elif [ "$1" = "SEC" ]; then - RES=$("$GPG" $GPG_OPTS --list-secret-key "$2" > /dev/null 2>&1; echo -n $?) + if [ "$MODE" = "PUB" ]; then + RES=$(gpg $GPG_OPTS --list-key "$KEYID" > /dev/null 2>&1; echo -n $?) + elif [ "$MODE" = "SEC" ]; then + RES=$(gpg $GPG_OPTS --list-secret-key "$KEYID" > /dev/null 2>&1; echo -n $?) else return 255 fi @@ -1294,34 +1421,34 @@ } function gpg_pub_avail { - gpg_key_cache PUB $1 + gpg_key_cache PUB "$@" } function gpg_sec_avail { - gpg_key_cache SEC $1 + gpg_key_cache SEC "$@" } function gpg_key_format { echo $1 | grep -q '^[0-9a-fA-F]\{8\}$' } -function gpg_split_keyset { - awk "BEGIN{ keys=toupper(\"$@\"); gsub(/[^A-Z0-9]/,\" \",keys); print keys }" -} - -function gpg_join_keyset { - local KEY_ID OUT - for KEY_ID in $@; do - [ -z "$OUT" ] && OUT=$KEY_ID || OUT=${OUT},${KEY_ID} - done - echo $OUT +#function gpg_split_keyset { +# return +# awk "BEGIN{ keys=toupper(\"$@\"); gsub(/[^A-Z0-9]/,\" \",keys); print keys }" +#} + +# splits a comma separated line into lines, respects escaped commas +function gpg_split_keyset2 { + local LIST + LIST=$(echo "$@" | awk '{ gsub(/,/,"\n",$0); gsub(/\\\n/,",",$0); print $0 }') + echo -e "$LIST" } function gpg_prefix_keyset { - local KEY_ID OUT KEYSET - [ -n "$2" ] && eval "local KEYSET=\"\${$2[@]}\"" - for KEY_ID in $KEYSET; do - OUT=${OUT}${1}${KEY_ID} + local PREFIX="$1" OUT="" + shift + for KEY_ID in "$@"; do + OUT="${OUT} $PREFIX $(qw ${KEY_ID})" done echo $OUT } @@ -1335,18 +1462,17 @@ print $0; exit}' "$CONF" } +# return success if at least one secret key is available function gpg_key_decryptable { - # decryption needs pass, might be empty, but must be set - #var_isset 'GPG_PW' || return 1 local KEY_ID - for KEY_ID in ${GPG_KEYS_ENC[@]}; do - gpg_sec_avail $KEY_ID && return 0 + for KEY_ID in "${GPG_KEYS_ENC_ARRAY[@]}"; do + gpg_sec_avail "$KEY_ID" && return 0 done return 1 } function gpg_symmetric { - [ -z "${GPG_KEY}${GPG_KEYS_ENC}" ] + [ -z "${GPG_KEY}${GPG_KEYS_ENC_ARRAY}" ] } # checks for max two params if they are set, typically GPG_PW & GPG_PW_SIGN @@ -1387,6 +1513,27 @@ return $ERR } +function gpg_custom_binary { + var_isset GPG && [ "$GPG" != "$DEFAULT_GPG" ] &&\ + echo "--gpg-binary $(qw "$GPG")" +} + +function gpg_binary { + local BIN + var_isset GPG && BIN="$GPG" || BIN="$DEFAULT_GPG" + echo "$BIN" +} + +function gpg_avail { + lookup $(gpg_binary) +} + +# enforce the use our selected gpg binary +function gpg { + command $(gpg_binary) "$@" +} +export -f gpg + # start of script ####################################################################### # confidentiality first, all we create is only readable by us @@ -1396,9 +1543,9 @@ [ -n "$ME_LONG" ] && [ -x "$ME_LONG" ] || error "$ME missing. Executable & available in path? ($ME_LONG)" if [ ${#@} -eq 1 ]; then - cmd="${1}" + cmd="${1}" else - FTPLCFG="${1}" ; cmd="${2}" + FTPLCFG="${1}" ; cmd="${2}" fi # deal with command before profile validation calls @@ -1438,6 +1585,11 @@ exit 0 ;; version|-version|--version|-v|-V) + # profile can override GPG, so import it if it was given + var_isset FTPLCFG && { + set_config + [ -r "$CONF" ] && . "$CONF" || warning "Cannot import config '$CONF'." + } version_info_using exit 0 ;; @@ -1461,13 +1613,18 @@ echo "Start $ME v$ME_VERSION, time is $(date_fix '%F %T')." # check system environment -DUPLICITY="$(which duplicity 2>/dev/null)" -[ -z "$DUPLICITY" ] && error_path "duplicity missing. installed und available in path?" + +# is duplicity avail +lookup duplicity || error_path "duplicity missing. installed und available in path?" # init, exec duplicity version check info duplicity_version_get duplicity_version_check -[ -z "$(which awk 2>/dev/null)" ] && error_path "awk missing. installed und available in path?" +# check for certain important helper programs +for f in awk grep; do + lookup "$f" || \ + error_path "$f missing. installed und available in path?" +done ### read configuration set_config @@ -1534,12 +1691,6 @@ }') eval ${TARGET_SPLIT_URL} -# check if backend specific software is in path -[ -n "$(echo ${TARGET_URL_PROT} | grep -i -e '^ftp://$')" ] && \ - [ -z "$(which ncftp 2>/dev/null)" ] && error_path "Protocol 'ftp' needs ncftp. Installed und available in path?" -[ -n "$(echo ${TARGET_URL_PROT} | grep -i -e '^ftps://$')" ] && \ - [ -z "$(which lftp 2>/dev/null)" ] && error_path "Protocol 'ftps' needs lftp. Installed und available in path?" - # fetch commmand from parameters ######################################################## # Hint: cmds is also used to check if authentification info sufficient in the next step cmds="$2"; shift 2 @@ -1609,54 +1760,25 @@ Hint: Remove conflicting setting." fi -# check if authentication information sufficient -if ( ( ! var_isset 'TARGET_USER' && ! var_isset 'TARGET_URL_USER' ) && \ - ( ! var_isset 'TARGET_PASS' && ! var_isset 'TARGET_URL_PASS' ) ); then - # ok here some exceptions: - # protocols that do not need passwords - # s3[+http] only needs password for write operations - if [ -n "$( tolower "${TARGET_URL_PROT}" | grep -e '^\(dpbx\|file\|tahoe\|ssh\|scp\|sftp\|swift\)://$' )" ]; then - : # all is well file/tahoe do not need passwords, ssh might use key auth - elif [ -n "$(tolower "${TARGET_URL_PROT}" | grep -e '^s3\(\+http\)\?://$')" ] && \ - [ -z "$(echo ${cmds} | grep -e '\(bkp\|incr\|full\|purge\|cleanup\)')" ]; then - : # still fine, it's possible to read only access configured buckets anonymously - else - error " Backup target credentials needed but not set in conf file - '$CONF'. - Setting TARGET_USER or TARGET_PASS or the corresponding values in TARGET url - are missing. Some protocols only might need it for write access to the backup - repository (commands: bkp,backup,full,incr,purge) but not for read only access - (e.g. verify,list,restore,fetch). - - Hints: - Add the credentials (user,password) to the conf file. - To force an empty password set TARGET_PASS='' or TARGET='prot://user:@host..'. -" - fi -fi - # GPG config plausibility check1 (disabled check) ############################# if gpg_disabled; then - : # encryption disabled, all is well - + : # encryption disabled, all is well elif [ -z "${GPG_KEY}${GPG_KEYS_ENC}${GPG_KEY_SIGN}" ] && ! var_isset 'GPG_PW'; then - warning "GPG_KEY and GPG_PW are empty or not set in conf file + warning "GPG_KEY, GPG_KEYS_ENC, GPG_KEY_SIGN and GPG_PW are empty/not set in conf file '$CONF'. Will disable encryption for duplicity now. Hint: If you really want to use _no_ encryption you can disable this warning by setting GPG_KEY='disabled' in conf file." - GPG_KEY='disabled' + GPG_KEY='disabled' fi # GPG availability check (now we know if gpg is really needed)################# if ! gpg_disabled; then - GPG="$(which gpg 2>/dev/null)" - [ -z "$GPG" ] && error_path "gpg missing. installed und available in path?" + gpg_avail || error_path "gpg '$(gpg_binary)' missing. installed und available in path?" fi - # Output versions info ######################################################## using_info @@ -1671,29 +1793,20 @@ '$CONF'." fi -# disabled as keys can really be given in too many forms e.g. short/long id, fingerprint, email, name ... -## check gpg keys format -#for KEY_SET_NAME in GPG_KEY GPG_KEYS_ENC $(gpg_signing && echo -n GPG_KEY_SIGN); do -# eval KEY_SET="\${${KEY_SET_NAME}}" -# for KEY_ID in $(gpg_split_keyset "$KEY_SET"); do -# # test format [ ! $(echo $GPG_KEY | grep '^[0-9a-fA-F]\{8\}$') ] not set correct (8 digit ID) or -# gpg_key_format ${KEY_ID} || \ -# error_gpg "GPG key '${KEY_ID}' set in '${KEY_SET_NAME}' is not \na valid 8 character hex digit string e.g. '012345AB'." -# done -#done - -# create enc gpg keys array, for further processing -GPG_KEYS_ENC=( $(gpg_split_keyset ${GPG_KEY}) $(gpg_split_keyset ${GPG_KEYS_ENC}) ) +# create array of gpg encr keys, for further processing +OIFS="$IFS" IFS=$'\n' +GPG_KEYS_ENC_ARRAY=( $( gpg_split_keyset2 ${GPG_KEY},${GPG_KEYS_ENC} ) ) +IFS="$OIFS" # check gpg encr public keys availability -for (( i = 0 ; i < ${#GPG_KEYS_ENC[@]} ; i++ )); do - KEY_ID=${GPG_KEYS_ENC[$i]} +for (( i = 0 ; i < ${#GPG_KEYS_ENC_ARRAY[@]} ; i++ )); do + KEY_ID="${GPG_KEYS_ENC_ARRAY[$i]}" # test availability, try to import, retest - if ! gpg_pub_avail ${KEY_ID}; then + if ! gpg_pub_avail "${KEY_ID}"; then echo "Encryption public key '${KEY_ID}' not found." gpg_import "${KEY_ID}" PUB - gpg_key_cache RESET ${KEY_ID} - gpg_pub_avail ${KEY_ID} || error_gpg_key "${KEY_ID}" "Public" + gpg_key_cache RESET "${KEY_ID}" + gpg_pub_avail "${KEY_ID}" || error_gpg_key "${KEY_ID}" "Public" fi done @@ -1703,19 +1816,19 @@ echo "Signing disabled per configuration." # try first key, if one set elif ! var_isset 'GPG_KEY_SIGN'; then - KEY_ID=${GPG_KEYS_ENC[0]} + KEY_ID="${GPG_KEYS_ENC_ARRAY[0]}" if [ -z "${KEY_ID}" ]; then echo "Signing disabled. Not GPG_KEY entries in config." GPG_KEY_SIGN='disabled' else # use avail OR try import OR fail if gpg_sec_avail "${KEY_ID}"; then - GPG_KEY_SIGN=${KEY_ID} + GPG_KEY_SIGN="${KEY_ID}" else gpg_import "${KEY_ID}" SEC - gpg_key_cache RESET ${KEY_ID} + gpg_key_cache RESET "${KEY_ID}" if gpg_sec_avail "${KEY_ID}"; then - GPG_KEY_SIGN=${KEY_ID} + GPG_KEY_SIGN="${KEY_ID}" fi fi @@ -1728,14 +1841,12 @@ fi fi else - KEY_ID=${GPG_KEY_SIGN} - if ! gpg_sec_avail ${KEY_ID}; then + KEY_ID="${GPG_KEY_SIGN}" + if ! gpg_sec_avail "${KEY_ID}"; then inform "Secret signing key defined in setting GPG_KEY_SIGN='${KEY_ID}' not found.\nTry to import." gpg_import "${KEY_ID}" SEC - gpg_key_cache RESET ${KEY_ID} - gpg_sec_avail ${KEY_ID} || error_gpg_key "${KEY_ID}" "Private" - else - echo "Use configured key '${KEY_ID}' as signing key." + gpg_key_cache RESET "${KEY_ID}" + gpg_sec_avail "${KEY_ID}" || error_gpg_key "${KEY_ID}" "Private" fi fi @@ -1756,14 +1867,15 @@ This is unfortunately impossible. For details see duplicity manpage, section 'A Note On Symmetric Encryption And Signing'. -Tip: Separate signing keys may have empty passwords e.g. GPG_PW_SIGN=''." +Tip: Separate signing keys may have empty passwords e.g. GPG_PW_SIGN=''. +Tip2: Use gpg-agent." fi # key enc can deal without, but might profit from gpg-agent # if GPG_PW is not set alltogether # if signing key is different from first (main) enc key (we can only pipe one pass into gpg) if ! gpg_symmetric && \ ( ! var_isset GPG_PW || \ - ( gpg_signing && ! var_isset GPG_PW_SIGN && [ "$GPG_KEY_SIGN" != "${GPG_KEYS_ENC[0]}" ] ) ); then + ( gpg_signing && ! var_isset GPG_PW_SIGN && [ "$GPG_KEY_SIGN" != "${GPG_KEYS_ENC_ARRAY[0]}" ] ) ); then GPG_AGENT_ERR=$(gpg_agent_avail ; echo $?) if [ "$GPG_AGENT_ERR" -eq 1 ]; then @@ -1780,34 +1892,30 @@ fi # config plausibility check - SPACE ########################################### -# is tmp writeable -# is tmp big enough -if [ ! -d "$TEMP_DIR" ]; then + +# is tmp is a folder +CMD_MSG="Checking TEMP_DIR '${TEMP_DIR}' is a folder" +run_cmd test -d "$TEMP_DIR" +if [ "$?" != "0" ]; then error "Temporary file space '$TEMP_DIR' is not a directory." -elif [ ! -w "$TEMP_DIR" ]; then +fi +# is tmp writeable +CMD_MSG="Checking TEMP_DIR '${TEMP_DIR}' is writable" +run_cmd test -w "$TEMP_DIR" +if [ "$?" != "0" ]; then error "Temporary file space '$TEMP_DIR' not writable." fi + # get volsize, default duplicity volume size is 25MB since v0.5.07 VOLSIZE=${VOLSIZE:-25} -# get free temp space -TEMP_FREE="$(df $TEMP_DIR 2>/dev/null | awk 'END{pos=(NF-2);if(pos>0) print $pos;}')" -# check for free space or FAIL -if [ "$((${TEMP_FREE:-0}-${VOLSIZE:-0}*1024))" -lt 0 ]; then - error "Temporary file space '$TEMP_DIR' free space is smaller ($((TEMP_FREE/1024))MB) -than one duplicity volume (${VOLSIZE}MB). - - Hint: Free space or change TEMP_DIR setting." -fi +# double if asynch is on +echo $@ $DUPL_PARAMS | grep -q -e '--asynchronous-upload' && FACTOR=2 || FACTOR=1 + +# TODO: check for enough (async= upload space and WARN only +# use function tmp_space +#echo TODO: reimplent tmp space check -# check for enough async upload space and WARN only -if [ $((${TEMP_FREE:-0}-2*${VOLSIZE:-0}*1024)) -lt 0 ]; then - warning "Temporary file space '$TEMP_DIR' free space is smaller ($((TEMP_FREE/1024))MB) -than two duplicity volumes (2x${VOLSIZE}MB). This can lead to problems when -using the --asynchronous-upload option. - - Hint: Free space or change TEMP_DIR setting." -fi # test - GPG SANITY ##################################################################### # if encryption is disabled, skip this whole section @@ -1825,32 +1933,34 @@ # signing enabled? if gpg_signing; then - CMD_PARAM_SIGN="--sign --default-key ${GPG_KEY_SIGN}" - CMD_MSG_SIGN="Sign with ${GPG_KEY_SIGN}" + CMD_PARAM_SIGN="--sign --default-key $(qw ${GPG_KEY_SIGN})" + CMD_MSG_SIGN="Sign with '${GPG_KEY_SIGN}'" fi # using keys -if [ ${#GPG_KEYS_ENC[@]} -gt 0 ]; then +if [ ${#GPG_KEYS_ENC_ARRAY[@]} -gt 0 ]; then - for KEY_ID in ${GPG_KEYS_ENC[@]}; do - CMD_PARAMS="$CMD_PARAMS -r ${KEY_ID}" + for KEY_ID in "${GPG_KEYS_ENC_ARRAY[@]}"; do + CMD_PARAMS="$CMD_PARAMS -r $(qw ${KEY_ID})" done # check encrypting - CMD_MSG="Test - Encrypt to $(gpg_join_keyset ${GPG_KEYS_ENC[@]})${CMD_MSG_SIGN:+ & $CMD_MSG_SIGN}" - run_cmd $(gpg_pass_pipein GPG_PW_SIGN GPG_PW) $GPG $CMD_PARAM_SIGN $(gpg_param_passwd GPG_PW_SIGN GPG_PW) $CMD_PARAMS $GPG_USEAGENT --status-fd 1 $GPG_OPTS -o "${GPG_TEST}_ENC" -e "$ME_LONG" + CMD_MSG="Test - Encrypt to '$(join "','" "${GPG_KEYS_ENC_ARRAY[@]}")'${CMD_MSG_SIGN:+ & $CMD_MSG_SIGN}" + run_cmd $(gpg_pass_pipein GPG_PW_SIGN GPG_PW) gpg $CMD_PARAM_SIGN $(gpg_param_passwd GPG_PW_SIGN GPG_PW) $CMD_PARAMS $GPG_USEAGENT --status-fd 1 $GPG_OPTS -o "${GPG_TEST}_ENC" -e "$ME_LONG" + CMD_ERR=$? if [ "$CMD_ERR" != "0" ]; then KEY_NOTRUST=$(echo "$CMD_OUT"|awk '/^\[GNUPG:\] INV_RECP 10/ { print $4 }') [ -n "$KEY_NOTRUST" ] && HINT="Key '${KEY_NOTRUST}' seems to be untrusted. If you really trust this key try to - 'gpg --edit-key $KEY_NOTRUST' and raise the trust level to ultimate. If you + 'gpg --edit-key "$KEY_NOTRUST"' and raise the trust level to ultimate. If you can trust all of your keys set GPG_OPTS='--trust-model always' in conf file." error_gpg_test "Encryption failed (Code $CMD_ERR).${CMD_OUT:+\n$CMD_OUT}" "$HINT" fi # check decrypting CMD_MSG="Test - Decrypt" - gpg_key_decryptable || CMD_DISABLED="No matching secret key or GPG_PW not set." - run_cmd $(gpg_pass_pipein GPG_PW) "$GPG" $(gpg_param_passwd GPG_PW) $GPG_OPTS -o "${GPG_TEST}_DEC" $GPG_USEAGENT -d "${GPG_TEST}_ENC" + gpg_key_decryptable || CMD_DISABLED="No matching secret key available." + run_cmd $(gpg_pass_pipein GPG_PW) gpg $(gpg_param_passwd GPG_PW) $GPG_OPTS -o "${GPG_TEST}_DEC" $GPG_USEAGENT -d "${GPG_TEST}_ENC" + CMD_ERR=$? if [ "$CMD_ERR" != "0" ]; then error_gpg_test "Decryption failed.${CMD_OUT:+\n$CMD_OUT}" @@ -1860,14 +1970,16 @@ else # check encrypting CMD_MSG="Test - Encryption with passphrase${CMD_MSG_SIGN:+ & $CMD_MSG_SIGN}" - run_cmd $(gpg_pass_pipein GPG_PW) "$GPG" $GPG_OPTS $CMD_PARAM_SIGN --passphrase-fd 0 -o "${GPG_TEST}_ENC" --batch -c "$ME_LONG" + run_cmd $(gpg_pass_pipein GPG_PW) gpg $GPG_OPTS $CMD_PARAM_SIGN --passphrase-fd 0 -o "${GPG_TEST}_ENC" --batch -c "$ME_LONG" + CMD_ERR=$? if [ "$CMD_ERR" != "0" ]; then error_gpg_test "Encryption failed.${CMD_OUT:+\n$CMD_OUT}" fi # check decrypting CMD_MSG="Test - Decryption with passphrase" - run_cmd $(gpg_pass_pipein GPG_PW) "$GPG" $GPG_OPTS --passphrase-fd 0 -o "${GPG_TEST}_DEC" --batch -d "${GPG_TEST}_ENC" + run_cmd $(gpg_pass_pipein GPG_PW) gpg $GPG_OPTS --passphrase-fd 0 -o "${GPG_TEST}_DEC" --batch -d "${GPG_TEST}_ENC" + CMD_ERR=$? if [ "$CMD_ERR" != "0" ]; then error_gpg_test "Decryption failed.${CMD_OUT:+\n$CMD_OUT}" fi @@ -1877,7 +1989,7 @@ CMD_MSG="Test - Compare" [ -r "${GPG_TEST}_DEC" ] || CMD_DISABLED="File not found. Nothing to compare." run_cmd "test \"\$(cat '$ME_LONG')\" = \"\$(cat '${GPG_TEST}_DEC')\"" - +CMD_ERR=$? if [ "$CMD_ERR" = "0" ]; then cleanup_gpgtest else @@ -1893,7 +2005,7 @@ [ -f "$EXCLUDE" ] || touch "$EXCLUDE" # export only used keys, if bkp not already exists ###################################### -gpg_export_if_needed "${GPG_KEYS_ENC[@]} $(gpg_signing && echo $GPG_KEY_SIGN)" +gpg_export_if_needed "${GPG_KEYS_ENC_ARRAY[@]}" "$(gpg_signing && echo $GPG_KEY_SIGN)" # command execution ##################################################################### @@ -1907,102 +2019,71 @@ var_isset 'TARGET_USER' && TARGET_URL_USER="$TARGET_USER" var_isset 'TARGET_PASS' && TARGET_URL_PASS="$TARGET_PASS" -# build target backend data depending on protocol +# issue some warnings case "$(tolower "${TARGET_URL_PROT%%:*}")" in - 's3'|'s3+http') - BACKEND_PARAMS="AWS_ACCESS_KEY_ID='${TARGET_URL_USER}' AWS_SECRET_ACCESS_KEY='${TARGET_URL_PASS}'" - BACKEND_URL="${TARGET_URL_PROT}${TARGET_URL_HOSTPATH}" - ;; - 'gs') - BACKEND_PARAMS="GS_ACCESS_KEY_ID='${TARGET_URL_USER}' GS_SECRET_ACCESS_KEY='${TARGET_URL_PASS}'" - BACKEND_URL="${TARGET_URL_PROT}${TARGET_URL_HOSTPATH}" - ;; 'cf+http') - # respect potentially set cloudfile env vars - var_isset 'CLOUDFILES_USERNAME' && TARGET_URL_USER="$CLOUDFILES_USERNAME" - var_isset 'CLOUDFILES_APIKEY' && TARGET_URL_PASS="$CLOUDFILES_APIKEY" - # add them to duplicity params - var_isset 'TARGET_URL_USER' && \ - BACKEND_PARAMS="CLOUDFILES_USERNAME=$(qw "${TARGET_URL_USER}")" - var_isset 'TARGET_URL_PASS' && \ - BACKEND_PARAMS="$BACKEND_PARAMS CLOUDFILES_APIKEY=$(qw "${TARGET_URL_PASS}")" - BACKEND_URL="${TARGET_URL_PROT}${TARGET_URL_HOSTPATH}" # info on missing AUTH_URL if ! var_isset 'CLOUDFILES_AUTHURL'; then - echo -e "INFO: No CLOUDFILES_AUTHURL defined (in conf).\n Will use default from python-cloudfiles (probably rackspace)." - else - BACKEND_PARAMS="$BACKEND_PARAMS CLOUDFILES_AUTHURL=$(qw "${CLOUDFILES_AUTHURL}")" + inform "No CLOUDFILES_AUTHURL exported (in conf). +Will use default which is probably rackspace." fi ;; - 'file'|'tahoe'|'dpbx') - BACKEND_URL="${TARGET_URL_PROT}${TARGET_URL_HOSTPATH}" - ;; 'swift') - BACKEND_URL="${TARGET_URL_PROT}${TARGET_URL_HOSTPATH}" - # respect possibly set swift env vars - var_isset 'SWIFT_USERNAME' && TARGET_URL_USER="$SWIFT_USERNAME" - var_isset 'SWIFT_PASSWORD' && TARGET_URL_PASS="$SWIFT_PASSWORD" - # add them to duplicity params like with cloudfile to make it look standardized - var_isset 'TARGET_URL_USER' && \ - BACKEND_PARAMS="$BACKEND_PARAMS SWIFT_USERNAME=$(qw "${TARGET_URL_USER}")" - var_isset 'SWIFT_AUTHURL' && \ - BACKEND_PARAMS="$BACKEND_PARAMS SWIFT_AUTHURL=$(qw "${SWIFT_AUTHURL}")" - ( var_isset 'TARGET_URL_USER' && ! var_isset 'SWIFT_AUTHURL' ) &&\ + # info on possibly missing AUTH_URL + var_isset 'SWIFT_AUTHURL' ||\ warning "\ -Swift will probably fail because the conf var SWIFT_AUTHURL was not defined!" - var_isset 'SWIFT_AUTHVERSION' && \ - BACKEND_PARAMS="$BACKEND_PARAMS SWIFT_AUTHVERSION=$(qw "${SWIFT_AUTHVERSION}")" - var_isset 'TARGET_URL_PASS' && \ - BACKEND_PARAMS="$BACKEND_PARAMS SWIFT_PASSWORD=$(qw "${TARGET_URL_PASS}")" - ;; +Swift will probably fail because the conf var SWIFT_AUTHURL was not exported!" + ;; 'rsync') # everything in url (this backend does not support pass in env var) # this is obsolete from version 0.6.10 (buggy), hopefully fixed in 0.6.11 # print warning older version is detected - var_isset 'TARGET_URL_USER' && BACKEND_CREDS="$(url_encode "${TARGET_URL_USER}")" - if duplicity_version_lt 610; then + duplicity_version_lt 610 && warning "\ Duplicity version '$DUPL_VERSION' does not support providing the password as env var for rsync backend. For security reasons you should consider to update to a version greater than '0.6.10' of duplicity." - var_isset 'TARGET_URL_PASS' && BACKEND_CREDS="${BACKEND_CREDS}:$(url_encode "${TARGET_URL_PASS}")" - else - var_isset 'TARGET_URL_PASS' && BACKEND_PARAMS="FTP_PASSWORD=$(qw "${TARGET_URL_PASS}")" - fi - var_isset 'BACKEND_CREDS' && BACKEND_CREDS="${BACKEND_CREDS}@" - BACKEND_URL="${TARGET_URL_PROT}${BACKEND_CREDS}${TARGET_URL_HOSTPATH}" + ;; +esac + + +# for all protocols we put username in url and pass into env var +# for sec�rity reasons, we url_encode username to protect special chars +# first sortout backends with special ways to handle password +case "$(tolower "${TARGET_URL_PROT%%:*}")" in + 'imap'|'imaps') + var_isset 'TARGET_URL_PASS' && BACKEND_PARAMS="IMAP_PASSWORD=$(qw "${TARGET_URL_PASS}")" ;; *) - # for all other protocols we put username in url and pass into env var - # for sec�rity reasons, we url_encode username to protect special chars - var_isset 'TARGET_URL_USER' && - BACKEND_CREDS="$(url_encode "${TARGET_URL_USER}")@" - # sortout backends with special ways to handle password + # add needed param for ssh backend case "$(tolower "${TARGET_URL_PROT%%:*}")" in - 'imap'|'imaps') - var_isset 'TARGET_URL_PASS' && BACKEND_PARAMS="IMAP_PASSWORD=$(qw "${TARGET_URL_PASS}")" - ;; 'ssh'|'sftp'|'scp') - # ssh backend wants to be told that theres a pass to use + # ssh backend wants to be told that there is a pass to use var_isset 'TARGET_URL_PASS' && \ DUPL_PARAMS="$DUPL_PARAMS --ssh-askpass" && \ BACKEND_PARAMS="FTP_PASSWORD=$(qw "${TARGET_URL_PASS}")" - ;; - *) - # rest uses FTP_PASS var - var_isset 'TARGET_URL_PASS' && \ - BACKEND_PARAMS="FTP_PASSWORD=$(qw "${TARGET_URL_PASS}")" - ;; + ;; esac - BACKEND_URL="${TARGET_URL_PROT}${BACKEND_CREDS}${TARGET_URL_HOSTPATH}" + # rest uses FTP_PASS var + var_isset 'TARGET_URL_PASS' && \ + BACKEND_PARAMS="FTP_PASSWORD=$(qw "${TARGET_URL_PASS}")" ;; esac +# insert url encoded username into target url if needed +if var_isset 'TARGET_URL_USER'; then + BACKEND_URL="${TARGET_URL_PROT}$(url_encode "${TARGET_URL_USER}")@${TARGET_URL_HOSTPATH}" +else + BACKEND_URL="$TARGET" +fi + # protect eval from special chars in url (e.g. open ')' in password, # spaces in path, quotes) happens above in duplify() via quotewrap() SOURCE="$SOURCE" BACKEND_URL="$BACKEND_URL" EXCLUDE="$EXCLUDE" +# since 0.7.03 --exclude-globbing-filelist is deprecated +EXCLUDE_PARAM="--exclude$(duplicity_version_lt 703 && echo -globbing)-filelist" # replace magic separators to condition command equivalents (+=and,-=or) cmds=$(awk -v cmds="$cmds" "BEGIN{ gsub(/\+/,\"_and_\",cmds); gsub(/\-/,\"_or_\",cmds); print cmds}") @@ -2017,28 +2098,42 @@ # raise index in cmd array for pre/post param var_isset 'CMD_NO' && CMD_NO=$((++CMD_NO)) || CMD_NO=0 -# get prev/nextcmd vars -nextno=$(($CMD_NO+1)) -[ "$nextno" -lt "${#CMDS[@]}" ] && CMD_NEXT=${CMDS[$nextno]} || CMD_NEXT='END' -prevno=$(($CMD_NO-1)) -[ "$prevno" -ge 0 ] && CMD_PREV=${CMDS[$prevno]} || CMD_PREV='START' - # deal with condition "commands" +unset SKIP_NOW if var_isset 'CMD_SKIP' && [ $CMD_SKIP -gt 0 ]; then echo -e "\n--- Skipping command $(toupper $cmd) ! ---" CMD_SKIP=$(($CMD_SKIP - 1)) - continue + SKIP_NOW="yes" elif [ "$cmd" == 'and' ] && [ "$CMD_ERR" -ne "0" ]; then CMD_SKIP=1 - continue + SKIP_NOW="yes" elif [ "$cmd" == 'or' ] && [ "$CMD_ERR" -eq "0" ]; then CMD_SKIP=1 - continue + SKIP_NOW="yes" elif [ "$cmd" == 'and' ] || [ "$cmd" == 'or' ]; then unset 'CMD_SKIP'; + SKIP_NOW="yes" +fi + +# sum up how many commands we skip and actually skip +if [ -n "$SKIP_NOW" ]; then + CMD_SKIPPED=$((${CMD_SKIPPED-0} + 1)) continue fi +# get prev/nextcmd vars +nextno=$(($CMD_NO+1)) +[ "$nextno" -lt "${#CMDS[@]}" ] && CMD_NEXT=${CMDS[$nextno]} || CMD_NEXT='END' +# get previous command minus skipped commands +prevno=$(( $CMD_NO - ${CMD_SKIPPED-0} - 1 )); unset CMD_SKIPPED +[ "$prevno" -ge 0 ] && CMD_PREV=${CMDS[$prevno]} || CMD_PREV='START' + +# export some useful env vars for external scripts/programs to use +export CONFDIR SOURCE TARGET_URL_PROT TARGET_URL_HOSTPATH \ + TARGET_URL_USER TARGET_URL_PASS \ + GPG_KEYS_ENC=$(join "\n" "${GPG_KEYS_ENC_ARRAY[@]}") GPG_KEY_SIGN \ + GPG_PW CMD_PREV CMD_NEXT CMD_ERR + # save start time RUN_START=$(date_fix %s)$(nsecs) # user info @@ -2055,25 +2150,37 @@ ( run_script "$script" ) ;; 'bkp') - duplify -- "${dupl_opts[@]}" --exclude-globbing-filelist "$EXCLUDE" \ + duplify -- "${dupl_opts[@]}" $EXCLUDE_PARAM "$EXCLUDE" \ "$SOURCE" "$BACKEND_URL" ;; 'incr') - duplify incr -- "${dupl_opts[@]}" --exclude-globbing-filelist "$EXCLUDE" \ + duplify incr -- "${dupl_opts[@]}" $EXCLUDE_PARAM "$EXCLUDE" \ "$SOURCE" "$BACKEND_URL" ;; 'full') - duplify full -- "${dupl_opts[@]}" --exclude-globbing-filelist "$EXCLUDE" \ + duplify full -- "${dupl_opts[@]}" $EXCLUDE_PARAM "$EXCLUDE" \ "$SOURCE" "$BACKEND_URL" ;; 'verify') - duplify verify -- "${dupl_opts[@]}" --exclude-globbing-filelist "$EXCLUDE" \ + TIME="${ftpl_pars[0]:+"-t ${ftpl_pars[0]}"}" + duplify verify -- $TIME "${dupl_opts[@]}" $EXCLUDE_PARAM "$EXCLUDE" \ "$BACKEND_URL" "$SOURCE" ;; + 'verifypath') + TIME="${ftpl_pars[2]:+"-t ${ftpl_pars[2]}"}" + IN_PATH="${ftpl_pars[0]}"; OUT_PATH="${ftpl_pars[1]}"; + ( [ -z "$IN_PATH" ] || [ -z "$OUT_PATH" ] ) && error " Missing parameter <rel_bkp_path> or <local_path> for verifyPath. + + Hint: + Syntax is -> $ME <profile> verifyPath <rel_bkp_path> <local_path> [<age>]" + + duplify verify -- $TIME "${dupl_opts[@]}" $EXCLUDE_PARAM "$EXCLUDE" \ + --file-to-restore "$IN_PATH" "$BACKEND_URL" "$OUT_PATH" + ;; 'list') # time param exists since 0.5.10+ - TIME="${ftpl_pars[0]:-now}" - duplify list-current-files -- -t "$TIME" "${dupl_opts[@]}" "$BACKEND_URL" + TIME="${ftpl_pars[0]:+"-t ${ftpl_pars[0]}"}" + duplify list-current-files -- $TIME "${dupl_opts[@]}" "$BACKEND_URL" ;; 'cleanup') duplify cleanup -- "${dupl_opts[@]}" "$BACKEND_URL" @@ -2129,7 +2236,8 @@ esac CMD_ERR=$? -RUN_END=$(date_fix %s)$(nsecs) ; RUNTIME=$(( $RUN_END - $RUN_START )) +RUN_END=$(date_fix %s)$(nsecs) +RUNTIME=$(( $RUN_END - $RUN_START )) # print message on error; set error code if [ "$CMD_ERR" -ne 0 ]; then