485 lines
14 KiB
Bash
Executable File
485 lines
14 KiB
Bash
Executable File
#!/bin/bash
|
|
|
|
|
|
# ************************************* #
|
|
# #
|
|
# sshbackup #
|
|
# #
|
|
# ************************************* #
|
|
|
|
|
|
# **** function definitions ****
|
|
bashtrap()
|
|
{
|
|
echo
|
|
echo "CTRL+C detected."
|
|
echo "commiting suicide."
|
|
exit 1
|
|
}
|
|
|
|
usage()
|
|
{
|
|
echo
|
|
echo "usage: sshbackup <options> [[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 <file> list of sources and destinations"
|
|
echo " -c, --config <file> alternate config file [~/.sshbackup]"
|
|
echo " -s, --sshkey <file> alternate sshkey [~/.ssh/id_rsa]"
|
|
echo " -b, --bandwidth <kbps> 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."
|
|
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
|
|
}
|
|
|
|
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
|
|
|
|
#TODO: check for dependencies and install them if necessary
|
|
#check for dependencies
|
|
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
|
|
#TODO: what if StrictHostKeyChecking is set but not to "no"
|
|
if [ -r $HOME/.ssh/config ]; then
|
|
cat $HOME/.ssh/config | grep "StrictHostKeyChecking no" >> /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
|
|
#deploy key to remote system
|
|
deploy ${sourcepath%:*} $pubkeyfile
|
|
return 0
|
|
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 ****
|