update "modern" error RCs (docs and code)

This commit is contained in:
Thomas Waldmann 2023-11-09 06:00:13 +01:00
parent 34bbce8e71
commit 9de07ebd46
No known key found for this signature in database
GPG Key ID: 243ACFA951F78E01
33 changed files with 408 additions and 209 deletions

View File

@ -565,92 +565,150 @@ Message IDs are strings that essentially give a log message or operation a name,
full text, since texts change more frequently. Message IDs are unambiguous and reduce the need to parse full text, since texts change more frequently. Message IDs are unambiguous and reduce the need to parse
log messages. log messages.
Assigned message IDs are: Assigned message IDs and related error RCs (exit codes) are:
.. See scripts/errorlist.py; this is slightly edited. .. See scripts/errorlist.py; this is slightly edited.
Errors Errors
Archive.AlreadyExists Error rc: 2 traceback: no
Archive {} already exists Error: {}
Archive.DoesNotExist ErrorWithTraceback rc: 2 traceback: yes
Archive {} does not exist Error: {}
Archive.IncompatibleFilesystemEncodingError
Failed to encode filename "{}" into file system encoding "{}". Consider configuring the LANG environment variable. ExtensionModuleError rc: 2 traceback: no
Cache.CacheInitAbortedError The Borg binary extension modules do not seem to be properly installed.
Cache initialization aborted PythonLibcTooOld rc: 2 traceback: no
Cache.EncryptionMethodMismatch FATAL: this Python was compiled for a too old (g)libc and misses required functionality.
Repository encryption method changed since last access, refusing to continue Buffer.MemoryLimitExceeded rc: 2 traceback: no
Cache.RepositoryAccessAborted
Repository access aborted
Cache.RepositoryIDNotUnique
Cache is newer than repository - do you have multiple, independently updated repos with same ID?
Cache.RepositoryReplay
Cache is newer than repository - this is either an attack or unsafe (multiple repos with same ID)
Buffer.MemoryLimitExceeded
Requested buffer size {} is above the limit of {}. Requested buffer size {} is above the limit of {}.
ExtensionModuleError EfficientCollectionQueue.SizeUnderflow rc: 2 traceback: no
The Borg binary extension modules do not seem to be installed properly Could not pop_front first {} elements, collection only has {} elements..
IntegrityError RTError rc: 2 traceback: no
Data integrity error: {} Runtime Error: {}
NoManifestError
Repository has no manifest. CancelledByUser rc: 3 traceback: no
PlaceholderError Cancelled by user.
CommandError rc: 4 traceback: no
Command Error: {}
PlaceholderError rc: 5 traceback: no
Formatting Error: "{}".format({}): {}({}) Formatting Error: "{}".format({}): {}({})
KeyfileInvalidError InvalidPlaceholder rc: 6 traceback: no
Invalid key file for repository {} found in {}. Invalid placeholder "{}" in string: {}
KeyfileMismatchError
Mismatch between repository {} and key file {}. Repository.AlreadyExists rc: 10 traceback: no
KeyfileNotFoundError A repository already exists at {}.
No key file for repository {} found in {}. Repository.CheckNeeded rc: 12 traceback: yes
PassphraseWrong Inconsistency detected. Please run "borg check {}".
passphrase supplied in BORG_PASSPHRASE is incorrect Repository.DoesNotExist rc: 13 traceback: no
PasswordRetriesExceeded Repository {} does not exist.
exceeded the maximum password retries Repository.InsufficientFreeSpaceError rc: 14 traceback: no
RepoKeyNotFoundError Insufficient free space to complete transaction (required: {}, available: {}).
No key entry found in the config of repository {}. Repository.InvalidRepository rc: 15 traceback: no
UnsupportedManifestError {} is not a valid repository. Check repo config.
Repository.InvalidRepositoryConfig rc: 16 traceback: no
{} does not have a valid configuration. Check repo config [{}].
Repository.ObjectNotFound rc: 17 traceback: yes
Object with key {} not found in repository {}.
Repository.ParentPathDoesNotExist rc: 18 traceback: no
The parent path of the repo directory [{}] does not exist.
Repository.PathAlreadyExists rc: 19 traceback: no
There is already something at {}.
Repository.StorageQuotaExceeded rc: 20 traceback: no
The storage quota ({}) has been exceeded ({}). Try deleting some archives.
MandatoryFeatureUnsupported rc: 25 traceback: no
Unsupported repository feature(s) {}. A newer version of borg is required to access this repository.
NoManifestError rc: 26 traceback: no
Repository has no manifest.
UnsupportedManifestError rc: 27 traceback: no
Unsupported manifest envelope. A newer version is required to access this repository. Unsupported manifest envelope. A newer version is required to access this repository.
UnsupportedPayloadError
Unsupported payload type {}. A newer version is required to access this repository. Archive.AlreadyExists rc: 30 traceback: no
NotABorgKeyFile Archive {} already exists
Archive.DoesNotExist rc: 31 traceback: no
Archive {} does not exist
Archive.IncompatibleFilesystemEncodingError rc: 32 traceback: no
Failed to encode filename "{}" into file system encoding "{}". Consider configuring the LANG environment variable.
KeyfileInvalidError rc: 40 traceback: no
Invalid key file for repository {} found in {}.
KeyfileMismatchError rc: 41 traceback: no
Mismatch between repository {} and key file {}.
KeyfileNotFoundError rc: 42 traceback: no
No key file for repository {} found in {}.
NotABorgKeyFile rc: 43 traceback: no
This file is not a borg key backup, aborting. This file is not a borg key backup, aborting.
RepoIdMismatch RepoKeyNotFoundError rc: 44 traceback: no
No key entry found in the config of repository {}.
RepoIdMismatch rc: 45 traceback: no
This key backup seems to be for a different backup repository, aborting. This key backup seems to be for a different backup repository, aborting.
UnencryptedRepo UnencryptedRepo rc: 46 traceback: no
Keymanagement not available for unencrypted repositories. Key management not available for unencrypted repositories.
UnknownKeyType UnknownKeyType rc: 47 traceback: no
Keytype {0} is unknown. Key type {0} is unknown.
LockError UnsupportedPayloadError rc: 48 traceback: no
Unsupported payload type {}. A newer version is required to access this repository.
UnsupportedKeyFormatError rc: 49 traceback:no
Your borg key is stored in an unsupported format. Try using a newer version of borg.
NoPassphraseFailure rc: 50 traceback: no
can not acquire a passphrase: {}
PasscommandFailure rc: 51 traceback: no
passcommand supplied in BORG_PASSCOMMAND failed: {}
PassphraseWrong rc: 52 traceback: no
passphrase supplied in BORG_PASSPHRASE, by BORG_PASSCOMMAND or via BORG_PASSPHRASE_FD is incorrect.
PasswordRetriesExceeded rc: 53 traceback: no
exceeded the maximum password retries
Cache.CacheInitAbortedError rc: 60 traceback: no
Cache initialization aborted
Cache.EncryptionMethodMismatch rc: 61 traceback: no
Repository encryption method changed since last access, refusing to continue
Cache.RepositoryAccessAborted rc: 62 traceback: no
Repository access aborted
Cache.RepositoryIDNotUnique rc: 63 traceback: no
Cache is newer than repository - do you have multiple, independently updated repos with same ID?
Cache.RepositoryReplay rc: 64 traceback: no
Cache, or information obtained from the security directory is newer than repository - this is either an attack or unsafe (multiple repos with same ID)
LockError rc: 70 traceback: no
Failed to acquire the lock {}. Failed to acquire the lock {}.
LockErrorT LockErrorT rc: 71 traceback: yes
Failed to acquire the lock {}. Failed to acquire the lock {}.
ConnectionClosed LockFailed rc: 72 traceback: yes
Failed to create/acquire the lock {} ({}).
LockTimeout rc: 73 traceback: no
Failed to create/acquire the lock {} (timeout).
NotLocked rc: 74 traceback: yes
Failed to release the lock {} (was not locked).
NotMyLock rc: 75 traceback: yes
Failed to release the lock {} (was/is locked, but not by me).
ConnectionClosed rc: 80 traceback: no
Connection closed by remote host Connection closed by remote host
InvalidRPCMethod ConnectionClosedWithHint rc: 81 traceback: no
Connection closed by remote host. {}
InvalidRPCMethod rc: 82 traceback: no
RPC method {} is not valid RPC method {} is not valid
PathNotAllowed PathNotAllowed rc: 83 traceback: no
Repository path not allowed Repository path not allowed: {}
RemoteRepository.RPCServerOutdated RemoteRepository.RPCServerOutdated rc: 84 traceback: no
Borg server is too old for {}. Required version {} Borg server is too old for {}. Required version {}
UnexpectedRPCDataFormatFromClient UnexpectedRPCDataFormatFromClient rc: 85 traceback: no
Borg {}: Got unexpected RPC data format from client. Borg {}: Got unexpected RPC data format from client.
UnexpectedRPCDataFormatFromServer UnexpectedRPCDataFormatFromServer rc: 86 traceback: no
Got unexpected RPC data format from server: Got unexpected RPC data format from server:
{} {}
Repository.AlreadyExists
Repository {} already exists. IntegrityError rc: 90 traceback: yes
Repository.CheckNeeded Data integrity error: {}
Inconsistency detected. Please run "borg check {}". FileIntegrityError rc: 91 traceback: yes
Repository.DoesNotExist File failed integrity check: {}
Repository {} does not exist. DecompressionError rc: 92 traceback: yes
Repository.InsufficientFreeSpaceError Decompression error: {}
Insufficient free space to complete transaction (required: {}, available: {}).
Repository.InvalidRepository
{} is not a valid repository. Check repo config.
Repository.AtticRepository
Attic repository detected. Please run "borg upgrade {}".
Repository.ObjectNotFound
Object with key {} not found in repository {}.
Operations Operations
- cache.begin_transaction - cache.begin_transaction

View File

@ -36,6 +36,9 @@ General:
Main usecase for this is to automate fully ``borg change-passphrase``. Main usecase for this is to automate fully ``borg change-passphrase``.
BORG_DISPLAY_PASSPHRASE BORG_DISPLAY_PASSPHRASE
When set, use the value to answer the "display the passphrase for verification" question when defining a new passphrase for encrypted repositories. When set, use the value to answer the "display the passphrase for verification" question when defining a new passphrase for encrypted repositories.
BORG_EXIT_CODES
When set to "modern", the borg process will return more specific exit codes (rc).
Default is "legacy" and returns rc 2 for all errors, 1 for all warnings, 0 for success.
BORG_HOST_ID BORG_HOST_ID
Borg usually computes a host id from the FQDN plus the results of ``uuid.getnode()`` (which usually returns Borg usually computes a host id from the FQDN plus the results of ``uuid.getnode()`` (which usually returns
a unique id based on the MAC address of the network interface. Except if that MAC happens to be all-zero - in a unique id based on the MAC address of the network interface. Except if that MAC happens to be all-zero - in

View File

@ -7,10 +7,12 @@ Borg can exit with the following return codes (rc):
Return code Meaning Return code Meaning
=========== ======= =========== =======
0 success (logged as INFO) 0 success (logged as INFO)
1 warning (operation reached its normal end, but there were warnings -- 1 generic warning (operation reached its normal end, but there were warnings --
you should check the log, logged as WARNING) you should check the log, logged as WARNING)
2 error (like a fatal error, a local or remote exception, the operation 2 generic error (like a fatal error, a local or remote exception, the operation
did not reach its normal end, logged as ERROR) did not reach its normal end, logged as ERROR)
3..99 specific error (enabled by BORG_EXIT_CODES=modern)
100..127 specific warning (enabled by BORG_EXIT_CODES=modern)
128+N killed by signal N (e.g. 137 == kill -9) 128+N killed by signal N (e.g. 137 == kill -9)
=========== ======= =========== =======

View File

@ -454,15 +454,21 @@ def archive_put_items(chunk_ids, *, repo_objs, cache=None, stats=None, add_refer
class Archive: class Archive:
class DoesNotExist(Error):
"""Archive {} does not exist"""
class AlreadyExists(Error): class AlreadyExists(Error):
"""Archive {} already exists""" """Archive {} already exists"""
exit_mcode = 30
class DoesNotExist(Error):
"""Archive {} does not exist"""
exit_mcode = 31
class IncompatibleFilesystemEncodingError(Error): class IncompatibleFilesystemEncodingError(Error):
"""Failed to encode filename "{}" into file system encoding "{}". Consider configuring the LANG environment variable.""" """Failed to encode filename "{}" into file system encoding "{}". Consider configuring the LANG environment variable."""
exit_mcode = 32
def __init__( def __init__(
self, self,
manifest, manifest,

View File

@ -25,7 +25,7 @@ try:
from .. import __version__ from .. import __version__
from ..constants import * # NOQA from ..constants import * # NOQA
from ..helpers import EXIT_SUCCESS, EXIT_WARNING, EXIT_ERROR, EXIT_SIGNAL_BASE from ..helpers import EXIT_SUCCESS, EXIT_WARNING, EXIT_ERROR, EXIT_SIGNAL_BASE
from ..helpers import Error, set_ec from ..helpers import Error, CommandError, set_ec, modern_ec
from ..helpers import format_file_size from ..helpers import format_file_size
from ..helpers import remove_surrogates, text_to_json from ..helpers import remove_surrogates, text_to_json
from ..helpers import DatetimeWrapper, replace_placeholders from ..helpers import DatetimeWrapper, replace_placeholders
@ -128,11 +128,6 @@ class Archiver(
self.prog = prog self.prog = prog
self.last_checkpoint = time.monotonic() self.last_checkpoint = time.monotonic()
def print_error(self, msg, *args):
msg = args and msg % args or msg
self.exit_code = EXIT_ERROR
logger.error(msg)
def print_warning(self, msg, *args): def print_warning(self, msg, *args):
msg = args and msg % args or msg msg = args and msg % args or msg
self.exit_code = EXIT_WARNING # we do not terminate here, so it is a warning self.exit_code = EXIT_WARNING # we do not terminate here, so it is a warning
@ -631,7 +626,7 @@ def main(): # pragma: no cover
except argparse.ArgumentTypeError as e: except argparse.ArgumentTypeError as e:
# we might not have logging setup yet, so get out quickly # we might not have logging setup yet, so get out quickly
print(str(e), file=sys.stderr) print(str(e), file=sys.stderr)
sys.exit(EXIT_ERROR) sys.exit(CommandError.exit_mcode if modern_ec else EXIT_ERROR)
except Exception: except Exception:
msg = "Local Exception" msg = "Local Exception"
tb = f"{traceback.format_exc()}\n{sysinfo()}" tb = f"{traceback.format_exc()}\n{sysinfo()}"
@ -687,9 +682,9 @@ def main(): # pragma: no cover
exit_msg = "terminating with %s status, rc %d" exit_msg = "terminating with %s status, rc %d"
if exit_code == EXIT_SUCCESS: if exit_code == EXIT_SUCCESS:
rc_logger.info(exit_msg % ("success", exit_code)) rc_logger.info(exit_msg % ("success", exit_code))
elif exit_code == EXIT_WARNING: elif exit_code == EXIT_WARNING or EXIT_WARNING_BASE <= exit_code < EXIT_SIGNAL_BASE:
rc_logger.warning(exit_msg % ("warning", exit_code)) rc_logger.warning(exit_msg % ("warning", exit_code))
elif exit_code == EXIT_ERROR: elif exit_code == EXIT_ERROR or EXIT_ERROR_BASE <= exit_code < EXIT_WARNING_BASE:
rc_logger.error(exit_msg % ("error", exit_code)) rc_logger.error(exit_msg % ("error", exit_code))
elif exit_code >= EXIT_SIGNAL_BASE: elif exit_code >= EXIT_SIGNAL_BASE:
rc_logger.error(exit_msg % ("signal", exit_code)) rc_logger.error(exit_msg % ("signal", exit_code))

View File

@ -2,7 +2,7 @@ import argparse
from ._common import with_repository, Highlander from ._common import with_repository, Highlander
from ..archive import ArchiveChecker from ..archive import ArchiveChecker
from ..constants import * # NOQA from ..constants import * # NOQA
from ..helpers import EXIT_SUCCESS, EXIT_WARNING, EXIT_ERROR from ..helpers import EXIT_SUCCESS, EXIT_WARNING, CancelledByUser, CommandError
from ..helpers import yes from ..helpers import yes
from ..logger import create_logger from ..logger import create_logger
@ -30,22 +30,19 @@ class CheckMixIn:
retry=False, retry=False,
env_var_override="BORG_CHECK_I_KNOW_WHAT_I_AM_DOING", env_var_override="BORG_CHECK_I_KNOW_WHAT_I_AM_DOING",
): ):
return EXIT_ERROR raise CancelledByUser()
if args.repo_only and any((args.verify_data, args.first, args.last, args.match_archives)): if args.repo_only and any((args.verify_data, args.first, args.last, args.match_archives)):
self.print_error( raise CommandError(
"--repository-only contradicts --first, --last, -a / --match-archives and --verify-data arguments." "--repository-only contradicts --first, --last, -a / --match-archives and --verify-data arguments."
) )
return EXIT_ERROR
if args.repair and args.max_duration: if args.repair and args.max_duration:
self.print_error("--repair does not allow --max-duration argument.") raise CommandError("--repair does not allow --max-duration argument.")
return EXIT_ERROR
if args.max_duration and not args.repo_only: if args.max_duration and not args.repo_only:
# when doing a partial repo check, we can only check crc32 checksums in segment files, # when doing a partial repo check, we can only check crc32 checksums in segment files,
# we can't build a fresh repo index in memory to verify the on-disk index against it. # we can't build a fresh repo index in memory to verify the on-disk index against it.
# thus, we should not do an archives check based on a unknown-quality on-disk repo index. # thus, we should not do an archives check based on a unknown-quality on-disk repo index.
# also, there is no max_duration support in the archives check code anyway. # also, there is no max_duration support in the archives check code anyway.
self.print_error("--repository-only is required for --max-duration support.") raise CommandError("--repository-only is required for --max-duration support.")
return EXIT_ERROR
if not args.archives_only: if not args.archives_only:
if not repository.check(repair=args.repair, max_duration=args.max_duration): if not repository.check(repair=args.repair, max_duration=args.max_duration):
return EXIT_WARNING return EXIT_WARNING

View File

@ -7,7 +7,7 @@ from ._common import with_repository
from ..cache import Cache, assert_secure from ..cache import Cache, assert_secure
from ..constants import * # NOQA from ..constants import * # NOQA
from ..helpers import EXIT_SUCCESS, EXIT_WARNING from ..helpers import EXIT_SUCCESS, EXIT_WARNING
from ..helpers import Error from ..helpers import Error, CommandError
from ..helpers import Location from ..helpers import Location
from ..helpers import parse_file_size from ..helpers import parse_file_size
from ..manifest import Manifest from ..manifest import Manifest
@ -99,9 +99,7 @@ class ConfigMixIn:
if not args.list: if not args.list:
if args.name is None: if args.name is None:
self.print_error("No config key name was provided.") raise CommandError("No config key name was provided.")
return self.exit_code
try: try:
section, name = args.name.split(".") section, name = args.name.split(".")
except ValueError: except ValueError:

View File

@ -29,6 +29,7 @@ from ..helpers import prepare_subprocess_env
from ..helpers import sig_int, ignore_sigint from ..helpers import sig_int, ignore_sigint
from ..helpers import iter_separated from ..helpers import iter_separated
from ..helpers import MakePathSafeAction from ..helpers import MakePathSafeAction
from ..helpers import Error, CommandError
from ..manifest import Manifest from ..manifest import Manifest
from ..patterns import PatternMatcher from ..patterns import PatternMatcher
from ..platform import is_win32 from ..platform import is_win32
@ -79,18 +80,15 @@ class CreateMixIn:
preexec_fn=None if is_win32 else ignore_sigint, preexec_fn=None if is_win32 else ignore_sigint,
) )
except (FileNotFoundError, PermissionError) as e: except (FileNotFoundError, PermissionError) as e:
self.print_error("Failed to execute command: %s", e) raise CommandError("Failed to execute command: %s", e)
return self.exit_code
status = fso.process_pipe( status = fso.process_pipe(
path=path, cache=cache, fd=proc.stdout, mode=mode, user=user, group=group path=path, cache=cache, fd=proc.stdout, mode=mode, user=user, group=group
) )
rc = proc.wait() rc = proc.wait()
if rc != 0: if rc != 0:
self.print_error("Command %r exited with status %d", args.paths[0], rc) raise CommandError("Command %r exited with status %d", args.paths[0], rc)
return self.exit_code
except BackupOSError as e: except BackupOSError as e:
self.print_error("%s: %s", path, e) raise Error("%s: %s", path, e)
return self.exit_code
else: else:
status = "+" # included status = "+" # included
self.print_file_status(status, path) self.print_file_status(status, path)
@ -103,8 +101,7 @@ class CreateMixIn:
args.paths, stdout=subprocess.PIPE, env=env, preexec_fn=None if is_win32 else ignore_sigint args.paths, stdout=subprocess.PIPE, env=env, preexec_fn=None if is_win32 else ignore_sigint
) )
except (FileNotFoundError, PermissionError) as e: except (FileNotFoundError, PermissionError) as e:
self.print_error("Failed to execute command: %s", e) raise CommandError("Failed to execute command: %s", e)
return self.exit_code
pipe_bin = proc.stdout pipe_bin = proc.stdout
else: # args.paths_from_stdin == True else: # args.paths_from_stdin == True
pipe_bin = sys.stdin.buffer pipe_bin = sys.stdin.buffer
@ -135,8 +132,7 @@ class CreateMixIn:
if args.paths_from_command: if args.paths_from_command:
rc = proc.wait() rc = proc.wait()
if rc != 0: if rc != 0:
self.print_error("Command %r exited with status %d", args.paths[0], rc) raise CommandError("Command %r exited with status %d", args.paths[0], rc)
return self.exit_code
else: else:
for path in args.paths: for path in args.paths:
if path == "": # issue #5637 if path == "": # issue #5637
@ -197,7 +193,7 @@ class CreateMixIn:
if sig_int: if sig_int:
# do not save the archive if the user ctrl-c-ed - it is valid, but incomplete. # do not save the archive if the user ctrl-c-ed - it is valid, but incomplete.
# we already have a checkpoint archive in this case. # we already have a checkpoint archive in this case.
self.print_error("Got Ctrl-C / SIGINT.") raise Error("Got Ctrl-C / SIGINT.")
else: else:
archive.save(comment=args.comment, timestamp=args.timestamp) archive.save(comment=args.comment, timestamp=args.timestamp)
args.stats |= args.json args.stats |= args.json

View File

@ -13,6 +13,7 @@ from ..helpers import bin_to_hex, prepare_dump_dict
from ..helpers import dash_open from ..helpers import dash_open
from ..helpers import StableDict from ..helpers import StableDict
from ..helpers import positive_int_validator, archivename_validator from ..helpers import positive_int_validator, archivename_validator
from ..helpers import CommandError, RTError
from ..manifest import Manifest from ..manifest import Manifest
from ..platform import get_process_id from ..platform import get_process_id
from ..repository import Repository, LIST_SCAN_LIMIT, TAG_PUT, TAG_DELETE, TAG_COMMIT from ..repository import Repository, LIST_SCAN_LIMIT, TAG_PUT, TAG_DELETE, TAG_COMMIT
@ -191,8 +192,7 @@ class DebugMixIn:
except (ValueError, UnicodeEncodeError): except (ValueError, UnicodeEncodeError):
wanted = None wanted = None
if not wanted: if not wanted:
self.print_error("search term needs to be hex:123abc or str:foobar style") raise CommandError("search term needs to be hex:123abc or str:foobar style")
return EXIT_ERROR
from ..crypto.key import key_factory from ..crypto.key import key_factory
@ -245,13 +245,11 @@ class DebugMixIn:
if len(id) != 32: # 256bit if len(id) != 32: # 256bit
raise ValueError("id must be 256bits or 64 hex digits") raise ValueError("id must be 256bits or 64 hex digits")
except ValueError as err: except ValueError as err:
print(f"object id {hex_id} is invalid [{str(err)}].") raise CommandError(f"object id {hex_id} is invalid [{str(err)}].")
return EXIT_ERROR
try: try:
data = repository.get(id) data = repository.get(id)
except Repository.ObjectNotFound: except Repository.ObjectNotFound:
print("object %s not found." % hex_id) raise RTError("object %s not found." % hex_id)
return EXIT_ERROR
with open(args.path, "wb") as f: with open(args.path, "wb") as f:
f.write(data) f.write(data)
print("object %s fetched." % hex_id) print("object %s fetched." % hex_id)
@ -278,8 +276,7 @@ class DebugMixIn:
if len(id) != 32: # 256bit if len(id) != 32: # 256bit
raise ValueError("id must be 256bits or 64 hex digits") raise ValueError("id must be 256bits or 64 hex digits")
except ValueError as err: except ValueError as err:
print(f"object id {hex_id} is invalid [{str(err)}].") raise CommandError(f"object id {hex_id} is invalid [{str(err)}].")
return EXIT_ERROR
with open(args.object_path, "rb") as f: with open(args.object_path, "rb") as f:
cdata = f.read() cdata = f.read()
@ -306,8 +303,7 @@ class DebugMixIn:
if len(id) != 32: # 256bit if len(id) != 32: # 256bit
raise ValueError("id must be 256bits or 64 hex digits") raise ValueError("id must be 256bits or 64 hex digits")
except ValueError as err: except ValueError as err:
print(f"object id {hex_id} is invalid [{str(err)}].") raise CommandError(f"object id {hex_id} is invalid [{str(err)}].")
return EXIT_ERROR
with open(args.binary_path, "rb") as f: with open(args.binary_path, "rb") as f:
data = f.read() data = f.read()
@ -334,8 +330,8 @@ class DebugMixIn:
if len(id) != 32: # 256bit if len(id) != 32: # 256bit
raise ValueError("id must be 256bits or 64 hex digits") raise ValueError("id must be 256bits or 64 hex digits")
except ValueError as err: except ValueError as err:
print(f"object id {hex_id} is invalid [{str(err)}].") raise CommandError(f"object id {hex_id} is invalid [{str(err)}].")
return EXIT_ERROR
repository.put(id, data) repository.put(id, data)
print("object %s put." % hex_id) print("object %s put." % hex_id)
repository.commit(compact=False) repository.commit(compact=False)

View File

@ -87,7 +87,7 @@ class DeleteMixIn:
uncommitted_deletes = 0 if checkpointed else (uncommitted_deletes + 1) uncommitted_deletes = 0 if checkpointed else (uncommitted_deletes + 1)
if sig_int: if sig_int:
# Ctrl-C / SIGINT: do not checkpoint (commit) again, we already have a checkpoint in this case. # Ctrl-C / SIGINT: do not checkpoint (commit) again, we already have a checkpoint in this case.
self.print_error("Got Ctrl-C / SIGINT.") raise Error("Got Ctrl-C / SIGINT.")
elif uncommitted_deletes > 0: elif uncommitted_deletes > 0:
checkpoint_func() checkpoint_func()
if args.stats: if args.stats:

View File

@ -6,7 +6,7 @@ from ..constants import * # NOQA
from ..crypto.key import AESOCBRepoKey, CHPORepoKey, Blake2AESOCBRepoKey, Blake2CHPORepoKey from ..crypto.key import AESOCBRepoKey, CHPORepoKey, Blake2AESOCBRepoKey, Blake2CHPORepoKey
from ..crypto.key import AESOCBKeyfileKey, CHPOKeyfileKey, Blake2AESOCBKeyfileKey, Blake2CHPOKeyfileKey from ..crypto.key import AESOCBKeyfileKey, CHPOKeyfileKey, Blake2AESOCBKeyfileKey, Blake2CHPOKeyfileKey
from ..crypto.keymanager import KeyManager from ..crypto.keymanager import KeyManager
from ..helpers import PathSpec from ..helpers import PathSpec, CommandError
from ..manifest import Manifest from ..manifest import Manifest
from ._common import with_repository from ._common import with_repository
@ -22,8 +22,7 @@ class KeysMixIn:
"""Change repository key file passphrase""" """Change repository key file passphrase"""
key = manifest.key key = manifest.key
if not hasattr(key, "change_passphrase"): if not hasattr(key, "change_passphrase"):
print("This repository is not encrypted, cannot change the passphrase.") raise CommandError("This repository is not encrypted, cannot change the passphrase.")
return EXIT_ERROR
key.change_passphrase() key.change_passphrase()
logger.info("Key updated") logger.info("Key updated")
if hasattr(key, "find_key"): if hasattr(key, "find_key"):
@ -36,8 +35,7 @@ class KeysMixIn:
"""Change repository key location""" """Change repository key location"""
key = manifest.key key = manifest.key
if not hasattr(key, "change_passphrase"): if not hasattr(key, "change_passphrase"):
print("This repository is not encrypted, cannot change the key location.") raise CommandError("This repository is not encrypted, cannot change the key location.")
return EXIT_ERROR
if args.key_mode == "keyfile": if args.key_mode == "keyfile":
if isinstance(key, AESOCBRepoKey): if isinstance(key, AESOCBRepoKey):
@ -109,8 +107,7 @@ class KeysMixIn:
else: else:
manager.export(args.path) manager.export(args.path)
except IsADirectoryError: except IsADirectoryError:
self.print_error(f"'{args.path}' must be a file, not a directory") raise CommandError(f"'{args.path}' must be a file, not a directory")
return EXIT_ERROR
return EXIT_SUCCESS return EXIT_SUCCESS
@with_repository(lock=False, exclusive=False, manifest=False, cache=False) @with_repository(lock=False, exclusive=False, manifest=False, cache=False)
@ -119,16 +116,13 @@ class KeysMixIn:
manager = KeyManager(repository) manager = KeyManager(repository)
if args.paper: if args.paper:
if args.path: if args.path:
self.print_error("with --paper import from file is not supported") raise CommandError("with --paper import from file is not supported")
return EXIT_ERROR
manager.import_paperkey(args) manager.import_paperkey(args)
else: else:
if not args.path: if not args.path:
self.print_error("input file to import key from expected") raise CommandError("expected input file to import key from")
return EXIT_ERROR
if args.path != "-" and not os.path.exists(args.path): if args.path != "-" and not os.path.exists(args.path):
self.print_error("input file does not exist: " + args.path) raise CommandError("input file does not exist: " + args.path)
return EXIT_ERROR
manager.import_keyfile(args) manager.import_keyfile(args)
return EXIT_SUCCESS return EXIT_SUCCESS

View File

@ -3,7 +3,7 @@ import os
from ._common import with_repository, Highlander from ._common import with_repository, Highlander
from ..constants import * # NOQA from ..constants import * # NOQA
from ..helpers import EXIT_ERROR from ..helpers import RTError
from ..helpers import PathSpec from ..helpers import PathSpec
from ..helpers import umount from ..helpers import umount
from ..manifest import Manifest from ..manifest import Manifest
@ -22,16 +22,13 @@ class MountMixIn:
from ..fuse_impl import llfuse, BORG_FUSE_IMPL from ..fuse_impl import llfuse, BORG_FUSE_IMPL
if llfuse is None: if llfuse is None:
self.print_error("borg mount not available: no FUSE support, BORG_FUSE_IMPL=%s." % BORG_FUSE_IMPL) raise RTError("borg mount not available: no FUSE support, BORG_FUSE_IMPL=%s." % BORG_FUSE_IMPL)
return self.exit_code
if not os.path.isdir(args.mountpoint): if not os.path.isdir(args.mountpoint):
self.print_error(f"{args.mountpoint}: Mountpoint must be an **existing directory**") raise RTError(f"{args.mountpoint}: Mountpoint must be an **existing directory**")
return self.exit_code
if not os.access(args.mountpoint, os.R_OK | os.W_OK | os.X_OK): if not os.access(args.mountpoint, os.R_OK | os.W_OK | os.X_OK):
self.print_error(f"{args.mountpoint}: Mountpoint must be a **writable** directory") raise RTError(f"{args.mountpoint}: Mountpoint must be a **writable** directory")
return self.exit_code
return self._do_mount(args) return self._do_mount(args)
@ -46,7 +43,7 @@ class MountMixIn:
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 = EXIT_ERROR raise RTError("FUSE mount failed")
return self.exit_code return self.exit_code
def do_umount(self, args): def do_umount(self, args):

View File

@ -10,7 +10,7 @@ from ._common import with_repository, Highlander
from ..archive import Archive, Statistics from ..archive import Archive, Statistics
from ..cache import Cache from ..cache import Cache
from ..constants import * # NOQA from ..constants import * # NOQA
from ..helpers import ArchiveFormatter, interval, sig_int, log_multi, ProgressIndicatorPercent from ..helpers import ArchiveFormatter, interval, sig_int, log_multi, ProgressIndicatorPercent, CommandError, Error
from ..manifest import Manifest from ..manifest import Manifest
from ..logger import create_logger from ..logger import create_logger
@ -77,12 +77,12 @@ class PruneMixIn:
if not any( if not any(
(args.secondly, args.minutely, args.hourly, args.daily, args.weekly, args.monthly, args.yearly, args.within) (args.secondly, args.minutely, args.hourly, args.daily, args.weekly, args.monthly, args.yearly, args.within)
): ):
self.print_error( raise CommandError(
'At least one of the "keep-within", "keep-last", ' 'At least one of the "keep-within", "keep-last", '
'"keep-secondly", "keep-minutely", "keep-hourly", "keep-daily", ' '"keep-secondly", "keep-minutely", "keep-hourly", "keep-daily", '
'"keep-weekly", "keep-monthly" or "keep-yearly" settings must be specified.' '"keep-weekly", "keep-monthly" or "keep-yearly" settings must be specified.'
) )
return self.exit_code
if args.format is not None: if args.format is not None:
format = args.format format = args.format
elif args.short: elif args.short:
@ -173,7 +173,7 @@ class PruneMixIn:
pi.finish() pi.finish()
if sig_int: if sig_int:
# Ctrl-C / SIGINT: do not checkpoint (commit) again, we already have a checkpoint in this case. # Ctrl-C / SIGINT: do not checkpoint (commit) again, we already have a checkpoint in this case.
self.print_error("Got Ctrl-C / SIGINT.") raise Error("Got Ctrl-C / SIGINT.")
elif uncommitted_deletes > 0: elif uncommitted_deletes > 0:
checkpoint_func() checkpoint_func()
if args.stats: if args.stats:

View File

@ -3,7 +3,7 @@ import argparse
from ._common import with_repository from ._common import with_repository
from ..cache import Cache, SecurityManager from ..cache import Cache, SecurityManager
from ..constants import * # NOQA from ..constants import * # NOQA
from ..helpers import EXIT_ERROR from ..helpers import CancelledByUser
from ..helpers import format_archive from ..helpers import format_archive
from ..helpers import bin_to_hex from ..helpers import bin_to_hex
from ..helpers import yes from ..helpers import yes
@ -72,8 +72,7 @@ class RDeleteMixIn:
retry=False, retry=False,
env_var_override="BORG_DELETE_I_KNOW_WHAT_I_AM_DOING", env_var_override="BORG_DELETE_I_KNOW_WHAT_I_AM_DOING",
): ):
self.exit_code = EXIT_ERROR raise CancelledByUser()
return self.exit_code
if not dry_run: if not dry_run:
repository.destroy() repository.destroy()
logger.info("Repository deleted.") logger.info("Repository deleted.")

View File

@ -5,7 +5,7 @@ from ._common import build_matcher
from ..archive import ArchiveRecreater from ..archive import ArchiveRecreater
from ..constants import * # NOQA from ..constants import * # NOQA
from ..compress import CompressionSpec from ..compress import CompressionSpec
from ..helpers import archivename_validator, comment_validator, PathSpec, ChunkerParams from ..helpers import archivename_validator, comment_validator, PathSpec, ChunkerParams, CommandError
from ..helpers import timestamp from ..helpers import timestamp
from ..manifest import Manifest from ..manifest import Manifest
@ -42,8 +42,7 @@ class RecreateMixIn:
archive_names = tuple(archive.name for archive in manifest.archives.list_considering(args)) archive_names = tuple(archive.name for archive in manifest.archives.list_considering(args))
if args.target is not None and len(archive_names) != 1: if args.target is not None and len(archive_names) != 1:
self.print_error("--target: Need to specify single archive") raise CommandError("--target: Need to specify single archive")
return self.exit_code
for name in archive_names: for name in archive_names:
if recreater.is_temporary_archive(name): if recreater.is_temporary_archive(name):
continue continue

View File

@ -365,20 +365,30 @@ class CacheConfig:
class Cache: class Cache:
"""Client Side cache""" """Client Side cache"""
class RepositoryIDNotUnique(Error):
"""Cache is newer than repository - do you have multiple, independently updated repos with same ID?"""
class RepositoryReplay(Error):
"""Cache, or information obtained from the security directory is newer than repository - this is either an attack or unsafe (multiple repos with same ID)"""
class CacheInitAbortedError(Error): class CacheInitAbortedError(Error):
"""Cache initialization aborted""" """Cache initialization aborted"""
exit_mcode = 60
class EncryptionMethodMismatch(Error):
"""Repository encryption method changed since last access, refusing to continue"""
exit_mcode = 61
class RepositoryAccessAborted(Error): class RepositoryAccessAborted(Error):
"""Repository access aborted""" """Repository access aborted"""
class EncryptionMethodMismatch(Error): exit_mcode = 62
"""Repository encryption method changed since last access, refusing to continue"""
class RepositoryIDNotUnique(Error):
"""Cache is newer than repository - do you have multiple, independently updated repos with same ID?"""
exit_mcode = 63
class RepositoryReplay(Error):
"""Cache, or information obtained from the security directory is newer than repository - this is either an attack or unsafe (multiple repos with same ID)"""
exit_mcode = 64
@staticmethod @staticmethod
def break_lock(repository, path=None): def break_lock(repository, path=None):

View File

@ -114,10 +114,11 @@ FILES_CACHE_MODE_UI_DEFAULT = "ctime,size,inode" # default for "borg create" co
FILES_CACHE_MODE_DISABLED = "d" # most borg commands do not use the files cache at all (disable) FILES_CACHE_MODE_DISABLED = "d" # most borg commands do not use the files cache at all (disable)
# return codes returned by borg command # return codes returned by borg command
# when borg is killed by signal N, rc = 128 + N
EXIT_SUCCESS = 0 # everything done, no problems EXIT_SUCCESS = 0 # everything done, no problems
EXIT_WARNING = 1 # reached normal end of operation, but there were issues EXIT_WARNING = 1 # reached normal end of operation, but there were issues (generic warning)
EXIT_ERROR = 2 # terminated abruptly, did not reach end of operation EXIT_ERROR = 2 # terminated abruptly, did not reach end of operation (generic error)
EXIT_ERROR_BASE = 3 # specific error codes are 3..99 (enabled by BORG_EXIT_CODES=modern)
EXIT_WARNING_BASE = 100 # specific warning codes are 100..127 (enabled by BORG_EXIT_CODES=modern)
EXIT_SIGNAL_BASE = 128 # terminated due to signal, rc = 128 + sig_no EXIT_SIGNAL_BASE = 128 # terminated due to signal, rc = 128 + sig_no
ISO_FORMAT_NO_USECS = "%Y-%m-%dT%H:%M:%S" ISO_FORMAT_NO_USECS = "%Y-%m-%dT%H:%M:%S"

View File

@ -124,6 +124,8 @@ SUPPORTED_ALGORITHMS = {
class FileIntegrityError(IntegrityError): class FileIntegrityError(IntegrityError):
"""File failed integrity check: {}""" """File failed integrity check: {}"""
exit_mcode = 91
class IntegrityCheckedFile(FileLikeWrapper): class IntegrityCheckedFile(FileLikeWrapper):
def __init__(self, path, write, filename=None, override_fd=None, integrity_data=None): def __init__(self, path, write, filename=None, override_fd=None, integrity_data=None):

View File

@ -38,30 +38,44 @@ AUTHENTICATED_NO_KEY = "authenticated_no_key" in workarounds
class UnsupportedPayloadError(Error): class UnsupportedPayloadError(Error):
"""Unsupported payload type {}. A newer version is required to access this repository.""" """Unsupported payload type {}. A newer version is required to access this repository."""
exit_mcode = 48
class UnsupportedManifestError(Error): class UnsupportedManifestError(Error):
"""Unsupported manifest envelope. A newer version is required to access this repository.""" """Unsupported manifest envelope. A newer version is required to access this repository."""
exit_mcode = 27
class KeyfileNotFoundError(Error): class KeyfileNotFoundError(Error):
"""No key file for repository {} found in {}.""" """No key file for repository {} found in {}."""
exit_mcode = 42
class KeyfileInvalidError(Error): class KeyfileInvalidError(Error):
"""Invalid key file for repository {} found in {}.""" """Invalid key file for repository {} found in {}."""
exit_mcode = 40
class KeyfileMismatchError(Error): class KeyfileMismatchError(Error):
"""Mismatch between repository {} and key file {}.""" """Mismatch between repository {} and key file {}."""
exit_mcode = 41
class RepoKeyNotFoundError(Error): class RepoKeyNotFoundError(Error):
"""No key entry found in the config of repository {}.""" """No key entry found in the config of repository {}."""
exit_mcode = 44
class UnsupportedKeyFormatError(Error): class UnsupportedKeyFormatError(Error):
"""Your borg key is stored in an unsupported format. Try using a newer version of borg.""" """Your borg key is stored in an unsupported format. Try using a newer version of borg."""
exit_mcode = 49
def key_creator(repository, args, *, other_key=None): def key_creator(repository, args, *, other_key=None):
for key in AVAILABLE_KEY_TYPES: for key in AVAILABLE_KEY_TYPES:

View File

@ -13,20 +13,28 @@ from ..repoobj import RepoObj
from .key import CHPOKeyfileKey, RepoKeyNotFoundError, KeyBlobStorage, identify_key from .key import CHPOKeyfileKey, RepoKeyNotFoundError, KeyBlobStorage, identify_key
class UnencryptedRepo(Error): class NotABorgKeyFile(Error):
"""Keymanagement not available for unencrypted repositories.""" """This file is not a borg key backup, aborting."""
exit_mcode = 43
class UnknownKeyType(Error):
"""Keytype {0} is unknown."""
class RepoIdMismatch(Error): class RepoIdMismatch(Error):
"""This key backup seems to be for a different backup repository, aborting.""" """This key backup seems to be for a different backup repository, aborting."""
exit_mcode = 45
class NotABorgKeyFile(Error):
"""This file is not a borg key backup, aborting.""" class UnencryptedRepo(Error):
"""Key management not available for unencrypted repositories."""
exit_mcode = 46
class UnknownKeyType(Error):
"""Key type {0} is unknown."""
exit_mcode = 47
def sha256_truncated(data, num): def sha256_truncated(data, num):

View File

@ -10,7 +10,8 @@ import os
from ..constants import * # NOQA from ..constants import * # NOQA
from .checks import check_extension_modules, check_python from .checks import check_extension_modules, check_python
from .datastruct import StableDict, Buffer, EfficientCollectionQueue from .datastruct import StableDict, Buffer, EfficientCollectionQueue
from .errors import Error, ErrorWithTraceback, IntegrityError, DecompressionError from .errors import Error, ErrorWithTraceback, IntegrityError, DecompressionError, CancelledByUser, CommandError
from .errors import RTError, modern_ec
from .fs import ensure_dir, join_base_dir, get_socket_filename from .fs import ensure_dir, join_base_dir, get_socket_filename
from .fs import get_security_dir, get_keys_dir, get_base_dir, get_cache_dir, get_config_dir, get_runtime_dir from .fs import get_security_dir, get_keys_dir, get_base_dir, get_cache_dir, get_config_dir, get_runtime_dir
from .fs import dir_is_tagged, dir_is_cachedir, remove_dotdot_prefixes, make_path_safe, scandir_inorder from .fs import dir_is_tagged, dir_is_cachedir, remove_dotdot_prefixes, make_path_safe, scandir_inorder

View File

@ -5,6 +5,9 @@ from ..constants import * # NOQA
from ..crypto.low_level import IntegrityError as IntegrityErrorBase from ..crypto.low_level import IntegrityError as IntegrityErrorBase
modern_ec = os.environ.get("BORG_EXIT_CODES", "legacy") == "modern"
class Error(Exception): class Error(Exception):
"""Error: {}""" """Error: {}"""
@ -30,9 +33,8 @@ class Error(Exception):
@property @property
def exit_code(self): def exit_code(self):
# legacy: borg used to always use rc 2 (EXIT_ERROR) for all errors. # legacy: borg used to always use rc 2 (EXIT_ERROR) for all errors.
# modern: users can opt in to more specific return codes, using BORG_RC_STYLE: # modern: users can opt in to more specific return codes, using BORG_EXIT_CODES:
modern = os.environ.get("BORG_EXIT_CODES", "legacy") == "modern" return self.exit_mcode if modern_ec else EXIT_ERROR
return self.exit_mcode if modern else EXIT_ERROR
class ErrorWithTraceback(Error): class ErrorWithTraceback(Error):
@ -45,6 +47,26 @@ class ErrorWithTraceback(Error):
class IntegrityError(ErrorWithTraceback, IntegrityErrorBase): class IntegrityError(ErrorWithTraceback, IntegrityErrorBase):
"""Data integrity error: {}""" """Data integrity error: {}"""
exit_mcode = 90
class DecompressionError(IntegrityError): class DecompressionError(IntegrityError):
"""Decompression error: {}""" """Decompression error: {}"""
exit_mcode = 92
class CancelledByUser(Error):
"""Cancelled by user."""
exit_mcode = 3
class RTError(Error):
"""Runtime Error: {}"""
class CommandError(Error):
"""Command Error: {}"""
exit_mcode = 4

View File

@ -224,10 +224,14 @@ class DatetimeWrapper:
class PlaceholderError(Error): class PlaceholderError(Error):
"""Formatting Error: "{}".format({}): {}({})""" """Formatting Error: "{}".format({}): {}({})"""
exit_mcode = 5
class InvalidPlaceholder(PlaceholderError): class InvalidPlaceholder(PlaceholderError):
"""Invalid placeholder "{}" in string: {}""" """Invalid placeholder "{}" in string: {}"""
exit_mcode = 6
def format_line(format, data): def format_line(format, data):
for _, key, _, conversion in Formatter().parse(format): for _, key, _, conversion in Formatter().parse(format):

View File

@ -17,18 +17,26 @@ logger = create_logger()
class NoPassphraseFailure(Error): class NoPassphraseFailure(Error):
"""can not acquire a passphrase: {}""" """can not acquire a passphrase: {}"""
exit_mcode = 50
class PassphraseWrong(Error):
"""passphrase supplied in BORG_PASSPHRASE, by BORG_PASSCOMMAND or via BORG_PASSPHRASE_FD is incorrect."""
class PasscommandFailure(Error): class PasscommandFailure(Error):
"""passcommand supplied in BORG_PASSCOMMAND failed: {}""" """passcommand supplied in BORG_PASSCOMMAND failed: {}"""
exit_mcode = 51
class PassphraseWrong(Error):
"""passphrase supplied in BORG_PASSPHRASE, by BORG_PASSCOMMAND or via BORG_PASSPHRASE_FD is incorrect."""
exit_mcode = 52
class PasswordRetriesExceeded(Error): class PasswordRetriesExceeded(Error):
"""exceeded the maximum password retries""" """exceeded the maximum password retries"""
exit_mcode = 53
class Passphrase(str): class Passphrase(str):
@classmethod @classmethod

View File

@ -71,26 +71,38 @@ class TimeoutTimer:
class LockError(Error): class LockError(Error):
"""Failed to acquire the lock {}.""" """Failed to acquire the lock {}."""
exit_mcode = 70
class LockErrorT(ErrorWithTraceback): class LockErrorT(ErrorWithTraceback):
"""Failed to acquire the lock {}.""" """Failed to acquire the lock {}."""
exit_mcode = 71
class LockTimeout(LockError):
"""Failed to create/acquire the lock {} (timeout)."""
class LockFailed(LockErrorT): class LockFailed(LockErrorT):
"""Failed to create/acquire the lock {} ({}).""" """Failed to create/acquire the lock {} ({})."""
exit_mcode = 72
class LockTimeout(LockError):
"""Failed to create/acquire the lock {} (timeout)."""
exit_mcode = 73
class NotLocked(LockErrorT): class NotLocked(LockErrorT):
"""Failed to release the lock {} (was not locked).""" """Failed to release the lock {} (was not locked)."""
exit_mcode = 74
class NotMyLock(LockErrorT): class NotMyLock(LockErrorT):
"""Failed to release the lock {} (was/is locked, but not by me).""" """Failed to release the lock {} (was/is locked, but not by me)."""
exit_mcode = 75
class ExclusiveLock: class ExclusiveLock:
"""An exclusive Lock based on mkdir fs operation being atomic. """An exclusive Lock based on mkdir fs operation being atomic.

View File

@ -18,12 +18,16 @@ from .patterns import get_regex_from_pattern
from .repoobj import RepoObj from .repoobj import RepoObj
class MandatoryFeatureUnsupported(Error):
"""Unsupported repository feature(s) {}. A newer version of borg is required to access this repository."""
exit_mcode = 25
class NoManifestError(Error): class NoManifestError(Error):
"""Repository has no manifest.""" """Repository has no manifest."""
exit_mcode = 26
class MandatoryFeatureUnsupported(Error):
"""Unsupported repository feature(s) {}. A newer version of borg is required to access this repository."""
ArchiveInfo = namedtuple("ArchiveInfo", "name id ts") ArchiveInfo = namedtuple("ArchiveInfo", "name id ts")

View File

@ -69,26 +69,38 @@ def os_write(fd, data):
class ConnectionClosed(Error): class ConnectionClosed(Error):
"""Connection closed by remote host""" """Connection closed by remote host"""
exit_mcode = 80
class ConnectionClosedWithHint(ConnectionClosed): class ConnectionClosedWithHint(ConnectionClosed):
"""Connection closed by remote host. {}""" """Connection closed by remote host. {}"""
exit_mcode = 81
class PathNotAllowed(Error): class PathNotAllowed(Error):
"""Repository path not allowed: {}""" """Repository path not allowed: {}"""
exit_mcode = 83
class InvalidRPCMethod(Error): class InvalidRPCMethod(Error):
"""RPC method {} is not valid""" """RPC method {} is not valid"""
exit_mcode = 82
class UnexpectedRPCDataFormatFromClient(Error): class UnexpectedRPCDataFormatFromClient(Error):
"""Borg {}: Got unexpected RPC data format from client.""" """Borg {}: Got unexpected RPC data format from client."""
exit_mcode = 85
class UnexpectedRPCDataFormatFromServer(Error): class UnexpectedRPCDataFormatFromServer(Error):
"""Got unexpected RPC data format from server:\n{}""" """Got unexpected RPC data format from server:\n{}"""
exit_mcode = 86
def __init__(self, data): def __init__(self, data):
try: try:
data = data.decode()[:128] data = data.decode()[:128]
@ -513,6 +525,8 @@ class RemoteRepository:
class RPCServerOutdated(Error): class RPCServerOutdated(Error):
"""Borg server is too old for {}. Required version {}""" """Borg server is too old for {}. Required version {}"""
exit_mcode = 84
@property @property
def method(self): def method(self):
return self.args[0] return self.args[0]

View File

@ -134,41 +134,61 @@ class Repository:
will still get rid of them. will still get rid of them.
""" """
class DoesNotExist(Error):
"""Repository {} does not exist."""
class AlreadyExists(Error): class AlreadyExists(Error):
"""A repository already exists at {}.""" """A repository already exists at {}."""
class PathAlreadyExists(Error): exit_mcode = 10
"""There is already something at {}."""
class ParentPathDoesNotExist(Error):
"""The parent path of the repo directory [{}] does not exist."""
class InvalidRepository(Error):
"""{} is not a valid repository. Check repo config."""
class InvalidRepositoryConfig(Error):
"""{} does not have a valid configuration. Check repo config [{}]."""
class CheckNeeded(ErrorWithTraceback): class CheckNeeded(ErrorWithTraceback):
"""Inconsistency detected. Please run "borg check {}".""" """Inconsistency detected. Please run "borg check {}"."""
exit_mcode = 12
class DoesNotExist(Error):
"""Repository {} does not exist."""
exit_mcode = 13
class InsufficientFreeSpaceError(Error):
"""Insufficient free space to complete transaction (required: {}, available: {})."""
exit_mcode = 14
class InvalidRepository(Error):
"""{} is not a valid repository. Check repo config."""
exit_mcode = 15
class InvalidRepositoryConfig(Error):
"""{} does not have a valid configuration. Check repo config [{}]."""
exit_mcode = 16
class ObjectNotFound(ErrorWithTraceback): class ObjectNotFound(ErrorWithTraceback):
"""Object with key {} not found in repository {}.""" """Object with key {} not found in repository {}."""
exit_mcode = 17
def __init__(self, id, repo): def __init__(self, id, repo):
if isinstance(id, bytes): if isinstance(id, bytes):
id = bin_to_hex(id) id = bin_to_hex(id)
super().__init__(id, repo) super().__init__(id, repo)
class InsufficientFreeSpaceError(Error): class ParentPathDoesNotExist(Error):
"""Insufficient free space to complete transaction (required: {}, available: {}).""" """The parent path of the repo directory [{}] does not exist."""
exit_mcode = 18
class PathAlreadyExists(Error):
"""There is already something at {}."""
exit_mcode = 19
class StorageQuotaExceeded(Error): class StorageQuotaExceeded(Error):
"""The storage quota ({}) has been exceeded ({}). Try deleting some archives.""" """The storage quota ({}) has been exceeded ({}). Try deleting some archives."""
exit_mcode = 20
def __init__( def __init__(
self, self,
path, path,

View File

@ -1,7 +1,9 @@
import os import os
import pytest
from ...constants import * # NOQA from ...constants import * # NOQA
from . import RK_ENCRYPTION, create_test_files, cmd, generate_archiver_tests from . import RK_ENCRYPTION, create_test_files, cmd, generate_archiver_tests
from ...helpers import CommandError
pytest_generate_tests = lambda metafunc: generate_archiver_tests(metafunc, kinds="local,binary") # NOQA pytest_generate_tests = lambda metafunc: generate_archiver_tests(metafunc, kinds="local,binary") # NOQA
@ -40,5 +42,9 @@ def test_config(archivers, request):
cmd(archiver, "config", cfg_key, exit_code=1) cmd(archiver, "config", cfg_key, exit_code=1)
cmd(archiver, "config", "--list", "--delete", exit_code=2) cmd(archiver, "config", "--list", "--delete", exit_code=2)
cmd(archiver, "config", exit_code=2) if archiver.FORK_DEFAULT:
cmd(archiver, "config", exit_code=2)
else:
with pytest.raises(CommandError):
cmd(archiver, "config")
cmd(archiver, "config", "invalid-option", exit_code=1) cmd(archiver, "config", "invalid-option", exit_code=1)

View File

@ -16,6 +16,7 @@ from ...constants import * # NOQA
from ...manifest import Manifest from ...manifest import Manifest
from ...platform import is_cygwin, is_win32, is_darwin from ...platform import is_cygwin, is_win32, is_darwin
from ...repository import Repository from ...repository import Repository
from ...helpers import CommandError
from .. import has_lchflags from .. import has_lchflags
from .. import changedir from .. import changedir
from .. import ( from .. import (
@ -360,8 +361,12 @@ def test_create_content_from_command(archivers, request):
def test_create_content_from_command_with_failed_command(archivers, request): def test_create_content_from_command_with_failed_command(archivers, request):
archiver = request.getfixturevalue(archivers) archiver = request.getfixturevalue(archivers)
cmd(archiver, "rcreate", RK_ENCRYPTION) cmd(archiver, "rcreate", RK_ENCRYPTION)
output = cmd(archiver, "create", "--content-from-command", "test", "--", "sh", "-c", "exit 73;", exit_code=2) if archiver.FORK_DEFAULT:
assert output.endswith("Command 'sh' exited with status 73" + os.linesep) output = cmd(archiver, "create", "--content-from-command", "test", "--", "sh", "-c", "exit 73;", exit_code=2)
assert output.endswith("Command 'sh' exited with status 73" + os.linesep)
else:
with pytest.raises(CommandError):
cmd(archiver, "create", "--content-from-command", "test", "--", "sh", "-c", "exit 73;")
archive_list = json.loads(cmd(archiver, "rlist", "--json")) archive_list = json.loads(cmd(archiver, "rlist", "--json"))
assert archive_list["archives"] == [] assert archive_list["archives"] == []
@ -408,8 +413,12 @@ def test_create_paths_from_command(archivers, request):
def test_create_paths_from_command_with_failed_command(archivers, request): def test_create_paths_from_command_with_failed_command(archivers, request):
archiver = request.getfixturevalue(archivers) archiver = request.getfixturevalue(archivers)
cmd(archiver, "rcreate", RK_ENCRYPTION) cmd(archiver, "rcreate", RK_ENCRYPTION)
output = cmd(archiver, "create", "--paths-from-command", "test", "--", "sh", "-c", "exit 73;", exit_code=2) if archiver.FORK_DEFAULT:
assert output.endswith("Command 'sh' exited with status 73" + os.linesep) output = cmd(archiver, "create", "--paths-from-command", "test", "--", "sh", "-c", "exit 73;", exit_code=2)
assert output.endswith("Command 'sh' exited with status 73" + os.linesep)
else:
with pytest.raises(CommandError):
cmd(archiver, "create", "--paths-from-command", "test", "--", "sh", "-c", "exit 73;")
archive_list = json.loads(cmd(archiver, "rlist", "--json")) archive_list = json.loads(cmd(archiver, "rlist", "--json"))
assert archive_list["archives"] == [] assert archive_list["archives"] == []

View File

@ -6,7 +6,7 @@ import pytest
from ...constants import * # NOQA from ...constants import * # NOQA
from ...crypto.key import AESOCBRepoKey, AESOCBKeyfileKey, CHPOKeyfileKey, Passphrase from ...crypto.key import AESOCBRepoKey, AESOCBKeyfileKey, CHPOKeyfileKey, Passphrase
from ...crypto.keymanager import RepoIdMismatch, NotABorgKeyFile from ...crypto.keymanager import RepoIdMismatch, NotABorgKeyFile
from ...helpers import EXIT_ERROR from ...helpers import EXIT_ERROR, CommandError
from ...helpers import bin_to_hex from ...helpers import bin_to_hex
from ...helpers import msgpack from ...helpers import msgpack
from ...repository import Repository from ...repository import Repository
@ -170,7 +170,11 @@ def test_key_export_directory(archivers, request):
export_directory = archiver.output_path + "/exported" export_directory = archiver.output_path + "/exported"
os.mkdir(export_directory) os.mkdir(export_directory)
cmd(archiver, "rcreate", RK_ENCRYPTION) cmd(archiver, "rcreate", RK_ENCRYPTION)
cmd(archiver, "key", "export", export_directory, exit_code=EXIT_ERROR) if archiver.FORK_DEFAULT:
cmd(archiver, "key", "export", export_directory, exit_code=EXIT_ERROR)
else:
with pytest.raises(CommandError):
cmd(archiver, "key", "export", export_directory)
def test_key_export_qr_directory(archivers, request): def test_key_export_qr_directory(archivers, request):
@ -178,14 +182,22 @@ def test_key_export_qr_directory(archivers, request):
export_directory = archiver.output_path + "/exported" export_directory = archiver.output_path + "/exported"
os.mkdir(export_directory) os.mkdir(export_directory)
cmd(archiver, "rcreate", RK_ENCRYPTION) cmd(archiver, "rcreate", RK_ENCRYPTION)
cmd(archiver, "key", "export", "--qr-html", export_directory, exit_code=EXIT_ERROR) if archiver.FORK_DEFAULT:
cmd(archiver, "key", "export", "--qr-html", export_directory, exit_code=EXIT_ERROR)
else:
with pytest.raises(CommandError):
cmd(archiver, "key", "export", "--qr-html", export_directory)
def test_key_import_errors(archivers, request): def test_key_import_errors(archivers, request):
archiver = request.getfixturevalue(archivers) archiver = request.getfixturevalue(archivers)
export_file = archiver.output_path + "/exported" export_file = archiver.output_path + "/exported"
cmd(archiver, "rcreate", KF_ENCRYPTION) cmd(archiver, "rcreate", KF_ENCRYPTION)
cmd(archiver, "key", "import", export_file, exit_code=EXIT_ERROR) if archiver.FORK_DEFAULT:
cmd(archiver, "key", "import", export_file, exit_code=EXIT_ERROR)
else:
with pytest.raises(CommandError):
cmd(archiver, "key", "import", export_file)
with open(export_file, "w") as fd: with open(export_file, "w") as fd:
fd.write("something not a key\n") fd.write("something not a key\n")

View File

@ -1,6 +1,9 @@
import os import os
import pytest
from ...constants import * # NOQA from ...constants import * # NOQA
from ...helpers import CancelledByUser
from . import create_regular_file, cmd, generate_archiver_tests, RK_ENCRYPTION from . import create_regular_file, cmd, generate_archiver_tests, RK_ENCRYPTION
pytest_generate_tests = lambda metafunc: generate_archiver_tests(metafunc, kinds="local,remote,binary") # NOQA pytest_generate_tests = lambda metafunc: generate_archiver_tests(metafunc, kinds="local,remote,binary") # NOQA
@ -14,7 +17,11 @@ def test_delete_repo(archivers, request):
cmd(archiver, "create", "test", "input") cmd(archiver, "create", "test", "input")
cmd(archiver, "create", "test.2", "input") cmd(archiver, "create", "test.2", "input")
os.environ["BORG_DELETE_I_KNOW_WHAT_I_AM_DOING"] = "no" os.environ["BORG_DELETE_I_KNOW_WHAT_I_AM_DOING"] = "no"
cmd(archiver, "rdelete", exit_code=2) if archiver.FORK_DEFAULT:
cmd(archiver, "rdelete", exit_code=2)
else:
with pytest.raises(CancelledByUser):
cmd(archiver, "rdelete")
assert os.path.exists(archiver.repository_path) assert os.path.exists(archiver.repository_path)
os.environ["BORG_DELETE_I_KNOW_WHAT_I_AM_DOING"] = "YES" os.environ["BORG_DELETE_I_KNOW_WHAT_I_AM_DOING"] = "YES"
cmd(archiver, "rdelete") cmd(archiver, "rdelete")

View File

@ -5,6 +5,7 @@ from datetime import datetime
import pytest import pytest
from ...constants import * # NOQA from ...constants import * # NOQA
from ...helpers import CommandError
from .. import changedir, are_hardlinks_supported from .. import changedir, are_hardlinks_supported
from . import ( from . import (
_create_test_caches, _create_test_caches,
@ -82,8 +83,12 @@ def test_recreate_hardlinked_tags(archivers, request): # test for issue #4911
def test_recreate_target_rc(archivers, request): def test_recreate_target_rc(archivers, request):
archiver = request.getfixturevalue(archivers) archiver = request.getfixturevalue(archivers)
cmd(archiver, "rcreate", RK_ENCRYPTION) cmd(archiver, "rcreate", RK_ENCRYPTION)
output = cmd(archiver, "recreate", "--target=asdf", exit_code=2) if archiver.FORK_DEFAULT:
assert "Need to specify single archive" in output output = cmd(archiver, "recreate", "--target=asdf", exit_code=2)
assert "Need to specify single archive" in output
else:
with pytest.raises(CommandError):
cmd(archiver, "recreate", "--target=asdf")
def test_recreate_target(archivers, request): def test_recreate_target(archivers, request):