From 9de07ebd464a4d12fad7a5eb7114072f1e09ea00 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 9 Nov 2023 06:00:13 +0100 Subject: [PATCH] update "modern" error RCs (docs and code) --- docs/internals/frontends.rst | 196 +++++++++++++------- docs/usage/general/environment.rst.inc | 3 + docs/usage/general/return-codes.rst.inc | 6 +- src/borg/archive.py | 12 +- src/borg/archiver/__init__.py | 13 +- src/borg/archiver/check_cmd.py | 13 +- src/borg/archiver/config_cmd.py | 6 +- src/borg/archiver/create_cmd.py | 18 +- src/borg/archiver/debug_cmd.py | 20 +- src/borg/archiver/delete_cmd.py | 2 +- src/borg/archiver/key_cmds.py | 20 +- src/borg/archiver/mount_cmds.py | 13 +- src/borg/archiver/prune_cmd.py | 8 +- src/borg/archiver/rdelete_cmd.py | 5 +- src/borg/archiver/recreate_cmd.py | 5 +- src/borg/cache.py | 26 ++- src/borg/constants.py | 7 +- src/borg/crypto/file_integrity.py | 2 + src/borg/crypto/key.py | 14 ++ src/borg/crypto/keymanager.py | 22 ++- src/borg/helpers/__init__.py | 3 +- src/borg/helpers/errors.py | 28 ++- src/borg/helpers/parseformat.py | 4 + src/borg/helpers/passphrase.py | 14 +- src/borg/locking.py | 18 +- src/borg/manifest.py | 10 +- src/borg/remote.py | 14 ++ src/borg/repository.py | 52 ++++-- src/borg/testsuite/archiver/config_cmd.py | 8 +- src/borg/testsuite/archiver/create_cmd.py | 17 +- src/borg/testsuite/archiver/key_cmds.py | 20 +- src/borg/testsuite/archiver/rdelete_cmd.py | 9 +- src/borg/testsuite/archiver/recreate_cmd.py | 9 +- 33 files changed, 408 insertions(+), 209 deletions(-) diff --git a/docs/internals/frontends.rst b/docs/internals/frontends.rst index 544ec2fd..26cf2edc 100644 --- a/docs/internals/frontends.rst +++ b/docs/internals/frontends.rst @@ -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 log messages. -Assigned message IDs are: +Assigned message IDs and related error RCs (exit codes) are: .. See scripts/errorlist.py; this is slightly edited. Errors - Archive.AlreadyExists - Archive {} already exists - Archive.DoesNotExist - Archive {} does not exist - Archive.IncompatibleFilesystemEncodingError - Failed to encode filename "{}" into file system encoding "{}". Consider configuring the LANG environment variable. - Cache.CacheInitAbortedError - Cache initialization aborted - Cache.EncryptionMethodMismatch - Repository encryption method changed since last access, refusing to continue - 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 + Error rc: 2 traceback: no + Error: {} + ErrorWithTraceback rc: 2 traceback: yes + Error: {} + + ExtensionModuleError rc: 2 traceback: no + The Borg binary extension modules do not seem to be properly installed. + PythonLibcTooOld rc: 2 traceback: no + FATAL: this Python was compiled for a too old (g)libc and misses required functionality. + Buffer.MemoryLimitExceeded rc: 2 traceback: no Requested buffer size {} is above the limit of {}. - ExtensionModuleError - The Borg binary extension modules do not seem to be installed properly - IntegrityError - Data integrity error: {} - NoManifestError - Repository has no manifest. - PlaceholderError + EfficientCollectionQueue.SizeUnderflow rc: 2 traceback: no + Could not pop_front first {} elements, collection only has {} elements.. + RTError rc: 2 traceback: no + Runtime Error: {} + + CancelledByUser rc: 3 traceback: no + Cancelled by user. + + CommandError rc: 4 traceback: no + Command Error: {} + PlaceholderError rc: 5 traceback: no Formatting Error: "{}".format({}): {}({}) - KeyfileInvalidError - Invalid key file for repository {} found in {}. - KeyfileMismatchError - Mismatch between repository {} and key file {}. - KeyfileNotFoundError - No key file for repository {} found in {}. - PassphraseWrong - passphrase supplied in BORG_PASSPHRASE is incorrect - PasswordRetriesExceeded - exceeded the maximum password retries - RepoKeyNotFoundError - No key entry found in the config of repository {}. - UnsupportedManifestError + InvalidPlaceholder rc: 6 traceback: no + Invalid placeholder "{}" in string: {} + + Repository.AlreadyExists rc: 10 traceback: no + A repository already exists at {}. + Repository.CheckNeeded rc: 12 traceback: yes + Inconsistency detected. Please run "borg check {}". + Repository.DoesNotExist rc: 13 traceback: no + Repository {} does not exist. + Repository.InsufficientFreeSpaceError rc: 14 traceback: no + Insufficient free space to complete transaction (required: {}, available: {}). + Repository.InvalidRepository rc: 15 traceback: no + {} 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. - UnsupportedPayloadError - Unsupported payload type {}. A newer version is required to access this repository. - NotABorgKeyFile + + Archive.AlreadyExists rc: 30 traceback: no + 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. - 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. - UnencryptedRepo - Keymanagement not available for unencrypted repositories. - UnknownKeyType - Keytype {0} is unknown. - LockError + UnencryptedRepo rc: 46 traceback: no + Key management not available for unencrypted repositories. + UnknownKeyType rc: 47 traceback: no + Key type {0} is unknown. + 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 {}. - LockErrorT + LockErrorT rc: 71 traceback: yes 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 - InvalidRPCMethod + ConnectionClosedWithHint rc: 81 traceback: no + Connection closed by remote host. {} + InvalidRPCMethod rc: 82 traceback: no RPC method {} is not valid - PathNotAllowed - Repository path not allowed - RemoteRepository.RPCServerOutdated + PathNotAllowed rc: 83 traceback: no + Repository path not allowed: {} + RemoteRepository.RPCServerOutdated rc: 84 traceback: no Borg server is too old for {}. Required version {} - UnexpectedRPCDataFormatFromClient + UnexpectedRPCDataFormatFromClient rc: 85 traceback: no Borg {}: Got unexpected RPC data format from client. - UnexpectedRPCDataFormatFromServer + UnexpectedRPCDataFormatFromServer rc: 86 traceback: no Got unexpected RPC data format from server: {} - Repository.AlreadyExists - Repository {} already exists. - Repository.CheckNeeded - Inconsistency detected. Please run "borg check {}". - Repository.DoesNotExist - Repository {} does not exist. - Repository.InsufficientFreeSpaceError - 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 {}. + + IntegrityError rc: 90 traceback: yes + Data integrity error: {} + FileIntegrityError rc: 91 traceback: yes + File failed integrity check: {} + DecompressionError rc: 92 traceback: yes + Decompression error: {} + Operations - cache.begin_transaction diff --git a/docs/usage/general/environment.rst.inc b/docs/usage/general/environment.rst.inc index 4902c0ae..fe06afde 100644 --- a/docs/usage/general/environment.rst.inc +++ b/docs/usage/general/environment.rst.inc @@ -36,6 +36,9 @@ General: Main usecase for this is to automate fully ``borg change-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. + 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 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 diff --git a/docs/usage/general/return-codes.rst.inc b/docs/usage/general/return-codes.rst.inc index 68f458c4..e908c928 100644 --- a/docs/usage/general/return-codes.rst.inc +++ b/docs/usage/general/return-codes.rst.inc @@ -7,10 +7,12 @@ Borg can exit with the following return codes (rc): Return code Meaning =========== ======= 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) -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) +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) =========== ======= diff --git a/src/borg/archive.py b/src/borg/archive.py index b048d5c1..639f8ef8 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -454,15 +454,21 @@ def archive_put_items(chunk_ids, *, repo_objs, cache=None, stats=None, add_refer class Archive: - class DoesNotExist(Error): - """Archive {} does not exist""" - class AlreadyExists(Error): """Archive {} already exists""" + exit_mcode = 30 + + class DoesNotExist(Error): + """Archive {} does not exist""" + + exit_mcode = 31 + class IncompatibleFilesystemEncodingError(Error): """Failed to encode filename "{}" into file system encoding "{}". Consider configuring the LANG environment variable.""" + exit_mcode = 32 + def __init__( self, manifest, diff --git a/src/borg/archiver/__init__.py b/src/borg/archiver/__init__.py index 8e360d31..f0f0ca5c 100644 --- a/src/borg/archiver/__init__.py +++ b/src/borg/archiver/__init__.py @@ -25,7 +25,7 @@ try: from .. import __version__ from ..constants import * # NOQA 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 remove_surrogates, text_to_json from ..helpers import DatetimeWrapper, replace_placeholders @@ -128,11 +128,6 @@ class Archiver( self.prog = prog 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): msg = args and msg % args or msg 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: # we might not have logging setup yet, so get out quickly print(str(e), file=sys.stderr) - sys.exit(EXIT_ERROR) + sys.exit(CommandError.exit_mcode if modern_ec else EXIT_ERROR) except Exception: msg = "Local Exception" tb = f"{traceback.format_exc()}\n{sysinfo()}" @@ -687,9 +682,9 @@ def main(): # pragma: no cover exit_msg = "terminating with %s status, rc %d" if exit_code == EXIT_SUCCESS: 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)) - 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)) elif exit_code >= EXIT_SIGNAL_BASE: rc_logger.error(exit_msg % ("signal", exit_code)) diff --git a/src/borg/archiver/check_cmd.py b/src/borg/archiver/check_cmd.py index 52563fc5..5ef4ebbc 100644 --- a/src/borg/archiver/check_cmd.py +++ b/src/borg/archiver/check_cmd.py @@ -2,7 +2,7 @@ import argparse from ._common import with_repository, Highlander from ..archive import ArchiveChecker 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 ..logger import create_logger @@ -30,22 +30,19 @@ class CheckMixIn: retry=False, 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)): - self.print_error( + raise CommandError( "--repository-only contradicts --first, --last, -a / --match-archives and --verify-data arguments." ) - return EXIT_ERROR if args.repair and args.max_duration: - self.print_error("--repair does not allow --max-duration argument.") - return EXIT_ERROR + raise CommandError("--repair does not allow --max-duration argument.") if args.max_duration and not args.repo_only: # 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. # 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. - self.print_error("--repository-only is required for --max-duration support.") - return EXIT_ERROR + raise CommandError("--repository-only is required for --max-duration support.") if not args.archives_only: if not repository.check(repair=args.repair, max_duration=args.max_duration): return EXIT_WARNING diff --git a/src/borg/archiver/config_cmd.py b/src/borg/archiver/config_cmd.py index 67595441..6704d716 100644 --- a/src/borg/archiver/config_cmd.py +++ b/src/borg/archiver/config_cmd.py @@ -7,7 +7,7 @@ from ._common import with_repository from ..cache import Cache, assert_secure from ..constants import * # NOQA from ..helpers import EXIT_SUCCESS, EXIT_WARNING -from ..helpers import Error +from ..helpers import Error, CommandError from ..helpers import Location from ..helpers import parse_file_size from ..manifest import Manifest @@ -99,9 +99,7 @@ class ConfigMixIn: if not args.list: if args.name is None: - self.print_error("No config key name was provided.") - return self.exit_code - + raise CommandError("No config key name was provided.") try: section, name = args.name.split(".") except ValueError: diff --git a/src/borg/archiver/create_cmd.py b/src/borg/archiver/create_cmd.py index fb9f3cfa..5c58d89e 100644 --- a/src/borg/archiver/create_cmd.py +++ b/src/borg/archiver/create_cmd.py @@ -29,6 +29,7 @@ from ..helpers import prepare_subprocess_env from ..helpers import sig_int, ignore_sigint from ..helpers import iter_separated from ..helpers import MakePathSafeAction +from ..helpers import Error, CommandError from ..manifest import Manifest from ..patterns import PatternMatcher from ..platform import is_win32 @@ -79,18 +80,15 @@ class CreateMixIn: preexec_fn=None if is_win32 else ignore_sigint, ) except (FileNotFoundError, PermissionError) as e: - self.print_error("Failed to execute command: %s", e) - return self.exit_code + raise CommandError("Failed to execute command: %s", e) status = fso.process_pipe( path=path, cache=cache, fd=proc.stdout, mode=mode, user=user, group=group ) rc = proc.wait() if rc != 0: - self.print_error("Command %r exited with status %d", args.paths[0], rc) - return self.exit_code + raise CommandError("Command %r exited with status %d", args.paths[0], rc) except BackupOSError as e: - self.print_error("%s: %s", path, e) - return self.exit_code + raise Error("%s: %s", path, e) else: status = "+" # included 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 ) except (FileNotFoundError, PermissionError) as e: - self.print_error("Failed to execute command: %s", e) - return self.exit_code + raise CommandError("Failed to execute command: %s", e) pipe_bin = proc.stdout else: # args.paths_from_stdin == True pipe_bin = sys.stdin.buffer @@ -135,8 +132,7 @@ class CreateMixIn: if args.paths_from_command: rc = proc.wait() if rc != 0: - self.print_error("Command %r exited with status %d", args.paths[0], rc) - return self.exit_code + raise CommandError("Command %r exited with status %d", args.paths[0], rc) else: for path in args.paths: if path == "": # issue #5637 @@ -197,7 +193,7 @@ class CreateMixIn: if sig_int: # 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. - self.print_error("Got Ctrl-C / SIGINT.") + raise Error("Got Ctrl-C / SIGINT.") else: archive.save(comment=args.comment, timestamp=args.timestamp) args.stats |= args.json diff --git a/src/borg/archiver/debug_cmd.py b/src/borg/archiver/debug_cmd.py index cab8075c..d7265a5b 100644 --- a/src/borg/archiver/debug_cmd.py +++ b/src/borg/archiver/debug_cmd.py @@ -13,6 +13,7 @@ from ..helpers import bin_to_hex, prepare_dump_dict from ..helpers import dash_open from ..helpers import StableDict from ..helpers import positive_int_validator, archivename_validator +from ..helpers import CommandError, RTError from ..manifest import Manifest from ..platform import get_process_id from ..repository import Repository, LIST_SCAN_LIMIT, TAG_PUT, TAG_DELETE, TAG_COMMIT @@ -191,8 +192,7 @@ class DebugMixIn: except (ValueError, UnicodeEncodeError): wanted = None if not wanted: - self.print_error("search term needs to be hex:123abc or str:foobar style") - return EXIT_ERROR + raise CommandError("search term needs to be hex:123abc or str:foobar style") from ..crypto.key import key_factory @@ -245,13 +245,11 @@ class DebugMixIn: if len(id) != 32: # 256bit raise ValueError("id must be 256bits or 64 hex digits") except ValueError as err: - print(f"object id {hex_id} is invalid [{str(err)}].") - return EXIT_ERROR + raise CommandError(f"object id {hex_id} is invalid [{str(err)}].") try: data = repository.get(id) except Repository.ObjectNotFound: - print("object %s not found." % hex_id) - return EXIT_ERROR + raise RTError("object %s not found." % hex_id) with open(args.path, "wb") as f: f.write(data) print("object %s fetched." % hex_id) @@ -278,8 +276,7 @@ class DebugMixIn: if len(id) != 32: # 256bit raise ValueError("id must be 256bits or 64 hex digits") except ValueError as err: - print(f"object id {hex_id} is invalid [{str(err)}].") - return EXIT_ERROR + raise CommandError(f"object id {hex_id} is invalid [{str(err)}].") with open(args.object_path, "rb") as f: cdata = f.read() @@ -306,8 +303,7 @@ class DebugMixIn: if len(id) != 32: # 256bit raise ValueError("id must be 256bits or 64 hex digits") except ValueError as err: - print(f"object id {hex_id} is invalid [{str(err)}].") - return EXIT_ERROR + raise CommandError(f"object id {hex_id} is invalid [{str(err)}].") with open(args.binary_path, "rb") as f: data = f.read() @@ -334,8 +330,8 @@ class DebugMixIn: if len(id) != 32: # 256bit raise ValueError("id must be 256bits or 64 hex digits") except ValueError as err: - print(f"object id {hex_id} is invalid [{str(err)}].") - return EXIT_ERROR + raise CommandError(f"object id {hex_id} is invalid [{str(err)}].") + repository.put(id, data) print("object %s put." % hex_id) repository.commit(compact=False) diff --git a/src/borg/archiver/delete_cmd.py b/src/borg/archiver/delete_cmd.py index e9444dcf..9e2b4e8b 100644 --- a/src/borg/archiver/delete_cmd.py +++ b/src/borg/archiver/delete_cmd.py @@ -87,7 +87,7 @@ class DeleteMixIn: uncommitted_deletes = 0 if checkpointed else (uncommitted_deletes + 1) if sig_int: # 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: checkpoint_func() if args.stats: diff --git a/src/borg/archiver/key_cmds.py b/src/borg/archiver/key_cmds.py index cd658b72..74b388ac 100644 --- a/src/borg/archiver/key_cmds.py +++ b/src/borg/archiver/key_cmds.py @@ -6,7 +6,7 @@ from ..constants import * # NOQA from ..crypto.key import AESOCBRepoKey, CHPORepoKey, Blake2AESOCBRepoKey, Blake2CHPORepoKey from ..crypto.key import AESOCBKeyfileKey, CHPOKeyfileKey, Blake2AESOCBKeyfileKey, Blake2CHPOKeyfileKey from ..crypto.keymanager import KeyManager -from ..helpers import PathSpec +from ..helpers import PathSpec, CommandError from ..manifest import Manifest from ._common import with_repository @@ -22,8 +22,7 @@ class KeysMixIn: """Change repository key file passphrase""" key = manifest.key if not hasattr(key, "change_passphrase"): - print("This repository is not encrypted, cannot change the passphrase.") - return EXIT_ERROR + raise CommandError("This repository is not encrypted, cannot change the passphrase.") key.change_passphrase() logger.info("Key updated") if hasattr(key, "find_key"): @@ -36,8 +35,7 @@ class KeysMixIn: """Change repository key location""" key = manifest.key if not hasattr(key, "change_passphrase"): - print("This repository is not encrypted, cannot change the key location.") - return EXIT_ERROR + raise CommandError("This repository is not encrypted, cannot change the key location.") if args.key_mode == "keyfile": if isinstance(key, AESOCBRepoKey): @@ -109,8 +107,7 @@ class KeysMixIn: else: manager.export(args.path) except IsADirectoryError: - self.print_error(f"'{args.path}' must be a file, not a directory") - return EXIT_ERROR + raise CommandError(f"'{args.path}' must be a file, not a directory") return EXIT_SUCCESS @with_repository(lock=False, exclusive=False, manifest=False, cache=False) @@ -119,16 +116,13 @@ class KeysMixIn: manager = KeyManager(repository) if args.paper: if args.path: - self.print_error("with --paper import from file is not supported") - return EXIT_ERROR + raise CommandError("with --paper import from file is not supported") manager.import_paperkey(args) else: if not args.path: - self.print_error("input file to import key from expected") - return EXIT_ERROR + raise CommandError("expected input file to import key from") if args.path != "-" and not os.path.exists(args.path): - self.print_error("input file does not exist: " + args.path) - return EXIT_ERROR + raise CommandError("input file does not exist: " + args.path) manager.import_keyfile(args) return EXIT_SUCCESS diff --git a/src/borg/archiver/mount_cmds.py b/src/borg/archiver/mount_cmds.py index 74c70f24..1bd96617 100644 --- a/src/borg/archiver/mount_cmds.py +++ b/src/borg/archiver/mount_cmds.py @@ -3,7 +3,7 @@ import os from ._common import with_repository, Highlander from ..constants import * # NOQA -from ..helpers import EXIT_ERROR +from ..helpers import RTError from ..helpers import PathSpec from ..helpers import umount from ..manifest import Manifest @@ -22,16 +22,13 @@ class MountMixIn: from ..fuse_impl import llfuse, BORG_FUSE_IMPL if llfuse is None: - self.print_error("borg mount not available: no FUSE support, BORG_FUSE_IMPL=%s." % BORG_FUSE_IMPL) - return self.exit_code + raise RTError("borg mount not available: no FUSE support, BORG_FUSE_IMPL=%s." % BORG_FUSE_IMPL) if not os.path.isdir(args.mountpoint): - self.print_error(f"{args.mountpoint}: Mountpoint must be an **existing directory**") - return self.exit_code + raise RTError(f"{args.mountpoint}: Mountpoint must be an **existing directory**") 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") - return self.exit_code + raise RTError(f"{args.mountpoint}: Mountpoint must be a **writable** directory") return self._do_mount(args) @@ -46,7 +43,7 @@ class MountMixIn: operations.mount(args.mountpoint, args.options, args.foreground) except RuntimeError: # Relevant error message already printed to stderr by FUSE - self.exit_code = EXIT_ERROR + raise RTError("FUSE mount failed") return self.exit_code def do_umount(self, args): diff --git a/src/borg/archiver/prune_cmd.py b/src/borg/archiver/prune_cmd.py index c0cf3aed..467fb76a 100644 --- a/src/borg/archiver/prune_cmd.py +++ b/src/borg/archiver/prune_cmd.py @@ -10,7 +10,7 @@ from ._common import with_repository, Highlander from ..archive import Archive, Statistics from ..cache import Cache 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 ..logger import create_logger @@ -77,12 +77,12 @@ class PruneMixIn: if not any( (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", ' '"keep-secondly", "keep-minutely", "keep-hourly", "keep-daily", ' '"keep-weekly", "keep-monthly" or "keep-yearly" settings must be specified.' ) - return self.exit_code + if args.format is not None: format = args.format elif args.short: @@ -173,7 +173,7 @@ class PruneMixIn: pi.finish() if sig_int: # 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: checkpoint_func() if args.stats: diff --git a/src/borg/archiver/rdelete_cmd.py b/src/borg/archiver/rdelete_cmd.py index 40ccbc72..dead4c31 100644 --- a/src/borg/archiver/rdelete_cmd.py +++ b/src/borg/archiver/rdelete_cmd.py @@ -3,7 +3,7 @@ import argparse from ._common import with_repository from ..cache import Cache, SecurityManager from ..constants import * # NOQA -from ..helpers import EXIT_ERROR +from ..helpers import CancelledByUser from ..helpers import format_archive from ..helpers import bin_to_hex from ..helpers import yes @@ -72,8 +72,7 @@ class RDeleteMixIn: retry=False, env_var_override="BORG_DELETE_I_KNOW_WHAT_I_AM_DOING", ): - self.exit_code = EXIT_ERROR - return self.exit_code + raise CancelledByUser() if not dry_run: repository.destroy() logger.info("Repository deleted.") diff --git a/src/borg/archiver/recreate_cmd.py b/src/borg/archiver/recreate_cmd.py index f1803a48..7f4b5da6 100644 --- a/src/borg/archiver/recreate_cmd.py +++ b/src/borg/archiver/recreate_cmd.py @@ -5,7 +5,7 @@ from ._common import build_matcher from ..archive import ArchiveRecreater from ..constants import * # NOQA 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 ..manifest import Manifest @@ -42,8 +42,7 @@ class RecreateMixIn: archive_names = tuple(archive.name for archive in manifest.archives.list_considering(args)) if args.target is not None and len(archive_names) != 1: - self.print_error("--target: Need to specify single archive") - return self.exit_code + raise CommandError("--target: Need to specify single archive") for name in archive_names: if recreater.is_temporary_archive(name): continue diff --git a/src/borg/cache.py b/src/borg/cache.py index c18f315a..a55dadc0 100644 --- a/src/borg/cache.py +++ b/src/borg/cache.py @@ -365,20 +365,30 @@ class CacheConfig: class 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): """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): """Repository access aborted""" - class EncryptionMethodMismatch(Error): - """Repository encryption method changed since last access, refusing to continue""" + exit_mcode = 62 + + 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 def break_lock(repository, path=None): diff --git a/src/borg/constants.py b/src/borg/constants.py index 7f4cbc31..76e63f79 100644 --- a/src/borg/constants.py +++ b/src/borg/constants.py @@ -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) # 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 +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 (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 ISO_FORMAT_NO_USECS = "%Y-%m-%dT%H:%M:%S" diff --git a/src/borg/crypto/file_integrity.py b/src/borg/crypto/file_integrity.py index 33b503a6..2bac18f2 100644 --- a/src/borg/crypto/file_integrity.py +++ b/src/borg/crypto/file_integrity.py @@ -124,6 +124,8 @@ SUPPORTED_ALGORITHMS = { class FileIntegrityError(IntegrityError): """File failed integrity check: {}""" + exit_mcode = 91 + class IntegrityCheckedFile(FileLikeWrapper): def __init__(self, path, write, filename=None, override_fd=None, integrity_data=None): diff --git a/src/borg/crypto/key.py b/src/borg/crypto/key.py index ee820c8b..cf72c8c3 100644 --- a/src/borg/crypto/key.py +++ b/src/borg/crypto/key.py @@ -38,30 +38,44 @@ AUTHENTICATED_NO_KEY = "authenticated_no_key" in workarounds class UnsupportedPayloadError(Error): """Unsupported payload type {}. A newer version is required to access this repository.""" + exit_mcode = 48 + class UnsupportedManifestError(Error): """Unsupported manifest envelope. A newer version is required to access this repository.""" + exit_mcode = 27 + class KeyfileNotFoundError(Error): """No key file for repository {} found in {}.""" + exit_mcode = 42 + class KeyfileInvalidError(Error): """Invalid key file for repository {} found in {}.""" + exit_mcode = 40 + class KeyfileMismatchError(Error): """Mismatch between repository {} and key file {}.""" + exit_mcode = 41 + class RepoKeyNotFoundError(Error): """No key entry found in the config of repository {}.""" + exit_mcode = 44 + class UnsupportedKeyFormatError(Error): """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): for key in AVAILABLE_KEY_TYPES: diff --git a/src/borg/crypto/keymanager.py b/src/borg/crypto/keymanager.py index eb4962bf..4051f469 100644 --- a/src/borg/crypto/keymanager.py +++ b/src/borg/crypto/keymanager.py @@ -13,20 +13,28 @@ from ..repoobj import RepoObj from .key import CHPOKeyfileKey, RepoKeyNotFoundError, KeyBlobStorage, identify_key -class UnencryptedRepo(Error): - """Keymanagement not available for unencrypted repositories.""" +class NotABorgKeyFile(Error): + """This file is not a borg key backup, aborting.""" - -class UnknownKeyType(Error): - """Keytype {0} is unknown.""" + exit_mcode = 43 class RepoIdMismatch(Error): """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): diff --git a/src/borg/helpers/__init__.py b/src/borg/helpers/__init__.py index 64f66078..6a3ee2ad 100644 --- a/src/borg/helpers/__init__.py +++ b/src/borg/helpers/__init__.py @@ -10,7 +10,8 @@ import os from ..constants import * # NOQA from .checks import check_extension_modules, check_python 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 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 diff --git a/src/borg/helpers/errors.py b/src/borg/helpers/errors.py index 882b65fc..fe800ed8 100644 --- a/src/borg/helpers/errors.py +++ b/src/borg/helpers/errors.py @@ -5,6 +5,9 @@ from ..constants import * # NOQA from ..crypto.low_level import IntegrityError as IntegrityErrorBase +modern_ec = os.environ.get("BORG_EXIT_CODES", "legacy") == "modern" + + class Error(Exception): """Error: {}""" @@ -30,9 +33,8 @@ class Error(Exception): @property def exit_code(self): # 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 = os.environ.get("BORG_EXIT_CODES", "legacy") == "modern" - return self.exit_mcode if modern else EXIT_ERROR + # modern: users can opt in to more specific return codes, using BORG_EXIT_CODES: + return self.exit_mcode if modern_ec else EXIT_ERROR class ErrorWithTraceback(Error): @@ -45,6 +47,26 @@ class ErrorWithTraceback(Error): class IntegrityError(ErrorWithTraceback, IntegrityErrorBase): """Data integrity error: {}""" + exit_mcode = 90 + class DecompressionError(IntegrityError): """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 diff --git a/src/borg/helpers/parseformat.py b/src/borg/helpers/parseformat.py index aeb07f83..e5752d34 100644 --- a/src/borg/helpers/parseformat.py +++ b/src/borg/helpers/parseformat.py @@ -224,10 +224,14 @@ class DatetimeWrapper: class PlaceholderError(Error): """Formatting Error: "{}".format({}): {}({})""" + exit_mcode = 5 + class InvalidPlaceholder(PlaceholderError): """Invalid placeholder "{}" in string: {}""" + exit_mcode = 6 + def format_line(format, data): for _, key, _, conversion in Formatter().parse(format): diff --git a/src/borg/helpers/passphrase.py b/src/borg/helpers/passphrase.py index 643709d3..963e1836 100644 --- a/src/borg/helpers/passphrase.py +++ b/src/borg/helpers/passphrase.py @@ -17,18 +17,26 @@ logger = create_logger() class NoPassphraseFailure(Error): """can not acquire a passphrase: {}""" - -class PassphraseWrong(Error): - """passphrase supplied in BORG_PASSPHRASE, by BORG_PASSCOMMAND or via BORG_PASSPHRASE_FD is incorrect.""" + exit_mcode = 50 class PasscommandFailure(Error): """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): """exceeded the maximum password retries""" + exit_mcode = 53 + class Passphrase(str): @classmethod diff --git a/src/borg/locking.py b/src/borg/locking.py index 59508180..67b94a4d 100644 --- a/src/borg/locking.py +++ b/src/borg/locking.py @@ -71,26 +71,38 @@ class TimeoutTimer: class LockError(Error): """Failed to acquire the lock {}.""" + exit_mcode = 70 + class LockErrorT(ErrorWithTraceback): """Failed to acquire the lock {}.""" - -class LockTimeout(LockError): - """Failed to create/acquire the lock {} (timeout).""" + exit_mcode = 71 class LockFailed(LockErrorT): """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): """Failed to release the lock {} (was not locked).""" + exit_mcode = 74 + class NotMyLock(LockErrorT): """Failed to release the lock {} (was/is locked, but not by me).""" + exit_mcode = 75 + class ExclusiveLock: """An exclusive Lock based on mkdir fs operation being atomic. diff --git a/src/borg/manifest.py b/src/borg/manifest.py index 8a6f9a34..9b23cb63 100644 --- a/src/borg/manifest.py +++ b/src/borg/manifest.py @@ -18,12 +18,16 @@ from .patterns import get_regex_from_pattern 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): """Repository has no manifest.""" - -class MandatoryFeatureUnsupported(Error): - """Unsupported repository feature(s) {}. A newer version of borg is required to access this repository.""" + exit_mcode = 26 ArchiveInfo = namedtuple("ArchiveInfo", "name id ts") diff --git a/src/borg/remote.py b/src/borg/remote.py index 4a1551f0..1471b495 100644 --- a/src/borg/remote.py +++ b/src/borg/remote.py @@ -69,26 +69,38 @@ def os_write(fd, data): class ConnectionClosed(Error): """Connection closed by remote host""" + exit_mcode = 80 + class ConnectionClosedWithHint(ConnectionClosed): """Connection closed by remote host. {}""" + exit_mcode = 81 + class PathNotAllowed(Error): """Repository path not allowed: {}""" + exit_mcode = 83 + class InvalidRPCMethod(Error): """RPC method {} is not valid""" + exit_mcode = 82 + class UnexpectedRPCDataFormatFromClient(Error): """Borg {}: Got unexpected RPC data format from client.""" + exit_mcode = 85 + class UnexpectedRPCDataFormatFromServer(Error): """Got unexpected RPC data format from server:\n{}""" + exit_mcode = 86 + def __init__(self, data): try: data = data.decode()[:128] @@ -513,6 +525,8 @@ class RemoteRepository: class RPCServerOutdated(Error): """Borg server is too old for {}. Required version {}""" + exit_mcode = 84 + @property def method(self): return self.args[0] diff --git a/src/borg/repository.py b/src/borg/repository.py index 7bc2b08a..dd59e488 100644 --- a/src/borg/repository.py +++ b/src/borg/repository.py @@ -134,41 +134,61 @@ class Repository: will still get rid of them. """ - class DoesNotExist(Error): - """Repository {} does not exist.""" - class AlreadyExists(Error): """A repository already exists at {}.""" - class PathAlreadyExists(Error): - """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 [{}].""" + exit_mcode = 10 class CheckNeeded(ErrorWithTraceback): """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): """Object with key {} not found in repository {}.""" + exit_mcode = 17 + def __init__(self, id, repo): if isinstance(id, bytes): id = bin_to_hex(id) super().__init__(id, repo) - class InsufficientFreeSpaceError(Error): - """Insufficient free space to complete transaction (required: {}, available: {}).""" + class ParentPathDoesNotExist(Error): + """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): """The storage quota ({}) has been exceeded ({}). Try deleting some archives.""" + exit_mcode = 20 + def __init__( self, path, diff --git a/src/borg/testsuite/archiver/config_cmd.py b/src/borg/testsuite/archiver/config_cmd.py index 29ef32f6..2215b55a 100644 --- a/src/borg/testsuite/archiver/config_cmd.py +++ b/src/borg/testsuite/archiver/config_cmd.py @@ -1,7 +1,9 @@ import os +import pytest from ...constants import * # NOQA 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 @@ -40,5 +42,9 @@ def test_config(archivers, request): cmd(archiver, "config", cfg_key, exit_code=1) 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) diff --git a/src/borg/testsuite/archiver/create_cmd.py b/src/borg/testsuite/archiver/create_cmd.py index 48d8fb23..d536c435 100644 --- a/src/borg/testsuite/archiver/create_cmd.py +++ b/src/borg/testsuite/archiver/create_cmd.py @@ -16,6 +16,7 @@ from ...constants import * # NOQA from ...manifest import Manifest from ...platform import is_cygwin, is_win32, is_darwin from ...repository import Repository +from ...helpers import CommandError from .. import has_lchflags from .. import changedir 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): archiver = request.getfixturevalue(archivers) cmd(archiver, "rcreate", RK_ENCRYPTION) - 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) + if archiver.FORK_DEFAULT: + 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")) 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): archiver = request.getfixturevalue(archivers) cmd(archiver, "rcreate", RK_ENCRYPTION) - 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) + if archiver.FORK_DEFAULT: + 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")) assert archive_list["archives"] == [] diff --git a/src/borg/testsuite/archiver/key_cmds.py b/src/borg/testsuite/archiver/key_cmds.py index dfddd41d..6a596cf4 100644 --- a/src/borg/testsuite/archiver/key_cmds.py +++ b/src/borg/testsuite/archiver/key_cmds.py @@ -6,7 +6,7 @@ import pytest from ...constants import * # NOQA from ...crypto.key import AESOCBRepoKey, AESOCBKeyfileKey, CHPOKeyfileKey, Passphrase 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 msgpack from ...repository import Repository @@ -170,7 +170,11 @@ def test_key_export_directory(archivers, request): export_directory = archiver.output_path + "/exported" os.mkdir(export_directory) 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): @@ -178,14 +182,22 @@ def test_key_export_qr_directory(archivers, request): export_directory = archiver.output_path + "/exported" os.mkdir(export_directory) 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): archiver = request.getfixturevalue(archivers) export_file = archiver.output_path + "/exported" 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: fd.write("something not a key\n") diff --git a/src/borg/testsuite/archiver/rdelete_cmd.py b/src/borg/testsuite/archiver/rdelete_cmd.py index 688dae99..e18eee46 100644 --- a/src/borg/testsuite/archiver/rdelete_cmd.py +++ b/src/borg/testsuite/archiver/rdelete_cmd.py @@ -1,6 +1,9 @@ import os +import pytest + from ...constants import * # NOQA +from ...helpers import CancelledByUser 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 @@ -14,7 +17,11 @@ def test_delete_repo(archivers, request): cmd(archiver, "create", "test", "input") cmd(archiver, "create", "test.2", "input") 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) os.environ["BORG_DELETE_I_KNOW_WHAT_I_AM_DOING"] = "YES" cmd(archiver, "rdelete") diff --git a/src/borg/testsuite/archiver/recreate_cmd.py b/src/borg/testsuite/archiver/recreate_cmd.py index d30460b9..078ec1ed 100644 --- a/src/borg/testsuite/archiver/recreate_cmd.py +++ b/src/borg/testsuite/archiver/recreate_cmd.py @@ -5,6 +5,7 @@ from datetime import datetime import pytest from ...constants import * # NOQA +from ...helpers import CommandError from .. import changedir, are_hardlinks_supported from . import ( _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): archiver = request.getfixturevalue(archivers) cmd(archiver, "rcreate", RK_ENCRYPTION) - output = cmd(archiver, "recreate", "--target=asdf", exit_code=2) - assert "Need to specify single archive" in output + if archiver.FORK_DEFAULT: + 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):