From 864333d6869d2c93840316879f5735d5b92a1a5d Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sun, 2 Oct 2016 00:43:06 +0200 Subject: [PATCH 01/15] pyinstaller: use a spec file to build borg.exe binary also: exclude osxfuse dylib on Mac OS X --- Vagrantfile | 2 +- scripts/borg.exe.spec | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 scripts/borg.exe.spec diff --git a/Vagrantfile b/Vagrantfile index ef38aa94a..60f1b02a8 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -263,7 +263,7 @@ def build_binary_with_pyinstaller(boxname) cd /vagrant/borg . borg-env/bin/activate cd borg - pyinstaller -F -n borg.exe --distpath=/vagrant/borg --clean borg/__main__.py + pyinstaller --clean --distpath=/vagrant/borg scripts/borg.exe.spec EOF end diff --git a/scripts/borg.exe.spec b/scripts/borg.exe.spec new file mode 100644 index 000000000..9834c1b8e --- /dev/null +++ b/scripts/borg.exe.spec @@ -0,0 +1,38 @@ +# -*- mode: python -*- +# this pyinstaller spec file is used to build borg binaries on posix platforms + +import os, sys + +basepath = '/vagrant/borg/borg' + +block_cipher = None + +a = Analysis([os.path.join(basepath, 'borg/__main__.py'), ], + pathex=[basepath, ], + binaries=[], + datas=[], + hiddenimports=['borg.platform.posix'], + hookspath=[], + runtime_hooks=[], + excludes=[], + win_no_prefer_redirects=False, + win_private_assemblies=False, + cipher=block_cipher) + +if sys.platform == 'darwin': + # do not bundle the osxfuse libraries, so we do not get a version + # mismatch to the installed kernel driver of osxfuse. + a.binaries = [b for b in a.binaries if 'libosxfuse' not in b[0]] + +pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) + +exe = EXE(pyz, + a.scripts, + a.binaries, + a.zipfiles, + a.datas, + name='borg.exe', + debug=False, + strip=False, + upx=True, + console=True ) From ce72d2482599e090850bd156032c25b93e197698 Mon Sep 17 00:00:00 2001 From: enkore Date: Sun, 2 Oct 2016 20:11:34 +0200 Subject: [PATCH 02/15] Clarify FAQ regarding backup of virtual machines (#1672) --- docs/faq.rst | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/docs/faq.rst b/docs/faq.rst index a84cf481b..47d0ce1a2 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -12,6 +12,39 @@ Yes, the `deduplication`_ technique used by |project_name| makes sure only the modified parts of the file are stored. Also, we have optional simple sparse file support for extract. +If you use non-snapshotting backup tools like Borg to back up virtual machines, +then these should be turned off for doing so. Backing up live VMs this way can (and will) +result in corrupted or inconsistent backup contents: a VM image is just a regular file to +Borg with the same issues as regular files when it comes to concurrent reading and writing from +the same file. + +For backing up live VMs use file system snapshots on the VM host, which establishes +crash-consistency for the VM images. This means that with most file systems +(that are journaling) the FS will always be fine in the backup (but may need a +journal replay to become accessible). + +Usually this does not mean that file *contents* on the VM are consistent, since file +contents are normally not journaled. Notable exceptions are ext4 in data=journal mode, +ZFS and btrfs (unless nodatacow is used). + +Applications designed with crash-consistency in mind (most relational databases +like PostgreSQL, SQLite etc. but also for example Borg repositories) should always +be able to recover to a consistent state from a backup created with +crash-consistent snapshots (even on ext4 with data=writeback or XFS). + +Hypervisor snapshots capturing most of the VM's state can also be used for backups +and can be a better alternative to pure file system based snapshots of the VM's disk, +since no state is lost. Depending on the application this can be the easiest and most +reliable way to create application-consistent backups. + +Other applications may require a lot of work to reach application-consistency: +It's a broad and complex issue that cannot be explained in entirety here. + +Borg doesn't intend to address these issues due to their huge complexity +and platform/software dependency. Combining Borg with the mechanisms provided +by the platform (snapshots, hypervisor features) will be the best approach +to start tackling them. + Can I backup from multiple servers into a single repository? ------------------------------------------------------------ From cf1c73b4f94fc8a4529d5e0c3f00ea167861fd51 Mon Sep 17 00:00:00 2001 From: Simon Heath Date: Sun, 2 Oct 2016 17:14:34 -0400 Subject: [PATCH 03/15] Added docs explaining multiple --restrict-to-path flags, with example (take 2) --- borg/archiver.py | 3 ++- docs/deployment.rst | 5 +++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/borg/archiver.py b/borg/archiver.py index ce7655fc5..785a98a6d 100644 --- a/borg/archiver.py +++ b/borg/archiver.py @@ -1011,7 +1011,8 @@ class Archiver: help='start repository server process') subparser.set_defaults(func=self.do_serve) subparser.add_argument('--restrict-to-path', dest='restrict_to_paths', action='append', - metavar='PATH', help='restrict repository access to PATH') + metavar='PATH', help='restrict repository access to PATH. ' + 'Can be specified multiple times to allow the client access to several directories.') subparser.add_argument('--append-only', dest='append_only', action='store_true', help='only allow appending to repository segment files') init_epilog = textwrap.dedent(""" diff --git a/docs/deployment.rst b/docs/deployment.rst index 3c76500fe..b4794300a 100644 --- a/docs/deployment.rst +++ b/docs/deployment.rst @@ -54,6 +54,11 @@ Restrictions Borg is instructed to restrict clients into their own paths: ``borg serve --restrict-to-path /home/backup/repos/`` +The client will be able to access any file or subdirectory inside of ``/home/backup/repos/`` +but no other directories. You can allow a client to access several directories by passing multiple +`--restrict-to-path` flags, for instance: ``borg serve --restrict-to-path /home/backup/repos//root --restrict-to-path /home/backup/repos//home``, +or instead simply use `--restrict-to-path` once to restrict the client to ``/home/backup/repos//*``. + There is only one ssh key per client allowed. Keys are added for ``johndoe.clnt.local``, ``web01.srv.local`` and ``app01.srv.local``. But they will access the backup under only one UNIX user account as: ``backup@backup01.srv.local``. Every key in ``$HOME/.ssh/authorized_keys`` has a From 573cb616d3979d2deae249565afddeb56457029c Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Thu, 6 Oct 2016 01:00:07 +0200 Subject: [PATCH 04/15] deployment: synthesize alternative --restrict-to-path example --- borg/archiver.py | 3 ++- docs/deployment.rst | 7 ++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/borg/archiver.py b/borg/archiver.py index 785a98a6d..ab465e4f6 100644 --- a/borg/archiver.py +++ b/borg/archiver.py @@ -1012,7 +1012,8 @@ class Archiver: subparser.set_defaults(func=self.do_serve) subparser.add_argument('--restrict-to-path', dest='restrict_to_paths', action='append', metavar='PATH', help='restrict repository access to PATH. ' - 'Can be specified multiple times to allow the client access to several directories.') + 'Can be specified multiple times to allow the client access to several directories. ' + 'Access to all sub-directories is granted implicitly; PATH doesn\'t need to directly point to a repository.') subparser.add_argument('--append-only', dest='append_only', action='store_true', help='only allow appending to repository segment files') init_epilog = textwrap.dedent(""" diff --git a/docs/deployment.rst b/docs/deployment.rst index b4794300a..c73c6ddb2 100644 --- a/docs/deployment.rst +++ b/docs/deployment.rst @@ -55,9 +55,10 @@ Borg is instructed to restrict clients into their own paths: ``borg serve --restrict-to-path /home/backup/repos/`` The client will be able to access any file or subdirectory inside of ``/home/backup/repos/`` -but no other directories. You can allow a client to access several directories by passing multiple -`--restrict-to-path` flags, for instance: ``borg serve --restrict-to-path /home/backup/repos//root --restrict-to-path /home/backup/repos//home``, -or instead simply use `--restrict-to-path` once to restrict the client to ``/home/backup/repos//*``. +but no other directories. You can allow a client to access several separate directories by passing multiple +`--restrict-to-path` flags, for instance: ``borg serve --restrict-to-path /home/backup/repos/ --restrict-to-path /home/backup/repos/``, +which could make sense if multiple machines belong to one person which should then have access to all the +backups of their machines. There is only one ssh key per client allowed. Keys are added for ``johndoe.clnt.local``, ``web01.srv.local`` and ``app01.srv.local``. But they will access the backup under only one UNIX user account as: From 67aafec1951e900bcb6cfe6edff1bd83d465f351 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 6 Oct 2016 05:15:01 +0200 Subject: [PATCH 05/15] backport bin_to_hex and use it simplifies code and also porting and merging between 1.0 and 1.1/master. --- borg/archive.py | 9 ++++----- borg/archiver.py | 10 +++++----- borg/cache.py | 16 ++++++++-------- borg/helpers.py | 5 +++++ borg/key.py | 6 +++--- borg/keymanager.py | 14 +++++++------- borg/repository.py | 6 +++--- borg/testsuite/archiver.py | 10 +++++----- borg/upgrader.py | 11 +++++------ 9 files changed, 45 insertions(+), 42 deletions(-) diff --git a/borg/archive.py b/borg/archive.py index 8619fd8f1..e725857e5 100644 --- a/borg/archive.py +++ b/borg/archive.py @@ -1,4 +1,3 @@ -from binascii import hexlify from contextlib import contextmanager from datetime import datetime, timezone from getpass import getuser @@ -17,7 +16,7 @@ import sys import time from io import BytesIO from . import xattr -from .helpers import Error, uid2user, user2uid, gid2group, group2gid, \ +from .helpers import Error, uid2user, user2uid, gid2group, group2gid, bin_to_hex, \ parse_timestamp, to_localtime, format_time, format_timedelta, remove_surrogates, \ Manifest, Statistics, decode_dict, make_path_safe, StableDict, int_to_bigint, bigint_to_int, \ ProgressIndicatorPercent @@ -254,7 +253,7 @@ class Archive: @property def fpr(self): - return hexlify(self.id).decode('ascii') + return bin_to_hex(self.id) @property def duration(self): @@ -522,7 +521,7 @@ Number of files: {0.stats.nfiles}'''.format( try: self.cache.chunk_decref(id, stats) except KeyError: - cid = hexlify(id).decode('ascii') + cid = bin_to_hex(id) raise ChunksIndexError(cid) except Repository.ObjectNotFound as e: # object not in repo - strange, but we wanted to delete it anyway. @@ -1010,7 +1009,7 @@ class ArchiveChecker: return _state def report(msg, chunk_id, chunk_no): - cid = hexlify(chunk_id).decode('ascii') + cid = bin_to_hex(chunk_id) msg += ' [chunk: %06d_%s]' % (chunk_no, cid) # see debug-dump-archive-items self.error_found = True logger.error(msg) diff --git a/borg/archiver.py b/borg/archiver.py index ab465e4f6..fb5c14db9 100644 --- a/borg/archiver.py +++ b/borg/archiver.py @@ -1,4 +1,4 @@ -from binascii import hexlify, unhexlify +from binascii import unhexlify from datetime import datetime from hashlib import sha256 from operator import attrgetter @@ -18,7 +18,7 @@ import collections from . import __version__ from .helpers import Error, location_validator, archivename_validator, format_line, format_time, format_file_size, \ - parse_pattern, PathPrefixPattern, to_localtime, timestamp, safe_timestamp, \ + parse_pattern, PathPrefixPattern, to_localtime, timestamp, safe_timestamp, bin_to_hex, \ get_cache_dir, prune_within, prune_split, \ Manifest, NoManifestError, remove_surrogates, update_excludes, format_archive, check_extension_modules, Statistics, \ dir_is_tagged, bigint_to_int, ChunkerParams, CompressionSpec, PrefixSpec, is_slow_msgpack, yes, sysinfo, \ @@ -631,7 +631,7 @@ class Archiver: """Show archive details such as disk space used""" stats = archive.calc_stats(cache) print('Name:', archive.name) - print('Fingerprint: %s' % hexlify(archive.id).decode('ascii')) + print('Fingerprint: %s' % bin_to_hex(archive.id)) print('Hostname:', archive.metadata[b'hostname']) print('Username:', archive.metadata[b'username']) print('Time (start): %s' % format_time(to_localtime(archive.ts))) @@ -727,7 +727,7 @@ class Archiver: archive = Archive(repository, key, manifest, args.location.archive) for i, item_id in enumerate(archive.metadata[b'items']): data = key.decrypt(item_id, repository.get(item_id)) - filename = '%06d_%s.items' % (i, hexlify(item_id).decode('ascii')) + filename = '%06d_%s.items' % (i, bin_to_hex(item_id)) print('Dumping', filename) with open(filename, 'wb') as fd: fd.write(data) @@ -748,7 +748,7 @@ class Archiver: cdata = repository.get(id) give_id = id if id != Manifest.MANIFEST_ID else None data = key.decrypt(give_id, cdata) - filename = '%06d_%s.obj' % (i, hexlify(id).decode('ascii')) + filename = '%06d_%s.obj' % (i, bin_to_hex(id)) print('Dumping', filename) with open(filename, 'wb') as fd: fd.write(data) diff --git a/borg/cache.py b/borg/cache.py index b843fc49e..e293d9e14 100644 --- a/borg/cache.py +++ b/borg/cache.py @@ -3,14 +3,14 @@ from .remote import cache_if_remote from collections import namedtuple import os import stat -from binascii import hexlify, unhexlify +from binascii import unhexlify import shutil from .key import PlaintextKey from .logger import create_logger logger = create_logger() from .helpers import Error, get_cache_dir, decode_dict, int_to_bigint, \ - bigint_to_int, format_file_size, yes + bigint_to_int, format_file_size, yes, bin_to_hex from .locking import Lock from .hashindex import ChunkIndex @@ -34,13 +34,13 @@ class Cache: @staticmethod def break_lock(repository, path=None): - path = path or os.path.join(get_cache_dir(), hexlify(repository.id).decode('ascii')) + path = path or os.path.join(get_cache_dir(), bin_to_hex(repository.id)) Lock(os.path.join(path, 'lock'), exclusive=True).break_lock() @staticmethod def destroy(repository, path=None): """destroy the cache for ``repository`` or at ``path``""" - path = path or os.path.join(get_cache_dir(), hexlify(repository.id).decode('ascii')) + path = path or os.path.join(get_cache_dir(), bin_to_hex(repository.id)) config = os.path.join(path, 'config') if os.path.exists(config): os.remove(config) # kill config first @@ -55,7 +55,7 @@ class Cache: self.repository = repository self.key = key self.manifest = manifest - self.path = path or os.path.join(get_cache_dir(), hexlify(repository.id).decode('ascii')) + self.path = path or os.path.join(get_cache_dir(), bin_to_hex(repository.id)) self.do_files = do_files # Warn user before sending data to a never seen before unencrypted repository if not os.path.exists(self.path): @@ -122,7 +122,7 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" config = configparser.ConfigParser(interpolation=None) config.add_section('cache') config.set('cache', 'version', '1') - config.set('cache', 'repository', hexlify(self.repository.id).decode('ascii')) + config.set('cache', 'repository', bin_to_hex(self.repository.id)) config.set('cache', 'manifest', '') with open(os.path.join(self.path, 'config'), 'w') as fd: config.write(fd) @@ -208,7 +208,7 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" if age == 0 and bigint_to_int(item[3]) < self._newest_mtime or \ age > 0 and age < ttl: msgpack.pack((path_hash, item), fd) - self.config.set('cache', 'manifest', hexlify(self.manifest.id).decode('ascii')) + self.config.set('cache', 'manifest', bin_to_hex(self.manifest.id)) self.config.set('cache', 'timestamp', self.manifest.timestamp) self.config.set('cache', 'key_type', str(self.key.TYPE)) self.config.set('cache', 'previous_location', self.repository._location.canonical_path()) @@ -251,7 +251,7 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" archive_path = os.path.join(self.path, 'chunks.archive.d') def mkpath(id, suffix=''): - id_hex = hexlify(id).decode('ascii') + id_hex = bin_to_hex(id) path = os.path.join(archive_path, id_hex + suffix) return path.encode('utf-8') diff --git a/borg/helpers.py b/borg/helpers.py index db579d34f..a452e17aa 100644 --- a/borg/helpers.py +++ b/borg/helpers.py @@ -1,4 +1,5 @@ import argparse +from binascii import hexlify from collections import namedtuple import contextlib from functools import wraps @@ -759,6 +760,10 @@ def safe_encode(s, coding='utf-8', errors='surrogateescape'): return s.encode(coding, errors) +def bin_to_hex(binary): + return hexlify(binary).decode('ascii') + + class Location: """Object representing a repository / archive location """ diff --git a/borg/key.py b/borg/key.py index e88baf57f..648d2193b 100644 --- a/borg/key.py +++ b/borg/key.py @@ -7,7 +7,7 @@ import textwrap from hmac import HMAC, compare_digest from hashlib import sha256, pbkdf2_hmac -from .helpers import IntegrityError, get_keys_dir, Error, yes +from .helpers import IntegrityError, get_keys_dir, Error, yes, bin_to_hex from .logger import create_logger logger = create_logger() @@ -201,7 +201,7 @@ class Passphrase(str): passphrase.encode('ascii') except UnicodeEncodeError: print('Your passphrase (UTF-8 encoding in hex): %s' % - hexlify(passphrase.encode('utf-8')).decode('ascii'), + bin_to_hex(passphrase.encode('utf-8')), file=sys.stderr) print('As you have a non-ASCII passphrase, it is recommended to keep the UTF-8 encoding in hex together with the passphrase at a safe place.', file=sys.stderr) @@ -427,7 +427,7 @@ class KeyfileKey(KeyfileKeyBase): def save(self, target, passphrase): key_data = self._save(passphrase) with open(target, 'w') as fd: - fd.write('%s %s\n' % (self.FILE_ID, hexlify(self.repository_id).decode('ascii'))) + fd.write('%s %s\n' % (self.FILE_ID, bin_to_hex(self.repository_id))) fd.write(key_data) fd.write('\n') self.target = target diff --git a/borg/keymanager.py b/borg/keymanager.py index 8eef581da..0b365e822 100644 --- a/borg/keymanager.py +++ b/borg/keymanager.py @@ -1,10 +1,10 @@ -from binascii import hexlify, unhexlify, a2b_base64, b2a_base64 +from binascii import unhexlify, a2b_base64, b2a_base64 import binascii import textwrap from hashlib import sha256 from .key import KeyfileKey, RepoKey, PassphraseKey, KeyfileNotFoundError, PlaintextKey -from .helpers import Manifest, NoManifestError, Error, yes +from .helpers import Manifest, NoManifestError, Error, yes, bin_to_hex from .repository import Repository @@ -79,7 +79,7 @@ class KeyManager: def store_keyfile(self, target): with open(target, 'w') as fd: - fd.write('%s %s\n' % (KeyfileKey.FILE_ID, hexlify(self.repository.id).decode('ascii'))) + fd.write('%s %s\n' % (KeyfileKey.FILE_ID, bin_to_hex(self.repository.id))) fd.write(self.keyblob) if not self.keyblob.endswith('\n'): fd.write('\n') @@ -103,7 +103,7 @@ class KeyManager: binary = a2b_base64(self.keyblob) export += 'BORG PAPER KEY v1\n' lines = (len(binary) + 17) // 18 - repoid = hexlify(self.repository.id).decode('ascii')[:18] + repoid = bin_to_hex(self.repository.id)[:18] complete_checksum = sha256_truncated(binary, 12) export += 'id: {0:d} / {1} / {2} - {3}\n'.format(lines, grouped(repoid), @@ -114,7 +114,7 @@ class KeyManager: idx += 1 binline = binary[:18] checksum = sha256_truncated(idx.to_bytes(2, byteorder='big') + binline, 2) - export += '{0:2d}: {1} - {2}\n'.format(idx, grouped(hexlify(binline).decode('ascii')), checksum) + export += '{0:2d}: {1} - {2}\n'.format(idx, grouped(bin_to_hex(binline)), checksum) binary = binary[18:] if path: @@ -125,7 +125,7 @@ class KeyManager: def import_keyfile(self, args): file_id = KeyfileKey.FILE_ID - first_line = file_id + ' ' + hexlify(self.repository.id).decode('ascii') + '\n' + first_line = file_id + ' ' + bin_to_hex(self.repository.id) + '\n' with open(args.path, 'r') as fd: file_first_line = fd.read(len(first_line)) if file_first_line != first_line: @@ -141,7 +141,7 @@ class KeyManager: # imported here because it has global side effects import readline - repoid = hexlify(self.repository.id).decode('ascii')[:18] + repoid = bin_to_hex(self.repository.id)[:18] try: while True: # used for repeating on overall checksum mismatch # id line input diff --git a/borg/repository.py b/borg/repository.py index fae22119a..fa6458c61 100644 --- a/borg/repository.py +++ b/borg/repository.py @@ -1,5 +1,5 @@ from configparser import ConfigParser -from binascii import hexlify, unhexlify +from binascii import unhexlify from datetime import datetime from itertools import islice import errno @@ -12,7 +12,7 @@ import struct from zlib import crc32 import msgpack -from .helpers import Error, ErrorWithTraceback, IntegrityError, Location, ProgressIndicatorPercent +from .helpers import Error, ErrorWithTraceback, IntegrityError, Location, ProgressIndicatorPercent, bin_to_hex from .hashindex import NSIndex from .locking import Lock, LockError, LockErrorT from .lrucache import LRUCache @@ -109,7 +109,7 @@ class Repository: config.set('repository', 'segments_per_dir', str(self.DEFAULT_SEGMENTS_PER_DIR)) config.set('repository', 'max_segment_size', str(self.DEFAULT_MAX_SEGMENT_SIZE)) config.set('repository', 'append_only', str(int(self.append_only))) - config.set('repository', 'id', hexlify(os.urandom(32)).decode('ascii')) + config.set('repository', 'id', bin_to_hex(os.urandom(32))) self.save_config(path, config) def save_config(self, path, config): diff --git a/borg/testsuite/archiver.py b/borg/testsuite/archiver.py index e7f805ed4..706319bd1 100644 --- a/borg/testsuite/archiver.py +++ b/borg/testsuite/archiver.py @@ -1,4 +1,4 @@ -from binascii import hexlify, unhexlify, b2a_base64 +from binascii import unhexlify, b2a_base64 from configparser import ConfigParser import errno import os @@ -21,7 +21,7 @@ from ..archive import Archive, ChunkBuffer, CHUNK_MAX_EXP, flags_noatime, flags_ from ..archiver import Archiver from ..cache import Cache from ..crypto import bytes_to_long, num_aes_blocks -from ..helpers import Manifest, PatternMatcher, parse_pattern, EXIT_SUCCESS, EXIT_WARNING, EXIT_ERROR +from ..helpers import Manifest, PatternMatcher, parse_pattern, EXIT_SUCCESS, EXIT_WARNING, EXIT_ERROR, bin_to_hex from ..key import RepoKey, KeyfileKey, Passphrase from ..keymanager import RepoIdMismatch, NotABorgKeyFile from ..remote import RemoteRepository, PathNotAllowed @@ -409,7 +409,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): def _set_repository_id(self, path, id): config = ConfigParser(interpolation=None) config.read(os.path.join(path, 'config')) - config.set('repository', 'id', hexlify(id).decode('ascii')) + config.set('repository', 'id', bin_to_hex(id)) with open(os.path.join(path, 'config'), 'w') as fd: config.write(fd) with Repository(self.repository_path) as repository: @@ -1205,7 +1205,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): with open(export_file, 'r') as fd: export_contents = fd.read() - assert export_contents.startswith('BORG_KEY ' + hexlify(repo_id).decode() + '\n') + assert export_contents.startswith('BORG_KEY ' + bin_to_hex(repo_id) + '\n') key_file = self.keys_path + '/' + os.listdir(self.keys_path)[0] @@ -1232,7 +1232,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): with open(export_file, 'r') as fd: export_contents = fd.read() - assert export_contents.startswith('BORG_KEY ' + hexlify(repo_id).decode() + '\n') + assert export_contents.startswith('BORG_KEY ' + bin_to_hex(repo_id) + '\n') with Repository(self.repository_path) as repository: repo_key = RepoKey(repository) diff --git a/borg/upgrader.py b/borg/upgrader.py index f4327e340..c6700262c 100644 --- a/borg/upgrader.py +++ b/borg/upgrader.py @@ -1,4 +1,3 @@ -from binascii import hexlify import datetime import logging logger = logging.getLogger(__name__) @@ -6,7 +5,7 @@ import os import shutil import time -from .helpers import get_keys_dir, get_cache_dir, ProgressIndicatorPercent +from .helpers import get_keys_dir, get_cache_dir, ProgressIndicatorPercent, bin_to_hex from .locking import Lock from .repository import Repository, MAGIC from .key import KeyfileKey, KeyfileNotFoundError @@ -188,8 +187,8 @@ class AtticRepositoryUpgrader(Repository): attic_cache_dir = os.environ.get('ATTIC_CACHE_DIR', os.path.join(os.path.expanduser('~'), '.cache', 'attic')) - attic_cache_dir = os.path.join(attic_cache_dir, hexlify(self.id).decode('ascii')) - borg_cache_dir = os.path.join(get_cache_dir(), hexlify(self.id).decode('ascii')) + attic_cache_dir = os.path.join(attic_cache_dir, bin_to_hex(self.id)) + borg_cache_dir = os.path.join(get_cache_dir(), bin_to_hex(self.id)) def copy_cache_file(path): """copy the given attic cache path into the borg directory @@ -263,7 +262,7 @@ class AtticKeyfileKey(KeyfileKey): assume the repository has been opened by the archiver yet """ get_keys_dir = cls.get_keys_dir - id = hexlify(repository.id).decode('ascii') + id = bin_to_hex(repository.id) keys_dir = get_keys_dir() if not os.path.exists(keys_dir): raise KeyfileNotFoundError(repository.path, keys_dir) @@ -313,7 +312,7 @@ class Borg0xxKeyfileKey(KeyfileKey): @classmethod def find_key_file(cls, repository): get_keys_dir = cls.get_keys_dir - id = hexlify(repository.id).decode('ascii') + id = bin_to_hex(repository.id) keys_dir = get_keys_dir() if not os.path.exists(keys_dir): raise KeyfileNotFoundError(repository.path, keys_dir) From 5f337e2c9c7015d17e7ba941e2ffa44d599a1a7e Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 6 Oct 2016 22:46:37 +0200 Subject: [PATCH 06/15] borg.key: include chunk id in exception msgs this is a backport of bcdce91dfb2883c139011322a9e8086059fbe5c2 improvements on the exception msgs. --- borg/key.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/borg/key.py b/borg/key.py index 648d2193b..7b65d9093 100644 --- a/borg/key.py +++ b/borg/key.py @@ -105,10 +105,10 @@ class PlaintextKey(KeyBase): def decrypt(self, id, data): if data[0] != self.TYPE: - raise IntegrityError('Invalid encryption envelope') + raise IntegrityError('Chunk %s: Invalid encryption envelope' % bin_to_hex(id)) data = self.compressor.decompress(memoryview(data)[1:]) if id and sha256(data).digest() != id: - raise IntegrityError('Chunk id verification failed') + raise IntegrityError('Chunk %s: id verification failed' % bin_to_hex(id)) return data @@ -142,24 +142,24 @@ class AESKeyBase(KeyBase): def decrypt(self, id, data): if not (data[0] == self.TYPE or data[0] == PassphraseKey.TYPE and isinstance(self, RepoKey)): - raise IntegrityError('Invalid encryption envelope') + raise IntegrityError('Chunk %s: Invalid encryption envelope' % bin_to_hex(id)) hmac_given = memoryview(data)[1:33] hmac_computed = memoryview(HMAC(self.enc_hmac_key, memoryview(data)[33:], sha256).digest()) if not compare_digest(hmac_computed, hmac_given): - raise IntegrityError('Encryption envelope checksum mismatch') + raise IntegrityError('Chunk %s: Encryption envelope checksum mismatch' % bin_to_hex(id)) self.dec_cipher.reset(iv=PREFIX + data[33:41]) data = self.compressor.decompress(self.dec_cipher.decrypt(data[41:])) if id: hmac_given = id hmac_computed = HMAC(self.id_key, data, sha256).digest() if not compare_digest(hmac_computed, hmac_given): - raise IntegrityError('Chunk id verification failed') + raise IntegrityError('Chunk %s: Chunk id verification failed' % bin_to_hex(id)) return data def extract_nonce(self, payload): if not (payload[0] == self.TYPE or payload[0] == PassphraseKey.TYPE and isinstance(self, RepoKey)): - raise IntegrityError('Invalid encryption envelope') + raise IntegrityError('Manifest: Invalid encryption envelope') nonce = bytes_to_long(payload[33:41]) return nonce From 75624f8e05a6b3e9edf68b0b9dbbb4823daaef01 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 6 Oct 2016 22:58:02 +0200 Subject: [PATCH 07/15] vagrant: update image name of boxcutter debian7 boxes debian711* is 404. --- Vagrantfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Vagrantfile b/Vagrantfile index 60f1b02a8..b6af51a23 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -385,7 +385,7 @@ Vagrant.configure(2) do |config| end config.vm.define "wheezy32" do |b| - b.vm.box = "boxcutter/debian711-i386" + b.vm.box = "boxcutter/debian7-i386" b.vm.provision "packages prepare wheezy", :type => :shell, :inline => packages_prepare_wheezy b.vm.provision "packages debianoid", :type => :shell, :inline => packages_debianoid b.vm.provision "install pyenv", :type => :shell, :privileged => false, :inline => install_pyenv("wheezy32") @@ -398,7 +398,7 @@ Vagrant.configure(2) do |config| end config.vm.define "wheezy64" do |b| - b.vm.box = "boxcutter/debian711" + b.vm.box = "boxcutter/debian7" b.vm.provision "packages prepare wheezy", :type => :shell, :inline => packages_prepare_wheezy b.vm.provision "packages debianoid", :type => :shell, :inline => packages_debianoid b.vm.provision "install pyenv", :type => :shell, :privileged => false, :inline => install_pyenv("wheezy64") From 7434010cdd4ad524c5ac181299f04faa051ba9ad Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 6 Oct 2016 23:36:22 +0200 Subject: [PATCH 08/15] IntegrityError: add placeholder for message, fixes #1572 So that the message we give appears not only in the traceback, but also in the (short) error message. --- borg/helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/borg/helpers.py b/borg/helpers.py index a452e17aa..f7bc54c36 100644 --- a/borg/helpers.py +++ b/borg/helpers.py @@ -64,7 +64,7 @@ class ErrorWithTraceback(Error): class IntegrityError(ErrorWithTraceback): - """Data integrity error""" + """Data integrity error: {}""" class ExtensionModuleError(Error): From a0df60e1b86d8e4b7efdb721090c5245524e01aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ketelaars?= Date: Sat, 8 Oct 2016 08:29:53 +0200 Subject: [PATCH 09/15] FUSE on OpenBSD MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Concerning #1696: OpenBSD offers in kernel support for FUSE 2.6. Borg relies on llfuse, which relies on FUSE >2.9. I'm not aware of plans to bring FUSE on OpenBSD to a more recent version. Signed-off-by: Björn Ketelaars --- Vagrantfile | 1 - 1 file changed, 1 deletion(-) diff --git a/Vagrantfile b/Vagrantfile index b6af51a23..7b09c375e 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -114,7 +114,6 @@ def packages_openbsd chsh -s /usr/local/bin/bash vagrant pkg_add openssl pkg_add lz4 - # pkg_add fuse # does not install, sdl dependency missing pkg_add git # no fakeroot pkg_add py3-setuptools ln -sf /usr/local/bin/python3.4 /usr/local/bin/python3 From f50068944d5a7840d2be6a4c53d854fc56372de8 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Mon, 10 Oct 2016 05:18:43 +0200 Subject: [PATCH 10/15] update changed repo location immediately after acceptance fixes #1524 before, if a longer backup got interrupted before commit(), it asked same question again. --- borg/cache.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/borg/cache.py b/borg/cache.py index e293d9e14..04c7ad5bb 100644 --- a/borg/cache.py +++ b/borg/cache.py @@ -77,6 +77,9 @@ class Cache: if not yes(msg, false_msg="Aborting.", invalid_msg="Invalid answer, aborting.", retry=False, env_var_override='BORG_RELOCATED_REPO_ACCESS_IS_OK'): raise self.RepositoryAccessAborted() + # adapt on-disk config immediately if the new location was accepted + self.begin_txn() + self.commit() if sync and self.manifest.id != self.manifest_id: # If repository is older than the cache something fishy is going on From 4fc5a35572680a995ffc54d4afa1254452e3bc01 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Mon, 10 Oct 2016 06:10:39 +0200 Subject: [PATCH 11/15] better messages for cache newer than repo, fixes #1700 --- borg/cache.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/borg/cache.py b/borg/cache.py index e293d9e14..2d3dba586 100644 --- a/borg/cache.py +++ b/borg/cache.py @@ -20,8 +20,11 @@ import msgpack 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 is newer than repository, refusing to continue""" + """Cache is newer than repository - this is either an attack or unsafe (multiple repos with same ID)""" class CacheInitAbortedError(Error): """Cache initialization aborted""" @@ -81,7 +84,10 @@ class Cache: if sync and self.manifest.id != self.manifest_id: # If repository is older than the cache something fishy is going on if self.timestamp and self.timestamp > manifest.timestamp: - raise self.RepositoryReplay() + if isinstance(key, PlaintextKey): + raise self.RepositoryIDNotUnique() + else: + raise self.RepositoryReplay() # Make sure an encrypted repository has not been swapped for an unencrypted repository if self.key_type is not None and self.key_type != str(key.TYPE): raise self.EncryptionMethodMismatch() From cf0359eba75a8233b248946c8c7f65480d08dee6 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Mon, 10 Oct 2016 19:16:35 +0200 Subject: [PATCH 12/15] allow pathes with colons, fixes #1705 also: - refactor / deduplicate the location parsing regexes - add comments - add more tests for Location parsing --- borg/helpers.py | 19 ++++++++++----- borg/testsuite/helpers.py | 49 ++++++++++++++++++++++++++++++--------- 2 files changed, 51 insertions(+), 17 deletions(-) diff --git a/borg/helpers.py b/borg/helpers.py index f7bc54c36..ef4b06874 100644 --- a/borg/helpers.py +++ b/borg/helpers.py @@ -768,20 +768,27 @@ class Location: """Object representing a repository / archive location """ proto = user = host = port = path = archive = None + # path may not contain :: (it ends at :: or string end), but may contain single colons. + # to avoid ambiguities with other regexes, it must also not start with ":". + path_re = r'(?!:)(?P([^:]|(:(?!:)))+)' + # optional ::archive_name at the end, archive name must not contain "/". # borg mount's FUSE filesystem creates one level of directories from - # the archive names. Thus, we must not accept "/" in archive names. + # the archive names and of course "/" is not valid in a directory name. + optional_archive_re = r'(?:::(?P[^/]+))?$' + # regexes for misc. kinds of supported location specifiers: ssh_re = re.compile(r'(?Pssh)://(?:(?P[^@]+)@)?' r'(?P[^:/#]+)(?::(?P\d+))?' - r'(?P[^:]+)(?:::(?P[^/]+))?$') + + path_re + optional_archive_re) file_re = re.compile(r'(?Pfile)://' - r'(?P[^:]+)(?:::(?P[^/]+))?$') + + path_re + optional_archive_re) + # note: scp_re is also use for local pathes scp_re = re.compile(r'((?:(?P[^@]+)@)?(?P[^:/]+):)?' - r'(?P[^:]+)(?:::(?P[^/]+))?$') - # get the repo from BORG_RE env and the optional archive from param. + + path_re + optional_archive_re) + # get the repo from BORG_REPO env and the optional archive from param. # if the syntax requires giving REPOSITORY (see "borg mount"), # use "::" to let it use the env var. # if REPOSITORY argument is optional, it'll automatically use the env. - env_re = re.compile(r'(?:::(?P[^/]+)?)?$') + env_re = re.compile(r'(?:::$)|' + optional_archive_re) def __init__(self, text=''): self.orig = text diff --git a/borg/testsuite/helpers.py b/borg/testsuite/helpers.py index cbe4cd9d6..8c9c20e52 100644 --- a/borg/testsuite/helpers.py +++ b/borg/testsuite/helpers.py @@ -39,6 +39,8 @@ class TestLocationWithoutEnv: "Location(proto='ssh', user='user', host='host', port=1234, path='/some/path', archive='archive')" assert repr(Location('ssh://user@host:1234/some/path')) == \ "Location(proto='ssh', user='user', host='host', port=1234, path='/some/path', archive=None)" + assert repr(Location('ssh://user@host/some/path')) == \ + "Location(proto='ssh', user='user', host='host', port=None, path='/some/path', archive=None)" def test_file(self, monkeypatch): monkeypatch.delenv('BORG_REPO', raising=False) @@ -75,6 +77,15 @@ class TestLocationWithoutEnv: assert repr(Location('some/relative/path')) == \ "Location(proto='file', user=None, host=None, port=None, path='some/relative/path', archive=None)" + def test_with_colons(self, monkeypatch): + monkeypatch.delenv('BORG_REPO', raising=False) + assert repr(Location('/abs/path:w:cols::arch:col')) == \ + "Location(proto='file', user=None, host=None, port=None, path='/abs/path:w:cols', archive='arch:col')" + assert repr(Location('/abs/path:with:colons::archive')) == \ + "Location(proto='file', user=None, host=None, port=None, path='/abs/path:with:colons', archive='archive')" + assert repr(Location('/abs/path:with:colons')) == \ + "Location(proto='file', user=None, host=None, port=None, path='/abs/path:with:colons', archive=None)" + def test_underspecified(self, monkeypatch): monkeypatch.delenv('BORG_REPO', raising=False) with pytest.raises(ValueError): @@ -84,11 +95,6 @@ class TestLocationWithoutEnv: with pytest.raises(ValueError): Location() - def test_no_double_colon(self, monkeypatch): - monkeypatch.delenv('BORG_REPO', raising=False) - with pytest.raises(ValueError): - Location('ssh://localhost:22/path:archive') - def test_no_slashes(self, monkeypatch): monkeypatch.delenv('BORG_REPO', raising=False) with pytest.raises(ValueError): @@ -119,43 +125,64 @@ class TestLocationWithEnv: monkeypatch.setenv('BORG_REPO', 'ssh://user@host:1234/some/path') assert repr(Location('::archive')) == \ "Location(proto='ssh', user='user', host='host', port=1234, path='/some/path', archive='archive')" - assert repr(Location()) == \ + assert repr(Location('::')) == \ "Location(proto='ssh', user='user', host='host', port=1234, path='/some/path', archive=None)" + assert repr(Location()) == \ + "Location(proto='ssh', user='user', host='host', port=1234, path='/some/path', archive=None)" def test_file(self, monkeypatch): monkeypatch.setenv('BORG_REPO', 'file:///some/path') assert repr(Location('::archive')) == \ "Location(proto='file', user=None, host=None, port=None, path='/some/path', archive='archive')" - assert repr(Location()) == \ + assert repr(Location('::')) == \ "Location(proto='file', user=None, host=None, port=None, path='/some/path', archive=None)" + assert repr(Location()) == \ + "Location(proto='file', user=None, host=None, port=None, path='/some/path', archive=None)" def test_scp(self, monkeypatch): monkeypatch.setenv('BORG_REPO', 'user@host:/some/path') assert repr(Location('::archive')) == \ "Location(proto='ssh', user='user', host='host', port=None, path='/some/path', archive='archive')" - assert repr(Location()) == \ + assert repr(Location('::')) == \ "Location(proto='ssh', user='user', host='host', port=None, path='/some/path', archive=None)" + assert repr(Location()) == \ + "Location(proto='ssh', user='user', host='host', port=None, path='/some/path', archive=None)" def test_folder(self, monkeypatch): monkeypatch.setenv('BORG_REPO', 'path') assert repr(Location('::archive')) == \ "Location(proto='file', user=None, host=None, port=None, path='path', archive='archive')" - assert repr(Location()) == \ + assert repr(Location('::')) == \ "Location(proto='file', user=None, host=None, port=None, path='path', archive=None)" + assert repr(Location()) == \ + "Location(proto='file', user=None, host=None, port=None, path='path', archive=None)" def test_abspath(self, monkeypatch): monkeypatch.setenv('BORG_REPO', '/some/absolute/path') assert repr(Location('::archive')) == \ "Location(proto='file', user=None, host=None, port=None, path='/some/absolute/path', archive='archive')" - assert repr(Location()) == \ + assert repr(Location('::')) == \ "Location(proto='file', user=None, host=None, port=None, path='/some/absolute/path', archive=None)" + assert repr(Location()) == \ + "Location(proto='file', user=None, host=None, port=None, path='/some/absolute/path', archive=None)" def test_relpath(self, monkeypatch): monkeypatch.setenv('BORG_REPO', 'some/relative/path') assert repr(Location('::archive')) == \ "Location(proto='file', user=None, host=None, port=None, path='some/relative/path', archive='archive')" - assert repr(Location()) == \ + assert repr(Location('::')) == \ "Location(proto='file', user=None, host=None, port=None, path='some/relative/path', archive=None)" + assert repr(Location()) == \ + "Location(proto='file', user=None, host=None, port=None, path='some/relative/path', archive=None)" + + def test_with_colons(self, monkeypatch): + monkeypatch.setenv('BORG_REPO', '/abs/path:w:cols') + assert repr(Location('::arch:col')) == \ + "Location(proto='file', user=None, host=None, port=None, path='/abs/path:w:cols', archive='arch:col')" + assert repr(Location('::')) == \ + "Location(proto='file', user=None, host=None, port=None, path='/abs/path:w:cols', archive=None)" + assert repr(Location()) == \ + "Location(proto='file', user=None, host=None, port=None, path='/abs/path:w:cols', archive=None)" def test_no_slashes(self, monkeypatch): monkeypatch.setenv('BORG_REPO', '/some/absolute/path') From e9ba14c6862bcf55be56660c78a9376a2ad959ff Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Mon, 10 Oct 2016 23:28:22 +0200 Subject: [PATCH 13/15] Location parsing regexes: use verbose REs just added whitespace and comments, no semantic changes --- borg/helpers.py | 44 +++++++++++++++++++++++++++++++++----------- 1 file changed, 33 insertions(+), 11 deletions(-) diff --git a/borg/helpers.py b/borg/helpers.py index ef4b06874..24a8231a4 100644 --- a/borg/helpers.py +++ b/borg/helpers.py @@ -768,27 +768,49 @@ class Location: """Object representing a repository / archive location """ proto = user = host = port = path = archive = None - # path may not contain :: (it ends at :: or string end), but may contain single colons. + + # path must not contain :: (it ends at :: or string end), but may contain single colons. # to avoid ambiguities with other regexes, it must also not start with ":". - path_re = r'(?!:)(?P([^:]|(:(?!:)))+)' + path_re = r""" + (?!:) # not starting with ":" + (?P([^:]|(:(?!:)))+) # any chars, but no "::" + """ # optional ::archive_name at the end, archive name must not contain "/". # borg mount's FUSE filesystem creates one level of directories from # the archive names and of course "/" is not valid in a directory name. - optional_archive_re = r'(?:::(?P[^/]+))?$' + optional_archive_re = r""" + (?: + :: # "::" as separator + (?P[^/]+) # archive name must not contain "/" + )?$""" # must match until the end + # regexes for misc. kinds of supported location specifiers: - ssh_re = re.compile(r'(?Pssh)://(?:(?P[^@]+)@)?' - r'(?P[^:/#]+)(?::(?P\d+))?' - + path_re + optional_archive_re) - file_re = re.compile(r'(?Pfile)://' - + path_re + optional_archive_re) + ssh_re = re.compile(r""" + (?Pssh):// # ssh:// + (?:(?P[^@]+)@)? # user@ (optional) + (?P[^:/#]+)(?::(?P\d+))? # host or host:port + """ + path_re + optional_archive_re, re.VERBOSE) # path or path::archive + + file_re = re.compile(r""" + (?Pfile):// # file:// + """ + path_re + optional_archive_re, re.VERBOSE) # path or path::archive + # note: scp_re is also use for local pathes - scp_re = re.compile(r'((?:(?P[^@]+)@)?(?P[^:/]+):)?' - + path_re + optional_archive_re) + scp_re = re.compile(r""" + ( + (?:(?P[^@]+)@)? # user@ (optional) + (?P[^:/]+): # host: (don't match / in host to disambiguate from file:) + )? # user@host: part is optional + """ + path_re + optional_archive_re, re.VERBOSE) # path with optional archive + # get the repo from BORG_REPO env and the optional archive from param. # if the syntax requires giving REPOSITORY (see "borg mount"), # use "::" to let it use the env var. # if REPOSITORY argument is optional, it'll automatically use the env. - env_re = re.compile(r'(?:::$)|' + optional_archive_re) + env_re = re.compile(r""" # the repo part is fetched from BORG_REPO + (?:::$) # just "::" is ok (when a pos. arg is required, no archive) + | # or + """ + optional_archive_re, re.VERBOSE) # archive name (optional, may be empty) def __init__(self, text=''): self.orig = text From 546c77f73d59413ce64a556d57981ceba2d5fadb Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Tue, 11 Oct 2016 00:12:25 +0200 Subject: [PATCH 14/15] ssh:// Location URL - remove not needed # nobody could make sense of it, so guess it is a mistake. --- borg/helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/borg/helpers.py b/borg/helpers.py index 24a8231a4..14caeec3c 100644 --- a/borg/helpers.py +++ b/borg/helpers.py @@ -788,7 +788,7 @@ class Location: ssh_re = re.compile(r""" (?Pssh):// # ssh:// (?:(?P[^@]+)@)? # user@ (optional) - (?P[^:/#]+)(?::(?P\d+))? # host or host:port + (?P[^:/]+)(?::(?P\d+))? # host or host:port """ + path_re + optional_archive_re, re.VERBOSE) # path or path::archive file_re = re.compile(r""" From 8d9475f704864b595abde6f5479bf95dbf250668 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 13 Oct 2016 03:53:26 +0200 Subject: [PATCH 15/15] fuse_mount contextmanager: accept any options not just the -o mount_options, but any options borg mount would take. simpler, more flexible. --- borg/testsuite/__init__.py | 6 ++---- borg/testsuite/archiver.py | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/borg/testsuite/__init__.py b/borg/testsuite/__init__.py index 64c240c2f..6b85538b4 100644 --- a/borg/testsuite/__init__.py +++ b/borg/testsuite/__init__.py @@ -94,11 +94,9 @@ class BaseTestCase(unittest.TestCase): self._assert_dirs_equal_cmp(sub_diff) @contextmanager - def fuse_mount(self, location, mountpoint, mount_options=None): + def fuse_mount(self, location, mountpoint, *options): os.mkdir(mountpoint) - args = ['mount', location, mountpoint] - if mount_options: - args += '-o', mount_options + args = ['mount', location, mountpoint] + list(options) self.cmd(*args, fork=True) self.wait_for_mount(mountpoint) yield diff --git a/borg/testsuite/archiver.py b/borg/testsuite/archiver.py index 706319bd1..8508639e0 100644 --- a/borg/testsuite/archiver.py +++ b/borg/testsuite/archiver.py @@ -1119,7 +1119,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): with pytest.raises(OSError) as excinfo: open(os.path.join(mountpoint, path)) assert excinfo.value.errno == errno.EIO - with self.fuse_mount(self.repository_location + '::archive', mountpoint, 'allow_damaged_files'): + with self.fuse_mount(self.repository_location + '::archive', mountpoint, '-o', 'allow_damaged_files'): open(os.path.join(mountpoint, path)).close() def verify_aes_counter_uniqueness(self, method):