Compare commits

...
228 Commits
Author SHA1 Message Date
Dan Helfman 4985b805b4 Bump version for release. 2024-11-18 20:40:51 -08:00
Dan Helfman d09b4c72a9 Fix a few remaining Pushover issues from the PR. 2024-11-18 20:32:17 -08:00
Dan HelfmanandGitHub 9807549f88 Add a Pushover monitoring hook.
Merge pull request #86 from tony1661/pushover-branch.
2024-11-18 20:17:28 -08:00
Dan Helfman 30c821120e Fix borgmatic ignoring the "BORG_RELOCATED_REPO_ACCESS_IS_OK" and "BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK" environment variables (#939). 2024-11-18 09:46:52 -08:00
Dan Helfman 13884bd448 Apply the "umask" option to all relevant actions, not just some of them (#441). 2024-11-18 09:07:29 -08:00
Antonio Fernandez 6bce4c4a0d changed version release to 1.9.2 2024-11-18 07:56:00 -05:00
Antonio Fernandez 25572c98d7 better fuction name 2024-11-18 07:52:19 -05:00
Antonio Fernandez dab0dfcb32 added test for value error 2024-11-18 07:49:53 -05:00
Dan Helfman 851c454ef0 Remove the restriction that the "extract" and "mount" actions must match a single repository (#722). 2024-11-17 21:39:59 -08:00
Dan Helfman c7a0cebaf7 Add a documentation link to NEWS (#934). 2024-11-17 12:46:58 -08:00
Dan Helfman 76cfeda290 Update the logic that probes for the borgmatic runtime directory to support more platforms and use cases (#934).
Reviewed-on: https://projects.torsion.org/borgmatic-collective/borgmatic/pulls/937
2024-11-17 19:57:09 +00:00
Dan Helfman 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
Dan Helfman 9ac3087304 Before creating a temp file in a directory, make sure the directory exists (#934). 2024-11-17 10:30:17 -08:00
Dan Helfman 7cca83b698 Log the path of the borgmatic runtime directory used (#934). 2024-11-17 10:15:58 -08:00
Dan Helfman 4b5df7117a Fix documentation type (#934). 2024-11-17 09:01:58 -08:00
Dan Helfman 57decfa4db Document the fact that the config bootstrap feature writes to the runtime directory (#934). 2024-11-16 16:45:49 -08:00
Dan Helfman b80f60a731 Create the borgmatic runtime directory if it doesn't exist (#934). 2024-11-16 16:03:18 -08:00
Dan Helfman 8f5ea95348 Fix use of borgmatic runtime directory in the restore action (#934). 2024-11-16 12:19:20 -08:00
Dan Helfman 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
Dan Helfman 073d6bddf6 Fix outdated comment (#934). 2024-11-16 07:26:23 -08:00
Dan Helfman 810b65589f Documentation for runtime/state directory changes (#934). 2024-11-15 22:23:49 -08:00
Dan Helfman 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
Antonio Fernandez 5f3d4f9b03 final fix for true/false and 1/0 2024-11-13 15:27:25 -05:00
Antonio Fernandez 5321301708 fix for true/false to 1/0 2024-11-13 15:23:47 -05:00
Antonio Fernandez a939a66fb4 raise ValueError on priprity 2 with retry or expire 2024-11-13 15:20:52 -05:00
Dan Helfman c0721a8cad Fix misleading example for user_runtime_directory. 2024-11-13 11:17:40 -08:00
Antonio Fernandez ea47704d86 added missing tests to get 100% test coverage 2024-11-13 12:09:25 -05:00
Antonio Fernandez 61e4eeff6c converted html to a boolean and updated documentation and schema 2024-11-13 11:52:08 -05:00
Antonio Fernandez 3ab4b45041 added test for early exit when state is not in config 2024-11-13 09:07:47 -05:00
Tony Fernandez 4e1f256b48 added dryrun test case with minimum config 2024-11-12 10:15:19 -05:00
Tony Fernandez 96bb402837 fix test 2 2024-11-12 10:10:13 -05:00
Tony Fernandez 97949266b3 fix test 2024-11-12 10:09:23 -05:00
Tony Fernandez e69d2385fc better name for test 2024-11-12 10:08:13 -05:00
Tony Fernandez 6d9340ebb2 better comment blocks 2024-11-12 10:05:57 -05:00
Tony Fernandez 0441e79b41 "fail" -> "fails" 2024-11-12 10:00:34 -05:00
Tony Fernandez b1af304125 better data dict creation 2024-11-12 09:59:02 -05:00
Tony Fernandez eb8f7e0329 better description for expire and retry 2024-11-12 09:55:07 -05:00
Dan Helfman bf978f2db4 Fix missing build backend setting in pyproject.toml to allow Fedora builds (#932). 2024-11-10 17:09:10 -08:00
Dan Helfman 22776b123d Bump version for release. 2024-11-10 08:13:18 -08:00
Tony Fernandez ef66349674 small fixed for some failing tests 2024-11-08 20:02:29 -05:00
Tony Fernandez 51b885e7db added global constant for priority 2024-11-08 19:49:23 -05:00
Tony Fernandez 1781787305 better schema description for retry and expire 2024-11-08 19:43:40 -05:00
Tony Fernandez 46ebb0cebb removed redundant code 2024-11-08 18:45:46 -05:00
Tony Fernandez 3e0fa57860 removed tests that are not needed 2024-11-08 17:44:27 -05:00
Tony Fernandez 59f8722e05 better spacing for comments 2024-11-08 17:42:46 -05:00
Tony Fernandez 4ba42e8905 better wording. Added 'by default' 2024-11-08 17:41:18 -05:00
Tony Fernandez 3b79482b24 better wording 2024-11-08 17:38:58 -05:00
Tony Fernandez 7eb19cb0a7 added period 2024-11-08 17:38:20 -05:00
Tony Fernandez a4fabb8521 fix version 2024-11-08 17:37:26 -05:00
Tony Fernandez 85ea8f4f45 fix 10min in seconds 2024-11-08 15:03:38 -05:00
Tony Fernandez 290559116d better logic for priority 2024-11-08 15:01:28 -05:00
Tony Fernandez 72b27b0858 better message description in schema 2024-11-08 14:57:44 -05:00
Tony Fernandez 0fdee067c7 double space fix 2024-11-08 14:45:36 -05:00
Tony Fernandez 0dca5eeafc fix title wordwrap 2024-11-08 14:09:03 -05:00
Tony Fernandez 02ce3ba190 fix url_title word wrap 2024-11-08 14:07:49 -05:00
Tony Fernandez dc78bf4d6b fix TTL wordwrap 2024-11-08 14:06:39 -05:00
Tony Fernandez 4b7fbce291 fix sound word wrap 2024-11-08 14:05:27 -05:00
Tony Fernandez 1817b9a9ea fix wordwrap for html 2024-11-08 13:50:22 -05:00
Tony Fernandez 009055c61a device description rewrap 2024-11-08 13:48:21 -05:00
Tony Fernandez 54884da8fa priority word wrap 2024-11-08 13:46:19 -05:00
Tony Fernandez 1177385e08 fix expire description 2024-11-08 13:44:12 -05:00
Tony Fernandez a45ba8553c removed duplicate type:object 2024-11-08 13:42:19 -05:00
Tony Fernandez d7d6e30178 moved checks from hook to schema 2024-11-08 13:40:23 -05:00
Dan Helfman 56304fdcad Add NEWS entry for multiple system credentials fix (#930). 2024-11-07 20:20:41 -08:00
Dan Helfman 3f75e9931f Only support a single systemd credential by default (#930).
Reviewed-on: https://projects.torsion.org/borgmatic-collective/borgmatic/pulls/930
Reviewed-by: Dan Helfman <witten@torsion.org>
2024-11-08 04:18:56 +00:00
Dan Helfman 227f475e17 Fix an error when implicitly upgrading the check state directory across filesystems (#931). 2024-11-07 19:19:56 -08:00
cvlc12 467ddd0e93 creds: Only support single credential by default 2024-11-08 00:36:24 +01:00
Dan Helfman be08e889f0 Fix the user runtime directory location on macOS (and possibly Cygwin) (#928). 2024-11-03 21:44:11 -08:00
Dan Helfman 94c8a56373 Reorder NEWS items. 2024-11-03 13:58:04 -08:00
Dan Helfman cecf04aa69 Bump version for release. 2024-11-03 13:23:57 -08:00
Dan Helfman 814cdb4b87 Deprecate the "borgmatic_source_directory" option in favor of "user_runtime_directory" and "user_state_directory" (#562). Move the default borgmatic streaming database dump and bootstrap metadata location on disk (#562). With Borg 1.4+, store database dumps and bootstrap metadata in a "/borgmatic" directory within a backup archive (#838). Add "--local-path", "--remote-path", and "--user-runtime-directory" flags to the "config bootstrap" action. 2024-11-03 13:14:05 -08:00
Dan Helfman 13878be254 Move the default check state directory (#562, #638). Deprecate the "borgmatic_source_directory" option in favor of "borgmatic_runtime_directory" and "borgmatic_state_directory" (#562). 2024-10-30 22:36:43 -07:00
Antonio Fernandez 94db527500 finalized support for Pushover 2024-10-30 15:43:06 -04:00
Antonio Fernandez 2849f54932 initial pushover commit 2024-10-30 11:25:26 -04:00
Dan Helfman 129f3e753c Flesh out the Zabbix monitoring hook tests, add a logo to the documentation, etc. 2024-10-29 10:33:19 -07:00
Dan HelfmanandGitHub c85bf46ad9 Add Zabbix monitoring hook.
Merge pull request #85 from tony1661/zabbix-hook
2024-10-29 09:01:15 -07:00
Tony Fernandez fa6a4734d4 added auth tests with item id 2024-10-29 09:00:06 -04:00
Tony Fernandez e52e29444f added user/pass auth test 2024-10-29 08:54:25 -04:00
Tony Fernandez f713f1df7e better function names and comments 2024-10-29 08:21:52 -04:00
Tony Fernandez 87d824553d converted constants to capitals 2024-10-29 08:05:05 -04:00
Tony Fernandez 52fbf8cb24 capital 2024-10-28 09:54:07 -04:00
Tony Fernandez f758374772 adjustment in docs to reflect changes in 385ef2d 2024-10-28 09:52:20 -04:00
Tony Fernandez 60c5949c23 adjusted to accept integers and strings 2024-10-28 09:51:08 -04:00
Tony Fernandez d11c517f67 better schema descriptions 2024-10-28 09:41:34 -04:00
Tony Fernandez 237999cc81 fix word-wrap 2024-10-28 09:40:14 -04:00
Tony Fernandez d060f8d77a fix log for dry_run_label 2024-10-28 09:38:17 -04:00
Tony Fernandez 385ef2d012 fixes and first unit test attempt 2024-10-28 09:36:30 -04:00
Dan Helfman 02a2e77315 Add Documentation=... to sample systemd service files. 2024-10-27 16:31:08 -07:00
Dan Helfman 9b623a8a8e Move docs exporting to a separate script in the hopes it'll actually work there. 2024-10-27 14:48:17 -07:00
Dan Helfman e1f60e4b09 Fix podman create command to use correct image name. 2024-10-27 14:33:58 -07:00
Dan Helfman 269c00b240 Yet another attempt at making a docs tarball from a container image. 2024-10-27 14:26:00 -07:00
Dan Helfman 48f008d720 Fix image name of docs export. 2024-10-27 13:51:12 -07:00
Dan Helfman 53d9ffd1d3 Attempt to make a borgmatic docs tarball package in CI. 2024-10-27 13:26:41 -07:00
Dan Helfman d7323e08ac Fix isort invocation referring to gone setup.cfg file (#922). 2024-10-26 20:44:14 -07:00
Dan Helfman fc7a2852e0 Code formatting. 2024-10-26 20:34:48 -07:00
Dan Helfman f8c9d985e8 Add pyproject.toml transitin to NEWS. 2024-10-26 16:40:15 -07:00
Dan Helfman c7ca9bf844 Replace setup.py with pyproject.toml.
Reviewed-on: https://projects.torsion.org/borgmatic-collective/borgmatic/pulls/922
2024-10-26 23:38:34 +00:00
Dan Helfman 7a117d5cc9 Add support for Borg 2's "sftp://" repository URLs. 2024-10-26 11:58:26 -07:00
kaliko bd9751586c Merge blake8 conf to tox.ini 2024-10-26 20:11:39 +02:00
Dan Helfman fe0fe27c36 Adjust for Borg 2's change from "rclone://" repository URLs to just "rclone:". 2024-10-26 09:41:01 -07:00
Dan Helfman 1426859e1c Mention required borgmatic version in docs about Borg 2.x series changes. 2024-10-25 22:33:22 -07:00
Tony Fernandez ffb431e3ab minor improvement 2024-10-25 17:34:33 -04:00
Tony Fernandez 11fee81486 converted string to dictionary as requested 2024-10-25 17:28:02 -04:00
Dan Helfman 83bc737185 Borg 2 changes: Default the "archive_name_format" option to just "{hostname}". Update the "--match-archives"/"--archive" flags to support series names / archive hashes. Add a "--match-archives" flag to the "prune" action. 2024-10-25 14:26:31 -07:00
Tony Fernandez cda83310c8 fixed version number 2024-10-25 08:10:25 -04:00
Tony Fernandez 90ccbecf07 better documatation description 2024-10-25 08:08:38 -04:00
Tony Fernandez ccbf668bea fix for if logic 2024-10-24 09:01:10 -04:00
Tony Fernandez f18219a768 changes made with 'tox - e isort' 2024-10-24 08:00:14 -04:00
Tony Fernandez 9ba8ca24eb changes made with 'tox -e black' 2024-10-24 07:52:32 -04:00
Tony Fernandez b42793a2dc convert concat to fstring 2024-10-23 18:26:22 -04:00
Tony Fernandez 2877c1ad0d added spaces after commas 2024-10-23 18:10:58 -04:00
Tony Fernandez 1b9f95ca47 better description in schema for server 2024-10-23 18:04:28 -04:00
Tony Fernandez 7aff22536d better itemid description in schema 2024-10-23 17:53:07 -04:00
Tony Fernandez d6e1cc3e12 better wording 2024-10-23 17:43:54 -04:00
Tony Fernandez c1a08edca2 subheader formatting 2024-10-23 17:42:11 -04:00
Tony Fernandez 02a219fac2 it will 2024-10-23 17:40:24 -04:00
Tony Fernandez 78f81c7b73 changed verison to 1.9 - zabbix hook 2024-10-23 17:39:36 -04:00
Tony Fernandez 8edb40a8e9 added dryrun check 2024-10-23 17:38:24 -04:00
Tony Fernandez f2d7687ca3 removed unused variable 2024-10-23 17:34:21 -04:00
Tony Fernandez b5fb0c8247 added returns when expected values are not provided 2024-10-23 17:33:09 -04:00
Tony Fernandez 9b2ac961d7 capitalized Zabbix in logging 2024-10-23 17:30:28 -04:00
Tony Fernandez 38ce98771b removed spaces before logging 2024-10-23 17:28:54 -04:00
Tony Fernandez f616284ffb better description for server string 2024-10-23 17:24:20 -04:00
Tony Fernandez 5da898003f removed unnecessary parentheses here and below 2024-10-23 17:21:30 -04:00
Tony Fernandez 5a464b3186 better logic for run_states 2024-10-23 17:19:51 -04:00
Tony Fernandez 42fb8c38e0 better comments and logging 2024-10-23 17:15:55 -04:00
Tony Fernandez 80839566d6 better wording for API key 2024-10-23 17:12:18 -04:00
Tony Fernandez 702e55e6f7 formatting fix for key 2024-10-23 17:08:37 -04:00
Tony Fernandez 1a8e8835c1 added quotes and period for host description 2024-10-23 17:00:31 -04:00
Tony Fernandez 9abc5c60d4 its 2024-10-23 16:53:18 -04:00
Tony Fernandez fc87b74ab0 capital fixed 2024-10-23 16:52:45 -04:00
Dan Helfman ad21eb41ae Add support for Borg 2's "rclone://" repository URLs. 2024-10-23 11:10:40 -07:00
kaliko 601e393ec7 Fixed package discovery in pyproject.toml 2024-10-22 22:04:33 +02:00
Dan Helfman e391fd196d Add loading of systemd credentials to NEWS (#902). 2024-10-22 09:01:18 -07:00
Dan Helfman 5f387b3991 Fix documentation URL in YAML (#902). 2024-10-22 08:59:13 -07:00
Dan Helfman ed957a940a Load systemd encrypted credentials (#902).
Reviewed-on: https://projects.torsion.org/borgmatic-collective/borgmatic/pulls/902
Reviewed-by: Dan Helfman <witten@torsion.org>
2024-10-22 15:58:41 +00:00
Dan Helfman bd4c672382 Apply the "working_directory" option to all actions, not just "create". Also fix the glob expansion of "source_directories" values to respect the "working_directory" option (#609). 2024-10-20 16:04:41 -07:00
Dan Helfman c71da46963 Fix change-passphrase to actually prompt for a new passphrase when an old one is configured (#911). 2024-10-19 09:58:32 -07:00
Tony Fernandez 3da7471fe6 added note about zabbix 7.0+ 2024-10-18 18:00:14 -04:00
Tony Fernandez b874e7e66f zabbix hook added 2024-10-18 17:57:37 -04:00
kaliko b029d1cb67 Add codespell to pyproject.toml 2024-10-15 12:52:05 +02:00
kaliko 33b1101ce1 Add isort to pyproject.toml 2024-10-15 12:51:33 +02:00
kaliko d96c5f79fb Add pytest to pyproject.toml 2024-10-14 18:30:39 +02:00
kaliko 9e29ce788f Fixed release script 2024-10-14 18:17:22 +02:00
kaliko 4658c5d1cb Moving to pyproject.toml 2024-10-14 18:17:14 +02:00
cvlc12 5280de86ff Load encrypted systemd credentials 2024-10-13 15:42:07 +02:00
Dan Helfman 29d5b36a78 Change soft failure command hooks to skip only the current repository (#921). 2024-10-06 17:39:02 -07:00
Dan Helfman 29f214a269 Change borgmatic 1.8.15 (unreleased) to 1.9.0 due to breaking changes. 2024-10-06 16:07:14 -07:00
Dan Helfman 6fdce2a4a6 Fix a confusing apparent hang when when the repository location changes, and instead show a helpful error message (#914). 2024-10-03 16:48:34 -07:00
Dan Helfman a4b65cf710 Update Borg 2 beta links. 2024-10-03 08:03:47 -07:00
Dan Helfman 79725d2ff7 Document a policy for versioning and breaking changes (#919). 2024-10-02 09:10:12 -07:00
Dan Helfman f7e8a2c1d1 Clarify the command-line help for the "--config" flag (#919). 2024-10-02 08:47:52 -07:00
Dan Helfman f54d566edc Fixes to make the inclusive language linter happy. 2024-10-01 13:37:31 -07:00
Dan Helfman b7efa0d3f0 When databases are configured, don't auto-enable the "one_file_system" option (#918). 2024-10-01 10:16:30 -07:00
Dan Helfman 88b945dcb9 Add missing test (#911). 2024-10-01 09:31:03 -07:00
Dan Helfman 34305d686c Revert "Add missing test (#911)" which accidentally included unrelated changes.
This reverts commit bf1e8bc44e.
2024-10-01 09:28:39 -07:00
Dan Helfman bf1e8bc44e Add missing test (#911). 2024-10-01 09:26:00 -07:00
Dan Helfman fd4f69f6c3 Rename repository actions for compatibility with recent Borg 2 changes (#915). 2024-09-09 10:05:32 -07:00
Dan Helfman 1fe6ae83a8 Add a "key change-passphrase" action to change the passphrase protecting a repository key (#911). 2024-09-01 11:13:39 -07:00
Dan Helfman 1197d6d0f6 Bump version for release. 2024-08-29 17:41:02 -07:00
Dan Helfman 288a4bf243 : Add documentation details for how to run custom database dump commands using binaries from running containers (#906). 2024-08-20 15:03:49 -07:00
Dan Helfman 1fb943e5f1 Documentation and config schema update about ~/.borgmatic volume mount (#906).
Reviewed-on: https://projects.torsion.org/borgmatic-collective/borgmatic/pulls/907
2024-08-20 21:51:51 +00:00
Dan Helfman e0298685a1 Fix the "source_directories_must_exist" option to work with relative "source_directories" paths when a "working_directory" is set (#905). 2024-08-20 13:52:06 -07:00
Dan Helfman c5633227bf Add glob ("*") support to the "--repository" flag (#898). 2024-08-20 12:49:50 -07:00
Jonathan Brockhaus 6087c12e09 docs: typo 2024-08-16 14:36:28 +02:00
Jonathan Brockhaus 9d83f02e24 fix(config/schema.yaml): Add note about mounting .borgmatic folder inside container db backups 2024-08-16 14:25:11 +02:00
Jonathan Brockhaus b6ccde6757 fix(docs): Add note about mounting .borgmatic folder inside container db backups 2024-08-16 14:23:59 +02:00
Dan Helfman 548aceb3d5 Fix config validation broken by schema change (#904). 2024-08-12 23:09:55 -07:00
Dan Helfman 2bd63bbdd2 Clarify the configuration reference about the "spot" check options (#904). 2024-08-12 22:50:58 -07:00
Dan Helfman 9b4fed64a6 Upgrade run-full-tests dependencies. 2024-08-11 20:41:43 -07:00
Dan Helfman ff1001a5f5 Fix NEWS typo. 2024-08-11 20:38:38 -07:00
Dan Helfman e7f1c3eda5 Add "color" option regression fix to NEWS. 2024-08-11 20:35:54 -07:00
Dan HelfmanandGitHub 21e343a948 Fix broken color configuration option.
Merge pull request #83 from TheoBrigitte/main
2024-08-11 20:32:43 -07:00
Theo Brigitte 4c4fd92013 update logger unit tests
* update color config structure
  * add check for color: True in config
  * ensure interactive_console is called wherever it should
2024-08-09 00:07:46 +02:00
Theo Brigitte 37735e464c Fix color configuration directive 2024-08-05 16:38:49 +02:00
Dan Helfman 424cc6b66c Fix for a "bad character" Borg error in which the "spot" check fed Borg an invalid pattern (#899). 2024-07-25 11:34:09 -07:00
Dan Helfman e128a3e0a9 Add a ticket number onto the traceback fix in NEWS. 2024-07-19 22:22:19 -07:00
Dan Helfman 27e7ece2f5 More fix for a potential traceback (TypeError) during the handling of another error. 2024-07-15 14:45:21 -07:00
Dan Helfman d44dc93509 Fix for a potential traceback (TypeError) during the handling of another error. 2024-07-14 10:07:15 -07:00
Dan Helfman 31778fd3bf Fix error in borgmatic rcreate/init on empty repository directory with Borg 1.4 (#896). 2024-07-08 11:24:48 -07:00
Dan Helfman 4313f90dd8 Add a couple of more code style guidelines. 2024-07-07 20:49:27 -07:00
Dan Helfman 1f82ea2798 Attempt to fix documentation build. 2024-07-06 10:23:10 -07:00
Dan Helfman 148536d867 Upgrade certifi in test requirements. 2024-07-06 10:17:53 -07:00
Dan Helfman 197f0a521d Add a recent contributors section to the documentation. 2024-07-06 10:15:16 -07:00
Dan Helfman 0d8e352033 Update documentation link to Borg 1.4.0 exit codes. 2024-07-04 20:21:40 -07:00
Dan Helfman 20a3995977 Bump version for release. 2024-07-04 08:54:09 -07:00
Dan Helfman 66aa953371 Add additional tests (#298). 2024-07-04 08:42:05 -07:00
Dan Helfman ba053de8f7 Add delete and rdelete actions (#298).
Reviewed-on: https://projects.torsion.org/borgmatic-collective/borgmatic/pulls/893
2024-07-04 06:07:30 +00:00
Dan Helfman 2f844d65d5 Automated tests complete (#298). 2024-07-02 23:11:58 -07:00
Dan Helfman 2dca5e1834 Initial automated tests for delete action (#298). 2024-07-02 20:26:29 -07:00
Dan Helfman 36197ce027 Dry run support for delete/rdelete actions (#298). 2024-06-28 20:56:42 -07:00
Dan Helfman e9a0226ee0 Initial work on delete/rdelete actions (#298). 2024-06-28 16:20:10 -07:00
Dan Helfman fc3b4a653e Add a development documentation note about using fully qualified imports. 2024-06-28 15:00:25 -07:00
Dan Helfman 3673abb01e Update documentation links that link to beta versions of Borg docs. 2024-06-27 14:42:53 -07:00
Dan Helfman ac4277d36c Add "new in version" label to Uptime Kuma documentation (#885). 2024-06-27 09:29:53 -07:00
Dan Helfman 21cbc99d9e Fix uptime_kuma monitor not being called after name change (#891).
Reviewed-on: https://projects.torsion.org/borgmatic-collective/borgmatic/pulls/891
2024-06-27 16:27:07 +00:00
Paul Wilde d080bf2ae9 [bugfix] uptime_kuma monitor is not called after name change 2024-06-27 12:49:07 +01:00
Dan Helfman 2a1c790655 Minor reorganization of borgmatic source code reference. 2024-06-26 19:39:32 -07:00
Dan Helfman 410204a70d Formatting, whitespace, and minor fixes for Uptime Kuma hook (#885). 2024-06-26 16:09:14 -07:00
Dan Helfman 4a0c167c1c Add Uptime Kuma monitoring hook (#885).
Reviewed-on: https://projects.torsion.org/borgmatic-collective/borgmatic/pulls/885
Reviewed-by: Dan Helfman <witten@torsion.org>
2024-06-26 22:50:11 +00:00
Dan Helfman 593c956d33 Add an "only_run_on" option to consistency checks so you can limit a check to running on particular days of the week (#785). 2024-06-26 14:57:59 -07:00
pswilde d18cb89493 Merge branch 'main' into main 2024-06-26 19:58:56 +00:00
Paul Wilde 067c79c606 renamed push_monitor back to ping_monitor in uptime kuma hook 2024-06-26 20:57:37 +01:00
Dan Helfman ebde88ccaa Fix the Healthchecks ping body size limit, restoring it to the documented 100,000 bytes (#889). 2024-06-25 12:45:44 -07:00
Dan Helfman cc402487d9 Minor development documentation clarifications. 2024-06-24 10:48:13 -07:00
Paul Wilde d108e6102b some minor corrections in how to uptime kuma docs 2024-06-24 12:00:22 +01:00
Paul Wilde 3e60043632 some minor corrections in how to uptime kuma docs 2024-06-24 11:57:12 +01:00
Paul Wilde a8d691169a some minor corrections in how to uptime kuma docs 2024-06-24 11:54:34 +01:00
Paul Wilde 939c2f6718 some minor corrections in how to uptime kuma docs 2024-06-24 11:54:12 +01:00
Paul Wilde 0837059e21 some minor corrections in how to uptime kuma docs 2024-06-24 11:51:41 +01:00
Paul Wilde 0ee166fdf0 added Uptime Kuma how-to docs 2024-06-24 11:46:38 +01:00
Paul Wilde b50996b864 change uptimekuma method names to 'push_*' instead of 'ping' 2024-06-24 11:07:09 +01:00
Paul Wilde 8f423c7293 black formatting on uptimekuma hook 2024-06-24 10:46:04 +01:00
Paul Wilde 14ce88e04b black formatting on test_uptimekuma.py 2024-06-24 10:37:44 +01:00
Paul Wilde f97968b72d variable renaming 2024-06-24 10:34:52 +01:00
Paul Wilde 612f867ea8 Merge remote-tracking branch 'upstream/main' 2024-06-24 10:28:02 +01:00
Paul Wilde 303d6609e4 removed unecessary tests for 'default urls' as these would never really be used anyway 2024-06-24 10:27:23 +01:00
Paul Wilde bf7b163ccd added early out for dry run 2024-06-24 10:25:08 +01:00
Paul Wilde 4bd798f0ad alpha ordered dispatch monitor hook names (including loki) 2024-06-24 10:22:27 +01:00
Paul Wilde 52aa7c5d21 switched to using full 'push_url' instead of separate 'server' and 'push_code' 2024-06-24 10:21:31 +01:00
Dan Helfman f5a1dd31c8 Fix PagerDuty hook traceback with Python < 3.10 (#886). 2024-06-23 18:28:41 -07:00
Paul Wilde a509cdedd5 Added Uptime Kuma tests 2024-06-22 10:46:17 +01:00
Paul Wilde dcbc30b164 WIP add uptime kuma tests 2024-06-22 10:19:34 +01:00
Paul Wilde 5ab99b4cc0 Added Uptime Kuma image to readme 2024-06-21 21:21:04 +01:00
Paul Wilde 27c90b7cf1 Added Uptime Kuma hook 2024-06-21 21:10:14 +01:00
Paul Wilde 6eb76454bb WIP added some schema info for uptime kuma 2024-06-21 17:00:44 +01:00
Paul Wilde 83bcea98dc WIP added some schema info for uptime kuma 2024-06-21 16:57:20 +01:00
Paul Wilde 4db09a73b3 WIP implement Uptime Kuma hook 2024-06-21 16:47:51 +01:00
157 changed files with 9298 additions and 1785 deletions
-1
View File
@@ -1 +0,0 @@
select = Q0
+2
View File
@@ -26,3 +26,5 @@ jobs:
PASSWORD: "${{ secrets.REGISTRY_PASSWORD }}"
- run: podman build --tag "$IMAGE_NAME" --file docs/Dockerfile --storage-opt "overlay.mount_program=/usr/bin/fuse-overlayfs" .
- run: podman push "$IMAGE_NAME"
- run: scripts/export-docs-from-image
- run: curl --user "${{ secrets.REGISTRY_USERNAME }}:${{ secrets.REGISTRY_PASSWORD }}" --upload-file borgmatic-docs.tar.gz https://projects.torsion.org/api/packages/borgmatic-collective/generic/borgmatic-docs/$(head --lines=1 NEWS)/borgmatic-docs.tar.gz
+104
View File
@@ -1,3 +1,107 @@
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.
* #609: BREAKING: Apply the "working_directory" option to all actions, not just "create". This
includes repository paths, destination paths, mount points, etc.
* #562: Deprecate the "borgmatic_source_directory" option in favor of "user_runtime_directory"
and "user_state_directory".
* #562: BREAKING: Move the default borgmatic streaming database dump and bootstrap metadata
directory from ~/.borgmatic to /run/user/$UID/borgmatic, which is more XDG-compliant. You can
override this location with the new "user_runtime_directory" option. Existing archives with
database dumps at the old location are still restorable.
* #562, #638: Move the default check state directory from ~/.borgmatic to
~/.local/state/borgmatic. This is more XDG-compliant and also prevents these state files from
getting backed up (unless you explicitly include them). You can override this location with the
new "user_state_directory" option. After the first time you run the "check" action with borgmatic
1.9.0, you can safely delete the ~/.borgmatic directory.
* #838: BREAKING: With Borg 1.4+, store database dumps and bootstrap metadata in a "/borgmatic"
directory within a backup archive, so the path doesn't depend on the current user. This means
that you can now backup as one user and restore or bootstrap as another user, among other use
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
"repo-create" for compatibility with recent changes in Borg 2.0.0b10.
* #918: BREAKING: When databases are configured, don't auto-enable the "one_file_system" option,
as existing auto-excludes of special files should be sufficient to prevent Borg from hanging on
them. But if this change causes problems for you, you can always enable "one_file_system"
explicitly.
* #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
* #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.
* When using Borg 2, default the "archive_name_format" option to just "{hostname}", as Borg 2 does
not require unique archive names; identical archive names form a common "series" that can be
targeted together. See the Borg 2 documentation for more information:
https://borgbackup.readthedocs.io/en/2.0.0b13/changes.html#borg-1-2-x-1-4-x-to-borg-2-0
* Add support for Borg 2's "rclone:" repository URLs, so you can backup to 70+ cloud storage
services whether or not they support Borg explicitly.
* Add support for Borg 2's "sftp://" repository URLs.
* Update the "--match-archives" and "--archive" flags to support Borg 2 series names or archive
hashes.
* Add a "--match-archives" flag to the "prune" action.
* Add "--local-path" and "--remote-path" flags to the "config bootstrap" action for setting the
Borg executable paths used for bootstrapping.
* Add a "--user-runtime-directory" flag to the "config bootstrap" action for helping borgmatic
locate the bootstrap metadata stored in an archive.
* Add a Zabbix monitoring hook. See the documentation for more information:
https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#zabbix-hook
* Add a tarball of borgmatic's HTML documentation to the packages on the project page.
1.8.14
* #896: Fix an error in borgmatic rcreate/init on an empty repository directory with Borg 1.4.
* #898: Add glob ("*") support to the "--repository" flag. Just quote any values containing
globs so your shell doesn't interpret them.
* #899: Fix for a "bad character" Borg error in which the "spot" check fed Borg an invalid pattern.
* #900: Fix for a potential traceback (TypeError) during the handling of another error.
* #904: Clarify the configuration reference about the "spot" check options:
https://torsion.org/borgmatic/docs/reference/configuration/
* #905: Fix the "source_directories_must_exist" option to work with relative "source_directories"
paths when a "working_directory" is set.
* #906: Add documentation details for how to run custom database dump commands using binaries from
running containers:
https://torsion.org/borgmatic/docs/how-to/backup-your-databases/#containers
* Fix a regression in which the "color" option had no effect.
* Add a recent contributors section to the documentation, because credit where credit's due! See:
https://torsion.org/borgmatic/#recent-contributors
1.8.13
* #298: Add "delete" and "rdelete" actions to delete archives or entire repositories.
* #785: Add an "only_run_on" option to consistency checks so you can limit a check to running on
particular days of the week. See the documentation for more information:
https://torsion.org/borgmatic/docs/how-to/deal-with-very-large-backups/#check-days
* #885: Add an Uptime Kuma monitoring hook. See the documentation for more information:
https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#uptime-kuma-hook
* #886: Fix a PagerDuty hook traceback with Python < 3.10.
* #889: Fix the Healthchecks ping body size limit, restoring it to the documented 100,000 bytes.
1.8.12
* #817: Add a "--max-duration" flag to the "check" action and a "max_duration" option to the
repository check configuration. This tells Borg to interrupt a repository check after a certain
+7
View File
@@ -62,12 +62,15 @@ borgmatic is powered by [Borg Backup](https://www.borgbackup.org/).
<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://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>
<a href="https://www.zabbix.com/"><img src="docs/static/zabbix.png" alt="Zabbix" height="40px" style="margin-bottom:20px; margin-right:20px;"></a>
<a href="https://www.borgbase.com/?utm_source=borgmatic"><img src="docs/static/borgbase.png" alt="BorgBase" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
@@ -154,3 +157,7 @@ general, contributions are very welcome. We don't bite!
Also, please check out the [borgmatic development
how-to](https://torsion.org/borgmatic/docs/how-to/develop-on-borgmatic/) for
info on cloning source code, running tests, etc.
### Recent contributors
{% include borgmatic/contributors.html %}
+2 -2
View File
@@ -1,7 +1,7 @@
import logging
import borgmatic.borg.borg
import borgmatic.borg.rlist
import borgmatic.borg.repo_list
import borgmatic.config.validate
logger = logging.getLogger(__name__)
@@ -25,7 +25,7 @@ def run_borg(
logger.info(
f'{repository.get("label", repository["path"])}: Running arbitrary Borg command'
)
archive_name = borgmatic.borg.rlist.resolve_archive_name(
archive_name = borgmatic.borg.repo_list.resolve_archive_name(
repository['path'],
borg_arguments.archive,
config,
+38
View File
@@ -0,0 +1,38 @@
import logging
import borgmatic.borg.change_passphrase
import borgmatic.config.validate
logger = logging.getLogger(__name__)
def run_change_passphrase(
repository,
config,
local_borg_version,
change_passphrase_arguments,
global_arguments,
local_path,
remote_path,
):
'''
Run the "key change-passprhase" action for the given repository.
'''
if (
change_passphrase_arguments.repository is None
or borgmatic.config.validate.repositories_match(
repository, change_passphrase_arguments.repository
)
):
logger.info(
f'{repository.get("label", repository["path"])}: Changing repository passphrase'
)
borgmatic.borg.change_passphrase.change_passphrase(
repository['path'],
config,
local_borg_version,
change_passphrase_arguments,
global_arguments,
local_path=local_path,
remote_path=remote_path,
)
+126 -61
View File
@@ -1,3 +1,4 @@
import calendar
import datetime
import hashlib
import itertools
@@ -5,14 +6,16 @@ import logging
import os
import pathlib
import random
import shutil
import borgmatic.borg.check
import borgmatic.borg.create
import borgmatic.borg.environment
import borgmatic.borg.extract
import borgmatic.borg.list
import borgmatic.borg.rlist
import borgmatic.borg.repo_list
import borgmatic.borg.state
import borgmatic.config.paths
import borgmatic.config.validate
import borgmatic.execute
import borgmatic.hooks.command
@@ -99,12 +102,17 @@ def parse_frequency(frequency):
raise ValueError(f"Could not parse consistency check frequency '{frequency}'")
WEEKDAY_DAYS = calendar.day_name[0:5]
WEEKEND_DAYS = calendar.day_name[5:7]
def filter_checks_on_frequency(
config,
borg_repository_id,
checks,
force,
archives_check_id=None,
datetime_now=datetime.datetime.now,
):
'''
Given a configuration dict with a "checks" sequence of dicts, a Borg repository ID, a sequence
@@ -143,6 +151,29 @@ def filter_checks_on_frequency(
if checks and check not in checks:
continue
only_run_on = check_config.get('only_run_on')
if only_run_on:
# Use a dict instead of a set to preserve ordering.
days = dict.fromkeys(only_run_on)
if 'weekday' in days:
days = {
**dict.fromkeys(day for day in days if day != 'weekday'),
**dict.fromkeys(WEEKDAY_DAYS),
}
if 'weekend' in days:
days = {
**dict.fromkeys(day for day in days if day != 'weekend'),
**dict.fromkeys(WEEKEND_DAYS),
}
if calendar.day_name[datetime_now().weekday()] not in days:
logger.info(
f"Skipping {check} check due to day of the week; check only runs on {'/'.join(days)} (use --force to check anyway)"
)
filtered_checks.remove(check)
continue
frequency_delta = parse_frequency(check_config.get('frequency'))
if not frequency_delta:
continue
@@ -153,8 +184,8 @@ def filter_checks_on_frequency(
# If we've not yet reached the time when the frequency dictates we're ready for another
# check, skip this check.
if datetime.datetime.now() < check_time + frequency_delta:
remaining = check_time + frequency_delta - datetime.datetime.now()
if datetime_now() < check_time + frequency_delta:
remaining = check_time + frequency_delta - datetime_now()
logger.info(
f'Skipping {check} check due to configured frequency; {remaining} until next check (use --force to check anyway)'
)
@@ -180,15 +211,11 @@ def make_check_time_path(config, borg_repository_id, check_type, archives_check_
"archives", etc.), and a unique hash of the archives filter flags, return a path for recording
that check's time (the time of that check last occurring).
'''
borgmatic_source_directory = os.path.expanduser(
config.get(
'borgmatic_source_directory', borgmatic.borg.state.DEFAULT_BORGMATIC_SOURCE_DIRECTORY
)
)
borgmatic_state_directory = borgmatic.config.paths.get_borgmatic_state_directory(config)
if check_type in ('archives', 'data'):
return os.path.join(
borgmatic_source_directory,
borgmatic_state_directory,
'checks',
borg_repository_id,
check_type,
@@ -196,7 +223,7 @@ def make_check_time_path(config, borg_repository_id, check_type, archives_check_
)
return os.path.join(
borgmatic_source_directory,
borgmatic_state_directory,
'checks',
borg_repository_id,
check_type,
@@ -229,7 +256,7 @@ def read_check_time(path):
def probe_for_check_time(config, borg_repository_id, check, archives_check_id):
'''
Given a configuration dict, a Borg repository ID, the name of a check type ("repository",
"archives", etc.), and a unique hash of the archives filter flags, return a the corresponding
"archives", etc.), and a unique hash of the archives filter flags, return the corresponding
check time or None if such a check time does not exist.
When the check type is "archives" or "data", this function probes two different paths to find
@@ -267,14 +294,37 @@ def upgrade_check_times(config, borg_repository_id):
Given a configuration dict and a Borg repository ID, upgrade any corresponding check times on
disk from old-style paths to new-style paths.
Currently, the only upgrade performed is renaming an archive or data check path that looks like:
One upgrade performed is moving the checks directory from:
~/.borgmatic/checks/1234567890/archives
{borgmatic_source_directory}/checks (e.g., ~/.borgmatic/checks)
to:
~/.borgmatic/checks/1234567890/archives/all
{borgmatic_state_directory}/checks (e.g. ~/.local/state/borgmatic)
Another upgrade is renaming an archive or data check path that looks like:
{borgmatic_state_directory}/checks/1234567890/archives
to:
{borgmatic_state_directory}/checks/1234567890/archives/all
'''
borgmatic_source_checks_path = os.path.join(
borgmatic.config.paths.get_borgmatic_source_directory(config), 'checks'
)
borgmatic_state_path = borgmatic.config.paths.get_borgmatic_state_directory(config)
borgmatic_state_checks_path = os.path.join(borgmatic_state_path, 'checks')
if os.path.exists(borgmatic_source_checks_path) and not os.path.exists(
borgmatic_state_checks_path
):
logger.debug(
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)
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')
old_path = os.path.dirname(new_path)
@@ -283,15 +333,15 @@ def upgrade_check_times(config, borg_repository_id):
if not os.path.isfile(old_path) and not os.path.isfile(temporary_path):
continue
logger.debug(f'Upgrading archives check time from {old_path} to {new_path}')
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(
@@ -319,7 +369,7 @@ def collect_spot_check_source_paths(
config_paths=(),
local_borg_version=local_borg_version,
global_arguments=global_arguments,
borgmatic_source_directories=(),
borgmatic_runtime_directories=(),
local_path=local_path,
remote_path=remote_path,
list_files=True,
@@ -327,17 +377,13 @@ def collect_spot_check_source_paths(
)
)
borg_environment = borgmatic.borg.environment.make_environment(config)
try:
working_directory = os.path.expanduser(config.get('working_directory'))
except TypeError:
working_directory = None
working_directory = borgmatic.config.paths.get_working_directory(config)
paths_output = borgmatic.execute.execute_command_and_capture_output(
create_flags + create_positional_arguments,
capture_stderr=True,
working_directory=working_directory,
extra_environment=borg_environment,
working_directory=working_directory,
borg_local_path=local_path,
borg_exit_codes=config.get('borg_exit_codes'),
)
@@ -348,26 +394,31 @@ def collect_spot_check_source_paths(
if path_line and path_line.startswith('- ') or path_line.startswith('+ ')
)
return tuple(path for path in paths if os.path.isfile(path))
return tuple(
path for path in paths if os.path.isfile(os.path.join(working_directory or '', path))
)
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).
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 = os.path.expanduser(
config.get(
'borgmatic_source_directory', borgmatic.borg.state.DEFAULT_BORGMATIC_SOURCE_DIRECTORY
)
)
borgmatic_source_directory = borgmatic.config.paths.get_borgmatic_source_directory(config)
return tuple(
path
@@ -383,7 +434,9 @@ def collect_spot_check_archive_paths(
)
for (file_type, path) in (line.split(' ', 1),)
if file_type != BORG_DIRECTORY_FILE_TYPE
if pathlib.Path('/borgmatic') not in pathlib.Path(path).parents
if pathlib.Path(borgmatic_source_directory) not in pathlib.Path(path).parents
if pathlib.Path(borgmatic_runtime_directory) not in pathlib.Path(path).parents
)
@@ -398,7 +451,7 @@ def compare_spot_check_hashes(
global_arguments,
local_path,
remote_path,
log_label,
log_prefix,
source_paths,
):
'''
@@ -415,11 +468,14 @@ def compare_spot_check_hashes(
int(len(source_paths) * (min(spot_check_config['data_sample_percentage'], 100) / 100)), 1
)
source_sample_paths = tuple(random.sample(source_paths, sample_count))
working_directory = borgmatic.config.paths.get_working_directory(config)
existing_source_sample_paths = {
source_path for source_path in source_sample_paths if os.path.exists(source_path)
source_path
for source_path in source_sample_paths
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)
@@ -440,7 +496,8 @@ def compare_spot_check_hashes(
(spot_check_config.get('xxh64sum_command', 'xxh64sum'),)
+ tuple(
path for path in source_sample_paths_subset if path in existing_source_sample_paths
)
),
working_directory=working_directory,
)
source_hashes.update(
@@ -495,18 +552,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(
@@ -528,9 +586,9 @@ def spot_check(
local_path,
remote_path,
)
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.rlist.resolve_archive_name(
archive = borgmatic.borg.repo_list.resolve_archive_name(
repository['path'],
'latest',
config,
@@ -539,7 +597,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,
@@ -549,8 +607,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.
@@ -558,10 +617,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"]}%)'
@@ -575,25 +634,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'
)
@@ -627,7 +686,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,
@@ -679,14 +740,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(
+72 -43
View File
@@ -3,55 +3,79 @@ import logging
import os
import borgmatic.borg.extract
import borgmatic.borg.rlist
import borgmatic.borg.repo_list
import borgmatic.config.paths
import borgmatic.config.validate
import borgmatic.hooks.command
from borgmatic.borg.state import DEFAULT_BORGMATIC_SOURCE_DIRECTORY
logger = logging.getLogger(__name__)
def get_config_paths(bootstrap_arguments, global_arguments, local_borg_version):
def make_bootstrap_config(bootstrap_arguments):
'''
Given the bootstrap arguments as an argparse.Namespace (containing the repository and archive
name, borgmatic source directory, destination directory, and whether to strip components), the
global arguments as an argparse.Namespace (containing the dry run flag and the local borg
version), return the config paths from the manifest.json file in the borgmatic source directory
after extracting it from the repository.
Given the bootstrap arguments as an argparse.Namespace, return a corresponding config dict.
'''
return {
'ssh_command': bootstrap_arguments.ssh_command,
# In case the repo has been moved or is accessed from a different path at the point of
# bootstrapping.
'relocated_repo_access_is_ok': True,
}
def get_config_paths(archive_name, bootstrap_arguments, global_arguments, local_borg_version):
'''
Given an archive name, the bootstrap arguments as an argparse.Namespace (containing the
repository and archive name, Borg local path, Borg remote path, borgmatic runtime directory,
borgmatic source directory, destination directory, and whether to strip components), the global
arguments as an argparse.Namespace (containing the dry run flag and the local borg version),
return the config paths from the manifest.json file in the borgmatic source directory or runtime
directory after extracting it from the repository archive.
Raise ValueError if the manifest JSON is missing, can't be decoded, or doesn't contain the
expected configuration path data.
'''
borgmatic_source_directory = (
bootstrap_arguments.borgmatic_source_directory or DEFAULT_BORGMATIC_SOURCE_DIRECTORY
borgmatic_source_directory = borgmatic.config.paths.get_borgmatic_source_directory(
{'borgmatic_source_directory': bootstrap_arguments.borgmatic_source_directory}
)
borgmatic_manifest_path = os.path.expanduser(
os.path.join(borgmatic_source_directory, 'bootstrap', 'manifest.json')
)
config = {'ssh_command': bootstrap_arguments.ssh_command}
config = make_bootstrap_config(bootstrap_arguments)
extract_process = borgmatic.borg.extract.extract_archive(
global_arguments.dry_run,
# 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.
with borgmatic.config.paths.Runtime_directory(
{'user_runtime_directory': bootstrap_arguments.user_runtime_directory},
bootstrap_arguments.repository,
borgmatic.borg.rlist.resolve_archive_name(
bootstrap_arguments.repository,
bootstrap_arguments.archive,
config,
local_borg_version,
global_arguments,
),
[borgmatic_manifest_path],
config,
local_borg_version,
global_arguments,
extract_to_stdout=True,
)
manifest_json = extract_process.stdout.read()
) 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'
)
if not manifest_json:
raise ValueError(
'Cannot read configuration paths from archive due to missing bootstrap manifest'
)
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'
)
try:
manifest_data = json.loads(manifest_json)
@@ -75,27 +99,32 @@ def run_bootstrap(bootstrap_arguments, global_arguments, local_borg_version):
Raise ValueError if the bootstrap configuration could not be loaded.
Raise CalledProcessError or OSError if Borg could not be run.
'''
config = make_bootstrap_config(bootstrap_arguments)
archive_name = borgmatic.borg.repo_list.resolve_archive_name(
bootstrap_arguments.repository,
bootstrap_arguments.archive,
config,
local_borg_version,
global_arguments,
local_path=bootstrap_arguments.local_path,
remote_path=bootstrap_arguments.remote_path,
)
manifest_config_paths = get_config_paths(
bootstrap_arguments, global_arguments, local_borg_version
archive_name, bootstrap_arguments, global_arguments, local_borg_version
)
config = {'ssh_command': bootstrap_arguments.ssh_command}
logger.info(f"Bootstrapping config paths: {', '.join(manifest_config_paths)}")
borgmatic.borg.extract.extract_archive(
global_arguments.dry_run,
bootstrap_arguments.repository,
borgmatic.borg.rlist.resolve_archive_name(
bootstrap_arguments.repository,
bootstrap_arguments.archive,
config,
local_borg_version,
global_arguments,
),
archive_name,
[config_path.lstrip(os.path.sep) for config_path in manifest_config_paths],
config,
local_borg_version,
global_arguments,
local_path=bootstrap_arguments.local_path,
remote_path=bootstrap_arguments.remote_path,
extract_to_stdout=False,
destination_path=bootstrap_arguments.destination,
strip_components=bootstrap_arguments.strip_components,
+64 -53
View File
@@ -5,7 +5,7 @@ import os
import borgmatic.actions.json
import borgmatic.borg.create
import borgmatic.borg.state
import borgmatic.config.paths
import borgmatic.config.validate
import borgmatic.hooks.command
import borgmatic.hooks.dispatch
@@ -14,20 +14,17 @@ import borgmatic.hooks.dump
logger = logging.getLogger(__name__)
def create_borgmatic_manifest(config, config_paths, dry_run):
def create_borgmatic_manifest(config, config_paths, borgmatic_runtime_directory, dry_run):
'''
Create a borgmatic manifest file to store the paths to the configuration files used to create
the archive.
Given a configuration dict, a sequence of config file paths, the borgmatic runtime directory,
and whether this is a dry run, create a borgmatic manifest file to store the paths to the
configuration files used to create the archive.
'''
if dry_run:
return
borgmatic_source_directory = config.get(
'borgmatic_source_directory', borgmatic.borg.state.DEFAULT_BORGMATIC_SOURCE_DIRECTORY
)
borgmatic_manifest_path = os.path.expanduser(
os.path.join(borgmatic_source_directory, 'bootstrap', 'manifest.json')
borgmatic_manifest_path = os.path.join(
borgmatic_runtime_directory, 'bootstrap', 'manifest.json'
)
if not os.path.exists(borgmatic_manifest_path):
@@ -74,54 +71,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.dump.DATA_SOURCE_HOOK_NAMES,
borgmatic_runtime_directory,
global_arguments.dry_run,
)
stream_processes = [process for processes in active_dumps.values() for process in processes]
active_dumps = borgmatic.hooks.dispatch.call_hooks(
'dump_data_sources',
config,
repository['path'],
borgmatic.hooks.dump.DATA_SOURCE_HOOK_NAMES,
borgmatic_runtime_directory,
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,
stats=create_arguments.stats,
json=create_arguments.json,
list_files=create_arguments.list_files,
stream_processes=stream_processes,
)
if json_output:
yield borgmatic.actions.json.parse_json(json_output, repository.get('label'))
if config.get('store_config_files', True):
create_borgmatic_manifest(
config,
config_paths,
borgmatic_runtime_directory,
global_arguments.dry_run,
)
json_output = borgmatic.borg.create.create_archive(
global_arguments.dry_run,
repository['path'],
config,
config_paths,
local_borg_version,
global_arguments,
borgmatic_runtime_directory,
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:
yield borgmatic.actions.json.parse_json(json_output, repository.get('label'))
borgmatic.hooks.dispatch.call_hooks_even_if_unconfigured(
'remove_data_source_dumps',
config,
config_filename,
borgmatic.hooks.dump.DATA_SOURCE_HOOK_NAMES,
borgmatic_runtime_directory,
global_arguments.dry_run,
)
borgmatic.hooks.dispatch.call_hooks_even_if_unconfigured(
'remove_data_source_dumps',
config,
config_filename,
borgmatic.hooks.dump.DATA_SOURCE_HOOK_NAMES,
global_arguments.dry_run,
)
borgmatic.hooks.command.execute_hook(
config.get('after_backup'),
config.get('umask'),
+50
View File
@@ -0,0 +1,50 @@
import logging
import borgmatic.actions.arguments
import borgmatic.borg.delete
import borgmatic.borg.repo_delete
import borgmatic.borg.repo_list
logger = logging.getLogger(__name__)
def run_delete(
repository,
config,
local_borg_version,
delete_arguments,
global_arguments,
local_path,
remote_path,
):
'''
Run the "delete" action for the given repository and archive(s).
'''
if delete_arguments.repository is None or borgmatic.config.validate.repositories_match(
repository, delete_arguments.repository
):
logger.answer(f'{repository.get("label", repository["path"])}: Deleting archives')
archive_name = (
borgmatic.borg.repo_list.resolve_archive_name(
repository['path'],
delete_arguments.archive,
config,
local_borg_version,
global_arguments,
local_path,
remote_path,
)
if delete_arguments.archive
else None
)
borgmatic.borg.delete.delete_archives(
repository,
config,
local_borg_version,
borgmatic.actions.arguments.update_arguments(delete_arguments, archive=archive_name),
global_arguments,
local_path,
remote_path,
)
+2 -2
View File
@@ -1,7 +1,7 @@
import logging
import borgmatic.borg.export_tar
import borgmatic.borg.rlist
import borgmatic.borg.repo_list
import borgmatic.config.validate
logger = logging.getLogger(__name__)
@@ -28,7 +28,7 @@ def run_export_tar(
borgmatic.borg.export_tar.export_tar_archive(
global_arguments.dry_run,
repository['path'],
borgmatic.borg.rlist.resolve_archive_name(
borgmatic.borg.repo_list.resolve_archive_name(
repository['path'],
export_tar_arguments.archive,
config,
+2 -2
View File
@@ -1,7 +1,7 @@
import logging
import borgmatic.borg.extract
import borgmatic.borg.rlist
import borgmatic.borg.repo_list
import borgmatic.config.validate
import borgmatic.hooks.command
@@ -39,7 +39,7 @@ def run_extract(
borgmatic.borg.extract.extract_archive(
global_arguments.dry_run,
repository['path'],
borgmatic.borg.rlist.resolve_archive_name(
borgmatic.borg.repo_list.resolve_archive_name(
repository['path'],
extract_arguments.archive,
config,
+2 -2
View File
@@ -3,7 +3,7 @@ import logging
import borgmatic.actions.arguments
import borgmatic.actions.json
import borgmatic.borg.info
import borgmatic.borg.rlist
import borgmatic.borg.repo_list
import borgmatic.config.validate
logger = logging.getLogger(__name__)
@@ -30,7 +30,7 @@ def run_info(
logger.answer(
f'{repository.get("label", repository["path"])}: Displaying archive summary information'
)
archive_name = borgmatic.borg.rlist.resolve_archive_name(
archive_name = borgmatic.borg.repo_list.resolve_archive_name(
repository['path'],
info_arguments.archive,
config,
+1 -1
View File
@@ -31,7 +31,7 @@ def run_list(
elif not list_arguments.archive: # pragma: no cover
logger.answer(f'{repository.get("label", repository["path"])}: Listing archives')
archive_name = borgmatic.borg.rlist.resolve_archive_name(
archive_name = borgmatic.borg.repo_list.resolve_archive_name(
repository['path'],
list_arguments.archive,
config,
+2 -2
View File
@@ -1,7 +1,7 @@
import logging
import borgmatic.borg.mount
import borgmatic.borg.rlist
import borgmatic.borg.repo_list
import borgmatic.config.validate
logger = logging.getLogger(__name__)
@@ -31,7 +31,7 @@ def run_mount(
borgmatic.borg.mount.mount_archive(
repository['path'],
borgmatic.borg.rlist.resolve_archive_name(
borgmatic.borg.repo_list.resolve_archive_name(
repository['path'],
mount_arguments.archive,
config,
-41
View File
@@ -1,41 +0,0 @@
import logging
import borgmatic.borg.rcreate
import borgmatic.config.validate
logger = logging.getLogger(__name__)
def run_rcreate(
repository,
config,
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(f'{repository.get("label", repository["path"])}: Creating repository')
borgmatic.borg.rcreate.create_repository(
global_arguments.dry_run,
repository['path'],
config,
local_borg_version,
global_arguments,
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,
)
+41
View File
@@ -0,0 +1,41 @@
import logging
import borgmatic.borg.repo_create
import borgmatic.config.validate
logger = logging.getLogger(__name__)
def run_repo_create(
repository,
config,
local_borg_version,
repo_create_arguments,
global_arguments,
local_path,
remote_path,
):
'''
Run the "repo-create" action for the given repository.
'''
if repo_create_arguments.repository and not borgmatic.config.validate.repositories_match(
repository, repo_create_arguments.repository
):
return
logger.info(f'{repository.get("label", repository["path"])}: Creating repository')
borgmatic.borg.repo_create.create_repository(
global_arguments.dry_run,
repository['path'],
config,
local_borg_version,
global_arguments,
repo_create_arguments.encryption_mode,
repo_create_arguments.source_repository,
repo_create_arguments.copy_crypt_key,
repo_create_arguments.append_only,
repo_create_arguments.storage_quota,
repo_create_arguments.make_parent_dirs,
local_path=local_path,
remote_path=remote_path,
)
+36
View File
@@ -0,0 +1,36 @@
import logging
import borgmatic.borg.repo_delete
logger = logging.getLogger(__name__)
def run_repo_delete(
repository,
config,
local_borg_version,
repo_delete_arguments,
global_arguments,
local_path,
remote_path,
):
'''
Run the "repo-delete" action for the given repository.
'''
if repo_delete_arguments.repository is None or borgmatic.config.validate.repositories_match(
repository, repo_delete_arguments.repository
):
logger.answer(
f'{repository.get("label", repository["path"])}: Deleting repository'
+ (' cache' if repo_delete_arguments.cache_only else '')
)
borgmatic.borg.repo_delete.delete_repository(
repository,
config,
local_borg_version,
repo_delete_arguments,
global_arguments,
local_path,
remote_path,
)
@@ -1,39 +1,39 @@
import logging
import borgmatic.actions.json
import borgmatic.borg.rinfo
import borgmatic.borg.repo_info
import borgmatic.config.validate
logger = logging.getLogger(__name__)
def run_rinfo(
def run_repo_info(
repository,
config,
local_borg_version,
rinfo_arguments,
repo_info_arguments,
global_arguments,
local_path,
remote_path,
):
'''
Run the "rinfo" action for the given repository.
Run the "repo-info" action for the given repository.
If rinfo_arguments.json is True, yield the JSON output from the info for the repository.
If repo_info_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 repo_info_arguments.repository is None or borgmatic.config.validate.repositories_match(
repository, repo_info_arguments.repository
):
if not rinfo_arguments.json:
if not repo_info_arguments.json:
logger.answer(
f'{repository.get("label", repository["path"])}: Displaying repository summary information'
)
json_output = borgmatic.borg.rinfo.display_repository_info(
json_output = borgmatic.borg.repo_info.display_repository_info(
repository['path'],
config,
local_borg_version,
rinfo_arguments=rinfo_arguments,
repo_info_arguments=repo_info_arguments,
global_arguments=global_arguments,
local_path=local_path,
remote_path=remote_path,
@@ -1,37 +1,37 @@
import logging
import borgmatic.actions.json
import borgmatic.borg.rlist
import borgmatic.borg.repo_list
import borgmatic.config.validate
logger = logging.getLogger(__name__)
def run_rlist(
def run_repo_list(
repository,
config,
local_borg_version,
rlist_arguments,
repo_list_arguments,
global_arguments,
local_path,
remote_path,
):
'''
Run the "rlist" action for the given repository.
Run the "repo-list" action for the given repository.
If rlist_arguments.json is True, yield the JSON output from listing the repository.
If repo_list_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 repo_list_arguments.repository is None or borgmatic.config.validate.repositories_match(
repository, repo_list_arguments.repository
):
if not rlist_arguments.json:
if not repo_list_arguments.json:
logger.answer(f'{repository.get("label", repository["path"])}: Listing repository')
json_output = borgmatic.borg.rlist.list_repository(
json_output = borgmatic.borg.repo_list.list_repository(
repository['path'],
config,
local_borg_version,
rlist_arguments=rlist_arguments,
repo_list_arguments=repo_list_arguments,
global_arguments=global_arguments,
local_path=local_path,
remote_path=remote_path,
+213 -134
View File
@@ -1,12 +1,15 @@
import copy
import logging
import os
import pathlib
import shutil
import tempfile
import borgmatic.borg.extract
import borgmatic.borg.list
import borgmatic.borg.mount
import borgmatic.borg.rlist
import borgmatic.borg.state
import borgmatic.borg.repo_list
import borgmatic.config.paths
import borgmatic.config.validate
import borgmatic.hooks.dispatch
import borgmatic.hooks.dump
@@ -61,6 +64,39 @@ def get_configured_data_source(
)
def strip_path_prefix_from_extracted_dump_destination(
destination_path, borgmatic_runtime_directory
):
'''
Directory-format dump files get extracted into a temporary directory containing a path prefix
that depends how the files were stored in the archive. So, given the destination path where the
dump was extracted and the borgmatic runtime directory, move the dump files such that the
restore doesn't have to deal with that varying path prefix.
For instance, if the dump was extracted to:
/run/user/0/borgmatic/tmp1234/borgmatic/postgresql_databases/test/...
or:
/run/user/0/borgmatic/tmp1234/root/.borgmatic/postgresql_databases/test/...
then this function moves it to:
/run/user/0/borgmatic/postgresql_databases/test/...
'''
for subdirectory_path, _, _ in os.walk(destination_path):
databases_directory = os.path.basename(subdirectory_path)
if not databases_directory.endswith('_databases'):
continue
shutil.move(
subdirectory_path, os.path.join(borgmatic_runtime_directory, databases_directory)
)
break
def restore_single_data_source(
repository,
config,
@@ -72,7 +108,8 @@ def restore_single_data_source(
hook_name,
data_source,
connection_params,
): # pragma: no cover
borgmatic_runtime_directory,
):
'''
Given (among other things) an archive name, a data source hook name, the hostname, port,
username/password as connection params, and a configured data source configuration dict, restore
@@ -82,31 +119,48 @@ def restore_single_data_source(
f'{repository.get("label", repository["path"])}: Restoring data source {data_source["name"]}'
)
dump_pattern = borgmatic.hooks.dispatch.call_hooks(
'make_data_source_dump_pattern',
dump_patterns = borgmatic.hooks.dispatch.call_hooks(
'make_data_source_dump_patterns',
config,
repository['path'],
borgmatic.hooks.dump.DATA_SOURCE_HOOK_NAMES,
borgmatic_runtime_directory,
data_source['name'],
)[hook_name]
# Kick off a single data source extract to stdout.
extract_process = borgmatic.borg.extract.extract_archive(
dry_run=global_arguments.dry_run,
repository=repository['path'],
archive=archive_name,
paths=borgmatic.hooks.dump.convert_glob_patterns_to_borg_patterns([dump_pattern]),
config=config,
local_borg_version=local_borg_version,
global_arguments=global_arguments,
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(data_source.get('format') != 'directory'),
destination_path = (
tempfile.mkdtemp(dir=borgmatic_runtime_directory)
if data_source.get('format') == 'directory'
else None
)
try:
# Kick off a single data source extract. If using a directory format, extract to a temporary
# directory. Otherwise extract the single dump file to stdout.
extract_process = borgmatic.borg.extract.extract_archive(
dry_run=global_arguments.dry_run,
repository=repository['path'],
archive=archive_name,
paths=[borgmatic.hooks.dump.convert_glob_patterns_to_borg_pattern(dump_patterns)],
config=config,
local_borg_version=local_borg_version,
global_arguments=global_arguments,
local_path=local_path,
remote_path=remote_path,
destination_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(data_source.get('format') != 'directory'),
)
if destination_path and not global_arguments.dry_run:
strip_path_prefix_from_extracted_dump_destination(
destination_path, borgmatic_runtime_directory
)
finally:
if destination_path and not global_arguments.dry_run:
shutil.rmtree(destination_path, ignore_errors=True)
# Run a single data source restore, consuming the extract stdout (if any).
borgmatic.hooks.dispatch.call_hooks(
function_name='restore_data_source_dump',
@@ -117,6 +171,7 @@ def restore_single_data_source(
dry_run=global_arguments.dry_run,
extract_process=extract_process,
connection_params=connection_params,
borgmatic_runtime_directory=borgmatic_runtime_directory,
)
@@ -128,18 +183,21 @@ def collect_archive_data_source_names(
global_arguments,
local_path,
remote_path,
borgmatic_runtime_directory,
):
'''
Given a local or remote repository path, a resolved archive name, a configuration dict, the
local Borg version, global_arguments an argparse.Namespace, and local and remote Borg paths,
query the archive for the names of data sources it contains as dumps and return them as a dict
from hook name to a sequence of data source names.
local Borg version, global_arguments an argparse.Namespace, local and remote Borg paths, and the
borgmatic runtime directory, query the archive for the names of data sources it contains as
dumps and return them as a dict from hook name to a sequence of data source names.
'''
borgmatic_source_directory = os.path.expanduser(
config.get(
'borgmatic_source_directory', borgmatic.borg.state.DEFAULT_BORGMATIC_SOURCE_DIRECTORY
)
).lstrip('/')
borgmatic_source_directory = str(
pathlib.Path(borgmatic.config.paths.get_borgmatic_source_directory(config))
)
# Probe for the data source dumps 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 dumps from previously created archives as well.
dump_paths = borgmatic.borg.list.capture_archive_listing(
repository,
archive,
@@ -147,10 +205,13 @@ def collect_archive_data_source_names(
local_borg_version,
global_arguments,
list_paths=[
os.path.expanduser(
borgmatic.hooks.dump.make_data_source_dump_path(borgmatic_source_directory, pattern)
'sh:'
+ borgmatic.hooks.dump.make_data_source_dump_path(base_directory, '*_databases/*/*')
for base_directory in (
'borgmatic',
borgmatic.config.paths.make_runtime_directory_glob(borgmatic_runtime_directory),
borgmatic_source_directory.lstrip('/'),
)
for pattern in ('*_databases/*/*',)
],
local_path=local_path,
remote_path=remote_path,
@@ -161,17 +222,28 @@ def collect_archive_data_source_names(
archive_data_source_names = {}
for dump_path in dump_paths:
try:
(hook_name, _, data_source_name) = dump_path.split(
borgmatic_source_directory + os.path.sep, 1
)[1].split(os.path.sep)[0:3]
except (ValueError, IndexError):
if not dump_path:
continue
for base_directory in (
'borgmatic',
borgmatic_runtime_directory,
borgmatic_source_directory,
):
try:
(hook_name, _, data_source_name) = dump_path.split(base_directory + os.path.sep, 1)[
1
].split(os.path.sep)[0:3]
except (ValueError, IndexError):
pass
else:
if data_source_name not in archive_data_source_names.get(hook_name, []):
archive_data_source_names.setdefault(hook_name, []).extend([data_source_name])
break
else:
logger.warning(
f'{repository}: Ignoring invalid data source dump path "{dump_path}" in archive {archive}'
)
else:
if data_source_name not in archive_data_source_names.get(hook_name, []):
archive_data_source_names.setdefault(hook_name, []).extend([data_source_name])
return archive_data_source_names
@@ -242,7 +314,7 @@ def ensure_data_sources_found(restore_names, remaining_restore_names, found_name
)
if not combined_restore_names and not found_names:
raise ValueError('No data sources were found to restore')
raise ValueError('No data source dumps were found to restore')
missing_names = sorted(set(combined_restore_names) - set(found_names))
if missing_names:
@@ -272,109 +344,116 @@ def run_restore(
):
return
logger.info(
f'{repository.get("label", repository["path"])}: Restoring data sources from archive {restore_arguments.archive}'
)
log_prefix = repository.get('label', repository['path'])
logger.info(f'{log_prefix}: Restoring data sources from archive {restore_arguments.archive}')
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,
)
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,
repository['path'],
borgmatic.hooks.dump.DATA_SOURCE_HOOK_NAMES,
borgmatic_runtime_directory,
global_arguments.dry_run,
)
archive_name = borgmatic.borg.rlist.resolve_archive_name(
repository['path'],
restore_arguments.archive,
config,
local_borg_version,
global_arguments,
local_path,
remote_path,
)
archive_data_source_names = collect_archive_data_source_names(
repository['path'],
archive_name,
config,
local_borg_version,
global_arguments,
local_path,
remote_path,
)
restore_names = find_data_sources_to_restore(
restore_arguments.data_sources, archive_data_source_names
)
found_names = set()
remaining_restore_names = {}
connection_params = {
'hostname': restore_arguments.hostname,
'port': restore_arguments.port,
'username': restore_arguments.username,
'password': restore_arguments.password,
'restore_path': restore_arguments.restore_path,
}
archive_name = borgmatic.borg.repo_list.resolve_archive_name(
repository['path'],
restore_arguments.archive,
config,
local_borg_version,
global_arguments,
local_path,
remote_path,
)
archive_data_source_names = collect_archive_data_source_names(
repository['path'],
archive_name,
config,
local_borg_version,
global_arguments,
local_path,
remote_path,
borgmatic_runtime_directory,
)
restore_names = find_data_sources_to_restore(
restore_arguments.data_sources, archive_data_source_names
)
found_names = set()
remaining_restore_names = {}
connection_params = {
'hostname': restore_arguments.hostname,
'port': restore_arguments.port,
'username': restore_arguments.username,
'password': restore_arguments.password,
'restore_path': restore_arguments.restore_path,
}
for hook_name, data_source_names in restore_names.items():
for data_source_name in data_source_names:
found_hook_name, found_data_source = get_configured_data_source(
config, archive_data_source_names, hook_name, data_source_name
)
if not found_data_source:
remaining_restore_names.setdefault(found_hook_name or hook_name, []).append(
data_source_name
for hook_name, data_source_names in restore_names.items():
for data_source_name in data_source_names:
found_hook_name, found_data_source = get_configured_data_source(
config, archive_data_source_names, hook_name, data_source_name
)
continue
found_names.add(data_source_name)
restore_single_data_source(
repository,
config,
local_borg_version,
global_arguments,
local_path,
remote_path,
archive_name,
found_hook_name or hook_name,
dict(found_data_source, **{'schemas': restore_arguments.schemas}),
connection_params,
)
if not found_data_source:
remaining_restore_names.setdefault(found_hook_name or hook_name, []).append(
data_source_name
)
continue
# For any data sources that weren't found via exact matches in the configuration, try to
# fallback to "all" entries.
for hook_name, data_source_names in remaining_restore_names.items():
for data_source_name in data_source_names:
found_hook_name, found_data_source = get_configured_data_source(
config, archive_data_source_names, hook_name, data_source_name, 'all'
)
found_names.add(data_source_name)
restore_single_data_source(
repository,
config,
local_borg_version,
global_arguments,
local_path,
remote_path,
archive_name,
found_hook_name or hook_name,
dict(found_data_source, **{'schemas': restore_arguments.schemas}),
connection_params,
borgmatic_runtime_directory,
)
if not found_data_source:
continue
# For any data sources that weren't found via exact matches in the configuration, try to
# fallback to "all" entries.
for hook_name, data_source_names in remaining_restore_names.items():
for data_source_name in data_source_names:
found_hook_name, found_data_source = get_configured_data_source(
config, archive_data_source_names, hook_name, data_source_name, 'all'
)
found_names.add(data_source_name)
data_source = copy.copy(found_data_source)
data_source['name'] = data_source_name
if not found_data_source:
continue
restore_single_data_source(
repository,
config,
local_borg_version,
global_arguments,
local_path,
remote_path,
archive_name,
found_hook_name or hook_name,
dict(data_source, **{'schemas': restore_arguments.schemas}),
connection_params,
)
found_names.add(data_source_name)
data_source = copy.copy(found_data_source)
data_source['name'] = data_source_name
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,
)
restore_single_data_source(
repository,
config,
local_borg_version,
global_arguments,
local_path,
remote_path,
archive_name,
found_hook_name or hook_name,
dict(data_source, **{'schemas': restore_arguments.schemas}),
connection_params,
borgmatic_runtime_directory,
)
borgmatic.hooks.dispatch.call_hooks_even_if_unconfigured(
'remove_data_source_dumps',
config,
repository['path'],
borgmatic.hooks.dump.DATA_SOURCE_HOOK_NAMES,
borgmatic_runtime_directory,
global_arguments.dry_run,
)
ensure_data_sources_found(restore_names, remaining_restore_names, found_names)
+2
View File
@@ -2,6 +2,7 @@ import logging
import shlex
import borgmatic.commands.arguments
import borgmatic.config.paths
import borgmatic.logger
from borgmatic.borg import environment, flags
from borgmatic.execute import DO_NOT_CAPTURE, execute_command
@@ -67,6 +68,7 @@ def run_arbitrary_borg(
'ARCHIVE': archive if archive else '',
},
),
working_directory=borgmatic.config.paths.get_working_directory(config),
borg_local_path=local_path,
borg_exit_codes=config.get('borg_exit_codes'),
)
+2
View File
@@ -1,5 +1,6 @@
import logging
import borgmatic.config.paths
from borgmatic.borg import environment, flags
from borgmatic.execute import execute_command
@@ -37,6 +38,7 @@ def break_lock(
execute_command(
full_command,
extra_environment=borg_environment,
working_directory=borgmatic.config.paths.get_working_directory(config),
borg_local_path=local_path,
borg_exit_codes=config.get('borg_exit_codes'),
)
+67
View File
@@ -0,0 +1,67 @@
import logging
import borgmatic.config.paths
import borgmatic.execute
import borgmatic.logger
from borgmatic.borg import environment, flags
logger = logging.getLogger(__name__)
def change_passphrase(
repository_path,
config,
local_borg_version,
change_passphrase_arguments,
global_arguments,
local_path='borg',
remote_path=None,
):
'''
Given a local or remote repository path, a configuration dict, the local Borg version, change
passphrase arguments, and optional local and remote Borg paths, change the repository passphrase
based on an interactive prompt.
'''
borgmatic.logger.add_custom_log_levels()
umask = config.get('umask', None)
lock_wait = config.get('lock_wait', None)
full_command = (
(local_path, 'key', 'change-passphrase')
+ (('--remote-path', remote_path) if remote_path else ())
+ (('--umask', str(umask)) if umask else ())
+ (('--log-json',) if global_arguments.log_json else ())
+ (('--lock-wait', str(lock_wait)) if lock_wait else ())
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
+ (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
+ flags.make_repository_flags(
repository_path,
local_borg_version,
)
)
if global_arguments.dry_run:
logger.info(f'{repository_path}: Skipping change password (dry run)')
return
# If the original passphrase is set programmatically, then Borg won't prompt for a new one! So
# don't give Borg any passphrase, and it'll ask the user for both old and new ones.
config_without_passphrase = {
option_name: value
for (option_name, value) in config.items()
if option_name not in ('encryption_passphrase', 'encryption_passcommand')
}
borgmatic.execute.execute_command(
full_command,
output_file=borgmatic.execute.DO_NOT_CAPTURE,
output_log_level=logging.ANSWER,
extra_environment=environment.make_environment(config_without_passphrase),
working_directory=borgmatic.config.paths.get_working_directory(config),
borg_local_path=local_path,
borg_exit_codes=config.get('borg_exit_codes'),
)
logger.answer(
f"{repository_path}: Don't forget to update your encryption_passphrase option (if needed)"
)
+9 -2
View File
@@ -2,7 +2,8 @@ import argparse
import json
import logging
from borgmatic.borg import environment, feature, flags, rinfo
import borgmatic.config.paths
from borgmatic.borg import environment, feature, flags, repo_info
from borgmatic.execute import DO_NOT_CAPTURE, execute_command
logger = logging.getLogger(__name__)
@@ -94,7 +95,7 @@ def get_repository_id(
'''
try:
return json.loads(
rinfo.display_repository_info(
repo_info.display_repository_info(
repository_path,
config,
local_borg_version,
@@ -149,6 +150,7 @@ def check_archives(
)
max_duration = check_arguments.max_duration or repository_check_config.get('max_duration')
umask = config.get('umask')
borg_environment = environment.make_environment(config)
borg_exit_codes = config.get('borg_exit_codes')
@@ -159,6 +161,7 @@ def check_archives(
+ (('--max-duration', str(max_duration)) if max_duration else ())
+ make_check_name_flags(checks, archive_filter_flags)
+ (('--remote-path', remote_path) if remote_path else ())
+ (('--umask', str(umask)) if umask else ())
+ (('--log-json',) if global_arguments.log_json else ())
+ (('--lock-wait', str(lock_wait)) if lock_wait else ())
+ verbosity_flags
@@ -167,6 +170,8 @@ def check_archives(
+ flags.make_repository_flags(repository_path, local_borg_version)
)
working_directory = borgmatic.config.paths.get_working_directory(config)
# The Borg repair option triggers an interactive prompt, which won't work when output is
# captured. And progress messes with the terminal directly.
if check_arguments.repair or check_arguments.progress:
@@ -174,6 +179,7 @@ def check_archives(
full_command,
output_file=DO_NOT_CAPTURE,
extra_environment=borg_environment,
working_directory=working_directory,
borg_local_path=local_path,
borg_exit_codes=borg_exit_codes,
)
@@ -181,6 +187,7 @@ def check_archives(
execute_command(
full_command,
extra_environment=borg_environment,
working_directory=working_directory,
borg_local_path=local_path,
borg_exit_codes=borg_exit_codes,
)
+2
View File
@@ -1,5 +1,6 @@
import logging
import borgmatic.config.paths
from borgmatic.borg import environment, flags
from borgmatic.execute import execute_command
@@ -49,6 +50,7 @@ def compact_segments(
full_command,
output_log_level=logging.INFO,
extra_environment=environment.make_environment(config),
working_directory=borgmatic.config.paths.get_working_directory(config),
borg_local_path=local_path,
borg_exit_codes=config.get('borg_exit_codes'),
)
+66 -47
View File
@@ -5,9 +5,11 @@ import os
import pathlib
import stat
import tempfile
import textwrap
import borgmatic.config.paths
import borgmatic.logger
from borgmatic.borg import environment, feature, flags, state
from borgmatic.borg import environment, feature, flags
from borgmatic.execute import (
DO_NOT_CAPTURE,
execute_command,
@@ -18,26 +20,28 @@ from borgmatic.execute import (
logger = logging.getLogger(__name__)
def expand_directory(directory):
def expand_directory(directory, working_directory):
'''
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.
'''
expanded_directory = os.path.expanduser(directory)
expanded_directory = os.path.join(working_directory or '', os.path.expanduser(directory))
return glob.glob(expanded_directory) or [expanded_directory]
def expand_directories(directories):
def expand_directories(directories, working_directory=None):
'''
Given a sequence of directory paths, expand tildes and globs in each one. Return all the
resulting directories as a single flattened tuple.
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) for directory in directories)
itertools.chain.from_iterable(
expand_directory(directory, working_directory) for directory in directories
)
)
@@ -52,17 +56,19 @@ def expand_home_directories(directories):
return tuple(os.path.expanduser(directory) for directory in directories)
def map_directories_to_devices(directories):
def map_directories_to_devices(directories, working_directory=None):
'''
Given a sequence of directories, return a map from directory to an identifier for the device on
which that directory resides or None if the path doesn't exist.
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(directory).st_dev if os.path.exists(directory) else None
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),)
}
@@ -215,18 +221,13 @@ def make_list_filter_flags(local_borg_version, dry_run):
return f'{base_flags}-'
def collect_borgmatic_source_directories(borgmatic_source_directory):
def collect_borgmatic_runtime_directories(borgmatic_runtime_directory):
'''
Return a list of borgmatic-specific source directories used for state like database backups.
Return a list of borgmatic-specific runtime directories used for temporary runtime data like
streaming database dumps and bootstrap metadata. If no such directories exist, return an empty
list.
'''
if not borgmatic_source_directory:
borgmatic_source_directory = state.DEFAULT_BORGMATIC_SOURCE_DIRECTORY
return (
[borgmatic_source_directory]
if os.path.exists(os.path.expanduser(borgmatic_source_directory))
else []
)
return [borgmatic_runtime_directory] if os.path.exists(borgmatic_runtime_directory) else []
ROOT_PATTERN_PREFIX = 'R '
@@ -306,20 +307,29 @@ def collect_special_file_paths(
)
def check_all_source_directories_exist(source_directories):
def check_all_source_directories_exist(source_directories, working_directory=None):
'''
Given a sequence of source directories, check that they all exist. If any do not, raise an
exception.
Given a sequence of source directories and an optional working directory to serve as a prefix
for each (if it's a relative directory), check that the source directories all exist. If any do
not, raise an exception.
'''
missing_directories = [
source_directory
for source_directory in source_directories
if not all([os.path.exists(directory) for directory in expand_directory(source_directory)])
if not all(
[
os.path.exists(os.path.join(working_directory or '', directory))
for directory in expand_directory(source_directory, working_directory)
]
)
]
if missing_directories:
raise ValueError(f"Source directories do not exist: {', '.join(missing_directories)}")
MAX_SPECIAL_FILE_PATHS_LENGTH = 1000
def make_base_create_command(
dry_run,
repository_path,
@@ -327,7 +337,7 @@ def make_base_create_command(
config_paths,
local_borg_version,
global_arguments,
borgmatic_source_directories,
borgmatic_runtime_directories,
local_path='borg',
remote_path=None,
progress=False,
@@ -342,19 +352,27 @@ def make_base_create_command(
(base Borg create command flags, Borg create command positional arguments, open pattern file
handle, open exclude file handle).
'''
working_directory = borgmatic.config.paths.get_working_directory(config)
if config.get('source_directories_must_exist', False):
check_all_source_directories_exist(config.get('source_directories'))
check_all_source_directories_exist(
config.get('source_directories'), working_directory=working_directory
)
sources = deduplicate_directories(
map_directories_to_devices(
expand_directories(
tuple(config.get('source_directories', ()))
+ borgmatic_source_directories
+ tuple(config_paths if config.get('store_config_files', True) else ())
+ borgmatic_runtime_directories
+ tuple(config_paths if config.get('store_config_files', True) else ()),
working_directory=working_directory,
)
),
additional_directory_devices=map_directories_to_devices(
expand_directories(pattern_root_directories(config.get('patterns')))
expand_directories(
pattern_root_directories(config.get('patterns')),
working_directory=working_directory,
)
),
)
@@ -376,7 +394,9 @@ def make_base_create_command(
lock_wait = config.get('lock_wait', None)
list_filter_flags = make_list_filter_flags(local_borg_version, dry_run)
files_cache = config.get('files_cache')
archive_name_format = config.get('archive_name_format', flags.DEFAULT_ARCHIVE_NAME_FORMAT)
archive_name_format = config.get(
'archive_name_format', flags.get_default_archive_name_format(local_borg_version)
)
extra_borg_options = config.get('extra_borg_options', {}).get('create', '')
if feature.available(feature.Feature.ATIME, local_borg_version):
@@ -414,7 +434,7 @@ def make_base_create_command(
+ (('--compression', compression) if compression else ())
+ upload_ratelimit_flags
+ (('--upload-buffer', str(upload_buffer_size)) if upload_buffer_size else ())
+ (('--one-file-system',) if config.get('one_file_system') or stream_processes else ())
+ (('--one-file-system',) if config.get('one_file_system') else ())
+ numeric_ids_flags
+ atime_flags
+ (('--noctime',) if config.get('ctime') is False else ())
@@ -445,11 +465,6 @@ def make_base_create_command(
logger.warning(
f'{repository_path}: Ignoring configured "read_special" value of false, as true is needed for database hooks.'
)
try:
working_directory = os.path.expanduser(config.get('working_directory'))
except TypeError:
working_directory = None
borg_environment = environment.make_environment(config)
logger.debug(f'{repository_path}: Collecting special file paths')
@@ -459,12 +474,17 @@ def make_base_create_command(
local_path,
working_directory,
borg_environment,
skip_directories=borgmatic_source_directories,
skip_directories=borgmatic_runtime_directories,
)
if special_file_paths:
truncated_special_file_paths = textwrap.shorten(
', '.join(special_file_paths),
width=MAX_SPECIAL_FILE_PATHS_LENGTH,
placeholder=' ...',
)
logger.warning(
f'{repository_path}: Excluding special files to prevent Borg from hanging: {", ".join(special_file_paths)}'
f'{repository_path}: Excluding special files to prevent Borg from hanging: {truncated_special_file_paths}'
)
exclude_file = write_pattern_file(
expand_home_directories(
@@ -484,6 +504,7 @@ def create_archive(
config_paths,
local_borg_version,
global_arguments,
borgmatic_runtime_directory,
local_path='borg',
remote_path=None,
progress=False,
@@ -501,8 +522,11 @@ def create_archive(
create command while also triggering the given processes to produce output.
'''
borgmatic.logger.add_custom_log_levels()
borgmatic_source_directories = expand_directories(
collect_borgmatic_source_directories(config.get('borgmatic_source_directory'))
working_directory = borgmatic.config.paths.get_working_directory(config)
borgmatic_runtime_directories = expand_directories(
collect_borgmatic_runtime_directories(borgmatic_runtime_directory),
working_directory=working_directory,
)
(create_flags, create_positional_arguments, pattern_file, exclude_file) = (
@@ -513,7 +537,7 @@ def create_archive(
config_paths,
local_borg_version,
global_arguments,
borgmatic_source_directories,
borgmatic_runtime_directories,
local_path,
remote_path,
progress,
@@ -534,11 +558,6 @@ def create_archive(
# the terminal directly.
output_file = DO_NOT_CAPTURE if progress else None
try:
working_directory = os.path.expanduser(config.get('working_directory'))
except TypeError:
working_directory = None
borg_environment = environment.make_environment(config)
create_flags += (
+135
View File
@@ -0,0 +1,135 @@
import argparse
import logging
import borgmatic.borg.environment
import borgmatic.borg.feature
import borgmatic.borg.flags
import borgmatic.borg.repo_delete
import borgmatic.config.paths
import borgmatic.execute
logger = logging.getLogger(__name__)
def make_delete_command(
repository,
config,
local_borg_version,
delete_arguments,
global_arguments,
local_path,
remote_path,
):
'''
Given a local or remote repository dict, a configuration dict, the local Borg version, the
arguments to the delete action as an argparse.Namespace, and global arguments, return a command
as a tuple to delete archives from the repository.
'''
return (
(local_path, 'delete')
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
+ (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
+ borgmatic.borg.flags.make_flags('dry-run', global_arguments.dry_run)
+ borgmatic.borg.flags.make_flags('remote-path', remote_path)
+ borgmatic.borg.flags.make_flags('umask', config.get('umask'))
+ borgmatic.borg.flags.make_flags('log-json', global_arguments.log_json)
+ borgmatic.borg.flags.make_flags('lock-wait', config.get('lock_wait'))
+ borgmatic.borg.flags.make_flags('list', delete_arguments.list_archives)
+ (
(('--force',) + (('--force',) if delete_arguments.force >= 2 else ()))
if delete_arguments.force
else ()
)
# Ignore match_archives and archive_name_format options from configuration, so the user has
# to be explicit on the command-line about the archives they want to delete.
+ borgmatic.borg.flags.make_match_archives_flags(
delete_arguments.match_archives or delete_arguments.archive,
archive_name_format=None,
local_borg_version=local_borg_version,
default_archive_name_format='*',
)
+ borgmatic.borg.flags.make_flags_from_arguments(
delete_arguments,
excludes=('list_archives', 'force', 'match_archives', 'archive', 'repository'),
)
+ borgmatic.borg.flags.make_repository_flags(repository['path'], local_borg_version)
)
ARCHIVE_RELATED_ARGUMENT_NAMES = (
'archive',
'match_archives',
'first',
'last',
'oldest',
'newest',
'older',
'newer',
)
def delete_archives(
repository,
config,
local_borg_version,
delete_arguments,
global_arguments,
local_path='borg',
remote_path=None,
):
'''
Given a local or remote repository dict, a configuration dict, the local Borg version, the
arguments to the delete action as an argparse.Namespace, global arguments as an
argparse.Namespace, and local and remote Borg paths, delete the selected archives from the
repository. If no archives are selected, then delete the entire repository.
'''
borgmatic.logger.add_custom_log_levels()
if not any(
getattr(delete_arguments, argument_name, None)
for argument_name in ARCHIVE_RELATED_ARGUMENT_NAMES
):
if borgmatic.borg.feature.available(
borgmatic.borg.feature.Feature.REPO_DELETE, local_borg_version
):
logger.warning(
'Deleting an entire repository with the delete action is deprecated when using Borg 2.x+. Use the repo-delete action instead.'
)
repo_delete_arguments = argparse.Namespace(
repository=repository['path'],
list_archives=delete_arguments.list_archives,
force=delete_arguments.force,
cache_only=delete_arguments.cache_only,
keep_security_info=delete_arguments.keep_security_info,
)
borgmatic.borg.repo_delete.delete_repository(
repository,
config,
local_borg_version,
repo_delete_arguments,
global_arguments,
local_path,
remote_path,
)
return
command = make_delete_command(
repository,
config,
local_borg_version,
delete_arguments,
global_arguments,
local_path,
remote_path,
)
borgmatic.execute.execute_command(
command,
output_log_level=logging.ANSWER,
extra_environment=borgmatic.borg.environment.make_environment(config),
working_directory=borgmatic.config.paths.get_working_directory(config),
borg_local_path=local_path,
borg_exit_codes=config.get('borg_exit_codes'),
)
+4 -2
View File
@@ -1,3 +1,5 @@
import os
OPTION_TO_ENVIRONMENT_VARIABLE = {
'borg_base_directory': 'BORG_BASE_DIR',
'borg_config_directory': 'BORG_CONFIG_DIR',
@@ -38,8 +40,8 @@ def make_environment(config):
option_name,
environment_variable_name,
) in DEFAULT_BOOL_OPTION_TO_DOWNCASE_ENVIRONMENT_VARIABLE.items():
value = config.get(option_name)
if value is not None:
if os.environ.get(environment_variable_name) is None:
value = config.get(option_name)
environment[environment_variable_name] = 'yes' if value else 'no'
for (
+8 -5
View File
@@ -1,6 +1,7 @@
import logging
import os
import borgmatic.config.paths
import borgmatic.logger
from borgmatic.borg import environment, flags
from borgmatic.execute import DO_NOT_CAPTURE, execute_command
@@ -18,9 +19,9 @@ def export_key(
remote_path=None,
):
'''
Given a local or remote repository path, a configuration dict, the local Borg version, and
optional local and remote Borg paths, export the repository key to the destination path
indicated in the export arguments.
Given a local or remote repository path, a configuration dict, the local Borg version, export
arguments, and optional local and remote Borg paths, export the repository key to the
destination path indicated in the export arguments.
If the destination path is empty or "-", then print the key to stdout instead of to a file.
@@ -29,9 +30,10 @@ def export_key(
borgmatic.logger.add_custom_log_levels()
umask = config.get('umask', None)
lock_wait = config.get('lock_wait', None)
working_directory = borgmatic.config.paths.get_working_directory(config)
if export_arguments.path and export_arguments.path != '-':
if os.path.exists(export_arguments.path):
if os.path.exists(os.path.join(working_directory or '', export_arguments.path)):
raise FileExistsError(
f'Destination path {export_arguments.path} already exists. Aborting.'
)
@@ -58,7 +60,7 @@ def export_key(
)
if global_arguments.dry_run:
logging.info(f'{repository_path}: Skipping key export (dry run)')
logger.info(f'{repository_path}: Skipping key export (dry run)')
return
execute_command(
@@ -66,6 +68,7 @@ def export_key(
output_file=output_file,
output_log_level=logging.ANSWER,
extra_environment=environment.make_environment(config),
working_directory=working_directory,
borg_local_path=local_path,
borg_exit_codes=config.get('borg_exit_codes'),
)
+2
View File
@@ -1,5 +1,6 @@
import logging
import borgmatic.config.paths
import borgmatic.logger
from borgmatic.borg import environment, flags
from borgmatic.execute import DO_NOT_CAPTURE, execute_command
@@ -70,6 +71,7 @@ def export_tar_archive(
output_file=DO_NOT_CAPTURE if destination_path == '-' else None,
output_log_level=output_log_level,
extra_environment=environment.make_environment(config),
working_directory=borgmatic.config.paths.get_working_directory(config),
borg_local_path=local_path,
borg_exit_codes=config.get('borg_exit_codes'),
)
+22 -10
View File
@@ -2,8 +2,9 @@ import logging
import os
import subprocess
import borgmatic.config.paths
import borgmatic.config.validate
from borgmatic.borg import environment, feature, flags, rlist
from borgmatic.borg import environment, feature, flags, repo_list
from borgmatic.execute import DO_NOT_CAPTURE, execute_command
logger = logging.getLogger(__name__)
@@ -29,7 +30,7 @@ def extract_last_archive_dry_run(
verbosity_flags = ('--info',)
try:
last_archive_name = rlist.resolve_archive_name(
last_archive_name = repo_list.resolve_archive_name(
repository_path,
'latest',
config,
@@ -58,8 +59,8 @@ def extract_last_archive_dry_run(
execute_command(
full_extract_command,
working_directory=None,
extra_environment=borg_environment,
working_directory=borgmatic.config.paths.get_working_directory(config),
borg_local_path=local_path,
borg_exit_codes=config.get('borg_exit_codes'),
)
@@ -109,9 +110,14 @@ def extract_archive(
# is so this doesn't count the final path component, e.g. the filename itself.
strip_components = max(
0,
*(len(tuple(piece for piece in path.split(os.path.sep) if piece)) - 1 for path in paths)
*(
len(tuple(piece for piece in path.split(os.path.sep) if piece)) - 1
for path in paths
),
)
working_directory = borgmatic.config.paths.get_working_directory(config)
full_command = (
(local_path, 'extract')
+ (('--remote-path', remote_path) if remote_path else ())
@@ -126,9 +132,12 @@ def extract_archive(
+ (('--progress',) if progress else ())
+ (('--stdout',) if extract_to_stdout else ())
+ flags.make_repository_archive_flags(
# Make the repository path absolute so the working directory changes below don't
# prevent Borg from finding the repo.
borgmatic.config.validate.normalize_repository_path(repository),
# Make the repository path absolute so the destination directory used below via changing
# the working directory doesn't prevent Borg from finding the repo. But also apply the
# user's configured working directory (if any) to the repo path.
borgmatic.config.validate.normalize_repository_path(
os.path.join(working_directory or '', repository)
),
archive,
local_borg_version,
)
@@ -137,6 +146,9 @@ def extract_archive(
borg_environment = environment.make_environment(config)
borg_exit_codes = config.get('borg_exit_codes')
full_destination_path = (
os.path.join(working_directory or '', destination_path) if destination_path else None
)
# The progress output isn't compatible with captured and logged output, as progress messes with
# the terminal directly.
@@ -144,8 +156,8 @@ def extract_archive(
return execute_command(
full_command,
output_file=DO_NOT_CAPTURE,
working_directory=destination_path,
extra_environment=borg_environment,
working_directory=full_destination_path,
borg_local_path=local_path,
borg_exit_codes=borg_exit_codes,
)
@@ -155,9 +167,9 @@ def extract_archive(
return execute_command(
full_command,
output_file=subprocess.PIPE,
working_directory=destination_path,
run_to_completion=False,
extra_environment=borg_environment,
working_directory=full_destination_path,
borg_local_path=local_path,
borg_exit_codes=borg_exit_codes,
)
@@ -166,8 +178,8 @@ def extract_archive(
# if the restore paths don't exist in the archive.
execute_command(
full_command,
working_directory=destination_path,
extra_environment=borg_environment,
working_directory=full_destination_path,
borg_local_path=local_path,
borg_exit_codes=borg_exit_codes,
)
+12 -8
View File
@@ -10,11 +10,13 @@ class Feature(Enum):
NUMERIC_IDS = 4
UPLOAD_RATELIMIT = 5
SEPARATE_REPOSITORY_ARCHIVE = 6
RCREATE = 7
RLIST = 8
RINFO = 9
MATCH_ARCHIVES = 10
EXCLUDED_FILES_MINUS = 11
REPO_CREATE = 7
REPO_LIST = 8
REPO_INFO = 9
REPO_DELETE = 10
MATCH_ARCHIVES = 11
EXCLUDED_FILES_MINUS = 12
ARCHIVE_SERIES = 13
FEATURE_TO_MINIMUM_BORG_VERSION = {
@@ -24,11 +26,13 @@ FEATURE_TO_MINIMUM_BORG_VERSION = {
Feature.NUMERIC_IDS: parse('1.2.0b3'), # borg create/extract/mount --numeric-ids
Feature.UPLOAD_RATELIMIT: parse('1.2.0b3'), # borg create --upload-ratelimit
Feature.SEPARATE_REPOSITORY_ARCHIVE: parse('2.0.0a2'), # --repo with separate archive
Feature.RCREATE: parse('2.0.0a2'), # borg rcreate
Feature.RLIST: parse('2.0.0a2'), # borg rlist
Feature.RINFO: parse('2.0.0a2'), # borg rinfo
Feature.REPO_CREATE: parse('2.0.0a2'), # borg repo-create
Feature.REPO_LIST: parse('2.0.0a2'), # borg repo-list
Feature.REPO_INFO: parse('2.0.0a2'), # borg repo-info
Feature.REPO_DELETE: parse('2.0.0a2'), # borg repo-delete
Feature.MATCH_ARCHIVES: parse('2.0.0b3'), # borg --match-archives
Feature.EXCLUDED_FILES_MINUS: parse('2.0.0b5'), # --list --filter uses "-" for excludes
Feature.ARCHIVE_SERIES: parse('2.0.0b11'), # identically named archives form a series
}
+44 -4
View File
@@ -50,6 +50,9 @@ def make_repository_flags(repository_path, local_borg_version):
) + (repository_path,)
ARCHIVE_HASH_PATTERN = re.compile('[0-9a-fA-F]{8,}$')
def make_repository_archive_flags(repository_path, archive, local_borg_version):
'''
Given the path of a Borg repository, an archive name or pattern, and the local Borg version,
@@ -57,16 +60,42 @@ def make_repository_archive_flags(repository_path, archive, local_borg_version):
and archive.
'''
return (
('--repo', repository_path, archive)
(
'--repo',
repository_path,
(
f'aid:{archive}'
if feature.available(feature.Feature.ARCHIVE_SERIES, local_borg_version)
and ARCHIVE_HASH_PATTERN.match(archive)
and not archive.startswith('aid:')
else archive
),
)
if feature.available(feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, local_borg_version)
else (f'{repository_path}::{archive}',)
)
DEFAULT_ARCHIVE_NAME_FORMAT = '{hostname}-{now:%Y-%m-%dT%H:%M:%S.%f}' # noqa: FS003
DEFAULT_ARCHIVE_NAME_FORMAT_WITHOUT_SERIES = '{hostname}-{now:%Y-%m-%dT%H:%M:%S.%f}' # noqa: FS003
DEFAULT_ARCHIVE_NAME_FORMAT_WITH_SERIES = '{hostname}' # noqa: FS003
def make_match_archives_flags(match_archives, archive_name_format, local_borg_version):
def get_default_archive_name_format(local_borg_version):
'''
Given the local Borg version, return the corresponding default archive name format.
'''
if feature.available(feature.Feature.ARCHIVE_SERIES, local_borg_version):
return DEFAULT_ARCHIVE_NAME_FORMAT_WITH_SERIES
return DEFAULT_ARCHIVE_NAME_FORMAT_WITHOUT_SERIES
def make_match_archives_flags(
match_archives,
archive_name_format,
local_borg_version,
default_archive_name_format=None,
):
'''
Return match archives flags based on the given match archives value, if any. If it isn't set,
return match archives flags to match archives created with the given (or default) archive name
@@ -78,12 +107,23 @@ def make_match_archives_flags(match_archives, archive_name_format, local_borg_ve
return ()
if feature.available(feature.Feature.MATCH_ARCHIVES, local_borg_version):
if (
feature.available(feature.Feature.ARCHIVE_SERIES, local_borg_version)
and ARCHIVE_HASH_PATTERN.match(match_archives)
and not match_archives.startswith('aid:')
):
return ('--match-archives', f'aid:{match_archives}')
return ('--match-archives', match_archives)
else:
return ('--glob-archives', re.sub(r'^sh:', '', match_archives))
derived_match_archives = re.sub(
r'\{(now|utcnow|pid)([:%\w\.-]*)\}', '*', archive_name_format or DEFAULT_ARCHIVE_NAME_FORMAT
r'\{(now|utcnow|pid)([:%\w\.-]*)\}',
'*',
archive_name_format
or default_archive_name_format
or get_default_archive_name_format(local_borg_version),
)
if derived_match_archives == '*':
+5
View File
@@ -1,6 +1,7 @@
import argparse
import logging
import borgmatic.config.paths
import borgmatic.logger
from borgmatic.borg import environment, feature, flags
from borgmatic.execute import execute_command, execute_command_and_capture_output
@@ -35,6 +36,7 @@ def make_info_command(
else ()
)
+ flags.make_flags('remote-path', remote_path)
+ flags.make_flags('umask', config.get('umask'))
+ flags.make_flags('log-json', global_arguments.log_json)
+ flags.make_flags('lock-wait', config.get('lock_wait'))
+ (
@@ -96,10 +98,12 @@ def display_archives_info(
remote_path,
)
borg_exit_codes = config.get('borg_exit_codes')
working_directory = borgmatic.config.paths.get_working_directory(config)
json_info = execute_command_and_capture_output(
json_command,
extra_environment=environment.make_environment(config),
working_directory=working_directory,
borg_local_path=local_path,
borg_exit_codes=borg_exit_codes,
)
@@ -113,6 +117,7 @@ def display_archives_info(
main_command,
output_log_level=logging.ANSWER,
extra_environment=environment.make_environment(config),
working_directory=working_directory,
borg_local_path=local_path,
borg_exit_codes=borg_exit_codes,
)
+31 -27
View File
@@ -3,20 +3,21 @@ import copy
import logging
import re
import borgmatic.config.paths
import borgmatic.logger
from borgmatic.borg import environment, feature, flags, rlist
from borgmatic.borg import environment, feature, flags, repo_list
from borgmatic.execute import execute_command, execute_command_and_capture_output
logger = logging.getLogger(__name__)
ARCHIVE_FILTER_FLAGS_MOVED_TO_RLIST = ('prefix', 'match_archives', 'sort_by', 'first', 'last')
ARCHIVE_FILTER_FLAGS_MOVED_TO_REPO_LIST = ('prefix', 'match_archives', 'sort_by', 'first', 'last')
MAKE_FLAGS_EXCLUDES = (
'repository',
'archive',
'paths',
'find_paths',
) + ARCHIVE_FILTER_FLAGS_MOVED_TO_RLIST
) + ARCHIVE_FILTER_FLAGS_MOVED_TO_REPO_LIST
def make_list_command(
@@ -33,8 +34,6 @@ def make_list_command(
and local and remote Borg paths, return a command as a tuple to list archives or paths within an
archive.
'''
lock_wait = config.get('lock_wait', None)
return (
(local_path, 'list')
+ (
@@ -48,8 +47,9 @@ def make_list_command(
else ()
)
+ flags.make_flags('remote-path', remote_path)
+ flags.make_flags('umask', config.get('umask'))
+ flags.make_flags('log-json', global_arguments.log_json)
+ flags.make_flags('lock-wait', lock_wait)
+ flags.make_flags('lock-wait', config.get('lock_wait'))
+ flags.make_flags_from_arguments(list_arguments, excludes=MAKE_FLAGS_EXCLUDES)
+ (
flags.make_repository_archive_flags(
@@ -100,10 +100,11 @@ def capture_archive_listing(
remote_path=None,
):
'''
Given a local or remote repository path, an archive name, a configuration dict, the local Borg
version, global arguments as an argparse.Namespace, the archive paths in which to list files,
the Borg path format to use for the output, and local and remote Borg paths, capture the output
of listing that archive and return it as a list of file paths.
Given a local or remote repository path, an archive name, a configuration
dict, the local Borg version, global arguments as an argparse.Namespace,
the archive paths (or Borg patterns) in which to list files, the Borg path
format to use for the output, 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(config)
@@ -116,7 +117,7 @@ def capture_archive_listing(
argparse.Namespace(
repository=repository_path,
archive=archive,
paths=[f'sh:{path}' for path in list_paths] if list_paths else None,
paths=[path for path in list_paths] if list_paths else None,
find_paths=None,
json=None,
format=path_format or '{path}{NL}', # noqa: FS003
@@ -126,6 +127,7 @@ def capture_archive_listing(
remote_path,
),
extra_environment=borg_environment,
working_directory=borgmatic.config.paths.get_working_directory(config),
borg_local_path=local_path,
borg_exit_codes=config.get('borg_exit_codes'),
)
@@ -144,22 +146,22 @@ def list_archive(
remote_path=None,
):
'''
Given a local or remote repository path, a configuration dict, the local Borg version, global
arguments as an argparse.Namespace, the arguments to the list action as an argparse.Namespace,
and local and remote Borg paths, display the output of listing the files of a Borg archive (or
return JSON output). If list_arguments.find_paths are given, 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.
Given a local or remote repository path, a configuration dict, the local Borg version, the
arguments to the list action as an argparse.Namespace, global arguments as an
argparse.Namespace, and local and remote Borg paths, display the output of listing the files of
a Borg archive (or return JSON output). If list_arguments.find_paths are given, 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.
'''
borgmatic.logger.add_custom_log_levels()
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.REPO_LIST, local_borg_version):
logger.warning(
'Omitting the --archive flag on the list action is deprecated when using Borg 2.x+. Use the rlist action instead.'
'Omitting the --archive flag on the list action is deprecated when using Borg 2.x+. Use the repo-list action instead.'
)
rlist_arguments = argparse.Namespace(
repo_list_arguments = argparse.Namespace(
repository=repository_path,
short=list_arguments.short,
format=list_arguments.format,
@@ -170,18 +172,18 @@ def list_archive(
first=list_arguments.first,
last=list_arguments.last,
)
return rlist.list_repository(
return repo_list.list_repository(
repository_path,
config,
local_borg_version,
rlist_arguments,
repo_list_arguments,
global_arguments,
local_path,
remote_path,
)
if list_arguments.archive:
for name in ARCHIVE_FILTER_FLAGS_MOVED_TO_RLIST:
for name in ARCHIVE_FILTER_FLAGS_MOVED_TO_REPO_LIST:
if getattr(list_arguments, name, None):
logger.warning(
f"The --{name.replace('_', '-')} flag on the list action is ignored when using the --archive flag."
@@ -198,7 +200,7 @@ def list_archive(
# If there are any paths to find (and there's not a single archive already selected), start by
# getting a list of archives to search.
if list_arguments.find_paths and not list_arguments.archive:
rlist_arguments = argparse.Namespace(
repo_list_arguments = argparse.Namespace(
repository=repository_path,
short=True,
format=None,
@@ -213,16 +215,17 @@ def list_archive(
# Ask Borg to list archives. Capture its output for use below.
archive_lines = tuple(
execute_command_and_capture_output(
rlist.make_rlist_command(
repo_list.make_repo_list_command(
repository_path,
config,
local_borg_version,
rlist_arguments,
repo_list_arguments,
global_arguments,
local_path,
remote_path,
),
extra_environment=borg_environment,
working_directory=borgmatic.config.paths.get_working_directory(config),
borg_local_path=local_path,
borg_exit_codes=borg_exit_codes,
)
@@ -241,7 +244,7 @@ def list_archive(
# This list call is to show the files in a single archive, not list multiple archives. So
# blank out any archive filtering flags. They'll break anyway in Borg 2.
for name in ARCHIVE_FILTER_FLAGS_MOVED_TO_RLIST:
for name in ARCHIVE_FILTER_FLAGS_MOVED_TO_REPO_LIST:
setattr(archive_arguments, name, None)
main_command = make_list_command(
@@ -258,6 +261,7 @@ def list_archive(
main_command,
output_log_level=logging.ANSWER,
extra_environment=borg_environment,
working_directory=borgmatic.config.paths.get_working_directory(config),
borg_local_path=local_path,
borg_exit_codes=borg_exit_codes,
)
+4
View File
@@ -1,5 +1,6 @@
import logging
import borgmatic.config.paths
from borgmatic.borg import environment, feature, flags
from borgmatic.execute import DO_NOT_CAPTURE, execute_command
@@ -59,6 +60,7 @@ def mount_archive(
)
borg_environment = environment.make_environment(config)
working_directory = borgmatic.config.paths.get_working_directory(config)
# Don't capture the output when foreground mode is used so that ctrl-C can work properly.
if mount_arguments.foreground:
@@ -66,6 +68,7 @@ def mount_archive(
full_command,
output_file=DO_NOT_CAPTURE,
extra_environment=borg_environment,
working_directory=working_directory,
borg_local_path=local_path,
borg_exit_codes=config.get('borg_exit_codes'),
)
@@ -74,6 +77,7 @@ def mount_archive(
execute_command(
full_command,
extra_environment=borg_environment,
working_directory=working_directory,
borg_local_path=local_path,
borg_exit_codes=config.get('borg_exit_codes'),
)
+8 -5
View File
@@ -1,5 +1,6 @@
import logging
import borgmatic.config.paths
import borgmatic.logger
from borgmatic.borg import environment, feature, flags
from borgmatic.execute import execute_command
@@ -7,9 +8,10 @@ from borgmatic.execute import execute_command
logger = logging.getLogger(__name__)
def make_prune_flags(config, local_borg_version):
def make_prune_flags(config, prune_arguments, local_borg_version):
'''
Given a configuration dict mapping from option name to value, transform it into an sequence of
Given a configuration dict mapping from option name to value, prune arguments as an
argparse.Namespace instance, and the local Borg version, produce a corresponding sequence of
command-line flags.
For example, given a retention config of:
@@ -39,7 +41,7 @@ def make_prune_flags(config, local_borg_version):
if prefix
else (
flags.make_match_archives_flags(
config.get('match_archives'),
prune_arguments.match_archives or config.get('match_archives'),
config.get('archive_name_format'),
local_borg_version,
)
@@ -68,7 +70,7 @@ def prune_archives(
full_command = (
(local_path, 'prune')
+ make_prune_flags(config, local_borg_version)
+ make_prune_flags(config, prune_arguments, local_borg_version)
+ (('--remote-path', remote_path) if remote_path else ())
+ (('--umask', str(umask)) if umask else ())
+ (('--log-json',) if global_arguments.log_json else ())
@@ -77,7 +79,7 @@ def prune_archives(
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
+ flags.make_flags_from_arguments(
prune_arguments,
excludes=('repository', 'stats', 'list_archives'),
excludes=('repository', 'match_archives', 'stats', 'list_archives'),
)
+ (('--list',) if prune_arguments.list_archives else ())
+ (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
@@ -95,6 +97,7 @@ def prune_archives(
full_command,
output_log_level=output_log_level,
extra_environment=environment.make_environment(config),
working_directory=borgmatic.config.paths.get_working_directory(config),
borg_local_path=local_path,
borg_exit_codes=config.get('borg_exit_codes'),
)
@@ -3,13 +3,14 @@ import json
import logging
import subprocess
from borgmatic.borg import environment, feature, flags, rinfo
import borgmatic.config.paths
from borgmatic.borg import environment, feature, flags, repo_info
from borgmatic.execute import DO_NOT_CAPTURE, execute_command
logger = logging.getLogger(__name__)
RINFO_REPOSITORY_NOT_FOUND_EXIT_CODES = {2, 13}
REPO_INFO_REPOSITORY_NOT_FOUND_EXIT_CODES = {2, 13, 15}
def create_repository(
@@ -39,7 +40,7 @@ def create_repository(
'''
try:
info_data = json.loads(
rinfo.display_repository_info(
repo_info.display_repository_info(
repository_path,
config,
local_borg_version,
@@ -59,17 +60,18 @@ def create_repository(
logger.info(f'{repository_path}: Repository already exists. Skipping creation.')
return
except subprocess.CalledProcessError as error:
if error.returncode not in RINFO_REPOSITORY_NOT_FOUND_EXIT_CODES:
if error.returncode not in REPO_INFO_REPOSITORY_NOT_FOUND_EXIT_CODES:
raise
lock_wait = config.get('lock_wait')
extra_borg_options = config.get('extra_borg_options', {}).get('rcreate', '')
umask = config.get('umask')
extra_borg_options = config.get('extra_borg_options', {}).get('repo-create', '')
rcreate_command = (
repo_create_command = (
(local_path,)
+ (
('rcreate',)
if feature.available(feature.Feature.RCREATE, local_borg_version)
('repo-create',)
if feature.available(feature.Feature.REPO_CREATE, local_borg_version)
else ('init',)
)
+ (('--encryption', encryption_mode) if encryption_mode else ())
@@ -83,6 +85,7 @@ def create_repository(
+ (('--log-json',) if global_arguments.log_json else ())
+ (('--lock-wait', str(lock_wait)) if lock_wait else ())
+ (('--remote-path', remote_path) if remote_path else ())
+ (('--umask', str(umask)) if umask else ())
+ (tuple(extra_borg_options.split(' ')) if extra_borg_options else ())
+ flags.make_repository_flags(repository_path, local_borg_version)
)
@@ -93,9 +96,10 @@ def create_repository(
# Do not capture output here, so as to support interactive prompts.
execute_command(
rcreate_command,
repo_create_command,
output_file=DO_NOT_CAPTURE,
extra_environment=environment.make_environment(config),
working_directory=borgmatic.config.paths.get_working_directory(config),
borg_local_path=local_path,
borg_exit_codes=config.get('borg_exit_codes'),
)
+95
View File
@@ -0,0 +1,95 @@
import logging
import borgmatic.borg.environment
import borgmatic.borg.feature
import borgmatic.borg.flags
import borgmatic.config.paths
import borgmatic.execute
logger = logging.getLogger(__name__)
def make_repo_delete_command(
repository,
config,
local_borg_version,
repo_delete_arguments,
global_arguments,
local_path,
remote_path,
):
'''
Given a local or remote repository dict, a configuration dict, the local Borg version, the
arguments to the repo_delete action as an argparse.Namespace, and global arguments, return a command
as a tuple to repo_delete the entire repository.
'''
return (
(local_path,)
+ (
('repo-delete',)
if borgmatic.borg.feature.available(
borgmatic.borg.feature.Feature.REPO_DELETE, local_borg_version
)
else ('delete',)
)
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
+ (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
+ borgmatic.borg.flags.make_flags('dry-run', global_arguments.dry_run)
+ borgmatic.borg.flags.make_flags('remote-path', remote_path)
+ borgmatic.borg.flags.make_flags('umask', config.get('umask'))
+ borgmatic.borg.flags.make_flags('log-json', global_arguments.log_json)
+ borgmatic.borg.flags.make_flags('lock-wait', config.get('lock_wait'))
+ borgmatic.borg.flags.make_flags('list', repo_delete_arguments.list_archives)
+ (
(('--force',) + (('--force',) if repo_delete_arguments.force >= 2 else ()))
if repo_delete_arguments.force
else ()
)
+ borgmatic.borg.flags.make_flags_from_arguments(
repo_delete_arguments, excludes=('list_archives', 'force', 'repository')
)
+ borgmatic.borg.flags.make_repository_flags(repository['path'], local_borg_version)
)
def delete_repository(
repository,
config,
local_borg_version,
repo_delete_arguments,
global_arguments,
local_path='borg',
remote_path=None,
):
'''
Given a local or remote repository dict, a configuration dict, the local Borg version, the
arguments to the repo_delete action as an argparse.Namespace, global arguments as an
argparse.Namespace, and local and remote Borg paths, repo_delete the entire repository.
'''
borgmatic.logger.add_custom_log_levels()
command = make_repo_delete_command(
repository,
config,
local_borg_version,
repo_delete_arguments,
global_arguments,
local_path,
remote_path,
)
borgmatic.execute.execute_command(
command,
output_log_level=logging.ANSWER,
# Don't capture output when Borg is expected to prompt for interactive confirmation, or the
# prompt won't work.
output_file=(
None
if repo_delete_arguments.force or repo_delete_arguments.cache_only
else borgmatic.execute.DO_NOT_CAPTURE
),
extra_environment=borgmatic.borg.environment.make_environment(config),
working_directory=borgmatic.config.paths.get_working_directory(config),
borg_local_path=local_path,
borg_exit_codes=config.get('borg_exit_codes'),
)
@@ -1,5 +1,6 @@
import logging
import borgmatic.config.paths
import borgmatic.logger
from borgmatic.borg import environment, feature, flags
from borgmatic.execute import execute_command, execute_command_and_capture_output
@@ -11,14 +12,14 @@ def display_repository_info(
repository_path,
config,
local_borg_version,
rinfo_arguments,
repo_info_arguments,
global_arguments,
local_path='borg',
remote_path=None,
):
'''
Given a local or remote repository path, a configuration dict, the local Borg version, the
arguments to the rinfo action, and global arguments as an argparse.Namespace, display summary
arguments to the repo_info action, and global arguments as an argparse.Namespace, display summary
information for the Borg repository or return JSON summary information.
'''
borgmatic.logger.add_custom_log_levels()
@@ -27,34 +28,37 @@ def display_repository_info(
full_command = (
(local_path,)
+ (
('rinfo',)
if feature.available(feature.Feature.RINFO, local_borg_version)
('repo-info',)
if feature.available(feature.Feature.REPO_INFO, local_borg_version)
else ('info',)
)
+ (
('--info',)
if logger.getEffectiveLevel() == logging.INFO and not rinfo_arguments.json
if logger.getEffectiveLevel() == logging.INFO and not repo_info_arguments.json
else ()
)
+ (
('--debug', '--show-rc')
if logger.isEnabledFor(logging.DEBUG) and not rinfo_arguments.json
if logger.isEnabledFor(logging.DEBUG) and not repo_info_arguments.json
else ()
)
+ flags.make_flags('remote-path', remote_path)
+ flags.make_flags('umask', config.get('umask'))
+ flags.make_flags('log-json', global_arguments.log_json)
+ flags.make_flags('lock-wait', lock_wait)
+ (('--json',) if rinfo_arguments.json else ())
+ (('--json',) if repo_info_arguments.json else ())
+ flags.make_repository_flags(repository_path, local_borg_version)
)
extra_environment = environment.make_environment(config)
working_directory = borgmatic.config.paths.get_working_directory(config)
borg_exit_codes = config.get('borg_exit_codes')
if rinfo_arguments.json:
if repo_info_arguments.json:
return execute_command_and_capture_output(
full_command,
extra_environment=extra_environment,
working_directory=working_directory,
borg_local_path=local_path,
borg_exit_codes=borg_exit_codes,
)
@@ -63,6 +67,7 @@ def display_repository_info(
full_command,
output_log_level=logging.ANSWER,
extra_environment=extra_environment,
working_directory=working_directory,
borg_local_path=local_path,
borg_exit_codes=borg_exit_codes,
)
@@ -1,6 +1,7 @@
import argparse
import logging
import borgmatic.config.paths
import borgmatic.logger
from borgmatic.borg import environment, feature, flags
from borgmatic.execute import execute_command, execute_command_and_capture_output
@@ -31,9 +32,14 @@ def resolve_archive_name(
full_command = (
(
local_path,
'rlist' if feature.available(feature.Feature.RLIST, local_borg_version) else 'list',
(
'repo-list'
if feature.available(feature.Feature.REPO_LIST, local_borg_version)
else 'list'
),
)
+ flags.make_flags('remote-path', remote_path)
+ flags.make_flags('umask', config.get('umask'))
+ flags.make_flags('log-json', global_arguments.log_json)
+ flags.make_flags('lock-wait', config.get('lock_wait'))
+ flags.make_flags('last', 1)
@@ -44,6 +50,7 @@ def resolve_archive_name(
output = execute_command_and_capture_output(
full_command,
extra_environment=environment.make_environment(config),
working_directory=borgmatic.config.paths.get_working_directory(config),
borg_local_path=local_path,
borg_exit_codes=config.get('borg_exit_codes'),
)
@@ -60,54 +67,59 @@ def resolve_archive_name(
MAKE_FLAGS_EXCLUDES = ('repository', 'prefix', 'match_archives')
def make_rlist_command(
def make_repo_list_command(
repository_path,
config,
local_borg_version,
rlist_arguments,
repo_list_arguments,
global_arguments,
local_path='borg',
remote_path=None,
):
'''
Given a local or remote repository path, a configuration dict, the local Borg version, the
arguments to the rlist action, global arguments as an argparse.Namespace instance, and local and
arguments to the repo_list action, global arguments as an argparse.Namespace instance, and local and
remote Borg paths, return a command as a tuple to list archives with a repository.
'''
return (
(
local_path,
'rlist' if feature.available(feature.Feature.RLIST, local_borg_version) else 'list',
(
'repo-list'
if feature.available(feature.Feature.REPO_LIST, local_borg_version)
else 'list'
),
)
+ (
('--info',)
if logger.getEffectiveLevel() == logging.INFO and not rlist_arguments.json
if logger.getEffectiveLevel() == logging.INFO and not repo_list_arguments.json
else ()
)
+ (
('--debug', '--show-rc')
if logger.isEnabledFor(logging.DEBUG) and not rlist_arguments.json
if logger.isEnabledFor(logging.DEBUG) and not repo_list_arguments.json
else ()
)
+ flags.make_flags('remote-path', remote_path)
+ flags.make_flags('umask', config.get('umask'))
+ flags.make_flags('log-json', global_arguments.log_json)
+ flags.make_flags('lock-wait', config.get('lock_wait'))
+ (
(
flags.make_flags('match-archives', f'sh:{rlist_arguments.prefix}*')
flags.make_flags('match-archives', f'sh:{repo_list_arguments.prefix}*')
if feature.available(feature.Feature.MATCH_ARCHIVES, local_borg_version)
else flags.make_flags('glob-archives', f'{rlist_arguments.prefix}*')
else flags.make_flags('glob-archives', f'{repo_list_arguments.prefix}*')
)
if rlist_arguments.prefix
if repo_list_arguments.prefix
else (
flags.make_match_archives_flags(
rlist_arguments.match_archives or config.get('match_archives'),
repo_list_arguments.match_archives or config.get('match_archives'),
config.get('archive_name_format'),
local_borg_version,
)
)
)
+ flags.make_flags_from_arguments(rlist_arguments, excludes=MAKE_FLAGS_EXCLUDES)
+ flags.make_flags_from_arguments(repo_list_arguments, excludes=MAKE_FLAGS_EXCLUDES)
+ flags.make_repository_flags(repository_path, local_borg_version)
)
@@ -116,7 +128,7 @@ def list_repository(
repository_path,
config,
local_borg_version,
rlist_arguments,
repo_list_arguments,
global_arguments,
local_path='borg',
remote_path=None,
@@ -130,34 +142,36 @@ def list_repository(
borgmatic.logger.add_custom_log_levels()
borg_environment = environment.make_environment(config)
main_command = make_rlist_command(
main_command = make_repo_list_command(
repository_path,
config,
local_borg_version,
rlist_arguments,
repo_list_arguments,
global_arguments,
local_path,
remote_path,
)
json_command = make_rlist_command(
json_command = make_repo_list_command(
repository_path,
config,
local_borg_version,
argparse.Namespace(**dict(rlist_arguments.__dict__, json=True)),
argparse.Namespace(**dict(repo_list_arguments.__dict__, json=True)),
global_arguments,
local_path,
remote_path,
)
working_directory = borgmatic.config.paths.get_working_directory(config)
borg_exit_codes = config.get('borg_exit_codes')
json_listing = execute_command_and_capture_output(
json_command,
extra_environment=borg_environment,
working_directory=working_directory,
borg_local_path=local_path,
borg_exit_codes=borg_exit_codes,
)
if rlist_arguments.json:
if repo_list_arguments.json:
return json_listing
flags.warn_for_aggressive_archive_flags(json_command, json_listing)
@@ -166,6 +180,7 @@ def list_repository(
main_command,
output_log_level=logging.ANSWER,
extra_environment=borg_environment,
working_directory=working_directory,
borg_local_path=local_path,
borg_exit_codes=borg_exit_codes,
)
+4 -1
View File
@@ -1,5 +1,6 @@
import logging
import borgmatic.config.paths
import borgmatic.logger
from borgmatic.borg import environment, flags
from borgmatic.execute import DO_NOT_CAPTURE, execute_command
@@ -29,6 +30,7 @@ def transfer_archives(
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
+ (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
+ flags.make_flags('remote-path', remote_path)
+ flags.make_flags('umask', config.get('umask'))
+ flags.make_flags('log-json', global_arguments.log_json)
+ flags.make_flags('lock-wait', config.get('lock_wait', None))
+ (
@@ -55,7 +57,8 @@ def transfer_archives(
full_command,
output_log_level=logging.ANSWER,
output_file=DO_NOT_CAPTURE if transfer_arguments.progress else None,
extra_environment=environment.make_environment(config),
working_directory=borgmatic.config.paths.get_working_directory(config),
borg_local_path=local_path,
borg_exit_codes=config.get('borg_exit_codes'),
extra_environment=environment.make_environment(config),
)
+5 -1
View File
@@ -1,5 +1,6 @@
import logging
import borgmatic.config.paths
from borgmatic.execute import execute_command
logger = logging.getLogger(__name__)
@@ -18,5 +19,8 @@ def unmount_archive(config, mount_point, local_path='borg'):
)
execute_command(
full_command, borg_local_path=local_path, borg_exit_codes=config.get('borg_exit_codes')
full_command,
working_directory=borgmatic.config.paths.get_working_directory(config),
borg_local_path=local_path,
borg_exit_codes=config.get('borg_exit_codes'),
)
+3 -1
View File
@@ -1,5 +1,6 @@
import logging
import borgmatic.config.paths
from borgmatic.borg import environment
from borgmatic.execute import execute_command_and_capture_output
@@ -8,7 +9,7 @@ logger = logging.getLogger(__name__)
def local_borg_version(config, local_path='borg'):
'''
Given a configuration dict and a local Borg binary path, return a version string for it.
Given a configuration dict and a local Borg executable path, return a version string for it.
Raise OSError or CalledProcessError if there is a problem running Borg.
Raise ValueError if the version cannot be parsed.
@@ -21,6 +22,7 @@ def local_borg_version(config, local_path='borg'):
output = execute_command_and_capture_output(
full_command,
extra_environment=environment.make_environment(config),
working_directory=borgmatic.config.paths.get_working_directory(config),
borg_local_path=local_path,
borg_exit_codes=config.get('borg_exit_codes'),
)
+264 -80
View File
@@ -6,20 +6,22 @@ from argparse import ArgumentParser
from borgmatic.config import collect
ACTION_ALIASES = {
'rcreate': ['init', '-I'],
'repo-create': ['rcreate', 'init', '-I'],
'prune': ['-p'],
'compact': [],
'create': ['-C'],
'check': ['-k'],
'config': [],
'delete': [],
'extract': ['-x'],
'export-tar': [],
'mount': ['-m'],
'umount': ['-u'],
'restore': ['-r'],
'rlist': [],
'repo-delete': ['rdelete'],
'repo-list': ['rlist'],
'list': ['-l'],
'rinfo': [],
'repo-info': ['rinfo'],
'info': ['-i'],
'transfer': [],
'break-lock': [],
@@ -72,11 +74,11 @@ def omit_values_colliding_with_action_names(unparsed_arguments, parsed_arguments
for action_name, parsed in parsed_arguments.items():
for value in vars(parsed).values():
if isinstance(value, str):
if value in ACTION_ALIASES.keys():
if value in ACTION_ALIASES.keys() and value in remaining_arguments:
remaining_arguments.remove(value)
elif isinstance(value, list):
for item in value:
if item in ACTION_ALIASES.keys():
if item in ACTION_ALIASES.keys() and item in remaining_arguments:
remaining_arguments.remove(item)
return tuple(remaining_arguments)
@@ -298,7 +300,7 @@ def make_parsers():
'--config',
dest='config_paths',
action='append',
help=f"Configuration filename or directory, can specify flag multiple times, defaults to: {' '.join(unexpanded_config_paths)}",
help=f"Configuration filename or directory, can specify flag multiple times, defaults to: -c {' -c '.join(unexpanded_config_paths)}",
)
global_group.add_argument(
'-n',
@@ -400,51 +402,51 @@ def make_parsers():
metavar='',
help='Specify zero or more actions. Defaults to create, prune, compact, and check. Use --help with action for details:',
)
rcreate_parser = action_parsers.add_parser(
'rcreate',
aliases=ACTION_ALIASES['rcreate'],
repo_create_parser = action_parsers.add_parser(
'repo-create',
aliases=ACTION_ALIASES['repo-create'],
help='Create a new, empty Borg repository',
description='Create a new, empty Borg repository',
add_help=False,
)
rcreate_group = rcreate_parser.add_argument_group('rcreate arguments')
rcreate_group.add_argument(
repo_create_group = repo_create_parser.add_argument_group('repo-create arguments')
repo_create_group.add_argument(
'-e',
'--encryption',
dest='encryption_mode',
help='Borg repository encryption mode',
required=True,
)
rcreate_group.add_argument(
repo_create_group.add_argument(
'--source-repository',
'--other-repo',
metavar='KEY_REPOSITORY',
help='Path to an existing Borg repository whose key material should be reused [Borg 2.x+ only]',
)
rcreate_group.add_argument(
repo_create_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',
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, quoted globs supported',
)
rcreate_group.add_argument(
repo_create_group.add_argument(
'--copy-crypt-key',
action='store_true',
help='Copy the crypt key used for authenticated encryption from the source repository, defaults to a new random key [Borg 2.x+ only]',
)
rcreate_group.add_argument(
repo_create_group.add_argument(
'--append-only',
action='store_true',
help='Create an append-only repository',
)
rcreate_group.add_argument(
repo_create_group.add_argument(
'--storage-quota',
help='Create a repository with a fixed storage quota',
)
rcreate_group.add_argument(
repo_create_group.add_argument(
'--make-parent-dirs',
action='store_true',
help='Create any missing parent directories of the repository directory',
)
rcreate_group.add_argument(
repo_create_group.add_argument(
'-h', '--help', action='help', help='Show this help message and exit'
)
@@ -458,7 +460,7 @@ def make_parsers():
transfer_group = transfer_parser.add_argument_group('transfer arguments')
transfer_group.add_argument(
'--repository',
help='Path of existing destination repository to transfer archives to, defaults to the configured repository if there is only one',
help='Path of existing destination repository to transfer archives to, defaults to the configured repository if there is only one, quoted globs supported',
)
transfer_group.add_argument(
'--source-repository',
@@ -467,7 +469,7 @@ def make_parsers():
)
transfer_group.add_argument(
'--archive',
help='Name of single archive to transfer (or "latest"), defaults to transferring all archives',
help='Name or hash of a single archive to transfer (or "latest"), defaults to transferring all archives',
)
transfer_group.add_argument(
'--upgrader',
@@ -484,7 +486,7 @@ def make_parsers():
'--match-archives',
'--glob-archives',
metavar='PATTERN',
help='Only transfer archives with names matching this pattern',
help='Only transfer archives with names, hashes, or series matching this pattern',
)
transfer_group.add_argument(
'--sort-by', metavar='KEYS', help='Comma-separated list of sorting keys'
@@ -531,14 +533,21 @@ def make_parsers():
prune_group = prune_parser.add_argument_group('prune arguments')
prune_group.add_argument(
'--repository',
help='Path of specific existing repository to prune (must be already specified in a borgmatic configuration file)',
help='Path of specific existing repository to prune (must be already specified in a borgmatic configuration file), quoted globs supported',
)
prune_group.add_argument(
'-a',
'--match-archives',
'--glob-archives',
metavar='PATTERN',
help='When pruning, only consider archives with names, hashes, or series matching this pattern',
)
prune_group.add_argument(
'--stats',
dest='stats',
default=False,
action='store_true',
help='Display statistics of archive',
help='Display statistics of the pruned archive',
)
prune_group.add_argument(
'--list', dest='list_archives', action='store_true', help='List archives kept/pruned'
@@ -575,7 +584,7 @@ def make_parsers():
compact_group = compact_parser.add_argument_group('compact arguments')
compact_group.add_argument(
'--repository',
help='Path of specific existing repository to compact (must be already specified in a borgmatic configuration file)',
help='Path of specific existing repository to compact (must be already specified in a borgmatic configuration file), quoted globs supported',
)
compact_group.add_argument(
'--progress',
@@ -611,7 +620,7 @@ def make_parsers():
create_group = create_parser.add_argument_group('create arguments')
create_group.add_argument(
'--repository',
help='Path of specific existing repository to backup to (must be already specified in a borgmatic configuration file)',
help='Path of specific existing repository to backup to (must be already specified in a borgmatic configuration file), quoted globs supported',
)
create_group.add_argument(
'--progress',
@@ -645,7 +654,7 @@ def make_parsers():
check_group = check_parser.add_argument_group('check arguments')
check_group.add_argument(
'--repository',
help='Path of specific existing repository to check (must be already specified in a borgmatic configuration file)',
help='Path of specific existing repository to check (must be already specified in a borgmatic configuration file), quoted globs supported',
)
check_group.add_argument(
'--progress',
@@ -671,7 +680,7 @@ def make_parsers():
'--match-archives',
'--glob-archives',
metavar='PATTERN',
help='Only check archives with names matching this pattern',
help='Only check archives with names, hashes, or series matching this pattern',
)
check_group.add_argument(
'--only',
@@ -689,6 +698,97 @@ def make_parsers():
)
check_group.add_argument('-h', '--help', action='help', help='Show this help message and exit')
delete_parser = action_parsers.add_parser(
'delete',
aliases=ACTION_ALIASES['delete'],
help='Delete an archive from a repository or delete an entire repository (with Borg 1.2+, you must run compact afterwards to actually free space)',
description='Delete an archive from a repository or delete an entire repository (with Borg 1.2+, you must run compact afterwards to actually free space)',
add_help=False,
)
delete_group = delete_parser.add_argument_group('delete arguments')
delete_group.add_argument(
'--repository',
help='Path of repository to delete or delete archives from, defaults to the configured repository if there is only one, quoted globs supported',
)
delete_group.add_argument(
'--archive',
help='Archive name, hash, or series to delete',
)
delete_group.add_argument(
'--list',
dest='list_archives',
action='store_true',
help='Show details for the deleted archives',
)
delete_group.add_argument(
'--stats',
action='store_true',
help='Display statistics for the deleted archives',
)
delete_group.add_argument(
'--cache-only',
action='store_true',
help='Delete only the local cache for the given repository',
)
delete_group.add_argument(
'--force',
action='count',
help='Force deletion of corrupted archives, can be given twice if once does not work',
)
delete_group.add_argument(
'--keep-security-info',
action='store_true',
help='Do not delete the local security info when deleting a repository',
)
delete_group.add_argument(
'--save-space',
action='store_true',
help='Work slower, but using less space [Not supported in Borg 2.x+]',
)
delete_group.add_argument(
'--checkpoint-interval',
type=int,
metavar='SECONDS',
help='Write a checkpoint at the given interval, defaults to 1800 seconds (30 minutes)',
)
delete_group.add_argument(
'-a',
'--match-archives',
'--glob-archives',
metavar='PATTERN',
help='Only delete archives with names, hashes, or series matching this pattern',
)
delete_group.add_argument(
'--sort-by', metavar='KEYS', help='Comma-separated list of sorting keys'
)
delete_group.add_argument(
'--first', metavar='N', help='Delete first N archives after other filters are applied'
)
delete_group.add_argument(
'--last', metavar='N', help='Delete last N archives after other filters are applied'
)
delete_group.add_argument(
'--oldest',
metavar='TIMESPAN',
help='Delete archives within a specified time range starting from the timestamp of the oldest archive (e.g. 7d or 12m) [Borg 2.x+ only]',
)
delete_group.add_argument(
'--newest',
metavar='TIMESPAN',
help='Delete archives within a time range that ends at timestamp of the newest archive and starts a specified time range ago (e.g. 7d or 12m) [Borg 2.x+ only]',
)
delete_group.add_argument(
'--older',
metavar='TIMESPAN',
help='Delete archives that are older than the specified time range (e.g. 7d or 12m) from the current time [Borg 2.x+ only]',
)
delete_group.add_argument(
'--newer',
metavar='TIMESPAN',
help='Delete archives that are newer than the specified time range (e.g. 7d or 12m) from the current time [Borg 2.x+ only]',
)
delete_group.add_argument('-h', '--help', action='help', help='Show this help message and exit')
extract_parser = action_parsers.add_parser(
'extract',
aliases=ACTION_ALIASES['extract'],
@@ -699,10 +799,10 @@ def make_parsers():
extract_group = extract_parser.add_argument_group('extract arguments')
extract_group.add_argument(
'--repository',
help='Path of repository to extract, defaults to the configured repository if there is only one',
help='Path of repository to extract, defaults to the configured repository if there is only one, quoted globs supported',
)
extract_group.add_argument(
'--archive', help='Name of archive to extract (or "latest")', required=True
'--archive', help='Name or hash of a single archive to extract (or "latest")', required=True
)
extract_group.add_argument(
'--path',
@@ -761,16 +861,30 @@ def make_parsers():
)
config_bootstrap_group.add_argument(
'--repository',
help='Path of repository to extract config files from',
help='Path of repository to extract config files from, quoted globs supported',
required=True,
)
config_bootstrap_group.add_argument(
'--local-path',
help='Alternate Borg local executable. Defaults to "borg"',
default='borg',
)
config_bootstrap_group.add_argument(
'--remote-path',
help='Alternate Borg remote executable. Defaults to "borg"',
default='borg',
)
config_bootstrap_group.add_argument(
'--user-runtime-directory',
help='Path used for temporary runtime data like bootstrap metadata. Defaults to $XDG_RUNTIME_DIR or $TMPDIR or $TEMP or /var/run/$UID',
)
config_bootstrap_group.add_argument(
'--borgmatic-source-directory',
help='Path that stores the config files used to create an archive and additional source files used for temporary internal state like borgmatic database dumps. Defaults to ~/.borgmatic',
help='Deprecated. Path formerly used for temporary runtime data like bootstrap metadata. Defaults to ~/.borgmatic',
)
config_bootstrap_group.add_argument(
'--archive',
help='Name of archive to extract config files from, defaults to "latest"',
help='Name or hash of a single archive to extract config files from, defaults to "latest"',
default='latest',
)
config_bootstrap_group.add_argument(
@@ -859,10 +973,10 @@ def make_parsers():
export_tar_group = export_tar_parser.add_argument_group('export-tar arguments')
export_tar_group.add_argument(
'--repository',
help='Path of repository to export from, defaults to the configured repository if there is only one',
help='Path of repository to export from, defaults to the configured repository if there is only one, quoted globs supported',
)
export_tar_group.add_argument(
'--archive', help='Name of archive to export (or "latest")', required=True
'--archive', help='Name or hash of a single archive to export (or "latest")', required=True
)
export_tar_group.add_argument(
'--path',
@@ -905,9 +1019,11 @@ def make_parsers():
mount_group = mount_parser.add_argument_group('mount arguments')
mount_group.add_argument(
'--repository',
help='Path of repository to use, defaults to the configured repository if there is only one',
help='Path of repository to use, defaults to the configured repository if there is only one, quoted globs supported',
)
mount_group.add_argument(
'--archive', help='Name or hash of a single archive to mount (or "latest")'
)
mount_group.add_argument('--archive', help='Name of archive to mount (or "latest")')
mount_group.add_argument(
'--mount-point',
metavar='PATH',
@@ -977,6 +1093,43 @@ def make_parsers():
)
umount_group.add_argument('-h', '--help', action='help', help='Show this help message and exit')
repo_delete_parser = action_parsers.add_parser(
'repo-delete',
aliases=ACTION_ALIASES['repo-delete'],
help='Delete an entire repository (with Borg 1.2+, you must run compact afterwards to actually free space)',
description='Delete an entire repository (with Borg 1.2+, you must run compact afterwards to actually free space)',
add_help=False,
)
repo_delete_group = repo_delete_parser.add_argument_group('delete arguments')
repo_delete_group.add_argument(
'--repository',
help='Path of repository to delete, defaults to the configured repository if there is only one, quoted globs supported',
)
repo_delete_group.add_argument(
'--list',
dest='list_archives',
action='store_true',
help='Show details for the archives in the given repository',
)
repo_delete_group.add_argument(
'--force',
action='count',
help='Force deletion of corrupted archives, can be given twice if once does not work',
)
repo_delete_group.add_argument(
'--cache-only',
action='store_true',
help='Delete only the local cache for the given repository',
)
repo_delete_group.add_argument(
'--keep-security-info',
action='store_true',
help='Do not delete the local security info when deleting a repository',
)
repo_delete_group.add_argument(
'-h', '--help', action='help', help='Show this help message and exit'
)
restore_parser = action_parsers.add_parser(
'restore',
aliases=ACTION_ALIASES['restore'],
@@ -987,10 +1140,12 @@ def make_parsers():
restore_group = restore_parser.add_argument_group('restore arguments')
restore_group.add_argument(
'--repository',
help='Path of repository to restore from, defaults to the configured repository if there is only one',
help='Path of repository to restore from, defaults to the configured repository if there is only one, quoted globs supported',
)
restore_group.add_argument(
'--archive', help='Name of archive to restore from (or "latest")', required=True
'--archive',
help='Name or hash of a single archive to restore from (or "latest")',
required=True,
)
restore_group.add_argument(
'--data-source',
@@ -1031,65 +1186,67 @@ def make_parsers():
'-h', '--help', action='help', help='Show this help message and exit'
)
rlist_parser = action_parsers.add_parser(
'rlist',
aliases=ACTION_ALIASES['rlist'],
repo_list_parser = action_parsers.add_parser(
'repo-list',
aliases=ACTION_ALIASES['repo-list'],
help='List repository',
description='List the archives in a repository',
add_help=False,
)
rlist_group = rlist_parser.add_argument_group('rlist arguments')
rlist_group.add_argument(
repo_list_group = repo_list_parser.add_argument_group('repo-list arguments')
repo_list_group.add_argument(
'--repository',
help='Path of repository to list, defaults to the configured repositories',
help='Path of repository to list, defaults to the configured repositories, quoted globs supported',
)
rlist_group.add_argument(
repo_list_group.add_argument(
'--short', default=False, action='store_true', help='Output only archive names'
)
rlist_group.add_argument('--format', help='Format for archive listing')
rlist_group.add_argument(
repo_list_group.add_argument('--format', help='Format for archive listing')
repo_list_group.add_argument(
'--json', default=False, action='store_true', help='Output results as JSON'
)
rlist_group.add_argument(
repo_list_group.add_argument(
'-P', '--prefix', help='Deprecated. Only list archive names starting with this prefix'
)
rlist_group.add_argument(
repo_list_group.add_argument(
'-a',
'--match-archives',
'--glob-archives',
metavar='PATTERN',
help='Only list archive names matching this pattern',
help='Only list archive names, hashes, or series matching this pattern',
)
rlist_group.add_argument(
repo_list_group.add_argument(
'--sort-by', metavar='KEYS', help='Comma-separated list of sorting keys'
)
rlist_group.add_argument(
repo_list_group.add_argument(
'--first', metavar='N', help='List first N archives after other filters are applied'
)
rlist_group.add_argument(
repo_list_group.add_argument(
'--last', metavar='N', help='List last N archives after other filters are applied'
)
rlist_group.add_argument(
repo_list_group.add_argument(
'--oldest',
metavar='TIMESPAN',
help='List archives within a specified time range starting from the timestamp of the oldest archive (e.g. 7d or 12m) [Borg 2.x+ only]',
)
rlist_group.add_argument(
repo_list_group.add_argument(
'--newest',
metavar='TIMESPAN',
help='List archives within a time range that ends at timestamp of the newest archive and starts a specified time range ago (e.g. 7d or 12m) [Borg 2.x+ only]',
)
rlist_group.add_argument(
repo_list_group.add_argument(
'--older',
metavar='TIMESPAN',
help='List archives that are older than the specified time range (e.g. 7d or 12m) from the current time [Borg 2.x+ only]',
)
rlist_group.add_argument(
repo_list_group.add_argument(
'--newer',
metavar='TIMESPAN',
help='List archives that are newer than the specified time range (e.g. 7d or 12m) from the current time [Borg 2.x+ only]',
)
rlist_group.add_argument('-h', '--help', action='help', help='Show this help message and exit')
repo_list_group.add_argument(
'-h', '--help', action='help', help='Show this help message and exit'
)
list_parser = action_parsers.add_parser(
'list',
@@ -1101,9 +1258,11 @@ def make_parsers():
list_group = list_parser.add_argument_group('list arguments')
list_group.add_argument(
'--repository',
help='Path of repository containing archive to list, defaults to the configured repositories',
help='Path of repository containing archive to list, defaults to the configured repositories, quoted globs supported',
)
list_group.add_argument(
'--archive', help='Name or hash of a single archive to list (or "latest")'
)
list_group.add_argument('--archive', help='Name of the archive to list (or "latest")')
list_group.add_argument(
'--path',
metavar='PATH',
@@ -1158,22 +1317,24 @@ def make_parsers():
)
list_group.add_argument('-h', '--help', action='help', help='Show this help message and exit')
rinfo_parser = action_parsers.add_parser(
'rinfo',
aliases=ACTION_ALIASES['rinfo'],
repo_info_parser = action_parsers.add_parser(
'repo-info',
aliases=ACTION_ALIASES['repo-info'],
help='Show repository summary information such as disk space used',
description='Show repository summary information such as disk space used',
add_help=False,
)
rinfo_group = rinfo_parser.add_argument_group('rinfo arguments')
rinfo_group.add_argument(
repo_info_group = repo_info_parser.add_argument_group('repo-info arguments')
repo_info_group.add_argument(
'--repository',
help='Path of repository to show info for, defaults to the configured repository if there is only one',
help='Path of repository to show info for, defaults to the configured repository if there is only one, quoted globs supported',
)
rinfo_group.add_argument(
repo_info_group.add_argument(
'--json', dest='json', default=False, action='store_true', help='Output results as JSON'
)
rinfo_group.add_argument('-h', '--help', action='help', help='Show this help message and exit')
repo_info_group.add_argument(
'-h', '--help', action='help', help='Show this help message and exit'
)
info_parser = action_parsers.add_parser(
'info',
@@ -1185,9 +1346,11 @@ def make_parsers():
info_group = info_parser.add_argument_group('info arguments')
info_group.add_argument(
'--repository',
help='Path of repository containing archive to show info for, defaults to the configured repository if there is only one',
help='Path of repository containing archive to show info for, defaults to the configured repository if there is only one, quoted globs supported',
)
info_group.add_argument(
'--archive', help='Archive name, hash, or series to show info for (or "latest")'
)
info_group.add_argument('--archive', help='Name of archive to show info for (or "latest")')
info_group.add_argument(
'--json', dest='json', default=False, action='store_true', help='Output results as JSON'
)
@@ -1201,7 +1364,7 @@ def make_parsers():
'--match-archives',
'--glob-archives',
metavar='PATTERN',
help='Only show info for archive names matching this pattern',
help='Only show info for archive names, hashes, or series matching this pattern',
)
info_group.add_argument(
'--sort-by', metavar='KEYS', help='Comma-separated list of sorting keys'
@@ -1246,7 +1409,7 @@ def make_parsers():
break_lock_group = break_lock_parser.add_argument_group('break-lock arguments')
break_lock_group.add_argument(
'--repository',
help='Path of repository to break the lock for, defaults to the configured repository if there is only one',
help='Path of repository to break the lock for, defaults to the configured repository if there is only one, quoted globs supported',
)
break_lock_group.add_argument(
'-h', '--help', action='help', help='Show this help message and exit'
@@ -1286,7 +1449,7 @@ def make_parsers():
)
key_export_group.add_argument(
'--repository',
help='Path of repository to export the key for, defaults to the configured repository if there is only one',
help='Path of repository to export the key for, defaults to the configured repository if there is only one, quoted globs supported',
)
key_export_group.add_argument(
'--path',
@@ -1297,6 +1460,23 @@ def make_parsers():
'-h', '--help', action='help', help='Show this help message and exit'
)
key_change_passphrase_parser = key_parsers.add_parser(
'change-passphrase',
help='Change the passphrase protecting the repository key',
description='Change the passphrase protecting the repository key',
add_help=False,
)
key_change_passphrase_group = key_change_passphrase_parser.add_argument_group(
'key change-passphrase arguments'
)
key_change_passphrase_group.add_argument(
'--repository',
help='Path of repository to change the passphrase for, defaults to the configured repository if there is only one, quoted globs supported',
)
key_change_passphrase_group.add_argument(
'-h', '--help', action='help', help='Show this help message and exit'
)
borg_parser = action_parsers.add_parser(
'borg',
aliases=ACTION_ALIASES['borg'],
@@ -1307,9 +1487,11 @@ def make_parsers():
borg_group = borg_parser.add_argument_group('borg arguments')
borg_group.add_argument(
'--repository',
help='Path of repository to pass to Borg, defaults to the configured repositories',
help='Path of repository to pass to Borg, defaults to the configured repositories, quoted globs supported',
)
borg_group.add_argument(
'--archive', help='Archive name, hash, or series to pass to Borg (or "latest")'
)
borg_group.add_argument('--archive', help='Name of archive to pass to Borg (or "latest")')
borg_group.add_argument(
'--',
metavar='OPTION',
@@ -1368,9 +1550,9 @@ def parse_arguments(*unparsed_arguments):
)
if (
('list' in arguments and 'rinfo' in arguments and arguments['list'].json)
('list' in arguments and 'repo-info' in arguments and arguments['list'].json)
or ('list' in arguments and 'info' in arguments and arguments['list'].json)
or ('rinfo' in arguments and 'info' in arguments and arguments['rinfo'].json)
or ('repo-info' in arguments and 'info' in arguments and arguments['repo-info'].json)
):
raise ValueError('With the --json flag, multiple actions cannot be used together.')
@@ -1388,9 +1570,11 @@ def parse_arguments(*unparsed_arguments):
'With the list action, only one of --prefix or --match-archives flags can be used.'
)
if 'rlist' in arguments and (arguments['rlist'].prefix and arguments['rlist'].match_archives):
if 'repo-list' in arguments and (
arguments['repo-list'].prefix and arguments['repo-list'].match_archives
):
raise ValueError(
'With the rlist action, only one of --prefix or --match-archives flags can be used.'
'With the repo-list action, only one of --prefix or --match-archives flags can be used.'
)
if 'info' in arguments and (
+62 -20
View File
@@ -12,12 +12,14 @@ import colorama
import borgmatic.actions.borg
import borgmatic.actions.break_lock
import borgmatic.actions.change_passphrase
import borgmatic.actions.check
import borgmatic.actions.compact
import borgmatic.actions.config.bootstrap
import borgmatic.actions.config.generate
import borgmatic.actions.config.validate
import borgmatic.actions.create
import borgmatic.actions.delete
import borgmatic.actions.export_key
import borgmatic.actions.export_tar
import borgmatic.actions.extract
@@ -25,10 +27,11 @@ import borgmatic.actions.info
import borgmatic.actions.list
import borgmatic.actions.mount
import borgmatic.actions.prune
import borgmatic.actions.rcreate
import borgmatic.actions.repo_create
import borgmatic.actions.repo_delete
import borgmatic.actions.repo_info
import borgmatic.actions.repo_list
import borgmatic.actions.restore
import borgmatic.actions.rinfo
import borgmatic.actions.rlist
import borgmatic.actions.transfer
import borgmatic.commands.completion.bash
import borgmatic.commands.completion.fish
@@ -169,7 +172,7 @@ def run_configuration(config_filename, config, config_paths, arguments):
continue
if command.considered_soft_failure(config_filename, error):
break
continue
yield from log_error_records(
f'{repository.get("label", repository["path"])}: Error running actions for repository',
@@ -304,8 +307,8 @@ def run_actions(
)
for action_name, action_arguments in arguments.items():
if action_name == 'rcreate' and action_name not in skip_actions:
borgmatic.actions.rcreate.run_rcreate(
if action_name == 'repo-create' and action_name not in skip_actions:
borgmatic.actions.repo_create.run_repo_create(
repository,
config,
local_borg_version,
@@ -419,8 +422,8 @@ def run_actions(
local_path,
remote_path,
)
elif action_name == 'rlist' and action_name not in skip_actions:
yield from borgmatic.actions.rlist.run_rlist(
elif action_name == 'repo-list' and action_name not in skip_actions:
yield from borgmatic.actions.repo_list.run_repo_list(
repository,
config,
local_borg_version,
@@ -439,8 +442,8 @@ def run_actions(
local_path,
remote_path,
)
elif action_name == 'rinfo' and action_name not in skip_actions:
yield from borgmatic.actions.rinfo.run_rinfo(
elif action_name == 'repo-info' and action_name not in skip_actions:
yield from borgmatic.actions.repo_info.run_repo_info(
repository,
config,
local_borg_version,
@@ -479,6 +482,36 @@ def run_actions(
local_path,
remote_path,
)
elif action_name == 'change-passphrase' and action_name not in skip_actions:
borgmatic.actions.change_passphrase.run_change_passphrase(
repository,
config,
local_borg_version,
action_arguments,
global_arguments,
local_path,
remote_path,
)
elif action_name == 'delete' and action_name not in skip_actions:
borgmatic.actions.delete.run_delete(
repository,
config,
local_borg_version,
action_arguments,
global_arguments,
local_path,
remote_path,
)
elif action_name == 'repo-delete' and action_name not in skip_actions:
borgmatic.actions.repo_delete.run_repo_delete(
repository,
config,
local_borg_version,
action_arguments,
global_arguments,
local_path,
remote_path,
)
elif action_name == 'borg' and action_name not in skip_actions:
borgmatic.actions.borg.run_borg(
repository,
@@ -557,7 +590,7 @@ def load_configurations(config_filenames, overrides=None, resolve_env=True):
)
),
logging.makeLogRecord(
dict(levelno=logging.CRITICAL, levelname='CRITICAL', msg=error)
dict(levelno=logging.CRITICAL, levelname='CRITICAL', msg=str(error))
),
]
)
@@ -578,6 +611,9 @@ def log_record(suppress_log=False, **kwargs):
return record
BORG_REPOSITORY_ACCESS_ABORTED_EXIT_CODE = 62
def log_error_records(
message, error=None, levelno=logging.CRITICAL, log_command_error_output=False
):
@@ -592,13 +628,13 @@ def log_error_records(
level_name = logging._levelToName[levelno]
if not error:
yield log_record(levelno=levelno, levelname=level_name, msg=message)
yield log_record(levelno=levelno, levelname=level_name, msg=str(message))
return
try:
raise error
except CalledProcessError as error:
yield log_record(levelno=levelno, levelname=level_name, msg=message)
yield log_record(levelno=levelno, levelname=level_name, msg=str(message))
if error.output:
try:
@@ -617,10 +653,17 @@ def log_error_records(
suppress_log=True,
)
yield log_record(levelno=levelno, levelname=level_name, msg=error)
yield log_record(levelno=levelno, levelname=level_name, msg=str(error))
if error.returncode == BORG_REPOSITORY_ACCESS_ABORTED_EXIT_CODE:
yield log_record(
levelno=levelno,
levelname=level_name,
msg='\nTo work around this, set either the "relocated_repo_access_is_ok" or "unknown_unencrypted_repo_access_is_ok" option to "true", as appropriate.',
)
except (ValueError, OSError) as error:
yield log_record(levelno=levelno, levelname=level_name, msg=message)
yield log_record(levelno=levelno, levelname=level_name, msg=error)
yield log_record(levelno=levelno, levelname=level_name, msg=str(message))
yield log_record(levelno=levelno, levelname=level_name, msg=str(error))
except: # noqa: E722
# Raising above only as a means of determining the error type. Swallow the exception here
# because we don't want the exception to propagate out of this function.
@@ -651,7 +694,9 @@ def collect_highlander_action_summary_logs(configs, arguments, configuration_par
if 'bootstrap' in arguments:
try:
# No configuration file is needed for bootstrap.
local_borg_version = borg_version.local_borg_version({}, 'borg')
local_borg_version = borg_version.local_borg_version(
{}, arguments['bootstrap'].local_path
)
except (OSError, CalledProcessError, ValueError) as error:
yield from log_error_records('Error getting local Borg version', error)
return
@@ -748,9 +793,6 @@ def collect_configuration_run_summary_logs(configs, config_paths, arguments):
break
try:
if 'extract' in arguments or 'mount' in arguments:
validate.guard_single_repository_selected(repository, configs)
validate.guard_configuration_contains_repository(repository, configs)
except ValueError as error:
yield from log_error_records(str(error))
+17 -2
View File
@@ -68,6 +68,17 @@ def normalize(config_filename, config):
'''
logs = normalize_sections(config_filename, config)
if config.get('borgmatic_source_directory'):
logs.append(
logging.makeLogRecord(
dict(
levelno=logging.WARNING,
levelname='WARNING',
msg=f'{config_filename}: The borgmatic_source_directory option is deprecated and will be removed from a future release. Use borgmatic_runtime_directory and borgmatic_state_directory instead.',
)
)
)
# Upgrade exclude_if_present from a string to a list.
exclude_if_present = config.get('exclude_if_present')
if isinstance(exclude_if_present, str):
@@ -233,7 +244,11 @@ def normalize(config_filename, config):
path=updated_repository_path,
)
)
elif repository_path.startswith('ssh://'):
elif (
repository_path.startswith('ssh://')
or repository_path.startswith('sftp://')
or repository_path.startswith('rclone:')
):
config['repositories'].append(repository_dict)
else:
rewritten_repository_path = f"ssh://{repository_path.replace(':~', '/~').replace(':/', '/').replace(':', '/./')}"
@@ -242,7 +257,7 @@ def normalize(config_filename, config):
dict(
levelno=logging.WARNING,
levelname='WARNING',
msg=f'{config_filename}: Remote repository paths without ssh:// syntax are deprecated and support will be removed from a future release. Interpreting "{repository_path}" as "{rewritten_repository_path}"',
msg=f'{config_filename}: Remote repository paths without ssh:// or rclone: syntax are deprecated and support will be removed from a future release. Interpreting "{repository_path}" as "{rewritten_repository_path}"',
)
)
)
+136
View File
@@ -0,0 +1,136 @@
import logging
import os
import tempfile
logger = logging.getLogger(__name__)
def expand_user_in_path(path):
'''
Given a directory path, expand any tildes in it.
'''
try:
return os.path.expanduser(path or '') or None
except TypeError:
return None
def get_working_directory(config): # pragma: no cover
'''
Given a configuration dict, get the working directory from it, expanding any tildes.
'''
return expand_user_in_path(config.get('working_directory'))
def get_borgmatic_source_directory(config):
'''
Given a configuration dict, get the (deprecated) borgmatic source directory, expanding any
tildes. Defaults to ~/.borgmatic.
'''
return expand_user_in_path(config.get('borgmatic_source_directory') or '~/.borgmatic')
TEMPORARY_DIRECTORY_PREFIX = 'borgmatic-'
class Runtime_directory:
'''
A Python context manager for creating and cleaning up the borgmatic runtime directory used for
storing temporary runtime data like streaming database dumps and bootstrap metadata.
Example use as a context manager:
with borgmatic.config.paths.Runtime_directory(config) as borgmatic_runtime_directory:
do_something_with(borgmatic_runtime_directory)
For the scope of that "with" statement, the runtime directory is available. Afterwards, it
automatically gets cleaned up as necessary.
'''
def __init__(self, config, log_prefix):
'''
Given a configuration dict and a log prefix, determine the borgmatic runtime directory,
creating a secure, temporary directory within it if necessary. Defaults to
$XDG_RUNTIME_DIR/./borgmatic or $RUNTIME_DIRECTORY/./borgmatic or
$TMPDIR/borgmatic-[random]/./borgmatic or $TEMP/borgmatic-[random]/./borgmatic or
/tmp/borgmatic-[random]/./borgmatic where "[random]" is a randomly generated string intended
to avoid path collisions.
If XDG_RUNTIME_DIR or RUNTIME_DIRECTORY is set and already ends in "/borgmatic", then don't
tack on a second "/borgmatic" path component.
The "/./" is taking advantage of a Borg feature such that the part of the path before the "/./"
does not get stored in the file path within an archive. That way, the path of the runtime
directory can change without leaving database dumps within an archive inaccessible.
'''
runtime_directory = (
config.get('user_runtime_directory')
or os.environ.get('XDG_RUNTIME_DIR') # Set by PAM on Linux.
or os.environ.get('RUNTIME_DIRECTORY') # Set by systemd if configured.
)
if runtime_directory:
self.temporary_directory = None
else:
base_directory = os.environ.get('TMPDIR') or os.environ.get('TEMP') or '/tmp'
os.makedirs(base_directory, mode=0o700, exist_ok=True)
self.temporary_directory = tempfile.TemporaryDirectory(
prefix=TEMPORARY_DIRECTORY_PREFIX,
dir=base_directory,
)
runtime_directory = self.temporary_directory.name
(base_path, final_directory) = os.path.split(runtime_directory.rstrip(os.path.sep))
self.runtime_path = expand_user_in_path(
os.path.join(
base_path if final_directory == 'borgmatic' else runtime_directory, '.', 'borgmatic'
)
)
os.makedirs(self.runtime_path, mode=0o700, exist_ok=True)
logger.debug(f'{log_prefix}: Using runtime directory {os.path.normpath(self.runtime_path)}')
def __enter__(self):
'''
Return the borgmatic runtime path as a string.
'''
return self.runtime_path
def __exit__(self, exception, value, traceback):
'''
Delete any temporary directory that was created as part of initialization.
'''
if self.temporary_directory:
self.temporary_directory.cleanup()
def make_runtime_directory_glob(borgmatic_runtime_directory):
'''
Given a borgmatic runtime directory path, make a glob that would match that path, specifically
replacing any randomly generated temporary subdirectory with "*" since such a directory's name
changes on every borgmatic run.
'''
return os.path.join(
*(
'*' if subdirectory.startswith(TEMPORARY_DIRECTORY_PREFIX) else subdirectory
for subdirectory in os.path.normpath(borgmatic_runtime_directory).split(os.path.sep)
)
)
def get_borgmatic_state_directory(config):
'''
Given a configuration dict, get the borgmatic state directory used for storing borgmatic state
files like records of when checks last ran. Defaults to $XDG_STATE_HOME/borgmatic or
~/.local/state/./borgmatic.
'''
return expand_user_in_path(
os.path.join(
config.get('user_state_directory')
or os.environ.get('XDG_STATE_HOME')
or os.environ.get('STATE_DIRECTORY') # Set by systemd if configured.
or '~/.local/state',
'borgmatic',
)
)
+524 -64
View File
@@ -58,9 +58,10 @@ properties:
working_directory:
type: string
description: |
Working directory for the "borg create" command. Tildes are
expanded. Useful for backing up using relative paths. See
http://borgbackup.readthedocs.io/en/stable/usage/create.html for
Working directory to use when running actions, useful for backing up
using relative source directory paths. Does not currently apply to
borgmatic configuration file paths or includes. Tildes are expanded.
See http://borgbackup.readthedocs.io/en/stable/usage/create.html for
details. Defaults to not set.
example: /path/to/working/directory
one_file_system:
@@ -206,11 +207,28 @@ properties:
borgmatic_source_directory:
type: string
description: |
Path for additional source files used for temporary internal state
like borgmatic database dumps. Note that changing this path prevents
"borgmatic restore" from finding any database dumps created before
the change. Defaults to ~/.borgmatic
Deprecated. Only used for locating database dumps and bootstrap
metadata within backup archives created prior to deprecation.
Replaced by borgmatic_runtime_directory and
borgmatic_state_directory. Defaults to ~/.borgmatic
example: /tmp/borgmatic
user_runtime_directory:
type: string
description: |
Path for storing temporary runtime data like streaming database
dumps and bootstrap metadata. borgmatic automatically creates and
uses a "borgmatic" subdirectory here. Defaults to $XDG_RUNTIME_DIR
or or $TMPDIR or $TEMP or /run/user/$UID.
example: /run/user/1001
user_state_directory:
type: string
description: |
Path for storing borgmatic state files like records of when checks
last ran. borgmatic automatically creates and uses a "borgmatic"
subdirectory here. If you change this option, borgmatic must
create the check records again (and therefore re-run checks).
Defaults to $XDG_STATE_HOME or ~/.local/state.
example: /var/lib/borgmatic
store_config_files:
type: boolean
description: |
@@ -231,7 +249,10 @@ properties:
key. Only use on repositories that were initialized with
passcommand/repokey/keyfile encryption. Note that if both
encryption_passcommand and encryption_passphrase are set, then
encryption_passphrase takes precedence. Defaults to not set.
encryption_passphrase takes precedence. This can also be used to
access encrypted systemd service credentials. Defaults to not set.
For more details, see:
https://torsion.org/borgmatic/docs/how-to/provide-your-passwords/
example: "secret-tool lookup borg-repository repo-name"
encryption_passphrase:
type: string
@@ -393,10 +414,13 @@ properties:
archive_name_format:
type: string
description: |
Name of the archive. Borg placeholders can be used. See the output
of "borg help placeholders" for details. Defaults to
"{hostname}-{now:%Y-%m-%dT%H:%M:%S.%f}". When running actions like
rlist, info, or check, borgmatic automatically tries to match only
Name of the archive to create. Borg placeholders can be used. See
the output of "borg help placeholders" for details. Defaults to
"{hostname}-{now:%Y-%m-%dT%H:%M:%S.%f}" with Borg 1 and
"{hostname}" with Borg 2, as Borg 2 does not require unique
archive names; identical archive names form a common "series" that
can be targeted together. When running actions like repo-list,
info, or check, borgmatic automatically tries to match only
archives created with this name format.
example: "{hostname}-documents-{now}"
match_archives:
@@ -521,18 +545,20 @@ properties:
- extract
- disabled
description: |
Name of consistency check to run: "repository",
"archives", "data", "spot", and/or "extract".
"repository" checks the consistency of the
repository, "archives" checks all of the
archives, "data" verifies the integrity of the
data within the archives, "spot" checks that
some percentage of source files are found in the
most recent archive (with identical contents),
and "extract" does an extraction dry-run of the
most recent archive. Note that "data" implies
"archives". See "skip_actions" for disabling
checks altogether.
Name of the consistency check to run:
* "repository" checks the consistency of the
repository.
* "archives" checks all of the archives.
* "data" verifies the integrity of the data
within the archives and implies the "archives"
check as well.
* "spot" checks that some percentage of source
files are found in the most recent archive (with
identical contents).
* "extract" does an extraction dry-run of the
most recent archive.
* See "skip_actions" for disabling checks
altogether.
example: spot
frequency:
type: string
@@ -546,6 +572,20 @@ properties:
"always": running this check every time checks
are run.
example: 2 weeks
only_run_on:
type: array
items:
type: string
description: |
After the "frequency" duration has elapsed, only
run this check if the current day of the week
matches one of these values (the name of a day of
the week in the current locale). "weekday" and
"weekend" are also accepted. Defaults to running
the check on any day of the week.
example:
- Saturday
- Sunday
- required: [name]
additionalProperties: false
properties:
@@ -554,18 +594,20 @@ properties:
enum:
- repository
description: |
Name of consistency check to run: "repository",
"archives", "data", "spot", and/or "extract".
"repository" checks the consistency of the
repository, "archives" checks all of the
archives, "data" verifies the integrity of the
data within the archives, "spot" checks that
some percentage of source files are found in the
most recent archive (with identical contents),
and "extract" does an extraction dry-run of the
most recent archive. Note that "data" implies
"archives". See "skip_actions" for disabling
checks altogether.
Name of the consistency check to run:
* "repository" checks the consistency of the
repository.
* "archives" checks all of the archives.
* "data" verifies the integrity of the data
within the archives and implies the "archives"
check as well.
* "spot" checks that some percentage of source
files are found in the most recent archive (with
identical contents).
* "extract" does an extraction dry-run of the
most recent archive.
* See "skip_actions" for disabling checks
altogether.
example: spot
frequency:
type: string
@@ -579,6 +621,20 @@ properties:
"always": running this check every time checks
are run.
example: 2 weeks
only_run_on:
type: array
items:
type: string
description: |
After the "frequency" duration has elapsed, only
run this check if the current day of the week
matches one of these values (the name of a day of
the week in the current locale). "weekday" and
"weekend" are also accepted. Defaults to running
the check on any day of the week.
example:
- Saturday
- Sunday
max_duration:
type: integer
description: |
@@ -602,18 +658,20 @@ properties:
enum:
- spot
description: |
Name of consistency check to run: "repository",
"archives", "data", "spot", and/or "extract".
"repository" checks the consistency of the
repository, "archives" checks all of the
archives, "data" verifies the integrity of the
data within the archives, "spot" checks that
some percentage of source files are found in the
most recent archive (with identical contents),
and "extract" does an extraction dry-run of the
most recent archive. Note that "data" implies
"archives". See "skip_actions" for disabling
checks altogether.
Name of the consistency check to run:
* "repository" checks the consistency of the
repository.
* "archives" checks all of the archives.
* "data" verifies the integrity of the data
within the archives and implies the "archives"
check as well.
* "spot" checks that some percentage of source
files are found in the most recent archive (with
identical contents).
* "extract" does an extraction dry-run of the
most recent archive.
* See "skip_actions" for disabling checks
altogether.
example: repository
frequency:
type: string
@@ -627,6 +685,20 @@ properties:
"always": running this check every time checks
are run.
example: 2 weeks
only_run_on:
type: array
items:
type: string
description: |
After the "frequency" duration has elapsed, only
run this check if the current day of the week
matches one of these values (the name of a day of
the week in the current locale). "weekday" and
"weekend" are also accepted. Defaults to running
the check on any day of the week.
example:
- Saturday
- Sunday
count_tolerance_percentage:
type: number
description: |
@@ -635,7 +707,8 @@ properties:
archive file count that is allowed before the
entire consistency check fails. This can catch
problems like incorrect excludes, inadvertent
deletes, etc. Only applies to the "spot" check.
deletes, etc. Required (and only valid) for the
"spot" check.
example: 10
data_sample_percentage:
type: number
@@ -643,7 +716,8 @@ properties:
The percentage of total files in the source
directories to randomly sample and compare to
their corresponding files in the most recent
backup archive. Only applies to the "spot" check.
backup archive. Required (and only valid) for the
"spot" check.
example: 1
data_tolerance_percentage:
type: number
@@ -655,7 +729,7 @@ properties:
been bulk-changed by malware, backups that have
been tampered with, etc. The value must be lower
than or equal to the "contents_sample_percentage".
Only applies to the "spot" check.
Required (and only valid) for the "spot" check.
example: 0.5
xxh64sum_command:
type: string
@@ -664,7 +738,7 @@ properties:
source files, usually found in an OS package named
"xxhash". Do not substitute with a different hash
type (SHA, MD5, etc.) or the check will never
succeed. Only applies to the "spot" check.
succeed. Only valid for the "spot" check.
example: /usr/local/bin/xxh64sum
description: |
List of one or more consistency checks to run on a periodic basis
@@ -699,21 +773,23 @@ properties:
items:
type: string
enum:
- rcreate
- repo-create
- transfer
- prune
- compact
- create
- check
- delete
- extract
- config
- export-tar
- mount
- umount
- repo-delete
- restore
- rlist
- repo-list
- list
- rinfo
- repo-info
- info
- break-lock
- key
@@ -992,9 +1068,12 @@ properties:
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 container). Defaults to
"pg_dump" for single database dump or "pg_dumpall" to
dump all databases.
(e.g., one inside a running container). If you run it
from within a container, make sure to mount your
host's ".borgmatic" folder into the container using
the same directory structure. 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
@@ -1108,7 +1187,10 @@ properties:
description: |
Command to use instead of "mariadb-dump". This can be
used to run a specific mariadb_dump version (e.g., one
inside a running container). Defaults to "mariadb-dump".
inside a running container). If you run it from within
a container, make sure to mount your host's
".borgmatic" folder into the container using the same
directory structure. Defaults to "mariadb-dump".
example: docker exec mariadb_container mariadb-dump
mariadb_command:
type: string
@@ -1236,9 +1318,12 @@ properties:
mysql_dump_command:
type: string
description: |
Command to use instead of "mysqldump". This can be used
to run a specific mysql_dump version (e.g., one inside a
running container). Defaults to "mysqldump".
Command to use instead of "mysqldump". This can be
used to run a specific mysql_dump version (e.g., one
inside a running container). If you run it from within
a container, make sure to mount your host's
".borgmatic" folder into the container using the same
directory structure. Defaults to "mysqldump".
example: docker exec mysql_container mysqldump
mysql_command:
type: string
@@ -1541,6 +1626,349 @@ properties:
example:
- start
- finish
pushover:
type: object
required: ['token', 'user']
additionalProperties: false
properties:
token:
type: string
description: |
Your application's API token.
example: 7ms6TXHpTokTou2P6x4SodDeentHRa
user:
type: string
description: |
Your user/group key (or that of your target user), viewable
when logged into your dashboard: often referred to as
USER_KEY in Pushover documentation and code examples.
example: hwRwoWsXMBWwgrSecfa9EfPey55WSN
start:
type: object
properties:
message:
type: string
description: |
Message to be sent to the user or group. If omitted
the default is the name of the state.
example: A backup job has started.
priority:
type: integer
description: |
A value of -2, -1, 0 (default), 1 or 2 that
indicates the message priority.
example: 0
expire:
type: integer
description: |
How many seconds your notification will continue
to be retried (every retry seconds). Defaults to
600. This settings only applies to priority 2
notifications.
example: 600
retry:
type: integer
description: |
The retry parameter specifies how often
(in seconds) the Pushover servers will send the
same notification to the user. Defaults to 30. This
settings only applies to priority 2 notifications.
example: 30
device:
type: string
description: |
The name of one of your devices to send just to
that device instead of all devices.
example: pixel8
html:
type: boolean
description: |
Set to True to enable HTML parsing of the message.
Set to False for plain text.
example: True
sound:
type: string
description: |
The name of a supported sound to override your
default sound choice. All options can be found
here: https://pushover.net/api#sounds
example: bike
title:
type: string
description: |
Your message's title, otherwise your app's name is
used.
example: A backup job has started.
ttl:
type: integer
description: |
The number of seconds that the message will live,
before being deleted automatically. The ttl
parameter is ignored for messages with a priority.
value of 2.
example: 3600
url:
type: string
description: |
A supplementary URL to show with your message.
example: https://pushover.net/apps/xxxxx-borgbackup
url_title:
type: string
description: |
A title for the URL specified as the url parameter,
otherwise just the URL is shown.
example: Pushover Link
finish:
type: object
properties:
message:
type: string
description: |
Message to be sent to the user or group. If omitted
the default is the name of the state.
example: A backup job has finished.
priority:
type: integer
description: |
A value of -2, -1, 0 (default), 1 or 2 that
indicates the message priority.
example: 0
expire:
type: integer
description: |
How many seconds your notification will continue
to be retried (every retry seconds). Defaults to
600. This settings only applies to priority 2
notifications.
example: 600
retry:
type: integer
description: |
The retry parameter specifies how often
(in seconds) the Pushover servers will send the
same notification to the user. Defaults to 30. This
settings only applies to priority 2 notifications.
example: 30
device:
type: string
description: |
The name of one of your devices to send just to
that device instead of all devices.
example: pixel8
html:
type: boolean
description: |
Set to True to enable HTML parsing of the message.
Set to False for plain text.
example: True
sound:
type: string
description: |
The name of a supported sound to override your
default sound choice. All options can be found
here: https://pushover.net/api#sounds
example: bike
title:
type: string
description: |
Your message's title, otherwise your app's name is
used.
example: A backup job has started.
ttl:
type: integer
description: |
The number of seconds that the message will live,
before being deleted automatically. The ttl
parameter is ignored for messages with a priority.
value of 2.
example: 3600
url:
type: string
description: |
A supplementary URL to show with your message.
example: https://pushover.net/apps/xxxxx-borgbackup
url_title:
type: string
description: |
A title for the URL specified as the url parameter,
otherwise just the URL is shown.
example: Pushover Link
fail:
type: object
properties:
message:
type: string
description: |
Message to be sent to the user or group. If omitted
the default is the name of the state.
example: A backup job has failed.
priority:
type: integer
description: |
A value of -2, -1, 0 (default), 1 or 2 that
indicates the message priority.
example: 0
expire:
type: integer
description: |
How many seconds your notification will continue
to be retried (every retry seconds). Defaults to
600. This settings only applies to priority 2
notifications.
example: 600
retry:
type: integer
description: |
The retry parameter specifies how often
(in seconds) the Pushover servers will send the
same notification to the user. Defaults to 30. This
settings only applies to priority 2 notifications.
example: 30
device:
type: string
description: |
The name of one of your devices to send just to
that device instead of all devices.
example: pixel8
html:
type: boolean
description: |
Set to True to enable HTML parsing of the message.
Set to False for plain text.
example: True
sound:
type: string
description: |
The name of a supported sound to override your
default sound choice. All options can be found
here: https://pushover.net/api#sounds
example: bike
title:
type: string
description: |
Your message's title, otherwise your app's name is
used.
example: A backup job has started.
ttl:
type: integer
description: |
The number of seconds that the message will live,
before being deleted automatically. The ttl
parameter is ignored for messages with a priority.
value of 2.
example: 3600
url:
type: string
description: |
A supplementary URL to show with your message.
example: https://pushover.net/apps/xxxxx-borgbackup
url_title:
type: string
description: |
A title for the URL specified as the url parameter,
otherwise just the URL is shown.
example: Pushover Link
states:
type: array
items:
type: string
enum:
- start
- finish
- fail
uniqueItems: true
description: |
List of one or more monitoring states to ping for: "start",
"finish", and/or "fail". Defaults to pinging for failure
only.
example:
- start
- finish
zabbix:
type: object
additionalProperties: false
properties:
itemid:
type: integer
description: |
The ID of the Zabbix item used for collecting data.
Unique across the entire Zabbix system.
example: 55105
host:
type: string
description: |
Host name where the item is stored. Required if "itemid"
is not set.
example: borg-server
key:
type: string
description: |
Key of the host where the item is stored. Required if
"itemid" is not set.
example: borg.status
server:
type: string
description: |
The address of your Zabbix instance.
example: https://zabbix.your-domain.com
username:
type: string
description: |
The username used for authentication. Not needed if using
an API key.
example: testuser
password:
type: string
description: |
The password used for authentication. Not needed if using
an API key.
example: fakepassword
api_key:
type: string
description: |
The API key used for authentication. Not needed if using
an username/password.
example: fakekey
start:
type: object
properties:
value:
type: ["integer", "string"]
description: |
The value to set the item to on start.
example: STARTED
finish:
type: object
properties:
value:
type: ["integer", "string"]
description: |
The value to set the item to on finish.
example: FINISH
fail:
type: object
properties:
value:
type: ["integer", "string"]
description: |
The value to set the item to on fail.
example: ERROR
states:
type: array
items:
type: string
enum:
- start
- finish
- fail
uniqueItems: true
description: |
List of one or more monitoring states to ping for: "start",
"finish", and/or "fail". Defaults to pinging for failure
only.
example:
- start
- finish
apprise:
type: object
required: ['services']
@@ -1724,6 +2152,38 @@ properties:
an account at https://healthchecks.io (or self-host Healthchecks) if
you'd like to use this service. See borgmatic monitoring
documentation for details.
uptime_kuma:
type: object
required: ['push_url']
additionalProperties: false
properties:
push_url:
type: string
description: |
Uptime Kuma push URL without query string (do not include the
question mark or anything after it).
example: https://example.uptime.kuma/api/push/abcd1234
states:
type: array
items:
type: string
enum:
- start
- finish
- fail
uniqueItems: true
description: |
List of one or more monitoring states to push for: "start",
"finish", and/or "fail". Defaults to pushing for all
states.
example:
- start
- finish
- fail
description: |
Configuration for a monitoring integration with Uptime Kuma using
the Push monitor type.
See more information here: https://uptime.kuma.pet
cronitor:
type: object
required: ['ping_url']
@@ -1788,8 +2248,8 @@ properties:
Allows setting custom labels for the logging stream. At
least one label is required. "__hostname" gets replaced by
the machine hostname automatically. "__config" gets replaced
by just the name of the configuration file. "__config_path"
gets replaced by the full path of the configuration file.
by the name of the configuration file. "__config_path" gets
replaced by the full path of the configuration file.
example:
app: "borgmatic"
config: "__config"
+18 -28
View File
@@ -1,3 +1,4 @@
import fnmatch
import os
import jsonschema
@@ -149,18 +150,30 @@ def normalize_repository_path(repository):
return repository
def glob_match(first, second):
'''
Given two strings, return whether the first matches the second. Globs are
supported.
'''
if first is None or second is None:
return False
return fnmatch.fnmatch(first, second) or fnmatch.fnmatch(second, first)
def repositories_match(first, second):
'''
Given two repository dicts with keys 'path' (relative and/or absolute),
and 'label', or two repository paths, return whether they match.
Given two repository dicts with keys "path" (relative and/or absolute),
and "label", two repository paths as strings, or a mix of the two formats,
return whether they match. Globs are supported.
'''
if isinstance(first, str):
first = {'path': first, 'label': first}
if isinstance(second, str):
second = {'path': second, 'label': second}
return (first.get('label') == second.get('label')) or (
normalize_repository_path(first.get('path'))
== normalize_repository_path(second.get('path'))
return glob_match(first.get('label'), second.get('label')) or glob_match(
normalize_repository_path(first.get('path')), normalize_repository_path(second.get('path'))
)
@@ -186,26 +199,3 @@ def guard_configuration_contains_repository(repository, configurations):
if count == 0:
raise ValueError(f'Repository "{repository}" not found in configuration files')
def guard_single_repository_selected(repository, configurations):
'''
Given a repository path and a dict mapping from config filename to corresponding parsed config
dict, ensure either a single repository exists across all configuration files or a repository
path was given.
'''
if repository:
return
count = len(
tuple(
config_repository
for config in configurations.values()
for config_repository in config['repositories']
)
)
if count != 1:
raise ValueError(
"Can't determine which repository to use. Use --repository to disambiguate"
)
+1 -1
View File
@@ -90,7 +90,7 @@ def considered_soft_failure(config_filename, error):
if exit_code == SOFT_FAIL_EXIT_CODE:
logger.info(
f'{config_filename}: Command hook exited with soft failure exit code ({SOFT_FAIL_EXIT_CODE}); skipping remaining actions',
f'{config_filename}: Command hook exited with soft failure exit code ({SOFT_FAIL_EXIT_CODE}); skipping remaining repository actions',
)
return True
+7 -1
View File
@@ -12,7 +12,10 @@ from borgmatic.hooks import (
ntfy,
pagerduty,
postgresql,
pushover,
sqlite,
uptimekuma,
zabbix,
)
logger = logging.getLogger(__name__)
@@ -22,14 +25,17 @@ HOOK_NAME_TO_MODULE = {
'cronhub': cronhub,
'cronitor': cronitor,
'healthchecks': healthchecks,
'loki': loki,
'mariadb_databases': mariadb,
'mongodb_databases': mongodb,
'mysql_databases': mysql,
'ntfy': ntfy,
'pagerduty': pagerduty,
'postgresql_databases': postgresql,
'pushover': pushover,
'sqlite_databases': sqlite,
'loki': loki,
'uptime_kuma': uptimekuma,
'zabbix': zabbix,
}
+18 -18
View File
@@ -1,9 +1,8 @@
import fnmatch
import logging
import os
import shutil
from borgmatic.borg.state import DEFAULT_BORGMATIC_SOURCE_DIRECTORY
logger = logging.getLogger(__name__)
DATA_SOURCE_HOOK_NAMES = (
@@ -15,15 +14,12 @@ DATA_SOURCE_HOOK_NAMES = (
)
def make_data_source_dump_path(borgmatic_source_directory, data_source_hook_name):
def make_data_source_dump_path(borgmatic_runtime_directory, data_source_hook_name):
'''
Given a borgmatic source directory (or None) and a data source hook name, construct a data
source dump path.
Given a borgmatic runtime directory and a data source hook name, construct a data source dump
path.
'''
if not borgmatic_source_directory:
borgmatic_source_directory = DEFAULT_BORGMATIC_SOURCE_DIRECTORY
return os.path.join(borgmatic_source_directory, data_source_hook_name)
return os.path.join(borgmatic_runtime_directory, data_source_hook_name)
def make_data_source_dump_filename(dump_path, name, hostname=None):
@@ -36,7 +32,7 @@ def make_data_source_dump_filename(dump_path, name, hostname=None):
if os.path.sep in name:
raise ValueError(f'Invalid data source name {name}')
return os.path.join(os.path.expanduser(dump_path), hostname or 'localhost', name)
return os.path.join(dump_path, hostname or 'localhost', name)
def create_parent_directory_for_dump(dump_path):
@@ -63,18 +59,22 @@ def remove_data_source_dumps(dump_path, data_source_type_name, log_prefix, dry_r
logger.debug(f'{log_prefix}: Removing {data_source_type_name} data source dumps{dry_run_label}')
expanded_path = os.path.expanduser(dump_path)
if dry_run:
return
if os.path.exists(expanded_path):
shutil.rmtree(expanded_path)
if os.path.exists(dump_path):
shutil.rmtree(dump_path)
def convert_glob_patterns_to_borg_patterns(patterns):
def convert_glob_patterns_to_borg_pattern(patterns):
'''
Convert a sequence of shell glob patterns like "/etc/*" to the corresponding Borg archive
patterns like "sh:etc/*".
Convert a sequence of shell glob patterns like "/etc/*", "/tmp/*" to the corresponding Borg
regular expression archive pattern as a single string like "re:etc/.*|tmp/.*".
'''
return [f'sh:{pattern.lstrip(os.path.sep)}' for pattern in patterns]
# Remove the "\Z" generated by fnmatch.translate() because we don't want the pattern to match
# only at the end of a path, as directory format dumps require extracting files with paths
# longer than the pattern. E.g., a pattern of "borgmatic/*/foo_databases/test" should also match
# paths like "borgmatic/*/foo_databases/test/toc.dat"
return 're:' + '|'.join(
fnmatch.translate(pattern.lstrip('/')).replace('\\Z', '') for pattern in patterns
)
+1 -1
View File
@@ -15,7 +15,7 @@ MONITOR_STATE_TO_HEALTHCHECKS = {
monitor.State.LOG: 'log',
}
DEFAULT_PING_BODY_LIMIT_BYTES = 1500
DEFAULT_PING_BODY_LIMIT_BYTES = 100000
HANDLER_IDENTIFIER = 'healthchecks'
+45 -24
View File
@@ -3,6 +3,7 @@ import logging
import os
import shlex
import borgmatic.config.paths
from borgmatic.execute import (
execute_command,
execute_command_and_capture_output,
@@ -13,13 +14,11 @@ from borgmatic.hooks import dump
logger = logging.getLogger(__name__)
def make_dump_path(config): # pragma: no cover
def make_dump_path(base_directory): # pragma: no cover
'''
Make the dump path from the given configuration dict and the name of this hook.
Given a base directory, make the corresponding dump path.
'''
return dump.make_data_source_dump_path(
config.get('borgmatic_source_directory'), 'mariadb_databases'
)
return dump.make_data_source_dump_path(base_directory, 'mariadb_databases')
SYSTEM_DATABASE_NAMES = ('information_schema', 'mysql', 'performance_schema', 'sys')
@@ -123,12 +122,12 @@ def use_streaming(databases, config, log_prefix):
return any(databases)
def dump_data_sources(databases, config, log_prefix, dry_run):
def dump_data_sources(databases, config, log_prefix, borgmatic_runtime_directory, dry_run):
'''
Dump the given MariaDB databases to a named pipe. The databases are supplied as a sequence of
dicts, one dict describing each database as per the configuration schema. Use the given
configuration dict to construct the destination path and the given log prefix in any log
entries.
borgmatic runtime directory to construct the destination path and the given log prefix in any
log entries.
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.
@@ -139,7 +138,7 @@ def dump_data_sources(databases, config, log_prefix, dry_run):
logger.info(f'{log_prefix}: Dumping MariaDB databases{dry_run_label}')
for database in databases:
dump_path = make_dump_path(config)
dump_path = make_dump_path(borgmatic_runtime_directory)
extra_environment = {'MYSQL_PWD': database['password']} if 'password' in database else None
dump_database_names = database_names_to_dump(
database, extra_environment, log_prefix, dry_run
@@ -182,33 +181,55 @@ def dump_data_sources(databases, config, log_prefix, dry_run):
return [process for process in processes if process]
def remove_data_source_dumps(databases, config, log_prefix, dry_run): # pragma: no cover
def remove_data_source_dumps(
databases, config, log_prefix, borgmatic_runtime_directory, dry_run
): # pragma: no cover
'''
Remove all database dump files for this hook regardless of the given databases. Use the given
configuration dict to construct the destination path and the log prefix in any log entries. If
this is a dry run, then don't actually remove anything.
Remove all database dump files for this hook regardless of the given databases. Use the
borgmatic_runtime_directory to construct the destination path and the log prefix in any log
entries. If this is a dry run, then don't actually remove anything.
'''
dump.remove_data_source_dumps(make_dump_path(config), 'MariaDB', log_prefix, dry_run)
dump.remove_data_source_dumps(
make_dump_path(borgmatic_runtime_directory), 'MariaDB', log_prefix, dry_run
)
def make_data_source_dump_pattern(databases, config, log_prefix, name=None): # pragma: no cover
def make_data_source_dump_patterns(
databases, config, log_prefix, borgmatic_runtime_directory, name=None
): # pragma: no cover
'''
Given a sequence of configurations dicts, a configuration dict, a prefix to log with, and a
database name to match, return the corresponding glob patterns to match the database dump in an
archive.
Given a sequence of configurations dicts, a configuration dict, a prefix to log with, the
borgmatic runtime directory, and a database name to match, return the corresponding glob
patterns to match the database dump in an archive.
'''
return dump.make_data_source_dump_filename(make_dump_path(config), name, hostname='*')
borgmatic_source_directory = borgmatic.config.paths.get_borgmatic_source_directory(config)
return (
dump.make_data_source_dump_filename(make_dump_path('borgmatic'), name, hostname='*'),
dump.make_data_source_dump_filename(
make_dump_path(borgmatic_runtime_directory), name, hostname='*'
),
dump.make_data_source_dump_filename(
make_dump_path(borgmatic_source_directory), name, hostname='*'
),
)
def restore_data_source_dump(
hook_config, config, log_prefix, data_source, dry_run, extract_process, connection_params
hook_config,
config,
log_prefix,
data_source,
dry_run,
extract_process,
connection_params,
borgmatic_runtime_directory,
):
'''
Restore a database from the given extract stream. The database is supplied as a data source
configuration dict, but the given hook configuration is ignored. The given configuration dict is
used to construct the destination path, and the given log prefix is used for 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.
configuration dict, but the given hook configuration is ignored. The given log prefix is used
for 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 ''
hostname = connection_params['hostname'] or data_source.get(
+46 -21
View File
@@ -1,19 +1,18 @@
import logging
import shlex
import borgmatic.config.paths
from borgmatic.execute import execute_command, execute_command_with_processes
from borgmatic.hooks import dump
logger = logging.getLogger(__name__)
def make_dump_path(config): # pragma: no cover
def make_dump_path(base_directory): # pragma: no cover
'''
Make the dump path from the given configuration dict and the name of this hook.
Given a base directory, make the corresponding dump path.
'''
return dump.make_data_source_dump_path(
config.get('borgmatic_source_directory'), 'mongodb_databases'
)
return dump.make_data_source_dump_path(base_directory, 'mongodb_databases')
def use_streaming(databases, config, log_prefix):
@@ -24,11 +23,12 @@ def use_streaming(databases, config, log_prefix):
return any(database.get('format') != 'directory' for database in databases)
def dump_data_sources(databases, config, log_prefix, dry_run):
def dump_data_sources(databases, config, log_prefix, borgmatic_runtime_directory, dry_run):
'''
Dump the given MongoDB databases to a named pipe. The databases are supplied as a sequence of
dicts, one dict describing each database as per the configuration schema. Use the configuration
dict to construct the destination path and the given log prefix in any log entries.
dicts, one dict describing each database as per the configuration schema. Use the borgmatic
runtime directory to construct the destination path (used for the directory format and the given
log prefix in any log entries.
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.
@@ -41,7 +41,7 @@ def dump_data_sources(databases, config, log_prefix, dry_run):
for database in databases:
name = database['name']
dump_filename = dump.make_data_source_dump_filename(
make_dump_path(config), name, database.get('hostname')
make_dump_path(borgmatic_runtime_directory), name, database.get('hostname')
)
dump_format = database.get('format', 'archive')
@@ -91,26 +91,49 @@ def build_dump_command(database, dump_filename, dump_format):
)
def remove_data_source_dumps(databases, config, log_prefix, dry_run): # pragma: no cover
def remove_data_source_dumps(
databases, config, log_prefix, borgmatic_runtime_directory, dry_run
): # pragma: no cover
'''
Remove all database dump files for this hook regardless of the given databases. Use the log
prefix in any log entries. Use the given configuration dict to construct the destination path.
If this is a dry run, then don't actually remove anything.
Remove all database dump files for this hook regardless of the given databases. Use the
borgmatic_runtime_directory to construct the destination path and the log prefix in any log
entries. If this is a dry run, then don't actually remove anything.
'''
dump.remove_data_source_dumps(make_dump_path(config), 'MongoDB', log_prefix, dry_run)
dump.remove_data_source_dumps(
make_dump_path(borgmatic_runtime_directory), 'MongoDB', log_prefix, dry_run
)
def make_data_source_dump_pattern(databases, config, log_prefix, name=None): # pragma: no cover
def make_data_source_dump_patterns(
databases, config, log_prefix, borgmatic_runtime_directory, name=None
): # pragma: no cover
'''
Given a sequence of database configurations dicts, a configuration dict, a prefix to log with,
and a database name to match, return the corresponding glob patterns to match the database dump
in an archive.
Given a sequence of configurations dicts, a configuration dict, a prefix to log with, the
borgmatic runtime directory, and a database name to match, return the corresponding glob
patterns to match the database dump in an archive.
'''
return dump.make_data_source_dump_filename(make_dump_path(config), name, hostname='*')
borgmatic_source_directory = borgmatic.config.paths.get_borgmatic_source_directory(config)
return (
dump.make_data_source_dump_filename(make_dump_path('borgmatic'), name, hostname='*'),
dump.make_data_source_dump_filename(
make_dump_path(borgmatic_runtime_directory), name, hostname='*'
),
dump.make_data_source_dump_filename(
make_dump_path(borgmatic_source_directory), name, hostname='*'
),
)
def restore_data_source_dump(
hook_config, config, log_prefix, data_source, dry_run, extract_process, connection_params
hook_config,
config,
log_prefix,
data_source,
dry_run,
extract_process,
connection_params,
borgmatic_runtime_directory,
):
'''
Restore a database from the given extract stream. The database is supplied as a data source
@@ -124,7 +147,9 @@ def restore_data_source_dump(
'''
dry_run_label = ' (dry run; not actually restoring anything)' if dry_run else ''
dump_filename = dump.make_data_source_dump_filename(
make_dump_path(config), data_source['name'], data_source.get('hostname')
make_dump_path(borgmatic_runtime_directory),
data_source['name'],
data_source.get('hostname'),
)
restore_command = build_restore_command(
extract_process, data_source, dump_filename, connection_params
+12 -1
View File
@@ -1,6 +1,17 @@
from enum import Enum
MONITOR_HOOK_NAMES = ('apprise', 'healthchecks', 'cronitor', 'cronhub', 'pagerduty', 'ntfy', 'loki')
MONITOR_HOOK_NAMES = (
'apprise',
'cronhub',
'cronitor',
'healthchecks',
'loki',
'ntfy',
'pagerduty',
'pushover',
'uptime_kuma',
'zabbix',
)
class State(Enum):
+45 -23
View File
@@ -3,6 +3,7 @@ import logging
import os
import shlex
import borgmatic.config.paths
from borgmatic.execute import (
execute_command,
execute_command_and_capture_output,
@@ -13,13 +14,11 @@ from borgmatic.hooks import dump
logger = logging.getLogger(__name__)
def make_dump_path(config): # pragma: no cover
def make_dump_path(base_directory): # pragma: no cover
'''
Make the dump path from the given configuration dict and the name of this hook.
Given a base directory, make the corresponding dump path.
'''
return dump.make_data_source_dump_path(
config.get('borgmatic_source_directory'), 'mysql_databases'
)
return dump.make_data_source_dump_path(base_directory, 'mysql_databases')
SYSTEM_DATABASE_NAMES = ('information_schema', 'mysql', 'performance_schema', 'sys')
@@ -122,11 +121,12 @@ def use_streaming(databases, config, log_prefix):
return any(databases)
def dump_data_sources(databases, config, log_prefix, dry_run):
def dump_data_sources(databases, config, log_prefix, borgmatic_runtime_directory, dry_run):
'''
Dump the given MySQL/MariaDB databases to a named pipe. The databases are supplied as a sequence
of dicts, one dict describing each database as per the configuration schema. Use the given
configuration dict to construct the destination path and the given log prefix in any log entries.
borgmatic runtime directory to construct the destination path and the given log prefix in any
log entries.
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.
@@ -137,7 +137,7 @@ def dump_data_sources(databases, config, log_prefix, dry_run):
logger.info(f'{log_prefix}: Dumping MySQL databases{dry_run_label}')
for database in databases:
dump_path = make_dump_path(config)
dump_path = make_dump_path(borgmatic_runtime_directory)
extra_environment = {'MYSQL_PWD': database['password']} if 'password' in database else None
dump_database_names = database_names_to_dump(
database, extra_environment, log_prefix, dry_run
@@ -180,33 +180,55 @@ def dump_data_sources(databases, config, log_prefix, dry_run):
return [process for process in processes if process]
def remove_data_source_dumps(databases, config, log_prefix, dry_run): # pragma: no cover
def remove_data_source_dumps(
databases, config, log_prefix, borgmatic_runtime_directory, dry_run
): # pragma: no cover
'''
Remove all database dump files for this hook regardless of the given databases. Use the given
configuration dict to construct the destination path and the log prefix in any log entries. If
this is a dry run, then don't actually remove anything.
Remove all database dump files for this hook regardless of the given databases. Use the
borgmatic runtime directory to construct the destination path and the log prefix in any log
entries. If this is a dry run, then don't actually remove anything.
'''
dump.remove_data_source_dumps(make_dump_path(config), 'MySQL', log_prefix, dry_run)
dump.remove_data_source_dumps(
make_dump_path(borgmatic_runtime_directory), 'MySQL', log_prefix, dry_run
)
def make_data_source_dump_pattern(databases, config, log_prefix, name=None): # pragma: no cover
def make_data_source_dump_patterns(
databases, config, log_prefix, borgmatic_runtime_directory, name=None
): # pragma: no cover
'''
Given a sequence of configurations dicts, a configuration dict, a prefix to log with, and a
database name to match, return the corresponding glob patterns to match the database dump in an
archive.
Given a sequence of configurations dicts, a configuration dict, a prefix to log with, the
borgmatic runtime directory, and a database name to match, return the corresponding glob
patterns to match the database dump in an archive.
'''
return dump.make_data_source_dump_filename(make_dump_path(config), name, hostname='*')
borgmatic_source_directory = borgmatic.config.paths.get_borgmatic_source_directory(config)
return (
dump.make_data_source_dump_filename(make_dump_path('borgmatic'), name, hostname='*'),
dump.make_data_source_dump_filename(
make_dump_path(borgmatic_runtime_directory), name, hostname='*'
),
dump.make_data_source_dump_filename(
make_dump_path(borgmatic_source_directory), name, hostname='*'
),
)
def restore_data_source_dump(
hook_config, config, log_prefix, data_source, dry_run, extract_process, connection_params
hook_config,
config,
log_prefix,
data_source,
dry_run,
extract_process,
connection_params,
borgmatic_runtime_directory,
):
'''
Restore a database from the given extract stream. The database is supplied as a data source
configuration dict, but the given hook configuration is ignored. The given configuration dict is
used to construct the destination path, and the given log prefix is used for 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.
configuration dict, but the given hook configuration is ignored. The given log prefix is used
for 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 ''
hostname = connection_params['hostname'] or data_source.get(
+1 -1
View File
@@ -40,7 +40,7 @@ def ping_monitor(hook_config, config, config_filename, state, monitoring_log_lev
return
hostname = platform.node()
local_timestamp = datetime.datetime.now(datetime.UTC).astimezone().isoformat()
local_timestamp = datetime.datetime.now(datetime.timezone.utc).astimezone().isoformat()
payload = json.dumps(
{
'routing_key': hook_config['integration_key'],
+52 -26
View File
@@ -2,8 +2,10 @@ import csv
import itertools
import logging
import os
import pathlib
import shlex
import borgmatic.config.paths
from borgmatic.execute import (
execute_command,
execute_command_and_capture_output,
@@ -14,13 +16,11 @@ from borgmatic.hooks import dump
logger = logging.getLogger(__name__)
def make_dump_path(config): # pragma: no cover
def make_dump_path(base_directory): # pragma: no cover
'''
Make the dump path from the given configuration dict and the name of this hook.
Given a base directory, make the corresponding dump path.
'''
return dump.make_data_source_dump_path(
config.get('borgmatic_source_directory'), 'postgresql_databases'
)
return dump.make_data_source_dump_path(base_directory, 'postgresql_databases')
def make_extra_environment(database, restore_connection_params=None):
@@ -104,12 +104,12 @@ def use_streaming(databases, config, log_prefix):
return any(database.get('format') != 'directory' for database in databases)
def dump_data_sources(databases, config, log_prefix, dry_run):
def dump_data_sources(databases, config, log_prefix, borgmatic_runtime_directory, dry_run):
'''
Dump the given PostgreSQL databases to a named pipe. The databases are supplied as a sequence of
dicts, one dict describing each database as per the configuration schema. Use the given
configuration dict to construct the destination path and the given log prefix in any log
entries.
borgmatic runtime directory to construct the destination path and the given log prefix in any
log entries.
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.
@@ -123,7 +123,7 @@ def dump_data_sources(databases, config, log_prefix, dry_run):
for database in databases:
extra_environment = make_extra_environment(database)
dump_path = make_dump_path(config)
dump_path = make_dump_path(borgmatic_runtime_directory)
dump_database_names = database_names_to_dump(
database, extra_environment, log_prefix, dry_run
)
@@ -206,33 +206,57 @@ def dump_data_sources(databases, config, log_prefix, dry_run):
return processes
def remove_data_source_dumps(databases, config, log_prefix, dry_run): # pragma: no cover
def remove_data_source_dumps(
databases, config, log_prefix, borgmatic_runtime_directory, dry_run
): # pragma: no cover
'''
Remove all database dump files for this hook regardless of the given databases. Use the given
configuration dict to construct the destination path and the log prefix in any log entries. If
this is a dry run, then don't actually remove anything.
Remove all database dump files for this hook regardless of the given databases. Use the
borgmatic runtime directory to construct the destination path and the log prefix in any log
entries. If this is a dry run, then don't actually remove anything.
'''
dump.remove_data_source_dumps(make_dump_path(config), 'PostgreSQL', log_prefix, dry_run)
dump.remove_data_source_dumps(
make_dump_path(borgmatic_runtime_directory), 'PostgreSQL', log_prefix, dry_run
)
def make_data_source_dump_pattern(databases, config, log_prefix, name=None): # pragma: no cover
def make_data_source_dump_patterns(
databases, config, log_prefix, borgmatic_runtime_directory, name=None
): # pragma: no cover
'''
Given a sequence of configurations dicts, a configuration dict, a prefix to log with, and a
database name to match, return the corresponding glob patterns to match the database dump in an
archive.
Given a sequence of configurations dicts, a configuration dict, a prefix to log with, the
borgmatic runtime directory, and a database name to match, return the corresponding glob
patterns to match the database dump in an archive.
'''
return dump.make_data_source_dump_filename(make_dump_path(config), name, hostname='*')
borgmatic_source_directory = borgmatic.config.paths.get_borgmatic_source_directory(config)
return (
dump.make_data_source_dump_filename(make_dump_path('borgmatic'), name, hostname='*'),
dump.make_data_source_dump_filename(
make_dump_path(borgmatic_runtime_directory), name, hostname='*'
),
dump.make_data_source_dump_filename(
make_dump_path(borgmatic_source_directory), name, hostname='*'
),
)
def restore_data_source_dump(
hook_config, config, log_prefix, data_source, dry_run, extract_process, connection_params
hook_config,
config,
log_prefix,
data_source,
dry_run,
extract_process,
connection_params,
borgmatic_runtime_directory,
):
'''
Restore a database from the given extract stream. The database is supplied as a data source
configuration dict, but the given hook configuration is ignored. The given configuration dict is
used to construct the destination path, and the given log prefix is used for 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.
configuration dict, but the given hook configuration is ignored. The given borgmatic runtime
directory is used to construct the destination path (used for the directory format), and the
given log prefix is used for 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.
If the extract process is None, then restore the dump from the filesystem rather than from an
extract stream.
@@ -253,7 +277,9 @@ def restore_data_source_dump(
all_databases = bool(data_source['name'] == 'all')
dump_filename = dump.make_data_source_dump_filename(
make_dump_path(config), data_source['name'], data_source.get('hostname')
make_dump_path(borgmatic_runtime_directory),
data_source['name'],
data_source.get('hostname'),
)
psql_command = tuple(
shlex.quote(part) for part in shlex.split(data_source.get('psql_command') or 'psql')
@@ -291,7 +317,7 @@ def restore_data_source_dump(
if 'restore_options' in data_source
else ()
)
+ (() if extract_process else (dump_filename,))
+ (() if extract_process else (str(pathlib.Path(dump_filename)),))
+ tuple(
itertools.chain.from_iterable(('--schema', schema) for schema in data_source['schemas'])
if data_source.get('schemas')
+87
View File
@@ -0,0 +1,87 @@
import logging
import requests
logger = logging.getLogger(__name__)
EMERGENCY_PRIORITY = 2
def initialize_monitor(
ping_url, config, config_filename, monitoring_log_level, dry_run
): # pragma: no cover
'''
No initialization is necessary for this monitor.
'''
pass
def ping_monitor(hook_config, config, config_filename, state, monitoring_log_level, dry_run):
'''
Post a message to the configured Pushover application.
If this is a dry run, then don't actually update anything.
'''
run_states = hook_config.get('states', ['fail'])
if state.name.lower() not in run_states:
return
dry_run_label = ' (dry run; not actually updating)' if dry_run else ''
state_config = hook_config.get(state.name.lower(), {})
token = hook_config.get('token')
user = hook_config.get('user')
logger.info(f'{config_filename}: Updating Pushover{dry_run_label}')
if state_config.get('priority') == EMERGENCY_PRIORITY:
if 'expire' not in state_config:
logger.info(f'{config_filename}: Setting expire to default (10 min).')
state_config['expire'] = 600
if 'retry' not in state_config:
logger.info(f'{config_filename}: Setting retry to default (30 sec).')
state_config['retry'] = 30
else:
if 'expire' in state_config or 'retry' in state_config:
raise ValueError(
'The configuration parameters retry and expire should not be set when priority is not equal to 2. Please remove them from the configuration.'
)
state_config = {
key: (int(value) if key == 'html' else value) for key, value in state_config.items()
}
data = dict(
{
'token': token,
'user': user,
# Default to state name. Can be overwritten by state_config below.
'message': state.name.lower(),
},
**state_config,
)
if not dry_run:
logging.getLogger('urllib3').setLevel(logging.ERROR)
try:
response = requests.post(
'https://api.pushover.net/1/messages.json',
headers={'Content-type': 'application/x-www-form-urlencoded'},
data=data,
)
if not response.ok:
response.raise_for_status()
except requests.exceptions.RequestException as error:
logger.warning(f'{config_filename}: Pushover error: {error}')
def destroy_monitor(
ping_url_or_uuid, config, config_filename, monitoring_log_level, dry_run
): # pragma: no cover
'''
No destruction is necessary for this monitor.
'''
pass
+45 -24
View File
@@ -2,19 +2,18 @@ import logging
import os
import shlex
import borgmatic.config.paths
from borgmatic.execute import execute_command, execute_command_with_processes
from borgmatic.hooks import dump
logger = logging.getLogger(__name__)
def make_dump_path(config): # pragma: no cover
def make_dump_path(base_directory): # pragma: no cover
'''
Make the dump path from the given configuration dict and the name of this hook.
Given a base directory, make the corresponding dump path.
'''
return dump.make_data_source_dump_path(
config.get('borgmatic_source_directory'), 'sqlite_databases'
)
return dump.make_data_source_dump_path(base_directory, 'sqlite_databases')
def use_streaming(databases, config, log_prefix):
@@ -25,11 +24,11 @@ def use_streaming(databases, config, log_prefix):
return any(databases)
def dump_data_sources(databases, config, log_prefix, dry_run):
def dump_data_sources(databases, config, log_prefix, borgmatic_runtime_directory, dry_run):
'''
Dump the given SQLite databases to a named pipe. The databases are supplied as a sequence of
configuration dicts, as per the configuration schema. Use the given configuration dict to
construct the destination path and the given log prefix in any log entries.
configuration dicts, as per the configuration schema. Use the given borgmatic runtime directory
to construct the destination path and the given log prefix in any log entries.
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.
@@ -49,7 +48,7 @@ def dump_data_sources(databases, config, log_prefix, dry_run):
f'{log_prefix}: No SQLite database at {database_path}; an empty database will be created and dumped'
)
dump_path = make_dump_path(config)
dump_path = make_dump_path(borgmatic_runtime_directory)
dump_filename = dump.make_data_source_dump_filename(dump_path, database['name'])
if os.path.exists(dump_filename):
@@ -77,33 +76,55 @@ def dump_data_sources(databases, config, log_prefix, dry_run):
return processes
def remove_data_source_dumps(databases, config, log_prefix, dry_run): # pragma: no cover
def remove_data_source_dumps(
databases, config, log_prefix, borgmatic_runtime_directory, dry_run
): # pragma: no cover
'''
Remove the given SQLite database dumps from the filesystem. The databases are supplied as a
sequence of configuration dicts, as per the configuration schema. Use the given configuration
dict to construct the destination path and the given log prefix in any log entries. If this is a
dry run, then don't actually remove anything.
Remove all database dump files for this hook regardless of the given databases. Use the
borgmatic runtime directory to construct the destination path and the log prefix in any log
entries. If this is a dry run, then don't actually remove anything.
'''
dump.remove_data_source_dumps(make_dump_path(config), 'SQLite', log_prefix, dry_run)
dump.remove_data_source_dumps(
make_dump_path(borgmatic_runtime_directory), 'SQLite', log_prefix, dry_run
)
def make_data_source_dump_pattern(databases, config, log_prefix, name=None): # pragma: no cover
def make_data_source_dump_patterns(
databases, config, log_prefix, borgmatic_runtime_directory, name=None
): # pragma: no cover
'''
Make a pattern that matches the given SQLite databases. The databases are supplied as a sequence
of configuration dicts, as per the configuration schema.
Given a sequence of configurations dicts, a configuration dict, a prefix to log with, the
borgmatic runtime directory, and a database name to match, return the corresponding glob
patterns to match the database dump in an archive.
'''
return dump.make_data_source_dump_filename(make_dump_path(config), name)
borgmatic_source_directory = borgmatic.config.paths.get_borgmatic_source_directory(config)
return (
dump.make_data_source_dump_filename(make_dump_path('borgmatic'), name, hostname='*'),
dump.make_data_source_dump_filename(
make_dump_path(borgmatic_runtime_directory), name, hostname='*'
),
dump.make_data_source_dump_filename(
make_dump_path(borgmatic_source_directory), name, hostname='*'
),
)
def restore_data_source_dump(
hook_config, config, log_prefix, data_source, dry_run, extract_process, connection_params
hook_config,
config,
log_prefix,
data_source,
dry_run,
extract_process,
connection_params,
borgmatic_runtime_directory,
):
'''
Restore a database from the given extract stream. The database is supplied as a data source
configuration dict, but the given hook configuration is ignored. The given configuration dict is
used to construct the destination path, and the given log prefix is used for 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.
configuration dict, but the given hook configuration is ignored. The given log prefix is used
for 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 ''
database_path = connection_params['restore_path'] or data_source.get(
+55
View File
@@ -0,0 +1,55 @@
import logging
import requests
logger = logging.getLogger(__name__)
def initialize_monitor(
push_url, config, config_filename, monitoring_log_level, dry_run
): # pragma: no cover
'''
No initialization is necessary for this monitor.
'''
pass
def ping_monitor(hook_config, config, config_filename, state, monitoring_log_level, dry_run):
'''
Make a get request to the configured Uptime Kuma push_url. Use the given configuration filename
in any log entries. If this is a dry run, then don't actually push anything.
'''
run_states = hook_config.get('states', ['start', 'finish', 'fail'])
if state.name.lower() not in run_states:
return
dry_run_label = ' (dry run; not actually pushing)' if dry_run else ''
status = 'down' if state.name.lower() == 'fail' else 'up'
push_url = hook_config.get('push_url', 'https://example.uptime.kuma/api/push/abcd1234')
query = f'status={status}&msg={state.name.lower()}'
logger.info(
f'{config_filename}: Pushing Uptime Kuma push_url {push_url}?{query} {dry_run_label}'
)
logger.debug(f'{config_filename}: Full Uptime Kuma state URL {push_url}?{query}')
if dry_run:
return
logging.getLogger('urllib3').setLevel(logging.ERROR)
try:
response = requests.get(f'{push_url}?{query}')
if not response.ok:
response.raise_for_status()
except requests.exceptions.RequestException as error:
logger.warning(f'{config_filename}: Uptime Kuma error: {error}')
def destroy_monitor(
push_url_or_uuid, config, config_filename, monitoring_log_level, dry_run
): # pragma: no cover
'''
No destruction is necessary for this monitor.
'''
pass
+135
View File
@@ -0,0 +1,135 @@
import logging
import requests
logger = logging.getLogger(__name__)
def initialize_monitor(
ping_url, config, config_filename, monitoring_log_level, dry_run
): # pragma: no cover
'''
No initialization is necessary for this monitor.
'''
pass
def ping_monitor(hook_config, config, config_filename, state, monitoring_log_level, dry_run):
'''
Update the configured Zabbix item using either the itemid, or a host and key.
If this is a dry run, then don't actually update anything.
'''
run_states = hook_config.get('states', ['fail'])
if state.name.lower() not in run_states:
return
dry_run_label = ' (dry run; not actually updating)' if dry_run else ''
state_config = hook_config.get(
state.name.lower(),
{
'value': state.name.lower(),
},
)
server = hook_config.get('server')
username = hook_config.get('username')
password = hook_config.get('password')
api_key = hook_config.get('api_key')
itemid = hook_config.get('itemid')
host = hook_config.get('host')
key = hook_config.get('key')
value = state_config.get('value')
headers = {'Content-Type': 'application/json-rpc'}
logger.info(f'{config_filename}: Updating Zabbix{dry_run_label}')
logger.debug(f'{config_filename}: Using Zabbix URL: {server}')
if server is None:
logger.warning(f'{config_filename}: Server missing for Zabbix')
return
# Determine the Zabbix method used to store the value: itemid or host/key
if itemid is not None:
logger.info(f'{config_filename}: Updating {itemid} on Zabbix')
data = {
'jsonrpc': '2.0',
'method': 'history.push',
'params': {'itemid': itemid, 'value': value},
'id': 1,
}
elif (host and key) is not None:
logger.info(f'{config_filename}: Updating Host:{host} and Key:{key} on Zabbix')
data = {
'jsonrpc': '2.0',
'method': 'history.push',
'params': {'host': host, 'key': key, 'value': value},
'id': 1,
}
elif host is not None:
logger.warning(f'{config_filename}: Key missing for Zabbix')
return
elif key is not None:
logger.warning(f'{config_filename}: Host missing for Zabbix.')
return
else:
logger.warning(f'{config_filename}: No zabbix itemid or host/key provided.')
return
# Determine the authentication method: API key or username/password
if api_key is not None:
logger.info(f'{config_filename}: Using API key auth for Zabbix')
headers['Authorization'] = 'Bearer ' + api_key
elif (username and password) is not None:
logger.info(f'{config_filename}: Using user/pass auth with user {username} for Zabbix')
auth_data = {
'jsonrpc': '2.0',
'method': 'user.login',
'params': {'username': username, 'password': password},
'id': 1,
}
if not dry_run:
logging.getLogger('urllib3').setLevel(logging.ERROR)
try:
response = requests.post(server, headers=headers, json=auth_data)
data['auth'] = response.json().get('result')
if not response.ok:
response.raise_for_status()
except requests.exceptions.RequestException as error:
logger.warning(f'{config_filename}: Zabbix error: {error}')
return
elif username is not None:
logger.warning(f'{config_filename}: Password missing for Zabbix authentication')
return
elif password is not None:
logger.warning(f'{config_filename}: Username missing for Zabbix authentication')
return
else:
logger.warning(f'{config_filename}: Authentication data missing for Zabbix')
return
if not dry_run:
logging.getLogger('urllib3').setLevel(logging.ERROR)
try:
response = requests.post(server, headers=headers, json=data)
if not response.ok:
response.raise_for_status()
except requests.exceptions.RequestException as error:
logger.warning(f'{config_filename}: Zabbix error: {error}')
def destroy_monitor(
ping_url_or_uuid, config, config_filename, monitoring_log_level, dry_run
): # pragma: no cover
'''
No destruction is necessary for this monitor.
'''
pass
+1 -1
View File
@@ -38,7 +38,7 @@ def should_do_markup(no_color, configs):
if no_color:
return False
if any(config.get('output', {}).get('color') is False for config in configs.values()):
if any(config.get('color', True) is False for config in configs.values()):
return False
if os.environ.get('NO_COLOR', None):
+7 -5
View File
@@ -1,14 +1,15 @@
FROM docker.io/alpine:3.17.1 as borgmatic
FROM docker.io/alpine:3.20.1 AS borgmatic
COPY . /app
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 --break-system-packages --no-cache /app && generate-borgmatic-config && chmod +r /etc/borgmatic/config.yaml
RUN borgmatic --help > /command-line.txt \
&& for action in rcreate transfer create prune compact check extract config "config bootstrap" "config generate" "config validate" export-tar mount umount restore rlist list rinfo info break-lock borg; do \
&& for action in repo-create transfer create prune compact check delete extract config "config bootstrap" "config generate" "config validate" export-tar mount umount repo-delete restore repo-list list repo-info info break-lock "key export" "key change-passphrase" borg; do \
echo -e "\n--------------------------------------------------------------------------------\n" >> /command-line.txt \
&& borgmatic $action --help >> /command-line.txt; done
RUN /app/docs/fetch-contributors >> /contributors.html
FROM docker.io/node:19.5.0-alpine as html
FROM docker.io/node:22.4.0-alpine AS html
ARG ENVIRONMENT=production
@@ -24,11 +25,12 @@ RUN npm install @11ty/eleventy \
markdown-it-replace-link
COPY --from=borgmatic /etc/borgmatic/config.yaml /source/docs/_includes/borgmatic/config.yaml
COPY --from=borgmatic /command-line.txt /source/docs/_includes/borgmatic/command-line.txt
COPY --from=borgmatic /contributors.html /source/docs/_includes/borgmatic/contributors.html
COPY . /source
RUN NODE_ENV=${ENVIRONMENT} npx eleventy --input=/source/docs --output=/output/docs \
&& mv /output/docs/index.html /output/index.html
FROM docker.io/nginx:1.22.1-alpine
FROM docker.io/nginx:1.26.1-alpine
COPY --from=html /output /usr/share/nginx/html
COPY --from=borgmatic /etc/borgmatic/config.yaml /usr/share/nginx/html/docs/reference/config.yaml
+4 -5
View File
@@ -167,12 +167,11 @@ li {
padding: .25em 0;
}
li ul {
margin: .5em 0;
padding-left: 1em;
list-style-type: disc;
padding-left: 2em;
}
li li {
padding-top: .1em;
padding-bottom: .1em;
li li:last-child {
padding-bottom: 0em;
}
/* Syntax highlighting and Code blocks */
+1
View File
@@ -3,6 +3,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="generator" content="{{ eleventy.generator }}">
<link rel="icon" href="docs/static/borgmatic.png" type="image/x-icon">
<title>{{ subtitle + ' - ' if subtitle}}{{ title }}</title>
{%- set css %}
+2 -2
View File
@@ -1,7 +1,7 @@
services:
docs:
image: borgmatic-docs
container_name: docs
container_name: borgmatic-docs
ports:
- 8080:80
build:
@@ -11,7 +11,7 @@ services:
ENVIRONMENT: development
message:
image: alpine
container_name: message
container_name: borgmatic-docs-message
command:
- sh
- -c
+68
View File
@@ -0,0 +1,68 @@
#!/usr/bin/python
'''
A script to fetch recent contributors to borgmatic, used during documentation generation.
'''
import datetime
import itertools
import operator
import subprocess
import requests
def list_merged_pulls(url):
'''
Given a Gitea or GitHub API endpoint URL for pull requests, fetch and return the corresponding
JSON for all such merged pull requests.
'''
response = requests.get(f'{url}?state=closed', headers={'Accept': 'application/json', 'Content-Type': 'application/json'})
if not response.ok:
response.raise_for_status()
return tuple(pull for pull in response.json() if pull.get('merged_at'))
API_ENDPOINT_URLS = (
'https://projects.torsion.org/api/v1/repos/borgmatic-collective/borgmatic/pulls',
'https://api.github.com/repos/borgmatic-collective/borgmatic/pulls',
)
RECENT_CONTRIBUTORS_CUTOFF_DAYS = 365
def print_contributors():
'''
Display the recent contributors as a row of avatars in an HTML fragment.
'''
pulls = tuple(itertools.chain.from_iterable(list_merged_pulls(url) for url in API_ENDPOINT_URLS))
seen_user_ids = set()
print('<p>')
for pull in sorted(pulls, key=operator.itemgetter('merged_at'), reverse=True):
merged_at = pull.get('merged_at')
user = pull.get('user')
if not merged_at or not user:
continue
user_id = user.get('id')
if not user_id or user_id in seen_user_ids:
continue
if datetime.datetime.fromisoformat(merged_at) < datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(days=RECENT_CONTRIBUTORS_CUTOFF_DAYS):
continue
seen_user_ids.add(user_id)
print(
f'''<a href="{user.get('html_url')}?tab=activity"><img src="{user.get('avatar_url')}" width="50" height="50" title="{user.get('full_name') or user.get('login')}" /></a>'''
)
print('</p>')
if __name__ == '__main__':
print_contributors()
@@ -34,9 +34,14 @@ test in the form of a borgmatic hook to see if backups should proceed or not.
The way the test works is that if any of your hook commands return a special
exit status of 75, that indicates to borgmatic that it's a temporary failure,
and borgmatic should skip all subsequent actions for that configuration file.
If you return any other status, then it's a standard success or error. (Zero is
success; anything else other than 75 is an error).
and borgmatic should skip all subsequent actions for the current repository.
<span class="minilink minilink-addedin">Prior to version 1.9.0</span> Soft
failures skipped subsequent actions for *all* repositories in the
configuration file, rather than just for the current repository.
If you return any status besides 75, then it's a standard success or error.
(Zero is success; anything else other than 75 is an error).
So for instance, if you have an external drive that's only sometimes mounted,
declare its repository in its own [separate configuration
@@ -71,9 +76,15 @@ option in the `hooks:` section of your configuration.
What this does is check if the `findmnt` command errors when probing for a
particular mount point. If it does error, then it returns exit code 75 to
borgmatic. borgmatic logs the soft failure, skips all further actions in that
configurable file, and proceeds onward to any other borgmatic configuration
files you may have.
borgmatic. borgmatic logs the soft failure, skips all further actions for the
current repository, and proceeds onward to any other repositories and/or
configuration files you may have.
If you'd prefer not to use a separate configuration file, and you'd rather
have multiple repositories in a single configuration file, you can make your
`before_backup` soft failure test [vary by
repository](https://torsion.org/borgmatic/docs/how-to/add-preparation-and-cleanup-steps-to-backups/#variable-interpolation).
That might require calling out to a separate script though.
Note that `before_backup` only runs on the `create` action. See below about
optionally using `before_actions` instead.
@@ -121,13 +132,16 @@ There are some caveats you should be aware of with this feature.
executing. So, like a standard error, it is an "early out". Unlike a standard
error, borgmatic does not display it in angry red text or consider it a
failure.
* The soft failure only applies to the scope of a single borgmatic
configuration file. So put anything that you don't want soft-failed, like
always-online cloud backups, in separate configuration files from your
soft-failing repositories.
* The soft failure doesn't have to apply to a repository. You can even perform
a test to make sure that individual source directories are mounted and
available. Use your imagination!
* Any given soft failure only applies to the a single borgmatic repository
(as of borgmatic 1.9.0). So if you have other repositories you don't want
soft-failed, then make your soft fail test [vary by
repository](https://torsion.org/borgmatic/docs/how-to/add-preparation-and-cleanup-steps-to-backups/#variable-interpolation)—or
put anything that you don't want soft-failed (like always-online cloud
backups) in separate configuration files from your soft-failing
repositories.
* The soft failure doesn't have to test anything related to a repository. You
can even perform a test to make sure that individual source directories are
mounted and available. Use your imagination!
* The soft failure feature also works for before/after hooks for other
actions as well. But it is not implemented for `before_everything` or
`after_everything`.
+73 -29
View File
@@ -63,14 +63,9 @@ dump formats, which can't stream and therefore do consume temporary disk
space. Additionally, prior to borgmatic 1.5.3, all database dumps consumed
temporary disk space.)
To support this, borgmatic creates temporary named pipes in `~/.borgmatic` by
default. To customize this path, set the `borgmatic_source_directory` option
in borgmatic's configuration.
Also note that using a database hook implicitly enables both the
`read_special` and `one_file_system` configuration settings (even if they're
disabled in your configuration) to support this dump and restore streaming.
See Limitations below for more on this.
Also note that using a database hook implicitly enables the `read_special`
configuration option (even if it's disabled in your configuration) to support
this dump and restore streaming. See Limitations below for more on this.
Here's a more involved example that connects to remote databases:
@@ -118,6 +113,40 @@ additional customization of the options passed to database commands (when
listing databases, restoring databases, etc.).
### Runtime directory
<span class="minilink minilink-addedin">New in version 1.9.0</span> To support
streaming database dumps to Borg, borgmatic uses a runtime directory for
temporary file storage, probing the following locations (in order) to find it:
1. The `user_runtime_directory` borgmatic configuration option.
2. The `XDG_RUNTIME_DIR` environment variable, usually `/run/user/$UID`
(where `$UID` is the current user's ID), automatically set by PAM on Linux
for a user with a session.
3. <span class="minilink minilink-addedin">New in version 1.9.2</span>The
`RUNTIME_DIRECTORY` environment variable, set by systemd if
`RuntimeDirectory=borgmatic` is added to borgmatic's systemd service file.
4. <span class="minilink minilink-addedin">New in version 1.9.1</span>The
`TMPDIR` environment variable, set on macOS for a user with a session,
among other operating systems.
5. <span class="minilink minilink-addedin">New in version 1.9.1</span>The
`TEMP` environment variable, set on various systems.
6. <span class="minilink minilink-addedin">New in version 1.9.2</span>
Hard-coded `/tmp`. <span class="minilink minilink-addedin">Prior to
version 1.9.2</span>This was instead hard-coded to `/run/user/$UID`.
Regardless of the runtime directory selected, borgmatic stores its files
within a `borgmatic` subdirectory of the runtime directory. Additionally, in
the case of `TMPDIR`, `TEMP`, and the hard-coded `/tmp`, borgmatic creates a
randomly named subdirectory in an effort to reduce path collisions in shared
system temporary directories.
<span class="minilink minilink-addedin">Prior to version 1.9.0</span>
borgmatic created temporary streaming database dumps within the `~/.borgmatic`
directory by default. At that time, the path was configurable by the
`borgmatic_source_directory` configuration option (now deprecated).
### All databases
If you want to dump all databases on a host, use `all` for the database name:
@@ -211,8 +240,8 @@ Normally, borgmatic dumps a database by running a database dump command (e.g.
connects to your containerized database via the given `hostname` and `port`.
But if you don't have any database dump commands installed on your host and
you'd rather use the commands inside your database container itself, borgmatic
supports that too. Just configure borgmatic to `exec` into your container to
run the dump command.
supports that too. For that, configure borgmatic to `exec` into your container
to run the dump command.
For instance, if using Docker and PostgreSQL, something like this might work:
@@ -229,7 +258,20 @@ hooks:
... where `my_pg_container` is the name of your database container. In this
example, you'd also need to set the `pg_restore_command` and `psql_command`
options.
options. If you choose to use the `pg_dump` command within the container
though, note that it will output the database dump to a file inside the
container. So you'll have to mount the `.borgmatic` folder from your host's
home folder into the container using the same directory structure.
See the following Docker compose file an as example:
```yaml
services:
db:
image: postgres
volumes:
- /home/USERNAME/.borgmatic:/home/USERNAME/.borgmatic
```
Similar command override options are available for (some of) the other
supported database types as well. See the [configuration
@@ -298,13 +340,14 @@ borgmatic does not currently create a database upon restore.
To restore a database dump from an archive, use the `borgmatic restore`
action. But the first step is to figure out which archive to restore from. A
good way to do that is to use the `rlist` action:
good way to do that is to use the `repo-list` action:
```bash
borgmatic rlist
borgmatic repo-list
```
(No borgmatic `rlist` action? Try `list` instead or upgrade borgmatic!)
(No borgmatic `repo-list` action? Try `rlist` or `list` instead or upgrade
borgmatic!)
That should yield output looking something like:
@@ -322,15 +365,15 @@ borgmatic restore --archive host-2023-01-02T04:06:07.080910
(No borgmatic `restore` action? Upgrade borgmatic!)
With newer versions of borgmatic, you can simplify this to:
Or you can simplify this to:
```bash
borgmatic restore --archive latest
```
The `--archive` value is the name of the archive to restore from. This
restores all databases dumps that borgmatic originally backed up to that
archive.
The `--archive` value is the name of the archive or archive hash to restore
from. This restores all databases dumps that borgmatic originally backed up to
that archive.
This is a destructive action! `borgmatic restore` replaces live databases by
restoring dumps from the selected archive. So be very careful when and where
@@ -445,20 +488,21 @@ special files (and symlinks to them) that Borg may get stuck on. Even so,
there are still potential edge cases in which applications on your system
create new special files *after* borgmatic constructs its exclude list,
resulting in Borg hangs. If that occurs, you can resort to manually excluding
those files. And if you explicitly set the `read-special` option to `true`,
those files. And if you explicitly set the `read_special` option to `true`,
borgmatic will opt you out of the auto-exclude feature entirely, but will
still instruct Borg to consume special files—you will just be on your own to
still instruct Borg to consume special files—and you will be on your own to
exclude them. <span class="minilink minilink-addedin">Prior to version
1.7.3</span>Special files were not auto-excluded, and you were responsible for
excluding them yourself. Common directories to exclude are `/dev` and `/run`,
but that may not be exhaustive.
5. Database hooks also implicitly enable the `one_file_system` option, which
means Borg won't cross filesystem boundaries when looking for files to backup.
This is especially important when running borgmatic in a container, as
container volumes are mounted as separate filesystems. One work-around is to
explicitly add each mounted volume you'd like to backup to
5. <span class="minilink minilink-addedin">Prior to version 1.9.0</span>
Database hooks also implicitly enabled the `one_file_system` option, which
meant Borg wouldn't cross filesystem boundaries when looking for files to
backup. When borgmatic was running in a container, this often required a
work-around to explicitly add each mounted backup volume to
`source_directories` instead of relying on Borg to include them implicitly via
a parent directory.
a parent directory. But as of borgmatic 1.9.0, `one_file_system` is no longer
auto-enabled and such work-arounds aren't necessary.
### Manual restoration
@@ -498,9 +542,9 @@ With PostgreSQL, MariaDB, and MySQL, if you're getting authentication errors
when borgmatic tries to connect to your database, a natural reaction is to
increase your borgmatic verbosity with `--verbosity 2` and go looking in the
logs. You'll notice though that your database password does not show up in the
logs. This is likely not the cause of the authentication problem unless you
mistyped your password, however; borgmatic passes your password to the
database via an environment variable that does not appear in the logs.
logs. But this is likely not the cause of the authentication problem unless
you mistyped your password; borgmatic passes your password to the database via
an environment variable that does not appear in the logs.
The cause of an authentication error is often on the database side—in the
configuration of which users are allowed to connect and how they are
+1 -1
View File
@@ -48,7 +48,7 @@ support for [more granular exit
codes](https://borgbackup.readthedocs.io/en/1.4-maint/usage/general.html#return-codes)
means that you can configure borgmatic to respond to specific Borg conditions.
See the full list of [Borg 1.4 error and warning exit
codes](https://borgbackup.readthedocs.io/en/1.4.0b1/internals/frontends.html#message-ids).
codes](https://borgbackup.readthedocs.io/en/stable/internals/frontends.html#message-ids).
The `rc:` numeric value there tells you the exit code for each.
For instance, this borgmatic configuration elevates all Borg backup file
+77 -10
View File
@@ -108,8 +108,8 @@ for more information.
The various consistency checks all have trade-offs around speed and
thoroughness, but most of them don't even look at your original source
files—arguably one important way to ensure your backups contain the files
you'll want to restore in the case of catastrophe (or just an accidentally
deleted file). Because if something goes wrong with your source files, most
you'll want to restore in the case of catastrophe (or an accidentally deleted
file). Because if something goes wrong with your source files, most
consistency checks will still pass with flying colors and you won't discover
there's a problem until you go to restore.
@@ -119,10 +119,10 @@ check comes in. This check actually compares your source file counts and data
against those in the latest archive, potentially catching problems like
incorrect excludes, inadvertent deletes, files changed by malware, etc.
However, because an exhaustive comparison of all source files against the
latest archive might be too slow, the spot check supports *sampling* a
percentage of your source files for the comparison, ensuring they fall within
configured tolerances.
But because an exhaustive comparison of all source files against the latest
archive might be too slow, the spot check supports *sampling* a percentage of
your source files for the comparison, ensuring they fall within configured
tolerances.
Here's how it works. Start by installing the `xxhash` OS package if you don't
already have it, so the spot check can run the `xxh64sum` command and
@@ -225,15 +225,31 @@ repository, at most once a month.
Unlike a real scheduler like cron, borgmatic only makes a best effort to run
checks on the configured frequency. It compares that frequency with how long
it's been since the last check for a given repository (as recorded in a file
within `~/.borgmatic/checks`). If it hasn't been long enough, the check is
skipped. And you still have to run `borgmatic check` (or `borgmatic` without
actions) in order for checks to run, even when a `frequency` is configured!
it's been since the last check for a given repository If it hasn't been long
enough, the check is skipped. And you still have to run `borgmatic check` (or
`borgmatic` without actions) in order for checks to run, even when a
`frequency` is configured!
This also applies *across* configuration files that have the same repository
configured. Make sure you have the same check frequency configured in each
though—or the most frequently configured check will apply.
<span class="minilink minilink-addedin">New in version 1.9.0</span>To support
this frequency logic, borgmatic records check timestamps within the
`~/.local/state/borgmatic/checks` directory. To override the `~/.local/state`
portion of this path, set the `user_state_directory` configuration option.
Alternatively, set the `XDG_STATE_HOME` environment variable.
<span class="minilink minilink-addedin">New in version 1.9.2</span>The
`STATE_DIRECTORY` environment variable also works for this purpose. It's set
by systemd if `StateDirectory=borgmatic` is added to borgmatic's systemd
service file.
<span class="minilink minilink-addedin">Prior to version 1.9.0</span>
borgmatic recorded check timestamps within the `~/.borgmatic` directory. At
that time, the path was configurable by the `borgmatic_source_directory`
configuration option (now deprecated).
If you want to temporarily ignore your configured frequencies, you can invoke
`borgmatic check --force` to run checks unconditionally.
@@ -242,6 +258,57 @@ check --force` runs `check` even if it's specified in the `skip_actions`
option.
### Check days
<span class="minilink minilink-addedin">New in version 1.8.13</span> You can
optionally configure checks to only run on particular days of the week. For
instance:
```yaml
checks:
- name: repository
only_run_on:
- Saturday
- Sunday
- name: archives
only_run_on:
- weekday
- name: spot
only_run_on:
- Friday
- weekend
```
Each day of the week is specified in the current locale (system
language/country settings). `weekend` and `weekday` are also accepted.
As with `frequency`, borgmatic only makes a best effort to run checks on the
given day of the week. For instance, if you run `borgmatic check` daily, then
every day borgmatic will have an opportunity to determine whether your checks
are configured to run on that day. If they are, then the checks run. If not,
they are skipped.
For instance, with the above configuration, if borgmatic is run on a Saturday,
the `repository` check will run. But on a Monday? The repository check will
get skipped. And if borgmatic is never run on a Saturday or a Sunday, that
check will never get a chance to run.
Also, the day of the week configuration applies *after* any configured
`frequency` for a check. So for instance, imagine the following configuration:
```yaml
checks:
- name: repository
frequency: 2 weeks
only_run_on:
- Monday
```
If you run borgmatic daily with that configuration, then borgmatic will first
wait two weeks after the previous check before running the check again—on the
first Monday after the `frequency` duration elapses.
### Running only checks
<span class="minilink minilink-addedin">New in version 1.7.1</span> If you
+16 -7
View File
@@ -9,7 +9,7 @@ eleventyNavigation:
To get set up to develop on borgmatic, first [`install
pipx`](https://torsion.org/borgmatic/docs/how-to/set-up-backups/#installation)
to make managing your borgmatic environment easy without impacting other
to make managing your borgmatic environment easier without impacting other
Python applications on your system.
Then, clone borgmatic via HTTPS or SSH:
@@ -102,9 +102,9 @@ and depend on containers for runtime dependencies. These tests do run on the
continuous integration (CI) server, and running them on your developer machine
is the closest thing to dev-CI parity.
If you would like to run the full test suite, first install Docker (or Podman;
see below) and [Docker Compose](https://docs.docker.com/compose/install/).
Then run:
If you would like to run the end-to-end tests, first install Docker (or
Podman; see below) and [Docker
Compose](https://docs.docker.com/compose/install/). Then run:
```bash
scripts/run-end-to-end-tests
@@ -152,12 +152,21 @@ the following deviations from it:
* In general, spell out words in variable names instead of shortening them.
So, think `index` instead of `idx`. There are some notable exceptions to
this though (like `config`).
* Favor blank lines around logical code groupings, `if` statements,
`return`s, etc. Readability is more important than packing code tightly.
* Import fully qualified Python modules instead of importing individual
functions, classes, or constants. E.g., do `import os.path` instead of
`from os import path`. (Some exceptions to this are made in tests.)
* Only use classes and OOP as a last resort, such as when integrating with
Python libraries that require it.
* Prefer functional code where it makes sense, e.g. when constructing a
command (to subsequently execute imperatively).
borgmatic code uses the [Black](https://black.readthedocs.io/en/stable/) code
borgmatic uses the [Black](https://black.readthedocs.io/en/stable/) code
formatter, the [Flake8](http://flake8.pycqa.org/en/latest/) code checker, and
the [isort](https://github.com/timothycrosley/isort) import orderer, so
certain code style requirements will be enforced when running automated tests.
See the Black, Flake8, and isort documentation for more information.
certain code style requirements are enforced when running automated tests. See
the Black, Flake8, and isort documentation for more information.
## Continuous integration
+29 -13
View File
@@ -9,13 +9,14 @@ eleventyNavigation:
When the worst happens—or you want to test your backups—the first step is
to figure out which archive to extract. A good way to do that is to use the
`rlist` action:
`repo-list` action:
```bash
borgmatic rlist
borgmatic repo-list
```
(No borgmatic `rlist` action? Try `list` instead or upgrade borgmatic!)
(No borgmatic `repo-list` action? Try `rlist` or `list` instead or upgrade
borgmatic!)
That should yield output looking something like:
@@ -39,10 +40,10 @@ Or simplify this to:
borgmatic extract --archive latest
```
The `--archive` value is the name of the archive to extract. This extracts the
entire contents of the archive to the current directory, so make sure you're
in the right place before running the command—or see below about the
`--destination` flag.
The `--archive` value is the name of the archive or archive hash to extract.
This extracts the entire contents of the archive to the current directory, so
make sure you're in the right place before running the command—or see below
about the `--destination` flag.
## Repository selection
@@ -130,6 +131,14 @@ Or use the "latest" value for the archive to mount the latest archive:
borgmatic mount --archive latest --mount-point /mnt
```
<span class="minilink minilink-addedin">New in borgmatic version 1.9.0 with
Borg version 2.x</span>You can provide a series name for the `--archive` value
to mount multiple archives in that series:
```bash
borgmatic mount --archive seriesname --mount-point /mnt
```
If you'd like to restrict the mounted filesystem to only particular paths from
your archive, use the `--path` flag, similar to the `extract` action above.
For instance:
@@ -147,12 +156,19 @@ borgmatic umount --mount-point /mnt
## Extract the configuration files used to create an archive
<span class="minilink minilink-addedin">New in version 1.7.15</span> borgmatic
automatically stores all the configuration files used to create an archive
inside the archive itself. They are stored in the archive using their full
paths from the machine being backed up. This is useful in cases where you've
lost a configuration file or you want to see what configurations were used to
create a particular archive.
<span class="minilink minilink-addedin">New in version 1.7.15</span> As part
of creating a backup archive, borgmatic automatically includes all of the
configuration files used when creating it, storing them inside the archive
itself with their full paths from the machine being backed up. This is useful
in cases where you've lost a configuration file or you want to see what
configurations were used to create a particular archive.
To support this, borgmatic creates a manifest file that records the paths of
all the borgmatic configuration files stored within an archive. The file gets
written to borgmatic's runtime directory on disk and then stored within the
archive. See the [runtime directory
documentation](https://torsion.org/borgmatic/docs/how-to/backup-your-databases/#runtime-directory)
for how and where that happens.
To extract the configuration files from an archive, use the `config bootstrap`
action. For example:
+22 -6
View File
@@ -62,13 +62,13 @@ for available values.
(No borgmatic `list` or `info` actions? Upgrade borgmatic!)
<span class="minilink minilink-addedin">New in borgmatic version 1.7.0</span>
There are also `rlist` and `rinfo` actions for displaying repository
information with Borg 2.x:
<span class="minilink minilink-addedin">New in version 1.9.0</span> There are
also `repo-list` and `repo-info` actions for displaying repository information
with Borg 2.x:
```bash
borgmatic rlist
borgmatic rinfo
borgmatic repo-list
borgmatic repo-info
```
See the [borgmatic command-line
@@ -107,12 +107,28 @@ 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
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.
<span class="minilink minilink-addedin">New in borgmatic version
1.9.0</span>Database dump files are stored at `/borgmatic` within a backup
archive, regardless of the user who performs the backup. (Note that Borg
doesn't store the leading `/`.)
<span class="minilink minilink-addedin">With Borg version 1.2 and
earlier</span>Database dump files are stored at a path dependent on the
[runtime
directory](https://torsion.org/borgmatic/docs/how-to/backup-your-databases/#runtime-directory)
in use at the time the archive was created.
<span class="minilink minilink-addedin">Prior to borgmatic version
1.9.0</span>Database dump files were instead stored at `~/.borgmatic` within
the backup archive (where `~` was expanded to the home directory of the user
who performed the backup). This applied with all versions of Borg.
## Logging
+18 -10
View File
@@ -82,20 +82,25 @@ this option in the `storage:` section of your configuration.
This example means that when borgmatic creates an archive, its name will start
with the string `home-directories-` and end with a timestamp for its creation
time. If `archive_name_format` is unspecified, the default is
time. If `archive_name_format` is unspecified, the default with Borg 1 is
`{hostname}-{now:%Y-%m-%dT%H:%M:%S.%f}`, meaning your system hostname plus a
timestamp in a particular format.
<span class="minilink minilink-addedin">New in borgmatic version 1.9.0 with
Borg version 2.x</span>The default is just `{hostname}`, as Borg 2 does not
require unique archive names; identical archive names form a common "series"
that can be targeted together.
### Archive filtering
<span class="minilink minilink-addedin">New in version 1.7.11</span> borgmatic
uses the `archive_name_format` option to automatically limit which archives
get used for actions operating on multiple archives. This prevents, for
instance, duplicate archives from showing up in `rlist` or `info` results—even
if the same repository appears in multiple borgmatic configuration files. To
take advantage of this feature, use a different `archive_name_format` in each
configuration file.
instance, duplicate archives from showing up in `repo-list` or `info`
results—even if the same repository appears in multiple borgmatic
configuration files. To take advantage of this feature, use a different
`archive_name_format` in each configuration file.
Under the hood, borgmatic accomplishes this by substituting globs for certain
ephemeral data placeholders in your `archive_name_format`—and using the result
@@ -113,7 +118,7 @@ this option in the `storage:` section of your configuration.
borgmatic considers `{now}` an emphemeral data placeholder that will probably
change per archive, while `{hostname}` won't. So it turns the example value
into `{hostname}-user-data-*` and applies it to filter down the set of
archives used for actions like `rlist`, `info`, `prune`, `check`, etc.
archives used for actions like `repo-list`, `info`, `prune`, `check`, etc.
The end result is that when borgmatic runs the actions for a particular
application-specific configuration file, it only operates on the archives
@@ -129,11 +134,14 @@ archive_name_format: {hostname}-user-data-{now}
match_archives: sh:myhost-user-data-*
```
For Borg 1.x, use a shell pattern for the `match_archives` value and see the
[Borg patterns
<span class="minilink minilink-addedin">With Borg version 1.x</span>Use a shell
pattern for the `match_archives` value and see the [Borg patterns
documentation](https://borgbackup.readthedocs.io/en/stable/usage/help.html#borg-help-patterns)
for more information. For Borg 2.x, see the [match archives
documentation](https://borgbackup.readthedocs.io/en/2.0.0b5/usage/help.html#borg-help-match-archives).
for more information.
<span class="minilink minilink-addedin">With Borg version 2.x</span>See the
[match archives
documentation](https://borgbackup.readthedocs.io/en/2.0.0b13/usage/help.html#borg-help-match-archives).
Some borgmatic command-line actions also have a `--match-archives` flag that
overrides both the auto-matching behavior and the `match_archives`
+195 -9
View File
@@ -39,13 +39,16 @@ below for how to configure this.
borgmatic integrates with these monitoring services and libraries, pinging
them as backups happen:
* [Healthchecks](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#healthchecks-hook)
* [Cronitor](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#cronitor-hook)
* [Cronhub](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#cronhub-hook)
* [PagerDuty](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#pagerduty-hook)
* [ntfy](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#ntfy-hook)
* [Grafana Loki](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#loki-hook)
* [Apprise](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#apprise-hook)
* [Cronhub](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#cronhub-hook)
* [Cronitor](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#cronitor-hook)
* [Grafana Loki](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#loki-hook)
* [Healthchecks](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#healthchecks-hook)
* [ntfy](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#ntfy-hook)
* [PagerDuty](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#pagerduty-hook)
* [Pushover](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#pushover-hook)
* [Uptime Kuma](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#uptime-kuma-hook)
* [Zabbix](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#zabbix-hook)
The idea is that you'll receive an alert when something goes wrong or when the
service doesn't hear from borgmatic for a configured interval (if supported).
@@ -288,6 +291,76 @@ If you have any issues with the integration, [please contact
us](https://torsion.org/borgmatic/#support-and-contributing).
## Pushover hook
<span class="minilink minilink-addedin">New in version 1.9.2</span>
[Pushover](https://pushover.net) makes it easy to get real-time notifications
on your Android, iPhone, iPad, and Desktop (Android Wear and Apple Watch,
too!).
First, create a Pushover account and login on your mobile device. Create an
Application in your Pushover dashboard.
Then, configure borgmatic with your user's unique "User Key" found in your
Pushover dashboard and the unique "API Token" from the created Application.
Here's a basic example:
```yaml
pushover:
token: 7ms6TXHpTokTou2P6x4SodDeentHRa
user: hwRwoWsXMBWwgrSecfa9EfPey55WSN
```
With this configuration, borgmatic creates a Pushover event for your service
whenever borgmatic fails, but only when any of the `create`, `prune`, `compact`,
or `check` actions are run. Note that borgmatic does not contact Pushover
when a backup starts or when it ends without error by default.
You can configure Pushover to have custom parameters declared for borgmatic's
`start`, `fail` and `finish` hooks states.
Here's a more advanced example:
```yaml
pushover:
token: 7ms6TXHpTokTou2P6x4SodDeentHRa
user: hwRwoWsXMBWwgrSecfa9EfPey55WSN
start:
message: "Backup <b>Started</b>"
priority: -2
title: "Backup Started"
html: True
ttl: 10 # Message will be deleted after 10 seconds.
fail:
message: "Backup <font color='#ff6961'>Failed</font>"
priority: 2 # Requests acknowledgement for messages.
expire: 600 # Used only for priority 2. Default is 600 seconds.
retry: 30 # Used only for priority 2. Default is 30 seconds.
device: "pixel8"
title: "Backup Failed"
html: True
sound: "siren"
url: "https://ticketing-system.example.com/login"
url_title: "Login to ticketing system"
finish:
message: "Backup <font color='#77dd77'>Finished</font>"
priority: 0
title: "Backup Finished"
html: True
ttl: 60
url: "https://ticketing-system.example.com/login"
url_title: "Login to ticketing system"
states:
- start
- finish
- fail
```
## ntfy hook
<span class="minilink minilink-addedin">New in version 1.6.3</span>
@@ -505,12 +578,125 @@ See the [configuration
reference](https://torsion.org/borgmatic/docs/reference/configuration/) for
details.
## Uptime Kuma hook
<span class="minilink minilink-addedin">New in version 1.8.13</span> [Uptime
Kuma](https://uptime.kuma.pet) is a self-hosted monitoring tool and can
provide a Push monitor type to accept HTTP `GET` requests from a service
instead of contacting it directly.
Uptime Kuma allows you to see a history of monitor states and can in turn
alert via ntfy, Gotify, Matrix, Apprise, Email, and many more.
An example configuration is shown here with all the available options:
```yaml
uptime_kuma:
push_url: https://kuma.my-domain.com/api/push/abcd1234
states:
- start
- finish
- fail
```
The `push_url` is provided to your from your Uptime Kuma service and
originally includes a query string—the text including and after the question
mark (`?`). But please do not include the query string in the `push_url`
configuration; borgmatic will add this automatically depending on the state of
your backup.
Using `start`, `finish` and `fail` states means you will get two "up beats" in
Uptime Kuma for successful backups and the ability to see failures if and when
the backup started (was there a `start` beat?).
A reasonable base-level configuration for an Uptime Kuma Monitor for a backup
is below:
```ini
# These are to be entered into Uptime Kuma and not into your borgmatic
# configuration.
# Push monitors wait for the client to contact Uptime Kuma instead of Uptime
# Kuma contacting the client. This is perfect for backup monitoring.
Monitor Type = Push
Heartbeat Interval = 90000 # = 25 hours = 1 day + 1 hour
# Wait 6 times the Heartbeat Retry (below) before logging a heartbeat missed.
Retries = 6
# Multiplied by Retries this gives a grace period within which the monitor
# goes into the "Pending" state.
Heartbeat Retry = 360 # = 10 minutes
# For each Heartbeat Interval if the backup fails repeatedly, a notification
# is sent each time.
Resend Notification every X times = 1
```
## Zabbix hook
<span class="minilink minilink-addedin">New in version 1.9.0</span>
[Zabbix](https://www.zabbix.com/) is an open-source monitoring tool used for
tracking and managing the performance and availability of networks, servers,
and applications in real-time.
This hook does not do any notifications on its own. Instead, it relies on your
Zabbix instance to notify and perform escalations based on the Zabbix
configuration. The `states` defined in the configuration determine which
states will trigger the hook. The value defined in the configuration of each
state is used to populate the data of the configured Zabbix item. If none are
provided, it defaults to a lower-case string of the state.
An example configuration is shown here with all the available options.
```yaml
zabbix:
server: http://cloud.zabbix.com/zabbix/api_jsonrpc.php
username: myuser
password: secret
api_key: b2ecba64d8beb47fc161ae48b164cfd7104a79e8e48e6074ef5b141d8a0aeeca
host: "borg-server"
key: borg.status
itemid: 55105
start:
value: "STARTED"
finish:
value: "OK"
fail:
value: "ERROR"
states:
- start
- finish
- fail
```
This hook requires the Zabbix server be running version 7.0+
### Authentication methods
Authentication can be accomplished via `api_key` or both `username` and
`password`. If all three are declared, only `api_key` is used.
### Items
The item to be updated can be chosen by either declaring the `itemid` or both
`host` and `key`. If all three are declared, only `itemid` is used.
Keep in mind that `host` is referring to the "Host name" on the Zabbix server
and not the "Visual name".
## Scripting borgmatic
To consume the output of borgmatic in other software, you can include an
optional `--json` flag with `create`, `rlist`, `rinfo`, or `info` to get the
output formatted as JSON.
optional `--json` flag with `create`, `repo-list`, `repo-info`, or `info` to
get the output formatted as JSON.
Note that when you specify the `--json` flag, Borg's other non-JSON output is
suppressed so as not to interfere with the captured JSON. Also note that JSON
@@ -521,7 +707,7 @@ output only shows up at the console and not in syslog.
All borgmatic actions that accept an `--archive` flag allow you to specify an
archive name of `latest`. This lets you get the latest archive without having
to first run `borgmatic rlist` manually, which can be handy in automated
to first run `borgmatic repo-list` manually, which can be handy in automated
scripts. Here's an example:
```bash
+49
View File
@@ -29,6 +29,55 @@ For example, to ask the *Pass* password manager to provide the passphrase:
encryption_passcommand: pass path/to/borg-repokey
```
### Using systemd service credentials
Borgmatic supports using encrypted [credentials](https://systemd.io/CREDENTIALS/).
Save your password as an encrypted credential to `/etc/credstore.encrypted/borgmatic.pw`, e.g.,
```
# systemd-ask-password -n | systemd-creds encrypt - /etc/credstore.encrypted/borgmatic.pw
```
Then uncomment or use the following in your configuration file:
```yaml
encryption_passcommand: "cat ${CREDENTIALS_DIRECTORY}/borgmatic.pw"
```
Note that the name `borgmatic.pw` is hardcoded in the systemd service file.
To use multiple different passwords, save them as encrypted credentials to `/etc/credstore.encrypted/borgmatic/`, e.g.,
```
# mkdir /etc/credstore.encrypted/borgmatic
# systemd-ask-password -n | systemd-creds encrypt --name=borgmatic_backupserver1 - /etc/credstore.encrypted/borgmatic/backupserver1
# systemd-ask-password -n | systemd-creds encrypt --name=borgmatic_pw2 - /etc/credstore.encrypted/borgmatic/pw2
...
```
Ensure that the file names, (e.g. `backupserver1`) match the corresponding part of
the `--name` option *after* the underscore (_), and that the part *before*
the underscore matches the directory name (e.g. `borgmatic`).
Then, uncomment the appropriate line in the systemd service file:
```
# systemctl edit borgmatic.service
...
# Load multiple encrypted credentials.
LoadCredentialEncrypted=borgmatic:/etc/credstore.encrypted/borgmatic/
```
Finally, use the following in your configuration file:
```
encryption_passcommand: "cat ${CREDENTIALS_DIRECTORY}/borgmatic_backupserver1"
```
Adjust `borgmatic_backupserver1` according to the name given to the credential
and the directory set in the service file.
### Environment variable interpolation
<span class="minilink minilink-addedin">New in version 1.6.4</span> borgmatic
+8 -4
View File
@@ -22,7 +22,7 @@ following, all based on your borgmatic configuration files or command-line
arguments:
* configured repositories, running your Borg command once for each one
* local and remote Borg binary paths
* local and remote Borg executable paths
* SSH settings and Borg environment variables
* lock wait settings
* verbosity
@@ -45,10 +45,14 @@ be preferred though for most uses.)
You can also specify Borg options for relevant commands. For instance:
```bash
borgmatic borg rlist --short
borgmatic borg repo-list --short
```
This runs Borg's `rlist` command once on each configured borgmatic repository.
(No borgmatic `repo-list` action? Try `rlist` or `list` instead or upgrade
borgmatic!)
This runs Borg's `repo-list` command once on each configured borgmatic
repository.
What if you only want to run Borg on a single configured borgmatic repository
when you've got several configured? Not a problem. The `--repository` argument
@@ -88,7 +92,7 @@ string instead of trying to resolve it from borgmatic's shell where it's not
yet set.
Or if you don't need borgmatic to resolve an archive name like `latest`, you
can just do:
can do:
```bash
borgmatic borg list ::your-actual-archive-name
+3 -3
View File
@@ -15,7 +15,7 @@ version 1.1. borgmatic does not install Borg automatically so as to avoid
conflicts with existing Borg installations.
Then, [install pipx](https://pypa.github.io/pipx/installation/) as the root
user (with `sudo`) to make installing borgmatic easy without impacting other
user (with `sudo`) to make installing borgmatic easier without impacting other
Python applications on your system. If you have trouble installing pipx with
pip, then you can install a system package instead. E.g. on Ubuntu or Debian,
run:
@@ -214,11 +214,11 @@ command like the following with Borg 1.x:
sudo borgmatic init --encryption repokey
```
<span class="minilink minilink-addedin">New in borgmatic version 1.7.0</span>
<span class="minilink minilink-addedin">New in borgmatic version 1.9.0</span>
Or, with Borg 2.x:
```bash
sudo borgmatic rcreate --encryption repokey-aes-ocb
sudo borgmatic repo-create --encryption repokey-aes-ocb
```
(Note that `repokey-chacha20-poly1305` may be faster than `repokey-aes-ocb` on
+25 -6
View File
@@ -82,6 +82,25 @@ ticket](https://torsion.org/borgmatic/#issues) for help with upgrading any old
INI-style configuration files you may have.
### Versioning and breaking changes
To avoid version number bloat, borgmatic doesn't follow traditional semantic
versioning. But here's how borgmatic versioning generally works:
* Major version bumps (e.g., 1 to 2): Major breaking changes. Configuration
file formats might change, deprecated features may be removed entirely, etc.
* Minor version bumps (e.g., 1.8 to 1.9): Medium breaking changes. Depending
on the features you use, this may be a drop-in replacement. But read the
release notes to make sure.
* Patch version bumps (e.g., 1.8.13 to 1.8.14): Minor breaking changes. These
include, for instance, bug fixes that are technically breaking but may only
affect a small subset of users.
Each breaking change is prefixed with "BREAKING:" in [borgmatic's release
notes](https://projects.torsion.org/borgmatic-collective/borgmatic/releases),
so there should hopefully be no surprises.
## Upgrading Borg
To upgrade to a new version of Borg, you can generally install a new version
@@ -93,7 +112,7 @@ Borg releases require additional steps that borgmatic can help with.
### Borg 1.2 to 2.0
<span class="minilink minilink-addedin">New in borgmatic version 1.7.0</span>
<span class="minilink minilink-addedin">New in borgmatic version 1.9.0</span>
Upgrading Borg from 1.2 to 2.0 requires manually upgrading your existing Borg
1 repositories before use with Borg or borgmatic. Here's how you can
accomplish that.
@@ -125,11 +144,11 @@ option was found in the `location:` section of your configuration.
<span class="minilink minilink-addedin">Prior to version 1.7.10</span> Omit
the `path:` portion of the `repositories` list.
Then, run the `rcreate` action (formerly `init`) to create that new Borg 2
Then, run the `repo-create` action (formerly `init`) to create that new Borg 2
repository:
```bash
borgmatic rcreate --verbosity 1 --encryption repokey-blake2-aes-ocb \
borgmatic repo-create --verbosity 1 --encryption repokey-blake2-aes-ocb \
--source-repository original.borg --repository upgraded.borg
```
@@ -146,12 +165,12 @@ 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 \
borgmatic repo-create --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)
modes](https://borgbackup.readthedocs.io/en/latest/usage/repo-create.html)
for more details.
To transfer data from your original Borg 1 repository to your newly created
@@ -174,7 +193,7 @@ 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
between related Borg 2 repositories without upgrading, even down to individual
archives. For more on that functionality, see the [Borg transfer
documentation](https://borgbackup.readthedocs.io/en/2.0.0b5/usage/transfer.html).
documentation](https://borgbackup.readthedocs.io/en/2.0.0b13/usage/transfer.html).
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
+7 -10
View File
@@ -10,20 +10,17 @@ eleventyNavigation:
If case you're interested in [developing on
borgmatic](https://torsion.org/borgmatic/docs/how-to/develop-on-borgmatic/),
here's an abridged primer on how its Python source code is organized to help
you get started. At the top level we have:
you get started. Starting at the top level, we have:
* [borgmatic](https://projects.torsion.org/borgmatic-collective/borgmatic/src/branch/main/borgmatic): The main borgmatic source module. Most of the code is here.
* [borgmatic](https://projects.torsion.org/borgmatic-collective/borgmatic/src/branch/main/borgmatic): The main borgmatic source module. Most of the code is here. Within that:
* [actions](https://projects.torsion.org/borgmatic-collective/borgmatic/src/branch/main/borgmatic/actions): borgmatic-specific logic for running each action (create, list, check, etc.).
* [borg](https://projects.torsion.org/borgmatic-collective/borgmatic/src/branch/main/borgmatic/borg): Lower-level code that's responsible for interacting with Borg to run each action.
* [commands](https://projects.torsion.org/borgmatic-collective/borgmatic/src/branch/main/borgmatic/commands): Looking to add a new flag or action? Start here. This contains borgmatic's entry point, argument parsing, and shell completion.
* [config](https://projects.torsion.org/borgmatic-collective/borgmatic/src/branch/main/borgmatic/config): Code responsible for loading, normalizing, and validating borgmatic's configuration.
* [hooks](https://projects.torsion.org/borgmatic-collective/borgmatic/src/branch/main/borgmatic/hooks): Looking to add a new database or monitoring integration? Start here.
* [docs](https://projects.torsion.org/borgmatic-collective/borgmatic/src/branch/main/docs): How-to and reference documentation, including the document you're reading now.
* [sample](https://projects.torsion.org/borgmatic-collective/borgmatic/src/branch/main/sample): Example configurations for cron and systemd.
* [scripts](https://projects.torsion.org/borgmatic-collective/borgmatic/src/branch/main/scripts): Dev-facing scripts for things like building documentation and running end-to-end tests.
* [tests](https://projects.torsion.org/borgmatic-collective/borgmatic/src/branch/main/tests): Automated tests organized by: end-to-end, integration, and unit.
Within the `borgmatic` directory you'll find:
* [actions](https://projects.torsion.org/borgmatic-collective/borgmatic/src/branch/main/borgmatic/actions): Mid-level code for running each borgmatic action (create, list, check, etc.).
* [borg](https://projects.torsion.org/borgmatic-collective/borgmatic/src/branch/main/borgmatic/borg): Lower-level code that actually shells out to Borg for each action.
* [commands](https://projects.torsion.org/borgmatic-collective/borgmatic/src/branch/main/borgmatic/commands): Looking to add a new flag or action? Start here. This contains borgmatic's entry point, argument parsing, and shell completion.
* [config](https://projects.torsion.org/borgmatic-collective/borgmatic/src/branch/main/borgmatic/config): Code responsible for loading, normalizing, and validating borgmatic's configuration.
* [hooks](https://projects.torsion.org/borgmatic-collective/borgmatic/src/branch/main/borgmatic/hooks): Looking to add a new database or monitoring integration? Start here.
So, broadly speaking, the control flow goes: `commands``config` followed by `commands``actions``borg` and `hooks`.
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

+58
View File
@@ -1,3 +1,61 @@
[project]
name = "borgmatic"
version = "1.9.2"
authors = [
{ name="Dan Helfman", email="witten@torsion.org" },
]
description = "Simple, configuration-driven backup software for servers and workstations"
readme = "README.md"
requires-python = ">=3.8"
classifiers=[
"Development Status :: 5 - Production/Stable",
"Environment :: Console",
"Intended Audience :: System Administrators",
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
"Programming Language :: Python",
"Topic :: Security :: Cryptography",
"Topic :: System :: Archiving :: Backup",
]
dependencies = [
"colorama>=0.4.1,<0.5",
"jsonschema",
"packaging",
"requests",
"ruamel.yaml>0.15.0",
]
[project.scripts]
borgmatic = "borgmatic.commands.borgmatic:main"
generate-borgmatic-config = "borgmatic.commands.generate_config:main"
validate-borgmatic-config = "borgmatic.commands.validate_config:main"
[project.optional-dependencies]
Apprise = ["apprise"]
[project.urls]
Homepage = "https://torsion.org/borgmatic"
[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"
[tool.setuptools.packages.find]
include = ["borgmatic*"]
namespaces = false
[tool.black]
line-length = 100
skip-string-normalization = true
[tool.pytest.ini_options]
testpaths = "tests"
addopts = "--cov-report term-missing:skip-covered --cov=borgmatic --ignore=tests/end-to-end"
[tool.isort]
profile = "black"
known_first_party = "borgmatic"
line_length = 100
skip = ".tox"
[tool.codespell]
skip = ".git,.tox,build"
+1
View File
@@ -3,6 +3,7 @@ Description=borgmatic backup
Wants=network-online.target
After=network-online.target
ConditionACPower=true
Documentation=https://torsion.org/borgmatic/
[Service]
Type=oneshot
+9
View File
@@ -5,9 +5,18 @@ After=network-online.target
# Prevent borgmatic from running unless the machine is plugged into power. Remove this line if you
# want to allow borgmatic to run anytime.
ConditionACPower=true
Documentation=https://torsion.org/borgmatic/
[Service]
Type=oneshot
RuntimeDirectory=borgmatic
StateDirectory=borgmatic
# Load single encrypted credential.
LoadCredentialEncrypted=borgmatic.pw
# Load multiple encrypted credentials.
# LoadCredentialEncrypted=borgmatic:/etc/credstore.encrypted/borgmatic/
# Security settings for systemd running as root, optional but recommended to improve security. You
# can disable individual settings if they cause problems for your use case. For more details, see
+1 -1
View File
@@ -8,4 +8,4 @@ if [ -e "$USER_PODMAN_SOCKET_PATH" ]; then
export DOCKER_HOST="unix://$USER_PODMAN_SOCKET_PATH"
fi
docker-compose --file docs/docker-compose.yaml up --build --force-recreate
BUILDKIT_PROGRESS=plain docker-compose --file docs/docker-compose.yaml up --build --force-recreate
+11
View File
@@ -0,0 +1,11 @@
#!/bin/bash
set -e
docs_container_id=$(podman create "$IMAGE_NAME")
podman cp $docs_container_id:/usr/share/nginx/html - > borgmatic-docs-dump.tar
tar xf borgmatic-docs-dump.tar
rm borgmatic-docs-dump.tar
mv html borgmatic-docs
tar cfz borgmatic-docs.tar.gz borgmatic-docs
podman rm --volumes $docs_container_id
+1 -2
View File
@@ -33,8 +33,7 @@ git push github $version
# Build borgmatic and publish to pypi.
rm -fr dist
python3 setup.py bdist_wheel
python3 setup.py sdist
python3 -m build
twine upload -r pypi --username __token__ dist/borgmatic-*.tar.gz
twine upload -r pypi --username __token__ dist/borgmatic-*-py3-none-any.whl
+1 -1
View File
@@ -21,7 +21,7 @@ apk add --no-cache python3 py3-pip borgbackup postgresql-client mariadb-client m
py3-ruamel.yaml py3-ruamel.yaml.clib py3-yaml bash sqlite fish
# If certain dependencies of black are available in this version of Alpine, install them.
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==24.2 setuptools==72.1.0
pip3 install --ignore-installed tox==4.11.3
export COVERAGE_FILE=/tmp/.coverage
-26
View File
@@ -1,26 +0,0 @@
[metadata]
description_file=README.md
[tool:pytest]
testpaths = tests
addopts = --cov-report term-missing:skip-covered --cov=borgmatic --ignore=tests/end-to-end
[flake8]
max-line-length = 100
extend-ignore = E203,E501,W503
exclude = *.*/*
multiline-quotes = '''
docstring-quotes = '''
[tool:isort]
profile=black
known_first_party = borgmatic
line_length = 100
skip = .tox
[codespell]
skip = .git,.tox,build
[pycodestyle]
ignore = E203
max_line_length = 100
-42
View File
@@ -1,42 +0,0 @@
from setuptools import find_packages, setup
VERSION = '1.8.12'
setup(
name='borgmatic',
version=VERSION,
description='Simple, configuration-driven backup software for servers and workstations',
author='Dan Helfman',
author_email='witten@torsion.org',
url='https://torsion.org/borgmatic',
classifiers=[
'Development Status :: 5 - Production/Stable',
'Environment :: Console',
'Intended Audience :: System Administrators',
'License :: OSI Approved :: GNU General Public License v3 (GPLv3)',
'Programming Language :: Python',
'Topic :: Security :: Cryptography',
'Topic :: System :: Archiving :: Backup',
],
packages=find_packages(exclude=['tests*']),
entry_points={
'console_scripts': [
'borgmatic = borgmatic.commands.borgmatic:main',
'generate-borgmatic-config = borgmatic.commands.generate_config:main',
'validate-borgmatic-config = borgmatic.commands.validate_config:main',
]
},
obsoletes=['atticmatic'],
install_requires=(
'colorama>=0.4.1,<0.5',
'jsonschema',
'packaging',
'requests',
'ruamel.yaml>0.15.0',
'setuptools',
),
extras_require={"Apprise": ["apprise"]},
include_package_data=True,
python_requires='>=3.8',
)
+1 -1
View File
@@ -2,7 +2,7 @@ appdirs==1.4.4
apprise==1.8.0
attrs==23.2.0
black==24.4.2
certifi==2024.2.2
certifi==2024.7.4
chardet==5.2.0
click==8.1.7
codespell==2.2.6
+1 -1
View File
@@ -44,7 +44,7 @@ def test_borgmatic_command():
generate_configuration(config_path, repository_path)
subprocess.check_call(
f'borgmatic -v 2 --config {config_path} rcreate --encryption repokey'.split(' ')
f'borgmatic -v 2 --config {config_path} repo-create --encryption repokey'.split(' ')
)
# Run borgmatic to generate a backup archive, and then list it to make sure it exists.
+61 -23
View File
@@ -14,7 +14,7 @@ def write_configuration(
source_directory,
config_path,
repository_path,
borgmatic_source_directory,
user_runtime_directory,
postgresql_dump_format='custom',
mongodb_dump_format='archive',
):
@@ -28,7 +28,7 @@ source_directories:
- {source_directory}
repositories:
- path: {repository_path}
borgmatic_source_directory: {borgmatic_source_directory}
user_runtime_directory: {user_runtime_directory}
encryption_passphrase: "test"
@@ -101,7 +101,7 @@ def write_custom_restore_configuration(
source_directory,
config_path,
repository_path,
borgmatic_source_directory,
user_runtime_directory,
postgresql_dump_format='custom',
mongodb_dump_format='archive',
):
@@ -115,7 +115,7 @@ source_directories:
- {source_directory}
repositories:
- path: {repository_path}
borgmatic_source_directory: {borgmatic_source_directory}
user_runtime_directory: {user_runtime_directory}
encryption_passphrase: "test"
@@ -173,7 +173,7 @@ def write_simple_custom_restore_configuration(
source_directory,
config_path,
repository_path,
borgmatic_source_directory,
user_runtime_directory,
postgresql_dump_format='custom',
):
'''
@@ -187,7 +187,7 @@ source_directories:
- {source_directory}
repositories:
- path: {repository_path}
borgmatic_source_directory: {borgmatic_source_directory}
user_runtime_directory: {user_runtime_directory}
encryption_passphrase: "test"
@@ -347,7 +347,6 @@ def test_database_dump_and_restore():
# Create a Borg repository.
temporary_directory = tempfile.mkdtemp()
repository_path = os.path.join(temporary_directory, 'test.borg')
borgmatic_source_directory = os.path.join(temporary_directory, '.borgmatic')
# Write out a special file to ensure that it gets properly excluded and Borg doesn't hang on it.
os.mkfifo(os.path.join(temporary_directory, 'special_file'))
@@ -357,13 +356,22 @@ def test_database_dump_and_restore():
try:
config_path = os.path.join(temporary_directory, 'test.yaml')
config = write_configuration(
temporary_directory, config_path, repository_path, borgmatic_source_directory
temporary_directory, config_path, repository_path, temporary_directory
)
create_test_tables(config)
select_test_tables(config)
subprocess.check_call(
['borgmatic', '-v', '2', '--config', config_path, 'rcreate', '--encryption', 'repokey']
[
'borgmatic',
'-v',
'2',
'--config',
config_path,
'repo-create',
'--encryption',
'repokey',
]
)
# Run borgmatic to generate a backup archive including database dumps.
@@ -397,20 +405,28 @@ def test_database_dump_and_restore_with_restore_cli_flags():
# Create a Borg repository.
temporary_directory = tempfile.mkdtemp()
repository_path = os.path.join(temporary_directory, 'test.borg')
borgmatic_source_directory = os.path.join(temporary_directory, '.borgmatic')
original_working_directory = os.getcwd()
try:
config_path = os.path.join(temporary_directory, 'test.yaml')
config = write_simple_custom_restore_configuration(
temporary_directory, config_path, repository_path, borgmatic_source_directory
temporary_directory, config_path, repository_path, temporary_directory
)
create_test_tables(config)
select_test_tables(config)
subprocess.check_call(
['borgmatic', '-v', '2', '--config', config_path, 'rcreate', '--encryption', 'repokey']
[
'borgmatic',
'-v',
'2',
'--config',
config_path,
'repo-create',
'--encryption',
'repokey',
]
)
# Run borgmatic to generate a backup archive including a database dump.
@@ -467,20 +483,28 @@ def test_database_dump_and_restore_with_restore_configuration_options():
# Create a Borg repository.
temporary_directory = tempfile.mkdtemp()
repository_path = os.path.join(temporary_directory, 'test.borg')
borgmatic_source_directory = os.path.join(temporary_directory, '.borgmatic')
original_working_directory = os.getcwd()
try:
config_path = os.path.join(temporary_directory, 'test.yaml')
config = write_custom_restore_configuration(
temporary_directory, config_path, repository_path, borgmatic_source_directory
temporary_directory, config_path, repository_path, temporary_directory
)
create_test_tables(config)
select_test_tables(config)
subprocess.check_call(
['borgmatic', '-v', '2', '--config', config_path, 'rcreate', '--encryption', 'repokey']
[
'borgmatic',
'-v',
'2',
'--config',
config_path,
'repo-create',
'--encryption',
'repokey',
]
)
# Run borgmatic to generate a backup archive including a database dump.
@@ -515,7 +539,6 @@ def test_database_dump_and_restore_with_directory_format():
# Create a Borg repository.
temporary_directory = tempfile.mkdtemp()
repository_path = os.path.join(temporary_directory, 'test.borg')
borgmatic_source_directory = os.path.join(temporary_directory, '.borgmatic')
original_working_directory = os.getcwd()
@@ -525,7 +548,7 @@ def test_database_dump_and_restore_with_directory_format():
temporary_directory,
config_path,
repository_path,
borgmatic_source_directory,
temporary_directory,
postgresql_dump_format='directory',
mongodb_dump_format='directory',
)
@@ -533,7 +556,16 @@ def test_database_dump_and_restore_with_directory_format():
select_test_tables(config)
subprocess.check_call(
['borgmatic', '-v', '2', '--config', config_path, 'rcreate', '--encryption', 'repokey']
[
'borgmatic',
'-v',
'2',
'--config',
config_path,
'repo-create',
'--encryption',
'repokey',
]
)
# Run borgmatic to generate a backup archive including a database dump.
@@ -557,18 +589,24 @@ def test_database_dump_with_error_causes_borgmatic_to_exit():
# Create a Borg repository.
temporary_directory = tempfile.mkdtemp()
repository_path = os.path.join(temporary_directory, 'test.borg')
borgmatic_source_directory = os.path.join(temporary_directory, '.borgmatic')
original_working_directory = os.getcwd()
try:
config_path = os.path.join(temporary_directory, 'test.yaml')
write_configuration(
temporary_directory, config_path, repository_path, borgmatic_source_directory
)
write_configuration(temporary_directory, config_path, repository_path, temporary_directory)
subprocess.check_call(
['borgmatic', '-v', '2', '--config', config_path, 'rcreate', '--encryption', 'repokey']
[
'borgmatic',
'-v',
'2',
'--config',
config_path,
'repo-create',
'--encryption',
'repokey',
]
)
# Run borgmatic with a config override such that the database dump fails.

Some files were not shown because too many files have changed in this diff Show More