Compare commits
5 Commits
Author | SHA1 | Date | |
---|---|---|---|
f3d5ebd276
|
|||
fb726a80ab
|
|||
4701ee0b05
|
|||
dd779d242b
|
|||
22a3e8d60f
|
@ -2,12 +2,12 @@
|
||||
.SH NAME
|
||||
simple_backup \- Backup files and folders using rsync
|
||||
.SH SYNOPSIS
|
||||
.BR simple_backup
|
||||
.B simple_backup
|
||||
\-h, \-\-help
|
||||
.PD 0
|
||||
.P
|
||||
.PD
|
||||
.BR simple_backup
|
||||
.B simple_backup
|
||||
[\-c, \-\-config FILE]
|
||||
[\-i, \-\-input INPUT [INPUT...]]
|
||||
[\-o, \-\-output DIR]
|
||||
@ -16,8 +16,8 @@ simple_backup \- Backup files and folders using rsync
|
||||
.PD
|
||||
.RS 14 [\-e, \-\-exclude FILE|DIR|PATTERN [FILE|...]]
|
||||
[\-k, \-\-keep N]
|
||||
[\-\-host HOSTNAME]
|
||||
[\-u, \-\-username USERNAME]
|
||||
[\-\-ssh\-host HOSTNAME]
|
||||
[\-\-ssh\-user USERNAME]
|
||||
[\-\-keyfile FILE]
|
||||
.PD 0
|
||||
.P
|
||||
@ -27,7 +27,7 @@ simple_backup \- Backup files and folders using rsync
|
||||
[\-\-remove\-before\-backup]
|
||||
.RE
|
||||
.SH DESCRIPTION
|
||||
.BR simple_backup
|
||||
.B simple_backup
|
||||
is a python script for performing backup of files and folders.
|
||||
.P
|
||||
It uses rsync to copy the files to the specified location. Parameters for the backup such as
|
||||
@ -62,10 +62,10 @@ will not be copied. Multiple elements can be specified, in the same way as for t
|
||||
Specify how many old backups (so excluding the current one) will be kept. The default behavior
|
||||
is to keep them all (same as N=\-1)
|
||||
.TP
|
||||
.B \-\-host HOSTNAME
|
||||
.B \-\-ssh\-host HOSTNAME
|
||||
Hostname of the server where to copy the files in case of remote backup through SSH
|
||||
.TP
|
||||
.B \-u, \-\-username USERNAME
|
||||
.B \-\-ssh\-user USERNAME
|
||||
Username for connecting to the server in case of remote backup
|
||||
.TP
|
||||
.B \-\-keyfile FILE
|
||||
@ -104,7 +104,7 @@ Options \-r and \-v are used in any case. Not that options must be specified wit
|
||||
.EE
|
||||
.TP
|
||||
Check
|
||||
.B rsync (1)
|
||||
.BR rsync (1)
|
||||
for details about the options.
|
||||
.RE
|
||||
.TP
|
||||
@ -123,7 +123,7 @@ Copy it to the default location ($HOME/.config/simple_backup) and edit it as nee
|
||||
.SH REMOTE BACKUP
|
||||
It is possible to choose a directory on a remote server as destination for the backup. The files
|
||||
are copied by rsync through SSH. Server hostname and username must be specified, either in the
|
||||
configuration file, or on the command line (\(aq\-\-host\(aq and \(aq\-\-username\(aq options).
|
||||
configuration file, or on the command line (\(aq\-\-ssh\-host\(aq and \(aq\-\-ssh\-user\(aq options).
|
||||
.SS AUTHENTICATION
|
||||
For authentication, it is possible to use SSH key or password.
|
||||
.P
|
||||
@ -145,7 +145,7 @@ in order to connect to the user\(aq s SSH agent it is necessary to preserve the
|
||||
It is also possible to make this permanent by editing the
|
||||
.B sudoers
|
||||
file (see
|
||||
.B sudoers (5)
|
||||
.BR sudoers (5)
|
||||
)
|
||||
.P
|
||||
If SSH key authentication is not available, password authentication will be used instead.
|
||||
@ -154,7 +154,7 @@ Note that in this case
|
||||
(if available) will be used to send the password to rsync, to avoid prompting the user for
|
||||
the password multiple
|
||||
times. This can pose some security risks, see
|
||||
.B sshpass (1)
|
||||
.BR sshpass (1)
|
||||
for details. For this reason, use SSH key authentication if possible.
|
||||
.SH EXIT STATUS
|
||||
.TP
|
||||
@ -172,6 +172,12 @@ Permission denied to access the output directory
|
||||
.TP
|
||||
.B 4
|
||||
rsync error (rsync returned a non-zero value)
|
||||
.TP
|
||||
.B 5
|
||||
SSH connection failed
|
||||
.TP
|
||||
.B 6
|
||||
Bad configuration file
|
||||
.SH SEE ALSO
|
||||
.BR rsync (1)
|
||||
.SH AUTHORS
|
||||
|
@ -15,8 +15,8 @@ keep=-1
|
||||
|
||||
# Uncomment the following section to enable backup to remote server through ssh
|
||||
# [server]
|
||||
# host=
|
||||
# username=
|
||||
# ssh_host=
|
||||
# ssh_user=
|
||||
# ssh_keyfile=
|
||||
# remote_sudo=
|
||||
# numeric_ids=
|
||||
|
@ -10,6 +10,7 @@ Classes:
|
||||
MyFormatter
|
||||
Backup
|
||||
"""
|
||||
|
||||
# Import libraries
|
||||
import sys
|
||||
import os
|
||||
@ -29,11 +30,15 @@ from getpass import getpass
|
||||
from glob import glob
|
||||
|
||||
from dotenv import load_dotenv
|
||||
import paramiko
|
||||
from paramiko import RSAKey, Ed25519Key, ECDSAKey, DSSKey
|
||||
|
||||
warnings.filterwarnings('error')
|
||||
|
||||
try:
|
||||
import paramiko
|
||||
from paramiko import RSAKey, Ed25519Key, ECDSAKey, DSSKey
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
try:
|
||||
from systemd import journal
|
||||
except ImportError:
|
||||
@ -114,9 +119,9 @@ class Backup:
|
||||
String representing main backup options for rsync
|
||||
keep: int
|
||||
Number of old backup to preserve
|
||||
host: str
|
||||
ssh_host: str
|
||||
Hostname of server (for remote backup)
|
||||
username: str
|
||||
ssh_user: str
|
||||
Username for server login (for remote backup)
|
||||
ssh_keyfile: str
|
||||
Location of ssh key
|
||||
@ -138,15 +143,15 @@ class Backup:
|
||||
Perform the backup
|
||||
"""
|
||||
|
||||
def __init__(self, inputs, output, exclude, keep, options, host=None, username=None,
|
||||
def __init__(self, inputs, output, exclude, keep, options, ssh_host=None, ssh_user=None,
|
||||
ssh_keyfile=None, remote_sudo=False, remove_before=False):
|
||||
self.inputs = inputs
|
||||
self.output = output
|
||||
self.exclude = exclude
|
||||
self.options = options
|
||||
self.keep = keep
|
||||
self.host = host
|
||||
self.username = username
|
||||
self.ssh_host = ssh_host
|
||||
self.ssh_user = ssh_user
|
||||
self.ssh_keyfile = ssh_keyfile
|
||||
self.remote_sudo = remote_sudo
|
||||
self._remove_before = remove_before
|
||||
@ -174,14 +179,14 @@ class Backup:
|
||||
|
||||
return 2
|
||||
|
||||
if self.host is not None and self.username is not None:
|
||||
if self.ssh_host is not None and self.ssh_user is not None:
|
||||
self._remote = True
|
||||
|
||||
if self._remote:
|
||||
self._ssh = self._ssh_connect()
|
||||
|
||||
if self._ssh is None:
|
||||
sys.exit(1)
|
||||
sys.exit(5)
|
||||
|
||||
_, stdout, _ = self._ssh.exec_command(f'if [ -d "{self.output}" ]; then echo "ok"; fi')
|
||||
|
||||
@ -211,7 +216,7 @@ class Backup:
|
||||
self._output_dir = f'{self.output}/simple_backup/{now}'
|
||||
|
||||
if self._remote:
|
||||
self._server = f'{self.username}@{self.host}:'
|
||||
self._server = f'{self.ssh_user}@{self.ssh_host}:'
|
||||
|
||||
def remove_old_backups(self):
|
||||
"""Remove old backups if there are more than indicated by 'keep'"""
|
||||
@ -279,7 +284,7 @@ class Backup:
|
||||
if self._remote:
|
||||
if self._ssh is None:
|
||||
logger.critical('SSH connection to server failed')
|
||||
sys.exit(1)
|
||||
sys.exit(5)
|
||||
|
||||
_, stdout, _ = self._ssh.exec_command(f'find {self.output}/simple_backup/ -mindepth 1 -maxdepth 1 -type d | sort')
|
||||
output = stdout.read().decode('utf-8').strip().split('\n')
|
||||
@ -311,7 +316,11 @@ class Backup:
|
||||
logger.info('No previous backups available')
|
||||
|
||||
def _ssh_connect(self):
|
||||
try:
|
||||
ssh = paramiko.SSHClient()
|
||||
except NameError:
|
||||
logger.error('Install paramiko for ssh support')
|
||||
return None
|
||||
|
||||
try:
|
||||
ssh.load_host_keys(filename=f'{homedir}/.ssh/known_hosts')
|
||||
@ -321,11 +330,11 @@ class Backup:
|
||||
ssh.set_missing_host_key_policy(paramiko.WarningPolicy())
|
||||
|
||||
try:
|
||||
ssh.connect(self.host, username=self.username)
|
||||
ssh.connect(self.ssh_host, username=self.ssh_user)
|
||||
|
||||
return ssh
|
||||
except UserWarning:
|
||||
k = input(f'Unknown key for host {self.host}. Continue anyway? (Y/N) ')
|
||||
k = input(f'Unknown key for host {self.ssh_host}. Continue anyway? (Y/N) ')
|
||||
|
||||
if k[0].upper() == 'Y':
|
||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
@ -340,7 +349,7 @@ class Backup:
|
||||
pass
|
||||
|
||||
try:
|
||||
ssh.connect(self.host, username=self.username)
|
||||
ssh.connect(self.ssh_host, username=self.ssh_user)
|
||||
|
||||
return ssh
|
||||
except paramiko.SSHException:
|
||||
@ -348,8 +357,8 @@ class Backup:
|
||||
|
||||
if self.ssh_keyfile is None:
|
||||
try:
|
||||
password = getpass(f'{self.username}@{self.host}\'s password: ')
|
||||
ssh.connect(self.host, username=self.username, password=password)
|
||||
password = getpass(f'{self.ssh_user}@{self.ssh_host}\'s password: ')
|
||||
ssh.connect(self.ssh_host, username=self.ssh_user, password=password)
|
||||
|
||||
self._password_auth = True
|
||||
os.environ['SSHPASS'] = password
|
||||
@ -401,7 +410,7 @@ class Backup:
|
||||
pass
|
||||
|
||||
try:
|
||||
ssh.connect(self.host, username=self.username, pkey=pkey)
|
||||
ssh.connect(self.ssh_host, username=self.ssh_user, pkey=pkey)
|
||||
except paramiko.SSHException:
|
||||
logger.critical('SSH connection to server failed')
|
||||
|
||||
@ -469,7 +478,7 @@ class Backup:
|
||||
if euid == 0 and self.ssh_keyfile is not None:
|
||||
rsync = f'{rsync} -e \'ssh -i {self.ssh_keyfile} -o StrictHostKeyChecking=no\''
|
||||
elif self._password_auth and which('sshpass'):
|
||||
rsync = f'{rsync} -e \'sshpass -e ssh -l {self.username} -o StrictHostKeyChecking=no\''
|
||||
rsync = f'{rsync} -e \'sshpass -e ssh -l {self.ssh_user} -o StrictHostKeyChecking=no\''
|
||||
else:
|
||||
rsync = f'{rsync} -e \'ssh -o StrictHostKeyChecking=no\''
|
||||
|
||||
@ -558,8 +567,8 @@ def _parse_arguments():
|
||||
parser.add_argument('-o', '--output', help='Output directory for the backup')
|
||||
parser.add_argument('-e', '--exclude', nargs='+', help='Files/directories/patterns to exclude from the backup')
|
||||
parser.add_argument('-k', '--keep', type=int, help='Number of old backups to keep')
|
||||
parser.add_argument('--host', help='Server hostname (for remote backup)')
|
||||
parser.add_argument('-u', '--username', help='Username to connect to server (for remote backup)')
|
||||
parser.add_argument('--ssh-host', help='Server hostname (for remote backup)')
|
||||
parser.add_argument('--ssh-user', help='Username to connect to server (for remote backup)')
|
||||
parser.add_argument('--keyfile', help='SSH key location')
|
||||
parser.add_argument('-s', '--checksum', action='store_true',
|
||||
help='Use checksum rsync option to compare files')
|
||||
@ -572,7 +581,7 @@ def _parse_arguments():
|
||||
help='Specify options for rsync')
|
||||
parser.add_argument('--remote-sudo', action='store_true', help='Run rsync on remote server with sudo if allowed')
|
||||
parser.add_argument('--numeric-ids', action='store_true',
|
||||
help='Use rsync \'--numeric-ids\' option (don\'t map uid/gid values by name')
|
||||
help='Use rsync \'--numeric-ids\' option (don\'t map uid/gid values by name)')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
@ -597,7 +606,15 @@ def _expand_inputs(inputs):
|
||||
|
||||
|
||||
def _read_config(config_file):
|
||||
config_args = {}
|
||||
config_args = {'inputs': None,
|
||||
'output': None,
|
||||
'exclude': None,
|
||||
'keep': -1,
|
||||
'ssh_host': None,
|
||||
'ssh_user': None,
|
||||
'ssh_keyfile': None,
|
||||
'remote_sudo': False,
|
||||
'numeric_ids': False}
|
||||
|
||||
if not os.path.isfile(config_file):
|
||||
logger.warning('Config file %s does not exist', config_file)
|
||||
@ -643,14 +660,14 @@ def _read_config(config_file):
|
||||
config_args['keep'] = keep
|
||||
|
||||
try:
|
||||
host = config.get('server', 'host')
|
||||
username = config.get('server', 'username')
|
||||
ssh_host = config.get('server', 'ssh_host')
|
||||
ssh_user = config.get('server', 'ssh_user')
|
||||
except (configparser.NoSectionError, configparser.NoOptionError):
|
||||
host = None
|
||||
username = None
|
||||
ssh_host = None
|
||||
ssh_user = None
|
||||
|
||||
config_args['host'] = host
|
||||
config_args['username'] = username
|
||||
config_args['ssh_host'] = ssh_host
|
||||
config_args['ssh_user'] = ssh_user
|
||||
|
||||
try:
|
||||
ssh_keyfile = config.get('server', 'ssh_keyfile')
|
||||
@ -709,14 +726,14 @@ def simple_backup():
|
||||
config_args = _read_config(args.config)
|
||||
except (configparser.NoSectionError, configparser.NoOptionError):
|
||||
logger.critical('Bad configuration file')
|
||||
sys.exit(1)
|
||||
sys.exit(6)
|
||||
|
||||
inputs = args.inputs if args.inputs is not None else config_args['inputs']
|
||||
output = args.output if args.output is not None else config_args['output']
|
||||
exclude = args.exclude if args.exclude is not None else config_args['exclude']
|
||||
keep = args.keep if args.keep is not None else config_args['keep']
|
||||
host = args.host if args.host is not None else config_args['host']
|
||||
username = args.username if args.username is not None else config_args['username']
|
||||
ssh_host = args.ssh_host if args.ssh_host is not None else config_args['ssh_host']
|
||||
ssh_user = args.ssh_user if args.ssh_user is not None else config_args['ssh_user']
|
||||
ssh_keyfile = args.keyfile if args.keyfile is not None else config_args['ssh_keyfile']
|
||||
remote_sudo = args.remote_sudo if args.remote_sudo is not None else config_args['remote_sudo']
|
||||
|
||||
@ -739,7 +756,7 @@ def simple_backup():
|
||||
|
||||
rsync_options = ' '.join(rsync_options)
|
||||
|
||||
backup = Backup(inputs, output, exclude, keep, rsync_options, host, username, ssh_keyfile,
|
||||
backup = Backup(inputs, output, exclude, keep, rsync_options, ssh_host, ssh_user, ssh_keyfile,
|
||||
remote_sudo, remove_before=args.remove_before_backup)
|
||||
|
||||
return_code = backup.check_params()
|
||||
|
Reference in New Issue
Block a user