Compare commits

...

1346 Commits
1.4.4 ... main

Author SHA1 Message Date
Dan Helfman 7f735cbe59 Fix a traceback with "check --only spot" when the "spot" check is unconfigured (#857).
build / test (push) Successful in 7m42s Details
build / docs (push) Successful in 2m10s Details
2024-04-24 16:12:58 -07:00
Dan Helfman a690ea4016 Add Healtchecks auto-provisioning to NEWS (#815).
build / test (push) Successful in 5m49s Details
build / docs (push) Successful in 2m16s Details
2024-04-23 09:25:29 -07:00
Dan Helfman 7a110c7acd Add Healthchecks auto-provisionning (#815).
build / docs (push) Blocked by required conditions Details
build / test (push) Has been cancelled Details
Reviewed-on: #852
Reviewed-by: Dan Helfman <witten@torsion.org>
2024-04-23 16:23:26 +00:00
estebanthilliez 407bb33359 Fix schema.yaml to comply with maximum line length 2024-04-22 20:47:03 +02:00
estebanthilliez 4b7f7bba04 Issue warning if using UUID URL scheme with create_slug 2024-04-22 20:45:36 +02:00
estebanthilliez cfdc0a1f2a Fix Healthchecks UUID regex 2024-04-22 20:44:31 +02:00
Dan Helfman f926055e67 Fix a traceback when the "data" consistency check is used (#854).
build / test (push) Successful in 7m36s Details
build / docs (push) Successful in 2m26s Details
2024-04-21 14:55:02 -07:00
Dan Helfman 058af95d70 Document limitation about using database hooks and "one_file_system" (#853).
build / test (push) Successful in 4m20s Details
build / docs (push) Successful in 52s Details
2024-04-20 14:53:41 -07:00
Dan Helfman 54facdc391 Clarify Apprise states configuration.
build / test (push) Successful in 6m2s Details
build / docs (push) Successful in 1m29s Details
2024-04-20 08:26:06 -07:00
estebanthi 2e4c0cc7e7 Support for healthchecks auto provisionning 2024-04-19 10:43:45 +02:00
Dan Helfman cb2fd7c5e8 Fix lack of file extraction when using "extract --strip-components all" on a path with a leading slash (#851).
build / test (push) Successful in 6m0s Details
build / docs (push) Successful in 1m30s Details
2024-04-17 16:50:09 -07:00
Dan Helfman 94133cc8b1 Add note about running spot check on a separate schedule (#656).
build / test (push) Successful in 4m15s Details
build / docs (push) Successful in 52s Details
2024-04-16 10:57:34 -07:00
Dan Helfman dcec89be90 Wording tweak (#656).
build / test (push) Successful in 4m17s Details
build / docs (push) Has been cancelled Details
2024-04-16 10:52:56 -07:00
Dan Helfman fefd5d1d0e Wording tweak (#656).
build / docs (push) Blocked by required conditions Details
build / test (push) Has been cancelled Details
2024-04-16 10:50:37 -07:00
Dan Helfman 163c37d77f Bump version for release. 2024-04-16 10:43:35 -07:00
Dan Helfman b0e49ebce0 When "--match-archives *" is used with "check" action, don't skip Borg's orphaned objects check (#779).
build / test (push) Successful in 4m17s Details
build / docs (push) Successful in 49s Details
2024-04-16 10:38:14 -07:00
Dan Helfman 7e51c41ebf Mask the password when logging a MongoDB dump or restore command (#848).
build / test (push) Successful in 5m55s Details
build / docs (push) Successful in 1m28s Details
2024-04-16 10:20:15 -07:00
Dan Helfman f9182514d8 Add spot consistency check (#656).
build / test (push) Successful in 4m18s Details
build / docs (push) Successful in 1m28s Details
Reviewed-on: #849
2024-04-15 21:25:50 +00:00
Dan Helfman 7700b87b60 Test requirements security upgrade.
build / test (push) Failing after 3m19s Details
build / docs (push) Has been skipped Details
2024-04-15 14:21:01 -07:00
Dan Helfman 75bdbe6087 Spot check documentation and edge case tweaks (#656). 2024-04-15 14:18:42 -07:00
Dan Helfman d243a8c836 Add spot check documentation (#656). 2024-04-15 12:51:07 -07:00
Dan Helfman 4c2eb2bfe3 Spot check basically complete other than docs (#656). 2024-04-15 11:02:05 -07:00
Dan Helfman 89ce060dbd Merge branch 'main' into spot-check 2024-04-05 12:25:50 -07:00
Dan Helfman ad7dcb4615 Fix "--json" error when Borg includes non-JSON warnings in JSON output (#847).
build / test (push) Failing after 3m16s Details
build / docs (push) Has been skipped Details
2024-04-05 12:23:50 -07:00
Dan Helfman 6680aece5a Split out (most of) command construction from create_archive() in preparation for reuse in spot check (#656). 2024-04-04 14:23:56 -07:00
Dan Helfman 57eb93760f Merge branch 'main' into spot-check 2024-03-22 11:27:24 -07:00
Dan Helfman f21a2c06e3 Add documentation link to Loki dashboard for borgmatic (#843).
build / test (push) Successful in 6m37s Details
build / docs (push) Successful in 1m29s Details
2024-03-22 11:25:33 -07:00
Dan Helfman 2212539cb0 Merge branch 'main' into spot-check. 2024-03-20 12:01:52 -07:00
Dan Helfman 36d10fecb1 Upgrade black in test requirements.
build / test (push) Successful in 5m56s Details
build / docs (push) Successful in 1m28s Details
2024-03-20 12:01:24 -07:00
Dan Helfman 3ecd0e731e Initial work on spot check schema and preparatory refactoring (#656). 2024-03-20 11:58:59 -07:00
Dan Helfman ecf5a7e294 When a command hook exits with a soft failure, ping the log and finish states for any configured monitoring hooks (#842).
build / test (push) Successful in 6m0s Details
build / docs (push) Successful in 1m28s Details
2024-03-18 23:15:28 -07:00
Dan Helfman 893fbcf9ff Add documentation about backing up containerized databases by configuring borgmatic to exec into a container to run a dump command.
build / test (push) Successful in 5m57s Details
build / docs (push) Successful in 1m29s Details
2024-03-14 18:00:52 -07:00
Dan Helfman f8f6560502 Fix handling of the NO_COLOR environment variable to ignore an empty value (#835).
build / test (push) Successful in 6m0s Details
build / docs (push) Successful in 1m29s Details
2024-03-13 09:35:19 -07:00
Dan Helfman 8c301ba688 Bump version for release.
build / test (push) Successful in 5m15s Details
build / docs (push) Successful in 1m5s Details
2024-03-11 13:27:08 -07:00
Dan Helfman 035e96156a Add an "access_token" option to the ntfy monitoring hook for authenticating without username/password (#811).
build / test (push) Successful in 5m2s Details
build / docs (push) Successful in 1m10s Details
2024-03-11 12:48:58 -07:00
Dan Helfman a08c7fc77a When running the "rcreate" action and the repository already exists but with a different encryption mode than requested, error (#840).
build / test (push) Successful in 4m55s Details
build / docs (push) Successful in 1m0s Details
2024-03-11 11:24:36 -07:00
Dan Helfman cf9e387811 Document a potentially breaking shell quoting edge case within error hooks (#839).
build / test (push) Successful in 6m43s Details
build / docs (push) Successful in 1m44s Details
2024-03-11 10:42:51 -07:00
Dan Helfman e37224606a Clarify dev-CI parity.
build / test (push) Successful in 5m12s Details
build / docs (push) Successful in 1m13s Details
2024-03-10 19:14:18 -07:00
Dan Helfman 9647301b99 Add log sending for the Apprise logging hook, enabled by default.
build / test (push) Successful in 7m4s Details
build / docs (push) Successful in 1m53s Details
2024-03-10 16:18:49 -07:00
Dan Helfman a0e5dbff96 Remove list of command in Bash script.
build / test (push) Successful in 5m5s Details
build / docs (push) Successful in 1m11s Details
2024-03-06 21:24:44 -08:00
Dan Helfman 86117edccf Remove build.torsion.org references from documentation.
build / test (push) Successful in 7m7s Details
build / docs (push) Successful in 1m56s Details
2024-03-06 20:01:32 -08:00
Dan Helfman 440f3eeb63 Remove Drone configuration/tests.
build / test (push) Successful in 5m1s Details
build / docs (push) Successful in 1m2s Details
2024-03-06 19:04:29 -08:00
Dan Helfman 181051eae1 Add new build server to NEWS.
build / test (push) Successful in 4m45s Details
build / docs (push) Successful in 2m5s Details
2024-03-06 18:52:27 -08:00
Dan Helfman ec0ee971ed Attempt to use secrets.
build / test (push) Successful in 4m46s Details
build / docs (push) Failing after 4s Details
2024-03-06 18:38:45 -08:00
Dan Helfman b83ffa0cf6 Attempt to fix trigger.
build / test (push) Successful in 4m45s Details
build / docs (push) Failing after 4s Details
2024-03-06 16:53:41 -08:00
Dan Helfman cf88665d37 Fix typo.
build / docs (push) Blocked by required conditions Details
build / test (push) Has been cancelled Details
2024-03-06 16:52:33 -08:00
Dan Helfman b233adba63 Fix build? 2024-03-06 16:51:49 -08:00
Dan Helfman 018f5e3315 Merge workflows, since Gitea doesn't yet support workflow_run. 2024-03-06 16:49:50 -08:00
Dan Helfman 284f26b49d Only run tests on pushes to main branch. 2024-03-06 16:40:39 -08:00
Dan Helfman 11b437794e Attempt to build documentation.
test / test (push) Successful in 4m53s Details
2024-03-06 16:38:34 -08:00
Dan Helfman 0665b50d57 Fixed debugging.
test / test (push) Successful in 4m40s Details
2024-03-06 16:17:12 -08:00
Dan Helfman 0586b80e5b More debugging.
test / test (push) Failing after 4m44s Details
2024-03-06 15:53:30 -08:00
Dan Helfman 272a7b4866 Actually kill other containers after tests finish.
test / test (push) Successful in 4m45s Details
2024-03-06 15:41:03 -08:00
Dan Helfman 98d4a59459 Another iteration.
test / test (push) Successful in 4m46s Details
2024-03-06 15:29:56 -08:00
Dan Helfman 744139cf97 Disable progress.
test / test (push) Has been cancelled Details
2024-03-06 15:21:45 -08:00
Dan Helfman 1339509e9b Flag order apparently matters to Docker Compose.
test / test (push) Waiting to run Details
2024-03-06 14:55:55 -08:00
Dan Helfman e14f61415b Fix spew in test script.
test / test (push) Failing after 2s Details
2024-03-06 14:54:53 -08:00
Dan Helfman 98cf8f7e20 Another try at exiting tests properly.
test / test (push) Has been cancelled Details
2024-03-06 14:42:06 -08:00
Dan Helfman 5f16b64639 Attempt to exit test containers on tests exit while also showing test output.
test / test (push) Failing after 3s Details
2024-03-06 14:39:23 -08:00
Dan Helfman fe62a81151 Add missing service name to test scrits.
test / test (push) Successful in 4m54s Details
2024-03-06 14:32:26 -08:00
Dan Helfman 585b1573ae Attempt to make containers stop after tests run.
test / test (push) Failing after 7s Details
2024-03-06 14:30:49 -08:00
Dan Helfman 141ba2771d Attempt to fix and debug read-only filesystem issue at build.
test / test (push) Has been cancelled Details
2024-03-06 11:10:20 -08:00
Dan Helfman a527f76d08 Add back checkout now that NodeJS is installed on the host.
continuous-integration/drone/push Build was killed Details
test / test (push) Has been cancelled Details
2024-03-06 08:49:53 -08:00
Dan Helfman a97c68b4c8 Debugging ls.
test / test (push) Failing after 0s Details
continuous-integration/drone/push Build was killed Details
2024-03-06 08:35:50 -08:00
Dan Helfman ef07005a75 Remove duplicative(?) checkout step.
test / test (push) Failing after 0s Details
continuous-integration/drone/push Build was killed Details
2024-03-06 08:35:05 -08:00
Dan Helfman 43c7c3b6be First attempt at using Gitea Actions to run tests.
test / test (push) Failing after 15s Details
continuous-integration/drone/push Build was killed Details
2024-03-06 08:32:55 -08:00
Dan Helfman 2f6ad9d173 Add NO_COLOR support to NEWS (#835).
continuous-integration/drone/push Build was killed Details
2024-03-04 13:49:54 -08:00
Dan Helfman 16bc0de3fb
Support for NO_COLOR environment variable (#835).
Merge pull request #82 from shivansh02/feature/support-no-color-env-var
2024-03-04 13:46:09 -08:00
shivansh02 458d157e62 NO_COLOR set to any value returns false 2024-03-05 00:15:52 +05:30
shivansh02 40c3a28620 support for NO_COLOR env var 2024-03-04 18:21:28 +05:30
Dan Helfman 60107f1ee8 Add custom dump/restore command options for MySQL and MariaDB (#311).
continuous-integration/drone/push Build was killed Details
2024-03-03 14:32:49 -08:00
Dan Helfman a1153a21fa
Custom dump command options for MySQL and MariaDB.
Merge pull request #81 from shivansh02/feature/custom-dump-restore-commands-mysql
2024-03-03 14:27:14 -08:00
shivansh02 b6cb7da98e custom dump commands for mariadb 2024-03-04 00:24:22 +05:30
shivansh02 9e3d19a406 custom commands escaped 2024-03-03 23:31:02 +05:30
shivansh02 2b755d8ade custom show command for mysql and schema description 2024-03-03 23:15:07 +05:30
shivansh02 925f99cfef custom dump command for mysql 2024-03-03 03:47:02 +05:30
Dan Helfman c9f20eb260 Fix "--override" values containing deprecated section headers not actually overriding configuration options under deprecated section headers (#829). 2024-02-15 21:12:42 -08:00
Dan Helfman f4744826fe When the "--json" flag is given, suppress console escape codes so as not to interfere with JSON output (#827).
continuous-integration/drone/push Build is passing Details
2024-02-11 17:44:43 -08:00
Dan Helfman 5586aab967 Clarify documentation about restoring a database: borgmatic does not create the database upon restore.
continuous-integration/drone/push Build is passing Details
2024-02-09 15:35:29 -08:00
Dan Helfman 6fa5dff79b Fix broken escaping logic for "pg_dump_command" (#822) + bonus shell injection fixes.
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
2024-01-31 10:53:32 -08:00
Dan Helfman 75d11aa9cd Pass the PostgreSQL "PGSSLMODE" environment variable through to Borg (#370).
continuous-integration/drone/push Build is passing Details
2024-01-25 14:18:01 -08:00
Dan Helfman ad1d104d65 Fix broken repository detection in the "rcreate" action with Borg 1.4 (#820).
continuous-integration/drone/push Build is passing Details
2024-01-24 15:45:51 -08:00
Dan Helfman 009062128d Remove Python 3.8+ restriction, as only Python 3.8+ is supported. 2024-01-22 09:41:43 -08:00
Dan Helfman e9813d2539 Allow the "--repository" flag to match across multiple configuration files (#818). 2024-01-21 18:25:44 -08:00
Dan Helfman f9998b50e8 Rephrase documentation and link to docs on exit codes feature (#798).
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
2024-01-21 14:47:21 -08:00
Dan Helfman 5f921a7f80 Add documentation heading (#798).
continuous-integration/drone/push Build is passing Details
2024-01-21 11:48:23 -08:00
Dan Helfman abf2b3a8c7 Elevate specific Borg warnings to errors or squash errors to warnings (#798).
continuous-integration/drone/push Build is passing Details
2024-01-21 11:34:40 -08:00
Dan Helfman 34f3c2bb16 Clarify "--override" command-line help (#814)
continuous-integration/drone/push Build is passing Details
2024-01-19 11:55:00 -08:00
Dan Helfman 4d79f582df Fix a traceback when providing an invalid "--override" value for a list option (#814).
continuous-integration/drone/push Build is passing Details
2024-01-18 10:39:40 -08:00
Dan Helfman 63198088c4 Store included configuration files within each backup archive in support of the "config bootstrap" action (#736).
continuous-integration/drone/push Build is passing Details
2024-01-09 13:47:20 -08:00
Dan Helfman 3c22a8ec16 Prevent various shell injection attacks (#810).
continuous-integration/drone/push Build is passing Details
2024-01-07 10:21:49 -08:00
Dan Helfman ca49109ce7 Bump version for release.
continuous-integration/drone/tag Build is passing Details
continuous-integration/drone/push Build is passing Details
2024-01-03 10:08:05 -08:00
Dan Helfman 6a7f71f92f Clarify prune action help concerning running compact afterwards (#808).
continuous-integration/drone/push Build is passing Details
2024-01-03 10:03:35 -08:00
Dan Helfman 5f3dc1cfb0 Stream SQLite databases directly to Borg instead of dumping to an intermediate file (#807).
continuous-integration/drone/push Build is passing Details
2023-12-31 11:07:59 -08:00
Dan Helfman f2023aed22 Fix typo.
continuous-integration/drone/push Build is passing Details
2023-12-30 15:48:55 -08:00
Dan Helfman a03c2744e5 Update docs/how-to/provide-your-passwords.md (#805).
continuous-integration/drone/push Build is passing Details
Reviewed-on: #805
Reviewed-by: Dan Helfman <witten@torsion.org>
2023-12-30 23:48:32 +00:00
axel simon 4176532317 Update docs/how-to/provide-your-passwords.md
Provide an explanation of encryption_passcommand.
Also, adjust headers for consistency.
2023-12-30 23:45:56 +00:00
Dan Helfman 9d6025e902 Validate the configured action names in the "skip_actions" option (#804).
continuous-integration/drone/push Build is passing Details
2023-12-28 20:07:57 -08:00
Dan Helfman cf739bc997 The "check --force" flag now runs checks even if "check" is in "skip_actions" (#802).
continuous-integration/drone/push Build is passing Details
2023-12-28 10:22:48 -08:00
Dan Helfman 84823dfb91 Clarify constants/placeholders interaction and improve examples (#763).
continuous-integration/drone/push Build is passing Details
2023-12-24 11:18:17 -08:00
Dan Helfman 20cf0f7089 Add an "--ssh-command" flag to the "config bootstrap" action (#767).
continuous-integration/drone/push Build is passing Details
2023-12-24 10:33:55 -08:00
Dan Helfman 67af0f5734 Document limitation with constant interpolation at the start of a value (#741).
continuous-integration/drone/push Build is passing Details
2023-12-22 21:39:44 -08:00
Dan Helfman e80e0a253c Add configured repository labels to the JSON output for all actions (#800). 2023-12-20 09:17:41 -08:00
Dan Helfman 72587a3b72 Merge branch 'main' of ssh://projects.torsion.org:3022/borgmatic-collective/borgmatic
continuous-integration/drone/push Build is passing Details
2023-12-04 11:17:59 -08:00
Dan Helfman 8b49a59aff Fix a traceback when the "repositories" option contains both strings and key/value pairs (#794). 2023-12-04 11:17:13 -08:00
Dan Helfman e120dff9ff Add debug message that logs borg version for every config (#714).
continuous-integration/drone/push Build is passing Details
Reviewed-on: #792
2023-11-25 03:59:40 +00:00
Tobias Hodapp 257678b66f Fixed borg -> Borg 2023-11-24 18:47:37 +01:00
Tobias Hodapp 422c5e32f4 Added debug message that logs borg version for every config 2023-11-23 11:46:10 +01:00
Dan Helfman c34ad7dde7 Update documentation about configuration includes and constants (#745).
continuous-integration/drone/push Build is passing Details
2023-11-19 21:22:10 -08:00
Dan Helfman fdb353d358 Bump version for release.
continuous-integration/drone/tag Build is passing Details
2023-11-19 21:14:56 -08:00
Dan Helfman 3b99f7c75a Constants support includes and command-line overrides (#745, #782) 2023-11-19 21:13:35 -08:00
Dan Helfman 8b9abc6cf8 Documentation clarifications (#791).
continuous-integration/drone/push Build is passing Details
2023-11-15 09:05:52 -08:00
Dan Helfman da034c316a Add another mention of "skip_actions" to the docs (#701).
continuous-integration/drone/push Build is passing Details
2023-11-08 18:22:17 -08:00
Dan Helfman 08d01d8bcd Documentation formatting.
continuous-integration/drone/push Build is passing Details
2023-11-08 17:57:31 -08:00
Dan Helfman eef69e23ee Document the possible units of times for a configured check frequency.
continuous-integration/drone/push Build is passing Details
2023-11-08 17:53:59 -08:00
Dan Helfman 26bb54a9dd Remove broken link in documentation (#786).
continuous-integration/drone/push Build is passing Details
2023-11-08 10:26:09 -08:00
Dan Helfman 715e2ac127 Add test support for Python 3.12.
continuous-integration/drone/push Build is passing Details
2023-11-07 10:17:55 -08:00
Dan Helfman f39cea4abf Remove additional Python 3.7-isms (#784).
continuous-integration/drone/push Build is passing Details
2023-11-07 10:17:16 -08:00
Dan Helfman 22101bdd49 Drop support for Python 3.7, which has been end-of-lifed (#784).
continuous-integration/drone/push Build is passing Details
2023-11-07 10:11:29 -08:00
Dan Helfman 13cf863d89 Fix tests (#783).
continuous-integration/drone/push Build is passing Details
2023-11-07 10:09:31 -08:00
Dan Helfman dcf25fa041 Upgrade ruamel.yaml dependency to support version 0.18.x (#783).
continuous-integration/drone/push Build is failing Details
2023-11-07 10:00:13 -08:00
Dan Helfman 12b75f9075 Update documentation about logging changes from version 1.8.3 (#665).
continuous-integration/drone/push Build is passing Details
2023-11-06 21:13:46 -08:00
Dan Helfman 9baf06a2f7
Fix typo.
continuous-integration/drone/push Build is passing Details
Typo
2023-11-04 08:56:39 -07:00
tdltdc 56302e22cd
Typo 2023-11-04 15:05:19 +01:00
Dan Helfman 6cc93c4eb9 Fix environment variable interpolation within configured repository paths (#782).
continuous-integration/drone/push Build is passing Details
2023-11-03 21:16:04 -07:00
Dan Helfman 2da43239f6 Fix docs: minor typos (#781).
continuous-integration/drone/push Build is passing Details
Reviewed-on: #781
2023-11-03 00:59:29 +00:00
debuglevel 4beef36d3c Update docs/how-to/inspect-your-backups.md 2023-11-02 23:14:31 +00:00
debuglevel eacfbd742b Typo 2023-11-02 23:13:45 +00:00
debuglevel 82a85986b6 Typo 2023-11-02 22:57:46 +00:00
Dan Helfman ef448e2dd1 Add a "skip_actions" option to skip running particular actions (#701).
continuous-integration/drone/push Build is passing Details
2023-10-31 21:54:41 -07:00
Dan Helfman c3efe1b90e Only parse "--override" values as complex data types when they're for options of those types (#779).
continuous-integration/drone/push Build is passing Details
2023-10-29 19:02:28 -07:00
Dan Helfman d85c1ee216 Correct changelog addition (#779).
continuous-integration/drone/push Build is passing Details
2023-10-29 16:25:40 -07:00
Dan Helfman b47088067c Add a "--match-archives" flag to the "check" action (#779).
continuous-integration/drone/push Build is passing Details
2023-10-29 16:22:39 -07:00
Dan Helfman c5732aa4fc Fix home page CSS layout to prevent overflow at certain window widths (#777).
continuous-integration/drone/push Build is passing Details
2023-10-27 14:12:35 -07:00
Dan Helfman a0323d9d6c Bump version for release.
continuous-integration/drone/tag Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-10-26 22:20:26 -07:00
Dan Helfman 8ad7b473f1 When an archive filter causes no matching archives for the "rlist" or "info" actions, warn (#748).
continuous-integration/drone/push Build is passing Details
2023-10-26 22:12:13 -07:00
Dan Helfman 895a0ccb3c Upgrade to tox 4. (Now a minimum requirement.)
continuous-integration/drone/push Build is passing Details
2023-10-23 17:39:27 -07:00
Dan Helfman 257ab77bea Disallow the "--dry-run" flag with the "borg" action (#774).
continuous-integration/drone/push Build is passing Details
2023-10-23 17:23:04 -07:00
Dan Helfman dccaa4014b
Update systemd service example with better filesystem protection options.
continuous-integration/drone/push Build is passing Details
Merge pull request #78 from Alphix/update-systemd-service
2023-10-15 08:59:39 -07:00
David Härdeman 2f3c0bec5b Update systemd .service example
First, ProtectSystem=strict will make the entire file system hierarchy (except
/dev, /proc/ and /sys) read-only, so separate ReadOnlyPaths= is not necessary.

Second, ProtectHome=tmpfs will not just mount an empty tmpfs on /root, but also
on /home and /run/user. As it's likely quite common to want to backup /home,
this seems like a footgun.

Finally, it's quite likely that borgbackup will want access to root's SSH keys
in order to connect to remote backup servers.

Note that all these options are commented out by default, so this is more of
a documentation change than any real change in functionality.
2023-10-15 11:30:11 +02:00
Dan Helfman 487d8ffd32 Fix normalization of deprecated sections to support empty sections without erroring (#771).
continuous-integration/drone/push Build is passing Details
2023-10-14 13:04:18 -07:00
Dan Helfman 30523a7c89 Update home page example of Healthchecks configuration not to use deprecated config.
continuous-integration/drone/push Build is passing Details
2023-10-11 12:56:21 -07:00
Dan Helfman 77b1907d03 Update Healthchecks deprecation warning message for clarity.
continuous-integration/drone/push Build is passing Details
2023-10-11 12:17:57 -07:00
Dan Helfman 09594c85bf Be more explicit in documentation that you don't have to use an environment variable for passphrases.
continuous-integration/drone/push Build is passing Details
2023-10-10 09:34:55 -07:00
Dan Helfman e07efdf68f Add documentation note about using includes for specifying passphrases (#769).
continuous-integration/drone/push Build is passing Details
2023-10-10 09:16:58 -07:00
Dan Helfman 1fed44f905 Add documentation note about sudo and sudoers "secure_path" option (#757). 2023-10-09 14:15:54 -07:00
Dan Helfman c687dafdd2 Fix a traceback when an invalid command-line flag or action is used (#768).
continuous-integration/drone/push Build is passing Details
2023-10-06 21:00:23 -07:00
Dan Helfman 3eff2c4248 Add Grafana Loki badge to integrations documentation.
continuous-integration/drone/push Build is passing Details
2023-10-05 09:06:06 -07:00
Dan Helfman d94fdb6faf Add apprise logo to integrations in readme (#715).
continuous-integration/drone/push Build is passing Details
Reviewed-on: #765
2023-10-05 15:51:04 +00:00
Pim Kunis a83282faf0 add apprise logo to integrations in readme 2023-10-05 15:38:32 +02:00
Dan Helfman e7169f6fb2 Upgrade certifi test dependency to fix security alert.
continuous-integration/drone/push Build is passing Details
2023-10-04 22:59:15 -07:00
Dan Helfman 9587fc2366 Update Apprise documentation to use sudo for pipx install (#715).
continuous-integration/drone/push Build is passing Details
2023-10-04 22:54:11 -07:00
Dan Helfman 5f06884d5a Fix Apprise/PyYAML end-to-end test breakage (#715).
continuous-integration/drone/push Build encountered an error Details
2023-10-04 22:51:05 -07:00
Dan Helfman f011431463 Apprise hook documentation (#715).
continuous-integration/drone/push Build encountered an error Details
2023-10-04 19:23:53 -07:00
Dan Helfman 9e14f209f1 Merge branch 'main' of ssh://projects.torsion.org:3022/borgmatic-collective/borgmatic
continuous-integration/drone/push Build is failing Details
2023-10-04 14:58:48 -07:00
Dan Helfman 9d34d2eec5 Support for Apprise (#759).
continuous-integration/drone/push Build is failing Details
Reviewed-on: #759
2023-10-04 21:58:20 +00:00
Pim Kunis 7a9625cd44 fix PR comments 2023-10-04 13:19:40 +02:00
Pim Kunis 4763c323d0 add unit tests for apprise hook 2023-10-01 16:59:59 +00:00
Pim Kunis eaa22be3db fix PR comments 2023-10-01 16:59:59 +00:00
Pim Kunis a587e207f9 pin Apprise dependencies for test requirements 2023-10-01 16:59:59 +00:00
Pim Kunis db8079b699 fix typo in setup.py
handle if apprise cannot be imported
2023-10-01 16:59:59 +00:00
Pim Kunis 5a989826a1 convert map to list for apprise function call
fix apprise config schema
remove apprise from required dependencies
2023-10-01 16:59:59 +00:00
Pim Kunis 21f4266273 incorporate PR review comments 2023-10-01 16:59:59 +00:00
Pim Kunis e7252c7545 remove comments about tags 2023-10-01 16:59:59 +00:00
Pim Kunis 86011c8418 default apprise notify type per borgmatic state 2023-10-01 16:59:59 +00:00
Pim Kunis f3295ccb4a add support for apprise 2023-10-01 16:59:59 +00:00
Dan Helfman cacb81f086 Bump version for release. 2023-09-30 13:37:10 -07:00
Dan Helfman 06c2154e6a Build docs regardless of Drone "event" (push, etc.).
continuous-integration/drone/push Build was killed Details
continuous-integration/drone Build is passing Details
2023-09-29 19:49:09 -07:00
Dan Helfman ac1e1a9407 Simplify logging logic (#665).
continuous-integration/drone/push Build was killed Details
continuous-integration/drone Build is passing Details
2023-09-29 14:16:47 -07:00
Dan Helfman 10933fd55b Fix for borgmatic not stopping Borg immediately when the user presses ctrl-C (#761).
continuous-integration/drone/push Build was killed Details
2023-09-27 08:52:00 -07:00
Dan Helfman af422ad705 Add documentation note about upgrading multiple pipx installations of borgmatic.
continuous-integration/drone/push Build was killed Details
2023-09-18 13:46:41 -07:00
Dan Helfman d9d35491fb Fix tense typo.
continuous-integration/drone/push Build is passing Details
2023-09-17 23:25:57 -07:00
Dan Helfman b540e63c0e Updated documentation so "sudo borgmatic" works for pipx borgmatic installations (#757).
continuous-integration/drone Build was killed Details
2023-09-17 22:46:33 -07:00
Dan Helfman 5a56208922 Fix documentation typo.
continuous-integration/drone/push Build is passing Details
2023-09-15 10:18:35 -07:00
Dan Helfman 5912769273 Fix error handling to log command output as one record per line (#754).
continuous-integration/drone/push Build is passing Details
2023-09-14 21:10:52 -07:00
Dan Helfman bac2aabe66 Attempt to unbreak ticket filing.
continuous-integration/drone/push Build is passing Details
2023-09-12 09:50:38 -07:00
Dan Helfman 9f3328781b When "archive_name_format" is not set, filter archives using the default archive name format (#753).
continuous-integration/drone/push Build is passing Details
2023-09-06 23:13:40 -07:00
Dan Helfman 0205748db8 Update documentation to recommend installing/upgrading borgmatic with pipx instead of pip.
continuous-integration/drone/push Build is passing Details
2023-09-04 16:25:10 -07:00
Dan Helfman d0a8251ad2 Add borgmatic version introducing Loki hook to docs (#743).
continuous-integration/drone/push Build is passing Details
2023-08-27 20:30:13 -07:00
Dan Helfman 32019ea8f3 Add documentation for Grafana Loki hook (#743).
continuous-integration/drone/push Build is passing Details
2023-08-25 10:52:00 -07:00
Dan Helfman fa9a061033 Merge branch 'main' of ssh://projects.torsion.org:3022/borgmatic-collective/borgmatic 2023-08-25 09:29:02 -07:00
Dan Helfman b3d2560563 Added support for grafana loki (#743).
continuous-integration/drone/push Build is passing Details
Reviewed-on: #747
2023-08-25 16:28:19 +00:00
Dan Helfman 4b4f56da42 Fix another database -> data source instance (#685). 2023-08-24 14:00:29 -07:00
Dan Helfman b96d1898f7 Prep work for eventual container-dumping hooks: Generalize internal database hook "API" (#685).
continuous-integration/drone/push Build is passing Details
2023-08-24 13:50:10 -07:00
Tobias Hodapp 099a712e53 Added more documentation to the test
Split tests to integration tests
2023-08-24 13:17:42 +02:00
Tobias Hodapp 9e2674ea5a Added unit tests
Removed useless dry run check
2023-08-23 17:17:23 +02:00
Tobias Hodapp 7e419ec995 Fixed spelling errors
Added documentation
Added log messages for dry run
2023-08-22 23:03:14 +02:00
Tobias Hodapp a3edf757ee Added changes of formatting tools 2023-08-22 13:40:05 +02:00
Tobias Hodapp e576403b64 Added support for grafana loki 2023-08-22 03:13:39 +02:00
Dan Helfman 7313430178 Make warning about sections a little more explicit (#721).
continuous-integration/drone/push Build is passing Details
2023-08-19 22:51:20 -07:00
Dan Helfman 962daaa8b9 Bump version for release.
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
2023-08-14 12:54:38 -07:00
Dan Helfman cd51e9c1ea Fix for database "restore" action not actually restore anything (#738).
continuous-integration/drone/push Build is passing Details
2023-08-14 12:43:21 -07:00
Dan Helfman 6dca7c1c15 Add "key export" action to export a copy of the repository key (#345).
continuous-integration/drone/push Build is passing Details
2023-08-07 12:28:39 -07:00
Dan Helfman fd8c56c6be Add brief source code reference documentation.
continuous-integration/drone/push Build is passing Details
2023-08-06 23:44:31 -07:00
Dan Helfman 065057c966
Fix typos.
continuous-integration/drone/push Build is passing Details
Merge pull request #77 from hop/main
2023-08-05 17:19:57 -07:00
Christoph Schindler c04517f843 Fix typos. 2023-08-06 02:16:31 +02:00
Dan Helfman 5d80c366fb Fix "borg create" flags/argument interleaving.
continuous-integration/drone/push Build is passing Details
2023-08-04 20:02:09 -07:00
Dan Helfman 193dd93de2 Fork a MariaDB database hook from the MySQL database hook (#727).
continuous-integration/drone/push Build is passing Details
2023-08-04 13:22:44 -07:00
Dan Helfman 8a94b9e2f1 Mention "store_config_files" in docs (#725).
continuous-integration/drone/push Build is passing Details
2023-08-03 22:11:02 -07:00
Dan Helfman 3c8f6040e2 Bump version for release.
continuous-integration/drone/tag Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-08-03 21:56:26 -07:00
Dan Helfman 14e2a6b89d Omit "--glob-archives" or "--match-archives" Borg flag when its value would be "*" (#734).
continuous-integration/drone/push Build is passing Details
2023-08-02 10:23:22 -07:00
Dan Helfman e607de7df1 Include multiple configuration files with a single "!include" (#732).
continuous-integration/drone/push Build is passing Details
Reviewed-on: #737
2023-08-02 04:46:33 +00:00
Dan Helfman e9bd5f4e1d Add documentation and NEWS link (#732).
continuous-integration/drone/push Build is passing Details
2023-08-01 21:12:49 -07:00
Dan Helfman 175003ff9b Additional test coverage (#732).
continuous-integration/drone/push Build is passing Details
2023-08-01 19:45:01 -07:00
Dan Helfman b8d349d048 Additional test coverage (#732).
continuous-integration/drone/push Build is passing Details
2023-08-01 16:27:53 -07:00
Dan Helfman f6f06551f0 Multiple configuration includes with a single "!include" (#732).
continuous-integration/drone/push Build is passing Details
2023-08-01 14:12:35 -07:00
Dan Helfman 69771fe7ce Add "store_config_files" option to NEWS (#725).
continuous-integration/drone/push Build is passing Details
2023-07-31 14:04:00 -07:00
Dan Helfman c5c3e2e0ce Code formatting (#725). 2023-07-31 14:01:20 -07:00
Dan Helfman 8491b2c416
Feat: optionally disable config bootstrap (#725).
Merge pull request #75 from diivi/feat/disable-config-bootstrap
2023-07-31 14:00:00 -07:00
Divyansh Singh 962267b3c5 add test for checking manifest creation 2023-08-01 02:26:36 +05:30
Divyansh Singh 562f4a281b add test 2023-08-01 02:01:15 +05:30
Divyansh Singh 6b09ca8022 exclude configs too and not just the manifest 2023-08-01 01:51:37 +05:30
Divyansh Singh f2ce2f387f feat: optionally disable config bootstrap 2023-08-01 01:32:06 +05:30
Dan Helfman 782a9bb70a Add new documentation to NEWS (#326, #697).
continuous-integration/drone/push Build is passing Details
2023-07-31 12:19:19 -07:00
Dan Helfman 88adb5b3de
Add docs for database restore params and config bootstrap (#326, #697).
continuous-integration/drone/push Build is passing Details
Merge pull request #74 from diivi/docs/database-restore-params-and-config-bootstrap
2023-07-31 11:56:32 -07:00
Divyansh Singh 59465b256d
Apply suggestions from code review 2023-07-31 23:50:46 +05:30
Divyansh Singh adfb89ee65 improve docs - wittens suggestions 2023-07-29 00:16:04 +05:30
Dan Helfman c11dcdef0a Fix some "borgmatic" capitalization issues.
continuous-integration/drone/push Build is passing Details
2023-07-26 23:38:08 -07:00
Dan Helfman 8a2514915c Fix for Borg's interactive prompt on the "check --repair" action automatically getting answered "NO" (#730).
continuous-integration/drone/push Build is passing Details
2023-07-22 15:25:55 -07:00
Dan Helfman 4d7a2876a5 Fix for "prune" action error when using the "keep_exclude_tags" option in configuration (#728).
continuous-integration/drone/push Build is passing Details
2023-07-22 10:26:52 -07:00
Dan Helfman 309f67e860 Fix documentation comma grammar issues.
continuous-integration/drone/push Build is passing Details
2023-07-18 23:27:45 -07:00
Dan Helfman 0ad7b4f408 Bump version for release.
continuous-integration/drone/tag Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-07-18 22:50:09 -07:00
Divyansh Singh 1f415a24b8 add docs for database restore params and config bootstrap 2023-07-15 12:27:54 +05:30
Dan Helfman 5c941600a6 Clarify database version description.
continuous-integration/drone/push Build is passing Details
2023-07-14 22:25:45 -07:00
Dan Helfman 5e0283b391 Remove broken markdown item.
continuous-integration/drone/push Build is passing Details
2023-07-14 22:24:21 -07:00
Dan Helfman ac0b23901b Satisfy Gitea validation.
continuous-integration/drone/push Build is failing Details
2023-07-14 22:23:41 -07:00
Dan Helfman cde50a75e9 Attempt to add some vertical whitespace for clarity.
continuous-integration/drone/push Build is passing Details
2023-07-14 22:23:06 -07:00
Dan Helfman 521c0eb600 Fix text areas in issue templates.
continuous-integration/drone/push Build is passing Details
2023-07-14 22:17:39 -07:00
Dan Helfman bc5dc2253c Convert issue templates from Markdown to YAML to take advantage of forms.
continuous-integration/drone/push Build is passing Details
2023-07-14 22:16:04 -07:00
Dan Helfman 973ab01afb Disable submitting issues without a template.
continuous-integration/drone/push Build is passing Details
2023-07-14 20:10:20 -07:00
Dan Helfman 9413a19eca Try out multiple Gitea templates. 2023-07-14 20:06:22 -07:00
Dan Helfman 0055de08a4 Merge branch 'main' of ssh://projects.torsion.org:3022/borgmatic-collective/borgmatic
continuous-integration/drone/push Build is passing Details
2023-07-13 20:21:42 -07:00
Dan Helfman 04fb351a11 Order documentation navigation sections.
continuous-integration/drone/push Build is passing Details
2023-07-13 20:20:48 -07:00
Dan Helfman e913cddcd1 Remove configuration sections (#723).
continuous-integration/drone/push Build is passing Details
Reviewed-on: #723
2023-07-14 03:10:51 +00:00
Dan Helfman da78929415 To prevent argument parsing errors on ambiguous commands, drop support for multiple consecutive flag values.
continuous-integration/drone/push Build is passing Details
2023-07-13 19:25:14 -07:00
Dan Helfman 18b3b569d0 Fix broken bootstrap action (#721). 2023-07-12 09:39:45 -07:00
Dan Helfman 054bd52482 Fix incorrect merge from main that broke tests (#721).
continuous-integration/drone/push Build is passing Details
2023-07-11 22:16:42 -07:00
Dan Helfman 2453ecad42 More documentation updates based on section removal (#721).
continuous-integration/drone/push Build is failing Details
2023-07-11 22:10:57 -07:00
Dan Helfman cedf562a7e Merge branch 'main' into remove-sections 2023-07-11 20:05:16 -07:00
Dan Helfman 5b991b88dd Rewrite documentation navigation URLs when being run locally.
continuous-integration/drone/push Build is passing Details
2023-07-11 19:58:49 -07:00
Dan Helfman d2fa205476 Update documentation for section removal (#721).
continuous-integration/drone/push Build is passing Details
2023-07-11 19:42:14 -07:00
Dan Helfman ecd9e62147 Fix last end-to-end database test (#721).
continuous-integration/drone/push Build is passing Details
2023-07-11 00:37:36 -07:00
Dan Helfman 003d4eac93 Remove extra argument (#721).
continuous-integration/drone/push Build is failing Details
2023-07-10 11:41:43 -07:00
Dan Helfman e8dbca9d68 Truncate long command output without assuming an encoded byte string. 2023-07-10 11:16:18 -07:00
Dan Helfman bd9d592560 Truncate long command error output. 2023-07-10 10:20:51 -07:00
Dan Helfman 196a226a7e Add "check_i_know_what_i_am_doing" option to NEWS (#724).
continuous-integration/drone/push Build is passing Details
2023-07-10 09:44:00 -07:00
Dan Helfman a3f47a6418 Remove some sections from tests (#721).
continuous-integration/drone/push Build is failing Details
2023-07-10 09:38:28 -07:00
Dan Helfman d29667ca3c Add a config entry for BORG_CHECK_I_KNOW_WHAT_I_AM_DOING env var (#724).
continuous-integration/drone/push Build is passing Details
Reviewed-on: #724
2023-07-10 16:38:09 +00:00
Yoann Laissus 7d18f59079 Add a config entry for BORG_CHECK_I_KNOW_WHAT_I_AM_DOING env var 2023-07-10 10:36:56 +02:00
Dan Helfman ab351548d2 Fix (some) end-to-end tests (#721).
continuous-integration/drone/push Build is failing Details
2023-07-09 17:40:02 -07:00
Dan Helfman 48b6a1679a Remove sections (#721).
continuous-integration/drone/push Build is failing Details
2023-07-08 23:14:30 -07:00
Dan Helfman 9cafc16052 For "borgmatic borg", pass the repository to Borg via a Borg-supported environment variable (#575).
continuous-integration/drone/push Build is passing Details
2023-07-03 00:08:54 -07:00
Dan Helfman fbbfc684ce Add referral link for Hetzner.
continuous-integration/drone/push Build is passing Details
2023-07-02 22:14:36 -07:00
Dan Helfman 13a37a1d9b Reddit is dead.
continuous-integration/drone/push Build is passing Details
2023-06-30 22:55:47 -07:00
Dan Helfman 9cf27fa4ba Deprecated configuration options warning logging.
continuous-integration/drone/push Build is passing Details
2023-06-29 10:03:36 -07:00
Dan Helfman e2c95327fb Fix an error when dumping a MySQL database and the "exclude_nodump" option is set (#720).
continuous-integration/drone/push Build is passing Details
2023-06-28 09:15:11 -07:00
Dan Helfman f60e97d5bf When merging two configuration files, error gracefully if the two files do not adhere to the same format.
continuous-integration/drone/push Build is passing Details
2023-06-26 16:46:09 -07:00
Dan Helfman 44f9ab95f9 Fix typos (#575).
continuous-integration/drone/push Build is passing Details
2023-06-26 14:37:23 -07:00
Dan Helfman bb6004fc4f Revamp "borg" action to support REPOSITORY and ARCHIVE env vars instead of implicitly injecting repository/archive into the Borg command (#575). 2023-06-26 14:35:07 -07:00
Dan Helfman b242078f54 Fix an error when running "borg key export" through borgmatic (#719).
continuous-integration/drone/push Build is passing Details
2023-06-26 09:30:46 -07:00
Dan Helfman c3004c6090 Some brief documentation on running only checks (#364).
continuous-integration/drone/push Build is passing Details
2023-06-25 22:49:36 -07:00
Dan Helfman b9a11e860d Remove legacy configuration parsing code, no longer needed with upgrade-borgmatic-config gone (#529). 2023-06-25 15:36:25 -07:00
Dan Helfman 37a0a0c421 Bump version for release.
continuous-integration/drone/tag Build is passing Details
2023-06-24 22:23:01 -07:00
Dan Helfman 325b561296 Switch from "init" to "rcreate" for creating repos in end-to-end tests.
continuous-integration/drone/push Build is passing Details
2023-06-24 15:52:20 -07:00
Dan Helfman b62017be4b Fix edge case in which "--config somepath.yaml" followed by an action alias (e.g. init for rcreate) wasn't parsed correctly (#716).
continuous-integration/drone/push Build is passing Details
2023-06-24 15:35:10 -07:00
Dan Helfman 8debcbeaba Remove duplicated tests (#716).
continuous-integration/drone/push Build is failing Details
2023-06-24 14:28:50 -07:00
Dan Helfman 35a11559ac Fix error parsing arguments with multiple verbosity flags (#716).
continuous-integration/drone/push Build is failing Details
2023-06-24 14:10:47 -07:00
Dan Helfman e4e455ee45 Deprecate validate-borgmatic-config in favor of new "config validate" action (#529).
continuous-integration/drone/push Build is passing Details
2023-06-23 10:11:41 -07:00
Dan Helfman 23809e9060 More Docker build fun (#326).
continuous-integration/drone/push Build is passing Details
2023-06-22 15:11:49 -07:00
Dan Helfman bb0dd14f69 Attempt to fix CI test failures (#326).
continuous-integration/drone/push Build was killed Details
2023-06-22 14:55:32 -07:00
Dan Helfman 308c96aeb5 Add comment describing need for dev-CI parity test. 2023-06-22 14:37:08 -07:00
Dan Helfman 62a2f5a1d0 Code formatting.
continuous-integration/drone/push Build is failing Details
2023-06-22 14:25:26 -07:00
Dan Helfman e8c862659c Add missing services to build service configuration and add a test to catch this in the future (#326).
continuous-integration/drone/push Build is failing Details
2023-06-22 14:20:42 -07:00
Dan Helfman 69611681e2 Add database restore overrides to NEWS, add a test, and move some tests (#326).
continuous-integration/drone/push Build is failing Details
2023-06-22 12:40:57 -07:00
Dan Helfman 9e0df595c8 Merge branch 'main' of github.com:borgmatic-collective/borgmatic 2023-06-22 12:29:32 -07:00
Dan Helfman 68d90e1e40
feat: allow restoring to different port/host/username (#326).
Merge pull request #73 from diivi/feat/restore-with-different-hostname-port-username
2023-06-22 12:28:34 -07:00
Dan Helfman 248500c7be Accidentally a word.
continuous-integration/drone/push Build is passing Details
2023-06-22 09:21:06 -07:00
Dan Helfman 3addb60fb8 Actually link to the most recent version.
continuous-integration/drone/push Build is passing Details
2023-06-22 09:13:45 -07:00
Dan Helfman 01fffab898 Clarify that references docs are only for the most recent version of borgmatic.
continuous-integration/drone/push Build is passing Details
2023-06-22 09:12:06 -07:00
Dan Helfman bc93401a70 Codespell fixes.
continuous-integration/drone/push Build is passing Details
2023-06-21 13:14:54 -07:00
Dan Helfman 1b90da5bf1 Deprecate generate-borgmatic-config in favor if new "config generate" action (#529).
continuous-integration/drone/push Build is failing Details
2023-06-21 12:19:49 -07:00
Dan Helfman 803fc25848 Add a test for another edge case (#712).
continuous-integration/drone/push Build is passing Details
2023-06-21 10:47:53 -07:00
Dan Helfman 248f82d6f6 Fix for another subaction argument-parsing edge case (#712).
continuous-integration/drone/push Build is passing Details
2023-06-21 10:41:32 -07:00
Divyansh Singh 87c6e5b349 make sure restore params in config aren't used when cli args are supplied 2023-06-21 00:03:07 +05:30
Dan Helfman 147516ae3f Remove additional upgrade-borgmatic-config code (#529).
continuous-integration/drone/push Build is passing Details
2023-06-20 09:41:26 -07:00
Dan Helfman b10aee3070 Remove upgrade-borgmatic-config command for upgrading borgmatic 1.1.0 INI-style configuration (#529).
continuous-integration/drone/push Build is failing Details
2023-06-19 23:17:59 -07:00
Dan Helfman 6098005f5d Fix an error when "data" check time files are accessed without getting upgraded first (#711, #713).
continuous-integration/drone/push Build is passing Details
2023-06-19 23:07:57 -07:00
Dan Helfman 7b8be800a4 Refactor arguments parsing to fix bootstrap action CLI issues (#712).
continuous-integration/drone/push Build is passing Details
2023-06-19 16:18:47 -07:00
Divyansh Singh 1a21eb03cd add tests for all databases 2023-06-20 00:52:01 +05:30
Divyansh Singh e2d82e9bba actually test port restores 2023-06-19 01:10:01 +05:30
Divyansh Singh 384182172a add unit tests for cases when cli/config restore args are used 2023-06-18 06:29:11 +05:30
Divyansh Singh 9016dcc418 all e2e tests 2023-06-18 05:47:35 +05:30
Divyansh Singh e53dd3da87 fix witten reported mysql error 2023-06-17 22:58:59 +05:30
Divyansh Singh 6c87608548 add tests for password logic 2023-06-17 00:47:15 +05:30
Dan Helfman ee2ebb79b8 Find sub-actions for an action without an isinstance() check.
continuous-integration/drone/push Build is passing Details
2023-06-16 10:57:01 -07:00
Divyansh Singh 89602d1614 pass all existing tests (and formatting) 2023-06-16 15:14:00 +05:30
Dan Helfman c294e78715 Use absolute paths when storing configuration files in an archive for later bootstrapping (#697).
continuous-integration/drone/push Build is passing Details
2023-06-15 21:45:43 -07:00
Dan Helfman 9152fed249 Add a documentation troubleshooting note for MySQL/MariaDB authentication errors (#399).
continuous-integration/drone/push Build is passing Details
2023-06-15 14:55:57 -07:00
Divyansh Singh 8389851f2f fix bug where port becomes truthy when none is converted to str 2023-06-15 23:34:50 +05:30
Dan Helfman bbc7f0596c Fix Bash completion for sub-actions like "borgmatic config bootstrap" (#697 follow-on work).
continuous-integration/drone/push Build is passing Details
2023-06-15 10:55:31 -07:00
Divyansh Singh 82d851d891 add argument for restore path 2023-06-15 23:05:53 +05:30
Divyansh Singh 62b6f13299 add restore-path support for sqlite 2023-06-15 23:02:09 +05:30
Divyansh Singh b7423c488e refactor password assignment logic 2023-06-15 22:54:06 +05:30
Dan Helfman 1d7c7eaaa7 Add sample systemd user serivce for running borgmatic as a non-root user (#669).
continuous-integration/drone/push Build is failing Details
2023-06-14 14:57:57 -07:00
Divyansh Singh a9386b7a87 add mongodb support, and sqlite restore path (config option only) 2023-06-15 02:18:24 +05:30
Divyansh Singh 205e5b1524 mysql support 2023-06-15 01:47:46 +05:30
Divyansh Singh 67f4d43aec witten review 2023-06-15 01:37:18 +05:30
Dan Helfman e15bec30e6 Mention some hang edge cases in database limitations (#710).
continuous-integration/drone/push Build is passing Details
2023-06-13 23:34:58 -07:00
Divyansh Singh 230cf6adc4 support command line args for hostname port username password 2023-06-14 00:11:19 +05:30
Divyansh Singh 8e8e64d920 add no-owner and refactor 2023-06-13 23:42:50 +05:30
Divyansh Singh f558cb3156 feat: allow restoring to different port/host/username 2023-06-12 21:54:39 +05:30
Dan Helfman 41924f2400 A little activism.
continuous-integration/drone/push Build is passing Details
2023-06-11 09:50:57 -07:00
Dan Helfman 670bdffb3c Code formatting.
continuous-integration/drone/push Build is passing Details
2023-06-10 19:25:49 -07:00
Dan Helfman 691d4f887a Fix incorrect log message (#697).
continuous-integration/drone/push Build is failing Details
2023-06-10 16:02:03 -07:00
Dan Helfman beb899d6fb Make user-facing manifest loading error messages a little friendlier (#697).
continuous-integration/drone/push Build is failing Details
2023-06-10 15:50:11 -07:00
Dan Helfman 0f9756e739 Fix failing test and add "bootstrap" action to CLI reference docs (#697).
continuous-integration/drone/push Build is passing Details
2023-06-10 15:17:18 -07:00
Dan Helfman d84f1ec616 Add bootstrap action to NEWS and make post-PR tweaks (#697).
continuous-integration/drone/push Build is failing Details
2023-06-10 14:52:00 -07:00
Dan Helfman ef409ad23c
Store configs used to create an archive in the archive and add borgmatic bootstrap (#697).
Merge pull request #71 from diivi/feat/store-config-in-archive
2023-06-10 14:39:53 -07:00
Divyansh Singh d370ff958d mock expand directories thrice 2023-06-10 01:05:34 +05:30
Divyansh Singh 197920d9ef improve tests and some docstrings. 2023-06-09 17:31:57 +05:30
Divyansh Singh 425f260a22 test parser merging 2023-06-09 04:15:18 +05:30
Divyansh Singh 3315555d06 cleaner test 2023-06-09 00:21:41 +05:30
Divyansh Singh 6475345a8f attempt to test parse_subparser_arguments 2023-06-08 01:02:43 +05:30
Divyansh Singh f90d30e0e1 remove duplicate comments 2023-06-08 00:08:39 +05:30
Divyansh Singh 8384eaefb1 reformat 2023-06-08 00:07:36 +05:30
Divyansh Singh dcb90bba50 some tests remaining 2023-06-07 23:56:02 +05:30
Divyansh Singh dc56fd33a0 formatting 2023-06-07 01:47:16 +05:30
Divyansh Singh 2d761dd86b coverage at 100 2023-06-07 01:43:01 +05:30
Divyansh Singh f82631e3bb tests for arguments.py 2023-06-07 00:56:19 +05:30
Divyansh Singh 4b024daae0 pass all tests with wittens recommendation 2023-06-06 23:37:09 +05:30
Divyansh Singh 6a1d1a2e59 fix indentation error that caused too many test failures 2023-06-05 20:31:09 +05:30
Divyansh Singh 206a9c9607 edit schema comments and work on witten review 2023-06-05 20:05:10 +05:30
Dan Helfman a6425b8867 Fix moved Arch Linux borgmatic URL.
continuous-integration/drone/push Build is passing Details
2023-06-04 22:21:16 -07:00
Dan Helfman b5d9398910 Stop uploading GPG signatures to pypi since it no longer supports them.
continuous-integration/drone/push Build is passing Details
2023-06-03 22:37:46 -07:00
Dan Helfman a185eb73b0 Fix GitHub release script now that "master" has been renamed to "main".
continuous-integration/drone/push Build is passing Details
2023-06-03 22:26:49 -07:00
Dan Helfman e80f27f922 Bump version for release.
continuous-integration/drone/tag Build is passing Details
2023-06-03 22:14:21 -07:00
Dan Helfman 1a5b3c9e4e Add Fedora schema loading fix to NEWS (#703).
continuous-integration/drone/push Build is passing Details
2023-06-03 22:07:24 -07:00
Dan Helfman b3f70434df Fix error loading configuration schema on Fedora Linux (#703).
continuous-integration/drone/push Build is failing Details
Reviewed-on: #702
2023-06-04 05:04:41 +00:00
Felix Kaechele c61d63b235 Use open() to test for file existance and readability
Signed-off-by: Felix Kaechele <felix@kaechele.ca>
2023-06-04 00:54:29 -04:00
Felix Kaechele ba0899660d Verify that schema path exists before returning it
Signed-off-by: Felix Kaechele <felix@kaechele.ca>
2023-06-03 23:42:20 -04:00
Felix Kaechele 15cabb93ca Drop importlib_metadata entirely
The fallback option using the dirname of the config module location
seems to be more robust in a number of cases.

Signed-off-by: Felix Kaechele <felix@kaechele.ca>
2023-06-03 23:42:20 -04:00
Felix Kaechele ce6daff12f Fix importlib.metadata.files workaround
Some distributions, such as Fedora, do not install the RECORDS file as
part of a package's dist-info. As a result importlib.metadata.files will
return None.

Use the workaround for these cases as well.

Signed-off-by: Felix Kaechele <felix@kaechele.ca>
2023-06-03 23:42:20 -04:00
Dan Helfman caf654366c Document work-around for colons in YAML strings (#708).
continuous-integration/drone/push Build is passing Details
2023-06-03 10:19:34 -07:00
Divyansh Singh bb60b25399 merge subparsers and refactor 2023-06-02 02:04:35 +05:30
Divyansh Singh 74aa28e027 support more flags 2023-06-01 16:53:34 +05:30
Dan Helfman 4f49b345af NEWS wording fix for clarity (#706).
continuous-integration/drone/push Build is passing Details
2023-05-30 23:21:55 -07:00
Dan Helfman 1784ca5910 Fix "check" action error when repository and archive checks are configured but the archive check gets skipped due to the configured frequency (#704).
continuous-integration/drone/push Build is passing Details
2023-05-30 23:19:33 -07:00
Dan Helfman 8f4cce5fa5 Make dev docs message stand out a little more.
continuous-integration/drone/push Build is failing Details
2023-05-30 22:30:06 -07:00
Dan Helfman 518aeabb2a Document verbosity levels (#484).
continuous-integration/drone/push Build is failing Details
2023-05-30 22:25:27 -07:00
Dan Helfman 341bd4118d Fix "--archive latest" on "list" and "info" actions only working on the first of multiple configured repositories (#706).
continuous-integration/drone/push Build is failing Details
2023-05-30 16:53:55 -07:00
Dan Helfman b222f6a60b Mention new verbosity level to NEWS (#484).
continuous-integration/drone/push Build is failing Details
2023-05-30 15:52:49 -07:00
Dan Helfman c0aaba6891 Add option to disable syslog output (#484).
continuous-integration/drone/push Build is failing Details
Reviewed-on: #675
2023-05-30 20:03:56 +00:00
Soumik Dutta a7f81d538d nit changes
- help strings in borgmatic commands
- test fixes in test_logger and test_borgmatic

Signed-off-by: Soumik Dutta <shalearkane@gmail.com>
2023-05-29 01:09:00 +05:30
Divyansh Singh 4c60bf84d7 extract config files 2023-05-28 01:36:32 +05:30
Divyansh Singh dbb778a4d6 finish parsing and add error for empty config subcommand 2023-05-26 22:44:31 +05:30
Divyansh Singh f4a169fdf3
Merge pull request #2 from witten/feat/store-config-in-archive 2023-05-26 21:29:18 +05:30
Soumik Dutta 3d41ed3a34 add test to check that log_file is disabled
if logging is disabled

Signed-off-by: Soumik Dutta <shalearkane@gmail.com>
2023-05-26 09:58:53 +05:30
Soumik Dutta 0283f9ae2a fix help string
Signed-off-by: Soumik Dutta <shalearkane@gmail.com>
2023-05-26 08:57:12 +05:30
Soumik Dutta d556a23f97 update borgmatic tests
Signed-off-by: Soumik Dutta <shalearkane@gmail.com>
2023-05-26 08:57:12 +05:30
Soumik Dutta f98d07e8d8 fix logger test
Signed-off-by: Soumik Dutta <shalearkane@gmail.com>
2023-05-26 08:57:12 +05:30
Soumik Dutta 09f59ad97d disable monitoring hooks if monitoring_log_level is set to DISABLED
Signed-off-by: Soumik Dutta <shalearkane@gmail.com>
2023-05-26 08:57:12 +05:30
Soumik Dutta 24be6272ed add test for logger
Signed-off-by: Soumik Dutta <shalearkane@gmail.com>
2023-05-26 08:57:12 +05:30
Soumik Dutta 5a9bb4b97f update help strings
Signed-off-by: Soumik Dutta <shalearkane@gmail.com>
2023-05-26 08:57:12 +05:30
Soumik Dutta 6a2eb1f157 make value of disabled level higher
so that no other log has higher priority

Signed-off-by: Soumik Dutta <shalearkane@gmail.com>
2023-05-26 08:57:12 +05:30
Soumik Dutta 99473c30a8 disable sending logs in Healthchecks
Signed-off-by: Soumik Dutta <shalearkane@gmail.com>
2023-05-26 08:57:12 +05:30
Soumik Dutta f512d1e460 add verbosity level -2
Signed-off-by: Soumik Dutta <shalearkane@gmail.com>
2023-05-26 08:57:12 +05:30
Dan Helfman 96adee444b Potential fix for nested subparsers not parsing correctly. 2023-05-25 15:03:15 -07:00
Divyansh Singh 8b7996dfda removed parents and used reversed remaining_args 2023-05-26 01:07:11 +05:30
Divyansh Singh 2241de11c0 start work on borgmatic config bootstrap command 2023-05-26 00:26:13 +05:30
Dan Helfman 84c21b062f Fix incorrect argument ordering (#659).
continuous-integration/drone/push Build is passing Details
2023-05-23 16:55:40 -07:00
Dan Helfman 76138faaf3 Add integration test for mount action (#659). 2023-05-23 14:49:04 -07:00
Dan Helfman 9299841a5b Add date-based matching flags to NEWS (#659).
continuous-integration/drone/push Build is failing Details
2023-05-23 14:30:16 -07:00
Dan Helfman 35b5c62ca6 Add Borg 2 date-based matching flags for archive selection (#659).
continuous-integration/drone/push Build is failing Details
Reviewed-on: #661
2023-05-23 21:26:17 +00:00
Dan Helfman 05b989347c Upgrade requests test requirement (security).
continuous-integration/drone/push Build is passing Details
2023-05-23 08:43:45 -07:00
Chirag Aggarwal 00e9bb011a test should mock out make_flags_from_arguments
Signed-off-by: Chirag Aggarwal <thechiragaggarwal@gmail.com>
2023-05-20 09:23:09 -04:00
Dan Helfman 833796d1c4 Add archive check probing logic tweak to NEWS (#688).
continuous-integration/drone/push Build is passing Details
2023-05-17 08:48:54 -07:00
Divyansh Singh ee32b076eb update tests and formatting 2023-05-16 23:17:35 +05:30
Dan Helfman e3425f48be Instead of taking the first check time found, take the maximum value (#688)
continuous-integration/drone/push Build is passing Details
2023-05-16 10:20:52 -07:00
Dan Helfman 79b094d035 Bump version for release.
continuous-integration/drone/tag Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-05-16 09:59:09 -07:00
Dan Helfman b45e45f161 Partial conversion of showing repository labels in logs instead of paths (part of #635).
continuous-integration/drone/push Build is running Details
2023-05-16 09:36:50 -07:00
Divyansh Singh b10148844b change config_paths var name to used_config_paths to avoid collisions 2023-05-16 14:00:23 +05:30
Dan Helfman ba845d4008 Codespell saves the day.
continuous-integration/drone/push Build is passing Details
2023-05-15 23:25:13 -07:00
Dan Helfman 645d29b040 Fix archive checks being skipped even when particular archives haven't been checked recently (#688).
continuous-integration/drone/push Build is failing Details
2023-05-15 23:17:45 -07:00
Divyansh Singh 49b4d371ce create and add content to borgmatic-manifest.json 2023-05-16 00:24:19 +05:30
Divyansh Singh 1bc7bb4971 feat: store configs used to create an archive in the archive 2023-05-15 23:04:42 +05:30
Dan Helfman e66e449c3b Merge branch 'main' of ssh://projects.torsion.org:3022/borgmatic-collective/borgmatic
continuous-integration/drone/push Build is passing Details
2023-05-14 12:51:23 -07:00
Dan Helfman 8eb05b840a Log a warning when "borgmatic borg" is run with an action that borgmatic natively supports (#694). 2023-05-14 09:59:28 -07:00
Dan Helfman f0fc638284 Docs: add Gentoo Linux to other ways to install (#696).
continuous-integration/drone/push Build is passing Details
Reviewed-on: #696
2023-05-13 16:33:11 +00:00
ennui c6126a9226 Docs: add Gentoo Linux to other ways to install 2023-05-13 11:22:47 +00:00
ennui 62b11ba16b Docs: add Gentoo Linux to other ways to install 2023-05-13 11:20:47 +00:00
Dan Helfman 403ae0f698 Clarify configuration comment about source_directories also accepting files (#693).
continuous-integration/drone/push Build is passing Details
2023-05-09 10:14:03 -07:00
Dan Helfman 92a2230a07 Add support for logging each log line as a JSON object via global "--log-json" flag (#680).
continuous-integration/drone/push Build is passing Details
2023-05-08 23:00:49 -07:00
Dan Helfman b3b08ee6d7 Fix error in "borgmatic restore" action when the configured repository path is relative (#691).
continuous-integration/drone/push Build is passing Details
2023-05-07 21:21:35 -07:00
Dan Helfman 15ef37d89f Add test coverage for exact_options_completion() raising (#686).
continuous-integration/drone/push Build is passing Details
2023-05-06 16:25:26 -07:00
Dan Helfman e84bac29e5 Remove value type for compatibility with Python 3.8 (#686). 2023-05-06 16:18:37 -07:00
Dan Helfman 1a956e8b05 Add fish shell completions to NEWS (#686).
continuous-integration/drone/push Build is failing Details
2023-05-06 16:04:15 -07:00
Dan Helfman 4aae7968b8
Add fish shell completions support (#686).
Merge pull request #70 from isaec/feat/fish-completions
2023-05-06 16:00:25 -07:00
Isaac 66964f613c
formatting! 2023-05-06 15:56:50 -07:00
Isaac 614c1bf2e4
rename test to make function under test clearer 2023-05-06 15:52:42 -07:00
Isaac aa770b98f9
follow unit test module convention 2023-05-06 15:50:37 -07:00
Isaac 453b78c852
drop messages 2023-05-06 15:49:07 -07:00
Isaac 0657106893
clarify dedent test name 2023-05-06 15:46:15 -07:00
Isaac 43c532bc57
add test for dedent strip 2023-05-06 11:51:35 -07:00
Isaac efb81fc2c1
rename last arg helper function to current arg for clarity 2023-05-06 11:42:32 -07:00
Isaac c8f4344f89
add more justification to checks 2023-05-06 11:39:02 -07:00
Isaac a047f856a1
tweak docstring, add comment 2023-05-06 11:37:38 -07:00
Isaac d732059979
fix rotted comments 2023-05-06 11:32:10 -07:00
Isaac ccfdd6806f
test the value of completions 2023-05-06 11:29:14 -07:00
Isaac aa564ac5fe
fix the error thrown, unit test for it, and add string explanations 2023-05-06 11:25:15 -07:00
Isaac 77dbb5c499
create way for test cases to be shared 2023-05-06 11:16:45 -07:00
Isaac e623f401b9
write more unit tests 2023-05-06 10:56:54 -07:00
Isaac 372622fbb1
add more doccomments, drop a check 2023-05-06 10:46:27 -07:00
Isaac 469e0ccace
create doccomments, start writing unit tests 2023-05-06 10:42:06 -07:00
Isaac 59a6ce1462
replace double quotes with single quotes 2023-05-05 00:03:43 -07:00
Isaac 5a7a1747f2
add safety check to avoid infinite cat hang 2023-05-05 00:01:45 -07:00
Isaac b557d635fd
async validity check 2023-05-04 23:57:37 -07:00
Isaac d59b9b817f
support required actions 2023-05-04 23:44:54 -07:00
Isaac 16ac4824a5
handle typed without default params 2023-05-04 23:42:04 -07:00
Isaac 3592ec3ddf
dont show deprecated options 2023-05-04 23:32:09 -07:00
Isaac 8f3039be23
handle the expanding filters better 2023-05-04 23:23:29 -07:00
Isaac b4a38d8be9
fix flag showing up for paths 2023-05-04 23:06:11 -07:00
Isaac d962376a9d
refactor to only show specific options if possible 2023-05-04 21:58:30 -07:00
Isaac 193731a017
rename function 2023-05-04 21:14:48 -07:00
Isaac bbc3e9d717
show possible choices 2023-05-04 21:12:24 -07:00
Isaac 639e88262e
create working file completion 2023-05-04 20:17:26 -07:00
Isaac f12a10d888
start work on conditional file completion 2023-05-04 19:50:49 -07:00
Isaac 28efc85660
rearrange to improve legability of the file 2023-05-04 18:11:13 -07:00
Isaac f1fd2e88dd
drop blank completion 2023-05-04 13:49:29 -07:00
Isaac 700f8e9d9c
replace .format with fstring 2023-05-04 13:39:48 -07:00
Isaac f04036e4a7
use fstring to produce completion lines 2023-05-04 13:33:21 -07:00
Isaac 062453af51
replace actionStr with action_name 2023-05-04 13:29:25 -07:00
Isaac b7fe2a5031
lowercase fish in docs 2023-05-04 13:27:57 -07:00
Isaac ca689505e5
add e2e fish test 2023-05-04 13:27:00 -07:00
Isaac 9ff5ea5240
add a unit test, fix isort and black 2023-05-04 13:22:09 -07:00
Dan Helfman 359afe5318 Error if --list is used with --json for create action (#680).
continuous-integration/drone/push Build is passing Details
2023-05-03 17:16:36 -07:00
Dan Helfman 0b397a5bf9 Fix borgmatic error when not finding the configuration schema for certain "pip install --editable" development installs (#687).
continuous-integration/drone/push Build is passing Details
2023-04-30 16:24:10 -07:00
Dan Helfman a60d7fd173 Run "borgmatic borg" action without capturing output so interactive prompts and flags like "--progress" still work.
continuous-integration/drone/push Build is passing Details
2023-04-30 15:43:41 -07:00
Isaac f7e4024fca
add to readme 2023-04-28 14:02:06 -07:00
Isaac 98e3a81fcf
allow file completions as applicable 2023-04-28 12:42:26 -07:00
Isaac 9c77ebb016
continue deduping 2023-04-28 12:15:01 -07:00
Isaac 23f478ce74
use less completion lines 2023-04-28 12:13:08 -07:00
Isaac d265b6ed6f
add comments in generated files 2023-04-28 11:57:16 -07:00
Dan Helfman 77c3161c77 Fix canonical home link in README.
continuous-integration/drone/push Build is passing Details
2023-04-28 08:36:03 -07:00
Isaac 2e658cfa56
only allow one parser 2023-04-27 21:57:50 -07:00
Isaac 412d18f218
show sub options 2023-04-27 21:31:53 -07:00
Isaac 8060586d8b
fix the script and drop unneeded options 2023-04-27 20:05:17 -07:00
Isaac 25b3db72a0
make more precise, fix the version check fn 2023-04-27 19:58:22 -07:00
Isaac 5678f3a96e
basic working version 2023-04-27 19:44:11 -07:00
Isaac 28b152aedd
make upgrade message a template 2023-04-27 19:31:42 -07:00
Isaac 0009471f67
start work on completion 2023-04-27 18:46:13 -07:00
jetchirag a62ac42cca Merge branch 'main' into borg2-archive-flags 2023-04-27 16:57:29 +00:00
Chirag Aggarwal 68ee9687f5 Added tests for all subcommands and used black formatter
Signed-off-by: Chirag Aggarwal <thechiragaggarwal@gmail.com>
2023-04-27 22:27:23 +05:30
Chirag Aggarwal 32395e47f9 Added duplicate flags test for prune
Signed-off-by: Chirag Aggarwal <thechiragaggarwal@gmail.com>
2023-04-24 20:49:41 +05:30
Chirag Aggarwal 8aaba9bb0a Added new flags to prune test for review
Signed-off-by: Chirag Aggarwal <thechiragaggarwal@gmail.com>
2023-04-24 20:43:34 +05:30
Chirag Aggarwal 96aca4f446 Updated existing tests to use new parameters
Signed-off-by: Chirag Aggarwal <thechiragaggarwal@gmail.com>
2023-04-24 20:24:41 +05:30
Dan Helfman 22b84a2fea Switch to Docker Compose for dev-docs script, so podman-docker is no longer needed for Podman users.
continuous-integration/drone/push Build is passing Details
2023-04-22 10:07:40 -07:00
Dan Helfman 5962fd473e Another try. Backing out psql error changes (#678).
continuous-integration/drone/push Build is passing Details
2023-04-21 10:34:50 -07:00
Dan Helfman 7e64f415ba Attempt to fix failing end-to-end database test that only fails in CI.
continuous-integration/drone/push Build is failing Details
2023-04-21 10:03:29 -07:00
Dan Helfman ae12ccd8e6 And fixing again...
continuous-integration/drone/push Build is failing Details
2023-04-21 09:31:37 -07:00
Dan Helfman 3cefeaa229 Fix end-to-end test command-line syntax.
continuous-integration/drone/push Build was killed Details
2023-04-21 09:30:08 -07:00
Dan Helfman 71b75800cd Get more verbose in the end-to-end test restore.
continuous-integration/drone/push Build is failing Details
2023-04-20 23:32:57 -07:00
Dan Helfman 9ca31530a0 Add missing test for check_all_source_directories_exist() raising.
continuous-integration/drone/push Build is failing Details
2023-04-20 23:15:22 -07:00
Dan Helfman b555fcb956 Add "source_directories_must_exist" expansion fix to NEWS (#682). 2023-04-20 23:08:21 -07:00
Dan Helfman 5829196b70 Expand source directories when checking for existence (#682).
continuous-integration/drone/push Build is failing Details
Reviewed-on: #683
2023-04-21 06:05:59 +00:00
Jesse Johnson a14870ce48 Expand source directories when checking for existence (#682). 2023-04-21 05:52:04 +00:00
Dan Helfman ee5c25f3bd Add additional tests for PostgreSQL hook fixes (#678).
continuous-integration/drone/push Build is failing Details
2023-04-20 21:44:42 -07:00
Dan Helfman da0f5a34f2 Fix multiple bugs in PostgreSQL hook (#678).
continuous-integration/drone/push Build is failing Details
Reviewed-on: #677
2023-04-21 04:05:22 +00:00
Dan Helfman 065be1d9d4 More inclusive language.
continuous-integration/drone/push Build is passing Details
2023-04-20 14:28:04 -07:00
Dan Helfman f2f6fb537a !!!
continuous-integration/drone/push Build is passing Details
2023-04-20 14:19:34 -07:00
Dan Helfman 7ff994a964 🤦
continuous-integration/drone/push Build was killed Details
2023-04-20 13:56:12 -07:00
Dan Helfman 08edecacae WTF?!
continuous-integration/drone/push Build was killed Details
2023-04-20 13:55:37 -07:00
Dan Helfman 1e03046d9a *Seriously?*
continuous-integration/drone/push Build is failing Details
2023-04-20 13:50:26 -07:00
Dan Helfman c9bf52ee45 Sigh again.
continuous-integration/drone/push Build was killed Details
2023-04-20 13:46:49 -07:00
Dan Helfman f947525fca ?
continuous-integration/drone/push Build was killed Details
2023-04-20 13:45:26 -07:00
Dan Helfman 7f7b89d79c Trying a different approach: Ditching Podman-in-Podman.
continuous-integration/drone/push Build is failing Details
2023-04-20 12:03:51 -07:00
Dan Helfman 499e42df35 😭
continuous-integration/drone/push Build was killed Details
2023-04-20 11:58:06 -07:00
Dan Helfman 4302a07c9b WTF.
continuous-integration/drone/push Build was killed Details
2023-04-20 11:53:52 -07:00
Dan Helfman 1721c05d2e Yet more.
continuous-integration/drone/push Build was killed Details
2023-04-20 11:52:23 -07:00
Dan Helfman 8a31c27078 To see what sticks.
continuous-integration/drone/push Build was killed Details
2023-04-20 11:50:25 -07:00
Dan Helfman d6e1cef356 Throwing stuff at the wall.
continuous-integration/drone/push Build was killed Details
2023-04-20 11:49:43 -07:00
Dan Helfman f82bf619ff More.
continuous-integration/drone/push Build is failing Details
2023-04-20 11:41:35 -07:00
Dan Helfman 02eeca1fc2 Hmm.
continuous-integration/drone/push Build is failing Details
2023-04-20 11:36:30 -07:00
Dan Helfman 4e78cf1b95 ಠ_ಠ
continuous-integration/drone/push Build was killed Details
2023-04-20 11:33:15 -07:00
Dan Helfman 9e9a7c50e5 😊🔫
continuous-integration/drone/push Build was killed Details
2023-04-20 11:30:30 -07:00
Dan Helfman 51bc53e5ca Whee.
continuous-integration/drone/push Build is failing Details
2023-04-20 11:24:59 -07:00
Dan Helfman b85538c54c Double sigh.
continuous-integration/drone/push Build is failing Details
2023-04-20 11:11:49 -07:00
Dan Helfman bb5028e484 Sigh.
continuous-integration/drone/push Build is failing Details
2023-04-20 11:11:08 -07:00
Dan Helfman 53ee0fcfad Another attempt at Podman-in-Podman incantations.
continuous-integration/drone/push Build is failing Details
2023-04-20 11:06:15 -07:00
Dan Helfman 5f8c79dd16 Attempt to get Podman-in-Podman builds working.
continuous-integration/drone/push Build is failing Details
2023-04-20 10:50:44 -07:00
Dan Helfman 0a6f5452f4 Fix broken Podman image name.
continuous-integration/drone Build was killed Details
continuous-integration/drone/push Build is failing Details
2023-04-19 23:16:15 -07:00
Dan Helfman 269fac074b Attempt to use Podman-in-Podman for building docs instead of Docker-in-Podman.
continuous-integration/drone/push Build encountered an error Details
2023-04-19 23:14:51 -07:00
Dan Helfman 3b21ce4ce8 Rename "master" development branch to "main" to use more inclusive language (#684).
continuous-integration/drone/push Build is failing Details
2023-04-19 21:43:08 -07:00
Dan Helfman 8bb7631f50 Fix missing mock in unit test.
continuous-integration/drone/push Build is failing Details
2023-04-19 21:22:51 -07:00
Dan Helfman 9f5769f87b Make docs/schema a little more container agnostic / less Docker specific.
continuous-integration/drone/push Build is passing Details
continuous-integration/drone Build is passing Details
2023-04-16 15:41:17 -07:00
Dan Helfman 991e08f16d Add Unraid borgmatic installation link to docs.
continuous-integration/drone/push Build is passing Details
2023-04-15 09:13:13 -07:00
Chirag Aggarwal 1ee56805f1 Merge remote-tracking branch 'upstream/master' into borg2-archive-flags 2023-04-15 17:29:20 +05:30
Dan Helfman 25506b8d2c Backing out upgrade of end-to-end test packages, because apparently we can't have nice things.
continuous-integration/drone/push Build is passing Details
2023-04-14 23:47:51 -07:00
Dan Helfman 28e62d824b Upgrade end-to-end test packages.
continuous-integration/drone/push Build is failing Details
2023-04-14 23:28:07 -07:00
Dan Helfman 7ee37a890e Fix broken end-to-end tests by no longer using an editable package there, a work-around for https://github.com/pypa/packaging-problems/issues/609
continuous-integration/drone/push Build is passing Details
2023-04-14 23:22:07 -07:00
Dan Helfman 8cb5a42a9e Drop deprecated pkg_resources in favor of importlib.metadata and packaging.
continuous-integration/drone/push Build is failing Details
2023-04-14 21:21:25 -07:00
Dan Helfman 5dbb71709c Upgrade test requirements and code style requirements. Auto-reformat code accordingly.
continuous-integration/drone/push Build is passing Details
2023-04-14 19:35:24 -07:00
Dan Helfman 1c67db5d62 Add documentation for "borgmatic restore --schema" (#375).
continuous-integration/drone/push Build is passing Details
2023-04-14 16:40:58 -07:00
Dan Helfman 96d4a8ee45 Add "borgmatic restore --schema" flag to NEWS (#375).
continuous-integration/drone/push Build is passing Details
2023-04-14 16:33:06 -07:00
Dan Helfman 81e167959b
feat: restore specific schemas (#375).
Merge pull request #67 from diivi/feat/restore-specific-schemas
2023-04-14 16:26:25 -07:00
Divyansh Singh f273e82d74 add tests 2023-04-15 02:57:51 +05:30
Jakub Jirutka 17f122bfe5 Use psql instead of pg_restore when format is "plain"
pg_restore: error: input file appears to be a text format dump. Please use psql.
2023-04-14 17:38:19 +02:00
Jakub Jirutka f0f43174c6 Swap if-else in restore_database_dump in postgresql hook for cleanliness 2023-04-14 17:38:19 +02:00
Jakub Jirutka dfccc1b94a Exit on error when restoring all PostgreSQL databases
"--set ON_ERROR_STOP=on" is equivalent to "--exit-on-error" in
pg_restore.
2023-04-14 17:38:18 +02:00
Jakub Jirutka 195024e505 Fix psql_command and pg_restore_command to accept command with arguments
These commands are executed without `shell=True`, so the subprocess
module treats e.g. "docker exec my_pg_container psql" as a single command
(resulting in Errno 2 "No such file or directory") instead of a command
with arguments.
2023-04-14 17:37:38 +02:00
Jakub Jirutka 19a00371f5 Run "psql" with "--no-psqlrc"
Some settings in user's .psqlrc, e.g. "linestyle unicode", may break the
CSV output. "--no-psqlrc" tells psql to not read startup file.

This is not necessary for the analyze_command and restore_command (with
all_databases), but it's generally recommended when running psql from a
script.
2023-04-14 17:37:37 +02:00
Jakub Jirutka 874fba7672 Fix PostgreSQL hook not using "psql_command" for list when dumping "all" 2023-04-14 15:13:49 +02:00
Dan Helfman 50b0a9ce38 Remove newline at end of file.
continuous-integration/drone/push Build is failing Details
2023-04-13 19:13:50 -07:00
Dan Helfman 8802f6888e Fix "TypeError: 'module' object is not callable" in test_commands.py' (#676).
continuous-integration/drone/push Build is failing Details
Reviewed-on: #676
2023-04-14 02:12:58 +00:00
polyzen ebe5c5e839 Fix "TypeError: 'module' object is not callable" in test_commands.py 2023-04-14 01:01:31 +00:00
Dan Helfman 613f6c602c Bump version for release.
continuous-integration/drone/tag Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-04-13 15:12:19 -07:00
Dan Helfman 4a94c2c9bf Selectively omit list values when including configuration files (#672).
continuous-integration/drone/push Build is passing Details
2023-04-13 14:39:36 -07:00
Dan Helfman 08843d51d9 Replace "sequence" with "list" in docs for consistency.
continuous-integration/drone/push Build is passing Details
2023-04-12 10:30:23 -07:00
Dan Helfman ea9213cb03 Spelling.
continuous-integration/drone/push Build is passing Details
2023-04-11 22:12:57 -07:00
Dan Helfman 1ea4433aa9 Selectively shallow merge certain mappings or sequences when including configuration files (#672).
continuous-integration/drone/push Build is failing Details
2023-04-11 21:49:10 -07:00
Divyansh Singh 2fea429d78 collection restore for mongodb 2023-04-12 09:34:19 +05:30
Divyansh Singh 264cebd2b1 complete psql multi schema backup 2023-04-11 23:19:49 +05:30
Dan Helfman 4c0e2cab78 View the results of configuration file merging via "validate-borgmatic-config --show" flag (#673).
continuous-integration/drone/push Build is passing Details
2023-04-11 10:49:09 -07:00
Dan Helfman 31a2ac914a Add optional support for running end-to-end tests and building documentation with rootless Podman instead of Docker.
continuous-integration/drone/push Build is passing Details
2023-04-10 14:26:54 -07:00
Dan Helfman d6ef0df50d Mention #670 being fixed in NEWS.
continuous-integration/drone/push Build is passing Details
2023-04-09 10:01:08 -07:00
Dan Helfman cc60a71210 Clarify "log_file" NEWS (#413).
continuous-integration/drone/push Build is passing Details
2023-04-06 14:12:12 -07:00
Dan Helfman 4cd7556a34 Add "log_file" command hook context to NEWS and docs (#413).
continuous-integration/drone/push Build is passing Details
2023-04-06 13:58:37 -07:00
Dan Helfman b4b1fa939d
feat: add logfile name to hook context for interpolation
Merge pull request #68 from diivi/feat/add-log-filename-to-hook-context
2023-04-06 13:46:45 -07:00
Divyansh Singh 16d7131fb7 refactor tests 2023-04-07 01:00:38 +05:30
Divyansh Singh 091d60c226 refactor and improve tests 2023-04-06 12:36:10 +05:30
Divyansh Singh 0fbdf8d860 feat: add logfile name to hook context for interpolation 2023-04-06 09:31:24 +05:30
Dan Helfman 192bfe46a9 Fix error when running the "prune" action with both "archive_name_format" and "prefix" options set (#668).
continuous-integration/drone/push Build is passing Details
2023-04-05 14:58:05 -07:00
Dan Helfman 080c3afa0d Fix documentation referring to "archive_name_format" in wrong configuration section.
continuous-integration/drone/push Build is passing Details
2023-04-05 14:00:21 -07:00
Divyansh Singh 9bc2322f9a feat: restore specific schemas 2023-04-06 02:10:36 +05:30
Dan Helfman a9a65ebe54 Fix integration tests to actually assert (#666).
continuous-integration/drone/push Build is passing Details
2023-04-04 22:11:36 -07:00
Dan Helfman 616eb6b6da Fix error with "info --match-archives" and fix "--match-archives" overriding logic (#666).
continuous-integration/drone/push Build is passing Details
2023-04-04 21:25:10 -07:00
Dan Helfman 00d1dea94e Bump version for release.
continuous-integration/drone/tag Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-04-03 16:11:25 -07:00
Dan Helfman 127ad1dd1f
Add favicon to documentation.
continuous-integration/drone/push Build is passing Details
Merge pull request #66 from diivi/add-favicon
2023-04-03 10:22:12 -07:00
Divyansh Singh fc58ba5763 add favicon to documentation 2023-04-03 17:36:24 +05:30
Dan Helfman 7e6bee84b0 Add "--log-file-format" flag for customizing the log message format (#658).
continuous-integration/drone/push Build is passing Details
2023-04-02 23:06:36 -07:00
Dan Helfman 01811e03ba Tagged the auto-matching archive behavior as breaking in NEWS.
continuous-integration/drone/push Build is passing Details
2023-04-02 14:38:35 -07:00
Dan Helfman 9712d00680 Add "match_archives" option (#588).
continuous-integration/drone/push Build is passing Details
2023-04-01 23:57:55 -07:00
Dan Helfman 275e99d0b9 Add codespell link to documentation.
continuous-integration/drone/push Build encountered an error Details
2023-04-01 14:38:52 -07:00
Dan Helfman b9328e6d42 Add spellchecking of source code to NEWS.
continuous-integration/drone/push Build is passing Details
2023-04-01 14:09:48 -07:00
Dan Helfman 2934d0902c Code spell checking on every test run!
continuous-integration/drone/push Build is passing Details
2023-04-01 11:03:59 -07:00
Dan Helfman 1ad43ad4b5
Fix: run typos to fix various typos in source code.
continuous-integration/drone/push Build is failing Details
Merge pull request #65 from diivi/fix/run-typos
2023-04-01 10:44:11 -07:00
Divyansh Singh 32ab17fa46 merge 2023-04-01 22:12:41 +05:30
Divyansh Singh 6054ced931 fix: run typos 2023-04-01 22:10:32 +05:30
Dan Helfman 1412038ed3
Fix randomly failing test: test_log_outputs_kills_other_processes_when_one_errors (#635).
continuous-integration/drone/push Build is passing Details
Merge pull request #64 from kxxt/master
2023-03-31 23:19:57 -07:00
kxxt fa8bc285c8 Fix randomly failing test. 2023-04-01 14:02:30 +08:00
Dan Helfman f256908b27 Document wording tweaks (#479).
continuous-integration/drone/push Build is passing Details
2023-03-31 15:36:59 -07:00
Dan Helfman 3f78ac4085 Automatically use the "archive_name_format" option to filter which archives get used for borgmatic actions that operate on multiple archives (#479).
continuous-integration/drone/push Build is passing Details
2023-03-31 15:21:08 -07:00
Dan Helfman 5f595f7ac3 Fix regression in which the "transfer" action produced a traceback (#663).
continuous-integration/drone/push Build is passing Details
2023-03-30 23:21:20 -07:00
Dan Helfman b27e625a77 Update schema comment for check_repositories to mention labels (#635).
continuous-integration/drone/push Build is passing Details
2023-03-28 15:44:38 -07:00
Dan Helfman fc2c181b74 Add missing Docker Compose depends.
continuous-integration/drone/push Build is passing Details
2023-03-28 15:31:37 -07:00
Dan Helfman 010b82d6d8 Remove unnecessary cd in dev documentation.
continuous-integration/drone/push Build is passing Details
2023-03-28 12:45:39 -07:00
Dan Helfman aaf3462d17 Fix Drone intentation.
continuous-integration/drone/push Build is passing Details
2023-03-28 12:03:12 -07:00
Dan Helfman f709125110 Error out if run-full-tests is run not inside a test container.
continuous-integration/drone/push Build encountered an error Details
2023-03-28 12:02:07 -07:00
Dan Helfman 3512191f3e Add check_repositories regression fix to NEWS (#662).
continuous-integration/drone/push Build is passing Details
2023-03-28 11:45:55 -07:00
Dan Helfman 06b5d81baa Merge branch 'master' of github.com:borgmatic-collective/borgmatic 2023-03-28 11:15:31 -07:00
Dan Helfman 9d71bf916e
fix: make check repositories work with dict and str repositories (#662).
Merge pull request #63 from diivi/fix/check-repositories-by-label
2023-03-28 11:15:01 -07:00
Dan Helfman 59fe01b56d Update script comment.
continuous-integration/drone/push Build is passing Details
2023-03-28 11:09:25 -07:00
Divyansh Singh 08e358e27f add and update tests 2023-03-28 22:51:35 +05:30
Divyansh Singh ce22d2d302 reformat 2023-03-28 22:29:21 +05:30
Divyansh Singh 2d08a63e60 fix: make check repositories work with dict and str repositories 2023-03-28 22:14:50 +05:30
Chirag Aggarwal 98c6aa6443 Use Square brackets to denote version specific flag
Signed-off-by: Chirag Aggarwal <thechiragaggarwal@gmail.com>
2023-03-28 18:15:49 +05:30
Chirag Aggarwal edd79ed86c removed individual action parameters, and used make_flags_from_arguments
Signed-off-by: Chirag Aggarwal <thechiragaggarwal@gmail.com>
2023-03-28 18:10:42 +05:30
Dan Helfman d96f2239c1 Update OpenBSD borgmatic link.
continuous-integration/drone/push Build is passing Details
2023-03-27 23:43:39 -07:00
Dan Helfman 67a349ae44 I had one job... (#461).
continuous-integration/drone/push Build is passing Details
2023-03-27 23:28:36 -07:00
Dan Helfman dcefded0fa Document that most command-line flags are not config-file-able (#461).
continuous-integration/drone/push Build is passing Details
2023-03-27 23:21:14 -07:00
Dan Helfman 1bcdebd1cc Fix multiple repositories example.
continuous-integration/drone/push Build is passing Details
2023-03-27 23:16:44 -07:00
Dan Helfman 7a8e0e89dd Mention prior versions of borgmatic in repositories schema.
continuous-integration/drone/push Build is passing Details
2023-03-27 21:54:01 -07:00
Dan Helfman 489ae080e5 Update docs with a few more "path:" repositories references (#635).
continuous-integration/drone/push Build is passing Details
2023-03-27 21:49:31 -07:00
Dan Helfman 0e3da7be63 Fix repository schema description.
continuous-integration/drone/push Build is passing Details
2023-03-27 16:15:24 -07:00
Dan Helfman c5ffb76dfa Bump version for release.
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
2023-03-27 15:56:49 -07:00
Dan Helfman 61c7b8f13c Add optional repository labels so you can select a repository via "--repository yourlabel" at the command-line (#635).
continuous-integration/drone/push Build is failing Details
2023-03-27 15:54:55 -07:00
Dan Helfman 3e8e38011b
Labels for repositories (#635).
Merge pull request #57 from diivi/feat/tag-repos
2023-03-27 15:46:22 -07:00
Chirag Aggarwal 4fa4fccab7 Use make_flags_from_arguments on mount; Pending test fixes
Signed-off-by: Chirag Aggarwal <thechiragaggarwal@gmail.com>
2023-03-27 23:24:17 +05:30
Dan Helfman d0d3a39833 When a database command errors, display and log the error message instead of swallowing it (#396).
continuous-integration/drone/push Build is passing Details
2023-03-27 10:36:39 -07:00
Divyansh Singh 8bef1c698b add feature to docs 2023-03-27 22:16:39 +05:30
Dan Helfman acbbd6670a Removing debugging command output.
continuous-integration/drone/push Build is passing Details
2023-03-26 21:26:35 -07:00
Divyansh Singh b336b9bedf add tests for repo labels 2023-03-27 00:19:23 +05:30
Divyansh Singh ec9def4e71 rename repository arg to repository_path in all borg actions 2023-03-26 23:52:25 +05:30
Divyansh Singh a136fda92d check all tests 2023-03-26 23:35:47 +05:30
Divyansh Singh b511e679ae remove optional label for repos from tests 2023-03-26 16:59:29 +05:30
Dan Helfman f56fdab7a9 Add troubleshooting documentation on PostgreSQL/MySQL authentication errors.
continuous-integration/drone/push Build is passing Details
2023-03-25 17:08:17 -07:00
jetchirag ff1f4dc09c minor fixes to prune argument help text 2023-03-26 02:06:46 +05:30
jetchirag 141474ff07 Added TIMESPAN flags to match archive in various commands (Borg2 feature)
Signed-off-by: jetchirag <thechiragaggarwal@gmail.com>
2023-03-26 01:58:03 +05:30
Dan Helfman 8c0eea7229 Add additional documentation link to environment variable feature. Rename constants section.
continuous-integration/drone/push Build is passing Details
2023-03-25 08:56:25 -07:00
Dan Helfman 19e95628c3 Add documentation and NEWS for custom constants feature (#612).
continuous-integration/drone/push Build is passing Details
2023-03-24 23:47:05 -07:00
Dan Helfman 4d01e53414
Fix: replace primitive values in config without quotes (#612).
Merge pull request #62 from diivi/fix/config-json-replacement
2023-03-24 23:45:36 -07:00
Divyansh Singh a082cb87cb fix: replace primitive values in config without quotes 2023-03-25 12:12:56 +05:30
Dan Helfman 1c51a8e229
Allow defining custom variables in config file (#612).
Merge pull request #60 from diivi/feat/constants-support
2023-03-24 22:50:57 -07:00
Dan Helfman d14a8df71a Hide obnoxious ruamel.yaml warnings during test runs.
continuous-integration/drone/push Build is passing Details
2023-03-24 22:43:10 -07:00
Dan Helfman 739a58fe47 Rename scripts/run-full-dev-tests to scripts/run-end-to-end-dev-tests and make it run end-to-end tests only.
continuous-integration/drone/push Build is passing Details
2023-03-24 16:24:00 -07:00
Dan Helfman af3431d6ae
fix: docs cli reference create spelling
continuous-integration/drone/push Build is passing Details
Merge pull request #61 from diivi/docs/cli-reference
2023-03-24 16:09:50 -07:00
Dan Helfman 9851abc2e1 Add documentation on backing up a database running in a container (#649).
continuous-integration/drone/push Build is passing Details
2023-03-24 15:18:49 -07:00
Divyansh Singh 61ce6f0473 fix: docs cli reference create spelling 2023-03-25 02:44:56 +05:30
Divyansh Singh 78e8bb6c8c reformat 2023-03-25 02:08:52 +05:30
Divyansh Singh af95134cd2 add test for complex constant 2023-03-25 02:03:36 +05:30
Divyansh Singh d6dfb8753a reformat 2023-03-25 01:50:47 +05:30
Divyansh Singh 1bc003560a Merge branch 'master' of https://github.com/diivi/borgmatic into feat/tag-repos 2023-03-25 01:39:26 +05:30
Divyansh Singh aeaf69f49e pass all tests 2023-03-25 01:34:03 +05:30
Divyansh Singh e83ad9e1e4 use repository["path"] instead of repository 2023-03-25 01:04:57 +05:30
Dan Helfman f42890430c Add code style plugins to enforce use of Python f-strings and prevent single-letter variables.
continuous-integration/drone/push Build is passing Details
2023-03-23 23:11:14 -07:00
Divyansh Singh 6f300b0079 feat: constants support 2023-03-24 02:39:37 +05:30
Dan Helfman 9bec029b4f
Fix: remove extra links from docs css.
continuous-integration/drone/push Build is failing Details
continuous-integration/drone Build is passing Details
Merge pull request #59 from diivi/fix/remove-extra-links-from-css
2023-03-23 12:57:55 -07:00
Divyansh Singh 08afad5d81 end with newline 2023-03-24 01:25:15 +05:30
Divyansh Singh a01dc62468 fix: remove extra links from docs css 2023-03-24 01:23:40 +05:30
Dan Helfman 8b61225b13
Copy to clipboard support in documentation.
continuous-integration/drone/push Build is passing Details
Merge pull request #58 from diivi/docs/copy-to-clipboard-support
2023-03-23 12:39:41 -07:00
Divyansh Singh 66d2f49f18 docs: copy to clipboard support 2023-03-23 14:45:23 +05:30
Dan Helfman 0a72c67c6c Add missing source directory error fix to NEWS (#655).
continuous-integration/drone/push Build is passing Details
2023-03-22 13:02:22 -07:00
Dan Helfman ab64b7ef67
Fix error when a source directory doesn't exist and databases are configured (#655).
Merge pull request #56 from diivi/fix/no-error-on-database-backup-without-source-dirs
2023-03-22 12:59:01 -07:00
Divyansh Singh 1e3a3bf1e7 review 2023-03-23 01:18:06 +05:30
Divyansh Singh 7a2f287918 reformat base 2023-03-23 01:08:30 +05:30
Divyansh Singh 8a63c49498 feat: tag repos 2023-03-23 01:01:26 +05:30
Divyansh Singh 3b5ede8044 remove extra parameter from function call 2023-03-22 23:11:44 +05:30
Divyansh Singh bd235f0426 use exit_code_indicates_error and modify it to accept a command 2023-03-22 16:23:53 +05:30
Divyansh Singh 09183464cd fix: no error on database backups without source dirs 2023-03-22 09:41:39 +05:30
Dan Helfman ca6fd6b061 Add confusing error message fix to NEWS (#623).
continuous-integration/drone/push Build is passing Details
2023-03-21 14:25:20 -07:00
Dan Helfman dd9a64f4b6
Fix confusing message when an error occurs running actions for a configuration file (#623).
Merge pull request #55 from diivi/fix/rephrase-error-message
2023-03-21 14:23:09 -07:00
Divyansh Singh 23e7f27ee4 fix: rephrase error when running from config
to avoid confusion, as the user might think the problem is with their config file
2023-03-22 02:22:43 +05:30
Dan Helfman f9ef52f9a5 Remove unused module and outdated test expectations (#576).
continuous-integration/drone/push Build is passing Details
2023-03-21 10:29:17 -07:00
Dan Helfman 3f17c355ca Add "file://" paths to NEWS (#576). 2023-03-21 10:24:51 -07:00
Dan Helfman c83fae5e5b
Support file:// paths for repositories (#576).
Merge pull request #54 from diivi/feat/file-urls-support
2023-03-21 10:22:39 -07:00
Divyansh Singh 39ad8f64c4 add tests and remove magic number 2023-03-21 17:06:03 +05:30
Divyansh Singh e86d223bbf Merge branch 'master' of https://github.com/diivi/borgmatic into feat/file-urls-support 2023-03-21 16:55:05 +05:30
Divyansh Singh 86587ab2dc send repo directly to extract and export_tar 2023-03-20 21:51:45 +05:30
Divyansh Singh 58c95d8015 feat: file:// URLs support 2023-03-20 02:43:23 +05:30
Dan Helfman 6351747da5 Add NixOS package link to installation docs.
continuous-integration/drone/push Build is passing Details
2023-03-19 09:02:47 -07:00
Dan Helfman 55c153409e Add "source_directories_must_exist" option to NEWS (#501).
continuous-integration/drone/push Build is passing Details
2023-03-18 14:07:38 -07:00
Dan Helfman b115fb2fbe Merge branch 'master' of github.com:borgmatic-collective/borgmatic 2023-03-18 14:01:52 -07:00
Dan Helfman 31d04d9ee3
Optionally error if a source directory does not exist.
feat: add optional check for existence of source directories
2023-03-18 13:59:20 -07:00
Divyansh Singh f803836416 reformat 2023-03-18 17:27:33 +05:30
Divyansh Singh 997f60b3e6 add tests 2023-03-18 17:24:21 +05:30
Dan Helfman c84b26499b Add "borg_files_cache_ttl" option to NEWS.
continuous-integration/drone/push Build is passing Details
2023-03-17 19:29:10 -07:00
Dan Helfman 214ae81cbb Add option to set borg_files_cache_ttl in config (#618).
continuous-integration/drone/push Build is passing Details
Reviewed-on: #654
2023-03-18 02:24:41 +00:00
Divyansh Singh d17b2c74db feat: add optional check for existence of source directories 2023-03-18 04:35:55 +05:30
Soumik Dutta fb9677230b add test to ensure integers are converted to string
before setting them up to be environment variable values

Signed-off-by: Soumik Dutta <shalearkane@gmail.com>
2023-03-18 02:57:56 +05:30
Soumik Dutta 0db137efdf add option to set borg_files_cache_ttl in config
Signed-off-by: Soumik Dutta <shalearkane@gmail.com>
2023-03-18 01:48:24 +05:30
Dan Helfman e6605c868d Clarify check frequency default behavior (#653).
continuous-integration/drone/push Build is passing Details
2023-03-17 10:09:36 -07:00
Dan Helfman bdfe4b61eb Bump version for release.
continuous-integration/drone/tag Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-03-16 13:42:15 -07:00
Dan Helfman ca4461820d Add support for Python 3.11.
continuous-integration/drone/push Build is passing Details
2023-03-16 13:29:37 -07:00
Dan Helfman 7605838bfe Add "--repository" flag to all actions where it makes sense (#564).
continuous-integration/drone/push Build is passing Details
2023-03-16 13:27:08 -07:00
Dan Helfman 7a784b8eba Add "--repository" flag to common actions (where it makes sense) (#652).
continuous-integration/drone/push Build is passing Details
Reviewed-on: #652
2023-03-16 20:21:40 +00:00
Nain 3e22414613 Update tests
Make them more explicit. Also formatting.
2023-03-16 14:01:29 -04:00
Nain 5f87ea3ec5 Add "--repository" flag to the "create" action 2023-03-16 13:15:49 -04:00
Nain a8aeace5b5 Add "--repository" flag to the "compact" action 2023-03-16 11:13:45 -04:00
Nain 480addd7ce Add "--repository" flag to the "check" action 2023-03-16 10:41:13 -04:00
Nain ce0ce4cd1c Merge mostly repetetive tests 2023-03-16 08:23:21 -04:00
Nain 7de9260b0d Remove test now that --repository isn't expected to error
As discussed #652#issuecomment-5579
2023-03-15 14:59:12 -04:00
Nain cdbe6cdf3a Add "--repository" flag to the "prune" action
part of ticket #564
2023-03-15 14:43:17 -04:00
Dan Helfman 95dcc20d5f Better indicate position of additional docs on page (#651).
continuous-integration/drone/push Build is passing Details
Reviewed-on: #651
2023-03-15 18:13:27 +00:00
Dan Helfman 49e0494924 Fix --editable (mode) option given --user as arg (#648).
continuous-integration/drone/push Build is passing Details
Reviewed-on: #650
2023-03-15 18:06:46 +00:00
Nain 5fad2bd408 Better indicate position of additional docs on page
On wide screens, the position of the documentation (how-to and reference guide)
is at same level as #it's-your-data.-keep-it-that-way.

So the jump due to anchor link makes it seem like we're taken to top aka
main content. Indicate that links are to the left so reader doesn't recurse.
2023-03-15 07:54:49 -04:00
Nain c6829782a3 Fix --editable (mode) option given --user as arg
--user option should be before, or after `--editable .` not in between.
Before seems better.
2023-03-15 06:50:47 -04:00
Dan Helfman 8cec7c74d8 Add "--strip-components all" on the "extract" action to remove leading path components (#647).
continuous-integration/drone/push Build is passing Details
2023-03-09 10:09:16 -08:00
Dan Helfman d3086788eb Document how to list database dumps in an archive.
continuous-integration/drone/push Build is passing Details
2023-03-08 16:09:41 -08:00
Dan Helfman 8d860ea02c
Enhanced docs with info on fetching mysql database size
Merge pull request #46 from Jelle-SamsonIT/patch-3
2023-03-08 15:52:28 -08:00
Dan Helfman b343363bb8 Change the default action order to: "create", "prune", "compact", "check" (#304).
continuous-integration/drone/push Build is passing Details
2023-03-08 14:05:06 -08:00
Dan Helfman 9db31bd1e9 Run any command-line actions in the order specified instead of using a fixed ordering (#304).
continuous-integration/drone/push Build is passing Details
2023-03-08 13:19:41 -08:00
Dan Helfman d88bcc8be9 Add Healthchecks "log" state feature to NEWS.
continuous-integration/drone/push Build is passing Details
2023-03-07 15:45:23 -08:00
Dan Helfman 332f7c4bb6 Add support for healthchecks "log" feature (#628).
continuous-integration/drone/push Build is failing Details
Reviewed-on: #645
2023-03-07 22:21:30 +00:00
Dan Helfman 5d19d86e4a Add flake8-quotes to complain about incorrect quoting so I don't have to!
continuous-integration/drone/push Build is passing Details
2023-03-07 14:08:35 -08:00
Soumik Dutta 044ae7869a fix tests
Signed-off-by: Soumik Dutta <shalearkane@gmail.com>
2023-03-08 03:30:12 +05:30
Dan Helfman 62ae82f2c0 Mention searching for files in the extract a backup guide.
continuous-integration/drone/push Build is passing Details
2023-03-06 22:59:34 -08:00
Dan Helfman 66194b7304 Update dates in documentation examples.
continuous-integration/drone/push Build is passing Details
2023-03-06 22:41:43 -08:00
Soumik Dutta 98e429594e added tests to make sure unsupported log states are detected
Signed-off-by: Soumik Dutta <shalearkane@gmail.com>
2023-03-06 20:31:00 +05:30
Soumik Dutta 4fcfddbe08 return early if unsupported state is passed
Signed-off-by: Soumik Dutta <shalearkane@gmail.com>
2023-03-06 19:58:57 +05:30
Soumik Dutta f442aeae9c fix logs_monitor_start_error()
Signed-off-by: Soumik Dutta <shalearkane@gmail.com>
2023-03-06 05:21:56 +05:30
Soumik Dutta e211863cba update test_borgmatic.py
Signed-off-by: Soumik Dutta <shalearkane@gmail.com>
2023-03-06 05:12:24 +05:30
Soumik Dutta 45256ae33f add test for healthchecks
Signed-off-by: Soumik Dutta <shalearkane@gmail.com>
2023-03-06 03:38:08 +05:30
Soumik Dutta 1573d68fe2 update schema.yaml description
also add monitor.State.LOG to cronitor.

Signed-off-by: Soumik Dutta <shalearkane@gmail.com>
2023-03-05 21:57:13 +05:30
Soumik Dutta 69f6695253 Add support for healthchecks "log" feature #628
Signed-off-by: Soumik Dutta <shalearkane@gmail.com>
2023-03-05 19:27:32 +05:30
Dan Helfman a7c055264d
Fix incorrect documentation TOC background by removing extra dark mode styles.
continuous-integration/drone/push Build is passing Details
Merge pull request #52 from diivi/fix/remove-special-dark-mode-attributes
2023-03-04 16:18:04 -08:00
Divyansh Singh db18364a73 fix: remove extra dark mode styles 2023-03-05 03:16:46 +05:30
Dan Helfman 22498ebd4c In the documentation, mention what version of borgmatic introduced SQLite support.
continuous-integration/drone/push Build is passing Details
2023-03-04 10:50:28 -08:00
Dan Helfman e1f02d9fa5 Add SQLite feature to NEWS and also integrations.
continuous-integration/drone/push Build is passing Details
2023-03-04 09:59:16 -08:00
Dan Helfman 9ec220c600
Add SQLite database dump/restore hook (#295).
feat: add dump-restore support for sqlite databases
2023-03-04 09:47:21 -08:00
Divyansh Singh cf0275a3ed remove test path 2023-03-04 23:00:57 +05:30
Divyansh Singh c71eb60cd2 mock os.remove instead of actually removing a file 2023-03-04 13:08:30 +05:30
Divyansh Singh 675e54ba9f use os.remove and improve tests 2023-03-04 12:43:07 +05:30
Divyansh Singh 1793ad74bd add sqlite for e2e tests 2023-03-04 02:41:14 +05:30
Divyansh Singh 767a7d900b e2e tests schema update 2023-03-04 01:29:01 +05:30
Divyansh Singh 903507bd03 code review 2023-03-04 01:27:07 +05:30
Dan Helfman b6cf7d2adc Bump version for release.
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
2023-03-02 15:34:22 -08:00
Dan Helfman a071e02d20 With the "create" action and the "--list" ("--files") flag, only show excluded files at verbosity 2 (#620).
continuous-integration/drone/push Build is failing Details
2023-03-02 15:33:42 -08:00
Divyansh Singh 3aa88085ed formatting fix 2023-03-03 00:01:52 +05:30
Divyansh Singh af1cc27988 feat: add dump-restore support for sqlite databases 2023-03-02 23:55:16 +05:30
Dan Helfman dbf8301c19 Add "checkpoint_volume" configuration option to creates checkpoints every specified number of bytes.
continuous-integration/drone/push Build is passing Details
2023-02-27 10:47:17 -08:00
Dan Helfman 2a306bef12 Fix tests.
continuous-integration/drone/push Build is passing Details
2023-02-26 23:34:17 -08:00
Dan Helfman 2a36a2a312 Add "--repository" flag to the "rcreate" action. Add "--progress" flag to the "transfer" action.
continuous-integration/drone/push Build is failing Details
2023-02-26 23:22:23 -08:00
Dan Helfman d7a07f0428 Support status character changes in Borg 2.0.0b5 when filtering out special files that cause Borg to hang.
continuous-integration/drone/push Build is passing Details
2023-02-26 22:36:13 -08:00
Dan Helfman da321e180d Fix the "create" action with the "--dry-run" flag querying for databases when a PostgreSQL/MySQL "all" database is configured.
continuous-integration/drone/push Build is passing Details
2023-02-26 22:15:12 -08:00
Dan Helfman c6582e1171 Internally support new Borg 2.0.0b5 "--filter" status characters / item flags for the "create" action.
continuous-integration/drone/push Build is passing Details
2023-02-26 17:17:25 -08:00
Dan Helfman 9b83afe491 With the "create" action, only one of "--list" ("--files") and "--progress" flags can be used.
continuous-integration/drone/push Build is passing Details
2023-02-26 17:05:56 -08:00
Dan Helfman 2814ac3642 Update Borg 2.0 documentation links.
continuous-integration/drone/push Build is passing Details
2023-02-26 16:44:43 -08:00
Dan Helfman 8a9d5d93f5 Add ntfy authentication to NEWS.
continuous-integration/drone/push Build is passing Details
2023-02-25 14:23:42 -08:00
Dan Helfman 783a6d3b45 Add authentication to the ntfy hook (#621).
continuous-integration/drone/push Build is passing Details
Reviewed-on: #644
2023-02-25 22:04:37 +00:00
Tom Hubrecht 95575c3450 Add auth test for the ntfy hook 2023-02-25 20:04:39 +01:00
Tom Hubrecht 9b071ff92f Make the auth logic more explicit and warnings if necessary 2023-02-25 20:04:39 +01:00
Tom Hubrecht d80e716822 Add authentication to the ntfy hook 2023-02-24 17:35:53 +01:00
Dan Helfman 418ebc8843 Add MySQL database hook "add_drop_database" configuration option to control whether dumped MySQL databases get dropped right before restore (#642).
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
2023-02-20 15:32:47 -08:00
Dan Helfman f5a448c7c2 Fix for potential data loss (data not getting backed up) when dumping large "directory" format PostgreSQL/MongoDB databases (#643).
continuous-integration/drone/push Build is passing Details
2023-02-20 15:18:51 -08:00
Dan Helfman 37ac542b31 Merge pull request 'setup: Add link to MacPorts package' (#641) from neverpanic/borgmatic:cal-docs-macports-port into master
continuous-integration/drone/push Build is passing Details
Reviewed-on: #641
2023-02-15 17:31:03 +00:00
Clemens Lang 8c7d7e3e41 setup: Add link to MacPorts package 2023-02-15 10:47:59 +01:00
Dan Helfman b811f125b2 Clarify "checks" configuration documentation for older versions of borgmatic (#639).
continuous-integration/drone/push Build is passing Details
2023-02-12 21:42:43 -08:00
Dan Helfman 061f3e7917 Remove related documentation links.
continuous-integration/drone/push Build is passing Details
2023-01-26 16:12:01 -08:00
Dan Helfman 6055918907 Upgrade documentation image dependencies. 2023-01-26 16:11:41 -08:00
Dan Helfman 4a90e090ad Clarify NEWS on database "all" dump feature applying to MySQL as well.
continuous-integration/drone/push Build is passing Details
2023-01-26 15:28:17 -08:00
Dan Helfman 301b29ee11 Bump version for release.
continuous-integration/drone/tag Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-01-26 15:17:19 -08:00
Dan Helfman c1eb210253 Fix code style flake issue.
continuous-integration/drone/push Build is passing Details
2023-01-26 15:09:35 -08:00
Dan Helfman 30cca62d09 Add configuration options for database command customization (#630).
continuous-integration/drone/push Build is failing Details
2023-01-26 14:59:17 -08:00
Dan Helfman 113c0e7616 Update documentation about changes to "all" database restores (#438, #560).
continuous-integration/drone/push Build is passing Details
2023-01-26 10:53:58 -08:00
Dan Helfman 0e6b2c6773 Optionally dump "all" PostgreSQL databases to separate files instead of one combined dump file (#438, #560).
continuous-integration/drone/push Build is passing Details
2023-01-25 23:31:07 -08:00
Dan Helfman 22c750b949 Mention "before_actions" command hook in soft failure documentation (#631).
continuous-integration/drone/push Build is passing Details
2023-01-25 13:01:52 -08:00
Dan Helfman 504cce39a1 Add NEWS entry for #629.
continuous-integration/drone/push Build is passing Details
2023-01-14 09:17:27 -08:00
Dan Helfman 6c4abb6803 Merge pull request 'Log warning for excluding special files only if list is not empty' (#629) from palto42/borgmatic:special_files_warn into master
continuous-integration/drone/push Build is passing Details
Reviewed-on: #629
2023-01-14 17:15:01 +00:00
palto42 fd7ad86daa
conditional warning for excluding special files 2023-01-03 21:53:51 +01:00
Dan Helfman 6f3b23c79d Lowercase borgmatic in documentation.
continuous-integration/drone/push Build is passing Details
2022-12-23 14:12:48 -08:00
Dan Helfman 4838f5e810 Add borgmatic minimum version to compact docs (#624).
continuous-integration/drone/push Build is passing Details
Reviewed-on: #625
2022-12-23 22:11:45 +00:00
Macguire Rintoul 116f1ab989 add borgmatic minimum version to compact docs 2022-12-23 13:32:01 -08:00
Dan Helfman 5e15c9f2bc Fix traceback when include merging on ARM64 (#622).
continuous-integration/drone/push Build is passing Details
2022-12-23 10:07:53 -08:00
Dan Helfman 442641f9f6 Update borgmatic social links.
continuous-integration/drone/push Build is passing Details
2022-12-16 11:39:05 -08:00
Dan Helfman f67c544be6 Optionally dump "all" PostgreSQL databases to separate files instead of one combined dump file (#438, #560).
continuous-integration/drone/push Build is passing Details
2022-12-15 22:59:42 -08:00
Dan Helfman 437fd4dbae Update developer constributing instructions as well.
continuous-integration/drone/push Build is passing Details
2022-12-13 23:56:32 -08:00
Dan Helfman 36873252d6 Update developer instructions.
continuous-integration/drone/push Build is passing Details
2022-12-13 23:44:27 -08:00
Dan Helfman 1ef82a27fa Clarify data/archives check implicit enabling.
continuous-integration/drone/push Build is passing Details
2022-12-12 16:03:05 -08:00
Dan Helfman 6837dcbf42 Clarify documentation about transferring archives between related repositories.
continuous-integration/drone/push Build is passing Details
2022-12-10 12:59:44 -08:00
Dan Helfman c657764367 Fix logs that interfere with JSON output by making warnings go to stderr instead of stdout (#602).
continuous-integration/drone/push Build is passing Details
2022-12-02 12:12:10 -08:00
Dan Helfman f79286fc91 Bump version for release.
continuous-integration/drone/tag Build is passing Details
continuous-integration/drone/push Build is passing Details
2022-11-27 09:00:40 -08:00
Dan Helfman 694d376d15 Clarify documentation about multiple repositories and separate configuration files (#613).
continuous-integration/drone/push Build is passing Details
2022-11-21 13:33:01 -08:00
Dan Helfman ab4c08019c Upgrade pytest test dependency (security).
continuous-integration/drone/push Build is passing Details
2022-11-18 11:13:51 -08:00
Dan Helfman fd39f54df7 Code formatting.
continuous-integration/drone/push Build is passing Details
2022-11-18 08:35:01 -08:00
Dan Helfman ca7e18bb29
Override PostgreSQL dump/restore commands via configuration options (#311).
Merge pull request #49 from jpaniagualaconich/specify-pg-dump-restore-commands
2022-11-18 08:33:14 -08:00
Dan Helfman 6975a5b155 Fix "data" consistency check to support "check_last" and consistency "prefix" options (#611).
continuous-integration/drone/push Build is passing Details
2022-11-17 10:19:48 -08:00
Dan Helfman b627d00595 More consistency checks documentation edits.
continuous-integration/drone/push Build is passing Details
2022-11-14 15:13:47 -08:00
Dan Helfman 9bd8f1a6df Clarify consistency check configuration.
continuous-integration/drone/push Build is passing Details
2022-11-14 14:58:42 -08:00
Javier Paniagua faf682ca35 specify pg dump/restore commands (#311) 2022-11-06 11:12:53 +01:00
Dan Helfman 6aeb74550d Clarify examples in include merging and deep merging documentation (#607).
continuous-integration/drone/push Build is passing Details
2022-10-28 19:33:19 -07:00
Dan Helfman 89500df429 Fix traceback when a configuration section is present but lacking any options (#604).
continuous-integration/drone/push Build is passing Details
2022-10-23 13:56:03 -07:00
Dan Helfman 82b072d0b7 Update documentation to mention using blake2 with "transfer" action.
continuous-integration/drone/push Build is passing Details
2022-10-17 15:04:30 -07:00
Dan Helfman 018c0296fd Document that special file exclusion also excludes symlinks to special files (#596).
continuous-integration/drone/push Build is passing Details
2022-10-15 10:14:46 -07:00
Dan Helfman 9c42e7e817 Fix regression in which "check" action errored on certain systems (#597, #598).
continuous-integration/drone/push Build is failing Details
continuous-integration/drone/tag Build is passing Details
2022-10-14 16:19:26 -07:00
Dan Helfman 953277a066 Fix special file detection when broken symlinks are encountered (#596).
continuous-integration/drone/push Build is passing Details
2022-10-14 09:41:08 -07:00
Dan Helfman e2002b5488 Bump version for release.
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
2022-10-12 10:59:54 -07:00
Dan Helfman c9742e1d04 Code formatting.
continuous-integration/drone/push Build is passing Details
2022-10-12 10:52:32 -07:00
Dan Helfman 906da838ef Add missing break-lock action command-line help (#357).
continuous-integration/drone/push Build is failing Details
2022-10-12 10:48:10 -07:00
Dan Helfman d7f1c10c8c To prevent Borg hangs, unconditionally delete stale named pipes before dumping databases (#360).
continuous-integration/drone/push Build is passing Details
2022-10-12 10:26:09 -07:00
Dan Helfman e8e4d17168 Clean up changelog for the current dev release.
continuous-integration/drone/push Build is passing Details
2022-10-06 22:06:03 -07:00
Dan Helfman a31ce337e9 Skip auto-exclusion of special files when user explicitly sets read_special to true (#587).
continuous-integration/drone/push Build is passing Details
2022-10-06 11:07:43 -07:00
Dan Helfman 902730df46 Update sample systemd file to allow system idle (#589).
continuous-integration/drone/push Build is passing Details
2022-10-05 10:20:25 -07:00
Dan Helfman c969c822ee Do not inhibit idle in borgmatic.service (#589).
continuous-integration/drone/push Build is passing Details
Reviewed-on: #589
2022-10-05 17:14:19 +00:00
Dan Helfman c31702d092 Fix for potential data loss with "patterns_from". Also, display excluded files (#590).
continuous-integration/drone/push Build is passing Details
2022-10-04 22:57:18 -07:00
Dan Helfman ba8fbe7a44 Add "break-lock" action for removing any repository and cache locks leftover from Borg aborting (#357).
continuous-integration/drone/push Build is passing Details
2022-10-04 13:42:18 -07:00
Dan Helfman 2774c2e4c0 Add support for Borg 2's "--match-archives" flag (replaces "--glob-archives") (#591).
continuous-integration/drone/push Build is passing Details
2022-10-03 22:50:37 -07:00
Dan Helfman ae036aebd7 When the "read_special" option is true or database hooks are enabled, auto-exclude special files for a "create" action to prevent Borg from hanging (#587).
continuous-integration/drone/push Build is passing Details
2022-10-03 12:58:13 -07:00
LaserEyess 2e9f70d496 Do not inhibit idle in borgmatic.service
When backing up a machine with a monitor using logind to control idle
timeout and things like DPMS, borgmatic can block the screen from
turning on/off with systemd-inhibit. This is because by default
systemd-inhibit will block "idle:sleep:shutdown". Borgmatic does not
need to care about idle, only about suspend and shutdown. So, add an
explicit `--what` flag for what borgmatic should inhibit.

For more information see systemd-inhibit(1).
2022-10-01 09:33:38 -04:00
Dan Helfman 90be5b84b1 Fix changelog development version.
continuous-integration/drone/push Build is passing Details
2022-09-20 14:02:48 -07:00
Dan Helfman 80e95f20a3 Add "borgmatic borg" documentation note about interactive prompts. 2022-09-20 14:01:47 -07:00
Dan Helfman ac7c7d4036 Warn when ignoring a configured "read_special" value of false, as true is needed when database hooks are enabled (#587).
continuous-integration/drone/push Build is failing Details
2022-09-20 13:52:13 -07:00
Dan Helfman 858b0b9fbe Note version of borgmatic needed for "borgmatic borg" action (#586).
continuous-integration/drone/push Build is passing Details
2022-09-13 09:05:18 -07:00
Dan Helfman 9cc043f60e Update "find" command in documentation to work on BSDs and not just Linux (#583).
continuous-integration/drone/push Build is passing Details
2022-09-11 20:02:30 -07:00
Dan Helfman 276a27d485 Bump version for release.
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
2022-09-08 10:29:44 -07:00
Dan Helfman 679bb839d7 Fix hang when database hooks are enabled and "patterns" contains a parent directory of "~/.borgmatic" (#582).
continuous-integration/drone/push Build is passing Details
2022-09-08 10:16:42 -07:00
Dan Helfman 9e64d847ef Fix regression in which "borgmatic info --archive ..." showed repository info instead of archive info with Borg 1 (#577).
continuous-integration/drone/push Build is passing Details
2022-08-30 20:42:42 -07:00
Dan Helfman 61fb275896 Fix duplicate-appearing log entries for "list" action. 2022-08-30 20:29:26 -07:00
Dan Helfman ca0c79c93c Fix duplicate bind path in sample systemd service.
continuous-integration/drone/push Build is running Details
2022-08-28 14:49:23 -07:00
Dan Helfman 87c97b7568 Fixed spurious, intermittent test failures related to command execution and logging.
continuous-integration/drone/push Build is passing Details
2022-08-28 09:06:06 -07:00
Dan Helfman 80b8c25bba Update docs about "source_directories" being optional.
continuous-integration/drone/push Build is passing Details
2022-08-25 13:24:26 -07:00
Dan Helfman d1837cd1d3 Bump version for release.
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
2022-08-25 11:58:06 -07:00
Dan Helfman c46f2b8508 Fix conflict between "patterns" and "source_directories" (#574), make "source_directories" optional (#542). 2022-08-25 11:55:34 -07:00
Dan Helfman a274c0dbf7 In generate-borgmatic-config, indicate that the example options are exhaustive.
continuous-integration/drone/push Build is passing Details
2022-08-24 09:53:54 -07:00
Dan Helfman ef7e95e22a Fix end-to-end tests.
continuous-integration/drone/push Build is passing Details
2022-08-21 23:29:13 -07:00
Dan Helfman 3be99de5b1 Update "repositories" examples in configuration to use ssh:// style syntax.
continuous-integration/drone/push Build is failing Details
2022-08-21 22:40:31 -07:00
Dan Helfman e7b7560477 Bump version for release.
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
2022-08-21 21:54:13 -07:00
Dan Helfman 317dc7fbce Add "before_actions" and "after_actions" command hooks that run before/after all the actions for each repository, update docs to cover per-repository configurations (#463).
continuous-integration/drone/push Build is passing Details
2022-08-21 21:48:37 -07:00
Dan Helfman 97fad15009 Switch to more accessible header permalink anchors in documentation. 2022-08-21 21:48:07 -07:00
Dan Helfman 462326406e Drop only-style actions like "--create", rename "prune --files" to "prune --list", and add "--list" alias to "create" and "export-tar" (#571).
continuous-integration/drone/push Build is passing Details
2022-08-21 14:25:16 -07:00
Dan Helfman bbdf4893d1 Clarify --format flag in documentation.
continuous-integration/drone/push Build is passing Details
2022-08-19 15:27:03 -07:00
Dan Helfman ef6617cfe6
Add link to Borg list --format documentation.
continuous-integration/drone/push Build is passing Details
2022-08-19 15:16:56 -07:00
Dan Helfman dbef0a440f
Merge branch 'master' into patch-2 2022-08-19 15:16:17 -07:00
Dan Helfman 22628ba5d4 Update ssh:// examples in documentation to use relative paths on the remote machine (#557).
continuous-integration/drone/push Build is passing Details
2022-08-19 12:00:40 -07:00
Dan Helfman 8576ac86b9 Fix incorrect version in documentation (#557).
continuous-integration/drone/push Build is passing Details
2022-08-19 09:44:31 -07:00
Dan Helfman 540f9f6b72 Add missing test for "transfer" action (#557).
continuous-integration/drone/push Build is passing Details
2022-08-19 09:40:29 -07:00
Dan Helfman f9d7faf884 Fix mount action to work without archive again (#557).
continuous-integration/drone/push Build is passing Details
2022-08-18 23:33:05 -07:00
Dan Helfman 7dee6194a2 Add new "transfer" action for Borg 2 (#557).
continuous-integration/drone/push Build is passing Details
2022-08-18 23:06:51 -07:00
Dan Helfman 68f9c1b950 Add generate-borgmatic-config end-to-end test.
continuous-integration/drone/push Build is passing Details
2022-08-18 14:28:46 -07:00
Dan Helfman 43d711463c Add additional command-line flags to rcreate action (#557). 2022-08-18 14:28:12 -07:00
Dan Helfman 00255a2437 Various documentation edits for Borg 2 (#557).
continuous-integration/drone/push Build is passing Details
2022-08-18 10:19:11 -07:00
Dan Helfman b40e9b7da2 Ignore archive filter parameters passed to list action when --archive is given (#557).
continuous-integration/drone Build is passing Details
2022-08-18 09:59:48 -07:00
Dan Helfman 89d201c8ff Fleshing out NEWS for the Borg 2 changes.
continuous-integration/drone/push Build is passing Details
2022-08-17 21:54:00 -07:00
Dan Helfman f47c98c4a5 Rename several configuration options to match Borg 2 (#557).
continuous-integration/drone/push Build is passing Details
2022-08-17 21:14:58 -07:00
Dan Helfman 3b6ed06686 Add --other-repo flag to rcreate action (#557).
continuous-integration/drone/push Build is passing Details
2022-08-17 17:33:09 -07:00
Dan Helfman 57009e22b5 Use flag-related utility functions in info action (#557).
continuous-integration/drone/push Build is running Details
2022-08-17 17:11:02 -07:00
Dan Helfman 3ab7a3b64a Replace use of --prefix with --glob-archives in info action (#557).
continuous-integration/drone/push Build is passing Details
2022-08-17 15:36:19 -07:00
Dan Helfman 596dd49cf5 Use --glob-archives instead of --prefix on rlist command (#557).
continuous-integration/drone/push Build is running Details
2022-08-17 14:26:35 -07:00
Dan Helfman 28d847b8b1 Warn and tranform on non-ssh://-style repositories (#557).
continuous-integration/drone/push Build is passing Details
2022-08-17 10:13:11 -07:00
Dan Helfman 2a1c6b1477 Update documentation with newly required ssh:// repository syntax for Borg 2 (#557).
continuous-integration/drone/push Build is passing Details
2022-08-16 11:41:35 -07:00
Dan Helfman 30abd0e3de Update borg action for Borg 2 support (#557).
continuous-integration/drone/push Build is passing Details
2022-08-16 09:30:00 -07:00
Dan Helfman f36e38ec20 Update mount action for Borg 2 support (#557).
continuous-integration/drone/push Build is passing Details
2022-08-15 19:32:37 -07:00
Dan Helfman d807ce095e Update export-tar action for Borg 2 support (#557).
continuous-integration/drone/push Build is passing Details
2022-08-15 17:34:12 -07:00
Dan Helfman 7626fe1189 Disallow borg list --json with --archive or --find (#557).
continuous-integration/drone/push Build is passing Details
2022-08-15 15:40:28 -07:00
Dan Helfman cc04bf57df Update list action for Borg 2 support, add rinfo action, and update extract consistency check for Borg 2.
continuous-integration/drone/push Build is passing Details
2022-08-15 15:04:40 -07:00
Dan Helfman cce6d56661 Update extract action for Borg 2 support (#557).
continuous-integration/drone/push Build is passing Details
2022-08-13 23:07:29 -07:00
Dan Helfman a05d0f378e Factor out repository/archive flags formatting code from create action (#557).
continuous-integration/drone/push Build is passing Details
2022-08-13 22:50:14 -07:00
Dan Helfman 94321aec7a Update compact action for Borg 2 support (#557).
continuous-integration/drone/push Build is passing Details
2022-08-13 22:07:15 -07:00
Dan Helfman 4a55749bd2 Update prune action for Borg 2 support (#557).
continuous-integration/drone/push Build is passing Details
2022-08-13 17:26:51 -07:00
Dan Helfman 2898e63166 Update create action for Borg 2 support (#557).
continuous-integration/drone/push Build is passing Details
2022-08-12 23:54:13 -07:00
Dan Helfman c7176bd00a Add rinfo action for Borg 2 support (#557).
continuous-integration/drone/push Build is passing Details
2022-08-12 23:06:56 -07:00
Dan Helfman 647ecdac29 Borg 2 support in borgmatic check action (#557).
continuous-integration/drone/push Build is passing Details
2022-08-12 15:46:33 -07:00
Dan Helfman e7a8acfb96 Add missing rinfo action source files (#557).
continuous-integration/drone/push Build is passing Details
2022-08-12 14:59:03 -07:00
Dan Helfman 622caa0c21 Support for Borg 2's rcreate and rinfo sub-commands (#557).
continuous-integration/drone/push Build is failing Details
2022-08-12 14:53:20 -07:00
Dan Helfman 22149c6401 Switch to self-hosted container registry for borgmatic documentation image.
continuous-integration/drone/push Build is passing Details
2022-08-01 21:17:59 -07:00
Dan Helfman 9aece3936a Modify "mount" and "extract" actions to require the "--repository" flag when multiple repositories are configured (#566).
continuous-integration/drone/push Build is passing Details
2022-07-25 11:30:02 -07:00
Dan Helfman c7e4e6f6c9 Add Healthchecks "verify_tls" option to NEWS.
continuous-integration/drone/push Build is passing Details
2022-07-23 23:16:06 -07:00
Dan Helfman bcad0de1a4
Add verify_tls option for Healthchecks to optionally disable TLS verification. 2022-07-23 23:11:41 -07:00
Uli 5c6407047f feat: add verify_tls flag for Healthchecks 2022-07-24 07:37:00 +02:00
Dan Helfman 6ddae20fa1 Fix handling of "repository" and "data" consistency checks to prevent invalid Borg flags (#565).
continuous-integration/drone/push Build is passing Details
2022-07-23 21:02:21 -07:00
Dan Helfman 23feac2f4c Bump version for release.
continuous-integration/drone/tag Build is passing Details
continuous-integration/drone/push Build is passing Details
2022-07-19 20:32:41 -07:00
Dan Helfman 16066942e3 Fix traceback with "create" action and "--json" flag when a database hook is configured (#563).
continuous-integration/drone/push Build is passing Details
2022-07-19 10:25:10 -07:00
Jelle @ Samson-IT 3720f22234
reworded and added 'all' caveat 2022-07-13 22:03:51 +02:00
Jelle @ Samson-IT f7c8e89a9f
update format specifier syntax link to use anchor 2022-07-13 21:52:21 +02:00
Jelle @ Samson-IT ba377952fd
Added link to borgbackup list --format docs
I kept searching for this link, so it's time to add it to official docs.
2022-07-13 13:52:48 +02:00
Jelle @ Samson-IT 1fdec480d6
Added some info about fetching mysql database size 2022-07-13 13:29:45 +02:00
Dan Helfman e85d551eac Fix all database hooks to error when the requested database to restore isn't present in the Borg archive (#560).
continuous-integration/drone/push Build is passing Details
2022-07-06 23:21:24 -07:00
Dan Helfman 2b23a63a08 Add end-to-end test for overrides.
continuous-integration/drone/push Build is passing Details
2022-07-06 18:20:51 -07:00
Dan Helfman c0f48e1071 Fix command-line "--override" flag to continue supporting old configuration file formats (#561).
continuous-integration/drone/push Build is passing Details
2022-07-06 18:14:44 -07:00
Dan Helfman 6005426684 Update documentation about configuring multiple consistency checks or multiple databases (#559).
continuous-integration/drone/push Build is passing Details
2022-07-03 22:24:25 -07:00
Dan Helfman 673ed1a2d3 Clarify check frequency documentation in regards to multiple configuration files.
continuous-integration/drone/push Build is passing Details
2022-07-02 09:40:49 -07:00
Dan Helfman 992f62edd2 Bump version for release.
continuous-integration/drone/tag Build is passing Details
continuous-integration/drone/push Build is passing Details
2022-06-30 22:14:41 -07:00
Dan Helfman f1ffa1da1d Add another recommended flag to the backup documentation (#554).
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
2022-06-30 16:54:22 -07:00
Dan Helfman 457ed80744 Fix environment variable plumbing so options in one configuration file aren't used for others (#555).
continuous-integration/drone/push Build is passing Details
2022-06-30 13:42:17 -07:00
Dan Helfman 1fc028ffae In documentation, be more explicit about default actions (#554).
continuous-integration/drone/push Build is passing Details
2022-06-29 21:32:00 -07:00
Dan Helfman 10723efc68 Fix all monitoring hooks to warn if the server returns an HTTP 4xx error (#554).
continuous-integration/drone/push Build is passing Details
2022-06-29 21:19:40 -07:00
Dan Helfman 2e0b2a308f Clarify --files flag action in documentation (#554).
continuous-integration/drone/push Build is passing Details
2022-06-29 09:20:13 -07:00
Dan Helfman bd4d109009 Fix logging to include the full traceback when Borg experiences an internal error (#553).
continuous-integration/drone/push Build is passing Details
2022-06-28 13:38:24 -07:00
Dan Helfman ae25386336 Update release script to abort if there are local changes. Prevents accidentally tagging a .dev0 changeset for release.
continuous-integration/drone/push Build is passing Details
2022-06-25 09:42:05 -07:00
Dan Helfman d929313d45 Bump version.
continuous-integration/drone/push Build is passing Details
2022-06-24 10:18:01 -07:00
Dan Helfman d372a86fe6 Code formatting.
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build encountered an error Details
2022-06-23 10:41:04 -07:00
Dan Helfman e306f03e1d Merge branch 'master' of ssh://projects.torsion.org:3022/borgmatic-collective/borgmatic
continuous-integration/drone/push Build is failing Details
2022-06-23 10:28:09 -07:00
Dan Helfman 8336165f23 Update documentation with environment variable escaping (#546). 2022-06-23 10:25:46 -07:00
Dan Helfman c664c6b17b Fix escaped environment variable in configuration (#546).
continuous-integration/drone/push Build is failing Details
Reviewed-on: #549
2022-06-23 17:16:09 +00:00
Sébastien MB b63c854509 Fix escaped environment variable in configuration
- when an env variable is escaped in the configuration file, we expect
  not to resolve it and remove the escape char `\`
2022-06-17 09:50:56 +02:00
Dan Helfman aa013af25e Remove some whitespace around "New in version ..." documentation labels.
continuous-integration/drone/push Build is passing Details
2022-06-16 20:49:15 -07:00
Dan Helfman cc32f0018b Start formalizing how new features are flagged by version in documentation.
continuous-integration/drone/push Build is passing Details
2022-06-16 20:23:16 -07:00
Dan Helfman dfc4db1860 Document environment variable interpolation (#546).
continuous-integration/drone/push Build is passing Details
2022-06-16 15:30:53 -07:00
Dan Helfman 35706604ea Upgrade documentation base images. 2022-06-16 15:22:59 -07:00
Dan Helfman 6d76e8e5cb Code formatting.
continuous-integration/drone/push Build is passing Details
2022-06-16 14:21:18 -07:00
Dan Helfman aecb6fcd74 Code style, rename command-line flag, and move new code into its own file (#546)
continuous-integration/drone/push Build is failing Details
2022-06-16 11:35:24 -07:00
Dan Helfman ea45f6c4c8 Environment variable resolution in configuration file (#546).
continuous-integration/drone/push Build is failing Details
Reviewed-on: #548
2022-06-16 18:18:12 +00:00
Sébastien MB 97b5cd089d Allow environment variable resolution in configuration file
- all string fields containing an environment variable like ${FOO} will
  be resolved
- supported format ${FOO}, ${FOO:-bar} and ${FOO-bar} to allow default
  values if variable is not present in environment
- add --no-env argument for CLI to disable the feature which is enabled
  by default

Resolves: #546
2022-06-16 18:52:54 +02:00
Dan Helfman f2c2f3139e Add periods to ntfy config descriptions.
continuous-integration/drone/push Build is passing Details
2022-06-10 09:42:41 -07:00
Dan Helfman dc4e7093e5 Remove link to related software that hasn't seen updates in the past couple years.
continuous-integration/drone/push Build is passing Details
2022-06-09 19:31:50 -07:00
Dan Helfman b6f1025ecb Bump version for release.
continuous-integration/drone/tag Build is passing Details
continuous-integration/drone/push Build is passing Details
2022-06-09 16:38:34 -07:00
Dan Helfman 65b2fe86c6 Fix Bash completion script to no longer alter your shell's settings.
continuous-integration/drone/push Build is passing Details
2022-06-09 16:29:54 -07:00
Dan Helfman 0e90a80680 Add links in documentation for ntfy monitoring hook (#543).
continuous-integration/drone/push Build is passing Details
2022-06-09 13:41:22 -07:00
Dan Helfman 7648bcff39 Add a hook for sending push notifications via ntfy.sh.
continuous-integration/drone/push Build is passing Details
Reviewed-on: #543
2022-06-09 20:26:06 +00:00
Gavin Chappell a8b8d507b6
add a hook for sending push notifications via ntfy.sh 2022-06-09 21:10:38 +01:00
Dan Helfman 3561c93d74 Fix Healthchecks tests that leak global state, breaking downstream tests (discovered in #543).
continuous-integration/drone/push Build is passing Details
2022-06-09 11:05:44 -07:00
Dan Helfman 331a503a25 Document the borgmatic version in which "borgmatic list --find" is available (#541).
continuous-integration/drone/push Build is passing Details
2022-06-03 16:55:54 -07:00
Dan Helfman 9aefb5179f Fix None find paths (#541).
continuous-integration/drone/push Build is passing Details
2022-06-03 15:20:05 -07:00
Dan Helfman d14f22e121 Add "borgmatic list --find" flag for searching for files across multiple archives (#541).
continuous-integration/drone/push Build is failing Details
2022-06-03 15:12:14 -07:00
Dan Helfman b6893f6455 Exclude deprecated "borg list --successful" flag from getting passed to Borg.
continuous-integration/drone/push Build is passing Details
2022-06-02 21:14:25 -07:00
Dan Helfman 80ec3e7d97 Deprecate "borgmatic list --successful" flag, as listing only non-checkpoint (successful) archives is now the default in newer versions of Borg.
continuous-integration/drone/push Build is passing Details
2022-06-02 20:35:39 -07:00
Dan Helfman cd834311eb Clarify completion docs.
continuous-integration/drone/push Build is passing Details
2022-06-01 10:57:23 -07:00
Dan Helfman d751cceeb0 Merge branch 'master' of ssh://projects.torsion.org:3022/borgmatic-collective/borgmatic 2022-06-01 10:38:05 -07:00
Dan Helfman ce78b07e4b Add macOs to install and Bash completion documentation.
continuous-integration/drone/push Build is passing Details
Reviewed-on: #540
2022-06-01 17:37:51 +00:00
adidalal 87f3c50931 setup: add macOS 2022-06-01 15:56:40 +00:00
Dan Helfman 8e9e06afe6 Bump version for release.
continuous-integration/drone/tag Build is passing Details
2022-05-31 09:41:20 -07:00
Dan Helfman 2bc91ac3d2 Add "generate-borgmatic-config --overwrite" flag to replace an existing destination file (#539).
continuous-integration/drone/push Build is passing Details
2022-05-29 16:03:55 -07:00
Dan Helfman 5b615d51a4 Add support for "borgmatic borg debug" command (#538).
continuous-integration/drone/push Build is passing Details
2022-05-29 15:43:03 -07:00
Dan Helfman c7f5d5fd0b Fix broken Bash completion of filenames, as in "-c config.yaml".
continuous-integration/drone/push Build is passing Details
2022-05-29 10:49:33 -07:00
Dan Helfman 6ef7538eb0 Fix typo in Bash completions script.
continuous-integration/drone/push Build is passing Details
2022-05-28 19:34:13 -07:00
Dan Helfman 8fa90053cf Add "borgmatic check --force" flag to ignore configured check frequencies (#523). 2022-05-28 19:29:33 -07:00
Dan Helfman b3682b61d1 Add another note about the consistency checks schema in old versions (#523). 2022-05-28 19:03:45 -07:00
Dan Helfman ad0e2e0d7c Tweak default check frequency to 1 month (#523). 2022-05-28 15:49:50 -07:00
Dan Helfman 6629f40cab In bash completion script, warn when script is out of date using script contents instead of version. (Fewer spurious warnings that way.) 2022-05-28 15:27:11 -07:00
Dan Helfman e76bfa555f Reduce the default consistency check frequency and support configuring the frequency independently for each check (#523). 2022-05-28 14:42:19 -07:00
Dan Helfman 8ddb7268eb Reuse "borg info" function.
continuous-integration/drone/push Build is passing Details
2022-05-27 13:51:11 -07:00
Dan Helfman cb5fe02ebd Fix broken Bash completion end-to-end test.
continuous-integration/drone/push Build is passing Details
2022-05-26 11:18:46 -07:00
Dan Helfman 77b84f8a48 Add Bash completion script so you can tab-complete the borgmatic command-line.
continuous-integration/drone/push Build is failing Details
2022-05-26 10:27:53 -07:00
Dan Helfman 691ec96909 Fix python_requires to support all versions of 3.7 (#537).
continuous-integration/drone/push Build is passing Details
Reviewed-on: #537
2022-05-26 15:51:46 +00:00
Steve Atwell 29b4666205 Fix python_requires to support all versions of 3.7
This is the standard way to support "Python 3.7 and newer" and it also
fixes use of borgmatic with some tools that do custom dependency
resolution.  E.g., using pex with --platform.
2022-05-26 07:05:04 -07:00
Dan Helfman 316a22701f Add documentation note about multiple merge limitation (#380).
continuous-integration/drone/push Build is passing Details
2022-05-25 23:12:42 -07:00
Dan Helfman be59a3e574 Fix generate-borgmatic-config with "--source" flag to support more complex schema changes like the new Healthchecks configuration options (#536).
continuous-integration/drone/push Build is passing Details
2022-05-25 10:26:26 -07:00
Dan Helfman 37327379bc Merge branch 'master' of ssh://projects.torsion.org:3022/borgmatic-collective/borgmatic
continuous-integration/drone/push Build is passing Details
2022-05-24 17:50:57 -07:00
Dan Helfman 22c2f13611 Remove trailing whitespace (#535).
continuous-integration/drone/push Build is passing Details
Reviewed-on: #535
2022-05-25 00:50:12 +00:00
polyzen 8708ca07f4 Remove trailing whitespace 2022-05-25 00:43:40 +00:00
Dan Helfman 634d9e4946 Bump version for release.
continuous-integration/drone/tag Build is passing Details
2022-05-24 16:22:37 -07:00
Dan Helfman 54933ebef5 Change connection failures for monitoring hooks to be warnings instead of errors (#439).
continuous-integration/drone/push Build is passing Details
2022-05-24 15:50:04 -07:00
Dan Helfman 157e59ac88 Add Healthchecks monitoring hook "send_logs" option to enable/disable sending borgmatic logs to the Healthchecks server (#460).
continuous-integration/drone/push Build is passing Details
2022-05-24 14:44:33 -07:00
Dan Helfman 666f0dd751 Add missing Healthchecks "states" option example in configuration schema (#525).
continuous-integration/drone/push Build is passing Details
2022-05-24 14:17:19 -07:00
Dan Helfman 8b179e4647 Reverse logic of Healtchecks "skip_states" option to just "states" (#525).
continuous-integration/drone/push Build is failing Details
2022-05-24 14:09:42 -07:00
Dan Helfman 865eff7d98 Add Healthchecks monitoring hook "skip_states" option to disable pinging for particular monitoring states (#525).
continuous-integration/drone/push Build is failing Details
2022-05-24 13:59:28 -07:00
Dan Helfman b9741f4d0b Add Healthchecks monitoring hook "ping_body_limit" option to configure how many bytes of logs to send to the Healthchecks server (#294).
continuous-integration/drone/push Build is passing Details
2022-05-24 12:23:38 -07:00
Dan Helfman 02781662f8 Change monitoring hooks to specify the ping URL / integration key as a named option.
continuous-integration/drone/push Build is passing Details
2022-05-23 20:02:10 -07:00
Dan Helfman 32a1043468 Remove the error when "archive_name_format" is specified but a retention prefix isn't (#402).
continuous-integration/drone/push Build is passing Details
2022-05-23 16:11:24 -07:00
Dan Helfman 3e4aeec649 Warn when an unsupported variable is used in a hook command (#420).
continuous-integration/drone/push Build is passing Details
2022-05-23 15:27:54 -07:00
Dan Helfman b98b827594 Remove stale comment.
continuous-integration/drone/push Build is passing Details
2022-05-23 10:59:56 -07:00
Dan Helfman 255cc6ec23 When deep merging common configuration, merge colliding list values by appending them (#531).
continuous-integration/drone/push Build is passing Details
2022-05-20 15:28:28 -07:00
Dan Helfman 51fc37d57a Improve the error message when a configuration override contains an invalid value (#528).
continuous-integration/drone/push Build is passing Details
2022-05-20 13:38:53 -07:00
Dan Helfman 1921f55a9d Add emojis to documentation table of contents to make it easier to find particular how-to and reference guides at a glance.
continuous-integration/drone/push Build is passing Details
2022-05-20 11:11:35 -07:00
Dan Helfman fbd381fcc1 Clarify manual database extraction documentation.
continuous-integration/drone/push Build is passing Details
2022-05-20 10:06:19 -07:00
Dan Helfman cd88f9f2ea Better explain where to find the dump file when doing a manual restore (#510).
continuous-integration/drone/push Build is passing Details
Reviewed-on: #510
2022-05-20 16:33:21 +00:00
Dan Helfman 788281cfb9 When a configuration include is a relative path, load it from either the current working directory or from the directory containing the file doing the including (#532).
continuous-integration/drone/push Build is passing Details
2022-05-19 17:15:05 -07:00
Dan Helfman cd234b689d Link to additional borgmatic Docker image.
continuous-integration/drone/push Build is passing Details
2022-05-12 12:00:12 -07:00
Dan Helfman 92354a77ee Mention that database dumps consumed disk space prior to borgmatic 1.5.3.
continuous-integration/drone/push Build is passing Details
2022-05-09 16:08:47 -07:00
Dan Helfman 48ff3e70d1 Clarify documentation about include merging mappings vs. values.
continuous-integration/drone/push Build is passing Details
2022-05-08 14:48:42 -07:00
Dan Helfman 7e9adfb899 Add NEWS entry for randomized systemd timer delay.
continuous-integration/drone/push Build is passing Details
2022-05-07 23:11:26 -07:00
Dan Helfman e238e256f7
Add randomized delay to systemd timer.
Merge pull request from Daniel15/patch-1
2022-05-07 23:08:02 -07:00
Daniel Lo Nigro 3ecb92a8d2
Add randomized delay to systemd timer 2022-05-07 16:42:06 -07:00
Dan Helfman d58d450628 Remove stale borgmatic binary link.
continuous-integration/drone/push Build is passing Details
2022-04-30 09:50:40 -07:00
Dan Helfman dee9c6e293 Remove link to stale borgmatic Docker image.
continuous-integration/drone/push Build is passing Details
2022-04-30 09:46:08 -07:00
Dan Helfman 897c4487de Add mention in documentation about multiple backup scheduling needs (#511).
continuous-integration/drone/push Build is passing Details
2022-04-28 11:16:31 -07:00
Dan Helfman 48b50b5209 Add documentation link to NEWS.
continuous-integration/drone/push Build is passing Details
2022-04-26 10:24:25 -07:00
Dan Helfman 13bae8c23b Typo.
continuous-integration/drone/push Build is passing Details
2022-04-26 10:12:02 -07:00
Dan Helfman 4a48e6aa04 Bump version for release.
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
2022-04-26 10:07:04 -07:00
Dan Helfman 525266ede6 Deep merging when including common configuration (#381).
continuous-integration/drone/push Build is passing Details
2022-04-25 21:18:37 -07:00
Dan Helfman d045eb55ac Add mention of sudo's "secure_path" option in borgmatic installation documentation (#513).
continuous-integration/drone/push Build is passing Details
2022-04-23 14:29:55 -07:00
Dan Helfman 0e6b425ac5 Fix "borgmatic borg key ..." to pass parameters to Borg in correct order (#515).
continuous-integration/drone/push Build is passing Details
2022-04-23 14:03:15 -07:00
Dan Helfman bdc26f2117 Add note about old, pre-1.6.0 hooks behavior.
continuous-integration/drone/push Build is passing Details
2022-04-22 19:58:28 -07:00
Dan Helfman ed7fe5c6d0 Instead of executing "before" command hooks before all borgmatic actions run (and "after" hooks after), execute these hooks right before/after the corresponding action (#473).
continuous-integration/drone/push Build is passing Details
2022-04-21 22:08:25 -07:00
Dan Helfman cbce6707f4 Clarify one_file_system behavior in schema comment (#520).
continuous-integration/drone/push Build is passing Details
2022-04-12 11:05:22 -07:00
Dan Helfman e40e726687 Change Healthchecks logs truncation size from 10k bytes to 100k bytes, corresponding to that same change on Healthchecks.io.
continuous-integration/drone/push Build is passing Details
2022-04-06 22:00:18 -07:00
Dan Helfman 0c027a3050 Fix handling of TERM signal to exit borgmatic, not just forward the signal to Borg (#516).
continuous-integration/drone/push Build is passing Details
2022-04-03 13:12:48 -07:00
Dan Helfman 9f44bbad65 Fix borgmatic exit code (so it's zero) when initial Borg calls fail but later retries succeed (#517).
continuous-integration/drone/push Build is passing Details
2022-04-02 22:28:41 -07:00
Dan Helfman 413a079f51 Clarify Python version support.
continuous-integration/drone/push Build is passing Details
2022-03-28 21:57:40 -07:00
gerdneuman 6f3accf691 Better explain where to find the dump file
continuous-integration/drone/pr Build is passing Details
I really had problem finding the dump file with the explanation as give before. I thought that the `~/.borgmatic/` would be my current user. So looked into `/home/gerd/.borgmatic` (wrong). Then I looked into `<EXTRACTED_DESTINATION_PATH/.borgmatic` (again wrong). Then finally (1h later and after having already prepared a bug ticketI figured out that the dump file is within `<EXTRACTED_DESTINATION_PATH/root/.borgmatic`. Hard to find because of course I d not only have `root` within `<EXTRACTED_DESTINATION_PATH/` but also all other backup'ed directories (including /etc/, /home/ on so on...)
2022-03-17 04:51:47 +00:00
Dan Helfman 5b3cfc542d Switch to PyPI API token.
continuous-integration/drone/push Build is passing Details
2022-03-14 14:00:03 -07:00
Dan Helfman c838c1d11b Fix header placement in documentation guide.
continuous-integration/drone/push Build is passing Details
2022-03-14 13:50:22 -07:00
Dan Helfman 4d1d8d7409 Bump version for release.
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
2022-03-14 13:43:24 -07:00
Dan Helfman db7499db82 Document "repositories" context to for "before_*" and "after_*" command action hooks (#469).
continuous-integration/drone/push Build is passing Details
2022-03-14 13:34:14 -07:00
Dan Helfman 6b500c2a8b Add repositories context for command hooks.
continuous-integration/drone/push Build is passing Details
Reviewed-on: #469
2022-03-14 20:13:15 +00:00
Dan Helfman 95c518e59b Documentation tip about dealing with hangs when database hook is enabled.
continuous-integration/drone/push Build is passing Details
2022-03-12 13:17:32 -08:00
Dan Helfman 976516d0e1 When loading a configuration file that is unreadable due to file permissions, warn instead of erroring (#444).
continuous-integration/drone/push Build is passing Details
2022-03-08 10:19:36 -08:00
Dan Helfman 574eb91921 Fix Borg usage error in the "compact" action when running "borgmatic --dry-run". Now, skip "compact" entirely during a dry run (#507).
continuous-integration/drone/push Build is passing Details
2022-03-07 21:46:12 -08:00
Dan Helfman 28fef3264b Fix handling of "patterns_from" and "exclude_from" options to error instead of warning when referencing unreadable files and running "create" action (#486).
continuous-integration/drone/push Build is passing Details
2022-03-07 15:32:07 -08:00
Dan Helfman 9161dbcb7d Removing unnecessary leading underscores from functions.
continuous-integration/drone/push Build is passing Details
2022-03-07 11:58:29 -08:00
Dan Helfman 4b3027e4fc Add test for new working_directory option (#431).
continuous-integration/drone/push Build is passing Details
2022-03-03 11:48:18 -08:00
Dan Helfman 0eb2634f9b Working directory option to support source directories with relative paths (#431).
continuous-integration/drone/push Build is failing Details
Reviewed-on: #477
2022-03-03 19:28:17 +00:00
Dan Helfman 7c5b68c98f Bump version for release.
continuous-integration/drone/tag Build is passing Details
continuous-integration/drone/push Build is passing Details
2022-02-10 10:29:18 -08:00
Dan Helfman 9317cbaaf0 Code formatting.
continuous-integration/drone/push Build is passing Details
2022-02-10 10:23:34 -08:00
Dan Helfman 1b5f04b79f When using the "remote_rate_limit" option, tailor the flags passed to Borg depending on the Borg version (#394).
continuous-integration/drone/push Build is failing Details
2022-02-10 10:16:09 -08:00
Dan Helfman 948c86f62c When using the "numeric_owner" option with the "extract" action, tailor the flags passed to Borg depending on the Borg version (#394).
continuous-integration/drone/push Build is passing Details
2022-02-10 10:09:18 -08:00
Dan Helfman 7e7209322a When using the "numeric_owner" option, tailor the flags passed to Borg depending on the Borg version (#394).
continuous-integration/drone/push Build is passing Details
2022-02-10 09:51:13 -08:00
Dan Helfman 00a57fd947 Code formatting.
continuous-integration/drone/push Build is passing Details
2022-02-09 21:20:28 -08:00
Dan Helfman 6bf6ac310b When using the "bsd_flags" option, tailor the flags passed to Borg depending on the Borg version (#394).
continuous-integration/drone/push Build is failing Details
2022-02-09 21:11:00 -08:00
Dan Helfman 4b5af2770d When the "atime" option is used, tailor the flags passed to Borg depending on version (#394).
continuous-integration/drone/push Build is passing Details
2022-02-09 16:54:35 -08:00
Dan Helfman b525e70e1c Run "compact" action by default when no actions are specified (#394). 2022-02-09 14:33:12 -08:00
Dan Helfman 4498671233 Remove references to removed long-deprecated options (#394).
continuous-integration/drone/push Build is passing Details
2022-02-09 11:08:02 -08:00
Dan Helfman 9997aa9a92 Fix capitalization on compact help.
continuous-integration/drone/push Build is passing Details
2022-02-08 15:58:09 -08:00
Dan Helfman cbf7284f64 Add compact action to command-line reference documentation.
continuous-integration/drone/push Build is passing Details
2022-02-08 15:37:24 -08:00
Dan Helfman ee466f870d Fixing ruamel.yaml.clib breakages harder.
continuous-integration/drone/push Build is passing Details
2022-02-08 13:21:11 -08:00
Dan Helfman e3f4bf0293 Build fix for ruamel.yaml.clib error.
continuous-integration/drone/push Build is failing Details
2022-02-08 12:52:45 -08:00
Dan Helfman 46688f10b1 Merge branch 'master' of ssh://projects.torsion.org:3022/borgmatic-collective/borgmatic
continuous-integration/drone/push Build is failing Details
2022-02-08 12:10:57 -08:00
Dan Helfman 48f44d2f3d Add tests for compact action (#394). 2022-02-08 12:05:02 -08:00
Dan Helfman bff1347ba3 Fix some test failures (#394).
continuous-integration/drone/push Build is failing Details
2022-02-08 09:35:03 -08:00
Dan Helfman 9582324c88 Compact repository segments with new "borgmatic compact" action (#394).
continuous-integration/drone/push Build is failing Details
2022-02-07 23:29:44 -08:00
Dan Helfman bb0716421d Add comment about systemd service setting that may interfere with external commands in hooks (#492).
continuous-integration/drone/push Build is passing Details
2022-01-25 09:26:11 -08:00
Dan Helfman bec73245e9 Fix traceback when a YAML validation error occurs (#480, #482).
continuous-integration/drone/push Build is passing Details
2022-01-19 20:39:03 -08:00
Dan Helfman dcead12e86 Attempt to fix documentation build error introduced by Eleventy upgrade.
continuous-integration/drone/push Build is passing Details
2022-01-09 14:21:27 -08:00
Dan Helfman 0119514c11 Add Python version requirements to setup.py.
continuous-integration/drone/push Build is failing Details
2022-01-09 10:19:53 -08:00
fabianschilling b39f08694d Merge branch 'master' into pr-working-directory
continuous-integration/drone/pr Build is passing Details
2022-01-05 09:30:27 +00:00
Dan Helfman 80bdf1430b Bump version for release.
continuous-integration/drone/tag Build is passing Details
2022-01-04 20:20:13 -08:00
Dan Helfman 2ee75546f5 Add MongoDB database hook documentation.
continuous-integration/drone/push Build is passing Details
2022-01-04 16:26:38 -08:00
Dan Helfman 07d7ae60d5 Add MongoDB database hook (#288).
continuous-integration/drone/push Build is passing Details
Reviewed-on: #483
2022-01-04 23:50:25 +00:00
Andrea Ghensi 87001337b4 Merge master into mongodb_hook
continuous-integration/drone/pr Build is passing Details
2022-01-04 22:20:44 +01:00
Dan Helfman 2e9964c200 Remove references to Lima Labs (shut down their storage business).
continuous-integration/drone/push Build is passing Details
Reviewed-on: #488
2022-01-03 17:34:38 +00:00
Ian Kerins 3ec3d8d045 Remove references to Lima Labs
continuous-integration/drone/pr Build is passing Details
From their homepage:
> Lima Labs is shutting down our storage business. We will try to keep data available as long as possible. No promises but we are targeting 3/1/2022 to bring down Archive and Canada.
2022-01-03 02:29:38 -05:00
Dan Helfman 96384d5ee1 Attempt to fix typed-ast build issue by relaxing version requirements in test.
continuous-integration/drone/push Build is passing Details
2022-01-02 23:22:24 -08:00
Dan Helfman 8ed5467435 Drop support for Python 3.6. Add support for 3.10.
continuous-integration/drone/push Build is failing Details
2022-01-02 23:17:57 -08:00
Andrea Ghensi 7c6ce9399c fix integration tests and mongodb auth
continuous-integration/drone/pr Build is failing Details
2021-12-29 22:18:50 +01:00
Andrea Ghensi 6b7653484b Add mongodb dump hook
continuous-integration/drone/pr Build is failing Details
2021-12-26 01:00:58 +01:00
Fabian Schilling 85e0334826 Add missing working_directory arg to pass tests
continuous-integration/drone/pr Build is passing Details
2021-12-10 18:24:41 +01:00
Fabian Schilling 2a80e48a92 Pass working directory to execute functions 2021-12-10 18:23:44 +01:00
Fabian Schilling 5821c6782e Add defaults to not set in schema 2021-12-10 18:23:08 +01:00
Fabian Schilling f15498f6d9 Add working_directory to borgmatic schema 2021-12-10 17:58:27 +01:00
Dan Helfman a1673d1fa1 Fix unicode error when restoring particular MySQL databases (#476).
continuous-integration/drone/push Build is passing Details
2021-12-08 16:40:25 -08:00
Dan Helfman 2e99a1898c Fix f-string with missing expression.
continuous-integration/drone/push Build is passing Details
2021-11-29 14:05:36 -08:00
Dan Helfman 7a086d8430 Fix import ordering.
continuous-integration/drone/push Build was killed Details
2021-11-29 14:00:14 -08:00
Dan Helfman 0e8e9ced64 When command-line configuration override produces a parse error, error cleanly (#471).
continuous-integration/drone/push Build is failing Details
2021-11-29 12:49:21 -08:00
Dan Helfman f34951c088 Add MySQL dump command adjustment to NEWS.
continuous-integration/drone/push Build is passing Details
2021-11-29 12:10:04 -08:00
Dan Helfman c6f47d4d56 Move mysqldump options to the beginning of the command due to MySQL bug 30994 (#470).
continuous-integration/drone/push Build is failing Details
Reviewed-on: #470
2021-11-29 20:08:59 +00:00
nebulon42 c3e76585fc
move mysqldump options to the beginning of the command due to MySQL bug 30994.
continuous-integration/drone/pr Build is passing Details
2021-11-26 17:16:03 +01:00
Chen Yufei 0014b149f8 remove configuration_filename as it's already set.
continuous-integration/drone/pr Build is passing Details
2021-11-26 11:38:58 +08:00
Chen Yufei 091c07bbe2 Add context for various hooks.
continuous-integration/drone/pr Build is passing Details
2021-11-26 11:35:10 +08:00
Dan Helfman 240547102f Enable auto-play on linked asciicast.
continuous-integration/drone/push Build is passing Details
2021-11-25 13:09:55 -08:00
Dan Helfman 2bbd53e25a
Merge pull request #43 from acsfer/patch-1
Github doesn't allow script embedding
2021-11-25 13:06:43 -08:00
acsfer 58f2f63977
Switch to HTML 2021-11-25 22:03:26 +01:00
acsfer 7df6a78c30
Github doesn't allow script embedding 2021-11-25 21:36:31 +01:00
Dan Helfman c646edf2c7 Bump version for release.
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
2021-11-22 13:19:15 -08:00
Dan Helfman bcc820d646 Add list_options setting (#306).
continuous-integration/drone/push Build is passing Details
Reviewed-on: #464
2021-11-22 21:14:02 +00:00
nebulon42 3729ba5ca3
add list_options setting, fixes #306
continuous-integration/drone/pr Build is passing Details
2021-11-20 15:43:58 +01:00
Dan Helfman 9c19591768 Revise hosting provider links.
continuous-integration/drone/push Build is passing Details
2021-11-15 20:06:09 -08:00
Dan Helfman 38ebfd2969 Rename retry_timeout to retry_wait and standardize log formatting (#28).
continuous-integration/drone/push Build is passing Details
2021-11-15 11:51:17 -08:00
Dan Helfman 180018fd81 Retry failing backups (#28, #432).
continuous-integration/drone/push Build is passing Details
Reviewed-on: #432
2021-11-15 19:34:24 +00:00
Dan Helfman 794ae94ac4 Attempt to limit documentation pushing to commits (so, not pull requests).
continuous-integration/drone/push Build is passing Details
2021-11-15 11:08:26 -08:00
Dan Helfman 4eb6359ed3 Remove now-unneeded build image workaround.
continuous-integration/drone/push Build is passing Details
2021-11-15 10:56:12 -08:00
cadamswaite 976a877a25 Formatting
continuous-integration/drone/pr Build is failing Details
2021-11-14 22:37:42 +00:00
cadamswaite b4117916b8 Add timeout and tests 2021-11-14 22:15:22 +00:00
cadamswaite 19cad89978 Add some tests for retry logic 2021-11-14 21:35:23 +00:00
cadamswaite 6b182c9d2d Merge branch 'master' into master
continuous-integration/drone/pr Build is failing Details
2021-11-14 18:24:17 +00:00
Dan Helfman 4d6ed27f73 Add to changelog: Add support for old version (2.x) of jsonschema library.
continuous-integration/drone/push Build is passing Details
2021-10-23 09:49:16 -07:00
Dan Helfman 745a8f9b8a Add support for both jsonschema v3 and old v2 (#459).
continuous-integration/drone/push Build is passing Details
Reviewed-on: #459
2021-10-23 16:47:53 +00:00
Dan Helfman 6299d8115d Limit documentation build to master of main repo, as it pushes a Docker image.
continuous-integration/drone/push Build is passing Details
2021-10-23 09:45:17 -07:00
Kim B. Heino 717cfd2d37 validate: add support for both jsonschema v3 and old v2
continuous-integration/drone/pr Build is failing Details
RHEL8 and RHEL7 have old jsonschema v2. Try v3 (Draft7) first but
fallback to v2 (Draft4) if needed.
2021-10-23 15:04:07 +03:00
Dan Helfman 7881327004 Upgrade CI test dependencies.
continuous-integration/drone/push Build is passing Details
2021-10-22 14:07:14 -07:00
Dan Helfman 549aa9a25f Update editable link.
continuous-integration/drone/push Build is passing Details
2021-10-22 14:06:27 -07:00
Dan Helfman 1c6890492b Bump version for release.
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
2021-10-11 17:02:32 -07:00
Dan Helfman a7c8e7c823 Bump version for release.
continuous-integration/drone/push Build is passing Details
2021-10-11 11:13:32 -07:00
Dan Helfman c8fcf6b336 Mention changing borgmatic path in cron documentation (#455).
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
2021-10-11 11:02:08 -07:00
Dan Helfman 449896f661 Fix error when configured source directories are not present on the filesystem at the time of backup (#387).
continuous-integration/drone/push Build is passing Details
2021-10-11 10:40:10 -07:00
Dan Helfman 1004500d65 Update sample systemd service file comments about more granular read-only filesystem settings.
continuous-integration/drone/push Build is passing Details
2021-10-11 09:33:07 -07:00
Dan Helfman 0a8d4e5dfb
Add more strict ProtectHome to systemd sample configuration.
Merge pull request #42 from VTimofeenko/systemd_protecthome
2021-10-11 09:26:28 -07:00
Dan Helfman 38e35bdb12 Skip TLS verify in documentation build clone to work around old drone/git CA certs.
continuous-integration/drone/push Build is passing Details
2021-10-04 14:31:15 -07:00
Dan Helfman 65503e38b6 Sigh.
continuous-integration/drone/push Build is failing Details
2021-10-04 13:14:19 -07:00
Dan Helfman d0c5bf6f6f Another attempt to unbreak build.
continuous-integration/drone/push Build is failing Details
2021-10-04 13:13:35 -07:00
Dan Helfman f129e4c301 Attempt to work-around outdated CA certificates in drone/git Docker image.
continuous-integration/drone/push Build is failing Details
2021-10-04 13:09:44 -07:00
Dan Helfman fbbb096cec Note in documentation that borgmatic requires Python 3.6+.
continuous-integration/drone/push Build is failing Details
2021-10-04 11:15:51 -07:00
Dan Helfman 77980511c6 Add another glob pattern example to exclude patterns.
continuous-integration/drone/push Build is passing Details
2021-09-16 09:51:40 -07:00
Dan Helfman 4ba206f8f4 Update build server URL to new organization namespace.
continuous-integration/drone/push Build is passing Details
2021-09-14 11:35:34 -07:00
Dan Helfman ecc849dd07 Move Gitea hosting from a personal namespace to an organization. 2021-09-14 11:32:01 -07:00
Dan Helfman 7ff6066d47 Move GitHub hosting from a personal namespace to an organization.
continuous-integration/drone/push Build is passing Details
2021-09-14 10:18:10 -07:00
Dan Helfman 2bb1fc9826 Mention Docker Compose under installation options.
continuous-integration/drone/push Build is passing Details
2021-09-12 13:15:34 -07:00
Vladimir Timofeenko 6df6176f3a
Added more strict ProtectHome to systemd unit
This commit changes the comment in sample systemd service.

Using a combination of 'ProtectHome' and 'BindPaths' it's possible to
hide the irrelevant paths inside /root from borgmatic service when it is
run.

ReadWritePaths are suggested to be used only for paths that contain borg
repositories and the backup sources can be specified as ReadOnlyPaths.
2021-08-30 11:20:34 -07:00
Dan Helfman acb2ca79d9 Fix traceback that can occur when dumping a database (#440).
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
2021-08-06 08:58:11 -07:00
Dan Helfman c9211320e1 Fix dev version in changelog.
continuous-integration/drone/push Build is passing Details
2021-08-04 15:32:51 -07:00
Dan Helfman 760286abe1 Dev release bump.
continuous-integration/drone/push Build is failing Details
2021-07-30 09:49:07 -07:00
Dan Helfman 5890a1cb48 Fix "message too long" error when logging to rsyslog (#389).
continuous-integration/drone/push Build was killed Details
2021-07-30 09:48:13 -07:00
Dan Helfman b3f5a9d18f Fix error when configuration file contains "umask" option (#437).
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
2021-07-27 10:04:22 -07:00
Dan Helfman 80b33fbf8a Code style reformatting.
continuous-integration/drone/push Build is passing Details
2021-07-27 09:39:48 -07:00
Dan Helfman 5389ff6160
Merge pull request #41 from mkszuba/tests_no_xxd
tests/integration/test_execute: use plain Python rather than xxd
2021-07-27 09:39:02 -07:00
Marek Szuba e8b8d86592 tests/integration/test_execute: use plain Python rather than xxd
Removes this test's dependencies on vim and /dev/urandom.

Signed-off-by: Marek Szuba <marek.szuba@cern.ch>
2021-07-27 13:50:16 +01:00
Dan Helfman 92d729a9dd Try temporary work around for Drone build bug: https://github.com/drone-plugins/drone-docker/pull/327
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
2021-07-26 16:33:41 -07:00
Dan Helfman c63219936e Wording tweaks to security policy.
continuous-integration/drone/push Build is failing Details
2021-07-26 13:44:14 -07:00
Dan Helfman 0aff497430 Bump version for release.
continuous-integration/drone/push Build is failing Details
2021-07-26 10:17:49 -07:00
Dan Helfman 1f3907a6a5 Fix for failing PostgreSQL directory format test (#430).
continuous-integration/drone/push Build is failing Details
2021-07-26 09:42:14 -07:00
Dan Helfman 2a8692c64f Fix integration test to hopefully work on Alpine (#430).
continuous-integration/drone/push Build is failing Details
2021-07-25 22:50:00 -07:00
Dan Helfman 1709f57ff0 Fix hang when restoring a PostgreSQL "tar" format database dump (#430).
continuous-integration/drone/push Build is failing Details
2021-07-25 22:30:15 -07:00
cadamswaite 89baf757cf Sort imports
continuous-integration/drone/pr Build is failing Details
2021-07-14 23:17:35 +01:00
cadamswaite 4f36fe2b9f Run Black on changed file
continuous-integration/drone/pr Build is failing Details
2021-07-14 22:53:01 +01:00
cadamswaite 510449ce65 Change default retries to 0 2021-07-14 22:49:03 +01:00
cadamswaite 4cc4b8d484 Add queue based retry logic 2021-07-14 22:46:02 +01:00
Dan Helfman 9c972cb0e5 Add documentation note about systemd configuration with alternate install methods (#428).
continuous-integration/drone/push Build is passing Details
2021-06-29 21:38:53 -07:00
Dan Helfman 9b1779065e Pin ruamel.yaml.clib to work around docs build issue. 2021-06-29 21:35:46 -07:00
Dan Helfman 057ec3e59b Add NEWS entry for #379: Suppress console output in sample crontab and systemd service files.
continuous-integration/drone/push Build is passing Details
2021-06-23 10:35:41 -07:00
Dan Helfman bc2e611a74 Suppress console output in sample crontab/systemd service files (#379).
continuous-integration/drone/push Build is passing Details
Reviewed-on: witten/borgmatic#379
2021-06-23 17:32:47 +00:00
Dan Helfman b6d3a1e02f Merge branch 'master' of ssh://projects.torsion.org:3022/witten/borgmatic
continuous-integration/drone/push Build is passing Details
2021-06-23 10:22:07 -07:00
Dan Helfman 54d57e1349 Add test for #407: Fix syslog logging on FreeBSD. 2021-06-23 10:21:45 -07:00
Dan Helfman af0b3da8ed Fix syslog logging on FreeBSD (#407).
continuous-integration/drone/push Build was killed Details
Reviewed-on: witten/borgmatic#407
2021-06-23 17:21:25 +00:00
Dan Helfman 27d37b606b Better error messages! Switch the library used for validating configuration files (from pykwalify to jsonschema).
continuous-integration/drone/push Build is passing Details
2021-06-22 13:27:59 -07:00
Dan Helfman 77a860cc62 Link borgmatic Ansible role from installation documentation.
continuous-integration/drone/push Build is passing Details
2021-06-19 19:04:22 -07:00
Dan Helfman 7bd6374751 Bump version for release.
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
2021-06-17 20:44:54 -07:00
Dan Helfman cf8882f2bc Run arbitrary Borg commands with new "borgmatic borg" action (#425).
continuous-integration/drone/push Build is passing Details
2021-06-17 20:41:44 -07:00
Dan Helfman b37dd1a79e Document use case of running backups conditionally based on laptop power level (#419).
continuous-integration/drone/push Build is passing Details
2021-06-09 10:03:35 -07:00
Dan Helfman fd59776f91 Bump version for release.
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
2021-06-08 11:44:53 -07:00
Dan Helfman 9fd28d2eed Fix error handling to error loudly when Borg gets killed due to running out of memory (#423)! 2021-06-08 11:43:55 -07:00
Dan Helfman f5c61c8013 Move #borgmatic IRC channel from Freenode to Libera Chat due to Freenode takeover drama.
continuous-integration/drone/push Build is passing Details
2021-06-06 21:09:40 -07:00
Dan Helfman 88cb49dcc4 Fix release script based on GitHub authentication query parameter deprecation.
continuous-integration/drone/push Build is passing Details
2021-04-24 20:27:53 -07:00
Dan Helfman 73235e59be Upgrade "py" test dependency (security).
continuous-integration/drone/push Build is passing Details
2021-04-20 10:39:49 -07:00
Dan Helfman 7076a7ff86 Add link to Hetzner storage offering from the documentation (#390).
continuous-integration/drone/push Build is passing Details
2021-04-18 18:03:43 -07:00
Dan Helfman d6e376d32d Fix end-to-end test broken by change in source directory examples.
continuous-integration/drone/push Build is passing Details
2021-04-18 17:54:54 -07:00
Dan Helfman 9016f4be43 Clarify that spaces in path names should not be backslashed in path names (#406).
continuous-integration/drone/push Build is failing Details
2021-04-18 17:28:11 -07:00
Jeffery To d1c403999f
Reduce console output in sample crontab/systemd service files.
continuous-integration/drone/pr Build is failing Details
As borgmatic will log to syslog in the sample crontab/systemd service
files, this makes console output redundant. (cron will mail any console
output to the root user; systemd will log any console output to syslog.)

This adds --verbosity -1 to both files to reduce console output to the
minimum.
2021-04-13 01:40:57 +08:00
Dan Helfman d543109ef4 "Fix" build failure with Alpine Edge by switching from Edge to Alpine 3.13.
continuous-integration/drone/push Build is passing Details
2021-04-09 15:58:23 -07:00
Dan Helfman 7085a45649 Fix build so as not to attempt to build and push documentation for a non-master branch.
continuous-integration/drone/push Build is failing Details
2021-04-09 15:04:09 -07:00
Dan Helfman cf4c603f1d Clarify canonical home of borgmatic in documentation (#398).
continuous-integration/drone/push Build is failing Details
2021-04-09 14:54:21 -07:00
Victor Bouvier-Deleau d2533313bc
Fix syslog logging on FreeBSD
continuous-integration/drone/pr Build is failing Details
The UNIX domain socket to use on FreeBSD is /var/run/log.
See syslogd FreeBSD man page: https://www.freebsd.org/cgi/man.cgi?query=syslogd&sektion=8
2021-04-02 14:11:50 +02:00
Dan Helfman c43b50b6e6 Upgrade PyYAML.
continuous-integration/drone/push Build is passing Details
2021-03-30 22:29:20 -07:00
Dan Helfman c072678936 Add support for ruamel.yaml 0.17.x YAML parsing library (#404).
continuous-integration/drone/push Build is passing Details
2021-03-30 15:53:19 -07:00
Dan Helfman 631da1465e Add support for Python 3.9. 2021-03-30 15:36:26 -07:00
Dan Helfman f29519a5cd
Merge pull request #38 from lukehsiao/patch-1
Fix link to issue tracker in documentation
2021-03-20 15:45:15 -07:00
Luke Hsiao 5d82b42ab8
Fix link to issue tracker in documentation
Fixes: a1d986d952
2021-03-18 17:26:37 -07:00
Dan Helfman 4897a78fd3 Fix database tests broken by PostgreSQL upgrade in Alpine Edge.
continuous-integration/drone/push Build is passing Details
2020-12-24 22:23:09 -08:00
Dan Helfman a1d986d952 Replace "improve this documentation" form with link to support and ticket tracker. 2020-12-24 14:57:51 -08:00
Dan Helfman 717c90a7d0 Clarify in systemd service file comment that security settings are optional.
continuous-integration/drone/push Build is failing Details
2020-12-09 10:08:07 -08:00
Dan Helfman 8fde19a7dc Update systemd service example to return a permission error when a system call isn't permitted.
continuous-integration/drone/push Build is passing Details
2020-11-30 22:14:28 -08:00
Dan Helfman ad7198ba66 Tweak to test failing on some machines.
continuous-integration/drone/push Build is passing Details
2020-11-26 16:22:42 -08:00
Dan Helfman eb4b4cc92b Fix line length in schema.
continuous-integration/drone/push Build is passing Details
2020-11-25 19:21:06 -08:00
Dan Helfman 41bf520585 Document that passphrase is used for Borg keyfile encryption, not just repokey encryption (#373).
continuous-integration/drone/push Build is failing Details
2020-11-25 18:36:23 -08:00
Dan Helfman c0ae01f5d5 Code formatting.
continuous-integration/drone/push Build is passing Details
2020-11-25 17:46:57 -08:00
Dan Helfman 8b8f92d717 Prevent newer (borgmatic-unsupported) version of Black code formatter installing in Alpine Edge. 2020-11-25 17:42:04 -08:00
Dan Helfman ccd1627175 Fix timing-related test error in Alpine Edge.
continuous-integration/drone/push Build is failing Details
2020-11-25 15:48:33 -08:00
Dan Helfman b8a7e23f46 Add missing pip to test script.
continuous-integration/drone/push Build is passing Details
2020-11-22 17:42:58 -08:00
Dan Helfman 1f4f28b4dc Drop support for Python 3.5. Only support black code formatter on Python 3.8+.
continuous-integration/drone/push Build is failing Details
2020-11-22 17:27:21 -08:00
Dan Helfman ea6cd53067 Update versions of test dependencies (test_requirements.txt and test containers).
continuous-integration/drone/push Build is failing Details
2020-11-22 14:48:07 -08:00
Dan Helfman 267138776d Add protection for accidentally releasing a dev version.
continuous-integration/drone/push Build is passing Details
2020-11-21 14:03:39 -08:00
Dan Helfman 604b3d5e17 Bump version.
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
2020-11-21 13:56:19 -08:00
Dan Helfman 667e1e5b15 Update document about new --override behavior (#361).
continuous-integration/drone/push Build is passing Details
2020-11-19 11:01:53 -08:00
Dan Helfman 9b819f32f8 Fix traceback when upgrading old INI-style configuration with upgrade-borgmatic-config (#367).
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
2020-11-18 15:01:10 -08:00
Dan Helfman b619bde037 Fix broken end-to-end test.
continuous-integration/drone/push Build is passing Details
2020-11-18 14:20:07 -08:00
Dan Helfman 97af16bd86 Add Lima-Labs to examples.
continuous-integration/drone/push Build is failing Details
2020-11-18 12:05:38 -08:00
Dan Helfman fa75f89acc Merge ssh://projects.torsion.org:3022/witten/borgmatic
continuous-integration/drone/push Build is failing Details
2020-11-18 11:54:44 -08:00
Dan Helfman 222b61b577 Update changelog with note about #369. 2020-11-18 11:26:20 -08:00
Dan Helfman e77757f0fd Mention placeholders for repositories in documentation; fix #369
continuous-integration/drone/push Build is failing Details
Reviewed-on: witten/borgmatic#371
2020-11-18 19:24:59 +00:00
François Poulain ebac02f118 Mention placeholders for repositories in documentation; fix #369 2020-11-18 20:22:41 +01:00
Dan Helfman 1c9ae81987 Fix signal forwarding from borgmatic to Borg resulting in recursion traceback (#368).
continuous-integration/drone/push Build is passing Details
2020-11-18 10:29:03 -08:00
Dan Helfman 7b1fb68c18 Add referral link to Lima-Labs.
continuous-integration/drone/push Build is passing Details
2020-11-17 14:04:24 -08:00
Dan Helfman 8aa7830f0d Fix broken "--override" action in Python 3.7 and below.
continuous-integration/drone/push Build is passing Details
2020-11-15 13:39:15 -08:00
Dan Helfman 79bee755ee
Merge pull request #37 from ThorpeJosh/patch-1
continuous-integration/drone/push Build is failing Details
Fix typo in large backup docs.
2020-11-03 22:22:12 -08:00
Josh Thorpe cde0ee96ff
Fix typo in large backup docs 2020-11-04 17:20:32 +11:00
Dan Helfman 1ea04aedf0 Merge override values when specifying the "--override" flag multiple times (#361).
continuous-integration/drone/push Build is failing Details
2020-10-23 16:06:00 -07:00
Dan Helfman 446a2bc15a
Fixed minor typo in help for "info" command.
continuous-integration/drone/push Build is passing Details
2020-10-11 15:08:43 -07:00
Diego Blanco 2d10e758e0 Fixed minor typo in help for "info" command 2020-10-12 00:04:15 +02:00
Dan Helfman 0e978299cf Fix traceback when a database hook value is null in a configuration file (#355).
continuous-integration/drone/push Build is passing Details
2020-10-07 15:41:45 -07:00
Dan Helfman d06c1f2943 Minor edit.
continuous-integration/drone/push Build is passing Details
2020-09-21 10:00:38 -07:00
Dan Helfman d768b50b97 Add note about empty source_directories.
continuous-integration/drone/push Build is passing Details
Reviewed-on: witten/borgmatic#358
2020-09-21 16:59:18 +00:00
Luke Murphy 034ade48f2
Add note about empty source_directories 2020-09-21 17:35:26 +02:00
Dan Helfman d1e9f74087 Restore referal link.
continuous-integration/drone/push Build is passing Details
2020-08-27 15:21:53 -07:00
Dan Helfman f262f77dbd Add temporary_directory option to changelog.
continuous-integration/drone/push Build is passing Details
2020-08-26 09:51:55 -07:00
Dan Helfman a3387953a9 Add configuration option for settings Borg's temporary directory (#341).
continuous-integration/drone/push Build is passing Details
Reviewed-on: witten/borgmatic#356
2020-08-26 16:54:49 +00:00
root 7cad5a8608 Add `temporary_directory` as a configuration option defaulting to $TMPDIR 2020-08-26 15:29:02 +00:00
Dan Helfman 9b83fcbf06 Add comment about MemoryDenyWriteExecute value and the tradeoffs thereof.
continuous-integration/drone/push Build is passing Details
2020-08-23 14:11:19 -07:00
Dan Helfman 32a93ce8a2 Loosen systemd memory security setting to allow Healthchecks ping.
continuous-integration/drone/push Build is passing Details
2020-08-22 13:37:34 -07:00
Dan Helfman e428329c03 Add systemd security improvement to NEWS.
continuous-integration/drone/push Build is passing Details
2020-08-22 09:49:06 -07:00
Dan Helfman e844bbee15 Improve systemd security settings (#352).
continuous-integration/drone/push Build is passing Details
2020-08-22 16:50:06 +00:00
Matthias 631c3068a9 systemd security settings 2020-08-22 15:41:25 +02:00
Dan Helfman 79d4888e22 Add documentation navigation links on left side of all documentation pages.
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
2020-08-21 14:27:47 -07:00
Dan Helfman de61fdef48 Mention "before_extract"/"after_extract" in README.
continuous-integration/drone/push Build is passing Details
2020-08-12 14:12:58 -07:00
Dan Helfman 93caeba200 Add before_extract and after_extract hooks (#347).
continuous-integration/drone/push Build is passing Details
Reviewed-on: witten/borgmatic#347
2020-08-12 21:14:15 +00:00
networkjanitor 3c723e8d99 Merge branch 'master' into master 2020-08-12 21:11:45 +00:00
networkjanitor c5776447b9 fixed description strings for before_extract and after_extract 2020-08-12 23:07:57 +02:00
Dan Helfman 5356f487a5 Move before/after_everything hooks so they're easier to find.
continuous-integration/drone/push Build is passing Details
2020-08-12 13:46:31 -07:00
Dan Helfman 72bd96c656 Fix traceback when a configuration directory is non-readable due to directory permissions (#350).
continuous-integration/drone/push Build is passing Details
2020-08-12 11:32:00 -07:00
Dan Helfman f611fe7be3 Clarify documentation overview of monitoring options.
continuous-integration/drone/push Build is passing Details
2020-08-12 10:40:39 -07:00
Dan Helfman dd6ea40a36 Clarify encryption section: It's not just for cron users.
continuous-integration/drone/push Build is passing Details
2020-08-10 14:43:03 -07:00
Dan Helfman ea1274d1c6 Remove inactive referral link.
continuous-integration/drone/push Build is passing Details
2020-08-07 10:55:05 -07:00
Dan Helfman 8526468975
Fix configuration schema comment on after_everything option.
continuous-integration/drone/push Build is passing Details
2020-08-07 09:49:25 -07:00
Jakub Duchateau 95c415f416
Change "before" to "after" in desc 2020-08-07 16:41:35 +02:00
Dan Helfman 06dc336481 Fix schema exclude patterns example path.
continuous-integration/drone/push Build is passing Details
2020-08-06 09:59:48 -07:00
networkjanitor 893fca2816 Add before_extract and after_extract hooks 2020-08-01 16:08:32 +02:00
Dan Helfman 99590cb6b6 Clarify documentation on configuration overrides, specifically the portion about list syntax.
continuous-integration/drone/push Build is passing Details
2020-07-23 21:33:42 -07:00
Dan Helfman b3fd1be5f6 Add "borgmatic export-tar" action to export an archive to a tar-formatted file or stream (#300).
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
2020-07-22 21:53:55 -07:00
Dan Helfman a23083f737 Clarify schema documentation about skipping pruning.
continuous-integration/drone/push Build is passing Details
2020-07-22 10:35:16 -07:00
Dan Helfman 8306b758e8 Add documentation on how to make backups redundant with multiple repositories.
continuous-integration/drone/push Build is passing Details
2020-07-17 16:00:50 -07:00
Dan Helfman 218cbd5289 In database documentation, include command for finding named pipes and special devices.
continuous-integration/drone/push Build is passing Details
2020-07-13 10:56:59 -07:00
Dan Helfman 2ac58670d5 Clarify database documentation about excluding named pipes and character/block devices to prevent hangs.
continuous-integration/drone/push Build is passing Details
2020-07-13 10:45:58 -07:00
Dan Helfman 6f82c9979b Add #339 to NEWS and add test.
continuous-integration/drone/push Build is passing Details
2020-07-07 22:31:17 -07:00
Dan Helfman 0a659a397f Attempt to fix no-repro test failure of log_outputs() (#339).
continuous-integration/drone/push Build is passing Details
2020-07-07 22:05:53 -07:00
Dan Helfman 2781873faf Bump version for release.
continuous-integration/drone/push Build is failing Details
continuous-integration/drone/tag Build is failing Details
2020-06-30 21:47:34 -07:00
Dan Helfman 3aaa89fb08 Formatting.
continuous-integration/drone/push Build is failing Details
2020-06-25 20:25:29 -07:00
Dan Helfman 35d542a676 Fix for traceback when running Cronitor, Cronhub, and PagerDuty monitor hooks (#336).
continuous-integration/drone/push Build was killed Details
2020-06-25 20:23:25 -07:00
Dan Helfman d0b9c436b1 Bump version for release.
continuous-integration/drone/tag Build is passing Details
continuous-integration/drone/push Build is passing Details
2020-06-23 11:21:43 -07:00
Dan Helfman 37cc229749 Fix duplicate logging to Healthchecks and send "after_*" hooks output to Healthchecks (#328).
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is failing Details
2020-06-23 11:01:03 -07:00
Dan Helfman 17c2d109e5 Add tests for pass-through of BORG_* environment variables.
continuous-integration/drone/push Build is passing Details
2020-06-21 14:41:22 -07:00
Dan Helfman c8d5de2179 Fix broken pass-through of BORG_* environment variables to Borg (#327).
continuous-integration/drone/push Build is failing Details
Reviewed-on: witten/borgmatic#327
2020-06-21 21:29:59 +00:00
Dan Helfman 32e15dc905 Add a few more mocks to PostgreSQL SSL tests.
continuous-integration/drone/push Build is passing Details
2020-06-20 14:39:16 -07:00
Dan Helfman f5ebca4907 Add SSL support to PostgreSQL database configuration (#331).
continuous-integration/drone/push Build is passing Details
Reviewed-on: witten/borgmatic#331
2020-06-20 21:24:14 +00:00
Edward Shornock 01db676d68 Change the example for the ssl_mode parameter 2020-06-20 23:32:24 +03:00
Edward Shornock d2d92b1f1a Add tests for the PostgreSQL SSL options 2020-06-20 23:32:24 +03:00
Dan Helfman 27cbe9dfc0 Fix for potential data loss (data not getting backed up) when borgmatic omitted configured source directories in certain situations (#333).
continuous-integration/drone/push Build is passing Details
2020-06-19 20:16:38 -07:00
Edward Shornock 8fb830099f Re-add the ilbpq-ssl documentation URL to the schema
It's been moved from describing `ssl_mode` to the general
postgresql_database description key.
2020-06-19 13:22:39 +03:00
Edward Shornock 463a133a63 Ensure schema lines are less than 80 characters in length 2020-06-19 13:22:39 +03:00
Edward Shornock a16fed8887 Rename PostgreSQL SSL config variables
e.g. s/sslmode/ssl_mode/g to conform with borgmatic naming conventions.
2020-06-19 13:20:14 +03:00
Edward Shornock 33113890f5 Reduce duplication with a common function 2020-06-19 12:32:36 +03:00
Edward Shornock abd47fc14e Add SSL support to PostgreSQL hooks 2020-06-19 02:19:17 +03:00
Dan Helfman 7fb4061759 Improve configuration reference documentation readability via more aggressive word-wrapping in configuration schema descriptions.
continuous-integration/drone/push Build is passing Details
2020-06-17 23:15:12 -07:00
Dan Helfman b320e74ad5 Update documentation code fragments theme to better match the rest of the page.
continuous-integration/drone/push Build is passing Details
2020-06-17 16:02:57 -07:00
Dan Helfman 0ed8f67b9d Documentation feedback: Clarify that a Borg manual install is required, separate from installing borgmatic.
continuous-integration/drone/push Build is passing Details
2020-06-17 11:42:40 -07:00
Ralph Heinkel a12a1121b6 Use values from BORG_* env variables if they are not specified in config.yaml 2020-06-15 19:50:11 +02:00
Dan Helfman 795e18773b Bump version for release.
continuous-integration/drone/tag Build is passing Details
continuous-integration/drone/push Build is passing Details
2020-06-06 15:01:56 -07:00
Dan Helfman aa14449857 Add "borgmatic extract --strip-components" flag to remove leading path components when extracting an archive (#324).
continuous-integration/drone/push Build is passing Details
2020-06-06 14:57:14 -07:00
Dan Helfman ed7b1cd3d7 Add some no-cover pragmas on functions that don't need tests.
continuous-integration/drone/push Build is passing Details
2020-06-06 14:33:06 -07:00
Dan Helfman a155eefa23 Fix for certain configuration options like ssh_command impacting Borg invocations for separate configuration files (#323). 2020-06-06 14:30:04 -07:00
Dan Helfman 398665be9e Allow before_backup and similiar hooks to exit with a soft failure without altering the monitoring status (#292).
continuous-integration/drone/push Build is passing Details
2020-06-02 14:33:41 -07:00
Dan Helfman 6db232d4ac Link to Borgmacator GNOME AppIndicator from monitoring documentation.
continuous-integration/drone/push Build is passing Details
2020-06-02 12:53:08 -07:00
Dan Helfman d7277893fb Fix hang when a stale database dump named pipe from an aborted borgmatic run remains on disk (#316).
continuous-integration/drone/push Build is passing Details
2020-06-02 12:40:32 -07:00
Dan Helfman 00033bf0a8 Tweak comment indentation in generated configuration file for clarity.
continuous-integration/drone/push Build is passing Details
2020-06-02 11:37:13 -07:00
Dan Helfman adda33dc4e Bump version for release.
continuous-integration/drone/tag Build is passing Details
continuous-integration/drone/push Build is passing Details
2020-05-26 13:15:01 -07:00
Dan Helfman 097a09578a Fix enabled database hooks to implicitly set one_file_system configuration option to true to prevent Borg hang. (#315).
continuous-integration/drone/push Build is passing Details
2020-05-26 09:20:14 -07:00
Dan Helfman 65472c8de2 Fix error message when there are no MySQL databases to dump for "all" databases (#319).
continuous-integration/drone/push Build is passing Details
2020-05-26 08:59:04 -07:00
Dan Helfman 602ad9e7ee Add note about indirect dbus dependency.
continuous-integration/drone/push Build is passing Details
2020-05-21 19:56:32 -07:00
Dan Helfman 96df52ec50 Fix hang when streaming a database dump to Borg with implicit duplicate source directories by deduplicating them first (#316).
continuous-integration/drone/push Build is passing Details
2020-05-20 13:33:53 -07:00
Dan Helfman 244dc35bae Global install documentation.
continuous-integration/drone/push Build is passing Details
2020-05-19 14:19:39 -07:00
Dan Helfman d9c9d7d2ee Improve documentation around the installation process. Specifically, making borgmatic commands runnable via the system PATH.
continuous-integration/drone/push Build is passing Details
2020-05-18 20:38:43 -07:00
Dan Helfman 89cb5eb76d Fix regression in support for PostgreSQL's "directory" dump format (#314).
continuous-integration/drone/push Build is passing Details
2020-05-18 11:31:29 -07:00
Dan Helfman 6d3802335e Adding docs note about upgrading to get --files flag.
continuous-integration/drone/push Build is passing Details
2020-05-18 08:43:32 -07:00
Dan Helfman c1d6232b79 Fix documentation to mention new "--files" flag.
continuous-integration/drone/push Build is passing Details
2020-05-15 10:45:58 -07:00
Dan Helfman 048a9ebb52 Add an additional end-to-end database test.
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
2020-05-15 10:12:49 -07:00
Dan Helfman de478f6ff7 Clarify wording in changelog.
continuous-integration/drone/push Build is passing Details
2020-05-14 23:23:21 -07:00
Dan Helfman 3e5a19d95a Add missing test coverage.
continuous-integration/drone/push Build is passing Details
2020-05-14 23:21:43 -07:00
Dan Helfman 2ddf38f99c Fix error handling when executing commands to handle more edge cases.
continuous-integration/drone/push Build is passing Details
2020-05-14 23:04:01 -07:00
Dan Helfman d88f321cef Fix legitimate database dump command errors (exit code 1) not being treated as errors by borgmatic (#310).
continuous-integration/drone/push Build is passing Details
2020-05-14 22:38:38 -07:00
Dan Helfman 74adac6c70 Bump version for release.
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
2020-05-12 21:03:09 -07:00
Dan Helfman 15ea70a71b Flesh out missing test coverage for execute.py.
continuous-integration/drone/push Build is passing Details
2020-05-12 15:49:49 -07:00
Dan Helfman 8b91c01a4c Add some missing test coverage. 2020-05-11 11:34:14 -07:00
Dan Helfman 3bcef72050 Add some missing test mocks that were masking lack of unit coverage.
continuous-integration/drone/push Build is passing Details
2020-05-11 11:17:24 -07:00
Dan Helfman 695c764a01 Merge log output functions into one. 2020-05-11 10:55:50 -07:00
Dan Helfman f7c93ea2e8 Wait for process to finish before trying to check exit status.
continuous-integration/drone/push Build is passing Details
2020-05-09 23:09:48 -07:00
Dan Helfman 1ea047dd94 Remove "borgmatic restore --progress" flag, as it now conflicts with streaming database restores. 2020-05-09 21:53:16 -07:00
Dan Helfman 4b523f9e2c Make database restore output only show at verbosity 2.
continuous-integration/drone/push Build is passing Details
2020-05-08 19:38:33 -07:00
Dan Helfman 6a61070d85 Use shell redirection rather than the --file flag to sidestep synchronization issues when pg_dump/pg_dumpall tries to write to a named pipe.
continuous-integration/drone/push Build is passing Details
2020-05-08 19:11:26 -07:00
Dan Helfman f36082938e Additional test coverage. 2020-05-08 09:48:04 -07:00
Dan Helfman 1ba996ad93 Additional test coverage.
continuous-integration/drone/push Build encountered an error Details
2020-05-07 12:14:27 -07:00
Dan Helfman a23fdf946d Stream database dumps and restores directly to/from Borg without using any additional filesystem space (#258).
continuous-integration/drone/push Build encountered an error Details
2020-05-07 11:44:04 -07:00
Dan Helfman 12cf6913ef Remove unused function parameter.
continuous-integration/drone/push Build is passing Details
2020-05-03 14:07:34 -07:00
Dan Helfman a4eef383c3 Spell out repository consistency check options in more detail.
continuous-integration/drone/push Build is passing Details
2020-04-29 11:08:41 -07:00
Dan Helfman ac124612ad Documentation on macOS launchd permissions issues with work-around for Full Disk Access (#293).
continuous-integration/drone/push Build is passing Details
2020-04-26 16:10:52 -07:00
Dan Helfman 95a479a86e Fix path in release script.
continuous-integration/drone/push Build is failing Details
2020-04-24 16:05:50 -07:00
Dan Helfman e4eff0e3dc Bump version for release.
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
2020-04-24 15:56:56 -07:00
Dan Helfman dce1928dc4
Fix PostgreSQL restore error on "all" database dump. 2020-04-24 15:50:33 -07:00
Nathan Beals 3c8dc4929f Added `test_restore_all_database_dump` unit test.
Updated the other unit tests, as I had to re-arrange argument order
Added an 'all' test for the postgres end-to-end test.

Ran black formatter on it all.
2020-04-24 18:32:53 -04:00
Dan Helfman e511014a28 Fix MySQL restore error on "all" database dump by excluding system tables (#301).
continuous-integration/drone/push Build is passing Details
2020-04-22 12:17:22 -07:00
Dan Helfman bae5f88824 Upgrade test database versions.
continuous-integration/drone/push Build is passing Details
2020-04-21 10:01:26 -07:00
Dan Helfman 41ad98653a https://github.com/docker/compose/issues/2127
continuous-integration/drone/push Build is passing Details
2020-04-21 09:39:20 -07:00
Dan Helfman 6a138aeb6e Move root vs. non-root instructions.
continuous-integration/drone/push Build is passing Details
2020-04-18 13:14:35 -07:00
Dan Helfman f0ce37801b Add root vs. non-root to set up guide.
continuous-integration/drone/push Build is passing Details
2020-04-17 20:30:10 -07:00
Dan Helfman 35f6aba365 Clarify that borgmatic should be run with sudo after a root --user install.
continuous-integration/drone/push Build is passing Details
2020-04-17 09:46:50 -07:00
Nathan Beals f6407bafcb Remove the `--create` flag, was causing an error 2020-04-10 11:24:13 -04:00
Nathan Beals d5e9f67cec Finished. Now uses 'psql' to run the plain-text scripts that pg_dumpall creates 2020-04-10 10:55:53 -04:00
Nathan Beals b14f371c05 First attempt at fixing this pg_dumpall/restoring issue 2020-04-10 09:20:00 -04:00
Dan Helfman 31a5d1b9c4 Docs feedback: Clarify PagerDuty integration instructions.
continuous-integration/drone/push Build is passing Details
2020-03-26 14:14:53 -07:00
Dan Helfman fb4305a953 Add link for Alpine packages of borgmatic to README.
continuous-integration/drone/push Build is passing Details
2020-03-10 21:10:02 -07:00
Dan Helfman eab872823c Clarify license version.
continuous-integration/drone/push Build is passing Details
2020-03-09 15:50:54 -07:00
Dan Helfman 3332750243 More documentation examples of a la carte actions.
continuous-integration/drone/push Build is passing Details
2020-03-09 11:20:18 -07:00
Dan Helfman 4942b7ce4d Feedback on PagerDuty hook documentation.
continuous-integration/drone/push Build is passing Details
2020-02-13 13:11:25 -08:00
Dan Helfman a2af77f363 Maybe fix release signing. 2020-02-03 09:57:34 -08:00
Dan Helfman a7490b56d1 Bump version for release.
continuous-integration/drone/tag Build is passing Details
2020-02-03 09:45:10 -08:00
Dan Helfman 66eb18d5ea Upgrade pip and tox in tests.
continuous-integration/drone/push Build is passing Details
2020-02-02 14:30:26 -08:00
Dan Helfman 46486138b6 Fix PagerDuty link.
continuous-integration/drone/push Build is passing Details
2020-02-01 13:43:37 -08:00
Dan Helfman d6562c4b1e Fix the "--stats" and "--files" flags so that they yield output at verbosity 0 (#290).
continuous-integration/drone/push Build is passing Details
2020-01-31 10:23:36 -08:00
Dan Helfman 1ddde0910c Add security policy, such as it is.
continuous-integration/drone/push Build is passing Details
2020-01-30 15:42:48 -08:00
Dan Helfman 79f3b84ca2 Documentation for "--archive latest" (#289).
continuous-integration/drone/push Build is passing Details
2020-01-29 17:08:03 -08:00
Dan Helfman 55141bda67 Specify "--archive latest" to all actions that accept an archive (#289). 2020-01-29 16:59:02 -08:00
Dan Helfman bc02c123e6 Monitor backups with PagerDuty hook integration (#245).
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
2020-01-27 15:32:09 -08:00
Dan Helfman e76d5ad988 Fix tests.
continuous-integration/drone/push Build is passing Details
2020-01-27 12:56:12 -08:00
Dan Helfman 8ad8a9c422 Add per-action hooks: "before_prune", "after_prune", "before_check", and "after_check" (#255).
continuous-integration/drone/push Build is failing Details
2020-01-27 11:07:07 -08:00
Dan Helfman b15c9b7dab Add missing "how to" text.
continuous-integration/drone/push Build is passing Details
2020-01-24 21:02:56 -08:00
Dan Helfman 2405e97c38 Backup to a removable drive or intermittent server via "soft failure" feature (#284).
continuous-integration/drone/push Build is passing Details
2020-01-24 20:52:48 -08:00
Dan Helfman fdbb2ee905 View consistency check progress via "--progress" flag for "check" action (#287).
continuous-integration/drone/push Build is passing Details
2020-01-24 11:27:16 -08:00
Dan Helfman 94b9ef56be Change "exclude_if_present" option to support multiple filenames, rather than just a single filename (#280).
continuous-integration/drone/push Build is passing Details
2020-01-23 13:41:37 -08:00
Dan Helfman 952168ce25 Fix unwanted console log messages with "list --json" and "info --json". 2020-01-23 13:40:54 -08:00
Dan Helfman 5273037a94 For "list" and "info" actions, show repository names even at verbosity 0.
continuous-integration/drone/push Build is failing Details
2020-01-23 11:17:39 -08:00
Dan Helfman 53e6ff9524 No longer list files or show stats by default at verbosity 2.
continuous-integration/drone/push Build is passing Details
2020-01-22 15:23:49 -08:00
Dan Helfman f66fd1caaa Customize Healthchecks log level via borgmatic "--monitoring-verbosity" flag (#277).
continuous-integration/drone/push Build is passing Details
2020-01-22 15:10:47 -08:00
Dan Helfman d93fdbc5ad Support "--files" and "--stats" flags at verbosity level 0.
continuous-integration/drone/push Build is passing Details
2020-01-22 13:28:24 -08:00
Dan Helfman 58e0439daf Disable per-file logging by default at verbosity 1; opt-in via new --files option.
continuous-integration/drone/push Build is passing Details
2020-01-22 20:36:59 +00:00
palto42 75b5e7254e changes as per comments in PR #283 2020-01-22 19:03:26 +01:00
Dan Helfman 39550a7fe9 Add ~/.config/borgmatic.d as another configuration directory default (#274).
continuous-integration/drone/push Build is passing Details
2020-01-22 09:26:58 -08:00
palto42 5f0c084bee Merge 'upstream/master' into list-files 2020-01-22 18:12:26 +01:00
Dan Helfman 88f06f7921 Revert "Use absolute paths in systemd commands."
continuous-integration/drone/push Build is passing Details
This reverts commit 24e1516ec5.
2020-01-21 16:03:24 -08:00
Dan Helfman 8d12079386 Bump version.
continuous-integration/drone/tag Build is passing Details
continuous-integration/drone/push Build is passing Details
2020-01-21 10:47:29 -08:00
Dan Helfman 7824a034ca Add test for database dump directory removal.
continuous-integration/drone/push Build is passing Details
2020-01-21 10:34:46 -08:00
Dan Helfman 8ef0ba2fae
After a backup of a database dump in directory format, properly remove the dump directory. 2020-01-21 10:29:40 -08:00
Dan Helfman cc384f4324 Second ticket for --json color bug.
continuous-integration/drone/push Build is passing Details
2020-01-21 08:33:41 -08:00
Ronan Dunklau 8a91c79fb0 Support directory format dump cleanup.
Previously, only deleting a dump in a single-file format was supported.
This led to errors when performing a PostgreSQL directory format backup.
2020-01-19 15:15:47 +01:00
Dan Helfman ac1d63bb0d Use more realistic repository examples in README.
continuous-integration/drone/push Build is passing Details
2020-01-18 20:00:18 -08:00
palto42 83632448be updated NEWS for mod. --stats & new --files opt. 2020-01-18 14:57:50 +01:00
palto42 e108526bab disable --stats by default 2020-01-18 14:38:59 +01:00
palto42 e27ba0d08a less detail at v1 + option "--files" for details 2020-01-11 16:38:07 +01:00
Dan Helfman 5afe0e3d63 Disable colored output when "--json" flag is used, so as to produce valid JSON ouput (#276).
continuous-integration/drone/push Build is passing Details
2020-01-04 15:50:41 -08:00
Dan Helfman c52f82f9ce Documentation: Enable and start borgmatic with a single systemctl command.
continuous-integration/drone/push Build is passing Details
2020-01-04 13:37:56 -08:00
Dan Helfman d0c533555e In "borgmatic --help", don't expand $HOME in listing of default "--config" paths.
continuous-integration/drone/push Build is passing Details
2020-01-02 10:37:31 -08:00
Dan Helfman 1995c80e60 Add comment about old versions of systemd and option compatibility (#275).
continuous-integration/drone/push Build is passing Details
2020-01-02 10:05:32 -08:00
Dan Helfman 24e1516ec5 Use absolute paths in systemd commands.
continuous-integration/drone/push Build is passing Details
2020-01-01 17:14:55 -08:00
Dan Helfman 5b1beda82b Add logrotate documentation suggestion.
continuous-integration/drone/push Build is passing Details
2019-12-31 15:06:53 -08:00
Dan Helfman e4f1094569 Bump version for release.
continuous-integration/drone/tag Build is passing Details
2019-12-20 14:04:49 -08:00
Dan Helfman 911668f0c8 Only trigger "on_error" hooks and monitoring failures for "prune", "create", and "check" actions, and not for other actions (#270).
continuous-integration/drone/push Build is passing Details
2019-12-20 13:58:02 -08:00
Dan Helfman 6bfa0783b9 Clarify that the documentation suggestion form is only for documentation.
continuous-integration/drone/push Build is passing Details
2019-12-17 20:16:13 -08:00
Dan Helfman d64bcd5e83 When pruning with verbosity level 1, list pruned and kept archives.
continuous-integration/drone/push Build is passing Details
2019-12-17 20:12:41 -08:00
Dan Helfman ed2ca9f476 Sign release files.
continuous-integration/drone/push Build is passing Details
2019-12-17 20:06:25 -08:00
Dan Helfman f787dfe809 Override particular configuration options from the command-line via "--override" flag (#268).
continuous-integration/drone/push Build is passing Details
2019-12-17 11:46:27 -08:00
Dan Helfman afaabd14a8 Clarify documentation on how /etc/borgmatic.d/ configuration files are interpreted.
continuous-integration/drone/push Build is passing Details
2019-12-13 11:42:17 -08:00
Dan Helfman e009bfeaa2 Update Healthchecks/Cronitor/Cronhub monitoring integrations to fire for "check" and "prune" actions, not just "create" (#249).
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
2019-12-12 22:54:45 -08:00
Dan Helfman f1358d52aa Add "borgmatic init" repository probing fix to NEWS. 2019-12-12 21:50:24 -08:00
Dan Helfman b04b333466
Use --remote-path, --debug and --info when checking for repo existence. 2019-12-13 05:47:47 +00:00
Matthew Daley dd16504329 Use --remote-path, --debug and --info when checking for repo existence
These are currently not being used in the call to `borg info` performed
as part of the borgmatic init command to check whether or not the repo
already exists.
2019-12-13 15:45:12 +13:00
Dan Helfman c6cb21a748 Switch to read-only container filesystem to avoid *.pyc getting created with busted permissions.
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
2019-12-11 21:24:37 -08:00
Dan Helfman 78aa4626fa Remove user switch in container due to CI permission issue.
continuous-integration/drone/push Build is passing Details
2019-12-11 16:58:08 -08:00
Dan Helfman d2df224da8 Use busybox short option to su.
continuous-integration/drone/push Build is failing Details
2019-12-11 16:46:24 -08:00
Dan Helfman 464ff2fe96 Run end-to-end tests on developer machines with Docker Compose for approximate parity with continuous integration tests.
continuous-integration/drone/push Build was killed Details
2019-12-11 16:43:01 -08:00
Dan Helfman 0cc711173a Merge branch 'master' into end-to-end-database-tests
continuous-integration/drone/push Build is passing Details
2019-12-11 12:27:14 -08:00
Dan Helfman 14e5cfc8f8 Support piping "borgmatic list" output to grep. Retain colored output when piping/redirecting (#271).
continuous-integration/drone/push Build is passing Details
2019-12-11 12:12:25 -08:00
Dan Helfman b8b888090d Select Postgres service to work with particular client version.
continuous-integration/drone/push Build is passing Details
2019-12-10 21:41:15 -08:00
Dan Helfman 68281339b7 Black.
continuous-integration/drone/push Build is failing Details
2019-12-10 16:57:12 -08:00
Dan Helfman 2e5be3d3f1 Add missing psql.
continuous-integration/drone/push Build was killed Details
2019-12-10 16:52:59 -08:00
Dan Helfman abd31a94fb Ports fix?
continuous-integration/drone/push Build was killed Details
2019-12-10 16:47:09 -08:00
Dan Helfman 01e2cf08d1 Fix Drone CI services syntax. 2019-12-10 16:43:43 -08:00
Dan Helfman 9f821862b7 End-to-end tests for database dump and restore. 2019-12-10 16:41:01 -08:00
Dan Helfman 8660af745e Optionally change the internal database dump path via "borgmatic_source_directory" option in location configuration section (#259).
continuous-integration/drone/push Build is passing Details
2019-12-10 16:04:34 -08:00
Dan Helfman 826e4352d1 Filter listed paths via "borgmatic list --path" flag (#269).
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
2019-12-08 14:07:02 -08:00
Dan Helfman b94999bba4 Fix "borgmatic umount" so it only runs Borg once instead of once per repository / configuration file.
continuous-integration/drone/push Build is passing Details
2019-12-07 21:36:51 -08:00
Dan Helfman 65cc4c9429 Fix "--repository" flag to accept relative paths.
continuous-integration/drone/push Build is passing Details
2019-12-06 16:29:41 -08:00
Dan Helfman df2be9620b Mount whole repositories via "borgmatic mount" without any "--archive" flag (#253).
continuous-integration/drone/push Build is passing Details
2019-12-06 15:58:54 -08:00
Dan Helfman 2ab9daaa0f Attempt to repair any inconsistencies found during a consistency check via "borgmatic check --repair" flag (#266).
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
2019-12-04 16:07:00 -08:00
Dan Helfman 0c6c61a272 Pass extra options directly to particular Borg commands, handy for Borg options that borgmatic does not yet support natively (#235).
continuous-integration/drone/push Build is passing Details
2019-12-04 15:48:10 -08:00
Dan Helfman 00f62ca023 Fix for "before_backup" hook not triggering an error when the command contains "borg" and has an exit code of 1 (#256).
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
2019-11-30 16:55:05 -08:00
Dan Helfman 9b2ca15de6 Fix for garbled Borg file listing when using "borgmatic create --progress" with verbosity level 1 or 2 (#257).
continuous-integration/drone/push Build is passing Details
2019-11-30 15:31:36 -08:00
Dan Helfman c4aa34bf5c Fix for missing Healthchecks monitoring payload or HTTP 500 due to incorrect unicode encoding (#260).
continuous-integration/drone/push Build is passing Details
2019-11-30 14:51:32 -08:00
Dan Helfman 4385f2a36a Merge branch 'master' of github.com:witten/borgmatic
continuous-integration/drone/tag Build is passing Details
continuous-integration/drone/push Build is passing Details
2019-11-25 15:28:21 -08:00
Dan Helfman ed6a9dadf8
Fix for database dump removal incorrectly skipping some database dumps. 2019-11-25 23:28:15 +00:00
Dan Helfman d978a2d190 Fix for database dump removal incorrectly skipping some database dumps. 2019-11-25 15:27:59 -08:00
Dan Helfman 375036e409 Support for mounting an archive as a FUSE filesystem via "borgmatic mount" action, and unmounting via "borgmatic umount" (#123).
continuous-integration/drone/push Build is passing Details
2019-11-25 14:56:20 -08:00
Raphael Heinrich 99168c1035 Fix error on cleanup multiple database dumps 2019-11-25 22:07:42 +01:00
Dan Helfman f4a231420f Show summary log errors regardless of verbosity. Log the "summary:" header with level based on the contained logs.
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
2019-11-25 10:31:09 -08:00
Dan Helfman 55ebfdda39 Show full error logs at "--verbosity 0" so you can see command output without upping the verbosity level.
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
2019-11-19 10:09:25 -08:00
Dan Helfman e63e2e0852 Rephrasing verbosity levels.
continuous-integration/drone/push Build is passing Details
2019-11-17 22:52:26 -08:00
Dan Helfman edc4b9e60e Update the command-line help description.
continuous-integration/drone/push Build is passing Details
2019-11-17 22:49:16 -08:00
Dan Helfman 78ff734e6c Add note about error logs only flowing to Healthchecks for the "create" action.
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
2019-11-17 19:10:11 -08:00
Dan Helfman 2cc743cf47 With "borgmatic check", consider Borg warnings as errors (#247). 2019-11-17 19:06:28 -08:00
Dan Helfman d99e6d1994 Remove obnoxious log message when Healthchecks hook sends failure for check action. 2019-11-17 18:55:40 -08:00
Dan Helfman 50f62d73b7 When using the Healthchecks monitoring hook, include borgmatic logs in the payloads for completion and failure pings (#241).
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
2019-11-17 16:54:27 -08:00
Dan Helfman 26a89de790 Higher resolution BorgBase logo.
continuous-integration/drone/push Build is passing Details
2019-11-14 12:39:33 -08:00
Dan Helfman c2276b18c5 Add troubleshooting documentation for MySQL lock table errors (#244).
continuous-integration/drone/push Build is passing Details
2019-11-14 10:33:47 -08:00
Dan Helfman 693434f8aa Icon spacing hack that'll show up on GitHub.
continuous-integration/drone/push Build is passing Details
2019-11-14 09:45:38 -08:00
Dan Helfman 1e8edc05e9 Marketing.
continuous-integration/drone/push Build is passing Details
2019-11-14 09:34:53 -08:00
Dan Helfman 1f166a47e9 Fix failing test due to dictionary order (or the lack thereof..) in Python 3.5.
continuous-integration/drone/push Build is passing Details
2019-11-13 16:42:58 -08:00
Dan Helfman 9ee6151999 Add link to unofficial Fedora package of borgmatic that's more up-to-date.
continuous-integration/drone/push Build is failing Details
2019-11-13 14:59:49 -08:00
Dan Helfman 6cdc92bd0c Fix for "borgmatic restore" showing success and incorrectly extracting archive files, even when no databases are configured to restore (#246).
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
2019-11-13 10:41:57 -08:00
Dan Helfman 612e1fea67 Add database version to issue template.
continuous-integration/drone/push Build is passing Details
2019-11-13 08:38:58 -08:00
Dan Helfman 0a9f4e8708 Reopen the file given by "--log-file" flag if an external program rotates the log file while borgmatic is running.
continuous-integration/drone/push Build is passing Details
2019-11-12 16:13:25 -08:00
Dan Helfman 781fac3266 Refactor ping monitors (Healthchecks, Cronitor, Cronhub) to share a common invocation and function signature.
continuous-integration/drone/push Build is passing Details
2019-11-12 15:31:07 -08:00
Dan Helfman 4c38810a32 Add note about rsyslog rate limiting.
continuous-integration/drone/push Build is passing Details
2019-11-12 12:32:32 -08:00
Dan Helfman bf0d38ff2a Update docs about MySQL/MariaDB database dumps and restores (#228).
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
2019-11-12 11:59:01 -08:00
Dan Helfman 04e5b42606 Fix repository does not exist error with "borgmatic extract" when repository is remote (#243).
continuous-integration/drone/push Build is passing Details
2019-11-12 11:47:24 -08:00
Dan Helfman 30525c43bf Another edge case: Don't error on non-matching restore path globs (#228).
continuous-integration/drone/push Build is passing Details
2019-11-12 11:24:31 -08:00
Dan Helfman ebeb5efe05 More accurately detecting restore of unconfigured database (#228).
continuous-integration/drone/push Build is passing Details
2019-11-12 11:10:47 -08:00
Dan Helfman a3e939f34b Factor out filtering of database configuration to function with tests (#228).
continuous-integration/drone/push Build is passing Details
2019-11-12 10:39:36 -08:00
Dan Helfman 2a771161e7 Finish test coverage for MySQL restore (#228).
continuous-integration/drone/push Build is passing Details
2019-11-12 09:50:26 -08:00
Dan Helfman ded042d8cc First crack at MySQL/MariaDB database restore (#228).
continuous-integration/drone/push Build is passing Details
2019-11-11 21:59:30 -08:00
Dan Helfman 4ed43ae4dc Indicate in AUTHORS that it's not an exhaustive list.
continuous-integration/drone/push Build is passing Details
2019-11-10 14:50:28 -08:00
Dan Helfman 9d29ecf304 Remove MySQL/MariaDB database dumps after backing them up (#228).
continuous-integration/drone/push Build is passing Details
2019-11-08 11:53:27 -08:00
Dan Helfman 427b57e2a9 Database dump hooks for MySQL/MariaDB (#228).
continuous-integration/drone/push Build is passing Details
2019-11-08 11:17:52 -08:00
Dan Helfman e4f0a336c2 Fix installation path in docs.
continuous-integration/drone/push Build is passing Details
2019-11-07 11:09:27 -08:00
Dan Helfman 68459c6795 Add note about setting PATH environment variable when installing borgmatic.
continuous-integration/drone/push Build was killed Details
2019-11-07 11:05:41 -08:00
Dan Helfman 17fda7281a Monitor backups with Cronhub hook integration. Fix Healthchecks/Cronitor hooks to respect dry run.
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
2019-11-07 10:08:44 -08:00
Dan Helfman ac777965d0 Fix regression of generate-borgmatic-config working without --source flag (#239).
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
2019-11-06 09:52:21 -08:00
Dan Helfman 31d3bc9bd8 In documentation, clarify when Healthchecks/Cronitor hooks fire in relation to other hooks (#238).
continuous-integration/drone/push Build was killed Details
2019-11-06 09:44:46 -08:00
Dan Helfman 2115eeb6a2 Upgrade your borgmatic configuration to get new options and comments via "generate-borgmatic-config --source" (#239).
continuous-integration/drone/push Build is failing Details
2019-11-06 09:31:00 -08:00
Dan Helfman 08f017bc3e Remove broken Tox Python version filter when running Black (code formatter) environment directly.
continuous-integration/drone/push Build is passing Details
2019-11-05 09:36:30 -08:00
Dan Helfman 7bc9482970 Add verbosity level -1 to NEWS.
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
2019-11-03 17:03:19 -08:00
Dan Helfman 57ffad4e04 Verbosity level "-1" for even quieter output: Errors only (#236).
continuous-integration/drone/push Build is passing Details
2019-11-04 01:02:13 +00:00
Dan Helfman 5422d14f93 Add Python 3.8.
continuous-integration/drone/push Build is passing Details
2019-11-03 14:12:46 -08:00
Dan Helfman e6d8c736d0 Documentation feedback: Don't hard-code Python 3.7 for Black.
continuous-integration/drone/push Build was killed Details
2019-11-03 14:08:19 -08:00
palto42 18d3542fbc new verbosity level "-1" for errors 2019-11-03 09:55:19 +01:00
Dan Helfman 93f453cecf Add Python 3.8 to build matrix.
continuous-integration/drone/push Build is passing Details
2019-11-02 16:38:50 -07:00
Dan Helfman 505bb778fa Fix logging docs typo.
continuous-integration/drone/push Build is passing Details
2019-11-02 12:54:03 -07:00
Dan Helfman b09d464162 Add missing PR link.
continuous-integration/drone/push Build is passing Details
2019-11-02 11:27:05 -07:00
Dan Helfman a9104ed090 Handle log file error more consistently with other error. Add --log-file-verbosity flag. Add docs.
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
2019-11-02 11:23:18 -07:00
Dan Helfman 06f134cc71 Log to file instead of syslog via command-line "--log-file" flag (#233).
continuous-integration/drone/push Build is passing Details
2019-11-02 17:43:39 +00:00
palto42 584359b6c0 .gitignore 2019-11-02 14:39:16 +01:00
palto42 26a1a3d1e0 test cases for logfile option 2019-11-02 14:34:16 +01:00
palto42 6da05cbe2d Exception handling for logfile option 2019-11-02 14:33:57 +01:00
palto42 b121290c0f Merge remote-tracking branch 'upstream/master' into logging 2019-11-01 19:44:04 +01:00
palto42 8c8640d0ab file-logger replaces syslog 2019-11-01 18:42:24 +01:00
palto42 22640a9ca0 new option for log-file 2019-10-31 10:44:22 +01:00
278 changed files with 39700 additions and 5952 deletions

View File

@ -1,57 +0,0 @@
---
kind: pipeline
name: python-3-5-alpine-3-10
steps:
- name: build
image: python:3.5-alpine3.10
pull: always
commands:
- scripts/run-tests
---
kind: pipeline
name: python-3-6-alpine-3-10
steps:
- name: build
image: python:3.6-alpine3.10
pull: always
commands:
- scripts/run-tests
---
kind: pipeline
name: python-3-7-alpine-3-10
steps:
- name: build
image: python:3.7-alpine3.10
pull: always
commands:
- scripts/run-tests
---
kind: pipeline
name: python-3-7-alpine-3-7
steps:
- name: build
image: python:3.7-alpine3.7
pull: always
commands:
- scripts/run-tests
---
kind: pipeline
name: documentation
steps:
- name: build
image: plugins/docker
settings:
username:
from_secret: docker_username
password:
from_secret: docker_password
repo: witten/borgmatic-docs
dockerfile: docs/Dockerfile
when:
branch:
- master

View File

@ -1,9 +1,13 @@
const pluginSyntaxHighlight = require("@11ty/eleventy-plugin-syntaxhighlight");
const codeClipboard = require("eleventy-plugin-code-clipboard");
const inclusiveLangPlugin = require("@11ty/eleventy-plugin-inclusive-language");
const navigationPlugin = require("@11ty/eleventy-navigation");
module.exports = function(eleventyConfig) {
eleventyConfig.addPlugin(pluginSyntaxHighlight);
eleventyConfig.addPlugin(inclusiveLangPlugin);
eleventyConfig.addPlugin(navigationPlugin);
eleventyConfig.addPlugin(codeClipboard);
let markdownIt = require("markdown-it");
let markdownItAnchor = require("markdown-it-anchor");
@ -21,8 +25,7 @@ module.exports = function(eleventyConfig) {
}
};
let markdownItAnchorOptions = {
permalink: true,
permalinkClass: "direct-link"
permalink: markdownItAnchor.permalink.headerLink()
};
eleventyConfig.setLibrary(
@ -30,8 +33,13 @@ module.exports = function(eleventyConfig) {
markdownIt(markdownItOptions)
.use(markdownItAnchor, markdownItAnchorOptions)
.use(markdownItReplaceLink)
.use(codeClipboard.markdownItCopyButton)
);
eleventyConfig.addPassthroughCopy({"docs/static": "static"});
eleventyConfig.setLiquidOptions({dynamicPartials: false});
return {
templateFormats: [
"md",

1
.flake8 Normal file
View File

@ -0,0 +1 @@
select = Q0

View File

@ -1,31 +0,0 @@
#### What I'm trying to do and why
#### Steps to reproduce (if a bug)
Include (sanitized) borgmatic configuration files if applicable.
#### Actual behavior (if a bug)
Include (sanitized) `--verbosity 2` output if applicable.
#### Expected behavior (if a bug)
#### Other notes / implementation ideas
#### Environment
**borgmatic version:** [version here]
Use `sudo borgmatic --version` or `sudo pip show borgmatic | grep ^Version`
**borgmatic installation method:** [e.g., Debian package, Docker container, etc.]
**Borg version:** [version here]
Use `sudo borg --version`
**Python version:** [version here]
Use `python3 --version`
**operating system and version:** [OS here]

View File

@ -0,0 +1,77 @@
name: "Bug or question/support"
about: "For filing a bug or getting support"
body:
- type: textarea
id: problem
attributes:
label: What I'm trying to do and why
validations:
required: true
- type: textarea
id: repro_steps
attributes:
label: Steps to reproduce
description: Include (sanitized) borgmatic configuration files if applicable.
validations:
required: false
- type: textarea
id: actual_behavior
attributes:
label: Actual behavior
description: Include (sanitized) `--verbosity 2` output if applicable.
validations:
required: false
- type: textarea
id: expected_behavior
attributes:
label: Expected behavior
validations:
required: false
- type: textarea
id: notes
attributes:
label: Other notes / implementation ideas
validations:
required: false
- type: input
id: borgmatic_version
attributes:
label: borgmatic version
description: Use `sudo borgmatic --version` or `sudo pip show borgmatic | grep ^Version`
validations:
required: false
- type: input
id: borgmatic_install_method
attributes:
label: borgmatic installation method
description: e.g., pip install, Debian package, container, etc.
validations:
required: false
- type: input
id: borg_version
attributes:
label: Borg version
description: Use `sudo borg --version`
validations:
required: false
- type: input
id: python_version
attributes:
label: Python version
description: Use `python3 --version`
validations:
required: false
- type: input
id: database_version
attributes:
label: Database version (if applicable)
description: Use `psql --version` / `mysql --version` / `mongodump --version` / `sqlite3 --version`
validations:
required: false
- type: input
id: operating_system_version
attributes:
label: Operating system and version
description: On Linux, use `cat /etc/os-release`
validations:
required: false

View File

@ -0,0 +1 @@
blank_issues_enabled: true

View File

@ -0,0 +1,15 @@
name: "Feature"
about: "For filing a feature request or idea"
body:
- type: textarea
id: request
attributes:
label: What I'd like to do and why
validations:
required: true
- type: textarea
id: notes
attributes:
label: Other notes / implementation ideas
validations:
required: false

View File

@ -0,0 +1,28 @@
name: build
run-name: ${{ gitea.actor }} is building
on:
push:
branches: [main]
jobs:
test:
runs-on: host
steps:
- uses: actions/checkout@v4
- run: scripts/run-end-to-end-tests
docs:
needs: [test]
runs-on: host
env:
IMAGE_NAME: projects.torsion.org/borgmatic-collective/borgmatic:docs
steps:
- uses: actions/checkout@v4
- run: podman login --username "$USERNAME" --password "$PASSWORD" projects.torsion.org
env:
USERNAME: "${{ secrets.REGISTRY_USERNAME }}"
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"

2
.gitignore vendored
View File

@ -2,7 +2,7 @@
*.pyc
*.swp
.cache
.coverage
.coverage*
.pytest_cache
.tox
__pycache__

View File

@ -10,3 +10,5 @@ newtonne: Read encryption password from external file
Robin `ypid` Schneider: Support additional options of Borg and add validate-borgmatic-config command
Scott Squires: Custom archive names
Thomas LÉVEIL: Support for a keep_minutely prune option. Support for the --json option
And many others! See the output of "git log".

846
NEWS
View File

@ -1,3 +1,845 @@
1.8.11.dev0
* #815: Add optional Healthchecks auto-provisioning via "create_slug" option.
* #851: Fix lack of file extraction when using "extract --strip-components all" on a path with a
leading slash.
* #854: Fix a traceback when the "data" consistency check is used.
# #857: Fix a traceback with "check --only spot" when the "spot" check is unconfigured.
1.8.10
* #656 (beta): Add a "spot" consistency check that compares file counts and contents between your
source files and the latest archive, ensuring they fall within configured tolerances. This can
catch problems like incorrect excludes, inadvertent deletes, files changed by malware, etc. See
the documentation for more information:
https://torsion.org/borgmatic/docs/how-to/deal-with-very-large-backups/#spot-check
* #779: When "--match-archives *" is used with "check" action, don't skip Borg's orphaned objects
check.
* #842: When a command hook exits with a soft failure, ping the log and finish states for any
configured monitoring hooks.
* #843: Add documentation link to Loki dashboard for borgmatic:
https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#loki-hook
* #847: Fix "--json" error when Borg includes non-JSON warnings in JSON output.
* #848: SECURITY: Mask the password when logging a MongoDB dump or restore command.
* Fix handling of the NO_COLOR environment variable to ignore an empty value.
* Add documentation about backing up containerized databases by configuring borgmatic to exec into
a container to run a dump command:
https://torsion.org/borgmatic/docs/how-to/backup-your-databases/#containers
1.8.9
* #311: Add custom dump/restore command options for MySQL and MariaDB.
* #811: Add an "access_token" option to the ntfy monitoring hook for authenticating
without username/password.
* #827: When the "--json" flag is given, suppress console escape codes so as not to
interfere with JSON output.
* #829: Fix "--override" values containing deprecated section headers not actually overriding
configuration options under deprecated section headers.
* #835: Add support for the NO_COLOR environment variable. See the documentation for more
information:
https://torsion.org/borgmatic/docs/how-to/set-up-backups/#colored-output
* #839: Add log sending for the Apprise logging hook, enabled by default. See the documentation for
more information:
https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#apprise-hook
* #839: Document a potentially breaking shell quoting edge case within error hooks:
https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#error-hooks
* #840: When running the "rcreate" action and the repository already exists but with a different
encryption mode than requested, error.
* Switch from Drone to Gitea Actions for continuous integration.
* Rename scripts/run-end-to-end-dev-tests to scripts/run-end-to-end-tests and use it in both dev
and CI for better dev-CI parity.
* Clarify documentation about restoring a database: borgmatic does not create the database upon
restore.
1.8.8
* #370: For the PostgreSQL hook, pass the "PGSSLMODE" environment variable through to Borg when the
database's configuration omits the "ssl_mode" option.
* #818: Allow the "--repository" flag to match across multiple configuration files.
* #820: Fix broken repository detection in the "rcreate" action with Borg 1.4. The issue did not
occur with other versions of Borg.
* #822: Fix broken escaping logic in the PostgreSQL hook's "pg_dump_command" option.
* SECURITY: Prevent additional shell injection attacks within the PostgreSQL hook.
1.8.7
* #736: Store included configuration files within each backup archive in support of the "config
bootstrap" action. Previously, only top-level configuration files were stored.
* #798: Elevate specific Borg warnings to errors or squash errors to
* warnings. See the documentation for more information:
https://torsion.org/borgmatic/docs/how-to/customize-warnings-and-errors/
* #810: SECURITY: Prevent shell injection attacks within the PostgreSQL hook, the MongoDB hook, the
SQLite hook, the "borgmatic borg" action, and command hook variable/constant interpolation.
* #814: Fix a traceback when providing an invalid "--override" value for a list option.
1.8.6
* #767: Add an "--ssh-command" flag to the "config bootstrap" action for setting a custom SSH
command, as no configuration is available (including the "ssh_command" option) until
bootstrapping completes.
* #794: Fix a traceback when the "repositories" option contains both strings and key/value pairs.
* #800: Add configured repository labels to the JSON output for all actions.
* #802: The "check --force" flag now runs checks even if "check" is in "skip_actions".
* #804: Validate the configured action names in the "skip_actions" option.
* #807: Stream SQLite databases directly to Borg instead of dumping to an intermediate file.
* When logging commands that borgmatic executes, log the environment variables that
borgmatic sets for those commands. (But don't log their values, since they often contain
passwords.)
1.8.5
* #701: Add a "skip_actions" option to skip running particular actions, handy for append-only or
checkless configurations. See the documentation for more information:
https://torsion.org/borgmatic/docs/how-to/set-up-backups/#skipping-actions
* #701: Deprecate the "disabled" value for the "checks" option in favor of the new "skip_actions"
option.
* #745: Constants now apply to included configuration, not just the file doing the includes. As a
side effect of this change, constants no longer apply to option names and only substitute into
configuration values.
* #779: Add a "--match-archives" flag to the "check" action for selecting the archives to check,
overriding the existing "archive_name_format" and "match_archives" options in configuration.
* #779: Only parse "--override" values as complex data types when they're for options of those
types.
* #782: Fix environment variable interpolation within configured repository paths.
* #782: Add configuration constant overriding via the existing "--override" flag.
* #783: Upgrade ruamel.yaml dependency to support version 0.18.x.
* #784: Drop support for Python 3.7, which has been end-of-lifed.
1.8.4
* #715: Add a monitoring hook for sending backup status to a variety of monitoring services via the
Apprise library. See the documentation for more information:
https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#apprise-hook
* #748: When an archive filter causes no matching archives for the "rlist" or "info"
actions, warn the user and suggest how to remove the filter.
* #768: Fix a traceback when an invalid command-line flag or action is used.
* #771: Fix normalization of deprecated sections ("location:", "storage:", "hooks:", etc.) to
support empty sections without erroring.
* #774: Disallow the "--dry-run" flag with the "borg" action, as borgmatic can't guarantee the Borg
command won't have side effects.
1.8.3
* #665: BREAKING: Simplify logging logic as follows: Syslog verbosity is now disabled by
default, but setting the "--syslog-verbosity" flag enables it regardless of whether you're at an
interactive console. Additionally, "--log-file-verbosity" and "--monitoring-verbosity" now
default to 1 (info about steps borgmatic is taking) instead of 0. And both syslog logging and
file logging can be enabled simultaneously.
* #743: Add a monitoring hook for sending backup status and logs to Grafana Loki. See the
documentation for more information:
https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#loki-hook
* #753: When "archive_name_format" is not set, filter archives using the default archive name
format.
* #754: Fix error handling to log command output as one record per line instead of truncating
too-long output and swallowing the end of some Borg error messages.
* #757: Update documentation so "sudo borgmatic" works for pipx borgmatic installations.
* #761: Fix for borgmatic not stopping Borg immediately when the user presses ctrl-C.
* Update documentation to recommend installing/upgrading borgmatic with pipx instead of pip. See the
documentation for more information:
https://torsion.org/borgmatic/docs/how-to/set-up-backups/#installation
https://torsion.org/borgmatic/docs/how-to/upgrade/#upgrading-borgmatic
1.8.2
* #345: Add "key export" action to export a copy of the repository key for safekeeping in case
the original goes missing or gets damaged.
* #727: Add a MariaDB database hook that uses native MariaDB commands instead of the deprecated
MySQL ones. Be aware though that any existing backups made with the "mysql_databases:" hook are
only restorable with a "mysql_databases:" configuration.
* #738: Fix for potential data loss (data not getting restored) in which the database "restore"
action didn't actually restore anything and indicated success anyway.
* Remove the deprecated use of the MongoDB hook's "--db" flag for database restoration.
* Add source code reference documentation for getting oriented with the borgmatic code as a
developer: https://torsion.org/borgmatic/docs/reference/source-code/
1.8.1
* #326: Add documentation for restoring a database to an alternate host:
https://torsion.org/borgmatic/docs/how-to/backup-your-databases/#restore-to-an-alternate-host
* #697: Add documentation for "bootstrap" action:
https://torsion.org/borgmatic/docs/how-to/extract-a-backup/#extract-the-configuration-files-used-to-create-an-archive
* #725: Add "store_config_files" option for disabling the automatic backup of configuration files
used by the "config bootstrap" action.
* #728: Fix for "prune" action error when using the "keep_exclude_tags" option.
* #730: Fix for Borg's interactive prompt on the "check --repair" action automatically getting
answered "NO" even when the "check_i_know_what_i_am_doing" option isn't set.
* #732: Include multiple configuration files with a single "!include". See the documentation for
more information:
https://torsion.org/borgmatic/docs/how-to/make-per-application-backups/#multiple-merge-includes
* #734: Omit "--glob-archives" or "--match-archives" Borg flag when its value would be "*" (meaning
all archives).
1.8.0
* #575: BREAKING: For the "borgmatic borg" action, instead of implicitly injecting
repository/archive into the resulting Borg command-line, pass repository to Borg via an
environment variable and make archive available for explicit use in your commands. See the
documentation for more information:
https://torsion.org/borgmatic/docs/how-to/run-arbitrary-borg-commands/
* #719: Fix an error when running "borg key export" through borgmatic.
* #720: Fix an error when dumping a database and the "exclude_nodump" option is set.
* #724: Add "check_i_know_what_i_am_doing" option to bypass Borg confirmation prompt when running
"check --repair".
* When merging two configuration files, error gracefully if the two files do not adhere to the same
format.
* #721: Remove configuration sections ("location:", "storage:", "hooks:", etc.), while still
keeping deprecated support for them. Now, all options are at the same level, and you don't need
to worry about commenting/uncommenting section headers when you change an option (if you remove
your sections first).
* #721: BREAKING: The retention prefix and the consistency prefix can no longer have different
values (unless one is not set).
* #721: BREAKING: The storage umask and the hooks umask can no longer have different values (unless
one is not set).
* BREAKING: Flags like "--config" that previously took multiple values now need to be given once
per value, e.g. "--config first.yaml --config second.yaml" instead of "--config first.yaml
second.yaml". This prevents argument parsing errors on ambiguous commands.
* BREAKING: Remove the deprecated (and silently ignored) "--successful" flag on the "list" action,
as newer versions of Borg list successful (non-checkpoint) archives by default.
* All deprecated configuration option values now generate warning logs.
* Remove the deprecated (and non-functional) "--excludes" flag in favor of excludes within
configuration.
* Fix an error when logging too-long command output during error handling. Now, long command output
is truncated before logging.
1.7.15
* #326: Add configuration options and command-line flags for backing up a database from one
location while restoring it somewhere else.
* #399: Add a documentation troubleshooting note for MySQL/MariaDB authentication errors.
* #529: Remove upgrade-borgmatic-config command for upgrading borgmatic 1.1.0 INI-style
configuration.
* #529: Deprecate generate-borgmatic-config in favor of new "config generate" action.
* #529: Deprecate validate-borgmatic-config in favor of new "config validate" action.
* #697, #712, #716: Extract borgmatic configuration from backup via new "config bootstrap"
action—even when borgmatic has no configuration yet!
* #669: Add sample systemd user service for running borgmatic as a non-root user.
* #711, #713: Fix an error when "data" check time files are accessed without getting upgraded
first.
1.7.14
* #484: Add a new verbosity level (-2) to disable output entirely (for console, syslog, log file,
or monitoring), so not even errors are shown.
* #688: Tweak archive check probing logic to use the newest timestamp found when multiple exist.
* #659: Add Borg 2 date-based matching flags to various actions for archive selection.
* #703: Fix an error when loading the configuration schema on Fedora Linux.
* #704: Fix "check" action error when repository and archive checks are configured but the archive
check gets skipped due to the configured frequency.
* #706: Fix "--archive latest" on "list" and "info" actions that only worked on the first of
multiple configured repositories.
1.7.13
* #375: Restore particular PostgreSQL schemas from a database dump via "borgmatic restore --schema"
flag. See the documentation for more information:
https://torsion.org/borgmatic/docs/how-to/backup-your-databases/#restore-particular-schemas
* #678: Fix error from PostgreSQL when dumping a database with a "format" of "plain".
* #678: Fix PostgreSQL hook to support "psql_command" and "pg_restore_command" options containing
commands with arguments.
* #678: Fix calls to psql in PostgreSQL hook to ignore "~/.psqlrc", whose settings can break
database dumping.
* #680: Add support for logging each log line as a JSON object via global "--log-json" flag.
* #682: Fix "source_directories_must_exist" option to expand globs and tildes in source directories.
* #684: Rename "master" development branch to "main" to use more inclusive language. You'll need to
update your development checkouts accordingly.
* #686: Add fish shell completion script so you can tab-complete on the borgmatic command-line. See
the documentation for more information:
https://torsion.org/borgmatic/docs/how-to/set-up-backups/#shell-completion
* #687: Fix borgmatic error when not finding the configuration schema for certain "pip install
--editable" development installs.
* #688: Fix archive checks being skipped even when particular archives haven't been checked
recently. This occurred when using multiple borgmatic configuration files with different
"archive_name_format"s, for instance.
* #691: Fix error in "borgmatic restore" action when the configured repository path is relative
instead of absolute.
* #694: Run "borgmatic borg" action without capturing output so interactive prompts and flags like
"--progress" still work.
1.7.12
* #413: Add "log_file" context to command hooks so your scripts can consume the borgmatic log file.
See the documentation for more information:
https://torsion.org/borgmatic/docs/how-to/add-preparation-and-cleanup-steps-to-backups/
* #666, #670: Fix error when running the "info" action with the "--match-archives" or "--archive"
flags. Also fix the "--match-archives"/"--archive" flags to correctly override the
"match_archives" configuration option for the "transfer", "list", "rlist", and "info" actions.
* #668: Fix error when running the "prune" action with both "archive_name_format" and "prefix"
options set.
* #672: Selectively shallow merge certain mappings or sequences when including configuration files.
See the documentation for more information:
https://torsion.org/borgmatic/docs/how-to/make-per-application-backups/#shallow-merge
* #672: Selectively omit list values when including configuration files. See the documentation for
more information:
https://torsion.org/borgmatic/docs/how-to/make-per-application-backups/#list-merge
* #673: View the results of configuration file merging via "validate-borgmatic-config --show" flag.
See the documentation for more information:
https://torsion.org/borgmatic/docs/how-to/make-per-application-backups/#debugging-includes
* Add optional support for running end-to-end tests and building documentation with rootless Podman
instead of Docker.
1.7.11
* #479, #588: BREAKING: Automatically use the "archive_name_format" option to filter which archives
get used for borgmatic actions that operate on multiple archives. Override this behavior with the
new "match_archives" option in the storage section. This change is "breaking" in that it silently
changes which archives get considered for "rlist", "prune", "check", etc. See the documentation
for more information:
https://torsion.org/borgmatic/docs/how-to/make-per-application-backups/#archive-naming
* #479, #588: The "prefix" options have been deprecated in favor of the new "archive_name_format"
auto-matching behavior and the "match_archives" option.
* #658: Add "--log-file-format" flag for customizing the log message format. See the documentation
for more information:
https://torsion.org/borgmatic/docs/how-to/inspect-your-backups/#logging-to-file
* #662: Fix regression in which the "check_repositories" option failed to match repositories.
* #663: Fix regression in which the "transfer" action produced a traceback.
* Add spellchecking of source code during test runs.
1.7.10
* #396: When a database command errors, display and log the error message instead of swallowing it.
* #501: Optionally error if a source directory does not exist via "source_directories_must_exist"
option in borgmatic's location configuration.
* #576: Add support for "file://" paths within "repositories" option.
* #612: Define and use custom constants in borgmatic configuration files. See the documentation for
more information:
https://torsion.org/borgmatic/docs/how-to/make-per-application-backups/#constant-interpolation
* #618: Add support for BORG_FILES_CACHE_TTL environment variable via "borg_files_cache_ttl" option
in borgmatic's storage configuration.
* #623: Fix confusing message when an error occurs running actions for a configuration file.
* #635: Add optional repository labels so you can select a repository via "--repository yourlabel"
at the command-line. See the configuration reference for more information:
https://torsion.org/borgmatic/docs/reference/configuration/
* #649: Add documentation on backing up a database running in a container:
https://torsion.org/borgmatic/docs/how-to/backup-your-databases/#containers
* #655: Fix error when databases are configured and a source directory doesn't exist.
* Add code style plugins to enforce use of Python f-strings and prevent single-letter variables.
To join in the pedantry, refresh your test environment with "tox --recreate".
* Rename scripts/run-full-dev-tests to scripts/run-end-to-end-dev-tests and make it run end-to-end
tests only. Continue using tox to run unit and integration tests.
1.7.9
* #295: Add a SQLite database dump/restore hook.
* #304: Change the default action order when no actions are specified on the command-line to:
"create", "prune", "compact", "check". If you'd like to retain the old ordering ("prune" and
"compact" first), then specify actions explicitly on the command-line.
* #304: Run any command-line actions in the order specified instead of using a fixed ordering.
* #564: Add "--repository" flag to all actions where it makes sense, so you can run borgmatic on
a single configured repository instead of all of them.
* #628: Add a Healthchecks "log" state to send borgmatic logs to Healthchecks without signalling
success or failure.
* #647: Add "--strip-components all" feature on the "extract" action to remove leading path
components of files you extract. Must be used with the "--path" flag.
* Add support for Python 3.11.
1.7.8
* #620: With the "create" action and the "--list" ("--files") flag, only show excluded files at
verbosity 2.
* #621: Add optional authentication to the ntfy monitoring hook.
* With the "create" action, only one of "--list" ("--files") and "--progress" flags can be used.
This lines up with the new behavior in Borg 2.0.0b5.
* Internally support new Borg 2.0.0b5 "--filter" status characters / item flags for the "create"
action.
* Fix the "create" action with the "--dry-run" flag querying for databases when a PostgreSQL/MySQL
"all" database is configured. Now, these queries are skipped due to the dry run.
* Add "--repository" flag to the "rcreate" action to optionally select one configured repository to
create.
* Add "--progress" flag to the "transfer" action, new in Borg 2.0.0b5.
* Add "checkpoint_volume" configuration option to creates checkpoints every specified number of
bytes during a long-running backup, new in Borg 2.0.0b5.
1.7.7
* #642: Add MySQL database hook "add_drop_database" configuration option to control whether dumped
MySQL databases get dropped right before restore.
* #643: Fix for potential data loss (data not getting backed up) when dumping large "directory"
format PostgreSQL/MongoDB databases. Prior to the fix, these dumps would not finish writing to
disk before Borg consumed them. Now, the dumping process completes before Borg starts. This only
applies to "directory" format databases; other formats still stream to Borg without using
temporary disk space.
* Fix MongoDB "directory" format to work with mongodump/mongorestore without error. Prior to this
fix, only the "archive" format worked.
1.7.6
* #393, #438, #560: Optionally dump "all" PostgreSQL/MySQL databases to separate files instead of
one combined dump file, allowing more convenient restores of individual databases. You can enable
this by specifying the database dump "format" option when the database is named "all".
* #602: Fix logs that interfere with JSON output by making warnings go to stderr instead of stdout.
* #622: Fix traceback when include merging configuration files on ARM64.
* #629: Skip warning about excluded special files when no special files have been excluded.
* #630: Add configuration options for database command customization: "list_options",
"restore_options", and "analyze_options" for PostgreSQL, "restore_options" for MySQL, and
"restore_options" for MongoDB.
1.7.5
* #311: Override PostgreSQL dump/restore commands via configuration options.
* #604: Fix traceback when a configuration section is present but lacking any options.
* #607: Clarify documentation examples for include merging and deep merging.
* #611: Fix "data" consistency check to support "check_last" and consistency "prefix" options.
* #613: Clarify documentation about multiple repositories and separate configuration files.
1.7.4
* #596: Fix special file detection erroring when broken symlinks are encountered.
* #597, #598: Fix regression in which "check" action errored on certain systems ("Cannot determine
Borg repository ID").
1.7.3
* #357: Add "break-lock" action for removing any repository and cache locks leftover from Borg
aborting.
* #360: To prevent Borg hangs, unconditionally delete stale named pipes before dumping databases.
* #587: When database hooks are enabled, auto-exclude special files from a "create" action to
prevent Borg from hanging. You can override/prevent this behavior by explicitly setting the
"read_special" option to true.
* #587: Warn when ignoring a configured "read_special" value of false, as true is needed when
database hooks are enabled.
* #589: Update sample systemd service file to allow system "idle" (e.g. a video monitor turning
off) while borgmatic is running.
* #590: Fix for potential data loss (data not getting backed up) when the "patterns_from" option
was used with "source_directories" (or the "~/.borgmatic" path existed, which got injected into
"source_directories" implicitly). The fix is for borgmatic to convert "source_directories" into
patterns whenever "patterns_from" is used, working around a Borg bug:
https://github.com/borgbackup/borg/issues/6994
* #590: In "borgmatic create --list" output, display which files get excluded from the backup due
to patterns or excludes.
* #591: Add support for Borg 2's "--match-archives" flag. This replaces "--glob-archives", which
borgmatic now treats as an alias for "--match-archives". But note that the two flags have
slightly different syntax. See the Borg 2 changelog for more information:
https://borgbackup.readthedocs.io/en/2.0.0b3/changes.html#version-2-0-0b3-2022-10-02
* Fix for "borgmatic --archive latest" not finding the latest archive when a verbosity is set.
1.7.2
* #577: Fix regression in which "borgmatic info --archive ..." showed repository info instead of
archive info with Borg 1.
* #582: Fix hang when database hooks are enabled and "patterns" contains a parent directory of
"~/.borgmatic".
1.7.1
* #542: Make the "source_directories" option optional. This is useful for "check"-only setups or
using "patterns" exclusively.
* #574: Fix for potential data loss (data not getting backed up) when the "patterns" option was
used with "source_directories" (or the "~/.borgmatic" path existed, which got injected into
"source_directories" implicitly). The fix is for borgmatic to convert "source_directories" into
patterns whenever "patterns" is used, working around a Borg bug:
https://github.com/borgbackup/borg/issues/6994
1.7.0
* #463: Add "before_actions" and "after_actions" command hooks that run before/after all the
actions for each repository. These new hooks are a good place to run per-repository steps like
mounting/unmounting a remote filesystem.
* #463: Update documentation to cover per-repository configurations:
https://torsion.org/borgmatic/docs/how-to/make-per-application-backups/
* #557: Support for Borg 2 while still working with Borg 1. This includes new borgmatic actions
like "rcreate" (replaces "init"), "rlist" (list archives in repository), "rinfo" (show repository
info), and "transfer" (for upgrading Borg repositories). For the most part, borgmatic tries to
smooth over differences between Borg 1 and 2 to make your upgrade process easier. However, there
are still a few cases where Borg made breaking changes. See the Borg 2.0 changelog for more
information: https://www.borgbackup.org/releases/borg-2.0.html
* #557: If you install Borg 2, you'll need to manually upgrade your existing Borg 1 repositories
before use. Note that Borg 2 stable is not yet released as of this borgmatic release, so don't
use Borg 2 for production until it is! See the documentation for more information:
https://torsion.org/borgmatic/docs/how-to/upgrade/#upgrading-borg
* #557: Rename several configuration options to match Borg 2: "remote_rate_limit" is now
"upload_rate_limit", "numeric_owner" is "numeric_ids", and "bsd_flags" is "flags". borgmatic
still works with the old options.
* #557: Remote repository paths without the "ssh://" syntax are deprecated but still supported for
now. Remote repository paths containing "~" are deprecated in borgmatic and no longer work in
Borg 2.
* #557: Omitting the "--archive" flag on the "list" action is deprecated when using Borg 2. Use
the new "rlist" action instead.
* #557: The "--dry-run" flag can now be used with the "rcreate"/"init" action.
* #565: Fix handling of "repository" and "data" consistency checks to prevent invalid Borg flags.
* #566: Modify "mount" and "extract" actions to require the "--repository" flag when multiple
repositories are configured.
* #571: BREAKING: Remove old-style command-line action flags like "--create, "--list", etc. If
you're already using actions like "create" and "list" instead, this change should not affect you.
* #571: BREAKING: Rename "--files" flag on "prune" action to "--list", as it lists archives, not
files.
* #571: Add "--list" as alias for "--files" flag on "create" and "export-tar" actions.
* Add support for disabling TLS verification in Healthchecks monitoring hook with "verify_tls"
option.
1.6.6
* #559: Update documentation about configuring multiple consistency checks or multiple databases.
* #560: Fix all database hooks to error when the requested database to restore isn't present in the
Borg archive.
* #561: Fix command-line "--override" flag to continue supporting old configuration file formats.
* #563: Fix traceback with "create" action and "--json" flag when a database hook is configured.
1.6.5
* #553: Fix logging to include the full traceback when Borg experiences an internal error, not just
the first few lines.
* #554: Fix all monitoring hooks to warn if the server returns an HTTP 4xx error. This can happen
with Healthchecks, for instance, when using an invalid ping URL.
* #555: Fix environment variable plumbing so options like "encryption_passphrase" and
"encryption_passcommand" in one configuration file aren't used for other configuration files.
1.6.4
* #546, #382: Keep your repository passphrases and database passwords outside of borgmatic's
configuration file with environment variable interpolation. See the documentation for more
information: https://torsion.org/borgmatic/docs/how-to/provide-your-passwords/
1.6.3
* #541: Add "borgmatic list --find" flag for searching for files across multiple archives, useful
for hunting down that file you accidentally deleted so you can extract it. See the documentation
for more information:
https://torsion.org/borgmatic/docs/how-to/inspect-your-backups/#searching-for-a-file
* #543: Add a monitoring hook for sending push notifications via ntfy. See the documentation for
more information: https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#ntfy-hook
* Fix Bash completion script to no longer alter your shell's settings (complain about unset
variables or error on pipe failures).
* Deprecate "borgmatic list --successful" flag, as listing only non-checkpoint (successful)
archives is now the default in newer versions of Borg.
1.6.2
* #523: Reduce the default consistency check frequency and support configuring the frequency
independently for each check. Also add "borgmatic check --force" flag to ignore configured
frequencies. See the documentation for more information:
https://torsion.org/borgmatic/docs/how-to/deal-with-very-large-backups/#check-frequency
* #536: Fix generate-borgmatic-config to support more complex schema changes like the new
Healthchecks configuration options when the "--source" flag is used.
* #538: Add support for "borgmatic borg debug" command.
* #539: Add "generate-borgmatic-config --overwrite" flag to replace an existing destination file.
* Add Bash completion script so you can tab-complete the borgmatic command-line. See the
documentation for more information:
https://torsion.org/borgmatic/docs/how-to/set-up-backups/#shell-completion
1.6.1
* #294: Add Healthchecks monitoring hook "ping_body_limit" option to configure how many bytes of
logs to send to the Healthchecks server.
* #402: Remove the error when "archive_name_format" is specified but a retention prefix isn't.
* #420: Warn when an unsupported variable is used in a hook command.
* #439: Change connection failures for monitoring hooks (Healthchecks, Cronitor, PagerDuty, and
Cronhub) to be warnings instead of errors. This way, the monitoring system failing does not block
backups.
* #460: Add Healthchecks monitoring hook "send_logs" option to enable/disable sending borgmatic
logs to the Healthchecks server.
* #525: Add Healthchecks monitoring hook "states" option to only enable pinging for particular
monitoring states (start, finish, fail).
* #528: Improve the error message when a configuration override contains an invalid value.
* #531: BREAKING: When deep merging common configuration, merge colliding list values by appending
them. Previously, one list replaced the other.
* #532: When a configuration include is a relative path, load it from either the current working
directory or from the directory containing the file doing the including. Previously, only the
working directory was used.
* Add a randomized delay to the sample systemd timer to spread out the load on a server.
* Change the configuration format for borgmatic monitoring hooks (Healthchecks, Cronitor,
PagerDuty, and Cronhub) to specify the ping URL / integration key as a named option. The intent
is to support additional options (some in this release). This change is backwards-compatible.
* Add emojis to documentation table of contents to make it easier to find particular how-to and
reference guides at a glance.
1.6.0
* #381: BREAKING: Greatly simplify configuration file reuse by deep merging when including common
configuration. See the documentation for more information:
https://torsion.org/borgmatic/docs/how-to/make-per-application-backups/#include-merging
* #473: BREAKING: Instead of executing "before" command hooks before all borgmatic actions run (and
"after" hooks after), execute these hooks right before/after the corresponding action. E.g.,
"before_check" now runs immediately before the "check" action. This better supports running
timing-sensitive tasks like pausing containers. Side effect: before/after command hooks now run
once for each configured repository instead of once per configuration file. Additionally, the
"repositories" interpolated variable has been changed to "repository", containing the path to the
current repository for the hook. See the documentation for more information:
https://torsion.org/borgmatic/docs/how-to/add-preparation-and-cleanup-steps-to-backups/
* #513: Add mention of sudo's "secure_path" option to borgmatic installation documentation.
* #515: Fix "borgmatic borg key ..." to pass parameters to Borg in the correct order.
* #516: Fix handling of TERM signal to exit borgmatic, not just forward the signal to Borg.
* #517: Fix borgmatic exit code (so it's zero) when initial Borg calls fail but later retries
succeed.
* Change Healthchecks logs truncation size from 10k bytes to 100k bytes, corresponding to that
same change on Healthchecks.io.
1.5.24
* #431: Add "working_directory" option to support source directories with relative paths.
* #444: When loading a configuration file that is unreadable due to file permissions, warn instead
of erroring. This supports running borgmatic as a non-root user with configuration in ~/.config
even if there is an unreadable global configuration file in /etc.
* #469: Add "repositories" context to "before_*" and "after_*" command action hooks. See the
documentation for more information:
https://torsion.org/borgmatic/docs/how-to/add-preparation-and-cleanup-steps-to-backups/
* #486: Fix handling of "patterns_from" and "exclude_from" options to error instead of warning when
referencing unreadable files and "create" action is run.
* #507: Fix Borg usage error in the "compact" action when running "borgmatic --dry-run". Now, skip
"compact" entirely during a dry run.
1.5.23
* #394: Compact repository segments and free space with new "borgmatic compact" action. Borg 1.2+
only. Also run "compact" by default when no actions are specified, as "prune" in Borg 1.2 no
longer frees up space unless "compact" is run.
* #394: When using the "atime", "bsd_flags", "numeric_owner", or "remote_rate_limit" options,
tailor the flags passed to Borg depending on the Borg version.
* #480, #482: Fix traceback when a YAML validation error occurs.
1.5.22
* #288: Add database dump hook for MongoDB.
* #470: Move mysqldump options to the beginning of the command due to MySQL bug 30994.
* #471: When command-line configuration override produces a parse error, error cleanly instead of
tracebacking.
* #476: Fix unicode error when restoring particular MySQL databases.
* Drop support for Python 3.6, which has been end-of-lifed.
* Add support for Python 3.10.
1.5.21
* #28: Optionally retry failing backups via "retries" and "retry_wait" configuration options.
* #306: Add "list_options" MySQL configuration option for passing additional arguments to MySQL
list command.
* #459: Add support for old version (2.x) of jsonschema library.
1.5.20
* Re-release with correct version without dev0 tag.
1.5.19
* #387: Fix error when configured source directories are not present on the filesystem at the time
of backup. Now, Borg will complain, but the backup will still continue.
* #455: Mention changing borgmatic path in cron documentation.
* Update sample systemd service file with more granular read-only filesystem settings.
* Move Gitea and GitHub hosting from a personal namespace to an organization for better
collaboration with related projects.
* 1k ★s on GitHub!
1.5.18
* #389: Fix "message too long" error when logging to rsyslog.
* #440: Fix traceback that can occur when dumping a database.
1.5.17
* #437: Fix error when configuration file contains "umask" option.
* Remove test dependency on vim and /dev/urandom.
1.5.16
* #379: Suppress console output in sample crontab and systemd service files.
* #407: Fix syslog logging on FreeBSD.
* #430: Fix hang when restoring a PostgreSQL "tar" format database dump.
* Better error messages! Switch the library used for validating configuration files (from pykwalify
to jsonschema).
* Link borgmatic Ansible role from installation documentation:
https://torsion.org/borgmatic/docs/how-to/set-up-backups/#other-ways-to-install
1.5.15
* #419: Document use case of running backups conditionally based on laptop power level:
https://torsion.org/borgmatic/docs/how-to/backup-to-a-removable-drive-or-an-intermittent-server/
* #425: Run arbitrary Borg commands with new "borgmatic borg" action. See the documentation for
more information: https://torsion.org/borgmatic/docs/how-to/run-arbitrary-borg-commands/
1.5.14
* #390: Add link to Hetzner storage offering from the documentation.
* #398: Clarify canonical home of borgmatic in documentation.
* #406: Clarify that spaces in path names should not be backslashed in path names.
* #423: Fix error handling to error loudly when Borg gets killed due to running out of memory!
* Fix build so as not to attempt to build and push documentation for a non-main branch.
* "Fix" build failure with Alpine Edge by switching from Edge to Alpine 3.13.
* Move #borgmatic IRC channel from Freenode to Libera Chat due to Freenode takeover drama.
IRC connection info: https://torsion.org/borgmatic/#issues
1.5.13
* #373: Document that passphrase is used for Borg keyfile encryption, not just repokey encryption.
* #404: Add support for ruamel.yaml 0.17.x YAML parsing library.
* Update systemd service example to return a permission error when a system call isn't permitted
(instead of terminating borgmatic outright).
* Drop support for Python 3.5, which has been end-of-lifed.
* Add support for Python 3.9.
* Update versions of test dependencies (test_requirements.txt and test containers).
* Only support black code formatter on Python 3.8+. New black dependencies make installation
difficult on older versions of Python.
* Replace "improve this documentation" form with link to support and ticket tracker.
1.5.12
* Fix for previous release with incorrect version suffix in setup.py. No other changes.
1.5.11
* #341: Add "temporary_directory" option for changing Borg's temporary directory.
* #352: Lock down systemd security settings in sample systemd service file.
* #355: Fix traceback when a database hook value is null in a configuration file.
* #361: Merge override values when specifying the "--override" flag multiple times. The previous
behavior was to take the value of the last "--override" flag only.
* #367: Fix traceback when upgrading old INI-style configuration with upgrade-borgmatic-config.
* #368: Fix signal forwarding from borgmatic to Borg resulting in recursion traceback.
* #369: Document support for Borg placeholders in repository names.
1.5.10
* #347: Add hooks that run for the "extract" action: "before_extract" and "after_extract".
* #350: Fix traceback when a configuration directory is non-readable due to directory permissions.
* Add documentation navigation links on left side of all documentation pages.
* Clarify documentation on configuration overrides, specifically the portion about list syntax:
http://torsion.org/borgmatic/docs/how-to/make-per-application-backups/#configuration-overrides
* Clarify documentation overview of monitoring options:
http://torsion.org/borgmatic/docs/how-to/monitor-your-backups/
1.5.9
* #300: Add "borgmatic export-tar" action to export an archive to a tar-formatted file or stream.
* #339: Fix for intermittent timing-related test failure of logging function.
* Clarify database documentation about excluding named pipes and character/block devices to prevent
hangs.
* Add documentation on how to make backups redundant with multiple repositories:
https://torsion.org/borgmatic/docs/how-to/make-backups-redundant/
1.5.8
* #336: Fix for traceback when running Cronitor, Cronhub, and PagerDuty monitor hooks.
1.5.7
* #327: Fix broken pass-through of BORG_* environment variables to Borg.
* #328: Fix duplicate logging to Healthchecks and send "after_*" hooks output to Healthchecks.
* #331: Add SSL support to PostgreSQL database configuration.
* #333: Fix for potential data loss (data not getting backed up) when borgmatic omitted configured
source directories in certain situations. Specifically, this occurred when two source directories
on different filesystems were related by parentage (e.g. "/foo" and "/foo/bar/baz") and the
one_file_system option was enabled.
* Update documentation code fragments theme to better match the rest of the page.
* Improve configuration reference documentation readability via more aggressive word-wrapping in
configuration schema descriptions.
1.5.6
* #292: Allow before_backup and similar hooks to exit with a soft failure without altering the
monitoring status on Healthchecks or other providers. Support this by waiting to ping monitoring
services with a "start" status until after before_* hooks finish. Failures in before_* hooks
still trigger a monitoring "fail" status.
* #316: Fix hang when a stale database dump named pipe from an aborted borgmatic run remains on
disk.
* #323: Fix for certain configuration options like ssh_command impacting Borg invocations for
separate configuration files.
* #324: Add "borgmatic extract --strip-components" flag to remove leading path components when
extracting an archive.
* Tweak comment indentation in generated configuration file for clarity.
* Link to Borgmacator GNOME AppIndicator from monitoring documentation.
1.5.5
* #314: Fix regression in support for PostgreSQL's "directory" dump format. Unlike other dump
formats, the "directory" dump format does not stream directly to/from Borg.
* #315: Fix enabled database hooks to implicitly set one_file_system configuration option to true.
This prevents Borg from reading devices like /dev/zero and hanging.
* #316: Fix hang when streaming a database dump to Borg with implicit duplicate source directories
by deduplicating them first.
* #319: Fix error message when there are no MySQL databases to dump for "all" databases.
* Improve documentation around the installation process. Specifically, making borgmatic commands
runnable via the system PATH and offering a global install option.
1.5.4
* #310: Fix legitimate database dump command errors (exit code 1) not being treated as errors by
borgmatic.
* For database dumps, replace the named pipe on every borgmatic run. This prevent hangs on stale
pipes left over from previous runs.
* Fix error handling to handle more edge cases when executing commands.
1.5.3
* #258: Stream database dumps and restores directly to/from Borg without using any additional
filesystem space. This feature is automatic, and works even on restores from archives made with
previous versions of borgmatic.
* #293: Documentation on macOS launchd permissions issues with work-around for Full Disk Access.
* Remove "borgmatic restore --progress" flag, as it now conflicts with streaming database restores.
1.5.2
* #301: Fix MySQL restore error on "all" database dump by excluding system tables.
* Fix PostgreSQL restore error on "all" database dump by using "psql" for the restore instead of
"pg_restore".
1.5.1
* #289: Tired of looking up the latest successful archive name in order to pass it to borgmatic
actions? Me too. Now you can specify "--archive latest" to all actions that accept an archive
flag.
* #290: Fix the "--stats" and "--files" flags so that they yield output at verbosity 0.
* Reduce the default verbosity of borgmatic logs sent to Healthchecks monitoring hook. Now, it's
warnings and errors only. You can increase the verbosity via the "--monitoring-verbosity" flag.
* Add security policy documentation in SECURITY.md.
1.5.0
* #245: Monitor backups with PagerDuty hook integration. See the documentation for more
information: https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#pagerduty-hook
* #255: Add per-action hooks: "before_prune", "after_prune", "before_check", and "after_check".
* #274: Add ~/.config/borgmatic.d as another configuration directory default.
* #277: Customize Healthchecks log level via borgmatic "--monitoring-verbosity" flag.
* #280: Change "exclude_if_present" option to support multiple filenames that indicate a directory
should be excluded from backups, rather than just a single filename.
* #284: Backup to a removable drive or intermittent server via "soft failure" feature. See the
documentation for more information:
https://torsion.org/borgmatic/docs/how-to/backup-to-a-removable-drive-or-an-intermittent-server/
* #287: View consistency check progress via "--progress" flag for "check" action.
* For "create" and "prune" actions, no longer list files or show detailed stats at any verbosities
by default. You can opt back in with "--files" or "--stats" flags.
* For "list" and "info" actions, show repository names even at verbosity 0.
1.4.22
* #276, #285: Disable colored output when "--json" flag is used, so as to produce valid JSON output.
* After a backup of a database dump in directory format, properly remove the dump directory.
* In "borgmatic --help", don't expand $HOME in listing of default "--config" paths.
1.4.21
* #268: Override particular configuration options from the command-line via "--override" flag. See
the documentation for more information:
https://torsion.org/borgmatic/docs/how-to/make-per-application-backups/#configuration-overrides
* #270: Only trigger "on_error" hooks and monitoring failures for "prune", "create", and "check"
actions, and not for other actions.
* When pruning with verbosity level 1, list pruned and kept archives. Previously, this information
was only shown at verbosity level 2.
1.4.20
* Fix repository probing during "borgmatic init" to respect verbosity flag and remote_path option.
* #249: Update Healthchecks/Cronitor/Cronhub monitoring integrations to fire for "check" and
"prune" actions, not just "create".
1.4.19
* #259: Optionally change the internal database dump path via "borgmatic_source_directory" option
in location configuration section.
* #271: Support piping "borgmatic list" output to grep by logging certain log levels to console
stdout and others to stderr.
* Retain colored output when piping or redirecting in an interactive terminal.
* Add end-to-end tests for database dump and restore. These are run on developer machines with
Docker Compose for approximate parity with continuous integration tests.
1.4.18
* Fix "--repository" flag to accept relative paths.
* Fix "borgmatic umount" so it only runs Borg once instead of once per repository / configuration
file.
* #253: Mount whole repositories via "borgmatic mount" without any "--archive" flag.
* #269: Filter listed paths via "borgmatic list --path" flag.
1.4.17
* #235: Pass extra options directly to particular Borg commands, handy for Borg options that
borgmatic does not yet support natively. Use "extra_borg_options" in the storage configuration
section.
* #266: Attempt to repair any inconsistencies found during a consistency check via
"borgmatic check --repair" flag.
1.4.16
* #256: Fix for "before_backup" hook not triggering an error when the command contains "borg" and
has an exit code of 1.
* #257: Fix for garbled Borg file listing when using "borgmatic create --progress" with
verbosity level 1 or 2.
* #260: Fix for missing Healthchecks monitoring payload or HTTP 500 due to incorrect unicode
encoding.
1.4.15
* Fix for database dump removal incorrectly skipping some database dumps.
* #123: Support for mounting an archive as a FUSE filesystem via "borgmatic mount" action, and
unmounting via "borgmatic umount". See the documentation for more information:
https://torsion.org/borgmatic/docs/how-to/extract-a-backup/#mount-a-filesystem
1.4.14
* Show summary log errors regardless of verbosity level, and log the "summary:" header with a log
level based on the contained summary logs.
1.4.13
* Show full error logs at "--verbosity 0" so you can see command output without upping the
verbosity level.
1.4.12
* #247: With "borgmatic check", consider Borg warnings as errors.
* Dial back the display of inline error logs a bit, so failed command output doesn't appear
multiple times in the logs (well, except for the summary).
1.4.11
* #241: When using the Healthchecks monitoring hook, include borgmatic logs in the payloads for
completion and failure pings.
* With --verbosity level 1 or 2, show error logs both inline when they occur and in the summary
logs at the bottom. With lower verbosity levels, suppress the summary and show error logs when
they occur.
1.4.10
* #246: Fix for "borgmatic restore" showing success and incorrectly extracting archive files, even
when no databases are configured to restore. As this can overwrite files from the archive and
lead to data loss, please upgrade to get the fix before using "borgmatic restore".
* Reopen the file given by "--log-file" flag if an external program rotates the log file while
borgmatic is running.
1.4.9
* #228: Database dump hooks for MySQL/MariaDB, so you can easily dump your databases before backups
run.
* #243: Fix repository does not exist error with "borgmatic extract" when repository is remote.
1.4.8
* Monitor backups with Cronhub hook integration. See the documentation for more information:
https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#cronhub-hook
* Fix Healthchecks/Cronitor hooks to skip actions when the borgmatic "--dry-run" flag is used.
1.4.7
* #238: In documentation, clarify when Healthchecks/Cronitor hooks fire in relation to other hooks.
* #239: Upgrade your borgmatic configuration to get new options and comments via
"generate-borgmatic-config --source". See the documentation for more information:
https://torsion.org/borgmatic/docs/how-to/upgrade/#upgrading-your-configuration
1.4.6
* Verbosity level "-1" for even quieter output: Errors only (#236).
1.4.5
* Log to file instead of syslog via command-line "--log-file" flag (#233).
1.4.4
* #234: Support for Borg --keep-exclude-tags and --exclude-nodump options.
@ -266,7 +1108,7 @@
* #77: Skip non-"*.yaml" config filenames in /etc/borgmatic.d/ so as not to parse backup files,
editor swap files, etc.
* #81: Document user-defined hooks run before/after backup, or on error.
* Add code style guidelines to the documention.
* Add code style guidelines to the documentation.
1.2.0
* #61: Support for Borg --list option via borgmatic command-line to list all archives.
@ -304,7 +1146,7 @@
* #49: Support for Borg experimental --patterns-from and --patterns options for specifying mixed
includes/excludes.
* Moved issue tracker from Taiga to integrated Gitea tracker at
https://projects.torsion.org/witten/borgmatic/issues
https://projects.torsion.org/borgmatic-collective/borgmatic/issues
1.1.12
* #46: Declare dependency on pykwalify 1.6 or above, as older versions yield "Unknown key: version"

188
README.md
View File

@ -2,126 +2,154 @@
title: borgmatic
permalink: index.html
---
<a href="https://build.torsion.org/witten/borgmatic" alt="build status">![Build Status](https://build.torsion.org/api/badges/witten/borgmatic/status.svg?ref=refs/heads/master)</a>
## Overview
## It's your data. Keep it that way.
<img src="https://projects.torsion.org/witten/borgmatic/raw/branch/master/static/borgmatic.png" alt="borgmatic logo" width="150px" style="float: right; padding-left: 1em;">
<img src="docs/static/borgmatic.png" alt="borgmatic logo" width="150px" style="float: right; padding-left: 1em;">
borgmatic is simple, configuration-driven backup software for servers and
workstations. Backup all of your machines from the command-line or scheduled
jobs. No GUI required. Built atop [Borg Backup](https://www.borgbackup.org/),
borgmatic initiates a backup, prunes any old backups according to a retention
policy, and validates backups for consistency. borgmatic supports specifying
your settings in a declarative configuration file, rather than having to put
them all on the command-line, and handles common errors.
workstations. Protect your files with client-side encryption. Backup your
databases too. Monitor it all with integrated third-party services.
Here's an example config file:
The canonical home of borgmatic is at <a href="https://torsion.org/borgmatic">https://torsion.org/borgmatic/</a>
Here's an example configuration file:
```yaml
location:
# List of source directories to backup. Globs are expanded.
source_directories:
- /home
- /etc
- /var/log/syslog*
# List of source directories to backup.
source_directories:
- /home
- /etc
# Paths to local or remote repositories.
repositories:
- user@backupserver:sourcehostname.borg
# Paths of local or remote repositories to backup to.
repositories:
- path: ssh://k8pDxu32@k8pDxu32.repo.borgbase.com/./repo
label: borgbase
- path: /var/lib/backups/local.borg
label: local
# Any paths matching these patterns are excluded from backups.
exclude_patterns:
- /home/*/.cache
# Retention policy for how many backups to keep.
keep_daily: 7
keep_weekly: 4
keep_monthly: 6
retention:
# Retention policy for how many backups to keep in each category.
keep_daily: 7
keep_weekly: 4
keep_monthly: 6
# List of checks to run to validate your backups.
checks:
- name: repository
- name: archives
frequency: 2 weeks
consistency:
# List of consistency checks to run: "repository", "archives", etc.
checks:
- repository
- archives
# Custom preparation scripts to run.
before_backup:
- prepare-for-backup.sh
hooks:
# Preparation scripts to run, databases to dump, and monitoring to perform.
before_backup:
- prepare-for-backup.sh
postgresql_databases:
- name: users
healthchecks: https://hc-ping.com/be067061-cf96-4412-8eae-62b0c50d6a8c
# Databases to dump and include in backups.
postgresql_databases:
- name: users
# Third-party services to notify you if backups aren't happening.
healthchecks:
ping_url: https://hc-ping.com/be067061-cf96-4412-8eae-62b0c50d6a8c
```
borgmatic is hosted at <https://torsion.org/borgmatic> with [source code
available](https://projects.torsion.org/witten/borgmatic). It's also mirrored
on [GitHub](https://github.com/witten/borgmatic) for convenience.
borgmatic is powered by [Borg Backup](https://www.borgbackup.org/).
Want to see borgmatic in action? Check out the <a
href="https://asciinema.org/a/203761" target="_blank">screencast</a>.
## Integrations
<script src="https://asciinema.org/a/203761.js" id="asciicast-203761" async></script>
<a href="https://www.postgresql.org/"><img src="docs/static/postgresql.png" alt="PostgreSQL" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
<a href="https://www.mysql.com/"><img src="docs/static/mysql.png" alt="MySQL" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
<a href="https://mariadb.com/"><img src="docs/static/mariadb.png" alt="MariaDB" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
<a href="https://www.mongodb.com/"><img src="docs/static/mongodb.png" alt="MongoDB" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
<a href="https://sqlite.org/"><img src="docs/static/sqlite.png" alt="SQLite" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
<a href="https://healthchecks.io/"><img src="docs/static/healthchecks.png" alt="Healthchecks" 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://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.borgbase.com/?utm_source=borgmatic"><img src="docs/static/borgbase.png" alt="BorgBase" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
## How-to guides
## Getting started
* [Set up backups with borgmatic](https://torsion.org/borgmatic/docs/how-to/set-up-backups/) ⬅ *Start here!*
* [Make per-application backups](https://torsion.org/borgmatic/docs/how-to/make-per-application-backups/)
* [Deal with very large backups](https://torsion.org/borgmatic/docs/how-to/deal-with-very-large-backups/)
* [Inspect your backups](https://torsion.org/borgmatic/docs/how-to/inspect-your-backups/)
* [Monitor your backups](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/)
* [Extract a backup](https://torsion.org/borgmatic/docs/how-to/extract-a-backup/)
* [Backup your databases](https://torsion.org/borgmatic/docs/how-to/backup-your-databases/)
* [Add preparation and cleanup steps to backups](https://torsion.org/borgmatic/docs/how-to/add-preparation-and-cleanup-steps-to-backups/)
* [Upgrade borgmatic](https://torsion.org/borgmatic/docs/how-to/upgrade/)
* [Develop on borgmatic](https://torsion.org/borgmatic/docs/how-to/develop-on-borgmatic/)
Your first step is to [install and configure
borgmatic](https://torsion.org/borgmatic/docs/how-to/set-up-backups/).
## Reference guides
* [borgmatic configuration reference](https://torsion.org/borgmatic/docs/reference/configuration/)
* [borgmatic command-line reference](https://torsion.org/borgmatic/docs/reference/command-line/)
For additional documentation, check out the links above (left panel on wide screens)
for <a href="https://torsion.org/borgmatic/#documentation">borgmatic how-to and
reference guides</a>.
## Hosting providers
Need somewhere to store your encrypted offsite backups? The following hosting
providers include specific support for Borg/borgmatic. Using these links and
services helps support borgmatic development and hosting. (These are referral
links, but without any tracking scripts or cookies.)
Need somewhere to store your encrypted off-site backups? The following hosting
providers include specific support for Borg/borgmatic—and fund borgmatic
development and hosting when you use these referral links to sign up:
<ul>
<li class="referral"><a href="https://www.rsync.net/cgi-bin/borg.cgi?campaign=borg&adgroup=borgmatic">rsync.net</a>: Cloud Storage provider with full support for borg and any other SSH/SFTP tool</li>
<li class="referral"><a href="https://www.borgbase.com/?utm_source=borgmatic">BorgBase</a>: Borg hosting service with support for monitoring, 2FA, and append-only repos</li>
<li class="referral"><a href="https://hetzner.cloud/?ref=v9dOJ98Ic9I8">Hetzner</a>: A "storage box" that includes support for Borg</li>
</ul>
Additionally, rsync.net has a compatible storage offering, but does not fund
borgmatic development or hosting.
## Support and contributing
### Issues
You've got issues? Or an idea for a feature enhancement? We've got an [issue
tracker](https://projects.torsion.org/witten/borgmatic/issues). In order to
create a new issue or comment on an issue, you'll need to [login
first](https://projects.torsion.org/user/login). Note that you can login with
an existing GitHub account if you prefer.
Are you experiencing an issue with borgmatic? Or do you have an idea for a
feature enhancement? Head on over to our [issue
tracker](https://projects.torsion.org/borgmatic-collective/borgmatic/issues).
In order to create a new issue or add a comment, you'll need to
[register](https://projects.torsion.org/user/sign_up?invite_code=borgmatic)
first. If you prefer to use an existing GitHub account, you can skip account
creation and [login directly](https://projects.torsion.org/user/login).
If you'd like to chat with borgmatic developers or users, head on over to the
`#borgmatic` IRC channel on Freenode, either via <a
href="https://webchat.freenode.net/?channels=borgmatic">web chat</a> or a
native <a href="irc://chat.freenode.net:6697">IRC client</a>.
Also see the [security
policy](https://torsion.org/borgmatic/docs/security-policy/) for any security
issues.
Other questions or comments? Contact <mailto:witten@torsion.org>.
### Social
Follow [borgmatic on Mastodon](https://fosstodon.org/@borgmatic).
### Chat
To chat with borgmatic developers or users, check out the `#borgmatic`
IRC channel on Libera Chat, either via <a
href="https://web.libera.chat/#borgmatic">web chat</a> or a native <a
href="ircs://irc.libera.chat:6697">IRC client</a>. If you don't get a response
right away, please hang around a while—or file a ticket instead.
### Other
Other questions or comments? Contact
[witten@torsion.org](mailto:witten@torsion.org).
### Contributing
borgmatic [source code is
available](https://projects.torsion.org/borgmatic-collective/borgmatic) and is also mirrored
on [GitHub](https://github.com/borgmatic-collective/borgmatic) for convenience.
borgmatic is licensed under the GNU General Public License version 3 or any
later version.
If you'd like to contribute to borgmatic development, please feel free to
submit a [Pull Request](https://projects.torsion.org/witten/borgmatic/pulls)
or open an [issue](https://projects.torsion.org/witten/borgmatic/issues) first
to discuss your idea. We also accept Pull Requests on GitHub, if that's more
your thing. In general, contributions are very welcome. We don't bite!
submit a [Pull
Request](https://projects.torsion.org/borgmatic-collective/borgmatic/pulls) or
open an
[issue](https://projects.torsion.org/borgmatic-collective/borgmatic/issues) to
discuss your idea. Note that you'll need to
[register](https://projects.torsion.org/user/sign_up?invite_code=borgmatic)
first. We also accept Pull Requests on GitHub, if that's more your thing. In
general, contributions are very welcome. We don't bite!
Also, please check out the [borgmatic development
how-to](https://torsion.org/borgmatic/docs/how-to/develop-on-borgmatic/) for

18
SECURITY.md Normal file
View File

@ -0,0 +1,18 @@
---
title: Security policy
permalink: security-policy/index.html
---
## Supported versions
While we want to hear about security vulnerabilities in all versions of
borgmatic, security fixes are only made to the most recently released version.
It's not practical for our small volunteer effort to maintain multiple release
branches and put out separate security patches for each.
## Reporting a vulnerability
If you find a security vulnerability, please [file a
ticket](https://torsion.org/borgmatic/#issues) or [send email
directly](mailto:witten@torsion.org) as appropriate. You should expect to hear
back within a few days at most and generally sooner.

View File

View File

@ -0,0 +1,9 @@
import argparse
def update_arguments(arguments, **updates):
'''
Given an argparse.Namespace instance of command-line arguments and one or more keyword argument
updates to perform, return a copy of the arguments with those updates applied.
'''
return argparse.Namespace(**dict(vars(arguments), **updates))

45
borgmatic/actions/borg.py Normal file
View File

@ -0,0 +1,45 @@
import logging
import borgmatic.borg.borg
import borgmatic.borg.rlist
import borgmatic.config.validate
logger = logging.getLogger(__name__)
def run_borg(
repository,
config,
local_borg_version,
borg_arguments,
global_arguments,
local_path,
remote_path,
):
'''
Run the "borg" action for the given repository.
'''
if borg_arguments.repository is None or borgmatic.config.validate.repositories_match(
repository, borg_arguments.repository
):
logger.info(
f'{repository.get("label", repository["path"])}: Running arbitrary Borg command'
)
archive_name = borgmatic.borg.rlist.resolve_archive_name(
repository['path'],
borg_arguments.archive,
config,
local_borg_version,
global_arguments,
local_path,
remote_path,
)
borgmatic.borg.borg.run_arbitrary_borg(
repository['path'],
config,
local_borg_version,
options=borg_arguments.options,
archive=archive_name,
local_path=local_path,
remote_path=remote_path,
)

View File

@ -0,0 +1,34 @@
import logging
import borgmatic.borg.break_lock
import borgmatic.config.validate
logger = logging.getLogger(__name__)
def run_break_lock(
repository,
config,
local_borg_version,
break_lock_arguments,
global_arguments,
local_path,
remote_path,
):
'''
Run the "break-lock" action for the given repository.
'''
if break_lock_arguments.repository is None or borgmatic.config.validate.repositories_match(
repository, break_lock_arguments.repository
):
logger.info(
f'{repository.get("label", repository["path"])}: Breaking repository and cache locks'
)
borgmatic.borg.break_lock.break_lock(
repository['path'],
config,
local_borg_version,
global_arguments,
local_path=local_path,
remote_path=remote_path,
)

672
borgmatic/actions/check.py Normal file
View File

@ -0,0 +1,672 @@
import datetime
import hashlib
import itertools
import logging
import os
import pathlib
import random
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.state
import borgmatic.config.validate
import borgmatic.execute
import borgmatic.hooks.command
DEFAULT_CHECKS = (
{'name': 'repository', 'frequency': '1 month'},
{'name': 'archives', 'frequency': '1 month'},
)
logger = logging.getLogger(__name__)
def parse_checks(config, only_checks=None):
'''
Given a configuration dict with a "checks" sequence of dicts and an optional list of override
checks, return a tuple of named checks to run.
For example, given a config of:
{'checks': ({'name': 'repository'}, {'name': 'archives'})}
This will be returned as:
('repository', 'archives')
If no "checks" option is present in the config, return the DEFAULT_CHECKS. If a checks value
has a name of "disabled", return an empty tuple, meaning that no checks should be run.
'''
checks = only_checks or tuple(
check_config['name'] for check_config in (config.get('checks', None) or DEFAULT_CHECKS)
)
checks = tuple(check.lower() for check in checks)
if 'disabled' in checks:
logger.warning(
'The "disabled" value for the "checks" option is deprecated and will be removed from a future release; use "skip_actions" instead'
)
if len(checks) > 1:
logger.warning(
'Multiple checks are configured, but one of them is "disabled"; not running any checks'
)
return ()
return checks
def parse_frequency(frequency):
'''
Given a frequency string with a number and a unit of time, return a corresponding
datetime.timedelta instance or None if the frequency is None or "always".
For instance, given "3 weeks", return datetime.timedelta(weeks=3)
Raise ValueError if the given frequency cannot be parsed.
'''
if not frequency:
return None
frequency = frequency.strip().lower()
if frequency == 'always':
return None
try:
number, time_unit = frequency.split(' ')
number = int(number)
except ValueError:
raise ValueError(f"Could not parse consistency check frequency '{frequency}'")
if not time_unit.endswith('s'):
time_unit += 's'
if time_unit == 'months':
number *= 30
time_unit = 'days'
elif time_unit == 'years':
number *= 365
time_unit = 'days'
try:
return datetime.timedelta(**{time_unit: number})
except TypeError:
raise ValueError(f"Could not parse consistency check frequency '{frequency}'")
def filter_checks_on_frequency(
config,
borg_repository_id,
checks,
force,
archives_check_id=None,
):
'''
Given a configuration dict with a "checks" sequence of dicts, a Borg repository ID, a sequence
of checks, whether to force checks to run, and an ID for the archives check potentially being
run (if any), filter down those checks based on the configured "frequency" for each check as
compared to its check time file.
In other words, a check whose check time file's timestamp is too new (based on the configured
frequency) will get cut from the returned sequence of checks. Example:
config = {
'checks': [
{
'name': 'archives',
'frequency': '2 weeks',
},
]
}
When this function is called with that config and "archives" in checks, "archives" will get
filtered out of the returned result if its check time file is newer than 2 weeks old, indicating
that it's not yet time to run that check again.
Raise ValueError if a frequency cannot be parsed.
'''
if not checks:
return checks
filtered_checks = list(checks)
if force:
return tuple(filtered_checks)
for check_config in config.get('checks', DEFAULT_CHECKS):
check = check_config['name']
if checks and check not in checks:
continue
frequency_delta = parse_frequency(check_config.get('frequency'))
if not frequency_delta:
continue
check_time = probe_for_check_time(config, borg_repository_id, check, archives_check_id)
if not check_time:
continue
# 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()
logger.info(
f'Skipping {check} check due to configured frequency; {remaining} until next check (use --force to check anyway)'
)
filtered_checks.remove(check)
return tuple(filtered_checks)
def make_archives_check_id(archive_filter_flags):
'''
Given a sequence of flags to filter archives, return a unique hash corresponding to those
particular flags. If there are no flags, return None.
'''
if not archive_filter_flags:
return None
return hashlib.sha256(' '.join(archive_filter_flags).encode()).hexdigest()
def make_check_time_path(config, borg_repository_id, check_type, archives_check_id=None):
'''
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 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
)
)
if check_type in ('archives', 'data'):
return os.path.join(
borgmatic_source_directory,
'checks',
borg_repository_id,
check_type,
archives_check_id if archives_check_id else 'all',
)
return os.path.join(
borgmatic_source_directory,
'checks',
borg_repository_id,
check_type,
)
def write_check_time(path): # pragma: no cover
'''
Record a check time of now as the modification time of the given path.
'''
logger.debug(f'Writing check time at {path}')
os.makedirs(os.path.dirname(path), mode=0o700, exist_ok=True)
pathlib.Path(path, mode=0o600).touch()
def read_check_time(path):
'''
Return the check time based on the modification time of the given path. Return None if the path
doesn't exist.
'''
logger.debug(f'Reading check time from {path}')
try:
return datetime.datetime.fromtimestamp(os.stat(path).st_mtime)
except FileNotFoundError:
return None
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
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
the check time, e.g.:
~/.borgmatic/checks/1234567890/archives/9876543210
~/.borgmatic/checks/1234567890/archives/all
... and returns the maximum modification time of the files found (if any). The first path
represents a more specific archives check time (a check on a subset of archives), and the second
is a fallback to the last "all" archives check.
For other check types, this function reads from a single check time path, e.g.:
~/.borgmatic/checks/1234567890/repository
'''
check_times = (
read_check_time(group[0])
for group in itertools.groupby(
(
make_check_time_path(config, borg_repository_id, check, archives_check_id),
make_check_time_path(config, borg_repository_id, check),
)
)
)
try:
return max(check_time for check_time in check_times if check_time)
except ValueError:
return None
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:
~/.borgmatic/checks/1234567890/archives
to:
~/.borgmatic/checks/1234567890/archives/all
'''
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)
temporary_path = f'{old_path}.temp'
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}')
try:
os.rename(old_path, temporary_path)
except FileNotFoundError:
pass
os.mkdir(old_path)
os.rename(temporary_path, new_path)
def collect_spot_check_source_paths(
repository, config, local_borg_version, global_arguments, local_path, remote_path
):
'''
Given a repository configuration dict, 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 source paths that Borg would use in an actual create (but only include files and
symlinks).
'''
stream_processes = any(
borgmatic.hooks.dispatch.call_hooks(
'use_streaming',
config,
repository['path'],
borgmatic.hooks.dump.DATA_SOURCE_HOOK_NAMES,
).values()
)
(create_flags, create_positional_arguments, pattern_file, exclude_file) = (
borgmatic.borg.create.make_base_create_command(
dry_run=True,
repository_path=repository['path'],
config=config,
config_paths=(),
local_borg_version=local_borg_version,
global_arguments=global_arguments,
borgmatic_source_directories=(),
local_path=local_path,
remote_path=remote_path,
list_files=True,
stream_processes=stream_processes,
)
)
borg_environment = borgmatic.borg.environment.make_environment(config)
try:
working_directory = os.path.expanduser(config.get('working_directory'))
except TypeError:
working_directory = None
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,
borg_local_path=local_path,
borg_exit_codes=config.get('borg_exit_codes'),
)
paths = tuple(
path_line.split(' ', 1)[1]
for path_line in paths_output.split('\n')
if path_line and path_line.startswith('- ') or path_line.startswith('+ ')
)
return tuple(path for path in paths if os.path.isfile(path) or os.path.islink(path))
BORG_DIRECTORY_FILE_TYPE = 'd'
def collect_spot_check_archive_paths(
repository, archive, config, local_borg_version, global_arguments, local_path, remote_path
):
'''
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).
'''
borgmatic_source_directory = os.path.expanduser(
config.get(
'borgmatic_source_directory', borgmatic.borg.state.DEFAULT_BORGMATIC_SOURCE_DIRECTORY
)
)
return tuple(
path
for line in borgmatic.borg.list.capture_archive_listing(
repository['path'],
archive,
config,
local_borg_version,
global_arguments,
path_format='{type} /{path}{NL}', # noqa: FS003
local_path=local_path,
remote_path=remote_path,
)
for (file_type, path) in (line.split(' ', 1),)
if file_type != BORG_DIRECTORY_FILE_TYPE
if pathlib.Path(borgmatic_source_directory) not in pathlib.Path(path).parents
)
def compare_spot_check_hashes(
repository,
archive,
config,
local_borg_version,
global_arguments,
local_path,
remote_path,
log_label,
source_paths,
):
'''
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, the
remote Borg path, a log label, and spot check source paths, compare the hashes for a sampling of
the source paths with hashes from corresponding paths in the given archive. Return a sequence of
the paths that fail that hash comparison.
'''
# Based on the configured sample percentage, come up with a list of random sample files from the
# source directories.
spot_check_config = next(check for check in config['checks'] if check['name'] == 'spot')
sample_count = max(
int(len(source_paths) * (min(spot_check_config['data_sample_percentage'], 100) / 100)), 1
)
source_sample_paths = tuple(random.sample(source_paths, sample_count))
existing_source_sample_paths = {
source_path for source_path in source_sample_paths if os.path.exists(source_path)
}
logger.debug(
f'{log_label}: Sampling {sample_count} source paths (~{spot_check_config["data_sample_percentage"]}%) for spot check'
)
# Hash each file in the sample paths (if it exists).
hash_output = borgmatic.execute.execute_command_and_capture_output(
(spot_check_config.get('xxh64sum_command', 'xxh64sum'),)
+ tuple(path for path in source_sample_paths if path in existing_source_sample_paths)
)
source_hashes = dict(
(reversed(line.split(' ', 1)) for line in hash_output.splitlines()),
**{path: '' for path in source_sample_paths if path not in existing_source_sample_paths},
)
archive_hashes = dict(
reversed(line.split(' ', 1))
for line in borgmatic.borg.list.capture_archive_listing(
repository['path'],
archive,
config,
local_borg_version,
global_arguments,
list_paths=source_sample_paths,
path_format='{xxh64} /{path}{NL}', # noqa: FS003
local_path=local_path,
remote_path=remote_path,
)
if line
)
# Compare the source hashes with the archive hashes to see how many match.
failing_paths = []
for path, source_hash in source_hashes.items():
archive_hash = archive_hashes.get(path)
if archive_hash is not None and archive_hash == source_hash:
continue
failing_paths.append(path)
return tuple(failing_paths)
def spot_check(
repository,
config,
local_borg_version,
global_arguments,
local_path,
remote_path,
):
'''
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.
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')
try:
spot_check_config = next(
check for check in config.get('checks', ()) if check.get('name') == 'spot'
)
except StopIteration:
raise ValueError('Cannot run spot check because it is unconfigured')
if spot_check_config['data_tolerance_percentage'] > spot_check_config['data_sample_percentage']:
raise ValueError(
'The data_tolerance_percentage must be less than or equal to the data_sample_percentage'
)
source_paths = collect_spot_check_source_paths(
repository,
config,
local_borg_version,
global_arguments,
local_path,
remote_path,
)
logger.debug(f'{log_label}: {len(source_paths)} total source paths for spot check')
archive = borgmatic.borg.rlist.resolve_archive_name(
repository['path'],
'latest',
config,
local_borg_version,
global_arguments,
local_path,
remote_path,
)
logger.debug(f'{log_label}: Using archive {archive} for spot check')
archive_paths = collect_spot_check_archive_paths(
repository,
archive,
config,
local_borg_version,
global_arguments,
local_path,
remote_path,
)
logger.debug(f'{log_label}: {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.
count_delta_percentage = abs(len(source_paths) - len(archive_paths)) / len(source_paths) * 100
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"}'
)
logger.debug(
f'{log_label}: 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"]}%)'
)
failing_paths = compare_spot_check_hashes(
repository,
archive,
config,
local_borg_version,
global_arguments,
local_path,
remote_path,
log_label,
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')
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)}'
)
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'
)
def run_check(
config_filename,
repository,
config,
hook_context,
local_borg_version,
check_arguments,
global_arguments,
local_path,
remote_path,
):
'''
Run the "check" action for the given repository.
Raise ValueError if the Borg repository ID cannot be determined.
'''
if check_arguments.repository and not borgmatic.config.validate.repositories_match(
repository, check_arguments.repository
):
return
borgmatic.hooks.command.execute_hook(
config.get('before_check'),
config.get('umask'),
config_filename,
'pre-check',
global_arguments.dry_run,
**hook_context,
)
logger.info(f'{repository.get("label", repository["path"])}: Running consistency checks')
repository_id = borgmatic.borg.check.get_repository_id(
repository['path'],
config,
local_borg_version,
global_arguments,
local_path=local_path,
remote_path=remote_path,
)
upgrade_check_times(config, repository_id)
configured_checks = parse_checks(config, check_arguments.only_checks)
archive_filter_flags = borgmatic.borg.check.make_archive_filter_flags(
local_borg_version, config, configured_checks, check_arguments
)
archives_check_id = make_archives_check_id(archive_filter_flags)
checks = filter_checks_on_frequency(
config,
repository_id,
configured_checks,
check_arguments.force,
archives_check_id,
)
borg_specific_checks = set(checks).intersection({'repository', 'archives', 'data'})
if borg_specific_checks:
borgmatic.borg.check.check_archives(
repository['path'],
config,
local_borg_version,
check_arguments,
global_arguments,
borg_specific_checks,
archive_filter_flags,
local_path=local_path,
remote_path=remote_path,
)
for check in borg_specific_checks:
write_check_time(make_check_time_path(config, repository_id, check, archives_check_id))
if 'extract' in checks:
borgmatic.borg.extract.extract_last_archive_dry_run(
config,
local_borg_version,
global_arguments,
repository['path'],
config.get('lock_wait'),
local_path,
remote_path,
)
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,
)
write_check_time(make_check_time_path(config, repository_id, 'spot'))
borgmatic.hooks.command.execute_hook(
config.get('after_check'),
config.get('umask'),
config_filename,
'post-check',
global_arguments.dry_run,
**hook_context,
)

View File

@ -0,0 +1,66 @@
import logging
import borgmatic.borg.compact
import borgmatic.borg.feature
import borgmatic.config.validate
import borgmatic.hooks.command
logger = logging.getLogger(__name__)
def run_compact(
config_filename,
repository,
config,
hook_context,
local_borg_version,
compact_arguments,
global_arguments,
dry_run_label,
local_path,
remote_path,
):
'''
Run the "compact" action for the given repository.
'''
if compact_arguments.repository and not borgmatic.config.validate.repositories_match(
repository, compact_arguments.repository
):
return
borgmatic.hooks.command.execute_hook(
config.get('before_compact'),
config.get('umask'),
config_filename,
'pre-compact',
global_arguments.dry_run,
**hook_context,
)
if borgmatic.borg.feature.available(borgmatic.borg.feature.Feature.COMPACT, local_borg_version):
logger.info(
f'{repository.get("label", repository["path"])}: Compacting segments{dry_run_label}'
)
borgmatic.borg.compact.compact_segments(
global_arguments.dry_run,
repository['path'],
config,
local_borg_version,
global_arguments,
local_path=local_path,
remote_path=remote_path,
progress=compact_arguments.progress,
cleanup_commits=compact_arguments.cleanup_commits,
threshold=compact_arguments.threshold,
)
else: # pragma: nocover
logger.info(
f'{repository.get("label", repository["path"])}: Skipping compact (only available/needed in Borg 1.2+)'
)
borgmatic.hooks.command.execute_hook(
config.get('after_compact'),
config.get('umask'),
config_filename,
'post-compact',
global_arguments.dry_run,
**hook_context,
)

View File

View File

@ -0,0 +1,103 @@
import json
import logging
import os
import borgmatic.borg.extract
import borgmatic.borg.rlist
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):
'''
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.
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_manifest_path = os.path.expanduser(
os.path.join(borgmatic_source_directory, 'bootstrap', 'manifest.json')
)
config = {'ssh_command': bootstrap_arguments.ssh_command}
extract_process = 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,
),
[borgmatic_manifest_path],
config,
local_borg_version,
global_arguments,
extract_to_stdout=True,
)
manifest_json = extract_process.stdout.read()
if not manifest_json:
raise ValueError(
'Cannot read configuration paths from archive due to missing bootstrap manifest'
)
try:
manifest_data = json.loads(manifest_json)
except json.JSONDecodeError as error:
raise ValueError(
f'Cannot read configuration paths from archive due to invalid bootstrap manifest JSON: {error}'
)
try:
return manifest_data['config_paths']
except KeyError:
raise ValueError(
'Cannot read configuration paths from archive due to invalid bootstrap manifest'
)
def run_bootstrap(bootstrap_arguments, global_arguments, local_borg_version):
'''
Run the "bootstrap" action for the given repository.
Raise ValueError if the bootstrap configuration could not be loaded.
Raise CalledProcessError or OSError if Borg could not be run.
'''
manifest_config_paths = get_config_paths(
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,
),
[config_path.lstrip(os.path.sep) for config_path in manifest_config_paths],
config,
local_borg_version,
global_arguments,
extract_to_stdout=False,
destination_path=bootstrap_arguments.destination,
strip_components=bootstrap_arguments.strip_components,
progress=bootstrap_arguments.progress,
)

View File

@ -0,0 +1,48 @@
import logging
import borgmatic.config.generate
import borgmatic.config.validate
import borgmatic.logger
logger = logging.getLogger(__name__)
def run_generate(generate_arguments, global_arguments):
'''
Given the generate arguments and the global arguments, each as an argparse.Namespace instance,
run the "generate" action.
Raise FileExistsError if a file already exists at the destination path and the generate
arguments do not have overwrite set.
'''
borgmatic.logger.add_custom_log_levels()
dry_run_label = ' (dry run; not actually writing anything)' if global_arguments.dry_run else ''
logger.answer(
f'Generating a configuration file at: {generate_arguments.destination_filename}{dry_run_label}'
)
borgmatic.config.generate.generate_sample_configuration(
global_arguments.dry_run,
generate_arguments.source_filename,
generate_arguments.destination_filename,
borgmatic.config.validate.schema_filename(),
overwrite=generate_arguments.overwrite,
)
if generate_arguments.source_filename:
logger.answer(
f'''
Merged in the contents of configuration file at: {generate_arguments.source_filename}
To review the changes made, run:
diff --unified {generate_arguments.source_filename} {generate_arguments.destination_filename}'''
)
logger.answer(
'''
This includes all available configuration options with example values, the few
required options as indicated. Please edit the file to suit your needs.
If you ever need help: https://torsion.org/borgmatic/#issues'''
)

View File

@ -0,0 +1,25 @@
import logging
import borgmatic.config.generate
import borgmatic.logger
logger = logging.getLogger(__name__)
def run_validate(validate_arguments, configs):
'''
Given the validate arguments as an argparse.Namespace instance and a dict of configuration
filename to corresponding parsed configuration, run the "validate" action.
Most of the validation is actually performed implicitly by the standard borgmatic configuration
loading machinery prior to here, so this function mainly exists to support additional validate
flags like "--show".
'''
borgmatic.logger.add_custom_log_levels()
if validate_arguments.show:
for config_path, config in configs.items():
if len(configs) > 1:
logger.answer('---')
logger.answer(borgmatic.config.generate.render_configuration(config))

132
borgmatic/actions/create.py Normal file
View File

@ -0,0 +1,132 @@
import importlib.metadata
import json
import logging
import os
import borgmatic.actions.json
import borgmatic.borg.create
import borgmatic.borg.state
import borgmatic.config.validate
import borgmatic.hooks.command
import borgmatic.hooks.dispatch
import borgmatic.hooks.dump
logger = logging.getLogger(__name__)
def create_borgmatic_manifest(config, config_paths, dry_run):
'''
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')
)
if not os.path.exists(borgmatic_manifest_path):
os.makedirs(os.path.dirname(borgmatic_manifest_path), exist_ok=True)
with open(borgmatic_manifest_path, 'w') as config_list_file:
json.dump(
{
'borgmatic_version': importlib.metadata.version('borgmatic'),
'config_paths': config_paths,
},
config_list_file,
)
def run_create(
config_filename,
repository,
config,
config_paths,
hook_context,
local_borg_version,
create_arguments,
global_arguments,
dry_run_label,
local_path,
remote_path,
):
'''
Run the "create" action for the given repository.
If create_arguments.json is True, yield the JSON output from creating the archive.
'''
if create_arguments.repository and not borgmatic.config.validate.repositories_match(
repository, create_arguments.repository
):
return
borgmatic.hooks.command.execute_hook(
config.get('before_backup'),
config.get('umask'),
config_filename,
'pre-backup',
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(
config,
config_paths,
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'))
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'),
config_filename,
'post-backup',
global_arguments.dry_run,
**hook_context,
)

View File

@ -0,0 +1,33 @@
import logging
import borgmatic.borg.export_key
import borgmatic.config.validate
logger = logging.getLogger(__name__)
def run_export_key(
repository,
config,
local_borg_version,
export_arguments,
global_arguments,
local_path,
remote_path,
):
'''
Run the "key export" action for the given repository.
'''
if export_arguments.repository is None or borgmatic.config.validate.repositories_match(
repository, export_arguments.repository
):
logger.info(f'{repository.get("label", repository["path"])}: Exporting repository key')
borgmatic.borg.export_key.export_key(
repository['path'],
config,
local_borg_version,
export_arguments,
global_arguments,
local_path=local_path,
remote_path=remote_path,
)

View File

@ -0,0 +1,50 @@
import logging
import borgmatic.borg.export_tar
import borgmatic.borg.rlist
import borgmatic.config.validate
logger = logging.getLogger(__name__)
def run_export_tar(
repository,
config,
local_borg_version,
export_tar_arguments,
global_arguments,
local_path,
remote_path,
):
'''
Run the "export-tar" action for the given repository.
'''
if export_tar_arguments.repository is None or borgmatic.config.validate.repositories_match(
repository, export_tar_arguments.repository
):
logger.info(
f'{repository["path"]}: Exporting archive {export_tar_arguments.archive} as tar file'
)
borgmatic.borg.export_tar.export_tar_archive(
global_arguments.dry_run,
repository['path'],
borgmatic.borg.rlist.resolve_archive_name(
repository['path'],
export_tar_arguments.archive,
config,
local_borg_version,
global_arguments,
local_path,
remote_path,
),
export_tar_arguments.paths,
export_tar_arguments.destination,
config,
local_borg_version,
global_arguments,
local_path=local_path,
remote_path=remote_path,
tar_filter=export_tar_arguments.tar_filter,
list_files=export_tar_arguments.list_files,
strip_components=export_tar_arguments.strip_components,
)

View File

@ -0,0 +1,68 @@
import logging
import borgmatic.borg.extract
import borgmatic.borg.rlist
import borgmatic.config.validate
import borgmatic.hooks.command
logger = logging.getLogger(__name__)
def run_extract(
config_filename,
repository,
config,
hook_context,
local_borg_version,
extract_arguments,
global_arguments,
local_path,
remote_path,
):
'''
Run the "extract" action for the given repository.
'''
borgmatic.hooks.command.execute_hook(
config.get('before_extract'),
config.get('umask'),
config_filename,
'pre-extract',
global_arguments.dry_run,
**hook_context,
)
if extract_arguments.repository is None or borgmatic.config.validate.repositories_match(
repository, extract_arguments.repository
):
logger.info(
f'{repository.get("label", repository["path"])}: Extracting archive {extract_arguments.archive}'
)
borgmatic.borg.extract.extract_archive(
global_arguments.dry_run,
repository['path'],
borgmatic.borg.rlist.resolve_archive_name(
repository['path'],
extract_arguments.archive,
config,
local_borg_version,
global_arguments,
local_path,
remote_path,
),
extract_arguments.paths,
config,
local_borg_version,
global_arguments,
local_path=local_path,
remote_path=remote_path,
destination_path=extract_arguments.destination,
strip_components=extract_arguments.strip_components,
progress=extract_arguments.progress,
)
borgmatic.hooks.command.execute_hook(
config.get('after_extract'),
config.get('umask'),
config_filename,
'post-extract',
global_arguments.dry_run,
**hook_context,
)

52
borgmatic/actions/info.py Normal file
View File

@ -0,0 +1,52 @@
import logging
import borgmatic.actions.arguments
import borgmatic.actions.json
import borgmatic.borg.info
import borgmatic.borg.rlist
import borgmatic.config.validate
logger = logging.getLogger(__name__)
def run_info(
repository,
config,
local_borg_version,
info_arguments,
global_arguments,
local_path,
remote_path,
):
'''
Run the "info" action for the given repository and archive.
If info_arguments.json is True, yield the JSON output from the info for the archive.
'''
if info_arguments.repository is None or borgmatic.config.validate.repositories_match(
repository, info_arguments.repository
):
if not info_arguments.json:
logger.answer(
f'{repository.get("label", repository["path"])}: Displaying archive summary information'
)
archive_name = borgmatic.borg.rlist.resolve_archive_name(
repository['path'],
info_arguments.archive,
config,
local_borg_version,
global_arguments,
local_path,
remote_path,
)
json_output = borgmatic.borg.info.display_archives_info(
repository['path'],
config,
local_borg_version,
borgmatic.actions.arguments.update_arguments(info_arguments, archive=archive_name),
global_arguments,
local_path,
remote_path,
)
if json_output:
yield borgmatic.actions.json.parse_json(json_output, repository.get('label'))

30
borgmatic/actions/json.py Normal file
View File

@ -0,0 +1,30 @@
import json
import logging
logger = logging.getLogger(__name__)
def parse_json(borg_json_output, label):
'''
Given a Borg JSON output string, parse it as JSON into a dict. Inject the given borgmatic
repository label into it and return the dict.
Raise JSONDecodeError if the JSON output cannot be parsed.
'''
lines = borg_json_output.splitlines()
start_line_index = 0
# Scan forward to find the first line starting with "{" and assume that's where the JSON starts.
for line_index, line in enumerate(lines):
if line.startswith('{'):
start_line_index = line_index
break
json_data = json.loads('\n'.join(lines[start_line_index:]))
if 'repository' not in json_data:
return json_data
json_data['repository']['label'] = label or ''
return json_data

53
borgmatic/actions/list.py Normal file
View File

@ -0,0 +1,53 @@
import logging
import borgmatic.actions.arguments
import borgmatic.actions.json
import borgmatic.borg.list
import borgmatic.config.validate
logger = logging.getLogger(__name__)
def run_list(
repository,
config,
local_borg_version,
list_arguments,
global_arguments,
local_path,
remote_path,
):
'''
Run the "list" action for the given repository and archive.
If list_arguments.json is True, yield the JSON output from listing the archive.
'''
if list_arguments.repository is None or borgmatic.config.validate.repositories_match(
repository, list_arguments.repository
):
if not list_arguments.json:
if list_arguments.find_paths: # pragma: no cover
logger.answer(f'{repository.get("label", repository["path"])}: Searching archives')
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(
repository['path'],
list_arguments.archive,
config,
local_borg_version,
global_arguments,
local_path,
remote_path,
)
json_output = borgmatic.borg.list.list_archive(
repository['path'],
config,
local_borg_version,
borgmatic.actions.arguments.update_arguments(list_arguments, archive=archive_name),
global_arguments,
local_path,
remote_path,
)
if json_output:
yield borgmatic.actions.json.parse_json(json_output, repository.get('label'))

View File

@ -0,0 +1,49 @@
import logging
import borgmatic.borg.mount
import borgmatic.borg.rlist
import borgmatic.config.validate
logger = logging.getLogger(__name__)
def run_mount(
repository,
config,
local_borg_version,
mount_arguments,
global_arguments,
local_path,
remote_path,
):
'''
Run the "mount" action for the given repository.
'''
if mount_arguments.repository is None or borgmatic.config.validate.repositories_match(
repository, mount_arguments.repository
):
if mount_arguments.archive:
logger.info(
f'{repository.get("label", repository["path"])}: Mounting archive {mount_arguments.archive}'
)
else: # pragma: nocover
logger.info(f'{repository.get("label", repository["path"])}: Mounting repository')
borgmatic.borg.mount.mount_archive(
repository['path'],
borgmatic.borg.rlist.resolve_archive_name(
repository['path'],
mount_arguments.archive,
config,
local_borg_version,
global_arguments,
local_path,
remote_path,
),
mount_arguments,
config,
local_borg_version,
global_arguments,
local_path=local_path,
remote_path=remote_path,
)

View File

@ -0,0 +1,56 @@
import logging
import borgmatic.borg.prune
import borgmatic.config.validate
import borgmatic.hooks.command
logger = logging.getLogger(__name__)
def run_prune(
config_filename,
repository,
config,
hook_context,
local_borg_version,
prune_arguments,
global_arguments,
dry_run_label,
local_path,
remote_path,
):
'''
Run the "prune" action for the given repository.
'''
if prune_arguments.repository and not borgmatic.config.validate.repositories_match(
repository, prune_arguments.repository
):
return
borgmatic.hooks.command.execute_hook(
config.get('before_prune'),
config.get('umask'),
config_filename,
'pre-prune',
global_arguments.dry_run,
**hook_context,
)
logger.info(f'{repository.get("label", repository["path"])}: Pruning archives{dry_run_label}')
borgmatic.borg.prune.prune_archives(
global_arguments.dry_run,
repository['path'],
config,
local_borg_version,
prune_arguments,
global_arguments,
local_path=local_path,
remote_path=remote_path,
)
borgmatic.hooks.command.execute_hook(
config.get('after_prune'),
config.get('umask'),
config_filename,
'post-prune',
global_arguments.dry_run,
**hook_context,
)

View File

@ -0,0 +1,41 @@
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,
)

View File

@ -0,0 +1,380 @@
import copy
import logging
import os
import borgmatic.borg.extract
import borgmatic.borg.list
import borgmatic.borg.mount
import borgmatic.borg.rlist
import borgmatic.borg.state
import borgmatic.config.validate
import borgmatic.hooks.dispatch
import borgmatic.hooks.dump
logger = logging.getLogger(__name__)
UNSPECIFIED_HOOK = object()
def get_configured_data_source(
config,
archive_data_source_names,
hook_name,
data_source_name,
configuration_data_source_name=None,
):
'''
Find the first data source with the given hook name and data source name in the configuration
dict and the given archive data source names dict (from hook name to data source names contained
in a particular backup archive). If UNSPECIFIED_HOOK is given as the hook name, search all data
source hooks for the named data source. If a configuration data source name is given, use that
instead of the data source name to lookup the data source in the given hooks configuration.
Return the found data source as a tuple of (found hook name, data source configuration dict) or
(None, None) if not found.
'''
if not configuration_data_source_name:
configuration_data_source_name = data_source_name
if hook_name == UNSPECIFIED_HOOK:
hooks_to_search = {
hook_name: value
for (hook_name, value) in config.items()
if hook_name in borgmatic.hooks.dump.DATA_SOURCE_HOOK_NAMES
}
else:
try:
hooks_to_search = {hook_name: config[hook_name]}
except KeyError:
return (None, None)
return next(
(
(name, hook_data_source)
for (name, hook) in hooks_to_search.items()
for hook_data_source in hook
if hook_data_source['name'] == configuration_data_source_name
and data_source_name in archive_data_source_names.get(name, [])
),
(None, None),
)
def restore_single_data_source(
repository,
config,
local_borg_version,
global_arguments,
local_path,
remote_path,
archive_name,
hook_name,
data_source,
connection_params,
): # pragma: no cover
'''
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
that data source from the archive.
'''
logger.info(
f'{repository.get("label", repository["path"])}: Restoring data source {data_source["name"]}'
)
dump_pattern = borgmatic.hooks.dispatch.call_hooks(
'make_data_source_dump_pattern',
config,
repository['path'],
borgmatic.hooks.dump.DATA_SOURCE_HOOK_NAMES,
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'),
)
# Run a single data source restore, consuming the extract stdout (if any).
borgmatic.hooks.dispatch.call_hooks(
function_name='restore_data_source_dump',
config=config,
log_prefix=repository['path'],
hook_names=[hook_name],
data_source=data_source,
dry_run=global_arguments.dry_run,
extract_process=extract_process,
connection_params=connection_params,
)
def collect_archive_data_source_names(
repository,
archive,
config,
local_borg_version,
global_arguments,
local_path,
remote_path,
):
'''
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.
'''
borgmatic_source_directory = os.path.expanduser(
config.get(
'borgmatic_source_directory', borgmatic.borg.state.DEFAULT_BORGMATIC_SOURCE_DIRECTORY
)
).lstrip('/')
dump_paths = borgmatic.borg.list.capture_archive_listing(
repository,
archive,
config,
local_borg_version,
global_arguments,
list_paths=[
os.path.expanduser(
borgmatic.hooks.dump.make_data_source_dump_path(borgmatic_source_directory, pattern)
)
for pattern in ('*_databases/*/*',)
],
local_path=local_path,
remote_path=remote_path,
)
# Determine the data source names corresponding to the dumps found in the archive and
# add them to restore_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):
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
def find_data_sources_to_restore(requested_data_source_names, archive_data_source_names):
'''
Given a sequence of requested data source names to restore and a dict of hook name to the names
of data sources found in an archive, return an expanded sequence of data source names to
restore, replacing "all" with actual data source names as appropriate.
Raise ValueError if any of the requested data source names cannot be found in the archive.
'''
# A map from data source hook name to the data source names to restore for that hook.
restore_names = (
{UNSPECIFIED_HOOK: requested_data_source_names}
if requested_data_source_names
else {UNSPECIFIED_HOOK: ['all']}
)
# If "all" is in restore_names, then replace it with the names of dumps found within the
# archive.
if 'all' in restore_names[UNSPECIFIED_HOOK]:
restore_names[UNSPECIFIED_HOOK].remove('all')
for hook_name, data_source_names in archive_data_source_names.items():
restore_names.setdefault(hook_name, []).extend(data_source_names)
# If a data source is to be restored as part of "all", then remove it from restore names
# so it doesn't get restored twice.
for data_source_name in data_source_names:
if data_source_name in restore_names[UNSPECIFIED_HOOK]:
restore_names[UNSPECIFIED_HOOK].remove(data_source_name)
if not restore_names[UNSPECIFIED_HOOK]:
restore_names.pop(UNSPECIFIED_HOOK)
combined_restore_names = set(
name for data_source_names in restore_names.values() for name in data_source_names
)
combined_archive_data_source_names = set(
name
for data_source_names in archive_data_source_names.values()
for name in data_source_names
)
missing_names = sorted(set(combined_restore_names) - combined_archive_data_source_names)
if missing_names:
joined_names = ', '.join(f'"{name}"' for name in missing_names)
raise ValueError(
f"Cannot restore data source{'s' if len(missing_names) > 1 else ''} {joined_names} missing from archive"
)
return restore_names
def ensure_data_sources_found(restore_names, remaining_restore_names, found_names):
'''
Given a dict from hook name to data source names to restore, a dict from hook name to remaining
data source names to restore, and a sequence of found (actually restored) data source names,
raise ValueError if requested data source to restore were missing from the archive and/or
configuration.
'''
combined_restore_names = set(
name
for data_source_names in tuple(restore_names.values())
+ tuple(remaining_restore_names.values())
for name in data_source_names
)
if not combined_restore_names and not found_names:
raise ValueError('No data sources were found to restore')
missing_names = sorted(set(combined_restore_names) - set(found_names))
if missing_names:
joined_names = ', '.join(f'"{name}"' for name in missing_names)
raise ValueError(
f"Cannot restore data source{'s' if len(missing_names) > 1 else ''} {joined_names} missing from borgmatic's configuration"
)
def run_restore(
repository,
config,
local_borg_version,
restore_arguments,
global_arguments,
local_path,
remote_path,
):
'''
Run the "restore" action for the given repository, but only if the repository matches the
requested repository in restore arguments.
Raise ValueError if a configured data source could not be found to restore.
'''
if restore_arguments.repository and not borgmatic.config.validate.repositories_match(
repository, restore_arguments.repository
):
return
logger.info(
f'{repository.get("label", repository["path"])}: 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,
)
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,
}
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
)
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,
)
# 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'
)
if not found_data_source:
continue
found_names.add(data_source_name)
data_source = copy.copy(found_data_source)
data_source['name'] = 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(data_source, **{'schemas': restore_arguments.schemas}),
connection_params,
)
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,
)
ensure_data_sources_found(restore_names, remaining_restore_names, found_names)

View File

@ -0,0 +1,42 @@
import logging
import borgmatic.actions.json
import borgmatic.borg.rinfo
import borgmatic.config.validate
logger = logging.getLogger(__name__)
def run_rinfo(
repository,
config,
local_borg_version,
rinfo_arguments,
global_arguments,
local_path,
remote_path,
):
'''
Run the "rinfo" action for the given repository.
If rinfo_arguments.json is True, yield the JSON output from the info for the repository.
'''
if rinfo_arguments.repository is None or borgmatic.config.validate.repositories_match(
repository, rinfo_arguments.repository
):
if not rinfo_arguments.json:
logger.answer(
f'{repository.get("label", repository["path"])}: Displaying repository summary information'
)
json_output = borgmatic.borg.rinfo.display_repository_info(
repository['path'],
config,
local_borg_version,
rinfo_arguments=rinfo_arguments,
global_arguments=global_arguments,
local_path=local_path,
remote_path=remote_path,
)
if json_output:
yield borgmatic.actions.json.parse_json(json_output, repository.get('label'))

View File

@ -0,0 +1,40 @@
import logging
import borgmatic.actions.json
import borgmatic.borg.rlist
import borgmatic.config.validate
logger = logging.getLogger(__name__)
def run_rlist(
repository,
config,
local_borg_version,
rlist_arguments,
global_arguments,
local_path,
remote_path,
):
'''
Run the "rlist" action for the given repository.
If rlist_arguments.json is True, yield the JSON output from listing the repository.
'''
if rlist_arguments.repository is None or borgmatic.config.validate.repositories_match(
repository, rlist_arguments.repository
):
if not rlist_arguments.json:
logger.answer(f'{repository.get("label", repository["path"])}: Listing repository')
json_output = borgmatic.borg.rlist.list_repository(
repository['path'],
config,
local_borg_version,
rlist_arguments=rlist_arguments,
global_arguments=global_arguments,
local_path=local_path,
remote_path=remote_path,
)
if json_output:
yield borgmatic.actions.json.parse_json(json_output, repository.get('label'))

View File

@ -0,0 +1,32 @@
import logging
import borgmatic.borg.transfer
logger = logging.getLogger(__name__)
def run_transfer(
repository,
config,
local_borg_version,
transfer_arguments,
global_arguments,
local_path,
remote_path,
):
'''
Run the "transfer" action for the given repository.
'''
logger.info(
f'{repository.get("label", repository["path"])}: Transferring archives to repository'
)
borgmatic.borg.transfer.transfer_archives(
global_arguments.dry_run,
repository['path'],
config,
local_borg_version,
transfer_arguments,
global_arguments,
local_path=local_path,
remote_path=remote_path,
)

72
borgmatic/borg/borg.py Normal file
View File

@ -0,0 +1,72 @@
import logging
import shlex
import borgmatic.commands.arguments
import borgmatic.logger
from borgmatic.borg import environment, flags
from borgmatic.execute import DO_NOT_CAPTURE, execute_command
logger = logging.getLogger(__name__)
BORG_SUBCOMMANDS_WITH_SUBCOMMANDS = {'key', 'debug'}
def run_arbitrary_borg(
repository_path,
config,
local_borg_version,
options,
archive=None,
local_path='borg',
remote_path=None,
):
'''
Given a local or remote repository path, a configuration dict, the local Borg version, a
sequence of arbitrary command-line Borg options, and an optional archive name, run an arbitrary
Borg command, passing in REPOSITORY and ARCHIVE environment variables for optional use in the
command.
'''
borgmatic.logger.add_custom_log_levels()
lock_wait = config.get('lock_wait', None)
try:
options = options[1:] if options[0] == '--' else options
# Borg commands like "key" have a sub-command ("export", etc.) that must follow it.
command_options_start_index = 2 if options[0] in BORG_SUBCOMMANDS_WITH_SUBCOMMANDS else 1
borg_command = tuple(options[:command_options_start_index])
command_options = tuple(options[command_options_start_index:])
if borg_command and borg_command[0] in borgmatic.commands.arguments.ACTION_ALIASES.keys():
logger.warning(
f"Borg's {borg_command[0]} subcommand is supported natively by borgmatic. Try this instead: borgmatic {borg_command[0]}"
)
except IndexError:
borg_command = ()
command_options = ()
full_command = (
(local_path,)
+ borg_command
+ (('--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('lock-wait', lock_wait)
+ command_options
)
return execute_command(
tuple(shlex.quote(part) for part in full_command),
output_file=DO_NOT_CAPTURE,
shell=True,
extra_environment=dict(
(environment.make_environment(config) or {}),
**{
'BORG_REPO': repository_path,
'ARCHIVE': archive if archive else '',
},
),
borg_local_path=local_path,
borg_exit_codes=config.get('borg_exit_codes'),
)

View File

@ -0,0 +1,42 @@
import logging
from borgmatic.borg import environment, flags
from borgmatic.execute import execute_command
logger = logging.getLogger(__name__)
def break_lock(
repository_path,
config,
local_borg_version,
global_arguments,
local_path='borg',
remote_path=None,
):
'''
Given a local or remote repository path, a configuration dict, the local Borg version, an
argparse.Namespace of global arguments, and optional local and remote Borg paths, break any
repository and cache locks leftover from Borg aborting.
'''
umask = config.get('umask', None)
lock_wait = config.get('lock_wait', None)
full_command = (
(local_path, 'break-lock')
+ (('--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)
)
borg_environment = environment.make_environment(config)
execute_command(
full_command,
extra_environment=borg_environment,
borg_local_path=local_path,
borg_exit_codes=config.get('borg_exit_codes'),
)

View File

@ -1,48 +1,59 @@
import argparse
import json
import logging
from borgmatic.borg import extract
from borgmatic.execute import execute_command
DEFAULT_CHECKS = ('repository', 'archives')
DEFAULT_PREFIX = '{hostname}-'
from borgmatic.borg import environment, feature, flags, rinfo
from borgmatic.execute import DO_NOT_CAPTURE, execute_command
logger = logging.getLogger(__name__)
def _parse_checks(consistency_config, only_checks=None):
def make_archive_filter_flags(local_borg_version, config, checks, check_arguments):
'''
Given a consistency config with a "checks" list, and an optional list of override checks,
transform them a tuple of named checks to run.
Given the local Borg version, a configuration dict, a parsed sequence of checks, and check
arguments as an argparse.Namespace instance, transform the checks into tuple of command-line
flags for filtering archives in a check command.
For example, given a retention config of:
{'checks': ['repository', 'archives']}
This will be returned as:
('repository', 'archives')
If no "checks" option is present in the config, return the DEFAULT_CHECKS. If the checks value
is the string "disabled", return an empty tuple, meaning that no checks should be run.
If the "data" option is present, then make sure the "archives" option is included as well.
If "check_last" is set in the configuration and "archives" is in checks, then include a "--last"
flag. And if "prefix" is set in configuration and "archives" is in checks, then include a
"--match-archives" flag.
'''
checks = [
check.lower() for check in (only_checks or consistency_config.get('checks', []) or [])
]
if checks == ['disabled']:
return ()
check_last = config.get('check_last', None)
prefix = config.get('prefix')
if 'data' in checks and 'archives' not in checks:
checks.append('archives')
if 'archives' in checks or 'data' in checks:
return (('--last', str(check_last)) if check_last else ()) + (
(
('--match-archives', f'sh:{prefix}*')
if feature.available(feature.Feature.MATCH_ARCHIVES, local_borg_version)
else ('--glob-archives', f'{prefix}*')
)
if prefix
else (
flags.make_match_archives_flags(
check_arguments.match_archives or config.get('match_archives'),
config.get('archive_name_format'),
local_borg_version,
)
)
)
return tuple(check for check in checks if check not in ('disabled', '')) or DEFAULT_CHECKS
if check_last:
logger.warning(
'Ignoring check_last option, as "archives" or "data" are not in consistency checks'
)
if prefix:
logger.warning(
'Ignoring consistency prefix option, as "archives" or "data" are not in consistency checks'
)
return ()
def _make_check_flags(checks, check_last=None, prefix=None):
def make_check_flags(checks, archive_filter_flags):
'''
Given a parsed sequence of checks, transform it into tuple of command-line flags.
Given a parsed checks set and a sequence of flags to filter archives,
transform the checks into tuple of command-line check flags.
For example, given parsed checks of:
@ -53,80 +64,106 @@ def _make_check_flags(checks, check_last=None, prefix=None):
('--repository-only',)
However, if both "repository" and "archives" are in checks, then omit them from the returned
flags because Borg does both checks by default.
Additionally, if a check_last value is given and "archives" is in checks, then include a
"--last" flag. And if a prefix value is given and "archives" is in checks, then include a
"--prefix" flag.
flags because Borg does both checks by default. If "data" is in checks, that implies "archives".
'''
if 'archives' in checks:
last_flags = ('--last', str(check_last)) if check_last else ()
prefix_flags = ('--prefix', prefix) if prefix else ()
if 'data' in checks:
data_flags = ('--verify-data',)
checks.update({'archives'})
else:
last_flags = ()
prefix_flags = ()
if check_last:
logger.warning(
'Ignoring check_last option, as "archives" is not in consistency checks.'
)
if prefix:
logger.warning(
'Ignoring consistency prefix option, as "archives" is not in consistency checks.'
)
data_flags = ()
common_flags = last_flags + prefix_flags + (('--verify-data',) if 'data' in checks else ())
common_flags = (archive_filter_flags if 'archives' in checks else ()) + data_flags
if set(DEFAULT_CHECKS).issubset(set(checks)):
if {'repository', 'archives'}.issubset(checks):
return common_flags
return (
tuple('--{}-only'.format(check) for check in checks if check in DEFAULT_CHECKS)
tuple(f'--{check}-only' for check in checks if check in ('repository', 'archives'))
+ common_flags
)
def check_archives(
repository,
storage_config,
consistency_config,
local_path='borg',
remote_path=None,
only_checks=None,
def get_repository_id(
repository_path, config, local_borg_version, global_arguments, local_path, remote_path
):
'''
Given a local or remote repository path, a storage config dict, a consistency config dict,
local/remote commands to run, and an optional list of checks to use instead of configured
checks, check the contained Borg archives for consistency.
Given a local or remote repository path, a configuration dict, the local Borg version, global
arguments, and local/remote commands to run, return the corresponding Borg repository ID.
If there are no consistency checks to run, skip running them.
Raise ValueError if the Borg repository ID cannot be determined.
'''
checks = _parse_checks(consistency_config, only_checks)
check_last = consistency_config.get('check_last', None)
lock_wait = None
try:
return json.loads(
rinfo.display_repository_info(
repository_path,
config,
local_borg_version,
argparse.Namespace(json=True),
global_arguments,
local_path,
remote_path,
)
)['repository']['id']
except (json.JSONDecodeError, KeyError):
raise ValueError(f'Cannot determine Borg repository ID for {repository_path}')
if set(checks).intersection(set(DEFAULT_CHECKS + ('data',))):
remote_path_flags = ('--remote-path', remote_path) if remote_path else ()
lock_wait = storage_config.get('lock_wait', None)
lock_wait_flags = ('--lock-wait', str(lock_wait)) if lock_wait else ()
verbosity_flags = ()
if logger.isEnabledFor(logging.INFO):
verbosity_flags = ('--info',)
if logger.isEnabledFor(logging.DEBUG):
verbosity_flags = ('--debug', '--show-rc')
def check_archives(
repository_path,
config,
local_borg_version,
check_arguments,
global_arguments,
checks,
archive_filter_flags,
local_path='borg',
remote_path=None,
):
'''
Given a local or remote repository path, a configuration dict, the local Borg version, check
arguments as an argparse.Namespace instance, global arguments, a set of named Borg checks to run
(some combination "repository", "archives", and/or "data"), archive filter flags, and
local/remote commands to run, check the contained Borg archives for consistency.
'''
lock_wait = config.get('lock_wait')
extra_borg_options = config.get('extra_borg_options', {}).get('check', '')
prefix = consistency_config.get('prefix', DEFAULT_PREFIX)
verbosity_flags = ()
if logger.isEnabledFor(logging.INFO):
verbosity_flags = ('--info',)
if logger.isEnabledFor(logging.DEBUG):
verbosity_flags = ('--debug', '--show-rc')
full_command = (
(local_path, 'check')
+ _make_check_flags(checks, check_last, prefix)
+ remote_path_flags
+ lock_wait_flags
+ verbosity_flags
+ (repository,)
full_command = (
(local_path, 'check')
+ (('--repair',) if check_arguments.repair else ())
+ make_check_flags(checks, archive_filter_flags)
+ (('--remote-path', remote_path) if remote_path else ())
+ (('--log-json',) if global_arguments.log_json else ())
+ (('--lock-wait', str(lock_wait)) if lock_wait else ())
+ verbosity_flags
+ (('--progress',) if check_arguments.progress else ())
+ (tuple(extra_borg_options.split(' ')) if extra_borg_options else ())
+ flags.make_repository_flags(repository_path, local_borg_version)
)
borg_environment = environment.make_environment(config)
borg_exit_codes = config.get('borg_exit_codes')
# 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:
execute_command(
full_command,
output_file=DO_NOT_CAPTURE,
extra_environment=borg_environment,
borg_local_path=local_path,
borg_exit_codes=borg_exit_codes,
)
else:
execute_command(
full_command,
extra_environment=borg_environment,
borg_local_path=local_path,
borg_exit_codes=borg_exit_codes,
)
execute_command(full_command)
if 'extract' in checks:
extract.extract_last_archive_dry_run(repository, lock_wait, local_path, remote_path)

54
borgmatic/borg/compact.py Normal file
View File

@ -0,0 +1,54 @@
import logging
from borgmatic.borg import environment, flags
from borgmatic.execute import execute_command
logger = logging.getLogger(__name__)
def compact_segments(
dry_run,
repository_path,
config,
local_borg_version,
global_arguments,
local_path='borg',
remote_path=None,
progress=False,
cleanup_commits=False,
threshold=None,
):
'''
Given dry-run flag, a local or remote repository path, a configuration dict, and the local Borg
version, compact the segments in a repository.
'''
umask = config.get('umask', None)
lock_wait = config.get('lock_wait', None)
extra_borg_options = config.get('extra_borg_options', {}).get('compact', '')
full_command = (
(local_path, 'compact')
+ (('--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 ())
+ (('--progress',) if progress else ())
+ (('--cleanup-commits',) if cleanup_commits else ())
+ (('--threshold', str(threshold)) if threshold else ())
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
+ (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
+ (tuple(extra_borg_options.split(' ')) if extra_borg_options else ())
+ flags.make_repository_flags(repository_path, local_borg_version)
)
if dry_run:
logging.info(f'{repository_path}: Skipping compact (dry run)')
return
execute_command(
full_command,
output_log_level=logging.INFO,
extra_environment=environment.make_environment(config),
borg_local_path=local_path,
borg_exit_codes=config.get('borg_exit_codes'),
)

View File

@ -2,14 +2,23 @@ import glob
import itertools
import logging
import os
import pathlib
import stat
import tempfile
from borgmatic.execute import execute_command, execute_command_without_capture
import borgmatic.logger
from borgmatic.borg import environment, feature, flags, state
from borgmatic.execute import (
DO_NOT_CAPTURE,
execute_command,
execute_command_and_capture_output,
execute_command_with_processes,
)
logger = logging.getLogger(__name__)
def _expand_directory(directory):
def expand_directory(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.
@ -19,7 +28,7 @@ def _expand_directory(directory):
return glob.glob(expanded_directory) or [expanded_directory]
def _expand_directories(directories):
def expand_directories(directories):
'''
Given a sequence of directory paths, expand tildes and globs in each one. Return all the
resulting directories as a single flattened tuple.
@ -28,11 +37,11 @@ def _expand_directories(directories):
return ()
return tuple(
itertools.chain.from_iterable(_expand_directory(directory) for directory in directories)
itertools.chain.from_iterable(expand_directory(directory) for directory in directories)
)
def _expand_home_directories(directories):
def expand_home_directories(directories):
'''
Given a sequence of directory paths, expand tildes in each one. Do not perform any globbing.
Return the results as a tuple.
@ -43,27 +52,106 @@ def _expand_home_directories(directories):
return tuple(os.path.expanduser(directory) for directory in directories)
def _write_pattern_file(patterns=None):
def map_directories_to_devices(directories):
'''
Given a sequence of patterns, write them to a named temporary file and return it. Return None
if no patterns are provided.
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.
This is handy for determining whether two different directories are on the same filesystem (have
the same device identifier).
'''
if not patterns:
return {
directory: os.stat(directory).st_dev if os.path.exists(directory) else None
for directory in directories
}
def deduplicate_directories(directory_devices, additional_directory_devices):
'''
Given a map from directory to the identifier for the device on which that directory resides,
return the directories as a sorted tuple with all duplicate child directories removed. For
instance, if paths is ('/foo', '/foo/bar'), return just: ('/foo',)
The one exception to this rule is if two paths are on different filesystems (devices). In that
case, they won't get de-duplicated in case they both need to be passed to Borg (e.g. the
location.one_file_system option is true).
The idea is that if Borg is given a parent directory, then it doesn't also need to be given
child directories, because it will naturally spider the contents of the parent directory. And
there are cases where Borg coming across the same file twice will result in duplicate reads and
even hangs, e.g. when a database hook is using a named pipe for streaming database dumps to
Borg.
If any additional directory devices are given, also deduplicate against them, but don't include
them in the returned directories.
'''
deduplicated = set()
directories = sorted(directory_devices.keys())
additional_directories = sorted(additional_directory_devices.keys())
all_devices = {**directory_devices, **additional_directory_devices}
for directory in directories:
deduplicated.add(directory)
parents = pathlib.PurePath(directory).parents
# If another directory in the given list (or the additional list) is a parent of current
# directory (even n levels up) and both are on the same filesystem, then the current
# directory is a duplicate.
for other_directory in directories + additional_directories:
for parent in parents:
if (
pathlib.PurePath(other_directory) == parent
and all_devices[directory] is not None
and all_devices[other_directory] == all_devices[directory]
):
if directory in deduplicated:
deduplicated.remove(directory)
break
return tuple(sorted(deduplicated))
def write_pattern_file(patterns=None, sources=None, pattern_file=None):
'''
Given a sequence of patterns and an optional sequence of source directories, write them to a
named temporary file (with the source directories as additional roots) and return the file.
If an optional open pattern file is given, overwrite it instead of making a new temporary file.
Return None if no patterns are provided.
'''
if not patterns and not sources:
return None
pattern_file = tempfile.NamedTemporaryFile('w')
pattern_file.write('\n'.join(patterns))
if pattern_file is None:
pattern_file = tempfile.NamedTemporaryFile('w')
else:
pattern_file.seek(0)
pattern_file.write(
'\n'.join(tuple(patterns or ()) + tuple(f'R {source}' for source in (sources or [])))
)
pattern_file.flush()
return pattern_file
def _make_pattern_flags(location_config, pattern_filename=None):
def ensure_files_readable(*filename_lists):
'''
Given a location config dict with a potential patterns_from option, and a filename containing
any additional patterns, return the corresponding Borg flags for those files as a tuple.
Given a sequence of filename sequences, ensure that each filename is openable. This prevents
unreadable files from being passed to Borg, which in certain situations only warns instead of
erroring.
'''
pattern_filenames = tuple(location_config.get('patterns_from') or ()) + (
for file_object in itertools.chain.from_iterable(
filename_list for filename_list in filename_lists if filename_list
):
open(file_object).close()
def make_pattern_flags(config, pattern_filename=None):
'''
Given a configuration dict with a potential patterns_from option, and a filename containing any
additional patterns, return the corresponding Borg flags for those files as a tuple.
'''
pattern_filenames = tuple(config.get('patterns_from') or ()) + (
(pattern_filename,) if pattern_filename else ()
)
@ -74,12 +162,12 @@ def _make_pattern_flags(location_config, pattern_filename=None):
)
def _make_exclude_flags(location_config, exclude_filename=None):
def make_exclude_flags(config, exclude_filename=None):
'''
Given a location config dict with various exclude options, and a filename containing any exclude
Given a configuration dict with various exclude options, and a filename containing any exclude
patterns, return the corresponding Borg flags as a tuple.
'''
exclude_filenames = tuple(location_config.get('exclude_from') or ()) + (
exclude_filenames = tuple(config.get('exclude_from') or ()) + (
(exclude_filename,) if exclude_filename else ()
)
exclude_from_flags = tuple(
@ -87,13 +175,15 @@ def _make_exclude_flags(location_config, exclude_filename=None):
('--exclude-from', exclude_filename) for exclude_filename in exclude_filenames
)
)
caches_flag = ('--exclude-caches',) if location_config.get('exclude_caches') else ()
if_present = location_config.get('exclude_if_present')
if_present_flags = ('--exclude-if-present', if_present) if if_present else ()
keep_exclude_tags_flags = (
('--keep-exclude-tags',) if location_config.get('keep_exclude_tags') else ()
caches_flag = ('--exclude-caches',) if config.get('exclude_caches') else ()
if_present_flags = tuple(
itertools.chain.from_iterable(
('--exclude-if-present', if_present)
for if_present in config.get('exclude_if_present', ())
)
)
exclude_nodump_flags = ('--exclude-nodump',) if location_config.get('exclude_nodump') else ()
keep_exclude_tags_flags = ('--keep-exclude-tags',) if config.get('keep_exclude_tags') else ()
exclude_nodump_flags = ('--exclude-nodump',) if config.get('exclude_nodump') else ()
return (
exclude_from_flags
@ -104,102 +194,386 @@ def _make_exclude_flags(location_config, exclude_filename=None):
)
BORGMATIC_SOURCE_DIRECTORY = '~/.borgmatic'
def make_list_filter_flags(local_borg_version, dry_run):
'''
Given the local Borg version and whether this is a dry run, return the corresponding flags for
passing to "--list --filter". The general idea is that excludes are shown for a dry run or when
the verbosity is debug.
'''
base_flags = 'AME'
show_excludes = logger.isEnabledFor(logging.DEBUG)
if feature.available(feature.Feature.EXCLUDED_FILES_MINUS, local_borg_version):
if show_excludes or dry_run:
return f'{base_flags}+-'
else:
return base_flags
if show_excludes:
return f'{base_flags}x-'
else:
return f'{base_flags}-'
def borgmatic_source_directories():
def collect_borgmatic_source_directories(borgmatic_source_directory):
'''
Return a list of borgmatic-specific source directories used for state like database backups.
'''
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))
[borgmatic_source_directory]
if os.path.exists(os.path.expanduser(borgmatic_source_directory))
else []
)
ROOT_PATTERN_PREFIX = 'R '
def pattern_root_directories(patterns=None):
'''
Given a sequence of patterns, parse out and return just the root directories.
'''
if not patterns:
return []
return [
pattern.split(ROOT_PATTERN_PREFIX, maxsplit=1)[1]
for pattern in patterns
if pattern.startswith(ROOT_PATTERN_PREFIX)
]
def special_file(path):
'''
Return whether the given path is a special file (character device, block device, or named pipe
/ FIFO).
'''
try:
mode = os.stat(path).st_mode
except (FileNotFoundError, OSError):
return False
return stat.S_ISCHR(mode) or stat.S_ISBLK(mode) or stat.S_ISFIFO(mode)
def any_parent_directories(path, candidate_parents):
'''
Return whether any of the given candidate parent directories are an actual parent of the given
path. This includes grandparents, etc.
'''
for parent in candidate_parents:
if pathlib.PurePosixPath(parent) in pathlib.PurePath(path).parents:
return True
return False
def collect_special_file_paths(
create_command, config, local_path, working_directory, borg_environment, skip_directories
):
'''
Given a Borg create command as a tuple, a configuration dict, a local Borg path, a working
directory, a dict of environment variables to pass to Borg, and a sequence of parent directories
to skip, collect the paths for any special files (character devices, block devices, and named
pipes / FIFOs) that Borg would encounter during a create. These are all paths that could cause
Borg to hang if its --read-special flag is used.
'''
# Omit "--exclude-nodump" from the Borg dry run command, because that flag causes Borg to open
# files including any named pipe we've created.
paths_output = execute_command_and_capture_output(
tuple(argument for argument in create_command if argument != '--exclude-nodump')
+ ('--dry-run', '--list'),
capture_stderr=True,
working_directory=working_directory,
extra_environment=borg_environment,
borg_local_path=local_path,
borg_exit_codes=config.get('borg_exit_codes'),
)
paths = tuple(
path_line.split(' ', 1)[1]
for path_line in paths_output.split('\n')
if path_line and path_line.startswith('- ') or path_line.startswith('+ ')
)
return tuple(
path
for path in paths
if special_file(path) and not any_parent_directories(path, skip_directories)
)
def check_all_source_directories_exist(source_directories):
'''
Given a sequence of source directories, check that they all exist. If any do not, raise an
exception.
'''
missing_directories = [
source_directory
for source_directory in source_directories
if not all([os.path.exists(directory) for directory in expand_directory(source_directory)])
]
if missing_directories:
raise ValueError(f"Source directories do not exist: {', '.join(missing_directories)}")
def make_base_create_command(
dry_run,
repository_path,
config,
config_paths,
local_borg_version,
global_arguments,
borgmatic_source_directories,
local_path='borg',
remote_path=None,
progress=False,
json=False,
list_files=False,
stream_processes=None,
):
'''
Given vebosity/dry-run flags, a local or remote repository path, a configuration dict, a
sequence of loaded configuration paths, the local Borg version, global arguments as an
argparse.Namespace instance, and a sequence of borgmatic source directories, return a tuple of
(base Borg create command flags, Borg create command positional arguments, open pattern file
handle, open exclude file handle).
'''
if config.get('source_directories_must_exist', False):
check_all_source_directories_exist(config.get('source_directories'))
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 ())
)
),
additional_directory_devices=map_directories_to_devices(
expand_directories(pattern_root_directories(config.get('patterns')))
),
)
ensure_files_readable(config.get('patterns_from'), config.get('exclude_from'))
pattern_file = (
write_pattern_file(config.get('patterns'), sources)
if config.get('patterns') or config.get('patterns_from')
else None
)
exclude_file = write_pattern_file(expand_home_directories(config.get('exclude_patterns')))
checkpoint_interval = config.get('checkpoint_interval', None)
checkpoint_volume = config.get('checkpoint_volume', None)
chunker_params = config.get('chunker_params', None)
compression = config.get('compression', None)
upload_rate_limit = config.get('upload_rate_limit', None)
umask = config.get('umask', None)
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)
extra_borg_options = config.get('extra_borg_options', {}).get('create', '')
if feature.available(feature.Feature.ATIME, local_borg_version):
atime_flags = ('--atime',) if config.get('atime') is True else ()
else:
atime_flags = ('--noatime',) if config.get('atime') is False else ()
if feature.available(feature.Feature.NOFLAGS, local_borg_version):
noflags_flags = ('--noflags',) if config.get('flags') is False else ()
else:
noflags_flags = ('--nobsdflags',) if config.get('flags') is False else ()
if feature.available(feature.Feature.NUMERIC_IDS, local_borg_version):
numeric_ids_flags = ('--numeric-ids',) if config.get('numeric_ids') else ()
else:
numeric_ids_flags = ('--numeric-owner',) if config.get('numeric_ids') else ()
if feature.available(feature.Feature.UPLOAD_RATELIMIT, local_borg_version):
upload_ratelimit_flags = (
('--upload-ratelimit', str(upload_rate_limit)) if upload_rate_limit else ()
)
else:
upload_ratelimit_flags = (
('--remote-ratelimit', str(upload_rate_limit)) if upload_rate_limit else ()
)
create_flags = (
tuple(local_path.split(' '))
+ ('create',)
+ make_pattern_flags(config, pattern_file.name if pattern_file else None)
+ make_exclude_flags(config, exclude_file.name if exclude_file else None)
+ (('--checkpoint-interval', str(checkpoint_interval)) if checkpoint_interval else ())
+ (('--checkpoint-volume', str(checkpoint_volume)) if checkpoint_volume else ())
+ (('--chunker-params', chunker_params) if chunker_params else ())
+ (('--compression', compression) if compression else ())
+ upload_ratelimit_flags
+ (('--one-file-system',) if config.get('one_file_system') or stream_processes else ())
+ numeric_ids_flags
+ atime_flags
+ (('--noctime',) if config.get('ctime') is False else ())
+ (('--nobirthtime',) if config.get('birthtime') is False else ())
+ (('--read-special',) if config.get('read_special') or stream_processes else ())
+ noflags_flags
+ (('--files-cache', files_cache) if files_cache else ())
+ (('--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 ())
+ (
('--list', '--filter', list_filter_flags)
if list_files and not json and not progress
else ()
)
+ (('--dry-run',) if dry_run else ())
+ (tuple(extra_borg_options.split(' ')) if extra_borg_options else ())
)
create_positional_arguments = flags.make_repository_archive_flags(
repository_path, archive_name_format, local_borg_version
) + (sources if not pattern_file else ())
# If database hooks are enabled (as indicated by streaming processes), exclude files that might
# cause Borg to hang. But skip this if the user has explicitly set the "read_special" to True.
if stream_processes and not config.get('read_special'):
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')
special_file_paths = collect_special_file_paths(
create_flags + create_positional_arguments,
config,
local_path,
working_directory,
borg_environment,
skip_directories=borgmatic_source_directories,
)
if special_file_paths:
logger.warning(
f'{repository_path}: Excluding special files to prevent Borg from hanging: {", ".join(special_file_paths)}'
)
exclude_file = write_pattern_file(
expand_home_directories(
tuple(config.get('exclude_patterns') or ()) + special_file_paths
),
pattern_file=exclude_file,
)
create_flags += make_exclude_flags(config, exclude_file.name)
return (create_flags, create_positional_arguments, pattern_file, exclude_file)
def create_archive(
dry_run,
repository,
location_config,
storage_config,
repository_path,
config,
config_paths,
local_borg_version,
global_arguments,
local_path='borg',
remote_path=None,
progress=False,
stats=False,
json=False,
list_files=False,
stream_processes=None,
):
'''
Given vebosity/dry-run flags, a local or remote repository path, a location config dict, and a
storage config dict, create a Borg archive and return Borg's JSON output (if any).
Given vebosity/dry-run flags, a local or remote repository path, a configuration dict, a
sequence of loaded configuration paths, the local Borg version, and global arguments as an
argparse.Namespace instance, create a Borg archive and return Borg's JSON output (if any).
If a sequence of stream processes is given (instances of subprocess.Popen), then execute the
create command while also triggering the given processes to produce output.
'''
sources = _expand_directories(
location_config['source_directories'] + borgmatic_source_directories()
borgmatic.logger.add_custom_log_levels()
borgmatic_source_directories = expand_directories(
collect_borgmatic_source_directories(config.get('borgmatic_source_directory'))
)
pattern_file = _write_pattern_file(location_config.get('patterns'))
exclude_file = _write_pattern_file(
_expand_home_directories(location_config.get('exclude_patterns'))
)
checkpoint_interval = storage_config.get('checkpoint_interval', None)
chunker_params = storage_config.get('chunker_params', None)
compression = storage_config.get('compression', None)
remote_rate_limit = storage_config.get('remote_rate_limit', None)
umask = storage_config.get('umask', None)
lock_wait = storage_config.get('lock_wait', None)
files_cache = location_config.get('files_cache')
default_archive_name_format = '{hostname}-{now:%Y-%m-%dT%H:%M:%S.%f}'
archive_name_format = storage_config.get('archive_name_format', default_archive_name_format)
full_command = (
(local_path, 'create')
+ _make_pattern_flags(location_config, pattern_file.name if pattern_file else None)
+ _make_exclude_flags(location_config, exclude_file.name if exclude_file else None)
+ (('--checkpoint-interval', str(checkpoint_interval)) if checkpoint_interval else ())
+ (('--chunker-params', chunker_params) if chunker_params else ())
+ (('--compression', compression) if compression else ())
+ (('--remote-ratelimit', str(remote_rate_limit)) if remote_rate_limit else ())
+ (('--one-file-system',) if location_config.get('one_file_system') else ())
+ (('--numeric-owner',) if location_config.get('numeric_owner') else ())
+ (('--noatime',) if location_config.get('atime') is False else ())
+ (('--noctime',) if location_config.get('ctime') is False else ())
+ (('--nobirthtime',) if location_config.get('birthtime') is False else ())
+ (('--read-special',) if location_config.get('read_special') else ())
+ (('--nobsdflags',) if location_config.get('bsd_flags') is False else ())
+ (('--files-cache', files_cache) if files_cache else ())
+ (('--remote-path', remote_path) if remote_path else ())
+ (('--umask', str(umask)) if umask else ())
+ (('--lock-wait', str(lock_wait)) if lock_wait else ())
+ (('--list', '--filter', 'AME-') if logger.isEnabledFor(logging.INFO) and not json else ())
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO and not json else ())
+ (
('--stats',)
if not dry_run and (logger.isEnabledFor(logging.INFO) or stats) and not json
else ()
(create_flags, create_positional_arguments, pattern_file, exclude_file) = (
make_base_create_command(
dry_run,
repository_path,
config,
config_paths,
local_borg_version,
global_arguments,
borgmatic_source_directories,
local_path,
remote_path,
progress,
json,
list_files,
stream_processes,
)
+ (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) and not json else ())
+ (('--dry-run',) if dry_run else ())
+ (('--progress',) if progress else ())
+ (('--json',) if json else ())
+ (
'{repository}::{archive_name_format}'.format(
repository=repository, archive_name_format=archive_name_format
),
)
+ sources
)
# The progress output isn't compatible with captured and logged output, as progress messes with
# the terminal directly.
if progress:
execute_command_without_capture(full_command)
return
if json:
output_log_level = None
elif stats:
output_log_level = logging.WARNING
elif list_files or (stats and not dry_run):
output_log_level = logging.ANSWER
else:
output_log_level = logging.INFO
return execute_command(full_command, output_log_level)
# The progress output isn't compatible with captured and logged output, as progress messes with
# 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 += (
(('--info',) if logger.getEffectiveLevel() == logging.INFO and not json else ())
+ (('--stats',) if stats and not json and not dry_run else ())
+ (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) and not json else ())
+ (('--progress',) if progress else ())
+ (('--json',) if json else ())
)
borg_exit_codes = config.get('borg_exit_codes')
if stream_processes:
return execute_command_with_processes(
create_flags + create_positional_arguments,
stream_processes,
output_log_level,
output_file,
working_directory=working_directory,
extra_environment=borg_environment,
borg_local_path=local_path,
borg_exit_codes=borg_exit_codes,
)
elif output_log_level is None:
return execute_command_and_capture_output(
create_flags + create_positional_arguments,
working_directory=working_directory,
extra_environment=borg_environment,
borg_local_path=local_path,
borg_exit_codes=borg_exit_codes,
)
else:
execute_command(
create_flags + create_positional_arguments,
output_log_level,
output_file,
working_directory=working_directory,
extra_environment=borg_environment,
borg_local_path=local_path,
borg_exit_codes=borg_exit_codes,
)

View File

@ -1,31 +1,57 @@
import os
OPTION_TO_ENVIRONMENT_VARIABLE = {
'borg_base_directory': 'BORG_BASE_DIR',
'borg_config_directory': 'BORG_CONFIG_DIR',
'borg_cache_directory': 'BORG_CACHE_DIR',
'borg_files_cache_ttl': 'BORG_FILES_CACHE_TTL',
'borg_security_directory': 'BORG_SECURITY_DIR',
'borg_keys_directory': 'BORG_KEYS_DIR',
'encryption_passcommand': 'BORG_PASSCOMMAND',
'encryption_passphrase': 'BORG_PASSPHRASE',
'ssh_command': 'BORG_RSH',
'temporary_directory': 'TMPDIR',
}
DEFAULT_BOOL_OPTION_TO_ENVIRONMENT_VARIABLE = {
DEFAULT_BOOL_OPTION_TO_DOWNCASE_ENVIRONMENT_VARIABLE = {
'relocated_repo_access_is_ok': 'BORG_RELOCATED_REPO_ACCESS_IS_OK',
'unknown_unencrypted_repo_access_is_ok': 'BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK',
}
DEFAULT_BOOL_OPTION_TO_UPPERCASE_ENVIRONMENT_VARIABLE = {
'check_i_know_what_i_am_doing': 'BORG_CHECK_I_KNOW_WHAT_I_AM_DOING',
}
def make_environment(config):
'''
Given a borgmatic configuration dict, return its options converted to a Borg environment
variable dict.
'''
environment = {}
def initialize(storage_config):
for option_name, environment_variable_name in OPTION_TO_ENVIRONMENT_VARIABLE.items():
value = storage_config.get(option_name)
value = config.get(option_name)
if value:
os.environ[environment_variable_name] = value
environment[environment_variable_name] = str(value)
for (
option_name,
environment_variable_name,
) in DEFAULT_BOOL_OPTION_TO_ENVIRONMENT_VARIABLE.items():
value = storage_config.get(option_name, False)
os.environ[environment_variable_name] = 'yes' if value else 'no'
) in DEFAULT_BOOL_OPTION_TO_DOWNCASE_ENVIRONMENT_VARIABLE.items():
value = config.get(option_name)
if value is not None:
environment[environment_variable_name] = 'yes' if value else 'no'
for (
option_name,
environment_variable_name,
) in DEFAULT_BOOL_OPTION_TO_UPPERCASE_ENVIRONMENT_VARIABLE.items():
value = config.get(option_name)
if value is not None:
environment[environment_variable_name] = 'YES' if value else 'NO'
# On Borg 1.4.0a1+, take advantage of more specific exit codes. No effect on
# older versions of Borg.
environment['BORG_EXIT_CODES'] = 'modern'
return environment

View File

@ -0,0 +1,71 @@
import logging
import os
import borgmatic.logger
from borgmatic.borg import environment, flags
from borgmatic.execute import DO_NOT_CAPTURE, execute_command
logger = logging.getLogger(__name__)
def export_key(
repository_path,
config,
local_borg_version,
export_arguments,
global_arguments,
local_path='borg',
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.
If the destination path is empty or "-", then print the key to stdout instead of to a file.
Raise FileExistsError if a path is given but it already exists on disk.
'''
borgmatic.logger.add_custom_log_levels()
umask = config.get('umask', None)
lock_wait = config.get('lock_wait', None)
if export_arguments.path and export_arguments.path != '-':
if os.path.exists(export_arguments.path):
raise FileExistsError(
f'Destination path {export_arguments.path} already exists. Aborting.'
)
output_file = None
else:
output_file = DO_NOT_CAPTURE
full_command = (
(local_path, 'key', 'export')
+ (('--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_flags('paper', export_arguments.paper)
+ flags.make_flags('qr-html', export_arguments.qr_html)
+ flags.make_repository_flags(
repository_path,
local_borg_version,
)
+ ((export_arguments.path,) if output_file is None else ())
)
if global_arguments.dry_run:
logging.info(f'{repository_path}: Skipping key export (dry run)')
return
execute_command(
full_command,
output_file=output_file,
output_log_level=logging.ANSWER,
extra_environment=environment.make_environment(config),
borg_local_path=local_path,
borg_exit_codes=config.get('borg_exit_codes'),
)

View File

@ -0,0 +1,75 @@
import logging
import borgmatic.logger
from borgmatic.borg import environment, flags
from borgmatic.execute import DO_NOT_CAPTURE, execute_command
logger = logging.getLogger(__name__)
def export_tar_archive(
dry_run,
repository_path,
archive,
paths,
destination_path,
config,
local_borg_version,
global_arguments,
local_path='borg',
remote_path=None,
tar_filter=None,
list_files=False,
strip_components=None,
):
'''
Given a dry-run flag, a local or remote repository path, an archive name, zero or more paths to
export from the archive, a destination path to export to, a configuration dict, the local Borg
version, optional local and remote Borg paths, an optional filter program, whether to include
per-file details, and an optional number of path components to strip, export the archive into
the given destination path as a tar-formatted file.
If the destination path is "-", then stream the output to stdout instead of to a file.
'''
borgmatic.logger.add_custom_log_levels()
umask = config.get('umask', None)
lock_wait = config.get('lock_wait', None)
full_command = (
(local_path, 'export-tar')
+ (('--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 ())
+ (('--list',) if list_files else ())
+ (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
+ (('--dry-run',) if dry_run else ())
+ (('--tar-filter', tar_filter) if tar_filter else ())
+ (('--strip-components', str(strip_components)) if strip_components else ())
+ flags.make_repository_archive_flags(
repository_path,
archive,
local_borg_version,
)
+ (destination_path,)
+ (tuple(paths) if paths else ())
)
if list_files:
output_log_level = logging.ANSWER
else:
output_log_level = logging.INFO
if dry_run:
logging.info(f'{repository_path}: Skipping export to tar file (dry run)')
return
execute_command(
full_command,
output_file=DO_NOT_CAPTURE if destination_path == '-' else None,
output_log_level=output_log_level,
extra_environment=environment.make_environment(config),
borg_local_path=local_path,
borg_exit_codes=config.get('borg_exit_codes'),
)

View File

@ -1,54 +1,68 @@
import logging
import os
import subprocess
from borgmatic.execute import execute_command, execute_command_without_capture
import borgmatic.config.validate
from borgmatic.borg import environment, feature, flags, rlist
from borgmatic.execute import DO_NOT_CAPTURE, execute_command
logger = logging.getLogger(__name__)
def extract_last_archive_dry_run(repository, lock_wait=None, local_path='borg', remote_path=None):
def extract_last_archive_dry_run(
config,
local_borg_version,
global_arguments,
repository_path,
lock_wait=None,
local_path='borg',
remote_path=None,
):
'''
Perform an extraction dry-run of the most recent archive. If there are no archives, skip the
dry-run.
'''
remote_path_flags = ('--remote-path', remote_path) if remote_path else ()
lock_wait_flags = ('--lock-wait', str(lock_wait)) if lock_wait else ()
verbosity_flags = ()
if logger.isEnabledFor(logging.DEBUG):
verbosity_flags = ('--debug', '--show-rc')
elif logger.isEnabledFor(logging.INFO):
verbosity_flags = ('--info',)
full_list_command = (
(local_path, 'list', '--short')
+ remote_path_flags
+ lock_wait_flags
+ verbosity_flags
+ (repository,)
)
list_output = execute_command(full_list_command, output_log_level=None)
try:
last_archive_name = list_output.strip().splitlines()[-1]
except IndexError:
last_archive_name = rlist.resolve_archive_name(
repository_path,
'latest',
config,
local_borg_version,
global_arguments,
local_path,
remote_path,
)
except ValueError:
logger.warning('No archives found. Skipping extract consistency check.')
return
list_flag = ('--list',) if logger.isEnabledFor(logging.DEBUG) else ()
borg_environment = environment.make_environment(config)
full_extract_command = (
(local_path, 'extract', '--dry-run')
+ remote_path_flags
+ lock_wait_flags
+ (('--remote-path', remote_path) if remote_path else ())
+ (('--log-json',) if global_arguments.log_json else ())
+ (('--lock-wait', str(lock_wait)) if lock_wait else ())
+ verbosity_flags
+ list_flag
+ (
'{repository}::{last_archive_name}'.format(
repository=repository, last_archive_name=last_archive_name
),
+ flags.make_repository_archive_flags(
repository_path, last_archive_name, local_borg_version
)
)
execute_command(full_extract_command, working_directory=None, error_on_warnings=True)
execute_command(
full_extract_command,
working_directory=None,
extra_environment=borg_environment,
borg_local_path=local_path,
borg_exit_codes=config.get('borg_exit_codes'),
)
def extract_archive(
@ -56,44 +70,104 @@ def extract_archive(
repository,
archive,
paths,
location_config,
storage_config,
config,
local_borg_version,
global_arguments,
local_path='borg',
remote_path=None,
destination_path=None,
strip_components=None,
progress=False,
extract_to_stdout=False,
):
'''
Given a dry-run flag, a local or remote repository path, an archive name, zero or more paths to
restore from the archive, location/storage configuration dicts, optional local and remote Borg
paths, and an optional destination path to extract to, extract the archive into the current
directory.
restore from the archive, the local Borg version string, an argparse.Namespace of global
arguments, a configuration dict, optional local and remote Borg paths, and an optional
destination path to extract to, extract the archive into the current directory.
If extract to stdout is True, then start the extraction streaming to stdout, and return that
extract process as an instance of subprocess.Popen.
'''
umask = storage_config.get('umask', None)
lock_wait = storage_config.get('lock_wait', None)
umask = config.get('umask', None)
lock_wait = config.get('lock_wait', None)
if progress and extract_to_stdout:
raise ValueError('progress and extract_to_stdout cannot both be set')
if feature.available(feature.Feature.NUMERIC_IDS, local_borg_version):
numeric_ids_flags = ('--numeric-ids',) if config.get('numeric_ids') else ()
else:
numeric_ids_flags = ('--numeric-owner',) if config.get('numeric_ids') else ()
if strip_components == 'all':
if not paths:
raise ValueError('The --strip-components flag with "all" requires at least one --path')
# Calculate the maximum number of leading path components of the given paths. "if piece"
# ignores empty path components, e.g. those resulting from a leading slash. And the "- 1"
# 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)
)
full_command = (
(local_path, 'extract')
+ (('--remote-path', remote_path) if remote_path else ())
+ (('--numeric-owner',) if location_config.get('numeric_owner') else ())
+ numeric_ids_flags
+ (('--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', '--list', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
+ (('--dry-run',) if dry_run else ())
+ (('--strip-components', str(strip_components)) if strip_components else ())
+ (('--progress',) if progress else ())
+ ('::'.join((os.path.abspath(repository), archive)),)
+ (('--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),
archive,
local_borg_version,
)
+ (tuple(paths) if paths else ())
)
borg_environment = environment.make_environment(config)
borg_exit_codes = config.get('borg_exit_codes')
# The progress output isn't compatible with captured and logged output, as progress messes with
# the terminal directly.
if progress:
execute_command_without_capture(
full_command, working_directory=destination_path, error_on_warnings=True
return execute_command(
full_command,
output_file=DO_NOT_CAPTURE,
working_directory=destination_path,
extra_environment=borg_environment,
borg_local_path=local_path,
borg_exit_codes=borg_exit_codes,
)
return
return None
# Error on warnings, as Borg only gives a warning if the restore paths don't exist in the
# archive!
execute_command(full_command, working_directory=destination_path, error_on_warnings=True)
if extract_to_stdout:
return execute_command(
full_command,
output_file=subprocess.PIPE,
working_directory=destination_path,
run_to_completion=False,
extra_environment=borg_environment,
borg_local_path=local_path,
borg_exit_codes=borg_exit_codes,
)
# Don't give Borg local path so as to error on warnings, as "borg extract" only gives a warning
# if the restore paths don't exist in the archive.
execute_command(
full_command,
working_directory=destination_path,
extra_environment=borg_environment,
borg_local_path=local_path,
borg_exit_codes=borg_exit_codes,
)

40
borgmatic/borg/feature.py Normal file
View File

@ -0,0 +1,40 @@
from enum import Enum
from packaging.version import parse
class Feature(Enum):
COMPACT = 1
ATIME = 2
NOFLAGS = 3
NUMERIC_IDS = 4
UPLOAD_RATELIMIT = 5
SEPARATE_REPOSITORY_ARCHIVE = 6
RCREATE = 7
RLIST = 8
RINFO = 9
MATCH_ARCHIVES = 10
EXCLUDED_FILES_MINUS = 11
FEATURE_TO_MINIMUM_BORG_VERSION = {
Feature.COMPACT: parse('1.2.0a2'), # borg compact
Feature.ATIME: parse('1.2.0a7'), # borg create --atime
Feature.NOFLAGS: parse('1.2.0a8'), # borg create --noflags
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.MATCH_ARCHIVES: parse('2.0.0b3'), # borg --match-archives
Feature.EXCLUDED_FILES_MINUS: parse('2.0.0b5'), # --list --filter uses "-" for excludes
}
def available(feature, borg_version):
'''
Given a Borg Feature constant and a Borg version string, return whether that feature is
available in that version of Borg.
'''
return FEATURE_TO_MINIMUM_BORG_VERSION[feature] <= parse(borg_version)

View File

@ -1,4 +1,11 @@
import itertools
import json
import logging
import re
from borgmatic.borg import feature
logger = logging.getLogger(__name__)
def make_flags(name, value):
@ -8,7 +15,7 @@ def make_flags(name, value):
if not value:
return ()
flag = '--{}'.format(name.replace('_', '-'))
flag = f"--{name.replace('_', '-')}"
if value is True:
return (flag,)
@ -29,3 +36,83 @@ def make_flags_from_arguments(arguments, excludes=()):
if name not in excludes and not name.startswith('_')
)
)
def make_repository_flags(repository_path, local_borg_version):
'''
Given the path of a Borg repository and the local Borg version, return Borg-version-appropriate
command-line flags (as a tuple) for selecting that repository.
'''
return (
('--repo',)
if feature.available(feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, local_borg_version)
else ()
) + (repository_path,)
def make_repository_archive_flags(repository_path, archive, local_borg_version):
'''
Given the path of a Borg repository, an archive name or pattern, and the local Borg version,
return Borg-version-appropriate command-line flags (as a tuple) for selecting that repository
and archive.
'''
return (
('--repo', repository_path, 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
def make_match_archives_flags(match_archives, archive_name_format, local_borg_version):
'''
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
format. This is done by replacing certain archive name format placeholders for ephemeral data
(like "{now}") with globs.
'''
if match_archives:
if match_archives in {'*', 're:.*', 'sh:*'}:
return ()
if feature.available(feature.Feature.MATCH_ARCHIVES, local_borg_version):
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
)
if derived_match_archives == '*':
return ()
if feature.available(feature.Feature.MATCH_ARCHIVES, local_borg_version):
return ('--match-archives', f'sh:{derived_match_archives}')
else:
return ('--glob-archives', f'{derived_match_archives}')
def warn_for_aggressive_archive_flags(json_command, json_output):
'''
Given a JSON archives command and the resulting JSON string output from running it, parse the
JSON and warn if the command used an archive flag but the output indicates zero archives were
found.
'''
archive_flags_used = {'--glob-archives', '--match-archives'}.intersection(set(json_command))
if not archive_flags_used:
return
try:
if len(json.loads(json_output)['archives']) == 0:
logger.warning('An archive filter was applied, but no matching archives were found.')
logger.warning(
'Try adding --match-archives "*" or adjusting archive_name_format/match_archives in configuration.'
)
except json.JSONDecodeError as error:
logger.debug(f'Cannot parse JSON output from archive command: {error}')
except (TypeError, KeyError):
logger.debug('Cannot parse JSON output from archive command: No "archives" key found')

View File

@ -1,22 +1,28 @@
import argparse
import logging
from borgmatic.borg.flags import make_flags, make_flags_from_arguments
from borgmatic.execute import execute_command
import borgmatic.logger
from borgmatic.borg import environment, feature, flags
from borgmatic.execute import execute_command, execute_command_and_capture_output
logger = logging.getLogger(__name__)
def display_archives_info(
repository, storage_config, info_arguments, local_path='borg', remote_path=None
def make_info_command(
repository_path,
config,
local_borg_version,
info_arguments,
global_arguments,
local_path,
remote_path,
):
'''
Given a local or remote repository path, a storage config dict, and the arguments to the info
action, display summary information for Borg archives in the repository or return JSON summary
information.
Given a local or remote repository path, a configuration dict, the local Borg version, the
arguments to the info action as an argparse.Namespace, and global arguments, return a command
as a tuple to display summary information for archives in the repository.
'''
lock_wait = storage_config.get('lock_wait', None)
full_command = (
return (
(local_path, 'info')
+ (
('--info',)
@ -28,16 +34,85 @@ def display_archives_info(
if logger.isEnabledFor(logging.DEBUG) and not info_arguments.json
else ()
)
+ make_flags('remote-path', remote_path)
+ make_flags('lock-wait', lock_wait)
+ make_flags_from_arguments(info_arguments, excludes=('repository', 'archive'))
+ flags.make_flags('remote-path', remote_path)
+ flags.make_flags('log-json', global_arguments.log_json)
+ flags.make_flags('lock-wait', config.get('lock_wait'))
+ (
'::'.join((repository, info_arguments.archive))
if info_arguments.archive
else repository,
(
flags.make_flags('match-archives', f'sh:{info_arguments.prefix}*')
if feature.available(feature.Feature.MATCH_ARCHIVES, local_borg_version)
else flags.make_flags('glob-archives', f'{info_arguments.prefix}*')
)
if info_arguments.prefix
else (
flags.make_match_archives_flags(
info_arguments.match_archives
or info_arguments.archive
or config.get('match_archives'),
config.get('archive_name_format'),
local_borg_version,
)
)
)
+ flags.make_flags_from_arguments(
info_arguments, excludes=('repository', 'archive', 'prefix', 'match_archives')
)
+ flags.make_repository_flags(repository_path, local_borg_version)
)
return execute_command(
full_command, output_log_level=None if info_arguments.json else logging.WARNING
def display_archives_info(
repository_path,
config,
local_borg_version,
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 info action as an argparse.Namespace, and global arguments, display summary
information for Borg archives in the repository or return JSON summary information.
'''
borgmatic.logger.add_custom_log_levels()
main_command = make_info_command(
repository_path,
config,
local_borg_version,
info_arguments,
global_arguments,
local_path,
remote_path,
)
json_command = make_info_command(
repository_path,
config,
local_borg_version,
argparse.Namespace(**dict(info_arguments.__dict__, json=True)),
global_arguments,
local_path,
remote_path,
)
borg_exit_codes = config.get('borg_exit_codes')
json_info = execute_command_and_capture_output(
json_command,
extra_environment=environment.make_environment(config),
borg_local_path=local_path,
borg_exit_codes=borg_exit_codes,
)
if info_arguments.json:
return json_info
flags.warn_for_aggressive_archive_flags(json_command, json_info)
execute_command(
main_command,
output_log_level=logging.ANSWER,
extra_environment=environment.make_environment(config),
borg_local_path=local_path,
borg_exit_codes=borg_exit_codes,
)

View File

@ -1,48 +0,0 @@
import logging
import subprocess
from borgmatic.execute import execute_command, execute_command_without_capture
logger = logging.getLogger(__name__)
INFO_REPOSITORY_NOT_FOUND_EXIT_CODE = 2
def initialize_repository(
repository,
encryption_mode,
append_only=None,
storage_quota=None,
local_path='borg',
remote_path=None,
):
'''
Given a local or remote repository path, a Borg encryption mode, whether the repository should
be append-only, and the storage quota to use, initialize the repository. If the repository
already exists, then log and skip initialization.
'''
info_command = (local_path, 'info', repository)
logger.debug(' '.join(info_command))
try:
execute_command(info_command, output_log_level=None)
logger.info('Repository already exists. Skipping initialization.')
return
except subprocess.CalledProcessError as error:
if error.returncode != INFO_REPOSITORY_NOT_FOUND_EXIT_CODE:
raise
init_command = (
(local_path, 'init')
+ (('--encryption', encryption_mode) if encryption_mode else ())
+ (('--append-only',) if append_only else ())
+ (('--storage-quota', storage_quota) if storage_quota else ())
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
+ (('--debug',) if logger.isEnabledFor(logging.DEBUG) else ())
+ (('--remote-path', remote_path) if remote_path else ())
+ (repository,)
)
# Don't use execute_command() here because it doesn't support interactive prompts.
execute_command_without_capture(init_command)

View File

@ -1,27 +1,41 @@
import argparse
import copy
import logging
import re
from borgmatic.borg.flags import make_flags, make_flags_from_arguments
from borgmatic.execute import execute_command
import borgmatic.logger
from borgmatic.borg import environment, feature, flags, rlist
from borgmatic.execute import execute_command, execute_command_and_capture_output
logger = logging.getLogger(__name__)
# A hack to convince Borg to exclude archives ending in ".checkpoint". This assumes that a
# non-checkpoint archive name ends in a digit (e.g. from a timestamp).
BORG_EXCLUDE_CHECKPOINTS_GLOB = '*[0123456789]'
ARCHIVE_FILTER_FLAGS_MOVED_TO_RLIST = ('prefix', 'match_archives', 'sort_by', 'first', 'last')
MAKE_FLAGS_EXCLUDES = (
'repository',
'archive',
'paths',
'find_paths',
) + ARCHIVE_FILTER_FLAGS_MOVED_TO_RLIST
def list_archives(repository, storage_config, list_arguments, local_path='borg', remote_path=None):
def make_list_command(
repository_path,
config,
local_borg_version,
list_arguments,
global_arguments,
local_path='borg',
remote_path=None,
):
'''
Given a local or remote repository path, a storage config dict, and the arguments to the list
action, display the output of listing Borg archives in the repository or return JSON output. Or,
if an archive name is given, listing the files in that archive.
Given a local or remote repository path, a configuration dict, the arguments to the list action,
and local and remote Borg paths, return a command as a tuple to list archives or paths within an
archive.
'''
lock_wait = storage_config.get('lock_wait', None)
if list_arguments.successful:
list_arguments.glob_archives = BORG_EXCLUDE_CHECKPOINTS_GLOB
lock_wait = config.get('lock_wait', None)
full_command = (
return (
(local_path, 'list')
+ (
('--info',)
@ -33,18 +47,217 @@ def list_archives(repository, storage_config, list_arguments, local_path='borg',
if logger.isEnabledFor(logging.DEBUG) and not list_arguments.json
else ()
)
+ make_flags('remote-path', remote_path)
+ make_flags('lock-wait', lock_wait)
+ make_flags_from_arguments(
list_arguments, excludes=('repository', 'archive', 'successful')
)
+ flags.make_flags('remote-path', remote_path)
+ flags.make_flags('log-json', global_arguments.log_json)
+ flags.make_flags('lock-wait', lock_wait)
+ flags.make_flags_from_arguments(list_arguments, excludes=MAKE_FLAGS_EXCLUDES)
+ (
'::'.join((repository, list_arguments.archive))
flags.make_repository_archive_flags(
repository_path, list_arguments.archive, local_borg_version
)
if list_arguments.archive
else repository,
else flags.make_repository_flags(repository_path, local_borg_version)
)
+ (tuple(list_arguments.paths) if list_arguments.paths else ())
)
return execute_command(
full_command, output_log_level=None if list_arguments.json else logging.WARNING
def make_find_paths(find_paths):
'''
Given a sequence of path fragments or patterns as passed to `--find`, transform all path
fragments into glob patterns. Pass through existing patterns untouched.
For example, given find_paths of:
['foo.txt', 'pp:root/somedir']
... transform that into:
['sh:**/*foo.txt*/**', 'pp:root/somedir']
'''
if not find_paths:
return ()
return tuple(
(
find_path
if re.compile(r'([-!+RrPp] )|(\w\w:)').match(find_path)
else f'sh:**/*{find_path}*/**'
)
for find_path in find_paths
)
def capture_archive_listing(
repository_path,
archive,
config,
local_borg_version,
global_arguments,
list_paths=None,
path_format=None,
local_path='borg',
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.
'''
borg_environment = environment.make_environment(config)
return tuple(
execute_command_and_capture_output(
make_list_command(
repository_path,
config,
local_borg_version,
argparse.Namespace(
repository=repository_path,
archive=archive,
paths=[f'sh:{path}' for path in list_paths] if list_paths else None,
find_paths=None,
json=None,
format=path_format or '{path}{NL}', # noqa: FS003
),
global_arguments,
local_path,
remote_path,
),
extra_environment=borg_environment,
borg_local_path=local_path,
borg_exit_codes=config.get('borg_exit_codes'),
)
.strip('\n')
.split('\n')
)
def list_archive(
repository_path,
config,
local_borg_version,
list_arguments,
global_arguments,
local_path='borg',
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.
'''
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):
logger.warning(
'Omitting the --archive flag on the list action is deprecated when using Borg 2.x+. Use the rlist action instead.'
)
rlist_arguments = argparse.Namespace(
repository=repository_path,
short=list_arguments.short,
format=list_arguments.format,
json=list_arguments.json,
prefix=list_arguments.prefix,
match_archives=list_arguments.match_archives,
sort_by=list_arguments.sort_by,
first=list_arguments.first,
last=list_arguments.last,
)
return rlist.list_repository(
repository_path,
config,
local_borg_version,
rlist_arguments,
global_arguments,
local_path,
remote_path,
)
if list_arguments.archive:
for name in ARCHIVE_FILTER_FLAGS_MOVED_TO_RLIST:
if getattr(list_arguments, name, None):
logger.warning(
f"The --{name.replace('_', '-')} flag on the list action is ignored when using the --archive flag."
)
if list_arguments.json:
raise ValueError(
'The --json flag on the list action is not supported when using the --archive/--find flags.'
)
borg_environment = environment.make_environment(config)
borg_exit_codes = config.get('borg_exit_codes')
# 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(
repository=repository_path,
short=True,
format=None,
json=None,
prefix=list_arguments.prefix,
match_archives=list_arguments.match_archives,
sort_by=list_arguments.sort_by,
first=list_arguments.first,
last=list_arguments.last,
)
# Ask Borg to list archives. Capture its output for use below.
archive_lines = tuple(
execute_command_and_capture_output(
rlist.make_rlist_command(
repository_path,
config,
local_borg_version,
rlist_arguments,
global_arguments,
local_path,
remote_path,
),
extra_environment=borg_environment,
borg_local_path=local_path,
borg_exit_codes=borg_exit_codes,
)
.strip('\n')
.split('\n')
)
else:
archive_lines = (list_arguments.archive,)
# For each archive listed by Borg, run list on the contents of that archive.
for archive in archive_lines:
logger.answer(f'{repository_path}: Listing archive {archive}')
archive_arguments = copy.copy(list_arguments)
archive_arguments.archive = 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:
setattr(archive_arguments, name, None)
main_command = make_list_command(
repository_path,
config,
local_borg_version,
archive_arguments,
global_arguments,
local_path,
remote_path,
) + make_find_paths(list_arguments.find_paths)
execute_command(
main_command,
output_log_level=logging.ANSWER,
extra_environment=borg_environment,
borg_local_path=local_path,
borg_exit_codes=borg_exit_codes,
)

79
borgmatic/borg/mount.py Normal file
View File

@ -0,0 +1,79 @@
import logging
from borgmatic.borg import environment, feature, flags
from borgmatic.execute import DO_NOT_CAPTURE, execute_command
logger = logging.getLogger(__name__)
def mount_archive(
repository_path,
archive,
mount_arguments,
config,
local_borg_version,
global_arguments,
local_path='borg',
remote_path=None,
):
'''
Given a local or remote repository path, an optional archive name, a filesystem mount point,
zero or more paths to mount from the archive, extra Borg mount options, a storage configuration
dict, the local Borg version, global arguments as an argparse.Namespace instance, and optional
local and remote Borg paths, mount the archive onto the mount point.
'''
umask = config.get('umask', None)
lock_wait = config.get('lock_wait', None)
full_command = (
(local_path, 'mount')
+ (('--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_flags_from_arguments(
mount_arguments,
excludes=('repository', 'archive', 'mount_point', 'paths', 'options'),
)
+ (('-o', mount_arguments.options) if mount_arguments.options else ())
+ (
(
flags.make_repository_flags(repository_path, local_borg_version)
+ (
('--match-archives', archive)
if feature.available(feature.Feature.MATCH_ARCHIVES, local_borg_version)
else ('--glob-archives', archive)
)
)
if feature.available(feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, local_borg_version)
else (
flags.make_repository_archive_flags(repository_path, archive, local_borg_version)
if archive
else flags.make_repository_flags(repository_path, local_borg_version)
)
)
+ (mount_arguments.mount_point,)
+ (tuple(mount_arguments.paths) if mount_arguments.paths else ())
)
borg_environment = environment.make_environment(config)
# Don't capture the output when foreground mode is used so that ctrl-C can work properly.
if mount_arguments.foreground:
execute_command(
full_command,
output_file=DO_NOT_CAPTURE,
extra_environment=borg_environment,
borg_local_path=local_path,
borg_exit_codes=config.get('borg_exit_codes'),
)
return
execute_command(
full_command,
extra_environment=borg_environment,
borg_local_path=local_path,
borg_exit_codes=config.get('borg_exit_codes'),
)

View File

@ -1,14 +1,16 @@
import logging
import borgmatic.logger
from borgmatic.borg import environment, feature, flags
from borgmatic.execute import execute_command
logger = logging.getLogger(__name__)
def _make_prune_flags(retention_config):
def make_prune_flags(config, local_borg_version):
'''
Given a retention config dict mapping from option name to value, tranform it into an iterable of
command-line name-value flag pairs.
Given a configuration dict mapping from option name to value, transform it into an sequence of
command-line flags.
For example, given a retention config of:
@ -21,47 +23,78 @@ def _make_prune_flags(retention_config):
('--keep-monthly', '6'),
)
'''
config = retention_config.copy()
flag_pairs = (
('--' + option_name.replace('_', '-'), str(value))
for option_name, value in config.items()
if option_name.startswith('keep_') and option_name != 'keep_exclude_tags'
)
prefix = config.get('prefix')
if 'prefix' not in config:
config['prefix'] = '{hostname}-'
elif not config['prefix']:
config.pop('prefix')
return (
('--' + option_name.replace('_', '-'), str(value)) for option_name, value in config.items()
return tuple(element for pair in flag_pairs for element in pair) + (
(
('--match-archives', f'sh:{prefix}*')
if feature.available(feature.Feature.MATCH_ARCHIVES, local_borg_version)
else ('--glob-archives', f'{prefix}*')
)
if prefix
else (
flags.make_match_archives_flags(
config.get('match_archives'),
config.get('archive_name_format'),
local_borg_version,
)
)
)
def prune_archives(
dry_run,
repository,
storage_config,
retention_config,
repository_path,
config,
local_borg_version,
prune_arguments,
global_arguments,
local_path='borg',
remote_path=None,
stats=False,
):
'''
Given dry-run flag, a local or remote repository path, a storage config dict, and a
retention config dict, prune Borg archives according to the retention policy specified in that
configuration.
Given dry-run flag, a local or remote repository path, and a configuration dict, prune Borg
archives according to the retention policy specified in that configuration.
'''
umask = storage_config.get('umask', None)
lock_wait = storage_config.get('lock_wait', None)
borgmatic.logger.add_custom_log_levels()
umask = config.get('umask', None)
lock_wait = config.get('lock_wait', None)
extra_borg_options = config.get('extra_borg_options', {}).get('prune', '')
full_command = (
(local_path, 'prune')
+ tuple(element for pair in _make_prune_flags(retention_config) for element in pair)
+ make_prune_flags(config, 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 ())
+ (('--lock-wait', str(lock_wait)) if lock_wait else ())
+ (('--stats',) if not dry_run and logger.isEnabledFor(logging.INFO) else ())
+ (('--stats',) if prune_arguments.stats and not dry_run else ())
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
+ (('--debug', '--list', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
+ flags.make_flags_from_arguments(
prune_arguments,
excludes=('repository', 'stats', 'list_archives'),
)
+ (('--list',) if prune_arguments.list_archives else ())
+ (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
+ (('--dry-run',) if dry_run else ())
+ (('--stats',) if stats else ())
+ (repository,)
+ (tuple(extra_borg_options.split(' ')) if extra_borg_options else ())
+ flags.make_repository_flags(repository_path, local_borg_version)
)
execute_command(full_command, output_log_level=logging.WARNING if stats else logging.INFO)
if prune_arguments.stats or prune_arguments.list_archives:
output_log_level = logging.ANSWER
else:
output_log_level = logging.INFO
execute_command(
full_command,
output_log_level=output_log_level,
extra_environment=environment.make_environment(config),
borg_local_path=local_path,
borg_exit_codes=config.get('borg_exit_codes'),
)

101
borgmatic/borg/rcreate.py Normal file
View File

@ -0,0 +1,101 @@
import argparse
import json
import logging
import subprocess
from borgmatic.borg import environment, feature, flags, rinfo
from borgmatic.execute import DO_NOT_CAPTURE, execute_command
logger = logging.getLogger(__name__)
RINFO_REPOSITORY_NOT_FOUND_EXIT_CODES = {2, 13}
def create_repository(
dry_run,
repository_path,
config,
local_borg_version,
global_arguments,
encryption_mode,
source_repository=None,
copy_crypt_key=False,
append_only=None,
storage_quota=None,
make_parent_dirs=False,
local_path='borg',
remote_path=None,
):
'''
Given a dry-run flag, a local or remote repository path, a configuration dict, the local Borg
version, a Borg encryption mode, the path to another repo whose key material should be reused,
whether the repository should be append-only, and the storage quota to use, create the
repository. If the repository already exists, then log and skip creation.
Raise ValueError if the requested encryption mode does not match that of the repository.
Raise json.decoder.JSONDecodeError if the "borg info" JSON outputcannot be decoded.
Raise subprocess.CalledProcessError if "borg info" returns an error exit code.
'''
try:
info_data = json.loads(
rinfo.display_repository_info(
repository_path,
config,
local_borg_version,
argparse.Namespace(json=True),
global_arguments,
local_path,
remote_path,
)
)
repository_encryption_mode = info_data.get('encryption', {}).get('mode')
if repository_encryption_mode != encryption_mode:
raise ValueError(
f'Requested encryption mode "{encryption_mode}" does not match existing repository encryption mode "{repository_encryption_mode}"'
)
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:
raise
lock_wait = config.get('lock_wait')
extra_borg_options = config.get('extra_borg_options', {}).get('rcreate', '')
rcreate_command = (
(local_path,)
+ (
('rcreate',)
if feature.available(feature.Feature.RCREATE, local_borg_version)
else ('init',)
)
+ (('--encryption', encryption_mode) if encryption_mode else ())
+ (('--other-repo', source_repository) if source_repository else ())
+ (('--copy-crypt-key',) if copy_crypt_key else ())
+ (('--append-only',) if append_only else ())
+ (('--storage-quota', storage_quota) if storage_quota else ())
+ (('--make-parent-dirs',) if make_parent_dirs else ())
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
+ (('--debug',) if logger.isEnabledFor(logging.DEBUG) else ())
+ (('--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 ())
+ (tuple(extra_borg_options.split(' ')) if extra_borg_options else ())
+ flags.make_repository_flags(repository_path, local_borg_version)
)
if dry_run:
logging.info(f'{repository_path}: Skipping repository creation (dry run)')
return
# Do not capture output here, so as to support interactive prompts.
execute_command(
rcreate_command,
output_file=DO_NOT_CAPTURE,
extra_environment=environment.make_environment(config),
borg_local_path=local_path,
borg_exit_codes=config.get('borg_exit_codes'),
)

68
borgmatic/borg/rinfo.py Normal file
View File

@ -0,0 +1,68 @@
import logging
import borgmatic.logger
from borgmatic.borg import environment, feature, flags
from borgmatic.execute import execute_command, execute_command_and_capture_output
logger = logging.getLogger(__name__)
def display_repository_info(
repository_path,
config,
local_borg_version,
rinfo_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
information for the Borg repository or return JSON summary information.
'''
borgmatic.logger.add_custom_log_levels()
lock_wait = config.get('lock_wait', None)
full_command = (
(local_path,)
+ (
('rinfo',)
if feature.available(feature.Feature.RINFO, local_borg_version)
else ('info',)
)
+ (
('--info',)
if logger.getEffectiveLevel() == logging.INFO and not rinfo_arguments.json
else ()
)
+ (
('--debug', '--show-rc')
if logger.isEnabledFor(logging.DEBUG) and not rinfo_arguments.json
else ()
)
+ flags.make_flags('remote-path', remote_path)
+ flags.make_flags('log-json', global_arguments.log_json)
+ flags.make_flags('lock-wait', lock_wait)
+ (('--json',) if rinfo_arguments.json else ())
+ flags.make_repository_flags(repository_path, local_borg_version)
)
extra_environment = environment.make_environment(config)
borg_exit_codes = config.get('borg_exit_codes')
if rinfo_arguments.json:
return execute_command_and_capture_output(
full_command,
extra_environment=extra_environment,
borg_local_path=local_path,
borg_exit_codes=borg_exit_codes,
)
else:
execute_command(
full_command,
output_log_level=logging.ANSWER,
extra_environment=extra_environment,
borg_local_path=local_path,
borg_exit_codes=borg_exit_codes,
)

171
borgmatic/borg/rlist.py Normal file
View File

@ -0,0 +1,171 @@
import argparse
import logging
import borgmatic.logger
from borgmatic.borg import environment, feature, flags
from borgmatic.execute import execute_command, execute_command_and_capture_output
logger = logging.getLogger(__name__)
def resolve_archive_name(
repository_path,
archive,
config,
local_borg_version,
global_arguments,
local_path='borg',
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, a local Borg path, and a remote Borg path,
return the archive name. But if the archive name is "latest", then instead introspect the
repository for the latest archive and return its name.
Raise ValueError if "latest" is given but there are no archives in the repository.
'''
if archive != 'latest':
return archive
full_command = (
(
local_path,
'rlist' if feature.available(feature.Feature.RLIST, local_borg_version) else 'list',
)
+ flags.make_flags('remote-path', remote_path)
+ flags.make_flags('log-json', global_arguments.log_json)
+ flags.make_flags('lock-wait', config.get('lock_wait'))
+ flags.make_flags('last', 1)
+ ('--short',)
+ flags.make_repository_flags(repository_path, local_borg_version)
)
output = execute_command_and_capture_output(
full_command,
extra_environment=environment.make_environment(config),
borg_local_path=local_path,
borg_exit_codes=config.get('borg_exit_codes'),
)
try:
latest_archive = output.strip().splitlines()[-1]
except IndexError:
raise ValueError('No archives found in the repository')
logger.debug(f'{repository_path}: Latest archive is {latest_archive}')
return latest_archive
MAKE_FLAGS_EXCLUDES = ('repository', 'prefix', 'match_archives')
def make_rlist_command(
repository_path,
config,
local_borg_version,
rlist_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
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',
)
+ (
('--info',)
if logger.getEffectiveLevel() == logging.INFO and not rlist_arguments.json
else ()
)
+ (
('--debug', '--show-rc')
if logger.isEnabledFor(logging.DEBUG) and not rlist_arguments.json
else ()
)
+ flags.make_flags('remote-path', remote_path)
+ 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}*')
if feature.available(feature.Feature.MATCH_ARCHIVES, local_borg_version)
else flags.make_flags('glob-archives', f'{rlist_arguments.prefix}*')
)
if rlist_arguments.prefix
else (
flags.make_match_archives_flags(
rlist_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_repository_flags(repository_path, local_borg_version)
)
def list_repository(
repository_path,
config,
local_borg_version,
rlist_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 list action, global arguments as an argparse.Namespace instance, and local and
remote Borg paths, display the output of listing Borg archives in the given repository (or
return JSON output).
'''
borgmatic.logger.add_custom_log_levels()
borg_environment = environment.make_environment(config)
main_command = make_rlist_command(
repository_path,
config,
local_borg_version,
rlist_arguments,
global_arguments,
local_path,
remote_path,
)
json_command = make_rlist_command(
repository_path,
config,
local_borg_version,
argparse.Namespace(**dict(rlist_arguments.__dict__, json=True)),
global_arguments,
local_path,
remote_path,
)
borg_exit_codes = config.get('borg_exit_codes')
json_listing = execute_command_and_capture_output(
json_command,
extra_environment=borg_environment,
borg_local_path=local_path,
borg_exit_codes=borg_exit_codes,
)
if rlist_arguments.json:
return json_listing
flags.warn_for_aggressive_archive_flags(json_command, json_listing)
execute_command(
main_command,
output_log_level=logging.ANSWER,
extra_environment=borg_environment,
borg_local_path=local_path,
borg_exit_codes=borg_exit_codes,
)

1
borgmatic/borg/state.py Normal file
View File

@ -0,0 +1 @@
DEFAULT_BORGMATIC_SOURCE_DIRECTORY = '~/.borgmatic'

View File

@ -0,0 +1,61 @@
import logging
import borgmatic.logger
from borgmatic.borg import environment, flags
from borgmatic.execute import DO_NOT_CAPTURE, execute_command
logger = logging.getLogger(__name__)
def transfer_archives(
dry_run,
repository_path,
config,
local_borg_version,
transfer_arguments,
global_arguments,
local_path='borg',
remote_path=None,
):
'''
Given a dry-run flag, a local or remote repository path, a configuration dict, the local Borg
version, the arguments to the transfer action, and global arguments as an argparse.Namespace
instance, transfer archives to the given repository.
'''
borgmatic.logger.add_custom_log_levels()
full_command = (
(local_path, 'transfer')
+ (('--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('log-json', global_arguments.log_json)
+ flags.make_flags('lock-wait', config.get('lock_wait', None))
+ (
flags.make_flags_from_arguments(
transfer_arguments,
excludes=('repository', 'source_repository', 'archive', 'match_archives'),
)
or (
flags.make_match_archives_flags(
transfer_arguments.match_archives
or transfer_arguments.archive
or config.get('match_archives'),
config.get('archive_name_format'),
local_borg_version,
)
)
)
+ flags.make_repository_flags(repository_path, local_borg_version)
+ flags.make_flags('other-repo', transfer_arguments.source_repository)
+ flags.make_flags('dry-run', dry_run)
)
return execute_command(
full_command,
output_log_level=logging.ANSWER,
output_file=DO_NOT_CAPTURE if transfer_arguments.progress else None,
borg_local_path=local_path,
borg_exit_codes=config.get('borg_exit_codes'),
extra_environment=environment.make_environment(config),
)

22
borgmatic/borg/umount.py Normal file
View File

@ -0,0 +1,22 @@
import logging
from borgmatic.execute import execute_command
logger = logging.getLogger(__name__)
def unmount_archive(config, mount_point, local_path='borg'):
'''
Given a mounted filesystem mount point, and an optional local Borg paths, umount the filesystem
from the mount point.
'''
full_command = (
(local_path, 'umount')
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
+ (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
+ (mount_point,)
)
execute_command(
full_command, borg_local_path=local_path, borg_exit_codes=config.get('borg_exit_codes')
)

31
borgmatic/borg/version.py Normal file
View File

@ -0,0 +1,31 @@
import logging
from borgmatic.borg import environment
from borgmatic.execute import execute_command_and_capture_output
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.
Raise OSError or CalledProcessError if there is a problem running Borg.
Raise ValueError if the version cannot be parsed.
'''
full_command = (
(local_path, '--version')
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
+ (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
)
output = execute_command_and_capture_output(
full_command,
extra_environment=environment.make_environment(config),
borg_local_path=local_path,
borg_exit_codes=config.get('borg_exit_codes'),
)
try:
return output.split(' ')[1].strip()
except IndexError:
raise ValueError('Could not parse Borg version string')

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,36 @@
import borgmatic.commands.arguments
def upgrade_message(language: str, upgrade_command: str, completion_file: str):
return f'''
Your {language} completions script is from a different version of borgmatic than is
currently installed. Please upgrade your script so your completions match the
command-line flags in your installed borgmatic! Try this to upgrade:
{upgrade_command}
source {completion_file}
'''
def available_actions(subparsers, current_action=None):
'''
Given subparsers as an argparse._SubParsersAction instance and a current action name (if
any), return the actions names that can follow the current action on a command-line.
This takes into account which sub-actions that the current action supports. For instance, if
"bootstrap" is a sub-action for "config", then "bootstrap" should be able to follow a current
action of "config" but not "list".
'''
action_to_subactions = borgmatic.commands.arguments.get_subactions_for_actions(
subparsers.choices
)
current_subactions = action_to_subactions.get(current_action)
if current_subactions:
return current_subactions
all_subactions = set(
subaction for subactions in action_to_subactions.values() for subaction in subactions
)
return tuple(action for action in subparsers.choices.keys() if action not in all_subactions)

View File

@ -0,0 +1,66 @@
import borgmatic.commands.arguments
import borgmatic.commands.completion.actions
def parser_flags(parser):
'''
Given an argparse.ArgumentParser instance, return its argument flags in a space-separated
string.
'''
return ' '.join(option for action in parser._actions for option in action.option_strings)
def bash_completion():
'''
Return a bash completion script for the borgmatic command. Produce this by introspecting
borgmatic's command-line argument parsers.
'''
(
unused_global_parser,
action_parsers,
global_plus_action_parser,
) = borgmatic.commands.arguments.make_parsers()
global_flags = parser_flags(global_plus_action_parser)
# Avert your eyes.
return '\n'.join(
(
'check_version() {',
' local this_script="$(cat "$BASH_SOURCE" 2> /dev/null)"',
' local installed_script="$(borgmatic --bash-completion 2> /dev/null)"',
' if [ "$this_script" != "$installed_script" ] && [ "$installed_script" != "" ];'
f''' then cat << EOF\n{borgmatic.commands.completion.actions.upgrade_message(
'bash',
'sudo sh -c "borgmatic --bash-completion > $BASH_SOURCE"',
'$BASH_SOURCE',
)}\nEOF''',
' fi',
'}',
'complete_borgmatic() {',
)
+ tuple(
''' if [[ " ${COMP_WORDS[*]} " =~ " %s " ]]; then
COMPREPLY=($(compgen -W "%s %s %s" -- "${COMP_WORDS[COMP_CWORD]}"))
return 0
fi'''
% (
action,
parser_flags(action_parser),
' '.join(
borgmatic.commands.completion.actions.available_actions(action_parsers, action)
),
global_flags,
)
for action, action_parser in reversed(action_parsers.choices.items())
)
+ (
' COMPREPLY=($(compgen -W "%s %s" -- "${COMP_WORDS[COMP_CWORD]}"))' # noqa: FS003
% (
' '.join(borgmatic.commands.completion.actions.available_actions(action_parsers)),
global_flags,
),
' (check_version &)',
'}',
'\ncomplete -o bashdefault -o default -F complete_borgmatic borgmatic',
)
)

View File

@ -0,0 +1,176 @@
import shlex
from argparse import Action
from textwrap import dedent
import borgmatic.commands.arguments
import borgmatic.commands.completion.actions
def has_file_options(action: Action):
'''
Given an argparse.Action instance, return True if it takes a file argument.
'''
return action.metavar in (
'FILENAME',
'PATH',
) or action.dest in ('config_paths',)
def has_choice_options(action: Action):
'''
Given an argparse.Action instance, return True if it takes one of a predefined set of arguments.
'''
return action.choices is not None
def has_unknown_required_param_options(action: Action):
'''
A catch-all for options that take a required parameter, but we don't know what the parameter is.
This should be used last. These are actions that take something like a glob, a list of numbers, or a string.
Actions that match this pattern should not show the normal arguments, because those are unlikely to be valid.
'''
return (
action.required is True
or action.nargs
in (
'+',
'*',
)
or action.metavar in ('PATTERN', 'KEYS', 'N')
or (action.type is not None and action.default is None)
)
def has_exact_options(action: Action):
return (
has_file_options(action)
or has_choice_options(action)
or has_unknown_required_param_options(action)
)
def exact_options_completion(action: Action):
'''
Given an argparse.Action instance, return a completion invocation that forces file completions, options completion,
or just that some value follow the action, if the action takes such an argument and was the last action on the
command line prior to the cursor.
Otherwise, return an empty string.
'''
if not has_exact_options(action):
return ''
args = ' '.join(action.option_strings)
if has_file_options(action):
return f'''\ncomplete -c borgmatic -Fr -n "__borgmatic_current_arg {args}"'''
if has_choice_options(action):
return f'''\ncomplete -c borgmatic -f -a '{' '.join(map(str, action.choices))}' -n "__borgmatic_current_arg {args}"'''
if has_unknown_required_param_options(action):
return f'''\ncomplete -c borgmatic -x -n "__borgmatic_current_arg {args}"'''
raise ValueError(
f'Unexpected action: {action} passes has_exact_options but has no choices produced'
)
def dedent_strip_as_tuple(string: str):
'''
Dedent a string, then strip it to avoid requiring your first line to have content, then return a tuple of the string.
Makes it easier to write multiline strings for completions when you join them with a tuple.
'''
return (dedent(string).strip('\n'),)
def fish_completion():
'''
Return a fish completion script for the borgmatic command. Produce this by introspecting
borgmatic's command-line argument parsers.
'''
(
unused_global_parser,
action_parsers,
global_plus_action_parser,
) = borgmatic.commands.arguments.make_parsers()
all_action_parsers = ' '.join(action for action in action_parsers.choices.keys())
exact_option_args = tuple(
' '.join(action.option_strings)
for action_parser in action_parsers.choices.values()
for action in action_parser._actions
if has_exact_options(action)
) + tuple(
' '.join(action.option_strings)
for action in global_plus_action_parser._actions
if len(action.option_strings) > 0
if has_exact_options(action)
)
# Avert your eyes.
return '\n'.join(
dedent_strip_as_tuple(
f'''
function __borgmatic_check_version
set -fx this_filename (status current-filename)
fish -c '
if test -f "$this_filename"
set this_script (cat $this_filename 2> /dev/null)
set installed_script (borgmatic --fish-completion 2> /dev/null)
if [ "$this_script" != "$installed_script" ] && [ "$installed_script" != "" ]
echo "{borgmatic.commands.completion.actions.upgrade_message(
'fish',
'borgmatic --fish-completion | sudo tee $this_filename',
'$this_filename',
)}"
end
end
' &
end
__borgmatic_check_version
function __borgmatic_current_arg --description 'Check if any of the given arguments are the last on the command line before the cursor'
set -l all_args (commandline -poc)
# premature optimization to avoid iterating all args if there aren't enough
# to have a last arg beyond borgmatic
if [ (count $all_args) -lt 2 ]
return 1
end
for arg in $argv
if [ "$arg" = "$all_args[-1]" ]
return 0
end
end
return 1
end
set --local action_parser_condition "not __fish_seen_subcommand_from {all_action_parsers}"
set --local exact_option_condition "not __borgmatic_current_arg {' '.join(exact_option_args)}"
'''
)
+ ('\n# action_parser completions',)
+ tuple(
f'''complete -c borgmatic -f -n "$action_parser_condition" -n "$exact_option_condition" -a '{action_name}' -d {shlex.quote(action_parser.description)}'''
for action_name, action_parser in action_parsers.choices.items()
)
+ ('\n# global flags',)
+ tuple(
# -n is checked in order, so put faster / more likely to be true checks first
f'''complete -c borgmatic -f -n "$exact_option_condition" -a '{' '.join(action.option_strings)}' -d {shlex.quote(action.help)}{exact_options_completion(action)}'''
for action in global_plus_action_parser._actions
# ignore the noargs action, as this is an impossible completion for fish
if len(action.option_strings) > 0
if 'Deprecated' not in action.help
)
+ ('\n# action_parser flags',)
+ tuple(
f'''complete -c borgmatic -f -n "$exact_option_condition" -a '{' '.join(action.option_strings)}' -d {shlex.quote(action.help)} -n "__fish_seen_subcommand_from {action_name}"{exact_options_completion(action)}'''
for action_name, action_parser in action_parsers.choices.items()
for action in action_parser._actions
if 'Deprecated' not in (action.help or ())
)
)

View File

@ -1,108 +0,0 @@
import os
import sys
import textwrap
from argparse import ArgumentParser
from ruamel import yaml
from borgmatic.config import convert, generate, legacy, validate
DEFAULT_SOURCE_CONFIG_FILENAME = '/etc/borgmatic/config'
DEFAULT_SOURCE_EXCLUDES_FILENAME = '/etc/borgmatic/excludes'
DEFAULT_DESTINATION_CONFIG_FILENAME = '/etc/borgmatic/config.yaml'
def parse_arguments(*arguments):
'''
Given command-line arguments with which this script was invoked, parse the arguments and return
them as an ArgumentParser instance.
'''
parser = ArgumentParser(
description='''
Convert legacy INI-style borgmatic configuration and excludes files to a single YAML
configuration file. Note that this replaces any comments from the source files.
'''
)
parser.add_argument(
'-s',
'--source-config',
dest='source_config_filename',
default=DEFAULT_SOURCE_CONFIG_FILENAME,
help='Source INI-style configuration filename. Default: {}'.format(
DEFAULT_SOURCE_CONFIG_FILENAME
),
)
parser.add_argument(
'-e',
'--source-excludes',
dest='source_excludes_filename',
default=DEFAULT_SOURCE_EXCLUDES_FILENAME
if os.path.exists(DEFAULT_SOURCE_EXCLUDES_FILENAME)
else None,
help='Excludes filename',
)
parser.add_argument(
'-d',
'--destination-config',
dest='destination_config_filename',
default=DEFAULT_DESTINATION_CONFIG_FILENAME,
help='Destination YAML configuration filename. Default: {}'.format(
DEFAULT_DESTINATION_CONFIG_FILENAME
),
)
return parser.parse_args(arguments)
TEXT_WRAP_CHARACTERS = 80
def display_result(args): # pragma: no cover
result_lines = textwrap.wrap(
'Your borgmatic configuration has been upgraded. Please review the result in {}.'.format(
args.destination_config_filename
),
TEXT_WRAP_CHARACTERS,
)
delete_lines = textwrap.wrap(
'Once you are satisfied, you can safely delete {}{}.'.format(
args.source_config_filename,
' and {}'.format(args.source_excludes_filename)
if args.source_excludes_filename
else '',
),
TEXT_WRAP_CHARACTERS,
)
print('\n'.join(result_lines))
print()
print('\n'.join(delete_lines))
def main(): # pragma: no cover
try:
args = parse_arguments(*sys.argv[1:])
schema = yaml.round_trip_load(open(validate.schema_filename()).read())
source_config = legacy.parse_configuration(
args.source_config_filename, legacy.CONFIG_FORMAT
)
source_config_file_mode = os.stat(args.source_config_filename).st_mode
source_excludes = (
open(args.source_excludes_filename).read().splitlines()
if args.source_excludes_filename
else []
)
destination_config = convert.convert_legacy_parsed_config(
source_config, source_excludes, schema
)
generate.write_configuration(
args.destination_config_filename, destination_config, mode=source_config_file_mode
)
display_result(args)
except (ValueError, OSError) as error:
print(error, file=sys.stderr)
sys.exit(1)

View File

@ -1,44 +1,17 @@
import logging
import sys
from argparse import ArgumentParser
from borgmatic.config import generate, validate
DEFAULT_DESTINATION_CONFIG_FILENAME = '/etc/borgmatic/config.yaml'
import borgmatic.commands.borgmatic
def parse_arguments(*arguments):
'''
Given command-line arguments with which this script was invoked, parse the arguments and return
them as an ArgumentParser instance.
'''
parser = ArgumentParser(description='Generate a sample borgmatic YAML configuration file.')
parser.add_argument(
'-d',
'--destination',
dest='destination_filename',
default=DEFAULT_DESTINATION_CONFIG_FILENAME,
help='Destination YAML configuration filename. Default: {}'.format(
DEFAULT_DESTINATION_CONFIG_FILENAME
),
def main():
warning_log = logging.makeLogRecord(
dict(
levelno=logging.WARNING,
levelname='WARNING',
msg='generate-borgmatic-config is deprecated and will be removed from a future release. Please use "borgmatic config generate" instead.',
)
)
return parser.parse_args(arguments)
def main(): # pragma: no cover
try:
args = parse_arguments(*sys.argv[1:])
generate.generate_sample_configuration(
args.destination_filename, validate.schema_filename()
)
print('Generated a sample configuration file at {}.'.format(args.destination_filename))
print()
print('Please edit the file to suit your needs. The values are representative.')
print('All fields are optional except where indicated.')
print()
print('If you ever need help: https://torsion.org/borgmatic/#issues')
except (ValueError, OSError) as error:
print(error, file=sys.stderr)
sys.exit(1)
sys.argv = ['borgmatic', 'config', 'generate'] + sys.argv[1:]
borgmatic.commands.borgmatic.main([warning_log])

View File

@ -1,56 +1,17 @@
import logging
import sys
from argparse import ArgumentParser
from borgmatic.config import collect, validate
logger = logging.getLogger(__name__)
import borgmatic.commands.borgmatic
def parse_arguments(*arguments):
'''
Given command-line arguments with which this script was invoked, parse the arguments and return
them as an ArgumentParser instance.
'''
config_paths = collect.get_default_config_paths()
parser = ArgumentParser(description='Validate borgmatic configuration file(s).')
parser.add_argument(
'-c',
'--config',
nargs='+',
dest='config_paths',
default=config_paths,
help='Configuration filenames or directories, defaults to: {}'.format(
' '.join(config_paths)
),
def main():
warning_log = logging.makeLogRecord(
dict(
levelno=logging.WARNING,
levelname='WARNING',
msg='validate-borgmatic-config is deprecated and will be removed from a future release. Please use "borgmatic config validate" instead.',
)
)
return parser.parse_args(arguments)
def main(): # pragma: no cover
args = parse_arguments(*sys.argv[1:])
logging.basicConfig(level=logging.INFO, format='%(message)s')
config_filenames = tuple(collect.collect_config_filenames(args.config_paths))
if len(config_filenames) == 0:
logger.critical('No files to validate found')
sys.exit(1)
found_issues = False
for config_filename in config_filenames:
try:
validate.parse_configuration(config_filename, validate.schema_filename())
except (ValueError, OSError, validate.Validation_error) as error:
logging.critical('{}: Error parsing configuration file'.format(config_filename))
logging.critical(error)
found_issues = True
if found_issues:
sys.exit(1)
else:
logger.info(
'All given configuration files are valid: {}'.format(', '.join(config_filenames))
)
sys.argv = ['borgmatic', 'config', 'validate'] + sys.argv[1:]
borgmatic.commands.borgmatic.main([warning_log])

View File

@ -1,9 +1,9 @@
def repository_enabled_for_checks(repository, consistency):
def repository_enabled_for_checks(repository, config):
'''
Given a repository name and a consistency configuration dict, return whether the repository
is enabled to have consistency checks run.
Given a repository name and a configuration dict, return whether the
repository is enabled to have consistency checks run.
'''
if not consistency.get('check_repositories'):
if not config.get('check_repositories'):
return True
return repository in consistency['check_repositories']
return repository in config['check_repositories']

View File

@ -1,29 +1,32 @@
import os
def get_default_config_paths():
def get_default_config_paths(expand_home=True):
'''
Based on the value of the XDG_CONFIG_HOME and HOME environment variables, return a list of
default configuration paths. This includes both system-wide configuration and configuration in
the current user's home directory.
Don't expand the home directory ($HOME) if the expand home flag is False.
'''
user_config_directory = os.getenv('XDG_CONFIG_HOME') or os.path.expandvars(
os.path.join('$HOME', '.config')
)
user_config_directory = os.getenv('XDG_CONFIG_HOME') or os.path.join('$HOME', '.config')
if expand_home:
user_config_directory = os.path.expandvars(user_config_directory)
return [
'/etc/borgmatic/config.yaml',
'/etc/borgmatic.d',
'%s/borgmatic/config.yaml' % user_config_directory,
os.path.join(user_config_directory, 'borgmatic/config.yaml'),
os.path.join(user_config_directory, 'borgmatic.d'),
]
def collect_config_filenames(config_paths):
'''
Given a sequence of config paths, both filenames and directories, resolve that to an iterable
of files. Accomplish this by listing any given directories looking for contained config files
(ending with the ".yaml" or ".yml" extension). This is non-recursive, so any directories within the given
directories are ignored.
of absolute files. Accomplish this by listing any given directories looking for contained config
files (ending with the ".yaml" or ".yml" extension). This is non-recursive, so any directories
within the given directories are ignored.
Return paths even if they don't exist on disk, so the user can find out about missing
configuration paths. However, skip a default config path if it's missing, so the user doesn't
@ -38,11 +41,14 @@ def collect_config_filenames(config_paths):
continue
if not os.path.isdir(path) or not exists:
yield path
yield os.path.abspath(path)
continue
if not os.access(path, os.R_OK):
continue
for filename in sorted(os.listdir(path)):
full_filename = os.path.join(path, filename)
matching_filetype = full_filename.endswith('.yaml') or full_filename.endswith('.yml')
if matching_filetype and not os.path.isdir(full_filename):
yield full_filename
yield os.path.abspath(full_filename)

View File

@ -0,0 +1,61 @@
import shlex
def coerce_scalar(value):
'''
Given a configuration value, coerce it to an integer or a boolean as appropriate and return the
result.
'''
try:
return int(value)
except (TypeError, ValueError):
pass
if value == 'true' or value == 'True':
return True
if value == 'false' or value == 'False':
return False
return value
def apply_constants(value, constants, shell_escape=False):
'''
Given a configuration value (bool, dict, int, list, or string) and a dict of named constants,
replace any configuration string values of the form "{constant}" (or containing it) with the
value of the correspondingly named key from the constants. Recurse as necessary into nested
configuration to find values to replace.
For instance, if a configuration value contains "{foo}", replace it with the value of the "foo"
key found within the configuration's "constants".
If shell escape is True, then escape the constant's value before applying it.
Return the configuration value and modify the original.
'''
if not value or not constants:
return value
if isinstance(value, str):
for constant_name, constant_value in constants.items():
value = value.replace(
'{' + constant_name + '}',
shlex.quote(str(constant_value)) if shell_escape else str(constant_value),
)
# Support constants within non-string scalars by coercing the value to its appropriate type.
value = coerce_scalar(value)
elif isinstance(value, list):
for index, list_value in enumerate(value):
value[index] = apply_constants(list_value, constants, shell_escape)
elif isinstance(value, dict):
for option_name, option_value in value.items():
shell_escape = (
shell_escape
or option_name.startswith('before_')
or option_name.startswith('after_')
or option_name == 'on_error'
)
value[option_name] = apply_constants(option_value, constants, shell_escape)
return value

View File

@ -1,95 +0,0 @@
import os
from ruamel import yaml
from borgmatic.config import generate
def _convert_section(source_section_config, section_schema):
'''
Given a legacy Parsed_config instance for a single section, convert it to its corresponding
yaml.comments.CommentedMap representation in preparation for actual serialization to YAML.
Where integer types exist in the given section schema, convert their values to integers.
'''
destination_section_config = yaml.comments.CommentedMap(
[
(
option_name,
int(option_value)
if section_schema['map'].get(option_name, {}).get('type') == 'int'
else option_value,
)
for option_name, option_value in source_section_config.items()
]
)
return destination_section_config
def convert_legacy_parsed_config(source_config, source_excludes, schema):
'''
Given a legacy Parsed_config instance loaded from an INI-style config file and a list of exclude
patterns, convert them to a corresponding yaml.comments.CommentedMap representation in
preparation for serialization to a single YAML config file.
Additionally, use the given schema as a source of helpful comments to include within the
returned CommentedMap.
'''
destination_config = yaml.comments.CommentedMap(
[
(section_name, _convert_section(section_config, schema['map'][section_name]))
for section_name, section_config in source_config._asdict().items()
]
)
# Split space-seperated values into actual lists, make "repository" into a list, and merge in
# excludes.
location = destination_config['location']
location['source_directories'] = source_config.location['source_directories'].split(' ')
location['repositories'] = [location.pop('repository')]
location['exclude_patterns'] = source_excludes
if source_config.consistency.get('checks'):
destination_config['consistency']['checks'] = source_config.consistency['checks'].split(' ')
# Add comments to each section, and then add comments to the fields in each section.
generate.add_comments_to_configuration_map(destination_config, schema)
for section_name, section_config in destination_config.items():
generate.add_comments_to_configuration_map(
section_config, schema['map'][section_name], indent=generate.INDENT
)
return destination_config
class Legacy_configuration_not_upgraded(FileNotFoundError):
def __init__(self):
super(Legacy_configuration_not_upgraded, self).__init__(
'''borgmatic changed its configuration file format in version 1.1.0 from INI-style
to YAML. This better supports validation, and has a more natural way to express
lists of values. To upgrade your existing configuration, run:
sudo upgrade-borgmatic-config
That will generate a new YAML configuration file at /etc/borgmatic/config.yaml
(by default) using the values from both your existing configuration and excludes
files. The new version of borgmatic will consume the YAML configuration file
instead of the old one.'''
)
def guard_configuration_upgraded(source_config_filename, destination_config_filenames):
'''
If legacy source configuration exists but no destination upgraded configs do, raise
Legacy_configuration_not_upgraded.
The idea is that we want to alert the user about upgrading their config if they haven't already.
'''
destination_config_exists = any(
os.path.exists(filename) for filename in destination_config_filenames
)
if os.path.exists(source_config_filename) and not destination_config_exists:
raise Legacy_configuration_not_upgraded()

View File

@ -0,0 +1,51 @@
import os
import re
VARIABLE_PATTERN = re.compile(
r'(?P<escape>\\)?(?P<variable>\$\{(?P<name>[A-Za-z0-9_]+)((:?-)(?P<default>[^}]+))?\})'
)
def resolve_string(matcher):
'''
Given a matcher containing a name and an optional default value, get the value from environment.
Raise ValueError if the variable is not defined in environment and no default value is provided.
'''
if matcher.group('escape') is not None:
# In the case of an escaped environment variable, unescape it.
return matcher.group('variable')
# Resolve the environment variable.
name, default = matcher.group('name'), matcher.group('default')
out = os.getenv(name, default=default)
if out is None:
raise ValueError(f'Cannot find variable {name} in environment')
return out
def resolve_env_variables(item):
'''
Resolves variables like or ${FOO} from given configuration with values from process environment.
Supported formats:
* ${FOO} will return FOO env variable
* ${FOO-bar} or ${FOO:-bar} will return FOO env variable if it exists, else "bar"
Raise if any variable is missing in environment and no default value is provided.
'''
if isinstance(item, str):
return VARIABLE_PATTERN.sub(resolve_string, item)
if isinstance(item, list):
for index, subitem in enumerate(item):
item[index] = resolve_env_variables(subitem)
if isinstance(item, dict):
for key, value in item.items():
item[key] = resolve_env_variables(value)
return item

View File

@ -1,66 +1,78 @@
import collections
import io
import os
import re
from ruamel import yaml
import ruamel.yaml
from borgmatic.config import load, normalize
INDENT = 4
SEQUENCE_INDENT = 2
def _insert_newline_before_comment(config, field_name):
def insert_newline_before_comment(config, field_name):
'''
Using some ruamel.yaml black magic, insert a blank line in the config right before the given
field and its comments.
'''
config.ca.items[field_name][1].insert(
0, yaml.tokens.CommentToken('\n', yaml.error.CommentMark(0), None)
0, ruamel.yaml.tokens.CommentToken('\n', ruamel.yaml.error.CommentMark(0), None)
)
def _schema_to_sample_configuration(schema, level=0, parent_is_sequence=False):
def get_properties(schema):
'''
Given a schema dict, return its properties. But if it's got sub-schemas with multiple different
potential properties, returned their merged properties instead.
'''
if 'oneOf' in schema:
return dict(
collections.ChainMap(*[sub_schema['properties'] for sub_schema in schema['oneOf']])
)
return schema['properties']
def schema_to_sample_configuration(schema, level=0, parent_is_sequence=False):
'''
Given a loaded configuration schema, generate and return sample config for it. Include comments
for each section based on the schema "desc" description.
for each option based on the schema "description".
'''
schema_type = schema.get('type')
example = schema.get('example')
if example is not None:
return example
if 'seq' in schema:
config = yaml.comments.CommentedSeq(
[
_schema_to_sample_configuration(item_schema, level, parent_is_sequence=True)
for item_schema in schema['seq']
]
if schema_type == 'array':
config = ruamel.yaml.comments.CommentedSeq(
[schema_to_sample_configuration(schema['items'], level, parent_is_sequence=True)]
)
add_comments_to_configuration_sequence(
config, schema, indent=(level * INDENT) + SEQUENCE_INDENT
)
elif 'map' in schema:
config = yaml.comments.CommentedMap(
add_comments_to_configuration_sequence(config, schema, indent=(level * INDENT))
elif schema_type == 'object':
config = ruamel.yaml.comments.CommentedMap(
[
(section_name, _schema_to_sample_configuration(section_schema, level + 1))
for section_name, section_schema in schema['map'].items()
(field_name, schema_to_sample_configuration(sub_schema, level + 1))
for field_name, sub_schema in get_properties(schema).items()
]
)
indent = (level * INDENT) + (SEQUENCE_INDENT if parent_is_sequence else 0)
add_comments_to_configuration_map(
add_comments_to_configuration_object(
config, schema, indent=indent, skip_first=parent_is_sequence
)
else:
raise ValueError('Schema at level {} is unsupported: {}'.format(level, schema))
raise ValueError(f'Schema at level {level} is unsupported: {schema}')
return config
def _comment_out_line(line):
def comment_out_line(line):
# If it's already is commented out (or empty), there's nothing further to do!
stripped_line = line.lstrip()
if not stripped_line or stripped_line.startswith('#'):
return line
# Comment out the names of optional sections, inserting the '#' after any indent for aesthetics.
# Comment out the names of optional options, inserting the '#' after any indent for aesthetics.
matches = re.match(r'(\s*)', line)
indent_spaces = matches.group(0) if matches else ''
count_indent_spaces = len(indent_spaces)
@ -68,46 +80,41 @@ def _comment_out_line(line):
return '# '.join((indent_spaces, line[count_indent_spaces:]))
REQUIRED_KEYS = {'source_directories', 'repositories', 'keep_daily'}
REQUIRED_SECTION_NAMES = {'location', 'retention'}
def _comment_out_optional_configuration(rendered_config):
def comment_out_optional_configuration(rendered_config):
'''
Post-process a rendered configuration string to comment out optional key/values. The idea is
that this prevents the user from having to comment out a bunch of configuration they don't care
about to get to a minimal viable configuration file.
Post-process a rendered configuration string to comment out optional key/values, as determined
by a sentinel in the comment before each key.
Ideally ruamel.yaml would support this during configuration generation, but it's not terribly
easy to accomplish that way.
The idea is that the pre-commented configuration prevents the user from having to comment out a
bunch of configuration they don't care about to get to a minimal viable configuration file.
Ideally ruamel.yaml would support commenting out keys during configuration generation, but it's
not terribly easy to accomplish that way.
'''
lines = []
required = False
optional = False
for line in rendered_config.split('\n'):
key = line.strip().split(':')[0]
if key in REQUIRED_SECTION_NAMES:
lines.append(line)
# Upon encountering an optional configuration option, comment out lines until the next blank
# line.
if line.strip().startswith(f'# {COMMENTED_OUT_SENTINEL}'):
optional = True
continue
# Upon encountering a required configuration option, skip commenting out lines until the
# next blank line.
if key in REQUIRED_KEYS:
required = True
elif not key:
required = False
# Hit a blank line, so reset commenting.
if not line.strip():
optional = False
lines.append(_comment_out_line(line) if not required else line)
lines.append(comment_out_line(line) if optional else line)
return '\n'.join(lines)
def _render_configuration(config):
def render_configuration(config):
'''
Given a config data structure of nested OrderedDicts, render the config as YAML and return it.
'''
dumper = yaml.YAML()
dumper = ruamel.yaml.YAML(typ='rt')
dumper.indent(mapping=INDENT, sequence=INDENT + SEQUENCE_INDENT, offset=INDENT)
rendered = io.StringIO()
dumper.dump(config, rendered)
@ -115,13 +122,16 @@ def _render_configuration(config):
return rendered.getvalue()
def write_configuration(config_filename, rendered_config, mode=0o600):
def write_configuration(config_filename, rendered_config, mode=0o600, overwrite=False):
'''
Given a target config filename and rendered config YAML, write it out to file. Create any
containing directories as needed.
containing directories as needed. But if the file already exists and overwrite is False,
abort before writing anything.
'''
if os.path.exists(config_filename):
raise FileExistsError('{} already exists. Aborting.'.format(config_filename))
if not overwrite and os.path.exists(config_filename):
raise FileExistsError(
f'{config_filename} already exists. Aborting. Use --overwrite to replace the file.'
)
try:
os.makedirs(os.path.dirname(config_filename), mode=0o700)
@ -136,26 +146,26 @@ def write_configuration(config_filename, rendered_config, mode=0o600):
def add_comments_to_configuration_sequence(config, schema, indent=0):
'''
If the given config sequence's items are maps, then mine the schema for the description of the
map's first item, and slap that atop the sequence. Indent the comment the given number of
If the given config sequence's items are object, then mine the schema for the description of the
object's first item, and slap that atop the sequence. Indent the comment the given number of
characters.
Doing this for sequences of maps results in nice comments that look like:
```
things:
# First key description. Added by this function.
# First key description. Added by this function.
- key: foo
# Second key description. Added by add_comments_to_configuration_map().
# Second key description. Added by add_comments_to_configuration_object().
other: bar
```
'''
if 'map' not in schema['seq'][0]:
if schema['items'].get('type') != 'object':
return
for field_name in config[0].keys():
field_schema = schema['seq'][0]['map'].get(field_name, {})
description = field_schema.get('desc')
field_schema = get_properties(schema['items']).get(field_name, {})
description = field_schema.get('description')
# No description to use? Skip it.
if not field_schema or not description:
@ -164,11 +174,15 @@ def add_comments_to_configuration_sequence(config, schema, indent=0):
config[0].yaml_set_start_comment(description, indent=indent)
# We only want the first key's description here, as the rest of the keys get commented by
# add_comments_to_configuration_map().
# add_comments_to_configuration_object().
return
def add_comments_to_configuration_map(config, schema, indent=0, skip_first=False):
REQUIRED_KEYS = {'source_directories', 'repositories', 'keep_daily'}
COMMENTED_OUT_SENTINEL = 'COMMENT_OUT'
def add_comments_to_configuration_object(config, schema, indent=0, skip_first=False):
'''
Using descriptions from a schema as a source, add those descriptions as comments to the given
config mapping, before each field. Indent the comment the given number of characters.
@ -177,27 +191,120 @@ def add_comments_to_configuration_map(config, schema, indent=0, skip_first=False
if skip_first and index == 0:
continue
field_schema = schema['map'].get(field_name, {})
description = field_schema.get('desc')
field_schema = get_properties(schema).get(field_name, {})
description = field_schema.get('description', '').strip()
# If this is an optional key, add an indicator to the comment flagging it to be commented
# out from the sample configuration. This sentinel is consumed by downstream processing that
# does the actual commenting out.
if field_name not in REQUIRED_KEYS:
description = (
'\n'.join((description, COMMENTED_OUT_SENTINEL))
if description
else COMMENTED_OUT_SENTINEL
)
# No description to use? Skip it.
if not field_schema or not description:
if not field_schema or not description: # pragma: no cover
continue
config.yaml_set_comment_before_after_key(key=field_name, before=description, indent=indent)
if index > 0:
_insert_newline_before_comment(config, field_name)
insert_newline_before_comment(config, field_name)
def generate_sample_configuration(config_filename, schema_filename):
RUAMEL_YAML_COMMENTS_INDEX = 1
def remove_commented_out_sentinel(config, field_name):
'''
Given a target config filename and the path to a schema filename in pykwalify YAML schema
format, write out a sample configuration file based on that schema.
Given a configuration CommentedMap and a top-level field name in it, remove any "commented out"
sentinel found at the end of its YAML comments. This prevents the given field name from getting
commented out by downstream processing that consumes the sentinel.
'''
schema = yaml.round_trip_load(open(schema_filename))
config = _schema_to_sample_configuration(schema)
try:
last_comment_value = config.ca.items[field_name][RUAMEL_YAML_COMMENTS_INDEX][-1].value
except KeyError:
return
if last_comment_value == f'# {COMMENTED_OUT_SENTINEL}\n':
config.ca.items[field_name][RUAMEL_YAML_COMMENTS_INDEX].pop()
def merge_source_configuration_into_destination(destination_config, source_config):
'''
Deep merge the given source configuration dict into the destination configuration CommentedMap,
favoring values from the source when there are collisions.
The purpose of this is to upgrade configuration files from old versions of borgmatic by adding
new configuration keys and comments.
'''
if not source_config:
return destination_config
if not destination_config or not isinstance(source_config, collections.abc.Mapping):
return source_config
for field_name, source_value in source_config.items():
# Since this key/value is from the source configuration, leave it uncommented and remove any
# sentinel that would cause it to get commented out.
remove_commented_out_sentinel(
ruamel.yaml.comments.CommentedMap(destination_config), field_name
)
# This is a mapping. Recurse for this key/value.
if isinstance(source_value, collections.abc.Mapping):
destination_config[field_name] = merge_source_configuration_into_destination(
destination_config[field_name], source_value
)
continue
# This is a sequence. Recurse for each item in it.
if isinstance(source_value, collections.abc.Sequence) and not isinstance(source_value, str):
destination_value = destination_config[field_name]
destination_config[field_name] = ruamel.yaml.comments.CommentedSeq(
[
merge_source_configuration_into_destination(
destination_value[index] if index < len(destination_value) else None,
source_item,
)
for index, source_item in enumerate(source_value)
]
)
continue
# This is some sort of scalar. Set it into the destination.
destination_config[field_name] = source_config[field_name]
return destination_config
def generate_sample_configuration(
dry_run, source_filename, destination_filename, schema_filename, overwrite=False
):
'''
Given an optional source configuration filename, and a required destination configuration
filename, the path to a schema filename in a YAML rendition of the JSON Schema format, and
whether to overwrite a destination file, write out a sample configuration file based on that
schema. If a source filename is provided, merge the parsed contents of that configuration into
the generated configuration.
'''
schema = ruamel.yaml.YAML(typ='safe').load(open(schema_filename))
source_config = None
if source_filename:
source_config = load.load_configuration(source_filename)
normalize.normalize(source_filename, source_config)
destination_config = merge_source_configuration_into_destination(
schema_to_sample_configuration(schema), source_config
)
if dry_run:
return
write_configuration(
config_filename, _comment_out_optional_configuration(_render_configuration(config))
destination_filename,
comment_out_optional_configuration(render_configuration(destination_config)),
overwrite=overwrite,
)

View File

@ -1,152 +0,0 @@
from collections import OrderedDict, namedtuple
from configparser import RawConfigParser
Section_format = namedtuple('Section_format', ('name', 'options'))
Config_option = namedtuple('Config_option', ('name', 'value_type', 'required'))
def option(name, value_type=str, required=True):
'''
Given a config file option name, an expected type for its value, and whether it's required,
return a Config_option capturing that information.
'''
return Config_option(name, value_type, required)
CONFIG_FORMAT = (
Section_format(
'location',
(
option('source_directories'),
option('one_file_system', value_type=bool, required=False),
option('remote_path', required=False),
option('repository'),
),
),
Section_format(
'storage',
(
option('encryption_passphrase', required=False),
option('compression', required=False),
option('umask', required=False),
),
),
Section_format(
'retention',
(
option('keep_within', required=False),
option('keep_hourly', int, required=False),
option('keep_daily', int, required=False),
option('keep_weekly', int, required=False),
option('keep_monthly', int, required=False),
option('keep_yearly', int, required=False),
option('prefix', required=False),
),
),
Section_format(
'consistency', (option('checks', required=False), option('check_last', required=False))
),
)
def validate_configuration_format(parser, config_format):
'''
Given an open RawConfigParser and an expected config file format, validate that the parsed
configuration file has the expected sections, that any required options are present in those
sections, and that there aren't any unexpected options.
A section is required if any of its contained options are required.
Raise ValueError if anything is awry.
'''
section_names = set(parser.sections())
required_section_names = tuple(
section.name
for section in config_format
if any(option.required for option in section.options)
)
unknown_section_names = section_names - set(
section_format.name for section_format in config_format
)
if unknown_section_names:
raise ValueError(
'Unknown config sections found: {}'.format(', '.join(unknown_section_names))
)
missing_section_names = set(required_section_names) - section_names
if missing_section_names:
raise ValueError('Missing config sections: {}'.format(', '.join(missing_section_names)))
for section_format in config_format:
if section_format.name not in section_names:
continue
option_names = parser.options(section_format.name)
expected_options = section_format.options
unexpected_option_names = set(option_names) - set(
option.name for option in expected_options
)
if unexpected_option_names:
raise ValueError(
'Unexpected options found in config section {}: {}'.format(
section_format.name, ', '.join(sorted(unexpected_option_names))
)
)
missing_option_names = tuple(
option.name
for option in expected_options
if option.required
if option.name not in option_names
)
if missing_option_names:
raise ValueError(
'Required options missing from config section {}: {}'.format(
section_format.name, ', '.join(missing_option_names)
)
)
def parse_section_options(parser, section_format):
'''
Given an open RawConfigParser and an expected section format, return the option values from that
section as a dict mapping from option name to value. Omit those options that are not present in
the parsed options.
Raise ValueError if any option values cannot be coerced to the expected Python data type.
'''
type_getter = {str: parser.get, int: parser.getint, bool: parser.getboolean}
return OrderedDict(
(option.name, type_getter[option.value_type](section_format.name, option.name))
for option in section_format.options
if parser.has_option(section_format.name, option.name)
)
def parse_configuration(config_filename, config_format):
'''
Given a config filename and an expected config file format, return the parsed configuration
as a namedtuple with one attribute for each parsed section.
Raise IOError if the file cannot be read, or ValueError if the format is not as expected.
'''
parser = RawConfigParser()
if not parser.read(config_filename):
raise ValueError('Configuration file cannot be opened: {}'.format(config_filename))
validate_configuration_format(parser, config_format)
# Describes a parsed configuration, where each attribute is the name of a configuration file
# section and each value is a dict of that section's parsed options.
Parsed_config = namedtuple(
'Parsed_config', (section_format.name for section_format in config_format)
)
return Parsed_config(
*(parse_section_options(parser, section_format) for section_format in config_format)
)

View File

@ -1,4 +1,7 @@
import functools
import itertools
import logging
import operator
import os
import ruamel.yaml
@ -6,54 +9,376 @@ import ruamel.yaml
logger = logging.getLogger(__name__)
def load_configuration(filename):
def probe_and_include_file(filename, include_directories, config_paths):
'''
Load the given configuration file and return its contents as a data structure of nested dicts
and lists.
Given a filename to include, a list of include directories to search for matching files, and a
set of configuration paths, probe for the file, load it, and return the loaded configuration as
a data structure of nested dicts, lists, etc. Add the filename to the given configuration paths.
Raise ruamel.yaml.error.YAMLError if something goes wrong parsing the YAML, or RecursionError
if there are too many recursive includes.
Raise FileNotFoundError if the included file was not found.
'''
yaml = ruamel.yaml.YAML(typ='safe')
yaml.Constructor = Include_constructor
expanded_filename = os.path.expanduser(filename)
return yaml.load(open(filename))
if os.path.isabs(expanded_filename):
return load_configuration(expanded_filename, config_paths)
candidate_filenames = {
os.path.join(directory, expanded_filename) for directory in include_directories
}
for candidate_filename in candidate_filenames:
if os.path.exists(candidate_filename):
return load_configuration(candidate_filename, config_paths)
raise FileNotFoundError(
f'Could not find include {filename} at {" or ".join(candidate_filenames)}'
)
def include_configuration(loader, filename_node):
def include_configuration(loader, filename_node, include_directory, config_paths):
'''
Load the given YAML filename (ignoring the given loader so we can use our own), and return its
contents as a data structure of nested dicts and lists.
Given a ruamel.yaml.loader.Loader, a ruamel.yaml.nodes.ScalarNode containing the included
filename (or a list containing multiple such filenames), an include directory path to search for
matching files, and a set of configuration paths, load the given YAML filenames (ignoring the
given loader so we can use our own) and return their contents as data structure of nested dicts,
lists, etc. Add the names of included files to the given configuration paths. If the given
filename node's value is a scalar string, then the return value will be a single value. But if
the given node value is a list, then the return value will be a list of values, one per loaded
configuration file.
If a filename is relative, probe for it within: 1. the current working directory and 2. the
given include directory.
Raise FileNotFoundError if an included file was not found.
'''
return load_configuration(os.path.expanduser(filename_node.value))
include_directories = [os.getcwd(), os.path.abspath(include_directory)]
if isinstance(filename_node.value, str):
return probe_and_include_file(filename_node.value, include_directories, config_paths)
if (
isinstance(filename_node.value, list)
and len(filename_node.value)
and isinstance(filename_node.value[0], ruamel.yaml.nodes.ScalarNode)
):
# Reversing the values ensures the correct ordering if these includes are subsequently
# merged together.
return [
probe_and_include_file(node.value, include_directories, config_paths)
for node in reversed(filename_node.value)
]
raise ValueError(
'!include value is not supported; use a single filename or a list of filenames'
)
def raise_retain_node_error(loader, node):
'''
Given a ruamel.yaml.loader.Loader and a YAML node, raise an error about "!retain" usage.
Raise ValueError if a mapping or sequence node is given, as that indicates that "!retain" was
used in a configuration file without a merge. In configuration files with a merge, mapping and
sequence nodes with "!retain" tags are handled by deep_merge_nodes() below.
Also raise ValueError if a scalar node is given, as "!retain" is not supported on scalar nodes.
'''
if isinstance(node, (ruamel.yaml.nodes.MappingNode, ruamel.yaml.nodes.SequenceNode)):
raise ValueError(
'The !retain tag may only be used within a configuration file containing a merged !include tag.'
)
raise ValueError('The !retain tag may only be used on a mapping or list.')
def raise_omit_node_error(loader, node):
'''
Given a ruamel.yaml.loader.Loader and a YAML node, raise an error about "!omit" usage.
Raise ValueError unconditionally, as an "!omit" node here indicates it was used in a
configuration file without a merge. In configuration files with a merge, nodes with "!omit"
tags are handled by deep_merge_nodes() below.
'''
raise ValueError(
'The !omit tag may only be used on a scalar (e.g., string) or list element within a configuration file containing a merged !include tag.'
)
class Include_constructor(ruamel.yaml.SafeConstructor):
'''
A YAML "constructor" (a ruamel.yaml concept) that supports a custom "!include" tag for including
separate YAML configuration files. Example syntax: `retention: !include common.yaml`
separate YAML configuration files. Example syntax: `option: !include common.yaml`
'''
def __init__(self, preserve_quotes=None, loader=None):
def __init__(
self, preserve_quotes=None, loader=None, include_directory=None, config_paths=None
):
super(Include_constructor, self).__init__(preserve_quotes, loader)
self.add_constructor('!include', include_configuration)
self.add_constructor(
'!include',
functools.partial(
include_configuration,
include_directory=include_directory,
config_paths=config_paths,
),
)
# These are catch-all error handlers for tags that don't get applied and removed by
# deep_merge_nodes() below.
self.add_constructor('!retain', raise_retain_node_error)
self.add_constructor('!omit', raise_omit_node_error)
def flatten_mapping(self, node):
'''
Support the special case of shallow merging included configuration into an existing mapping
Support the special case of deep merging included configuration into an existing mapping
using the YAML '<<' merge key. Example syntax:
```
retention:
keep_daily: 1
<<: !include common.yaml
option:
sub_option: 1
<<: !include common.yaml
```
These includes are deep merged into the current configuration file. For instance, in this
example, any "option" with sub-options in common.yaml will get merged into the corresponding
"option" with sub-options in the example configuration file.
'''
representer = ruamel.yaml.representer.SafeRepresenter()
for index, (key_node, value_node) in enumerate(node.value):
if key_node.tag == u'tag:yaml.org,2002:merge' and value_node.tag == '!include':
included_value = representer.represent_data(self.construct_object(value_node))
node.value[index] = (key_node, included_value)
# Replace the merge include with a sequence of included configuration nodes ready
# for merging. The construct_object() call here triggers include_configuration()
# among other constructors.
node.value[index] = (
key_node,
representer.represent_data(self.construct_object(value_node)),
)
# This super().flatten_mapping() call actually performs "<<" merges.
super(Include_constructor, self).flatten_mapping(node)
node.value = deep_merge_nodes(node.value)
def load_configuration(filename, config_paths=None):
'''
Load the given configuration file and return its contents as a data structure of nested dicts
and lists. Add the filename to the given configuration paths set, and also add any included
configuration filenames.
Raise ruamel.yaml.error.YAMLError if something goes wrong parsing the YAML, or RecursionError
if there are too many recursive includes.
'''
if config_paths is None:
config_paths = set()
# Use an embedded derived class for the include constructor so as to capture the include
# directory and configuration paths values. (functools.partial doesn't work for this use case
# because yaml.Constructor has to be an actual class.)
class Include_constructor_with_extras(Include_constructor):
def __init__(self, preserve_quotes=None, loader=None):
super(Include_constructor_with_extras, self).__init__(
preserve_quotes,
loader,
include_directory=os.path.dirname(filename),
config_paths=config_paths,
)
yaml = ruamel.yaml.YAML(typ='safe')
yaml.Constructor = Include_constructor_with_extras
config_paths.add(filename)
with open(filename) as file:
return yaml.load(file.read())
def filter_omitted_nodes(nodes, values):
'''
Given a nested borgmatic configuration data structure as a list of tuples in the form of:
[
(
ruamel.yaml.nodes.ScalarNode as a key,
ruamel.yaml.nodes.MappingNode or other Node as a value,
),
...
]
... and a combined list of all values for those nodes, return a filtered list of the values,
omitting any that have an "!omit" tag (or with a value matching such nodes).
But if only a single node is given, bail and return the given values unfiltered, as "!omit" only
applies when there are merge includes (and therefore multiple nodes).
'''
if len(nodes) <= 1:
return values
omitted_values = tuple(node.value for node in values if node.tag == '!omit')
return [node for node in values if node.value not in omitted_values]
def merge_values(nodes):
'''
Given a nested borgmatic configuration data structure as a list of tuples in the form of:
[
(
ruamel.yaml.nodes.ScalarNode as a key,
ruamel.yaml.nodes.MappingNode or other Node as a value,
),
...
]
... merge its sequence or mapping node values and return the result. For sequence nodes, this
means appending together its contained lists. For mapping nodes, it means merging its contained
dicts.
'''
return functools.reduce(operator.add, (value.value for key, value in nodes))
def deep_merge_nodes(nodes):
'''
Given a nested borgmatic configuration data structure as a list of tuples in the form of:
[
(
ruamel.yaml.nodes.ScalarNode as a key,
ruamel.yaml.nodes.MappingNode or other Node as a value,
),
...
]
... deep merge any node values corresponding to duplicate keys and return the result. The
purpose of merging like this is to support, for instance, merging one borgmatic configuration
file into another for reuse, such that a configuration option with sub-options does not
completely replace the corresponding option in a merged file.
If there are colliding keys with scalar values (e.g., integers or strings), the last of the
values wins.
For instance, given node values of:
[
(
ScalarNode(tag='tag:yaml.org,2002:str', value='option'),
MappingNode(tag='tag:yaml.org,2002:map', value=[
(
ScalarNode(tag='tag:yaml.org,2002:str', value='sub_option1'),
ScalarNode(tag='tag:yaml.org,2002:int', value='1')
),
(
ScalarNode(tag='tag:yaml.org,2002:str', value='sub_option2'),
ScalarNode(tag='tag:yaml.org,2002:int', value='2')
),
]),
),
(
ScalarNode(tag='tag:yaml.org,2002:str', value='option'),
MappingNode(tag='tag:yaml.org,2002:map', value=[
(
ScalarNode(tag='tag:yaml.org,2002:str', value='sub_option2'),
ScalarNode(tag='tag:yaml.org,2002:int', value='5')
),
]),
),
]
... the returned result would be:
[
(
ScalarNode(tag='tag:yaml.org,2002:str', value='option'),
MappingNode(tag='tag:yaml.org,2002:map', value=[
(
ScalarNode(tag='tag:yaml.org,2002:str', value='sub_option1'),
ScalarNode(tag='tag:yaml.org,2002:int', value='1')
),
(
ScalarNode(tag='tag:yaml.org,2002:str', value='sub_option2'),
ScalarNode(tag='tag:yaml.org,2002:int', value='5')
),
]),
),
]
This function supports multi-way merging, meaning that if the same option name exists three or
more times (at the same scope level), all of those instances get merged together.
If a mapping or sequence node has a YAML "!retain" tag, then that node is not merged.
Raise ValueError if a merge is implied using multiple incompatible types.
'''
merged_nodes = []
def get_node_key_name(node):
return node[0].value
# Bucket the nodes by their keys. Then merge all of the values sharing the same key.
for key_name, grouped_nodes in itertools.groupby(
sorted(nodes, key=get_node_key_name), get_node_key_name
):
grouped_nodes = list(grouped_nodes)
# The merged node inherits its attributes from the final node in the group.
(last_node_key, last_node_value) = grouped_nodes[-1]
value_types = set(type(value) for (_, value) in grouped_nodes)
if len(value_types) > 1:
raise ValueError(
f'Incompatible types found when trying to merge "{key_name}:" values across configuration files: {", ".join(value_type.id for value_type in value_types)}'
)
# If we're dealing with MappingNodes, recurse and merge its values as well.
if ruamel.yaml.nodes.MappingNode in value_types:
# A "!retain" tag says to skip deep merging for this node. Replace the tag so
# downstream schema validation doesn't break on our application-specific tag.
if last_node_value.tag == '!retain' and len(grouped_nodes) > 1:
last_node_value.tag = 'tag:yaml.org,2002:map'
merged_nodes.append((last_node_key, last_node_value))
else:
merged_nodes.append(
(
last_node_key,
ruamel.yaml.nodes.MappingNode(
tag=last_node_value.tag,
value=deep_merge_nodes(merge_values(grouped_nodes)),
start_mark=last_node_value.start_mark,
end_mark=last_node_value.end_mark,
flow_style=last_node_value.flow_style,
comment=last_node_value.comment,
anchor=last_node_value.anchor,
),
)
)
continue
# If we're dealing with SequenceNodes, merge by appending sequences together.
if ruamel.yaml.nodes.SequenceNode in value_types:
if last_node_value.tag == '!retain' and len(grouped_nodes) > 1:
last_node_value.tag = 'tag:yaml.org,2002:seq'
merged_nodes.append((last_node_key, last_node_value))
else:
merged_nodes.append(
(
last_node_key,
ruamel.yaml.nodes.SequenceNode(
tag=last_node_value.tag,
value=filter_omitted_nodes(grouped_nodes, merge_values(grouped_nodes)),
start_mark=last_node_value.start_mark,
end_mark=last_node_value.end_mark,
flow_style=last_node_value.flow_style,
comment=last_node_value.comment,
anchor=last_node_value.anchor,
),
)
)
continue
merged_nodes.append((last_node_key, last_node_value))
return merged_nodes

View File

@ -0,0 +1,269 @@
import logging
import os
def normalize_sections(config_filename, config):
'''
Given a configuration filename and a configuration dict of its loaded contents, airlift any
options out of sections ("location:", etc.) to the global scope and delete those sections.
Return any log message warnings produced based on the normalization performed.
Raise ValueError if the "prefix" option is set in both "location" and "consistency" sections.
'''
try:
location = config.get('location') or {}
except AttributeError:
raise ValueError('Configuration does not contain any options')
storage = config.get('storage') or {}
consistency = config.get('consistency') or {}
hooks = config.get('hooks') or {}
if (
location.get('prefix')
and consistency.get('prefix')
and location.get('prefix') != consistency.get('prefix')
):
raise ValueError(
'The retention prefix and the consistency prefix cannot have different values (unless one is not set).'
)
if storage.get('umask') and hooks.get('umask') and storage.get('umask') != hooks.get('umask'):
raise ValueError(
'The storage umask and the hooks umask cannot have different values (unless one is not set).'
)
any_section_upgraded = False
# Move any options from deprecated sections into the global scope.
for section_name in ('location', 'storage', 'retention', 'consistency', 'output', 'hooks'):
section_config = config.get(section_name)
if section_config is not None:
any_section_upgraded = True
del config[section_name]
config.update(section_config)
if any_section_upgraded:
return [
logging.makeLogRecord(
dict(
levelno=logging.WARNING,
levelname='WARNING',
msg=f'{config_filename}: Configuration sections (like location:, storage:, retention:, consistency:, and hooks:) are deprecated and support will be removed from a future release. To prepare for this, move your options out of sections to the global scope.',
)
)
]
return []
def normalize(config_filename, config):
'''
Given a configuration filename and a configuration dict of its loaded contents, apply particular
hard-coded rules to normalize the configuration to adhere to the current schema. Return any log
message warnings produced based on the normalization performed.
Raise ValueError the configuration cannot be normalized.
'''
logs = normalize_sections(config_filename, config)
# Upgrade exclude_if_present from a string to a list.
exclude_if_present = config.get('exclude_if_present')
if isinstance(exclude_if_present, str):
logs.append(
logging.makeLogRecord(
dict(
levelno=logging.WARNING,
levelname='WARNING',
msg=f'{config_filename}: The exclude_if_present option now expects a list value. String values for this option are deprecated and support will be removed from a future release.',
)
)
)
config['exclude_if_present'] = [exclude_if_present]
# Upgrade various monitoring hooks from a string to a dict.
healthchecks = config.get('healthchecks')
if isinstance(healthchecks, str):
logs.append(
logging.makeLogRecord(
dict(
levelno=logging.WARNING,
levelname='WARNING',
msg=f'{config_filename}: The healthchecks hook now expects a key/value pair with "ping_url" as a key. String values for this option are deprecated and support will be removed from a future release.',
)
)
)
config['healthchecks'] = {'ping_url': healthchecks}
cronitor = config.get('cronitor')
if isinstance(cronitor, str):
logs.append(
logging.makeLogRecord(
dict(
levelno=logging.WARNING,
levelname='WARNING',
msg=f'{config_filename}: The healthchecks hook now expects key/value pairs. String values for this option are deprecated and support will be removed from a future release.',
)
)
)
config['cronitor'] = {'ping_url': cronitor}
pagerduty = config.get('pagerduty')
if isinstance(pagerduty, str):
logs.append(
logging.makeLogRecord(
dict(
levelno=logging.WARNING,
levelname='WARNING',
msg=f'{config_filename}: The healthchecks hook now expects key/value pairs. String values for this option are deprecated and support will be removed from a future release.',
)
)
)
config['pagerduty'] = {'integration_key': pagerduty}
cronhub = config.get('cronhub')
if isinstance(cronhub, str):
logs.append(
logging.makeLogRecord(
dict(
levelno=logging.WARNING,
levelname='WARNING',
msg=f'{config_filename}: The healthchecks hook now expects key/value pairs. String values for this option are deprecated and support will be removed from a future release.',
)
)
)
config['cronhub'] = {'ping_url': cronhub}
# Upgrade consistency checks from a list of strings to a list of dicts.
checks = config.get('checks')
if isinstance(checks, list) and len(checks) and isinstance(checks[0], str):
logs.append(
logging.makeLogRecord(
dict(
levelno=logging.WARNING,
levelname='WARNING',
msg=f'{config_filename}: The checks option now expects a list of key/value pairs. Lists of strings for this option are deprecated and support will be removed from a future release.',
)
)
)
config['checks'] = [{'name': check_type} for check_type in checks]
# Rename various configuration options.
numeric_owner = config.pop('numeric_owner', None)
if numeric_owner is not None:
logs.append(
logging.makeLogRecord(
dict(
levelno=logging.WARNING,
levelname='WARNING',
msg=f'{config_filename}: The numeric_owner option has been renamed to numeric_ids. numeric_owner is deprecated and support will be removed from a future release.',
)
)
)
config['numeric_ids'] = numeric_owner
bsd_flags = config.pop('bsd_flags', None)
if bsd_flags is not None:
logs.append(
logging.makeLogRecord(
dict(
levelno=logging.WARNING,
levelname='WARNING',
msg=f'{config_filename}: The bsd_flags option has been renamed to flags. bsd_flags is deprecated and support will be removed from a future release.',
)
)
)
config['flags'] = bsd_flags
remote_rate_limit = config.pop('remote_rate_limit', None)
if remote_rate_limit is not None:
logs.append(
logging.makeLogRecord(
dict(
levelno=logging.WARNING,
levelname='WARNING',
msg=f'{config_filename}: The remote_rate_limit option has been renamed to upload_rate_limit. remote_rate_limit is deprecated and support will be removed from a future release.',
)
)
)
config['upload_rate_limit'] = remote_rate_limit
# Upgrade remote repositories to ssh:// syntax, required in Borg 2.
repositories = config.get('repositories')
if repositories:
if any(isinstance(repository, str) for repository in repositories):
logs.append(
logging.makeLogRecord(
dict(
levelno=logging.WARNING,
levelname='WARNING',
msg=f'{config_filename}: The repositories option now expects a list of key/value pairs. Lists of strings for this option are deprecated and support will be removed from a future release.',
)
)
)
config['repositories'] = [
{'path': repository} if isinstance(repository, str) else repository
for repository in repositories
]
repositories = config['repositories']
config['repositories'] = []
for repository_dict in repositories:
repository_path = repository_dict['path']
if '~' in repository_path:
logs.append(
logging.makeLogRecord(
dict(
levelno=logging.WARNING,
levelname='WARNING',
msg=f'{config_filename}: Repository paths containing "~" are deprecated in borgmatic and support will be removed from a future release.',
)
)
)
if ':' in repository_path:
if repository_path.startswith('file://'):
updated_repository_path = os.path.abspath(
repository_path.partition('file://')[-1]
)
config['repositories'].append(
dict(
repository_dict,
path=updated_repository_path,
)
)
elif repository_path.startswith('ssh://'):
config['repositories'].append(repository_dict)
else:
rewritten_repository_path = f"ssh://{repository_path.replace(':~', '/~').replace(':/', '/').replace(':', '/./')}"
logs.append(
logging.makeLogRecord(
dict(
levelno=logging.WARNING,
levelname='WARNING',
msg=f'{config_filename}: Remote repository paths without ssh:// syntax are deprecated and support will be removed from a future release. Interpreting "{repository_path}" as "{rewritten_repository_path}"',
)
)
)
config['repositories'].append(
dict(
repository_dict,
path=rewritten_repository_path,
)
)
else:
config['repositories'].append(repository_dict)
if config.get('prefix'):
logs.append(
logging.makeLogRecord(
dict(
levelno=logging.WARNING,
levelname='WARNING',
msg=f'{config_filename}: The prefix option is deprecated and support will be removed from a future release. Use archive_name_format or match_archives instead.',
)
)
)
return logs

View File

@ -0,0 +1,139 @@
import io
import ruamel.yaml
def set_values(config, keys, value):
'''
Given a hierarchy of configuration dicts, a sequence of parsed key strings, and a string value,
descend into the hierarchy based on the keys to set the value into the right place.
'''
if not keys:
return
first_key = keys[0]
if len(keys) == 1:
if isinstance(config, list):
raise ValueError(
'When overriding a list option, the value must use list syntax (e.g., "[foo, bar]" or "[{key: value}]" as appropriate)'
)
config[first_key] = value
return
if first_key not in config:
config[first_key] = {}
set_values(config[first_key], keys[1:], value)
def convert_value_type(value, option_type):
'''
Given a string value and its schema type as a string, determine its logical type (string,
boolean, integer, etc.), and return it converted to that type.
If the option type is a string, leave the value as a string so that special characters in it
don't get interpreted as YAML during conversion.
Raise ruamel.yaml.error.YAMLError if there's a parse issue with the YAML.
'''
if option_type == 'string':
return value
return ruamel.yaml.YAML(typ='safe').load(io.StringIO(value))
LEGACY_SECTION_NAMES = {'location', 'storage', 'retention', 'consistency', 'output', 'hooks'}
def strip_section_names(parsed_override_key):
'''
Given a parsed override key as a tuple of option and suboption names, strip out any initial
legacy section names, since configuration file normalization also strips them out.
'''
if parsed_override_key[0] in LEGACY_SECTION_NAMES:
return parsed_override_key[1:]
return parsed_override_key
def type_for_option(schema, option_keys):
'''
Given a configuration schema and a sequence of keys identifying an option, e.g.
('extra_borg_options', 'init'), return the schema type of that option as a string.
Return None if the option or its type cannot be found in the schema.
'''
option_schema = schema
for key in option_keys:
try:
option_schema = option_schema['properties'][key]
except KeyError:
return None
try:
return option_schema['type']
except KeyError:
return None
def parse_overrides(raw_overrides, schema):
'''
Given a sequence of configuration file override strings in the form of "option.suboption=value"
and a configuration schema dict, parse and return a sequence of tuples (keys, values), where
keys is a sequence of strings. For instance, given the following raw overrides:
['my_option.suboption=value1', 'other_option=value2']
... return this:
(
(('my_option', 'suboption'), 'value1'),
(('other_option'), 'value2'),
)
Raise ValueError if an override can't be parsed.
'''
if not raw_overrides:
return ()
parsed_overrides = []
for raw_override in raw_overrides:
try:
raw_keys, value = raw_override.split('=', 1)
keys = tuple(raw_keys.split('.'))
option_type = type_for_option(schema, keys)
parsed_overrides.append(
(
keys,
convert_value_type(value, option_type),
)
)
except ValueError:
raise ValueError(
f"Invalid override '{raw_override}'. Make sure you use the form: OPTION=VALUE or OPTION.SUBOPTION=VALUE"
)
except ruamel.yaml.error.YAMLError as error:
raise ValueError(f"Invalid override '{raw_override}': {error.problem}")
return tuple(parsed_overrides)
def apply_overrides(config, schema, raw_overrides):
'''
Given a configuration dict, a corresponding configuration schema dict, and a sequence of
configuration file override strings in the form of "option.suboption=value", parse each override
and set it into the configuration dict.
Set the overrides into the configuration both with and without deprecated section names (if
used), so that the overrides work regardless of whether the configuration is also using
deprecated section names.
'''
overrides = parse_overrides(raw_overrides, schema)
for keys, value in overrides:
set_values(config, keys, value)
set_values(config, strip_section_names(keys), value)

File diff suppressed because it is too large Load Diff

View File

@ -1,38 +1,68 @@
import logging
import os
import pkg_resources
import pykwalify.core
import pykwalify.errors
import jsonschema
import ruamel.yaml
from borgmatic.config import load
import borgmatic.config
from borgmatic.config import constants, environment, load, normalize, override
def schema_filename():
'''
Path to the installed YAML configuration schema file, used to validate and parse the
configuration.
Raise FileNotFoundError when the schema path does not exist.
'''
return pkg_resources.resource_filename('borgmatic', 'config/schema.yaml')
schema_path = os.path.join(os.path.dirname(borgmatic.config.__file__), 'schema.yaml')
with open(schema_path):
return schema_path
def format_json_error_path_element(path_element):
'''
Given a path element into a JSON data structure, format it for display as a string.
'''
if isinstance(path_element, int):
return str(f'[{path_element}]')
return str(f'.{path_element}')
def format_json_error(error):
'''
Given an instance of jsonschema.exceptions.ValidationError, format it for display as a string.
'''
if not error.path:
return f'At the top level: {error.message}'
formatted_path = ''.join(format_json_error_path_element(element) for element in error.path)
return f"At '{formatted_path.lstrip('.')}': {error.message}"
class Validation_error(ValueError):
'''
A collection of error message strings generated when attempting to validate a particular
configurartion file.
A collection of error messages generated when attempting to validate a particular
configuration file.
'''
def __init__(self, config_filename, error_messages):
def __init__(self, config_filename, errors):
'''
Given a configuration filename path and a sequence of string error messages, create a
Validation_error.
'''
self.config_filename = config_filename
self.error_messages = error_messages
self.errors = errors
def __str__(self):
'''
Render a validation error as a user-facing string.
'''
return 'An error occurred while parsing a configuration file at {}:\n'.format(
self.config_filename
) + '\n'.join(self.error_messages)
return (
f'An error occurred while parsing a configuration file at {self.config_filename}:\n'
+ '\n'.join(error for error in self.errors)
)
def apply_logical_validation(config_filename, parsed_configuration):
@ -41,115 +71,141 @@ def apply_logical_validation(config_filename, parsed_configuration):
below), run through any additional logical validation checks. If there are any such validation
problems, raise a Validation_error.
'''
archive_name_format = parsed_configuration.get('storage', {}).get('archive_name_format')
prefix = parsed_configuration.get('retention', {}).get('prefix')
if archive_name_format and not prefix:
raise Validation_error(
config_filename,
('If you provide an archive_name_format, you must also specify a retention prefix.',),
)
location_repositories = parsed_configuration.get('location', {}).get('repositories')
check_repositories = parsed_configuration.get('consistency', {}).get('check_repositories', [])
repositories = parsed_configuration.get('repositories')
check_repositories = parsed_configuration.get('check_repositories', [])
for repository in check_repositories:
if repository not in location_repositories:
if not any(
repositories_match(repository, config_repository) for config_repository in repositories
):
raise Validation_error(
config_filename,
(
'Unknown repository in the consistency section\'s check_repositories: {}'.format(
repository
),
),
(f'Unknown repository in "check_repositories": {repository}',),
)
def remove_examples(schema):
def parse_configuration(config_filename, schema_filename, overrides=None, resolve_env=True):
'''
pykwalify gets angry if the example field is not a string. So rather than bend to its will,
remove all examples from the given schema before passing the schema to pykwalify.
'''
if 'map' in schema:
for item_name, item_schema in schema['map'].items():
item_schema.pop('example', None)
remove_examples(item_schema)
elif 'seq' in schema:
for item_schema in schema['seq']:
item_schema.pop('example', None)
remove_examples(item_schema)
Given the path to a config filename in YAML format, the path to a schema filename in a YAML
rendition of JSON Schema format, a sequence of configuration file override strings in the form
of "option.suboption=value", return the parsed configuration as a data structure of nested dicts
and lists corresponding to the schema. Example return value:
return schema
{
'source_directories': ['/home', '/etc'],
'repository': 'hostname.borg',
'keep_daily': 7,
'checks': ['repository', 'archives'],
}
def parse_configuration(config_filename, schema_filename):
'''
Given the path to a config filename in YAML format and the path to a schema filename in
pykwalify YAML schema format, return the parsed configuration as a data structure of nested
dicts and lists corresponding to the schema. Example return value:
{'location': {'source_directories': ['/home', '/etc'], 'repository': 'hostname.borg'},
'retention': {'keep_daily': 7}, 'consistency': {'checks': ['repository', 'archives']}}
Also return a set of loaded configuration paths and a sequence of logging.LogRecord instances
containing any warnings about the configuration.
Raise FileNotFoundError if the file does not exist, PermissionError if the user does not
have permissions to read the file, or Validation_error if the config does not match the schema.
'''
logging.getLogger('pykwalify').setLevel(logging.ERROR)
config_paths = set()
try:
config = load.load_configuration(config_filename)
config = load.load_configuration(config_filename, config_paths)
schema = load.load_configuration(schema_filename)
except (ruamel.yaml.error.YAMLError, RecursionError) as error:
raise Validation_error(config_filename, (str(error),))
validator = pykwalify.core.Core(source_data=config, schema_data=remove_examples(schema))
parsed_result = validator.validate(raise_exception=False)
override.apply_overrides(config, schema, overrides)
constants.apply_constants(config, config.get('constants') if config else {})
if validator.validation_errors:
raise Validation_error(config_filename, validator.validation_errors)
if resolve_env:
environment.resolve_env_variables(config)
apply_logical_validation(config_filename, parsed_result)
logs = normalize.normalize(config_filename, config)
return parsed_result
try:
validator = jsonschema.Draft7Validator(schema)
except AttributeError: # pragma: no cover
validator = jsonschema.Draft4Validator(schema)
validation_errors = tuple(validator.iter_errors(config))
if validation_errors:
raise Validation_error(
config_filename, tuple(format_json_error(error) for error in validation_errors)
)
apply_logical_validation(config_filename, config)
return config, config_paths, logs
def normalize_repository_path(repository):
'''
Given a repository path, return the absolute path of it (for local repositories).
'''
# A colon in the repository could mean that it's either a file:// URL or a remote repository.
# If it's a remote repository, we don't want to normalize it. If it's a file:// URL, we do.
if ':' not in repository:
return os.path.abspath(repository)
elif repository.startswith('file://'):
return os.path.abspath(repository.partition('file://')[-1])
else:
return repository
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.
'''
if isinstance(first, str):
first = {'path': first, 'label': first}
if isinstance(second, str):
second = {'path': second, 'label': second}
return (first.get('label') == second.get('label')) or (
normalize_repository_path(first.get('path'))
== normalize_repository_path(second.get('path'))
)
def guard_configuration_contains_repository(repository, configurations):
'''
Given a repository path and a dict mapping from config filename to corresponding parsed config
dict, ensure that the repository is declared exactly once in all of the configurations.
dict, ensure that the repository is declared at least once in all of the configurations. If no
repository is given, skip this check.
If no repository is given, then error if there are multiple configured repositories.
Raise ValueError if the repository is not found in a configuration, or is declared multiple
times.
Raise ValueError if the repository is not found in any configurations.
'''
if not repository:
count = len(
tuple(
config_repository
for config in configurations.values()
for config_repository in config['location']['repositories']
)
)
if count > 1:
raise ValueError(
'Can\'t determine which repository to use. Use --repository option to disambiguate'.format(
repository
)
)
return
count = len(
tuple(
config_repository
for config in configurations.values()
for config_repository in config['location']['repositories']
if repository == config_repository
for config_repository in config['repositories']
if repositories_match(config_repository, repository)
)
)
if count == 0:
raise ValueError('Repository {} not found in configuration files'.format(repository))
if count > 1:
raise ValueError('Repository {} found in multiple configuration files'.format(repository))
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"
)

View File

@ -1,117 +1,433 @@
import collections
import enum
import logging
import os
import select
import subprocess
import textwrap
logger = logging.getLogger(__name__)
ERROR_OUTPUT_MAX_LINE_COUNT = 25
BORG_ERROR_EXIT_CODE = 2
BORG_ERROR_EXIT_CODE_START = 2
BORG_ERROR_EXIT_CODE_END = 99
def exit_code_indicates_error(command, exit_code, error_on_warnings=False):
class Exit_status(enum.Enum):
STILL_RUNNING = 1
SUCCESS = 2
WARNING = 3
ERROR = 4
def interpret_exit_code(command, exit_code, borg_local_path=None, borg_exit_codes=None):
'''
Return True if the given exit code from running the command corresponds to an error.
Return an Exit_status value (e.g. SUCCESS, ERROR, or WARNING) based on interpreting the given
exit code. If a Borg local path is given and matches the process' command, then interpret the
exit code based on Borg's documented exit code semantics. And if Borg exit codes are given as a
sequence of exit code configuration dicts, then take those configured preferences into account.
'''
# If we're running something other than Borg, treat all non-zero exit codes as errors.
if 'borg' in command[0] and not error_on_warnings:
return bool(exit_code >= BORG_ERROR_EXIT_CODE)
if exit_code is None:
return Exit_status.STILL_RUNNING
if exit_code == 0:
return Exit_status.SUCCESS
return bool(exit_code != 0)
if borg_local_path and command[0] == borg_local_path:
# First try looking for the exit code in the borg_exit_codes configuration.
for entry in borg_exit_codes or ():
if entry.get('code') == exit_code:
treat_as = entry.get('treat_as')
if treat_as == 'error':
logger.error(
f'Treating exit code {exit_code} as an error, as per configuration'
)
return Exit_status.ERROR
elif treat_as == 'warning':
logger.warning(
f'Treating exit code {exit_code} as a warning, as per configuration'
)
return Exit_status.WARNING
# If the exit code doesn't have explicit configuration, then fall back to the default Borg
# behavior.
return (
Exit_status.ERROR
if (
exit_code < 0
or (
exit_code >= BORG_ERROR_EXIT_CODE_START
and exit_code <= BORG_ERROR_EXIT_CODE_END
)
)
else Exit_status.WARNING
)
return Exit_status.ERROR
def execute_and_log_output(
full_command, output_log_level, shell, environment, working_directory, error_on_warnings
):
last_lines = []
process = subprocess.Popen(
full_command,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
shell=shell,
env=environment,
cwd=working_directory,
)
def command_for_process(process):
'''
Given a process as an instance of subprocess.Popen, return the command string that was used to
invoke it.
'''
return process.args if isinstance(process.args, str) else ' '.join(process.args)
while process.poll() is None:
line = process.stdout.readline().rstrip().decode()
if not line:
continue
# Keep the last few lines of output in case the command errors, and we need the output for
# the exception below.
last_lines.append(line)
if len(last_lines) > ERROR_OUTPUT_MAX_LINE_COUNT:
last_lines.pop(0)
def output_buffer_for_process(process, exclude_stdouts):
'''
Given a process as an instance of subprocess.Popen and a sequence of stdouts to exclude, return
either the process's stdout or stderr. The idea is that if stdout is excluded for a process, we
still have stderr to log.
'''
return process.stderr if process.stdout in exclude_stdouts else process.stdout
def append_last_lines(last_lines, captured_output, line, output_log_level):
'''
Given a rolling list of last lines, a list of captured output, a line to append, and an output
log level, append the line to the last lines and (if necessary) the captured output. Then log
the line at the requested output log level.
'''
last_lines.append(line)
if len(last_lines) > ERROR_OUTPUT_MAX_LINE_COUNT:
last_lines.pop(0)
if output_log_level is None:
captured_output.append(line)
else:
logger.log(output_log_level, line)
remaining_output = process.stdout.read().rstrip().decode()
if remaining_output: # pragma: no cover
logger.log(output_log_level, remaining_output)
exit_code = process.poll()
def log_outputs(processes, exclude_stdouts, output_log_level, borg_local_path, borg_exit_codes):
'''
Given a sequence of subprocess.Popen() instances for multiple processes, log the output for each
process with the requested log level. Additionally, raise a CalledProcessError if a process
exits with an error (or a warning for exit code 1, if that process does not match the Borg local
path).
if exit_code_indicates_error(full_command, exit_code, error_on_warnings):
# If an error occurs, include its output in the raised exception so that we don't
# inadvertently hide error output.
if len(last_lines) == ERROR_OUTPUT_MAX_LINE_COUNT:
last_lines.insert(0, '...')
If output log level is None, then instead of logging, capture output for each process and return
it as a dict from the process to its output. Use the given Borg local path and exit code
configuration to decide what's an error and what's a warning.
raise subprocess.CalledProcessError(
exit_code, ' '.join(full_command), '\n'.join(last_lines)
For simplicity, it's assumed that the output buffer for each process is its stdout. But if any
stdouts are given to exclude, then for any matching processes, log from their stderr instead.
Note that stdout for a process can be None if output is intentionally not captured. In which
case it won't be logged.
'''
# Map from output buffer to sequence of last lines.
buffer_last_lines = collections.defaultdict(list)
process_for_output_buffer = {
output_buffer_for_process(process, exclude_stdouts): process
for process in processes
if process.stdout or process.stderr
}
output_buffers = list(process_for_output_buffer.keys())
captured_outputs = collections.defaultdict(list)
still_running = True
# Log output for each process until they all exit.
while True:
if output_buffers:
(ready_buffers, _, _) = select.select(output_buffers, [], [])
for ready_buffer in ready_buffers:
ready_process = process_for_output_buffer.get(ready_buffer)
# The "ready" process has exited, but it might be a pipe destination with other
# processes (pipe sources) waiting to be read from. So as a measure to prevent
# hangs, vent all processes when one exits.
if ready_process and ready_process.poll() is not None:
for other_process in processes:
if (
other_process.poll() is None
and other_process.stdout
and other_process.stdout not in output_buffers
):
# Add the process's output to output_buffers to ensure it'll get read.
output_buffers.append(other_process.stdout)
while True:
line = ready_buffer.readline().rstrip().decode()
if not line or not ready_process:
break
# Keep the last few lines of output in case the process errors, and we need the output for
# the exception below.
append_last_lines(
buffer_last_lines[ready_buffer],
captured_outputs[ready_process],
line,
output_log_level,
)
if not still_running:
break
still_running = False
for process in processes:
exit_code = process.poll() if output_buffers else process.wait()
if exit_code is None:
still_running = True
command = process.args.split(' ') if isinstance(process.args, str) else process.args
continue
command = process.args.split(' ') if isinstance(process.args, str) else process.args
exit_status = interpret_exit_code(command, exit_code, borg_local_path, borg_exit_codes)
if exit_status in (Exit_status.ERROR, Exit_status.WARNING):
# If an error occurs, include its output in the raised exception so that we don't
# inadvertently hide error output.
output_buffer = output_buffer_for_process(process, exclude_stdouts)
last_lines = buffer_last_lines[output_buffer] if output_buffer else []
# Collect any straggling output lines that came in since we last gathered output.
while output_buffer: # pragma: no cover
line = output_buffer.readline().rstrip().decode()
if not line:
break
append_last_lines(
last_lines, captured_outputs[process], line, output_log_level=logging.ERROR
)
if len(last_lines) == ERROR_OUTPUT_MAX_LINE_COUNT:
last_lines.insert(0, '...')
# Something has gone wrong. So vent each process' output buffer to prevent it from
# hanging. And then kill the process.
for other_process in processes:
if other_process.poll() is None:
other_process.stdout.read(0)
other_process.kill()
if exit_status == Exit_status.ERROR:
raise subprocess.CalledProcessError(
exit_code, command_for_process(process), '\n'.join(last_lines)
)
still_running = False
break
if captured_outputs:
return {
process: '\n'.join(output_lines) for process, output_lines in captured_outputs.items()
}
SECRET_COMMAND_FLAG_NAMES = {'--password'}
def mask_command_secrets(full_command):
'''
Given a command as a sequence, mask secret values for flags like "--password" in preparation for
logging.
'''
masked_command = []
previous_piece = None
for piece in full_command:
masked_command.append('***' if previous_piece in SECRET_COMMAND_FLAG_NAMES else piece)
previous_piece = piece
return tuple(masked_command)
MAX_LOGGED_COMMAND_LENGTH = 1000
def log_command(full_command, input_file=None, output_file=None, environment=None):
'''
Log the given command (a sequence of command/argument strings), along with its input/output file
paths and extra environment variables (with omitted values in case they contain passwords).
'''
logger.debug(
textwrap.shorten(
' '.join(
tuple(f'{key}=***' for key in (environment or {}).keys())
+ mask_command_secrets(full_command)
),
width=MAX_LOGGED_COMMAND_LENGTH,
placeholder=' ...',
)
+ (f" < {getattr(input_file, 'name', '')}" if input_file else '')
+ (f" > {getattr(output_file, 'name', '')}" if output_file else '')
)
# A sentinel passed as an output file to execute_command() to indicate that the command's output
# should be allowed to flow through to stdout without being captured for logging. Useful for
# commands with interactive prompts or those that mess directly with the console.
DO_NOT_CAPTURE = object()
def execute_command(
full_command,
output_log_level=logging.INFO,
output_file=None,
input_file=None,
shell=False,
extra_environment=None,
working_directory=None,
error_on_warnings=False,
borg_local_path=None,
borg_exit_codes=None,
run_to_completion=True,
):
'''
Execute the given command (a sequence of command/argument strings) and log its output at the
given log level. If output log level is None, instead capture and return the output. If
shell is True, execute the command within a shell. If an extra environment dict is given, then
use it to augment the current environment, and pass the result into the command. If a working
directory is given, use that as the present working directory when running the command.
given log level. If an open output file object is given, then write stdout to the file and only
log stderr. If an open input file object is given, then read stdin from the file. If shell is
True, execute the command within a shell. If an extra environment dict is given, then use it to
augment the current environment, and pass the result into the command. If a working directory is
given, use that as the present working directory when running the command. If a Borg local path
is given, and the command matches it (regardless of arguments), treat exit code 1 as a warning
instead of an error. But if Borg exit codes are given as a sequence of exit code configuration
dicts, then use that configuration to decide what's an error and what's a warning. If run to
completion is False, then return the process for the command without executing it to completion.
Raise subprocesses.CalledProcessError if an error occurs while running the command.
'''
logger.debug(' '.join(full_command))
log_command(full_command, input_file, output_file, extra_environment)
environment = {**os.environ, **extra_environment} if extra_environment else None
do_not_capture = bool(output_file is DO_NOT_CAPTURE)
command = ' '.join(full_command) if shell else full_command
if output_log_level is None:
output = subprocess.check_output(
full_command, shell=shell, env=environment, cwd=working_directory
)
return output.decode() if output is not None else None
else:
execute_and_log_output(
full_command,
output_log_level,
shell=shell,
environment=environment,
working_directory=working_directory,
error_on_warnings=error_on_warnings,
)
process = subprocess.Popen(
command,
stdin=input_file,
stdout=None if do_not_capture else (output_file or subprocess.PIPE),
stderr=None if do_not_capture else (subprocess.PIPE if output_file else subprocess.STDOUT),
shell=shell,
env=environment,
cwd=working_directory,
)
if not run_to_completion:
return process
log_outputs(
(process,),
(input_file, output_file),
output_log_level,
borg_local_path,
borg_exit_codes,
)
def execute_command_without_capture(full_command, working_directory=None, error_on_warnings=False):
def execute_command_and_capture_output(
full_command,
capture_stderr=False,
shell=False,
extra_environment=None,
working_directory=None,
borg_local_path=None,
borg_exit_codes=None,
):
'''
Execute the given command (a sequence of command/argument strings), but don't capture or log its
output in any way. This is necessary for commands that monkey with the terminal (e.g. progress
display) or provide interactive prompts.
Execute the given command (a sequence of command/argument strings), capturing and returning its
output (stdout). If capture stderr is True, then capture and return stderr in addition to
stdout. If shell is True, execute the command within a shell. If an extra environment dict is
given, then use it to augment the current environment, and pass the result into the command. If
a working directory is given, use that as the present working directory when running the
command. If a Borg local path is given, and the command matches it (regardless of arguments),
treat exit code 1 as a warning instead of an error. But if Borg exit codes are given as a
sequence of exit code configuration dicts, then use that configuration to decide what's an error
and what's a warning.
If a working directory is given, use that as the present working directory when running the
command.
Raise subprocesses.CalledProcessError if an error occurs while running the command.
'''
logger.debug(' '.join(full_command))
log_command(full_command, environment=extra_environment)
environment = {**os.environ, **extra_environment} if extra_environment else None
command = ' '.join(full_command) if shell else full_command
try:
subprocess.check_call(full_command, cwd=working_directory)
output = subprocess.check_output(
command,
stderr=subprocess.STDOUT if capture_stderr else None,
shell=shell,
env=environment,
cwd=working_directory,
)
except subprocess.CalledProcessError as error:
if exit_code_indicates_error(full_command, error.returncode, error_on_warnings):
if (
interpret_exit_code(command, error.returncode, borg_local_path, borg_exit_codes)
== Exit_status.ERROR
):
raise
output = error.output
return output.decode() if output is not None else None
def execute_command_with_processes(
full_command,
processes,
output_log_level=logging.INFO,
output_file=None,
input_file=None,
shell=False,
extra_environment=None,
working_directory=None,
borg_local_path=None,
borg_exit_codes=None,
):
'''
Execute the given command (a sequence of command/argument strings) and log its output at the
given log level. Simultaneously, continue to poll one or more active processes so that they
run as well. This is useful, for instance, for processes that are streaming output to a named
pipe that the given command is consuming from.
If an open output file object is given, then write stdout to the file and only log stderr. But
if output log level is None, instead suppress logging and return the captured output for (only)
the given command. If an open input file object is given, then read stdin from the file. If
shell is True, execute the command within a shell. If an extra environment dict is given, then
use it to augment the current environment, and pass the result into the command. If a working
directory is given, use that as the present working directory when running the command. If a
Borg local path is given, then for any matching command or process (regardless of arguments),
treat exit code 1 as a warning instead of an error. But if Borg exit codes are given as a
sequence of exit code configuration dicts, then use that configuration to decide what's an error
and what's a warning.
Raise subprocesses.CalledProcessError if an error occurs while running the command or in the
upstream process.
'''
log_command(full_command, input_file, output_file, extra_environment)
environment = {**os.environ, **extra_environment} if extra_environment else None
do_not_capture = bool(output_file is DO_NOT_CAPTURE)
command = ' '.join(full_command) if shell else full_command
try:
command_process = subprocess.Popen(
command,
stdin=input_file,
stdout=None if do_not_capture else (output_file or subprocess.PIPE),
stderr=(
None if do_not_capture else (subprocess.PIPE if output_file else subprocess.STDOUT)
),
shell=shell,
env=environment,
cwd=working_directory,
)
except (subprocess.CalledProcessError, OSError):
# Something has gone wrong. So vent each process' output buffer to prevent it from hanging.
# And then kill the process.
for process in processes:
if process.poll() is None:
process.stdout.read(0)
process.kill()
raise
captured_outputs = log_outputs(
tuple(processes) + (command_process,),
(input_file, output_file),
output_log_level,
borg_local_path,
borg_exit_codes,
)
if output_log_level is None:
return captured_outputs.get(command_process)

109
borgmatic/hooks/apprise.py Normal file
View File

@ -0,0 +1,109 @@
import logging
import operator
import borgmatic.hooks.logs
import borgmatic.hooks.monitor
logger = logging.getLogger(__name__)
DEFAULT_LOGS_SIZE_LIMIT_BYTES = 100000
HANDLER_IDENTIFIER = 'apprise'
def initialize_monitor(hook_config, config, config_filename, monitoring_log_level, dry_run):
'''
Add a handler to the root logger that stores in memory the most recent logs emitted. That way,
we can send them all to an Apprise notification service upon a finish or failure state. But skip
this if the "send_logs" option is false.
'''
if hook_config.get('send_logs') is False:
return
logs_size_limit = max(
hook_config.get('logs_size_limit', DEFAULT_LOGS_SIZE_LIMIT_BYTES)
- len(borgmatic.hooks.logs.PAYLOAD_TRUNCATION_INDICATOR),
0,
)
borgmatic.hooks.logs.add_handler(
borgmatic.hooks.logs.Forgetful_buffering_handler(
HANDLER_IDENTIFIER, logs_size_limit, monitoring_log_level
)
)
def ping_monitor(hook_config, config, config_filename, state, monitoring_log_level, dry_run):
'''
Ping the configured Apprise service URLs. Use the given configuration filename in any log
entries. If this is a dry run, then don't actually ping anything.
'''
try:
import apprise
from apprise import NotifyFormat, NotifyType
except ImportError: # pragma: no cover
logger.warning('Unable to import Apprise in monitoring hook')
return
state_to_notify_type = {
'start': NotifyType.INFO,
'finish': NotifyType.SUCCESS,
'fail': NotifyType.FAILURE,
'log': NotifyType.INFO,
}
run_states = hook_config.get('states', ['fail'])
if state.name.lower() not in run_states:
return
state_config = hook_config.get(
state.name.lower(),
{
'title': f'A borgmatic {state.name} event happened',
'body': f'A borgmatic {state.name} event happened',
},
)
if not hook_config.get('services'):
logger.info(f'{config_filename}: No Apprise services to ping')
return
dry_run_string = ' (dry run; not actually pinging)' if dry_run else ''
labels_string = ', '.join(map(operator.itemgetter('label'), hook_config.get('services')))
logger.info(f'{config_filename}: Pinging Apprise services: {labels_string}{dry_run_string}')
apprise_object = apprise.Apprise()
apprise_object.add(list(map(operator.itemgetter('url'), hook_config.get('services'))))
if dry_run:
return
body = state_config.get('body')
if state in (
borgmatic.hooks.monitor.State.FINISH,
borgmatic.hooks.monitor.State.FAIL,
borgmatic.hooks.monitor.State.LOG,
):
formatted_logs = borgmatic.hooks.logs.format_buffered_logs_for_payload(HANDLER_IDENTIFIER)
if formatted_logs:
body += f'\n\n{formatted_logs}'
result = apprise_object.notify(
title=state_config.get('title', ''),
body=body,
body_format=NotifyFormat.TEXT,
notify_type=state_to_notify_type[state.name.lower()],
)
if result is False:
logger.warning(f'{config_filename}: Error sending some Apprise notifications')
def destroy_monitor(hook_config, config, config_filename, monitoring_log_level, dry_run):
'''
Remove the monitor handler that was added to the root logger. This prevents the handler from
getting reused by other instances of this monitor.
'''
borgmatic.hooks.logs.remove_handler(HANDLER_IDENTIFIER)

View File

@ -1,18 +1,28 @@
import logging
import os
import re
import shlex
from borgmatic import execute
logger = logging.getLogger(__name__)
def interpolate_context(command, context):
SOFT_FAIL_EXIT_CODE = 75
def interpolate_context(config_filename, hook_description, command, context):
'''
Given a single hook command and a dict of context names/values, interpolate the values by
"{name}" into the command and return the result.
Given a config filename, a hook description, a single hook command, and a dict of context
names/values, interpolate the values by "{name}" into the command and return the result.
'''
for name, value in context.items():
command = command.replace('{%s}' % name, str(value))
command = command.replace(f'{{{name}}}', shlex.quote(str(value)))
for unsupported_variable in re.findall(r'{\w+}', command):
logger.warning(
f"{config_filename}: Variable '{unsupported_variable}' is not supported in {hook_description} hook"
)
return command
@ -23,35 +33,32 @@ def execute_hook(commands, umask, config_filename, description, dry_run, **conte
a hook description, and whether this is a dry run, run the given commands. Or, don't run them
if this is a dry run.
The context contains optional values interpolated by name into the hook commands. Currently,
this only applies to the on_error hook.
The context contains optional values interpolated by name into the hook commands.
Raise ValueError if the umask cannot be parsed.
Raise subprocesses.CalledProcessError if an error occurs in a hook.
'''
if not commands:
logger.debug('{}: No commands to run for {} hook'.format(config_filename, description))
logger.debug(f'{config_filename}: No commands to run for {description} hook')
return
dry_run_label = ' (dry run; not actually running hooks)' if dry_run else ''
context['configuration_filename'] = config_filename
commands = [interpolate_context(command, context) for command in commands]
commands = [
interpolate_context(config_filename, description, command, context) for command in commands
]
if len(commands) == 1:
logger.info(
'{}: Running command for {} hook{}'.format(config_filename, description, dry_run_label)
)
logger.info(f'{config_filename}: Running command for {description} hook{dry_run_label}')
else:
logger.info(
'{}: Running {} commands for {} hook{}'.format(
config_filename, len(commands), description, dry_run_label
)
f'{config_filename}: Running {len(commands)} commands for {description} hook{dry_run_label}',
)
if umask:
parsed_umask = int(str(umask), 8)
logger.debug('{}: Set hook umask to {}'.format(config_filename, oct(parsed_umask)))
logger.debug(f'{config_filename}: Set hook umask to {oct(parsed_umask)}')
original_umask = os.umask(parsed_umask)
else:
original_umask = None
@ -61,11 +68,30 @@ def execute_hook(commands, umask, config_filename, description, dry_run, **conte
if not dry_run:
execute.execute_command(
[command],
output_log_level=logging.ERROR
if description == 'on-error'
else logging.WARNING,
output_log_level=(
logging.ERROR if description == 'on-error' else logging.WARNING
),
shell=True,
)
finally:
if original_umask:
os.umask(original_umask)
def considered_soft_failure(config_filename, error):
'''
Given a configuration filename and an exception object, return whether the exception object
represents a subprocess.CalledProcessError with a return code of SOFT_FAIL_EXIT_CODE. If so,
that indicates that the error is a "soft failure", and should not result in an error.
'''
exit_code = getattr(error, 'returncode', None)
if exit_code is None:
return False
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',
)
return True
return False

View File

@ -0,0 +1,63 @@
import logging
import requests
from borgmatic.hooks import monitor
logger = logging.getLogger(__name__)
MONITOR_STATE_TO_CRONHUB = {
monitor.State.START: 'start',
monitor.State.FINISH: 'finish',
monitor.State.FAIL: 'fail',
}
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):
'''
Ping the configured Cronhub URL, modified with the monitor.State. Use the given configuration
filename in any log entries. If this is a dry run, then don't actually ping anything.
'''
if state not in MONITOR_STATE_TO_CRONHUB:
logger.debug(
f'{config_filename}: Ignoring unsupported monitoring {state.name.lower()} in Cronhub hook'
)
return
dry_run_label = ' (dry run; not actually pinging)' if dry_run else ''
formatted_state = f'/{MONITOR_STATE_TO_CRONHUB[state]}/'
ping_url = (
hook_config['ping_url']
.replace('/start/', formatted_state)
.replace('/ping/', formatted_state)
)
logger.info(f'{config_filename}: Pinging Cronhub {state.name.lower()}{dry_run_label}')
logger.debug(f'{config_filename}: Using Cronhub ping URL {ping_url}')
if not dry_run:
logging.getLogger('urllib3').setLevel(logging.ERROR)
try:
response = requests.get(ping_url)
if not response.ok:
response.raise_for_status()
except requests.exceptions.RequestException as error:
logger.warning(f'{config_filename}: Cronhub 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

View File

@ -2,23 +2,57 @@ import logging
import requests
from borgmatic.hooks import monitor
logger = logging.getLogger(__name__)
MONITOR_STATE_TO_CRONITOR = {
monitor.State.START: 'run',
monitor.State.FINISH: 'complete',
monitor.State.FAIL: 'fail',
}
def ping_cronitor(ping_url, config_filename, dry_run, append):
def initialize_monitor(
ping_url, config, config_filename, monitoring_log_level, dry_run
): # pragma: no cover
'''
Ping the given Cronitor URL, appending the append string. Use the given configuration filename
in any log entries. If this is a dry run, then don't actually ping anything.
No initialization is necessary for this monitor.
'''
if not ping_url:
logger.debug('{}: No Cronitor hook set'.format(config_filename))
pass
def ping_monitor(hook_config, config, config_filename, state, monitoring_log_level, dry_run):
'''
Ping the configured Cronitor URL, modified with the monitor.State. Use the given configuration
filename in any log entries. If this is a dry run, then don't actually ping anything.
'''
if state not in MONITOR_STATE_TO_CRONITOR:
logger.debug(
f'{config_filename}: Ignoring unsupported monitoring {state.name.lower()} in Cronitor hook'
)
return
dry_run_label = ' (dry run; not actually pinging)' if dry_run else ''
ping_url = '{}/{}'.format(ping_url, append)
ping_url = f"{hook_config['ping_url']}/{MONITOR_STATE_TO_CRONITOR[state]}"
logger.info('{}: Pinging Cronitor {}{}'.format(config_filename, append, dry_run_label))
logger.debug('{}: Using Cronitor ping URL {}'.format(config_filename, ping_url))
logger.info(f'{config_filename}: Pinging Cronitor {state.name.lower()}{dry_run_label}')
logger.debug(f'{config_filename}: Using Cronitor ping URL {ping_url}')
logging.getLogger('urllib3').setLevel(logging.ERROR)
requests.get(ping_url)
if not dry_run:
logging.getLogger('urllib3').setLevel(logging.ERROR)
try:
response = requests.get(ping_url)
if not response.ok:
response.raise_for_status()
except requests.exceptions.RequestException as error:
logger.warning(f'{config_filename}: Cronitor 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

View File

@ -0,0 +1,91 @@
import logging
from borgmatic.hooks import (
apprise,
cronhub,
cronitor,
healthchecks,
loki,
mariadb,
mongodb,
mysql,
ntfy,
pagerduty,
postgresql,
sqlite,
)
logger = logging.getLogger(__name__)
HOOK_NAME_TO_MODULE = {
'apprise': apprise,
'cronhub': cronhub,
'cronitor': cronitor,
'healthchecks': healthchecks,
'mariadb_databases': mariadb,
'mongodb_databases': mongodb,
'mysql_databases': mysql,
'ntfy': ntfy,
'pagerduty': pagerduty,
'postgresql_databases': postgresql,
'sqlite_databases': sqlite,
'loki': loki,
}
def call_hook(function_name, config, log_prefix, hook_name, *args, **kwargs):
'''
Given a configuration dict and a prefix to use in log entries, call the requested function of
the Python module corresponding to the given hook name. Supply that call with the configuration
for this hook (if any), the log prefix, and any given args and kwargs. Return any return value.
Raise ValueError if the hook name is unknown.
Raise AttributeError if the function name is not found in the module.
Raise anything else that the called function raises.
'''
hook_config = config.get(hook_name, {})
try:
module = HOOK_NAME_TO_MODULE[hook_name]
except KeyError:
raise ValueError(f'Unknown hook name: {hook_name}')
logger.debug(f'{log_prefix}: Calling {hook_name} hook function {function_name}')
return getattr(module, function_name)(hook_config, config, log_prefix, *args, **kwargs)
def call_hooks(function_name, config, log_prefix, hook_names, *args, **kwargs):
'''
Given a configuration dict and a prefix to use in log entries, call the requested function of
the Python module corresponding to each given hook name. Supply each call with the configuration
for that hook, the log prefix, and any given args and kwargs. Collect any return values into a
dict from hook name to return value.
If the hook name is not present in the hooks configuration, then don't call the function for it
and omit it from the return values.
Raise ValueError if the hook name is unknown.
Raise AttributeError if the function name is not found in the module.
Raise anything else that a called function raises. An error stops calls to subsequent functions.
'''
return {
hook_name: call_hook(function_name, config, log_prefix, hook_name, *args, **kwargs)
for hook_name in hook_names
if config.get(hook_name)
}
def call_hooks_even_if_unconfigured(function_name, config, log_prefix, hook_names, *args, **kwargs):
'''
Given a configuration dict and a prefix to use in log entries, call the requested function of
the Python module corresponding to each given hook name. Supply each call with the configuration
for that hook, the log prefix, and any given args and kwargs. Collect any return values into a
dict from hook name to return value.
Raise AttributeError if the function name is not found in the module.
Raise anything else that a called function raises. An error stops calls to subsequent functions.
'''
return {
hook_name: call_hook(function_name, config, log_prefix, hook_name, *args, **kwargs)
for hook_name in hook_names
}

80
borgmatic/hooks/dump.py Normal file
View File

@ -0,0 +1,80 @@
import logging
import os
import shutil
from borgmatic.borg.state import DEFAULT_BORGMATIC_SOURCE_DIRECTORY
logger = logging.getLogger(__name__)
DATA_SOURCE_HOOK_NAMES = (
'mariadb_databases',
'mysql_databases',
'mongodb_databases',
'postgresql_databases',
'sqlite_databases',
)
def make_data_source_dump_path(borgmatic_source_directory, data_source_hook_name):
'''
Given a borgmatic source directory (or None) 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)
def make_data_source_dump_filename(dump_path, name, hostname=None):
'''
Based on the given dump directory path, data source name, and hostname, return a filename to use
for the data source dump. The hostname defaults to localhost.
Raise ValueError if the data source name is invalid.
'''
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)
def create_parent_directory_for_dump(dump_path):
'''
Create a directory to contain the given dump path.
'''
os.makedirs(os.path.dirname(dump_path), mode=0o700, exist_ok=True)
def create_named_pipe_for_dump(dump_path):
'''
Create a named pipe at the given dump path.
'''
create_parent_directory_for_dump(dump_path)
os.mkfifo(dump_path, mode=0o600)
def remove_data_source_dumps(dump_path, data_source_type_name, log_prefix, dry_run):
'''
Remove all data source dumps in the given dump directory path (including the directory itself).
If this is a dry run, then don't actually remove anything.
'''
dry_run_label = ' (dry run; not actually removing anything)' if dry_run else ''
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)
def convert_glob_patterns_to_borg_patterns(patterns):
'''
Convert a sequence of shell glob patterns like "/etc/*" to the corresponding Borg archive
patterns like "sh:etc/*".
'''
return [f'sh:{pattern.lstrip(os.path.sep)}' for pattern in patterns]

View File

@ -1,36 +1,102 @@
import logging
import re
import requests
import borgmatic.hooks.logs
from borgmatic.hooks import monitor
logger = logging.getLogger(__name__)
MONITOR_STATE_TO_HEALTHCHECKS = {
monitor.State.START: 'start',
monitor.State.FINISH: None, # Healthchecks doesn't append to the URL for the finished state.
monitor.State.FAIL: 'fail',
monitor.State.LOG: 'log',
}
def ping_healthchecks(ping_url_or_uuid, config_filename, dry_run, append=None):
DEFAULT_PING_BODY_LIMIT_BYTES = 1500
HANDLER_IDENTIFIER = 'healthchecks'
def initialize_monitor(hook_config, config, config_filename, monitoring_log_level, dry_run):
'''
Ping the given Healthchecks URL or UUID, appending the append string if any. Use the given
configuration filename in any log entries. If this is a dry run, then don't actually ping
anything.
Add a handler to the root logger that stores in memory the most recent logs emitted. That way,
we can send them all to Healthchecks upon a finish or failure state. But skip this if the
"send_logs" option is false.
'''
if not ping_url_or_uuid:
logger.debug('{}: No Healthchecks hook set'.format(config_filename))
if hook_config.get('send_logs') is False:
return
ping_body_limit = max(
hook_config.get('ping_body_limit', DEFAULT_PING_BODY_LIMIT_BYTES)
- len(borgmatic.hooks.logs.PAYLOAD_TRUNCATION_INDICATOR),
0,
)
borgmatic.hooks.logs.add_handler(
borgmatic.hooks.logs.Forgetful_buffering_handler(
HANDLER_IDENTIFIER, ping_body_limit, monitoring_log_level
)
)
def ping_monitor(hook_config, config, config_filename, state, monitoring_log_level, dry_run):
'''
Ping the configured Healthchecks URL or UUID, modified with the monitor.State. Use the given
configuration filename in any log entries, and log to Healthchecks with the giving log level.
If this is a dry run, then don't actually ping anything.
'''
ping_url = (
ping_url_or_uuid
if ping_url_or_uuid.startswith('http')
else 'https://hc-ping.com/{}'.format(ping_url_or_uuid)
hook_config['ping_url']
if hook_config['ping_url'].startswith('http')
else f"https://hc-ping.com/{hook_config['ping_url']}"
)
dry_run_label = ' (dry run; not actually pinging)' if dry_run else ''
if append:
ping_url = '{}/{}'.format(ping_url, append)
logger.info(
'{}: Pinging Healthchecks{}{}'.format(
config_filename, ' ' + append if append else '', dry_run_label
if 'states' in hook_config and state.name.lower() not in hook_config['states']:
logger.info(
f'{config_filename}: Skipping Healthchecks {state.name.lower()} ping due to configured states'
)
)
logger.debug('{}: Using Healthchecks ping URL {}'.format(config_filename, ping_url))
return
logging.getLogger('urllib3').setLevel(logging.ERROR)
requests.get(ping_url)
ping_url_is_uuid = re.search(r'\w{8}-\w{4}-\w{4}-\w{4}-\w{12}$', ping_url)
healthchecks_state = MONITOR_STATE_TO_HEALTHCHECKS.get(state)
if healthchecks_state:
ping_url = f'{ping_url}/{healthchecks_state}'
if hook_config.get('create_slug'):
if ping_url_is_uuid:
logger.warning(
f'{config_filename}: Healthchecks UUIDs do not support auto provisionning; ignoring'
)
else:
ping_url = f'{ping_url}?create=1'
logger.info(f'{config_filename}: Pinging Healthchecks {state.name.lower()}{dry_run_label}')
logger.debug(f'{config_filename}: Using Healthchecks ping URL {ping_url}')
if state in (monitor.State.FINISH, monitor.State.FAIL, monitor.State.LOG):
payload = borgmatic.hooks.logs.format_buffered_logs_for_payload(HANDLER_IDENTIFIER)
else:
payload = ''
if not dry_run:
logging.getLogger('urllib3').setLevel(logging.ERROR)
try:
response = requests.post(
ping_url, data=payload.encode('utf-8'), verify=hook_config.get('verify_tls', True)
)
if not response.ok:
response.raise_for_status()
except requests.exceptions.RequestException as error:
logger.warning(f'{config_filename}: Healthchecks error: {error}')
def destroy_monitor(hook_config, config, config_filename, monitoring_log_level, dry_run):
'''
Remove the monitor handler that was added to the root logger. This prevents the handler from
getting reused by other instances of this monitor.
'''
borgmatic.hooks.logs.remove_handler(HANDLER_IDENTIFIER)

91
borgmatic/hooks/logs.py Normal file
View File

@ -0,0 +1,91 @@
import logging
PAYLOAD_TRUNCATION_INDICATOR = '...\n'
class Forgetful_buffering_handler(logging.Handler):
'''
A buffering log handler that stores log messages in memory, and throws away messages (oldest
first) once a particular capacity in bytes is reached. But if the given byte capacity is zero,
don't throw away any messages.
The given identifier is used to distinguish the instance of this handler used for one monitoring
hook from those instances used for other monitoring hooks.
'''
def __init__(self, identifier, byte_capacity, log_level):
super().__init__()
self.identifier = identifier
self.byte_capacity = byte_capacity
self.byte_count = 0
self.buffer = []
self.forgot = False
self.setLevel(log_level)
def emit(self, record):
message = record.getMessage() + '\n'
self.byte_count += len(message)
self.buffer.append(message)
if not self.byte_capacity:
return
while self.byte_count > self.byte_capacity and self.buffer:
self.byte_count -= len(self.buffer[0])
self.buffer.pop(0)
self.forgot = True
def add_handler(handler): # pragma: no cover
'''
Add the given handler to the global logger.
'''
logging.getLogger().addHandler(handler)
def get_handler(identifier):
'''
Given the identifier for an existing Forgetful_buffering_handler instance, return the handler.
Raise ValueError if the handler isn't found.
'''
try:
return next(
handler
for handler in logging.getLogger().handlers
if isinstance(handler, Forgetful_buffering_handler) and handler.identifier == identifier
)
except StopIteration:
raise ValueError(f'A buffering handler for {identifier} was not found')
def format_buffered_logs_for_payload(identifier):
'''
Get the handler previously added to the root logger, and slurp buffered logs out of it to
send to Healthchecks.
'''
try:
buffering_handler = get_handler(identifier)
except ValueError:
# No handler means no payload.
return ''
payload = ''.join(message for message in buffering_handler.buffer)
if buffering_handler.forgot:
return PAYLOAD_TRUNCATION_INDICATOR + payload
return payload
def remove_handler(identifier):
'''
Given the identifier for an existing Forgetful_buffering_handler instance, remove it.
'''
logger = logging.getLogger()
try:
logger.removeHandler(get_handler(identifier))
except ValueError:
pass

154
borgmatic/hooks/loki.py Normal file
View File

@ -0,0 +1,154 @@
import json
import logging
import os
import platform
import time
import requests
from borgmatic.hooks import monitor
logger = logging.getLogger(__name__)
MONITOR_STATE_TO_LOKI = {
monitor.State.START: 'Started',
monitor.State.FINISH: 'Finished',
monitor.State.FAIL: 'Failed',
}
# Threshold at which logs get flushed to loki
MAX_BUFFER_LINES = 100
class Loki_log_buffer:
'''
A log buffer that allows to output the logs as loki requests in json. Allows
adding labels to the log stream and takes care of communication with loki.
'''
def __init__(self, url, dry_run):
self.url = url
self.dry_run = dry_run
self.root = {'streams': [{'stream': {}, 'values': []}]}
def add_value(self, value):
'''
Add a log entry to the stream.
'''
timestamp = str(time.time_ns())
self.root['streams'][0]['values'].append((timestamp, value))
def add_label(self, label, value):
'''
Add a label to the logging stream.
'''
self.root['streams'][0]['stream'][label] = value
def to_request(self):
return json.dumps(self.root)
def __len__(self):
'''
Gets the number of lines currently in the buffer.
'''
return len(self.root['streams'][0]['values'])
def flush(self):
if self.dry_run:
# Just empty the buffer and skip
self.root['streams'][0]['values'] = []
logger.info('Skipped uploading logs to loki due to dry run')
return
if len(self) == 0:
# Skip as there are not logs to send yet
return
request_body = self.to_request()
self.root['streams'][0]['values'] = []
request_header = {'Content-Type': 'application/json'}
try:
result = requests.post(self.url, headers=request_header, data=request_body, timeout=5)
result.raise_for_status()
except requests.RequestException:
logger.warning('Failed to upload logs to loki')
class Loki_log_handler(logging.Handler):
'''
A log handler that sends logs to loki.
'''
def __init__(self, url, dry_run):
super().__init__()
self.buffer = Loki_log_buffer(url, dry_run)
def emit(self, record):
'''
Add a log record from the logging module to the stream.
'''
self.raw(record.getMessage())
def add_label(self, key, value):
'''
Add a label to the logging stream.
'''
self.buffer.add_label(key, value)
def raw(self, msg):
'''
Add an arbitrary string as a log entry to the stream.
'''
self.buffer.add_value(msg)
if len(self.buffer) > MAX_BUFFER_LINES:
self.buffer.flush()
def flush(self):
'''
Send the logs to loki and empty the buffer.
'''
self.buffer.flush()
def initialize_monitor(hook_config, config, config_filename, monitoring_log_level, dry_run):
'''
Add a handler to the root logger to regularly send the logs to loki.
'''
url = hook_config.get('url')
loki = Loki_log_handler(url, dry_run)
for key, value in hook_config.get('labels').items():
if value == '__hostname':
loki.add_label(key, platform.node())
elif value == '__config':
loki.add_label(key, os.path.basename(config_filename))
elif value == '__config_path':
loki.add_label(key, config_filename)
else:
loki.add_label(key, value)
logging.getLogger().addHandler(loki)
def ping_monitor(hook_config, config, config_filename, state, monitoring_log_level, dry_run):
'''
Add an entry to the loki logger with the current state.
'''
for handler in tuple(logging.getLogger().handlers):
if isinstance(handler, Loki_log_handler):
if state in MONITOR_STATE_TO_LOKI.keys():
handler.raw(f'{config_filename}: {MONITOR_STATE_TO_LOKI[state]} backup')
def destroy_monitor(hook_config, config, config_filename, monitoring_log_level, dry_run):
'''
Remove the monitor handler that was added to the root logger.
'''
logger = logging.getLogger()
for handler in tuple(logger.handlers):
if isinstance(handler, Loki_log_handler):
handler.flush()
logger.removeHandler(handler)

257
borgmatic/hooks/mariadb.py Normal file
View File

@ -0,0 +1,257 @@
import copy
import logging
import os
import shlex
from borgmatic.execute import (
execute_command,
execute_command_and_capture_output,
execute_command_with_processes,
)
from borgmatic.hooks import dump
logger = logging.getLogger(__name__)
def make_dump_path(config): # pragma: no cover
'''
Make the dump path from the given configuration dict and the name of this hook.
'''
return dump.make_data_source_dump_path(
config.get('borgmatic_source_directory'), 'mariadb_databases'
)
SYSTEM_DATABASE_NAMES = ('information_schema', 'mysql', 'performance_schema', 'sys')
def database_names_to_dump(database, extra_environment, log_prefix, dry_run):
'''
Given a requested database config, return the corresponding sequence of database names to dump.
In the case of "all", query for the names of databases on the configured host and return them,
excluding any system databases that will cause problems during restore.
'''
if database['name'] != 'all':
return (database['name'],)
if dry_run:
return ()
mariadb_show_command = tuple(
shlex.quote(part) for part in shlex.split(database.get('mariadb_command') or 'mariadb')
)
show_command = (
mariadb_show_command
+ (tuple(database['list_options'].split(' ')) if 'list_options' in database else ())
+ (('--host', database['hostname']) if 'hostname' in database else ())
+ (('--port', str(database['port'])) if 'port' in database else ())
+ (('--protocol', 'tcp') if 'hostname' in database or 'port' in database else ())
+ (('--user', database['username']) if 'username' in database else ())
+ ('--skip-column-names', '--batch')
+ ('--execute', 'show schemas')
)
logger.debug(f'{log_prefix}: Querying for "all" MariaDB databases to dump')
show_output = execute_command_and_capture_output(
show_command, extra_environment=extra_environment
)
return tuple(
show_name
for show_name in show_output.strip().splitlines()
if show_name not in SYSTEM_DATABASE_NAMES
)
def execute_dump_command(
database, log_prefix, dump_path, database_names, extra_environment, dry_run, dry_run_label
):
'''
Kick off a dump for the given MariaDB database (provided as a configuration dict) to a named
pipe constructed from the given dump path and database name. Use the given log prefix in any
log entries.
Return a subprocess.Popen instance for the dump process ready to spew to a named pipe. But if
this is a dry run, then don't actually dump anything and return None.
'''
database_name = database['name']
dump_filename = dump.make_data_source_dump_filename(
dump_path, database['name'], database.get('hostname')
)
if os.path.exists(dump_filename):
logger.warning(
f'{log_prefix}: Skipping duplicate dump of MariaDB database "{database_name}" to {dump_filename}'
)
return None
mariadb_dump_command = tuple(
shlex.quote(part)
for part in shlex.split(database.get('mariadb_dump_command') or 'mariadb-dump')
)
dump_command = (
mariadb_dump_command
+ (tuple(database['options'].split(' ')) if 'options' in database else ())
+ (('--add-drop-database',) if database.get('add_drop_database', True) else ())
+ (('--host', database['hostname']) if 'hostname' in database else ())
+ (('--port', str(database['port'])) if 'port' in database else ())
+ (('--protocol', 'tcp') if 'hostname' in database or 'port' in database else ())
+ (('--user', database['username']) if 'username' in database else ())
+ ('--databases',)
+ database_names
+ ('--result-file', dump_filename)
)
logger.debug(
f'{log_prefix}: Dumping MariaDB database "{database_name}" to {dump_filename}{dry_run_label}'
)
if dry_run:
return None
dump.create_named_pipe_for_dump(dump_filename)
return execute_command(
dump_command,
extra_environment=extra_environment,
run_to_completion=False,
)
def use_streaming(databases, config, log_prefix):
'''
Given a sequence of MariaDB database configuration dicts, a configuration dict (ignored), and a
log prefix (ignored), return whether streaming will be using during dumps.
'''
return any(databases)
def dump_data_sources(databases, config, log_prefix, 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.
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.
'''
dry_run_label = ' (dry run; not actually dumping anything)' if dry_run else ''
processes = []
logger.info(f'{log_prefix}: Dumping MariaDB databases{dry_run_label}')
for database in databases:
dump_path = make_dump_path(config)
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
)
if not dump_database_names:
if dry_run:
continue
raise ValueError('Cannot find any MariaDB databases to dump.')
if database['name'] == 'all' and database.get('format'):
for dump_name in dump_database_names:
renamed_database = copy.copy(database)
renamed_database['name'] = dump_name
processes.append(
execute_dump_command(
renamed_database,
log_prefix,
dump_path,
(dump_name,),
extra_environment,
dry_run,
dry_run_label,
)
)
else:
processes.append(
execute_dump_command(
database,
log_prefix,
dump_path,
dump_database_names,
extra_environment,
dry_run,
dry_run_label,
)
)
return [process for process in processes if process]
def remove_data_source_dumps(databases, config, log_prefix, 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.
'''
dump.remove_data_source_dumps(make_dump_path(config), 'MariaDB', log_prefix, dry_run)
def make_data_source_dump_pattern(databases, config, log_prefix, 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.
'''
return dump.make_data_source_dump_filename(make_dump_path(config), name, hostname='*')
def restore_data_source_dump(
hook_config, config, log_prefix, data_source, dry_run, extract_process, connection_params
):
'''
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.
'''
dry_run_label = ' (dry run; not actually restoring anything)' if dry_run else ''
hostname = connection_params['hostname'] or data_source.get(
'restore_hostname', data_source.get('hostname')
)
port = str(
connection_params['port'] or data_source.get('restore_port', data_source.get('port', ''))
)
username = connection_params['username'] or data_source.get(
'restore_username', data_source.get('username')
)
password = connection_params['password'] or data_source.get(
'restore_password', data_source.get('password')
)
mariadb_restore_command = tuple(
shlex.quote(part) for part in shlex.split(data_source.get('mariadb_command') or 'mariadb')
)
restore_command = (
mariadb_restore_command
+ ('--batch',)
+ (
tuple(data_source['restore_options'].split(' '))
if 'restore_options' in data_source
else ()
)
+ (('--host', hostname) if hostname else ())
+ (('--port', str(port)) if port else ())
+ (('--protocol', 'tcp') if hostname or port else ())
+ (('--user', username) if username else ())
)
extra_environment = {'MYSQL_PWD': password} if password else None
logger.debug(f"{log_prefix}: Restoring MariaDB database {data_source['name']}{dry_run_label}")
if dry_run:
return
# Don't give Borg local path so as to error on warnings, as "borg extract" only gives a warning
# if the restore paths don't exist in the archive.
execute_command_with_processes(
restore_command,
[extract_process],
output_log_level=logging.DEBUG,
input_file=extract_process.stdout,
extra_environment=extra_environment,
)

185
borgmatic/hooks/mongodb.py Normal file
View File

@ -0,0 +1,185 @@
import logging
import shlex
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
'''
Make the dump path from the given configuration dict and the name of this hook.
'''
return dump.make_data_source_dump_path(
config.get('borgmatic_source_directory'), 'mongodb_databases'
)
def use_streaming(databases, config, log_prefix):
'''
Given a sequence of MongoDB database configuration dicts, a configuration dict (ignored), and a
log prefix (ignored), return whether streaming will be using during dumps.
'''
return any(database.get('format') != 'directory' for database in databases)
def dump_data_sources(databases, config, log_prefix, 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.
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.
'''
dry_run_label = ' (dry run; not actually dumping anything)' if dry_run else ''
logger.info(f'{log_prefix}: Dumping MongoDB databases{dry_run_label}')
processes = []
for database in databases:
name = database['name']
dump_filename = dump.make_data_source_dump_filename(
make_dump_path(config), name, database.get('hostname')
)
dump_format = database.get('format', 'archive')
logger.debug(
f'{log_prefix}: Dumping MongoDB database {name} to {dump_filename}{dry_run_label}',
)
if dry_run:
continue
command = build_dump_command(database, dump_filename, dump_format)
if dump_format == 'directory':
dump.create_parent_directory_for_dump(dump_filename)
execute_command(command, shell=True)
else:
dump.create_named_pipe_for_dump(dump_filename)
processes.append(execute_command(command, shell=True, run_to_completion=False))
return processes
def build_dump_command(database, dump_filename, dump_format):
'''
Return the mongodump command from a single database configuration.
'''
all_databases = database['name'] == 'all'
return (
('mongodump',)
+ (('--out', shlex.quote(dump_filename)) if dump_format == 'directory' else ())
+ (('--host', shlex.quote(database['hostname'])) if 'hostname' in database else ())
+ (('--port', shlex.quote(str(database['port']))) if 'port' in database else ())
+ (('--username', shlex.quote(database['username'])) if 'username' in database else ())
+ (('--password', shlex.quote(database['password'])) if 'password' in database else ())
+ (
('--authenticationDatabase', shlex.quote(database['authentication_database']))
if 'authentication_database' in database
else ()
)
+ (('--db', shlex.quote(database['name'])) if not all_databases else ())
+ (
tuple(shlex.quote(option) for option in database['options'].split(' '))
if 'options' in database
else ()
)
+ (('--archive', '>', shlex.quote(dump_filename)) if dump_format != 'directory' else ())
)
def remove_data_source_dumps(databases, config, log_prefix, 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.
'''
dump.remove_data_source_dumps(make_dump_path(config), 'MongoDB', log_prefix, dry_run)
def make_data_source_dump_pattern(databases, config, log_prefix, 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.
'''
return dump.make_data_source_dump_filename(make_dump_path(config), name, hostname='*')
def restore_data_source_dump(
hook_config, config, log_prefix, data_source, dry_run, extract_process, connection_params
):
'''
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.
If the extract process is None, then restore the dump from the filesystem rather than from an
extract stream.
'''
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')
)
restore_command = build_restore_command(
extract_process, data_source, dump_filename, connection_params
)
logger.debug(f"{log_prefix}: Restoring MongoDB database {data_source['name']}{dry_run_label}")
if dry_run:
return
# Don't give Borg local path so as to error on warnings, as "borg extract" only gives a warning
# if the restore paths don't exist in the archive.
execute_command_with_processes(
restore_command,
[extract_process] if extract_process else [],
output_log_level=logging.DEBUG,
input_file=extract_process.stdout if extract_process else None,
)
def build_restore_command(extract_process, database, dump_filename, connection_params):
'''
Return the mongorestore command from a single database configuration.
'''
hostname = connection_params['hostname'] or database.get(
'restore_hostname', database.get('hostname')
)
port = str(connection_params['port'] or database.get('restore_port', database.get('port', '')))
username = connection_params['username'] or database.get(
'restore_username', database.get('username')
)
password = connection_params['password'] or database.get(
'restore_password', database.get('password')
)
command = ['mongorestore']
if extract_process:
command.append('--archive')
else:
command.extend(('--dir', dump_filename))
if database['name'] != 'all':
command.extend(('--drop',))
if hostname:
command.extend(('--host', hostname))
if port:
command.extend(('--port', str(port)))
if username:
command.extend(('--username', username))
if password:
command.extend(('--password', password))
if 'authentication_database' in database:
command.extend(('--authenticationDatabase', database['authentication_database']))
if 'restore_options' in database:
command.extend(database['restore_options'].split(' '))
if database.get('schemas'):
for schema in database['schemas']:
command.extend(('--nsInclude', schema))
return command

View File

@ -0,0 +1,10 @@
from enum import Enum
MONITOR_HOOK_NAMES = ('apprise', 'healthchecks', 'cronitor', 'cronhub', 'pagerduty', 'ntfy', 'loki')
class State(Enum):
START = 1
FINISH = 2
FAIL = 3
LOG = 4

255
borgmatic/hooks/mysql.py Normal file
View File

@ -0,0 +1,255 @@
import copy
import logging
import os
import shlex
from borgmatic.execute import (
execute_command,
execute_command_and_capture_output,
execute_command_with_processes,
)
from borgmatic.hooks import dump
logger = logging.getLogger(__name__)
def make_dump_path(config): # pragma: no cover
'''
Make the dump path from the given configuration dict and the name of this hook.
'''
return dump.make_data_source_dump_path(
config.get('borgmatic_source_directory'), 'mysql_databases'
)
SYSTEM_DATABASE_NAMES = ('information_schema', 'mysql', 'performance_schema', 'sys')
def database_names_to_dump(database, extra_environment, log_prefix, dry_run):
'''
Given a requested database config, return the corresponding sequence of database names to dump.
In the case of "all", query for the names of databases on the configured host and return them,
excluding any system databases that will cause problems during restore.
'''
if database['name'] != 'all':
return (database['name'],)
if dry_run:
return ()
mysql_show_command = tuple(
shlex.quote(part) for part in shlex.split(database.get('mysql_command') or 'mysql')
)
show_command = (
mysql_show_command
+ (tuple(database['list_options'].split(' ')) if 'list_options' in database else ())
+ (('--host', database['hostname']) if 'hostname' in database else ())
+ (('--port', str(database['port'])) if 'port' in database else ())
+ (('--protocol', 'tcp') if 'hostname' in database or 'port' in database else ())
+ (('--user', database['username']) if 'username' in database else ())
+ ('--skip-column-names', '--batch')
+ ('--execute', 'show schemas')
)
logger.debug(f'{log_prefix}: Querying for "all" MySQL databases to dump')
show_output = execute_command_and_capture_output(
show_command, extra_environment=extra_environment
)
return tuple(
show_name
for show_name in show_output.strip().splitlines()
if show_name not in SYSTEM_DATABASE_NAMES
)
def execute_dump_command(
database, log_prefix, dump_path, database_names, extra_environment, dry_run, dry_run_label
):
'''
Kick off a dump for the given MySQL/MariaDB database (provided as a configuration dict) to a
named pipe constructed from the given dump path and database name. Use the given log prefix in
any log entries.
Return a subprocess.Popen instance for the dump process ready to spew to a named pipe. But if
this is a dry run, then don't actually dump anything and return None.
'''
database_name = database['name']
dump_filename = dump.make_data_source_dump_filename(
dump_path, database['name'], database.get('hostname')
)
if os.path.exists(dump_filename):
logger.warning(
f'{log_prefix}: Skipping duplicate dump of MySQL database "{database_name}" to {dump_filename}'
)
return None
mysql_dump_command = tuple(
shlex.quote(part) for part in shlex.split(database.get('mysql_dump_command') or 'mysqldump')
)
dump_command = (
mysql_dump_command
+ (tuple(database['options'].split(' ')) if 'options' in database else ())
+ (('--add-drop-database',) if database.get('add_drop_database', True) else ())
+ (('--host', database['hostname']) if 'hostname' in database else ())
+ (('--port', str(database['port'])) if 'port' in database else ())
+ (('--protocol', 'tcp') if 'hostname' in database or 'port' in database else ())
+ (('--user', database['username']) if 'username' in database else ())
+ ('--databases',)
+ database_names
+ ('--result-file', dump_filename)
)
logger.debug(
f'{log_prefix}: Dumping MySQL database "{database_name}" to {dump_filename}{dry_run_label}'
)
if dry_run:
return None
dump.create_named_pipe_for_dump(dump_filename)
return execute_command(
dump_command,
extra_environment=extra_environment,
run_to_completion=False,
)
def use_streaming(databases, config, log_prefix):
'''
Given a sequence of MySQL database configuration dicts, a configuration dict (ignored), and a
log prefix (ignored), return whether streaming will be using during dumps.
'''
return any(databases)
def dump_data_sources(databases, config, log_prefix, 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.
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.
'''
dry_run_label = ' (dry run; not actually dumping anything)' if dry_run else ''
processes = []
logger.info(f'{log_prefix}: Dumping MySQL databases{dry_run_label}')
for database in databases:
dump_path = make_dump_path(config)
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
)
if not dump_database_names:
if dry_run:
continue
raise ValueError('Cannot find any MySQL databases to dump.')
if database['name'] == 'all' and database.get('format'):
for dump_name in dump_database_names:
renamed_database = copy.copy(database)
renamed_database['name'] = dump_name
processes.append(
execute_dump_command(
renamed_database,
log_prefix,
dump_path,
(dump_name,),
extra_environment,
dry_run,
dry_run_label,
)
)
else:
processes.append(
execute_dump_command(
database,
log_prefix,
dump_path,
dump_database_names,
extra_environment,
dry_run,
dry_run_label,
)
)
return [process for process in processes if process]
def remove_data_source_dumps(databases, config, log_prefix, 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.
'''
dump.remove_data_source_dumps(make_dump_path(config), 'MySQL', log_prefix, dry_run)
def make_data_source_dump_pattern(databases, config, log_prefix, 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.
'''
return dump.make_data_source_dump_filename(make_dump_path(config), name, hostname='*')
def restore_data_source_dump(
hook_config, config, log_prefix, data_source, dry_run, extract_process, connection_params
):
'''
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.
'''
dry_run_label = ' (dry run; not actually restoring anything)' if dry_run else ''
hostname = connection_params['hostname'] or data_source.get(
'restore_hostname', data_source.get('hostname')
)
port = str(
connection_params['port'] or data_source.get('restore_port', data_source.get('port', ''))
)
username = connection_params['username'] or data_source.get(
'restore_username', data_source.get('username')
)
password = connection_params['password'] or data_source.get(
'restore_password', data_source.get('password')
)
mysql_restore_command = tuple(
shlex.quote(part) for part in shlex.split(data_source.get('mysql_command') or 'mysql')
)
restore_command = (
mysql_restore_command
+ ('--batch',)
+ (
tuple(data_source['restore_options'].split(' '))
if 'restore_options' in data_source
else ()
)
+ (('--host', hostname) if hostname else ())
+ (('--port', str(port)) if port else ())
+ (('--protocol', 'tcp') if hostname or port else ())
+ (('--user', username) if username else ())
)
extra_environment = {'MYSQL_PWD': password} if password else None
logger.debug(f"{log_prefix}: Restoring MySQL database {data_source['name']}{dry_run_label}")
if dry_run:
return
# Don't give Borg local path so as to error on warnings, as "borg extract" only gives a warning
# if the restore paths don't exist in the archive.
execute_command_with_processes(
restore_command,
[extract_process],
output_log_level=logging.DEBUG,
input_file=extract_process.stdout,
extra_environment=extra_environment,
)

90
borgmatic/hooks/ntfy.py Normal file
View File

@ -0,0 +1,90 @@
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):
'''
Ping the configured Ntfy topic. Use the given configuration filename in any log entries.
If this is a dry run, then don't actually ping anything.
'''
run_states = hook_config.get('states', ['fail'])
if state.name.lower() in run_states:
dry_run_label = ' (dry run; not actually pinging)' if dry_run else ''
state_config = hook_config.get(
state.name.lower(),
{
'title': f'A borgmatic {state.name} event happened',
'message': f'A borgmatic {state.name} event happened',
'priority': 'default',
'tags': 'borgmatic',
},
)
base_url = hook_config.get('server', 'https://ntfy.sh')
topic = hook_config.get('topic')
logger.info(f'{config_filename}: Pinging ntfy topic {topic}{dry_run_label}')
logger.debug(f'{config_filename}: Using Ntfy ping URL {base_url}/{topic}')
headers = {
'X-Title': state_config.get('title'),
'X-Message': state_config.get('message'),
'X-Priority': state_config.get('priority'),
'X-Tags': state_config.get('tags'),
}
username = hook_config.get('username')
password = hook_config.get('password')
access_token = hook_config.get('access_token')
auth = None
if access_token is not None:
if username or password:
logger.warning(
f'{config_filename}: ntfy access_token is set but so is username/password, only using access_token'
)
auth = requests.auth.HTTPBasicAuth('', access_token)
elif (username and password) is not None:
auth = requests.auth.HTTPBasicAuth(username, password)
logger.info(f'{config_filename}: Using basic auth with user {username} for ntfy')
elif username is not None:
logger.warning(
f'{config_filename}: Password missing for ntfy authentication, defaulting to no auth'
)
elif password is not None:
logger.warning(
f'{config_filename}: Username missing for ntfy authentication, defaulting to no auth'
)
if not dry_run:
logging.getLogger('urllib3').setLevel(logging.ERROR)
try:
response = requests.post(f'{base_url}/{topic}', headers=headers, auth=auth)
if not response.ok:
response.raise_for_status()
except requests.exceptions.RequestException as error:
logger.warning(f'{config_filename}: ntfy 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

View File

@ -0,0 +1,83 @@
import datetime
import json
import logging
import platform
import requests
from borgmatic.hooks import monitor
logger = logging.getLogger(__name__)
EVENTS_API_URL = 'https://events.pagerduty.com/v2/enqueue'
def initialize_monitor(
integration_key, 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):
'''
If this is an error state, create a PagerDuty event with the configured integration key. Use
the given configuration filename in any log entries. If this is a dry run, then don't actually
create an event.
'''
if state != monitor.State.FAIL:
logger.debug(
f'{config_filename}: Ignoring unsupported monitoring {state.name.lower()} in PagerDuty hook',
)
return
dry_run_label = ' (dry run; not actually sending)' if dry_run else ''
logger.info(f'{config_filename}: Sending failure event to PagerDuty {dry_run_label}')
if dry_run:
return
hostname = platform.node()
local_timestamp = (
datetime.datetime.utcnow().replace(tzinfo=datetime.timezone.utc).astimezone().isoformat()
)
payload = json.dumps(
{
'routing_key': hook_config['integration_key'],
'event_action': 'trigger',
'payload': {
'summary': f'backup failed on {hostname}',
'severity': 'error',
'source': hostname,
'timestamp': local_timestamp,
'component': 'borgmatic',
'group': 'backups',
'class': 'backup failure',
'custom_details': {
'hostname': hostname,
'configuration filename': config_filename,
'server time': local_timestamp,
},
},
}
)
logger.debug(f'{config_filename}: Using PagerDuty payload: {payload}')
logging.getLogger('urllib3').setLevel(logging.ERROR)
try:
response = requests.post(EVENTS_API_URL, data=payload.encode('utf-8'))
if not response.ok:
response.raise_for_status()
except requests.exceptions.RequestException as error:
logger.warning(f'{config_filename}: PagerDuty 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

View File

@ -1,184 +1,321 @@
import glob
import csv
import itertools
import logging
import os
import shlex
from borgmatic.execute import execute_command
from borgmatic.execute import (
execute_command,
execute_command_and_capture_output,
execute_command_with_processes,
)
from borgmatic.hooks import dump
DUMP_PATH = '~/.borgmatic/postgresql_databases'
logger = logging.getLogger(__name__)
def make_database_dump_filename(name, hostname=None):
def make_dump_path(config): # pragma: no cover
'''
Based on the given database name and hostname, return a filename to use for the database dump.
Raise ValueError if the database name is invalid.
Make the dump path from the given configuration dict and the name of this hook.
'''
if os.path.sep in name:
raise ValueError('Invalid database name {}'.format(name))
return os.path.join(os.path.expanduser(DUMP_PATH), hostname or 'localhost', name)
return dump.make_data_source_dump_path(
config.get('borgmatic_source_directory'), 'postgresql_databases'
)
def dump_databases(databases, log_prefix, dry_run):
def make_extra_environment(database, restore_connection_params=None):
'''
Dump the given PostgreSQL databases to disk. The databases are supplied as a sequence of dicts,
one dict describing each database as per the configuration schema. Use the given log prefix in
any log entries. If this is a dry run, then don't actually dump anything.
Make the extra_environment dict from the given database configuration. If restore connection
params are given, this is for a restore operation.
'''
if not databases:
logger.debug('{}: No PostgreSQL databases configured'.format(log_prefix))
return
extra = dict()
try:
if restore_connection_params:
extra['PGPASSWORD'] = restore_connection_params.get('password') or database.get(
'restore_password', database['password']
)
else:
extra['PGPASSWORD'] = database['password']
except (AttributeError, KeyError):
pass
if 'ssl_mode' in database:
extra['PGSSLMODE'] = database['ssl_mode']
if 'ssl_cert' in database:
extra['PGSSLCERT'] = database['ssl_cert']
if 'ssl_key' in database:
extra['PGSSLKEY'] = database['ssl_key']
if 'ssl_root_cert' in database:
extra['PGSSLROOTCERT'] = database['ssl_root_cert']
if 'ssl_crl' in database:
extra['PGSSLCRL'] = database['ssl_crl']
return extra
EXCLUDED_DATABASE_NAMES = ('template0', 'template1')
def database_names_to_dump(database, extra_environment, log_prefix, dry_run):
'''
Given a requested database config, return the corresponding sequence of database names to dump.
In the case of "all" when a database format is given, query for the names of databases on the
configured host and return them. For "all" without a database format, just return a sequence
containing "all".
'''
requested_name = database['name']
if requested_name != 'all':
return (requested_name,)
if not database.get('format'):
return ('all',)
if dry_run:
return ()
psql_command = tuple(
shlex.quote(part) for part in shlex.split(database.get('psql_command') or 'psql')
)
list_command = (
psql_command
+ ('--list', '--no-password', '--no-psqlrc', '--csv', '--tuples-only')
+ (('--host', database['hostname']) if 'hostname' in database else ())
+ (('--port', str(database['port'])) if 'port' in database else ())
+ (('--username', database['username']) if 'username' in database else ())
+ (tuple(database['list_options'].split(' ')) if 'list_options' in database else ())
)
logger.debug(f'{log_prefix}: Querying for "all" PostgreSQL databases to dump')
list_output = execute_command_and_capture_output(
list_command, extra_environment=extra_environment
)
return tuple(
row[0]
for row in csv.reader(list_output.splitlines(), delimiter=',', quotechar='"')
if row[0] not in EXCLUDED_DATABASE_NAMES
)
def use_streaming(databases, config, log_prefix):
'''
Given a sequence of PostgreSQL database configuration dicts, a configuration dict (ignored), and
a log prefix (ignored), return whether streaming will be using during dumps.
'''
return any(database.get('format') != 'directory' for database in databases)
def dump_data_sources(databases, config, log_prefix, 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.
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.
Raise ValueError if the databases to dump cannot be determined.
'''
dry_run_label = ' (dry run; not actually dumping anything)' if dry_run else ''
processes = []
logger.info('{}: Dumping PostgreSQL databases{}'.format(log_prefix, dry_run_label))
logger.info(f'{log_prefix}: Dumping PostgreSQL databases{dry_run_label}')
for database in databases:
name = database['name']
dump_filename = make_database_dump_filename(name, database.get('hostname'))
all_databases = bool(name == 'all')
command = (
('pg_dumpall' if all_databases else 'pg_dump', '--no-password', '--clean')
+ ('--file', dump_filename)
+ (('--host', database['hostname']) if 'hostname' in database else ())
+ (('--port', str(database['port'])) if 'port' in database else ())
+ (('--username', database['username']) if 'username' in database else ())
+ (() if all_databases else ('--format', database.get('format', 'custom')))
+ (tuple(database['options'].split(' ')) if 'options' in database else ())
+ (() if all_databases else (name,))
extra_environment = make_extra_environment(database)
dump_path = make_dump_path(config)
dump_database_names = database_names_to_dump(
database, extra_environment, log_prefix, dry_run
)
extra_environment = {'PGPASSWORD': database['password']} if 'password' in database else None
logger.debug('{}: Dumping PostgreSQL database {}{}'.format(log_prefix, name, dry_run_label))
if not dry_run:
os.makedirs(os.path.dirname(dump_filename), mode=0o700, exist_ok=True)
execute_command(command, extra_environment=extra_environment)
if not dump_database_names:
if dry_run:
continue
raise ValueError('Cannot find any PostgreSQL databases to dump.')
def remove_database_dumps(databases, log_prefix, dry_run):
'''
Remove the database dumps for the given databases. The databases are supplied as a sequence of
dicts, one dict describing each database as per the configuration schema. Use the log prefix in
any log entries. If this is a dry run, then don't actually remove anything.
'''
if not databases:
logger.debug('{}: No PostgreSQL databases configured'.format(log_prefix))
return
dry_run_label = ' (dry run; not actually removing anything)' if dry_run else ''
logger.info('{}: Removing PostgreSQL database dumps{}'.format(log_prefix, dry_run_label))
for database in databases:
dump_filename = make_database_dump_filename(database['name'], database.get('hostname'))
logger.debug(
'{}: Removing PostgreSQL database dump {} from {}{}'.format(
log_prefix, database['name'], dump_filename, dry_run_label
for database_name in dump_database_names:
dump_format = database.get('format', None if database_name == 'all' else 'custom')
default_dump_command = 'pg_dumpall' if database_name == 'all' else 'pg_dump'
dump_command = tuple(
shlex.quote(part)
for part in shlex.split(database.get('pg_dump_command') or default_dump_command)
)
)
if dry_run:
continue
os.remove(dump_filename)
dump_path = os.path.dirname(dump_filename)
if len(os.listdir(dump_path)) == 0:
os.rmdir(dump_path)
def make_database_dump_patterns(names):
'''
Given a sequence of database names, return the corresponding glob patterns to match the database
dumps in an archive. An empty sequence of names indicates that the patterns should match all
dumps.
'''
return [make_database_dump_filename(name, hostname='*') for name in (names or ['*'])]
def convert_glob_patterns_to_borg_patterns(patterns):
'''
Convert a sequence of shell glob patterns like "/etc/*" to the corresponding Borg archive
patterns like "sh:etc/*".
'''
return ['sh:{}'.format(pattern.lstrip(os.path.sep)) for pattern in patterns]
def get_database_names_from_dumps(patterns):
'''
Given a sequence of database dump patterns, find the corresponding database dumps on disk and
return the database names from their filenames.
'''
return [os.path.basename(dump_path) for pattern in patterns for dump_path in glob.glob(pattern)]
def get_database_configurations(databases, names):
'''
Given the full database configuration dicts as per the configuration schema, and a sequence of
database names, filter down and yield the configuration for just the named databases.
Additionally, if a database configuration is named "all", project out that configuration for
each named database.
Raise ValueError if one of the database names cannot be matched to a database in borgmatic's
database configuration.
'''
named_databases = {database['name']: database for database in databases}
for name in names:
database = named_databases.get(name)
if database:
yield database
continue
if 'all' in named_databases:
yield {**named_databases['all'], **{'name': name}}
continue
raise ValueError(
'Cannot restore database "{}", as it is not defined in borgmatic\'s configuration'.format(
name
dump_filename = dump.make_data_source_dump_filename(
dump_path, database_name, database.get('hostname')
)
)
if os.path.exists(dump_filename):
logger.warning(
f'{log_prefix}: Skipping duplicate dump of PostgreSQL database "{database_name}" to {dump_filename}'
)
continue
command = (
dump_command
+ (
'--no-password',
'--clean',
'--if-exists',
)
+ (('--host', shlex.quote(database['hostname'])) if 'hostname' in database else ())
+ (('--port', shlex.quote(str(database['port']))) if 'port' in database else ())
+ (
('--username', shlex.quote(database['username']))
if 'username' in database
else ()
)
+ (('--no-owner',) if database.get('no_owner', False) else ())
+ (('--format', shlex.quote(dump_format)) if dump_format else ())
+ (('--file', shlex.quote(dump_filename)) if dump_format == 'directory' else ())
+ (
tuple(shlex.quote(option) for option in database['options'].split(' '))
if 'options' in database
else ()
)
+ (() if database_name == 'all' else (shlex.quote(database_name),))
# Use shell redirection rather than the --file flag to sidestep synchronization issues
# when pg_dump/pg_dumpall tries to write to a named pipe. But for the directory dump
# format in a particular, a named destination is required, and redirection doesn't work.
+ (('>', shlex.quote(dump_filename)) if dump_format != 'directory' else ())
)
logger.debug(
f'{log_prefix}: Dumping PostgreSQL database "{database_name}" to {dump_filename}{dry_run_label}'
)
if dry_run:
continue
if dump_format == 'directory':
dump.create_parent_directory_for_dump(dump_filename)
execute_command(
command,
shell=True,
extra_environment=extra_environment,
)
else:
dump.create_named_pipe_for_dump(dump_filename)
processes.append(
execute_command(
command,
shell=True,
extra_environment=extra_environment,
run_to_completion=False,
)
)
return processes
def restore_database_dumps(databases, log_prefix, dry_run):
def remove_data_source_dumps(databases, config, log_prefix, dry_run): # pragma: no cover
'''
Restore the given PostgreSQL databases from disk. The databases are supplied as a sequence of
dicts, one dict describing each database as per the configuration schema. Use the given log
prefix in any log entries. If this is a dry run, then don't actually restore anything.
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.
'''
if not databases:
logger.debug('{}: No PostgreSQL databases configured'.format(log_prefix))
return
dump.remove_data_source_dumps(make_dump_path(config), 'PostgreSQL', log_prefix, dry_run)
def make_data_source_dump_pattern(databases, config, log_prefix, 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.
'''
return dump.make_data_source_dump_filename(make_dump_path(config), name, hostname='*')
def restore_data_source_dump(
hook_config, config, log_prefix, data_source, dry_run, extract_process, connection_params
):
'''
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.
If the extract process is None, then restore the dump from the filesystem rather than from an
extract stream.
Use the given connection parameters to connect to the database. The connection parameters are
hostname, port, username, and password.
'''
dry_run_label = ' (dry run; not actually restoring anything)' if dry_run else ''
hostname = connection_params['hostname'] or data_source.get(
'restore_hostname', data_source.get('hostname')
)
port = str(
connection_params['port'] or data_source.get('restore_port', data_source.get('port', ''))
)
username = connection_params['username'] or data_source.get(
'restore_username', data_source.get('username')
)
for database in databases:
dump_filename = make_database_dump_filename(database['name'], database.get('hostname'))
restore_command = (
('pg_restore', '--no-password', '--clean', '--if-exists', '--exit-on-error')
+ (('--host', database['hostname']) if 'hostname' in database else ())
+ (('--port', str(database['port'])) if 'port' in database else ())
+ (('--username', database['username']) if 'username' in database else ())
+ ('--dbname', database['name'])
+ (dump_filename,)
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')
)
psql_command = tuple(
shlex.quote(part) for part in shlex.split(data_source.get('psql_command') or 'psql')
)
analyze_command = (
psql_command
+ ('--no-password', '--no-psqlrc', '--quiet')
+ (('--host', hostname) if hostname else ())
+ (('--port', port) if port else ())
+ (('--username', username) if username else ())
+ (('--dbname', data_source['name']) if not all_databases else ())
+ (
tuple(data_source['analyze_options'].split(' '))
if 'analyze_options' in data_source
else ()
)
extra_environment = {'PGPASSWORD': database['password']} if 'password' in database else None
analyze_command = (
('psql', '--no-password', '--quiet')
+ (('--host', database['hostname']) if 'hostname' in database else ())
+ (('--port', str(database['port'])) if 'port' in database else ())
+ (('--username', database['username']) if 'username' in database else ())
+ ('--dbname', database['name'])
+ ('--command', 'ANALYZE')
+ ('--command', 'ANALYZE')
)
use_psql_command = all_databases or data_source.get('format') == 'plain'
pg_restore_command = tuple(
shlex.quote(part)
for part in shlex.split(data_source.get('pg_restore_command') or 'pg_restore')
)
restore_command = (
(psql_command if use_psql_command else pg_restore_command)
+ ('--no-password',)
+ (('--no-psqlrc',) if use_psql_command else ('--if-exists', '--exit-on-error', '--clean'))
+ (('--dbname', data_source['name']) if not all_databases else ())
+ (('--host', hostname) if hostname else ())
+ (('--port', port) if port else ())
+ (('--username', username) if username else ())
+ (('--no-owner',) if data_source.get('no_owner', False) else ())
+ (
tuple(data_source['restore_options'].split(' '))
if 'restore_options' in data_source
else ()
)
+ (() if extract_process else (dump_filename,))
+ tuple(
itertools.chain.from_iterable(('--schema', schema) for schema in data_source['schemas'])
if data_source.get('schemas')
else ()
)
)
logger.debug(
'{}: Restoring PostgreSQL database {}{}'.format(
log_prefix, database['name'], dry_run_label
)
)
if not dry_run:
execute_command(restore_command, extra_environment=extra_environment)
execute_command(analyze_command, extra_environment=extra_environment)
extra_environment = make_extra_environment(
data_source, restore_connection_params=connection_params
)
logger.debug(
f"{log_prefix}: Restoring PostgreSQL database {data_source['name']}{dry_run_label}"
)
if dry_run:
return
# Don't give Borg local path so as to error on warnings, as "borg extract" only gives a warning
# if the restore paths don't exist in the archive.
execute_command_with_processes(
restore_command,
[extract_process] if extract_process else [],
output_log_level=logging.DEBUG,
input_file=extract_process.stdout if extract_process else None,
extra_environment=extra_environment,
)
execute_command(analyze_command, extra_environment=extra_environment)

135
borgmatic/hooks/sqlite.py Normal file
View File

@ -0,0 +1,135 @@
import logging
import os
import shlex
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
'''
Make the dump path from the given configuration dict and the name of this hook.
'''
return dump.make_data_source_dump_path(
config.get('borgmatic_source_directory'), 'sqlite_databases'
)
def use_streaming(databases, config, log_prefix):
'''
Given a sequence of SQLite database configuration dicts, a configuration dict (ignored), and a
log prefix (ignored), return whether streaming will be using during dumps.
'''
return any(databases)
def dump_data_sources(databases, config, log_prefix, 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.
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.
'''
dry_run_label = ' (dry run; not actually dumping anything)' if dry_run else ''
processes = []
logger.info(f'{log_prefix}: Dumping SQLite databases{dry_run_label}')
for database in databases:
database_path = database['path']
if database['name'] == 'all':
logger.warning('The "all" database name has no meaning for SQLite databases')
if not os.path.exists(database_path):
logger.warning(
f'{log_prefix}: No SQLite database at {database_path}; an empty database will be created and dumped'
)
dump_path = make_dump_path(config)
dump_filename = dump.make_data_source_dump_filename(dump_path, database['name'])
if os.path.exists(dump_filename):
logger.warning(
f'{log_prefix}: Skipping duplicate dump of SQLite database at {database_path} to {dump_filename}'
)
continue
command = (
'sqlite3',
shlex.quote(database_path),
'.dump',
'>',
shlex.quote(dump_filename),
)
logger.debug(
f'{log_prefix}: Dumping SQLite database at {database_path} to {dump_filename}{dry_run_label}'
)
if dry_run:
continue
dump.create_named_pipe_for_dump(dump_filename)
processes.append(execute_command(command, shell=True, run_to_completion=False))
return processes
def remove_data_source_dumps(databases, config, log_prefix, 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.
'''
dump.remove_data_source_dumps(make_dump_path(config), 'SQLite', log_prefix, dry_run)
def make_data_source_dump_pattern(databases, config, log_prefix, 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.
'''
return dump.make_data_source_dump_filename(make_dump_path(config), name)
def restore_data_source_dump(
hook_config, config, log_prefix, data_source, dry_run, extract_process, connection_params
):
'''
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.
'''
dry_run_label = ' (dry run; not actually restoring anything)' if dry_run else ''
database_path = connection_params['restore_path'] or data_source.get(
'restore_path', data_source.get('path')
)
logger.debug(f'{log_prefix}: Restoring SQLite database at {database_path}{dry_run_label}')
if dry_run:
return
try:
os.remove(database_path)
logger.warning(f'{log_prefix}: Removed existing SQLite database at {database_path}')
except FileNotFoundError: # pragma: no cover
pass
restore_command = (
'sqlite3',
database_path,
)
# Don't give Borg local path so as to error on warnings, as "borg extract" only gives a warning
# if the restore paths don't exist in the archive.
execute_command_with_processes(
restore_command,
[extract_process],
output_log_level=logging.DEBUG,
input_file=extract_process.stdout,
)

View File

@ -1,4 +1,5 @@
import logging
import logging.handlers
import os
import sys
@ -26,7 +27,7 @@ def interactive_console():
Return whether the current console is "interactive". Meaning: Capable of
user input and not just something like a cron job.
'''
return sys.stdout.isatty() and os.environ.get('TERM') != 'dumb'
return sys.stderr.isatty() and os.environ.get('TERM') != 'dumb'
def should_do_markup(no_color, configs):
@ -40,6 +41,9 @@ def should_do_markup(no_color, configs):
if any(config.get('output', {}).get('color') is False for config in configs.values()):
return False
if os.environ.get('NO_COLOR', None):
return False
py_colors = os.environ.get('PY_COLORS', None)
if py_colors is not None:
@ -48,18 +52,55 @@ def should_do_markup(no_color, configs):
return interactive_console()
LOG_LEVEL_TO_COLOR = {
logging.CRITICAL: colorama.Fore.RED,
logging.ERROR: colorama.Fore.RED,
logging.WARN: colorama.Fore.YELLOW,
logging.INFO: colorama.Fore.GREEN,
logging.DEBUG: colorama.Fore.CYAN,
}
class Multi_stream_handler(logging.Handler):
'''
A logging handler that dispatches each log record to one of multiple stream handlers depending
on the record's log level.
'''
def __init__(self, log_level_to_stream_handler):
super(Multi_stream_handler, self).__init__()
self.log_level_to_handler = log_level_to_stream_handler
self.handlers = set(self.log_level_to_handler.values())
def flush(self): # pragma: no cover
super(Multi_stream_handler, self).flush()
for handler in self.handlers:
handler.flush()
def emit(self, record):
'''
Dispatch the log record to the appropriate stream handler for the record's log level.
'''
self.log_level_to_handler[record.levelno].emit(record)
def setFormatter(self, formatter): # pragma: no cover
super(Multi_stream_handler, self).setFormatter(formatter)
for handler in self.handlers:
handler.setFormatter(formatter)
def setLevel(self, level): # pragma: no cover
super(Multi_stream_handler, self).setLevel(level)
for handler in self.handlers:
handler.setLevel(level)
class Console_color_formatter(logging.Formatter):
def format(self, record):
color = LOG_LEVEL_TO_COLOR.get(record.levelno)
add_custom_log_levels()
color = {
logging.CRITICAL: colorama.Fore.RED,
logging.ERROR: colorama.Fore.RED,
logging.WARN: colorama.Fore.YELLOW,
logging.ANSWER: colorama.Fore.MAGENTA,
logging.INFO: colorama.Fore.GREEN,
logging.DEBUG: colorama.Fore.CYAN,
}.get(record.levelno)
return color_text(color, record.msg)
@ -70,32 +111,127 @@ def color_text(color, message):
if not color:
return message
return '{}{}{}'.format(color, message, colorama.Style.RESET_ALL)
return f'{color}{message}{colorama.Style.RESET_ALL}'
def configure_logging(console_log_level, syslog_log_level=None):
def add_logging_level(level_name, level_number):
'''
Configure logging to go to both the console and syslog. Use the given log levels, respectively.
Globally add a custom logging level based on the given (all uppercase) level name and number.
Do this idempotently.
Inspired by https://stackoverflow.com/questions/2183233/how-to-add-a-custom-loglevel-to-pythons-logging-facility/35804945#35804945
'''
method_name = level_name.lower()
if not hasattr(logging, level_name):
logging.addLevelName(level_number, level_name)
setattr(logging, level_name, level_number)
if not hasattr(logging, method_name):
def log_for_level(self, message, *args, **kwargs): # pragma: no cover
if self.isEnabledFor(level_number):
self._log(level_number, message, args, **kwargs)
setattr(logging.getLoggerClass(), method_name, log_for_level)
if not hasattr(logging.getLoggerClass(), method_name):
def log_to_root(message, *args, **kwargs): # pragma: no cover
logging.log(level_number, message, *args, **kwargs)
setattr(logging, method_name, log_to_root)
ANSWER = logging.WARN - 5
DISABLED = logging.CRITICAL + 10
def add_custom_log_levels(): # pragma: no cover
'''
Add a custom log level between WARN and INFO for user-requested answers.
'''
add_logging_level('ANSWER', ANSWER)
add_logging_level('DISABLED', DISABLED)
def configure_logging(
console_log_level,
syslog_log_level=None,
log_file_log_level=None,
monitoring_log_level=None,
log_file=None,
log_file_format=None,
color_enabled=True,
):
'''
Configure logging to go to both the console and (syslog or log file). Use the given log levels,
respectively. If color is enabled, set up log formatting accordingly.
Raise FileNotFoundError or PermissionError if the log file could not be opened for writing.
'''
add_custom_log_levels()
if syslog_log_level is None:
syslog_log_level = console_log_level
syslog_log_level = logging.DISABLED
if log_file_log_level is None:
log_file_log_level = console_log_level
if monitoring_log_level is None:
monitoring_log_level = console_log_level
# Log certain log levels to console stderr and others to stdout. This supports use cases like
# grepping (non-error) output.
console_disabled = logging.NullHandler()
console_error_handler = logging.StreamHandler(sys.stderr)
console_standard_handler = logging.StreamHandler(sys.stdout)
console_handler = Multi_stream_handler(
{
logging.DISABLED: console_disabled,
logging.CRITICAL: console_error_handler,
logging.ERROR: console_error_handler,
logging.WARN: console_error_handler,
logging.ANSWER: console_standard_handler,
logging.INFO: console_standard_handler,
logging.DEBUG: console_standard_handler,
}
)
if color_enabled:
console_handler.setFormatter(Console_color_formatter())
console_handler = logging.StreamHandler()
console_handler.setFormatter(Console_color_formatter())
console_handler.setLevel(console_log_level)
syslog_path = None
if os.path.exists('/dev/log'):
syslog_path = '/dev/log'
elif os.path.exists('/var/run/syslog'):
syslog_path = '/var/run/syslog'
handlers = [console_handler]
if syslog_path and not interactive_console():
syslog_handler = logging.handlers.SysLogHandler(address=syslog_path)
syslog_handler.setFormatter(logging.Formatter('borgmatic: %(levelname)s %(message)s'))
syslog_handler.setLevel(syslog_log_level)
handlers = (console_handler, syslog_handler)
else:
handlers = (console_handler,)
if syslog_log_level != logging.DISABLED:
syslog_path = None
logging.basicConfig(level=min(console_log_level, syslog_log_level), handlers=handlers)
if os.path.exists('/dev/log'):
syslog_path = '/dev/log'
elif os.path.exists('/var/run/syslog'):
syslog_path = '/var/run/syslog'
elif os.path.exists('/var/run/log'):
syslog_path = '/var/run/log'
if syslog_path:
syslog_handler = logging.handlers.SysLogHandler(address=syslog_path)
syslog_handler.setFormatter(
logging.Formatter('borgmatic: {levelname} {message}', style='{') # noqa: FS003
)
syslog_handler.setLevel(syslog_log_level)
handlers.append(syslog_handler)
if log_file and log_file_log_level != logging.DISABLED:
file_handler = logging.handlers.WatchedFileHandler(log_file)
file_handler.setFormatter(
logging.Formatter(
log_file_format or '[{asctime}] {levelname}: {message}', style='{' # noqa: FS003
)
)
file_handler.setLevel(log_file_log_level)
handlers.append(file_handler)
logging.basicConfig(
level=min(handler.level for handler in handlers),
handlers=handlers,
)

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