mirror of https://github.com/borgbackup/borg.git
commit
52fd2ad3da
|
@ -19,7 +19,8 @@ from .helpers import Error, location_validator, format_time, format_file_size, \
|
||||||
format_file_mode, ExcludePattern, IncludePattern, exclude_path, adjust_patterns, to_localtime, timestamp, \
|
format_file_mode, ExcludePattern, IncludePattern, exclude_path, adjust_patterns, to_localtime, timestamp, \
|
||||||
get_cache_dir, get_keys_dir, format_timedelta, prune_within, prune_split, \
|
get_cache_dir, get_keys_dir, format_timedelta, prune_within, prune_split, \
|
||||||
Manifest, remove_surrogates, update_excludes, format_archive, check_extension_modules, Statistics, \
|
Manifest, remove_surrogates, update_excludes, format_archive, check_extension_modules, Statistics, \
|
||||||
is_cachedir, bigint_to_int, ChunkerParams, CompressionSpec, have_cython
|
is_cachedir, bigint_to_int, ChunkerParams, CompressionSpec, have_cython, \
|
||||||
|
EXIT_SUCCESS, EXIT_WARNING, EXIT_ERROR
|
||||||
from .logger import create_logger, setup_logging
|
from .logger import create_logger, setup_logging
|
||||||
logger = create_logger()
|
logger = create_logger()
|
||||||
if have_cython():
|
if have_cython():
|
||||||
|
@ -37,7 +38,7 @@ has_lchflags = hasattr(os, 'lchflags')
|
||||||
class Archiver:
|
class Archiver:
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.exit_code = 0
|
self.exit_code = EXIT_SUCCESS
|
||||||
|
|
||||||
def open_repository(self, location, create=False, exclusive=False):
|
def open_repository(self, location, create=False, exclusive=False):
|
||||||
if location.proto == 'ssh':
|
if location.proto == 'ssh':
|
||||||
|
@ -49,7 +50,7 @@ class Archiver:
|
||||||
|
|
||||||
def print_error(self, msg, *args):
|
def print_error(self, msg, *args):
|
||||||
msg = args and msg % args or msg
|
msg = args and msg % args or msg
|
||||||
self.exit_code = 1
|
self.exit_code = EXIT_WARNING # we do not terminate here, so it is a warning
|
||||||
logger.error('borg: ' + msg)
|
logger.error('borg: ' + msg)
|
||||||
|
|
||||||
def print_verbose(self, msg, *args, **kw):
|
def print_verbose(self, msg, *args, **kw):
|
||||||
|
@ -92,18 +93,18 @@ Type "Yes I am sure" if you understand this and want to continue.\n""")
|
||||||
if repository.check(repair=args.repair):
|
if repository.check(repair=args.repair):
|
||||||
logger.info('Repository check complete, no problems found.')
|
logger.info('Repository check complete, no problems found.')
|
||||||
else:
|
else:
|
||||||
return 1
|
return EXIT_WARNING
|
||||||
if not args.repo_only and not ArchiveChecker().check(
|
if not args.repo_only and not ArchiveChecker().check(
|
||||||
repository, repair=args.repair, archive=args.repository.archive, last=args.last):
|
repository, repair=args.repair, archive=args.repository.archive, last=args.last):
|
||||||
return 1
|
return EXIT_WARNING
|
||||||
return 0
|
return EXIT_SUCCESS
|
||||||
|
|
||||||
def do_change_passphrase(self, args):
|
def do_change_passphrase(self, args):
|
||||||
"""Change repository key file passphrase"""
|
"""Change repository key file passphrase"""
|
||||||
repository = self.open_repository(args.repository)
|
repository = self.open_repository(args.repository)
|
||||||
manifest, key = Manifest.load(repository)
|
manifest, key = Manifest.load(repository)
|
||||||
key.change_passphrase()
|
key.change_passphrase()
|
||||||
return 0
|
return EXIT_SUCCESS
|
||||||
|
|
||||||
def do_create(self, args):
|
def do_create(self, args):
|
||||||
"""Create new archive"""
|
"""Create new archive"""
|
||||||
|
@ -326,7 +327,7 @@ Type "Yes I am sure" if you understand this and want to continue.\n""")
|
||||||
print("""Type "YES" if you understand this and want to continue.\n""", file=sys.stderr)
|
print("""Type "YES" if you understand this and want to continue.\n""", file=sys.stderr)
|
||||||
# XXX: prompt may end up on stdout, but we'll assume that input() does the right thing
|
# XXX: prompt may end up on stdout, but we'll assume that input() does the right thing
|
||||||
if input('Do you want to continue? ') != 'YES':
|
if input('Do you want to continue? ') != 'YES':
|
||||||
self.exit_code = 1
|
self.exit_code = EXIT_ERROR
|
||||||
return self.exit_code
|
return self.exit_code
|
||||||
repository.destroy()
|
repository.destroy()
|
||||||
logger.info("Repository deleted.")
|
logger.info("Repository deleted.")
|
||||||
|
@ -358,7 +359,7 @@ Type "Yes I am sure" if you understand this and want to continue.\n""")
|
||||||
operations.mount(args.mountpoint, args.options, args.foreground)
|
operations.mount(args.mountpoint, args.options, args.foreground)
|
||||||
except RuntimeError:
|
except RuntimeError:
|
||||||
# Relevant error message already printed to stderr by fuse
|
# Relevant error message already printed to stderr by fuse
|
||||||
self.exit_code = 1
|
self.exit_code = EXIT_ERROR
|
||||||
return self.exit_code
|
return self.exit_code
|
||||||
|
|
||||||
def do_list(self, args):
|
def do_list(self, args):
|
||||||
|
@ -431,7 +432,7 @@ Type "Yes I am sure" if you understand this and want to continue.\n""")
|
||||||
if args.hourly + args.daily + args.weekly + args.monthly + args.yearly == 0 and args.within is None:
|
if args.hourly + args.daily + args.weekly + args.monthly + args.yearly == 0 and args.within is None:
|
||||||
self.print_error('At least one of the "within", "keep-hourly", "keep-daily", "keep-weekly", '
|
self.print_error('At least one of the "within", "keep-hourly", "keep-daily", "keep-weekly", '
|
||||||
'"keep-monthly" or "keep-yearly" settings must be specified')
|
'"keep-monthly" or "keep-yearly" settings must be specified')
|
||||||
return 1
|
return EXIT_ERROR
|
||||||
if args.prefix:
|
if args.prefix:
|
||||||
archives = [archive for archive in archives if archive.name.startswith(args.prefix)]
|
archives = [archive for archive in archives if archive.name.startswith(args.prefix)]
|
||||||
keep = []
|
keep = []
|
||||||
|
@ -1044,22 +1045,33 @@ def main(): # pragma: no cover
|
||||||
setup_signal_handlers()
|
setup_signal_handlers()
|
||||||
archiver = Archiver()
|
archiver = Archiver()
|
||||||
try:
|
try:
|
||||||
|
msg = None
|
||||||
exit_code = archiver.run(sys.argv[1:])
|
exit_code = archiver.run(sys.argv[1:])
|
||||||
except Error as e:
|
except Error as e:
|
||||||
archiver.print_error(e.get_message() + "\n%s" % traceback.format_exc())
|
msg = e.get_message() + "\n%s" % traceback.format_exc()
|
||||||
exit_code = e.exit_code
|
exit_code = e.exit_code
|
||||||
except RemoteRepository.RPCError as e:
|
except RemoteRepository.RPCError as e:
|
||||||
archiver.print_error('Error: Remote Exception.\n%s' % str(e))
|
msg = 'Remote Exception.\n%s' % str(e)
|
||||||
exit_code = 1
|
exit_code = EXIT_ERROR
|
||||||
except Exception:
|
except Exception:
|
||||||
archiver.print_error('Error: Local Exception.\n%s' % traceback.format_exc())
|
msg = 'Local Exception.\n%s' % traceback.format_exc()
|
||||||
exit_code = 1
|
exit_code = EXIT_ERROR
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
archiver.print_error('Error: Keyboard interrupt.\n%s' % traceback.format_exc())
|
msg = 'Keyboard interrupt.\n%s' % traceback.format_exc()
|
||||||
exit_code = 1
|
exit_code = EXIT_ERROR
|
||||||
if exit_code:
|
if msg:
|
||||||
archiver.print_error('Exiting with failure status due to previous errors')
|
logger.error(msg)
|
||||||
|
exit_msg = 'terminating with %s status, rc %d'
|
||||||
|
if exit_code == EXIT_SUCCESS:
|
||||||
|
logger.info(exit_msg % ('success', exit_code))
|
||||||
|
elif exit_code == EXIT_WARNING:
|
||||||
|
logger.warning(exit_msg % ('warning', exit_code))
|
||||||
|
elif exit_code == EXIT_ERROR:
|
||||||
|
logger.error(exit_msg % ('error', exit_code))
|
||||||
|
else:
|
||||||
|
logger.error(exit_msg % ('abnormal', exit_code))
|
||||||
sys.exit(exit_code)
|
sys.exit(exit_code)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
main()
|
main()
|
||||||
|
|
|
@ -44,13 +44,27 @@ if have_cython():
|
||||||
import msgpack
|
import msgpack
|
||||||
|
|
||||||
|
|
||||||
|
# return codes returned by borg command
|
||||||
|
# when borg is killed by signal N, rc = 128 + N
|
||||||
|
EXIT_SUCCESS = 0 # everything done, no problems
|
||||||
|
EXIT_WARNING = 1 # reached normal end of operation, but there were issues
|
||||||
|
EXIT_ERROR = 2 # terminated abruptly, did not reach end of operation
|
||||||
|
|
||||||
|
|
||||||
class Error(Exception):
|
class Error(Exception):
|
||||||
"""Error base class"""
|
"""Error base class"""
|
||||||
|
|
||||||
exit_code = 1
|
# if we raise such an Error and it is only catched by the uppermost
|
||||||
|
# exception handler (that exits short after with the given exit_code),
|
||||||
|
# it is always a (fatal and abrupt) EXIT_ERROR, never just a warning.
|
||||||
|
exit_code = EXIT_ERROR
|
||||||
|
|
||||||
def get_message(self):
|
def get_message(self):
|
||||||
return 'Error: ' + type(self).__doc__.format(*self.args)
|
return type(self).__doc__.format(*self.args)
|
||||||
|
|
||||||
|
|
||||||
|
class IntegrityError(Error):
|
||||||
|
"""Data integrity error"""
|
||||||
|
|
||||||
|
|
||||||
class ExtensionModuleError(Error):
|
class ExtensionModuleError(Error):
|
||||||
|
@ -487,10 +501,6 @@ def format_archive(archive):
|
||||||
return '%-36s %s' % (archive.name, to_localtime(archive.ts).strftime('%c'))
|
return '%-36s %s' % (archive.name, to_localtime(archive.ts).strftime('%c'))
|
||||||
|
|
||||||
|
|
||||||
class IntegrityError(Error):
|
|
||||||
"""Data integrity error"""
|
|
||||||
|
|
||||||
|
|
||||||
def memoize(function):
|
def memoize(function):
|
||||||
cache = {}
|
cache = {}
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,7 @@ from ..archive import Archive, ChunkBuffer, CHUNK_MAX_EXP
|
||||||
from ..archiver import Archiver
|
from ..archiver import Archiver
|
||||||
from ..cache import Cache
|
from ..cache import Cache
|
||||||
from ..crypto import bytes_to_long, num_aes_blocks
|
from ..crypto import bytes_to_long, num_aes_blocks
|
||||||
from ..helpers import Manifest
|
from ..helpers import Manifest, EXIT_SUCCESS, EXIT_WARNING, EXIT_ERROR
|
||||||
from ..remote import RemoteRepository, PathNotAllowed
|
from ..remote import RemoteRepository, PathNotAllowed
|
||||||
from ..repository import Repository
|
from ..repository import Repository
|
||||||
from . import BaseTestCase
|
from . import BaseTestCase
|
||||||
|
@ -122,6 +122,24 @@ def cmd(request):
|
||||||
return exec_fn
|
return exec_fn
|
||||||
|
|
||||||
|
|
||||||
|
def test_return_codes(cmd, tmpdir):
|
||||||
|
repo = tmpdir.mkdir('repo')
|
||||||
|
input = tmpdir.mkdir('input')
|
||||||
|
output = tmpdir.mkdir('output')
|
||||||
|
input.join('test_file').write('content')
|
||||||
|
rc, out = cmd('init', '%s' % str(repo))
|
||||||
|
assert rc == EXIT_SUCCESS
|
||||||
|
rc, out = cmd('create', '%s::archive' % repo, str(input))
|
||||||
|
assert rc == EXIT_SUCCESS
|
||||||
|
with changedir(str(output)):
|
||||||
|
rc, out = cmd('extract', '%s::archive' % repo)
|
||||||
|
assert rc == EXIT_SUCCESS
|
||||||
|
rc, out = cmd('extract', '%s::archive' % repo, 'does/not/match')
|
||||||
|
assert rc == EXIT_WARNING # pattern did not match
|
||||||
|
rc, out = cmd('create', '%s::archive' % repo, str(input))
|
||||||
|
assert rc == EXIT_ERROR # duplicate archive name
|
||||||
|
|
||||||
|
|
||||||
class ArchiverTestCaseBase(BaseTestCase):
|
class ArchiverTestCaseBase(BaseTestCase):
|
||||||
EXE = None # python source based
|
EXE = None # python source based
|
||||||
FORK_DEFAULT = False
|
FORK_DEFAULT = False
|
||||||
|
|
Loading…
Reference in New Issue