forked from borgmatic-collective/borgmatic
Compare commits
91 Commits
Author | SHA1 | Date |
---|---|---|
Dan Helfman | 8cec7c74d8 | |
Dan Helfman | d3086788eb | |
Dan Helfman | 8d860ea02c | |
Dan Helfman | b343363bb8 | |
Dan Helfman | 9db31bd1e9 | |
Dan Helfman | d88bcc8be9 | |
Dan Helfman | 332f7c4bb6 | |
Dan Helfman | 5d19d86e4a | |
Soumik Dutta | 044ae7869a | |
Dan Helfman | 62ae82f2c0 | |
Dan Helfman | 66194b7304 | |
Soumik Dutta | 98e429594e | |
Soumik Dutta | 4fcfddbe08 | |
Soumik Dutta | f442aeae9c | |
Soumik Dutta | e211863cba | |
Soumik Dutta | 45256ae33f | |
Soumik Dutta | 1573d68fe2 | |
Soumik Dutta | 69f6695253 | |
Dan Helfman | a7c055264d | |
Divyansh Singh | db18364a73 | |
Dan Helfman | 22498ebd4c | |
Dan Helfman | e1f02d9fa5 | |
Dan Helfman | 9ec220c600 | |
Divyansh Singh | cf0275a3ed | |
Divyansh Singh | c71eb60cd2 | |
Divyansh Singh | 675e54ba9f | |
Divyansh Singh | 1793ad74bd | |
Divyansh Singh | 767a7d900b | |
Divyansh Singh | 903507bd03 | |
Dan Helfman | b6cf7d2adc | |
Dan Helfman | a071e02d20 | |
Divyansh Singh | 3aa88085ed | |
Divyansh Singh | af1cc27988 | |
Dan Helfman | dbf8301c19 | |
Dan Helfman | 2a306bef12 | |
Dan Helfman | 2a36a2a312 | |
Dan Helfman | d7a07f0428 | |
Dan Helfman | da321e180d | |
Dan Helfman | c6582e1171 | |
Dan Helfman | 9b83afe491 | |
Dan Helfman | 2814ac3642 | |
Dan Helfman | 8a9d5d93f5 | |
Dan Helfman | 783a6d3b45 | |
Tom Hubrecht | 95575c3450 | |
Tom Hubrecht | 9b071ff92f | |
Tom Hubrecht | d80e716822 | |
Dan Helfman | 418ebc8843 | |
Dan Helfman | f5a448c7c2 | |
Dan Helfman | 37ac542b31 | |
Clemens Lang | 8c7d7e3e41 | |
Dan Helfman | b811f125b2 | |
Dan Helfman | 061f3e7917 | |
Dan Helfman | 6055918907 | |
Dan Helfman | 4a90e090ad | |
Dan Helfman | 301b29ee11 | |
Dan Helfman | c1eb210253 | |
Dan Helfman | 30cca62d09 | |
Dan Helfman | 113c0e7616 | |
Dan Helfman | 0e6b2c6773 | |
Dan Helfman | 22c750b949 | |
Dan Helfman | 504cce39a1 | |
Dan Helfman | 6c4abb6803 | |
palto42 | fd7ad86daa | |
Dan Helfman | 6f3b23c79d | |
Dan Helfman | 4838f5e810 | |
Macguire Rintoul | 116f1ab989 | |
Dan Helfman | 5e15c9f2bc | |
Dan Helfman | 442641f9f6 | |
Dan Helfman | f67c544be6 | |
Dan Helfman | 437fd4dbae | |
Dan Helfman | 36873252d6 | |
Dan Helfman | 1ef82a27fa | |
Dan Helfman | 6837dcbf42 | |
Dan Helfman | c657764367 | |
Dan Helfman | f79286fc91 | |
Dan Helfman | 694d376d15 | |
Dan Helfman | ab4c08019c | |
Dan Helfman | fd39f54df7 | |
Dan Helfman | ca7e18bb29 | |
Dan Helfman | 6975a5b155 | |
Dan Helfman | b627d00595 | |
Dan Helfman | 9bd8f1a6df | |
Javier Paniagua | faf682ca35 | |
Dan Helfman | 6aeb74550d | |
Dan Helfman | 89500df429 | |
Dan Helfman | 82b072d0b7 | |
Dan Helfman | 018c0296fd | |
Dan Helfman | 9c42e7e817 | |
Dan Helfman | 953277a066 | |
Jelle @ Samson-IT | 3720f22234 | |
Jelle @ Samson-IT | 1fdec480d6 |
61
NEWS
61
NEWS
|
@ -1,3 +1,64 @@
|
||||||
|
1.7.9.dev0
|
||||||
|
* #295: Add a SQLite database dump/restore hook.
|
||||||
|
* #304: Change the default action order when no actions are specified on the command-line to:
|
||||||
|
"create", "prune", "compact", "check". If you'd like to retain the old ordering ("prune" and
|
||||||
|
"compact" first), then specify actions explicitly on the command-line.
|
||||||
|
* #304: Run any command-line actions in the order specified instead of using a fixed ordering.
|
||||||
|
* #628: Add a Healthchecks "log" state to send borgmatic logs to Healthchecks without signalling
|
||||||
|
success or failure.
|
||||||
|
* #647: Add "--strip-components all" feature on the "extract" action to remove leading path
|
||||||
|
components of files you extract. Must be used with the "--path" flag.
|
||||||
|
|
||||||
|
1.7.8
|
||||||
|
* #620: With the "create" action and the "--list" ("--files") flag, only show excluded files at
|
||||||
|
verbosity 2.
|
||||||
|
* #621: Add optional authentication to the ntfy monitoring hook.
|
||||||
|
* With the "create" action, only one of "--list" ("--files") and "--progress" flags can be used.
|
||||||
|
This lines up with the new behavior in Borg 2.0.0b5.
|
||||||
|
* Internally support new Borg 2.0.0b5 "--filter" status characters / item flags for the "create"
|
||||||
|
action.
|
||||||
|
* Fix the "create" action with the "--dry-run" flag querying for databases when a PostgreSQL/MySQL
|
||||||
|
"all" database is configured. Now, these queries are skipped due to the dry run.
|
||||||
|
* Add "--repository" flag to the "rcreate" action to optionally select one configured repository to
|
||||||
|
create.
|
||||||
|
* Add "--progress" flag to the "transfer" action, new in Borg 2.0.0b5.
|
||||||
|
* Add "checkpoint_volume" configuration option to creates checkpoints every specified number of
|
||||||
|
bytes during a long-running backup, new in Borg 2.0.0b5.
|
||||||
|
|
||||||
|
1.7.7
|
||||||
|
* #642: Add MySQL database hook "add_drop_database" configuration option to control whether dumped
|
||||||
|
MySQL databases get dropped right before restore.
|
||||||
|
* #643: Fix for potential data loss (data not getting backed up) when dumping large "directory"
|
||||||
|
format PostgreSQL/MongoDB databases. Prior to the fix, these dumps would not finish writing to
|
||||||
|
disk before Borg consumed them. Now, the dumping process completes before Borg starts. This only
|
||||||
|
applies to "directory" format databases; other formats still stream to Borg without using
|
||||||
|
temporary disk space.
|
||||||
|
* Fix MongoDB "directory" format to work with mongodump/mongorestore without error. Prior to this
|
||||||
|
fix, only the "archive" format worked.
|
||||||
|
|
||||||
|
1.7.6
|
||||||
|
* #393, #438, #560: Optionally dump "all" PostgreSQL/MySQL databases to separate files instead of
|
||||||
|
one combined dump file, allowing more convenient restores of individual databases. You can enable
|
||||||
|
this by specifying the database dump "format" option when the database is named "all".
|
||||||
|
* #602: Fix logs that interfere with JSON output by making warnings go to stderr instead of stdout.
|
||||||
|
* #622: Fix traceback when include merging configuration files on ARM64.
|
||||||
|
* #629: Skip warning about excluded special files when no special files have been excluded.
|
||||||
|
* #630: Add configuration options for database command customization: "list_options",
|
||||||
|
"restore_options", and "analyze_options" for PostgreSQL, "restore_options" for MySQL, and
|
||||||
|
"restore_options" for MongoDB.
|
||||||
|
|
||||||
|
1.7.5
|
||||||
|
* #311: Override PostgreSQL dump/restore commands via configuration options.
|
||||||
|
* #604: Fix traceback when a configuration section is present but lacking any options.
|
||||||
|
* #607: Clarify documentation examples for include merging and deep merging.
|
||||||
|
* #611: Fix "data" consistency check to support "check_last" and consistency "prefix" options.
|
||||||
|
* #613: Clarify documentation about multiple repositories and separate configuration files.
|
||||||
|
|
||||||
|
1.7.4
|
||||||
|
* #596: Fix special file detection erroring when broken symlinks are encountered.
|
||||||
|
* #597, #598: Fix regression in which "check" action errored on certain systems ("Cannot determine
|
||||||
|
Borg repository ID").
|
||||||
|
|
||||||
1.7.3
|
1.7.3
|
||||||
* #357: Add "break-lock" action for removing any repository and cache locks leftover from Borg
|
* #357: Add "break-lock" action for removing any repository and cache locks leftover from Borg
|
||||||
aborting.
|
aborting.
|
||||||
|
|
52
README.md
52
README.md
|
@ -67,6 +67,7 @@ borgmatic is powered by [Borg Backup](https://www.borgbackup.org/).
|
||||||
<a href="https://www.mysql.com/"><img src="docs/static/mysql.png" alt="MySQL" height="60px" style="margin-bottom:20px;"></a>
|
<a href="https://www.mysql.com/"><img src="docs/static/mysql.png" alt="MySQL" height="60px" style="margin-bottom:20px;"></a>
|
||||||
<a href="https://mariadb.com/"><img src="docs/static/mariadb.png" alt="MariaDB" height="60px" style="margin-bottom:20px;"></a>
|
<a href="https://mariadb.com/"><img src="docs/static/mariadb.png" alt="MariaDB" height="60px" style="margin-bottom:20px;"></a>
|
||||||
<a href="https://www.mongodb.com/"><img src="docs/static/mongodb.png" alt="MongoDB" height="60px" style="margin-bottom:20px;"></a>
|
<a href="https://www.mongodb.com/"><img src="docs/static/mongodb.png" alt="MongoDB" height="60px" style="margin-bottom:20px;"></a>
|
||||||
|
<a href="https://sqlite.org/"><img src="docs/static/sqlite.png" alt="SQLite" height="60px" style="margin-bottom:20px;"></a>
|
||||||
<a href="https://healthchecks.io/"><img src="docs/static/healthchecks.png" alt="Healthchecks" height="60px" style="margin-bottom:20px;"></a>
|
<a href="https://healthchecks.io/"><img src="docs/static/healthchecks.png" alt="Healthchecks" height="60px" style="margin-bottom:20px;"></a>
|
||||||
<a href="https://cronitor.io/"><img src="docs/static/cronitor.png" alt="Cronitor" height="60px" style="margin-bottom:20px;"></a>
|
<a href="https://cronitor.io/"><img src="docs/static/cronitor.png" alt="Cronitor" height="60px" style="margin-bottom:20px;"></a>
|
||||||
<a href="https://cronhub.io/"><img src="docs/static/cronhub.png" alt="Cronhub" height="60px" style="margin-bottom:20px;"></a>
|
<a href="https://cronhub.io/"><img src="docs/static/cronhub.png" alt="Cronhub" height="60px" style="margin-bottom:20px;"></a>
|
||||||
|
@ -104,23 +105,38 @@ offerings, but do not currently fund borgmatic development or hosting.
|
||||||
|
|
||||||
### Issues
|
### Issues
|
||||||
|
|
||||||
You've got issues? Or an idea for a feature enhancement? We've got an [issue
|
Are you experiencing an issue with borgmatic? Or do you have an idea for a
|
||||||
tracker](https://projects.torsion.org/borgmatic-collective/borgmatic/issues). In order to
|
feature enhancement? Head on over to our [issue
|
||||||
create a new issue or comment on an issue, you'll need to [login
|
tracker](https://projects.torsion.org/borgmatic-collective/borgmatic/issues).
|
||||||
first](https://projects.torsion.org/user/login). Note that you can login with
|
In order to create a new issue or add a comment, you'll need to
|
||||||
an existing GitHub account if you prefer.
|
[register](https://projects.torsion.org/user/sign_up?invite_code=borgmatic)
|
||||||
|
first. If you prefer to use an existing GitHub account, you can skip account
|
||||||
If you'd like to chat with borgmatic developers or users, head on over to the
|
creation and [login directly](https://projects.torsion.org/user/login).
|
||||||
`#borgmatic` IRC channel on Libera Chat, either via <a
|
|
||||||
href="https://web.libera.chat/#borgmatic">web chat</a> or a
|
|
||||||
native <a href="ircs://irc.libera.chat:6697">IRC client</a>. If you
|
|
||||||
don't get a response right away, please hang around a while—or file a ticket
|
|
||||||
instead.
|
|
||||||
|
|
||||||
Also see the [security
|
Also see the [security
|
||||||
policy](https://torsion.org/borgmatic/docs/security-policy/) for any security
|
policy](https://torsion.org/borgmatic/docs/security-policy/) for any security
|
||||||
issues.
|
issues.
|
||||||
|
|
||||||
|
|
||||||
|
### Social
|
||||||
|
|
||||||
|
Check out the [Borg subreddit](https://www.reddit.com/r/BorgBackup/) for
|
||||||
|
general Borg and borgmatic discussion and support.
|
||||||
|
|
||||||
|
Also follow [borgmatic on Mastodon](https://fosstodon.org/@borgmatic).
|
||||||
|
|
||||||
|
|
||||||
|
### Chat
|
||||||
|
|
||||||
|
To chat with borgmatic developers or users, check out the `#borgmatic`
|
||||||
|
IRC channel on Libera Chat, either via <a
|
||||||
|
href="https://web.libera.chat/#borgmatic">web chat</a> or a native <a
|
||||||
|
href="ircs://irc.libera.chat:6697">IRC client</a>. If you don't get a response
|
||||||
|
right away, please hang around a while—or file a ticket instead.
|
||||||
|
|
||||||
|
|
||||||
|
### Other
|
||||||
|
|
||||||
Other questions or comments? Contact
|
Other questions or comments? Contact
|
||||||
[witten@torsion.org](mailto:witten@torsion.org).
|
[witten@torsion.org](mailto:witten@torsion.org).
|
||||||
|
|
||||||
|
@ -135,10 +151,14 @@ borgmatic is licensed under the GNU General Public License version 3 or any
|
||||||
later version.
|
later version.
|
||||||
|
|
||||||
If you'd like to contribute to borgmatic development, please feel free to
|
If you'd like to contribute to borgmatic development, please feel free to
|
||||||
submit a [Pull Request](https://projects.torsion.org/borgmatic-collective/borgmatic/pulls)
|
submit a [Pull
|
||||||
or open an [issue](https://projects.torsion.org/borgmatic-collective/borgmatic/issues) first
|
Request](https://projects.torsion.org/borgmatic-collective/borgmatic/pulls) or
|
||||||
to discuss your idea. We also accept Pull Requests on GitHub, if that's more
|
open an
|
||||||
your thing. In general, contributions are very welcome. We don't bite!
|
[issue](https://projects.torsion.org/borgmatic-collective/borgmatic/issues) to
|
||||||
|
discuss your idea. Note that you'll need to
|
||||||
|
[register](https://projects.torsion.org/user/sign_up?invite_code=borgmatic)
|
||||||
|
first. We also accept Pull Requests on GitHub, if that's more your thing. In
|
||||||
|
general, contributions are very welcome. We don't bite!
|
||||||
|
|
||||||
Also, please check out the [borgmatic development
|
Also, please check out the [borgmatic development
|
||||||
how-to](https://torsion.org/borgmatic/docs/how-to/develop-on-borgmatic/) for
|
how-to](https://torsion.org/borgmatic/docs/how-to/develop-on-borgmatic/) for
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import borgmatic.borg.borg
|
||||||
|
import borgmatic.borg.rlist
|
||||||
|
import borgmatic.config.validate
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def run_borg(
|
||||||
|
repository, storage, local_borg_version, borg_arguments, local_path, remote_path,
|
||||||
|
):
|
||||||
|
'''
|
||||||
|
Run the "borg" action for the given repository.
|
||||||
|
'''
|
||||||
|
if borg_arguments.repository is None or borgmatic.config.validate.repositories_match(
|
||||||
|
repository, borg_arguments.repository
|
||||||
|
):
|
||||||
|
logger.info('{}: Running arbitrary Borg command'.format(repository))
|
||||||
|
archive_name = borgmatic.borg.rlist.resolve_archive_name(
|
||||||
|
repository,
|
||||||
|
borg_arguments.archive,
|
||||||
|
storage,
|
||||||
|
local_borg_version,
|
||||||
|
local_path,
|
||||||
|
remote_path,
|
||||||
|
)
|
||||||
|
borgmatic.borg.borg.run_arbitrary_borg(
|
||||||
|
repository,
|
||||||
|
storage,
|
||||||
|
local_borg_version,
|
||||||
|
options=borg_arguments.options,
|
||||||
|
archive=archive_name,
|
||||||
|
local_path=local_path,
|
||||||
|
remote_path=remote_path,
|
||||||
|
)
|
|
@ -0,0 +1,21 @@
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import borgmatic.borg.break_lock
|
||||||
|
import borgmatic.config.validate
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def run_break_lock(
|
||||||
|
repository, storage, local_borg_version, break_lock_arguments, local_path, remote_path,
|
||||||
|
):
|
||||||
|
'''
|
||||||
|
Run the "break-lock" action for the given repository.
|
||||||
|
'''
|
||||||
|
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')
|
||||||
|
borgmatic.borg.break_lock.break_lock(
|
||||||
|
repository, storage, local_borg_version, local_path=local_path, remote_path=remote_path,
|
||||||
|
)
|
|
@ -0,0 +1,55 @@
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import borgmatic.borg.check
|
||||||
|
import borgmatic.hooks.command
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def run_check(
|
||||||
|
config_filename,
|
||||||
|
repository,
|
||||||
|
location,
|
||||||
|
storage,
|
||||||
|
consistency,
|
||||||
|
hooks,
|
||||||
|
hook_context,
|
||||||
|
local_borg_version,
|
||||||
|
check_arguments,
|
||||||
|
global_arguments,
|
||||||
|
local_path,
|
||||||
|
remote_path,
|
||||||
|
):
|
||||||
|
'''
|
||||||
|
Run the "check" action for the given repository.
|
||||||
|
'''
|
||||||
|
borgmatic.hooks.command.execute_hook(
|
||||||
|
hooks.get('before_check'),
|
||||||
|
hooks.get('umask'),
|
||||||
|
config_filename,
|
||||||
|
'pre-check',
|
||||||
|
global_arguments.dry_run,
|
||||||
|
**hook_context,
|
||||||
|
)
|
||||||
|
logger.info('{}: Running consistency checks'.format(repository))
|
||||||
|
borgmatic.borg.check.check_archives(
|
||||||
|
repository,
|
||||||
|
location,
|
||||||
|
storage,
|
||||||
|
consistency,
|
||||||
|
local_borg_version,
|
||||||
|
local_path=local_path,
|
||||||
|
remote_path=remote_path,
|
||||||
|
progress=check_arguments.progress,
|
||||||
|
repair=check_arguments.repair,
|
||||||
|
only_checks=check_arguments.only,
|
||||||
|
force=check_arguments.force,
|
||||||
|
)
|
||||||
|
borgmatic.hooks.command.execute_hook(
|
||||||
|
hooks.get('after_check'),
|
||||||
|
hooks.get('umask'),
|
||||||
|
config_filename,
|
||||||
|
'post-check',
|
||||||
|
global_arguments.dry_run,
|
||||||
|
**hook_context,
|
||||||
|
)
|
|
@ -0,0 +1,57 @@
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import borgmatic.borg.compact
|
||||||
|
import borgmatic.borg.feature
|
||||||
|
import borgmatic.hooks.command
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def run_compact(
|
||||||
|
config_filename,
|
||||||
|
repository,
|
||||||
|
storage,
|
||||||
|
retention,
|
||||||
|
hooks,
|
||||||
|
hook_context,
|
||||||
|
local_borg_version,
|
||||||
|
compact_arguments,
|
||||||
|
global_arguments,
|
||||||
|
dry_run_label,
|
||||||
|
local_path,
|
||||||
|
remote_path,
|
||||||
|
):
|
||||||
|
'''
|
||||||
|
Run the "compact" action for the given repository.
|
||||||
|
'''
|
||||||
|
borgmatic.hooks.command.execute_hook(
|
||||||
|
hooks.get('before_compact'),
|
||||||
|
hooks.get('umask'),
|
||||||
|
config_filename,
|
||||||
|
'pre-compact',
|
||||||
|
global_arguments.dry_run,
|
||||||
|
**hook_context,
|
||||||
|
)
|
||||||
|
if borgmatic.borg.feature.available(borgmatic.borg.feature.Feature.COMPACT, local_borg_version):
|
||||||
|
logger.info('{}: Compacting segments{}'.format(repository, dry_run_label))
|
||||||
|
borgmatic.borg.compact.compact_segments(
|
||||||
|
global_arguments.dry_run,
|
||||||
|
repository,
|
||||||
|
storage,
|
||||||
|
local_borg_version,
|
||||||
|
local_path=local_path,
|
||||||
|
remote_path=remote_path,
|
||||||
|
progress=compact_arguments.progress,
|
||||||
|
cleanup_commits=compact_arguments.cleanup_commits,
|
||||||
|
threshold=compact_arguments.threshold,
|
||||||
|
)
|
||||||
|
else: # pragma: nocover
|
||||||
|
logger.info('{}: Skipping compact (only available/needed in Borg 1.2+)'.format(repository))
|
||||||
|
borgmatic.hooks.command.execute_hook(
|
||||||
|
hooks.get('after_compact'),
|
||||||
|
hooks.get('umask'),
|
||||||
|
config_filename,
|
||||||
|
'post-compact',
|
||||||
|
global_arguments.dry_run,
|
||||||
|
**hook_context,
|
||||||
|
)
|
|
@ -0,0 +1,90 @@
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import borgmatic.borg.create
|
||||||
|
import borgmatic.hooks.command
|
||||||
|
import borgmatic.hooks.dispatch
|
||||||
|
import borgmatic.hooks.dump
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def run_create(
|
||||||
|
config_filename,
|
||||||
|
repository,
|
||||||
|
location,
|
||||||
|
storage,
|
||||||
|
hooks,
|
||||||
|
hook_context,
|
||||||
|
local_borg_version,
|
||||||
|
create_arguments,
|
||||||
|
global_arguments,
|
||||||
|
dry_run_label,
|
||||||
|
local_path,
|
||||||
|
remote_path,
|
||||||
|
):
|
||||||
|
'''
|
||||||
|
Run the "create" action for the given repository.
|
||||||
|
|
||||||
|
If create_arguments.json is True, yield the JSON output from creating the archive.
|
||||||
|
'''
|
||||||
|
borgmatic.hooks.command.execute_hook(
|
||||||
|
hooks.get('before_backup'),
|
||||||
|
hooks.get('umask'),
|
||||||
|
config_filename,
|
||||||
|
'pre-backup',
|
||||||
|
global_arguments.dry_run,
|
||||||
|
**hook_context,
|
||||||
|
)
|
||||||
|
logger.info('{}: Creating archive{}'.format(repository, dry_run_label))
|
||||||
|
borgmatic.hooks.dispatch.call_hooks_even_if_unconfigured(
|
||||||
|
'remove_database_dumps',
|
||||||
|
hooks,
|
||||||
|
repository,
|
||||||
|
borgmatic.hooks.dump.DATABASE_HOOK_NAMES,
|
||||||
|
location,
|
||||||
|
global_arguments.dry_run,
|
||||||
|
)
|
||||||
|
active_dumps = borgmatic.hooks.dispatch.call_hooks(
|
||||||
|
'dump_databases',
|
||||||
|
hooks,
|
||||||
|
repository,
|
||||||
|
borgmatic.hooks.dump.DATABASE_HOOK_NAMES,
|
||||||
|
location,
|
||||||
|
global_arguments.dry_run,
|
||||||
|
)
|
||||||
|
stream_processes = [process for processes in active_dumps.values() for process in processes]
|
||||||
|
|
||||||
|
json_output = borgmatic.borg.create.create_archive(
|
||||||
|
global_arguments.dry_run,
|
||||||
|
repository,
|
||||||
|
location,
|
||||||
|
storage,
|
||||||
|
local_borg_version,
|
||||||
|
local_path=local_path,
|
||||||
|
remote_path=remote_path,
|
||||||
|
progress=create_arguments.progress,
|
||||||
|
stats=create_arguments.stats,
|
||||||
|
json=create_arguments.json,
|
||||||
|
list_files=create_arguments.list_files,
|
||||||
|
stream_processes=stream_processes,
|
||||||
|
)
|
||||||
|
if json_output: # pragma: nocover
|
||||||
|
yield json.loads(json_output)
|
||||||
|
|
||||||
|
borgmatic.hooks.dispatch.call_hooks_even_if_unconfigured(
|
||||||
|
'remove_database_dumps',
|
||||||
|
hooks,
|
||||||
|
config_filename,
|
||||||
|
borgmatic.hooks.dump.DATABASE_HOOK_NAMES,
|
||||||
|
location,
|
||||||
|
global_arguments.dry_run,
|
||||||
|
)
|
||||||
|
borgmatic.hooks.command.execute_hook(
|
||||||
|
hooks.get('after_backup'),
|
||||||
|
hooks.get('umask'),
|
||||||
|
config_filename,
|
||||||
|
'post-backup',
|
||||||
|
global_arguments.dry_run,
|
||||||
|
**hook_context,
|
||||||
|
)
|
|
@ -0,0 +1,48 @@
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import borgmatic.borg.export_tar
|
||||||
|
import borgmatic.borg.rlist
|
||||||
|
import borgmatic.config.validate
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def run_export_tar(
|
||||||
|
repository,
|
||||||
|
storage,
|
||||||
|
local_borg_version,
|
||||||
|
export_tar_arguments,
|
||||||
|
global_arguments,
|
||||||
|
local_path,
|
||||||
|
remote_path,
|
||||||
|
):
|
||||||
|
'''
|
||||||
|
Run the "export-tar" action for the given repository.
|
||||||
|
'''
|
||||||
|
if export_tar_arguments.repository is None or borgmatic.config.validate.repositories_match(
|
||||||
|
repository, export_tar_arguments.repository
|
||||||
|
):
|
||||||
|
logger.info(
|
||||||
|
'{}: Exporting archive {} as tar file'.format(repository, export_tar_arguments.archive)
|
||||||
|
)
|
||||||
|
borgmatic.borg.export_tar.export_tar_archive(
|
||||||
|
global_arguments.dry_run,
|
||||||
|
repository,
|
||||||
|
borgmatic.borg.rlist.resolve_archive_name(
|
||||||
|
repository,
|
||||||
|
export_tar_arguments.archive,
|
||||||
|
storage,
|
||||||
|
local_borg_version,
|
||||||
|
local_path,
|
||||||
|
remote_path,
|
||||||
|
),
|
||||||
|
export_tar_arguments.paths,
|
||||||
|
export_tar_arguments.destination,
|
||||||
|
storage,
|
||||||
|
local_borg_version,
|
||||||
|
local_path=local_path,
|
||||||
|
remote_path=remote_path,
|
||||||
|
tar_filter=export_tar_arguments.tar_filter,
|
||||||
|
list_files=export_tar_arguments.list_files,
|
||||||
|
strip_components=export_tar_arguments.strip_components,
|
||||||
|
)
|
|
@ -0,0 +1,67 @@
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import borgmatic.borg.extract
|
||||||
|
import borgmatic.borg.rlist
|
||||||
|
import borgmatic.config.validate
|
||||||
|
import borgmatic.hooks.command
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def run_extract(
|
||||||
|
config_filename,
|
||||||
|
repository,
|
||||||
|
location,
|
||||||
|
storage,
|
||||||
|
hooks,
|
||||||
|
hook_context,
|
||||||
|
local_borg_version,
|
||||||
|
extract_arguments,
|
||||||
|
global_arguments,
|
||||||
|
local_path,
|
||||||
|
remote_path,
|
||||||
|
):
|
||||||
|
'''
|
||||||
|
Run the "extract" action for the given repository.
|
||||||
|
'''
|
||||||
|
borgmatic.hooks.command.execute_hook(
|
||||||
|
hooks.get('before_extract'),
|
||||||
|
hooks.get('umask'),
|
||||||
|
config_filename,
|
||||||
|
'pre-extract',
|
||||||
|
global_arguments.dry_run,
|
||||||
|
**hook_context,
|
||||||
|
)
|
||||||
|
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))
|
||||||
|
borgmatic.borg.extract.extract_archive(
|
||||||
|
global_arguments.dry_run,
|
||||||
|
repository,
|
||||||
|
borgmatic.borg.rlist.resolve_archive_name(
|
||||||
|
repository,
|
||||||
|
extract_arguments.archive,
|
||||||
|
storage,
|
||||||
|
local_borg_version,
|
||||||
|
local_path,
|
||||||
|
remote_path,
|
||||||
|
),
|
||||||
|
extract_arguments.paths,
|
||||||
|
location,
|
||||||
|
storage,
|
||||||
|
local_borg_version,
|
||||||
|
local_path=local_path,
|
||||||
|
remote_path=remote_path,
|
||||||
|
destination_path=extract_arguments.destination,
|
||||||
|
strip_components=extract_arguments.strip_components,
|
||||||
|
progress=extract_arguments.progress,
|
||||||
|
)
|
||||||
|
borgmatic.hooks.command.execute_hook(
|
||||||
|
hooks.get('after_extract'),
|
||||||
|
hooks.get('umask'),
|
||||||
|
config_filename,
|
||||||
|
'post-extract',
|
||||||
|
global_arguments.dry_run,
|
||||||
|
**hook_context,
|
||||||
|
)
|
|
@ -0,0 +1,41 @@
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import borgmatic.borg.info
|
||||||
|
import borgmatic.borg.rlist
|
||||||
|
import borgmatic.config.validate
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def run_info(
|
||||||
|
repository, storage, local_borg_version, info_arguments, local_path, remote_path,
|
||||||
|
):
|
||||||
|
'''
|
||||||
|
Run the "info" action for the given repository and archive.
|
||||||
|
|
||||||
|
If info_arguments.json is True, yield the JSON output from the info for the archive.
|
||||||
|
'''
|
||||||
|
if info_arguments.repository is None or borgmatic.config.validate.repositories_match(
|
||||||
|
repository, info_arguments.repository
|
||||||
|
):
|
||||||
|
if not info_arguments.json: # pragma: nocover
|
||||||
|
logger.answer(f'{repository}: Displaying archive summary information')
|
||||||
|
info_arguments.archive = borgmatic.borg.rlist.resolve_archive_name(
|
||||||
|
repository,
|
||||||
|
info_arguments.archive,
|
||||||
|
storage,
|
||||||
|
local_borg_version,
|
||||||
|
local_path,
|
||||||
|
remote_path,
|
||||||
|
)
|
||||||
|
json_output = borgmatic.borg.info.display_archives_info(
|
||||||
|
repository,
|
||||||
|
storage,
|
||||||
|
local_borg_version,
|
||||||
|
info_arguments=info_arguments,
|
||||||
|
local_path=local_path,
|
||||||
|
remote_path=remote_path,
|
||||||
|
)
|
||||||
|
if json_output: # pragma: nocover
|
||||||
|
yield json.loads(json_output)
|
|
@ -0,0 +1,43 @@
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import borgmatic.borg.list
|
||||||
|
import borgmatic.config.validate
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def run_list(
|
||||||
|
repository, storage, local_borg_version, list_arguments, local_path, remote_path,
|
||||||
|
):
|
||||||
|
'''
|
||||||
|
Run the "list" action for the given repository and archive.
|
||||||
|
|
||||||
|
If list_arguments.json is True, yield the JSON output from listing the archive.
|
||||||
|
'''
|
||||||
|
if list_arguments.repository is None or borgmatic.config.validate.repositories_match(
|
||||||
|
repository, list_arguments.repository
|
||||||
|
):
|
||||||
|
if not list_arguments.json: # pragma: nocover
|
||||||
|
if list_arguments.find_paths:
|
||||||
|
logger.answer(f'{repository}: Searching archives')
|
||||||
|
elif not list_arguments.archive:
|
||||||
|
logger.answer(f'{repository}: Listing archives')
|
||||||
|
list_arguments.archive = borgmatic.borg.rlist.resolve_archive_name(
|
||||||
|
repository,
|
||||||
|
list_arguments.archive,
|
||||||
|
storage,
|
||||||
|
local_borg_version,
|
||||||
|
local_path,
|
||||||
|
remote_path,
|
||||||
|
)
|
||||||
|
json_output = borgmatic.borg.list.list_archive(
|
||||||
|
repository,
|
||||||
|
storage,
|
||||||
|
local_borg_version,
|
||||||
|
list_arguments=list_arguments,
|
||||||
|
local_path=local_path,
|
||||||
|
remote_path=remote_path,
|
||||||
|
)
|
||||||
|
if json_output: # pragma: nocover
|
||||||
|
yield json.loads(json_output)
|
|
@ -0,0 +1,42 @@
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import borgmatic.borg.mount
|
||||||
|
import borgmatic.borg.rlist
|
||||||
|
import borgmatic.config.validate
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def run_mount(
|
||||||
|
repository, storage, local_borg_version, mount_arguments, local_path, remote_path,
|
||||||
|
):
|
||||||
|
'''
|
||||||
|
Run the "mount" action for the given repository.
|
||||||
|
'''
|
||||||
|
if mount_arguments.repository is None or borgmatic.config.validate.repositories_match(
|
||||||
|
repository, mount_arguments.repository
|
||||||
|
):
|
||||||
|
if mount_arguments.archive:
|
||||||
|
logger.info('{}: Mounting archive {}'.format(repository, mount_arguments.archive))
|
||||||
|
else: # pragma: nocover
|
||||||
|
logger.info('{}: Mounting repository'.format(repository))
|
||||||
|
|
||||||
|
borgmatic.borg.mount.mount_archive(
|
||||||
|
repository,
|
||||||
|
borgmatic.borg.rlist.resolve_archive_name(
|
||||||
|
repository,
|
||||||
|
mount_arguments.archive,
|
||||||
|
storage,
|
||||||
|
local_borg_version,
|
||||||
|
local_path,
|
||||||
|
remote_path,
|
||||||
|
),
|
||||||
|
mount_arguments.mount_point,
|
||||||
|
mount_arguments.paths,
|
||||||
|
mount_arguments.foreground,
|
||||||
|
mount_arguments.options,
|
||||||
|
storage,
|
||||||
|
local_borg_version,
|
||||||
|
local_path=local_path,
|
||||||
|
remote_path=remote_path,
|
||||||
|
)
|
|
@ -0,0 +1,53 @@
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import borgmatic.borg.prune
|
||||||
|
import borgmatic.hooks.command
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def run_prune(
|
||||||
|
config_filename,
|
||||||
|
repository,
|
||||||
|
storage,
|
||||||
|
retention,
|
||||||
|
hooks,
|
||||||
|
hook_context,
|
||||||
|
local_borg_version,
|
||||||
|
prune_arguments,
|
||||||
|
global_arguments,
|
||||||
|
dry_run_label,
|
||||||
|
local_path,
|
||||||
|
remote_path,
|
||||||
|
):
|
||||||
|
'''
|
||||||
|
Run the "prune" action for the given repository.
|
||||||
|
'''
|
||||||
|
borgmatic.hooks.command.execute_hook(
|
||||||
|
hooks.get('before_prune'),
|
||||||
|
hooks.get('umask'),
|
||||||
|
config_filename,
|
||||||
|
'pre-prune',
|
||||||
|
global_arguments.dry_run,
|
||||||
|
**hook_context,
|
||||||
|
)
|
||||||
|
logger.info('{}: Pruning archives{}'.format(repository, dry_run_label))
|
||||||
|
borgmatic.borg.prune.prune_archives(
|
||||||
|
global_arguments.dry_run,
|
||||||
|
repository,
|
||||||
|
storage,
|
||||||
|
retention,
|
||||||
|
local_borg_version,
|
||||||
|
local_path=local_path,
|
||||||
|
remote_path=remote_path,
|
||||||
|
stats=prune_arguments.stats,
|
||||||
|
list_archives=prune_arguments.list_archives,
|
||||||
|
)
|
||||||
|
borgmatic.hooks.command.execute_hook(
|
||||||
|
hooks.get('after_prune'),
|
||||||
|
hooks.get('umask'),
|
||||||
|
config_filename,
|
||||||
|
'post-prune',
|
||||||
|
global_arguments.dry_run,
|
||||||
|
**hook_context,
|
||||||
|
)
|
|
@ -0,0 +1,40 @@
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import borgmatic.borg.rcreate
|
||||||
|
import borgmatic.config.validate
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def run_rcreate(
|
||||||
|
repository,
|
||||||
|
storage,
|
||||||
|
local_borg_version,
|
||||||
|
rcreate_arguments,
|
||||||
|
global_arguments,
|
||||||
|
local_path,
|
||||||
|
remote_path,
|
||||||
|
):
|
||||||
|
'''
|
||||||
|
Run the "rcreate" action for the given repository.
|
||||||
|
'''
|
||||||
|
if rcreate_arguments.repository and not borgmatic.config.validate.repositories_match(
|
||||||
|
repository, rcreate_arguments.repository
|
||||||
|
):
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.info('{}: Creating repository'.format(repository))
|
||||||
|
borgmatic.borg.rcreate.create_repository(
|
||||||
|
global_arguments.dry_run,
|
||||||
|
repository,
|
||||||
|
storage,
|
||||||
|
local_borg_version,
|
||||||
|
rcreate_arguments.encryption_mode,
|
||||||
|
rcreate_arguments.source_repository,
|
||||||
|
rcreate_arguments.copy_crypt_key,
|
||||||
|
rcreate_arguments.append_only,
|
||||||
|
rcreate_arguments.storage_quota,
|
||||||
|
rcreate_arguments.make_parent_dirs,
|
||||||
|
local_path=local_path,
|
||||||
|
remote_path=remote_path,
|
||||||
|
)
|
|
@ -0,0 +1,345 @@
|
||||||
|
import copy
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
|
||||||
|
import borgmatic.borg.extract
|
||||||
|
import borgmatic.borg.list
|
||||||
|
import borgmatic.borg.mount
|
||||||
|
import borgmatic.borg.rlist
|
||||||
|
import borgmatic.borg.state
|
||||||
|
import borgmatic.config.validate
|
||||||
|
import borgmatic.hooks.dispatch
|
||||||
|
import borgmatic.hooks.dump
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
UNSPECIFIED_HOOK = object()
|
||||||
|
|
||||||
|
|
||||||
|
def get_configured_database(
|
||||||
|
hooks, archive_database_names, hook_name, database_name, configuration_database_name=None
|
||||||
|
):
|
||||||
|
'''
|
||||||
|
Find the first database with the given hook name and database name in the configured hooks
|
||||||
|
dict and the given archive database names dict (from hook name to database names contained in
|
||||||
|
a particular backup archive). If UNSPECIFIED_HOOK is given as the hook name, search all database
|
||||||
|
hooks for the named database. If a configuration database name is given, use that instead of the
|
||||||
|
database name to lookup the database in the given hooks configuration.
|
||||||
|
|
||||||
|
Return the found database as a tuple of (found hook name, database configuration dict).
|
||||||
|
'''
|
||||||
|
if not configuration_database_name:
|
||||||
|
configuration_database_name = database_name
|
||||||
|
|
||||||
|
if hook_name == UNSPECIFIED_HOOK:
|
||||||
|
hooks_to_search = hooks
|
||||||
|
else:
|
||||||
|
hooks_to_search = {hook_name: hooks[hook_name]}
|
||||||
|
|
||||||
|
return next(
|
||||||
|
(
|
||||||
|
(name, hook_database)
|
||||||
|
for (name, hook) in hooks_to_search.items()
|
||||||
|
for hook_database in hook
|
||||||
|
if hook_database['name'] == configuration_database_name
|
||||||
|
and database_name in archive_database_names.get(name, [])
|
||||||
|
),
|
||||||
|
(None, None),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_configured_hook_name_and_database(hooks, database_name):
|
||||||
|
'''
|
||||||
|
Find the hook name and first database dict with the given database name in the configured hooks
|
||||||
|
dict. This searches across all database hooks.
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
def restore_single_database(
|
||||||
|
repository,
|
||||||
|
location,
|
||||||
|
storage,
|
||||||
|
hooks,
|
||||||
|
local_borg_version,
|
||||||
|
global_arguments,
|
||||||
|
local_path,
|
||||||
|
remote_path,
|
||||||
|
archive_name,
|
||||||
|
hook_name,
|
||||||
|
database,
|
||||||
|
): # pragma: no cover
|
||||||
|
'''
|
||||||
|
Given (among other things) an archive name, a database hook name, and a configured database
|
||||||
|
configuration dict, restore that database from the archive.
|
||||||
|
'''
|
||||||
|
logger.info(f'{repository}: Restoring database {database["name"]}')
|
||||||
|
|
||||||
|
dump_pattern = borgmatic.hooks.dispatch.call_hooks(
|
||||||
|
'make_database_dump_pattern',
|
||||||
|
hooks,
|
||||||
|
repository,
|
||||||
|
borgmatic.hooks.dump.DATABASE_HOOK_NAMES,
|
||||||
|
location,
|
||||||
|
database['name'],
|
||||||
|
)[hook_name]
|
||||||
|
|
||||||
|
# Kick off a single database extract to stdout.
|
||||||
|
extract_process = borgmatic.borg.extract.extract_archive(
|
||||||
|
dry_run=global_arguments.dry_run,
|
||||||
|
repository=repository,
|
||||||
|
archive=archive_name,
|
||||||
|
paths=borgmatic.hooks.dump.convert_glob_patterns_to_borg_patterns([dump_pattern]),
|
||||||
|
location_config=location,
|
||||||
|
storage_config=storage,
|
||||||
|
local_borg_version=local_borg_version,
|
||||||
|
local_path=local_path,
|
||||||
|
remote_path=remote_path,
|
||||||
|
destination_path='/',
|
||||||
|
# A directory format dump isn't a single file, and therefore can't extract
|
||||||
|
# to stdout. In this case, the extract_process return value is None.
|
||||||
|
extract_to_stdout=bool(database.get('format') != 'directory'),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Run a single database restore, consuming the extract stdout (if any).
|
||||||
|
borgmatic.hooks.dispatch.call_hooks(
|
||||||
|
'restore_database_dump',
|
||||||
|
{hook_name: [database]},
|
||||||
|
repository,
|
||||||
|
borgmatic.hooks.dump.DATABASE_HOOK_NAMES,
|
||||||
|
location,
|
||||||
|
global_arguments.dry_run,
|
||||||
|
extract_process,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def collect_archive_database_names(
|
||||||
|
repository, archive, location, storage, local_borg_version, local_path, remote_path,
|
||||||
|
):
|
||||||
|
'''
|
||||||
|
Given a local or remote repository path, a resolved archive name, a location configuration dict,
|
||||||
|
a storage configuration dict, the local Borg version, and local and remote Borg paths, query the
|
||||||
|
archive for the names of databases it contains and return them as a dict from hook name to a
|
||||||
|
sequence of database names.
|
||||||
|
'''
|
||||||
|
borgmatic_source_directory = os.path.expanduser(
|
||||||
|
location.get(
|
||||||
|
'borgmatic_source_directory', borgmatic.borg.state.DEFAULT_BORGMATIC_SOURCE_DIRECTORY
|
||||||
|
)
|
||||||
|
).lstrip('/')
|
||||||
|
parent_dump_path = os.path.expanduser(
|
||||||
|
borgmatic.hooks.dump.make_database_dump_path(borgmatic_source_directory, '*_databases/*/*')
|
||||||
|
)
|
||||||
|
dump_paths = borgmatic.borg.list.capture_archive_listing(
|
||||||
|
repository,
|
||||||
|
archive,
|
||||||
|
storage,
|
||||||
|
local_borg_version,
|
||||||
|
list_path=parent_dump_path,
|
||||||
|
local_path=local_path,
|
||||||
|
remote_path=remote_path,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Determine the database names corresponding to the dumps found in the archive and
|
||||||
|
# add them to restore_names.
|
||||||
|
archive_database_names = {}
|
||||||
|
|
||||||
|
for dump_path in dump_paths:
|
||||||
|
try:
|
||||||
|
(hook_name, _, database_name) = dump_path.split(
|
||||||
|
borgmatic_source_directory + os.path.sep, 1
|
||||||
|
)[1].split(os.path.sep)[0:3]
|
||||||
|
except (ValueError, IndexError):
|
||||||
|
logger.warning(
|
||||||
|
f'{repository}: Ignoring invalid database dump path "{dump_path}" in archive {archive}'
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
if database_name not in archive_database_names.get(hook_name, []):
|
||||||
|
archive_database_names.setdefault(hook_name, []).extend([database_name])
|
||||||
|
|
||||||
|
return archive_database_names
|
||||||
|
|
||||||
|
|
||||||
|
def find_databases_to_restore(requested_database_names, archive_database_names):
|
||||||
|
'''
|
||||||
|
Given a sequence of requested database names to restore and a dict of hook name to the names of
|
||||||
|
databases found in an archive, return an expanded sequence of database names to restore,
|
||||||
|
replacing "all" with actual database names as appropriate.
|
||||||
|
|
||||||
|
Raise ValueError if any of the requested database names cannot be found in the archive.
|
||||||
|
'''
|
||||||
|
# A map from database hook name to the database names to restore for that hook.
|
||||||
|
restore_names = (
|
||||||
|
{UNSPECIFIED_HOOK: requested_database_names}
|
||||||
|
if requested_database_names
|
||||||
|
else {UNSPECIFIED_HOOK: ['all']}
|
||||||
|
)
|
||||||
|
|
||||||
|
# If "all" is in restore_names, then replace it with the names of dumps found within the
|
||||||
|
# archive.
|
||||||
|
if 'all' in restore_names[UNSPECIFIED_HOOK]:
|
||||||
|
restore_names[UNSPECIFIED_HOOK].remove('all')
|
||||||
|
|
||||||
|
for (hook_name, database_names) in archive_database_names.items():
|
||||||
|
restore_names.setdefault(hook_name, []).extend(database_names)
|
||||||
|
|
||||||
|
# If a database is to be restored as part of "all", then remove it from restore names so
|
||||||
|
# it doesn't get restored twice.
|
||||||
|
for database_name in database_names:
|
||||||
|
if database_name in restore_names[UNSPECIFIED_HOOK]:
|
||||||
|
restore_names[UNSPECIFIED_HOOK].remove(database_name)
|
||||||
|
|
||||||
|
if not restore_names[UNSPECIFIED_HOOK]:
|
||||||
|
restore_names.pop(UNSPECIFIED_HOOK)
|
||||||
|
|
||||||
|
combined_restore_names = set(
|
||||||
|
name for database_names in restore_names.values() for name in database_names
|
||||||
|
)
|
||||||
|
combined_archive_database_names = set(
|
||||||
|
name for database_names in archive_database_names.values() for name in database_names
|
||||||
|
)
|
||||||
|
|
||||||
|
missing_names = sorted(set(combined_restore_names) - combined_archive_database_names)
|
||||||
|
if missing_names:
|
||||||
|
joined_names = ', '.join(f'"{name}"' for name in missing_names)
|
||||||
|
raise ValueError(
|
||||||
|
f"Cannot restore database{'s' if len(missing_names) > 1 else ''} {joined_names} missing from archive"
|
||||||
|
)
|
||||||
|
|
||||||
|
return restore_names
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_databases_found(restore_names, remaining_restore_names, found_names):
|
||||||
|
'''
|
||||||
|
Given a dict from hook name to database names to restore, a dict from hook name to remaining
|
||||||
|
database names to restore, and a sequence of found (actually restored) database names, raise
|
||||||
|
ValueError if requested databases to restore were missing from the archive and/or configuration.
|
||||||
|
'''
|
||||||
|
combined_restore_names = set(
|
||||||
|
name
|
||||||
|
for database_names in tuple(restore_names.values())
|
||||||
|
+ tuple(remaining_restore_names.values())
|
||||||
|
for name in database_names
|
||||||
|
)
|
||||||
|
|
||||||
|
if not combined_restore_names and not found_names:
|
||||||
|
raise ValueError('No databases were found to restore')
|
||||||
|
|
||||||
|
missing_names = sorted(set(combined_restore_names) - set(found_names))
|
||||||
|
if missing_names:
|
||||||
|
joined_names = ', '.join(f'"{name}"' for name in missing_names)
|
||||||
|
raise ValueError(
|
||||||
|
f"Cannot restore database{'s' if len(missing_names) > 1 else ''} {joined_names} missing from borgmatic's configuration"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def run_restore(
|
||||||
|
repository,
|
||||||
|
location,
|
||||||
|
storage,
|
||||||
|
hooks,
|
||||||
|
local_borg_version,
|
||||||
|
restore_arguments,
|
||||||
|
global_arguments,
|
||||||
|
local_path,
|
||||||
|
remote_path,
|
||||||
|
):
|
||||||
|
'''
|
||||||
|
Run the "restore" action for the given repository, but only if the repository matches the
|
||||||
|
requested repository in restore arguments.
|
||||||
|
|
||||||
|
Raise ValueError if a configured database could not be found to restore.
|
||||||
|
'''
|
||||||
|
if restore_arguments.repository and not borgmatic.config.validate.repositories_match(
|
||||||
|
repository, restore_arguments.repository
|
||||||
|
):
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
'{}: Restoring databases from archive {}'.format(repository, restore_arguments.archive)
|
||||||
|
)
|
||||||
|
borgmatic.hooks.dispatch.call_hooks_even_if_unconfigured(
|
||||||
|
'remove_database_dumps',
|
||||||
|
hooks,
|
||||||
|
repository,
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
archive_database_names = collect_archive_database_names(
|
||||||
|
repository, 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()
|
||||||
|
remaining_restore_names = {}
|
||||||
|
|
||||||
|
for hook_name, database_names in restore_names.items():
|
||||||
|
for database_name in database_names:
|
||||||
|
found_hook_name, found_database = get_configured_database(
|
||||||
|
hooks, archive_database_names, hook_name, database_name
|
||||||
|
)
|
||||||
|
|
||||||
|
if not found_database:
|
||||||
|
remaining_restore_names.setdefault(found_hook_name or hook_name, []).append(
|
||||||
|
database_name
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
found_names.add(database_name)
|
||||||
|
restore_single_database(
|
||||||
|
repository,
|
||||||
|
location,
|
||||||
|
storage,
|
||||||
|
hooks,
|
||||||
|
local_borg_version,
|
||||||
|
global_arguments,
|
||||||
|
local_path,
|
||||||
|
remote_path,
|
||||||
|
archive_name,
|
||||||
|
found_hook_name or hook_name,
|
||||||
|
found_database,
|
||||||
|
)
|
||||||
|
|
||||||
|
# For any database that weren't found via exact matches in the hooks configuration, try to
|
||||||
|
# fallback to "all" entries.
|
||||||
|
for hook_name, database_names in remaining_restore_names.items():
|
||||||
|
for database_name in database_names:
|
||||||
|
found_hook_name, found_database = get_configured_database(
|
||||||
|
hooks, archive_database_names, hook_name, database_name, 'all'
|
||||||
|
)
|
||||||
|
|
||||||
|
if not found_database:
|
||||||
|
continue
|
||||||
|
|
||||||
|
found_names.add(database_name)
|
||||||
|
database = copy.copy(found_database)
|
||||||
|
database['name'] = database_name
|
||||||
|
|
||||||
|
restore_single_database(
|
||||||
|
repository,
|
||||||
|
location,
|
||||||
|
storage,
|
||||||
|
hooks,
|
||||||
|
local_borg_version,
|
||||||
|
global_arguments,
|
||||||
|
local_path,
|
||||||
|
remote_path,
|
||||||
|
archive_name,
|
||||||
|
found_hook_name or hook_name,
|
||||||
|
database,
|
||||||
|
)
|
||||||
|
|
||||||
|
borgmatic.hooks.dispatch.call_hooks_even_if_unconfigured(
|
||||||
|
'remove_database_dumps',
|
||||||
|
hooks,
|
||||||
|
repository,
|
||||||
|
borgmatic.hooks.dump.DATABASE_HOOK_NAMES,
|
||||||
|
location,
|
||||||
|
global_arguments.dry_run,
|
||||||
|
)
|
||||||
|
|
||||||
|
ensure_databases_found(restore_names, remaining_restore_names, found_names)
|
|
@ -0,0 +1,32 @@
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import borgmatic.borg.rinfo
|
||||||
|
import borgmatic.config.validate
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def run_rinfo(
|
||||||
|
repository, storage, local_borg_version, rinfo_arguments, local_path, remote_path,
|
||||||
|
):
|
||||||
|
'''
|
||||||
|
Run the "rinfo" action for the given repository.
|
||||||
|
|
||||||
|
If rinfo_arguments.json is True, yield the JSON output from the info for the repository.
|
||||||
|
'''
|
||||||
|
if rinfo_arguments.repository is None or borgmatic.config.validate.repositories_match(
|
||||||
|
repository, rinfo_arguments.repository
|
||||||
|
):
|
||||||
|
if not rinfo_arguments.json: # pragma: nocover
|
||||||
|
logger.answer('{}: Displaying repository summary information'.format(repository))
|
||||||
|
json_output = borgmatic.borg.rinfo.display_repository_info(
|
||||||
|
repository,
|
||||||
|
storage,
|
||||||
|
local_borg_version,
|
||||||
|
rinfo_arguments=rinfo_arguments,
|
||||||
|
local_path=local_path,
|
||||||
|
remote_path=remote_path,
|
||||||
|
)
|
||||||
|
if json_output: # pragma: nocover
|
||||||
|
yield json.loads(json_output)
|
|
@ -0,0 +1,32 @@
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import borgmatic.borg.rlist
|
||||||
|
import borgmatic.config.validate
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def run_rlist(
|
||||||
|
repository, storage, local_borg_version, rlist_arguments, local_path, remote_path,
|
||||||
|
):
|
||||||
|
'''
|
||||||
|
Run the "rlist" action for the given repository.
|
||||||
|
|
||||||
|
If rlist_arguments.json is True, yield the JSON output from listing the repository.
|
||||||
|
'''
|
||||||
|
if rlist_arguments.repository is None or borgmatic.config.validate.repositories_match(
|
||||||
|
repository, rlist_arguments.repository
|
||||||
|
):
|
||||||
|
if not rlist_arguments.json: # pragma: nocover
|
||||||
|
logger.answer('{}: Listing repository'.format(repository))
|
||||||
|
json_output = borgmatic.borg.rlist.list_repository(
|
||||||
|
repository,
|
||||||
|
storage,
|
||||||
|
local_borg_version,
|
||||||
|
rlist_arguments=rlist_arguments,
|
||||||
|
local_path=local_path,
|
||||||
|
remote_path=remote_path,
|
||||||
|
)
|
||||||
|
if json_output: # pragma: nocover
|
||||||
|
yield json.loads(json_output)
|
|
@ -0,0 +1,29 @@
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import borgmatic.borg.transfer
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def run_transfer(
|
||||||
|
repository,
|
||||||
|
storage,
|
||||||
|
local_borg_version,
|
||||||
|
transfer_arguments,
|
||||||
|
global_arguments,
|
||||||
|
local_path,
|
||||||
|
remote_path,
|
||||||
|
):
|
||||||
|
'''
|
||||||
|
Run the "transfer" action for the given repository.
|
||||||
|
'''
|
||||||
|
logger.info(f'{repository}: Transferring archives to repository')
|
||||||
|
borgmatic.borg.transfer.transfer_archives(
|
||||||
|
global_arguments.dry_run,
|
||||||
|
repository,
|
||||||
|
storage,
|
||||||
|
local_borg_version,
|
||||||
|
transfer_arguments,
|
||||||
|
local_path=local_path,
|
||||||
|
remote_path=remote_path,
|
||||||
|
)
|
|
@ -1,5 +1,6 @@
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
import borgmatic.logger
|
||||||
from borgmatic.borg import environment, flags
|
from borgmatic.borg import environment, flags
|
||||||
from borgmatic.execute import execute_command
|
from borgmatic.execute import execute_command
|
||||||
|
|
||||||
|
@ -25,6 +26,7 @@ def run_arbitrary_borg(
|
||||||
sequence of arbitrary command-line Borg options, and an optional archive name, run an arbitrary
|
sequence of arbitrary command-line Borg options, and an optional archive name, run an arbitrary
|
||||||
Borg command on the given repository/archive.
|
Borg command on the given repository/archive.
|
||||||
'''
|
'''
|
||||||
|
borgmatic.logger.add_custom_log_levels()
|
||||||
lock_wait = storage_config.get('lock_wait', None)
|
lock_wait = storage_config.get('lock_wait', None)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -60,7 +62,7 @@ def run_arbitrary_borg(
|
||||||
|
|
||||||
return execute_command(
|
return execute_command(
|
||||||
full_command,
|
full_command,
|
||||||
output_log_level=logging.WARNING,
|
output_log_level=logging.ANSWER,
|
||||||
borg_local_path=local_path,
|
borg_local_path=local_path,
|
||||||
extra_environment=environment.make_environment(storage_config),
|
extra_environment=environment.make_environment(storage_config),
|
||||||
)
|
)
|
||||||
|
|
|
@ -139,7 +139,7 @@ def filter_checks_on_frequency(
|
||||||
if datetime.datetime.now() < check_time + frequency_delta:
|
if datetime.datetime.now() < check_time + frequency_delta:
|
||||||
remaining = check_time + frequency_delta - datetime.datetime.now()
|
remaining = check_time + frequency_delta - datetime.datetime.now()
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Skipping {check} check due to configured frequency; {remaining} until next check"
|
f'Skipping {check} check due to configured frequency; {remaining} until next check'
|
||||||
)
|
)
|
||||||
filtered_checks.remove(check)
|
filtered_checks.remove(check)
|
||||||
|
|
||||||
|
@ -166,6 +166,12 @@ def make_check_flags(local_borg_version, checks, check_last=None, prefix=None):
|
||||||
"--last" flag. And if a prefix value is given and "archives" is in checks, then include a
|
"--last" flag. And if a prefix value is given and "archives" is in checks, then include a
|
||||||
"--match-archives" flag.
|
"--match-archives" flag.
|
||||||
'''
|
'''
|
||||||
|
if 'data' in checks:
|
||||||
|
data_flags = ('--verify-data',)
|
||||||
|
checks += ('archives',)
|
||||||
|
else:
|
||||||
|
data_flags = ()
|
||||||
|
|
||||||
if 'archives' in checks:
|
if 'archives' in checks:
|
||||||
last_flags = ('--last', str(check_last)) if check_last else ()
|
last_flags = ('--last', str(check_last)) if check_last else ()
|
||||||
if feature.available(feature.Feature.MATCH_ARCHIVES, local_borg_version):
|
if feature.available(feature.Feature.MATCH_ARCHIVES, local_borg_version):
|
||||||
|
@ -176,17 +182,13 @@ def make_check_flags(local_borg_version, checks, check_last=None, prefix=None):
|
||||||
last_flags = ()
|
last_flags = ()
|
||||||
match_archives_flags = ()
|
match_archives_flags = ()
|
||||||
if check_last:
|
if check_last:
|
||||||
logger.info('Ignoring check_last option, as "archives" is not in consistency checks')
|
logger.warning(
|
||||||
if prefix:
|
'Ignoring check_last option, as "archives" or "data" are not in consistency checks'
|
||||||
logger.info(
|
)
|
||||||
'Ignoring consistency prefix option, as "archives" is not in consistency checks'
|
if prefix:
|
||||||
|
logger.warning(
|
||||||
|
'Ignoring consistency prefix option, as "archives" or "data" are not in consistency checks'
|
||||||
)
|
)
|
||||||
|
|
||||||
if 'data' in checks:
|
|
||||||
data_flags = ('--verify-data',)
|
|
||||||
checks += ('archives',)
|
|
||||||
else:
|
|
||||||
data_flags = ()
|
|
||||||
|
|
||||||
common_flags = last_flags + match_archives_flags + data_flags
|
common_flags = last_flags + match_archives_flags + data_flags
|
||||||
|
|
||||||
|
|
|
@ -6,8 +6,14 @@ import pathlib
|
||||||
import stat
|
import stat
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
|
import borgmatic.logger
|
||||||
from borgmatic.borg import environment, feature, flags, state
|
from borgmatic.borg import environment, feature, flags, state
|
||||||
from borgmatic.execute import DO_NOT_CAPTURE, execute_command, execute_command_with_processes
|
from borgmatic.execute import (
|
||||||
|
DO_NOT_CAPTURE,
|
||||||
|
execute_command,
|
||||||
|
execute_command_and_capture_output,
|
||||||
|
execute_command_with_processes,
|
||||||
|
)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -190,6 +196,27 @@ def make_exclude_flags(location_config, exclude_filename=None):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def make_list_filter_flags(local_borg_version, dry_run):
|
||||||
|
'''
|
||||||
|
Given the local Borg version and whether this is a dry run, return the corresponding flags for
|
||||||
|
passing to "--list --filter". The general idea is that excludes are shown for a dry run or when
|
||||||
|
the verbosity is debug.
|
||||||
|
'''
|
||||||
|
base_flags = 'AME'
|
||||||
|
show_excludes = logger.isEnabledFor(logging.DEBUG)
|
||||||
|
|
||||||
|
if feature.available(feature.Feature.EXCLUDED_FILES_MINUS, local_borg_version):
|
||||||
|
if show_excludes or dry_run:
|
||||||
|
return f'{base_flags}+-'
|
||||||
|
else:
|
||||||
|
return base_flags
|
||||||
|
|
||||||
|
if show_excludes:
|
||||||
|
return f'{base_flags}x-'
|
||||||
|
else:
|
||||||
|
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}'
|
||||||
|
|
||||||
|
|
||||||
|
@ -229,7 +256,11 @@ def special_file(path):
|
||||||
Return whether the given path is a special file (character device, block device, or named pipe
|
Return whether the given path is a special file (character device, block device, or named pipe
|
||||||
/ FIFO).
|
/ FIFO).
|
||||||
'''
|
'''
|
||||||
mode = os.stat(path).st_mode
|
try:
|
||||||
|
mode = os.stat(path).st_mode
|
||||||
|
except (FileNotFoundError, OSError):
|
||||||
|
return False
|
||||||
|
|
||||||
return stat.S_ISCHR(mode) or stat.S_ISBLK(mode) or stat.S_ISFIFO(mode)
|
return stat.S_ISCHR(mode) or stat.S_ISBLK(mode) or stat.S_ISFIFO(mode)
|
||||||
|
|
||||||
|
|
||||||
|
@ -255,10 +286,9 @@ def collect_special_file_paths(
|
||||||
Borg would encounter during a create. These are all paths that could cause Borg to hang if its
|
Borg would encounter during a create. These are all paths that could cause Borg to hang if its
|
||||||
--read-special flag is used.
|
--read-special flag is used.
|
||||||
'''
|
'''
|
||||||
paths_output = execute_command(
|
paths_output = execute_command_and_capture_output(
|
||||||
create_command + ('--dry-run', '--list'),
|
create_command + ('--dry-run', '--list'),
|
||||||
output_log_level=None,
|
capture_stderr=True,
|
||||||
borg_local_path=local_path,
|
|
||||||
working_directory=working_directory,
|
working_directory=working_directory,
|
||||||
extra_environment=borg_environment,
|
extra_environment=borg_environment,
|
||||||
)
|
)
|
||||||
|
@ -266,7 +296,7 @@ def collect_special_file_paths(
|
||||||
paths = tuple(
|
paths = tuple(
|
||||||
path_line.split(' ', 1)[1]
|
path_line.split(' ', 1)[1]
|
||||||
for path_line in paths_output.split('\n')
|
for path_line in paths_output.split('\n')
|
||||||
if path_line and path_line.startswith('- ')
|
if path_line and path_line.startswith('- ') or path_line.startswith('+ ')
|
||||||
)
|
)
|
||||||
|
|
||||||
return tuple(
|
return tuple(
|
||||||
|
@ -297,6 +327,7 @@ def create_archive(
|
||||||
If a sequence of stream processes is given (instances of subprocess.Popen), then execute the
|
If a sequence of stream processes is given (instances of subprocess.Popen), then execute the
|
||||||
create command while also triggering the given processes to produce output.
|
create command while also triggering the given processes to produce output.
|
||||||
'''
|
'''
|
||||||
|
borgmatic.logger.add_custom_log_levels()
|
||||||
borgmatic_source_directories = expand_directories(
|
borgmatic_source_directories = expand_directories(
|
||||||
collect_borgmatic_source_directories(location_config.get('borgmatic_source_directory'))
|
collect_borgmatic_source_directories(location_config.get('borgmatic_source_directory'))
|
||||||
)
|
)
|
||||||
|
@ -327,11 +358,13 @@ def create_archive(
|
||||||
expand_home_directories(location_config.get('exclude_patterns'))
|
expand_home_directories(location_config.get('exclude_patterns'))
|
||||||
)
|
)
|
||||||
checkpoint_interval = storage_config.get('checkpoint_interval', None)
|
checkpoint_interval = storage_config.get('checkpoint_interval', None)
|
||||||
|
checkpoint_volume = storage_config.get('checkpoint_volume', None)
|
||||||
chunker_params = storage_config.get('chunker_params', None)
|
chunker_params = storage_config.get('chunker_params', None)
|
||||||
compression = storage_config.get('compression', None)
|
compression = storage_config.get('compression', None)
|
||||||
upload_rate_limit = storage_config.get('upload_rate_limit', None)
|
upload_rate_limit = storage_config.get('upload_rate_limit', None)
|
||||||
umask = storage_config.get('umask', None)
|
umask = storage_config.get('umask', None)
|
||||||
lock_wait = storage_config.get('lock_wait', None)
|
lock_wait = storage_config.get('lock_wait', None)
|
||||||
|
list_filter_flags = make_list_filter_flags(local_borg_version, dry_run)
|
||||||
files_cache = location_config.get('files_cache')
|
files_cache = location_config.get('files_cache')
|
||||||
archive_name_format = storage_config.get('archive_name_format', DEFAULT_ARCHIVE_NAME_FORMAT)
|
archive_name_format = storage_config.get('archive_name_format', DEFAULT_ARCHIVE_NAME_FORMAT)
|
||||||
extra_borg_options = storage_config.get('extra_borg_options', {}).get('create', '')
|
extra_borg_options = storage_config.get('extra_borg_options', {}).get('create', '')
|
||||||
|
@ -371,6 +404,7 @@ def create_archive(
|
||||||
+ make_pattern_flags(location_config, pattern_file.name if pattern_file else None)
|
+ make_pattern_flags(location_config, pattern_file.name if pattern_file else None)
|
||||||
+ make_exclude_flags(location_config, exclude_file.name if exclude_file else None)
|
+ make_exclude_flags(location_config, exclude_file.name if exclude_file else None)
|
||||||
+ (('--checkpoint-interval', str(checkpoint_interval)) if checkpoint_interval else ())
|
+ (('--checkpoint-interval', str(checkpoint_interval)) if checkpoint_interval else ())
|
||||||
|
+ (('--checkpoint-volume', str(checkpoint_volume)) if checkpoint_volume else ())
|
||||||
+ (('--chunker-params', chunker_params) if chunker_params else ())
|
+ (('--chunker-params', chunker_params) if chunker_params else ())
|
||||||
+ (('--compression', compression) if compression else ())
|
+ (('--compression', compression) if compression else ())
|
||||||
+ upload_ratelimit_flags
|
+ upload_ratelimit_flags
|
||||||
|
@ -389,7 +423,11 @@ def create_archive(
|
||||||
+ (('--remote-path', remote_path) if remote_path else ())
|
+ (('--remote-path', remote_path) if remote_path else ())
|
||||||
+ (('--umask', str(umask)) if umask else ())
|
+ (('--umask', str(umask)) if umask else ())
|
||||||
+ (('--lock-wait', str(lock_wait)) if lock_wait else ())
|
+ (('--lock-wait', str(lock_wait)) if lock_wait else ())
|
||||||
+ (('--list', '--filter', 'AMEx-') if list_files and not json and not progress else ())
|
+ (
|
||||||
|
('--list', '--filter', list_filter_flags)
|
||||||
|
if list_files and not json and not progress
|
||||||
|
else ()
|
||||||
|
)
|
||||||
+ (('--dry-run',) if dry_run else ())
|
+ (('--dry-run',) if dry_run else ())
|
||||||
+ (tuple(extra_borg_options.split(' ')) if extra_borg_options 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, archive_name_format, local_borg_version)
|
||||||
|
@ -398,8 +436,8 @@ def create_archive(
|
||||||
|
|
||||||
if json:
|
if json:
|
||||||
output_log_level = None
|
output_log_level = None
|
||||||
elif (stats or list_files) and logger.getEffectiveLevel() == logging.WARNING:
|
elif list_files or (stats and not dry_run):
|
||||||
output_log_level = logging.WARNING
|
output_log_level = logging.ANSWER
|
||||||
else:
|
else:
|
||||||
output_log_level = logging.INFO
|
output_log_level = logging.INFO
|
||||||
|
|
||||||
|
@ -420,18 +458,17 @@ def create_archive(
|
||||||
borg_environment,
|
borg_environment,
|
||||||
skip_directories=borgmatic_source_directories,
|
skip_directories=borgmatic_source_directories,
|
||||||
)
|
)
|
||||||
logger.warning(
|
|
||||||
f'{repository}: Excluding special files to prevent Borg from hanging: {", ".join(special_file_paths)}'
|
|
||||||
)
|
|
||||||
|
|
||||||
exclude_file = write_pattern_file(
|
if special_file_paths:
|
||||||
expand_home_directories(
|
logger.warning(
|
||||||
tuple(location_config.get('exclude_patterns') or ()) + special_file_paths
|
f'{repository}: Excluding special files to prevent Borg from hanging: {", ".join(special_file_paths)}'
|
||||||
),
|
)
|
||||||
pattern_file=exclude_file,
|
exclude_file = write_pattern_file(
|
||||||
)
|
expand_home_directories(
|
||||||
|
tuple(location_config.get('exclude_patterns') or ()) + special_file_paths
|
||||||
if exclude_file:
|
),
|
||||||
|
pattern_file=exclude_file,
|
||||||
|
)
|
||||||
create_command += make_exclude_flags(location_config, exclude_file.name)
|
create_command += make_exclude_flags(location_config, exclude_file.name)
|
||||||
|
|
||||||
create_command += (
|
create_command += (
|
||||||
|
@ -452,12 +489,16 @@ def create_archive(
|
||||||
working_directory=working_directory,
|
working_directory=working_directory,
|
||||||
extra_environment=borg_environment,
|
extra_environment=borg_environment,
|
||||||
)
|
)
|
||||||
|
elif output_log_level is None:
|
||||||
return execute_command(
|
return execute_command_and_capture_output(
|
||||||
create_command,
|
create_command, working_directory=working_directory, extra_environment=borg_environment,
|
||||||
output_log_level,
|
)
|
||||||
output_file,
|
else:
|
||||||
borg_local_path=local_path,
|
execute_command(
|
||||||
working_directory=working_directory,
|
create_command,
|
||||||
extra_environment=borg_environment,
|
output_log_level,
|
||||||
)
|
output_file,
|
||||||
|
borg_local_path=local_path,
|
||||||
|
working_directory=working_directory,
|
||||||
|
extra_environment=borg_environment,
|
||||||
|
)
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
import borgmatic.logger
|
||||||
from borgmatic.borg import environment, flags
|
from borgmatic.borg import environment, flags
|
||||||
from borgmatic.execute import DO_NOT_CAPTURE, execute_command
|
from borgmatic.execute import DO_NOT_CAPTURE, execute_command
|
||||||
|
|
||||||
|
@ -30,6 +31,7 @@ def export_tar_archive(
|
||||||
|
|
||||||
If the destination path is "-", then stream the output to stdout instead of to a file.
|
If the destination path is "-", then stream the output to stdout instead of to a file.
|
||||||
'''
|
'''
|
||||||
|
borgmatic.logger.add_custom_log_levels()
|
||||||
umask = storage_config.get('umask', None)
|
umask = storage_config.get('umask', None)
|
||||||
lock_wait = storage_config.get('lock_wait', None)
|
lock_wait = storage_config.get('lock_wait', None)
|
||||||
|
|
||||||
|
@ -53,8 +55,8 @@ def export_tar_archive(
|
||||||
+ (tuple(paths) if paths else ())
|
+ (tuple(paths) if paths else ())
|
||||||
)
|
)
|
||||||
|
|
||||||
if list_files and logger.getEffectiveLevel() == logging.WARNING:
|
if list_files:
|
||||||
output_log_level = logging.WARNING
|
output_log_level = logging.ANSWER
|
||||||
else:
|
else:
|
||||||
output_log_level = logging.INFO
|
output_log_level = logging.INFO
|
||||||
|
|
||||||
|
|
|
@ -87,6 +87,13 @@ def extract_archive(
|
||||||
else:
|
else:
|
||||||
numeric_ids_flags = ('--numeric-owner',) if location_config.get('numeric_ids') else ()
|
numeric_ids_flags = ('--numeric-owner',) if location_config.get('numeric_ids') else ()
|
||||||
|
|
||||||
|
if strip_components == 'all':
|
||||||
|
if not paths:
|
||||||
|
raise ValueError('The --strip-components flag with "all" requires at least one --path')
|
||||||
|
|
||||||
|
# Calculate the maximum number of leading path components of the given paths.
|
||||||
|
strip_components = max(0, *(len(path.split(os.path.sep)) - 1 for path in paths))
|
||||||
|
|
||||||
full_command = (
|
full_command = (
|
||||||
(local_path, 'extract')
|
(local_path, 'extract')
|
||||||
+ (('--remote-path', remote_path) if remote_path else ())
|
+ (('--remote-path', remote_path) if remote_path else ())
|
||||||
|
|
|
@ -14,6 +14,7 @@ class Feature(Enum):
|
||||||
RLIST = 8
|
RLIST = 8
|
||||||
RINFO = 9
|
RINFO = 9
|
||||||
MATCH_ARCHIVES = 10
|
MATCH_ARCHIVES = 10
|
||||||
|
EXCLUDED_FILES_MINUS = 11
|
||||||
|
|
||||||
|
|
||||||
FEATURE_TO_MINIMUM_BORG_VERSION = {
|
FEATURE_TO_MINIMUM_BORG_VERSION = {
|
||||||
|
@ -27,6 +28,7 @@ FEATURE_TO_MINIMUM_BORG_VERSION = {
|
||||||
Feature.RLIST: parse_version('2.0.0a2'), # borg rlist
|
Feature.RLIST: parse_version('2.0.0a2'), # borg rlist
|
||||||
Feature.RINFO: parse_version('2.0.0a2'), # borg rinfo
|
Feature.RINFO: parse_version('2.0.0a2'), # borg rinfo
|
||||||
Feature.MATCH_ARCHIVES: parse_version('2.0.0b3'), # borg --match-archives
|
Feature.MATCH_ARCHIVES: parse_version('2.0.0b3'), # borg --match-archives
|
||||||
|
Feature.EXCLUDED_FILES_MINUS: parse_version('2.0.0b5'), # --list --filter uses "-" for excludes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
import borgmatic.logger
|
||||||
from borgmatic.borg import environment, feature, flags
|
from borgmatic.borg import environment, feature, flags
|
||||||
from borgmatic.execute import execute_command
|
from borgmatic.execute import execute_command, execute_command_and_capture_output
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -19,6 +20,7 @@ def display_archives_info(
|
||||||
arguments to the info action, display summary information for Borg archives in the repository or
|
arguments to the info action, display summary information for Borg archives in the repository or
|
||||||
return JSON summary information.
|
return JSON summary information.
|
||||||
'''
|
'''
|
||||||
|
borgmatic.logger.add_custom_log_levels()
|
||||||
lock_wait = storage_config.get('lock_wait', None)
|
lock_wait = storage_config.get('lock_wait', None)
|
||||||
|
|
||||||
full_command = (
|
full_command = (
|
||||||
|
@ -55,9 +57,14 @@ def display_archives_info(
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
return execute_command(
|
if info_arguments.json:
|
||||||
full_command,
|
return execute_command_and_capture_output(
|
||||||
output_log_level=None if info_arguments.json else logging.WARNING,
|
full_command, extra_environment=environment.make_environment(storage_config),
|
||||||
borg_local_path=local_path,
|
)
|
||||||
extra_environment=environment.make_environment(storage_config),
|
else:
|
||||||
)
|
execute_command(
|
||||||
|
full_command,
|
||||||
|
output_log_level=logging.ANSWER,
|
||||||
|
borg_local_path=local_path,
|
||||||
|
extra_environment=environment.make_environment(storage_config),
|
||||||
|
)
|
||||||
|
|
|
@ -3,8 +3,9 @@ import copy
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
import borgmatic.logger
|
||||||
from borgmatic.borg import environment, feature, flags, rlist
|
from borgmatic.borg import environment, feature, flags, rlist
|
||||||
from borgmatic.execute import execute_command
|
from borgmatic.execute import execute_command, execute_command_and_capture_output
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -84,6 +85,46 @@ def make_find_paths(find_paths):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def capture_archive_listing(
|
||||||
|
repository,
|
||||||
|
archive,
|
||||||
|
storage_config,
|
||||||
|
local_borg_version,
|
||||||
|
list_path=None,
|
||||||
|
local_path='borg',
|
||||||
|
remote_path=None,
|
||||||
|
):
|
||||||
|
'''
|
||||||
|
Given a local or remote repository path, an archive name, a storage config dict, the local Borg
|
||||||
|
version, the archive path in which to list files, and local and remote Borg paths, capture the
|
||||||
|
output of listing that archive and return it as a list of file paths.
|
||||||
|
'''
|
||||||
|
borg_environment = environment.make_environment(storage_config)
|
||||||
|
|
||||||
|
return tuple(
|
||||||
|
execute_command_and_capture_output(
|
||||||
|
make_list_command(
|
||||||
|
repository,
|
||||||
|
storage_config,
|
||||||
|
local_borg_version,
|
||||||
|
argparse.Namespace(
|
||||||
|
repository=repository,
|
||||||
|
archive=archive,
|
||||||
|
paths=[f'sh:{list_path}'],
|
||||||
|
find_paths=None,
|
||||||
|
json=None,
|
||||||
|
format='{path}{NL}',
|
||||||
|
),
|
||||||
|
local_path,
|
||||||
|
remote_path,
|
||||||
|
),
|
||||||
|
extra_environment=borg_environment,
|
||||||
|
)
|
||||||
|
.strip('\n')
|
||||||
|
.split('\n')
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def list_archive(
|
def list_archive(
|
||||||
repository,
|
repository,
|
||||||
storage_config,
|
storage_config,
|
||||||
|
@ -99,6 +140,8 @@ def list_archive(
|
||||||
list the files by searching across multiple archives. If neither find_paths nor archive name
|
list the files by searching across multiple archives. If neither find_paths nor archive name
|
||||||
are given, instead list the archives in the given repository.
|
are given, instead list the archives in the given repository.
|
||||||
'''
|
'''
|
||||||
|
borgmatic.logger.add_custom_log_levels()
|
||||||
|
|
||||||
if not list_arguments.archive and not list_arguments.find_paths:
|
if not list_arguments.archive and not list_arguments.find_paths:
|
||||||
if feature.available(feature.Feature.RLIST, local_borg_version):
|
if feature.available(feature.Feature.RLIST, local_borg_version):
|
||||||
logger.warning(
|
logger.warning(
|
||||||
|
@ -151,7 +194,7 @@ def list_archive(
|
||||||
|
|
||||||
# Ask Borg to list archives. Capture its output for use below.
|
# Ask Borg to list archives. Capture its output for use below.
|
||||||
archive_lines = tuple(
|
archive_lines = tuple(
|
||||||
execute_command(
|
execute_command_and_capture_output(
|
||||||
rlist.make_rlist_command(
|
rlist.make_rlist_command(
|
||||||
repository,
|
repository,
|
||||||
storage_config,
|
storage_config,
|
||||||
|
@ -160,8 +203,6 @@ def list_archive(
|
||||||
local_path,
|
local_path,
|
||||||
remote_path,
|
remote_path,
|
||||||
),
|
),
|
||||||
output_log_level=None,
|
|
||||||
borg_local_path=local_path,
|
|
||||||
extra_environment=borg_environment,
|
extra_environment=borg_environment,
|
||||||
)
|
)
|
||||||
.strip('\n')
|
.strip('\n')
|
||||||
|
@ -172,7 +213,7 @@ def list_archive(
|
||||||
|
|
||||||
# For each archive listed by Borg, run list on the contents of that archive.
|
# For each archive listed by Borg, run list on the contents of that archive.
|
||||||
for archive in archive_lines:
|
for archive in archive_lines:
|
||||||
logger.warning(f'{repository}: Listing archive {archive}')
|
logger.answer(f'{repository}: Listing archive {archive}')
|
||||||
|
|
||||||
archive_arguments = copy.copy(list_arguments)
|
archive_arguments = copy.copy(list_arguments)
|
||||||
archive_arguments.archive = archive
|
archive_arguments.archive = archive
|
||||||
|
@ -193,7 +234,7 @@ def list_archive(
|
||||||
|
|
||||||
execute_command(
|
execute_command(
|
||||||
main_command,
|
main_command,
|
||||||
output_log_level=logging.WARNING,
|
output_log_level=logging.ANSWER,
|
||||||
borg_local_path=local_path,
|
borg_local_path=local_path,
|
||||||
extra_environment=borg_environment,
|
extra_environment=borg_environment,
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
import borgmatic.logger
|
||||||
from borgmatic.borg import environment, feature, flags
|
from borgmatic.borg import environment, feature, flags
|
||||||
from borgmatic.execute import execute_command
|
from borgmatic.execute import execute_command
|
||||||
|
|
||||||
|
@ -52,6 +53,7 @@ def prune_archives(
|
||||||
retention config dict, prune Borg archives according to the retention policy specified in that
|
retention config dict, prune Borg archives according to the retention policy specified in that
|
||||||
configuration.
|
configuration.
|
||||||
'''
|
'''
|
||||||
|
borgmatic.logger.add_custom_log_levels()
|
||||||
umask = storage_config.get('umask', None)
|
umask = storage_config.get('umask', None)
|
||||||
lock_wait = storage_config.get('lock_wait', None)
|
lock_wait = storage_config.get('lock_wait', None)
|
||||||
extra_borg_options = storage_config.get('extra_borg_options', {}).get('prune', '')
|
extra_borg_options = storage_config.get('extra_borg_options', {}).get('prune', '')
|
||||||
|
@ -75,8 +77,8 @@ def prune_archives(
|
||||||
+ flags.make_repository_flags(repository, local_borg_version)
|
+ flags.make_repository_flags(repository, local_borg_version)
|
||||||
)
|
)
|
||||||
|
|
||||||
if (stats or list_archives) and logger.getEffectiveLevel() == logging.WARNING:
|
if stats or list_archives:
|
||||||
output_log_level = logging.WARNING
|
output_log_level = logging.ANSWER
|
||||||
else:
|
else:
|
||||||
output_log_level = logging.INFO
|
output_log_level = logging.INFO
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
import borgmatic.logger
|
||||||
from borgmatic.borg import environment, feature, flags
|
from borgmatic.borg import environment, feature, flags
|
||||||
from borgmatic.execute import execute_command
|
from borgmatic.execute import execute_command, execute_command_and_capture_output
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -19,6 +20,7 @@ def display_repository_info(
|
||||||
arguments to the rinfo action, display summary information for the Borg repository or return
|
arguments to the rinfo action, display summary information for the Borg repository or return
|
||||||
JSON summary information.
|
JSON summary information.
|
||||||
'''
|
'''
|
||||||
|
borgmatic.logger.add_custom_log_levels()
|
||||||
lock_wait = storage_config.get('lock_wait', None)
|
lock_wait = storage_config.get('lock_wait', None)
|
||||||
|
|
||||||
full_command = (
|
full_command = (
|
||||||
|
@ -44,9 +46,16 @@ def display_repository_info(
|
||||||
+ flags.make_repository_flags(repository, local_borg_version)
|
+ flags.make_repository_flags(repository, local_borg_version)
|
||||||
)
|
)
|
||||||
|
|
||||||
return execute_command(
|
extra_environment = environment.make_environment(storage_config)
|
||||||
full_command,
|
|
||||||
output_log_level=None if rinfo_arguments.json else logging.WARNING,
|
if rinfo_arguments.json:
|
||||||
borg_local_path=local_path,
|
return execute_command_and_capture_output(
|
||||||
extra_environment=environment.make_environment(storage_config),
|
full_command, extra_environment=extra_environment,
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
execute_command(
|
||||||
|
full_command,
|
||||||
|
output_log_level=logging.ANSWER,
|
||||||
|
borg_local_path=local_path,
|
||||||
|
extra_environment=extra_environment,
|
||||||
|
)
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
import borgmatic.logger
|
||||||
from borgmatic.borg import environment, feature, flags
|
from borgmatic.borg import environment, feature, flags
|
||||||
from borgmatic.execute import execute_command
|
from borgmatic.execute import execute_command, execute_command_and_capture_output
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -16,7 +17,7 @@ def resolve_archive_name(
|
||||||
|
|
||||||
Raise ValueError if "latest" is given but there are no archives in the repository.
|
Raise ValueError if "latest" is given but there are no archives in the repository.
|
||||||
'''
|
'''
|
||||||
if archive != "latest":
|
if archive != 'latest':
|
||||||
return archive
|
return archive
|
||||||
|
|
||||||
lock_wait = storage_config.get('lock_wait', None)
|
lock_wait = storage_config.get('lock_wait', None)
|
||||||
|
@ -33,11 +34,8 @@ def resolve_archive_name(
|
||||||
+ flags.make_repository_flags(repository, local_borg_version)
|
+ flags.make_repository_flags(repository, local_borg_version)
|
||||||
)
|
)
|
||||||
|
|
||||||
output = execute_command(
|
output = execute_command_and_capture_output(
|
||||||
full_command,
|
full_command, extra_environment=environment.make_environment(storage_config),
|
||||||
output_log_level=None,
|
|
||||||
borg_local_path=local_path,
|
|
||||||
extra_environment=environment.make_environment(storage_config),
|
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
latest_archive = output.strip().splitlines()[-1]
|
latest_archive = output.strip().splitlines()[-1]
|
||||||
|
@ -111,18 +109,19 @@ def list_repository(
|
||||||
arguments to the list action, and local and remote Borg paths, display the output of listing
|
arguments to the list action, and local and remote Borg paths, display the output of listing
|
||||||
Borg archives in the given repository (or return JSON output).
|
Borg archives in the given repository (or return JSON output).
|
||||||
'''
|
'''
|
||||||
|
borgmatic.logger.add_custom_log_levels()
|
||||||
borg_environment = environment.make_environment(storage_config)
|
borg_environment = environment.make_environment(storage_config)
|
||||||
|
|
||||||
main_command = make_rlist_command(
|
main_command = make_rlist_command(
|
||||||
repository, storage_config, local_borg_version, rlist_arguments, local_path, remote_path
|
repository, storage_config, local_borg_version, rlist_arguments, local_path, remote_path
|
||||||
)
|
)
|
||||||
|
|
||||||
output = execute_command(
|
|
||||||
main_command,
|
|
||||||
output_log_level=None if rlist_arguments.json else logging.WARNING,
|
|
||||||
borg_local_path=local_path,
|
|
||||||
extra_environment=borg_environment,
|
|
||||||
)
|
|
||||||
|
|
||||||
if rlist_arguments.json:
|
if rlist_arguments.json:
|
||||||
return output
|
return execute_command_and_capture_output(main_command, extra_environment=borg_environment,)
|
||||||
|
else:
|
||||||
|
execute_command(
|
||||||
|
main_command,
|
||||||
|
output_log_level=logging.ANSWER,
|
||||||
|
borg_local_path=local_path,
|
||||||
|
extra_environment=borg_environment,
|
||||||
|
)
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
import borgmatic.logger
|
||||||
from borgmatic.borg import environment, flags
|
from borgmatic.borg import environment, flags
|
||||||
from borgmatic.execute import execute_command
|
from borgmatic.execute import DO_NOT_CAPTURE, execute_command
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -19,12 +20,15 @@ def transfer_archives(
|
||||||
Given a dry-run flag, a local or remote repository path, a storage config dict, the local Borg
|
Given a dry-run flag, a local or remote repository path, a storage config dict, the local Borg
|
||||||
version, and the arguments to the transfer action, transfer archives to the given repository.
|
version, and the arguments to the transfer action, transfer archives to the given repository.
|
||||||
'''
|
'''
|
||||||
|
borgmatic.logger.add_custom_log_levels()
|
||||||
|
|
||||||
full_command = (
|
full_command = (
|
||||||
(local_path, 'transfer')
|
(local_path, 'transfer')
|
||||||
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
|
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
|
||||||
+ (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
|
+ (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
|
||||||
+ flags.make_flags('remote-path', remote_path)
|
+ flags.make_flags('remote-path', remote_path)
|
||||||
+ flags.make_flags('lock-wait', storage_config.get('lock_wait', None))
|
+ flags.make_flags('lock-wait', storage_config.get('lock_wait', None))
|
||||||
|
+ (('--progress',) if transfer_arguments.progress else ())
|
||||||
+ (
|
+ (
|
||||||
flags.make_flags(
|
flags.make_flags(
|
||||||
'match-archives', transfer_arguments.match_archives or transfer_arguments.archive
|
'match-archives', transfer_arguments.match_archives or transfer_arguments.archive
|
||||||
|
@ -41,7 +45,8 @@ def transfer_archives(
|
||||||
|
|
||||||
return execute_command(
|
return execute_command(
|
||||||
full_command,
|
full_command,
|
||||||
output_log_level=logging.WARNING,
|
output_log_level=logging.ANSWER,
|
||||||
|
output_file=DO_NOT_CAPTURE if transfer_arguments.progress else None,
|
||||||
borg_local_path=local_path,
|
borg_local_path=local_path,
|
||||||
extra_environment=environment.make_environment(storage_config),
|
extra_environment=environment.make_environment(storage_config),
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from borgmatic.borg import environment
|
from borgmatic.borg import environment
|
||||||
from borgmatic.execute import execute_command
|
from borgmatic.execute import execute_command_and_capture_output
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -18,11 +18,8 @@ def local_borg_version(storage_config, local_path='borg'):
|
||||||
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
|
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
|
||||||
+ (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
|
+ (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
|
||||||
)
|
)
|
||||||
output = execute_command(
|
output = execute_command_and_capture_output(
|
||||||
full_command,
|
full_command, extra_environment=environment.make_environment(storage_config),
|
||||||
output_log_level=None,
|
|
||||||
borg_local_path=local_path,
|
|
||||||
extra_environment=environment.make_environment(storage_config),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -46,11 +46,12 @@ def parse_subparser_arguments(unparsed_arguments, subparsers):
|
||||||
if 'borg' in unparsed_arguments:
|
if 'borg' in unparsed_arguments:
|
||||||
subparsers = {'borg': subparsers['borg']}
|
subparsers = {'borg': subparsers['borg']}
|
||||||
|
|
||||||
for subparser_name, subparser in subparsers.items():
|
for argument in remaining_arguments:
|
||||||
if subparser_name not in remaining_arguments:
|
canonical_name = alias_to_subparser_name.get(argument, argument)
|
||||||
continue
|
subparser = subparsers.get(canonical_name)
|
||||||
|
|
||||||
canonical_name = alias_to_subparser_name.get(subparser_name, subparser_name)
|
if not subparser:
|
||||||
|
continue
|
||||||
|
|
||||||
# If a parsed value happens to be the same as the name of a subparser, remove it from the
|
# If a parsed value happens to be the same as the name of a subparser, remove it from the
|
||||||
# remaining arguments. This prevents, for instance, "check --only extract" from triggering
|
# remaining arguments. This prevents, for instance, "check --only extract" from triggering
|
||||||
|
@ -67,9 +68,9 @@ def parse_subparser_arguments(unparsed_arguments, subparsers):
|
||||||
|
|
||||||
arguments[canonical_name] = parsed
|
arguments[canonical_name] = parsed
|
||||||
|
|
||||||
# If no actions are explicitly requested, assume defaults: prune, compact, create, and check.
|
# If no actions are explicitly requested, assume defaults.
|
||||||
if not arguments and '--help' not in unparsed_arguments and '-h' not in unparsed_arguments:
|
if not arguments and '--help' not in unparsed_arguments and '-h' not in unparsed_arguments:
|
||||||
for subparser_name in ('prune', 'compact', 'create', 'check'):
|
for subparser_name in ('create', 'prune', 'compact', 'check'):
|
||||||
subparser = subparsers[subparser_name]
|
subparser = subparsers[subparser_name]
|
||||||
parsed, unused_remaining = subparser.parse_known_args(unparsed_arguments)
|
parsed, unused_remaining = subparser.parse_known_args(unparsed_arguments)
|
||||||
arguments[subparser_name] = parsed
|
arguments[subparser_name] = parsed
|
||||||
|
@ -215,7 +216,7 @@ def make_parsers():
|
||||||
top_level_parser = ArgumentParser(
|
top_level_parser = ArgumentParser(
|
||||||
description='''
|
description='''
|
||||||
Simple, configuration-driven backup software for servers and workstations. If none of
|
Simple, configuration-driven backup software for servers and workstations. If none of
|
||||||
the action options are given, then borgmatic defaults to: prune, compact, create, and
|
the action options are given, then borgmatic defaults to: create, prune, compact, and
|
||||||
check.
|
check.
|
||||||
''',
|
''',
|
||||||
parents=[global_parser],
|
parents=[global_parser],
|
||||||
|
@ -224,7 +225,7 @@ def make_parsers():
|
||||||
subparsers = top_level_parser.add_subparsers(
|
subparsers = top_level_parser.add_subparsers(
|
||||||
title='actions',
|
title='actions',
|
||||||
metavar='',
|
metavar='',
|
||||||
help='Specify zero or more actions. Defaults to prune, compact, create, and check. Use --help with action for details:',
|
help='Specify zero or more actions. Defaults to creat, prune, compact, and check. Use --help with action for details:',
|
||||||
)
|
)
|
||||||
rcreate_parser = subparsers.add_parser(
|
rcreate_parser = subparsers.add_parser(
|
||||||
'rcreate',
|
'rcreate',
|
||||||
|
@ -247,6 +248,10 @@ def make_parsers():
|
||||||
metavar='KEY_REPOSITORY',
|
metavar='KEY_REPOSITORY',
|
||||||
help='Path to an existing Borg repository whose key material should be reused (Borg 2.x+ only)',
|
help='Path to an existing Borg repository whose key material should be reused (Borg 2.x+ only)',
|
||||||
)
|
)
|
||||||
|
rcreate_group.add_argument(
|
||||||
|
'--repository',
|
||||||
|
help='Path of the new repository to create (must be already specified in a borgmatic configuration file), defaults to the configured repository if there is only one',
|
||||||
|
)
|
||||||
rcreate_group.add_argument(
|
rcreate_group.add_argument(
|
||||||
'--copy-crypt-key',
|
'--copy-crypt-key',
|
||||||
action='store_true',
|
action='store_true',
|
||||||
|
@ -292,6 +297,12 @@ def make_parsers():
|
||||||
'--upgrader',
|
'--upgrader',
|
||||||
help='Upgrader type used to convert the transfered data, e.g. "From12To20" to upgrade data from Borg 1.2 to 2.0 format, defaults to no conversion',
|
help='Upgrader type used to convert the transfered data, e.g. "From12To20" to upgrade data from Borg 1.2 to 2.0 format, defaults to no conversion',
|
||||||
)
|
)
|
||||||
|
transfer_group.add_argument(
|
||||||
|
'--progress',
|
||||||
|
default=False,
|
||||||
|
action='store_true',
|
||||||
|
help='Display progress as each archive is transferred',
|
||||||
|
)
|
||||||
transfer_group.add_argument(
|
transfer_group.add_argument(
|
||||||
'-a',
|
'-a',
|
||||||
'--match-archives',
|
'--match-archives',
|
||||||
|
@ -337,8 +348,8 @@ def make_parsers():
|
||||||
compact_parser = subparsers.add_parser(
|
compact_parser = subparsers.add_parser(
|
||||||
'compact',
|
'compact',
|
||||||
aliases=SUBPARSER_ALIASES['compact'],
|
aliases=SUBPARSER_ALIASES['compact'],
|
||||||
help='Compact segments to free space (Borg 1.2+ only)',
|
help='Compact segments to free space (Borg 1.2+, borgmatic 1.5.23+ only)',
|
||||||
description='Compact segments to free space (Borg 1.2+ only)',
|
description='Compact segments to free space (Borg 1.2+, borgmatic 1.5.23+ only)',
|
||||||
add_help=False,
|
add_help=False,
|
||||||
)
|
)
|
||||||
compact_group = compact_parser.add_argument_group('compact arguments')
|
compact_group = compact_parser.add_argument_group('compact arguments')
|
||||||
|
@ -465,10 +476,9 @@ def make_parsers():
|
||||||
)
|
)
|
||||||
extract_group.add_argument(
|
extract_group.add_argument(
|
||||||
'--strip-components',
|
'--strip-components',
|
||||||
type=int,
|
type=lambda number: number if number == 'all' else int(number),
|
||||||
metavar='NUMBER',
|
metavar='NUMBER',
|
||||||
dest='strip_components',
|
help='Number of leading path components to remove from each extracted path or "all" to strip all leading path components. Skip paths with fewer elements',
|
||||||
help='Number of leading path components to remove from each extracted path. Skip paths with fewer elements',
|
|
||||||
)
|
)
|
||||||
extract_group.add_argument(
|
extract_group.add_argument(
|
||||||
'--progress',
|
'--progress',
|
||||||
|
@ -601,7 +611,7 @@ def make_parsers():
|
||||||
metavar='NAME',
|
metavar='NAME',
|
||||||
nargs='+',
|
nargs='+',
|
||||||
dest='databases',
|
dest='databases',
|
||||||
help='Names of databases to restore from archive, defaults to all databases. Note that any databases to restore must be defined in borgmatic\'s configuration',
|
help="Names of databases to restore from archive, defaults to all databases. Note that any databases to restore must be defined in borgmatic's configuration",
|
||||||
)
|
)
|
||||||
restore_group.add_argument(
|
restore_group.add_argument(
|
||||||
'-h', '--help', action='help', help='Show this help message and exit'
|
'-h', '--help', action='help', help='Show this help message and exit'
|
||||||
|
@ -795,7 +805,7 @@ def make_parsers():
|
||||||
'borg',
|
'borg',
|
||||||
aliases=SUBPARSER_ALIASES['borg'],
|
aliases=SUBPARSER_ALIASES['borg'],
|
||||||
help='Run an arbitrary Borg command',
|
help='Run an arbitrary Borg command',
|
||||||
description='Run an arbitrary Borg command based on borgmatic\'s configuration',
|
description="Run an arbitrary Borg command based on borgmatic's configuration",
|
||||||
add_help=False,
|
add_help=False,
|
||||||
)
|
)
|
||||||
borg_group = borg_parser.add_argument_group('borg arguments')
|
borg_group = borg_parser.add_argument_group('borg arguments')
|
||||||
|
@ -833,6 +843,11 @@ def parse_arguments(*unparsed_arguments):
|
||||||
'The --excludes flag has been replaced with exclude_patterns in configuration.'
|
'The --excludes flag has been replaced with exclude_patterns in configuration.'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if 'create' in arguments and arguments['create'].list_files and arguments['create'].progress:
|
||||||
|
raise ValueError(
|
||||||
|
'With the create action, only one of --list (--files) and --progress flags can be used.'
|
||||||
|
)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
('list' in arguments and 'rinfo' in arguments and arguments['list'].json)
|
('list' in arguments and 'rinfo' in arguments and arguments['list'].json)
|
||||||
or ('list' in arguments and 'info' in arguments and arguments['list'].json)
|
or ('list' in arguments and 'info' in arguments and arguments['list'].json)
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import collections
|
import collections
|
||||||
import copy
|
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
@ -11,29 +10,29 @@ from subprocess import CalledProcessError
|
||||||
import colorama
|
import colorama
|
||||||
import pkg_resources
|
import pkg_resources
|
||||||
|
|
||||||
|
import borgmatic.actions.borg
|
||||||
|
import borgmatic.actions.break_lock
|
||||||
|
import borgmatic.actions.check
|
||||||
|
import borgmatic.actions.compact
|
||||||
|
import borgmatic.actions.create
|
||||||
|
import borgmatic.actions.export_tar
|
||||||
|
import borgmatic.actions.extract
|
||||||
|
import borgmatic.actions.info
|
||||||
|
import borgmatic.actions.list
|
||||||
|
import borgmatic.actions.mount
|
||||||
|
import borgmatic.actions.prune
|
||||||
|
import borgmatic.actions.rcreate
|
||||||
|
import borgmatic.actions.restore
|
||||||
|
import borgmatic.actions.rinfo
|
||||||
|
import borgmatic.actions.rlist
|
||||||
|
import borgmatic.actions.transfer
|
||||||
import borgmatic.commands.completion
|
import borgmatic.commands.completion
|
||||||
from borgmatic.borg import borg as borg_borg
|
|
||||||
from borgmatic.borg import break_lock as borg_break_lock
|
|
||||||
from borgmatic.borg import check as borg_check
|
|
||||||
from borgmatic.borg import compact as borg_compact
|
|
||||||
from borgmatic.borg import create as borg_create
|
|
||||||
from borgmatic.borg import export_tar as borg_export_tar
|
|
||||||
from borgmatic.borg import extract as borg_extract
|
|
||||||
from borgmatic.borg import feature as borg_feature
|
|
||||||
from borgmatic.borg import info as borg_info
|
|
||||||
from borgmatic.borg import list as borg_list
|
|
||||||
from borgmatic.borg import mount as borg_mount
|
|
||||||
from borgmatic.borg import prune as borg_prune
|
|
||||||
from borgmatic.borg import rcreate as borg_rcreate
|
|
||||||
from borgmatic.borg import rinfo as borg_rinfo
|
|
||||||
from borgmatic.borg import rlist as borg_rlist
|
|
||||||
from borgmatic.borg import transfer as borg_transfer
|
|
||||||
from borgmatic.borg import umount as borg_umount
|
from borgmatic.borg import umount as borg_umount
|
||||||
from borgmatic.borg import version as borg_version
|
from borgmatic.borg import version as borg_version
|
||||||
from borgmatic.commands.arguments import parse_arguments
|
from borgmatic.commands.arguments import parse_arguments
|
||||||
from borgmatic.config import checks, collect, convert, validate
|
from borgmatic.config import checks, collect, convert, validate
|
||||||
from borgmatic.hooks import command, dispatch, dump, monitor
|
from borgmatic.hooks import command, dispatch, monitor
|
||||||
from borgmatic.logger import configure_logging, should_do_markup
|
from borgmatic.logger import add_custom_log_levels, configure_logging, should_do_markup
|
||||||
from borgmatic.signals import configure_signals
|
from borgmatic.signals import configure_signals
|
||||||
from borgmatic.verbosity import verbosity_to_log_level
|
from borgmatic.verbosity import verbosity_to_log_level
|
||||||
|
|
||||||
|
@ -45,8 +44,8 @@ LEGACY_CONFIG_PATH = '/etc/borgmatic/config'
|
||||||
def run_configuration(config_filename, config, arguments):
|
def run_configuration(config_filename, config, arguments):
|
||||||
'''
|
'''
|
||||||
Given a config filename, the corresponding parsed config dict, and command-line arguments as a
|
Given a config filename, the corresponding parsed config dict, and command-line arguments as a
|
||||||
dict from subparser name to a namespace of parsed arguments, execute the defined prune, compact,
|
dict from subparser name to a namespace of parsed arguments, execute the defined create, prune,
|
||||||
create, check, and/or other actions.
|
compact, check, and/or other actions.
|
||||||
|
|
||||||
Yield a combination of:
|
Yield a combination of:
|
||||||
|
|
||||||
|
@ -65,7 +64,7 @@ def run_configuration(config_filename, config, arguments):
|
||||||
retry_wait = storage.get('retry_wait', 0)
|
retry_wait = storage.get('retry_wait', 0)
|
||||||
encountered_error = None
|
encountered_error = None
|
||||||
error_repository = ''
|
error_repository = ''
|
||||||
using_primary_action = {'prune', 'compact', 'create', 'check'}.intersection(arguments)
|
using_primary_action = {'create', 'prune', 'compact', 'check'}.intersection(arguments)
|
||||||
monitoring_log_level = verbosity_to_log_level(global_arguments.monitoring_verbosity)
|
monitoring_log_level = verbosity_to_log_level(global_arguments.monitoring_verbosity)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -153,6 +152,25 @@ def run_configuration(config_filename, config, arguments):
|
||||||
encountered_error = error
|
encountered_error = error
|
||||||
error_repository = repository_path
|
error_repository = repository_path
|
||||||
|
|
||||||
|
try:
|
||||||
|
if using_primary_action:
|
||||||
|
# send logs irrespective of error
|
||||||
|
dispatch.call_hooks(
|
||||||
|
'ping_monitor',
|
||||||
|
hooks,
|
||||||
|
config_filename,
|
||||||
|
monitor.MONITOR_HOOK_NAMES,
|
||||||
|
monitor.State.LOG,
|
||||||
|
monitoring_log_level,
|
||||||
|
global_arguments.dry_run,
|
||||||
|
)
|
||||||
|
except (OSError, CalledProcessError) as error:
|
||||||
|
if command.considered_soft_failure(config_filename, error):
|
||||||
|
return
|
||||||
|
|
||||||
|
encountered_error = error
|
||||||
|
yield from log_error_records('{}: Error pinging monitor'.format(config_filename), error)
|
||||||
|
|
||||||
if not encountered_error:
|
if not encountered_error:
|
||||||
try:
|
try:
|
||||||
if using_primary_action:
|
if using_primary_action:
|
||||||
|
@ -244,6 +262,7 @@ def run_actions(
|
||||||
action or a hook. Raise ValueError if the arguments or configuration passed to action are
|
action or a hook. Raise ValueError if the arguments or configuration passed to action are
|
||||||
invalid.
|
invalid.
|
||||||
'''
|
'''
|
||||||
|
add_custom_log_levels()
|
||||||
repository = os.path.expanduser(repository_path)
|
repository = os.path.expanduser(repository_path)
|
||||||
global_arguments = arguments['global']
|
global_arguments = arguments['global']
|
||||||
dry_run_label = ' (dry run; not making any changes)' if global_arguments.dry_run else ''
|
dry_run_label = ' (dry run; not making any changes)' if global_arguments.dry_run else ''
|
||||||
|
@ -262,509 +281,161 @@ def run_actions(
|
||||||
**hook_context,
|
**hook_context,
|
||||||
)
|
)
|
||||||
|
|
||||||
if 'rcreate' in arguments:
|
for (action_name, action_arguments) in arguments.items():
|
||||||
logger.info('{}: Creating repository'.format(repository))
|
if action_name == 'rcreate':
|
||||||
borg_rcreate.create_repository(
|
borgmatic.actions.rcreate.run_rcreate(
|
||||||
global_arguments.dry_run,
|
|
||||||
repository,
|
|
||||||
storage,
|
|
||||||
local_borg_version,
|
|
||||||
arguments['rcreate'].encryption_mode,
|
|
||||||
arguments['rcreate'].source_repository,
|
|
||||||
arguments['rcreate'].copy_crypt_key,
|
|
||||||
arguments['rcreate'].append_only,
|
|
||||||
arguments['rcreate'].storage_quota,
|
|
||||||
arguments['rcreate'].make_parent_dirs,
|
|
||||||
local_path=local_path,
|
|
||||||
remote_path=remote_path,
|
|
||||||
)
|
|
||||||
if 'transfer' in arguments:
|
|
||||||
logger.info(f'{repository}: Transferring archives to repository')
|
|
||||||
borg_transfer.transfer_archives(
|
|
||||||
global_arguments.dry_run,
|
|
||||||
repository,
|
|
||||||
storage,
|
|
||||||
local_borg_version,
|
|
||||||
transfer_arguments=arguments['transfer'],
|
|
||||||
local_path=local_path,
|
|
||||||
remote_path=remote_path,
|
|
||||||
)
|
|
||||||
if 'prune' in arguments:
|
|
||||||
command.execute_hook(
|
|
||||||
hooks.get('before_prune'),
|
|
||||||
hooks.get('umask'),
|
|
||||||
config_filename,
|
|
||||||
'pre-prune',
|
|
||||||
global_arguments.dry_run,
|
|
||||||
**hook_context,
|
|
||||||
)
|
|
||||||
logger.info('{}: Pruning archives{}'.format(repository, dry_run_label))
|
|
||||||
borg_prune.prune_archives(
|
|
||||||
global_arguments.dry_run,
|
|
||||||
repository,
|
|
||||||
storage,
|
|
||||||
retention,
|
|
||||||
local_borg_version,
|
|
||||||
local_path=local_path,
|
|
||||||
remote_path=remote_path,
|
|
||||||
stats=arguments['prune'].stats,
|
|
||||||
list_archives=arguments['prune'].list_archives,
|
|
||||||
)
|
|
||||||
command.execute_hook(
|
|
||||||
hooks.get('after_prune'),
|
|
||||||
hooks.get('umask'),
|
|
||||||
config_filename,
|
|
||||||
'post-prune',
|
|
||||||
global_arguments.dry_run,
|
|
||||||
**hook_context,
|
|
||||||
)
|
|
||||||
if 'compact' in arguments:
|
|
||||||
command.execute_hook(
|
|
||||||
hooks.get('before_compact'),
|
|
||||||
hooks.get('umask'),
|
|
||||||
config_filename,
|
|
||||||
'pre-compact',
|
|
||||||
global_arguments.dry_run,
|
|
||||||
)
|
|
||||||
if borg_feature.available(borg_feature.Feature.COMPACT, local_borg_version):
|
|
||||||
logger.info('{}: Compacting segments{}'.format(repository, dry_run_label))
|
|
||||||
borg_compact.compact_segments(
|
|
||||||
global_arguments.dry_run,
|
|
||||||
repository,
|
repository,
|
||||||
storage,
|
storage,
|
||||||
local_borg_version,
|
local_borg_version,
|
||||||
local_path=local_path,
|
action_arguments,
|
||||||
remote_path=remote_path,
|
global_arguments,
|
||||||
progress=arguments['compact'].progress,
|
local_path,
|
||||||
cleanup_commits=arguments['compact'].cleanup_commits,
|
remote_path,
|
||||||
threshold=arguments['compact'].threshold,
|
|
||||||
)
|
)
|
||||||
else: # pragma: nocover
|
elif action_name == 'transfer':
|
||||||
logger.info(
|
borgmatic.actions.transfer.run_transfer(
|
||||||
'{}: Skipping compact (only available/needed in Borg 1.2+)'.format(repository)
|
repository,
|
||||||
)
|
storage,
|
||||||
command.execute_hook(
|
local_borg_version,
|
||||||
hooks.get('after_compact'),
|
action_arguments,
|
||||||
hooks.get('umask'),
|
global_arguments,
|
||||||
config_filename,
|
local_path,
|
||||||
'post-compact',
|
remote_path,
|
||||||
global_arguments.dry_run,
|
)
|
||||||
)
|
elif action_name == 'create':
|
||||||
if 'create' in arguments:
|
yield from borgmatic.actions.create.run_create(
|
||||||
command.execute_hook(
|
config_filename,
|
||||||
hooks.get('before_backup'),
|
|
||||||
hooks.get('umask'),
|
|
||||||
config_filename,
|
|
||||||
'pre-backup',
|
|
||||||
global_arguments.dry_run,
|
|
||||||
**hook_context,
|
|
||||||
)
|
|
||||||
logger.info('{}: Creating archive{}'.format(repository, dry_run_label))
|
|
||||||
dispatch.call_hooks_even_if_unconfigured(
|
|
||||||
'remove_database_dumps',
|
|
||||||
hooks,
|
|
||||||
repository,
|
|
||||||
dump.DATABASE_HOOK_NAMES,
|
|
||||||
location,
|
|
||||||
global_arguments.dry_run,
|
|
||||||
)
|
|
||||||
active_dumps = dispatch.call_hooks(
|
|
||||||
'dump_databases',
|
|
||||||
hooks,
|
|
||||||
repository,
|
|
||||||
dump.DATABASE_HOOK_NAMES,
|
|
||||||
location,
|
|
||||||
global_arguments.dry_run,
|
|
||||||
)
|
|
||||||
stream_processes = [process for processes in active_dumps.values() for process in processes]
|
|
||||||
|
|
||||||
json_output = borg_create.create_archive(
|
|
||||||
global_arguments.dry_run,
|
|
||||||
repository,
|
|
||||||
location,
|
|
||||||
storage,
|
|
||||||
local_borg_version,
|
|
||||||
local_path=local_path,
|
|
||||||
remote_path=remote_path,
|
|
||||||
progress=arguments['create'].progress,
|
|
||||||
stats=arguments['create'].stats,
|
|
||||||
json=arguments['create'].json,
|
|
||||||
list_files=arguments['create'].list_files,
|
|
||||||
stream_processes=stream_processes,
|
|
||||||
)
|
|
||||||
if json_output: # pragma: nocover
|
|
||||||
yield json.loads(json_output)
|
|
||||||
|
|
||||||
dispatch.call_hooks_even_if_unconfigured(
|
|
||||||
'remove_database_dumps',
|
|
||||||
hooks,
|
|
||||||
config_filename,
|
|
||||||
dump.DATABASE_HOOK_NAMES,
|
|
||||||
location,
|
|
||||||
global_arguments.dry_run,
|
|
||||||
)
|
|
||||||
command.execute_hook(
|
|
||||||
hooks.get('after_backup'),
|
|
||||||
hooks.get('umask'),
|
|
||||||
config_filename,
|
|
||||||
'post-backup',
|
|
||||||
global_arguments.dry_run,
|
|
||||||
**hook_context,
|
|
||||||
)
|
|
||||||
|
|
||||||
if 'check' in arguments and checks.repository_enabled_for_checks(repository, consistency):
|
|
||||||
command.execute_hook(
|
|
||||||
hooks.get('before_check'),
|
|
||||||
hooks.get('umask'),
|
|
||||||
config_filename,
|
|
||||||
'pre-check',
|
|
||||||
global_arguments.dry_run,
|
|
||||||
**hook_context,
|
|
||||||
)
|
|
||||||
logger.info('{}: Running consistency checks'.format(repository))
|
|
||||||
borg_check.check_archives(
|
|
||||||
repository,
|
|
||||||
location,
|
|
||||||
storage,
|
|
||||||
consistency,
|
|
||||||
local_borg_version,
|
|
||||||
local_path=local_path,
|
|
||||||
remote_path=remote_path,
|
|
||||||
progress=arguments['check'].progress,
|
|
||||||
repair=arguments['check'].repair,
|
|
||||||
only_checks=arguments['check'].only,
|
|
||||||
force=arguments['check'].force,
|
|
||||||
)
|
|
||||||
command.execute_hook(
|
|
||||||
hooks.get('after_check'),
|
|
||||||
hooks.get('umask'),
|
|
||||||
config_filename,
|
|
||||||
'post-check',
|
|
||||||
global_arguments.dry_run,
|
|
||||||
**hook_context,
|
|
||||||
)
|
|
||||||
if 'extract' in arguments:
|
|
||||||
command.execute_hook(
|
|
||||||
hooks.get('before_extract'),
|
|
||||||
hooks.get('umask'),
|
|
||||||
config_filename,
|
|
||||||
'pre-extract',
|
|
||||||
global_arguments.dry_run,
|
|
||||||
**hook_context,
|
|
||||||
)
|
|
||||||
if arguments['extract'].repository is None or validate.repositories_match(
|
|
||||||
repository, arguments['extract'].repository
|
|
||||||
):
|
|
||||||
logger.info(
|
|
||||||
'{}: Extracting archive {}'.format(repository, arguments['extract'].archive)
|
|
||||||
)
|
|
||||||
borg_extract.extract_archive(
|
|
||||||
global_arguments.dry_run,
|
|
||||||
repository,
|
repository,
|
||||||
borg_rlist.resolve_archive_name(
|
|
||||||
repository,
|
|
||||||
arguments['extract'].archive,
|
|
||||||
storage,
|
|
||||||
local_borg_version,
|
|
||||||
local_path,
|
|
||||||
remote_path,
|
|
||||||
),
|
|
||||||
arguments['extract'].paths,
|
|
||||||
location,
|
location,
|
||||||
storage,
|
storage,
|
||||||
local_borg_version,
|
|
||||||
local_path=local_path,
|
|
||||||
remote_path=remote_path,
|
|
||||||
destination_path=arguments['extract'].destination,
|
|
||||||
strip_components=arguments['extract'].strip_components,
|
|
||||||
progress=arguments['extract'].progress,
|
|
||||||
)
|
|
||||||
command.execute_hook(
|
|
||||||
hooks.get('after_extract'),
|
|
||||||
hooks.get('umask'),
|
|
||||||
config_filename,
|
|
||||||
'post-extract',
|
|
||||||
global_arguments.dry_run,
|
|
||||||
**hook_context,
|
|
||||||
)
|
|
||||||
if 'export-tar' in arguments:
|
|
||||||
if arguments['export-tar'].repository is None or validate.repositories_match(
|
|
||||||
repository, arguments['export-tar'].repository
|
|
||||||
):
|
|
||||||
logger.info(
|
|
||||||
'{}: Exporting archive {} as tar file'.format(
|
|
||||||
repository, arguments['export-tar'].archive
|
|
||||||
)
|
|
||||||
)
|
|
||||||
borg_export_tar.export_tar_archive(
|
|
||||||
global_arguments.dry_run,
|
|
||||||
repository,
|
|
||||||
borg_rlist.resolve_archive_name(
|
|
||||||
repository,
|
|
||||||
arguments['export-tar'].archive,
|
|
||||||
storage,
|
|
||||||
local_borg_version,
|
|
||||||
local_path,
|
|
||||||
remote_path,
|
|
||||||
),
|
|
||||||
arguments['export-tar'].paths,
|
|
||||||
arguments['export-tar'].destination,
|
|
||||||
storage,
|
|
||||||
local_borg_version,
|
|
||||||
local_path=local_path,
|
|
||||||
remote_path=remote_path,
|
|
||||||
tar_filter=arguments['export-tar'].tar_filter,
|
|
||||||
list_files=arguments['export-tar'].list_files,
|
|
||||||
strip_components=arguments['export-tar'].strip_components,
|
|
||||||
)
|
|
||||||
if 'mount' in arguments:
|
|
||||||
if arguments['mount'].repository is None or validate.repositories_match(
|
|
||||||
repository, arguments['mount'].repository
|
|
||||||
):
|
|
||||||
if arguments['mount'].archive:
|
|
||||||
logger.info(
|
|
||||||
'{}: Mounting archive {}'.format(repository, arguments['mount'].archive)
|
|
||||||
)
|
|
||||||
else: # pragma: nocover
|
|
||||||
logger.info('{}: Mounting repository'.format(repository))
|
|
||||||
|
|
||||||
borg_mount.mount_archive(
|
|
||||||
repository,
|
|
||||||
borg_rlist.resolve_archive_name(
|
|
||||||
repository,
|
|
||||||
arguments['mount'].archive,
|
|
||||||
storage,
|
|
||||||
local_borg_version,
|
|
||||||
local_path,
|
|
||||||
remote_path,
|
|
||||||
),
|
|
||||||
arguments['mount'].mount_point,
|
|
||||||
arguments['mount'].paths,
|
|
||||||
arguments['mount'].foreground,
|
|
||||||
arguments['mount'].options,
|
|
||||||
storage,
|
|
||||||
local_borg_version,
|
|
||||||
local_path=local_path,
|
|
||||||
remote_path=remote_path,
|
|
||||||
)
|
|
||||||
if 'restore' in arguments: # pragma: nocover
|
|
||||||
if arguments['restore'].repository is None or validate.repositories_match(
|
|
||||||
repository, arguments['restore'].repository
|
|
||||||
):
|
|
||||||
logger.info(
|
|
||||||
'{}: Restoring databases from archive {}'.format(
|
|
||||||
repository, arguments['restore'].archive
|
|
||||||
)
|
|
||||||
)
|
|
||||||
dispatch.call_hooks_even_if_unconfigured(
|
|
||||||
'remove_database_dumps',
|
|
||||||
hooks,
|
hooks,
|
||||||
repository,
|
hook_context,
|
||||||
dump.DATABASE_HOOK_NAMES,
|
|
||||||
location,
|
|
||||||
global_arguments.dry_run,
|
|
||||||
)
|
|
||||||
|
|
||||||
restore_names = arguments['restore'].databases or []
|
|
||||||
if 'all' in restore_names:
|
|
||||||
restore_names = []
|
|
||||||
|
|
||||||
archive_name = borg_rlist.resolve_archive_name(
|
|
||||||
repository,
|
|
||||||
arguments['restore'].archive,
|
|
||||||
storage,
|
|
||||||
local_borg_version,
|
local_borg_version,
|
||||||
|
action_arguments,
|
||||||
|
global_arguments,
|
||||||
|
dry_run_label,
|
||||||
local_path,
|
local_path,
|
||||||
remote_path,
|
remote_path,
|
||||||
)
|
)
|
||||||
found_names = set()
|
elif action_name == 'prune':
|
||||||
|
borgmatic.actions.prune.run_prune(
|
||||||
for hook_name, per_hook_restore_databases in hooks.items():
|
config_filename,
|
||||||
if hook_name not in dump.DATABASE_HOOK_NAMES:
|
repository,
|
||||||
continue
|
storage,
|
||||||
|
retention,
|
||||||
for restore_database in per_hook_restore_databases:
|
|
||||||
database_name = restore_database['name']
|
|
||||||
if restore_names and database_name not in restore_names:
|
|
||||||
continue
|
|
||||||
|
|
||||||
found_names.add(database_name)
|
|
||||||
dump_pattern = dispatch.call_hooks(
|
|
||||||
'make_database_dump_pattern',
|
|
||||||
hooks,
|
|
||||||
repository,
|
|
||||||
dump.DATABASE_HOOK_NAMES,
|
|
||||||
location,
|
|
||||||
database_name,
|
|
||||||
)[hook_name]
|
|
||||||
|
|
||||||
# Kick off a single database extract to stdout.
|
|
||||||
extract_process = borg_extract.extract_archive(
|
|
||||||
dry_run=global_arguments.dry_run,
|
|
||||||
repository=repository,
|
|
||||||
archive=archive_name,
|
|
||||||
paths=dump.convert_glob_patterns_to_borg_patterns([dump_pattern]),
|
|
||||||
location_config=location,
|
|
||||||
storage_config=storage,
|
|
||||||
local_borg_version=local_borg_version,
|
|
||||||
local_path=local_path,
|
|
||||||
remote_path=remote_path,
|
|
||||||
destination_path='/',
|
|
||||||
# A directory format dump isn't a single file, and therefore can't extract
|
|
||||||
# to stdout. In this case, the extract_process return value is None.
|
|
||||||
extract_to_stdout=bool(restore_database.get('format') != 'directory'),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Run a single database restore, consuming the extract stdout (if any).
|
|
||||||
dispatch.call_hooks(
|
|
||||||
'restore_database_dump',
|
|
||||||
{hook_name: [restore_database]},
|
|
||||||
repository,
|
|
||||||
dump.DATABASE_HOOK_NAMES,
|
|
||||||
location,
|
|
||||||
global_arguments.dry_run,
|
|
||||||
extract_process,
|
|
||||||
)
|
|
||||||
|
|
||||||
dispatch.call_hooks_even_if_unconfigured(
|
|
||||||
'remove_database_dumps',
|
|
||||||
hooks,
|
hooks,
|
||||||
repository,
|
hook_context,
|
||||||
dump.DATABASE_HOOK_NAMES,
|
local_borg_version,
|
||||||
location,
|
action_arguments,
|
||||||
global_arguments.dry_run,
|
global_arguments,
|
||||||
|
dry_run_label,
|
||||||
|
local_path,
|
||||||
|
remote_path,
|
||||||
)
|
)
|
||||||
|
elif action_name == 'compact':
|
||||||
if not restore_names and not found_names:
|
borgmatic.actions.compact.run_compact(
|
||||||
raise ValueError('No databases were found to restore')
|
config_filename,
|
||||||
|
repository,
|
||||||
missing_names = sorted(set(restore_names) - found_names)
|
storage,
|
||||||
if missing_names:
|
retention,
|
||||||
raise ValueError(
|
hooks,
|
||||||
'Cannot restore database(s) {} missing from borgmatic\'s configuration'.format(
|
hook_context,
|
||||||
', '.join(missing_names)
|
local_borg_version,
|
||||||
)
|
action_arguments,
|
||||||
|
global_arguments,
|
||||||
|
dry_run_label,
|
||||||
|
local_path,
|
||||||
|
remote_path,
|
||||||
|
)
|
||||||
|
elif action_name == 'check':
|
||||||
|
if checks.repository_enabled_for_checks(repository, consistency):
|
||||||
|
borgmatic.actions.check.run_check(
|
||||||
|
config_filename,
|
||||||
|
repository,
|
||||||
|
location,
|
||||||
|
storage,
|
||||||
|
consistency,
|
||||||
|
hooks,
|
||||||
|
hook_context,
|
||||||
|
local_borg_version,
|
||||||
|
action_arguments,
|
||||||
|
global_arguments,
|
||||||
|
local_path,
|
||||||
|
remote_path,
|
||||||
)
|
)
|
||||||
if 'rlist' in arguments:
|
elif action_name == 'extract':
|
||||||
if arguments['rlist'].repository is None or validate.repositories_match(
|
borgmatic.actions.extract.run_extract(
|
||||||
repository, arguments['rlist'].repository
|
config_filename,
|
||||||
):
|
|
||||||
rlist_arguments = copy.copy(arguments['rlist'])
|
|
||||||
if not rlist_arguments.json: # pragma: nocover
|
|
||||||
logger.warning('{}: Listing repository'.format(repository))
|
|
||||||
json_output = borg_rlist.list_repository(
|
|
||||||
repository,
|
repository,
|
||||||
|
location,
|
||||||
storage,
|
storage,
|
||||||
|
hooks,
|
||||||
|
hook_context,
|
||||||
local_borg_version,
|
local_borg_version,
|
||||||
rlist_arguments=rlist_arguments,
|
action_arguments,
|
||||||
local_path=local_path,
|
global_arguments,
|
||||||
remote_path=remote_path,
|
|
||||||
)
|
|
||||||
if json_output: # pragma: nocover
|
|
||||||
yield json.loads(json_output)
|
|
||||||
if 'list' in arguments:
|
|
||||||
if arguments['list'].repository is None or validate.repositories_match(
|
|
||||||
repository, arguments['list'].repository
|
|
||||||
):
|
|
||||||
list_arguments = copy.copy(arguments['list'])
|
|
||||||
if not list_arguments.json: # pragma: nocover
|
|
||||||
if list_arguments.find_paths:
|
|
||||||
logger.warning('{}: Searching archives'.format(repository))
|
|
||||||
elif not list_arguments.archive:
|
|
||||||
logger.warning('{}: Listing archives'.format(repository))
|
|
||||||
list_arguments.archive = borg_rlist.resolve_archive_name(
|
|
||||||
repository,
|
|
||||||
list_arguments.archive,
|
|
||||||
storage,
|
|
||||||
local_borg_version,
|
|
||||||
local_path,
|
local_path,
|
||||||
remote_path,
|
remote_path,
|
||||||
)
|
)
|
||||||
json_output = borg_list.list_archive(
|
elif action_name == 'export-tar':
|
||||||
|
borgmatic.actions.export_tar.run_export_tar(
|
||||||
repository,
|
repository,
|
||||||
storage,
|
storage,
|
||||||
local_borg_version,
|
local_borg_version,
|
||||||
list_arguments=list_arguments,
|
action_arguments,
|
||||||
local_path=local_path,
|
global_arguments,
|
||||||
remote_path=remote_path,
|
|
||||||
)
|
|
||||||
if json_output: # pragma: nocover
|
|
||||||
yield json.loads(json_output)
|
|
||||||
if 'rinfo' in arguments:
|
|
||||||
if arguments['rinfo'].repository is None or validate.repositories_match(
|
|
||||||
repository, arguments['rinfo'].repository
|
|
||||||
):
|
|
||||||
rinfo_arguments = copy.copy(arguments['rinfo'])
|
|
||||||
if not rinfo_arguments.json: # pragma: nocover
|
|
||||||
logger.warning('{}: Displaying repository summary information'.format(repository))
|
|
||||||
json_output = borg_rinfo.display_repository_info(
|
|
||||||
repository,
|
|
||||||
storage,
|
|
||||||
local_borg_version,
|
|
||||||
rinfo_arguments=rinfo_arguments,
|
|
||||||
local_path=local_path,
|
|
||||||
remote_path=remote_path,
|
|
||||||
)
|
|
||||||
if json_output: # pragma: nocover
|
|
||||||
yield json.loads(json_output)
|
|
||||||
if 'info' in arguments:
|
|
||||||
if arguments['info'].repository is None or validate.repositories_match(
|
|
||||||
repository, arguments['info'].repository
|
|
||||||
):
|
|
||||||
info_arguments = copy.copy(arguments['info'])
|
|
||||||
if not info_arguments.json: # pragma: nocover
|
|
||||||
logger.warning('{}: Displaying archive summary information'.format(repository))
|
|
||||||
info_arguments.archive = borg_rlist.resolve_archive_name(
|
|
||||||
repository,
|
|
||||||
info_arguments.archive,
|
|
||||||
storage,
|
|
||||||
local_borg_version,
|
|
||||||
local_path,
|
local_path,
|
||||||
remote_path,
|
remote_path,
|
||||||
)
|
)
|
||||||
json_output = borg_info.display_archives_info(
|
elif action_name == 'mount':
|
||||||
|
borgmatic.actions.mount.run_mount(
|
||||||
repository,
|
repository,
|
||||||
storage,
|
storage,
|
||||||
local_borg_version,
|
local_borg_version,
|
||||||
info_arguments=info_arguments,
|
arguments['mount'],
|
||||||
local_path=local_path,
|
|
||||||
remote_path=remote_path,
|
|
||||||
)
|
|
||||||
if json_output: # pragma: nocover
|
|
||||||
yield json.loads(json_output)
|
|
||||||
if 'break-lock' in arguments:
|
|
||||||
if arguments['break-lock'].repository is None or validate.repositories_match(
|
|
||||||
repository, arguments['break-lock'].repository
|
|
||||||
):
|
|
||||||
logger.warning(f'{repository}: Breaking repository and cache locks')
|
|
||||||
borg_break_lock.break_lock(
|
|
||||||
repository,
|
|
||||||
storage,
|
|
||||||
local_borg_version,
|
|
||||||
local_path=local_path,
|
|
||||||
remote_path=remote_path,
|
|
||||||
)
|
|
||||||
if 'borg' in arguments:
|
|
||||||
if arguments['borg'].repository is None or validate.repositories_match(
|
|
||||||
repository, arguments['borg'].repository
|
|
||||||
):
|
|
||||||
logger.warning('{}: Running arbitrary Borg command'.format(repository))
|
|
||||||
archive_name = borg_rlist.resolve_archive_name(
|
|
||||||
repository,
|
|
||||||
arguments['borg'].archive,
|
|
||||||
storage,
|
|
||||||
local_borg_version,
|
|
||||||
local_path,
|
local_path,
|
||||||
remote_path,
|
remote_path,
|
||||||
)
|
)
|
||||||
borg_borg.run_arbitrary_borg(
|
elif action_name == 'restore':
|
||||||
|
borgmatic.actions.restore.run_restore(
|
||||||
|
repository,
|
||||||
|
location,
|
||||||
|
storage,
|
||||||
|
hooks,
|
||||||
|
local_borg_version,
|
||||||
|
action_arguments,
|
||||||
|
global_arguments,
|
||||||
|
local_path,
|
||||||
|
remote_path,
|
||||||
|
)
|
||||||
|
elif action_name == 'rlist':
|
||||||
|
yield from borgmatic.actions.rlist.run_rlist(
|
||||||
|
repository, storage, local_borg_version, action_arguments, local_path, remote_path,
|
||||||
|
)
|
||||||
|
elif action_name == 'list':
|
||||||
|
yield from borgmatic.actions.list.run_list(
|
||||||
|
repository, storage, local_borg_version, action_arguments, local_path, remote_path,
|
||||||
|
)
|
||||||
|
elif action_name == 'rinfo':
|
||||||
|
yield from borgmatic.actions.rinfo.run_rinfo(
|
||||||
|
repository, storage, local_borg_version, action_arguments, local_path, remote_path,
|
||||||
|
)
|
||||||
|
elif action_name == 'info':
|
||||||
|
yield from borgmatic.actions.info.run_info(
|
||||||
|
repository, storage, local_borg_version, action_arguments, local_path, remote_path,
|
||||||
|
)
|
||||||
|
elif action_name == 'break-lock':
|
||||||
|
borgmatic.actions.break_lock.run_break_lock(
|
||||||
repository,
|
repository,
|
||||||
storage,
|
storage,
|
||||||
local_borg_version,
|
local_borg_version,
|
||||||
options=arguments['borg'].options,
|
arguments['break-lock'],
|
||||||
archive=archive_name,
|
local_path,
|
||||||
local_path=local_path,
|
remote_path,
|
||||||
remote_path=remote_path,
|
)
|
||||||
|
elif action_name == 'borg':
|
||||||
|
borgmatic.actions.borg.run_borg(
|
||||||
|
repository, storage, local_borg_version, action_arguments, local_path, remote_path,
|
||||||
)
|
)
|
||||||
|
|
||||||
command.execute_hook(
|
command.execute_hook(
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import functools
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
@ -6,43 +7,17 @@ import ruamel.yaml
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class Yaml_with_loader_stream(ruamel.yaml.YAML):
|
def include_configuration(loader, filename_node, include_directory):
|
||||||
'''
|
'''
|
||||||
A derived class of ruamel.yaml.YAML that simply tacks the loaded stream (file object) onto the
|
Given a ruamel.yaml.loader.Loader, a ruamel.yaml.serializer.ScalarNode containing the included
|
||||||
loader class so that it's available anywhere that's passed a loader (in this case,
|
filename, and an include directory path to search for matching files, load the given YAML
|
||||||
include_configuration() below).
|
filename (ignoring the given loader so we can use our own) and return its contents as a data
|
||||||
'''
|
structure of nested dicts and lists. If the filename is relative, probe for it within 1. the
|
||||||
|
current working directory and 2. the given include directory.
|
||||||
def get_constructor_parser(self, stream):
|
|
||||||
constructor, parser = super(Yaml_with_loader_stream, self).get_constructor_parser(stream)
|
|
||||||
constructor.loader.stream = stream
|
|
||||||
return constructor, parser
|
|
||||||
|
|
||||||
|
|
||||||
def load_configuration(filename):
|
|
||||||
'''
|
|
||||||
Load the given configuration file and return its contents as a data structure of nested dicts
|
|
||||||
and lists.
|
|
||||||
|
|
||||||
Raise ruamel.yaml.error.YAMLError if something goes wrong parsing the YAML, or RecursionError
|
|
||||||
if there are too many recursive includes.
|
|
||||||
'''
|
|
||||||
yaml = Yaml_with_loader_stream(typ='safe')
|
|
||||||
yaml.Constructor = Include_constructor
|
|
||||||
|
|
||||||
return yaml.load(open(filename))
|
|
||||||
|
|
||||||
|
|
||||||
def include_configuration(loader, filename_node):
|
|
||||||
'''
|
|
||||||
Load the given YAML filename (ignoring the given loader so we can use our own) and return its
|
|
||||||
contents as a data structure of nested dicts and lists. If the filename is relative, probe for
|
|
||||||
it within 1. the current working directory and 2. the directory containing the YAML file doing
|
|
||||||
the including.
|
|
||||||
|
|
||||||
Raise FileNotFoundError if an included file was not found.
|
Raise FileNotFoundError if an included file was not found.
|
||||||
'''
|
'''
|
||||||
include_directories = [os.getcwd(), os.path.abspath(os.path.dirname(loader.stream.name))]
|
include_directories = [os.getcwd(), os.path.abspath(include_directory)]
|
||||||
include_filename = os.path.expanduser(filename_node.value)
|
include_filename = os.path.expanduser(filename_node.value)
|
||||||
|
|
||||||
if not os.path.isabs(include_filename):
|
if not os.path.isabs(include_filename):
|
||||||
|
@ -62,6 +37,70 @@ def include_configuration(loader, filename_node):
|
||||||
return load_configuration(include_filename)
|
return load_configuration(include_filename)
|
||||||
|
|
||||||
|
|
||||||
|
class Include_constructor(ruamel.yaml.SafeConstructor):
|
||||||
|
'''
|
||||||
|
A YAML "constructor" (a ruamel.yaml concept) that supports a custom "!include" tag for including
|
||||||
|
separate YAML configuration files. Example syntax: `retention: !include common.yaml`
|
||||||
|
'''
|
||||||
|
|
||||||
|
def __init__(self, preserve_quotes=None, loader=None, include_directory=None):
|
||||||
|
super(Include_constructor, self).__init__(preserve_quotes, loader)
|
||||||
|
self.add_constructor(
|
||||||
|
'!include',
|
||||||
|
functools.partial(include_configuration, include_directory=include_directory),
|
||||||
|
)
|
||||||
|
|
||||||
|
def flatten_mapping(self, node):
|
||||||
|
'''
|
||||||
|
Support the special case of deep merging included configuration into an existing mapping
|
||||||
|
using the YAML '<<' merge key. Example syntax:
|
||||||
|
|
||||||
|
```
|
||||||
|
retention:
|
||||||
|
keep_daily: 1
|
||||||
|
|
||||||
|
<<: !include common.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
These includes are deep merged into the current configuration file. For instance, in this
|
||||||
|
example, any "retention" options in common.yaml will get merged into the "retention" section
|
||||||
|
in the example configuration file.
|
||||||
|
'''
|
||||||
|
representer = ruamel.yaml.representer.SafeRepresenter()
|
||||||
|
|
||||||
|
for index, (key_node, value_node) in enumerate(node.value):
|
||||||
|
if key_node.tag == u'tag:yaml.org,2002:merge' and value_node.tag == '!include':
|
||||||
|
included_value = representer.represent_data(self.construct_object(value_node))
|
||||||
|
node.value[index] = (key_node, included_value)
|
||||||
|
|
||||||
|
super(Include_constructor, self).flatten_mapping(node)
|
||||||
|
|
||||||
|
node.value = deep_merge_nodes(node.value)
|
||||||
|
|
||||||
|
|
||||||
|
def load_configuration(filename):
|
||||||
|
'''
|
||||||
|
Load the given configuration file and return its contents as a data structure of nested dicts
|
||||||
|
and lists.
|
||||||
|
|
||||||
|
Raise ruamel.yaml.error.YAMLError if something goes wrong parsing the YAML, or RecursionError
|
||||||
|
if there are too many recursive includes.
|
||||||
|
'''
|
||||||
|
# Use an embedded derived class for the include constructor so as to capture the filename
|
||||||
|
# value. (functools.partial doesn't work for this use case because yaml.Constructor has to be
|
||||||
|
# an actual class.)
|
||||||
|
class Include_constructor_with_include_directory(Include_constructor):
|
||||||
|
def __init__(self, preserve_quotes=None, loader=None):
|
||||||
|
super(Include_constructor_with_include_directory, self).__init__(
|
||||||
|
preserve_quotes, loader, include_directory=os.path.dirname(filename)
|
||||||
|
)
|
||||||
|
|
||||||
|
yaml = ruamel.yaml.YAML(typ='safe')
|
||||||
|
yaml.Constructor = Include_constructor_with_include_directory
|
||||||
|
|
||||||
|
return yaml.load(open(filename))
|
||||||
|
|
||||||
|
|
||||||
DELETED_NODE = object()
|
DELETED_NODE = object()
|
||||||
|
|
||||||
|
|
||||||
|
@ -175,41 +214,3 @@ def deep_merge_nodes(nodes):
|
||||||
return [
|
return [
|
||||||
replaced_nodes.get(node, node) for node in nodes if replaced_nodes.get(node) != DELETED_NODE
|
replaced_nodes.get(node, node) for node in nodes if replaced_nodes.get(node) != DELETED_NODE
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class Include_constructor(ruamel.yaml.SafeConstructor):
|
|
||||||
'''
|
|
||||||
A YAML "constructor" (a ruamel.yaml concept) that supports a custom "!include" tag for including
|
|
||||||
separate YAML configuration files. Example syntax: `retention: !include common.yaml`
|
|
||||||
'''
|
|
||||||
|
|
||||||
def __init__(self, preserve_quotes=None, loader=None):
|
|
||||||
super(Include_constructor, self).__init__(preserve_quotes, loader)
|
|
||||||
self.add_constructor('!include', include_configuration)
|
|
||||||
|
|
||||||
def flatten_mapping(self, node):
|
|
||||||
'''
|
|
||||||
Support the special case of deep merging included configuration into an existing mapping
|
|
||||||
using the YAML '<<' merge key. Example syntax:
|
|
||||||
|
|
||||||
```
|
|
||||||
retention:
|
|
||||||
keep_daily: 1
|
|
||||||
|
|
||||||
<<: !include common.yaml
|
|
||||||
```
|
|
||||||
|
|
||||||
These includes are deep merged into the current configuration file. For instance, in this
|
|
||||||
example, any "retention" options in common.yaml will get merged into the "retention" section
|
|
||||||
in the example configuration file.
|
|
||||||
'''
|
|
||||||
representer = ruamel.yaml.representer.SafeRepresenter()
|
|
||||||
|
|
||||||
for index, (key_node, value_node) in enumerate(node.value):
|
|
||||||
if key_node.tag == u'tag:yaml.org,2002:merge' and value_node.tag == '!include':
|
|
||||||
included_value = representer.represent_data(self.construct_object(value_node))
|
|
||||||
node.value[index] = (key_node, included_value)
|
|
||||||
|
|
||||||
super(Include_constructor, self).flatten_mapping(node)
|
|
||||||
|
|
||||||
node.value = deep_merge_nodes(node.value)
|
|
||||||
|
|
|
@ -8,49 +8,53 @@ def normalize(config_filename, config):
|
||||||
message warnings produced based on the normalization performed.
|
message warnings produced based on the normalization performed.
|
||||||
'''
|
'''
|
||||||
logs = []
|
logs = []
|
||||||
|
location = config.get('location') or {}
|
||||||
|
storage = config.get('storage') or {}
|
||||||
|
consistency = config.get('consistency') or {}
|
||||||
|
hooks = config.get('hooks') or {}
|
||||||
|
|
||||||
# Upgrade exclude_if_present from a string to a list.
|
# Upgrade exclude_if_present from a string to a list.
|
||||||
exclude_if_present = config.get('location', {}).get('exclude_if_present')
|
exclude_if_present = location.get('exclude_if_present')
|
||||||
if isinstance(exclude_if_present, str):
|
if isinstance(exclude_if_present, str):
|
||||||
config['location']['exclude_if_present'] = [exclude_if_present]
|
config['location']['exclude_if_present'] = [exclude_if_present]
|
||||||
|
|
||||||
# Upgrade various monitoring hooks from a string to a dict.
|
# Upgrade various monitoring hooks from a string to a dict.
|
||||||
healthchecks = config.get('hooks', {}).get('healthchecks')
|
healthchecks = hooks.get('healthchecks')
|
||||||
if isinstance(healthchecks, str):
|
if isinstance(healthchecks, str):
|
||||||
config['hooks']['healthchecks'] = {'ping_url': healthchecks}
|
config['hooks']['healthchecks'] = {'ping_url': healthchecks}
|
||||||
|
|
||||||
cronitor = config.get('hooks', {}).get('cronitor')
|
cronitor = hooks.get('cronitor')
|
||||||
if isinstance(cronitor, str):
|
if isinstance(cronitor, str):
|
||||||
config['hooks']['cronitor'] = {'ping_url': cronitor}
|
config['hooks']['cronitor'] = {'ping_url': cronitor}
|
||||||
|
|
||||||
pagerduty = config.get('hooks', {}).get('pagerduty')
|
pagerduty = hooks.get('pagerduty')
|
||||||
if isinstance(pagerduty, str):
|
if isinstance(pagerduty, str):
|
||||||
config['hooks']['pagerduty'] = {'integration_key': pagerduty}
|
config['hooks']['pagerduty'] = {'integration_key': pagerduty}
|
||||||
|
|
||||||
cronhub = config.get('hooks', {}).get('cronhub')
|
cronhub = hooks.get('cronhub')
|
||||||
if isinstance(cronhub, str):
|
if isinstance(cronhub, str):
|
||||||
config['hooks']['cronhub'] = {'ping_url': cronhub}
|
config['hooks']['cronhub'] = {'ping_url': cronhub}
|
||||||
|
|
||||||
# Upgrade consistency checks from a list of strings to a list of dicts.
|
# Upgrade consistency checks from a list of strings to a list of dicts.
|
||||||
checks = config.get('consistency', {}).get('checks')
|
checks = consistency.get('checks')
|
||||||
if isinstance(checks, list) and len(checks) and isinstance(checks[0], str):
|
if isinstance(checks, list) and len(checks) and isinstance(checks[0], str):
|
||||||
config['consistency']['checks'] = [{'name': check_type} for check_type in checks]
|
config['consistency']['checks'] = [{'name': check_type} for check_type in checks]
|
||||||
|
|
||||||
# Rename various configuration options.
|
# Rename various configuration options.
|
||||||
numeric_owner = config.get('location', {}).pop('numeric_owner', None)
|
numeric_owner = location.pop('numeric_owner', None)
|
||||||
if numeric_owner is not None:
|
if numeric_owner is not None:
|
||||||
config['location']['numeric_ids'] = numeric_owner
|
config['location']['numeric_ids'] = numeric_owner
|
||||||
|
|
||||||
bsd_flags = config.get('location', {}).pop('bsd_flags', None)
|
bsd_flags = location.pop('bsd_flags', None)
|
||||||
if bsd_flags is not None:
|
if bsd_flags is not None:
|
||||||
config['location']['flags'] = bsd_flags
|
config['location']['flags'] = bsd_flags
|
||||||
|
|
||||||
remote_rate_limit = config.get('storage', {}).pop('remote_rate_limit', None)
|
remote_rate_limit = storage.pop('remote_rate_limit', None)
|
||||||
if remote_rate_limit is not None:
|
if remote_rate_limit is not None:
|
||||||
config['storage']['upload_rate_limit'] = remote_rate_limit
|
config['storage']['upload_rate_limit'] = remote_rate_limit
|
||||||
|
|
||||||
# Upgrade remote repositories to ssh:// syntax, required in Borg 2.
|
# Upgrade remote repositories to ssh:// syntax, required in Borg 2.
|
||||||
repositories = config.get('location', {}).get('repositories')
|
repositories = location.get('repositories')
|
||||||
if repositories:
|
if repositories:
|
||||||
config['location']['repositories'] = []
|
config['location']['repositories'] = []
|
||||||
for repository in repositories:
|
for repository in repositories:
|
||||||
|
|
|
@ -240,6 +240,16 @@ properties:
|
||||||
for details. Defaults to checkpoints every 1800 seconds (30
|
for details. Defaults to checkpoints every 1800 seconds (30
|
||||||
minutes).
|
minutes).
|
||||||
example: 1800
|
example: 1800
|
||||||
|
checkpoint_volume:
|
||||||
|
type: integer
|
||||||
|
description: |
|
||||||
|
Number of backed up bytes between each checkpoint during a
|
||||||
|
long-running backup. Only supported with Borg 2+. See
|
||||||
|
https://borgbackup.readthedocs.io/en/stable/faq.html
|
||||||
|
for details. Defaults to only time-based checkpointing (see
|
||||||
|
"checkpoint_interval") instead of volume-based
|
||||||
|
checkpointing.
|
||||||
|
example: 1048576
|
||||||
chunker_params:
|
chunker_params:
|
||||||
type: string
|
type: string
|
||||||
description: |
|
description: |
|
||||||
|
@ -359,6 +369,11 @@ properties:
|
||||||
description: |
|
description: |
|
||||||
Extra command-line options to pass to "borg init".
|
Extra command-line options to pass to "borg init".
|
||||||
example: "--extra-option"
|
example: "--extra-option"
|
||||||
|
create:
|
||||||
|
type: string
|
||||||
|
description: |
|
||||||
|
Extra command-line options to pass to "borg create".
|
||||||
|
example: "--extra-option"
|
||||||
prune:
|
prune:
|
||||||
type: string
|
type: string
|
||||||
description: |
|
description: |
|
||||||
|
@ -369,11 +384,6 @@ properties:
|
||||||
description: |
|
description: |
|
||||||
Extra command-line options to pass to "borg compact".
|
Extra command-line options to pass to "borg compact".
|
||||||
example: "--extra-option"
|
example: "--extra-option"
|
||||||
create:
|
|
||||||
type: string
|
|
||||||
description: |
|
|
||||||
Extra command-line options to pass to "borg create".
|
|
||||||
example: "--extra-option"
|
|
||||||
check:
|
check:
|
||||||
type: string
|
type: string
|
||||||
description: |
|
description: |
|
||||||
|
@ -653,11 +663,11 @@ properties:
|
||||||
type: string
|
type: string
|
||||||
description: |
|
description: |
|
||||||
List of one or more shell commands or scripts to execute
|
List of one or more shell commands or scripts to execute
|
||||||
when an exception occurs during a "prune", "compact",
|
when an exception occurs during a "create", "prune",
|
||||||
"create", or "check" action or an associated before/after
|
"compact", or "check" action or an associated before/after
|
||||||
hook.
|
hook.
|
||||||
example:
|
example:
|
||||||
- echo "Error during prune/compact/create/check."
|
- echo "Error during create/prune/compact/check."
|
||||||
before_everything:
|
before_everything:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
|
@ -691,10 +701,13 @@ properties:
|
||||||
type: string
|
type: string
|
||||||
description: |
|
description: |
|
||||||
Database name (required if using this hook). Or
|
Database name (required if using this hook). Or
|
||||||
"all" to dump all databases on the host. Note
|
"all" to dump all databases on the host. (Also
|
||||||
that using this database hook implicitly enables
|
set the "format" to dump each database to a
|
||||||
both read_special and one_file_system (see
|
separate file instead of one combined file.)
|
||||||
above) to support dump and restore streaming.
|
Note that using this database hook implicitly
|
||||||
|
enables both read_special and one_file_system
|
||||||
|
(see above) to support dump and restore
|
||||||
|
streaming.
|
||||||
example: users
|
example: users
|
||||||
hostname:
|
hostname:
|
||||||
type: string
|
type: string
|
||||||
|
@ -729,9 +742,14 @@ properties:
|
||||||
description: |
|
description: |
|
||||||
Database dump output format. One of "plain",
|
Database dump output format. One of "plain",
|
||||||
"custom", "directory", or "tar". Defaults to
|
"custom", "directory", or "tar". Defaults to
|
||||||
"custom" (unlike raw pg_dump). See pg_dump
|
"custom" (unlike raw pg_dump) for a single
|
||||||
documentation for details. Note that format is
|
database. Or, when database name is "all" and
|
||||||
ignored when the database name is "all".
|
format is blank, dumps all databases to a single
|
||||||
|
file. But if a format is specified with an "all"
|
||||||
|
database name, dumps each database to a separate
|
||||||
|
file of that format, allowing more convenient
|
||||||
|
restores of individual databases. See the
|
||||||
|
pg_dump documentation for more about formats.
|
||||||
example: directory
|
example: directory
|
||||||
ssl_mode:
|
ssl_mode:
|
||||||
type: string
|
type: string
|
||||||
|
@ -764,6 +782,32 @@ properties:
|
||||||
description: |
|
description: |
|
||||||
Path to a certificate revocation list.
|
Path to a certificate revocation list.
|
||||||
example: "/root/.postgresql/root.crl"
|
example: "/root/.postgresql/root.crl"
|
||||||
|
pg_dump_command:
|
||||||
|
type: string
|
||||||
|
description: |
|
||||||
|
Command to use instead of "pg_dump" or
|
||||||
|
"pg_dumpall". This can be used to run a specific
|
||||||
|
pg_dump version (e.g., one inside a running
|
||||||
|
docker container). Defaults to "pg_dump" for
|
||||||
|
single database dump or "pg_dumpall" to dump
|
||||||
|
all databases.
|
||||||
|
example: docker exec my_pg_container pg_dump
|
||||||
|
pg_restore_command:
|
||||||
|
type: string
|
||||||
|
description: |
|
||||||
|
Command to use instead of "pg_restore". This
|
||||||
|
can be used to run a specific pg_restore
|
||||||
|
version (e.g., one inside a running docker
|
||||||
|
container). Defaults to "pg_restore".
|
||||||
|
example: docker exec my_pg_container pg_restore
|
||||||
|
psql_command:
|
||||||
|
type: string
|
||||||
|
description: |
|
||||||
|
Command to use instead of "psql". This can be
|
||||||
|
used to run a specific psql version (e.g.,
|
||||||
|
one inside a running docker container).
|
||||||
|
Defaults to "psql".
|
||||||
|
example: docker exec my_pg_container psql
|
||||||
options:
|
options:
|
||||||
type: string
|
type: string
|
||||||
description: |
|
description: |
|
||||||
|
@ -772,6 +816,30 @@ properties:
|
||||||
any validation on them. See pg_dump
|
any validation on them. See pg_dump
|
||||||
documentation for details.
|
documentation for details.
|
||||||
example: --role=someone
|
example: --role=someone
|
||||||
|
list_options:
|
||||||
|
type: string
|
||||||
|
description: |
|
||||||
|
Additional psql options to pass directly to the
|
||||||
|
psql command that lists available databases,
|
||||||
|
without performing any validation on them. See
|
||||||
|
psql documentation for details.
|
||||||
|
example: --role=someone
|
||||||
|
restore_options:
|
||||||
|
type: string
|
||||||
|
description: |
|
||||||
|
Additional pg_restore/psql options to pass
|
||||||
|
directly to the restore command, without
|
||||||
|
performing any validation on them. See
|
||||||
|
pg_restore/psql documentation for details.
|
||||||
|
example: --role=someone
|
||||||
|
analyze_options:
|
||||||
|
type: string
|
||||||
|
description: |
|
||||||
|
Additional psql options to pass directly to the
|
||||||
|
analyze command run after a restore, without
|
||||||
|
performing any validation on them. See psql
|
||||||
|
documentation for details.
|
||||||
|
example: --role=someone
|
||||||
description: |
|
description: |
|
||||||
List of one or more PostgreSQL databases to dump before
|
List of one or more PostgreSQL databases to dump before
|
||||||
creating a backup, run once per configuration file. The
|
creating a backup, run once per configuration file. The
|
||||||
|
@ -821,14 +889,26 @@ properties:
|
||||||
configured to trust the configured username
|
configured to trust the configured username
|
||||||
without a password.
|
without a password.
|
||||||
example: trustsome1
|
example: trustsome1
|
||||||
list_options:
|
format:
|
||||||
type: string
|
type: string
|
||||||
|
enum: ['sql']
|
||||||
description: |
|
description: |
|
||||||
Additional mysql options to pass directly to
|
Database dump output format. Currenly only "sql"
|
||||||
the mysql command that lists available
|
is supported. Defaults to "sql" for a single
|
||||||
databases, without performing any validation on
|
database. Or, when database name is "all" and
|
||||||
them. See mysql documentation for details.
|
format is blank, dumps all databases to a single
|
||||||
example: --defaults-extra-file=my.cnf
|
file. But if a format is specified with an "all"
|
||||||
|
database name, dumps each database to a separate
|
||||||
|
file of that format, allowing more convenient
|
||||||
|
restores of individual databases.
|
||||||
|
example: directory
|
||||||
|
add_drop_database:
|
||||||
|
type: boolean
|
||||||
|
description: |
|
||||||
|
Use the "--add-drop-database" flag with
|
||||||
|
mysqldump, causing the database to be dropped
|
||||||
|
right before restore. Defaults to true.
|
||||||
|
example: false
|
||||||
options:
|
options:
|
||||||
type: string
|
type: string
|
||||||
description: |
|
description: |
|
||||||
|
@ -837,6 +917,22 @@ properties:
|
||||||
validation on them. See mysqldump documentation
|
validation on them. See mysqldump documentation
|
||||||
for details.
|
for details.
|
||||||
example: --skip-comments
|
example: --skip-comments
|
||||||
|
list_options:
|
||||||
|
type: string
|
||||||
|
description: |
|
||||||
|
Additional mysql options to pass directly to
|
||||||
|
the mysql command that lists available
|
||||||
|
databases, without performing any validation on
|
||||||
|
them. See mysql documentation for details.
|
||||||
|
example: --defaults-extra-file=my.cnf
|
||||||
|
restore_options:
|
||||||
|
type: string
|
||||||
|
description: |
|
||||||
|
Additional mysql options to pass directly to
|
||||||
|
the mysql command that restores database dumps,
|
||||||
|
without performing any validation on them. See
|
||||||
|
mysql documentation for details.
|
||||||
|
example: --defaults-extra-file=my.cnf
|
||||||
description: |
|
description: |
|
||||||
List of one or more MySQL/MariaDB databases to dump before
|
List of one or more MySQL/MariaDB databases to dump before
|
||||||
creating a backup, run once per configuration file. The
|
creating a backup, run once per configuration file. The
|
||||||
|
@ -845,6 +941,31 @@ properties:
|
||||||
mysqldump/mysql commands (from either MySQL or MariaDB). See
|
mysqldump/mysql commands (from either MySQL or MariaDB). See
|
||||||
https://dev.mysql.com/doc/refman/8.0/en/mysqldump.html or
|
https://dev.mysql.com/doc/refman/8.0/en/mysqldump.html or
|
||||||
https://mariadb.com/kb/en/library/mysqldump/ for details.
|
https://mariadb.com/kb/en/library/mysqldump/ for details.
|
||||||
|
sqlite_databases:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: object
|
||||||
|
required: ['path','name']
|
||||||
|
additionalProperties: false
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
description: |
|
||||||
|
This is used to tag the database dump file
|
||||||
|
with a name. It is not the path to the database
|
||||||
|
file itself. The name "all" has no special
|
||||||
|
meaning for SQLite databases.
|
||||||
|
example: users
|
||||||
|
path:
|
||||||
|
type: string
|
||||||
|
description: |
|
||||||
|
Path to the SQLite database file to dump. If
|
||||||
|
relative, it is relative to the current working
|
||||||
|
directory. Note that using this
|
||||||
|
database hook implicitly enables both
|
||||||
|
read_special and one_file_system (see above) to
|
||||||
|
support dump and restore streaming.
|
||||||
|
example: /var/lib/sqlite/users.db
|
||||||
mongodb_databases:
|
mongodb_databases:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
|
@ -909,7 +1030,15 @@ properties:
|
||||||
directly to the dump command, without performing
|
directly to the dump command, without performing
|
||||||
any validation on them. See mongodump
|
any validation on them. See mongodump
|
||||||
documentation for details.
|
documentation for details.
|
||||||
example: --role=someone
|
example: --dumpDbUsersAndRoles
|
||||||
|
restore_options:
|
||||||
|
type: string
|
||||||
|
description: |
|
||||||
|
Additional mongorestore options to pass
|
||||||
|
directly to the dump command, without performing
|
||||||
|
any validation on them. See mongorestore
|
||||||
|
documentation for details.
|
||||||
|
example: --restoreDbUsersAndRoles
|
||||||
description: |
|
description: |
|
||||||
List of one or more MongoDB databases to dump before
|
List of one or more MongoDB databases to dump before
|
||||||
creating a backup, run once per configuration file. The
|
creating a backup, run once per configuration file. The
|
||||||
|
@ -935,6 +1064,16 @@ properties:
|
||||||
description: |
|
description: |
|
||||||
The address of your self-hosted ntfy.sh instance.
|
The address of your self-hosted ntfy.sh instance.
|
||||||
example: https://ntfy.your-domain.com
|
example: https://ntfy.your-domain.com
|
||||||
|
username:
|
||||||
|
type: string
|
||||||
|
description: |
|
||||||
|
The username used for authentication.
|
||||||
|
example: testuser
|
||||||
|
password:
|
||||||
|
type: string
|
||||||
|
description: |
|
||||||
|
The password used for authentication.
|
||||||
|
example: fakepassword
|
||||||
start:
|
start:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
|
@ -1029,7 +1168,7 @@ properties:
|
||||||
type: string
|
type: string
|
||||||
description: |
|
description: |
|
||||||
Healthchecks ping URL or UUID to notify when a
|
Healthchecks ping URL or UUID to notify when a
|
||||||
backup begins, ends, or errors.
|
backup begins, ends, errors or just to send logs.
|
||||||
example: https://hc-ping.com/your-uuid-here
|
example: https://hc-ping.com/your-uuid-here
|
||||||
verify_tls:
|
verify_tls:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
@ -1041,7 +1180,8 @@ properties:
|
||||||
type: boolean
|
type: boolean
|
||||||
description: |
|
description: |
|
||||||
Send borgmatic logs to Healthchecks as part the
|
Send borgmatic logs to Healthchecks as part the
|
||||||
"finish" state. Defaults to true.
|
"finish", "fail", and "log" states. Defaults to
|
||||||
|
true.
|
||||||
example: false
|
example: false
|
||||||
ping_body_limit:
|
ping_body_limit:
|
||||||
type: integer
|
type: integer
|
||||||
|
@ -1060,10 +1200,11 @@ properties:
|
||||||
- start
|
- start
|
||||||
- finish
|
- finish
|
||||||
- fail
|
- fail
|
||||||
|
- log
|
||||||
uniqueItems: true
|
uniqueItems: true
|
||||||
description: |
|
description: |
|
||||||
List of one or more monitoring states to ping for:
|
List of one or more monitoring states to ping for:
|
||||||
"start", "finish", and/or "fail". Defaults to
|
"start", "finish", "fail", and/or "log". Defaults to
|
||||||
pinging for all states.
|
pinging for all states.
|
||||||
example:
|
example:
|
||||||
- finish
|
- finish
|
||||||
|
|
|
@ -186,5 +186,5 @@ def guard_single_repository_selected(repository, configurations):
|
||||||
|
|
||||||
if count != 1:
|
if count != 1:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
'Can\'t determine which repository to use. Use --repository to disambiguate'
|
"Can't determine which repository to use. Use --repository to disambiguate"
|
||||||
)
|
)
|
||||||
|
|
|
@ -49,7 +49,8 @@ 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
|
Given a sequence of subprocess.Popen() instances for multiple processes, log the output for each
|
||||||
process with the requested log level. Additionally, raise a CalledProcessError if a process
|
process with the requested log level. Additionally, raise a CalledProcessError if a process
|
||||||
exits with an error (or a warning for exit code 1, if that process matches the Borg local path).
|
exits with an error (or a warning for exit code 1, if that process does not match the Borg local
|
||||||
|
path).
|
||||||
|
|
||||||
If output log level is None, then instead of logging, capture output for each process and return
|
If output log level is None, then instead of logging, capture output for each process and return
|
||||||
it as a dict from the process to its output.
|
it as a dict from the process to its output.
|
||||||
|
@ -147,7 +148,7 @@ def log_outputs(processes, exclude_stdouts, output_log_level, borg_local_path):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def log_command(full_command, input_file, output_file):
|
def log_command(full_command, input_file=None, output_file=None):
|
||||||
'''
|
'''
|
||||||
Log the given command (a sequence of command/argument strings), along with its input/output file
|
Log the given command (a sequence of command/argument strings), along with its input/output file
|
||||||
paths.
|
paths.
|
||||||
|
@ -178,15 +179,14 @@ def execute_command(
|
||||||
):
|
):
|
||||||
'''
|
'''
|
||||||
Execute the given command (a sequence of command/argument strings) and log its output at the
|
Execute the given command (a sequence of command/argument strings) and log its output at the
|
||||||
given log level. If output log level is None, instead capture and return the output. (Implies
|
given log level. If an open output file object is given, then write stdout to the file and only
|
||||||
run_to_completion.) If an open output file object is given, then write stdout to the file and
|
log stderr. If an open input file object is given, then read stdin from the file. If shell is
|
||||||
only log stderr (but only if an output log level is set). If an open input file object is given,
|
True, execute the command within a shell. If an extra environment dict is given, then use it to
|
||||||
then read stdin from the file. If shell is True, execute the command within a shell. If an extra
|
augment the current environment, and pass the result into the command. If a working directory is
|
||||||
environment dict is given, then use it to augment the current environment, and pass the result
|
given, use that as the present working directory when running the command. If a Borg local path
|
||||||
into the command. If a working directory is given, use that as the present working directory
|
is given, and the command matches it (regardless of arguments), treat exit code 1 as a warning
|
||||||
when running the command. If a Borg local path is given, and the command matches it (regardless
|
instead of an error. If run to completion is False, then return the process for the command
|
||||||
of arguments), treat exit code 1 as a warning instead of an error. If run to completion is
|
without executing it to completion.
|
||||||
False, then return the process for the command without executing it to completion.
|
|
||||||
|
|
||||||
Raise subprocesses.CalledProcessError if an error occurs while running the command.
|
Raise subprocesses.CalledProcessError if an error occurs while running the command.
|
||||||
'''
|
'''
|
||||||
|
@ -195,12 +195,6 @@ def execute_command(
|
||||||
do_not_capture = bool(output_file is DO_NOT_CAPTURE)
|
do_not_capture = bool(output_file is DO_NOT_CAPTURE)
|
||||||
command = ' '.join(full_command) if shell else full_command
|
command = ' '.join(full_command) if shell else full_command
|
||||||
|
|
||||||
if output_log_level is None:
|
|
||||||
output = subprocess.check_output(
|
|
||||||
command, stderr=subprocess.STDOUT, shell=shell, env=environment, cwd=working_directory
|
|
||||||
)
|
|
||||||
return output.decode() if output is not None else None
|
|
||||||
|
|
||||||
process = subprocess.Popen(
|
process = subprocess.Popen(
|
||||||
command,
|
command,
|
||||||
stdin=input_file,
|
stdin=input_file,
|
||||||
|
@ -218,6 +212,33 @@ def execute_command(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def execute_command_and_capture_output(
|
||||||
|
full_command, capture_stderr=False, shell=False, extra_environment=None, working_directory=None,
|
||||||
|
):
|
||||||
|
'''
|
||||||
|
Execute the given command (a sequence of command/argument strings), capturing and returning its
|
||||||
|
output (stdout). If capture stderr is True, then capture and return stderr in addition to
|
||||||
|
stdout. If shell is True, execute the command within a shell. If an extra environment dict is
|
||||||
|
given, then use it to augment the current environment, and pass the result into the command. If
|
||||||
|
a working directory is given, use that as the present working directory when running the command.
|
||||||
|
|
||||||
|
Raise subprocesses.CalledProcessError if an error occurs while running the command.
|
||||||
|
'''
|
||||||
|
log_command(full_command)
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
|
return output.decode() if output is not None else None
|
||||||
|
|
||||||
|
|
||||||
def execute_command_with_processes(
|
def execute_command_with_processes(
|
||||||
full_command,
|
full_command,
|
||||||
processes,
|
processes,
|
||||||
|
|
|
@ -27,6 +27,12 @@ def ping_monitor(hook_config, config_filename, state, monitoring_log_level, dry_
|
||||||
Ping the configured Cronhub URL, modified with the monitor.State. Use the given configuration
|
Ping the configured Cronhub URL, modified with the monitor.State. Use the given configuration
|
||||||
filename in any log entries. If this is a dry run, then don't actually ping anything.
|
filename in any log entries. If this is a dry run, then don't actually ping anything.
|
||||||
'''
|
'''
|
||||||
|
if state not in MONITOR_STATE_TO_CRONHUB:
|
||||||
|
logger.debug(
|
||||||
|
f'{config_filename}: Ignoring unsupported monitoring {state.name.lower()} in Cronhub hook'
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
dry_run_label = ' (dry run; not actually pinging)' if dry_run else ''
|
dry_run_label = ' (dry run; not actually pinging)' if dry_run else ''
|
||||||
formatted_state = '/{}/'.format(MONITOR_STATE_TO_CRONHUB[state])
|
formatted_state = '/{}/'.format(MONITOR_STATE_TO_CRONHUB[state])
|
||||||
ping_url = (
|
ping_url = (
|
||||||
|
|
|
@ -27,6 +27,12 @@ def ping_monitor(hook_config, config_filename, state, monitoring_log_level, dry_
|
||||||
Ping the configured Cronitor URL, modified with the monitor.State. Use the given configuration
|
Ping the configured Cronitor URL, modified with the monitor.State. Use the given configuration
|
||||||
filename in any log entries. If this is a dry run, then don't actually ping anything.
|
filename in any log entries. If this is a dry run, then don't actually ping anything.
|
||||||
'''
|
'''
|
||||||
|
if state not in MONITOR_STATE_TO_CRONITOR:
|
||||||
|
logger.debug(
|
||||||
|
f'{config_filename}: Ignoring unsupported monitoring {state.name.lower()} in Cronitor hook'
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
dry_run_label = ' (dry run; not actually pinging)' if dry_run else ''
|
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 = '{}/{}'.format(hook_config['ping_url'], MONITOR_STATE_TO_CRONITOR[state])
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,7 @@ from borgmatic.hooks import (
|
||||||
ntfy,
|
ntfy,
|
||||||
pagerduty,
|
pagerduty,
|
||||||
postgresql,
|
postgresql,
|
||||||
|
sqlite,
|
||||||
)
|
)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -22,6 +23,7 @@ HOOK_NAME_TO_MODULE = {
|
||||||
'ntfy': ntfy,
|
'ntfy': ntfy,
|
||||||
'pagerduty': pagerduty,
|
'pagerduty': pagerduty,
|
||||||
'postgresql_databases': postgresql,
|
'postgresql_databases': postgresql,
|
||||||
|
'sqlite_databases': sqlite,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,12 @@ from borgmatic.borg.state import DEFAULT_BORGMATIC_SOURCE_DIRECTORY
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
DATABASE_HOOK_NAMES = ('postgresql_databases', 'mysql_databases', 'mongodb_databases')
|
DATABASE_HOOK_NAMES = (
|
||||||
|
'postgresql_databases',
|
||||||
|
'mysql_databases',
|
||||||
|
'mongodb_databases',
|
||||||
|
'sqlite_databases',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def make_database_dump_path(borgmatic_source_directory, database_hook_name):
|
def make_database_dump_path(borgmatic_source_directory, database_hook_name):
|
||||||
|
|
|
@ -10,6 +10,7 @@ MONITOR_STATE_TO_HEALTHCHECKS = {
|
||||||
monitor.State.START: 'start',
|
monitor.State.START: 'start',
|
||||||
monitor.State.FINISH: None, # Healthchecks doesn't append to the URL for the finished state.
|
monitor.State.FINISH: None, # Healthchecks doesn't append to the URL for the finished state.
|
||||||
monitor.State.FAIL: 'fail',
|
monitor.State.FAIL: 'fail',
|
||||||
|
monitor.State.LOG: 'log',
|
||||||
}
|
}
|
||||||
|
|
||||||
PAYLOAD_TRUNCATION_INDICATOR = '...\n'
|
PAYLOAD_TRUNCATION_INDICATOR = '...\n'
|
||||||
|
@ -117,7 +118,7 @@ def ping_monitor(hook_config, config_filename, state, monitoring_log_level, dry_
|
||||||
)
|
)
|
||||||
logger.debug('{}: Using Healthchecks ping URL {}'.format(config_filename, ping_url))
|
logger.debug('{}: Using Healthchecks ping URL {}'.format(config_filename, ping_url))
|
||||||
|
|
||||||
if state in (monitor.State.FINISH, monitor.State.FAIL):
|
if state in (monitor.State.FINISH, monitor.State.FAIL, monitor.State.LOG):
|
||||||
payload = format_buffered_logs_for_payload()
|
payload = format_buffered_logs_for_payload()
|
||||||
else:
|
else:
|
||||||
payload = ''
|
payload = ''
|
||||||
|
|
|
@ -45,13 +45,14 @@ def dump_databases(databases, log_prefix, location_config, dry_run):
|
||||||
if dry_run:
|
if dry_run:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
command = build_dump_command(database, dump_filename, dump_format)
|
||||||
|
|
||||||
if dump_format == 'directory':
|
if dump_format == 'directory':
|
||||||
dump.create_parent_directory_for_dump(dump_filename)
|
dump.create_parent_directory_for_dump(dump_filename)
|
||||||
|
execute_command(command, shell=True)
|
||||||
else:
|
else:
|
||||||
dump.create_named_pipe_for_dump(dump_filename)
|
dump.create_named_pipe_for_dump(dump_filename)
|
||||||
|
processes.append(execute_command(command, shell=True, run_to_completion=False))
|
||||||
command = build_dump_command(database, dump_filename, dump_format)
|
|
||||||
processes.append(execute_command(command, shell=True, run_to_completion=False))
|
|
||||||
|
|
||||||
return processes
|
return processes
|
||||||
|
|
||||||
|
@ -61,9 +62,9 @@ def build_dump_command(database, dump_filename, dump_format):
|
||||||
Return the mongodump command from a single database configuration.
|
Return the mongodump command from a single database configuration.
|
||||||
'''
|
'''
|
||||||
all_databases = database['name'] == 'all'
|
all_databases = database['name'] == 'all'
|
||||||
command = ['mongodump', '--archive']
|
command = ['mongodump']
|
||||||
if dump_format == 'directory':
|
if dump_format == 'directory':
|
||||||
command.append(dump_filename)
|
command.extend(('--out', dump_filename))
|
||||||
if 'hostname' in database:
|
if 'hostname' in database:
|
||||||
command.extend(('--host', database['hostname']))
|
command.extend(('--host', database['hostname']))
|
||||||
if 'port' in database:
|
if 'port' in database:
|
||||||
|
@ -79,7 +80,7 @@ def build_dump_command(database, dump_filename, dump_format):
|
||||||
if 'options' in database:
|
if 'options' in database:
|
||||||
command.extend(database['options'].split(' '))
|
command.extend(database['options'].split(' '))
|
||||||
if dump_format != 'directory':
|
if dump_format != 'directory':
|
||||||
command.extend(('>', dump_filename))
|
command.extend(('--archive', '>', dump_filename))
|
||||||
return command
|
return command
|
||||||
|
|
||||||
|
|
||||||
|
@ -145,9 +146,11 @@ def build_restore_command(extract_process, database, dump_filename):
|
||||||
'''
|
'''
|
||||||
Return the mongorestore command from a single database configuration.
|
Return the mongorestore command from a single database configuration.
|
||||||
'''
|
'''
|
||||||
command = ['mongorestore', '--archive']
|
command = ['mongorestore']
|
||||||
if not extract_process:
|
if extract_process:
|
||||||
command.append(dump_filename)
|
command.append('--archive')
|
||||||
|
else:
|
||||||
|
command.extend(('--dir', dump_filename))
|
||||||
if database['name'] != 'all':
|
if database['name'] != 'all':
|
||||||
command.extend(('--drop', '--db', database['name']))
|
command.extend(('--drop', '--db', database['name']))
|
||||||
if 'hostname' in database:
|
if 'hostname' in database:
|
||||||
|
@ -160,4 +163,6 @@ def build_restore_command(extract_process, database, dump_filename):
|
||||||
command.extend(('--password', database['password']))
|
command.extend(('--password', database['password']))
|
||||||
if 'authentication_database' in database:
|
if 'authentication_database' in database:
|
||||||
command.extend(('--authenticationDatabase', database['authentication_database']))
|
command.extend(('--authenticationDatabase', database['authentication_database']))
|
||||||
|
if 'restore_options' in database:
|
||||||
|
command.extend(database['restore_options'].split(' '))
|
||||||
return command
|
return command
|
||||||
|
|
|
@ -7,3 +7,4 @@ class State(Enum):
|
||||||
START = 1
|
START = 1
|
||||||
FINISH = 2
|
FINISH = 2
|
||||||
FAIL = 3
|
FAIL = 3
|
||||||
|
LOG = 4
|
||||||
|
|
|
@ -1,6 +1,12 @@
|
||||||
|
import copy
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
|
|
||||||
from borgmatic.execute import execute_command, execute_command_with_processes
|
from borgmatic.execute import (
|
||||||
|
execute_command,
|
||||||
|
execute_command_and_capture_output,
|
||||||
|
execute_command_with_processes,
|
||||||
|
)
|
||||||
from borgmatic.hooks import dump
|
from borgmatic.hooks import dump
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -18,16 +24,16 @@ def make_dump_path(location_config): # pragma: no cover
|
||||||
SYSTEM_DATABASE_NAMES = ('information_schema', 'mysql', 'performance_schema', 'sys')
|
SYSTEM_DATABASE_NAMES = ('information_schema', 'mysql', 'performance_schema', 'sys')
|
||||||
|
|
||||||
|
|
||||||
def database_names_to_dump(database, extra_environment, log_prefix, dry_run_label):
|
def database_names_to_dump(database, extra_environment, log_prefix, dry_run):
|
||||||
'''
|
'''
|
||||||
Given a requested database name, return the corresponding sequence of database names to dump.
|
Given a requested database config, return the corresponding sequence of database names to dump.
|
||||||
In the case of "all", query for the names of databases on the configured host and return them,
|
In the case of "all", query for the names of databases on the configured host and return them,
|
||||||
excluding any system databases that will cause problems during restore.
|
excluding any system databases that will cause problems during restore.
|
||||||
'''
|
'''
|
||||||
requested_name = database['name']
|
if database['name'] != 'all':
|
||||||
|
return (database['name'],)
|
||||||
if requested_name != 'all':
|
if dry_run:
|
||||||
return (requested_name,)
|
return ()
|
||||||
|
|
||||||
show_command = (
|
show_command = (
|
||||||
('mysql',)
|
('mysql',)
|
||||||
|
@ -39,11 +45,9 @@ def database_names_to_dump(database, extra_environment, log_prefix, dry_run_labe
|
||||||
+ ('--skip-column-names', '--batch')
|
+ ('--skip-column-names', '--batch')
|
||||||
+ ('--execute', 'show schemas')
|
+ ('--execute', 'show schemas')
|
||||||
)
|
)
|
||||||
logger.debug(
|
logger.debug(f'{log_prefix}: Querying for "all" MySQL databases to dump')
|
||||||
'{}: Querying for "all" MySQL databases to dump{}'.format(log_prefix, dry_run_label)
|
show_output = execute_command_and_capture_output(
|
||||||
)
|
show_command, extra_environment=extra_environment
|
||||||
show_output = execute_command(
|
|
||||||
show_command, output_log_level=None, extra_environment=extra_environment
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return tuple(
|
return tuple(
|
||||||
|
@ -53,6 +57,55 @@ def database_names_to_dump(database, extra_environment, log_prefix, dry_run_labe
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def execute_dump_command(
|
||||||
|
database, log_prefix, dump_path, database_names, extra_environment, dry_run, dry_run_label
|
||||||
|
):
|
||||||
|
'''
|
||||||
|
Kick off a dump for the given MySQL/MariaDB database (provided as a configuration dict) to a
|
||||||
|
named pipe constructed from the given dump path and database names. Use the given log prefix in
|
||||||
|
any log entries.
|
||||||
|
|
||||||
|
Return a subprocess.Popen instance for the dump process ready to spew to a named pipe. But if
|
||||||
|
this is a dry run, then don't actually dump anything and return None.
|
||||||
|
'''
|
||||||
|
database_name = database['name']
|
||||||
|
dump_filename = dump.make_database_dump_filename(
|
||||||
|
dump_path, database['name'], database.get('hostname')
|
||||||
|
)
|
||||||
|
if os.path.exists(dump_filename):
|
||||||
|
logger.warning(
|
||||||
|
f'{log_prefix}: Skipping duplicate dump of MySQL database "{database_name}" to {dump_filename}'
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
dump_command = (
|
||||||
|
('mysqldump',)
|
||||||
|
+ (tuple(database['options'].split(' ')) if 'options' in database else ())
|
||||||
|
+ (('--add-drop-database',) if database.get('add_drop_database', True) else ())
|
||||||
|
+ (('--host', database['hostname']) if 'hostname' in database else ())
|
||||||
|
+ (('--port', str(database['port'])) if 'port' in database else ())
|
||||||
|
+ (('--protocol', 'tcp') if 'hostname' in database or 'port' in database else ())
|
||||||
|
+ (('--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)
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
f'{log_prefix}: Dumping MySQL database "{database_name}" to {dump_filename}{dry_run_label}'
|
||||||
|
)
|
||||||
|
if dry_run:
|
||||||
|
return None
|
||||||
|
|
||||||
|
dump.create_named_pipe_for_dump(dump_filename)
|
||||||
|
|
||||||
|
return execute_command(
|
||||||
|
dump_command, shell=True, extra_environment=extra_environment, run_to_completion=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def dump_databases(databases, log_prefix, location_config, dry_run):
|
def dump_databases(databases, log_prefix, location_config, dry_run):
|
||||||
'''
|
'''
|
||||||
Dump the given MySQL/MariaDB databases to a named pipe. The databases are supplied as a sequence
|
Dump the given MySQL/MariaDB databases to a named pipe. The databases are supplied as a sequence
|
||||||
|
@ -69,52 +122,47 @@ def dump_databases(databases, log_prefix, location_config, dry_run):
|
||||||
logger.info('{}: Dumping MySQL databases{}'.format(log_prefix, dry_run_label))
|
logger.info('{}: Dumping MySQL databases{}'.format(log_prefix, dry_run_label))
|
||||||
|
|
||||||
for database in databases:
|
for database in databases:
|
||||||
requested_name = database['name']
|
dump_path = make_dump_path(location_config)
|
||||||
dump_filename = dump.make_database_dump_filename(
|
|
||||||
make_dump_path(location_config), requested_name, database.get('hostname')
|
|
||||||
)
|
|
||||||
extra_environment = {'MYSQL_PWD': database['password']} if 'password' in database else None
|
extra_environment = {'MYSQL_PWD': database['password']} if 'password' in database else None
|
||||||
dump_database_names = database_names_to_dump(
|
dump_database_names = database_names_to_dump(
|
||||||
database, extra_environment, log_prefix, dry_run_label
|
database, extra_environment, log_prefix, dry_run
|
||||||
)
|
)
|
||||||
|
|
||||||
if not dump_database_names:
|
if not dump_database_names:
|
||||||
|
if dry_run:
|
||||||
|
continue
|
||||||
|
|
||||||
raise ValueError('Cannot find any MySQL databases to dump.')
|
raise ValueError('Cannot find any MySQL databases to dump.')
|
||||||
|
|
||||||
dump_command = (
|
if database['name'] == 'all' and database.get('format'):
|
||||||
('mysqldump',)
|
for dump_name in dump_database_names:
|
||||||
+ (tuple(database['options'].split(' ')) if 'options' in database else ())
|
renamed_database = copy.copy(database)
|
||||||
+ ('--add-drop-database',)
|
renamed_database['name'] = dump_name
|
||||||
+ (('--host', database['hostname']) if 'hostname' in database else ())
|
processes.append(
|
||||||
+ (('--port', str(database['port'])) if 'port' in database else ())
|
execute_dump_command(
|
||||||
+ (('--protocol', 'tcp') if 'hostname' in database or 'port' in database else ())
|
renamed_database,
|
||||||
+ (('--user', database['username']) if 'username' in database else ())
|
log_prefix,
|
||||||
+ ('--databases',)
|
dump_path,
|
||||||
+ dump_database_names
|
(dump_name,),
|
||||||
# Use shell redirection rather than execute_command(output_file=open(...)) to prevent
|
extra_environment,
|
||||||
# the open() call on a named pipe from hanging the main borgmatic process.
|
dry_run,
|
||||||
+ ('>', dump_filename)
|
dry_run_label,
|
||||||
)
|
)
|
||||||
|
)
|
||||||
logger.debug(
|
else:
|
||||||
'{}: Dumping MySQL database {} to {}{}'.format(
|
processes.append(
|
||||||
log_prefix, requested_name, dump_filename, dry_run_label
|
execute_dump_command(
|
||||||
|
database,
|
||||||
|
log_prefix,
|
||||||
|
dump_path,
|
||||||
|
dump_database_names,
|
||||||
|
extra_environment,
|
||||||
|
dry_run,
|
||||||
|
dry_run_label,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
|
||||||
if dry_run:
|
|
||||||
continue
|
|
||||||
|
|
||||||
dump.create_named_pipe_for_dump(dump_filename)
|
return [process for process in processes if process]
|
||||||
|
|
||||||
processes.append(
|
|
||||||
execute_command(
|
|
||||||
dump_command,
|
|
||||||
shell=True,
|
|
||||||
extra_environment=extra_environment,
|
|
||||||
run_to_completion=False,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
return processes
|
|
||||||
|
|
||||||
|
|
||||||
def remove_database_dumps(databases, log_prefix, location_config, dry_run): # pragma: no cover
|
def remove_database_dumps(databases, log_prefix, location_config, dry_run): # pragma: no cover
|
||||||
|
@ -153,6 +201,7 @@ def restore_database_dump(database_config, log_prefix, location_config, dry_run,
|
||||||
database = database_config[0]
|
database = database_config[0]
|
||||||
restore_command = (
|
restore_command = (
|
||||||
('mysql', '--batch')
|
('mysql', '--batch')
|
||||||
|
+ (tuple(database['restore_options'].split(' ')) if 'restore_options' in database else ())
|
||||||
+ (('--host', database['hostname']) if 'hostname' in database else ())
|
+ (('--host', database['hostname']) if 'hostname' in database else ())
|
||||||
+ (('--port', str(database['port'])) if 'port' in database else ())
|
+ (('--port', str(database['port'])) if 'port' in database else ())
|
||||||
+ (('--protocol', 'tcp') if 'hostname' in database or 'port' in database else ())
|
+ (('--protocol', 'tcp') if 'hostname' in database or 'port' in database else ())
|
||||||
|
|
|
@ -2,16 +2,8 @@ import logging
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from borgmatic.hooks import monitor
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
MONITOR_STATE_TO_NTFY = {
|
|
||||||
monitor.State.START: None,
|
|
||||||
monitor.State.FINISH: None,
|
|
||||||
monitor.State.FAIL: None,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def initialize_monitor(
|
def initialize_monitor(
|
||||||
ping_url, config_filename, monitoring_log_level, dry_run
|
ping_url, config_filename, monitoring_log_level, dry_run
|
||||||
|
@ -56,14 +48,30 @@ def ping_monitor(hook_config, config_filename, state, monitoring_log_level, dry_
|
||||||
'X-Tags': state_config.get('tags'),
|
'X-Tags': state_config.get('tags'),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
username = hook_config.get('username')
|
||||||
|
password = hook_config.get('password')
|
||||||
|
|
||||||
|
auth = None
|
||||||
|
if (username and password) is not None:
|
||||||
|
auth = requests.auth.HTTPBasicAuth(username, password)
|
||||||
|
logger.info(f'{config_filename}: Using basic auth with user {username} for ntfy')
|
||||||
|
elif username is not None:
|
||||||
|
logger.warning(
|
||||||
|
f'{config_filename}: Password missing for ntfy authentication, defaulting to no auth'
|
||||||
|
)
|
||||||
|
elif password is not None:
|
||||||
|
logger.warning(
|
||||||
|
f'{config_filename}: Username missing for ntfy authentication, defaulting to no auth'
|
||||||
|
)
|
||||||
|
|
||||||
if not dry_run:
|
if not dry_run:
|
||||||
logging.getLogger('urllib3').setLevel(logging.ERROR)
|
logging.getLogger('urllib3').setLevel(logging.ERROR)
|
||||||
try:
|
try:
|
||||||
response = requests.post(f'{base_url}/{topic}', headers=headers)
|
response = requests.post(f'{base_url}/{topic}', headers=headers, auth=auth)
|
||||||
if not response.ok:
|
if not response.ok:
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
except requests.exceptions.RequestException as error:
|
except requests.exceptions.RequestException as error:
|
||||||
logger.warning(f'{config_filename}: Ntfy error: {error}')
|
logger.warning(f'{config_filename}: ntfy error: {error}')
|
||||||
|
|
||||||
|
|
||||||
def destroy_monitor(
|
def destroy_monitor(
|
||||||
|
|
|
@ -1,6 +1,12 @@
|
||||||
|
import csv
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
|
|
||||||
from borgmatic.execute import execute_command, execute_command_with_processes
|
from borgmatic.execute import (
|
||||||
|
execute_command,
|
||||||
|
execute_command_and_capture_output,
|
||||||
|
execute_command_with_processes,
|
||||||
|
)
|
||||||
from borgmatic.hooks import dump
|
from borgmatic.hooks import dump
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -34,6 +40,44 @@ def make_extra_environment(database):
|
||||||
return extra
|
return extra
|
||||||
|
|
||||||
|
|
||||||
|
EXCLUDED_DATABASE_NAMES = ('template0', 'template1')
|
||||||
|
|
||||||
|
|
||||||
|
def database_names_to_dump(database, extra_environment, log_prefix, dry_run):
|
||||||
|
'''
|
||||||
|
Given a requested database config, return the corresponding sequence of database names to dump.
|
||||||
|
In the case of "all" when a database format is given, query for the names of databases on the
|
||||||
|
configured host and return them. For "all" without a database format, just return a sequence
|
||||||
|
containing "all".
|
||||||
|
'''
|
||||||
|
requested_name = database['name']
|
||||||
|
|
||||||
|
if requested_name != 'all':
|
||||||
|
return (requested_name,)
|
||||||
|
if not database.get('format'):
|
||||||
|
return ('all',)
|
||||||
|
if dry_run:
|
||||||
|
return ()
|
||||||
|
|
||||||
|
list_command = (
|
||||||
|
('psql', '--list', '--no-password', '--csv', '--tuples-only')
|
||||||
|
+ (('--host', database['hostname']) if 'hostname' in database else ())
|
||||||
|
+ (('--port', str(database['port'])) if 'port' in database else ())
|
||||||
|
+ (('--username', database['username']) if 'username' in database else ())
|
||||||
|
+ (tuple(database['list_options'].split(' ')) if 'list_options' in database else ())
|
||||||
|
)
|
||||||
|
logger.debug(f'{log_prefix}: Querying for "all" PostgreSQL databases to dump')
|
||||||
|
list_output = execute_command_and_capture_output(
|
||||||
|
list_command, extra_environment=extra_environment
|
||||||
|
)
|
||||||
|
|
||||||
|
return tuple(
|
||||||
|
row[0]
|
||||||
|
for row in csv.reader(list_output.splitlines(), delimiter=',', quotechar='"')
|
||||||
|
if row[0] not in EXCLUDED_DATABASE_NAMES
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def dump_databases(databases, log_prefix, location_config, dry_run):
|
def dump_databases(databases, log_prefix, location_config, dry_run):
|
||||||
'''
|
'''
|
||||||
Dump the given PostgreSQL databases to a named pipe. The databases are supplied as a sequence of
|
Dump the given PostgreSQL databases to a named pipe. The databases are supplied as a sequence of
|
||||||
|
@ -43,6 +87,8 @@ def dump_databases(databases, log_prefix, location_config, dry_run):
|
||||||
|
|
||||||
Return a sequence of subprocess.Popen instances for the dump processes ready to spew to a named
|
Return a sequence of subprocess.Popen instances for the dump processes ready to spew to a named
|
||||||
pipe. But if this is a dry run, then don't actually dump anything and return an empty sequence.
|
pipe. But if this is a dry run, then don't actually dump anything and return an empty sequence.
|
||||||
|
|
||||||
|
Raise ValueError if the databases to dump cannot be determined.
|
||||||
'''
|
'''
|
||||||
dry_run_label = ' (dry run; not actually dumping anything)' if dry_run else ''
|
dry_run_label = ' (dry run; not actually dumping anything)' if dry_run else ''
|
||||||
processes = []
|
processes = []
|
||||||
|
@ -50,51 +96,67 @@ def dump_databases(databases, log_prefix, location_config, dry_run):
|
||||||
logger.info('{}: Dumping PostgreSQL databases{}'.format(log_prefix, dry_run_label))
|
logger.info('{}: Dumping PostgreSQL databases{}'.format(log_prefix, dry_run_label))
|
||||||
|
|
||||||
for database in databases:
|
for database in databases:
|
||||||
name = database['name']
|
|
||||||
dump_filename = dump.make_database_dump_filename(
|
|
||||||
make_dump_path(location_config), name, database.get('hostname')
|
|
||||||
)
|
|
||||||
all_databases = bool(name == 'all')
|
|
||||||
dump_format = database.get('format', 'custom')
|
|
||||||
command = (
|
|
||||||
(
|
|
||||||
'pg_dumpall' if all_databases else 'pg_dump',
|
|
||||||
'--no-password',
|
|
||||||
'--clean',
|
|
||||||
'--if-exists',
|
|
||||||
)
|
|
||||||
+ (('--host', database['hostname']) if 'hostname' in database else ())
|
|
||||||
+ (('--port', str(database['port'])) if 'port' in database else ())
|
|
||||||
+ (('--username', database['username']) if 'username' in database else ())
|
|
||||||
+ (() if all_databases else ('--format', dump_format))
|
|
||||||
+ (('--file', dump_filename) if dump_format == 'directory' else ())
|
|
||||||
+ (tuple(database['options'].split(' ')) if 'options' in database else ())
|
|
||||||
+ (() if all_databases else (name,))
|
|
||||||
# Use shell redirection rather than the --file flag to sidestep synchronization issues
|
|
||||||
# when pg_dump/pg_dumpall tries to write to a named pipe. But for the directory dump
|
|
||||||
# format in a particular, a named destination is required, and redirection doesn't work.
|
|
||||||
+ (('>', dump_filename) if dump_format != 'directory' else ())
|
|
||||||
)
|
|
||||||
extra_environment = make_extra_environment(database)
|
extra_environment = make_extra_environment(database)
|
||||||
|
dump_path = make_dump_path(location_config)
|
||||||
logger.debug(
|
dump_database_names = database_names_to_dump(
|
||||||
'{}: Dumping PostgreSQL database {} to {}{}'.format(
|
database, extra_environment, log_prefix, dry_run
|
||||||
log_prefix, name, dump_filename, dry_run_label
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
if dry_run:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if dump_format == 'directory':
|
if not dump_database_names:
|
||||||
dump.create_parent_directory_for_dump(dump_filename)
|
if dry_run:
|
||||||
else:
|
continue
|
||||||
dump.create_named_pipe_for_dump(dump_filename)
|
|
||||||
|
|
||||||
processes.append(
|
raise ValueError('Cannot find any PostgreSQL databases to dump.')
|
||||||
execute_command(
|
|
||||||
command, shell=True, extra_environment=extra_environment, run_to_completion=False
|
for database_name in dump_database_names:
|
||||||
|
dump_format = database.get('format', None if database_name == 'all' else 'custom')
|
||||||
|
default_dump_command = 'pg_dumpall' if database_name == 'all' else 'pg_dump'
|
||||||
|
dump_command = database.get('pg_dump_command') or default_dump_command
|
||||||
|
dump_filename = dump.make_database_dump_filename(
|
||||||
|
dump_path, database_name, database.get('hostname')
|
||||||
)
|
)
|
||||||
)
|
if os.path.exists(dump_filename):
|
||||||
|
logger.warning(
|
||||||
|
f'{log_prefix}: Skipping duplicate dump of PostgreSQL database "{database_name}" to {dump_filename}'
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
command = (
|
||||||
|
(dump_command, '--no-password', '--clean', '--if-exists',)
|
||||||
|
+ (('--host', database['hostname']) if 'hostname' in database else ())
|
||||||
|
+ (('--port', str(database['port'])) if 'port' in database else ())
|
||||||
|
+ (('--username', database['username']) if 'username' in database else ())
|
||||||
|
+ (('--format', dump_format) if dump_format else ())
|
||||||
|
+ (('--file', dump_filename) if dump_format == 'directory' else ())
|
||||||
|
+ (tuple(database['options'].split(' ')) if 'options' in database else ())
|
||||||
|
+ (() if database_name == 'all' else (database_name,))
|
||||||
|
# Use shell redirection rather than the --file flag to sidestep synchronization issues
|
||||||
|
# when pg_dump/pg_dumpall tries to write to a named pipe. But for the directory dump
|
||||||
|
# format in a particular, a named destination is required, and redirection doesn't work.
|
||||||
|
+ (('>', dump_filename) if dump_format != 'directory' else ())
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
f'{log_prefix}: Dumping PostgreSQL database "{database_name}" to {dump_filename}{dry_run_label}'
|
||||||
|
)
|
||||||
|
if dry_run:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if dump_format == 'directory':
|
||||||
|
dump.create_parent_directory_for_dump(dump_filename)
|
||||||
|
execute_command(
|
||||||
|
command, shell=True, extra_environment=extra_environment,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
dump.create_named_pipe_for_dump(dump_filename)
|
||||||
|
processes.append(
|
||||||
|
execute_command(
|
||||||
|
command,
|
||||||
|
shell=True,
|
||||||
|
extra_environment=extra_environment,
|
||||||
|
run_to_completion=False,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
return processes
|
return processes
|
||||||
|
|
||||||
|
@ -140,16 +202,19 @@ def restore_database_dump(database_config, log_prefix, location_config, dry_run,
|
||||||
dump_filename = dump.make_database_dump_filename(
|
dump_filename = dump.make_database_dump_filename(
|
||||||
make_dump_path(location_config), database['name'], database.get('hostname')
|
make_dump_path(location_config), database['name'], database.get('hostname')
|
||||||
)
|
)
|
||||||
|
psql_command = database.get('psql_command') or 'psql'
|
||||||
analyze_command = (
|
analyze_command = (
|
||||||
('psql', '--no-password', '--quiet')
|
(psql_command, '--no-password', '--quiet')
|
||||||
+ (('--host', database['hostname']) if 'hostname' in database else ())
|
+ (('--host', database['hostname']) if 'hostname' in database else ())
|
||||||
+ (('--port', str(database['port'])) if 'port' in database else ())
|
+ (('--port', str(database['port'])) if 'port' in database else ())
|
||||||
+ (('--username', database['username']) if 'username' in database else ())
|
+ (('--username', database['username']) if 'username' in database else ())
|
||||||
+ (('--dbname', database['name']) if not all_databases else ())
|
+ (('--dbname', database['name']) if not all_databases else ())
|
||||||
|
+ (tuple(database['analyze_options'].split(' ')) if 'analyze_options' in database else ())
|
||||||
+ ('--command', 'ANALYZE')
|
+ ('--command', 'ANALYZE')
|
||||||
)
|
)
|
||||||
|
pg_restore_command = database.get('pg_restore_command') or 'pg_restore'
|
||||||
restore_command = (
|
restore_command = (
|
||||||
('psql' if all_databases else 'pg_restore', '--no-password')
|
(psql_command if all_databases else pg_restore_command, '--no-password')
|
||||||
+ (
|
+ (
|
||||||
('--if-exists', '--exit-on-error', '--clean', '--dbname', database['name'])
|
('--if-exists', '--exit-on-error', '--clean', '--dbname', database['name'])
|
||||||
if not all_databases
|
if not all_databases
|
||||||
|
@ -158,6 +223,7 @@ def restore_database_dump(database_config, log_prefix, location_config, dry_run,
|
||||||
+ (('--host', database['hostname']) if 'hostname' in database else ())
|
+ (('--host', database['hostname']) if 'hostname' in database else ())
|
||||||
+ (('--port', str(database['port'])) if 'port' in database else ())
|
+ (('--port', str(database['port'])) if 'port' in database else ())
|
||||||
+ (('--username', database['username']) if 'username' in database else ())
|
+ (('--username', database['username']) if 'username' in database else ())
|
||||||
|
+ (tuple(database['restore_options'].split(' ')) if 'restore_options' in database else ())
|
||||||
+ (() if extract_process else (dump_filename,))
|
+ (() if extract_process else (dump_filename,))
|
||||||
)
|
)
|
||||||
extra_environment = make_extra_environment(database)
|
extra_environment = make_extra_environment(database)
|
||||||
|
|
|
@ -0,0 +1,125 @@
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
|
||||||
|
from borgmatic.execute import execute_command, execute_command_with_processes
|
||||||
|
from borgmatic.hooks import dump
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def make_dump_path(location_config): # pragma: no cover
|
||||||
|
'''
|
||||||
|
Make the dump path from the given location configuration and the name of this hook.
|
||||||
|
'''
|
||||||
|
return dump.make_database_dump_path(
|
||||||
|
location_config.get('borgmatic_source_directory'), 'sqlite_databases'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def dump_databases(databases, log_prefix, location_config, dry_run):
|
||||||
|
'''
|
||||||
|
Dump the given SQLite3 databases to a file. The databases are supplied as a sequence of
|
||||||
|
configuration dicts, as per the configuration schema. Use the given log prefix in any log
|
||||||
|
entries. Use the given location configuration dict to construct the destination path. If this
|
||||||
|
is a dry run, then don't actually dump anything.
|
||||||
|
'''
|
||||||
|
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))
|
||||||
|
|
||||||
|
for database in databases:
|
||||||
|
database_path = database['path']
|
||||||
|
|
||||||
|
if database['name'] == 'all':
|
||||||
|
logger.warning('The "all" database name has no meaning for SQLite3 databases')
|
||||||
|
if not os.path.exists(database_path):
|
||||||
|
logger.warning(
|
||||||
|
f'{log_prefix}: No SQLite database at {database_path}; An empty database will be created and dumped'
|
||||||
|
)
|
||||||
|
|
||||||
|
dump_path = make_dump_path(location_config)
|
||||||
|
dump_filename = dump.make_database_dump_filename(dump_path, database['name'])
|
||||||
|
if os.path.exists(dump_filename):
|
||||||
|
logger.warning(
|
||||||
|
f'{log_prefix}: Skipping duplicate dump of SQLite database at {database_path} to {dump_filename}'
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
command = (
|
||||||
|
'sqlite3',
|
||||||
|
database_path,
|
||||||
|
'.dump',
|
||||||
|
'>',
|
||||||
|
dump_filename,
|
||||||
|
)
|
||||||
|
logger.debug(
|
||||||
|
f'{log_prefix}: Dumping SQLite database at {database_path} to {dump_filename}{dry_run_label}'
|
||||||
|
)
|
||||||
|
if dry_run:
|
||||||
|
continue
|
||||||
|
|
||||||
|
dump.create_parent_directory_for_dump(dump_filename)
|
||||||
|
processes.append(execute_command(command, shell=True, run_to_completion=False))
|
||||||
|
|
||||||
|
return processes
|
||||||
|
|
||||||
|
|
||||||
|
def remove_database_dumps(databases, log_prefix, location_config, dry_run): # pragma: no cover
|
||||||
|
'''
|
||||||
|
Remove the given SQLite3 database dumps from the filesystem. The databases are supplied as a
|
||||||
|
sequence of configuration dicts, as per the configuration schema. Use the given log prefix in
|
||||||
|
any log entries. Use the given location configuration dict to construct the destination path.
|
||||||
|
If this is a dry run, then don't actually remove anything.
|
||||||
|
'''
|
||||||
|
dump.remove_database_dumps(make_dump_path(location_config), 'SQLite', log_prefix, dry_run)
|
||||||
|
|
||||||
|
|
||||||
|
def make_database_dump_pattern(
|
||||||
|
databases, log_prefix, location_config, name=None
|
||||||
|
): # pragma: no cover
|
||||||
|
'''
|
||||||
|
Make a pattern that matches the given SQLite3 databases. The databases are supplied as a
|
||||||
|
sequence of configuration dicts, as per the configuration schema.
|
||||||
|
'''
|
||||||
|
return dump.make_database_dump_filename(make_dump_path(location_config), name)
|
||||||
|
|
||||||
|
|
||||||
|
def restore_database_dump(database_config, log_prefix, location_config, dry_run, extract_process):
|
||||||
|
'''
|
||||||
|
Restore the given SQLite3 database from an extract stream. The database is supplied as a
|
||||||
|
one-element sequence containing a dict describing the database, as per the configuration schema.
|
||||||
|
Use the given log prefix in any log entries. If this is a dry run, then don't actually restore
|
||||||
|
anything. Trigger the given active extract process (an instance of subprocess.Popen) to produce
|
||||||
|
output to consume.
|
||||||
|
'''
|
||||||
|
dry_run_label = ' (dry run; not actually restoring anything)' if dry_run else ''
|
||||||
|
|
||||||
|
if len(database_config) != 1:
|
||||||
|
raise ValueError('The database configuration value is invalid')
|
||||||
|
|
||||||
|
database_path = database_config[0]['path']
|
||||||
|
|
||||||
|
logger.debug(f'{log_prefix}: Restoring SQLite database at {database_path}{dry_run_label}')
|
||||||
|
if dry_run:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
os.remove(database_path)
|
||||||
|
logger.warning(f'{log_prefix}: Removed existing SQLite database at {database_path}')
|
||||||
|
except FileNotFoundError: # pragma: no cover
|
||||||
|
pass
|
||||||
|
|
||||||
|
restore_command = (
|
||||||
|
'sqlite3',
|
||||||
|
database_path,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Don't give Borg local path so as to error on warnings, as "borg extract" only gives a warning
|
||||||
|
# if the restore paths don't exist in the archive.
|
||||||
|
execute_command_with_processes(
|
||||||
|
restore_command,
|
||||||
|
[extract_process],
|
||||||
|
output_log_level=logging.DEBUG,
|
||||||
|
input_file=extract_process.stdout,
|
||||||
|
)
|
|
@ -85,18 +85,19 @@ class Multi_stream_handler(logging.Handler):
|
||||||
handler.setLevel(level)
|
handler.setLevel(level)
|
||||||
|
|
||||||
|
|
||||||
LOG_LEVEL_TO_COLOR = {
|
|
||||||
logging.CRITICAL: colorama.Fore.RED,
|
|
||||||
logging.ERROR: colorama.Fore.RED,
|
|
||||||
logging.WARN: colorama.Fore.YELLOW,
|
|
||||||
logging.INFO: colorama.Fore.GREEN,
|
|
||||||
logging.DEBUG: colorama.Fore.CYAN,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class Console_color_formatter(logging.Formatter):
|
class Console_color_formatter(logging.Formatter):
|
||||||
def format(self, record):
|
def format(self, record):
|
||||||
color = LOG_LEVEL_TO_COLOR.get(record.levelno)
|
add_custom_log_levels()
|
||||||
|
|
||||||
|
color = {
|
||||||
|
logging.CRITICAL: colorama.Fore.RED,
|
||||||
|
logging.ERROR: colorama.Fore.RED,
|
||||||
|
logging.WARN: colorama.Fore.YELLOW,
|
||||||
|
logging.ANSWER: colorama.Fore.MAGENTA,
|
||||||
|
logging.INFO: colorama.Fore.GREEN,
|
||||||
|
logging.DEBUG: colorama.Fore.CYAN,
|
||||||
|
}.get(record.levelno)
|
||||||
|
|
||||||
return color_text(color, record.msg)
|
return color_text(color, record.msg)
|
||||||
|
|
||||||
|
|
||||||
|
@ -110,6 +111,45 @@ def color_text(color, message):
|
||||||
return '{}{}{}'.format(color, message, colorama.Style.RESET_ALL)
|
return '{}{}{}'.format(color, message, colorama.Style.RESET_ALL)
|
||||||
|
|
||||||
|
|
||||||
|
def add_logging_level(level_name, level_number):
|
||||||
|
'''
|
||||||
|
Globally add a custom logging level based on the given (all uppercase) level name and number.
|
||||||
|
Do this idempotently.
|
||||||
|
|
||||||
|
Inspired by https://stackoverflow.com/questions/2183233/how-to-add-a-custom-loglevel-to-pythons-logging-facility/35804945#35804945
|
||||||
|
'''
|
||||||
|
method_name = level_name.lower()
|
||||||
|
|
||||||
|
if not hasattr(logging, level_name):
|
||||||
|
logging.addLevelName(level_number, level_name)
|
||||||
|
setattr(logging, level_name, level_number)
|
||||||
|
|
||||||
|
if not hasattr(logging, method_name):
|
||||||
|
|
||||||
|
def log_for_level(self, message, *args, **kwargs): # pragma: no cover
|
||||||
|
if self.isEnabledFor(level_number):
|
||||||
|
self._log(level_number, message, args, **kwargs)
|
||||||
|
|
||||||
|
setattr(logging.getLoggerClass(), method_name, log_for_level)
|
||||||
|
|
||||||
|
if not hasattr(logging.getLoggerClass(), method_name):
|
||||||
|
|
||||||
|
def log_to_root(message, *args, **kwargs): # pragma: no cover
|
||||||
|
logging.log(level_number, message, *args, **kwargs)
|
||||||
|
|
||||||
|
setattr(logging, method_name, log_to_root)
|
||||||
|
|
||||||
|
|
||||||
|
ANSWER = logging.WARN - 5
|
||||||
|
|
||||||
|
|
||||||
|
def add_custom_log_levels(): # pragma: no cover
|
||||||
|
'''
|
||||||
|
Add a custom log level between WARN and INFO for user-requested answers.
|
||||||
|
'''
|
||||||
|
add_logging_level('ANSWER', ANSWER)
|
||||||
|
|
||||||
|
|
||||||
def configure_logging(
|
def configure_logging(
|
||||||
console_log_level,
|
console_log_level,
|
||||||
syslog_log_level=None,
|
syslog_log_level=None,
|
||||||
|
@ -130,6 +170,8 @@ def configure_logging(
|
||||||
if monitoring_log_level is None:
|
if monitoring_log_level is None:
|
||||||
monitoring_log_level = console_log_level
|
monitoring_log_level = console_log_level
|
||||||
|
|
||||||
|
add_custom_log_levels()
|
||||||
|
|
||||||
# Log certain log levels to console stderr and others to stdout. This supports use cases like
|
# Log certain log levels to console stderr and others to stdout. This supports use cases like
|
||||||
# grepping (non-error) output.
|
# grepping (non-error) output.
|
||||||
console_error_handler = logging.StreamHandler(sys.stderr)
|
console_error_handler = logging.StreamHandler(sys.stderr)
|
||||||
|
@ -138,7 +180,8 @@ def configure_logging(
|
||||||
{
|
{
|
||||||
logging.CRITICAL: console_error_handler,
|
logging.CRITICAL: console_error_handler,
|
||||||
logging.ERROR: console_error_handler,
|
logging.ERROR: console_error_handler,
|
||||||
logging.WARN: console_standard_handler,
|
logging.WARN: console_error_handler,
|
||||||
|
logging.ANSWER: console_standard_handler,
|
||||||
logging.INFO: console_standard_handler,
|
logging.INFO: console_standard_handler,
|
||||||
logging.DEBUG: console_standard_handler,
|
logging.DEBUG: console_standard_handler,
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
import borgmatic.logger
|
||||||
|
|
||||||
VERBOSITY_ERROR = -1
|
VERBOSITY_ERROR = -1
|
||||||
VERBOSITY_WARNING = 0
|
VERBOSITY_ANSWER = 0
|
||||||
VERBOSITY_SOME = 1
|
VERBOSITY_SOME = 1
|
||||||
VERBOSITY_LOTS = 2
|
VERBOSITY_LOTS = 2
|
||||||
|
|
||||||
|
@ -10,9 +12,11 @@ def verbosity_to_log_level(verbosity):
|
||||||
'''
|
'''
|
||||||
Given a borgmatic verbosity value, return the corresponding Python log level.
|
Given a borgmatic verbosity value, return the corresponding Python log level.
|
||||||
'''
|
'''
|
||||||
|
borgmatic.logger.add_custom_log_levels()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
VERBOSITY_ERROR: logging.ERROR,
|
VERBOSITY_ERROR: logging.ERROR,
|
||||||
VERBOSITY_WARNING: logging.WARNING,
|
VERBOSITY_ANSWER: logging.ANSWER,
|
||||||
VERBOSITY_SOME: logging.INFO,
|
VERBOSITY_SOME: logging.INFO,
|
||||||
VERBOSITY_LOTS: logging.DEBUG,
|
VERBOSITY_LOTS: logging.DEBUG,
|
||||||
}.get(verbosity, logging.WARNING)
|
}.get(verbosity, logging.WARNING)
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
FROM alpine:3.16.0 as borgmatic
|
FROM alpine:3.17.1 as borgmatic
|
||||||
|
|
||||||
COPY . /app
|
COPY . /app
|
||||||
RUN apk add --no-cache py3-pip py3-ruamel.yaml py3-ruamel.yaml.clib
|
RUN apk add --no-cache py3-pip py3-ruamel.yaml py3-ruamel.yaml.clib
|
||||||
RUN pip install --no-cache /app && generate-borgmatic-config && chmod +r /etc/borgmatic/config.yaml
|
RUN pip install --no-cache /app && generate-borgmatic-config && chmod +r /etc/borgmatic/config.yaml
|
||||||
RUN borgmatic --help > /command-line.txt \
|
RUN borgmatic --help > /command-line.txt \
|
||||||
&& for action in rcreate transfer prune compact create check extract export-tar mount umount restore rlist list rinfo info break-lock borg; do \
|
&& for action in rcreate transfer create prune compact check extract export-tar mount umount restore rlist list rinfo info break-lock borg; do \
|
||||||
echo -e "\n--------------------------------------------------------------------------------\n" >> /command-line.txt \
|
echo -e "\n--------------------------------------------------------------------------------\n" >> /command-line.txt \
|
||||||
&& borgmatic "$action" --help >> /command-line.txt; done
|
&& borgmatic "$action" --help >> /command-line.txt; done
|
||||||
|
|
||||||
FROM node:18.4.0-alpine as html
|
FROM node:19.5.0-alpine as html
|
||||||
|
|
||||||
ARG ENVIRONMENT=production
|
ARG ENVIRONMENT=production
|
||||||
|
|
||||||
|
@ -27,7 +27,7 @@ COPY . /source
|
||||||
RUN NODE_ENV=${ENVIRONMENT} npx eleventy --input=/source/docs --output=/output/docs \
|
RUN NODE_ENV=${ENVIRONMENT} npx eleventy --input=/source/docs --output=/output/docs \
|
||||||
&& mv /output/docs/index.html /output/index.html
|
&& mv /output/docs/index.html /output/index.html
|
||||||
|
|
||||||
FROM nginx:1.22.0-alpine
|
FROM nginx:1.22.1-alpine
|
||||||
|
|
||||||
COPY --from=html /output /usr/share/nginx/html
|
COPY --from=html /output /usr/share/nginx/html
|
||||||
COPY --from=borgmatic /etc/borgmatic/config.yaml /usr/share/nginx/html/docs/reference/config.yaml
|
COPY --from=borgmatic /etc/borgmatic/config.yaml /usr/share/nginx/html/docs/reference/config.yaml
|
||||||
|
|
|
@ -63,11 +63,6 @@
|
||||||
top: -2px;
|
top: -2px;
|
||||||
bottom: 2px;
|
bottom: 2px;
|
||||||
}
|
}
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
.inlinelist .inlinelist-item code:before {
|
|
||||||
border-left-color: rgba(0,0,0,.8);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
a.buzzword {
|
a.buzzword {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
|
@ -91,26 +86,9 @@ a.buzzword {
|
||||||
.buzzword {
|
.buzzword {
|
||||||
background-color: #f7f7f7;
|
background-color: #f7f7f7;
|
||||||
}
|
}
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
.buzzword-list li,
|
|
||||||
.buzzword {
|
|
||||||
background-color: #080808;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.inlinelist .inlinelist-item {
|
.inlinelist .inlinelist-item {
|
||||||
background-color: #e9e9e9;
|
background-color: #e9e9e9;
|
||||||
}
|
}
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
.inlinelist .inlinelist-item {
|
|
||||||
background-color: #000;
|
|
||||||
}
|
|
||||||
.inlinelist .inlinelist-item a {
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
.inlinelist .inlinelist-item code {
|
|
||||||
color: inherit;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.inlinelist .inlinelist-item:hover,
|
.inlinelist .inlinelist-item:hover,
|
||||||
.inlinelist .inlinelist-item:focus,
|
.inlinelist .inlinelist-item:focus,
|
||||||
.buzzword-list li:hover,
|
.buzzword-list li:hover,
|
||||||
|
@ -217,12 +195,6 @@ main p a.buzzword {
|
||||||
height: 1.75em;
|
height: 1.75em;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
.numberflag {
|
|
||||||
background-color: #00bcd4;
|
|
||||||
color: #222;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
h1 .numberflag,
|
h1 .numberflag,
|
||||||
h2 .numberflag,
|
h2 .numberflag,
|
||||||
h3 .numberflag,
|
h3 .numberflag,
|
||||||
|
@ -244,11 +216,6 @@ h2 .numberflag:after {
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
width: calc(100% + 0.4em); /* 16px /40 */
|
width: calc(100% + 0.4em); /* 16px /40 */
|
||||||
}
|
}
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
h2 .numberflag:after {
|
|
||||||
background-color: #222;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Super featured list on home page */
|
/* Super featured list on home page */
|
||||||
.list-superfeatured .avatar {
|
.list-superfeatured .avatar {
|
||||||
|
|
|
@ -12,16 +12,6 @@
|
||||||
line-height: 1.285714285714; /* 18px /14 */
|
line-height: 1.285714285714; /* 18px /14 */
|
||||||
font-family: system-ui, -apple-system, sans-serif;
|
font-family: system-ui, -apple-system, sans-serif;
|
||||||
}
|
}
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
.minilink {
|
|
||||||
background-color: #222;
|
|
||||||
/*
|
|
||||||
!important to override .elv-callout a
|
|
||||||
see _includes/components/callout.css
|
|
||||||
*/
|
|
||||||
color: #fff !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
table .minilink {
|
table .minilink {
|
||||||
margin-top: 6px;
|
margin-top: 6px;
|
||||||
}
|
}
|
||||||
|
@ -32,12 +22,6 @@ table .minilink {
|
||||||
.minilink[href]:focus {
|
.minilink[href]:focus {
|
||||||
background-color: #bbb;
|
background-color: #bbb;
|
||||||
}
|
}
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
.minilink[href]:hover,
|
|
||||||
.minilink[href]:focus {
|
|
||||||
background-color: #444;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pre + .minilink {
|
pre + .minilink {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
border-radius: 0 0 0.2857142857143em 0.2857142857143em; /* 4px /14 */
|
border-radius: 0 0 0.2857142857143em 0.2857142857143em; /* 4px /14 */
|
||||||
|
@ -74,11 +58,6 @@ h4 .minilink {
|
||||||
text-transform: none;
|
text-transform: none;
|
||||||
box-shadow: 0 0 0 1px rgba(0,0,0,0.3);
|
box-shadow: 0 0 0 1px rgba(0,0,0,0.3);
|
||||||
}
|
}
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
.minilink-addedin {
|
|
||||||
box-shadow: 0 0 0 1px rgba(255,255,255,0.3);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.minilink-addedin:not(:first-child) {
|
.minilink-addedin:not(:first-child) {
|
||||||
margin-left: .5em;
|
margin-left: .5em;
|
||||||
}
|
}
|
||||||
|
|
|
@ -79,22 +79,11 @@
|
||||||
border-bottom: 1px solid #ddd;
|
border-bottom: 1px solid #ddd;
|
||||||
margin-bottom: 0.25em; /* 4px /16 */
|
margin-bottom: 0.25em; /* 4px /16 */
|
||||||
}
|
}
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
.elv-toc-list > li > a {
|
|
||||||
color: #fff;
|
|
||||||
border-color: #444;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Active links */
|
/* Active links */
|
||||||
.elv-toc-list li.elv-toc-active > a {
|
.elv-toc-list li.elv-toc-active > a {
|
||||||
background-color: #dff7ff;
|
background-color: #dff7ff;
|
||||||
}
|
}
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
.elv-toc-list li.elv-toc-active > a {
|
|
||||||
background-color: #353535;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.elv-toc-list ul .elv-toc-active > a:after {
|
.elv-toc-list ul .elv-toc-active > a:after {
|
||||||
content: "";
|
content: "";
|
||||||
}
|
}
|
||||||
|
|
|
@ -285,11 +285,6 @@ footer.elv-layout {
|
||||||
.elv-hero {
|
.elv-hero {
|
||||||
background-color: #222;
|
background-color: #222;
|
||||||
}
|
}
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
.elv-hero {
|
|
||||||
background-color: #292929;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.elv-hero img,
|
.elv-hero img,
|
||||||
.elv-hero svg {
|
.elv-hero svg {
|
||||||
width: 42.95774646vh;
|
width: 42.95774646vh;
|
||||||
|
|
|
@ -68,6 +68,9 @@ borgmatic. borgmatic logs the soft failure, skips all further actions in that
|
||||||
configurable file, and proceeds onward to any other borgmatic configuration
|
configurable file, and proceeds onward to any other borgmatic configuration
|
||||||
files you may have.
|
files you may have.
|
||||||
|
|
||||||
|
Note that `before_backup` only runs on the `create` action. See below about
|
||||||
|
optionally using `before_actions` instead.
|
||||||
|
|
||||||
You can imagine a similar check for the sometimes-online server case:
|
You can imagine a similar check for the sometimes-online server case:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
|
@ -93,6 +96,12 @@ hooks:
|
||||||
|
|
||||||
(Writing the battery script is left as an exercise to the reader.)
|
(Writing the battery script is left as an exercise to the reader.)
|
||||||
|
|
||||||
|
<span class="minilink minilink-addedin">New in version 1.7.0</span> The
|
||||||
|
`before_actions` and `after_actions` hooks run before/after all the actions
|
||||||
|
(like `create`, `prune`, etc.) for each repository. So if you'd like your soft
|
||||||
|
failure command hook to run regardless of action, consider using
|
||||||
|
`before_actions` instead of `before_backup`.
|
||||||
|
|
||||||
|
|
||||||
## Caveats and details
|
## Caveats and details
|
||||||
|
|
||||||
|
|
|
@ -15,8 +15,7 @@ consistent snapshot that is more suited for backups.
|
||||||
|
|
||||||
Fortunately, borgmatic includes built-in support for creating database dumps
|
Fortunately, borgmatic includes built-in support for creating database dumps
|
||||||
prior to running backups. For example, here is everything you need to dump and
|
prior to running backups. For example, here is everything you need to dump and
|
||||||
backup a couple of local PostgreSQL databases, a MySQL/MariaDB database, and a
|
backup a couple of local PostgreSQL databases and a MySQL/MariaDB database.
|
||||||
MongoDB database:
|
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
hooks:
|
hooks:
|
||||||
|
@ -25,10 +24,27 @@ hooks:
|
||||||
- name: orders
|
- name: orders
|
||||||
mysql_databases:
|
mysql_databases:
|
||||||
- name: posts
|
- name: posts
|
||||||
|
```
|
||||||
|
|
||||||
|
<span class="minilink minilink-addedin">New in version 1.5.22</span> You can
|
||||||
|
also dump MongoDB databases. For example:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
hooks:
|
||||||
mongodb_databases:
|
mongodb_databases:
|
||||||
- name: messages
|
- name: messages
|
||||||
```
|
```
|
||||||
|
|
||||||
|
<span class="minilink minilink-addedin">New in version 1.7.9</span>
|
||||||
|
Additionally, you can dump SQLite databases. For example:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
hooks:
|
||||||
|
sqlite_databases:
|
||||||
|
- name: mydb
|
||||||
|
path: /var/lib/sqlite3/mydb.sqlite
|
||||||
|
```
|
||||||
|
|
||||||
As part of each backup, borgmatic streams a database dump for each configured
|
As part of each backup, borgmatic streams a database dump for each configured
|
||||||
database directly to Borg, so it's included in the backup without consuming
|
database directly to Borg, so it's included in the backup without consuming
|
||||||
additional disk space. (The exceptions are the PostgreSQL/MongoDB "directory"
|
additional disk space. (The exceptions are the PostgreSQL/MongoDB "directory"
|
||||||
|
@ -74,8 +90,19 @@ hooks:
|
||||||
password: trustsome1
|
password: trustsome1
|
||||||
authentication_database: mongousers
|
authentication_database: mongousers
|
||||||
options: "--ssl"
|
options: "--ssl"
|
||||||
|
sqlite_databases:
|
||||||
|
- name: mydb
|
||||||
|
path: /var/lib/sqlite3/mydb.sqlite
|
||||||
```
|
```
|
||||||
|
|
||||||
|
See your [borgmatic configuration
|
||||||
|
file](https://torsion.org/borgmatic/docs/reference/configuration/) for
|
||||||
|
additional customization of the options passed to database commands (when
|
||||||
|
listing databases, restoring databases, etc.).
|
||||||
|
|
||||||
|
|
||||||
|
### All databases
|
||||||
|
|
||||||
If you want to dump all databases on a host, use `all` for the database name:
|
If you want to dump all databases on a host, use `all` for the database name:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
|
@ -91,9 +118,32 @@ hooks:
|
||||||
Note that you may need to use a `username` of the `postgres` superuser for
|
Note that you may need to use a `username` of the `postgres` superuser for
|
||||||
this to work with PostgreSQL.
|
this to work with PostgreSQL.
|
||||||
|
|
||||||
If you would like to backup databases only and not source directories, you can
|
The SQLite hook in particular does not consider "all" a special database name.
|
||||||
specify an empty `source_directories` value (as it is a mandatory field prior
|
|
||||||
to borgmatic 1.7.1):
|
<span class="minilink minilink-addedin">New in version 1.7.6</span> With
|
||||||
|
PostgreSQL and MySQL, you can optionally dump "all" databases to separate
|
||||||
|
files instead of one combined dump file, allowing more convenient restores of
|
||||||
|
individual databases. Enable this by specifying your desired database dump
|
||||||
|
`format`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
hooks:
|
||||||
|
postgresql_databases:
|
||||||
|
- name: all
|
||||||
|
format: custom
|
||||||
|
mysql_databases:
|
||||||
|
- name: all
|
||||||
|
format: sql
|
||||||
|
```
|
||||||
|
|
||||||
|
### No source directories
|
||||||
|
|
||||||
|
<span class="minilink minilink-addedin">New in version 1.7.1</span> If you
|
||||||
|
would like to backup databases only and not source directories, you can omit
|
||||||
|
`source_directories` entirely.
|
||||||
|
|
||||||
|
In older versions of borgmatic, instead specify an empty `source_directories`
|
||||||
|
value, as it is a mandatory option prior to version 1.7.1:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
location:
|
location:
|
||||||
|
@ -103,8 +153,7 @@ hooks:
|
||||||
- name: all
|
- name: all
|
||||||
```
|
```
|
||||||
|
|
||||||
<span class="minilink minilink-addedin">New in version 1.7.1</span> You can
|
|
||||||
omit `source_directories` entirely.
|
|
||||||
|
|
||||||
### External passwords
|
### External passwords
|
||||||
|
|
||||||
|
@ -126,11 +175,11 @@ bring back any missing configuration files in order to restore a database.
|
||||||
|
|
||||||
## Supported databases
|
## Supported databases
|
||||||
|
|
||||||
As of now, borgmatic supports PostgreSQL, MySQL/MariaDB, and MongoDB databases
|
As of now, borgmatic supports PostgreSQL, MySQL/MariaDB, MongoDB, and SQLite
|
||||||
directly. But see below about general-purpose preparation and cleanup hooks as
|
databases directly. But see below about general-purpose preparation and
|
||||||
a work-around with other database systems. Also, please [file a
|
cleanup hooks as a work-around with other database systems. Also, please [file
|
||||||
ticket](https://torsion.org/borgmatic/#issues) for additional database systems
|
a ticket](https://torsion.org/borgmatic/#issues) for additional database
|
||||||
that you'd like supported.
|
systems that you'd like supported.
|
||||||
|
|
||||||
|
|
||||||
## Database restoration
|
## Database restoration
|
||||||
|
@ -148,15 +197,15 @@ borgmatic rlist
|
||||||
That should yield output looking something like:
|
That should yield output looking something like:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
host-2019-01-01T04:05:06.070809 Tue, 2019-01-01 04:05:06 [...]
|
host-2023-01-01T04:05:06.070809 Tue, 2023-01-01 04:05:06 [...]
|
||||||
host-2019-01-02T04:06:07.080910 Wed, 2019-01-02 04:06:07 [...]
|
host-2023-01-02T04:06:07.080910 Wed, 2023-01-02 04:06:07 [...]
|
||||||
```
|
```
|
||||||
|
|
||||||
Assuming that you want to restore all database dumps from the archive with the
|
Assuming that you want to restore all database dumps from the archive with the
|
||||||
most up-to-date files and therefore the latest timestamp, run a command like:
|
most up-to-date files and therefore the latest timestamp, run a command like:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
borgmatic restore --archive host-2019-01-02T04:06:07.080910
|
borgmatic restore --archive host-2023-01-02T04:06:07.080910
|
||||||
```
|
```
|
||||||
|
|
||||||
(No borgmatic `restore` action? Upgrade borgmatic!)
|
(No borgmatic `restore` action? Upgrade borgmatic!)
|
||||||
|
@ -185,7 +234,7 @@ 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 path containing the archive to restore. Here's an example:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
borgmatic restore --repository repo.borg --archive host-2019-...
|
borgmatic restore --repository repo.borg --archive host-2023-...
|
||||||
```
|
```
|
||||||
|
|
||||||
### Restore particular databases
|
### Restore particular databases
|
||||||
|
@ -195,9 +244,39 @@ restore one of them, use the `--database` flag to select one or more
|
||||||
databases. For instance:
|
databases. For instance:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
borgmatic restore --archive host-2019-... --database users
|
borgmatic restore --archive host-2023-... --database users
|
||||||
```
|
```
|
||||||
|
|
||||||
|
<span class="minilink minilink-addedin">New in version 1.7.6</span> You can
|
||||||
|
also restore individual databases even if you dumped them as "all"—as long as
|
||||||
|
you dumped them into separate files via use of the "format" option. See above
|
||||||
|
for more information.
|
||||||
|
|
||||||
|
|
||||||
|
### Restore all databases
|
||||||
|
|
||||||
|
To restore all databases:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
borgmatic restore --archive host-2023-... --database all
|
||||||
|
```
|
||||||
|
|
||||||
|
Or omit the `--database` flag entirely:
|
||||||
|
|
||||||
|
|
||||||
|
```bash
|
||||||
|
borgmatic restore --archive host-2023-...
|
||||||
|
```
|
||||||
|
|
||||||
|
Prior to borgmatic version 1.7.6, this restores a combined "all" database
|
||||||
|
dump from the archive.
|
||||||
|
|
||||||
|
<span class="minilink minilink-addedin">New in version 1.7.6</span> Restoring
|
||||||
|
"all" databases restores each database found in the selected archive. That
|
||||||
|
includes any combined dump file named "all" and any other individual database
|
||||||
|
dumps found in the archive.
|
||||||
|
|
||||||
|
|
||||||
### Limitations
|
### Limitations
|
||||||
|
|
||||||
There are a few important limitations with borgmatic's current database
|
There are a few important limitations with borgmatic's current database
|
||||||
|
@ -220,8 +299,8 @@ such files. Common directories to exclude are `/dev` and `/run`, but that may
|
||||||
not be exhaustive. <span class="minilink minilink-addedin">New in version
|
not be exhaustive. <span class="minilink minilink-addedin">New in version
|
||||||
1.7.3</span> When database hooks are enabled, borgmatic automatically excludes
|
1.7.3</span> When database hooks are enabled, borgmatic automatically excludes
|
||||||
special files that may cause Borg to hang, so you no longer need to manually
|
special files that may cause Borg to hang, so you no longer need to manually
|
||||||
exclude them. You can override/prevent this behavior by explicitly setting
|
exclude them. (This includes symlinks with special files as a destination.) You
|
||||||
`read_special` to true.
|
can override/prevent this behavior by explicitly setting `read_special` to true.
|
||||||
|
|
||||||
|
|
||||||
### Manual restoration
|
### Manual restoration
|
||||||
|
@ -237,7 +316,10 @@ user and you're extracting to `/tmp`, then the dump will be in
|
||||||
`/tmp/root/.borgmatic`.
|
`/tmp/root/.borgmatic`.
|
||||||
|
|
||||||
After extraction, you can manually restore the dump file using native database
|
After extraction, you can manually restore the dump file using native database
|
||||||
commands like `pg_restore`, `mysql`, `mongorestore` or similar.
|
commands like `pg_restore`, `mysql`, `mongorestore`, `sqlite`, or similar.
|
||||||
|
|
||||||
|
Also see the documentation on [listing database
|
||||||
|
dumps](https://torsion.org/borgmatic/docs/how-to/inspect-your-backups/#listing-database-dumps).
|
||||||
|
|
||||||
|
|
||||||
## Preparation and cleanup hooks
|
## Preparation and cleanup hooks
|
||||||
|
|
|
@ -9,44 +9,56 @@ eleventyNavigation:
|
||||||
|
|
||||||
Borg itself is great for efficiently de-duplicating data across successive
|
Borg itself is great for efficiently de-duplicating data across successive
|
||||||
backup archives, even when dealing with very large repositories. But you may
|
backup archives, even when dealing with very large repositories. But you may
|
||||||
find that while borgmatic's default mode of `prune`, `compact`, `create`, and
|
find that while borgmatic's default actions of `create`, `prune`, `compact`,
|
||||||
`check` works well on small repositories, it's not so great on larger ones.
|
and `check` works well on small repositories, it's not so great on larger
|
||||||
That's because running the default pruning, compact, and consistency checks
|
ones. That's because running the default pruning, compact, and consistency
|
||||||
take a long time on large repositories.
|
checks take a long time on large repositories.
|
||||||
|
|
||||||
|
<span class="minilink minilink-addedin">Prior to version 1.7.9</span> The
|
||||||
|
default action ordering was `prune`, `compact`, `create`, and `check`.
|
||||||
|
|
||||||
### A la carte actions
|
### A la carte actions
|
||||||
|
|
||||||
If you find yourself in this situation, you have some options. First, you can
|
If you find yourself wanting to customize the actions, you have some options.
|
||||||
run borgmatic's `prune`, `compact`, `create`, or `check` actions separately.
|
First, you can run borgmatic's `prune`, `compact`, `create`, or `check`
|
||||||
For instance, the following optional actions are available:
|
actions separately. For instance, the following optional actions are
|
||||||
|
available (among others):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
borgmatic create
|
||||||
borgmatic prune
|
borgmatic prune
|
||||||
borgmatic compact
|
borgmatic compact
|
||||||
borgmatic create
|
|
||||||
borgmatic check
|
borgmatic check
|
||||||
```
|
```
|
||||||
|
|
||||||
You can run with only one of these actions provided, or you can mix and match
|
You can run borgmatic with only one of these actions provided, or you can mix
|
||||||
any number of them in a single borgmatic run. This supports approaches like
|
and match any number of them in a single borgmatic run. This supports
|
||||||
skipping certain actions while running others. For instance, this skips
|
approaches like skipping certain actions while running others. For instance,
|
||||||
`prune` and `compact` and only runs `create` and `check`:
|
this skips `prune` and `compact` and only runs `create` and `check`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
borgmatic create check
|
borgmatic create check
|
||||||
```
|
```
|
||||||
|
|
||||||
Or, you can make backups with `create` on a frequent schedule (e.g. with
|
<span class="minilink minilink-addedin">New in version 1.7.9</span> borgmatic
|
||||||
`borgmatic create` called from one cron job), while only running expensive
|
now respects your specified command-line action order, running actions in the
|
||||||
consistency checks with `check` on a much less frequent basis (e.g. with
|
order you specify. In previous versions, borgmatic ran your specified actions
|
||||||
`borgmatic check` called from a separate cron job).
|
in a fixed ordering regardless of the order they appeared on the command-line.
|
||||||
|
|
||||||
|
But instead of running actions together, another option is to run backups with
|
||||||
|
`create` on a frequent schedule (e.g. with `borgmatic create` called from one
|
||||||
|
cron job), while only running expensive consistency checks with `check` on a
|
||||||
|
much less frequent basis (e.g. with `borgmatic check` called from a separate
|
||||||
|
cron job).
|
||||||
|
|
||||||
|
|
||||||
### Consistency check configuration
|
### Consistency check configuration
|
||||||
|
|
||||||
Another option is to customize your consistency checks. The default
|
Another option is to customize your consistency checks. By default, if you
|
||||||
consistency checks run both full-repository checks and per-archive checks
|
omit consistency checks from configuration, borgmatic runs full-repository
|
||||||
within each repository no more than once a month.
|
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.
|
||||||
|
|
||||||
But if you find that archive checks are too slow, for example, you can
|
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
|
configure borgmatic to run repository checks only. Configure this in the
|
||||||
|
@ -58,14 +70,25 @@ consistency:
|
||||||
- name: repository
|
- name: repository
|
||||||
```
|
```
|
||||||
|
|
||||||
(Prior to borgmatic 1.6.2, `checks` was a plain list of strings without the `name:` part.)
|
<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:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
consistency:
|
||||||
|
checks:
|
||||||
|
- repository
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
Here are the available checks from fastest to slowest:
|
Here are the available checks from fastest to slowest:
|
||||||
|
|
||||||
* `repository`: Checks the consistency of the repository itself.
|
* `repository`: Checks the consistency of the repository itself.
|
||||||
* `archives`: Checks all of the archives in the repository.
|
* `archives`: Checks all of the archives in the repository.
|
||||||
* `extract`: Performs an extraction dry-run of the most recent archive.
|
* `extract`: Performs an extraction dry-run of the most recent archive.
|
||||||
* `data`: Verifies the data integrity of all archives contents, decrypting and decompressing all data (implies `archives` as well).
|
* `data`: Verifies the data integrity of all archives contents, decrypting and decompressing all data.
|
||||||
|
|
||||||
|
Note that the `data` check is a more thorough version of the `archives` check,
|
||||||
|
so enabling the `data` check implicitly enables the `archives` check as well.
|
||||||
|
|
||||||
See [Borg's check
|
See [Borg's check
|
||||||
documentation](https://borgbackup.readthedocs.io/en/stable/usage/check.html)
|
documentation](https://borgbackup.readthedocs.io/en/stable/usage/check.html)
|
||||||
|
@ -120,7 +143,16 @@ consistency:
|
||||||
- name: disabled
|
- name: disabled
|
||||||
```
|
```
|
||||||
|
|
||||||
Or, if you have multiple repositories in your borgmatic configuration file,
|
<span class="minilink minilink-addedin">Prior to version 1.6.2</span> `checks`
|
||||||
|
was a plain list of strings without the `name:` part. For instance:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
consistency:
|
||||||
|
checks:
|
||||||
|
- disabled
|
||||||
|
```
|
||||||
|
|
||||||
|
If you have multiple repositories in your borgmatic configuration file,
|
||||||
you can keep running consistency checks, but only against a subset of the
|
you can keep running consistency checks, but only against a subset of the
|
||||||
repositories:
|
repositories:
|
||||||
|
|
||||||
|
|
|
@ -20,15 +20,15 @@ borgmatic rlist
|
||||||
That should yield output looking something like:
|
That should yield output looking something like:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
host-2019-01-01T04:05:06.070809 Tue, 2019-01-01 04:05:06 [...]
|
host-2023-01-01T04:05:06.070809 Tue, 2023-01-01 04:05:06 [...]
|
||||||
host-2019-01-02T04:06:07.080910 Wed, 2019-01-02 04:06:07 [...]
|
host-2023-01-02T04:06:07.080910 Wed, 2023-01-02 04:06:07 [...]
|
||||||
```
|
```
|
||||||
|
|
||||||
Assuming that you want to extract the archive with the most up-to-date files
|
Assuming that you want to extract the archive with the most up-to-date files
|
||||||
and therefore the latest timestamp, run a command like:
|
and therefore the latest timestamp, run a command like:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
borgmatic extract --archive host-2019-01-02T04:06:07.080910
|
borgmatic extract --archive host-2023-01-02T04:06:07.080910
|
||||||
```
|
```
|
||||||
|
|
||||||
(No borgmatic `extract` action? Upgrade borgmatic!)
|
(No borgmatic `extract` action? Upgrade borgmatic!)
|
||||||
|
@ -54,7 +54,7 @@ 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 path containing the archive to extract. Here's an example:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
borgmatic extract --repository repo.borg --archive host-2019-...
|
borgmatic extract --repository repo.borg --archive host-2023-...
|
||||||
```
|
```
|
||||||
|
|
||||||
## Extract particular files
|
## Extract particular files
|
||||||
|
@ -74,6 +74,13 @@ run the `extract` command above, borgmatic will extract `/var/path/1` and
|
||||||
`/var/path/2`.
|
`/var/path/2`.
|
||||||
|
|
||||||
|
|
||||||
|
### Searching for files
|
||||||
|
|
||||||
|
If you're not sure which archive contains the files you're looking for, you
|
||||||
|
can [search across
|
||||||
|
archives](https://torsion.org/borgmatic/docs/how-to/inspect-your-backups/#searching-for-a-file).
|
||||||
|
|
||||||
|
|
||||||
## Extract to a particular destination
|
## Extract to a particular destination
|
||||||
|
|
||||||
By default, borgmatic extracts files into the current directory. To instead
|
By default, borgmatic extracts files into the current directory. To instead
|
||||||
|
|
|
@ -91,6 +91,19 @@ example, to search only the last five archives:
|
||||||
borgmatic list --find foo.txt --last 5
|
borgmatic list --find foo.txt --last 5
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Listing database dumps
|
||||||
|
|
||||||
|
If you have enabled borgmatic's [database
|
||||||
|
hooks](https://torsion.org/borgmatic/docs/how-to/backup-your-databases/), you
|
||||||
|
can list backed up database dumps via borgmatic. For example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
borgmatic list --archive latest --find .borgmatic/*_databases
|
||||||
|
```
|
||||||
|
|
||||||
|
This gives you a listing of all database dump files contained in the latest
|
||||||
|
archive, complete with file sizes.
|
||||||
|
|
||||||
|
|
||||||
## Logging
|
## Logging
|
||||||
|
|
||||||
|
|
|
@ -42,3 +42,13 @@ potentially across providers.
|
||||||
See [Borg repository URLs
|
See [Borg repository URLs
|
||||||
documentation](https://borgbackup.readthedocs.io/en/stable/usage/general.html#repository-urls)
|
documentation](https://borgbackup.readthedocs.io/en/stable/usage/general.html#repository-urls)
|
||||||
for more information on how to specify local and remote repository paths.
|
for more information on how to specify local and remote repository paths.
|
||||||
|
|
||||||
|
### Different options per repository
|
||||||
|
|
||||||
|
What if you want borgmatic to backup to multiple repositories—while also
|
||||||
|
setting different options for each one? In that case, you'll need to use
|
||||||
|
[a separate borgmatic configuration file for each
|
||||||
|
repository](https://torsion.org/borgmatic/docs/how-to/make-per-application-backups/)
|
||||||
|
instead of the multiple repositories in one configuration file as described
|
||||||
|
above. That's because all of the repositories in a particular configuration
|
||||||
|
file get the same options applied.
|
||||||
|
|
|
@ -106,11 +106,60 @@ But if you do want to merge in a YAML key *and* its values, keep reading!
|
||||||
|
|
||||||
## Include merging
|
## Include merging
|
||||||
|
|
||||||
If you need to get even fancier and pull in common configuration options while
|
If you need to get even fancier and merge in common configuration options, you
|
||||||
potentially overriding individual options, you can perform a YAML merge of
|
can perform a YAML merge of included configuration using the YAML `<<` key.
|
||||||
included configuration using the YAML `<<` key. For instance, here's an
|
For instance, here's an example of a main configuration file that pulls in
|
||||||
example of a main configuration file that pulls in two retention options via
|
retention and consistency options via a single include:
|
||||||
an include and then overrides one of them locally:
|
|
||||||
|
```yaml
|
||||||
|
<<: !include /etc/borgmatic/common.yaml
|
||||||
|
|
||||||
|
location:
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
This is what `common.yaml` might look like:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
retention:
|
||||||
|
keep_hourly: 24
|
||||||
|
keep_daily: 7
|
||||||
|
|
||||||
|
consistency:
|
||||||
|
checks:
|
||||||
|
- name: repository
|
||||||
|
```
|
||||||
|
|
||||||
|
Once this include gets merged in, the resulting configuration would have all
|
||||||
|
of the `location` options from the original configuration file *and* the
|
||||||
|
`retention` and `consistency` options from the include.
|
||||||
|
|
||||||
|
Prior to borgmatic version 1.6.0, when there's a section collision between the
|
||||||
|
local file and the merged include, the local file's section takes precedence.
|
||||||
|
So if the `retention` section appears in both the local file and the include
|
||||||
|
file, the included `retention` is ignored in favor of the local `retention`.
|
||||||
|
But see below about deep merge in version 1.6.0+.
|
||||||
|
|
||||||
|
Note that this `<<` include merging syntax is only for merging in mappings
|
||||||
|
(configuration options and their values). But if you'd like to include a
|
||||||
|
single value directly, please see the section above about standard includes.
|
||||||
|
|
||||||
|
Additionally, there is a limitation preventing multiple `<<` include merges
|
||||||
|
per section. So for instance, that means you can do one `<<` merge at the
|
||||||
|
global level, another `<<` within each configuration section, etc. (This is a
|
||||||
|
YAML limitation.)
|
||||||
|
|
||||||
|
|
||||||
|
### Deep merge
|
||||||
|
|
||||||
|
<span class="minilink minilink-addedin">New in version 1.6.0</span> borgmatic
|
||||||
|
performs a deep merge of merged include files, meaning that values are merged
|
||||||
|
at all levels in the two configuration files. This allows you to include
|
||||||
|
common configuration—up to full borgmatic configuration files—while overriding
|
||||||
|
only the parts you want to customize.
|
||||||
|
|
||||||
|
For instance, here's an example of a main configuration file that pulls in two
|
||||||
|
retention options via an include and then overrides one of them locally:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
<<: !include /etc/borgmatic/common.yaml
|
<<: !include /etc/borgmatic/common.yaml
|
||||||
|
@ -136,24 +185,8 @@ Once this include gets merged in, the resulting configuration would have a
|
||||||
When there's an option collision between the local file and the merged
|
When there's an option collision between the local file and the merged
|
||||||
include, the local file's option takes precedence.
|
include, the local file's option takes precedence.
|
||||||
|
|
||||||
Note that this `<<` include merging syntax is only for merging in mappings
|
<span class="minilink minilink-addedin">New in version 1.6.1</span> Colliding
|
||||||
(configuration options and their values). But if you'd like to include a
|
list values are appended together.
|
||||||
single value directly, please see the section above about standard includes.
|
|
||||||
|
|
||||||
Additionally, there is a limitation preventing multiple `<<` include merges
|
|
||||||
per section. So for instance, that means you can do one `<<` merge at the
|
|
||||||
global level, another `<<` within each configuration section, etc. (This is a
|
|
||||||
YAML limitation.)
|
|
||||||
|
|
||||||
|
|
||||||
### Deep merge
|
|
||||||
|
|
||||||
<span class="minilink minilink-addedin">New in version 1.6.0</span> borgmatic
|
|
||||||
performs a deep merge of merged include files, meaning that values are merged
|
|
||||||
at all levels in the two configuration files. Colliding list values are
|
|
||||||
appended together. This allows you to include common configuration—up to full
|
|
||||||
borgmatic configuration files—while overriding only the parts you want to
|
|
||||||
customize.
|
|
||||||
|
|
||||||
|
|
||||||
## Configuration overrides
|
## Configuration overrides
|
||||||
|
|
|
@ -83,7 +83,7 @@ tests](https://torsion.org/borgmatic/docs/how-to/extract-a-backup/).
|
||||||
|
|
||||||
## Error hooks
|
## Error hooks
|
||||||
|
|
||||||
When an error occurs during a `prune`, `compact`, `create`, or `check` action,
|
When an error occurs during a `create`, `prune`, `compact`, or `check` action,
|
||||||
borgmatic can run configurable shell commands to fire off custom error
|
borgmatic can run configurable shell commands to fire off custom error
|
||||||
notifications or take other actions, so you can get alerted as soon as
|
notifications or take other actions, so you can get alerted as soon as
|
||||||
something goes wrong. Here's a not-so-useful example:
|
something goes wrong. Here's a not-so-useful example:
|
||||||
|
@ -116,8 +116,8 @@ the repository. Here's the full set of supported variables you can use here:
|
||||||
* `output`: output of the command that failed (may be blank if an error
|
* `output`: output of the command that failed (may be blank if an error
|
||||||
occurred without running a command)
|
occurred without running a command)
|
||||||
|
|
||||||
Note that borgmatic runs the `on_error` hooks only for `prune`, `compact`,
|
Note that borgmatic runs the `on_error` hooks only for `create`, `prune`,
|
||||||
`create`, or `check` actions or hooks in which an error occurs, and not other
|
`compact`, or `check` actions or hooks in which an error occurs, and not other
|
||||||
actions. borgmatic does not run `on_error` hooks if an error occurs within a
|
actions. borgmatic does not run `on_error` hooks if an error occurs within a
|
||||||
`before_everything` or `after_everything` hook. For more about hooks, see the
|
`before_everything` or `after_everything` hook. For more about hooks, see the
|
||||||
[borgmatic hooks
|
[borgmatic hooks
|
||||||
|
@ -144,7 +144,7 @@ With this hook in place, borgmatic pings your Healthchecks project when a
|
||||||
backup begins, ends, or errors. Specifically, after the <a
|
backup begins, ends, or errors. Specifically, after the <a
|
||||||
href="https://torsion.org/borgmatic/docs/how-to/add-preparation-and-cleanup-steps-to-backups/">`before_backup`
|
href="https://torsion.org/borgmatic/docs/how-to/add-preparation-and-cleanup-steps-to-backups/">`before_backup`
|
||||||
hooks</a> run, borgmatic lets Healthchecks know that it has started if any of
|
hooks</a> run, borgmatic lets Healthchecks know that it has started if any of
|
||||||
the `prune`, `compact`, `create`, or `check` actions are run.
|
the `create`, `prune`, `compact`, or `check` actions are run.
|
||||||
|
|
||||||
Then, if the actions complete successfully, borgmatic notifies Healthchecks of
|
Then, if the actions complete successfully, borgmatic notifies Healthchecks of
|
||||||
the success after the `after_backup` hooks run, and includes borgmatic logs in
|
the success after the `after_backup` hooks run, and includes borgmatic logs in
|
||||||
|
@ -154,8 +154,8 @@ in the Healthchecks UI, although be aware that Healthchecks currently has a
|
||||||
|
|
||||||
If an error occurs during any action or hook, borgmatic notifies Healthchecks
|
If an error occurs during any action or hook, borgmatic notifies Healthchecks
|
||||||
after the `on_error` hooks run, also tacking on logs including the error
|
after the `on_error` hooks run, also tacking on logs including the error
|
||||||
itself. But the logs are only included for errors that occur when a `prune`,
|
itself. But the logs are only included for errors that occur when a `create`,
|
||||||
`compact`, `create`, or `check` action is run.
|
`prune`, `compact`, or `check` action is run.
|
||||||
|
|
||||||
You can customize the verbosity of the logs that are sent to Healthchecks with
|
You can customize the verbosity of the logs that are sent to Healthchecks with
|
||||||
borgmatic's `--monitoring-verbosity` flag. The `--list` and `--stats` flags
|
borgmatic's `--monitoring-verbosity` flag. The `--list` and `--stats` flags
|
||||||
|
|
|
@ -93,6 +93,7 @@ installing borgmatic:
|
||||||
* [OpenBSD](http://ports.su/sysutils/borgmatic)
|
* [OpenBSD](http://ports.su/sysutils/borgmatic)
|
||||||
* [openSUSE](https://software.opensuse.org/package/borgmatic)
|
* [openSUSE](https://software.opensuse.org/package/borgmatic)
|
||||||
* [macOS (via Homebrew)](https://formulae.brew.sh/formula/borgmatic)
|
* [macOS (via Homebrew)](https://formulae.brew.sh/formula/borgmatic)
|
||||||
|
* [macOS (via MacPorts)](https://ports.macports.org/port/borgmatic/)
|
||||||
* [Ansible role](https://github.com/borgbase/ansible-role-borgbackup)
|
* [Ansible role](https://github.com/borgbase/ansible-role-borgbackup)
|
||||||
* [virtualenv](https://virtualenv.pypa.io/en/stable/)
|
* [virtualenv](https://virtualenv.pypa.io/en/stable/)
|
||||||
|
|
||||||
|
@ -257,9 +258,9 @@ See `borgmatic --help` and `borgmatic create --help` for more information.
|
||||||
|
|
||||||
If you omit `create` and other actions, borgmatic runs through a set of
|
If you omit `create` and other actions, borgmatic runs through a set of
|
||||||
default actions: `prune` any old backups as per the configured retention
|
default actions: `prune` any old backups as per the configured retention
|
||||||
policy, `compact` segments to free up space (with Borg 1.2+), `create` a
|
policy, `compact` segments to free up space (with Borg 1.2+, borgmatic
|
||||||
backup, *and* `check` backups for consistency problems due to things like file
|
1.5.23+), `create` a backup, *and* `check` backups for consistency problems
|
||||||
damage. For instance:
|
due to things like file damage. For instance:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo borgmatic --verbosity 1 --list --stats
|
sudo borgmatic --verbosity 1 --list --stats
|
||||||
|
|
|
@ -160,17 +160,31 @@ Then, run the `rcreate` action (formerly `init`) to create that new Borg 2
|
||||||
repository:
|
repository:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
borgmatic rcreate --verbosity 1 --encryption repokey-aes-ocb \
|
borgmatic rcreate --verbosity 1 --encryption repokey-blake2-aes-ocb \
|
||||||
--source-repository original.borg --repository upgraded.borg
|
--source-repository original.borg --repository upgraded.borg
|
||||||
```
|
```
|
||||||
|
|
||||||
(Note that `repokey-chacha20-poly1305` may be faster than `repokey-aes-ocb` on
|
|
||||||
certain platforms like ARM64.)
|
|
||||||
|
|
||||||
This creates an empty repository and doesn't actually transfer any data yet.
|
This creates an empty repository and doesn't actually transfer any data yet.
|
||||||
The `--source-repository` flag is necessary to reuse key material from your
|
The `--source-repository` flag is necessary to reuse key material from your
|
||||||
Borg 1 repository so that the subsequent data transfer can work.
|
Borg 1 repository so that the subsequent data transfer can work.
|
||||||
|
|
||||||
|
The `--encryption` value above selects the same chunk ID algorithm (`blake2`)
|
||||||
|
commonly used in Borg 1, thereby making deduplication work across transferred
|
||||||
|
archives and new archives.
|
||||||
|
|
||||||
|
If you get an error about "You must keep the same ID hash" from Borg, that
|
||||||
|
means the encryption value you specified doesn't correspond to your source
|
||||||
|
repository's chunk ID algorithm. In that case, try not using `blake2`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
borgmatic rcreate --verbosity 1 --encryption repokey-aes-ocb \
|
||||||
|
--source-repository original.borg --repository upgraded.borg
|
||||||
|
```
|
||||||
|
|
||||||
|
Read about [Borg encryption
|
||||||
|
modes](https://borgbackup.readthedocs.io/en/2.0.0b5/usage/rcreate.html#encryption-mode-tldr)
|
||||||
|
for more details.
|
||||||
|
|
||||||
To transfer data from your original Borg 1 repository to your newly created
|
To transfer data from your original Borg 1 repository to your newly created
|
||||||
Borg 2 repository:
|
Borg 2 repository:
|
||||||
|
|
||||||
|
@ -189,9 +203,9 @@ might take a while), and the final command with `--dry-run` again provides
|
||||||
confirmation of success—or tells you if something hasn't been transferred yet.
|
confirmation of success—or tells you if something hasn't been transferred yet.
|
||||||
|
|
||||||
Note that by omitting the `--upgrader` flag, you can also do archive transfers
|
Note that by omitting the `--upgrader` flag, you can also do archive transfers
|
||||||
between Borg 2 repositories without upgrading, even down to individual
|
between related Borg 2 repositories without upgrading, even down to individual
|
||||||
archives. For more on that functionality, see the [Borg transfer
|
archives. For more on that functionality, see the [Borg transfer
|
||||||
documentation](https://borgbackup.readthedocs.io/en/2.0.0b1/usage/transfer.html).
|
documentation](https://borgbackup.readthedocs.io/en/2.0.0b5/usage/transfer.html).
|
||||||
|
|
||||||
That's it! Now you can use your new Borg 2 repository as normal with
|
That's it! Now you can use your new Borg 2 repository as normal with
|
||||||
borgmatic. If you've got multiple repositories, repeat the above process for
|
borgmatic. If you've got multiple repositories, repeat the above process for
|
||||||
|
|
|
@ -13,9 +13,3 @@ each action sub-command:
|
||||||
```
|
```
|
||||||
{% include borgmatic/command-line.txt %}
|
{% include borgmatic/command-line.txt %}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
## Related documentation
|
|
||||||
|
|
||||||
* [Set up backups with borgmatic](https://torsion.org/borgmatic/docs/how-to/set-up-backups/)
|
|
||||||
* [borgmatic configuration reference](https://torsion.org/borgmatic/docs/reference/configuration/)
|
|
||||||
|
|
|
@ -15,9 +15,3 @@ Here is a full sample borgmatic configuration file including all available optio
|
||||||
|
|
||||||
Note that you can also [download this configuration
|
Note that you can also [download this configuration
|
||||||
file](https://torsion.org/borgmatic/docs/reference/config.yaml) for use locally.
|
file](https://torsion.org/borgmatic/docs/reference/config.yaml) for use locally.
|
||||||
|
|
||||||
|
|
||||||
## Related documentation
|
|
||||||
|
|
||||||
* [Set up backups with borgmatic](https://torsion.org/borgmatic/docs/how-to/set-up-backups/)
|
|
||||||
* [borgmatic command-line reference](https://torsion.org/borgmatic/docs/reference/command-line/)
|
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 4.6 KiB |
|
@ -11,7 +11,7 @@
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
apk add --no-cache python3 py3-pip borgbackup postgresql-client mariadb-client mongodb-tools \
|
apk add --no-cache python3 py3-pip borgbackup postgresql-client mariadb-client mongodb-tools \
|
||||||
py3-ruamel.yaml py3-ruamel.yaml.clib bash
|
py3-ruamel.yaml py3-ruamel.yaml.clib bash sqlite
|
||||||
# If certain dependencies of black are available in this version of Alpine, install them.
|
# If certain dependencies of black are available in this version of Alpine, install them.
|
||||||
apk add --no-cache py3-typed-ast py3-regex || true
|
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
|
python3 -m pip install --no-cache --upgrade pip==22.2.2 setuptools==64.0.1
|
||||||
|
|
|
@ -10,6 +10,8 @@ filterwarnings =
|
||||||
[flake8]
|
[flake8]
|
||||||
ignore = E501,W503
|
ignore = E501,W503
|
||||||
exclude = *.*/*
|
exclude = *.*/*
|
||||||
|
multiline-quotes = '''
|
||||||
|
docstring-quotes = '''
|
||||||
|
|
||||||
[tool:isort]
|
[tool:isort]
|
||||||
force_single_line = False
|
force_single_line = False
|
||||||
|
|
2
setup.py
2
setup.py
|
@ -1,6 +1,6 @@
|
||||||
from setuptools import find_packages, setup
|
from setuptools import find_packages, setup
|
||||||
|
|
||||||
VERSION = '1.7.3'
|
VERSION = '1.7.9.dev0'
|
||||||
|
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
|
|
|
@ -5,6 +5,7 @@ click==7.1.2; python_version >= '3.8'
|
||||||
colorama==0.4.4
|
colorama==0.4.4
|
||||||
coverage==5.3
|
coverage==5.3
|
||||||
flake8==4.0.1
|
flake8==4.0.1
|
||||||
|
flake8-quotes==3.3.2
|
||||||
flexmock==0.10.4
|
flexmock==0.10.4
|
||||||
isort==5.9.1
|
isort==5.9.1
|
||||||
mccabe==0.6.1
|
mccabe==0.6.1
|
||||||
|
@ -14,8 +15,8 @@ py==1.10.0
|
||||||
pycodestyle==2.8.0
|
pycodestyle==2.8.0
|
||||||
pyflakes==2.4.0
|
pyflakes==2.4.0
|
||||||
jsonschema==3.2.0
|
jsonschema==3.2.0
|
||||||
pytest==6.2.5
|
pytest==7.2.0
|
||||||
pytest-cov==3.0.0
|
pytest-cov==4.0.0
|
||||||
regex; python_version >= '3.8'
|
regex; python_version >= '3.8'
|
||||||
requests==2.25.0
|
requests==2.25.0
|
||||||
ruamel.yaml>0.15.0,<0.18.0
|
ruamel.yaml>0.15.0,<0.18.0
|
||||||
|
|
|
@ -14,6 +14,7 @@ def write_configuration(
|
||||||
repository_path,
|
repository_path,
|
||||||
borgmatic_source_directory,
|
borgmatic_source_directory,
|
||||||
postgresql_dump_format='custom',
|
postgresql_dump_format='custom',
|
||||||
|
mongodb_dump_format='archive',
|
||||||
):
|
):
|
||||||
'''
|
'''
|
||||||
Write out borgmatic configuration into a file at the config path. Set the options so as to work
|
Write out borgmatic configuration into a file at the config path. Set the options so as to work
|
||||||
|
@ -42,6 +43,11 @@ hooks:
|
||||||
hostname: postgresql
|
hostname: postgresql
|
||||||
username: postgres
|
username: postgres
|
||||||
password: test
|
password: test
|
||||||
|
- name: all
|
||||||
|
format: custom
|
||||||
|
hostname: postgresql
|
||||||
|
username: postgres
|
||||||
|
password: test
|
||||||
mysql_databases:
|
mysql_databases:
|
||||||
- name: test
|
- name: test
|
||||||
hostname: mysql
|
hostname: mysql
|
||||||
|
@ -51,16 +57,25 @@ hooks:
|
||||||
hostname: mysql
|
hostname: mysql
|
||||||
username: root
|
username: root
|
||||||
password: test
|
password: test
|
||||||
|
- name: all
|
||||||
|
format: sql
|
||||||
|
hostname: mysql
|
||||||
|
username: root
|
||||||
|
password: test
|
||||||
mongodb_databases:
|
mongodb_databases:
|
||||||
- name: test
|
- name: test
|
||||||
hostname: mongodb
|
hostname: mongodb
|
||||||
username: root
|
username: root
|
||||||
password: test
|
password: test
|
||||||
authentication_database: admin
|
authentication_database: admin
|
||||||
|
format: {mongodb_dump_format}
|
||||||
- name: all
|
- name: all
|
||||||
hostname: mongodb
|
hostname: mongodb
|
||||||
username: root
|
username: root
|
||||||
password: test
|
password: test
|
||||||
|
sqlite_databases:
|
||||||
|
- name: sqlite_test
|
||||||
|
path: /tmp/sqlite_test.db
|
||||||
'''
|
'''
|
||||||
|
|
||||||
with open(config_path, 'w') as config_file:
|
with open(config_path, 'w') as config_file:
|
||||||
|
@ -126,6 +141,7 @@ def test_database_dump_and_restore_with_directory_format():
|
||||||
repository_path,
|
repository_path,
|
||||||
borgmatic_source_directory,
|
borgmatic_source_directory,
|
||||||
postgresql_dump_format='directory',
|
postgresql_dump_format='directory',
|
||||||
|
mongodb_dump_format='directory',
|
||||||
)
|
)
|
||||||
|
|
||||||
subprocess.check_call(
|
subprocess.check_call(
|
||||||
|
|
|
@ -422,6 +422,13 @@ def test_parse_arguments_with_list_flag_but_no_relevant_action_raises_value_erro
|
||||||
module.parse_arguments('--list', 'rcreate')
|
module.parse_arguments('--list', 'rcreate')
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_arguments_disallows_list_with_progress_for_create_action():
|
||||||
|
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||||
|
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
module.parse_arguments('create', '--list', '--progress')
|
||||||
|
|
||||||
|
|
||||||
def test_parse_arguments_allows_json_with_list_or_info():
|
def test_parse_arguments_allows_json_with_list_or_info():
|
||||||
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
from flexmock import flexmock
|
||||||
|
|
||||||
|
from borgmatic.actions import borg as module
|
||||||
|
|
||||||
|
|
||||||
|
def test_run_borg_does_not_raise():
|
||||||
|
flexmock(module.logger).answer = lambda message: None
|
||||||
|
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(True)
|
||||||
|
flexmock(module.borgmatic.borg.rlist).should_receive('resolve_archive_name').and_return(
|
||||||
|
flexmock()
|
||||||
|
)
|
||||||
|
flexmock(module.borgmatic.borg.borg).should_receive('run_arbitrary_borg')
|
||||||
|
borg_arguments = flexmock(repository=flexmock(), archive=flexmock(), options=flexmock())
|
||||||
|
|
||||||
|
module.run_borg(
|
||||||
|
repository='repo',
|
||||||
|
storage={},
|
||||||
|
local_borg_version=None,
|
||||||
|
borg_arguments=borg_arguments,
|
||||||
|
local_path=None,
|
||||||
|
remote_path=None,
|
||||||
|
)
|
|
@ -0,0 +1,19 @@
|
||||||
|
from flexmock import flexmock
|
||||||
|
|
||||||
|
from borgmatic.actions import break_lock as module
|
||||||
|
|
||||||
|
|
||||||
|
def test_run_break_lock_does_not_raise():
|
||||||
|
flexmock(module.logger).answer = lambda message: None
|
||||||
|
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(True)
|
||||||
|
flexmock(module.borgmatic.borg.break_lock).should_receive('break_lock')
|
||||||
|
break_lock_arguments = flexmock(repository=flexmock())
|
||||||
|
|
||||||
|
module.run_break_lock(
|
||||||
|
repository='repo',
|
||||||
|
storage={},
|
||||||
|
local_borg_version=None,
|
||||||
|
break_lock_arguments=break_lock_arguments,
|
||||||
|
local_path=None,
|
||||||
|
remote_path=None,
|
||||||
|
)
|
|
@ -0,0 +1,31 @@
|
||||||
|
from flexmock import flexmock
|
||||||
|
|
||||||
|
from borgmatic.actions import check as module
|
||||||
|
|
||||||
|
|
||||||
|
def test_run_check_calls_hooks():
|
||||||
|
flexmock(module.logger).answer = lambda message: None
|
||||||
|
flexmock(module.borgmatic.config.checks).should_receive(
|
||||||
|
'repository_enabled_for_checks'
|
||||||
|
).and_return(True)
|
||||||
|
flexmock(module.borgmatic.borg.check).should_receive('check_archives')
|
||||||
|
flexmock(module.borgmatic.hooks.command).should_receive('execute_hook').times(2)
|
||||||
|
check_arguments = flexmock(
|
||||||
|
progress=flexmock(), repair=flexmock(), only=flexmock(), force=flexmock()
|
||||||
|
)
|
||||||
|
global_arguments = flexmock(monitoring_verbosity=1, dry_run=False)
|
||||||
|
|
||||||
|
module.run_check(
|
||||||
|
config_filename='test.yaml',
|
||||||
|
repository='repo',
|
||||||
|
location={'repositories': ['repo']},
|
||||||
|
storage={},
|
||||||
|
consistency={},
|
||||||
|
hooks={},
|
||||||
|
hook_context={},
|
||||||
|
local_borg_version=None,
|
||||||
|
check_arguments=check_arguments,
|
||||||
|
global_arguments=global_arguments,
|
||||||
|
local_path=None,
|
||||||
|
remote_path=None,
|
||||||
|
)
|
|
@ -0,0 +1,29 @@
|
||||||
|
from flexmock import flexmock
|
||||||
|
|
||||||
|
from borgmatic.actions import compact as module
|
||||||
|
|
||||||
|
|
||||||
|
def test_compact_actions_calls_hooks():
|
||||||
|
flexmock(module.logger).answer = lambda message: None
|
||||||
|
flexmock(module.borgmatic.borg.feature).should_receive('available').and_return(True)
|
||||||
|
flexmock(module.borgmatic.borg.compact).should_receive('compact_segments')
|
||||||
|
flexmock(module.borgmatic.hooks.command).should_receive('execute_hook').times(2)
|
||||||
|
compact_arguments = flexmock(
|
||||||
|
progress=flexmock(), cleanup_commits=flexmock(), threshold=flexmock()
|
||||||
|
)
|
||||||
|
global_arguments = flexmock(monitoring_verbosity=1, dry_run=False)
|
||||||
|
|
||||||
|
module.run_compact(
|
||||||
|
config_filename='test.yaml',
|
||||||
|
repository='repo',
|
||||||
|
storage={},
|
||||||
|
retention={},
|
||||||
|
hooks={},
|
||||||
|
hook_context={},
|
||||||
|
local_borg_version=None,
|
||||||
|
compact_arguments=compact_arguments,
|
||||||
|
global_arguments=global_arguments,
|
||||||
|
dry_run_label='',
|
||||||
|
local_path=None,
|
||||||
|
remote_path=None,
|
||||||
|
)
|
|
@ -0,0 +1,34 @@
|
||||||
|
from flexmock import flexmock
|
||||||
|
|
||||||
|
from borgmatic.actions import create as module
|
||||||
|
|
||||||
|
|
||||||
|
def test_run_create_executes_and_calls_hooks():
|
||||||
|
flexmock(module.logger).answer = lambda message: None
|
||||||
|
flexmock(module.borgmatic.borg.create).should_receive('create_archive')
|
||||||
|
flexmock(module.borgmatic.hooks.command).should_receive('execute_hook').times(2)
|
||||||
|
flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hooks').and_return({})
|
||||||
|
flexmock(module.borgmatic.hooks.dispatch).should_receive(
|
||||||
|
'call_hooks_even_if_unconfigured'
|
||||||
|
).and_return({})
|
||||||
|
create_arguments = flexmock(
|
||||||
|
progress=flexmock(), stats=flexmock(), json=flexmock(), list_files=flexmock()
|
||||||
|
)
|
||||||
|
global_arguments = flexmock(monitoring_verbosity=1, dry_run=False)
|
||||||
|
|
||||||
|
list(
|
||||||
|
module.run_create(
|
||||||
|
config_filename='test.yaml',
|
||||||
|
repository='repo',
|
||||||
|
location={},
|
||||||
|
storage={},
|
||||||
|
hooks={},
|
||||||
|
hook_context={},
|
||||||
|
local_borg_version=None,
|
||||||
|
create_arguments=create_arguments,
|
||||||
|
global_arguments=global_arguments,
|
||||||
|
dry_run_label='',
|
||||||
|
local_path=None,
|
||||||
|
remote_path=None,
|
||||||
|
)
|
||||||
|
)
|
|
@ -0,0 +1,29 @@
|
||||||
|
from flexmock import flexmock
|
||||||
|
|
||||||
|
from borgmatic.actions import export_tar as module
|
||||||
|
|
||||||
|
|
||||||
|
def test_run_export_tar_does_not_raise():
|
||||||
|
flexmock(module.logger).answer = lambda message: None
|
||||||
|
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(True)
|
||||||
|
flexmock(module.borgmatic.borg.export_tar).should_receive('export_tar_archive')
|
||||||
|
export_tar_arguments = flexmock(
|
||||||
|
repository=flexmock(),
|
||||||
|
archive=flexmock(),
|
||||||
|
paths=flexmock(),
|
||||||
|
destination=flexmock(),
|
||||||
|
tar_filter=flexmock(),
|
||||||
|
list_files=flexmock(),
|
||||||
|
strip_components=flexmock(),
|
||||||
|
)
|
||||||
|
global_arguments = flexmock(monitoring_verbosity=1, dry_run=False)
|
||||||
|
|
||||||
|
module.run_export_tar(
|
||||||
|
repository='repo',
|
||||||
|
storage={},
|
||||||
|
local_borg_version=None,
|
||||||
|
export_tar_arguments=export_tar_arguments,
|
||||||
|
global_arguments=global_arguments,
|
||||||
|
local_path=None,
|
||||||
|
remote_path=None,
|
||||||
|
)
|
|
@ -0,0 +1,33 @@
|
||||||
|
from flexmock import flexmock
|
||||||
|
|
||||||
|
from borgmatic.actions import extract as module
|
||||||
|
|
||||||
|
|
||||||
|
def test_run_extract_calls_hooks():
|
||||||
|
flexmock(module.logger).answer = lambda message: None
|
||||||
|
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(True)
|
||||||
|
flexmock(module.borgmatic.borg.extract).should_receive('extract_archive')
|
||||||
|
flexmock(module.borgmatic.hooks.command).should_receive('execute_hook').times(2)
|
||||||
|
extract_arguments = flexmock(
|
||||||
|
paths=flexmock(),
|
||||||
|
progress=flexmock(),
|
||||||
|
destination=flexmock(),
|
||||||
|
strip_components=flexmock(),
|
||||||
|
archive=flexmock(),
|
||||||
|
repository='repo',
|
||||||
|
)
|
||||||
|
global_arguments = flexmock(monitoring_verbosity=1, dry_run=False)
|
||||||
|
|
||||||
|
module.run_extract(
|
||||||
|
config_filename='test.yaml',
|
||||||
|
repository='repo',
|
||||||
|
location={'repositories': ['repo']},
|
||||||
|
storage={},
|
||||||
|
hooks={},
|
||||||
|
hook_context={},
|
||||||
|
local_borg_version=None,
|
||||||
|
extract_arguments=extract_arguments,
|
||||||
|
global_arguments=global_arguments,
|
||||||
|
local_path=None,
|
||||||
|
remote_path=None,
|
||||||
|
)
|
|
@ -0,0 +1,24 @@
|
||||||
|
from flexmock import flexmock
|
||||||
|
|
||||||
|
from borgmatic.actions import info as module
|
||||||
|
|
||||||
|
|
||||||
|
def test_run_info_does_not_raise():
|
||||||
|
flexmock(module.logger).answer = lambda message: None
|
||||||
|
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(True)
|
||||||
|
flexmock(module.borgmatic.borg.rlist).should_receive('resolve_archive_name').and_return(
|
||||||
|
flexmock()
|
||||||
|
)
|
||||||
|
flexmock(module.borgmatic.borg.info).should_receive('display_archives_info')
|
||||||
|
info_arguments = flexmock(repository=flexmock(), archive=flexmock(), json=flexmock())
|
||||||
|
|
||||||
|
list(
|
||||||
|
module.run_info(
|
||||||
|
repository='repo',
|
||||||
|
storage={},
|
||||||
|
local_borg_version=None,
|
||||||
|
info_arguments=info_arguments,
|
||||||
|
local_path=None,
|
||||||
|
remote_path=None,
|
||||||
|
)
|
||||||
|
)
|
|
@ -0,0 +1,24 @@
|
||||||
|
from flexmock import flexmock
|
||||||
|
|
||||||
|
from borgmatic.actions import list as module
|
||||||
|
|
||||||
|
|
||||||
|
def test_run_list_does_not_raise():
|
||||||
|
flexmock(module.logger).answer = lambda message: None
|
||||||
|
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(True)
|
||||||
|
flexmock(module.borgmatic.borg.rlist).should_receive('resolve_archive_name').and_return(
|
||||||
|
flexmock()
|
||||||
|
)
|
||||||
|
flexmock(module.borgmatic.borg.list).should_receive('list_archive')
|
||||||
|
list_arguments = flexmock(repository=flexmock(), archive=flexmock(), json=flexmock())
|
||||||
|
|
||||||
|
list(
|
||||||
|
module.run_list(
|
||||||
|
repository='repo',
|
||||||
|
storage={},
|
||||||
|
local_borg_version=None,
|
||||||
|
list_arguments=list_arguments,
|
||||||
|
local_path=None,
|
||||||
|
remote_path=None,
|
||||||
|
)
|
||||||
|
)
|
|
@ -0,0 +1,26 @@
|
||||||
|
from flexmock import flexmock
|
||||||
|
|
||||||
|
from borgmatic.actions import mount as module
|
||||||
|
|
||||||
|
|
||||||
|
def test_run_mount_does_not_raise():
|
||||||
|
flexmock(module.logger).answer = lambda message: None
|
||||||
|
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(True)
|
||||||
|
flexmock(module.borgmatic.borg.mount).should_receive('mount_archive')
|
||||||
|
mount_arguments = flexmock(
|
||||||
|
repository=flexmock(),
|
||||||
|
archive=flexmock(),
|
||||||
|
mount_point=flexmock(),
|
||||||
|
paths=flexmock(),
|
||||||
|
foreground=flexmock(),
|
||||||
|
options=flexmock(),
|
||||||
|
)
|
||||||
|
|
||||||
|
module.run_mount(
|
||||||
|
repository='repo',
|
||||||
|
storage={},
|
||||||
|
local_borg_version=None,
|
||||||
|
mount_arguments=mount_arguments,
|
||||||
|
local_path=None,
|
||||||
|
remote_path=None,
|
||||||
|
)
|
|
@ -0,0 +1,26 @@
|
||||||
|
from flexmock import flexmock
|
||||||
|
|
||||||
|
from borgmatic.actions import prune as module
|
||||||
|
|
||||||
|
|
||||||
|
def test_run_prune_calls_hooks():
|
||||||
|
flexmock(module.logger).answer = lambda message: None
|
||||||
|
flexmock(module.borgmatic.borg.prune).should_receive('prune_archives')
|
||||||
|
flexmock(module.borgmatic.hooks.command).should_receive('execute_hook').times(2)
|
||||||
|
prune_arguments = flexmock(stats=flexmock(), list_archives=flexmock())
|
||||||
|
global_arguments = flexmock(monitoring_verbosity=1, dry_run=False)
|
||||||
|
|
||||||
|
module.run_prune(
|
||||||
|
config_filename='test.yaml',
|
||||||
|
repository='repo',
|
||||||
|
storage={},
|
||||||
|
retention={},
|
||||||
|
hooks={},
|
||||||
|
hook_context={},
|
||||||
|
local_borg_version=None,
|
||||||
|
prune_arguments=prune_arguments,
|
||||||
|
global_arguments=global_arguments,
|
||||||
|
dry_run_label='',
|
||||||
|
local_path=None,
|
||||||
|
remote_path=None,
|
||||||
|
)
|
|
@ -0,0 +1,55 @@
|
||||||
|
from flexmock import flexmock
|
||||||
|
|
||||||
|
from borgmatic.actions import rcreate as module
|
||||||
|
|
||||||
|
|
||||||
|
def test_run_rcreate_does_not_raise():
|
||||||
|
flexmock(module.logger).answer = lambda message: None
|
||||||
|
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(True)
|
||||||
|
flexmock(module.borgmatic.borg.rcreate).should_receive('create_repository')
|
||||||
|
arguments = flexmock(
|
||||||
|
encryption_mode=flexmock(),
|
||||||
|
source_repository=flexmock(),
|
||||||
|
repository=flexmock(),
|
||||||
|
copy_crypt_key=flexmock(),
|
||||||
|
append_only=flexmock(),
|
||||||
|
storage_quota=flexmock(),
|
||||||
|
make_parent_dirs=flexmock(),
|
||||||
|
)
|
||||||
|
|
||||||
|
module.run_rcreate(
|
||||||
|
repository='repo',
|
||||||
|
storage={},
|
||||||
|
local_borg_version=None,
|
||||||
|
rcreate_arguments=arguments,
|
||||||
|
global_arguments=flexmock(dry_run=False),
|
||||||
|
local_path=None,
|
||||||
|
remote_path=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_run_rcreate_bails_if_repository_does_not_match():
|
||||||
|
flexmock(module.logger).answer = lambda message: None
|
||||||
|
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(
|
||||||
|
False
|
||||||
|
)
|
||||||
|
flexmock(module.borgmatic.borg.rcreate).should_receive('create_repository').never()
|
||||||
|
arguments = flexmock(
|
||||||
|
encryption_mode=flexmock(),
|
||||||
|
source_repository=flexmock(),
|
||||||
|
repository=flexmock(),
|
||||||
|
copy_crypt_key=flexmock(),
|
||||||
|
append_only=flexmock(),
|
||||||
|
storage_quota=flexmock(),
|
||||||
|
make_parent_dirs=flexmock(),
|
||||||
|
)
|
||||||
|
|
||||||
|
module.run_rcreate(
|
||||||
|
repository='repo',
|
||||||
|
storage={},
|
||||||
|
local_borg_version=None,
|
||||||
|
rcreate_arguments=arguments,
|
||||||
|
global_arguments=flexmock(dry_run=False),
|
||||||
|
local_path=None,
|
||||||
|
remote_path=None,
|
||||||
|
)
|
|
@ -0,0 +1,495 @@
|
||||||
|
import pytest
|
||||||
|
from flexmock import flexmock
|
||||||
|
|
||||||
|
import borgmatic.actions.restore as module
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_configured_database_matches_database_by_name():
|
||||||
|
assert module.get_configured_database(
|
||||||
|
hooks={
|
||||||
|
'other_databases': [{'name': 'other'}],
|
||||||
|
'postgresql_databases': [{'name': 'foo'}, {'name': 'bar'}],
|
||||||
|
},
|
||||||
|
archive_database_names={'postgresql_databases': ['other', 'foo', 'bar']},
|
||||||
|
hook_name='postgresql_databases',
|
||||||
|
database_name='bar',
|
||||||
|
) == ('postgresql_databases', {'name': 'bar'})
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_configured_database_matches_nothing_when_database_name_not_configured():
|
||||||
|
assert module.get_configured_database(
|
||||||
|
hooks={'postgresql_databases': [{'name': 'foo'}, {'name': 'bar'}]},
|
||||||
|
archive_database_names={'postgresql_databases': ['foo']},
|
||||||
|
hook_name='postgresql_databases',
|
||||||
|
database_name='quux',
|
||||||
|
) == (None, None)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_configured_database_matches_nothing_when_database_name_not_in_archive():
|
||||||
|
assert module.get_configured_database(
|
||||||
|
hooks={'postgresql_databases': [{'name': 'foo'}, {'name': 'bar'}]},
|
||||||
|
archive_database_names={'postgresql_databases': ['bar']},
|
||||||
|
hook_name='postgresql_databases',
|
||||||
|
database_name='foo',
|
||||||
|
) == (None, None)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_configured_database_matches_database_by_configuration_database_name():
|
||||||
|
assert module.get_configured_database(
|
||||||
|
hooks={'postgresql_databases': [{'name': 'all'}, {'name': 'bar'}]},
|
||||||
|
archive_database_names={'postgresql_databases': ['foo']},
|
||||||
|
hook_name='postgresql_databases',
|
||||||
|
database_name='foo',
|
||||||
|
configuration_database_name='all',
|
||||||
|
) == ('postgresql_databases', {'name': 'all'})
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_configured_database_with_unspecified_hook_matches_database_by_name():
|
||||||
|
assert module.get_configured_database(
|
||||||
|
hooks={
|
||||||
|
'other_databases': [{'name': 'other'}],
|
||||||
|
'postgresql_databases': [{'name': 'foo'}, {'name': 'bar'}],
|
||||||
|
},
|
||||||
|
archive_database_names={'postgresql_databases': ['other', 'foo', 'bar']},
|
||||||
|
hook_name=module.UNSPECIFIED_HOOK,
|
||||||
|
database_name='bar',
|
||||||
|
) == ('postgresql_databases', {'name': 'bar'})
|
||||||
|
|
||||||
|
|
||||||
|
def test_collect_archive_database_names_parses_archive_paths():
|
||||||
|
flexmock(module.borgmatic.hooks.dump).should_receive('make_database_dump_path').and_return('')
|
||||||
|
flexmock(module.borgmatic.borg.list).should_receive('capture_archive_listing').and_return(
|
||||||
|
[
|
||||||
|
'.borgmatic/postgresql_databases/localhost/foo',
|
||||||
|
'.borgmatic/postgresql_databases/localhost/bar',
|
||||||
|
'.borgmatic/mysql_databases/localhost/quux',
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
archive_database_names = module.collect_archive_database_names(
|
||||||
|
repository='repo',
|
||||||
|
archive='archive',
|
||||||
|
location={'borgmatic_source_directory': '.borgmatic'},
|
||||||
|
storage=flexmock(),
|
||||||
|
local_borg_version=flexmock(),
|
||||||
|
local_path=flexmock(),
|
||||||
|
remote_path=flexmock(),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert archive_database_names == {
|
||||||
|
'postgresql_databases': ['foo', 'bar'],
|
||||||
|
'mysql_databases': ['quux'],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_collect_archive_database_names_parses_directory_format_archive_paths():
|
||||||
|
flexmock(module.borgmatic.hooks.dump).should_receive('make_database_dump_path').and_return('')
|
||||||
|
flexmock(module.borgmatic.borg.list).should_receive('capture_archive_listing').and_return(
|
||||||
|
[
|
||||||
|
'.borgmatic/postgresql_databases/localhost/foo/table1',
|
||||||
|
'.borgmatic/postgresql_databases/localhost/foo/table2',
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
archive_database_names = module.collect_archive_database_names(
|
||||||
|
repository='repo',
|
||||||
|
archive='archive',
|
||||||
|
location={'borgmatic_source_directory': '.borgmatic'},
|
||||||
|
storage=flexmock(),
|
||||||
|
local_borg_version=flexmock(),
|
||||||
|
local_path=flexmock(),
|
||||||
|
remote_path=flexmock(),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert archive_database_names == {
|
||||||
|
'postgresql_databases': ['foo'],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_collect_archive_database_names_skips_bad_archive_paths():
|
||||||
|
flexmock(module.borgmatic.hooks.dump).should_receive('make_database_dump_path').and_return('')
|
||||||
|
flexmock(module.borgmatic.borg.list).should_receive('capture_archive_listing').and_return(
|
||||||
|
['.borgmatic/postgresql_databases/localhost/foo', '.borgmatic/invalid', 'invalid/as/well']
|
||||||
|
)
|
||||||
|
|
||||||
|
archive_database_names = module.collect_archive_database_names(
|
||||||
|
repository='repo',
|
||||||
|
archive='archive',
|
||||||
|
location={'borgmatic_source_directory': '.borgmatic'},
|
||||||
|
storage=flexmock(),
|
||||||
|
local_borg_version=flexmock(),
|
||||||
|
local_path=flexmock(),
|
||||||
|
remote_path=flexmock(),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert archive_database_names == {
|
||||||
|
'postgresql_databases': ['foo'],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_find_databases_to_restore_passes_through_requested_names_found_in_archive():
|
||||||
|
restore_names = module.find_databases_to_restore(
|
||||||
|
requested_database_names=['foo', 'bar'],
|
||||||
|
archive_database_names={'postresql_databases': ['foo', 'bar', 'baz']},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert restore_names == {module.UNSPECIFIED_HOOK: ['foo', 'bar']}
|
||||||
|
|
||||||
|
|
||||||
|
def test_find_databases_to_restore_raises_for_requested_names_missing_from_archive():
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
module.find_databases_to_restore(
|
||||||
|
requested_database_names=['foo', 'bar'],
|
||||||
|
archive_database_names={'postresql_databases': ['foo']},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_find_databases_to_restore_without_requested_names_finds_all_archive_databases():
|
||||||
|
archive_database_names = {'postresql_databases': ['foo', 'bar']}
|
||||||
|
|
||||||
|
restore_names = module.find_databases_to_restore(
|
||||||
|
requested_database_names=[], archive_database_names=archive_database_names,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert restore_names == archive_database_names
|
||||||
|
|
||||||
|
|
||||||
|
def test_find_databases_to_restore_with_all_in_requested_names_finds_all_archive_databases():
|
||||||
|
archive_database_names = {'postresql_databases': ['foo', 'bar']}
|
||||||
|
|
||||||
|
restore_names = module.find_databases_to_restore(
|
||||||
|
requested_database_names=['all'], archive_database_names=archive_database_names,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert restore_names == archive_database_names
|
||||||
|
|
||||||
|
|
||||||
|
def test_find_databases_to_restore_with_all_in_requested_names_plus_additional_requested_names_omits_duplicates():
|
||||||
|
archive_database_names = {'postresql_databases': ['foo', 'bar']}
|
||||||
|
|
||||||
|
restore_names = module.find_databases_to_restore(
|
||||||
|
requested_database_names=['all', 'foo', 'bar'],
|
||||||
|
archive_database_names=archive_database_names,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert restore_names == archive_database_names
|
||||||
|
|
||||||
|
|
||||||
|
def test_find_databases_to_restore_raises_for_all_in_requested_names_and_requested_named_missing_from_archives():
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
module.find_databases_to_restore(
|
||||||
|
requested_database_names=['all', 'foo', 'bar'],
|
||||||
|
archive_database_names={'postresql_databases': ['foo']},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_ensure_databases_found_with_all_databases_found_does_not_raise():
|
||||||
|
module.ensure_databases_found(
|
||||||
|
restore_names={'postgresql_databases': ['foo']},
|
||||||
|
remaining_restore_names={'postgresql_databases': ['bar']},
|
||||||
|
found_names=['foo', 'bar'],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_ensure_databases_found_with_no_databases_raises():
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
module.ensure_databases_found(
|
||||||
|
restore_names={'postgresql_databases': []}, remaining_restore_names={}, found_names=[],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_ensure_databases_found_with_missing_databases_raises():
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
module.ensure_databases_found(
|
||||||
|
restore_names={'postgresql_databases': ['foo']},
|
||||||
|
remaining_restore_names={'postgresql_databases': ['bar']},
|
||||||
|
found_names=['foo'],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_run_restore_restores_each_database():
|
||||||
|
restore_names = {
|
||||||
|
'postgresql_databases': ['foo', 'bar'],
|
||||||
|
}
|
||||||
|
|
||||||
|
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(True)
|
||||||
|
flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hooks_even_if_unconfigured')
|
||||||
|
flexmock(module.borgmatic.borg.rlist).should_receive('resolve_archive_name').and_return(
|
||||||
|
flexmock()
|
||||||
|
)
|
||||||
|
flexmock(module).should_receive('collect_archive_database_names').and_return(flexmock())
|
||||||
|
flexmock(module).should_receive('find_databases_to_restore').and_return(restore_names)
|
||||||
|
flexmock(module).should_receive('get_configured_database').and_return(
|
||||||
|
('postgresql_databases', {'name': 'foo'})
|
||||||
|
).and_return(('postgresql_databases', {'name': 'bar'}))
|
||||||
|
flexmock(module).should_receive('restore_single_database').with_args(
|
||||||
|
repository=object,
|
||||||
|
location=object,
|
||||||
|
storage=object,
|
||||||
|
hooks=object,
|
||||||
|
local_borg_version=object,
|
||||||
|
global_arguments=object,
|
||||||
|
local_path=object,
|
||||||
|
remote_path=object,
|
||||||
|
archive_name=object,
|
||||||
|
hook_name='postgresql_databases',
|
||||||
|
database={'name': 'foo'},
|
||||||
|
).once()
|
||||||
|
flexmock(module).should_receive('restore_single_database').with_args(
|
||||||
|
repository=object,
|
||||||
|
location=object,
|
||||||
|
storage=object,
|
||||||
|
hooks=object,
|
||||||
|
local_borg_version=object,
|
||||||
|
global_arguments=object,
|
||||||
|
local_path=object,
|
||||||
|
remote_path=object,
|
||||||
|
archive_name=object,
|
||||||
|
hook_name='postgresql_databases',
|
||||||
|
database={'name': 'bar'},
|
||||||
|
).once()
|
||||||
|
flexmock(module).should_receive('ensure_databases_found')
|
||||||
|
|
||||||
|
module.run_restore(
|
||||||
|
repository='repo',
|
||||||
|
location=flexmock(),
|
||||||
|
storage=flexmock(),
|
||||||
|
hooks=flexmock(),
|
||||||
|
local_borg_version=flexmock(),
|
||||||
|
restore_arguments=flexmock(repository='repo', archive='archive', databases=flexmock()),
|
||||||
|
global_arguments=flexmock(dry_run=False),
|
||||||
|
local_path=flexmock(),
|
||||||
|
remote_path=flexmock(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_run_restore_bails_for_non_matching_repository():
|
||||||
|
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(
|
||||||
|
False
|
||||||
|
)
|
||||||
|
flexmock(module.borgmatic.hooks.dispatch).should_receive(
|
||||||
|
'call_hooks_even_if_unconfigured'
|
||||||
|
).never()
|
||||||
|
flexmock(module).should_receive('restore_single_database').never()
|
||||||
|
|
||||||
|
module.run_restore(
|
||||||
|
repository='repo',
|
||||||
|
location=flexmock(),
|
||||||
|
storage=flexmock(),
|
||||||
|
hooks=flexmock(),
|
||||||
|
local_borg_version=flexmock(),
|
||||||
|
restore_arguments=flexmock(repository='repo', archive='archive', databases=flexmock()),
|
||||||
|
global_arguments=flexmock(dry_run=False),
|
||||||
|
local_path=flexmock(),
|
||||||
|
remote_path=flexmock(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_run_restore_restores_database_configured_with_all_name():
|
||||||
|
restore_names = {
|
||||||
|
'postgresql_databases': ['foo', 'bar'],
|
||||||
|
}
|
||||||
|
|
||||||
|
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(True)
|
||||||
|
flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hooks_even_if_unconfigured')
|
||||||
|
flexmock(module.borgmatic.borg.rlist).should_receive('resolve_archive_name').and_return(
|
||||||
|
flexmock()
|
||||||
|
)
|
||||||
|
flexmock(module).should_receive('collect_archive_database_names').and_return(flexmock())
|
||||||
|
flexmock(module).should_receive('find_databases_to_restore').and_return(restore_names)
|
||||||
|
flexmock(module).should_receive('get_configured_database').with_args(
|
||||||
|
hooks=object,
|
||||||
|
archive_database_names=object,
|
||||||
|
hook_name='postgresql_databases',
|
||||||
|
database_name='foo',
|
||||||
|
).and_return(('postgresql_databases', {'name': 'foo'}))
|
||||||
|
flexmock(module).should_receive('get_configured_database').with_args(
|
||||||
|
hooks=object,
|
||||||
|
archive_database_names=object,
|
||||||
|
hook_name='postgresql_databases',
|
||||||
|
database_name='bar',
|
||||||
|
).and_return((None, None))
|
||||||
|
flexmock(module).should_receive('get_configured_database').with_args(
|
||||||
|
hooks=object,
|
||||||
|
archive_database_names=object,
|
||||||
|
hook_name='postgresql_databases',
|
||||||
|
database_name='bar',
|
||||||
|
configuration_database_name='all',
|
||||||
|
).and_return(('postgresql_databases', {'name': 'bar'}))
|
||||||
|
flexmock(module).should_receive('restore_single_database').with_args(
|
||||||
|
repository=object,
|
||||||
|
location=object,
|
||||||
|
storage=object,
|
||||||
|
hooks=object,
|
||||||
|
local_borg_version=object,
|
||||||
|
global_arguments=object,
|
||||||
|
local_path=object,
|
||||||
|
remote_path=object,
|
||||||
|
archive_name=object,
|
||||||
|
hook_name='postgresql_databases',
|
||||||
|
database={'name': 'foo'},
|
||||||
|
).once()
|
||||||
|
flexmock(module).should_receive('restore_single_database').with_args(
|
||||||
|
repository=object,
|
||||||
|
location=object,
|
||||||
|
storage=object,
|
||||||
|
hooks=object,
|
||||||
|
local_borg_version=object,
|
||||||
|
global_arguments=object,
|
||||||
|
local_path=object,
|
||||||
|
remote_path=object,
|
||||||
|
archive_name=object,
|
||||||
|
hook_name='postgresql_databases',
|
||||||
|
database={'name': 'bar'},
|
||||||
|
).once()
|
||||||
|
flexmock(module).should_receive('ensure_databases_found')
|
||||||
|
|
||||||
|
module.run_restore(
|
||||||
|
repository='repo',
|
||||||
|
location=flexmock(),
|
||||||
|
storage=flexmock(),
|
||||||
|
hooks=flexmock(),
|
||||||
|
local_borg_version=flexmock(),
|
||||||
|
restore_arguments=flexmock(repository='repo', archive='archive', databases=flexmock()),
|
||||||
|
global_arguments=flexmock(dry_run=False),
|
||||||
|
local_path=flexmock(),
|
||||||
|
remote_path=flexmock(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_run_restore_skips_missing_database():
|
||||||
|
restore_names = {
|
||||||
|
'postgresql_databases': ['foo', 'bar'],
|
||||||
|
}
|
||||||
|
|
||||||
|
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(True)
|
||||||
|
flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hooks_even_if_unconfigured')
|
||||||
|
flexmock(module.borgmatic.borg.rlist).should_receive('resolve_archive_name').and_return(
|
||||||
|
flexmock()
|
||||||
|
)
|
||||||
|
flexmock(module).should_receive('collect_archive_database_names').and_return(flexmock())
|
||||||
|
flexmock(module).should_receive('find_databases_to_restore').and_return(restore_names)
|
||||||
|
flexmock(module).should_receive('get_configured_database').with_args(
|
||||||
|
hooks=object,
|
||||||
|
archive_database_names=object,
|
||||||
|
hook_name='postgresql_databases',
|
||||||
|
database_name='foo',
|
||||||
|
).and_return(('postgresql_databases', {'name': 'foo'}))
|
||||||
|
flexmock(module).should_receive('get_configured_database').with_args(
|
||||||
|
hooks=object,
|
||||||
|
archive_database_names=object,
|
||||||
|
hook_name='postgresql_databases',
|
||||||
|
database_name='bar',
|
||||||
|
).and_return((None, None))
|
||||||
|
flexmock(module).should_receive('get_configured_database').with_args(
|
||||||
|
hooks=object,
|
||||||
|
archive_database_names=object,
|
||||||
|
hook_name='postgresql_databases',
|
||||||
|
database_name='bar',
|
||||||
|
configuration_database_name='all',
|
||||||
|
).and_return((None, None))
|
||||||
|
flexmock(module).should_receive('restore_single_database').with_args(
|
||||||
|
repository=object,
|
||||||
|
location=object,
|
||||||
|
storage=object,
|
||||||
|
hooks=object,
|
||||||
|
local_borg_version=object,
|
||||||
|
global_arguments=object,
|
||||||
|
local_path=object,
|
||||||
|
remote_path=object,
|
||||||
|
archive_name=object,
|
||||||
|
hook_name='postgresql_databases',
|
||||||
|
database={'name': 'foo'},
|
||||||
|
).once()
|
||||||
|
flexmock(module).should_receive('restore_single_database').with_args(
|
||||||
|
repository=object,
|
||||||
|
location=object,
|
||||||
|
storage=object,
|
||||||
|
hooks=object,
|
||||||
|
local_borg_version=object,
|
||||||
|
global_arguments=object,
|
||||||
|
local_path=object,
|
||||||
|
remote_path=object,
|
||||||
|
archive_name=object,
|
||||||
|
hook_name='postgresql_databases',
|
||||||
|
database={'name': 'bar'},
|
||||||
|
).never()
|
||||||
|
flexmock(module).should_receive('ensure_databases_found')
|
||||||
|
|
||||||
|
module.run_restore(
|
||||||
|
repository='repo',
|
||||||
|
location=flexmock(),
|
||||||
|
storage=flexmock(),
|
||||||
|
hooks=flexmock(),
|
||||||
|
local_borg_version=flexmock(),
|
||||||
|
restore_arguments=flexmock(repository='repo', archive='archive', databases=flexmock()),
|
||||||
|
global_arguments=flexmock(dry_run=False),
|
||||||
|
local_path=flexmock(),
|
||||||
|
remote_path=flexmock(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_run_restore_restores_databases_from_different_hooks():
|
||||||
|
restore_names = {
|
||||||
|
'postgresql_databases': ['foo'],
|
||||||
|
'mysql_databases': ['bar'],
|
||||||
|
}
|
||||||
|
|
||||||
|
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(True)
|
||||||
|
flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hooks_even_if_unconfigured')
|
||||||
|
flexmock(module.borgmatic.borg.rlist).should_receive('resolve_archive_name').and_return(
|
||||||
|
flexmock()
|
||||||
|
)
|
||||||
|
flexmock(module).should_receive('collect_archive_database_names').and_return(flexmock())
|
||||||
|
flexmock(module).should_receive('find_databases_to_restore').and_return(restore_names)
|
||||||
|
flexmock(module).should_receive('get_configured_database').with_args(
|
||||||
|
hooks=object,
|
||||||
|
archive_database_names=object,
|
||||||
|
hook_name='postgresql_databases',
|
||||||
|
database_name='foo',
|
||||||
|
).and_return(('postgresql_databases', {'name': 'foo'}))
|
||||||
|
flexmock(module).should_receive('get_configured_database').with_args(
|
||||||
|
hooks=object,
|
||||||
|
archive_database_names=object,
|
||||||
|
hook_name='mysql_databases',
|
||||||
|
database_name='bar',
|
||||||
|
).and_return(('mysql_databases', {'name': 'bar'}))
|
||||||
|
flexmock(module).should_receive('restore_single_database').with_args(
|
||||||
|
repository=object,
|
||||||
|
location=object,
|
||||||
|
storage=object,
|
||||||
|
hooks=object,
|
||||||
|
local_borg_version=object,
|
||||||
|
global_arguments=object,
|
||||||
|
local_path=object,
|
||||||
|
remote_path=object,
|
||||||
|
archive_name=object,
|
||||||
|
hook_name='postgresql_databases',
|
||||||
|
database={'name': 'foo'},
|
||||||
|
).once()
|
||||||
|
flexmock(module).should_receive('restore_single_database').with_args(
|
||||||
|
repository=object,
|
||||||
|
location=object,
|
||||||
|
storage=object,
|
||||||
|
hooks=object,
|
||||||
|
local_borg_version=object,
|
||||||
|
global_arguments=object,
|
||||||
|
local_path=object,
|
||||||
|
remote_path=object,
|
||||||
|
archive_name=object,
|
||||||
|
hook_name='mysql_databases',
|
||||||
|
database={'name': 'bar'},
|
||||||
|
).once()
|
||||||
|
flexmock(module).should_receive('ensure_databases_found')
|
||||||
|
|
||||||
|
module.run_restore(
|
||||||
|
repository='repo',
|
||||||
|
location=flexmock(),
|
||||||
|
storage=flexmock(),
|
||||||
|
hooks=flexmock(),
|
||||||
|
local_borg_version=flexmock(),
|
||||||
|
restore_arguments=flexmock(repository='repo', archive='archive', databases=flexmock()),
|
||||||
|
global_arguments=flexmock(dry_run=False),
|
||||||
|
local_path=flexmock(),
|
||||||
|
remote_path=flexmock(),
|
||||||
|
)
|
|
@ -0,0 +1,21 @@
|
||||||
|
from flexmock import flexmock
|
||||||
|
|
||||||
|
from borgmatic.actions import rinfo as module
|
||||||
|
|
||||||
|
|
||||||
|
def test_run_rinfo_does_not_raise():
|
||||||
|
flexmock(module.logger).answer = lambda message: None
|
||||||
|
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(True)
|
||||||
|
flexmock(module.borgmatic.borg.rinfo).should_receive('display_repository_info')
|
||||||
|
rinfo_arguments = flexmock(repository=flexmock(), json=flexmock())
|
||||||
|
|
||||||
|
list(
|
||||||
|
module.run_rinfo(
|
||||||
|
repository='repo',
|
||||||
|
storage={},
|
||||||
|
local_borg_version=None,
|
||||||
|
rinfo_arguments=rinfo_arguments,
|
||||||
|
local_path=None,
|
||||||
|
remote_path=None,
|
||||||
|
)
|
||||||
|
)
|
|
@ -0,0 +1,21 @@
|
||||||
|
from flexmock import flexmock
|
||||||
|
|
||||||
|
from borgmatic.actions import rlist as module
|
||||||
|
|
||||||
|
|
||||||
|
def test_run_rlist_does_not_raise():
|
||||||
|
flexmock(module.logger).answer = lambda message: None
|
||||||
|
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(True)
|
||||||
|
flexmock(module.borgmatic.borg.rlist).should_receive('list_repository')
|
||||||
|
rlist_arguments = flexmock(repository=flexmock(), json=flexmock())
|
||||||
|
|
||||||
|
list(
|
||||||
|
module.run_rlist(
|
||||||
|
repository='repo',
|
||||||
|
storage={},
|
||||||
|
local_borg_version=None,
|
||||||
|
rlist_arguments=rlist_arguments,
|
||||||
|
local_path=None,
|
||||||
|
remote_path=None,
|
||||||
|
)
|
||||||
|
)
|
|
@ -0,0 +1,20 @@
|
||||||
|
from flexmock import flexmock
|
||||||
|
|
||||||
|
from borgmatic.actions import transfer as module
|
||||||
|
|
||||||
|
|
||||||
|
def test_run_transfer_does_not_raise():
|
||||||
|
flexmock(module.logger).answer = lambda message: None
|
||||||
|
flexmock(module.borgmatic.borg.transfer).should_receive('transfer_archives')
|
||||||
|
transfer_arguments = flexmock()
|
||||||
|
global_arguments = flexmock(monitoring_verbosity=1, dry_run=False)
|
||||||
|
|
||||||
|
module.run_transfer(
|
||||||
|
repository='repo',
|
||||||
|
storage={},
|
||||||
|
local_borg_version=None,
|
||||||
|
transfer_arguments=transfer_arguments,
|
||||||
|
global_arguments=global_arguments,
|
||||||
|
local_path=None,
|
||||||
|
remote_path=None,
|
||||||
|
)
|
|
@ -8,12 +8,14 @@ from ..test_verbosity import insert_logging_mock
|
||||||
|
|
||||||
|
|
||||||
def test_run_arbitrary_borg_calls_borg_with_parameters():
|
def test_run_arbitrary_borg_calls_borg_with_parameters():
|
||||||
|
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||||
|
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||||
flexmock(module.flags).should_receive('make_flags').and_return(())
|
flexmock(module.flags).should_receive('make_flags').and_return(())
|
||||||
flexmock(module.environment).should_receive('make_environment')
|
flexmock(module.environment).should_receive('make_environment')
|
||||||
flexmock(module).should_receive('execute_command').with_args(
|
flexmock(module).should_receive('execute_command').with_args(
|
||||||
('borg', 'break-lock', 'repo'),
|
('borg', 'break-lock', 'repo'),
|
||||||
output_log_level=logging.WARNING,
|
output_log_level=module.borgmatic.logger.ANSWER,
|
||||||
borg_local_path='borg',
|
borg_local_path='borg',
|
||||||
extra_environment=None,
|
extra_environment=None,
|
||||||
)
|
)
|
||||||
|
@ -24,12 +26,14 @@ def test_run_arbitrary_borg_calls_borg_with_parameters():
|
||||||
|
|
||||||
|
|
||||||
def test_run_arbitrary_borg_with_log_info_calls_borg_with_info_parameter():
|
def test_run_arbitrary_borg_with_log_info_calls_borg_with_info_parameter():
|
||||||
|
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||||
|
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||||
flexmock(module.flags).should_receive('make_flags').and_return(())
|
flexmock(module.flags).should_receive('make_flags').and_return(())
|
||||||
flexmock(module.environment).should_receive('make_environment')
|
flexmock(module.environment).should_receive('make_environment')
|
||||||
flexmock(module).should_receive('execute_command').with_args(
|
flexmock(module).should_receive('execute_command').with_args(
|
||||||
('borg', 'break-lock', 'repo', '--info'),
|
('borg', 'break-lock', 'repo', '--info'),
|
||||||
output_log_level=logging.WARNING,
|
output_log_level=module.borgmatic.logger.ANSWER,
|
||||||
borg_local_path='borg',
|
borg_local_path='borg',
|
||||||
extra_environment=None,
|
extra_environment=None,
|
||||||
)
|
)
|
||||||
|
@ -41,12 +45,14 @@ def test_run_arbitrary_borg_with_log_info_calls_borg_with_info_parameter():
|
||||||
|
|
||||||
|
|
||||||
def test_run_arbitrary_borg_with_log_debug_calls_borg_with_debug_parameter():
|
def test_run_arbitrary_borg_with_log_debug_calls_borg_with_debug_parameter():
|
||||||
|
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||||
|
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||||
flexmock(module.flags).should_receive('make_flags').and_return(())
|
flexmock(module.flags).should_receive('make_flags').and_return(())
|
||||||
flexmock(module.environment).should_receive('make_environment')
|
flexmock(module.environment).should_receive('make_environment')
|
||||||
flexmock(module).should_receive('execute_command').with_args(
|
flexmock(module).should_receive('execute_command').with_args(
|
||||||
('borg', 'break-lock', 'repo', '--debug', '--show-rc'),
|
('borg', 'break-lock', 'repo', '--debug', '--show-rc'),
|
||||||
output_log_level=logging.WARNING,
|
output_log_level=module.borgmatic.logger.ANSWER,
|
||||||
borg_local_path='borg',
|
borg_local_path='borg',
|
||||||
extra_environment=None,
|
extra_environment=None,
|
||||||
)
|
)
|
||||||
|
@ -58,6 +64,8 @@ def test_run_arbitrary_borg_with_log_debug_calls_borg_with_debug_parameter():
|
||||||
|
|
||||||
|
|
||||||
def test_run_arbitrary_borg_with_lock_wait_calls_borg_with_lock_wait_parameters():
|
def test_run_arbitrary_borg_with_lock_wait_calls_borg_with_lock_wait_parameters():
|
||||||
|
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||||
|
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||||
storage_config = {'lock_wait': 5}
|
storage_config = {'lock_wait': 5}
|
||||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||||
flexmock(module.flags).should_receive('make_flags').and_return(()).and_return(
|
flexmock(module.flags).should_receive('make_flags').and_return(()).and_return(
|
||||||
|
@ -66,7 +74,7 @@ def test_run_arbitrary_borg_with_lock_wait_calls_borg_with_lock_wait_parameters(
|
||||||
flexmock(module.environment).should_receive('make_environment')
|
flexmock(module.environment).should_receive('make_environment')
|
||||||
flexmock(module).should_receive('execute_command').with_args(
|
flexmock(module).should_receive('execute_command').with_args(
|
||||||
('borg', 'break-lock', 'repo', '--lock-wait', '5'),
|
('borg', 'break-lock', 'repo', '--lock-wait', '5'),
|
||||||
output_log_level=logging.WARNING,
|
output_log_level=module.borgmatic.logger.ANSWER,
|
||||||
borg_local_path='borg',
|
borg_local_path='borg',
|
||||||
extra_environment=None,
|
extra_environment=None,
|
||||||
)
|
)
|
||||||
|
@ -80,6 +88,8 @@ def test_run_arbitrary_borg_with_lock_wait_calls_borg_with_lock_wait_parameters(
|
||||||
|
|
||||||
|
|
||||||
def test_run_arbitrary_borg_with_archive_calls_borg_with_archive_parameter():
|
def test_run_arbitrary_borg_with_archive_calls_borg_with_archive_parameter():
|
||||||
|
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||||
|
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||||
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
|
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
|
||||||
('repo::archive',)
|
('repo::archive',)
|
||||||
)
|
)
|
||||||
|
@ -87,7 +97,7 @@ def test_run_arbitrary_borg_with_archive_calls_borg_with_archive_parameter():
|
||||||
flexmock(module.environment).should_receive('make_environment')
|
flexmock(module.environment).should_receive('make_environment')
|
||||||
flexmock(module).should_receive('execute_command').with_args(
|
flexmock(module).should_receive('execute_command').with_args(
|
||||||
('borg', 'break-lock', 'repo::archive'),
|
('borg', 'break-lock', 'repo::archive'),
|
||||||
output_log_level=logging.WARNING,
|
output_log_level=module.borgmatic.logger.ANSWER,
|
||||||
borg_local_path='borg',
|
borg_local_path='borg',
|
||||||
extra_environment=None,
|
extra_environment=None,
|
||||||
)
|
)
|
||||||
|
@ -102,12 +112,14 @@ def test_run_arbitrary_borg_with_archive_calls_borg_with_archive_parameter():
|
||||||
|
|
||||||
|
|
||||||
def test_run_arbitrary_borg_with_local_path_calls_borg_via_local_path():
|
def test_run_arbitrary_borg_with_local_path_calls_borg_via_local_path():
|
||||||
|
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||||
|
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||||
flexmock(module.flags).should_receive('make_flags').and_return(())
|
flexmock(module.flags).should_receive('make_flags').and_return(())
|
||||||
flexmock(module.environment).should_receive('make_environment')
|
flexmock(module.environment).should_receive('make_environment')
|
||||||
flexmock(module).should_receive('execute_command').with_args(
|
flexmock(module).should_receive('execute_command').with_args(
|
||||||
('borg1', 'break-lock', 'repo'),
|
('borg1', 'break-lock', 'repo'),
|
||||||
output_log_level=logging.WARNING,
|
output_log_level=module.borgmatic.logger.ANSWER,
|
||||||
borg_local_path='borg1',
|
borg_local_path='borg1',
|
||||||
extra_environment=None,
|
extra_environment=None,
|
||||||
)
|
)
|
||||||
|
@ -122,6 +134,8 @@ def test_run_arbitrary_borg_with_local_path_calls_borg_via_local_path():
|
||||||
|
|
||||||
|
|
||||||
def test_run_arbitrary_borg_with_remote_path_calls_borg_with_remote_path_parameters():
|
def test_run_arbitrary_borg_with_remote_path_calls_borg_with_remote_path_parameters():
|
||||||
|
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||||
|
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||||
flexmock(module.flags).should_receive('make_flags').and_return(
|
flexmock(module.flags).should_receive('make_flags').and_return(
|
||||||
('--remote-path', 'borg1')
|
('--remote-path', 'borg1')
|
||||||
|
@ -129,7 +143,7 @@ def test_run_arbitrary_borg_with_remote_path_calls_borg_with_remote_path_paramet
|
||||||
flexmock(module.environment).should_receive('make_environment')
|
flexmock(module.environment).should_receive('make_environment')
|
||||||
flexmock(module).should_receive('execute_command').with_args(
|
flexmock(module).should_receive('execute_command').with_args(
|
||||||
('borg', 'break-lock', 'repo', '--remote-path', 'borg1'),
|
('borg', 'break-lock', 'repo', '--remote-path', 'borg1'),
|
||||||
output_log_level=logging.WARNING,
|
output_log_level=module.borgmatic.logger.ANSWER,
|
||||||
borg_local_path='borg',
|
borg_local_path='borg',
|
||||||
extra_environment=None,
|
extra_environment=None,
|
||||||
)
|
)
|
||||||
|
@ -144,12 +158,14 @@ def test_run_arbitrary_borg_with_remote_path_calls_borg_with_remote_path_paramet
|
||||||
|
|
||||||
|
|
||||||
def test_run_arbitrary_borg_passes_borg_specific_parameters_to_borg():
|
def test_run_arbitrary_borg_passes_borg_specific_parameters_to_borg():
|
||||||
|
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||||
|
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||||
flexmock(module.flags).should_receive('make_flags').and_return(())
|
flexmock(module.flags).should_receive('make_flags').and_return(())
|
||||||
flexmock(module.environment).should_receive('make_environment')
|
flexmock(module.environment).should_receive('make_environment')
|
||||||
flexmock(module).should_receive('execute_command').with_args(
|
flexmock(module).should_receive('execute_command').with_args(
|
||||||
('borg', 'list', 'repo', '--progress'),
|
('borg', 'list', 'repo', '--progress'),
|
||||||
output_log_level=logging.WARNING,
|
output_log_level=module.borgmatic.logger.ANSWER,
|
||||||
borg_local_path='borg',
|
borg_local_path='borg',
|
||||||
extra_environment=None,
|
extra_environment=None,
|
||||||
)
|
)
|
||||||
|
@ -163,12 +179,14 @@ def test_run_arbitrary_borg_passes_borg_specific_parameters_to_borg():
|
||||||
|
|
||||||
|
|
||||||
def test_run_arbitrary_borg_omits_dash_dash_in_parameters_passed_to_borg():
|
def test_run_arbitrary_borg_omits_dash_dash_in_parameters_passed_to_borg():
|
||||||
|
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||||
|
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||||
flexmock(module.flags).should_receive('make_flags').and_return(())
|
flexmock(module.flags).should_receive('make_flags').and_return(())
|
||||||
flexmock(module.environment).should_receive('make_environment')
|
flexmock(module.environment).should_receive('make_environment')
|
||||||
flexmock(module).should_receive('execute_command').with_args(
|
flexmock(module).should_receive('execute_command').with_args(
|
||||||
('borg', 'break-lock', 'repo'),
|
('borg', 'break-lock', 'repo'),
|
||||||
output_log_level=logging.WARNING,
|
output_log_level=module.borgmatic.logger.ANSWER,
|
||||||
borg_local_path='borg',
|
borg_local_path='borg',
|
||||||
extra_environment=None,
|
extra_environment=None,
|
||||||
)
|
)
|
||||||
|
@ -182,11 +200,16 @@ def test_run_arbitrary_borg_omits_dash_dash_in_parameters_passed_to_borg():
|
||||||
|
|
||||||
|
|
||||||
def test_run_arbitrary_borg_without_borg_specific_parameters_does_not_raise():
|
def test_run_arbitrary_borg_without_borg_specific_parameters_does_not_raise():
|
||||||
|
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||||
|
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||||
flexmock(module.flags).should_receive('make_repository_flags').never()
|
flexmock(module.flags).should_receive('make_repository_flags').never()
|
||||||
flexmock(module.flags).should_receive('make_flags').and_return(())
|
flexmock(module.flags).should_receive('make_flags').and_return(())
|
||||||
flexmock(module.environment).should_receive('make_environment')
|
flexmock(module.environment).should_receive('make_environment')
|
||||||
flexmock(module).should_receive('execute_command').with_args(
|
flexmock(module).should_receive('execute_command').with_args(
|
||||||
('borg',), output_log_level=logging.WARNING, borg_local_path='borg', extra_environment=None,
|
('borg',),
|
||||||
|
output_log_level=module.borgmatic.logger.ANSWER,
|
||||||
|
borg_local_path='borg',
|
||||||
|
extra_environment=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
module.run_arbitrary_borg(
|
module.run_arbitrary_borg(
|
||||||
|
@ -195,12 +218,14 @@ def test_run_arbitrary_borg_without_borg_specific_parameters_does_not_raise():
|
||||||
|
|
||||||
|
|
||||||
def test_run_arbitrary_borg_passes_key_sub_command_to_borg_before_repository():
|
def test_run_arbitrary_borg_passes_key_sub_command_to_borg_before_repository():
|
||||||
|
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||||
|
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||||
flexmock(module.flags).should_receive('make_flags').and_return(())
|
flexmock(module.flags).should_receive('make_flags').and_return(())
|
||||||
flexmock(module.environment).should_receive('make_environment')
|
flexmock(module.environment).should_receive('make_environment')
|
||||||
flexmock(module).should_receive('execute_command').with_args(
|
flexmock(module).should_receive('execute_command').with_args(
|
||||||
('borg', 'key', 'export', 'repo'),
|
('borg', 'key', 'export', 'repo'),
|
||||||
output_log_level=logging.WARNING,
|
output_log_level=module.borgmatic.logger.ANSWER,
|
||||||
borg_local_path='borg',
|
borg_local_path='borg',
|
||||||
extra_environment=None,
|
extra_environment=None,
|
||||||
)
|
)
|
||||||
|
@ -211,12 +236,14 @@ def test_run_arbitrary_borg_passes_key_sub_command_to_borg_before_repository():
|
||||||
|
|
||||||
|
|
||||||
def test_run_arbitrary_borg_passes_debug_sub_command_to_borg_before_repository():
|
def test_run_arbitrary_borg_passes_debug_sub_command_to_borg_before_repository():
|
||||||
|
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||||
|
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||||
flexmock(module.flags).should_receive('make_flags').and_return(())
|
flexmock(module.flags).should_receive('make_flags').and_return(())
|
||||||
flexmock(module.environment).should_receive('make_environment')
|
flexmock(module.environment).should_receive('make_environment')
|
||||||
flexmock(module).should_receive('execute_command').with_args(
|
flexmock(module).should_receive('execute_command').with_args(
|
||||||
('borg', 'debug', 'dump-manifest', 'repo', 'path'),
|
('borg', 'debug', 'dump-manifest', 'repo', 'path'),
|
||||||
output_log_level=logging.WARNING,
|
output_log_level=module.borgmatic.logger.ANSWER,
|
||||||
borg_local_path='borg',
|
borg_local_path='borg',
|
||||||
extra_environment=None,
|
extra_environment=None,
|
||||||
)
|
)
|
||||||
|
@ -230,12 +257,14 @@ def test_run_arbitrary_borg_passes_debug_sub_command_to_borg_before_repository()
|
||||||
|
|
||||||
|
|
||||||
def test_run_arbitrary_borg_with_debug_info_command_does_not_pass_borg_repository():
|
def test_run_arbitrary_borg_with_debug_info_command_does_not_pass_borg_repository():
|
||||||
|
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||||
|
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||||
flexmock(module.flags).should_receive('make_repository_flags').never()
|
flexmock(module.flags).should_receive('make_repository_flags').never()
|
||||||
flexmock(module.flags).should_receive('make_flags').and_return(())
|
flexmock(module.flags).should_receive('make_flags').and_return(())
|
||||||
flexmock(module.environment).should_receive('make_environment')
|
flexmock(module.environment).should_receive('make_environment')
|
||||||
flexmock(module).should_receive('execute_command').with_args(
|
flexmock(module).should_receive('execute_command').with_args(
|
||||||
('borg', 'debug', 'info'),
|
('borg', 'debug', 'info'),
|
||||||
output_log_level=logging.WARNING,
|
output_log_level=module.borgmatic.logger.ANSWER,
|
||||||
borg_local_path='borg',
|
borg_local_path='borg',
|
||||||
extra_environment=None,
|
extra_environment=None,
|
||||||
)
|
)
|
||||||
|
@ -246,12 +275,14 @@ def test_run_arbitrary_borg_with_debug_info_command_does_not_pass_borg_repositor
|
||||||
|
|
||||||
|
|
||||||
def test_run_arbitrary_borg_with_debug_convert_profile_command_does_not_pass_borg_repository():
|
def test_run_arbitrary_borg_with_debug_convert_profile_command_does_not_pass_borg_repository():
|
||||||
|
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||||
|
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||||
flexmock(module.flags).should_receive('make_repository_flags').never()
|
flexmock(module.flags).should_receive('make_repository_flags').never()
|
||||||
flexmock(module.flags).should_receive('make_flags').and_return(())
|
flexmock(module.flags).should_receive('make_flags').and_return(())
|
||||||
flexmock(module.environment).should_receive('make_environment')
|
flexmock(module.environment).should_receive('make_environment')
|
||||||
flexmock(module).should_receive('execute_command').with_args(
|
flexmock(module).should_receive('execute_command').with_args(
|
||||||
('borg', 'debug', 'convert-profile', 'in', 'out'),
|
('borg', 'debug', 'convert-profile', 'in', 'out'),
|
||||||
output_log_level=logging.WARNING,
|
output_log_level=module.borgmatic.logger.ANSWER,
|
||||||
borg_local_path='borg',
|
borg_local_path='borg',
|
||||||
extra_environment=None,
|
extra_environment=None,
|
||||||
)
|
)
|
||||||
|
|
|
@ -265,6 +265,14 @@ def test_make_check_flags_with_archives_check_and_last_includes_last_flag():
|
||||||
assert flags == ('--archives-only', '--last', '3')
|
assert flags == ('--archives-only', '--last', '3')
|
||||||
|
|
||||||
|
|
||||||
|
def test_make_check_flags_with_data_check_and_last_includes_last_flag():
|
||||||
|
flexmock(module.feature).should_receive('available').and_return(True)
|
||||||
|
|
||||||
|
flags = module.make_check_flags('1.2.3', ('data',), check_last=3)
|
||||||
|
|
||||||
|
assert flags == ('--archives-only', '--last', '3', '--verify-data')
|
||||||
|
|
||||||
|
|
||||||
def test_make_check_flags_with_repository_check_and_last_omits_last_flag():
|
def test_make_check_flags_with_repository_check_and_last_omits_last_flag():
|
||||||
flexmock(module.feature).should_receive('available').and_return(True)
|
flexmock(module.feature).should_receive('available').and_return(True)
|
||||||
|
|
||||||
|
@ -289,6 +297,14 @@ def test_make_check_flags_with_archives_check_and_prefix_includes_match_archives
|
||||||
assert flags == ('--archives-only', '--match-archives', 'sh:foo-*')
|
assert flags == ('--archives-only', '--match-archives', 'sh:foo-*')
|
||||||
|
|
||||||
|
|
||||||
|
def test_make_check_flags_with_data_check_and_prefix_includes_match_archives_flag():
|
||||||
|
flexmock(module.feature).should_receive('available').and_return(True)
|
||||||
|
|
||||||
|
flags = module.make_check_flags('1.2.3', ('data',), prefix='foo-')
|
||||||
|
|
||||||
|
assert flags == ('--archives-only', '--match-archives', 'sh:foo-*', '--verify-data')
|
||||||
|
|
||||||
|
|
||||||
def test_make_check_flags_with_archives_check_and_empty_prefix_omits_match_archives_flag():
|
def test_make_check_flags_with_archives_check_and_empty_prefix_omits_match_archives_flag():
|
||||||
flexmock(module.feature).should_receive('available').and_return(True)
|
flexmock(module.feature).should_receive('available').and_return(True)
|
||||||
|
|
||||||
|
|
|
@ -284,6 +284,48 @@ def test_make_exclude_flags_is_empty_when_config_has_no_excludes():
|
||||||
assert exclude_flags == ()
|
assert exclude_flags == ()
|
||||||
|
|
||||||
|
|
||||||
|
def test_make_list_filter_flags_with_debug_and_feature_available_includes_plus_and_minus():
|
||||||
|
flexmock(module.logger).should_receive('isEnabledFor').and_return(True)
|
||||||
|
flexmock(module.feature).should_receive('available').and_return(True)
|
||||||
|
|
||||||
|
assert module.make_list_filter_flags(local_borg_version=flexmock(), dry_run=False) == 'AME+-'
|
||||||
|
|
||||||
|
|
||||||
|
def test_make_list_filter_flags_with_info_and_feature_available_omits_plus_and_minus():
|
||||||
|
flexmock(module.logger).should_receive('isEnabledFor').and_return(False)
|
||||||
|
flexmock(module.feature).should_receive('available').and_return(True)
|
||||||
|
|
||||||
|
assert module.make_list_filter_flags(local_borg_version=flexmock(), dry_run=False) == 'AME'
|
||||||
|
|
||||||
|
|
||||||
|
def test_make_list_filter_flags_with_debug_and_feature_available_and_dry_run_includes_plus_and_minus():
|
||||||
|
flexmock(module.logger).should_receive('isEnabledFor').and_return(True)
|
||||||
|
flexmock(module.feature).should_receive('available').and_return(True)
|
||||||
|
|
||||||
|
assert module.make_list_filter_flags(local_borg_version=flexmock(), dry_run=True) == 'AME+-'
|
||||||
|
|
||||||
|
|
||||||
|
def test_make_list_filter_flags_with_info_and_feature_available_and_dry_run_includes_plus_and_minus():
|
||||||
|
flexmock(module.logger).should_receive('isEnabledFor').and_return(False)
|
||||||
|
flexmock(module.feature).should_receive('available').and_return(True)
|
||||||
|
|
||||||
|
assert module.make_list_filter_flags(local_borg_version=flexmock(), dry_run=True) == 'AME+-'
|
||||||
|
|
||||||
|
|
||||||
|
def test_make_list_filter_flags_with_debug_and_feature_not_available_includes_x():
|
||||||
|
flexmock(module.logger).should_receive('isEnabledFor').and_return(True)
|
||||||
|
flexmock(module.feature).should_receive('available').and_return(False)
|
||||||
|
|
||||||
|
assert module.make_list_filter_flags(local_borg_version=flexmock(), dry_run=False) == 'AMEx-'
|
||||||
|
|
||||||
|
|
||||||
|
def test_make_list_filter_flags_with_info_and_feature_not_available_omits_x():
|
||||||
|
flexmock(module.logger).should_receive('isEnabledFor').and_return(False)
|
||||||
|
flexmock(module.feature).should_receive('available').and_return(False)
|
||||||
|
|
||||||
|
assert module.make_list_filter_flags(local_borg_version=flexmock(), dry_run=False) == 'AME-'
|
||||||
|
|
||||||
|
|
||||||
def test_collect_borgmatic_source_directories_set_when_directory_exists():
|
def test_collect_borgmatic_source_directories_set_when_directory_exists():
|
||||||
flexmock(module.os.path).should_receive('exists').and_return(True)
|
flexmock(module.os.path).should_receive('exists').and_return(True)
|
||||||
flexmock(module.os.path).should_receive('expanduser')
|
flexmock(module.os.path).should_receive('expanduser')
|
||||||
|
@ -338,6 +380,12 @@ def test_special_file_looks_at_file_type(character_device, block_device, fifo, e
|
||||||
assert module.special_file('/dev/special') == expected_result
|
assert module.special_file('/dev/special') == expected_result
|
||||||
|
|
||||||
|
|
||||||
|
def test_special_file_treats_broken_symlink_as_non_special():
|
||||||
|
flexmock(module.os).should_receive('stat').and_raise(FileNotFoundError)
|
||||||
|
|
||||||
|
assert module.special_file('/broken/symlink') is False
|
||||||
|
|
||||||
|
|
||||||
def test_any_parent_directories_treats_parents_as_match():
|
def test_any_parent_directories_treats_parents_as_match():
|
||||||
module.any_parent_directories('/foo/bar.txt', ('/foo', '/etc'))
|
module.any_parent_directories('/foo/bar.txt', ('/foo', '/etc'))
|
||||||
|
|
||||||
|
@ -351,8 +399,8 @@ def test_any_parent_directories_treats_unrelated_paths_as_non_match():
|
||||||
|
|
||||||
|
|
||||||
def test_collect_special_file_paths_parses_special_files_from_borg_dry_run_file_list():
|
def test_collect_special_file_paths_parses_special_files_from_borg_dry_run_file_list():
|
||||||
flexmock(module).should_receive('execute_command').and_return(
|
flexmock(module).should_receive('execute_command_and_capture_output').and_return(
|
||||||
'Processing files ...\n- /foo\n- /bar\n- /baz'
|
'Processing files ...\n- /foo\n+ /bar\n- /baz'
|
||||||
)
|
)
|
||||||
flexmock(module).should_receive('special_file').and_return(True)
|
flexmock(module).should_receive('special_file').and_return(True)
|
||||||
flexmock(module).should_receive('any_parent_directories').and_return(False)
|
flexmock(module).should_receive('any_parent_directories').and_return(False)
|
||||||
|
@ -367,7 +415,9 @@ def test_collect_special_file_paths_parses_special_files_from_borg_dry_run_file_
|
||||||
|
|
||||||
|
|
||||||
def test_collect_special_file_paths_excludes_requested_directories():
|
def test_collect_special_file_paths_excludes_requested_directories():
|
||||||
flexmock(module).should_receive('execute_command').and_return('- /foo\n- /bar\n- /baz')
|
flexmock(module).should_receive('execute_command_and_capture_output').and_return(
|
||||||
|
'+ /foo\n- /bar\n- /baz'
|
||||||
|
)
|
||||||
flexmock(module).should_receive('special_file').and_return(True)
|
flexmock(module).should_receive('special_file').and_return(True)
|
||||||
flexmock(module).should_receive('any_parent_directories').and_return(False).and_return(
|
flexmock(module).should_receive('any_parent_directories').and_return(False).and_return(
|
||||||
True
|
True
|
||||||
|
@ -383,7 +433,9 @@ def test_collect_special_file_paths_excludes_requested_directories():
|
||||||
|
|
||||||
|
|
||||||
def test_collect_special_file_paths_excludes_non_special_files():
|
def test_collect_special_file_paths_excludes_non_special_files():
|
||||||
flexmock(module).should_receive('execute_command').and_return('- /foo\n- /bar\n- /baz')
|
flexmock(module).should_receive('execute_command_and_capture_output').and_return(
|
||||||
|
'+ /foo\n+ /bar\n+ /baz'
|
||||||
|
)
|
||||||
flexmock(module).should_receive('special_file').and_return(True).and_return(False).and_return(
|
flexmock(module).should_receive('special_file').and_return(True).and_return(False).and_return(
|
||||||
True
|
True
|
||||||
)
|
)
|
||||||
|
@ -403,6 +455,8 @@ REPO_ARCHIVE_WITH_PATHS = (f'repo::{DEFAULT_ARCHIVE_NAME}', 'foo', 'bar')
|
||||||
|
|
||||||
|
|
||||||
def test_create_archive_calls_borg_with_parameters():
|
def test_create_archive_calls_borg_with_parameters():
|
||||||
|
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||||
|
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||||
flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
|
flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
|
||||||
flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
|
flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
|
||||||
flexmock(module).should_receive('map_directories_to_devices').and_return({})
|
flexmock(module).should_receive('map_directories_to_devices').and_return({})
|
||||||
|
@ -411,6 +465,7 @@ def test_create_archive_calls_borg_with_parameters():
|
||||||
flexmock(module.os.path).should_receive('expanduser').and_raise(TypeError)
|
flexmock(module.os.path).should_receive('expanduser').and_raise(TypeError)
|
||||||
flexmock(module).should_receive('expand_home_directories').and_return(())
|
flexmock(module).should_receive('expand_home_directories').and_return(())
|
||||||
flexmock(module).should_receive('write_pattern_file').and_return(None)
|
flexmock(module).should_receive('write_pattern_file').and_return(None)
|
||||||
|
flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
|
||||||
flexmock(module.feature).should_receive('available').and_return(True)
|
flexmock(module.feature).should_receive('available').and_return(True)
|
||||||
flexmock(module).should_receive('ensure_files_readable')
|
flexmock(module).should_receive('ensure_files_readable')
|
||||||
flexmock(module).should_receive('make_pattern_flags').and_return(())
|
flexmock(module).should_receive('make_pattern_flags').and_return(())
|
||||||
|
@ -442,6 +497,8 @@ def test_create_archive_calls_borg_with_parameters():
|
||||||
|
|
||||||
|
|
||||||
def test_create_archive_calls_borg_with_environment():
|
def test_create_archive_calls_borg_with_environment():
|
||||||
|
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||||
|
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||||
flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
|
flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
|
||||||
flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
|
flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
|
||||||
flexmock(module).should_receive('map_directories_to_devices').and_return({})
|
flexmock(module).should_receive('map_directories_to_devices').and_return({})
|
||||||
|
@ -450,6 +507,7 @@ def test_create_archive_calls_borg_with_environment():
|
||||||
flexmock(module.os.path).should_receive('expanduser').and_raise(TypeError)
|
flexmock(module.os.path).should_receive('expanduser').and_raise(TypeError)
|
||||||
flexmock(module).should_receive('expand_home_directories').and_return(())
|
flexmock(module).should_receive('expand_home_directories').and_return(())
|
||||||
flexmock(module).should_receive('write_pattern_file').and_return(None)
|
flexmock(module).should_receive('write_pattern_file').and_return(None)
|
||||||
|
flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
|
||||||
flexmock(module.feature).should_receive('available').and_return(True)
|
flexmock(module.feature).should_receive('available').and_return(True)
|
||||||
flexmock(module).should_receive('ensure_files_readable')
|
flexmock(module).should_receive('ensure_files_readable')
|
||||||
flexmock(module).should_receive('make_pattern_flags').and_return(())
|
flexmock(module).should_receive('make_pattern_flags').and_return(())
|
||||||
|
@ -482,6 +540,8 @@ def test_create_archive_calls_borg_with_environment():
|
||||||
|
|
||||||
|
|
||||||
def test_create_archive_with_patterns_calls_borg_with_patterns_including_converted_source_directories():
|
def test_create_archive_with_patterns_calls_borg_with_patterns_including_converted_source_directories():
|
||||||
|
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||||
|
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||||
pattern_flags = ('--patterns-from', 'patterns')
|
pattern_flags = ('--patterns-from', 'patterns')
|
||||||
flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
|
flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
|
||||||
flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
|
flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
|
||||||
|
@ -493,6 +553,7 @@ def test_create_archive_with_patterns_calls_borg_with_patterns_including_convert
|
||||||
flexmock(module).should_receive('write_pattern_file').and_return(
|
flexmock(module).should_receive('write_pattern_file').and_return(
|
||||||
flexmock(name='/tmp/patterns')
|
flexmock(name='/tmp/patterns')
|
||||||
).and_return(None)
|
).and_return(None)
|
||||||
|
flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
|
||||||
flexmock(module.feature).should_receive('available').and_return(True)
|
flexmock(module.feature).should_receive('available').and_return(True)
|
||||||
flexmock(module).should_receive('ensure_files_readable')
|
flexmock(module).should_receive('ensure_files_readable')
|
||||||
flexmock(module).should_receive('make_pattern_flags').and_return(pattern_flags)
|
flexmock(module).should_receive('make_pattern_flags').and_return(pattern_flags)
|
||||||
|
@ -524,6 +585,8 @@ def test_create_archive_with_patterns_calls_borg_with_patterns_including_convert
|
||||||
|
|
||||||
|
|
||||||
def test_create_archive_with_exclude_patterns_calls_borg_with_excludes():
|
def test_create_archive_with_exclude_patterns_calls_borg_with_excludes():
|
||||||
|
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||||
|
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||||
exclude_flags = ('--exclude-from', 'excludes')
|
exclude_flags = ('--exclude-from', 'excludes')
|
||||||
flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
|
flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
|
||||||
flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
|
flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
|
||||||
|
@ -535,6 +598,7 @@ def test_create_archive_with_exclude_patterns_calls_borg_with_excludes():
|
||||||
flexmock(module).should_receive('write_pattern_file').and_return(None).and_return(
|
flexmock(module).should_receive('write_pattern_file').and_return(None).and_return(
|
||||||
flexmock(name='/tmp/excludes')
|
flexmock(name='/tmp/excludes')
|
||||||
)
|
)
|
||||||
|
flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
|
||||||
flexmock(module.feature).should_receive('available').and_return(True)
|
flexmock(module.feature).should_receive('available').and_return(True)
|
||||||
flexmock(module).should_receive('ensure_files_readable')
|
flexmock(module).should_receive('ensure_files_readable')
|
||||||
flexmock(module).should_receive('make_pattern_flags').and_return(())
|
flexmock(module).should_receive('make_pattern_flags').and_return(())
|
||||||
|
@ -566,6 +630,8 @@ def test_create_archive_with_exclude_patterns_calls_borg_with_excludes():
|
||||||
|
|
||||||
|
|
||||||
def test_create_archive_with_log_info_calls_borg_with_info_parameter():
|
def test_create_archive_with_log_info_calls_borg_with_info_parameter():
|
||||||
|
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||||
|
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||||
flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
|
flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
|
||||||
flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
|
flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
|
||||||
flexmock(module).should_receive('map_directories_to_devices').and_return({})
|
flexmock(module).should_receive('map_directories_to_devices').and_return({})
|
||||||
|
@ -574,6 +640,7 @@ def test_create_archive_with_log_info_calls_borg_with_info_parameter():
|
||||||
flexmock(module.os.path).should_receive('expanduser').and_raise(TypeError)
|
flexmock(module.os.path).should_receive('expanduser').and_raise(TypeError)
|
||||||
flexmock(module).should_receive('expand_home_directories').and_return(())
|
flexmock(module).should_receive('expand_home_directories').and_return(())
|
||||||
flexmock(module).should_receive('write_pattern_file').and_return(None)
|
flexmock(module).should_receive('write_pattern_file').and_return(None)
|
||||||
|
flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
|
||||||
flexmock(module.feature).should_receive('available').and_return(True)
|
flexmock(module.feature).should_receive('available').and_return(True)
|
||||||
flexmock(module).should_receive('ensure_files_readable')
|
flexmock(module).should_receive('ensure_files_readable')
|
||||||
flexmock(module).should_receive('make_pattern_flags').and_return(())
|
flexmock(module).should_receive('make_pattern_flags').and_return(())
|
||||||
|
@ -606,6 +673,8 @@ def test_create_archive_with_log_info_calls_borg_with_info_parameter():
|
||||||
|
|
||||||
|
|
||||||
def test_create_archive_with_log_info_and_json_suppresses_most_borg_output():
|
def test_create_archive_with_log_info_and_json_suppresses_most_borg_output():
|
||||||
|
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||||
|
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||||
flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
|
flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
|
||||||
flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
|
flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
|
||||||
flexmock(module).should_receive('map_directories_to_devices').and_return({})
|
flexmock(module).should_receive('map_directories_to_devices').and_return({})
|
||||||
|
@ -614,6 +683,7 @@ def test_create_archive_with_log_info_and_json_suppresses_most_borg_output():
|
||||||
flexmock(module.os.path).should_receive('expanduser').and_raise(TypeError)
|
flexmock(module.os.path).should_receive('expanduser').and_raise(TypeError)
|
||||||
flexmock(module).should_receive('expand_home_directories').and_return(())
|
flexmock(module).should_receive('expand_home_directories').and_return(())
|
||||||
flexmock(module).should_receive('write_pattern_file').and_return(None)
|
flexmock(module).should_receive('write_pattern_file').and_return(None)
|
||||||
|
flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
|
||||||
flexmock(module.feature).should_receive('available').and_return(True)
|
flexmock(module.feature).should_receive('available').and_return(True)
|
||||||
flexmock(module).should_receive('ensure_files_readable')
|
flexmock(module).should_receive('ensure_files_readable')
|
||||||
flexmock(module).should_receive('make_pattern_flags').and_return(())
|
flexmock(module).should_receive('make_pattern_flags').and_return(())
|
||||||
|
@ -622,11 +692,8 @@ def test_create_archive_with_log_info_and_json_suppresses_most_borg_output():
|
||||||
(f'repo::{DEFAULT_ARCHIVE_NAME}',)
|
(f'repo::{DEFAULT_ARCHIVE_NAME}',)
|
||||||
)
|
)
|
||||||
flexmock(module.environment).should_receive('make_environment')
|
flexmock(module.environment).should_receive('make_environment')
|
||||||
flexmock(module).should_receive('execute_command').with_args(
|
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
|
||||||
('borg', 'create') + REPO_ARCHIVE_WITH_PATHS + ('--json',),
|
('borg', 'create') + REPO_ARCHIVE_WITH_PATHS + ('--json',),
|
||||||
output_log_level=None,
|
|
||||||
output_file=None,
|
|
||||||
borg_local_path='borg',
|
|
||||||
working_directory=None,
|
working_directory=None,
|
||||||
extra_environment=None,
|
extra_environment=None,
|
||||||
)
|
)
|
||||||
|
@ -647,6 +714,8 @@ def test_create_archive_with_log_info_and_json_suppresses_most_borg_output():
|
||||||
|
|
||||||
|
|
||||||
def test_create_archive_with_log_debug_calls_borg_with_debug_parameter():
|
def test_create_archive_with_log_debug_calls_borg_with_debug_parameter():
|
||||||
|
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||||
|
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||||
flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
|
flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
|
||||||
flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
|
flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
|
||||||
flexmock(module).should_receive('map_directories_to_devices').and_return({})
|
flexmock(module).should_receive('map_directories_to_devices').and_return({})
|
||||||
|
@ -655,6 +724,7 @@ def test_create_archive_with_log_debug_calls_borg_with_debug_parameter():
|
||||||
flexmock(module.os.path).should_receive('expanduser').and_raise(TypeError)
|
flexmock(module.os.path).should_receive('expanduser').and_raise(TypeError)
|
||||||
flexmock(module).should_receive('expand_home_directories').and_return(())
|
flexmock(module).should_receive('expand_home_directories').and_return(())
|
||||||
flexmock(module).should_receive('write_pattern_file').and_return(None)
|
flexmock(module).should_receive('write_pattern_file').and_return(None)
|
||||||
|
flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
|
||||||
flexmock(module.feature).should_receive('available').and_return(True)
|
flexmock(module.feature).should_receive('available').and_return(True)
|
||||||
flexmock(module).should_receive('ensure_files_readable')
|
flexmock(module).should_receive('ensure_files_readable')
|
||||||
flexmock(module).should_receive('make_pattern_flags').and_return(())
|
flexmock(module).should_receive('make_pattern_flags').and_return(())
|
||||||
|
@ -687,6 +757,8 @@ def test_create_archive_with_log_debug_calls_borg_with_debug_parameter():
|
||||||
|
|
||||||
|
|
||||||
def test_create_archive_with_log_debug_and_json_suppresses_most_borg_output():
|
def test_create_archive_with_log_debug_and_json_suppresses_most_borg_output():
|
||||||
|
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||||
|
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||||
flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
|
flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
|
||||||
flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
|
flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
|
||||||
flexmock(module).should_receive('map_directories_to_devices').and_return({})
|
flexmock(module).should_receive('map_directories_to_devices').and_return({})
|
||||||
|
@ -695,6 +767,7 @@ def test_create_archive_with_log_debug_and_json_suppresses_most_borg_output():
|
||||||
flexmock(module.os.path).should_receive('expanduser').and_raise(TypeError)
|
flexmock(module.os.path).should_receive('expanduser').and_raise(TypeError)
|
||||||
flexmock(module).should_receive('expand_home_directories').and_return(())
|
flexmock(module).should_receive('expand_home_directories').and_return(())
|
||||||
flexmock(module).should_receive('write_pattern_file').and_return(None)
|
flexmock(module).should_receive('write_pattern_file').and_return(None)
|
||||||
|
flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
|
||||||
flexmock(module.feature).should_receive('available').and_return(True)
|
flexmock(module.feature).should_receive('available').and_return(True)
|
||||||
flexmock(module).should_receive('ensure_files_readable')
|
flexmock(module).should_receive('ensure_files_readable')
|
||||||
flexmock(module).should_receive('make_pattern_flags').and_return(())
|
flexmock(module).should_receive('make_pattern_flags').and_return(())
|
||||||
|
@ -703,11 +776,8 @@ def test_create_archive_with_log_debug_and_json_suppresses_most_borg_output():
|
||||||
(f'repo::{DEFAULT_ARCHIVE_NAME}',)
|
(f'repo::{DEFAULT_ARCHIVE_NAME}',)
|
||||||
)
|
)
|
||||||
flexmock(module.environment).should_receive('make_environment')
|
flexmock(module.environment).should_receive('make_environment')
|
||||||
flexmock(module).should_receive('execute_command').with_args(
|
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
|
||||||
('borg', 'create') + REPO_ARCHIVE_WITH_PATHS + ('--json',),
|
('borg', 'create') + REPO_ARCHIVE_WITH_PATHS + ('--json',),
|
||||||
output_log_level=None,
|
|
||||||
output_file=None,
|
|
||||||
borg_local_path='borg',
|
|
||||||
working_directory=None,
|
working_directory=None,
|
||||||
extra_environment=None,
|
extra_environment=None,
|
||||||
)
|
)
|
||||||
|
@ -728,6 +798,8 @@ def test_create_archive_with_log_debug_and_json_suppresses_most_borg_output():
|
||||||
|
|
||||||
|
|
||||||
def test_create_archive_with_dry_run_calls_borg_with_dry_run_parameter():
|
def test_create_archive_with_dry_run_calls_borg_with_dry_run_parameter():
|
||||||
|
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||||
|
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||||
flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
|
flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
|
||||||
flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
|
flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
|
||||||
flexmock(module).should_receive('map_directories_to_devices').and_return({})
|
flexmock(module).should_receive('map_directories_to_devices').and_return({})
|
||||||
|
@ -736,6 +808,7 @@ def test_create_archive_with_dry_run_calls_borg_with_dry_run_parameter():
|
||||||
flexmock(module.os.path).should_receive('expanduser').and_raise(TypeError)
|
flexmock(module.os.path).should_receive('expanduser').and_raise(TypeError)
|
||||||
flexmock(module).should_receive('expand_home_directories').and_return(())
|
flexmock(module).should_receive('expand_home_directories').and_return(())
|
||||||
flexmock(module).should_receive('write_pattern_file').and_return(None)
|
flexmock(module).should_receive('write_pattern_file').and_return(None)
|
||||||
|
flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
|
||||||
flexmock(module.feature).should_receive('available').and_return(True)
|
flexmock(module.feature).should_receive('available').and_return(True)
|
||||||
flexmock(module).should_receive('ensure_files_readable')
|
flexmock(module).should_receive('ensure_files_readable')
|
||||||
flexmock(module).should_receive('make_pattern_flags').and_return(())
|
flexmock(module).should_receive('make_pattern_flags').and_return(())
|
||||||
|
@ -769,6 +842,8 @@ def test_create_archive_with_dry_run_calls_borg_with_dry_run_parameter():
|
||||||
def test_create_archive_with_stats_and_dry_run_calls_borg_without_stats_parameter():
|
def test_create_archive_with_stats_and_dry_run_calls_borg_without_stats_parameter():
|
||||||
# --dry-run and --stats are mutually exclusive, see:
|
# --dry-run and --stats are mutually exclusive, see:
|
||||||
# https://borgbackup.readthedocs.io/en/stable/usage/create.html#description
|
# https://borgbackup.readthedocs.io/en/stable/usage/create.html#description
|
||||||
|
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||||
|
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||||
flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
|
flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
|
||||||
flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
|
flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
|
||||||
flexmock(module).should_receive('map_directories_to_devices').and_return({})
|
flexmock(module).should_receive('map_directories_to_devices').and_return({})
|
||||||
|
@ -777,6 +852,7 @@ def test_create_archive_with_stats_and_dry_run_calls_borg_without_stats_paramete
|
||||||
flexmock(module.os.path).should_receive('expanduser').and_raise(TypeError)
|
flexmock(module.os.path).should_receive('expanduser').and_raise(TypeError)
|
||||||
flexmock(module).should_receive('expand_home_directories').and_return(())
|
flexmock(module).should_receive('expand_home_directories').and_return(())
|
||||||
flexmock(module).should_receive('write_pattern_file').and_return(None)
|
flexmock(module).should_receive('write_pattern_file').and_return(None)
|
||||||
|
flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
|
||||||
flexmock(module.feature).should_receive('available').and_return(True)
|
flexmock(module.feature).should_receive('available').and_return(True)
|
||||||
flexmock(module).should_receive('ensure_files_readable')
|
flexmock(module).should_receive('ensure_files_readable')
|
||||||
flexmock(module).should_receive('make_pattern_flags').and_return(())
|
flexmock(module).should_receive('make_pattern_flags').and_return(())
|
||||||
|
@ -810,6 +886,8 @@ def test_create_archive_with_stats_and_dry_run_calls_borg_without_stats_paramete
|
||||||
|
|
||||||
|
|
||||||
def test_create_archive_with_checkpoint_interval_calls_borg_with_checkpoint_interval_parameters():
|
def test_create_archive_with_checkpoint_interval_calls_borg_with_checkpoint_interval_parameters():
|
||||||
|
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||||
|
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||||
flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
|
flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
|
||||||
flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
|
flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
|
||||||
flexmock(module).should_receive('map_directories_to_devices').and_return({})
|
flexmock(module).should_receive('map_directories_to_devices').and_return({})
|
||||||
|
@ -818,6 +896,7 @@ def test_create_archive_with_checkpoint_interval_calls_borg_with_checkpoint_inte
|
||||||
flexmock(module.os.path).should_receive('expanduser').and_raise(TypeError)
|
flexmock(module.os.path).should_receive('expanduser').and_raise(TypeError)
|
||||||
flexmock(module).should_receive('expand_home_directories').and_return(())
|
flexmock(module).should_receive('expand_home_directories').and_return(())
|
||||||
flexmock(module).should_receive('write_pattern_file').and_return(None)
|
flexmock(module).should_receive('write_pattern_file').and_return(None)
|
||||||
|
flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
|
||||||
flexmock(module.feature).should_receive('available').and_return(True)
|
flexmock(module.feature).should_receive('available').and_return(True)
|
||||||
flexmock(module).should_receive('ensure_files_readable')
|
flexmock(module).should_receive('ensure_files_readable')
|
||||||
flexmock(module).should_receive('make_pattern_flags').and_return(())
|
flexmock(module).should_receive('make_pattern_flags').and_return(())
|
||||||
|
@ -848,7 +927,9 @@ def test_create_archive_with_checkpoint_interval_calls_borg_with_checkpoint_inte
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_create_archive_with_chunker_params_calls_borg_with_chunker_params_parameters():
|
def test_create_archive_with_checkpoint_volume_calls_borg_with_checkpoint_volume_parameters():
|
||||||
|
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||||
|
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||||
flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
|
flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
|
||||||
flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
|
flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
|
||||||
flexmock(module).should_receive('map_directories_to_devices').and_return({})
|
flexmock(module).should_receive('map_directories_to_devices').and_return({})
|
||||||
|
@ -857,6 +938,49 @@ def test_create_archive_with_chunker_params_calls_borg_with_chunker_params_param
|
||||||
flexmock(module.os.path).should_receive('expanduser').and_raise(TypeError)
|
flexmock(module.os.path).should_receive('expanduser').and_raise(TypeError)
|
||||||
flexmock(module).should_receive('expand_home_directories').and_return(())
|
flexmock(module).should_receive('expand_home_directories').and_return(())
|
||||||
flexmock(module).should_receive('write_pattern_file').and_return(None)
|
flexmock(module).should_receive('write_pattern_file').and_return(None)
|
||||||
|
flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
|
||||||
|
flexmock(module.feature).should_receive('available').and_return(True)
|
||||||
|
flexmock(module).should_receive('ensure_files_readable')
|
||||||
|
flexmock(module).should_receive('make_pattern_flags').and_return(())
|
||||||
|
flexmock(module).should_receive('make_exclude_flags').and_return(())
|
||||||
|
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
|
||||||
|
(f'repo::{DEFAULT_ARCHIVE_NAME}',)
|
||||||
|
)
|
||||||
|
flexmock(module.environment).should_receive('make_environment')
|
||||||
|
flexmock(module).should_receive('execute_command').with_args(
|
||||||
|
('borg', 'create', '--checkpoint-volume', '1024') + REPO_ARCHIVE_WITH_PATHS,
|
||||||
|
output_log_level=logging.INFO,
|
||||||
|
output_file=None,
|
||||||
|
borg_local_path='borg',
|
||||||
|
working_directory=None,
|
||||||
|
extra_environment=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
module.create_archive(
|
||||||
|
dry_run=False,
|
||||||
|
repository='repo',
|
||||||
|
location_config={
|
||||||
|
'source_directories': ['foo', 'bar'],
|
||||||
|
'repositories': ['repo'],
|
||||||
|
'exclude_patterns': None,
|
||||||
|
},
|
||||||
|
storage_config={'checkpoint_volume': 1024},
|
||||||
|
local_borg_version='1.2.3',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_archive_with_chunker_params_calls_borg_with_chunker_params_parameters():
|
||||||
|
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||||
|
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||||
|
flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
|
||||||
|
flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
|
||||||
|
flexmock(module).should_receive('map_directories_to_devices').and_return({})
|
||||||
|
flexmock(module).should_receive('expand_directories').and_return(())
|
||||||
|
flexmock(module).should_receive('pattern_root_directories').and_return([])
|
||||||
|
flexmock(module.os.path).should_receive('expanduser').and_raise(TypeError)
|
||||||
|
flexmock(module).should_receive('expand_home_directories').and_return(())
|
||||||
|
flexmock(module).should_receive('write_pattern_file').and_return(None)
|
||||||
|
flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
|
||||||
flexmock(module.feature).should_receive('available').and_return(True)
|
flexmock(module.feature).should_receive('available').and_return(True)
|
||||||
flexmock(module).should_receive('ensure_files_readable')
|
flexmock(module).should_receive('ensure_files_readable')
|
||||||
flexmock(module).should_receive('make_pattern_flags').and_return(())
|
flexmock(module).should_receive('make_pattern_flags').and_return(())
|
||||||
|
@ -888,6 +1012,8 @@ def test_create_archive_with_chunker_params_calls_borg_with_chunker_params_param
|
||||||
|
|
||||||
|
|
||||||
def test_create_archive_with_compression_calls_borg_with_compression_parameters():
|
def test_create_archive_with_compression_calls_borg_with_compression_parameters():
|
||||||
|
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||||
|
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||||
flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
|
flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
|
||||||
flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
|
flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
|
||||||
flexmock(module).should_receive('map_directories_to_devices').and_return({})
|
flexmock(module).should_receive('map_directories_to_devices').and_return({})
|
||||||
|
@ -896,6 +1022,7 @@ def test_create_archive_with_compression_calls_borg_with_compression_parameters(
|
||||||
flexmock(module.os.path).should_receive('expanduser').and_raise(TypeError)
|
flexmock(module.os.path).should_receive('expanduser').and_raise(TypeError)
|
||||||
flexmock(module).should_receive('expand_home_directories').and_return(())
|
flexmock(module).should_receive('expand_home_directories').and_return(())
|
||||||
flexmock(module).should_receive('write_pattern_file').and_return(None)
|
flexmock(module).should_receive('write_pattern_file').and_return(None)
|
||||||
|
flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
|
||||||
flexmock(module.feature).should_receive('available').and_return(True)
|
flexmock(module.feature).should_receive('available').and_return(True)
|
||||||
flexmock(module).should_receive('ensure_files_readable')
|
flexmock(module).should_receive('ensure_files_readable')
|
||||||
flexmock(module).should_receive('make_pattern_flags').and_return(())
|
flexmock(module).should_receive('make_pattern_flags').and_return(())
|
||||||
|
@ -932,6 +1059,8 @@ def test_create_archive_with_compression_calls_borg_with_compression_parameters(
|
||||||
def test_create_archive_with_upload_rate_limit_calls_borg_with_upload_ratelimit_parameters(
|
def test_create_archive_with_upload_rate_limit_calls_borg_with_upload_ratelimit_parameters(
|
||||||
feature_available, option_flag
|
feature_available, option_flag
|
||||||
):
|
):
|
||||||
|
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||||
|
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||||
flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
|
flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
|
||||||
flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
|
flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
|
||||||
flexmock(module).should_receive('map_directories_to_devices').and_return({})
|
flexmock(module).should_receive('map_directories_to_devices').and_return({})
|
||||||
|
@ -940,6 +1069,7 @@ def test_create_archive_with_upload_rate_limit_calls_borg_with_upload_ratelimit_
|
||||||
flexmock(module.os.path).should_receive('expanduser').and_raise(TypeError)
|
flexmock(module.os.path).should_receive('expanduser').and_raise(TypeError)
|
||||||
flexmock(module).should_receive('expand_home_directories').and_return(())
|
flexmock(module).should_receive('expand_home_directories').and_return(())
|
||||||
flexmock(module).should_receive('write_pattern_file').and_return(None)
|
flexmock(module).should_receive('write_pattern_file').and_return(None)
|
||||||
|
flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
|
||||||
flexmock(module.feature).should_receive('available').and_return(feature_available)
|
flexmock(module.feature).should_receive('available').and_return(feature_available)
|
||||||
flexmock(module).should_receive('ensure_files_readable')
|
flexmock(module).should_receive('ensure_files_readable')
|
||||||
flexmock(module).should_receive('make_pattern_flags').and_return(())
|
flexmock(module).should_receive('make_pattern_flags').and_return(())
|
||||||
|
@ -971,6 +1101,8 @@ def test_create_archive_with_upload_rate_limit_calls_borg_with_upload_ratelimit_
|
||||||
|
|
||||||
|
|
||||||
def test_create_archive_with_working_directory_calls_borg_with_working_directory():
|
def test_create_archive_with_working_directory_calls_borg_with_working_directory():
|
||||||
|
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||||
|
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||||
flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
|
flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
|
||||||
flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
|
flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
|
||||||
flexmock(module).should_receive('map_directories_to_devices').and_return({})
|
flexmock(module).should_receive('map_directories_to_devices').and_return({})
|
||||||
|
@ -981,6 +1113,7 @@ def test_create_archive_with_working_directory_calls_borg_with_working_directory
|
||||||
)
|
)
|
||||||
flexmock(module).should_receive('expand_home_directories').and_return(())
|
flexmock(module).should_receive('expand_home_directories').and_return(())
|
||||||
flexmock(module).should_receive('write_pattern_file').and_return(None)
|
flexmock(module).should_receive('write_pattern_file').and_return(None)
|
||||||
|
flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
|
||||||
flexmock(module.feature).should_receive('available').and_return(True)
|
flexmock(module.feature).should_receive('available').and_return(True)
|
||||||
flexmock(module).should_receive('ensure_files_readable')
|
flexmock(module).should_receive('ensure_files_readable')
|
||||||
flexmock(module).should_receive('make_pattern_flags').and_return(())
|
flexmock(module).should_receive('make_pattern_flags').and_return(())
|
||||||
|
@ -1013,6 +1146,8 @@ def test_create_archive_with_working_directory_calls_borg_with_working_directory
|
||||||
|
|
||||||
|
|
||||||
def test_create_archive_with_one_file_system_calls_borg_with_one_file_system_parameter():
|
def test_create_archive_with_one_file_system_calls_borg_with_one_file_system_parameter():
|
||||||
|
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||||
|
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||||
flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
|
flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
|
||||||
flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
|
flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
|
||||||
flexmock(module).should_receive('map_directories_to_devices').and_return({})
|
flexmock(module).should_receive('map_directories_to_devices').and_return({})
|
||||||
|
@ -1021,6 +1156,7 @@ def test_create_archive_with_one_file_system_calls_borg_with_one_file_system_par
|
||||||
flexmock(module.os.path).should_receive('expanduser').and_raise(TypeError)
|
flexmock(module.os.path).should_receive('expanduser').and_raise(TypeError)
|
||||||
flexmock(module).should_receive('expand_home_directories').and_return(())
|
flexmock(module).should_receive('expand_home_directories').and_return(())
|
||||||
flexmock(module).should_receive('write_pattern_file').and_return(None)
|
flexmock(module).should_receive('write_pattern_file').and_return(None)
|
||||||
|
flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
|
||||||
flexmock(module.feature).should_receive('available').and_return(True)
|
flexmock(module.feature).should_receive('available').and_return(True)
|
||||||
flexmock(module).should_receive('ensure_files_readable')
|
flexmock(module).should_receive('ensure_files_readable')
|
||||||
flexmock(module).should_receive('make_pattern_flags').and_return(())
|
flexmock(module).should_receive('make_pattern_flags').and_return(())
|
||||||
|
@ -1058,6 +1194,8 @@ def test_create_archive_with_one_file_system_calls_borg_with_one_file_system_par
|
||||||
def test_create_archive_with_numeric_ids_calls_borg_with_numeric_ids_parameter(
|
def test_create_archive_with_numeric_ids_calls_borg_with_numeric_ids_parameter(
|
||||||
feature_available, option_flag
|
feature_available, option_flag
|
||||||
):
|
):
|
||||||
|
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||||
|
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||||
flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
|
flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
|
||||||
flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
|
flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
|
||||||
flexmock(module).should_receive('map_directories_to_devices').and_return({})
|
flexmock(module).should_receive('map_directories_to_devices').and_return({})
|
||||||
|
@ -1066,6 +1204,7 @@ def test_create_archive_with_numeric_ids_calls_borg_with_numeric_ids_parameter(
|
||||||
flexmock(module.os.path).should_receive('expanduser').and_raise(TypeError)
|
flexmock(module.os.path).should_receive('expanduser').and_raise(TypeError)
|
||||||
flexmock(module).should_receive('expand_home_directories').and_return(())
|
flexmock(module).should_receive('expand_home_directories').and_return(())
|
||||||
flexmock(module).should_receive('write_pattern_file').and_return(None)
|
flexmock(module).should_receive('write_pattern_file').and_return(None)
|
||||||
|
flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
|
||||||
flexmock(module.feature).should_receive('available').and_return(feature_available)
|
flexmock(module.feature).should_receive('available').and_return(feature_available)
|
||||||
flexmock(module).should_receive('ensure_files_readable')
|
flexmock(module).should_receive('ensure_files_readable')
|
||||||
flexmock(module).should_receive('make_pattern_flags').and_return(())
|
flexmock(module).should_receive('make_pattern_flags').and_return(())
|
||||||
|
@ -1098,6 +1237,8 @@ def test_create_archive_with_numeric_ids_calls_borg_with_numeric_ids_parameter(
|
||||||
|
|
||||||
|
|
||||||
def test_create_archive_with_read_special_calls_borg_with_read_special_parameter():
|
def test_create_archive_with_read_special_calls_borg_with_read_special_parameter():
|
||||||
|
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||||
|
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||||
flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
|
flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
|
||||||
flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
|
flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
|
||||||
flexmock(module).should_receive('map_directories_to_devices').and_return({})
|
flexmock(module).should_receive('map_directories_to_devices').and_return({})
|
||||||
|
@ -1106,6 +1247,7 @@ def test_create_archive_with_read_special_calls_borg_with_read_special_parameter
|
||||||
flexmock(module.os.path).should_receive('expanduser').and_raise(TypeError)
|
flexmock(module.os.path).should_receive('expanduser').and_raise(TypeError)
|
||||||
flexmock(module).should_receive('expand_home_directories').and_return(())
|
flexmock(module).should_receive('expand_home_directories').and_return(())
|
||||||
flexmock(module).should_receive('write_pattern_file').and_return(None)
|
flexmock(module).should_receive('write_pattern_file').and_return(None)
|
||||||
|
flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
|
||||||
flexmock(module.feature).should_receive('available').and_return(True)
|
flexmock(module.feature).should_receive('available').and_return(True)
|
||||||
flexmock(module).should_receive('ensure_files_readable')
|
flexmock(module).should_receive('ensure_files_readable')
|
||||||
flexmock(module).should_receive('make_pattern_flags').and_return(())
|
flexmock(module).should_receive('make_pattern_flags').and_return(())
|
||||||
|
@ -1154,6 +1296,8 @@ def test_create_archive_with_read_special_calls_borg_with_read_special_parameter
|
||||||
def test_create_archive_with_basic_option_calls_borg_with_corresponding_parameter(
|
def test_create_archive_with_basic_option_calls_borg_with_corresponding_parameter(
|
||||||
option_name, option_value
|
option_name, option_value
|
||||||
):
|
):
|
||||||
|
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||||
|
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||||
option_flag = '--no' + option_name.replace('', '') if option_value is False else None
|
option_flag = '--no' + option_name.replace('', '') if option_value is False else None
|
||||||
flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
|
flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
|
||||||
flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
|
flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
|
||||||
|
@ -1163,6 +1307,7 @@ def test_create_archive_with_basic_option_calls_borg_with_corresponding_paramete
|
||||||
flexmock(module.os.path).should_receive('expanduser').and_raise(TypeError)
|
flexmock(module.os.path).should_receive('expanduser').and_raise(TypeError)
|
||||||
flexmock(module).should_receive('expand_home_directories').and_return(())
|
flexmock(module).should_receive('expand_home_directories').and_return(())
|
||||||
flexmock(module).should_receive('write_pattern_file').and_return(None)
|
flexmock(module).should_receive('write_pattern_file').and_return(None)
|
||||||
|
flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
|
||||||
flexmock(module.feature).should_receive('available').and_return(True)
|
flexmock(module.feature).should_receive('available').and_return(True)
|
||||||
flexmock(module).should_receive('ensure_files_readable')
|
flexmock(module).should_receive('ensure_files_readable')
|
||||||
flexmock(module).should_receive('make_pattern_flags').and_return(())
|
flexmock(module).should_receive('make_pattern_flags').and_return(())
|
||||||
|
@ -1206,6 +1351,8 @@ def test_create_archive_with_basic_option_calls_borg_with_corresponding_paramete
|
||||||
def test_create_archive_with_atime_option_calls_borg_with_corresponding_parameter(
|
def test_create_archive_with_atime_option_calls_borg_with_corresponding_parameter(
|
||||||
option_value, feature_available, option_flag
|
option_value, feature_available, option_flag
|
||||||
):
|
):
|
||||||
|
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||||
|
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||||
flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
|
flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
|
||||||
flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
|
flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
|
||||||
flexmock(module).should_receive('map_directories_to_devices').and_return({})
|
flexmock(module).should_receive('map_directories_to_devices').and_return({})
|
||||||
|
@ -1214,6 +1361,7 @@ def test_create_archive_with_atime_option_calls_borg_with_corresponding_paramete
|
||||||
flexmock(module.os.path).should_receive('expanduser').and_raise(TypeError)
|
flexmock(module.os.path).should_receive('expanduser').and_raise(TypeError)
|
||||||
flexmock(module).should_receive('expand_home_directories').and_return(())
|
flexmock(module).should_receive('expand_home_directories').and_return(())
|
||||||
flexmock(module).should_receive('write_pattern_file').and_return(None)
|
flexmock(module).should_receive('write_pattern_file').and_return(None)
|
||||||
|
flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
|
||||||
flexmock(module.feature).should_receive('available').and_return(feature_available)
|
flexmock(module.feature).should_receive('available').and_return(feature_available)
|
||||||
flexmock(module).should_receive('ensure_files_readable')
|
flexmock(module).should_receive('ensure_files_readable')
|
||||||
flexmock(module).should_receive('make_pattern_flags').and_return(())
|
flexmock(module).should_receive('make_pattern_flags').and_return(())
|
||||||
|
@ -1257,6 +1405,8 @@ def test_create_archive_with_atime_option_calls_borg_with_corresponding_paramete
|
||||||
def test_create_archive_with_flags_option_calls_borg_with_corresponding_parameter(
|
def test_create_archive_with_flags_option_calls_borg_with_corresponding_parameter(
|
||||||
option_value, feature_available, option_flag
|
option_value, feature_available, option_flag
|
||||||
):
|
):
|
||||||
|
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||||
|
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||||
flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
|
flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
|
||||||
flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
|
flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
|
||||||
flexmock(module).should_receive('map_directories_to_devices').and_return({})
|
flexmock(module).should_receive('map_directories_to_devices').and_return({})
|
||||||
|
@ -1265,6 +1415,7 @@ def test_create_archive_with_flags_option_calls_borg_with_corresponding_paramete
|
||||||
flexmock(module.os.path).should_receive('expanduser').and_raise(TypeError)
|
flexmock(module.os.path).should_receive('expanduser').and_raise(TypeError)
|
||||||
flexmock(module).should_receive('expand_home_directories').and_return(())
|
flexmock(module).should_receive('expand_home_directories').and_return(())
|
||||||
flexmock(module).should_receive('write_pattern_file').and_return(None)
|
flexmock(module).should_receive('write_pattern_file').and_return(None)
|
||||||
|
flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
|
||||||
flexmock(module.feature).should_receive('available').and_return(feature_available)
|
flexmock(module.feature).should_receive('available').and_return(feature_available)
|
||||||
flexmock(module).should_receive('ensure_files_readable')
|
flexmock(module).should_receive('ensure_files_readable')
|
||||||
flexmock(module).should_receive('make_pattern_flags').and_return(())
|
flexmock(module).should_receive('make_pattern_flags').and_return(())
|
||||||
|
@ -1297,6 +1448,8 @@ def test_create_archive_with_flags_option_calls_borg_with_corresponding_paramete
|
||||||
|
|
||||||
|
|
||||||
def test_create_archive_with_files_cache_calls_borg_with_files_cache_parameters():
|
def test_create_archive_with_files_cache_calls_borg_with_files_cache_parameters():
|
||||||
|
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||||
|
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||||
flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
|
flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
|
||||||
flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
|
flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
|
||||||
flexmock(module).should_receive('map_directories_to_devices').and_return({})
|
flexmock(module).should_receive('map_directories_to_devices').and_return({})
|
||||||
|
@ -1305,6 +1458,7 @@ def test_create_archive_with_files_cache_calls_borg_with_files_cache_parameters(
|
||||||
flexmock(module.os.path).should_receive('expanduser').and_raise(TypeError)
|
flexmock(module.os.path).should_receive('expanduser').and_raise(TypeError)
|
||||||
flexmock(module).should_receive('expand_home_directories').and_return(())
|
flexmock(module).should_receive('expand_home_directories').and_return(())
|
||||||
flexmock(module).should_receive('write_pattern_file').and_return(None)
|
flexmock(module).should_receive('write_pattern_file').and_return(None)
|
||||||
|
flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
|
||||||
flexmock(module.feature).should_receive('available').and_return(True)
|
flexmock(module.feature).should_receive('available').and_return(True)
|
||||||
flexmock(module).should_receive('ensure_files_readable')
|
flexmock(module).should_receive('ensure_files_readable')
|
||||||
flexmock(module).should_receive('make_pattern_flags').and_return(())
|
flexmock(module).should_receive('make_pattern_flags').and_return(())
|
||||||
|
@ -1337,6 +1491,8 @@ def test_create_archive_with_files_cache_calls_borg_with_files_cache_parameters(
|
||||||
|
|
||||||
|
|
||||||
def test_create_archive_with_local_path_calls_borg_via_local_path():
|
def test_create_archive_with_local_path_calls_borg_via_local_path():
|
||||||
|
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||||
|
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||||
flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
|
flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
|
||||||
flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
|
flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
|
||||||
flexmock(module).should_receive('map_directories_to_devices').and_return({})
|
flexmock(module).should_receive('map_directories_to_devices').and_return({})
|
||||||
|
@ -1345,6 +1501,7 @@ def test_create_archive_with_local_path_calls_borg_via_local_path():
|
||||||
flexmock(module.os.path).should_receive('expanduser').and_raise(TypeError)
|
flexmock(module.os.path).should_receive('expanduser').and_raise(TypeError)
|
||||||
flexmock(module).should_receive('expand_home_directories').and_return(())
|
flexmock(module).should_receive('expand_home_directories').and_return(())
|
||||||
flexmock(module).should_receive('write_pattern_file').and_return(None)
|
flexmock(module).should_receive('write_pattern_file').and_return(None)
|
||||||
|
flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
|
||||||
flexmock(module.feature).should_receive('available').and_return(True)
|
flexmock(module.feature).should_receive('available').and_return(True)
|
||||||
flexmock(module).should_receive('ensure_files_readable')
|
flexmock(module).should_receive('ensure_files_readable')
|
||||||
flexmock(module).should_receive('make_pattern_flags').and_return(())
|
flexmock(module).should_receive('make_pattern_flags').and_return(())
|
||||||
|
@ -1377,6 +1534,8 @@ def test_create_archive_with_local_path_calls_borg_via_local_path():
|
||||||
|
|
||||||
|
|
||||||
def test_create_archive_with_remote_path_calls_borg_with_remote_path_parameters():
|
def test_create_archive_with_remote_path_calls_borg_with_remote_path_parameters():
|
||||||
|
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||||
|
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||||
flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
|
flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
|
||||||
flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
|
flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
|
||||||
flexmock(module).should_receive('map_directories_to_devices').and_return({})
|
flexmock(module).should_receive('map_directories_to_devices').and_return({})
|
||||||
|
@ -1385,6 +1544,7 @@ def test_create_archive_with_remote_path_calls_borg_with_remote_path_parameters(
|
||||||
flexmock(module.os.path).should_receive('expanduser').and_raise(TypeError)
|
flexmock(module.os.path).should_receive('expanduser').and_raise(TypeError)
|
||||||
flexmock(module).should_receive('expand_home_directories').and_return(())
|
flexmock(module).should_receive('expand_home_directories').and_return(())
|
||||||
flexmock(module).should_receive('write_pattern_file').and_return(None)
|
flexmock(module).should_receive('write_pattern_file').and_return(None)
|
||||||
|
flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
|
||||||
flexmock(module.feature).should_receive('available').and_return(True)
|
flexmock(module.feature).should_receive('available').and_return(True)
|
||||||
flexmock(module).should_receive('ensure_files_readable')
|
flexmock(module).should_receive('ensure_files_readable')
|
||||||
flexmock(module).should_receive('make_pattern_flags').and_return(())
|
flexmock(module).should_receive('make_pattern_flags').and_return(())
|
||||||
|
@ -1417,6 +1577,8 @@ def test_create_archive_with_remote_path_calls_borg_with_remote_path_parameters(
|
||||||
|
|
||||||
|
|
||||||
def test_create_archive_with_umask_calls_borg_with_umask_parameters():
|
def test_create_archive_with_umask_calls_borg_with_umask_parameters():
|
||||||
|
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||||
|
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||||
flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
|
flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
|
||||||
flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
|
flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
|
||||||
flexmock(module).should_receive('map_directories_to_devices').and_return({})
|
flexmock(module).should_receive('map_directories_to_devices').and_return({})
|
||||||
|
@ -1425,6 +1587,7 @@ def test_create_archive_with_umask_calls_borg_with_umask_parameters():
|
||||||
flexmock(module.os.path).should_receive('expanduser').and_raise(TypeError)
|
flexmock(module.os.path).should_receive('expanduser').and_raise(TypeError)
|
||||||
flexmock(module).should_receive('expand_home_directories').and_return(())
|
flexmock(module).should_receive('expand_home_directories').and_return(())
|
||||||
flexmock(module).should_receive('write_pattern_file').and_return(None)
|
flexmock(module).should_receive('write_pattern_file').and_return(None)
|
||||||
|
flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
|
||||||
flexmock(module.feature).should_receive('available').and_return(True)
|
flexmock(module.feature).should_receive('available').and_return(True)
|
||||||
flexmock(module).should_receive('ensure_files_readable')
|
flexmock(module).should_receive('ensure_files_readable')
|
||||||
flexmock(module).should_receive('make_pattern_flags').and_return(())
|
flexmock(module).should_receive('make_pattern_flags').and_return(())
|
||||||
|
@ -1456,6 +1619,8 @@ def test_create_archive_with_umask_calls_borg_with_umask_parameters():
|
||||||
|
|
||||||
|
|
||||||
def test_create_archive_with_lock_wait_calls_borg_with_lock_wait_parameters():
|
def test_create_archive_with_lock_wait_calls_borg_with_lock_wait_parameters():
|
||||||
|
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||||
|
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||||
flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
|
flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
|
||||||
flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
|
flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
|
||||||
flexmock(module).should_receive('map_directories_to_devices').and_return({})
|
flexmock(module).should_receive('map_directories_to_devices').and_return({})
|
||||||
|
@ -1464,6 +1629,7 @@ def test_create_archive_with_lock_wait_calls_borg_with_lock_wait_parameters():
|
||||||
flexmock(module.os.path).should_receive('expanduser').and_raise(TypeError)
|
flexmock(module.os.path).should_receive('expanduser').and_raise(TypeError)
|
||||||
flexmock(module).should_receive('expand_home_directories').and_return(())
|
flexmock(module).should_receive('expand_home_directories').and_return(())
|
||||||
flexmock(module).should_receive('write_pattern_file').and_return(None)
|
flexmock(module).should_receive('write_pattern_file').and_return(None)
|
||||||
|
flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
|
||||||
flexmock(module.feature).should_receive('available').and_return(True)
|
flexmock(module.feature).should_receive('available').and_return(True)
|
||||||
flexmock(module).should_receive('ensure_files_readable')
|
flexmock(module).should_receive('ensure_files_readable')
|
||||||
flexmock(module).should_receive('make_pattern_flags').and_return(())
|
flexmock(module).should_receive('make_pattern_flags').and_return(())
|
||||||
|
@ -1494,7 +1660,9 @@ def test_create_archive_with_lock_wait_calls_borg_with_lock_wait_parameters():
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_create_archive_with_stats_calls_borg_with_stats_parameter_and_warning_output_log_level():
|
def test_create_archive_with_stats_calls_borg_with_stats_parameter_and_answer_output_log_level():
|
||||||
|
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||||
|
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||||
flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
|
flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
|
||||||
flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
|
flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
|
||||||
flexmock(module).should_receive('map_directories_to_devices').and_return({})
|
flexmock(module).should_receive('map_directories_to_devices').and_return({})
|
||||||
|
@ -1503,6 +1671,7 @@ def test_create_archive_with_stats_calls_borg_with_stats_parameter_and_warning_o
|
||||||
flexmock(module.os.path).should_receive('expanduser').and_raise(TypeError)
|
flexmock(module.os.path).should_receive('expanduser').and_raise(TypeError)
|
||||||
flexmock(module).should_receive('expand_home_directories').and_return(())
|
flexmock(module).should_receive('expand_home_directories').and_return(())
|
||||||
flexmock(module).should_receive('write_pattern_file').and_return(None)
|
flexmock(module).should_receive('write_pattern_file').and_return(None)
|
||||||
|
flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
|
||||||
flexmock(module.feature).should_receive('available').and_return(True)
|
flexmock(module.feature).should_receive('available').and_return(True)
|
||||||
flexmock(module).should_receive('ensure_files_readable')
|
flexmock(module).should_receive('ensure_files_readable')
|
||||||
flexmock(module).should_receive('make_pattern_flags').and_return(())
|
flexmock(module).should_receive('make_pattern_flags').and_return(())
|
||||||
|
@ -1513,7 +1682,7 @@ def test_create_archive_with_stats_calls_borg_with_stats_parameter_and_warning_o
|
||||||
flexmock(module.environment).should_receive('make_environment')
|
flexmock(module.environment).should_receive('make_environment')
|
||||||
flexmock(module).should_receive('execute_command').with_args(
|
flexmock(module).should_receive('execute_command').with_args(
|
||||||
('borg', 'create') + REPO_ARCHIVE_WITH_PATHS + ('--stats',),
|
('borg', 'create') + REPO_ARCHIVE_WITH_PATHS + ('--stats',),
|
||||||
output_log_level=logging.WARNING,
|
output_log_level=module.borgmatic.logger.ANSWER,
|
||||||
output_file=None,
|
output_file=None,
|
||||||
borg_local_path='borg',
|
borg_local_path='borg',
|
||||||
working_directory=None,
|
working_directory=None,
|
||||||
|
@ -1534,7 +1703,9 @@ def test_create_archive_with_stats_calls_borg_with_stats_parameter_and_warning_o
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_create_archive_with_stats_and_log_info_calls_borg_with_stats_parameter_and_info_output_log_level():
|
def test_create_archive_with_files_calls_borg_with_list_parameter_and_answer_output_log_level():
|
||||||
|
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||||
|
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||||
flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
|
flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
|
||||||
flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
|
flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
|
||||||
flexmock(module).should_receive('map_directories_to_devices').and_return({})
|
flexmock(module).should_receive('map_directories_to_devices').and_return({})
|
||||||
|
@ -1543,6 +1714,7 @@ def test_create_archive_with_stats_and_log_info_calls_borg_with_stats_parameter_
|
||||||
flexmock(module.os.path).should_receive('expanduser').and_raise(TypeError)
|
flexmock(module.os.path).should_receive('expanduser').and_raise(TypeError)
|
||||||
flexmock(module).should_receive('expand_home_directories').and_return(())
|
flexmock(module).should_receive('expand_home_directories').and_return(())
|
||||||
flexmock(module).should_receive('write_pattern_file').and_return(None)
|
flexmock(module).should_receive('write_pattern_file').and_return(None)
|
||||||
|
flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
|
||||||
flexmock(module.feature).should_receive('available').and_return(True)
|
flexmock(module.feature).should_receive('available').and_return(True)
|
||||||
flexmock(module).should_receive('ensure_files_readable')
|
flexmock(module).should_receive('ensure_files_readable')
|
||||||
flexmock(module).should_receive('make_pattern_flags').and_return(())
|
flexmock(module).should_receive('make_pattern_flags').and_return(())
|
||||||
|
@ -1552,95 +1724,13 @@ def test_create_archive_with_stats_and_log_info_calls_borg_with_stats_parameter_
|
||||||
)
|
)
|
||||||
flexmock(module.environment).should_receive('make_environment')
|
flexmock(module.environment).should_receive('make_environment')
|
||||||
flexmock(module).should_receive('execute_command').with_args(
|
flexmock(module).should_receive('execute_command').with_args(
|
||||||
('borg', 'create') + REPO_ARCHIVE_WITH_PATHS + ('--info', '--stats'),
|
('borg', 'create', '--list', '--filter', 'FOO') + REPO_ARCHIVE_WITH_PATHS,
|
||||||
output_log_level=logging.INFO,
|
output_log_level=module.borgmatic.logger.ANSWER,
|
||||||
output_file=None,
|
output_file=None,
|
||||||
borg_local_path='borg',
|
borg_local_path='borg',
|
||||||
working_directory=None,
|
working_directory=None,
|
||||||
extra_environment=None,
|
extra_environment=None,
|
||||||
)
|
)
|
||||||
insert_logging_mock(logging.INFO)
|
|
||||||
|
|
||||||
module.create_archive(
|
|
||||||
dry_run=False,
|
|
||||||
repository='repo',
|
|
||||||
location_config={
|
|
||||||
'source_directories': ['foo', 'bar'],
|
|
||||||
'repositories': ['repo'],
|
|
||||||
'exclude_patterns': None,
|
|
||||||
},
|
|
||||||
storage_config={},
|
|
||||||
local_borg_version='1.2.3',
|
|
||||||
stats=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_create_archive_with_files_calls_borg_with_list_parameter_and_warning_output_log_level():
|
|
||||||
flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
|
|
||||||
flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
|
|
||||||
flexmock(module).should_receive('map_directories_to_devices').and_return({})
|
|
||||||
flexmock(module).should_receive('expand_directories').and_return(())
|
|
||||||
flexmock(module).should_receive('pattern_root_directories').and_return([])
|
|
||||||
flexmock(module.os.path).should_receive('expanduser').and_raise(TypeError)
|
|
||||||
flexmock(module).should_receive('expand_home_directories').and_return(())
|
|
||||||
flexmock(module).should_receive('write_pattern_file').and_return(None)
|
|
||||||
flexmock(module.feature).should_receive('available').and_return(True)
|
|
||||||
flexmock(module).should_receive('ensure_files_readable')
|
|
||||||
flexmock(module).should_receive('make_pattern_flags').and_return(())
|
|
||||||
flexmock(module).should_receive('make_exclude_flags').and_return(())
|
|
||||||
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
|
|
||||||
(f'repo::{DEFAULT_ARCHIVE_NAME}',)
|
|
||||||
)
|
|
||||||
flexmock(module.environment).should_receive('make_environment')
|
|
||||||
flexmock(module).should_receive('execute_command').with_args(
|
|
||||||
('borg', 'create', '--list', '--filter', 'AMEx-') + REPO_ARCHIVE_WITH_PATHS,
|
|
||||||
output_log_level=logging.WARNING,
|
|
||||||
output_file=None,
|
|
||||||
borg_local_path='borg',
|
|
||||||
working_directory=None,
|
|
||||||
extra_environment=None,
|
|
||||||
)
|
|
||||||
|
|
||||||
module.create_archive(
|
|
||||||
dry_run=False,
|
|
||||||
repository='repo',
|
|
||||||
location_config={
|
|
||||||
'source_directories': ['foo', 'bar'],
|
|
||||||
'repositories': ['repo'],
|
|
||||||
'exclude_patterns': None,
|
|
||||||
},
|
|
||||||
storage_config={},
|
|
||||||
local_borg_version='1.2.3',
|
|
||||||
list_files=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_create_archive_with_files_and_log_info_calls_borg_with_list_parameter_and_info_output_log_level():
|
|
||||||
flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
|
|
||||||
flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
|
|
||||||
flexmock(module).should_receive('map_directories_to_devices').and_return({})
|
|
||||||
flexmock(module).should_receive('expand_directories').and_return(())
|
|
||||||
flexmock(module).should_receive('pattern_root_directories').and_return([])
|
|
||||||
flexmock(module.os.path).should_receive('expanduser').and_raise(TypeError)
|
|
||||||
flexmock(module).should_receive('expand_home_directories').and_return(())
|
|
||||||
flexmock(module).should_receive('write_pattern_file').and_return(None)
|
|
||||||
flexmock(module.feature).should_receive('available').and_return(True)
|
|
||||||
flexmock(module).should_receive('ensure_files_readable')
|
|
||||||
flexmock(module).should_receive('make_pattern_flags').and_return(())
|
|
||||||
flexmock(module).should_receive('make_exclude_flags').and_return(())
|
|
||||||
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
|
|
||||||
(f'repo::{DEFAULT_ARCHIVE_NAME}',)
|
|
||||||
)
|
|
||||||
flexmock(module.environment).should_receive('make_environment')
|
|
||||||
flexmock(module).should_receive('execute_command').with_args(
|
|
||||||
('borg', 'create', '--list', '--filter', 'AMEx-') + REPO_ARCHIVE_WITH_PATHS + ('--info',),
|
|
||||||
output_log_level=logging.INFO,
|
|
||||||
output_file=None,
|
|
||||||
borg_local_path='borg',
|
|
||||||
working_directory=None,
|
|
||||||
extra_environment=None,
|
|
||||||
)
|
|
||||||
insert_logging_mock(logging.INFO)
|
|
||||||
|
|
||||||
module.create_archive(
|
module.create_archive(
|
||||||
dry_run=False,
|
dry_run=False,
|
||||||
|
@ -1657,6 +1747,8 @@ def test_create_archive_with_files_and_log_info_calls_borg_with_list_parameter_a
|
||||||
|
|
||||||
|
|
||||||
def test_create_archive_with_progress_and_log_info_calls_borg_with_progress_parameter_and_no_list():
|
def test_create_archive_with_progress_and_log_info_calls_borg_with_progress_parameter_and_no_list():
|
||||||
|
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||||
|
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||||
flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
|
flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
|
||||||
flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
|
flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
|
||||||
flexmock(module).should_receive('map_directories_to_devices').and_return({})
|
flexmock(module).should_receive('map_directories_to_devices').and_return({})
|
||||||
|
@ -1665,6 +1757,7 @@ def test_create_archive_with_progress_and_log_info_calls_borg_with_progress_para
|
||||||
flexmock(module.os.path).should_receive('expanduser').and_raise(TypeError)
|
flexmock(module.os.path).should_receive('expanduser').and_raise(TypeError)
|
||||||
flexmock(module).should_receive('expand_home_directories').and_return(())
|
flexmock(module).should_receive('expand_home_directories').and_return(())
|
||||||
flexmock(module).should_receive('write_pattern_file').and_return(None)
|
flexmock(module).should_receive('write_pattern_file').and_return(None)
|
||||||
|
flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
|
||||||
flexmock(module.feature).should_receive('available').and_return(True)
|
flexmock(module.feature).should_receive('available').and_return(True)
|
||||||
flexmock(module).should_receive('ensure_files_readable')
|
flexmock(module).should_receive('ensure_files_readable')
|
||||||
flexmock(module).should_receive('make_pattern_flags').and_return(())
|
flexmock(module).should_receive('make_pattern_flags').and_return(())
|
||||||
|
@ -1698,6 +1791,8 @@ def test_create_archive_with_progress_and_log_info_calls_borg_with_progress_para
|
||||||
|
|
||||||
|
|
||||||
def test_create_archive_with_progress_calls_borg_with_progress_parameter():
|
def test_create_archive_with_progress_calls_borg_with_progress_parameter():
|
||||||
|
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||||
|
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||||
flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
|
flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
|
||||||
flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
|
flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
|
||||||
flexmock(module).should_receive('map_directories_to_devices').and_return({})
|
flexmock(module).should_receive('map_directories_to_devices').and_return({})
|
||||||
|
@ -1706,6 +1801,7 @@ def test_create_archive_with_progress_calls_borg_with_progress_parameter():
|
||||||
flexmock(module.os.path).should_receive('expanduser').and_raise(TypeError)
|
flexmock(module.os.path).should_receive('expanduser').and_raise(TypeError)
|
||||||
flexmock(module).should_receive('expand_home_directories').and_return(())
|
flexmock(module).should_receive('expand_home_directories').and_return(())
|
||||||
flexmock(module).should_receive('write_pattern_file').and_return(None)
|
flexmock(module).should_receive('write_pattern_file').and_return(None)
|
||||||
|
flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
|
||||||
flexmock(module.feature).should_receive('available').and_return(True)
|
flexmock(module.feature).should_receive('available').and_return(True)
|
||||||
flexmock(module).should_receive('ensure_files_readable')
|
flexmock(module).should_receive('ensure_files_readable')
|
||||||
flexmock(module).should_receive('make_pattern_flags').and_return(())
|
flexmock(module).should_receive('make_pattern_flags').and_return(())
|
||||||
|
@ -1738,6 +1834,8 @@ def test_create_archive_with_progress_calls_borg_with_progress_parameter():
|
||||||
|
|
||||||
|
|
||||||
def test_create_archive_with_progress_and_stream_processes_calls_borg_with_progress_parameter():
|
def test_create_archive_with_progress_and_stream_processes_calls_borg_with_progress_parameter():
|
||||||
|
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||||
|
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||||
processes = flexmock()
|
processes = flexmock()
|
||||||
flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
|
flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
|
||||||
flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
|
flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
|
||||||
|
@ -1747,6 +1845,7 @@ def test_create_archive_with_progress_and_stream_processes_calls_borg_with_progr
|
||||||
flexmock(module.os.path).should_receive('expanduser').and_raise(TypeError)
|
flexmock(module.os.path).should_receive('expanduser').and_raise(TypeError)
|
||||||
flexmock(module).should_receive('expand_home_directories').and_return(())
|
flexmock(module).should_receive('expand_home_directories').and_return(())
|
||||||
flexmock(module).should_receive('write_pattern_file').and_return(None)
|
flexmock(module).should_receive('write_pattern_file').and_return(None)
|
||||||
|
flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
|
||||||
flexmock(module.feature).should_receive('available').and_return(True)
|
flexmock(module.feature).should_receive('available').and_return(True)
|
||||||
flexmock(module).should_receive('ensure_files_readable')
|
flexmock(module).should_receive('ensure_files_readable')
|
||||||
flexmock(module).should_receive('make_pattern_flags').and_return(())
|
flexmock(module).should_receive('make_pattern_flags').and_return(())
|
||||||
|
@ -1796,6 +1895,8 @@ def test_create_archive_with_progress_and_stream_processes_calls_borg_with_progr
|
||||||
|
|
||||||
|
|
||||||
def test_create_archive_with_stream_processes_ignores_read_special_false_and_logs_warnings():
|
def test_create_archive_with_stream_processes_ignores_read_special_false_and_logs_warnings():
|
||||||
|
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||||
|
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||||
processes = flexmock()
|
processes = flexmock()
|
||||||
flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
|
flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
|
||||||
flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
|
flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
|
||||||
|
@ -1804,7 +1905,8 @@ def test_create_archive_with_stream_processes_ignores_read_special_false_and_log
|
||||||
flexmock(module).should_receive('pattern_root_directories').and_return([])
|
flexmock(module).should_receive('pattern_root_directories').and_return([])
|
||||||
flexmock(module.os.path).should_receive('expanduser').and_raise(TypeError)
|
flexmock(module.os.path).should_receive('expanduser').and_raise(TypeError)
|
||||||
flexmock(module).should_receive('expand_home_directories').and_return(())
|
flexmock(module).should_receive('expand_home_directories').and_return(())
|
||||||
flexmock(module).should_receive('write_pattern_file').and_return(None)
|
flexmock(module).should_receive('write_pattern_file').and_return(flexmock(name='/tmp/excludes'))
|
||||||
|
flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
|
||||||
flexmock(module.feature).should_receive('available').and_return(True)
|
flexmock(module.feature).should_receive('available').and_return(True)
|
||||||
flexmock(module).should_receive('ensure_files_readable')
|
flexmock(module).should_receive('ensure_files_readable')
|
||||||
flexmock(module.logger).should_receive('warning').twice()
|
flexmock(module.logger).should_receive('warning').twice()
|
||||||
|
@ -1814,7 +1916,7 @@ def test_create_archive_with_stream_processes_ignores_read_special_false_and_log
|
||||||
(f'repo::{DEFAULT_ARCHIVE_NAME}',)
|
(f'repo::{DEFAULT_ARCHIVE_NAME}',)
|
||||||
)
|
)
|
||||||
flexmock(module.environment).should_receive('make_environment')
|
flexmock(module.environment).should_receive('make_environment')
|
||||||
flexmock(module).should_receive('collect_special_file_paths').and_return(())
|
flexmock(module).should_receive('collect_special_file_paths').and_return(('/dev/null',))
|
||||||
create_command = (
|
create_command = (
|
||||||
'borg',
|
'borg',
|
||||||
'create',
|
'create',
|
||||||
|
@ -1856,6 +1958,8 @@ def test_create_archive_with_stream_processes_ignores_read_special_false_and_log
|
||||||
|
|
||||||
|
|
||||||
def test_create_archive_with_stream_processes_adds_special_files_to_excludes():
|
def test_create_archive_with_stream_processes_adds_special_files_to_excludes():
|
||||||
|
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||||
|
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||||
processes = flexmock()
|
processes = flexmock()
|
||||||
flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
|
flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
|
||||||
flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
|
flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
|
||||||
|
@ -1869,6 +1973,7 @@ def test_create_archive_with_stream_processes_adds_special_files_to_excludes():
|
||||||
flexmock(module).should_receive('write_pattern_file').and_return(None).and_return(
|
flexmock(module).should_receive('write_pattern_file').and_return(None).and_return(
|
||||||
flexmock(name='/excludes')
|
flexmock(name='/excludes')
|
||||||
)
|
)
|
||||||
|
flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
|
||||||
flexmock(module.feature).should_receive('available').and_return(True)
|
flexmock(module.feature).should_receive('available').and_return(True)
|
||||||
flexmock(module).should_receive('ensure_files_readable')
|
flexmock(module).should_receive('ensure_files_readable')
|
||||||
flexmock(module).should_receive('make_pattern_flags').and_return(())
|
flexmock(module).should_receive('make_pattern_flags').and_return(())
|
||||||
|
@ -1920,6 +2025,8 @@ def test_create_archive_with_stream_processes_adds_special_files_to_excludes():
|
||||||
|
|
||||||
|
|
||||||
def test_create_archive_with_stream_processes_and_read_special_does_not_add_special_files_to_excludes():
|
def test_create_archive_with_stream_processes_and_read_special_does_not_add_special_files_to_excludes():
|
||||||
|
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||||
|
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||||
processes = flexmock()
|
processes = flexmock()
|
||||||
flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
|
flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
|
||||||
flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
|
flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
|
||||||
|
@ -1931,6 +2038,7 @@ def test_create_archive_with_stream_processes_and_read_special_does_not_add_spec
|
||||||
('special',)
|
('special',)
|
||||||
)
|
)
|
||||||
flexmock(module).should_receive('write_pattern_file').and_return(None)
|
flexmock(module).should_receive('write_pattern_file').and_return(None)
|
||||||
|
flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
|
||||||
flexmock(module.feature).should_receive('available').and_return(True)
|
flexmock(module.feature).should_receive('available').and_return(True)
|
||||||
flexmock(module).should_receive('ensure_files_readable')
|
flexmock(module).should_receive('ensure_files_readable')
|
||||||
flexmock(module).should_receive('make_pattern_flags').and_return(())
|
flexmock(module).should_receive('make_pattern_flags').and_return(())
|
||||||
|
@ -1981,6 +2089,8 @@ def test_create_archive_with_stream_processes_and_read_special_does_not_add_spec
|
||||||
|
|
||||||
|
|
||||||
def test_create_archive_with_json_calls_borg_with_json_parameter():
|
def test_create_archive_with_json_calls_borg_with_json_parameter():
|
||||||
|
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||||
|
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||||
flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
|
flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
|
||||||
flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
|
flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
|
||||||
flexmock(module).should_receive('map_directories_to_devices').and_return({})
|
flexmock(module).should_receive('map_directories_to_devices').and_return({})
|
||||||
|
@ -1989,6 +2099,7 @@ def test_create_archive_with_json_calls_borg_with_json_parameter():
|
||||||
flexmock(module.os.path).should_receive('expanduser').and_raise(TypeError)
|
flexmock(module.os.path).should_receive('expanduser').and_raise(TypeError)
|
||||||
flexmock(module).should_receive('expand_home_directories').and_return(())
|
flexmock(module).should_receive('expand_home_directories').and_return(())
|
||||||
flexmock(module).should_receive('write_pattern_file').and_return(None)
|
flexmock(module).should_receive('write_pattern_file').and_return(None)
|
||||||
|
flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
|
||||||
flexmock(module.feature).should_receive('available').and_return(True)
|
flexmock(module.feature).should_receive('available').and_return(True)
|
||||||
flexmock(module).should_receive('ensure_files_readable')
|
flexmock(module).should_receive('ensure_files_readable')
|
||||||
flexmock(module).should_receive('make_pattern_flags').and_return(())
|
flexmock(module).should_receive('make_pattern_flags').and_return(())
|
||||||
|
@ -1997,11 +2108,8 @@ def test_create_archive_with_json_calls_borg_with_json_parameter():
|
||||||
(f'repo::{DEFAULT_ARCHIVE_NAME}',)
|
(f'repo::{DEFAULT_ARCHIVE_NAME}',)
|
||||||
)
|
)
|
||||||
flexmock(module.environment).should_receive('make_environment')
|
flexmock(module.environment).should_receive('make_environment')
|
||||||
flexmock(module).should_receive('execute_command').with_args(
|
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
|
||||||
('borg', 'create') + REPO_ARCHIVE_WITH_PATHS + ('--json',),
|
('borg', 'create') + REPO_ARCHIVE_WITH_PATHS + ('--json',),
|
||||||
output_log_level=None,
|
|
||||||
output_file=None,
|
|
||||||
borg_local_path='borg',
|
|
||||||
working_directory=None,
|
working_directory=None,
|
||||||
extra_environment=None,
|
extra_environment=None,
|
||||||
).and_return('[]')
|
).and_return('[]')
|
||||||
|
@ -2023,6 +2131,8 @@ def test_create_archive_with_json_calls_borg_with_json_parameter():
|
||||||
|
|
||||||
|
|
||||||
def test_create_archive_with_stats_and_json_calls_borg_without_stats_parameter():
|
def test_create_archive_with_stats_and_json_calls_borg_without_stats_parameter():
|
||||||
|
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||||
|
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||||
flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
|
flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
|
||||||
flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
|
flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
|
||||||
flexmock(module).should_receive('map_directories_to_devices').and_return({})
|
flexmock(module).should_receive('map_directories_to_devices').and_return({})
|
||||||
|
@ -2031,6 +2141,7 @@ def test_create_archive_with_stats_and_json_calls_borg_without_stats_parameter()
|
||||||
flexmock(module.os.path).should_receive('expanduser').and_raise(TypeError)
|
flexmock(module.os.path).should_receive('expanduser').and_raise(TypeError)
|
||||||
flexmock(module).should_receive('expand_home_directories').and_return(())
|
flexmock(module).should_receive('expand_home_directories').and_return(())
|
||||||
flexmock(module).should_receive('write_pattern_file').and_return(None)
|
flexmock(module).should_receive('write_pattern_file').and_return(None)
|
||||||
|
flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
|
||||||
flexmock(module.feature).should_receive('available').and_return(True)
|
flexmock(module.feature).should_receive('available').and_return(True)
|
||||||
flexmock(module).should_receive('ensure_files_readable')
|
flexmock(module).should_receive('ensure_files_readable')
|
||||||
flexmock(module).should_receive('make_pattern_flags').and_return(())
|
flexmock(module).should_receive('make_pattern_flags').and_return(())
|
||||||
|
@ -2039,11 +2150,8 @@ def test_create_archive_with_stats_and_json_calls_borg_without_stats_parameter()
|
||||||
(f'repo::{DEFAULT_ARCHIVE_NAME}',)
|
(f'repo::{DEFAULT_ARCHIVE_NAME}',)
|
||||||
)
|
)
|
||||||
flexmock(module.environment).should_receive('make_environment')
|
flexmock(module.environment).should_receive('make_environment')
|
||||||
flexmock(module).should_receive('execute_command').with_args(
|
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
|
||||||
('borg', 'create') + REPO_ARCHIVE_WITH_PATHS + ('--json',),
|
('borg', 'create') + REPO_ARCHIVE_WITH_PATHS + ('--json',),
|
||||||
output_log_level=None,
|
|
||||||
output_file=None,
|
|
||||||
borg_local_path='borg',
|
|
||||||
working_directory=None,
|
working_directory=None,
|
||||||
extra_environment=None,
|
extra_environment=None,
|
||||||
).and_return('[]')
|
).and_return('[]')
|
||||||
|
@ -2066,6 +2174,8 @@ def test_create_archive_with_stats_and_json_calls_borg_without_stats_parameter()
|
||||||
|
|
||||||
|
|
||||||
def test_create_archive_with_source_directories_glob_expands():
|
def test_create_archive_with_source_directories_glob_expands():
|
||||||
|
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||||
|
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||||
flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
|
flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
|
||||||
flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'food'))
|
flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'food'))
|
||||||
flexmock(module).should_receive('map_directories_to_devices').and_return({})
|
flexmock(module).should_receive('map_directories_to_devices').and_return({})
|
||||||
|
@ -2074,6 +2184,7 @@ def test_create_archive_with_source_directories_glob_expands():
|
||||||
flexmock(module.os.path).should_receive('expanduser').and_raise(TypeError)
|
flexmock(module.os.path).should_receive('expanduser').and_raise(TypeError)
|
||||||
flexmock(module).should_receive('expand_home_directories').and_return(())
|
flexmock(module).should_receive('expand_home_directories').and_return(())
|
||||||
flexmock(module).should_receive('write_pattern_file').and_return(None)
|
flexmock(module).should_receive('write_pattern_file').and_return(None)
|
||||||
|
flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
|
||||||
flexmock(module.feature).should_receive('available').and_return(True)
|
flexmock(module.feature).should_receive('available').and_return(True)
|
||||||
flexmock(module).should_receive('ensure_files_readable')
|
flexmock(module).should_receive('ensure_files_readable')
|
||||||
flexmock(module).should_receive('make_pattern_flags').and_return(())
|
flexmock(module).should_receive('make_pattern_flags').and_return(())
|
||||||
|
@ -2106,6 +2217,8 @@ def test_create_archive_with_source_directories_glob_expands():
|
||||||
|
|
||||||
|
|
||||||
def test_create_archive_with_non_matching_source_directories_glob_passes_through():
|
def test_create_archive_with_non_matching_source_directories_glob_passes_through():
|
||||||
|
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||||
|
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||||
flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
|
flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
|
||||||
flexmock(module).should_receive('deduplicate_directories').and_return(('foo*',))
|
flexmock(module).should_receive('deduplicate_directories').and_return(('foo*',))
|
||||||
flexmock(module).should_receive('map_directories_to_devices').and_return({})
|
flexmock(module).should_receive('map_directories_to_devices').and_return({})
|
||||||
|
@ -2114,6 +2227,7 @@ def test_create_archive_with_non_matching_source_directories_glob_passes_through
|
||||||
flexmock(module.os.path).should_receive('expanduser').and_raise(TypeError)
|
flexmock(module.os.path).should_receive('expanduser').and_raise(TypeError)
|
||||||
flexmock(module).should_receive('expand_home_directories').and_return(())
|
flexmock(module).should_receive('expand_home_directories').and_return(())
|
||||||
flexmock(module).should_receive('write_pattern_file').and_return(None)
|
flexmock(module).should_receive('write_pattern_file').and_return(None)
|
||||||
|
flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
|
||||||
flexmock(module.feature).should_receive('available').and_return(True)
|
flexmock(module.feature).should_receive('available').and_return(True)
|
||||||
flexmock(module).should_receive('ensure_files_readable')
|
flexmock(module).should_receive('ensure_files_readable')
|
||||||
flexmock(module).should_receive('make_pattern_flags').and_return(())
|
flexmock(module).should_receive('make_pattern_flags').and_return(())
|
||||||
|
@ -2146,6 +2260,8 @@ def test_create_archive_with_non_matching_source_directories_glob_passes_through
|
||||||
|
|
||||||
|
|
||||||
def test_create_archive_with_glob_calls_borg_with_expanded_directories():
|
def test_create_archive_with_glob_calls_borg_with_expanded_directories():
|
||||||
|
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||||
|
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||||
flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
|
flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
|
||||||
flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'food'))
|
flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'food'))
|
||||||
flexmock(module).should_receive('map_directories_to_devices').and_return({})
|
flexmock(module).should_receive('map_directories_to_devices').and_return({})
|
||||||
|
@ -2154,6 +2270,7 @@ def test_create_archive_with_glob_calls_borg_with_expanded_directories():
|
||||||
flexmock(module.os.path).should_receive('expanduser').and_raise(TypeError)
|
flexmock(module.os.path).should_receive('expanduser').and_raise(TypeError)
|
||||||
flexmock(module).should_receive('expand_home_directories').and_return(())
|
flexmock(module).should_receive('expand_home_directories').and_return(())
|
||||||
flexmock(module).should_receive('write_pattern_file').and_return(None)
|
flexmock(module).should_receive('write_pattern_file').and_return(None)
|
||||||
|
flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
|
||||||
flexmock(module.feature).should_receive('available').and_return(True)
|
flexmock(module.feature).should_receive('available').and_return(True)
|
||||||
flexmock(module).should_receive('ensure_files_readable')
|
flexmock(module).should_receive('ensure_files_readable')
|
||||||
flexmock(module).should_receive('make_pattern_flags').and_return(())
|
flexmock(module).should_receive('make_pattern_flags').and_return(())
|
||||||
|
@ -2185,6 +2302,8 @@ def test_create_archive_with_glob_calls_borg_with_expanded_directories():
|
||||||
|
|
||||||
|
|
||||||
def test_create_archive_with_archive_name_format_calls_borg_with_archive_name():
|
def test_create_archive_with_archive_name_format_calls_borg_with_archive_name():
|
||||||
|
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||||
|
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||||
flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
|
flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
|
||||||
flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
|
flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
|
||||||
flexmock(module).should_receive('map_directories_to_devices').and_return({})
|
flexmock(module).should_receive('map_directories_to_devices').and_return({})
|
||||||
|
@ -2193,6 +2312,7 @@ def test_create_archive_with_archive_name_format_calls_borg_with_archive_name():
|
||||||
flexmock(module.os.path).should_receive('expanduser').and_raise(TypeError)
|
flexmock(module.os.path).should_receive('expanduser').and_raise(TypeError)
|
||||||
flexmock(module).should_receive('expand_home_directories').and_return(())
|
flexmock(module).should_receive('expand_home_directories').and_return(())
|
||||||
flexmock(module).should_receive('write_pattern_file').and_return(None)
|
flexmock(module).should_receive('write_pattern_file').and_return(None)
|
||||||
|
flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
|
||||||
flexmock(module.feature).should_receive('available').and_return(True)
|
flexmock(module.feature).should_receive('available').and_return(True)
|
||||||
flexmock(module).should_receive('ensure_files_readable')
|
flexmock(module).should_receive('ensure_files_readable')
|
||||||
flexmock(module).should_receive('make_pattern_flags').and_return(())
|
flexmock(module).should_receive('make_pattern_flags').and_return(())
|
||||||
|
@ -2224,6 +2344,8 @@ def test_create_archive_with_archive_name_format_calls_borg_with_archive_name():
|
||||||
|
|
||||||
|
|
||||||
def test_create_archive_with_archive_name_format_accepts_borg_placeholders():
|
def test_create_archive_with_archive_name_format_accepts_borg_placeholders():
|
||||||
|
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||||
|
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||||
repository_archive_pattern = 'repo::Documents_{hostname}-{now}'
|
repository_archive_pattern = 'repo::Documents_{hostname}-{now}'
|
||||||
flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
|
flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
|
||||||
flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
|
flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
|
||||||
|
@ -2233,6 +2355,7 @@ def test_create_archive_with_archive_name_format_accepts_borg_placeholders():
|
||||||
flexmock(module.os.path).should_receive('expanduser').and_raise(TypeError)
|
flexmock(module.os.path).should_receive('expanduser').and_raise(TypeError)
|
||||||
flexmock(module).should_receive('expand_home_directories').and_return(())
|
flexmock(module).should_receive('expand_home_directories').and_return(())
|
||||||
flexmock(module).should_receive('write_pattern_file').and_return(None)
|
flexmock(module).should_receive('write_pattern_file').and_return(None)
|
||||||
|
flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
|
||||||
flexmock(module.feature).should_receive('available').and_return(True)
|
flexmock(module.feature).should_receive('available').and_return(True)
|
||||||
flexmock(module).should_receive('ensure_files_readable')
|
flexmock(module).should_receive('ensure_files_readable')
|
||||||
flexmock(module).should_receive('make_pattern_flags').and_return(())
|
flexmock(module).should_receive('make_pattern_flags').and_return(())
|
||||||
|
@ -2264,6 +2387,8 @@ def test_create_archive_with_archive_name_format_accepts_borg_placeholders():
|
||||||
|
|
||||||
|
|
||||||
def test_create_archive_with_repository_accepts_borg_placeholders():
|
def test_create_archive_with_repository_accepts_borg_placeholders():
|
||||||
|
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||||
|
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||||
repository_archive_pattern = '{fqdn}::Documents_{hostname}-{now}'
|
repository_archive_pattern = '{fqdn}::Documents_{hostname}-{now}'
|
||||||
flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
|
flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
|
||||||
flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
|
flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
|
||||||
|
@ -2273,6 +2398,7 @@ def test_create_archive_with_repository_accepts_borg_placeholders():
|
||||||
flexmock(module.os.path).should_receive('expanduser').and_raise(TypeError)
|
flexmock(module.os.path).should_receive('expanduser').and_raise(TypeError)
|
||||||
flexmock(module).should_receive('expand_home_directories').and_return(())
|
flexmock(module).should_receive('expand_home_directories').and_return(())
|
||||||
flexmock(module).should_receive('write_pattern_file').and_return(None)
|
flexmock(module).should_receive('write_pattern_file').and_return(None)
|
||||||
|
flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
|
||||||
flexmock(module.feature).should_receive('available').and_return(True)
|
flexmock(module.feature).should_receive('available').and_return(True)
|
||||||
flexmock(module).should_receive('ensure_files_readable')
|
flexmock(module).should_receive('ensure_files_readable')
|
||||||
flexmock(module).should_receive('make_pattern_flags').and_return(())
|
flexmock(module).should_receive('make_pattern_flags').and_return(())
|
||||||
|
@ -2304,6 +2430,8 @@ def test_create_archive_with_repository_accepts_borg_placeholders():
|
||||||
|
|
||||||
|
|
||||||
def test_create_archive_with_extra_borg_options_calls_borg_with_extra_options():
|
def test_create_archive_with_extra_borg_options_calls_borg_with_extra_options():
|
||||||
|
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||||
|
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||||
flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
|
flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
|
||||||
flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
|
flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
|
||||||
flexmock(module).should_receive('map_directories_to_devices').and_return({})
|
flexmock(module).should_receive('map_directories_to_devices').and_return({})
|
||||||
|
@ -2312,6 +2440,7 @@ def test_create_archive_with_extra_borg_options_calls_borg_with_extra_options():
|
||||||
flexmock(module.os.path).should_receive('expanduser').and_raise(TypeError)
|
flexmock(module.os.path).should_receive('expanduser').and_raise(TypeError)
|
||||||
flexmock(module).should_receive('expand_home_directories').and_return(())
|
flexmock(module).should_receive('expand_home_directories').and_return(())
|
||||||
flexmock(module).should_receive('write_pattern_file').and_return(None)
|
flexmock(module).should_receive('write_pattern_file').and_return(None)
|
||||||
|
flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
|
||||||
flexmock(module.feature).should_receive('available').and_return(True)
|
flexmock(module.feature).should_receive('available').and_return(True)
|
||||||
flexmock(module).should_receive('ensure_files_readable')
|
flexmock(module).should_receive('ensure_files_readable')
|
||||||
flexmock(module).should_receive('make_pattern_flags').and_return(())
|
flexmock(module).should_receive('make_pattern_flags').and_return(())
|
||||||
|
@ -2343,6 +2472,8 @@ def test_create_archive_with_extra_borg_options_calls_borg_with_extra_options():
|
||||||
|
|
||||||
|
|
||||||
def test_create_archive_with_stream_processes_calls_borg_with_processes_and_read_special():
|
def test_create_archive_with_stream_processes_calls_borg_with_processes_and_read_special():
|
||||||
|
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||||
|
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||||
processes = flexmock()
|
processes = flexmock()
|
||||||
flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
|
flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
|
||||||
flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
|
flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
|
||||||
|
@ -2352,6 +2483,7 @@ def test_create_archive_with_stream_processes_calls_borg_with_processes_and_read
|
||||||
flexmock(module.os.path).should_receive('expanduser').and_raise(TypeError)
|
flexmock(module.os.path).should_receive('expanduser').and_raise(TypeError)
|
||||||
flexmock(module).should_receive('expand_home_directories').and_return(())
|
flexmock(module).should_receive('expand_home_directories').and_return(())
|
||||||
flexmock(module).should_receive('write_pattern_file').and_return(None)
|
flexmock(module).should_receive('write_pattern_file').and_return(None)
|
||||||
|
flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
|
||||||
flexmock(module.feature).should_receive('available').and_return(True)
|
flexmock(module.feature).should_receive('available').and_return(True)
|
||||||
flexmock(module).should_receive('ensure_files_readable')
|
flexmock(module).should_receive('ensure_files_readable')
|
||||||
flexmock(module).should_receive('make_pattern_flags').and_return(())
|
flexmock(module).should_receive('make_pattern_flags').and_return(())
|
||||||
|
|
|
@ -21,6 +21,8 @@ def insert_execute_command_mock(
|
||||||
|
|
||||||
|
|
||||||
def test_export_tar_archive_calls_borg_with_path_parameters():
|
def test_export_tar_archive_calls_borg_with_path_parameters():
|
||||||
|
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||||
|
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||||
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
|
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
|
||||||
('repo::archive',)
|
('repo::archive',)
|
||||||
)
|
)
|
||||||
|
@ -41,6 +43,8 @@ def test_export_tar_archive_calls_borg_with_path_parameters():
|
||||||
|
|
||||||
|
|
||||||
def test_export_tar_archive_calls_borg_with_local_path_parameters():
|
def test_export_tar_archive_calls_borg_with_local_path_parameters():
|
||||||
|
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||||
|
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||||
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
|
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
|
||||||
('repo::archive',)
|
('repo::archive',)
|
||||||
)
|
)
|
||||||
|
@ -62,6 +66,8 @@ def test_export_tar_archive_calls_borg_with_local_path_parameters():
|
||||||
|
|
||||||
|
|
||||||
def test_export_tar_archive_calls_borg_with_remote_path_parameters():
|
def test_export_tar_archive_calls_borg_with_remote_path_parameters():
|
||||||
|
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||||
|
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||||
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
|
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
|
||||||
('repo::archive',)
|
('repo::archive',)
|
||||||
)
|
)
|
||||||
|
@ -83,6 +89,8 @@ def test_export_tar_archive_calls_borg_with_remote_path_parameters():
|
||||||
|
|
||||||
|
|
||||||
def test_export_tar_archive_calls_borg_with_umask_parameters():
|
def test_export_tar_archive_calls_borg_with_umask_parameters():
|
||||||
|
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||||
|
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||||
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
|
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
|
||||||
('repo::archive',)
|
('repo::archive',)
|
||||||
)
|
)
|
||||||
|
@ -103,6 +111,8 @@ def test_export_tar_archive_calls_borg_with_umask_parameters():
|
||||||
|
|
||||||
|
|
||||||
def test_export_tar_archive_calls_borg_with_lock_wait_parameters():
|
def test_export_tar_archive_calls_borg_with_lock_wait_parameters():
|
||||||
|
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||||
|
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||||
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
|
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
|
||||||
('repo::archive',)
|
('repo::archive',)
|
||||||
)
|
)
|
||||||
|
@ -123,6 +133,8 @@ def test_export_tar_archive_calls_borg_with_lock_wait_parameters():
|
||||||
|
|
||||||
|
|
||||||
def test_export_tar_archive_with_log_info_calls_borg_with_info_parameter():
|
def test_export_tar_archive_with_log_info_calls_borg_with_info_parameter():
|
||||||
|
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||||
|
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||||
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
|
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
|
||||||
('repo::archive',)
|
('repo::archive',)
|
||||||
)
|
)
|
||||||
|
@ -142,6 +154,8 @@ def test_export_tar_archive_with_log_info_calls_borg_with_info_parameter():
|
||||||
|
|
||||||
|
|
||||||
def test_export_tar_archive_with_log_debug_calls_borg_with_debug_parameters():
|
def test_export_tar_archive_with_log_debug_calls_borg_with_debug_parameters():
|
||||||
|
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||||
|
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||||
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
|
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
|
||||||
('repo::archive',)
|
('repo::archive',)
|
||||||
)
|
)
|
||||||
|
@ -163,6 +177,8 @@ def test_export_tar_archive_with_log_debug_calls_borg_with_debug_parameters():
|
||||||
|
|
||||||
|
|
||||||
def test_export_tar_archive_calls_borg_with_dry_run_parameter():
|
def test_export_tar_archive_calls_borg_with_dry_run_parameter():
|
||||||
|
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||||
|
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||||
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
|
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
|
||||||
('repo::archive',)
|
('repo::archive',)
|
||||||
)
|
)
|
||||||
|
@ -181,6 +197,8 @@ def test_export_tar_archive_calls_borg_with_dry_run_parameter():
|
||||||
|
|
||||||
|
|
||||||
def test_export_tar_archive_calls_borg_with_tar_filter_parameters():
|
def test_export_tar_archive_calls_borg_with_tar_filter_parameters():
|
||||||
|
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||||
|
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||||
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
|
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
|
||||||
('repo::archive',)
|
('repo::archive',)
|
||||||
)
|
)
|
||||||
|
@ -202,13 +220,15 @@ def test_export_tar_archive_calls_borg_with_tar_filter_parameters():
|
||||||
|
|
||||||
|
|
||||||
def test_export_tar_archive_calls_borg_with_list_parameter():
|
def test_export_tar_archive_calls_borg_with_list_parameter():
|
||||||
|
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||||
|
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||||
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
|
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
|
||||||
('repo::archive',)
|
('repo::archive',)
|
||||||
)
|
)
|
||||||
flexmock(module.os.path).should_receive('abspath').and_return('repo')
|
flexmock(module.os.path).should_receive('abspath').and_return('repo')
|
||||||
insert_execute_command_mock(
|
insert_execute_command_mock(
|
||||||
('borg', 'export-tar', '--list', 'repo::archive', 'test.tar'),
|
('borg', 'export-tar', '--list', 'repo::archive', 'test.tar'),
|
||||||
output_log_level=logging.WARNING,
|
output_log_level=logging.ANSWER,
|
||||||
)
|
)
|
||||||
|
|
||||||
module.export_tar_archive(
|
module.export_tar_archive(
|
||||||
|
@ -224,6 +244,8 @@ def test_export_tar_archive_calls_borg_with_list_parameter():
|
||||||
|
|
||||||
|
|
||||||
def test_export_tar_archive_calls_borg_with_strip_components_parameter():
|
def test_export_tar_archive_calls_borg_with_strip_components_parameter():
|
||||||
|
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||||
|
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||||
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
|
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
|
||||||
('repo::archive',)
|
('repo::archive',)
|
||||||
)
|
)
|
||||||
|
@ -245,6 +267,8 @@ def test_export_tar_archive_calls_borg_with_strip_components_parameter():
|
||||||
|
|
||||||
|
|
||||||
def test_export_tar_archive_skips_abspath_for_remote_repository_parameter():
|
def test_export_tar_archive_skips_abspath_for_remote_repository_parameter():
|
||||||
|
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||||
|
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||||
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
|
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
|
||||||
('server:repo::archive',)
|
('server:repo::archive',)
|
||||||
)
|
)
|
||||||
|
@ -263,6 +287,8 @@ def test_export_tar_archive_skips_abspath_for_remote_repository_parameter():
|
||||||
|
|
||||||
|
|
||||||
def test_export_tar_archive_calls_borg_with_stdout_destination_path():
|
def test_export_tar_archive_calls_borg_with_stdout_destination_path():
|
||||||
|
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||||
|
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||||
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
|
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
|
||||||
('repo::archive',)
|
('repo::archive',)
|
||||||
)
|
)
|
||||||
|
|
|
@ -15,13 +15,6 @@ def insert_execute_command_mock(command, working_directory=None):
|
||||||
).once()
|
).once()
|
||||||
|
|
||||||
|
|
||||||
def insert_execute_command_output_mock(command, result):
|
|
||||||
flexmock(module.environment).should_receive('make_environment')
|
|
||||||
flexmock(module).should_receive('execute_command').with_args(
|
|
||||||
command, output_log_level=None, borg_local_path=command[0], extra_environment=None,
|
|
||||||
).and_return(result).once()
|
|
||||||
|
|
||||||
|
|
||||||
def test_extract_last_archive_dry_run_calls_borg_with_last_archive():
|
def test_extract_last_archive_dry_run_calls_borg_with_last_archive():
|
||||||
flexmock(module.rlist).should_receive('resolve_archive_name').and_return('archive')
|
flexmock(module.rlist).should_receive('resolve_archive_name').and_return('archive')
|
||||||
insert_execute_command_mock(('borg', 'extract', '--dry-run', 'repo::archive'))
|
insert_execute_command_mock(('borg', 'extract', '--dry-run', 'repo::archive'))
|
||||||
|
@ -319,6 +312,57 @@ def test_extract_archive_calls_borg_with_strip_components():
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_extract_archive_calls_borg_with_strip_components_calculated_from_all():
|
||||||
|
flexmock(module.os.path).should_receive('abspath').and_return('repo')
|
||||||
|
insert_execute_command_mock(
|
||||||
|
(
|
||||||
|
'borg',
|
||||||
|
'extract',
|
||||||
|
'--strip-components',
|
||||||
|
'2',
|
||||||
|
'repo::archive',
|
||||||
|
'foo/bar/baz.txt',
|
||||||
|
'foo/bar.txt',
|
||||||
|
)
|
||||||
|
)
|
||||||
|
flexmock(module.feature).should_receive('available').and_return(True)
|
||||||
|
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
|
||||||
|
('repo::archive',)
|
||||||
|
)
|
||||||
|
|
||||||
|
module.extract_archive(
|
||||||
|
dry_run=False,
|
||||||
|
repository='repo',
|
||||||
|
archive='archive',
|
||||||
|
paths=['foo/bar/baz.txt', 'foo/bar.txt'],
|
||||||
|
location_config={},
|
||||||
|
storage_config={},
|
||||||
|
local_borg_version='1.2.3',
|
||||||
|
strip_components='all',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_extract_archive_with_strip_components_all_and_no_paths_raises():
|
||||||
|
flexmock(module.os.path).should_receive('abspath').and_return('repo')
|
||||||
|
flexmock(module.feature).should_receive('available').and_return(True)
|
||||||
|
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
|
||||||
|
('repo::archive',)
|
||||||
|
)
|
||||||
|
flexmock(module).should_receive('execute_command').never()
|
||||||
|
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
module.extract_archive(
|
||||||
|
dry_run=False,
|
||||||
|
repository='repo',
|
||||||
|
archive='archive',
|
||||||
|
paths=None,
|
||||||
|
location_config={},
|
||||||
|
storage_config={},
|
||||||
|
local_borg_version='1.2.3',
|
||||||
|
strip_components='all',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_extract_archive_calls_borg_with_progress_parameter():
|
def test_extract_archive_calls_borg_with_progress_parameter():
|
||||||
flexmock(module.os.path).should_receive('abspath').and_return('repo')
|
flexmock(module.os.path).should_receive('abspath').and_return('repo')
|
||||||
flexmock(module.environment).should_receive('make_environment')
|
flexmock(module.environment).should_receive('make_environment')
|
||||||
|
|
|
@ -9,13 +9,15 @@ from ..test_verbosity import insert_logging_mock
|
||||||
|
|
||||||
|
|
||||||
def test_display_archives_info_calls_borg_with_parameters():
|
def test_display_archives_info_calls_borg_with_parameters():
|
||||||
|
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||||
|
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||||
flexmock(module.flags).should_receive('make_flags').and_return(())
|
flexmock(module.flags).should_receive('make_flags').and_return(())
|
||||||
flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
|
flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
|
||||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
|
flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
|
||||||
flexmock(module.environment).should_receive('make_environment')
|
flexmock(module.environment).should_receive('make_environment')
|
||||||
flexmock(module).should_receive('execute_command').with_args(
|
flexmock(module).should_receive('execute_command').with_args(
|
||||||
('borg', 'info', '--repo', 'repo'),
|
('borg', 'info', '--repo', 'repo'),
|
||||||
output_log_level=logging.WARNING,
|
output_log_level=module.borgmatic.logger.ANSWER,
|
||||||
borg_local_path='borg',
|
borg_local_path='borg',
|
||||||
extra_environment=None,
|
extra_environment=None,
|
||||||
)
|
)
|
||||||
|
@ -29,13 +31,15 @@ def test_display_archives_info_calls_borg_with_parameters():
|
||||||
|
|
||||||
|
|
||||||
def test_display_archives_info_with_log_info_calls_borg_with_info_parameter():
|
def test_display_archives_info_with_log_info_calls_borg_with_info_parameter():
|
||||||
|
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||||
|
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||||
flexmock(module.flags).should_receive('make_flags').and_return(())
|
flexmock(module.flags).should_receive('make_flags').and_return(())
|
||||||
flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
|
flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
|
||||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
|
flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
|
||||||
flexmock(module.environment).should_receive('make_environment')
|
flexmock(module.environment).should_receive('make_environment')
|
||||||
flexmock(module).should_receive('execute_command').with_args(
|
flexmock(module).should_receive('execute_command').with_args(
|
||||||
('borg', 'info', '--info', '--repo', 'repo'),
|
('borg', 'info', '--info', '--repo', 'repo'),
|
||||||
output_log_level=logging.WARNING,
|
output_log_level=module.borgmatic.logger.ANSWER,
|
||||||
borg_local_path='borg',
|
borg_local_path='borg',
|
||||||
extra_environment=None,
|
extra_environment=None,
|
||||||
)
|
)
|
||||||
|
@ -49,15 +53,14 @@ def test_display_archives_info_with_log_info_calls_borg_with_info_parameter():
|
||||||
|
|
||||||
|
|
||||||
def test_display_archives_info_with_log_info_and_json_suppresses_most_borg_output():
|
def test_display_archives_info_with_log_info_and_json_suppresses_most_borg_output():
|
||||||
|
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||||
|
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||||
flexmock(module.flags).should_receive('make_flags').and_return(())
|
flexmock(module.flags).should_receive('make_flags').and_return(())
|
||||||
flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(('--json',))
|
flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(('--json',))
|
||||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
|
flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
|
||||||
flexmock(module.environment).should_receive('make_environment')
|
flexmock(module.environment).should_receive('make_environment')
|
||||||
flexmock(module).should_receive('execute_command').with_args(
|
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
|
||||||
('borg', 'info', '--json', '--repo', 'repo'),
|
('borg', 'info', '--json', '--repo', 'repo'), extra_environment=None,
|
||||||
output_log_level=None,
|
|
||||||
borg_local_path='borg',
|
|
||||||
extra_environment=None,
|
|
||||||
).and_return('[]')
|
).and_return('[]')
|
||||||
|
|
||||||
insert_logging_mock(logging.INFO)
|
insert_logging_mock(logging.INFO)
|
||||||
|
@ -72,13 +75,15 @@ def test_display_archives_info_with_log_info_and_json_suppresses_most_borg_outpu
|
||||||
|
|
||||||
|
|
||||||
def test_display_archives_info_with_log_debug_calls_borg_with_debug_parameter():
|
def test_display_archives_info_with_log_debug_calls_borg_with_debug_parameter():
|
||||||
|
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||||
|
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||||
flexmock(module.flags).should_receive('make_flags').and_return(())
|
flexmock(module.flags).should_receive('make_flags').and_return(())
|
||||||
flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
|
flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
|
||||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
|
flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
|
||||||
flexmock(module.environment).should_receive('make_environment')
|
flexmock(module.environment).should_receive('make_environment')
|
||||||
flexmock(module).should_receive('execute_command').with_args(
|
flexmock(module).should_receive('execute_command').with_args(
|
||||||
('borg', 'info', '--debug', '--show-rc', '--repo', 'repo'),
|
('borg', 'info', '--debug', '--show-rc', '--repo', 'repo'),
|
||||||
output_log_level=logging.WARNING,
|
output_log_level=module.borgmatic.logger.ANSWER,
|
||||||
borg_local_path='borg',
|
borg_local_path='borg',
|
||||||
extra_environment=None,
|
extra_environment=None,
|
||||||
)
|
)
|
||||||
|
@ -93,15 +98,14 @@ def test_display_archives_info_with_log_debug_calls_borg_with_debug_parameter():
|
||||||
|
|
||||||
|
|
||||||
def test_display_archives_info_with_log_debug_and_json_suppresses_most_borg_output():
|
def test_display_archives_info_with_log_debug_and_json_suppresses_most_borg_output():
|
||||||
|
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||||
|
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||||
flexmock(module.flags).should_receive('make_flags').and_return(())
|
flexmock(module.flags).should_receive('make_flags').and_return(())
|
||||||
flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(('--json',))
|
flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(('--json',))
|
||||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
|
flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
|
||||||
flexmock(module.environment).should_receive('make_environment')
|
flexmock(module.environment).should_receive('make_environment')
|
||||||
flexmock(module).should_receive('execute_command').with_args(
|
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
|
||||||
('borg', 'info', '--json', '--repo', 'repo'),
|
('borg', 'info', '--json', '--repo', 'repo'), extra_environment=None,
|
||||||
output_log_level=None,
|
|
||||||
borg_local_path='borg',
|
|
||||||
extra_environment=None,
|
|
||||||
).and_return('[]')
|
).and_return('[]')
|
||||||
|
|
||||||
insert_logging_mock(logging.DEBUG)
|
insert_logging_mock(logging.DEBUG)
|
||||||
|
@ -116,15 +120,14 @@ def test_display_archives_info_with_log_debug_and_json_suppresses_most_borg_outp
|
||||||
|
|
||||||
|
|
||||||
def test_display_archives_info_with_json_calls_borg_with_json_parameter():
|
def test_display_archives_info_with_json_calls_borg_with_json_parameter():
|
||||||
|
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||||
|
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||||
flexmock(module.flags).should_receive('make_flags').and_return(())
|
flexmock(module.flags).should_receive('make_flags').and_return(())
|
||||||
flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(('--json',))
|
flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(('--json',))
|
||||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
|
flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
|
||||||
flexmock(module.environment).should_receive('make_environment')
|
flexmock(module.environment).should_receive('make_environment')
|
||||||
flexmock(module).should_receive('execute_command').with_args(
|
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
|
||||||
('borg', 'info', '--json', '--repo', 'repo'),
|
('borg', 'info', '--json', '--repo', 'repo'), extra_environment=None,
|
||||||
output_log_level=None,
|
|
||||||
borg_local_path='borg',
|
|
||||||
extra_environment=None,
|
|
||||||
).and_return('[]')
|
).and_return('[]')
|
||||||
|
|
||||||
json_output = module.display_archives_info(
|
json_output = module.display_archives_info(
|
||||||
|
@ -138,6 +141,8 @@ def test_display_archives_info_with_json_calls_borg_with_json_parameter():
|
||||||
|
|
||||||
|
|
||||||
def test_display_archives_info_with_archive_calls_borg_with_match_archives_parameter():
|
def test_display_archives_info_with_archive_calls_borg_with_match_archives_parameter():
|
||||||
|
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||||
|
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||||
flexmock(module.flags).should_receive('make_flags').and_return(())
|
flexmock(module.flags).should_receive('make_flags').and_return(())
|
||||||
flexmock(module.flags).should_receive('make_flags').with_args(
|
flexmock(module.flags).should_receive('make_flags').with_args(
|
||||||
'match-archives', 'archive'
|
'match-archives', 'archive'
|
||||||
|
@ -147,7 +152,7 @@ def test_display_archives_info_with_archive_calls_borg_with_match_archives_param
|
||||||
flexmock(module.environment).should_receive('make_environment')
|
flexmock(module.environment).should_receive('make_environment')
|
||||||
flexmock(module).should_receive('execute_command').with_args(
|
flexmock(module).should_receive('execute_command').with_args(
|
||||||
('borg', 'info', '--repo', 'repo', '--match-archives', 'archive'),
|
('borg', 'info', '--repo', 'repo', '--match-archives', 'archive'),
|
||||||
output_log_level=logging.WARNING,
|
output_log_level=module.borgmatic.logger.ANSWER,
|
||||||
borg_local_path='borg',
|
borg_local_path='borg',
|
||||||
extra_environment=None,
|
extra_environment=None,
|
||||||
)
|
)
|
||||||
|
@ -161,13 +166,15 @@ def test_display_archives_info_with_archive_calls_borg_with_match_archives_param
|
||||||
|
|
||||||
|
|
||||||
def test_display_archives_info_with_local_path_calls_borg_via_local_path():
|
def test_display_archives_info_with_local_path_calls_borg_via_local_path():
|
||||||
|
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||||
|
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||||
flexmock(module.flags).should_receive('make_flags').and_return(())
|
flexmock(module.flags).should_receive('make_flags').and_return(())
|
||||||
flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
|
flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
|
||||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
|
flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
|
||||||
flexmock(module.environment).should_receive('make_environment')
|
flexmock(module.environment).should_receive('make_environment')
|
||||||
flexmock(module).should_receive('execute_command').with_args(
|
flexmock(module).should_receive('execute_command').with_args(
|
||||||
('borg1', 'info', '--repo', 'repo'),
|
('borg1', 'info', '--repo', 'repo'),
|
||||||
output_log_level=logging.WARNING,
|
output_log_level=module.borgmatic.logger.ANSWER,
|
||||||
borg_local_path='borg1',
|
borg_local_path='borg1',
|
||||||
extra_environment=None,
|
extra_environment=None,
|
||||||
)
|
)
|
||||||
|
@ -182,6 +189,8 @@ def test_display_archives_info_with_local_path_calls_borg_via_local_path():
|
||||||
|
|
||||||
|
|
||||||
def test_display_archives_info_with_remote_path_calls_borg_with_remote_path_parameters():
|
def test_display_archives_info_with_remote_path_calls_borg_with_remote_path_parameters():
|
||||||
|
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||||
|
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||||
flexmock(module.flags).should_receive('make_flags').and_return(())
|
flexmock(module.flags).should_receive('make_flags').and_return(())
|
||||||
flexmock(module.flags).should_receive('make_flags').with_args(
|
flexmock(module.flags).should_receive('make_flags').with_args(
|
||||||
'remote-path', 'borg1'
|
'remote-path', 'borg1'
|
||||||
|
@ -191,7 +200,7 @@ def test_display_archives_info_with_remote_path_calls_borg_with_remote_path_para
|
||||||
flexmock(module.environment).should_receive('make_environment')
|
flexmock(module.environment).should_receive('make_environment')
|
||||||
flexmock(module).should_receive('execute_command').with_args(
|
flexmock(module).should_receive('execute_command').with_args(
|
||||||
('borg', 'info', '--remote-path', 'borg1', '--repo', 'repo'),
|
('borg', 'info', '--remote-path', 'borg1', '--repo', 'repo'),
|
||||||
output_log_level=logging.WARNING,
|
output_log_level=module.borgmatic.logger.ANSWER,
|
||||||
borg_local_path='borg',
|
borg_local_path='borg',
|
||||||
extra_environment=None,
|
extra_environment=None,
|
||||||
)
|
)
|
||||||
|
@ -206,6 +215,8 @@ def test_display_archives_info_with_remote_path_calls_borg_with_remote_path_para
|
||||||
|
|
||||||
|
|
||||||
def test_display_archives_info_with_lock_wait_calls_borg_with_lock_wait_parameters():
|
def test_display_archives_info_with_lock_wait_calls_borg_with_lock_wait_parameters():
|
||||||
|
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||||
|
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||||
flexmock(module.flags).should_receive('make_flags').and_return(())
|
flexmock(module.flags).should_receive('make_flags').and_return(())
|
||||||
flexmock(module.flags).should_receive('make_flags').with_args('lock-wait', 5).and_return(
|
flexmock(module.flags).should_receive('make_flags').with_args('lock-wait', 5).and_return(
|
||||||
('--lock-wait', '5')
|
('--lock-wait', '5')
|
||||||
|
@ -216,7 +227,7 @@ def test_display_archives_info_with_lock_wait_calls_borg_with_lock_wait_paramete
|
||||||
flexmock(module.environment).should_receive('make_environment')
|
flexmock(module.environment).should_receive('make_environment')
|
||||||
flexmock(module).should_receive('execute_command').with_args(
|
flexmock(module).should_receive('execute_command').with_args(
|
||||||
('borg', 'info', '--lock-wait', '5', '--repo', 'repo'),
|
('borg', 'info', '--lock-wait', '5', '--repo', 'repo'),
|
||||||
output_log_level=logging.WARNING,
|
output_log_level=module.borgmatic.logger.ANSWER,
|
||||||
borg_local_path='borg',
|
borg_local_path='borg',
|
||||||
extra_environment=None,
|
extra_environment=None,
|
||||||
)
|
)
|
||||||
|
@ -230,6 +241,8 @@ def test_display_archives_info_with_lock_wait_calls_borg_with_lock_wait_paramete
|
||||||
|
|
||||||
|
|
||||||
def test_display_archives_info_with_prefix_calls_borg_with_match_archives_parameters():
|
def test_display_archives_info_with_prefix_calls_borg_with_match_archives_parameters():
|
||||||
|
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||||
|
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||||
flexmock(module.flags).should_receive('make_flags').and_return(())
|
flexmock(module.flags).should_receive('make_flags').and_return(())
|
||||||
flexmock(module.flags).should_receive('make_flags').with_args(
|
flexmock(module.flags).should_receive('make_flags').with_args(
|
||||||
'match-archives', 'sh:foo*'
|
'match-archives', 'sh:foo*'
|
||||||
|
@ -239,7 +252,7 @@ def test_display_archives_info_with_prefix_calls_borg_with_match_archives_parame
|
||||||
flexmock(module.environment).should_receive('make_environment')
|
flexmock(module.environment).should_receive('make_environment')
|
||||||
flexmock(module).should_receive('execute_command').with_args(
|
flexmock(module).should_receive('execute_command').with_args(
|
||||||
('borg', 'info', '--match-archives', 'sh:foo*', '--repo', 'repo'),
|
('borg', 'info', '--match-archives', 'sh:foo*', '--repo', 'repo'),
|
||||||
output_log_level=logging.WARNING,
|
output_log_level=module.borgmatic.logger.ANSWER,
|
||||||
borg_local_path='borg',
|
borg_local_path='borg',
|
||||||
extra_environment=None,
|
extra_environment=None,
|
||||||
)
|
)
|
||||||
|
@ -254,6 +267,8 @@ def test_display_archives_info_with_prefix_calls_borg_with_match_archives_parame
|
||||||
|
|
||||||
@pytest.mark.parametrize('argument_name', ('match_archives', 'sort_by', 'first', 'last'))
|
@pytest.mark.parametrize('argument_name', ('match_archives', 'sort_by', 'first', 'last'))
|
||||||
def test_display_archives_info_passes_through_arguments_to_borg(argument_name):
|
def test_display_archives_info_passes_through_arguments_to_borg(argument_name):
|
||||||
|
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||||
|
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||||
flag_name = f"--{argument_name.replace('_', ' ')}"
|
flag_name = f"--{argument_name.replace('_', ' ')}"
|
||||||
flexmock(module.flags).should_receive('make_flags').and_return(())
|
flexmock(module.flags).should_receive('make_flags').and_return(())
|
||||||
flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(
|
flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(
|
||||||
|
@ -263,7 +278,7 @@ def test_display_archives_info_passes_through_arguments_to_borg(argument_name):
|
||||||
flexmock(module.environment).should_receive('make_environment')
|
flexmock(module.environment).should_receive('make_environment')
|
||||||
flexmock(module).should_receive('execute_command').with_args(
|
flexmock(module).should_receive('execute_command').with_args(
|
||||||
('borg', 'info', flag_name, 'value', '--repo', 'repo'),
|
('borg', 'info', flag_name, 'value', '--repo', 'repo'),
|
||||||
output_log_level=logging.WARNING,
|
output_log_level=module.borgmatic.logger.ANSWER,
|
||||||
borg_local_path='borg',
|
borg_local_path='borg',
|
||||||
extra_environment=None,
|
extra_environment=None,
|
||||||
)
|
)
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue