Compare commits

...

129 Commits
1.9.0 ... main

Author SHA1 Message Date
cfff6c6855 Btrfs snapshotting (#251).
All checks were successful
build / test (push) Successful in 5m46s
build / docs (push) Successful in 1m38s
Reviewed-on: #946
2024-11-30 19:19:09 +00:00
37efaeae88 Warn if Btrfs is configured but there are no Btrfs subvolumes detected (#251). 2024-11-30 10:55:30 -08:00
0978c669ad A little more Btrfs error handling (#251). 2024-11-30 10:25:01 -08:00
1366269586 Add a couple of missing tests (#251). 2024-11-30 09:44:55 -08:00
a9a0910817 Add Btrfs logo to integrations docs (#251). 2024-11-30 09:36:52 -08:00
5bcc7b60c8 Tests for Btrfs (#251). 2024-11-30 09:32:50 -08:00
84a0552277 Improve Btrfs hook factoring/organization (#251). 2024-11-29 09:36:46 -08:00
d4a02f73b5 Create Btrfs snapshots as read-only (#251). 2024-11-28 22:18:44 -08:00
3f901c0a52 Btrfs hook documentation (#251). 2024-11-28 20:32:12 -08:00
b5b5c1fafa Initial work on a Btrfs hook (#251). 2024-11-28 18:47:15 -08:00
86e5085acc Fix incorrect documentation links to source.
All checks were successful
build / test (push) Successful in 4m5s
build / docs (push) Successful in 1m38s
2024-11-27 08:54:19 -08:00
08a5e8717b Merge branch 'main' of ssh://projects.torsion.org:3022/borgmatic-collective/borgmatic
Some checks failed
build / docs (push) Blocked by required conditions
build / test (push) Has been cancelled
2024-11-27 08:51:00 -08:00
6b2f2b2ac4 Reorganize data source and monitoring hooks to make developing new hooks easier. 2024-11-27 08:50:34 -08:00
a07cf9e699 Revert temporary reversion of 1.9.4.dev0.
All checks were successful
build / test (push) Successful in 4m9s
build / docs (push) Successful in 6s
revert Temporary revert of 1.9.4.dev0 changeset so we can re-build 1.9.3 (which never actually got built).

revert Fix library error when running within a PyInstaller bundle (#926).
2024-11-26 16:20:06 +00:00
bf40b01077 Temporary revert of 1.9.4.dev0 changeset so we can re-build 1.9.3 (which never actually got built).
All checks were successful
build / test (push) Successful in 4m8s
build / docs (push) Successful in 55s
revert Fix library error when running within a PyInstaller bundle (#926).
2024-11-26 16:13:39 +00:00
a5c6a2fe1c Fix library error when running within a PyInstaller bundle (#926).
All checks were successful
build / test (push) Successful in 5m47s
build / docs (push) Successful in 1m39s
2024-11-25 20:14:18 -08:00
82141fe981 Bump version for release. 2024-11-25 07:49:11 -08:00
228a83978d Check docs clarifications.
All checks were successful
build / test (push) Successful in 4m19s
build / docs (push) Successful in 1m38s
2024-11-24 19:40:00 -08:00
638db3770b Clarify how frequent default checks run.
Some checks failed
build / docs (push) Blocked by required conditions
build / test (push) Has been cancelled
2024-11-24 19:38:20 -08:00
98df5c3af2 Fix docs about the relative speeds of different checks (#945).
All checks were successful
build / test (push) Successful in 4m9s
build / docs (push) Successful in 1m2s
Reviewed-on: #945
2024-11-25 03:23:39 +00:00
b0e906c0e7 NEWS clarifications.
All checks were successful
build / test (push) Successful in 4m9s
build / docs (push) Successful in 59s
2024-11-24 19:05:11 -08:00
e8dccbf1c1 Promote the "spot" check from a beta feature to stable.
Some checks failed
build / docs (push) Blocked by required conditions
build / test (push) Has been cancelled
2024-11-24 19:03:05 -08:00
4a997bc234 Add rclone to the integrations.
Some checks failed
build / docs (push) Blocked by required conditions
build / test (push) Has been cancelled
2024-11-24 18:59:21 -08:00
3197178b3d Fix flake errors.
All checks were successful
build / test (push) Successful in 4m10s
build / docs (push) Successful in 1m9s
2024-11-24 16:32:48 -08:00
5e618154d0 Clarify error message.
Some checks failed
build / test (push) Failing after 1m50s
build / docs (push) Has been skipped
2024-11-24 16:18:29 -08:00
84f611ae4f Require the runtime directory to be an absolute path.
Some checks failed
build / test (push) Failing after 1m51s
build / docs (push) Has been skipped
2024-11-24 16:15:19 -08:00
5dc8450c8e Adding missing bootstrap files. 2024-11-24 16:15:12 -08:00
689643e5fa Move bootstrap manifest file creation into a hook so it can actually clean up after itself.
Some checks failed
build / test (push) Failing after 1m9s
build / docs (push) Has been skipped
2024-11-24 16:00:33 -08:00
0a3d87eaea
docs: repository check means full CRC check -> slow
Ref: https://borgbackup.readthedocs.io/en/stable/usage/check.html#description
2024-11-24 18:04:58 +01:00
b45b62cd38 Don't recursively snapshot ZFS datasets, since we're not mounting them recursively (#261).
All checks were successful
build / test (push) Successful in 4m9s
build / docs (push) Successful in 56s
2024-11-23 22:37:46 -08:00
8de7094691 ZFS snapshots (#261).
All checks were successful
build / test (push) Successful in 6m1s
build / docs (push) Successful in 1m40s
Reviewed-on: #944
2024-11-24 04:42:19 +00:00
8c7e68305e A few clarifications to the ZFS docs (#261). 2024-11-23 20:41:43 -08:00
65a323433c Add comment (#261). 2024-11-23 20:12:40 -08:00
b5a3589471 A little more error handling (#261). 2024-11-23 18:09:59 -08:00
f4a736bdfe Deduplicate directories again after hooks have their way with them (#261). 2024-11-23 14:33:41 -08:00
eab0ec15ef Expand ZFS NEWS entry (#261). 2024-11-23 11:46:39 -08:00
c65aa24001 Add test coverage for ZFS hook (#261). 2024-11-23 10:50:58 -08:00
5a24bf2037 Get tests passing (#261). 2024-11-22 20:16:18 -08:00
324dbc3a79 Swallow temporary directory removal errors (#261). 2024-11-22 10:56:07 -08:00
9fe7db320a Consider ZFS hook as a beta feature (#261). 2024-11-22 09:53:46 -08:00
4d19596616 Add ZFS documentation (#261). 2024-11-22 08:33:24 -08:00
5cec2bf3d9 Don't unmount directories that don't exist. 2024-11-21 22:16:05 -08:00
06e0f98fd8 More refactoring for better organization of ZFS hook (#261). 2024-11-21 22:09:18 -08:00
87f36caf8d Factoring out some utility functions (#261). 2024-11-21 20:17:57 -08:00
ab7acceff6 Unmount and remove mounted snapshot directories, not just for the current process but for previous borgmatic runs as well (#261). 2024-11-21 19:09:30 -08:00
1b2b0c3020 Update out-of-date ZFS hook comments (#261). 2024-11-21 16:49:25 -08:00
289d178581 Also support discovery of ZFS datasets tagged with a borgmatic-specific user property (#261). 2024-11-21 16:45:44 -08:00
1e7f6d9f41 ZFS hook support for borgmatic's --dry-run (#261). 2024-11-21 11:55:45 -08:00
d0c90389fb Remove ZFS "enabled" option and fix override command options. 2024-11-21 10:52:00 -08:00
f9e920dce9 Prevent ZFS snapshots from ending up in the Borg archive twice (#261). 2024-11-21 10:23:27 -08:00
0ed52bbc4a Proceed gracefully in ZFS data source removal if ZFS isn't installed (#261). 2024-11-21 08:59:59 -08:00
da8278b566 Use os.path.normpath() instead of custom list comprehension (#261). 2024-11-21 08:36:15 -08:00
2af3522902 Fix broken check action (#261). 2024-11-21 08:32:02 -08:00
5e4784991a Comment tweaks and additional TODOs (#261). 2024-11-20 22:33:23 -08:00
ab43ef00ce ZFS snapshots WIP (#261). 2024-11-20 22:21:27 -08:00
47a8a95b29 Test path fix for finding schema file.
All checks were successful
build / test (push) Successful in 4m7s
build / docs (push) Successful in 58s
2024-11-20 08:18:06 -08:00
7c90c04ce0 Add a "--deleted" flag to the "repo-list" action (Borg 2 only).
All checks were successful
build / test (push) Successful in 5m49s
build / docs (push) Successful in 1m39s
2024-11-19 22:33:15 -08:00
97305cc3ce Fix broken tests when NO_COLOR=1 is set (#943).
All checks were successful
build / test (push) Successful in 4m7s
build / docs (push) Successful in 56s
2024-11-19 08:48:21 -08:00
4985b805b4 Bump version for release. 2024-11-18 20:40:51 -08:00
d09b4c72a9 Fix a few remaining Pushover issues from the PR.
All checks were successful
build / test (push) Successful in 5m45s
build / docs (push) Successful in 1m39s
2024-11-18 20:32:17 -08:00
9807549f88
Add a Pushover monitoring hook.
Merge pull request #86 from tony1661/pushover-branch.
2024-11-18 20:17:28 -08:00
30c821120e Fix borgmatic ignoring the "BORG_RELOCATED_REPO_ACCESS_IS_OK" and "BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK" environment variables (#939).
All checks were successful
build / test (push) Successful in 4m5s
build / docs (push) Successful in 55s
2024-11-18 09:46:52 -08:00
13884bd448 Apply the "umask" option to all relevant actions, not just some of them (#441).
All checks were successful
build / test (push) Successful in 4m5s
build / docs (push) Successful in 56s
2024-11-18 09:07:29 -08:00
6bce4c4a0d changed version release to 1.9.2 2024-11-18 07:56:00 -05:00
25572c98d7 better fuction name 2024-11-18 07:52:19 -05:00
dab0dfcb32 added test for value error 2024-11-18 07:49:53 -05:00
851c454ef0 Remove the restriction that the "extract" and "mount" actions must match a single repository (#722).
All checks were successful
build / test (push) Successful in 5m47s
build / docs (push) Successful in 1m40s
2024-11-17 21:39:59 -08:00
c7a0cebaf7 Add a documentation link to NEWS (#934).
All checks were successful
build / test (push) Successful in 4m5s
build / docs (push) Successful in 56s
2024-11-17 12:46:58 -08:00
76cfeda290 Update the logic that probes for the borgmatic runtime directory to support more platforms and use cases (#934).
All checks were successful
build / test (push) Successful in 5m45s
build / docs (push) Successful in 1m40s
Reviewed-on: #937
2024-11-17 19:57:09 +00:00
afdf831c59 Fix broken restore/bootstrap when using Borg 1.2 and a randomly named temporary directory (#934). 2024-11-17 11:55:10 -08:00
9ac3087304 Before creating a temp file in a directory, make sure the directory exists (#934). 2024-11-17 10:30:17 -08:00
7cca83b698 Log the path of the borgmatic runtime directory used (#934). 2024-11-17 10:15:58 -08:00
4b5df7117a Fix documentation type (#934). 2024-11-17 09:01:58 -08:00
57decfa4db Document the fact that the config bootstrap feature writes to the runtime directory (#934). 2024-11-16 16:45:49 -08:00
b80f60a731 Create the borgmatic runtime directory if it doesn't exist (#934). 2024-11-16 16:03:18 -08:00
8f5ea95348 Fix use of borgmatic runtime directory in the restore action (#934). 2024-11-16 12:19:20 -08:00
b0cad58d6c Add a dash to the prefix of the randomly named temporary directory to improve readability (#937). 2024-11-16 11:26:24 -08:00
073d6bddf6 Fix outdated comment (#934). 2024-11-16 07:26:23 -08:00
810b65589f Documentation for runtime/state directory changes (#934). 2024-11-15 22:23:49 -08:00
295bfb0c57 Update the logic that probes for the borgmatic streaming database dump, bootstrap metadata, and check state directories to support more platforms and use cases (#934). 2024-11-15 18:15:32 -08:00
5f3d4f9b03 final fix for true/false and 1/0 2024-11-13 15:27:25 -05:00
5321301708 fix for true/false to 1/0 2024-11-13 15:23:47 -05:00
a939a66fb4 raise ValueError on priprity 2 with retry or expire 2024-11-13 15:20:52 -05:00
c0721a8cad Fix misleading example for user_runtime_directory.
All checks were successful
build / test (push) Successful in 5m45s
build / docs (push) Successful in 1m40s
2024-11-13 11:17:40 -08:00
ea47704d86 added missing tests to get 100% test coverage 2024-11-13 12:09:25 -05:00
61e4eeff6c converted html to a boolean and updated documentation and schema 2024-11-13 11:52:08 -05:00
3ab4b45041 added test for early exit when state is not in config 2024-11-13 09:07:47 -05:00
4e1f256b48 added dryrun test case with minimum config 2024-11-12 10:15:19 -05:00
96bb402837 fix test 2 2024-11-12 10:10:13 -05:00
97949266b3 fix test 2024-11-12 10:09:23 -05:00
e69d2385fc better name for test 2024-11-12 10:08:13 -05:00
6d9340ebb2 better comment blocks 2024-11-12 10:05:57 -05:00
0441e79b41 "fail" -> "fails" 2024-11-12 10:00:34 -05:00
b1af304125 better data dict creation 2024-11-12 09:59:02 -05:00
eb8f7e0329 better description for expire and retry 2024-11-12 09:55:07 -05:00
bf978f2db4 Fix missing build backend setting in pyproject.toml to allow Fedora builds (#932).
All checks were successful
build / test (push) Successful in 4m3s
build / docs (push) Successful in 56s
2024-11-10 17:09:10 -08:00
22776b123d Bump version for release.
All checks were successful
build / test (push) Successful in 10m24s
build / docs (push) Successful in 1m37s
2024-11-10 08:13:18 -08:00
ef66349674 small fixed for some failing tests 2024-11-08 20:02:29 -05:00
51b885e7db added global constant for priority 2024-11-08 19:49:23 -05:00
1781787305 better schema description for retry and expire 2024-11-08 19:43:40 -05:00
46ebb0cebb removed redundant code 2024-11-08 18:45:46 -05:00
3e0fa57860 removed tests that are not needed 2024-11-08 17:44:27 -05:00
59f8722e05 better spacing for comments 2024-11-08 17:42:46 -05:00
4ba42e8905 better wording. Added 'by default' 2024-11-08 17:41:18 -05:00
3b79482b24 better wording 2024-11-08 17:38:58 -05:00
7eb19cb0a7 added period 2024-11-08 17:38:20 -05:00
a4fabb8521 fix version 2024-11-08 17:37:26 -05:00
85ea8f4f45 fix 10min in seconds 2024-11-08 15:03:38 -05:00
290559116d better logic for priority 2024-11-08 15:01:28 -05:00
72b27b0858 better message description in schema 2024-11-08 14:57:44 -05:00
0fdee067c7 double space fix 2024-11-08 14:45:36 -05:00
0dca5eeafc fix title wordwrap 2024-11-08 14:09:03 -05:00
02ce3ba190 fix url_title word wrap 2024-11-08 14:07:49 -05:00
dc78bf4d6b fix TTL wordwrap 2024-11-08 14:06:39 -05:00
4b7fbce291 fix sound word wrap 2024-11-08 14:05:27 -05:00
1817b9a9ea fix wordwrap for html 2024-11-08 13:50:22 -05:00
009055c61a device description rewrap 2024-11-08 13:48:21 -05:00
54884da8fa priority word wrap 2024-11-08 13:46:19 -05:00
1177385e08 fix expire description 2024-11-08 13:44:12 -05:00
a45ba8553c removed duplicate type:object 2024-11-08 13:42:19 -05:00
d7d6e30178 moved checks from hook to schema 2024-11-08 13:40:23 -05:00
56304fdcad Add NEWS entry for multiple system credentials fix (#930).
All checks were successful
build / test (push) Successful in 4m10s
build / docs (push) Successful in 1m37s
2024-11-07 20:20:41 -08:00
3f75e9931f Only support a single systemd credential by default (#930).
Some checks failed
build / docs (push) Blocked by required conditions
build / test (push) Has been cancelled
Reviewed-on: #930
Reviewed-by: Dan Helfman <witten@torsion.org>
2024-11-08 04:18:56 +00:00
227f475e17 Fix an error when implicitly upgrading the check state directory across filesystems (#931).
All checks were successful
build / test (push) Successful in 5m48s
build / docs (push) Successful in 1m38s
2024-11-07 19:19:56 -08:00
467ddd0e93 creds: Only support single credential by default 2024-11-08 00:36:24 +01:00
be08e889f0 Fix the user runtime directory location on macOS (and possibly Cygwin) (#928).
All checks were successful
build / test (push) Successful in 6m31s
build / docs (push) Successful in 2m5s
2024-11-03 21:44:11 -08:00
94c8a56373 Reorder NEWS items. 2024-11-03 13:58:04 -08:00
94db527500 finalized support for Pushover 2024-10-30 15:43:06 -04:00
2849f54932 initial pushover commit 2024-10-30 11:25:26 -04:00
123 changed files with 6232 additions and 1844 deletions

43
NEWS
View File

@ -1,3 +1,44 @@
1.9.4.dev0
* #251 (beta): Add a Btrfs hook for snapshotting and backing up Btrfs subvolumes. See the
documentation for more information:
https://torsion.org/borgmatic/docs/how-to/snapshot-your-filesystems/
* #926: Fix library error when running within a PyInstaller bundle.
* Reorganize data source and monitoring hooks to make developing new hooks easier.
1.9.3
* #261 (beta): Add a ZFS hook for snapshotting and backing up ZFS datasets. See the documentation
for more information: https://torsion.org/borgmatic/docs/how-to/snapshot-your-filesystems/
* Remove any temporary copies of the manifest file created in support of the "bootstrap" action.
* Deprecate the "store_config_files" option at the global scope and move it under the "bootstrap"
hook. See the documentation for more information:
https://torsion.org/borgmatic/docs/how-to/extract-a-backup/#extract-the-configuration-files-used-to-create-an-archive
* Require the runtime directory to be an absolute path.
* Add a "--deleted" flag to the "repo-list" action for listing deleted archives that haven't
yet been compacted (Borg 2 only).
* Promote the "spot" check from a beta feature to stable.
1.9.2
* #441: Apply the "umask" option to all relevant actions, not just some of them.
* #722: Remove the restriction that the "extract" and "mount" actions must match a single
repository. Now they work more like other actions, where each repository is applied in turn.
* #932: Fix the missing build backend setting in pyproject.toml to allow Fedora builds.
* #934: Update the logic that probes for the borgmatic streaming database dump, bootstrap
metadata, and check state directories to support more platforms and use cases. See the
documentation for more information:
https://torsion.org/borgmatic/docs/how-to/backup-your-databases/#runtime-directory
* #934: Add the "RuntimeDirectory" and "StateDirectory" options to the sample systemd service
file to support the new runtime and state directory logic.
* #939: Fix borgmatic ignoring the "BORG_RELOCATED_REPO_ACCESS_IS_OK" and
"BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK" environment variables.
* Add a Pushover monitoring hook. See the documentation for more information:
https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#pushover-hook
1.9.1
* #928: Fix the user runtime directory location on macOS (and possibly Cygwin).
* #930: Fix an error with the sample systemd service when no credentials are configured.
* #931: Fix an error when implicitly upgrading the check state directory from ~/.borgmatic to
~/.local/state/borgmatic across filesystems.
1.9.0
* #609: Fix the glob expansion of "source_directories" values to respect the "working_directory"
option.
@ -20,6 +61,7 @@
cases.
* #902: Add loading of encrypted systemd credentials. See the documentation for more information:
https://torsion.org/borgmatic/docs/how-to/provide-your-passwords/#using-systemd-service-credentials
* #911: Add a "key change-passphrase" action to change the passphrase protecting a repository key.
* #914: Fix a confusing apparent hang when when the repository location changes, and instead
show a helpful error message.
* #915: BREAKING: Rename repository actions like "rcreate" to more explicit names like
@ -31,7 +73,6 @@
* #919: Clarify the command-line help for the "--config" flag.
* #919: Document a policy for versioning and breaking changes:
https://torsion.org/borgmatic/docs/how-to/upgrade/#versioning-and-breaking-changes
* #911: Add a "key change-passphrase" action to change the passphrase protecting a repository key.
* #921: BREAKING: Change soft failure command hooks to skip only the current repository rather than
all repositories in the configuration file.
* #922: Replace setup.py (Python packaging metadata) with the more modern pyproject.toml.

View File

@ -61,11 +61,15 @@ borgmatic is powered by [Borg Backup](https://www.borgbackup.org/).
<a href="https://mariadb.com/"><img src="docs/static/mariadb.png" alt="MariaDB" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
<a href="https://www.mongodb.com/"><img src="docs/static/mongodb.png" alt="MongoDB" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
<a href="https://sqlite.org/"><img src="docs/static/sqlite.png" alt="SQLite" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
<a href="https://openzfs.org/"><img src="docs/static/openzfs.png" alt="OpenZFS" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
<a href="https://btrfs.readthedocs.io/"><img src="docs/static/btrfs.png" alt="Btrfs" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
<a href="https://rclone.org"><img src="docs/static/rclone.png" alt="rclone" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
<a href="https://healthchecks.io/"><img src="docs/static/healthchecks.png" alt="Healthchecks" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
<a href="https://uptime.kuma.pet/"><img src="docs/static/uptimekuma.png" alt="Uptime Kuma" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
<a href="https://cronitor.io/"><img src="docs/static/cronitor.png" alt="Cronitor" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
<a href="https://cronhub.io/"><img src="docs/static/cronhub.png" alt="Cronhub" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
<a href="https://www.pagerduty.com/"><img src="docs/static/pagerduty.png" alt="PagerDuty" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
<a href="https://www.pushover.net/"><img src="docs/static/pushover.png" alt="Pushover" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
<a href="https://ntfy.sh/"><img src="docs/static/ntfy.png" alt="ntfy" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
<a href="https://grafana.com/oss/loki/"><img src="docs/static/loki.png" alt="Loki" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
<a href="https://github.com/caronc/apprise/wiki"><img src="docs/static/apprise.png" alt="Apprise" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>

View File

@ -6,7 +6,9 @@ import logging
import os
import pathlib
import random
import shutil
import borgmatic.actions.create
import borgmatic.borg.check
import borgmatic.borg.create
import borgmatic.borg.environment
@ -322,7 +324,7 @@ def upgrade_check_times(config, borg_repository_id):
f'Upgrading archives check times directory from {borgmatic_source_checks_path} to {borgmatic_state_checks_path}'
)
os.makedirs(borgmatic_state_path, mode=0o700, exist_ok=True)
os.rename(borgmatic_source_checks_path, borgmatic_state_checks_path)
shutil.move(borgmatic_source_checks_path, borgmatic_state_checks_path)
for check_type in ('archives', 'data'):
new_path = make_check_time_path(config, borg_repository_id, check_type, 'all')
@ -335,16 +337,22 @@ def upgrade_check_times(config, borg_repository_id):
logger.debug(f'Upgrading archives check time file from {old_path} to {new_path}')
try:
os.rename(old_path, temporary_path)
shutil.move(old_path, temporary_path)
except FileNotFoundError:
pass
os.mkdir(old_path)
os.rename(temporary_path, new_path)
shutil.move(temporary_path, new_path)
def collect_spot_check_source_paths(
repository, config, local_borg_version, global_arguments, local_path, remote_path
repository,
config,
local_borg_version,
global_arguments,
local_path,
remote_path,
borgmatic_runtime_directory,
):
'''
Given a repository configuration dict, a configuration dict, the local Borg version, global
@ -356,7 +364,7 @@ def collect_spot_check_source_paths(
'use_streaming',
config,
repository['path'],
borgmatic.hooks.dump.DATA_SOURCE_HOOK_NAMES,
borgmatic.hooks.dispatch.Hook_type.DATA_SOURCE,
).values()
)
@ -365,10 +373,12 @@ def collect_spot_check_source_paths(
dry_run=True,
repository_path=repository['path'],
config=config,
config_paths=(),
source_directories=borgmatic.actions.create.process_source_directories(
config,
),
local_borg_version=local_borg_version,
global_arguments=global_arguments,
borgmatic_runtime_directories=(),
borgmatic_runtime_directory=borgmatic_runtime_directory,
local_path=local_path,
remote_path=remote_path,
list_files=True,
@ -402,16 +412,22 @@ BORG_DIRECTORY_FILE_TYPE = 'd'
def collect_spot_check_archive_paths(
repository, archive, config, local_borg_version, global_arguments, local_path, remote_path
repository,
archive,
config,
local_borg_version,
global_arguments,
local_path,
remote_path,
borgmatic_runtime_directory,
):
'''
Given a repository configuration dict, the name of the latest archive, a configuration dict, the
local Borg version, global arguments as an argparse.Namespace instance, the local Borg path, and
the remote Borg path, collect the paths from the given archive (but only include files and
symlinks and exclude borgmatic runtime directories).
local Borg version, global arguments as an argparse.Namespace instance, the local Borg path, the
remote Borg path, and the borgmatic runtime directory, collect the paths from the given archive
(but only include files and symlinks and exclude borgmatic runtime directories).
'''
borgmatic_source_directory = borgmatic.config.paths.get_borgmatic_source_directory(config)
borgmatic_runtime_directory = borgmatic.config.paths.get_borgmatic_runtime_directory(config)
return tuple(
path
@ -444,7 +460,7 @@ def compare_spot_check_hashes(
global_arguments,
local_path,
remote_path,
log_label,
log_prefix,
source_paths,
):
'''
@ -468,7 +484,7 @@ def compare_spot_check_hashes(
if os.path.exists(os.path.join(working_directory or '', source_path))
}
logger.debug(
f'{log_label}: Sampling {sample_count} source paths (~{spot_check_config["data_sample_percentage"]}%) for spot check'
f'{log_prefix}: Sampling {sample_count} source paths (~{spot_check_config["data_sample_percentage"]}%) for spot check'
)
source_sample_paths_iterator = iter(source_sample_paths)
@ -545,18 +561,19 @@ def spot_check(
global_arguments,
local_path,
remote_path,
borgmatic_runtime_directory,
):
'''
Given a repository dict, a loaded configuration dict, the local Borg version, global arguments
as an argparse.Namespace instance, the local Borg path, and the remote Borg path, perform a spot
check for the latest archive in the given repository.
as an argparse.Namespace instance, the local Borg path, the remote Borg path, and the borgmatic
runtime directory, perform a spot check for the latest archive in the given repository.
A spot check compares file counts and also the hashes for a random sampling of source files on
disk to those stored in the latest archive. If any differences are beyond configured tolerances,
then the check fails.
'''
log_label = f'{repository.get("label", repository["path"])}'
logger.debug(f'{log_label}: Running spot check')
log_prefix = f'{repository.get("label", repository["path"])}'
logger.debug(f'{log_prefix}: Running spot check')
try:
spot_check_config = next(
@ -577,8 +594,9 @@ def spot_check(
global_arguments,
local_path,
remote_path,
borgmatic_runtime_directory,
)
logger.debug(f'{log_label}: {len(source_paths)} total source paths for spot check')
logger.debug(f'{log_prefix}: {len(source_paths)} total source paths for spot check')
archive = borgmatic.borg.repo_list.resolve_archive_name(
repository['path'],
@ -589,7 +607,7 @@ def spot_check(
local_path,
remote_path,
)
logger.debug(f'{log_label}: Using archive {archive} for spot check')
logger.debug(f'{log_prefix}: Using archive {archive} for spot check')
archive_paths = collect_spot_check_archive_paths(
repository,
@ -599,8 +617,9 @@ def spot_check(
global_arguments,
local_path,
remote_path,
borgmatic_runtime_directory,
)
logger.debug(f'{log_label}: {len(archive_paths)} total archive paths for spot check')
logger.debug(f'{log_prefix}: {len(archive_paths)} total archive paths for spot check')
# Calculate the percentage delta between the source paths count and the archive paths count, and
# compare that delta to the configured count tolerance percentage.
@ -608,10 +627,10 @@ def spot_check(
if count_delta_percentage > spot_check_config['count_tolerance_percentage']:
logger.debug(
f'{log_label}: Paths in source paths but not latest archive: {", ".join(set(source_paths) - set(archive_paths)) or "none"}'
f'{log_prefix}: Paths in source paths but not latest archive: {", ".join(set(source_paths) - set(archive_paths)) or "none"}'
)
logger.debug(
f'{log_label}: Paths in latest archive but not source paths: {", ".join(set(archive_paths) - set(source_paths)) or "none"}'
f'{log_prefix}: Paths in latest archive but not source paths: {", ".join(set(archive_paths) - set(source_paths)) or "none"}'
)
raise ValueError(
f'Spot check failed: {count_delta_percentage:.2f}% file count delta between source paths and latest archive (tolerance is {spot_check_config["count_tolerance_percentage"]}%)'
@ -625,25 +644,25 @@ def spot_check(
global_arguments,
local_path,
remote_path,
log_label,
log_prefix,
source_paths,
)
# Error if the percentage of failing hashes exceeds the configured tolerance percentage.
logger.debug(f'{log_label}: {len(failing_paths)} non-matching spot check hashes')
logger.debug(f'{log_prefix}: {len(failing_paths)} non-matching spot check hashes')
data_tolerance_percentage = spot_check_config['data_tolerance_percentage']
failing_percentage = (len(failing_paths) / len(source_paths)) * 100
if failing_percentage > data_tolerance_percentage:
logger.debug(
f'{log_label}: Source paths with data not matching the latest archive: {", ".join(failing_paths)}'
f'{log_prefix}: Source paths with data not matching the latest archive: {", ".join(failing_paths)}'
)
raise ValueError(
f'Spot check failed: {failing_percentage:.2f}% of source paths with data not matching the latest archive (tolerance is {data_tolerance_percentage}%)'
)
logger.info(
f'{log_label}: Spot check passed with a {count_delta_percentage:.2f}% file count delta and a {failing_percentage:.2f}% file data delta'
f'{log_prefix}: Spot check passed with a {count_delta_percentage:.2f}% file count delta and a {failing_percentage:.2f}% file data delta'
)
@ -677,7 +696,9 @@ def run_check(
**hook_context,
)
logger.info(f'{repository.get("label", repository["path"])}: Running consistency checks')
log_prefix = repository.get('label', repository['path'])
logger.info(f'{log_prefix}: Running consistency checks')
repository_id = borgmatic.borg.check.get_repository_id(
repository['path'],
config,
@ -729,14 +750,18 @@ def run_check(
write_check_time(make_check_time_path(config, repository_id, 'extract'))
if 'spot' in checks:
spot_check(
repository,
config,
local_borg_version,
global_arguments,
local_path,
remote_path,
)
with borgmatic.config.paths.Runtime_directory(
config, log_prefix
) as borgmatic_runtime_directory:
spot_check(
repository,
config,
local_borg_version,
global_arguments,
local_path,
remote_path,
borgmatic_runtime_directory,
)
write_check_time(make_check_time_path(config, repository_id, 'spot'))
borgmatic.hooks.command.execute_hook(

View File

@ -38,37 +38,44 @@ def get_config_paths(archive_name, bootstrap_arguments, global_arguments, local_
borgmatic_source_directory = borgmatic.config.paths.get_borgmatic_source_directory(
{'borgmatic_source_directory': bootstrap_arguments.borgmatic_source_directory}
)
borgmatic_runtime_directory = borgmatic.config.paths.get_borgmatic_runtime_directory(
{'user_runtime_directory': bootstrap_arguments.user_runtime_directory}
)
config = make_bootstrap_config(bootstrap_arguments)
# Probe for the manifest file in multiple locations, as the default location has moved to the
# borgmatic runtime directory (which get stored as just "/borgmatic" with Borg 1.4+). But we
# still want to support reading the manifest from previously created archives as well.
for base_directory in ('borgmatic', borgmatic_runtime_directory, borgmatic_source_directory):
borgmatic_manifest_path = os.path.join(base_directory, 'bootstrap', 'manifest.json')
with borgmatic.config.paths.Runtime_directory(
{'user_runtime_directory': bootstrap_arguments.user_runtime_directory},
bootstrap_arguments.repository,
) as borgmatic_runtime_directory:
for base_directory in (
'borgmatic',
borgmatic.config.paths.make_runtime_directory_glob(borgmatic_runtime_directory),
borgmatic_source_directory,
):
borgmatic_manifest_path = 'sh:' + os.path.join(
base_directory, 'bootstrap', 'manifest.json'
)
extract_process = borgmatic.borg.extract.extract_archive(
global_arguments.dry_run,
bootstrap_arguments.repository,
archive_name,
[borgmatic_manifest_path],
config,
local_borg_version,
global_arguments,
local_path=bootstrap_arguments.local_path,
remote_path=bootstrap_arguments.remote_path,
extract_to_stdout=True,
)
manifest_json = extract_process.stdout.read()
extract_process = borgmatic.borg.extract.extract_archive(
global_arguments.dry_run,
bootstrap_arguments.repository,
archive_name,
[borgmatic_manifest_path],
config,
local_borg_version,
global_arguments,
local_path=bootstrap_arguments.local_path,
remote_path=bootstrap_arguments.remote_path,
extract_to_stdout=True,
)
manifest_json = extract_process.stdout.read()
if manifest_json:
break
else:
raise ValueError(
'Cannot read configuration paths from archive due to missing bootstrap manifest'
)
if manifest_json:
break
else:
raise ValueError(
'Cannot read configuration paths from archive due to missing bootstrap manifest'
)
try:
manifest_data = json.loads(manifest_json)

View File

@ -1,7 +1,8 @@
import importlib.metadata
import json
import glob
import itertools
import logging
import os
import pathlib
import borgmatic.actions.json
import borgmatic.borg.create
@ -9,35 +10,137 @@ import borgmatic.config.paths
import borgmatic.config.validate
import borgmatic.hooks.command
import borgmatic.hooks.dispatch
import borgmatic.hooks.dump
logger = logging.getLogger(__name__)
def create_borgmatic_manifest(config, config_paths, dry_run):
def expand_directory(directory, working_directory):
'''
Create a borgmatic manifest file to store the paths to the configuration files used to create
the archive.
Given a directory path, expand any tilde (representing a user's home directory) and any globs
therein. Return a list of one or more resulting paths.
'''
if dry_run:
return
expanded_directory = os.path.join(working_directory or '', os.path.expanduser(directory))
borgmatic_runtime_directory = borgmatic.config.paths.get_borgmatic_runtime_directory(config)
borgmatic_manifest_path = os.path.join(
borgmatic_runtime_directory, 'bootstrap', 'manifest.json'
return glob.glob(expanded_directory) or [expanded_directory]
def expand_directories(directories, working_directory=None):
'''
Given a sequence of directory paths and an optional working directory, expand tildes and globs
in each one. Return all the resulting directories as a single flattened tuple.
'''
if directories is None:
return ()
return tuple(
itertools.chain.from_iterable(
expand_directory(directory, working_directory) for directory in directories
)
)
if not os.path.exists(borgmatic_manifest_path):
os.makedirs(os.path.dirname(borgmatic_manifest_path), exist_ok=True)
with open(borgmatic_manifest_path, 'w') as config_list_file:
json.dump(
{
'borgmatic_version': importlib.metadata.version('borgmatic'),
'config_paths': config_paths,
},
config_list_file,
)
def map_directories_to_devices(directories, working_directory=None):
'''
Given a sequence of directories and an optional working directory, return a map from directory
to an identifier for the device on which that directory resides or None if the path doesn't
exist.
This is handy for determining whether two different directories are on the same filesystem (have
the same device identifier).
'''
return {
directory: os.stat(full_directory).st_dev if os.path.exists(full_directory) else None
for directory in directories
for full_directory in (os.path.join(working_directory or '', directory),)
}
def deduplicate_directories(directory_devices, additional_directory_devices):
'''
Given a map from directory to the identifier for the device on which that directory resides,
return the directories as a sorted sequence with all duplicate child directories removed. For
instance, if paths is ['/foo', '/foo/bar'], return just: ['/foo']
The one exception to this rule is if two paths are on different filesystems (devices). In that
case, they won't get de-duplicated in case they both need to be passed to Borg (e.g. the
location.one_file_system option is true).
The idea is that if Borg is given a parent directory, then it doesn't also need to be given
child directories, because it will naturally spider the contents of the parent directory. And
there are cases where Borg coming across the same file twice will result in duplicate reads and
even hangs, e.g. when a database hook is using a named pipe for streaming database dumps to
Borg.
If any additional directory devices are given, also deduplicate against them, but don't include
them in the returned directories.
'''
deduplicated = set()
directories = sorted(directory_devices.keys())
additional_directories = sorted(additional_directory_devices.keys())
all_devices = {**directory_devices, **additional_directory_devices}
for directory in directories:
deduplicated.add(directory)
parents = pathlib.PurePath(directory).parents
# If another directory in the given list (or the additional list) is a parent of current
# directory (even n levels up) and both are on the same filesystem, then the current
# directory is a duplicate.
for other_directory in directories + additional_directories:
for parent in parents:
if (
pathlib.PurePath(other_directory) == parent
and all_devices[directory] is not None
and all_devices[other_directory] == all_devices[directory]
):
if directory in deduplicated:
deduplicated.remove(directory)
break
return sorted(deduplicated)
ROOT_PATTERN_PREFIX = 'R '
def pattern_root_directories(patterns=None):
'''
Given a sequence of patterns, parse out and return just the root directories.
'''
if not patterns:
return []
return [
pattern.split(ROOT_PATTERN_PREFIX, maxsplit=1)[1]
for pattern in patterns
if pattern.startswith(ROOT_PATTERN_PREFIX)
]
def process_source_directories(config, source_directories=None):
'''
Given a sequence of source directories (either in the source_directories argument or, lacking
that, from config), expand and deduplicate the source directories, returning the result.
'''
working_directory = borgmatic.config.paths.get_working_directory(config)
if source_directories is None:
source_directories = tuple(config.get('source_directories', ()))
return deduplicate_directories(
map_directories_to_devices(
expand_directories(
tuple(source_directories),
working_directory=working_directory,
)
),
additional_directory_devices=map_directories_to_devices(
expand_directories(
pattern_root_directories(config.get('patterns')),
working_directory=working_directory,
)
),
)
def run_create(
@ -71,54 +174,68 @@ def run_create(
global_arguments.dry_run,
**hook_context,
)
logger.info(f'{repository.get("label", repository["path"])}: Creating archive{dry_run_label}')
borgmatic.hooks.dispatch.call_hooks_even_if_unconfigured(
'remove_data_source_dumps',
config,
repository['path'],
borgmatic.hooks.dump.DATA_SOURCE_HOOK_NAMES,
global_arguments.dry_run,
)
active_dumps = borgmatic.hooks.dispatch.call_hooks(
'dump_data_sources',
config,
repository['path'],
borgmatic.hooks.dump.DATA_SOURCE_HOOK_NAMES,
global_arguments.dry_run,
)
if config.get('store_config_files', True):
create_borgmatic_manifest(
log_prefix = repository.get('label', repository['path'])
logger.info(f'{log_prefix}: Creating archive{dry_run_label}')
with borgmatic.config.paths.Runtime_directory(
config, log_prefix
) as borgmatic_runtime_directory:
borgmatic.hooks.dispatch.call_hooks_even_if_unconfigured(
'remove_data_source_dumps',
config,
config_paths,
repository['path'],
borgmatic.hooks.dispatch.Hook_type.DATA_SOURCE,
borgmatic_runtime_directory,
global_arguments.dry_run,
)
source_directories = process_source_directories(config)
active_dumps = borgmatic.hooks.dispatch.call_hooks(
'dump_data_sources',
config,
repository['path'],
borgmatic.hooks.dispatch.Hook_type.DATA_SOURCE,
config_paths,
borgmatic_runtime_directory,
source_directories,
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['path'],
config,
config_paths,
local_borg_version,
global_arguments,
local_path=local_path,
remote_path=remote_path,
progress=create_arguments.progress,