#!/bin/bash # ************************************* # # # # sshbackup # # # # ************************************* # # **** function definitions **** bashtrap() { echo echo "CTRL+C detected." echo "commiting suicide." exit 1 } usage() { echo echo "usage: sshbackup [[user@]server:]/source/path /destination/path [versions]" echo echo "OPTIONS:" echo " -h, --help show this message" echo " -v, --version version information" echo echo " -l, --list list of sources and destinations" echo " -c, --config alternate config file [~/.sshbackup]" echo " -s, --sshkey alternate sshkey [~/.ssh/id_rsa]" echo " -b, --bandwidth bandwidth limit in kbit/s" echo echo " -d, --deploy deploy settings to remote host" echo " -n, --no-root run without root privileges" echo } version() { echo echo -e "vesion: \033[1;37m$version\033[0m" echo "author: $author" echo } interactive() { tty -s return $? } amiroot() { if [ $(id -u) -eq 0 ]; then return 0 else return 1 fi } findhome() { #get executing user id local userid=$(id -u) #find out where executing users $HOME is while read line; do line=$(echo $line | sed 's/ //g') line=$(echo $line | sed 's/:/ /g') if [ $(echo $line | awk '{print $3}') -eq $userid ]; then local home=$(echo $line | awk '{print $6}') fi done < /etc/passwd #return home directory echo $home } #this function is needed to pass on the #password to the remote sudo command pipewrap() { echo $1 local lockfile=$2; while true; do if [ ! -e $lockfile ]; then return 0 fi read -t 1 line if [ $? -eq 0 ]; then echo $line fi done } deploy() { local machine=$1 #user@machine.example.com local user=${machine%@*} #user if [ -z $user ]; then user=$USER #set $USER if none is specified fi local machine=${machine#*@} #machine.example.com local username="" local password="" local script="/tmp/sshbackup_deploy`date +%s`" local pubkey=$(cat $2) #create script which is executed on remote host (as root) #TODO: check dependencies on remote system as well echo '#!/bin/bash user="'$user'" pubkeyfile="'$pubkeyfile'" machine="'$machine'" cat /etc/passwd | grep -e ^'$user' >> /dev/null if [ $? -eq 0 ]; then echo; echo "aborting mission. user '$user' already exists on '$machine'." exit 1 else echo; echo "attempting to create user: '$user'" useradd -m -d /home/'$user' -s /bin/bash '$user' mkdir /home/'$user'/.ssh echo "'$pubkey'" > /home/'$user'/.ssh/authorized_keys chown -R '$user':'$user' /home/'$user'/.ssh chmod 600 /home/'$user'/.ssh/authorized_keys sshgroups=$(cat /etc/ssh/sshd_config | grep AllowGroups) sshgroups=${sshgroups#AllowGroups } usermod -a -G ${sshgroups// /,} '$user' echo "'$user' ALL=(root)NOPASSWD: /usr/bin/rsync" >> /etc/sudoers echo "remote settings deployed. hopefully :)" fi' > $script echo "i will now attempt to create the user $user and apply all needed" echo "settings on following remote host: $machine" echo "in order to do so i need a sudo enabled username and" echo "password on the remote host. (hit Ctrl-C to abort)" echo -e "please enter your [sudo] username: \c" read username echo -e "please enter your [sudo] password: \c" read -s password sshpass -p "$password" scp -q "$script" "$username@$machine:'$script'" sshpass -p "$password" ssh -q "$username@$machine" "chmod +x '$script'" sshpass -p "$password" ssh -q "$username@$machine" "sudo -K" local lockfile=`mktemp` eval pipewrap '$password' '$lockfile' | (sshpass -p "$password" ssh -q "$username@$machine" "sudo -S '$script'"; rm "$lockfile") sshpass -p $password ssh -q "$username@$machine" rm $script rm $script #we will assume everything went fine return 0 } preflight() { #source and destination path? if [ -z $sourcepath ]; then echo "aborting mission. no source path given." return 1 elif [ -z $destpath ]; then echo "aborting mission. no destination path given." return 1 fi #amiroot? if ( ! amiroot ) && [ $noroot -eq 0 ]; then echo "aborting mission. you need be root or use the --no-root option." return 1 fi #check for dependencies #TODO: and install them if necessary local deps="rsync cat ssh scp sshpass rm mv grep awk date tty id" local missingdeps="" local depscount="0" for dep in $deps; do if ! ( command -v $dep >> /dev/null ); then if [ $depscount -eq "0" ]; then missingdeps="$dep" else missingdeps="$missingdeps $dep" fi let depscount++ fi done if [ $depscount -ne "0" ]; then echo "aborting mission. missing dependencies. [$missingdeps]" return 1 fi #if there is a remote source or destination check for ssh key and config if [[ $sourcepath =~ .*@.* ]]; then #deactivate StrictHostKeyChecking for ssh client if [ -r $HOME/.ssh/config ]; then cat $HOME/.ssh/config | grep "StrictHostKeyChecking" >> /dev/null if [ $? -ne 0 ]; then echo "StrictHostKeyChecking no" >> $HOME/.ssh/config fi else echo "StrictHostKeyChecking no" > $HOME/.ssh/config fi if [ -r $privkeyfile ]; then #ssh key found if ( interactive ) && [ $deploy -eq 1 ]; then deploy ${sourcepath%:*} $pubkeyfile fi else if ( interactive ); then echo -e "no ssh key found. do you want to create a new key pair? [y/n] \c" read choice if [ -z $choice ]; then echo "aborting mission. no ssh key found." return 1 elif [ $choice == "y" ] || [ $choice == "Y" ]; then #creating ssh key pair with default values ssh-keygen -q -N "" -f $privkeyfile if [ $? -ne 0 ]; then echo "aborting mission. error occured while creating ssh key pair" return 1 fi echo -e "do you want to create a user and deploy this key now as well \c" read choice if [ -z $choice ]; then echo "aborting mission. remote host must accept ssh key." return 1 elif [ $choice == "y" ] || [ $choice == "Y" ]; then #deploy key to remote system deploy ${sourcepath%:*} $pubkeyfile fi else echo "aborting mission. no ssh key found." return 1 fi else echo "aborting mission. no ssh key found." return 1 fi fi fi return 0 } sshbackup() { #creating local rsync options var local cmdopt="$rsyncoptions" #move existing versions local num=$versions while [ -d "$destpath/0" ]; do if [ -d "$destpath/$num" ]; then mv $destpath/$num $destpath/$((num+1)) fi let num-- done #create destpath if not existing mkdir -p $destpath/0 #add link-destination option if existing if [ -d $destpath/1 ]; then cmdopt="$cmdopt --link-dest=$destpath/1" fi #bandwidth limit if [ $limit -gt 0 ]; then limit=$((limit/8)) cmdopt="$cmdopt --bwlimit=$limit" fi #run rsync $localcmd $cmdopt -e "ssh -q -i $privkeyfile" --rsync-path="$remotecmd" $sourcepath $destpath/0 if [ $? -ne "0" ]; then echo "an error occured while running backup for $sourcepath" return 1 fi #removing obsolet version(s) local i=1 while [ -d $destpath/$((versions+i)) ]; do rm -rf $destpath/$((versions+i)) let i++ done return 0 } # **** config section **** version="0.4.0" author="david@socialnerds.org" HOME=$(findhome) configfile="$HOME/.sshbackup" privkeyfile="$HOME/.ssh/id_rsa" pubkeyfile="$HOME/.ssh/id_rsa.pub" #rsync options. rsyncoptions="-qpogEthrzl --numeric-ids --no-motd" #dotglob option removes bug while rsyncing folder with no visible files in it. #TODO: only works if bash is the remote default shell remotecmd="shopt -s dotglob; /usr/bin/sudo /usr/bin/rsync" localcmd="rsync" versions=999 config=0 sshkey=0 bandwidth=0 limit=0 list=0 noroot=0 deploy=0 options=$* # **** start of script **** #initialize bashtrap trap bashtrap INT # **** option handler **** #end script if no options are given if [ -z $1 ]; then usage exit 0 fi for option in $options; do case "$option" in -h|--help) usage exit 0 ;; -v|--version) version exit 0 ;; -c|--config) config=1 ;; -l|--list) list=1 ;; -s|--sshkey) sshkey=1 ;; -b|--bandwidth) bandwidth=1 ;; -n|--no-root) noroot=1 ;; -d|--deploy) deploy=1 ;; *) if [ $config -eq 1 ]; then if [ -r "$option" ]; then configfile=$option config=0 else echo "aborting mission. cannot read configfile. [$option]" exit 1 fi elif [ $sshkey -eq 1 ]; then if [ -r $option ]; then privkeyfile=$option pubkeyfile=$option".pub" sshkey=0 else echo "aborting mission. cannot read privkeyfile. [$option]" exit 1 fi elif [ $bandwidth -eq 1 ]; then if [ -z "${option//[0-9]/}" ]; then limit=$option else echo "aborting mission. unknown bandwidth limit given. [$option]" exit 1 fi elif [ $list -eq 1 ]; then if [ -r $option ]; then listfile=$option list=0 else echo "aborting mission. cannot read listfile. [$option]" exit 1 fi else if [[ $option =~ ^-.* ]]; then echo "aborting mission. unknown option given. [$option]" usage exit 1 #TODO: what if source or destination is a number? elif [ -z "${option//[0-9]/}" ]; then versions="$option" else if [ -z "$sourcepath" ]; then if [[ $option =~ .*@.* ]]; then sourcepath="${option%/}/*" elif [[ $option =~ ^/.* ]]; then sourcepath="${option%/}/*" elif [[ $option =~ .*:.* ]]; then sourcepath="$USER@${option%/}/*" else sourcepath="$(pwd)/${option%/}/*" fi else if [ -z "$destpath" ]; then if [[ $option =~ ^/.* ]]; then destpath="${option%/}" elif [[ $option =~ .*@.* ]] || [[ $option =~ .*:.* ]]; then echo "aborting mission. invalid destination path. [$option]" exit 1 else destpath="$(pwd)/${option%/}" fi fi fi fi fi ;; esac done #read config if there is one if [ -r "$configfile" ]; then source "$configfile" fi #read list if listfile is given if [ -r "$listfile" ]; then while read line; do #find first letter fletter=${line:0:1} if [ -z $fletter ]; then #skip line it's empty : elif [ $fletter = "#" ]; then #skip line it's a comment : else sourcepath="" destpath="" for option in $line; do if [ -z "${option//[0-9]/}" ]; then versions="$option" else if [ -z "$sourcepath" ]; then if [[ $option =~ .*@.* ]]; then sourcepath="${option%/}/*" elif [[ $option =~ ^/.* ]]; then sourcepath="${option%/}/*" elif [[ $option =~ .*:.* ]]; then sourcepath="$USER@${option%/}/*" else sourcepath="$(pwd)/${option%/}/*" fi else if [ -z "$destpath" ]; then if [[ $option =~ ^/.* ]]; then destpath="${option%/}" elif [[ $option =~ .*@.* ]] || [[ $option =~ .*:.* ]]; then echo "aborting mission. invalid destination path. [$option]" exit 1 else destpath="$(pwd)/${option%/}" fi fi fi fi done if ( preflight ); then sshbackup fi fi done < "$listfile" else if ( preflight ); then sshbackup fi fi exit 0 # **** end of script ****