diff --git a/backup.cfg b/backup.cfg deleted file mode 100644 index a676cf2..0000000 --- a/backup.cfg +++ /dev/null @@ -1,11 +0,0 @@ -protocol=ssh # ftp, sftp, ftps, ssh or local -remote_host=hostname.tld -backup_dir=relative_or_full_path -#snap_file=/var/backup/snapshot.list -#exclude_list=/usr/local/etc/backup/excludes.list -compress_format=xz # gz, bz2, xz or empty for non-compressed -remote_user=username -remote_pass=PassWd -source_dirs=( '/home/user/source1:/var/backup/snapshot.list' ) - -# vim: ft=zsh diff --git a/backup.zsh b/backup.zsh index 5d7e64a..6b13d12 100755 --- a/backup.zsh +++ b/backup.zsh @@ -1,71 +1,111 @@ #!/usr/bin/env zsh -self_name=$0 -default_cfg='/usr/local/etc/backup.cfg' -default_postfix=$(date +%F-%H%M) +# +# The MIT License (MIT) +# +# Copyright (c) 2014 Von Random +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# +self_name=$0 + +# some hardcoded defaults +default_cfg='/etc/backup.zsh.cfg' +default_postfix=$(date +%F-%H%M) +default_ftp_port='21' +default_ssh_port='22' + +# echo to stderr function err { - [[ -n $1 ]] && echo $1 >&2 + [[ -n $1 ]] && printf "%s\n" $1 >&2 } +# standard configuration error message to stderr function cfg_err { - [[ -n $1 ]] && echo "$1 is not set in configuration, but is required by $0 to work." >&2 + [[ -n $1 ]] && printf "%s is not set in configuration, but is required by %s to work.\n" $1 $self_name >&2 } +# print help function usage { - echo "usage: $self_name [--help|--config] - --help -h - show this message - --config -c - use config from the specified path - - Default config path /usr/local/etc/backup.cfg will be used if invoked without options" + printf "usage: %s [--help|--conf /path/to/config]\n" $self_name + printf " --help -h show this message\n" + printf " --conf -c use config from the specified path\n\n" + printf "Default config path %s will be used if invoked without options\n" $default_cfg } -# function to read the configuration file and spit out some exceptions if stuff is missing +# read the configuration file and spit out some exceptions if stuff is missing function apply_config { - source $cfg || err 15 'Config file does not exist' - [[ -z $source_dirs ]] && { cfg_err 'Backup source'; exit 5 } - [[ -z $remote_host ]] && { cfg_err 'Remote host'; exit 5 } - [[ -z $protocol ]] && { cfg_err 'Backup protocol'; exit 5 } - [[ -z $backup_dir && $protocol != 'ssh' ]] && { cfg_err 'Target directory'; exit 5 } - if [[ -z $local_host ]]; then - local_host=$HOST - fi - # date postfix - if [[ -z $outfile_postfix ]]; then - postfix=$default_postfix - else - postfix=$outfile_postfix - fi - if [[ -z $remote_port ]]; then - case $protocol in - ('ftp'|'ftps') remote_port='21';; - ('sftp'|'ssh') remote_port='22';; - ('local') unset remote_port;; - (*) err 1 "$protocol is not a valid value for the protocol option.";; - esac + # testing remote settings only needed for non-local backups + function test_remote_settings + { + if [[ -z $remote_host ]]; then + cfg_err 'remote_host' + return 5 + fi + if [[ -z $remote_user ]]; then + cfg_err 'remote_user' + return 5 + fi + if [[ -z $remote_pass ]]; then + cfg_err 'remote_pass' + return 5 + fi + if ! (( port )); then + err 'remote_port is not a numeric value.' + return 5 + fi + } + # import contents of the config (including functions if present) + source $cfg || { err "Config file $cfg is unreadable or does not exist"; return 15 } + hostname=${local_host:-$HOST} + postfix=${outfile_postfix:-$default_postfix} + # do the tests + if [[ -z $source_dirs ]]; then + cfg_err 'source_dirs' + return 5 fi + # set defaults and / or fail to run if something is missing + local exit_code + case $protocol in + ('ftp'|'ftps') port=${remote_port:-$default_ftp_port}; test_remote_settings; exit_code=$?; (( exit_code )) && return $exit_code;; + ('sftp'|'ssh') port=${remote_port:-$default_ssh_port}; test_remote_settings; exit_code=$?; (( exit_code )) && return $exit_code;; + ('local') unset remote_port;; + (*) cfg_err 'protocol'; return 5;; + esac + unset exit_code + # set variables for tar command case $compress_format in ('xz') compress_flag='J' ;; ('bz2') compress_flag='j' ;; ('gz') compress_flag='z' ;; - ('') unset compress_flag; unset compress_format ;; - (*) err 1 "$compress_format is not a valid value for the compression format option.";; + ('') unset compress_flag ;; + (*) err "$compress_format is not a valid value for the compression format option."; return 5;; esac if [[ -n $exclude_list ]]; then if [[ -r $exclude_list ]]; then exclude_option='-X' else - err 1 "Exclusion list $exclude_list is either unreadable or does not exist." - fi - fi - if [[ -n $snap_file ]]; then - if printf '' >> $snap_file; then - snapshot_option='-g' - else - err 1 "Snapshot file $snap_file cannot be written." + err "Exclusion list $exclude_list is either unreadable or does not exist. Proceeding without it." fi fi } @@ -75,59 +115,91 @@ function generate_fullpath { local backup_type # increment or full backup - if [[ -s $snap_file ]]; then + if [[ -s $snapshot_file ]]; then backup_type='incr' else backup_type='full' fi if [[ -z $backup_filename ]]; then - outfile="$backup_dir/${local_host}-${src_basename}_${postfix}_${backup_type}.t${compress_format:-'ar'}" + outfile="${backup_dir}${backup_dir:+/}${hostname}-${src_basename}_${postfix}_${backup_type}${gnupg_key:+.gpg.}.t${compress_format:-ar}" else outfile=$backup_filename fi } -function compress # compress to stdout +# compress to stdout +function compress { - tar cf$compress_flag - -C $src_basedir $src_basename $snapshot_option $snapshot_file $exclude_option $exclude_list --ignore-failed-read + # snapshot file is per directory so we cannot test it within apply_config() + if [[ -n $snapshot_file ]]; then + if printf '' >> $snapshot_file; then + snapshot_option='-g' + else + err "Snapshot file $snapshot_file cannot be written. Proceeding with full backup." + fi + fi + # do the magic and spit to stdout + tar -C $src_basedir -c$compress_flag $snapshot_option $snapshot_file $exclude_option $exclude_list --ignore-failed-read $src_basename } -function store # store to local or remote +# store to local or remote +function store { + # take from stdin and do the magic case $protocol in ('local') dd of=$outfile ;; - ('ssh') ssh -p$remote_port $remote_user@$remote_host "dd of=$outfile" ;; - ('sftp'|'ftp'|'ftps') curl -ksS -T - $protocol://$remote_host:$remote_port/$outfile -u $remote_user:$remote_pass ;; - (*) err 1 'Wrong protocol!' ;; + ('ssh') ssh -p$port $remote_user@$remote_host "dd of=$outfile" ;; + ('sftp'|'ftp'|'ftps') curl -ksS -T - $protocol://$remote_host:$port/$outfile -u $remote_user:$remote_pass ;; esac } +# encrypt asymmetrically via gpg +function encrypt +{ + gpg -r $gnupg_key -e - +} + +# self explanatory, using case statement, so one dash multiple opts is not supported function parse_opts { while [[ -n $1 ]]; do case $1 in - ('--help'|'-h') usage; return 0;; - ('--config'|'-c') shift; opt_cfg=$1; shift;; + ('--help'|'-h') usage; exit 0;; + ('--conf'|'-c') shift; opt_cfg=$1; return 0;; + (*) err "unknown parameter $1"; exit 127;; esac done } +# the main logic function main { - parse_opts - if [[ -z $opt_cfg ]]; then - cfg=$default_cfg - else - cfg=$opt_cfg - fi + # parse options + parse_opts $@ + # set default config if $opt_cfg is not defined via an option + cfg=${opt_cfg:-$default_cfg} + # run config tests and fill in defaults apply_config + # fail in case something goes wrong + local exit_code=$? + (( exit_code )) && return $exit_code + unset exit_code + # run backups per directory for i in $source_dirs; do + # prepare the set of variables unset src_basename src_basedir outfile - IFS=':' read source_dir snap_file <<< $i + IFS=':' read source_dir snapshot_file <<< $i src_basename=${source_dir:t} src_basedir=${source_dir:h} + # generate the backups path generate_fullpath - compress | store + err "Creating a backup of $source_dir via $protocol to store it in $outfile." + # pipe magic into magic + if [[ -n $gnupg_key ]]; then + compress | encrypt | store + else + compress | store + fi done return 0 } diff --git a/backup.zsh.cfg b/backup.zsh.cfg new file mode 100644 index 0000000..b773677 --- /dev/null +++ b/backup.zsh.cfg @@ -0,0 +1,56 @@ +#### general options #### +## The protocol we want to use to store our backups. +## Can be ftp, sftp, ftps, ssh or local. +protocol='ssh' + +## The directory to store backups in, locally or remotely. +backup_dir='relative_or_full_path' + +## The list of patterns to exclude from backups, for +## more details look into tar -X option. +#exclude_list='/usr/local/etc/backup/excludes.list' + +## The compression algorithm for backups. +## Can be gz, bz2, xz or empty (for non-compressed). +compress_format='xz' + +## An array with the set of directories within it. +## Optionally snapshot file can be added to store +## incremental diffs (tar -g option used). +## You'll have to deal with snapshots on your own: +## backup.zsh only handles backups (i.e. you can remove +## snapshot via cron on regular basis to ensure that +## full backups are created from time to time.) +source_dirs=( '/home/user/source1:/var/backup/snapshot.list' + '/etc' '/var/spool/mail:/var/backup/spool_snapshot.list' ) + +## Use with caution, the file existance is not checked +## on execution. +## Since this config is sourced, I advise adding some +## logic for that, or you can handle filename collisions +## externally. +#backup_filename='somebackup' + +## GPG key to encrypt backups, uses name of the private +## key in your keyring. It is also entirely possible to +## add the GNUPGHOME environment variable export here in +## order to use the private key from a specific location. +#gnupg_key='keyname' + +#### remote options #### +## Remote host. +remote_host='hostname.tld' + +## Remote user. +remote_user='username' + +## Password, due to how openssh handles security it only +## works for *ftp* protocols; backups via ssh protocol +## work interactively. Later versions will have support +## for ssh keys... If I ever decide to make it happen. +remote_pass='PassWd' + +## Port is optional, the defaults are hardcoded. +#remote_port='443' + +# vim: ft=zsh