mirror of
https://github.com/borgmatic-collective/borgmatic.git
synced 2026-07-25 03:13:02 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c5ffb76dfa | ||
|
|
61c7b8f13c | ||
|
|
3e8e38011b | ||
|
|
d0d3a39833 | ||
|
|
8bef1c698b | ||
|
|
acbbd6670a | ||
|
|
b336b9bedf | ||
|
|
ec9def4e71 | ||
|
|
a136fda92d | ||
|
|
b511e679ae | ||
|
|
f56fdab7a9 | ||
|
|
8c0eea7229 | ||
|
|
19e95628c3 | ||
|
|
4d01e53414 | ||
|
|
a082cb87cb | ||
|
|
1c51a8e229 | ||
|
|
d14a8df71a | ||
|
|
739a58fe47 | ||
|
|
af3431d6ae | ||
|
|
9851abc2e1 | ||
|
|
61ce6f0473 | ||
|
|
78e8bb6c8c | ||
|
|
af95134cd2 | ||
|
|
d6dfb8753a | ||
|
|
1bc003560a | ||
|
|
aeaf69f49e | ||
|
|
e83ad9e1e4 | ||
|
|
f42890430c | ||
|
|
6f300b0079 | ||
|
|
9bec029b4f | ||
|
|
08afad5d81 | ||
|
|
a01dc62468 | ||
|
|
8b61225b13 | ||
|
|
66d2f49f18 | ||
|
|
0a72c67c6c | ||
|
|
ab64b7ef67 | ||
|
|
1e3a3bf1e7 | ||
|
|
7a2f287918 | ||
|
|
8a63c49498 | ||
|
|
3b5ede8044 | ||
|
|
bd235f0426 | ||
|
|
09183464cd | ||
|
|
ca6fd6b061 | ||
|
|
dd9a64f4b6 | ||
|
|
23e7f27ee4 | ||
|
|
f9ef52f9a5 | ||
|
|
3f17c355ca | ||
|
|
c83fae5e5b | ||
|
|
39ad8f64c4 | ||
|
|
e86d223bbf | ||
|
|
86587ab2dc | ||
|
|
58c95d8015 | ||
|
|
6351747da5 | ||
|
|
55c153409e | ||
|
|
b115fb2fbe | ||
|
|
31d04d9ee3 | ||
|
|
f803836416 | ||
|
|
997f60b3e6 | ||
|
|
c84b26499b | ||
|
|
214ae81cbb | ||
|
|
d17b2c74db | ||
|
|
fb9677230b | ||
|
|
0db137efdf | ||
|
|
e6605c868d |
@@ -1,4 +1,5 @@
|
||||
const pluginSyntaxHighlight = require("@11ty/eleventy-plugin-syntaxhighlight");
|
||||
const codeClipboard = require("eleventy-plugin-code-clipboard");
|
||||
const inclusiveLangPlugin = require("@11ty/eleventy-plugin-inclusive-language");
|
||||
const navigationPlugin = require("@11ty/eleventy-navigation");
|
||||
|
||||
@@ -6,6 +7,7 @@ module.exports = function(eleventyConfig) {
|
||||
eleventyConfig.addPlugin(pluginSyntaxHighlight);
|
||||
eleventyConfig.addPlugin(inclusiveLangPlugin);
|
||||
eleventyConfig.addPlugin(navigationPlugin);
|
||||
eleventyConfig.addPlugin(codeClipboard);
|
||||
|
||||
let markdownIt = require("markdown-it");
|
||||
let markdownItAnchor = require("markdown-it-anchor");
|
||||
@@ -31,6 +33,7 @@ module.exports = function(eleventyConfig) {
|
||||
markdownIt(markdownItOptions)
|
||||
.use(markdownItAnchor, markdownItAnchorOptions)
|
||||
.use(markdownItReplaceLink)
|
||||
.use(codeClipboard.markdownItCopyButton)
|
||||
);
|
||||
|
||||
eleventyConfig.addPassthroughCopy({"docs/static": "static"});
|
||||
|
||||
@@ -1,3 +1,25 @@
|
||||
1.7.10
|
||||
* #396: When a database command errors, display and log the error message instead of swallowing it.
|
||||
* #501: Optionally error if a source directory does not exist via "source_directories_must_exist"
|
||||
option in borgmatic's location configuration.
|
||||
* #576: Add support for "file://" paths within "repositories" option.
|
||||
* #612: Define and use custom constants in borgmatic configuration files. See the documentation for
|
||||
more information:
|
||||
https://torsion.org/borgmatic/docs/how-to/make-per-application-backups/#constant-interpolation
|
||||
* #618: Add support for BORG_FILES_CACHE_TTL environment variable via "borg_files_cache_ttl" option
|
||||
in borgmatic's storage configuration.
|
||||
* #623: Fix confusing message when an error occurs running actions for a configuration file.
|
||||
* #635: Add optional repository labels so you can select a repository via "--repository yourlabel"
|
||||
at the command-line. See the configuration reference for more information:
|
||||
https://torsion.org/borgmatic/docs/reference/configuration/
|
||||
* #649: Add documentation on backing up a database running in a container:
|
||||
https://torsion.org/borgmatic/docs/how-to/backup-your-databases/#containers
|
||||
* #655: Fix error when databases are configured and a source directory doesn't exist.
|
||||
* Add code style plugins to enforce use of Python f-strings and prevent single-letter variables.
|
||||
To join in the pedantry, refresh your test environment with "tox --recreate".
|
||||
* Rename scripts/run-full-dev-tests to scripts/run-end-to-end-dev-tests and make it run end-to-end
|
||||
tests only. Continue using tox to run unit and integration tests.
|
||||
|
||||
1.7.9
|
||||
* #295: Add a SQLite database dump/restore hook.
|
||||
* #304: Change the default action order when no actions are specified on the command-line to:
|
||||
|
||||
@@ -24,9 +24,10 @@ location:
|
||||
|
||||
# Paths of local or remote repositories to backup to.
|
||||
repositories:
|
||||
- ssh://1234@usw-s001.rsync.net/./backups.borg
|
||||
- ssh://k8pDxu32@k8pDxu32.repo.borgbase.com/./repo
|
||||
- /var/lib/backups/local.borg
|
||||
- path: ssh://k8pDxu32@k8pDxu32.repo.borgbase.com/./repo
|
||||
label: borgbase
|
||||
- path: /var/lib/backups/local.borg
|
||||
label: local
|
||||
|
||||
retention:
|
||||
# Retention policy for how many backups to keep.
|
||||
|
||||
@@ -16,9 +16,9 @@ def run_borg(
|
||||
if borg_arguments.repository is None or borgmatic.config.validate.repositories_match(
|
||||
repository, borg_arguments.repository
|
||||
):
|
||||
logger.info('{}: Running arbitrary Borg command'.format(repository))
|
||||
logger.info(f'{repository["path"]}: Running arbitrary Borg command')
|
||||
archive_name = borgmatic.borg.rlist.resolve_archive_name(
|
||||
repository,
|
||||
repository['path'],
|
||||
borg_arguments.archive,
|
||||
storage,
|
||||
local_borg_version,
|
||||
@@ -26,7 +26,7 @@ def run_borg(
|
||||
remote_path,
|
||||
)
|
||||
borgmatic.borg.borg.run_arbitrary_borg(
|
||||
repository,
|
||||
repository['path'],
|
||||
storage,
|
||||
local_borg_version,
|
||||
options=borg_arguments.options,
|
||||
|
||||
@@ -15,7 +15,11 @@ def run_break_lock(
|
||||
if break_lock_arguments.repository is None or borgmatic.config.validate.repositories_match(
|
||||
repository, break_lock_arguments.repository
|
||||
):
|
||||
logger.info(f'{repository}: Breaking repository and cache locks')
|
||||
logger.info(f'{repository["path"]}: Breaking repository and cache locks')
|
||||
borgmatic.borg.break_lock.break_lock(
|
||||
repository, storage, local_borg_version, local_path=local_path, remote_path=remote_path,
|
||||
repository['path'],
|
||||
storage,
|
||||
local_borg_version,
|
||||
local_path=local_path,
|
||||
remote_path=remote_path,
|
||||
)
|
||||
|
||||
@@ -37,9 +37,9 @@ def run_check(
|
||||
global_arguments.dry_run,
|
||||
**hook_context,
|
||||
)
|
||||
logger.info('{}: Running consistency checks'.format(repository))
|
||||
logger.info(f'{repository["path"]}: Running consistency checks')
|
||||
borgmatic.borg.check.check_archives(
|
||||
repository,
|
||||
repository['path'],
|
||||
location,
|
||||
storage,
|
||||
consistency,
|
||||
|
||||
@@ -39,10 +39,10 @@ def run_compact(
|
||||
**hook_context,
|
||||
)
|
||||
if borgmatic.borg.feature.available(borgmatic.borg.feature.Feature.COMPACT, local_borg_version):
|
||||
logger.info('{}: Compacting segments{}'.format(repository, dry_run_label))
|
||||
logger.info(f'{repository["path"]}: Compacting segments{dry_run_label}')
|
||||
borgmatic.borg.compact.compact_segments(
|
||||
global_arguments.dry_run,
|
||||
repository,
|
||||
repository['path'],
|
||||
storage,
|
||||
local_borg_version,
|
||||
local_path=local_path,
|
||||
@@ -52,7 +52,7 @@ def run_compact(
|
||||
threshold=compact_arguments.threshold,
|
||||
)
|
||||
else: # pragma: nocover
|
||||
logger.info('{}: Skipping compact (only available/needed in Borg 1.2+)'.format(repository))
|
||||
logger.info(f'{repository["path"]}: Skipping compact (only available/needed in Borg 1.2+)')
|
||||
borgmatic.hooks.command.execute_hook(
|
||||
hooks.get('after_compact'),
|
||||
hooks.get('umask'),
|
||||
|
||||
@@ -42,11 +42,11 @@ def run_create(
|
||||
global_arguments.dry_run,
|
||||
**hook_context,
|
||||
)
|
||||
logger.info('{}: Creating archive{}'.format(repository, dry_run_label))
|
||||
logger.info(f'{repository["path"]}: Creating archive{dry_run_label}')
|
||||
borgmatic.hooks.dispatch.call_hooks_even_if_unconfigured(
|
||||
'remove_database_dumps',
|
||||
hooks,
|
||||
repository,
|
||||
repository['path'],
|
||||
borgmatic.hooks.dump.DATABASE_HOOK_NAMES,
|
||||
location,
|
||||
global_arguments.dry_run,
|
||||
@@ -54,7 +54,7 @@ def run_create(
|
||||
active_dumps = borgmatic.hooks.dispatch.call_hooks(
|
||||
'dump_databases',
|
||||
hooks,
|
||||
repository,
|
||||
repository['path'],
|
||||
borgmatic.hooks.dump.DATABASE_HOOK_NAMES,
|
||||
location,
|
||||
global_arguments.dry_run,
|
||||
@@ -63,7 +63,7 @@ def run_create(
|
||||
|
||||
json_output = borgmatic.borg.create.create_archive(
|
||||
global_arguments.dry_run,
|
||||
repository,
|
||||
repository['path'],
|
||||
location,
|
||||
storage,
|
||||
local_borg_version,
|
||||
|
||||
@@ -23,13 +23,13 @@ def run_export_tar(
|
||||
repository, export_tar_arguments.repository
|
||||
):
|
||||
logger.info(
|
||||
'{}: Exporting archive {} as tar file'.format(repository, export_tar_arguments.archive)
|
||||
f'{repository["path"]}: Exporting archive {export_tar_arguments.archive} as tar file'
|
||||
)
|
||||
borgmatic.borg.export_tar.export_tar_archive(
|
||||
global_arguments.dry_run,
|
||||
repository,
|
||||
repository['path'],
|
||||
borgmatic.borg.rlist.resolve_archive_name(
|
||||
repository,
|
||||
repository['path'],
|
||||
export_tar_arguments.archive,
|
||||
storage,
|
||||
local_borg_version,
|
||||
|
||||
@@ -35,12 +35,12 @@ def run_extract(
|
||||
if extract_arguments.repository is None or borgmatic.config.validate.repositories_match(
|
||||
repository, extract_arguments.repository
|
||||
):
|
||||
logger.info('{}: Extracting archive {}'.format(repository, extract_arguments.archive))
|
||||
logger.info(f'{repository["path"]}: Extracting archive {extract_arguments.archive}')
|
||||
borgmatic.borg.extract.extract_archive(
|
||||
global_arguments.dry_run,
|
||||
repository,
|
||||
repository['path'],
|
||||
borgmatic.borg.rlist.resolve_archive_name(
|
||||
repository,
|
||||
repository['path'],
|
||||
extract_arguments.archive,
|
||||
storage,
|
||||
local_borg_version,
|
||||
|
||||
@@ -20,9 +20,9 @@ def run_info(
|
||||
repository, info_arguments.repository
|
||||
):
|
||||
if not info_arguments.json: # pragma: nocover
|
||||
logger.answer(f'{repository}: Displaying archive summary information')
|
||||
logger.answer(f'{repository["path"]}: Displaying archive summary information')
|
||||
info_arguments.archive = borgmatic.borg.rlist.resolve_archive_name(
|
||||
repository,
|
||||
repository['path'],
|
||||
info_arguments.archive,
|
||||
storage,
|
||||
local_borg_version,
|
||||
@@ -30,7 +30,7 @@ def run_info(
|
||||
remote_path,
|
||||
)
|
||||
json_output = borgmatic.borg.info.display_archives_info(
|
||||
repository,
|
||||
repository['path'],
|
||||
storage,
|
||||
local_borg_version,
|
||||
info_arguments=info_arguments,
|
||||
|
||||
@@ -20,11 +20,11 @@ def run_list(
|
||||
):
|
||||
if not list_arguments.json: # pragma: nocover
|
||||
if list_arguments.find_paths:
|
||||
logger.answer(f'{repository}: Searching archives')
|
||||
logger.answer(f'{repository["path"]}: Searching archives')
|
||||
elif not list_arguments.archive:
|
||||
logger.answer(f'{repository}: Listing archives')
|
||||
logger.answer(f'{repository["path"]}: Listing archives')
|
||||
list_arguments.archive = borgmatic.borg.rlist.resolve_archive_name(
|
||||
repository,
|
||||
repository['path'],
|
||||
list_arguments.archive,
|
||||
storage,
|
||||
local_borg_version,
|
||||
@@ -32,7 +32,7 @@ def run_list(
|
||||
remote_path,
|
||||
)
|
||||
json_output = borgmatic.borg.list.list_archive(
|
||||
repository,
|
||||
repository['path'],
|
||||
storage,
|
||||
local_borg_version,
|
||||
list_arguments=list_arguments,
|
||||
|
||||
@@ -17,14 +17,14 @@ def run_mount(
|
||||
repository, mount_arguments.repository
|
||||
):
|
||||
if mount_arguments.archive:
|
||||
logger.info('{}: Mounting archive {}'.format(repository, mount_arguments.archive))
|
||||
logger.info(f'{repository["path"]}: Mounting archive {mount_arguments.archive}')
|
||||
else: # pragma: nocover
|
||||
logger.info('{}: Mounting repository'.format(repository))
|
||||
logger.info(f'{repository["path"]}: Mounting repository')
|
||||
|
||||
borgmatic.borg.mount.mount_archive(
|
||||
repository,
|
||||
repository['path'],
|
||||
borgmatic.borg.rlist.resolve_archive_name(
|
||||
repository,
|
||||
repository['path'],
|
||||
mount_arguments.archive,
|
||||
storage,
|
||||
local_borg_version,
|
||||
|
||||
@@ -37,10 +37,10 @@ def run_prune(
|
||||
global_arguments.dry_run,
|
||||
**hook_context,
|
||||
)
|
||||
logger.info('{}: Pruning archives{}'.format(repository, dry_run_label))
|
||||
logger.info(f'{repository["path"]}: Pruning archives{dry_run_label}')
|
||||
borgmatic.borg.prune.prune_archives(
|
||||
global_arguments.dry_run,
|
||||
repository,
|
||||
repository['path'],
|
||||
storage,
|
||||
retention,
|
||||
local_borg_version,
|
||||
|
||||
@@ -23,10 +23,10 @@ def run_rcreate(
|
||||
):
|
||||
return
|
||||
|
||||
logger.info('{}: Creating repository'.format(repository))
|
||||
logger.info(f'{repository["path"]}: Creating repository')
|
||||
borgmatic.borg.rcreate.create_repository(
|
||||
global_arguments.dry_run,
|
||||
repository,
|
||||
repository['path'],
|
||||
storage,
|
||||
local_borg_version,
|
||||
rcreate_arguments.encryption_mode,
|
||||
|
||||
@@ -256,22 +256,34 @@ def run_restore(
|
||||
return
|
||||
|
||||
logger.info(
|
||||
'{}: Restoring databases from archive {}'.format(repository, restore_arguments.archive)
|
||||
f'{repository["path"]}: Restoring databases from archive {restore_arguments.archive}'
|
||||
)
|
||||
|
||||
borgmatic.hooks.dispatch.call_hooks_even_if_unconfigured(
|
||||
'remove_database_dumps',
|
||||
hooks,
|
||||
repository,
|
||||
repository['path'],
|
||||
borgmatic.hooks.dump.DATABASE_HOOK_NAMES,
|
||||
location,
|
||||
global_arguments.dry_run,
|
||||
)
|
||||
|
||||
archive_name = borgmatic.borg.rlist.resolve_archive_name(
|
||||
repository, restore_arguments.archive, storage, local_borg_version, local_path, remote_path,
|
||||
repository['path'],
|
||||
restore_arguments.archive,
|
||||
storage,
|
||||
local_borg_version,
|
||||
local_path,
|
||||
remote_path,
|
||||
)
|
||||
archive_database_names = collect_archive_database_names(
|
||||
repository, archive_name, location, storage, local_borg_version, local_path, remote_path,
|
||||
repository['path'],
|
||||
archive_name,
|
||||
location,
|
||||
storage,
|
||||
local_borg_version,
|
||||
local_path,
|
||||
remote_path,
|
||||
)
|
||||
restore_names = find_databases_to_restore(restore_arguments.databases, archive_database_names)
|
||||
found_names = set()
|
||||
@@ -291,7 +303,7 @@ def run_restore(
|
||||
|
||||
found_names.add(database_name)
|
||||
restore_single_database(
|
||||
repository,
|
||||
repository['path'],
|
||||
location,
|
||||
storage,
|
||||
hooks,
|
||||
@@ -320,7 +332,7 @@ def run_restore(
|
||||
database['name'] = database_name
|
||||
|
||||
restore_single_database(
|
||||
repository,
|
||||
repository['path'],
|
||||
location,
|
||||
storage,
|
||||
hooks,
|
||||
@@ -336,7 +348,7 @@ def run_restore(
|
||||
borgmatic.hooks.dispatch.call_hooks_even_if_unconfigured(
|
||||
'remove_database_dumps',
|
||||
hooks,
|
||||
repository,
|
||||
repository['path'],
|
||||
borgmatic.hooks.dump.DATABASE_HOOK_NAMES,
|
||||
location,
|
||||
global_arguments.dry_run,
|
||||
|
||||
@@ -19,9 +19,10 @@ def run_rinfo(
|
||||
repository, rinfo_arguments.repository
|
||||
):
|
||||
if not rinfo_arguments.json: # pragma: nocover
|
||||
logger.answer('{}: Displaying repository summary information'.format(repository))
|
||||
logger.answer(f'{repository["path"]}: Displaying repository summary information')
|
||||
|
||||
json_output = borgmatic.borg.rinfo.display_repository_info(
|
||||
repository,
|
||||
repository['path'],
|
||||
storage,
|
||||
local_borg_version,
|
||||
rinfo_arguments=rinfo_arguments,
|
||||
|
||||
@@ -19,9 +19,10 @@ def run_rlist(
|
||||
repository, rlist_arguments.repository
|
||||
):
|
||||
if not rlist_arguments.json: # pragma: nocover
|
||||
logger.answer('{}: Listing repository'.format(repository))
|
||||
logger.answer(f'{repository["path"]}: Listing repository')
|
||||
|
||||
json_output = borgmatic.borg.rlist.list_repository(
|
||||
repository,
|
||||
repository['path'],
|
||||
storage,
|
||||
local_borg_version,
|
||||
rlist_arguments=rlist_arguments,
|
||||
|
||||
@@ -13,7 +13,7 @@ BORG_SUBCOMMANDS_WITHOUT_REPOSITORY = (('debug', 'info'), ('debug', 'convert-pro
|
||||
|
||||
|
||||
def run_arbitrary_borg(
|
||||
repository,
|
||||
repository_path,
|
||||
storage_config,
|
||||
local_borg_version,
|
||||
options,
|
||||
@@ -44,10 +44,10 @@ def run_arbitrary_borg(
|
||||
repository_archive_flags = ()
|
||||
elif archive:
|
||||
repository_archive_flags = flags.make_repository_archive_flags(
|
||||
repository, archive, local_borg_version
|
||||
repository_path, archive, local_borg_version
|
||||
)
|
||||
else:
|
||||
repository_archive_flags = flags.make_repository_flags(repository, local_borg_version)
|
||||
repository_archive_flags = flags.make_repository_flags(repository_path, local_borg_version)
|
||||
|
||||
full_command = (
|
||||
(local_path,)
|
||||
|
||||
@@ -7,7 +7,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def break_lock(
|
||||
repository, storage_config, local_borg_version, local_path='borg', remote_path=None,
|
||||
repository_path, storage_config, local_borg_version, local_path='borg', remote_path=None,
|
||||
):
|
||||
'''
|
||||
Given a local or remote repository path, a storage configuration dict, the local Borg version,
|
||||
@@ -24,7 +24,7 @@ def break_lock(
|
||||
+ (('--lock-wait', str(lock_wait)) if lock_wait else ())
|
||||
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
|
||||
+ (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
|
||||
+ flags.make_repository_flags(repository, local_borg_version)
|
||||
+ flags.make_repository_flags(repository_path, local_borg_version)
|
||||
)
|
||||
|
||||
borg_environment = environment.make_environment(storage_config)
|
||||
|
||||
@@ -12,7 +12,7 @@ DEFAULT_CHECKS = (
|
||||
{'name': 'repository', 'frequency': '1 month'},
|
||||
{'name': 'archives', 'frequency': '1 month'},
|
||||
)
|
||||
DEFAULT_PREFIX = '{hostname}-'
|
||||
DEFAULT_PREFIX = '{hostname}-' # noqa: FS003
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -196,7 +196,7 @@ def make_check_flags(local_borg_version, checks, check_last=None, prefix=None):
|
||||
return common_flags
|
||||
|
||||
return (
|
||||
tuple('--{}-only'.format(check) for check in checks if check in ('repository', 'archives'))
|
||||
tuple(f'--{check}-only' for check in checks if check in ('repository', 'archives'))
|
||||
+ common_flags
|
||||
)
|
||||
|
||||
@@ -243,7 +243,7 @@ def read_check_time(path):
|
||||
|
||||
|
||||
def check_archives(
|
||||
repository,
|
||||
repository_path,
|
||||
location_config,
|
||||
storage_config,
|
||||
consistency_config,
|
||||
@@ -268,7 +268,7 @@ def check_archives(
|
||||
try:
|
||||
borg_repository_id = json.loads(
|
||||
rinfo.display_repository_info(
|
||||
repository,
|
||||
repository_path,
|
||||
storage_config,
|
||||
local_borg_version,
|
||||
argparse.Namespace(json=True),
|
||||
@@ -277,7 +277,7 @@ def check_archives(
|
||||
)
|
||||
)['repository']['id']
|
||||
except (json.JSONDecodeError, KeyError):
|
||||
raise ValueError(f'Cannot determine Borg repository ID for {repository}')
|
||||
raise ValueError(f'Cannot determine Borg repository ID for {repository_path}')
|
||||
|
||||
checks = filter_checks_on_frequency(
|
||||
location_config,
|
||||
@@ -310,7 +310,7 @@ def check_archives(
|
||||
+ verbosity_flags
|
||||
+ (('--progress',) if progress else ())
|
||||
+ (tuple(extra_borg_options.split(' ')) if extra_borg_options else ())
|
||||
+ flags.make_repository_flags(repository, local_borg_version)
|
||||
+ flags.make_repository_flags(repository_path, local_borg_version)
|
||||
)
|
||||
|
||||
borg_environment = environment.make_environment(storage_config)
|
||||
@@ -329,6 +329,6 @@ def check_archives(
|
||||
|
||||
if 'extract' in checks:
|
||||
extract.extract_last_archive_dry_run(
|
||||
storage_config, local_borg_version, repository, lock_wait, local_path, remote_path
|
||||
storage_config, local_borg_version, repository_path, lock_wait, local_path, remote_path
|
||||
)
|
||||
write_check_time(make_check_time_path(location_config, borg_repository_id, 'extract'))
|
||||
|
||||
@@ -8,7 +8,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
def compact_segments(
|
||||
dry_run,
|
||||
repository,
|
||||
repository_path,
|
||||
storage_config,
|
||||
local_borg_version,
|
||||
local_path='borg',
|
||||
@@ -36,11 +36,11 @@ def compact_segments(
|
||||
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
|
||||
+ (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
|
||||
+ (tuple(extra_borg_options.split(' ')) if extra_borg_options else ())
|
||||
+ flags.make_repository_flags(repository, local_borg_version)
|
||||
+ flags.make_repository_flags(repository_path, local_borg_version)
|
||||
)
|
||||
|
||||
if dry_run:
|
||||
logging.info(f'{repository}: Skipping compact (dry run)')
|
||||
logging.info(f'{repository_path}: Skipping compact (dry run)')
|
||||
return
|
||||
|
||||
execute_command(
|
||||
|
||||
@@ -217,7 +217,7 @@ def make_list_filter_flags(local_borg_version, dry_run):
|
||||
return f'{base_flags}-'
|
||||
|
||||
|
||||
DEFAULT_ARCHIVE_NAME_FORMAT = '{hostname}-{now:%Y-%m-%dT%H:%M:%S.%f}'
|
||||
DEFAULT_ARCHIVE_NAME_FORMAT = '{hostname}-{now:%Y-%m-%dT%H:%M:%S.%f}' # noqa: FS003
|
||||
|
||||
|
||||
def collect_borgmatic_source_directories(borgmatic_source_directory):
|
||||
@@ -306,9 +306,23 @@ def collect_special_file_paths(
|
||||
)
|
||||
|
||||
|
||||
def check_all_source_directories_exist(source_directories):
|
||||
'''
|
||||
Given a sequence of source directories, check that they all exist. If any do not, raise an
|
||||
exception.
|
||||
'''
|
||||
missing_directories = [
|
||||
source_directory
|
||||
for source_directory in source_directories
|
||||
if not os.path.exists(source_directory)
|
||||
]
|
||||
if missing_directories:
|
||||
raise ValueError(f"Source directories do not exist: {', '.join(missing_directories)}")
|
||||
|
||||
|
||||
def create_archive(
|
||||
dry_run,
|
||||
repository,
|
||||
repository_path,
|
||||
location_config,
|
||||
storage_config,
|
||||
local_borg_version,
|
||||
@@ -331,6 +345,8 @@ def create_archive(
|
||||
borgmatic_source_directories = expand_directories(
|
||||
collect_borgmatic_source_directories(location_config.get('borgmatic_source_directory'))
|
||||
)
|
||||
if location_config.get('source_directories_must_exist', False):
|
||||
check_all_source_directories_exist(location_config.get('source_directories'))
|
||||
sources = deduplicate_directories(
|
||||
map_directories_to_devices(
|
||||
expand_directories(
|
||||
@@ -395,7 +411,7 @@ def create_archive(
|
||||
|
||||
if stream_processes and location_config.get('read_special') is False:
|
||||
logger.warning(
|
||||
f'{repository}: Ignoring configured "read_special" value of false, as true is needed for database hooks.'
|
||||
f'{repository_path}: Ignoring configured "read_special" value of false, as true is needed for database hooks.'
|
||||
)
|
||||
|
||||
create_command = (
|
||||
@@ -430,7 +446,9 @@ def create_archive(
|
||||
)
|
||||
+ (('--dry-run',) if dry_run else ())
|
||||
+ (tuple(extra_borg_options.split(' ')) if extra_borg_options else ())
|
||||
+ flags.make_repository_archive_flags(repository, archive_name_format, local_borg_version)
|
||||
+ flags.make_repository_archive_flags(
|
||||
repository_path, archive_name_format, local_borg_version
|
||||
)
|
||||
+ (sources if not pattern_file else ())
|
||||
)
|
||||
|
||||
@@ -450,7 +468,7 @@ def create_archive(
|
||||
# If database hooks are enabled (as indicated by streaming processes), exclude files that might
|
||||
# cause Borg to hang. But skip this if the user has explicitly set the "read_special" to True.
|
||||
if stream_processes and not location_config.get('read_special'):
|
||||
logger.debug(f'{repository}: Collecting special file paths')
|
||||
logger.debug(f'{repository_path}: Collecting special file paths')
|
||||
special_file_paths = collect_special_file_paths(
|
||||
create_command,
|
||||
local_path,
|
||||
@@ -461,7 +479,7 @@ def create_archive(
|
||||
|
||||
if special_file_paths:
|
||||
logger.warning(
|
||||
f'{repository}: Excluding special files to prevent Borg from hanging: {", ".join(special_file_paths)}'
|
||||
f'{repository_path}: Excluding special files to prevent Borg from hanging: {", ".join(special_file_paths)}'
|
||||
)
|
||||
exclude_file = write_pattern_file(
|
||||
expand_home_directories(
|
||||
|
||||
@@ -2,6 +2,7 @@ OPTION_TO_ENVIRONMENT_VARIABLE = {
|
||||
'borg_base_directory': 'BORG_BASE_DIR',
|
||||
'borg_config_directory': 'BORG_CONFIG_DIR',
|
||||
'borg_cache_directory': 'BORG_CACHE_DIR',
|
||||
'borg_files_cache_ttl': 'BORG_FILES_CACHE_TTL',
|
||||
'borg_security_directory': 'BORG_SECURITY_DIR',
|
||||
'borg_keys_directory': 'BORG_KEYS_DIR',
|
||||
'encryption_passcommand': 'BORG_PASSCOMMAND',
|
||||
@@ -27,7 +28,7 @@ def make_environment(storage_config):
|
||||
value = storage_config.get(option_name)
|
||||
|
||||
if value:
|
||||
environment[environment_variable_name] = value
|
||||
environment[environment_variable_name] = str(value)
|
||||
|
||||
for (
|
||||
option_name,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import logging
|
||||
import os
|
||||
|
||||
import borgmatic.logger
|
||||
from borgmatic.borg import environment, flags
|
||||
@@ -10,7 +9,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
def export_tar_archive(
|
||||
dry_run,
|
||||
repository,
|
||||
repository_path,
|
||||
archive,
|
||||
paths,
|
||||
destination_path,
|
||||
@@ -46,11 +45,7 @@ def export_tar_archive(
|
||||
+ (('--dry-run',) if dry_run else ())
|
||||
+ (('--tar-filter', tar_filter) if tar_filter else ())
|
||||
+ (('--strip-components', str(strip_components)) if strip_components else ())
|
||||
+ flags.make_repository_archive_flags(
|
||||
repository if ':' in repository else os.path.abspath(repository),
|
||||
archive,
|
||||
local_borg_version,
|
||||
)
|
||||
+ flags.make_repository_archive_flags(repository_path, archive, local_borg_version,)
|
||||
+ (destination_path,)
|
||||
+ (tuple(paths) if paths else ())
|
||||
)
|
||||
@@ -61,7 +56,7 @@ def export_tar_archive(
|
||||
output_log_level = logging.INFO
|
||||
|
||||
if dry_run:
|
||||
logging.info('{}: Skipping export to tar file (dry run)'.format(repository))
|
||||
logging.info(f'{repository_path}: Skipping export to tar file (dry run)')
|
||||
return
|
||||
|
||||
execute_command(
|
||||
|
||||
@@ -11,7 +11,7 @@ logger = logging.getLogger(__name__)
|
||||
def extract_last_archive_dry_run(
|
||||
storage_config,
|
||||
local_borg_version,
|
||||
repository,
|
||||
repository_path,
|
||||
lock_wait=None,
|
||||
local_path='borg',
|
||||
remote_path=None,
|
||||
@@ -30,7 +30,7 @@ def extract_last_archive_dry_run(
|
||||
|
||||
try:
|
||||
last_archive_name = rlist.resolve_archive_name(
|
||||
repository, 'latest', storage_config, local_borg_version, local_path, remote_path
|
||||
repository_path, 'latest', storage_config, local_borg_version, local_path, remote_path
|
||||
)
|
||||
except ValueError:
|
||||
logger.warning('No archives found. Skipping extract consistency check.')
|
||||
@@ -44,7 +44,9 @@ def extract_last_archive_dry_run(
|
||||
+ lock_wait_flags
|
||||
+ verbosity_flags
|
||||
+ list_flag
|
||||
+ flags.make_repository_archive_flags(repository, last_archive_name, local_borg_version)
|
||||
+ flags.make_repository_archive_flags(
|
||||
repository_path, last_archive_name, local_borg_version
|
||||
)
|
||||
)
|
||||
|
||||
execute_command(
|
||||
@@ -106,11 +108,7 @@ def extract_archive(
|
||||
+ (('--strip-components', str(strip_components)) if strip_components else ())
|
||||
+ (('--progress',) if progress else ())
|
||||
+ (('--stdout',) if extract_to_stdout else ())
|
||||
+ flags.make_repository_archive_flags(
|
||||
repository if ':' in repository else os.path.abspath(repository),
|
||||
archive,
|
||||
local_borg_version,
|
||||
)
|
||||
+ flags.make_repository_archive_flags(repository, archive, local_borg_version,)
|
||||
+ (tuple(paths) if paths else ())
|
||||
)
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ def make_flags(name, value):
|
||||
if not value:
|
||||
return ()
|
||||
|
||||
flag = '--{}'.format(name.replace('_', '-'))
|
||||
flag = f"--{name.replace('_', '-')}"
|
||||
|
||||
if value is True:
|
||||
return (flag,)
|
||||
@@ -33,7 +33,7 @@ def make_flags_from_arguments(arguments, excludes=()):
|
||||
)
|
||||
|
||||
|
||||
def make_repository_flags(repository, local_borg_version):
|
||||
def make_repository_flags(repository_path, local_borg_version):
|
||||
'''
|
||||
Given the path of a Borg repository and the local Borg version, return Borg-version-appropriate
|
||||
command-line flags (as a tuple) for selecting that repository.
|
||||
@@ -42,17 +42,17 @@ def make_repository_flags(repository, local_borg_version):
|
||||
('--repo',)
|
||||
if feature.available(feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, local_borg_version)
|
||||
else ()
|
||||
) + (repository,)
|
||||
) + (repository_path,)
|
||||
|
||||
|
||||
def make_repository_archive_flags(repository, archive, local_borg_version):
|
||||
def make_repository_archive_flags(repository_path, archive, local_borg_version):
|
||||
'''
|
||||
Given the path of a Borg repository, an archive name or pattern, and the local Borg version,
|
||||
return Borg-version-appropriate command-line flags (as a tuple) for selecting that repository
|
||||
and archive.
|
||||
'''
|
||||
return (
|
||||
('--repo', repository, archive)
|
||||
('--repo', repository_path, archive)
|
||||
if feature.available(feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, local_borg_version)
|
||||
else (f'{repository}::{archive}',)
|
||||
else (f'{repository_path}::{archive}',)
|
||||
)
|
||||
|
||||
@@ -8,7 +8,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def display_archives_info(
|
||||
repository,
|
||||
repository_path,
|
||||
storage_config,
|
||||
local_borg_version,
|
||||
info_arguments,
|
||||
@@ -49,7 +49,7 @@ def display_archives_info(
|
||||
+ flags.make_flags_from_arguments(
|
||||
info_arguments, excludes=('repository', 'archive', 'prefix')
|
||||
)
|
||||
+ flags.make_repository_flags(repository, local_borg_version)
|
||||
+ flags.make_repository_flags(repository_path, local_borg_version)
|
||||
+ (
|
||||
flags.make_flags('match-archives', info_arguments.archive)
|
||||
if feature.available(feature.Feature.MATCH_ARCHIVES, local_borg_version)
|
||||
|
||||
+19
-14
@@ -21,7 +21,7 @@ MAKE_FLAGS_EXCLUDES = (
|
||||
|
||||
|
||||
def make_list_command(
|
||||
repository,
|
||||
repository_path,
|
||||
storage_config,
|
||||
local_borg_version,
|
||||
list_arguments,
|
||||
@@ -52,10 +52,10 @@ def make_list_command(
|
||||
+ flags.make_flags_from_arguments(list_arguments, excludes=MAKE_FLAGS_EXCLUDES)
|
||||
+ (
|
||||
flags.make_repository_archive_flags(
|
||||
repository, list_arguments.archive, local_borg_version
|
||||
repository_path, list_arguments.archive, local_borg_version
|
||||
)
|
||||
if list_arguments.archive
|
||||
else flags.make_repository_flags(repository, local_borg_version)
|
||||
else flags.make_repository_flags(repository_path, local_borg_version)
|
||||
)
|
||||
+ (tuple(list_arguments.paths) if list_arguments.paths else ())
|
||||
)
|
||||
@@ -86,7 +86,7 @@ def make_find_paths(find_paths):
|
||||
|
||||
|
||||
def capture_archive_listing(
|
||||
repository,
|
||||
repository_path,
|
||||
archive,
|
||||
storage_config,
|
||||
local_borg_version,
|
||||
@@ -104,16 +104,16 @@ def capture_archive_listing(
|
||||
return tuple(
|
||||
execute_command_and_capture_output(
|
||||
make_list_command(
|
||||
repository,
|
||||
repository_path,
|
||||
storage_config,
|
||||
local_borg_version,
|
||||
argparse.Namespace(
|
||||
repository=repository,
|
||||
repository=repository_path,
|
||||
archive=archive,
|
||||
paths=[f'sh:{list_path}'],
|
||||
find_paths=None,
|
||||
json=None,
|
||||
format='{path}{NL}',
|
||||
format='{path}{NL}', # noqa: FS003
|
||||
),
|
||||
local_path,
|
||||
remote_path,
|
||||
@@ -126,7 +126,7 @@ def capture_archive_listing(
|
||||
|
||||
|
||||
def list_archive(
|
||||
repository,
|
||||
repository_path,
|
||||
storage_config,
|
||||
local_borg_version,
|
||||
list_arguments,
|
||||
@@ -149,7 +149,7 @@ def list_archive(
|
||||
)
|
||||
|
||||
rlist_arguments = argparse.Namespace(
|
||||
repository=repository,
|
||||
repository=repository_path,
|
||||
short=list_arguments.short,
|
||||
format=list_arguments.format,
|
||||
json=list_arguments.json,
|
||||
@@ -160,7 +160,12 @@ def list_archive(
|
||||
last=list_arguments.last,
|
||||
)
|
||||
return rlist.list_repository(
|
||||
repository, storage_config, local_borg_version, rlist_arguments, local_path, remote_path
|
||||
repository_path,
|
||||
storage_config,
|
||||
local_borg_version,
|
||||
rlist_arguments,
|
||||
local_path,
|
||||
remote_path,
|
||||
)
|
||||
|
||||
if list_arguments.archive:
|
||||
@@ -181,7 +186,7 @@ def list_archive(
|
||||
# getting a list of archives to search.
|
||||
if list_arguments.find_paths and not list_arguments.archive:
|
||||
rlist_arguments = argparse.Namespace(
|
||||
repository=repository,
|
||||
repository=repository_path,
|
||||
short=True,
|
||||
format=None,
|
||||
json=None,
|
||||
@@ -196,7 +201,7 @@ def list_archive(
|
||||
archive_lines = tuple(
|
||||
execute_command_and_capture_output(
|
||||
rlist.make_rlist_command(
|
||||
repository,
|
||||
repository_path,
|
||||
storage_config,
|
||||
local_borg_version,
|
||||
rlist_arguments,
|
||||
@@ -213,7 +218,7 @@ def list_archive(
|
||||
|
||||
# For each archive listed by Borg, run list on the contents of that archive.
|
||||
for archive in archive_lines:
|
||||
logger.answer(f'{repository}: Listing archive {archive}')
|
||||
logger.answer(f'{repository_path}: Listing archive {archive}')
|
||||
|
||||
archive_arguments = copy.copy(list_arguments)
|
||||
archive_arguments.archive = archive
|
||||
@@ -224,7 +229,7 @@ def list_archive(
|
||||
setattr(archive_arguments, name, None)
|
||||
|
||||
main_command = make_list_command(
|
||||
repository,
|
||||
repository_path,
|
||||
storage_config,
|
||||
local_borg_version,
|
||||
archive_arguments,
|
||||
|
||||
@@ -7,7 +7,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def mount_archive(
|
||||
repository,
|
||||
repository_path,
|
||||
archive,
|
||||
mount_point,
|
||||
paths,
|
||||
@@ -38,7 +38,7 @@ def mount_archive(
|
||||
+ (('-o', options) if options else ())
|
||||
+ (
|
||||
(
|
||||
flags.make_repository_flags(repository, local_borg_version)
|
||||
flags.make_repository_flags(repository_path, local_borg_version)
|
||||
+ (
|
||||
('--match-archives', archive)
|
||||
if feature.available(feature.Feature.MATCH_ARCHIVES, local_borg_version)
|
||||
@@ -47,9 +47,9 @@ def mount_archive(
|
||||
)
|
||||
if feature.available(feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, local_borg_version)
|
||||
else (
|
||||
flags.make_repository_archive_flags(repository, archive, local_borg_version)
|
||||
flags.make_repository_archive_flags(repository_path, archive, local_borg_version)
|
||||
if archive
|
||||
else flags.make_repository_flags(repository, local_borg_version)
|
||||
else flags.make_repository_flags(repository_path, local_borg_version)
|
||||
)
|
||||
)
|
||||
+ (mount_point,)
|
||||
|
||||
@@ -24,7 +24,7 @@ def make_prune_flags(retention_config, local_borg_version):
|
||||
)
|
||||
'''
|
||||
config = retention_config.copy()
|
||||
prefix = config.pop('prefix', '{hostname}-')
|
||||
prefix = config.pop('prefix', '{hostname}-') # noqa: FS003
|
||||
|
||||
if prefix:
|
||||
if feature.available(feature.Feature.MATCH_ARCHIVES, local_borg_version):
|
||||
@@ -39,7 +39,7 @@ def make_prune_flags(retention_config, local_borg_version):
|
||||
|
||||
def prune_archives(
|
||||
dry_run,
|
||||
repository,
|
||||
repository_path,
|
||||
storage_config,
|
||||
retention_config,
|
||||
local_borg_version,
|
||||
@@ -74,7 +74,7 @@ def prune_archives(
|
||||
+ (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
|
||||
+ (('--dry-run',) if dry_run else ())
|
||||
+ (tuple(extra_borg_options.split(' ')) if extra_borg_options else ())
|
||||
+ flags.make_repository_flags(repository, local_borg_version)
|
||||
+ flags.make_repository_flags(repository_path, local_borg_version)
|
||||
)
|
||||
|
||||
if stats or list_archives:
|
||||
|
||||
@@ -13,7 +13,7 @@ RINFO_REPOSITORY_NOT_FOUND_EXIT_CODE = 2
|
||||
|
||||
def create_repository(
|
||||
dry_run,
|
||||
repository,
|
||||
repository_path,
|
||||
storage_config,
|
||||
local_borg_version,
|
||||
encryption_mode,
|
||||
@@ -33,14 +33,14 @@ def create_repository(
|
||||
'''
|
||||
try:
|
||||
rinfo.display_repository_info(
|
||||
repository,
|
||||
repository_path,
|
||||
storage_config,
|
||||
local_borg_version,
|
||||
argparse.Namespace(json=True),
|
||||
local_path,
|
||||
remote_path,
|
||||
)
|
||||
logger.info(f'{repository}: Repository already exists. Skipping creation.')
|
||||
logger.info(f'{repository_path}: Repository already exists. Skipping creation.')
|
||||
return
|
||||
except subprocess.CalledProcessError as error:
|
||||
if error.returncode != RINFO_REPOSITORY_NOT_FOUND_EXIT_CODE:
|
||||
@@ -65,11 +65,11 @@ def create_repository(
|
||||
+ (('--debug',) if logger.isEnabledFor(logging.DEBUG) else ())
|
||||
+ (('--remote-path', remote_path) if remote_path else ())
|
||||
+ (tuple(extra_borg_options.split(' ')) if extra_borg_options else ())
|
||||
+ flags.make_repository_flags(repository, local_borg_version)
|
||||
+ flags.make_repository_flags(repository_path, local_borg_version)
|
||||
)
|
||||
|
||||
if dry_run:
|
||||
logging.info(f'{repository}: Skipping repository creation (dry run)')
|
||||
logging.info(f'{repository_path}: Skipping repository creation (dry run)')
|
||||
return
|
||||
|
||||
# Do not capture output here, so as to support interactive prompts.
|
||||
|
||||
@@ -8,7 +8,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def display_repository_info(
|
||||
repository,
|
||||
repository_path,
|
||||
storage_config,
|
||||
local_borg_version,
|
||||
rinfo_arguments,
|
||||
@@ -43,7 +43,7 @@ def display_repository_info(
|
||||
+ flags.make_flags('remote-path', remote_path)
|
||||
+ flags.make_flags('lock-wait', lock_wait)
|
||||
+ (('--json',) if rinfo_arguments.json else ())
|
||||
+ flags.make_repository_flags(repository, local_borg_version)
|
||||
+ flags.make_repository_flags(repository_path, local_borg_version)
|
||||
)
|
||||
|
||||
extra_environment = environment.make_environment(storage_config)
|
||||
|
||||
+18
-8
@@ -8,7 +8,12 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def resolve_archive_name(
|
||||
repository, archive, storage_config, local_borg_version, local_path='borg', remote_path=None
|
||||
repository_path,
|
||||
archive,
|
||||
storage_config,
|
||||
local_borg_version,
|
||||
local_path='borg',
|
||||
remote_path=None,
|
||||
):
|
||||
'''
|
||||
Given a local or remote repository path, an archive name, a storage config dict, a local Borg
|
||||
@@ -31,7 +36,7 @@ def resolve_archive_name(
|
||||
+ flags.make_flags('lock-wait', lock_wait)
|
||||
+ flags.make_flags('last', 1)
|
||||
+ ('--short',)
|
||||
+ flags.make_repository_flags(repository, local_borg_version)
|
||||
+ flags.make_repository_flags(repository_path, local_borg_version)
|
||||
)
|
||||
|
||||
output = execute_command_and_capture_output(
|
||||
@@ -42,7 +47,7 @@ def resolve_archive_name(
|
||||
except IndexError:
|
||||
raise ValueError('No archives found in the repository')
|
||||
|
||||
logger.debug('{}: Latest archive is {}'.format(repository, latest_archive))
|
||||
logger.debug(f'{repository_path}: Latest archive is {latest_archive}')
|
||||
|
||||
return latest_archive
|
||||
|
||||
@@ -51,7 +56,7 @@ MAKE_FLAGS_EXCLUDES = ('repository', 'prefix')
|
||||
|
||||
|
||||
def make_rlist_command(
|
||||
repository,
|
||||
repository_path,
|
||||
storage_config,
|
||||
local_borg_version,
|
||||
rlist_arguments,
|
||||
@@ -92,12 +97,12 @@ def make_rlist_command(
|
||||
else ()
|
||||
)
|
||||
+ flags.make_flags_from_arguments(rlist_arguments, excludes=MAKE_FLAGS_EXCLUDES)
|
||||
+ flags.make_repository_flags(repository, local_borg_version)
|
||||
+ flags.make_repository_flags(repository_path, local_borg_version)
|
||||
)
|
||||
|
||||
|
||||
def list_repository(
|
||||
repository,
|
||||
repository_path,
|
||||
storage_config,
|
||||
local_borg_version,
|
||||
rlist_arguments,
|
||||
@@ -113,11 +118,16 @@ def list_repository(
|
||||
borg_environment = environment.make_environment(storage_config)
|
||||
|
||||
main_command = make_rlist_command(
|
||||
repository, storage_config, local_borg_version, rlist_arguments, local_path, remote_path
|
||||
repository_path,
|
||||
storage_config,
|
||||
local_borg_version,
|
||||
rlist_arguments,
|
||||
local_path,
|
||||
remote_path,
|
||||
)
|
||||
|
||||
if rlist_arguments.json:
|
||||
return execute_command_and_capture_output(main_command, extra_environment=borg_environment,)
|
||||
return execute_command_and_capture_output(main_command, extra_environment=borg_environment)
|
||||
else:
|
||||
execute_command(
|
||||
main_command,
|
||||
|
||||
@@ -9,7 +9,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
def transfer_archives(
|
||||
dry_run,
|
||||
repository,
|
||||
repository_path,
|
||||
storage_config,
|
||||
local_borg_version,
|
||||
transfer_arguments,
|
||||
@@ -38,7 +38,7 @@ def transfer_archives(
|
||||
transfer_arguments,
|
||||
excludes=('repository', 'source_repository', 'archive', 'match_archives'),
|
||||
)
|
||||
+ flags.make_repository_flags(repository, local_borg_version)
|
||||
+ flags.make_repository_flags(repository_path, local_borg_version)
|
||||
+ flags.make_flags('other-repo', transfer_arguments.source_repository)
|
||||
+ flags.make_flags('dry-run', dry_run)
|
||||
)
|
||||
|
||||
@@ -131,9 +131,7 @@ def make_parsers():
|
||||
nargs='*',
|
||||
dest='config_paths',
|
||||
default=config_paths,
|
||||
help='Configuration filenames or directories, defaults to: {}'.format(
|
||||
' '.join(unexpanded_config_paths)
|
||||
),
|
||||
help=f"Configuration filenames or directories, defaults to: {' '.join(unexpanded_config_paths)}",
|
||||
)
|
||||
global_group.add_argument(
|
||||
'--excludes',
|
||||
@@ -225,7 +223,7 @@ def make_parsers():
|
||||
subparsers = top_level_parser.add_subparsers(
|
||||
title='actions',
|
||||
metavar='',
|
||||
help='Specify zero or more actions. Defaults to creat, prune, compact, and check. Use --help with action for details:',
|
||||
help='Specify zero or more actions. Defaults to create, prune, compact, and check. Use --help with action for details:',
|
||||
)
|
||||
rcreate_parser = subparsers.add_parser(
|
||||
'rcreate',
|
||||
|
||||
@@ -70,9 +70,7 @@ def run_configuration(config_filename, config, arguments):
|
||||
try:
|
||||
local_borg_version = borg_version.local_borg_version(storage, local_path)
|
||||
except (OSError, CalledProcessError, ValueError) as error:
|
||||
yield from log_error_records(
|
||||
'{}: Error getting local Borg version'.format(config_filename), error
|
||||
)
|
||||
yield from log_error_records(f'{config_filename}: Error getting local Borg version', error)
|
||||
return
|
||||
|
||||
try:
|
||||
@@ -100,7 +98,7 @@ def run_configuration(config_filename, config, arguments):
|
||||
return
|
||||
|
||||
encountered_error = error
|
||||
yield from log_error_records('{}: Error pinging monitor'.format(config_filename), error)
|
||||
yield from log_error_records(f'{config_filename}: Error pinging monitor', error)
|
||||
|
||||
if not encountered_error:
|
||||
repo_queue = Queue()
|
||||
@@ -108,7 +106,8 @@ def run_configuration(config_filename, config, arguments):
|
||||
repo_queue.put((repo, 0),)
|
||||
|
||||
while not repo_queue.empty():
|
||||
repository_path, retry_num = repo_queue.get()
|
||||
repository, retry_num = repo_queue.get()
|
||||
logger.debug(f'{repository["path"]}: Running actions for repository')
|
||||
timeout = retry_num * retry_wait
|
||||
if timeout:
|
||||
logger.warning(f'{config_filename}: Sleeping {timeout}s before next retry')
|
||||
@@ -125,14 +124,14 @@ def run_configuration(config_filename, config, arguments):
|
||||
local_path=local_path,
|
||||
remote_path=remote_path,
|
||||
local_borg_version=local_borg_version,
|
||||
repository_path=repository_path,
|
||||
repository=repository,
|
||||
)
|
||||
except (OSError, CalledProcessError, ValueError) as error:
|
||||
if retry_num < retries:
|
||||
repo_queue.put((repository_path, retry_num + 1),)
|
||||
repo_queue.put((repository, retry_num + 1),)
|
||||
tuple( # Consume the generator so as to trigger logging.
|
||||
log_error_records(
|
||||
'{}: Error running actions for repository'.format(repository_path),
|
||||
f'{repository["path"]}: Error running actions for repository',
|
||||
error,
|
||||
levelno=logging.WARNING,
|
||||
log_command_error_output=True,
|
||||
@@ -147,10 +146,10 @@ def run_configuration(config_filename, config, arguments):
|
||||
return
|
||||
|
||||
yield from log_error_records(
|
||||
'{}: Error running actions for repository'.format(repository_path), error
|
||||
f'{repository["path"]}: Error running actions for repository', error
|
||||
)
|
||||
encountered_error = error
|
||||
error_repository = repository_path
|
||||
error_repository = repository['path']
|
||||
|
||||
try:
|
||||
if using_primary_action:
|
||||
@@ -169,7 +168,7 @@ def run_configuration(config_filename, config, arguments):
|
||||
return
|
||||
|
||||
encountered_error = error
|
||||
yield from log_error_records('{}: Error pinging monitor'.format(config_filename), error)
|
||||
yield from log_error_records(f'{repository["path"]}: Error pinging monitor', error)
|
||||
|
||||
if not encountered_error:
|
||||
try:
|
||||
@@ -196,7 +195,7 @@ def run_configuration(config_filename, config, arguments):
|
||||
return
|
||||
|
||||
encountered_error = error
|
||||
yield from log_error_records('{}: Error pinging monitor'.format(config_filename), error)
|
||||
yield from log_error_records(f'{config_filename}: Error pinging monitor', error)
|
||||
|
||||
if encountered_error and using_primary_action:
|
||||
try:
|
||||
@@ -231,9 +230,7 @@ def run_configuration(config_filename, config, arguments):
|
||||
if command.considered_soft_failure(config_filename, error):
|
||||
return
|
||||
|
||||
yield from log_error_records(
|
||||
'{}: Error running on-error hook'.format(config_filename), error
|
||||
)
|
||||
yield from log_error_records(f'{config_filename}: Error running on-error hook', error)
|
||||
|
||||
|
||||
def run_actions(
|
||||
@@ -248,7 +245,7 @@ def run_actions(
|
||||
local_path,
|
||||
remote_path,
|
||||
local_borg_version,
|
||||
repository_path,
|
||||
repository,
|
||||
):
|
||||
'''
|
||||
Given parsed command-line arguments as an argparse.ArgumentParser instance, the configuration
|
||||
@@ -263,13 +260,13 @@ def run_actions(
|
||||
invalid.
|
||||
'''
|
||||
add_custom_log_levels()
|
||||
repository = os.path.expanduser(repository_path)
|
||||
repository_path = os.path.expanduser(repository['path'])
|
||||
global_arguments = arguments['global']
|
||||
dry_run_label = ' (dry run; not making any changes)' if global_arguments.dry_run else ''
|
||||
hook_context = {
|
||||
'repository': repository_path,
|
||||
# Deprecated: For backwards compatibility with borgmatic < 1.6.0.
|
||||
'repositories': ','.join(location['repositories']),
|
||||
'repositories': ','.join([repo['path'] for repo in location['repositories']]),
|
||||
}
|
||||
|
||||
command.execute_hook(
|
||||
@@ -472,9 +469,7 @@ def load_configurations(config_filenames, overrides=None, resolve_env=True):
|
||||
dict(
|
||||
levelno=logging.WARNING,
|
||||
levelname='WARNING',
|
||||
msg='{}: Insufficient permissions to read configuration file'.format(
|
||||
config_filename
|
||||
),
|
||||
msg=f'{config_filename}: Insufficient permissions to read configuration file',
|
||||
)
|
||||
),
|
||||
]
|
||||
@@ -486,7 +481,7 @@ def load_configurations(config_filenames, overrides=None, resolve_env=True):
|
||||
dict(
|
||||
levelno=logging.CRITICAL,
|
||||
levelname='CRITICAL',
|
||||
msg='{}: Error parsing configuration file'.format(config_filename),
|
||||
msg=f'{config_filename}: Error parsing configuration file',
|
||||
)
|
||||
),
|
||||
logging.makeLogRecord(
|
||||
@@ -587,9 +582,7 @@ def collect_configuration_run_summary_logs(configs, arguments):
|
||||
|
||||
if not configs:
|
||||
yield from log_error_records(
|
||||
'{}: No valid configuration files found'.format(
|
||||
' '.join(arguments['global'].config_paths)
|
||||
)
|
||||
f"{' '.join(arguments['global'].config_paths)}: No valid configuration files found",
|
||||
)
|
||||
return
|
||||
|
||||
@@ -615,23 +608,21 @@ def collect_configuration_run_summary_logs(configs, arguments):
|
||||
error_logs = tuple(result for result in results if isinstance(result, logging.LogRecord))
|
||||
|
||||
if error_logs:
|
||||
yield from log_error_records(
|
||||
'{}: Error running configuration file'.format(config_filename)
|
||||
)
|
||||
yield from log_error_records(f'{config_filename}: An error occurred')
|
||||
yield from error_logs
|
||||
else:
|
||||
yield logging.makeLogRecord(
|
||||
dict(
|
||||
levelno=logging.INFO,
|
||||
levelname='INFO',
|
||||
msg='{}: Successfully ran configuration file'.format(config_filename),
|
||||
msg=f'{config_filename}: Successfully ran configuration file',
|
||||
)
|
||||
)
|
||||
if results:
|
||||
json_results.extend(results)
|
||||
|
||||
if 'umount' in arguments:
|
||||
logger.info('Unmounting mount point {}'.format(arguments['umount'].mount_point))
|
||||
logger.info(f"Unmounting mount point {arguments['umount'].mount_point}")
|
||||
try:
|
||||
borg_umount.unmount_archive(
|
||||
mount_point=arguments['umount'].mount_point, local_path=get_local_path(configs),
|
||||
@@ -679,7 +670,7 @@ def main(): # pragma: no cover
|
||||
if error.code == 0:
|
||||
raise error
|
||||
configure_logging(logging.CRITICAL)
|
||||
logger.critical('Error parsing arguments: {}'.format(' '.join(sys.argv)))
|
||||
logger.critical(f"Error parsing arguments: {' '.join(sys.argv)}")
|
||||
exit_with_help_link()
|
||||
|
||||
global_arguments = arguments['global']
|
||||
@@ -712,7 +703,7 @@ def main(): # pragma: no cover
|
||||
)
|
||||
except (FileNotFoundError, PermissionError) as error:
|
||||
configure_logging(logging.CRITICAL)
|
||||
logger.critical('Error configuring logging: {}'.format(error))
|
||||
logger.critical(f'Error configuring logging: {error}')
|
||||
exit_with_help_link()
|
||||
|
||||
logger.debug('Ensuring legacy configuration is upgraded')
|
||||
|
||||
@@ -34,7 +34,7 @@ def bash_completion():
|
||||
' local this_script="$(cat "$BASH_SOURCE" 2> /dev/null)"',
|
||||
' local installed_script="$(borgmatic --bash-completion 2> /dev/null)"',
|
||||
' if [ "$this_script" != "$installed_script" ] && [ "$installed_script" != "" ];'
|
||||
' then cat << EOF\n%s\nEOF' % UPGRADE_MESSAGE,
|
||||
f' then cat << EOF\n{UPGRADE_MESSAGE}\nEOF',
|
||||
' fi',
|
||||
'}',
|
||||
'complete_borgmatic() {',
|
||||
@@ -48,7 +48,7 @@ def bash_completion():
|
||||
for action, subparser in subparsers.choices.items()
|
||||
)
|
||||
+ (
|
||||
' COMPREPLY=($(compgen -W "%s %s" -- "${COMP_WORDS[COMP_CWORD]}"))'
|
||||
' COMPREPLY=($(compgen -W "%s %s" -- "${COMP_WORDS[COMP_CWORD]}"))' # noqa: FS003
|
||||
% (actions, global_flags),
|
||||
' (check_version &)',
|
||||
'}',
|
||||
|
||||
@@ -28,9 +28,7 @@ def parse_arguments(*arguments):
|
||||
'--source-config',
|
||||
dest='source_config_filename',
|
||||
default=DEFAULT_SOURCE_CONFIG_FILENAME,
|
||||
help='Source INI-style configuration filename. Default: {}'.format(
|
||||
DEFAULT_SOURCE_CONFIG_FILENAME
|
||||
),
|
||||
help=f'Source INI-style configuration filename. Default: {DEFAULT_SOURCE_CONFIG_FILENAME}',
|
||||
)
|
||||
parser.add_argument(
|
||||
'-e',
|
||||
@@ -46,9 +44,7 @@ def parse_arguments(*arguments):
|
||||
'--destination-config',
|
||||
dest='destination_config_filename',
|
||||
default=DEFAULT_DESTINATION_CONFIG_FILENAME,
|
||||
help='Destination YAML configuration filename. Default: {}'.format(
|
||||
DEFAULT_DESTINATION_CONFIG_FILENAME
|
||||
),
|
||||
help=f'Destination YAML configuration filename. Default: {DEFAULT_DESTINATION_CONFIG_FILENAME}',
|
||||
)
|
||||
|
||||
return parser.parse_args(arguments)
|
||||
@@ -59,19 +55,15 @@ TEXT_WRAP_CHARACTERS = 80
|
||||
|
||||
def display_result(args): # pragma: no cover
|
||||
result_lines = textwrap.wrap(
|
||||
'Your borgmatic configuration has been upgraded. Please review the result in {}.'.format(
|
||||
args.destination_config_filename
|
||||
),
|
||||
f'Your borgmatic configuration has been upgraded. Please review the result in {args.destination_config_filename}.',
|
||||
TEXT_WRAP_CHARACTERS,
|
||||
)
|
||||
|
||||
excludes_phrase = (
|
||||
f' and {args.source_excludes_filename}' if args.source_excludes_filename else ''
|
||||
)
|
||||
delete_lines = textwrap.wrap(
|
||||
'Once you are satisfied, you can safely delete {}{}.'.format(
|
||||
args.source_config_filename,
|
||||
' and {}'.format(args.source_excludes_filename)
|
||||
if args.source_excludes_filename
|
||||
else '',
|
||||
),
|
||||
f'Once you are satisfied, you can safely delete {args.source_config_filename}{excludes_phrase}.',
|
||||
TEXT_WRAP_CHARACTERS,
|
||||
)
|
||||
|
||||
|
||||
@@ -23,9 +23,7 @@ def parse_arguments(*arguments):
|
||||
'--destination',
|
||||
dest='destination_filename',
|
||||
default=DEFAULT_DESTINATION_CONFIG_FILENAME,
|
||||
help='Destination YAML configuration file, default: {}'.format(
|
||||
DEFAULT_DESTINATION_CONFIG_FILENAME
|
||||
),
|
||||
help=f'Destination YAML configuration file, default: {DEFAULT_DESTINATION_CONFIG_FILENAME}',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--overwrite',
|
||||
@@ -48,17 +46,13 @@ def main(): # pragma: no cover
|
||||
overwrite=args.overwrite,
|
||||
)
|
||||
|
||||
print('Generated a sample configuration file at {}.'.format(args.destination_filename))
|
||||
print(f'Generated a sample configuration file at {args.destination_filename}.')
|
||||
print()
|
||||
if args.source_filename:
|
||||
print(
|
||||
'Merged in the contents of configuration file at {}.'.format(args.source_filename)
|
||||
)
|
||||
print(f'Merged in the contents of configuration file at {args.source_filename}.')
|
||||
print('To review the changes made, run:')
|
||||
print()
|
||||
print(
|
||||
' diff --unified {} {}'.format(args.source_filename, args.destination_filename)
|
||||
)
|
||||
print(f' diff --unified {args.source_filename} {args.destination_filename}')
|
||||
print()
|
||||
print('This includes all available configuration options with example values. The few')
|
||||
print('required options are indicated. Please edit the file to suit your needs.')
|
||||
|
||||
@@ -21,9 +21,7 @@ def parse_arguments(*arguments):
|
||||
nargs='+',
|
||||
dest='config_paths',
|
||||
default=config_paths,
|
||||
help='Configuration filenames or directories, defaults to: {}'.format(
|
||||
' '.join(config_paths)
|
||||
),
|
||||
help=f'Configuration filenames or directories, defaults to: {config_paths}',
|
||||
)
|
||||
|
||||
return parser.parse_args(arguments)
|
||||
@@ -44,13 +42,11 @@ def main(): # pragma: no cover
|
||||
try:
|
||||
validate.parse_configuration(config_filename, validate.schema_filename())
|
||||
except (ValueError, OSError, validate.Validation_error) as error:
|
||||
logging.critical('{}: Error parsing configuration file'.format(config_filename))
|
||||
logging.critical(f'{config_filename}: Error parsing configuration file')
|
||||
logging.critical(error)
|
||||
found_issues = True
|
||||
|
||||
if found_issues:
|
||||
sys.exit(1)
|
||||
else:
|
||||
logger.info(
|
||||
'All given configuration files are valid: {}'.format(', '.join(config_filenames))
|
||||
)
|
||||
logger.info(f"All given configuration files are valid: {', '.join(config_filenames)}")
|
||||
|
||||
@@ -16,8 +16,8 @@ def get_default_config_paths(expand_home=True):
|
||||
return [
|
||||
'/etc/borgmatic/config.yaml',
|
||||
'/etc/borgmatic.d',
|
||||
'%s/borgmatic/config.yaml' % user_config_directory,
|
||||
'%s/borgmatic.d' % user_config_directory,
|
||||
os.path.join(user_config_directory, 'borgmatic/config.yaml'),
|
||||
os.path.join(user_config_directory, 'borgmatic.d'),
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -14,11 +14,14 @@ def _resolve_string(matcher):
|
||||
if matcher.group('escape') is not None:
|
||||
# in case of escaped envvar, unescape it
|
||||
return matcher.group('variable')
|
||||
|
||||
# resolve the env var
|
||||
name, default = matcher.group('name'), matcher.group('default')
|
||||
out = os.getenv(name, default=default)
|
||||
|
||||
if out is None:
|
||||
raise ValueError('Cannot find variable ${name} in environment'.format(name=name))
|
||||
raise ValueError(f'Cannot find variable {name} in environment')
|
||||
|
||||
return out
|
||||
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ def _schema_to_sample_configuration(schema, level=0, parent_is_sequence=False):
|
||||
config, schema, indent=indent, skip_first=parent_is_sequence
|
||||
)
|
||||
else:
|
||||
raise ValueError('Schema at level {} is unsupported: {}'.format(level, schema))
|
||||
raise ValueError(f'Schema at level {level} is unsupported: {schema}')
|
||||
|
||||
return config
|
||||
|
||||
@@ -84,7 +84,7 @@ def _comment_out_optional_configuration(rendered_config):
|
||||
for line in rendered_config.split('\n'):
|
||||
# Upon encountering an optional configuration option, comment out lines until the next blank
|
||||
# line.
|
||||
if line.strip().startswith('# {}'.format(COMMENTED_OUT_SENTINEL)):
|
||||
if line.strip().startswith(f'# {COMMENTED_OUT_SENTINEL}'):
|
||||
optional = True
|
||||
continue
|
||||
|
||||
@@ -117,9 +117,7 @@ def write_configuration(config_filename, rendered_config, mode=0o600, overwrite=
|
||||
'''
|
||||
if not overwrite and os.path.exists(config_filename):
|
||||
raise FileExistsError(
|
||||
'{} already exists. Aborting. Use --overwrite to replace the file.'.format(
|
||||
config_filename
|
||||
)
|
||||
f'{config_filename} already exists. Aborting. Use --overwrite to replace the file.'
|
||||
)
|
||||
|
||||
try:
|
||||
@@ -218,7 +216,7 @@ def remove_commented_out_sentinel(config, field_name):
|
||||
except KeyError:
|
||||
return
|
||||
|
||||
if last_comment_value == '# {}\n'.format(COMMENTED_OUT_SENTINEL):
|
||||
if last_comment_value == f'# {COMMENTED_OUT_SENTINEL}\n':
|
||||
config.ca.items[field_name][RUAMEL_YAML_COMMENTS_INDEX].pop()
|
||||
|
||||
|
||||
|
||||
@@ -70,13 +70,11 @@ def validate_configuration_format(parser, config_format):
|
||||
section_format.name for section_format in config_format
|
||||
)
|
||||
if unknown_section_names:
|
||||
raise ValueError(
|
||||
'Unknown config sections found: {}'.format(', '.join(unknown_section_names))
|
||||
)
|
||||
raise ValueError(f"Unknown config sections found: {', '.join(unknown_section_names)}")
|
||||
|
||||
missing_section_names = set(required_section_names) - section_names
|
||||
if missing_section_names:
|
||||
raise ValueError('Missing config sections: {}'.format(', '.join(missing_section_names)))
|
||||
raise ValueError(f"Missing config sections: {', '.join(missing_section_names)}")
|
||||
|
||||
for section_format in config_format:
|
||||
if section_format.name not in section_names:
|
||||
@@ -91,9 +89,7 @@ def validate_configuration_format(parser, config_format):
|
||||
|
||||
if unexpected_option_names:
|
||||
raise ValueError(
|
||||
'Unexpected options found in config section {}: {}'.format(
|
||||
section_format.name, ', '.join(sorted(unexpected_option_names))
|
||||
)
|
||||
f"Unexpected options found in config section {section_format.name}: {', '.join(sorted(unexpected_option_names))}",
|
||||
)
|
||||
|
||||
missing_option_names = tuple(
|
||||
@@ -105,9 +101,7 @@ def validate_configuration_format(parser, config_format):
|
||||
|
||||
if missing_option_names:
|
||||
raise ValueError(
|
||||
'Required options missing from config section {}: {}'.format(
|
||||
section_format.name, ', '.join(missing_option_names)
|
||||
)
|
||||
f"Required options missing from config section {section_format.name}: {', '.join(missing_option_names)}",
|
||||
)
|
||||
|
||||
|
||||
@@ -137,7 +131,7 @@ def parse_configuration(config_filename, config_format):
|
||||
'''
|
||||
parser = RawConfigParser()
|
||||
if not parser.read(config_filename):
|
||||
raise ValueError('Configuration file cannot be opened: {}'.format(config_filename))
|
||||
raise ValueError(f'Configuration file cannot be opened: {config_filename}')
|
||||
|
||||
validate_configuration_format(parser, config_format)
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import functools
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
|
||||
@@ -81,7 +82,8 @@ class Include_constructor(ruamel.yaml.SafeConstructor):
|
||||
def load_configuration(filename):
|
||||
'''
|
||||
Load the given configuration file and return its contents as a data structure of nested dicts
|
||||
and lists.
|
||||
and lists. Also, replace any "{constant}" strings with the value of the "constant" key in the
|
||||
"constants" section of the configuration file.
|
||||
|
||||
Raise ruamel.yaml.error.YAMLError if something goes wrong parsing the YAML, or RecursionError
|
||||
if there are too many recursive includes.
|
||||
@@ -98,7 +100,19 @@ def load_configuration(filename):
|
||||
yaml = ruamel.yaml.YAML(typ='safe')
|
||||
yaml.Constructor = Include_constructor_with_include_directory
|
||||
|
||||
return yaml.load(open(filename))
|
||||
with open(filename) as file:
|
||||
file_contents = file.read()
|
||||
config = yaml.load(file_contents)
|
||||
|
||||
if config and 'constants' in config:
|
||||
for key, value in config['constants'].items():
|
||||
value = json.dumps(value)
|
||||
file_contents = file_contents.replace(f'{{{key}}}', value.strip('"'))
|
||||
|
||||
config = yaml.load(file_contents)
|
||||
del config['constants']
|
||||
|
||||
return config
|
||||
|
||||
|
||||
DELETED_NODE = object()
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import logging
|
||||
import os
|
||||
|
||||
|
||||
def normalize(config_filename, config):
|
||||
@@ -56,9 +57,15 @@ def normalize(config_filename, config):
|
||||
# Upgrade remote repositories to ssh:// syntax, required in Borg 2.
|
||||
repositories = location.get('repositories')
|
||||
if repositories:
|
||||
if isinstance(repositories[0], str):
|
||||
config['location']['repositories'] = [
|
||||
{'path': repository} for repository in repositories
|
||||
]
|
||||
repositories = config['location']['repositories']
|
||||
config['location']['repositories'] = []
|
||||
for repository in repositories:
|
||||
if '~' in repository:
|
||||
for repository_dict in repositories:
|
||||
repository_path = repository_dict['path']
|
||||
if '~' in repository_path:
|
||||
logs.append(
|
||||
logging.makeLogRecord(
|
||||
dict(
|
||||
@@ -68,21 +75,31 @@ def normalize(config_filename, config):
|
||||
)
|
||||
)
|
||||
)
|
||||
if ':' in repository and not repository.startswith('ssh://'):
|
||||
rewritten_repository = (
|
||||
f"ssh://{repository.replace(':~', '/~').replace(':/', '/').replace(':', '/./')}"
|
||||
)
|
||||
logs.append(
|
||||
logging.makeLogRecord(
|
||||
dict(
|
||||
levelno=logging.WARNING,
|
||||
levelname='WARNING',
|
||||
msg=f'{config_filename}: Remote repository paths without ssh:// syntax are deprecated. Interpreting "{repository}" as "{rewritten_repository}"',
|
||||
if ':' in repository_path:
|
||||
if repository_path.startswith('file://'):
|
||||
updated_repository_path = os.path.abspath(
|
||||
repository_path.partition('file://')[-1]
|
||||
)
|
||||
config['location']['repositories'].append(
|
||||
dict(repository_dict, path=updated_repository_path,)
|
||||
)
|
||||
elif repository_path.startswith('ssh://'):
|
||||
config['location']['repositories'].append(repository_dict)
|
||||
else:
|
||||
rewritten_repository_path = f"ssh://{repository_path.replace(':~', '/~').replace(':/', '/').replace(':', '/./')}"
|
||||
logs.append(
|
||||
logging.makeLogRecord(
|
||||
dict(
|
||||
levelno=logging.WARNING,
|
||||
levelname='WARNING',
|
||||
msg=f'{config_filename}: Remote repository paths without ssh:// syntax are deprecated. Interpreting "{repository_path}" as "{rewritten_repository_path}"',
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
config['location']['repositories'].append(rewritten_repository)
|
||||
config['location']['repositories'].append(
|
||||
dict(repository_dict, path=rewritten_repository_path,)
|
||||
)
|
||||
else:
|
||||
config['location']['repositories'].append(repository)
|
||||
config['location']['repositories'].append(repository_dict)
|
||||
|
||||
return logs
|
||||
|
||||
@@ -3,6 +3,17 @@ required:
|
||||
- location
|
||||
additionalProperties: false
|
||||
properties:
|
||||
constants:
|
||||
type: object
|
||||
description: |
|
||||
Constants to use in the configuration file. All occurences of the
|
||||
constant name within culy braces will be replaced with the value.
|
||||
For example, if you have a constant named "hostname" with the value
|
||||
"myhostname", then the string "{hostname}" will be replaced with
|
||||
"myhostname" in the configuration file.
|
||||
example:
|
||||
hostname: myhostname
|
||||
prefix: myprefix
|
||||
location:
|
||||
type: object
|
||||
description: |
|
||||
@@ -29,19 +40,40 @@ properties:
|
||||
repositories:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: |
|
||||
Paths to local or remote repositories (required). Tildes are
|
||||
expanded. Multiple repositories are backed up to in
|
||||
sequence. Borg placeholders can be used. See the output of
|
||||
"borg help placeholders" for details. See ssh_command for
|
||||
SSH options like identity file or port. If systemd service
|
||||
is used, then add local repository paths in the systemd
|
||||
service file to the ReadWritePaths list.
|
||||
type: object
|
||||
required:
|
||||
- path
|
||||
properties:
|
||||
path:
|
||||
type: string
|
||||
description: |
|
||||
Path to local or remote repository
|
||||
(required). Tildes are expanded.
|
||||
Multiple repositories are backed up
|
||||
to in sequence. Borg placeholders
|
||||
can be used. See the output of
|
||||
"borg help placeholders" for
|
||||
details. See ssh_command for SSH
|
||||
options like identity file or port.
|
||||
If systemd service is used, then
|
||||
add local repository paths in the
|
||||
systemd service file to the
|
||||
ReadWritePaths list.
|
||||
example: ssh://user@backupserver/./{fqdn}
|
||||
label:
|
||||
type: string
|
||||
description: |
|
||||
Optional label for the repository. This
|
||||
can be used with the --repository flag
|
||||
to select a repository to backup to.
|
||||
If not specified, the repository path is
|
||||
used as the label.
|
||||
example: backupserver
|
||||
example:
|
||||
- ssh://user@backupserver/./sourcehostname.borg
|
||||
- ssh://user@backupserver/./{fqdn}
|
||||
- /var/local/backups/local.borg
|
||||
- path: ssh://user@backupserver/./sourcehostname.borg
|
||||
label: backupserver
|
||||
- path: /mnt/backup
|
||||
label: local
|
||||
working_directory:
|
||||
type: string
|
||||
description: |
|
||||
@@ -202,6 +234,12 @@ properties:
|
||||
path prevents "borgmatic restore" from finding any database
|
||||
dumps created before the change. Defaults to ~/.borgmatic
|
||||
example: /tmp/borgmatic
|
||||
source_directories_must_exist:
|
||||
type: boolean
|
||||
description: |
|
||||
If true, then source directories must exist, otherwise an
|
||||
error is raised. Defaults to false.
|
||||
example: true
|
||||
storage:
|
||||
type: object
|
||||
description: |
|
||||
@@ -315,6 +353,12 @@ properties:
|
||||
Path for Borg cache files. Defaults to
|
||||
$borg_base_directory/.cache/borg
|
||||
example: /path/to/base/cache
|
||||
borg_files_cache_ttl:
|
||||
type: integer
|
||||
description: |
|
||||
Maximum time to live (ttl) for entries in the Borg files
|
||||
cache.
|
||||
example: 20
|
||||
borg_security_directory:
|
||||
type: string
|
||||
description: |
|
||||
|
||||
@@ -20,9 +20,9 @@ def format_json_error_path_element(path_element):
|
||||
Given a path element into a JSON data structure, format it for display as a string.
|
||||
'''
|
||||
if isinstance(path_element, int):
|
||||
return str('[{}]'.format(path_element))
|
||||
return str(f'[{path_element}]')
|
||||
|
||||
return str('.{}'.format(path_element))
|
||||
return str(f'.{path_element}')
|
||||
|
||||
|
||||
def format_json_error(error):
|
||||
@@ -30,10 +30,10 @@ def format_json_error(error):
|
||||
Given an instance of jsonschema.exceptions.ValidationError, format it for display as a string.
|
||||
'''
|
||||
if not error.path:
|
||||
return 'At the top level: {}'.format(error.message)
|
||||
return f'At the top level: {error.message}'
|
||||
|
||||
formatted_path = ''.join(format_json_error_path_element(element) for element in error.path)
|
||||
return "At '{}': {}".format(formatted_path.lstrip('.'), error.message)
|
||||
return f"At '{formatted_path.lstrip('.')}': {error.message}"
|
||||
|
||||
|
||||
class Validation_error(ValueError):
|
||||
@@ -54,9 +54,10 @@ class Validation_error(ValueError):
|
||||
'''
|
||||
Render a validation error as a user-facing string.
|
||||
'''
|
||||
return 'An error occurred while parsing a configuration file at {}:\n'.format(
|
||||
self.config_filename
|
||||
) + '\n'.join(error for error in self.errors)
|
||||
return (
|
||||
f'An error occurred while parsing a configuration file at {self.config_filename}:\n'
|
||||
+ '\n'.join(error for error in self.errors)
|
||||
)
|
||||
|
||||
|
||||
def apply_logical_validation(config_filename, parsed_configuration):
|
||||
@@ -72,9 +73,7 @@ def apply_logical_validation(config_filename, parsed_configuration):
|
||||
raise Validation_error(
|
||||
config_filename,
|
||||
(
|
||||
'Unknown repository in the "consistency" section\'s "check_repositories": {}'.format(
|
||||
repository
|
||||
),
|
||||
f'Unknown repository in the "consistency" section\'s "check_repositories": {repository}',
|
||||
),
|
||||
)
|
||||
|
||||
@@ -126,18 +125,29 @@ def normalize_repository_path(repository):
|
||||
'''
|
||||
Given a repository path, return the absolute path of it (for local repositories).
|
||||
'''
|
||||
# A colon in the repository indicates it's a remote repository. Bail.
|
||||
if ':' in repository:
|
||||
# A colon in the repository could mean that it's either a file:// URL or a remote repository.
|
||||
# If it's a remote repository, we don't want to normalize it. If it's a file:// URL, we do.
|
||||
if ':' not in repository:
|
||||
return os.path.abspath(repository)
|
||||
elif repository.startswith('file://'):
|
||||
return os.path.abspath(repository.partition('file://')[-1])
|
||||
else:
|
||||
return repository
|
||||
|
||||
return os.path.abspath(repository)
|
||||
|
||||
|
||||
def repositories_match(first, second):
|
||||
'''
|
||||
Given two repository paths (relative and/or absolute), return whether they match.
|
||||
Given two repository dicts with keys 'path' (relative and/or absolute),
|
||||
and 'label', or two repository paths, return whether they match.
|
||||
'''
|
||||
return normalize_repository_path(first) == normalize_repository_path(second)
|
||||
if isinstance(first, str):
|
||||
first = {'path': first, 'label': first}
|
||||
if isinstance(second, str):
|
||||
second = {'path': second, 'label': second}
|
||||
return (first.get('label') == second.get('label')) or (
|
||||
normalize_repository_path(first.get('path'))
|
||||
== normalize_repository_path(second.get('path'))
|
||||
)
|
||||
|
||||
|
||||
def guard_configuration_contains_repository(repository, configurations):
|
||||
@@ -157,14 +167,14 @@ def guard_configuration_contains_repository(repository, configurations):
|
||||
config_repository
|
||||
for config in configurations.values()
|
||||
for config_repository in config['location']['repositories']
|
||||
if repositories_match(repository, config_repository)
|
||||
if repositories_match(config_repository, repository)
|
||||
)
|
||||
)
|
||||
|
||||
if count == 0:
|
||||
raise ValueError('Repository {} not found in configuration files'.format(repository))
|
||||
raise ValueError(f'Repository {repository} not found in configuration files')
|
||||
if count > 1:
|
||||
raise ValueError('Repository {} found in multiple configuration files'.format(repository))
|
||||
raise ValueError(f'Repository {repository} found in multiple configuration files')
|
||||
|
||||
|
||||
def guard_single_repository_selected(repository, configurations):
|
||||
|
||||
+51
-23
@@ -11,7 +11,7 @@ ERROR_OUTPUT_MAX_LINE_COUNT = 25
|
||||
BORG_ERROR_EXIT_CODE = 2
|
||||
|
||||
|
||||
def exit_code_indicates_error(process, exit_code, borg_local_path=None):
|
||||
def exit_code_indicates_error(command, exit_code, borg_local_path=None):
|
||||
'''
|
||||
Return True if the given exit code from running a command corresponds to an error. If a Borg
|
||||
local path is given and matches the process' command, then treat exit code 1 as a warning
|
||||
@@ -20,8 +20,6 @@ def exit_code_indicates_error(process, exit_code, borg_local_path=None):
|
||||
if exit_code is None:
|
||||
return False
|
||||
|
||||
command = process.args.split(' ') if isinstance(process.args, str) else process.args
|
||||
|
||||
if borg_local_path and command[0] == borg_local_path:
|
||||
return bool(exit_code < 0 or exit_code >= BORG_ERROR_EXIT_CODE)
|
||||
|
||||
@@ -45,6 +43,23 @@ def output_buffer_for_process(process, exclude_stdouts):
|
||||
return process.stderr if process.stdout in exclude_stdouts else process.stdout
|
||||
|
||||
|
||||
def append_last_lines(last_lines, captured_output, line, output_log_level):
|
||||
'''
|
||||
Given a rolling list of last lines, a list of captured output, a line to append, and an output
|
||||
log level, append the line to the last lines and (if necessary) the captured output. Then log
|
||||
the line at the requested output log level.
|
||||
'''
|
||||
last_lines.append(line)
|
||||
|
||||
if len(last_lines) > ERROR_OUTPUT_MAX_LINE_COUNT:
|
||||
last_lines.pop(0)
|
||||
|
||||
if output_log_level is None:
|
||||
captured_output.append(line)
|
||||
else:
|
||||
logger.log(output_log_level, line)
|
||||
|
||||
|
||||
def log_outputs(processes, exclude_stdouts, output_log_level, borg_local_path):
|
||||
'''
|
||||
Given a sequence of subprocess.Popen() instances for multiple processes, log the output for each
|
||||
@@ -100,15 +115,12 @@ def log_outputs(processes, exclude_stdouts, output_log_level, borg_local_path):
|
||||
|
||||
# Keep the last few lines of output in case the process errors, and we need the output for
|
||||
# the exception below.
|
||||
last_lines = buffer_last_lines[ready_buffer]
|
||||
last_lines.append(line)
|
||||
if len(last_lines) > ERROR_OUTPUT_MAX_LINE_COUNT:
|
||||
last_lines.pop(0)
|
||||
|
||||
if output_log_level is None:
|
||||
captured_outputs[ready_process].append(line)
|
||||
else:
|
||||
logger.log(output_log_level, line)
|
||||
append_last_lines(
|
||||
buffer_last_lines[ready_buffer],
|
||||
captured_outputs[ready_process],
|
||||
line,
|
||||
output_log_level,
|
||||
)
|
||||
|
||||
if not still_running:
|
||||
break
|
||||
@@ -121,13 +133,24 @@ def log_outputs(processes, exclude_stdouts, output_log_level, borg_local_path):
|
||||
if exit_code is None:
|
||||
still_running = True
|
||||
|
||||
command = process.args.split(' ') if isinstance(process.args, str) else process.args
|
||||
# If any process errors, then raise accordingly.
|
||||
if exit_code_indicates_error(process, exit_code, borg_local_path):
|
||||
if exit_code_indicates_error(command, exit_code, borg_local_path):
|
||||
# If an error occurs, include its output in the raised exception so that we don't
|
||||
# inadvertently hide error output.
|
||||
output_buffer = output_buffer_for_process(process, exclude_stdouts)
|
||||
|
||||
last_lines = buffer_last_lines[output_buffer] if output_buffer else []
|
||||
|
||||
# Collect any straggling output lines that came in since we last gathered output.
|
||||
while output_buffer: # pragma: no cover
|
||||
line = output_buffer.readline().rstrip().decode()
|
||||
if not line:
|
||||
break
|
||||
|
||||
append_last_lines(
|
||||
last_lines, captured_outputs[process], line, output_log_level=logging.ERROR
|
||||
)
|
||||
|
||||
if len(last_lines) == ERROR_OUTPUT_MAX_LINE_COUNT:
|
||||
last_lines.insert(0, '...')
|
||||
|
||||
@@ -155,8 +178,8 @@ def log_command(full_command, input_file=None, output_file=None):
|
||||
'''
|
||||
logger.debug(
|
||||
' '.join(full_command)
|
||||
+ (' < {}'.format(getattr(input_file, 'name', '')) if input_file else '')
|
||||
+ (' > {}'.format(getattr(output_file, 'name', '')) if output_file else '')
|
||||
+ (f" < {getattr(input_file, 'name', '')}" if input_file else '')
|
||||
+ (f" > {getattr(output_file, 'name', '')}" if output_file else '')
|
||||
)
|
||||
|
||||
|
||||
@@ -228,13 +251,18 @@ def execute_command_and_capture_output(
|
||||
environment = {**os.environ, **extra_environment} if extra_environment else None
|
||||
command = ' '.join(full_command) if shell else full_command
|
||||
|
||||
output = subprocess.check_output(
|
||||
command,
|
||||
stderr=subprocess.STDOUT if capture_stderr else None,
|
||||
shell=shell,
|
||||
env=environment,
|
||||
cwd=working_directory,
|
||||
)
|
||||
try:
|
||||
output = subprocess.check_output(
|
||||
command,
|
||||
stderr=subprocess.STDOUT if capture_stderr else None,
|
||||
shell=shell,
|
||||
env=environment,
|
||||
cwd=working_directory,
|
||||
)
|
||||
except subprocess.CalledProcessError as error:
|
||||
if exit_code_indicates_error(command, error.returncode):
|
||||
raise
|
||||
output = error.output
|
||||
|
||||
return output.decode() if output is not None else None
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ def interpolate_context(config_filename, hook_description, command, context):
|
||||
names/values, interpolate the values by "{name}" into the command and return the result.
|
||||
'''
|
||||
for name, value in context.items():
|
||||
command = command.replace('{%s}' % name, str(value))
|
||||
command = command.replace(f'{{{name}}}', str(value))
|
||||
|
||||
for unsupported_variable in re.findall(r'{\w+}', command):
|
||||
logger.warning(
|
||||
@@ -38,7 +38,7 @@ def execute_hook(commands, umask, config_filename, description, dry_run, **conte
|
||||
Raise subprocesses.CalledProcessError if an error occurs in a hook.
|
||||
'''
|
||||
if not commands:
|
||||
logger.debug('{}: No commands to run for {} hook'.format(config_filename, description))
|
||||
logger.debug(f'{config_filename}: No commands to run for {description} hook')
|
||||
return
|
||||
|
||||
dry_run_label = ' (dry run; not actually running hooks)' if dry_run else ''
|
||||
@@ -49,19 +49,15 @@ def execute_hook(commands, umask, config_filename, description, dry_run, **conte
|
||||
]
|
||||
|
||||
if len(commands) == 1:
|
||||
logger.info(
|
||||
'{}: Running command for {} hook{}'.format(config_filename, description, dry_run_label)
|
||||
)
|
||||
logger.info(f'{config_filename}: Running command for {description} hook{dry_run_label}')
|
||||
else:
|
||||
logger.info(
|
||||
'{}: Running {} commands for {} hook{}'.format(
|
||||
config_filename, len(commands), description, dry_run_label
|
||||
)
|
||||
f'{config_filename}: Running {len(commands)} commands for {description} hook{dry_run_label}',
|
||||
)
|
||||
|
||||
if umask:
|
||||
parsed_umask = int(str(umask), 8)
|
||||
logger.debug('{}: Set hook umask to {}'.format(config_filename, oct(parsed_umask)))
|
||||
logger.debug(f'{config_filename}: Set hook umask to {oct(parsed_umask)}')
|
||||
original_umask = os.umask(parsed_umask)
|
||||
else:
|
||||
original_umask = None
|
||||
@@ -93,9 +89,7 @@ def considered_soft_failure(config_filename, error):
|
||||
|
||||
if exit_code == SOFT_FAIL_EXIT_CODE:
|
||||
logger.info(
|
||||
'{}: Command hook exited with soft failure exit code ({}); skipping remaining actions'.format(
|
||||
config_filename, SOFT_FAIL_EXIT_CODE
|
||||
)
|
||||
f'{config_filename}: Command hook exited with soft failure exit code ({SOFT_FAIL_EXIT_CODE}); skipping remaining actions',
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
@@ -34,17 +34,15 @@ def ping_monitor(hook_config, config_filename, state, monitoring_log_level, dry_
|
||||
return
|
||||
|
||||
dry_run_label = ' (dry run; not actually pinging)' if dry_run else ''
|
||||
formatted_state = '/{}/'.format(MONITOR_STATE_TO_CRONHUB[state])
|
||||
formatted_state = f'/{MONITOR_STATE_TO_CRONHUB[state]}/'
|
||||
ping_url = (
|
||||
hook_config['ping_url']
|
||||
.replace('/start/', formatted_state)
|
||||
.replace('/ping/', formatted_state)
|
||||
)
|
||||
|
||||
logger.info(
|
||||
'{}: Pinging Cronhub {}{}'.format(config_filename, state.name.lower(), dry_run_label)
|
||||
)
|
||||
logger.debug('{}: Using Cronhub ping URL {}'.format(config_filename, ping_url))
|
||||
logger.info(f'{config_filename}: Pinging Cronhub {state.name.lower()}{dry_run_label}')
|
||||
logger.debug(f'{config_filename}: Using Cronhub ping URL {ping_url}')
|
||||
|
||||
if not dry_run:
|
||||
logging.getLogger('urllib3').setLevel(logging.ERROR)
|
||||
|
||||
@@ -34,12 +34,10 @@ def ping_monitor(hook_config, config_filename, state, monitoring_log_level, dry_
|
||||
return
|
||||
|
||||
dry_run_label = ' (dry run; not actually pinging)' if dry_run else ''
|
||||
ping_url = '{}/{}'.format(hook_config['ping_url'], MONITOR_STATE_TO_CRONITOR[state])
|
||||
ping_url = f"{hook_config['ping_url']}/{MONITOR_STATE_TO_CRONITOR[state]}"
|
||||
|
||||
logger.info(
|
||||
'{}: Pinging Cronitor {}{}'.format(config_filename, state.name.lower(), dry_run_label)
|
||||
)
|
||||
logger.debug('{}: Using Cronitor ping URL {}'.format(config_filename, ping_url))
|
||||
logger.info(f'{config_filename}: Pinging Cronitor {state.name.lower()}{dry_run_label}')
|
||||
logger.debug(f'{config_filename}: Using Cronitor ping URL {ping_url}')
|
||||
|
||||
if not dry_run:
|
||||
logging.getLogger('urllib3').setLevel(logging.ERROR)
|
||||
|
||||
@@ -43,9 +43,9 @@ def call_hook(function_name, hooks, log_prefix, hook_name, *args, **kwargs):
|
||||
try:
|
||||
module = HOOK_NAME_TO_MODULE[hook_name]
|
||||
except KeyError:
|
||||
raise ValueError('Unknown hook name: {}'.format(hook_name))
|
||||
raise ValueError(f'Unknown hook name: {hook_name}')
|
||||
|
||||
logger.debug('{}: Calling {} hook function {}'.format(log_prefix, hook_name, function_name))
|
||||
logger.debug(f'{log_prefix}: Calling {hook_name} hook function {function_name}')
|
||||
return getattr(module, function_name)(config, log_prefix, *args, **kwargs)
|
||||
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ def make_database_dump_filename(dump_path, name, hostname=None):
|
||||
Raise ValueError if the database name is invalid.
|
||||
'''
|
||||
if os.path.sep in name:
|
||||
raise ValueError('Invalid database name {}'.format(name))
|
||||
raise ValueError(f'Invalid database name {name}')
|
||||
|
||||
return os.path.join(os.path.expanduser(dump_path), hostname or 'localhost', name)
|
||||
|
||||
@@ -60,9 +60,7 @@ def remove_database_dumps(dump_path, database_type_name, log_prefix, dry_run):
|
||||
'''
|
||||
dry_run_label = ' (dry run; not actually removing anything)' if dry_run else ''
|
||||
|
||||
logger.debug(
|
||||
'{}: Removing {} database dumps{}'.format(log_prefix, database_type_name, dry_run_label)
|
||||
)
|
||||
logger.debug(f'{log_prefix}: Removing {database_type_name} database dumps{dry_run_label}')
|
||||
|
||||
expanded_path = os.path.expanduser(dump_path)
|
||||
|
||||
@@ -78,4 +76,4 @@ def convert_glob_patterns_to_borg_patterns(patterns):
|
||||
Convert a sequence of shell glob patterns like "/etc/*" to the corresponding Borg archive
|
||||
patterns like "sh:etc/*".
|
||||
'''
|
||||
return ['sh:{}'.format(pattern.lstrip(os.path.sep)) for pattern in patterns]
|
||||
return [f'sh:{pattern.lstrip(os.path.sep)}' for pattern in patterns]
|
||||
|
||||
@@ -99,7 +99,7 @@ def ping_monitor(hook_config, config_filename, state, monitoring_log_level, dry_
|
||||
ping_url = (
|
||||
hook_config['ping_url']
|
||||
if hook_config['ping_url'].startswith('http')
|
||||
else 'https://hc-ping.com/{}'.format(hook_config['ping_url'])
|
||||
else f"https://hc-ping.com/{hook_config['ping_url']}"
|
||||
)
|
||||
dry_run_label = ' (dry run; not actually pinging)' if dry_run else ''
|
||||
|
||||
@@ -111,12 +111,10 @@ def ping_monitor(hook_config, config_filename, state, monitoring_log_level, dry_
|
||||
|
||||
healthchecks_state = MONITOR_STATE_TO_HEALTHCHECKS.get(state)
|
||||
if healthchecks_state:
|
||||
ping_url = '{}/{}'.format(ping_url, healthchecks_state)
|
||||
ping_url = f'{ping_url}/{healthchecks_state}'
|
||||
|
||||
logger.info(
|
||||
'{}: Pinging Healthchecks {}{}'.format(config_filename, state.name.lower(), dry_run_label)
|
||||
)
|
||||
logger.debug('{}: Using Healthchecks ping URL {}'.format(config_filename, ping_url))
|
||||
logger.info(f'{config_filename}: Pinging Healthchecks {state.name.lower()}{dry_run_label}')
|
||||
logger.debug(f'{config_filename}: Using Healthchecks ping URL {ping_url}')
|
||||
|
||||
if state in (monitor.State.FINISH, monitor.State.FAIL, monitor.State.LOG):
|
||||
payload = format_buffered_logs_for_payload()
|
||||
|
||||
@@ -27,7 +27,7 @@ def dump_databases(databases, log_prefix, location_config, dry_run):
|
||||
'''
|
||||
dry_run_label = ' (dry run; not actually dumping anything)' if dry_run else ''
|
||||
|
||||
logger.info('{}: Dumping MongoDB databases{}'.format(log_prefix, dry_run_label))
|
||||
logger.info(f'{log_prefix}: Dumping MongoDB databases{dry_run_label}')
|
||||
|
||||
processes = []
|
||||
for database in databases:
|
||||
@@ -38,9 +38,7 @@ def dump_databases(databases, log_prefix, location_config, dry_run):
|
||||
dump_format = database.get('format', 'archive')
|
||||
|
||||
logger.debug(
|
||||
'{}: Dumping MongoDB database {} to {}{}'.format(
|
||||
log_prefix, name, dump_filename, dry_run_label
|
||||
)
|
||||
f'{log_prefix}: Dumping MongoDB database {name} to {dump_filename}{dry_run_label}',
|
||||
)
|
||||
if dry_run:
|
||||
continue
|
||||
@@ -126,9 +124,7 @@ def restore_database_dump(database_config, log_prefix, location_config, dry_run,
|
||||
)
|
||||
restore_command = build_restore_command(extract_process, database, dump_filename)
|
||||
|
||||
logger.debug(
|
||||
'{}: Restoring MongoDB database {}{}'.format(log_prefix, database['name'], dry_run_label)
|
||||
)
|
||||
logger.debug(f"{log_prefix}: Restoring MongoDB database {database['name']}{dry_run_label}")
|
||||
if dry_run:
|
||||
return
|
||||
|
||||
|
||||
@@ -88,9 +88,7 @@ def execute_dump_command(
|
||||
+ (('--user', database['username']) if 'username' in database else ())
|
||||
+ ('--databases',)
|
||||
+ database_names
|
||||
# Use shell redirection rather than execute_command(output_file=open(...)) to prevent
|
||||
# the open() call on a named pipe from hanging the main borgmatic process.
|
||||
+ ('>', dump_filename)
|
||||
+ ('--result-file', dump_filename)
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
@@ -102,7 +100,7 @@ def execute_dump_command(
|
||||
dump.create_named_pipe_for_dump(dump_filename)
|
||||
|
||||
return execute_command(
|
||||
dump_command, shell=True, extra_environment=extra_environment, run_to_completion=False,
|
||||
dump_command, extra_environment=extra_environment, run_to_completion=False,
|
||||
)
|
||||
|
||||
|
||||
@@ -119,7 +117,7 @@ def dump_databases(databases, log_prefix, location_config, dry_run):
|
||||
dry_run_label = ' (dry run; not actually dumping anything)' if dry_run else ''
|
||||
processes = []
|
||||
|
||||
logger.info('{}: Dumping MySQL databases{}'.format(log_prefix, dry_run_label))
|
||||
logger.info(f'{log_prefix}: Dumping MySQL databases{dry_run_label}')
|
||||
|
||||
for database in databases:
|
||||
dump_path = make_dump_path(location_config)
|
||||
@@ -209,9 +207,7 @@ def restore_database_dump(database_config, log_prefix, location_config, dry_run,
|
||||
)
|
||||
extra_environment = {'MYSQL_PWD': database['password']} if 'password' in database else None
|
||||
|
||||
logger.debug(
|
||||
'{}: Restoring MySQL database {}{}'.format(log_prefix, database['name'], dry_run_label)
|
||||
)
|
||||
logger.debug(f"{log_prefix}: Restoring MySQL database {database['name']}{dry_run_label}")
|
||||
if dry_run:
|
||||
return
|
||||
|
||||
|
||||
@@ -29,14 +29,12 @@ def ping_monitor(hook_config, config_filename, state, monitoring_log_level, dry_
|
||||
'''
|
||||
if state != monitor.State.FAIL:
|
||||
logger.debug(
|
||||
'{}: Ignoring unsupported monitoring {} in PagerDuty hook'.format(
|
||||
config_filename, state.name.lower()
|
||||
)
|
||||
f'{config_filename}: Ignoring unsupported monitoring {state.name.lower()} in PagerDuty hook',
|
||||
)
|
||||
return
|
||||
|
||||
dry_run_label = ' (dry run; not actually sending)' if dry_run else ''
|
||||
logger.info('{}: Sending failure event to PagerDuty {}'.format(config_filename, dry_run_label))
|
||||
logger.info(f'{config_filename}: Sending failure event to PagerDuty {dry_run_label}')
|
||||
|
||||
if dry_run:
|
||||
return
|
||||
@@ -50,7 +48,7 @@ def ping_monitor(hook_config, config_filename, state, monitoring_log_level, dry_
|
||||
'routing_key': hook_config['integration_key'],
|
||||
'event_action': 'trigger',
|
||||
'payload': {
|
||||
'summary': 'backup failed on {}'.format(hostname),
|
||||
'summary': f'backup failed on {hostname}',
|
||||
'severity': 'error',
|
||||
'source': hostname,
|
||||
'timestamp': local_timestamp,
|
||||
@@ -65,7 +63,7 @@ def ping_monitor(hook_config, config_filename, state, monitoring_log_level, dry_
|
||||
},
|
||||
}
|
||||
)
|
||||
logger.debug('{}: Using PagerDuty payload: {}'.format(config_filename, payload))
|
||||
logger.debug(f'{config_filename}: Using PagerDuty payload: {payload}')
|
||||
|
||||
logging.getLogger('urllib3').setLevel(logging.ERROR)
|
||||
try:
|
||||
|
||||
@@ -93,7 +93,7 @@ def dump_databases(databases, log_prefix, location_config, dry_run):
|
||||
dry_run_label = ' (dry run; not actually dumping anything)' if dry_run else ''
|
||||
processes = []
|
||||
|
||||
logger.info('{}: Dumping PostgreSQL databases{}'.format(log_prefix, dry_run_label))
|
||||
logger.info(f'{log_prefix}: Dumping PostgreSQL databases{dry_run_label}')
|
||||
|
||||
for database in databases:
|
||||
extra_environment = make_extra_environment(database)
|
||||
@@ -228,9 +228,7 @@ def restore_database_dump(database_config, log_prefix, location_config, dry_run,
|
||||
)
|
||||
extra_environment = make_extra_environment(database)
|
||||
|
||||
logger.debug(
|
||||
'{}: Restoring PostgreSQL database {}{}'.format(log_prefix, database['name'], dry_run_label)
|
||||
)
|
||||
logger.debug(f"{log_prefix}: Restoring PostgreSQL database {database['name']}{dry_run_label}")
|
||||
if dry_run:
|
||||
return
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ def dump_databases(databases, log_prefix, location_config, dry_run):
|
||||
dry_run_label = ' (dry run; not actually dumping anything)' if dry_run else ''
|
||||
processes = []
|
||||
|
||||
logger.info('{}: Dumping SQLite databases{}'.format(log_prefix, dry_run_label))
|
||||
logger.info(f'{log_prefix}: Dumping SQLite databases{dry_run_label}')
|
||||
|
||||
for database in databases:
|
||||
database_path = database['path']
|
||||
|
||||
+1
-1
@@ -108,7 +108,7 @@ def color_text(color, message):
|
||||
if not color:
|
||||
return message
|
||||
|
||||
return '{}{}{}'.format(color, message, colorama.Style.RESET_ALL)
|
||||
return f'{color}{message}{colorama.Style.RESET_ALL}'
|
||||
|
||||
|
||||
def add_logging_level(level_name, level_number):
|
||||
|
||||
@@ -18,6 +18,7 @@ RUN npm install @11ty/eleventy \
|
||||
@11ty/eleventy-plugin-syntaxhighlight \
|
||||
@11ty/eleventy-plugin-inclusive-language \
|
||||
@11ty/eleventy-navigation \
|
||||
eleventy-plugin-code-clipboard \
|
||||
markdown-it \
|
||||
markdown-it-anchor \
|
||||
markdown-it-replace-link
|
||||
|
||||
@@ -533,3 +533,18 @@ main .elv-toc + h1 .direct-link {
|
||||
.header-anchor:hover::after {
|
||||
content: " 🔗";
|
||||
}
|
||||
|
||||
.mdi {
|
||||
display: inline-block;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
background-color: currentColor;
|
||||
-webkit-mask: no-repeat center / 100%;
|
||||
mask: no-repeat center / 100%;
|
||||
-webkit-mask-image: var(--svg);
|
||||
mask-image: var(--svg);
|
||||
}
|
||||
|
||||
.mdi.mdi-content-copy {
|
||||
--svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='black' d='M19 21H8V7h11m0-2H8a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h11a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2m-3-4H4a2 2 0 0 0-2 2v14h2V3h12V1Z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
@@ -22,6 +22,6 @@
|
||||
<body>
|
||||
|
||||
{{ content | safe }}
|
||||
|
||||
{% initClipboardJS %}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -136,6 +136,53 @@ hooks:
|
||||
format: sql
|
||||
```
|
||||
|
||||
### Containers
|
||||
|
||||
If your database is running within a Docker container and borgmatic is too, no
|
||||
problem—simply configure borgmatic to connect to the container's name on its
|
||||
exposed port. For instance:
|
||||
|
||||
```yaml
|
||||
hooks:
|
||||
postgresql_databases:
|
||||
- name: users
|
||||
hostname: your-database-container-name
|
||||
port: 5433
|
||||
username: postgres
|
||||
password: trustsome1
|
||||
```
|
||||
|
||||
But what if borgmatic is running on the host? You can still connect to a
|
||||
database container if its ports are properly exposed to the host. For
|
||||
instance, when running the database container with Docker, you can specify
|
||||
`--publish 127.0.0.1:5433:5432` so that it exposes the container's port 5432
|
||||
to port 5433 on the host (only reachable on localhost, in this case). Or the
|
||||
same thing with Docker Compose:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
your-database-container-name:
|
||||
image: postgres
|
||||
ports:
|
||||
- 127.0.0.1:5433:5432
|
||||
```
|
||||
|
||||
And then you can connect to the database from borgmatic running on the host:
|
||||
|
||||
```yaml
|
||||
hooks:
|
||||
postgresql_databases:
|
||||
- name: users
|
||||
hostname: 127.0.0.1
|
||||
port: 5433
|
||||
username: postgres
|
||||
password: trustsome1
|
||||
```
|
||||
|
||||
Of course, alter the ports in these examples to suit your particular database
|
||||
system.
|
||||
|
||||
|
||||
### No source directories
|
||||
|
||||
<span class="minilink minilink-addedin">New in version 1.7.1</span> If you
|
||||
@@ -154,7 +201,6 @@ hooks:
|
||||
```
|
||||
|
||||
|
||||
|
||||
### External passwords
|
||||
|
||||
If you don't want to keep your database passwords in your borgmatic
|
||||
@@ -231,7 +277,8 @@ If you have a single repository in your borgmatic configuration file(s), no
|
||||
problem: the `restore` action figures out which repository to use.
|
||||
|
||||
But if you have multiple repositories configured, then you'll need to specify
|
||||
the repository path containing the archive to restore. Here's an example:
|
||||
the repository to use via the `--repository` flag. This can be done either
|
||||
with the repository's path or its label as configured in your borgmatic configuration file.
|
||||
|
||||
```bash
|
||||
borgmatic restore --repository repo.borg --archive host-2023-...
|
||||
@@ -334,6 +381,23 @@ dumps with any database system.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### PostgreSQL/MySQL authentication errors
|
||||
|
||||
With PostgreSQL and MySQL/MariaDB, if you're getting authentication errors
|
||||
when borgmatic tries to connect to your database, a natural reaction is to
|
||||
increase your borgmatic verbosity with `--verbosity 2` and go looking in the
|
||||
logs. You'll notice however that your database password does not show up in
|
||||
the logs. This is likely not the cause of the authentication problem unless
|
||||
you mistyped your password, however; borgmatic passes your password to the
|
||||
database via an environment variable that does not appear in the logs.
|
||||
|
||||
The cause of an authentication error is often on the database side—in the
|
||||
configuration of which users are allowed to connect and how they are
|
||||
authenticated. For instance, with PostgreSQL, check your
|
||||
[pg_hba.conf](https://www.postgresql.org/docs/current/auth-pg-hba-conf.html)
|
||||
file for that configuration.
|
||||
|
||||
|
||||
### MySQL table lock errors
|
||||
|
||||
If you encounter table lock errors during a database dump with MySQL/MariaDB,
|
||||
|
||||
@@ -57,8 +57,8 @@ cron job).
|
||||
Another option is to customize your consistency checks. By default, if you
|
||||
omit consistency checks from configuration, borgmatic runs full-repository
|
||||
checks (`repository`) and per-archive checks (`archives`) within each
|
||||
repository, no more than once a month. This is equivalent to what `borg check`
|
||||
does if run without options.
|
||||
repository. (Although see below about check frequency.) This is equivalent to
|
||||
what `borg check` does if run without options.
|
||||
|
||||
But if you find that archive checks are too slow, for example, you can
|
||||
configure borgmatic to run repository checks only. Configure this in the
|
||||
@@ -70,8 +70,9 @@ consistency:
|
||||
- name: repository
|
||||
```
|
||||
|
||||
<span class="minilink minilink-addedin">Prior to version 1.6.2</span> `checks`
|
||||
was a plain list of strings without the `name:` part. For example:
|
||||
<span class="minilink minilink-addedin">Prior to version 1.6.2</span> The
|
||||
`checks` option was a plain list of strings without the `name:` part, and
|
||||
borgmatic ran each configured check every time checks were run. For example:
|
||||
|
||||
```yaml
|
||||
consistency:
|
||||
@@ -112,8 +113,13 @@ consistency:
|
||||
This tells borgmatic to run the `repository` consistency check at most once
|
||||
every two weeks for a given repository and the `archives` check at most once a
|
||||
month. The `frequency` value is a number followed by a unit of time, e.g. "3
|
||||
days", "1 week", "2 months", etc. The `frequency` defaults to `always`, which
|
||||
means run this check every time checks run.
|
||||
days", "1 week", "2 months", etc.
|
||||
|
||||
The `frequency` defaults to `always` for a check configured without a
|
||||
`frequency`, which means run this check every time checks run. But if you omit
|
||||
consistency checks from configuration entirely, borgmatic runs full-repository
|
||||
checks (`repository`) and per-archive checks (`archives`) within each
|
||||
repository, at most once a month.
|
||||
|
||||
Unlike a real scheduler like cron, borgmatic only makes a best effort to run
|
||||
checks on the configured frequency. It compares that frequency with how long
|
||||
|
||||
@@ -87,7 +87,7 @@ If you would like to run the full test suite, first install Docker and [Docker
|
||||
Compose](https://docs.docker.com/compose/install/). Then run:
|
||||
|
||||
```bash
|
||||
scripts/run-full-dev-tests
|
||||
scripts/run-end-to-end-dev-tests
|
||||
```
|
||||
|
||||
Note that this scripts assumes you have permission to run Docker. If you
|
||||
|
||||
@@ -51,7 +51,8 @@ If you have a single repository in your borgmatic configuration file(s), no
|
||||
problem: the `extract` action figures out which repository to use.
|
||||
|
||||
But if you have multiple repositories configured, then you'll need to specify
|
||||
the repository path containing the archive to extract. Here's an example:
|
||||
the repository to use via the `--repository` flag. This can be done either
|
||||
with the repository's path or its label as configured in your borgmatic configuration file.
|
||||
|
||||
```bash
|
||||
borgmatic extract --repository repo.borg --archive host-2023-...
|
||||
|
||||
@@ -20,9 +20,8 @@ location:
|
||||
|
||||
# Paths of local or remote repositories to backup to.
|
||||
repositories:
|
||||
- ssh://1234@usw-s001.rsync.net/./backups.borg
|
||||
- ssh://k8pDxu32@k8pDxu32.repo.borgbase.com/./repo
|
||||
- /var/lib/backups/local.borg
|
||||
- path: ssh://k8pDxu32@k8pDxu32.repo.borgbase.com/./repo
|
||||
- path: /var/lib/backups/local.borg
|
||||
```
|
||||
|
||||
When you run borgmatic with this configuration, it invokes Borg once for each
|
||||
|
||||
@@ -255,3 +255,63 @@ Be sure to quote your overrides if they contain spaces or other characters
|
||||
that your shell may interpret.
|
||||
|
||||
An alternate to command-line overrides is passing in your values via [environment variables](https://torsion.org/borgmatic/docs/how-to/provide-your-passwords/).
|
||||
|
||||
|
||||
## Constant interpolation
|
||||
|
||||
<span class="minilink minilink-addedin">New in version 1.7.10</span> Another
|
||||
tool is borgmatic's support for defining custom constants. This is similar to
|
||||
the [variable interpolation
|
||||
feature](https://torsion.org/borgmatic/docs/how-to/add-preparation-and-cleanup-steps-to-backups/#variable-interpolation)
|
||||
for command hooks, but the constants feature lets you substitute your own
|
||||
custom values into anywhere in the entire configuration file. (Constants don't
|
||||
work across includes or separate configuration files though.)
|
||||
|
||||
Here's an example usage:
|
||||
|
||||
```yaml
|
||||
constants:
|
||||
user: foo
|
||||
my_prefix: bar-
|
||||
|
||||
location:
|
||||
source_directories:
|
||||
- /home/{user}/.config
|
||||
- /home/{user}/.ssh
|
||||
...
|
||||
|
||||
storage:
|
||||
archive_name_format: '{my_prefix}{now}'
|
||||
|
||||
retention:
|
||||
prefix: {my_prefix}
|
||||
|
||||
consistency:
|
||||
prefix: {my_prefix}
|
||||
```
|
||||
|
||||
In this example, when borgmatic runs, all instances of `{user}` get replaced
|
||||
with `foo` and all instances of `{my_prefix}` get replaced with `bar-`. (And
|
||||
in this particular example, `{now}` doesn't get replaced with anything, but
|
||||
gets passed directly to Borg.) After substitution, the logical result looks
|
||||
something like this:
|
||||
|
||||
```yaml
|
||||
location:
|
||||
source_directories:
|
||||
- /home/foo/.config
|
||||
- /home/foo/.ssh
|
||||
...
|
||||
|
||||
storage:
|
||||
archive_name_format: 'bar-{now}'
|
||||
|
||||
retention:
|
||||
prefix: bar-
|
||||
|
||||
consistency:
|
||||
prefix: bar-
|
||||
```
|
||||
|
||||
An alternate to constants is passing in your values via [environment
|
||||
variables](https://torsion.org/borgmatic/docs/how-to/provide-your-passwords/).
|
||||
|
||||
@@ -53,7 +53,8 @@ This runs Borg's `rlist` command once on each configured borgmatic repository.
|
||||
(The native `borgmatic rlist` action should be preferred for most use.)
|
||||
|
||||
What if you only want to run Borg on a single configured borgmatic repository
|
||||
when you've got several configured? Not a problem.
|
||||
when you've got several configured? Not a problem. The `--repository` argument
|
||||
lets you specify the repository to use, either by its path or its label:
|
||||
|
||||
```bash
|
||||
borgmatic borg --repository repo.borg break-lock
|
||||
|
||||
@@ -94,6 +94,7 @@ installing borgmatic:
|
||||
* [openSUSE](https://software.opensuse.org/package/borgmatic)
|
||||
* [macOS (via Homebrew)](https://formulae.brew.sh/formula/borgmatic)
|
||||
* [macOS (via MacPorts)](https://ports.macports.org/port/borgmatic/)
|
||||
* [NixOS](https://search.nixos.org/packages?show=borgmatic&sort=relevance&type=packages&query=borgmatic)
|
||||
* [Ansible role](https://github.com/borgbase/ansible-role-borgbackup)
|
||||
* [virtualenv](https://virtualenv.pypa.io/en/stable/)
|
||||
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
# This script installs test dependencies and runs all tests, including end-to-end tests. It
|
||||
# is designed to run inside a test container, and presumes that other test infrastructure like
|
||||
# databases are already running. Therefore, on a developer machine, you should not run this script
|
||||
# directly. Instead, run scripts/run-full-dev-tests
|
||||
# directly. Instead, run scripts/run-end-to-end-dev-tests
|
||||
#
|
||||
# For more information, see:
|
||||
# https://torsion.org/borgmatic/docs/how-to/develop-on-borgmatic/
|
||||
|
||||
set -e
|
||||
set -ex
|
||||
|
||||
apk add --no-cache python3 py3-pip borgbackup postgresql-client mariadb-client mongodb-tools \
|
||||
py3-ruamel.yaml py3-ruamel.yaml.clib bash sqlite
|
||||
@@ -17,5 +17,9 @@ apk add --no-cache py3-typed-ast py3-regex || true
|
||||
python3 -m pip install --no-cache --upgrade pip==22.2.2 setuptools==64.0.1
|
||||
pip3 install --ignore-installed tox==3.25.1
|
||||
export COVERAGE_FILE=/tmp/.coverage
|
||||
tox --workdir /tmp/.tox --sitepackages
|
||||
|
||||
if [ "$1" != "--end-to-end-only" ] ; then
|
||||
tox --workdir /tmp/.tox --sitepackages
|
||||
fi
|
||||
|
||||
tox --workdir /tmp/.tox --sitepackages -e end-to-end
|
||||
|
||||
@@ -5,7 +5,7 @@ description_file=README.md
|
||||
testpaths = tests
|
||||
addopts = --cov-report term-missing:skip-covered --cov=borgmatic --ignore=tests/end-to-end
|
||||
filterwarnings =
|
||||
ignore:Coverage disabled.*:pytest.PytestWarning
|
||||
ignore:Deprecated call to `pkg_resources.declare_namespace\('ruamel'\)`.*:DeprecationWarning
|
||||
|
||||
[flake8]
|
||||
ignore = E501,W503
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from setuptools import find_packages, setup
|
||||
|
||||
VERSION = '1.7.9'
|
||||
VERSION = '1.7.10'
|
||||
|
||||
|
||||
setup(
|
||||
|
||||
@@ -6,6 +6,8 @@ colorama==0.4.4
|
||||
coverage==5.3
|
||||
flake8==4.0.1
|
||||
flake8-quotes==3.3.2
|
||||
flake8-use-fstring==1.4
|
||||
flake8-variables-names==0.0.5
|
||||
flexmock==0.10.4
|
||||
isort==5.9.1
|
||||
mccabe==0.6.1
|
||||
|
||||
@@ -23,8 +23,8 @@ services:
|
||||
- "/app/borgmatic.egg-info"
|
||||
tty: true
|
||||
working_dir: /app
|
||||
command:
|
||||
- /app/scripts/run-full-tests
|
||||
entrypoint: /app/scripts/run-full-tests
|
||||
command: --end-to-end-only
|
||||
depends_on:
|
||||
- postgresql
|
||||
- mysql
|
||||
|
||||
@@ -12,17 +12,14 @@ def generate_configuration(config_path, repository_path):
|
||||
to work for testing (including injecting the given repository path and tacking on an encryption
|
||||
passphrase).
|
||||
'''
|
||||
subprocess.check_call(
|
||||
'generate-borgmatic-config --destination {}'.format(config_path).split(' ')
|
||||
)
|
||||
subprocess.check_call(f'generate-borgmatic-config --destination {config_path}'.split(' '))
|
||||
config = (
|
||||
open(config_path)
|
||||
.read()
|
||||
.replace('ssh://user@backupserver/./sourcehostname.borg', repository_path)
|
||||
.replace('- ssh://user@backupserver/./{fqdn}', '')
|
||||
.replace('- /var/local/backups/local.borg', '')
|
||||
.replace('- /home/user/path with spaces', '')
|
||||
.replace('- /home', '- {}'.format(config_path))
|
||||
.replace('- path: /mnt/backup', '')
|
||||
.replace('label: local', '')
|
||||
.replace('- /home', f'- {config_path}')
|
||||
.replace('- /etc', '')
|
||||
.replace('- /var/log/syslog*', '')
|
||||
+ 'storage:\n encryption_passphrase: "test"'
|
||||
@@ -47,13 +44,13 @@ def test_borgmatic_command():
|
||||
generate_configuration(config_path, repository_path)
|
||||
|
||||
subprocess.check_call(
|
||||
'borgmatic -v 2 --config {} init --encryption repokey'.format(config_path).split(' ')
|
||||
f'borgmatic -v 2 --config {config_path} init --encryption repokey'.split(' ')
|
||||
)
|
||||
|
||||
# Run borgmatic to generate a backup archive, and then list it to make sure it exists.
|
||||
subprocess.check_call('borgmatic --config {}'.format(config_path).split(' '))
|
||||
subprocess.check_call(f'borgmatic --config {config_path}'.split(' '))
|
||||
output = subprocess.check_output(
|
||||
'borgmatic --config {} list --json'.format(config_path).split(' ')
|
||||
f'borgmatic --config {config_path} list --json'.split(' ')
|
||||
).decode(sys.stdout.encoding)
|
||||
parsed_output = json.loads(output)
|
||||
|
||||
@@ -64,16 +61,14 @@ def test_borgmatic_command():
|
||||
# Extract the created archive into the current (temporary) directory, and confirm that the
|
||||
# extracted file looks right.
|
||||
output = subprocess.check_output(
|
||||
'borgmatic --config {} extract --archive {}'.format(config_path, archive_name).split(
|
||||
' '
|
||||
)
|
||||
f'borgmatic --config {config_path} extract --archive {archive_name}'.split(' '),
|
||||
).decode(sys.stdout.encoding)
|
||||
extracted_config_path = os.path.join(extract_path, config_path)
|
||||
assert open(extracted_config_path).read() == open(config_path).read()
|
||||
|
||||
# Exercise the info action.
|
||||
output = subprocess.check_output(
|
||||
'borgmatic --config {} info --json'.format(config_path).split(' ')
|
||||
f'borgmatic --config {config_path} info --json'.split(' '),
|
||||
).decode(sys.stdout.encoding)
|
||||
parsed_output = json.loads(output)
|
||||
|
||||
|
||||
@@ -189,7 +189,7 @@ def test_database_dump_with_error_causes_borgmatic_to_exit():
|
||||
'-v',
|
||||
'2',
|
||||
'--override',
|
||||
"hooks.postgresql_databases=[{'name': 'nope'}]",
|
||||
"hooks.postgresql_databases=[{'name': 'nope'}]", # noqa: FS003
|
||||
]
|
||||
)
|
||||
finally:
|
||||
|
||||
@@ -10,17 +10,15 @@ def generate_configuration(config_path, repository_path):
|
||||
to work for testing (including injecting the given repository path and tacking on an encryption
|
||||
passphrase).
|
||||
'''
|
||||
subprocess.check_call(
|
||||
'generate-borgmatic-config --destination {}'.format(config_path).split(' ')
|
||||
)
|
||||
subprocess.check_call(f'generate-borgmatic-config --destination {config_path}'.split(' '))
|
||||
config = (
|
||||
open(config_path)
|
||||
.read()
|
||||
.replace('ssh://user@backupserver/./sourcehostname.borg', repository_path)
|
||||
.replace('- ssh://user@backupserver/./{fqdn}', '')
|
||||
.replace('- ssh://user@backupserver/./{fqdn}', '') # noqa: FS003
|
||||
.replace('- /var/local/backups/local.borg', '')
|
||||
.replace('- /home/user/path with spaces', '')
|
||||
.replace('- /home', '- {}'.format(config_path))
|
||||
.replace('- /home', f'- {config_path}')
|
||||
.replace('- /etc', '')
|
||||
.replace('- /var/log/syslog*', '')
|
||||
+ 'storage:\n encryption_passphrase: "test"'
|
||||
|
||||
@@ -7,12 +7,8 @@ def test_validate_config_command_with_valid_configuration_succeeds():
|
||||
with tempfile.TemporaryDirectory() as temporary_directory:
|
||||
config_path = os.path.join(temporary_directory, 'test.yaml')
|
||||
|
||||
subprocess.check_call(
|
||||
'generate-borgmatic-config --destination {}'.format(config_path).split(' ')
|
||||
)
|
||||
exit_code = subprocess.call(
|
||||
'validate-borgmatic-config --config {}'.format(config_path).split(' ')
|
||||
)
|
||||
subprocess.check_call(f'generate-borgmatic-config --destination {config_path}'.split(' '))
|
||||
exit_code = subprocess.call(f'validate-borgmatic-config --config {config_path}'.split(' '))
|
||||
|
||||
assert exit_code == 0
|
||||
|
||||
@@ -21,16 +17,12 @@ def test_validate_config_command_with_invalid_configuration_fails():
|
||||
with tempfile.TemporaryDirectory() as temporary_directory:
|
||||
config_path = os.path.join(temporary_directory, 'test.yaml')
|
||||
|
||||
subprocess.check_call(
|
||||
'generate-borgmatic-config --destination {}'.format(config_path).split(' ')
|
||||
)
|
||||
subprocess.check_call(f'generate-borgmatic-config --destination {config_path}'.split(' '))
|
||||
config = open(config_path).read().replace('keep_daily: 7', 'keep_daily: "7"')
|
||||
config_file = open(config_path, 'w')
|
||||
config_file.write(config)
|
||||
config_file.close()
|
||||
|
||||
exit_code = subprocess.call(
|
||||
'validate-borgmatic-config --config {}'.format(config_path).split(' ')
|
||||
)
|
||||
exit_code = subprocess.call(f'validate-borgmatic-config --config {config_path}'.split(' '))
|
||||
|
||||
assert exit_code == 1
|
||||
|
||||
@@ -7,7 +7,7 @@ from borgmatic.config import legacy as module
|
||||
|
||||
def test_parse_section_options_with_punctuation_should_return_section_options():
|
||||
parser = module.RawConfigParser()
|
||||
parser.read_file(StringIO('[section]\nfoo: {}\n'.format(string.punctuation)))
|
||||
parser.read_file(StringIO(f'[section]\nfoo: {string.punctuation}\n'))
|
||||
|
||||
section_format = module.Section_format(
|
||||
'section', (module.Config_option('foo', str, required=True),)
|
||||
|
||||
@@ -10,11 +10,41 @@ from borgmatic.config import load as module
|
||||
|
||||
def test_load_configuration_parses_contents():
|
||||
builtins = flexmock(sys.modules['builtins'])
|
||||
builtins.should_receive('open').with_args('config.yaml').and_return('key: value')
|
||||
|
||||
config_file = io.StringIO('key: value')
|
||||
config_file.name = 'config.yaml'
|
||||
builtins.should_receive('open').with_args('config.yaml').and_return(config_file)
|
||||
assert module.load_configuration('config.yaml') == {'key': 'value'}
|
||||
|
||||
|
||||
def test_load_configuration_replaces_constants():
|
||||
builtins = flexmock(sys.modules['builtins'])
|
||||
config_file = io.StringIO(
|
||||
'''
|
||||
constants:
|
||||
key: value
|
||||
key: {key}
|
||||
'''
|
||||
)
|
||||
config_file.name = 'config.yaml'
|
||||
builtins.should_receive('open').with_args('config.yaml').and_return(config_file)
|
||||
assert module.load_configuration('config.yaml') == {'key': 'value'}
|
||||
|
||||
|
||||
def test_load_configuration_replaces_complex_constants():
|
||||
builtins = flexmock(sys.modules['builtins'])
|
||||
config_file = io.StringIO(
|
||||
'''
|
||||
constants:
|
||||
key:
|
||||
subkey: value
|
||||
key: {key}
|
||||
'''
|
||||
)
|
||||
config_file.name = 'config.yaml'
|
||||
builtins.should_receive('open').with_args('config.yaml').and_return(config_file)
|
||||
assert module.load_configuration('config.yaml') == {'key': {'subkey': 'value'}}
|
||||
|
||||
|
||||
def test_load_configuration_inlines_include_relative_to_current_directory():
|
||||
builtins = flexmock(sys.modules['builtins'])
|
||||
flexmock(module.os).should_receive('getcwd').and_return('/tmp')
|
||||
|
||||
@@ -63,7 +63,10 @@ def test_parse_configuration_transforms_file_into_mapping():
|
||||
config, logs = module.parse_configuration('/tmp/config.yaml', '/tmp/schema.yaml')
|
||||
|
||||
assert config == {
|
||||
'location': {'source_directories': ['/home', '/etc'], 'repositories': ['hostname.borg']},
|
||||
'location': {
|
||||
'source_directories': ['/home', '/etc'],
|
||||
'repositories': [{'path': 'hostname.borg'}],
|
||||
},
|
||||
'retention': {'keep_daily': 7, 'keep_hourly': 24, 'keep_minutely': 60},
|
||||
'consistency': {'checks': [{'name': 'repository'}, {'name': 'archives'}]},
|
||||
}
|
||||
@@ -89,7 +92,7 @@ def test_parse_configuration_passes_through_quoted_punctuation():
|
||||
assert config == {
|
||||
'location': {
|
||||
'source_directories': [f'/home/{string.punctuation}'],
|
||||
'repositories': ['test.borg'],
|
||||
'repositories': [{'path': 'test.borg'}],
|
||||
}
|
||||
}
|
||||
assert logs == []
|
||||
@@ -151,7 +154,7 @@ def test_parse_configuration_inlines_include():
|
||||
config, logs = module.parse_configuration('/tmp/config.yaml', '/tmp/schema.yaml')
|
||||
|
||||
assert config == {
|
||||
'location': {'source_directories': ['/home'], 'repositories': ['hostname.borg']},
|
||||
'location': {'source_directories': ['/home'], 'repositories': [{'path': 'hostname.borg'}]},
|
||||
'retention': {'keep_daily': 7, 'keep_hourly': 24},
|
||||
}
|
||||
assert logs == []
|
||||
@@ -185,7 +188,7 @@ def test_parse_configuration_merges_include():
|
||||
config, logs = module.parse_configuration('/tmp/config.yaml', '/tmp/schema.yaml')
|
||||
|
||||
assert config == {
|
||||
'location': {'source_directories': ['/home'], 'repositories': ['hostname.borg']},
|
||||
'location': {'source_directories': ['/home'], 'repositories': [{'path': 'hostname.borg'}]},
|
||||
'retention': {'keep_daily': 1, 'keep_hourly': 24},
|
||||
}
|
||||
assert logs == []
|
||||
@@ -247,7 +250,7 @@ def test_parse_configuration_applies_overrides():
|
||||
assert config == {
|
||||
'location': {
|
||||
'source_directories': ['/home'],
|
||||
'repositories': ['hostname.borg'],
|
||||
'repositories': [{'path': 'hostname.borg'}],
|
||||
'local_path': 'borg2',
|
||||
}
|
||||
}
|
||||
@@ -273,7 +276,7 @@ def test_parse_configuration_applies_normalization():
|
||||
assert config == {
|
||||
'location': {
|
||||
'source_directories': ['/home'],
|
||||
'repositories': ['hostname.borg'],
|
||||
'repositories': [{'path': 'hostname.borg'}],
|
||||
'exclude_if_present': ['.nobackup'],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,10 +138,10 @@ def test_log_outputs_kills_other_processes_when_one_errors():
|
||||
|
||||
process = subprocess.Popen(['grep'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
flexmock(module).should_receive('exit_code_indicates_error').with_args(
|
||||
process, None, 'borg'
|
||||
['grep'], None, 'borg'
|
||||
).and_return(False)
|
||||
flexmock(module).should_receive('exit_code_indicates_error').with_args(
|
||||
process, 2, 'borg'
|
||||
['grep'], 2, 'borg'
|
||||
).and_return(True)
|
||||
other_process = subprocess.Popen(
|
||||
['sleep', '2'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT
|
||||
@@ -239,21 +239,20 @@ def test_log_outputs_does_not_error_when_one_process_exits():
|
||||
|
||||
|
||||
def test_log_outputs_truncates_long_error_output():
|
||||
flexmock(module).ERROR_OUTPUT_MAX_LINE_COUNT = 0
|
||||
flexmock(module.logger).should_receive('log')
|
||||
flexmock(module).should_receive('command_for_process').and_return('grep')
|
||||
|
||||
process = subprocess.Popen(['grep'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
flexmock(module).should_receive('exit_code_indicates_error').with_args(
|
||||
process, None, 'borg'
|
||||
['grep'], None, 'borg'
|
||||
).and_return(False)
|
||||
flexmock(module).should_receive('exit_code_indicates_error').with_args(
|
||||
process, 2, 'borg'
|
||||
['grep'], 2, 'borg'
|
||||
).and_return(True)
|
||||
flexmock(module).should_receive('output_buffer_for_process').and_return(process.stdout)
|
||||
|
||||
with pytest.raises(subprocess.CalledProcessError) as error:
|
||||
module.log_outputs(
|
||||
flexmock(module, ERROR_OUTPUT_MAX_LINE_COUNT=0).log_outputs(
|
||||
(process,), exclude_stdouts=(), output_log_level=logging.INFO, borg_local_path='borg'
|
||||
)
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ def test_run_borg_does_not_raise():
|
||||
borg_arguments = flexmock(repository=flexmock(), archive=flexmock(), options=flexmock())
|
||||
|
||||
module.run_borg(
|
||||
repository='repo',
|
||||
repository={'path': 'repos'},
|
||||
storage={},
|
||||
local_borg_version=None,
|
||||
borg_arguments=borg_arguments,
|
||||
|
||||
@@ -10,7 +10,7 @@ def test_run_break_lock_does_not_raise():
|
||||
break_lock_arguments = flexmock(repository=flexmock())
|
||||
|
||||
module.run_break_lock(
|
||||
repository='repo',
|
||||
repository={'path': 'repo'},
|
||||
storage={},
|
||||
local_borg_version=None,
|
||||
break_lock_arguments=break_lock_arguments,
|
||||
|
||||
@@ -18,7 +18,7 @@ def test_run_check_calls_hooks_for_configured_repository():
|
||||
|
||||
module.run_check(
|
||||
config_filename='test.yaml',
|
||||
repository='repo',
|
||||
repository={'path': 'repo'},
|
||||
location={'repositories': ['repo']},
|
||||
storage={},
|
||||
consistency={},
|
||||
@@ -49,7 +49,7 @@ def test_run_check_runs_with_selected_repository():
|
||||
|
||||
module.run_check(
|
||||
config_filename='test.yaml',
|
||||
repository=flexmock(),
|
||||
repository={'path': 'repo'},
|
||||
location={'repositories': ['repo']},
|
||||
storage={},
|
||||
consistency={},
|
||||
@@ -80,7 +80,7 @@ def test_run_check_bails_if_repository_does_not_match():
|
||||
|
||||
module.run_check(
|
||||
config_filename='test.yaml',
|
||||
repository='repo',
|
||||
repository={'path': 'repo'},
|
||||
location={'repositories': ['repo']},
|
||||
storage={},
|
||||
consistency={},
|
||||
|
||||
@@ -16,7 +16,7 @@ def test_compact_actions_calls_hooks_for_configured_repository():
|
||||
|
||||
module.run_compact(
|
||||
config_filename='test.yaml',
|
||||
repository='repo',
|
||||
repository={'path': 'repo'},
|
||||
storage={},
|
||||
retention={},
|
||||
hooks={},
|
||||
@@ -44,7 +44,7 @@ def test_compact_runs_with_selected_repository():
|
||||
|
||||
module.run_compact(
|
||||
config_filename='test.yaml',
|
||||
repository='repo',
|
||||
repository={'path': 'repo'},
|
||||
storage={},
|
||||
retention={},
|
||||
hooks={},
|
||||
@@ -72,7 +72,7 @@ def test_compact_bails_if_repository_does_not_match():
|
||||
|
||||
module.run_compact(
|
||||
config_filename='test.yaml',
|
||||
repository='repo',
|
||||
repository={'path': 'repo'},
|
||||
storage={},
|
||||
retention={},
|
||||
hooks={},
|
||||
|
||||
@@ -24,7 +24,7 @@ def test_run_create_executes_and_calls_hooks_for_configured_repository():
|
||||
list(
|
||||
module.run_create(
|
||||
config_filename='test.yaml',
|
||||
repository='repo',
|
||||
repository={'path': 'repo'},
|
||||
location={},
|
||||
storage={},
|
||||
hooks={},
|
||||
@@ -57,7 +57,7 @@ def test_run_create_runs_with_selected_repository():
|
||||
list(
|
||||
module.run_create(
|
||||
config_filename='test.yaml',
|
||||
repository='repo',
|
||||
repository={'path': 'repo'},
|
||||
location={},
|
||||
storage={},
|
||||
hooks={},
|
||||
|
||||
@@ -19,7 +19,7 @@ def test_run_export_tar_does_not_raise():
|
||||
global_arguments = flexmock(monitoring_verbosity=1, dry_run=False)
|
||||
|
||||
module.run_export_tar(
|
||||
repository='repo',
|
||||
repository={'path': 'repo'},
|
||||
storage={},
|
||||
local_borg_version=None,
|
||||
export_tar_arguments=export_tar_arguments,
|
||||
|
||||
@@ -20,7 +20,7 @@ def test_run_extract_calls_hooks():
|
||||
|
||||
module.run_extract(
|
||||
config_filename='test.yaml',
|
||||
repository='repo',
|
||||
repository={'path': 'repo'},
|
||||
location={'repositories': ['repo']},
|
||||
storage={},
|
||||
hooks={},
|
||||
|
||||
@@ -14,7 +14,7 @@ def test_run_info_does_not_raise():
|
||||
|
||||
list(
|
||||
module.run_info(
|
||||
repository='repo',
|
||||
repository={'path': 'repo'},
|
||||
storage={},
|
||||
local_borg_version=None,
|
||||
info_arguments=info_arguments,
|
||||
|
||||
@@ -14,7 +14,7 @@ def test_run_list_does_not_raise():
|
||||
|
||||
list(
|
||||
module.run_list(
|
||||
repository='repo',
|
||||
repository={'path': 'repo'},
|
||||
storage={},
|
||||
local_borg_version=None,
|
||||
list_arguments=list_arguments,
|
||||
|
||||
@@ -17,7 +17,7 @@ def test_run_mount_does_not_raise():
|
||||
)
|
||||
|
||||
module.run_mount(
|
||||
repository='repo',
|
||||
repository={'path': 'repo'},
|
||||
storage={},
|
||||
local_borg_version=None,
|
||||
mount_arguments=mount_arguments,
|
||||
|
||||
@@ -13,7 +13,7 @@ def test_run_prune_calls_hooks_for_configured_repository():
|
||||
|
||||
module.run_prune(
|
||||
config_filename='test.yaml',
|
||||
repository='repo',
|
||||
repository={'path': 'repo'},
|
||||
storage={},
|
||||
retention={},
|
||||
hooks={},
|
||||
@@ -38,7 +38,7 @@ def test_run_prune_runs_with_selected_repository():
|
||||
|
||||
module.run_prune(
|
||||
config_filename='test.yaml',
|
||||
repository='repo',
|
||||
repository={'path': 'repo'},
|
||||
storage={},
|
||||
retention={},
|
||||
hooks={},
|
||||
|
||||
@@ -18,7 +18,7 @@ def test_run_rcreate_does_not_raise():
|
||||
)
|
||||
|
||||
module.run_rcreate(
|
||||
repository='repo',
|
||||
repository={'path': 'repo'},
|
||||
storage={},
|
||||
local_borg_version=None,
|
||||
rcreate_arguments=arguments,
|
||||
@@ -45,7 +45,7 @@ def test_run_rcreate_bails_if_repository_does_not_match():
|
||||
)
|
||||
|
||||
module.run_rcreate(
|
||||
repository='repo',
|
||||
repository={'path': 'repo'},
|
||||
storage={},
|
||||
local_borg_version=None,
|
||||
rcreate_arguments=arguments,
|
||||
|
||||
@@ -67,7 +67,7 @@ def test_collect_archive_database_names_parses_archive_paths():
|
||||
)
|
||||
|
||||
archive_database_names = module.collect_archive_database_names(
|
||||
repository='repo',
|
||||
repository={'path': 'repo'},
|
||||
archive='archive',
|
||||
location={'borgmatic_source_directory': '.borgmatic'},
|
||||
storage=flexmock(),
|
||||
@@ -92,7 +92,7 @@ def test_collect_archive_database_names_parses_directory_format_archive_paths():
|
||||
)
|
||||
|
||||
archive_database_names = module.collect_archive_database_names(
|
||||
repository='repo',
|
||||
repository={'path': 'repo'},
|
||||
archive='archive',
|
||||
location={'borgmatic_source_directory': '.borgmatic'},
|
||||
storage=flexmock(),
|
||||
@@ -113,7 +113,7 @@ def test_collect_archive_database_names_skips_bad_archive_paths():
|
||||
)
|
||||
|
||||
archive_database_names = module.collect_archive_database_names(
|
||||
repository='repo',
|
||||
repository={'path': 'repo'},
|
||||
archive='archive',
|
||||
location={'borgmatic_source_directory': '.borgmatic'},
|
||||
storage=flexmock(),
|
||||
@@ -251,7 +251,7 @@ def test_run_restore_restores_each_database():
|
||||
flexmock(module).should_receive('ensure_databases_found')
|
||||
|
||||
module.run_restore(
|
||||
repository='repo',
|
||||
repository={'path': 'repo'},
|
||||
location=flexmock(),
|
||||
storage=flexmock(),
|
||||
hooks=flexmock(),
|
||||
@@ -273,7 +273,7 @@ def test_run_restore_bails_for_non_matching_repository():
|
||||
flexmock(module).should_receive('restore_single_database').never()
|
||||
|
||||
module.run_restore(
|
||||
repository='repo',
|
||||
repository={'path': 'repo'},
|
||||
location=flexmock(),
|
||||
storage=flexmock(),
|
||||
hooks=flexmock(),
|
||||
@@ -345,7 +345,7 @@ def test_run_restore_restores_database_configured_with_all_name():
|
||||
flexmock(module).should_receive('ensure_databases_found')
|
||||
|
||||
module.run_restore(
|
||||
repository='repo',
|
||||
repository={'path': 'repo'},
|
||||
location=flexmock(),
|
||||
storage=flexmock(),
|
||||
hooks=flexmock(),
|
||||
@@ -417,7 +417,7 @@ def test_run_restore_skips_missing_database():
|
||||
flexmock(module).should_receive('ensure_databases_found')
|
||||
|
||||
module.run_restore(
|
||||
repository='repo',
|
||||
repository={'path': 'repo'},
|
||||
location=flexmock(),
|
||||
storage=flexmock(),
|
||||
hooks=flexmock(),
|
||||
@@ -483,7 +483,7 @@ def test_run_restore_restores_databases_from_different_hooks():
|
||||
flexmock(module).should_receive('ensure_databases_found')
|
||||
|
||||
module.run_restore(
|
||||
repository='repo',
|
||||
repository={'path': 'repo'},
|
||||
location=flexmock(),
|
||||
storage=flexmock(),
|
||||
hooks=flexmock(),
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user